From c9a6797a970f63f04346684a2fc227022fc89a7f Mon Sep 17 00:00:00 2001 From: Kuba Sawulski Date: Sat, 10 Oct 2026 23:21:54 +0200 Subject: [PATCH 1/4] (socket_transmitter) Send the payload as a span and adopt Parented helper --- .../socket_transmitter/socket_transmitter.cpp | 12 +++++----- .../socket_transmitter/socket_transmitter.h | 22 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/components/socket_transmitter/socket_transmitter.cpp b/components/socket_transmitter/socket_transmitter.cpp index 826d8d9a..a46a9393 100644 --- a/components/socket_transmitter/socket_transmitter.cpp +++ b/components/socket_transmitter/socket_transmitter.cpp @@ -2,11 +2,11 @@ namespace esphome { namespace socket_transmitter { -void SocketTransmitter::send(std::string data) { return this->send((uint8_t *) data.c_str(), data.length()); } - -void SocketTransmitter::send(std::vector data) { return this->send(data.data(), data.size()); } +void SocketTransmitter::send(std::string_view data) { + this->send(std::span(reinterpret_cast(data.data()), data.size())); +} -void SocketTransmitter::send(const uint8_t *data, size_t length) { +void SocketTransmitter::send(std::span data) { ESP_LOGD(TAG, "Setting up socket transmitter"); this->socket_ = socket::socket_ip(this->protocol, 0); int enable = 1; @@ -20,8 +20,8 @@ void SocketTransmitter::send(const uint8_t *data, size_t length) { return; } - ESP_LOGD(TAG, "Sending frame [%d bytes]", length); - int n_bytes = this->socket_->write(data, length); + ESP_LOGD(TAG, "Sending frame [%zu bytes]", data.size()); + int n_bytes = this->socket_->write(data.data(), data.size()); if (n_bytes < 0) ESP_LOGE(TAG, "Failed to send message"); this->socket_->close(); diff --git a/components/socket_transmitter/socket_transmitter.h b/components/socket_transmitter/socket_transmitter.h index 61a81de7..a06654ab 100644 --- a/components/socket_transmitter/socket_transmitter.h +++ b/components/socket_transmitter/socket_transmitter.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -13,12 +14,11 @@ static const char *TAG = "socket_transmitter"; class SocketTransmitter : public Component { public: - void set_host(std::string host) { this->host = host; }; - void set_port(int port) { this->port = port; }; - void set_protocol(int protocol) { this->protocol = protocol; }; - void send(std::string data); - void send(std::vector data); - void send(const uint8_t *data, size_t length); + void set_host(std::string host) { this->host = std::move(host); } + void set_port(int port) { this->port = port; } + void set_protocol(int protocol) { this->protocol = protocol; } + void send(std::span data); + void send(std::string_view data); void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } @@ -29,16 +29,14 @@ class SocketTransmitter : public Component { std::unique_ptr socket_; }; -template class SocketTransmitterSendAction : public Action { +template +class SocketTransmitterSendAction : public Action, public Parented { public: - SocketTransmitterSendAction(SocketTransmitter *parent) : parent_(parent) {} + SocketTransmitterSendAction(SocketTransmitter *parent = nullptr) : Parented(parent) {} - TEMPLATABLE_VALUE(StrOrVector, data) + TEMPLATABLE_VALUE(T, data) void play(const Ts &...x) override { this->parent_->send(this->data_.value(x...)); } - - protected: - SocketTransmitter *parent_; }; } // namespace socket_transmitter } // namespace esphome From 626504eee090f7741d454511e8d3fc58ae939307 Mon Sep 17 00:00:00 2001 From: Kuba Sawulski Date: Sat, 10 Oct 2026 23:22:18 +0200 Subject: [PATCH 2/4] (wmbus_radio) Modularize transceiver architecture and add SX1262 support --- README.md | 37 ++- components/wmbus_meter/wmbus_meter.cpp | 2 +- components/wmbus_radio/__init__.py | 175 +++--------- components/wmbus_radio/automation.h | 4 +- components/wmbus_radio/component.cpp | 99 +++---- components/wmbus_radio/component.h | 25 +- components/wmbus_radio/decode3of6.cpp | 23 +- components/wmbus_radio/decode3of6.h | 3 +- components/wmbus_radio/packet.cpp | 220 ++++----------- components/wmbus_radio/packet.h | 26 +- components/wmbus_radio/radio.py | 108 +++++++ components/wmbus_radio/transceiver.cpp | 39 +-- components/wmbus_radio/transceiver.h | 53 ++-- components/wmbus_radio/transceiver.py | 49 ++++ components/wmbus_radio/transceiver_sx1262.cpp | 266 ++++++++++++++++++ components/wmbus_radio/transceiver_sx1262.h | 66 +++++ components/wmbus_radio/transceiver_sx1262.py | 58 ++++ components/wmbus_radio/transceiver_sx1276.cpp | 156 +++++----- components/wmbus_radio/transceiver_sx1276.h | 17 +- components/wmbus_radio/transceiver_sx1276.py | 19 ++ 20 files changed, 908 insertions(+), 537 deletions(-) mode change 100755 => 100644 components/wmbus_radio/__init__.py mode change 100755 => 100644 components/wmbus_radio/component.cpp mode change 100755 => 100644 components/wmbus_radio/component.h mode change 100755 => 100644 components/wmbus_radio/decode3of6.cpp mode change 100755 => 100644 components/wmbus_radio/decode3of6.h create mode 100644 components/wmbus_radio/radio.py create mode 100644 components/wmbus_radio/transceiver.py create mode 100644 components/wmbus_radio/transceiver_sx1262.cpp create mode 100644 components/wmbus_radio/transceiver_sx1262.h create mode 100644 components/wmbus_radio/transceiver_sx1262.py create mode 100644 components/wmbus_radio/transceiver_sx1276.py diff --git a/README.md b/README.md index 8d3f3112..3032c8e4 100644 --- a/README.md +++ b/README.md @@ -168,30 +168,53 @@ When you use `sensor` and `text_sensor` platforms, required fields will be autom For both `sensor` and `text_sensor`, all config from generic [Sensor](https://esphome.io/components/sensor/index.html) and [Text Sensor](https://esphome.io/components/text_sensor/index.html) components is available, so you can use filters, icons, etc. ## `wmbus_radio` -This component provides a radio interface LoRa 868MHz transceivers allowing to receive wM-Bus packets. At the moment it support only Semtech SX1276 (RFM95/RFM96). -As a dependency, it requires `spi` component to be configured in ESPHome. +This component provides a radio interface for 868 MHz LoRa transceivers, allowing it to receive wM-Bus packets. It supports Semtech SX1276 (RFM95/RFM96) and SX1262 modules. +It needs ESPHome 2026.8.0 or newer and an `spi` component configured. -**Example configuration:** +**SX1276 example:** ```yaml spi: - clk_pin: GPIO1 + clk_pin: GPIO0 + mosi_pin: GPIO1 miso_pin: GPIO2 - mosi_pin: GPIO3 id: spi_bus wmbus_radio: id: radio_component radio_type: SX1276 + cs_pin: GPIO3 reset_pin: GPIO4 - irq_pin: GPIO5 + dio1_pin: GPIO5 on_frame: - wmbus_radio.send_frame_with_socket: id: transmitter format: rtlwmbus ``` -For SX1276, `reset_pin` should be connected to the reset pin and `irq_pin` should be connected to the DIO1 pin of the radio module. +`reset_pin` and `dio1_pin` go to the RESET and DIO1 pins of the radio module. `irq_pin` is the old name of `dio1_pin`; it still works with a deprecation warning and will be removed in 2027.3.0. + +**SX1262 example:** + +```yaml +spi: + clk_pin: GPIO0 + mosi_pin: GPIO1 + miso_pin: GPIO2 + +wmbus_radio: + id: radio_component + radio_type: SX1262 + cs_pin: GPIO3 + reset_pin: GPIO4 + busy_pin: GPIO5 + dio1_pin: GPIO6 + rf_switch: true + tcxo_voltage: 1.8V + use_dcdc: true +``` + +SX1262 also needs `busy_pin` wired to its BUSY pin. `tcxo_voltage` is optional and accepts the supported SX1262 voltages: `1.6V`, `1.7V`, `1.8V`, `2.2V`, `2.4V`, `2.7V`, `3.0V`, or `3.3V`. It defaults to `0V`, for modules where DIO3 does not control a TCXO. `rf_switch` defaults to `false`; enable it only when DIO2 drives the RF switch. `use_dcdc` defaults to `true`. `rx_boost` (default `false`) switches the receiver to boosted gain, trading a little more current for better sensitivity. The `on_frame` trigger can be used to send received wM-Bus packets to a remote server using `socket_transmitter` component. It can also be used to process packets in any other way, such as sending them to MQTT broker or HTTP server. diff --git a/components/wmbus_meter/wmbus_meter.cpp b/components/wmbus_meter/wmbus_meter.cpp index e0a81d23..3da341b5 100644 --- a/components/wmbus_meter/wmbus_meter.cpp +++ b/components/wmbus_meter/wmbus_meter.cpp @@ -30,7 +30,7 @@ bool Meter::is_encrypted() const { return this->meter && this->meter->meterKeys( void Meter::set_radio(wmbus_radio::Radio *radio) { this->radio = radio; - radio->add_frame_handler([this](wmbus_radio::Frame *frame) { return this->handle_frame(frame); }); + radio->on_frame([this](wmbus_radio::Frame *frame) { return this->handle_frame(frame); }); } void Meter::dump_config() { diff --git a/components/wmbus_radio/__init__.py b/components/wmbus_radio/__init__.py old mode 100755 new mode 100644 index 79f5c002..3ce48c26 --- a/components/wmbus_radio/__init__.py +++ b/components/wmbus_radio/__init__.py @@ -1,159 +1,50 @@ -from contextlib import suppress +from pathlib import Path + import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins, automation -from esphome.components import spi -from esphome.cpp_generator import LambdaExpression -from esphome.const import ( - CONF_ID, - CONF_RESET_PIN, - CONF_IRQ_PIN, - CONF_TRIGGER_ID, - CONF_FORMAT, - CONF_DATA, -) -from pathlib import Path +from esphome.core import CORE -CODEOWNERS = ["@kubasaw"] +from . import transceiver_sx1262, transceiver_sx1276 +from .radio import SCHEMA as RADIO_SCHEMA +from .radio import RadioComponent, register_radio +from .transceiver import CONF_RADIO_TYPE -DEPENDENCIES = ["esp32", "spi"] +__all__ = ["RadioComponent"] +CODEOWNERS = ["@kubasaw"] +DEPENDENCIES = ["esp32", "spi"] AUTO_LOAD = ["wmbus_common"] - MULTI_CONF = True -CONF_RADIO_ID = "radio_id" -CONF_ON_PACKET = "on_packet" -CONF_ON_FRAME = "on_frame" -CONF_RADIO_TYPE = "radio_type" -CONF_MARK_AS_HANDLED = "mark_as_handled" -CONF_PACKET_TRIGGER_ID = "packet_trigger_id" - -radio_ns = cg.esphome_ns.namespace("wmbus_radio") -RadioComponent = radio_ns.class_("Radio", cg.Component) -RadioTransceiver = radio_ns.class_("RadioTransceiver", spi.SPIDevice, cg.Component) -Frame = radio_ns.class_("Frame") -FrameOutputFormat = Frame.enum("OutputFormat") -FramePtr = Frame.operator("ptr") -FrameTrigger = radio_ns.class_("FrameTrigger", automation.Trigger.template(FramePtr)) -Packet = radio_ns.class_("Packet") -PacketPtr = Packet.operator("ptr") -PacketTrigger = radio_ns.class_("PacketTrigger", automation.Trigger.template(PacketPtr)) - -TRANSCEIVER_NAMES = { - r.stem.removeprefix("transceiver_").upper() - for r in Path(__file__).parent.glob("transceiver_*.cpp") - if r.is_file() +TRANSCEIVERS = { + module.__name__.split(".")[-1].removeprefix("transceiver_").upper(): module + for module in (transceiver_sx1262, transceiver_sx1276) } -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(RadioComponent), - cv.GenerateID(CONF_RADIO_ID): cv.declare_id(RadioTransceiver), - cv.Required(CONF_RADIO_TYPE): cv.one_of(*TRANSCEIVER_NAMES, upper=True), - cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_ON_FRAME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FrameTrigger), - cv.Optional(CONF_MARK_AS_HANDLED, default=False): cv.boolean, - } - ), - cv.Optional(CONF_ON_PACKET): automation.validate_automation( - { - cv.GenerateID(CONF_PACKET_TRIGGER_ID): cv.declare_id(PacketTrigger), - } - ), - } - ) - .extend(spi.spi_device_schema()) - .extend(cv.COMPONENT_SCHEMA) -) - -async def to_code(config): - cg.add(cg.LineComment("WMBus RadioTransceiver")) - - config[CONF_RADIO_ID].type = radio_ns.class_( - config[CONF_RADIO_TYPE], RadioTransceiver - ) - radio_var = cg.new_Pvariable(config[CONF_RADIO_ID]) - - reset_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) - cg.add(radio_var.set_reset_pin(reset_pin)) - - irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) - cg.add(radio_var.set_irq_pin(irq_pin)) - - await spi.register_spi_device(radio_var, config) - await cg.register_component(radio_var, config) - - cg.add(cg.LineComment("WMBus Component")) - var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_radio(radio_var)) - - await cg.register_component(var, config) - - for conf in config.get(CONF_ON_FRAME, []): - trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, conf[CONF_MARK_AS_HANDLED]) - await automation.build_automation( - trig, - [(FramePtr, "frame")], - conf, - ) - - for conf in config.get(CONF_ON_PACKET, []): - trig = cg.new_Pvariable(conf[CONF_PACKET_TRIGGER_ID], var) - await automation.build_automation( - trig, - [(PacketPtr, "packet")], - conf, - ) - - -with suppress(ImportError): - from esphome.components.socket_transmitter import ( - SOCKET_SEND_ACTION_SCHEMA, - SocketTransmitterSendAction, - ) +CONFIG_SCHEMA = cv.typed_schema( + { + name: module.config_schema(name, RADIO_SCHEMA) + for name, module in TRANSCEIVERS.items() + }, + key=CONF_RADIO_TYPE, + upper=True, +) - FRAME_SOCKET_SEND_SCHEMA = SOCKET_SEND_ACTION_SCHEMA.extend( - { - cv.Required(CONF_FORMAT): cv.one_of( - "hex", - "raw", - "rtlwmbus", - lower=True, - ), - cv.Optional(CONF_DATA): cv.invalid( - "If you want to specify data to be sent, use generic 'socket_transmitter.send' action" - ), - } - ) - @automation.register_action( - "wmbus_radio.send_frame_with_socket", - SocketTransmitterSendAction, - FRAME_SOCKET_SEND_SCHEMA, - synchronous=True, - ) - async def send_frame_with_socket_to_code(config, action_id, template_arg, args): - output_type = { - "hex": cg.std_string, - "raw": cg.std_vector.template(cg.uint8), - "rtlwmbus": cg.std_string, - }[config[CONF_FORMAT]] +def FILTER_SOURCE_FILES(): + used = {config[CONF_RADIO_TYPE] for config in CORE.config["wmbus_radio"]} + return [ + Path(module.__file__).with_suffix(suffix).name + for name, module in TRANSCEIVERS.items() + if name not in used + for suffix in (".cpp", ".h") + ] - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable( - action_id, cg.TemplateArguments(output_type, *template_arg), paren - ) - template_ = LambdaExpression( - f"return frame->as_{config[CONF_FORMAT]}();", args, "" - ) - cg.add(var.set_data(template_)) +async def to_code(config): + trx_var = await TRANSCEIVERS[config[CONF_RADIO_TYPE]].to_code(config) + radio_var = await register_radio(config) - return var + cg.add(radio_var.set_transceiver(trx_var)) diff --git a/components/wmbus_radio/automation.h b/components/wmbus_radio/automation.h index 16232e6d..45ebaa3d 100644 --- a/components/wmbus_radio/automation.h +++ b/components/wmbus_radio/automation.h @@ -9,7 +9,7 @@ namespace wmbus_radio { class FrameTrigger : public Trigger { public: explicit FrameTrigger(wmbus_radio::Radio *radio, bool mark_handled) { - radio->add_frame_handler([this, mark_handled](Frame *frame) { + radio->on_frame([this, mark_handled](Frame *frame) { this->trigger(frame); if (mark_handled) frame->mark_as_handled(); @@ -25,4 +25,4 @@ class PacketTrigger : public Trigger { }; } // namespace wmbus_radio -} // namespace esphome \ No newline at end of file +} // namespace esphome diff --git a/components/wmbus_radio/component.cpp b/components/wmbus_radio/component.cpp old mode 100755 new mode 100644 index 223527cb..501f6109 --- a/components/wmbus_radio/component.cpp +++ b/components/wmbus_radio/component.cpp @@ -1,45 +1,40 @@ #include "component.h" #include "freertos/task.h" -#include "freertos/queue.h" #include "esphome/core/helpers.h" - -#define ASSERT(expr, expected, before_exit) \ - { \ - auto result = (expr); \ - if (!!result != expected) { \ - ESP_LOGE(TAG, "Assertion failed: %s -> %d", #expr, result); \ - before_exit; \ - return; \ - } \ - } - -#define ASSERT_SETUP(expr) ASSERT(expr, 1, this->mark_failed()) +#include "esphome/core/log.h" namespace esphome { namespace wmbus_radio { static const char *TAG = "wmbus"; void Radio::setup() { - ASSERT_SETUP((this->packet_queue_ = xQueueCreate(3, sizeof(Packet *))) != nullptr); - - ASSERT_SETUP(xTaskCreate((TaskFunction_t) this->receiver_task, "radio_recv", 3 * 1024, this, 2, - &(this->receiver_task_handle_))); + if (this->transceiver_->is_failed()) { + ESP_LOGE(TAG, "Transceiver failed, not starting the receiver"); + this->mark_failed(); + return; + } - ESP_LOGI(TAG, "Receiver task created [%p]", this->receiver_task_handle_); + if (xTaskCreate((TaskFunction_t) this->receiver_task, "radio_recv", 3 * 1024, this, 2, nullptr) != pdPASS) { + ESP_LOGE(TAG, "Failed to create receiver task"); + this->mark_failed(); + return; + } - this->radio->attach_data_interrupt(Radio::wakeup_receiver_task_from_isr, &(this->receiver_task_handle_)); + ESP_LOGI(TAG, "Receiver task created"); } void Radio::loop() { - Packet *p; - if (xQueueReceive(this->packet_queue_, &p, 0) != pdPASS) + std::unique_ptr packet{this->packet_queue_.pop()}; + if (!packet) { + this->disable_loop(); return; + } - this->on_packet_callback_manager(p); + this->on_packet_callback_manager_(packet.get()); - auto frame = p->convert_to_frame(); + auto frame = packet->convert_to_frame(); if (!frame) return; @@ -47,75 +42,49 @@ void Radio::loop() { ESP_LOGI(TAG, "Frame created (%zu bytes) [RSSI: %d, mode:%s%s]", frame->data().size(), frame->rssi(), toString(frame->link_mode()), toString(frame->block_type())); - uint8_t packet_handled = 0; - for (auto &handler : this->frame_handlers_) - handler(&frame.value()); + this->on_frame_callback_manager_(&frame.value()); ESP_LOGI(TAG, "Telegram handled by %d handlers", frame->frame_handlers_count()); } -void IRAM_ATTR Radio::wakeup_receiver_task_from_isr(TaskHandle_t *arg) { - BaseType_t xHigherPriorityTaskWoken; - vTaskNotifyGiveFromISR(*arg, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); -} - void Radio::receive_frame() { - this->radio->restart_rx(); + this->transceiver_->reset_receiver(); auto packet = std::make_unique(); - if (!ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(60000))) { - ESP_LOGD(TAG, "Radio interrupt timeout"); + if (!this->transceiver_->read(packet->head(), pdMS_TO_TICKS(60000))) { + ESP_LOGV(TAG, "Failed to read packet head"); return; } - if (!this->radio->read(packet->rx_data_ptr(), packet->rx_capacity())) { - ESP_LOGV(TAG, "Failed to read preamble"); + const auto rest = packet->rest(); + if (rest.empty()) { + ESP_LOGV(TAG, "Received invalid packet head: [%s]", format_hex_pretty(packet->get_raw_data()).c_str()); return; } - if (!packet->validate_preamble()) { - ESP_LOGV(TAG, "Received invalid preamble: [%s]", format_hex_pretty(packet->get_raw_data()).c_str()); - return; - } + packet->set_rssi(this->transceiver_->get_rssi()); - if (!packet->calculate_payload_size()) { - ESP_LOGV(TAG, "Cannot calculate payload size"); - return; - } - - packet->set_rssi(this->radio->get_rssi()); - - if (!this->radio->read(packet->rx_data_ptr(), packet->rx_capacity())) { + if (!this->transceiver_->read(rest, Transceiver::SINGLE_BYTE_TIMEOUT)) { ESP_LOGW(TAG, "Failed to read data"); return; } - auto packet_ptr = packet.get(); - - if (xQueueSend(this->packet_queue_, &packet_ptr, 0) == pdTRUE) { - ESP_LOGV(TAG, "Queue items: %zu", uxQueueMessagesWaiting(this->packet_queue_)); - ESP_LOGV(TAG, "Queue send success"); + if (this->packet_queue_.push(packet.get())) { packet.release(); - } else + this->enable_loop_soon_any_context(); + ESP_LOGV(TAG, "Queue items: %zu", this->packet_queue_.size()); + ESP_LOGV(TAG, "Queue send success"); + } else { ESP_LOGW(TAG, "Queue send failed"); + } } void Radio::receiver_task(Radio *arg) { - ESP_LOGE(TAG, "Hello from radio task!"); - int counter = 0; + arg->transceiver_->start_receiver(xTaskGetCurrentTaskHandle()); while (true) arg->receive_frame(); } -void Radio::add_frame_handler(std::function &&callback) { - this->frame_handlers_.push_back(std::move(callback)); -} - -void Radio::on_packet(std::function &&callback) { - this->on_packet_callback_manager.add(std::move(callback)); -} - } // namespace wmbus_radio } // namespace esphome diff --git a/components/wmbus_radio/component.h b/components/wmbus_radio/component.h old mode 100755 new mode 100644 index 53e5f882..495a8e2f --- a/components/wmbus_radio/component.h +++ b/components/wmbus_radio/component.h @@ -2,10 +2,9 @@ #include -#include "freertos/FreeRTOS.h" - #include "esphome/core/component.h" #include "esphome/core/gpio.h" +#include "esphome/core/lock_free_queue.h" #include "esphome/components/spi/spi.h" @@ -17,26 +16,24 @@ namespace wmbus_radio { class Radio : public Component { public: - void set_radio(RadioTransceiver *radio) { this->radio = radio; }; - void setup() override; void loop() override; void receive_frame(); - void add_frame_handler(std::function &&callback); - void on_packet(std::function &&callback); + void set_transceiver(Transceiver *transceiver) { this->transceiver_ = transceiver; } + void on_frame(std::function &&callback) { this->on_frame_callback_manager_.add(std::move(callback)); } + void on_packet(std::function &&callback) { + this->on_packet_callback_manager_.add(std::move(callback)); + } protected: - static void wakeup_receiver_task_from_isr(TaskHandle_t *arg); static void receiver_task(Radio *arg); - RadioTransceiver *radio{nullptr}; - TaskHandle_t receiver_task_handle_{nullptr}; - QueueHandle_t packet_queue_{nullptr}; - - std::vector> frame_handlers_; + Transceiver *transceiver_{nullptr}; + LockFreeQueue packet_queue_; - CallbackManager on_packet_callback_manager; + CallbackManager on_frame_callback_manager_; + LazyCallbackManager on_packet_callback_manager_; }; } // namespace wmbus_radio -} // namespace esphome \ No newline at end of file +} // namespace esphome diff --git a/components/wmbus_radio/decode3of6.cpp b/components/wmbus_radio/decode3of6.cpp old mode 100755 new mode 100644 index 081b345e..a6eebee6 --- a/components/wmbus_radio/decode3of6.cpp +++ b/components/wmbus_radio/decode3of6.cpp @@ -1,6 +1,7 @@ #include "decode3of6.h" -#include +#include +#include #include "esphome/core/log.h" #include "esphome/core/helpers.h" @@ -8,13 +9,12 @@ namespace esphome { namespace wmbus_radio { static const char *TAG = "3of6"; -std::optional> decode3of6(std::vector &coded_data) { - static const std::map lookupTable = { - {0b010110, 0x0}, {0b001101, 0x1}, {0b001110, 0x2}, {0b001011, 0x3}, {0b011100, 0x4}, {0b011001, 0x5}, - {0b011010, 0x6}, {0b010011, 0x7}, {0b101100, 0x8}, {0b100101, 0x9}, {0b100110, 0xA}, {0b100011, 0xB}, - {0b110100, 0xC}, {0b110001, 0xD}, {0b110010, 0xE}, {0b101001, 0xF}, - }; +static constexpr std::array CODES = { + 0b010110, 0b001101, 0b001110, 0b001011, 0b011100, 0b011001, 0b011010, 0b010011, + 0b101100, 0b100101, 0b100110, 0b100011, 0b110100, 0b110001, 0b110010, 0b101001, +}; +std::optional> decode3of6(std::span coded_data) { // ESP_LOGD(TAG, "Decoding 3of6 data: %s", format_hex(coded_data).c_str()); std::vector decodedBytes; @@ -31,16 +31,17 @@ std::optional> decode3of6(std::vector &coded_data) code |= (data[byte_idx + 1] >> (8 - bit_offset)); code >>= 2; - auto it = lookupTable.find(code); - if (it == lookupTable.end()) { + auto it = std::ranges::find(CODES, code); + if (it == CODES.end()) { // ESP_LOGW(TAG, "Invalid code: 0x%02X", code); return {}; } + uint8_t nibble = it - CODES.begin(); if (i % 2 == 0) - decodedBytes.push_back(it->second << 4); + decodedBytes.push_back(nibble << 4); else - decodedBytes.back() |= it->second; + decodedBytes.back() |= nibble; } // ESP_LOGV(TAG, "Successfully decoded %zu bytes", decodedBytes.size()); diff --git a/components/wmbus_radio/decode3of6.h b/components/wmbus_radio/decode3of6.h old mode 100755 new mode 100644 index 124e53e7..c06534d5 --- a/components/wmbus_radio/decode3of6.h +++ b/components/wmbus_radio/decode3of6.h @@ -3,10 +3,11 @@ #include #include #include +#include namespace esphome { namespace wmbus_radio { -std::optional> decode3of6(std::vector &coded_data); +std::optional> decode3of6(std::span coded_data); size_t encoded_size(size_t decoded_size); } // namespace wmbus_radio } // namespace esphome \ No newline at end of file diff --git a/components/wmbus_radio/packet.cpp b/components/wmbus_radio/packet.cpp index 357ea81e..891adef5 100644 --- a/components/wmbus_radio/packet.cpp +++ b/components/wmbus_radio/packet.cpp @@ -3,12 +3,13 @@ #include #include "esphome/core/helpers.h" -#include "esphome/components/wmbus_common/meters.h" +#include "esphome/components/wmbus_common/wmbus.h" #include "decode3of6.h" +#include "esphome/core/log.h" // 3 bytes for mode C marks + len or first 3 bytes of mode T to decode into 2 bytes -#define WMBUS_FRAME_PRELOAD_SIZE (3) +#define WMBUS_PACKET_HEAD_SIZE (3) // Mode C frame starts with \x54\xCD or \x54\x3D #define WMBUS_MODE_C_MARK (0x54) @@ -23,197 +24,96 @@ static const char *TAG = "wmbus"; const char *toString(BlockType type) { switch (type) { case BlockType::A: - return "a"; + return "A"; case BlockType::B: - return "b"; + return "B"; default: return ""; } } -Packet::Packet() { this->data_.reserve(WMBUS_FRAME_PRELOAD_SIZE); } +Packet::Packet() : data_(WMBUS_PACKET_HEAD_SIZE) {} -// Determine the link mode based on the first byte of the data -LinkMode Packet::link_mode() { - if (this->link_mode_ == LinkMode::UNKNOWN) - if (this->data_.size()) { - if (this->data_[0] == WMBUS_MODE_C_MARK) - this->link_mode_ = LinkMode::C1; - else - this->link_mode_ = LinkMode::T1; - } - - return this->link_mode_; -} - -// Determine block type for C1 mode based on second byte of data -BlockType Packet::block_type() { - if (this->block_type_ == BlockType::UNKNOWN) { - if (this->link_mode() == LinkMode::C1) { - switch (this->data_[1]) { - case WMBUS_MODE_C_BLOCK_A_MARK: - this->block_type_ = BlockType::A; - break; - case WMBUS_MODE_C_BLOCK_B_MARK: - this->block_type_ = BlockType::B; - break; - default: - break; - } - } - } +std::span Packet::head() { return std::span(this->data_).first(WMBUS_PACKET_HEAD_SIZE); } - return this->block_type_; -} +std::span Packet::rest() { + const auto head = this->head(); -void Packet::set_rssi(int8_t rssi) { this->rssi_ = rssi; } - -// Get value of L-field -uint8_t Packet::l_field() { - switch (this->link_mode()) { - case LinkMode::C1: - return this->data_[0]; - case LinkMode::T1: { - auto decoded = decode3of6(this->data_); - if (decoded) - return (*decoded)[0]; - } - default: - break; - } - return 0; -} - -size_t Packet::expected_size() { - if (!this->expected_size_) { - // Format A - // L-field = length without CRC fields and without L (1 byte) - // Format B - // L-field = length with CRC fields and without L (1 byte) - - auto l_field = this->l_field(); - - if (l_field == 0) { - return 0; - } - - // The 2 first blocks contains 25 bytes when excluding CRC and the L-field - // The other blocks contains 16 bytes when excluding the CRC-fields - // Less than 26 (15 + 10) - auto nrBlocks = l_field < 26 ? 2 : (l_field - 26) / 16 + 3; - - // Add all extra fields, excluding the CRC fields + 2 CRC bytes for each block - auto nrBytes = l_field + 1 + 2 * nrBlocks; - - switch (this->link_mode()) { - case LinkMode::C1: - switch (this->block_type()) { - case BlockType::A: - this->expected_size_ = nrBytes; - break; - case BlockType::B: - this->expected_size_ = 1 + l_field; - break; - default: - break; - } + uint8_t l_field; + if (head[0] == WMBUS_MODE_C_MARK) { + this->link_mode_ = LinkMode::C1; + // C1 frame must have valid block type: A or B + switch (head[1]) { + case WMBUS_MODE_C_BLOCK_A_MARK: + this->block_type_ = BlockType::A; break; - case LinkMode::T1: - this->expected_size_ = encoded_size(nrBytes); + case WMBUS_MODE_C_BLOCK_B_MARK: + this->block_type_ = BlockType::B; break; default: - break; + return {}; } + l_field = head[WMBUS_MODE_C_MARKS_LEN]; + } else { + // T1 frame has no block type + this->link_mode_ = LinkMode::T1; + const auto decoded = decode3of6(head); + if (!decoded) + return {}; + l_field = (*decoded)[0]; } - return this->expected_size_; -} - -size_t Packet::rx_capacity() { - // TODO: Remove side effects? - auto cap = this->data_.capacity() - this->data_.size(); - this->data_.resize(this->data_.capacity()); - return cap; -} -uint8_t *Packet::rx_data_ptr() { return this->data_.data() + this->data_.size(); } - -bool Packet::calculate_payload_size() { - auto total_length = this->expected_size(); - this->data_.reserve(total_length); - return total_length; -} + if (l_field == 0) + return {}; -bool Packet::validate_preamble() { - bool is_preamble_valid = false; - - switch (this->link_mode()) { - case LinkMode::C1: - // C1 frame must have valid block type: A or B - if (this->block_type() == BlockType::A || this->block_type() == BlockType::B) { - is_preamble_valid = true; - } - break; - case LinkMode::T1: - // T1 frame has no block type - is_preamble_valid = true; - break; - default: - break; - } - - if (is_preamble_valid) { - this->trim_preamble(); - } + // Format A + // L-field = length without CRC fields and without L (1 byte) + // Format B + // L-field = length with CRC fields and without L (1 byte) + // The 2 first blocks contains 25 bytes when excluding CRC and the L-field + // The other blocks contains 16 bytes when excluding the CRC-fields + const size_t blocks = l_field < 26 ? 2 : (l_field - 26) / 16 + 3; + const size_t frame_size = this->block_type_ == BlockType::B ? 1 + l_field : 1 + l_field + 2 * blocks; - return is_preamble_valid; + this->data_.resize(this->link_mode_ == LinkMode::C1 ? WMBUS_MODE_C_MARKS_LEN + frame_size : encoded_size(frame_size)); + return std::span(this->data_).subspan(WMBUS_PACKET_HEAD_SIZE); } -void Packet::trim_preamble() { - switch (this->link_mode()) { - case LinkMode::C1: - this->data_.erase(this->data_.begin(), this->data_.begin() + 2); - break; - default: - break; - } -} +void Packet::set_rssi(int8_t rssi) { this->rssi_ = rssi; } std::optional Packet::convert_to_frame() { - ESP_LOGD(TAG, "Try to make frame from packet %s%s of size %u", toString(this->link_mode()), - toString(this->block_type()), this->expected_size()); - - std::optional frame = {}; - - if (this->link_mode() == LinkMode::T1 && this->expected_size() == this->data_.size()) { - auto decoded_data = decode3of6(this->data_); - if (decoded_data) - this->data_ = decoded_data.value(); + ESP_LOGD(TAG, "Try to make frame from packet %s%s of size %zu", linkModeName(this->link_mode_).c_str(), + toString(this->block_type_), this->data_.size()); + + std::vector data; + if (this->link_mode_ == LinkMode::C1) { + const auto frame = std::span(this->data_).subspan(WMBUS_MODE_C_MARKS_LEN); + data.assign(frame.begin(), frame.end()); + } else { + auto decoded = decode3of6(this->data_); + if (!decoded) + return {}; + data = std::move(*decoded); } - removeAnyDLLCRCs(this->data_); + removeAnyDLLCRCs(data); int dummy; - if (checkWMBusFrame(this->data_, (size_t *) &dummy, &dummy, &dummy, false) == FrameStatus::FullFrame) - frame.emplace(this); - - delete this; + if (checkWMBusFrame(data, (size_t *) &dummy, &dummy, &dummy, false) != FrameStatus::FullFrame) { + ESP_LOGD(TAG, "Dropped a packet of %zu bytes: bad CRC or not a wM-Bus frame", data.size()); + return {}; + } - return frame; + return Frame(std::move(data), this->link_mode_, this->block_type_, this->rssi_); } const std::vector &Packet::get_raw_data() const { return data_; } -Frame::Frame(Packet *packet) - : data_(std::move(packet->data_)), - link_mode_(packet->link_mode_), - block_type_(packet->block_type_), - rssi_(packet->rssi_) {} - std::vector &Frame::data() { return this->data_; } LinkMode Frame::link_mode() { return this->link_mode_; } BlockType Frame::block_type() { return this->block_type_; } int8_t Frame::rssi() { return this->rssi_; } -std::vector Frame::as_raw() { return this->data_; } +std::span Frame::as_raw() const { return this->data_; } std::string Frame::as_hex() { return format_hex(this->data_); } std::string Frame::as_rtlwmbus() { const size_t time_repr_size = sizeof("YYYY-MM-DD HH:MM:SS.00Z"); @@ -245,4 +145,4 @@ void Frame::mark_as_handled() { this->frame_handlers_count_++; } uint8_t Frame::frame_handlers_count() { return this->frame_handlers_count_; } } // namespace wmbus_radio -} // namespace esphome \ No newline at end of file +} // namespace esphome diff --git a/components/wmbus_radio/packet.h b/components/wmbus_radio/packet.h index 920d4fee..74ad8abf 100644 --- a/components/wmbus_radio/packet.h +++ b/components/wmbus_radio/packet.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -12,48 +13,33 @@ namespace esphome { namespace wmbus_radio { -enum class BlockType { UNKNOWN = 0, A = 1, B = 2 }; +enum class BlockType { UNKNOWN = 0, A, B }; const char *toString(BlockType type); struct Frame; struct Packet { - friend class Frame; - public: Packet(); - uint8_t *rx_data_ptr(); - size_t rx_capacity(); - bool calculate_payload_size(); + std::span head(); + // Empty when the head does not start a valid packet + std::span rest(); void set_rssi(int8_t rssi); - bool validate_preamble(); std::optional convert_to_frame(); const std::vector &get_raw_data() const; protected: std::vector data_; - - size_t expected_size(); - size_t expected_size_ = 0; - - LinkMode link_mode(); LinkMode link_mode_ = LinkMode::UNKNOWN; - - BlockType block_type(); BlockType block_type_ = BlockType::UNKNOWN; - - void trim_preamble(); - uint8_t l_field(); - int8_t rssi_; }; struct Frame { public: - Frame(Packet *packet); Frame(std::vector data, LinkMode lm, BlockType bt, int8_t rssi) : data_(std::move(data)), link_mode_(lm), block_type_(bt), rssi_(rssi){}; @@ -62,7 +48,7 @@ struct Frame { BlockType block_type(); int8_t rssi(); - std::vector as_raw(); + std::span as_raw() const; std::string as_hex(); std::string as_rtlwmbus(); std::string meter_id(); diff --git a/components/wmbus_radio/radio.py b/components/wmbus_radio/radio.py new file mode 100644 index 00000000..adc788a3 --- /dev/null +++ b/components/wmbus_radio/radio.py @@ -0,0 +1,108 @@ +from contextlib import suppress + +from esphome import automation +from esphome import codegen as cg +from esphome import config_validation as cv +from esphome.const import CONF_DATA, CONF_FORMAT, CONF_ID, CONF_TRIGGER_ID +from esphome.cpp_generator import LambdaExpression + +radio_ns = cg.esphome_ns.namespace("wmbus_radio") +RadioComponent = radio_ns.class_("Radio", cg.Component) + +CONF_MARK_AS_HANDLED = "mark_as_handled" +CONF_ON_FRAME = "on_frame" +CONF_ON_PACKET = "on_packet" +CONF_PACKET_TRIGGER_ID = "packet_trigger_id" + +FramePtr = radio_ns.class_("Frame").operator("ptr") +FrameTrigger = radio_ns.class_("FrameTrigger", automation.Trigger.template(FramePtr)) +PacketPtr = radio_ns.class_("Packet").operator("ptr") +PacketTrigger = radio_ns.class_("PacketTrigger", automation.Trigger.template(PacketPtr)) + + +SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RadioComponent), + cv.Optional(CONF_ON_FRAME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FrameTrigger), + cv.Optional(CONF_MARK_AS_HANDLED, default=False): cv.boolean, + } + ), + cv.Optional(CONF_ON_PACKET): automation.validate_automation( + { + cv.GenerateID(CONF_PACKET_TRIGGER_ID): cv.declare_id(PacketTrigger), + } + ), + } +) + + +async def register_radio(config): + cg.add(cg.LineComment("wM-Bus Component")) + + var = await cg.register_component(cg.new_Pvariable(config[CONF_ID]), config) + + for conf in config.get(CONF_ON_FRAME, []): + trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, conf[CONF_MARK_AS_HANDLED]) + await automation.build_automation( + trig, + [(FramePtr, "frame")], + conf, + ) + + for conf in config.get(CONF_ON_PACKET, []): + trig = cg.new_Pvariable(conf[CONF_PACKET_TRIGGER_ID], var) + await automation.build_automation( + trig, + [(PacketPtr, "packet")], + conf, + ) + + return var + + +with suppress(ImportError): + from esphome.components.socket_transmitter import ( + SOCKET_SEND_ACTION_SCHEMA, + SocketTransmitterSendAction, + ) + + OUTPUT_FORMATS = { + "hex": cg.std_string, + "raw": cg.std_span.template(cg.uint8.operator("const")), + "rtlwmbus": cg.std_string, + } + + FRAME_SOCKET_SEND_SCHEMA = SOCKET_SEND_ACTION_SCHEMA.extend( + { + cv.Required(CONF_FORMAT): cv.one_of( + *OUTPUT_FORMATS.keys(), + lower=True, + ), + cv.Optional(CONF_DATA): cv.invalid( + "If you want to specify data to be sent, use generic 'socket_transmitter.send' action" + ), + } + ) + + @automation.register_action( + "wmbus_radio.send_frame_with_socket", + SocketTransmitterSendAction, + FRAME_SOCKET_SEND_SCHEMA, + synchronous=True, + ) + async def send_frame_with_socket_to_code(config, action_id, template_arg, args): + output_type = OUTPUT_FORMATS[config[CONF_FORMAT]] + + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable( + action_id, cg.TemplateArguments(output_type, *template_arg), paren + ) + template_ = LambdaExpression( + f"return frame->as_{config[CONF_FORMAT]}();", args, "" + ) + + cg.add(var.set_data(template_)) + + return var diff --git a/components/wmbus_radio/transceiver.cpp b/components/wmbus_radio/transceiver.cpp index ba01b4dc..b697b2b0 100644 --- a/components/wmbus_radio/transceiver.cpp +++ b/components/wmbus_radio/transceiver.cpp @@ -2,50 +2,33 @@ #include "esphome/core/log.h" -#include "freertos/FreeRTOS.h" - namespace esphome { namespace wmbus_radio { + static const char *TAG = "wmbus.transceiver"; -void RadioTransceiver::set_reset_pin(InternalGPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } +void Transceiver::set_reset_pin(InternalGPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } -void RadioTransceiver::set_irq_pin(InternalGPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } +void Transceiver::set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } -void RadioTransceiver::reset() { +void Transceiver::reset() { this->reset_pin_->digital_write(0); delay(5); this->reset_pin_->digital_write(1); delay(5); } -void RadioTransceiver::common_setup() { - this->reset_pin_->setup(); - this->irq_pin_->setup(); - this->spi_setup(); +void IRAM_ATTR Transceiver::notify_interrupt(InterruptContext *context) { + BaseType_t higher_priority_task_woken = pdFALSE; + xTaskNotifyFromISR(context->task, context->value, eSetBits, &higher_priority_task_woken); + portYIELD_FROM_ISR(higher_priority_task_woken); } -uint8_t RadioTransceiver::spi_transaction(uint8_t operation, uint8_t address, std::initializer_list data) { - this->delegate_->begin_transaction(); - auto rval = this->delegate_->transfer(operation | address); - for (auto byte : data) - rval = this->delegate_->transfer(byte); - this->delegate_->end_transaction(); - return rval; -} - -uint8_t RadioTransceiver::spi_read(uint8_t address) { return this->spi_transaction(0x00, address, {0}); } - -void RadioTransceiver::spi_write(uint8_t address, std::initializer_list data) { - this->spi_transaction(0x80, address, data); -} - -void RadioTransceiver::spi_write(uint8_t address, uint8_t data) { this->spi_write(address, {data}); } - -void RadioTransceiver::dump_config() { +void Transceiver::dump_config() { ESP_LOGCONFIG(TAG, "Transceiver: %s", this->get_name()); LOG_PIN("Reset Pin: ", this->reset_pin_); - LOG_PIN("IRQ Pin: ", this->irq_pin_); + LOG_PIN("DIO1 Pin: ", this->dio1_pin_); } + } // namespace wmbus_radio } // namespace esphome diff --git a/components/wmbus_radio/transceiver.h b/components/wmbus_radio/transceiver.h index aad9c193..b756f9c0 100644 --- a/components/wmbus_radio/transceiver.h +++ b/components/wmbus_radio/transceiver.h @@ -1,44 +1,57 @@ #pragma once + #include -#include "esphome/core/optional.h" +#include + #include "esphome/components/spi/spi.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#define BYTE(x, n) ((uint8_t) (x >> (n * 8))) - namespace esphome { namespace wmbus_radio { -class RadioTransceiver : public Component, - public spi::SPIDevice { + +constexpr uint8_t byte(uint32_t val, uint8_t n) { return static_cast(val >> (n * 8)); } + +class Transceiver : public Component, + public spi::SPIDevice { public: + static constexpr TickType_t SINGLE_BYTE_TIMEOUT = pdMS_TO_TICKS(2); + virtual void setup() override = 0; void dump_config() override; - template void attach_data_interrupt(void (*callback)(T *), T *arg) { - this->irq_pin_->attach_interrupt(callback, arg, gpio::INTERRUPT_FALLING_EDGE); - } - virtual void restart_rx() = 0; + float get_setup_priority() const override { return setup_priority::HARDWARE - 10.0f; } + + // Called from the receiver task before its first reset_receiver(). + virtual void start_receiver(TaskHandle_t receiver_task) = 0; + virtual void reset_receiver() = 0; + // Fills the whole buffer; false when the first byte does not come within first_byte_timeout + // or a later one within SINGLE_BYTE_TIMEOUT. + virtual bool read(std::span buffer, TickType_t first_byte_timeout) = 0; virtual int8_t get_rssi() = 0; virtual const char *get_name() = 0; - virtual bool read(uint8_t *buffer, size_t length) = 0; - - void set_spi(spi::SPIDelegate *spi); void set_reset_pin(InternalGPIOPin *reset_pin); - void set_irq_pin(InternalGPIOPin *irq_pin); + void set_dio1_pin(InternalGPIOPin *dio1_pin); protected: + struct InterruptContext { + TaskHandle_t task; + uint32_t value; + }; + InternalGPIOPin *reset_pin_; - InternalGPIOPin *irq_pin_; + InternalGPIOPin *dio1_pin_; void reset(); - void common_setup(); - uint8_t spi_transaction(uint8_t operation, uint8_t address, std::initializer_list data); - uint8_t spi_read(uint8_t address); - void spi_write(uint8_t address, std::initializer_list data); - void spi_write(uint8_t address, uint8_t data); + static void IRAM_ATTR notify_interrupt(InterruptContext *context); + + void spi_transaction(std::span data) { + this->enable(); + this->transfer_array(data.data(), data.size()); + this->disable(); + } }; } // namespace wmbus_radio diff --git a/components/wmbus_radio/transceiver.py b/components/wmbus_radio/transceiver.py new file mode 100644 index 00000000..11819cd2 --- /dev/null +++ b/components/wmbus_radio/transceiver.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi +from esphome.const import CONF_RESET_PIN + +from .radio import radio_ns + +CONF_RADIO_ID = "radio_id" +CONF_RADIO_TYPE = "radio_type" +CONF_DIO1_PIN = "dio1_pin" + + +def transceiver_schema(trx_name, schema): + return ( + cv.Schema( + { + cv.GenerateID(CONF_RADIO_ID): cv.declare_id( + radio_ns.class_( + trx_name, + radio_ns.class_("Transceiver"), + spi.SPIDevice, + cg.Component, + ) + ), + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, + } + ) + .extend(spi.spi_device_schema()) + .extend(cv.COMPONENT_SCHEMA) + .extend(schema) + ) + + +async def register_transceiver(config): + cg.add(cg.LineComment("wM-Bus Transceiver")) + + var = await cg.register_component(cg.new_Pvariable(config[CONF_RADIO_ID]), config) + + reset_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset_pin)) + + dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN]) + cg.add(var.set_dio1_pin(dio1_pin)) + + await spi.register_spi_device(var, config) + + return var diff --git a/components/wmbus_radio/transceiver_sx1262.cpp b/components/wmbus_radio/transceiver_sx1262.cpp new file mode 100644 index 00000000..17d2e1ca --- /dev/null +++ b/components/wmbus_radio/transceiver_sx1262.cpp @@ -0,0 +1,266 @@ +#include "transceiver_sx1262.h" + +#include +#include +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace wmbus_radio { + +static const char *TAG = "SX1262"; +static constexpr uint32_t F_XTAL = 32000000; + +enum class SX1262::Register : uint16_t { + VERSION_STRING = 0x0320, + RX_TX_PAYLOAD_LEN = 0x06BB, + SYNC_WORD_0 = 0x06C0, + SYNC_WORD_1 = 0x06C1, + RX_ADDR_POINTER = 0x0803, + RX_GAIN = 0x08AC, +}; + +enum class SX1262::Opcode : uint8_t { + CLEAR_IRQ_STATUS = 0x02, + CLEAR_DEVICE_ERRORS = 0x07, + SET_DIO_IRQ_PARAMS = 0x08, + WRITE_REGISTER = 0x0D, + GET_PACKET_STATUS = 0x14, + GET_DEVICE_ERRORS = 0x17, + READ_REGISTER = 0x1D, + READ_BUFFER = 0x1E, + SET_STANDBY = 0x80, + SET_RX = 0x82, + SET_RF_FREQUENCY = 0x86, + CALIBRATE = 0x89, + SET_PACKET_TYPE = 0x8A, + SET_MODULATION_PARAMS = 0x8B, + SET_PACKET_PARAMS = 0x8C, + SET_BUFFER_BASE_ADDRESS = 0x8F, + SET_REGULATOR_MODE = 0x96, + SET_DIO3_AS_TCXO_CTRL = 0x97, + CALIBRATE_IMAGE = 0x98, + SET_DIO2_AS_RF_SWITCH_CTRL = 0x9D, +}; + +void SX1262::setup() { + this->busy_pin_->setup(); + this->reset_pin_->setup(); + this->dio1_pin_->setup(); + this->busy_ready_interrupt_ = {xTaskGetCurrentTaskHandle(), static_cast(Interrupt::BUSY_READY)}; + this->busy_pin_->attach_interrupt(&Transceiver::notify_interrupt, &this->busy_ready_interrupt_, + gpio::INTERRUPT_FALLING_EDGE); + this->spi_setup(); + + if (!this->configure_chip()) { + this->busy_pin_->detach_interrupt(); + this->mark_failed(); + } +} + +bool SX1262::configure_chip() { + ESP_LOGV(TAG, "Setting up SX1262"); + this->reset(); + if (!this->wait_for_busy()) { + ESP_LOGE(TAG, "No response, BUSY stays high after reset"); + return false; + } + + std::array version; + this->read_registers(Register::VERSION_STRING, version); + if (!std::string_view(reinterpret_cast(version.data()), version.size()).starts_with("SX126")) { + char hex[format_hex_pretty_size(version.size())]; + ESP_LOGE(TAG, "Unexpected version string: %s", format_hex_pretty_to(hex, version)); + return false; + } + + ESP_LOGVV(TAG, "standby on RC oscillator"); + this->command(Opcode::SET_STANDBY, {0x00}); + + if (this->tcxo_voltage_ != TcxoVoltage::NONE) { + ESP_LOGVV(TAG, "power TCXO from DIO3"); + constexpr uint32_t startup_delay_us = 5000; + constexpr uint32_t startup_delay = startup_delay_us * 1000 / 15625; // 15.625 us steps + this->command(Opcode::SET_DIO3_AS_TCXO_CTRL, {static_cast(this->tcxo_voltage_), byte(startup_delay, 2), + byte(startup_delay, 1), byte(startup_delay, 0)}); + } + + ESP_LOGVV(TAG, "calibrate blocks and image for 863-870 MHz"); + this->command(Opcode::CALIBRATE, {0x7F}); + this->command(Opcode::CALIBRATE_IMAGE, {0xD7, 0xDB}); + + std::array errors; + this->command(Opcode::GET_DEVICE_ERRORS, {}, errors); + // Power-up raises XOSC_START_ERR when a TCXO is used. + const uint16_t device_errors = encode_uint16(errors[0], errors[1]) & ~(1 << 5); + if (device_errors != 0) { + ESP_LOGE(TAG, "Calibration failed (device errors 0x%04X), check tcxo_voltage", device_errors); + return false; + } + this->command(Opcode::CLEAR_DEVICE_ERRORS, {0x00, 0x00}); + + if (this->rf_switch_) { + ESP_LOGVV(TAG, "drive RF switch from DIO2"); + this->command(Opcode::SET_DIO2_AS_RF_SWITCH_CTRL, {0x01}); + } + + ESP_LOGVV(TAG, "set %s regulator", this->use_dcdc_ ? "DC-DC" : "LDO"); + this->command(Opcode::SET_REGULATOR_MODE, {this->use_dcdc_}); + + ESP_LOGVV(TAG, "set GFSK packet type"); + this->command(Opcode::SET_PACKET_TYPE, {0x00}); + + ESP_LOGVV(TAG, "set radio frequency"); + constexpr uint32_t frequency = 868950000; + constexpr uint32_t frf = (uint64_t{frequency} << 25) / F_XTAL; + this->command(Opcode::SET_RF_FREQUENCY, {byte(frf, 3), byte(frf, 2), byte(frf, 1), byte(frf, 0)}); + + ESP_LOGVV(TAG, "set bitrate, gaussian BT 0.5 shaping, 234.3 kHz bandwidth and frequency deviation"); + constexpr uint32_t bitrate = 100000; + constexpr uint32_t br = 32 * F_XTAL / bitrate; + constexpr uint32_t freq_dev = 50000; + constexpr uint32_t fdev = ((uint64_t{freq_dev} << 25) + F_XTAL / 2) / F_XTAL; + this->command(Opcode::SET_MODULATION_PARAMS, + {byte(br, 2), byte(br, 1), byte(br, 0), 0x09, 0x0A, byte(fdev, 2), byte(fdev, 1), byte(fdev, 0)}); + + ESP_LOGVV(TAG, "set 16-bit preamble and sync word, fixed 255-byte length, no CRC, no whitening"); + this->command(Opcode::SET_PACKET_PARAMS, {0x00, 0x10, 0x04, 0x10, 0x00, 0x00, 0xFF, 0x01, 0x00}); + this->command(Opcode::SET_BUFFER_BASE_ADDRESS, {0x00, 0x00}); + + ESP_LOGVV(TAG, "set sync word"); + this->write_register(Register::SYNC_WORD_0, 0x54); + this->write_register(Register::SYNC_WORD_1, 0x3D); + + ESP_LOGVV(TAG, "set %s RX gain", this->rx_boost_ ? "boosted" : "power saving"); + this->write_register(Register::RX_GAIN, this->rx_boost_ ? 0x96 : 0x94); + + ESP_LOGVV(TAG, "route SyncWordValid IRQ to DIO1"); + this->command(Opcode::SET_DIO_IRQ_PARAMS, {0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}); + + ESP_LOGI(TAG, "SX1262 setup done"); + return true; +} + +void SX1262::dump_config() { + Transceiver::dump_config(); + LOG_PIN("Busy Pin: ", this->busy_pin_); + if (this->tcxo_voltage_ != TcxoVoltage::NONE) { + ESP_LOGCONFIG(TAG, "TCXO voltage code: 0x%02X", static_cast(this->tcxo_voltage_)); + } + ESP_LOGCONFIG(TAG, "DIO2 as RF switch: %s", YESNO(this->rf_switch_)); + ESP_LOGCONFIG(TAG, "Regulator: %s", this->use_dcdc_ ? "DC-DC" : "LDO"); + ESP_LOGCONFIG(TAG, "RX gain: %s", this->rx_boost_ ? "boosted" : "power saving"); +} + +void SX1262::start_receiver(TaskHandle_t receiver_task) { + this->busy_ready_interrupt_.task = receiver_task; + this->sync_word_interrupt_ = {receiver_task, static_cast(Interrupt::SYNC_WORD)}; + this->dio1_pin_->attach_interrupt(&Transceiver::notify_interrupt, &this->sync_word_interrupt_, + gpio::INTERRUPT_RISING_EDGE); +} + +void SX1262::reset_receiver() { + this->command(Opcode::SET_STANDBY, {0x01}); + this->rx_read_offset_ = 0; + this->command(Opcode::SET_BUFFER_BASE_ADDRESS, {0x00, 0x00}); + this->write_register(Register::RX_TX_PAYLOAD_LEN, 0xFF); + this->command(Opcode::CLEAR_IRQ_STATUS, {0xFF, 0xFF}); + ulTaskNotifyValueClear(nullptr, static_cast(Interrupt::SYNC_WORD)); + this->command(Opcode::SET_RX, {0xFF, 0xFF, 0xFF}); +} + +bool SX1262::read(std::span buffer, TickType_t first_byte_timeout) { + // SyncWordValid holds DIO1 high until reset_receiver() clears it. + if (!this->dio1_pin_->digital_read() && !this->wait_for(Interrupt::SYNC_WORD, first_byte_timeout)) + return false; + + while (!buffer.empty()) { + const size_t bytes = this->read_buffer(buffer); + if (bytes != 0) { + buffer = buffer.subspan(bytes); + continue; + } + + delay(SINGLE_BYTE_TIMEOUT); + if (this->available_bytes() == 0) + return false; + } + return true; +} + +int8_t SX1262::get_rssi() { + std::array packet_status; + this->command(Opcode::GET_PACKET_STATUS, {}, packet_status); + const auto [rx_status, rssi_sync, rssi_average] = packet_status; + return -static_cast(rssi_sync / 2); +} + +const char *SX1262::get_name() { return TAG; } + +bool SX1262::wait_for_busy() { + ulTaskNotifyValueClear(nullptr, static_cast(Interrupt::BUSY_READY)); + while (this->busy_pin_->digital_read()) { + if (!this->wait_for(Interrupt::BUSY_READY, pdMS_TO_TICKS(100)) && this->busy_pin_->digital_read()) { + ESP_LOGW(TAG, "BUSY wait timeout"); + return false; + } + } + return true; +} + +bool SX1262::wait_for(Interrupt interrupt, TickType_t timeout) { + const auto mask = static_cast(interrupt); + uint32_t notified; + while (xTaskNotifyWait(0, mask, ¬ified, timeout) == pdTRUE) { + if (notified & mask) + return true; + } + return false; +} + +void SX1262::command(Opcode opcode, std::initializer_list parameters, std::span response) { + this->wait_for_busy(); + this->enable(); + this->write_byte(static_cast(opcode)); + this->write_array(parameters.begin(), parameters.size()); + if (!response.empty()) { + // Reads return the status byte before the data. + this->write_byte(0x00); + this->read_array(response.data(), response.size()); + } + this->disable(); +} + +void SX1262::read_registers(Register address, std::span data) { + const auto raw = static_cast(address); + this->command(Opcode::READ_REGISTER, {byte(raw, 1), byte(raw, 0)}, data); +} + +void SX1262::write_register(Register address, uint8_t value) { + const auto raw = static_cast(address); + this->command(Opcode::WRITE_REGISTER, {byte(raw, 1), byte(raw, 0), value}); +} + +uint8_t SX1262::available_bytes() { + std::array rx_pointer; + this->read_registers(Register::RX_ADDR_POINTER, rx_pointer); + return rx_pointer[0] - this->rx_read_offset_; +} + +size_t SX1262::read_buffer(std::span target) { + target = target.first(std::min(this->available_bytes(), target.size())); + if (target.empty()) + return 0; + + this->command(Opcode::READ_BUFFER, {this->rx_read_offset_}, target); + this->rx_read_offset_ += target.size(); + // The data buffer is a 256-byte ring. Keeping the fixed payload length just behind the read offset lets + // a frame run past 255 bytes while the chip never overwrites what is still unread. + this->write_register(Register::RX_TX_PAYLOAD_LEN, this->rx_read_offset_ - 1); + return target.size(); +} + +} // namespace wmbus_radio +} // namespace esphome diff --git a/components/wmbus_radio/transceiver_sx1262.h b/components/wmbus_radio/transceiver_sx1262.h new file mode 100644 index 00000000..7c4a9266 --- /dev/null +++ b/components/wmbus_radio/transceiver_sx1262.h @@ -0,0 +1,66 @@ +#pragma once + +#include "transceiver.h" + +namespace esphome { +namespace wmbus_radio { + +class SX1262 final : public Transceiver { + public: + enum class TcxoVoltage : uint8_t { + V1_6 = 0x00, + V1_7 = 0x01, + V1_8 = 0x02, + V2_2 = 0x03, + V2_4 = 0x04, + V2_7 = 0x05, + V3_0 = 0x06, + V3_3 = 0x07, + NONE = 0xFF, + }; + + void setup() override; + void dump_config() override; + void start_receiver(TaskHandle_t receiver_task) override; + void reset_receiver() override; + bool read(std::span buffer, TickType_t first_byte_timeout) override; + int8_t get_rssi() override; + const char *get_name() override; + + void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } + void set_tcxo_voltage(TcxoVoltage tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; } + void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; } + void set_use_dcdc(bool use_dcdc) { this->use_dcdc_ = use_dcdc; } + void set_rx_boost(bool rx_boost) { this->rx_boost_ = rx_boost; } + + private: + enum class Register : uint16_t; + enum class Opcode : uint8_t; + enum class Interrupt : uint32_t { + SYNC_WORD = 1 << 0, + BUSY_READY = 1 << 1, + }; + + InternalGPIOPin *busy_pin_; + TcxoVoltage tcxo_voltage_; + bool rf_switch_; + bool use_dcdc_; + bool rx_boost_; + uint8_t rx_read_offset_{0}; + InterruptContext sync_word_interrupt_; + InterruptContext busy_ready_interrupt_; + + bool configure_chip(); + bool wait_for_busy(); + bool wait_for(Interrupt interrupt, TickType_t timeout); + + void command(Opcode opcode, std::initializer_list parameters, std::span response = {}); + void read_registers(Register address, std::span data); + void write_register(Register address, uint8_t value); + + uint8_t available_bytes(); + size_t read_buffer(std::span target); +}; + +} // namespace wmbus_radio +} // namespace esphome diff --git a/components/wmbus_radio/transceiver_sx1262.py b/components/wmbus_radio/transceiver_sx1262.py new file mode 100644 index 00000000..59e942ab --- /dev/null +++ b/components/wmbus_radio/transceiver_sx1262.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_BUSY_PIN + +from .radio import radio_ns +from .transceiver import register_transceiver, transceiver_schema + +CONF_TCXO_VOLTAGE = "tcxo_voltage" +CONF_RF_SWITCH = "rf_switch" +CONF_USE_DCDC = "use_dcdc" +CONF_RX_BOOST = "rx_boost" + +TcxoVoltage = radio_ns.class_("SX1262").enum("TcxoVoltage", is_class=True) + +TCXO_VOLTAGE = { + "0mV": TcxoVoltage.NONE, + "1600mV": TcxoVoltage.V1_6, + "1700mV": TcxoVoltage.V1_7, + "1800mV": TcxoVoltage.V1_8, + "2200mV": TcxoVoltage.V2_2, + "2400mV": TcxoVoltage.V2_4, + "2700mV": TcxoVoltage.V2_7, + "3000mV": TcxoVoltage.V3_0, + "3300mV": TcxoVoltage.V3_3, +} + + +def config_schema(radio_type, radio_schema): + return radio_schema.extend( + transceiver_schema( + radio_type, + { + cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_TCXO_VOLTAGE, default="0V"): cv.All( + cv.voltage, + lambda volts: f"{round(volts * 1000)}mV", + cv.enum(TCXO_VOLTAGE), + ), + cv.Optional(CONF_RF_SWITCH, default=False): cv.boolean, + cv.Optional(CONF_USE_DCDC, default=True): cv.boolean, + cv.Optional(CONF_RX_BOOST, default=False): cv.boolean, + }, + ) + ) + + +async def to_code(config): + var = await register_transceiver(config) + + busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + cg.add(var.set_busy_pin(busy_pin)) + cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE])) + cg.add(var.set_rf_switch(config[CONF_RF_SWITCH])) + cg.add(var.set_use_dcdc(config[CONF_USE_DCDC])) + cg.add(var.set_rx_boost(config[CONF_RX_BOOST])) + + return var diff --git a/components/wmbus_radio/transceiver_sx1276.cpp b/components/wmbus_radio/transceiver_sx1276.cpp index a3e81043..bb0f5adb 100644 --- a/components/wmbus_radio/transceiver_sx1276.cpp +++ b/components/wmbus_radio/transceiver_sx1276.cpp @@ -2,131 +2,161 @@ #include "esphome/core/log.h" -#define F_OSC (32000000) - namespace esphome { namespace wmbus_radio { + static const char *TAG = "SX1276"; +static constexpr uint32_t F_OSC = 32000000; + +enum class SX1276::Register : uint8_t { + FIFO = 0x00, + OP_MODE = 0x01, + BITRATE_MSB = 0x02, + FDEV_MSB = 0x04, + FRF_MSB = 0x06, + RX_CONFIG = 0x0D, + RSSI_CONFIG = 0x0E, + RSSI_VALUE = 0x11, + RX_BW = 0x12, + PREAMBLE_DETECT = 0x1F, + OSC = 0x24, + PREAMBLE_MSB = 0x25, + SYNC_CONFIG = 0x27, + PACKET_CONFIG1 = 0x30, + PAYLOAD_LENGTH = 0x32, + IRQ_FLAGS2 = 0x3F, + DIO_MAPPING1 = 0x40, + VERSION = 0x42, + BITRATE_FRAC = 0x5D, +}; void SX1276::setup() { - this->common_setup(); + this->reset_pin_->setup(); + this->dio1_pin_->setup(); + this->spi_setup(); - ESP_LOGV(TAG, "Setup"); + ESP_LOGV(TAG, "Setting up SX1276"); ESP_LOGVV(TAG, "reset"); this->reset(); ESP_LOGVV(TAG, "checking silicon revision"); - uint8_t revision = this->spi_read(0x42); + uint8_t revision = this->read_register(Register::VERSION); ESP_LOGVV(TAG, "revision: %02X", revision); if (revision < 0x11 || revision > 0x13) { ESP_LOGE(TAG, "Invalid silicon revision: %02X", revision); + this->mark_failed(); return; } ESP_LOGVV(TAG, "setting radio frequency"); - const uint32_t frequency = 868950000; + constexpr uint32_t frequency = 868950000; + constexpr uint32_t frf = ((uint64_t) frequency * (1 << 19)) / F_OSC; + this->write_register(Register::FRF_MSB, {byte(frf, 2), byte(frf, 1), byte(frf, 0)}); - uint32_t frf = ((uint64_t) frequency * (1 << 19)) / F_OSC; - this->spi_write(0x06, {BYTE(frf, 2), BYTE(frf, 1), BYTE(frf, 0)}); - - // TODO: Calculate in some rational way ESP_LOGVV(TAG, "setting radio bandwidth"); - this->spi_write(0x12, {2, 2}); + this->write_register(Register::RX_BW, {2, 2}); ESP_LOGVV(TAG, "set frequency deviation"); - const uint16_t freq_dev = 50000; - uint16_t frd = ((uint64_t) freq_dev * (1 << 19)) / F_OSC; - this->spi_write(0x04, {BYTE(frd, 1), BYTE(frd, 0)}); + constexpr uint16_t freq_dev = 50000; + constexpr uint16_t frd = ((uint64_t) freq_dev * (1 << 19)) / F_OSC; + this->write_register(Register::FDEV_MSB, {byte(frd, 1), byte(frd, 0)}); ESP_LOGVV(TAG, "set bitrate"); - const uint32_t bitrate = 100000; - uint32_t br = (F_OSC << 4) / bitrate; - // Fractional part of the bitrate - this->spi_write(0x5D, (uint8_t) (br & 0x0F)); - br >>= 4; - // Integer part of the bitrate - this->spi_write(0x02, {BYTE(br, 1), BYTE(br, 0)}); + constexpr uint32_t bitrate = 100000; + constexpr uint32_t br = (F_OSC << 4) / bitrate; // 1/16 steps + this->write_register(Register::BITRATE_FRAC, {static_cast(br & 0x0F)}); + this->write_register(Register::BITRATE_MSB, {byte(br >> 4, 1), byte(br >> 4, 0)}); ESP_LOGVV(TAG, "set preamble length"); - uint16_t preamble_length = 32 / 8; - this->spi_write(0x25, {BYTE(preamble_length, 1), BYTE(preamble_length, 0)}); + constexpr uint16_t preamble_length = 32 / 8; + this->write_register(Register::PREAMBLE_MSB, {byte(preamble_length, 1), byte(preamble_length, 0)}); ESP_LOGVV(TAG, "enable preamble detection"); - uint8_t preamble_detection = (1 << 7) | (1 << 5) | 0x0A; - this->spi_write(0x1F, preamble_detection); + constexpr uint8_t preamble_detection = (1 << 7) | (1 << 5) | 0x0A; + this->write_register(Register::PREAMBLE_DETECT, {preamble_detection}); ESP_LOGVV(TAG, "enable auto agc/afc"); - uint8_t agc_afc = (1 << 4) | (1 << 3) | 0b110; - this->spi_write(0x0D, agc_afc); + constexpr uint8_t agc_afc = (1 << 4) | (1 << 3) | 0b110; + this->write_register(Register::RX_CONFIG, {agc_afc}); ESP_LOGVV(TAG, "disable clock output"); - uint8_t clock_output = 0b111; - this->spi_write(0x24, clock_output); + this->write_register(Register::OSC, {0b111}); ESP_LOGVV(TAG, "set sync word and reverse preamble polarity"); - uint8_t reverse_preamble_sync_bytes = (1 << 5) | (1 << 4) | (2 - 1); - this->spi_write(0x27, {reverse_preamble_sync_bytes, 0x54, 0x3D}); + constexpr uint8_t reverse_preamble_sync_bytes = (1 << 5) | (1 << 4) | (2 - 1); + this->write_register(Register::SYNC_CONFIG, {reverse_preamble_sync_bytes, 0x54, 0x3D}); ESP_LOGVV(TAG, "disable crc check/fixed packet length"); - uint8_t crc_check = 0; - this->spi_write(0x30, crc_check); + this->write_register(Register::PACKET_CONFIG1, {0}); ESP_LOGVV(TAG, "set unlimited packet mode/zero length"); - uint8_t packet_mode = 0; - this->spi_write(0x32, packet_mode); + this->write_register(Register::PAYLOAD_LENGTH, {0}); ESP_LOGVV(TAG, "set fifo empty flag on DIO1"); - uint8_t fifo_empty_flag = 0b01 << 4; - this->spi_write(0x40, fifo_empty_flag); + this->write_register(Register::DIO_MAPPING1, {0b01 << 4}); ESP_LOGVV(TAG, "set RRSI smoothing"); - uint8_t rssi_smoothing = 0b111; - this->spi_write(0x0E, rssi_smoothing); + this->write_register(Register::RSSI_CONFIG, {0b111}); - ESP_LOGV(TAG, "SX1276 setup done"); + ESP_LOGI(TAG, "SX1276 setup done"); } -bool IRAM_ATTR SX1276::read(uint8_t *buffer, size_t length) { - while (length > 0) { - if (this->irq_pin_->digital_read()) { - if (!ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2))) { +void SX1276::start_receiver(TaskHandle_t receiver_task) { + this->data_ready_interrupt_ = {receiver_task, 1}; + this->dio1_pin_->attach_interrupt(&Transceiver::notify_interrupt, &this->data_ready_interrupt_, + gpio::INTERRUPT_FALLING_EDGE); +} + +bool SX1276::read(std::span buffer, TickType_t first_byte_timeout) { + if (this->dio1_pin_->digital_read() && !ulTaskNotifyTake(pdTRUE, first_byte_timeout)) + return false; + + while (!buffer.empty()) { + if (this->dio1_pin_->digital_read()) { + if (!ulTaskNotifyTake(pdTRUE, SINGLE_BYTE_TIMEOUT)) return false; - } } - this->delegate_->begin_transaction(); - this->delegate_->transfer(0x00); - while (length > 0 && !this->irq_pin_->digital_read()) { - *buffer++ = this->delegate_->transfer(0x00); - length--; + this->enable(); + this->transfer_byte(static_cast(Register::FIFO)); + while (!buffer.empty() && !this->dio1_pin_->digital_read()) { + buffer.front() = this->transfer_byte(0x00); + buffer = buffer.subspan(1); } - this->delegate_->end_transaction(); + this->disable(); } return true; } -void SX1276::restart_rx() { - // Standby mode - this->spi_write(0x01, (uint8_t) 0b001); +void SX1276::reset_receiver() { + this->write_register(Register::OP_MODE, {0b001}); delay(5); - - // Clear FIFO - this->spi_write(0x3F, (uint8_t) (1 << 4)); - - // Clear pending IRQs we not consumed in read() method + this->write_register(Register::IRQ_FLAGS2, {1 << 4}); ulTaskNotifyTake(pdTRUE, 0); - - // Enable RX - this->spi_write(0x01, (uint8_t) 0b101); + this->write_register(Register::OP_MODE, {0b101}); delay(5); } int8_t SX1276::get_rssi() { - uint8_t rssi = this->spi_read(0x11); + uint8_t rssi = this->read_register(Register::RSSI_VALUE); return (int8_t) (-rssi / 2); } const char *SX1276::get_name() { return TAG; } + +uint8_t SX1276::read_register(Register address) { + std::array transaction{static_cast(address), 0x00}; + this->spi_transaction(transaction); + return transaction[1]; +} + +void SX1276::write_register(Register address, std::initializer_list data) { + this->enable(); + this->write_byte(static_cast(address) | 0x80); + this->write_array(data.begin(), data.size()); + this->disable(); +} + } // namespace wmbus_radio } // namespace esphome diff --git a/components/wmbus_radio/transceiver_sx1276.h b/components/wmbus_radio/transceiver_sx1276.h index 477fe842..d4ae1bc5 100644 --- a/components/wmbus_radio/transceiver_sx1276.h +++ b/components/wmbus_radio/transceiver_sx1276.h @@ -1,15 +1,26 @@ #pragma once + #include "transceiver.h" namespace esphome { namespace wmbus_radio { -class SX1276 : public RadioTransceiver { + +class SX1276 final : public Transceiver { public: void setup() override; - bool read(uint8_t *buffer, size_t length) override; - void restart_rx() override; + void start_receiver(TaskHandle_t receiver_task) override; + void reset_receiver() override; + bool read(std::span buffer, TickType_t first_byte_timeout) override; int8_t get_rssi() override; const char *get_name() override; + + private: + enum class Register : uint8_t; + InterruptContext data_ready_interrupt_; + + uint8_t read_register(Register address); + void write_register(Register address, std::initializer_list data); }; + } // namespace wmbus_radio } // namespace esphome diff --git a/components/wmbus_radio/transceiver_sx1276.py b/components/wmbus_radio/transceiver_sx1276.py new file mode 100644 index 00000000..2a2a97c5 --- /dev/null +++ b/components/wmbus_radio/transceiver_sx1276.py @@ -0,0 +1,19 @@ +from esphome import config_validation as cv +from esphome.const import CONF_IRQ_PIN + +from .transceiver import CONF_DIO1_PIN, register_transceiver, transceiver_schema + + +def config_schema(radio_type, radio_schema): + return cv.All( + cv.rename_key( + CONF_IRQ_PIN, + CONF_DIO1_PIN, + removed_in="2027.3.0", + component="wmbus_radio", + ), + radio_schema.extend(transceiver_schema(radio_type, {})), + ) + + +to_code = register_transceiver From 014c6ea548646cccb20bc322cfde2d7997d9f52d Mon Sep 17 00:00:00 2001 From: Kuba Sawulski Date: Sat, 10 Oct 2026 23:22:23 +0200 Subject: [PATCH 3/4] (tests) Build tests for each radio and ESP32 variant --- .github/workflows/build.yaml | 15 ++- .../test.yaml} | 7 +- tests/wmbus.yaml | 58 ------------ tests/wmbus/common.yaml | 92 +++++++++++++++++++ tests/wmbus/sx1262.yaml | 11 +++ tests/wmbus/sx1276.yaml | 8 ++ 6 files changed, 126 insertions(+), 65 deletions(-) rename tests/{panasonic_aquarea.yaml => panasonic_aquarea/test.yaml} (92%) delete mode 100644 tests/wmbus.yaml create mode 100644 tests/wmbus/common.yaml create mode 100644 tests/wmbus/sx1262.yaml create mode 100644 tests/wmbus/sx1276.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eb379d96..fbe8cf3b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,15 +8,20 @@ jobs: strategy: fail-fast: false matrix: - name: - - wmbus - - panasonic_aquarea + config: + - wmbus/sx1276 + - wmbus/sx1262 + - panasonic_aquarea/test + variant: + - esp32 + - esp32s3 + - esp32c3 steps: - name: Checkout code uses: actions/checkout@v7 - name: Build firmware - id: esphome_build uses: esphome/build-action@v8.1.0 with: - yaml-file: tests/${{ matrix.name }}.yaml + yaml-file: tests/${{ matrix.config }}.yaml version: "2026.9.1" + substitutions: variant=${{ matrix.variant }} diff --git a/tests/panasonic_aquarea.yaml b/tests/panasonic_aquarea/test.yaml similarity index 92% rename from tests/panasonic_aquarea.yaml rename to tests/panasonic_aquarea/test.yaml index 8149358b..d9b48961 100644 --- a/tests/panasonic_aquarea.yaml +++ b/tests/panasonic_aquarea/test.yaml @@ -1,13 +1,16 @@ +substitutions: + variant: esp32 + esphome: name: test esp32: - board: esp32dev + variant: ${variant} framework: type: esp-idf external_components: - - source: ../components + - source: ../../components wifi: ssid: TestSSID diff --git a/tests/wmbus.yaml b/tests/wmbus.yaml deleted file mode 100644 index 77040581..00000000 --- a/tests/wmbus.yaml +++ /dev/null @@ -1,58 +0,0 @@ -esphome: - name: test - -esp32: - board: esp32dev - framework: - type: esp-idf - -external_components: - - source: ../components - -wifi: - ssid: TestSSID - password: TestPass - -mqtt: - broker: 192.168.1.1 - port: 1883 - -spi: - clk_pin: GPIO0 - mosi_pin: GPIO1 - miso_pin: GPIO2 - -socket_transmitter: - id: test_transmitter - ip_address: 192.168.1.1 - port: 12345 - protocol: TCP - -wmbus_common: - drivers: all - -wmbus_radio: - radio_type: SX1276 - cs_pin: GPIO3 - reset_pin: GPIO4 - irq_pin: GPIO5 - on_frame: - - wmbus_radio.send_frame_with_socket: - id: test_transmitter - format: rtlwmbus - -wmbus_meter: - id: test_meter - meter_id: 012abcd - type: apator162 - on_telegram: - - wmbus_meter.send_telegram_with_mqtt: - topic: test_topic - -sensor: - - platform: wmbus_meter - id: test_sensor - parent_id: test_meter - field: total_m3 - name: "Test Water Consumption" - diff --git a/tests/wmbus/common.yaml b/tests/wmbus/common.yaml new file mode 100644 index 00000000..f53e16b8 --- /dev/null +++ b/tests/wmbus/common.yaml @@ -0,0 +1,92 @@ +substitutions: + variant: esp32 + +esphome: + name: test-wmbus + +esp32: + variant: ${variant} + framework: + type: esp-idf + +external_components: + - source: ../../components + +logger: + +wifi: + ssid: TestSSID + password: TestPass + +mqtt: + broker: 192.168.1.1 + port: 1883 + +spi: + clk_pin: GPIO0 + mosi_pin: GPIO1 + miso_pin: GPIO2 + +socket_transmitter: + - id: tcp_transmitter + ip_address: 192.168.1.1 + port: 12345 + protocol: TCP + - id: udp_transmitter + ip_address: 192.168.1.1 + port: 12346 + protocol: UDP + +wmbus_common: + drivers: all + +wmbus_radio: + on_packet: + - then: + - logger.log: + format: "packet of %d bytes" + args: ["(int) packet->get_raw_data().size()"] + on_frame: + - then: + - lambda: ESP_LOGI("test", "Frame from %s", frame->meter_id().c_str()); + - wmbus_radio.send_frame_with_socket: + id: tcp_transmitter + format: rtlwmbus + - mark_as_handled: true + then: + - wmbus_radio.send_frame_with_socket: + id: udp_transmitter + format: raw + - wmbus_radio.send_frame_with_socket: + id: udp_transmitter + format: hex + +wmbus_meter: + - id: test_meter + meter_id: "01234567" + type: apator162 + key: "00000000000000000000000000000000" + on_telegram: + - wmbus_meter.send_telegram_with_mqtt: + topic: test_topic + - socket_transmitter.send: + id: tcp_transmitter + data: !lambda return meter->as_json(); + +sensor: + - platform: wmbus_meter + id: test_sensor + parent_id: test_meter + field: total_m3 + name: "Test Water Consumption" + +text_sensor: + - platform: wmbus_meter + id: test_text_sensor + parent_id: test_meter + field: timestamp + name: "Test Meter Timestamp" + - platform: wmbus_meter + parent_id: test_meter + field: media + name: "Test Meter Media" diff --git a/tests/wmbus/sx1262.yaml b/tests/wmbus/sx1262.yaml new file mode 100644 index 00000000..c955a253 --- /dev/null +++ b/tests/wmbus/sx1262.yaml @@ -0,0 +1,11 @@ +packages: + wmbus: !include common.yaml + +wmbus_radio: + radio_type: SX1262 + cs_pin: GPIO3 + reset_pin: GPIO4 + busy_pin: GPIO5 + dio1_pin: GPIO21 + tcxo_voltage: 1.8V + rf_switch: true diff --git a/tests/wmbus/sx1276.yaml b/tests/wmbus/sx1276.yaml new file mode 100644 index 00000000..05f32346 --- /dev/null +++ b/tests/wmbus/sx1276.yaml @@ -0,0 +1,8 @@ +packages: + wmbus: !include common.yaml + +wmbus_radio: + radio_type: SX1276 + cs_pin: GPIO3 + reset_pin: GPIO4 + dio1_pin: GPIO5 From 42050c5d82542abd48af2b1ffa714accf8cb02cc Mon Sep 17 00:00:00 2001 From: Kuba Sawulski Date: Sun, 11 Oct 2026 20:12:29 +0200 Subject: [PATCH 4/4] (wmbus_radio) Use StaticTask for receiver task --- components/wmbus_radio/component.cpp | 2 +- components/wmbus_radio/component.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/wmbus_radio/component.cpp b/components/wmbus_radio/component.cpp index 501f6109..fff2d6aa 100644 --- a/components/wmbus_radio/component.cpp +++ b/components/wmbus_radio/component.cpp @@ -16,7 +16,7 @@ void Radio::setup() { return; } - if (xTaskCreate((TaskFunction_t) this->receiver_task, "radio_recv", 3 * 1024, this, 2, nullptr) != pdPASS) { + if (!this->receiver_task_.create((TaskFunction_t) this->receiver_task, "radio_recv", 3 * 1024, this, 2, false)) { ESP_LOGE(TAG, "Failed to create receiver task"); this->mark_failed(); return; diff --git a/components/wmbus_radio/component.h b/components/wmbus_radio/component.h index 495a8e2f..87613a9d 100644 --- a/components/wmbus_radio/component.h +++ b/components/wmbus_radio/component.h @@ -5,6 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/gpio.h" #include "esphome/core/lock_free_queue.h" +#include "esphome/core/static_task.h" #include "esphome/components/spi/spi.h" @@ -31,6 +32,7 @@ class Radio : public Component { Transceiver *transceiver_{nullptr}; LockFreeQueue packet_queue_; + StaticTask receiver_task_; CallbackManager on_frame_callback_manager_; LazyCallbackManager on_packet_callback_manager_;