mirror of
https://github.com/esphome/esphome.git
synced 2025-10-26 20:53:50 +00:00
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:
@@ -23,6 +23,8 @@ CODEOWNERS = ["@martgras"]
|
||||
|
||||
AUTO_LOAD = ["modbus"]
|
||||
|
||||
CONF_READ_LAMBDA = "read_lambda"
|
||||
CONF_SERVER_REGISTERS = "server_registers"
|
||||
MULTI_CONF = True
|
||||
|
||||
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
||||
@@ -31,6 +33,7 @@ ModbusController = modbus_controller_ns.class_(
|
||||
)
|
||||
|
||||
SensorItem = modbus_controller_ns.struct("SensorItem")
|
||||
ServerRegister = modbus_controller_ns.struct("ServerRegister")
|
||||
|
||||
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
|
||||
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
|
||||
@@ -94,10 +97,18 @@ TYPE_REGISTER_MAP = {
|
||||
"FP32_R": 2,
|
||||
}
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -106,6 +117,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_COMMAND_THROTTLE, default="0ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_SERVER_REGISTERS,
|
||||
): cv.ensure_list(ModbusServerRegisterSchema),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -154,6 +168,17 @@ def validate_modbus_register(config):
|
||||
return config
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
if CONF_SERVER_REGISTERS in config:
|
||||
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
|
||||
config
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def modbus_calc_properties(config):
|
||||
byte_offset = 0
|
||||
reg_count = 0
|
||||
@@ -183,7 +208,7 @@ def modbus_calc_properties(config):
|
||||
|
||||
|
||||
async def add_modbus_base_properties(
|
||||
var, config, sensor_type, lamdba_param_type=cg.float_, lamdba_return_type=float
|
||||
var, config, sensor_type, lambda_param_type=cg.float_, lambda_return_type=float
|
||||
):
|
||||
if CONF_CUSTOM_COMMAND in config:
|
||||
cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND]))
|
||||
@@ -196,13 +221,13 @@ async def add_modbus_base_properties(
|
||||
config[CONF_LAMBDA],
|
||||
[
|
||||
(sensor_type.operator("ptr"), "item"),
|
||||
(lamdba_param_type, "x"),
|
||||
(lambda_param_type, "x"),
|
||||
(
|
||||
cg.std_vector.template(cg.uint8).operator("const").operator("ref"),
|
||||
"data",
|
||||
),
|
||||
],
|
||||
return_type=cg.optional.template(lamdba_return_type),
|
||||
return_type=cg.optional.template(lambda_return_type),
|
||||
)
|
||||
cg.add(var.set_template(template_))
|
||||
|
||||
@@ -211,6 +236,23 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
|
||||
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]:
|
||||
cg.add(
|
||||
var.add_server_register(
|
||||
cg.new_Pvariable(
|
||||
server_register[CONF_ID],
|
||||
server_register[CONF_ADDRESS],
|
||||
server_register[CONF_VALUE_TYPE],
|
||||
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
|
||||
await cg.process_lambda(
|
||||
server_register[CONF_READ_LAMBDA],
|
||||
[],
|
||||
return_type=cg.float_,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
await register_modbus_device(var, config)
|
||||
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ namespace modbus_controller {
|
||||
|
||||
static const char *const TAG = "modbus_controller";
|
||||
|
||||
void ModbusController::setup() {
|
||||
// Modbus::setup();
|
||||
this->create_register_ranges_();
|
||||
}
|
||||
void ModbusController::setup() { this->create_register_ranges_(); }
|
||||
|
||||
/*
|
||||
To work with the existing modbus class and avoid polling for responses a command queue is used.
|
||||
@@ -102,6 +99,51 @@ 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);
|
||||
|
||||
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) {
|
||||
float value = server_register->read_lambda();
|
||||
|
||||
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.",
|
||||
server_register->address, static_cast<uint8_t>(server_register->value_type),
|
||||
server_register->register_count, value);
|
||||
number_to_payload(sixteen_bit_response, value, server_register->value_type);
|
||||
current_address += server_register->register_count;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address);
|
||||
std::vector<uint8_t> error_response;
|
||||
error_response.push_back(this->address_);
|
||||
error_response.push_back(0x81);
|
||||
error_response.push_back(0x02);
|
||||
this->send_raw(error_response);
|
||||
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());
|
||||
}
|
||||
|
||||
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
|
||||
auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) {
|
||||
return (r.start_address == start_address && r.register_type == register_type);
|
||||
@@ -190,7 +232,7 @@ void ModbusController::update() {
|
||||
// walk through the sensors and determine the register ranges to read
|
||||
size_t ModbusController::create_register_ranges_() {
|
||||
register_ranges_.clear();
|
||||
if (sensorset_.empty()) {
|
||||
if (this->parent_->role == modbus::ModbusRole::CLIENT && sensorset_.empty()) {
|
||||
ESP_LOGW(TAG, "No sensors registered");
|
||||
return 0;
|
||||
}
|
||||
@@ -309,6 +351,11 @@ void ModbusController::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
||||
it.start_address, it.register_count, it.skip_updates);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "server registers");
|
||||
for (auto &r : server_registers_) {
|
||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address,
|
||||
static_cast<uint8_t>(r->value_type), r->register_count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <list>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -251,6 +252,21 @@ class SensorItem {
|
||||
bool force_new_range{false};
|
||||
};
|
||||
|
||||
class ServerRegister {
|
||||
public:
|
||||
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count,
|
||||
std::function<float()> read_lambda) {
|
||||
this->address = address;
|
||||
this->value_type = value_type;
|
||||
this->register_count = register_count;
|
||||
this->read_lambda = std::move(read_lambda);
|
||||
}
|
||||
uint16_t address;
|
||||
SensorValueType value_type;
|
||||
uint8_t register_count;
|
||||
std::function<float()> read_lambda;
|
||||
};
|
||||
|
||||
// ModbusController::create_register_ranges_ tries to optimize register range
|
||||
// for this the sensors must be ordered by register_type, start_address and bitmask
|
||||
class SensorItemsComparator {
|
||||
@@ -418,10 +434,14 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
void queue_command(const ModbusCommandItem &command);
|
||||
/// Registers a sensor with the controller. Called by esphomes code generator
|
||||
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
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
/// called when a modbus error response was received
|
||||
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
|
||||
/// called when a modbus request (function code 3 or 4) was parsed without errors
|
||||
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
|
||||
/// 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);
|
||||
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
|
||||
@@ -452,6 +472,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
void dump_sensors_();
|
||||
/// Collection of all sensors for this component
|
||||
SensorSet sensorset_;
|
||||
/// Collection of all server registers for this component
|
||||
std::vector<ServerRegister *> server_registers_;
|
||||
/// Continuous range of modbus registers
|
||||
std::vector<RegisterRange> register_ranges_;
|
||||
/// Hold the pending requests to be sent
|
||||
|
||||
Reference in New Issue
Block a user