Skip to content

Commit 50dd986

Browse files
committed
Web interface for the Modbus gateway
Web interface, settings stored in EEPROM. Support for Modbus RTU over TCP/UDP.
1 parent 452ef5b commit 50dd986

14 files changed

+1738
-579
lines changed

01-interfaces.ino

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/* *******************************************************************
2+
Ethernet and serial interface functions
3+
4+
startSerial
5+
- starts HW serial interface which we use for RS485 line
6+
- calculates Modbus RTU character timeout and frame delay
7+
8+
startEthernet
9+
- initiates ethernet interface
10+
- if enabled, gets IP from DHCP
11+
- starts all servers (Modbus TCP, UDP, web server)
12+
13+
resetFunc
14+
- well... resets Arduino
15+
16+
maintainUptime
17+
- maintains up time in case of millis() overflow
18+
19+
maintainCounters
20+
- synchronizes roll-over of data counters to zero
21+
22+
+ preprocessor code for identifying microcontroller board
23+
24+
***************************************************************** */
25+
26+
void startSerial() {
27+
Serial.begin(localConfig.baud, localConfig.serialConfig);
28+
// Calculate Modbus RTU character timeout and frame delay
29+
byte bits = // number of bits per character (11 in default Modbus RTU settings)
30+
1 + // start bit
31+
(((localConfig.serialConfig & 0x06) >> 1) + 5) + // data bits
32+
(((localConfig.serialConfig & 0x08) >> 3) + 1); // stop bits
33+
if (((localConfig.serialConfig & 0x30) >> 4) > 1) bits += 1; // parity bit (if present)
34+
int T = ((unsigned long)bits * 1000000UL) / localConfig.baud; // time to send 1 character over serial in microseconds
35+
if (localConfig.baud <= 19200) {
36+
charTimeout = 1.5 * T; // inter-character time-out should be 1,5T
37+
frameDelay = 3.5 * T; // inter-frame delay should be 3,5T
38+
}
39+
else {
40+
charTimeout = 750;
41+
frameDelay = 1750;
42+
}
43+
pinMode(SerialTxControl, OUTPUT);
44+
digitalWrite(SerialTxControl, RS485Receive); // Init Transceiver
45+
}
46+
47+
void startEthernet() {
48+
Ethernet.setRstPin(ethResetPin);
49+
#ifdef ENABLE_DHCP
50+
if (!localConfig.enableDhcp || !Ethernet.begin(localConfig.mac)) {
51+
Ethernet.begin(localConfig.mac, localConfig.ip, localConfig.subnet, localConfig.gateway, localConfig.dns);
52+
}
53+
#else /* ENABLE_DHCP */
54+
Ethernet.begin(localConfig.mac, localConfig.ip, localConfig.subnet, localConfig.gateway, localConfig.dns);
55+
localConfig.enableDhcp = false; // Ensure Dhcp is disabled in config
56+
#endif /* ENABLE_DHCP */
57+
modbusServer = EthernetServer(localConfig.tcpPort);
58+
webServer = EthernetServer(localConfig.webPort);
59+
Udp.begin(localConfig.udpPort);
60+
modbusServer.begin();
61+
webServer.begin();
62+
dbg(F("[arduino] Server available at http://"));
63+
dbgln(Ethernet.localIP());
64+
}
65+
66+
void(* resetFunc) (void) = 0; //declare reset function at address 0
67+
68+
void maintainUptime()
69+
{
70+
unsigned long milliseconds = millis();
71+
if (last_milliseconds > milliseconds) {
72+
//in case of millis() overflow, store existing passed seconds
73+
remaining_seconds = seconds;
74+
}
75+
//store last millis(), so that we can detect on the next call
76+
//if there is a millis() overflow ( millis() returns 0 )
77+
last_milliseconds = milliseconds;
78+
//In case of overflow, the "remaining_seconds" variable contains seconds counted before the overflow.
79+
//We add the "remaining_seconds", so that we can continue measuring the time passed from the last boot of the device.
80+
seconds = (milliseconds / 1000) + remaining_seconds;
81+
}
82+
83+
void maintainCounters()
84+
{
85+
// synchronize roll-over of data counters to zero, at 0xFFFFF000
86+
if (serialTxCount > 0xFFFFF000 || serialRxCount > 0xFFFFF000 || ethTxCount > 0xFFFFF000 || ethRxCount > 0xFFFFF000) {
87+
serialRxCount = 0;
88+
serialTxCount = 0;
89+
ethRxCount = 0;
90+
ethTxCount = 0;
91+
}
92+
}
93+
94+
// Board definitions
95+
#if defined(TEENSYDUINO)
96+
97+
// --------------- Teensy -----------------
98+
99+
#if defined(__AVR_ATmega32U4__)
100+
#define BOARD F("Teensy 2.0")
101+
#elif defined(__AVR_AT90USB1286__)
102+
#define BOARD F("Teensy++ 2.0")
103+
#elif defined(__MK20DX128__)
104+
#define BOARD F("Teensy 3.0")
105+
#elif defined(__MK20DX256__)
106+
#define BOARD F("Teensy 3.2") // and Teensy 3.1 (obsolete)
107+
#elif defined(__MKL26Z64__)
108+
#define BOARD F("Teensy LC")
109+
#elif defined(__MK64FX512__)
110+
#define BOARD F("Teensy 3.5")
111+
#elif defined(__MK66FX1M0__)
112+
#define BOARD F("Teensy 3.6")
113+
#else
114+
#error "Unknown board"
115+
#endif
116+
117+
#else // --------------- Arduino ------------------
118+
119+
#if defined(ARDUINO_AVR_ADK)
120+
#define BOARD F("Arduino Mega Adk")
121+
#elif defined(ARDUINO_AVR_BT) // Bluetooth
122+
#define BOARD F("Arduino Bt")
123+
#elif defined(ARDUINO_AVR_DUEMILANOVE)
124+
#define BOARD F("Arduino Duemilanove")
125+
#elif defined(ARDUINO_AVR_ESPLORA)
126+
#define BOARD F("Arduino Esplora")
127+
#elif defined(ARDUINO_AVR_ETHERNET)
128+
#define BOARD F("Arduino Ethernet")
129+
#elif defined(ARDUINO_AVR_FIO)
130+
#define BOARD F("Arduino Fio")
131+
#elif defined(ARDUINO_AVR_GEMMA)
132+
#define BOARD F("Arduino Gemma")
133+
#elif defined(ARDUINO_AVR_LEONARDO)
134+
#define BOARD F("Arduino Leonardo")
135+
#elif defined(ARDUINO_AVR_LILYPAD)
136+
#define BOARD F("Arduino Lilypad")
137+
#elif defined(ARDUINO_AVR_LILYPAD_USB)
138+
#define BOARD F("Arduino Lilypad Usb")
139+
#elif defined(ARDUINO_AVR_MEGA)
140+
#define BOARD F("Arduino Mega")
141+
#elif defined(ARDUINO_AVR_MEGA2560)
142+
#define BOARD F("Arduino Mega 2560")
143+
#elif defined(ARDUINO_AVR_MICRO)
144+
#define BOARD F("Arduino Micro")
145+
#elif defined(ARDUINO_AVR_MINI)
146+
#define BOARD F("Arduino Mini")
147+
#elif defined(ARDUINO_AVR_NANO)
148+
#define BOARD F("Arduino Nano")
149+
#elif defined(ARDUINO_AVR_NG)
150+
#define BOARD F("Arduino NG")
151+
#elif defined(ARDUINO_AVR_PRO)
152+
#define BOARD F("Arduino Pro")
153+
#elif defined(ARDUINO_AVR_ROBOT_CONTROL)
154+
#define BOARD F("Arduino Robot Ctrl")
155+
#elif defined(ARDUINO_AVR_ROBOT_MOTOR)
156+
#define BOARD F("Arduino Robot Motor")
157+
#elif defined(ARDUINO_AVR_UNO)
158+
#define BOARD F("Arduino Uno")
159+
#elif defined(ARDUINO_AVR_YUN)
160+
#define BOARD F("Arduino Yun")
161+
162+
// These boards must be installed separately:
163+
#elif defined(ARDUINO_SAM_DUE)
164+
#define BOARD F("Arduino Due")
165+
#elif defined(ARDUINO_SAMD_ZERO)
166+
#define BOARD F("Arduino Zero")
167+
#elif defined(ARDUINO_ARC32_TOOLS)
168+
#define BOARD F("Arduino 101")
169+
#else
170+
#error "Unknown board"
171+
#endif
172+
173+
#endif

02-modbus-tcp.ino

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/* *******************************************************************
2+
Modbus TCP/UDP functions
3+
4+
recvUdp
5+
- receives Modbus UDP (or Modbus RTU over UDP) messages
6+
- calls checkRequest
7+
- stores requests in queue or replies with error
8+
9+
recvTcp
10+
- receives Modbus TCP (or Modbus RTU over TCP) messages
11+
- calls checkRequest
12+
- stores requests in queue or replies with error
13+
14+
processRequests
15+
- inserts scan request into queue
16+
- optimizes queue
17+
18+
checkRequest
19+
- checks Modbus TCP/UDP requests (correct MBAP header, CRC in case of Modbus RTU over TCP/UDP)
20+
- checks availability of queue
21+
22+
deleteRequest
23+
- deletes requests from queue
24+
25+
***************************************************************** */
26+
27+
28+
BitBool<maxSlaves> slavesResponding;
29+
30+
typedef struct {
31+
byte tid[2]; // MBAP Transaction ID
32+
byte uid; // MBAP Unit ID (address)
33+
byte PDUlen; // lenght of PDU (func + data) stored in queuePDUs
34+
IPAddress remIP; // remote IP for UDP client (UDP response is sent back to remote IP)
35+
unsigned int remPort; // remote port for UDP client (UDP response is sent back to remote port)
36+
byte clientNum; // TCP client who sent the request, UDP_REQUEST (0xFF) designates UDP client
37+
} header;
38+
39+
// each request is stored in 3 queues (all queues are written to, read and deleted in sync)
40+
CircularBuffer<header, reqQueueCount> queueHeaders; // queue of requests' headers and metadata (MBAP transaction ID, MBAP unit ID, PDU length, remIP, remPort, TCP client)
41+
CircularBuffer<byte, reqQueueSize> queuePDUs; // queue of PDU data (function code, data)
42+
CircularBuffer<byte, reqQueueCount> queueRetries; // queue of retry counters
43+
44+
void recvUdp()
45+
{
46+
int packetSize = Udp.parsePacket();
47+
if (packetSize)
48+
{
49+
ethRxCount += packetSize;
50+
byte udpInBuffer[modbusSize + 4]; // Modbus TCP frame is 4 bytes longer than Modbus RTU frame
51+
// Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address).....
52+
// Modbus RTU frame: [0] address.....
53+
Udp.read(udpInBuffer, sizeof(udpInBuffer));
54+
Udp.flush();
55+
56+
int errorCode = checkRequest(udpInBuffer, packetSize);
57+
byte pduStart; // first byte of Protocol Data Unit (i.e. Function code)
58+
if (localConfig.enableRtuOverTcp) pduStart = 1; // In Modbus RTU, Function code is second byte (after address)
59+
else pduStart = 7; // In Modbus TCP/UDP, Function code is 8th byte (after address)
60+
if (errorCode == 0) {
61+
// Store in request queue: 2 bytes MBAP Transaction ID (ignored in Modbus RTU over TCP); MBAP Unit ID (address); PDUlen (func + data);remote IP; remote port; TCP client Number (socket) - 0xFF for UDP
62+
queueHeaders.push(header {{udpInBuffer[0], udpInBuffer[1]}, udpInBuffer[pduStart - 1], packetSize - pduStart, Udp.remoteIP(), Udp.remotePort(), UDP_REQUEST});
63+
queueRetries.push(0);
64+
for (byte i = 0; i < packetSize - pduStart; i++) {
65+
queuePDUs.push(udpInBuffer[i + pduStart]);
66+
}
67+
} else if (errorCode > 0) {
68+
// send back message with error code
69+
Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
70+
if (!localConfig.enableRtuOverTcp) {
71+
Udp.write(udpInBuffer, 5);
72+
Udp.write(0x03);
73+
}
74+
Udp.write(udpInBuffer[pduStart - 1]); // address
75+
Udp.write(udpInBuffer[pduStart] + 0x80); // function + 0x80
76+
Udp.write(errorCode);
77+
if (localConfig.enableRtuOverTcp) {
78+
crc = 0xFFFF;
79+
calculateCRC(udpInBuffer[pduStart - 1]);
80+
calculateCRC(udpInBuffer[pduStart] + 0x80);
81+
calculateCRC(errorCode);
82+
Udp.write(lowByte(crc)); // send CRC, low byte first
83+
Udp.write(highByte(crc));
84+
}
85+
Udp.endPacket();
86+
ethTxCount += 5;
87+
if (!localConfig.enableRtuOverTcp) ethTxCount += 4;
88+
}
89+
}
90+
}
91+
92+
93+
void recvTcp()
94+
{
95+
EthernetClient client = modbusServer.available();
96+
if (client) {
97+
int packetSize = client.available();
98+
ethRxCount += packetSize;
99+
byte tcpInBuffer[modbusSize + 4]; // Modbus TCP frame is 4 bytes longer than Modbus RTU frame
100+
// Modbus TCP/UDP frame: [0][1] transaction ID, [2][3] protocol ID, [4][5] length and [6] unit ID (address).....
101+
// Modbus RTU frame: [0] address.....
102+
client.read(tcpInBuffer, sizeof(tcpInBuffer));
103+
client.flush();
104+
int errorCode = checkRequest(tcpInBuffer, packetSize);
105+
byte pduStart; // first byte of Protocol Data Unit (i.e. Function code)
106+
if (localConfig.enableRtuOverTcp) pduStart = 1; // In Modbus RTU, Function code is second byte (after address)
107+
else pduStart = 7; // In Modbus TCP/UDP, Function code is 8th byte (after address)
108+
if (errorCode == 0) {
109+
// Store in request queue: 2 bytes MBAP Transaction ID (ignored in Modbus RTU over TCP); MBAP Unit ID (address); PDUlen (func + data);remote IP; remote port; TCP client Number (socket) - 0xFF for UDP
110+
queueHeaders.push(header {{tcpInBuffer[0], tcpInBuffer[1]}, tcpInBuffer[pduStart - 1], packetSize - pduStart, {}, 0, client.getSocketNumber()});
111+
queueRetries.push(0);
112+
for (byte i = 0; i < packetSize - pduStart; i++) {
113+
queuePDUs.push(tcpInBuffer[i + pduStart]);
114+
}
115+
} else if (errorCode > 0) {
116+
// send back message with error code
117+
if (!localConfig.enableRtuOverTcp) {
118+
client.write(tcpInBuffer, 5);
119+
client.write(0x03);
120+
}
121+
client.write(tcpInBuffer[pduStart - 1]); // address
122+
client.write(tcpInBuffer[pduStart] + 0x80); // function + 0x80
123+
client.write(errorCode);
124+
if (localConfig.enableRtuOverTcp) {
125+
crc = 0xFFFF;
126+
calculateCRC(tcpInBuffer[pduStart - 1]);
127+
calculateCRC(tcpInBuffer[pduStart] + 0x80);
128+
calculateCRC(errorCode);
129+
client.write(lowByte(crc)); // send CRC, low byte first
130+
client.write(highByte(crc));
131+
}
132+
client.stop();
133+
ethTxCount += 5;
134+
if (!localConfig.enableRtuOverTcp) ethTxCount += 4;
135+
}
136+
}
137+
}
138+
139+
void processRequests()
140+
{
141+
// Insert scan request into queue
142+
if (scanCounter != 0 && queueHeaders.available() > 1 && queuePDUs.available() > 1) {
143+
// Store scan request in request queue
144+
queueHeaders.push(header {{0x00, 0x00}, scanCounter, sizeof(scanCommand), {}, 0, SCAN_REQUEST});
145+
queueRetries.push(localConfig.serialRetry - 1); // scan requests are only sent once, so set "queueRetries" to one attempt below limit
146+
for (byte i = 0; i < sizeof(scanCommand); i++) {
147+
queuePDUs.push(scanCommand[i]);
148+
}
149+
scanCounter++;
150+
if (scanCounter == maxSlaves + 1) scanCounter = 0;
151+
}
152+
153+
// Optimize queue (prioritize requests from responding slaves) and trigger sending via serial
154+
if (serialState == IDLE) { // send new data over serial only if we are not waiting for response
155+
if (!queueHeaders.isEmpty()) {
156+
boolean queueHasRespondingSlaves; // true if queue holds at least one request to responding slaves
157+
for (byte i = 0; i < queueHeaders.size(); i++) {
158+
if (slavesResponding[queueHeaders[i].uid] == true) {
159+
queueHasRespondingSlaves = true;
160+
break;
161+
} else {
162+
queueHasRespondingSlaves = false;
163+
}
164+
}
165+
while (queueHasRespondingSlaves == true && slavesResponding[queueHeaders.first().uid] == false) {
166+
// move requests to non responding slaves to the tail of the queue
167+
for (byte i = 0; i < queueHeaders.first().PDUlen; i++) {
168+
queuePDUs.push(queuePDUs.shift());
169+
}
170+
queueRetries.push(queueRetries.shift());
171+
queueHeaders.push(queueHeaders.shift());
172+
}
173+
serialState = SENDING; // trigger sendSerial()
174+
}
175+
}
176+
}
177+
178+
int checkRequest(byte buffer[], int bufferSize) {
179+
byte address;
180+
if (localConfig.enableRtuOverTcp) address = buffer[0];
181+
else address = buffer[6];
182+
183+
if (localConfig.enableRtuOverTcp) { // check CRC for Modbus RTU over TCP/UDP
184+
if (checkCRC(buffer, bufferSize) == false) {
185+
return -1; // reject: do nothing and return no error code
186+
}
187+
} else { // check MBAP header structure for Modbus TCP/UDP
188+
if (buffer[2] != 0x00 || buffer[3] != 0x00 || buffer[4] != 0x00 || buffer[5] + 6 != bufferSize) {
189+
return -1; // reject: do nothing and return no error code
190+
}
191+
}
192+
193+
if (queueHeaders.isEmpty() == false && slavesResponding[address] == false) { // allow only one request to non responding slaves
194+
for (byte j = queueHeaders.size(); j > 0 ; j--) { // start searching from tail because requests to non-responsive slaves are usually towards the tail of the queue
195+
if (queueHeaders[j - 1].uid == address) {
196+
return 0x0B; // return modbus error 11 (Gateway Target Device Failed to Respond) - usually means that target device (address) is not present
197+
}
198+
}
199+
}
200+
// check if we have space in request queue
201+
if (queueHeaders.available() < 1 || (localConfig.enableRtuOverTcp && queuePDUs.available() < bufferSize - 1) || (!localConfig.enableRtuOverTcp && queuePDUs.available() < bufferSize - 7)) {
202+
return 0x06; // return modbus error 6 (Slave Device Busy) - try again later
203+
}
204+
// al checkes passed OK, we can store the incoming data in request queue
205+
return 0;
206+
}
207+
208+
void deleteRequest() // delete request from queue
209+
{
210+
for (byte i = 0; i < queueHeaders.first().PDUlen; i++) {
211+
queuePDUs.shift();
212+
}
213+
queueHeaders.shift();
214+
queueRetries.shift();
215+
}

0 commit comments

Comments
 (0)