/* xdrv_63_modbus_bridge.ino - modbus bridge support for Tasmota Copyright (C) 2021 Theo Arends and Jeroenst 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 . */ #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 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/ \*********************************************************************************************/ #define XDRV_63 63 #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" #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 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" #define D_CMND_MODBUS_TCP_MQTT "TCPMqtt" const char kModbusBridgeCommands[] PROGMEM = "Modbus|" // Prefix 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; void (*const ModbusBridgeCommand[])(void) PROGMEM = { &CmndModbusTCPStart, &CmndModbusTCPConnect, &CmndModbusTCPMqtt, &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; bool output_mqtt = false; }; ModbusBridgeTCP modbusBridgeTCP; #endif #include TasmotaModbus *modbusBridgeModbus = nullptr; enum class ModbusBridgeError { noerror = 0, nodataexpected = 1, wrongdeviceaddress = 2, wrongfunctioncode = 3, wrongstartaddress = 4, wrongtype = 5, wrongdataCount = 6, wrongcount = 7, tomanydata = 8 }; enum class ModbusBridgeFunctionCode { mb_undefined = 0, mb_readCoilStatus = 1, mb_readInputStatus = 2, mb_readHoldingRegisters = 3, mb_readInputRegisters = 4, mb_writeSingleCoil = 5, mb_writeSingleRegister = 6, mb_writeMultipleCoils = 15, mb_writeMultipleRegisters = 16 }; enum class ModbusBridgeType { mb_undefined, mb_uint8, mb_uint16, mb_uint32, mb_int8, mb_int16, mb_int32, mb_float, mb_raw, mb_hex, mb_bit }; enum class ModbusBridgeEndian { mb_undefined, mb_msb, mb_lsb }; 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; 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 }; ModbusBridge modbusBridge; /********************************************************************************************/ // // Helper functions // uint16_t ModbusBridgeSwapEndian16(uint16_t num) { return (num>>8) | (num<<8); } void ModbusBridgeAllocError(const char* s) { AddLog(LOG_LEVEL_ERROR, PSTR("MBS: could not allocate %s buffer"), s); } /********************************************************************************************/ // // Applies serial configuration to modbus serial port // bool ModbusBridgeBegin(void) { if ((Settings->modbus_sbaudrate < 1) || (Settings->modbus_sbaudrate > (115200 / 300))) Settings->modbus_sbaudrate = (uint8_t)((uint32_t)MBR_BAUDRATE / 300); if (Settings->modbus_sconfig > TS_SERIAL_8O2) 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(); } AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR %s ser init at %d baud"), (2 == result ? "HW" : "SW"), Settings->modbus_sbaudrate * 300); } modbusBridge.buffer = (uint8_t *)malloc(MBR_RECEIVE_BUFFER_SIZE); return result; } void ModbusBridgeSetConfig(uint32_t serial_config) { if (serial_config > TS_SERIAL_8O2) { serial_config = TS_SERIAL_8N1; } if (serial_config != Settings->modbus_sconfig) { Settings->modbus_sconfig = serial_config; ModbusBridgeBegin(); } } void ModbusBridgeSetBaudrate(uint32_t baudrate) { if ((baudrate >= 300) && (baudrate <= 115200)) { if (baudrate / 300 != Settings->modbus_sbaudrate) { Settings->modbus_sbaudrate = baudrate / 300; ModbusBridgeBegin(); } } } /********************************************************************************************/ // // Handles data received from tasmota modbus wrapper and send this to (TCP and/or) MQTT client // void ModbusBridgeHandle(void) { bool data_ready = modbusBridgeModbus->ReceiveReady(); 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 { ModbusBridgeAllocError(PSTR("read")); modbusBridge.dataCount = 0; modbusBridge.byteCount = 0; return; } memset(modbusBridge.buffer, 0, MBR_RECEIVE_BUFFER_SIZE); uint32_t error = modbusBridgeModbus->ReceiveBuffer(modbusBridge.buffer, 0, MBR_RECEIVE_BUFFER_SIZE - 9); #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 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(); AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBRTCP from Modbus deviceAddress %d, writing %d bytes to client"), modbusBridge.buffer[0], nrOfBytes); } } #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 errorcode = ModbusBridgeError::nodataexpected; } else if (modbusBridge.deviceAddress != (uint8_t)modbusBridge.buffer[0]) errorcode = ModbusBridgeError::wrongdeviceaddress; else if ((uint8_t)modbusBridge.functionCode != (uint8_t)modbusBridge.buffer[1]) 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) { 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++) { ResponseAppend_P(PSTR("%d"), modbusBridge.buffer[i]); if (i < modbusBridgeModbus->ReceiveCount() - 1) ResponseAppend_P(PSTR(",")); } ResponseAppend_P(PSTR("]}")); ResponseJsonEnd(); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED)); } 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(); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED)); } else if ((modbusBridge.buffer[1] > 0) && (modbusBridge.buffer[1] < 7)) { // Read and process Registers uint8_t dataOffset = 3; 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()); 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++) { char svalue[MBR_MAX_VALUE_LENGTH + 1] = ""; if (modbusBridge.type == ModbusBridgeType::mb_float) { // Convert next 4 bytes to float 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)]; } 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)); } 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)]; } if (modbusBridge.type == ModbusBridgeType::mb_int32) snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", (int32_t)value); 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)]; } if (modbusBridge.type == ModbusBridgeType::mb_int16) snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", (int16_t)value); 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); } } ResponseAppend_P(PSTR("%s"), svalue); if (count < data_count - 1) ResponseAppend_P(PSTR(",")); } ResponseAppend_P(PSTR("]}")); ResponseJsonEnd(); if (errorcode == ModbusBridgeError::noerror) MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED)); } else if ((modbusBridge.buffer[1] == 15) || (modbusBridge.buffer[1] == 16)) // Write Multiple Registers { 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]); ResponseAppend_P(PSTR("}")); ResponseJsonEnd(); if (errorcode == ModbusBridgeError::noerror) MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED)); } else errorcode = ModbusBridgeError::wrongfunctioncode; } 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)); ModbusBridgeBegin(); #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 (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 #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 (!modbusBridgeModbus) 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])); uint16_t *writeData = NULL; 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 / 2)+1); 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); modbusBridgeModbus->Send(mbdeviceaddress, mbfunctioncode, mbstartaddress, count, writeData); 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); } #endif /*********************************************************************************************\ * Commands \*********************************************************************************************/ void CmndModbusBridgeSend(void) { uint16_t *writeData = NULL; uint8_t writeDataSize = 0; bool bitMode = false; ModbusBridgeError errorcode = ModbusBridgeError::noerror; JsonParser parser(XdrvMailbox.data); 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); 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; 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 else { modbusBridge.functionCode = static_cast(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) { modbusBridge.type = ModbusBridgeType::mb_int16; modbusBridge.dataCount = modbusBridge.count; } else if (strcmp(stype, "int32") == 0) { modbusBridge.type = ModbusBridgeType::mb_int32; modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count; } 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 { modbusBridge.type = ModbusBridgeType::mb_uint16; modbusBridge.dataCount = modbusBridge.count; } else if (strcmp(stype, "uint32") == 0) { modbusBridge.type = ModbusBridgeType::mb_uint32; modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count; } else if (strcmp(stype, "float") == 0) { modbusBridge.type = ModbusBridgeType::mb_float; modbusBridge.dataCount = bitMode ? 2 * modbusBridge.count : modbusBridge.count; } else if (strcmp(stype, "raw") == 0) { modbusBridge.type = ModbusBridgeType::mb_raw; modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1; } 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) { modbusBridge.type = ModbusBridgeType::mb_bit; modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 16) + 1 ; } 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; // Get Json data for writing 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())) { if (modbusBridge.dataCount > 40) { errorcode = ModbusBridgeError::tomanydata; } else { writeData = (uint16_t *)malloc(modbusBridge.dataCount); if (nullptr == writeData) { ModbusBridgeAllocError(PSTR("write")); return; } for (uint8_t jsonDataArrayPointer = 0; jsonDataArrayPointer < writeDataSize; jsonDataArrayPointer++) { if (errorcode != ModbusBridgeError::noerror) break; switch (modbusBridge.type) { case ModbusBridgeType::mb_bit: { // Initialize current data/register to 0 if (jsonDataArrayPointer % 16 == 0) { writeData[jsonDataArrayPointer / 15] = 0; } // 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; } } } // 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 if (errorcode != ModbusBridgeError::noerror) { AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Send Error %u"), (uint8_t)errorcode); free(writeData); return; } // If writing a single coil or single register, the register count is always 1. We also prevent writing data out of range if ((modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleCoil) || (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleRegister)) modbusBridge.dataCount = 1; uint8_t error = modbusBridgeModbus->Send(modbusBridge.deviceAddress, (uint8_t)modbusBridge.functionCode, modbusBridge.startAddress, modbusBridge.dataCount, writeData); free(writeData); if (error) { AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver send error %u"), error); return; } ResponseCmndDone(); } void CmndModbusBridgeSetBaudrate(void) { ModbusBridgeSetBaudrate(XdrvMailbox.payload); ResponseCmndNumber(Settings->modbus_sbaudrate * 300); } 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); } } else if ((XdrvMailbox.payload >= 5) && (XdrvMailbox.payload <= 8)) { int8_t serial_config = ParseSerialConfig(XdrvMailbox.data); if (serial_config >= 0) { ModbusBridgeSetConfig(serial_config); } } } ResponseCmndChar(GetSerialConfig(Settings->modbus_sconfig).c_str()); } #ifdef USE_MODBUS_BRIDGE_TCP // // Command `TCPStart` // Params: port, // void CmndModbusTCPStart(void) { if (!modbusBridgeModbus) { 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 (!modbusBridgeModbus) { 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(); } void CmndModbusTCPMqtt(void) { modbusBridgeTCP.output_mqtt = XdrvMailbox.payload; ResponseCmndDone(); } #endif /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv63(uint32_t function) { bool result = false; if (FUNC_PRE_INIT == function) { ModbusBridgeInit(); } else if (modbusBridgeModbus) { switch (function) { case FUNC_COMMAND: result = DecodeCommand(kModbusBridgeCommands, ModbusBridgeCommand); break; case FUNC_LOOP: ModbusBridgeHandle(); #ifdef USE_MODBUS_BRIDGE_TCP ModbusTCPHandle(); #endif break; } } return result; } #endif // USE_MODBUS_BRIDGE