From 38c20bc4eda9b478c26544422da8d1086b0dcf08 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:14:37 +0200 Subject: [PATCH 01/48] P183 initial state --- src/_P183_modbus.ino | 496 +++++++++++++++++++++ src/_P199_rainsensor.ino | 304 +++++++++++++ src/src/PluginStructs/P199_data_struct.cpp | 13 + src/src/PluginStructs/P199_data_struct.h | 34 ++ 4 files changed, 847 insertions(+) create mode 100644 src/_P183_modbus.ino create mode 100644 src/_P199_rainsensor.ino create mode 100644 src/src/PluginStructs/P199_data_struct.cpp create mode 100644 src/src/PluginStructs/P199_data_struct.h diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino new file mode 100644 index 0000000000..6d95aa1c56 --- /dev/null +++ b/src/_P183_modbus.ino @@ -0,0 +1,496 @@ +#include "_Plugin_Helper.h" + +#ifdef USES_P199 + +// ####################################################################################################### +// ############## Plugin 199: Modbus rain sensor ############### +// ####################################################################################################### + +/* + Plugin written by: Flashmark + + This plugin reads values from a Modbus RTU device. + */ + + +# define P199_DEBUG +# define PLUGIN_199 +# define PLUGIN_ID_199 199 +# define PLUGIN_NAME_199 "[testing] Modbus RTU" +# define P199_NR_OUTPUT_VALUES 4 +# define PLUGIN_VALUENAME1_199 "Value1" +# define PLUGIN_VALUENAME2_199 "Value2" +# define PLUGIN_VALUENAME3_199 "Value3" +# define PLUGIN_VALUENAME4_199 "Value4" + +// Plugin configuration parameters +// PCONFIG(0) is the Modbus device ID, +// PCONFIG(1) is the baud rate. +// PCONFIG(2) is used for flags, where bit 0 indicates collision detection +// PCONFIG(3) is Modbus register address for value 1 +// PCONFIG(4) is Modbus register address for value 2 +// PCONFIG(5) is Modbus register address for value 3 +// PCONFIG(6) is Modbus register address for value 4 +# define P199_DEV_ID PCONFIG(0) +# define P199_DEV_ID_LABEL PCONFIG_LABEL(0) +# define P199_BAUDRATE PCONFIG(1) +# define P199_BAUDRATE_LABEL PCONFIG_LABEL(1) +# define P199_ADDRESS(x) PCONFIG(3 + x) +# define P199_ADDRESS_LABEL(x) concat(F("addr"), x) + +# define P199_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) +# define P199_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) +# define P199_FLAG_COLL_DETECT_LABEL "colldet" + +# define P199_QUERY1_CONFIG_POS 3 + +# define P199_DEPIN CONFIG_PIN3 + +# define P199_DEV_ID_DFLT 1 +# define P199_BAUDRATE_DFLT 3 // 9600 baud + +# include + +// These pointers may be used among multiple instances of the same plugin, +// as long as the same serial settings are used. +ESPeasySerial *P199_ESPEasySerial = nullptr; +boolean P199_init = false; + +void P199_scan_modbus(); +void P199_scan_module(uint8_t node_id, uint8_t start_reg = 0x00, uint8_t end_reg = 0xFF); + +boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + auto& dev = Device[++deviceCount]; + dev.Number = PLUGIN_ID_199; + dev.Type = DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins + dev.VType = Sensor_VType::SENSOR_TYPE_SINGLE; // Only one value + dev.FormulaOption = true; + dev.ValueCount = P199_NR_OUTPUT_VALUES; + dev.OutputDataType = Output_Data_type_t::Simple; + dev.SendDataOption = true; + dev.TimerOption = true; + dev.PluginStats = true; + dev.TaskLogsOwnPeaks = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_199); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_199)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_199)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_199)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_199)); + } + + case PLUGIN_GET_DEVICEGPIONAMES: + { + serialHelper_modbus_getGpioNames(event); + break; + } + + case PLUGIN_WEBFORM_SHOW_CONFIG: + { + string += serialHelper_getSerialTypeLabel(event); + success = true; + break; + } + + case PLUGIN_SET_DEFAULTS: + { + P199_DEV_ID = P199_DEV_ID_DFLT; + P199_BAUDRATE = P199_BAUDRATE_DFLT; + + success = true; + break; + } + + case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS: + { + if ((P199_DEV_ID == 0) || (P199_DEV_ID > 247) || (P199_BAUDRATE >= 6)) { + // Load some defaults + P199_DEV_ID = P199_DEV_ID_DFLT; + P199_BAUDRATE = P199_BAUDRATE_DFLT; + } + { + String options_baudrate[6]; + + for (int i = 0; i < 6; ++i) { + options_baudrate[i] = P199_storageValueToBaudrate(i); + } + constexpr size_t optionCount = NR_ELEMENTS(options_baudrate); + const FormSelectorOptions selector(optionCount, options_baudrate); + selector.addFormSelector(F("Baud Rate"), P199_BAUDRATE_LABEL, P199_BAUDRATE); + addUnit(F("baud")); + } + + addFormNumericBox(F("Modbus Address"), P199_DEV_ID_LABEL, P199_DEV_ID, 1, 247); + + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), F(P199_FLAG_COLL_DETECT_LABEL), P199_GET_FLAG_COLL_DETECT); + addFormNote(F("/RE connected to GND, only supported on hardware serial")); + # endif // ifdef ESP32 + + break; + } + + case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: + { + for (int outputIndex = 0; outputIndex < P199_NR_OUTPUT_VALUES; ++outputIndex) + { + addFormNumericBox(concat(F("Value "), outputIndex + 1), P199_ADDRESS_LABEL(outputIndex), P199_ADDRESS(outputIndex)); + } + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + + P199_DEV_ID = getFormItemInt(P199_DEV_ID_LABEL); + P199_BAUDRATE = getFormItemInt(P199_BAUDRATE_LABEL); + # ifdef ESP32 + P199_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P199_FLAG_COLL_DETECT_LABEL))); + # endif // ifdef ESP32 + + for (int outputIndex = 0; outputIndex < P199_NR_OUTPUT_VALUES; ++outputIndex) + { + P199_ADDRESS(outputIndex) = getFormItemInt( P199_ADDRESS_LABEL(outputIndex)); + } + + P199_init = false; // Force device setup next time + success = true; + break; + } + + case PLUGIN_INIT: + { + P199_init = true; + + // (re)create the serial port object + // If the serial port object already exists, delete it first. + if (P199_ESPEasySerial != nullptr) { + delete P199_ESPEasySerial; + P199_ESPEasySerial = nullptr; + } + P199_ESPEasySerial = new (std::nothrow) ESPeasySerial(static_cast(CONFIG_PORT), CONFIG_PIN1, CONFIG_PIN2); + + if (P199_ESPEasySerial == nullptr) { + break; + } + + // Set RS485 mode if requested using selected pin for RTS + bool rs485Mode = P199_ESPEasySerial->setRS485Mode(P199_DEPIN, P199_GET_FLAG_COLL_DETECT); + + unsigned int baudrate = P199_storageValueToBaudrate(P199_BAUDRATE); + P199_ESPEasySerial->begin(baudrate); + + #ifdef P199_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P199: Init serial: RX pin "); + log += CONFIG_PIN1; + log += F(", TX pin "); + log += CONFIG_PIN2; + log += F(", RS485 mode selected on pin "); + log += P199_DEPIN; + log += F(", baudrate "); + log += P199_storageValueToBaudrate(P199_BAUDRATE); + log += F(", collision detection "); + log += P199_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); + log += F(", RS485mode enabled: "); + log += rs485Mode ? F("yes") : F("no"); + addLogMove(LOG_LEVEL_INFO, log); + } + #endif // ifdef P199_DEBUG + + success = true; + break; + } + + case PLUGIN_EXIT: + { + P199_init = false; + + delete P199_ESPEasySerial; + P199_ESPEasySerial = nullptr; + + break; + } + + case PLUGIN_TEN_PER_SECOND: + { + + break; + } + + case PLUGIN_ONCE_A_SECOND: + { + + break; + } + + case PLUGIN_READ: + { + uint16_t value = 0; + for (int outputIndex = 0; outputIndex < P199_NR_OUTPUT_VALUES; ++outputIndex) + { + P199_modbus_readRegister(P199_DEV_ID, P199_ADDRESS(outputIndex), &value); + UserVar.setFloat(event->TaskIndex, outputIndex, value); + } + break; + } + case PLUGIN_WRITE: + { + if (P199_ESPEasySerial != nullptr) { + const String cmd = parseString(string, 1); + + if (equals(cmd, F("modbus"))) { + const String subcmd = parseString(string, 2); + + if (equals(subcmd, F("write"))) { + // Write a value to a Modbus register + int address = parseString(string, 3).toInt(); + uint16_t value = parseString(string, 4).toInt(); + P199_modbus_writeRegister(P199_DEV_ID, address, value); + String log = F("Modbus: write value "); + log += value; + log += F(" to address "); + log += address; + addLogMove(LOG_LEVEL_INFO, log); + success = true; + } + else if (equals(subcmd, F("scan"))) { + // Scan for Modbus devices + addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); + P199_scan_modbus(); + success = true; + } + else if (equals(subcmd, F("read"))) { + // Read a value from a Modbus register + int address = parseString(string, 3).toInt(); + uint16_t value = 0; + P199_modbus_readRegister(P199_DEV_ID, address, &value); + String log = F("Modbus: read value "); + log += value; + log += F(" from address "); + log += address; + addLogMove(LOG_LEVEL_INFO, log); + success = true; + } + else if (equals(subcmd, F("dump"))) { + int start_address = parseString(string, 3).toInt(); + int end_address = parseString(string, 4).toInt(); + if (end_address < start_address) { + end_address = start_address; + } + if (end_address - start_address > 100) { + end_address = start_address + 100; // Limit to 100 addresses + } + addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); + P199_scan_module(P199_DEV_ID, start_address, end_address); + success = true; + } + else { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); + } + } + } + break; + } + + } + return success; +} + +int P199_storageValueToBaudrate(uint8_t baudrate_setting) { + int baudrate = 9600; + + if (baudrate_setting < 6) { + baudrate = 1200 << baudrate_setting; + } + return baudrate; +} + +int P199_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) +{ + uint8_t buffer[8]; // Buffer for Modbus request + uint8_t response[8]; // Buffer for Modbus response + + buffer[0] = node_id; + buffer[1] = 0x03; // Function code for reading holding registers + buffer[2] = highByte(reg); // High byte of register address + buffer[3] = lowByte(reg); // Low byte of register address + buffer[4] = 0x00; // Number of registers to read (2 bytes) + buffer[5] = 0x01; // Number of registers to read (2 bytes) + uint16_t crc = P199_calculateCRC((uint8_t*)buffer, 6); + buffer[6] = lowByte(crc); // CRC low byte + buffer[7] = highByte(crc); // CRC high byte + + if (P199_modbus_exchange_message(buffer, response, 8, 7) < 0) { + return -1; // Failed to exchange message + } + + if (response[0] == node_id && response[1] == 0x03 && response[2] == 0x02) { + *value = (response[3] << 8) | response[4]; // Combine high and low byte + addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: received value: ", *value)); + return 0; // Success + } else { + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); + return -2; // Invalid response + } +} + +int P199_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) +{ + uint8_t buffer[8]; + uint8_t response[8]; + + buffer[0] = node_id; + buffer[1] = 0x06; // Function code for reading holding registers + buffer[2] = highByte(reg); // High byte of register address + buffer[3] = lowByte(reg); // Low byte of register address + buffer[4] = highByte(value); // High byte of value to write + buffer[5] = lowByte(value); // Low byte of value to write + uint16_t crc = P199_calculateCRC((uint8_t*)buffer, 6); + buffer[6] = lowByte(crc); // CRC low byte + buffer[7] = highByte(crc); // CRC high byte + + if (P199_modbus_exchange_message(buffer, response, 8, 7) < 0) { + return -1; // Failed to exchange message + } + if (response[0] == node_id && response[1] == 0x06 && response[2] == highByte(reg) && response[3] == lowByte(reg)) { + uint16_t crc = P199_calculateCRC((uint8_t*)response, 6); + if (response[5] != lowByte(crc) || response[6] != highByte(crc)) { + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid CRC in response")); + return -2; // Invalid response + } + addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: Success send value ", value)); + return 0; // Success + } else { + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); + return -2; // Invalid response + } +} + +// Exchange Modbus RTU messages. Send the tx_buffer and wait for a response in rx_buffer. +int P199_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t tx_size, uint8_t rx_size) +{ + + if (P199_ESPEasySerial == nullptr) { + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Serial not initialized")); + return -1; // Not initialized + } + + for (int i = P199_ESPEasySerial->available(); i > 0; --i) { + P199_ESPEasySerial->read(); // Clear any existing data in the buffer + } + + P199_dump_buffer((uint8_t*)tx_buffer, tx_size); // Debug: Dump the transmit buffer content + P199_ESPEasySerial->write((uint8_t*)tx_buffer, tx_size); + unsigned long startTime = millis(); + while (P199_ESPEasySerial->available() < rx_size && (millis() - startTime) < 1000) { + delay(10); // Wait for response + } + if (P199_ESPEasySerial->available() >= rx_size) { + + P199_ESPEasySerial->readBytes(rx_buffer, rx_size); + P199_dump_buffer((uint8_t*)rx_buffer, rx_size); // Debug: Dump the receive buffer content + return 0; + } else { + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Timeout waiting for response")); + return -3; // Timeout + } +} + +// Calculate CRC-16 for Modbus RTU +// This function calculates the CRC-16 checksum for a given array of bytes. +uint16_t P199_calculateCRC(const uint8_t *array, uint8_t len) { + uint16_t _crc, _flag; + _crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { + _crc ^= (uint16_t)array[i]; + for (uint8_t j = 8; j; j--) { + _flag = _crc & 0x0001; + _crc >>= 1; + if (_flag) + _crc ^= 0xA001; + } + } + return _crc; +} + +// Dump the content of a buffer to the log +// This function takes a pointer to a buffer and its length, and logs the content in hexadecimal format. +void P199_dump_buffer(const uint8_t *buffer, size_t length) { +#ifdef P199_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Modbus: Dumping buffer: "); + for (size_t i = 0; i < length; ++i) { + log += String(buffer[i], HEX); + if (i < length - 1) { + log += F(", "); + } + } + addLogMove(LOG_LEVEL_DEBUG, log); + } +#endif // ifdef P199_DEBUG +} + +// Scan Modbus registers from 0x00 to 0xFF for a given node ID +void P199_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) +{ + String log; + uint16_t value = 0; + for (uint8_t reg = start_reg; reg <= end_reg; reg++) { + int result = P199_modbus_readRegister(node_id, reg, &value); + log += F("** Address "); + log += String(reg); + log += F(" (0x"); + log += String(reg, HEX); + + if (result == 0) { + log += F(") = "); + log += String(value); + } else { + log += F(") invalid"); + } + addLogMove(LOG_LEVEL_INFO, log); + } +} + +// Scan Modbus addreses from 0x00 to 0xFF for a given node ID +void P199_scan_modbus() +{ + String log; + uint16_t value = 0; + for (uint8_t id = 0; id <= 247; id++) { + int result = P199_modbus_readRegister(id, 1, &value); + log += F("** Address "); + log += String(id); + + if (result == 0) { + log += F(" OK"); + } else { + log += F(" no response"); + } + addLogMove(LOG_LEVEL_INFO, log); + } +} +#endif // USES_P199 diff --git a/src/_P199_rainsensor.ino b/src/_P199_rainsensor.ino new file mode 100644 index 0000000000..cac71651b3 --- /dev/null +++ b/src/_P199_rainsensor.ino @@ -0,0 +1,304 @@ +#include "_Plugin_Helper.h" + +#ifdef USES_P199 + +// ####################################################################################################### +// ############## Plugin 199: Modbus rain sensor ############### +// ####################################################################################################### + +/* + Plugin written by: Sergio Faustino sjfaustino__AT__gmail.com + + This plugin reads available values of an Eastron SDM120C SDM120/SDM120CT/220/230/630/72D & also DDM18SD. + */ + + + +# define PLUGIN_199 +# define PLUGIN_ID_199 199 +# define PLUGIN_NAME_199 "[testing] Rainsensor - Modbus" +# define P199_NR_OUTPUT_VALUES 1 +# define PLUGIN_VALUENAME1_199 "Rain" + + +# include "src/PluginStructs/P199_data_struct.h" + +// These pointers may be used among multiple instances of the same plugin, +// as long as the same serial settings are used. +ESPeasySerial *P199_ESPEasySerial = nullptr; +SDM *Plugin_199_SDM = nullptr; +boolean P199_init = false; + +boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + auto& dev = Device[++deviceCount]; + dev.Number = PLUGIN_ID_199; + dev.Type = DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins + dev.VType = Sensor_VType::SENSOR_TYPE_SINGLE; // Only one value + dev.FormulaOption = true; + dev.ValueCount = P199_NR_OUTPUT_VALUES; + dev.OutputDataType = Output_Data_type_t::Simple; + dev.SendDataOption = true; + dev.TimerOption = true; + dev.PluginStats = true; + dev.TaskLogsOwnPeaks = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_199); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_199)); + } + + case PLUGIN_GET_DEVICEGPIONAMES: + { + serialHelper_modbus_getGpioNames(event); + break; + } + + case PLUGIN_WEBFORM_SHOW_CONFIG: + { + string += serialHelper_getSerialTypeLabel(event); + success = true; + break; + } + + case PLUGIN_SET_DEFAULTS: + { + P199_DEV_ID = P199_DEV_ID_DFLT; + P199_MODEL = P199_MODEL_DFLT; + P199_BAUDRATE = P199_BAUDRATE_DFLT; + + success = true; + break; + } + + case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS: + { + if ((P199_DEV_ID == 0) || (P199_DEV_ID > 247) || (P199_BAUDRATE >= 6)) { + // Load some defaults + P199_DEV_ID = P199_DEV_ID_DFLT; + P199_MODEL = P199_MODEL_DFLT; + P199_BAUDRATE = P199_BAUDRATE_DFLT; + } + { + String options_baudrate[6]; + + for (int i = 0; i < 6; ++i) { + options_baudrate[i] = P199_storageValueToBaudrate(i); + } + constexpr size_t optionCount = NR_ELEMENTS(options_baudrate); + const FormSelectorOptions selector(optionCount, options_baudrate); + selector.addFormSelector(F("Baud Rate"), P199_BAUDRATE_LABEL, P199_BAUDRATE); + addUnit(F("baud")); + } + + addFormNumericBox(F("Modbus Address"), P199_DEV_ID_LABEL, P199_DEV_ID, 1, 247); + + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), F(P199_FLAG_COLL_DETECT_LABEL), P199_GET_FLAG_COLL_DETECT); + addFormNote(F("/RE connected to GND, only supported on hardware serial")); + # endif // ifdef ESP32 + + break; + } + + case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: + { + + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + + P199_DEV_ID = getFormItemInt(P199_DEV_ID_LABEL); + P199_MODEL = getFormItemInt(P199_MODEL_LABEL); + P199_BAUDRATE = getFormItemInt(P199_BAUDRATE_LABEL); + # ifdef ESP32 + P199_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P199_FLAG_COLL_DETECT_LABEL))); + # endif // ifdef ESP32 + + P199_init = false; // Force device setup next time + success = true; + break; + } + + case PLUGIN_INIT: + { + P199_init = true; + + if (P199_ESPEasySerial != nullptr) { + delete P199_ESPEasySerial; + P199_ESPEasySerial = nullptr; + } + P199_ESPEasySerial = new (std::nothrow) ESPeasySerial(static_cast(CONFIG_PORT), CONFIG_PIN1, CONFIG_PIN2); + + if (P199_ESPEasySerial == nullptr) { + break; + } + + String log = F("P199: Init serial: "); + log += F("RX pin "); + log += CONFIG_PIN1; + log += F(", TX pin "); + log += CONFIG_PIN2; + log += F(", RS485 mode enabled on pin "); + log += P199_DEPIN; + log += F(", baudrate "); + log += P199_storageValueToBaudrate(P199_BAUDRATE); + log += F(", collision detection "); + log += P199_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); + addLogMove(LOG_LEVEL_INFO, log); + + if (P199_ESPEasySerial->setRS485Mode(P199_DEPIN, P199_GET_FLAG_COLL_DETECT)) { + addLogMove(LOG_LEVEL_INFO, F("Using RS485 mode")); + } else { + addLogMove(LOG_LEVEL_INFO, F("Not using RS485 mode, using normal serial mode")); + } + unsigned int baudrate = P199_storageValueToBaudrate(P199_BAUDRATE); + + P199_ESPEasySerial->begin(baudrate); + + success = true; + break; + } + + case PLUGIN_EXIT: + { + P199_init = false; + + delete P199_ESPEasySerial; + P199_ESPEasySerial = nullptr; + + break; + } + + case PLUGIN_TEN_PER_SECOND: + { + + break; + } + + case PLUGIN_ONCE_A_SECOND: + { + uint16_t value = 0; + P199_modbus_readRegister(0x01, 0x0000, &value); // Example register address 0x0000 + addLogMove(LOG_LEVEL_INFO, concat(F("P199 : debug "), value)); + break; + } + + case PLUGIN_READ: + { + uint16_t value = 0; + P199_modbus_readRegister(0x01, 0x0000, &value); // Example register address 0x0000 + UserVar.setFloat(event->TaskIndex, 0, value); + break; + } + + + } + return success; +} + +int P199_storageValueToBaudrate(uint8_t baudrate_setting) { + int baudrate = 9600; + + if (baudrate_setting < 6) { + baudrate = 1200 << baudrate_setting; + } + return baudrate; +} + +int P199_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) +{ + char buffer[32]; + + if (P199_ESPEasySerial == nullptr) { + addLogMove(LOG_LEVEL_DEBUG, F("P199: Serial not initialized")); + return -1; // Not initialized + } + + for (int i = P199_ESPEasySerial->available(); i > 0; --i) { + P199_ESPEasySerial->read(); // Clear any existing data in the buffer + } + + buffer[0] = node_id; + buffer[1] = 0x03; // Function code for reading holding registers + buffer[2] = highByte(reg); // High byte of register address + buffer[3] = lowByte(reg); // Low byte of register address + buffer[4] = 0x00; // Number of registers to read (2 bytes) + buffer[5] = 0x01; // Number of registers to read (2 bytes) + uint16_t crc = P199_calculateCRC((uint8_t*)buffer, 6); + buffer[6] = lowByte(crc); // CRC low byte + buffer[7] = highByte(crc); // CRC high byte + P199_dump_buffer((uint8_t*)buffer, 8); // Debug: Dump the buffer content + P199_ESPEasySerial->write((uint8_t*)buffer, 8); + unsigned long startTime = millis(); + while (P199_ESPEasySerial->available() < 5 && (millis() - startTime) < 1000) { + delay(10); // Wait for response + } + if (P199_ESPEasySerial->available() >= 7) { + uint8_t response[7]; + P199_ESPEasySerial->readBytes(response, 7); + P199_dump_buffer((uint8_t*)response, 8); + + if (response[0] == node_id && response[1] == 0x03 && response[2] == 0x02) { + *value = (response[3] << 8) | response[4]; // Combine high and low byte + addLogMove(LOG_LEVEL_DEBUG, concat("P199: received value: ", *value)); + return 0; // Success + } else { + addLogMove(LOG_LEVEL_DEBUG, F("P199: Invalid response received")); + return -2; // Invalid response + } + } else { + addLogMove(LOG_LEVEL_DEBUG, F("P199: Timeout waiting for response")); + return -3; // Timeout + } +} + +uint16_t P199_calculateCRC(const uint8_t *array, uint8_t len) { + uint16_t _crc, _flag; + _crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { + _crc ^= (uint16_t)array[i]; + for (uint8_t j = 8; j; j--) { + _flag = _crc & 0x0001; + _crc >>= 1; + if (_flag) + _crc ^= 0xA001; + } + } + return _crc; +} + +void P199_dump_buffer(const uint8_t *buffer, size_t length) { + String log = F("P199: Dumping buffer: "); + for (size_t i = 0; i < length; ++i) { + log += String(buffer[i], HEX); + if (i < length - 1) { + log += F(", "); + } + } + addLogMove(LOG_LEVEL_DEBUG, log); +} + +#endif // USES_P199 diff --git a/src/src/PluginStructs/P199_data_struct.cpp b/src/src/PluginStructs/P199_data_struct.cpp new file mode 100644 index 0000000000..6397b52278 --- /dev/null +++ b/src/src/PluginStructs/P199_data_struct.cpp @@ -0,0 +1,13 @@ +#include "../PluginStructs/P199_data_struct.h" + +#include "../../_Plugin_Helper.h" + +#ifdef USES_P199 + +# include + + + + + +#endif // ifdef USES_P199 diff --git a/src/src/PluginStructs/P199_data_struct.h b/src/src/PluginStructs/P199_data_struct.h new file mode 100644 index 0000000000..a992aa6039 --- /dev/null +++ b/src/src/PluginStructs/P199_data_struct.h @@ -0,0 +1,34 @@ +#ifndef PLUGINSTRUCTS_P199_DATA_STRUCT_H +#define PLUGINSTRUCTS_P199_DATA_STRUCT_H + +#include "../../ESPEasy_common.h" + +#ifdef USES_P199 +# include "../../_Plugin_Helper.h" +# include + + +# define P199_DEV_ID PCONFIG(0) +# define P199_DEV_ID_LABEL PCONFIG_LABEL(0) +# define P199_MODEL PCONFIG(1) +# define P199_MODEL_LABEL PCONFIG_LABEL(1) +# define P199_BAUDRATE PCONFIG(2) +# define P199_BAUDRATE_LABEL PCONFIG_LABEL(2) + +# define P199_GET_FLAG_COLL_DETECT bitRead(PCONFIG(7), 0) +# define P199_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(7), 0, x) +# define P199_FLAG_COLL_DETECT_LABEL "colldet" + +# define P199_QUERY1_CONFIG_POS 3 + +# define P199_DEPIN CONFIG_PIN3 + +# define P199_DEV_ID_DFLT 1 +# define P199_MODEL_DFLT 0 // SDM120C +# define P199_BAUDRATE_DFLT 3 // 9600 baud + + + +#endif // ifdef USES_P199 + +#endif // ifndef PLUGINSTRUCTS_P199_DATA_STRUCT_H From 46f2a50bc8b8256615f1f250d8f707a36697bdec Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:16:48 +0200 Subject: [PATCH 02/48] P183 2nd attempt --- src/_P183_modbus.ino | 227 +++++++-------- src/_P199_rainsensor.ino | 304 --------------------- src/src/PluginStructs/P199_data_struct.cpp | 13 - src/src/PluginStructs/P199_data_struct.h | 34 --- 4 files changed, 116 insertions(+), 462 deletions(-) delete mode 100644 src/_P199_rainsensor.ino delete mode 100644 src/src/PluginStructs/P199_data_struct.cpp delete mode 100644 src/src/PluginStructs/P199_data_struct.h diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 6d95aa1c56..e050df41a2 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -1,9 +1,9 @@ #include "_Plugin_Helper.h" -#ifdef USES_P199 +#ifdef USES_P183 // ####################################################################################################### -// ############## Plugin 199: Modbus rain sensor ############### +// ############## Plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### /* @@ -12,54 +12,59 @@ This plugin reads values from a Modbus RTU device. */ +/** + * Changelog: + * 2025-08-24 flasmark: Initial version + */ -# define P199_DEBUG -# define PLUGIN_199 -# define PLUGIN_ID_199 199 -# define PLUGIN_NAME_199 "[testing] Modbus RTU" -# define P199_NR_OUTPUT_VALUES 4 -# define PLUGIN_VALUENAME1_199 "Value1" -# define PLUGIN_VALUENAME2_199 "Value2" -# define PLUGIN_VALUENAME3_199 "Value3" -# define PLUGIN_VALUENAME4_199 "Value4" +# define P183_DEBUG // Switch on additional debug logging +# define PLUGIN_183 +# define PLUGIN_ID_183 183 +# define PLUGIN_NAME_183 "[testing] Modbus RTU" +# define P183_NR_OUTPUT_VALUES 4 +# define PLUGIN_VALUENAME1_183 "Value1" +# define PLUGIN_VALUENAME2_183 "Value2" +# define PLUGIN_VALUENAME3_183 "Value3" +# define PLUGIN_VALUENAME4_183 "Value4" // Plugin configuration parameters // PCONFIG(0) is the Modbus device ID, -// PCONFIG(1) is the baud rate. +// PCONFIG(1) is the serial baud rate. // PCONFIG(2) is used for flags, where bit 0 indicates collision detection -// PCONFIG(3) is Modbus register address for value 1 -// PCONFIG(4) is Modbus register address for value 2 -// PCONFIG(5) is Modbus register address for value 3 -// PCONFIG(6) is Modbus register address for value 4 -# define P199_DEV_ID PCONFIG(0) -# define P199_DEV_ID_LABEL PCONFIG_LABEL(0) -# define P199_BAUDRATE PCONFIG(1) -# define P199_BAUDRATE_LABEL PCONFIG_LABEL(1) -# define P199_ADDRESS(x) PCONFIG(3 + x) -# define P199_ADDRESS_LABEL(x) concat(F("addr"), x) +// PCONFIG(3) is the Modbus register address for value 1 +// PCONFIG(4) is the Modbus register address for value 2 +// PCONFIG(5) is the Modbus register address for value 3 +// PCONFIG(6) is the Modbus register address for value 4 +// Use P183_ADDRESS(x) to access the PCONFIG value for value x +# define P183_DEV_ID PCONFIG(0) +# define P183_DEV_ID_LABEL PCONFIG_LABEL(0) +# define P183_BAUDRATE PCONFIG(1) +# define P183_BAUDRATE_LABEL PCONFIG_LABEL(1) +# define P183_ADDRESS(x) PCONFIG(3 + x) +# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) -# define P199_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) -# define P199_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) -# define P199_FLAG_COLL_DETECT_LABEL "colldet" +# define P183_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) +# define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) +# define P183_FLAG_COLL_DETECT_LABEL "colldet" -# define P199_QUERY1_CONFIG_POS 3 +# define P183_QUERY1_CONFIG_POS 3 -# define P199_DEPIN CONFIG_PIN3 +# define P183_DEPIN CONFIG_PIN3 -# define P199_DEV_ID_DFLT 1 -# define P199_BAUDRATE_DFLT 3 // 9600 baud +# define P183_DEV_ID_DFLT 1 +# define P183_BAUDRATE_DFLT 3 // 9600 baud # include // These pointers may be used among multiple instances of the same plugin, // as long as the same serial settings are used. -ESPeasySerial *P199_ESPEasySerial = nullptr; -boolean P199_init = false; +ESPeasySerial *P183_ESPEasySerial = nullptr; +boolean P183_init = false; -void P199_scan_modbus(); -void P199_scan_module(uint8_t node_id, uint8_t start_reg = 0x00, uint8_t end_reg = 0xFF); +void P183_scan_modbus(); +void P183_scan_module(uint8_t node_id, uint8_t start_reg = 0x00, uint8_t end_reg = 0xFF); -boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) +boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { boolean success = false; @@ -68,11 +73,11 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_DEVICE_ADD: { auto& dev = Device[++deviceCount]; - dev.Number = PLUGIN_ID_199; + dev.Number = PLUGIN_ID_183; dev.Type = DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins - dev.VType = Sensor_VType::SENSOR_TYPE_SINGLE; // Only one value + dev.VType = Sensor_VType::SENSOR_TYPE_QUAD; dev.FormulaOption = true; - dev.ValueCount = P199_NR_OUTPUT_VALUES; + dev.ValueCount = P183_NR_OUTPUT_VALUES; dev.OutputDataType = Output_Data_type_t::Simple; dev.SendDataOption = true; dev.TimerOption = true; @@ -83,16 +88,16 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_GET_DEVICENAME: { - string = F(PLUGIN_NAME_199); + string = F(PLUGIN_NAME_183); break; } case PLUGIN_GET_DEVICEVALUENAMES: { - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_199)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_199)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_199)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_199)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_183)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_183)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_183)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_183)); } case PLUGIN_GET_DEVICEGPIONAMES: @@ -110,8 +115,8 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: { - P199_DEV_ID = P199_DEV_ID_DFLT; - P199_BAUDRATE = P199_BAUDRATE_DFLT; + P183_DEV_ID = P183_DEV_ID_DFLT; + P183_BAUDRATE = P183_BAUDRATE_DFLT; success = true; break; @@ -119,27 +124,27 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS: { - if ((P199_DEV_ID == 0) || (P199_DEV_ID > 247) || (P199_BAUDRATE >= 6)) { + if ((P183_DEV_ID == 0) || (P183_DEV_ID > 247) || (P183_BAUDRATE >= 6)) { // Load some defaults - P199_DEV_ID = P199_DEV_ID_DFLT; - P199_BAUDRATE = P199_BAUDRATE_DFLT; + P183_DEV_ID = P183_DEV_ID_DFLT; + P183_BAUDRATE = P183_BAUDRATE_DFLT; } { String options_baudrate[6]; for (int i = 0; i < 6; ++i) { - options_baudrate[i] = P199_storageValueToBaudrate(i); + options_baudrate[i] = P183_storageValueToBaudrate(i); } constexpr size_t optionCount = NR_ELEMENTS(options_baudrate); const FormSelectorOptions selector(optionCount, options_baudrate); - selector.addFormSelector(F("Baud Rate"), P199_BAUDRATE_LABEL, P199_BAUDRATE); + selector.addFormSelector(F("Baud Rate"), P183_BAUDRATE_LABEL, P183_BAUDRATE); addUnit(F("baud")); } - addFormNumericBox(F("Modbus Address"), P199_DEV_ID_LABEL, P199_DEV_ID, 1, 247); + addFormNumericBox(F("Modbus Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); # ifdef ESP32 - addFormCheckBox(F("Enable Collision Detection"), F(P199_FLAG_COLL_DETECT_LABEL), P199_GET_FLAG_COLL_DETECT); + addFormCheckBox(F("Enable Collision Detection"), F(P183_FLAG_COLL_DETECT_LABEL), P183_GET_FLAG_COLL_DETECT); addFormNote(F("/RE connected to GND, only supported on hardware serial")); # endif // ifdef ESP32 @@ -148,9 +153,9 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: { - for (int outputIndex = 0; outputIndex < P199_NR_OUTPUT_VALUES; ++outputIndex) + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { - addFormNumericBox(concat(F("Value "), outputIndex + 1), P199_ADDRESS_LABEL(outputIndex), P199_ADDRESS(outputIndex)); + addFormNumericBox(concat(F("Value "), outputIndex + 1), P183_ADDRESS_LABEL(outputIndex), P183_ADDRESS(outputIndex)); } break; } @@ -164,61 +169,61 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - P199_DEV_ID = getFormItemInt(P199_DEV_ID_LABEL); - P199_BAUDRATE = getFormItemInt(P199_BAUDRATE_LABEL); + P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); + P183_BAUDRATE = getFormItemInt(P183_BAUDRATE_LABEL); # ifdef ESP32 - P199_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P199_FLAG_COLL_DETECT_LABEL))); + P183_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P183_FLAG_COLL_DETECT_LABEL))); # endif // ifdef ESP32 - for (int outputIndex = 0; outputIndex < P199_NR_OUTPUT_VALUES; ++outputIndex) + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { - P199_ADDRESS(outputIndex) = getFormItemInt( P199_ADDRESS_LABEL(outputIndex)); + P183_ADDRESS(outputIndex) = getFormItemInt( P183_ADDRESS_LABEL(outputIndex)); } - P199_init = false; // Force device setup next time + P183_init = false; // Force device setup next time success = true; break; } case PLUGIN_INIT: { - P199_init = true; + P183_init = true; // (re)create the serial port object // If the serial port object already exists, delete it first. - if (P199_ESPEasySerial != nullptr) { - delete P199_ESPEasySerial; - P199_ESPEasySerial = nullptr; + if (P183_ESPEasySerial != nullptr) { + delete P183_ESPEasySerial; + P183_ESPEasySerial = nullptr; } - P199_ESPEasySerial = new (std::nothrow) ESPeasySerial(static_cast(CONFIG_PORT), CONFIG_PIN1, CONFIG_PIN2); + P183_ESPEasySerial = new (std::nothrow) ESPeasySerial(static_cast(CONFIG_PORT), CONFIG_PIN1, CONFIG_PIN2); - if (P199_ESPEasySerial == nullptr) { + if (P183_ESPEasySerial == nullptr) { break; } // Set RS485 mode if requested using selected pin for RTS - bool rs485Mode = P199_ESPEasySerial->setRS485Mode(P199_DEPIN, P199_GET_FLAG_COLL_DETECT); + bool rs485Mode = P183_ESPEasySerial->setRS485Mode(P183_DEPIN, P183_GET_FLAG_COLL_DETECT); - unsigned int baudrate = P199_storageValueToBaudrate(P199_BAUDRATE); - P199_ESPEasySerial->begin(baudrate); + unsigned int baudrate = P183_storageValueToBaudrate(P183_BAUDRATE); + P183_ESPEasySerial->begin(baudrate); - #ifdef P199_DEBUG + #ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P199: Init serial: RX pin "); + String log = F("P183: Init serial: RX pin "); log += CONFIG_PIN1; log += F(", TX pin "); log += CONFIG_PIN2; log += F(", RS485 mode selected on pin "); - log += P199_DEPIN; + log += P183_DEPIN; log += F(", baudrate "); - log += P199_storageValueToBaudrate(P199_BAUDRATE); + log += P183_storageValueToBaudrate(P183_BAUDRATE); log += F(", collision detection "); - log += P199_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); + log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); log += F(", RS485mode enabled: "); log += rs485Mode ? F("yes") : F("no"); addLogMove(LOG_LEVEL_INFO, log); } - #endif // ifdef P199_DEBUG + #endif // ifdef P183_DEBUG success = true; break; @@ -226,10 +231,10 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_EXIT: { - P199_init = false; + P183_init = false; - delete P199_ESPEasySerial; - P199_ESPEasySerial = nullptr; + delete P183_ESPEasySerial; + P183_ESPEasySerial = nullptr; break; } @@ -249,16 +254,16 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { uint16_t value = 0; - for (int outputIndex = 0; outputIndex < P199_NR_OUTPUT_VALUES; ++outputIndex) + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { - P199_modbus_readRegister(P199_DEV_ID, P199_ADDRESS(outputIndex), &value); + P183_modbus_readRegister(P183_DEV_ID, P183_ADDRESS(outputIndex), &value); UserVar.setFloat(event->TaskIndex, outputIndex, value); } break; } case PLUGIN_WRITE: { - if (P199_ESPEasySerial != nullptr) { + if (P183_ESPEasySerial != nullptr) { const String cmd = parseString(string, 1); if (equals(cmd, F("modbus"))) { @@ -268,7 +273,7 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) // Write a value to a Modbus register int address = parseString(string, 3).toInt(); uint16_t value = parseString(string, 4).toInt(); - P199_modbus_writeRegister(P199_DEV_ID, address, value); + P183_modbus_writeRegister(P183_DEV_ID, address, value); String log = F("Modbus: write value "); log += value; log += F(" to address "); @@ -279,14 +284,14 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) else if (equals(subcmd, F("scan"))) { // Scan for Modbus devices addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); - P199_scan_modbus(); + P183_scan_modbus(); success = true; } else if (equals(subcmd, F("read"))) { // Read a value from a Modbus register int address = parseString(string, 3).toInt(); uint16_t value = 0; - P199_modbus_readRegister(P199_DEV_ID, address, &value); + P183_modbus_readRegister(P183_DEV_ID, address, &value); String log = F("Modbus: read value "); log += value; log += F(" from address "); @@ -304,7 +309,7 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) end_address = start_address + 100; // Limit to 100 addresses } addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); - P199_scan_module(P199_DEV_ID, start_address, end_address); + P183_scan_module(P183_DEV_ID, start_address, end_address); success = true; } else { @@ -319,7 +324,7 @@ boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) return success; } -int P199_storageValueToBaudrate(uint8_t baudrate_setting) { +int P183_storageValueToBaudrate(uint8_t baudrate_setting) { int baudrate = 9600; if (baudrate_setting < 6) { @@ -328,7 +333,7 @@ int P199_storageValueToBaudrate(uint8_t baudrate_setting) { return baudrate; } -int P199_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) +int P183_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) { uint8_t buffer[8]; // Buffer for Modbus request uint8_t response[8]; // Buffer for Modbus response @@ -339,11 +344,11 @@ int P199_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) buffer[3] = lowByte(reg); // Low byte of register address buffer[4] = 0x00; // Number of registers to read (2 bytes) buffer[5] = 0x01; // Number of registers to read (2 bytes) - uint16_t crc = P199_calculateCRC((uint8_t*)buffer, 6); + uint16_t crc = P183_calculateCRC((uint8_t*)buffer, 6); buffer[6] = lowByte(crc); // CRC low byte buffer[7] = highByte(crc); // CRC high byte - if (P199_modbus_exchange_message(buffer, response, 8, 7) < 0) { + if (P183_modbus_exchange_message(buffer, response, 8, 7) < 0) { return -1; // Failed to exchange message } @@ -357,7 +362,7 @@ int P199_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) } } -int P199_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) +int P183_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) { uint8_t buffer[8]; uint8_t response[8]; @@ -368,15 +373,15 @@ int P199_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) buffer[3] = lowByte(reg); // Low byte of register address buffer[4] = highByte(value); // High byte of value to write buffer[5] = lowByte(value); // Low byte of value to write - uint16_t crc = P199_calculateCRC((uint8_t*)buffer, 6); + uint16_t crc = P183_calculateCRC((uint8_t*)buffer, 6); buffer[6] = lowByte(crc); // CRC low byte buffer[7] = highByte(crc); // CRC high byte - if (P199_modbus_exchange_message(buffer, response, 8, 7) < 0) { + if (P183_modbus_exchange_message(buffer, response, 8, 7) < 0) { return -1; // Failed to exchange message } if (response[0] == node_id && response[1] == 0x06 && response[2] == highByte(reg) && response[3] == lowByte(reg)) { - uint16_t crc = P199_calculateCRC((uint8_t*)response, 6); + uint16_t crc = P183_calculateCRC((uint8_t*)response, 6); if (response[5] != lowByte(crc) || response[6] != highByte(crc)) { addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid CRC in response")); return -2; // Invalid response @@ -390,28 +395,28 @@ int P199_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) } // Exchange Modbus RTU messages. Send the tx_buffer and wait for a response in rx_buffer. -int P199_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t tx_size, uint8_t rx_size) +int P183_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t tx_size, uint8_t rx_size) { - if (P199_ESPEasySerial == nullptr) { + if (P183_ESPEasySerial == nullptr) { addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Serial not initialized")); return -1; // Not initialized } - for (int i = P199_ESPEasySerial->available(); i > 0; --i) { - P199_ESPEasySerial->read(); // Clear any existing data in the buffer + for (int i = P183_ESPEasySerial->available(); i > 0; --i) { + P183_ESPEasySerial->read(); // Clear any existing data in the buffer } - P199_dump_buffer((uint8_t*)tx_buffer, tx_size); // Debug: Dump the transmit buffer content - P199_ESPEasySerial->write((uint8_t*)tx_buffer, tx_size); + P183_dump_buffer((uint8_t*)tx_buffer, tx_size); // Debug: Dump the transmit buffer content + P183_ESPEasySerial->write((uint8_t*)tx_buffer, tx_size); unsigned long startTime = millis(); - while (P199_ESPEasySerial->available() < rx_size && (millis() - startTime) < 1000) { + while (P183_ESPEasySerial->available() < rx_size && (millis() - startTime) < 1000) { delay(10); // Wait for response } - if (P199_ESPEasySerial->available() >= rx_size) { + if (P183_ESPEasySerial->available() >= rx_size) { - P199_ESPEasySerial->readBytes(rx_buffer, rx_size); - P199_dump_buffer((uint8_t*)rx_buffer, rx_size); // Debug: Dump the receive buffer content + P183_ESPEasySerial->readBytes(rx_buffer, rx_size); + P183_dump_buffer((uint8_t*)rx_buffer, rx_size); // Debug: Dump the receive buffer content return 0; } else { addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Timeout waiting for response")); @@ -421,7 +426,7 @@ int P199_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t // Calculate CRC-16 for Modbus RTU // This function calculates the CRC-16 checksum for a given array of bytes. -uint16_t P199_calculateCRC(const uint8_t *array, uint8_t len) { +uint16_t P183_calculateCRC(const uint8_t *array, uint8_t len) { uint16_t _crc, _flag; _crc = 0xFFFF; for (uint8_t i = 0; i < len; i++) { @@ -438,8 +443,8 @@ uint16_t P199_calculateCRC(const uint8_t *array, uint8_t len) { // Dump the content of a buffer to the log // This function takes a pointer to a buffer and its length, and logs the content in hexadecimal format. -void P199_dump_buffer(const uint8_t *buffer, size_t length) { -#ifdef P199_DEBUG +void P183_dump_buffer(const uint8_t *buffer, size_t length) { +#ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: Dumping buffer: "); for (size_t i = 0; i < length; ++i) { @@ -450,16 +455,16 @@ void P199_dump_buffer(const uint8_t *buffer, size_t length) { } addLogMove(LOG_LEVEL_DEBUG, log); } -#endif // ifdef P199_DEBUG +#endif // ifdef P183_DEBUG } // Scan Modbus registers from 0x00 to 0xFF for a given node ID -void P199_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) +void P183_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { String log; uint16_t value = 0; for (uint8_t reg = start_reg; reg <= end_reg; reg++) { - int result = P199_modbus_readRegister(node_id, reg, &value); + int result = P183_modbus_readRegister(node_id, reg, &value); log += F("** Address "); log += String(reg); log += F(" (0x"); @@ -476,12 +481,12 @@ void P199_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) } // Scan Modbus addreses from 0x00 to 0xFF for a given node ID -void P199_scan_modbus() +void P183_scan_modbus() { String log; uint16_t value = 0; for (uint8_t id = 0; id <= 247; id++) { - int result = P199_modbus_readRegister(id, 1, &value); + int result = P183_modbus_readRegister(id, 1, &value); log += F("** Address "); log += String(id); @@ -493,4 +498,4 @@ void P199_scan_modbus() addLogMove(LOG_LEVEL_INFO, log); } } -#endif // USES_P199 +#endif // USES_P183 diff --git a/src/_P199_rainsensor.ino b/src/_P199_rainsensor.ino deleted file mode 100644 index cac71651b3..0000000000 --- a/src/_P199_rainsensor.ino +++ /dev/null @@ -1,304 +0,0 @@ -#include "_Plugin_Helper.h" - -#ifdef USES_P199 - -// ####################################################################################################### -// ############## Plugin 199: Modbus rain sensor ############### -// ####################################################################################################### - -/* - Plugin written by: Sergio Faustino sjfaustino__AT__gmail.com - - This plugin reads available values of an Eastron SDM120C SDM120/SDM120CT/220/230/630/72D & also DDM18SD. - */ - - - -# define PLUGIN_199 -# define PLUGIN_ID_199 199 -# define PLUGIN_NAME_199 "[testing] Rainsensor - Modbus" -# define P199_NR_OUTPUT_VALUES 1 -# define PLUGIN_VALUENAME1_199 "Rain" - - -# include "src/PluginStructs/P199_data_struct.h" - -// These pointers may be used among multiple instances of the same plugin, -// as long as the same serial settings are used. -ESPeasySerial *P199_ESPEasySerial = nullptr; -SDM *Plugin_199_SDM = nullptr; -boolean P199_init = false; - -boolean Plugin_199(uint8_t function, struct EventStruct *event, String& string) -{ - boolean success = false; - - switch (function) - { - case PLUGIN_DEVICE_ADD: - { - auto& dev = Device[++deviceCount]; - dev.Number = PLUGIN_ID_199; - dev.Type = DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins - dev.VType = Sensor_VType::SENSOR_TYPE_SINGLE; // Only one value - dev.FormulaOption = true; - dev.ValueCount = P199_NR_OUTPUT_VALUES; - dev.OutputDataType = Output_Data_type_t::Simple; - dev.SendDataOption = true; - dev.TimerOption = true; - dev.PluginStats = true; - dev.TaskLogsOwnPeaks = true; - break; - } - - case PLUGIN_GET_DEVICENAME: - { - string = F(PLUGIN_NAME_199); - break; - } - - case PLUGIN_GET_DEVICEVALUENAMES: - { - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_199)); - } - - case PLUGIN_GET_DEVICEGPIONAMES: - { - serialHelper_modbus_getGpioNames(event); - break; - } - - case PLUGIN_WEBFORM_SHOW_CONFIG: - { - string += serialHelper_getSerialTypeLabel(event); - success = true; - break; - } - - case PLUGIN_SET_DEFAULTS: - { - P199_DEV_ID = P199_DEV_ID_DFLT; - P199_MODEL = P199_MODEL_DFLT; - P199_BAUDRATE = P199_BAUDRATE_DFLT; - - success = true; - break; - } - - case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS: - { - if ((P199_DEV_ID == 0) || (P199_DEV_ID > 247) || (P199_BAUDRATE >= 6)) { - // Load some defaults - P199_DEV_ID = P199_DEV_ID_DFLT; - P199_MODEL = P199_MODEL_DFLT; - P199_BAUDRATE = P199_BAUDRATE_DFLT; - } - { - String options_baudrate[6]; - - for (int i = 0; i < 6; ++i) { - options_baudrate[i] = P199_storageValueToBaudrate(i); - } - constexpr size_t optionCount = NR_ELEMENTS(options_baudrate); - const FormSelectorOptions selector(optionCount, options_baudrate); - selector.addFormSelector(F("Baud Rate"), P199_BAUDRATE_LABEL, P199_BAUDRATE); - addUnit(F("baud")); - } - - addFormNumericBox(F("Modbus Address"), P199_DEV_ID_LABEL, P199_DEV_ID, 1, 247); - - # ifdef ESP32 - addFormCheckBox(F("Enable Collision Detection"), F(P199_FLAG_COLL_DETECT_LABEL), P199_GET_FLAG_COLL_DETECT); - addFormNote(F("/RE connected to GND, only supported on hardware serial")); - # endif // ifdef ESP32 - - break; - } - - case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: - { - - break; - } - - case PLUGIN_WEBFORM_LOAD: - { - success = true; - break; - } - - case PLUGIN_WEBFORM_SAVE: - { - - P199_DEV_ID = getFormItemInt(P199_DEV_ID_LABEL); - P199_MODEL = getFormItemInt(P199_MODEL_LABEL); - P199_BAUDRATE = getFormItemInt(P199_BAUDRATE_LABEL); - # ifdef ESP32 - P199_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P199_FLAG_COLL_DETECT_LABEL))); - # endif // ifdef ESP32 - - P199_init = false; // Force device setup next time - success = true; - break; - } - - case PLUGIN_INIT: - { - P199_init = true; - - if (P199_ESPEasySerial != nullptr) { - delete P199_ESPEasySerial; - P199_ESPEasySerial = nullptr; - } - P199_ESPEasySerial = new (std::nothrow) ESPeasySerial(static_cast(CONFIG_PORT), CONFIG_PIN1, CONFIG_PIN2); - - if (P199_ESPEasySerial == nullptr) { - break; - } - - String log = F("P199: Init serial: "); - log += F("RX pin "); - log += CONFIG_PIN1; - log += F(", TX pin "); - log += CONFIG_PIN2; - log += F(", RS485 mode enabled on pin "); - log += P199_DEPIN; - log += F(", baudrate "); - log += P199_storageValueToBaudrate(P199_BAUDRATE); - log += F(", collision detection "); - log += P199_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); - addLogMove(LOG_LEVEL_INFO, log); - - if (P199_ESPEasySerial->setRS485Mode(P199_DEPIN, P199_GET_FLAG_COLL_DETECT)) { - addLogMove(LOG_LEVEL_INFO, F("Using RS485 mode")); - } else { - addLogMove(LOG_LEVEL_INFO, F("Not using RS485 mode, using normal serial mode")); - } - unsigned int baudrate = P199_storageValueToBaudrate(P199_BAUDRATE); - - P199_ESPEasySerial->begin(baudrate); - - success = true; - break; - } - - case PLUGIN_EXIT: - { - P199_init = false; - - delete P199_ESPEasySerial; - P199_ESPEasySerial = nullptr; - - break; - } - - case PLUGIN_TEN_PER_SECOND: - { - - break; - } - - case PLUGIN_ONCE_A_SECOND: - { - uint16_t value = 0; - P199_modbus_readRegister(0x01, 0x0000, &value); // Example register address 0x0000 - addLogMove(LOG_LEVEL_INFO, concat(F("P199 : debug "), value)); - break; - } - - case PLUGIN_READ: - { - uint16_t value = 0; - P199_modbus_readRegister(0x01, 0x0000, &value); // Example register address 0x0000 - UserVar.setFloat(event->TaskIndex, 0, value); - break; - } - - - } - return success; -} - -int P199_storageValueToBaudrate(uint8_t baudrate_setting) { - int baudrate = 9600; - - if (baudrate_setting < 6) { - baudrate = 1200 << baudrate_setting; - } - return baudrate; -} - -int P199_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) -{ - char buffer[32]; - - if (P199_ESPEasySerial == nullptr) { - addLogMove(LOG_LEVEL_DEBUG, F("P199: Serial not initialized")); - return -1; // Not initialized - } - - for (int i = P199_ESPEasySerial->available(); i > 0; --i) { - P199_ESPEasySerial->read(); // Clear any existing data in the buffer - } - - buffer[0] = node_id; - buffer[1] = 0x03; // Function code for reading holding registers - buffer[2] = highByte(reg); // High byte of register address - buffer[3] = lowByte(reg); // Low byte of register address - buffer[4] = 0x00; // Number of registers to read (2 bytes) - buffer[5] = 0x01; // Number of registers to read (2 bytes) - uint16_t crc = P199_calculateCRC((uint8_t*)buffer, 6); - buffer[6] = lowByte(crc); // CRC low byte - buffer[7] = highByte(crc); // CRC high byte - P199_dump_buffer((uint8_t*)buffer, 8); // Debug: Dump the buffer content - P199_ESPEasySerial->write((uint8_t*)buffer, 8); - unsigned long startTime = millis(); - while (P199_ESPEasySerial->available() < 5 && (millis() - startTime) < 1000) { - delay(10); // Wait for response - } - if (P199_ESPEasySerial->available() >= 7) { - uint8_t response[7]; - P199_ESPEasySerial->readBytes(response, 7); - P199_dump_buffer((uint8_t*)response, 8); - - if (response[0] == node_id && response[1] == 0x03 && response[2] == 0x02) { - *value = (response[3] << 8) | response[4]; // Combine high and low byte - addLogMove(LOG_LEVEL_DEBUG, concat("P199: received value: ", *value)); - return 0; // Success - } else { - addLogMove(LOG_LEVEL_DEBUG, F("P199: Invalid response received")); - return -2; // Invalid response - } - } else { - addLogMove(LOG_LEVEL_DEBUG, F("P199: Timeout waiting for response")); - return -3; // Timeout - } -} - -uint16_t P199_calculateCRC(const uint8_t *array, uint8_t len) { - uint16_t _crc, _flag; - _crc = 0xFFFF; - for (uint8_t i = 0; i < len; i++) { - _crc ^= (uint16_t)array[i]; - for (uint8_t j = 8; j; j--) { - _flag = _crc & 0x0001; - _crc >>= 1; - if (_flag) - _crc ^= 0xA001; - } - } - return _crc; -} - -void P199_dump_buffer(const uint8_t *buffer, size_t length) { - String log = F("P199: Dumping buffer: "); - for (size_t i = 0; i < length; ++i) { - log += String(buffer[i], HEX); - if (i < length - 1) { - log += F(", "); - } - } - addLogMove(LOG_LEVEL_DEBUG, log); -} - -#endif // USES_P199 diff --git a/src/src/PluginStructs/P199_data_struct.cpp b/src/src/PluginStructs/P199_data_struct.cpp deleted file mode 100644 index 6397b52278..0000000000 --- a/src/src/PluginStructs/P199_data_struct.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "../PluginStructs/P199_data_struct.h" - -#include "../../_Plugin_Helper.h" - -#ifdef USES_P199 - -# include - - - - - -#endif // ifdef USES_P199 diff --git a/src/src/PluginStructs/P199_data_struct.h b/src/src/PluginStructs/P199_data_struct.h deleted file mode 100644 index a992aa6039..0000000000 --- a/src/src/PluginStructs/P199_data_struct.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef PLUGINSTRUCTS_P199_DATA_STRUCT_H -#define PLUGINSTRUCTS_P199_DATA_STRUCT_H - -#include "../../ESPEasy_common.h" - -#ifdef USES_P199 -# include "../../_Plugin_Helper.h" -# include - - -# define P199_DEV_ID PCONFIG(0) -# define P199_DEV_ID_LABEL PCONFIG_LABEL(0) -# define P199_MODEL PCONFIG(1) -# define P199_MODEL_LABEL PCONFIG_LABEL(1) -# define P199_BAUDRATE PCONFIG(2) -# define P199_BAUDRATE_LABEL PCONFIG_LABEL(2) - -# define P199_GET_FLAG_COLL_DETECT bitRead(PCONFIG(7), 0) -# define P199_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(7), 0, x) -# define P199_FLAG_COLL_DETECT_LABEL "colldet" - -# define P199_QUERY1_CONFIG_POS 3 - -# define P199_DEPIN CONFIG_PIN3 - -# define P199_DEV_ID_DFLT 1 -# define P199_MODEL_DFLT 0 // SDM120C -# define P199_BAUDRATE_DFLT 3 // 9600 baud - - - -#endif // ifdef USES_P199 - -#endif // ifndef PLUGINSTRUCTS_P199_DATA_STRUCT_H From 7d0e802ef1144d1d80c8d17889be927daaeade37 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:15:57 +0200 Subject: [PATCH 03/48] P183 Add initial documentation --- docs/source/Plugin/P183.rst | 108 ++++++++++++++++++ docs/source/Plugin/P183_commands.repl | 0 .../Plugin/_plugin_substitutions_p18x.repl | 13 +++ 3 files changed, 121 insertions(+) create mode 100644 docs/source/Plugin/P183.rst create mode 100644 docs/source/Plugin/P183_commands.repl create mode 100644 docs/source/Plugin/_plugin_substitutions_p18x.repl diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst new file mode 100644 index 0000000000..fc487a1f9d --- /dev/null +++ b/docs/source/Plugin/P183.rst @@ -0,0 +1,108 @@ +.. include:: ../Plugin/_plugin_substitutions_p18x.repl +.. _P183_page: + +|P183_typename| +================================================== + +|P183_shortinfo| + +Plugin details +-------------- + +Type: |P183_type| + +Name: |P183_name| + +Status: |P183_status| + +GitHub: |P183_github|_ + +Maintainer: |P183_maintainer| + +Used libraries: |P183_usedlibraries| + +Introduction +------------ + +Modbus is a serial communication protocol commonly used for connecting industrial electronic devices. +It is a master/slave (or client/server) protocol, which means that one device (the master) initiates communication and the other devices (the slaves) respond. +Modbus RTU (Remote Terminal Unit) is a variant of the Modbus protocol that uses binary representation of data and is typically used over serial communication lines such as RS-485 or RS-232. +This plugin supports Modbus RTU communication over a serial interface. To support RS-485 communication, an external RS-485 to TTL converter is required. + +Modbus RTU uses a register-based addressing scheme, where each device has a unique address and data is stored in registers. +Registers can be of different types, such as holding registers, input registers, coils, and discrete inputs. +This plugin supports reading holding registers (function code 03) and writing to holding registers (function code 06). + +The plugin can be configured to read up to 4 holding registers from a Modbus slave device and store the values in user variables. +It can also write values to a specified holding register on the Modbus slave device. + +The plugin does not support any hardware specific features. Instead it is a generic plugin that can be used with any Modbus RTU compatible device. You have to lookup the correct register addresses and data formats in the documentation of the Modbus device you want to communicate with. + +Supported hardware +------------------ + +|P183_usedby| + +Configuration +------------- + +* **Name**: Required by ESPEasy, must be unique among the list of available devices/tasks. + +* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources. + +Sensor +^^^^^^ + +The available Serial protocol settings here depend on the build used. + +* **Serial Port**: The serial port to use for Modbus communication. + +* **Baudrate**: The baudrate for the serial communication. Common values are 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200. + +* **Modbus Device Address**: The Modbus slave device ID to communicate with (1..247). + +* **Enable Collision Detection**: Enable collision detection to avoid data corruption when multiple devices are connected to the same serial bus. This requires a GPIO pin to be configured as input and connected to the bus transceiver's DE/RE pin. + +* **ESP RX GPIO ← TX (RO)**: The GPIO pin to use as RX input for the serial communication. This pin should be connected to the TX output of the RS-485 to TTL converter. + +* **ESP TX GPIO → RX (DI)**: The GPIO pin to use as TX output for the serial communication. This pin should be connected to the RX input of the RS-485 to TTL converter. + +* **GPIO → RE/DE(optional)**: The GPIO pin to use for collision detection. This pin should be connected to the DE/RE pin of the RS-485 to TTL converter. + +Output Configuration +^^^^^^^^^^^^^^^^^^^^ + +* **Number of registers to read**: The number of holding registers the plugin will read (1..4). + +* **Holding register for valueX**: The Modbus holding register address to read for valueX (X=1..4). + + +Commands available +^^^^^^^^^^^^^^^^^^ + +.. include:: P183_commands.repl + +.. Events +.. ~~~~~~ + +.. .. include:: P183_events.repl + +Change log +---------- + +.. versionchanged:: 2.0 + ... + + |added| + Major overhaul for 2.0 release. + +.. versionadded:: 1.0 + ... + + |added| + Initial release version. + + + + + diff --git a/docs/source/Plugin/P183_commands.repl b/docs/source/Plugin/P183_commands.repl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/source/Plugin/_plugin_substitutions_p18x.repl b/docs/source/Plugin/_plugin_substitutions_p18x.repl new file mode 100644 index 0000000000..92202600df --- /dev/null +++ b/docs/source/Plugin/_plugin_substitutions_p18x.repl @@ -0,0 +1,13 @@ + +.. |P183_name| replace:: :cyan:`Modbus RTU` +.. |P183_type| replace:: :cyan:`Communication` +.. |P183_typename| replace:: :cyan:`Communication - Modbus RTU` +.. |P183_porttype| replace:: `.` +.. |P183_status| replace:: :yellow:`COLLECTION G` :yellow:`CLIMATE` +.. |P183_github| replace:: P183_modbus.ino +.. _P183_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P183_modbus.ino +.. |P183_usedby| replace:: `Modbus RTU over RS485 devices` +.. |P183_shortinfo| replace:: `Generic Modus RTU sensor` +.. |P183_maintainer| replace:: `flashmark` +.. |P183_compileinfo| replace:: `.` +.. |P183_usedlibraries| replace:: `.` From cc31b1013f56f107a2411272444c9574c5ee8c63 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:08:19 +0200 Subject: [PATCH 04/48] P183 Add documentation (preliminary) --- docs/source/Plugin/P183.rst | 7 +++ docs/source/Plugin/P183_commands.repl | 28 ++++++++++ docs/source/Plugin/P183_output_config.png | Bin 0 -> 13698 bytes docs/source/Plugin/P183_sensor_config.png | Bin 0 -> 24343 bytes docs/source/Plugin/_Plugin.rst | 2 +- docs/source/Plugin/_plugin_categories.repl | 2 +- docs/source/Plugin/_plugin_sets_overview.repl | 38 ++++++++++--- docs/source/Plugin/_plugin_substitutions.repl | 1 + .../Plugin/_plugin_substitutions_p18x.repl | 8 +-- src/_P183_modbus.ino | 52 +++++++++--------- 10 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 docs/source/Plugin/P183_output_config.png create mode 100644 docs/source/Plugin/P183_sensor_config.png diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst index fc487a1f9d..7b85e18b5a 100644 --- a/docs/source/Plugin/P183.rst +++ b/docs/source/Plugin/P183.rst @@ -11,6 +11,8 @@ Plugin details Type: |P183_type| +Port Type: |P183_porttype| + Name: |P183_name| Status: |P183_status| @@ -53,6 +55,8 @@ Configuration Sensor ^^^^^^ +.. image:: P183_sensor_config.png + The available Serial protocol settings here depend on the build used. * **Serial Port**: The serial port to use for Modbus communication. @@ -72,10 +76,13 @@ The available Serial protocol settings here depend on the build used. Output Configuration ^^^^^^^^^^^^^^^^^^^^ +.. image:: P183_output_config.png + * **Number of registers to read**: The number of holding registers the plugin will read (1..4). * **Holding register for valueX**: The Modbus holding register address to read for valueX (X=1..4). +The holding register address is a 16-bit value (0..65535). This is the register address as specified with the Modbus device and is used directly in the Modbus read holding registers (function code 03). The plugin will read the number of values specified, starting with value1 Commands available ^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/Plugin/P183_commands.repl b/docs/source/Plugin/P183_commands.repl index e69de29bb2..8b8ffaf4ae 100644 --- a/docs/source/Plugin/P183_commands.repl +++ b/docs/source/Plugin/P183_commands.repl @@ -0,0 +1,28 @@ +.. csv-table:: + :header: "Command", "Extra information" + :widths: 20, 30 + + " + ``modbus,write,
,`` + + "," + Write a value into the specified holding register. + " + " + ``modbus,read,`` + + "," + Read the value from the specified holding register. + " + " + ``modbus,scan`` + + "," + Scans the modbus address range for responding units. It will do a read function for holding register 1. Note this will take some time. Ouput is in the logging. + " + " + ``modbus,dump,,`` + + "," + Dumps the holding registers in the given address range. + " \ No newline at end of file diff --git a/docs/source/Plugin/P183_output_config.png b/docs/source/Plugin/P183_output_config.png new file mode 100644 index 0000000000000000000000000000000000000000..6f4802c231a50567bddd1a2e04ddf8d34b27067b GIT binary patch literal 13698 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P183_sensor_config.png b/docs/source/Plugin/P183_sensor_config.png new file mode 100644 index 0000000000000000000000000000000000000000..898e98e227e6fcdf0e39368f2675fd0ac30b88f2 GIT binary patch literal 24343 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 P183_NR_OUTPUT_VALUES)) { + P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs + } + addFormNumericBox(F("Number of values to read"), P183_NR_OUTPUTS_LABEL, P183_NR_OUTPUTS); + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { - addFormNumericBox(concat(F("Value "), outputIndex + 1), P183_ADDRESS_LABEL(outputIndex), P183_ADDRESS(outputIndex)); + addFormNumericBox(concat(F("Holding Register for value"), outputIndex + 1), P183_ADDRESS_LABEL(outputIndex), P183_ADDRESS(outputIndex)); } break; } @@ -175,6 +184,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P183_FLAG_COLL_DETECT_LABEL))); # endif // ifdef ESP32 + P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { P183_ADDRESS(outputIndex) = getFormItemInt( P183_ADDRESS_LABEL(outputIndex)); @@ -239,22 +249,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_TEN_PER_SECOND: - { - - break; - } - - case PLUGIN_ONCE_A_SECOND: - { - - break; - } - case PLUGIN_READ: { uint16_t value = 0; - for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { P183_modbus_readRegister(P183_DEV_ID, P183_ADDRESS(outputIndex), &value); UserVar.setFloat(event->TaskIndex, outputIndex, value); @@ -281,12 +279,6 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) addLogMove(LOG_LEVEL_INFO, log); success = true; } - else if (equals(subcmd, F("scan"))) { - // Scan for Modbus devices - addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); - P183_scan_modbus(); - success = true; - } else if (equals(subcmd, F("read"))) { // Read a value from a Modbus register int address = parseString(string, 3).toInt(); @@ -312,6 +304,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_scan_module(P183_DEV_ID, start_address, end_address); success = true; } + else if (equals(subcmd, F("scan"))) { + // Scan for Modbus devices + addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); + P183_scan_modbus(); + success = true; + } else { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); } From 53ec9309d11cd477fec5994d42f5468f05e83913 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:51:59 +0200 Subject: [PATCH 05/48] P183 Minor rework --- src/_P183_modbus.ino | 55 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 88ee3855dc..42ebaf6b26 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -50,15 +50,22 @@ # define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) # define P183_FLAG_COLL_DETECT_LABEL "colldet" -# define P183_QUERY1_CONFIG_POS 3 - # define P183_DEPIN CONFIG_PIN3 # define P183_DEV_ID_DFLT 1 # define P183_BAUDRATE_DFLT 3 // 9600 baud +# define P183_MAX_BAUDRATE_SEL 8 + # include +// Modbus properties +# define P183_MAX_MODBUS_NODES 247 +# define P183_MODBUS_TIMEOUT 1000 // milliseconds +# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address +# define P183_MODBUS_FUNC_READ_HOLDING_REGISTERS 0x03 +# define P183_MODBUS_FUNC_WRITE_SINGLE_REGISTER 0x06 + // These pointers may be used among multiple instances of the same plugin, // as long as the same serial settings are used. ESPeasySerial *P183_ESPEasySerial = nullptr; @@ -127,15 +134,15 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS: { - if ((P183_DEV_ID == 0) || (P183_DEV_ID > 247) || (P183_BAUDRATE >= 6)) { + if ((P183_DEV_ID <= 0) || (P183_DEV_ID > P183_MAX_MODBUS_NODES) || (P183_BAUDRATE >= 6)) { // Load some defaults P183_DEV_ID = P183_DEV_ID_DFLT; P183_BAUDRATE = P183_BAUDRATE_DFLT; } { - String options_baudrate[6]; + String options_baudrate[P183_MAX_BAUDRATE_SEL]; - for (int i = 0; i < 6; ++i) { + for (int i = 0; i < P183_MAX_BAUDRATE_SEL; ++i) { options_baudrate[i] = P183_storageValueToBaudrate(i); } constexpr size_t optionCount = NR_ELEMENTS(options_baudrate); @@ -242,10 +249,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_EXIT: { P183_init = false; - delete P183_ESPEasySerial; P183_ESPEasySerial = nullptr; - + success = true; break; } @@ -257,6 +263,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_modbus_readRegister(P183_DEV_ID, P183_ADDRESS(outputIndex), &value); UserVar.setFloat(event->TaskIndex, outputIndex, value); } + success = true; break; } case PLUGIN_WRITE: @@ -322,22 +329,44 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) return success; } +// Convert stored baudrate setting (enumeration value) to actual baudrate value +// Returns the actual baudrate value. int P183_storageValueToBaudrate(uint8_t baudrate_setting) { int baudrate = 9600; - if (baudrate_setting < 6) { - baudrate = 1200 << baudrate_setting; + switch (baudrate_setting) + { + case 0: + baudrate = 1200; break; + case 1: + baudrate = 2400; break; + case 2: + baudrate = 4800; break; + case 3: + baudrate = 9600; break; + case 4: + baudrate = 19200; break; + case 5: + baudrate = 38400; break; + case 6: + baudrate = 57600; break; + case 7: + baudrate = 115200; break; + default: + baudrate = 9600; break; // Default value for fallback } return baudrate; } +// Read a single Modbus register from a device with given node ID +// On success, the read value is stored in *value and 0 is returned. int P183_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) { uint8_t buffer[8]; // Buffer for Modbus request uint8_t response[8]; // Buffer for Modbus response buffer[0] = node_id; - buffer[1] = 0x03; // Function code for reading holding registers + buffer[1] = P183_MODBUS_FUNC_READ_HOLDING_REGISTERS; buffer[2] = highByte(reg); // High byte of register address buffer[3] = lowByte(reg); // Low byte of register address buffer[4] = 0x00; // Number of registers to read (2 bytes) @@ -360,13 +389,15 @@ int P183_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) } } +// Write a single Modbus register to a device with given node ID +// On success, 0 is returned. int P183_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) { uint8_t buffer[8]; uint8_t response[8]; buffer[0] = node_id; - buffer[1] = 0x06; // Function code for reading holding registers + buffer[1] = P183_MODBUS_FUNC_WRITE_SINGLE_REGISTER; buffer[2] = highByte(reg); // High byte of register address buffer[3] = lowByte(reg); // Low byte of register address buffer[4] = highByte(value); // High byte of value to write @@ -408,7 +439,7 @@ int P183_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t P183_dump_buffer((uint8_t*)tx_buffer, tx_size); // Debug: Dump the transmit buffer content P183_ESPEasySerial->write((uint8_t*)tx_buffer, tx_size); unsigned long startTime = millis(); - while (P183_ESPEasySerial->available() < rx_size && (millis() - startTime) < 1000) { + while (P183_ESPEasySerial->available() < rx_size && (millis() - startTime) < P183_MODBUS_TIMEOUT) { delay(10); // Wait for response } if (P183_ESPEasySerial->available() >= rx_size) { From a7155446be0b99a152a5e342332e3555e7541b7b Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:06:46 +0200 Subject: [PATCH 06/48] Baseline before refacoring --- docs/source/Plugin/P183.rst | 7 +++ docs/source/Plugin/P183_commands.repl | 8 ++-- docs/source/Plugin/P183_config_values.repl | 10 +++++ docs/source/Plugin/_plugin_sets_overview.repl | 28 ++++++------ src/_P183_modbus.ino | 43 +++++++++++++------ 5 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 docs/source/Plugin/P183_config_values.repl diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst index 7b85e18b5a..e800df9b08 100644 --- a/docs/source/Plugin/P183.rst +++ b/docs/source/Plugin/P183.rst @@ -94,6 +94,13 @@ Commands available .. .. include:: P183_events.repl +Get Config Values +^^^^^^^^^^^^^^^^^ + +Get Config Values retrieves values or settings from the sensor or plugin, and can be used in Rules, Display plugins, Formula's etc. The square brackets **are** part of the variable. Replace ```` by the **Name** of the task. + +.. include:: P183_config_values.repl + Change log ---------- diff --git a/docs/source/Plugin/P183_commands.repl b/docs/source/Plugin/P183_commands.repl index 8b8ffaf4ae..b750376cc6 100644 --- a/docs/source/Plugin/P183_commands.repl +++ b/docs/source/Plugin/P183_commands.repl @@ -6,13 +6,13 @@ ``modbus,write,

,`` "," - Write a value into the specified holding register. + Write value ```` into the holding register ``
``. " " - ``modbus,read,`` + ``modbus,read,
`` "," - Read the value from the specified holding register. + Read the value from the holding register ``
``. Note that the value is read from the Modbus device but it will not be returned as part of the command. The value is printed in the logging. " " ``modbus,scan`` @@ -24,5 +24,5 @@ ``modbus,dump,,`` "," - Dumps the holding registers in the given address range. + Dumps the holding registers in the address range ```` until ````. " \ No newline at end of file diff --git a/docs/source/Plugin/P183_config_values.repl b/docs/source/Plugin/P183_config_values.repl new file mode 100644 index 0000000000..65f63d65ea --- /dev/null +++ b/docs/source/Plugin/P183_config_values.repl @@ -0,0 +1,10 @@ +.. csv-table:: + :header: "Config value", "Information" + :widths: 20, 30 + + " + | ``[#register,]`` + "," + | Returns the value of holding register ```` of the device. The value is directly read from the Modbus device. The holding register number ```` can be any valid register in the device. + " + diff --git a/docs/source/Plugin/_plugin_sets_overview.repl b/docs/source/Plugin/_plugin_sets_overview.repl index fb9b9f32d3..05c1fc0c1b 100644 --- a/docs/source/Plugin/_plugin_sets_overview.repl +++ b/docs/source/Plugin/_plugin_sets_overview.repl @@ -88,20 +88,26 @@ Build set: :yellow:`COLLECTION A` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -112,6 +118,7 @@ Build set: :yellow:`COLLECTION A` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -397,26 +404,20 @@ Build set: :yellow:`COLLECTION D` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" - ":ref:`P007_page`","P007" - ":ref:`P008_page`","P008" - ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" - ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" - ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" - ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -427,7 +428,6 @@ Build set: :yellow:`COLLECTION D` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" - ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -1068,9 +1068,6 @@ Build set: :yellow:`IR` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" - ":ref:`P007_page`","P007" - ":ref:`P008_page`","P008" - ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" @@ -1078,17 +1075,14 @@ Build set: :yellow:`IR` ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" ":ref:`P016_page`","P016" - ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" - ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" - ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -1100,7 +1094,6 @@ Build set: :yellow:`IR` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" - ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" @@ -1206,20 +1199,26 @@ Build set: :yellow:`NEOPIXEL` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -1230,6 +1229,7 @@ Build set: :yellow:`NEOPIXEL` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" ":ref:`P041_page`","P041" ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 42ebaf6b26..478738955a 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -108,6 +108,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_183)); strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_183)); strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_183)); + break; } case PLUGIN_GET_DEVICEGPIONAMES: @@ -225,7 +226,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_ESPEasySerial->begin(baudrate); #ifdef P183_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("P183: Init serial: RX pin "); log += CONFIG_PIN1; log += F(", TX pin "); @@ -238,7 +239,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); log += F(", RS485mode enabled: "); log += rs485Mode ? F("yes") : F("no"); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_DEBUG, log); } #endif // ifdef P183_DEBUG @@ -279,11 +280,13 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) int address = parseString(string, 3).toInt(); uint16_t value = parseString(string, 4).toInt(); P183_modbus_writeRegister(P183_DEV_ID, address, value); - String log = F("Modbus: write value "); - log += value; - log += F(" to address "); - log += address; - addLogMove(LOG_LEVEL_INFO, log); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Modbus: write value "); + log += value; + log += F(" to address "); + log += address; + addLogMove(LOG_LEVEL_INFO, log); + } success = true; } else if (equals(subcmd, F("read"))) { @@ -291,11 +294,13 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) int address = parseString(string, 3).toInt(); uint16_t value = 0; P183_modbus_readRegister(P183_DEV_ID, address, &value); - String log = F("Modbus: read value "); - log += value; - log += F(" from address "); - log += address; - addLogMove(LOG_LEVEL_INFO, log); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Modbus: read value "); + log += value; + log += F(" from address "); + log += address; + addLogMove(LOG_LEVEL_INFO, log); + } success = true; } else if (equals(subcmd, F("dump"))) { @@ -324,7 +329,17 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } break; } - + case PLUGIN_GET_CONFIG_VALUE: { + const String cmd = parseString(string, 1); + if (equals(cmd, F("register"))) { + int address = parseString(string, 2).toInt(); + uint16_t value = 0; + P183_modbus_readRegister(P183_DEV_ID, address, &value); + string = String(value); + success = true; + } + break; + } } return success; } @@ -474,7 +489,7 @@ uint16_t P183_calculateCRC(const uint8_t *array, uint8_t len) { // This function takes a pointer to a buffer and its length, and logs the content in hexadecimal format. void P183_dump_buffer(const uint8_t *buffer, size_t length) { #ifdef P183_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("Modbus: Dumping buffer: "); for (size_t i = 0; i < length; ++i) { log += String(buffer[i], HEX); From aa34ac38b075d64cf40cbbfb2777a5bffd4f5993 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:02:05 +0200 Subject: [PATCH 07/48] P183 Uncrustify --- src/_P183_modbus.ino | 246 ++++++++++++----------- src/src/CustomBuild/define_plugin_sets.h | 2 +- 2 files changed, 135 insertions(+), 113 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 478738955a..30fdec54f9 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -14,10 +14,10 @@ /** * Changelog: - * 2025-08-24 flasmark: Initial version + * 2025-08-24 flasmark: Initial version */ -# define P183_DEBUG // Switch on additional debug logging +# define P183_DEBUG // Switch on additional debug logging # define PLUGIN_183 # define PLUGIN_ID_183 183 # define PLUGIN_NAME_183 "[testing] Modbus RTU" @@ -36,15 +36,15 @@ // PCONFIG(5) is the Modbus register address for value 2 // PCONFIG(6) is the Modbus register address for value 3 // PCONFIG(7) is the Modbus register address for value 4 -// Use P183_ADDRESS(x) to access the PCONFIG value for value x +// Use P183_ADDRESS(x) to access the PCONFIG value for value x # define P183_DEV_ID PCONFIG(0) # define P183_DEV_ID_LABEL PCONFIG_LABEL(0) # define P183_BAUDRATE PCONFIG(1) # define P183_BAUDRATE_LABEL PCONFIG_LABEL(1) # define P183_NR_OUTPUTS PCONFIG(3) # define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) -# define P183_ADDRESS(x) PCONFIG(4 + x) -# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) +# define P183_ADDRESS(x) PCONFIG(4 + x) +# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) # define P183_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) # define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) @@ -68,11 +68,13 @@ // These pointers may be used among multiple instances of the same plugin, // as long as the same serial settings are used. -ESPeasySerial *P183_ESPEasySerial = nullptr; -boolean P183_init = false; +ESPeasySerial * P183_ESPEasySerial = nullptr; +boolean P183_init = false; void P183_scan_modbus(); -void P183_scan_module(uint8_t node_id, uint8_t start_reg = 0x00, uint8_t end_reg = 0xFF); +void P183_scan_module(uint8_t node_id, + uint8_t start_reg = 0x00, + uint8_t end_reg = 0xFF); boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { @@ -164,7 +166,6 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: { - if ((P183_NR_OUTPUTS < 1) || (P183_NR_OUTPUTS > P183_NR_OUTPUT_VALUES)) { P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs } @@ -172,7 +173,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { - addFormNumericBox(concat(F("Holding Register for value"), outputIndex + 1), P183_ADDRESS_LABEL(outputIndex), P183_ADDRESS(outputIndex)); + addFormNumericBox(concat(F("Holding Register for value"), outputIndex + 1), P183_ADDRESS_LABEL(outputIndex), + P183_ADDRESS(outputIndex)); } break; } @@ -185,21 +187,21 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); P183_BAUDRATE = getFormItemInt(P183_BAUDRATE_LABEL); # ifdef ESP32 P183_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P183_FLAG_COLL_DETECT_LABEL))); # endif // ifdef ESP32 - + P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { - P183_ADDRESS(outputIndex) = getFormItemInt( P183_ADDRESS_LABEL(outputIndex)); + P183_ADDRESS(outputIndex) = getFormItemInt(P183_ADDRESS_LABEL(outputIndex)); } P183_init = false; // Force device setup next time - success = true; + success = true; break; } @@ -221,11 +223,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) // Set RS485 mode if requested using selected pin for RTS bool rs485Mode = P183_ESPEasySerial->setRS485Mode(P183_DEPIN, P183_GET_FLAG_COLL_DETECT); - + unsigned int baudrate = P183_storageValueToBaudrate(P183_BAUDRATE); P183_ESPEasySerial->begin(baudrate); - #ifdef P183_DEBUG + # ifdef P183_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("P183: Init serial: RX pin "); log += CONFIG_PIN1; @@ -234,16 +237,16 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) log += F(", RS485 mode selected on pin "); log += P183_DEPIN; log += F(", baudrate "); - log += P183_storageValueToBaudrate(P183_BAUDRATE); + log += P183_storageValueToBaudrate(P183_BAUDRATE); log += F(", collision detection "); log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); log += F(", RS485mode enabled: "); log += rs485Mode ? F("yes") : F("no"); addLogMove(LOG_LEVEL_DEBUG, log); } - #endif // ifdef P183_DEBUG + # endif // ifdef P183_DEBUG - success = true; + success = true; break; } @@ -259,6 +262,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { uint16_t value = 0; + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { P183_modbus_readRegister(P183_DEV_ID, P183_ADDRESS(outputIndex), &value); @@ -277,9 +281,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (equals(subcmd, F("write"))) { // Write a value to a Modbus register - int address = parseString(string, 3).toInt(); + int address = parseString(string, 3).toInt(); uint16_t value = parseString(string, 4).toInt(); P183_modbus_writeRegister(P183_DEV_ID, address, value); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: write value "); log += value; @@ -288,12 +293,13 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) addLogMove(LOG_LEVEL_INFO, log); } success = true; - } + } else if (equals(subcmd, F("read"))) { // Read a value from a Modbus register - int address = parseString(string, 3).toInt(); + int address = parseString(string, 3).toInt(); uint16_t value = 0; P183_modbus_readRegister(P183_DEV_ID, address, &value); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: read value "); log += value; @@ -306,9 +312,11 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) else if (equals(subcmd, F("dump"))) { int start_address = parseString(string, 3).toInt(); int end_address = parseString(string, 4).toInt(); + if (end_address < start_address) { end_address = start_address; } + if (end_address - start_address > 100) { end_address = start_address + 100; // Limit to 100 addresses } @@ -321,7 +329,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); P183_scan_modbus(); success = true; - } + } else { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); } @@ -331,11 +339,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_GET_CONFIG_VALUE: { const String cmd = parseString(string, 1); + if (equals(cmd, F("register"))) { - int address = parseString(string, 2).toInt(); - uint16_t value = 0; + int address = parseString(string, 2).toInt(); + uint16_t value = 0; P183_modbus_readRegister(P183_DEV_ID, address, &value); - string = String(value); + string = String(value); success = true; } break; @@ -345,30 +354,30 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } // Convert stored baudrate setting (enumeration value) to actual baudrate value -// Returns the actual baudrate value. +// Returns the actual baudrate value. int P183_storageValueToBaudrate(uint8_t baudrate_setting) { int baudrate = 9600; switch (baudrate_setting) { - case 0: - baudrate = 1200; break; - case 1: - baudrate = 2400; break; - case 2: - baudrate = 4800; break; - case 3: - baudrate = 9600; break; - case 4: - baudrate = 19200; break; - case 5: - baudrate = 38400; break; - case 6: - baudrate = 57600; break; - case 7: - baudrate = 115200; break; - default: - baudrate = 9600; break; // Default value for fallback + case 0: + baudrate = 1200; break; + case 1: + baudrate = 2400; break; + case 2: + baudrate = 4800; break; + case 3: + baudrate = 9600; break; + case 4: + baudrate = 19200; break; + case 5: + baudrate = 38400; break; + case 6: + baudrate = 57600; break; + case 7: + baudrate = 115200; break; + default: + baudrate = 9600; break; // Default value for fallback } return baudrate; } @@ -377,30 +386,30 @@ int P183_storageValueToBaudrate(uint8_t baudrate_setting) { // On success, the read value is stored in *value and 0 is returned. int P183_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) { - uint8_t buffer[8]; // Buffer for Modbus request - uint8_t response[8]; // Buffer for Modbus response + uint8_t buffer[8]; // Buffer for Modbus request + uint8_t response[8]; // Buffer for Modbus response buffer[0] = node_id; buffer[1] = P183_MODBUS_FUNC_READ_HOLDING_REGISTERS; buffer[2] = highByte(reg); // High byte of register address - buffer[3] = lowByte(reg); // Low byte of register address - buffer[4] = 0x00; // Number of registers to read (2 bytes) - buffer[5] = 0x01; // Number of registers to read (2 bytes) - uint16_t crc = P183_calculateCRC((uint8_t*)buffer, 6); - buffer[6] = lowByte(crc); // CRC low byte + buffer[3] = lowByte(reg); // Low byte of register address + buffer[4] = 0x00; // Number of registers to read (2 bytes) + buffer[5] = 0x01; // Number of registers to read (2 bytes) + uint16_t crc = P183_calculateCRC((uint8_t *)buffer, 6); + buffer[6] = lowByte(crc); // CRC low byte buffer[7] = highByte(crc); // CRC high byte - + if (P183_modbus_exchange_message(buffer, response, 8, 7) < 0) { - return -1; // Failed to exchange message + return -1; // Failed to exchange message } - if (response[0] == node_id && response[1] == 0x03 && response[2] == 0x02) { + if ((response[0] == node_id) && (response[1] == 0x03) && (response[2] == 0x02)) { *value = (response[3] << 8) | response[4]; // Combine high and low byte addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: received value: ", *value)); - return 0; // Success + return 0; // Success } else { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); - return -2; // Invalid response + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); + return -2; // Invalid response } } @@ -417,69 +426,76 @@ int P183_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) buffer[3] = lowByte(reg); // Low byte of register address buffer[4] = highByte(value); // High byte of value to write buffer[5] = lowByte(value); // Low byte of value to write - uint16_t crc = P183_calculateCRC((uint8_t*)buffer, 6); - buffer[6] = lowByte(crc); // CRC low byte - buffer[7] = highByte(crc); // CRC high byte + uint16_t crc = P183_calculateCRC((uint8_t *)buffer, 6); + buffer[6] = lowByte(crc); // CRC low byte + buffer[7] = highByte(crc); // CRC high byte if (P183_modbus_exchange_message(buffer, response, 8, 7) < 0) { - return -1; // Failed to exchange message + return -1; // Failed to exchange message } - if (response[0] == node_id && response[1] == 0x06 && response[2] == highByte(reg) && response[3] == lowByte(reg)) { - uint16_t crc = P183_calculateCRC((uint8_t*)response, 6); - if (response[5] != lowByte(crc) || response[6] != highByte(crc)) { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid CRC in response")); - return -2; // Invalid response - } - addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: Success send value ", value)); - return 0; // Success + + if ((response[0] == node_id) && (response[1] == 0x06) && (response[2] == highByte(reg)) && (response[3] == lowByte(reg))) { + uint16_t crc = P183_calculateCRC((uint8_t *)response, 6); + + if ((response[5] != lowByte(crc)) || (response[6] != highByte(crc))) { + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid CRC in response")); + return -2; // Invalid response + } + addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: Success send value ", value)); + return 0; // Success } else { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); - return -2; // Invalid response - } + addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); + return -2; // Invalid response + } } // Exchange Modbus RTU messages. Send the tx_buffer and wait for a response in rx_buffer. int P183_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t tx_size, uint8_t rx_size) { - if (P183_ESPEasySerial == nullptr) { addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Serial not initialized")); return -1; // Not initialized } for (int i = P183_ESPEasySerial->available(); i > 0; --i) { - P183_ESPEasySerial->read(); // Clear any existing data in the buffer + P183_ESPEasySerial->read(); // Clear any existing data in the buffer } - P183_dump_buffer((uint8_t*)tx_buffer, tx_size); // Debug: Dump the transmit buffer content - P183_ESPEasySerial->write((uint8_t*)tx_buffer, tx_size); + P183_dump_buffer((uint8_t *)tx_buffer, tx_size); // Debug: Dump the transmit buffer content + P183_ESPEasySerial->write((uint8_t *)tx_buffer, tx_size); unsigned long startTime = millis(); + while (P183_ESPEasySerial->available() < rx_size && (millis() - startTime) < P183_MODBUS_TIMEOUT) { delay(10); // Wait for response } + if (P183_ESPEasySerial->available() >= rx_size) { - P183_ESPEasySerial->readBytes(rx_buffer, rx_size); - P183_dump_buffer((uint8_t*)rx_buffer, rx_size); // Debug: Dump the receive buffer content + P183_dump_buffer((uint8_t *)rx_buffer, rx_size); // Debug: Dump the receive buffer content return 0; } else { addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Timeout waiting for response")); return -3; // Timeout - } + } } // Calculate CRC-16 for Modbus RTU // This function calculates the CRC-16 checksum for a given array of bytes. uint16_t P183_calculateCRC(const uint8_t *array, uint8_t len) { uint16_t _crc, _flag; + _crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { _crc ^= (uint16_t)array[i]; + for (uint8_t j = 8; j; j--) { - _flag = _crc & 0x0001; + _flag = _crc & 0x0001; _crc >>= 1; - if (_flag) + + if (_flag) { _crc ^= 0xA001; + } } } return _crc; @@ -488,58 +504,64 @@ uint16_t P183_calculateCRC(const uint8_t *array, uint8_t len) { // Dump the content of a buffer to the log // This function takes a pointer to a buffer and its length, and logs the content in hexadecimal format. void P183_dump_buffer(const uint8_t *buffer, size_t length) { -#ifdef P183_DEBUG +# ifdef P183_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("Modbus: Dumping buffer: "); + for (size_t i = 0; i < length; ++i) { log += String(buffer[i], HEX); + if (i < length - 1) { log += F(", "); } } addLogMove(LOG_LEVEL_DEBUG, log); } -#endif // ifdef P183_DEBUG +# endif // ifdef P183_DEBUG } // Scan Modbus registers from 0x00 to 0xFF for a given node ID void P183_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { - String log; - uint16_t value = 0; - for (uint8_t reg = start_reg; reg <= end_reg; reg++) { - int result = P183_modbus_readRegister(node_id, reg, &value); - log += F("** Address "); - log += String(reg); - log += F(" (0x"); - log += String(reg, HEX); - - if (result == 0) { - log += F(") = "); - log += String(value); - } else { - log += F(") invalid"); - } - addLogMove(LOG_LEVEL_INFO, log); + String log; + uint16_t value = 0; + + for (uint8_t reg = start_reg; reg <= end_reg; reg++) { + int result = P183_modbus_readRegister(node_id, reg, &value); + log += F("** Address "); + log += String(reg); + log += F(" (0x"); + log += String(reg, HEX); + + if (result == 0) { + log += F(") = "); + log += String(value); + } else { + log += F(") invalid"); } + addLogMove(LOG_LEVEL_INFO, log); + } } // Scan Modbus addreses from 0x00 to 0xFF for a given node ID void P183_scan_modbus() { - String log; - uint16_t value = 0; - for (uint8_t id = 0; id <= 247; id++) { - int result = P183_modbus_readRegister(id, 1, &value); - log += F("** Address "); - log += String(id); - - if (result == 0) { - log += F(" OK"); + String log; + uint16_t value = 0; + + for (uint8_t id = 0; id <= 247; id++) { + int result = P183_modbus_readRegister(id, 1, &value); + log += F("** Address "); + log += String(id); + + if (result == 0) { + log += F(" OK"); } else { - log += F(" no response"); - } - addLogMove(LOG_LEVEL_INFO, log); + log += F(" no response"); } + addLogMove(LOG_LEVEL_INFO, log); + } } + #endif // USES_P183 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 9a6a2bc2f2..4419534214 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -2660,7 +2660,7 @@ To create/register a plugin, you have to : #endif #endif -#if defined(USES_P085) || defined (USES_P052) || defined(USES_P078) || defined(USES_P108) +#if defined(USES_P085) || defined (USES_P052) || defined(USES_P078) || defined(USES_P108) || defined(USES_P183) // FIXME TD-er: Is this correct? Those plugins use Modbus_RTU. #ifdef FEATURE_MODBUS #undef FEATURE_MODBUS From 11cf754a7df196b5ad823f86f66b5a7c0ed63201 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:38:07 +0200 Subject: [PATCH 08/48] Starting complete make over --- src/_P183_modbus.ino | 167 +++++--------------- src/src/Helpers/Modbus_device.cpp | 250 ++++++++++++++++++++++++++++++ src/src/Helpers/Modbus_device.h | 107 +++++++++++++ src/src/Helpers/Modbus_link.cpp | 210 +++++++++++++++++++++++++ src/src/Helpers/Modbus_link.h | 94 +++++++++++ src/src/Helpers/Modbus_mgr.cpp | 72 +++++++++ src/src/Helpers/Modbus_mgr.h | 50 ++++++ 7 files changed, 821 insertions(+), 129 deletions(-) create mode 100644 src/src/Helpers/Modbus_device.cpp create mode 100644 src/src/Helpers/Modbus_device.h create mode 100644 src/src/Helpers/Modbus_link.cpp create mode 100644 src/src/Helpers/Modbus_link.h create mode 100644 src/src/Helpers/Modbus_mgr.cpp create mode 100644 src/src/Helpers/Modbus_mgr.h diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 30fdec54f9..50599301fa 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -58,6 +58,8 @@ # define P183_MAX_BAUDRATE_SEL 8 # include +# include "src/Helpers/Modbus_device.h" +# include "src/Helpers/Modbus_mgr.h" // Modbus properties # define P183_MAX_MODBUS_NODES 247 @@ -68,8 +70,9 @@ // These pointers may be used among multiple instances of the same plugin, // as long as the same serial settings are used. -ESPeasySerial * P183_ESPEasySerial = nullptr; -boolean P183_init = false; +ModbusDEVICE_struct * P183_ModbusDevice = nullptr; +ModbusQueueState_t P183_ModbusStatus = ModbusQueueState_t::EMPTY; +boolean P183_init = false; void P183_scan_modbus(); void P183_scan_module(uint8_t node_id, @@ -211,21 +214,27 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) // (re)create the serial port object // If the serial port object already exists, delete it first. - if (P183_ESPEasySerial != nullptr) { - delete P183_ESPEasySerial; - P183_ESPEasySerial = nullptr; + if (P183_ModbusDevice != nullptr) { + delete P183_ModbusDevice; + P183_ModbusDevice = nullptr; } - P183_ESPEasySerial = new (std::nothrow) ESPeasySerial(static_cast(CONFIG_PORT), CONFIG_PIN1, CONFIG_PIN2); + P183_ModbusDevice = new ModbusDEVICE_struct(); - if (P183_ESPEasySerial == nullptr) { + if (P183_ModbusDevice == nullptr) { + P183_init = false; + addLogMove(LOG_LEVEL_ERROR, F("P183: Unable to allocate Modbus device object")); break; } - // Set RS485 mode if requested using selected pin for RTS - bool rs485Mode = P183_ESPEasySerial->setRS485Mode(P183_DEPIN, P183_GET_FLAG_COLL_DETECT); - - unsigned int baudrate = P183_storageValueToBaudrate(P183_BAUDRATE); - P183_ESPEasySerial->begin(baudrate); + if (!P183_ModbusDevice->init(P183_DEV_ID, static_cast(CONFIG_PORT), + CONFIG_PIN1, + CONFIG_PIN2, + P183_storageValueToBaudrate(P183_BAUDRATE), + P183_DEPIN, + P183_GET_FLAG_COLL_DETECT)) { + break; + } + P183_ModbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); # ifdef P183_DEBUG @@ -240,8 +249,6 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) log += P183_storageValueToBaudrate(P183_BAUDRATE); log += F(", collision detection "); log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); - log += F(", RS485mode enabled: "); - log += rs485Mode ? F("yes") : F("no"); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef P183_DEBUG @@ -253,9 +260,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_EXIT: { P183_init = false; - delete P183_ESPEasySerial; - P183_ESPEasySerial = nullptr; - success = true; + delete P183_ModbusDevice; + P183_ModbusDevice = nullptr; + success = true; break; } @@ -265,7 +272,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { - P183_modbus_readRegister(P183_DEV_ID, P183_ADDRESS(outputIndex), &value); + P183_modbus_readRegister(P183_ADDRESS(outputIndex), &value); UserVar.setFloat(event->TaskIndex, outputIndex, value); } success = true; @@ -273,7 +280,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_WRITE: { - if (P183_ESPEasySerial != nullptr) { + if (P183_ModbusDevice != nullptr) { const String cmd = parseString(string, 1); if (equals(cmd, F("modbus"))) { @@ -298,7 +305,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) // Read a value from a Modbus register int address = parseString(string, 3).toInt(); uint16_t value = 0; - P183_modbus_readRegister(P183_DEV_ID, address, &value); + P183_modbus_readRegister(address, &value); if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: read value "); @@ -343,7 +350,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (equals(cmd, F("register"))) { int address = parseString(string, 2).toInt(); uint16_t value = 0; - P183_modbus_readRegister(P183_DEV_ID, address, &value); + P183_modbus_readRegister(address, &value); string = String(value); success = true; } @@ -384,121 +391,22 @@ int P183_storageValueToBaudrate(uint8_t baudrate_setting) { // Read a single Modbus register from a device with given node ID // On success, the read value is stored in *value and 0 is returned. -int P183_modbus_readRegister(uint8_t node_id, uint16_t reg, uint16_t *value) +int P183_modbus_readRegister(uint16_t reg, uint16_t *value) { - uint8_t buffer[8]; // Buffer for Modbus request - uint8_t response[8]; // Buffer for Modbus response - - buffer[0] = node_id; - buffer[1] = P183_MODBUS_FUNC_READ_HOLDING_REGISTERS; - buffer[2] = highByte(reg); // High byte of register address - buffer[3] = lowByte(reg); // Low byte of register address - buffer[4] = 0x00; // Number of registers to read (2 bytes) - buffer[5] = 0x01; // Number of registers to read (2 bytes) - uint16_t crc = P183_calculateCRC((uint8_t *)buffer, 6); - buffer[6] = lowByte(crc); // CRC low byte - buffer[7] = highByte(crc); // CRC high byte - - if (P183_modbus_exchange_message(buffer, response, 8, 7) < 0) { - return -1; // Failed to exchange message - } - - if ((response[0] == node_id) && (response[1] == 0x03) && (response[2] == 0x02)) { - *value = (response[3] << 8) | response[4]; // Combine high and low byte - addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: received value: ", *value)); - return 0; // Success - } else { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); - return -2; // Invalid response + if (P183_ModbusDevice != nullptr) { + P183_ModbusDevice->readHoldingRegister(reg, value, &P183_ModbusStatus); } + return 0; } // Write a single Modbus register to a device with given node ID // On success, 0 is returned. int P183_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) { - uint8_t buffer[8]; - uint8_t response[8]; - - buffer[0] = node_id; - buffer[1] = P183_MODBUS_FUNC_WRITE_SINGLE_REGISTER; - buffer[2] = highByte(reg); // High byte of register address - buffer[3] = lowByte(reg); // Low byte of register address - buffer[4] = highByte(value); // High byte of value to write - buffer[5] = lowByte(value); // Low byte of value to write - uint16_t crc = P183_calculateCRC((uint8_t *)buffer, 6); - buffer[6] = lowByte(crc); // CRC low byte - buffer[7] = highByte(crc); // CRC high byte - - if (P183_modbus_exchange_message(buffer, response, 8, 7) < 0) { - return -1; // Failed to exchange message - } - - if ((response[0] == node_id) && (response[1] == 0x06) && (response[2] == highByte(reg)) && (response[3] == lowByte(reg))) { - uint16_t crc = P183_calculateCRC((uint8_t *)response, 6); - - if ((response[5] != lowByte(crc)) || (response[6] != highByte(crc))) { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid CRC in response")); - return -2; // Invalid response - } - addLogMove(LOG_LEVEL_DEBUG, concat("Modbus: Success send value ", value)); - return 0; // Success - } else { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Invalid response received")); - return -2; // Invalid response - } -} - -// Exchange Modbus RTU messages. Send the tx_buffer and wait for a response in rx_buffer. -int P183_modbus_exchange_message(uint8_t *tx_buffer, uint8_t *rx_buffer, uint8_t tx_size, uint8_t rx_size) -{ - if (P183_ESPEasySerial == nullptr) { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Serial not initialized")); - return -1; // Not initialized - } - - for (int i = P183_ESPEasySerial->available(); i > 0; --i) { - P183_ESPEasySerial->read(); // Clear any existing data in the buffer - } - - P183_dump_buffer((uint8_t *)tx_buffer, tx_size); // Debug: Dump the transmit buffer content - P183_ESPEasySerial->write((uint8_t *)tx_buffer, tx_size); - unsigned long startTime = millis(); - - while (P183_ESPEasySerial->available() < rx_size && (millis() - startTime) < P183_MODBUS_TIMEOUT) { - delay(10); // Wait for response - } - - if (P183_ESPEasySerial->available() >= rx_size) { - P183_ESPEasySerial->readBytes(rx_buffer, rx_size); - P183_dump_buffer((uint8_t *)rx_buffer, rx_size); // Debug: Dump the receive buffer content - return 0; - } else { - addLogMove(LOG_LEVEL_DEBUG, F("Modbus: Timeout waiting for response")); - return -3; // Timeout - } -} - -// Calculate CRC-16 for Modbus RTU -// This function calculates the CRC-16 checksum for a given array of bytes. -uint16_t P183_calculateCRC(const uint8_t *array, uint8_t len) { - uint16_t _crc, _flag; - - _crc = 0xFFFF; - - for (uint8_t i = 0; i < len; i++) { - _crc ^= (uint16_t)array[i]; - - for (uint8_t j = 8; j; j--) { - _flag = _crc & 0x0001; - _crc >>= 1; - - if (_flag) { - _crc ^= 0xA001; - } - } + if (P183_ModbusDevice != nullptr) { + P183_ModbusDevice->writeSingleRegister(reg, value, &P183_ModbusStatus); } - return _crc; + return 0; } // Dump the content of a buffer to the log @@ -528,7 +436,7 @@ void P183_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) uint16_t value = 0; for (uint8_t reg = start_reg; reg <= end_reg; reg++) { - int result = P183_modbus_readRegister(node_id, reg, &value); + int result = P183_modbus_readRegister(reg, &value); log += F("** Address "); log += String(reg); log += F(" (0x"); @@ -551,7 +459,8 @@ void P183_scan_modbus() uint16_t value = 0; for (uint8_t id = 0; id <= 247; id++) { - int result = P183_modbus_readRegister(id, 1, &value); + //TODO: how to scan the Modbus devices in teh new structure + int result = P183_modbus_readRegister(1, &value); log += F("** Address "); log += String(id); diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp new file mode 100644 index 0000000000..90df8d7064 --- /dev/null +++ b/src/src/Helpers/Modbus_device.cpp @@ -0,0 +1,250 @@ + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS + +# include "Modbus_device.h" +# include "modbus_link.h" +# include "modbus_mgr.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusDEVICE_struct::~ModbusDEVICE_struct() { + reset(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::reset() { + if (_modbus_link != nullptr) { + ModbusMGR_singleton.disconnect(_deviceID); + _modbus_link = nullptr; + } + _deviceID = 0; + _queueID = 0; + _sendframe_size = 0; + _recv_buf_used = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::init(uint8_t slaveAddress, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate) { + return init(slaveAddress, port, serial_rx, serial_tx, baudrate, -1); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::init(uint8_t slaveAddress, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect) { + // Request the Modbus manager to connect this device to a Modbus link with the given parameters. + bool success = ModbusMGR_singleton.connect(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect, &_modbus_link, &_deviceID); + + _modbus_address = slaveAddress; + + // TODO: further implementation needed + return success; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::isInitialized() const { + return (_modbus_link != nullptr) && (_modbus_link->isInitialized()); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::setModbusTimeout(uint16_t timeout) { + _timeout = timeout; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusDEVICE_struct::getModbusTimeout() const +{ + return _timeout; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modubus holding register. The result will be available later. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, + uint16_t *valuePtr, + ModbusQueueState_t *statePtr) { + buildFrame(_modbus_address, MODBUS_READ_HOLDING_REGISTERS, address, 1); + uint16_t crc = CalculateCRC(_sendframe, _sendframe_size); + _sendframe[_sendframe_size++] = lowByte(crc); // CRC low byte + _sendframe[_sendframe_size++] = highByte(crc); // CRC high byte + _queueID = queueFrame(); + *statePtr = ModbusQueueState_t::QUEUED; + _statePtr = statePtr; + _resultPtr = valuePtr; + _state = ModbusQueueState_t::QUEUED; + _messageType = ModbusMessageType::READ_HOLDING_REGISTERS; + + // Don't touch *valueptr here, it might contain a previous valid result. + return false; // TODO: implement +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, + uint16_t value, + ModbusQueueState_t *statePtr) +{ + buildFrame(_modbus_address, MODBUS_WRITE_SINGLE_REGISTER, address, 1); + _sendframe[4] = highByte(value); + _sendframe[5] = lowByte(value); + uint16_t crc = CalculateCRC(_sendframe, _sendframe_size); + _sendframe[_sendframe_size++] = lowByte(crc); // CRC low byte + _sendframe[_sendframe_size++] = highByte(crc); // CRC high byte + _queueID = queueFrame(); + *statePtr = ModbusQueueState_t::QUEUED; + _statePtr = statePtr; + _state = ModbusQueueState_t::QUEUED; + _messageType = ModbusMessageType::WRITE_SINGLE_REGISTER; + return false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::processCommand() { + if (_modbus_link != nullptr) { + _modbus_link->processCommand(); // Trigger processing of the command queue on the link + } + else { + _state = ModbusQueueState_t::ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback function called by the Modbus link when a response is received for a queued request. +// Note that the response might be an invalid response or a timeout +// The queueID identifies the request. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::linkCallback(uint16_t queueID) +{ + if (queueID != _queueID) { + return; // Not for us + } + bool response = _modbus_link->getResponse(_queueID, _recv_buf, _recv_buf_used); + + if (response) { + switch (_messageType) { + case ModbusMessageType::READ_HOLDING_REGISTERS: + { + if ((_recv_buf[0] == _modbus_address) && (_recv_buf[1] == MODBUS_READ_HOLDING_REGISTERS) && (_recv_buf[2] == 2)) { + uint16_t crc = CalculateCRC(_recv_buf, 5); + + if ((_recv_buf[5] == lowByte(crc)) && (_recv_buf[6] == highByte(crc))) { + // Valid response + if (_resultPtr != nullptr) { + *_resultPtr = (_recv_buf[3] << 8) | _recv_buf[4]; // Combine high and low byte + } + _state = ModbusQueueState_t::AVAILABLE; + } else { + // Invalid CRC + _state = ModbusQueueState_t::ERROR; + } + } else { + // Invalid response + _state = ModbusQueueState_t::ERROR; + } + return; + break; + } + case ModbusMessageType::WRITE_SINGLE_REGISTER: + { + if ((_recv_buf[0] == _modbus_address) && (_recv_buf[1] == MODBUS_READ_HOLDING_REGISTERS) && (_recv_buf[2] == 2)) { + uint16_t crc = CalculateCRC(_recv_buf, 5); + + if ((_recv_buf[5] == lowByte(crc)) && (_recv_buf[6] == highByte(crc))) { + // Valid response + _state = ModbusQueueState_t::AVAILABLE; + } + else { + // Invalid CRC + _state = ModbusQueueState_t::ERROR; + } + } else { + // Invalid response + _state = ModbusQueueState_t::ERROR; + } + break; + } + case ModbusMessageType::NONE: + { + // Should not happen + _state = ModbusQueueState_t::ERROR; + break; + } + + default: + { + // Unknown message type + _state = ModbusQueueState_t::ERROR; + break; + } + } + } + + // Update the state as seen by the client + if (_statePtr != nullptr) { + *_statePtr = _state; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Build a Modbus RTU frame filling in the standard fields. +// Note that thsi does not include the CRC. +// The frame is stored in the _sendframe buffer and the size is stored in _sendframe_size. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::buildFrame(uint8_t slaveAddress, + uint8_t functionCode, + uint16_t startAddress, + uint8_t byteCount) { + _sendframe[0] = slaveAddress; + _sendframe[1] = functionCode; + _sendframe[2] = highByte(startAddress); + _sendframe[3] = lowByte(startAddress); + _sendframe[4] = highByte(byteCount); + _sendframe[5] = lowByte(byteCount); + _sendframe_size = 6; // Size without the CRC +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Compute the Modbus RTU CRC +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusDEVICE_struct::CalculateCRC(uint8_t *buf, int len) { + uint16_t crc = 0xFFFF; + + for (int pos = 0; pos < len; pos++) { + crc ^= (uint16_t)buf[pos]; // XOR uint8_t into least sig. uint8_t of crc + + for (int i = 8; i != 0; i--) { // Loop over each bit + if ((crc & 0x0001) != 0) { // If the LSB is set + crc >>= 1; // Shift right and XOR 0xA001 + crc ^= 0xA001; + } else { // Else LSB is not set + crc >>= 1; // Just shift right + } + } + } + return crc; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Queue the assembled Modbus frame for transmission over the link using the ModbusLink object. +// The function returns the queue ID assigned to the request, or 0 if queuing failed +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusDEVICE_struct::queueFrame() { + if (_modbus_link == nullptr) { + return false; + } + _queueID = _modbus_link->queueRequest(this, _sendframe, _sendframe_size, MODBUS_RECEIVE_BUFFER, _timeout); + return _queueID; +} + +#endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h new file mode 100644 index 0000000000..4c5b356f2c --- /dev/null +++ b/src/src/Helpers/Modbus_device.h @@ -0,0 +1,107 @@ +#ifndef HELPERS_MODBUS_DEVICE_H +#define HELPERS_MODBUS_DEVICE_H + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS + +# include +# include "Modbus_link.h" + +# define MODBUS_TRANSMIT_BUFFER 12 + +typedef enum class ModbusQueueState { + EMPTY = 0, // The entry was not found in the queue + QUEUED = 1, // The entry is in the queue, but not yet processed + BUSY = 2, // The entry is being processed + AVAILABLE = 3, // The entry has been processed and the result is available + ERROR = 4 // An error occurred during processing +} ModbusQueueState_t; + +// ModbusDEVICE structure representing a MODBUS Device +// This is a single device that may share it's Modbus link with multiple other devices. +// It uses the ModbusLINKManager to find the ModbusLINK object that handles the data transport. +// It is the ModbusDEVICE that builds the Modbus request frames and parses the responses. +struct ModbusDEVICE_struct { +private: + + enum class ModbusMessageType { + NONE = 0, + READ_HOLDING_REGISTERS = 1, + WRITE_SINGLE_REGISTER = 2 + }; + +public: + + ModbusDEVICE_struct() = default; + + ~ModbusDEVICE_struct(); + + void reset(); + + bool init(uint8_t slaveAddress, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate); + + bool init(uint8_t slaveAddress, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect = false); + + bool isInitialized() const; + + + void setModbusTimeout(uint16_t timeout); + + uint16_t getModbusTimeout() const; + + + void processCommand(); + + void linkCallback(uint16_t queueID); + + // Start reading a Modubus holding register. The result will be available later. + // The function returns true if the request was queued. + // The state variable will signal the processing state of the request. + bool readHoldingRegister(uint16_t address, + uint16_t *valueptr, + ModbusQueueState_t *stateptr); + + bool writeSingleRegister(uint16_t address, + uint16_t value, + ModbusQueueState_t *stateptr); + +private: + + uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _queueID = 0; // ID from link identifying the last request + uint8_t _sendframe[MODBUS_TRANSMIT_BUFFER] = { 0 }; // STorage for the Modbus request frame + uint8_t _sendframe_size = 0; // Size of the actual Modbus request frame + uint8_t _recv_buf[MODBUS_RECEIVE_BUFFER] = { 0 }; // Storage for the Modbus response frame + uint8_t _recv_buf_used = 0; // Size of the expected Modbus response frame + ModbusQueueState_t _state = ModbusQueueState_t::EMPTY; + uint16_t *_resultPtr = nullptr; // Pointer to the variable to store the result in + ModbusQueueState_t *_statePtr = nullptr; // Pointer to the variable to store the state in + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + ModbusMessageType _messageType = ModbusMessageType::NONE; + + void buildFrame(uint8_t slaveAddress, // Modbus device slave address + uint8_t functionCode, // Modbus function code + uint16_t startAddress, // Starting address in the Modbus device to read from or write to + uint8_t byteCount); // Size of the message in the buffer + + uint16_t CalculateCRC(uint8_t *buf, + int len); + + uint16_t queueFrame(); +}; + +#endif // FEAURE_MODBUS +#endif // HELPERS_MODBUS_LINK_H diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp new file mode 100644 index 0000000000..0996071bfc --- /dev/null +++ b/src/src/Helpers/Modbus_link.cpp @@ -0,0 +1,210 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MODBUS link class +// This class implements a Modbus link over a serial connection. +// It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. +// It exepcts a Modbus device instance to construct and interpret the Modbus messages for the specific device. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS + +# include "Modbus_device.h" +# include "Modbus_link.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusLINK_struct::~ModbusLINK_struct() { + reset(); + + if (_easySerial != nullptr) { + delete _easySerial; + _easySerial = nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Reset the ModbusLINK structure +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusLINK_struct::reset() { +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialize the link with the given serial port and parameters +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::init(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate) { + return init(port, serial_rx, serial_tx, baudrate, -1); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialize the link with the given serial port and parameters, including a dere pin for RS485 +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::init(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect) { + // (re)create the serial port object + // If the serial port object already exists, delete it first. + if (ModbusLINK_struct::_easySerial != nullptr) { + delete ModbusLINK_struct::_easySerial; + ModbusLINK_struct::_easySerial = nullptr; + } + ModbusLINK_struct::_easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx); + + if (ModbusLINK_struct::_easySerial == nullptr) { + return false; + } + + // Set RS485 mode if requested using selected pin for RTS + bool rs485Mode = ModbusLINK_struct::_easySerial->setRS485Mode(dere_pin, collision_detect); + + ModbusLINK_struct::_easySerial->begin(baudrate); + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("P183: Init serial: RX pin "); + log += serial_rx; + log += F(", TX pin "); + log += serial_tx; + log += F(", RS485 mode selected on pin "); + log += dere_pin; + log += F(", baudrate "); + log += baudrate; + log += F(", collision detection "); + log += collision_detect ? F("enabled") : F("disabled"); + log += F(", RS485mode enabled: "); + log += rs485Mode ? F("yes") : F("no"); + addLogMove(LOG_LEVEL_DEBUG, log); + } + + return true; +} + +// Return the initialization status of the link +bool ModbusLINK_struct::isInitialized() const { + return _easySerial != nullptr; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Queue a Modbus request. The request is appended to the request and assigned a unique identifier. +// The client can use this identifier to retrieve the response later. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t ModbusLINK_struct::queueRequest(struct ModbusDEVICE_struct *device, + uint8_t *sendframe, + uint16_t sendframe_length, + uint16_t rcvframe_length, + uint16_t timeout) { + struct Modbus_RequestQueueElement *req = new Modbus_RequestQueueElement(_queueID, 0); + + req->_id = ++(_queueID); // Assign a unique ID to the request + req->_device = device; // The Modbus device making the request + + for (int i = 0; i < sendframe_length; ++i) { // Make a copy of the sendframe to persist it in teh queue element + req->_sendframe[i] = sendframe[i]; + } + req->_sendframe_length = sendframe_length; + req->_rcvframe_length = rcvframe_length; + req->_timeout = timeout; + req->_state = 0; // Initial state + + _requestQueue.push_back(*req); // Append the request to the queue + return req->_id; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Check if there is a response available for the given request ID and retrieve it if available +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::getResponse(uint16_t id, uint8_t *rcvframe, uint16_t rcvframe_length) { + for (auto it = _requestQueue.begin(); it != _requestQueue.end();) { + if (it->_id == id) { // Found the request with the matching ID + if (it->_state == 1) { // Response is ready + if (rcvframe_length >= it->_rcvframe_length) { + for (int i = 0; i < it->_rcvframe_length; ++i) { // Copy the response to the provided buffer + rcvframe[i] = it->_rcvframe[i]; + } + it = _requestQueue.erase(it); // Remove the request from the queue after retrieving the response + return true; // Provided buffer is too small + } else { + return false; + } + } else { // Response not ready yet + return false; + } + } else { + ++it; + } + } + return false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Remove a request from the queue based on its ID +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::removeRequest(uint16_t id) { + for (auto it = _requestQueue.begin(); it != _requestQueue.end();) { + if (it->_id == id) { + it = _requestQueue.erase(it); + } else { + ++it; + } + } + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Evaluate the next action to take to process the queue +// This function shall be called periodically to keep the Modbus link active +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint8_t ModbusLINK_struct::processCommand() +{ + if (_easySerial == nullptr) { + return 0; // Serial port not initialized + } + + for (auto it = _requestQueue.begin(); it != _requestQueue.end(); it++) { + if (it->_state == 0) { + // Send the request + if (_easySerial->available() > 0) { + // Clear any pending input + for (int i = _easySerial->available(); i > 0; --i) { + _easySerial->read(); + } + } + + _easySerial->write(it->_sendframe, it->_sendframe_length); + it->_state = 1; // Mark as sent, waiting for response + it->_deadline = millis() + it->_timeout; // Record the dealine value for the response + return 1; // Indicate that a request was sent + } + + if (it->_state == 1) { + // Waiting for response + if (_easySerial->available() >= it->_rcvframe_length) { + _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); + it->_state = 2; // Mark as response received + + if (it->_device != nullptr) { + it->_device->linkCallback(it->_id); // Notify the device that a response was received + } + } + else if (millis() > it->_deadline) { + // Timeout expired + it->_state = 3; // Mark as error + + if (it->_device != nullptr) { + it->_device->linkCallback(it->_id); // Notify the device that a response was received + return 2; + } + else { + return 0; // Still waiting + } + } + } + } + return 0; +} + +#endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h new file mode 100644 index 0000000000..f2e8c404cf --- /dev/null +++ b/src/src/Helpers/Modbus_link.h @@ -0,0 +1,94 @@ +#ifndef HELPERS_MODBUS_LINK_H +#define HELPERS_MODBUS_LINK_H + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS + +# include "../../_Plugin_Helper.h" +# include +# include "MODBUS_RTU.h" + +// Forward declaration of ModbusDEVICE_struct to avoid circular dependency issues +struct ModbusDEVICE_struct; + +// Modbus request queue element structure +// This structure represents a single Modbus request and its associated response. +struct Modbus_RequestQueueElement { + Modbus_RequestQueueElement(uint16_t id, uint8_t state) + : _id(id), + _state(state) + {} + + uint16_t _id = 0; // ID of the request + struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the action + uint8_t *_sendframe = { 0 }; // Reqest frame to send + uint8_t *_rcvframe = { 0 }; // Response frame received + uint16_t _sendframe_length = 0; // Length of the request frame + uint16_t _rcvframe_length = 0; // Expected length of the response frame expected + uint8_t _state = 0; // State of the request exchange + uint16_t _timeout = 0; // Specified timeout value for the request + unsigned long _deadline = 0; // Timeout deadline for the request +}; + +// Queue of Modbus request elements +typedef std::list Modbus_RequestQueue; + + +// ModbusLINK structure representing a MODBUS LINK +// This is a single serial link that can have multiple Modbus devices conected to it. +// It is used by the ModbusLINKManager to manage multiple links. +// Each ModbusLINK can have multiple ModbusDEVICE_struct instances representing the devices on the link. +// The ModbusLINK structure maintains a queue of Modbus requests and associated responses. +struct ModbusLINK_struct { + ModbusLINK_struct() = default; + + ~ModbusLINK_struct(); + + void reset(); + + bool init(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate); + + bool init(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect = false); + + bool isInitialized() const; + + uint16_t queueRequest( + struct ModbusDEVICE_struct *device, + uint8_t *sendframe, + uint16_t sendframe_length, + uint16_t rcvframe_length, + uint16_t timeout); + + bool getResponse(uint16_t id, + uint8_t *rcvframe, + uint16_t rcvframe_length); + + bool removeRequest(uint16_t id); + + uint8_t processCommand(); + +private: + + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object + int8_t _dere_pin = -1; // Pin to control DE/RE of RS485 transceiver + Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process + uint16_t _queueID = 0; // ID for the last request queued + uint32_t _reads_pass = 0; + uint32_t _reads_crc_failed = 0; + uint32_t _reads_nodata = 0; // This will be reset as soon as a valid packet has been received. + uint16_t _modbus_timeout = 180; + uint8_t _last_error = 0; +}; + + +#endif // FEATURE_MODBUS +#endif // HELPERS_MODBUS_LINK_H diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp new file mode 100644 index 0000000000..f2098fbb81 --- /dev/null +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -0,0 +1,72 @@ + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS + +# include +# include "Modbus_mgr.h" + + +// ModbusMGR structure representing the singleton Modbus Management entity +// Thw manager has an overview of all Modbus links and the conneted devices. +// The manager allows multiple Modbus devices to connect to a single Modbus link while supporting multiple links. +// The modbus manager is not involved in the actual data transport, this is handled by a direct relation between Modbus device and +// ModbusLINK object. + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct::~ModbusMGR_struct() +{ + reset(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusMGR_struct::reset() +{ + _modbus_link->reset(); + delete _modbus_link; + _modbus_link = nullptr; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::connect(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + ModbusLINK_struct **link, + uint8_t *deviceID) { + return connect(port, serial_rx, serial_tx, baudrate, -1, false, link, deviceID); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::connect(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect, + ModbusLINK_struct **link, + uint8_t *deviceID) { + if (_modbus_link == nullptr) { + _modbus_link = new (std::nothrow) ModbusLINK_struct(); + if (_modbus_link == nullptr) { + return false; // Memory allocation failed + } + if (!_modbus_link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + delete _modbus_link; + _modbus_link = nullptr; + return false; // Initialization failed + } + else { + *link = _modbus_link; + *deviceID = 1; // First device ID + } + } + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::disconnect(uint8_t deviceID) { + return false; // TODO: implement +} + +#endif // FEAURE_MODBUS diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h new file mode 100644 index 0000000000..3f04d860c5 --- /dev/null +++ b/src/src/Helpers/Modbus_mgr.h @@ -0,0 +1,50 @@ +#ifndef HELPERS_MODBUS_MGR_H +#define HELPERS_MODBUS_MGR_H + +#include "../../ESPEasy_common.h" + +#if FEATURE_MODBUS + +# include +# include "Modbus_link.h" + + +// ModbusMGR structure representing the singleton Modbus Management entity +// Thw manager has an overview of all Modbus links and the conneted devices. +// The manager allows multiple Modbus devices to connect to a single Modbus link while supporting multiple links. +// The modbus manager is not involved in the actual data transport, this is handled by a direct relation between Modbus device and +// ModbusLINK object. +struct ModbusMGR_struct { + ModbusMGR_struct() = default; + + ~ModbusMGR_struct(); + + void reset(); + + bool connect(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + ModbusLINK_struct **link, + uint8_t *deviceID); + + bool connect(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect, + ModbusLINK_struct **link, + uint8_t *deviceID); + + bool disconnect(uint8_t deviceID); + +private: + + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object +}; + +static struct ModbusMGR_struct ModbusMGR_singleton = {}; // Singleton instance of the Modbus Manager + +#endif // FEAURE_MODBUS +#endif // HELPERS_MODBUS_MGR_H From e7aabda45b0b699b1b42500dce87021c4765cedc Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:46:54 +0200 Subject: [PATCH 09/48] P183 Rework phase 1 --- src/_P183_modbus.ino | 32 ++-- src/src/Helpers/Modbus_device.cpp | 226 +++++++++++++-------------- src/src/Helpers/Modbus_device.h | 50 ++---- src/src/Helpers/Modbus_link.cpp | 244 ++++++++++++++++++++---------- src/src/Helpers/Modbus_link.h | 66 +++++--- src/src/Helpers/Modbus_mgr.cpp | 124 +++++++++++++-- src/src/Helpers/Modbus_mgr.h | 26 +++- 7 files changed, 483 insertions(+), 285 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 50599301fa..e917e028af 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -71,7 +71,7 @@ // These pointers may be used among multiple instances of the same plugin, // as long as the same serial settings are used. ModbusDEVICE_struct * P183_ModbusDevice = nullptr; -ModbusQueueState_t P183_ModbusStatus = ModbusQueueState_t::EMPTY; +ModbusQueueState_t P183_ModbusStatus = {}; boolean P183_init = false; void P183_scan_modbus(); @@ -211,6 +211,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_INIT: { P183_init = true; + addLogMove(LOG_LEVEL_INFO, "P183 INIT"); // (re)create the serial port object // If the serial port object already exists, delete it first. @@ -219,6 +220,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_ModbusDevice = nullptr; } P183_ModbusDevice = new ModbusDEVICE_struct(); + addLogMove(LOG_LEVEL_INFO, "P183 INIT AFTER NEW"); if (P183_ModbusDevice == nullptr) { P183_init = false; @@ -234,8 +236,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_GET_FLAG_COLL_DETECT)) { break; } + addLogMove(LOG_LEVEL_INFO, "P183 INIT AFTER INIT"); P183_ModbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); - + addLogMove(LOG_LEVEL_DEBUG, "AFTER TIMEOUT"); # ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { @@ -260,20 +263,23 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_EXIT: { P183_init = false; - delete P183_ModbusDevice; - P183_ModbusDevice = nullptr; - success = true; + + if (P183_ModbusDevice != nullptr) { + delete P183_ModbusDevice; + P183_ModbusDevice = nullptr; + } + success = true; break; } case PLUGIN_READ: { - uint16_t value = 0; - + static uint16_t registerValues[4] = {0, 0, 0, 0}; for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { - P183_modbus_readRegister(P183_ADDRESS(outputIndex), &value); - UserVar.setFloat(event->TaskIndex, outputIndex, value); + // TODO: Abuse PCONFIG_LONG static storage for now. + P183_modbus_readRegister(P183_ADDRESS(outputIndex), &(registerValues[outputIndex])); + UserVar.setFloat(event->TaskIndex, outputIndex, registerValues[outputIndex]); } success = true; break; @@ -356,6 +362,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } break; } + case PLUGIN_TEN_PER_SECOND: { + if (P183_init && (P183_ModbusDevice != nullptr)) { + P183_ModbusDevice->processCommand(); + } + break; + } } return success; } @@ -459,7 +471,7 @@ void P183_scan_modbus() uint16_t value = 0; for (uint8_t id = 0; id <= 247; id++) { - //TODO: how to scan the Modbus devices in teh new structure + // TODO: how to scan the Modbus devices in teh new structure int result = P183_modbus_readRegister(1, &value); log += F("** Address "); log += String(id); diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 90df8d7064..279567985b 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -16,13 +16,12 @@ ModbusDEVICE_struct::~ModbusDEVICE_struct() { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::reset() { if (_modbus_link != nullptr) { + _modbus_link->freeTransactions(this); ModbusMGR_singleton.disconnect(_deviceID); _modbus_link = nullptr; } - _deviceID = 0; - _queueID = 0; - _sendframe_size = 0; - _recv_buf_used = 0; + _deviceID = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; } @@ -48,6 +47,16 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, _modbus_address = slaveAddress; + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("---> ModbusDevice Init: Slave address = "); + log += slaveAddress; + log += F(", This = "); + log += (ulong)this; + log += F(", deviceID = "); + log += _deviceID; + addLogMove(LOG_LEVEL_INFO, log); + } + // TODO: further implementation needed return success; } @@ -74,17 +83,29 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t *valuePtr, - ModbusQueueState_t *statePtr) { - buildFrame(_modbus_address, MODBUS_READ_HOLDING_REGISTERS, address, 1); - uint16_t crc = CalculateCRC(_sendframe, _sendframe_size); - _sendframe[_sendframe_size++] = lowByte(crc); // CRC low byte - _sendframe[_sendframe_size++] = highByte(crc); // CRC high byte - _queueID = queueFrame(); - *statePtr = ModbusQueueState_t::QUEUED; - _statePtr = statePtr; - _resultPtr = valuePtr; - _state = ModbusQueueState_t::QUEUED; - _messageType = ModbusMessageType::READ_HOLDING_REGISTERS; + ModbusQueueState_t *statePtr) +{ + if (_modbus_link == nullptr) { + return false; + } + Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + + request->_messageType = ModbusMessageType::READ_HOLDING_REGISTERS; + request->_userData = valuePtr; + request->_sendframe[0] = _modbus_address; + request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; + request->_sendframe[2] = highByte(address); + request->_sendframe[3] = lowByte(address); + request->_sendframe[4] = 0; + request->_sendframe[5] = 1; // Read 1 register + uint16_t crc = CalculateCRC(request->_sendframe, 6); + request->_sendframe[6] = lowByte(crc); // CRC low byte + request->_sendframe[7] = highByte(crc); // CRC high byte + request->_sendframe_length = 8; // Size with CRC + request->_rcvframe_length = 7; // Expect 8 bytes in response + dump_buffer(request->_sendframe, request->_sendframe_length); + uint16_t queueID = _modbus_link->queueRequest(request); + *statePtr = ModbusQueueState_t::QUEUED; // Don't touch *valueptr here, it might contain a previous valid result. return false; // TODO: implement @@ -95,17 +116,22 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, ModbusQueueState_t *statePtr) { - buildFrame(_modbus_address, MODBUS_WRITE_SINGLE_REGISTER, address, 1); - _sendframe[4] = highByte(value); - _sendframe[5] = lowByte(value); - uint16_t crc = CalculateCRC(_sendframe, _sendframe_size); - _sendframe[_sendframe_size++] = lowByte(crc); // CRC low byte - _sendframe[_sendframe_size++] = highByte(crc); // CRC high byte - _queueID = queueFrame(); - *statePtr = ModbusQueueState_t::QUEUED; - _statePtr = statePtr; - _state = ModbusQueueState_t::QUEUED; - _messageType = ModbusMessageType::WRITE_SINGLE_REGISTER; + Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + + request->_sendframe[0] = _modbus_address; + request->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; + request->_sendframe[2] = highByte(address); + request->_sendframe[3] = lowByte(address); + request->_sendframe[4] = highByte(value); + request->_sendframe[5] = lowByte(value); + uint16_t crc = CalculateCRC(request->_sendframe, 6); + request->_sendframe[6] = lowByte(crc); // CRC low byte + request->_sendframe[7] = highByte(crc); // CRC high byte + request->_sendframe_length = 8; // Size with CRC + request->_rcvframe_length = 8; // Expect 8 bytes in response + uint16_t queueID = _modbus_link->queueRequest(request); + *statePtr = ModbusQueueState_t::QUEUED; + request->_messageType = ModbusMessageType::WRITE_SINGLE_REGISTER; return false; } @@ -114,9 +140,6 @@ void ModbusDEVICE_struct::processCommand() { if (_modbus_link != nullptr) { _modbus_link->processCommand(); // Trigger processing of the command queue on the link } - else { - _state = ModbusQueueState_t::ERROR; - } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -124,94 +147,67 @@ void ModbusDEVICE_struct::processCommand() { // Note that the response might be an invalid response or a timeout // The queueID identifies the request. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::linkCallback(uint16_t queueID) +void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) { - if (queueID != _queueID) { - return; // Not for us - } - bool response = _modbus_link->getResponse(_queueID, _recv_buf, _recv_buf_used); - - if (response) { - switch (_messageType) { - case ModbusMessageType::READ_HOLDING_REGISTERS: - { - if ((_recv_buf[0] == _modbus_address) && (_recv_buf[1] == MODBUS_READ_HOLDING_REGISTERS) && (_recv_buf[2] == 2)) { - uint16_t crc = CalculateCRC(_recv_buf, 5); - - if ((_recv_buf[5] == lowByte(crc)) && (_recv_buf[6] == highByte(crc))) { - // Valid response - if (_resultPtr != nullptr) { - *_resultPtr = (_recv_buf[3] << 8) | _recv_buf[4]; // Combine high and low byte - } - _state = ModbusQueueState_t::AVAILABLE; - } else { - // Invalid CRC - _state = ModbusQueueState_t::ERROR; + String log = F("---> Device callback: "); + + log += req->_id; + log += F(", Message = "); + log += (uint8_t)req->_messageType; + + + switch (req->_messageType) { + case ModbusMessageType::READ_HOLDING_REGISTERS: + { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { + uint16_t crc = CalculateCRC(req->_rcvframe, 5); + + if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + // Valid response + if (req->_userData != nullptr) { + *((uint16_t *)req->_userData) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte } } else { - // Invalid response - _state = ModbusQueueState_t::ERROR; + // Invalid CRC } - return; - break; + } else { + // Invalid response } - case ModbusMessageType::WRITE_SINGLE_REGISTER: - { - if ((_recv_buf[0] == _modbus_address) && (_recv_buf[1] == MODBUS_READ_HOLDING_REGISTERS) && (_recv_buf[2] == 2)) { - uint16_t crc = CalculateCRC(_recv_buf, 5); - - if ((_recv_buf[5] == lowByte(crc)) && (_recv_buf[6] == highByte(crc))) { - // Valid response - _state = ModbusQueueState_t::AVAILABLE; - } - else { - // Invalid CRC - _state = ModbusQueueState_t::ERROR; - } - } else { - // Invalid response - _state = ModbusQueueState_t::ERROR; + break; + } + + case ModbusMessageType::WRITE_SINGLE_REGISTER: + { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { + uint16_t crc = CalculateCRC(req->_rcvframe, 5); + + if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + // Valid response } - break; - } - case ModbusMessageType::NONE: - { - // Should not happen - _state = ModbusQueueState_t::ERROR; - break; + else { + // Invalid CRC + } + } else { + // Invalid response } + break; + } - default: - { - // Unknown message type - _state = ModbusQueueState_t::ERROR; - break; - } + case ModbusMessageType::NONE: + { + // Should not happen + break; } - } - // Update the state as seen by the client - if (_statePtr != nullptr) { - *_statePtr = _state; + default: + { + // Unknown message type + break; + } } -} -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Build a Modbus RTU frame filling in the standard fields. -// Note that thsi does not include the CRC. -// The frame is stored in the _sendframe buffer and the size is stored in _sendframe_size. -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::buildFrame(uint8_t slaveAddress, - uint8_t functionCode, - uint16_t startAddress, - uint8_t byteCount) { - _sendframe[0] = slaveAddress; - _sendframe[1] = functionCode; - _sendframe[2] = highByte(startAddress); - _sendframe[3] = lowByte(startAddress); - _sendframe[4] = highByte(byteCount); - _sendframe[5] = lowByte(byteCount); - _sendframe_size = 6; // Size without the CRC + _modbus_link->freeTransaction(req); + addLogMove(LOG_LEVEL_INFO, log); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -236,15 +232,19 @@ uint16_t ModbusDEVICE_struct::CalculateCRC(uint8_t *buf, int len) { } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Queue the assembled Modbus frame for transmission over the link using the ModbusLink object. -// The function returns the queue ID assigned to the request, or 0 if queuing failed -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint16_t ModbusDEVICE_struct::queueFrame() { - if (_modbus_link == nullptr) { - return false; +void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("---> Modbus: Dumping buffer: "); + + for (size_t i = 0; i < length; ++i) { + log += String(buffer[i], HEX); + + if (i < length - 1) { + log += F(", "); + } + } + addLogMove(LOG_LEVEL_INFO, log); } - _queueID = _modbus_link->queueRequest(this, _sendframe, _sendframe_size, MODBUS_RECEIVE_BUFFER, _timeout); - return _queueID; } #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 4c5b356f2c..5bc5b8d657 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -8,16 +8,6 @@ # include # include "Modbus_link.h" -# define MODBUS_TRANSMIT_BUFFER 12 - -typedef enum class ModbusQueueState { - EMPTY = 0, // The entry was not found in the queue - QUEUED = 1, // The entry is in the queue, but not yet processed - BUSY = 2, // The entry is being processed - AVAILABLE = 3, // The entry has been processed and the result is available - ERROR = 4 // An error occurred during processing -} ModbusQueueState_t; - // ModbusDEVICE structure representing a MODBUS Device // This is a single device that may share it's Modbus link with multiple other devices. // It uses the ModbusLINKManager to find the ModbusLINK object that handles the data transport. @@ -25,12 +15,6 @@ typedef enum class ModbusQueueState { struct ModbusDEVICE_struct { private: - enum class ModbusMessageType { - NONE = 0, - READ_HOLDING_REGISTERS = 1, - WRITE_SINGLE_REGISTER = 2 - }; - public: ModbusDEVICE_struct() = default; @@ -63,7 +47,7 @@ struct ModbusDEVICE_struct { void processCommand(); - void linkCallback(uint16_t queueID); + void linkCallback(Modbus_RequestQueueElement *transaction); // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. @@ -78,29 +62,15 @@ struct ModbusDEVICE_struct { private: - uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; - ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object - uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device - uint16_t _queueID = 0; // ID from link identifying the last request - uint8_t _sendframe[MODBUS_TRANSMIT_BUFFER] = { 0 }; // STorage for the Modbus request frame - uint8_t _sendframe_size = 0; // Size of the actual Modbus request frame - uint8_t _recv_buf[MODBUS_RECEIVE_BUFFER] = { 0 }; // Storage for the Modbus response frame - uint8_t _recv_buf_used = 0; // Size of the expected Modbus response frame - ModbusQueueState_t _state = ModbusQueueState_t::EMPTY; - uint16_t *_resultPtr = nullptr; // Pointer to the variable to store the result in - ModbusQueueState_t *_statePtr = nullptr; // Pointer to the variable to store the state in - uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests - ModbusMessageType _messageType = ModbusMessageType::NONE; - - void buildFrame(uint8_t slaveAddress, // Modbus device slave address - uint8_t functionCode, // Modbus function code - uint16_t startAddress, // Starting address in the Modbus device to read from or write to - uint8_t byteCount); // Size of the message in the buffer - - uint16_t CalculateCRC(uint8_t *buf, - int len); - - uint16_t queueFrame(); + uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + + static uint16_t CalculateCRC(uint8_t *buf, + int len); + static void dump_buffer(const uint8_t *buffer, + size_t length); }; #endif // FEAURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 0996071bfc..ffe0de7efd 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -89,122 +89,208 @@ bool ModbusLINK_struct::isInitialized() const { } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Queue a Modbus request. The request is appended to the request and assigned a unique identifier. -// The client can use this identifier to retrieve the response later. +// Provide a new transaction structure that can be used to build a Modbus request and queue it at this link /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint16_t ModbusLINK_struct::queueRequest(struct ModbusDEVICE_struct *device, - uint8_t *sendframe, - uint16_t sendframe_length, - uint16_t rcvframe_length, - uint16_t timeout) { - struct Modbus_RequestQueueElement *req = new Modbus_RequestQueueElement(_queueID, 0); - - req->_id = ++(_queueID); // Assign a unique ID to the request - req->_device = device; // The Modbus device making the request +Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVICE_struct *device) +{ + Modbus_RequestQueueElement *req = new Modbus_RequestQueueElement(0, ModbusQueueState::NOT_QUEUED); - for (int i = 0; i < sendframe_length; ++i) { // Make a copy of the sendframe to persist it in teh queue element - req->_sendframe[i] = sendframe[i]; + if (req != nullptr) { + req->_id = ++(_queueID); // Assign a unique ID to the transaction + req->_device = device; // The Modbus device making the request + req->_timeout = _modbus_timeout; // Default timeout value + req->_state = ModbusQueueState::NOT_QUEUED; // Initial state + return req; } - req->_sendframe_length = sendframe_length; - req->_rcvframe_length = rcvframe_length; - req->_timeout = timeout; - req->_state = 0; // Initial state + else { + return nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Free a previously allocated transaction structure +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) { + String log = F("---> Free: "); - _requestQueue.push_back(*req); // Append the request to the queue - return req->_id; + if (transaction != nullptr) { + transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark as freed + log += transaction->_id; + addLogMove(LOG_LEVEL_INFO, log); + return true; + } + else { + log += F("Attempt to free null transaction"); + addLogMove(LOG_LEVEL_INFO, log); + return false; + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Check if there is a response available for the given request ID and retrieve it if available +// Free all queued transactions for the given device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::getResponse(uint16_t id, uint8_t *rcvframe, uint16_t rcvframe_length) { - for (auto it = _requestQueue.begin(); it != _requestQueue.end();) { - if (it->_id == id) { // Found the request with the matching ID - if (it->_state == 1) { // Response is ready - if (rcvframe_length >= it->_rcvframe_length) { - for (int i = 0; i < it->_rcvframe_length; ++i) { // Copy the response to the provided buffer - rcvframe[i] = it->_rcvframe[i]; - } - it = _requestQueue.erase(it); // Remove the request from the queue after retrieving the response - return true; // Provided buffer is too small - } else { - return false; - } - } else { // Response not ready yet - return false; - } - } else { - ++it; +void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct * device) +{ + for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { + if (it->_device == device) { + it->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed } } - return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Remove a request from the queue based on its ID +// Queue a Modbus request. The request is appended to the request and assigned a unique identifier. +// The client can use this identifier to retrieve the response later. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::removeRequest(uint16_t id) { - for (auto it = _requestQueue.begin(); it != _requestQueue.end();) { - if (it->_id == id) { - it = _requestQueue.erase(it); - } else { - ++it; - } +uint16_t ModbusLINK_struct::queueRequest(Modbus_RequestQueueElement *transaction) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("---> Modbus queue request: request ID = "); + log += transaction->_id; + log += F(", State = "); + log += uint(transaction->_state); + addLogMove(LOG_LEVEL_INFO, log); } - return true; + transaction->_state = ModbusQueueState::QUEUED; // Initial state + _requestQueue.push_back(*transaction); // Append the request to the queue + processCommand(); // Trigger processing of the command queue + return transaction->_id; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Check if there is a response available for the given request ID and retrieve it if available +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusLINK_struct::getResponse(uint16_t id, Modbus_RequestQueueElement **transaction) { + return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Evaluate the next action to take to process the queue // This function shall be called periodically to keep the Modbus link active /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint8_t ModbusLINK_struct::processCommand() +void ModbusLINK_struct::processCommand() { + String log = F("---> Process queue: "); + if (_easySerial == nullptr) { - return 0; // Serial port not initialized + log += F("Serial port not initialized"); + addLogMove(LOG_LEVEL_INFO, log); + return; // Serial port not initialized } - for (auto it = _requestQueue.begin(); it != _requestQueue.end(); it++) { - if (it->_state == 0) { - // Send the request - if (_easySerial->available() > 0) { - // Clear any pending input - for (int i = _easySerial->available(); i > 0; --i) { - _easySerial->read(); + auto it = _requestQueue.begin(); // Iterator for the request queue + bool busy = false; // Only process one request at a time + + while ((it != _requestQueue.end()) && !busy) { + dumpQueueElement(&(*it)); + + switch (it->_state) { + case ModbusQueueState::QUEUED: + { + log += F(" state QUEUED, ID = "); + log += it->_id; + + // Send the request + if (_easySerial->available() > 0) { + // Clear any pending input + for (int i = _easySerial->available(); i > 0; --i) { + _easySerial->read(); + } } + + _easySerial->write(it->_sendframe, it->_sendframe_length); + it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response + it->_deadline = millis() + it->_timeout; // Record the dealine value for the response + log += F(" state QUEUED, ID = "); + log += it->_id; + busy = true; // Only process one request at a time + break; } - _easySerial->write(it->_sendframe, it->_sendframe_length); - it->_state = 1; // Mark as sent, waiting for response - it->_deadline = millis() + it->_timeout; // Record the dealine value for the response - return 1; // Indicate that a request was sent - } + case ModbusQueueState::MESSAGE_SENT: + { + log += F(" state MESSAGE_SENT, ID = "); + log += it->_id; - if (it->_state == 1) { - // Waiting for response - if (_easySerial->available() >= it->_rcvframe_length) { - _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); - it->_state = 2; // Mark as response received + // Waiting for response + if (_easySerial->available() >= it->_rcvframe_length) { + _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); + it->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received - if (it->_device != nullptr) { - it->_device->linkCallback(it->_id); // Notify the device that a response was received + if (it->_device != nullptr) { + it->_device->linkCallback(&(*it)); // Notify the device that a response was received + } } - } - else if (millis() > it->_deadline) { - // Timeout expired - it->_state = 3; // Mark as error + else if (millis() > it->_deadline) { + // Timeout expired + it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + log += F(" Timeout "); - if (it->_device != nullptr) { - it->_device->linkCallback(it->_id); // Notify the device that a response was received - return 2; + if (it->_device != nullptr) { + it->_device->linkCallback(&(*it)); // Notify the device that a response was received + } + else { + log += F(" Available="); + log += _easySerial->available(); + } + it++; } else { - return 0; // Still waiting + // Still waiting + busy = true; // Only process one request at a time } + break; } - } + + case ModbusQueueState::ERROR_OCCURRED: + { + log += F(" state ERROR_OCCURRED, ID = "); + log += it->_id; + + it++; + break; + } + + case ModbusQueueState::READY_FOR_DESTROY: + { + log += F(" state READY_FOR_DESTROY, ID = "); + log += it->_id; + + it = _requestQueue.erase(it); + break; + } + + default: + it++; + break; + } // switch + addLogMove(LOG_LEVEL_INFO, log); + } // next iterarion + + return; +} + +void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { + String log = F("[ ID="); + + log += el->_id; + log += F(", Device="); + log += String((uint32_t)(el->_device), HEX); + log += F(", State="); + log += (uint)el->_state; + log += F(", TX="); + + for (int i = 0; i < el->_sendframe_length; i++) { + log += String(el->_sendframe[i], HEX); + log += F(","); + } + log += F(", RX="); + + for (int i = 0; i < el->_rcvframe_length; i++) { + log += String(el->_rcvframe[i], HEX); + log += F(","); } - return 0; + log += F("] "); + addLogMove(LOG_LEVEL_INFO, log); } #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index f2e8c404cf..faa0a84856 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -9,26 +9,48 @@ # include # include "MODBUS_RTU.h" +# define MODBUS_XMIT_BUFFER 12 +# define MODBUS_RCV_BUFFER 256 + // Forward declaration of ModbusDEVICE_struct to avoid circular dependency issues struct ModbusDEVICE_struct; +typedef enum class ModbusQueueState { + NOT_QUEUED = 0, + QUEUED = 1, + MESSAGE_SENT = 2, + RESPONSE_RECEIVED = 3, + ERROR_OCCURRED = 4, + READY_FOR_DESTROY = 5 +} ModbusQueueState_t; + +enum class ModbusMessageType { + NONE = 0, + READ_HOLDING_REGISTERS = 1, + WRITE_SINGLE_REGISTER = 2 +}; + // Modbus request queue element structure // This structure represents a single Modbus request and its associated response. struct Modbus_RequestQueueElement { - Modbus_RequestQueueElement(uint16_t id, uint8_t state) + Modbus_RequestQueueElement(uint16_t id, ModbusQueueState state) : _id(id), _state(state) {} - uint16_t _id = 0; // ID of the request - struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the action - uint8_t *_sendframe = { 0 }; // Reqest frame to send - uint8_t *_rcvframe = { 0 }; // Response frame received - uint16_t _sendframe_length = 0; // Length of the request frame - uint16_t _rcvframe_length = 0; // Expected length of the response frame expected - uint8_t _state = 0; // State of the request exchange - uint16_t _timeout = 0; // Specified timeout value for the request - unsigned long _deadline = 0; // Timeout deadline for the request + ModbusMessageType _messageType = ModbusMessageType::NONE; // Type of Modbus message + void *_userData = nullptr; // Pointer to user data + uint16_t _id = 0; // ID of the request + struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the + // action + uint16_t _sendframe_length = 0; // Length of the request frame + uint16_t _rcvframe_length = 0; // Expected length of the response frame + // expected + enum ModbusQueueState _state = ModbusQueueState::NOT_QUEUED; // State of the request exchange + uint16_t _timeout = 0; // Specified timeout value for the request + unsigned long _deadline = 0; // Timeout deadline for the request + uint8_t _sendframe[MODBUS_XMIT_BUFFER] = { 0 }; // Reqest frame to send + uint8_t _rcvframe[MODBUS_RCV_BUFFER] = { 0 }; // Response frame received }; // Queue of Modbus request elements @@ -59,25 +81,21 @@ struct ModbusLINK_struct { int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const; + bool isInitialized() const; - uint16_t queueRequest( - struct ModbusDEVICE_struct *device, - uint8_t *sendframe, - uint16_t sendframe_length, - uint16_t rcvframe_length, - uint16_t timeout); + Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); + bool freeTransaction(Modbus_RequestQueueElement *transaction); + void freeTransactions(struct ModbusDEVICE_struct *device); + uint16_t queueRequest(Modbus_RequestQueueElement *transaction); + bool getResponse(uint16_t id, + Modbus_RequestQueueElement **transaction); - bool getResponse(uint16_t id, - uint8_t *rcvframe, - uint16_t rcvframe_length); - - bool removeRequest(uint16_t id); - - uint8_t processCommand(); + void processCommand(); private: + static void dumpQueueElement(Modbus_RequestQueueElement *el); + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object int8_t _dere_pin = -1; // Pin to control DE/RE of RS485 transceiver Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index f2098fbb81..da7265ba33 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -16,15 +16,15 @@ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct::~ModbusMGR_struct() { - reset(); + reset(); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::reset() { - _modbus_link->reset(); - delete _modbus_link; - _modbus_link = nullptr; + //////_modbus_link->; + //////delete _modbus_link; + /////_modbus_link = nullptr; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -46,26 +46,118 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, bool collision_detect, ModbusLINK_struct **link, uint8_t *deviceID) { - if (_modbus_link == nullptr) { - _modbus_link = new (std::nothrow) ModbusLINK_struct(); - if (_modbus_link == nullptr) { - return false; // Memory allocation failed + ModbusLinkInfo_struct *linkInfoPtr = nullptr; + ModbusDeviceInfo_struct *deviceInfoPtr = nullptr; + + String log = F("-MGR-> Connect: port="); + + log += (int)port; + + // Check if link is already used by another device + for (int i = 0; i < 5; i++) { + if ((_modbus_links[i] != nullptr) && (_modbus_links[i]->port == port)) { + // Found existing link with matching port identifier + linkInfoPtr = _modbus_links[i]; + log += F(" Found existing link= "); + log += i; + } + } + + if (linkInfoPtr == nullptr) { + linkInfoPtr = new (std::nothrow) ModbusLinkInfo_struct(); + + for (int i = 0; i < 5; i++) { + if (_modbus_links[i] == nullptr) { + _modbus_links[i] = linkInfoPtr; + log += F(" Created new link= "); + log += i; + break; + } } - if (!_modbus_link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { - delete _modbus_link; - _modbus_link = nullptr; - return false; // Initialization failed + } + + if (linkInfoPtr != nullptr) { + if (linkInfoPtr->link == nullptr) { + // No existing link, create a new one + linkInfoPtr->link = new (std::nothrow) ModbusLINK_struct(); + + if (linkInfoPtr->link != nullptr) { + // Initialize the new link + if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + delete linkInfoPtr->link; + linkInfoPtr->link = nullptr; + delete linkInfoPtr; + linkInfoPtr = nullptr; + return false; // Initialization failed + } + else { + // Store the link parameters + linkInfoPtr->port = port; + linkInfoPtr->serial_rx = serial_rx; + linkInfoPtr->serial_tx = serial_tx; + linkInfoPtr->baudrate = baudrate; + linkInfoPtr->dere_pin = dere_pin; + linkInfoPtr->rs485_mode = (dere_pin != -1); + linkInfoPtr->collision_detect = collision_detect; + } + } } - else { - *link = _modbus_link; - *deviceID = 1; // First device ID + } + + for (int i = 0; i < 16; i++) { + if (_modbus_devices[i] == nullptr) { + // Found an available device slot + _modbus_devices[i] = new (std::nothrow) ModbusDeviceInfo_struct(); + _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-16) + _modbus_devices[i]->link = linkInfoPtr; + *deviceID = _modbus_devices[i]->deviceID; + *link = linkInfoPtr->link; + log += F(" Assigned deviceID= "); + log += *deviceID; + break; } } - return true; + addLogMove(LOG_LEVEL_INFO, log); + return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::disconnect(uint8_t deviceID) { + for (int i = 0; i < 16; i++) { + if ((_modbus_devices[i] != nullptr) && (_modbus_devices[i]->deviceID == deviceID)) { + // Found the device to disconnect + ModbusLinkInfo_struct *linkInfoPtr = _modbus_devices[i]->link; + + // Remove the device entry + delete _modbus_devices[i]; + _modbus_devices[i] = nullptr; + + // Check if any other devices are using the same link + bool linkInUse = false; + + for (int j = 0; j < 16; j++) { + if ((_modbus_devices[j] != nullptr) && (_modbus_devices[j]->link == linkInfoPtr)) { + linkInUse = true; + break; + } + } + + if (!linkInUse) { + // No other devices are using this link, so we can delete it + for (int k = 0; k < 5; k++) { + if (_modbus_links[k] == linkInfoPtr) { + delete _modbus_links[k]->link; + delete _modbus_links[k]; + _modbus_links[k] = nullptr; + break; + } + } + } + + return true; // Successfully disconnected + } + } + return false; // TODO: implement } diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 3f04d860c5..2e4cf4e0db 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -25,7 +25,7 @@ struct ModbusMGR_struct { const int16_t serial_rx, const int16_t serial_tx, int16_t baudrate, - ModbusLINK_struct **link, + ModbusLINK_struct **link, uint8_t *deviceID); bool connect(const ESPEasySerialPort port, @@ -34,14 +34,34 @@ struct ModbusMGR_struct { int16_t baudrate, int8_t dere_pin, bool collision_detect, - ModbusLINK_struct **link, + ModbusLINK_struct **link, uint8_t *deviceID); bool disconnect(uint8_t deviceID); private: - ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + struct ModbusLinkInfo_struct { + ESPEasySerialPort port = ESPEasySerialPort::not_set; + int16_t serial_rx = -1; + int16_t serial_tx = -1; + int16_t baudrate = 9600; + int8_t dere_pin = -1; // Pin used for RS485 DE/RE control, -1 if not used + bool rs485_mode = false; // True if RS485 mode is enabled + bool collision_detect = false; // True if collision detection is enabled + struct ModbusLINK_struct *link = nullptr; // Pointer to the Modbus link object + }; + + struct ModbusDeviceInfo_struct { + uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager + struct ModbusDEVICE_struct *device = nullptr; // Pointer to the Modbus device object + struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info + }; + + ModbusLinkInfo_struct *_modbus_links[5] = {nullptr}; // Pointer to the Modbus link object + ModbusDeviceInfo_struct *_modbus_devices[16] = {nullptr}; // Array of connected Modbus devices + + ModbusLINK_struct *_modbus_link = nullptr; // Legacy, to be cleaned up }; static struct ModbusMGR_struct ModbusMGR_singleton = {}; // Singleton instance of the Modbus Manager From 18e8b865e0eaa0f4016cca9b3426749829c766ed Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:19:18 +0200 Subject: [PATCH 10/48] P183 Small refactoring --- src/_P183_modbus.ino | 7 ++++-- src/src/Helpers/Modbus_device.h | 10 ++++---- src/src/Helpers/Modbus_link.cpp | 11 +++++---- src/src/Helpers/Modbus_link.h | 41 +++++++++++++++++---------------- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index e917e028af..c93cbede2e 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -5,7 +5,8 @@ // ####################################################################################################### // ############## Plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### - +// TODO: Refactor for a better Modbus implementation using the modbus_device for all functions. +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /* Plugin written by: Flashmark @@ -219,7 +220,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) delete P183_ModbusDevice; P183_ModbusDevice = nullptr; } - P183_ModbusDevice = new ModbusDEVICE_struct(); + P183_ModbusDevice = new (std::nothrow) ModbusDEVICE_struct(); addLogMove(LOG_LEVEL_INFO, "P183 INIT AFTER NEW"); if (P183_ModbusDevice == nullptr) { @@ -274,6 +275,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { + // TODO: This is a very ugly way to reserve static memory for the return values of the Modbus read function. + // This does not work if multiple instances of this plugin are used. static uint16_t registerValues[4] = {0, 0, 0, 0}; for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 5bc5b8d657..6748313fb1 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -39,12 +39,10 @@ struct ModbusDEVICE_struct { bool isInitialized() const; - void setModbusTimeout(uint16_t timeout); uint16_t getModbusTimeout() const; - void processCommand(); void linkCallback(Modbus_RequestQueueElement *transaction); @@ -62,10 +60,10 @@ struct ModbusDEVICE_struct { private: - uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; - ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object - uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device - uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests static uint16_t CalculateCRC(uint8_t *buf, int len); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index ffe0de7efd..f566990391 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -93,7 +93,7 @@ bool ModbusLINK_struct::isInitialized() const { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVICE_struct *device) { - Modbus_RequestQueueElement *req = new Modbus_RequestQueueElement(0, ModbusQueueState::NOT_QUEUED); + Modbus_RequestQueueElement *req = new (std::nothrow) Modbus_RequestQueueElement(); if (req != nullptr) { req->_id = ++(_queueID); // Assign a unique ID to the transaction @@ -190,16 +190,17 @@ void ModbusLINK_struct::processCommand() log += it->_id; // Send the request - if (_easySerial->available() > 0) { + int available = _easySerial->available(); + if (available > 0) { // Clear any pending input - for (int i = _easySerial->available(); i > 0; --i) { + for (int i = available; i > 0; --i) { _easySerial->read(); } } _easySerial->write(it->_sendframe, it->_sendframe_length); it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response - it->_deadline = millis() + it->_timeout; // Record the dealine value for the response + it->_startTime = millis(); // Record the time the transaction log += F(" state QUEUED, ID = "); log += it->_id; busy = true; // Only process one request at a time @@ -220,7 +221,7 @@ void ModbusLINK_struct::processCommand() it->_device->linkCallback(&(*it)); // Notify the device that a response was received } } - else if (millis() > it->_deadline) { + else if (timePassedSince(it->_startTime) > it->_timeout) { // Timeout expired it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error log += F(" Timeout "); diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index faa0a84856..b3c79d19a2 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -15,28 +15,29 @@ // Forward declaration of ModbusDEVICE_struct to avoid circular dependency issues struct ModbusDEVICE_struct; +// States for the Modbus queue elements typedef enum class ModbusQueueState { - NOT_QUEUED = 0, - QUEUED = 1, - MESSAGE_SENT = 2, - RESPONSE_RECEIVED = 3, - ERROR_OCCURRED = 4, - READY_FOR_DESTROY = 5 + NOT_QUEUED = 0, // Initial state, element is created but not yet queued + QUEUED = 1, // Element is queued and waiting to be processed + MESSAGE_SENT = 2, // Request message has been sent, waiting for response + RESPONSE_RECEIVED = 3, // Response has been received and is being processed + ERROR_OCCURRED = 4, // An error occurred during processing (e.g., timeout, invalid response) + READY_FOR_DESTROY = 5 // Element is marked for deletion and can be freed } ModbusQueueState_t; +// Types of Modbus transactions supported by the Modbuss_device +// This enumeration is used by the Modbus device to indicate which transaction is associated with the queue element. +// See Modbus specification for details on function codes. enum class ModbusMessageType { - NONE = 0, - READ_HOLDING_REGISTERS = 1, - WRITE_SINGLE_REGISTER = 2 + NONE = 0, // Undefined/unknown transaction type + READ_HOLDING_REGISTERS = 1, // Read holding registers (function code 0x03) + WRITE_SINGLE_REGISTER = 2 // Write single register (function code 0x06) }; // Modbus request queue element structure // This structure represents a single Modbus request and its associated response. struct Modbus_RequestQueueElement { - Modbus_RequestQueueElement(uint16_t id, ModbusQueueState state) - : _id(id), - _state(state) - {} + Modbus_RequestQueueElement() = default; ModbusMessageType _messageType = ModbusMessageType::NONE; // Type of Modbus message void *_userData = nullptr; // Pointer to user data @@ -48,7 +49,7 @@ struct Modbus_RequestQueueElement { // expected enum ModbusQueueState _state = ModbusQueueState::NOT_QUEUED; // State of the request exchange uint16_t _timeout = 0; // Specified timeout value for the request - unsigned long _deadline = 0; // Timeout deadline for the request + unsigned long _startTime = 0; // Time the request was issued uint8_t _sendframe[MODBUS_XMIT_BUFFER] = { 0 }; // Reqest frame to send uint8_t _rcvframe[MODBUS_RCV_BUFFER] = { 0 }; // Response frame received }; @@ -97,14 +98,14 @@ struct ModbusLINK_struct { static void dumpQueueElement(Modbus_RequestQueueElement *el); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object - int8_t _dere_pin = -1; // Pin to control DE/RE of RS485 transceiver Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process uint16_t _queueID = 0; // ID for the last request queued - uint32_t _reads_pass = 0; - uint32_t _reads_crc_failed = 0; - uint32_t _reads_nodata = 0; // This will be reset as soon as a valid packet has been received. - uint16_t _modbus_timeout = 180; - uint8_t _last_error = 0; + uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds + uint32_t _reads_pass = 0; // TODO: statistics + uint32_t _reads_crc_failed = 0; // TODO: statistics + uint32_t _reads_nodata = 0; // TODO: statistics + + uint8_t _last_error = 0; }; From bd7661e349e909d15efeaa12a9677af2bd6d0adb Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:45:15 +0200 Subject: [PATCH 11/48] P183 Snapshot refactoring --- src/_P183_modbus.ino | 217 +++++---------------- src/src/CustomBuild/define_plugin_sets.h | 21 +- src/src/Helpers/Modbus_device.cpp | 32 +-- src/src/Helpers/Modbus_device.h | 28 ++- src/src/Helpers/Modbus_link.cpp | 75 ++++--- src/src/Helpers/Modbus_link.h | 11 +- src/src/Helpers/Modbus_mgr.cpp | 2 +- src/src/Helpers/Modbus_mgr.h | 2 +- src/src/PluginStructs/P183_data_struct.cpp | 192 ++++++++++++++++++ src/src/PluginStructs/P183_data_struct.h | 48 +++++ 10 files changed, 406 insertions(+), 222 deletions(-) create mode 100644 src/src/PluginStructs/P183_data_struct.cpp create mode 100644 src/src/PluginStructs/P183_data_struct.h diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index c93cbede2e..b729aacc1e 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -1,5 +1,4 @@ #include "_Plugin_Helper.h" - #ifdef USES_P183 // ####################################################################################################### @@ -7,18 +6,27 @@ // ####################################################################################################### // TODO: Refactor for a better Modbus implementation using the modbus_device for all functions. // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + /* Plugin written by: Flashmark - This plugin reads values from a Modbus RTU device. + This plugin reads values from a generic Modbus RTU device. It sees the device as a series of registers. + Up to 4 registers can be monitored and presented as standard output values of the plugin. + The plugin also provides means to write register using the PLUGIN_WRITE commands. + For debugging the Modbus and accessing other registers additional commands are available. + This plugin uses a generic MODBUS_FAC facility to share a single Modbus link with multiple device instances. */ /** * Changelog: - * 2025-08-24 flasmark: Initial version + * 2025-08-24 flashmark: Initial version + * 2025-10-12 flashmark: Restructuring and adding a MODBUS_FAC facility */ # define P183_DEBUG // Switch on additional debug logging +# ifdef BUILD_NO_DEBUG +# undef P183_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG # define PLUGIN_183 # define PLUGIN_ID_183 183 # define PLUGIN_NAME_183 "[testing] Modbus RTU" @@ -59,12 +67,13 @@ # define P183_MAX_BAUDRATE_SEL 8 # include +# include "src/PluginStructs/P183_data_struct.h" # include "src/Helpers/Modbus_device.h" # include "src/Helpers/Modbus_mgr.h" // Modbus properties # define P183_MAX_MODBUS_NODES 247 -# define P183_MODBUS_TIMEOUT 1000 // milliseconds + # define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address # define P183_MODBUS_FUNC_READ_HOLDING_REGISTERS 0x03 # define P183_MODBUS_FUNC_WRITE_SINGLE_REGISTER 0x06 @@ -72,13 +81,8 @@ // These pointers may be used among multiple instances of the same plugin, // as long as the same serial settings are used. ModbusDEVICE_struct * P183_ModbusDevice = nullptr; -ModbusQueueState_t P183_ModbusStatus = {}; -boolean P183_init = false; - -void P183_scan_modbus(); -void P183_scan_module(uint8_t node_id, - uint8_t start_reg = 0x00, - uint8_t end_reg = 0xFF); +ModbusResultState_t P183_ModbusStatus = {}; +boolean P183_init = false; boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { @@ -211,63 +215,34 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_INIT: { - P183_init = true; - addLogMove(LOG_LEVEL_INFO, "P183 INIT"); - - // (re)create the serial port object - // If the serial port object already exists, delete it first. - if (P183_ModbusDevice != nullptr) { - delete P183_ModbusDevice; - P183_ModbusDevice = nullptr; + initPluginTaskData(event->TaskIndex, new (std::nothrow) P183_data_struct(event)); + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data != nullptr) { + success = P183_data->plugin_init(P183_DEV_ID, + static_cast(CONFIG_PORT), + CONFIG_PIN1, + CONFIG_PIN2, + P183_storageValueToBaudrate(P183_BAUDRATE), + P183_DEPIN, + P183_GET_FLAG_COLL_DETECT); } - P183_ModbusDevice = new (std::nothrow) ModbusDEVICE_struct(); - addLogMove(LOG_LEVEL_INFO, "P183 INIT AFTER NEW"); - - if (P183_ModbusDevice == nullptr) { - P183_init = false; - addLogMove(LOG_LEVEL_ERROR, F("P183: Unable to allocate Modbus device object")); - break; - } - - if (!P183_ModbusDevice->init(P183_DEV_ID, static_cast(CONFIG_PORT), - CONFIG_PIN1, - CONFIG_PIN2, - P183_storageValueToBaudrate(P183_BAUDRATE), - P183_DEPIN, - P183_GET_FLAG_COLL_DETECT)) { - break; + else { + addLog(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); } - addLogMove(LOG_LEVEL_INFO, "P183 INIT AFTER INIT"); - P183_ModbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); - addLogMove(LOG_LEVEL_DEBUG, "AFTER TIMEOUT"); - # ifdef P183_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("P183: Init serial: RX pin "); - log += CONFIG_PIN1; - log += F(", TX pin "); - log += CONFIG_PIN2; - log += F(", RS485 mode selected on pin "); - log += P183_DEPIN; - log += F(", baudrate "); - log += P183_storageValueToBaudrate(P183_BAUDRATE); - log += F(", collision detection "); - log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); - addLogMove(LOG_LEVEL_DEBUG, log); - } - # endif // ifdef P183_DEBUG - success = true; + P183_init = true; + success = true; break; } case PLUGIN_EXIT: { - P183_init = false; + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (P183_ModbusDevice != nullptr) { - delete P183_ModbusDevice; - P183_ModbusDevice = nullptr; + if (P183_data != nullptr) { + delete P183_data; + P183_data = nullptr; } success = true; break; @@ -275,20 +250,18 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { - // TODO: This is a very ugly way to reserve static memory for the return values of the Modbus read function. - // This does not work if multiple instances of this plugin are used. - static uint16_t registerValues[4] = {0, 0, 0, 0}; - for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) - { - // TODO: Abuse PCONFIG_LONG static storage for now. - P183_modbus_readRegister(P183_ADDRESS(outputIndex), &(registerValues[outputIndex])); - UserVar.setFloat(event->TaskIndex, outputIndex, registerValues[outputIndex]); - } - success = true; + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + success = P183_data->plugin_read(event); // Delegate to data_struct break; } case PLUGIN_WRITE: { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + return false; + } + if (P183_ModbusDevice != nullptr) { const String cmd = parseString(string, 1); @@ -299,7 +272,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) // Write a value to a Modbus register int address = parseString(string, 3).toInt(); uint16_t value = parseString(string, 4).toInt(); - P183_modbus_writeRegister(P183_DEV_ID, address, value); + P183_data->writeResgister(address, value); if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: write value "); @@ -314,7 +287,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) // Read a value from a Modbus register int address = parseString(string, 3).toInt(); uint16_t value = 0; - P183_modbus_readRegister(address, &value); + value = P183_data->readRegisterWait(address); if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: read value "); @@ -336,14 +309,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (end_address - start_address > 100) { end_address = start_address + 100; // Limit to 100 addresses } - addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); - P183_scan_module(P183_DEV_ID, start_address, end_address); + P183_data->scan_device(P183_DEV_ID, start_address, end_address); success = true; } else if (equals(subcmd, F("scan"))) { // Scan for Modbus devices - addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); - P183_scan_modbus(); + P183_data->scan_modbus(); success = true; } else { @@ -354,24 +325,28 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } case PLUGIN_GET_CONFIG_VALUE: { - const String cmd = parseString(string, 1); + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + const String cmd = parseString(string, 1); if (equals(cmd, F("register"))) { int address = parseString(string, 2).toInt(); uint16_t value = 0; - P183_modbus_readRegister(address, &value); + value = P183_data->readRegisterWait(address); string = String(value); success = true; } break; } case PLUGIN_TEN_PER_SECOND: { - if (P183_init && (P183_ModbusDevice != nullptr)) { - P183_ModbusDevice->processCommand(); + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data != nullptr) { + P183_data->plugin_ten_per_second(event); } break; } } + return success; } @@ -404,88 +379,4 @@ int P183_storageValueToBaudrate(uint8_t baudrate_setting) { return baudrate; } -// Read a single Modbus register from a device with given node ID -// On success, the read value is stored in *value and 0 is returned. -int P183_modbus_readRegister(uint16_t reg, uint16_t *value) -{ - if (P183_ModbusDevice != nullptr) { - P183_ModbusDevice->readHoldingRegister(reg, value, &P183_ModbusStatus); - } - return 0; -} - -// Write a single Modbus register to a device with given node ID -// On success, 0 is returned. -int P183_modbus_writeRegister(uint8_t node_id, uint16_t reg, uint16_t value) -{ - if (P183_ModbusDevice != nullptr) { - P183_ModbusDevice->writeSingleRegister(reg, value, &P183_ModbusStatus); - } - return 0; -} - -// Dump the content of a buffer to the log -// This function takes a pointer to a buffer and its length, and logs the content in hexadecimal format. -void P183_dump_buffer(const uint8_t *buffer, size_t length) { -# ifdef P183_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("Modbus: Dumping buffer: "); - - for (size_t i = 0; i < length; ++i) { - log += String(buffer[i], HEX); - - if (i < length - 1) { - log += F(", "); - } - } - addLogMove(LOG_LEVEL_DEBUG, log); - } -# endif // ifdef P183_DEBUG -} - -// Scan Modbus registers from 0x00 to 0xFF for a given node ID -void P183_scan_module(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) -{ - String log; - uint16_t value = 0; - - for (uint8_t reg = start_reg; reg <= end_reg; reg++) { - int result = P183_modbus_readRegister(reg, &value); - log += F("** Address "); - log += String(reg); - log += F(" (0x"); - log += String(reg, HEX); - - if (result == 0) { - log += F(") = "); - log += String(value); - } else { - log += F(") invalid"); - } - addLogMove(LOG_LEVEL_INFO, log); - } -} - -// Scan Modbus addreses from 0x00 to 0xFF for a given node ID -void P183_scan_modbus() -{ - String log; - uint16_t value = 0; - - for (uint8_t id = 0; id <= 247; id++) { - // TODO: how to scan the Modbus devices in teh new structure - int result = P183_modbus_readRegister(1, &value); - log += F("** Address "); - log += String(id); - - if (result == 0) { - log += F(" OK"); - } else { - log += F(" no response"); - } - addLogMove(LOG_LEVEL_INFO, log); - } -} - #endif // USES_P183 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 79f8b057ee..8a56e01155 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -2836,7 +2836,7 @@ To create/register a plugin, you have to : #endif #endif -#if defined(USES_P085) || defined (USES_P052) || defined(USES_P078) || defined(USES_P108) || defined(USES_P183) +#if defined(USES_P085) || defined (USES_P052) || defined(USES_P078) || defined(USES_P108) // FIXME TD-er: Is this correct? Those plugins use Modbus_RTU. #ifdef FEATURE_MODBUS #undef FEATURE_MODBUS @@ -2844,6 +2844,21 @@ To create/register a plugin, you have to : #define FEATURE_MODBUS 1 #endif +#if defined(USES_P183) + // FIXME TD-er: Is this correct? Those plugins use Modbus_RTU. + #ifdef FEATURE_MODBUS_FAC + #undef FEATURE_MODBUS_FAC + #endif + #define FEATURE_MODBUS_FAC 1 +#endif + +#if defined(USES_P085) || defined (USES_P052) || defined(USES_P078) || defined(USES_P108) || defined(USES_P183) + // FIXME TD-er: Is this correct? Those plugins use Modbus_RTU. + #ifdef FEATURE_MODBUS + #undef FEATURE_MODBUS + #endif + #define FEATURE_MODBUS 1 +#endif #if defined(USES_C001) || defined (USES_C002) || defined(USES_P029) #ifndef FEATURE_DOMOTICZ #define FEATURE_DOMOTICZ 1 @@ -3462,6 +3477,10 @@ To create/register a plugin, you have to : #define FEATURE_MODBUS 0 #endif +#ifndef FEATURE_MODBUS_FAC +#define FEATURE_MODBUS_FAC 0 +#endif + #ifndef FEATURE_MQTT #define FEATURE_MQTT 0 #endif diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 279567985b..e9c5178fdf 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -1,12 +1,16 @@ #include "../../ESPEasy_common.h" -#if FEATURE_MODBUS +#if FEATURE_MODBUS_FAC # include "Modbus_device.h" # include "modbus_link.h" # include "modbus_mgr.h" +const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; +const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; +const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; +const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusDEVICE_struct::~ModbusDEVICE_struct() { @@ -51,7 +55,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, String log = F("---> ModbusDevice Init: Slave address = "); log += slaveAddress; log += F(", This = "); - log += (ulong)this; + log += (size_t)this; log += F(", deviceID = "); log += _deviceID; addLogMove(LOG_LEVEL_INFO, log); @@ -83,14 +87,14 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t *valuePtr, - ModbusQueueState_t *statePtr) + ModbusResultState_t *statePtr) { if (_modbus_link == nullptr) { return false; } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); - request->_messageType = ModbusMessageType::READ_HOLDING_REGISTERS; + request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; request->_userData = valuePtr; request->_sendframe[0] = _modbus_address; request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; @@ -105,7 +109,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, request->_rcvframe_length = 7; // Expect 8 bytes in response dump_buffer(request->_sendframe, request->_sendframe_length); uint16_t queueID = _modbus_link->queueRequest(request); - *statePtr = ModbusQueueState_t::QUEUED; + *statePtr = ModbusResultState::BUSY; // Don't touch *valueptr here, it might contain a previous valid result. return false; // TODO: implement @@ -114,7 +118,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, - ModbusQueueState_t *statePtr) + ModbusResultState_t *statePtr) { Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); @@ -130,8 +134,8 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 8; // Expect 8 bytes in response uint16_t queueID = _modbus_link->queueRequest(request); - *statePtr = ModbusQueueState_t::QUEUED; - request->_messageType = ModbusMessageType::WRITE_SINGLE_REGISTER; + *statePtr = ModbusResultState::BUSY; + request->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; return false; } @@ -151,13 +155,17 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) { String log = F("---> Device callback: "); + if (req == nullptr) { + log += F("ERROR: Null pointer passed"); + return; + } log += req->_id; log += F(", Message = "); - log += (uint8_t)req->_messageType; + log += static_cast(req->_messageType); switch (req->_messageType) { - case ModbusMessageType::READ_HOLDING_REGISTERS: + case ModbusTransactionType::READ_HOLDING_REGISTERS: { if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { uint16_t crc = CalculateCRC(req->_rcvframe, 5); @@ -176,7 +184,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) break; } - case ModbusMessageType::WRITE_SINGLE_REGISTER: + case ModbusTransactionType::WRITE_SINGLE_REGISTER: { if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { uint16_t crc = CalculateCRC(req->_rcvframe, 5); @@ -193,7 +201,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) break; } - case ModbusMessageType::NONE: + case ModbusTransactionType::NONE: { // Should not happen break; diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 6748313fb1..aee9d36d03 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -3,11 +3,23 @@ #include "../../ESPEasy_common.h" -#if FEATURE_MODBUS +#if FEATURE_MODBUS_FAC # include # include "Modbus_link.h" +# ifndef MODBUS_BROADCAST_ADDRESS +# define MODBUS_BROADCAST_ADDRESS 0xFE // Address used for boardcast messages +# endif // ifndef MODBUS_BROADCAST_ADDRESS + +// States for the Modbus queue elements +typedef enum class ModbusResultState { + BUSY = 0, // Transaction is not completed + SUCCESS = 1, // Transaction successfully completed + ERROR = 2, // Transaction completed with an error +} ModbusResultState_t; + + // ModbusDEVICE structure representing a MODBUS Device // This is a single device that may share it's Modbus link with multiple other devices. // It uses the ModbusLINKManager to find the ModbusLINK object that handles the data transport. @@ -50,13 +62,13 @@ struct ModbusDEVICE_struct { // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. // The state variable will signal the processing state of the request. - bool readHoldingRegister(uint16_t address, - uint16_t *valueptr, - ModbusQueueState_t *stateptr); + bool readHoldingRegister(uint16_t address, + uint16_t *valueptr, + ModbusResultState_t *stateptr); - bool writeSingleRegister(uint16_t address, - uint16_t value, - ModbusQueueState_t *stateptr); + bool writeSingleRegister(uint16_t address, + uint16_t value, + ModbusResultState_t *stateptr); private: @@ -71,5 +83,5 @@ struct ModbusDEVICE_struct { size_t length); }; -#endif // FEAURE_MODBUS +#endif // FEAURE_MODBUS_FAC #endif // HELPERS_MODBUS_LINK_H diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index f566990391..137571fb63 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -7,7 +7,7 @@ #include "../../ESPEasy_common.h" -#if FEATURE_MODBUS +#if FEATURE_MODBUS_FAC # include "Modbus_device.h" # include "Modbus_link.h" @@ -26,6 +26,14 @@ ModbusLINK_struct::~ModbusLINK_struct() { // Reset the ModbusLINK structure /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::reset() { + for (auto it = _requestQueue.begin(); it != _requestQueue.end(); it++) { + (*it)->_state = ModbusQueueState::ERROR_OCCURRED; + + if ((*it)->_device != nullptr) { + (*it)->_device->linkCallback(*it); // Notify the device that the request finished with an error + } + it = _requestQueue.erase(it); + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -113,9 +121,14 @@ Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVI bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) { String log = F("---> Free: "); + // TODO: Transacton not yet on queue shall be deleted here if (transaction != nullptr) { - transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark as freed - log += transaction->_id; + if (transaction->_state == ModbusQueueState::NOT_QUEUED) { + delete transaction; + } else { + transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be freed + } + log += transaction->_id; addLogMove(LOG_LEVEL_INFO, log); return true; } @@ -129,11 +142,11 @@ bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Free all queued transactions for the given device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct * device) +void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) { for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { - if (it->_device == device) { - it->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed + if ((*it)->_device == device) { + (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed } } } @@ -151,7 +164,7 @@ uint16_t ModbusLINK_struct::queueRequest(Modbus_RequestQueueElement *transaction addLogMove(LOG_LEVEL_INFO, log); } transaction->_state = ModbusQueueState::QUEUED; // Initial state - _requestQueue.push_back(*transaction); // Append the request to the queue + _requestQueue.push_back(transaction); // Append the request to the queue processCommand(); // Trigger processing of the command queue return transaction->_id; } @@ -181,16 +194,17 @@ void ModbusLINK_struct::processCommand() bool busy = false; // Only process one request at a time while ((it != _requestQueue.end()) && !busy) { - dumpQueueElement(&(*it)); + dumpQueueElement(*it); - switch (it->_state) { + switch ((*it)->_state) { case ModbusQueueState::QUEUED: { log += F(" state QUEUED, ID = "); - log += it->_id; + log += (*it)->_id; // Send the request int available = _easySerial->available(); + if (available > 0) { // Clear any pending input for (int i = available; i > 0; --i) { @@ -198,36 +212,36 @@ void ModbusLINK_struct::processCommand() } } - _easySerial->write(it->_sendframe, it->_sendframe_length); - it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response - it->_startTime = millis(); // Record the time the transaction - log += F(" state QUEUED, ID = "); - log += it->_id; - busy = true; // Only process one request at a time + _easySerial->write((*it)->_sendframe, (*it)->_sendframe_length); + (*it)->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response + (*it)->_startTime = millis(); // Record the time the transaction + log += F(" state QUEUED, ID = "); + log += (*it)->_id; + busy = true; // Only process one request at a time break; } case ModbusQueueState::MESSAGE_SENT: { log += F(" state MESSAGE_SENT, ID = "); - log += it->_id; + log += (*it)->_id; // Waiting for response - if (_easySerial->available() >= it->_rcvframe_length) { - _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); - it->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received + if (_easySerial->available() >= (*it)->_rcvframe_length) { + _easySerial->readBytes((*it)->_rcvframe, (*it)->_rcvframe_length); + (*it)->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received - if (it->_device != nullptr) { - it->_device->linkCallback(&(*it)); // Notify the device that a response was received + if ((*it)->_device != nullptr) { + (*it)->_device->linkCallback(*it); // Notify the device that a response was received } } - else if (timePassedSince(it->_startTime) > it->_timeout) { + else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { // Timeout expired - it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error - log += F(" Timeout "); + (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + log += F(" Timeout "); - if (it->_device != nullptr) { - it->_device->linkCallback(&(*it)); // Notify the device that a response was received + if ((*it)->_device != nullptr) { + (*it)->_device->linkCallback(*it); // Notify the device that a response was received } else { log += F(" Available="); @@ -245,7 +259,7 @@ void ModbusLINK_struct::processCommand() case ModbusQueueState::ERROR_OCCURRED: { log += F(" state ERROR_OCCURRED, ID = "); - log += it->_id; + log += (*it)->_id; it++; break; @@ -254,9 +268,10 @@ void ModbusLINK_struct::processCommand() case ModbusQueueState::READY_FOR_DESTROY: { log += F(" state READY_FOR_DESTROY, ID = "); - log += it->_id; + log += (*it)->_id; - it = _requestQueue.erase(it); + delete (*it); // destroy the queue element + it = _requestQueue.erase(it); // Remove it from the list break; } diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index b3c79d19a2..8d8e016f11 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -3,11 +3,10 @@ #include "../../ESPEasy_common.h" -#if FEATURE_MODBUS +#if FEATURE_MODBUS_FAC # include "../../_Plugin_Helper.h" # include -# include "MODBUS_RTU.h" # define MODBUS_XMIT_BUFFER 12 # define MODBUS_RCV_BUFFER 256 @@ -28,18 +27,18 @@ typedef enum class ModbusQueueState { // Types of Modbus transactions supported by the Modbuss_device // This enumeration is used by the Modbus device to indicate which transaction is associated with the queue element. // See Modbus specification for details on function codes. -enum class ModbusMessageType { +typedef enum class ModbusTransactionType { NONE = 0, // Undefined/unknown transaction type READ_HOLDING_REGISTERS = 1, // Read holding registers (function code 0x03) WRITE_SINGLE_REGISTER = 2 // Write single register (function code 0x06) -}; +} ModbusTransactionType_t; // Modbus request queue element structure // This structure represents a single Modbus request and its associated response. struct Modbus_RequestQueueElement { Modbus_RequestQueueElement() = default; - ModbusMessageType _messageType = ModbusMessageType::NONE; // Type of Modbus message + ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message void *_userData = nullptr; // Pointer to user data uint16_t _id = 0; // ID of the request struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the @@ -55,7 +54,7 @@ struct Modbus_RequestQueueElement { }; // Queue of Modbus request elements -typedef std::list Modbus_RequestQueue; +typedef std::list Modbus_RequestQueue; // ModbusLINK structure representing a MODBUS LINK diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index da7265ba33..1dae40d368 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -1,7 +1,7 @@ #include "../../ESPEasy_common.h" -#if FEATURE_MODBUS +#if FEATURE_MODBUS_FAC # include # include "Modbus_mgr.h" diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 2e4cf4e0db..0c09daab78 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -3,7 +3,7 @@ #include "../../ESPEasy_common.h" -#if FEATURE_MODBUS +#if FEATURE_MODBUS_FAC # include # include "Modbus_link.h" diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp new file mode 100644 index 0000000000..a4e7e8bfde --- /dev/null +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -0,0 +1,192 @@ +#include "P183_data_struct.h" + +#ifdef USES_P183 + +// ####################################################################################################### +// ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### +// ####################################################################################################### + +# define P183_NR_OUTPUTS PCONFIG(3) +# define P183_ADDRESS(x) PCONFIG(4 + x) + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +P183_data_struct::P183_data_struct(struct EventStruct *event) { + _taskIndex = event->TaskIndex; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +P183_data_struct::~P183_data_struct() { + plugin_exit(); // Destruct dynamic structures contained in this object +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_init(uint8_t slaveAddress, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect) { + // Create a fresh Modbus_device object to handle the Modbus communication + if (_modbusDevice != nullptr) { + delete _modbusDevice; + _modbusDevice = nullptr; + } + _modbusDevice = new (std::nothrow) ModbusDEVICE_struct(); + + if (_modbusDevice == nullptr) { + addLogMove(LOG_LEVEL_ERROR, F("P183: Unable to allocate Modbus device object")); + return false; + } + + // Initialize our own Modbus_device with the provided serial link parameters + // Note that the link configuration is expected to be the same for all plugins reusing the same serial port + if (!_modbusDevice->init(slaveAddress, port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + return false; + } + _modbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); + + # ifdef P183_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("P183: Init serial: RX pin "); + log += CONFIG_PIN1; + log += F(", TX pin "); + log += CONFIG_PIN2; + log += F(", RS485 mode selected on pin "); + log += P183_DEPIN; + log += F(", baudrate "); + log += P183_storageValueToBaudrate(P183_BAUDRATE); + log += F(", collision detection "); + log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); + addLogMove(LOG_LEVEL_DEBUG, log); + } + # endif // ifdef P183_DEBUG + + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::plugin_exit() +{ + if (_modbusDevice == nullptr) { + delete _modbusDevice; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_once_a_second(struct EventStruct *event) { + // TODO + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_ten_per_second(struct EventStruct *event) { + if (nullptr != _modbusDevice) { + _modbusDevice->processCommand(); + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Plugin read function implementataion. +// A new request to update the Modbus registers is queued. The plugin output valuea are updated with the previous results +bool P183_data_struct::plugin_read(struct EventStruct *event) { + if (_modbusDevice == nullptr) { + return false; + } + + for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) + { + _modbusDevice->readHoldingRegister(P183_ADDRESS(outputIndex), &(_registerValues[outputIndex]), &(_queueStates[outputIndex])); + UserVar.setFloat(event->TaskIndex, outputIndex, _registerValues[outputIndex]); + } + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) +{ + String log; + uint16_t value = 0; + ModbusQueueState_t state = ModbusQueueState::NOT_QUEUED; + + addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); + + // for (uint8_t reg = start_reg; reg <= end_reg; reg++) { + // int result = _modbusDevice->readHoldingRegister(reg, &value, &state); + // // TODO: Find a way to manage the delay on the queue + // log += F("** Address "); + // log += String(reg); + // log += F(" (0x"); + // log += String(reg, HEX); + // + // if (result == 0) { + // log += F(") = "); + // log += String(value); + // } else { + // log += F(") invalid"); + // } + // addLogMove(LOG_LEVEL_INFO, log); + // } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Scan Modbus addreses from 0x00 to 0xFF for a given node ID +void P183_data_struct::scan_modbus() +{ + String log; + uint16_t value = 0; + ModbusQueueState_t state = ModbusQueueState::NOT_QUEUED; + + addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); + + // for (uint8_t id = 0; id <= 247; id++) { + // // TODO: how to scan the Modbus devices in teh new structure + // int result = P183_modbus_readRegister(1, &value); + // log += F("** Address "); + // log += String(id); + // + // if (result == 0) { + // log += F(" OK"); + // } else { + // log += F(" no response"); + // } + // addLogMove(LOG_LEVEL_INFO, log); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Read a Modbus register from the device. Wait untial the data is available +// Warning: this may take time as we waith for the Modbus message to be exchanged +uint16_t P183_data_struct::readRegisterWait(uint16_t address) { + uint16_t value = 0; + ModbusResultState_t state = ModbusResultState::BUSY; + + if (_modbusDevice == nullptr) { + return 0; + } + + _modbusDevice->readHoldingRegister(address, &value, &state); // Queue the read action + + while (state == ModbusResultState::BUSY) { + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + } + return value; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::writeResgister(uint16_t address, uint16_t value) +{ + static ModbusResultState_t state = ModbusResultState::BUSY; // Static ias it will be accessed after teh function has finished + + if (_modbusDevice == nullptr) { + return; + } + + _modbusDevice->writeSingleRegister(address, value, &state); // Queue the action (and for now forget it) +} + +#endif // ifdef USES_P183 diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h new file mode 100644 index 0000000000..a96431cecf --- /dev/null +++ b/src/src/PluginStructs/P183_data_struct.h @@ -0,0 +1,48 @@ +#ifndef PLUGINSTRUCTS_P183_DATA_STRUCT_H +#define PLUGINSTRUCTS_P183_DATA_STRUCT_H + +#include "../../_Plugin_Helper.h" +#ifdef USES_P183 + +# include "../Helpers/Modbus_device.h" + +# define P183_MODBUS_TIMEOUT 1000 // milliseconds + +// The default set of single-value VType options +constexpr uint8_t P183_START_VTYPE = 0; + +struct P183_data_struct : public PluginTaskData_base { + P183_data_struct(struct EventStruct *event); + P183_data_struct() = delete; + virtual ~P183_data_struct(); + + bool plugin_init(uint8_t slaveAddress, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect); + + void plugin_exit(); + bool plugin_read(struct EventStruct *event); + bool plugin_once_a_second(struct EventStruct *event); + bool plugin_ten_per_second(struct EventStruct *event); + void scan_device(uint8_t node_id, + uint8_t start_reg, + uint8_t end_reg); + void scan_modbus(); + uint16_t readRegisterWait(uint16_t address); + void writeResgister(uint16_t address, + uint16_t value); + +private: + + taskIndex_t _taskIndex = INVALID_TASK_INDEX; + struct ModbusDEVICE_struct *_modbusDevice = nullptr; + uint16_t _registerValues[4] = {}; // Modus register values retrieved for output values + ModbusResultState_t _queueStates[4] = {}; // State of read hloding register transactions +}; + +#endif // ifdef USES_P183 +#endif // ifndef PLUGINSTRUCTS_P183_DATA_STRUCT_H From 8c66411f1a27cf485a32e2647aaa2783923f5361 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:06:39 +0200 Subject: [PATCH 12/48] P183 Start cleaning up --- src/_P183_modbus.ino | 122 +++++++++--------- src/src/CustomBuild/define_plugin_sets.h | 9 +- src/src/Helpers/Modbus_device.cpp | 80 +++++++----- src/src/Helpers/Modbus_link.cpp | 141 ++++++++++++--------- src/src/Helpers/Modbus_link.h | 11 +- src/src/PluginStructs/P183_data_struct.cpp | 97 ++++++++------ src/src/PluginStructs/P183_data_struct.h | 5 +- 7 files changed, 252 insertions(+), 213 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index b729aacc1e..a19efde723 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -78,12 +78,6 @@ # define P183_MODBUS_FUNC_READ_HOLDING_REGISTERS 0x03 # define P183_MODBUS_FUNC_WRITE_SINGLE_REGISTER 0x06 -// These pointers may be used among multiple instances of the same plugin, -// as long as the same serial settings are used. -ModbusDEVICE_struct * P183_ModbusDevice = nullptr; -ModbusResultState_t P183_ModbusStatus = {}; -boolean P183_init = false; - boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { boolean success = false; @@ -208,8 +202,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_ADDRESS(outputIndex) = getFormItemInt(P183_ADDRESS_LABEL(outputIndex)); } - P183_init = false; // Force device setup next time - success = true; + success = true; break; } @@ -231,8 +224,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) addLog(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); } - P183_init = true; - success = true; + success = true; break; } @@ -256,72 +248,72 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_WRITE: { - P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { + addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Write invalid data struct")); return false; } - if (P183_ModbusDevice != nullptr) { - const String cmd = parseString(string, 1); - - if (equals(cmd, F("modbus"))) { - const String subcmd = parseString(string, 2); - - if (equals(subcmd, F("write"))) { - // Write a value to a Modbus register - int address = parseString(string, 3).toInt(); - uint16_t value = parseString(string, 4).toInt(); - P183_data->writeResgister(address, value); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus: write value "); - log += value; - log += F(" to address "); - log += address; - addLogMove(LOG_LEVEL_INFO, log); - } - success = true; - } - else if (equals(subcmd, F("read"))) { - // Read a value from a Modbus register - int address = parseString(string, 3).toInt(); - uint16_t value = 0; - value = P183_data->readRegisterWait(address); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus: read value "); - log += value; - log += F(" from address "); - log += address; - addLogMove(LOG_LEVEL_INFO, log); - } - success = true; + const String cmd = parseString(string, 1); + + if (equals(cmd, F("modbus"))) { + const String subcmd = parseString(string, 2); + + if (equals(subcmd, F("write"))) { + // Write a value to a Modbus register + int address = parseString(string, 3).toInt(); + uint16_t value = parseString(string, 4).toInt(); + P183_data->writeRegister(address, value); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Modbus: write value "); + log += value; + log += F(" to address "); + log += address; + addLogMove(LOG_LEVEL_INFO, log); } - else if (equals(subcmd, F("dump"))) { - int start_address = parseString(string, 3).toInt(); - int end_address = parseString(string, 4).toInt(); - - if (end_address < start_address) { - end_address = start_address; - } - - if (end_address - start_address > 100) { - end_address = start_address + 100; // Limit to 100 addresses - } - P183_data->scan_device(P183_DEV_ID, start_address, end_address); - success = true; + success = true; + } + else if (equals(subcmd, F("read"))) { + // Read a value from a Modbus register + int address = parseString(string, 3).toInt(); + uint16_t value = 0; + value = P183_data->readRegisterWait(address); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Modbus: read value "); + log += value; + log += F(" from address "); + log += address; + addLogMove(LOG_LEVEL_INFO, log); } - else if (equals(subcmd, F("scan"))) { - // Scan for Modbus devices - P183_data->scan_modbus(); - success = true; + success = true; + } + else if (equals(subcmd, F("dump"))) { + int start_address = parseString(string, 3).toInt(); + int end_address = parseString(string, 4).toInt(); + + if (end_address < start_address) { + end_address = start_address; } - else { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); + + if (end_address - start_address > 100) { + end_address = start_address + 100; // Limit to 100 addresses } + P183_data->scan_device(P183_DEV_ID, start_address, end_address); + success = true; + } + else if (equals(subcmd, F("scan"))) { + // Scan for Modbus devices + P183_data->scan_modbus(); + success = true; + } + else { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); } } + break; } case PLUGIN_GET_CONFIG_VALUE: { diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 8a56e01155..12934bdc05 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -2845,20 +2845,13 @@ To create/register a plugin, you have to : #endif #if defined(USES_P183) - // FIXME TD-er: Is this correct? Those plugins use Modbus_RTU. + //P183 uses the Modbus facilities #ifdef FEATURE_MODBUS_FAC #undef FEATURE_MODBUS_FAC #endif #define FEATURE_MODBUS_FAC 1 #endif -#if defined(USES_P085) || defined (USES_P052) || defined(USES_P078) || defined(USES_P108) || defined(USES_P183) - // FIXME TD-er: Is this correct? Those plugins use Modbus_RTU. - #ifdef FEATURE_MODBUS - #undef FEATURE_MODBUS - #endif - #define FEATURE_MODBUS 1 -#endif #if defined(USES_C001) || defined (USES_C002) || defined(USES_P029) #ifndef FEATURE_DOMOTICZ #define FEATURE_DOMOTICZ 1 diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index e9c5178fdf..8fe3021f26 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -7,9 +7,14 @@ # include "modbus_link.h" # include "modbus_mgr.h" -const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; -const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; -const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; +# define MODBUS_DEBUG +# ifdef BUILD_NO_DEBUG +# undef MODBUS_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + +const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; +const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; +const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -51,6 +56,8 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, _modbus_address = slaveAddress; + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("---> ModbusDevice Init: Slave address = "); log += slaveAddress; @@ -60,8 +67,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, log += _deviceID; addLogMove(LOG_LEVEL_INFO, log); } - - // TODO: further implementation needed + # endif // MODBUS_DEBUG return success; } @@ -85,8 +91,8 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, - uint16_t *valuePtr, +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, + uint16_t *valuePtr, ModbusResultState_t *statePtr) { if (_modbus_link == nullptr) { @@ -96,6 +102,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; request->_userData = valuePtr; + request->_userState = statePtr; request->_sendframe[0] = _modbus_address; request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; request->_sendframe[2] = highByte(address); @@ -108,20 +115,26 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 7; // Expect 8 bytes in response dump_buffer(request->_sendframe, request->_sendframe_length); - uint16_t queueID = _modbus_link->queueRequest(request); + uint16_t queueID = _modbus_link->queueTransaction(request); *statePtr = ModbusResultState::BUSY; // Don't touch *valueptr here, it might contain a previous valid result. - return false; // TODO: implement + return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, - uint16_t value, +bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, + uint16_t value, ModbusResultState_t *statePtr) { + if (_modbus_link == nullptr) { + return false; + } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + request->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; + request->_userState = statePtr; + request->_sendframe[0] = _modbus_address; request->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; request->_sendframe[2] = highByte(address); @@ -133,10 +146,11 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_sendframe[7] = highByte(crc); // CRC high byte request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 8; // Expect 8 bytes in response - uint16_t queueID = _modbus_link->queueRequest(request); - *statePtr = ModbusResultState::BUSY; - request->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; - return false; + dump_buffer(request->_sendframe, request->_sendframe_length); + uint16_t queueID = _modbus_link->queueTransaction(request); + *statePtr = ModbusResultState::BUSY; + + return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -153,16 +167,19 @@ void ModbusDEVICE_struct::processCommand() { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) { - String log = F("---> Device callback: "); + ModbusResultState_t resultState = ModbusResultState::ERROR; // Default to error unless proven otherwise if (req == nullptr) { - log += F("ERROR: Null pointer passed"); + addLogMove(LOG_LEVEL_INFO, F("Modbus ERROR: Null pointer passed in callback")); return; } + + # ifdef MODBUS_DEBUG + String log = F("---> Device callback: "); log += req->_id; log += F(", Message = "); log += static_cast(req->_messageType); - + # endif // MODBUS_DEBUG switch (req->_messageType) { case ModbusTransactionType::READ_HOLDING_REGISTERS: @@ -173,13 +190,10 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { // Valid response if (req->_userData != nullptr) { - *((uint16_t *)req->_userData) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + resultState = ModbusResultState::SUCCESS; } - } else { - // Invalid CRC } - } else { - // Invalid response } break; } @@ -190,32 +204,32 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) uint16_t crc = CalculateCRC(req->_rcvframe, 5); if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { - // Valid response - } - else { - // Invalid CRC + resultState = ModbusResultState::SUCCESS; } - } else { - // Invalid response } break; } case ModbusTransactionType::NONE: { - // Should not happen + // Error condition, this transaction type should not be queued + log += F(" Invalid transaction type"); break; } default: { - // Unknown message type + // Error condition, missed a transaction type + log += F(" Unknown transaction type"); break; } } + *(static_cast(req->_userState)) = resultState; _modbus_link->freeTransaction(req); + # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); + # endif // MODBUS_DEBUG } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -241,6 +255,8 @@ uint16_t ModbusDEVICE_struct::CalculateCRC(uint8_t *buf, int len) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("---> Modbus: Dumping buffer: "); @@ -255,4 +271,6 @@ void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { } } +# endif // MODBUS_DEBUG + #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 137571fb63..3163b589c5 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -12,6 +12,11 @@ # include "Modbus_device.h" # include "Modbus_link.h" +# define MODBUS_DEBUG +# ifdef BUILD_NO_DEBUG +# undef MODBUS_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusLINK_struct::~ModbusLINK_struct() { reset(); @@ -72,8 +77,10 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, ModbusLINK_struct::_easySerial->begin(baudrate); + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("P183: Init serial: RX pin "); + String log = F("Modbus_link: Init serial: RX pin "); log += serial_rx; log += F(", TX pin "); log += serial_tx; @@ -87,7 +94,7 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, log += rs485Mode ? F("yes") : F("no"); addLogMove(LOG_LEVEL_DEBUG, log); } - + # endif // MODBUS_DEBUG return true; } @@ -119,22 +126,16 @@ Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVI // Free a previously allocated transaction structure /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) { - String log = F("---> Free: "); - - // TODO: Transacton not yet on queue shall be deleted here if (transaction != nullptr) { - if (transaction->_state == ModbusQueueState::NOT_QUEUED) { + if (transaction->_state == ModbusQueueState::NOT_QUEUED) { // Not on the queue, can be directly deleted delete transaction; } else { - transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be freed + transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be freed by queue processing } - log += transaction->_id; - addLogMove(LOG_LEVEL_INFO, log); return true; } else { - log += F("Attempt to free null transaction"); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_ERROR, F("Modbus_link: Attempt to free null transaction")); return false; } } @@ -144,7 +145,7 @@ bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) { - for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { + for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { if ((*it)->_device == device) { (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed } @@ -155,7 +156,9 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) // Queue a Modbus request. The request is appended to the request and assigned a unique identifier. // The client can use this identifier to retrieve the response later. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint16_t ModbusLINK_struct::queueRequest(Modbus_RequestQueueElement *transaction) { +uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transaction) { + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("---> Modbus queue request: request ID = "); log += transaction->_id; @@ -163,30 +166,21 @@ uint16_t ModbusLINK_struct::queueRequest(Modbus_RequestQueueElement *transaction log += uint(transaction->_state); addLogMove(LOG_LEVEL_INFO, log); } + # endif // MODBUS_DEBUG transaction->_state = ModbusQueueState::QUEUED; // Initial state _requestQueue.push_back(transaction); // Append the request to the queue processCommand(); // Trigger processing of the command queue return transaction->_id; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Check if there is a response available for the given request ID and retrieve it if available -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::getResponse(uint16_t id, Modbus_RequestQueueElement **transaction) { - return false; -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Evaluate the next action to take to process the queue // This function shall be called periodically to keep the Modbus link active /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processCommand() { - String log = F("---> Process queue: "); - if (_easySerial == nullptr) { - log += F("Serial port not initialized"); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, F("Modbus_link: Serial port not initialized")); return; // Serial port not initialized } @@ -194,14 +188,14 @@ void ModbusLINK_struct::processCommand() bool busy = false; // Only process one request at a time while ((it != _requestQueue.end()) && !busy) { + # ifdef MODBUS_DEBUG dumpQueueElement(*it); + dumpState((*it)->_state); + # endif // MODBUS_DEBUG switch ((*it)->_state) { case ModbusQueueState::QUEUED: { - log += F(" state QUEUED, ID = "); - log += (*it)->_id; - // Send the request int available = _easySerial->available(); @@ -215,17 +209,12 @@ void ModbusLINK_struct::processCommand() _easySerial->write((*it)->_sendframe, (*it)->_sendframe_length); (*it)->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response (*it)->_startTime = millis(); // Record the time the transaction - log += F(" state QUEUED, ID = "); - log += (*it)->_id; busy = true; // Only process one request at a time break; } case ModbusQueueState::MESSAGE_SENT: { - log += F(" state MESSAGE_SENT, ID = "); - log += (*it)->_id; - // Waiting for response if (_easySerial->available() >= (*it)->_rcvframe_length) { _easySerial->readBytes((*it)->_rcvframe, (*it)->_rcvframe_length); @@ -238,15 +227,11 @@ void ModbusLINK_struct::processCommand() else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { // Timeout expired (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error - log += F(" Timeout "); if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback(*it); // Notify the device that a response was received - } - else { - log += F(" Available="); - log += _easySerial->available(); + (*it)->_device->linkCallback(*it); // Notify the device that a response was received } + else {} it++; } else { @@ -258,18 +243,12 @@ void ModbusLINK_struct::processCommand() case ModbusQueueState::ERROR_OCCURRED: { - log += F(" state ERROR_OCCURRED, ID = "); - log += (*it)->_id; - it++; break; } case ModbusQueueState::READY_FOR_DESTROY: { - log += F(" state READY_FOR_DESTROY, ID = "); - log += (*it)->_id; - delete (*it); // destroy the queue element it = _requestQueue.erase(it); // Remove it from the list break; @@ -279,34 +258,70 @@ void ModbusLINK_struct::processCommand() it++; break; } // switch - addLogMove(LOG_LEVEL_INFO, log); } // next iterarion return; } void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { - String log = F("[ ID="); - - log += el->_id; - log += F(", Device="); - log += String((uint32_t)(el->_device), HEX); - log += F(", State="); - log += (uint)el->_state; - log += F(", TX="); - - for (int i = 0; i < el->_sendframe_length; i++) { - log += String(el->_sendframe[i], HEX); - log += F(","); + # ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("[ ID="); + + log += el->_id; + log += F(", Device="); + log += String((uint32_t)(el->_device), HEX); + log += F(", State="); + log += (uint)el->_state; + log += F(", TX="); + + for (int i = 0; i < el->_sendframe_length; i++) { + log += String(el->_sendframe[i], HEX); + log += F(","); + } + log += F(", RX="); + + for (int i = 0; i < el->_rcvframe_length; i++) { + log += String(el->_rcvframe[i], HEX); + log += F(","); + } + log += F("] "); + addLogMove(LOG_LEVEL_INFO, log); } - log += F(", RX="); + # endif // MODBUS_DEBUG +} + +void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { + # ifdef MODBUS_DEBUG - for (int i = 0; i < el->_rcvframe_length; i++) { - log += String(el->_rcvframe[i], HEX); - log += F(","); + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("--->Modbus State= "); + + switch (state) { + case ModbusQueueState::NOT_QUEUED: + log += F("NOT_QUEUED"); + break; + case ModbusQueueState::QUEUED: + log += F("QUEUED"); + break; + case ModbusQueueState::MESSAGE_SENT: + log += F("MESSAGE_SENT"); + break; + case ModbusQueueState::RESPONSE_RECEIVED: + log += F("RESPONSE_RECEIVED"); + break; + case ModbusQueueState::ERROR_OCCURRED: + log += F("ERROR_OCCURRED"); + break; + case ModbusQueueState::READY_FOR_DESTROY: + log += F("READY_FOR_DESTROY"); + break; + } + + addLogMove(LOG_LEVEL_INFO, log); } - log += F("] "); - addLogMove(LOG_LEVEL_INFO, log); + # endif // MODBUS_DEBUG } #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 8d8e016f11..72a4ceca71 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -39,7 +39,8 @@ struct Modbus_RequestQueueElement { Modbus_RequestQueueElement() = default; ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message - void *_userData = nullptr; // Pointer to user data + void *_userData = nullptr; // Pointer to user (device) data + void *_userState = nullptr; // Pointer to user (device) defined state uint16_t _id = 0; // ID of the request struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the // action @@ -54,7 +55,7 @@ struct Modbus_RequestQueueElement { }; // Queue of Modbus request elements -typedef std::list Modbus_RequestQueue; +typedef std::list Modbus_RequestQueue; // ModbusLINK structure representing a MODBUS LINK @@ -86,15 +87,13 @@ struct ModbusLINK_struct { Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); bool freeTransaction(Modbus_RequestQueueElement *transaction); void freeTransactions(struct ModbusDEVICE_struct *device); - uint16_t queueRequest(Modbus_RequestQueueElement *transaction); - bool getResponse(uint16_t id, - Modbus_RequestQueueElement **transaction); - + uint16_t queueTransaction(Modbus_RequestQueueElement *transaction); void processCommand(); private: static void dumpQueueElement(Modbus_RequestQueueElement *el); + static void dumpState(ModbusQueueState_t state); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index a4e7e8bfde..f9450cf473 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -9,6 +9,9 @@ # define P183_NR_OUTPUTS PCONFIG(3) # define P183_ADDRESS(x) PCONFIG(4 + x) +# ifdef BUILD_NO_DEBUG +# undef P183_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// P183_data_struct::P183_data_struct(struct EventStruct *event) { @@ -17,7 +20,7 @@ P183_data_struct::P183_data_struct(struct EventStruct *event) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// P183_data_struct::~P183_data_struct() { - plugin_exit(); // Destruct dynamic structures contained in this object + plugin_exit(); // Destruct dynamic structures contained in this object } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -111,27 +114,37 @@ bool P183_data_struct::plugin_read(struct EventStruct *event) { void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { String log; - uint16_t value = 0; - ModbusQueueState_t state = ModbusQueueState::NOT_QUEUED; + uint16_t value = 0; + ModbusResultState_t state = ModbusResultState::BUSY; addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); - // for (uint8_t reg = start_reg; reg <= end_reg; reg++) { - // int result = _modbusDevice->readHoldingRegister(reg, &value, &state); - // // TODO: Find a way to manage the delay on the queue - // log += F("** Address "); - // log += String(reg); - // log += F(" (0x"); - // log += String(reg, HEX); - // - // if (result == 0) { - // log += F(") = "); - // log += String(value); - // } else { - // log += F(") invalid"); - // } - // addLogMove(LOG_LEVEL_INFO, log); - // } + if (_modbusDevice == nullptr) { + return; + } + + for (uint8_t reg = start_reg; reg <= end_reg; reg++) { + int result = _modbusDevice->readHoldingRegister(reg, &value, &state); + + _modbusDevice->readHoldingRegister(1, &value, &state); + + while (state == ModbusResultState::BUSY) { + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + } + + log += F("** Address "); + log += String(reg); + log += F(" (0x"); + log += String(reg, HEX); + + if (result == 0) { + log += F(") = "); + log += String(value); + } else { + log += F(") invalid"); + } + addLogMove(LOG_LEVEL_INFO, log); + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -139,23 +152,31 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e void P183_data_struct::scan_modbus() { String log; - uint16_t value = 0; - ModbusQueueState_t state = ModbusQueueState::NOT_QUEUED; + uint16_t value = 0; + ModbusResultState_t state = ModbusResultState::BUSY; addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); - // for (uint8_t id = 0; id <= 247; id++) { - // // TODO: how to scan the Modbus devices in teh new structure - // int result = P183_modbus_readRegister(1, &value); - // log += F("** Address "); - // log += String(id); - // - // if (result == 0) { - // log += F(" OK"); - // } else { - // log += F(" no response"); - // } - // addLogMove(LOG_LEVEL_INFO, log); + if (_modbusDevice == nullptr) { + return; + } + + for (uint8_t id = 0; id <= 247; id++) { + _modbusDevice->readHoldingRegister(1, &value, &state); + + while (state == ModbusResultState::BUSY) { + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + } + log += F("** Address "); + log += String(id); + + if (state == ModbusResultState::SUCCESS) { + log += F(" OK"); + } else { + log += F(" no response"); + } + addLogMove(LOG_LEVEL_INFO, log); + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -172,21 +193,21 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { _modbusDevice->readHoldingRegister(address, &value, &state); // Queue the read action while (state == ModbusResultState::BUSY) { - _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + delay(50); + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } + return value; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void P183_data_struct::writeResgister(uint16_t address, uint16_t value) +void P183_data_struct::writeRegister(uint16_t address, uint16_t value) { - static ModbusResultState_t state = ModbusResultState::BUSY; // Static ias it will be accessed after teh function has finished - if (_modbusDevice == nullptr) { return; } - _modbusDevice->writeSingleRegister(address, value, &state); // Queue the action (and for now forget it) + _modbusDevice->writeSingleRegister(address, value, &_lastActionState); // Queue the action (and for now forget it) } #endif // ifdef USES_P183 diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index a96431cecf..a32df4905e 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -33,8 +33,8 @@ struct P183_data_struct : public PluginTaskData_base { uint8_t end_reg); void scan_modbus(); uint16_t readRegisterWait(uint16_t address); - void writeResgister(uint16_t address, - uint16_t value); + void writeRegister(uint16_t address, + uint16_t value); private: @@ -42,6 +42,7 @@ struct P183_data_struct : public PluginTaskData_base { struct ModbusDEVICE_struct *_modbusDevice = nullptr; uint16_t _registerValues[4] = {}; // Modus register values retrieved for output values ModbusResultState_t _queueStates[4] = {}; // State of read hloding register transactions + ModbusResultState_t _lastActionState = ModbusResultState::BUSY; }; #endif // ifdef USES_P183 From a4dfca15a0fbcd374173b32f89ddc8efa130dbd4 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:27:18 +0200 Subject: [PATCH 13/48] P183 Pending rework, not final --- src/_P183_modbus.ino | 81 ++++++---------------- src/src/Helpers/Modbus_device.cpp | 56 +++++++++------ src/src/Helpers/Modbus_device.h | 5 ++ src/src/Helpers/Modbus_link.cpp | 73 ++++++++++--------- src/src/Helpers/Modbus_link.h | 5 +- src/src/Helpers/Modbus_mgr.cpp | 2 - src/src/PluginStructs/P183_data_struct.cpp | 63 ++++++----------- src/src/PluginStructs/P183_data_struct.h | 32 ++++++++- 8 files changed, 154 insertions(+), 163 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index a19efde723..c975667c3c 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -4,8 +4,6 @@ // ####################################################################################################### // ############## Plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### -// TODO: Refactor for a better Modbus implementation using the modbus_device for all functions. -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /* Plugin written by: Flashmark @@ -36,48 +34,11 @@ # define PLUGIN_VALUENAME3_183 "Value3" # define PLUGIN_VALUENAME4_183 "Value4" -// Plugin configuration parameters -// PCONFIG(0) is the Modbus device ID. -// PCONFIG(1) is the serial baud rate. -// PCONFIG(2) is used for flags, where bit 0 indicates collision detection -// PCONFIG(3) is the number of active output values (1-4) -// PCONFIG(4) is the Modbus register address for value 1 -// PCONFIG(5) is the Modbus register address for value 2 -// PCONFIG(6) is the Modbus register address for value 3 -// PCONFIG(7) is the Modbus register address for value 4 -// Use P183_ADDRESS(x) to access the PCONFIG value for value x -# define P183_DEV_ID PCONFIG(0) -# define P183_DEV_ID_LABEL PCONFIG_LABEL(0) -# define P183_BAUDRATE PCONFIG(1) -# define P183_BAUDRATE_LABEL PCONFIG_LABEL(1) -# define P183_NR_OUTPUTS PCONFIG(3) -# define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) -# define P183_ADDRESS(x) PCONFIG(4 + x) -# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) - -# define P183_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) -# define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) -# define P183_FLAG_COLL_DETECT_LABEL "colldet" - -# define P183_DEPIN CONFIG_PIN3 - -# define P183_DEV_ID_DFLT 1 -# define P183_BAUDRATE_DFLT 3 // 9600 baud - -# define P183_MAX_BAUDRATE_SEL 8 - # include # include "src/PluginStructs/P183_data_struct.h" # include "src/Helpers/Modbus_device.h" # include "src/Helpers/Modbus_mgr.h" -// Modbus properties -# define P183_MAX_MODBUS_NODES 247 - -# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address -# define P183_MODBUS_FUNC_READ_HOLDING_REGISTERS 0x03 -# define P183_MODBUS_FUNC_WRITE_SINGLE_REGISTER 0x06 - boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { boolean success = false; @@ -232,9 +193,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (P183_data != nullptr) { - delete P183_data; - P183_data = nullptr; + if (nullptr != P183_data) { + P183_data->plugin_exit(); } success = true; break; @@ -243,12 +203,17 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Read invalid data struct")); + return false; + } success = P183_data->plugin_read(event); // Delegate to data_struct break; } case PLUGIN_WRITE: { - P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Write invalid data struct")); @@ -262,37 +227,29 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (equals(subcmd, F("write"))) { // Write a value to a Modbus register - int address = parseString(string, 3).toInt(); - uint16_t value = parseString(string, 4).toInt(); + int address = event->Par2; + uint16_t value = event->Par3; P183_data->writeRegister(address, value); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus: write value "); - log += value; - log += F(" to address "); - log += address; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus: write value %u to address 0x%04x"), value, address)); } success = true; } else if (equals(subcmd, F("read"))) { // Read a value from a Modbus register - int address = parseString(string, 3).toInt(); + int address = event->Par2; uint16_t value = 0; value = P183_data->readRegisterWait(address); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus: read value "); - log += value; - log += F(" from address "); - log += address; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus: read value %u from address 0x%04x"), value, address)); } success = true; } else if (equals(subcmd, F("dump"))) { - int start_address = parseString(string, 3).toInt(); - int end_address = parseString(string, 4).toInt(); + int start_address = event->Par2; + int end_address = event->Par3; if (end_address < start_address) { end_address = start_address; @@ -318,7 +275,13 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_GET_CONFIG_VALUE: { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); - const String cmd = parseString(string, 1); + + if (P183_data == nullptr) { + addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Get config invalid data struct")); + return false; + } + + const String cmd = parseString(string, 1); if (equals(cmd, F("register"))) { int address = parseString(string, 2).toInt(); diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 8fe3021f26..70d24648a9 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -7,7 +7,7 @@ # include "modbus_link.h" # include "modbus_mgr.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -19,18 +19,25 @@ const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusDEVICE_struct::~ModbusDEVICE_struct() { - reset(); + if (_modbus_link != nullptr) { + _modbus_link->freeTransactions(this); // Make sure all queued transactions for this device are freed to prevent callbacks to a + // destructed object + } + ModbusMGR_singleton.disconnect(_deviceID); + _modbus_link = nullptr; + _deviceID = 0; + _modbus_address = MODBUS_BROADCAST_ADDRESS; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::reset() { if (_modbus_link != nullptr) { - _modbus_link->freeTransactions(this); - ModbusMGR_singleton.disconnect(_deviceID); - _modbus_link = nullptr; + _modbus_link->freeTransactions(this); // Make sure all queued transactions for this device are freed to prevent callbacks to a + // destructed object } - _deviceID = 0; - + ModbusMGR_singleton.disconnect(_deviceID); + _modbus_link = nullptr; + _deviceID = 0; _modbus_address = MODBUS_BROADCAST_ADDRESS; } @@ -59,13 +66,8 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("---> ModbusDevice Init: Slave address = "); - log += slaveAddress; - log += F(", This = "); - log += (size_t)this; - log += F(", deviceID = "); - log += _deviceID; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, + strformat(F("ModbusDevice Init: Slave address = %u, This = %p, deviceID = %u"), slaveAddress, this, _deviceID)); } # endif // MODBUS_DEBUG return success; @@ -93,7 +95,18 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t *valuePtr, - ModbusResultState_t *statePtr) + ModbusResultState_t *statePtr) { + return readModuleHoldingRegister(_modbus_address, address, valuePtr, statePtr); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modubus holding register from another module on teh bus. The result will be available later. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t *valuePtr, + ModbusResultState_t *statePtr) { if (_modbus_link == nullptr) { return false; @@ -103,10 +116,10 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; request->_userData = valuePtr; request->_userState = statePtr; - request->_sendframe[0] = _modbus_address; + request->_sendframe[0] = busAddress; request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; - request->_sendframe[2] = highByte(address); - request->_sendframe[3] = lowByte(address); + request->_sendframe[2] = highByte(registerAddress); + request->_sendframe[3] = lowByte(registerAddress); request->_sendframe[4] = 0; request->_sendframe[5] = 1; // Read 1 register uint16_t crc = CalculateCRC(request->_sendframe, 6); @@ -213,14 +226,18 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) case ModbusTransactionType::NONE: { // Error condition, this transaction type should not be queued + # ifdef MODBUS_DEBUG log += F(" Invalid transaction type"); + # endif // MODBUS_DEBUG break; } default: { // Error condition, missed a transaction type + # ifdef MODBUS_DEBUG log += F(" Unknown transaction type"); + # endif // MODBUS_DEBUG break; } } @@ -269,8 +286,7 @@ void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { } addLogMove(LOG_LEVEL_INFO, log); } + # endif // MODBUS_DEBUG } -# endif // MODBUS_DEBUG - #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index aee9d36d03..e9f342374a 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -69,6 +69,11 @@ struct ModbusDEVICE_struct { bool writeSingleRegister(uint16_t address, uint16_t value, ModbusResultState_t *stateptr); + + bool readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t *valuePtr, + ModbusResultState_t *statePtr); private: diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 3163b589c5..18b4e9f90a 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -12,7 +12,7 @@ # include "Modbus_device.h" # include "Modbus_link.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -160,11 +160,8 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("---> Modbus queue request: request ID = "); - log += transaction->_id; - log += F(", State = "); - log += uint(transaction->_state); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_link: Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); } # endif // MODBUS_DEBUG transaction->_state = ModbusQueueState::QUEUED; // Initial state @@ -266,14 +263,9 @@ void ModbusLINK_struct::processCommand() void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { # ifdef MODBUS_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("[ ID="); - - log += el->_id; - log += F(", Device="); - log += String((uint32_t)(el->_device), HEX); - log += F(", State="); - log += (uint)el->_state; + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = strformat(F("[ID=%u, Device=%p, State="), el->_id, el->_device); + log += formatState(el->_state); log += F(", TX="); for (int i = 0; i < el->_sendframe_length; i++) { @@ -295,33 +287,38 @@ void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("--->Modbus State= "); - - switch (state) { - case ModbusQueueState::NOT_QUEUED: - log += F("NOT_QUEUED"); - break; - case ModbusQueueState::QUEUED: - log += F("QUEUED"); - break; - case ModbusQueueState::MESSAGE_SENT: - log += F("MESSAGE_SENT"); - break; - case ModbusQueueState::RESPONSE_RECEIVED: - log += F("RESPONSE_RECEIVED"); - break; - case ModbusQueueState::ERROR_OCCURRED: - log += F("ERROR_OCCURRED"); - break; - case ModbusQueueState::READY_FOR_DESTROY: - log += F("READY_FOR_DESTROY"); - break; - } - + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("---> Modbus State= "); + log += formatState(state); addLogMove(LOG_LEVEL_INFO, log); } # endif // MODBUS_DEBUG } +String ModbusLINK_struct::formatState(ModbusQueueState_t state) { + String log; + + switch (state) { + case ModbusQueueState::NOT_QUEUED: + log += F("NOT_QUEUED"); + break; + case ModbusQueueState::QUEUED: + log += F("QUEUED"); + break; + case ModbusQueueState::MESSAGE_SENT: + log += F("MESSAGE_SENT"); + break; + case ModbusQueueState::RESPONSE_RECEIVED: + log += F("RESPONSE_RECEIVED"); + break; + case ModbusQueueState::ERROR_OCCURRED: + log += F("ERROR_OCCURRED"); + break; + case ModbusQueueState::READY_FOR_DESTROY: + log += F("READY_FOR_DESTROY"); + break; + } + return log; +} + #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 72a4ceca71..62c4015f9c 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -92,8 +92,9 @@ struct ModbusLINK_struct { private: - static void dumpQueueElement(Modbus_RequestQueueElement *el); - static void dumpState(ModbusQueueState_t state); + static void dumpQueueElement(Modbus_RequestQueueElement *el); + static void dumpState(ModbusQueueState_t state); + static String formatState(ModbusQueueState_t state); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 1dae40d368..05bb59d3db 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -129,7 +129,6 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { ModbusLinkInfo_struct *linkInfoPtr = _modbus_devices[i]->link; // Remove the device entry - delete _modbus_devices[i]; _modbus_devices[i] = nullptr; // Check if any other devices are using the same link @@ -146,7 +145,6 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { // No other devices are using this link, so we can delete it for (int k = 0; k < 5; k++) { if (_modbus_links[k] == linkInfoPtr) { - delete _modbus_links[k]->link; delete _modbus_links[k]; _modbus_links[k] = nullptr; break; diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index f9450cf473..43c29dc91f 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -5,10 +5,6 @@ // ####################################################################################################### // ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### - -# define P183_NR_OUTPUTS PCONFIG(3) -# define P183_ADDRESS(x) PCONFIG(4 + x) - # ifdef BUILD_NO_DEBUG # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -20,7 +16,10 @@ P183_data_struct::P183_data_struct(struct EventStruct *event) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// P183_data_struct::~P183_data_struct() { - plugin_exit(); // Destruct dynamic structures contained in this object + if (_modbusDevice != nullptr) { + delete _modbusDevice; + _modbusDevice = nullptr; + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -53,17 +52,14 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, # ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("P183: Init serial: RX pin "); - log += CONFIG_PIN1; - log += F(", TX pin "); - log += CONFIG_PIN2; - log += F(", RS485 mode selected on pin "); - log += P183_DEPIN; - log += F(", baudrate "); - log += P183_storageValueToBaudrate(P183_BAUDRATE); - log += F(", collision detection "); - log += P183_GET_FLAG_COLL_DETECT ? F("enabled") : F("disabled"); - addLogMove(LOG_LEVEL_DEBUG, log); + addLogMove(LOG_LEVEL_DEBUG, + strformat(F("P183: Init address %d, RX pin %d, TX pin %d, RS485 mode selected on pin %d, baudrate %d, collision detection %s"), + slaveAddress, + serial_rx, + serial_tx, + dere_pin, + baudrate, + collision_detect ? F("enabled") : F("disabled"))); } # endif // ifdef P183_DEBUG @@ -74,20 +70,21 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::plugin_exit() { - if (_modbusDevice == nullptr) { + if (_modbusDevice != nullptr) { delete _modbusDevice; + _modbusDevice = nullptr; } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool P183_data_struct::plugin_once_a_second(struct EventStruct *event) { - // TODO + // No actions return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool P183_data_struct::plugin_ten_per_second(struct EventStruct *event) { - if (nullptr != _modbusDevice) { + if (_modbusDevice != nullptr) { _modbusDevice->processCommand(); } @@ -113,7 +110,6 @@ bool P183_data_struct::plugin_read(struct EventStruct *event) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { - String log; uint16_t value = 0; ModbusResultState_t state = ModbusResultState::BUSY; @@ -132,18 +128,11 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } - log += F("** Address "); - log += String(reg); - log += F(" (0x"); - log += String(reg, HEX); - - if (result == 0) { - log += F(") = "); - log += String(value); + if (state == ModbusResultState::SUCCESS) { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), reg, reg, value, value)); } else { - log += F(") invalid"); + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) no response"), reg, reg)); } - addLogMove(LOG_LEVEL_INFO, log); } } @@ -151,7 +140,6 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e // Scan Modbus addreses from 0x00 to 0xFF for a given node ID void P183_data_struct::scan_modbus() { - String log; uint16_t value = 0; ModbusResultState_t state = ModbusResultState::BUSY; @@ -162,20 +150,13 @@ void P183_data_struct::scan_modbus() } for (uint8_t id = 0; id <= 247; id++) { - _modbusDevice->readHoldingRegister(1, &value, &state); + _modbusDevice->readModuleHoldingRegister(id, 1, &value, &state); while (state == ModbusResultState::BUSY) { _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } - log += F("** Address "); - log += String(id); - - if (state == ModbusResultState::SUCCESS) { - log += F(" OK"); - } else { - log += F(" no response"); - } - addLogMove(LOG_LEVEL_INFO, log); + String s = state == ModbusResultState::SUCCESS ? F(" OK") : F(" no response"); + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) %s"), id, id, s)); } } diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index a32df4905e..3d3636cd95 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -6,10 +6,40 @@ # include "../Helpers/Modbus_device.h" +// Plugin configuration parameters +// PCONFIG(0) is the Modbus device ID. +// PCONFIG(1) is the serial baud rate. +// PCONFIG(2) is used for flags, where bit 0 indicates collision detection +// PCONFIG(3) is the number of active output values (1-4) +// PCONFIG(4) is the Modbus register address for value 1 +// PCONFIG(5) is the Modbus register address for value 2 +// PCONFIG(6) is the Modbus register address for value 3 +// PCONFIG(7) is the Modbus register address for value 4 +// Use P183_ADDRESS(x) to access the PCONFIG value for value x +# define P183_DEV_ID PCONFIG(0) +# define P183_DEV_ID_LABEL PCONFIG_LABEL(0) +# define P183_BAUDRATE PCONFIG(1) +# define P183_BAUDRATE_LABEL PCONFIG_LABEL(1) +# define P183_NR_OUTPUTS PCONFIG(3) +# define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) +# define P183_ADDRESS(x) PCONFIG(4 + x) +# define P183_ADDRESS_LABEL(x) concat(F("addr"), x) + +# define P183_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) +# define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) +# define P183_FLAG_COLL_DETECT_LABEL "colldet" + +# define P183_DEPIN CONFIG_PIN3 + +# define P183_DEV_ID_DFLT 1 +# define P183_BAUDRATE_DFLT 3 // 9600 baud +# define P183_MAX_BAUDRATE_SEL 8 # define P183_MODBUS_TIMEOUT 1000 // milliseconds +# define P183_MAX_MODBUS_NODES 247 +# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address // The default set of single-value VType options -constexpr uint8_t P183_START_VTYPE = 0; +// constexpr uint8_t P183_START_VTYPE = 0; struct P183_data_struct : public PluginTaskData_base { P183_data_struct(struct EventStruct *event); From 7795cc11005e21db03339fad017edbebeb30edd2 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sat, 18 Oct 2025 21:21:33 +0200 Subject: [PATCH 14/48] P183 Several fixes --- src/_P183_modbus.ino | 11 ++- src/src/Helpers/Modbus_device.cpp | 90 +++++++++-------- src/src/Helpers/Modbus_link.cpp | 4 +- src/src/Helpers/Modbus_mgr.cpp | 109 ++++++++++++++++----- src/src/Helpers/Modbus_mgr.h | 23 +++-- src/src/PluginStructs/P183_data_struct.cpp | 6 +- 6 files changed, 162 insertions(+), 81 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index c975667c3c..96d229df19 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -205,7 +205,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Read invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, F("P183 Modbus: Read invalid data struct")); return false; } success = P183_data->plugin_read(event); // Delegate to data_struct @@ -216,7 +216,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Write invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, F("P183: Modbus: Write invalid data struct")); return false; } @@ -266,6 +266,11 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data->scan_modbus(); success = true; } + else if (equals(subcmd, F("debug"))) { + // Dump Modbus admin info + ModbusMGR_singleton.dumpAdminInfo(); + success = true; + } else { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); } @@ -277,7 +282,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("******* Modbus: Get config invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, F("P183 Modbus: Get config invalid data struct")); return false; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 70d24648a9..007adcc9e3 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -7,7 +7,7 @@ # include "modbus_link.h" # include "modbus_mgr.h" -////# define MODBUS_DEBUG +//# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -127,7 +127,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddr request->_sendframe[7] = highByte(crc); // CRC high byte request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 7; // Expect 8 bytes in response - dump_buffer(request->_sendframe, request->_sendframe_length); + ////dump_buffer(request->_sendframe, request->_sendframe_length); uint16_t queueID = _modbus_link->queueTransaction(request); *statePtr = ModbusResultState::BUSY; @@ -159,7 +159,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_sendframe[7] = highByte(crc); // CRC high byte request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 8; // Expect 8 bytes in response - dump_buffer(request->_sendframe, request->_sendframe_length); + ////dump_buffer(request->_sendframe, request->_sendframe_length); uint16_t queueID = _modbus_link->queueTransaction(request); *statePtr = ModbusResultState::BUSY; @@ -188,63 +188,73 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) } # ifdef MODBUS_DEBUG - String log = F("---> Device callback: "); - log += req->_id; - log += F(", Message = "); - log += static_cast(req->_messageType); + String log = strformat(F("Modbus device callback: device= %d, Request= %d, Message= %d"), + _deviceID, + req->_id, + static_cast(req->_messageType) + ); # endif // MODBUS_DEBUG - switch (req->_messageType) { - case ModbusTransactionType::READ_HOLDING_REGISTERS: - { - if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { - uint16_t crc = CalculateCRC(req->_rcvframe, 5); - - if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { - // Valid response - if (req->_userData != nullptr) { - *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte - resultState = ModbusResultState::SUCCESS; + if (req->_state == ModbusQueueState::ERROR_OCCURRED) { + # ifdef MODBUS_DEBUG + log += F(" Link error occurred"); + # endif // MODBUS_DEBUG + } + else { + switch (req->_messageType) { + case ModbusTransactionType::READ_HOLDING_REGISTERS: + { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { + uint16_t crc = CalculateCRC(req->_rcvframe, 5); + + if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + // Valid response + if (req->_userData != nullptr) { + *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + resultState = ModbusResultState::SUCCESS; + } } } + break; } - break; - } - case ModbusTransactionType::WRITE_SINGLE_REGISTER: - { - if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { - uint16_t crc = CalculateCRC(req->_rcvframe, 5); + case ModbusTransactionType::WRITE_SINGLE_REGISTER: + { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { + uint16_t crc = CalculateCRC(req->_rcvframe, 5); - if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { - resultState = ModbusResultState::SUCCESS; + if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + resultState = ModbusResultState::SUCCESS; + } } + break; } - break; - } - case ModbusTransactionType::NONE: - { - // Error condition, this transaction type should not be queued + case ModbusTransactionType::NONE: + { + // Error condition, this transaction type should not be queued # ifdef MODBUS_DEBUG - log += F(" Invalid transaction type"); + log += F(" Invalid transaction type"); # endif // MODBUS_DEBUG - break; - } + break; + } - default: - { - // Error condition, missed a transaction type + default: + { + // Error condition, missed a transaction type # ifdef MODBUS_DEBUG - log += F(" Unknown transaction type"); + log += F(" Unknown transaction type"); # endif // MODBUS_DEBUG - break; + break; + } } } *(static_cast(req->_userState)) = resultState; _modbus_link->freeTransaction(req); # ifdef MODBUS_DEBUG + log += F(", Result = "); + log += (resultState == ModbusResultState::SUCCESS) ? F("SUCCESS") : F("ERROR"); addLogMove(LOG_LEVEL_INFO, log); # endif // MODBUS_DEBUG } @@ -275,7 +285,7 @@ void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("---> Modbus: Dumping buffer: "); + String log = F("Modbus device: Dumping buffer: "); for (size_t i = 0; i < length; ++i) { log += String(buffer[i], HEX); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 18b4e9f90a..5dbf5fb5f5 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -12,7 +12,7 @@ # include "Modbus_device.h" # include "Modbus_link.h" -////# define MODBUS_DEBUG +//# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -288,7 +288,7 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("---> Modbus State= "); + String log = F("Modbus link: State= "); log += formatState(state); addLogMove(LOG_LEVEL_INFO, log); } diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 05bb59d3db..fb801e4fbc 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -6,12 +6,15 @@ # include # include "Modbus_mgr.h" +//# define MODBUS_DEBUG +# ifdef BUILD_NO_DEBUG +# undef MODBUS_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG -// ModbusMGR structure representing the singleton Modbus Management entity -// Thw manager has an overview of all Modbus links and the conneted devices. -// The manager allows multiple Modbus devices to connect to a single Modbus link while supporting multiple links. -// The modbus manager is not involved in the actual data transport, this is handled by a direct relation between Modbus device and -// ModbusLINK object. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Singleton administration object for Modbus manager +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct_t ModbusMGR_singleton = {}; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct::~ModbusMGR_struct() @@ -22,9 +25,7 @@ ModbusMGR_struct::~ModbusMGR_struct() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::reset() { - //////_modbus_link->; - //////delete _modbus_link; - /////_modbus_link = nullptr; + } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -49,28 +50,32 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, ModbusLinkInfo_struct *linkInfoPtr = nullptr; ModbusDeviceInfo_struct *deviceInfoPtr = nullptr; - String log = F("-MGR-> Connect: port="); + # ifdef MODBUS_DEBUG + String log = F("Modbus_mgr: Connect port="); log += (int)port; + # endif // ifdef MODBUS_DEBUG // Check if link is already used by another device - for (int i = 0; i < 5; i++) { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if ((_modbus_links[i] != nullptr) && (_modbus_links[i]->port == port)) { // Found existing link with matching port identifier linkInfoPtr = _modbus_links[i]; - log += F(" Found existing link= "); - log += i; + # ifdef MODBUS_DEBUG + log += strformat(F(", Found existing link= %d for port=%s"), i, ESPEasySerialPort_toString(_modbus_links[i]->port)); + # endif // ifdef MODBUS_DEBUG } } if (linkInfoPtr == nullptr) { linkInfoPtr = new (std::nothrow) ModbusLinkInfo_struct(); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if (_modbus_links[i] == nullptr) { _modbus_links[i] = linkInfoPtr; - log += F(" Created new link= "); - log += i; + # ifdef MODBUS_DEBUG + log += strformat(F(", Created new link= %d for port=%s"), i, ESPEasySerialPort_toString(_modbus_links[i]->port)); + # endif // ifdef MODBUS_DEBUG break; } } @@ -104,26 +109,38 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, } } - for (int i = 0; i < 16; i++) { + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { if (_modbus_devices[i] == nullptr) { // Found an available device slot _modbus_devices[i] = new (std::nothrow) ModbusDeviceInfo_struct(); - _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-16) + _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-MAX_MODBUS_DEVICES) _modbus_devices[i]->link = linkInfoPtr; *deviceID = _modbus_devices[i]->deviceID; *link = linkInfoPtr->link; + #ifdef MODBUS_DEBUG log += F(" Assigned deviceID= "); log += *deviceID; + #endif break; } } + # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::disconnect(uint8_t deviceID) { - for (int i = 0; i < 16; i++) { + dumpAdminInfo(); + # ifdef MODBUS_DEBUG + String log = F("Modbus_mgr: Disconnect device="); + + log += (int)deviceID; + # endif // ifdef MODBUS_DEBUG + + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { if ((_modbus_devices[i] != nullptr) && (_modbus_devices[i]->deviceID == deviceID)) { // Found the device to disconnect ModbusLinkInfo_struct *linkInfoPtr = _modbus_devices[i]->link; @@ -134,7 +151,7 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { // Check if any other devices are using the same link bool linkInUse = false; - for (int j = 0; j < 16; j++) { + for (int j = 0; j < MAX_MODBUS_DEVICES; j++) { if ((_modbus_devices[j] != nullptr) && (_modbus_devices[j]->link == linkInfoPtr)) { linkInUse = true; break; @@ -142,8 +159,12 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { } if (!linkInUse) { + # ifdef MODBUS_DEBUG + log += F(", No other devices using link, deleting link"); + # endif // ifdef MODBUS_DEBUG + // No other devices are using this link, so we can delete it - for (int k = 0; k < 5; k++) { + for (int k = 0; k < MAX_MODBUS_LINKS; k++) { if (_modbus_links[k] == linkInfoPtr) { delete _modbus_links[k]; _modbus_links[k] = nullptr; @@ -155,8 +176,52 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { return true; // Successfully disconnected } } + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusMGR_struct::dumpAdminInfo() +{ + addLogMove(LOG_LEVEL_INFO, F("Modbus_mgr: Dumping admin info")); + #ifdef MODBUS_DEBUG + // Iterate over the modbus links and dump their info + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] != nullptr) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: Link[%d] Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, RS485Mode=%s, CollisionDetect=%s"), + i, + ESPEasySerialPort_toString(_modbus_links[i]->port), + _modbus_links[i]->serial_rx, + _modbus_links[i]->serial_tx, + _modbus_links[i]->baudrate, + _modbus_links[i]->dere_pin, + _modbus_links[i]->rs485_mode ? F("Yes") : F("No"), + _modbus_links[i]->collision_detect ? F("Yes") : F("No") + )); + } + else { + addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Link[%d] "), i)); + } + } - return false; // TODO: implement + // Iterate over the modbus devices and dump their info + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + if (_modbus_devices[i] != nullptr) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: Device[%d] DeviceID=%d, LinkPort=%s"), + i, + _modbus_devices[i]->deviceID, + ESPEasySerialPort_toString(_modbus_devices[i]->link->port) + )); + } + else { + addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Device[%d] "), i)); + } + } + #endif // MODBUS_DEBUG } -#endif // FEAURE_MODBUS +#endif // FEATURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 0c09daab78..59713ddc61 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -14,7 +14,7 @@ // The manager allows multiple Modbus devices to connect to a single Modbus link while supporting multiple links. // The modbus manager is not involved in the actual data transport, this is handled by a direct relation between Modbus device and // ModbusLINK object. -struct ModbusMGR_struct { +typedef struct ModbusMGR_struct { ModbusMGR_struct() = default; ~ModbusMGR_struct(); @@ -39,8 +39,13 @@ struct ModbusMGR_struct { bool disconnect(uint8_t deviceID); + void dumpAdminInfo(); + private: + static const int MAX_MODBUS_LINKS = 5; // Maximum number of Modbus links supported + static const int MAX_MODBUS_DEVICES = 16; // Maximum number of Modbus devices supported + struct ModbusLinkInfo_struct { ESPEasySerialPort port = ESPEasySerialPort::not_set; int16_t serial_rx = -1; @@ -53,18 +58,16 @@ struct ModbusMGR_struct { }; struct ModbusDeviceInfo_struct { - uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager - struct ModbusDEVICE_struct *device = nullptr; // Pointer to the Modbus device object - struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info + uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager + struct ModbusDEVICE_struct *device = nullptr; // Pointer to the Modbus device object + struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info }; - ModbusLinkInfo_struct *_modbus_links[5] = {nullptr}; // Pointer to the Modbus link object - ModbusDeviceInfo_struct *_modbus_devices[16] = {nullptr}; // Array of connected Modbus devices - - ModbusLINK_struct *_modbus_link = nullptr; // Legacy, to be cleaned up -}; + ModbusLinkInfo_struct *_modbus_links[MAX_MODBUS_LINKS] = { nullptr }; // Pointer to the Modbus link object + ModbusDeviceInfo_struct *_modbus_devices[MAX_MODBUS_DEVICES] = { nullptr }; // Array of connected Modbus devices +} ModbusMGR_struct_t; -static struct ModbusMGR_struct ModbusMGR_singleton = {}; // Singleton instance of the Modbus Manager +extern ModbusMGR_struct_t ModbusMGR_singleton; // Singleton instance of the Modbus Manager #endif // FEAURE_MODBUS #endif // HELPERS_MODBUS_MGR_H diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 43c29dc91f..dc899c97f2 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -16,10 +16,8 @@ P183_data_struct::P183_data_struct(struct EventStruct *event) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// P183_data_struct::~P183_data_struct() { - if (_modbusDevice != nullptr) { - delete _modbusDevice; - _modbusDevice = nullptr; - } + delete _modbusDevice; + _modbusDevice = nullptr; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// From 6e201fbd009405a78660a9277b083e2085ea7aaa Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:09:56 +0200 Subject: [PATCH 15/48] P183 Update logging --- src/_P183_modbus.ino | 14 +++++++------- src/src/Helpers/Modbus_device.cpp | 8 ++++---- src/src/Helpers/Modbus_link.cpp | 12 ++++++------ src/src/Helpers/Modbus_mgr.cpp | 6 +++--- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 96d229df19..e167a6fb5e 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -182,7 +182,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_GET_FLAG_COLL_DETECT); } else { - addLog(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); + addLogMove(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); } success = true; @@ -205,7 +205,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("P183 Modbus: Read invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read invalid data struct")); return false; } success = P183_data->plugin_read(event); // Delegate to data_struct @@ -216,7 +216,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("P183: Modbus: Write invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus write invalid data struct")); return false; } @@ -232,7 +232,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data->writeRegister(address, value); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus: write value %u to address 0x%04x"), value, address)); + addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus write value %u to address 0x%04x"), value, address)); } success = true; } @@ -243,7 +243,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterWait(address); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus: read value %u from address 0x%04x"), value, address)); + addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus read value %u from address 0x%04x"), value, address)); } success = true; } @@ -272,7 +272,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) success = true; } else { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Unknown command")); + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Unknown command")); } } @@ -282,7 +282,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("P183 Modbus: Get config invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config invalid data struct")); return false; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 007adcc9e3..08c5c3a6a3 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -67,7 +67,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("ModbusDevice Init: Slave address = %u, This = %p, deviceID = %u"), slaveAddress, this, _deviceID)); + strformat(F("Modbus: Device Init, Slave address = %u, This = %p, deviceID = %u"), slaveAddress, this, _deviceID)); } # endif // MODBUS_DEBUG return success; @@ -183,12 +183,12 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) ModbusResultState_t resultState = ModbusResultState::ERROR; // Default to error unless proven otherwise if (req == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("Modbus ERROR: Null pointer passed in callback")); + addLogMove(LOG_LEVEL_INFO, F("Modbus: ERROR, Null pointer passed in callback")); return; } # ifdef MODBUS_DEBUG - String log = strformat(F("Modbus device callback: device= %d, Request= %d, Message= %d"), + String log = strformat(F("Modbus: Device callback, device= %d, Request= %d, Message= %d"), _deviceID, req->_id, static_cast(req->_messageType) @@ -285,7 +285,7 @@ void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus device: Dumping buffer: "); + String log = F("Modbus: Device, Dumping buffer: "); for (size_t i = 0; i < length; ++i) { log += String(buffer[i], HEX); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 5dbf5fb5f5..c04f6f4ac2 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -80,7 +80,7 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("Modbus_link: Init serial: RX pin "); + String log = F("Modbus: Link, Init serial, RX pin "); log += serial_rx; log += F(", TX pin "); log += serial_tx; @@ -135,7 +135,7 @@ bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) return true; } else { - addLogMove(LOG_LEVEL_ERROR, F("Modbus_link: Attempt to free null transaction")); + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to free null transaction")); return false; } } @@ -161,7 +161,7 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus_link: Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); + strformat(F("Modbus: Link, Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); } # endif // MODBUS_DEBUG transaction->_state = ModbusQueueState::QUEUED; // Initial state @@ -177,7 +177,7 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac void ModbusLINK_struct::processCommand() { if (_easySerial == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("Modbus_link: Serial port not initialized")); + addLogMove(LOG_LEVEL_INFO, F("Modbus: Link, Serial port not initialized")); return; // Serial port not initialized } @@ -264,7 +264,7 @@ void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = strformat(F("[ID=%u, Device=%p, State="), el->_id, el->_device); + String log = strformat(F("Modbus: [ID=%u, Device=%p, State="), el->_id, el->_device); log += formatState(el->_state); log += F(", TX="); @@ -288,7 +288,7 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus link: State= "); + String log = F("Modbus: Link, State= "); log += formatState(state); addLogMove(LOG_LEVEL_INFO, log); } diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index fb801e4fbc..3e3ab115a9 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -51,7 +51,7 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, ModbusDeviceInfo_struct *deviceInfoPtr = nullptr; # ifdef MODBUS_DEBUG - String log = F("Modbus_mgr: Connect port="); + String log = F("Modbus: Manager, Connect port="); log += (int)port; # endif // ifdef MODBUS_DEBUG @@ -135,7 +135,7 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, bool ModbusMGR_struct::disconnect(uint8_t deviceID) { dumpAdminInfo(); # ifdef MODBUS_DEBUG - String log = F("Modbus_mgr: Disconnect device="); + String log = F("Modbus: Manager, Disconnect device="); log += (int)deviceID; # endif // ifdef MODBUS_DEBUG @@ -185,7 +185,7 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::dumpAdminInfo() { - addLogMove(LOG_LEVEL_INFO, F("Modbus_mgr: Dumping admin info")); + addLogMove(LOG_LEVEL_INFO, F("Modbus: Manager, Dumping admin info")); #ifdef MODBUS_DEBUG // Iterate over the modbus links and dump their info for (int i = 0; i < MAX_MODBUS_LINKS; i++) { From 280e8bf45447fa18a859b81a24cb4bf0beae0bc2 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:39:13 +0200 Subject: [PATCH 16/48] P183: Init link fix & minor updates --- src/src/Helpers/Modbus_link.cpp | 28 +++++--------- src/src/Helpers/Modbus_link.h | 3 +- src/src/Helpers/Modbus_mgr.cpp | 65 +++++++++++++++++---------------- 3 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index c04f6f4ac2..38e2112810 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -265,7 +265,7 @@ void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = strformat(F("Modbus: [ID=%u, Device=%p, State="), el->_id, el->_device); - log += formatState(el->_state); + log += toString(el->_state); log += F(", TX="); for (int i = 0; i < el->_sendframe_length; i++) { @@ -289,36 +289,28 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Modbus: Link, State= "); - log += formatState(state); + log += toString(state); addLogMove(LOG_LEVEL_INFO, log); } # endif // MODBUS_DEBUG } -String ModbusLINK_struct::formatState(ModbusQueueState_t state) { - String log; - +const __FlashStringHelper* toString(ModbusQueueState_t state) { switch (state) { case ModbusQueueState::NOT_QUEUED: - log += F("NOT_QUEUED"); - break; + return F("NOT_QUEUED"); case ModbusQueueState::QUEUED: - log += F("QUEUED"); - break; + return F("QUEUED"); case ModbusQueueState::MESSAGE_SENT: - log += F("MESSAGE_SENT"); - break; + return F("MESSAGE_SENT"); case ModbusQueueState::RESPONSE_RECEIVED: - log += F("RESPONSE_RECEIVED"); - break; + return F("RESPONSE_RECEIVED"); case ModbusQueueState::ERROR_OCCURRED: - log += F("ERROR_OCCURRED"); - break; + return F("ERROR_OCCURRED"); case ModbusQueueState::READY_FOR_DESTROY: - log += F("READY_FOR_DESTROY"); - break; + return F("READY_FOR_DESTROY"); } - return log; + return F(""); } #endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 62c4015f9c..b0566031b6 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -24,6 +24,8 @@ typedef enum class ModbusQueueState { READY_FOR_DESTROY = 5 // Element is marked for deletion and can be freed } ModbusQueueState_t; +const __FlashStringHelper* toString(ModbusQueueState_t state); + // Types of Modbus transactions supported by the Modbuss_device // This enumeration is used by the Modbus device to indicate which transaction is associated with the queue element. // See Modbus specification for details on function codes. @@ -94,7 +96,6 @@ struct ModbusLINK_struct { static void dumpQueueElement(Modbus_RequestQueueElement *el); static void dumpState(ModbusQueueState_t state); - static String formatState(ModbusQueueState_t state); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 3e3ab115a9..75349c56a1 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -6,7 +6,7 @@ # include # include "Modbus_mgr.h" -//# define MODBUS_DEBUG +// # define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -14,7 +14,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Singleton administration object for Modbus manager ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -ModbusMGR_struct_t ModbusMGR_singleton = {}; +ModbusMGR_struct_t ModbusMGR_singleton = {}; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct::~ModbusMGR_struct() @@ -25,7 +25,6 @@ ModbusMGR_struct::~ModbusMGR_struct() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::reset() { - } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -81,30 +80,31 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, } } - if (linkInfoPtr != nullptr) { - if (linkInfoPtr->link == nullptr) { + if (linkInfoPtr != nullptr) { // Sanity check for successful link admin creation + if (linkInfoPtr->link == nullptr) { // Check if link object already exists // No existing link, create a new one linkInfoPtr->link = new (std::nothrow) ModbusLINK_struct(); + } - if (linkInfoPtr->link != nullptr) { - // Initialize the new link - if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { - delete linkInfoPtr->link; - linkInfoPtr->link = nullptr; - delete linkInfoPtr; - linkInfoPtr = nullptr; - return false; // Initialization failed - } - else { - // Store the link parameters - linkInfoPtr->port = port; - linkInfoPtr->serial_rx = serial_rx; - linkInfoPtr->serial_tx = serial_tx; - linkInfoPtr->baudrate = baudrate; - linkInfoPtr->dere_pin = dere_pin; - linkInfoPtr->rs485_mode = (dere_pin != -1); - linkInfoPtr->collision_detect = collision_detect; - } + if (linkInfoPtr->link != nullptr) { // Sanity check for successful creation + // (re)initialize the new link + if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + // Initialization failed, clean up + delete linkInfoPtr->link; + linkInfoPtr->link = nullptr; + delete linkInfoPtr; + linkInfoPtr = nullptr; + return false; // Initialization failed + } + else { + // Store the link parameters + linkInfoPtr->port = port; + linkInfoPtr->serial_rx = serial_rx; + linkInfoPtr->serial_tx = serial_tx; + linkInfoPtr->baudrate = baudrate; + linkInfoPtr->dere_pin = dere_pin; + linkInfoPtr->rs485_mode = (dere_pin != -1); + linkInfoPtr->collision_detect = collision_detect; } } } @@ -117,10 +117,10 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, _modbus_devices[i]->link = linkInfoPtr; *deviceID = _modbus_devices[i]->deviceID; *link = linkInfoPtr->link; - #ifdef MODBUS_DEBUG - log += F(" Assigned deviceID= "); - log += *deviceID; - #endif + # ifdef MODBUS_DEBUG + log += F(" Assigned deviceID= "); + log += *deviceID; + # endif // ifdef MODBUS_DEBUG break; } } @@ -137,7 +137,7 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { # ifdef MODBUS_DEBUG String log = F("Modbus: Manager, Disconnect device="); - log += (int)deviceID; + log += deviceID; # endif // ifdef MODBUS_DEBUG for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { @@ -186,7 +186,8 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { void ModbusMGR_struct::dumpAdminInfo() { addLogMove(LOG_LEVEL_INFO, F("Modbus: Manager, Dumping admin info")); - #ifdef MODBUS_DEBUG + # ifdef MODBUS_DEBUG + // Iterate over the modbus links and dump their info for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if (_modbus_links[i] != nullptr) { @@ -221,7 +222,7 @@ void ModbusMGR_struct::dumpAdminInfo() addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Device[%d] "), i)); } } - #endif // MODBUS_DEBUG + # endif // ifdef MODBUS_DEBUG } -#endif // FEATURE_MODBUS_FAC +#endif // if FEATURE_MODBUS_FAC From 4515c8cdf84a2b5606ad149abbbf548253ccaffb Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:28:44 +0200 Subject: [PATCH 17/48] P183: initial UML design documentation --- misc/modbusFacility/Modbus_class.puml | 42 ++++++++++++++++++++ misc/modbusFacility/Modbus_seq1.puml | 56 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 misc/modbusFacility/Modbus_class.puml create mode 100644 misc/modbusFacility/Modbus_seq1.puml diff --git a/misc/modbusFacility/Modbus_class.puml b/misc/modbusFacility/Modbus_class.puml new file mode 100644 index 0000000000..fe6f2ea0f7 --- /dev/null +++ b/misc/modbusFacility/Modbus_class.puml @@ -0,0 +1,42 @@ +@startuml + +class plugin { + +} + +class plugin_struct { + +} + + class Modbus_mgr <> { + + } + + class Modbus_link { + } + + class Modbus_device { + + } + +class Serial_port { + } + +class Queue { + +} + +class Transaction { + +} + +plugin *-- plugin_struct +plugin_struct *-- Modbus_device +Modbus_device "*" -right-> "1" Modbus_link +Modbus_link "1" -right-> "1" Serial_port +Modbus_mgr "1" *-- "*" Modbus_link +Modbus_mgr "1" -- "*" Modbus_device +Modbus_link *-- "1" Queue +Queue o-- "*" Transaction +Modbus_device "1" --> "*" Transaction +@enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq1.puml b/misc/modbusFacility/Modbus_seq1.puml new file mode 100644 index 0000000000..f3f1882271 --- /dev/null +++ b/misc/modbusFacility/Modbus_seq1.puml @@ -0,0 +1,56 @@ +@startuml +actor System as system +participant Plugin as plugin +participant Plugin_struct as plugin_struct +participant Modbus_device as modbus_device +participant Modbus_mgr as modbus_mgr +participant Modbus_link as modbus_link +queue Queue as queue + +system -> plugin ++ : init +create plugin_struct +plugin -> plugin_struct ++ : new() +create modbus_device +plugin_struct -> modbus_device ++ : new() +modbus_device -> modbus_mgr ++ : connect() +alt link not exists + create modbus_link +modbus_mgr -> modbus_link : new() +end +modbus_mgr -> modbus_link : init() +modbus_device <-- modbus_mgr -- : link +plugin_struct <-- modbus_device -- +plugin <-- plugin_struct -- +system <-- plugin -- + +system -> plugin ++ : read +plugin -> plugin_struct ++ : read() +plugin_struct -> modbus_device ++ : readHoldingRegister() +modbus_device -> modbus_link ++: queueTransaction() +modbus_link -> queue : add +modbus_device <-- modbus_link -- +plugin_struct <-- modbus_device -- +plugin_struct -> plugin_struct : retrieve latest value +plugin <-- plugin_struct -- +system <-- plugin -- + +system -> plugin ++ : ten_per_second +plugin -> plugin_struct ++ : ten_per_second() +plugin_struct -> modbus_device ++ : processCommand() +modbus_device -> modbus_link ++: processCommand() +alt no transaction + modbus_link -> queue : get next transaction + alt transaction available + modbus_link -> modbus_link : send request + end +end +alt response available + modbus_link -> modbus_device ++ : linkCallback() + modbus_device -> plugin_struct : update latest value + modbus_link <-- modbus_device -- +end +modbus_device <-- modbus_link -- +plugin_struct <-- modbus_device -- +plugin <-- plugin_struct -- +system <-- plugin -- +@enduml \ No newline at end of file From 21b427ecf4fda07e57eb110db3e61a61b97b6d90 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:35:02 +0100 Subject: [PATCH 18/48] P183: Minor fixes and start of design documentation --- misc/modbusFacility/Modbus_notes.txt | 34 ++++++++++++++++++++++ misc/modbusFacility/Modbus_seq1.puml | 10 +++---- src/_P183_modbus.ino | 8 ++--- src/src/Helpers/Modbus_device.cpp | 5 ++-- src/src/Helpers/Modbus_link.cpp | 4 +-- src/src/PluginStructs/P183_data_struct.cpp | 2 +- src/src/PluginStructs/P183_data_struct.h | 5 ++++ 7 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 misc/modbusFacility/Modbus_notes.txt diff --git a/misc/modbusFacility/Modbus_notes.txt b/misc/modbusFacility/Modbus_notes.txt new file mode 100644 index 0000000000..9cdf7a9b5e --- /dev/null +++ b/misc/modbusFacility/Modbus_notes.txt @@ -0,0 +1,34 @@ +The MODBUS_FAC facilities intend to provide a framework that allows multiple Modbus RTU devices to connect to a single physical Modbus link. It supports multiple physical Modbus links in parallel. +This is realized by introducing 3 classes: +- Modbus_device: Represents a single device. The Modbus_device is responsibel for coding/decoding the Modus protocol. +- Modbus_link: Represents a single Modbus conenction. Each Modbus_link object is connected to a separate serial link. +- Modbus_mgr: Singleton manager that manages all links and keeps track of which devices are connected to which link. + +The main design requirement is to allow multiple plugins to use the same Modbus link. This is achieved by separating the link control from the device control. +Modbus connunication consists of a request-reply message exchange over a serial link. After a request is sent the bus is occupied until the reply is received or a timeout occurred. +A matching request-reply message pair is called a transaction in MODBUS_FAC. + +The main design constraint for the MODBUS_FAC is to reduce the time waisted in waiting for a response message. For this a transaction queue is used. A device puts a request message on the queue and does not wait fro the reply. +Once the matching reply is received the device is notified by a callback function. + +The Modbus_device uses transactions to communicate to the associated hardware module. Messages are encoded in Modbus RTU protocol format by the Modbus_device. +Once the request message is created it is transferred to the Modbus_link where it is added the the transaction queue. +When the transaction is done, either a response message is received or a timeout occurred, the Modbus_device callback function is called and the response message is decoded and evaluated. + +The Modbus_link uses a queue to handle transactions sequentially. Each transaction has a state to track if it is queued, being processed, or completed. At maximum one transaction is being processed at a time. +The modbus_link uses a ESPEasy serial link for the actual transmit and receive of the messages. + +To match a Modbus_device with a Modbus_link a singleton Modus_mgr is used. This Modbus_mgr owns all Modbus_links and determines to which link a Modbus_device is assigned. +In case no matching link is found the Modbus_mgr creates a corresponding Modbus_link. Currently ESPEasy has no means to configure the serial link used by the Modbus_link. Instead the link is configured in the plugin. +The plugin passes the configuration to the device. When the Modbus_device requests the Modbus_mgr to connect to a Modbus_link it passes the serial parameters. The Modbus_mgr uses the serial port type to distinguish the links. +Whenever a new device is connected to a Modbus_link the link will be reprogrammed with the new serial parameters. + +In future a separate configuration screen can be implemented to configure each Modus_link. Once this is available the Modbus_device can mention the link identification without the need to pass all configuration parameters. + +Drawback of the queue mechanism is the need for the device (and the associated plugin) to wait for the response message without blocking the CPU. For this the device needs to track "state". +Either the plugin can queue only a single transaction or it must be able to track for which transaction in the queue a response is received. +To keep this state tracking generic the transaction contains a pointer to a userState and userData. The Modbus_device can use these values depending on the transaction type to return state information to the plugin. + +The owning plugin can only return a value once it is received. For repetitive read events the previous received value can be returned. +For specific read commands either a polling mechanism must be implemented or the plugin blocks until the response is received. +A dedicated trigger event for the plugins can reduce the CPU load by removing the need for regular polling (e.g. tem_per_second) or blocking. \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq1.puml b/misc/modbusFacility/Modbus_seq1.puml index f3f1882271..e4b770d038 100644 --- a/misc/modbusFacility/Modbus_seq1.puml +++ b/misc/modbusFacility/Modbus_seq1.puml @@ -38,17 +38,17 @@ system -> plugin ++ : ten_per_second plugin -> plugin_struct ++ : ten_per_second() plugin_struct -> modbus_device ++ : processCommand() modbus_device -> modbus_link ++: processCommand() +alt response available + modbus_link -> modbus_device ++ : linkCallback() + modbus_device -> plugin_struct : update latest value + modbus_link <-- modbus_device -- +end alt no transaction modbus_link -> queue : get next transaction alt transaction available modbus_link -> modbus_link : send request end end -alt response available - modbus_link -> modbus_device ++ : linkCallback() - modbus_device -> plugin_struct : update latest value - modbus_link <-- modbus_device -- -end modbus_device <-- modbus_link -- plugin_struct <-- modbus_device -- plugin <-- plugin_struct -- diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index e167a6fb5e..1d5bef42bb 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -17,17 +17,13 @@ /** * Changelog: - * 2025-08-24 flashmark: Initial version * 2025-10-12 flashmark: Restructuring and adding a MODBUS_FAC facility + * 2025-08-24 flashmark: Initial version */ -# define P183_DEBUG // Switch on additional debug logging -# ifdef BUILD_NO_DEBUG -# undef P183_DEBUG // Debugging switched off -# endif // ifdef BUILD_NO_DEBUG # define PLUGIN_183 # define PLUGIN_ID_183 183 -# define PLUGIN_NAME_183 "[testing] Modbus RTU" +# define PLUGIN_NAME_183 "Communication - Modbus RTU" # define P183_NR_OUTPUT_VALUES 4 # define PLUGIN_VALUENAME1_183 "Value1" # define PLUGIN_VALUENAME2_183 "Value2" diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 08c5c3a6a3..ed374dca6c 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -3,9 +3,8 @@ #if FEATURE_MODBUS_FAC -# include "Modbus_device.h" -# include "modbus_link.h" -# include "modbus_mgr.h" +# include "../Helpers/Modbus_device.h" +# include "../Helpers/Modbus_mgr.h" //# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 38e2112810..a1d2cded9a 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -288,9 +288,7 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus: Link, State= "); - log += toString(state); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("Modbus: Link, State= "), toString(state))); } # endif // MODBUS_DEBUG } diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index dc899c97f2..e76483c6fe 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -1,4 +1,4 @@ -#include "P183_data_struct.h" +#include "../PluginStructs/P183_data_struct.h" #ifdef USES_P183 diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 3d3636cd95..24d7a0ac73 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -4,6 +4,11 @@ #include "../../_Plugin_Helper.h" #ifdef USES_P183 +# define P183_DEBUG // Switch on additional debug logging +# ifdef BUILD_NO_DEBUG +# undef P183_DEBUG // Debugging switched off +# endif // ifdef BUILD_NO_DEBUG + # include "../Helpers/Modbus_device.h" // Plugin configuration parameters From c182cea58fd94d3a843a9b31174d45ed7ad4378d Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:16:44 +0100 Subject: [PATCH 19/48] P183 Added documentation --- docs/source/Plugin/P183.rst | 11 ++++ docs/source/Plugin/_plugin_sets_overview.repl | 24 ++++++--- misc/modbusFacility/Modbus_notes.txt | 24 +++++---- misc/modbusFacility/Modbus_seq2.puml | 52 +++++++++++++++++++ src/src/Helpers/Modbus_device.cpp | 36 +++++++++++-- src/src/Helpers/Modbus_link.cpp | 16 ++++-- src/src/Helpers/Modbus_mgr.cpp | 34 +++++++++--- 7 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 misc/modbusFacility/Modbus_seq2.puml diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst index e800df9b08..6ef0db396a 100644 --- a/docs/source/Plugin/P183.rst +++ b/docs/source/Plugin/P183.rst @@ -101,6 +101,17 @@ Get Config Values retrieves values or settings from the sensor or plugin, and ca .. include:: P183_config_values.repl +Plugin state +------------ + +This plugin uses a new Modbus facility that allows multiple Modbus devices to share the same Modbus link. +The current implementataion of the Modbus facility requires that the plugin specifies the serial link parameters. +When a Modbus link is shared between multiple plugins, the last plugin that connects to the Modbus link will determine the serial link parameters. +This may lead to unexpected behavior if multiple plugins are using different serial link parameters on the same serial port. For a deterministic behavior, make sure that all plugins using the same serial port use the same serial link parameters. + +In the future the Modbus facility may be extended to configure the Modbus links as separate entities. As a result the plugins would only need to specify which Modbus link to use, instead of the serial link parameters. +This change will be backward incompatible with the current implementation. Therefore the plugin status is set to 'Experimental'. + Change log ---------- diff --git a/docs/source/Plugin/_plugin_sets_overview.repl b/docs/source/Plugin/_plugin_sets_overview.repl index f34dab5a6a..1f33933195 100644 --- a/docs/source/Plugin/_plugin_sets_overview.repl +++ b/docs/source/Plugin/_plugin_sets_overview.repl @@ -235,7 +235,6 @@ Build set: :yellow:`COLLECTION B` ":ref:`P034_page`","P034" ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" - ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" @@ -347,6 +346,7 @@ Build set: :yellow:`COLLECTION C` ":ref:`P034_page`","P034" ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" + ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" @@ -456,6 +456,7 @@ Build set: :yellow:`COLLECTION D` ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -564,6 +565,7 @@ Build set: :yellow:`COLLECTION E` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -832,9 +834,6 @@ Build set: :yellow:`COLLECTION G` ":ref:`P168_page`","P168" ":ref:`P170_page`","P170" ":ref:`P172_page`","P172" - ":ref:`P173_page`","P173" - ":ref:`P177_page`","P177" - ":ref:`P178_page`","P178" ":ref:`P180_page`","P180" ":ref:`C001_page`","C001" ":ref:`C002_page`","C002" @@ -937,6 +936,9 @@ Build set: :yellow:`COLLECTION H` ":ref:`P139_page`","P139" ":ref:`P146_page`","P146" ":ref:`P152_page`","P152" + ":ref:`P173_page`","P173" + ":ref:`P177_page`","P177" + ":ref:`P178_page`","P178" ":ref:`P180_page`","P180" ":ref:`C001_page`","C001" ":ref:`C002_page`","C002" @@ -1169,7 +1171,6 @@ Build set: :yellow:`DISPLAY A` ":ref:`P052_page`","P052" ":ref:`P053_page`","P053" ":ref:`P056_page`","P056" - ":ref:`P057_page`","P057" ":ref:`P059_page`","P059" ":ref:`P063_page`","P063" ":ref:`P073_page`","P073" @@ -1214,20 +1215,26 @@ Build set: :yellow:`DISPLAY B` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -1237,6 +1244,7 @@ Build set: :yellow:`DISPLAY B` ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" @@ -1410,6 +1418,7 @@ Build set: :yellow:`IR` ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" @@ -1480,7 +1489,6 @@ Build set: :yellow:`IRext` ":ref:`P034_page`","P034" ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" - ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" @@ -1769,8 +1777,8 @@ Build set: :yellow:`MAX` ":ref:`P176_page`","P176" ":ref:`P177_page`","P177" ":ref:`P178_page`","P178" - ":ref:`P180_page`","P183" - ":ref:`P183_page`","P180" + ":ref:`P180_page`","P180" + ":ref:`P183_page`","P183" ":ref:`C001_page`","C001" ":ref:`C002_page`","C002" ":ref:`C003_page`","C003" diff --git a/misc/modbusFacility/Modbus_notes.txt b/misc/modbusFacility/Modbus_notes.txt index 9cdf7a9b5e..afe8fce90b 100644 --- a/misc/modbusFacility/Modbus_notes.txt +++ b/misc/modbusFacility/Modbus_notes.txt @@ -8,27 +8,33 @@ The main design requirement is to allow multiple plugins to use the same Modbus Modbus connunication consists of a request-reply message exchange over a serial link. After a request is sent the bus is occupied until the reply is received or a timeout occurred. A matching request-reply message pair is called a transaction in MODBUS_FAC. -The main design constraint for the MODBUS_FAC is to reduce the time waisted in waiting for a response message. For this a transaction queue is used. A device puts a request message on the queue and does not wait fro the reply. -Once the matching reply is received the device is notified by a callback function. +The main design constraint for the MODBUS_FAC is to reduce the time wasted in waiting for a response message. For this a transaction queue is used. +The queue is owned by the Modbus_link. A Modbus_device puts a request message on the queue and does not wait for the reply. +Once the matching reply is received by the Modbus_link the Modbus_device is notified by a callback function. The Modbus_device uses transactions to communicate to the associated hardware module. Messages are encoded in Modbus RTU protocol format by the Modbus_device. Once the request message is created it is transferred to the Modbus_link where it is added the the transaction queue. When the transaction is done, either a response message is received or a timeout occurred, the Modbus_device callback function is called and the response message is decoded and evaluated. The Modbus_link uses a queue to handle transactions sequentially. Each transaction has a state to track if it is queued, being processed, or completed. At maximum one transaction is being processed at a time. -The modbus_link uses a ESPEasy serial link for the actual transmit and receive of the messages. +The modbus_link uses an ESPEasy serial link for the actual transmit and receive of the messages. To match a Modbus_device with a Modbus_link a singleton Modus_mgr is used. This Modbus_mgr owns all Modbus_links and determines to which link a Modbus_device is assigned. -In case no matching link is found the Modbus_mgr creates a corresponding Modbus_link. Currently ESPEasy has no means to configure the serial link used by the Modbus_link. Instead the link is configured in the plugin. -The plugin passes the configuration to the device. When the Modbus_device requests the Modbus_mgr to connect to a Modbus_link it passes the serial parameters. The Modbus_mgr uses the serial port type to distinguish the links. -Whenever a new device is connected to a Modbus_link the link will be reprogrammed with the new serial parameters. +In case no matching link is found the Modbus_mgr creates a corresponding Modbus_link. Currently ESPEasy has no means to configure a Modbus_link object with the serial link properties. Instead the serial link is configured in the plugin. +The plugin passes the configuration data to the device. When the Modbus_device requests the Modbus_mgr to connect to a Modbus_link it passes the serial parameters. The Modbus_mgr uses the serial port type to distinguish the links. +Whenever a new device is connected to a Modbus_link the link will be reprogrammed with the new serial parameters. Note that all devices using the same serial link shall provide the same serial link parameters. +In case of different serial configuration parameters there is no guarantee which set will be used for the Modbus_link. -In future a separate configuration screen can be implemented to configure each Modus_link. Once this is available the Modbus_device can mention the link identification without the need to pass all configuration parameters. +In future a separate configuration screen should be implemented to configure each Modus_link. Once this is available the Modbus_device can mention the link identification without the need to pass all configuration parameters. -Drawback of the queue mechanism is the need for the device (and the associated plugin) to wait for the response message without blocking the CPU. For this the device needs to track "state". +The current implementataion of the Modbus_mgr uses the serial port (ESPEasySerialPort enum) to identify the Modbus_link. This implies that only one software serial link can be used. +It should be possible to extend this by checking the assigned RX and TX pins when a software serial port is selected. + +Drawback of the queue mechanism is the need for the Modbus_device (and the associated plugin) to wait for the response message without blocking the CPU. For this the Modbus_device needs to track "state". Either the plugin can queue only a single transaction or it must be able to track for which transaction in the queue a response is received. To keep this state tracking generic the transaction contains a pointer to a userState and userData. The Modbus_device can use these values depending on the transaction type to return state information to the plugin. The owning plugin can only return a value once it is received. For repetitive read events the previous received value can be returned. For specific read commands either a polling mechanism must be implemented or the plugin blocks until the response is received. -A dedicated trigger event for the plugins can reduce the CPU load by removing the need for regular polling (e.g. tem_per_second) or blocking. \ No newline at end of file +The most efficient mechanism would be a trigger event that can be sent by the Modbus_device once the data is received. Such trigger event for plugins can reduce the CPU load by removing the need for regular polling (e.g. ten_per_second) or blocking on a transaction. +For this the plugin event must be extended with the possibility to send "updates" to the plugin. Such an extension shall be generic and not Modbus specific. It is beyond the Modbus facilities development to add such new generic plugin features. \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq2.puml b/misc/modbusFacility/Modbus_seq2.puml new file mode 100644 index 0000000000..abaff06842 --- /dev/null +++ b/misc/modbusFacility/Modbus_seq2.puml @@ -0,0 +1,52 @@ +@startuml +actor System as system +participant Plugin as plugin +participant Plugin_struct as plugin_struct +participant Modbus_device as modbus_device +participant Modbus_mgr as modbus_mgr +participant Modbus_link as modbus_link +queue Queue as queue + +system -> plugin ++ : init +create plugin_struct +plugin -> plugin_struct ++ : new() +create modbus_device +plugin_struct -> modbus_device ++ : new() +modbus_device -> modbus_mgr ++ : connect() +alt link not exists + create modbus_link +modbus_mgr -> modbus_link : new() +end +modbus_mgr -> modbus_link : init() +modbus_device <-- modbus_mgr -- : link +plugin_struct <-- modbus_device -- +plugin <-- plugin_struct -- +system <-- plugin -- + +system -> plugin ++ : read +plugin -> plugin_struct ++ : read() +plugin_struct -> modbus_device ++ : readHoldingRegister() +modbus_device -> modbus_link ++: queueTransaction() +modbus_link -> queue : add +modbus_device <-- modbus_link -- +plugin_struct <-- modbus_device -- +plugin_struct -> plugin_struct : retrieve latest value +plugin <-- plugin_struct -- +system <-- plugin -- + +system -> modbus_mgr ++ : ten_per_second +modbus_mgr -> modbus_link ++: processCommand() +alt response available + modbus_link -> modbus_device ++ : linkCallback() + modbus_device -> plugin : update event (new) + modbus_link <-- modbus_device -- +end +alt no transaction + modbus_link -> queue : get next transaction + alt transaction available + modbus_link -> modbus_link : send request + end +end +modbus_mgr <-- modbus_link -- +system <-- modbus_mgr -- +@enduml \ No newline at end of file diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index ed374dca6c..f028970f6e 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -1,4 +1,10 @@ - +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MODBUS device class +// This class implements a single Modbus device connected over a serial link. +// It is part of the Modbus facilities supporting multiple Modbus devices on multiple serial Modbus links. +// It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. +// The Modbus device class will interpret the Modbus messages for the connected hardware and queue it at the link class. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "../../ESPEasy_common.h" #if FEATURE_MODBUS_FAC @@ -16,6 +22,8 @@ const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus device class /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusDEVICE_struct::~ModbusDEVICE_struct() { if (_modbus_link != nullptr) { @@ -28,6 +36,8 @@ ModbusDEVICE_struct::~ModbusDEVICE_struct() { _modbus_address = MODBUS_BROADCAST_ADDRESS; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Reset the Modbus device class to initial state /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::reset() { if (_modbus_link != nullptr) { @@ -40,6 +50,8 @@ void ModbusDEVICE_struct::reset() { _modbus_address = MODBUS_BROADCAST_ADDRESS; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Simplified initialization function without dere pin and collision detection specified /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::init(uint8_t slaveAddress, const ESPEasySerialPort port, @@ -49,6 +61,8 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, return init(slaveAddress, port, serial_rx, serial_tx, baudrate, -1); } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Full initialization function with dere pin and collision detection specified /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::init(uint8_t slaveAddress, const ESPEasySerialPort port, @@ -71,17 +85,22 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, # endif // MODBUS_DEBUG return success; } - +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Checker for device class initialization status /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::isInitialized() const { return (_modbus_link != nullptr) && (_modbus_link->isInitialized()); } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Set the Modbus timeout value for this device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::setModbusTimeout(uint16_t timeout) { _timeout = timeout; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Retrieve the Modbus timeout value for this device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t ModbusDEVICE_struct::getModbusTimeout() const { @@ -99,8 +118,10 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Start reading a Modubus holding register from another module on teh bus. The result will be available later. +// Start reading a Modbus holding register from another module on the bus. The result will be available later. // The function returns true if the request was queued. +// Note: This function accesses registers from other devices on the same Modbus bus. +// This should be used with care to prevent conflicts. This is beyond the intended scope of the Modbus device class. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, uint16_t registerAddress, @@ -130,10 +151,13 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddr uint16_t queueID = _modbus_link->queueTransaction(request); *statePtr = ModbusResultState::BUSY; - // Don't touch *valueptr here, it might contain a previous valid result. + // Don't touch *valueptr here, it might contain a previous valid result still to be handled. return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start writing a Modbus single register. +// The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, @@ -165,6 +189,8 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Periodic processing function to allow the Modbus device to process its queued requests /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::processCommand() { if (_modbus_link != nullptr) { @@ -279,6 +305,8 @@ uint16_t ModbusDEVICE_struct::CalculateCRC(uint8_t *buf, int len) { return crc; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the buffer contents to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { # ifdef MODBUS_DEBUG diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index a1d2cded9a..71af75eae5 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -1,6 +1,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MODBUS link class -// This class implements a Modbus link over a serial connection. +// This class implements a Modbus link over a serial connection. It is part of the Modbus facilities supporting multiple Modbus +// devices on multiple serial Modbus links. // It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. // It exepcts a Modbus device instance to construct and interpret the Modbus messages for the specific device. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -17,6 +18,8 @@ # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus link class /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusLINK_struct::~ModbusLINK_struct() { reset(); @@ -28,7 +31,8 @@ ModbusLINK_struct::~ModbusLINK_struct() { } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Reset the ModbusLINK structure +// Reset the ModbusLINK class to initial state +// This aborts all pending transactions and frees the associated resources. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::reset() { for (auto it = _requestQueue.begin(); it != _requestQueue.end(); it++) { @@ -153,7 +157,7 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Queue a Modbus request. The request is appended to the request and assigned a unique identifier. +// Queue a Modbus transaction. The request is appended to the queue and assigned a unique identifier. // The client can use this identifier to retrieve the response later. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transaction) { @@ -260,6 +264,9 @@ void ModbusLINK_struct::processCommand() return; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the queue element contents to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { # ifdef MODBUS_DEBUG @@ -284,6 +291,9 @@ void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { # endif // MODBUS_DEBUG } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the queue element state to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 75349c56a1..419bd80307 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -1,3 +1,9 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MODBUS manager class +// This class implements a singleton administration object for Modbus devices & links. +// It is part of the Modbus facilities supporting multiple Modbus devices on multiple serial Modbus links. +// It associates Modbus devices with Modbus links and manages their lifecycle. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "../../ESPEasy_common.h" @@ -12,21 +18,29 @@ # endif // ifdef BUILD_NO_DEBUG ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Singleton administration object for Modbus manager +// SIngleton instance of the Modbus manager ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct_t ModbusMGR_singleton = {}; +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the Modbus manager class, should not be called as this is intended to be a singleton /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct::~ModbusMGR_struct() { - reset(); + // This class is a singleton, so destructor should not be called. + // However, in case it is called, we clean up the allocated resources. + // Note that deletion of a device will also delete the associated link if no other device is using it. + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + if (_modbus_devices[i] != nullptr) { + delete _modbus_devices[i]; + _modbus_devices[i] = nullptr; + } + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusMGR_struct::reset() -{ -} - +// Connect a Modbus device to a Modbus link with the given parameters. The link will be created if it does not exist yet. +// The function returns true if the connection was successful. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::connect(const ESPEasySerialPort port, const int16_t serial_rx, @@ -37,6 +51,9 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, return connect(port, serial_rx, serial_tx, baudrate, -1, false, link, deviceID); } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Connect a Modbus device to a Modbus link with the given parameters. The link will be created if it does not exist yet. +// The function returns true if the connection was successful. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::connect(const ESPEasySerialPort port, const int16_t serial_rx, @@ -132,6 +149,9 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Disconnect the Modbus device with the given device ID. +// If no other devices are using the same link, the link is also deleted. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::disconnect(uint8_t deviceID) { dumpAdminInfo(); # ifdef MODBUS_DEBUG @@ -182,6 +202,8 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Dump the Modbus manager admin information to the log for debugging purposes /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::dumpAdminInfo() { From ef31d27251565d72e7e2b0a97b02b15b33adaf0c Mon Sep 17 00:00:00 2001 From: TD-er Date: Fri, 13 Feb 2026 22:41:31 +0100 Subject: [PATCH 20/48] [P183] Make new generic Modbus RTU PR build again --- src/src/CustomBuild/define_plugin_sets.h | 30 +++++++++++++++++++++- src/src/Helpers/Modbus_device.cpp | 20 +++++++-------- src/src/Helpers/Modbus_device.h | 17 ++++++------ src/src/PluginStructs/P183_data_struct.cpp | 16 ++++++------ src/src/PluginStructs/P183_data_struct.h | 4 +-- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index a2b9cb383a..180291b22e 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1711,6 +1711,9 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif // Remove plugins from 'collection' builds which rely on the neopixel library // to make sure those builds will fit again. #ifdef USES_P038 @@ -1951,6 +1954,10 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif + #endif // ifdef PLUGIN_ENERGY_COLLECTION @@ -2063,6 +2070,10 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif + #endif // ifdef PLUGIN_DISPLAY_A_COLLECTION // Collection of display plugins, set B (AdaGFX_Helper). @@ -2174,6 +2185,10 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif + #endif // ifdef PLUGIN_DISPLAY_B_COLLECTION // Collection of climate A plugins. @@ -2343,6 +2358,9 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif // Controllers @@ -2439,6 +2457,9 @@ To create/register a plugin, you have to : #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif // Controllers @@ -2520,9 +2541,13 @@ To create/register a plugin, you have to : #define USES_P165 // Display - NeoPixel (7-Segment) #endif - #ifndef USES_P180 + #if !defined(USES_P180) && defined(ESP32) #define USES_P180 // Generic - I2C Generic #endif + #if !defined(USES_P183) && defined(ESP32) + #define USES_P183 // Communication - Modbus RTU + #endif + #endif // ifdef PLUGIN_NEOPIXEL_COLLECTION @@ -2992,6 +3017,9 @@ To create/register a plugin, you have to : #ifndef USES_P180 #define USES_P180 // Generic - I2C Generic #endif + #ifndef USES_P183 + #define USES_P183 // Communication - Modbus RTU + #endif // Controllers #ifndef USES_C015 diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index f028970f6e..9042306e02 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -113,7 +113,7 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t *valuePtr, - ModbusResultState_t *statePtr) { + ModbusResultState *statePtr) { return readModuleHoldingRegister(_modbus_address, address, valuePtr, statePtr); } @@ -126,7 +126,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, uint16_t registerAddress, uint16_t *valuePtr, - ModbusResultState_t *statePtr) + ModbusResultState *statePtr) { if (_modbus_link == nullptr) { return false; @@ -149,7 +149,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddr request->_rcvframe_length = 7; // Expect 8 bytes in response ////dump_buffer(request->_sendframe, request->_sendframe_length); uint16_t queueID = _modbus_link->queueTransaction(request); - *statePtr = ModbusResultState::BUSY; + *statePtr = ModbusResultState::Busy; // Don't touch *valueptr here, it might contain a previous valid result still to be handled. return true; @@ -161,7 +161,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddr /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState_t *statePtr) + ModbusResultState *statePtr) { if (_modbus_link == nullptr) { return false; @@ -184,7 +184,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_rcvframe_length = 8; // Expect 8 bytes in response ////dump_buffer(request->_sendframe, request->_sendframe_length); uint16_t queueID = _modbus_link->queueTransaction(request); - *statePtr = ModbusResultState::BUSY; + *statePtr = ModbusResultState::Busy; return true; } @@ -205,7 +205,7 @@ void ModbusDEVICE_struct::processCommand() { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) { - ModbusResultState_t resultState = ModbusResultState::ERROR; // Default to error unless proven otherwise + ModbusResultState resultState = ModbusResultState::Error; // Default to error unless proven otherwise if (req == nullptr) { addLogMove(LOG_LEVEL_INFO, F("Modbus: ERROR, Null pointer passed in callback")); @@ -236,7 +236,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) // Valid response if (req->_userData != nullptr) { *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte - resultState = ModbusResultState::SUCCESS; + resultState = ModbusResultState::Success; } } } @@ -249,7 +249,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) uint16_t crc = CalculateCRC(req->_rcvframe, 5); if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { - resultState = ModbusResultState::SUCCESS; + resultState = ModbusResultState::Success; } } break; @@ -275,11 +275,11 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) } } - *(static_cast(req->_userState)) = resultState; + *(static_cast(req->_userState)) = resultState; _modbus_link->freeTransaction(req); # ifdef MODBUS_DEBUG log += F(", Result = "); - log += (resultState == ModbusResultState::SUCCESS) ? F("SUCCESS") : F("ERROR"); + log += (resultState == ModbusResultState::Success) ? F("SUCCESS") : F("ERROR"); addLogMove(LOG_LEVEL_INFO, log); # endif // MODBUS_DEBUG } diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index e9f342374a..aa4c099fe4 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -13,12 +13,11 @@ # endif // ifndef MODBUS_BROADCAST_ADDRESS // States for the Modbus queue elements -typedef enum class ModbusResultState { - BUSY = 0, // Transaction is not completed - SUCCESS = 1, // Transaction successfully completed - ERROR = 2, // Transaction completed with an error -} ModbusResultState_t; - +enum class ModbusResultState { + Busy = 0, // Transaction is not completed + Success = 1, // Transaction successfully completed + Error = 2, // Transaction completed with an error +}; // ModbusDEVICE structure representing a MODBUS Device // This is a single device that may share it's Modbus link with multiple other devices. @@ -64,16 +63,16 @@ struct ModbusDEVICE_struct { // The state variable will signal the processing state of the request. bool readHoldingRegister(uint16_t address, uint16_t *valueptr, - ModbusResultState_t *stateptr); + ModbusResultState *stateptr); bool writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState_t *stateptr); + ModbusResultState *stateptr); bool readModuleHoldingRegister(uint8_t busAddress, uint16_t registerAddress, uint16_t *valuePtr, - ModbusResultState_t *statePtr); + ModbusResultState *statePtr); private: diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index e76483c6fe..77c44c76cd 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -109,7 +109,7 @@ bool P183_data_struct::plugin_read(struct EventStruct *event) { void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { uint16_t value = 0; - ModbusResultState_t state = ModbusResultState::BUSY; + ModbusResultState state = ModbusResultState::Busy; addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); @@ -122,11 +122,11 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e _modbusDevice->readHoldingRegister(1, &value, &state); - while (state == ModbusResultState::BUSY) { + while (state == ModbusResultState::Busy) { _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } - if (state == ModbusResultState::SUCCESS) { + if (state == ModbusResultState::Success) { addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), reg, reg, value, value)); } else { addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) no response"), reg, reg)); @@ -139,7 +139,7 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e void P183_data_struct::scan_modbus() { uint16_t value = 0; - ModbusResultState_t state = ModbusResultState::BUSY; + ModbusResultState state = ModbusResultState::Busy; addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); @@ -150,10 +150,10 @@ void P183_data_struct::scan_modbus() for (uint8_t id = 0; id <= 247; id++) { _modbusDevice->readModuleHoldingRegister(id, 1, &value, &state); - while (state == ModbusResultState::BUSY) { + while (state == ModbusResultState::Busy) { _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } - String s = state == ModbusResultState::SUCCESS ? F(" OK") : F(" no response"); + String s = state == ModbusResultState::Success ? F(" OK") : F(" no response"); addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) %s"), id, id, s)); } } @@ -163,7 +163,7 @@ void P183_data_struct::scan_modbus() // Warning: this may take time as we waith for the Modbus message to be exchanged uint16_t P183_data_struct::readRegisterWait(uint16_t address) { uint16_t value = 0; - ModbusResultState_t state = ModbusResultState::BUSY; + ModbusResultState state = ModbusResultState::Busy; if (_modbusDevice == nullptr) { return 0; @@ -171,7 +171,7 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { _modbusDevice->readHoldingRegister(address, &value, &state); // Queue the read action - while (state == ModbusResultState::BUSY) { + while (state == ModbusResultState::Busy) { delay(50); _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 24d7a0ac73..5f58db7944 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -76,8 +76,8 @@ struct P183_data_struct : public PluginTaskData_base { taskIndex_t _taskIndex = INVALID_TASK_INDEX; struct ModbusDEVICE_struct *_modbusDevice = nullptr; uint16_t _registerValues[4] = {}; // Modus register values retrieved for output values - ModbusResultState_t _queueStates[4] = {}; // State of read hloding register transactions - ModbusResultState_t _lastActionState = ModbusResultState::BUSY; + ModbusResultState _queueStates[4] = {}; // State of read hloding register transactions + ModbusResultState _lastActionState = ModbusResultState::Busy; }; #endif // ifdef USES_P183 From 0098e3c6f87f8d90de67de3c8841c86f37b4ba6e Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:30:37 +0100 Subject: [PATCH 21/48] Secure work in progress, don't merge to master --- docs/source/Plugin/_plugin_sets_overview.repl | 86 +++++++- src/src/Helpers/Modbus_device.cpp | 4 +- src/src/Helpers/Modbus_link.cpp | 34 +++- src/src/Helpers/Modbus_link.h | 22 +- src/src/Helpers/Modbus_mgr.cpp | 191 +++++++++++++++++- src/src/Helpers/Modbus_mgr.h | 5 + src/src/Helpers/Scheduler_IntervalTimer.cpp | 7 + src/src/WebServer/InterfacesPage.cpp | 11 +- 8 files changed, 347 insertions(+), 13 deletions(-) diff --git a/docs/source/Plugin/_plugin_sets_overview.repl b/docs/source/Plugin/_plugin_sets_overview.repl index 1f33933195..db392aa430 100644 --- a/docs/source/Plugin/_plugin_sets_overview.repl +++ b/docs/source/Plugin/_plugin_sets_overview.repl @@ -73,6 +73,11 @@ Build set: :green:`NORMAL` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION A` --------------------------------------------- @@ -190,6 +195,11 @@ Build set: :yellow:`COLLECTION A` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION B` --------------------------------------------- @@ -301,6 +311,11 @@ Build set: :yellow:`COLLECTION B` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION C` --------------------------------------------- @@ -346,7 +361,6 @@ Build set: :yellow:`COLLECTION C` ":ref:`P034_page`","P034" ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" - ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" @@ -410,6 +424,11 @@ Build set: :yellow:`COLLECTION C` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION D` --------------------------------------------- @@ -518,6 +537,11 @@ Build set: :yellow:`COLLECTION D` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION E` --------------------------------------------- @@ -563,7 +587,6 @@ Build set: :yellow:`COLLECTION E` ":ref:`P034_page`","P034" ":ref:`P036_page`","P036" ":ref:`P037_page`","P037" - ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" ":ref:`P040_page`","P040" ":ref:`P043_page`","P043" @@ -630,6 +653,11 @@ Build set: :yellow:`COLLECTION E` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION F` --------------------------------------------- @@ -740,6 +768,11 @@ Build set: :yellow:`COLLECTION F` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION G` --------------------------------------------- @@ -853,6 +886,11 @@ Build set: :yellow:`COLLECTION G` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`COLLECTION H` --------------------------------------------- @@ -958,6 +996,11 @@ Build set: :yellow:`COLLECTION H` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`CLIMATE A` --------------------------------------------- @@ -1030,6 +1073,7 @@ Build set: :yellow:`CLIMATE A` ":ref:`P153_page`","P153" ":ref:`P154_page`","P154" ":ref:`P164_page`","P164" + ":ref:`P167_page`","P167" ":ref:`P180_page`","P180" ":ref:`C011_page`","C011" ":ref:`N001_page`","N001" @@ -1106,7 +1150,6 @@ Build set: :yellow:`CLIMATE B` ":ref:`P153_page`","P153" ":ref:`P154_page`","P154" ":ref:`P164_page`","P164" - ":ref:`P167_page`","P167" ":ref:`P168_page`","P168" ":ref:`P169_page`","P169" ":ref:`P172_page`","P172" @@ -1171,6 +1214,7 @@ Build set: :yellow:`DISPLAY A` ":ref:`P052_page`","P052" ":ref:`P053_page`","P053" ":ref:`P056_page`","P056" + ":ref:`P057_page`","P057" ":ref:`P059_page`","P059" ":ref:`P063_page`","P063" ":ref:`P073_page`","P073" @@ -1198,6 +1242,11 @@ Build set: :yellow:`DISPLAY A` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`DISPLAY B` --------------------------------------------- @@ -1282,6 +1331,11 @@ Build set: :yellow:`DISPLAY B` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`ENERGY` --------------------------------------------- @@ -1343,6 +1397,7 @@ Build set: :yellow:`ENERGY` ":ref:`P078_page`","P078" ":ref:`P079_page`","P079" ":ref:`P085_page`","P085" + ":ref:`P087_page`","P087" ":ref:`P089_page`","P089" ":ref:`P093_page`","P093" ":ref:`P102_page`","P102" @@ -1370,6 +1425,11 @@ Build set: :yellow:`ENERGY` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`IR` --------------------------------------------- @@ -1444,6 +1504,11 @@ Build set: :yellow:`IR` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`IRext` --------------------------------------------- @@ -1517,6 +1582,11 @@ Build set: :yellow:`IRext` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`NEOPIXEL` --------------------------------------------- @@ -1600,6 +1670,11 @@ Build set: :yellow:`NEOPIXEL` ":ref:`C013_page`","C013" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" Build set: :yellow:`MAX` --------------------------------------------- @@ -1798,4 +1873,9 @@ Build set: :yellow:`MAX` ":ref:`C018_page`","C018" ":ref:`N001_page`","N001" ":ref:`N002_page`","N002" + ":ref:`NW001_page`","NW001" + ":ref:`NW002_page`","NW002" + ":ref:`NW003_page`","NW003" + ":ref:`NW004_page`","NW004" + ":ref:`NW005_page`","NW005" diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 9042306e02..6207bf8981 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -193,8 +193,10 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, // Periodic processing function to allow the Modbus device to process its queued requests /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::processCommand() { + // TODO: This function is currently not used since the Modbus link processing is triggered directly from the scheduler. if (_modbus_link != nullptr) { - _modbus_link->processCommand(); // Trigger processing of the command queue on the link + ////_modbus_link->processCommand(); // Trigger processing of the command queue on the link + ////ModbusMGR_singleton.processLinks(); } } diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 71af75eae5..339128fc6b 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -80,7 +80,9 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, bool rs485Mode = ModbusLINK_struct::_easySerial->setRS485Mode(dere_pin, collision_detect); ModbusLINK_struct::_easySerial->begin(baudrate); - + _dere_pin = dere_pin; + _collision_detect = collision_detect; + # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { @@ -264,6 +266,36 @@ void ModbusLINK_struct::processCommand() return; } +int16_t ModbusLINK_struct::getBaudrate(void) const +{ +return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; +} + +ESPEasySerialPort ModbusLINK_struct::getPort(void) const +{ +return ESPEasySerialPort(); +} + +int16_t ModbusLINK_struct::getSerialRX(void) const +{ +return _easySerial != nullptr ? _easySerial->getRxPin() : -1; +} + +int16_t ModbusLINK_struct::getSerialTX(void) const +{ +return _easySerial != nullptr ? _easySerial->getTxPin() : -1; +} + +int8_t ModbusLINK_struct::getDerePin(void) const +{ +return _dere_pin; +} + +bool ModbusLINK_struct::getCollisionDetect(void) const +{ +return _collision_detect; +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Debugging function to dump the queue element contents to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index b0566031b6..3683ec004e 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -22,6 +22,7 @@ typedef enum class ModbusQueueState { RESPONSE_RECEIVED = 3, // Response has been received and is being processed ERROR_OCCURRED = 4, // An error occurred during processing (e.g., timeout, invalid response) READY_FOR_DESTROY = 5 // Element is marked for deletion and can be freed + } ModbusQueueState_t; const __FlashStringHelper* toString(ModbusQueueState_t state); @@ -33,6 +34,7 @@ typedef enum class ModbusTransactionType { NONE = 0, // Undefined/unknown transaction type READ_HOLDING_REGISTERS = 1, // Read holding registers (function code 0x03) WRITE_SINGLE_REGISTER = 2 // Write single register (function code 0x06) + } ModbusTransactionType_t; // Modbus request queue element structure @@ -54,6 +56,7 @@ struct Modbus_RequestQueueElement { unsigned long _startTime = 0; // Time the request was issued uint8_t _sendframe[MODBUS_XMIT_BUFFER] = { 0 }; // Reqest frame to send uint8_t _rcvframe[MODBUS_RCV_BUFFER] = { 0 }; // Response frame received + }; // Queue of Modbus request elements @@ -92,10 +95,22 @@ struct ModbusLINK_struct { uint16_t queueTransaction(Modbus_RequestQueueElement *transaction); void processCommand(); + int16_t getBaudrate(void) const; + + ESPEasySerialPort getPort(void) const; + + int16_t getSerialRX(void) const; + + int16_t getSerialTX(void) const; + + int8_t getDerePin(void) const; + + bool getCollisionDetect(void) const; + private: - static void dumpQueueElement(Modbus_RequestQueueElement *el); - static void dumpState(ModbusQueueState_t state); + static void dumpQueueElement(Modbus_RequestQueueElement *el); + static void dumpState(ModbusQueueState_t state); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process @@ -104,8 +119,11 @@ struct ModbusLINK_struct { uint32_t _reads_pass = 0; // TODO: statistics uint32_t _reads_crc_failed = 0; // TODO: statistics uint32_t _reads_nodata = 0; // TODO: statistics + uint8_t _dere_pin = 0; // Pin for RS485 direction control + bool _collision_detect = false; // Flag to indicate if collision detection is enabled uint8_t _last_error = 0; + }; diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 419bd80307..787ded4d08 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MODBUS manager class -// This class implements a singleton administration object for Modbus devices & links. +// This class implements a singleton administration object for Modbus devices & links. // It is part of the Modbus facilities supporting multiple Modbus devices on multiple serial Modbus links. // It associates Modbus devices with Modbus links and manages their lifecycle. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -12,16 +12,21 @@ # include # include "Modbus_mgr.h" -// # define MODBUS_DEBUG +# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG +#define MODBUS_MAX_BAUDRATE_SEL 8 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// SIngleton instance of the Modbus manager +// Singleton instance of the Modbus manager ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct_t ModbusMGR_singleton = {}; +int modbus_storageValueToBaudrate(uint8_t baudrate_setting); +uint8_t modbus_baudrateToStorageValue(int baudrate); + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Destructor of the Modbus manager class, should not be called as this is intended to be a singleton /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -149,9 +154,9 @@ bool ModbusMGR_struct::connect(const ESPEasySerialPort port, } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Disconnect the Modbus device with the given device ID. +// Disconnect the Modbus device with the given device ID. // If no other devices are using the same link, the link is also deleted. -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::disconnect(uint8_t deviceID) { dumpAdminInfo(); # ifdef MODBUS_DEBUG @@ -202,6 +207,20 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// This function should be called periodically to allow the Modbus manager to process the Modbus links +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusMGR_struct::processLinks() +{ + ModbusLinkInfo_struct *linkInfoPtr = nullptr; + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] != nullptr) { + linkInfoPtr = _modbus_links[i]; + linkInfoPtr->link->processCommand(); // Trigger processing of the command queue on the link + } + } +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Dump the Modbus manager admin information to the log for debugging purposes /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -247,4 +266,166 @@ void ModbusMGR_struct::dumpAdminInfo() # endif // ifdef MODBUS_DEBUG } +void ModbusMGR_struct::show_modbus_interfaces() +{ + String options_baudrate[MODBUS_MAX_BAUDRATE_SEL]; // Array to hold the baudrate options for the selector + String options_port[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Port otions for the selector, only valid ports will be filled. + int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Map to keep track of valid ports and their indices in the options_port array + + constexpr size_t optionBaudCount = NR_ELEMENTS(options_baudrate); + for (int i = 0; i < optionBaudCount; ++i) { + options_baudrate[i] = modbus_storageValueToBaudrate(i); + } + const FormSelectorOptions baudselector(optionBaudCount, options_baudrate); + + int optionPortCount = 1; + options_port[0] = F("Not set"); + portMap[0] = 0; // Map the "Not set" option to index 0 + for (int i = 1; i < NR_ELEMENTS(options_port); i++) { + if (validSerialPort(static_cast(i))) { + options_port[optionPortCount] = ESPEasySerialPort_toString(static_cast(i)); + portMap[i] = optionPortCount; // Store the index of the valid port in the options_port array + optionPortCount++; + } + else { + options_port[i] = F("Invalid"); + portMap[i] = 0; // Map invalid/unused ports to the "Not set" option + } + } + const FormSelectorOptions portSelector(optionPortCount, options_port); + for (int i =0; i < NR_ELEMENTS(options_port); i++) { + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: portMap[%d]= %d Port=%s"), + i, + portMap[i], + options_port[portMap[i]].c_str() + )); + } + + // Iterate over the modbus links and show their configuration on the web page + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) + { + if (_modbus_links[link] != nullptr) { + addFormSubHeader(strformat(F("Modbus %u"), link)); + + int idx = static_cast(_modbus_links[link]->port); + portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); + + String id = strformat(F("MBtx%u"), link); + addRowLabel_tr_id(formatGpioName_serialTX(false), id); + addPinSelect(PinSelectPurpose::Serial_input, id, _modbus_links[link]->serial_tx); + + id = strformat(F("MBrx%u"), link); + addRowLabel_tr_id(formatGpioName_serialRX(false), id); + addPinSelect(PinSelectPurpose::Serial_output, id, _modbus_links[link]->serial_rx); + + id = strformat(F("MBde%u"), link); + addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); + addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link]->dere_pin); + + baudselector.addFormSelector(F("Baud Rate"), strformat(F("MBbaud%u"), link), modbus_baudrateToStorageValue(_modbus_links[link]->baudrate)); + addUnit(F("baud")); + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), link), _modbus_links[link]->collision_detect); + addFormNote(F("/RE connected to GND, only supported on hardware serial")); + # endif // ifdef ESP32 + } + } +} + + + +// Convert stored baudrate setting (enumeration value) to actual baudrate value +// Returns the actual baudrate value. +int modbus_storageValueToBaudrate(uint8_t baudrate_setting) { + int baudrate = 9600; + + switch (baudrate_setting) + { + case 0: + baudrate = 1200; break; + case 1: + baudrate = 2400; break; + case 2: + baudrate = 4800; break; + case 3: + baudrate = 9600; break; + case 4: + baudrate = 19200; break; + case 5: + baudrate = 38400; break; + case 6: + baudrate = 57600; break; + case 7: + baudrate = 115200; break; + default: + baudrate = 9600; break; // Default value for fallback + } + return baudrate; +} + +uint8_t modbus_baudrateToStorageValue(int baudrate) { + if (baudrate <= 1200) {return 0;} + else if (baudrate <= 2400) {return 1;} + else if (baudrate <= 4800) {return 2;} + else if (baudrate <= 9600) {return 3;} + else if (baudrate <= 19200) {return 4;} + else if (baudrate <= 38400) {return 5;} + else if (baudrate <= 57600) {return 6;} + else if (baudrate <= 115200) {return 7;} + else {return 3;} // Default to 9600 baud for unsupported values +} + +bool ModbusMGR_struct::save_modbus_interfaces(String &error) +{ + int portCount = 1; + // Create mapping table from dropdown enum index to actual port identifier, index 0 is reserved for "Not set" + int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; + portMap[0] = 0; // Map the "Not set" option to index 0 + for (int i = 1; i < NR_ELEMENTS(portMap); i++) { + if (validSerialPort(static_cast(i))) { + portMap[portCount++] = i; + } + } + + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { + int port = 0; + if (_modbus_links[link] != nullptr) { + if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port)) { + _modbus_links[link]->port = static_cast(portMap[port]); + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: New Port[%d]= %d (%s)"), + link, + portMap[port], + ESPEasySerialPort_toString(_modbus_links[link]->port) + )); + } + int baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link]->baudrate); + if (update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting)) { + _modbus_links[link]->baudrate = modbus_storageValueToBaudrate(baudrate_setting); + } + int tx_setting = _modbus_links[link]->serial_tx; + if (update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting)) { + _modbus_links[link]->serial_tx = tx_setting; + } + int rx_setting = _modbus_links[link]->serial_rx; + if (update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting)) { + _modbus_links[link]->serial_rx = rx_setting; + } + int dere_setting = _modbus_links[link]->dere_pin; + if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting)) { + _modbus_links[link]->dere_pin = dere_setting; + # ifdef ESP32 + // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. + // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. + bool collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); + _modbus_links[link]->collision_detect = collision_detect_setting; + # endif // ifdef ESP32 + } + } + } + dumpAdminInfo(); //TODO: remove after debugging + return false; +} + #endif // if FEATURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 59713ddc61..ff0a5971c6 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -39,8 +39,13 @@ typedef struct ModbusMGR_struct { bool disconnect(uint8_t deviceID); + void processLinks(); + void dumpAdminInfo(); + void show_modbus_interfaces(); + bool save_modbus_interfaces(String& error); + private: static const int MAX_MODBUS_LINKS = 5; // Maximum number of Modbus links supported diff --git a/src/src/Helpers/Scheduler_IntervalTimer.cpp b/src/src/Helpers/Scheduler_IntervalTimer.cpp index 2c62b1e348..ded3109e1c 100644 --- a/src/src/Helpers/Scheduler_IntervalTimer.cpp +++ b/src/src/Helpers/Scheduler_IntervalTimer.cpp @@ -13,6 +13,9 @@ #include "../Helpers/Networking.h" #include "../Helpers/PeriodicalActions.h" +#if FEATURE_MODBUS_FAC +#include "MOdbus_mgr.h" +#endif /*********************************************************************************************\ * Interval Timer @@ -165,6 +168,10 @@ void ESPEasy_Scheduler::process_interval_timer(SchedulerTimerID timerID, unsigne if (!UseRTOSMultitasking) { run10TimesPerSecond(); } + // TODO: Flashmark Hackey way to hook the modbus here. Needs a better way to trigger the modbus processing at regular intervals. +#if FEATURE_MODBUS_FAC + ModbusMGR_singleton.processLinks(); +#endif break; case SchedulerIntervalTimer_e::TIMER_1SEC: runOncePerSecond(); break; case SchedulerIntervalTimer_e::TIMER_30SEC: runEach30Seconds(); break; diff --git a/src/src/WebServer/InterfacesPage.cpp b/src/src/WebServer/InterfacesPage.cpp index 6b629e72de..24a303ad47 100644 --- a/src/src/WebServer/InterfacesPage.cpp +++ b/src/src/WebServer/InterfacesPage.cpp @@ -26,6 +26,9 @@ # include "../Helpers/I2C_access.h" # include "../Helpers/Hardware_device_info.h" # endif // if FEATURE_I2C_MULTIPLE +# if FEATURE_MODBUS_FAC +# include "../Helpers/Modbus_mgr.h" +# endif // ******************************************************************************** // Web Interface hardware page @@ -53,7 +56,9 @@ void handle_interfaces() { #if FEATURE_SPI interfaces_show_SPI(); #endif - + #if FEATURE_MODBUS_FAC + ModbusMGR_singleton.show_modbus_interfaces(); + #endif addFormSeparator(2); @@ -82,6 +87,10 @@ void save_interfaces() { if (save_SPI(error)) { updated = true; } #endif + #if FEATURE_MODBUS_FAC + if (ModbusMGR_singleton.save_modbus_interfaces(error)) { updated = false; } // For now updated is false + #endif + if (updated) { error += SaveSettings(); addHtmlError(error); From 610c3e9b87da40c71e55782188716241c05f6ce6 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:59:37 +0200 Subject: [PATCH 22/48] P183 snapshot --- src/src/CustomBuild/define_plugin_sets.h | 7 ----- src/src/Helpers/Modbus_link.cpp | 26 ++++++++++++++++++ src/src/Helpers/Modbus_link.h | 9 ++++++- src/src/Helpers/Scheduler_IntervalTimer.cpp | 2 +- src/src/WebServer/ESPEasy_WebServer.cpp | 4 +-- src/src/WebServer/InterfacesPage.cpp | 30 +++++++++------------ src/src/WebServer/InterfacesPage.h | 4 +-- src/src/WebServer/WebTemplateParser.cpp | 16 +++++------ 8 files changed, 59 insertions(+), 39 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index e4ea162c7a..e1ea080574 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -3792,13 +3792,6 @@ To create/register a plugin, you have to : #ifndef FEATURE_MODBUS_FAC #define FEATURE_MODBUS_FAC 0 #endif -#ifndef FEATURE_MODBUS_INTERFACES_TAB // TODO Temporary, until P183 finished -#ifdef USES_P183 -#define FEATURE_MODBUS_INTERFACES_TAB 1 -#else -#define FEATURE_MODBUS_INTERFACES_TAB 0 -#endif -#endif #ifndef FEATURE_CAN #define FEATURE_CAN 0 diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 339128fc6b..7e4a43106a 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -109,6 +109,32 @@ bool ModbusLINK_struct::isInitialized() const { return _easySerial != nullptr; } +bool ModbusLINK_struct::reconfigure(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect) +{ + if (isInitialized()) { + if (_easySerial->getSerialPortType() == port) { + // Same port, just reconfigure + _easySerial->resetConfig(port, serial_rx, serial_tx); + _easySerial->begin(baudrate); + _dere_pin = dere_pin; + _collision_detect = collision_detect; + return true; + } + else { + // Different port, need to reinitialize + } + } + else { + return init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect); + } + return false; +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Provide a new transaction structure that can be used to build a Modbus request and queue it at this link /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 3683ec004e..820fcd689f 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -87,7 +87,14 @@ struct ModbusLINK_struct { int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const; + bool isInitialized() const; + + bool reconfigure(const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect = false); Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); bool freeTransaction(Modbus_RequestQueueElement *transaction); diff --git a/src/src/Helpers/Scheduler_IntervalTimer.cpp b/src/src/Helpers/Scheduler_IntervalTimer.cpp index ded3109e1c..39f346ccb6 100644 --- a/src/src/Helpers/Scheduler_IntervalTimer.cpp +++ b/src/src/Helpers/Scheduler_IntervalTimer.cpp @@ -14,7 +14,7 @@ #include "../Helpers/PeriodicalActions.h" #if FEATURE_MODBUS_FAC -#include "MOdbus_mgr.h" +#include "../Helpers/Modbus_mgr.h" #endif /*********************************************************************************************\ diff --git a/src/src/WebServer/ESPEasy_WebServer.cpp b/src/src/WebServer/ESPEasy_WebServer.cpp index d8fc4aa8ea..d895a12bde 100644 --- a/src/src/WebServer/ESPEasy_WebServer.cpp +++ b/src/src/WebServer/ESPEasy_WebServer.cpp @@ -281,9 +281,9 @@ void WebServerInit() #if FEATURE_SPI web_server.on(F("/interfaces_spi"), handle_interfaces_spi); #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC web_server.on(F("/interfaces_modbus"), handle_interfaces_modbus); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN web_server.on(F("/interfaces_can"), handle_interfaces_can); #endif // if FEATURE_CAN diff --git a/src/src/WebServer/InterfacesPage.cpp b/src/src/WebServer/InterfacesPage.cpp index 51dea93a64..527f127ca9 100644 --- a/src/src/WebServer/InterfacesPage.cpp +++ b/src/src/WebServer/InterfacesPage.cpp @@ -47,12 +47,12 @@ void handle_interfaces_spi() { } #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC void handle_interfaces_modbus() { navMenuIndex = MENU_INDEX_INTERFACES_MODBUS; handle_interfaces(); } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN void handle_interfaces_can() { @@ -86,7 +86,7 @@ void handle_interfaces() { navMenuIndex = MENU_INDEX_INTERFACES_I2C; #elif FEATURE_SPI navMenuIndex = MENU_INDEX_INTERFACES_SPI; -#elif FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#elif FEATURE_MODBUS_FAC navMenuIndex = MENU_INDEX_INTERFACES_MODBUS; #elif FEATURE_CAN navMenuIndex = MENU_INDEX_INTERFACES_CAN; @@ -120,15 +120,12 @@ void handle_interfaces() { interfaces_show_SPI(); } #endif - #if FEATURE_MODBUS_FAC - ModbusMGR_singleton.show_modbus_interfaces(); - #endif - #if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB + #if FEATURE_MODBUS_FAC if (navMenuIndex == MENU_INDEX_INTERFACES_MODBUS) { interfaces_show_MODBUS(); } - #endif // if FEATURE_MODBUS + #endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN if (navMenuIndex == MENU_INDEX_INTERFACES_CAN) { @@ -176,11 +173,8 @@ void save_interfaces() { #endif #if FEATURE_MODBUS_FAC - if (ModbusMGR_singleton.save_modbus_interfaces(error)) { updated = false; } // For now updated is false - #endif - #if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB if ((navMenuIndex == MENU_INDEX_INTERFACES_MODBUS) && save_MODBUS(error)) { updated = true; } - #endif // if FEATURE_MODBUS + #endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN if ((navMenuIndex == MENU_INDEX_INTERFACES_CAN) && save_CAN(error)) { updated = true; } @@ -344,11 +338,11 @@ bool save_SPI(String& error) { #endif -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC bool save_MODBUS(String& error) { - return false; // TODO + return ModbusMGR_singleton.save_modbus_interfaces(error); } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN bool save_CAN(String& error) { @@ -550,11 +544,11 @@ void interfaces_show_SPI() { } #endif -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC void interfaces_show_MODBUS() { - addRowLabel(F("TODO")); // TODO + ModbusMGR_singleton.show_modbus_interfaces(); } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN void interfaces_show_CAN() { diff --git a/src/src/WebServer/InterfacesPage.h b/src/src/WebServer/InterfacesPage.h index a96601c7ad..b3d6f110a0 100644 --- a/src/src/WebServer/InterfacesPage.h +++ b/src/src/WebServer/InterfacesPage.h @@ -21,11 +21,11 @@ bool save_SPI(String& error); void interfaces_show_SPI(); #endif -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC void handle_interfaces_modbus(); bool save_MODBUS(String& error); void interfaces_show_MODBUS(); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN void handle_interfaces_can(); diff --git a/src/src/WebServer/WebTemplateParser.cpp b/src/src/WebServer/WebTemplateParser.cpp index 7f88138ec3..44632ea883 100644 --- a/src/src/WebServer/WebTemplateParser.cpp +++ b/src/src/WebServer/WebTemplateParser.cpp @@ -122,9 +122,9 @@ const __FlashStringHelper* getGpMenuIcon(uint8_t index) { #if FEATURE_SPI case MENU_INDEX_INTERFACES_SPI: return ICON("SPI"); #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC case MENU_INDEX_INTERFACES_MODBUS: return ICON("Modbus"); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN case MENU_INDEX_INTERFACES_CAN: return ICON("CAN bus"); #endif // if FEATURE_CAN @@ -156,13 +156,13 @@ const __FlashStringHelper* getGpMenuLabel(uint8_t index) { #if FEATURE_SPI case MENU_INDEX_INTERFACES_SPI: #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC case MENU_INDEX_INTERFACES_MODBUS: -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN case MENU_INDEX_INTERFACES_CAN: #endif // if FEATURE_CAN -#if FEATURE_I2C || FEATURE_SPI || (FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB) || FEATURE_CAN +#if FEATURE_I2C || FEATURE_SPI || FEATURE_MODBUS_FAC || FEATURE_CAN break; // No label, only an 'icon', for the second-level menu #endif // if FEATURE_I2C || FEATURE_SPI || FEATURE_MODBUS || FEATURE_CAN #if FEATURE_WRMBUS @@ -193,9 +193,9 @@ const __FlashStringHelper* getGpMenuURL(uint8_t index) { #if FEATURE_SPI case MENU_INDEX_INTERFACES_SPI: return F("/interfaces_spi"); #endif // if FEATURE_SPI -#if FEATURE_MODBUS && FEATURE_MODBUS_INTERFACES_TAB +#if FEATURE_MODBUS_FAC case MENU_INDEX_INTERFACES_MODBUS: return F("/interfaces_modbus"); -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC #if FEATURE_CAN case MENU_INDEX_INTERFACES_CAN: return F("/interfaces_can"); #endif // if FEATURE_CAN @@ -223,7 +223,7 @@ bool GpMenuVisible(uint8_t index) { case MENU_INDEX_INTERFACES: return MENU_INDEX_INTERFACES_VISIBLE; case MENU_INDEX_INTERFACES_I2C: return (1 == FEATURE_I2C); case MENU_INDEX_INTERFACES_SPI: return (1 == FEATURE_SPI); - case MENU_INDEX_INTERFACES_MODBUS: return (1 == FEATURE_MODBUS && 1 == FEATURE_MODBUS_INTERFACES_TAB); + case MENU_INDEX_INTERFACES_MODBUS: return (1 == FEATURE_MODBUS_FAC); #if defined(FEATURE_CAN) case MENU_INDEX_INTERFACES_CAN: return (1 == FEATURE_CAN); #endif // if defined(FEATURE_CAN) From 6b774aaba916517ef1a66d60bcd3b19d9cdd5816 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:57:02 +0200 Subject: [PATCH 23/48] P183 modbus interfaces support --- src/_P183_modbus.ino | 94 +--- src/src/CustomBuild/StorageLayout.h | 9 +- src/src/DataTypes/SettingsType.cpp | 16 +- src/src/DataTypes/SettingsType.h | 4 +- src/src/Helpers/Modbus_device.cpp | 38 +- src/src/Helpers/Modbus_device.h | 15 +- src/src/Helpers/Modbus_link.cpp | 9 +- src/src/Helpers/Modbus_mgr.cpp | 528 ++++++++++++++------- src/src/Helpers/Modbus_mgr.h | 39 +- src/src/PluginStructs/P183_data_struct.cpp | 35 +- src/src/PluginStructs/P183_data_struct.h | 14 +- 11 files changed, 445 insertions(+), 356 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 1d5bef42bb..d1ebb5adb1 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -17,6 +17,7 @@ /** * Changelog: + * 2026-04-13 flashmark: Separate Modbus link definition from plugin. * 2025-10-12 flashmark: Restructuring and adding a MODBUS_FAC facility * 2025-08-24 flashmark: Initial version */ @@ -45,7 +46,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { auto& dev = Device[++deviceCount]; dev.Number = PLUGIN_ID_183; - dev.Type = DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins + dev.Type = DEVICE_TYPE_CUSTOM0; //////DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins dev.VType = Sensor_VType::SENSOR_TYPE_QUAD; dev.FormulaOption = true; dev.ValueCount = P183_NR_OUTPUT_VALUES; @@ -74,13 +75,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_GET_DEVICEGPIONAMES: { - serialHelper_modbus_getGpioNames(event); break; } case PLUGIN_WEBFORM_SHOW_CONFIG: { - string += serialHelper_getSerialTypeLabel(event); + //string += serialHelper_getSerialTypeLabel(event); success = true; break; } @@ -88,41 +88,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: { P183_DEV_ID = P183_DEV_ID_DFLT; - P183_BAUDRATE = P183_BAUDRATE_DFLT; - success = true; break; } - case PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS: - { - if ((P183_DEV_ID <= 0) || (P183_DEV_ID > P183_MAX_MODBUS_NODES) || (P183_BAUDRATE >= 6)) { - // Load some defaults - P183_DEV_ID = P183_DEV_ID_DFLT; - P183_BAUDRATE = P183_BAUDRATE_DFLT; - } - { - String options_baudrate[P183_MAX_BAUDRATE_SEL]; - - for (int i = 0; i < P183_MAX_BAUDRATE_SEL; ++i) { - options_baudrate[i] = P183_storageValueToBaudrate(i); - } - constexpr size_t optionCount = NR_ELEMENTS(options_baudrate); - const FormSelectorOptions selector(optionCount, options_baudrate); - selector.addFormSelector(F("Baud Rate"), P183_BAUDRATE_LABEL, P183_BAUDRATE); - addUnit(F("baud")); - } - - addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); - - # ifdef ESP32 - addFormCheckBox(F("Enable Collision Detection"), F(P183_FLAG_COLL_DETECT_LABEL), P183_GET_FLAG_COLL_DETECT); - addFormNote(F("/RE connected to GND, only supported on hardware serial")); - # endif // ifdef ESP32 - - break; - } - case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: { if ((P183_NR_OUTPUTS < 1) || (P183_NR_OUTPUTS > P183_NR_OUTPUT_VALUES)) { @@ -140,6 +109,14 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { + addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); + addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); + + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), F(P183_FLAG_COLL_DETECT_LABEL), P183_GET_FLAG_COLL_DETECT); + addFormNote(F("/RE connected to GND, only supported on hardware serial")); + # endif // ifdef ESP32 + success = true; break; } @@ -147,11 +124,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); - P183_BAUDRATE = getFormItemInt(P183_BAUDRATE_LABEL); - # ifdef ESP32 - P183_SET_FLAG_COLL_DETECT(isFormItemChecked(F(P183_FLAG_COLL_DETECT_LABEL))); - # endif // ifdef ESP32 - + P183_LINK_ID = getFormItemInt(P183_LINK_ID_LABEL); P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) @@ -169,13 +142,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data != nullptr) { - success = P183_data->plugin_init(P183_DEV_ID, - static_cast(CONFIG_PORT), - CONFIG_PIN1, - CONFIG_PIN2, - P183_storageValueToBaudrate(P183_BAUDRATE), - P183_DEPIN, - P183_GET_FLAG_COLL_DETECT); + success = P183_data->plugin_init(P183_DEV_ID, P183_LINK_ID); } else { addLogMove(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); @@ -274,7 +241,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_GET_CONFIG_VALUE: { + case PLUGIN_GET_CONFIG_VALUE: + { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { @@ -293,7 +261,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } break; } - case PLUGIN_TEN_PER_SECOND: { + case PLUGIN_TEN_PER_SECOND: + { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data != nullptr) { @@ -306,33 +275,4 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) return success; } -// Convert stored baudrate setting (enumeration value) to actual baudrate value -// Returns the actual baudrate value. -int P183_storageValueToBaudrate(uint8_t baudrate_setting) { - int baudrate = 9600; - - switch (baudrate_setting) - { - case 0: - baudrate = 1200; break; - case 1: - baudrate = 2400; break; - case 2: - baudrate = 4800; break; - case 3: - baudrate = 9600; break; - case 4: - baudrate = 19200; break; - case 5: - baudrate = 38400; break; - case 6: - baudrate = 57600; break; - case 7: - baudrate = 115200; break; - default: - baudrate = 9600; break; // Default value for fallback - } - return baudrate; -} - #endif // USES_P183 diff --git a/src/src/CustomBuild/StorageLayout.h b/src/src/CustomBuild/StorageLayout.h index b9958d6d9a..1ac3034aa3 100644 --- a/src/src/CustomBuild/StorageLayout.h +++ b/src/src/CustomBuild/StorageLayout.h @@ -134,7 +134,9 @@ # define DAT_NETWORK_INTERFACE_SIZE 1024 #endif - +#ifndef DAT_MODBUS_INTERFACE_SIZE +# define DAT_MODBUS_INTERFACE_SIZE 256 // Reserved size for Modbus link settings +#endif /* @@ -232,7 +234,10 @@ // All other network interface plugins will be ESP32-only. # ifndef DAT_NETWORK_INTERFACES_OFFSET # define DAT_NETWORK_INTERFACES_OFFSET 16384 - # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER + # endif + # ifndef DAT_MODBUS_INTERFACE_OFFSET + # define DAT_MODBUS_INTERFACE_OFFSET 6144 // each Modbus link config = 256 bytes, 4 max + # endif # ifndef DAT_OFFSET_DEV_CREDENTIALS # define DAT_OFFSET_DEV_CREDENTIALS 0 # endif diff --git a/src/src/DataTypes/SettingsType.cpp b/src/src/DataTypes/SettingsType.cpp index fde993e3ed..8d77844094 100644 --- a/src/src/DataTypes/SettingsType.cpp +++ b/src/src/DataTypes/SettingsType.cpp @@ -33,7 +33,9 @@ const __FlashStringHelper * SettingsType::getSettingsTypeString(Enum settingsTyp #if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE case Enum::DeviceSpecificCredentials_type: return F("DeviceSpecificCredentials"); #endif - +#if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: return F("ModbusInterface"); +#endif case Enum::SettingsType_MAX: break; } @@ -161,7 +163,18 @@ bool SettingsType::getSettingsParameters(Enum settingsType, int index, int& max_ } break; #endif +#if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: + { + max_index = 4; // up to 4 Modbus interfaces TODO: use poroper define for this. + offset = (DAT_MODBUS_INTERFACE_OFFSET) + (index * (DAT_MODBUS_INTERFACE_SIZE)); + max_size = (DAT_MODBUS_INTERFACE_SIZE); + // struct_size may differ. + struct_size = 0; + } + break; +#endif //FEATURE_MODBUS_FAC case Enum::SettingsType_MAX: { @@ -265,6 +278,7 @@ SettingsType::SettingsFileEnum SettingsType::getSettingsFile(Enum settingsType) #if FEATURE_STORE_NETWORK_INTERFACE_SETTINGS case Enum::NetworkInterfaceSettings_Type: #endif + case Enum::ModbusInterfaceSettings_Type: return SettingsFileEnum::FILE_CONFIG_type; case Enum::NotificationSettings_Type: return SettingsFileEnum::FILE_NOTIFICATION_type; diff --git a/src/src/DataTypes/SettingsType.h b/src/src/DataTypes/SettingsType.h index b5f2c43cb7..380130988d 100644 --- a/src/src/DataTypes/SettingsType.h +++ b/src/src/DataTypes/SettingsType.h @@ -25,7 +25,9 @@ class SettingsType { #if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE DeviceSpecificCredentials_type, #endif - +#if FEATURE_MODBUS_FAC + ModbusInterfaceSettings_Type, +#endif //FEATURE_MODBUS_FAC SettingsType_MAX }; diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 6207bf8981..90d8cf5a60 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -11,8 +11,9 @@ # include "../Helpers/Modbus_device.h" # include "../Helpers/Modbus_mgr.h" +#include "Modbus_device.h" -//# define MODBUS_DEBUG +# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -51,41 +52,20 @@ void ModbusDEVICE_struct::reset() { } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Simplified initialization function without dere pin and collision detection specified +// Initialization connected to an existing link. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::init(uint8_t slaveAddress, - const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate) { - return init(slaveAddress, port, serial_rx, serial_tx, baudrate, -1); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Full initialization function with dere pin and collision detection specified -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::init(uint8_t slaveAddress, - const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect) { - // Request the Modbus manager to connect this device to a Modbus link with the given parameters. - bool success = ModbusMGR_singleton.connect(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect, &_modbus_link, &_deviceID); - +bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId) +{ + bool success = ModbusMGR_singleton.connect(linkId, &_modbus_link, &_deviceID); _modbus_address = slaveAddress; - - # ifdef MODBUS_DEBUG - + # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus: Device Init, Slave address = %u, This = %p, deviceID = %u"), slaveAddress, this, _deviceID)); + strformat(F("Modbus: Device Init, Slave address = %u, This = %p, deviceID = %u, linkId=%d"), slaveAddress, this, _deviceID, linkId)); } # endif // MODBUS_DEBUG - return success; + return success; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Checker for device class initialization status /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::isInitialized() const { diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index aa4c099fe4..18eeeedb2a 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -34,19 +34,8 @@ struct ModbusDEVICE_struct { void reset(); - bool init(uint8_t slaveAddress, - const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate); - - bool init(uint8_t slaveAddress, - const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect = false); +bool init( uint8_t slaveAddress, + int linkId); bool isInitialized() const; diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 7e4a43106a..096a2ec31d 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -13,7 +13,7 @@ # include "Modbus_device.h" # include "Modbus_link.h" -//# define MODBUS_DEBUG +# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -104,11 +104,16 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Return the initialization status of the link +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusLINK_struct::isInitialized() const { return _easySerial != nullptr; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Reconfigure the link with new parameters. If the serial port is the same, it is reconfigured, otherwise it is reinitialized. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusLINK_struct::reconfigure(const ESPEasySerialPort port, const int16_t serial_rx, const int16_t serial_tx, @@ -126,7 +131,7 @@ bool ModbusLINK_struct::reconfigure(const ESPEasySerialPort port, return true; } else { - // Different port, need to reinitialize + return init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect); } } else { diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 787ded4d08..1723eaf201 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -17,16 +17,44 @@ # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG -#define MODBUS_MAX_BAUDRATE_SEL 8 +# define MODBUS_MAX_BAUDRATE_SEL 8 + +// Key indices for storing Modbus link settings in the key-value store +# define MODBUS_PORT_KEY_INDEX 1 +# define MODBUS_RX_KEY_INDEX 2 +# define MODBUS_TX_KEY_INDEX 3 +# define MODBUS_BAUDRATE_KEY_INDEX 4 +# define MODBUS_DERE_PIN_KEY_INDEX 5 +# define MODBUS_COLLISION_DETECT_KEY_INDEX 6 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Singleton instance of the Modbus manager ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct_t ModbusMGR_singleton = {}; -int modbus_storageValueToBaudrate(uint8_t baudrate_setting); +int modbus_storageValueToBaudrate(uint8_t baudrate_setting); uint8_t modbus_baudrateToStorageValue(int baudrate); + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Constructor of the Modbus manager class, initializes the internal data structures +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +ModbusMGR_struct::ModbusMGR_struct() +{ + # ifdef MODBUS_DEBUG + String log = F("Modbus: Manager, Constructor "); + # endif // ifdef MODBUS_DEBUG + + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + _modbus_links[i] = nullptr; + } + + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + _modbus_devices[i] = nullptr; + } +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Destructor of the Modbus manager class, should not be called as this is intended to be a singleton /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -34,123 +62,139 @@ ModbusMGR_struct::~ModbusMGR_struct() { // This class is a singleton, so destructor should not be called. // However, in case it is called, we clean up the allocated resources. - // Note that deletion of a device will also delete the associated link if no other device is using it. + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { if (_modbus_devices[i] != nullptr) { delete _modbus_devices[i]; _modbus_devices[i] = nullptr; } } + + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] != nullptr) { + if (_modbus_links[i]->link != nullptr) { + delete _modbus_links[i]->link; + _modbus_links[i]->link = nullptr; + } + + if (_modbus_links[i]->kvs != nullptr) { + delete _modbus_links[i]->kvs; + _modbus_links[i]->kvs = nullptr; + } + delete _modbus_links[i]; + _modbus_links[i] = nullptr; + } + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Connect a Modbus device to a Modbus link with the given parameters. The link will be created if it does not exist yet. -// The function returns true if the connection was successful. +// Initialize the Modbus manager link administration. +// This will read the persisted data for each link and create the link objects for the links that are configured. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusMGR_struct::connect(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - ModbusLINK_struct **link, - uint8_t *deviceID) { - return connect(port, serial_rx, serial_tx, baudrate, -1, false, link, deviceID); +bool ModbusMGR_struct::initialize() +{ + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if (_modbus_links[i] == nullptr) { + int8_t val; + _modbus_links[i] = new (std::nothrow) ModbusLinkInfo_struct(); + _modbus_links[i]->link = nullptr; + _modbus_links[i]->kvs = new (std::nothrow) ESPEasy_key_value_store; + _modbus_links[i]->kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); + # ifdef MODBUS_DEBUG + _modbus_links[i]->kvs->dump(); + # endif + _modbus_links[i]->kvs->getValue(MODBUS_PORT_KEY_INDEX, val); + _modbus_links[i]->port = static_cast(val); + _modbus_links[i]->kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i]->serial_rx); + _modbus_links[i]->kvs->getValue(MODBUS_TX_KEY_INDEX, _modbus_links[i]->serial_tx); + _modbus_links[i]->kvs->getValue(MODBUS_BAUDRATE_KEY_INDEX, _modbus_links[i]->baudrate); + _modbus_links[i]->kvs->getValue(MODBUS_DERE_PIN_KEY_INDEX, _modbus_links[i]->dere_pin); + _modbus_links[i]->kvs->getValue(MODBUS_COLLISION_DETECT_KEY_INDEX, _modbus_links[i]->collision_detect); + + if (_modbus_links[i]->port != ESPEasySerialPort::not_set) { + _modbus_links[i]->link = new (std::nothrow) ModbusLINK_struct(); + _modbus_links[i]->link->init(_modbus_links[i]->port, + _modbus_links[i]->serial_rx, + _modbus_links[i]->serial_tx, + modbus_storageValueToBaudrate(_modbus_links[i]->baudrate), + _modbus_links[i]->dere_pin, + _modbus_links[i]->collision_detect); + } + } + } + + # ifdef MODBUS_DEBUG + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG + + return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Connect a Modbus device to a Modbus link with the given parameters. The link will be created if it does not exist yet. -// The function returns true if the connection was successful. +// Connect a Modbus device to a Modbus link. A unique device ID is assigned to the device. +// Returns a pointer to the Modbus link object and the assigned device ID if connection is successful, otherwise returns false. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusMGR_struct::connect(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect, - ModbusLINK_struct **link, - uint8_t *deviceID) { - ModbusLinkInfo_struct *linkInfoPtr = nullptr; - ModbusDeviceInfo_struct *deviceInfoPtr = nullptr; - +bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *deviceID) +{ # ifdef MODBUS_DEBUG - String log = F("Modbus: Manager, Connect port="); - - log += (int)port; + String log = F("Modbus: Manager, Connect linkId="); + log += linkId; # endif // ifdef MODBUS_DEBUG - // Check if link is already used by another device - for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if ((_modbus_links[i] != nullptr) && (_modbus_links[i]->port == port)) { - // Found existing link with matching port identifier - linkInfoPtr = _modbus_links[i]; - # ifdef MODBUS_DEBUG - log += strformat(F(", Found existing link= %d for port=%s"), i, ESPEasySerialPort_toString(_modbus_links[i]->port)); - # endif // ifdef MODBUS_DEBUG - } - } + initialize(); // TODO Initialization sequence to be refactored. - if (linkInfoPtr == nullptr) { - linkInfoPtr = new (std::nothrow) ModbusLinkInfo_struct(); + if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + return false; + } - for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if (_modbus_links[i] == nullptr) { - _modbus_links[i] = linkInfoPtr; + *deviceID = -1; // Default to -1, will be set to a valid device ID if connection is successful + *link = nullptr; + + if (_modbus_links[linkId] != nullptr) { + for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { + if (_modbus_devices[i] == nullptr) { + // Found an available device slot + _modbus_devices[i] = new (std::nothrow) ModbusDeviceInfo_struct(); + _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-MAX_MODBUS_DEVICES) + _modbus_devices[i]->link = _modbus_links[linkId]; + *deviceID = _modbus_devices[i]->deviceID; + *link = _modbus_devices[i]->link->link; # ifdef MODBUS_DEBUG - log += strformat(F(", Created new link= %d for port=%s"), i, ESPEasySerialPort_toString(_modbus_links[i]->port)); + log += F(" Assigned deviceID= "); + log += *deviceID; # endif // ifdef MODBUS_DEBUG break; } } } - - if (linkInfoPtr != nullptr) { // Sanity check for successful link admin creation - if (linkInfoPtr->link == nullptr) { // Check if link object already exists - // No existing link, create a new one - linkInfoPtr->link = new (std::nothrow) ModbusLINK_struct(); - } - - if (linkInfoPtr->link != nullptr) { // Sanity check for successful creation - // (re)initialize the new link - if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { - // Initialization failed, clean up - delete linkInfoPtr->link; - linkInfoPtr->link = nullptr; - delete linkInfoPtr; - linkInfoPtr = nullptr; - return false; // Initialization failed - } - else { - // Store the link parameters - linkInfoPtr->port = port; - linkInfoPtr->serial_rx = serial_rx; - linkInfoPtr->serial_tx = serial_tx; - linkInfoPtr->baudrate = baudrate; - linkInfoPtr->dere_pin = dere_pin; - linkInfoPtr->rs485_mode = (dere_pin != -1); - linkInfoPtr->collision_detect = collision_detect; - } - } + else { + # ifdef MODBUS_DEBUG + log += F(" No link available at linkIndex= "); + log += linkId; + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + return false; } - for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { - if (_modbus_devices[i] == nullptr) { - // Found an available device slot - _modbus_devices[i] = new (std::nothrow) ModbusDeviceInfo_struct(); - _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-MAX_MODBUS_DEVICES) - _modbus_devices[i]->link = linkInfoPtr; - *deviceID = _modbus_devices[i]->deviceID; - *link = linkInfoPtr->link; - # ifdef MODBUS_DEBUG - log += F(" Assigned deviceID= "); - log += *deviceID; - # endif // ifdef MODBUS_DEBUG - break; - } + if (*deviceID == -1) { + // No available device slot found, connection failed + # ifdef MODBUS_DEBUG + log += F(" Failed to assign device ID, no available device slots"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + return false; } + # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); dumpAdminInfo(); # endif // ifdef MODBUS_DEBUG - return true; + + return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -161,7 +205,6 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { dumpAdminInfo(); # ifdef MODBUS_DEBUG String log = F("Modbus: Manager, Disconnect device="); - log += deviceID; # endif // ifdef MODBUS_DEBUG @@ -171,33 +214,9 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { ModbusLinkInfo_struct *linkInfoPtr = _modbus_devices[i]->link; // Remove the device entry + delete _modbus_devices[i]; _modbus_devices[i] = nullptr; - // Check if any other devices are using the same link - bool linkInUse = false; - - for (int j = 0; j < MAX_MODBUS_DEVICES; j++) { - if ((_modbus_devices[j] != nullptr) && (_modbus_devices[j]->link == linkInfoPtr)) { - linkInUse = true; - break; - } - } - - if (!linkInUse) { - # ifdef MODBUS_DEBUG - log += F(", No other devices using link, deleting link"); - # endif // ifdef MODBUS_DEBUG - - // No other devices are using this link, so we can delete it - for (int k = 0; k < MAX_MODBUS_LINKS; k++) { - if (_modbus_links[k] == linkInfoPtr) { - delete _modbus_links[k]; - _modbus_links[k] = nullptr; - break; - } - } - } - return true; // Successfully disconnected } } @@ -212,11 +231,13 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::processLinks() { - ModbusLinkInfo_struct *linkInfoPtr = nullptr; + ModbusLinkInfo_struct *linkInfoPtr = nullptr; + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if (_modbus_links[i] != nullptr) { linkInfoPtr = _modbus_links[i]; - linkInfoPtr->link->processCommand(); // Trigger processing of the command queue on the link + + ///////////// linkInfoPtr->link->processCommand(); // Trigger processing of the command queue on the link } } } @@ -266,13 +287,20 @@ void ModbusMGR_struct::dumpAdminInfo() # endif // ifdef MODBUS_DEBUG } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Put the Modbus link configuration on the web page +// This is called from the interfaces configuration page to show the Modbus link configuration. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::show_modbus_interfaces() { - String options_baudrate[MODBUS_MAX_BAUDRATE_SEL]; // Array to hold the baudrate options for the selector - String options_port[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Port otions for the selector, only valid ports will be filled. - int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Map to keep track of valid ports and their indices in the options_port array + String options_baudrate[MODBUS_MAX_BAUDRATE_SEL]; // Array to hold the baudrate options for the selector + String options_port[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Port otions for the selector, only valid ports will be + // filled. + int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Map to keep track of valid ports and their indices in the + // options_port array constexpr size_t optionBaudCount = NR_ELEMENTS(options_baudrate); + for (int i = 0; i < optionBaudCount; ++i) { options_baudrate[i] = modbus_storageValueToBaudrate(i); } @@ -280,37 +308,31 @@ void ModbusMGR_struct::show_modbus_interfaces() int optionPortCount = 1; options_port[0] = F("Not set"); - portMap[0] = 0; // Map the "Not set" option to index 0 + portMap[0] = 0; // Map the "Not set" option to index 0 + for (int i = 1; i < NR_ELEMENTS(options_port); i++) { if (validSerialPort(static_cast(i))) { options_port[optionPortCount] = ESPEasySerialPort_toString(static_cast(i)); - portMap[i] = optionPortCount; // Store the index of the valid port in the options_port array + portMap[i] = optionPortCount; // Store the index of the valid port in the options_port array optionPortCount++; } else { options_port[i] = F("Invalid"); - portMap[i] = 0; // Map invalid/unused ports to the "Not set" option + portMap[i] = 0; // Map invalid/unused ports to the "Not set" option } } const FormSelectorOptions portSelector(optionPortCount, options_port); - for (int i =0; i < NR_ELEMENTS(options_port); i++) { - addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus_mgr: portMap[%d]= %d Port=%s"), - i, - portMap[i], - options_port[portMap[i]].c_str() - )); - } // Iterate over the modbus links and show their configuration on the web page for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { - if (_modbus_links[link] != nullptr) { + if (_modbus_links[link] != nullptr) { addFormSubHeader(strformat(F("Modbus %u"), link)); + addFormDetailsStart(link == 0); int idx = static_cast(_modbus_links[link]->port); portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); - + String id = strformat(F("MBtx%u"), link); addRowLabel_tr_id(formatGpioName_serialTX(false), id); addPinSelect(PinSelectPurpose::Serial_input, id, _modbus_links[link]->serial_tx); @@ -323,109 +345,263 @@ void ModbusMGR_struct::show_modbus_interfaces() addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link]->dere_pin); - baudselector.addFormSelector(F("Baud Rate"), strformat(F("MBbaud%u"), link), modbus_baudrateToStorageValue(_modbus_links[link]->baudrate)); + baudselector.addFormSelector(F("Baud Rate"), + strformat(F("MBbaud%u"), link), + modbus_baudrateToStorageValue(_modbus_links[link]->baudrate)); addUnit(F("baud")); + # ifdef ESP32 addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), link), _modbus_links[link]->collision_detect); addFormNote(F("/RE connected to GND, only supported on hardware serial")); # endif // ifdef ESP32 + + addFormDetailsEnd(); } } } - - +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Convert stored baudrate setting (enumeration value) to actual baudrate value // Returns the actual baudrate value. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int modbus_storageValueToBaudrate(uint8_t baudrate_setting) { int baudrate = 9600; switch (baudrate_setting) { case 0: - baudrate = 1200; break; + baudrate = 1200; + break; case 1: - baudrate = 2400; break; + baudrate = 2400; + break; case 2: - baudrate = 4800; break; + baudrate = 4800; + break; case 3: - baudrate = 9600; break; + baudrate = 9600; + break; case 4: - baudrate = 19200; break; + baudrate = 19200; + break; case 5: - baudrate = 38400; break; + baudrate = 38400; + break; case 6: - baudrate = 57600; break; + baudrate = 57600; + break; case 7: - baudrate = 115200; break; + baudrate = 115200; + break; default: - baudrate = 9600; break; // Default value for fallback + baudrate = 9600; + break; // Default value for fallback } return baudrate; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Convert actual baudrate value to stored baudrate setting (enumeration value) +// Returns the stored baudrate setting. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint8_t modbus_baudrateToStorageValue(int baudrate) { - if (baudrate <= 1200) {return 0;} - else if (baudrate <= 2400) {return 1;} - else if (baudrate <= 4800) {return 2;} - else if (baudrate <= 9600) {return 3;} - else if (baudrate <= 19200) {return 4;} - else if (baudrate <= 38400) {return 5;} - else if (baudrate <= 57600) {return 6;} - else if (baudrate <= 115200) {return 7;} - else {return 3;} // Default to 9600 baud for unsupported values + if (baudrate <= 1200) { return 0; } + else if (baudrate <= 2400) { return 1; } + else if (baudrate <= 4800) { return 2; } + else if (baudrate <= 9600) { return 3; } + else if (baudrate <= 19200) { return 4; } + else if (baudrate <= 38400) { return 5; } + else if (baudrate <= 57600) { return 6; } + else if (baudrate <= 115200) { return 7; } + else { return 3; } // Default to 9600 baud for unsupported values } -bool ModbusMGR_struct::save_modbus_interfaces(String &error) +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// process the Modbus link configuration from the web page save action and update the Modbus manager configuration +// This is called from the interfaces configuration page to show the Modbus link configuration. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::save_modbus_interfaces(String& error) { int portCount = 1; + // Create mapping table from dropdown enum index to actual port identifier, index 0 is reserved for "Not set" int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; + portMap[0] = 0; // Map the "Not set" option to index 0 + for (int i = 1; i < NR_ELEMENTS(portMap); i++) { if (validSerialPort(static_cast(i))) { - portMap[portCount++] = i; + portMap[portCount++] = i; } } - for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { - int port = 0; - if (_modbus_links[link] != nullptr) { - if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port)) { - _modbus_links[link]->port = static_cast(portMap[port]); - addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus_mgr: New Port[%d]= %d (%s)"), - link, - portMap[port], - ESPEasySerialPort_toString(_modbus_links[link]->port) - )); + for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { + int port_setting = 0; + int baudrate_setting = 0; + int tx_setting = 0; + int rx_setting = 0; + int dere_setting = 0; + bool collision_detect_setting = false; + bool settingsChanged = false; + + if (_modbus_links[link] != nullptr) { + for (int i = 0; i < NR_ELEMENTS(portMap); i++) { + if (portMap[i] == static_cast(_modbus_links[link]->port)) { + port_setting = i; + break; + } + } + + if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting)) { + settingsChanged |= (portMap[port_setting] != static_cast(_modbus_links[link]->port)); } - int baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link]->baudrate); + baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link]->baudrate); + if (update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting)) { - _modbus_links[link]->baudrate = modbus_storageValueToBaudrate(baudrate_setting); + settingsChanged |= (modbus_storageValueToBaudrate(baudrate_setting) != _modbus_links[link]->baudrate); } - int tx_setting = _modbus_links[link]->serial_tx; + tx_setting = _modbus_links[link]->serial_tx; + if (update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting)) { - _modbus_links[link]->serial_tx = tx_setting; + settingsChanged |= (tx_setting != _modbus_links[link]->serial_tx); } - int rx_setting = _modbus_links[link]->serial_rx; + rx_setting = _modbus_links[link]->serial_rx; + if (update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting)) { - _modbus_links[link]->serial_rx = rx_setting; + settingsChanged |= (rx_setting != _modbus_links[link]->serial_rx); } - int dere_setting = _modbus_links[link]->dere_pin; + dere_setting = _modbus_links[link]->dere_pin; + if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting)) { - _modbus_links[link]->dere_pin = dere_setting; + settingsChanged |= (dere_setting != _modbus_links[link]->dere_pin); # ifdef ESP32 + // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. - bool collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); - _modbus_links[link]->collision_detect = collision_detect_setting; + collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); + settingsChanged |= (collision_detect_setting != _modbus_links[link]->collision_detect); # endif // ifdef ESP32 } } + + if (settingsChanged) { + setLink(link, + static_cast(portMap[port_setting]), + rx_setting, + tx_setting, + modbus_storageValueToBaudrate(baudrate_setting), + dere_setting, + collision_detect_setting); + } } - dumpAdminInfo(); //TODO: remove after debugging return false; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Setup the Modbus link with the specified parameters. Settings will be persisted on disk. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::setLink(const int linkIndex, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect) +{ + ModbusLinkInfo_struct *linkInfoPtr = nullptr; + + + # ifdef MODBUS_DEBUG + String log = F("Modbus_mgr: SetLink. "); + + addLogMove(LOG_LEVEL_INFO, + strformat(F("*** setlink***: LinkID=%d, Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, CollisionDetect=%s"), + linkIndex, + ESPEasySerialPort_toString(port), + serial_rx, + serial_tx, + baudrate, + dere_pin, + collision_detect ? F("Yes") : F("No") + )); + # endif // ifdef MODBUS_DEBUG + + if ((linkIndex >= 0) && (linkIndex < MAX_MODBUS_LINKS)) { + if (_modbus_links[linkIndex] == nullptr) { + linkInfoPtr = new (std::nothrow) ModbusLinkInfo_struct(); + _modbus_links[linkIndex] = linkInfoPtr; + # ifdef MODBUS_DEBUG + log += strformat(F("New link for linkIndex=%d"), linkIndex); + # endif // ifdef MODBUS_DEBUG + } + else { + linkInfoPtr = _modbus_links[linkIndex]; // Link admin already exists, will be reused for the new link configuration + # ifdef MODBUS_DEBUG + log += strformat(F("Existing link for linkIndex=%d"), linkIndex); + # endif // ifdef MODBUS_DEBUG + } + } + else { + log += strformat(F("Invalid link for linkIndex=%d"), linkIndex); + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return false; // Invalid link index + } + + if (linkInfoPtr != nullptr) { // Sanity check for successful link admin creation + if (linkInfoPtr->link == nullptr) { // Check if link object already exists + // No existing link, create a new one + linkInfoPtr->link = new (std::nothrow) ModbusLINK_struct(); + } + + if (linkInfoPtr->link != nullptr) { // Sanity check for successful creation + // (re)initialize the new link + if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + // Initialization failed, clean up + delete linkInfoPtr->link; + linkInfoPtr->link = nullptr; + delete linkInfoPtr; + linkInfoPtr = nullptr; + return false; // Initialization failed + } + else { + // Store the link parameters + linkInfoPtr->port = port; + linkInfoPtr->serial_rx = serial_rx; + linkInfoPtr->serial_tx = serial_tx; + linkInfoPtr->baudrate = baudrate; + linkInfoPtr->dere_pin = dere_pin; + linkInfoPtr->rs485_mode = (dere_pin != -1); + linkInfoPtr->collision_detect = collision_detect; + } + } + } + else { + return false; // Failed to create link admin + } + + if (_modbus_links[linkIndex]->kvs == nullptr) { + _modbus_links[linkIndex]->kvs = new (std::nothrow) ESPEasy_key_value_store; + } + + if (_modbus_links[linkIndex]->kvs) { + // Store the link configuration parameters in the key-value store for persistence + linkInfoPtr->kvs->setValue(MODBUS_PORT_KEY_INDEX, static_cast(port)); + linkInfoPtr->kvs->setValue(MODBUS_RX_KEY_INDEX, static_cast(serial_rx)); + linkInfoPtr->kvs->setValue(MODBUS_TX_KEY_INDEX, static_cast(serial_tx)); + linkInfoPtr->kvs->setValue(MODBUS_BAUDRATE_KEY_INDEX, static_cast(baudrate)); + linkInfoPtr->kvs->setValue(MODBUS_DERE_PIN_KEY_INDEX, static_cast(dere_pin)); + linkInfoPtr->kvs->setValue(MODBUS_COLLISION_DETECT_KEY_INDEX, static_cast(collision_detect)); + + linkInfoPtr->kvs->store(SettingsType::Enum::ModbusInterfaceSettings_Type, linkIndex, 0, 0); + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, F("Modbus: setlink successfull")); + dumpAdminInfo(); + # endif // ifdef MODBUS_DEBUG + return true; +} + #endif // if FEATURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index ff0a5971c6..64d713074e 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -7,6 +7,7 @@ # include # include "Modbus_link.h" +# include "../../src/Helpers/_ESPEasy_key_value_store.h" // ModbusMGR structure representing the singleton Modbus Management entity @@ -15,25 +16,12 @@ // The modbus manager is not involved in the actual data transport, this is handled by a direct relation between Modbus device and // ModbusLINK object. typedef struct ModbusMGR_struct { - ModbusMGR_struct() = default; - + ModbusMGR_struct(); ~ModbusMGR_struct(); - void reset(); + bool initialize(); - bool connect(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - ModbusLINK_struct **link, - uint8_t *deviceID); - - bool connect(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect, + bool connect(int linkId, ModbusLINK_struct **link, uint8_t *deviceID); @@ -48,28 +36,39 @@ typedef struct ModbusMGR_struct { private: - static const int MAX_MODBUS_LINKS = 5; // Maximum number of Modbus links supported + static const int MAX_MODBUS_LINKS = 4; // Maximum number of Modbus links supported static const int MAX_MODBUS_DEVICES = 16; // Maximum number of Modbus devices supported + // Structure representing the information of a Modbus link, including its configuration and associated ModbusLINK object struct ModbusLinkInfo_struct { ESPEasySerialPort port = ESPEasySerialPort::not_set; - int16_t serial_rx = -1; - int16_t serial_tx = -1; + int8_t serial_rx = -1; + int8_t serial_tx = -1; int16_t baudrate = 9600; int8_t dere_pin = -1; // Pin used for RS485 DE/RE control, -1 if not used bool rs485_mode = false; // True if RS485 mode is enabled bool collision_detect = false; // True if collision detection is enabled struct ModbusLINK_struct *link = nullptr; // Pointer to the Modbus link object + ESPEasy_key_value_store *kvs = nullptr; // Key-value store for storing link-specific settings and parameters }; + // Structure representing the information of a Modbus device, including assocuited ModbusDEVICE object struct ModbusDeviceInfo_struct { uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager - struct ModbusDEVICE_struct *device = nullptr; // Pointer to the Modbus device object struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info }; ModbusLinkInfo_struct *_modbus_links[MAX_MODBUS_LINKS] = { nullptr }; // Pointer to the Modbus link object ModbusDeviceInfo_struct *_modbus_devices[MAX_MODBUS_DEVICES] = { nullptr }; // Array of connected Modbus devices + + bool setLink(const int linkIndex, + const ESPEasySerialPort port, + const int16_t serial_rx, + const int16_t serial_tx, + int16_t baudrate, + int8_t dere_pin, + bool collision_detect); + } ModbusMGR_struct_t; extern ModbusMGR_struct_t ModbusMGR_singleton; // Singleton instance of the Modbus Manager diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 77c44c76cd..01cb5ce167 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -1,4 +1,5 @@ #include "../PluginStructs/P183_data_struct.h" +#include "P183_data_struct.h" #ifdef USES_P183 @@ -20,15 +21,10 @@ P183_data_struct::~P183_data_struct() { _modbusDevice = nullptr; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool P183_data_struct::plugin_init(uint8_t slaveAddress, - const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect) { - // Create a fresh Modbus_device object to handle the Modbus communication +bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) +{ + //// TODO Implement + // Create a fresh Modbus_device object to handle the Modbus communication if (_modbusDevice != nullptr) { delete _modbusDevice; _modbusDevice = nullptr; @@ -42,29 +38,12 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, // Initialize our own Modbus_device with the provided serial link parameters // Note that the link configuration is expected to be the same for all plugins reusing the same serial port - if (!_modbusDevice->init(slaveAddress, port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + if (!_modbusDevice->init(slaveAddress, linkId)) { return false; } _modbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); - - # ifdef P183_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - addLogMove(LOG_LEVEL_DEBUG, - strformat(F("P183: Init address %d, RX pin %d, TX pin %d, RS485 mode selected on pin %d, baudrate %d, collision detection %s"), - slaveAddress, - serial_rx, - serial_tx, - dere_pin, - baudrate, - collision_detect ? F("enabled") : F("disabled"))); - } - # endif // ifdef P183_DEBUG - - - return true; + return false; } - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::plugin_exit() { diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 5f58db7944..cc768ebb56 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -13,7 +13,7 @@ // Plugin configuration parameters // PCONFIG(0) is the Modbus device ID. -// PCONFIG(1) is the serial baud rate. +// PCONFIG(1) is the Modbus link ID. // PCONFIG(2) is used for flags, where bit 0 indicates collision detection // PCONFIG(3) is the number of active output values (1-4) // PCONFIG(4) is the Modbus register address for value 1 @@ -23,8 +23,8 @@ // Use P183_ADDRESS(x) to access the PCONFIG value for value x # define P183_DEV_ID PCONFIG(0) # define P183_DEV_ID_LABEL PCONFIG_LABEL(0) -# define P183_BAUDRATE PCONFIG(1) -# define P183_BAUDRATE_LABEL PCONFIG_LABEL(1) +# define P183_LINK_ID PCONFIG(1) +# define P183_LINK_ID_LABEL PCONFIG_LABEL(1) # define P183_NR_OUTPUTS PCONFIG(3) # define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) # define P183_ADDRESS(x) PCONFIG(4 + x) @@ -34,11 +34,7 @@ # define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) # define P183_FLAG_COLL_DETECT_LABEL "colldet" -# define P183_DEPIN CONFIG_PIN3 - # define P183_DEV_ID_DFLT 1 -# define P183_BAUDRATE_DFLT 3 // 9600 baud -# define P183_MAX_BAUDRATE_SEL 8 # define P183_MODBUS_TIMEOUT 1000 // milliseconds # define P183_MAX_MODBUS_NODES 247 # define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address @@ -59,6 +55,9 @@ struct P183_data_struct : public PluginTaskData_base { int8_t dere_pin, bool collision_detect); + bool plugin_init(uint8_t slaveAddress, + int linkId); + void plugin_exit(); bool plugin_read(struct EventStruct *event); bool plugin_once_a_second(struct EventStruct *event); @@ -78,6 +77,7 @@ struct P183_data_struct : public PluginTaskData_base { uint16_t _registerValues[4] = {}; // Modus register values retrieved for output values ModbusResultState _queueStates[4] = {}; // State of read hloding register transactions ModbusResultState _lastActionState = ModbusResultState::Busy; + }; #endif // ifdef USES_P183 From 2e2ab3476e27ec22b6240dbfeee18df08e7526fc Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:03:35 +0200 Subject: [PATCH 24/48] Save state some init problems left --- src/_P183_modbus.ino | 14 --- src/src/DataTypes/SettingsType.cpp | 2 + src/src/Helpers/Modbus_device.cpp | 11 -- src/src/Helpers/Modbus_device.h | 2 - src/src/Helpers/Modbus_link.cpp | 119 ++++++++------------ src/src/Helpers/Modbus_link.h | 22 +--- src/src/Helpers/Modbus_mgr.cpp | 28 ++--- src/src/Helpers/Modbus_mgr.h | 25 ++-- src/src/Helpers/PeriodicalActions.cpp | 7 ++ src/src/Helpers/Scheduler_IntervalTimer.cpp | 8 -- src/src/PluginStructs/P183_data_struct.cpp | 17 +-- src/src/PluginStructs/P183_data_struct.h | 6 +- 12 files changed, 98 insertions(+), 163 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index d1ebb5adb1..d8d1394be8 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -112,11 +112,6 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); - # ifdef ESP32 - addFormCheckBox(F("Enable Collision Detection"), F(P183_FLAG_COLL_DETECT_LABEL), P183_GET_FLAG_COLL_DETECT); - addFormNote(F("/RE connected to GND, only supported on hardware serial")); - # endif // ifdef ESP32 - success = true; break; } @@ -261,15 +256,6 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } break; } - case PLUGIN_TEN_PER_SECOND: - { - P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); - - if (P183_data != nullptr) { - P183_data->plugin_ten_per_second(event); - } - break; - } } return success; diff --git a/src/src/DataTypes/SettingsType.cpp b/src/src/DataTypes/SettingsType.cpp index 8d77844094..7f76378f79 100644 --- a/src/src/DataTypes/SettingsType.cpp +++ b/src/src/DataTypes/SettingsType.cpp @@ -278,8 +278,10 @@ SettingsType::SettingsFileEnum SettingsType::getSettingsFile(Enum settingsType) #if FEATURE_STORE_NETWORK_INTERFACE_SETTINGS case Enum::NetworkInterfaceSettings_Type: #endif +#if FEATURE_MODBUS_FAC case Enum::ModbusInterfaceSettings_Type: return SettingsFileEnum::FILE_CONFIG_type; +#endif //FEATURE_MODBUS_FAC case Enum::NotificationSettings_Type: return SettingsFileEnum::FILE_NOTIFICATION_type; case Enum::SecuritySettings_Type: diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 90d8cf5a60..b6d2521a16 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -169,17 +169,6 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, return true; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Periodic processing function to allow the Modbus device to process its queued requests -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::processCommand() { - // TODO: This function is currently not used since the Modbus link processing is triggered directly from the scheduler. - if (_modbus_link != nullptr) { - ////_modbus_link->processCommand(); // Trigger processing of the command queue on the link - ////ModbusMGR_singleton.processLinks(); - } -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Callback function called by the Modbus link when a response is received for a queued request. // Note that the response might be an invalid response or a timeout diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 18eeeedb2a..081fe82849 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -43,8 +43,6 @@ bool init( uint8_t slaveAddress, uint16_t getModbusTimeout() const; - void processCommand(); - void linkCallback(Modbus_RequestQueueElement *transaction); // Start reading a Modubus holding register. The result will be available later. diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 096a2ec31d..a09dcb7e77 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MODBUS link class -// This class implements a Modbus link over a serial connection. It is part of the Modbus facilities supporting multiple Modbus +// This class implements a Modbus link over a serial connection. It is part of the Modbus facilities supporting multiple Modbus // devices on multiple serial Modbus links. // It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. // It exepcts a Modbus device instance to construct and interpret the Modbus messages for the specific device. @@ -45,48 +45,50 @@ void ModbusLINK_struct::reset() { } } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Initialize the link with the given serial port and parameters -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::init(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate) { - return init(port, serial_rx, serial_tx, baudrate, -1); -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Initialize the link with the given serial port and parameters, including a dere pin for RS485 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusLINK_struct::init(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, + const int8_t serial_rx, + const int8_t serial_tx, int16_t baudrate, int8_t dere_pin, bool collision_detect) { + bool rs485Mode = false; + int available = 0; + // (re)create the serial port object // If the serial port object already exists, delete it first. - if (ModbusLINK_struct::_easySerial != nullptr) { - delete ModbusLINK_struct::_easySerial; - ModbusLINK_struct::_easySerial = nullptr; + if (_easySerial != nullptr) { + delete _easySerial; + _easySerial = nullptr; } - ModbusLINK_struct::_easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx); + _easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx); - if (ModbusLINK_struct::_easySerial == nullptr) { + if (_easySerial == nullptr) { return false; } // Set RS485 mode if requested using selected pin for RTS - bool rs485Mode = ModbusLINK_struct::_easySerial->setRS485Mode(dere_pin, collision_detect); - - ModbusLINK_struct::_easySerial->begin(baudrate); - _dere_pin = dere_pin; + rs485Mode = _easySerial->setRS485Mode(dere_pin, collision_detect); + _easySerial->begin(baudrate); + _easySerial->flush(); + available = _easySerial->available(); + + if (available > 0) { + // Clear any pending input + for (int i = available; i > 0; --i) { + _easySerial->read(); + } + } + rs485Mode = _easySerial->setRS485Mode(dere_pin, collision_detect); + _dere_pin = dere_pin; _collision_detect = collision_detect; - + # ifdef MODBUS_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("Modbus: Link, Init serial, RX pin "); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("---> Modbus: Link, Init serial, RX pin "); log += serial_rx; log += F(", TX pin "); log += serial_tx; @@ -98,9 +100,12 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, log += collision_detect ? F("enabled") : F("disabled"); log += F(", RS485mode enabled: "); log += rs485Mode ? F("yes") : F("no"); - addLogMove(LOG_LEVEL_DEBUG, log); + log += F(", Serial isRxEnabled: "); + log += _easySerial->isRxEnabled() ? F("yes") : F("no"); + addLogMove(LOG_LEVEL_INFO, log); } # endif // MODBUS_DEBUG + _initialized = true; return true; } @@ -111,35 +116,6 @@ bool ModbusLINK_struct::isInitialized() const { return _easySerial != nullptr; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Reconfigure the link with new parameters. If the serial port is the same, it is reconfigured, otherwise it is reinitialized. -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::reconfigure(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect) -{ - if (isInitialized()) { - if (_easySerial->getSerialPortType() == port) { - // Same port, just reconfigure - _easySerial->resetConfig(port, serial_rx, serial_tx); - _easySerial->begin(baudrate); - _dere_pin = dere_pin; - _collision_detect = collision_detect; - return true; - } - else { - return init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect); - } - } - else { - return init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect); - } - return false; -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Provide a new transaction structure that can be used to build a Modbus request and queue it at this link /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -213,21 +189,25 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processCommand() { - if (_easySerial == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("Modbus: Link, Serial port not initialized")); - return; // Serial port not initialized + if ((!_initialized) || (_easySerial == nullptr)) { + return; // Serial port not initialized + } + + if (_requestQueue.empty()) { + return; // No transactions to process } auto it = _requestQueue.begin(); // Iterator for the request queue bool busy = false; // Only process one request at a time - while ((it != _requestQueue.end()) && !busy) { + while ((it != _requestQueue.end()) && !busy && ((*it) != nullptr)) { # ifdef MODBUS_DEBUG dumpQueueElement(*it); dumpState((*it)->_state); # endif // MODBUS_DEBUG - switch ((*it)->_state) { + switch ((*it)->_state) + { case ModbusQueueState::QUEUED: { // Send the request @@ -255,7 +235,7 @@ void ModbusLINK_struct::processCommand() (*it)->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback(*it); // Notify the device that a response was received + (*it)->_device->linkCallback((*it)); // Notify the device that a response was received } } else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { @@ -263,7 +243,7 @@ void ModbusLINK_struct::processCommand() (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback(*it); // Notify the device that a response was received + (*it)->_device->linkCallback((*it)); // Notify the device that a response was received } else {} it++; @@ -299,32 +279,32 @@ void ModbusLINK_struct::processCommand() int16_t ModbusLINK_struct::getBaudrate(void) const { -return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; + return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; } ESPEasySerialPort ModbusLINK_struct::getPort(void) const { -return ESPEasySerialPort(); + return ESPEasySerialPort(); } int16_t ModbusLINK_struct::getSerialRX(void) const { -return _easySerial != nullptr ? _easySerial->getRxPin() : -1; + return _easySerial != nullptr ? _easySerial->getRxPin() : -1; } int16_t ModbusLINK_struct::getSerialTX(void) const { -return _easySerial != nullptr ? _easySerial->getTxPin() : -1; + return _easySerial != nullptr ? _easySerial->getTxPin() : -1; } int8_t ModbusLINK_struct::getDerePin(void) const { -return _dere_pin; + return _dere_pin; } bool ModbusLINK_struct::getCollisionDetect(void) const { -return _collision_detect; + return _collision_detect; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -367,7 +347,8 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { } const __FlashStringHelper* toString(ModbusQueueState_t state) { - switch (state) { + switch (state) + { case ModbusQueueState::NOT_QUEUED: return F("NOT_QUEUED"); case ModbusQueueState::QUEUED: diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 820fcd689f..092804f3bf 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -76,25 +76,13 @@ struct ModbusLINK_struct { void reset(); bool init(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate); - - bool init(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, + const int8_t serial_rx, + const int8_t serial_tx, int16_t baudrate, int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const; - - bool reconfigure(const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect = false); + bool isInitialized() const; Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); bool freeTransaction(Modbus_RequestQueueElement *transaction); @@ -128,8 +116,8 @@ struct ModbusLINK_struct { uint32_t _reads_nodata = 0; // TODO: statistics uint8_t _dere_pin = 0; // Pin for RS485 direction control bool _collision_detect = false; // Flag to indicate if collision detection is enabled - - uint8_t _last_error = 0; + bool _initialized = false; + uint8_t _last_error = 0; }; diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 1723eaf201..cb4b8b6bc6 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -93,6 +93,7 @@ ModbusMGR_struct::~ModbusMGR_struct() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::initialize() { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if (_modbus_links[i] == nullptr) { int8_t val; @@ -100,9 +101,9 @@ bool ModbusMGR_struct::initialize() _modbus_links[i]->link = nullptr; _modbus_links[i]->kvs = new (std::nothrow) ESPEasy_key_value_store; _modbus_links[i]->kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); - # ifdef MODBUS_DEBUG - _modbus_links[i]->kvs->dump(); - # endif + ////# ifdef MODBUS_DEBUG + ////_modbus_links[i]->kvs->dump(); + ////# endif _modbus_links[i]->kvs->getValue(MODBUS_PORT_KEY_INDEX, val); _modbus_links[i]->port = static_cast(val); _modbus_links[i]->kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i]->serial_rx); @@ -122,7 +123,8 @@ bool ModbusMGR_struct::initialize() } } } - + _initialized = true; + # ifdef MODBUS_DEBUG dumpAdminInfo(); # endif // ifdef MODBUS_DEBUG @@ -231,13 +233,11 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::processLinks() { - ModbusLinkInfo_struct *linkInfoPtr = nullptr; - - for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if (_modbus_links[i] != nullptr) { - linkInfoPtr = _modbus_links[i]; - - ///////////// linkInfoPtr->link->processCommand(); // Trigger processing of the command queue on the link + if (_initialized) { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { + if ((_modbus_links[i] != nullptr) && (_modbus_links[i]->link != nullptr)) { + _modbus_links[i]->link->processCommand(); // Trigger processing of the command queue on the link + } } } } @@ -349,7 +349,7 @@ void ModbusMGR_struct::show_modbus_interfaces() strformat(F("MBbaud%u"), link), modbus_baudrateToStorageValue(_modbus_links[link]->baudrate)); addUnit(F("baud")); - + # ifdef ESP32 addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), link), _modbus_links[link]->collision_detect); addFormNote(F("/RE connected to GND, only supported on hardware serial")); @@ -502,8 +502,8 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::setLink(const int linkIndex, const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, + const int8_t serial_rx, + const int8_t serial_tx, int16_t baudrate, int8_t dere_pin, bool collision_detect) diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 64d713074e..60fff679f3 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -6,8 +6,8 @@ #if FEATURE_MODBUS_FAC # include -# include "Modbus_link.h" -# include "../../src/Helpers/_ESPEasy_key_value_store.h" +# include "../Helpers/Modbus_link.h" +# include "../Helpers/_ESPEasy_key_value_store.h" // ModbusMGR structure representing the singleton Modbus Management entity @@ -21,14 +21,14 @@ typedef struct ModbusMGR_struct { bool initialize(); - bool connect(int linkId, - ModbusLINK_struct **link, - uint8_t *deviceID); + bool connect(int linkId, + ModbusLINK_struct **link, + uint8_t *deviceID); bool disconnect(uint8_t deviceID); void processLinks(); - + void dumpAdminInfo(); void show_modbus_interfaces(); @@ -50,28 +50,31 @@ typedef struct ModbusMGR_struct { bool collision_detect = false; // True if collision detection is enabled struct ModbusLINK_struct *link = nullptr; // Pointer to the Modbus link object ESPEasy_key_value_store *kvs = nullptr; // Key-value store for storing link-specific settings and parameters + }; // Structure representing the information of a Modbus device, including assocuited ModbusDEVICE object struct ModbusDeviceInfo_struct { - uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager - struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info + uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager + struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info + }; ModbusLinkInfo_struct *_modbus_links[MAX_MODBUS_LINKS] = { nullptr }; // Pointer to the Modbus link object ModbusDeviceInfo_struct *_modbus_devices[MAX_MODBUS_DEVICES] = { nullptr }; // Array of connected Modbus devices + bool _initialized = false; // Flag indicating if the manager is initialized bool setLink(const int linkIndex, const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, + const int8_t serial_rx, + const int8_t serial_tx, int16_t baudrate, int8_t dere_pin, bool collision_detect); } ModbusMGR_struct_t; -extern ModbusMGR_struct_t ModbusMGR_singleton; // Singleton instance of the Modbus Manager +extern ModbusMGR_struct_t ModbusMGR_singleton; // Singleton instance of the Modbus Manager #endif // FEAURE_MODBUS #endif // HELPERS_MODBUS_MGR_H diff --git a/src/src/Helpers/PeriodicalActions.cpp b/src/src/Helpers/PeriodicalActions.cpp index ec05a25ee8..5cf07b91d8 100644 --- a/src/src/Helpers/PeriodicalActions.cpp +++ b/src/src/Helpers/PeriodicalActions.cpp @@ -49,6 +49,9 @@ #include "../Helpers/MDNS_Helper.h" #endif +#if FEATURE_MODBUS_FAC +#include "../Helpers/Modbus_mgr.h" +#endif #define PLUGIN_ID_MQTT_IMPORT 37 @@ -136,6 +139,10 @@ void run10TimesPerSecond() { Blynk_Run_c015(); } #endif + #if FEATURE_MODBUS_FAC + ModbusMGR_singleton.processLinks(); + #endif + if (!UseRTOSMultitasking && (ESPEasy::net::NetworkConnected() || ESPEasy::net::wifi::wifiAPmodeActivelyUsed())) { // FIXME TD-er: What about client connected via AP? diff --git a/src/src/Helpers/Scheduler_IntervalTimer.cpp b/src/src/Helpers/Scheduler_IntervalTimer.cpp index 39f346ccb6..7b6f3a7144 100644 --- a/src/src/Helpers/Scheduler_IntervalTimer.cpp +++ b/src/src/Helpers/Scheduler_IntervalTimer.cpp @@ -13,10 +13,6 @@ #include "../Helpers/Networking.h" #include "../Helpers/PeriodicalActions.h" -#if FEATURE_MODBUS_FAC -#include "../Helpers/Modbus_mgr.h" -#endif - /*********************************************************************************************\ * Interval Timer * These timers set a new scheduled timer, based on the old value. @@ -168,10 +164,6 @@ void ESPEasy_Scheduler::process_interval_timer(SchedulerTimerID timerID, unsigne if (!UseRTOSMultitasking) { run10TimesPerSecond(); } - // TODO: Flashmark Hackey way to hook the modbus here. Needs a better way to trigger the modbus processing at regular intervals. -#if FEATURE_MODBUS_FAC - ModbusMGR_singleton.processLinks(); -#endif break; case SchedulerIntervalTimer_e::TIMER_1SEC: runOncePerSecond(); break; case SchedulerIntervalTimer_e::TIMER_30SEC: runEach30Seconds(); break; diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 01cb5ce167..abb6dfe58b 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -61,10 +61,7 @@ bool P183_data_struct::plugin_once_a_second(struct EventStruct *event) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool P183_data_struct::plugin_ten_per_second(struct EventStruct *event) { - if (_modbusDevice != nullptr) { - _modbusDevice->processCommand(); - } - + // No actions return true; } @@ -101,10 +98,6 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e _modbusDevice->readHoldingRegister(1, &value, &state); - while (state == ModbusResultState::Busy) { - _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue - } - if (state == ModbusResultState::Success) { addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), reg, reg, value, value)); } else { @@ -129,9 +122,9 @@ void P183_data_struct::scan_modbus() for (uint8_t id = 0; id <= 247; id++) { _modbusDevice->readModuleHoldingRegister(id, 1, &value, &state); - while (state == ModbusResultState::Busy) { - _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue - } +//// while (state == ModbusResultState::Busy) { +//// _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue +//// } String s = state == ModbusResultState::Success ? F(" OK") : F(" no response"); addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) %s"), id, id, s)); } @@ -152,7 +145,7 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { while (state == ModbusResultState::Busy) { delay(50); - _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue +//// _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } return value; diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index cc768ebb56..2726705272 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -14,7 +14,7 @@ // Plugin configuration parameters // PCONFIG(0) is the Modbus device ID. // PCONFIG(1) is the Modbus link ID. -// PCONFIG(2) is used for flags, where bit 0 indicates collision detection +// PCONFIG(2) is used for flags for future use. Currently not used. // PCONFIG(3) is the number of active output values (1-4) // PCONFIG(4) is the Modbus register address for value 1 // PCONFIG(5) is the Modbus register address for value 2 @@ -30,10 +30,6 @@ # define P183_ADDRESS(x) PCONFIG(4 + x) # define P183_ADDRESS_LABEL(x) concat(F("addr"), x) -# define P183_GET_FLAG_COLL_DETECT bitRead(PCONFIG(2), 0) -# define P183_SET_FLAG_COLL_DETECT(x) bitWrite(PCONFIG(2), 0, x) -# define P183_FLAG_COLL_DETECT_LABEL "colldet" - # define P183_DEV_ID_DFLT 1 # define P183_MODBUS_TIMEOUT 1000 // milliseconds # define P183_MAX_MODBUS_NODES 247 From 470d3c25b4cf6676c0dbd6e1f292e8055f99a735 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:30:06 +0200 Subject: [PATCH 25/48] securing state, not ready yet Co-authored-by: Copilot --- misc/modbusFacility/Modbus_link_state.puml | 10 ++ misc/modbusFacility/Modbus_seq2.puml | 8 +- src/_P183_modbus.ino | 29 +++-- src/src/Helpers/Modbus_device.cpp | 98 +++++++++----- src/src/Helpers/Modbus_device.h | 43 ++++-- src/src/Helpers/Modbus_link.cpp | 67 ++++------ src/src/Helpers/Modbus_link.h | 13 +- src/src/Helpers/Modbus_mgr.cpp | 19 ++- src/src/Helpers/Modbus_mgr.h | 4 +- src/src/PluginStructs/P183_data_struct.cpp | 145 +++++++++++++++------ src/src/PluginStructs/P183_data_struct.h | 24 ++-- 11 files changed, 291 insertions(+), 169 deletions(-) create mode 100644 misc/modbusFacility/Modbus_link_state.puml diff --git a/misc/modbusFacility/Modbus_link_state.puml b/misc/modbusFacility/Modbus_link_state.puml new file mode 100644 index 0000000000..9c4485d93e --- /dev/null +++ b/misc/modbusFacility/Modbus_link_state.puml @@ -0,0 +1,10 @@ +@startuml +[*] --> NOT_QUEUED +NOT_QUEUED --> QUEUED : queueTransaction() +QUEUED --> MESSAGE_SENT : link inactive +MESSAGE_SENT --> RESPONSE_RECEIVED : received valid response +MESSAGE_SENT --> ERROR_OCCURRED : response timeout or invalid response +RESPONSE_RECEIVED --> READY_FOR_DESTROY : freeTransaction() +ERROR_OCCURRED --> READY_FOR_DESTROY : freeTransaction() +READY_FOR_DESTROY --> [*] +@enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq2.puml b/misc/modbusFacility/Modbus_seq2.puml index abaff06842..54206e13d2 100644 --- a/misc/modbusFacility/Modbus_seq2.puml +++ b/misc/modbusFacility/Modbus_seq2.puml @@ -23,6 +23,8 @@ plugin_struct <-- modbus_device -- plugin <-- plugin_struct -- system <-- plugin -- +== read == + system -> plugin ++ : read plugin -> plugin_struct ++ : read() plugin_struct -> modbus_device ++ : readHoldingRegister() @@ -38,7 +40,11 @@ system -> modbus_mgr ++ : ten_per_second modbus_mgr -> modbus_link ++: processCommand() alt response available modbus_link -> modbus_device ++ : linkCallback() - modbus_device -> plugin : update event (new) + modbus_device -> plugin ++ : PLUGIN_TASKTIMER_IN + plugin -> plugin_struct ++ : plugin_task_timer() + plugin <-- plugin_struct -- + modbus_device <-- plugin -- + modbus_device -> modbus_link : freeTransaction() modbus_link <-- modbus_device -- end alt no transaction diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index d8d1394be8..8dbd7afedd 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -46,7 +46,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) { auto& dev = Device[++deviceCount]; dev.Number = PLUGIN_ID_183; - dev.Type = DEVICE_TYPE_CUSTOM0; //////DEVICE_TYPE_SERIAL_PLUS1; // connected through 3 datapins + dev.Type = DEVICE_TYPE_CUSTOM0; // Custom device type, connects to Modbus dev.VType = Sensor_VType::SENSOR_TYPE_QUAD; dev.FormulaOption = true; dev.ValueCount = P183_NR_OUTPUT_VALUES; @@ -80,15 +80,14 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SHOW_CONFIG: { - //string += serialHelper_getSerialTypeLabel(event); success = true; break; } case PLUGIN_SET_DEFAULTS: { - P183_DEV_ID = P183_DEV_ID_DFLT; - success = true; + P183_DEV_ID = P183_DEV_ID_DFLT; + success = true; break; } @@ -109,8 +108,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { - addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); - addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); + addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); + addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); success = true; break; @@ -118,8 +117,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); - P183_LINK_ID = getFormItemInt(P183_LINK_ID_LABEL); + P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); + P183_LINK_ID = getFormItemInt(P183_LINK_ID_LABEL); P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) @@ -169,6 +168,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) success = P183_data->plugin_read(event); // Delegate to data_struct break; } + case PLUGIN_WRITE: { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); @@ -236,6 +236,19 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } + + case PLUGIN_TASKTIMER_IN: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus task timer invalid data struct")); + return false; + } + success = P183_data->plugin_task_timer(event); // Delegate to data_struct + break; + } + case PLUGIN_GET_CONFIG_VALUE: { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index b6d2521a16..b5248bb8dc 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MODBUS device class -// This class implements a single Modbus device connected over a serial link. +// This class implements a single Modbus device connected over a serial link. // It is part of the Modbus facilities supporting multiple Modbus devices on multiple serial Modbus links. // It supports queuing Modbus requests and responses for multiple Modbus devices sharing the same physical link. // The Modbus device class will interpret the Modbus messages for the connected hardware and queue it at the link class. @@ -11,9 +11,9 @@ # include "../Helpers/Modbus_device.h" # include "../Helpers/Modbus_mgr.h" -#include "Modbus_device.h" +# include "Modbus_device.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -52,20 +52,25 @@ void ModbusDEVICE_struct::reset() { } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Initialization connected to an existing link. +// Initialization connected to an existing link. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId) +bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t taskIndex) { - bool success = ModbusMGR_singleton.connect(linkId, &_modbus_link, &_deviceID); + bool success = ModbusMGR_singleton.connect(linkId, &_modbus_link, &_deviceID); + _modbus_address = slaveAddress; - # ifdef MODBUS_DEBUG + _taskIndex = taskIndex; + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus: Device Init, Slave address = %u, This = %p, deviceID = %u, linkId=%d"), slaveAddress, this, _deviceID, linkId)); + strformat(F("Modbus: Device Init, Slave address = %u, This = %p, deviceID = %u, linkId=%d, taskIndex=%d"), + slaveAddress, this, _deviceID, linkId, taskIndex)); } # endif // MODBUS_DEBUG - return success; + return success; } + // Checker for device class initialization status /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::isInitialized() const { @@ -75,9 +80,7 @@ bool ModbusDEVICE_struct::isInitialized() const { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Set the Modbus timeout value for this device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::setModbusTimeout(uint16_t timeout) { - _timeout = timeout; -} +void ModbusDEVICE_struct::setModbusTimeout(uint16_t timeout) { _timeout = timeout; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Retrieve the Modbus timeout value for this device @@ -91,22 +94,24 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, - uint16_t *valuePtr, - ModbusResultState *statePtr) { - return readModuleHoldingRegister(_modbus_address, address, valuePtr, statePtr); +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, + uint16_t *valuePtr, + ModbusResultState *statePtr) { return false; } + +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t uid) +{ + return readModuleHoldingRegister(_modbus_address, address, uid); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start reading a Modbus holding register from another module on the bus. The result will be available later. // The function returns true if the request was queued. -// Note: This function accesses registers from other devices on the same Modbus bus. +// Note: This function accesses registers from other devices on the same Modbus bus. // This should be used with care to prevent conflicts. This is beyond the intended scope of the Modbus device class. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, - uint16_t registerAddress, - uint16_t *valuePtr, - ModbusResultState *statePtr) +bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t uid) { if (_modbus_link == nullptr) { return false; @@ -114,8 +119,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddr Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - request->_userData = valuePtr; - request->_userState = statePtr; + request->_userId = uid; request->_sendframe[0] = busAddress; request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; request->_sendframe[2] = highByte(registerAddress); @@ -127,20 +131,22 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddr request->_sendframe[7] = highByte(crc); // CRC high byte request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 7; // Expect 8 bytes in response - ////dump_buffer(request->_sendframe, request->_sendframe_length); uint16_t queueID = _modbus_link->queueTransaction(request); - *statePtr = ModbusResultState::Busy; - - // Don't touch *valueptr here, it might contain a previous valid result still to be handled. return true; } + +bool ModbusDEVICE_struct::readHoldingRegisterResult(uint16_t uid, uint16_t *valuePtr) +{ + return false; +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start writing a Modbus single register. -// The function returns true if the request was queued. +// The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, - uint16_t value, +bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, + uint16_t value, ModbusResultState *statePtr) { if (_modbus_link == nullptr) { @@ -197,18 +203,22 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) # endif // MODBUS_DEBUG } else { - switch (req->_messageType) { + switch (req->_messageType) + { case ModbusTransactionType::READ_HOLDING_REGISTERS: { if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { uint16_t crc = CalculateCRC(req->_rcvframe, 5); if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { + int val = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + // Valid response if (req->_userData != nullptr) { - *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + *(static_cast(req->_userData)) = val; resultState = ModbusResultState::Success; } + sendEvent(req, val, 0, 0, 0); } } break; @@ -246,7 +256,9 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) } } - *(static_cast(req->_userState)) = resultState; + if (req->_userState != nullptr) { + *(static_cast(req->_userState)) = resultState; + } _modbus_link->freeTransaction(req); # ifdef MODBUS_DEBUG log += F(", Result = "); @@ -255,6 +267,26 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) # endif // MODBUS_DEBUG } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Send a PLUGIN_TASKTIMER_IN event to the task associated with this device. +// This is used by the Modbus link to notify the device of responses received for queued requests. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement *req, int par1, int par2, int par3, int par4) +{ + struct EventStruct TempEvent; + + TempEvent.Par1 = par1; + TempEvent.Par2 = par2; + TempEvent.Par3 = par3; + TempEvent.Par4 = par4; + TempEvent.TaskIndex = _taskIndex; // Send to the task associated with this device + TempEvent.idx = req->_userId; // Identifier as specified by the client in the request + TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM; + String dummy; + + PluginCall(PLUGIN_TASKTIMER_IN, &TempEvent, dummy); +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Compute the Modbus RTU CRC /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 081fe82849..3cab47a9a3 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -17,6 +17,7 @@ enum class ModbusResultState { Busy = 0, // Transaction is not completed Success = 1, // Transaction successfully completed Error = 2, // Transaction completed with an error + }; // ModbusDEVICE structure representing a MODBUS Device @@ -34,8 +35,9 @@ struct ModbusDEVICE_struct { void reset(); -bool init( uint8_t slaveAddress, - int linkId); + bool init(uint8_t slaveAddress, + int linkId, + taskIndex_t taskIndex); bool isInitialized() const; @@ -48,30 +50,43 @@ bool init( uint8_t slaveAddress, // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. // The state variable will signal the processing state of the request. - bool readHoldingRegister(uint16_t address, - uint16_t *valueptr, + bool readHoldingRegister(uint16_t address, + uint16_t *valueptr, ModbusResultState *stateptr); - bool writeSingleRegister(uint16_t address, - uint16_t value, + bool readHoldingRegister(uint16_t address, + uint16_t uid); + + bool readHoldingRegisterResult(uint16_t uid, + uint16_t *valuePtr); + + bool writeSingleRegister(uint16_t address, + uint16_t value, ModbusResultState *stateptr); - - bool readModuleHoldingRegister(uint8_t busAddress, - uint16_t registerAddress, - uint16_t *valuePtr, - ModbusResultState *statePtr); + + bool readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t uid); private: uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; - ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object - uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device - uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device + + void sendEvent(Modbus_RequestQueueElement *req, + int par1, + int par2, + int par3, + int par4); static uint16_t CalculateCRC(uint8_t *buf, int len); static void dump_buffer(const uint8_t *buffer, size_t length); + }; #endif // FEAURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index a09dcb7e77..ec299a6940 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -13,7 +13,7 @@ # include "Modbus_device.h" # include "Modbus_link.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -51,11 +51,10 @@ void ModbusLINK_struct::reset() { bool ModbusLINK_struct::init(const ESPEasySerialPort port, const int8_t serial_rx, const int8_t serial_tx, - int16_t baudrate, + uint16_t baudrate, int8_t dere_pin, bool collision_detect) { - bool rs485Mode = false; - int available = 0; + int available = 0; // (re)create the serial port object // If the serial port object already exists, delete it first. @@ -70,7 +69,7 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, } // Set RS485 mode if requested using selected pin for RTS - rs485Mode = _easySerial->setRS485Mode(dere_pin, collision_detect); + const bool rs485Mode = _easySerial->setRS485Mode(dere_pin, collision_detect); _easySerial->begin(baudrate); _easySerial->flush(); available = _easySerial->available(); @@ -81,27 +80,33 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, _easySerial->read(); } } - rs485Mode = _easySerial->setRS485Mode(dere_pin, collision_detect); _dere_pin = dere_pin; _collision_detect = collision_detect; # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("---> Modbus: Link, Init serial, RX pin "); - log += serial_rx; - log += F(", TX pin "); - log += serial_tx; - log += F(", RS485 mode selected on pin "); - log += dere_pin; - log += F(", baudrate "); - log += baudrate; - log += F(", collision detection "); - log += collision_detect ? F("enabled") : F("disabled"); - log += F(", RS485mode enabled: "); - log += rs485Mode ? F("yes") : F("no"); - log += F(", Serial isRxEnabled: "); - log += _easySerial->isRxEnabled() ? F("yes") : F("no"); + String log = + strformat(F( + "---> Modbus: Link %s, Init serial, RX pin %d, TX pin %d, RS485 pin %d, baudrate %d, collision detection %s, RS485 mode %s"), + ESPEasySerialPort_toString(port), + serial_rx, + serial_tx, + dere_pin, + baudrate, + collision_detect ? F("enabled") : F("disabled"), + rs485Mode ? F("enabled") : F("disabled") + ); + addLogMove(LOG_LEVEL_INFO, log); + + log = strformat(F("+++> Modbus: Link %s, Init serial, RX pin %d, TX pin %d, baudrate %d, TxEnabled %s, RxEnabled %s"), + ESPEasySerialPort_toString(port), + _easySerial->getRxPin(), + _easySerial->getTxPin(), + _easySerial->getBaudRate(), + _easySerial->isTxEnabled() ? F("enabled") : F("disabled"), + _easySerial->isRxEnabled() ? F("enabled") : F("disabled") + ); addLogMove(LOG_LEVEL_INFO, log); } # endif // MODBUS_DEBUG @@ -109,13 +114,6 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, return true; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Return the initialization status of the link -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::isInitialized() const { - return _easySerial != nullptr; -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Provide a new transaction structure that can be used to build a Modbus request and queue it at this link /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -189,12 +187,8 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processCommand() { - if ((!_initialized) || (_easySerial == nullptr)) { - return; // Serial port not initialized - } - - if (_requestQueue.empty()) { - return; // No transactions to process + if (!isInitialized() || (_requestQueue.empty())) { + return; // Serial port not initialized or queue is empty, nothing to process } auto it = _requestQueue.begin(); // Iterator for the request queue @@ -203,7 +197,6 @@ void ModbusLINK_struct::processCommand() while ((it != _requestQueue.end()) && !busy && ((*it) != nullptr)) { # ifdef MODBUS_DEBUG dumpQueueElement(*it); - dumpState((*it)->_state); # endif // MODBUS_DEBUG switch ((*it)->_state) @@ -219,7 +212,6 @@ void ModbusLINK_struct::processCommand() _easySerial->read(); } } - _easySerial->write((*it)->_sendframe, (*it)->_sendframe_length); (*it)->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response (*it)->_startTime = millis(); // Record the time the transaction @@ -282,11 +274,6 @@ int16_t ModbusLINK_struct::getBaudrate(void) const return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; } -ESPEasySerialPort ModbusLINK_struct::getPort(void) const -{ - return ESPEasySerialPort(); -} - int16_t ModbusLINK_struct::getSerialRX(void) const { return _easySerial != nullptr ? _easySerial->getRxPin() : -1; diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 092804f3bf..6426d08472 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -45,6 +45,8 @@ struct Modbus_RequestQueueElement { ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message void *_userData = nullptr; // Pointer to user (device) data void *_userState = nullptr; // Pointer to user (device) defined state + uint16_t _userId = 0; // Cleint defined identifier for this transaction, + // can be used to match responses to requests uint16_t _id = 0; // ID of the request struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the // action @@ -76,13 +78,13 @@ struct ModbusLINK_struct { void reset(); bool init(const ESPEasySerialPort port, - const int8_t serial_rx, - const int8_t serial_tx, - int16_t baudrate, + const int8_t serial_rx, + const int8_t serial_tx, + uint16_t baudrate, int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const; + bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); bool freeTransaction(Modbus_RequestQueueElement *transaction); @@ -92,8 +94,6 @@ struct ModbusLINK_struct { int16_t getBaudrate(void) const; - ESPEasySerialPort getPort(void) const; - int16_t getSerialRX(void) const; int16_t getSerialTX(void) const; @@ -102,6 +102,7 @@ struct ModbusLINK_struct { bool getCollisionDetect(void) const; + private: static void dumpQueueElement(Modbus_RequestQueueElement *el); diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index cb4b8b6bc6..1855be948b 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -12,7 +12,7 @@ # include # include "Modbus_mgr.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -101,9 +101,6 @@ bool ModbusMGR_struct::initialize() _modbus_links[i]->link = nullptr; _modbus_links[i]->kvs = new (std::nothrow) ESPEasy_key_value_store; _modbus_links[i]->kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); - ////# ifdef MODBUS_DEBUG - ////_modbus_links[i]->kvs->dump(); - ////# endif _modbus_links[i]->kvs->getValue(MODBUS_PORT_KEY_INDEX, val); _modbus_links[i]->port = static_cast(val); _modbus_links[i]->kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i]->serial_rx); @@ -117,14 +114,14 @@ bool ModbusMGR_struct::initialize() _modbus_links[i]->link->init(_modbus_links[i]->port, _modbus_links[i]->serial_rx, _modbus_links[i]->serial_tx, - modbus_storageValueToBaudrate(_modbus_links[i]->baudrate), + _modbus_links[i]->baudrate, _modbus_links[i]->dere_pin, _modbus_links[i]->collision_detect); } } } _initialized = true; - + # ifdef MODBUS_DEBUG dumpAdminInfo(); # endif // ifdef MODBUS_DEBUG @@ -233,7 +230,7 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::processLinks() { - if (_initialized) { + if (isInitialized()) { for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if ((_modbus_links[i] != nullptr) && (_modbus_links[i]->link != nullptr)) { _modbus_links[i]->link->processCommand(); // Trigger processing of the command queue on the link @@ -502,9 +499,9 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::setLink(const int linkIndex, const ESPEasySerialPort port, - const int8_t serial_rx, - const int8_t serial_tx, - int16_t baudrate, + const int8_t serial_rx, + const int8_t serial_tx, + uint16_t baudrate, int8_t dere_pin, bool collision_detect) { @@ -542,8 +539,8 @@ bool ModbusMGR_struct::setLink(const int linkIndex, } } else { - log += strformat(F("Invalid link for linkIndex=%d"), linkIndex); # ifdef MODBUS_DEBUG + log += strformat(F("Invalid link for linkIndex=%d"), linkIndex); addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef MODBUS_DEBUG return false; // Invalid link index diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 60fff679f3..326cf3febd 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -33,6 +33,7 @@ typedef struct ModbusMGR_struct { void show_modbus_interfaces(); bool save_modbus_interfaces(String& error); + bool isInitialized() const { return _initialized; } private: @@ -63,12 +64,13 @@ typedef struct ModbusMGR_struct { ModbusLinkInfo_struct *_modbus_links[MAX_MODBUS_LINKS] = { nullptr }; // Pointer to the Modbus link object ModbusDeviceInfo_struct *_modbus_devices[MAX_MODBUS_DEVICES] = { nullptr }; // Array of connected Modbus devices bool _initialized = false; // Flag indicating if the manager is initialized + bool _testing = false; bool setLink(const int linkIndex, const ESPEasySerialPort port, const int8_t serial_rx, const int8_t serial_tx, - int16_t baudrate, + uint16_t baudrate, int8_t dere_pin, bool collision_detect); diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index abb6dfe58b..317793c032 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -3,6 +3,10 @@ #ifdef USES_P183 + +# define ACTION_DUMP_RANGE 0xFFFF +# define ACTION_SCAN_BUS 0xFFFE + // ####################################################################################################### // ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### @@ -10,21 +14,27 @@ # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Constructor of the plugin data structure. Initializes the data members to default values. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// P183_data_struct::P183_data_struct(struct EventStruct *event) { _taskIndex = event->TaskIndex; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Destructor of the plugin data structure. Clean up any resources used by the plugin. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// P183_data_struct::~P183_data_struct() { delete _modbusDevice; _modbusDevice = nullptr; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization. Takes the Modbus device address and link ID as parameters. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) { - //// TODO Implement - // Create a fresh Modbus_device object to handle the Modbus communication + // Create a fresh Modbus_device object to handle the Modbus communication if (_modbusDevice != nullptr) { delete _modbusDevice; _modbusDevice = nullptr; @@ -38,12 +48,13 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) // Initialize our own Modbus_device with the provided serial link parameters // Note that the link configuration is expected to be the same for all plugins reusing the same serial port - if (!_modbusDevice->init(slaveAddress, linkId)) { + if (!_modbusDevice->init(slaveAddress, linkId, _taskIndex)) { return false; } _modbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); - return false; + return false; } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::plugin_exit() { @@ -54,20 +65,8 @@ void P183_data_struct::plugin_exit() } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool P183_data_struct::plugin_once_a_second(struct EventStruct *event) { - // No actions - return true; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool P183_data_struct::plugin_ten_per_second(struct EventStruct *event) { - // No actions - return true; -} - +// Plugin read function. Queues a new request to read the Modbus registers. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Plugin read function implementataion. -// A new request to update the Modbus registers is queued. The plugin output valuea are updated with the previous results bool P183_data_struct::plugin_read(struct EventStruct *event) { if (_modbusDevice == nullptr) { return false; @@ -75,16 +74,57 @@ bool P183_data_struct::plugin_read(struct EventStruct *event) { for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { - _modbusDevice->readHoldingRegister(P183_ADDRESS(outputIndex), &(_registerValues[outputIndex]), &(_queueStates[outputIndex])); - UserVar.setFloat(event->TaskIndex, outputIndex, _registerValues[outputIndex]); + _modbusDevice->readHoldingRegister(P183_ADDRESS(outputIndex), outputIndex); } return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Handles the PLUGIN_TASKTIMER_IN event. +// This is used to process the results of Modbus read requests and to trigger the next step in a Modbus scan sequence. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_task_timer(EventStruct *event) +{ + addLogMove(LOG_LEVEL_INFO, + strformat(F("P183: TaskTimer called IDX=%d, par1=%d, par2=%d, par3=%d, par4=%d"), + event->idx, event->Par1, event->Par2, event->Par3, event->Par4)); + + if (event->idx == ACTION_DUMP_RANGE) { + if (true) { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), _lastAddress, _lastAddress, event->Par1, event->Par1)); + } else { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) no response"), _lastAddress, _lastAddress)); + } + _lastAddress++; + scan_next_address(); + return true; + } + else if (event->idx == ACTION_SCAN_BUS) { + if (true) { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Device found at address %u (0x%02X)"), _lastAddress, _lastAddress)); + } + _lastAddress++; + scan_next_module(); + return true; + } + else { + int outputIndex = event->idx; + + if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { + addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); + return false; + } + UserVar.setFloat(event->TaskIndex, outputIndex, event->Par1); // Update the user variable with the value read from Modbus + return false; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start iterating over a register range of a Modbus device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { - uint16_t value = 0; + uint16_t value = 0; ModbusResultState state = ModbusResultState::Busy; addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); @@ -93,48 +133,72 @@ void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t e return; } - for (uint8_t reg = start_reg; reg <= end_reg; reg++) { - int result = _modbusDevice->readHoldingRegister(reg, &value, &state); - - _modbusDevice->readHoldingRegister(1, &value, &state); + if (start_reg < end_reg) { + _lastAddress = start_reg; + _endAddress = end_reg; + _scanning = true; + scan_next_address(); + } + else + { + _scanning = false; + } +} - if (state == ModbusResultState::Success) { - addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), reg, reg, value, value)); +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// REad the next holding register from the Modbus device +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_next_address() +{ + if (_scanning) { + if (_lastAddress <= _endAddress) { + _modbusDevice->readHoldingRegister(_lastAddress, ACTION_DUMP_RANGE); } else { - addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) no response"), reg, reg)); + _scanning = false; + addLogMove(LOG_LEVEL_INFO, F("Modbus: Finished scanning device")); } + return; } + _scanning = true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Scan Modbus addreses from 0x00 to 0xFF for a given node ID +// Scan Modbus addreses on the bus and log the results +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::scan_modbus() { - uint16_t value = 0; - ModbusResultState state = ModbusResultState::Busy; - addLogMove(LOG_LEVEL_INFO, F("Modbus: Scanning for Modbus modules")); if (_modbusDevice == nullptr) { return; } - for (uint8_t id = 0; id <= 247; id++) { - _modbusDevice->readModuleHoldingRegister(id, 1, &value, &state); + _lastAddress = 1; + _endAddress = 247; + _scanning = true; + scan_next_module(); +} -//// while (state == ModbusResultState::Busy) { -//// _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue -//// } - String s = state == ModbusResultState::Success ? F(" OK") : F(" no response"); - addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) %s"), id, id, s)); +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void P183_data_struct::scan_next_module() +{ + if (_scanning) { + if (_lastAddress <= _endAddress) { + _modbusDevice->readModuleHoldingRegister(_lastAddress, 1, ACTION_SCAN_BUS); + } else { + _scanning = false; + addLogMove(LOG_LEVEL_INFO, F("Modbus: Finished scanning device")); + } } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Read a Modbus register from the device. Wait untial the data is available // Warning: this may take time as we waith for the Modbus message to be exchanged +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t P183_data_struct::readRegisterWait(uint16_t address) { - uint16_t value = 0; + uint16_t value = 0; ModbusResultState state = ModbusResultState::Busy; if (_modbusDevice == nullptr) { @@ -145,7 +209,8 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { while (state == ModbusResultState::Busy) { delay(50); -//// _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + + //// _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } return value; diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 2726705272..7ee7d1285a 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -43,21 +43,12 @@ struct P183_data_struct : public PluginTaskData_base { P183_data_struct() = delete; virtual ~P183_data_struct(); - bool plugin_init(uint8_t slaveAddress, - const ESPEasySerialPort port, - const int16_t serial_rx, - const int16_t serial_tx, - int16_t baudrate, - int8_t dere_pin, - bool collision_detect); - bool plugin_init(uint8_t slaveAddress, int linkId); void plugin_exit(); bool plugin_read(struct EventStruct *event); - bool plugin_once_a_second(struct EventStruct *event); - bool plugin_ten_per_second(struct EventStruct *event); + bool plugin_task_timer(struct EventStruct *event); void scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg); @@ -68,12 +59,15 @@ struct P183_data_struct : public PluginTaskData_base { private: - taskIndex_t _taskIndex = INVALID_TASK_INDEX; - struct ModbusDEVICE_struct *_modbusDevice = nullptr; - uint16_t _registerValues[4] = {}; // Modus register values retrieved for output values - ModbusResultState _queueStates[4] = {}; // State of read hloding register transactions - ModbusResultState _lastActionState = ModbusResultState::Busy; + taskIndex_t _taskIndex = INVALID_TASK_INDEX; + struct ModbusDEVICE_struct *_modbusDevice = nullptr; + ModbusResultState _lastActionState = ModbusResultState::Busy; + uint16_t _lastAddress = 0; + uint16_t _endAddress = 0; + bool _scanning = false; + void scan_next_address(); + void scan_next_module(); }; #endif // ifdef USES_P183 From c66aee981cfca805fd33140c9a19b61297ef420a Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sat, 2 May 2026 21:27:23 +0200 Subject: [PATCH 26/48] Functionality ready, to be debugged and cleaned up --- misc/modbusFacility/Modbus_link_state.puml | 5 +- misc/modbusFacility/Modbus_seq2.puml | 12 ++--- src/_P183_modbus.ino | 32 ++++++----- src/src/Helpers/Modbus_device.cpp | 63 ++++++++++++++++------ src/src/Helpers/Modbus_device.h | 27 ++++++++-- src/src/Helpers/Modbus_link.cpp | 16 ++---- src/src/Helpers/Modbus_link.h | 2 +- src/src/Helpers/Modbus_mgr.cpp | 2 +- src/src/PluginStructs/P183_data_struct.cpp | 38 ++++++++----- 9 files changed, 130 insertions(+), 67 deletions(-) diff --git a/misc/modbusFacility/Modbus_link_state.puml b/misc/modbusFacility/Modbus_link_state.puml index 9c4485d93e..c15f6e3fe6 100644 --- a/misc/modbusFacility/Modbus_link_state.puml +++ b/misc/modbusFacility/Modbus_link_state.puml @@ -1,10 +1,11 @@ @startuml [*] --> NOT_QUEUED +state active { NOT_QUEUED --> QUEUED : queueTransaction() QUEUED --> MESSAGE_SENT : link inactive MESSAGE_SENT --> RESPONSE_RECEIVED : received valid response MESSAGE_SENT --> ERROR_OCCURRED : response timeout or invalid response -RESPONSE_RECEIVED --> READY_FOR_DESTROY : freeTransaction() -ERROR_OCCURRED --> READY_FOR_DESTROY : freeTransaction() +} +active -down-> READY_FOR_DESTROY : freeTransaction() READY_FOR_DESTROY --> [*] @enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq2.puml b/misc/modbusFacility/Modbus_seq2.puml index 54206e13d2..2ec0f09708 100644 --- a/misc/modbusFacility/Modbus_seq2.puml +++ b/misc/modbusFacility/Modbus_seq2.puml @@ -3,21 +3,20 @@ actor System as system participant Plugin as plugin participant Plugin_struct as plugin_struct participant Modbus_device as modbus_device -participant Modbus_mgr as modbus_mgr +participant Modbus_mgr as modbus_mgr <> participant Modbus_link as modbus_link queue Queue as queue +system -> modbus_mgr ++ : init +create modbus_link +modbus_mgr -> modbus_link : new() +system <-- modbus_mgr -- system -> plugin ++ : init create plugin_struct plugin -> plugin_struct ++ : new() create modbus_device plugin_struct -> modbus_device ++ : new() modbus_device -> modbus_mgr ++ : connect() -alt link not exists - create modbus_link -modbus_mgr -> modbus_link : new() -end -modbus_mgr -> modbus_link : init() modbus_device <-- modbus_mgr -- : link plugin_struct <-- modbus_device -- plugin <-- plugin_struct -- @@ -44,7 +43,6 @@ alt response available plugin -> plugin_struct ++ : plugin_task_timer() plugin <-- plugin_struct -- modbus_device <-- plugin -- - modbus_device -> modbus_link : freeTransaction() modbus_link <-- modbus_device -- end alt no transaction diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 8dbd7afedd..9659b17a43 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -17,11 +17,14 @@ /** * Changelog: + * 2026-04-29 flashmark: Refactor for new modbus facility using separated Modbus link object. * 2026-04-13 flashmark: Separate Modbus link definition from plugin. * 2025-10-12 flashmark: Restructuring and adding a MODBUS_FAC facility * 2025-08-24 flashmark: Initial version */ +//// # define P183_DEBUG + # define PLUGIN_183 # define PLUGIN_ID_183 183 # define PLUGIN_NAME_183 "Communication - Modbus RTU" @@ -73,17 +76,6 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_GET_DEVICEGPIONAMES: - { - break; - } - - case PLUGIN_WEBFORM_SHOW_CONFIG: - { - success = true; - break; - } - case PLUGIN_SET_DEFAULTS: { P183_DEV_ID = P183_DEV_ID_DFLT; @@ -138,9 +130,11 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data != nullptr) { success = P183_data->plugin_init(P183_DEV_ID, P183_LINK_ID); } + # ifdef P183_DEBUG else { addLogMove(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); } + # endif // P183_DEBUG success = true; break; @@ -162,19 +156,23 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read invalid data struct")); + # endif // P183_DEBUG return false; } success = P183_data->plugin_read(event); // Delegate to data_struct break; } - + case PLUGIN_WRITE: { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus write invalid data struct")); + # endif // P183_DEBUG return false; } @@ -188,10 +186,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) int address = event->Par2; uint16_t value = event->Par3; P183_data->writeRegister(address, value); + # ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus write value %u to address 0x%04x"), value, address)); } + # endif // P183_DEBUG success = true; } else if (equals(subcmd, F("read"))) { @@ -199,10 +199,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) int address = event->Par2; uint16_t value = 0; value = P183_data->readRegisterWait(address); + # ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus read value %u from address 0x%04x"), value, address)); } + # endif // P183_DEBUG success = true; } else if (equals(subcmd, F("dump"))) { @@ -229,9 +231,11 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) ModbusMGR_singleton.dumpAdminInfo(); success = true; } + # ifdef P183_DEBUG else { addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Unknown command")); } + # endif // P183_DEBUG } break; @@ -242,7 +246,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus task timer invalid data struct")); + # endif // P183_DEBUG return false; } success = P183_data->plugin_task_timer(event); // Delegate to data_struct @@ -254,7 +260,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config invalid data struct")); + # endif // P183_DEBUG return false; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index b5248bb8dc..546065ad52 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -11,7 +11,6 @@ # include "../Helpers/Modbus_device.h" # include "../Helpers/Modbus_mgr.h" -# include "Modbus_device.h" ////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG @@ -92,12 +91,27 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start reading a Modubus holding register. The result will be available later. -// The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t *valuePtr, - ModbusResultState *statePtr) { return false; } + ModbusResultState *statePtr) +{ + if (_modbus_link == nullptr) { + return false; + } + Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + request->_userData = valuePtr; + request->_userState = statePtr; + request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + createReadFrame(request, _modbus_address, address); + uint16_t queueID = _modbus_link->queueTransaction(request); + return true; +} +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modubus holding register. The result will be available later through an task event. +// The function returns true if the request was queued. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t uid) { return readModuleHoldingRegister(_modbus_address, address, uid); @@ -117,9 +131,23 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, return false; } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + request->_userData = nullptr; + request->_userState = nullptr; + request->_userId = uid; + createReadFrame(request, busAddress, registerAddress); + uint16_t queueID = _modbus_link->queueTransaction(request); + return true; +} +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Construct a Modbus read holding registers message +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement *request, + uint8_t busAddress, + uint16_t registerAddress) +{ request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - request->_userId = uid; request->_sendframe[0] = busAddress; request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; request->_sendframe[2] = highByte(registerAddress); @@ -131,15 +159,9 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, request->_sendframe[7] = highByte(crc); // CRC high byte request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 7; // Expect 8 bytes in response - uint16_t queueID = _modbus_link->queueTransaction(request); - return true; } - -bool ModbusDEVICE_struct::readHoldingRegisterResult(uint16_t uid, uint16_t *valuePtr) -{ - return false; -} +bool ModbusDEVICE_struct::readHoldingRegisterResult(uint16_t uid, uint16_t *valuePtr) { return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start writing a Modbus single register. @@ -175,6 +197,13 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, return true; } +void ModbusDEVICE_struct::processCommand(void) +{ + if (_modbus_link != nullptr) { + _modbus_link->processCommand(); + } +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Callback function called by the Modbus link when a response is received for a queued request. // Note that the response might be an invalid response or a timeout @@ -198,6 +227,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) # endif // MODBUS_DEBUG if (req->_state == ModbusQueueState::ERROR_OCCURRED) { + sendEvent(req, false, 0, 0, 0); # ifdef MODBUS_DEBUG log += F(" Link error occurred"); # endif // MODBUS_DEBUG @@ -218,7 +248,10 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) *(static_cast(req->_userData)) = val; resultState = ModbusResultState::Success; } - sendEvent(req, val, 0, 0, 0); + else + { + sendEvent(req, true, val, 0, 0); + } } } break; @@ -226,7 +259,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) case ModbusTransactionType::WRITE_SINGLE_REGISTER: { - if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_WRITE_SINGLE_REGISTER) && (req->_rcvframe[2] == 2)) { uint16_t crc = CalculateCRC(req->_rcvframe, 5); if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { @@ -259,7 +292,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) if (req->_userState != nullptr) { *(static_cast(req->_userState)) = resultState; } - _modbus_link->freeTransaction(req); + _modbus_link->freeTransaction(req); // Free the transaction to prevent memory leaks. # ifdef MODBUS_DEBUG log += F(", Result = "); log += (resultState == ModbusResultState::Success) ? F("SUCCESS") : F("ERROR"); @@ -329,4 +362,4 @@ void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { # endif // MODBUS_DEBUG } -#endif // if FEATURE_MODBUS +#endif // if FEATURE_MODBUS_FAC diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 3cab47a9a3..782bf2c00a 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -54,27 +54,41 @@ struct ModbusDEVICE_struct { uint16_t *valueptr, ModbusResultState *stateptr); + // Start reading a Modbus holding register with reslt returned through event PLUGIN_TASKTIMER_IN + // The function returns true if the request was queued. + // Use uid to identify the request. This value will be passed back in the PLUGIN_TASKTIMER_IN event bool readHoldingRegister(uint16_t address, uint16_t uid); + // Fetch the result of a readHoldingRegister request + // The function returns true if the result is available + // Use uid to identify the request. bool readHoldingRegisterResult(uint16_t uid, uint16_t *valuePtr); + // Start writing a single Modbus register. + // The function returns true if the request was queued. bool writeSingleRegister(uint16_t address, uint16_t value, ModbusResultState *stateptr); + // Start reading a Modbus holding register from another module. The result will be available later. + // The function returns true if the request was queued. + // Note: This function accesses registers from other devices on the same Modbus bus. This is beyond the intended scope of the Modbus + // device class. bool readModuleHoldingRegister(uint8_t busAddress, uint16_t registerAddress, uint16_t uid); + void processCommand(void); + private: uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; - ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object - uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device - uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests - taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device void sendEvent(Modbus_RequestQueueElement *req, int par1, @@ -82,8 +96,13 @@ struct ModbusDEVICE_struct { int par3, int par4); + void createReadFrame(Modbus_RequestQueueElement *request, + uint8_t busAddress, + uint16_t registerAddress); + static uint16_t CalculateCRC(uint8_t *buf, int len); + static void dump_buffer(const uint8_t *buffer, size_t length); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index ec299a6940..efa04404db 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -10,8 +10,8 @@ #if FEATURE_MODBUS_FAC -# include "Modbus_device.h" -# include "Modbus_link.h" +# include "../Helpers/Modbus_device.h" +# include "../Helpers/Modbus_link.h" ////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG @@ -98,16 +98,6 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, rs485Mode ? F("enabled") : F("disabled") ); addLogMove(LOG_LEVEL_INFO, log); - - log = strformat(F("+++> Modbus: Link %s, Init serial, RX pin %d, TX pin %d, baudrate %d, TxEnabled %s, RxEnabled %s"), - ESPEasySerialPort_toString(port), - _easySerial->getRxPin(), - _easySerial->getTxPin(), - _easySerial->getBaudRate(), - _easySerial->isTxEnabled() ? F("enabled") : F("disabled"), - _easySerial->isRxEnabled() ? F("enabled") : F("disabled") - ); - addLogMove(LOG_LEVEL_INFO, log); } # endif // MODBUS_DEBUG _initialized = true; @@ -188,7 +178,7 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac void ModbusLINK_struct::processCommand() { if (!isInitialized() || (_requestQueue.empty())) { - return; // Serial port not initialized or queue is empty, nothing to process + return; // Serial port not initialized or queue is empty, nothing to process } auto it = _requestQueue.begin(); // Iterator for the request queue diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 6426d08472..7b57a83ace 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -45,7 +45,7 @@ struct Modbus_RequestQueueElement { ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message void *_userData = nullptr; // Pointer to user (device) data void *_userState = nullptr; // Pointer to user (device) defined state - uint16_t _userId = 0; // Cleint defined identifier for this transaction, + uint16_t _userId = 0; // Client defined identifier for this transaction, // can be used to match responses to requests uint16_t _id = 0; // ID of the request struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 1855be948b..c21dd2fecc 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -10,7 +10,7 @@ #if FEATURE_MODBUS_FAC # include -# include "Modbus_mgr.h" +# include "../Helpers/Modbus_mgr.h" ////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 317793c032..386de624d3 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -1,19 +1,20 @@ #include "../PluginStructs/P183_data_struct.h" -#include "P183_data_struct.h" - -#ifdef USES_P183 -# define ACTION_DUMP_RANGE 0xFFFF -# define ACTION_SCAN_BUS 0xFFFE +#ifdef USES_P183 // ####################################################################################################### // ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### +# define P183_DEBUG # ifdef BUILD_NO_DEBUG # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG +// Actions for PLUGIN_TASKTIMER_IN event to distinguish between regular read results and scan sequences +# define ACTION_DUMP_RANGE 0xFFFF +# define ACTION_SCAN_BUS 0xFFFE + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Constructor of the plugin data structure. Initializes the data members to default values. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -42,12 +43,12 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) _modbusDevice = new (std::nothrow) ModbusDEVICE_struct(); if (_modbusDevice == nullptr) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183: Unable to allocate Modbus device object")); + # endif // P183_DEBUG return false; } - // Initialize our own Modbus_device with the provided serial link parameters - // Note that the link configuration is expected to be the same for all plugins reusing the same serial port if (!_modbusDevice->init(slaveAddress, linkId, _taskIndex)) { return false; } @@ -74,6 +75,8 @@ bool P183_data_struct::plugin_read(struct EventStruct *event) { for (int outputIndex = 0; outputIndex < P183_NR_OUTPUTS; ++outputIndex) { + // Queue a read request for each active output value. The result will be processed in the task timer event. + // Use the output index as the event index to identify which output value the result belongs to. _modbusDevice->readHoldingRegister(P183_ADDRESS(outputIndex), outputIndex); } return true; @@ -82,16 +85,21 @@ bool P183_data_struct::plugin_read(struct EventStruct *event) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Handles the PLUGIN_TASKTIMER_IN event. // This is used to process the results of Modbus read requests and to trigger the next step in a Modbus scan sequence. +// event->idx is used to identify which transaction the result belongs to. +// event->Par1 is used to indicate whether the Modbus read was successful (true) or not (false). +// event->Par2 is used to pass the value read from Modbus when the read was successful. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool P183_data_struct::plugin_task_timer(EventStruct *event) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_INFO, strformat(F("P183: TaskTimer called IDX=%d, par1=%d, par2=%d, par3=%d, par4=%d"), event->idx, event->Par1, event->Par2, event->Par3, event->Par4)); + # endif // P183_DEBUG if (event->idx == ACTION_DUMP_RANGE) { - if (true) { - addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), _lastAddress, _lastAddress, event->Par1, event->Par1)); + if (event->Par1) { + addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) = %u (0x%02X)"), _lastAddress, _lastAddress, event->Par2, event->Par2)); } else { addLogMove(LOG_LEVEL_INFO, strformat(F("** Address %u (0x%02X) no response"), _lastAddress, _lastAddress)); } @@ -100,7 +108,7 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) return true; } else if (event->idx == ACTION_SCAN_BUS) { - if (true) { + if (event->Par1) { addLogMove(LOG_LEVEL_INFO, strformat(F("** Device found at address %u (0x%02X)"), _lastAddress, _lastAddress)); } _lastAddress++; @@ -111,10 +119,16 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) int outputIndex = event->idx; if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { + # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); + # endif // P183_DEBUG return false; } - UserVar.setFloat(event->TaskIndex, outputIndex, event->Par1); // Update the user variable with the value read from Modbus + + if (event->Par1) { + UserVar.setFloat(event->TaskIndex, outputIndex, event->Par2); // Update the user variable with the value read from Modbus + return true; + } return false; } } @@ -210,7 +224,7 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { while (state == ModbusResultState::Busy) { delay(50); - //// _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue } return value; From afa1b6aca8b8f5d1ececce37ff3acb86ad7aa998 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 3 May 2026 13:50:21 +0200 Subject: [PATCH 27/48] Small review comment updates --- misc/modbusFacility/Modbus_seq1.puml | 56 ---------------------------- src/_P183_modbus.ino | 7 +--- src/src/Helpers/Modbus_device.cpp | 9 +++-- 3 files changed, 7 insertions(+), 65 deletions(-) delete mode 100644 misc/modbusFacility/Modbus_seq1.puml diff --git a/misc/modbusFacility/Modbus_seq1.puml b/misc/modbusFacility/Modbus_seq1.puml deleted file mode 100644 index e4b770d038..0000000000 --- a/misc/modbusFacility/Modbus_seq1.puml +++ /dev/null @@ -1,56 +0,0 @@ -@startuml -actor System as system -participant Plugin as plugin -participant Plugin_struct as plugin_struct -participant Modbus_device as modbus_device -participant Modbus_mgr as modbus_mgr -participant Modbus_link as modbus_link -queue Queue as queue - -system -> plugin ++ : init -create plugin_struct -plugin -> plugin_struct ++ : new() -create modbus_device -plugin_struct -> modbus_device ++ : new() -modbus_device -> modbus_mgr ++ : connect() -alt link not exists - create modbus_link -modbus_mgr -> modbus_link : new() -end -modbus_mgr -> modbus_link : init() -modbus_device <-- modbus_mgr -- : link -plugin_struct <-- modbus_device -- -plugin <-- plugin_struct -- -system <-- plugin -- - -system -> plugin ++ : read -plugin -> plugin_struct ++ : read() -plugin_struct -> modbus_device ++ : readHoldingRegister() -modbus_device -> modbus_link ++: queueTransaction() -modbus_link -> queue : add -modbus_device <-- modbus_link -- -plugin_struct <-- modbus_device -- -plugin_struct -> plugin_struct : retrieve latest value -plugin <-- plugin_struct -- -system <-- plugin -- - -system -> plugin ++ : ten_per_second -plugin -> plugin_struct ++ : ten_per_second() -plugin_struct -> modbus_device ++ : processCommand() -modbus_device -> modbus_link ++: processCommand() -alt response available - modbus_link -> modbus_device ++ : linkCallback() - modbus_device -> plugin_struct : update latest value - modbus_link <-- modbus_device -- -end -alt no transaction - modbus_link -> queue : get next transaction - alt transaction available - modbus_link -> modbus_link : send request - end -end -modbus_device <-- modbus_link -- -plugin_struct <-- modbus_device -- -plugin <-- plugin_struct -- -system <-- plugin -- -@enduml \ No newline at end of file diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 9659b17a43..1e346555a6 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -130,13 +130,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data != nullptr) { success = P183_data->plugin_init(P183_DEV_ID, P183_LINK_ID); } - # ifdef P183_DEBUG else { addLogMove(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); + success = false; } - # endif // P183_DEBUG - - success = true; break; } @@ -170,9 +167,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - # ifdef P183_DEBUG addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus write invalid data struct")); - # endif // P183_DEBUG return false; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 546065ad52..bef9394ee9 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -100,6 +100,9 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, return false; } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + if (request == nullptr) { + return false; // Failed to allocate a request structure + } request->_userData = valuePtr; request->_userState = statePtr; request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; @@ -127,10 +130,10 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, uint16_t registerAddress, uint16_t uid) { - if (_modbus_link == nullptr) { - return false; - } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + if (request == nullptr) { + return false; // Failed to allocate a request structure + } request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; request->_userData = nullptr; request->_userState = nullptr; From 0f9c1dd3eb3b51683c3cf5696362181e6ca4cc34 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 3 May 2026 21:24:56 +0200 Subject: [PATCH 28/48] Rework & fixed initialization --- misc/modbusFacility/Modbus_class.puml | 18 ++++----- misc/modbusFacility/Modbus_class.svg | 1 + misc/modbusFacility/Modbus_link_state.puml | 4 +- misc/modbusFacility/Modbus_link_state.svg | 1 + misc/modbusFacility/Modbus_seq2.svg | 1 + src/_P183_modbus.ino | 20 ++++++---- src/src/Helpers/Modbus_device.cpp | 43 ++++++++++++---------- src/src/Helpers/Modbus_device.h | 2 +- src/src/Helpers/Modbus_link.cpp | 16 +++++++- src/src/Helpers/Modbus_mgr.cpp | 2 +- src/src/PluginStructs/P183_data_struct.cpp | 17 +++++---- src/src/PluginStructs/P183_data_struct.h | 2 +- 12 files changed, 76 insertions(+), 51 deletions(-) create mode 100644 misc/modbusFacility/Modbus_class.svg create mode 100644 misc/modbusFacility/Modbus_link_state.svg create mode 100644 misc/modbusFacility/Modbus_seq2.svg diff --git a/misc/modbusFacility/Modbus_class.puml b/misc/modbusFacility/Modbus_class.puml index fe6f2ea0f7..9e26150018 100644 --- a/misc/modbusFacility/Modbus_class.puml +++ b/misc/modbusFacility/Modbus_class.puml @@ -8,35 +8,33 @@ class plugin_struct { } +package "modbusFacility" { class Modbus_mgr <> { - } class Modbus_link { } class Modbus_device { - } -class Serial_port { + class Queue { } -class Queue { - + struct Transaction { + } } -class Transaction { - +class Serial_port { } plugin *-- plugin_struct -plugin_struct *-- Modbus_device -Modbus_device "*" -right-> "1" Modbus_link +plugin_struct *-right- Modbus_device +Modbus_device "*" -down-> "1" Modbus_link Modbus_link "1" -right-> "1" Serial_port Modbus_mgr "1" *-- "*" Modbus_link Modbus_mgr "1" -- "*" Modbus_device Modbus_link *-- "1" Queue -Queue o-- "*" Transaction +Queue o-left- "*" Transaction Modbus_device "1" --> "*" Transaction @enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_class.svg b/misc/modbusFacility/Modbus_class.svg new file mode 100644 index 0000000000..79a12939c4 --- /dev/null +++ b/misc/modbusFacility/Modbus_class.svg @@ -0,0 +1 @@ +modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_port*1111*1*1*1* \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_link_state.puml b/misc/modbusFacility/Modbus_link_state.puml index c15f6e3fe6..c46230be2f 100644 --- a/misc/modbusFacility/Modbus_link_state.puml +++ b/misc/modbusFacility/Modbus_link_state.puml @@ -1,5 +1,5 @@ @startuml -[*] --> NOT_QUEUED +[*] --> NOT_QUEUED : newTransaction() state active { NOT_QUEUED --> QUEUED : queueTransaction() QUEUED --> MESSAGE_SENT : link inactive @@ -7,5 +7,5 @@ MESSAGE_SENT --> RESPONSE_RECEIVED : received valid response MESSAGE_SENT --> ERROR_OCCURRED : response timeout or invalid response } active -down-> READY_FOR_DESTROY : freeTransaction() -READY_FOR_DESTROY --> [*] +READY_FOR_DESTROY --> [*] : processing @enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_link_state.svg b/misc/modbusFacility/Modbus_link_state.svg new file mode 100644 index 0000000000..b6ff91aed4 --- /dev/null +++ b/misc/modbusFacility/Modbus_link_state.svg @@ -0,0 +1 @@ +activeQUEUEDMESSAGE_SENTRESPONSE_RECEIVEDERROR_OCCURREDNOT_QUEUEDREADY_FOR_DESTROYnewTransaction()queueTransaction()link inactivereceived valid responseresponse timeout or invalid responsefreeTransaction()processing \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_seq2.svg b/misc/modbusFacility/Modbus_seq2.svg new file mode 100644 index 0000000000..6adf1e64b6 --- /dev/null +++ b/misc/modbusFacility/Modbus_seq2.svg @@ -0,0 +1 @@ +SystemSystemPluginPluginPlugin_structModbus_device«singleton»Modbus_mgr«singleton»Modbus_mgrModbus_linkQueueQueueinitnew()Modbus_linkinitnew()Plugin_structnew()Modbus_deviceconnect()linkreadreadread()readHoldingRegister()queueTransaction()addretrieve latest valueten_per_secondprocessCommand()alt[response available]linkCallback()PLUGIN_TASKTIMER_INplugin_task_timer()alt[no transaction]get next transactionalt[transaction available]send request \ No newline at end of file diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 1e346555a6..8e7583a82e 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -131,7 +131,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) success = P183_data->plugin_init(P183_DEV_ID, P183_LINK_ID); } else { + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183 : Cannot initialize")); + # endif // LIMIT_BUILD_SIZE success = false; } break; @@ -153,9 +155,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - # ifdef P183_DEBUG + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read invalid data struct")); - # endif // P183_DEBUG + # endif // LIMIT_BUILD_SIZE return false; } success = P183_data->plugin_read(event); // Delegate to data_struct @@ -167,7 +169,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus write invalid data struct")); + # endif // LIMIT_BUILD_SIZE return false; } @@ -226,11 +230,11 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) ModbusMGR_singleton.dumpAdminInfo(); success = true; } - # ifdef P183_DEBUG + # ifndef LIMIT_BUILD_SIZE else { addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Unknown command")); } - # endif // P183_DEBUG + # endif // LIMIT_BUILD_SIZE } break; @@ -241,9 +245,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - # ifdef P183_DEBUG + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus task timer invalid data struct")); - # endif // P183_DEBUG + # endif // LIMIT_BUILD_SIZE return false; } success = P183_data->plugin_task_timer(event); // Delegate to data_struct @@ -255,9 +259,9 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); if (P183_data == nullptr) { - # ifdef P183_DEBUG + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config invalid data struct")); - # endif // P183_DEBUG + # endif // LIMIT_BUILD_SIZE return false; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index bef9394ee9..2dfab51d71 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -96,17 +96,18 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t *valuePtr, ModbusResultState *statePtr) { - if (_modbus_link == nullptr) { + if (!isInitialized()) { return false; } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + if (request == nullptr) { return false; // Failed to allocate a request structure } request->_userData = valuePtr; request->_userState = statePtr; request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - createReadFrame(request, _modbus_address, address); + createReadFrame(*request, _modbus_address, address); uint16_t queueID = _modbus_link->queueTransaction(request); return true; } @@ -130,7 +131,11 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, uint16_t registerAddress, uint16_t uid) { + if (!isInitialized()) { + return false; + } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + if (request == nullptr) { return false; // Failed to allocate a request structure } @@ -138,7 +143,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, request->_userData = nullptr; request->_userState = nullptr; request->_userId = uid; - createReadFrame(request, busAddress, registerAddress); + createReadFrame(*request, busAddress, registerAddress); uint16_t queueID = _modbus_link->queueTransaction(request); return true; } @@ -146,22 +151,22 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Construct a Modbus read holding registers message /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement *request, +void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, uint8_t busAddress, uint16_t registerAddress) { - request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - request->_sendframe[0] = busAddress; - request->_sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; - request->_sendframe[2] = highByte(registerAddress); - request->_sendframe[3] = lowByte(registerAddress); - request->_sendframe[4] = 0; - request->_sendframe[5] = 1; // Read 1 register - uint16_t crc = CalculateCRC(request->_sendframe, 6); - request->_sendframe[6] = lowByte(crc); // CRC low byte - request->_sendframe[7] = highByte(crc); // CRC high byte - request->_sendframe_length = 8; // Size with CRC - request->_rcvframe_length = 7; // Expect 8 bytes in response + request._messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + request._sendframe[0] = busAddress; + request._sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; + request._sendframe[2] = highByte(registerAddress); + request._sendframe[3] = lowByte(registerAddress); + request._sendframe[4] = 0; + request._sendframe[5] = 1; // Read 1 register + uint16_t crc = CalculateCRC(request._sendframe, 6); + request._sendframe[6] = lowByte(crc); // CRC low byte + request._sendframe[7] = highByte(crc); // CRC high byte + request._sendframe_length = 8; // Size with CRC + request._rcvframe_length = 7; // Expect 8 bytes in response } bool ModbusDEVICE_struct::readHoldingRegisterResult(uint16_t uid, uint16_t *valuePtr) { return false; } @@ -174,7 +179,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, ModbusResultState *statePtr) { - if (_modbus_link == nullptr) { + if (!isInitialized()) { return false; } Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); @@ -202,9 +207,9 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, void ModbusDEVICE_struct::processCommand(void) { - if (_modbus_link != nullptr) { + if (!isInitialized()) { _modbus_link->processCommand(); - } + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 782bf2c00a..0f98f0df74 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -96,7 +96,7 @@ struct ModbusDEVICE_struct { int par3, int par4); - void createReadFrame(Modbus_RequestQueueElement *request, + void createReadFrame(Modbus_RequestQueueElement& request, uint8_t busAddress, uint16_t registerAddress); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index efa04404db..ec4d1963c7 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -109,6 +109,11 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVICE_struct *device) { + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to create transaction on uninitialized link")); + return nullptr; + } + Modbus_RequestQueueElement *req = new (std::nothrow) Modbus_RequestQueueElement(); if (req != nullptr) { @@ -146,6 +151,11 @@ bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) { + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to free transactions on uninitialized link")); + return; + } + for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { if ((*it)->_device == device) { (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed @@ -158,8 +168,12 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) // The client can use this identifier to retrieve the response later. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transaction) { + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); + return 0; + } + # ifdef MODBUS_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus: Link, Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index c21dd2fecc..696e6fe7ca 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -193,7 +193,7 @@ bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *de dumpAdminInfo(); # endif // ifdef MODBUS_DEBUG - return false; + return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 386de624d3..0a8010619b 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -6,7 +6,6 @@ // ####################################################################################################### // ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### -# define P183_DEBUG # ifdef BUILD_NO_DEBUG # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -43,9 +42,9 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) _modbusDevice = new (std::nothrow) ModbusDEVICE_struct(); if (_modbusDevice == nullptr) { - # ifdef P183_DEBUG + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183: Unable to allocate Modbus device object")); - # endif // P183_DEBUG + # endif // LIMIT_BUILD_SIZE return false; } @@ -53,7 +52,7 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) return false; } _modbusDevice->setModbusTimeout(P183_MODBUS_TIMEOUT); - return false; + return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -119,9 +118,9 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) int outputIndex = event->idx; if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { - # ifdef P183_DEBUG + # ifdef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); - # endif // P183_DEBUG + # endif // LIMIT_BUILD_SIZE return false; } @@ -200,9 +199,11 @@ void P183_data_struct::scan_next_module() if (_scanning) { if (_lastAddress <= _endAddress) { _modbusDevice->readModuleHoldingRegister(_lastAddress, 1, ACTION_SCAN_BUS); - } else { + } + else + { _scanning = false; - addLogMove(LOG_LEVEL_INFO, F("Modbus: Finished scanning device")); + addLogMove(LOG_LEVEL_INFO, F("Modbus: Finished scanning for modules")); } } } diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 7ee7d1285a..666bc15690 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -4,7 +4,7 @@ #include "../../_Plugin_Helper.h" #ifdef USES_P183 -# define P183_DEBUG // Switch on additional debug logging +///# define P183_DEBUG // Switch on additional debug logging # ifdef BUILD_NO_DEBUG # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG From d254ae4e3d389a352c2041ee4f98aa5615089661 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Tue, 5 May 2026 20:51:34 +0200 Subject: [PATCH 29/48] P183 documentation update --- docs/source/Interfaces/Interfaces.rst | 13 +- docs/source/Interfaces/Modbus_Interface.png | Bin 0 -> 36479 bytes docs/source/Plugin/P183.rst | 16 +- docs/source/Plugin/P183_device_settings.png | Bin 0 -> 5822 bytes docs/source/Plugin/P183_sensor_config.png | Bin 24343 -> 0 bytes docs/source/Plugin/_plugin_sets_overview.repl | 1859 ----------------- src/_P183_modbus.ino | 8 +- 7 files changed, 21 insertions(+), 1875 deletions(-) create mode 100644 docs/source/Interfaces/Modbus_Interface.png create mode 100644 docs/source/Plugin/P183_device_settings.png delete mode 100644 docs/source/Plugin/P183_sensor_config.png diff --git a/docs/source/Interfaces/Interfaces.rst b/docs/source/Interfaces/Interfaces.rst index 87e9084f16..8025e9885e 100644 --- a/docs/source/Interfaces/Interfaces.rst +++ b/docs/source/Interfaces/Interfaces.rst @@ -205,5 +205,16 @@ NB: When selecting the *User-defined* option, **all 3 GPIO pins should be set**, Modbus ------ -TODO Modbus configuration and documentation to be added. +When using devices that use the Modbus RTU protocol the Modbus interface can be configured here. The Modbus RTU devices are connected via a RS485 serial interface. Multiple devices can be connected to the same serial interface, and are distinguished by their Modbus address. Note that a serial to RS485 converter is required to connect Modbus RTU devices to the ESP board. +.. image:: Modbus_Interface.png + +Main configuration item is the selection of the serial interface to use for Modbus. The available options depend on the ESP board used. For details about the available serial interfaces, see serial helper page. The option "Not set" means that Modbus link is not used. When a serial interface is selected, it will be initialized and the Modbus RTU devices can be connected to it. + +Selection of the RX and TX pins is required. + +The Modbus RTU devices are connected to the selected serial interface via a RS485 converter. The converter has a DE (Driver Enable) pin that can be controlled by the ESP board to switch between sending and receiving data. The DE pin can be configured here. If the converter has an automatic DE function, this can be set to "-None-". + +The Baud rate for the Modbus RTU communication shall be set. The default value is 9600 baud, but it can be set to other values as required by the connected devices. + +Collision Detection is a feature that can be used when the serial port hardware supports it, otherwise it is ignored. \ No newline at end of file diff --git a/docs/source/Interfaces/Modbus_Interface.png b/docs/source/Interfaces/Modbus_Interface.png new file mode 100644 index 0000000000000000000000000000000000000000..5987bb147da2ef5d4b5b1c433ab59ecf0d431e29 GIT binary patch literal 36479 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%0D2N4lvh>Ei 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst index 6ef0db396a..8952920b63 100644 --- a/docs/source/Plugin/P183.rst +++ b/docs/source/Plugin/P183.rst @@ -55,24 +55,14 @@ Configuration Sensor ^^^^^^ -.. image:: P183_sensor_config.png +.. image:: P183_device_settings.png -The available Serial protocol settings here depend on the build used. +The available Modbus protocol settings here depend on the build used. -* **Serial Port**: The serial port to use for Modbus communication. - -* **Baudrate**: The baudrate for the serial communication. Common values are 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200. +* **Modbus Link**: The Modbs link teh device is connected to. The link must be configured in the Interafces page. * **Modbus Device Address**: The Modbus slave device ID to communicate with (1..247). -* **Enable Collision Detection**: Enable collision detection to avoid data corruption when multiple devices are connected to the same serial bus. This requires a GPIO pin to be configured as input and connected to the bus transceiver's DE/RE pin. - -* **ESP RX GPIO ← TX (RO)**: The GPIO pin to use as RX input for the serial communication. This pin should be connected to the TX output of the RS-485 to TTL converter. - -* **ESP TX GPIO → RX (DI)**: The GPIO pin to use as TX output for the serial communication. This pin should be connected to the RX input of the RS-485 to TTL converter. - -* **GPIO → RE/DE(optional)**: The GPIO pin to use for collision detection. This pin should be connected to the DE/RE pin of the RS-485 to TTL converter. - Output Configuration ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/Plugin/P183_device_settings.png b/docs/source/Plugin/P183_device_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..1e1fb8f788c457c98f158578c2d7d1996366a2dd GIT binary patch literal 5822 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P183_sensor_config.png b/docs/source/Plugin/P183_sensor_config.png deleted file mode 100644 index 898e98e227e6fcdf0e39368f2675fd0ac30b88f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24343 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 uint16_t value = 0; value = P183_data->readRegisterWait(address); - # ifdef P183_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus read value %u from address 0x%04x"), value, address)); } - # endif // P183_DEBUG success = true; } else if (equals(subcmd, F("dump"))) { @@ -276,6 +274,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } break; } + case PLUGIN_WEBFORM_SHOW_CONFIG: + { + string += strformat(F("Modbus %d"), P183_LINK_ID); + success = true; + break; + } } return success; From 30d2b129498c80e6974f51fab1a37d4291403c1f Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sat, 9 May 2026 20:24:27 +0200 Subject: [PATCH 30/48] P183 Modbus cleanup, not ready yet --- misc/modbusFacility/Modbus_notes.txt | 61 ++++++++++++++-------- src/src/CustomBuild/StorageLayout.h | 46 +++++++++------- src/src/CustomBuild/define_plugin_sets.h | 2 +- src/src/DataTypes/SettingsType.cpp | 4 ++ src/src/Helpers/Modbus_device.cpp | 8 ++- src/src/Helpers/Modbus_device.h | 6 --- src/src/Helpers/Modbus_link.cpp | 8 +++ src/src/Helpers/Modbus_mgr.cpp | 9 +--- src/src/PluginStructs/P183_data_struct.cpp | 1 - 9 files changed, 84 insertions(+), 61 deletions(-) diff --git a/misc/modbusFacility/Modbus_notes.txt b/misc/modbusFacility/Modbus_notes.txt index afe8fce90b..13db191fb1 100644 --- a/misc/modbusFacility/Modbus_notes.txt +++ b/misc/modbusFacility/Modbus_notes.txt @@ -1,8 +1,9 @@ -The MODBUS_FAC facilities intend to provide a framework that allows multiple Modbus RTU devices to connect to a single physical Modbus link. It supports multiple physical Modbus links in parallel. +The MODBUS_FAC facilities intend to provide a framework that allows multiple Modbus RTU devices to connect to a single physical Modbus link. It supports multiple physical Modbus links in parallel on the ame bus. This is realized by introducing 3 classes: -- Modbus_device: Represents a single device. The Modbus_device is responsibel for coding/decoding the Modus protocol. -- Modbus_link: Represents a single Modbus conenction. Each Modbus_link object is connected to a separate serial link. +- Modbus_device: Represents a single device. The Modbus_device is responsible for coding/decoding the Modus protocol. +- Modbus_link: Represents a single Modbus connection. Each Modbus_link object is connected to a separate serial link. - Modbus_mgr: Singleton manager that manages all links and keeps track of which devices are connected to which link. +See class diagram Modbus_class for relations. The main design requirement is to allow multiple plugins to use the same Modbus link. This is achieved by separating the link control from the device control. Modbus connunication consists of a request-reply message exchange over a serial link. After a request is sent the bus is occupied until the reply is received or a timeout occurred. @@ -12,29 +13,43 @@ The main design constraint for the MODBUS_FAC is to reduce the time wasted in wa The queue is owned by the Modbus_link. A Modbus_device puts a request message on the queue and does not wait for the reply. Once the matching reply is received by the Modbus_link the Modbus_device is notified by a callback function. -The Modbus_device uses transactions to communicate to the associated hardware module. Messages are encoded in Modbus RTU protocol format by the Modbus_device. +The Modbus_device uses transactions to communicate to the associated Modbus hardware module. Messages are encoded in Modbus RTU protocol format by the Modbus_device. Once the request message is created it is transferred to the Modbus_link where it is added the the transaction queue. -When the transaction is done, either a response message is received or a timeout occurred, the Modbus_device callback function is called and the response message is decoded and evaluated. +When the transaction is done, either a response message is received or a timeout has occurred, the Modbus_device callback function is called and the response message or timeout is decoded and evaluated. -The Modbus_link uses a queue to handle transactions sequentially. Each transaction has a state to track if it is queued, being processed, or completed. At maximum one transaction is being processed at a time. -The modbus_link uses an ESPEasy serial link for the actual transmit and receive of the messages. +The Modbus_link uses a queue to handle transactions sequentially. Each transaction has a state to track if it is queued, being processed, or completed. At maximum one single transaction per link is being processed at a time. +The modbus_link uses an ESPEasy serial link for the actual transmitting and receiving of the messages. -To match a Modbus_device with a Modbus_link a singleton Modus_mgr is used. This Modbus_mgr owns all Modbus_links and determines to which link a Modbus_device is assigned. -In case no matching link is found the Modbus_mgr creates a corresponding Modbus_link. Currently ESPEasy has no means to configure a Modbus_link object with the serial link properties. Instead the serial link is configured in the plugin. -The plugin passes the configuration data to the device. When the Modbus_device requests the Modbus_mgr to connect to a Modbus_link it passes the serial parameters. The Modbus_mgr uses the serial port type to distinguish the links. -Whenever a new device is connected to a Modbus_link the link will be reprogrammed with the new serial parameters. Note that all devices using the same serial link shall provide the same serial link parameters. -In case of different serial configuration parameters there is no guarantee which set will be used for the Modbus_link. - -In future a separate configuration screen should be implemented to configure each Modus_link. Once this is available the Modbus_device can mention the link identification without the need to pass all configuration parameters. - -The current implementataion of the Modbus_mgr uses the serial port (ESPEasySerialPort enum) to identify the Modbus_link. This implies that only one software serial link can be used. -It should be possible to extend this by checking the assigned RX and TX pins when a software serial port is selected. +A singleton Modbus_mgr owns the Modbus_links. The Modbus_mgt is responsible for creating and configuring the Modbus_links. Configuration is done through the interfaces page, tab Modbus. +The Modbus_device is configured with a link ID. The Modbus_device uses the link ID to register itself at the Modbus_mgr. The Modbus_mgr will return a pointer to the associated Modbus_link. Once connected the Modbus_device will access teh Modbus_link directly. Drawback of the queue mechanism is the need for the Modbus_device (and the associated plugin) to wait for the response message without blocking the CPU. For this the Modbus_device needs to track "state". -Either the plugin can queue only a single transaction or it must be able to track for which transaction in the queue a response is received. -To keep this state tracking generic the transaction contains a pointer to a userState and userData. The Modbus_device can use these values depending on the transaction type to return state information to the plugin. +A plugin can queue only a single transaction or it must be able to track for which transaction in the queue a response is received. +Sevaral mechanisms are used to track transactions at link level: +1) The Modbus_link assigns a unique ID to each transaction. This is a 16 bit sequence number that will wrap around. The Modbus_device can use the ID to match a respose to the right pending transaction. +2) The Modbus_device can use the field _userID to store a 16 bit identifier. This identifier is not changed or interpreted by the Modbus_link. It can be used to recognize the transaction based on a Modbus_device determined algorithm +3) The Modbus_device can use the fields _userData and _userState to add two pointers that will not be changed or interpreted by the Modbus_link. These can be used to associate transaction related data with the transaction. + +The Modbus_device provides Modbus access functions (commands) to the plugin. Access like reading or writing registers is translated into a Modbus send message and queued on the Modbus_link. Once the message is queued the Modbus_devcie will return. +The Modbus_device may have multiple pending transactions. The Modbus_device uses one of the mentioned tracking mechanisms to recognize to which Modbus command the response message belongs. The response will be decoded. +In case the plugin needs to be informed about the results, e.g. a read command, the Modbus_device will send a PLUGIN_TASKTIMER_IN event to the plugin. This allows the plugin to process the results. See Modbus_seq2 sequence diagram. + +The PLUGIN_TASKTIMER_IN event works well for plugin functions that allow independent processing once the data is available. Some plugin functions like PLUGIN_GET_CONFIG_VALUE require a direct response. +If this response depends on Modbus tarnsactions the plugin shall either regularly poll the data and cache it or take the penalty and use a blocking funtion to retrieve this data. -The owning plugin can only return a value once it is received. For repetitive read events the previous received value can be returned. -For specific read commands either a polling mechanism must be implemented or the plugin blocks until the response is received. -The most efficient mechanism would be a trigger event that can be sent by the Modbus_device once the data is received. Such trigger event for plugins can reduce the CPU load by removing the need for regular polling (e.g. ten_per_second) or blocking on a transaction. -For this the plugin event must be extended with the possibility to send "updates" to the plugin. Such an extension shall be generic and not Modbus specific. It is beyond the Modbus facilities development to add such new generic plugin features. \ No newline at end of file +The Modbus_device provides the following functions to a plugin: + +1) bool readHoldingRegister(uint16_t address, uint16_t *valueptr, ModbusResultState *stateptr); +This function reads a single holding register from the Modbus hardware. The valueptr returns the value from the register once the transaction is finished. The stateptr shows the actual state of the transaction. The plugin shall poll the state until the tranaction is finished. +The Modbus_device will not use a PLUGIN_TASKTIMER_IN event to indicate the function has finished + +2) bool readHoldingRegister(uint16_t address, uint16_t uid); +This function reads a single holding register from the Modbus hardware. The uid shall be used by the plugin to recognize which read action the result belongs to. This uid will also be used in the PLUGIN_TASKTIMER_IN event to indicate the command. +At the end of the transaction the Modbus_device sends a PLUGIN_TASKTIMER_IN event with the following data: + idx: The uid given with the command + par1: Success of the transaction 0=not succseful 1=succesful, result available + par2: The value of the holding register cast to a int + +3) bool writeSingleRegister(uint16_t address, uint16_t value, ModbusResultState *stateptr); +This function writes a single holding register to the Modbus hardware. The value will be written to the specified register address. The stateptr shows the actual state of the transaction. The plugin may poll the state until the tranaction is finished. +The Modbus_device will not use a PLUGIN_TASKTIMER_IN event to indicate the function has finished \ No newline at end of file diff --git a/src/src/CustomBuild/StorageLayout.h b/src/src/CustomBuild/StorageLayout.h index 1ac3034aa3..8d7dc87618 100644 --- a/src/src/CustomBuild/StorageLayout.h +++ b/src/src/CustomBuild/StorageLayout.h @@ -63,7 +63,7 @@ #else # define DAT_BASIC_SETTINGS_SIZE 3072 // Current Settings Struct size is ~1.3k, leave some room to extend #endif -#endif +#endif #ifdef ESP32 # define DAT_BASIC_SETTINGS_SIZE 6144 // Current Settings Struct size is ~3k, leave some room to extend #endif @@ -132,11 +132,11 @@ #ifndef DAT_NETWORK_INTERFACE_SIZE # define DAT_NETWORK_INTERFACE_SIZE 1024 -#endif +#endif #ifndef DAT_MODBUS_INTERFACE_SIZE # define DAT_MODBUS_INTERFACE_SIZE 256 // Reserved size for Modbus link settings -#endif +#endif /* @@ -171,11 +171,11 @@ # define DAT_OFFSET_CONTROLLER (DAT_OFFSET_TASKS + (DAT_TASKS_DISTANCE * TASKS_MAX)) // each controller = 1k, 3 max, DAT_OFFSET_CDN is at position of any 4th controller. # endif // ifndef DAT_OFFSET_CONTROLLER # ifndef DAT_OFFSET_CUSTOM_CONTROLLER - # define DAT_OFFSET_CUSTOM_CONTROLLER (DAT_OFFSET_CONTROLLER + (DAT_CONTROLLER_SIZE * CONTROLLER_MAX)) // each custom controller config = 1k, 3 max + # define DAT_OFFSET_CUSTOM_CONTROLLER (DAT_OFFSET_CONTROLLER + (DAT_CONTROLLER_SIZE * CONTROLLER_MAX)) // each custom controllerconfig = 1k, 3 max # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER # ifndef DAT_OFFSET_CDN # define DAT_OFFSET_CDN (DAT_OFFSET_CUSTOM_CONTROLLER - DAT_CDN_SIZE) // single CDN settings block of 1k - # endif + # endif # ifndef CONFIG_FILE_SIZE # define CONFIG_FILE_SIZE 65536 @@ -193,8 +193,8 @@ # define DAT_OFFSET_CUSTOM_CONTROLLER 32768 // each custom controller config = 1k, 4 max. # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER # ifndef DAT_OFFSET_CDN - # define DAT_OFFSET_CDN (DAT_OFFSET_TASKS - DAT_CDN_SIZE) // single CDN settings block of 1k - # endif + # define DAT_OFFSET_CDN (DAT_OFFSET_TASKS - DAT_CDN_SIZE) // single CDN settings block of 1k + # endif # ifdef LIMIT_BUILD_SIZE // Limit the config size for 1M builds, since their file system is also quite small @@ -220,23 +220,20 @@ # define DAT_OFFSET_CUSTOM_CONTROLLER 12288 // each custom controller config = 1k, 4 max. # endif // ifndef DAT_OFFSET_CUSTOM_CONTROLLER # ifndef DAT_OFFSET_CDN - # define DAT_OFFSET_CDN (DAT_OFFSET_CONTROLLER - DAT_CDN_SIZE) // single CDN settings block of 1k - # endif + # define DAT_OFFSET_CDN (DAT_OFFSET_CONTROLLER - DAT_CDN_SIZE) // single CDN settings block of 1k + # endif # ifndef CONFIG_FILE_SIZE # define CONFIG_FILE_SIZE (DAT_OFFSET_TASKS + ((DAT_TASKS_SIZE + DAT_TASKS_CUSTOM_SIZE) * TASKS_MAX)) # endif // ifndef CONFIG_FILE_SIZE - // On ESP8266 we will not store Network Interface settings - // The only 2 interfaces supported on ESP8266 are: - // - WiFi STA - // - WiFi AP - // Both do not need extra settings for basic functionality - // All other network interface plugins will be ESP32-only. +// On ESP8266 we will not store Network Interface settings +// The only 2 interfaces supported on ESP8266 are: +// - WiFi STA +// - WiFi AP +// Both do not need extra settings for basic functionality +// All other network interface plugins will be ESP32-only. # ifndef DAT_NETWORK_INTERFACES_OFFSET # define DAT_NETWORK_INTERFACES_OFFSET 16384 - # endif - # ifndef DAT_MODBUS_INTERFACE_OFFSET - # define DAT_MODBUS_INTERFACE_OFFSET 6144 // each Modbus link config = 256 bytes, 4 max # endif # ifndef DAT_OFFSET_DEV_CREDENTIALS # define DAT_OFFSET_DEV_CREDENTIALS 0 @@ -246,5 +243,18 @@ # endif #endif // if defined(ESP32) + #ifndef DAT_MODBUS_INTERFACE_OFFSET + # if FEATURE_NON_STANDARD_24_TASKS && defined(ESP8266) + # if defined(FEATURE_MODBUS_FAC) && FEATURE_MODBUS_FAC + # error "Not yet defined where to store modbus data, see https: // github.com/letscontrolit/ESPEasy/pull/5390#pullrequestreview-4124760094" + // " + # endif + # else // ifdef FEATURE_NON_STANDARD_24_TASKS + # if defined(FEATURE_MODBUS_FAC) && FEATURE_MODBUS_FAC + # define DAT_MODBUS_INTERFACE_OFFSET DAT_BASIC_SETTINGS_SIZE // Stored in the 1k right after the basic settings, each Modbus link + // config = 256 bytes, 4 max + # endif + # endif // ifdef FEATURE_NON_STANDARD_24_TASKS + #endif // ifndef DAT_MODBUS_INTERFACE_OFFSET #endif // CUSTOMBUILD_STORAGE_LAYOUT_H diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index b73d4ea959..baa2f64413 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -4516,7 +4516,7 @@ To create/register a plugin, you have to : # endif #endif -#if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE || FEATURE_STORE_NETWORK_INTERFACE_SETTINGS +#if FEATURE_STORE_CREDENTIALS_SEPARATE_FILE || FEATURE_STORE_NETWORK_INTERFACE_SETTINGS || FEATURE_MODBUS_FAC # ifdef FEATURE_ESPEASY_KEY_VALUE_STORE # undef FEATURE_ESPEASY_KEY_VALUE_STORE # endif diff --git a/src/src/DataTypes/SettingsType.cpp b/src/src/DataTypes/SettingsType.cpp index 7f76378f79..01bc068d0b 100644 --- a/src/src/DataTypes/SettingsType.cpp +++ b/src/src/DataTypes/SettingsType.cpp @@ -256,6 +256,10 @@ unsigned int SettingsType::getSVGcolor(Enum settingsType) { case Enum::CdnSettings_Type: return 0xff6600; #endif +# if FEATURE_MODBUS_FAC + case Enum::ModbusInterfaceSettings_Type: + return 0x0066FF; +# endif case Enum::SettingsType_MAX: break; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 2dfab51d71..85831ee5a4 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -108,7 +108,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, request->_userState = statePtr; request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; createReadFrame(*request, _modbus_address, address); - uint16_t queueID = _modbus_link->queueTransaction(request); + (void) _modbus_link->queueTransaction(request); return true; } @@ -144,7 +144,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, request->_userState = nullptr; request->_userId = uid; createReadFrame(*request, busAddress, registerAddress); - uint16_t queueID = _modbus_link->queueTransaction(request); + (void) _modbus_link->queueTransaction(request); return true; } @@ -169,8 +169,6 @@ void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, request._rcvframe_length = 7; // Expect 8 bytes in response } -bool ModbusDEVICE_struct::readHoldingRegisterResult(uint16_t uid, uint16_t *valuePtr) { return false; } - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start writing a Modbus single register. // The function returns true if the request was queued. @@ -199,7 +197,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 8; // Expect 8 bytes in response ////dump_buffer(request->_sendframe, request->_sendframe_length); - uint16_t queueID = _modbus_link->queueTransaction(request); + (void) _modbus_link->queueTransaction(request); *statePtr = ModbusResultState::Busy; return true; diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 0f98f0df74..8f7ca73fa1 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -60,12 +60,6 @@ struct ModbusDEVICE_struct { bool readHoldingRegister(uint16_t address, uint16_t uid); - // Fetch the result of a readHoldingRegister request - // The function returns true if the result is available - // Use uid to identify the request. - bool readHoldingRegisterResult(uint16_t uid, - uint16_t *valuePtr); - // Start writing a single Modbus register. // The function returns true if the request was queued. bool writeSingleRegister(uint16_t address, diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index ec4d1963c7..73b80876d7 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -172,6 +172,14 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); return 0; } + if (transaction == nullptr) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); + return 0; + } + if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER ) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, receive buffer too large")); + return 0; + } # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 696e6fe7ca..9323e75eb7 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -209,14 +209,9 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { if ((_modbus_devices[i] != nullptr) && (_modbus_devices[i]->deviceID == deviceID)) { - // Found the device to disconnect - ModbusLinkInfo_struct *linkInfoPtr = _modbus_devices[i]->link; - - // Remove the device entry + // Remove the device entry delete _modbus_devices[i]; _modbus_devices[i] = nullptr; - - return true; // Successfully disconnected } } # ifdef MODBUS_DEBUG @@ -296,7 +291,7 @@ void ModbusMGR_struct::show_modbus_interfaces() int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Map to keep track of valid ports and their indices in the // options_port array - constexpr size_t optionBaudCount = NR_ELEMENTS(options_baudrate); + constexpr int optionBaudCount = static_cast(NR_ELEMENTS(options_baudrate)); for (int i = 0; i < optionBaudCount; ++i) { options_baudrate[i] = modbus_storageValueToBaudrate(i); diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 0a8010619b..f35b3c8695 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -138,7 +138,6 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) { uint16_t value = 0; - ModbusResultState state = ModbusResultState::Busy; addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); From 6f2cb43b00a01164f0551be2511cef1fb1b0f6b8 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 17 May 2026 16:13:15 +0200 Subject: [PATCH 31/48] P183 Modbus add cache region --- src/_P183_modbus.ino | 59 ++++++-- src/src/Helpers/Modbus_device.cpp | 151 ++++++++++++++++----- src/src/Helpers/Modbus_device.h | 64 ++++++--- src/src/Helpers/Modbus_link.cpp | 23 +++- src/src/PluginStructs/P183_data_struct.cpp | 64 ++++++++- src/src/PluginStructs/P183_data_struct.h | 38 ++++-- 6 files changed, 312 insertions(+), 87 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 83759cae51..288f7421e3 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -100,8 +100,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { - addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); - addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, 247); + addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); + addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, P183_MAX_MODBUS_NODES); + addFormNumericBox(F("Cache size"), P183_CACHE_SIZE_LABEL, static_cast(P183_CACHE_SIZE), 0, P183_CACHE_SIZE_MAX); + addFormNumericBox(F("Cache start address"), P183_CACHE_START_LABEL, static_cast(P183_CACHE_START), 0, P183_CACHE_START_MAX); success = true; break; @@ -109,9 +111,11 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); - P183_LINK_ID = getFormItemInt(P183_LINK_ID_LABEL); - P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); + P183_DEV_ID = getFormItemInt(P183_DEV_ID_LABEL); + P183_LINK_ID = getFormItemInt(P183_LINK_ID_LABEL); + P183_NR_OUTPUTS = getFormItemInt(P183_NR_OUTPUTS_LABEL); + P183_CACHE_START = getFormItemInt(P183_CACHE_START_LABEL); + P183_CACHE_SIZE = getFormItemInt(P183_CACHE_SIZE_LABEL); for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { @@ -195,9 +199,15 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } else if (equals(subcmd, F("read"))) { // Read a value from a Modbus register - int address = event->Par2; - uint16_t value = 0; - value = P183_data->readRegisterWait(address); + uint16_t address = event->Par2; + uint16_t value = 0; + + if ((address >= P183_CACHE_START) && (address < P183_CACHE_START + P183_CACHE_SIZE)) { + value = P183_data->readRegisterCache(address); + } + else { + value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged + } if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus read value %u from address 0x%04x"), value, address)); @@ -205,8 +215,8 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) success = true; } else if (equals(subcmd, F("dump"))) { - int start_address = event->Par2; - int end_address = event->Par3; + uint16_t start_address = event->Par2; + uint16_t end_address = event->Par3; if (end_address < start_address) { end_address = start_address; @@ -266,20 +276,43 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) const String cmd = parseString(string, 1); if (equals(cmd, F("register"))) { - int address = parseString(string, 2).toInt(); - uint16_t value = 0; - value = P183_data->readRegisterWait(address); + uint16_t address = parseString(string, 2).toInt(); + uint16_t value = 0; + + if ((address >= P183_CACHE_START) && (address < P183_CACHE_START + P183_CACHE_SIZE)) { + value = P183_data->readRegisterCache(address); + } + else { + value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged + } string = String(value); success = true; } break; } + case PLUGIN_WEBFORM_SHOW_CONFIG: { string += strformat(F("Modbus %d"), P183_LINK_ID); success = true; break; } + + case PLUGIN_ONCE_A_SECOND: + { + P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (P183_data == nullptr) { + # ifndef LIMIT_BUILD_SIZE + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config invalid data struct")); + # endif // LIMIT_BUILD_SIZE + return false; + } + + success = P183_data->plugin_once_per_second(event); + break; + } + } return success; diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 85831ee5a4..283c596923 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -93,8 +93,8 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const // Start reading a Modubus holding register. The result will be available later. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, - uint16_t *valuePtr, - ModbusResultState *statePtr) + uint16_t & valuePtr, + ModbusResultState& statePtr) { if (!isInitialized()) { return false; @@ -104,11 +104,11 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, if (request == nullptr) { return false; // Failed to allocate a request structure } - request->_userData = valuePtr; - request->_userState = statePtr; + request->_userData = &valuePtr; + request->_userState = &statePtr; request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; createReadFrame(*request, _modbus_address, address); - (void) _modbus_link->queueTransaction(request); + (void)_modbus_link->queueTransaction(request); return true; } @@ -121,6 +121,25 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t uid) return readModuleHoldingRegister(_modbus_address, address, uid); } +bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, uint16_t uid) +{ + if (!isInitialized()) { + return false; + } + Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + + if (request == nullptr) { + return false; // Failed to allocate a request structure + } + request->_userData = nullptr; + request->_userState = nullptr; + request->_userId = uid; + request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + createReadFrame(*request, _modbus_address, address, size); + (void)_modbus_link->queueTransaction(request); + return true; +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start reading a Modbus holding register from another module on the bus. The result will be available later. // The function returns true if the request was queued. @@ -144,7 +163,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, request->_userState = nullptr; request->_userId = uid; createReadFrame(*request, busAddress, registerAddress); - (void) _modbus_link->queueTransaction(request); + (void)_modbus_link->queueTransaction(request); return true; } @@ -153,20 +172,21 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, uint8_t busAddress, - uint16_t registerAddress) + uint16_t registerAddress, + uint16_t registerCount) { - request._messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + request._messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; request._sendframe[0] = busAddress; request._sendframe[1] = MODBUS_READ_HOLDING_REGISTERS; request._sendframe[2] = highByte(registerAddress); request._sendframe[3] = lowByte(registerAddress); - request._sendframe[4] = 0; - request._sendframe[5] = 1; // Read 1 register + request._sendframe[4] = highByte(registerCount); // Number of registers to read + request._sendframe[5] = lowByte(registerCount); uint16_t crc = CalculateCRC(request._sendframe, 6); - request._sendframe[6] = lowByte(crc); // CRC low byte - request._sendframe[7] = highByte(crc); // CRC high byte - request._sendframe_length = 8; // Size with CRC - request._rcvframe_length = 7; // Expect 8 bytes in response + request._sendframe[6] = lowByte(crc); // CRC low byte + request._sendframe[7] = highByte(crc); // CRC high byte + request._sendframe_length = 8; // Size with CRC + request._rcvframe_length = 5 + (2 * registerCount); // Expected # bytes in response } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -175,7 +195,7 @@ void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState *statePtr) + ModbusResultState& statePtr) { if (!isInitialized()) { return false; @@ -183,7 +203,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); request->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; - request->_userState = statePtr; + request->_userState = &statePtr; request->_sendframe[0] = _modbus_address; request->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; @@ -196,16 +216,15 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_sendframe[7] = highByte(crc); // CRC high byte request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 8; // Expect 8 bytes in response - ////dump_buffer(request->_sendframe, request->_sendframe_length); - (void) _modbus_link->queueTransaction(request); - *statePtr = ModbusResultState::Busy; + (void)_modbus_link->queueTransaction(request); + statePtr = ModbusResultState::Busy; return true; } void ModbusDEVICE_struct::processCommand(void) { - if (!isInitialized()) { + if (isInitialized()) { _modbus_link->processCommand(); } } @@ -233,7 +252,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) # endif // MODBUS_DEBUG if (req->_state == ModbusQueueState::ERROR_OCCURRED) { - sendEvent(req, false, 0, 0, 0); + sendEvent(*req, ModbusResultMessageType::Error, 0, 0, 0); # ifdef MODBUS_DEBUG log += F(" Link error occurred"); # endif // MODBUS_DEBUG @@ -243,22 +262,51 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) { case ModbusTransactionType::READ_HOLDING_REGISTERS: { - if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && (req->_rcvframe[2] == 2)) { - uint16_t crc = CalculateCRC(req->_rcvframe, 5); + int registerCount = req->_sendframe[4] << 8 | req->_sendframe[5]; // Number of registers requested in the original request - if ((req->_rcvframe[5] == lowByte(crc)) && (req->_rcvframe[6] == highByte(crc))) { - int val = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; // Combine high and low byte + // Validate the response: Check slave address, function code and byte count + if ((req->_rcvframe[0] == _modbus_address) && (req->_rcvframe[1] == MODBUS_READ_HOLDING_REGISTERS) && + (registerCount == req->_rcvframe[2] >> 1)) + { + uint16_t crc = CalculateCRC(req->_rcvframe, req->_rcvframe_length - 2); + + if ((req->_rcvframe[req->_rcvframe_length - 2] == lowByte(crc)) && (req->_rcvframe[req->_rcvframe_length -1] == highByte(crc))) { - // Valid response + // Check which return mode is used by the client and return the value accordingly if (req->_userData != nullptr) { - *(static_cast(req->_userData)) = val; - resultState = ModbusResultState::Success; + // Return value through user data pointer specified by the client when queuing the request + // Note: this is only the first register value if multiple registers were read. Returning multiple register values through + // user data pointer is not supported. + *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4];; + } + else if (registerCount == 1) + { + // Return value through an event with one parameter. + sendEvent(*req, ModbusResultMessageType::SingleValue, (req->_rcvframe[3] << 8) | req->_rcvframe[4]); } else { - sendEvent(req, true, val, 0, 0); + // Return value through an event with a ModbusRegisterSet_struct. + + for (int i = 0; i < registerCount && i < 8; i++) { + _registerSet.data[i] = (req->_rcvframe[3 + (i * 2)] << 8) | req->_rcvframe[4 + (i * 2)]; + } + _registerSet.size = registerCount; + sendEvent(*req, ModbusResultMessageType::MultiValue, &_registerSet); } + resultState = ModbusResultState::Success; + } + else { + # ifdef MODBUS_DEBUG + log += F(" CRC check failed"); + # endif // ifdef MODBUS_DEBUG } + + } + else { + # ifdef MODBUS_DEBUG + log += F(" Invalid response format"); + # endif // ifdef MODBUS_DEBUG } break; } @@ -309,20 +357,57 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Send a PLUGIN_TASKTIMER_IN event to the task associated with this device. // This is used by the Modbus link to notify the device of responses received for queued requests. +// This version of the function is used to pass data through parameters in the event. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement *req, int par1, int par2, int par3, int par4) +void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement& req, + ModbusResultMessageType messageType, + int par2, + int par3, + int par4, + int par5, + int par6, + int par7, + int par8) { struct EventStruct TempEvent; - TempEvent.Par1 = par1; + TempEvent.Par1 = static_cast(messageType); TempEvent.Par2 = par2; TempEvent.Par3 = par3; TempEvent.Par4 = par4; - TempEvent.TaskIndex = _taskIndex; // Send to the task associated with this device - TempEvent.idx = req->_userId; // Identifier as specified by the client in the request + TempEvent.Par5 = par5; + TempEvent.Par6 = par6; + TempEvent.Par7 = par7; + TempEvent.Par8 = par8; + TempEvent.TaskIndex = _taskIndex; // Send to the task associated with this device + TempEvent.idx = req._userId; // Identifier as specified by the client in the request + TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM; + String dummy; + + PluginCall(PLUGIN_TASKTIMER_IN, &TempEvent, dummy); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Send a PLUGIN_TASKTIMER_IN event to the task associated with this device. +// This is used by the Modbus link to notify the device of responses received for queued requests. +// This version of the function is used to pass multiple register values in a ModbusRegisterSet_struct. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement& req, + ModbusResultMessageType messageType, + ModbusRegisterSet_struct *registerSet) +{ + struct EventStruct TempEvent; + + TempEvent.Par1 = static_cast(messageType); + TempEvent.Data = reinterpret_cast(registerSet); + TempEvent.TaskIndex = _taskIndex; // Send to the task associated with this device + TempEvent.idx = req._userId; // Identifier as specified by the client in the request TempEvent.Source = EventValueSource::Enum::VALUE_SOURCE_SYSTEM; String dummy; + // Note: The ModbusRegisterSet_struct is passed as a pointer in the Data field of the event to avoid a deep copy. + // This assumes PluginCall will secure that the pointer will remain valid when the event is processed. + // And the data is consumed when the function returns (No multi-threading) PluginCall(PLUGIN_TASKTIMER_IN, &TempEvent, dummy); } diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 8f7ca73fa1..b18fd9a72e 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -9,9 +9,11 @@ # include "Modbus_link.h" # ifndef MODBUS_BROADCAST_ADDRESS -# define MODBUS_BROADCAST_ADDRESS 0xFE // Address used for boardcast messages +# define MODBUS_BROADCAST_ADDRESS 0xFE // Address used for boardcast messages # endif // ifndef MODBUS_BROADCAST_ADDRESS +# define MODBUS_MAX_REGISTERS_PER_TRANSACTION 125 // Modbus protocol allows up to 125 registers in a single read/write transaction + // States for the Modbus queue elements enum class ModbusResultState { Busy = 0, // Transaction is not completed @@ -20,12 +22,26 @@ enum class ModbusResultState { }; +// Type of callback message sent to the device when a response is received for a queued request +enum class ModbusResultMessageType { + Error = 0, // Request completed with an error, no data available + SingleValue = 1, // Transaction returned a value as a parameter in the callback + MultiValue = 2, // Transaction returned multiple values in a structure + +}; + +struct ModbusRegisterSet_struct +{ + uint16_t data[MODBUS_MAX_REGISTERS_PER_TRANSACTION]; // Modbus allows up to 125 registers in a single read/write transaction + uint16_t size; // Number of valid registers in the data array + +}; + // ModbusDEVICE structure representing a MODBUS Device // This is a single device that may share it's Modbus link with multiple other devices. // It uses the ModbusLINKManager to find the ModbusLINK object that handles the data transport. // It is the ModbusDEVICE that builds the Modbus request frames and parses the responses. struct ModbusDEVICE_struct { -private: public: @@ -51,8 +67,8 @@ struct ModbusDEVICE_struct { // The function returns true if the request was queued. // The state variable will signal the processing state of the request. bool readHoldingRegister(uint16_t address, - uint16_t *valueptr, - ModbusResultState *stateptr); + uint16_t & valueptr, + ModbusResultState& stateptr); // Start reading a Modbus holding register with reslt returned through event PLUGIN_TASKTIMER_IN // The function returns true if the request was queued. @@ -60,11 +76,15 @@ struct ModbusDEVICE_struct { bool readHoldingRegister(uint16_t address, uint16_t uid); + bool readHoldingRegisters(uint16_t address, + uint16_t size, + uint16_t uid); + // Start writing a single Modbus register. // The function returns true if the request was queued. bool writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState *stateptr); + ModbusResultState& stateptr); // Start reading a Modbus holding register from another module. The result will be available later. // The function returns true if the request was queued. @@ -78,21 +98,31 @@ struct ModbusDEVICE_struct { private: - uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; - ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object - uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device - uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests - taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device - - void sendEvent(Modbus_RequestQueueElement *req, - int par1, - int par2, - int par3, - int par4); + uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; + ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object + uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device + uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests + taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device + ModbusRegisterSet_struct _registerSet = {}; // Structure to hold multiple register values for multi-value responses + + void sendEvent(Modbus_RequestQueueElement& req, + ModbusResultMessageType messageType, + int par2 = 0, + int par3 = 0, + int par4 = 0, + int par5 = 0, + int par6 = 0, + int par7 = 0, + int par8 = 0); + + void sendEvent(Modbus_RequestQueueElement& req, + ModbusResultMessageType messageType, + ModbusRegisterSet_struct *registerSet); void createReadFrame(Modbus_RequestQueueElement& request, uint8_t busAddress, - uint16_t registerAddress); + uint16_t registerAddress, + uint16_t registerCount = 1); static uint16_t CalculateCRC(uint8_t *buf, int len); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 73b80876d7..2040d29d3f 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -172,16 +172,19 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); return 0; } + if (transaction == nullptr) { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); return 0; } - if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER ) { + + if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER) { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, receive buffer too large")); return 0; } - + # ifdef MODBUS_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus: Link, Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); @@ -315,19 +318,25 @@ void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = strformat(F("Modbus: [ID=%u, Device=%p, State="), el->_id, el->_device); log += toString(el->_state); - log += F(", TX="); + log += F(", TX=("); for (int i = 0; i < el->_sendframe_length; i++) { log += String(el->_sendframe[i], HEX); - log += F(","); + + if (i < el->_sendframe_length - 1) { + log += F(","); + } } - log += F(", RX="); + log += F("), RX=("); for (int i = 0; i < el->_rcvframe_length; i++) { log += String(el->_rcvframe[i], HEX); - log += F(","); + + if (i < el->_rcvframe_length - 1) { + log += F(","); + } } - log += F("] "); + log += F(")] "); addLogMove(LOG_LEVEL_INFO, log); } # endif // MODBUS_DEBUG diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index f35b3c8695..2d23bfc4b6 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -1,4 +1,5 @@ #include "../PluginStructs/P183_data_struct.h" +#include "P183_data_struct.h" #ifdef USES_P183 @@ -6,6 +7,7 @@ // ####################################################################################################### // ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### +////# define P183_DEBUG # ifdef BUILD_NO_DEBUG # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -13,6 +15,7 @@ // Actions for PLUGIN_TASKTIMER_IN event to distinguish between regular read results and scan sequences # define ACTION_DUMP_RANGE 0xFFFF # define ACTION_SCAN_BUS 0xFFFE +# define ACTION_READ_CACHE 0xFFFD /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Constructor of the plugin data structure. Initializes the data members to default values. @@ -114,11 +117,26 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) scan_next_module(); return true; } + else if (event->idx == ACTION_READ_CACHE) { + // This is the result of the regular cache read triggered in plugin_once_per_second. Update the user variables with the cache values. + + ModbusRegisterSet_struct *registerSet = reinterpret_cast(event->Data); + int count = registerSet->size; + + if (count > _cacheSize) { + count = _cacheSize; // Prevent overflow if the device returns more registers than the cache can hold + } + + for (int i = 0; i < count; i++) { + _RegisterCache[i] = registerSet->data[i]; + } + return true; + } else { int outputIndex = event->idx; if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { - # ifdef LIMIT_BUILD_SIZE + # ifndef LIMIT_BUILD_SIZE addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); # endif // LIMIT_BUILD_SIZE return false; @@ -132,12 +150,32 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) } } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Triggered once per second. Fetch the cache values +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool P183_data_struct::plugin_once_per_second(EventStruct *event) +{ + if (_modbusDevice == nullptr) { + return false; + } + + if (P183_CACHE_SIZE == 0) { + return true; // Cache not used, nothing to do + } + + // Queue a read request for the cache values. The result will be processed in the task timer event. + _cacheStart = P183_CACHE_START; + _cacheSize = P183_CACHE_SIZE; + _modbusDevice->readHoldingRegisters(_cacheStart, _cacheSize, ACTION_READ_CACHE); + return true; +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Start iterating over a register range of a Modbus device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void P183_data_struct::scan_device(uint8_t node_id, uint8_t start_reg, uint8_t end_reg) +void P183_data_struct::scan_device(uint8_t node_id, uint16_t start_reg, uint16_t end_reg) { - uint16_t value = 0; + uint16_t value = 0; addLogMove(LOG_LEVEL_INFO, F("Modbus: dumping module registers")); @@ -213,23 +251,37 @@ void P183_data_struct::scan_next_module() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t P183_data_struct::readRegisterWait(uint16_t address) { uint16_t value = 0; + ulong startTime = millis(); ModbusResultState state = ModbusResultState::Busy; if (_modbusDevice == nullptr) { return 0; } - _modbusDevice->readHoldingRegister(address, &value, &state); // Queue the read action + _modbusDevice->readHoldingRegister(address, value, state); // Queue the read action while (state == ModbusResultState::Busy) { delay(50); - _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + if (millis() - startTime > P183_MODBUS_TIMEOUT) { + state = ModbusResultState::Error; // Timeout, exit the loop with an error state + } } return value; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint16_t P183_data_struct::readRegisterCache(uint16_t address) +{ + if ( (address < _cacheStart) || (address >= _cacheStart + _cacheSize)) { + return 0; + } + else { + return _RegisterCache[address - _cacheStart]; + } +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::writeRegister(uint16_t address, uint16_t value) { @@ -237,7 +289,7 @@ void P183_data_struct::writeRegister(uint16_t address, uint16_t value) return; } - _modbusDevice->writeSingleRegister(address, value, &_lastActionState); // Queue the action (and for now forget it) + _modbusDevice->writeSingleRegister(address, value, _lastActionState); // Queue the action (and for now forget it) } #endif // ifdef USES_P183 diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 666bc15690..2dbb8c3eb1 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -21,6 +21,8 @@ // PCONFIG(6) is the Modbus register address for value 3 // PCONFIG(7) is the Modbus register address for value 4 // Use P183_ADDRESS(x) to access the PCONFIG value for value x +// PCONFIG_LONG(0) is the number of Modbus registers to keep in cache +// PCONFIG_LONG(1) is the starting register address for the cache # define P183_DEV_ID PCONFIG(0) # define P183_DEV_ID_LABEL PCONFIG_LABEL(0) # define P183_LINK_ID PCONFIG(1) @@ -30,10 +32,17 @@ # define P183_ADDRESS(x) PCONFIG(4 + x) # define P183_ADDRESS_LABEL(x) concat(F("addr"), x) +# define P183_CACHE_SIZE PCONFIG_LONG(0) +# define P183_CACHE_SIZE_LABEL F("P183sz") +# define P183_CACHE_START PCONFIG_LONG(1) +# define P183_CACHE_START_LABEL F("P183st") + # define P183_DEV_ID_DFLT 1 -# define P183_MODBUS_TIMEOUT 1000 // milliseconds +# define P183_MODBUS_TIMEOUT 1000 // milliseconds # define P183_MAX_MODBUS_NODES 247 -# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address +# define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address +# define P183_CACHE_SIZE_MAX 100 // Maximum number of registers to keep in cache +# define P183_CACHE_START_MAX 65535 // Maximum starting address for cache // The default set of single-value VType options // constexpr uint8_t P183_START_VTYPE = 0; @@ -49,25 +58,32 @@ struct P183_data_struct : public PluginTaskData_base { void plugin_exit(); bool plugin_read(struct EventStruct *event); bool plugin_task_timer(struct EventStruct *event); - void scan_device(uint8_t node_id, - uint8_t start_reg, - uint8_t end_reg); + bool plugin_once_per_second(struct EventStruct *event); + void scan_device(uint8_t node_id, + uint16_t start_reg, + uint16_t end_reg); void scan_modbus(); uint16_t readRegisterWait(uint16_t address); + uint16_t readRegisterCache(uint16_t address); + void writeRegister(uint16_t address, uint16_t value); private: - taskIndex_t _taskIndex = INVALID_TASK_INDEX; - struct ModbusDEVICE_struct *_modbusDevice = nullptr; - ModbusResultState _lastActionState = ModbusResultState::Busy; - uint16_t _lastAddress = 0; - uint16_t _endAddress = 0; - bool _scanning = false; + taskIndex_t _taskIndex = INVALID_TASK_INDEX; + struct ModbusDEVICE_struct *_modbusDevice = nullptr; + uint16_t _RegisterCache[P183_CACHE_SIZE_MAX] = { 0 }; + uint16_t _cacheStart = 0; + int _cacheSize = 0; + ModbusResultState _lastActionState = ModbusResultState::Busy; + uint16_t _lastAddress = 0; + uint16_t _endAddress = 0; + bool _scanning = false; void scan_next_address(); void scan_next_module(); + }; #endif // ifdef USES_P183 From e5583dac0b69e53f69c6438051e224e10385bcc7 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 28 May 2026 22:35:50 +0200 Subject: [PATCH 32/48] P183 Unfinished rework --- src/_P183_modbus.ino | 9 +- src/src/Helpers/Modbus_device.cpp | 102 +++++++++++--------- src/src/Helpers/Modbus_device.h | 8 +- src/src/Helpers/Modbus_link.cpp | 105 +++++++-------------- src/src/Helpers/Modbus_link.h | 73 ++++++++------ src/src/PluginStructs/P183_data_struct.cpp | 23 +++-- 6 files changed, 158 insertions(+), 162 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 288f7421e3..ed8469f7d1 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -100,10 +100,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { - addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); - addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, P183_MAX_MODBUS_NODES); - addFormNumericBox(F("Cache size"), P183_CACHE_SIZE_LABEL, static_cast(P183_CACHE_SIZE), 0, P183_CACHE_SIZE_MAX); - addFormNumericBox(F("Cache start address"), P183_CACHE_START_LABEL, static_cast(P183_CACHE_START), 0, P183_CACHE_START_MAX); + addFormNumericBox(F("Modbus Link"), P183_LINK_ID_LABEL, P183_LINK_ID, 0, 3); + addFormNumericBox(F("Modbus Device Address"), P183_DEV_ID_LABEL, P183_DEV_ID, 1, P183_MAX_MODBUS_NODES); + addFormNumericBox(F("Cache size"), P183_CACHE_SIZE_LABEL, P183_CACHE_SIZE, 0, P183_CACHE_SIZE_MAX); + addFormNumericBox(F("Cache start address"), P183_CACHE_START_LABEL, P183_CACHE_START, 0, P183_CACHE_START_MAX); success = true; break; @@ -248,6 +248,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } + // Event from Modbus device for a response received on a queued request case PLUGIN_TASKTIMER_IN: { P183_data_struct *P183_data = static_cast(getPluginTaskData(event->TaskIndex)); diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 283c596923..cc5b68de5a 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -17,6 +17,7 @@ # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG +// Modbus function codes, see Modbus specification const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; @@ -51,7 +52,7 @@ void ModbusDEVICE_struct::reset() { } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Initialization connected to an existing link. +// Initializae the Modbus device and connect it to the given Modbus link /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t taskIndex) { @@ -70,6 +71,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t tas return success; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Checker for device class initialization status /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::isInitialized() const { @@ -90,30 +92,32 @@ uint16_t ModbusDEVICE_struct::getModbusTimeout() const } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Start reading a Modubus holding register. The result will be available later. +// Start reading a Modubus holding register. The result will be available later through the provided pointers. +// The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, - uint16_t & valuePtr, - ModbusResultState& statePtr) +bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, + uint16_t *valuePtr, + ModbusResultState*statePtr) { if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + ModbusTransactionType::READ_HOLDING_REGISTERS, + 0, + valuePtr, + statePtr); if (request == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - request->_userData = &valuePtr; - request->_userState = &statePtr; - request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; createReadFrame(*request, _modbus_address, address); - (void)_modbus_link->queueTransaction(request); + (void)_modbus_link->queueTransaction(request); // Transaction is now owned by the Modbus link return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Start reading a Modubus holding register. The result will be available later through an task event. +// Start reading a Modubus holding register. The result will be available later through a task event. // The function returns true if the request was queued. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t uid) @@ -121,49 +125,53 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, uint16_t uid) return readModuleHoldingRegister(_modbus_address, address, uid); } -bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, uint16_t uid) +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Start reading a Modbus holding register from another module on the bus. The result will be available later. +// The function returns true if the request was queued. +// Note: This function accesses registers from other devices on the same Modbus bus. +// This should be used with care to prevent conflicts. This is beyond the intended scope of the Modbus device class. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, + uint16_t registerAddress, + uint16_t uid) { if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + ModbusTransactionType::READ_HOLDING_REGISTERS, + uid, + nullptr, + nullptr); if (request == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - request->_userData = nullptr; - request->_userState = nullptr; - request->_userId = uid; - request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - createReadFrame(*request, _modbus_address, address, size); - (void)_modbus_link->queueTransaction(request); + createReadFrame(*request, busAddress, registerAddress); + (void)_modbus_link->queueTransaction(request); // Transaction is now owned by the Modbus link return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Start reading a Modbus holding register from another module on the bus. The result will be available later. +// Start reading multiple Modubus holding registers. The result will be available later through a task event. // The function returns true if the request was queued. -// Note: This function accesses registers from other devices on the same Modbus bus. -// This should be used with care to prevent conflicts. This is beyond the intended scope of the Modbus device class. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, - uint16_t registerAddress, - uint16_t uid) +bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, uint16_t uid) { if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); + Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + ModbusTransactionType::READ_HOLDING_REGISTERS, + uid, + nullptr, + nullptr); if (request == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - request->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - request->_userData = nullptr; - request->_userState = nullptr; - request->_userId = uid; - createReadFrame(*request, busAddress, registerAddress); - (void)_modbus_link->queueTransaction(request); + createReadFrame(*request, _modbus_address, address, size); + (void)_modbus_link->queueTransaction(request); // Transaction is now owned by the Modbus link return true; } @@ -195,15 +203,16 @@ void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState& statePtr) + ModbusResultState *statePtr) { if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = _modbus_link->newTransaction(this); - - request->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; - request->_userState = &statePtr; + Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + ModbusTransactionType::WRITE_SINGLE_REGISTER, + 0, + nullptr, + statePtr); request->_sendframe[0] = _modbus_address; request->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; @@ -217,11 +226,15 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, request->_sendframe_length = 8; // Size with CRC request->_rcvframe_length = 8; // Expect 8 bytes in response (void)_modbus_link->queueTransaction(request); - statePtr = ModbusResultState::Busy; + *statePtr = ModbusResultState::Busy; return true; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Process any pending commands for this device. +// Should only be used when polling for an immediate response, not for regular asynchronous requests. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::processCommand(void) { if (isInitialized()) { @@ -270,14 +283,14 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) { uint16_t crc = CalculateCRC(req->_rcvframe, req->_rcvframe_length - 2); - if ((req->_rcvframe[req->_rcvframe_length - 2] == lowByte(crc)) && (req->_rcvframe[req->_rcvframe_length -1] == highByte(crc))) { + if ((req->_rcvframe[req->_rcvframe_length - 2] == lowByte(crc)) && (req->_rcvframe[req->_rcvframe_length - 1] == highByte(crc))) { // Check which return mode is used by the client and return the value accordingly if (req->_userData != nullptr) { // Return value through user data pointer specified by the client when queuing the request // Note: this is only the first register value if multiple registers were read. Returning multiple register values through // user data pointer is not supported. - *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4];; + *(static_cast(req->_userData)) = (req->_rcvframe[3] << 8) | req->_rcvframe[4]; } else if (registerCount == 1) { @@ -346,7 +359,6 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) if (req->_userState != nullptr) { *(static_cast(req->_userState)) = resultState; } - _modbus_link->freeTransaction(req); // Free the transaction to prevent memory leaks. # ifdef MODBUS_DEBUG log += F(", Result = "); log += (resultState == ModbusResultState::Success) ? F("SUCCESS") : F("ERROR"); @@ -406,7 +418,7 @@ void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement& req, String dummy; // Note: The ModbusRegisterSet_struct is passed as a pointer in the Data field of the event to avoid a deep copy. - // This assumes PluginCall will secure that the pointer will remain valid when the event is processed. + // This assumes PluginCall will secure that the pointer will remain valid when the event is processed. // And the data is consumed when the function returns (No multi-threading) PluginCall(PLUGIN_TASKTIMER_IN, &TempEvent, dummy); } diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index b18fd9a72e..f6eda7354a 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -66,9 +66,9 @@ struct ModbusDEVICE_struct { // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. // The state variable will signal the processing state of the request. - bool readHoldingRegister(uint16_t address, - uint16_t & valueptr, - ModbusResultState& stateptr); + bool readHoldingRegister(uint16_t address, + uint16_t *valueptr, + ModbusResultState*stateptr); // Start reading a Modbus holding register with reslt returned through event PLUGIN_TASKTIMER_IN // The function returns true if the request was queued. @@ -84,7 +84,7 @@ struct ModbusDEVICE_struct { // The function returns true if the request was queued. bool writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState& stateptr); + ModbusResultState * stateptr); // Start reading a Modbus holding register from another module. The result will be available later. // The function returns true if the request was queued. diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 2040d29d3f..2679ec088f 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -41,6 +41,7 @@ void ModbusLINK_struct::reset() { if ((*it)->_device != nullptr) { (*it)->_device->linkCallback(*it); // Notify the device that the request finished with an error } + delete (*it); // destroy the queue element it = _requestQueue.erase(it); } } @@ -104,48 +105,6 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, return true; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Provide a new transaction structure that can be used to build a Modbus request and queue it at this link -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -Modbus_RequestQueueElement * ModbusLINK_struct::newTransaction(struct ModbusDEVICE_struct *device) -{ - if (!isInitialized()) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to create transaction on uninitialized link")); - return nullptr; - } - - Modbus_RequestQueueElement *req = new (std::nothrow) Modbus_RequestQueueElement(); - - if (req != nullptr) { - req->_id = ++(_queueID); // Assign a unique ID to the transaction - req->_device = device; // The Modbus device making the request - req->_timeout = _modbus_timeout; // Default timeout value - req->_state = ModbusQueueState::NOT_QUEUED; // Initial state - return req; - } - else { - return nullptr; - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Free a previously allocated transaction structure -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::freeTransaction(Modbus_RequestQueueElement *transaction) { - if (transaction != nullptr) { - if (transaction->_state == ModbusQueueState::NOT_QUEUED) { // Not on the queue, can be directly deleted - delete transaction; - } else { - transaction->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be freed by queue processing - } - return true; - } - else { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to free null transaction")); - return false; - } -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Free all queued transactions for the given device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -166,20 +125,24 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Queue a Modbus transaction. The request is appended to the queue and assigned a unique identifier. // The client can use this identifier to retrieve the response later. +// This function will transfer ownership of the transaction to the Modbus link, which is responsible for freeing +// the associated resources when the transaction is completed. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transaction) { - if (!isInitialized()) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); + if (transaction == nullptr) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); return 0; } - if (transaction == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); + delete transaction; // Free the transaction since it won't be queued return 0; } if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER) { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, receive buffer too large")); + delete transaction; // Free the transaction since it won't be queued return 0; } @@ -190,9 +153,14 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac strformat(F("Modbus: Link, Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); } # endif // MODBUS_DEBUG - transaction->_state = ModbusQueueState::QUEUED; // Initial state - _requestQueue.push_back(transaction); // Append the request to the queue - processCommand(); // Trigger processing of the command queue + transaction->_id = ++(_queueID); // Assign a unique ID to the transaction + transaction->_timeout = _modbus_timeout; // Default timeout value + transaction->_state = ModbusQueueState::QUEUED; // Transaction is now queued + _requestQueue.push_back(transaction); // Append the request to the queue + + if (!_processing) { // Prevent reentrancy issues + processCommand(); // Trigger processing of the command queue + } return transaction->_id; } @@ -202,6 +170,8 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processCommand() { + _processing = true; // Set processing flag to prevent reentrancy issues + if (!isInitialized() || (_requestQueue.empty())) { return; // Serial port not initialized or queue is empty, nothing to process } @@ -240,47 +210,44 @@ void ModbusLINK_struct::processCommand() if (_easySerial->available() >= (*it)->_rcvframe_length) { _easySerial->readBytes((*it)->_rcvframe, (*it)->_rcvframe_length); (*it)->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received - - if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback((*it)); // Notify the device that a response was received - } } else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { // Timeout expired (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + } + else { + // Still waiting + busy = true; // Only process one request at a time + } + if (!busy) { // We received a response or an error occurred, process the result if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback((*it)); // Notify the device that a response was received + (*it)->_device->linkCallback((*it)); // Notify the device that a response was received } - else {} - it++; + + delete (*it); // destroy the queue element + it = _requestQueue.erase(it); // Remove it from the list } else { - // Still waiting - busy = true; // Only process one request at a time + it++; // Move to the next transaction in the queue } break; } - case ModbusQueueState::ERROR_OCCURRED: + case ModbusQueueState::NOT_QUEUED: // This state should not be on the queue { - it++; - break; + it++; // Not queued yet, move to the next transaction in the queue } - - case ModbusQueueState::READY_FOR_DESTROY: + default: { delete (*it); // destroy the queue element it = _requestQueue.erase(it); // Remove it from the list break; } + } // switch + } // next iterarion - default: - it++; - break; - } // switch - } // next iterarion - + _processing = false; // Clear processing flag to allow new processing cycles return; } diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 7b57a83ace..02171a5eed 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -41,18 +41,29 @@ typedef enum class ModbusTransactionType { // This structure represents a single Modbus request and its associated response. struct Modbus_RequestQueueElement { Modbus_RequestQueueElement() = default; - - ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message - void *_userData = nullptr; // Pointer to user (device) data - void *_userState = nullptr; // Pointer to user (device) defined state - uint16_t _userId = 0; // Client defined identifier for this transaction, + Modbus_RequestQueueElement(ModbusDEVICE_struct *device, + ModbusTransactionType_t messageType, + uint16_t userId, + void *userData, + void *userState) + { + _device = device; + _messageType = messageType; + _userId = userId; + _userData = userData; + _userState = userState; + } + + ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message + void *_userData = nullptr; // Pointer to user (device) data + void *_userState = nullptr; // Pointer to user (device) defined state + uint16_t _userId = 0; // Client defined identifier for this transaction, // can be used to match responses to requests - uint16_t _id = 0; // ID of the request - struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the + uint16_t _id = 0; // Unique ID of the request (for debugging) + struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the // action uint16_t _sendframe_length = 0; // Length of the request frame uint16_t _rcvframe_length = 0; // Expected length of the response frame - // expected enum ModbusQueueState _state = ModbusQueueState::NOT_QUEUED; // State of the request exchange uint16_t _timeout = 0; // Specified timeout value for the request unsigned long _startTime = 0; // Time the request was issued @@ -84,41 +95,41 @@ struct ModbusLINK_struct { int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } - - Modbus_RequestQueueElement* newTransaction(struct ModbusDEVICE_struct *device); - bool freeTransaction(Modbus_RequestQueueElement *transaction); - void freeTransactions(struct ModbusDEVICE_struct *device); - uint16_t queueTransaction(Modbus_RequestQueueElement *transaction); - void processCommand(); + bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } - int16_t getBaudrate(void) const; + void freeTransactions(struct ModbusDEVICE_struct *device); + uint16_t queueTransaction(Modbus_RequestQueueElement *transaction); + void processCommand(); - int16_t getSerialRX(void) const; + int16_t getBaudrate(void) const; - int16_t getSerialTX(void) const; + int16_t getSerialRX(void) const; - int8_t getDerePin(void) const; + int16_t getSerialTX(void) const; - bool getCollisionDetect(void) const; + int8_t getDerePin(void) const; + bool getCollisionDetect(void) const; private: static void dumpQueueElement(Modbus_RequestQueueElement *el); static void dumpState(ModbusQueueState_t state); - ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object - Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process - uint16_t _queueID = 0; // ID for the last request queued - uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds - uint32_t _reads_pass = 0; // TODO: statistics - uint32_t _reads_crc_failed = 0; // TODO: statistics - uint32_t _reads_nodata = 0; // TODO: statistics - uint8_t _dere_pin = 0; // Pin for RS485 direction control - bool _collision_detect = false; // Flag to indicate if collision detection is enabled - bool _initialized = false; - uint8_t _last_error = 0; + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object + Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process + uint16_t _queueID = 0; // ID for the last request queued + uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds + + uint8_t _dere_pin = 0; // Pin for RS485 direction control + bool _collision_detect = false; // Flag to indicate if collision detection is enabled + bool _initialized = false; + + ////uint32_t _reads_pass = 0; // TODO: statistics + ////uint32_t _reads_crc_failed = 0; // TODO: statistics + ////uint32_t _reads_nodata = 0; // TODO: statistics + ////uint8_t _last_error = 0; // TODO: statistics, last error code received from Modbus device + bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to prevent reentrancy issues }; diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 2d23bfc4b6..e9ae340aa1 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -120,6 +120,10 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) else if (event->idx == ACTION_READ_CACHE) { // This is the result of the regular cache read triggered in plugin_once_per_second. Update the user variables with the cache values. + if (event->Data == nullptr) { + addLogMove(LOG_LEVEL_ERROR, F("P183: No data received for cache read")); + return false; + } ModbusRegisterSet_struct *registerSet = reinterpret_cast(event->Data); int count = registerSet->size; @@ -128,7 +132,7 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) } for (int i = 0; i < count; i++) { - _RegisterCache[i] = registerSet->data[i]; + _RegisterCache[i] = registerSet->data[i]; // Copy the received values to the cache } return true; } @@ -196,7 +200,7 @@ void P183_data_struct::scan_device(uint8_t node_id, uint16_t start_reg, uint16_t } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// REad the next holding register from the Modbus device +// Read the next holding register from the Modbus device /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void P183_data_struct::scan_next_address() { @@ -247,23 +251,24 @@ void P183_data_struct::scan_next_module() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Read a Modbus register from the device. Wait untial the data is available -// Warning: this may take time as we waith for the Modbus message to be exchanged +// Warning: this may take time as we wait for the Modbus message to be exchanged /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t P183_data_struct::readRegisterWait(uint16_t address) { uint16_t value = 0; - ulong startTime = millis(); + uint32_t startTime = millis(); ModbusResultState state = ModbusResultState::Busy; if (_modbusDevice == nullptr) { return 0; } - _modbusDevice->readHoldingRegister(address, value, state); // Queue the read action + _modbusDevice->readHoldingRegister(address, &value, &state); // Queue the read action while (state == ModbusResultState::Busy) { delay(50); - _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue - if (millis() - startTime > P183_MODBUS_TIMEOUT) { + _modbusDevice->processCommand(); // Trigger Modbus facilities to process the Modbus queue + + if (timePassedSince(startTime) > P183_MODBUS_TIMEOUT) { state = ModbusResultState::Error; // Timeout, exit the loop with an error state } } @@ -274,7 +279,7 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t P183_data_struct::readRegisterCache(uint16_t address) { - if ( (address < _cacheStart) || (address >= _cacheStart + _cacheSize)) { + if ((address < _cacheStart) || (address >= _cacheStart + _cacheSize)) { return 0; } else { @@ -289,7 +294,7 @@ void P183_data_struct::writeRegister(uint16_t address, uint16_t value) return; } - _modbusDevice->writeSingleRegister(address, value, _lastActionState); // Queue the action (and for now forget it) + _modbusDevice->writeSingleRegister(address, value, &_lastActionState); // Queue the action (and for now forget it) } #endif // ifdef USES_P183 From c117ebd36297d6f6060f1bef6220c07d69a8d333 Mon Sep 17 00:00:00 2001 From: TD-er Date: Fri, 5 Jun 2026 17:50:31 +0200 Subject: [PATCH 33/48] [P183] Fix failing build Modbus RTU on ESP8266 builds --- src/src/PluginStructs/P183_data_struct.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index e9ae340aa1..da75830d09 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -1,6 +1,4 @@ #include "../PluginStructs/P183_data_struct.h" -#include "P183_data_struct.h" - #ifdef USES_P183 From 298517c122483fd9b62ebdcfda7f26df57640fcb Mon Sep 17 00:00:00 2001 From: TD-er Date: Fri, 5 Jun 2026 22:50:09 +0200 Subject: [PATCH 34/48] [P183] Disable Modbus RTU plugin for now for Neopixel builds due to size --- src/src/CustomBuild/define_plugin_sets.h | 3 ++- src/src/PluginStructs/P183_data_struct.cpp | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index c9a1a79f73..c0f088a169 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -2560,7 +2560,8 @@ To create/register a plugin, you have to : #define USES_P180 // Generic - I2C Generic #endif #if !defined(USES_P183) && defined(ESP32) - #define USES_P183 // Communication - Modbus RTU +// #define USES_P183 // Communication - Modbus RTU +// Disabled for now due to build size #endif diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index da75830d09..635432d867 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -6,9 +6,9 @@ // ############## Data structure for plugin 183: Modbus RTU generic sensor interface ############### // ####################################################################################################### ////# define P183_DEBUG -# ifdef BUILD_NO_DEBUG +# ifndef PLUGIN_BUILD_MAX_ESP32 # undef P183_DEBUG // Debugging switched off -# endif // ifdef BUILD_NO_DEBUG +# endif // Actions for PLUGIN_TASKTIMER_IN event to distinguish between regular read results and scan sequences # define ACTION_DUMP_RANGE 0xFFFF From c723180054f4f98561f3c31c03de6a50183dd5af Mon Sep 17 00:00:00 2001 From: TD-er Date: Sat, 6 Jun 2026 11:36:52 +0200 Subject: [PATCH 35/48] [Modbus RTU] Disable P183 for DisplayB builds due to build size --- src/src/CustomBuild/define_plugin_sets.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index c0f088a169..a06ec0ce95 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -2093,6 +2093,9 @@ To create/register a plugin, you have to : #ifndef PLUGIN_DESCR #define PLUGIN_DESCR "Display B" #endif + #ifndef BUILD_NO_DEBUG + #define BUILD_NO_DEBUG + #endif #if !defined(LIMIT_BUILD_SIZE) && (defined(ESP8266) || !(ESP_IDF_VERSION_MAJOR > 3)) #ifndef PLUGIN_BUILD_MAX_ESP32 #define LIMIT_BUILD_SIZE // Reduce buildsize (on ESP8266 / pre-IDF4.x) to fit in all Display plugins @@ -2198,7 +2201,8 @@ To create/register a plugin, you have to : #define USES_P180 // Generic - I2C Generic #endif #if !defined(USES_P183) && defined(ESP32) - #define USES_P183 // Communication - Modbus RTU +// #define USES_P183 // Communication - Modbus RTU + // TODO TD-er: Disabled due to build size #endif #endif // ifdef PLUGIN_DISPLAY_B_COLLECTION From 90c3a19872a125c65c886635d5f6560b46664792 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:10:07 +0200 Subject: [PATCH 36/48] P183: Rework stage 1 --- misc/modbusFacility/Modbus_class.puml | 2 +- src/src/Helpers/Modbus_device.cpp | 20 +- src/src/Helpers/Modbus_device.h | 11 +- src/src/Helpers/Modbus_link.cpp | 38 ++- src/src/Helpers/Modbus_link.h | 12 +- src/src/Helpers/Modbus_mgr.cpp | 420 +++++++++----------------- src/src/Helpers/Modbus_mgr.h | 21 +- 7 files changed, 201 insertions(+), 323 deletions(-) diff --git a/misc/modbusFacility/Modbus_class.puml b/misc/modbusFacility/Modbus_class.puml index 9e26150018..e4b9b73575 100644 --- a/misc/modbusFacility/Modbus_class.puml +++ b/misc/modbusFacility/Modbus_class.puml @@ -33,7 +33,7 @@ plugin_struct *-right- Modbus_device Modbus_device "*" -down-> "1" Modbus_link Modbus_link "1" -right-> "1" Serial_port Modbus_mgr "1" *-- "*" Modbus_link -Modbus_mgr "1" -- "*" Modbus_device +Modbus_mgr "1" <-- "*" Modbus_device Modbus_link *-- "1" Queue Queue o-left- "*" Transaction Modbus_device "1" --> "*" Transaction diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index cc5b68de5a..34998c95fd 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -60,6 +60,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t tas _modbus_address = slaveAddress; _taskIndex = taskIndex; + _linkId = linkId; # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -102,7 +103,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, ModbusTransactionType::READ_HOLDING_REGISTERS, 0, valuePtr, @@ -138,7 +139,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, ModbusTransactionType::READ_HOLDING_REGISTERS, uid, nullptr, @@ -161,7 +162,7 @@ bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, ModbusTransactionType::READ_HOLDING_REGISTERS, uid, nullptr, @@ -178,7 +179,7 @@ bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Construct a Modbus read holding registers message /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::createReadFrame(Modbus_RequestQueueElement& request, +void ModbusDEVICE_struct::createReadFrame(Modbus_Transaction& request, uint8_t busAddress, uint16_t registerAddress, uint16_t registerCount) @@ -208,7 +209,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, if (!isInitialized()) { return false; } - Modbus_RequestQueueElement *request = new (std::nothrow) Modbus_RequestQueueElement(this, + Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, ModbusTransactionType::WRITE_SINGLE_REGISTER, 0, nullptr, @@ -247,7 +248,7 @@ void ModbusDEVICE_struct::processCommand(void) // Note that the response might be an invalid response or a timeout // The queueID identifies the request. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) +void ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) { ModbusResultState resultState = ModbusResultState::Error; // Default to error unless proven otherwise @@ -257,8 +258,9 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) } # ifdef MODBUS_DEBUG - String log = strformat(F("Modbus: Device callback, device= %d, Request= %d, Message= %d"), + String log = strformat(F("Modbus: Device callback, device= %d, link= %d, Request= %d, Message= %d"), _deviceID, + _linkId, req->_id, static_cast(req->_messageType) ); @@ -371,7 +373,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_RequestQueueElement *req) // This is used by the Modbus link to notify the device of responses received for queued requests. // This version of the function is used to pass data through parameters in the event. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement& req, +void ModbusDEVICE_struct::sendEvent(Modbus_Transaction& req, ModbusResultMessageType messageType, int par2, int par3, @@ -404,7 +406,7 @@ void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement& req, // This is used by the Modbus link to notify the device of responses received for queued requests. // This version of the function is used to pass multiple register values in a ModbusRegisterSet_struct. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::sendEvent(Modbus_RequestQueueElement& req, +void ModbusDEVICE_struct::sendEvent(Modbus_Transaction& req, ModbusResultMessageType messageType, ModbusRegisterSet_struct *registerSet) { diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index f6eda7354a..4ffbcf059d 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -61,7 +61,7 @@ struct ModbusDEVICE_struct { uint16_t getModbusTimeout() const; - void linkCallback(Modbus_RequestQueueElement *transaction); + void linkCallback(Modbus_Transaction *transaction); // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. @@ -84,7 +84,7 @@ struct ModbusDEVICE_struct { // The function returns true if the request was queued. bool writeSingleRegister(uint16_t address, uint16_t value, - ModbusResultState * stateptr); + ModbusResultState *stateptr); // Start reading a Modbus holding register from another module. The result will be available later. // The function returns true if the request was queued. @@ -99,13 +99,14 @@ struct ModbusDEVICE_struct { private: uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; + uint8_t _linkId = -1; // Link index used to identify the Modbus link ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device ModbusRegisterSet_struct _registerSet = {}; // Structure to hold multiple register values for multi-value responses - void sendEvent(Modbus_RequestQueueElement& req, + void sendEvent(Modbus_Transaction& req, ModbusResultMessageType messageType, int par2 = 0, int par3 = 0, @@ -115,11 +116,11 @@ struct ModbusDEVICE_struct { int par7 = 0, int par8 = 0); - void sendEvent(Modbus_RequestQueueElement& req, + void sendEvent(Modbus_Transaction& req, ModbusResultMessageType messageType, ModbusRegisterSet_struct *registerSet); - void createReadFrame(Modbus_RequestQueueElement& request, + void createReadFrame(Modbus_Transaction& request, uint8_t busAddress, uint16_t registerAddress, uint16_t registerCount = 1); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 2679ec088f..e65aaac491 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -57,13 +57,22 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, bool collision_detect) { int available = 0; + _initialized = false; // In case of reinitialization the flag might already been set + // (re)create the serial port object // If the serial port object already exists, delete it first. if (_easySerial != nullptr) { delete _easySerial; _easySerial = nullptr; } - _easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx); + + if (port == ESPEasySerialPort::not_set) { + return true; // No port configured, report this as a successful initialization of a not connected link + // Note that the link object exists, but is flagged as not initialized and has no serial port object + } + else { + _easySerial = new (std::nothrow) ESPeasySerial(port, serial_rx, serial_tx); + } if (_easySerial == nullptr) { return false; @@ -100,6 +109,7 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, ); addLogMove(LOG_LEVEL_INFO, log); } + # endif // MODBUS_DEBUG _initialized = true; return true; @@ -128,7 +138,7 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) // This function will transfer ownership of the transaction to the Modbus link, which is responsible for freeing // the associated resources when the transaction is completed. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transaction) { +uint16_t ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { if (transaction == nullptr) { addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); return 0; @@ -146,21 +156,20 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac return 0; } + transaction->_id = ++(_queueID); // Assign a unique ID to the transaction + transaction->_timeout = _modbus_timeout; // Default timeout value + transaction->_state = ModbusQueueState::QUEUED; // Transaction is now queued + _requestQueue.push_back(transaction); // Append the request to the queue + # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus: Link, Queueing transaction ID %u, state %u"), transaction->_id, static_cast(transaction->_state))); + strformat(F("Modbus: Link=%p, Queueing transaction ID %u, state %s"), this, transaction->_id, toString(transaction->_state))); } # endif // MODBUS_DEBUG - transaction->_id = ++(_queueID); // Assign a unique ID to the transaction - transaction->_timeout = _modbus_timeout; // Default timeout value - transaction->_state = ModbusQueueState::QUEUED; // Transaction is now queued - _requestQueue.push_back(transaction); // Append the request to the queue - if (!_processing) { // Prevent reentrancy issues - processCommand(); // Trigger processing of the command queue - } + processCommand(); // Trigger processing of the command queue return transaction->_id; } @@ -170,11 +179,10 @@ uint16_t ModbusLINK_struct::queueTransaction(Modbus_RequestQueueElement *transac /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processCommand() { - _processing = true; // Set processing flag to prevent reentrancy issues - - if (!isInitialized() || (_requestQueue.empty())) { + if (_processing || !isInitialized() || (_requestQueue.empty())) { return; // Serial port not initialized or queue is empty, nothing to process } + _processing = true; // Set processing flag to prevent reentrancy issues auto it = _requestQueue.begin(); // Iterator for the request queue bool busy = false; // Only process one request at a time @@ -279,11 +287,11 @@ bool ModbusLINK_struct::getCollisionDetect(void) const /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Debugging function to dump the queue element contents to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusLINK_struct::dumpQueueElement(Modbus_RequestQueueElement *el) { +void ModbusLINK_struct::dumpQueueElement(Modbus_Transaction *el) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = strformat(F("Modbus: [ID=%u, Device=%p, State="), el->_id, el->_device); + String log = strformat(F("Modbus: Transaction [ID=%u, Device=%p, State="), el->_id, el->_device); log += toString(el->_state); log += F(", TX=("); diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 02171a5eed..1256b00c47 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -39,9 +39,9 @@ typedef enum class ModbusTransactionType { // Modbus request queue element structure // This structure represents a single Modbus request and its associated response. -struct Modbus_RequestQueueElement { - Modbus_RequestQueueElement() = default; - Modbus_RequestQueueElement(ModbusDEVICE_struct *device, +struct Modbus_Transaction { + Modbus_Transaction() = default; + Modbus_Transaction(ModbusDEVICE_struct *device, ModbusTransactionType_t messageType, uint16_t userId, void *userData, @@ -73,7 +73,7 @@ struct Modbus_RequestQueueElement { }; // Queue of Modbus request elements -typedef std::list Modbus_RequestQueue; +typedef std::list Modbus_RequestQueue; // ModbusLINK structure representing a MODBUS LINK @@ -98,7 +98,7 @@ struct ModbusLINK_struct { bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } void freeTransactions(struct ModbusDEVICE_struct *device); - uint16_t queueTransaction(Modbus_RequestQueueElement *transaction); + uint16_t queueTransaction(Modbus_Transaction *transaction); void processCommand(); int16_t getBaudrate(void) const; @@ -113,7 +113,7 @@ struct ModbusLINK_struct { private: - static void dumpQueueElement(Modbus_RequestQueueElement *el); + static void dumpQueueElement(Modbus_Transaction *el); static void dumpState(ModbusQueueState_t state); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 9323e75eb7..b6142e9a99 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -46,13 +46,6 @@ ModbusMGR_struct::ModbusMGR_struct() String log = F("Modbus: Manager, Constructor "); # endif // ifdef MODBUS_DEBUG - for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - _modbus_links[i] = nullptr; - } - - for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { - _modbus_devices[i] = nullptr; - } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -63,28 +56,19 @@ ModbusMGR_struct::~ModbusMGR_struct() // This class is a singleton, so destructor should not be called. // However, in case it is called, we clean up the allocated resources. - for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { - if (_modbus_devices[i] != nullptr) { - delete _modbus_devices[i]; - _modbus_devices[i] = nullptr; - } - } - for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if (_modbus_links[i] != nullptr) { - if (_modbus_links[i]->link != nullptr) { - delete _modbus_links[i]->link; - _modbus_links[i]->link = nullptr; - } - if (_modbus_links[i]->kvs != nullptr) { - delete _modbus_links[i]->kvs; - _modbus_links[i]->kvs = nullptr; - } - delete _modbus_links[i]; - _modbus_links[i] = nullptr; + if (_modbus_links[i].link != nullptr) { + delete _modbus_links[i].link; + _modbus_links[i].link = nullptr; + } + + if (_modbus_links[i].kvs != nullptr) { + delete _modbus_links[i].kvs; + _modbus_links[i].kvs = nullptr; } } + } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -93,35 +77,36 @@ ModbusMGR_struct::~ModbusMGR_struct() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::initialize() { + if (!_initialized) { + for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if (_modbus_links[i] == nullptr) { int8_t val; - _modbus_links[i] = new (std::nothrow) ModbusLinkInfo_struct(); - _modbus_links[i]->link = nullptr; - _modbus_links[i]->kvs = new (std::nothrow) ESPEasy_key_value_store; - _modbus_links[i]->kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); - _modbus_links[i]->kvs->getValue(MODBUS_PORT_KEY_INDEX, val); - _modbus_links[i]->port = static_cast(val); - _modbus_links[i]->kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i]->serial_rx); - _modbus_links[i]->kvs->getValue(MODBUS_TX_KEY_INDEX, _modbus_links[i]->serial_tx); - _modbus_links[i]->kvs->getValue(MODBUS_BAUDRATE_KEY_INDEX, _modbus_links[i]->baudrate); - _modbus_links[i]->kvs->getValue(MODBUS_DERE_PIN_KEY_INDEX, _modbus_links[i]->dere_pin); - _modbus_links[i]->kvs->getValue(MODBUS_COLLISION_DETECT_KEY_INDEX, _modbus_links[i]->collision_detect); - - if (_modbus_links[i]->port != ESPEasySerialPort::not_set) { - _modbus_links[i]->link = new (std::nothrow) ModbusLINK_struct(); - _modbus_links[i]->link->init(_modbus_links[i]->port, - _modbus_links[i]->serial_rx, - _modbus_links[i]->serial_tx, - _modbus_links[i]->baudrate, - _modbus_links[i]->dere_pin, - _modbus_links[i]->collision_detect); + _modbus_links[i].link = nullptr; // Initialize link pointer to a safe value + // Create a key-value store for the link and load the persisted settings + _modbus_links[i].kvs = new (std::nothrow) ESPEasy_key_value_store; + _modbus_links[i].kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); + _modbus_links[i].kvs->getValue(MODBUS_PORT_KEY_INDEX, val); + _modbus_links[i].port = static_cast(val); + _modbus_links[i].kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i].serial_rx); + _modbus_links[i].kvs->getValue(MODBUS_TX_KEY_INDEX, _modbus_links[i].serial_tx); + _modbus_links[i].kvs->getValue(MODBUS_BAUDRATE_KEY_INDEX, _modbus_links[i].baudrate); + _modbus_links[i].kvs->getValue(MODBUS_DERE_PIN_KEY_INDEX, _modbus_links[i].dere_pin); + _modbus_links[i].kvs->getValue(MODBUS_COLLISION_DETECT_KEY_INDEX, _modbus_links[i].collision_detect); + + // Create the Modbus link object if a port is configured for the link + if (_modbus_links[i].port != ESPEasySerialPort::not_set) { + _modbus_links[i].link = new (std::nothrow) ModbusLINK_struct(); + _modbus_links[i].link->init(_modbus_links[i].port, + _modbus_links[i].serial_rx, + _modbus_links[i].serial_tx, + _modbus_links[i].baudrate, + _modbus_links[i].dere_pin, + _modbus_links[i].collision_detect); } + } + _initialized = true; } - _initialized = true; - # ifdef MODBUS_DEBUG dumpAdminInfo(); # endif // ifdef MODBUS_DEBUG @@ -140,7 +125,10 @@ bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *de log += linkId; # endif // ifdef MODBUS_DEBUG - initialize(); // TODO Initialization sequence to be refactored. + initialize(); // TODO Initialization sequence to be refactored. + + *deviceID = -1; // Default to -1, currently not used anymore + *link = nullptr; if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS)) { # ifdef MODBUS_DEBUG @@ -150,27 +138,7 @@ bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *de return false; } - *deviceID = -1; // Default to -1, will be set to a valid device ID if connection is successful - *link = nullptr; - - if (_modbus_links[linkId] != nullptr) { - for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { - if (_modbus_devices[i] == nullptr) { - // Found an available device slot - _modbus_devices[i] = new (std::nothrow) ModbusDeviceInfo_struct(); - _modbus_devices[i]->deviceID = i + 1; // Assign a unique device ID (1-MAX_MODBUS_DEVICES) - _modbus_devices[i]->link = _modbus_links[linkId]; - *deviceID = _modbus_devices[i]->deviceID; - *link = _modbus_devices[i]->link->link; - # ifdef MODBUS_DEBUG - log += F(" Assigned deviceID= "); - log += *deviceID; - # endif // ifdef MODBUS_DEBUG - break; - } - } - } - else { + if (_modbus_links[linkId].port == ESPEasySerialPort::not_set) { # ifdef MODBUS_DEBUG log += F(" No link available at linkIndex= "); log += linkId; @@ -179,14 +147,8 @@ bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *de return false; } - if (*deviceID == -1) { - // No available device slot found, connection failed - # ifdef MODBUS_DEBUG - log += F(" Failed to assign device ID, no available device slots"); - addLogMove(LOG_LEVEL_ERROR, log); - # endif // ifdef MODBUS_DEBUG - return false; - } + *deviceID = ++_deviceCounter; + *link = _modbus_links[linkId].link; # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); @@ -207,13 +169,7 @@ bool ModbusMGR_struct::disconnect(uint8_t deviceID) { log += deviceID; # endif // ifdef MODBUS_DEBUG - for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { - if ((_modbus_devices[i] != nullptr) && (_modbus_devices[i]->deviceID == deviceID)) { - // Remove the device entry - delete _modbus_devices[i]; - _modbus_devices[i] = nullptr; - } - } + # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef MODBUS_DEBUG @@ -227,8 +183,8 @@ void ModbusMGR_struct::processLinks() { if (isInitialized()) { for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if ((_modbus_links[i] != nullptr) && (_modbus_links[i]->link != nullptr)) { - _modbus_links[i]->link->processCommand(); // Trigger processing of the command queue on the link + if ((_modbus_links[i].link != nullptr)) { + _modbus_links[i].link->processCommand(); // Trigger processing of the command queue on the link } } } @@ -244,38 +200,20 @@ void ModbusMGR_struct::dumpAdminInfo() // Iterate over the modbus links and dump their info for (int i = 0; i < MAX_MODBUS_LINKS; i++) { - if (_modbus_links[i] != nullptr) { - addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus_mgr: Link[%d] Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, RS485Mode=%s, CollisionDetect=%s"), - i, - ESPEasySerialPort_toString(_modbus_links[i]->port), - _modbus_links[i]->serial_rx, - _modbus_links[i]->serial_tx, - _modbus_links[i]->baudrate, - _modbus_links[i]->dere_pin, - _modbus_links[i]->rs485_mode ? F("Yes") : F("No"), - _modbus_links[i]->collision_detect ? F("Yes") : F("No") - )); - } - else { - addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Link[%d] "), i)); - } - } - // Iterate over the modbus devices and dump their info - for (int i = 0; i < MAX_MODBUS_DEVICES; i++) { - if (_modbus_devices[i] != nullptr) { - addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus_mgr: Device[%d] DeviceID=%d, LinkPort=%s"), - i, - _modbus_devices[i]->deviceID, - ESPEasySerialPort_toString(_modbus_devices[i]->link->port) - )); - } - else { - addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus_mgr: Device[%d] "), i)); - } + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus_mgr: Link[%d] Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, RS485Mode=%s, CollisionDetect=%s"), + i, + ESPEasySerialPort_toString(_modbus_links[i].port), + _modbus_links[i].serial_rx, + _modbus_links[i].serial_tx, + _modbus_links[i].baudrate, + _modbus_links[i].dere_pin, + _modbus_links[i].rs485_mode ? F("Yes") : F("No"), + _modbus_links[i].collision_detect ? F("Yes") : F("No") + )); } + # endif // ifdef MODBUS_DEBUG } @@ -318,94 +256,67 @@ void ModbusMGR_struct::show_modbus_interfaces() // Iterate over the modbus links and show their configuration on the web page for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { - if (_modbus_links[link] != nullptr) { - addFormSubHeader(strformat(F("Modbus %u"), link)); - addFormDetailsStart(link == 0); - int idx = static_cast(_modbus_links[link]->port); - portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); + addFormSubHeader(strformat(F("Modbus %u"), link)); + addFormDetailsStart(link == 0); - String id = strformat(F("MBtx%u"), link); - addRowLabel_tr_id(formatGpioName_serialTX(false), id); - addPinSelect(PinSelectPurpose::Serial_input, id, _modbus_links[link]->serial_tx); + int idx = static_cast(_modbus_links[link].port); + portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); - id = strformat(F("MBrx%u"), link); - addRowLabel_tr_id(formatGpioName_serialRX(false), id); - addPinSelect(PinSelectPurpose::Serial_output, id, _modbus_links[link]->serial_rx); + String id = strformat(F("MBtx%u"), link); + addRowLabel_tr_id(formatGpioName_serialTX(false), id); + addPinSelect(PinSelectPurpose::Serial_input, id, _modbus_links[link].serial_tx); - id = strformat(F("MBde%u"), link); - addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); - addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link]->dere_pin); + id = strformat(F("MBrx%u"), link); + addRowLabel_tr_id(formatGpioName_serialRX(false), id); + addPinSelect(PinSelectPurpose::Serial_output, id, _modbus_links[link].serial_rx); - baudselector.addFormSelector(F("Baud Rate"), - strformat(F("MBbaud%u"), link), - modbus_baudrateToStorageValue(_modbus_links[link]->baudrate)); - addUnit(F("baud")); + id = strformat(F("MBde%u"), link); + addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); + addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link].dere_pin); + + baudselector.addFormSelector(F("Baud Rate"), + strformat(F("MBbaud%u"), link), + modbus_baudrateToStorageValue(_modbus_links[link].baudrate)); + addUnit(F("baud")); # ifdef ESP32 - addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), link), _modbus_links[link]->collision_detect); - addFormNote(F("/RE connected to GND, only supported on hardware serial")); + addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), link), _modbus_links[link].collision_detect); + addFormNote(F("/RE connected to GND, only supported on hardware serial")); # endif // ifdef ESP32 - addFormDetailsEnd(); - } + addFormDetailsEnd(); + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Convert stored baudrate setting (enumeration value) to actual baudrate value // Returns the actual baudrate value. +// The first value is 1200 baud, and each subsequent value doubles the baudrate up to 115200 baud. +// Values outside this range will be mapped to the closest valid value. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int modbus_storageValueToBaudrate(uint8_t baudrate_setting) { - int baudrate = 9600; - - switch (baudrate_setting) - { - case 0: - baudrate = 1200; - break; - case 1: - baudrate = 2400; - break; - case 2: - baudrate = 4800; - break; - case 3: - baudrate = 9600; - break; - case 4: - baudrate = 19200; - break; - case 5: - baudrate = 38400; - break; - case 6: - baudrate = 57600; - break; - case 7: - baudrate = 115200; - break; - default: - baudrate = 9600; - break; // Default value for fallback - } - return baudrate; + if ((baudrate_setting > 7) || (baudrate_setting < 0)) { return 9600; } + return 1200 << baudrate_setting; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Convert actual baudrate value to stored baudrate setting (enumeration value) // Returns the stored baudrate setting. +// The first value is 1200 baud, and each subsequent value doubles the baudrate up to 115200 baud. +// Values outside this range will be mapped to the closest valid value. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint8_t modbus_baudrateToStorageValue(int baudrate) { - if (baudrate <= 1200) { return 0; } - else if (baudrate <= 2400) { return 1; } - else if (baudrate <= 4800) { return 2; } - else if (baudrate <= 9600) { return 3; } - else if (baudrate <= 19200) { return 4; } - else if (baudrate <= 38400) { return 5; } - else if (baudrate <= 57600) { return 6; } - else if (baudrate <= 115200) { return 7; } - else { return 3; } // Default to 9600 baud for unsupported values + if ((baudrate < 1200) || (baudrate > 115200)) { return modbus_baudrateToStorageValue(9600); } + int bd = 1200; + uint8_t index = 0; + + while (bd < baudrate) { + bd *= 2; + ++index; + } + return index; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -436,46 +347,46 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) bool collision_detect_setting = false; bool settingsChanged = false; - if (_modbus_links[link] != nullptr) { - for (int i = 0; i < NR_ELEMENTS(portMap); i++) { - if (portMap[i] == static_cast(_modbus_links[link]->port)) { - port_setting = i; - break; - } - } - if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting)) { - settingsChanged |= (portMap[port_setting] != static_cast(_modbus_links[link]->port)); + for (int i = 0; i < NR_ELEMENTS(portMap); i++) { + if (portMap[i] == static_cast(_modbus_links[link].port)) { + port_setting = i; + break; } - baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link]->baudrate); + } - if (update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting)) { - settingsChanged |= (modbus_storageValueToBaudrate(baudrate_setting) != _modbus_links[link]->baudrate); - } - tx_setting = _modbus_links[link]->serial_tx; + if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting)) { + settingsChanged |= (portMap[port_setting] != static_cast(_modbus_links[link].port)); + } + baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); - if (update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting)) { - settingsChanged |= (tx_setting != _modbus_links[link]->serial_tx); - } - rx_setting = _modbus_links[link]->serial_rx; + if (update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting)) { + settingsChanged |= (modbus_storageValueToBaudrate(baudrate_setting) != _modbus_links[link].baudrate); + } + tx_setting = _modbus_links[link].serial_tx; - if (update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting)) { - settingsChanged |= (rx_setting != _modbus_links[link]->serial_rx); - } - dere_setting = _modbus_links[link]->dere_pin; + if (update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting)) { + settingsChanged |= (tx_setting != _modbus_links[link].serial_tx); + } + rx_setting = _modbus_links[link].serial_rx; + + if (update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting)) { + settingsChanged |= (rx_setting != _modbus_links[link].serial_rx); + } + dere_setting = _modbus_links[link].dere_pin; - if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting)) { - settingsChanged |= (dere_setting != _modbus_links[link]->dere_pin); + if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting)) { + settingsChanged |= (dere_setting != _modbus_links[link].dere_pin); # ifdef ESP32 - // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. - // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. - collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); - settingsChanged |= (collision_detect_setting != _modbus_links[link]->collision_detect); + // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. + // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. + collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); + settingsChanged |= (collision_detect_setting != _modbus_links[link].collision_detect); # endif // ifdef ESP32 - } } + if (settingsChanged) { setLink(link, static_cast(portMap[port_setting]), @@ -496,13 +407,10 @@ bool ModbusMGR_struct::setLink(const int linkIndex, const ESPEasySerialPort port, const int8_t serial_rx, const int8_t serial_tx, - uint16_t baudrate, + uint16_t baudrate, int8_t dere_pin, bool collision_detect) { - ModbusLinkInfo_struct *linkInfoPtr = nullptr; - - # ifdef MODBUS_DEBUG String log = F("Modbus_mgr: SetLink. "); @@ -518,75 +426,41 @@ bool ModbusMGR_struct::setLink(const int linkIndex, )); # endif // ifdef MODBUS_DEBUG - if ((linkIndex >= 0) && (linkIndex < MAX_MODBUS_LINKS)) { - if (_modbus_links[linkIndex] == nullptr) { - linkInfoPtr = new (std::nothrow) ModbusLinkInfo_struct(); - _modbus_links[linkIndex] = linkInfoPtr; - # ifdef MODBUS_DEBUG - log += strformat(F("New link for linkIndex=%d"), linkIndex); - # endif // ifdef MODBUS_DEBUG - } - else { - linkInfoPtr = _modbus_links[linkIndex]; // Link admin already exists, will be reused for the new link configuration - # ifdef MODBUS_DEBUG - log += strformat(F("Existing link for linkIndex=%d"), linkIndex); - # endif // ifdef MODBUS_DEBUG - } - } - else { + if ((linkIndex < 0) || (linkIndex >= MAX_MODBUS_LINKS)) { # ifdef MODBUS_DEBUG log += strformat(F("Invalid link for linkIndex=%d"), linkIndex); addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef MODBUS_DEBUG - return false; // Invalid link index + return false; // Invalid link index } - if (linkInfoPtr != nullptr) { // Sanity check for successful link admin creation - if (linkInfoPtr->link == nullptr) { // Check if link object already exists - // No existing link, create a new one - linkInfoPtr->link = new (std::nothrow) ModbusLINK_struct(); + if (_modbus_links[linkIndex].link != nullptr) { // Sanity check for successful creation + // (re)initialize the new link + if (!_modbus_links[linkIndex].link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + return false; // Initialization failed } - - if (linkInfoPtr->link != nullptr) { // Sanity check for successful creation - // (re)initialize the new link - if (!linkInfoPtr->link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { - // Initialization failed, clean up - delete linkInfoPtr->link; - linkInfoPtr->link = nullptr; - delete linkInfoPtr; - linkInfoPtr = nullptr; - return false; // Initialization failed - } - else { - // Store the link parameters - linkInfoPtr->port = port; - linkInfoPtr->serial_rx = serial_rx; - linkInfoPtr->serial_tx = serial_tx; - linkInfoPtr->baudrate = baudrate; - linkInfoPtr->dere_pin = dere_pin; - linkInfoPtr->rs485_mode = (dere_pin != -1); - linkInfoPtr->collision_detect = collision_detect; - } + else { + // Store the link parameters + _modbus_links[linkIndex].port = port; + _modbus_links[linkIndex].serial_rx = serial_rx; + _modbus_links[linkIndex].serial_tx = serial_tx; + _modbus_links[linkIndex].baudrate = baudrate; + _modbus_links[linkIndex].dere_pin = dere_pin; + _modbus_links[linkIndex].rs485_mode = (dere_pin != -1); + _modbus_links[linkIndex].collision_detect = collision_detect; } } - else { - return false; // Failed to create link admin - } - - if (_modbus_links[linkIndex]->kvs == nullptr) { - _modbus_links[linkIndex]->kvs = new (std::nothrow) ESPEasy_key_value_store; - } - if (_modbus_links[linkIndex]->kvs) { + if (_modbus_links[linkIndex].kvs) { // Store the link configuration parameters in the key-value store for persistence - linkInfoPtr->kvs->setValue(MODBUS_PORT_KEY_INDEX, static_cast(port)); - linkInfoPtr->kvs->setValue(MODBUS_RX_KEY_INDEX, static_cast(serial_rx)); - linkInfoPtr->kvs->setValue(MODBUS_TX_KEY_INDEX, static_cast(serial_tx)); - linkInfoPtr->kvs->setValue(MODBUS_BAUDRATE_KEY_INDEX, static_cast(baudrate)); - linkInfoPtr->kvs->setValue(MODBUS_DERE_PIN_KEY_INDEX, static_cast(dere_pin)); - linkInfoPtr->kvs->setValue(MODBUS_COLLISION_DETECT_KEY_INDEX, static_cast(collision_detect)); - - linkInfoPtr->kvs->store(SettingsType::Enum::ModbusInterfaceSettings_Type, linkIndex, 0, 0); + _modbus_links[linkIndex].kvs->setValue(MODBUS_PORT_KEY_INDEX, static_cast(port)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_RX_KEY_INDEX, static_cast(serial_rx)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_TX_KEY_INDEX, static_cast(serial_tx)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_BAUDRATE_KEY_INDEX, static_cast(baudrate)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_DERE_PIN_KEY_INDEX, static_cast(dere_pin)); + _modbus_links[linkIndex].kvs->setValue(MODBUS_COLLISION_DETECT_KEY_INDEX, static_cast(collision_detect)); + + _modbus_links[linkIndex].kvs->store(SettingsType::Enum::ModbusInterfaceSettings_Type, linkIndex, 0, 0); } # ifdef MODBUS_DEBUG diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 326cf3febd..1ba83e3dcf 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -33,14 +33,15 @@ typedef struct ModbusMGR_struct { void show_modbus_interfaces(); bool save_modbus_interfaces(String& error); + bool isInitialized() const { return _initialized; } private: static const int MAX_MODBUS_LINKS = 4; // Maximum number of Modbus links supported - static const int MAX_MODBUS_DEVICES = 16; // Maximum number of Modbus devices supported - // Structure representing the information of a Modbus link, including its configuration and associated ModbusLINK object + // Structure representing the information of a Modbus link as managed by the Modbus_mgr + // Includes associated ModbusLINK object, link configuration and config storage struct ModbusLinkInfo_struct { ESPEasySerialPort port = ESPEasySerialPort::not_set; int8_t serial_rx = -1; @@ -54,23 +55,15 @@ typedef struct ModbusMGR_struct { }; - // Structure representing the information of a Modbus device, including assocuited ModbusDEVICE object - struct ModbusDeviceInfo_struct { - uint8_t deviceID = 0; // Unique ID assigned by the Modbus manager - struct ModbusLinkInfo_struct *link = nullptr; // Pointer to the Modbus link info - - }; - - ModbusLinkInfo_struct *_modbus_links[MAX_MODBUS_LINKS] = { nullptr }; // Pointer to the Modbus link object - ModbusDeviceInfo_struct *_modbus_devices[MAX_MODBUS_DEVICES] = { nullptr }; // Array of connected Modbus devices - bool _initialized = false; // Flag indicating if the manager is initialized - bool _testing = false; + ModbusLinkInfo_struct _modbus_links[MAX_MODBUS_LINKS] = {}; // All links managed by the Modbus manager + bool _initialized = false; // Flag indicating if the manager is initialized + int _deviceCounter = 0; bool setLink(const int linkIndex, const ESPEasySerialPort port, const int8_t serial_rx, const int8_t serial_tx, - uint16_t baudrate, + uint16_t baudrate, int8_t dere_pin, bool collision_detect); From 305d41e49a13b5302c9e2b75238020e41ba080d5 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:44:48 +0200 Subject: [PATCH 37/48] P183: refactored transaction handling --- misc/modbusFacility/Modbus_class.puml | 11 +- misc/modbusFacility/Modbus_class.svg | 2 +- .../Modbus_class/Modbus_class.svg | 1 + .../Modbus_link_state/Modbus_link_state.svg | 1 + src/src/Helpers/Modbus_device.cpp | 161 ++++++++------- src/src/Helpers/Modbus_device.h | 34 ++-- src/src/Helpers/Modbus_link.cpp | 190 ++++++++++-------- src/src/Helpers/Modbus_link.h | 65 +++--- src/src/Helpers/Modbus_mgr.cpp | 118 ++++++++--- src/src/Helpers/Modbus_mgr.h | 21 +- 10 files changed, 359 insertions(+), 245 deletions(-) create mode 100644 out/misc/modbusFacility/Modbus_class/Modbus_class.svg create mode 100644 out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg diff --git a/misc/modbusFacility/Modbus_class.puml b/misc/modbusFacility/Modbus_class.puml index e4b9b73575..bc6f9509f3 100644 --- a/misc/modbusFacility/Modbus_class.puml +++ b/misc/modbusFacility/Modbus_class.puml @@ -28,13 +28,20 @@ package "modbusFacility" { class Serial_port { } +class interface_gui { +} + +class kvs { +} + plugin *-- plugin_struct plugin_struct *-right- Modbus_device -Modbus_device "*" -down-> "1" Modbus_link Modbus_link "1" -right-> "1" Serial_port -Modbus_mgr "1" *-- "*" Modbus_link +Modbus_mgr "1" *--- "*" Modbus_link Modbus_mgr "1" <-- "*" Modbus_device Modbus_link *-- "1" Queue Queue o-left- "*" Transaction Modbus_device "1" --> "*" Transaction +interface_gui -left-> Modbus_mgr +Modbus_mgr --> kvs @enduml \ No newline at end of file diff --git a/misc/modbusFacility/Modbus_class.svg b/misc/modbusFacility/Modbus_class.svg index 79a12939c4..9da7462b91 100644 --- a/misc/modbusFacility/Modbus_class.svg +++ b/misc/modbusFacility/Modbus_class.svg @@ -1 +1 @@ -modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_port*1111*1*1*1* \ No newline at end of file +modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_portinterface_guikvs111*1*1*1* \ No newline at end of file diff --git a/out/misc/modbusFacility/Modbus_class/Modbus_class.svg b/out/misc/modbusFacility/Modbus_class/Modbus_class.svg new file mode 100644 index 0000000000..9da7462b91 --- /dev/null +++ b/out/misc/modbusFacility/Modbus_class/Modbus_class.svg @@ -0,0 +1 @@ +modbusFacility«singleton»Modbus_mgrModbus_linkModbus_deviceQueueTransactionpluginplugin_structSerial_portinterface_guikvs111*1*1*1* \ No newline at end of file diff --git a/out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg b/out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg new file mode 100644 index 0000000000..b6ff91aed4 --- /dev/null +++ b/out/misc/modbusFacility/Modbus_link_state/Modbus_link_state.svg @@ -0,0 +1 @@ +activeQUEUEDMESSAGE_SENTRESPONSE_RECEIVEDERROR_OCCURREDNOT_QUEUEDREADY_FOR_DESTROYnewTransaction()queueTransaction()link inactivereceived valid responseresponse timeout or invalid responsefreeTransaction()processing \ No newline at end of file diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 34998c95fd..0797074f6e 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -27,12 +27,8 @@ const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; // Destructor of the Modbus device class /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusDEVICE_struct::~ModbusDEVICE_struct() { - if (_modbus_link != nullptr) { - _modbus_link->freeTransactions(this); // Make sure all queued transactions for this device are freed to prevent callbacks to a - // destructed object - } - ModbusMGR_singleton.disconnect(_deviceID); - _modbus_link = nullptr; + + ModbusMGR_singleton.disconnect(_linkId, this); _deviceID = 0; _modbus_address = MODBUS_BROADCAST_ADDRESS; } @@ -41,12 +37,8 @@ ModbusDEVICE_struct::~ModbusDEVICE_struct() { // Reset the Modbus device class to initial state /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::reset() { - if (_modbus_link != nullptr) { - _modbus_link->freeTransactions(this); // Make sure all queued transactions for this device are freed to prevent callbacks to a - // destructed object - } - ModbusMGR_singleton.disconnect(_deviceID); - _modbus_link = nullptr; + + ModbusMGR_singleton.disconnect(_linkId, this); _deviceID = 0; _modbus_address = MODBUS_BROADCAST_ADDRESS; } @@ -56,11 +48,12 @@ void ModbusDEVICE_struct::reset() { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t taskIndex) { - bool success = ModbusMGR_singleton.connect(linkId, &_modbus_link, &_deviceID); + bool success = ModbusMGR_singleton.connect(linkId, this); _modbus_address = slaveAddress; _taskIndex = taskIndex; _linkId = linkId; + # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -76,7 +69,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t tas // Checker for device class initialization status /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::isInitialized() const { - return (_modbus_link != nullptr) && (_modbus_link->isInitialized()); + return (_linkId >= 0); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -103,17 +96,18 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, if (!isInitialized()) { return false; } - Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, - ModbusTransactionType::READ_HOLDING_REGISTERS, - 0, - valuePtr, - statePtr); - - if (request == nullptr) { - return false; // Failed to allocate a request structure + Modbus_Transaction *transaction = nullptr; // Declare the request pointer + ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure } - createReadFrame(*request, _modbus_address, address); - (void)_modbus_link->queueTransaction(request); // Transaction is now owned by the Modbus link + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = 0; // Not used for this type of request + transaction->_userData = valuePtr; + transaction->_userState = statePtr; + createReadFrame(*transaction, _modbus_address, address); + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction ); // Transaction is now owned by the Modbus link return true; } @@ -139,17 +133,19 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, if (!isInitialized()) { return false; } - Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, - ModbusTransactionType::READ_HOLDING_REGISTERS, - uid, - nullptr, - nullptr); - - if (request == nullptr) { - return false; // Failed to allocate a request structure + + Modbus_Transaction *transaction = nullptr; // Declare the request pointer + ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure } - createReadFrame(*request, busAddress, registerAddress); - (void)_modbus_link->queueTransaction(request); // Transaction is now owned by the Modbus link + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = uid; + transaction->_userData = nullptr; + transaction->_userState = nullptr; + createReadFrame(*transaction, busAddress, registerAddress); + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction ); // Transaction is now owned by the Modbus link return true; } @@ -162,17 +158,19 @@ bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, if (!isInitialized()) { return false; } - Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, - ModbusTransactionType::READ_HOLDING_REGISTERS, - uid, - nullptr, - nullptr); - - if (request == nullptr) { - return false; // Failed to allocate a request structure + + Modbus_Transaction *transaction = nullptr; // Declare the request pointer + ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure } - createReadFrame(*request, _modbus_address, address, size); - (void)_modbus_link->queueTransaction(request); // Transaction is now owned by the Modbus link + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = uid; + transaction->_userData = nullptr; + transaction->_userState = nullptr; + createReadFrame(*transaction, _modbus_address, address, size); + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction ); // Transaction is now owned by the Modbus link return true; } @@ -180,9 +178,9 @@ bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, // Construct a Modbus read holding registers message /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusDEVICE_struct::createReadFrame(Modbus_Transaction& request, - uint8_t busAddress, - uint16_t registerAddress, - uint16_t registerCount) + uint8_t busAddress, + uint16_t registerAddress, + uint16_t registerCount) { request._messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; request._sendframe[0] = busAddress; @@ -209,24 +207,31 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, if (!isInitialized()) { return false; } - Modbus_Transaction *request = new (std::nothrow) Modbus_Transaction(this, - ModbusTransactionType::WRITE_SINGLE_REGISTER, - 0, - nullptr, - statePtr); - - request->_sendframe[0] = _modbus_address; - request->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; - request->_sendframe[2] = highByte(address); - request->_sendframe[3] = lowByte(address); - request->_sendframe[4] = highByte(value); - request->_sendframe[5] = lowByte(value); - uint16_t crc = CalculateCRC(request->_sendframe, 6); - request->_sendframe[6] = lowByte(crc); // CRC low byte - request->_sendframe[7] = highByte(crc); // CRC high byte - request->_sendframe_length = 8; // Size with CRC - request->_rcvframe_length = 8; // Expect 8 bytes in response - (void)_modbus_link->queueTransaction(request); + + Modbus_Transaction *transaction = nullptr; // Declare the request pointer + ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + + if (transaction == nullptr) { + return false; // Failed to allocate a request structure + } + transaction->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; + transaction->_userId = 0; // Not used for this type of request + transaction->_userData = nullptr; + transaction->_userState = statePtr; + + + transaction->_sendframe[0] = _modbus_address; + transaction->_sendframe[1] = MODBUS_WRITE_SINGLE_REGISTER; + transaction->_sendframe[2] = highByte(address); + transaction->_sendframe[3] = lowByte(address); + transaction->_sendframe[4] = highByte(value); + transaction->_sendframe[5] = lowByte(value); + uint16_t crc = CalculateCRC(transaction->_sendframe, 6); + transaction->_sendframe[6] = lowByte(crc); // CRC low byte + transaction->_sendframe[7] = highByte(crc); // CRC high byte + transaction->_sendframe_length = 8; // Size with CRC + transaction->_rcvframe_length = 8; // Expect 8 bytes in response + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link *statePtr = ModbusResultState::Busy; return true; @@ -239,7 +244,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, void ModbusDEVICE_struct::processCommand(void) { if (isInitialized()) { - _modbus_link->processCommand(); + ModbusMGR_singleton.processLinks(); } } @@ -373,15 +378,15 @@ void ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) // This is used by the Modbus link to notify the device of responses received for queued requests. // This version of the function is used to pass data through parameters in the event. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::sendEvent(Modbus_Transaction& req, - ModbusResultMessageType messageType, - int par2, - int par3, - int par4, - int par5, - int par6, - int par7, - int par8) +void ModbusDEVICE_struct::sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + int par2, + int par3, + int par4, + int par5, + int par6, + int par7, + int par8) { struct EventStruct TempEvent; @@ -406,9 +411,9 @@ void ModbusDEVICE_struct::sendEvent(Modbus_Transaction& req, // This is used by the Modbus link to notify the device of responses received for queued requests. // This version of the function is used to pass multiple register values in a ModbusRegisterSet_struct. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::sendEvent(Modbus_Transaction& req, - ModbusResultMessageType messageType, - ModbusRegisterSet_struct *registerSet) +void ModbusDEVICE_struct::sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + ModbusRegisterSet_struct *registerSet) { struct EventStruct TempEvent; diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 4ffbcf059d..8871a2b679 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -99,31 +99,31 @@ struct ModbusDEVICE_struct { private: uint8_t _modbus_address = MODBUS_BROADCAST_ADDRESS; - uint8_t _linkId = -1; // Link index used to identify the Modbus link + uint8_t _linkId = 0xFF; // Link index used to identify the Modbus link ModbusLINK_struct *_modbus_link = nullptr; // Pointer to the Modbus link object uint8_t _deviceID = 0; // Identifier used by the Modbus manager to identify this device uint16_t _timeout = 200; // Timeout value in milliseconds for Modbus requests taskIndex_t _taskIndex = 0; // Task index for sending events to the task associated with this device ModbusRegisterSet_struct _registerSet = {}; // Structure to hold multiple register values for multi-value responses - void sendEvent(Modbus_Transaction& req, - ModbusResultMessageType messageType, - int par2 = 0, - int par3 = 0, - int par4 = 0, - int par5 = 0, - int par6 = 0, - int par7 = 0, - int par8 = 0); + void sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + int par2 = 0, + int par3 = 0, + int par4 = 0, + int par5 = 0, + int par6 = 0, + int par7 = 0, + int par8 = 0); - void sendEvent(Modbus_Transaction& req, - ModbusResultMessageType messageType, - ModbusRegisterSet_struct *registerSet); + void sendEvent(Modbus_Transaction & req, + ModbusResultMessageType messageType, + ModbusRegisterSet_struct *registerSet); void createReadFrame(Modbus_Transaction& request, - uint8_t busAddress, - uint16_t registerAddress, - uint16_t registerCount = 1); + uint8_t busAddress, + uint16_t registerAddress, + uint16_t registerCount = 1); static uint16_t CalculateCRC(uint8_t *buf, int len); @@ -134,4 +134,4 @@ struct ModbusDEVICE_struct { }; #endif // FEAURE_MODBUS_FAC -#endif // HELPERS_MODBUS_LINK_H +#endif // HELPERS_MODBUS_DEVICE_H diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index e65aaac491..75ae33ecbd 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -35,14 +35,15 @@ ModbusLINK_struct::~ModbusLINK_struct() { // This aborts all pending transactions and frees the associated resources. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::reset() { - for (auto it = _requestQueue.begin(); it != _requestQueue.end(); it++) { + for (auto it = _transactionQueue.begin(); it != _transactionQueue.end(); it++) { (*it)->_state = ModbusQueueState::ERROR_OCCURRED; if ((*it)->_device != nullptr) { (*it)->_device->linkCallback(*it); // Notify the device that the request finished with an error } + delete (*it); // destroy the queue element - it = _requestQueue.erase(it); + it = _transactionQueue.erase(it); } } @@ -98,7 +99,7 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = strformat(F( - "---> Modbus: Link %s, Init serial, RX pin %d, TX pin %d, RS485 pin %d, baudrate %d, collision detection %s, RS485 mode %s"), + "Modbus Link: Init link=%s, RX pin %d, TX pin %d, RS485 pin %d, baudrate %d, collision detection %s, RS485 mode %s"), ESPEasySerialPort_toString(port), serial_rx, serial_tx, @@ -109,27 +110,51 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, ); addLogMove(LOG_LEVEL_INFO, log); } - # endif // MODBUS_DEBUG + _initialized = true; return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Free all queued transactions for the given device +// This shall be called when a device is removed to free the pending requests for the device preventing callback issues. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) { if (!isInitialized()) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to free transactions on uninitialized link")); + addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to free transactions on uninitialized link")); return; } - for ( auto it = _requestQueue.begin(); it != _requestQueue.end(); ++it ) { + for ( auto it = _transactionQueue.begin(); it != _transactionQueue.end(); ++it ) { if ((*it)->_device == device) { (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed } } + processQueue(); // Trigger processing of the command queue to free the marked transactions +} + +bool ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device, Modbus_transaction_ptr& transaction) +{ + bool success = false; + + if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to create transaction for uninitialized link")); + } + else { + Modbus_Transaction *tr = new (std::nothrow) Modbus_Transaction(device); // Create a new transaction + tr->_id = ++(_queueID); // Assign a unique ID to the transaction + tr->_timeout = _modbus_timeout; // Default timeout value + _transactionQueue.push_back(tr); // Put a new transaction at the end of the queue + transaction = tr; // Return the pointer to the new transaction + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, F("Modbus Link: new transaction")); + # endif // MODBUS_DEBUG + tr->print(); // Print the transaction details for debugging + success = true; + } + return success; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -138,62 +163,62 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) // This function will transfer ownership of the transaction to the Modbus link, which is responsible for freeing // the associated resources when the transaction is completed. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint16_t ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { +bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { + bool success = false; + if (transaction == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue null transaction")); - return 0; + addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to queue null transaction")); + } else if (!isInitialized()) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to queue transaction on uninitialized link")); + transaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error } - - if (!isInitialized()) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, Attempt to queue transaction on uninitialized link")); - delete transaction; // Free the transaction since it won't be queued - return 0; + else if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER) { + addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: receive buffer too large")); + transaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error } - - if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus: Link, receive buffer too large")); - delete transaction; // Free the transaction since it won't be queued - return 0; + else { + transaction->_state = ModbusQueueState::QUEUED; // Transaction is now queued + success = true; } - - transaction->_id = ++(_queueID); // Assign a unique ID to the transaction - transaction->_timeout = _modbus_timeout; // Default timeout value - transaction->_state = ModbusQueueState::QUEUED; // Transaction is now queued - _requestQueue.push_back(transaction); // Append the request to the queue - # ifdef MODBUS_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_INFO) && (transaction != nullptr)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus: Link=%p, Queueing transaction ID %u, state %s"), this, transaction->_id, toString(transaction->_state))); + strformat(F("Modbus link: link=%p, Queueing transaction ID %u, state %s"), this, transaction->_id, + toString(transaction->_state))); } # endif // MODBUS_DEBUG - processCommand(); // Trigger processing of the command queue - return transaction->_id; + processQueue(); // Trigger processing of the command queue + return success; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Evaluate the next action to take to process the queue // This function shall be called periodically to keep the Modbus link active /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusLINK_struct::processCommand() +void ModbusLINK_struct::processQueue() { - if (_processing || !isInitialized() || (_requestQueue.empty())) { - return; // Serial port not initialized or queue is empty, nothing to process + if (_processing || !isInitialized() || (_transactionQueue.empty())) { + return; // Serial port not initialized or queue is empty, nothing to process } - _processing = true; // Set processing flag to prevent reentrancy issues + _processing = true; // Set processing flag to prevent reentrancy issues - auto it = _requestQueue.begin(); // Iterator for the request queue - bool busy = false; // Only process one request at a time + auto it = _transactionQueue.begin(); // Iterator for the request queue + bool busy = false; // Only process one request at a time - while ((it != _requestQueue.end()) && !busy && ((*it) != nullptr)) { + while ((it != _transactionQueue.end()) && !busy) { # ifdef MODBUS_DEBUG - dumpQueueElement(*it); + (*it)->print(); # endif // MODBUS_DEBUG switch ((*it)->_state) { + case ModbusQueueState::NOT_QUEUED: + { + it++; // Not queued yet, move to the next transaction in the queue + } + case ModbusQueueState::QUEUED: { // Send the request @@ -232,30 +257,26 @@ void ModbusLINK_struct::processCommand() if ((*it)->_device != nullptr) { (*it)->_device->linkCallback((*it)); // Notify the device that a response was received } - - delete (*it); // destroy the queue element - it = _requestQueue.erase(it); // Remove it from the list + (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; } else { - it++; // Move to the next transaction in the queue + it++; // Move to the next transaction in the queue } break; } - case ModbusQueueState::NOT_QUEUED: // This state should not be on the queue - { - it++; // Not queued yet, move to the next transaction in the queue - } default: { - delete (*it); // destroy the queue element - it = _requestQueue.erase(it); // Remove it from the list + (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; + + delete (*it); // destroy the queue element + it = _transactionQueue.erase(it); // Remove it from the list break; } - } // switch - } // next iterarion + } // switch + } // next iterarion - _processing = false; // Clear processing flag to allow new processing cycles + _processing = false; // Clear processing flag to allow new processing cycles return; } @@ -284,39 +305,6 @@ bool ModbusLINK_struct::getCollisionDetect(void) const return _collision_detect; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Debugging function to dump the queue element contents to the log -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusLINK_struct::dumpQueueElement(Modbus_Transaction *el) { - # ifdef MODBUS_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = strformat(F("Modbus: Transaction [ID=%u, Device=%p, State="), el->_id, el->_device); - log += toString(el->_state); - log += F(", TX=("); - - for (int i = 0; i < el->_sendframe_length; i++) { - log += String(el->_sendframe[i], HEX); - - if (i < el->_sendframe_length - 1) { - log += F(","); - } - } - log += F("), RX=("); - - for (int i = 0; i < el->_rcvframe_length; i++) { - log += String(el->_rcvframe[i], HEX); - - if (i < el->_rcvframe_length - 1) { - log += F(","); - } - } - log += F(")] "); - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // MODBUS_DEBUG -} - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Debugging function to dump the queue element state to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -324,7 +312,7 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("Modbus: Link, State= "), toString(state))); + addLogMove(LOG_LEVEL_INFO, concat(F("Modbus Link: State= "), toString(state))); } # endif // MODBUS_DEBUG } @@ -349,3 +337,37 @@ const __FlashStringHelper* toString(ModbusQueueState_t state) { } #endif // if FEATURE_MODBUS + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Debugging function to dump the queue element contents to the log +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void Modbus_Transaction::print() +{ + #ifdef MODBUS_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = strformat(F("Modbus Transaction: [ID=%u, Device=%p, State="), _id, _device); + log += toString(_state); + log += F(", TX=("); + + for (int i = 0; i < _sendframe_length; i++) { + log += String(_sendframe[i], HEX); + + if (i < _sendframe_length - 1) { + log += F(","); + } + } + log += F("), RX=("); + + for (int i = 0; i < _rcvframe_length; i++) { + log += String(_rcvframe[i], HEX); + + if (i < _rcvframe_length - 1) { + log += F(","); + } + } + log += F(")] "); + addLogMove(LOG_LEVEL_INFO, log); + } + #endif // MODBUS_DEBUG +} diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 1256b00c47..d53a4d05d8 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -41,11 +41,16 @@ typedef enum class ModbusTransactionType { // This structure represents a single Modbus request and its associated response. struct Modbus_Transaction { Modbus_Transaction() = default; + Modbus_Transaction(ModbusDEVICE_struct *device) + { + _device = device; + } + Modbus_Transaction(ModbusDEVICE_struct *device, - ModbusTransactionType_t messageType, - uint16_t userId, - void *userData, - void *userState) + ModbusTransactionType_t messageType, + uint16_t userId, + void *userData, + void *userState) { _device = device; _messageType = messageType; @@ -54,6 +59,8 @@ struct Modbus_Transaction { _userState = userState; } + void print(); + ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message void *_userData = nullptr; // Pointer to user (device) data void *_userState = nullptr; // Pointer to user (device) defined state @@ -62,8 +69,8 @@ struct Modbus_Transaction { uint16_t _id = 0; // Unique ID of the request (for debugging) struct ModbusDEVICE_struct *_device = nullptr; // Pointer to the Modbus device requesting the // action - uint16_t _sendframe_length = 0; // Length of the request frame - uint16_t _rcvframe_length = 0; // Expected length of the response frame + uint16_t _sendframe_length = 0; // Length of the request frame + uint16_t _rcvframe_length = 0; // Expected length of the response frame enum ModbusQueueState _state = ModbusQueueState::NOT_QUEUED; // State of the request exchange uint16_t _timeout = 0; // Specified timeout value for the request unsigned long _startTime = 0; // Time the request was issued @@ -72,8 +79,10 @@ struct Modbus_Transaction { }; +typedef Modbus_Transaction *Modbus_transaction_ptr; + // Queue of Modbus request elements -typedef std::list Modbus_RequestQueue; +typedef std::list Modbus_TransactionQueue; // ModbusLINK structure representing a MODBUS LINK @@ -95,41 +104,39 @@ struct ModbusLINK_struct { int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } + bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } - void freeTransactions(struct ModbusDEVICE_struct *device); - uint16_t queueTransaction(Modbus_Transaction *transaction); - void processCommand(); + void freeTransactions(struct ModbusDEVICE_struct *device); + bool newTransaction(ModbusDEVICE_struct *device, + Modbus_transaction_ptr& transaction); + bool queueTransaction(Modbus_Transaction *transaction); + void processQueue(); - int16_t getBaudrate(void) const; + int16_t getBaudrate(void) const; - int16_t getSerialRX(void) const; + int16_t getSerialRX(void) const; - int16_t getSerialTX(void) const; + int16_t getSerialTX(void) const; - int8_t getDerePin(void) const; + int8_t getDerePin(void) const; - bool getCollisionDetect(void) const; + bool getCollisionDetect(void) const; private: - static void dumpQueueElement(Modbus_Transaction *el); static void dumpState(ModbusQueueState_t state); - ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object - Modbus_RequestQueue _requestQueue = {}; // Queue of Modbus requests to process - uint16_t _queueID = 0; // ID for the last request queued - uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object + Modbus_TransactionQueue _transactionQueue = {}; // Queue of Modbus requests to process + uint16_t _queueID = 0; // ID for the last request queued + uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds - uint8_t _dere_pin = 0; // Pin for RS485 direction control - bool _collision_detect = false; // Flag to indicate if collision detection is enabled + uint8_t _dere_pin = 0; // Pin for RS485 direction control + bool _collision_detect = false; // Flag to indicate if collision detection is enabled bool _initialized = false; - - ////uint32_t _reads_pass = 0; // TODO: statistics - ////uint32_t _reads_crc_failed = 0; // TODO: statistics - ////uint32_t _reads_nodata = 0; // TODO: statistics - ////uint8_t _last_error = 0; // TODO: statistics, last error code received from Modbus device - bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to prevent reentrancy issues + bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to + // prevent + // reentrancy issues }; diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index b6142e9a99..2c3a7f103c 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -11,8 +11,9 @@ # include # include "../Helpers/Modbus_mgr.h" +# include "Modbus_mgr.h" -////# define MODBUS_DEBUG +# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -118,37 +119,33 @@ bool ModbusMGR_struct::initialize() // Connect a Modbus device to a Modbus link. A unique device ID is assigned to the device. // Returns a pointer to the Modbus link object and the assigned device ID if connection is successful, otherwise returns false. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *deviceID) +bool ModbusMGR_struct::connect(int linkId, ModbusDEVICE_struct *device) { + bool returnValue = false; + # ifdef MODBUS_DEBUG - String log = F("Modbus: Manager, Connect linkId="); - log += linkId; + String log = strformat(F("Modbus Manager: Connect device= %p to linkId=%d"), device, linkId); # endif // ifdef MODBUS_DEBUG - initialize(); // TODO Initialization sequence to be refactored. - - *deviceID = -1; // Default to -1, currently not used anymore - *link = nullptr; + initialize(); // TODO Initialization sequence to be refactored. - if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS)) { + if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { # ifdef MODBUS_DEBUG - log += F("Invalid linkId"); + log += F(" Invalid linkId"); addLogMove(LOG_LEVEL_ERROR, log); # endif // ifdef MODBUS_DEBUG - return false; } - - if (_modbus_links[linkId].port == ESPEasySerialPort::not_set) { - # ifdef MODBUS_DEBUG + else if (_modbus_links[linkId].port == ESPEasySerialPort::not_set) { + # ifdef MODBUS_DEBUG log += F(" No link available at linkIndex= "); log += linkId; addLogMove(LOG_LEVEL_ERROR, log); - # endif // ifdef MODBUS_DEBUG - return false; + # endif // ifdef MODBUS_DEBUG + } + else { + // No administration to be done on manager or link side + returnValue = true; } - - *deviceID = ++_deviceCounter; - *link = _modbus_links[linkId].link; # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); @@ -162,18 +159,83 @@ bool ModbusMGR_struct::connect(int linkId, ModbusLINK_struct **link, uint8_t *de // Disconnect the Modbus device with the given device ID. // If no other devices are using the same link, the link is also deleted. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusMGR_struct::disconnect(uint8_t deviceID) { +bool ModbusMGR_struct::disconnect(int linkId, ModbusDEVICE_struct*device) { + + bool returnValue = false; + + # ifdef MODBUS_DEBUG dumpAdminInfo(); + String log = strformat(F("Modbus Manager: Disconnect device= %p from linkId= %d "), device, linkId); + # endif // ifdef MODBUS_DEBUG + + if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + } + else { + _modbus_links[linkId].link->freeTransactions(device); + returnValue = true; + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return returnValue; +} + +bool ModbusMGR_struct::newTransaction(int linkId, ModbusDEVICE_struct *device, Modbus_transaction_ptr& transaction) +{ + bool returnValue = false; + # ifdef MODBUS_DEBUG - String log = F("Modbus: Manager, Disconnect device="); - log += deviceID; + String log = strformat(F("Modbus Manager: New transaction for linkId= %d, device= %p"), linkId, device); # endif // ifdef MODBUS_DEBUG + if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + # endif // ifdef MODBUS_DEBUG + } + else { + returnValue = _modbus_links[linkId].link->newTransaction(device, transaction); + # ifdef MODBUS_DEBUG + log += strformat(F(" Transaction pointer= %p"), transaction); + transaction->print(); // Print the transaction details for debugging + # endif // ifdef MODBUS_DEBUG + } # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef MODBUS_DEBUG - return true; + return returnValue; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool ModbusMGR_struct::queueTransaction(int linkId, Modbus_Transaction *transaction) +{ + bool returnValue = false; + + # ifdef MODBUS_DEBUG + String log = strformat(F("Modbus Manager: Queue transaction for linkId= %d "), linkId); + # endif // ifdef MODBUS_DEBUG + + if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + # ifdef MODBUS_DEBUG + log += F("Invalid linkId"); + addLogMove(LOG_LEVEL_ERROR, log); + # endif // ifdef MODBUS_DEBUG + } + else { + returnValue = _modbus_links[linkId].link->queueTransaction(transaction); + } + + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return returnValue; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -184,7 +246,7 @@ void ModbusMGR_struct::processLinks() if (isInitialized()) { for (int i = 0; i < MAX_MODBUS_LINKS; i++) { if ((_modbus_links[i].link != nullptr)) { - _modbus_links[i].link->processCommand(); // Trigger processing of the command queue on the link + _modbus_links[i].link->processQueue(); // Trigger processing of the command queue on the link } } } @@ -195,22 +257,22 @@ void ModbusMGR_struct::processLinks() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::dumpAdminInfo() { - addLogMove(LOG_LEVEL_INFO, F("Modbus: Manager, Dumping admin info")); # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, F("Modbus Manager: Dumping admin info")); // Iterate over the modbus links and dump their info for (int i = 0; i < MAX_MODBUS_LINKS; i++) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus_mgr: Link[%d] Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, RS485Mode=%s, CollisionDetect=%s"), + strformat(F("Modbus Admin: Link[%d] Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, RS485Mode=%s, CollisionDetect=%s"), i, ESPEasySerialPort_toString(_modbus_links[i].port), _modbus_links[i].serial_rx, _modbus_links[i].serial_tx, _modbus_links[i].baudrate, _modbus_links[i].dere_pin, - _modbus_links[i].rs485_mode ? F("Yes") : F("No"), - _modbus_links[i].collision_detect ? F("Yes") : F("No") + FsP(_modbus_links[i].rs485_mode ? F("Yes") : F("No")), + FsP(_modbus_links[i].collision_detect ? F("Yes") : F("No")) )); } diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 1ba83e3dcf..4433e97471 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -9,6 +9,8 @@ # include "../Helpers/Modbus_link.h" # include "../Helpers/_ESPEasy_key_value_store.h" +// Forward declaration of ModbusDEVICE_struct to avoid circular dependency issues +struct ModbusDEVICE_struct; // ModbusMGR structure representing the singleton Modbus Management entity // Thw manager has an overview of all Modbus links and the conneted devices. @@ -20,14 +22,21 @@ typedef struct ModbusMGR_struct { ~ModbusMGR_struct(); bool initialize(); + void processLinks(); - bool connect(int linkId, - ModbusLINK_struct **link, - uint8_t *deviceID); + bool connect(int linkId, + ModbusDEVICE_struct *deviceID); - bool disconnect(uint8_t deviceID); + bool disconnect(int linkId, + ModbusDEVICE_struct*deviceID); + + bool newTransaction(int linkId, + ModbusDEVICE_struct *device, + Modbus_transaction_ptr &transaction); + + bool queueTransaction(int linkId, + Modbus_Transaction *transaction); - void processLinks(); void dumpAdminInfo(); @@ -38,7 +47,7 @@ typedef struct ModbusMGR_struct { private: - static const int MAX_MODBUS_LINKS = 4; // Maximum number of Modbus links supported + static const int MAX_MODBUS_LINKS = 4; // Maximum number of Modbus links supported // Structure representing the information of a Modbus link as managed by the Modbus_mgr // Includes associated ModbusLINK object, link configuration and config storage From 49e803b1d2ed231a884c231959a1d6aed631cb63 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:12:06 +0200 Subject: [PATCH 38/48] P183: Small fixes --- src/src/Helpers/Modbus_link.cpp | 2 +- src/src/Helpers/Modbus_link.h | 23 +++-------------------- src/src/Helpers/Modbus_mgr.cpp | 1 - 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 75ae33ecbd..2085229438 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -341,7 +341,7 @@ const __FlashStringHelper* toString(ModbusQueueState_t state) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Debugging function to dump the queue element contents to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void Modbus_Transaction::print() +void Modbus_Transaction::print() { #ifdef MODBUS_DEBUG diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index d53a4d05d8..e2184dd2c6 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -37,7 +37,7 @@ typedef enum class ModbusTransactionType { } ModbusTransactionType_t; -// Modbus request queue element structure +// Modbus transaction structure // This structure represents a single Modbus request and its associated response. struct Modbus_Transaction { Modbus_Transaction() = default; @@ -46,20 +46,7 @@ struct Modbus_Transaction { _device = device; } - Modbus_Transaction(ModbusDEVICE_struct *device, - ModbusTransactionType_t messageType, - uint16_t userId, - void *userData, - void *userState) - { - _device = device; - _messageType = messageType; - _userId = userId; - _userData = userData; - _userState = userState; - } - - void print(); + void print(); // Print the transaction details for debugging ModbusTransactionType _messageType = ModbusTransactionType::NONE; // Type of Modbus message void *_userData = nullptr; // Pointer to user (device) data @@ -113,13 +100,9 @@ struct ModbusLINK_struct { void processQueue(); int16_t getBaudrate(void) const; - int16_t getSerialRX(void) const; - int16_t getSerialTX(void) const; - int8_t getDerePin(void) const; - bool getCollisionDetect(void) const; private: @@ -127,7 +110,7 @@ struct ModbusLINK_struct { static void dumpState(ModbusQueueState_t state); ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object - Modbus_TransactionQueue _transactionQueue = {}; // Queue of Modbus requests to process + Modbus_TransactionQueue _transactionQueue = {}; // Queue of Modbus transactions to process uint16_t _queueID = 0; // ID for the last request queued uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 2c3a7f103c..2deb7e33f9 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -11,7 +11,6 @@ # include # include "../Helpers/Modbus_mgr.h" -# include "Modbus_mgr.h" # define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG From 67bdaf74746ac4f43b0f03b49ac73b891c92b215 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:51:50 +0200 Subject: [PATCH 39/48] P183: Conditional disabling of blocking wait support --- src/_P183_modbus.ino | 11 +++++++++++ src/src/Helpers/Modbus_device.cpp | 8 ++++---- src/src/Helpers/Modbus_link.cpp | 11 +++++++++-- src/src/Helpers/Modbus_mgr.cpp | 2 +- src/src/PluginStructs/P183_data_struct.cpp | 8 +++++++- src/src/PluginStructs/P183_data_struct.h | 4 ++++ 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index ed8469f7d1..425bb0ca10 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -206,7 +206,13 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterCache(address); } else { + #ifdef P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged + #else + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read command with invalid address")); + value = 0; + #endif // ifdef P183_ALLOW_MODBUS_WAIT + } if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -284,7 +290,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterCache(address); } else { + #ifdef P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged + #else + addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config command invalid address")); + value = 0; + #endif // ifdef P183_ALLOW_MODBUS_WAIT } string = String(value); success = true; diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 0797074f6e..46ed00b6e8 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -58,7 +58,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t tas if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus: Device Init, Slave address = %u, This = %p, deviceID = %u, linkId=%d, taskIndex=%d"), + strformat(F("Modbus Device: Init, Slave address = %u, This = %p, deviceID = %u, linkId=%d, taskIndex=%d"), slaveAddress, this, _deviceID, linkId, taskIndex)); } # endif // MODBUS_DEBUG @@ -258,12 +258,12 @@ void ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) ModbusResultState resultState = ModbusResultState::Error; // Default to error unless proven otherwise if (req == nullptr) { - addLogMove(LOG_LEVEL_INFO, F("Modbus: ERROR, Null pointer passed in callback")); + addLogMove(LOG_LEVEL_INFO, F("Modbus Device: ERROR, Null pointer passed in callback")); return; } # ifdef MODBUS_DEBUG - String log = strformat(F("Modbus: Device callback, device= %d, link= %d, Request= %d, Message= %d"), + String log = strformat(F("Modbus Device: Device callback, device= %d, link= %d, Request= %d, Message= %d"), _deviceID, _linkId, req->_id, @@ -458,7 +458,7 @@ void ModbusDEVICE_struct::dump_buffer(const uint8_t *buffer, size_t length) { # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Modbus: Device, Dumping buffer: "); + String log = F("Modbus Device Dumping buffer: "); for (size_t i = 0; i < length; ++i) { log += String(buffer[i], HEX); diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 2085229438..39eddd14a2 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -214,11 +214,14 @@ void ModbusLINK_struct::processQueue() switch ((*it)->_state) { + // Transaction is created but not yet queued. Skip it untill it is queued or discarded. case ModbusQueueState::NOT_QUEUED: { it++; // Not queued yet, move to the next transaction in the queue } + // First tranaction in queue ready to be transmitted. No other transaction is already being processed. + // Send the request and mark the transaction as sent, waiting for response. case ModbusQueueState::QUEUED: { // Send the request @@ -237,6 +240,7 @@ void ModbusLINK_struct::processQueue() break; } + // Transaction is sent, waiting for response. Check if a response is received or if the timeout has expired. case ModbusQueueState::MESSAGE_SENT: { // Waiting for response @@ -247,6 +251,9 @@ void ModbusLINK_struct::processQueue() else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { // Timeout expired (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, (*it)->_id, + _easySerial->available(), (*it)->_rcvframe_length)); } else { // Still waiting @@ -265,10 +272,10 @@ void ModbusLINK_struct::processQueue() break; } + // All remaining states indicate that the transaction can be removed from the queue and destroyed. default: { (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; - delete (*it); // destroy the queue element it = _transactionQueue.erase(it); // Remove it from the list break; @@ -341,7 +348,7 @@ const __FlashStringHelper* toString(ModbusQueueState_t state) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Debugging function to dump the queue element contents to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void Modbus_Transaction::print() +void Modbus_Transaction::print() { #ifdef MODBUS_DEBUG diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 2deb7e33f9..32db6690de 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -12,7 +12,7 @@ # include # include "../Helpers/Modbus_mgr.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index 635432d867..da107819fd 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -165,6 +165,10 @@ bool P183_data_struct::plugin_once_per_second(EventStruct *event) return true; // Cache not used, nothing to do } + if (P183_CACHE_SIZE > P183_CACHE_SIZE_MAX) { + return false; // Cache size exceeds maximum allowed size + } + // Queue a read request for the cache values. The result will be processed in the task timer event. _cacheStart = P183_CACHE_START; _cacheSize = P183_CACHE_SIZE; @@ -247,8 +251,9 @@ void P183_data_struct::scan_next_module() } } +#ifdef P183_ALLOW_MODBUS_WAIT /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Read a Modbus register from the device. Wait untial the data is available +// Read a Modbus register from the device. Block until the data is available // Warning: this may take time as we wait for the Modbus message to be exchanged /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t P183_data_struct::readRegisterWait(uint16_t address) { @@ -273,6 +278,7 @@ uint16_t P183_data_struct::readRegisterWait(uint16_t address) { return value; } +#endif // ifdef P183_ALLOW_MODBUS_WAIT /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint16_t P183_data_struct::readRegisterCache(uint16_t address) diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 2dbb8c3eb1..2a7c72c9ed 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -9,6 +9,8 @@ # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG +////# define P183_ALLOW_MODBUS_WAIT + # include "../Helpers/Modbus_device.h" // Plugin configuration parameters @@ -63,7 +65,9 @@ struct P183_data_struct : public PluginTaskData_base { uint16_t start_reg, uint16_t end_reg); void scan_modbus(); + #ifdef P183_ALLOW_MODBUS_WAIT uint16_t readRegisterWait(uint16_t address); + #endif // ifdef P183_ALLOW_MODBUS_WAIT uint16_t readRegisterCache(uint16_t address); void writeRegister(uint16_t address, From 485c46bc023aa1d56c81f68279a14954742d6310 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:48:55 +0200 Subject: [PATCH 40/48] P183: Change queue from pointer type to Transaction type (remove indirection) --- src/_P183_modbus.ino | 2 +- src/src/Helpers/Modbus_link.cpp | 85 +++++++++++++++++---------------- src/src/Helpers/Modbus_link.h | 2 +- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 425bb0ca10..8dc6a5075b 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -305,7 +305,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SHOW_CONFIG: { - string += strformat(F("Modbus %d"), P183_LINK_ID); + string += strformat(F("Modbus %d
Addr: %d"), P183_LINK_ID, P183_DEV_ID); success = true; break; } diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 39eddd14a2..80ee101159 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -13,7 +13,7 @@ # include "../Helpers/Modbus_device.h" # include "../Helpers/Modbus_link.h" -////# define MODBUS_DEBUG +# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -36,13 +36,13 @@ ModbusLINK_struct::~ModbusLINK_struct() { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::reset() { for (auto it = _transactionQueue.begin(); it != _transactionQueue.end(); it++) { - (*it)->_state = ModbusQueueState::ERROR_OCCURRED; + (*it)._state = ModbusQueueState::ERROR_OCCURRED; - if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback(*it); // Notify the device that the request finished with an error + if ((*it)._device != nullptr) { + (*it)._device->linkCallback(&(*it)); // Notify the device that the request finished with an error } - delete (*it); // destroy the queue element + ////delete (*it); // destroy the queue element it = _transactionQueue.erase(it); } } @@ -128,13 +128,16 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) } for ( auto it = _transactionQueue.begin(); it != _transactionQueue.end(); ++it ) { - if ((*it)->_device == device) { - (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed + if ((*it)._device == device) { + (*it)._state = ModbusQueueState::READY_FOR_DESTROY; // Mark to be destroyed } } - processQueue(); // Trigger processing of the command queue to free the marked transactions + processQueue(); // Trigger processing of the command queue to free the marked transactions } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device, Modbus_transaction_ptr& transaction) { bool success = false; @@ -143,15 +146,15 @@ bool ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device, Modbus_trans addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to create transaction for uninitialized link")); } else { - Modbus_Transaction *tr = new (std::nothrow) Modbus_Transaction(device); // Create a new transaction - tr->_id = ++(_queueID); // Assign a unique ID to the transaction - tr->_timeout = _modbus_timeout; // Default timeout value - _transactionQueue.push_back(tr); // Put a new transaction at the end of the queue - transaction = tr; // Return the pointer to the new transaction + + auto& tr = _transactionQueue.emplace_back(device); // Put a new transaction at the end of the queue + tr._id = ++(_queueID); // Assign a unique ID to the transaction + tr._timeout = _modbus_timeout; // Default timeout value + transaction = &tr; // Return the pointer to the new transaction # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, F("Modbus Link: new transaction")); # endif // MODBUS_DEBUG - tr->print(); // Print the transaction details for debugging + tr.print(); // Print the transaction details for debugging success = true; } return success; @@ -209,10 +212,10 @@ void ModbusLINK_struct::processQueue() while ((it != _transactionQueue.end()) && !busy) { # ifdef MODBUS_DEBUG - (*it)->print(); + (*it).print(); # endif // MODBUS_DEBUG - switch ((*it)->_state) + switch ((*it)._state) { // Transaction is created but not yet queued. Skip it untill it is queued or discarded. case ModbusQueueState::NOT_QUEUED: @@ -233,10 +236,10 @@ void ModbusLINK_struct::processQueue() _easySerial->read(); } } - _easySerial->write((*it)->_sendframe, (*it)->_sendframe_length); - (*it)->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response - (*it)->_startTime = millis(); // Record the time the transaction - busy = true; // Only process one request at a time + _easySerial->write((*it)._sendframe, (*it)._sendframe_length); + (*it)._state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response + (*it)._startTime = millis(); // Record the time the transaction + busy = true; // Only process one request at a time break; } @@ -244,16 +247,16 @@ void ModbusLINK_struct::processQueue() case ModbusQueueState::MESSAGE_SENT: { // Waiting for response - if (_easySerial->available() >= (*it)->_rcvframe_length) { - _easySerial->readBytes((*it)->_rcvframe, (*it)->_rcvframe_length); - (*it)->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received + if (_easySerial->available() >= (*it)._rcvframe_length) { + _easySerial->readBytes((*it)._rcvframe, (*it)._rcvframe_length); + (*it)._state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received } - else if (timePassedSince((*it)->_startTime) > (*it)->_timeout) { + else if (timePassedSince((*it)._startTime) > (*it)._timeout) { // Timeout expired - (*it)->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + (*it)._state = ModbusQueueState::ERROR_OCCURRED; // Mark as error addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, (*it)->_id, - _easySerial->available(), (*it)->_rcvframe_length)); + strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, (*it)._id, + _easySerial->available(), (*it)._rcvframe_length)); } else { // Still waiting @@ -261,10 +264,10 @@ void ModbusLINK_struct::processQueue() } if (!busy) { // We received a response or an error occurred, process the result - if ((*it)->_device != nullptr) { - (*it)->_device->linkCallback((*it)); // Notify the device that a response was received + if ((*it)._device != nullptr) { + (*it)._device->linkCallback(&(*it)); // Notify the device that a response was received } - (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; + (*it)._state = ModbusQueueState::READY_FOR_DESTROY; } else { it++; // Move to the next transaction in the queue @@ -275,15 +278,14 @@ void ModbusLINK_struct::processQueue() // All remaining states indicate that the transaction can be removed from the queue and destroyed. default: { - (*it)->_state = ModbusQueueState::READY_FOR_DESTROY; - delete (*it); // destroy the queue element - it = _transactionQueue.erase(it); // Remove it from the list + (*it)._state = ModbusQueueState::READY_FOR_DESTROY; + it = _transactionQueue.erase(it); // Remove it from the list break; } - } // switch - } // next iterarion + } // switch + } // next iterarion - _processing = false; // Clear processing flag to allow new processing cycles + _processing = false; // Clear processing flag to allow new processing cycles return; } @@ -325,6 +327,8 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { } const __FlashStringHelper* toString(ModbusQueueState_t state) { + # ifdef MODBUS_DEBUG + switch (state) { case ModbusQueueState::NOT_QUEUED: @@ -340,17 +344,16 @@ const __FlashStringHelper* toString(ModbusQueueState_t state) { case ModbusQueueState::READY_FOR_DESTROY: return F("READY_FOR_DESTROY"); } + # endif // MODBUS_DEBUG return F(""); } -#endif // if FEATURE_MODBUS - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Debugging function to dump the queue element contents to the log /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void Modbus_Transaction::print() { - #ifdef MODBUS_DEBUG + # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = strformat(F("Modbus Transaction: [ID=%u, Device=%p, State="), _id, _device); @@ -376,5 +379,7 @@ void Modbus_Transaction::print() log += F(")] "); addLogMove(LOG_LEVEL_INFO, log); } - #endif // MODBUS_DEBUG + # endif // MODBUS_DEBUG } + +#endif // if FEATURE_MODBUS diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index e2184dd2c6..87c2b95b3d 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -69,7 +69,7 @@ struct Modbus_Transaction { typedef Modbus_Transaction *Modbus_transaction_ptr; // Queue of Modbus request elements -typedef std::list Modbus_TransactionQueue; +typedef std::list Modbus_TransactionQueue; // ModbusLINK structure representing a MODBUS LINK From e7eac10d06f300c8632c25556a4c47f5537edf41 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:47:41 +0200 Subject: [PATCH 41/48] P183: Rework --- src/_P183_modbus.ino | 8 +- src/src/Helpers/Modbus_device.cpp | 12 +- src/src/Helpers/Modbus_link.cpp | 100 +++++------ src/src/Helpers/Modbus_link.h | 23 ++- src/src/Helpers/Modbus_mgr.cpp | 199 +++++++++++---------- src/src/Helpers/Modbus_mgr.h | 23 +-- src/src/PluginStructs/P183_data_struct.cpp | 2 +- src/src/PluginStructs/P183_data_struct.h | 6 +- 8 files changed, 195 insertions(+), 178 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 8dc6a5075b..34efb9a048 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -206,12 +206,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterCache(address); } else { - #ifdef P183_ALLOW_MODBUS_WAIT + #if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged #else addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read command with invalid address")); value = 0; - #endif // ifdef P183_ALLOW_MODBUS_WAIT + #endif // if P183_ALLOW_MODBUS_WAIT } @@ -290,12 +290,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterCache(address); } else { - #ifdef P183_ALLOW_MODBUS_WAIT + #if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged #else addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config command invalid address")); value = 0; - #endif // ifdef P183_ALLOW_MODBUS_WAIT + #endif // if P183_ALLOW_MODBUS_WAIT } string = String(value); success = true; diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 46ed00b6e8..2019e051ad 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -96,8 +96,7 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, if (!isInitialized()) { return false; } - Modbus_Transaction *transaction = nullptr; // Declare the request pointer - ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { return false; // Failed to allocate a request structure @@ -134,8 +133,7 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, return false; } - Modbus_Transaction *transaction = nullptr; // Declare the request pointer - ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { return false; // Failed to allocate a request structure @@ -159,8 +157,7 @@ bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, return false; } - Modbus_Transaction *transaction = nullptr; // Declare the request pointer - ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { return false; // Failed to allocate a request structure @@ -208,8 +205,7 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, return false; } - Modbus_Transaction *transaction = nullptr; // Declare the request pointer - ModbusMGR_singleton.newTransaction(_linkId, this, transaction); + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { return false; // Failed to allocate a request structure diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 80ee101159..45e0ae91ec 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -13,7 +13,7 @@ # include "../Helpers/Modbus_device.h" # include "../Helpers/Modbus_link.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG @@ -53,7 +53,7 @@ void ModbusLINK_struct::reset() { bool ModbusLINK_struct::init(const ESPEasySerialPort port, const int8_t serial_rx, const int8_t serial_tx, - uint16_t baudrate, + uint32_t baudrate, int8_t dere_pin, bool collision_detect) { int available = 0; @@ -120,7 +120,7 @@ bool ModbusLINK_struct::init(const ESPEasySerialPort port, // Free all queued transactions for the given device // This shall be called when a device is removed to free the pending requests for the device preventing callback issues. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) +void ModbusLINK_struct::freeTransactions(const ModbusDEVICE_struct *device) { if (!isInitialized()) { addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to free transactions on uninitialized link")); @@ -138,26 +138,22 @@ void ModbusLINK_struct::freeTransactions(ModbusDEVICE_struct *device) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device, Modbus_transaction_ptr& transaction) +Modbus_transaction_ptr ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device) { - bool success = false; - if (!isInitialized()) { addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to create transaction for uninitialized link")); + return nullptr; } else { - auto& tr = _transactionQueue.emplace_back(device); // Put a new transaction at the end of the queue - tr._id = ++(_queueID); // Assign a unique ID to the transaction - tr._timeout = _modbus_timeout; // Default timeout value - transaction = &tr; // Return the pointer to the new transaction + tr._id = ++(_queueID); // Assign a unique ID to the transaction + tr._timeout = _modbus_timeout; // Default timeout value # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, F("Modbus Link: new transaction")); + tr.print(); // Print the transaction details for debugging # endif // MODBUS_DEBUG - tr.print(); // Print the transaction details for debugging - success = true; + return &tr; } - return success; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -203,24 +199,25 @@ bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { void ModbusLINK_struct::processQueue() { if (_processing || !isInitialized() || (_transactionQueue.empty())) { - return; // Serial port not initialized or queue is empty, nothing to process + return; // Serial port not initialized or queue is empty, nothing to process } - _processing = true; // Set processing flag to prevent reentrancy issues + _processing = true; // Set processing flag to prevent reentrancy issues - auto it = _transactionQueue.begin(); // Iterator for the request queue - bool busy = false; // Only process one request at a time + auto it = _transactionQueue.begin(); // Iterator for the request queue + bool linkOccupied = false; // Only process one request at a time - while ((it != _transactionQueue.end()) && !busy) { + while ((it != _transactionQueue.end()) && !linkOccupied) { # ifdef MODBUS_DEBUG - (*it).print(); + it->print(); # endif // MODBUS_DEBUG - switch ((*it)._state) + switch (it->_state) { // Transaction is created but not yet queued. Skip it untill it is queued or discarded. case ModbusQueueState::NOT_QUEUED: { it++; // Not queued yet, move to the next transaction in the queue + break; } // First tranaction in queue ready to be transmitted. No other transaction is already being processed. @@ -236,10 +233,11 @@ void ModbusLINK_struct::processQueue() _easySerial->read(); } } - _easySerial->write((*it)._sendframe, (*it)._sendframe_length); - (*it)._state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response - (*it)._startTime = millis(); // Record the time the transaction - busy = true; // Only process one request at a time + _easySerial->write(it->_sendframe, it->_sendframe_length); + it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response + it->_startTime = millis(); // Record the time the transaction + linkOccupied = true; // Only process one request at a time + it++; // Proforma move to the next transaction in the queue break; } @@ -247,69 +245,71 @@ void ModbusLINK_struct::processQueue() case ModbusQueueState::MESSAGE_SENT: { // Waiting for response - if (_easySerial->available() >= (*it)._rcvframe_length) { - _easySerial->readBytes((*it)._rcvframe, (*it)._rcvframe_length); - (*it)._state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received + if (_easySerial->available() >= (it->_rcvframe_length)) { + _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); + it->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received } - else if (timePassedSince((*it)._startTime) > (*it)._timeout) { + else if (timePassedSince(it->_startTime) > it->_timeout) { // Timeout expired - (*it)._state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, (*it)._id, - _easySerial->available(), (*it)._rcvframe_length)); + strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, it->_id, + _easySerial->available(), it->_rcvframe_length)); + # endif // MODBUS_DEBUG } else { // Still waiting - busy = true; // Only process one request at a time + linkOccupied = true; // Only process one request at a time } - if (!busy) { // We received a response or an error occurred, process the result - if ((*it)._device != nullptr) { - (*it)._device->linkCallback(&(*it)); // Notify the device that a response was received + if (!linkOccupied) { // We received a response or an error occurred, process the result + if (it->_device != nullptr) { + it->_device->linkCallback(&(*it)); // Notify the device that a response was received } - (*it)._state = ModbusQueueState::READY_FOR_DESTROY; - } - else { - it++; // Move to the next transaction in the queue + it->_state = ModbusQueueState::READY_FOR_DESTROY; } + it++; // Move to the next transaction in the queue break; } // All remaining states indicate that the transaction can be removed from the queue and destroyed. - default: + case ModbusQueueState::READY_FOR_DESTROY: + case ModbusQueueState::ERROR_OCCURRED: + case ModbusQueueState::RESPONSE_RECEIVED: { - (*it)._state = ModbusQueueState::READY_FOR_DESTROY; - it = _transactionQueue.erase(it); // Remove it from the list + it->_state = ModbusQueueState::READY_FOR_DESTROY; + it = _transactionQueue.erase(it); // Remove it from the list break; } - } // switch - } // next iterarion + } // switch + } // next iterarion - _processing = false; // Clear processing flag to allow new processing cycles + _processing = false; // Clear processing flag to allow new processing cycles return; } -int16_t ModbusLINK_struct::getBaudrate(void) const +int16_t ModbusLINK_struct::getBaudrate() const { return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; } -int16_t ModbusLINK_struct::getSerialRX(void) const +int16_t ModbusLINK_struct::getSerialRX() const { return _easySerial != nullptr ? _easySerial->getRxPin() : -1; } -int16_t ModbusLINK_struct::getSerialTX(void) const +int16_t ModbusLINK_struct::getSerialTX() const { return _easySerial != nullptr ? _easySerial->getTxPin() : -1; } -int8_t ModbusLINK_struct::getDerePin(void) const +int8_t ModbusLINK_struct::getDerePin() const { return _dere_pin; } -bool ModbusLINK_struct::getCollisionDetect(void) const +bool ModbusLINK_struct::getCollisionDetect() const { return _collision_detect; } diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 87c2b95b3d..5ed2e1e1d9 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -87,23 +87,22 @@ struct ModbusLINK_struct { bool init(const ESPEasySerialPort port, const int8_t serial_rx, const int8_t serial_tx, - uint16_t baudrate, + uint32_t baudrate, int8_t dere_pin, bool collision_detect = false); - bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } + bool isInitialized() const { return (_easySerial != nullptr) && _initialized; } - void freeTransactions(struct ModbusDEVICE_struct *device); - bool newTransaction(ModbusDEVICE_struct *device, - Modbus_transaction_ptr& transaction); - bool queueTransaction(Modbus_Transaction *transaction); - void processQueue(); + void freeTransactions(const ModbusDEVICE_struct *device); + Modbus_transaction_ptr newTransaction(ModbusDEVICE_struct *device); + bool queueTransaction(Modbus_Transaction *transaction); + void processQueue(); - int16_t getBaudrate(void) const; - int16_t getSerialRX(void) const; - int16_t getSerialTX(void) const; - int8_t getDerePin(void) const; - bool getCollisionDetect(void) const; + int16_t getBaudrate() const; + int16_t getSerialRX() const; + int16_t getSerialTX() const; + int8_t getDerePin() const; + bool getCollisionDetect() const; private: diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 32db6690de..dd6e677b34 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -12,11 +12,15 @@ # include # include "../Helpers/Modbus_mgr.h" -////# define MODBUS_DEBUG +# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG + +// Macro to check if a linkId is valid and if the link is initialized +# define MODBUS_MGR_VALID_LINK_ID(id) (validLinkId(id) && (_modbus_links[id].link != nullptr)) + # define MODBUS_MAX_BAUDRATE_SEL 8 // Key indices for storing Modbus link settings in the key-value store @@ -33,20 +37,15 @@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct_t ModbusMGR_singleton = {}; -int modbus_storageValueToBaudrate(uint8_t baudrate_setting); -uint8_t modbus_baudrateToStorageValue(int baudrate); +uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting); +uint8_t modbus_baudrateToStorageValue(uint32_t baudrate); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Constructor of the Modbus manager class, initializes the internal data structures /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModbusMGR_struct::ModbusMGR_struct() -{ - # ifdef MODBUS_DEBUG - String log = F("Modbus: Manager, Constructor "); - # endif // ifdef MODBUS_DEBUG - -} +{} /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Destructor of the Modbus manager class, should not be called as this is intended to be a singleton @@ -84,24 +83,30 @@ bool ModbusMGR_struct::initialize() _modbus_links[i].link = nullptr; // Initialize link pointer to a safe value // Create a key-value store for the link and load the persisted settings _modbus_links[i].kvs = new (std::nothrow) ESPEasy_key_value_store; - _modbus_links[i].kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); - _modbus_links[i].kvs->getValue(MODBUS_PORT_KEY_INDEX, val); - _modbus_links[i].port = static_cast(val); - _modbus_links[i].kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i].serial_rx); - _modbus_links[i].kvs->getValue(MODBUS_TX_KEY_INDEX, _modbus_links[i].serial_tx); - _modbus_links[i].kvs->getValue(MODBUS_BAUDRATE_KEY_INDEX, _modbus_links[i].baudrate); - _modbus_links[i].kvs->getValue(MODBUS_DERE_PIN_KEY_INDEX, _modbus_links[i].dere_pin); - _modbus_links[i].kvs->getValue(MODBUS_COLLISION_DETECT_KEY_INDEX, _modbus_links[i].collision_detect); + + if (_modbus_links[i].kvs != nullptr) { + _modbus_links[i].kvs->load(SettingsType::Enum::ModbusInterfaceSettings_Type, i, 0, 0); + _modbus_links[i].kvs->getValue(MODBUS_PORT_KEY_INDEX, val); + _modbus_links[i].port = static_cast(val); + _modbus_links[i].kvs->getValue(MODBUS_RX_KEY_INDEX, _modbus_links[i].serial_rx); + _modbus_links[i].kvs->getValue(MODBUS_TX_KEY_INDEX, _modbus_links[i].serial_tx); + _modbus_links[i].kvs->getValue(MODBUS_BAUDRATE_KEY_INDEX, _modbus_links[i].baudrate); + _modbus_links[i].kvs->getValue(MODBUS_DERE_PIN_KEY_INDEX, _modbus_links[i].dere_pin); + _modbus_links[i].kvs->getValue(MODBUS_COLLISION_DETECT_KEY_INDEX, _modbus_links[i].collision_detect); + } // Create the Modbus link object if a port is configured for the link if (_modbus_links[i].port != ESPEasySerialPort::not_set) { _modbus_links[i].link = new (std::nothrow) ModbusLINK_struct(); - _modbus_links[i].link->init(_modbus_links[i].port, - _modbus_links[i].serial_rx, - _modbus_links[i].serial_tx, - _modbus_links[i].baudrate, - _modbus_links[i].dere_pin, - _modbus_links[i].collision_detect); + + if (_modbus_links[i].link != nullptr) { + _modbus_links[i].link->init(_modbus_links[i].port, + _modbus_links[i].serial_rx, + _modbus_links[i].serial_tx, + _modbus_links[i].baudrate, + _modbus_links[i].dere_pin, + _modbus_links[i].collision_detect); + } } } @@ -118,7 +123,7 @@ bool ModbusMGR_struct::initialize() // Connect a Modbus device to a Modbus link. A unique device ID is assigned to the device. // Returns a pointer to the Modbus link object and the assigned device ID if connection is successful, otherwise returns false. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusMGR_struct::connect(int linkId, ModbusDEVICE_struct *device) +bool ModbusMGR_struct::connect(int linkId, const ModbusDEVICE_struct *device) { bool returnValue = false; @@ -128,7 +133,7 @@ bool ModbusMGR_struct::connect(int linkId, ModbusDEVICE_struct *device) initialize(); // TODO Initialization sequence to be refactored. - if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { # ifdef MODBUS_DEBUG log += F(" Invalid linkId"); addLogMove(LOG_LEVEL_ERROR, log); @@ -151,14 +156,14 @@ bool ModbusMGR_struct::connect(int linkId, ModbusDEVICE_struct *device) dumpAdminInfo(); # endif // ifdef MODBUS_DEBUG - return true; + return returnValue; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Disconnect the Modbus device with the given device ID. // If no other devices are using the same link, the link is also deleted. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool ModbusMGR_struct::disconnect(int linkId, ModbusDEVICE_struct*device) { +bool ModbusMGR_struct::disconnect(int linkId, const ModbusDEVICE_struct*device) { bool returnValue = false; @@ -167,7 +172,7 @@ bool ModbusMGR_struct::disconnect(int linkId, ModbusDEVICE_struct*device) { String log = strformat(F("Modbus Manager: Disconnect device= %p from linkId= %d "), device, linkId); # endif // ifdef MODBUS_DEBUG - if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { # ifdef MODBUS_DEBUG log += F("Invalid linkId"); addLogMove(LOG_LEVEL_ERROR, log); @@ -184,21 +189,21 @@ bool ModbusMGR_struct::disconnect(int linkId, ModbusDEVICE_struct*device) { return returnValue; } -bool ModbusMGR_struct::newTransaction(int linkId, ModbusDEVICE_struct *device, Modbus_transaction_ptr& transaction) +Modbus_transaction_ptr ModbusMGR_struct::newTransaction(int linkId, ModbusDEVICE_struct *device) { - bool returnValue = false; + Modbus_transaction_ptr transaction = nullptr; # ifdef MODBUS_DEBUG String log = strformat(F("Modbus Manager: New transaction for linkId= %d, device= %p"), linkId, device); # endif // ifdef MODBUS_DEBUG - if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { # ifdef MODBUS_DEBUG log += F("Invalid linkId"); # endif // ifdef MODBUS_DEBUG } else { - returnValue = _modbus_links[linkId].link->newTransaction(device, transaction); + transaction = _modbus_links[linkId].link->newTransaction(device); # ifdef MODBUS_DEBUG log += strformat(F(" Transaction pointer= %p"), transaction); transaction->print(); // Print the transaction details for debugging @@ -208,7 +213,7 @@ bool ModbusMGR_struct::newTransaction(int linkId, ModbusDEVICE_struct *device, M # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef MODBUS_DEBUG - return returnValue; + return transaction; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -221,7 +226,7 @@ bool ModbusMGR_struct::queueTransaction(int linkId, Modbus_Transaction *transact String log = strformat(F("Modbus Manager: Queue transaction for linkId= %d "), linkId); # endif // ifdef MODBUS_DEBUG - if ((linkId < 0) || (linkId >= MAX_MODBUS_LINKS) || (_modbus_links[linkId].link == nullptr)) { + if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { # ifdef MODBUS_DEBUG log += F("Invalid linkId"); addLogMove(LOG_LEVEL_ERROR, log); @@ -293,7 +298,7 @@ void ModbusMGR_struct::show_modbus_interfaces() constexpr int optionBaudCount = static_cast(NR_ELEMENTS(options_baudrate)); for (int i = 0; i < optionBaudCount; ++i) { - options_baudrate[i] = modbus_storageValueToBaudrate(i); + options_baudrate[i] = modbus_storageValueToBaudrate(i); // Fill the baudrate options with actual baudrate values } const FormSelectorOptions baudselector(optionBaudCount, options_baudrate); @@ -357,9 +362,20 @@ void ModbusMGR_struct::show_modbus_interfaces() // The first value is 1200 baud, and each subsequent value doubles the baudrate up to 115200 baud. // Values outside this range will be mapped to the closest valid value. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -int modbus_storageValueToBaudrate(uint8_t baudrate_setting) { +uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting) { if ((baudrate_setting > 7) || (baudrate_setting < 0)) { return 9600; } - return 1200 << baudrate_setting; + switch (baudrate_setting) { + case 0: return 1200; + case 1: return 2400; + case 2: return 4800; + case 3: return 9600; + case 4: return 19200; + case 5: return 38400; + case 6: return 57600; + case 7: return 115200; + default: + return 9600; // Default to a safe value if the setting is invalid + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -368,16 +384,15 @@ int modbus_storageValueToBaudrate(uint8_t baudrate_setting) { // The first value is 1200 baud, and each subsequent value doubles the baudrate up to 115200 baud. // Values outside this range will be mapped to the closest valid value. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -uint8_t modbus_baudrateToStorageValue(int baudrate) { - if ((baudrate < 1200) || (baudrate > 115200)) { return modbus_baudrateToStorageValue(9600); } - int bd = 1200; - uint8_t index = 0; - - while (bd < baudrate) { - bd *= 2; - ++index; - } - return index; +uint8_t modbus_baudrateToStorageValue(uint32_t baudrate) { + if (baudrate <= 1200) { return 0; } + if (baudrate <= 2400) { return 1; } + if (baudrate <= 4800) { return 2; } + if (baudrate <= 9600) { return 3; } + if (baudrate <= 19200) { return 4; } + if (baudrate <= 38400) { return 5; } + if (baudrate <= 57600) { return 6; } + return 7; // 115200 } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -389,7 +404,7 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) int portCount = 1; // Create mapping table from dropdown enum index to actual port identifier, index 0 is reserved for "Not set" - int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; + int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)] {}; portMap[0] = 0; // Map the "Not set" option to index 0 @@ -401,14 +416,13 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { int port_setting = 0; - int baudrate_setting = 0; - int tx_setting = 0; - int rx_setting = 0; - int dere_setting = 0; - bool collision_detect_setting = false; + int baudrate_setting = _modbus_links[link].baudrate; + int tx_setting = _modbus_links[link].serial_tx; + int rx_setting = _modbus_links[link].serial_rx; + int dere_setting = _modbus_links[link].dere_pin; + bool collision_detect_setting = _modbus_links[link].collision_detect; bool settingsChanged = false; - for (int i = 0; i < NR_ELEMENTS(portMap); i++) { if (portMap[i] == static_cast(_modbus_links[link].port)) { port_setting = i; @@ -416,39 +430,30 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) } } - if (update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting)) { - settingsChanged |= (portMap[port_setting] != static_cast(_modbus_links[link].port)); - } + update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting, &settingsChanged); baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); - - if (update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting)) { - settingsChanged |= (modbus_storageValueToBaudrate(baudrate_setting) != _modbus_links[link].baudrate); - } + update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting, &settingsChanged); tx_setting = _modbus_links[link].serial_tx; - - if (update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting)) { - settingsChanged |= (tx_setting != _modbus_links[link].serial_tx); - } + update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting, &settingsChanged); rx_setting = _modbus_links[link].serial_rx; - - if (update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting)) { - settingsChanged |= (rx_setting != _modbus_links[link].serial_rx); - } + update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting, &settingsChanged); dere_setting = _modbus_links[link].dere_pin; - if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting)) { - settingsChanged |= (dere_setting != _modbus_links[link].dere_pin); - # ifdef ESP32 + if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting, &settingsChanged)) + { + # ifdef ESP32 // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); settingsChanged |= (collision_detect_setting != _modbus_links[link].collision_detect); - # endif // ifdef ESP32 + # endif // ifdef ESP32 } - if (settingsChanged) { + if ((port_setting < 0) || (port_setting > NR_ELEMENTS(portMap))) { + port_setting = 0; // Reset to "Not set" if the selected port is invalid + } setLink(link, static_cast(portMap[port_setting]), rx_setting, @@ -487,31 +492,46 @@ bool ModbusMGR_struct::setLink(const int linkIndex, )); # endif // ifdef MODBUS_DEBUG - if ((linkIndex < 0) || (linkIndex >= MAX_MODBUS_LINKS)) { + if (!validLinkId(linkIndex)) { # ifdef MODBUS_DEBUG log += strformat(F("Invalid link for linkIndex=%d"), linkIndex); addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef MODBUS_DEBUG - return false; // Invalid link index + return false; // Invalid link index } - if (_modbus_links[linkIndex].link != nullptr) { // Sanity check for successful creation - // (re)initialize the new link - if (!_modbus_links[linkIndex].link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { - return false; // Initialization failed + // Check of the link is being disabled (port set to not_set) or enabled (port set to a valid port) + if (_modbus_links[linkIndex].port == ESPEasySerialPort::not_set) { + if ((port != ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link == nullptr)) { + _modbus_links[linkIndex].link = new (std::nothrow) ModbusLINK_struct(); } - else { - // Store the link parameters - _modbus_links[linkIndex].port = port; - _modbus_links[linkIndex].serial_rx = serial_rx; - _modbus_links[linkIndex].serial_tx = serial_tx; - _modbus_links[linkIndex].baudrate = baudrate; - _modbus_links[linkIndex].dere_pin = dere_pin; - _modbus_links[linkIndex].rs485_mode = (dere_pin != -1); - _modbus_links[linkIndex].collision_detect = collision_detect; + } + else if ((port == ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link != nullptr)) { + delete _modbus_links[linkIndex].link; + _modbus_links[linkIndex].link = nullptr; + } + + // If the link object is enabled (has a valid pointer), initialize it with the new parameters + if (_modbus_links[linkIndex].link != nullptr) { + if (!_modbus_links[linkIndex].link->init(port, serial_rx, serial_tx, baudrate, dere_pin, collision_detect)) { + # ifdef MODBUS_DEBUG + log += strformat(F("Link initialization failed")); + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef MODBUS_DEBUG + return false; // Initialization failed } } + // Store the updated link parameters in the Modbus manager's internal data structure + _modbus_links[linkIndex].port = port; + _modbus_links[linkIndex].serial_rx = serial_rx; + _modbus_links[linkIndex].serial_tx = serial_tx; + _modbus_links[linkIndex].baudrate = baudrate; + _modbus_links[linkIndex].dere_pin = dere_pin; + _modbus_links[linkIndex].rs485_mode = (dere_pin != -1); + _modbus_links[linkIndex].collision_detect = collision_detect; + + // Store the link configuration parameters in the key-value store for persistence if (_modbus_links[linkIndex].kvs) { // Store the link configuration parameters in the key-value store for persistence _modbus_links[linkIndex].kvs->setValue(MODBUS_PORT_KEY_INDEX, static_cast(port)); @@ -520,7 +540,6 @@ bool ModbusMGR_struct::setLink(const int linkIndex, _modbus_links[linkIndex].kvs->setValue(MODBUS_BAUDRATE_KEY_INDEX, static_cast(baudrate)); _modbus_links[linkIndex].kvs->setValue(MODBUS_DERE_PIN_KEY_INDEX, static_cast(dere_pin)); _modbus_links[linkIndex].kvs->setValue(MODBUS_COLLISION_DETECT_KEY_INDEX, static_cast(collision_detect)); - _modbus_links[linkIndex].kvs->store(SettingsType::Enum::ModbusInterfaceSettings_Type, linkIndex, 0, 0); } diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index 4433e97471..a175f2d810 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -21,21 +21,20 @@ typedef struct ModbusMGR_struct { ModbusMGR_struct(); ~ModbusMGR_struct(); - bool initialize(); - void processLinks(); + bool initialize(); + void processLinks(); - bool connect(int linkId, - ModbusDEVICE_struct *deviceID); + bool connect(int linkId, + const ModbusDEVICE_struct *deviceID); - bool disconnect(int linkId, - ModbusDEVICE_struct*deviceID); + bool disconnect(int linkId, + const ModbusDEVICE_struct*deviceID); - bool newTransaction(int linkId, - ModbusDEVICE_struct *device, - Modbus_transaction_ptr &transaction); + Modbus_transaction_ptr newTransaction(int linkId, + ModbusDEVICE_struct *device); - bool queueTransaction(int linkId, - Modbus_Transaction *transaction); + bool queueTransaction(int linkId, + Modbus_Transaction *transaction); void dumpAdminInfo(); @@ -64,6 +63,8 @@ typedef struct ModbusMGR_struct { }; + static bool validLinkId(int linkId) { return ((linkId >= 0) && (linkId < ModbusMGR_struct::MAX_MODBUS_LINKS)); } + ModbusLinkInfo_struct _modbus_links[MAX_MODBUS_LINKS] = {}; // All links managed by the Modbus manager bool _initialized = false; // Flag indicating if the manager is initialized int _deviceCounter = 0; diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index da107819fd..bf8a3ccfa6 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -251,7 +251,7 @@ void P183_data_struct::scan_next_module() } } -#ifdef P183_ALLOW_MODBUS_WAIT +#if P183_ALLOW_MODBUS_WAIT /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Read a Modbus register from the device. Block until the data is available // Warning: this may take time as we wait for the Modbus message to be exchanged diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 2a7c72c9ed..70daadff86 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -9,7 +9,9 @@ # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG -////# define P183_ALLOW_MODBUS_WAIT +#ifndef P183_ALLOW_MODBUS_WAIT +# define P183_ALLOW_MODBUS_WAIT 0 // Default to disabled +#endif // ifndef P183_ALLOW_MODBUS_WAIT # include "../Helpers/Modbus_device.h" @@ -65,7 +67,7 @@ struct P183_data_struct : public PluginTaskData_base { uint16_t start_reg, uint16_t end_reg); void scan_modbus(); - #ifdef P183_ALLOW_MODBUS_WAIT + #if P183_ALLOW_MODBUS_WAIT uint16_t readRegisterWait(uint16_t address); #endif // ifdef P183_ALLOW_MODBUS_WAIT uint16_t readRegisterCache(uint16_t address); From d68d88b713a9a43ead7c7a132856f7fb38bd5964 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:44:18 +0200 Subject: [PATCH 42/48] P183: Fixes --- src/_P183_modbus.ino | 14 ++--- src/src/Helpers/Modbus_device.cpp | 63 ++++++++++++---------- src/src/Helpers/Modbus_device.h | 2 +- src/src/Helpers/Modbus_link.cpp | 59 +++++++++++++++++--- src/src/Helpers/Modbus_link.h | 8 +-- src/src/Helpers/Modbus_mgr.cpp | 2 +- src/src/PluginStructs/P183_data_struct.cpp | 8 +-- 7 files changed, 104 insertions(+), 52 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 34efb9a048..a258cbba1f 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -209,14 +209,14 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) #if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged #else - addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus read command with invalid address")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus read command with invalid address"), event->TaskIndex)); value = 0; #endif // if P183_ALLOW_MODBUS_WAIT } if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P183 : Modbus read value %u from address 0x%04x"), value, address)); + addLogMove(LOG_LEVEL_INFO, strformat(F("P183[%d]: Modbus read value %u from address 0x%04x"), event->TaskIndex, value, address)); } success = true; } @@ -246,7 +246,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } # ifndef LIMIT_BUILD_SIZE else { - addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Unknown command")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Unknown command"), event->TaskIndex)); } # endif // LIMIT_BUILD_SIZE } @@ -261,7 +261,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus task timer invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus task timer invalid data struct"), event->TaskIndex)); # endif // LIMIT_BUILD_SIZE return false; } @@ -275,7 +275,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config invalid data struct"), event->TaskIndex)); # endif // LIMIT_BUILD_SIZE return false; } @@ -293,7 +293,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) #if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged #else - addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config command invalid address")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config command invalid address"), event->TaskIndex)); value = 0; #endif // if P183_ALLOW_MODBUS_WAIT } @@ -316,7 +316,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, F("P183 : Modbus Get config invalid data struct")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus once per second invalid data struct"), event->TaskIndex)); # endif // LIMIT_BUILD_SIZE return false; } diff --git a/src/src/Helpers/Modbus_device.cpp b/src/src/Helpers/Modbus_device.cpp index 2019e051ad..93913d09d3 100644 --- a/src/src/Helpers/Modbus_device.cpp +++ b/src/src/Helpers/Modbus_device.cpp @@ -18,10 +18,14 @@ # endif // ifdef BUILD_NO_DEBUG // Modbus function codes, see Modbus specification -const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; -const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; -const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; -const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; +const uint8_t MODBUS_READ_COILS = 0x01; +const uint8_t MODBUS_READ_DISCRETE_INPUTS = 0x02; +const uint8_t MODBUS_READ_HOLDING_REGISTERS = 0x03; +const uint8_t MODBUS_READ_INPUT_REGISTERS = 0x04; +const uint8_t MODBUS_WRITE_SINGLE_COIL = 0x05; +const uint8_t MODBUS_WRITE_SINGLE_REGISTER = 0x06; +const uint8_t MODBUS_WRITE_MULTIPLE_REGISTERS = 0x10; +const uint8_t MODBUS_READ_WRITE_MULTIPLE_REGISTERS = 0x17; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Destructor of the Modbus device class @@ -69,7 +73,7 @@ bool ModbusDEVICE_struct::init(uint8_t slaveAddress, int linkId, taskIndex_t tas // Checker for device class initialization status /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusDEVICE_struct::isInitialized() const { - return (_linkId >= 0); + return _linkId >= 0; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -99,14 +103,14 @@ bool ModbusDEVICE_struct::readHoldingRegister(uint16_t address, Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; transaction->_userId = 0; // Not used for this type of request - transaction->_userData = valuePtr; + transaction->_userData = valuePtr; transaction->_userState = statePtr; createReadFrame(*transaction, _modbus_address, address); - (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction ); // Transaction is now owned by the Modbus link + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link return true; } @@ -136,14 +140,14 @@ bool ModbusDEVICE_struct::readModuleHoldingRegister(uint8_t busAddress, Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - transaction->_userId = uid; - transaction->_userData = nullptr; + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = uid; + transaction->_userData = nullptr; transaction->_userState = nullptr; createReadFrame(*transaction, busAddress, registerAddress); - (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction ); // Transaction is now owned by the Modbus link + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link return true; } @@ -160,14 +164,14 @@ bool ModbusDEVICE_struct::readHoldingRegisters(uint16_t address, uint16_t size, Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; - transaction->_userId = uid; - transaction->_userData = nullptr; + transaction->_messageType = ModbusTransactionType::READ_HOLDING_REGISTERS; + transaction->_userId = uid; + transaction->_userData = nullptr; transaction->_userState = nullptr; createReadFrame(*transaction, _modbus_address, address, size); - (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction ); // Transaction is now owned by the Modbus link + (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link return true; } @@ -204,15 +208,15 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, if (!isInitialized()) { return false; } - + Modbus_Transaction *transaction = ModbusMGR_singleton.newTransaction(_linkId, this); if (transaction == nullptr) { - return false; // Failed to allocate a request structure + return false; // Failed to allocate a request structure } - transaction->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; + transaction->_messageType = ModbusTransactionType::WRITE_SINGLE_REGISTER; transaction->_userId = 0; // Not used for this type of request - transaction->_userData = nullptr; + transaction->_userData = nullptr; transaction->_userState = statePtr; @@ -223,10 +227,10 @@ bool ModbusDEVICE_struct::writeSingleRegister(uint16_t address, transaction->_sendframe[4] = highByte(value); transaction->_sendframe[5] = lowByte(value); uint16_t crc = CalculateCRC(transaction->_sendframe, 6); - transaction->_sendframe[6] = lowByte(crc); // CRC low byte - transaction->_sendframe[7] = highByte(crc); // CRC high byte - transaction->_sendframe_length = 8; // Size with CRC - transaction->_rcvframe_length = 8; // Expect 8 bytes in response + transaction->_sendframe[6] = lowByte(crc); // CRC low byte + transaction->_sendframe[7] = highByte(crc); // CRC high byte + transaction->_sendframe_length = 8; // Size with CRC + transaction->_rcvframe_length = 8; // Expect 8 bytes in response (void)ModbusMGR_singleton.queueTransaction(_linkId, transaction); // Transaction is now owned by the Modbus link *statePtr = ModbusResultState::Busy; @@ -249,13 +253,13 @@ void ModbusDEVICE_struct::processCommand(void) // Note that the response might be an invalid response or a timeout // The queueID identifies the request. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) +bool ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) { ModbusResultState resultState = ModbusResultState::Error; // Default to error unless proven otherwise if (req == nullptr) { addLogMove(LOG_LEVEL_INFO, F("Modbus Device: ERROR, Null pointer passed in callback")); - return; + return false; } # ifdef MODBUS_DEBUG @@ -367,6 +371,7 @@ void ModbusDEVICE_struct::linkCallback(Modbus_Transaction *req) log += (resultState == ModbusResultState::Success) ? F("SUCCESS") : F("ERROR"); addLogMove(LOG_LEVEL_INFO, log); # endif // MODBUS_DEBUG + return resultState == ModbusResultState::Success; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/src/Helpers/Modbus_device.h b/src/src/Helpers/Modbus_device.h index 8871a2b679..5f5d974696 100644 --- a/src/src/Helpers/Modbus_device.h +++ b/src/src/Helpers/Modbus_device.h @@ -61,7 +61,7 @@ struct ModbusDEVICE_struct { uint16_t getModbusTimeout() const; - void linkCallback(Modbus_Transaction *transaction); + bool linkCallback(Modbus_Transaction *transaction); // Start reading a Modubus holding register. The result will be available later. // The function returns true if the request was queued. diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 45e0ae91ec..8c494b63c5 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -195,6 +195,17 @@ bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Evaluate the next action to take to process the queue // This function shall be called periodically to keep the Modbus link active +// The queue is ordered by the order of queuing, and max one transaction is processed at a time. +// Elements are examined in order until either +// - the active transaction is found which is checked for response or timeout +// - The first waiting transaction is found which is sent and set as active +// - The transaction is completed or discarded which is removed from the queue +// - The queue is empty and nothing is done +// The flag linkOccupied is used to ensure that only one transaction is processed at a time. +// Processing of the queue is stopped when this flag is set or queue is empty. +// +// Note that a timeout for a transaction assumes no late response message will be received. +// If a late response is received, it most likely interferes with the next transaction queued. Thst transaction most likely fails. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processQueue() { @@ -204,8 +215,10 @@ void ModbusLINK_struct::processQueue() _processing = true; // Set processing flag to prevent reentrancy issues auto it = _transactionQueue.begin(); // Iterator for the request queue - bool linkOccupied = false; // Only process one request at a time + bool linkOccupied = false; // Stopping condition for the loop, see comments above. + // Main loop to process the queue. The loop is exited when either the end of the queue is reached or a transaction is being processed. + // Note that the iterator is explicitly incremented in the loop, or implicitly when a transaction is removed from the queue. while ((it != _transactionQueue.end()) && !linkOccupied) { # ifdef MODBUS_DEBUG it->print(); @@ -221,7 +234,7 @@ void ModbusLINK_struct::processQueue() } // First tranaction in queue ready to be transmitted. No other transaction is already being processed. - // Send the request and mark the transaction as sent, waiting for response. + // Send the request and mark the transaction as sent (linkOccupied = true), waiting for response. case ModbusQueueState::QUEUED: { // Send the request @@ -244,6 +257,8 @@ void ModbusLINK_struct::processQueue() // Transaction is sent, waiting for response. Check if a response is received or if the timeout has expired. case ModbusQueueState::MESSAGE_SENT: { + bool responseOK = true; // Assume response message is OK unless proven otherwise + // Waiting for response if (_easySerial->available() >= (it->_rcvframe_length)) { _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); @@ -251,7 +266,8 @@ void ModbusLINK_struct::processQueue() } else if (timePassedSince(it->_startTime) > it->_timeout) { // Timeout expired - it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error + it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark transaction as error + _prev_transact_failed = true; // Mark link that the previous transaction failed # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, it->_id, @@ -260,19 +276,46 @@ void ModbusLINK_struct::processQueue() } else { // Still waiting - linkOccupied = true; // Only process one request at a time + linkOccupied = true; // Only process one request at a time } - if (!linkOccupied) { // We received a response or an error occurred, process the result + if (!linkOccupied) { // We received a response or an error occurred, process the result if (it->_device != nullptr) { - it->_device->linkCallback(&(*it)); // Notify the device that a response was received + responseOK = it->_device->linkCallback(&(*it)); // Notify the device that a response was received + } + + if (_prev_transact_failed && !responseOK) { + it->_state = ModbusQueueState::RECOVERING; // Wait till timeout to recover from the error + linkOccupied = true; // Keep the link blocked until the recovery is completed + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link RECOVERING: link=%p, transaction ID= %u, available= %d, expected= %d"), this, it->_id, + _easySerial->available(), it->_rcvframe_length)); + # endif // MODBUS_DEBUG + } + else { + it->_state = ModbusQueueState::READY_FOR_DESTROY; } - it->_state = ModbusQueueState::READY_FOR_DESTROY; } it++; // Move to the next transaction in the queue break; } + // Transaction failed in suspicious conditions, wait for timeout to recover from the error. + case ModbusQueueState::RECOVERING: + { + if (timePassedSince(it->_startTime) > it->_timeout) { + // Timeout expired + _prev_transact_failed = false; // Clear the previous transaction failed flag + it->_state = ModbusQueueState::READY_FOR_DESTROY; + } + else { + linkOccupied = true; // Only process one request at a time + } + it++; // Move to the next transaction in the queue + break; + } + // All remaining states indicate that the transaction can be removed from the queue and destroyed. case ModbusQueueState::READY_FOR_DESTROY: case ModbusQueueState::ERROR_OCCURRED: @@ -343,6 +386,8 @@ const __FlashStringHelper* toString(ModbusQueueState_t state) { return F("ERROR_OCCURRED"); case ModbusQueueState::READY_FOR_DESTROY: return F("READY_FOR_DESTROY"); + case ModbusQueueState::RECOVERING: + return F("RECOVERING"); } # endif // MODBUS_DEBUG return F(""); diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 5ed2e1e1d9..613a3a72a2 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -21,7 +21,8 @@ typedef enum class ModbusQueueState { MESSAGE_SENT = 2, // Request message has been sent, waiting for response RESPONSE_RECEIVED = 3, // Response has been received and is being processed ERROR_OCCURRED = 4, // An error occurred during processing (e.g., timeout, invalid response) - READY_FOR_DESTROY = 5 // Element is marked for deletion and can be freed + READY_FOR_DESTROY = 5, // Element is marked for deletion and can be freed + RECOVERING = 6 // The transaction is being recovered from a link error } ModbusQueueState_t; @@ -117,8 +118,9 @@ struct ModbusLINK_struct { bool _collision_detect = false; // Flag to indicate if collision detection is enabled bool _initialized = false; bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to - // prevent - // reentrancy issues + // prevent reentrancy issues + bool _prev_transact_failed = false; // Flag to indicate if the previous transaction failed, used to trigger recovery + // actions }; diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index dd6e677b34..6c94b73ec8 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -12,7 +12,7 @@ # include # include "../Helpers/Modbus_mgr.h" -# define MODBUS_DEBUG +////# define MODBUS_DEBUG # ifdef BUILD_NO_DEBUG # undef MODBUS_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG diff --git a/src/src/PluginStructs/P183_data_struct.cpp b/src/src/PluginStructs/P183_data_struct.cpp index bf8a3ccfa6..59546259e5 100644 --- a/src/src/PluginStructs/P183_data_struct.cpp +++ b/src/src/PluginStructs/P183_data_struct.cpp @@ -44,7 +44,7 @@ bool P183_data_struct::plugin_init(uint8_t slaveAddress, int linkId) if (_modbusDevice == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, F("P183: Unable to allocate Modbus device object")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Unable to allocate Modbus device object"), _taskIndex)); # endif // LIMIT_BUILD_SIZE return false; } @@ -93,7 +93,7 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) { # ifdef P183_DEBUG addLogMove(LOG_LEVEL_INFO, - strformat(F("P183: TaskTimer called IDX=%d, par1=%d, par2=%d, par3=%d, par4=%d"), + strformat(F("P183[%d]: TaskTimer called IDX=%d, par1=%d, par2=%d, par3=%d, par4=%d"), event->idx, event->Par1, event->Par2, event->Par3, event->Par4)); # endif // P183_DEBUG @@ -119,7 +119,7 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) // This is the result of the regular cache read triggered in plugin_once_per_second. Update the user variables with the cache values. if (event->Data == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("P183: No data received for cache read")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: No data received for cache read"), _taskIndex)); return false; } ModbusRegisterSet_struct *registerSet = reinterpret_cast(event->Data); @@ -139,7 +139,7 @@ bool P183_data_struct::plugin_task_timer(EventStruct *event) if ((outputIndex < 0) || (outputIndex >= P183_NR_OUTPUTS)) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, F("P183: Invalid output index in task timer event")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Invalid output index in task timer event"), _taskIndex)); # endif // LIMIT_BUILD_SIZE return false; } From 1af346c4cca4ae21d992c09da264ed4783508be3 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:00:07 +0200 Subject: [PATCH 43/48] P183: Refactor link processing --- docs/source/Interfaces/Interfaces.rst | 8 +- misc/modbusFacility/Modbus_link_state.puml | 3 + src/src/DataStructs/TimingStats.cpp | 3 + src/src/DataStructs/TimingStats.h | 5 +- src/src/Helpers/Modbus_link.cpp | 245 +++++++++++++-------- src/src/Helpers/Modbus_link.h | 28 ++- src/src/Helpers/Modbus_mgr.cpp | 4 +- src/src/Helpers/PeriodicalActions.cpp | 4 +- 8 files changed, 190 insertions(+), 110 deletions(-) diff --git a/docs/source/Interfaces/Interfaces.rst b/docs/source/Interfaces/Interfaces.rst index 8025e9885e..c3a77c6f00 100644 --- a/docs/source/Interfaces/Interfaces.rst +++ b/docs/source/Interfaces/Interfaces.rst @@ -205,7 +205,11 @@ NB: When selecting the *User-defined* option, **all 3 GPIO pins should be set**, Modbus ------ -When using devices that use the Modbus RTU protocol the Modbus interface can be configured here. The Modbus RTU devices are connected via a RS485 serial interface. Multiple devices can be connected to the same serial interface, and are distinguished by their Modbus address. Note that a serial to RS485 converter is required to connect Modbus RTU devices to the ESP board. +When using devices that use the Modbus RTU protocol the Modbus interface can be configured here. The Modbus RTU devices are connected via a RS485 serial interface. Multiple devices can be connected to the same serial interface, and are distinguished by their Modbus address. A serial to RS485 converter is required to connect Modbus RTU devices to the ESP board. +Note that many plugins implement the Modbuis protocol for specific devices without the use of this generic Modbus interface. Only use the Modbus interface as configured on this page when using one or more plugins that depend on this generic Modbus RTU facility. + +The Modbus interface uses a serial port on the ESP board to communicate with the Modbus RTU devices. The RS485 link is half-duplex, meaning that the ESP board can either send or receive data at a time. Most RS485 converters have a DE (Driver Enable) pin that can be controlled by the ESP board to switch between sending and receiving data. +The DE pin can be configured here. If the converter has an automatic DE function, this can be set to "-None-". Some serial ports on the ESP board have a collision detect feature, which can be enabled here. When supported and enabled, the ESP board will detect if another device is sending data at the same time, and will wait sending data. This can help to avoid collisions on the RS485 link. In case all Modbus devices are well-behaving and plugins are well-configured the collision detect function is not required for Modbus RTU operation. .. image:: Modbus_Interface.png @@ -213,7 +217,7 @@ Main configuration item is the selection of the serial interface to use for Modb Selection of the RX and TX pins is required. -The Modbus RTU devices are connected to the selected serial interface via a RS485 converter. The converter has a DE (Driver Enable) pin that can be controlled by the ESP board to switch between sending and receiving data. The DE pin can be configured here. If the converter has an automatic DE function, this can be set to "-None-". +If the RS485 converter has a DE (Driver Enable) pin to be controlled by the ESP board it shall be configured here. If the converter has an automatic DE function, this can be set to "-None-". The Baud rate for the Modbus RTU communication shall be set. The default value is 9600 baud, but it can be set to other values as required by the connected devices. diff --git a/misc/modbusFacility/Modbus_link_state.puml b/misc/modbusFacility/Modbus_link_state.puml index c46230be2f..8373f9d9fd 100644 --- a/misc/modbusFacility/Modbus_link_state.puml +++ b/misc/modbusFacility/Modbus_link_state.puml @@ -5,7 +5,10 @@ NOT_QUEUED --> QUEUED : queueTransaction() QUEUED --> MESSAGE_SENT : link inactive MESSAGE_SENT --> RESPONSE_RECEIVED : received valid response MESSAGE_SENT --> ERROR_OCCURRED : response timeout or invalid response +MESSAGE_SENT -right-> RECOVERING : response timeout or invalid response after previous timeout +RECOVERING --> ERROR_OCCURRED : timeout } active -down-> READY_FOR_DESTROY : freeTransaction() READY_FOR_DESTROY --> [*] : processing + @enduml \ No newline at end of file diff --git a/src/src/DataStructs/TimingStats.cpp b/src/src/DataStructs/TimingStats.cpp index 22e3232786..aab757e591 100644 --- a/src/src/DataStructs/TimingStats.cpp +++ b/src/src/DataStructs/TimingStats.cpp @@ -278,6 +278,9 @@ const __FlashStringHelper* getMiscStatsName_F(TimingStatsElements stat) { #if FEATURE_MQTT case TimingStatsElements::PERIODICAL_MQTT: return F("Periodical MQTT"); #endif +#if FEATURE_MODBUS_FAC + case TimingStatsElements::MODBUS_PROCESS_LINKS: return F("Modbus process links"); +#endif // if FEATURE_MODBUS_FAC case TimingStatsElements::PARSE_TEMPLATE_PADDED: return F("parseTemplate_padded()"); case TimingStatsElements::PARSE_SYSVAR: return F("parseSystemVariables()"); case TimingStatsElements::PARSE_SYSVAR_NOCHANGE: return F("parseSystemVariables() No change"); diff --git a/src/src/DataStructs/TimingStats.h b/src/src/DataStructs/TimingStats.h index 354140a7a5..bc6757ab61 100644 --- a/src/src/DataStructs/TimingStats.h +++ b/src/src/DataStructs/TimingStats.h @@ -120,7 +120,10 @@ enum class TimingStatsElements { C013_RECEIVE_SENSOR_DATA, WEBSERVER_HANDLE_CLIENT, UPDATE_RTTTL, - +#if FEATURE_MODBUS_FAC + MODBUS_PROCESS_LINKS, +#endif // if FEATURE_MODBUS_FAC + // Web serving HANDLE_SERVING_WEBPAGE, HANDLE_SERVING_WEBPAGE_JSON, diff --git a/src/src/Helpers/Modbus_link.cpp b/src/src/Helpers/Modbus_link.cpp index 8c494b63c5..06ce3ac360 100644 --- a/src/src/Helpers/Modbus_link.cpp +++ b/src/src/Helpers/Modbus_link.cpp @@ -136,21 +136,22 @@ void ModbusLINK_struct::freeTransactions(const ModbusDEVICE_struct *device) } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// +// Create a new transaction. The transaction is added to the queue +// The transaction is not queued for processing yet. +// After filling the details of the transaction, the client shall call queueTransaction() to queue it for processing. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Modbus_transaction_ptr ModbusLINK_struct::newTransaction(ModbusDEVICE_struct *device) { if (!isInitialized()) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to create transaction for uninitialized link")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: Attempt to create transaction for uninitialized link %d"), this->_linkId)); return nullptr; } else { auto& tr = _transactionQueue.emplace_back(device); // Put a new transaction at the end of the queue - tr._id = ++(_queueID); // Assign a unique ID to the transaction tr._timeout = _modbus_timeout; // Default timeout value # ifdef MODBUS_DEBUG - addLogMove(LOG_LEVEL_INFO, F("Modbus Link: new transaction")); - tr.print(); // Print the transaction details for debugging + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus Link: new transaction link=%d, transaction ID %u, device %p"), this->_linkId, tr._id, device)); # endif // MODBUS_DEBUG return &tr; } @@ -166,13 +167,13 @@ bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { bool success = false; if (transaction == nullptr) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to queue null transaction")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: Attempt to queue null transaction for link %d"), this->_linkId)); } else if (!isInitialized()) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: Attempt to queue transaction on uninitialized link")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: Attempt to queue transaction on uninitialized link %d"), this->_linkId)); transaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error } else if (transaction->_rcvframe_length > MODBUS_RCV_BUFFER) { - addLogMove(LOG_LEVEL_ERROR, F("Modbus Link: receive buffer too large")); + addLogMove(LOG_LEVEL_ERROR, strformat(F("Modbus Link: receive buffer too large for link %d"), this->_linkId)); transaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark as error } else { @@ -183,7 +184,7 @@ bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { if (loglevelActiveFor(LOG_LEVEL_INFO) && (transaction != nullptr)) { addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus link: link=%p, Queueing transaction ID %u, state %s"), this, transaction->_id, + strformat(F("Modbus link: link=%d, Queueing transaction ID %u, state %s"), this->_linkId, transaction->_id, toString(transaction->_state))); } # endif // MODBUS_DEBUG @@ -196,162 +197,221 @@ bool ModbusLINK_struct::queueTransaction(Modbus_Transaction *transaction) { // Evaluate the next action to take to process the queue // This function shall be called periodically to keep the Modbus link active // The queue is ordered by the order of queuing, and max one transaction is processed at a time. +// _activeTransaction points to the transaction currently being processed, or nullptr if no transaction is active. +// If there is an active transaction it is handled first. +// If the link is not busy with an active transaction the queue is examined // Elements are examined in order until either -// - the active transaction is found which is checked for response or timeout -// - The first waiting transaction is found which is sent and set as active -// - The transaction is completed or discarded which is removed from the queue -// - The queue is empty and nothing is done -// The flag linkOccupied is used to ensure that only one transaction is processed at a time. -// Processing of the queue is stopped when this flag is set or queue is empty. +// - an element is found that can be sent, which is then sent and set as active +// - an element is found that is marked for destruction, which is then removed from the queue +// - the queue is empty and nothing is done // // Note that a timeout for a transaction assumes no late response message will be received. -// If a late response is received, it most likely interferes with the next transaction queued. Thst transaction most likely fails. +// If a late response is received, it most likely interferes with the next transaction queued. That transaction most likely fails. +// A rough mitigation is to wait for a full timeout if the next transaction also fails /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusLINK_struct::processQueue() { if (_processing || !isInitialized() || (_transactionQueue.empty())) { - return; // Serial port not initialized or queue is empty, nothing to process + return; // Serial port not initialized or queue is empty, nothing to process } - _processing = true; // Set processing flag to prevent reentrancy issues + _processing = true; // Set processing flag to prevent reentrancy issues - auto it = _transactionQueue.begin(); // Iterator for the request queue - bool linkOccupied = false; // Stopping condition for the loop, see comments above. - - // Main loop to process the queue. The loop is exited when either the end of the queue is reached or a transaction is being processed. - // Note that the iterator is explicitly incremented in the loop, or implicitly when a transaction is removed from the queue. - while ((it != _transactionQueue.end()) && !linkOccupied) { - # ifdef MODBUS_DEBUG - it->print(); - # endif // MODBUS_DEBUG + // Check if there is an active transaction and check if it can be completed + if (_activeTransaction != nullptr) { - switch (it->_state) + switch (_activeTransaction->_state) { - // Transaction is created but not yet queued. Skip it untill it is queued or discarded. - case ModbusQueueState::NOT_QUEUED: - { - it++; // Not queued yet, move to the next transaction in the queue - break; - } - - // First tranaction in queue ready to be transmitted. No other transaction is already being processed. - // Send the request and mark the transaction as sent (linkOccupied = true), waiting for response. - case ModbusQueueState::QUEUED: - { - // Send the request - int available = _easySerial->available(); - - if (available > 0) { - // Clear any pending input - for (int i = available; i > 0; --i) { - _easySerial->read(); - } - } - _easySerial->write(it->_sendframe, it->_sendframe_length); - it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response - it->_startTime = millis(); // Record the time the transaction - linkOccupied = true; // Only process one request at a time - it++; // Proforma move to the next transaction in the queue - break; - } - - // Transaction is sent, waiting for response. Check if a response is received or if the timeout has expired. case ModbusQueueState::MESSAGE_SENT: { - bool responseOK = true; // Assume response message is OK unless proven otherwise + + bool responseOK = true; // Assume response message is OK unless proven otherwise + bool linkBusy = false; // Assume we are done unless proven otherwise + int available = _easySerial->available(); // Cache the number of bytes available in the serial buffer // Waiting for response - if (_easySerial->available() >= (it->_rcvframe_length)) { - _easySerial->readBytes(it->_rcvframe, it->_rcvframe_length); - it->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received + if (available >= (_activeTransaction->_rcvframe_length)) { + _easySerial->readBytes(_activeTransaction->_rcvframe, _activeTransaction->_rcvframe_length); + _activeTransaction->_state = ModbusQueueState::RESPONSE_RECEIVED; // Mark as response received } - else if (timePassedSince(it->_startTime) > it->_timeout) { + else if (timePassedSince(_activeTransaction->_startTime) > _activeTransaction->_timeout) { // Timeout expired - it->_state = ModbusQueueState::ERROR_OCCURRED; // Mark transaction as error - _prev_transact_failed = true; // Mark link that the previous transaction failed + _activeTransaction->_state = ModbusQueueState::ERROR_OCCURRED; // Mark transaction as error + _prev_transact_failed = true; // Mark link that the previous transaction failed # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus link ERROR: link=%p, transaction ID= %u, available= %d, expected= %d"), this, it->_id, - _easySerial->available(), it->_rcvframe_length)); + strformat(F("Modbus link ERROR: link=%d, transaction ID= %u, available= %d, expected= %d"), this->_linkId, + _activeTransaction->_id, available, _activeTransaction->_rcvframe_length)); # endif // MODBUS_DEBUG } else { // Still waiting - linkOccupied = true; // Only process one request at a time + linkBusy = true; // Only process one request at a time } - if (!linkOccupied) { // We received a response or an error occurred, process the result - if (it->_device != nullptr) { - responseOK = it->_device->linkCallback(&(*it)); // Notify the device that a response was received + if (!linkBusy) { // We received a response or an error occurred, + + // process the result + if (_activeTransaction->_device != nullptr) { + responseOK = _activeTransaction->_device->linkCallback(_activeTransaction); // Notify the device that a response was + // received + } + else { + addLogMove(LOG_LEVEL_ERROR, F("Modbus link: ERROR, Null pointer for device in active transaction")); } if (_prev_transact_failed && !responseOK) { - it->_state = ModbusQueueState::RECOVERING; // Wait till timeout to recover from the error - linkOccupied = true; // Keep the link blocked until the recovery is completed + _activeTransaction->_state = ModbusQueueState::RECOVERING; // Wait till timeout to recover from the error # ifdef MODBUS_DEBUG addLogMove(LOG_LEVEL_INFO, - strformat(F("Modbus link RECOVERING: link=%p, transaction ID= %u, available= %d, expected= %d"), this, it->_id, - _easySerial->available(), it->_rcvframe_length)); + strformat(F("Modbus link RECOVERING: link=%d, transaction ID= %u"), this->_linkId, _activeTransaction->_id)); # endif // MODBUS_DEBUG } else { - it->_state = ModbusQueueState::READY_FOR_DESTROY; + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link FINISHED: link=%d, transaction ID= %u"), this->_linkId, + _activeTransaction->_id)); + # endif // MODBUS_DEBUG } } - it++; // Move to the next transaction in the queue - break; } - // Transaction failed in suspicious conditions, wait for timeout to recover from the error. + // Waiting for the timeout to recover from potential link errors. case ModbusQueueState::RECOVERING: { - if (timePassedSince(it->_startTime) > it->_timeout) { + if (timePassedSince(_activeTransaction->_startTime) > _activeTransaction->_timeout) { // Timeout expired - _prev_transact_failed = false; // Clear the previous transaction failed flag - it->_state = ModbusQueueState::READY_FOR_DESTROY; - } - else { - linkOccupied = true; // Only process one request at a time + _prev_transact_failed = false; // Clear the previous transaction failed flag + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link RECOVERY FINISHED: link=%d, transaction ID= %u"), this->_linkId, + _activeTransaction->_id)); + # endif // MODBUS_DEBUG } - it++; // Move to the next transaction in the queue break; } - // All remaining states indicate that the transaction can be removed from the queue and destroyed. + // Transaction should already be marked for destruction and activeTransaction should have been cleared + // The activeTransaction can be set to READY_FOR_DESTROY if the device discarded it case ModbusQueueState::READY_FOR_DESTROY: case ModbusQueueState::ERROR_OCCURRED: case ModbusQueueState::RESPONSE_RECEIVED: { - it->_state = ModbusQueueState::READY_FOR_DESTROY; - it = _transactionQueue.erase(it); // Remove it from the list + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; break; } - } // switch - } // next iterarion - _processing = false; // Clear processing flag to allow new processing cycles + // This is an error condition. activeTransaction should not point to a transaction in this state. + // Set the activeTransaction to nullptr to allow processing of the next transaction in the queue. + case ModbusQueueState::NOT_QUEUED: + case ModbusQueueState::QUEUED: + { + _activeTransaction->_state = ModbusQueueState::READY_FOR_DESTROY; + } + } + } + + // If there is no active transaction, check the queue for the next transaction to process + if ((_activeTransaction == nullptr) || (_activeTransaction->_state == ModbusQueueState::READY_FOR_DESTROY)) { + auto it = _transactionQueue.begin(); // Iterator for the request queue + + // Main loop to process the queue. + // Note that the iterator is explicitly incremented in the loop, or implicitly when a transaction is removed from the queue. + while (it != _transactionQueue.end()) { + + switch (it->_state) + { + // Transaction is created but not yet queued. Skip it untill it is queued or discarded. + case ModbusQueueState::NOT_QUEUED: + { + it++; // Not queued yet, move to the next transaction in the queue + break; + } + + // Transaction in queue ready to be transmitted. + // If there is no active transaction yet, send the request and mark it as active. + case ModbusQueueState::QUEUED: + { + if (_activeTransaction == nullptr) { // Pick only the first rtansaction from the queue to process + + // Clear the serial input buffer before sending the request + int available = _easySerial->available(); + + if (available > 0) { + // Clear any pending input + for (int i = available; i > 0; --i) { + _easySerial->read(); + } + } + _easySerial->write(it->_sendframe, it->_sendframe_length); + it->_state = ModbusQueueState::MESSAGE_SENT; // Mark as sent, waiting for response + it->_startTime = millis(); // Record the time the transaction + _activeTransaction = &(*it); // Mark this transaction as active + # ifdef MODBUS_DEBUG + addLogMove(LOG_LEVEL_INFO, + strformat(F("Modbus link: PICKING link=%d, transaction ID=%u, state=%s"), this->_linkId, it->_id, + toString(it->_state))); + # endif // MODBUS_DEBUG + } + it++; // Move to the next transaction in the queue + break; + } + + // Transaction is sent, waiting for response. Handled above in the active Transaction processing section. + case ModbusQueueState::MESSAGE_SENT: + // Transaction failed in suspicious conditions, Handled above in the active Transaction processing section. + case ModbusQueueState::RECOVERING: + { + it++; // Move to the next transaction in the queue + break; + } + + // All remaining states indicate that the transaction can be removed from the queue and destroyed. + case ModbusQueueState::READY_FOR_DESTROY: + case ModbusQueueState::ERROR_OCCURRED: // Not exepected + case ModbusQueueState::RESPONSE_RECEIVED: // Not expected + { + if (_activeTransaction == &(*it)) { + _activeTransaction = nullptr; // Clear the active transaction if it is being removed from the queue + } + it->_state = ModbusQueueState::READY_FOR_DESTROY; // For safety + it = _transactionQueue.erase(it); // Remove it from the list + break; + } + } // switch + } // next iterarion + } // check for inactive link conditions + _processing = false; // Clear processing flag to allow new processing cycles return; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int16_t ModbusLINK_struct::getBaudrate() const { return _easySerial != nullptr ? _easySerial->getBaudRate() : 0; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int16_t ModbusLINK_struct::getSerialRX() const { return _easySerial != nullptr ? _easySerial->getRxPin() : -1; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int16_t ModbusLINK_struct::getSerialTX() const { return _easySerial != nullptr ? _easySerial->getTxPin() : -1; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int8_t ModbusLINK_struct::getDerePin() const { return _dere_pin; } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusLINK_struct::getCollisionDetect() const { return _collision_detect; @@ -369,6 +429,7 @@ void ModbusLINK_struct::dumpState(ModbusQueueState_t state) { # endif // MODBUS_DEBUG } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const __FlashStringHelper* toString(ModbusQueueState_t state) { # ifdef MODBUS_DEBUG @@ -401,7 +462,7 @@ void Modbus_Transaction::print() # ifdef MODBUS_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = strformat(F("Modbus Transaction: [ID=%u, Device=%p, State="), _id, _device); + String log = strformat(F("Modbus Transaction %p: [ID=%u, Device=%p, State="), this, _id, _device); log += toString(_state); log += F(", TX=("); diff --git a/src/src/Helpers/Modbus_link.h b/src/src/Helpers/Modbus_link.h index 613a3a72a2..d05560a954 100644 --- a/src/src/Helpers/Modbus_link.h +++ b/src/src/Helpers/Modbus_link.h @@ -45,6 +45,7 @@ struct Modbus_Transaction { Modbus_Transaction(ModbusDEVICE_struct *device) { _device = device; + _id = identifier++; // Assign a unique ID to the transaction } void print(); // Print the transaction details for debugging @@ -65,6 +66,7 @@ struct Modbus_Transaction { uint8_t _sendframe[MODBUS_XMIT_BUFFER] = { 0 }; // Reqest frame to send uint8_t _rcvframe[MODBUS_RCV_BUFFER] = { 0 }; // Response frame received + static inline uint16_t identifier = 0; // Static variable to generate unique identifiers for each transaction }; typedef Modbus_Transaction *Modbus_transaction_ptr; @@ -80,6 +82,7 @@ typedef std::list Modbus_TransactionQueue; // The ModbusLINK structure maintains a queue of Modbus requests and associated responses. struct ModbusLINK_struct { ModbusLINK_struct() = default; + ModbusLINK_struct(int8_t linkId) : _linkId(linkId) {} ~ModbusLINK_struct(); @@ -109,18 +112,19 @@ struct ModbusLINK_struct { static void dumpState(ModbusQueueState_t state); - ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object - Modbus_TransactionQueue _transactionQueue = {}; // Queue of Modbus transactions to process - uint16_t _queueID = 0; // ID for the last request queued - uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds - - uint8_t _dere_pin = 0; // Pin for RS485 direction control - bool _collision_detect = false; // Flag to indicate if collision detection is enabled - bool _initialized = false; - bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to - // prevent reentrancy issues - bool _prev_transact_failed = false; // Flag to indicate if the previous transaction failed, used to trigger recovery - // actions + ESPeasySerial *_easySerial = nullptr; // Pointer to the serial port object + Modbus_TransactionQueue _transactionQueue = {}; // Queue of Modbus transactions to process + Modbus_transaction_ptr _activeTransaction = nullptr; // Pointer to the currently active transaction being processed + + uint16_t _modbus_timeout = 180; // Default Modbus timeout in milliseconds + int8_t _linkId = -1; // Link ID assigned by the Modbus manager + int8_t _dere_pin = -1; // Pin for RS485 direction control + bool _collision_detect = false; // Flag to indicate if collision detection is enabled + bool _initialized = false; + bool _processing = false; // Flag to indicate if the command queue is currently being processed, used to + // prevent reentrancy issues + bool _prev_transact_failed = false; // Flag to indicate if the previous transaction failed, used to trigger recovery + // actions }; diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 6c94b73ec8..c021890c1e 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -97,7 +97,7 @@ bool ModbusMGR_struct::initialize() // Create the Modbus link object if a port is configured for the link if (_modbus_links[i].port != ESPEasySerialPort::not_set) { - _modbus_links[i].link = new (std::nothrow) ModbusLINK_struct(); + _modbus_links[i].link = new (std::nothrow) ModbusLINK_struct(i); if (_modbus_links[i].link != nullptr) { _modbus_links[i].link->init(_modbus_links[i].port, @@ -503,7 +503,7 @@ bool ModbusMGR_struct::setLink(const int linkIndex, // Check of the link is being disabled (port set to not_set) or enabled (port set to a valid port) if (_modbus_links[linkIndex].port == ESPEasySerialPort::not_set) { if ((port != ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link == nullptr)) { - _modbus_links[linkIndex].link = new (std::nothrow) ModbusLINK_struct(); + _modbus_links[linkIndex].link = new (std::nothrow) ModbusLINK_struct(linkIndex); } } else if ((port == ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link != nullptr)) { diff --git a/src/src/Helpers/PeriodicalActions.cpp b/src/src/Helpers/PeriodicalActions.cpp index 26184ec75d..e6e95101d0 100644 --- a/src/src/Helpers/PeriodicalActions.cpp +++ b/src/src/Helpers/PeriodicalActions.cpp @@ -140,7 +140,9 @@ void run10TimesPerSecond() { } #endif #if FEATURE_MODBUS_FAC - ModbusMGR_singleton.processLinks(); + START_TIMER; + ModbusMGR_singleton.processLinks(); + STOP_TIMER(MODBUS_PROCESS_LINKS); #endif if (!UseRTOSMultitasking && From 41d11b5b2d1d9ca29c43e597f7eb5e772413a7da Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:15:25 +0200 Subject: [PATCH 44/48] P183: Rework after review --- src/_P183_modbus.ino | 39 ++++++++++++++++-------- src/src/Helpers/Modbus_mgr.cpp | 2 +- src/src/PluginStructs/P183_data_struct.h | 11 ++++--- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index a258cbba1f..462a9960d3 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -23,7 +23,7 @@ * 2025-08-24 flashmark: Initial version */ -//// # define P183_DEBUG + //// # define P183_DEBUG # define PLUGIN_183 # define PLUGIN_ID_183 183 @@ -76,19 +76,34 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) break; } + case PLUGIN_GET_DEVICEVALUECOUNT: + { + event->Par1 = getValueCountFromSensorType(static_cast(P183_NR_OUTPUTS)); + success = true; + break; + } + + case PLUGIN_GET_DEVICEVTYPE: + { + event->sensorType = static_cast(P183_NR_OUTPUTS); + event->idx = P183_NR_OUTPUTS_INDEX; + success = true; + break; + } + case PLUGIN_SET_DEFAULTS: { - P183_DEV_ID = P183_DEV_ID_DFLT; + P183_DEV_ID = P183_NR_OUTPUT_VALUES; // Default to max outputs success = true; break; } case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: { - if ((P183_NR_OUTPUTS < 1) || (P183_NR_OUTPUTS > P183_NR_OUTPUT_VALUES)) { - P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs - } - addFormNumericBox(F("Number of values to read"), P183_NR_OUTPUTS_LABEL, P183_NR_OUTPUTS); + // if ((P183_NR_OUTPUTS < 1) || (P183_NR_OUTPUTS > P183_NR_OUTPUT_VALUES)) { + // P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs + // } +// addFormNumericBox(F("Number of values to read"), P183_NR_OUTPUTS_LABEL, P183_NR_OUTPUTS); for (int outputIndex = 0; outputIndex < P183_NR_OUTPUT_VALUES; ++outputIndex) { @@ -206,12 +221,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterCache(address); } else { - #if P183_ALLOW_MODBUS_WAIT + # if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged - #else + # else addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus read command with invalid address"), event->TaskIndex)); value = 0; - #endif // if P183_ALLOW_MODBUS_WAIT + # endif // if P183_ALLOW_MODBUS_WAIT } @@ -290,12 +305,12 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) value = P183_data->readRegisterCache(address); } else { - #if P183_ALLOW_MODBUS_WAIT + # if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged - #else + # else addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config command invalid address"), event->TaskIndex)); value = 0; - #endif // if P183_ALLOW_MODBUS_WAIT + # endif // if P183_ALLOW_MODBUS_WAIT } string = String(value); success = true; diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index c021890c1e..1154ff9d14 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -324,7 +324,7 @@ void ModbusMGR_struct::show_modbus_interfaces() { addFormSubHeader(strformat(F("Modbus %u"), link)); - addFormDetailsStart(link == 0); + addFormDetailsStart(link == 0 || _modbus_links[link].port != ESPEasySerialPort::not_set); int idx = static_cast(_modbus_links[link].port); portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 70daadff86..369e03c8d6 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -9,9 +9,9 @@ # undef P183_DEBUG // Debugging switched off # endif // ifdef BUILD_NO_DEBUG -#ifndef P183_ALLOW_MODBUS_WAIT -# define P183_ALLOW_MODBUS_WAIT 0 // Default to disabled -#endif // ifndef P183_ALLOW_MODBUS_WAIT +# ifndef P183_ALLOW_MODBUS_WAIT +# define P183_ALLOW_MODBUS_WAIT 0 // Default to disabled +# endif // ifndef P183_ALLOW_MODBUS_WAIT # include "../Helpers/Modbus_device.h" @@ -31,6 +31,7 @@ # define P183_DEV_ID_LABEL PCONFIG_LABEL(0) # define P183_LINK_ID PCONFIG(1) # define P183_LINK_ID_LABEL PCONFIG_LABEL(1) +# define P183_NR_OUTPUTS_INDEX 3 # define P183_NR_OUTPUTS PCONFIG(3) # define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) # define P183_ADDRESS(x) PCONFIG(4 + x) @@ -67,9 +68,9 @@ struct P183_data_struct : public PluginTaskData_base { uint16_t start_reg, uint16_t end_reg); void scan_modbus(); - #if P183_ALLOW_MODBUS_WAIT + # if P183_ALLOW_MODBUS_WAIT uint16_t readRegisterWait(uint16_t address); - #endif // ifdef P183_ALLOW_MODBUS_WAIT + # endif // ifdef P183_ALLOW_MODBUS_WAIT uint16_t readRegisterCache(uint16_t address); void writeRegister(uint16_t address, From 3a873c538a0db44485fb6a6100201932dd096a4c Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:06:16 +0200 Subject: [PATCH 45/48] P183: Update serial settings --- misc/modbusFacility/Modbus_class.puml | 2 +- src/_P183_modbus.ino | 19 +- src/src/Helpers/Modbus_mgr.cpp | 96 ++-- src/src/Helpers/_Plugin_Helper_serial.cpp | 579 ++++++++++++++++------ src/src/Helpers/_Plugin_Helper_serial.h | 40 +- src/src/PluginStructs/P183_data_struct.h | 1 + 6 files changed, 504 insertions(+), 233 deletions(-) diff --git a/misc/modbusFacility/Modbus_class.puml b/misc/modbusFacility/Modbus_class.puml index bc6f9509f3..50365290f7 100644 --- a/misc/modbusFacility/Modbus_class.puml +++ b/misc/modbusFacility/Modbus_class.puml @@ -40,7 +40,7 @@ Modbus_link "1" -right-> "1" Serial_port Modbus_mgr "1" *--- "*" Modbus_link Modbus_mgr "1" <-- "*" Modbus_device Modbus_link *-- "1" Queue -Queue o-left- "*" Transaction +Queue *-left- "*" Transaction Modbus_device "1" --> "*" Transaction interface_gui -left-> Modbus_mgr Modbus_mgr --> kvs diff --git a/src/_P183_modbus.ino b/src/_P183_modbus.ino index 462a9960d3..73d8e06e88 100644 --- a/src/_P183_modbus.ino +++ b/src/_P183_modbus.ino @@ -93,7 +93,10 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: { - P183_DEV_ID = P183_NR_OUTPUT_VALUES; // Default to max outputs + P183_DEV_ID = P183_DEV_ID_DFLT; + P183_LINK_ID = P183_LINK_ID_DFLT; + P183_NR_OUTPUTS = P183_NR_OUTPUT_VALUES; // Default to max outputs + P183_CACHE_SIZE = 0; // Default to no cache success = true; break; } @@ -224,14 +227,14 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) # if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged # else - addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus read command with invalid address"), event->TaskIndex)); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus read command with invalid address"), event->TaskIndex+1)); value = 0; # endif // if P183_ALLOW_MODBUS_WAIT } if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P183[%d]: Modbus read value %u from address 0x%04x"), event->TaskIndex, value, address)); + addLogMove(LOG_LEVEL_INFO, strformat(F("P183[%d]: Modbus read value %u from address 0x%04x"), event->TaskIndex+1, value, address)); } success = true; } @@ -261,7 +264,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) } # ifndef LIMIT_BUILD_SIZE else { - addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Unknown command"), event->TaskIndex)); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Unknown command"), event->TaskIndex+1)); } # endif // LIMIT_BUILD_SIZE } @@ -276,7 +279,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus task timer invalid data struct"), event->TaskIndex)); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus task timer invalid data struct"), event->TaskIndex+1)); # endif // LIMIT_BUILD_SIZE return false; } @@ -290,7 +293,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config invalid data struct"), event->TaskIndex)); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config invalid data struct"), event->TaskIndex+1)); # endif // LIMIT_BUILD_SIZE return false; } @@ -308,7 +311,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) # if P183_ALLOW_MODBUS_WAIT value = P183_data->readRegisterWait(address); // Warning: this may take time as we waith for the Modbus message to be exchanged # else - addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config command invalid address"), event->TaskIndex)); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus Get config command invalid address"), event->TaskIndex+1)); value = 0; # endif // if P183_ALLOW_MODBUS_WAIT } @@ -331,7 +334,7 @@ boolean Plugin_183(uint8_t function, struct EventStruct *event, String& string) if (P183_data == nullptr) { # ifndef LIMIT_BUILD_SIZE - addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus once per second invalid data struct"), event->TaskIndex)); + addLogMove(LOG_LEVEL_ERROR, strformat(F("P183[%d]: Modbus once per second invalid data struct"), event->TaskIndex+1)); # endif // LIMIT_BUILD_SIZE return false; } diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 1154ff9d14..285a230d82 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -302,42 +302,16 @@ void ModbusMGR_struct::show_modbus_interfaces() } const FormSelectorOptions baudselector(optionBaudCount, options_baudrate); - int optionPortCount = 1; - options_port[0] = F("Not set"); - portMap[0] = 0; // Map the "Not set" option to index 0 - - for (int i = 1; i < NR_ELEMENTS(options_port); i++) { - if (validSerialPort(static_cast(i))) { - options_port[optionPortCount] = ESPEasySerialPort_toString(static_cast(i)); - portMap[i] = optionPortCount; // Store the index of the valid port in the options_port array - optionPortCount++; - } - else { - options_port[i] = F("Invalid"); - portMap[i] = 0; // Map invalid/unused ports to the "Not set" option - } - } - const FormSelectorOptions portSelector(optionPortCount, options_port); - // Iterate over the modbus links and show their configuration on the web page for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { - addFormSubHeader(strformat(F("Modbus %u"), link)); addFormDetailsStart(link == 0 || _modbus_links[link].port != ESPEasySerialPort::not_set); - int idx = static_cast(_modbus_links[link].port); - portSelector.addFormSelector(F("Port"), strformat(F("MBport%u"), link), portMap[idx]); - - String id = strformat(F("MBtx%u"), link); - addRowLabel_tr_id(formatGpioName_serialTX(false), id); - addPinSelect(PinSelectPurpose::Serial_input, id, _modbus_links[link].serial_tx); + serialHelper_webformLoad(link, _modbus_links[link].port, static_cast(_modbus_links[link].serial_rx), + static_cast(_modbus_links[link].serial_tx), true); - id = strformat(F("MBrx%u"), link); - addRowLabel_tr_id(formatGpioName_serialRX(false), id); - addPinSelect(PinSelectPurpose::Serial_output, id, _modbus_links[link].serial_rx); - - id = strformat(F("MBde%u"), link); + String id = strformat(F("MBde%u"), link); addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link].dere_pin); @@ -364,7 +338,9 @@ void ModbusMGR_struct::show_modbus_interfaces() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting) { if ((baudrate_setting > 7) || (baudrate_setting < 0)) { return 9600; } - switch (baudrate_setting) { + + switch (baudrate_setting) + { case 0: return 1200; case 1: return 2400; case 2: return 4800; @@ -386,11 +362,17 @@ uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint8_t modbus_baudrateToStorageValue(uint32_t baudrate) { if (baudrate <= 1200) { return 0; } + if (baudrate <= 2400) { return 1; } + if (baudrate <= 4800) { return 2; } + if (baudrate <= 9600) { return 3; } + if (baudrate <= 19200) { return 4; } + if (baudrate <= 38400) { return 5; } + if (baudrate <= 57600) { return 6; } return 7; // 115200 } @@ -401,43 +383,36 @@ uint8_t modbus_baudrateToStorageValue(uint32_t baudrate) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool ModbusMGR_struct::save_modbus_interfaces(String& error) { - int portCount = 1; - - // Create mapping table from dropdown enum index to actual port identifier, index 0 is reserved for "Not set" - int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)] {}; - - portMap[0] = 0; // Map the "Not set" option to index 0 - - for (int i = 1; i < NR_ELEMENTS(portMap); i++) { - if (validSerialPort(static_cast(i))) { - portMap[portCount++] = i; - } - } - for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { - int port_setting = 0; - int baudrate_setting = _modbus_links[link].baudrate; + int port_setting = static_cast(_modbus_links[link].port); int tx_setting = _modbus_links[link].serial_tx; int rx_setting = _modbus_links[link].serial_rx; + int baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); int dere_setting = _modbus_links[link].dere_pin; bool collision_detect_setting = _modbus_links[link].collision_detect; bool settingsChanged = false; - for (int i = 0; i < NR_ELEMENTS(portMap); i++) { - if (portMap[i] == static_cast(_modbus_links[link].port)) { - port_setting = i; - break; - } + int8_t rxPin = rx_setting; + int8_t txPin = tx_setting; + uint8_t port = port_setting; + serialHelper_webformSave(link, port, rxPin, txPin); + + if (port != port_setting) { + port_setting = port; + settingsChanged = true; + } + + if (rxPin != rx_setting) { + rx_setting = rxPin; + settingsChanged = true; + } + + if (txPin != tx_setting) { + tx_setting = txPin; + settingsChanged = true; } - update_whenset_FormItemInt(strformat(F("MBport%u"), link), port_setting, &settingsChanged); - baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting, &settingsChanged); - tx_setting = _modbus_links[link].serial_tx; - update_whenset_FormItemInt(strformat(F("MBtx%u"), link), tx_setting, &settingsChanged); - rx_setting = _modbus_links[link].serial_rx; - update_whenset_FormItemInt(strformat(F("MBrx%u"), link), rx_setting, &settingsChanged); - dere_setting = _modbus_links[link].dere_pin; if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting, &settingsChanged)) { @@ -451,11 +426,8 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) } if (settingsChanged) { - if ((port_setting < 0) || (port_setting > NR_ELEMENTS(portMap))) { - port_setting = 0; // Reset to "Not set" if the selected port is invalid - } setLink(link, - static_cast(portMap[port_setting]), + static_cast(port_setting), rx_setting, tx_setting, modbus_storageValueToBaudrate(baudrate_setting), @@ -509,7 +481,7 @@ bool ModbusMGR_struct::setLink(const int linkIndex, else if ((port == ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link != nullptr)) { delete _modbus_links[linkIndex].link; _modbus_links[linkIndex].link = nullptr; - } + } // If the link object is enabled (has a valid pointer), initialize it with the new parameters if (_modbus_links[linkIndex].link != nullptr) { diff --git a/src/src/Helpers/_Plugin_Helper_serial.cpp b/src/src/Helpers/_Plugin_Helper_serial.cpp index 7ab423bacc..08acc744f5 100644 --- a/src/src/Helpers/_Plugin_Helper_serial.cpp +++ b/src/src/Helpers/_Plugin_Helper_serial.cpp @@ -3,33 +3,32 @@ #ifdef PLUGIN_USES_SERIAL -#include "../../_Plugin_Helper.h" +# include "../../_Plugin_Helper.h" -#include "../DataStructs/ESPEasy_EventStruct.h" -#include "../Globals/Cache.h" -#include "../Helpers/StringConverter.h" -#include "../Helpers/StringGenerator_GPIO.h" +# include "../DataStructs/ESPEasy_EventStruct.h" +# include "../Globals/Cache.h" +# include "../Helpers/StringConverter.h" +# include "../Helpers/StringGenerator_GPIO.h" -#include "../WebServer/HTML_wrappers.h" -#include "../WebServer/Markup.h" -#include "../WebServer/Markup_Forms.h" - -#include +# include "../WebServer/HTML_wrappers.h" +# include "../WebServer/Markup.h" +# include "../WebServer/Markup_Forms.h" +# include const __FlashStringHelper* serialHelper_getSerialTypeLabel(ESPEasySerialPort serType, bool shortName) { return ESPEasySerialPort_toString(serType, shortName); } void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, int config_pin1, int config_pin2) { - #ifndef BUILD_NO_DEBUG + # ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("Serial : "); log += serialHelper_getGpioDescription(typeHint, config_pin1, config_pin2, " "); addLogMove(LOG_LEVEL_DEBUG, log); } - #endif // ifndef BUILD_NO_DEBUG + # endif // ifndef BUILD_NO_DEBUG } String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pin1, int config_pin2, const String& newline) { @@ -38,7 +37,8 @@ String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pi result.reserve(20); const ESPEasySerialPort porttype = ESPeasySerialType::getSerialType(typeHint, config_pin1, config_pin2); -#if USES_I2C_SC16IS752 +# if USES_I2C_SC16IS752 + if (porttype == ESPEasySerialPort::sc16is752) { result += formatToHex(config_pin1); @@ -47,14 +47,17 @@ String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pi result += config_pin2 == 0 ? 'A' : 'B'; return result; } -#endif -#if USES_HWCDC || USES_USBCDC -#if USES_HWCDC +# endif // if USES_I2C_SC16IS752 +# if USES_HWCDC || USES_USBCDC +# if USES_HWCDC + if (porttype == ESPEasySerialPort::usb_hw_cdc) -#else +# else // if USES_HWCDC + if (porttype == ESPEasySerialPort::usb_cdc_0 /*|| - porttype == ESPEasySerialPort::usb_cdc_1*/) -#endif + porttype == ESPEasySerialPort::usb_cdc_1*/ + ) +# endif // if USES_HWCDC { result += getConflictingUse(PIN_USB_D_MIN); result += formatGpioLabel(PIN_USB_D_MIN, false); @@ -63,8 +66,9 @@ String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pi result += formatGpioLabel(PIN_USB_D_PLUS, false); return result; } -#endif - if (useGPIOpins(porttype)) +# endif // if USES_HWCDC || USES_USBCDC + + if (useGPIOpins(porttype)) { result += F("RX: "); result += formatGpioLabel(config_pin1, false); @@ -81,26 +85,25 @@ void serialHelper_getGpioNames(struct EventStruct *event, bool rxOptional, bool } void serialHelper_modbus_getGpioNames(struct EventStruct *event, - bool rxOptional, - bool txOptional, - bool DE_RE_optional) + bool rxOptional, + bool txOptional, + bool DE_RE_optional) { serialHelper_getGpioNames(event, rxOptional, txOptional); event->String1.replace(F("TX"), F("TX (RO)")); event->String2.replace(F("RX"), F("RX (DI)")); - if (DE_RE_optional) + + if (DE_RE_optional) { event->String3 = formatGpioName_output_optional(F("~RE/DE")); - else + } + else { event->String3 = formatGpioName_output(F("~RE/DE")); + } } -int8_t serialHelper_getRxPin(struct EventStruct *event) { - return CONFIG_PIN1; -} +int8_t serialHelper_getRxPin(struct EventStruct *event) { return CONFIG_PIN1; } -int8_t serialHelper_getTxPin(struct EventStruct *event) { - return CONFIG_PIN2; -} +int8_t serialHelper_getTxPin(struct EventStruct *event) { return CONFIG_PIN2; } ESPEasySerialPort serialHelper_getSerialType(struct EventStruct *event) { ESPEasySerialPort serialType = static_cast(CONFIG_PORT); @@ -119,13 +122,14 @@ String serialHelper_getSerialTypeLabel(struct EventStruct *event) { return serialHelper_getSerialTypeLabel(serialHelper_getSerialType(event)); } -#ifndef DISABLE_SC16IS752_Serial +# ifndef DISABLE_SC16IS752_Serial + void serialHelper_addI2CuartSelectors(int address, int channel) { - # define SC16IS752_I2C_ADDRESSES 16 - # define SC16IS752_I2C_BASE_ADDR (0x90 >> 1) - # define SC16IS752_CHANNELS 2 - # define SC16IS752_CHANNEL_A 0x00 - # define SC16IS752_CHANNEL_B 0x01 + # define SC16IS752_I2C_ADDRESSES 16 + # define SC16IS752_I2C_BASE_ADDR (0x90 >> 1) + # define SC16IS752_CHANNELS 2 + # define SC16IS752_CHANNEL_A 0x00 + # define SC16IS752_CHANNEL_B 0x01 { String id = F("i2cuart_addr"); addRowLabel_tr_id(F("I2C Address"), id); @@ -157,23 +161,79 @@ void serialHelper_addI2CuartSelectors(int address, int channel) { F("A"), F("B"), }; + /* - const int chValues[] = { - SC16IS752_CHANNEL_A, - SC16IS752_CHANNEL_B, - }; - */ + const int chValues[] = { + SC16IS752_CHANNEL_A, + SC16IS752_CHANNEL_B, + }; + */ const FormSelectorOptions selector(NR_ELEMENTS(chOptions), chOptions); selector.addFormSelector(F("Channel"), F("i2cuart_ch"), channel); } } -#endif // ifndef DISABLE_SC16IS752_Serial +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Flashmark: experimental extension +// Allow multiple ports being configured on a single web page, by using a linkId to identify the port. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void serialHelper_addI2CuartSelectors(int linkId, int address, int channel) { + { + String id = strformat(F("i2cuart%d_addr"), linkId); + addRowLabel_tr_id(F("I2C Address"), id); + do_addSelector_Head(id, F(""), EMPTY_STRING, false); -void serialHelper_webformLoad(struct EventStruct *event) { - serialHelper_webformLoad(event, true); + if ((address < SC16IS752_I2C_BASE_ADDR) || (address >= (SC16IS752_I2C_BASE_ADDR + SC16IS752_I2C_ADDRESSES))) { + // selected address is not in range + address = SC16IS752_I2C_BASE_ADDR; + } + + for (int i = 0; i < SC16IS752_I2C_ADDRESSES; i++) + { + int addr = SC16IS752_I2C_BASE_ADDR + i; + String option; + option.reserve(24); + option = formatToHex(addr); + option += F(" (datasheet: "); + option += formatToHex(addr * 2); + option += ')'; + addSelector_Item(option, addr, addr == address); + } + addSelector_Foot(); + } + { + if ((channel != SC16IS752_CHANNEL_A) && (channel != SC16IS752_CHANNEL_B)) { + channel = SC16IS752_CHANNEL_A; + } + const __FlashStringHelper *chOptions[] = { + F("A"), + F("B"), + }; + const FormSelectorOptions selector(NR_ELEMENTS(chOptions), chOptions); + selector.addFormSelector(F("Channel"), strformat(F("i2cuart%d_ch"), linkId), channel); + } +} + +# endif // ifndef DISABLE_SC16IS752_Serial + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Flashmark: experimental extension +// Add selectors for RX and TX pins for a serial port, identified by linkId. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void serialHelper_addPinSelectors(int8_t linkId, int rxPin, int txPin) { + String id = strformat(F("serial%d_txpin"), linkId); + + addRowLabel_tr_id(formatGpioName_serialTX(false), id); + addPinSelect(PinSelectPurpose::Serial_input, id, txPin); + + id = strformat(F("serial%d_rxpin"), linkId); + addRowLabel_tr_id(formatGpioName_serialRX(false), id); + addPinSelect(PinSelectPurpose::Serial_output, id, rxPin); } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void serialHelper_webformLoad(struct EventStruct *event) { serialHelper_webformLoad(event, true); } + // These helper functions were made to create a generic interface to setup serial port config. // See issue #2343 and Pull request https://github.com/letscontrolit/ESPEasy/pull/2352 // For now P020 and P044 have been reverted to make them work again. @@ -184,10 +244,11 @@ void serialHelper_webformLoad(struct EventStruct *event, bool allowSoftwareSeria allowSoftwareSerial); } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef, bool allowSoftwareSerial) { // Field for I2C addr & RX are shared // Field for channel and TX are shared - #ifdef ESP8266 + # ifdef ESP8266 // Script to show GPIO pins for SoftwareSerial or I2C addresses for the I2C to UART bridge // "function serialPortChanged(elem) {var style = elem.value == 6 ? '' : 'none';var i2cstyle = elem.value == 1 ? '' : @@ -200,8 +261,8 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef "document.getElementById('tr_i2cuart_addr').style.display=l," "document.getElementById('tr_i2cuart_ch').style.display=l}"), false); - #endif // ifdef ESP8266 - #ifdef ESP32 + # endif // ifdef ESP8266 + # ifdef ESP32 // Script to show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge // "function serialPortChanged(elem) {var style = (elem.value == 2 || elem.value == 4 || elem.value == 5) ? '' : 'none';var i2cstyle = @@ -209,94 +270,98 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef // style;document.getElementById('tr_taskdevicepin2').style.display = style;document.getElementById('tr_i2cuart_addr').style.display = // i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;}"), html_add_script(F("function serialPortChanged(elem) {" -" var style = 'none';" -" var i2cstyle = elem.value == 1 ? '' : 'none';" -" if (elem.value == 2) {" -//" document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX0) ";" -//" document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX0) ";" -" style = '';" -# if USABLE_SOC_UART_NUM > 1 -" } else if (elem.value == 4) {" -//" document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX1) ";" -//" document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX1) ";" -" style = '';" -#endif -# if USABLE_SOC_UART_NUM > 2 -" } else if (elem.value == 5) {" -//" document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX2) ";" -//" document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX2) ";" -" style = '';" -#endif -# if USABLE_SOC_UART_NUM > 3 -" } else if (elem.value == 10) {" -" style = '';" -#endif -# if USABLE_SOC_UART_NUM > 4 -" } else if (elem.value == 11) {" -" style = '';" -#endif -# if USABLE_SOC_UART_NUM > 5 -" } else if (elem.value == 12) {" -" style = '';" -#endif -#if USES_SW_SERIAL -" } else if (elem.value == 6) {" -" style = '';" -#endif -" }" -" document.getElementById('tr_taskdevicepin1').style.display = style;" -" document.getElementById('tr_taskdevicepin2').style.display = style;" -" document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;" -" document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;" -"}"), + " var style = 'none';" + " var i2cstyle = elem.value == 1 ? '' : 'none';" + " if (elem.value == 2) {" + +// " document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX0) ";" +// " document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX0) ";" + " style = '';" +# if USABLE_SOC_UART_NUM > 1 + " } else if (elem.value == 4) {" + + // " document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX1) ";" +// " document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX1) ";" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 1 +# if USABLE_SOC_UART_NUM > 2 + " } else if (elem.value == 5) {" + +// " document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX2) ";" +// " document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX2) ";" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 2 +# if USABLE_SOC_UART_NUM > 3 + " } else if (elem.value == 10) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 3 +# if USABLE_SOC_UART_NUM > 4 + " } else if (elem.value == 11) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 4 +# if USABLE_SOC_UART_NUM > 5 + " } else if (elem.value == 12) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 5 +# if USES_SW_SERIAL + " } else if (elem.value == 6) {" + " style = '';" +# endif // if USES_SW_SERIAL + " }" + " document.getElementById('tr_taskdevicepin1').style.display = style;" + " document.getElementById('tr_taskdevicepin2').style.display = style;" + " document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;" + " document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;" + "}"), true); - #endif // ifdef ESP32 + # endif // ifdef ESP32 - #if !USES_SW_SERIAL + # if !USES_SW_SERIAL allowSoftwareSerial = false; - #endif + # endif const int ids[] = { - static_cast(ESPEasySerialPort::not_set) - ,static_cast(ESPEasySerialPort::serial0) -#ifdef ESP8266 - ,static_cast(ESPEasySerialPort::serial0_swap) -#endif // ifdef ESP8266 -#if USABLE_SOC_UART_NUM > 1 - ,static_cast(ESPEasySerialPort::serial1) -#endif -#if USABLE_SOC_UART_NUM > 2 - ,static_cast(ESPEasySerialPort::serial2) -#endif // if USABLE_SOC_UART_NUM > 2 -#if USABLE_SOC_UART_NUM > 3 - ,static_cast(ESPEasySerialPort::serial3) -#endif -#if USABLE_SOC_UART_NUM > 4 - ,static_cast(ESPEasySerialPort::serial4) -#endif -#if USABLE_SOC_UART_NUM > 5 - ,static_cast(ESPEasySerialPort::serial5) -#endif -#if USES_SW_SERIAL - ,static_cast(ESPEasySerialPort::software) -#endif // if USES_SW_SERIAL - -#if USES_HWCDC - ,static_cast(ESPEasySerialPort::usb_hw_cdc) -#endif // if USES_HWCDC -#if USES_USBCDC - ,static_cast(ESPEasySerialPort::usb_cdc_0) -// ,static_cast(ESPEasySerialPort::usb_cdc_1) -#endif // if USES_USBCDC -#if USES_I2C_SC16IS752 - ,static_cast(ESPEasySerialPort::sc16is752) -#endif // if USES_I2C_SC16IS752 + static_cast(ESPEasySerialPort::not_set) + , static_cast(ESPEasySerialPort::serial0) +# ifdef ESP8266 + , static_cast(ESPEasySerialPort::serial0_swap) +# endif // ifdef ESP8266 +# if USABLE_SOC_UART_NUM > 1 + , static_cast(ESPEasySerialPort::serial1) +# endif +# if USABLE_SOC_UART_NUM > 2 + , static_cast(ESPEasySerialPort::serial2) +# endif // if USABLE_SOC_UART_NUM > 2 +# if USABLE_SOC_UART_NUM > 3 + , static_cast(ESPEasySerialPort::serial3) +# endif +# if USABLE_SOC_UART_NUM > 4 + , static_cast(ESPEasySerialPort::serial4) +# endif +# if USABLE_SOC_UART_NUM > 5 + , static_cast(ESPEasySerialPort::serial5) +# endif +# if USES_SW_SERIAL + , static_cast(ESPEasySerialPort::software) +# endif // if USES_SW_SERIAL + +# if USES_HWCDC + , static_cast(ESPEasySerialPort::usb_hw_cdc) +# endif // if USES_HWCDC +# if USES_USBCDC + , static_cast(ESPEasySerialPort::usb_cdc_0) + + // ,static_cast(ESPEasySerialPort::usb_cdc_1) +# endif // if USES_USBCDC +# if USES_I2C_SC16IS752 + , static_cast(ESPEasySerialPort::sc16is752) +# endif // if USES_I2C_SC16IS752 }; constexpr int NR_ESPEASY_SERIAL_TYPES = NR_ELEMENTS(ids); String options[NR_ESPEASY_SERIAL_TYPES]; -// String attr[NR_ESPEASY_SERIAL_TYPES]; + // String attr[NR_ESPEASY_SERIAL_TYPES]; for (int i = 0; (i < NR_ESPEASY_SERIAL_TYPES); ++i) { ESPEasySerialPort serType = static_cast(ids[i]); @@ -304,8 +369,9 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef String option; option.reserve(48); option = serialHelper_getSerialTypeLabel(serType); -#ifdef ESP8266 +# ifdef ESP8266 int rxPin, txPin; + if (ESPeasySerialType::getSerialTypePins(serType, rxPin, txPin)) { if (isHWserial(serType)) @@ -322,30 +388,236 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef option += F("RX"); } } -#endif +# endif // ifdef ESP8266 options[i] = option; } FormSelectorOptions selector(NR_ESPEASY_SERIAL_TYPES, options, ids); // Script to toggle GPIO visibility when changing selection. - selector.onChangeCall = F("serialPortChanged(this)"); + selector.onChangeCall = F("serialPortChanged(this)"); selector.addFormSelector( F("Serial Port"), F("serPort"), static_cast(ESPeasySerialType::getSerialType(port, rxPinDef, txPinDef))); -#if USES_I2C_SC16IS752 +# if USES_I2C_SC16IS752 serialHelper_addI2CuartSelectors(rxPinDef, txPinDef); -#endif // ifndef DISABLE_SC16IS752_Serial +# endif // ifndef DISABLE_SC16IS752_Serial -#ifdef ESP8266 +# ifdef ESP8266 if ((rxPinDef == 15) || (txPinDef == 15)) { addFormNote(F("GPIO-15 (D8) requires a Buffer Circuit (PNP transistor) or ESP boot may fail.")); } -#endif // ifdef ESP8266 +# endif // ifdef ESP8266 +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Flashmark: experimental extension +// Allow multiple ports being configured on a single web page, by using a linkId to identify the port. +// Port configuration depends on the selected port type. A script is used to show/hide the correct fields for the selected port type. +// Fields are either: +// - Empty when configuration is fixed by hardware (e.g. ESP8266 UART) +// - Configurable RX and TX GPIO pins (e.g. ESP32 UART, software serial) +// - Configurable I2C address and channel (e.g. SC16IS752 I2C to UART bridge) +// Parameters rxPinDef and txPinDef are used for either RX and TX pin fields or the I2C address and channel field. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void serialHelper_webformLoad(int8_t linkId, ESPEasySerialPort port, int rxPinDef, int txPinDef, bool allowSoftwareSerial) { + + # ifdef ESP8266 + + // Script to show GPIO pins for SoftwareSerial or I2C addresses for the I2C to UART bridge + // "function serialPortChanged(elem) {var style = elem.value == 6 ? '' : 'none';var i2cstyle = elem.value == 1 ? '' : + // 'none';document.getElementById('tr_taskdevicepin1').style.display = style;document.getElementById('tr_taskdevicepin2').style.display = + // style;document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = + // i2cstyle;}"), + html_add_script(F("function serialPortChanged(e){var t=6==e.value?'':'none',l=1==e.value?'':'none';" + "document.getElementById('tr_taskdevicepin1').style.display=t," + "document.getElementById('tr_taskdevicepin2').style.display=t," + "document.getElementById('tr_i2cuart_addr').style.display=l," + "document.getElementById('tr_i2cuart_ch').style.display=l}"), + false); + # endif // ifdef ESP8266 + # ifdef ESP32 + + // Build a script to dynamically show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge + // Script to show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge + // "function serialPortChanged(elem) {var style = (elem.value == 2 || elem.value == 4 || elem.value == 5) ? '' : 'none';var i2cstyle = + // elem.value == 1 ? '' : 'none';document.getElementById('tr_taskdevicepin1').style.display = + // style;document.getElementById('tr_taskdevicepin2').style.display = style;document.getElementById('tr_i2cuart_addr').style.display = + // i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;}"), + html_add_script(true); + + addHtml(strformat(F("function serialPort%dChanged(elem) {"), linkId)); + addHtml(F(" var style = 'none';" + " var i2cstyle = elem.value == 1 ? '' : 'none';" + " if (elem.value == 2) {" + " style = '';" +# if USABLE_SOC_UART_NUM > 1 + " } else if (elem.value == 4) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 1 +# if USABLE_SOC_UART_NUM > 2 + " } else if (elem.value == 5) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 2 +# if USABLE_SOC_UART_NUM > 3 + " } else if (elem.value == 10) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 3 +# if USABLE_SOC_UART_NUM > 4 + " } else if (elem.value == 11) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 4 +# if USABLE_SOC_UART_NUM > 5 + " } else if (elem.value == 12) {" + " style = '';" +# endif // if USABLE_SOC_UART_NUM > 5 +# if USES_SW_SERIAL + " } else if (elem.value == 6) {" + " style = '';" +# endif // if USES_SW_SERIAL + " }" + )); + addHtml(strformat(F(" document.getElementById('tr_serial%d_rxpin').style.display = style;"), linkId)); + addHtml(strformat(F(" document.getElementById('tr_serial%d_txpin').style.display = style;"), linkId)); + addHtml(strformat(F(" document.getElementById('tr_i2cuart%d_addr').style.display = i2cstyle;"), linkId)); + addHtml(strformat(F(" document.getElementById('tr_i2cuart%d_ch').style.display = i2cstyle;}"), linkId)); + html_add_script_end(); + + # endif // ifdef ESP32 + + // Script done. Build the selector for the serial port type. + # if !USES_SW_SERIAL + allowSoftwareSerial = false; + # endif + + const int ids[] = { + static_cast(ESPEasySerialPort::not_set) + , static_cast(ESPEasySerialPort::serial0) +# ifdef ESP8266 + , static_cast(ESPEasySerialPort::serial0_swap) +# endif // ifdef ESP8266 +# if USABLE_SOC_UART_NUM > 1 + , static_cast(ESPEasySerialPort::serial1) +# endif +# if USABLE_SOC_UART_NUM > 2 + , static_cast(ESPEasySerialPort::serial2) +# endif // if USABLE_SOC_UART_NUM > 2 +# if USABLE_SOC_UART_NUM > 3 + , static_cast(ESPEasySerialPort::serial3) +# endif +# if USABLE_SOC_UART_NUM > 4 + , static_cast(ESPEasySerialPort::serial4) +# endif +# if USABLE_SOC_UART_NUM > 5 + , static_cast(ESPEasySerialPort::serial5) +# endif +# if USES_SW_SERIAL + , static_cast(ESPEasySerialPort::software) +# endif // if USES_SW_SERIAL + +# if USES_HWCDC + , static_cast(ESPEasySerialPort::usb_hw_cdc) +# endif // if USES_HWCDC +# if USES_USBCDC + , static_cast(ESPEasySerialPort::usb_cdc_0) + + // ,static_cast(ESPEasySerialPort::usb_cdc_1) +# endif // if USES_USBCDC +# if USES_I2C_SC16IS752 + , static_cast(ESPEasySerialPort::sc16is752) +# endif // if USES_I2C_SC16IS752 + }; + + constexpr int NR_ESPEASY_SERIAL_TYPES = NR_ELEMENTS(ids); + String options[NR_ESPEASY_SERIAL_TYPES]; + + for (int i = 0; (i < NR_ESPEASY_SERIAL_TYPES); ++i) { + ESPEasySerialPort serType = static_cast(ids[i]); + + String option; + option.reserve(48); + option = serialHelper_getSerialTypeLabel(serType); +# ifdef ESP8266 + int rxPin, txPin; + + if (ESPeasySerialType::getSerialTypePins(serType, rxPin, txPin)) { + + if (isHWserial(serType)) + { + // Show pins for ports with fixed pins + option += F(": "); + option += formatGpioLabel(rxPin, false); + option += ' '; + option += formatGpioDirection(gpio_direction::gpio_input); + option += F("TX / "); + option += formatGpioLabel(txPin, false); + option += ' '; + option += formatGpioDirection(gpio_direction::gpio_output); + option += F("RX"); + } + } +# endif // ifdef ESP8266 + options[i] = option; + } + FormSelectorOptions selector(NR_ESPEASY_SERIAL_TYPES, options, ids); + + // Script to toggle GPIO visibility when changing selection. + selector.onChangeCall = strformat(F("serialPort%dChanged(this)"), linkId); + selector.addFormSelector( + F("Serial Port"), strformat(F("serPort%d"), linkId), static_cast(port)); + + serialHelper_addPinSelectors(linkId, rxPinDef, txPinDef); +# if USES_I2C_SC16IS752 + serialHelper_addI2CuartSelectors(linkId, rxPinDef, txPinDef); +# endif // ifndef DISABLE_SC16IS752_Serial + + html_add_script(strformat(F("document.getElementById('serPort%d').onchange();"), linkId), true); + +# ifdef ESP8266 + + if ((rxPinDef == 15) || (txPinDef == 15)) { + addFormNote(F("GPIO-15 (D8) requires a Buffer Circuit (PNP transistor) or ESP boot may fail.")); + } +# endif // ifdef ESP8266 +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Flashmark: experimental extension +// Allow multiple ports being configured on a single web page, by using a linkId to identify the port. +// Save one entry of a serial port from a webform as setup with serialHelper_webformLoad() +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void serialHelper_webformSave(int8_t linkId, uint8_t& port, int8_t& rxPin, int8_t& txPin) { + + port = getFormItemInt(strformat(F("serPort%d"), linkId), port); + + rxPin = getFormItemInt(strformat(F("serial%d_rxpin"), linkId), rxPin); + txPin = getFormItemInt(strformat(F("serial%d_txpin"), linkId), txPin); + + # ifndef DISABLE_SC16IS752_Serial + + if (static_cast(port) == ESPEasySerialPort::sc16is752) { + rxPin = getFormItemInt(strformat(F("i2cuart%d_addr"), linkId), rxPin); + txPin = getFormItemInt(strformat(F("i2cuart%d_ch"), linkId), txPin); + } + # endif // ifndef DISABLE_SC16IS752_Serial + # ifdef ESP8266 + + if (isHWserial(serType)) { + + // Ports with a fixed pin layout, so load the defaults. + int tmprxPin, tmptxPin; + + if (ESPeasySerialType::getSerialTypePins(serType, tmprxPin, tmptxPin)) { + rxPin = tmprxPin; + txPin = tmptxPin; + } + } + # endif // ifdef ESP8266 } void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) { int serialPortSelected = getFormItemInt(F("serPort"), -1); + if (serialPortSelected < 0) { return; } rxPin = getFormItemInt(F("taskdevicepin1"), -1); @@ -355,13 +627,15 @@ void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) { port = serialPortSelected; - #ifndef DISABLE_SC16IS752_Serial + # ifndef DISABLE_SC16IS752_Serial + if (serType == ESPEasySerialPort::sc16is752) { rxPin = getFormItemInt(F("i2cuart_addr"), rxPin); txPin = getFormItemInt(F("i2cuart_ch"), txPin); } - #endif // ifndef DISABLE_SC16IS752_Serial - #ifdef ESP8266 + # endif // ifndef DISABLE_SC16IS752_Serial + # ifdef ESP8266 + if (isHWserial(serType)) { // Ports with a fixed pin layout, so load the defaults. @@ -372,12 +646,10 @@ void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) { txPin = tmptxPin; } } - #endif // ifdef ESP8266 + # endif // ifdef ESP8266 } -void serialHelper_webformSave(struct EventStruct *event) { - serialHelper_webformSave(CONFIG_PORT, CONFIG_PIN1, CONFIG_PIN2); -} +void serialHelper_webformSave(struct EventStruct *event) { serialHelper_webformSave(CONFIG_PORT, CONFIG_PIN1, CONFIG_PIN2); } bool serialHelper_isValid_serialconfig(uint8_t serialconfig) { if ((serialconfig >= 0x10) && (serialconfig <= 0x3f)) { @@ -407,18 +679,27 @@ void serialHelper_serialconfig_webformLoad(struct EventStruct *event, uint8_t cu label += F(" bit / parity: "); int value = ((bits - 5) << 2); - switch (parity) { - case 0: label += F("None"); break; - case 1: label += F("Even"); value += 2; break; - case 2: label += F("Odd"); value += 3; break; + switch (parity) + { + case 0: label += F("None"); + break; + case 1: label += F("Even"); + value += 2; + break; + case 2: label += F("Odd"); + value += 3; + break; } label += F(" / stop bits: "); label += String(stopBits); // There are also values for 0 and "1.5" stop bit, not used now. - switch (stopBits) { - case 1: value += 0x10; break; - case 2: value += 0x30; break; + switch (stopBits) + { + case 1: value += 0x10; + break; + case 2: value += 0x30; + break; } addSelector_Item(label, value, value == currentSelection); } @@ -460,4 +741,4 @@ uint8_t serialHelper_convertOldSerialConfig(uint8_t newLocationConfig) { return static_cast(SERIAL_8N1 & 0xFF); // Some default } -#endif \ No newline at end of file +#endif // ifdef PLUGIN_USES_SERIAL diff --git a/src/src/Helpers/_Plugin_Helper_serial.h b/src/src/Helpers/_Plugin_Helper_serial.h index c889331ef2..3099d653d7 100644 --- a/src/src/Helpers/_Plugin_Helper_serial.h +++ b/src/src/Helpers/_Plugin_Helper_serial.h @@ -6,17 +6,17 @@ #ifdef PLUGIN_USES_SERIAL -#include +# include struct ESPeasySerialType; const __FlashStringHelper* serialHelper_getSerialTypeLabel(ESPEasySerialPort serType, - bool shortName = false); + bool shortName = false); -void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, - int config_pin1, - int config_pin2); +void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, + int config_pin1, + int config_pin2); String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pin1, @@ -28,9 +28,9 @@ void serialHelper_getGpioNames(struct EventStruct *event, bool txOptional = false); void serialHelper_modbus_getGpioNames(struct EventStruct *event, - bool rxOptional = false, - bool txOptional = false, - bool DE_RE_optional = true); + bool rxOptional = false, + bool txOptional = false, + bool DE_RE_optional = true); int8_t serialHelper_getRxPin(struct EventStruct *event); @@ -40,10 +40,14 @@ ESPEasySerialPort serialHelper_getSerialType(struct EventStruct *event); String serialHelper_getSerialTypeLabel(struct EventStruct *event); -#ifndef DISABLE_SC16IS752_Serial -void serialHelper_addI2CuartSelectors(int address, - int channel); -#endif // ifndef DISABLE_SC16IS752_Serial +# ifndef DISABLE_SC16IS752_Serial +void serialHelper_addI2CuartSelectors(int address, + int channel); + +void serialHelper_addI2CuartSelectors(int linkId, + int address, + int channel); +# endif // ifndef DISABLE_SC16IS752_Serial void serialHelper_webformLoad(struct EventStruct *event); @@ -58,11 +62,21 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int txPinDef, bool allowSoftwareSerial); +void serialHelper_webformLoad(int8_t linkId, + ESPEasySerialPort port, + int rxPinDef, + int txPinDef, + bool allowSoftwareSerial); void serialHelper_webformSave(uint8_t& port, int8_t & rxPin, int8_t & txPin); -void serialHelper_webformSave(struct EventStruct *event); +void serialHelper_webformSave(struct EventStruct *event); + +void serialHelper_webformSave(int8_t linkId, + uint8_t& port, + int8_t & rxPin, + int8_t & txPin); bool serialHelper_isValid_serialconfig(uint8_t serialconfig); diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index 369e03c8d6..a75aae59dd 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -43,6 +43,7 @@ # define P183_CACHE_START_LABEL F("P183st") # define P183_DEV_ID_DFLT 1 +# define P183_LINK_ID_DFLT 0 # define P183_MODBUS_TIMEOUT 1000 // milliseconds # define P183_MAX_MODBUS_NODES 247 # define P183_MODBUS_BROADCAST_ID 0 // Modbus broadcast address From e90a72f42542fe3191721f4f384aa7a1d492e63f Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:01:32 +0200 Subject: [PATCH 46/48] P183: Revert changes in serial helper. Compiles, but lacks the setup functionality. --- src/src/Helpers/Modbus_mgr.cpp | 6 +- src/src/Helpers/_Plugin_Helper_serial.cpp | 579 ++++++---------------- src/src/Helpers/_Plugin_Helper_serial.h | 40 +- 3 files changed, 165 insertions(+), 460 deletions(-) diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 285a230d82..55c4bf3f8f 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -308,8 +308,8 @@ void ModbusMGR_struct::show_modbus_interfaces() addFormSubHeader(strformat(F("Modbus %u"), link)); addFormDetailsStart(link == 0 || _modbus_links[link].port != ESPEasySerialPort::not_set); - serialHelper_webformLoad(link, _modbus_links[link].port, static_cast(_modbus_links[link].serial_rx), - static_cast(_modbus_links[link].serial_tx), true); +//// serialHelper_webformLoad(link, _modbus_links[link].port, static_cast(_modbus_links[link].serial_rx), +//// static_cast(_modbus_links[link].serial_tx), true); String id = strformat(F("MBde%u"), link); addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); @@ -395,7 +395,7 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) int8_t rxPin = rx_setting; int8_t txPin = tx_setting; uint8_t port = port_setting; - serialHelper_webformSave(link, port, rxPin, txPin); +//// serialHelper_webformSave(link, port, rxPin, txPin); if (port != port_setting) { port_setting = port; diff --git a/src/src/Helpers/_Plugin_Helper_serial.cpp b/src/src/Helpers/_Plugin_Helper_serial.cpp index 08acc744f5..7ab423bacc 100644 --- a/src/src/Helpers/_Plugin_Helper_serial.cpp +++ b/src/src/Helpers/_Plugin_Helper_serial.cpp @@ -3,32 +3,33 @@ #ifdef PLUGIN_USES_SERIAL -# include "../../_Plugin_Helper.h" +#include "../../_Plugin_Helper.h" -# include "../DataStructs/ESPEasy_EventStruct.h" -# include "../Globals/Cache.h" -# include "../Helpers/StringConverter.h" -# include "../Helpers/StringGenerator_GPIO.h" +#include "../DataStructs/ESPEasy_EventStruct.h" +#include "../Globals/Cache.h" +#include "../Helpers/StringConverter.h" +#include "../Helpers/StringGenerator_GPIO.h" -# include "../WebServer/HTML_wrappers.h" -# include "../WebServer/Markup.h" -# include "../WebServer/Markup_Forms.h" +#include "../WebServer/HTML_wrappers.h" +#include "../WebServer/Markup.h" +#include "../WebServer/Markup_Forms.h" + +#include -# include const __FlashStringHelper* serialHelper_getSerialTypeLabel(ESPEasySerialPort serType, bool shortName) { return ESPEasySerialPort_toString(serType, shortName); } void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, int config_pin1, int config_pin2) { - # ifndef BUILD_NO_DEBUG + #ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("Serial : "); log += serialHelper_getGpioDescription(typeHint, config_pin1, config_pin2, " "); addLogMove(LOG_LEVEL_DEBUG, log); } - # endif // ifndef BUILD_NO_DEBUG + #endif // ifndef BUILD_NO_DEBUG } String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pin1, int config_pin2, const String& newline) { @@ -37,8 +38,7 @@ String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pi result.reserve(20); const ESPEasySerialPort porttype = ESPeasySerialType::getSerialType(typeHint, config_pin1, config_pin2); -# if USES_I2C_SC16IS752 - +#if USES_I2C_SC16IS752 if (porttype == ESPEasySerialPort::sc16is752) { result += formatToHex(config_pin1); @@ -47,17 +47,14 @@ String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pi result += config_pin2 == 0 ? 'A' : 'B'; return result; } -# endif // if USES_I2C_SC16IS752 -# if USES_HWCDC || USES_USBCDC -# if USES_HWCDC - +#endif +#if USES_HWCDC || USES_USBCDC +#if USES_HWCDC if (porttype == ESPEasySerialPort::usb_hw_cdc) -# else // if USES_HWCDC - +#else if (porttype == ESPEasySerialPort::usb_cdc_0 /*|| - porttype == ESPEasySerialPort::usb_cdc_1*/ - ) -# endif // if USES_HWCDC + porttype == ESPEasySerialPort::usb_cdc_1*/) +#endif { result += getConflictingUse(PIN_USB_D_MIN); result += formatGpioLabel(PIN_USB_D_MIN, false); @@ -66,9 +63,8 @@ String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pi result += formatGpioLabel(PIN_USB_D_PLUS, false); return result; } -# endif // if USES_HWCDC || USES_USBCDC - - if (useGPIOpins(porttype)) +#endif + if (useGPIOpins(porttype)) { result += F("RX: "); result += formatGpioLabel(config_pin1, false); @@ -85,25 +81,26 @@ void serialHelper_getGpioNames(struct EventStruct *event, bool rxOptional, bool } void serialHelper_modbus_getGpioNames(struct EventStruct *event, - bool rxOptional, - bool txOptional, - bool DE_RE_optional) + bool rxOptional, + bool txOptional, + bool DE_RE_optional) { serialHelper_getGpioNames(event, rxOptional, txOptional); event->String1.replace(F("TX"), F("TX (RO)")); event->String2.replace(F("RX"), F("RX (DI)")); - - if (DE_RE_optional) { + if (DE_RE_optional) event->String3 = formatGpioName_output_optional(F("~RE/DE")); - } - else { + else event->String3 = formatGpioName_output(F("~RE/DE")); - } } -int8_t serialHelper_getRxPin(struct EventStruct *event) { return CONFIG_PIN1; } +int8_t serialHelper_getRxPin(struct EventStruct *event) { + return CONFIG_PIN1; +} -int8_t serialHelper_getTxPin(struct EventStruct *event) { return CONFIG_PIN2; } +int8_t serialHelper_getTxPin(struct EventStruct *event) { + return CONFIG_PIN2; +} ESPEasySerialPort serialHelper_getSerialType(struct EventStruct *event) { ESPEasySerialPort serialType = static_cast(CONFIG_PORT); @@ -122,14 +119,13 @@ String serialHelper_getSerialTypeLabel(struct EventStruct *event) { return serialHelper_getSerialTypeLabel(serialHelper_getSerialType(event)); } -# ifndef DISABLE_SC16IS752_Serial - +#ifndef DISABLE_SC16IS752_Serial void serialHelper_addI2CuartSelectors(int address, int channel) { - # define SC16IS752_I2C_ADDRESSES 16 - # define SC16IS752_I2C_BASE_ADDR (0x90 >> 1) - # define SC16IS752_CHANNELS 2 - # define SC16IS752_CHANNEL_A 0x00 - # define SC16IS752_CHANNEL_B 0x01 + # define SC16IS752_I2C_ADDRESSES 16 + # define SC16IS752_I2C_BASE_ADDR (0x90 >> 1) + # define SC16IS752_CHANNELS 2 + # define SC16IS752_CHANNEL_A 0x00 + # define SC16IS752_CHANNEL_B 0x01 { String id = F("i2cuart_addr"); addRowLabel_tr_id(F("I2C Address"), id); @@ -161,79 +157,23 @@ void serialHelper_addI2CuartSelectors(int address, int channel) { F("A"), F("B"), }; - /* - const int chValues[] = { - SC16IS752_CHANNEL_A, - SC16IS752_CHANNEL_B, - }; - */ + const int chValues[] = { + SC16IS752_CHANNEL_A, + SC16IS752_CHANNEL_B, + }; + */ const FormSelectorOptions selector(NR_ELEMENTS(chOptions), chOptions); selector.addFormSelector(F("Channel"), F("i2cuart_ch"), channel); } } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Flashmark: experimental extension -// Allow multiple ports being configured on a single web page, by using a linkId to identify the port. -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void serialHelper_addI2CuartSelectors(int linkId, int address, int channel) { - { - String id = strformat(F("i2cuart%d_addr"), linkId); - addRowLabel_tr_id(F("I2C Address"), id); - do_addSelector_Head(id, F(""), EMPTY_STRING, false); +#endif // ifndef DISABLE_SC16IS752_Serial - if ((address < SC16IS752_I2C_BASE_ADDR) || (address >= (SC16IS752_I2C_BASE_ADDR + SC16IS752_I2C_ADDRESSES))) { - // selected address is not in range - address = SC16IS752_I2C_BASE_ADDR; - } - - for (int i = 0; i < SC16IS752_I2C_ADDRESSES; i++) - { - int addr = SC16IS752_I2C_BASE_ADDR + i; - String option; - option.reserve(24); - option = formatToHex(addr); - option += F(" (datasheet: "); - option += formatToHex(addr * 2); - option += ')'; - addSelector_Item(option, addr, addr == address); - } - addSelector_Foot(); - } - { - if ((channel != SC16IS752_CHANNEL_A) && (channel != SC16IS752_CHANNEL_B)) { - channel = SC16IS752_CHANNEL_A; - } - const __FlashStringHelper *chOptions[] = { - F("A"), - F("B"), - }; - const FormSelectorOptions selector(NR_ELEMENTS(chOptions), chOptions); - selector.addFormSelector(F("Channel"), strformat(F("i2cuart%d_ch"), linkId), channel); - } -} - -# endif // ifndef DISABLE_SC16IS752_Serial - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Flashmark: experimental extension -// Add selectors for RX and TX pins for a serial port, identified by linkId. -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void serialHelper_addPinSelectors(int8_t linkId, int rxPin, int txPin) { - String id = strformat(F("serial%d_txpin"), linkId); - - addRowLabel_tr_id(formatGpioName_serialTX(false), id); - addPinSelect(PinSelectPurpose::Serial_input, id, txPin); - - id = strformat(F("serial%d_rxpin"), linkId); - addRowLabel_tr_id(formatGpioName_serialRX(false), id); - addPinSelect(PinSelectPurpose::Serial_output, id, rxPin); +void serialHelper_webformLoad(struct EventStruct *event) { + serialHelper_webformLoad(event, true); } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void serialHelper_webformLoad(struct EventStruct *event) { serialHelper_webformLoad(event, true); } - // These helper functions were made to create a generic interface to setup serial port config. // See issue #2343 and Pull request https://github.com/letscontrolit/ESPEasy/pull/2352 // For now P020 and P044 have been reverted to make them work again. @@ -244,11 +184,10 @@ void serialHelper_webformLoad(struct EventStruct *event, bool allowSoftwareSeria allowSoftwareSerial); } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef, bool allowSoftwareSerial) { // Field for I2C addr & RX are shared // Field for channel and TX are shared - # ifdef ESP8266 + #ifdef ESP8266 // Script to show GPIO pins for SoftwareSerial or I2C addresses for the I2C to UART bridge // "function serialPortChanged(elem) {var style = elem.value == 6 ? '' : 'none';var i2cstyle = elem.value == 1 ? '' : @@ -261,8 +200,8 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef "document.getElementById('tr_i2cuart_addr').style.display=l," "document.getElementById('tr_i2cuart_ch').style.display=l}"), false); - # endif // ifdef ESP8266 - # ifdef ESP32 + #endif // ifdef ESP8266 + #ifdef ESP32 // Script to show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge // "function serialPortChanged(elem) {var style = (elem.value == 2 || elem.value == 4 || elem.value == 5) ? '' : 'none';var i2cstyle = @@ -270,276 +209,103 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int rxPinDef, int txPinDef // style;document.getElementById('tr_taskdevicepin2').style.display = style;document.getElementById('tr_i2cuart_addr').style.display = // i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;}"), html_add_script(F("function serialPortChanged(elem) {" - " var style = 'none';" - " var i2cstyle = elem.value == 1 ? '' : 'none';" - " if (elem.value == 2) {" - -// " document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX0) ";" -// " document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX0) ";" - " style = '';" -# if USABLE_SOC_UART_NUM > 1 - " } else if (elem.value == 4) {" - - // " document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX1) ";" -// " document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX1) ";" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 1 -# if USABLE_SOC_UART_NUM > 2 - " } else if (elem.value == 5) {" - -// " document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX2) ";" -// " document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX2) ";" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 2 -# if USABLE_SOC_UART_NUM > 3 - " } else if (elem.value == 10) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 3 -# if USABLE_SOC_UART_NUM > 4 - " } else if (elem.value == 11) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 4 -# if USABLE_SOC_UART_NUM > 5 - " } else if (elem.value == 12) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 5 -# if USES_SW_SERIAL - " } else if (elem.value == 6) {" - " style = '';" -# endif // if USES_SW_SERIAL - " }" - " document.getElementById('tr_taskdevicepin1').style.display = style;" - " document.getElementById('tr_taskdevicepin2').style.display = style;" - " document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;" - " document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;" - "}"), - true); - # endif // ifdef ESP32 - - # if !USES_SW_SERIAL - allowSoftwareSerial = false; - # endif - - const int ids[] = { - static_cast(ESPEasySerialPort::not_set) - , static_cast(ESPEasySerialPort::serial0) -# ifdef ESP8266 - , static_cast(ESPEasySerialPort::serial0_swap) -# endif // ifdef ESP8266 +" var style = 'none';" +" var i2cstyle = elem.value == 1 ? '' : 'none';" +" if (elem.value == 2) {" +//" document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX0) ";" +//" document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX0) ";" +" style = '';" # if USABLE_SOC_UART_NUM > 1 - , static_cast(ESPEasySerialPort::serial1) -# endif +" } else if (elem.value == 4) {" +//" document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX1) ";" +//" document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX1) ";" +" style = '';" +#endif # if USABLE_SOC_UART_NUM > 2 - , static_cast(ESPEasySerialPort::serial2) -# endif // if USABLE_SOC_UART_NUM > 2 +" } else if (elem.value == 5) {" +//" document.querySelector('#taskdevicepin1').value =" STRINGIFY(SOC_RX2) ";" +//" document.querySelector('#taskdevicepin2').value =" STRINGIFY(SOC_TX2) ";" +" style = '';" +#endif # if USABLE_SOC_UART_NUM > 3 - , static_cast(ESPEasySerialPort::serial3) -# endif +" } else if (elem.value == 10) {" +" style = '';" +#endif # if USABLE_SOC_UART_NUM > 4 - , static_cast(ESPEasySerialPort::serial4) -# endif +" } else if (elem.value == 11) {" +" style = '';" +#endif # if USABLE_SOC_UART_NUM > 5 - , static_cast(ESPEasySerialPort::serial5) -# endif -# if USES_SW_SERIAL - , static_cast(ESPEasySerialPort::software) -# endif // if USES_SW_SERIAL - -# if USES_HWCDC - , static_cast(ESPEasySerialPort::usb_hw_cdc) -# endif // if USES_HWCDC -# if USES_USBCDC - , static_cast(ESPEasySerialPort::usb_cdc_0) - - // ,static_cast(ESPEasySerialPort::usb_cdc_1) -# endif // if USES_USBCDC -# if USES_I2C_SC16IS752 - , static_cast(ESPEasySerialPort::sc16is752) -# endif // if USES_I2C_SC16IS752 - }; - - constexpr int NR_ESPEASY_SERIAL_TYPES = NR_ELEMENTS(ids); - String options[NR_ESPEASY_SERIAL_TYPES]; - - // String attr[NR_ESPEASY_SERIAL_TYPES]; - - for (int i = 0; (i < NR_ESPEASY_SERIAL_TYPES); ++i) { - ESPEasySerialPort serType = static_cast(ids[i]); - - String option; - option.reserve(48); - option = serialHelper_getSerialTypeLabel(serType); -# ifdef ESP8266 - int rxPin, txPin; - - if (ESPeasySerialType::getSerialTypePins(serType, rxPin, txPin)) { - - if (isHWserial(serType)) - { - // Show pins for ports with fixed pins - option += F(": "); - option += formatGpioLabel(rxPin, false); - option += ' '; - option += formatGpioDirection(gpio_direction::gpio_input); - option += F("TX / "); - option += formatGpioLabel(txPin, false); - option += ' '; - option += formatGpioDirection(gpio_direction::gpio_output); - option += F("RX"); - } - } -# endif // ifdef ESP8266 - options[i] = option; - } - FormSelectorOptions selector(NR_ESPEASY_SERIAL_TYPES, options, ids); - - // Script to toggle GPIO visibility when changing selection. - selector.onChangeCall = F("serialPortChanged(this)"); - selector.addFormSelector( - F("Serial Port"), F("serPort"), - static_cast(ESPeasySerialType::getSerialType(port, rxPinDef, txPinDef))); -# if USES_I2C_SC16IS752 - serialHelper_addI2CuartSelectors(rxPinDef, txPinDef); -# endif // ifndef DISABLE_SC16IS752_Serial - -# ifdef ESP8266 - - if ((rxPinDef == 15) || (txPinDef == 15)) { - addFormNote(F("GPIO-15 (D8) requires a Buffer Circuit (PNP transistor) or ESP boot may fail.")); - } -# endif // ifdef ESP8266 -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Flashmark: experimental extension -// Allow multiple ports being configured on a single web page, by using a linkId to identify the port. -// Port configuration depends on the selected port type. A script is used to show/hide the correct fields for the selected port type. -// Fields are either: -// - Empty when configuration is fixed by hardware (e.g. ESP8266 UART) -// - Configurable RX and TX GPIO pins (e.g. ESP32 UART, software serial) -// - Configurable I2C address and channel (e.g. SC16IS752 I2C to UART bridge) -// Parameters rxPinDef and txPinDef are used for either RX and TX pin fields or the I2C address and channel field. -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void serialHelper_webformLoad(int8_t linkId, ESPEasySerialPort port, int rxPinDef, int txPinDef, bool allowSoftwareSerial) { - - # ifdef ESP8266 - - // Script to show GPIO pins for SoftwareSerial or I2C addresses for the I2C to UART bridge - // "function serialPortChanged(elem) {var style = elem.value == 6 ? '' : 'none';var i2cstyle = elem.value == 1 ? '' : - // 'none';document.getElementById('tr_taskdevicepin1').style.display = style;document.getElementById('tr_taskdevicepin2').style.display = - // style;document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = - // i2cstyle;}"), - html_add_script(F("function serialPortChanged(e){var t=6==e.value?'':'none',l=1==e.value?'':'none';" - "document.getElementById('tr_taskdevicepin1').style.display=t," - "document.getElementById('tr_taskdevicepin2').style.display=t," - "document.getElementById('tr_i2cuart_addr').style.display=l," - "document.getElementById('tr_i2cuart_ch').style.display=l}"), - false); - # endif // ifdef ESP8266 - # ifdef ESP32 +" } else if (elem.value == 12) {" +" style = '';" +#endif +#if USES_SW_SERIAL +" } else if (elem.value == 6) {" +" style = '';" +#endif +" }" +" document.getElementById('tr_taskdevicepin1').style.display = style;" +" document.getElementById('tr_taskdevicepin2').style.display = style;" +" document.getElementById('tr_i2cuart_addr').style.display = i2cstyle;" +" document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;" +"}"), + true); + #endif // ifdef ESP32 - // Build a script to dynamically show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge - // Script to show GPIO pins for HW serial ports or I2C addresses for the I2C to UART bridge - // "function serialPortChanged(elem) {var style = (elem.value == 2 || elem.value == 4 || elem.value == 5) ? '' : 'none';var i2cstyle = - // elem.value == 1 ? '' : 'none';document.getElementById('tr_taskdevicepin1').style.display = - // style;document.getElementById('tr_taskdevicepin2').style.display = style;document.getElementById('tr_i2cuart_addr').style.display = - // i2cstyle;document.getElementById('tr_i2cuart_ch').style.display = i2cstyle;}"), - html_add_script(true); - - addHtml(strformat(F("function serialPort%dChanged(elem) {"), linkId)); - addHtml(F(" var style = 'none';" - " var i2cstyle = elem.value == 1 ? '' : 'none';" - " if (elem.value == 2) {" - " style = '';" -# if USABLE_SOC_UART_NUM > 1 - " } else if (elem.value == 4) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 1 -# if USABLE_SOC_UART_NUM > 2 - " } else if (elem.value == 5) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 2 -# if USABLE_SOC_UART_NUM > 3 - " } else if (elem.value == 10) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 3 -# if USABLE_SOC_UART_NUM > 4 - " } else if (elem.value == 11) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 4 -# if USABLE_SOC_UART_NUM > 5 - " } else if (elem.value == 12) {" - " style = '';" -# endif // if USABLE_SOC_UART_NUM > 5 -# if USES_SW_SERIAL - " } else if (elem.value == 6) {" - " style = '';" -# endif // if USES_SW_SERIAL - " }" - )); - addHtml(strformat(F(" document.getElementById('tr_serial%d_rxpin').style.display = style;"), linkId)); - addHtml(strformat(F(" document.getElementById('tr_serial%d_txpin').style.display = style;"), linkId)); - addHtml(strformat(F(" document.getElementById('tr_i2cuart%d_addr').style.display = i2cstyle;"), linkId)); - addHtml(strformat(F(" document.getElementById('tr_i2cuart%d_ch').style.display = i2cstyle;}"), linkId)); - html_add_script_end(); - - # endif // ifdef ESP32 - - // Script done. Build the selector for the serial port type. - # if !USES_SW_SERIAL + #if !USES_SW_SERIAL allowSoftwareSerial = false; - # endif + #endif const int ids[] = { - static_cast(ESPEasySerialPort::not_set) - , static_cast(ESPEasySerialPort::serial0) -# ifdef ESP8266 - , static_cast(ESPEasySerialPort::serial0_swap) -# endif // ifdef ESP8266 -# if USABLE_SOC_UART_NUM > 1 - , static_cast(ESPEasySerialPort::serial1) -# endif -# if USABLE_SOC_UART_NUM > 2 - , static_cast(ESPEasySerialPort::serial2) -# endif // if USABLE_SOC_UART_NUM > 2 -# if USABLE_SOC_UART_NUM > 3 - , static_cast(ESPEasySerialPort::serial3) -# endif -# if USABLE_SOC_UART_NUM > 4 - , static_cast(ESPEasySerialPort::serial4) -# endif -# if USABLE_SOC_UART_NUM > 5 - , static_cast(ESPEasySerialPort::serial5) -# endif -# if USES_SW_SERIAL - , static_cast(ESPEasySerialPort::software) -# endif // if USES_SW_SERIAL - -# if USES_HWCDC - , static_cast(ESPEasySerialPort::usb_hw_cdc) -# endif // if USES_HWCDC -# if USES_USBCDC - , static_cast(ESPEasySerialPort::usb_cdc_0) - - // ,static_cast(ESPEasySerialPort::usb_cdc_1) -# endif // if USES_USBCDC -# if USES_I2C_SC16IS752 - , static_cast(ESPEasySerialPort::sc16is752) -# endif // if USES_I2C_SC16IS752 + static_cast(ESPEasySerialPort::not_set) + ,static_cast(ESPEasySerialPort::serial0) +#ifdef ESP8266 + ,static_cast(ESPEasySerialPort::serial0_swap) +#endif // ifdef ESP8266 +#if USABLE_SOC_UART_NUM > 1 + ,static_cast(ESPEasySerialPort::serial1) +#endif +#if USABLE_SOC_UART_NUM > 2 + ,static_cast(ESPEasySerialPort::serial2) +#endif // if USABLE_SOC_UART_NUM > 2 +#if USABLE_SOC_UART_NUM > 3 + ,static_cast(ESPEasySerialPort::serial3) +#endif +#if USABLE_SOC_UART_NUM > 4 + ,static_cast(ESPEasySerialPort::serial4) +#endif +#if USABLE_SOC_UART_NUM > 5 + ,static_cast(ESPEasySerialPort::serial5) +#endif +#if USES_SW_SERIAL + ,static_cast(ESPEasySerialPort::software) +#endif // if USES_SW_SERIAL + +#if USES_HWCDC + ,static_cast(ESPEasySerialPort::usb_hw_cdc) +#endif // if USES_HWCDC +#if USES_USBCDC + ,static_cast(ESPEasySerialPort::usb_cdc_0) +// ,static_cast(ESPEasySerialPort::usb_cdc_1) +#endif // if USES_USBCDC +#if USES_I2C_SC16IS752 + ,static_cast(ESPEasySerialPort::sc16is752) +#endif // if USES_I2C_SC16IS752 }; constexpr int NR_ESPEASY_SERIAL_TYPES = NR_ELEMENTS(ids); String options[NR_ESPEASY_SERIAL_TYPES]; +// String attr[NR_ESPEASY_SERIAL_TYPES]; + for (int i = 0; (i < NR_ESPEASY_SERIAL_TYPES); ++i) { ESPEasySerialPort serType = static_cast(ids[i]); String option; option.reserve(48); option = serialHelper_getSerialTypeLabel(serType); -# ifdef ESP8266 +#ifdef ESP8266 int rxPin, txPin; - if (ESPeasySerialType::getSerialTypePins(serType, rxPin, txPin)) { if (isHWserial(serType)) @@ -556,68 +322,30 @@ void serialHelper_webformLoad(int8_t linkId, ESPEasySerialPort port, int rxPinDe option += F("RX"); } } -# endif // ifdef ESP8266 +#endif options[i] = option; } FormSelectorOptions selector(NR_ESPEASY_SERIAL_TYPES, options, ids); // Script to toggle GPIO visibility when changing selection. - selector.onChangeCall = strformat(F("serialPort%dChanged(this)"), linkId); + selector.onChangeCall = F("serialPortChanged(this)"); selector.addFormSelector( - F("Serial Port"), strformat(F("serPort%d"), linkId), static_cast(port)); - - serialHelper_addPinSelectors(linkId, rxPinDef, txPinDef); -# if USES_I2C_SC16IS752 - serialHelper_addI2CuartSelectors(linkId, rxPinDef, txPinDef); -# endif // ifndef DISABLE_SC16IS752_Serial - - html_add_script(strformat(F("document.getElementById('serPort%d').onchange();"), linkId), true); + F("Serial Port"), F("serPort"), + static_cast(ESPeasySerialType::getSerialType(port, rxPinDef, txPinDef))); +#if USES_I2C_SC16IS752 + serialHelper_addI2CuartSelectors(rxPinDef, txPinDef); +#endif // ifndef DISABLE_SC16IS752_Serial -# ifdef ESP8266 +#ifdef ESP8266 if ((rxPinDef == 15) || (txPinDef == 15)) { addFormNote(F("GPIO-15 (D8) requires a Buffer Circuit (PNP transistor) or ESP boot may fail.")); } -# endif // ifdef ESP8266 -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Flashmark: experimental extension -// Allow multiple ports being configured on a single web page, by using a linkId to identify the port. -// Save one entry of a serial port from a webform as setup with serialHelper_webformLoad() -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void serialHelper_webformSave(int8_t linkId, uint8_t& port, int8_t& rxPin, int8_t& txPin) { - - port = getFormItemInt(strformat(F("serPort%d"), linkId), port); - - rxPin = getFormItemInt(strformat(F("serial%d_rxpin"), linkId), rxPin); - txPin = getFormItemInt(strformat(F("serial%d_txpin"), linkId), txPin); - - # ifndef DISABLE_SC16IS752_Serial - - if (static_cast(port) == ESPEasySerialPort::sc16is752) { - rxPin = getFormItemInt(strformat(F("i2cuart%d_addr"), linkId), rxPin); - txPin = getFormItemInt(strformat(F("i2cuart%d_ch"), linkId), txPin); - } - # endif // ifndef DISABLE_SC16IS752_Serial - # ifdef ESP8266 - - if (isHWserial(serType)) { - - // Ports with a fixed pin layout, so load the defaults. - int tmprxPin, tmptxPin; - - if (ESPeasySerialType::getSerialTypePins(serType, tmprxPin, tmptxPin)) { - rxPin = tmprxPin; - txPin = tmptxPin; - } - } - # endif // ifdef ESP8266 +#endif // ifdef ESP8266 } void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) { int serialPortSelected = getFormItemInt(F("serPort"), -1); - if (serialPortSelected < 0) { return; } rxPin = getFormItemInt(F("taskdevicepin1"), -1); @@ -627,15 +355,13 @@ void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) { port = serialPortSelected; - # ifndef DISABLE_SC16IS752_Serial - + #ifndef DISABLE_SC16IS752_Serial if (serType == ESPEasySerialPort::sc16is752) { rxPin = getFormItemInt(F("i2cuart_addr"), rxPin); txPin = getFormItemInt(F("i2cuart_ch"), txPin); } - # endif // ifndef DISABLE_SC16IS752_Serial - # ifdef ESP8266 - + #endif // ifndef DISABLE_SC16IS752_Serial + #ifdef ESP8266 if (isHWserial(serType)) { // Ports with a fixed pin layout, so load the defaults. @@ -646,10 +372,12 @@ void serialHelper_webformSave(uint8_t& port, int8_t& rxPin, int8_t& txPin) { txPin = tmptxPin; } } - # endif // ifdef ESP8266 + #endif // ifdef ESP8266 } -void serialHelper_webformSave(struct EventStruct *event) { serialHelper_webformSave(CONFIG_PORT, CONFIG_PIN1, CONFIG_PIN2); } +void serialHelper_webformSave(struct EventStruct *event) { + serialHelper_webformSave(CONFIG_PORT, CONFIG_PIN1, CONFIG_PIN2); +} bool serialHelper_isValid_serialconfig(uint8_t serialconfig) { if ((serialconfig >= 0x10) && (serialconfig <= 0x3f)) { @@ -679,27 +407,18 @@ void serialHelper_serialconfig_webformLoad(struct EventStruct *event, uint8_t cu label += F(" bit / parity: "); int value = ((bits - 5) << 2); - switch (parity) - { - case 0: label += F("None"); - break; - case 1: label += F("Even"); - value += 2; - break; - case 2: label += F("Odd"); - value += 3; - break; + switch (parity) { + case 0: label += F("None"); break; + case 1: label += F("Even"); value += 2; break; + case 2: label += F("Odd"); value += 3; break; } label += F(" / stop bits: "); label += String(stopBits); // There are also values for 0 and "1.5" stop bit, not used now. - switch (stopBits) - { - case 1: value += 0x10; - break; - case 2: value += 0x30; - break; + switch (stopBits) { + case 1: value += 0x10; break; + case 2: value += 0x30; break; } addSelector_Item(label, value, value == currentSelection); } @@ -741,4 +460,4 @@ uint8_t serialHelper_convertOldSerialConfig(uint8_t newLocationConfig) { return static_cast(SERIAL_8N1 & 0xFF); // Some default } -#endif // ifdef PLUGIN_USES_SERIAL +#endif \ No newline at end of file diff --git a/src/src/Helpers/_Plugin_Helper_serial.h b/src/src/Helpers/_Plugin_Helper_serial.h index 3099d653d7..c889331ef2 100644 --- a/src/src/Helpers/_Plugin_Helper_serial.h +++ b/src/src/Helpers/_Plugin_Helper_serial.h @@ -6,17 +6,17 @@ #ifdef PLUGIN_USES_SERIAL -# include +#include struct ESPeasySerialType; const __FlashStringHelper* serialHelper_getSerialTypeLabel(ESPEasySerialPort serType, - bool shortName = false); + bool shortName = false); -void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, - int config_pin1, - int config_pin2); +void serialHelper_log_GpioDescription(ESPEasySerialPort typeHint, + int config_pin1, + int config_pin2); String serialHelper_getGpioDescription(ESPEasySerialPort typeHint, int config_pin1, @@ -28,9 +28,9 @@ void serialHelper_getGpioNames(struct EventStruct *event, bool txOptional = false); void serialHelper_modbus_getGpioNames(struct EventStruct *event, - bool rxOptional = false, - bool txOptional = false, - bool DE_RE_optional = true); + bool rxOptional = false, + bool txOptional = false, + bool DE_RE_optional = true); int8_t serialHelper_getRxPin(struct EventStruct *event); @@ -40,14 +40,10 @@ ESPEasySerialPort serialHelper_getSerialType(struct EventStruct *event); String serialHelper_getSerialTypeLabel(struct EventStruct *event); -# ifndef DISABLE_SC16IS752_Serial -void serialHelper_addI2CuartSelectors(int address, - int channel); - -void serialHelper_addI2CuartSelectors(int linkId, - int address, - int channel); -# endif // ifndef DISABLE_SC16IS752_Serial +#ifndef DISABLE_SC16IS752_Serial +void serialHelper_addI2CuartSelectors(int address, + int channel); +#endif // ifndef DISABLE_SC16IS752_Serial void serialHelper_webformLoad(struct EventStruct *event); @@ -62,21 +58,11 @@ void serialHelper_webformLoad(ESPEasySerialPort port, int txPinDef, bool allowSoftwareSerial); -void serialHelper_webformLoad(int8_t linkId, - ESPEasySerialPort port, - int rxPinDef, - int txPinDef, - bool allowSoftwareSerial); void serialHelper_webformSave(uint8_t& port, int8_t & rxPin, int8_t & txPin); -void serialHelper_webformSave(struct EventStruct *event); - -void serialHelper_webformSave(int8_t linkId, - uint8_t& port, - int8_t & rxPin, - int8_t & txPin); +void serialHelper_webformSave(struct EventStruct *event); bool serialHelper_isValid_serialconfig(uint8_t serialconfig); From 7b7649bc148f88b5c50d86ed2e977bbdd2aebd69 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Sun, 26 Jul 2026 17:26:22 +0200 Subject: [PATCH 47/48] P183: Adapted to new serial helper libraray --- src/src/Helpers/Modbus_mgr.cpp | 107 +++++++++++++---------- src/src/Helpers/Modbus_mgr.h | 12 +-- src/src/PluginStructs/P183_data_struct.h | 8 +- 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 55c4bf3f8f..1b59f3ba3d 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -223,12 +223,12 @@ bool ModbusMGR_struct::queueTransaction(int linkId, Modbus_Transaction *transact bool returnValue = false; # ifdef MODBUS_DEBUG - String log = strformat(F("Modbus Manager: Queue transaction for linkId= %d "), linkId); + String log = concat(F("Modbus Manager: Queue transaction for linkId= "), linkId); # endif // ifdef MODBUS_DEBUG if (!MODBUS_MGR_VALID_LINK_ID(linkId)) { # ifdef MODBUS_DEBUG - log += F("Invalid linkId"); + log += F(" Invalid linkId"); addLogMove(LOG_LEVEL_ERROR, log); # endif // ifdef MODBUS_DEBUG } @@ -289,41 +289,64 @@ void ModbusMGR_struct::dumpAdminInfo() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ModbusMGR_struct::show_modbus_interfaces() { - String options_baudrate[MODBUS_MAX_BAUDRATE_SEL]; // Array to hold the baudrate options for the selector - String options_port[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Port otions for the selector, only valid ports will be - // filled. - int portMap[static_cast(ESPEasySerialPort::MAX_SERIAL_TYPE)]; // Map to keep track of valid ports and their indices in the - // options_port array + String options_baudrate[MODBUS_MAX_BAUDRATE_SEL]{}; // Array to hold the baudrate options for the selector constexpr int optionBaudCount = static_cast(NR_ELEMENTS(options_baudrate)); for (int i = 0; i < optionBaudCount; ++i) { options_baudrate[i] = modbus_storageValueToBaudrate(i); // Fill the baudrate options with actual baudrate values } - const FormSelectorOptions baudselector(optionBaudCount, options_baudrate); + const FormSelectorOptions baudselector(optionBaudCount, + options_baudrate); // Iterate over the modbus links and show their configuration on the web page for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { - addFormSubHeader(strformat(F("Modbus %u"), link)); - addFormDetailsStart(link == 0 || _modbus_links[link].port != ESPEasySerialPort::not_set); -//// serialHelper_webformLoad(link, _modbus_links[link].port, static_cast(_modbus_links[link].serial_rx), -//// static_cast(_modbus_links[link].serial_tx), true); + addFormSubHeader(concat(F("Modbus "), link)); + addFormDetailsStart(link == 0 || _modbus_links[link].port != ESPEasySerialPort::not_set); - String id = strformat(F("MBde%u"), link); - addRowLabel_tr_id(formatGpioName_output_optional(F("~RE/DE")), id); - addPinSelect(PinSelectPurpose::Generic_output, id, _modbus_links[link].dere_pin); + const String slid = concat(F("MBport"), link); + const String txid = concat(F("MBtx"), link); + const String rxid = concat(F("MBrx"), link); + + serialHelper_webformLoad(_modbus_links[link].port, + _modbus_links[link].serial_rx, + _modbus_links[link].serial_tx, + INCLUDE_HW_SERIAL + INCLUDE_SW_SERIAL + INCLUDE_I2C_SERIAL, + F("Serial Port"), + slid, + txid, + rxid, + concat(F("i2cad"), link), + concat(F("i2cch"), link)); + + addFormPinSelect(PinSelectPurpose::Serial_input, + formatGpioName_serialTX(false), + txid, + _modbus_links[link].serial_tx); + + addFormPinSelect(PinSelectPurpose::Serial_output, + formatGpioName_serialRX(false), + rxid, + _modbus_links[link].serial_rx); + + html_add_script(strformat(F("elId('%s').onchange();"), slid.c_str()), false); + + addFormPinSelect(PinSelectPurpose::Generic_output, + formatGpioName_output_optional(F("~RE/DE")), + concat(F("MBde"), link), + _modbus_links[link].dere_pin); baudselector.addFormSelector(F("Baud Rate"), - strformat(F("MBbaud%u"), link), + concat(F("MBbaud"), link), modbus_baudrateToStorageValue(_modbus_links[link].baudrate)); addUnit(F("baud")); - # ifdef ESP32 - addFormCheckBox(F("Enable Collision Detection"), strformat(F("MBcoll%u"), link), _modbus_links[link].collision_detect); + # ifdef ESP32 + addFormCheckBox(F("Enable Collision Detection"), concat(F("MBcoll"), link), _modbus_links[link].collision_detect); addFormNote(F("/RE connected to GND, only supported on hardware serial")); - # endif // ifdef ESP32 + # endif // ifdef ESP32 addFormDetailsEnd(); @@ -337,7 +360,7 @@ void ModbusMGR_struct::show_modbus_interfaces() // Values outside this range will be mapped to the closest valid value. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// uint32_t modbus_storageValueToBaudrate(uint8_t baudrate_setting) { - if ((baudrate_setting > 7) || (baudrate_setting < 0)) { return 9600; } + if (baudrate_setting > 7) { return 9600; } switch (baudrate_setting) { @@ -384,43 +407,37 @@ uint8_t modbus_baudrateToStorageValue(uint32_t baudrate) { bool ModbusMGR_struct::save_modbus_interfaces(String& error) { for (int link = 0; link < MAX_MODBUS_LINKS; ++link) { + int baudrate_setting; int port_setting = static_cast(_modbus_links[link].port); int tx_setting = _modbus_links[link].serial_tx; int rx_setting = _modbus_links[link].serial_rx; - int baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); int dere_setting = _modbus_links[link].dere_pin; bool collision_detect_setting = _modbus_links[link].collision_detect; bool settingsChanged = false; - int8_t rxPin = rx_setting; - int8_t txPin = tx_setting; - uint8_t port = port_setting; -//// serialHelper_webformSave(link, port, rxPin, txPin); + update_whenset_FormItemInt(concat(F("MBport"), link), port_setting, &settingsChanged); + baudrate_setting = modbus_baudrateToStorageValue(_modbus_links[link].baudrate); + update_whenset_FormItemInt(concat(F("MBbaud"), link), baudrate_setting, &settingsChanged); + tx_setting = _modbus_links[link].serial_tx; + rx_setting = _modbus_links[link].serial_rx; - if (port != port_setting) { - port_setting = port; - settingsChanged = true; + if (port_setting == static_cast(ESPEasySerialPort::sc16is752)) { + update_whenset_FormItemInt(concat(F("i2cad"), link), rx_setting, &settingsChanged); + update_whenset_FormItemInt(concat(F("i2cch"), link), tx_setting, &settingsChanged); } - - if (rxPin != rx_setting) { - rx_setting = rxPin; - settingsChanged = true; - } - - if (txPin != tx_setting) { - tx_setting = txPin; - settingsChanged = true; + else { + update_whenset_FormItemInt(concat(F("MBtx"), link), tx_setting, &settingsChanged); + update_whenset_FormItemInt(concat(F("MBrx"), link), rx_setting, &settingsChanged); } + dere_setting = _modbus_links[link].dere_pin; - update_whenset_FormItemInt(strformat(F("MBbaud%u"), link), baudrate_setting, &settingsChanged); - - if (update_whenset_FormItemInt(strformat(F("MBde%u"), link), dere_setting, &settingsChanged)) + if (update_whenset_FormItemInt(concat(F("MBde"), link), dere_setting, &settingsChanged)) { # ifdef ESP32 // Checkbox existence cannot be determined from the HTML response. Assume its there when dere_setting is detected. // The Collision detection setting is only available on ESP32 and only when a DE/RE pin is configured. - collision_detect_setting = isFormItemChecked(strformat(F("MBcoll%u"), link)); + collision_detect_setting = isFormItemChecked(concat(F("MBcoll"), link)); settingsChanged |= (collision_detect_setting != _modbus_links[link].collision_detect); # endif // ifdef ESP32 } @@ -435,7 +452,7 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) collision_detect_setting); } } - return false; + return true; // Success } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -453,14 +470,14 @@ bool ModbusMGR_struct::setLink(const int linkIndex, String log = F("Modbus_mgr: SetLink. "); addLogMove(LOG_LEVEL_INFO, - strformat(F("*** setlink***: LinkID=%d, Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, CollisionDetect=%s"), + strformat(F("setlink: LinkID=%d, Port=%s, RX=%d, TX=%d, Baudrate=%d, DerePin=%d, CollisionDetect=%s"), linkIndex, ESPEasySerialPort_toString(port), serial_rx, serial_tx, baudrate, dere_pin, - collision_detect ? F("Yes") : F("No") + FsP(collision_detect ? F("Yes") : F("No")) )); # endif // ifdef MODBUS_DEBUG @@ -481,7 +498,7 @@ bool ModbusMGR_struct::setLink(const int linkIndex, else if ((port == ESPEasySerialPort::not_set) && (_modbus_links[linkIndex].link != nullptr)) { delete _modbus_links[linkIndex].link; _modbus_links[linkIndex].link = nullptr; - } + } // If the link object is enabled (has a valid pointer), initialize it with the new parameters if (_modbus_links[linkIndex].link != nullptr) { diff --git a/src/src/Helpers/Modbus_mgr.h b/src/src/Helpers/Modbus_mgr.h index a175f2d810..6fb1a84874 100644 --- a/src/src/Helpers/Modbus_mgr.h +++ b/src/src/Helpers/Modbus_mgr.h @@ -24,10 +24,10 @@ typedef struct ModbusMGR_struct { bool initialize(); void processLinks(); - bool connect(int linkId, + bool connect(int linkId, const ModbusDEVICE_struct *deviceID); - bool disconnect(int linkId, + bool disconnect(int linkId, const ModbusDEVICE_struct*deviceID); Modbus_transaction_ptr newTransaction(int linkId, @@ -63,11 +63,11 @@ typedef struct ModbusMGR_struct { }; - static bool validLinkId(int linkId) { return ((linkId >= 0) && (linkId < ModbusMGR_struct::MAX_MODBUS_LINKS)); } + static bool validLinkId(int linkId) { return (linkId >= 0) && (linkId < ModbusMGR_struct::MAX_MODBUS_LINKS); } - ModbusLinkInfo_struct _modbus_links[MAX_MODBUS_LINKS] = {}; // All links managed by the Modbus manager - bool _initialized = false; // Flag indicating if the manager is initialized - int _deviceCounter = 0; + ModbusLinkInfo_struct _modbus_links[MAX_MODBUS_LINKS]{}; // All links managed by the Modbus manager + bool _initialized = false; // Flag indicating if the manager is initialized + int _deviceCounter = 0; bool setLink(const int linkIndex, const ESPEasySerialPort port, diff --git a/src/src/PluginStructs/P183_data_struct.h b/src/src/PluginStructs/P183_data_struct.h index a75aae59dd..7f4531267c 100644 --- a/src/src/PluginStructs/P183_data_struct.h +++ b/src/src/PluginStructs/P183_data_struct.h @@ -31,10 +31,10 @@ # define P183_DEV_ID_LABEL PCONFIG_LABEL(0) # define P183_LINK_ID PCONFIG(1) # define P183_LINK_ID_LABEL PCONFIG_LABEL(1) -# define P183_NR_OUTPUTS_INDEX 3 -# define P183_NR_OUTPUTS PCONFIG(3) -# define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(3) -# define P183_ADDRESS(x) PCONFIG(4 + x) +# define P183_NR_OUTPUTS_INDEX 3 +# define P183_NR_OUTPUTS PCONFIG(P183_NR_OUTPUTS_INDEX) +# define P183_NR_OUTPUTS_LABEL PCONFIG_LABEL(P183_NR_OUTPUTS_INDEX) +# define P183_ADDRESS(x) PCONFIG(P183_NR_OUTPUTS_INDEX + 1 + x) # define P183_ADDRESS_LABEL(x) concat(F("addr"), x) # define P183_CACHE_SIZE PCONFIG_LONG(0) From e831f0fb345c2a8ce1928d7de1d8905de3dd84f7 Mon Sep 17 00:00:00 2001 From: flashmark <15078748+flashmark@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:40:46 +0200 Subject: [PATCH 48/48] P183: Disable I2C and software serial. Update documentation --- docs/source/Interfaces/Interfaces.rst | 9 ++++-- docs/source/Interfaces/Modbus_Interface.png | Bin 36479 -> 25466 bytes docs/source/Plugin/P183.rst | 30 +++++++++++++------- docs/source/Plugin/P183_commands.repl | 4 +-- docs/source/Plugin/P183_config_values.repl | 3 +- docs/source/Plugin/P183_device_settings.png | Bin 5822 -> 10872 bytes docs/source/Plugin/P183_output_config.png | Bin 13698 -> 26378 bytes src/src/Helpers/Modbus_mgr.cpp | 15 ++++++---- 8 files changed, 38 insertions(+), 23 deletions(-) diff --git a/docs/source/Interfaces/Interfaces.rst b/docs/source/Interfaces/Interfaces.rst index c3a77c6f00..67398942d9 100644 --- a/docs/source/Interfaces/Interfaces.rst +++ b/docs/source/Interfaces/Interfaces.rst @@ -211,14 +211,17 @@ Note that many plugins implement the Modbuis protocol for specific devices witho The Modbus interface uses a serial port on the ESP board to communicate with the Modbus RTU devices. The RS485 link is half-duplex, meaning that the ESP board can either send or receive data at a time. Most RS485 converters have a DE (Driver Enable) pin that can be controlled by the ESP board to switch between sending and receiving data. The DE pin can be configured here. If the converter has an automatic DE function, this can be set to "-None-". Some serial ports on the ESP board have a collision detect feature, which can be enabled here. When supported and enabled, the ESP board will detect if another device is sending data at the same time, and will wait sending data. This can help to avoid collisions on the RS485 link. In case all Modbus devices are well-behaving and plugins are well-configured the collision detect function is not required for Modbus RTU operation. +The number of Modbus links that can be configured depends on the software build. Standard builds with plugins depending on this Modbus RTU link allow 4 modbus links. The number can be extended using custom builds. No Modbus link can be configured when the build does not include any plugins that depend on this Modbus RTU link. + .. image:: Modbus_Interface.png -Main configuration item is the selection of the serial interface to use for Modbus. The available options depend on the ESP board used. For details about the available serial interfaces, see serial helper page. The option "Not set" means that Modbus link is not used. When a serial interface is selected, it will be initialized and the Modbus RTU devices can be connected to it. +Main configuration item is the selection of the serial interface to use for Modbus. The available options depend on the ESP board used. For details about the available serial interfaces, see serial helper page. A blank option means that Modbus link is not used. When a serial interface is selected, it will be initialized and the Modbus RTU devices can be connected to it. +The current implementation only supports hardware serial interfaces due to limitations in the software serial and I2C serial implementations. The hardware serial interfaces are more reliable and can operate at higher speeds. -Selection of the RX and TX pins is required. +If the RX and TX pins for the selecetd serial interface are not fixed by the ESP hardware then they shall be configured here. Without proper configuration the Modbus interface may not function. The RX and TX pins cannot be shared with other plugins or other Modbus RTU links. If the RS485 converter has a DE (Driver Enable) pin to be controlled by the ESP board it shall be configured here. If the converter has an automatic DE function, this can be set to "-None-". -The Baud rate for the Modbus RTU communication shall be set. The default value is 9600 baud, but it can be set to other values as required by the connected devices. +The Baud rate for the Modbus RTU communication shall be set. The default value is 9600 baud, but it can be set to other values as required by the connected devices. All harware modules connected to the same link shall use the same baud rate. Collision Detection is a feature that can be used when the serial port hardware supports it, otherwise it is ignored. \ No newline at end of file diff --git a/docs/source/Interfaces/Modbus_Interface.png b/docs/source/Interfaces/Modbus_Interface.png index 5987bb147da2ef5d4b5b1c433ab59ecf0d431e29..55a817367dd414cfe155e4553d6218c30090d76d 100644 GIT binary patch literal 25466 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 L{GIUI^?UybNhZcL literal 36479 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%0D2N4lvh>Ei zkhazkUqY#;bFRJotvvoRo&mCC{XeKxR_z1Y6S0DCxA6fB4<>P&@P#U5mg9ErC7Mug zn$>U#UKq*35PcLXg|(FY`cVm=j|p1%&Xo@Zk}0{2;?-c*Cp_E3RJMps6C8S66Q**FUEpl8Rlzgdj{$0ot<9`i zl`sf?XQZmP%vLbzW=k2g;{GnmndN&Ln8ilN6PBUNo4>{;n?Bt?k+opD_ajOt1Q<3yG#nP9ja&-bM`s{ zM+@q!6V>t4<5x5+@zdU^H9N-Yj$IWo2irn%dp5?n_g8&Sd}Wn&$?KH)i!1r67wq=5 zZ_XH|?!EJ7^KU3SsHLs0We@`tuukN}7v~mp0TaohN6ONuJ5`tG+1hPrOLp|bs_E|Y zWPTTwLt3cQCIsQhr>Wb8H@)+%#7z925^v21kA=@DAKR|Hc!Fh*Dl(4J^+v-N?8`r8 zk|!N_=^*R2g}I_mnQn$RG8Wj;;vZdn?h)3Q93b64_`QN0E3}JP_d0w2yf&^ceDaJ< z%>e3KdOMLfX!js-={H+~gYNFR4*c({MNTWTE=F*2|AmMIRog(3Bllq{5o8hKZs;Uv zHSO7)AoZt$xP`+MrDL6Z;QgLr@13~ZfWI|=V`SVOpA)SkVhGq$erXc%FT5=9z1 zzK)v}_Qh%G)b*UxP4j zEg{72N<3jYX6pc-Zj?-@#5`j9OjeKrVlqQ*Dt95STC;_$c94FCs*V)$K61+Yr`}d< z;+7z-^q!}~gk#rOS5tNO{Jo>3iu}GXj(vup7^yt;oHc9e`-)UQ>U##Ir80I@vTpvg zB73C>nvGe$cJD=DE~~9ta@pqT__HzT@*g!yLVHqH@p#iBo(+$vvC-{#XXy<#rneHf z+FProTU9Y-vxK-L<*|UoYpDptaoIxwx2g%p%q&@V$4R#_k~){ilUIadDFhZ=_c~LZ zSZ3ba7-y|wE2i|^YE0Z(MwvICewEbZ9Zj1a)LoA#8GN|37dPq0wN=};$VTatoV4fb zpt)bNnn*6GRCmNZC6h2+`5H@j+*#pVYbSc(fW!uP`)_?)-+k zfrONllO%~aiA11q8W>JhMY9jF$m);m3?1$m%O(B1z%eEDxpyit;3C8-@alE&7S|sx z{(lG)5T`Jbwf&0{`tO%9Mg&5p;NvI5UCYItGi%v3x#2ROY_L0RY!dTka@3%*)z;B@ zm_yQ*w_vYOJ-y&S=b3u=U1qIjAmu=)&Ex?W)_2?M!m|}+sT~YqT|j(SC6kSIR24iu zl@w(JNi*NMX;Kruq%8FndZoeEQ-OW>es6_YIIdLxj2U<-3oqpt)$ zQFM(Hc6&!a`0dT5r7AO<7@1G>9p{|*ee}YlN2yfk)SM<^;_9m4%I=vp3CWZ*iA-15)mW*IJJB<3V2_yr5n+Q^6JG&h`pR|tYE5nDLRPiW@ic)-R3i`&yk z0yj7Ff^jl5Q-)f&n{Z?E6eC@&Ni#<>ic0;`qg^;C-5oJN=HFZzXy#eHc9=b2YjB!9 zzH64-UTbWWdCt@X5esT+Rc4{>YIA~%kXP#^nASsC~A zUo$3&OZ_j}Tm`<~iNy?Am>hkG^q6NJ971_Yg??XB=q|0C>5SL>Bj3eg7oN3LCfbR& zUG9`x(f%=z@}loGweV)%YTe;}KE7R1?(k*V`e?S_Q5lanu?w?af-eYi9p=5}h=l5& z1iY)gWC%~mlGb`Epy$mv+u2TiH#%K7h!Z>2`G2uBO^a~9-7O+loBtL+Utfz43b z%$I?)5IeAcQ!KGYkSWYqcJ_Y8*ykoT!(17RXVyqb_EEg2Nlh{$wRKM`^M`oQ8`2eY zSzD$pgBtlC!gXo4I_sw#kg4T9b{Ad4SyfhrMXy8z$P-WeB%}~EM0W;4-hP(qon2OC zyX<0WQl&L&#^Go;QDHXeL7(86=+MD=OQX-RRMYZtMHya3xJ!M!VmwZ9am3glIDEC~ zO{DPUO!Yp;Nt+mUu*pA_iH*(wGJ9K-3|IzEXn%0o0RFE3%8WKdW8lUEOXAB^x`})H zpM>K7Lzm${J(*PYmkRrBUFXy7;%Bdc2n!X;;PfKqb*eF%gs!Z}-&cu;BR&dNm5+=z zL5DrO|Jz`CdXdRogtUam=6eqicJ>=Hm3(_{VCQDPo`EHnxQq9HY?DJ|K?Du7o#y&` zqh2%p+H;14Kgb}-azmej>I7rMOo?@0Af#c=LEoHsZ+7_>Xv51WFn3Wc+F#V12d` z>!r^gi+vyqe}MlS_=5{17t=ol6FvlV0uKP>86Fr3HzL}U%BT6ObS>E0U~~5jLi~(! zcSa$vuyFBJ_R~$8O^=q;Y*SFa#fIjXW^oP%ZcOG3?B) z$L8vQo`Tw%-{2tEsTZ0RrUCqjnJk%m@`gd7<1^=b+rS$DzxUtm65w&C#)IWwxA144 v9K`4TC!)>YQxy`wm->o#|Ap4&gEI*I^F6Oo>6&lET~Jlje2Beo`SSk(;QW1= diff --git a/docs/source/Plugin/P183.rst b/docs/source/Plugin/P183.rst index 8952920b63..2af2fc094d 100644 --- a/docs/source/Plugin/P183.rst +++ b/docs/source/Plugin/P183.rst @@ -38,7 +38,10 @@ This plugin supports reading holding registers (function code 03) and writing to The plugin can be configured to read up to 4 holding registers from a Modbus slave device and store the values in user variables. It can also write values to a specified holding register on the Modbus slave device. -The plugin does not support any hardware specific features. Instead it is a generic plugin that can be used with any Modbus RTU compatible device. You have to lookup the correct register addresses and data formats in the documentation of the Modbus device you want to communicate with. +The plugin can use a cache to store a limeted amount of consequitive register values. These registers are available through commands. + +The plugin does not support any hardware specific features. Instead it is a generic plugin that can be used with any Modbus RTU compatible device. You have to lookup the correct register addresses and data formats in the documentation of the Modbus device you want to communicate with. +For more complex devices dedicated plugins may be available that support the specific device and its features. Supported hardware ------------------ @@ -59,16 +62,20 @@ Sensor The available Modbus protocol settings here depend on the build used. -* **Modbus Link**: The Modbs link teh device is connected to. The link must be configured in the Interafces page. +* **Modbus Link**: The Modbs link the device is connected to. The link must be configured in the Interfaces page. * **Modbus Device Address**: The Modbus slave device ID to communicate with (1..247). +* **Cache size**: Number of holding registers that will be read into the cache. Set to zero to disable reading data into the cache. + +* **Cache start address**: The address of the first holding register to be read into the cache. + Output Configuration ^^^^^^^^^^^^^^^^^^^^ .. image:: P183_output_config.png -* **Number of registers to read**: The number of holding registers the plugin will read (1..4). +* **Number Output Values**: The number of output values to provide. Each value corresponds to a holding register the plugin will read (1..4). * **Holding register for valueX**: The Modbus holding register address to read for valueX (X=1..4). @@ -91,16 +98,17 @@ Get Config Values retrieves values or settings from the sensor or plugin, and ca .. include:: P183_config_values.repl -Plugin state ------------- +Modbus RTU facility +------------------- -This plugin uses a new Modbus facility that allows multiple Modbus devices to share the same Modbus link. -The current implementataion of the Modbus facility requires that the plugin specifies the serial link parameters. -When a Modbus link is shared between multiple plugins, the last plugin that connects to the Modbus link will determine the serial link parameters. -This may lead to unexpected behavior if multiple plugins are using different serial link parameters on the same serial port. For a deterministic behavior, make sure that all plugins using the same serial port use the same serial link parameters. +This plugin uses a Modbus facility that allows multiple Modbus devices to share the same Modbus link. +The serial link parameters are defined for each link, not for the device itself. The Modbus facility is configured on the interfaces page, Modbus tab. Multiple Modbus devices can be connected to the same Modbus link as long as they share the same link settings. +The modbus facility can support multiple Modbus links in parallel. Use the **Modbus link** setting to select the link to be used by this device. -In the future the Modbus facility may be extended to configure the Modbus links as separate entities. As a result the plugins would only need to specify which Modbus link to use, instead of the serial link parameters. -This change will be backward incompatible with the current implementation. Therefore the plugin status is set to 'Experimental'. +The Modbus facility uses a queue to process Modbus requests one after another. The Modbus facilty will not block the plugin or other ESPEasy software while the queue is processed. It even allows to process multiple Modbus links in parallel without blocking each other. +This queueing implies that the plugin must cache the data read from the Modbus and that the values the plugin provides may be delayed. This P183 plugin reads the global cache once per second. Values are read through the Modbus from the device at the configured interval. +At that moment the plugin will stil present the value from its internal cache. Once the reply is received from the hardware then the plugin will update both the cache and the value. Note that the cache for the values is separate from the global holding register cache. +The holding register address for the values can be configured outside the cache range. Change log ---------- diff --git a/docs/source/Plugin/P183_commands.repl b/docs/source/Plugin/P183_commands.repl index b750376cc6..02a1d1fc51 100644 --- a/docs/source/Plugin/P183_commands.repl +++ b/docs/source/Plugin/P183_commands.repl @@ -12,7 +12,7 @@ ``modbus,read,

`` "," - Read the value from the holding register ``
``. Note that the value is read from the Modbus device but it will not be returned as part of the command. The value is printed in the logging. + Read the value from the holding register ``
``. The ``
`` shall be within the configured cache. Note that the value is read from the Modbus device but it will not be returned as part of the command. The value is printed in the logging. " " ``modbus,scan`` @@ -24,5 +24,5 @@ ``modbus,dump,,`` "," - Dumps the holding registers in the address range ```` until ````. + Dumps the holding registers in the address range ```` until ````. The output is un the logging. " \ No newline at end of file diff --git a/docs/source/Plugin/P183_config_values.repl b/docs/source/Plugin/P183_config_values.repl index 65f63d65ea..9c2537244a 100644 --- a/docs/source/Plugin/P183_config_values.repl +++ b/docs/source/Plugin/P183_config_values.repl @@ -5,6 +5,7 @@ " | ``[#register,]`` "," - | Returns the value of holding register ```` of the device. The value is directly read from the Modbus device. The holding register number ```` can be any valid register in the device. + | Returns the value of holding register ```` of the device. The value is read from the plugin cache. The holding register number ```` shall be defined within the configured cache range. + Note: some custom build versions may allow reading outside the cache range at the cost of serious hiccups in ESPEasy software as it blocks all software while waiting for a response from teh Modbus device. " diff --git a/docs/source/Plugin/P183_device_settings.png b/docs/source/Plugin/P183_device_settings.png index 1e1fb8f788c457c98f158578c2d7d1996366a2dd..46e6d98776cb4399d5d5f6ae8c59de3a782804ee 100644 GIT binary patch literal 10872 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 Rz^6q)in6NEa%t0${{f5hyf6R& literal 5822 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 diff --git a/docs/source/Plugin/P183_output_config.png b/docs/source/Plugin/P183_output_config.png index 6f4802c231a50567bddd1a2e04ddf8d34b27067b..38a8a224006fc17aeb7df9b1ba07e5925ebc0ac0 100644 GIT binary patch literal 26378 zcma&OWmud&^FF+Ii$ig3ai>t+7MJ4E;$FPC%L2uvIK|y5?(S9`iWZmR?y#`%U+&(| z@9u~9!y66`E-tdk%w&?xnVdtYl7bW(G7&NW06>$G7FPiP;3fb7m^UvFp>GnOq(RUh zFperxqJZ)-l0E1z@Ma?NA^<>D6v~4k0`&KncG8-T004T=^Eb?ZZP6zHz%)}vTtv-H z?`Rq1gtOB|du#i_^#IK<`b8)+EsV@O7GeaIRR!T8=ir(#c8hZ?uKMs$o)qZ zN0N*H<#K2pqXp85-X3b4?riXtshJtUbX6T0!fYr}6x%l6ZyqL>1r_}-lU+p!k<0aB z(MG}*5S4qW56A0RY!(Y_};B&4Yx-&-;~_VyoVE<7`&z&Ah$D~`IJP}-P)&fS#y z>CIDD2RzoFk~|p4ZEC*><|rsM#R{i4;{EqMOtdjFK%)EbGklD+8WSzWp8_~f^hxQg zJD7XR|IYu{FU_j7kUs_RW3JF{xJJSN4OeU^?y9`9@_oLo$K=wRwpGy_v9Etm>p4)XXjO?@gD7gwyPrQP) zH2$o1Zy%1CDpA!!PTG7gMqER@&n33^H49)V+|2t?mB95ZH1qrYjr5#ncRTTZ79`YS zqn|HDwdV?irPa226hcnE2zjN_MywD1QLRTaKvvKUq0_@#{rJE`^SCkd) J93A2; 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 literal 13698 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 diff --git a/src/src/Helpers/Modbus_mgr.cpp b/src/src/Helpers/Modbus_mgr.cpp index 1b59f3ba3d..b5ffe0901c 100644 --- a/src/src/Helpers/Modbus_mgr.cpp +++ b/src/src/Helpers/Modbus_mgr.cpp @@ -313,7 +313,7 @@ void ModbusMGR_struct::show_modbus_interfaces() serialHelper_webformLoad(_modbus_links[link].port, _modbus_links[link].serial_rx, _modbus_links[link].serial_tx, - INCLUDE_HW_SERIAL + INCLUDE_SW_SERIAL + INCLUDE_I2C_SERIAL, + INCLUDE_HW_SERIAL, // I2C and SW serial are not supported for Modbus links (yet) F("Serial Port"), slid, txid, @@ -421,11 +421,14 @@ bool ModbusMGR_struct::save_modbus_interfaces(String& error) tx_setting = _modbus_links[link].serial_tx; rx_setting = _modbus_links[link].serial_rx; - if (port_setting == static_cast(ESPEasySerialPort::sc16is752)) { - update_whenset_FormItemInt(concat(F("i2cad"), link), rx_setting, &settingsChanged); - update_whenset_FormItemInt(concat(F("i2cch"), link), tx_setting, &settingsChanged); - } - else { + // I2C is not supported for Modbus links (yet), but if it is selected, we will store the I2C address and channel in the same fields as + // the TX and RX pins, respectively. + // if (port_setting == static_cast(ESPEasySerialPort::sc16is752)) { + // update_whenset_FormItemInt(concat(F("i2cad"), link), rx_setting, &settingsChanged); + // update_whenset_FormItemInt(concat(F("i2cch"), link), tx_setting, &settingsChanged); + // } + // else + { update_whenset_FormItemInt(concat(F("MBtx"), link), tx_setting, &settingsChanged); update_whenset_FormItemInt(concat(F("MBrx"), link), rx_setting, &settingsChanged); }