From 0152d0307919f0c01ad8ee3fa86bc0f0b7895f59 Mon Sep 17 00:00:00 2001 From: budulinek Date: Sat, 18 Feb 2023 13:28:46 +0100 Subject: [PATCH 1/5] Modbus requests via WebUI --- .../01-interfaces.ino | 152 ++++- .../02-modbus-tcp.ino | 145 ++-- .../03-modbus-rtu.ino | 54 +- .../04-webserver.ino | 407 ++++++----- arduino-modbus-rtu-tcp-gateway/05-pages.ino | 641 ++++++++++-------- .../arduino-modbus-rtu-tcp-gateway.ino | 97 +-- 6 files changed, 908 insertions(+), 588 deletions(-) diff --git a/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino b/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino index 63ffb6f..ee66752 100644 --- a/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino +++ b/arduino-modbus-rtu-tcp-gateway/01-interfaces.ino @@ -35,15 +35,7 @@ void startSerial() { - mySerial.flush(); - mySerial.end(); - queueHeaders.clear(); // <- eats memory! - queueData.clear(); - scanReqInQueue = false; - priorityReqInQueue= false; - memset(stat[STAT_ERROR_0B_QUEUE], 0, sizeof(stat[STAT_ERROR_0B_QUEUE])); - sendTimer.sleep(0); - mySerial.begin(localConfig.baud, localConfig.serialConfig); + mySerial.begin((localConfig.baud * 100UL), localConfig.serialConfig); #ifdef RS485_CONTROL_PIN pinMode(RS485_CONTROL_PIN, OUTPUT); digitalWrite(RS485_CONTROL_PIN, RS485_RECEIVE); // Init Transceiver @@ -57,12 +49,12 @@ unsigned long charTime() { (((localConfig.serialConfig & 0x06) >> 1) + 5) + // data bits (((localConfig.serialConfig & 0x08) >> 3) + 1); // stop bits if (((localConfig.serialConfig & 0x30) >> 4) > 1) bits += 1; // parity bit (if present) - return (bits * 1000000UL) / (unsigned long)localConfig.baud; + return (bits * 10000UL) / (unsigned long)localConfig.baud; } // character timeout unsigned long charTimeOut() { - if (localConfig.baud <= 19200UL) { + if (localConfig.baud <= 192) { return 1.5 * charTime(); // inter-character time-out should be 1,5T } else { return 750; @@ -71,7 +63,7 @@ unsigned long charTimeOut() { // minimum frame delay unsigned long frameDelay() { - if (localConfig.baud <= 19200UL) { + if (localConfig.baud <= 192) { return 3.5 * charTime(); // inter-frame delay should be 3,5T } else { return 1750; // 1750 μs @@ -99,14 +91,16 @@ void startEthernet() { #else /* ENABLE_DHCP */ Ethernet.begin(mac, localConfig.ip, {}, localConfig.gateway, localConfig.subnet); // No DNS #endif /* ENABLE_DHCP */ + Ethernet.setRetransmissionTimeout(TCP_RETRANSMISSION_TIMEOUT); + Ethernet.setRetransmissionCount(TCP_RETRANSMISSION_COUNT); modbusServer = EthernetServer(localConfig.tcpPort); webServer = EthernetServer(localConfig.webPort); Udp.begin(localConfig.udpPort); modbusServer.begin(); webServer.begin(); - if (Ethernet.hardwareStatus() == EthernetW5100) { - maxSockNum = 4; // W5100 chip never supports more than 4 sockets - } +#if MAX_SOCK_NUM > 4 + if (W5100.getChip() == 51) maxSockNum = 4; // W5100 chip never supports more than 4 sockets +#endif } void (*resetFunc)(void) = 0; //declare reset function at address 0 @@ -123,6 +117,7 @@ void maintainDhcp() { } #endif /* ENABLE_DHCP */ +#ifdef ENABLE_EXTRA_DIAG void maintainUptime() { unsigned long milliseconds = millis(); if (last_milliseconds > milliseconds) { @@ -136,19 +131,23 @@ void maintainUptime() { //We add the "remaining_seconds", so that we can continue measuring the time passed from the last boot of the device. seconds = (milliseconds / 1000) + remaining_seconds; } +#endif /* ENABLE_EXTRA_DIAG */ bool rollover() { // synchronize roll-over of run time, data counters and modbus stats to zero, at 0xFFFFFF00 const unsigned long ROLLOVER = 0xFFFFFF00; - for (byte i = 0; i < STAT_ERROR_0B_QUEUE; i++) { // there is no counter for STAT_ERROR_0B_QUEUE + for (byte i = 0; i < SLAVE_ERROR_0B_QUEUE; i++) { // there is no counter for SLAVE_ERROR_0B_QUEUE if (errorCount[i] > ROLLOVER) { return true; } } - if (errorTcpCount > ROLLOVER || errorRtuCount > ROLLOVER || errorTimeoutCount > ROLLOVER || seconds > ROLLOVER) { + if (errorTcpCount > ROLLOVER || errorRtuCount > ROLLOVER || errorTimeoutCount > ROLLOVER) { return true; } #ifdef ENABLE_EXTRA_DIAG + if (seconds > ROLLOVER) { + return true; + } if (serialTxCount > ROLLOVER || serialRxCount > ROLLOVER || ethTxCount > ROLLOVER || ethRxCount > ROLLOVER) { return true; } @@ -161,8 +160,8 @@ void resetStats() { errorTcpCount = 0; errorRtuCount = 0; errorTimeoutCount = 0; - remaining_seconds = -(millis() / 1000); #ifdef ENABLE_EXTRA_DIAG + remaining_seconds = -(millis() / 1000); ethRxCount = 0; ethTxCount = 0; serialRxCount = 0; @@ -181,6 +180,123 @@ void generateMac() { } } + + +#if MAX_SOCK_NUM == 8 +unsigned long lastSocketUse[MAX_SOCK_NUM] = { 0, 0, 0, 0, 0, 0, 0, 0 }; // +rs 03Feb2019 - records last interaction involving each socket to enable detecting sockets unused for longest time period +byte socketInQueue[MAX_SOCK_NUM] = { 0, 0, 0, 0, 0, 0, 0, 0 }; +#elif MAX_SOCK_NUM == 4 +unsigned long lastSocketUse[MAX_SOCK_NUM] = { 0, 0, 0, 0 }; // +rs 03Feb2019 - records last interaction involving each socket to enable detecting sockets unused for longest time period +byte socketInQueue[MAX_SOCK_NUM] = { 0, 0, 0, 0 }; +#endif + +// 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 + byte modbusListening = MAX_SOCK_NUM; + byte webListening = MAX_SOCK_NUM; + byte dataAvailable = MAX_SOCK_NUM; + byte socketsAvailable = 0; + // SPI.beginTransaction(SPI_ETHERNET_SETTINGS); // begin SPI transaction + // look at all the hardware sockets, record and take action based on current states + for (byte s = 0; s < maxSockNum; s++) { // for each hardware socket ... + byte status = W5100.readSnSR(s); // get socket status... + uint32_t sockAge = millis() - lastSocketUse[s]; // age of the current socket + if (socketInQueue[s] > 0) { + lastSocketUse[s] = millis(); + continue; // do not close Modbus TCP sockets currently processed (in queue) + } + + switch (status) { + case SnSR::CLOSED: + { + socketsAvailable++; + } + break; + case SnSR::LISTEN: + case SnSR::SYNRECV: + { + lastSocketUse[s] = millis(); + if (W5100.readSnPORT(s) == localConfig.webPort) { + webListening = s; + } else { + modbusListening = s; + } + } + break; + case SnSR::FIN_WAIT: + case SnSR::CLOSING: + case SnSR::TIME_WAIT: + case SnSR::LAST_ACK: + { + socketsAvailable++; // socket will be available soon + if (sockAge > TCP_DISCON_TIMEOUT) { // if it's been more than TCP_CLIENT_DISCON_TIMEOUT since disconnect command was sent... + W5100.execCmdSn(s, Sock_CLOSE); // send CLOSE command... + lastSocketUse[s] = millis(); // and record time at which it was sent so we don't do it repeatedly. + } + } + break; + case SnSR::ESTABLISHED: + case SnSR::CLOSE_WAIT: + { + if (EthernetClient(s).available() > 0) { + dataAvailable = s; + lastSocketUse[s] = millis(); + } else { + // remote host closed connection, our end still open + if (status == SnSR::CLOSE_WAIT) { + socketsAvailable++; // socket will be available soon + W5100.execCmdSn(s, Sock_DISCON); // send DISCON command... + lastSocketUse[s] = millis(); // record time at which it was sent... + // status becomes LAST_ACK for short time + } else if (((W5100.readSnPORT(s) == localConfig.webPort && sockAge > TCP_WEB_DISCON_AGE) + || (W5100.readSnPORT(s) == localConfig.tcpPort && sockAge > (localConfig.tcpTimeout * 1000UL))) + && sockAge > maxAge) { + oldest = s; // record the socket number... + maxAge = sockAge; // and make its age the new max age. + } + } + } + break; + default: + break; + } + } + + if (dataAvailable != MAX_SOCK_NUM) { + EthernetClient client = EthernetClient(dataAvailable); + if (W5100.readSnPORT(dataAvailable) == localConfig.webPort) { + recvWeb(client); + } else { + recvTcp(client); + } + } + + if (modbusListening == MAX_SOCK_NUM) { + modbusServer.begin(); + } else if (webListening == MAX_SOCK_NUM) { + webServer.begin(); + } + + // If needed, disconnect socket that's been idle (ESTABLISHED without data recieved) the longest + if (oldest != MAX_SOCK_NUM && socketsAvailable == 0 && (webListening == MAX_SOCK_NUM || modbusListening == MAX_SOCK_NUM)) { + disconSocket(oldest); + } + + // SPI.endTransaction(); // Serves to o release the bus for other devices to access it. Since the ethernet chip is the only device + // we do not need SPI.beginTransaction(SPI_ETHERNET_SETTINGS) or SPI.endTransaction() +} + +void disconSocket(byte s) { + if (W5100.readSnSR(s) == SnSR::ESTABLISHED) { + W5100.execCmdSn(s, Sock_DISCON); // Sock_DISCON does not close LISTEN sockets + lastSocketUse[s] = millis(); // record time at which it was sent... + } else { + W5100.execCmdSn(s, Sock_CLOSE); // send DISCON command... + } +} + // https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed void CreateTrulyRandomSeed() { seed1 = 0; diff --git a/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino b/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino index 25e355b..db93127 100644 --- a/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino +++ b/arduino-modbus-rtu-tcp-gateway/02-modbus-tcp.ino @@ -25,15 +25,16 @@ ***************************************************************** */ +uint8_t masks[8] = { 1, 2, 4, 8, 16, 32, 64, 128 }; + // Stored in "header.requestType" #define PRIORITY_REQUEST B10000000 // Request to slave which is not "nonresponding" #define SCAN_REQUEST B01000000 // Request triggered by slave scanner #define UDP_REQUEST B00100000 // UDP request -#define TCP_REQUEST B00001111 // TCP request, also stores TCP client number - -uint8_t masks[8] = { 1, 2, 4, 8, 16, 32, 64, 128 }; +#define TCP_REQUEST B00001000 // TCP request +#define TCP_REQUEST_MASK B00000111 // Stores TCP client number -byte addressPos; +uint16_t crc; void recvUdp() { unsigned int msgLength = Udp.parsePacket(); @@ -42,11 +43,11 @@ void recvUdp() { ethRxCount += msgLength; #endif /* ENABLE_EXTRA_DIAG */ byte inBuffer[MODBUS_SIZE + 4]; // Modbus TCP frame is 4 bytes longer than Modbus RTU frame - // Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address)..... no CRC - // Modbus RTU frame: [0] address.....[n-1][n] CRC + // Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address)..... no CRC + // Modbus RTU frame: [0] address.....[n-1][n] CRC Udp.read(inBuffer, sizeof(inBuffer)); - Udp.flush(); - byte errorCode = checkRequest(inBuffer, msgLength, (IPAddress)Udp.remoteIP(), Udp.remotePort(), UDP_REQUEST); + while (Udp.available()) Udp.read(); + byte errorCode = checkRequest(inBuffer, msgLength, (uint32_t)Udp.remoteIP(), Udp.remotePort(), UDP_REQUEST); if (errorCode) { // send back message with error code Udp.beginPacket(Udp.remoteIP(), Udp.remotePort()); @@ -75,68 +76,76 @@ void recvUdp() { } } -void recvTcp() { - EthernetClient client = modbusServer.available(); - if (client) { - unsigned int msgLength = client.available(); +void recvTcp(EthernetClient &client) { + unsigned int msgLength = client.available(); #ifdef ENABLE_EXTRA_DIAG - ethRxCount += msgLength; -#endif /* ENABLE_EXTRA_DIAG */ - byte inBuffer[MODBUS_SIZE + 4]; // Modbus TCP frame is 4 bytes longer than Modbus RTU frame - // Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address)..... - // Modbus RTU frame: [0] address..... - client.read(inBuffer, sizeof(inBuffer)); - client.flush(); - byte errorCode = checkRequest(inBuffer, msgLength, (IPAddress){}, 0, client.getSocketNumber()); - if (errorCode) { - // send back message with error code - if (!localConfig.enableRtuOverTcp) { - client.write(inBuffer, 5); - client.write(0x03); - } - byte addressPos = 6 * !localConfig.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) - client.write(inBuffer[addressPos]); // address - client.write(inBuffer[addressPos + 1] + 0x80); // function + 0x80 - client.write(errorCode); - if (localConfig.enableRtuOverTcp) { - crc = 0xFFFF; - calculateCRC(inBuffer[addressPos]); - calculateCRC(inBuffer[addressPos + 1] + 0x80); - calculateCRC(errorCode); - client.write(lowByte(crc)); // send CRC, low byte first - client.write(highByte(crc)); - } + ethRxCount += msgLength; +#endif /* ENABLE_EXTRA_DIAG */ + byte inBuffer[MODBUS_SIZE + 4]; // Modbus TCP frame is 4 bytes longer than Modbus RTU frame + // Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address)..... + // Modbus RTU frame: [0] address..... + client.read(inBuffer, sizeof(inBuffer)); + while (client.available()) client.read(); + byte errorCode = checkRequest(inBuffer, msgLength, {}, client.remotePort(), TCP_REQUEST | client.getSocketNumber()); + if (errorCode) { + // send back message with error code + byte i = 0; + byte outBuffer[9]; + if (!localConfig.enableRtuOverTcp) { + memcpy(outBuffer, inBuffer, 5); + outBuffer[5] = 0x03; + i = 6; + } + byte addressPos = 6 * !localConfig.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) + outBuffer[i++] = inBuffer[addressPos]; // address + outBuffer[i++] = inBuffer[addressPos + 1] + 0x80; // function + 0x80 + outBuffer[i++] = errorCode; + if (localConfig.enableRtuOverTcp) { + crc = 0xFFFF; + calculateCRC(inBuffer[addressPos]); + calculateCRC(inBuffer[addressPos + 1] + 0x80); + calculateCRC(errorCode); + outBuffer[i++] = lowByte(crc); // send CRC, low byte first + outBuffer[i++] = highByte(crc); + } + client.write(outBuffer, i); #ifdef ENABLE_EXTRA_DIAG - ethTxCount += 5; - if (!localConfig.enableRtuOverTcp) ethTxCount += 4; + ethTxCount += 5; + if (!localConfig.enableRtuOverTcp) ethTxCount += 4; #endif /* ENABLE_EXTRA_DIAG */ - } } } + void scanRequest() { // Insert scan request into queue, allow only one scan request in a queue - if (scanCounter != 0 && queueHeaders.available() > 1 && queueData.available() > sizeof(SCAN_COMMAND) + 1 && scanReqInQueue == false) { + static byte scanCommand[] = { SCAN_FUNCTION_FIRST, 0x00, SCAN_DATA_ADDRESS, 0x00, 0x01 }; + if (scanCounter != 0 && queueHeaders.available() > 1 && queueData.available() > sizeof(scanCommand) + 1 && scanReqInQueue == false) { scanReqInQueue = true; // Store scan request in request queue queueHeaders.push(header{ - { 0x00, 0x00 }, // tid[2] - sizeof(SCAN_COMMAND) + 1, // msgLen - {}, // remIP - 0, // remPort - SCAN_REQUEST, // requestType - 0, // atts + { 0x00, 0x00 }, // tid[2] + sizeof(scanCommand) + 1, // msgLen + {}, // remIP + 0, // remPort + SCAN_REQUEST, // requestType + 0, // atts }); queueData.push(scanCounter); // address of the scanned slave - for (byte i = 0; i < sizeof(SCAN_COMMAND); i++) { - queueData.push(SCAN_COMMAND[i]); + for (byte i = 0; i < sizeof(scanCommand); i++) { + queueData.push(scanCommand[i]); + } + if (scanCommand[0] == SCAN_FUNCTION_FIRST) { + scanCommand[0] = SCAN_FUNCTION_SECOND; + } else { + scanCommand[0] = SCAN_FUNCTION_FIRST; + scanCounter++; } - scanCounter++; if (scanCounter == MAX_SLAVES + 1) scanCounter = 0; } } -byte checkRequest(const byte inBuffer[], unsigned int msgLength, const IPAddress remoteIP, const unsigned int remotePort, byte requestType) { +byte checkRequest(byte inBuffer[], unsigned int msgLength, const uint32_t remoteIP, const unsigned int remotePort, byte requestType) { byte addressPos = 6 * !localConfig.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 (localConfig.enableRtuOverTcp) { // check CRC for Modbus RTU over TCP/UDP if (checkCRC(inBuffer, msgLength) == false) { @@ -152,15 +161,15 @@ byte checkRequest(const byte inBuffer[], unsigned int msgLength, const IPAddress msgLength = msgLength - addressPos - (2 * localConfig.enableRtuOverTcp); // in Modbus RTU over TCP/UDP do not store CRC // check if we have space in request queue if (queueHeaders.available() < 1 || queueData.available() < msgLength) { - setSlaveStatus(inBuffer[addressPos], STAT_ERROR_0A, true, false); + setSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0A, true, false); return 0x0A; // return modbus error 0x0A (Gateway Overloaded) } // allow only one request to non responding slaves - if (getSlaveStatus(inBuffer[addressPos], STAT_ERROR_0B_QUEUE)) { - errorCount[STAT_ERROR_0B]++; + if (getSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0B_QUEUE)) { + errorCount[SLAVE_ERROR_0B]++; return 0x0B; // return modbus error 11 (Gateway Target Device Failed to Respond) - usually means that target device (address) is not present - } else if (getSlaveStatus(inBuffer[addressPos], STAT_ERROR_0B)) { - setSlaveStatus(inBuffer[addressPos], STAT_ERROR_0B_QUEUE, true, false); + } else if (getSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0B)) { + setSlaveStatus(inBuffer[addressPos], SLAVE_ERROR_0B_QUEUE, true, false); } else { // Add PRIORITY_REQUEST flag to requests for responding slaves requestType = requestType | PRIORITY_REQUEST; @@ -170,13 +179,16 @@ byte checkRequest(const byte inBuffer[], unsigned int msgLength, const IPAddress requestType = requestType | SCAN_REQUEST; // Treat broadcast as scan (only one attempt, short timeout, do not expect response) } // all checkes passed OK, we can store the incoming data in request queue + if (requestType & TCP_REQUEST) { + socketInQueue[requestType & TCP_REQUEST_MASK]++; + } // Store in request queue queueHeaders.push(header{ { inBuffer[0], inBuffer[1] }, // tid[2] (ignored in Modbus RTU over TCP/UDP) - (byte)msgLength, // msgLen + byte(msgLength), // msgLen (IPAddress)remoteIP, // remIP (unsigned int)remotePort, // remPort - (byte)(requestType), // requestType + byte(requestType), // requestType 0, // atts }); for (byte i = 0; i < msgLength; i++) { @@ -191,6 +203,7 @@ void deleteRequest() // delete request from queue { header myHeader = queueHeaders.first(); if (myHeader.requestType & SCAN_REQUEST) scanReqInQueue = false; + if (myHeader.requestType & TCP_REQUEST) socketInQueue[myHeader.requestType & TCP_REQUEST_MASK]--; if (myHeader.requestType & PRIORITY_REQUEST) priorityReqInQueue--; for (byte i = 0; i < myHeader.msgLen; i++) { queueData.shift(); @@ -198,6 +211,16 @@ void deleteRequest() // delete request from queue queueHeaders.shift(); } +void clearQueue() { + queueHeaders.clear(); // <- eats memory! + queueData.clear(); + scanReqInQueue = false; + priorityReqInQueue = false; + memset(socketInQueue, 0, sizeof(socketInQueue)); + memset(stat[SLAVE_ERROR_0B_QUEUE], 0, sizeof(stat[SLAVE_ERROR_0B_QUEUE])); + sendTimer.sleep(0); +} + bool getSlaveStatus(const uint8_t slave, const byte status) { if (slave >= MAX_SLAVES) return false; // error return (stat[status][slave / 8] & masks[slave & 7]) > 0; @@ -208,10 +231,10 @@ void setSlaveStatus(const uint8_t slave, byte status, const bool value, const bo if (value == 0) { stat[status][slave / 8] &= ~masks[slave & 7]; } else { - for (byte i = 0; i < STAT_NUM; i++) { + for (byte i = 0; i < SLAVE_ERROR_LAST; i++) { stat[i][slave / 8] &= ~masks[slave & 7]; // set all other flags to false } stat[status][slave / 8] |= masks[slave & 7]; - if (status != STAT_ERROR_0B_QUEUE && isScan == false) errorCount[status]++; // there is no counter for STAT_ERROR_0B_QUEUE, ignor scans in statistics + if (status != SLAVE_ERROR_0B_QUEUE && isScan == false) errorCount[status]++; // there is no counter for SLAVE_ERROR_0B_QUEUE, ignor scans in statistics } } diff --git a/arduino-modbus-rtu-tcp-gateway/03-modbus-rtu.ino b/arduino-modbus-rtu-tcp-gateway/03-modbus-rtu.ino index 380205a..949b562 100644 --- a/arduino-modbus-rtu-tcp-gateway/03-modbus-rtu.ino +++ b/arduino-modbus-rtu-tcp-gateway/03-modbus-rtu.ino @@ -16,8 +16,6 @@ ***************************************************************** */ -int rxNdx = 0; -int txNdx = 0; void sendSerial() { if (!sendTimer.isOver()) { @@ -26,6 +24,7 @@ void sendSerial() { if (queueHeaders.isEmpty()) { return; } + static int txNdx = 0; header myHeader = queueHeaders.first(); switch (serialState) { case 0: // IDLE: Optimize queue (prioritize requests from responding slaves) and trigger sending via serial @@ -93,9 +92,16 @@ void sendSerial() { deleteRequest(); } else if (myHeader.atts >= localConfig.serialAttempts) { // send modbus error 0x0B (Gateway Target Device Failed to Respond) - usually means that target device (address) is not present - setSlaveStatus(queueData[0], STAT_ERROR_0B, true, false); - byte MBAP[] = { myHeader.tid[0], myHeader.tid[1], 0x00, 0x00, 0x00, 0x03 }; - byte PDU[5] = { queueData[0], (byte)(queueData[1] + 0x80), 0x0B }; + setSlaveStatus(queueData[0], SLAVE_ERROR_0B, true, false); + byte MBAP[] = { myHeader.tid[0], + myHeader.tid[1], + 0x00, + 0x00, + 0x00, + 0x03 }; + byte PDU[5] = { queueData[0], + byte(queueData[1] + 0x80), + 0x0B }; crc = 0xFFFF; for (byte i = 0; i < 3; i++) { calculateCRC(PDU[i]); @@ -105,7 +111,7 @@ void sendSerial() { sendResponse(MBAP, PDU, 5); errorTimeoutCount++; } else { - setSlaveStatus(queueData[0], STAT_ERROR_0B_QUEUE, true, false); + setSlaveStatus(queueData[0], SLAVE_ERROR_0B_QUEUE, true, false); errorTimeoutCount++; } // if (myHeader.atts >= MAX_RETRY) serialState = 0; // IDLE @@ -117,14 +123,14 @@ void sendSerial() { } void recvSerial() { + static int rxNdx = 0; static byte serialIn[MODBUS_SIZE]; while (mySerial.available() > 0) { + byte b = mySerial.read(); if (rxNdx < MODBUS_SIZE) { - serialIn[rxNdx] = mySerial.read(); + serialIn[rxNdx] = b; rxNdx++; - } else { // frame longer than maximum allowed - mySerial.read(); // CRC will fail and errorRtuCount will be recorded down the road - } + } // if frame longer than maximum allowed, CRC will fail and errorRtuCount will be recorded down the road recvTimer.sleep(charTimeOut()); sendTimer.sleep(localConfig.frameDelay * 1000UL); // delay next serial write } @@ -134,11 +140,18 @@ void recvSerial() { header myHeader = queueHeaders.first(); if (checkCRC(serialIn, rxNdx) == true && serialIn[0] == queueData[0] && serialState == WAITING) { if (serialIn[1] > 0x80 && (myHeader.requestType & SCAN_REQUEST) == false) { - setSlaveStatus(serialIn[0], STAT_ERROR_0X, true, false); + setSlaveStatus(serialIn[0], SLAVE_ERROR_0X, true, false); } else { - setSlaveStatus(serialIn[0], STAT_OK, true, myHeader.requestType & SCAN_REQUEST); + setSlaveStatus(serialIn[0], SLAVE_OK, true, myHeader.requestType & SCAN_REQUEST); } - byte MBAP[] = { myHeader.tid[0], myHeader.tid[1], 0x00, 0x00, highByte(rxNdx - 2), lowByte(rxNdx - 2) }; + byte MBAP[] = { + myHeader.tid[0], + myHeader.tid[1], + 0x00, + 0x00, + highByte(rxNdx - 2), + lowByte(rxNdx - 2) + }; sendResponse(MBAP, serialIn, rxNdx); serialState = IDLE; } else { @@ -151,8 +164,15 @@ void recvSerial() { } } -void sendResponse(const byte MBAP[], const byte PDU[], const unsigned int pduLength) { +void sendResponse(const byte MBAP[], const byte PDU[], const int pduLength) { header myHeader = queueHeaders.first(); + responseLen = 0; + while (responseLen < pduLength) { // include CRC + if (responseLen < MAX_RESPONSE_LEN) { + response[responseLen] = PDU[responseLen]; + } + responseLen++; + } if (myHeader.requestType & UDP_REQUEST) { Udp.beginPacket(myHeader.remIP, myHeader.remPort); if (localConfig.enableRtuOverTcp) Udp.write(PDU, pduLength); @@ -166,8 +186,10 @@ void sendResponse(const byte MBAP[], const byte PDU[], const unsigned int pduLen if (!localConfig.enableRtuOverTcp) ethTxCount += 4; #endif /* ENABLE_EXTRA_DIAG */ } else if (myHeader.requestType & TCP_REQUEST) { - EthernetClient client = EthernetClient(myHeader.requestType & TCP_REQUEST); - if (client.connected()) { // TODO check remote IP and port? + byte sock = myHeader.requestType & TCP_REQUEST_MASK; + EthernetClient client = EthernetClient(sock); + // if (W5100.readSnSR(sock) == SnSR::ESTABLISHED && W5100.readSnDPORT(sock) == myHeader.remPort) { // Check remote port should be enough or check also rem IP? + if (W5100.readSnSR(sock) == SnSR::ESTABLISHED) { // Check remote port should be enough or check also rem IP? if (localConfig.enableRtuOverTcp) client.write(PDU, pduLength); else { client.write(MBAP, 6); diff --git a/arduino-modbus-rtu-tcp-gateway/04-webserver.ino b/arduino-modbus-rtu-tcp-gateway/04-webserver.ino index f637f41..ea77cca 100644 --- a/arduino-modbus-rtu-tcp-gateway/04-webserver.ino +++ b/arduino-modbus-rtu-tcp-gateway/04-webserver.ino @@ -16,20 +16,21 @@ ***************************************************************** */ -const byte WEB_IN_BUFFER_SIZE = 128; // size of web server read buffer (reads a complete line), 128 bytes necessary for POST data -const byte SMALL_BUFFER_SIZE = 32; // a smaller buffer for uri +const byte URI_SIZE = 24; // a smaller buffer for uri +const byte POST_SIZE = 24; // a smaller buffer for single post parameter + key // Actions that need to be taken after saving configuration. enum action_type : byte { NONE, - FACTORY, // Load default factory settings (but keep MAC address) - MAC, // Generate new random MAC - REBOOT, // Reboot the microcontroller - ETH_SOFT, // Ethernet software reset - SERIAL_SOFT, // Serial software reset - SCAN, // Initialize RS485 scan - RST_STATS, // Reset Modbus Statistics - WEB // Restart webserver + FACTORY, // Load default factory settings (but keep MAC address) + MAC, // Generate new random MAC + REBOOT, // Reboot the microcontroller + RESET_ETH, // Ethernet reset + RESET_SERIAL, // Serial reset + SCAN, // Initialize RTU scan + RESET_STATS, // Reset Modbus Statistics + CLEAR_REQUEST, // Clear Modbus Request form + WEB // Restart webserver }; enum action_type action; @@ -37,13 +38,14 @@ enum action_type action; // URL of the page (*.htm) contains number corresponding to its position in this array. // The following enum array can have a maximum of 10 elements (incl. PAGE_NONE and PAGE_WAIT) enum page : byte { - PAGE_NONE, // reserved for NULL + PAGE_ERROR, // 404 Error PAGE_INFO, PAGE_STATUS, PAGE_IP, PAGE_TCP, PAGE_RTU, PAGE_WAIT, // page with "Reloading. Please wait..." message. + PAGE_DATA, // data.json }; // Keys for POST parameters, used in web forms and processed by processPost() function. @@ -67,21 +69,34 @@ enum post_key : byte { POST_DNS, POST_DNS_1, POST_DNS_2, - POST_DNS_3, // DNS || all these 16 enum elements must be listed in succession!! || - POST_TCP, // TCP port || Because HTML code for these 3 ports || - POST_UDP, // UDP port || is generated through one for-loop, || - POST_WEB, // web UI port || these 3 elements must be listed in succession!! || - POST_RTU_OVER, // RTU over TCP/UDP - POST_BAUD, // baud rate - POST_DATA, // data bits - POST_PARITY, // parity - POST_STOP, // stop bits - POST_FRAMEDELAY, //frame delay - POST_TIMEOUT, // response timeout - POST_ATTEMPTS, // number of request attempts - POST_ACTION, // actions on Tools page + POST_DNS_3, // DNS || all these 16 enum elements must be listed in succession!! || + POST_TCP, // TCP port || Because HTML code for these 3 ports || + POST_UDP, // UDP port || is generated through one for-loop, || + POST_WEB, // web UI port || these 3 elements must be listed in succession!! || + POST_RTU_OVER, // RTU over TCP/UDP + POST_TCP_TIMEOUT, // Modbus TCP socket close timeout + POST_BAUD, // baud rate + POST_DATA, // data bits + POST_PARITY, // parity + POST_STOP, // stop bits + POST_FRAMEDELAY, //frame delay + POST_TIMEOUT, // response timeout + POST_ATTEMPTS, // number of request attempts + POST_REQ, // Modbus request send from WebUI (first byte) + POST_REQ_1, + POST_REQ_2, + POST_REQ_3, + POST_REQ_4, + POST_REQ_5, + POST_REQ_6, + POST_REQ_LAST, // 8 bytes in total + POST_ACTION, // actions on Tools page }; +byte request[POST_REQ_LAST - POST_REQ + 1]; // Array to store Modbus request sent from WebUI +byte requestLen = 0; // Length of the Modbus request send from WebUI + + // Keys for JSON elements, used in: 1) JSON documents, 2) ID of span tags, 3) Javascript. enum JSON_type : byte { JSON_SECS, // Runtime seconds @@ -97,208 +112,196 @@ enum JSON_type : byte { JSON_ERROR_TIMEOUT, JSON_QUEUE_DATA, JSON_QUEUE_REQUESTS, - JSON_MASTERS, // list of Modbus TCP/UDP masters separated by
- JSON_SLAVES, // list of Modbus RTU slaves separated by
-#ifdef ENABLE_EXTRA_DIAG - JSON_RS485_TX, - JSON_RS485_RX, + JSON_TCP_UDP_MASTERS, // list of Modbus TCP/UDP masters separated by
+ JSON_SLAVES, // list of Modbus RTU slaves separated by
+ JSON_RTU_TX, + JSON_RTU_RX, JSON_ETH_TX, JSON_ETH_RX, -#endif /* ENABLE_EXTRA_DIAG */ + JSON_RESPONSE, + JSON_SOCKETS, JSON_LAST, // Must be the very last element in this array }; +void recvWeb(EthernetClient &client) { + char uri[URI_SIZE]; // the requested page + memset(uri, 0, sizeof(uri)); + while (client.available()) { // start reading the first line which should look like: GET /uri HTTP/1.1 + if (client.read() == ' ') break; // find space before /uri + } + byte len = 0; + while (client.available() && len < sizeof(uri) - 1) { + char c = client.read(); // parse uri + if (c == ' ') break; // find space after /uri + uri[len] = c; + len++; + } + while (client.available()) { + if (client.read() == '\r') + if (client.read() == '\n') + if (client.read() == '\r') + if (client.read() == '\n') + break; // find 2 end of lines between header and body + } + if (client.available()) { + processPost(client); // parse post parameters + } -void recvWeb() { - EthernetClient client = webServer.available(); - if (client) { - char uri[SMALL_BUFFER_SIZE]; // the requested page - // char requestParameter[SMALL_BUFFER_SIZE]; // parameter appended to the URI after a ? - // char postParameter[SMALL_BUFFER_SIZE] {'\0'}; // parameter transmitted in the body / by POST - if (client.available()) { - char webInBuffer[WEB_IN_BUFFER_SIZE]{ '\0' }; // buffer for incoming data - unsigned int i = 0; // index / current read position - enum status_type : byte { - REQUEST, - CONTENT_LENGTH, - EMPTY_LINE, - BODY - }; - enum status_type status; - status = REQUEST; - while (client.available()) { - char c = client.read(); - if (c == '\n') { - if (status == REQUEST) // read the first line - { - // now split the input - char *ptr; - ptr = strtok(webInBuffer, " "); // strtok willdestroy the newRequest - ptr = strtok(NULL, " "); - strlcpy(uri, ptr, sizeof(uri)); // enthält noch evtl. parameter - status = EMPTY_LINE; // jump to next status - } else if (status > REQUEST && i < 2) // check if we have an empty line - { - status = BODY; - } - i = 0; - memset(webInBuffer, 0, sizeof(webInBuffer)); - } else { - if (i < (WEB_IN_BUFFER_SIZE - 1)) { - webInBuffer[i] = c; - i++; - webInBuffer[i] = '\0'; - } - } - } - if (status == BODY) // status 3 could end without linefeed, therefore we takeover here also - { - if (webInBuffer[0] != '\0') { - processPost(webInBuffer); - } - } - } - - // Get number of the requested page from URI - byte reqPage = 0; //requested page - if (!strcmp(uri, "/")) // the homepage System Info + // Get the requested page from URI + byte reqPage = PAGE_ERROR; // requested page, 404 error is a default + if (uri[0] == '/') { + if (uri[1] == '\0') // the homepage System Info reqPage = PAGE_INFO; - else if ((uri[0] == '/') && !strcmp(uri + 2, ".htm")) { - reqPage = (byte)(uri[1] - 48); // Convert single ASCII char to byte - } - - // Actions that require "please wait" page - if (action == WEB || action == REBOOT || action == ETH_SOFT || action == FACTORY || action == MAC) { - reqPage = PAGE_WAIT; + else if (!strcmp(uri + 2, ".htm")) { + reqPage = byte(uri[1] - 48); // Convert single ASCII char to byte + if (reqPage >= PAGE_WAIT) reqPage = PAGE_ERROR; + } else if (!strcmp(uri, "/d.json")) { + reqPage = PAGE_DATA; } + } + // Actions that require "please wait" page + if (action == WEB || action == MAC || action == RESET_ETH || action == REBOOT || action == FACTORY) { + reqPage = PAGE_WAIT; + } + // Send page + sendPage(client, reqPage); - // Send page - if ((reqPage > 0) && (reqPage <= PAGE_WAIT)) - sendPage(client, reqPage); - else if (!strcmp(uri, "/data.json")) - sendJson(client); - else if (!strcmp(uri, "/favicon.ico")) // a favicon - send204(client); // if you don't have a favicon, send 204 - else // if the page is unknown, HTTP response code 404 - send404(client); // defaults to 404 error - - // Do all actions before the "please wait" redirects (5s delay at the moment) - if (reqPage == PAGE_WAIT) { - for (byte n = 0; n < maxSockNum; n++) { - // in case of webserver restart, stop only clients from old webserver (clients with port different from current settings) - EthernetClient client = EthernetClient(n); - unsigned int port = client.localPort(); - // for WEB, stop only clients from old webserver (those that do not match modbus ports or current web port); for other actions stop all clients - if (action != WEB || (port && port != localConfig.webPort && port != localConfig.udpPort && port != localConfig.tcpPort)) { - client.flush(); - client.stop(); + // Do all actions before the "please wait" redirects (5s delay at the moment) + if (reqPage == PAGE_WAIT) { + switch (action) { + case WEB: + for (byte s = 0; s < maxSockNum; s++) { + // close old webserver TCP connections + if (EthernetClient(s).localPort() != localConfig.tcpPort) { + disconSocket(s); + } } - } - switch (action) { - case WEB: - webServer = EthernetServer(localConfig.webPort); - break; - case REBOOT: - mySerial.flush(); - mySerial.end(); - resetFunc(); - break; - default: // ETH_SOFT, FACTORY, MAC - Udp.stop(); - startEthernet(); - break; - } + webServer = EthernetServer(localConfig.webPort); + break; + case MAC: + case RESET_ETH: + for (byte s = 0; s < maxSockNum; s++) { + // close all TCP and UDP sockets + disconSocket(s); + } + startEthernet(); + break; + case REBOOT: + case FACTORY: + resetFunc(); + break; + default: + break; } - action = NONE; } + action = NONE; } + // This function stores POST parameter values in localConfig. // 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. -void processPost(char postParameter[]) { - char *point = NULL; - char *sav1 = NULL; // for outer strtok_r - point = strtok_r(postParameter, "&", &sav1); - while (point != NULL) { - char *paramKey; - char *paramValue; - char *sav2 = NULL; // for inner strtok_r - paramKey = strtok_r(point, "=", &sav2); // inner strtok_r, use sav2 - paramValue = strtok_r(NULL, "=", &sav2); - point = strtok_r(NULL, "&", &sav1); - if (!paramValue) +void processPost(EthernetClient &client) { + while (client.available()) { + char post[POST_SIZE]; + byte len = 0; + while (client.available() && len < sizeof(post) - 1) { + char c = client.read(); + if (c == '&') break; + post[len] = c; + len++; + } + post[len] = '\0'; + char *paramKey = post; + char *paramValue = post; + while (*paramValue) { + if (*paramValue == '=') { + paramValue++; + break; + } + paramValue++; + } + if (*paramValue == '\0') continue; // do not process POST parameter if there is no parameter value - byte paramKeyByte = atoi(paramKey); - unsigned long paramValueUlong = atol(paramValue); + byte paramKeyByte = strToByte(paramKey); + unsigned int paramValueUint = atol(paramValue); // TODO use atoi? switch (paramKeyByte) { case POST_NONE: // reserved, because atoi / atol returns NULL in case of error break; #ifdef ENABLE_DHCP case POST_DHCP: { - extraConfig.enableDhcp = (byte)paramValueUlong; + extraConfig.enableDhcp = byte(paramValueUint); } break; case POST_DNS ... POST_DNS_3: { - extraConfig.dns[paramKeyByte - POST_DNS] = (byte)paramValueUlong; + extraConfig.dns[paramKeyByte - POST_DNS] = byte(paramValueUint); } break; #endif /* ENABLE_DHCP */ + case POST_REQ ... POST_REQ_LAST: + { + requestLen = paramKeyByte - POST_REQ + 1; + request[requestLen - 1] = strToByte(paramValue); + } + break; case POST_IP ... POST_IP_3: { - action = ETH_SOFT; // this ETH_SOFT is triggered when the user changes anything on the "IP Settings" page. - // No need to trigger ETH_SOFT for other cases (POST_SUBNET, POST_GATEWAY etc.) - localConfig.ip[paramKeyByte - POST_IP] = (byte)paramValueUlong; + action = RESET_ETH; // this RESET_ETH is triggered when the user changes anything on the "IP Settings" page. + // No need to trigger RESET_ETH for other cases (POST_SUBNET, POST_GATEWAY etc.) + localConfig.ip[paramKeyByte - POST_IP] = byte(paramValueUint); } break; case POST_SUBNET ... POST_SUBNET_3: { - localConfig.subnet[paramKeyByte - POST_SUBNET] = (byte)paramValueUlong; + localConfig.subnet[paramKeyByte - POST_SUBNET] = byte(paramValueUint); } break; case POST_GATEWAY ... POST_GATEWAY_3: { - localConfig.gateway[paramKeyByte - POST_GATEWAY] = (byte)paramValueUlong; + localConfig.gateway[paramKeyByte - POST_GATEWAY] = byte(paramValueUint); } break; case POST_TCP: { - for (byte i = 0; i < maxSockNum; i++) { - EthernetClient client = EthernetClient(i); - if (client.localPort() == localConfig.tcpPort) { - client.flush(); - client.stop(); + if (paramValueUint != localConfig.webPort && paramValueUint != localConfig.tcpPort) { // continue only of the value changed and it differs from WebUI port + for (byte s = 0; s < maxSockNum; s++) { + if (EthernetClient(s).localPort() == localConfig.tcpPort) { // close only Modbus TCP sockets + disconSocket(s); + } } + localConfig.tcpPort = paramValueUint; + modbusServer = EthernetServer(localConfig.tcpPort); } - localConfig.tcpPort = (unsigned int)paramValueUlong; - modbusServer = EthernetServer(localConfig.tcpPort); } break; case POST_UDP: { - localConfig.udpPort = (unsigned int)paramValueUlong; + localConfig.udpPort = paramValueUint; Udp.stop(); Udp.begin(localConfig.udpPort); } break; case POST_WEB: { - if (localConfig.webPort != (unsigned int)paramValueUlong) { - localConfig.webPort = (unsigned int)paramValueUlong; + if (paramValueUint != localConfig.webPort && paramValueUint != localConfig.tcpPort) { // continue only of the value changed and it differs from Modbus TCP port + localConfig.webPort = paramValueUint; action = WEB; } } break; case POST_RTU_OVER: - localConfig.enableRtuOverTcp = (byte)paramValueUlong; + localConfig.enableRtuOverTcp = byte(paramValueUint); + break; + case POST_TCP_TIMEOUT: + localConfig.tcpTimeout = paramValueUint; break; case POST_BAUD: { - action = SERIAL_SOFT; // this SERIAL_SOFT is triggered when the user changes anything on the "RS485 Settings" page. - // No need to trigger ETH_SOFT for other cases (POST_DATA, POST_PARITY etc.) - localConfig.baud = paramValueUlong; - byte minFrameDelay = (byte)(frameDelay() / 1000UL) + 1; + action = RESET_SERIAL; // this RESET_SERIAL is triggered when the user changes anything on the "RTU Settings" page. + // No need to trigger RESET_ETH for other cases (POST_DATA, POST_PARITY etc.) + localConfig.baud = paramValueUint; + byte minFrameDelay = byte((frameDelay() / 1000UL) + 1); if (localConfig.frameDelay < minFrameDelay) { localConfig.frameDelay = minFrameDelay; } @@ -306,30 +309,30 @@ void processPost(char postParameter[]) { break; case POST_DATA: { - localConfig.serialConfig = (localConfig.serialConfig & 0xF9) | (((byte)paramValueUlong - 5) << 1); + localConfig.serialConfig = (localConfig.serialConfig & 0xF9) | ((byte(paramValueUint) - 5) << 1); } break; case POST_PARITY: { - localConfig.serialConfig = (localConfig.serialConfig & 0xCF) | ((byte)paramValueUlong << 4); + localConfig.serialConfig = (localConfig.serialConfig & 0xCF) | (byte(paramValueUint) << 4); } break; case POST_STOP: { - localConfig.serialConfig = (localConfig.serialConfig & 0xF7) | (((byte)paramValueUlong - 1) << 3); + localConfig.serialConfig = (localConfig.serialConfig & 0xF7) | ((byte(paramValueUint) - 1) << 3); } break; case POST_FRAMEDELAY: - localConfig.frameDelay = (byte)paramValueUlong; + localConfig.frameDelay = byte(paramValueUint); break; case POST_TIMEOUT: - localConfig.serialTimeout = paramValueUlong; + localConfig.serialTimeout = paramValueUint; break; case POST_ATTEMPTS: - localConfig.serialAttempts = (byte)paramValueUlong; + localConfig.serialAttempts = byte(paramValueUint); break; case POST_ACTION: - action = (action_type)paramValueUlong; + action = action_type(paramValueUint); break; default: break; @@ -342,29 +345,83 @@ void processPost(char postParameter[]) { memcpy(tempMac, localConfig.macEnd, 3); // keep current MAC localConfig = DEFAULT_CONFIG; memcpy(localConfig.macEnd, tempMac, 3); - startSerial(); break; } case MAC: generateMac(); break; - case RST_STATS: + case RESET_STATS: resetStats(); break; + case RESET_SERIAL: + clearQueue(); + startSerial(); + break; case SCAN: scanCounter = 1; memset(&stat, 0, sizeof(stat)); // clear all status flags break; + case CLEAR_REQUEST: + requestLen = 0; + responseLen = 0; + break; default: 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) + // push to queue + queueHeaders.push(header{ + { 0x00, 0x00 }, // tid[2] + requestLen, // msgLen + { 0, 0, 0, 0 }, // remIP[4] + 0, // remPort + 0, // requestType + 0, // atts + }); + for (byte i = 0; i < requestLen; i++) { + queueData.push(request[i]); + } + responseLen = 0; // clear old Modbus Response from WebUI + } // new parameter values received, save them to EEPROM EEPROM.put(CONFIG_START + 1, localConfig); // it is safe to call, only changed values are updated #ifdef ENABLE_DHCP EEPROM.put(CONFIG_START + 1 + sizeof(localConfig), extraConfig); -#endif /* ENABLE_DHCP */ - if (action == SERIAL_SOFT) { // can do it without "please wait" page - startSerial(); - action = NONE; +#endif +} + +// takes 2 chars, 1 char + null byte or 1 null byte +byte strToByte(const char myStr[]) { + if (!myStr) return 0; + byte x = 0; + for (byte i = 0; i < 2; i++) { + char c = myStr[i]; + if (c >= '0' && c <= '9') { + x *= 16; + x += c - '0'; + } else if (c >= 'A' && c <= 'F') { + x *= 16; + x += (c - 'A') + 10; + } else if (c >= 'a' && c <= 'f') { + x *= 16; + x += (c - 'a') + 10; + } + } + return x; +} + +// from https://github.com/RobTillaart/printHelpers +char __printbuffer[3]; +char *hex(byte val) { + char *buffer = __printbuffer; + byte digits = 2; + buffer[digits] = '\0'; + while (digits > 0) { + byte v = val & 0x0F; + val >>= 4; + digits--; + buffer[digits] = (v < 10) ? '0' + v : ('A' - 10) + v; } + return buffer; } diff --git a/arduino-modbus-rtu-tcp-gateway/05-pages.ino b/arduino-modbus-rtu-tcp-gateway/05-pages.ino index 9f39c2f..3ca0551 100644 --- a/arduino-modbus-rtu-tcp-gateway/05-pages.ino +++ b/arduino-modbus-rtu-tcp-gateway/05-pages.ino @@ -2,12 +2,12 @@ Pages for Webserver sendPage - - displays main page, renders title and left menu using tables + - 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 - menuItem + stringPageName - returns menu item string depending on the the number (i.e. URL) of the requested web page contentInfo, contentStatus, contentIp, contentTcp, contentRtu @@ -16,8 +16,8 @@ contentWait - renders the "please wait" message instead of the content (= request page number 0xFF, will be forwarded to home page after 5 seconds) - helperInput - helperStats + tagInputNumber + stringStats - renders some repetitive HTML code for inputs send404, send204 @@ -25,12 +25,38 @@ ***************************************************************** */ -const byte WEB_OUT_BUFFER_SIZE = 128; // size of web server write buffer (used by StreamLib) +const byte WEB_OUT_BUFFER_SIZE = 64; // size of web server write buffer (used by StreamLib) void sendPage(EthernetClient &client, byte reqPage) { char webOutBuffer[WEB_OUT_BUFFER_SIZE]; ChunkedPrint chunked(client, webOutBuffer, sizeof(webOutBuffer)); // the StreamLib object to replace client print + if (reqPage == PAGE_ERROR) { + chunked.print(F("HTTP/1.1 404 Not Found\r\n" + "\r\n" + "404 Not found")); + chunked.end(); + return; + } else if (reqPage == PAGE_DATA) { + chunked.print(F("HTTP/1.1 200\r\n" + "Content-Type: application/json\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n")); + chunked.begin(); + chunked.print(F("{")); + for (byte i = 0; i < JSON_LAST; i++) { + if (i) chunked.print(F(",")); + chunked.print(F("\"")); + chunked.print(i); + chunked.print(F("\":\"")); + jsonVal(chunked, i); + chunked.print(F("\"")); + } + chunked.print(F("}")); + chunked.end(); + return; + } chunked.print(F("HTTP/1.1 200 OK\r\n" + // "Connection: close\r\n" "Content-Type: text/html\r\n" "Transfer-Encoding: chunked\r\n" "\r\n")); @@ -48,14 +74,22 @@ void sendPage(EthernetClient &client, byte reqPage) { chunked.print(F(">" "Modbus RTU ⇒ Modbus TCP/UDP Gateway" "" "" "