From b16885cfe1db4e39e48d6024bd984383297ed608 Mon Sep 17 00:00:00 2001 From: budulinek Date: Tue, 16 Jan 2024 00:30:26 +0100 Subject: [PATCH] Bugfix Modbus RTU Request form, code comments --- README.md | 6 +- .../01-interfaces.ino | 149 +++++++------- .../02-modbus-tcp.ino | 27 ++- .../04-webserver.ino | 60 +++--- arduino-modbus-rtu-tcp-gateway/05-pages.ino | 187 +++++++++++++----- .../arduino-modbus-rtu-tcp-gateway.ino | 8 +- 6 files changed, 280 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index c739b99..4f889be 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Allows you to connect your Modbus devices (such as sensors, energy meters, HVAC - all web interface inputs have proper validation - factory defaults for user settings can be specified in advanced_settings.h - settings marked \* are only available if ENABLE_DHCP is defined in the sketch - - settings marked \*\* are only available if ENABLE_EXTRA_DIAG is defined in the sketch + - settings marked \*\* are only available if ENABLE_EXTENDED_WEBUI is defined in the sketch * advanced settings: - can be changed in sketch (advanced_settings.h) - stored in flash memory @@ -98,7 +98,7 @@ Connect your Arduino to ethernet and use your web browser to access the web inte Enjoy :-) # Settings - settings marked \* are only available if ENABLE_DHCP is defined in the sketch - - settings marked \*\* are only available if ENABLE_EXTRA_DIAG is defined in the sketch + - settings marked \*\* are only available if ENABLE_EXTENDED_WEBUI is defined in the sketch ## System Info modbus1 @@ -260,7 +260,7 @@ The number of used sockets is determined (by the Ethernet.h library) based on mi ## Memory -Not everything could fit into the limited flash memory of Arduino Nano / Uno. If you have a microcontroller with more memory (such as Mega), you can enable extra settings in the main sketch by defining ENABLE_DHCP and/or ENABLE_EXTRA_DIAG in advanced settings. +Not everything could fit into the limited flash memory of Arduino Nano / Uno. If you have a microcontroller with more memory (such as Mega), you can enable extra settings in the main sketch by defining ENABLE_DHCP and/or ENABLE_EXTENDED_WEBUI in advanced settings. # Links and Credits diff --git a/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino b/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino index 88310ef..b05f9a9 100644 --- a/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino +++ b/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino @@ -1,51 +1,8 @@ -/* ******************************************************************* - Ethernet and serial interface functions - - startSerial() - - starts HW serial interface which we use for RS485 line - - charTime(), charTimeOut(), frameDelay() - - calculate Modbus RTU character timeout and inter-frame delay - - startEthernet() - - initiates ethernet interface - - if enabled, gets IP from DHCP - - starts all servers (Modbus TCP, UDP, web server) - - resetFunc() - - well... resets Arduino - - maintainDhcp() - - maintain DHCP lease - - maintainUptime() - - maintains up time in case of millis() overflow - - maintainCounters(), rollover() - - synchronizes roll-over of data counters to zero - - resetStats() - - resets Modbus stats - - generateMac() - - generate random MAC using pseudo random generator (faster and than build-in random()) - - manageSockets() - - closes sockets which are waiting to be closed or which refuse to close - - forwards sockets with data available (webserver or Modbus TCP) for further processing - - disconnects (closes) sockets which are too old / idle for too long - - opens new sockets if needed (and if available) - - CreateTrulyRandomSeed() - - seed pseudorandom generator using watch dog timer interrupt (works only on AVR) - - see https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed - - - + preprocessor code for identifying microcontroller board - - ***************************************************************** */ - - +/**************************************************************************/ +/*! + @brief Initiates HW serial interface which we use for the RS485 line. +*/ +/**************************************************************************/ void startSerial() { mySerial.begin((data.config.baud * 100UL), data.config.serialConfig); #ifdef RS485_CONTROL_PIN @@ -54,7 +11,7 @@ void startSerial() { #endif /* RS485_CONTROL_PIN */ } -// number of bits per character (11 in default Modbus RTU settings) +// Number of bits per character (11 in default Modbus RTU settings) byte bitsPerChar() { byte bits = 1 + // start bit @@ -64,7 +21,7 @@ byte bitsPerChar() { return bits; } -// character timeout in micros +// Character timeout in micros uint32_t charTimeOut() { if (data.config.baud <= 192) { return (15000UL * bitsPerChar()) / data.config.baud; // inter-character time-out should be 1,5T @@ -73,7 +30,7 @@ uint32_t charTimeOut() { } } -// minimum frame delay in micros +// Minimum frame delay in micros uint32_t frameDelay() { if (data.config.baud <= 192) { return (35000UL * bitsPerChar()) / data.config.baud; // inter-frame delay should be 3,5T @@ -82,6 +39,12 @@ uint32_t frameDelay() { } } +/**************************************************************************/ +/*! + @brief Initiates ethernet interface, if DHCP enabled, gets IP from DHCP, + starts all servers (UDP, web server). +*/ +/**************************************************************************/ void startEthernet() { if (ETH_RESET_PIN != 0) { pinMode(ETH_RESET_PIN, OUTPUT); @@ -112,8 +75,18 @@ void startEthernet() { #endif } +/**************************************************************************/ +/*! + @brief Resets Arduino (works only on AVR chips). +*/ +/**************************************************************************/ void (*resetFunc)(void) = 0; //declare reset function at address 0 +/**************************************************************************/ +/*! + @brief Checks SPI connection to the W5X00 chip. +*/ +/**************************************************************************/ void checkEthernet() { static byte attempts = 0; IPAddress tempIP = Ethernet.localIP(); @@ -128,6 +101,11 @@ void checkEthernet() { checkEthTimer.sleep(CHECK_ETH_INTERVAL); } +/**************************************************************************/ +/*! + @brief Maintains DHCP lease. +*/ +/**************************************************************************/ #ifdef ENABLE_DHCP void maintainDhcp() { if (data.config.enableDhcp && dhcpSuccess == true) { // only call maintain if initial DHCP request by startEthernet was successfull @@ -136,6 +114,11 @@ void maintainDhcp() { } #endif /* ENABLE_DHCP */ +/**************************************************************************/ +/*! + @brief Maintains uptime in case of millis() overflow. +*/ +/**************************************************************************/ #ifdef ENABLE_EXTENDED_WEBUI void maintainUptime() { uint32_t milliseconds = millis(); @@ -152,8 +135,12 @@ void maintainUptime() { } #endif /* ENABLE_EXTENDED_WEBUI */ +/**************************************************************************/ +/*! + @brief Synchronizes roll-over of data counters to zero. +*/ +/**************************************************************************/ bool rollover() { - // synchronize roll-over of run time, data counters and modbus stats to zero, at 0xFFFFFF00 const uint32_t ROLLOVER = 0xFFFFFF00; for (byte i = 0; i < ERROR_LAST; i++) { if (data.errorCnt[i] > ROLLOVER) { @@ -173,7 +160,11 @@ bool rollover() { return false; } -// resets counters to 0: data.errorCnt, data.rtuCnt, data.ethCnt +/**************************************************************************/ +/*! + @brief Resets error stats, RTU counter and ethernet data counter. +*/ +/**************************************************************************/ void resetStats() { memset(data.errorCnt, 0, sizeof(data.errorCnt)); #ifdef ENABLE_EXTENDED_WEBUI @@ -183,7 +174,12 @@ void resetStats() { #endif /* ENABLE_EXTENDED_WEBUI */ } -// generate new MAC (bytes 0, 1 and 2 are static, bytes 3, 4 and 5 are generated randomly) +/**************************************************************************/ +/*! + @brief Generate random MAC using pseudo random generator, + bytes 0, 1 and 2 are static (MAC_START), bytes 3, 4 and 5 are generated randomly +*/ +/**************************************************************************/ void generateMac() { // Marsaglia algorithm from https://github.com/RobTillaart/randomHelpers seed1 = 36969L * (seed1 & 65535L) + (seed1 >> 16); @@ -196,21 +192,29 @@ void generateMac() { } } +/**************************************************************************/ +/*! + @brief Write (update) data to Arduino EEPROM. +*/ +/**************************************************************************/ void updateEeprom() { eepromTimer.sleep(EEPROM_INTERVAL * 60UL * 60UL * 1000UL); // EEPROM_INTERVAL is in hours, sleep is in milliseconds! data.eepromWrites++; // we assume that at least some bytes are written to EEPROM during EEPROM.update or EEPROM.put EEPROM.put(DATA_START, data); } -#if MAX_SOCK_NUM == 8 -uint32_t lastSocketUse[MAX_SOCK_NUM] = { 0, 0, 0, 0, 0, 0, 0, 0 }; -byte socketInQueue[MAX_SOCK_NUM] = { 0, 0, 0, 0, 0, 0, 0, 0 }; -#elif MAX_SOCK_NUM == 4 -uint32_t lastSocketUse[MAX_SOCK_NUM] = { 0, 0, 0, 0 }; -byte socketInQueue[MAX_SOCK_NUM] = { 0, 0, 0, 0 }; -#endif -// from https://github.com/SapientHetero/Ethernet/blob/master/src/socket.cpp +uint32_t lastSocketUse[MAX_SOCK_NUM]; +byte socketInQueue[MAX_SOCK_NUM]; +/**************************************************************************/ +/*! + @brief Closes sockets which are waiting to be closed or which refuse to close, + forwards sockets with data available for further processing by the webserver, + disconnects (closes) sockets which are too old (idle for too long), opens + new sockets if needed (and if available). + From https://github.com/SapientHetero/Ethernet/blob/master/src/socket.cpp +*/ +/**************************************************************************/ void manageSockets() { uint32_t maxAge = 0; // the 'age' of the socket in a 'disconnectable' state that was last used the longest time ago byte oldest = MAX_SOCK_NUM; // the socket number of the 'oldest' disconnectable socket @@ -308,6 +312,12 @@ void manageSockets() { // we do not need SPI.beginTransaction(SPI_ETHERNET_SETTINGS) or SPI.endTransaction() ?? } +/**************************************************************************/ +/*! + @brief Disconnect or close a socket. + @param s Socket number. +*/ +/**************************************************************************/ void disconSocket(byte s) { if (W5100.readSnSR(s) == SnSR::ESTABLISHED) { W5100.execCmdSn(s, Sock_DISCON); // Sock_DISCON does not close LISTEN sockets @@ -317,7 +327,13 @@ void disconSocket(byte s) { } } -// https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed + +/**************************************************************************/ +/*! + @brief Seed pseudorandom generator using watch dog timer interrupt (works only on AVR). + See https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed +*/ +/**************************************************************************/ void CreateTrulyRandomSeed() { seed1 = 0; nrot = 32; // Must be at least 4, but more increased the uniformity of the produced seeds entropy. @@ -344,11 +360,9 @@ ISR(WDT_vect) { seed1 = seed1 ^ TCNT1L; } -// Board definitions +// Preprocessor code for identifying microcontroller board #if defined(TEENSYDUINO) - // --------------- Teensy ----------------- - #if defined(__AVR_ATmega32U4__) #define BOARD F("Teensy 2.0") #elif defined(__AVR_AT90USB1286__) @@ -366,9 +380,7 @@ ISR(WDT_vect) { #else #define BOARD F("Unknown Board") #endif - #else // --------------- Arduino ------------------ - #if defined(ARDUINO_AVR_ADK) #define BOARD F("Arduino Mega Adk") #elif defined(ARDUINO_AVR_BT) // Bluetooth @@ -422,5 +434,4 @@ ISR(WDT_vect) { #else #define BOARD F("Unknown Board") #endif - #endif diff --git a/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino b/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino index 328657d..f18a867 100644 --- a/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino +++ b/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino @@ -39,6 +39,11 @@ byte masks[8] = { 1, 2, 4, 8, 16, 32, 64, 128 }; uint16_t crc; +/**************************************************************************/ +/*! + @brief Receives Modbus UDP (or Modbus RTU over UDP) messages, calls @ref checkRequest() +*/ +/**************************************************************************/ void recvUdp() { uint16_t msgLength = Udp.parsePacket(); if (msgLength) { @@ -80,6 +85,11 @@ void recvUdp() { } } +/**************************************************************************/ +/*! + @brief Receives Modbus TCP (or Modbus RTU over TCP) messages, calls @ref checkRequest() +*/ +/**************************************************************************/ void recvTcp(EthernetClient &client) { uint16_t msgLength = client.available(); #ifdef ENABLE_EXTENDED_WEBUI @@ -150,6 +160,19 @@ void scanRequest() { } } +/**************************************************************************/ +/*! + @brief Checks Modbus TCP/UDP requests (correct MBAP header, + CRC in case of Modbus RTU over TCP/UDP), checks availability of queue, + stores requests into queue or returns an error. + @param inBuffer Modbus TCP/UDP requests + @param msgLength Length of the Modbus TCP/UDP requests + @param remoteIP Remote IP + @param remotePort Remote port + @param requestType UDP or TCP, priority or scan request + @return Modbus error code to be sent back to the recipient. +*/ +/**************************************************************************/ byte checkRequest(byte inBuffer[], uint16_t msgLength, const uint32_t remoteIP, const uint16_t remotePort, byte requestType) { byte addressPos = 6 * !data.config.enableRtuOverTcp; // position of slave address in the incoming TCP/UDP message (0 for Modbus RTU over TCP/UDP and 6 for Modbus RTU over TCP/UDP) if (data.config.enableRtuOverTcp) { // check CRC for Modbus RTU over TCP/UDP @@ -167,12 +190,12 @@ byte checkRequest(byte inBuffer[], uint16_t msgLength, const uint32_t remoteIP, // check if we have space in request queue if (queueHeaders.available() < 1 || queueData.available() < msgLength) { setSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0A, true, false); - return 0x0A; // return modbus error 0x0A (Gateway Overloaded) + return 0x0A; // return Modbus error code 10 (Gateway Overloaded) } // allow only one request to non responding slaves if (getSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0B_QUEUE)) { data.errorCnt[SLAVE_ERROR_0B]++; - return 0x0B; // return modbus error 11 (Gateway Target Device Failed to Respond) + return 0x0B; // return Modbus error code 11 (Gateway Target Device Failed to Respond) } else if (getSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0B)) { setSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0B_QUEUE, true, false); } else { diff --git a/arduino-modbus-rtu-tcp-gateway/04-webserver.ino b/arduino-modbus-rtu-tcp-gateway/04-webserver.ino index 2068ca8..c5ab7be 100644 --- a/arduino-modbus-rtu-tcp-gateway/04-webserver.ino +++ b/arduino-modbus-rtu-tcp-gateway/04-webserver.ino @@ -1,24 +1,3 @@ -/* ******************************************************************* - Webserver functions - - recvWeb() - - receives GET requests for web pages - - receives POST data from web forms - - calls processPost - - sends web pages, for simplicity, all web pages should are numbered (1.htm, 2.htm, ...), the page number is passed to sendPage() function - - executes actions (such as ethernet restart, reboot) during "please wait" web page - - processPost() - - processes POST data from forms and buttons - - updates data.config (in RAM) - - saves config into EEPROM - - executes actions which do not require webserver restart - - strToByte(), hex() - - helper functions for parsing and writing hex data - - ***************************************************************** */ - const byte URI_SIZE = 24; // a smaller buffer for uri const byte POST_SIZE = 24; // a smaller buffer for single post parameter + key @@ -49,7 +28,7 @@ enum page : byte { PAGE_RTU, PAGE_TOOLS, PAGE_WAIT, // page with "Reloading. Please wait..." message. - PAGE_DATA, // data.json + PAGE_DATA, // d.json }; // Keys for POST parameters, used in web forms and processed by processPost() function. @@ -121,6 +100,16 @@ enum JSON_type : byte { JSON_LAST, // Must be the very last element in this array }; +/**************************************************************************/ +/*! + @brief Receives GET requests for web pages, receives POST data from web forms, + calls @ref processPost() function, sends web pages. For simplicity, all web pages + should are numbered (1.htm, 2.htm, ...), the page number is passed to + the @ref sendPage() function. Also executes actions (such as ethernet restart, + reboot) during "please wait" web page. + @param client Ethernet TCP client. +*/ +/**************************************************************************/ void recvWeb(EthernetClient &client) { char uri[URI_SIZE]; // the requested page memset(uri, 0, sizeof(uri)); @@ -195,8 +184,13 @@ void recvWeb(EthernetClient &client) { action = ACT_NONE; } -// This function stores POST parameter values in data.config. -// Most changes are saved and applied immediatelly, some changes (IP settings, web server port, reboot) are saved but applied later after "please wait" page is sent. +/**************************************************************************/ +/*! + @brief Processes POST data from forms and buttons, updates data.config (in RAM) + and saves config into EEPROM. Executes actions which do not require webserver restart + @param client Ethernet TCP client. +*/ +/**************************************************************************/ void processPost(EthernetClient &client) { while (client.available()) { char post[POST_SIZE]; @@ -367,7 +361,7 @@ void processPost(EthernetClient &client) { break; } // if new Modbus request received, put into queue - if (requestLen > 1 && queueHeaders.available() > 1 && queueData.available() > requestLen) { // at least 2 bytes in request (slave address and function) + if (action != ACT_SCAN && action != ACT_RESET_STATS && requestLen > 1 && queueHeaders.available() > 1 && queueData.available() > requestLen) { // at least 2 bytes in request (slave address and function) // push to queue queueHeaders.push(header_t{ { 0x00, 0x00 }, // tid[2] @@ -386,7 +380,13 @@ void processPost(EthernetClient &client) { updateEeprom(); // it is safe to call, only changed values (and changed error and data counters) are updated } -// takes 2 chars, 1 char + null byte or 1 null byte +/**************************************************************************/ +/*! + @brief Parses string and returns single byte. + @param myStr String (2 chars, 1 char + null or 1 null) to be parsed. + @return Parsed byte. +*/ +/**************************************************************************/ byte strToByte(const char myStr[]) { if (!myStr) return 0; byte x = 0; @@ -406,8 +406,14 @@ byte strToByte(const char myStr[]) { return x; } -// from https://github.com/RobTillaart/printHelpers char __printbuffer[3]; +/**************************************************************************/ +/*! + @brief Converts byte to char string, from https://github.com/RobTillaart/printHelpers + @param val Byte to be conferted. + @return Char string. +*/ +/**************************************************************************/ char *hex(byte val) { char *buffer = __printbuffer; byte digits = 2; diff --git a/arduino-modbus-rtu-tcp-gateway/05-pages.ino b/arduino-modbus-rtu-tcp-gateway/05-pages.ino index a70eaae..524a887 100644 --- a/arduino-modbus-rtu-tcp-gateway/05-pages.ino +++ b/arduino-modbus-rtu-tcp-gateway/05-pages.ino @@ -1,46 +1,19 @@ -/* ******************************************************************* - Pages for Webserver - - sendPage() - - sends the requested page (incl. 404 error and JSON document) - - displays main page, renders title and left menu using
- - calls content functions depending on the number (i.e. URL) of the requested web page - - also displays buttons for some of the pages - - in order to save flash memory, some HTML closing tags are omitted, new lines in HTML code are also omitted - - contentInfo(), contentStatus(), contentIp(), contentTcp(), contentRtu(), contentTools() - - render the content of the requested page - - contentWait() - - renders the "please wait" message instead of the content, will be forwarded to home page after 5 seconds - - tagInputNumber(), tagLabelDiv(), tagButton(), tagDivClose(), tagSpan() - - render snippets of repetitive HTML code for ,
" "")); //
} +/**************************************************************************/ +/*! + @brief + + @param chunked Chunked buffer + @param JSONKEY JSON_ id +*/ +/**************************************************************************/ void tagSpan(ChunkedPrint &chunked, const byte JSONKEY) { chunked.print(F("")); } -// Menu item strings +/**************************************************************************/ +/*! + @brief Menu item strings + + @param chunked Chunked buffer + @param item Page number +*/ +/**************************************************************************/ void stringPageName(ChunkedPrint &chunked, byte item) { switch (item) { case PAGE_INFO: @@ -648,6 +727,14 @@ void stringStats(ChunkedPrint &chunked, const byte stat) { chunked.print(F("
")); } +/**************************************************************************/ +/*! + @brief Provide JSON value to a corresponding JSON key. The value is printed + in and in JSON document fetched on the background. + @param chunked Chunked buffer + @param JSONKEY JSON key +*/ +/**************************************************************************/ void jsonVal(ChunkedPrint &chunked, const byte JSONKEY) { switch (JSONKEY) { #ifdef ENABLE_EXTENDED_WEBUI diff --git a/arduino-modbus-rtu-tcp-gateway/arduino-modbus-rtu-tcp-gateway.ino b/arduino-modbus-rtu-tcp-gateway/arduino-modbus-rtu-tcp-gateway.ino index 53c9d07..aaca94c 100644 --- a/arduino-modbus-rtu-tcp-gateway/arduino-modbus-rtu-tcp-gateway.ino +++ b/arduino-modbus-rtu-tcp-gateway/arduino-modbus-rtu-tcp-gateway.ino @@ -24,9 +24,10 @@ v7.1 2023-08-25 Simplify EEPROM read and write, Tools page v7.2 2023-10-20 Disable DHCP renewal fallback, better advanced_settings.h layout ENABLE_EXTENDED_WEBUI and ENABLE_DHCP is set by default for Mega + v7.3 2024-01-16 Bugfix Modbus RTU Request form, code comments */ -const byte VERSION[] = { 7, 2 }; +const byte VERSION[] = { 7, 3 }; #include #include @@ -130,11 +131,6 @@ CircularBuffer queueData; // queue of PDU data /****** ETHERNET AND SERIAL ******/ -#ifdef UDP_TX_PACKET_MAX_SIZE -#undef UDP_TX_PACKET_MAX_SIZE -#define UDP_TX_PACKET_MAX_SIZE MODBUS_SIZE -#endif - byte maxSockNum = MAX_SOCK_NUM; #ifdef ENABLE_DHCP