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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] [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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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