diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 9bf93bf55..eeaa50aba 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -754,6 +754,7 @@ //#define USE_DYP // Add support for DYP ME-007 ultrasonic distance sensor, serial port version (+0k5 code) #define USE_SERIAL_BRIDGE // Add support for software Serial Bridge (+0k8 code) //#define USE_MODBUS_BRIDGE // Add support for software Modbus Bridge (+3k code) +//#define USE_MODBUS_BRIDGE_TCP // Add support for software Modbus TCP Bridge (Also enable Modbus Bridge!) (? code) //#define USE_TCP_BRIDGE // Add support for Serial to TCP bridge (+1.3k code) //#define USE_MP3_PLAYER // Use of the DFPlayer Mini MP3 Player RB-DFR-562 commands: play, pause, stop, track, volume and reset #define MP3_VOLUME 30 // Set the startup volume on init, the range can be 0..100(max) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino b/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino index faf102934..af4d39896 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino @@ -17,19 +17,24 @@ along with this program. If not, see . */ -#ifdef USE_MODBUS_BRIDGE +#if defined(USE_MODBUS_BRIDGE) /*********************************************************************************************\ * Modbus Bridge using Modbus library (TasmotaModbus) * + * Can be used trough web/mqtt commands and also via direct TCP connection (when defined) + * + * When USE_MODBUS_BRIDGE_TCP is also defined, this bridge can also be used as an ModbusTCP + * bridge. + * * Example Command: - * ModbusSend {"deviceaddress": 1, "functioncode": 3, "startaddress": 1, "type":"uint8", "count":4} + * ModbusSend {"deviceaddress": 1, "functioncode": 3, "startaddress": 1, "type":"uint16", "count":2} \*********************************************************************************************/ -#define XDRV_63 63 +#define XDRV_63 63 -#define MBR_MAX_VALUE_LENGTH 30 -#define MBR_SPEED TM_MODBUS_BAUDRATE -#define MBR_MAX_REGISTERS 64 +#define MBR_MAX_VALUE_LENGTH 30 +#define MBR_SPEED TM_MODBUS_BAUDRATE +#define MBR_MAX_REGISTERS 64 #define D_CMND_MODBUS_SEND "Send" #define D_CMND_MODBUS_SETBAUDRATE "Baudrate" @@ -45,11 +50,41 @@ #define D_JSON_MODBUS_VALUES "Values" #define D_JSON_MODBUS_LENGTH "Length" +#ifndef USE_MODBUS_BRIDGE_TCP const char kModbusBridgeCommands[] PROGMEM = "Modbus|" // Prefix D_CMND_MODBUS_SEND "|" D_CMND_MODBUS_SETBAUDRATE "|" D_CMND_MODBUS_SETSERIALCONFIG; void (*const ModbusBridgeCommand[])(void) PROGMEM = { &CmndModbusBridgeSend, &CmndModbusBridgeSetBaudrate, &CmndModbusBridgeSetConfig}; +#endif + +#ifdef USE_MODBUS_BRIDGE_TCP + +#define MODBUS_BRIDGE_TCP_CONNECTIONS 1 // number of maximum parallel connections, only 1 supported with modbus +#define MODBUS_BRIDGE_TCP_BUF_SIZE 255 // size of the buffer, above 132 required for efficient XMODEM + +#define D_CMND_MODBUS_TCP_START "TCPStart" +#define D_CMND_MODBUS_TCP_CONNECT "TCPConnect" + +const char kModbusBridgeCommands[] PROGMEM = "Modbus|" // Prefix + D_CMND_MODBUS_TCP_START "|" D_CMND_MODBUS_TCP_CONNECT "|" D_CMND_MODBUS_SEND "|" D_CMND_MODBUS_SETBAUDRATE "|" D_CMND_MODBUS_SETSERIALCONFIG; + +void (*const ModbusBridgeCommand[])(void) PROGMEM = { + &CmndModbusTCPStart, &CmndModbusTCPConnect, + &CmndModbusBridgeSend, &CmndModbusBridgeSetBaudrate, &CmndModbusBridgeSetConfig}; + +struct ModbusBridgeTCP +{ + WiFiServer *server_tcp = nullptr; + WiFiClient client_tcp[MODBUS_BRIDGE_TCP_CONNECTIONS]; + uint8_t client_next = 0; + uint8_t *tcp_buf = nullptr; // data transfer buffer + IPAddress ip_filter; + uint16_t tcp_transaction_id = 0; +}; + +ModbusBridgeTCP modbusBridgeTCP; +#endif #include TasmotaModbus *tasmotaModbus = nullptr; @@ -80,15 +115,13 @@ enum class ModbusBridgeFunctionCode enum class ModbusBridgeType { mb_undefined, - mb_uint8, mb_uint16, mb_uint32, - mb_int8, mb_int16, mb_int32, mb_float, mb_raw, - mb_bit8 + mb_bit }; enum class ModbusBridgeEndian @@ -116,10 +149,21 @@ struct ModbusBridge ModbusBridge modbusBridge; /********************************************************************************************/ - -bool SetModbusBridgeBegin(void) +// +// Applies serial configuration to modbus serial port +// +bool ModbusBridgeBegin(void) { - return tasmotaModbus->begin(Settings->baudrate * 300, ConvertSerialConfig(Settings->sserial_config)); // Reinitialize modbus port with new baud rate + int result = tasmotaModbus->Begin(Settings->baudrate * 300, ConvertSerialConfig(Settings->sserial_config)); // Reinitialize modbus port with new baud rate + if (result) + { + if (2 == result) + { + ClaimSerial(); + } + AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR %s ser init at %d baud"), (2 == result ? "HW" : "SW"), Settings->baudrate * 300); + } + return result; } void SetModbusBridgeConfig(uint32_t serial_config) @@ -131,33 +175,83 @@ void SetModbusBridgeConfig(uint32_t serial_config) if (serial_config != Settings->sserial_config) { Settings->sserial_config = serial_config; - SetModbusBridgeBegin(); + ModbusBridgeBegin(); + } +} + +void SetModbusBridgeBaudrate(uint32_t baudrate) +{ + if (baudrate >= 300) + { + Settings->baudrate = baudrate / 300; + ModbusBridgeBegin(); } } /********************************************************************************************/ - +// +// Handles data received from tasmota modbus wrapper and send this to (TCP or) MQTT client +// void ModbusBridgeHandle(void) { bool data_ready = tasmotaModbus->ReceiveReady(); if (data_ready) { uint8_t *buffer; - buffer = (uint8_t *)malloc(5 + modbusBridge.registerCount); // Addres(1), Function(1), Length(1), Data(1..n), CRC(2) + buffer = (uint8_t *)malloc(5 + (modbusBridge.registerCount * 2)); // Addres(1), Function(1), Length(1), Data(1..n), CRC(2) uint32_t error = tasmotaModbus->ReceiveBuffer(buffer, modbusBridge.registerCount); - ModbusBridgeError errorcode = ModbusBridgeError::noerror; if (error) { AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver error %d"), error); + free(buffer); + return; } - else if (modbusBridge.deviceAddress == 0) + +#ifdef USE_MODBUS_BRIDGE_TCP + for (uint32_t i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++) + { + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + if (client) + { + uint8_t MBAP_Header[7]; + MBAP_Header[0] = modbusBridgeTCP.tcp_transaction_id >> 8; + MBAP_Header[1] = modbusBridgeTCP.tcp_transaction_id; + MBAP_Header[2] = 0; + MBAP_Header[3] = 0; + MBAP_Header[4] = ((modbusBridge.registerCount * 2) + 3) >> 8; + MBAP_Header[5] = (modbusBridge.registerCount * 2) + 3; + MBAP_Header[6] = buffer[0]; // Send slave address + client.write(MBAP_Header, 7); + client.write(buffer + 1, 1); // Send Functioncode + uint8_t bytecount[1]; + bytecount[0] = modbusBridge.registerCount * 2; + client.write(bytecount, 1); // Send length of rtu data + client.write(buffer + 3, (modbusBridge.registerCount * 2)); // Don't send CRC + client.flush(); + AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBRTCP from Modbus deviceAddress %d, writing %d bytes to client"), buffer[0], (modbusBridge.registerCount * 2) + 9); + } + } +#endif + + ModbusBridgeError errorcode = ModbusBridgeError::noerror; + if (modbusBridge.deviceAddress == 0) + { +#ifdef USE_MODBUS_BRIDGE_TCP + // If tcp client connected don't log error and exit this function (do not process) + if (nitems(modbusBridgeTCP.client_tcp)) + { + free(buffer); + return; + } +#endif errorcode = ModbusBridgeError::nodataexpected; + } else if (modbusBridge.deviceAddress != (uint8_t)buffer[0]) errorcode = ModbusBridgeError::wrongdeviceaddress; else if ((uint8_t)modbusBridge.functionCode != (uint8_t)buffer[1]) errorcode = ModbusBridgeError::wrongfunctioncode; - else if ((uint8_t)modbusBridge.registerCount != (uint8_t)buffer[2]) + else if ((uint8_t)modbusBridge.registerCount * 2 != (uint8_t)buffer[2]) errorcode = ModbusBridgeError::wrongregistercount; else { @@ -174,7 +268,7 @@ void ModbusBridgeHandle(void) ResponseJsonEnd(); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED)); } - else if ((buffer[1] > 0) && (buffer[0] < 5)) // Read Registers, writing is not supported at this moment + else if ((buffer[1] > 0) && (buffer[1] < 5)) // Read Registers, writing is not supported at this moment { Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{")); ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_DEVICE_ADDRESS "\":%d,"), buffer[0]); @@ -222,15 +316,7 @@ void ModbusBridgeHandle(void) else snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", value); } - else if ((modbusBridge.type == ModbusBridgeType::mb_int8) || - (modbusBridge.type == ModbusBridgeType::mb_uint8)) - { - if (modbusBridge.type == ModbusBridgeType::mb_int8) - snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", (int8_t)(buffer[3 + count])); - else - snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", (uint8_t)(buffer[3 + count])); - } - else if (modbusBridge.type == ModbusBridgeType::mb_bit8) + else if (modbusBridge.type == ModbusBridgeType::mb_bit) { uint8_t value = (uint8_t)(buffer[3 + count]); snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d%d%d%d%d%d%d%d", ((value >> 7) & 1), ((value >> 6) & 1), ((value >> 5) & 1), ((value >> 4) & 1), ((value >> 3) & 1), ((value >> 2) & 1), ((value >> 1) & 1), (value & 1)); @@ -247,6 +333,8 @@ void ModbusBridgeHandle(void) if (errorcode == ModbusBridgeError::noerror) MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED)); } + else + errorcode = ModbusBridgeError::wrongfunctioncode; } if (errorcode != ModbusBridgeError::noerror) { @@ -258,25 +346,120 @@ void ModbusBridgeHandle(void) } /********************************************************************************************/ - +// +// Inits the tasmota modbus driver, sets serialport and if TCP enabled allocates a TCP buffer +// void ModbusBridgeInit(void) { if (PinUsed(GPIO_MBR_RX) && PinUsed(GPIO_MBR_TX)) { tasmotaModbus = new TasmotaModbus(Pin(GPIO_MBR_RX), Pin(GPIO_MBR_TX)); - uint8_t result = tasmotaModbus->Begin(MBR_SPEED); - if (result) + SetModbusBridgeConfig(TS_SERIAL_8E1); + SetModbusBridgeBaudrate(MBR_SPEED); + +#ifdef USE_MODBUS_BRIDGE_TCP + // If TCP bridge is enabled allocate a TCP receive buffer + modbusBridgeTCP.tcp_buf = (uint8_t *)malloc(MODBUS_BRIDGE_TCP_BUF_SIZE); + if (!modbusBridgeTCP.tcp_buf) { - if (2 == result) - { - Serial.begin(MBR_SPEED, SERIAL_8E1); - ClaimSerial(); - } - AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR %s ser init at %d baud"), (2 == result ? "HW" : "SW"), MBR_SPEED); + AddLog(LOG_LEVEL_ERROR, PSTR("MBS: MBRTCP could not allocate buffer")); + return; } +#endif } } +#ifdef USE_MODBUS_BRIDGE_TCP +/********************************************************************************************/ +// +// Handles data for TCP server and TCP client. Sends requests to Modbus Devices +// +void ModbusTCPHandle(void) +{ + uint8_t c; + bool busy; // did we transfer some data? + int32_t buf_len; + + if (!tasmotaModbus) + return; + + // check for a new client connection + if ((modbusBridgeTCP.server_tcp) && (modbusBridgeTCP.server_tcp->hasClient())) + { + WiFiClient new_client = modbusBridgeTCP.server_tcp->available(); + + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBRTCP Got connection from %s"), new_client.remoteIP().toString().c_str()); + // Check for IP filtering if it's enabled. + if (modbusBridgeTCP.ip_filter) + { + if (modbusBridgeTCP.ip_filter != new_client.remoteIP()) + { + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBRTCP Rejected due to filtering")); + new_client.stop(); + } + else + { + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBRTCP Allowed through filter")); + } + } + + // find an empty slot + uint32_t i; + for (i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++) + { + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + if (!client) + { + client = new_client; + break; + } + } + if (i >= nitems(modbusBridgeTCP.client_tcp)) + { + i = modbusBridgeTCP.client_next++ % nitems(modbusBridgeTCP.client_tcp); + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + client.stop(); + client = new_client; + } + } + + do + { + busy = false; // exit loop if no data was transferred + + // handle data received from TCP + for (uint32_t i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++) + { + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + buf_len = 0; + while (client && (buf_len < MODBUS_BRIDGE_TCP_BUF_SIZE) && (client.available())) + { + c = client.read(); + if (c >= 0) + { + modbusBridgeTCP.tcp_buf[buf_len++] = c; + busy = true; + } + } + if (buf_len == 12) + { + uint8_t mbdeviceaddress = (uint8_t)modbusBridgeTCP.tcp_buf[6]; + uint8_t mbfunctioncode = (uint8_t)modbusBridgeTCP.tcp_buf[7]; + uint16_t mbstartaddress = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[8]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[9])); + modbusBridge.registerCount = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11])); + modbusBridgeTCP.tcp_transaction_id = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[0]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[1])); + + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("MBS: MBRTCP to Modbus Transactionid:%d, deviceAddress:%d, functionCode:%d, startAddress:%d, Count:%d"), + modbusBridgeTCP.tcp_transaction_id, mbdeviceaddress, mbfunctioncode, mbstartaddress, modbusBridge.registerCount); + + tasmotaModbus->Send(mbdeviceaddress, mbfunctioncode, mbstartaddress, modbusBridge.registerCount); + } + } + yield(); // avoid WDT if heavy traffic + } while (busy); +} +#endif + /*********************************************************************************************\ * Commands \*********************************************************************************************/ @@ -309,55 +492,46 @@ void CmndModbusBridgeSend(void) } modbusBridge.type = ModbusBridgeType::mb_undefined; - if (strcmp(stype, "int8") == 0) - { - modbusBridge.type = ModbusBridgeType::mb_int8; - modbusBridge.registerCount = 1 * modbusBridge.count; - } - else if (strcmp(stype, "int16") == 0) + if (strcmp(stype, "int16") == 0) { modbusBridge.type = ModbusBridgeType::mb_int16; - modbusBridge.registerCount = 2 * modbusBridge.count; + modbusBridge.registerCount = modbusBridge.count; } else if (strcmp(stype, "int32") == 0) { modbusBridge.type = ModbusBridgeType::mb_int32; - modbusBridge.registerCount = 4 * modbusBridge.count; - } - else if (strcmp(stype, "uint8") == 0) - { - modbusBridge.type = ModbusBridgeType::mb_uint8; - modbusBridge.registerCount = 1 * modbusBridge.count; + modbusBridge.registerCount = 2 * modbusBridge.count; } else if (strcmp(stype, "uint16") == 0) { modbusBridge.type = ModbusBridgeType::mb_uint16; - modbusBridge.registerCount = 2 * modbusBridge.count; + modbusBridge.registerCount = modbusBridge.count; } else if (strcmp(stype, "uint32") == 0) { modbusBridge.type = ModbusBridgeType::mb_uint32; - modbusBridge.registerCount = 4 * modbusBridge.count; + modbusBridge.registerCount = 2 * modbusBridge.count; } else if (strcmp(stype, "float") == 0) { modbusBridge.type = ModbusBridgeType::mb_float; - modbusBridge.registerCount = 4 * modbusBridge.count; + modbusBridge.registerCount = 2 * modbusBridge.count; } else if (strcmp(stype, "raw") == 0) { modbusBridge.type = ModbusBridgeType::mb_raw; modbusBridge.registerCount = modbusBridge.count; } - else if (strcmp(stype, "bit8") == 0) + else if (strcmp(stype, "bit") == 0) { - modbusBridge.type = ModbusBridgeType::mb_bit8; + modbusBridge.type = ModbusBridgeType::mb_bit; modbusBridge.registerCount = modbusBridge.count; } else errorcode = ModbusBridgeError::wrongtype; - if (modbusBridge.registerCount > MBR_MAX_REGISTERS) errorcode = ModbusBridgeError::wrongcount; + if (modbusBridge.registerCount > MBR_MAX_REGISTERS) + errorcode = ModbusBridgeError::wrongcount; if (errorcode != ModbusBridgeError::noerror) { @@ -371,13 +545,8 @@ void CmndModbusBridgeSend(void) void CmndModbusBridgeSetBaudrate(void) { - if (XdrvMailbox.payload >= 300) - { - XdrvMailbox.payload /= 300; // Make it a valid baudrate - Settings->sbaudrate = XdrvMailbox.payload; - SetModbusBridgeBegin(); - } - ResponseCmndNumber(Settings->sbaudrate * 300); + SetModbusBridgeBaudrate(XdrvMailbox.payload); + ResponseCmndNumber(Settings->baudrate * 300); } void CmndModbusBridgeSetConfig(void) @@ -407,6 +576,114 @@ void CmndModbusBridgeSetConfig(void) ResponseCmndChar(GetSerialConfig(Settings->sserial_config).c_str()); } +#ifdef USE_MODBUS_BRIDGE_TCP +// +// Command `TCPStart` +// Params: port, +// +void CmndModbusTCPStart(void) +{ + + if (!tasmotaModbus) + { + return; + } + + int32_t tcp_port = XdrvMailbox.payload; + if (ArgC() == 2) + { + char sub_string[XdrvMailbox.data_len]; + modbusBridgeTCP.ip_filter.fromString(ArgV(sub_string, 2)); + } + else + { + // Disable whitelist if previously set + modbusBridgeTCP.ip_filter = (uint32_t)0; + } + + if (modbusBridgeTCP.server_tcp) + { + AddLog(LOG_LEVEL_INFO, PSTR("MBS: MBRTCP Stopping server")); + modbusBridgeTCP.server_tcp->stop(); + delete modbusBridgeTCP.server_tcp; + modbusBridgeTCP.server_tcp = nullptr; + + for (uint32_t i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++) + { + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + client.stop(); + } + } + if (tcp_port > 0) + { + AddLog(LOG_LEVEL_INFO, PSTR("MBS: MBRTCP Starting server on port %d"), tcp_port); + if (modbusBridgeTCP.ip_filter) + { + AddLog(LOG_LEVEL_INFO, PSTR("MBS: MBRTCP Filtering %s"), modbusBridgeTCP.ip_filter.toString().c_str()); + } + modbusBridgeTCP.server_tcp = new WiFiServer(tcp_port); + modbusBridgeTCP.server_tcp->begin(); // start TCP server + modbusBridgeTCP.server_tcp->setNoDelay(true); + } + + ResponseCmndDone(); +} + +// +// Command `Connect` +// Params: port, +// +void CmndModbusTCPConnect(void) +{ + int32_t tcp_port = XdrvMailbox.payload; + + if (!tasmotaModbus) + { + return; + } + + if (ArgC() == 2) + { + char sub_string[XdrvMailbox.data_len]; + WiFiClient new_client; + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBRTCP Connecting to %s on port %d"), ArgV(sub_string, 2), tcp_port); + if (new_client.connect(ArgV(sub_string, 2), tcp_port)) + { + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBRTCP connected!")); + } + else + { + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBRTCP error connecting!")); + } + + // find an empty slot + uint32_t i; + for (i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++) + { + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + if (!client) + { + client = new_client; + break; + } + } + if (i >= nitems(modbusBridgeTCP.client_tcp)) + { + i = modbusBridgeTCP.client_next++ % nitems(modbusBridgeTCP.client_tcp); + WiFiClient &client = modbusBridgeTCP.client_tcp[i]; + client.stop(); + client = new_client; + } + } + else + { + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_TCP "MBS: MBR Usage: port,ip_address")); + } + + ResponseCmndDone(); +} +#endif + /*********************************************************************************************\ * Interface \*********************************************************************************************/ @@ -423,12 +700,15 @@ bool Xdrv63(uint8_t function) { switch (function) { - case FUNC_EVERY_250_MSECOND: - ModbusBridgeHandle(); - break; case FUNC_COMMAND: result = DecodeCommand(kModbusBridgeCommands, ModbusBridgeCommand); break; + case FUNC_LOOP: + ModbusBridgeHandle(); +#ifdef USE_MODBUS_BRIDGE_TCP + ModbusTCPHandle(); +#endif + break; } } return result;