mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 17:00:21 +00:00
[modbus_controller] Refactor to simplify message handling (#11781)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e31a43af17
commit
b93b21eab1
@@ -6,209 +6,221 @@ namespace esphome::modbus_controller {
|
||||
|
||||
static const char *const TAG = "modbus_controller";
|
||||
|
||||
void ModbusController::setup() { this->create_register_ranges_(); }
|
||||
void ModbusController::setup() { this->create_polling_commands_(); }
|
||||
|
||||
/*
|
||||
To work with the existing modbus class and avoid polling for responses a command queue is used.
|
||||
send_next_command will submit the command at the top of the queue and set the corresponding callback
|
||||
to handle the response from the device.
|
||||
Once the response has been processed it is removed from the queue and the next command is sent
|
||||
*/
|
||||
bool ModbusController::send_next_command_() {
|
||||
uint32_t last_send = millis() - this->last_command_timestamp_;
|
||||
ModbusCommandItem::ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address,
|
||||
RegisterRange &&range)
|
||||
: modbus::ModbusClientDevice(parent, address),
|
||||
sensors(std::move(range.sensors)),
|
||||
skip_updates(range.skip_updates),
|
||||
register_type_(range.register_type),
|
||||
start_address_(range.start_address),
|
||||
register_count_(range.register_count),
|
||||
function_code_(modbus::helpers::modbus_register_read_function(range.register_type)),
|
||||
controller_(&controller) {}
|
||||
|
||||
if ((last_send > this->command_throttle_) && this->ready_for_immediate_send() && !this->command_queue_.empty()) {
|
||||
auto &command = this->command_queue_.front();
|
||||
|
||||
// remove from queue if command was sent too often
|
||||
if (!command->should_retry(this->max_cmd_retries_)) {
|
||||
if (!this->module_offline_) {
|
||||
ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
|
||||
|
||||
if (this->offline_skip_updates_ > 0) {
|
||||
// Update skip_updates_counter to stop flooding channel with timeouts
|
||||
for (auto &r : this->register_ranges_) {
|
||||
r.skip_updates_counter = this->offline_skip_updates_;
|
||||
}
|
||||
}
|
||||
|
||||
this->module_offline_ = true;
|
||||
this->offline_callback_.call((int) command->function_code, command->register_address);
|
||||
}
|
||||
ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue",
|
||||
this->address_, command->register_address);
|
||||
this->command_queue_.pop_front();
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
|
||||
command->register_address, command->register_count);
|
||||
command->send();
|
||||
|
||||
this->last_command_timestamp_ = millis();
|
||||
|
||||
this->command_sent_callback_.call((int) command->function_code, command->register_address);
|
||||
|
||||
// remove from queue if no handler is defined
|
||||
if (!command->on_data_func) {
|
||||
this->command_queue_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
return (!this->command_queue_.empty());
|
||||
ModbusCommandItem::ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address,
|
||||
SensorItem *sensor)
|
||||
: modbus::ModbusClientDevice(parent, address),
|
||||
skip_updates(sensor->skip_updates),
|
||||
start_address_(sensor->start_address),
|
||||
register_count_(sensor->register_count),
|
||||
function_code_(FunctionCode::CUSTOM),
|
||||
custom_data_(&sensor->custom_data),
|
||||
controller_(&controller) {
|
||||
this->sensors.insert(sensor);
|
||||
}
|
||||
|
||||
// Queue incoming response
|
||||
void ModbusController::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
if (this->command_queue_.empty()) {
|
||||
ESP_LOGW(TAG, "Received modbus data but command queue is empty");
|
||||
return;
|
||||
// The base deletes copy/move; command items re-provide construction. The moved-from device must not
|
||||
// unregister the hub slot we just took over, so its parent_ is cleared. The copy constructor exists
|
||||
// only for callers that pass an lvalue to queue_command() (in-tree callers move); remove it when
|
||||
// queue_command() is removed.
|
||||
ModbusCommandItem::ModbusCommandItem(const ModbusCommandItem &other)
|
||||
: modbus::ModbusClientDevice(other.parent_, other.address_),
|
||||
sensors(other.sensors),
|
||||
skip_updates(other.skip_updates),
|
||||
on_data_func(other.on_data_func),
|
||||
register_type_(other.register_type_),
|
||||
start_address_(other.start_address_),
|
||||
register_count_(other.register_count_),
|
||||
function_code_(other.function_code_),
|
||||
custom_data_(other.custom_data_),
|
||||
controller_(other.controller_) {
|
||||
// SmallInlineBuffer is move-only, so deep-copy the bytes explicitly.
|
||||
this->payload.set(other.payload.data(), other.payload.size());
|
||||
}
|
||||
|
||||
ModbusCommandItem::ModbusCommandItem(ModbusCommandItem &&other) noexcept
|
||||
: modbus::ModbusClientDevice(other.parent_, other.address_),
|
||||
sensors(std::move(other.sensors)),
|
||||
skip_updates(other.skip_updates),
|
||||
on_data_func(std::move(other.on_data_func)),
|
||||
payload(std::move(other.payload)),
|
||||
register_type_(other.register_type_),
|
||||
start_address_(other.start_address_),
|
||||
register_count_(other.register_count_),
|
||||
function_code_(other.function_code_),
|
||||
custom_data_(other.custom_data_),
|
||||
controller_(other.controller_) {
|
||||
other.parent_ = nullptr;
|
||||
}
|
||||
|
||||
// A valid response: the device is online. Dispatch the payload to the handler or the range's sensors.
|
||||
void ModbusCommandItem::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
if (this->controller_ != nullptr)
|
||||
this->controller_->set_online(true, static_cast<int>(this->function_code_), this->start_address_);
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (this->on_data_func) {
|
||||
this->on_data_func(this->register_type_, this->start_address_, data);
|
||||
} else if (modbus::helpers::is_function_code_write(static_cast<uint8_t>(this->function_code_))) {
|
||||
// write acknowledgement - nothing to publish
|
||||
} else {
|
||||
for (auto *sensor : this->sensors)
|
||||
sensor->parse_and_publish(data);
|
||||
}
|
||||
auto ¤t_command = this->command_queue_.front();
|
||||
if (current_command != nullptr) {
|
||||
if (this->controller_ != nullptr)
|
||||
this->controller_->unqueue_command(this);
|
||||
}
|
||||
|
||||
// An exception response is still a legitimate reply, so the device is considered online.
|
||||
void ModbusCommandItem::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) {
|
||||
const uint8_t function_code = request_pdu.empty() ? 0 : request_pdu[0];
|
||||
ESP_LOGW(TAG, "Modbus error function code: 0x%X register 0x%X exception: %d", function_code, this->start_address_,
|
||||
static_cast<uint8_t>(exception_code));
|
||||
if (this->controller_ != nullptr) {
|
||||
this->controller_->set_online(true, function_code, this->start_address_);
|
||||
this->controller_->unqueue_command(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Not being sent says nothing about online/offline status; just drop it from the pending list.
|
||||
void ModbusCommandItem::on_not_sent(std::span<const uint8_t> request_pdu) {
|
||||
// A dropped write is lost while the entity has already published optimistically, so surface it.
|
||||
if (modbus::helpers::is_function_code_write(static_cast<uint8_t>(this->function_code_))) {
|
||||
ESP_LOGW(TAG, "Write not sent: function 0x%X register 0x%X", static_cast<uint8_t>(this->function_code_),
|
||||
this->start_address_);
|
||||
}
|
||||
if (this->controller_ != nullptr)
|
||||
this->controller_->unqueue_command(this);
|
||||
}
|
||||
|
||||
// Fired once per wire transmission (including hub re-queues from a retry), so the on_command_sent
|
||||
// trigger reflects when the frame actually went out, not when it was queued.
|
||||
void ModbusCommandItem::on_sent(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ != nullptr)
|
||||
this->controller_->command_sent(static_cast<int>(this->function_code_), this->start_address_);
|
||||
}
|
||||
|
||||
bool ModbusCommandItem::on_no_response(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ == nullptr)
|
||||
return false;
|
||||
this->controller_->increment_non_response_count();
|
||||
if (this->controller_->can_send()) {
|
||||
// Have the hub re-queue the frame it is holding; on_sent fires again when it goes back out.
|
||||
return true;
|
||||
}
|
||||
this->controller_->set_online(false, static_cast<int>(this->function_code_), this->start_address_);
|
||||
this->controller_->unqueue_command(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModbusController::set_online(bool online, int function_code, int register_address) {
|
||||
if (online) {
|
||||
this->cmd_non_responses_ = 0;
|
||||
if (this->module_offline_) {
|
||||
ESP_LOGW(TAG, "Modbus device=%d back online", this->address_);
|
||||
|
||||
if (this->offline_skip_updates_ > 0) {
|
||||
// Restore skip_updates_counter to restore commands updates
|
||||
for (auto &r : this->register_ranges_) {
|
||||
r.skip_updates_counter = 0;
|
||||
}
|
||||
}
|
||||
// Restore module online state
|
||||
this->module_offline_ = false;
|
||||
this->online_callback_.call((int) current_command->function_code, current_command->register_address);
|
||||
this->online_callback_.call(function_code, register_address);
|
||||
}
|
||||
} else {
|
||||
// Offline is a property of the physical device, so drop every sender's queued frames for its
|
||||
// address; retired frames get on_not_sent(), which reclaims one-shots through the normal path.
|
||||
this->hub_->clear_tx_queue_for_address(this->address_);
|
||||
if (!this->module_offline_) {
|
||||
ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
|
||||
this->module_offline_ = true;
|
||||
this->module_offline_at_ = this->update_counter_;
|
||||
this->offline_callback_.call(function_code, register_address);
|
||||
}
|
||||
|
||||
// Move the commandItem to the response queue. The span points into the hub's receive buffer, so
|
||||
// copy the payload into the command for deferred processing in loop().
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
current_command->payload.assign(data.begin(), data.end());
|
||||
this->incoming_queue_.push(std::move(current_command));
|
||||
ESP_LOGV(TAG, "Modbus response queued");
|
||||
this->command_queue_.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the response to the registered handler
|
||||
void ModbusController::process_modbus_data_(const ModbusCommandItem *response) {
|
||||
ESP_LOGV(TAG, "Process modbus response for address 0x%X size: %zu", response->register_address,
|
||||
response->payload.size());
|
||||
response->on_data_func(response->register_type, response->register_address, response->payload);
|
||||
void ModbusController::queue_command(ModbusCommandItem command) {
|
||||
this->sweep_completed_one_shots_(); // reclaim finished one-shots before adding a new one
|
||||
// Duplicates are the caller's to manage; the controller only holds the item until its terminal callback.
|
||||
this->one_shot_command_items_.push_back(make_unique<ModbusCommandItem>(std::move(command)));
|
||||
// A refused frame gets no terminal callback (see the hub contract), so reclaim the item here.
|
||||
auto &item = this->one_shot_command_items_.back();
|
||||
if (!item->send()) {
|
||||
// The caller (e.g. a write entity) has usually already published optimistically - surface the loss.
|
||||
ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast<uint8_t>(item->register_type()),
|
||||
item->register_address());
|
||||
item->pending_removal = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusController::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) {
|
||||
// The request function code (request_pdu[0]) already carries what the log needs; the exception bit only
|
||||
// ever appears on the response, so no masking is needed here.
|
||||
const uint8_t function_code = request_pdu.empty() ? 0 : request_pdu[0];
|
||||
ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, static_cast<uint8_t>(exception_code));
|
||||
if (this->command_queue_.empty()) {
|
||||
void ModbusController::unqueue_command(const ModbusCommandItem *command) {
|
||||
// Called as the last action of the command's own callback, and from send() after send_pdu (which may
|
||||
// synchronously call on_not_sent). Destroying `command` here would leave send() and the hub touching a
|
||||
// freed object, so we only FLAG it; sweep_completed_one_shots_() erases it later at a safe point. No-op
|
||||
// for polling commands (they persist and are not in the one-shot list).
|
||||
for (auto &item : this->one_shot_command_items_) {
|
||||
if (item.get() == command) {
|
||||
item->pending_removal = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusController::sweep_completed_one_shots_() {
|
||||
this->one_shot_command_items_.remove_if(
|
||||
[](const std::unique_ptr<ModbusCommandItem> &item) { return item->pending_removal; });
|
||||
}
|
||||
|
||||
void ModbusController::update_range_(ModbusCommandItem &cmd) {
|
||||
if (this->update_counter_ % (cmd.skip_updates + 1) != 0) {
|
||||
ESP_LOGVV(TAG, "Skipping update for range 0x%X", cmd.register_address());
|
||||
return;
|
||||
}
|
||||
// Remove pending command waiting for a response
|
||||
auto ¤t_command = this->command_queue_.front();
|
||||
if (current_command != nullptr) {
|
||||
ESP_LOGE(TAG,
|
||||
"Modbus error - last command: function code=0x%X register address = 0x%X "
|
||||
"registers count=%d "
|
||||
"payload size=%zu",
|
||||
function_code, current_command->register_address, current_command->register_count,
|
||||
current_command->payload.size());
|
||||
this->command_queue_.pop_front();
|
||||
}
|
||||
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
|
||||
if (!cmd.send())
|
||||
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address());
|
||||
}
|
||||
|
||||
SensorSet ModbusController::find_sensors_(modbus::EntityType register_type, uint16_t start_address) const {
|
||||
auto reg_it = std::find_if(
|
||||
std::begin(this->register_ranges_), std::end(this->register_ranges_),
|
||||
[=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); });
|
||||
|
||||
if (reg_it == this->register_ranges_.end()) {
|
||||
ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address);
|
||||
} else {
|
||||
return reg_it->sensors;
|
||||
}
|
||||
|
||||
// not found
|
||||
return {};
|
||||
}
|
||||
void ModbusController::on_register_data(modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address);
|
||||
|
||||
// loop through all sensors in this range; each reads its own bytes from the position resolved for it.
|
||||
auto sensors = find_sensors_(register_type, start_address);
|
||||
for (auto *sensor : sensors) {
|
||||
sensor->parse_and_publish(data);
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusController::queue_command(const ModbusCommandItem &command) {
|
||||
if (!this->allow_duplicate_commands_) {
|
||||
// check if this command is already qeued.
|
||||
// not very effective but the queue is never really large
|
||||
for (auto &item : this->command_queue_) {
|
||||
if (item->is_equal(command)) {
|
||||
ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u",
|
||||
static_cast<uint8_t>(command.register_type), command.register_address, command.register_count);
|
||||
// update the payload of the queued command
|
||||
// replaces a previous command
|
||||
item->payload = command.payload;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->command_queue_.push_back(make_unique<ModbusCommandItem>(command));
|
||||
}
|
||||
|
||||
void ModbusController::update_range_(RegisterRange &r) {
|
||||
ESP_LOGV(TAG, "Range : %X Size: %x (%d) skip: %d", r.start_address, r.register_count, (int) r.register_type,
|
||||
r.skip_updates_counter);
|
||||
if (r.skip_updates_counter == 0) {
|
||||
// if a custom command is used the user supplied custom_data is only available in the SensorItem.
|
||||
if (r.register_type == modbus::EntityType::CUSTOM) {
|
||||
auto sensors = this->find_sensors_(r.register_type, r.start_address);
|
||||
if (!sensors.empty()) {
|
||||
auto sensor = sensors.cbegin();
|
||||
auto command_item = ModbusCommandItem::create_custom_command(
|
||||
this, (*sensor)->custom_data,
|
||||
[this](modbus::EntityType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
|
||||
this->on_register_data(modbus::EntityType::CUSTOM, start_address, data);
|
||||
});
|
||||
command_item.register_address = (*sensor)->start_address;
|
||||
command_item.register_count = (*sensor)->register_count;
|
||||
command_item.function_code = FunctionCode::CUSTOM;
|
||||
queue_command(command_item);
|
||||
void ModbusController::update() {
|
||||
this->sweep_completed_one_shots_(); // reclaim one-shots deferred out of their own callbacks
|
||||
if (this->module_offline_) {
|
||||
// Offline probing follows the offline cadence alone; per-range skip_updates resumes once the
|
||||
// device is back online. Requiring both cadences to coincide would leave phase combinations
|
||||
// where a probe never goes out.
|
||||
if (offline_retry_due(this->update_counter_, this->module_offline_at_, this->offline_skip_updates_)) {
|
||||
ESP_LOGV(TAG, "Module offline - retrying");
|
||||
this->cmd_non_responses_ = 0; // allow the probe through can_send()
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
if (!cmd.send())
|
||||
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address());
|
||||
}
|
||||
} else {
|
||||
queue_command(ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count));
|
||||
ESP_LOGV(TAG, "Module offline - skipping update");
|
||||
}
|
||||
r.skip_updates_counter = r.skip_updates; // reset counter to config value
|
||||
} else {
|
||||
r.skip_updates_counter--;
|
||||
}
|
||||
}
|
||||
//
|
||||
// Queue the modbus requests to be send.
|
||||
// Once we get a response to the command it is removed from the queue and the next command is send
|
||||
//
|
||||
void ModbusController::update() {
|
||||
if (!this->command_queue_.empty()) {
|
||||
ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Updating modbus component");
|
||||
this->update_counter_++;
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto &r : this->register_ranges_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", r.start_address);
|
||||
update_range_(r);
|
||||
if (this->can_send()) {
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", cmd.register_address());
|
||||
this->update_range_(cmd);
|
||||
}
|
||||
}
|
||||
this->update_counter_++;
|
||||
}
|
||||
|
||||
// walk through the sensors and determine the register ranges to read
|
||||
size_t ModbusController::create_register_ranges_() {
|
||||
this->register_ranges_.clear();
|
||||
void ModbusController::create_polling_commands_() {
|
||||
if (this->sensorset_.empty()) {
|
||||
ESP_LOGW(TAG, "No sensors registered");
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sensors are walked in the sensor set's order (see SensorItemsComparator): register type, then
|
||||
@@ -299,7 +311,7 @@ size_t ModbusController::create_register_ranges_() {
|
||||
if (!join) {
|
||||
if (have_range) {
|
||||
ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
|
||||
this->register_ranges_.push_back(std::move(r));
|
||||
this->create_polling_command_(std::move(r));
|
||||
}
|
||||
r = {};
|
||||
range_bytes = curr->get_register_size();
|
||||
@@ -311,7 +323,6 @@ size_t ModbusController::create_register_ranges_() {
|
||||
r.register_count = curr->register_count;
|
||||
r.register_type = curr->register_type;
|
||||
r.skip_updates = curr->skip_updates;
|
||||
r.skip_updates_counter = 0;
|
||||
have_range = true;
|
||||
} else if (curr->skip_updates != 0) {
|
||||
// use the lowest non-zero skip_updates for the whole range (0 is the default and is excluded)
|
||||
@@ -326,10 +337,11 @@ size_t ModbusController::create_register_ranges_() {
|
||||
}
|
||||
if (have_range) {
|
||||
ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
|
||||
this->register_ranges_.push_back(std::move(r));
|
||||
this->create_polling_command_(std::move(r));
|
||||
}
|
||||
|
||||
return this->register_ranges_.size();
|
||||
// Reclaim growth slack; safe here because nothing has registered with the hub yet (see the
|
||||
// lifetime note on polling_command_items_).
|
||||
this->polling_command_items_.shrink_to_fit();
|
||||
}
|
||||
|
||||
void ModbusController::dump_config() {
|
||||
@@ -348,222 +360,163 @@ void ModbusController::dump_config() {
|
||||
it->get_register_size());
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "ranges");
|
||||
for (auto &it : this->register_ranges_) {
|
||||
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);
|
||||
for (auto &it : this->polling_command_items_) {
|
||||
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type()),
|
||||
it.register_address(), it.register_count(), it.skip_updates);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ModbusController::loop() {
|
||||
// Incoming data to process?
|
||||
if (!this->incoming_queue_.empty()) {
|
||||
auto &message = this->incoming_queue_.front();
|
||||
if (message != nullptr)
|
||||
this->process_modbus_data_(message.get());
|
||||
this->incoming_queue_.pop();
|
||||
|
||||
void ModbusController::on_write_register_response(EntityType register_type, uint16_t start_address,
|
||||
std::span<const uint8_t> data) {
|
||||
// A well-formed write ACK echoes address and value, but a truncated PDU yields a short/empty span.
|
||||
if (data.size() >= 3) {
|
||||
ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data<uint16_t>(data.data(), 0),
|
||||
modbus::helpers::get_data<int16_t>(data.data(), 1));
|
||||
} else {
|
||||
// all messages processed send pending commands
|
||||
this->send_next_command_();
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusController::on_write_register_response(modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data<uint16_t>(data, 0),
|
||||
modbus::helpers::get_data<int16_t>(data, 1));
|
||||
}
|
||||
|
||||
void ModbusController::dump_sensors_() {
|
||||
ESP_LOGV(TAG, "sensors");
|
||||
for (auto &it : this->sensorset_) {
|
||||
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count,
|
||||
it->get_register_size(), it->offset);
|
||||
ESP_LOGV(TAG, "Command ACK (short payload, %zu bytes)", data.size());
|
||||
}
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_read_command(
|
||||
ModbusController *modbusdevice, modbus::EntityType register_type, uint16_t start_address, uint16_t register_count,
|
||||
std::function<void(modbus::EntityType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
|
||||
&&handler) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.register_type = register_type;
|
||||
cmd.function_code = modbus::helpers::modbus_register_read_function(register_type);
|
||||
cmd.register_address = start_address;
|
||||
cmd.register_count = register_count;
|
||||
ModbusController *modbusdevice, EntityType register_type, uint16_t start_address, uint16_t register_count,
|
||||
std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> &&handler) {
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.set_command_(modbus::helpers::modbus_register_read_function(register_type), register_type, start_address,
|
||||
register_count);
|
||||
cmd.on_data_func = std::move(handler);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbusdevice,
|
||||
modbus::EntityType register_type, uint16_t start_address,
|
||||
uint16_t register_count) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.register_type = register_type;
|
||||
cmd.function_code = modbus::helpers::modbus_register_read_function(register_type);
|
||||
cmd.register_address = start_address;
|
||||
cmd.register_count = register_count;
|
||||
cmd.on_data_func = [modbusdevice](modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
modbusdevice->on_register_data(register_type, start_address, data);
|
||||
};
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_write_multiple_command(ModbusController *modbusdevice,
|
||||
uint16_t start_address, uint16_t register_count,
|
||||
const std::vector<uint16_t> &values) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.register_type = modbus::EntityType::HOLDING;
|
||||
cmd.function_code = FunctionCode::WRITE_MULTIPLE_REGISTERS;
|
||||
cmd.register_address = start_address;
|
||||
cmd.register_count = register_count;
|
||||
cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.set_command_(FunctionCode::WRITE_MULTIPLE_REGISTERS, EntityType::HOLDING, start_address, register_count);
|
||||
cmd.on_data_func = [modbusdevice](EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
|
||||
modbusdevice->on_write_register_response(register_type, start_address, data);
|
||||
};
|
||||
uint8_t *p = cmd.payload.init(values.size() * 2);
|
||||
for (auto v : values) {
|
||||
auto decoded_value = decode_value(v);
|
||||
cmd.payload.push_back(decoded_value[0]);
|
||||
cmd.payload.push_back(decoded_value[1]);
|
||||
*p++ = decoded_value[0];
|
||||
*p++ = decoded_value[1];
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_write_single_coil(ModbusController *modbusdevice, uint16_t address,
|
||||
bool value) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.register_type = modbus::EntityType::COIL;
|
||||
cmd.function_code = FunctionCode::WRITE_SINGLE_COIL;
|
||||
cmd.register_address = address;
|
||||
cmd.register_count = 1;
|
||||
cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.set_command_(FunctionCode::WRITE_SINGLE_COIL, EntityType::COIL, address, 1);
|
||||
cmd.on_data_func = [modbusdevice](EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
|
||||
modbusdevice->on_write_register_response(register_type, start_address, data);
|
||||
};
|
||||
cmd.payload.push_back(value ? 0xFF : 0);
|
||||
cmd.payload.push_back(0);
|
||||
uint8_t *p = cmd.payload.init(2);
|
||||
p[0] = value ? 0xFF : 0;
|
||||
p[1] = 0;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address,
|
||||
const std::vector<bool> &values) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.register_type = modbus::EntityType::COIL;
|
||||
cmd.function_code = FunctionCode::WRITE_MULTIPLE_COILS;
|
||||
cmd.register_address = start_address;
|
||||
cmd.register_count = values.size();
|
||||
cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.set_command_(FunctionCode::WRITE_MULTIPLE_COILS, EntityType::COIL, start_address, values.size());
|
||||
cmd.on_data_func = [modbusdevice](EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
|
||||
modbusdevice->on_write_register_response(register_type, start_address, data);
|
||||
};
|
||||
|
||||
uint8_t bitmask = 0;
|
||||
int bitcounter = 0;
|
||||
uint8_t *p = cmd.payload.init((values.size() + 7) / 8);
|
||||
memset(p, 0, (values.size() + 7) / 8);
|
||||
size_t bit = 0;
|
||||
for (auto coil : values) {
|
||||
if (coil) {
|
||||
bitmask |= (1 << bitcounter);
|
||||
p[bit / 8] |= (1 << (bit % 8));
|
||||
}
|
||||
bitcounter++;
|
||||
if (bitcounter % 8 == 0) {
|
||||
cmd.payload.push_back(bitmask);
|
||||
bitmask = 0;
|
||||
}
|
||||
}
|
||||
// add remaining bits
|
||||
if (bitcounter % 8) {
|
||||
cmd.payload.push_back(bitmask);
|
||||
bit++;
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
|
||||
uint16_t value) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.register_type = modbus::EntityType::HOLDING;
|
||||
cmd.function_code = FunctionCode::WRITE_SINGLE_REGISTER;
|
||||
cmd.register_address = start_address;
|
||||
cmd.register_count = 1; // not used here anyways
|
||||
cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.set_command_(FunctionCode::WRITE_SINGLE_REGISTER, EntityType::HOLDING, start_address, 1);
|
||||
cmd.on_data_func = [modbusdevice](EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
|
||||
modbusdevice->on_write_register_response(register_type, start_address, data);
|
||||
};
|
||||
|
||||
auto decoded_value = decode_value(value);
|
||||
cmd.payload.push_back(decoded_value[0]);
|
||||
cmd.payload.push_back(decoded_value[1]);
|
||||
uint8_t *p = cmd.payload.init(2);
|
||||
p[0] = decoded_value[0];
|
||||
p[1] = decoded_value[1];
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
ModbusController *modbusdevice, const std::vector<uint8_t> &values,
|
||||
std::function<void(modbus::EntityType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
|
||||
&&handler) {
|
||||
ModbusCommandItem cmd;
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.function_code = FunctionCode::CUSTOM;
|
||||
std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> &&handler) {
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.function_code_ = FunctionCode::CUSTOM;
|
||||
if (handler == nullptr) {
|
||||
cmd.on_data_func = [](modbus::EntityType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
|
||||
cmd.on_data_func = [](EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
|
||||
ESP_LOGI(TAG, "Custom Command sent");
|
||||
};
|
||||
} else {
|
||||
cmd.on_data_func = handler;
|
||||
}
|
||||
cmd.payload = values;
|
||||
cmd.payload.set(values.data(), values.size());
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
ModbusController *modbusdevice, const std::vector<uint16_t> &values,
|
||||
std::function<void(modbus::EntityType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
|
||||
&&handler) {
|
||||
ModbusCommandItem cmd = {};
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.function_code = FunctionCode::CUSTOM;
|
||||
std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> &&handler) {
|
||||
ModbusCommandItem cmd(*modbusdevice, modbusdevice->hub(), modbusdevice->device_address());
|
||||
cmd.function_code_ = FunctionCode::CUSTOM;
|
||||
if (handler == nullptr) {
|
||||
cmd.on_data_func = [](modbus::EntityType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
|
||||
cmd.on_data_func = [](EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
|
||||
ESP_LOGI(TAG, "Custom Command sent");
|
||||
};
|
||||
} else {
|
||||
cmd.on_data_func = handler;
|
||||
}
|
||||
uint8_t *p = cmd.payload.init(values.size() * 2);
|
||||
for (auto v : values) {
|
||||
cmd.payload.push_back((v >> 8) & 0xFF);
|
||||
cmd.payload.push_back(v & 0xFF);
|
||||
*p++ = (v >> 8) & 0xFF;
|
||||
*p++ = v & 0xFF;
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
bool ModbusCommandItem::send() {
|
||||
if (this->function_code != FunctionCode::CUSTOM) {
|
||||
modbusdevice->send_pdu(
|
||||
modbus::helpers::create_client_pdu(this->function_code, this->register_address, this->register_count,
|
||||
this->payload.empty() ? nullptr : &this->payload[0], this->payload.size()));
|
||||
bool accepted;
|
||||
if (this->function_code_ != FunctionCode::CUSTOM) {
|
||||
accepted = this->send_pdu(modbus::helpers::create_client_pdu(
|
||||
this->function_code_, this->start_address_, this->register_count_,
|
||||
this->payload.empty() ? nullptr : this->payload.data(), this->payload.size()));
|
||||
} else {
|
||||
modbusdevice->send_raw(this->payload);
|
||||
// Custom command: the bytes are a complete raw frame (address + PDU). Send the PDU to the frame's own
|
||||
// address (which may differ from this controller's); the hub appends the CRC and routes the response
|
||||
// back to this item by pointer. (send_raw() is deprecated, so send_pdu() is called with the extracted
|
||||
// address. Raw-frame semantics are kept here; the custom_pdu migration is a later step.)
|
||||
std::span<const uint8_t> frame =
|
||||
this->custom_data_ != nullptr ? std::span<const uint8_t>(*this->custom_data_) : this->payload;
|
||||
if (frame.empty()) {
|
||||
ESP_LOGW(TAG, "Empty custom command frame, not sent");
|
||||
accepted = false;
|
||||
} else {
|
||||
accepted = this->parent_->send_pdu(frame[0], frame.subspan(1), this);
|
||||
}
|
||||
}
|
||||
this->send_count_++;
|
||||
ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address,
|
||||
this->register_count, this->send_count_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) {
|
||||
// for custom commands we have to check for identical payloads, since
|
||||
// address/count/type fields will be set to zero
|
||||
return this->function_code == FunctionCode::CUSTOM
|
||||
? this->payload == other.payload
|
||||
: other.register_address == this->register_address && other.register_count == this->register_count &&
|
||||
other.register_type == this->register_type && other.function_code == this->function_code;
|
||||
// The on_command_sent trigger fires from on_sent() when the frame actually reaches the wire.
|
||||
if (accepted) {
|
||||
ESP_LOGV(TAG, "Command queued %d 0x%X %d", uint8_t(this->function_code_), this->start_address_,
|
||||
this->register_count_);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus_controller
|
||||
|
||||
Reference in New Issue
Block a user