From 24ed333b1d96654179e608460ed8d2fdc09ad5a9 Mon Sep 17 00:00:00 2001
From: soylentOrange <soylentOrange@users.noreply.github.com>
Date: Wed, 9 Oct 2024 18:05:33 +0200
Subject: [PATCH] Added Function Code 08 (DIAGNOSTICS_SERIAL)

---
 include/localmodbus.h |  13 +++-
 include/pages.h       |   3 +-
 src/localmodbus.cpp   |  48 ++++++++++--
 src/main.cpp          |  22 +++---
 src/pages.cpp         | 177 +++++++++++++++++++++++++++++++++++++-----
 5 files changed, 221 insertions(+), 42 deletions(-)

diff --git a/include/localmodbus.h b/include/localmodbus.h
index 994ab84..24ac499 100644
--- a/include/localmodbus.h
+++ b/include/localmodbus.h
@@ -3,13 +3,20 @@
 
     #include <WiFiManager.h>
     #include <ModbusBridgeWiFi.h>
+    #include <ModbusClientRTU.h>
     #include <Update.h>
     #include "config.h"
 
 enum SubFunctionCode : uint16_t {
-  RETURN_QUERY_DATA             = 0x00,
-  RESTART_COMMUNICATION_OPTION  = 0x01,
+  RETURN_QUERY_DATA                         = 0x00,
+  RESTART_COMMUNICATION_OPTION              = 0x01,
+  RETURN_DIAGNOSTIC_REGISTER                = 0x02,
+  CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER    = 0x0a,
+  RETURN_BUS_MESSAGE_COUNT                  = 0x0b,
+  RETURN_BUS_COMMUNICATION_ERROR_COUNT      = 0x0c,
+  RETURN_SLAVE_MESSAGE_COUNT                = 0x0e,
+
 };
 
-    void setupLocalModbus(uint8_t serverID, ModbusBridgeWiFi *bridge, Config *config, WiFiManager *wm);
+void setupLocalModbus(uint8_t serverID, ModbusClientRTU *rtu, ModbusBridgeWiFi *bridge, Config *config, WiFiManager *wm);
 #endif /* LOCALMODBUS_H */
\ No newline at end of file
diff --git a/include/pages.h b/include/pages.h
index 3fe2a58..38b6a76 100644
--- a/include/pages.h
+++ b/include/pages.h
@@ -15,7 +15,8 @@
     void sendButton(AsyncResponseStream *response, const char *title, const char *action, const char *css = "");
     void sendTableRow(AsyncResponseStream *response, const char *name, uint32_t value);
     void sendTableRow(AsyncResponseStream *response, const char *name, String value);
-    void sendDebugForm(AsyncResponseStream *response, String slaveId, String reg, String function, String count);
+    void sendDebugForm_read(AsyncResponseStream *response, String slaveId, String reg, String function, String count);
+    void sendDebugForm_diagnosticSerial(AsyncResponseStream *response, String slaveId, String subFunction, String data);
     void sendMinCss(AsyncResponseStream *response);
     const String ErrorName(Modbus::Error code);
     const String WiFiQuality(int rssiValue);
diff --git a/src/localmodbus.cpp b/src/localmodbus.cpp
index 5c948ad..d5c6820 100644
--- a/src/localmodbus.cpp
+++ b/src/localmodbus.cpp
@@ -1,6 +1,10 @@
 #include "localmodbus.h"
 #define ETAG "\"" __DATE__ "" __TIME__ "\""
 
+static ModbusClientRTU* _rtu = NULL;
+static ModbusBridgeWiFi* _bridge = NULL;
+
+
 // FC03: worker do serve Modbus function code 0x03 (READ_HOLD_REGISTER)
 ModbusMessage FC03(ModbusMessage request) {
     uint16_t address;           // requested register address
@@ -32,32 +36,66 @@ ModbusMessage FC03(ModbusMessage request) {
 ModbusMessage FC08(ModbusMessage request) {
     uint16_t subFunctionCode;   // Sub-function code
     ModbusMessage response;     // response message to be sent back
+    uint16_t resultWord = 0;
 
-    LOG_D("WORKER CALLED FC08");
+    LOG_D("WORKER CALLED FC08\n");
 
     // get request values
     request.get(2, subFunctionCode);
 
     switch(subFunctionCode) {
         case RETURN_QUERY_DATA:
-            LOG_D("Return Query Data");
+            LOG_D("Return Query Data\n");
             response = request; 
             break;
         case RESTART_COMMUNICATION_OPTION:
-            LOG_D("Restart Communications Option");
+            LOG_D("Restart Communications Option\n");
+            response = request; 
+            break;
+        case CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER:
+            LOG_D("Restart Communications Option\n");
+            if(_rtu != NULL) {
+                _rtu->resetCounts();
+            }
+            if(_bridge != NULL) {
+                _bridge->resetCounts();
+            }
             response = request; 
             break;
+        case RETURN_BUS_MESSAGE_COUNT:
+            LOG_D("Return Bus Message Count\n");
+            if(_bridge != NULL) {
+                resultWord = _bridge->getMessageCount();
+            }
+            response = ModbusMessage(request.getServerID(), DIAGNOSTICS_SERIAL, RETURN_BUS_MESSAGE_COUNT, resultWord);
+            break;
+        case RETURN_BUS_COMMUNICATION_ERROR_COUNT:
+            LOG_D("Return Bus Communication Error Count\n");
+            if(_bridge != NULL) {
+                resultWord = _bridge->getErrorCount();
+            }
+            response = ModbusMessage(request.getServerID(), DIAGNOSTICS_SERIAL, RETURN_BUS_COMMUNICATION_ERROR_COUNT, resultWord);
+            break;
+        case RETURN_SLAVE_MESSAGE_COUNT:
+            LOG_D("Return Slave Message Count\n");
+            if(_rtu != NULL) {
+                resultWord = _rtu->getMessageCount();
+            }
+            response = ModbusMessage(request.getServerID(), DIAGNOSTICS_SERIAL, RETURN_SLAVE_MESSAGE_COUNT, resultWord);
+            break;
         default: 
-            LOG_D("default: ILLEGAL_FUNCTION");
+            LOG_D("default: ILLEGAL_FUNCTION\n");
             response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_FUNCTION);
     }
 
     return response;
 }
 
-void setupLocalModbus(uint8_t serverID, ModbusBridgeWiFi *bridge, Config *config, WiFiManager *wm) {
+void setupLocalModbus(uint8_t serverID, ModbusClientRTU *rtu, ModbusBridgeWiFi *bridge, Config *config, WiFiManager *wm) {
     String EfuseMac = String(ESP.getEfuseMac(), 16);
     dbgln(EfuseMac);
+    _rtu = rtu;
+    _bridge = bridge;
     bridge->registerWorker(serverID, READ_HOLD_REGISTER, &FC03);
     bridge->registerWorker(serverID, DIAGNOSTICS_SERIAL, &FC08);
     //bridge->registerWorker(serverID, REPORT_SERVER_ID_SERIAL, &FC08);
diff --git a/src/main.cpp b/src/main.cpp
index 8a6abb7..b6b7492 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -59,20 +59,16 @@ boolean WiFiConnect() {
 // Callback for reconnecting
 void WiFiLostIP(WiFiEvent_t event, WiFiEventInfo_t info) {
   dbgln("[WiFi] (possibly) disconnected");
+  wm.disconnect();
+  dbgln("[WiFi] trying to reconnect");
+  MDNS.end();
   
-  if(WiFi.status() != WL_CONNECTED) {
-    dbgln("[WiFi] trying to reconnect");
-    MDNS.end();
-    
-    isConnected = false;
-    isConnected = WiFiConnect();
+  isConnected = false;
+  isConnected = WiFiConnect();
 
-    // Start mDNS
-    if(isConnected) {
-      startMDNS(80, config.getTcpPort());
-    }
-  } else {
-    dbgln("[WiFi] actually is connected");
+  // Start mDNS
+  if(isConnected) {
+    startMDNS(80, config.getTcpPort());
   }
 }
 
@@ -113,7 +109,7 @@ void setup() {
 
   // register worker for local Modbus function
   if(skipAddress) {
-    setupLocalModbus(config.getLocalModbusAddress(), &MBbridge, &config, &wm);
+    setupLocalModbus(config.getLocalModbusAddress(), MBclient, &MBbridge, &config, &wm);
   }
 
   // Start Modbus Bridge
diff --git a/src/pages.cpp b/src/pages.cpp
index 054e375..59bdcd9 100644
--- a/src/pages.cpp
+++ b/src/pages.cpp
@@ -2,8 +2,13 @@
 #include "localmodbus.h"
 #define ETAG "\"" __DATE__ "" __TIME__ "\""
 
+// Remember last slave-ID
+static uint8_t _lastSlaveID;
+
 void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *bridge, Config *config, WiFiManager *wm){
   
+  _lastSlaveID = 1;
+
   server->on("/", HTTP_GET, [](AsyncWebServerRequest *request){
     dbgln("[webserver] GET /");
     auto *response = request->beginResponseStream("text/html");
@@ -26,7 +31,7 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
 
     // show ESP infos...
     sendTableRow(response, "WiFi TX Power (dBm)", String(((float)WiFi.getTxPower())/4, 2));
-    sendTableRow(response, "ESP Temperature (C)", String(temperatureRead(), 2));
+    sendTableRow(response, "ESP Temperature (°C)", String(temperatureRead(), 2));
     sendTableRow(response, "ESP Uptime (sec)", esp_timer_get_time() / 1000000);
     sendTableRow(response, "ESP SSID", WiFi.SSID());
     sendTableRow(response, "ESP RSSI", WiFi.RSSI());
@@ -369,29 +374,40 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
   server->on("/debug", HTTP_GET, [](AsyncWebServerRequest *request){
     dbgln("[webserver] GET /debug");
     auto *response = request->beginResponseStream("text/html");
-    sendResponseHeader(response, "Debug - Read from Device");
-    sendDebugForm(response, "1", "1", "3", "1");
-    //sendButton(response, "Read Device", "/read");
+    sendResponseHeader(response, "Modbus Debug");
+    sendButton(response, "Read (FC01 - FC04)", "debug_read");
+    sendButton(response, "Diagnostic Serial (FC08)", "debug_diagnosticSerial");
     sendButton(response, "Back", "/");
     sendResponseTrailer(response);
     request->send(response);
   });
 
-  // server->on("read", HTTP_GET, [](AsyncWebServerRequest *request){
-  //   dbgln("[webserver] GET /read");
-  //   auto *response = request->beginResponseStream("text/html");
-  //   sendResponseHeader(response, "Debug - Read from Device");
-  //   sendDebugForm(response, "1", "1", "3", "1");
-  //   sendButton(response, "Back", "/debug");
-  //   sendResponseTrailer(response);
-  //   request->send(response);
-  // });
-
-  server->on("/debug", HTTP_POST, [config, rtu, bridge](AsyncWebServerRequest *request){
-    dbgln("[webserver] POST /debug");
+  server->on("/debug_read", HTTP_GET, [](AsyncWebServerRequest *request){
+    dbgln("[webserver] GET /debug_read");
+    auto *response = request->beginResponseStream("text/html");
+    sendResponseHeader(response, "Modbus Debug - Read from Device");
+    sendDebugForm_read(response, String(_lastSlaveID), "1", "3", "1");
+    sendButton(response, "Back", "debug");
+    sendResponseTrailer(response);
+    request->send(response);
+  });
+
+  server->on("/debug_diagnosticSerial", HTTP_GET, [](AsyncWebServerRequest *request){
+    dbgln("[webserver] GET /debug_diagnosticSerial");
+    auto *response = request->beginResponseStream("text/html");
+    sendResponseHeader(response, "Modbus Debug - Diagnostic Serial");
+    sendDebugForm_diagnosticSerial(response, String(_lastSlaveID), "0", "0000");
+    sendButton(response, "Back", "debug");
+    sendResponseTrailer(response);
+    request->send(response);
+  });
+
+  server->on("/debug_read", HTTP_POST, [config, rtu, bridge](AsyncWebServerRequest *request){
+    dbgln("[webserver] POST /debug_read");
     String slaveId = "1";
     if (request->hasParam("slave", true)){
       slaveId = request->getParam("slave", true)->value();
+      _lastSlaveID = (uint8_t) slaveId.toInt();
     }
     String reg = "1";
     if (request->hasParam("reg", true)){
@@ -406,7 +422,7 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
       count = request->getParam("count", true)->value();
     }
     auto *response = request->beginResponseStream("text/html");
-    sendResponseHeader(response, "Debug - Read from Device");
+    sendResponseHeader(response, "Modbus Debug - Read from Device");
     response->print("<pre>");
     auto previous = LOGDEVICE;
     auto previousLevel = MBUlogLvl;
@@ -444,8 +460,77 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
     else{
       response->printf("<span class=\"e\">Error: %#02x (%s)</span>", error, ErrorName(error).c_str());
     }
-    sendDebugForm(response, slaveId, reg, func, count);
-    sendButton(response, "Back", "/");
+    sendDebugForm_read(response, slaveId, reg, func, count);
+    sendButton(response, "Back", "debug");
+    sendResponseTrailer(response);
+    request->send(response);
+  });
+
+  server->on("/debug_diagnosticSerial", HTTP_POST, [config, rtu, bridge](AsyncWebServerRequest *request){
+    dbgln("[webserver] POST /debug_diagnosticSerial");
+    String slaveId = "1";
+    if (request->hasParam("slave", true)){
+      slaveId = request->getParam("slave", true)->value();
+      _lastSlaveID = (uint8_t) slaveId.toInt();
+    }
+    String subFunction = "0";
+    if (request->hasParam("sf", true)){
+      subFunction = request->getParam("sf", true)->value();
+    }
+    String data = "0000";
+    if (request->hasParam("dt", true)){
+      data = request->getParam("dt", true)->value();
+    }
+
+    // convert data string to WORD
+    uint16_t dataWord = 0;
+    if(data.length() != 0) {
+      dataWord = (uint16_t) strtol(data.c_str(), NULL, 16);
+    } 
+    
+    auto *response = request->beginResponseStream("text/html");
+    sendResponseHeader(response, "Modbus Debug - Diagnostic Serial");
+    response->print("<pre>");
+    auto previous = LOGDEVICE;
+    auto previousLevel = MBUlogLvl;
+    auto debug = WebPrint(previous, response);
+    LOGDEVICE = &debug;
+    MBUlogLvl = LOG_LEVEL_DEBUG;
+    ModbusMessage answer;
+    // Call local worker (when enabled)
+    if(slaveId.toInt() == config->getLocalModbusAddress() && config->getLocalModbusEnable()) {
+      MBSworker lclWrkr = bridge->getWorker(slaveId.toInt(), DIAGNOSTICS_SERIAL);
+      if(lclWrkr != NULL) {
+        LOG_D("found local worker... calling for response\n");
+        answer = lclWrkr(ModbusMessage(slaveId.toInt(), DIAGNOSTICS_SERIAL, subFunction.toInt(), dataWord));        
+      } else {
+        LOG_D("no local worker found... answering with ILLEGAL_FUNCTION\n");
+        answer.setError(slaveId.toInt(), DIAGNOSTICS_SERIAL, ILLEGAL_FUNCTION);
+      }
+    } else {
+      // Call via RTU
+      answer = rtu->syncRequest(0xdeadbeef, slaveId.toInt(), DIAGNOSTICS_SERIAL, subFunction.toInt(), dataWord);   
+    }    
+    MBUlogLvl = previousLevel;
+    LOGDEVICE = previous;
+    response->print("</pre>");
+    auto error = answer.getError();
+    if (error == SUCCESS){
+      auto count = answer.size() - 2;
+      if(count < 0) {
+        count = 0;
+      }
+      response->print("<span >Answer: 0x");
+      for (size_t i = 0; i < count; i++) {
+        response->printf("%02x", answer[i + 2]);
+      }      
+      response->print("</span>");
+    }
+    else {
+      response->printf("<span class=\"e\">Error: %#02x (%s)</span>", error, ErrorName(error).c_str());
+    }
+    sendDebugForm_diagnosticSerial(response, slaveId, subFunction, data);
+    sendButton(response, "Back", "debug");
     sendResponseTrailer(response);
     request->send(response);
 
@@ -540,6 +625,7 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
     sendResponseTrailer(response);
     request->send(response);
   });
+
   server->on("/wifi", HTTP_POST, [wm](AsyncWebServerRequest *request){
     dbgln("[webserver] POST /wifi");
     request->redirect("/");
@@ -549,10 +635,12 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
     ESP.restart();
     dbgln("[webserver] rebooted...");
   });
+
   server->on("/favicon.ico", [](AsyncWebServerRequest *request){
     dbgln("[webserver] GET /favicon.ico");
     request->send(204);//TODO add favicon
   });
+
   server->on("/style.css", [](AsyncWebServerRequest *request){
     if (request->hasHeader("If-None-Match")){
       auto header = request->getHeader("If-None-Match");
@@ -588,6 +676,7 @@ void setupPages(AsyncWebServer *server, ModbusClientRTU *rtu, ModbusBridgeWiFi *
     response->addHeader("ETag", ETAG);
     request->send(response);
   });
+
   server->onNotFound([](AsyncWebServerRequest *request){
     dbg("[webserver] request to "); dbg(request->url()); dbgln("not found");
     request->send(404, "text/plain", "404");
@@ -672,7 +761,55 @@ void sendTableRow(AsyncResponseStream *response, const char *name, uint32_t valu
       "</tr>", name, value);
 }
 
-void sendDebugForm(AsyncResponseStream *response, String slaveId, String reg, String function, String count){
+void sendDebugForm_diagnosticSerial(AsyncResponseStream *response, String slaveId, String subFunction, String data) {
+    response->print("<form method=\"post\">");
+    response->print("<table>"
+      "<tr>"
+        "<td>"
+          "<label for=\"slave\">Slave ID</label>"
+        "</td>"
+        "<td>");
+    response->printf("<input type=\"number\" min=\"0\" max=\"247\" id=\"slave\" name=\"slave\" value=\"%s\">", slaveId.c_str());
+    response->print("</td>"
+        "</tr>"
+        "<tr>"
+          "<td>"
+            "<label for=\"func\">Function</label>"
+          "</td>"
+          "<td>");
+    response->printf("<select id=\"sf\" name=\"sf\" data-value=\"%s\">", subFunction.c_str());
+    response->print("<option value=\"0\">00 Return Query Data</option>"
+              "<option value=\"1\">01 Restart Communications</option>"
+              "<option value=\"2\">02 Return Diagnostic Register</option>"
+              "<option value=\"10\">10 Clear Counters and Diagnostic Register</option>"
+              "<option value=\"11\">11 Return Bus Message Count</option>"
+              "<option value=\"12\">12 Return Bus Communication Error Count</option>" 
+              "<option value=\"14\">14 Return Slave Message Count</option>"             
+            "</select>"
+          "</td>"
+        "</tr>"
+        "<tr>"
+          "<td>"
+            "<label for=\"reg\">Data 0x:</label>"
+          "</td>"
+          "<td>");
+    response->printf("<input type=\"text\" minlength=\"0\" maxlength=\"4\" id=\"dt\" name=\"dt\" value=\"%s\">", data.c_str());
+    response->print("</td>"
+        "</tr>"
+      "</table>");
+    response->print("<button class=\"r\">Make it so</button>"
+      "</form>"
+      "<p></p>");
+    response->print("<script>"
+      "(function(){"
+        "var s = document.querySelectorAll('select[data-value]');"
+        "for(d of s){"
+          "d.querySelector(`option[value='${d.dataset.value}']`).selected=true"
+      "}})();"
+      "</script>");
+}
+
+void sendDebugForm_read(AsyncResponseStream *response, String slaveId, String reg, String function, String count) {
     response->print("<form method=\"post\">");
     response->print("<table>"
       "<tr>"