Add support for acting as Modbus server (#4874)

Co-authored-by: Jeroen van Oort <jeroen.vanoort@webparking.nl>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Jeroen van Oort
2024-05-22 06:17:32 +02:00
committed by GitHub
parent 76abf2200c
commit 1ca7c2d7dd
7 changed files with 203 additions and 21 deletions
+35
View File
@@ -1,5 +1,9 @@
from __future__ import annotations
from typing import Literal
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.cpp_helpers import gpio_pin_expression
from esphome.components import uart
from esphome.const import (
@@ -17,13 +21,21 @@ Modbus = modbus_ns.class_("Modbus", cg.Component, uart.UARTDevice)
ModbusDevice = modbus_ns.class_("ModbusDevice")
MULTI_CONF = True
CONF_ROLE = "role"
CONF_MODBUS_ID = "modbus_id"
CONF_SEND_WAIT_TIME = "send_wait_time"
ModbusRole = modbus_ns.enum("ModbusRole")
MODBUS_ROLES = {
"client": ModbusRole.CLIENT,
"server": ModbusRole.SERVER,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Modbus),
cv.Optional(CONF_ROLE, default="client"): cv.enum(MODBUS_ROLES),
cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema,
cv.Optional(
CONF_SEND_WAIT_TIME, default="250ms"
@@ -43,6 +55,7 @@ async def to_code(config):
await uart.register_uart_device(var, config)
cg.add(var.set_role(config[CONF_ROLE]))
if CONF_FLOW_CONTROL_PIN in config:
pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
cg.add(var.set_flow_control_pin(pin))
@@ -62,6 +75,28 @@ def modbus_device_schema(default_address):
return cv.Schema(schema)
def final_validate_modbus_device(
name: str, *, role: Literal["server", "client"] | None = None
):
def validate_role(value):
assert role in MODBUS_ROLES
if value != role:
raise cv.Invalid(f"Component {name} requires role to be {role}")
return value
def validate_hub(hub_config):
hub_schema = {}
if role is not None:
hub_schema[cv.Required(CONF_ROLE)] = validate_role
return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config)
return cv.Schema(
{cv.Required(CONF_MODBUS_ID): fv.id_declaration_match_schema(validate_hub)},
extra=cv.ALLOW_EXTRA,
)
async def register_modbus_device(var, config):
parent = await cg.get_variable(config[CONF_MODBUS_ID])
cg.add(var.set_parent(parent))
+19 -8
View File
@@ -77,7 +77,13 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
} else {
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
// data starts at 2 and length is 4 for read registers commands
if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) {
data_offset = 2;
data_len = 4;
}
// the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
data_offset = 2;
data_len = 4;
@@ -123,6 +129,9 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
// Ignore modbus exception not related to a pending command
ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
}
} else if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) {
device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
uint16_t(data[3]) | (uint16_t(data[2]) << 8));
} else {
device->on_modbus_data(data);
}
@@ -164,16 +173,18 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
std::vector<uint8_t> data;
data.push_back(address);
data.push_back(function_code);
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
if (function_code != 0x5 && function_code != 0x6) {
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
if (this->role == ModbusRole::CLIENT) {
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
if (function_code != 0x5 && function_code != 0x6) {
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
}
}
if (payload != nullptr) {
if (function_code == 0xF || function_code == 0x10) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
} else {
payload_len = 2; // Write single register or coil
}
+9
View File
@@ -8,6 +8,11 @@
namespace esphome {
namespace modbus {
enum ModbusRole {
CLIENT,
SERVER,
};
class ModbusDevice;
class Modbus : public uart::UARTDevice, public Component {
@@ -27,11 +32,14 @@ class Modbus : public uart::UARTDevice, public Component {
void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
uint8_t payload_len = 0, const uint8_t *payload = nullptr);
void send_raw(const std::vector<uint8_t> &payload);
void set_role(ModbusRole role) { this->role = role; }
void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
uint8_t waiting_for_response{0};
void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
ModbusRole role;
protected:
GPIOPin *flow_control_pin_{nullptr};
@@ -50,6 +58,7 @@ class ModbusDevice {
void set_address(uint8_t address) { address_ = address; }
virtual void on_modbus_data(const std::vector<uint8_t> &data) = 0;
virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){};
void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
const uint8_t *payload = nullptr) {
this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);