Tasmota/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino

1345 lines
50 KiB
Arduino
Raw Normal View History

2022-07-19 09:37:03 +01:00
/*
xdrv_63_modbus_bridge.ino - modbus bridge support for Tasmota
2022-07-19 09:37:03 +01:00
Copyright (C) 2021 Theo Arends and Jeroenst
2022-07-19 09:37:03 +01:00
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(USE_MODBUS_BRIDGE)
2022-07-19 09:37:03 +01:00
/*********************************************************************************************\
* Modbus Bridge using Modbus library (TasmotaModbus)
*
* Can be used trough web/mqtt commands and also via direct TCP connection (when defined)
2022-08-17 18:55:41 +01:00
*
* When USE_MODBUS_BRIDGE_TCP is also defined, this bridge can also be used as an ModbusTCP
* bridge.
*
* Example Commands:
* -- Read Coils --
* ModbusSend {"deviceaddress": 1, "functioncode": 1, "startaddress": 1, "type":"bit", "count":2}
*
* -- Read Input Register --
* ModbusSend {"deviceaddress": 1, "functioncode": 3, "startaddress": 1, "type":"uint16", "count":2}
*
* -- Write multiple coils --
* ModbusSend {"deviceaddress": 1, "functioncode": 15, "startaddress": 1, "type":"bit", "count":4, "values":[1,0,1,1]}
*
* Info for modbusBridgeTCPServer:
* https://ipc2u.com/articles/knowledge-base/detailed-description-of-the-modbus-tcp-protocol-with-command-examples/
*
* Info for modbus serial communications:
* https://ozeki.hu/p_5879-mobdbus-function-code-4-read-input-registers.html
* https://www.modbustools.com/modbus.html
* https://ipc2u.com/articles/knowledge-base/modbus-rtu-made-simple-with-detailed-descriptions-and-examples/
2022-07-19 09:37:03 +01:00
\*********************************************************************************************/
#define XDRV_63 63
2022-07-19 09:37:03 +01:00
#define MBR_MAX_VALUE_LENGTH 30
#define MBR_BAUDRATE TM_MODBUS_BAUDRATE
#define MBR_MAX_REGISTERS 64
#define MBR_RECEIVE_BUFFER_SIZE (MBR_MAX_REGISTERS * 2) + 9 // Addres(1), Function(1), Length(1) or StartAddress(2), N/A or Number of addresses(2),Data(1..n), CRC(2)
#define D_CMND_MODBUS_SEND "Send"
#define D_CMND_MODBUS_SETBAUDRATE "Baudrate"
#define D_CMND_MODBUS_SETSERIALCONFIG "SerialConfig"
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
#define D_CMND_MODBUS_SETSERIALTIMEOUT "SerialTimeout"
2022-07-19 09:37:03 +01:00
#define D_JSON_MODBUS_RECEIVED "ModbusReceived"
#define D_JSON_MODBUS_DEVICE_ADDRESS "DeviceAddress"
#define D_JSON_MODBUS_FUNCTION_CODE "FunctionCode"
#define D_JSON_MODBUS_START_ADDRESS "StartAddress"
#define D_JSON_MODBUS_COUNT "Count"
#define D_JSON_MODBUS_ENDIAN "Endian"
#define D_JSON_MODBUS_TYPE "Type" // allready defined
#define D_JSON_MODBUS_VALUES "Values"
#define D_JSON_MODBUS_LENGTH "Length"
#ifndef USE_MODBUS_BRIDGE_TCP
const char kModbusBridgeCommands[] PROGMEM = "Modbus|" // Prefix
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
D_CMND_MODBUS_SEND "|" D_CMND_MODBUS_SETBAUDRATE "|" D_CMND_MODBUS_SETSERIALCONFIG "|" D_CMND_MODBUS_SETSERIALTIMEOUT;
2022-07-19 09:37:03 +01:00
void (*const ModbusBridgeCommand[])(void) PROGMEM = {
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
&CmndModbusBridgeSend, &CmndModbusBridgeSetBaudrate, &CmndModbusBridgeSetConfig, &CmndModbusBridgeSetTimeout};
#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"
#define D_CMND_MODBUS_TCP_MQTT "TCPMqtt"
const char kModbusBridgeCommands[] PROGMEM = "Modbus|" // Prefix
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
D_CMND_MODBUS_TCP_START "|" D_CMND_MODBUS_TCP_CONNECT "|" D_CMND_MODBUS_TCP_MQTT "|" D_CMND_MODBUS_SEND "|" D_CMND_MODBUS_SETBAUDRATE "|" D_CMND_MODBUS_SETSERIALCONFIG "|" D_CMND_MODBUS_SETSERIALTIMEOUT;
void (*const ModbusBridgeCommand[])(void) PROGMEM = {
&CmndModbusTCPStart, &CmndModbusTCPConnect, &CmndModbusTCPMqtt,
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
&CmndModbusBridgeSend, &CmndModbusBridgeSetBaudrate, &CmndModbusBridgeSetConfig, &CmndModbusBridgeSetTimeout};
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;
# define MODBUS_TCP_INVALID_TRANSACTION_ID UINT32_MAX
// Invalid ID value denotes, that the data 'tcp_buf' points to
// should not be used to send Modbus TCP response (very likely
// the response was already sent).
uint32_t tcp_transaction_id = MODBUS_TCP_INVALID_TRANSACTION_ID;
bool output_mqtt = false;
};
ModbusBridgeTCP modbusBridgeTCP;
#endif // USE_MODBUS_BRIDGE_TCP
2022-07-19 09:37:03 +01:00
#include <TasmotaModbus.h>
TasmotaModbus *modbusBridgeModbus = nullptr;
2022-07-19 09:37:03 +01:00
enum class ModbusBridgeError
{
noerror = 0,
nodataexpected = 1,
wrongdeviceaddress = 2,
wrongfunctioncode = 3,
wrongstartaddress = 4,
wrongtype = 5,
2022-08-17 18:55:41 +01:00
wrongdataCount = 6,
2022-08-17 18:55:41 +01:00
wrongcount = 7,
tomanydata = 8
2022-07-19 09:37:03 +01:00
};
enum class ModbusBridgeFunctionCode
{
mb_undefined = 0,
mb_readCoilStatus = 1,
mb_readInputStatus = 2,
mb_readHoldingRegisters = 3,
mb_readInputRegisters = 4,
2022-07-19 09:37:03 +01:00
mb_writeSingleCoil = 5,
mb_writeSingleRegister = 6,
mb_writeMultipleCoils = 15,
mb_writeMultipleRegisters = 16
2022-07-19 09:37:03 +01:00
};
enum class ModbusBridgeType
{
mb_undefined,
mb_uint8,
2022-07-19 09:37:03 +01:00
mb_uint16,
mb_uint32,
mb_int8,
2022-07-19 09:37:03 +01:00
mb_int16,
mb_int32,
mb_float,
mb_raw,
mb_hex,
mb_bit
2022-07-19 09:37:03 +01:00
};
enum class ModbusBridgeEndian
{
mb_undefined,
mb_msb,
mb_lsb
};
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
#ifndef MODBUS_SERIAL_TIMEOUT_MS
#define MODBUS_SERIAL_TIMEOUT_MS 1000
#endif
2022-07-19 09:37:03 +01:00
struct ModbusBridge
{
unsigned long polling_window = 0;
int in_byte_counter = 0;
ModbusBridgeFunctionCode functionCode = ModbusBridgeFunctionCode::mb_undefined;
ModbusBridgeType type = ModbusBridgeType::mb_undefined;
ModbusBridgeEndian endian = ModbusBridgeEndian::mb_undefined;
2022-07-19 09:37:03 +01:00
uint16_t dataCount = 0; // Number of bits or registers to read/write
uint16_t byteCount = 0; // Number of bytes to read/write
uint16_t startAddress = 0; // First address to read/write
uint8_t deviceAddress = 0; // Modbus address of device to read
uint8_t count = 0; // Number of values to read / write
bool raw = false;
uint8_t *buffer = nullptr; // Buffer for storing read / write data
bool enabled = false;
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
// Buffer to store command data received from CmndModbusBridgeSend()
char *command_data = nullptr;
private:
// Timeout in [ms]. How long we will wait for Modbus response.
uint32_t modbusSerialTimeout_ms = MODBUS_SERIAL_TIMEOUT_MS;
// Holds the value of millis() after we set
// waitingForAnswerFromSerial flag to true.
uint32_t sendDataToSerial_ms;
// If true, then do not sent another Modbus request until:
// millis() - sendDataToSerial_ms > modbusSerialTimeout_ms
bool waitingForAnswerFromSerial = false;
public:
void setModbusSerialTimeout_ms(const uint32_t new_timeout)
{
modbusSerialTimeout_ms = new_timeout;
}
uint32_t getModbusSerialTimeout_ms() const
{
return modbusSerialTimeout_ms;
}
void setWaitingForAnswerFromSerial(const bool new_value)
{
waitingForAnswerFromSerial = new_value;
if (waitingForAnswerFromSerial)
sendDataToSerial_ms = millis();
}
bool isWaitingForAnswerFromSerial() const
{
return waitingForAnswerFromSerial;
}
bool isWaitingForAnswerFromSerialTimedOut() const
{
const auto t1 = millis() - sendDataToSerial_ms;
return (t1 > modbusSerialTimeout_ms) ? true : false;
}
2022-07-19 09:37:03 +01:00
};
ModbusBridge modbusBridge;
/********************************************************************************************/
//
// Helper functions
//
uint16_t ModbusBridgeSwapEndian16(uint16_t num)
{
return (num>>8) | (num<<8);
}
2022-11-06 09:12:08 +00:00
void ModbusBridgeAllocError(const char* s)
{
AddLog(LOG_LEVEL_ERROR, PSTR("MBS: could not allocate %s buffer"), s);
}
2022-07-19 09:37:03 +01:00
/********************************************************************************************/
//
// Applies serial configuration to modbus serial port
//
bool ModbusBridgeBegin(void) {
if ((Settings->modbus_sbaudrate < 1) || (Settings->modbus_sbaudrate > (115200 / 300))) {
2022-08-17 18:55:41 +01:00
Settings->modbus_sbaudrate = (uint8_t)((uint32_t)MBR_BAUDRATE / 300);
}
if (Settings->modbus_sconfig > TS_SERIAL_8O2) {
2022-08-17 18:55:41 +01:00
Settings->modbus_sconfig = TS_SERIAL_8N1;
}
int result = modbusBridgeModbus->Begin(Settings->modbus_sbaudrate * 300, ConvertSerialConfig(Settings->modbus_sconfig)); // Reinitialize modbus port with new baud rate
if (result) {
if (2 == result) {
ClaimSerial();
}
#ifdef ESP32
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: Serial UART%d"), modbusBridgeModbus->getUart());
#endif
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR %s ser init at %d baud"), (2 == result ? "HW" : "SW"), Settings->modbus_sbaudrate * 300);
if (nullptr == modbusBridge.buffer) {
modbusBridge.buffer = (uint8_t *)malloc(MBR_RECEIVE_BUFFER_SIZE);
}
if (nullptr == modbusBridge.buffer) {
ModbusBridgeAllocError(PSTR("BUFFER"));
result = false;
}
}
return result;
2022-07-19 09:37:03 +01:00
}
void ModbusBridgeSetConfig(uint32_t serial_config)
2022-07-19 09:37:03 +01:00
{
if (serial_config > TS_SERIAL_8O2)
{
serial_config = TS_SERIAL_8N1;
}
if (serial_config != Settings->modbus_sconfig)
2022-07-19 09:37:03 +01:00
{
Settings->modbus_sconfig = serial_config;
ModbusBridgeBegin();
2022-07-19 09:37:03 +01:00
}
}
void ModbusBridgeSetBaudrate(uint32_t baudrate)
{
if ((baudrate >= 300) && (baudrate <= 115200))
{
2022-08-17 18:55:41 +01:00
if (baudrate / 300 != Settings->modbus_sbaudrate)
{
Settings->modbus_sbaudrate = baudrate / 300;
ModbusBridgeBegin();
}
}
}
2022-07-19 09:37:03 +01:00
/********************************************************************************************/
//
// Handles data received from tasmota modbus wrapper and send this to (TCP and/or) MQTT client
//
2022-07-19 09:37:03 +01:00
void ModbusBridgeHandle(void)
{
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
uint32_t error = 0;
bool data_ready = modbusBridgeModbus->ReceiveReady();
2022-07-19 09:37:03 +01:00
if (data_ready)
{
if (modbusBridge.byteCount == 0) modbusBridge.byteCount = modbusBridge.dataCount * 2;
if (nullptr == modbusBridge.buffer) // If buffer is not initialized do not process received data
2022-11-06 09:12:08 +00:00
{
ModbusBridgeAllocError(PSTR("read"));
modbusBridge.dataCount = 0;
modbusBridge.byteCount = 0;
2022-11-06 09:12:08 +00:00
return;
}
memset(modbusBridge.buffer, 0, MBR_RECEIVE_BUFFER_SIZE);
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
error = modbusBridgeModbus->ReceiveBuffer(modbusBridge.buffer, 0, MBR_RECEIVE_BUFFER_SIZE - 9);
modbusBridge.setWaitingForAnswerFromSerial(false);
}
else if (modbusBridge.isWaitingForAnswerFromSerial()
&& modbusBridge.isWaitingForAnswerFromSerialTimedOut())
{
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Recv timed out"));
modbusBridge.setWaitingForAnswerFromSerial(false);
// MODBUS Application Protocol Specification V1.1b3,
// p.7 MODBUS Exception Responses
error = 11; // The targeted device failed to respond
}
2022-07-19 09:37:03 +01:00
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
if (data_ready || error)
{
#ifdef USE_MODBUS_BRIDGE_TCP
for (uint32_t i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++)
{
WiFiClient &client = modbusBridgeTCP.client_tcp[i];
if (client && modbusBridgeTCP.tcp_transaction_id != MODBUS_TCP_INVALID_TRANSACTION_ID)
{
uint8_t header[9];
uint8_t nrOfBytes = 8;
header[0] = modbusBridgeTCP.tcp_transaction_id >> 8;
header[1] = modbusBridgeTCP.tcp_transaction_id;
header[2] = 0;
header[3] = 0;
header[6] = modbusBridge.buffer[0]; // Send slave address
header[7] = modbusBridge.buffer[1]; // Send function code
if (error)
{
header[4] = 0; // Message Length Hi-Byte
header[5] = 3; // Message Length Low-Byte
header[7] = modbusBridge.buffer[1] | 0x80; // Send function code
header[8] = error;
nrOfBytes += 1;
client.write(header, 9);
}
else if (modbusBridge.buffer[1] <= 4)
{
uint8_t received_data_bytes = modbusBridgeModbus->ReceiveCount() - 5;
header[4] = received_data_bytes >> 8;
header[5] = received_data_bytes + 3;
header[8] = received_data_bytes;
client.write(header, 9);
nrOfBytes += 1;
client.write(modbusBridge.buffer + 3, received_data_bytes); // Don't send CRC
nrOfBytes += received_data_bytes;
}
else
{
header[4] = 0; // Message Length Hi-Byte
header[5] = 6; // Message Length Low-Byte
client.write(header, 8);
client.write(modbusBridge.buffer + 2, 4); // Don't send CRC
nrOfBytes += 4;
}
client.flush();
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBRTCP from Modbus TransactionId:%d, deviceAddress:%d, writing:%d bytes to client (error:%d)"), (static_cast<uint16_t>(header[0]) << 8) + header[1], modbusBridge.buffer[0], nrOfBytes, error);
modbusBridgeTCP.tcp_transaction_id = MODBUS_TCP_INVALID_TRANSACTION_ID;
}
2022-07-19 09:37:03 +01:00
}
#endif
modbusBridge.byteCount = 0;
if (error)
{
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver receive error %d"), error);
modbusBridge.dataCount = 0;
return;
}
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) && !modbusBridgeTCP.output_mqtt)
{
return;
}
#endif
2022-07-19 09:37:03 +01:00
errorcode = ModbusBridgeError::nodataexpected;
}
else if (modbusBridge.deviceAddress != (uint8_t)modbusBridge.buffer[0])
2022-07-19 09:37:03 +01:00
errorcode = ModbusBridgeError::wrongdeviceaddress;
else if ((uint8_t)modbusBridge.functionCode != (uint8_t)modbusBridge.buffer[1])
2022-07-19 09:37:03 +01:00
errorcode = ModbusBridgeError::wrongfunctioncode;
else if ((uint8_t)modbusBridge.functionCode < 5)
{
// Do not check buffer[2] but received bytes for correct length but use the nr of received bytes
uint8_t received_data_bytes = modbusBridgeModbus->ReceiveCount() - 5;
if ((uint8_t)modbusBridge.functionCode < 3)
{
// Check if returned number of bits matches the requested number of bits
if ((uint8_t)(((modbusBridge.dataCount - 1) >> 3) + 1) > received_data_bytes)
errorcode = ModbusBridgeError::wrongdataCount;
}
else
{
if ((modbusBridge.type == ModbusBridgeType::mb_int8 || modbusBridge.type == ModbusBridgeType::mb_uint8) && ((uint8_t)modbusBridge.dataCount > received_data_bytes))
errorcode = ModbusBridgeError::wrongdataCount;
else if ((modbusBridge.type == ModbusBridgeType::mb_bit) && ((uint8_t)modbusBridge.dataCount > received_data_bytes))
errorcode = ModbusBridgeError::wrongdataCount;
else if ((modbusBridge.type == ModbusBridgeType::mb_int16 || modbusBridge.type == ModbusBridgeType::mb_uint16) && ((uint8_t)modbusBridge.dataCount > received_data_bytes))
errorcode = ModbusBridgeError::wrongdataCount;
else if ((modbusBridge.type == ModbusBridgeType::mb_int32 || modbusBridge.type == ModbusBridgeType::mb_uint32 || modbusBridge.type == ModbusBridgeType::mb_float) && ((uint8_t)modbusBridge.dataCount > received_data_bytes))
errorcode = ModbusBridgeError::wrongdataCount;
}
}
if (errorcode == ModbusBridgeError::noerror)
2022-07-19 09:37:03 +01:00
{
if (modbusBridge.type == ModbusBridgeType::mb_raw)
{
// Ouput raw data as decimal bytes
Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{\"RAW\":["));
for (uint8_t i = 0; i < modbusBridgeModbus->ReceiveCount(); i++)
2022-07-19 09:37:03 +01:00
{
ResponseAppend_P(PSTR("%d"), modbusBridge.buffer[i]);
if (i < modbusBridgeModbus->ReceiveCount() - 1)
2022-07-19 09:37:03 +01:00
ResponseAppend_P(PSTR(","));
}
ResponseAppend_P(PSTR("]}"));
ResponseJsonEnd();
if (Settings->flag6.mqtt_disable_modbus ) { // SetOption158 If it is activated, Tasmota will not publish ModbusReceived MQTT messages, but it will proccess event trigger rules
XdrvRulesProcess(0);
} else {
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
}
2022-07-19 09:37:03 +01:00
}
else if (modbusBridge.type == ModbusBridgeType::mb_hex)
{
// Output raw data as hexadecimal bytes
Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{\"HEX\":["));
for (uint8_t i = 0; i < modbusBridgeModbus->ReceiveCount(); i++)
{
ResponseAppend_P(PSTR("0x%02X"), modbusBridge.buffer[i]);
if (i < modbusBridgeModbus->ReceiveCount() - 1)
ResponseAppend_P(PSTR(","));
}
ResponseAppend_P(PSTR("]}"));
ResponseJsonEnd();
if (Settings->flag6.mqtt_disable_modbus ) { // SetOption158 If it is activated, Tasmota will not publish ModbusReceived MQTT messages, but it will proccess event trigger rules
XdrvRulesProcess(0);
} else {
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
}
}
else if ((modbusBridge.buffer[1] > 0) && (modbusBridge.buffer[1] < 7))
2022-07-19 09:37:03 +01:00
{
// Read and process Registers
uint8_t dataOffset = 3;
2022-07-19 09:37:03 +01:00
Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{"));
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_DEVICE_ADDRESS "\":%d,"), modbusBridge.buffer[0]);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_FUNCTION_CODE "\":%d,"), modbusBridge.buffer[1]);
if (modbusBridge.buffer[1] < 5)
{
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), modbusBridge.startAddress);
}
else
{
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), (modbusBridge.buffer[2] << 8) + modbusBridge.buffer[3]);
dataOffset = 4;
}
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_LENGTH "\":%d,"), modbusBridgeModbus->ReceiveCount());
2022-07-19 09:37:03 +01:00
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_COUNT "\":%d,"), modbusBridge.count);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_VALUES "\":["));
uint8_t data_count = modbusBridge.count;
if ((uint8_t)modbusBridge.functionCode < 3)
{
// Calculate number of values to return in bitmode
if (modbusBridge.type == ModbusBridgeType::mb_int8 || modbusBridge.type == ModbusBridgeType::mb_uint8)
data_count = (uint8_t)(((modbusBridge.count - 1) >> 3) + 1);
else if (modbusBridge.type == ModbusBridgeType::mb_int16 || modbusBridge.type == ModbusBridgeType::mb_uint16)
data_count = (uint8_t)(((modbusBridge.count - 1) >> 4) + 1);
else if (modbusBridge.type == ModbusBridgeType::mb_int32 || modbusBridge.type == ModbusBridgeType::mb_uint32 || modbusBridge.type == ModbusBridgeType::mb_float)
data_count = (uint8_t)(((modbusBridge.count - 1) >> 5) + 1);
}
// Copy modbus data to requested variables type
for (uint8_t count = 0; count < data_count; count++)
2022-07-19 09:37:03 +01:00
{
char svalue[MBR_MAX_VALUE_LENGTH + 1] = "";
if (modbusBridge.type == ModbusBridgeType::mb_float)
{
// Convert next 4 bytes to float
2022-07-19 09:37:03 +01:00
float value = 0;
if (modbusBridge.endian == ModbusBridgeEndian::mb_lsb)
{
// In lsb mode swap bytes
if (modbusBridge.buffer[2] - (count * 4))
((uint8_t *)&value)[0] = modbusBridge.buffer[dataOffset + (count * 4)]; // Get float values
if ((modbusBridge.buffer[2] - (count * 4)) >> 1)
((uint8_t *)&value)[1] = modbusBridge.buffer[dataOffset + 1 + (count * 4)];
if ((modbusBridge.buffer[2] - (count * 4) - 1) >> 1)
((uint8_t *)&value)[2] = modbusBridge.buffer[dataOffset + 2 + (count * 4)];
if ((modbusBridge.buffer[2] - (count * 4)) >> 2)
((uint8_t *)&value)[3] = modbusBridge.buffer[dataOffset + 3 + (count * 4)];
}
else
{
((uint8_t *)&value)[3] = modbusBridge.buffer[dataOffset + (count * 4)]; // Get float values
((uint8_t *)&value)[2] = modbusBridge.buffer[dataOffset + 1 + (count * 4)];
((uint8_t *)&value)[1] = modbusBridge.buffer[dataOffset + 2 + (count * 4)];
((uint8_t *)&value)[0] = modbusBridge.buffer[dataOffset + 3 + (count * 4)];
}
2022-07-19 09:37:03 +01:00
ext_snprintf_P(svalue, sizeof(svalue), "%*_f", 10, &value);
}
else if (modbusBridge.type == ModbusBridgeType::mb_bit)
{
uint8_t bits_left = modbusBridge.count - ((count/8) * 8);
uint8_t value = 0;
if (bits_left < 8)
{
uint8_t bits_skip = 8 - bits_left;
value = (uint8_t)(modbusBridge.buffer[dataOffset + ((count + bits_skip) >> 3)]);
}
else
{
value = (uint8_t)(modbusBridge.buffer[dataOffset + (count >> 3)]);
}
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", ((value >> (count & 7)) & 1));
}
2022-07-19 09:37:03 +01:00
else
{
if ((modbusBridge.type == ModbusBridgeType::mb_int32) ||
(modbusBridge.type == ModbusBridgeType::mb_uint32))
{
uint32_t value = 0;
if (modbusBridge.endian == ModbusBridgeEndian::mb_lsb)
{
// In lsb mode swap bytes
if (modbusBridge.buffer[2] - (count * 4))
((uint8_t *)&value)[0] = modbusBridge.buffer[dataOffset + (count * 4)]; // Get uint values
if (modbusBridge.buffer[2] - ((count * 4) - 1))
((uint8_t *)&value)[1] = modbusBridge.buffer[dataOffset + 1 + (count * 4)];
if (modbusBridge.buffer[2] - ((count * 4) - 2))
((uint8_t *)&value)[2] = modbusBridge.buffer[dataOffset + 2 + (count * 4)];
if (modbusBridge.buffer[2] - ((count * 4) - 3))
((uint8_t *)&value)[3] = modbusBridge.buffer[dataOffset + 3 + (count * 4)];
}
else
{
((uint8_t *)&value)[3] = modbusBridge.buffer[dataOffset + (count * 4)]; // Get uint values
((uint8_t *)&value)[2] = modbusBridge.buffer[dataOffset + 1 + (count * 4)];
((uint8_t *)&value)[1] = modbusBridge.buffer[dataOffset + 2 + (count * 4)];
((uint8_t *)&value)[0] = modbusBridge.buffer[dataOffset + 3 + (count * 4)];
}
2022-07-19 09:37:03 +01:00
if (modbusBridge.type == ModbusBridgeType::mb_int32)
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", (int32_t)value);
2022-07-19 09:37:03 +01:00
else
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", value);
}
else if ((modbusBridge.type == ModbusBridgeType::mb_int16) ||
(modbusBridge.type == ModbusBridgeType::mb_uint16))
{
uint16_t value = 0;
if (modbusBridge.endian == ModbusBridgeEndian::mb_lsb)
{
// In lsb mode swap bytes
if (modbusBridge.buffer[2] - (count * 2))
((uint8_t *)&value)[0] = modbusBridge.buffer[dataOffset + (count * 2)];
if (modbusBridge.buffer[2] - ((count * 2) - 1))
((uint8_t *)&value)[1] = modbusBridge.buffer[dataOffset + 1 + (count * 2)];
}
else
{
((uint8_t *)&value)[1] = modbusBridge.buffer[dataOffset + (count * 2)];
((uint8_t *)&value)[0] = modbusBridge.buffer[dataOffset + 1 + (count * 2)];
}
2022-07-19 09:37:03 +01:00
if (modbusBridge.type == ModbusBridgeType::mb_int16)
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", (int16_t)value);
2022-07-19 09:37:03 +01:00
else
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", value);
}
else if ((modbusBridge.type == ModbusBridgeType::mb_int8) ||
(modbusBridge.type == ModbusBridgeType::mb_uint8))
{
uint8_t value = modbusBridge.buffer[dataOffset + (count * 1)];
if (modbusBridge.type == ModbusBridgeType::mb_int8)
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", (int8_t)value);
else
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", value);
}
2022-07-19 09:37:03 +01:00
}
ResponseAppend_P(PSTR("%s"), svalue);
if (count < data_count - 1)
2022-07-19 09:37:03 +01:00
ResponseAppend_P(PSTR(","));
}
ResponseAppend_P(PSTR("]}"));
ResponseJsonEnd();
if (errorcode == ModbusBridgeError::noerror)
if (Settings->flag6.mqtt_disable_modbus ) { // SetOption158 If it is activated, Tasmota will not publish ModbusReceived MQTT messages, but it will proccess event trigger rules
XdrvRulesProcess(0);
} else {
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
}
2022-07-19 09:37:03 +01:00
}
else if ((modbusBridge.buffer[1] == 15) || (modbusBridge.buffer[1] == 16)) // Write Multiple Registers
2022-08-17 18:55:41 +01:00
{
Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{"));
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_DEVICE_ADDRESS "\":%d,"), modbusBridge.buffer[0]);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_FUNCTION_CODE "\":%d,"), modbusBridge.buffer[1]);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), (modbusBridge.buffer[2] << 8) + modbusBridge.buffer[3]);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_LENGTH "\":%d,"), modbusBridgeModbus->ReceiveCount());
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_COUNT "\":%d"), (modbusBridge.buffer[4] << 8) + modbusBridge.buffer[5]);
2022-08-17 18:55:41 +01:00
ResponseAppend_P(PSTR("}"));
ResponseJsonEnd();
if (errorcode == ModbusBridgeError::noerror)
if (Settings->flag6.mqtt_disable_modbus ) { // SetOption158 If it is activated, Tasmota will not publish ModbusReceived MQTT messages, but it will proccess event trigger rules
XdrvRulesProcess(0);
} else {
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
}
2022-08-17 18:55:41 +01:00
}
else
errorcode = ModbusBridgeError::wrongfunctioncode;
2022-07-19 09:37:03 +01:00
}
if (errorcode != ModbusBridgeError::noerror)
{
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Recv Error %d"), (uint8_t)errorcode);
}
modbusBridge.deviceAddress = 0;
}
}
/********************************************************************************************/
//
// 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)) {
modbusBridgeModbus = new TasmotaModbus(Pin(GPIO_MBR_RX), Pin(GPIO_MBR_TX), Pin(GPIO_MBR_TX_ENA));
if (ModbusBridgeBegin()) {
modbusBridge.enabled = true;
#ifdef USE_MODBUS_BRIDGE_TCP
// If TCP bridge is enabled allocate a TCP receive buffer
if (nullptr == modbusBridgeTCP.tcp_buf) modbusBridgeTCP.tcp_buf = (uint8_t *)malloc(MODBUS_BRIDGE_TCP_BUF_SIZE);
if (nullptr == modbusBridgeTCP.tcp_buf) {
ModbusBridgeAllocError(PSTR("TCP"));
return;
}
#ifdef MODBUS_BRIDGE_TCP_DEFAULT_PORT
else {
AddLog(LOG_LEVEL_INFO, PSTR("MBS: MBRTCP Starting server on port %d"), MODBUS_BRIDGE_TCP_DEFAULT_PORT);
modbusBridgeTCP.server_tcp = new WiFiServer(MODBUS_BRIDGE_TCP_DEFAULT_PORT);
modbusBridgeTCP.server_tcp->begin(); // start TCP server
modbusBridgeTCP.server_tcp->setNoDelay(true);
}
#endif // MODBUS_BRIDGE_TCP_DEFAULT_PORT
#endif // USE_MODBUS_BRIDGE_TCP
}
}
}
#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;
// 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++)
2022-07-19 09:37:03 +01:00
{
WiFiClient &client = modbusBridgeTCP.client_tcp[i];
if (!client)
2022-07-19 09:37:03 +01:00
{
client = new_client;
break;
2022-07-19 09:37:03 +01:00
}
}
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;
2022-07-19 09:37:03 +01:00
}
modbusBridgeTCP.tcp_transaction_id = MODBUS_TCP_INVALID_TRANSACTION_ID;
2022-07-19 09:37:03 +01:00
}
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]));
uint16_t *writeData = nullptr;
uint16_t count = 0;
modbusBridgeTCP.tcp_transaction_id = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[0]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[1]));
if (mbfunctioncode <= 2) // Multiple Coils, Inputs
{
count = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
modbusBridge.byteCount = ((count - 1) >> 3) + 1;
modbusBridge.dataCount = count;
modbusBridge.type = ModbusBridgeType::mb_bit;
}
else if (mbfunctioncode <= 4) // Multiple Holding or input registers
{
count = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
modbusBridge.byteCount = count * 2;
modbusBridge.dataCount = count;
modbusBridge.type = ModbusBridgeType::mb_uint16;
}
else // Write coil(s) or register(s)
{
// For functioncode 15 & 16 ignore bytecount, modbusBridgeModbus does calculate this
uint8_t dataStartByte = mbfunctioncode <= 6 ? 10 : 13;
uint16_t byteCount = (buf_len - dataStartByte);
modbusBridge.byteCount = 2;
modbusBridge.dataCount = 1;
modbusBridge.type = ModbusBridgeType::mb_uint16;
writeData = (uint16_t *)malloc(byteCount+1);
2022-11-06 09:12:08 +00:00
if (nullptr == writeData)
{
ModbusBridgeAllocError(PSTR("write"));
return;
}
if ((mbfunctioncode == 15) || (mbfunctioncode == 16)) count = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
else count = 1;
for (uint16_t dataPointer = 0; dataPointer < byteCount; dataPointer++)
{
if (dataPointer % 2 == 0)
{
writeData[dataPointer / 2] = (uint16_t)(((uint16_t)modbusBridgeTCP.tcp_buf[dataStartByte + dataPointer]) << 8);
}
else
{
writeData[dataPointer / 2] |= ((uint16_t)modbusBridgeTCP.tcp_buf[dataStartByte + dataPointer]);
}
}
}
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("MBS: MBRTCP to Modbus TransactionId:%d, deviceAddress:%d, functionCode:%d, startAddress:%d, count:%d, recvCount:%d, recvBytes:%d"),
modbusBridgeTCP.tcp_transaction_id, mbdeviceaddress, mbfunctioncode, mbstartaddress, count, modbusBridge.dataCount, modbusBridge.byteCount);
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
if (modbusBridgeModbus->Send(mbdeviceaddress, mbfunctioncode, mbstartaddress, count, writeData) == 0)
{
modbusBridge.setWaitingForAnswerFromSerial(true);
}
if (modbusBridgeTCP.output_mqtt)
{
modbusBridge.deviceAddress = mbdeviceaddress;
modbusBridge.functionCode = (ModbusBridgeFunctionCode)mbfunctioncode;
modbusBridge.startAddress = mbstartaddress;
modbusBridge.count = count;
}
free(writeData);
}
}
yield(); // avoid WDT if heavy traffic
} while (busy);
2022-07-19 09:37:03 +01:00
}
#endif
2022-07-19 09:37:03 +01:00
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
void CmndModbusBridgeSend(void)
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
{
if (!modbusBridge.isWaitingForAnswerFromSerial())
{
if (modbusBridge.command_data == nullptr)
{
CmndModbusBridgeSend(XdrvMailbox.data);
return;
}
else
{
// There is already a command pending
ResponseCmndFailed();
return;
}
}
else
{
if (modbusBridge.command_data == nullptr)
{
// Let's store the command data
modbusBridge.command_data = (char *)malloc(XdrvMailbox.data_len);
if (modbusBridge.command_data == nullptr)
{
// We couldn't store the command data.
ModbusBridgeAllocError(PSTR("COMMAND"));
ResponseCmndFailed();
return;
}
memcpy(modbusBridge.command_data, XdrvMailbox.data, XdrvMailbox.data_len);
}
else
{
// We're processing a command and yet another is waiting.
ResponseCmndFailed();
return;
}
}
}
void CmndModbusBridgeSend(char *json_in)
2022-07-19 09:37:03 +01:00
{
uint16_t *writeData = nullptr;
uint8_t writeDataSize = 0;
bool bitMode = false;
ModbusBridgeError errorcode = ModbusBridgeError::noerror;
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
JsonParser parser(json_in);
2022-07-19 09:37:03 +01:00
JsonParserObject root = parser.getRootObject();
if (!root)
return;
modbusBridge.deviceAddress = root.getUInt(PSTR(D_JSON_MODBUS_DEVICE_ADDRESS), 0);
uint8_t functionCode = root.getUInt(PSTR(D_JSON_MODBUS_FUNCTION_CODE), 0);
modbusBridge.startAddress = root.getULong(PSTR(D_JSON_MODBUS_START_ADDRESS), 0);
2022-07-19 09:37:03 +01:00
const char *stype = root.getStr(PSTR(D_JSON_MODBUS_TYPE), "uint8");
modbusBridge.count = root.getUInt(PSTR(D_JSON_MODBUS_COUNT), 1); // Number of values to read / write
const char *sendian = root.getStr(PSTR(D_JSON_MODBUS_ENDIAN), "undefined");
modbusBridge.endian = ModbusBridgeEndian::mb_undefined;
// If functioncode is 1, 2 or 15, the count is not the number of registers but the number
// of bits to read or write, so calculate the number data bytes to read/write.
if ((functionCode == 1) || (functionCode == 2) || (functionCode == 15))
{
bitMode = true;
modbusBridge.endian = ModbusBridgeEndian::mb_lsb;
}
// Change endianess when specified
if (strcmp (sendian,"msb") == 0) modbusBridge.endian = ModbusBridgeEndian::mb_msb;
if (strcmp (sendian,"lsb") == 0) modbusBridge.endian = ModbusBridgeEndian::mb_lsb;
2022-07-19 09:37:03 +01:00
if (modbusBridge.deviceAddress == 0)
errorcode = ModbusBridgeError::wrongdeviceaddress;
else if ((functionCode > (uint8_t)ModbusBridgeFunctionCode::mb_writeSingleRegister) &&
(functionCode != (uint8_t)ModbusBridgeFunctionCode::mb_writeMultipleCoils) &&
(functionCode != (uint8_t)ModbusBridgeFunctionCode::mb_writeMultipleRegisters))
errorcode = ModbusBridgeError::wrongfunctioncode; // Invalid function code
2022-07-19 09:37:03 +01:00
else
{
modbusBridge.functionCode = static_cast<ModbusBridgeFunctionCode>(functionCode);
if (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_undefined)
errorcode = ModbusBridgeError::wrongfunctioncode;
}
modbusBridge.type = ModbusBridgeType::mb_undefined;
if (bitMode) modbusBridge.byteCount = (modbusBridge.count + 7) / 8;
if (strcmp(stype, "int8") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_int8;
modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
}
else if (strcmp(stype, "int16") == 0)
2022-07-19 09:37:03 +01:00
{
modbusBridge.type = ModbusBridgeType::mb_int16;
2022-08-17 18:55:41 +01:00
modbusBridge.dataCount = modbusBridge.count;
2022-07-19 09:37:03 +01:00
}
else if (strcmp(stype, "int32") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_int32;
modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count;
2022-07-19 09:37:03 +01:00
}
else if ((strcmp(stype, "uint8") == 0))
{
modbusBridge.type = ModbusBridgeType::mb_uint8;
modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
}
else if ((strcmp(stype, "uint16") == 0) || (strcmp(stype, "") == 0)) // Default is uint16
2022-07-19 09:37:03 +01:00
{
modbusBridge.type = ModbusBridgeType::mb_uint16;
2022-08-17 18:55:41 +01:00
modbusBridge.dataCount = modbusBridge.count;
2022-07-19 09:37:03 +01:00
}
else if (strcmp(stype, "uint32") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_uint32;
modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count;
2022-07-19 09:37:03 +01:00
}
else if (strcmp(stype, "float") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_float;
modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count;
2022-07-19 09:37:03 +01:00
}
else if (strcmp(stype, "raw") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_raw;
modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
2022-07-19 09:37:03 +01:00
}
else if (strcmp(stype, "hex") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_hex;
modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
}
else if (strcmp(stype, "bit") == 0)
2022-07-19 09:37:03 +01:00
{
modbusBridge.type = ModbusBridgeType::mb_bit;
modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 16) + 1 ;
2022-07-19 09:37:03 +01:00
}
else
errorcode = ModbusBridgeError::wrongtype;
// Prevent buffer overflow due to usage of to many registers
if ((!bitMode) && (modbusBridge.dataCount > MBR_MAX_REGISTERS))
errorcode = ModbusBridgeError::wrongcount;
if ((bitMode) && (modbusBridge.dataCount > MBR_MAX_REGISTERS * 8))
errorcode = ModbusBridgeError::wrongcount;
2022-07-19 09:37:03 +01:00
// Get Json data for writing
2022-08-17 18:55:41 +01:00
JsonParserArray jsonDataArray = root[PSTR(D_JSON_MODBUS_VALUES)].getArray();
writeDataSize = jsonDataArray.size();
// Check if number of supplied data items is valid
switch (modbusBridge.functionCode)
{
case ModbusBridgeFunctionCode::mb_writeMultipleCoils:
// In writeMultipleCoil mode the amount of given data bits is less or equal to the count
switch (modbusBridge.type)
{
case ModbusBridgeType::mb_bit:
if (modbusBridge.count > writeDataSize) errorcode = ModbusBridgeError::wrongcount;
break;
case ModbusBridgeType::mb_uint8:
case ModbusBridgeType::mb_int8:
case ModbusBridgeType::mb_raw:
case ModbusBridgeType::mb_hex:
if (modbusBridge.count > writeDataSize * 8) errorcode = ModbusBridgeError::wrongcount;
break;
case ModbusBridgeType::mb_uint16:
case ModbusBridgeType::mb_int16:
if (modbusBridge.count > writeDataSize * 16) errorcode = ModbusBridgeError::wrongcount;
break;
case ModbusBridgeType::mb_uint32:
case ModbusBridgeType::mb_int32:
if (modbusBridge.count > writeDataSize * 32) errorcode = ModbusBridgeError::wrongcount;
break;
}
break;
case ModbusBridgeFunctionCode::mb_writeSingleRegister:
case ModbusBridgeFunctionCode::mb_writeSingleCoil:
if (modbusBridge.count != 1) errorcode = ModbusBridgeError::wrongcount;
break;
case ModbusBridgeFunctionCode::mb_writeMultipleRegisters:
if (modbusBridge.count != writeDataSize) errorcode = ModbusBridgeError::wrongcount;
break;
}
// If write data is specified in JSON copy it into writeData array
if ((errorcode == ModbusBridgeError::noerror) && (jsonDataArray.isArray()))
{
2022-08-17 18:55:41 +01:00
if (modbusBridge.dataCount > 40)
{
2022-08-17 18:55:41 +01:00
errorcode = ModbusBridgeError::tomanydata;
}
else
{
writeData = (uint16_t *)malloc(modbusBridge.dataCount * 2);
if (nullptr == writeData)
2022-11-06 09:12:08 +00:00
{
ModbusBridgeAllocError(PSTR("write"));
return;
}
2022-08-17 18:55:41 +01:00
for (uint8_t jsonDataArrayPointer = 0; jsonDataArrayPointer < writeDataSize; jsonDataArrayPointer++)
2022-08-17 18:55:41 +01:00
{
if (errorcode != ModbusBridgeError::noerror) break;
switch (modbusBridge.type)
2022-08-17 18:55:41 +01:00
{
case ModbusBridgeType::mb_bit:
{
// Initialize current data/register to 0
if (jsonDataArrayPointer % 16 == 0)
2022-08-17 18:55:41 +01:00
{
writeData[jsonDataArrayPointer / 15] = 0;
2022-08-17 18:55:41 +01:00
}
// Swap low and high bytes according to modbus specification
uint16_t bitValue = (jsonDataArray[jsonDataArrayPointer].getUInt(0) == 1) ? 1 : 0;
uint8_t bitPointer = (jsonDataArrayPointer % 16) + 8;
if (bitPointer > 15) bitPointer -= 16;
writeData[jsonDataArrayPointer / 16] += bitValue << bitPointer;
}
break;
case ModbusBridgeType::mb_int8:
if (jsonDataArrayPointer % 2)
writeData[jsonDataArrayPointer / 2] += (int8_t)jsonDataArray[jsonDataArrayPointer].getInt(0);
else
writeData[jsonDataArrayPointer / 2] = (int8_t)jsonDataArray[jsonDataArrayPointer / 2].getInt(0) << 8;
if (modbusBridge.dataCount != writeDataSize / 2) errorcode = ModbusBridgeError::wrongcount;
break;
case ModbusBridgeType::mb_hex:
case ModbusBridgeType::mb_raw:
case ModbusBridgeType::mb_uint8:
if (jsonDataArrayPointer % 2)
writeData[jsonDataArrayPointer / 2] += (uint8_t)jsonDataArray[jsonDataArrayPointer].getUInt(0);
else
writeData[jsonDataArrayPointer / 2] = (uint8_t)jsonDataArray[jsonDataArrayPointer].getUInt(0) << 8;
if (modbusBridge.dataCount != writeDataSize / 2) errorcode = ModbusBridgeError::wrongcount;
break;
case ModbusBridgeType::mb_int16:
writeData[jsonDataArrayPointer] = modbusBridge.endian != ModbusBridgeEndian::mb_lsb ? jsonDataArray[jsonDataArrayPointer].getInt(0)
: ModbusBridgeSwapEndian16(jsonDataArray[jsonDataArrayPointer].getInt(0));
break;
case ModbusBridgeType::mb_uint16:
writeData[jsonDataArrayPointer] = modbusBridge.endian != ModbusBridgeEndian::mb_lsb ? jsonDataArray[jsonDataArrayPointer].getUInt(0)
: ModbusBridgeSwapEndian16(jsonDataArray[jsonDataArrayPointer].getUInt(0));
break;
case ModbusBridgeType::mb_int32:
writeData[(jsonDataArrayPointer * 2)] = modbusBridge.endian != ModbusBridgeEndian::mb_lsb ? ModbusBridgeSwapEndian16(jsonDataArray[jsonDataArrayPointer].getInt(0))
: (int16_t)(jsonDataArray[jsonDataArrayPointer].getInt(0) >> 16);
writeData[(jsonDataArrayPointer * 2) + 1] = modbusBridge.endian != ModbusBridgeEndian::mb_lsb ? ModbusBridgeSwapEndian16(jsonDataArray[jsonDataArrayPointer].getInt(0) >> 16)
: (uint16_t)(jsonDataArray[jsonDataArrayPointer].getInt(0));
break;
case ModbusBridgeType::mb_uint32:
writeData[(jsonDataArrayPointer * 2)] = modbusBridge.endian != ModbusBridgeEndian::mb_lsb ? ModbusBridgeSwapEndian16(jsonDataArray[jsonDataArrayPointer].getUInt(0))
: (uint16_t)(jsonDataArray[jsonDataArrayPointer].getUInt(0) >> 16);
writeData[(jsonDataArrayPointer * 2) + 1] = modbusBridge.endian != ModbusBridgeEndian::mb_lsb ? ModbusBridgeSwapEndian16(jsonDataArray[jsonDataArrayPointer].getUInt(0) >> 16)
: (uint16_t)(jsonDataArray[jsonDataArrayPointer].getUInt(0));
break;
case ModbusBridgeType::mb_float:
// TODO
default:
errorcode = ModbusBridgeError::wrongtype;
break;
2022-08-17 18:55:41 +01:00
}
}
}
// Adapt data according to modbus protocol
if (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleCoil)
{
writeData[0] = writeData[0] ? 0xFF00 : 0x0000; // High Byte
}
}
// Handle errorcode and exit function when an error has occured
2022-07-19 09:37:03 +01:00
if (errorcode != ModbusBridgeError::noerror)
{
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Send Error %u"), (uint8_t)errorcode);
free(writeData);
2022-07-19 09:37:03 +01:00
return;
}
// If writing a single coil or single register, the register count is always 1. We also prevent writing data out of range
2022-08-17 18:55:41 +01:00
if ((modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleCoil) || (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleRegister))
2022-08-17 18:55:41 +01:00
modbusBridge.dataCount = 1;
uint8_t error = modbusBridgeModbus->Send(modbusBridge.deviceAddress, (uint8_t)modbusBridge.functionCode, modbusBridge.startAddress, modbusBridge.dataCount, writeData);
free(writeData);
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
if (error == 0)
{
modbusBridge.setWaitingForAnswerFromSerial(true);
}
else
{
AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver send error %u"), error);
return;
}
2022-07-19 09:37:03 +01:00
ResponseCmndDone();
}
void CmndModbusBridgeSetBaudrate(void)
{
ModbusBridgeSetBaudrate(XdrvMailbox.payload);
ResponseCmndNumber(Settings->modbus_sbaudrate * 300);
2022-07-19 09:37:03 +01:00
}
void CmndModbusBridgeSetConfig(void)
{
// See TasmotaModusConfig for possible options
// ModbusConfig 0..23 where 3 equals 8N1
// ModbusConfig 8N1
if (XdrvMailbox.data_len > 0)
{
if (XdrvMailbox.data_len < 3)
{ // Use 0..23 as serial config option
if ((XdrvMailbox.payload >= TS_SERIAL_5N1) && (XdrvMailbox.payload <= TS_SERIAL_8O2))
{
ModbusBridgeSetConfig(XdrvMailbox.payload);
2022-07-19 09:37:03 +01:00
}
}
else if ((XdrvMailbox.payload >= 5) && (XdrvMailbox.payload <= 8))
{
int8_t serial_config = ParseSerialConfig(XdrvMailbox.data);
if (serial_config >= 0)
{
ModbusBridgeSetConfig(serial_config);
2022-07-19 09:37:03 +01:00
}
}
}
ResponseCmndChar(GetSerialConfig(Settings->modbus_sconfig).c_str());
2022-07-19 09:37:03 +01:00
}
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
void CmndModbusBridgeSetTimeout(void)
{
if (XdrvMailbox.data_len > 0)
{
const int timeout_ms = XdrvMailbox.payload;
if (timeout_ms > 0) // Accepts values in the range of: 1..INT_MAX
{
modbusBridge.setModbusSerialTimeout_ms(timeout_ms);
}
}
ResponseCmndNumber(modbusBridge.getModbusSerialTimeout_ms());
}
#ifdef USE_MODBUS_BRIDGE_TCP
//
// Command `TCPStart`
// Params: port,<IPv4 allow>
//
void CmndModbusTCPStart(void)
{
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,<IPv4>
//
void CmndModbusTCPConnect(void)
{
int32_t tcp_port = XdrvMailbox.payload;
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();
}
void CmndModbusTCPMqtt(void)
{
modbusBridgeTCP.output_mqtt = XdrvMailbox.payload;
ResponseCmndDone();
}
#endif
2022-07-19 09:37:03 +01:00
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
2022-11-11 09:44:56 +00:00
bool Xdrv63(uint32_t function)
2022-07-19 09:37:03 +01:00
{
bool result = false;
if (FUNC_PRE_INIT == function) {
2022-07-19 09:37:03 +01:00
ModbusBridgeInit();
} else if (modbusBridge.enabled) {
switch (function) {
case FUNC_LOOP:
ModbusBridgeHandle();
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
// Check whether we can send a stored command
if (modbusBridge.command_data && !modbusBridge.isWaitingForAnswerFromSerial())
{
CmndModbusBridgeSend(modbusBridge.command_data);
free(modbusBridge.command_data), modbusBridge.command_data = nullptr;
}
#ifdef USE_MODBUS_BRIDGE_TCP
Fix ModbusBridge request/response logic (#22075) - Fix cases where the subsequent Modbus packet   can be send to the serial port (triggered either by   'ModbusSend' command or request from TCP bridge)   before an answer was received to the previous packet.   This can happen in a setup where simultaneously:   - two (or more) modbus TCP clients are sending requests through the modbus-proxy [1] to Tasmota,   - ModbusSend commands are executed (e.g. using Berry).   Log excerpt (from build with TASMOTAMODBUSDEBUG enabled):   14:51:18.940 MBS: Serial Send: 04 03 01 00 00 09 84 65   14:51:19.054 MBS: Serial Send: 04 03 10 0A 00 05 A1 5E   14:51:19.136 MBS: Serial Received: 04 03 0A 00 00 00 D0 00 00 01 AB 00 00 89 62   Fix adds 'waitingForAnswerFromSerial' flag which is set after   we send data to the serial port and prevents sending another   requests before we receive an answer or timeout happened. Fix stores temporarily a 'ModbusSend' command data and tries to execute it after Modbus response has been received or timeout has happened. - Add 'ModbusSerialTimeout' command which sets timeout in [ms] for how long we will be waiting for an answer from the client device.   Default value is 1000 [ms] and it is not restored after reboot. - Sends error 11 (0xB) (as TCP response) when no answer was received   from the serial port within the timeout set by 'ModbusSerialTimeout' command. - Add Modbus 'TransactionId' to the logging. [1] https://github.com/tiagocoutinho/modbus-proxy Signed-off-by: Damian Wrobel <dwrobel@ertelnet.rybnik.pl>
2024-09-09 09:26:20 +01:00
if (!modbusBridge.isWaitingForAnswerFromSerial())
{
ModbusTCPHandle();
}
#endif
break;
case FUNC_COMMAND:
result = DecodeCommand(kModbusBridgeCommands, ModbusBridgeCommand);
break;
case FUNC_ACTIVE:
result = true;
break;
2022-07-19 09:37:03 +01:00
}
}
return result;
}
#endif // USE_MODBUS_BRIDGE