Compare commits

..
3 changed files with 252 additions and 87 deletions
@@ -30,10 +30,53 @@ using modbus::ModbusFunctionCode;
using modbus::ModbusRegisterType;
#pragma GCC diagnostic pop
// Span overloads of the former modbus_controller helpers: read lambdas receive their payload as a
// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }
ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) {
return modbus::helpers::modbus_register_read_function(reg_type);
}
ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) {
return modbus::helpers::modbus_register_write_function(reg_type);
}
ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }
ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
return modbus::helpers::byte_from_hex_str(value, pos);
}
ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
return modbus::helpers::word_from_hex_str(value, pos);
}
ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
return modbus::helpers::dword_from_hex_str(value, pos);
}
ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
return modbus::helpers::qword_from_hex_str(value, pos);
}
template<typename T>
ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
return modbus::helpers::get_data<T>(data, buffer_offset);
}
// Span overloads of the deprecated helpers below: read lambdas receive their payload as a
// std::span<const uint8_t> (previously a const std::vector<uint8_t> &), and a span does not convert to
// a vector, so existing lambdas calling these by name need an overload that accepts one. These carry
// the 2026.8.0 deprecation window, since the span forms only exist from it.
// this release's deprecation window, since the span forms only exist from it.
// payload_to_number() deliberately has no such overload: one of its arguments is a modbus::helpers
// type, so a span call already reaches the helper by argument-dependent lookup, and a forwarder here
// would only make that call ambiguous.
@@ -56,6 +99,33 @@ inline bool coil_from_vector(int coil, std::span<const uint8_t> data) {
return modbus::helpers::bit_from_packed(coil, data);
}
template<typename N>
ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
N mask_and_shift_by_rightbit(N data, uint32_t mask) {
return modbus::helpers::mask_and_shift_by_rightbit(data, mask);
}
ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
modbus::helpers::number_to_payload(data, value, value_type);
}
ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
return modbus::helpers::payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask)
.value_or(0);
}
ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
std::vector<uint16_t> data;
modbus::helpers::float_to_payload(data, value, value_type);
return data;
}
class ModbusController;
/// How an item relates to the register range built just before it (same register type, address order).
/// The numeric order doubles as the comparator tiebreak for items at the same address (see
/// SensorItemsComparator): AUTO items form the shared range first, so a NEVER item comes last and
@@ -64,6 +64,9 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void loop() override;
#endif
#endif
#if (defined(USE_ESP32) && SOC_RMT_SUPPORTED) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || \
defined(REMOTE_TRANSMITTER_BK_PWM)
@@ -145,21 +148,32 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
void wait_for_rmt_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
static bool tx_done_callback(rmt_channel_handle_t channel, const rmt_tx_done_event_data_t *event, void *arg);
size_t encode_symbols_(rmt_symbol_half_t *out, uint32_t send_wait, uint32_t *offset);
void wait_all_done_();
void deliver_completion_();
RemoteTransmitterComponentStore store_{};
std::vector<rmt_symbol_half_t> rmt_temp_;
#else
std::vector<rmt_symbol_word_t> rmt_temp_;
#endif
uint32_t current_carrier_frequency_{38000};
bool initialized_{false};
bool with_dma_{false};
bool eot_level_{false};
rmt_channel_handle_t channel_{NULL};
rmt_encoder_handle_t encoder_{NULL};
esp_err_t error_code_{ESP_OK};
std::string error_string_;
bool initialized_{false};
bool with_dma_{false};
bool eot_level_{false};
bool inverted_{false};
bool non_blocking_{false};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
// set when a frame is handed to the hardware, cleared once its completion is reported;
// the transmit done interrupt sets tx_done_
bool tx_active_{false};
volatile bool tx_done_{false};
#endif
#endif
uint8_t carrier_duty_percent_{50};
@@ -15,6 +15,9 @@ static const char *const TAG = "remote_transmitter";
static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
// How long a blocking wait sleeps between watchdog feeds
static constexpr int RMT_WAIT_SLICE_MS = 50;
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free,
rmt_symbol_word_t *symbols, bool *done, void *arg) {
auto *store = static_cast<RemoteTransmitterComponentStore *>(arg);
@@ -49,6 +52,31 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
*done = false;
return count;
}
// Splits a duration into 15-bit symbols; with out == nullptr only counts them
static size_t write_symbols(rmt_symbol_half_t *out, size_t pos, uint32_t ticks, bool level) {
size_t count = 0;
while (ticks > 0) {
uint32_t duration = std::min(ticks, RMT_SYMBOL_DURATION_MAX);
if (out != nullptr) {
out[pos + count] = {
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(level),
};
}
ticks -= duration;
count++;
}
return count;
}
bool IRAM_ATTR HOT RemoteTransmitterComponent::tx_done_callback(rmt_channel_handle_t channel,
const rmt_tx_done_event_data_t *event, void *arg) {
auto *self = static_cast<RemoteTransmitterComponent *>(arg);
self->tx_done_ = true;
self->enable_loop_soon_any_context();
return false;
}
#endif
void RemoteTransmitterComponent::setup() {
@@ -83,8 +111,12 @@ void RemoteTransmitterComponent::digital_write(bool value) {
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = value;
config.flags.queue_nonblocking = 1;
// a frame still on the wire finishes first and reports its completion
this->wait_for_rmt_();
this->store_.times = 1;
this->store_.index = 0;
rmt_encoder_handle_t encoder = this->encoder_;
#else
rmt_symbol_word_t symbol = {
.duration0 = 1,
@@ -95,17 +127,24 @@ void RemoteTransmitterComponent::digital_write(bool value) {
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = value;
rmt_encoder_handle_t encoder = this->encoder_;
#endif
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config);
esp_err_t error = rmt_transmit(this->channel_, encoder, &symbol, sizeof(symbol), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
this->wait_all_done_();
// a level write is not a frame, so its completion is not reported
this->tx_done_ = false;
#else
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
#endif
}
void RemoteTransmitterComponent::configure_rmt_() {
@@ -152,6 +191,17 @@ void RemoteTransmitterComponent::configure_rmt_() {
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
rmt_tx_event_callbacks_t callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.on_trans_done = tx_done_callback;
error = rmt_tx_register_event_callbacks(this->channel_, &callbacks, this);
if (error != ESP_OK) {
this->error_code_ = error;
this->error_string_ = "in rmt_tx_register_event_callbacks";
this->mark_failed();
return;
}
rmt_simple_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder));
encoder.callback = encoder_callback;
@@ -206,6 +256,118 @@ void RemoteTransmitterComponent::configure_rmt_() {
}
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
// Blocks until the hardware is idle, feeding the watchdog while waiting
void RemoteTransmitterComponent::wait_all_done_() {
esp_err_t error;
while ((error = rmt_tx_wait_all_done(this->channel_, RMT_WAIT_SLICE_MS)) == ESP_ERR_TIMEOUT) {
App.feed_wdt();
}
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
}
void RemoteTransmitterComponent::deliver_completion_() {
this->tx_done_ = false;
this->tx_active_ = false;
this->complete_trigger_.trigger();
}
// Blocks until any frame on the wire has gone out and reports its completion
void RemoteTransmitterComponent::wait_for_rmt_() {
this->wait_all_done_();
if (this->tx_active_)
this->deliver_completion_();
}
void RemoteTransmitterComponent::loop() {
if (this->tx_done_)
this->deliver_completion_();
// the transmit done interrupt re-enables the loop
this->disable_loop();
}
// Encodes the repeat gap followed by the frame; with out == nullptr only counts symbols.
// The gap leads the buffer so the encoder skips it on the first pass and replays it
// before every repeat; offset receives the index of the first frame symbol.
size_t RemoteTransmitterComponent::encode_symbols_(rmt_symbol_half_t *out, uint32_t send_wait, uint32_t *offset) {
size_t count = write_symbols(out, 0, this->from_microseconds_(send_wait), this->eot_level_);
*offset = count;
for (int32_t value : this->temp_.get_data()) {
bool level = value >= 0;
if (!level) {
value = -value;
}
count += write_symbols(out, count, this->from_microseconds_(static_cast<uint32_t>(value)), level ^ this->inverted_);
}
return count;
}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed()) {
return;
}
if (this->tx_active_ && this->tx_done_) {
// finished, but loop() has not run yet
this->deliver_completion_();
}
if (send_times == 0 || this->tx_active_) {
// nothing is sent, but both triggers still fire so an on_complete-sequenced automation
// does not stall; a zero repeat count would never finish in the encoder, and a frame that
// arrives while another is on the wire is dropped rather than blocking the loop
if (this->tx_active_) {
ESP_LOGW(TAG, "Transmitter busy, dropping");
this->status_set_warning();
}
this->transmit_trigger_.trigger();
this->complete_trigger_.trigger();
return;
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
}
uint32_t offset;
size_t count = this->encode_symbols_(nullptr, send_wait, &offset);
if (count <= offset) {
ESP_LOGE(TAG, "Empty data");
return;
}
this->rmt_temp_.resize(count);
this->encode_symbols_(this->rmt_temp_.data(), send_wait, &offset);
this->store_.times = send_times;
this->store_.index = offset;
this->transmit_trigger_.trigger();
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = this->eot_level_;
config.flags.queue_nonblocking = 1;
this->tx_done_ = false;
this->tx_active_ = true;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
// nothing will complete, so report it now
this->deliver_completion_();
return;
}
this->status_clear_warning();
if (!this->non_blocking_) {
this->wait_for_rmt_();
}
}
#else
void RemoteTransmitterComponent::wait_for_rmt_() {
esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
@@ -216,87 +378,6 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
this->complete_trigger_.trigger();
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
uint64_t total_duration = 0;
if (this->is_failed()) {
return;
}
// if the timeout was cancelled, block until the tx is complete
if (this->non_blocking_ && this->cancel_timeout("complete")) {
this->wait_for_rmt_();
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
}
this->rmt_temp_.clear();
this->rmt_temp_.reserve(this->temp_.get_data().size() + 1);
// encode any delay at the start of the buffer to simplify the encoder callback
// this will be skipped the first time around
total_duration += send_wait * (send_times - 1);
send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
while (send_wait > 0) {
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
this->rmt_temp_.push_back({
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(this->eot_level_),
});
send_wait -= duration;
}
// encode data
size_t offset = this->rmt_temp_.size();
for (int32_t value : this->temp_.get_data()) {
bool level = value >= 0;
if (!level) {
value = -value;
}
total_duration += value * send_times;
value = this->from_microseconds_(static_cast<uint32_t>(value));
while (value > 0) {
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
this->rmt_temp_.push_back({
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(level ^ this->inverted_),
});
value -= duration;
}
}
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) {
ESP_LOGE(TAG, "Empty data");
return;
}
this->transmit_trigger_.trigger();
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = this->eot_level_;
this->store_.times = send_times;
this->store_.index = offset;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
} else {
this->status_clear_warning();
}
if (this->non_blocking_) {
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
} else {
this->wait_for_rmt_();
}
}
#else
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed())
return;