mirror of
https://github.com/esphome/esphome.git
synced 2026-05-31 07:57:40 +08:00
[modbus] Split modbus_server from modbus_controller (#15509)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
@@ -347,6 +347,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm
|
|||||||
esphome/components/modbus_controller/sensor/* @martgras
|
esphome/components/modbus_controller/sensor/* @martgras
|
||||||
esphome/components/modbus_controller/switch/* @martgras
|
esphome/components/modbus_controller/switch/* @martgras
|
||||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||||
|
esphome/components/modbus_server/* @exciton
|
||||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||||
esphome/components/mopeka_pro_check/* @spbrogan
|
esphome/components/mopeka_pro_check/* @spbrogan
|
||||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import binascii
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import modbus
|
from esphome.components import modbus
|
||||||
from esphome.components.const import CONF_ENABLED
|
|
||||||
from esphome.components.modbus.helpers import (
|
from esphome.components.modbus.helpers import (
|
||||||
CPP_TYPE_REGISTER_MAP,
|
|
||||||
MODBUS_REGISTER_TYPE,
|
MODBUS_REGISTER_TYPE,
|
||||||
SENSOR_VALUE_TYPE,
|
|
||||||
TYPE_REGISTER_MAP,
|
TYPE_REGISTER_MAP,
|
||||||
ModbusRegisterType,
|
ModbusRegisterType,
|
||||||
)
|
)
|
||||||
@@ -29,11 +26,10 @@ from .const import (
|
|||||||
CONF_ON_OFFLINE,
|
CONF_ON_OFFLINE,
|
||||||
CONF_ON_ONLINE,
|
CONF_ON_ONLINE,
|
||||||
CONF_REGISTER_COUNT,
|
CONF_REGISTER_COUNT,
|
||||||
CONF_REGISTER_LAST_ADDRESS,
|
|
||||||
CONF_REGISTER_TYPE,
|
CONF_REGISTER_TYPE,
|
||||||
CONF_REGISTER_VALUE,
|
|
||||||
CONF_RESPONSE_SIZE,
|
CONF_RESPONSE_SIZE,
|
||||||
CONF_SERVER_COURTESY_RESPONSE,
|
CONF_SERVER_COURTESY_RESPONSE,
|
||||||
|
CONF_SERVER_REGISTERS,
|
||||||
CONF_SKIP_UPDATES,
|
CONF_SKIP_UPDATES,
|
||||||
CONF_VALUE_TYPE,
|
CONF_VALUE_TYPE,
|
||||||
)
|
)
|
||||||
@@ -42,9 +38,6 @@ CODEOWNERS = ["@martgras"]
|
|||||||
|
|
||||||
AUTO_LOAD = ["modbus"]
|
AUTO_LOAD = ["modbus"]
|
||||||
|
|
||||||
CONF_READ_LAMBDA = "read_lambda"
|
|
||||||
CONF_WRITE_LAMBDA = "write_lambda"
|
|
||||||
CONF_SERVER_REGISTERS = "server_registers"
|
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
||||||
@@ -53,30 +46,9 @@ ModbusController = modbus_controller_ns.class_(
|
|||||||
)
|
)
|
||||||
|
|
||||||
SensorItem = modbus_controller_ns.struct("SensorItem")
|
SensorItem = modbus_controller_ns.struct("SensorItem")
|
||||||
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
|
|
||||||
ServerRegister = modbus_controller_ns.struct("ServerRegister")
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
|
|
||||||
{
|
|
||||||
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
|
|
||||||
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
|
|
||||||
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ModbusServerRegisterSchema = cv.Schema(
|
|
||||||
{
|
|
||||||
cv.GenerateID(): cv.declare_id(ServerRegister),
|
|
||||||
cv.Required(CONF_ADDRESS): cv.positive_int,
|
|
||||||
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
|
|
||||||
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
|
|
||||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -85,12 +57,16 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_COMMAND_THROTTLE, default="0ms"
|
CONF_COMMAND_THROTTLE, default="0ms"
|
||||||
): cv.positive_time_period_milliseconds,
|
): cv.positive_time_period_milliseconds,
|
||||||
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
|
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): cv.invalid(
|
||||||
|
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
|
||||||
|
),
|
||||||
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
|
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
|
||||||
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_SERVER_REGISTERS,
|
CONF_SERVER_REGISTERS,
|
||||||
): cv.ensure_list(ModbusServerRegisterSchema),
|
): cv.invalid(
|
||||||
|
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
|
||||||
|
),
|
||||||
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}),
|
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}),
|
||||||
cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}),
|
cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}),
|
||||||
cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}),
|
cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}),
|
||||||
@@ -142,11 +118,9 @@ def validate_modbus_register(config):
|
|||||||
|
|
||||||
|
|
||||||
def _final_validate(config):
|
def _final_validate(config):
|
||||||
if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config:
|
return modbus.final_validate_modbus_device("modbus_controller", role="client")(
|
||||||
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
|
config
|
||||||
config
|
)
|
||||||
)
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
@@ -228,53 +202,8 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
|
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
|
||||||
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
|
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
|
||||||
if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE):
|
|
||||||
cg.add(
|
|
||||||
var.set_server_courtesy_response(
|
|
||||||
cg.StructInitializer(
|
|
||||||
ServerCourtesyResponse,
|
|
||||||
("enabled", server_courtesy_response[CONF_ENABLED]),
|
|
||||||
(
|
|
||||||
"register_last_address",
|
|
||||||
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
|
|
||||||
),
|
|
||||||
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
|
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
|
||||||
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
||||||
if CONF_SERVER_REGISTERS in config:
|
|
||||||
for server_register in config[CONF_SERVER_REGISTERS]:
|
|
||||||
server_register_var = cg.new_Pvariable(
|
|
||||||
server_register[CONF_ID],
|
|
||||||
server_register[CONF_ADDRESS],
|
|
||||||
server_register[CONF_VALUE_TYPE],
|
|
||||||
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
|
|
||||||
)
|
|
||||||
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
|
|
||||||
cg.add(
|
|
||||||
server_register_var.set_read_lambda(
|
|
||||||
cg.TemplateArguments(cpp_type),
|
|
||||||
await cg.process_lambda(
|
|
||||||
server_register[CONF_READ_LAMBDA],
|
|
||||||
[(cg.uint16, "address")],
|
|
||||||
return_type=cpp_type,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if CONF_WRITE_LAMBDA in server_register:
|
|
||||||
cg.add(
|
|
||||||
server_register_var.set_write_lambda(
|
|
||||||
cg.TemplateArguments(cpp_type),
|
|
||||||
await cg.process_lambda(
|
|
||||||
server_register[CONF_WRITE_LAMBDA],
|
|
||||||
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
|
|
||||||
return_type=cg.bool_,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
cg.add(var.add_server_register(server_register_var))
|
|
||||||
await register_modbus_device(var, config)
|
await register_modbus_device(var, config)
|
||||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ CONF_REGISTER_TYPE = "register_type"
|
|||||||
CONF_REGISTER_VALUE = "register_value"
|
CONF_REGISTER_VALUE = "register_value"
|
||||||
CONF_RESPONSE_SIZE = "response_size"
|
CONF_RESPONSE_SIZE = "response_size"
|
||||||
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
|
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
|
||||||
|
CONF_SERVER_REGISTERS = "server_registers"
|
||||||
CONF_SKIP_UPDATES = "skip_updates"
|
CONF_SKIP_UPDATES = "skip_updates"
|
||||||
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
|
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
|
||||||
CONF_VALUE_TYPE = "value_type"
|
CONF_VALUE_TYPE = "value_type"
|
||||||
|
|||||||
@@ -112,167 +112,6 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
|
|
||||||
uint16_t number_of_registers) {
|
|
||||||
ESP_LOGD(TAG,
|
|
||||||
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
|
||||||
"0x%X.",
|
|
||||||
this->address_, function_code, start_address, number_of_registers);
|
|
||||||
|
|
||||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
|
|
||||||
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint16_t> sixteen_bit_response;
|
|
||||||
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
|
|
||||||
bool found = false;
|
|
||||||
for (auto *server_register : this->server_registers_) {
|
|
||||||
if (server_register->address == current_address) {
|
|
||||||
if (!server_register->read_lambda) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int64_t value = server_register->read_lambda();
|
|
||||||
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
|
|
||||||
server_register->address, static_cast<size_t>(server_register->value_type),
|
|
||||||
server_register->register_count, server_register->format_value(value).c_str());
|
|
||||||
|
|
||||||
std::vector<uint16_t> payload;
|
|
||||||
payload.reserve(server_register->register_count * 2);
|
|
||||||
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
|
|
||||||
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
|
|
||||||
current_address += server_register->register_count;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
if (this->server_courtesy_response_.enabled &&
|
|
||||||
(current_address <= this->server_courtesy_response_.register_last_address)) {
|
|
||||||
ESP_LOGD(TAG,
|
|
||||||
"Could not match any register to address 0x%02X, but default allowed. "
|
|
||||||
"Returning default value: %d.",
|
|
||||||
current_address, this->server_courtesy_response_.register_value);
|
|
||||||
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
|
|
||||||
current_address += 1; // Just increment by 1, as the default response is a single register
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG,
|
|
||||||
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
|
|
||||||
current_address);
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> response;
|
|
||||||
for (auto v : sixteen_bit_response) {
|
|
||||||
auto decoded_value = decode_value(v);
|
|
||||||
response.push_back(decoded_value[0]);
|
|
||||||
response.push_back(decoded_value[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
|
|
||||||
uint16_t number_of_registers;
|
|
||||||
uint16_t payload_offset;
|
|
||||||
|
|
||||||
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
|
||||||
if (data.size() < 5) {
|
|
||||||
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
|
|
||||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
|
|
||||||
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint16_t payload_size = data[4];
|
|
||||||
if (payload_size != number_of_registers * 2) {
|
|
||||||
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
|
|
||||||
payload_size, number_of_registers);
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.size() < 5 + payload_size) {
|
|
||||||
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
|
|
||||||
5 + payload_size);
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload_offset = 5;
|
|
||||||
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
|
||||||
if (data.size() < 4) {
|
|
||||||
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
number_of_registers = 1;
|
|
||||||
payload_offset = 2;
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
|
|
||||||
ESP_LOGD(TAG,
|
|
||||||
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
|
||||||
"0x%X.",
|
|
||||||
this->address_, function_code, start_address, number_of_registers);
|
|
||||||
|
|
||||||
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
|
|
||||||
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
|
|
||||||
uint16_t offset = payload_offset;
|
|
||||||
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
|
|
||||||
bool ok = false;
|
|
||||||
for (auto *server_register : this->server_registers_) {
|
|
||||||
if (server_register->address == current_address) {
|
|
||||||
ok = callback(server_register, offset);
|
|
||||||
current_address += server_register->register_count;
|
|
||||||
offset += server_register->register_count * sizeof(uint16_t);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// check all registers are writable before writing to any of them:
|
|
||||||
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
|
|
||||||
return server_register->write_lambda != nullptr;
|
|
||||||
})) {
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actually write to the registers:
|
|
||||||
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
|
|
||||||
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
|
|
||||||
return server_register->write_lambda(number);
|
|
||||||
})) {
|
|
||||||
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> response;
|
|
||||||
response.reserve(6);
|
|
||||||
response.push_back(this->address_);
|
|
||||||
response.push_back(function_code);
|
|
||||||
response.insert(response.end(), data.begin(), data.begin() + 4);
|
|
||||||
this->send_raw(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
|
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
|
||||||
auto reg_it = std::find_if(
|
auto reg_it = std::find_if(
|
||||||
std::begin(this->register_ranges_), std::end(this->register_ranges_),
|
std::begin(this->register_ranges_), std::end(this->register_ranges_),
|
||||||
@@ -472,14 +311,8 @@ void ModbusController::dump_config() {
|
|||||||
"ModbusController:\n"
|
"ModbusController:\n"
|
||||||
" Address: 0x%02X\n"
|
" Address: 0x%02X\n"
|
||||||
" Max Command Retries: %d\n"
|
" Max Command Retries: %d\n"
|
||||||
" Offline Skip Updates: %d\n"
|
" Offline Skip Updates: %d\n",
|
||||||
" Server Courtesy Response:\n"
|
this->address_, this->max_cmd_retries_, this->offline_skip_updates_);
|
||||||
" Enabled: %s\n"
|
|
||||||
" Register Last Address: 0x%02X\n"
|
|
||||||
" Register Value: %d",
|
|
||||||
this->address_, this->max_cmd_retries_, this->offline_skip_updates_,
|
|
||||||
this->server_courtesy_response_.enabled ? "true" : "false",
|
|
||||||
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
|
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
ESP_LOGCONFIG(TAG, "sensormap");
|
ESP_LOGCONFIG(TAG, "sensormap");
|
||||||
@@ -493,11 +326,6 @@ void ModbusController::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
||||||
it.start_address, it.register_count, it.skip_updates);
|
it.start_address, it.register_count, it.skip_updates);
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, "server registers");
|
|
||||||
for (auto &r : this->server_registers_) {
|
|
||||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
|
|
||||||
static_cast<uint8_t>(r->value_type), r->register_count);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,82 +120,6 @@ class SensorItem {
|
|||||||
bool force_new_range{false};
|
bool force_new_range{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ServerCourtesyResponse {
|
|
||||||
bool enabled{false};
|
|
||||||
uint16_t register_last_address{0xFFFF};
|
|
||||||
uint16_t register_value{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
class ServerRegister {
|
|
||||||
using ReadLambda = std::function<int64_t()>;
|
|
||||||
using WriteLambda = std::function<bool(int64_t value)>;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
|
|
||||||
this->address = address;
|
|
||||||
this->value_type = value_type;
|
|
||||||
this->register_count = register_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
|
|
||||||
this->read_lambda = [this, user_read_lambda]() -> int64_t {
|
|
||||||
T user_value = user_read_lambda(this->address);
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
return bit_cast<uint32_t>(user_value);
|
|
||||||
} else {
|
|
||||||
return static_cast<int64_t>(user_value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
|
|
||||||
this->write_lambda = [this, user_write_lambda](int64_t number) {
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
|
|
||||||
return user_write_lambda(this->address, float_value);
|
|
||||||
}
|
|
||||||
return user_write_lambda(this->address, static_cast<T>(number));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formats a raw value into a string representation based on the value type for debugging
|
|
||||||
std::string format_value(int64_t value) const {
|
|
||||||
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
|
|
||||||
// plus null terminator = 43, rounded to 44 for 4-byte alignment
|
|
||||||
char buf[44];
|
|
||||||
switch (this->value_type) {
|
|
||||||
case SensorValueType::U_WORD:
|
|
||||||
case SensorValueType::U_DWORD:
|
|
||||||
case SensorValueType::U_DWORD_R:
|
|
||||||
case SensorValueType::U_QWORD:
|
|
||||||
case SensorValueType::U_QWORD_R:
|
|
||||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
|
|
||||||
return buf;
|
|
||||||
case SensorValueType::S_WORD:
|
|
||||||
case SensorValueType::S_DWORD:
|
|
||||||
case SensorValueType::S_DWORD_R:
|
|
||||||
case SensorValueType::S_QWORD:
|
|
||||||
case SensorValueType::S_QWORD_R:
|
|
||||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
|
||||||
return buf;
|
|
||||||
case SensorValueType::FP32_R:
|
|
||||||
case SensorValueType::FP32:
|
|
||||||
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
|
||||||
return buf;
|
|
||||||
default:
|
|
||||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t address{0};
|
|
||||||
SensorValueType value_type{SensorValueType::RAW};
|
|
||||||
uint8_t register_count{0};
|
|
||||||
ReadLambda read_lambda;
|
|
||||||
WriteLambda write_lambda;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ModbusController::create_register_ranges_ tries to optimize register range
|
// ModbusController::create_register_ranges_ tries to optimize register range
|
||||||
// for this the sensors must be ordered by register_type, start_address and bitmask
|
// for this the sensors must be ordered by register_type, start_address and bitmask
|
||||||
class SensorItemsComparator {
|
class SensorItemsComparator {
|
||||||
@@ -367,16 +291,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
|||||||
void queue_command(const ModbusCommandItem &command);
|
void queue_command(const ModbusCommandItem &command);
|
||||||
/// Registers a sensor with the controller. Called by esphomes code generator
|
/// Registers a sensor with the controller. Called by esphomes code generator
|
||||||
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
|
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
|
||||||
/// Registers a server register with the controller. Called by esphomes code generator
|
|
||||||
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
|
|
||||||
/// called when a modbus response was parsed without errors
|
/// called when a modbus response was parsed without errors
|
||||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||||
/// called when a modbus error response was received
|
/// called when a modbus error response was received
|
||||||
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
|
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
|
||||||
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
|
|
||||||
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
|
|
||||||
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
|
|
||||||
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
|
|
||||||
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
|
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
|
||||||
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
|
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
|
||||||
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
|
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
|
||||||
@@ -413,12 +331,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
|||||||
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
|
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
|
||||||
/// get how many times a command will be (re)sent if no response is received
|
/// get how many times a command will be (re)sent if no response is received
|
||||||
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
|
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
|
||||||
/// Called by esphome generated code to set the server courtesy response object
|
|
||||||
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
|
|
||||||
this->server_courtesy_response_ = server_courtesy_response;
|
|
||||||
}
|
|
||||||
/// Get the server courtesy response object
|
|
||||||
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// parse sensormap_ and create range of sequential addresses
|
/// parse sensormap_ and create range of sequential addresses
|
||||||
@@ -435,8 +347,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
|||||||
void dump_sensors_();
|
void dump_sensors_();
|
||||||
/// Collection of all sensors for this component
|
/// Collection of all sensors for this component
|
||||||
SensorSet sensorset_;
|
SensorSet sensorset_;
|
||||||
/// Collection of all server registers for this component
|
|
||||||
std::vector<ServerRegister *> server_registers_{};
|
|
||||||
/// Continuous range of modbus registers
|
/// Continuous range of modbus registers
|
||||||
std::vector<RegisterRange> register_ranges_{};
|
std::vector<RegisterRange> register_ranges_{};
|
||||||
/// Hold the pending requests to be sent
|
/// Hold the pending requests to be sent
|
||||||
@@ -461,9 +371,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
|||||||
CallbackManager<void(int, int)> online_callback_{};
|
CallbackManager<void(int, int)> online_callback_{};
|
||||||
/// Server offline callback
|
/// Server offline callback
|
||||||
CallbackManager<void(int, int)> offline_callback_{};
|
CallbackManager<void(int, int)> offline_callback_{};
|
||||||
/// Server courtesy response
|
|
||||||
ServerCourtesyResponse server_courtesy_response_{
|
|
||||||
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Convert vector<uint8_t> response payload to float.
|
/** Convert vector<uint8_t> response payload to float.
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import modbus
|
||||||
|
from esphome.components.const import CONF_ENABLED
|
||||||
|
from esphome.components.modbus.helpers import (
|
||||||
|
CPP_TYPE_REGISTER_MAP,
|
||||||
|
SENSOR_VALUE_TYPE,
|
||||||
|
TYPE_REGISTER_MAP,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_COURTESY_RESPONSE,
|
||||||
|
CONF_READ_LAMBDA,
|
||||||
|
CONF_REGISTER_LAST_ADDRESS,
|
||||||
|
CONF_REGISTER_VALUE,
|
||||||
|
CONF_REGISTERS,
|
||||||
|
CONF_VALUE_TYPE,
|
||||||
|
CONF_WRITE_LAMBDA,
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@exciton"]
|
||||||
|
|
||||||
|
AUTO_LOAD = ["modbus"]
|
||||||
|
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
modbus_server_ns = cg.esphome_ns.namespace("modbus_server")
|
||||||
|
ModbusServer = modbus_server_ns.class_(
|
||||||
|
"ModbusServer", cg.Component, modbus.ModbusDevice
|
||||||
|
)
|
||||||
|
|
||||||
|
ServerCourtesyResponse = modbus_server_ns.struct("ServerCourtesyResponse")
|
||||||
|
ServerRegister = modbus_server_ns.struct("ServerRegister")
|
||||||
|
|
||||||
|
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
|
||||||
|
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ModbusServerRegisterSchema = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(ServerRegister),
|
||||||
|
cv.Required(CONF_ADDRESS): cv.positive_int,
|
||||||
|
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
|
||||||
|
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
|
||||||
|
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(ModbusServer),
|
||||||
|
cv.Optional(CONF_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_REGISTERS,
|
||||||
|
): cv.ensure_list(ModbusServerRegisterSchema),
|
||||||
|
}
|
||||||
|
).extend(modbus.modbus_device_schema(0x01)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(config):
|
||||||
|
return modbus.final_validate_modbus_device("modbus_server", role="server")(config)
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
if server_courtesy_response := config.get(CONF_COURTESY_RESPONSE):
|
||||||
|
cg.add(
|
||||||
|
var.set_server_courtesy_response(
|
||||||
|
cg.StructInitializer(
|
||||||
|
ServerCourtesyResponse,
|
||||||
|
("enabled", server_courtesy_response[CONF_ENABLED]),
|
||||||
|
(
|
||||||
|
"register_last_address",
|
||||||
|
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
|
||||||
|
),
|
||||||
|
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if CONF_REGISTERS in config:
|
||||||
|
for server_register in config[CONF_REGISTERS]:
|
||||||
|
server_register_var = cg.new_Pvariable(
|
||||||
|
server_register[CONF_ID],
|
||||||
|
server_register[CONF_ADDRESS],
|
||||||
|
server_register[CONF_VALUE_TYPE],
|
||||||
|
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
|
||||||
|
)
|
||||||
|
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
|
||||||
|
cg.add(
|
||||||
|
server_register_var.set_read_lambda(
|
||||||
|
cg.TemplateArguments(cpp_type),
|
||||||
|
await cg.process_lambda(
|
||||||
|
server_register[CONF_READ_LAMBDA],
|
||||||
|
[(cg.uint16, "address")],
|
||||||
|
return_type=cpp_type,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if CONF_WRITE_LAMBDA in server_register:
|
||||||
|
cg.add(
|
||||||
|
server_register_var.set_write_lambda(
|
||||||
|
cg.TemplateArguments(cpp_type),
|
||||||
|
await cg.process_lambda(
|
||||||
|
server_register[CONF_WRITE_LAMBDA],
|
||||||
|
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
|
||||||
|
return_type=cg.bool_,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cg.add(var.add_server_register(server_register_var))
|
||||||
|
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
return await modbus.register_modbus_device(var, config)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
CONF_REGISTER_LAST_ADDRESS = "register_last_address"
|
||||||
|
CONF_REGISTER_VALUE = "register_value"
|
||||||
|
CONF_VALUE_TYPE = "value_type"
|
||||||
|
CONF_COURTESY_RESPONSE = "courtesy_response"
|
||||||
|
CONF_READ_LAMBDA = "read_lambda"
|
||||||
|
CONF_WRITE_LAMBDA = "write_lambda"
|
||||||
|
CONF_REGISTERS = "registers"
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#include "modbus_server.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::modbus_server {
|
||||||
|
using modbus::ModbusFunctionCode;
|
||||||
|
using modbus::ModbusExceptionCode;
|
||||||
|
|
||||||
|
static const char *const TAG = "modbus_server";
|
||||||
|
|
||||||
|
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
|
||||||
|
uint16_t number_of_registers) {
|
||||||
|
ESP_LOGD(TAG,
|
||||||
|
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
||||||
|
"0x%X.",
|
||||||
|
this->address_, function_code, start_address, number_of_registers);
|
||||||
|
|
||||||
|
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
|
||||||
|
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint16_t> sixteen_bit_response;
|
||||||
|
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
|
||||||
|
bool found = false;
|
||||||
|
for (auto *server_register : this->server_registers_) {
|
||||||
|
if (server_register->address == current_address) {
|
||||||
|
if (!server_register->read_lambda) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int64_t value = server_register->read_lambda();
|
||||||
|
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
|
||||||
|
server_register->address, static_cast<size_t>(server_register->value_type),
|
||||||
|
server_register->register_count, server_register->format_value(value).c_str());
|
||||||
|
|
||||||
|
std::vector<uint16_t> payload;
|
||||||
|
payload.reserve(server_register->register_count * 2);
|
||||||
|
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
|
||||||
|
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
|
||||||
|
current_address += server_register->register_count;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
if (this->server_courtesy_response_.enabled &&
|
||||||
|
(current_address <= this->server_courtesy_response_.register_last_address)) {
|
||||||
|
ESP_LOGD(TAG,
|
||||||
|
"Could not match any register to address 0x%02X, but default allowed. "
|
||||||
|
"Returning default value: %d.",
|
||||||
|
current_address, this->server_courtesy_response_.register_value);
|
||||||
|
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
|
||||||
|
current_address += 1; // Just increment by 1, as the default response is a single register
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
|
||||||
|
current_address);
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> response;
|
||||||
|
for (auto v : sixteen_bit_response) {
|
||||||
|
auto decoded_value = decode_value(v);
|
||||||
|
response.push_back(decoded_value[0]);
|
||||||
|
response.push_back(decoded_value[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
|
||||||
|
uint16_t number_of_registers;
|
||||||
|
uint16_t payload_offset;
|
||||||
|
|
||||||
|
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||||
|
if (data.size() < 5) {
|
||||||
|
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
|
||||||
|
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
|
||||||
|
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint16_t payload_size = data[4];
|
||||||
|
if (payload_size != number_of_registers * 2) {
|
||||||
|
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
|
||||||
|
payload_size, number_of_registers);
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.size() < 5 + payload_size) {
|
||||||
|
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
|
||||||
|
5 + payload_size);
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload_offset = 5;
|
||||||
|
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
||||||
|
if (data.size() < 4) {
|
||||||
|
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
number_of_registers = 1;
|
||||||
|
payload_offset = 2;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
|
||||||
|
ESP_LOGD(TAG,
|
||||||
|
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
||||||
|
"0x%X.",
|
||||||
|
this->address_, function_code, start_address, number_of_registers);
|
||||||
|
|
||||||
|
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
|
||||||
|
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
|
||||||
|
uint16_t offset = payload_offset;
|
||||||
|
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
|
||||||
|
bool ok = false;
|
||||||
|
for (auto *server_register : this->server_registers_) {
|
||||||
|
if (server_register->address == current_address) {
|
||||||
|
ok = callback(server_register, offset);
|
||||||
|
current_address += server_register->register_count;
|
||||||
|
offset += server_register->register_count * sizeof(uint16_t);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// check all registers are writable before writing to any of them:
|
||||||
|
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
|
||||||
|
return server_register->write_lambda != nullptr;
|
||||||
|
})) {
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually write to the registers:
|
||||||
|
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
|
||||||
|
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
|
||||||
|
return server_register->write_lambda(number);
|
||||||
|
})) {
|
||||||
|
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> response;
|
||||||
|
response.reserve(6);
|
||||||
|
response.push_back(this->address_);
|
||||||
|
response.push_back(function_code);
|
||||||
|
response.insert(response.end(), data.begin(), data.begin() + 4);
|
||||||
|
this->send_raw(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModbusServer::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"ModbusServer:\n"
|
||||||
|
" Address: 0x%02X\n"
|
||||||
|
" Server Courtesy Response:\n"
|
||||||
|
" Enabled: %s\n"
|
||||||
|
" Register Last Address: 0x%02X\n"
|
||||||
|
" Register Value: %" PRIu16,
|
||||||
|
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
|
||||||
|
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
|
||||||
|
|
||||||
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
ESP_LOGCONFIG(TAG, "server registers");
|
||||||
|
for (auto &r : this->server_registers_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
|
||||||
|
static_cast<uint8_t>(r->value_type), r->register_count);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::modbus_server
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
#include "esphome/components/modbus/modbus.h"
|
||||||
|
#include "esphome/components/modbus/modbus_helpers.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace esphome::modbus_server {
|
||||||
|
|
||||||
|
using modbus::helpers::SensorValueType;
|
||||||
|
|
||||||
|
struct ServerCourtesyResponse {
|
||||||
|
bool enabled{false};
|
||||||
|
uint16_t register_last_address{0xFFFF};
|
||||||
|
uint16_t register_value{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ServerRegister {
|
||||||
|
using ReadLambda = std::function<int64_t()>;
|
||||||
|
using WriteLambda = std::function<bool(int64_t value)>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
|
||||||
|
this->address = address;
|
||||||
|
this->value_type = value_type;
|
||||||
|
this->register_count = register_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
|
||||||
|
this->read_lambda = [this, user_read_lambda]() -> int64_t {
|
||||||
|
T user_value = user_read_lambda(this->address);
|
||||||
|
if constexpr (std::is_same_v<T, float>) {
|
||||||
|
return bit_cast<uint32_t>(user_value);
|
||||||
|
} else {
|
||||||
|
return static_cast<int64_t>(user_value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
|
||||||
|
this->write_lambda = [this, user_write_lambda](int64_t number) {
|
||||||
|
if constexpr (std::is_same_v<T, float>) {
|
||||||
|
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
|
||||||
|
return user_write_lambda(this->address, float_value);
|
||||||
|
}
|
||||||
|
return user_write_lambda(this->address, static_cast<T>(number));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a raw value into a string representation based on the value type for debugging
|
||||||
|
std::string format_value(int64_t value) const {
|
||||||
|
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
|
||||||
|
// plus null terminator = 43, rounded to 44 for 4-byte alignment
|
||||||
|
char buf[44];
|
||||||
|
switch (this->value_type) {
|
||||||
|
case SensorValueType::U_WORD:
|
||||||
|
case SensorValueType::U_DWORD:
|
||||||
|
case SensorValueType::U_DWORD_R:
|
||||||
|
case SensorValueType::U_QWORD:
|
||||||
|
case SensorValueType::U_QWORD_R:
|
||||||
|
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
|
||||||
|
return buf;
|
||||||
|
case SensorValueType::S_WORD:
|
||||||
|
case SensorValueType::S_DWORD:
|
||||||
|
case SensorValueType::S_DWORD_R:
|
||||||
|
case SensorValueType::S_QWORD:
|
||||||
|
case SensorValueType::S_QWORD_R:
|
||||||
|
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||||
|
return buf;
|
||||||
|
case SensorValueType::FP32_R:
|
||||||
|
case SensorValueType::FP32:
|
||||||
|
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||||
|
return buf;
|
||||||
|
default:
|
||||||
|
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t address{0};
|
||||||
|
SensorValueType value_type{SensorValueType::RAW};
|
||||||
|
uint8_t register_count{0};
|
||||||
|
ReadLambda read_lambda;
|
||||||
|
WriteLambda write_lambda;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModbusServer : public Component, public modbus::ModbusDevice {
|
||||||
|
public:
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
/// Not used for ModbusServer.
|
||||||
|
void on_modbus_data(const std::vector<uint8_t> &data) override{};
|
||||||
|
/// Registers a server register with the controller. Called by esphomes code generator
|
||||||
|
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
|
||||||
|
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
|
||||||
|
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
|
||||||
|
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
|
||||||
|
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
|
||||||
|
/// Called by esphome generated code to set the server courtesy response object
|
||||||
|
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
|
||||||
|
this->server_courtesy_response_ = server_courtesy_response;
|
||||||
|
}
|
||||||
|
/// Get the server courtesy response object
|
||||||
|
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/// Collection of all server registers for this component
|
||||||
|
std::vector<ServerRegister *> server_registers_{};
|
||||||
|
/// Server courtesy response
|
||||||
|
ServerCourtesyResponse server_courtesy_response_{
|
||||||
|
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::modbus_server
|
||||||
@@ -1,53 +1,11 @@
|
|||||||
modbus:
|
|
||||||
- id: mod_bus2
|
|
||||||
uart_id: uart_bus
|
|
||||||
role: server
|
|
||||||
|
|
||||||
modbus_controller:
|
modbus_controller:
|
||||||
- id: modbus_controller1
|
- id: modbus_controller1
|
||||||
address: 0x2
|
address: 0x2
|
||||||
modbus_id: modbus_bus
|
modbus_id: modbus_bus
|
||||||
allow_duplicate_commands: false
|
|
||||||
on_online:
|
on_online:
|
||||||
then:
|
then:
|
||||||
logger.log: "Module Online"
|
logger.log: "Module Online"
|
||||||
- id: modbus_controller2
|
|
||||||
address: 0x2
|
|
||||||
modbus_id: mod_bus2
|
|
||||||
server_registers:
|
|
||||||
- address: 0x0000
|
|
||||||
value_type: S_DWORD_R
|
|
||||||
read_lambda: |-
|
|
||||||
return 42.3;
|
|
||||||
max_cmd_retries: 0
|
|
||||||
- id: modbus_controller3
|
|
||||||
address: 0x3
|
|
||||||
modbus_id: mod_bus2
|
|
||||||
server_registers:
|
|
||||||
- address: 0x0009
|
|
||||||
value_type: S_DWORD
|
|
||||||
read_lambda: |-
|
|
||||||
return 31;
|
|
||||||
write_lambda: |-
|
|
||||||
printf("address=%d, value=%d", x);
|
|
||||||
return true;
|
|
||||||
max_cmd_retries: 0
|
|
||||||
- id: modbus_controller4
|
|
||||||
modbus_id: mod_bus2
|
|
||||||
address: 0x4
|
|
||||||
server_courtesy_response:
|
|
||||||
enabled: true
|
|
||||||
register_last_address: 100
|
|
||||||
register_value: 0
|
|
||||||
server_registers:
|
|
||||||
- address: 0x0001
|
|
||||||
value_type: U_WORD
|
|
||||||
read_lambda: |-
|
|
||||||
return 0x8;
|
|
||||||
- address: 0x0005
|
|
||||||
value_type: U_WORD
|
|
||||||
read_lambda: |-
|
|
||||||
return (random_uint32() % 100);
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: modbus_controller1
|
modbus_controller_id: modbus_controller1
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
modbus:
|
||||||
|
- id: mod_bus2
|
||||||
|
uart_id: uart_bus
|
||||||
|
role: server
|
||||||
|
|
||||||
|
modbus_server:
|
||||||
|
- id: modbus_server2
|
||||||
|
address: 0x2
|
||||||
|
modbus_id: mod_bus2
|
||||||
|
registers:
|
||||||
|
- address: 0x0
|
||||||
|
value_type: S_DWORD_R
|
||||||
|
read_lambda: |-
|
||||||
|
return 42.3;
|
||||||
|
- id: modbus_server3
|
||||||
|
address: 0x3
|
||||||
|
modbus_id: mod_bus2
|
||||||
|
registers:
|
||||||
|
- address: 0x9
|
||||||
|
value_type: S_DWORD
|
||||||
|
read_lambda: |-
|
||||||
|
return 31;
|
||||||
|
write_lambda: |-
|
||||||
|
printf("address=%d, value=%d", x);
|
||||||
|
return true;
|
||||||
|
- id: modbus_server4
|
||||||
|
modbus_id: mod_bus2
|
||||||
|
address: 0x4
|
||||||
|
courtesy_response:
|
||||||
|
enabled: true
|
||||||
|
register_last_address: 100
|
||||||
|
register_value: 0
|
||||||
|
registers:
|
||||||
|
- address: 0x1
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: |-
|
||||||
|
return 0x8;
|
||||||
|
- address: 0x5
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: |-
|
||||||
|
return (random_uint32() % 100);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -86,9 +86,9 @@ modbus:
|
|||||||
uart_id: virtual_uart_dev
|
uart_id: virtual_uart_dev
|
||||||
role: server
|
role: server
|
||||||
|
|
||||||
modbus_controller:
|
modbus_server:
|
||||||
- address: 1
|
- address: 1
|
||||||
server_registers:
|
registers:
|
||||||
- address: 0x03
|
- address: 0x03
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
read_lambda: |-
|
read_lambda: |-
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ uart_mock:
|
|||||||
data: !lambda return data;
|
data: !lambda return data;
|
||||||
- id: virtual_uart_controller
|
- id: virtual_uart_controller
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
auto_start: true # See comment on virtual_uart_server above
|
auto_start: true # See comment on virtual_uart_server above
|
||||||
debug:
|
debug:
|
||||||
on_tx:
|
on_tx:
|
||||||
- then:
|
- then:
|
||||||
@@ -56,10 +56,11 @@ modbus_controller:
|
|||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
id: modbus_controller_1
|
id: modbus_controller_1
|
||||||
|
|
||||||
|
modbus_server:
|
||||||
- address: 1
|
- address: 1
|
||||||
modbus_id: virtual_modbus_server
|
modbus_id: virtual_modbus_server
|
||||||
id: modbus_server_1
|
id: modbus_server_1
|
||||||
server_registers:
|
registers:
|
||||||
- address: 0x01
|
- address: 0x01
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
read_lambda: return 99;
|
read_lambda: return 99;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ uart_mock:
|
|||||||
data: !lambda return data;
|
data: !lambda return data;
|
||||||
- id: virtual_uart_server_2
|
- id: virtual_uart_server_2
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
auto_start: true # See comment on virtual_uart_server above
|
auto_start: true # See comment on virtual_uart_server above
|
||||||
debug:
|
debug:
|
||||||
on_tx:
|
on_tx:
|
||||||
- then:
|
- then:
|
||||||
@@ -48,7 +48,7 @@ uart_mock:
|
|||||||
data: !lambda return data;
|
data: !lambda return data;
|
||||||
- id: virtual_uart_controller
|
- id: virtual_uart_controller
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
auto_start: true # See comment on virtual_uart_server above
|
auto_start: true # See comment on virtual_uart_server above
|
||||||
debug:
|
debug:
|
||||||
on_tx:
|
on_tx:
|
||||||
- then:
|
- then:
|
||||||
@@ -81,15 +81,16 @@ modbus_controller:
|
|||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
id: modbus_controller_2
|
id: modbus_controller_2
|
||||||
|
|
||||||
|
modbus_server:
|
||||||
- address: 1
|
- address: 1
|
||||||
modbus_id: virtual_modbus_server
|
modbus_id: virtual_modbus_server
|
||||||
server_registers:
|
registers:
|
||||||
- address: 0x01
|
- address: 0x01
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
read_lambda: return 919;
|
read_lambda: return 919;
|
||||||
- address: 2
|
- address: 2
|
||||||
modbus_id: virtual_modbus_server_2
|
modbus_id: virtual_modbus_server_2
|
||||||
server_registers:
|
registers:
|
||||||
- address: 0x01
|
- address: 0x01
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
read_lambda: return 929;
|
read_lambda: return 929;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ uart_mock:
|
|||||||
data: !lambda return data;
|
data: !lambda return data;
|
||||||
- id: virtual_uart_controller
|
- id: virtual_uart_controller
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
auto_start: true # See comment on virtual_uart_server above
|
auto_start: true # See comment on virtual_uart_server above
|
||||||
debug:
|
debug:
|
||||||
on_tx:
|
on_tx:
|
||||||
- then:
|
- then:
|
||||||
@@ -94,10 +94,11 @@ modbus_controller:
|
|||||||
update_interval: 2s
|
update_interval: 2s
|
||||||
id: modbus_controller_1
|
id: modbus_controller_1
|
||||||
|
|
||||||
|
modbus_server:
|
||||||
- address: 1
|
- address: 1
|
||||||
modbus_id: virtual_modbus_server
|
modbus_id: virtual_modbus_server
|
||||||
id: modbus_server_1
|
id: modbus_server_1
|
||||||
server_registers:
|
registers:
|
||||||
- address: 0x01
|
- address: 0x01
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
read_lambda: return id(stored_u_word);
|
read_lambda: return id(stored_u_word);
|
||||||
|
|||||||
Reference in New Issue
Block a user