mono/packages/kbot/logs/params.json

49 lines
92 KiB
JSON

{
"model": "anthropic/claude-sonnet-4",
"messages": [
{
"role": "user",
"content": "test-prompt-glob-extension"
},
{
"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 <enums.h>\n#include <vector>\n#include <xmath.h>\n#include <macros.h>\n#include <App.h>\n#include <Component.h>\n#include <Bridge.h>\n#include <SerialMessage.h>\n#include <ArduinoLog.h>\n#include <Logger.h>\n#include <ArduinoJson.h>\n#include <LittleFS.h>\n#include <xstatistics.h>\n\n#include <modbus/ModbusTCP.h>\n#include <modbus/ModbusTypes.h>\n#include <profiles/SignalPlot.h>\n#include <profiles/WiFiNetworkSettings.h>\n\n#include <components/OmronE5.h>\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<String> logBuffer;\n size_t currentLogIndex = 0;\n CircularLogPrinter *logPrinter = nullptr; \n std::vector<String> getLogSnapshot()\n {\n std::vector<String> 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 <components/RestServer.h>\r\n#include <ESPAsyncWebServer.h>\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<Plunger*>(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<Plunger *>(comp);\r\n if (!json.is<JsonObject>())\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<JsonObject>();\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<Plunger*>(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<JsonObject>() || !json[\"slot\"].is<int>()) {\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<int>();\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<JsonObject>() || !json[\"slot\"].is<int>()) {\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<int>();\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<JsonObject>() || !json[\"command\"].is<String>()) {\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<String>();\r\n Log.infoln(\"REST: Received method call command: %s\", cmdStr.c_str());\r\n\r\n Bridge* bridge = static_cast<Bridge*>(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<JsonObject>())\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<JsonObject>();\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<String> 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<JsonArray>();\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<JsonObject>();\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<JsonArray>();\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<JsonObject>();\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<JsonObject>())\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<JsonObject>();\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<JsonArray>();\r\n\r\n Bridge *bridge = static_cast<Bridge *>(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<SComponentInfo *> &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<Component *>(compInfo->instance);\r\n JsonObject methodObj = methodsArray.add<JsonObject>();\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 <ArduinoLog.h>\r\n#include <ArduinoJson.h>\r\n\r\n"
},
{
"role": "user",
"path": "PHAppNetwork.cpp",
"content": "#include <Arduino.h>\r\n#include <macros.h>\r\n#include <Component.h>\r\n#include <enums.h>\r\n#include \"Logger.h\"\r\n#include \"./PHApp.h\"\r\n#include <ESPmDNS.h> \r\n#include <LittleFS.h> \r\n#include <ArduinoJson.h> \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 <modbus/ModbusTCP.h>\r\n#include <modbus/ModbusTypes.h>\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<JsonObject>();\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 <Arduino.h>\n#include <macros.h>\n#include <Component.h>\n#include <enums.h>\n#include <Logger.h>\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 <components/OmronE5Types.h>\n#include <components/OmronE5.h>\n\n#ifdef ENABLE_PROFILE_SIGNAL_PLOT\n#include <profiles/SignalPlot.h>\n#endif\n\n#ifdef ENABLE_MODBUS_TCP\n#include <modbus/ModbusTCP.h>\n#include <modbus/ModbusTypes.h>\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<GPIO_PinConfig> 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<OmronE5 *>(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<uint16_t>(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 <ArduinoLog.h>\n#include <enums.h>\n#include <profiles/TemperatureProfile.h>\n\n#ifdef ENABLE_PROFILE_TEMPERATURE\n#include <LittleFS.h>\n#include <ArduinoJson.h>\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<JsonArray>())\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<JsonArray>();\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<JsonArray>()) {\n Log.errorln(F(\"PHApp::load() - Signal plot JSON root is not an array.\"));\n } else {\n JsonArray spArray = signalPlotDoc.as<JsonArray>();\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<JsonArray>();\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<JsonObject>();\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<JsonArray>();\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<JsonObject>();\n pointObj[\"x\"] = points[j].x;\n pointObj[\"y\"] = points[j].y; \n }\n JsonArray targetRegistersArray = profileObj[\"targetRegisters\"].to<JsonArray>();\n const std::vector<uint16_t> &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<JsonObject>())\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<JsonObject>();\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<JsonArray>();\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<JsonObject>();\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<JsonObject>();\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<uint16_t> &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<JsonArray>();\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<JsonObject>();\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<JsonObject>();\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 <modbus/ModbusTCP.h> // For ModbusManager class\r\n#include <ArduinoLog.h> // For logging\r\n#include <enums.h> // For error codes like E_INVALID_PARAMETER\r\n#include <ModbusServerTCPasync.h> // Include for ModbusServerTCPasync\r\n#include <enums.h>\r\n#include <modbus/ModbusTypes.h>\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<PHApp *>(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<ushort>(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<ushort>(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<ushort>(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<ushort>(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<ushort>(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<ushort>(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": []
}