diff --git a/packages/kbot/dist-in/async-iterator.d.ts b/packages/kbot/dist-in/async-iterator.d.ts index 4e1c1b17..e7cd1596 100644 --- a/packages/kbot/dist-in/async-iterator.d.ts +++ b/packages/kbot/dist-in/async-iterator.d.ts @@ -50,5 +50,7 @@ export interface TransformWithOptionsInput { onTransformed?: OnTransformedCallback; kbotOptions?: Partial; } +export declare const defaultOnTransform: OnTransformCallback; +export declare const defaultOnTransformed: OnTransformedCallback; export declare function transformObjectWithOptions(obj: Record, transform: AsyncTransformer, options: TransformWithOptionsInput): Promise; export declare const defaultOptions: (options?: Partial) => TransformOptions; diff --git a/packages/kbot/dist-in/async-iterator.js b/packages/kbot/dist-in/async-iterator.js index fc876d12..d52709f2 100644 --- a/packages/kbot/dist-in/async-iterator.js +++ b/packages/kbot/dist-in/async-iterator.js @@ -114,8 +114,8 @@ export const defaultError = (path, value, error) => { console.error(`Error at path: ${path}, value: ${value}, error: ${error}`); }; // Default no-op implementations for the new callbacks -const defaultOnTransform = async (_, value) => value; -const defaultOnTransformed = async (_, transformedValue) => transformedValue; +export const defaultOnTransform = async (_, value) => value; +export const defaultOnTransformed = async (_, transformedValue) => transformedValue; export async function transformObjectWithOptions(obj, transform, options) { const { jsonPath, targetPath = null, network = {}, errorCallback = defaultError, filterCallback = testFilters(defaultFilters()), onTransform = defaultOnTransform, // Use default if not provided onTransformed = defaultOnTransformed, // Use default if not provided @@ -181,4 +181,4 @@ export const defaultOptions = (options = {}) => { targetPath: options.targetPath }; }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXN5bmMtaXRlcmF0b3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvYXN5bmMtaXRlcmF0b3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUN4QyxPQUFPLFNBQVMsTUFBTSxZQUFZLENBQUE7QUFDbEMsT0FBTyxJQUFJLE1BQU0sT0FBTyxDQUFBO0FBQ3hCLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQTtBQWlCbEQsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQThCO0lBQzlELGFBQWEsRUFBRSxJQUFJO0lBQ25CLGVBQWUsRUFBRSxDQUFDO0lBQ2xCLFVBQVUsRUFBRSxDQUFDO0lBQ2IsVUFBVSxFQUFFLElBQUk7Q0FDbkIsQ0FBQztBQW1CRixvQ0FBb0M7QUFDcEMsTUFBTSxLQUFLLEdBQUcsQ0FBQyxFQUFVLEVBQUUsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBRTlFLE1BQU0sQ0FBQyxNQUFNLFFBQVEsR0FBVyxLQUFLLEVBQUUsS0FBYSxFQUFFLEVBQUUsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFBO0FBQ3hGLE1BQU0sQ0FBQyxNQUFNLFNBQVMsR0FBVyxLQUFLLEVBQUUsS0FBYSxFQUFFLEVBQUUsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUE7QUFDdkYsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFXLEtBQUssRUFBRSxLQUFhLEVBQUUsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUE7QUFFakYsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFHLENBQUMsT0FBaUIsRUFBa0IsRUFBRTtJQUM3RCxPQUFPLEtBQUssRUFB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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXN5bmMtaXRlcmF0b3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvYXN5bmMtaXRlcmF0b3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUN4QyxPQUFPLFNBQVMsTUFBTSxZQUFZLENBQUE7QUFDbEMsT0FBTyxJQUFJLE1BQU0sT0FBTyxDQUFBO0FBQ3hCLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQTtBQWlCbEQsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQThCO0lBQzlELGFBQWEsRUFBRSxJQUFJO0lBQ25CLGVBQWUsRUFBRSxDQUFDO0lBQ2xCLFVBQVUsRUFBRSxDQUFDO0lBQ2IsVUFBVSxFQUFFLElBQUk7Q0FDbkIsQ0FBQztBQW1CRixvQ0FBb0M7QUFDcEMsTUFBTSxLQUFLLEdBQUcsQ0FBQyxFQUFVLEVBQUUsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBRTlFLE1BQU0sQ0FBQyxNQUFNLFFBQVEsR0FBVyxLQUFLLEVBQUUsS0FBYSxFQUFFLEVBQUUsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFBO0FBQ3hGLE1BQU0sQ0FBQyxNQUFNLFNBQVMsR0FBVyxLQUFLLEVBQUUsS0FBYSxFQUFFLEVBQUUsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUE7QUFDdkYsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFXLEtBQUssRUFBRSxLQUFhLEVBQUUsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUE7QUFFakYsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFHLENBQUMsT0FBaUIsRUFBa0IsRUFBRTtJQUM3RCxPQUFPLEtBQUssRUFB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 \ No newline at end of file diff --git a/packages/kbot/logs/params.json b/packages/kbot/logs/params.json index 94160367..d7b3b150 100644 --- a/packages/kbot/logs/params.json +++ b/packages/kbot/logs/params.json @@ -1,48 +1,13 @@ { - "model": "anthropic/claude-sonnet-4", + "model": "openai/gpt-4o-mini", "messages": [ { "role": "user", - "content": "test-prompt-glob-extension" + "content": "Analyze the following text and extract keywords and sentiment. Respond ONLY with a valid JSON object matching the schema provided in the 'format' field. Do not include any other text, explanations, or markdown formatting.\n\nText to analyze:\n\"The section analyzes keywords and sentiment from positive weather-related text.\"" }, { "role": "user", "content": "" - }, - { - "role": "user", - "path": "PHApp.h", - "content": "#ifndef PHAPP_H\n#define PHAPP_H\n\n#include \"config.h\"\n#include \"config-modbus.h\" \n#include \"features.h\"\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n\n#include \n#include \n#include \n#include \n\n#include \n\n\nclass POT;\nclass Relay;\nclass RS485;\nclass Pos3Analog;\nclass StatusLight;\nclass RESTServer;\nclass PIDController; \nclass TemperatureProfile;\nclass SignalPlot;\nclass SAKO_VFD;\nclass MB_GPIO;\nclass AnalogLevelSwitch;\nclass LEDFeedback;\nclass Extruder;\nclass Plunger;\nclass Joystick;\nclass PHApp;\nclass AmperageBudgetManager;\n\nclass AsyncWebServerRequest;\n\n\nclass PHApp : public App\n{\npublic:\n //////////////////////////////////////////////////////////////\n // Enums\n //////////////////////////////////////////////////////////////\n enum CONTROLLER_STATE\n {\n E_CS_OK = 0,\n E_CS_ERROR = 10\n };\n enum APP_STATE\n {\n RESET = 0,\n EXTRUDING = 1,\n STANDBY = 2,\n ERROR = 5,\n PID_TIMEOUT = 11,\n FEED_TIMEOUT = 12,\n CONTROL_PANEL_INVALID = 13,\n PID_ERROR = 20,\n FEED_ERROR = 40,\n };\n\n //////////////////////////////////////////////////////////////\n // Constructor / Destructor\n //////////////////////////////////////////////////////////////\n PHApp();\n ~PHApp() override;\n\n //////////////////////////////////////////////////////////////\n // Core Application Logic\n //////////////////////////////////////////////////////////////\n virtual short setup();\n virtual short onRun();\n short loop() override;\n short load(short val0 = 0, short val1 = 0);\n virtual short serial_register(Bridge *bridge);\n virtual Component *byId(ushort id);\n // App States & Error Handling\n short _state;\n short _cstate;\n short _error;\n short setAppState(short newState);\n short getAppState(short val);\n short getLastError() { return _error; }\n short setLastError(short val = 0) { _error = val; return _error; }\n short onError(short id, short code);\n short clearError();\n short reset(short arg1, short arg2); // Related to resetting state?\n\n //////////////////////////////////////////////////////////////\n // Components\n //////////////////////////////////////////////////////////////\n SerialMessage *com_serial;\n POT *pot_0;\n POT *pot_1;\n POT *pot_2;\n StatusLight *statusLight_0;\n StatusLight *statusLight_1;\n Relay *relay_0;\n Relay *relay_1;\n Relay *relay_2;\n Relay *relay_3;\n Relay *relay_4;\n Relay *relay_5;\n Relay *relay_6;\n Relay *relay_7;\n Pos3Analog *pos3Analog_0;\n Pos3Analog *pos3Analog_1;\n PIDController *pidController_0; \n SAKO_VFD *vfd_0;\n Extruder *extruder_0;\n Plunger *plunger_0; \n MB_GPIO *gpio_0;\n AnalogLevelSwitch *analogLevelSwitch_0;\n LEDFeedback *ledFeedback_0;\n Joystick *joystick_0;\n AmperageBudgetManager *pidManagerAmperage;\n \n // Component Callbacks/Control\n short onStop(short code = 0);\n short onWarning(short code);\n\n //////////////////////////////////////////////////////////////\n // Logging\n //////////////////////////////////////////////////////////////\n std::vector logBuffer;\n size_t currentLogIndex = 0;\n CircularLogPrinter *logPrinter = nullptr; \n std::vector getLogSnapshot()\n {\n std::vector snapshot;\n snapshot.reserve(logBuffer.size());\n if (logBuffer.size() < LOG_BUFFER_LINES)\n { \n for (size_t i = 0; i < logBuffer.size(); ++i)\n {\n snapshot.push_back(logBuffer[i]);\n }\n }\n else\n {\n // Buffer is full and circular\n size_t startIndex = (currentLogIndex + 1) % LOG_BUFFER_LINES; // <-- Note: LOG_BUFFER_LINES is now defined in Logger.h\n for (size_t i = 0; i < LOG_BUFFER_LINES; ++i)\n {\n snapshot.push_back(logBuffer[(startIndex + i) % LOG_BUFFER_LINES]);\n }\n }\n return snapshot;\n }\n\n //////////////////////////////////////////////////////////////\n // Network Management\n //////////////////////////////////////////////////////////////\n short setupNetwork();\n short loadNetworkSettings();\n short saveNetworkSettings(JsonObject& doc);\n WiFiNetworkSettings wifiSettings;\n //////////////////////////////////////////////////////////////\n // Modbus TCP\n //////////////////////////////////////////////////////////////\n ModbusTCP *modbusManager;\n short loopModbus();\n#ifdef ENABLE_MODBUS_TCP\n short setupModbus();\n \n short mb_tcp_write(short address, short value) override;\n short mb_tcp_write(MB_Registers *reg, short networkValue) override;\n short mb_tcp_read(short address) override;\n \n void mb_tcp_register(ModbusTCP *manager) const override;\n ModbusBlockView *mb_tcp_blocks() const override;\n\n int client_count;\n int client_max;\n int client_total;\n millis_t client_track_ts;\n \n short updateClientCount(short val0, short val1);\n short resetClientStats(short val0, short val1);\n short getClientStats(short val0, short val1);\n\n // Modbus PID Specific (Conditional)\n short getConnectedClients() const; // Returns number of currently connected Modbus TCP clients\n\n#ifdef ENABLE_PID\n short getPid2Register(short offset, short unused);\n short setPid2Register(short offset, short value);\n#endif // ENABLE_PID\n#endif // ENABLE_MODBUS_TCP\n RESTServer *webServer;\n short loopWeb();\n\n#ifdef ENABLE_RS485\n friend class RS485Devices; \n#endif // ENABLE_RS485\n \n //////////////////////////////////////////////////////////////\n // Component Overrides / Message Handling\n /////////////////////////////////////////////////////////////\n /**\n * @brief Handles incoming messages, including RTU updates via void*.\n */\n short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override;\n //////////////////////////////////////////////////////////////\n // Debugging & Utility Methods\n //////////////////////////////////////////////////////////////\n void printRegisters();\n short list(short val0, short val1);\n short print(short arg1, short arg2);\n\n //////////////////////////////////////////////////////////////\n // Profiling & Feature Specific (Conditional)\n //////////////////////////////////////////////////////////////\n#ifdef ENABLE_PROFILER\n static uint32_t initialFreeHeap;\n static uint64_t initialCpuTicks;\n#endif // ENABLE_PROFILER\n\n#ifdef ENABLE_PROFILE_TEMPERATURE\n TemperatureProfile* tempProfiles[PROFILE_TEMPERATURE_COUNT]; // Array to hold multiple temperature profiles \n void getProfilesHandler(AsyncWebServerRequest *request);\n void setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot); // Adjusted for body handling\n bool saveProfilesToJson();\n#endif // ENABLE_PROCESS_PROFILE\n\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\n SignalPlot* signalPlots[PROFILE_SIGNAL_PLOT_COUNT]; // Array to hold multiple signal plot profiles\n void getSignalPlotsHandler(AsyncWebServerRequest *request);\n void setSignalPlotsHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot);\n bool saveSignalPlotsToJson();\n // Methods to control SignalPlot from TemperatureProfile\n void startSignalPlot(short slotId);\n void stopSignalPlot(short slotId);\n void enableSignalPlot(short slotId, bool enable);\n void pauseSignalPlot(short slotId);\n void resumeSignalPlot(short slotId);\n#endif // ENABLE_PROFILE_SIGNAL_PLOT\n \n //////////////////////////////////////////////////////////////\n // Web Server\n ////////////////////////////////////////////////////////////// \n /**\n * @brief Register routes with the RESTServer. This will be called upon built-in RESTServer initialization.\n * \n * @param server The RESTServer instance to register routes with.\n * @return short The result of the operation.\n */\n short registerRoutes(RESTServer *instance);\n // Network settings handlers\n#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS\n void handleGetNetworkSettings(AsyncWebServerRequest *request);\n void handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json);\n#endif\n void getSystemLogsHandler(AsyncWebServerRequest *request);\n void getBridgeMethodsHandler(AsyncWebServerRequest *request); \n\nprivate:\n //////////////////////////////////////////////////////////////\n // Private Methods\n //////////////////////////////////////////////////////////////\n void handleSerialCommand(const String &command); // Moved here as it's private impl detail\n void cleanupComponents(); // Moved here as it's private impl detail\n};\n\n#endif" - }, - { - "role": "user", - "path": "PHAppWeb.cpp", - "content": "#include \"PHApp.h\"\r\n#include \r\n#include \r\n\r\nshort PHApp::registerRoutes(RESTServer *instance)\r\n{\r\n\r\n#ifdef ENABLE_PLUNGER\r\n instance->server.on(\"/api/v1/plunger/settings\", HTTP_GET, [instance](AsyncWebServerRequest *request)\r\n {\r\n Component* comp = instance->owner->byId(COMPONENT_KEY_PLUNGER); \r\n if (!comp) {\r\n request->send(404, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Plunger component not found\\\"}\");\r\n return;\r\n }\r\n Plunger* plunger = static_cast(comp); \r\n AsyncResponseStream *response = request->beginResponseStream(\"application/json\");\r\n JsonDocument doc; \r\n plunger->getSettingsJson(doc);\r\n serializeJson(doc, *response);\r\n request->send(response); });\r\n AsyncCallbackJsonWebHandler *setPlungerSettingsHandler = new AsyncCallbackJsonWebHandler(\"/api/v1/plunger/settings\",\r\n [instance](AsyncWebServerRequest *request, JsonVariant &json)\r\n {\r\n Component *comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);\r\n if (!comp)\r\n {\r\n request->send(404, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Plunger component not found\\\"}\");\r\n return;\r\n }\r\n Plunger *plunger = static_cast(comp);\r\n if (!json.is())\r\n {\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid JSON payload: Expected an object.\\\"}\");\r\n return;\r\n }\r\n JsonObject jsonObj = json.as();\r\n if (plunger->updateSettingsFromJson(jsonObj))\r\n {\r\n request->send(200, \"application/json\", \"{\\\"success\\\":true,\\\"message\\\":\\\"Plunger settings updated and saved.\\\"}\");\r\n }\r\n else\r\n {\r\n request->send(500, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Failed to update or save Plunger settings.\\\"}\");\r\n }\r\n });\r\n\r\n setPlungerSettingsHandler->setMethod(HTTP_POST);\r\n instance->server.addHandler(setPlungerSettingsHandler);\r\n\r\n instance->server.on(\"/api/v1/plunger/settings/load-defaults\", HTTP_POST, [instance](AsyncWebServerRequest *request)\r\n {\r\n Component* comp = instance->owner->byId(COMPONENT_KEY_PLUNGER); \r\n if (!comp) {\r\n request->send(404, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Plunger component not found\\\"}\");\r\n return;\r\n }\r\n Plunger* plunger = static_cast(comp);\r\n if (plunger->loadDefaultSettings()) {\r\n request->send(200, \"application/json\", \"{\\\"success\\\":true,\\\"message\\\":\\\"Plunger default settings loaded and applied to operational settings.\\\"}\");\r\n } else {\r\n request->send(500, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Failed to load default settings or save them to operational path.\\\"}\");\r\n } });\r\n\r\n#endif\r\n\r\n#ifdef ENABLE_PROFILE_TEMPERATURE\r\n // --- Temperature Profile Routes ---\r\n instance->server.on(\"/api/v1/profiles\", HTTP_GET, [this](AsyncWebServerRequest *request)\r\n { this->getProfilesHandler(request); });\r\n\r\n // Handler for POST /api/v1/profiles\r\n // The slot is now taken from the JSON payload.\r\n AsyncCallbackJsonWebHandler* postProfileHandler = new AsyncCallbackJsonWebHandler(\"/api/v1/profiles\", \r\n [this](AsyncWebServerRequest *request, JsonVariant &json) {\r\n if (!json.is() || !json[\"slot\"].is()) {\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid payload: Must be JSON object containing an integer 'slot' field.\\\"}\");\r\n return;\r\n }\r\n \r\n int slot = json[\"slot\"].as();\r\n\r\n // Basic validation for slot number (e.g., non-negative)\r\n // You might want to add an upper bound check against PROFILE_TEMPERATURE_COUNT if it's accessible here\r\n // or rely on setProfilesHandler to do more thorough validation.\r\n if (slot < 0) { \r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid profile slot number in payload.\\\"}\");\r\n return;\r\n }\r\n\r\n // Call the actual handler, passing the parsed JSON and extracted slot number\r\n this->setProfilesHandler(request, json, slot);\r\n });\r\n postProfileHandler->setMethod(HTTP_POST); // Ensure it's set for POST\r\n instance->server.addHandler(postProfileHandler);\r\n#endif\r\n\r\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\r\n // --- Signal Plot Profile Routes ---\r\n instance->server.on(\"/api/v1/signalplots\", HTTP_GET, [this](AsyncWebServerRequest *request)\r\n { this->getSignalPlotsHandler(request); });\r\n\r\n AsyncCallbackJsonWebHandler* postSignalPlotHandler = new AsyncCallbackJsonWebHandler(\"/api/v1/signalplots\", \r\n [this](AsyncWebServerRequest *request, JsonVariant &json) {\r\n if (!json.is() || !json[\"slot\"].is()) {\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid payload: Must be JSON object containing an integer 'slot' field.\\\"}\");\r\n return;\r\n }\r\n int slot = json[\"slot\"].as();\r\n if (slot < 0) { \r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid profile slot number in payload.\\\"}\");\r\n return;\r\n }\r\n this->setSignalPlotsHandler(request, json, slot); \r\n });\r\n postSignalPlotHandler->setMethod(HTTP_POST);\r\n instance->server.addHandler(postSignalPlotHandler);\r\n#endif\r\n\r\n#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS\r\n instance->server.on(\"/api/network/settings\", HTTP_GET, std::bind(&PHApp::handleGetNetworkSettings, this, std::placeholders::_1));\r\n AsyncCallbackJsonWebHandler *setNetworkSettingsHandler = new AsyncCallbackJsonWebHandler(\"/api/network/settings\",\r\n std::bind(&PHApp::handleSetNetworkSettings, this, std::placeholders::_1, std::placeholders::_2));\r\n setNetworkSettingsHandler->setMethod(HTTP_POST);\r\n instance->server.addHandler(setNetworkSettingsHandler);\r\n#endif\r\n\r\n instance->server.on(\"/api/v1/system/logs\", HTTP_GET, [this](AsyncWebServerRequest *request)\r\n { this->getSystemLogsHandler(request); });\r\n\r\n instance->server.on(\"/api/v1/methods\", HTTP_GET, [this](AsyncWebServerRequest *request)\r\n { this->getBridgeMethodsHandler(request); });\r\n\r\n AsyncCallbackJsonWebHandler* postMethodHandler = new AsyncCallbackJsonWebHandler(\"/api/v1/methods\", \r\n [this](AsyncWebServerRequest *request, JsonVariant &json) {\r\n if (!json.is() || !json[\"command\"].is()) {\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid payload: Must be JSON object containing a 'command' string field.\\\"}\");\r\n return;\r\n }\r\n \r\n String cmdStr = json[\"command\"].as();\r\n Log.infoln(\"REST: Received method call command: %s\", cmdStr.c_str());\r\n\r\n Bridge* bridge = static_cast(byId(COMPONENT_KEY_MB_BRIDGE));\r\n if (!bridge) {\r\n Log.errorln(\"REST: Bridge component not found!\");\r\n request->send(500, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Bridge component not found\\\"}\");\r\n return;\r\n }\r\n\r\n CommandMessage msg;\r\n if (!msg.parse(cmdStr)) {\r\n Log.errorln(\"REST: Failed to parse command string: %s\", cmdStr.c_str());\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid command string format\\\"}\");\r\n return;\r\n }\r\n\r\n short result = bridge->onMessage(msg.id, msg.verb, msg.flags, msg.payload, this);\r\n if (result == E_OK) {\r\n request->send(200, \"application/json\", \"{\\\"success\\\":true,\\\"message\\\":\\\"Method executed successfully\\\"}\");\r\n } else {\r\n Log.errorln(\"REST: Method execution failed with error %d\", result);\r\n request->send(500, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Method execution failed\\\"}\");\r\n }\r\n });\r\n postMethodHandler->setMethod(HTTP_POST);\r\n instance->server.addHandler(postMethodHandler);\r\n\r\n return E_OK;\r\n}\r\n\r\n#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS\r\nvoid PHApp::handleGetNetworkSettings(AsyncWebServerRequest *request)\r\n{\r\n JsonDocument doc = wifiSettings.toJSON();\r\n String responseStr;\r\n serializeJson(doc, responseStr);\r\n request->send(200, \"application/json\", responseStr);\r\n}\r\n\r\nvoid PHApp::handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json)\r\n{\r\n if (!json.is())\r\n {\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid JSON payload: Expected an object.\\\"}\");\r\n return;\r\n }\r\n JsonObject jsonObj = json.as();\r\n\r\n // Attempt to save the settings\r\n short saveResult = saveNetworkSettings(jsonObj);\r\n if (saveResult != E_OK)\r\n {\r\n Log.errorln(\"REST: Failed to save network settings, error: %d\", saveResult);\r\n request->send(500, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Failed to save network settings to persistent storage.\\\"}\");\r\n return;\r\n }\r\n\r\n // Attempt to load and apply the new settings immediately\r\n short loadResult = loadNetworkSettings();\r\n if (loadResult != E_OK && loadResult != E_NOT_FOUND)\r\n { // E_NOT_FOUND is ok if we just saved it, means it was applied from the save buffer\r\n Log.warningln(\"REST: Issue loading network settings after save, error: %d. Settings might not be immediately active.\", loadResult);\r\n // Decide if this is a critical failure for the response\r\n }\r\n request->send(200, \"application/json\", \"{\\\"success\\\":true,\\\"message\\\":\\\"Network settings saved. Device will attempt to apply them. A restart might be required for all changes to take effect.\\\"}\");\r\n}\r\n\r\nvoid PHApp::getSystemLogsHandler(AsyncWebServerRequest *request)\r\n{\r\n String levelStr = \"verbose\"; // Default to verbose\r\n if (request->hasParam(\"level\"))\r\n {\r\n levelStr = request->getParam(\"level\")->value();\r\n }\r\n\r\n // Map string log levels to their integer values\r\n int requestedLevel = LOG_LEVEL_VERBOSE; // Default to verbose\r\n if (levelStr == \"none\")\r\n requestedLevel = LOG_LEVEL_SILENT;\r\n else if (levelStr == \"error\")\r\n requestedLevel = LOG_LEVEL_ERROR;\r\n else if (levelStr == \"warning\")\r\n requestedLevel = LOG_LEVEL_WARNING;\r\n else if (levelStr == \"notice\")\r\n requestedLevel = LOG_LEVEL_NOTICE;\r\n else if (levelStr == \"trace\")\r\n requestedLevel = LOG_LEVEL_TRACE;\r\n else if (levelStr == \"verbose\")\r\n requestedLevel = LOG_LEVEL_VERBOSE;\r\n else\r\n {\r\n request->send(400, \"application/json\", \"{\\\"error\\\":\\\"Invalid log level\\\"}\");\r\n return;\r\n }\r\n String response;\r\n // Get logs using existing logBuffer implementation in PHApp\r\n std::vector logSnapshot = getLogSnapshot();\r\n\r\n // Begin JSON array response\r\n response = \"[\";\r\n bool first = true;\r\n\r\n // Function to escape special characters in JSON\r\n auto escapeJSON = [](const String &str) -> String\r\n {\r\n String result;\r\n for (size_t i = 0; i < str.length(); i++)\r\n {\r\n char c = str.charAt(i);\r\n switch (c)\r\n {\r\n case '\"':\r\n result += \"\\\\\\\"\";\r\n break;\r\n case '\\\\':\r\n result += \"\\\\\\\\\";\r\n break;\r\n case '\\b':\r\n result += \"\\\\b\";\r\n break;\r\n case '\\f':\r\n result += \"\\\\f\";\r\n break;\r\n case '\\n':\r\n result += \"\\\\n\";\r\n break;\r\n case '\\r':\r\n result += \"\\\\r\";\r\n break;\r\n case '\\t':\r\n result += \"\\\\t\";\r\n break;\r\n default:\r\n if (c < ' ')\r\n {\r\n char hex[7];\r\n snprintf(hex, sizeof(hex), \"\\\\u%04x\", c);\r\n result += hex;\r\n }\r\n else\r\n {\r\n result += c;\r\n }\r\n }\r\n }\r\n return result;\r\n };\r\n\r\n // Function to determine log level from a log line\r\n auto getLogLevel = [](const String &line) -> int\r\n {\r\n if (line.startsWith(\"E:\"))\r\n return LOG_LEVEL_ERROR;\r\n if (line.startsWith(\"W:\"))\r\n return LOG_LEVEL_WARNING;\r\n if (line.startsWith(\"N:\"))\r\n return LOG_LEVEL_NOTICE;\r\n if (line.startsWith(\"T:\"))\r\n return LOG_LEVEL_TRACE;\r\n if (line.startsWith(\"V:\"))\r\n return LOG_LEVEL_VERBOSE;\r\n if (line.startsWith(\"I:\"))\r\n return LOG_LEVEL_INFO;\r\n return LOG_LEVEL_VERBOSE; // Default to verbose if no prefix found\r\n };\r\n\r\n // Add each log entry to the response if it meets the requested level\r\n for (const auto &logLine : logSnapshot)\r\n {\r\n int lineLevel = getLogLevel(logLine);\r\n if (lineLevel <= requestedLevel)\r\n {\r\n if (!first)\r\n response += \",\";\r\n response += \"\\\"\" + escapeJSON(logLine) + \"\\\"\";\r\n first = false;\r\n }\r\n }\r\n response += \"]\";\r\n request->send(200, \"application/json\", response);\r\n}\r\n\r\n#endif\r\n\r\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\r\n/**\r\n * @brief Handles GET requests to /api/v1/signalplots\r\n * Returns a list of available signal plot profiles.\r\n */\r\nvoid PHApp::getSignalPlotsHandler(AsyncWebServerRequest *request)\r\n{\r\n AsyncResponseStream *response = request->beginResponseStream(\"application/json\");\r\n JsonDocument doc;\r\n JsonArray profilesArray = doc[\"signalplots\"].to();\r\n\r\n for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; ++i)\r\n {\r\n SignalPlot *profile = this->signalPlots[i];\r\n if (profile)\r\n {\r\n Log.verboseln(\" Processing SignalPlot Slot %d: %s\", i, profile->name.c_str());\r\n JsonObject profileObj = profilesArray.add();\r\n profileObj[\"slot\"] = i;\r\n profileObj[\"name\"] = profile->name;\r\n profileObj[\"duration\"] = profile->getDuration(); \r\n profileObj[\"status\"] = (int)profile->getCurrentStatus();\r\n profileObj[\"enabled\"] = profile->enabled();\r\n profileObj[\"elapsed\"] = profile->getElapsedMs();\r\n profileObj[\"remaining\"] = profile->getRemainingTime();\r\n \r\n JsonArray pointsArray = profileObj[\"controlPoints\"].to();\r\n const S_SignalControlPoint *points = profile->getControlPoints(); \r\n uint8_t numPoints = profile->getNumControlPoints(); \r\n\r\n for (uint8_t j = 0; j < numPoints; ++j) \r\n {\r\n const S_SignalControlPoint& cp = points[j]; \r\n JsonObject pointObj = pointsArray.add();\r\n pointObj[\"id\"] = cp.id;\r\n pointObj[\"time\"] = cp.time;\r\n pointObj[\"name\"] = cp.name;\r\n pointObj[\"description\"] = cp.description;\r\n pointObj[\"state\"] = (int16_t)cp.state;\r\n pointObj[\"type\"] = (int16_t)cp.type;\r\n pointObj[\"arg_0\"] = cp.arg_0;\r\n pointObj[\"arg_1\"] = cp.arg_1;\r\n pointObj[\"arg_2\"] = cp.arg_2;\r\n }\r\n }\r\n else\r\n {\r\n Log.warningln(\" SignalPlot slot %d is null\", i);\r\n }\r\n }\r\n serializeJson(doc, *response);\r\n request->send(response);\r\n}\r\n\r\n/**\r\n * @brief Handles POST requests to /api/v1/signalplots (slot from payload)\r\n * Updates the specified signal plot profile using the provided JSON data.\r\n *\r\n * @param request The incoming web request.\r\n * @param json The parsed JSON body from the request.\r\n * @param slot The profile slot number extracted from the payload.\r\n */\r\nvoid PHApp::setSignalPlotsHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot)\r\n{\r\n if (slot < 0 || slot >= PROFILE_SIGNAL_PLOT_COUNT) \r\n {\r\n Log.warningln(\"REST: setSignalPlotsHandler - Invalid slot number %d provided.\", slot);\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid profile slot number\\\"}\");\r\n return;\r\n }\r\n\r\n SignalPlot *targetProfile = this->signalPlots[slot];\r\n if (!targetProfile)\r\n {\r\n Log.warningln(\"REST: setSignalPlotsHandler - No profile found for slot %d.\", slot);\r\n request->send(404, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Profile slot not found or not initialized\\\"}\");\r\n return;\r\n }\r\n\r\n if (!json.is())\r\n {\r\n Log.warningln(\"REST: setSignalPlotsHandler - Invalid JSON payload (not an object) for slot %d.\", slot);\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid JSON payload: must be an object.\\\"}\");\r\n return;\r\n }\r\n JsonObject jsonObj = json.as();\r\n\r\n bool success = targetProfile->load(jsonObj); \r\n\r\n if (success)\r\n {\r\n Log.infoln(\"REST: SignalPlot slot %d updated successfully.\", slot);\r\n if (saveSignalPlotsToJson()) { \r\n Log.infoln(\"REST: All SignalPlot profiles saved to JSON successfully after update.\");\r\n request->send(200, \"application/json\", \"{\\\"success\\\":true, \\\"message\\\":\\\"SignalPlot profile updated and saved.\\\"}\");\r\n } else {\r\n Log.errorln(\"REST: SignalPlot slot %d updated, but failed to save all profiles to JSON.\", slot);\r\n request->send(500, \"application/json\", \"{\\\"success\\\":true, \\\"message\\\":\\\"SignalPlot profile updated but failed to save configuration.\\\"}\");\r\n }\r\n }\r\n else\r\n {\r\n Log.errorln(\"REST: Failed to update SignalPlot slot %d from JSON.\", slot);\r\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Failed to load SignalPlot profile data. Check format and values.\\\"}\");\r\n }\r\n}\r\n#endif // ENABLE_PROFILE_SIGNAL_PLOT\r\n\r\nvoid PHApp::getBridgeMethodsHandler(AsyncWebServerRequest *request)\r\n{\r\n AsyncResponseStream *response = request->beginResponseStream(\"application/json\");\r\n JsonDocument doc;\r\n JsonArray methodsArray = doc.to();\r\n\r\n Bridge *bridge = static_cast(byId(COMPONENT_KEY_MB_BRIDGE));\r\n if (!bridge)\r\n {\r\n Log.errorln(F(\"REST: Bridge component not found!\"));\r\n request->send(500, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Bridge component not found\\\"}\");\r\n return;\r\n }\r\n\r\n const Vector &componentList = bridge->getComponentList();\r\n for (size_t i = 0; i < componentList.size(); ++i)\r\n {\r\n SComponentInfo *compInfo = componentList.at(i);\r\n if (compInfo && compInfo->instance)\r\n {\r\n Component *component = static_cast(compInfo->instance);\r\n JsonObject methodObj = methodsArray.add();\r\n methodObj[\"id\"] = compInfo->key;\r\n methodObj[\"component\"] = component->name;\r\n methodObj[\"method\"] = compInfo->methodName;\r\n }\r\n }\r\n serializeJson(doc, *response);\r\n request->send(response);\r\n}\r\n" - }, - { - "role": "user", - "path": "PHAppSettings.cpp", - "content": "#include \"PHApp.h\"\r\n#include \"config.h\"\r\n#include \r\n#include \r\n\r\n" - }, - { - "role": "user", - "path": "PHAppNetwork.cpp", - "content": "#include \r\n#include \r\n#include \r\n#include \r\n#include \"Logger.h\"\r\n#include \"./PHApp.h\"\r\n#include \r\n#include \r\n#include \r\n\r\n#include \"./config.h\"\r\n#include \"./config_adv.h\"\r\n#include \"./config-modbus.h\"\r\n#include \"./features.h\"\r\n\r\n#ifdef ENABLE_PROCESS_PROFILE\r\n#include \"profiles/PlotBase.h\"\r\n#include \"profiles/SignalPlot.h\"\r\n#include \"profiles/TemperatureProfile.h\"\r\n#endif\r\n\r\n#include \r\n#include \r\n\r\n\r\nshort PHApp::loadNetworkSettings() {\r\n Log.infoln(F(\"PHApp::loadNetworkSettings() - Attempting to load network configuration from LittleFS...\"));\r\n if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted (true formats if necessary)\r\n Log.errorln(F(\"PHApp::loadNetworkSettings() - Failed to mount LittleFS. Cannot load network configuration.\"));\r\n wifiSettings.print(); // Print defaults before returning\r\n return E_FATAL; // Use E_FATAL for critical FS failure\r\n }\r\n\r\n File configFile = LittleFS.open(NETWORK_CONFIG_FILENAME, \"r\");\r\n if (!configFile) {\r\n Log.warningln(F(\"PHApp::loadNetworkSettings() - Failed to open network config file: %s. Using default settings.\"), NETWORK_CONFIG_FILENAME);\r\n LittleFS.end(); // Close LittleFS\r\n wifiSettings.print(); // Print defaults before returning\r\n return E_NOT_FOUND; // Indicates file wasn't found, defaults will be used.\r\n }\r\n\r\n Log.infoln(F(\"PHApp::loadNetworkSettings() - Opened network config file: %s\"), NETWORK_CONFIG_FILENAME);\r\n\r\n JsonDocument doc; // Using JsonDocument for automatic memory management\r\n\r\n DeserializationError error = deserializeJson(doc, configFile);\r\n configFile.close(); // Close the file as soon as possible\r\n\r\n if (error) {\r\n Log.errorln(F(\"PHApp::loadNetworkSettings() - Failed to parse network config JSON: %s. Using default settings.\"), error.c_str());\r\n LittleFS.end(); // Close LittleFS\r\n wifiSettings.print(); // Print defaults before returning\r\n return E_INVALID_PARAMETER; // Indicates a parsing error, defaults will be used.\r\n }\r\n\r\n JsonObject root = doc.as();\r\n if (root.isNull()) {\r\n Log.errorln(F(\"PHApp::loadNetworkSettings() - Network config JSON root is not an object. Using default settings.\"));\r\n LittleFS.end();\r\n wifiSettings.print(); // Print defaults before returning\r\n return E_INVALID_PARAMETER;\r\n }\r\n \r\n Log.infoln(F(\"PHApp::loadNetworkSettings() - Successfully parsed network config file. Applying settings...\"));\r\n short loadResult = wifiSettings.loadSettings(root); // Call the existing method in WiFiNetworkSettings\r\n\r\n LittleFS.end(); // Ensure LittleFS is closed after operations\r\n\r\n if (loadResult == E_OK) {\r\n Log.infoln(F(\"PHApp::loadNetworkSettings() - Network settings loaded successfully from %s.\"), NETWORK_CONFIG_FILENAME);\r\n } else {\r\n Log.warningln(F(\"PHApp::loadNetworkSettings() - Issues applying parsed network settings. Some defaults may still be in use.\"));\r\n }\r\n wifiSettings.print(); // Print settings after attempting to load them\r\n return loadResult; \r\n}\r\n\r\nshort PHApp::saveNetworkSettings(JsonObject& doc) {\r\n Log.infoln(F(\"PHApp::saveNetworkSettings() - Attempting to save network configuration to LittleFS...\"));\r\n\r\n if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted\r\n Log.errorln(F(\"PHApp::saveNetworkSettings() - Failed to mount LittleFS. Cannot save network configuration.\"));\r\n return E_FATAL; // Or a more specific LittleFS error\r\n }\r\n\r\n File configFile = LittleFS.open(NETWORK_CONFIG_FILENAME, \"w\"); // Open for writing, creates if not exists, truncates if exists\r\n if (!configFile) {\r\n Log.errorln(F(\"PHApp::saveNetworkSettings() - Failed to open network config file '%s' for writing.\"), NETWORK_CONFIG_FILENAME);\r\n LittleFS.end(); // Close LittleFS\r\n return E_FATAL; // Replaced E_FS_ERROR with E_FATAL\r\n }\r\n\r\n Log.infoln(F(\"PHApp::saveNetworkSettings() - Opened/created network config file: %s for writing.\"), NETWORK_CONFIG_FILENAME);\r\n\r\n size_t bytesWritten = serializeJson(doc, configFile);\r\n configFile.close(); // Close the file as soon as possible\r\n\r\n if (bytesWritten > 0) {\r\n Log.infoln(F(\"PHApp::saveNetworkSettings() - Successfully wrote %d bytes to %s.\"), bytesWritten, NETWORK_CONFIG_FILENAME);\r\n } else {\r\n Log.errorln(F(\"PHApp::saveNetworkSettings() - Failed to serialize JSON to file or wrote 0 bytes to %s.\"), NETWORK_CONFIG_FILENAME);\r\n LittleFS.end(); // Close LittleFS\r\n // Attempt to remove the (potentially empty or corrupted) file if serialization failed.\r\n if (LittleFS.exists(NETWORK_CONFIG_FILENAME)) {\r\n LittleFS.remove(NETWORK_CONFIG_FILENAME);\r\n }\r\n return E_INVALID_PARAMETER; // Or a more specific serialization error\r\n }\r\n\r\n LittleFS.end(); // Ensure LittleFS is closed after operations\r\n Log.infoln(F(\"PHApp::saveNetworkSettings() - Network settings saved successfully to %s.\"), NETWORK_CONFIG_FILENAME);\r\n // Optionally, after saving, you might want to immediately reload and apply these settings:\r\n // loadNetworkSettings(); \r\n // Or, signal that a restart is needed for settings to take full effect if they are only read at boot.\r\n return E_OK;\r\n}\r\n\r\nshort PHApp::setupNetwork()\r\n{\r\n loadNetworkSettings(); // Load settings from LittleFS first\r\n bool sta_connected = false;\r\n bool ap_started = false;\r\n\r\n#if defined(ENABLE_AP_STA)\r\n WiFi.mode(WIFI_AP_STA);\r\n Log.infoln(\"Setting up AP_STA with SSID: %s\", wifiSettings.ap_ssid.c_str());\r\n if (!WiFi.softAPConfig(wifiSettings.ap_config_ip, wifiSettings.ap_config_gateway, wifiSettings.ap_config_subnet))\r\n {\r\n Log.errorln(\"AP Failed to configure\");\r\n }\r\n else\r\n {\r\n if (!WiFi.softAP(wifiSettings.ap_ssid.c_str(), wifiSettings.ap_password.c_str()))\r\n {\r\n Log.errorln(\"AP Failed to start\");\r\n }\r\n else\r\n {\r\n Log.infoln(\"AP IP address: %s\", WiFi.softAPIP().toString().c_str());\r\n ap_started = true;\r\n }\r\n }\r\n\r\n // Configure Station (STA) part\r\n Log.infoln(\"Configuring STA for AP_STA mode...\");\r\n \r\n if (!WiFi.config(wifiSettings.sta_local_IP, wifiSettings.sta_gateway, wifiSettings.sta_subnet, wifiSettings.sta_primary_dns, wifiSettings.sta_secondary_dns))\r\n {\r\n Log.errorln(\"STA (for AP_STA) Failed to configure\");\r\n }\r\n WiFi.begin(wifiSettings.sta_ssid.c_str(), wifiSettings.sta_password.c_str());\r\n Log.infoln(\"Attempting to connect to STA WiFi: %s\", wifiSettings.sta_ssid.c_str());\r\n\r\n int connect_timeout_ms = 30000;\r\n unsigned long start_time = millis();\r\n while (WiFi.status() != WL_CONNECTED && (millis() - start_time < connect_timeout_ms))\r\n {\r\n delay(100);\r\n }\r\n\r\n if (WiFi.status() == WL_CONNECTED)\r\n {\r\n Log.infoln(\"STA IP address (AP_STA mode): %s\", WiFi.localIP().toString().c_str());\r\n sta_connected = true;\r\n }\r\n else\r\n {\r\n Log.warningln(\"STA (for AP_STA) connection failed or timed out. AP is still active.\");\r\n }\r\n\r\n#elif defined(ENABLE_WIFI) // STA mode only\r\n Log.infoln(\"Configuring WiFi in STA mode...\");\r\n if (!WiFi.config(wifiSettings.sta_local_IP, wifiSettings.sta_gateway, wifiSettings.sta_subnet, wifiSettings.sta_primary_dns, wifiSettings.sta_secondary_dns))\r\n {\r\n Log.errorln(\"STA Failed to configure\");\r\n }\r\n WiFi.begin(wifiSettings.sta_ssid.c_str(), wifiSettings.sta_password.c_str());\r\n int connect_timeout_ms = 30000;\r\n unsigned long start_time = millis();\r\n while (WiFi.status() != WL_CONNECTED && (millis() - start_time < connect_timeout_ms))\r\n {\r\n delay(100);\r\n }\r\n if (WiFi.status() == WL_CONNECTED)\r\n {\r\n Log.infoln(\"IP address: %s\", WiFi.localIP().toString().c_str());\r\n sta_connected = true;\r\n }\r\n else\r\n {\r\n Log.errorln(\"WiFi connection timed out!\");\r\n // return E_WIFI_CONNECTION_FAILED; // Keep network setup going if AP might work or for mDNS on AP\r\n }\r\n#endif\r\n\r\n // Initialize mDNS\r\n // It should be started if either STA is connected or AP is successfully started.\r\n if (sta_connected || ap_started) {\r\n const char* mdns_hostname = \"polymech-cassandra\";\r\n if (MDNS.begin(mdns_hostname)) {\r\n Log.infoln(\"mDNS responder started. Hostname: %s\", mdns_hostname);\r\n MDNS.addService(\"http\", \"tcp\", 80);\r\n Log.infoln(\"mDNS service _http._tcp.local on port 80 advertised.\");\r\n Log.infoln(\"Access the web server at: http://%s.local\", mdns_hostname);\r\n } else {\r\n Log.errorln(\"Error starting mDNS responder!\");\r\n }\r\n } else {\r\n Log.warningln(\"Neither STA connected nor AP started. mDNS will not be initialized.\");\r\n }\r\n\r\n#ifdef ENABLE_MODBUS_TCP\r\n setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);\r\n setupModbus();\r\n#else\r\n modbusManager = nullptr;\r\n#endif\r\n\r\n#if defined(ENABLE_WEBSERVER) && defined(ENABLE_MODBUS_TCP)\r\n\r\n if (modbusManager) // Check Modbus dependency first\r\n {\r\n IPAddress webserverIP = IPAddress(0,0,0,0);\r\n bool canStartWebServer = false;\r\n\r\n#if defined(ENABLE_AP_STA)\r\n webserverIP = WiFi.softAPIP(); // IP of the AP interface\r\n if (webserverIP && webserverIP != IPAddress(0,0,0,0)) {\r\n Log.infoln(\"AP_STA mode: Web server will use AP IP: %s\", webserverIP.toString().c_str());\r\n canStartWebServer = true;\r\n } else {\r\n Log.errorln(\"AP_STA mode: Soft AP IP is invalid or not yet available. Cannot determine IP for web server on AP.\");\r\n }\r\n // Log STA IP for informational purposes if connected\r\n if (WiFi.status() == WL_CONNECTED) {\r\n Log.infoln(\"AP_STA mode: STA interface is also connected with IP: %s\", WiFi.localIP().toString().c_str());\r\n Log.infoln(\" External clients (on STA network) might try http://%s\", WiFi.localIP().toString().c_str());\r\n }\r\n#elif defined(ENABLE_WIFI) // STA mode only\r\n if (WiFi.status() == WL_CONNECTED) {\r\n webserverIP = WiFi.localIP();\r\n Log.infoln(\"STA mode: Web server will use STA IP: %s\", webserverIP.toString().c_str());\r\n canStartWebServer = true;\r\n } else {\r\n Log.errorln(\"STA mode: WiFi not connected. Cannot start web server.\");\r\n }\r\n#else\r\n // This case should not be hit if ENABLE_WEBSERVER implies one of the WiFi modes for IP-based server.\r\n Log.warningln(\"WebServer enabled, but no WiFi mode (AP_STA or STA) is configured to provide an IP address.\");\r\n#endif\r\n if (canStartWebServer) {\r\n webServer = new RESTServer(webserverIP, 80, modbusManager, this);\r\n components.push_back(webServer);\r\n Log.infoln(\"RESTServer initialized.\");\r\n Log.infoln(\"Clients connected to the ESP32 (e.g., via AP) should try accessing the server at: http://%s\", webserverIP.toString().c_str());\r\n } else {\r\n Log.errorln(\"Cannot initialize RESTServer: No suitable IP address available from current WiFi configuration.\");\r\n webServer = nullptr;\r\n }\r\n }\r\n else\r\n {\r\n Log.errorln(\"Cannot initialize RESTServer: ModbusTCP is null! Ensure Modbus is setup first.\");\r\n webServer = nullptr;\r\n return E_DEPENDENCY_NOT_MET;\r\n }\r\n#elif defined(ENABLE_WEBSERVER) && !defined(ENABLE_MODBUS_TCP)\r\n Log.warningln(\"WebServer enabled but Modbus TCP is not. RESTServer initialization might be incomplete.\");\r\n webServer = nullptr; // Keep it null if it relies on ModbusTCP\r\n#endif\r\n return E_OK;\r\n}" - }, - { - "role": "user", - "path": "PHApp.cpp", - "content": "#include \n#include \n#include \n#include \n#include \n\n#include \"./PHApp.h\"\n#include \"./config.h\"\n#include \"./config_adv.h\"\n#include \"./config-modbus.h\"\n#include \"./features.h\"\n\n\n#include \n#include \n\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\n#include \n#endif\n\n#ifdef ENABLE_MODBUS_TCP\n#include \n#include \n#endif\n\n#define MB_R_APP_STATE_REG 9\n#define MB_R_SYSTEM_CMD_PRINT_RESET 1\n#define MB_R_SYSTEM_CMD_PRINT_REGS 2\n#define MB_R_SYSTEM_CMD_PRINT_MEMORY 5\n#define MB_R_SYSTEM_CMD_PRINT_VFD 6\n\n#ifdef ENABLE_PROFILER\nuint32_t PHApp::initialFreeHeap = 0;\nuint64_t PHApp::initialCpuTicks = 0;\n#endif\n\n#ifndef LOG_LEVEL\n#define LOG_LEVEL LOG_LEVEL_NONE\n#endif\n///////////////////////////////////////////////////////////////////////////////////////////////////\n//\n// Network Servers\n//\n#if defined(ENABLE_WEBSERVER)\nWiFiServer server(80);\n#endif\n\n#ifdef ENABLE_MODBUS_TCP\nModbusServerTCPasync mb;\n#endif\n\n///////////////////////////////////////////////////////////////////////////////////////////////////\n//\n// Omron E5\n//\n#undef ENABLE_TRUTH_COLLECTOR\n\n///////////////////////////////////////////////////////////////////////////////////////////////////\n//\n// Factory : Instances\n//\n#define ADD_RELAY(relayNum, relayPin, relayKey, relayAddr) \\\n relay_##relayNum = new Relay(this, relayPin, relayKey, relayAddr); \\\n components.push_back(relay_##relayNum);\n\n#define ADD_POT(potNum, potPin, potKey, potAddr) \\\n pot_##potNum = new POT(this, potPin, potKey, potAddr); \\\n components.push_back(pot_##potNum);\n\n#define ADD_POS3ANALOG(posNum, switchPin1, switchPin2, switchKey, switchAddr) \\\n pos3Analog_##posNum = new Pos3Analog(this, switchPin1, switchPin2, switchKey, switchAddr); \\\n components.push_back(pos3Analog_##posNum);\n\n#ifdef ENABLE_PID\n#define ADD_PID(pidNum, nameStr, doPin, csPin, clkPin, outPin, key) \\\n pidController_##pidNum = new PIDController(key, nameStr, doPin, csPin, clkPin, outPin); \\\n components.push_back(pidController_##pidNum);\n#endif\n\nvoid PHApp::printRegisters()\n{\n Log.verboseln(F(\"--- Entering PHApp::printRegisters ---\"));\n\n#if ENABLED(HAS_MODBUS_REGISTER_DESCRIPTIONS)\n Log.setShowLevel(false);\n Serial.print(\"| Name | ID | Address | RW | Function Code | Number Addresses |Register Description| \\n\");\n Serial.print(\"|------|----------|----|----|----|----|-------|\\n\");\n short size = components.size();\n Log.verboseln(F(\"PHApp::printRegisters - Processing %d components...\"), size);\n for (int i = 0; i < size; i++)\n {\n Component *component = components[i];\n if (!component)\n {\n Log.errorln(F(\"PHApp::printRegisters - Found NULL component at index %d\"), i);\n continue;\n }\n Log.verboseln(F(\"PHApp::printRegisters - Component %d: ID=%d, Name=%s\"), i, component->id, component->name.c_str());\n // if (!(component->nFlags & 1 << OBJECT_NET_CAPS::E_NCAPS_MODBUS)) // <-- Modbus flag check might be different now\n // {\n // continue;\n // }\n // Log.verbose(\"| %s | %d | %d | %s | %d | %d | %s |\\n\", // <-- Calls to removed ModbusGateway methods\n // component->name.c_str(),\n // component->id,\n // component->getAddress(),\n // component->getRegisterMode(),\n // component->getFunctionCode(),\n // component->getNumberAddresses(),\n // component->getRegisterDescription().c_str());\n Log.verbose(\"| %s | %d | - | - | - | - | - |\\n\", // <-- Simplified output\n component->name.c_str(),\n component->id);\n }\n Log.setShowLevel(true);\n#endif\n Log.verboseln(F(\"--- Exiting PHApp::printRegisters ---\"));\n}\nshort PHApp::reset(short val0, short val1)\n{\n _state = APP_STATE::RESET;\n _error = E_OK;\n\n#if defined(ESP32) || defined(ESP8266) // Use ESP.restart() for ESP32 and ESP8266\n ESP.restart();\n#else\n return E_NOT_IMPLEMENTED;\n#endif\n return E_OK;\n}\nshort PHApp::list(short val0, short val1)\n{\n uchar s = components.size();\n for (uchar i = 0; i < s; i++)\n {\n Component *component = components[i];\n if (component)\n {\n Log.verboseln(\"PHApp::list - %d | %s (ID: %d)\", i, component->name.c_str(), component->id);\n }\n else\n {\n Log.warningln(\"PHApp::list - NULL component at index %d\", i);\n }\n }\n return E_OK;\n}\nshort PHApp::setup()\n{\n Log.verbose(\"--------------------PHApp::setup() Begin.-------------------\");\n _state = APP_STATE::RESET;\n _error = E_OK;\n#ifdef ENABLE_PROFILER\n if (initialFreeHeap == 0 && initialCpuTicks == 0)\n {\n initialFreeHeap = ESP.getFreeHeap();\n initialCpuTicks = esp_cpu_get_ccount();\n }\n#endif\n#ifndef DISABLE_SERIAL_LOGGING\n // Serial Setup\n Serial.begin(SERIAL_BAUD_RATE);\n while (!Serial && !Serial.available())\n {\n }\n // Log Setup\n Log.begin(LOG_LEVEL, &Serial);\n Log.setShowLevel(true);\n#else\n // Log Setup (without Serial)\n Log.begin(LOG_LEVEL_WARNING, nullptr); // Or LOG_LEVEL_NONE, or some other target if available\n Log.setShowLevel(false);\n#endif\n\n // Components\n bridge = new Bridge(this);\n#ifndef DISABLE_SERIAL_LOGGING\n com_serial = new SerialMessage(Serial, bridge);\n components.push_back(com_serial);\n#endif\n components.push_back(bridge);\n // Network\n short networkSetupResult = setupNetwork();\n if (networkSetupResult != E_OK)\n {\n Log.errorln(\"Network setup failed with error code: %d\", networkSetupResult);\n }\n\n // Components\n#ifdef ENABLE_RELAYS\n#ifdef AUX_RELAY_0\n ADD_RELAY(0, AUX_RELAY_0, COMPONENT_KEY_RELAY_0, MB_ADDR_AUX_5);\n#endif\n#ifdef AUX_RELAY_1\n ADD_RELAY(1, AUX_RELAY_1, COMPONENT_KEY_RELAY_1, MB_ADDR_AUX_6);\n#endif\n#endif\n\n#ifdef MB_ANALOG_0\n ADD_POT(0, MB_ANALOG_0, COMPONENT_KEY_ANALOG_0, MB_ADDR_AUX_2);\n#endif\n\n#ifdef MB_ANALOG_1\n ADD_POT(1, MB_ANALOG_1, COMPONENT_KEY_ANALOG_1, MB_ADDR_AUX_3);\n#endif\n\n#if (defined(AUX_ANALOG_3POS_SWITCH_0) && (defined(AUX_ANALOG_3POS_SWITCH_1)))\n ADD_POS3ANALOG(0, AUX_ANALOG_3POS_SWITCH_0, AUX_ANALOG_3POS_SWITCH_1, COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_0, MB_ADDR_AUX_3);\n#endif\n\n#if (defined(MB_ANALOG_3POS_SWITCH_2) && (defined(MB_ANALOG_3POS_SWITCH_3)))\n // ADD_POS3ANALOG(1, MB_ANALOG_3POS_SWITCH_2, MB_ANALOG_3POS_SWITCH_3, COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_1, MB_R_SWITCH_1); // <-- Temporarily disable\n#endif\n\n#ifdef MB_GPIO_MB_MAP_7\n // --- Define configuration for the MB_GPIO group ---\n std::vector gpioConfigs;\n gpioConfigs.reserve(2); // Reserve space for 2 elements\n gpioConfigs.push_back(\n GPIO_PinConfig(\n E_GPIO_7, // pinNumber: The physical pin to manage\n E_GPIO_TYPE_ANALOG_INPUT, // pinType: Treat as analog input\n 300, // startAddress: Modbus register address\n E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register\n MB_ACCESS_READ_ONLY, // access: Allow Modbus read only\n 1000, // opIntervalMs: Update interval in milliseconds\n \"GPIO_6\", // name: Custom name for this pin\n \"GPIO_Group\" // group: Group name for this pin\n ));\n\n gpioConfigs.push_back(\n GPIO_PinConfig(\n E_GPIO_15, // pinNumber: The physical pin to manage\n E_GPIO_TYPE_ANALOG_INPUT, // pinType: Treat as analog input\n 301, // startAddress: Modbus register address\n E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register\n MB_ACCESS_READ_ONLY, // access: Allow Modbus read only\n 1000, // opIntervalMs: Update interval in milliseconds\n \"GPIO_15\", // name: Custom name for this pin\n \"GPIO_Group\" // group: Group name for this pin\n ));\n const short gpioGroupId = COMPONENT_KEY_GPIO_MAP; // Using defined key\n gpio_0 = new MB_GPIO(this, gpioGroupId, gpioConfigs);\n components.push_back(gpio_0);\n#endif\n\n#ifdef PIN_ANALOG_LEVEL_SWITCH_0\n analogLevelSwitch_0 = new AnalogLevelSwitch(\n this, // owner\n PIN_ANALOG_LEVEL_SWITCH_0, // analogPin\n ALS_0_NUM_LEVELS, // numLevels\n ALS_0_ADC_STEP, // levelStep\n ALS_0_ADC_OFFSET, // adcValueOffset\n ID_ANALOG_LEVEL_SWITCH_0, // id\n ALS_0_MB_ADDR // modbusAddress\n );\n if (analogLevelSwitch_0)\n {\n components.push_back(analogLevelSwitch_0);\n Log.infoln(F(\"AnalogLevelSwitch_0 initialized. Pin:%d, ID:%d, Levels:%d, Step:%d, Offset:%d, Smooth:%d(Fixed), Debounce:%d(Fixed), MB:%d\"),\n PIN_ANALOG_LEVEL_SWITCH_0, ID_ANALOG_LEVEL_SWITCH_0, ALS_0_NUM_LEVELS,\n ALS_0_ADC_STEP, ALS_0_ADC_OFFSET,\n ALS_SMOOTHING_SIZE,\n ALS_DEBOUNCE_COUNT,\n ALS_0_MB_ADDR);\n }\n else\n {\n Log.errorln(F(\"AnalogLevelSwitch_0 initialization failed.\"));\n }\n#endif\n\n#ifdef ENABLE_STATUS\n statusLight_0 = new StatusLight(this,\n STATUS_WARNING_PIN,\n COMPONENT_KEY_FEEDBACK_0,\n MB_MONITORING_STATUS_FEEDBACK_0); // Keep original address for now, add to config-modbus.h later if needed\n components.push_back(statusLight_0);\n statusLight_1 = new StatusLight(this,\n STATUS_ERROR_PIN,\n COMPONENT_KEY_FEEDBACK_1,\n MB_MONITORING_STATUS_FEEDBACK_1); // Keep original address for now\n components.push_back(statusLight_1);\n#else\n statusLight_0 = NULL;\n statusLight_1 = NULL;\n#endif\n Log.infoln(\"PHApp::setup - Base App::setup() called.\");\n\n#ifdef PIN_LED_FEEDBACK_0\n ledFeedback_0 = new LEDFeedback(\n this, // owner\n PIN_LED_FEEDBACK_0, // pin\n LED_PIXEL_COUNT_0, // pixelCount\n ID_LED_FEEDBACK_0, // id\n LED_FEEDBACK_0_MB_ADDR // modbusAddress\n );\n if (ledFeedback_0)\n {\n components.push_back(ledFeedback_0);\n Log.infoln(F(\"LEDFeedback_0 initialized. Pin:%d, Count:%d, ID:%d, MB:%d\"),\n PIN_LED_FEEDBACK_0, LED_PIXEL_COUNT_0,\n ID_LED_FEEDBACK_0, LED_FEEDBACK_0_MB_ADDR);\n }\n else\n {\n Log.errorln(F(\"LEDFeedback_0 initialization failed.\"));\n }\n#endif\n\n#ifdef ENABLE_JOYSTICK\n joystick_0 = new Joystick(\n this, // owner\n PIN_JOYSTICK_UP, // UP pin\n PIN_JOYSTICK_DOWN, // DOWN pin\n PIN_JOYSTICK_LEFT, // LEFT pin\n PIN_JOYSTICK_RIGHT, // RIGHT pin\n MB_ADDR_AUX_7 // modbusAddress\n );\n if (joystick_0)\n {\n components.push_back(joystick_0);\n }\n else\n {\n Log.errorln(F(\"Joystick_0 initialization failed.\"));\n }\n#endif\n // Motors\n#ifdef ENABLE_SAKO_VFD\n vfd_0 = new SAKO_VFD(MB_SAKO_VFD_SLAVE_ID, MB_SAKO_VFD_READ_INTERVAL);\n components.push_back(vfd_0);\n#endif\n // Temperature\n#ifdef ENABLE_PROFILE_TEMPERATURE\n for (ushort i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)\n {\n // Assign unique ID: COMPONENT_KEY_PROFILE_START (910) + slot index\n ushort profileComponentId = COMPONENT_KEY_PROFILE_START + i;\n tempProfiles[i] = new TemperatureProfile(this, i, profileComponentId);\n components.push_back(tempProfiles[i]);\n Log.infoln(\"PHApp::setup - Initialized TemperatureProfile Slot %d with Component ID %d\", i, profileComponentId);\n }\n#endif\n\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\n for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; i++) {\n ushort profileComponentId = COMPONENT_KEY_SIGNAL_PLOT_START + i;\n signalPlots[i] = new SignalPlot(this, i, profileComponentId);\n components.push_back(signalPlots[i]);\n Log.infoln(\"PHApp::setup - Initialized SignalPlot Slot %d with Component ID %d\", i, profileComponentId);\n }\n#endif\n\n\n#ifdef ENABLE_PID\n const int8_t PID2_THERMO_DO = 19; // Example MISO pin\n const int8_t PID2_THERMO_CS = 5; // Example Chip Select pin\n const int8_t PID2_THERMO_CLK = 18; // Example SCK pin\n const int8_t PID2_OUTPUT_PIN = 23; // Example PWM/Output pin\n ADD_PID(0, \"PID Temp Controller 2\", PID2_THERMO_DO, PID2_THERMO_CS, PID2_THERMO_CLK, PID2_OUTPUT_PIN, COMPONENT_KEY_PID_2);\n#endif\n// RS485\n#ifdef ENABLE_RS485\n rs485 = new RS485(this);\n components.push_back(rs485);\n#endif\n#ifdef ENABLE_AMPERAGE_BUDGET_MANAGER\n pidManagerAmperage = new AmperageBudgetManager(this);\n components.push_back(pidManagerAmperage);\n#endif\n\n// Systems : Extruder\n#ifdef ENABLE_EXTRUDER\n extruder_0 = new Extruder(this, vfd_0, nullptr, nullptr, nullptr);\n components.push_back(extruder_0);\n#endif\n\n#ifdef ENABLE_PLUNGER\n plunger_0 = new Plunger(this, vfd_0, joystick_0, pot_0, pot_1);\n components.push_back(plunger_0);\n#endif\n\n // Application stuff\n registerComponents(bridge);\n serial_register(bridge);\n App::setup();\n onRun();\n return E_OK;\n}\n\nshort PHApp::onRun()\n{\n App::onRun();\n\n#ifdef ENABLE_MODBUS_TCP\n for (Component *comp : components)\n {\n if (comp && comp->hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS))\n {\n comp->mb_tcp_register(modbusManager);\n }\n }\n#endif\n\n#ifdef ENABLE_PROFILE_TEMPERATURE\n load(0, 0);\n#endif\n\n#ifdef ENABLE_RELAYS\n#ifdef AUX_RELAY_0\n relay_0->setValue(1);\n#endif\n#endif\n ///////////////////////////////////////////////////////////////////////////////////////////////\n //\n // Post initialization\n //\n#ifdef ENABLE_WEBSERVER\n registerRoutes(webServer);\n#endif\n\n#if ENABLED(ENABLE_AMPERAGE_BUDGET_MANAGER, ENABLE_OMRON_E5)\n RTU_Base *const *devices = rs485->deviceManager.getDevices();\n int numDevicesInManager = rs485->deviceManager.getMaxDevices();\n for (int i = 0; i < numDevicesInManager; ++i)\n {\n if (devices[i] != nullptr)\n {\n Component *comp = devices[i];\n if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID)\n {\n continue;\n }\n \n if (!pidManagerAmperage->addManagedDevice(static_cast(comp)))\n {\n Log.errorln(\"Failed to add OmronE5 device to AmperageBudgetManager\");\n }\n \n }\n }\n#endif\n\n\n#if ENABLED(ENABLE_TEMPERATURE_PROFILES, ENABLE_OMRON_E5, ENABLE_MODBUS_TCP)\n tempProfiles[0]->disable();\n if (tempProfiles[0] && rs485)\n {\n RTU_Base *const *devices = rs485->deviceManager.getDevices();\n int numDevicesInManager = rs485->deviceManager.getMaxDevices();\n int targetRegisterIndex = 0; // Dedicated index for _targetRegisters\n for (int i = 0; i < numDevicesInManager; i++)\n {\n Component *comp = devices[i];\n if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID)\n {\n continue;\n }\n // Ensure we don't write out of bounds for _targetRegisters\n /*\n if (targetRegisterIndex < TEMP_PROFILE_MAX_TARGET_REGS)\n {\n uint16_t spCmdAddr = comp->mb_tcp_base_address() + static_cast(E_OmronTcpOffset::CMD_SP);\n tempProfiles[0]->setTargetRegister(targetRegisterIndex, spCmdAddr);\n targetRegisterIndex++;\n }\n else\n {\n Log.warningln(\"Max target registers (%d) reached. Cannot set for Omron device (Slave ID: %d)\", TEMP_PROFILE_MAX_TARGET_REGS, comp->slaveId);\n }\n */\n }\n }\n#endif\n return E_OK;\n}\nshort PHApp::serial_register(Bridge *bridge)\n{\n bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR(\"list\"), (ComponentFnPtr)&PHApp::list);\n bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR(\"reset\"), (ComponentFnPtr)&PHApp::reset);\n bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR(\"printRegisters\"), (ComponentFnPtr)&PHApp::printRegisters);\n bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR(\"load\"), (ComponentFnPtr)&PHApp::load);\n return E_OK;\n}\nshort PHApp::onWarning(short code)\n{\n return E_OK;\n}\nshort PHApp::onError(short id, short code)\n{\n if (code == getLastError())\n {\n return code;\n }\n Log.error(F(\"* App:onError - component=%d code=%d\" CR), id, code);\n setLastError(code);\n#ifdef ENABLE_STATUS\n if (statusLight_0)\n {\n switch (id)\n {\n case COMPONENT_KEY_PLUNGER:\n {\n switch (code)\n {\n\n#if defined(ENABLE_PLUNGER)\n case (short)PlungerState::IDLE:\n {\n statusLight_1->set(0, 0);\n break;\n }\n case (short)PlungerState::JAMMED:\n {\n statusLight_1->set(1, 1);\n break;\n }\n default:\n {\n statusLight_1->set(1, 0);\n break;\n }\n }\n#endif\n }\n }\n }\n#endif\n return code;\n}\n\nshort PHApp::onStop(short val)\n{\n return E_OK;\n}\nshort PHApp::clearError()\n{\n setLastError(E_OK);\n return E_OK;\n}\nshort PHApp::loop()\n{\n _loop_start_time_us = micros();\n App::loop(); \n\n#ifdef ENABLE_WEBSERVER\n loopWeb();\n#endif\n#ifdef ENABLE_MODBUS_TCP\n loopModbus();\n#endif\n _loop_duration_us = micros() - _loop_start_time_us;\n return E_OK;\n}\nshort PHApp::loopWeb()\n{\n#if defined(ENABLE_WIFI) && defined(ENABLE_WEBSERVER)\n if (webServer != nullptr)\n {\n webServer->loop();\n }\n#endif\n return E_OK;\n}\nshort PHApp::getAppState(short val)\n{\n return _state;\n}\nPHApp::PHApp() : App()\n{\n name = \"PHApp\";\n webServer = nullptr;\n pidController_0 = nullptr;\n bridge = nullptr;\n com_serial = nullptr;\n pot_0 = nullptr;\n pot_1 = nullptr;\n pot_2 = nullptr;\n statusLight_0 = nullptr;\n statusLight_1 = nullptr;\n relay_0 = nullptr;\n relay_1 = nullptr;\n relay_2 = nullptr;\n relay_3 = nullptr;\n relay_4 = nullptr;\n relay_5 = nullptr;\n relay_6 = nullptr;\n relay_7 = nullptr;\n pos3Analog_0 = nullptr;\n pos3Analog_1 = nullptr;\n modbusManager = nullptr;\n logPrinter = nullptr;\n rs485 = nullptr;\n joystick_0 = nullptr;\n#ifdef ENABLE_PROFILE_TEMPERATURE\n // Initialize the array elements to nullptr\n for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)\n {\n tempProfiles[i] = nullptr;\n }\n#endif\n\n // WiFi settings are now initialized by WiFiNetworkSettings constructor\n}\nvoid PHApp::cleanupComponents()\n{\n Log.infoln(\"PHApp::cleanupComponents - Cleaning up %d components...\", components.size());\n for (Component *comp : components)\n {\n delete comp;\n }\n components.clear(); // Clear the vector AFTER deleting objects\n\n // Nullify pointers that were manually managed or outside the vector\n bridge = nullptr;\n com_serial = nullptr;\n pot_0 = nullptr;\n pot_1 = nullptr;\n pot_2 = nullptr;\n statusLight_0 = nullptr;\n statusLight_1 = nullptr;\n relay_0 = nullptr;\n relay_1 = nullptr;\n relay_2 = nullptr;\n relay_3 = nullptr;\n relay_4 = nullptr;\n relay_5 = nullptr;\n relay_6 = nullptr;\n relay_7 = nullptr;\n pos3Analog_0 = nullptr;\n pos3Analog_1 = nullptr;\n pidController_0 = nullptr;\n modbusManager = nullptr;\n joystick_0 = nullptr;\n#ifdef ENABLE_PROFILE_TEMPERATURE\n // Clean up temperature profiles (they were also added to components vector, so already deleted there)\n // Ensure the pointers in the array are nulled\n for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)\n {\n tempProfiles[i] = nullptr;\n }\n#endif\n#ifdef ENABLE_WEBSERVER\n if (webServer)\n {\n delete webServer;\n webServer = nullptr;\n }\n#endif\n#ifdef ENABLE_RS485\n rs485 = nullptr; // RS485 interface was in the vector, already deleted\n#endif\n Log.infoln(\"PHApp::cleanupComponents - Cleanup complete.\");\n}\nPHApp::~PHApp()\n{\n cleanupComponents();\n}\nshort PHApp::setAppState(short newState)\n{\n if (_state != newState)\n {\n _state = (APP_STATE)newState;\n }\n return E_OK;\n}\n\n#ifdef ENABLE_PID\nshort PHApp::getPid2Register(short offset, short unused)\n{\n if (!pidController_0)\n {\n Log.errorln(\"Serial Command Error: PID Controller 2 not initialized.\");\n return E_INVALID_PARAMETER; // Use defined error code\n }\n if (offset < 0 || offset >= PID_2_REGISTER_COUNT)\n {\n Log.errorln(\"Serial Command Error: Invalid PID2 offset %d.\", offset);\n return E_INVALID_PARAMETER;\n }\n\n short address = MB_HREG_PID_2_BASE_ADDRESS + offset;\n short value = pidController_0->mb_tcp_read(address);\n Log.noticeln(\"PID2 Register Offset %d (Addr %d) Value: %d\", offset, address, value);\n // Optionally send value back over serial if needed by the protocol\n return E_OK;\n}\n\nshort PHApp::setPid2Register(short offset, short value)\n{\n if (!pidController_0)\n {\n Log.errorln(\"Serial Command Error: PID Controller 2 not initialized.\");\n return E_INVALID_PARAMETER; // Use defined error code\n }\n if (offset < 0 || offset >= PID_2_REGISTER_COUNT)\n {\n Log.errorln(\"Serial Command Error: Invalid PID2 offset %d.\", offset);\n return E_INVALID_PARAMETER;\n }\n\n short address = MB_HREG_PID_2_BASE_ADDRESS + offset;\n short result = pidController_0->mb_tcp_write(address, value);\n\n if (result == E_OK)\n {\n Log.noticeln(\"PID2 Register Offset %d (Addr %d) set to: %d\", offset, address, value);\n }\n else\n {\n Log.errorln(\"PID2 Register Offset %d (Addr %d) failed to set to %d. Error: %d\", offset, address, value, result);\n }\n return result;\n}\n\n#endif\n\nshort PHApp::onMessage(int id, E_CALLS verb, E_MessageFlags flags, void *user, Component *src)\n{\n#if ENABLED(ENABLE_RS485, ENABLE_WEBSERVER, ENABLE_WEBSOCKET)\n if (verb == E_CALLS::EC_USER && user != nullptr && webServer != nullptr)\n {\n return webServer->onMessage(id, E_CALLS::EC_USER, E_MessageFlags::E_MF_NONE, user, this);\n }\n#endif\n return App::onMessage(id, verb, flags, user, src);\n}\n\n/**\n * @brief Retrieves a component by its ID.\n *\n * @param id The ID of the component to retrieve.\n * @return A pointer to the component with the specified ID, or nullptr if not found.\n * @note Top-Level PHApp cant be part of components vector, so we need to handle it separately.\n */\nComponent *PHApp::byId(ushort id)\n{\n Component *comp = App::byId(id);\n if (comp)\n {\n return comp;\n }\n else if (id == COMPONENT_KEY_APP)\n {\n return this;\n }\n return nullptr;\n}\n\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\nvoid PHApp::startSignalPlot(short slotId)\n{\n if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)\n {\n Log.infoln(\"PHApp: Starting SignalPlot in slot %d (triggered by TemperatureProfile).\", slotId);\n signalPlots[slotId]->start();\n }\n else\n {\n Log.warningln(\"PHApp: Could not start SignalPlot. Invalid slotId %d or plot not initialized.\", slotId);\n }\n}\n\nvoid PHApp::stopSignalPlot(short slotId)\n{\n if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)\n {\n Log.infoln(\"PHApp: Stopping SignalPlot in slot %d (triggered by TemperatureProfile).\", slotId);\n signalPlots[slotId]->stop();\n }\n else\n {\n Log.warningln(\"PHApp: Could not stop SignalPlot. Invalid slotId %d or plot not initialized.\", slotId);\n }\n}\n\nvoid PHApp::enableSignalPlot(short slotId, bool enable)\n{\n if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)\n {\n if (enable)\n {\n Log.infoln(\"PHApp: Enabling SignalPlot in slot %d (triggered by TemperatureProfile).\", slotId);\n signalPlots[slotId]->enable();\n }\n else\n {\n Log.infoln(\"PHApp: Disabling SignalPlot in slot %d (triggered by TemperatureProfile).\", slotId);\n signalPlots[slotId]->disable();\n }\n }\n else\n {\n Log.warningln(\"PHApp: Could not enable/disable SignalPlot. Invalid slotId %d or plot not initialized.\", slotId);\n }\n}\n\nvoid PHApp::pauseSignalPlot(short slotId)\n{\n if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)\n {\n Log.infoln(\"PHApp: Pausing SignalPlot in slot %d (triggered by TemperatureProfile).\", slotId);\n signalPlots[slotId]->pause();\n }\n else\n {\n Log.warningln(\"PHApp: Could not pause SignalPlot. Invalid slotId %d or plot not initialized.\", slotId);\n }\n}\n\nvoid PHApp::resumeSignalPlot(short slotId)\n{\n if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)\n {\n Log.infoln(\"PHApp: Resuming SignalPlot in slot %d (triggered by TemperatureProfile).\", slotId);\n signalPlots[slotId]->resume();\n }\n else\n {\n Log.warningln(\"PHApp: Could not resume SignalPlot. Invalid slotId %d or plot not initialized.\", slotId);\n }\n}\n#endif // ENABLE_PROFILE_SIGNAL_PLOT\n" - }, - { - "role": "user", - "path": "PHApp-Profiles.cpp", - "content": "#include \"PHApp.h\"\n#include \"config.h\"\n#include \n#include \n#include \n\n#ifdef ENABLE_PROFILE_TEMPERATURE\n#include \n#include \n#endif\n\nshort PHApp::load(short val0, short val1)\n{\n Log.infoln(F(\"PHApp::load() - Loading application data...\"));\n\n Log.infoln(F(\"PHApp::load() - Attempting to load temperature profiles...\"));\n\n if (!LittleFS.begin(true))\n { // Ensure LittleFS is mounted (true formats if necessary)\n Log.errorln(F(\"PHApp::load() - Failed to mount LittleFS. Cannot load profiles.\"));\n return E_INVALID_PARAMETER; // Use invalid parameter as fallback\n }\n\n const char *filename = \"/profiles/defaults.json\"; // Path in LittleFS\n File file = LittleFS.open(filename, \"r\");\n if (!file)\n {\n Log.errorln(F(\"PHApp::load() - Failed to open profile file: %s\"), filename);\n LittleFS.end(); // Close LittleFS\n return E_NOT_FOUND; // Use standard not found\n }\n\n // Increased size slightly for safety, adjust if needed\n // DynamicJsonDocument doc(JSON_ARRAY_SIZE(PROFILE_TEMPERATURE_COUNT) + PROFILE_TEMPERATURE_COUNT * JSON_OBJECT_SIZE(5 + MAX_TEMP_CONTROL_POINTS));\n // Replace DynamicJsonDocument with JsonDocument, letting it handle allocation.\n JsonDocument doc;\n\n // Deserialize the JSON document\n DeserializationError error = deserializeJson(doc, file);\n file.close(); // Close the file ASAP\n LittleFS.end(); // Close LittleFS\n\n if (error)\n {\n Log.errorln(F(\"PHApp::load() - Failed to parse profile JSON: %s\"), error.c_str());\n return E_INVALID_PARAMETER; // Use invalid parameter\n }\n\n // Check if the root is a JSON array\n if (!doc.is())\n {\n Log.errorln(F(\"PHApp::load() - Profile JSON root is not an array.\"));\n return E_INVALID_PARAMETER; // Use invalid parameter\n }\n\n JsonArray profilesArray = doc.as();\n Log.infoln(F(\"PHApp::load() - Found %d profiles in JSON file.\"), profilesArray.size());\n\n uint8_t profileIndex = 0;\n for (JsonObject profileJson : profilesArray)\n {\n if (profileIndex >= PROFILE_TEMPERATURE_COUNT)\n {\n Log.warningln(F(\"PHApp::load() - Too many profiles in JSON (%d), only loading the first %d.\"), profilesArray.size(), PROFILE_TEMPERATURE_COUNT);\n break;\n }\n\n if (!tempProfiles[profileIndex])\n {\n Log.errorln(F(\"PHApp::load() - TemperatureProfile slot %d is not initialized. Skipping JSON profile.\"), profileIndex);\n // Don't increment profileIndex here, try to load next JSON into same slot if possible?\n // Or increment profileIndex to align JSON index with slot index? Let's align.\n profileIndex++;\n continue;\n }\n\n // Assuming TemperatureProfile (or its base PlotBase) has a public method\n // like loadFromJson that takes the JsonObject and calls the protected virtual load.\n // We also assume it returns bool or short (E_OK for success).\n Log.infoln(F(\"PHApp::load() - Loading JSON data into TemperatureProfile slot %d...\"), profileIndex);\n // Now call the protected load() directly, as PHApp is a friend\n if (tempProfiles[profileIndex]->load(profileJson))\n { // returns bool\n const char *name = profileJson[\"name\"] | \"Unnamed\"; // Get name for logging\n Log.infoln(F(\"PHApp::load() - Successfully loaded profile '%s' into slot %d.\"), name, profileIndex);\n }\n else\n {\n Log.errorln(F(\"PHApp::load() - Failed to load profile data into slot %d.\"), profileIndex);\n // Decide if we should return an error or just continue loading others\n // return E_INVALID_PARAMETER; // Option: Stop loading on first failure\n }\n Log.infoln(F(\"PHApp::load() - Loaded %d profiles from JSON into %d available slots and %d target registers.\"), profileIndex, PROFILE_TEMPERATURE_COUNT, tempProfiles[profileIndex]->getTargetRegisters().size());\n profileIndex++; // Move to the next TemperatureProfile slot\n }\n\n // Handle case where JSON has fewer profiles than allocated slots\n if (profileIndex < profilesArray.size())\n {\n Log.warningln(F(\"PHApp::load() - Processed %d JSON profiles but only %d slots were available/initialized.\"), profilesArray.size(), profileIndex);\n }\n else if (profileIndex < PROFILE_TEMPERATURE_COUNT)\n {\n Log.infoln(F(\"PHApp::load() - Loaded %d profiles from JSON into %d available slots.\"), profileIndex, PROFILE_TEMPERATURE_COUNT);\n }\n\n // --- Load Signal Plot Profiles ---\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\n Log.infoln(F(\"PHApp::load() - Attempting to load signal plot profiles...\"));\n if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted, or re-mount if it was closed\n Log.errorln(F(\"PHApp::load() - Failed to mount LittleFS for signal plots. Cannot load signal profiles.\"));\n // Decide on return strategy: return E_INVALID_PARAMETER or continue?\n // For now, let's log and continue, as temp profiles might have loaded.\n } else {\n const char *signalPlotFilename = \"/profiles/signal_plots.json\";\n File signalPlotFile = LittleFS.open(signalPlotFilename, \"r\");\n if (!signalPlotFile) {\n Log.errorln(F(\"PHApp::load() - Failed to open signal plot profile file: %s. This might be normal if it doesn't exist yet.\"), signalPlotFilename);\n } else {\n JsonDocument signalPlotDoc; // Use a new document for signal plots\n DeserializationError spError = deserializeJson(signalPlotDoc, signalPlotFile);\n signalPlotFile.close();\n\n if (spError) {\n Log.errorln(F(\"PHApp::load() - Failed to parse signal plot JSON: %s\"), spError.c_str());\n } else if (!signalPlotDoc.is()) {\n Log.errorln(F(\"PHApp::load() - Signal plot JSON root is not an array.\"));\n } else {\n JsonArray spArray = signalPlotDoc.as();\n Log.infoln(F(\"PHApp::load() - Found %d signal plot profiles in JSON file.\"), spArray.size());\n uint8_t spIndex = 0;\n for (JsonObject spJson : spArray) {\n if (spIndex >= PROFILE_SIGNAL_PLOT_COUNT) {\n Log.warningln(F(\"PHApp::load() - Too many signal plot profiles in JSON (%d), only loading the first %d.\"), spArray.size(), PROFILE_SIGNAL_PLOT_COUNT);\n break;\n }\n if (signalPlots[spIndex]) {\n Log.infoln(F(\"PHApp::load() - Loading JSON data into SignalPlot slot %d...\"), spIndex);\n if (signalPlots[spIndex]->load(spJson)) {\n const char *spName = spJson[\"name\"] | \"Unnamed Signal Plot\";\n Log.infoln(F(\"PHApp::load() - Successfully loaded signal plot profile '%s' into slot %d.\"), spName, spIndex);\n } else {\n Log.errorln(F(\"PHApp::load() - Failed to load signal plot profile data into slot %d.\"), spIndex);\n }\n } else {\n Log.errorln(F(\"PHApp::load() - SignalPlot slot %d is not initialized. Skipping JSON profile.\"), spIndex);\n }\n spIndex++;\n }\n Log.infoln(F(\"PHApp::load() - Loaded %d signal plot profiles from JSON into %d available slots.\"), spIndex, PROFILE_SIGNAL_PLOT_COUNT);\n }\n }\n LittleFS.end(); // Close LittleFS after signal plots are done\n }\n#endif // ENABLE_PROFILE_SIGNAL_PLOT\n\n return E_OK;\n}\n\n#ifdef ENABLE_PROFILE_TEMPERATURE\n\n/**\n * @brief Handles GET requests to /api/v1/profiles\n * Returns a list of available temperature profile slots.\n */\nvoid PHApp::getProfilesHandler(AsyncWebServerRequest *request)\n{\n AsyncResponseStream *response = request->beginResponseStream(\"application/json\");\n JsonDocument doc;\n JsonArray profilesArray = doc[\"profiles\"].to();\n\n for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)\n {\n TemperatureProfile *profile = this->tempProfiles[i];\n if (profile)\n {\n Log.verboseln(\" Processing Profile Slot %d: %s\", i, profile->name.c_str());\n JsonObject profileObj = profilesArray.add();\n profileObj[\"slot\"] = i;\n profileObj[\"duration\"] = profile->getDuration(); \n profileObj[\"status\"] = (int)profile->getCurrentStatus();\n profileObj[\"currentTemp\"] = profile->getTemperature(profile->getElapsedMs());\n profileObj[\"name\"] = profile->name;\n profileObj[\"max\"] = profile->max;\n profileObj[\"enabled\"] = profile->enabled();\n profileObj[\"elapsed\"] = profile->getElapsedMs();\n profileObj[\"remaining\"] = profile->getRemainingTime();\n profileObj[\"signalPlot\"] = profile->getSignalPlotSlotId();\n JsonArray pointsArray = profileObj[\"controlPoints\"].to();\n const TempControlPoint *points = profile->getTempControlPoints();\n uint8_t numPoints = profile->getNumTempControlPoints();\n for (uint8_t j = 0; j < numPoints; ++j)\n {\n JsonObject pointObj = pointsArray.add();\n pointObj[\"x\"] = points[j].x;\n pointObj[\"y\"] = points[j].y; \n }\n JsonArray targetRegistersArray = profileObj[\"targetRegisters\"].to();\n const std::vector &targets = profile->getTargetRegisters();\n for (uint16_t targetReg : targets)\n {\n targetRegistersArray.add(targetReg);\n }\n }\n else\n {\n Log.warningln(\" Profile slot %d is null\", i);\n }\n }\n\n serializeJson(doc, *response);\n request->send(response);\n}\n\n/**\n * @brief Handles POST requests to /api/v1/profiles/{slot}\n * Updates the specified temperature profile using the provided JSON data.\n *\n * @param request The incoming web request.\n * @param json The parsed JSON body from the request.\n * @param slot The profile slot number extracted from the URL.\n */\nvoid PHApp::setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot)\n{\n\n if (slot < 0 || slot >= PROFILE_TEMPERATURE_COUNT)\n {\n Log.warningln(\"REST: setProfileHandler - Invalid slot number %d provided.\", slot);\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid profile slot number\\\"}\");\n return;\n }\n\n // Check if the profile object exists for this slot\n TemperatureProfile *targetProfile = this->tempProfiles[slot];\n if (!targetProfile)\n {\n Log.warningln(\"REST: setProfileHandler - No profile found for slot %d.\", slot);\n request->send(404, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Profile slot not found or not initialized\\\"}\");\n return;\n }\n\n // Check if the JSON is an object\n if (!json.is())\n {\n Log.warningln(\"REST: setProfileHandler - Invalid JSON payload (not an object) for slot %d.\", slot);\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Invalid JSON payload: must be an object.\\\"}\");\n return;\n }\n JsonObject jsonObj = json.as();\n\n // Attempt to load the configuration into the profile object\n bool success = targetProfile->load(jsonObj);\n\n if (success)\n {\n Log.infoln(\"REST: Profile slot %d updated successfully.\", slot);\n // Attempt to save all profiles back to JSON\n if (saveProfilesToJson()) {\n Log.infoln(\"REST: All profiles saved to JSON successfully after update.\");\n request->send(200, \"application/json\", \"{\\\"success\\\":true, \\\"message\\\":\\\"Profile updated and saved.\\\"}\");\n } else {\n Log.errorln(\"REST: Profile slot %d updated, but failed to save all profiles to JSON.\", slot);\n request->send(500, \"application/json\", \"{\\\"success\\\":true, \\\"message\\\":\\\"Profile updated but failed to save configuration.\\\"}\"); // Send 200 as profile was updated, but indicate save error\n }\n }\n else\n {\n Log.errorln(\"REST: Failed to update profile slot %d from JSON.\", slot);\n // Provide a more specific error if `load` can indicate the reason\n request->send(400, \"application/json\", \"{\\\"success\\\":false,\\\"error\\\":\\\"Failed to load profile data. Check format and values.\\\"}\");\n }\n}\n\nbool PHApp::saveProfilesToJson() {\n Log.infoln(F(\"PHApp::saveProfilesToJson() - Saving all temperature profiles to JSON...\"));\n\n if (!LittleFS.begin(true)) {\n Log.errorln(F(\"PHApp::saveProfilesToJson() - Failed to mount LittleFS. Cannot save profiles.\"));\n return false;\n }\n\n const char *filename = \"/profiles/defaults.json\"; // Path in LittleFS\n JsonDocument doc; // Use a single JsonDocument for the array\n JsonArray profilesArray = doc.to();\n\n for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {\n TemperatureProfile *profile = this->tempProfiles[i];\n if (profile) {\n JsonObject profileJson = profilesArray.add();\n profileJson[\"type\"] = \"temperature\"; // Assuming this is fixed for now\n profileJson[\"slot\"] = i;\n profileJson[\"name\"] = profile->name;\n profileJson[\"duration\"] = profile->getDuration(); // Duration in ms\n profileJson[\"max\"] = profile->max;\n profileJson[\"signalPlot\"] = profile->getSignalPlotSlotId();\n // controlPoints\n JsonArray pointsArray = profileJson.createNestedArray(\"controlPoints\");\n const TempControlPoint *points = profile->getTempControlPoints();\n uint8_t numPoints = profile->getNumTempControlPoints();\n for (uint8_t j = 0; j < numPoints; ++j) {\n JsonObject pointObj = pointsArray.add();\n pointObj[\"x\"] = points[j].x;\n pointObj[\"y\"] = points[j].y;\n }\n // targetRegisters\n JsonArray targetRegistersArray = profileJson.createNestedArray(\"targetRegisters\");\n const std::vector &targets = profile->getTargetRegisters();\n for (uint16_t targetReg : targets) {\n targetRegistersArray.add(targetReg);\n }\n } else {\n Log.warningln(F(\"PHApp::saveProfilesToJson() - Profile slot %d is null, skipping.\"), i);\n }\n }\n\n File file = LittleFS.open(filename, \"w\"); // Open for writing, creates if not exists, truncates if exists\n if (!file) {\n Log.errorln(F(\"PHApp::saveProfilesToJson() - Failed to open profile file for writing: %s\"), filename);\n LittleFS.end();\n return false;\n }\n\n size_t bytesWritten = serializeJson(doc, file);\n file.close();\n LittleFS.end();\n\n if (bytesWritten > 0) {\n Log.infoln(F(\"PHApp::saveProfilesToJson() - Successfully wrote %d bytes to %s\"), bytesWritten, filename);\n return true;\n } else {\n Log.errorln(F(\"PHApp::saveProfilesToJson() - Failed to serialize JSON or write to file: %s\"), filename);\n return false;\n }\n}\n\n\n#endif // ENABLE_PROFILE_TEMPERATURE\n\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\nbool PHApp::saveSignalPlotsToJson() { \n \n Log.infoln(F(\"PHApp::saveSignalPlotsToJson() - Saving all SignalPlot profiles to JSON...\"));\n\n if (!LittleFS.begin(true)) {\n Log.errorln(F(\"PHApp::saveSignalPlotsToJson() - Failed to mount LittleFS. Cannot save profiles.\"));\n return false;\n }\n\n const char *filename = \"/profiles/signal_plots.json\"; // Corrected filename\n JsonDocument doc; \n JsonArray profilesArray = doc.to();\n\n for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; ++i) {\n SignalPlot *profile = this->signalPlots[i];\n if (profile) {\n JsonObject profileJson = profilesArray.add();\n profileJson[\"type\"] = \"signal\";\n profileJson[\"slot\"] = i;\n profileJson[\"name\"] = profile->name;\n profileJson[\"duration\"] = profile->getDuration(); \n \n JsonArray pointsArray = profileJson.createNestedArray(\"controlPoints\");\n const S_SignalControlPoint *points = profile->getControlPoints();\n uint8_t numPoints = profile->getNumControlPoints();\n for (uint8_t j = 0; j < numPoints; ++j) {\n JsonObject pointObj = pointsArray.add();\n pointObj[\"id\"] = points[j].id;\n pointObj[\"time\"] = points[j].time;\n pointObj[\"name\"] = points[j].name;\n pointObj[\"description\"] = points[j].description;\n pointObj[\"state\"] = (int16_t)points[j].state;\n pointObj[\"type\"] = (int16_t)points[j].type;\n pointObj[\"arg_0\"] = points[j].arg_0;\n pointObj[\"arg_1\"] = points[j].arg_1;\n pointObj[\"arg_2\"] = points[j].arg_2;\n }\n } else {\n Log.warningln(F(\"PHApp::saveSignalPlotsToJson() - SignalPlot slot %d is null, skipping.\"), i);\n }\n }\n\n File file = LittleFS.open(filename, \"w\"); \n if (!file) {\n Log.errorln(F(\"PHApp::saveSignalPlotsToJson() - Failed to open profile file for writing: %s\"), filename);\n LittleFS.end();\n return false;\n }\n\n size_t bytesWritten = serializeJson(doc, file);\n file.close();\n LittleFS.end();\n\n if (bytesWritten > 0) {\n Log.infoln(F(\"PHApp::saveSignalPlotsToJson() - Successfully wrote %d bytes to %s\"), bytesWritten, filename);\n return true;\n } else {\n Log.errorln(F(\"PHApp::saveSignalPlotsToJson() - Failed to serialize JSON or write to file: %s\"), filename);\n return false;\n }\n}\n#endif // ENABLE_PROFILE_SIGNAL_PLOT\n" - }, - { - "role": "user", - "path": "PHApp-Modbus.cpp", - "content": "#include \"PHApp.h\"\r\n\r\n#ifdef ENABLE_MODBUS_TCP\r\n\r\n#include // For ModbusManager class\r\n#include // For logging\r\n#include // For error codes like E_INVALID_PARAMETER\r\n#include // Include for ModbusServerTCPasync\r\n#include \r\n#include \r\n\r\n#include \"config-modbus.h\" // Include centralized addresses (defines MODBUS_PORT)\r\n\r\nextern ModbusServerTCPasync mb;\r\n\r\nvoid PHApp::mb_tcp_register(ModbusTCP *manager) const\r\n{\r\n if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS))\r\n return;\r\n ModbusBlockView *blocksView = mb_tcp_blocks();\r\n Component *thiz = const_cast(this);\r\n for (int i = 0; i < blocksView->count; ++i)\r\n {\r\n MB_Registers info = blocksView->data[i];\r\n info.componentId = this->id;\r\n manager->registerModbus(thiz, info);\r\n }\r\n}\r\n\r\nModbusBlockView *PHApp::mb_tcp_blocks() const\r\n{\r\n static const MB_Registers kBlocks[] = {\r\n {MB_ADDR_SYSTEM_ERROR, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast(this->id), 0, \"PHApp: System Error\",\"PHApp\"},\r\n {MB_ADDR_APP_STATE, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast(this->id), 0, \"PHApp: App State\",\"PHApp\"},\r\n {MB_ADDR_SUB_STATE_0, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast(this->id), 0, \"PHApp: Sub State 0\",\"PHApp\"},\r\n {MB_ADDR_SUB_STATE_1, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast(this->id), 0, \"PHApp: Sub State 1\",\"PHApp\"},\r\n {MB_ADDR_RESET_CONTROLLER, 1, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast(this->id), 0, \"PHApp: Reset Controller\",\"PHApp\"},\r\n {MB_ADDR_ECHO_TEST, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast(this->id), 0, \"PHApp: Echo Test\",\"PHApp\"},\r\n };\r\n static ModbusBlockView blockView = {kBlocks, int(sizeof(kBlocks) / sizeof(kBlocks[0]))};\r\n return &blockView;\r\n}\r\n\r\n/**\r\n * @brief Handles Modbus read requests for PHApp's specific registers.\r\n */\r\nshort PHApp::mb_tcp_read(short address)\r\n{\r\n switch (address)\r\n {\r\n case MB_ADDR_SYSTEM_ERROR:\r\n return (short)getLastError();\r\n case MB_ADDR_APP_STATE:\r\n return (short)_state;\r\n case MB_ADDR_ECHO_TEST:\r\n return (short)88;\r\n default:\r\n return 0;\r\n }\r\n}\r\nshort PHApp::mb_tcp_write(MB_Registers *reg, short networkValue)\r\n{\r\n return mb_tcp_write(reg->startAddress, networkValue);\r\n}\r\n/**\r\n * @brief Handles Modbus write requests for PHApp's specific registers.\r\n */\r\nshort PHApp::mb_tcp_write(short address, short value)\r\n{\r\n switch (address)\r\n {\r\n case MB_ADDR_RESET_CONTROLLER:\r\n reset(0, 0);\r\n return E_OK; \r\n default:\r\n return E_INVALID_PARAMETER;\r\n }\r\n}\r\nshort PHApp::loopModbus()\r\n{\r\n return E_OK;\r\n}\r\n\r\nshort PHApp::getConnectedClients() const \r\n{\r\n if (!modbusManager) return 0;\r\n return modbusManager->getConnectedClients();\r\n}\r\n\r\nshort PHApp::setupModbus()\r\n{\r\n Log.infoln(\"Setting up Modbus TCP...\");\r\n modbusManager = new ModbusTCP(this, &mb);\r\n if (!modbusManager)\r\n {\r\n Log.fatalln(\"Failed to create ModbusTCP!\");\r\n return E_INVALID_PARAMETER;\r\n }\r\n components.push_back(modbusManager); // Add manager to component list for setup/loop calls\r\n setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);\r\n // --- Register Components with Modbus Manager ---\r\n Log.infoln(\"PHApp::setupModbus - Registering components with ModbusTCP...\");\r\n mb_tcp_register(modbusManager);\r\n Log.infoln(\"--- End Modbus Mappings DUMP --- \");\r\n if (modbusManager->enabled())\r\n {\r\n mb.start(MODBUS_PORT, 10, 2000);\r\n }\r\n else\r\n {\r\n Log.warningln(\"PHApp::setupModbus - ModbusTCP not available or disabled. Skipping Modbus server start.\");\r\n }\r\n return E_OK;\r\n}\r\n\r\n#endif // ENABLE_MODBUS_TCP" } ], "tools": [] diff --git a/packages/kbot/package.json b/packages/kbot/package.json index e6b9873a..e8231ba7 100644 --- a/packages/kbot/package.json +++ b/packages/kbot/package.json @@ -35,6 +35,8 @@ "test:web": "vitest run tests/unit/web.test.ts", "test:web:crwl": "vitest run tests/unit/web/crwl.test.ts", "test:files": "vitest run tests/unit/files.test.ts", + "test:async-iterator": "vitest run tests/unit/core/async-iterator.test.ts", + "test:iterator": "vitest run tests/unit/core/iterator.test.ts", "test:research": "vitest run tests/unit/research.test.ts", "test:core": "vitest run tests/unit/core", "test:core:watch": "vitest watch tests/unit/core", diff --git a/packages/kbot/src/async-iterator.ts b/packages/kbot/src/async-iterator.ts index e3e1ef3e..b5cdbc95 100644 --- a/packages/kbot/src/async-iterator.ts +++ b/packages/kbot/src/async-iterator.ts @@ -200,8 +200,8 @@ export interface TransformWithOptionsInput { } // Default no-op implementations for the new callbacks -const defaultOnTransform: OnTransformCallback = async (_, value) => value; -const defaultOnTransformed: OnTransformedCallback = async (_, transformedValue) => transformedValue; +export const defaultOnTransform: OnTransformCallback = async (_, value) => value; +export const defaultOnTransformed: OnTransformedCallback = async (_, transformedValue) => transformedValue; export async function transformObjectWithOptions( obj: Record, diff --git a/packages/kbot/tests/test-data/core/async-iterator-data.json b/packages/kbot/tests/test-data/core/async-iterator-data.json index a8005247..9d37a602 100644 --- a/packages/kbot/tests/test-data/core/async-iterator-data.json +++ b/packages/kbot/tests/test-data/core/async-iterator-data.json @@ -4,54 +4,54 @@ { "id": "f1", "name": "apple", - "description": "A deliciously juicy fruit that bursts with natural sweetness and offers a satisfying crunch, making every bite a refreshing and energizing treat for your senses.", + "description": "\"Plump, juicy, and irresistibly crunchy, this luscious fruit bursts with sweetness, its tender flesh and golden skin a perfect harmony of texture and taste.\"", "details": { "color": "red", "origin": "Worldwide", - "nutrition": "Rich in fiber and vitamin C, which supports healthy digestion and promotes regular bowel movements. Vitamin C boosts the immune system, aids in collagen production, and helps protect cells from oxidative damage." + "nutrition": "\"Rich in fiber and vitamin C, this food supports healthy digestion, boosts the immune system, and helps protect against cell damage and inflammation, promoting overall well-being and antioxidant defenses.\"" }, - "marketingName": "Orchard Bliss" + "marketingName": "Here are a few suggestions for a more appealing marketing name:\n\n1. **CoreDelight**: This name plays on the idea of the apple's core and sounds delightful and appealing.\n2. **Appleaura**: This name combines \"apple\" with \"aura,\" suggesting a vibrant and healthy glow.\n3. **Harvest Bite**: This name evokes the idea of biting into a fresh, juicy apple, with \"Harvest\" implying a natural and wholesome product.\n4. **Autumn Crunch**: This name captures the essence of the apple season, with \"Crunch\" emphasizing the satisfying texture of biting into a crisp apple.\n5. **Pomium**: This name comes from the Latin word for apple, \"pomum,\" giving it a sleek and modern sound.\n\nWhich one do you like best? Or do you have any specific preferences (e.g., play on words, alliteration, etc.)?" }, { "id": "f2", "name": "banana", - "description": "A vibrant, sun-kissed tropical fruit with a smooth golden skin, juicy flesh bursting with sweet, tangy flavor, and an irresistibly refreshing aroma that captures the essence of summer.", + "description": "\"Vibrant and juicy, the sunshine-hued tropical fruit bursts with sweet flavor, its bright yellow skin inviting you to indulge in a taste of exotic paradise.\"", "details": { "color": "yellow", "origin": "Southeast Asia", - "nutrition": "High in potassium, which helps regulate blood pressure, supports proper muscle and nerve function, and aids in maintaining fluid balance in the body, contributing to overall cardiovascular and muscular health." + "nutrition": "\"High in potassium, supporting healthy blood pressure, promoting strong bone health, and aiding in muscle recovery, this food helps maintain overall cardiovascular and muscular well-being naturally.\"" }, - "marketingName": "Golden Tropic Delight" + "marketingName": "Here are some options:\n\n1. **Bananator**: A playful name that sounds fun and energetic.\n2. **YellowGenie**: This name evokes the idea of a magical genie that grants wishes, with a nod to the bright yellow color of a banana.\n3. **BananaBuzz**: This name creates a buzz around the product and suggests excitement and energy.\n4. **PeelPro**: A clever name that references the banana peel, with a professional twist.\n5. **SunnySpeak**: This name conveys a sense of happiness and warmth, with a nod to the sunny yellow color of a banana.\n\nWhich one do you like best? Or would you like me to come up with more options?" } ], "vegetables": [ { "id": "v1", "name": "carrot", - "description": "A vibrant, nutrient-packed orange root vegetable with a crisp texture and sweet, earthy flavor, often enjoyed fresh, roasted, or blended into nourishing soups and juices.", + "description": "\"Vibrant and succulent, the orange root vegetable boasts a sweet, earthy flavor and crunchy texture, its juicy sweetness bursting with every delightful bite of this nutritious treasure.\"", "details": { "color": "orange", "origin": "Eurasia", - "nutrition": "Supports sharp vision by protecting eye cells from oxidative stress, reduces risk of age-related macular degeneration, and helps maintain retinal health for overall improved eye function and long-term visual clarity." + "nutrition": "Supports eye health, helping to prevent age-related macular degeneration and reduce the risk of cataracts. Rich in antioxidants, it also protects against blue light damage and promotes healthy retina function." }, - "marketingName": "Golden Crunch" + "marketingName": "Here are some options:\n\n1. **SunsetBoost**: evokes the idea of a healthy, vibrant orange color and a boost of energy.\n2. **Orange Bliss**: conveys a sense of happiness and satisfaction, associated with a tasty and nutritious snack.\n3. **BetaBoost**: highlights the scientific name for carrot, Beta vulgaris, and adds a modern, energetic twist.\n4. **Carrotium**: a playful, catchy name that sounds like a fun, healthy treat.\n5. **Harvest Delight**: suggests a connection to nature, harvest time, and the delight of enjoying a crunchy, tasty snack.\n\nWhich one do you like best? Or would you like me to come up with more options?" }, { "id": "v2", "name": "broccoli", - "description": "A vibrant green, nutrient-packed cruciferous vegetable known for its crisp texture and subtle peppery flavor, rich in vitamins, minerals, and powerful antioxidants promoting overall health.", + "description": "\"A vibrant, emerald-green cruciferous superstar, bursting with nutrients and flavor, with tightly packed florets and a sturdy stem, adding a nutritious twist to any meal.\"", "details": { "color": "green", "origin": "Italy", - "nutrition": "Rich in vitamins K and C, this supports strong immune function, promotes healthy blood clotting, and enhances collagen production for skin health. Together, they also help reduce inflammation and support bone strength." + "nutrition": "\"High in vitamins K and C, supporting bone health, immune function, and antioxidant defenses. Vitamin K promotes blood clotting, while vitamin C boosts collagen production and protects against cell damage.\"" }, - "marketingName": "Emerald Crown Veggie" + "marketingName": "Here are some options:\n\n1. **BroccoBoost**: Suggests a boost of energy and nutrition from the broccoli.\n2. **GreenGenie**: Evokes the idea of a magical, healthy solution.\n3. **Broccoli Bliss**: Conveys a sense of happiness and well-being associated with consuming broccoli.\n4. **VeggieVitality**: Emphasizes the product's vitality and health benefits.\n5. **BroccoPower**: Positions broccoli as a powerful, nutritious food.\n\nWhich one do you like best? Or would you like me to come up with more options?" } ] }, "metadata": { "lastUpdated": "2023-04-15", "source": "Foods Database", - "description": "A vibrant assortment of fresh, colorful fruits and crisp vegetables, featuring a diverse selection of popular, nutrient-rich produce perfect for healthy snacks, delicious recipes, and vibrant meals." + "description": "\"Vibrant medley of everyday favorites, featuring a colorful assortment of juicy fruits and crisp vegetables, carefully curated to showcase nature's bounty in all its delicious and nutritious glory.\"" } } \ No newline at end of file diff --git a/packages/kbot/tests/test-data/core/md-test-out.json b/packages/kbot/tests/test-data/core/md-test-out.json index 8731a3da..f6aad3b7 100644 --- a/packages/kbot/tests/test-data/core/md-test-out.json +++ b/packages/kbot/tests/test-data/core/md-test-out.json @@ -7,7 +7,7 @@ "children": [ { "type": "text", - "value": "Intro Chapter", + "value": "\"Chapter 1: Overview\"", "position": { "start": { "line": 1, @@ -40,7 +40,7 @@ "children": [ { "type": "text", - "value": "The document introduces topics related to AI and Markdown processing.", + "value": "The document introduces topics on AI and Markdown processing.", "position": { "start": { "line": 3, @@ -74,7 +74,7 @@ "children": [ { "type": "text", - "value": "Background Overview", + "value": "\"1.1: Background Overview\"", "position": { "start": { "line": 5, @@ -107,7 +107,7 @@ "children": [ { "type": "text", - "value": "Markdown has an interesting history that we will explore.", + "value": "The history of Markdown is interesting and warrants background information.", "position": { "start": { "line": 7, @@ -141,7 +141,7 @@ "children": [ { "type": "text", - "value": "\"Early Days\"", + "value": "\"1.1.1: Origins\"", "position": { "start": { "line": 9, @@ -174,7 +174,7 @@ "children": [ { "type": "text", - "value": "Early days are described in the details provided.", + "value": "Early days are described in the following details.", "position": { "start": { "line": 11, @@ -208,7 +208,7 @@ "children": [ { "type": "text", - "value": "Methodology Overview", + "value": "\"Chapter 2: Methods\"", "position": { "start": { "line": 13, @@ -241,7 +241,7 @@ "children": [ { "type": "text", - "value": "This chapter describes the employed methods.", + "value": "This chapter describes the methods employed.", "position": { "start": { "line": 15, @@ -275,7 +275,7 @@ "children": [ { "type": "text", - "value": "\"Markdown Parsing\"", + "value": "\"2.1: Markdown Parsing\"", "position": { "start": { "line": 17, @@ -308,7 +308,7 @@ "children": [ { "type": "text", - "value": "User preferences indicate a focus on concise and direct responses.", + "value": "It appears the text you provided is incomplete. Please provide the full paragraph for summarization.", "position": { "start": { "line": 19, @@ -340,7 +340,7 @@ }, { "type": "text", - "value": "The paragraph consists only of an empty conjunction and lacks meaningful content.", + "value": "The text consists solely of the word \"and.\"", "position": { "start": { "line": 19, @@ -372,7 +372,7 @@ }, { "type": "text", - "value": "To manage Markdown formatting.", + "value": "To process Markdown.", "position": { "start": { "line": 19, @@ -406,7 +406,7 @@ "children": [ { "type": "text", - "value": "Content Transformation", + "value": "\"Content Transformation\"", "position": { "start": { "line": 21, @@ -439,7 +439,7 @@ "children": [ { "type": "text", - "value": "The process entails navigating the AST and implementing LLM transformations.", + "value": "The process entails navigating the AST to implement LLM transformations.", "position": { "start": { "line": 23, @@ -473,7 +473,7 @@ "children": [ { "type": "text", - "value": "Data Representation", + "value": "\"Chapter 3: Data Formats\"", "position": { "start": { "line": 25, @@ -506,7 +506,7 @@ "children": [ { "type": "text", - "value": "The table includes three headers and various data cells, highlighting important content in one cell.", + "value": "The table contains three rows of data across three headers.", "position": { "start": { "line": 27, @@ -539,7 +539,7 @@ "children": [ { "type": "text", - "value": "The table displays sample data with transformable cells.", + "value": "The table displays sample data, with each cell available for transformation.", "position": { "start": { "line": 33, @@ -573,7 +573,7 @@ "children": [ { "type": "text", - "value": "Structured Output Analysis", + "value": "\"Chapter 4: Structured Output Analysis\"", "position": { "start": { "line": 35, @@ -606,7 +606,7 @@ "children": [ { "type": "text", - "value": "The text mentions sunny weather, making it ideal for a park walk.", + "value": "The section analyzes keywords and sentiment from positive weather-related text.", "position": { "start": { "line": 37, @@ -633,7 +633,18 @@ "offset": 1193 } }, - "tempId": "analyze-me" + "manualAnalysisResult": { + "keywords": [ + "section", + "analyzes", + "keywords", + "sentiment", + "positive", + "weather-related", + "text" + ], + "sentiment": "positive" + } }, { "type": "heading", @@ -641,7 +652,7 @@ "children": [ { "type": "text", - "value": "Final Thoughts", + "value": "\"Conclusion\"", "position": { "start": { "line": 39, @@ -674,7 +685,7 @@ "children": [ { "type": "text", - "value": "A summary of the key findings is presented in the concluding paragraph.", + "value": "A summary of the main findings is presented in the concluding paragraph.", "position": { "start": { "line": 41, diff --git a/packages/kbot/tests/test-data/core/md-test-out.md b/packages/kbot/tests/test-data/core/md-test-out.md index 67b94139..55a3abcf 100644 --- a/packages/kbot/tests/test-data/core/md-test-out.md +++ b/packages/kbot/tests/test-data/core/md-test-out.md @@ -1,37 +1,37 @@ -# Intro Chapter +# "Chapter 1: Overview" -The document introduces topics related to AI and Markdown processing. +The document introduces topics on AI and Markdown processing. -## Background Overview +## "1.1: Background Overview" -Markdown has an interesting history that we will explore. +The history of Markdown is interesting and warrants background information. -### "Early Days" +### "1.1.1: Origins" -Early days are described in the details provided. +Early days are described in the following details. -# Methodology Overview +# "Chapter 2: Methods" -This chapter describes the employed methods. +This chapter describes the methods employed. -## "Markdown Parsing" +## "2.1: Markdown Parsing" -User preferences indicate a focus on concise and direct responses.`unified`The paragraph consists only of an empty conjunction and lacks meaningful content.`remark-parse`To manage Markdown formatting. +It appears the text you provided is incomplete. Please provide the full paragraph for summarization.`unified`The text consists solely of the word "and."`remark-parse`To process Markdown. -## Content Transformation +## "Content Transformation" -The process entails navigating the AST and implementing LLM transformations. +The process entails navigating the AST to implement LLM transformations. -# Data Representation +# "Chapter 3: Data Formats" -The table includes three headers and various data cells, highlighting important content in one cell. +The table contains three rows of data across three headers. -The table displays sample data with transformable cells. +The table displays sample data, with each cell available for transformation. -# Structured Output Analysis +# "Chapter 4: Structured Output Analysis" -The text mentions sunny weather, making it ideal for a park walk. +The section analyzes keywords and sentiment from positive weather-related text. -# Final Thoughts +# "Conclusion" -A summary of the key findings is presented in the concluding paragraph. +A summary of the main findings is presented in the concluding paragraph. diff --git a/packages/kbot/tests/unit/core/async-iterator.test.ts b/packages/kbot/tests/unit/core/async-iterator.test.ts index 5ac12b9a..492672a7 100644 --- a/packages/kbot/tests/unit/core/async-iterator.test.ts +++ b/packages/kbot/tests/unit/core/async-iterator.test.ts @@ -9,7 +9,9 @@ import { ErrorCallback, FilterCallback, INetworkOptions, - DEFAULT_NETWORK_OPTIONS + DEFAULT_NETWORK_OPTIONS, + defaultOnTransform, + defaultOnTransformed } from '../../../src/async-iterator' // Helper function to manually transform paths in the test data based on mockFilterCallback and mockTransform @@ -310,7 +312,9 @@ describe('async-iterator', () => { '$.items[*].name', networkOptions, mockErrorCallback, - mockFilterCallback + mockFilterCallback, + defaultOnTransform, + defaultOnTransformed ) // Should not throw errors and object should remain empty @@ -326,7 +330,9 @@ describe('async-iterator', () => { '', networkOptions, mockErrorCallback, - mockFilterCallback + mockFilterCallback, + defaultOnTransform, + defaultOnTransformed ) // Should not transform anything with empty path @@ -486,7 +492,9 @@ describe('async-iterator', () => { '$[*].[name,description]', // JSONPath expression targeting name and description in array items networkOptions, mockErrorCallback, - filter + filter, + defaultOnTransform, + defaultOnTransformed ) // Verify the actual function works as expected with arrays @@ -513,7 +521,9 @@ describe('async-iterator', () => { path, networkOptions, mockErrorCallback, - mockFilterCallback + mockFilterCallback, + defaultOnTransform, + defaultOnTransformed ) expect(testObj.items[0].description).toBe('A FRUIT') @@ -533,7 +543,9 @@ describe('async-iterator', () => { path, networkOptions, mockErrorCallback, - mockFilterCallback + mockFilterCallback, + defaultOnTransform, + defaultOnTransformed ) expect(testObj.items[0].description).toBe('A FRUIT')