From 1ef7b2d64f5a7e8fad2aac63a3be15ff69b9b9a8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:37:18 -0400 Subject: [PATCH] [sx127x] Add sx127x component (#7490) Co-authored-by: Jonathan Swoboda --- CODEOWNERS | 1 + esphome/components/sx127x/__init__.py | 325 ++++++++++++ esphome/components/sx127x/automation.h | 62 +++ .../sx127x/packet_transport/__init__.py | 26 + .../packet_transport/sx127x_transport.cpp | 26 + .../packet_transport/sx127x_transport.h | 25 + esphome/components/sx127x/sx127x.cpp | 493 ++++++++++++++++++ esphome/components/sx127x/sx127x.h | 125 +++++ esphome/components/sx127x/sx127x_reg.h | 295 +++++++++++ tests/components/sx127x/common.yaml | 45 ++ tests/components/sx127x/test.esp32-ard.yaml | 9 + .../components/sx127x/test.esp32-c3-ard.yaml | 9 + .../components/sx127x/test.esp32-c3-idf.yaml | 9 + tests/components/sx127x/test.esp32-idf.yaml | 9 + tests/components/sx127x/test.esp8266-ard.yaml | 9 + tests/components/sx127x/test.rp2040-ard.yaml | 9 + 16 files changed, 1477 insertions(+) create mode 100644 esphome/components/sx127x/__init__.py create mode 100644 esphome/components/sx127x/automation.h create mode 100644 esphome/components/sx127x/packet_transport/__init__.py create mode 100644 esphome/components/sx127x/packet_transport/sx127x_transport.cpp create mode 100644 esphome/components/sx127x/packet_transport/sx127x_transport.h create mode 100644 esphome/components/sx127x/sx127x.cpp create mode 100644 esphome/components/sx127x/sx127x.h create mode 100644 esphome/components/sx127x/sx127x_reg.h create mode 100644 tests/components/sx127x/common.yaml create mode 100644 tests/components/sx127x/test.esp32-ard.yaml create mode 100644 tests/components/sx127x/test.esp32-c3-ard.yaml create mode 100644 tests/components/sx127x/test.esp32-c3-idf.yaml create mode 100644 tests/components/sx127x/test.esp32-idf.yaml create mode 100644 tests/components/sx127x/test.esp8266-ard.yaml create mode 100644 tests/components/sx127x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 295dd9b1b2..540f33853d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -441,6 +441,7 @@ esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 esphome/components/switch/* @esphome/core esphome/components/switch/binary_sensor/* @ssieb +esphome/components/sx127x/* @swoboda1337 esphome/components/syslog/* @clydebarrow esphome/components/t6615/* @tylermenezes esphome/components/tc74/* @sethgirvan diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py new file mode 100644 index 0000000000..4d034801cc --- /dev/null +++ b/esphome/components/sx127x/__init__.py @@ -0,0 +1,325 @@ +from esphome import automation, pins +import esphome.codegen as cg +from esphome.components import spi +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID + +MULTI_CONF = True +CODEOWNERS = ["@swoboda1337"] +DEPENDENCIES = ["spi"] + +CONF_SX127X_ID = "sx127x_id" + +CONF_AUTO_CAL = "auto_cal" +CONF_BANDWIDTH = "bandwidth" +CONF_BITRATE = "bitrate" +CONF_BITSYNC = "bitsync" +CONF_CODING_RATE = "coding_rate" +CONF_CRC_ENABLE = "crc_enable" +CONF_DEVIATION = "deviation" +CONF_DIO0_PIN = "dio0_pin" +CONF_MODULATION = "modulation" +CONF_ON_PACKET = "on_packet" +CONF_PA_PIN = "pa_pin" +CONF_PA_POWER = "pa_power" +CONF_PA_RAMP = "pa_ramp" +CONF_PACKET_MODE = "packet_mode" +CONF_PAYLOAD_LENGTH = "payload_length" +CONF_PREAMBLE_DETECT = "preamble_detect" +CONF_PREAMBLE_ERRORS = "preamble_errors" +CONF_PREAMBLE_POLARITY = "preamble_polarity" +CONF_PREAMBLE_SIZE = "preamble_size" +CONF_RST_PIN = "rst_pin" +CONF_RX_FLOOR = "rx_floor" +CONF_RX_START = "rx_start" +CONF_SHAPING = "shaping" +CONF_SPREADING_FACTOR = "spreading_factor" +CONF_SYNC_VALUE = "sync_value" + +sx127x_ns = cg.esphome_ns.namespace("sx127x") +SX127x = sx127x_ns.class_("SX127x", cg.Component, spi.SPIDevice) +SX127xListener = sx127x_ns.class_("SX127xListener") +SX127xBw = sx127x_ns.enum("SX127xBw") +SX127xOpMode = sx127x_ns.enum("SX127xOpMode") +SX127xPaConfig = sx127x_ns.enum("SX127xPaConfig") +SX127xPaRamp = sx127x_ns.enum("SX127xPaRamp") +SX127xModemCfg1 = sx127x_ns.enum("SX127xModemCfg1") + +BW = { + "2_6kHz": SX127xBw.SX127X_BW_2_6, + "3_1kHz": SX127xBw.SX127X_BW_3_1, + "3_9kHz": SX127xBw.SX127X_BW_3_9, + "5_2kHz": SX127xBw.SX127X_BW_5_2, + "6_3kHz": SX127xBw.SX127X_BW_6_3, + "7_8kHz": SX127xBw.SX127X_BW_7_8, + "10_4kHz": SX127xBw.SX127X_BW_10_4, + "12_5kHz": SX127xBw.SX127X_BW_12_5, + "15_6kHz": SX127xBw.SX127X_BW_15_6, + "20_8kHz": SX127xBw.SX127X_BW_20_8, + "25_0kHz": SX127xBw.SX127X_BW_25_0, + "31_3kHz": SX127xBw.SX127X_BW_31_3, + "41_7kHz": SX127xBw.SX127X_BW_41_7, + "50_0kHz": SX127xBw.SX127X_BW_50_0, + "62_5kHz": SX127xBw.SX127X_BW_62_5, + "83_3kHz": SX127xBw.SX127X_BW_83_3, + "100_0kHz": SX127xBw.SX127X_BW_100_0, + "125_0kHz": SX127xBw.SX127X_BW_125_0, + "166_7kHz": SX127xBw.SX127X_BW_166_7, + "200_0kHz": SX127xBw.SX127X_BW_200_0, + "250_0kHz": SX127xBw.SX127X_BW_250_0, + "500_0kHz": SX127xBw.SX127X_BW_500_0, +} + +CODING_RATE = { + "CR_4_5": SX127xModemCfg1.CODING_RATE_4_5, + "CR_4_6": SX127xModemCfg1.CODING_RATE_4_6, + "CR_4_7": SX127xModemCfg1.CODING_RATE_4_7, + "CR_4_8": SX127xModemCfg1.CODING_RATE_4_8, +} + +MOD = { + "LORA": SX127xOpMode.MOD_LORA, + "FSK": SX127xOpMode.MOD_FSK, + "OOK": SX127xOpMode.MOD_OOK, +} + +PA_PIN = { + "RFO": SX127xPaConfig.PA_PIN_RFO, + "BOOST": SX127xPaConfig.PA_PIN_BOOST, +} + +RAMP = { + "10us": SX127xPaRamp.PA_RAMP_10, + "12us": SX127xPaRamp.PA_RAMP_12, + "15us": SX127xPaRamp.PA_RAMP_15, + "20us": SX127xPaRamp.PA_RAMP_20, + "25us": SX127xPaRamp.PA_RAMP_25, + "31us": SX127xPaRamp.PA_RAMP_31, + "40us": SX127xPaRamp.PA_RAMP_40, + "50us": SX127xPaRamp.PA_RAMP_50, + "62us": SX127xPaRamp.PA_RAMP_62, + "100us": SX127xPaRamp.PA_RAMP_100, + "125us": SX127xPaRamp.PA_RAMP_125, + "250us": SX127xPaRamp.PA_RAMP_250, + "500us": SX127xPaRamp.PA_RAMP_500, + "1000us": SX127xPaRamp.PA_RAMP_1000, + "2000us": SX127xPaRamp.PA_RAMP_2000, + "3400us": SX127xPaRamp.PA_RAMP_3400, +} + +SHAPING = { + "CUTOFF_BR_X_2": SX127xPaRamp.CUTOFF_BR_X_2, + "CUTOFF_BR_X_1": SX127xPaRamp.CUTOFF_BR_X_1, + "GAUSSIAN_BT_0_3": SX127xPaRamp.GAUSSIAN_BT_0_3, + "GAUSSIAN_BT_0_5": SX127xPaRamp.GAUSSIAN_BT_0_5, + "GAUSSIAN_BT_1_0": SX127xPaRamp.GAUSSIAN_BT_1_0, + "NONE": SX127xPaRamp.SHAPING_NONE, +} + +RunImageCalAction = sx127x_ns.class_( + "RunImageCalAction", automation.Action, cg.Parented.template(SX127x) +) +SendPacketAction = sx127x_ns.class_( + "SendPacketAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeTxAction = sx127x_ns.class_( + "SetModeTxAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeRxAction = sx127x_ns.class_( + "SetModeRxAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeSleepAction = sx127x_ns.class_( + "SetModeSleepAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeStandbyAction = sx127x_ns.class_( + "SetModeStandbyAction", automation.Action, cg.Parented.template(SX127x) +) + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +def validate_config(config): + if config[CONF_MODULATION] == "LORA": + bws = [ + "7_8kHz", + "10_4kHz", + "15_6kHz", + "20_8kHz", + "31_3kHz", + "41_7kHz", + "62_5kHz", + "125_0kHz", + "250_0kHz", + "500_0kHz", + ] + if config[CONF_BANDWIDTH] not in bws: + raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") + if CONF_DIO0_PIN not in config: + raise cv.Invalid("Cannot use LoRa without dio0_pin") + if 0 < config[CONF_PREAMBLE_SIZE] < 6: + raise cv.Invalid("Minimum preamble size is 6 with LORA") + if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0: + raise cv.Invalid("Payload length must be set when spreading factor is 6") + else: + if config[CONF_BANDWIDTH] == "500_0kHz": + raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is only available with LORA") + if CONF_BITSYNC not in config: + raise cv.Invalid("Config 'bitsync' required with FSK/OOK") + if CONF_PACKET_MODE not in config: + raise cv.Invalid("Config 'packet_mode' required with FSK/OOK") + if config[CONF_PACKET_MODE] and CONF_DIO0_PIN not in config: + raise cv.Invalid("Config 'dio0_pin' required in packet mode") + if config[CONF_PAYLOAD_LENGTH] > 64: + raise cv.Invalid("Payload length must be <= 64 with FSK/OOK") + if config[CONF_PA_PIN] == "RFO" and config[CONF_PA_POWER] > 15: + raise cv.Invalid("PA power must be <= 15 dbm when using the RFO pin") + if config[CONF_PA_PIN] == "BOOST" and config[CONF_PA_POWER] < 2: + raise cv.Invalid("PA power must be >= 2 dbm when using the BOOST pin") + return config + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SX127x), + cv.Optional(CONF_AUTO_CAL, default=True): cv.boolean, + cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), + cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=500, max=300000), + cv.Optional(CONF_BITSYNC): cv.boolean, + cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), + cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_MODULATION): cv.enum(MOD), + cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), + cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN), + cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=0, max=17), + cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), + cv.Optional(CONF_PACKET_MODE): cv.boolean, + cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), + cv.Optional(CONF_PREAMBLE_DETECT, default=0): cv.int_range(min=0, max=3), + cv.Optional(CONF_PREAMBLE_ERRORS, default=0): cv.int_range(min=0, max=31), + cv.Optional(CONF_PREAMBLE_POLARITY, default=0xAA): cv.All( + cv.hex_int, cv.one_of(0xAA, 0x55) + ), + cv.Optional(CONF_PREAMBLE_SIZE, default=0): cv.int_range(min=0, max=65535), + cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_RX_FLOOR, default=-94): cv.float_range(min=-128, max=-1), + cv.Optional(CONF_RX_START, default=True): cv.boolean, + cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), + cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12), + cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t), + }, + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(True, 8e6, "mode0")) + .add_extra(validate_config) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + if CONF_ON_PACKET in config: + await automation.build_automation( + var.get_packet_trigger(), + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.float_, "rssi"), + (cg.float_, "snr"), + ], + config[CONF_ON_PACKET], + ) + if CONF_DIO0_PIN in config: + dio0_pin = await cg.gpio_pin_expression(config[CONF_DIO0_PIN]) + cg.add(var.set_dio0_pin(dio0_pin)) + rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN]) + cg.add(var.set_rst_pin(rst_pin)) + cg.add(var.set_auto_cal(config[CONF_AUTO_CAL])) + cg.add(var.set_bandwidth(config[CONF_BANDWIDTH])) + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + cg.add(var.set_deviation(config[CONF_DEVIATION])) + cg.add(var.set_modulation(config[CONF_MODULATION])) + if config[CONF_MODULATION] != "LORA": + cg.add(var.set_bitrate(config[CONF_BITRATE])) + cg.add(var.set_bitsync(config[CONF_BITSYNC])) + cg.add(var.set_packet_mode(config[CONF_PACKET_MODE])) + cg.add(var.set_pa_pin(config[CONF_PA_PIN])) + cg.add(var.set_pa_ramp(config[CONF_PA_RAMP])) + cg.add(var.set_pa_power(config[CONF_PA_POWER])) + cg.add(var.set_shaping(config[CONF_SHAPING])) + cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE])) + cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH])) + cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT])) + cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE])) + cg.add(var.set_preamble_polarity(config[CONF_PREAMBLE_POLARITY])) + cg.add(var.set_preamble_errors(config[CONF_PREAMBLE_ERRORS])) + cg.add(var.set_coding_rate(config[CONF_CODING_RATE])) + cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR])) + cg.add(var.set_sync_value(config[CONF_SYNC_VALUE])) + cg.add(var.set_rx_floor(config[CONF_RX_FLOOR])) + cg.add(var.set_rx_start(config[CONF_RX_START])) + + +NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SX127x), + } +) + + +@automation.register_action( + "sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA +) +async def no_args_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(SX127x), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, +) + + +@automation.register_action( + "sx127x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA +) +async def send_packet_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + return var diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h new file mode 100644 index 0000000000..2b9c261de1 --- /dev/null +++ b/esphome/components/sx127x/automation.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sx127x/sx127x.h" + +namespace esphome { +namespace sx127x { + +template class RunImageCalAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->run_image_cal(); } +}; + +template class SendPacketAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void play(Ts... x) override { + if (this->static_) { + this->parent_->transmit_packet(this->data_static_); + } else { + this->parent_->transmit_packet(this->data_func_(x...)); + } + } + + protected: + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +template class SetModeTxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_tx(); } +}; + +template class SetModeRxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_rx(); } +}; + +template class SetModeSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_sleep(); } +}; + +template class SetModeStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_standby(); } +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/packet_transport/__init__.py b/esphome/components/sx127x/packet_transport/__init__.py new file mode 100644 index 0000000000..2f3a0f6e2b --- /dev/null +++ b/esphome/components/sx127x/packet_transport/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +from esphome.components.packet_transport import ( + PacketTransport, + new_packet_transport, + transport_schema, +) +import esphome.config_validation as cv +from esphome.cpp_types import PollingComponent + +from .. import CONF_SX127X_ID, SX127x, SX127xListener, sx127x_ns + +SX127xTransport = sx127x_ns.class_( + "SX127xTransport", PacketTransport, PollingComponent, SX127xListener +) + +CONFIG_SCHEMA = transport_schema(SX127xTransport).extend( + { + cv.GenerateID(CONF_SX127X_ID): cv.use_id(SX127x), + } +) + + +async def to_code(config): + var, _ = await new_packet_transport(config) + sx127x = await cg.get_variable(config[CONF_SX127X_ID]) + cg.add(var.set_parent(sx127x)) diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp new file mode 100644 index 0000000000..b1d014bb96 --- /dev/null +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp @@ -0,0 +1,26 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "sx127x_transport.h" + +namespace esphome { +namespace sx127x { + +static const char *const TAG = "sx127x_transport"; + +void SX127xTransport::setup() { + PacketTransport::setup(); + this->parent_->register_listener(this); +} + +void SX127xTransport::update() { + PacketTransport::update(); + this->updated_ = true; + this->resend_data_ = true; +} + +void SX127xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); } + +void SX127xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); } + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.h b/esphome/components/sx127x/packet_transport/sx127x_transport.h new file mode 100644 index 0000000000..e27b7f8d57 --- /dev/null +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sx127x/sx127x.h" +#include "esphome/components/packet_transport/packet_transport.h" +#include + +namespace esphome { +namespace sx127x { + +class SX127xTransport : public packet_transport::PacketTransport, public Parented, public SX127xListener { + public: + void setup() override; + void update() override; + void on_packet(const std::vector &packet, float rssi, float snr) override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + void send_packet(const std::vector &buf) const override; + bool should_send() override { return true; } + size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); } +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp new file mode 100644 index 0000000000..e41efe098c --- /dev/null +++ b/esphome/components/sx127x/sx127x.cpp @@ -0,0 +1,493 @@ +#include "sx127x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx127x { + +static const char *const TAG = "sx127x"; +static const uint32_t FXOSC = 32000000u; +static const uint16_t RAMP[16] = {3400, 2000, 1000, 500, 250, 125, 100, 62, 50, 40, 31, 25, 20, 15, 12, 10}; +static const uint32_t BW_HZ[22] = {2604, 3125, 3906, 5208, 6250, 7812, 10416, 12500, 15625, 20833, 25000, + 31250, 41666, 50000, 62500, 83333, 100000, 125000, 166666, 200000, 250000, 500000}; +static const uint8_t BW_LORA[22] = {BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_10_4, BW_15_6, + BW_15_6, BW_20_8, BW_31_3, BW_31_3, BW_41_7, BW_62_5, BW_62_5, BW_125_0, + BW_125_0, BW_125_0, BW_250_0, BW_250_0, BW_250_0, BW_500_0}; +static const uint8_t BW_FSK_OOK[22] = {RX_BW_2_6, RX_BW_3_1, RX_BW_3_9, RX_BW_5_2, RX_BW_6_3, RX_BW_7_8, + RX_BW_10_4, RX_BW_12_5, RX_BW_15_6, RX_BW_20_8, RX_BW_25_0, RX_BW_31_3, + RX_BW_41_7, RX_BW_50_0, RX_BW_62_5, RX_BW_83_3, RX_BW_100_0, RX_BW_125_0, + RX_BW_166_7, RX_BW_200_0, RX_BW_250_0, RX_BW_250_0}; +static const int32_t RSSI_OFFSET_HF = 157; +static const int32_t RSSI_OFFSET_LF = 164; + +uint8_t SX127x::read_register_(uint8_t reg) { + this->enable(); + this->write_byte(reg & 0x7F); + uint8_t value = this->read_byte(); + this->disable(); + return value; +} + +void SX127x::write_register_(uint8_t reg, uint8_t value) { + this->enable(); + this->write_byte(reg | 0x80); + this->write_byte(value); + this->disable(); +} + +void SX127x::read_fifo_(std::vector &packet) { + this->enable(); + this->write_byte(REG_FIFO & 0x7F); + this->read_array(packet.data(), packet.size()); + this->disable(); +} + +void SX127x::write_fifo_(const std::vector &packet) { + this->enable(); + this->write_byte(REG_FIFO | 0x80); + this->write_array(packet.data(), packet.size()); + this->disable(); +} + +void SX127x::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + + // setup reset + this->rst_pin_->setup(); + + // setup dio0 + if (this->dio0_pin_) { + this->dio0_pin_->setup(); + } + + // start spi + this->spi_setup(); + + // configure rf + this->configure(); +} + +void SX127x::configure() { + // toggle chip reset + this->rst_pin_->digital_write(false); + delayMicroseconds(1000); + this->rst_pin_->digital_write(true); + delayMicroseconds(10000); + + // check silicon version to make sure hw is ok + if (this->read_register_(REG_VERSION) != 0x12) { + this->mark_failed(); + return; + } + + // enter sleep mode + this->set_mode_(MOD_FSK, MODE_SLEEP); + + // set freq + uint64_t frf = ((uint64_t) this->frequency_ << 19) / FXOSC; + this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF)); + this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF)); + this->write_register_(REG_FRF_LSB, (uint8_t) ((frf >> 0) & 0xFF)); + + // enter standby mode + this->set_mode_(MOD_FSK, MODE_STDBY); + + // run image cal + this->run_image_cal(); + + // go back to sleep + this->set_mode_sleep(); + + // config pa + if (this->pa_pin_ == PA_PIN_BOOST) { + this->pa_power_ = std::max(this->pa_power_, (uint8_t) 2); + this->pa_power_ = std::min(this->pa_power_, (uint8_t) 17); + this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 2) | this->pa_pin_ | PA_MAX_POWER); + } else { + this->pa_power_ = std::min(this->pa_power_, (uint8_t) 14); + this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 0) | this->pa_pin_ | PA_MAX_POWER); + } + if (this->modulation_ != MOD_LORA) { + this->write_register_(REG_PA_RAMP, this->pa_ramp_ | this->shaping_); + } else { + this->write_register_(REG_PA_RAMP, this->pa_ramp_); + } + + // configure modem + if (this->modulation_ != MOD_LORA) { + this->configure_fsk_ook_(); + } else { + this->configure_lora_(); + } + + // switch to rx or sleep + if (this->rx_start_) { + this->set_mode_rx(); + } else { + this->set_mode_sleep(); + } +} + +void SX127x::configure_fsk_ook_() { + // set the channel bw + this->write_register_(REG_RX_BW, BW_FSK_OOK[this->bandwidth_]); + + // set fdev + uint32_t fdev = std::min((this->deviation_ * 4096) / 250000, (uint32_t) 0x3FFF); + this->write_register_(REG_FDEV_MSB, (uint8_t) ((fdev >> 8) & 0xFF)); + this->write_register_(REG_FDEV_LSB, (uint8_t) ((fdev >> 0) & 0xFF)); + + // set bitrate + uint64_t bitrate = (FXOSC + this->bitrate_ / 2) / this->bitrate_; // round up + this->write_register_(REG_BITRATE_MSB, (uint8_t) ((bitrate >> 8) & 0xFF)); + this->write_register_(REG_BITRATE_LSB, (uint8_t) ((bitrate >> 0) & 0xFF)); + + // configure rx and afc + uint8_t trigger = (this->preamble_detect_ > 0) ? TRIGGER_PREAMBLE : TRIGGER_RSSI; + this->write_register_(REG_AFC_FEI, AFC_AUTO_CLEAR_ON); + if (this->modulation_ == MOD_FSK) { + this->write_register_(REG_RX_CONFIG, AFC_AUTO_ON | AGC_AUTO_ON | trigger); + } else { + this->write_register_(REG_RX_CONFIG, AGC_AUTO_ON | trigger); + } + + // configure packet mode + if (this->packet_mode_) { + uint8_t crc_mode = (this->crc_enable_) ? CRC_ON : CRC_OFF; + this->write_register_(REG_FIFO_THRESH, TX_START_FIFO_EMPTY); + if (this->payload_length_ > 0) { + this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->payload_length_); + this->write_register_(REG_PACKET_CONFIG_1, crc_mode | FIXED_LENGTH); + } else { + this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->get_max_packet_size() - 1); + this->write_register_(REG_PACKET_CONFIG_1, crc_mode | VARIABLE_LENGTH); + } + this->write_register_(REG_PACKET_CONFIG_2, PACKET_MODE); + } else { + this->write_register_(REG_PACKET_CONFIG_2, CONTINUOUS_MODE); + } + this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00); + + // config bit synchronizer + uint8_t polarity = (this->preamble_polarity_ == 0xAA) ? PREAMBLE_AA : PREAMBLE_55; + if (!this->sync_value_.empty()) { + uint8_t size = this->sync_value_.size() - 1; + this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity | SYNC_ON | size); + for (uint32_t i = 0; i < this->sync_value_.size(); i++) { + this->write_register_(REG_SYNC_VALUE1 + i, this->sync_value_[i]); + } + } else { + this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity); + } + + // config preamble detector + if (this->preamble_detect_ > 0) { + uint8_t size = (this->preamble_detect_ - 1) << PREAMBLE_DETECTOR_SIZE_SHIFT; + uint8_t tol = this->preamble_errors_ << PREAMBLE_DETECTOR_TOL_SHIFT; + this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_ON | size | tol); + } else { + this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_OFF); + } + this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_SIZE_LSB, this->preamble_size_ & 0xFF); + + // config sync generation and setup ook threshold + uint8_t bitsync = this->bitsync_ ? BIT_SYNC_ON : BIT_SYNC_OFF; + this->write_register_(REG_OOK_PEAK, bitsync | OOK_THRESH_STEP_0_5 | OOK_THRESH_PEAK); + this->write_register_(REG_OOK_AVG, OOK_AVG_RESERVED | OOK_THRESH_DEC_1_8); + + // set rx floor + this->write_register_(REG_OOK_FIX, 256 + int(this->rx_floor_ * 2.0)); + this->write_register_(REG_RSSI_THRESH, std::abs(int(this->rx_floor_ * 2.0))); +} + +void SX127x::configure_lora_() { + // config modem + uint8_t header_mode = this->payload_length_ > 0 ? IMPLICIT_HEADER : EXPLICIT_HEADER; + uint8_t crc_mode = (this->crc_enable_) ? RX_PAYLOAD_CRC_ON : RX_PAYLOAD_CRC_OFF; + uint8_t spreading_factor = this->spreading_factor_ << SPREADING_FACTOR_SHIFT; + this->write_register_(REG_MODEM_CONFIG1, BW_LORA[this->bandwidth_] | this->coding_rate_ | header_mode); + this->write_register_(REG_MODEM_CONFIG2, spreading_factor | crc_mode); + + // config fifo and payload length + this->write_register_(REG_FIFO_TX_BASE_ADDR, 0x00); + this->write_register_(REG_FIFO_RX_BASE_ADDR, 0x00); + this->write_register_(REG_PAYLOAD_LENGTH, std::max(this->payload_length_, (uint32_t) 1)); + + // config preamble + if (this->preamble_size_ >= 6) { + this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_LEN_LSB, this->preamble_size_ & 0xFF); + } + + // optimize detection + float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_]; + if (duration > 16) { + this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON | LOW_DATA_RATE_OPTIMIZE_ON); + } else { + this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON); + } + if (this->spreading_factor_ == 6) { + this->write_register_(REG_DETECT_OPTIMIZE, 0xC5); + this->write_register_(REG_DETECT_THRESHOLD, 0x0C); + } else { + this->write_register_(REG_DETECT_OPTIMIZE, 0xC3); + this->write_register_(REG_DETECT_THRESHOLD, 0x0A); + } + + // config sync word + if (!this->sync_value_.empty()) { + this->write_register_(REG_SYNC_WORD, this->sync_value_[0]); + } +} + +size_t SX127x::get_max_packet_size() { + if (this->payload_length_ > 0) { + return this->payload_length_; + } + if (this->modulation_ == MOD_LORA) { + return 256; + } else { + return 64; + } +} + +void SX127x::transmit_packet(const std::vector &packet) { + if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) { + ESP_LOGE(TAG, "Packet size does not match config"); + return; + } + if (packet.empty() || packet.size() > this->get_max_packet_size()) { + ESP_LOGE(TAG, "Packet size out of range"); + return; + } + if (this->modulation_ == MOD_LORA) { + this->set_mode_standby(); + if (this->payload_length_ == 0) { + this->write_register_(REG_PAYLOAD_LENGTH, packet.size()); + } + this->write_register_(REG_IRQ_FLAGS, 0xFF); + this->write_register_(REG_FIFO_ADDR_PTR, 0); + this->write_fifo_(packet); + this->set_mode_tx(); + } else { + this->set_mode_standby(); + if (this->payload_length_ == 0) { + this->write_register_(REG_FIFO, packet.size()); + } + this->write_fifo_(packet); + this->set_mode_tx(); + } + // wait until transmit completes, typically the delay will be less than 100 ms + uint32_t start = millis(); + while (!this->dio0_pin_->digital_read()) { + if (millis() - start > 4000) { + ESP_LOGE(TAG, "Transmit packet failure"); + break; + } + } + if (this->rx_start_) { + this->set_mode_rx(); + } else { + this->set_mode_sleep(); + } +} + +void SX127x::call_listeners_(const std::vector &packet, float rssi, float snr) { + for (auto &listener : this->listeners_) { + listener->on_packet(packet, rssi, snr); + } + this->packet_trigger_->trigger(packet, rssi, snr); +} + +void SX127x::loop() { + if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) { + return; + } + + if (this->modulation_ == MOD_LORA) { + uint8_t status = this->read_register_(REG_IRQ_FLAGS); + this->write_register_(REG_IRQ_FLAGS, 0xFF); + if ((status & PAYLOAD_CRC_ERROR) == 0) { + uint8_t bytes = this->read_register_(REG_NB_RX_BYTES); + uint8_t addr = this->read_register_(REG_FIFO_RX_CURR_ADDR); + uint8_t rssi = this->read_register_(REG_PKT_RSSI_VALUE); + int8_t snr = (int8_t) this->read_register_(REG_PKT_SNR_VALUE); + std::vector packet(bytes); + this->write_register_(REG_FIFO_ADDR_PTR, addr); + this->read_fifo_(packet); + if (this->frequency_ > 700000000) { + this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4); + } else { + this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4); + } + } + } else if (this->packet_mode_) { + std::vector packet; + uint8_t payload_length = this->payload_length_; + if (payload_length == 0) { + payload_length = this->read_register_(REG_FIFO); + } + packet.resize(payload_length); + this->read_fifo_(packet); + this->call_listeners_(packet, 0.0f, 0.0f); + } +} + +void SX127x::run_image_cal() { + uint32_t start = millis(); + uint8_t mode = this->read_register_(REG_OP_MODE); + if ((mode & MODE_MASK) != MODE_STDBY) { + ESP_LOGE(TAG, "Need to be in standby for image cal"); + return; + } + if (mode & MOD_LORA) { + this->set_mode_(MOD_FSK, MODE_SLEEP); + this->set_mode_(MOD_FSK, MODE_STDBY); + } + if (this->auto_cal_) { + this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START | AUTO_IMAGE_CAL_ON | TEMP_THRESHOLD_10C); + } else { + this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START); + } + while (this->read_register_(REG_IMAGE_CAL) & IMAGE_CAL_RUNNING) { + if (millis() - start > 20) { + ESP_LOGE(TAG, "Image cal failure"); + break; + } + } + if (mode & MOD_LORA) { + this->set_mode_(this->modulation_, MODE_SLEEP); + this->set_mode_(this->modulation_, MODE_STDBY); + } +} + +void SX127x::set_mode_(uint8_t modulation, uint8_t mode) { + uint32_t start = millis(); + this->write_register_(REG_OP_MODE, modulation | mode); + while (true) { + uint8_t curr = this->read_register_(REG_OP_MODE) & MODE_MASK; + if ((curr == mode) || (mode == MODE_RX && curr == MODE_RX_FS)) { + if (mode == MODE_SLEEP) { + this->write_register_(REG_OP_MODE, modulation | mode); + } + break; + } + if (millis() - start > 20) { + ESP_LOGE(TAG, "Set mode failure"); + break; + } + } +} + +void SX127x::set_mode_rx() { + this->set_mode_(this->modulation_, MODE_RX); + if (this->modulation_ == MOD_LORA) { + this->write_register_(REG_IRQ_FLAGS_MASK, 0x00); + this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00); + } +} + +void SX127x::set_mode_tx() { + this->set_mode_(this->modulation_, MODE_TX); + if (this->modulation_ == MOD_LORA) { + this->write_register_(REG_IRQ_FLAGS_MASK, 0x00); + this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_01); + } +} + +void SX127x::set_mode_standby() { this->set_mode_(this->modulation_, MODE_STDBY); } + +void SX127x::set_mode_sleep() { this->set_mode_(this->modulation_, MODE_SLEEP); } + +void SX127x::dump_config() { + ESP_LOGCONFIG(TAG, "SX127x:"); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" RST Pin: ", this->rst_pin_); + LOG_PIN(" DIO0 Pin: ", this->dio0_pin_); + const char *shaping = "NONE"; + if (this->shaping_ == CUTOFF_BR_X_2) { + shaping = "CUTOFF_BR_X_2"; + } else if (this->shaping_ == CUTOFF_BR_X_1) { + shaping = "CUTOFF_BR_X_1"; + } else if (this->shaping_ == GAUSSIAN_BT_0_3) { + shaping = "GAUSSIAN_BT_0_3"; + } else if (this->shaping_ == GAUSSIAN_BT_0_5) { + shaping = "GAUSSIAN_BT_0_5"; + } else if (this->shaping_ == GAUSSIAN_BT_1_0) { + shaping = "GAUSSIAN_BT_1_0"; + } + const char *pa_pin = "RFO"; + if (this->pa_pin_ == PA_PIN_BOOST) { + pa_pin = "BOOST"; + } + ESP_LOGCONFIG(TAG, + " Auto Cal: %s\n" + " Frequency: %" PRIu32 " Hz\n" + " Bandwidth: %" PRIu32 " Hz\n" + " PA Pin: %s\n" + " PA Power: %" PRIu8 " dBm\n" + " PA Ramp: %" PRIu16 " us\n" + " Shaping: %s", + TRUEFALSE(this->auto_cal_), this->frequency_, BW_HZ[this->bandwidth_], pa_pin, this->pa_power_, + RAMP[this->pa_ramp_], shaping); + if (this->modulation_ == MOD_FSK) { + ESP_LOGCONFIG(TAG, " Deviation: %" PRIu32 " Hz", this->deviation_); + } + if (this->modulation_ == MOD_LORA) { + const char *cr = "4/8"; + if (this->coding_rate_ == CODING_RATE_4_5) { + cr = "4/5"; + } else if (this->coding_rate_ == CODING_RATE_4_6) { + cr = "4/6"; + } else if (this->coding_rate_ == CODING_RATE_4_7) { + cr = "4/7"; + } + ESP_LOGCONFIG(TAG, + " Modulation: LORA\n" + " Preamble Size: %" PRIu16 "\n" + " Spreading Factor: %" PRIu8 "\n" + " Coding Rate: %s\n" + " CRC Enable: %s", + this->preamble_size_, this->spreading_factor_, cr, TRUEFALSE(this->crc_enable_)); + if (this->payload_length_ > 0) { + ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); + } + if (!this->sync_value_.empty()) { + ESP_LOGCONFIG(TAG, " Sync Value: 0x%02x", this->sync_value_[0]); + } + } else { + ESP_LOGCONFIG(TAG, + " Modulation: %s\n" + " Bitrate: %" PRIu32 "b/s\n" + " Bitsync: %s\n" + " Rx Start: %s\n" + " Rx Floor: %.1f dBm\n" + " Packet Mode: %s", + this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_), + TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_)); + if (this->packet_mode_) { + ESP_LOGCONFIG(TAG, " CRC Enable: %s", TRUEFALSE(this->crc_enable_)); + } + if (this->payload_length_ > 0) { + ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); + } + if (!this->sync_value_.empty()) { + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + } + if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) { + ESP_LOGCONFIG(TAG, + " Preamble Polarity: 0x%X\n" + " Preamble Size: %" PRIu16 "\n" + " Preamble Detect: %" PRIu8 "\n" + " Preamble Errors: %" PRIu8, + this->preamble_polarity_, this->preamble_size_, this->preamble_detect_, this->preamble_errors_); + } + } + if (this->is_failed()) { + ESP_LOGE(TAG, "Configuring SX127x failed"); + } +} + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h new file mode 100644 index 0000000000..fe9f60e860 --- /dev/null +++ b/esphome/components/sx127x/sx127x.h @@ -0,0 +1,125 @@ +#pragma once + +#include "sx127x_reg.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace sx127x { + +enum SX127xBw : uint8_t { + SX127X_BW_2_6, + SX127X_BW_3_1, + SX127X_BW_3_9, + SX127X_BW_5_2, + SX127X_BW_6_3, + SX127X_BW_7_8, + SX127X_BW_10_4, + SX127X_BW_12_5, + SX127X_BW_15_6, + SX127X_BW_20_8, + SX127X_BW_25_0, + SX127X_BW_31_3, + SX127X_BW_41_7, + SX127X_BW_50_0, + SX127X_BW_62_5, + SX127X_BW_83_3, + SX127X_BW_100_0, + SX127X_BW_125_0, + SX127X_BW_166_7, + SX127X_BW_200_0, + SX127X_BW_250_0, + SX127X_BW_500_0, +}; + +class SX127xListener { + public: + virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0; +}; + +class SX127x : public Component, + public spi::SPIDevice { + public: + size_t get_max_packet_size(); + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void setup() override; + void loop() override; + void dump_config() override; + void set_auto_cal(bool auto_cal) { this->auto_cal_ = auto_cal; } + void set_bandwidth(SX127xBw bandwidth) { this->bandwidth_ = bandwidth; } + void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } + void set_bitsync(bool bitsync) { this->bitsync_ = bitsync; } + void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } + void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } + void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } + void set_dio0_pin(InternalGPIOPin *dio0_pin) { this->dio0_pin_ = dio0_pin; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + void set_mode_rx(); + void set_mode_tx(); + void set_mode_standby(); + void set_mode_sleep(); + void set_modulation(uint8_t modulation) { this->modulation_ = modulation; } + void set_pa_pin(uint8_t pin) { this->pa_pin_ = pin; } + void set_pa_power(uint8_t power) { this->pa_power_ = power; } + void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; } + void set_packet_mode(bool packet_mode) { this->packet_mode_ = packet_mode; } + void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } + void set_preamble_errors(uint8_t preamble_errors) { this->preamble_errors_ = preamble_errors; } + void set_preamble_polarity(uint8_t preamble_polarity) { this->preamble_polarity_ = preamble_polarity; } + void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } + void set_preamble_detect(uint8_t preamble_detect) { this->preamble_detect_ = preamble_detect; } + void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } + void set_rx_floor(float floor) { this->rx_floor_ = floor; } + void set_rx_start(bool start) { this->rx_start_ = start; } + void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } + void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; } + void set_sync_value(const std::vector &sync_value) { this->sync_value_ = sync_value; } + void run_image_cal(); + void configure(); + void transmit_packet(const std::vector &packet); + void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); } + Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + + protected: + void configure_fsk_ook_(); + void configure_lora_(); + void set_mode_(uint8_t modulation, uint8_t mode); + void write_fifo_(const std::vector &packet); + void read_fifo_(std::vector &packet); + void write_register_(uint8_t reg, uint8_t value); + void call_listeners_(const std::vector &packet, float rssi, float snr); + uint8_t read_register_(uint8_t reg); + Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + std::vector listeners_; + std::vector sync_value_; + InternalGPIOPin *dio0_pin_{nullptr}; + InternalGPIOPin *rst_pin_{nullptr}; + SX127xBw bandwidth_; + uint32_t bitrate_; + uint32_t deviation_; + uint32_t frequency_; + uint32_t payload_length_; + uint16_t preamble_size_; + uint8_t coding_rate_; + uint8_t modulation_; + uint8_t pa_pin_; + uint8_t pa_power_; + uint8_t pa_ramp_; + uint8_t preamble_detect_; + uint8_t preamble_errors_; + uint8_t preamble_polarity_; + uint8_t shaping_; + uint8_t spreading_factor_; + float rx_floor_; + bool auto_cal_{false}; + bool bitsync_{false}; + bool crc_enable_{false}; + bool packet_mode_{false}; + bool rx_start_{false}; +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/sx127x_reg.h b/esphome/components/sx127x/sx127x_reg.h new file mode 100644 index 0000000000..d5e9c50957 --- /dev/null +++ b/esphome/components/sx127x/sx127x_reg.h @@ -0,0 +1,295 @@ +#pragma once + +#include "esphome/core/hal.h" + +namespace esphome { +namespace sx127x { + +enum SX127xReg : uint8_t { + // Common registers + REG_FIFO = 0x00, + REG_OP_MODE = 0x01, + REG_BITRATE_MSB = 0x02, + REG_BITRATE_LSB = 0x03, + REG_FDEV_MSB = 0x04, + REG_FDEV_LSB = 0x05, + REG_FRF_MSB = 0x06, + REG_FRF_MID = 0x07, + REG_FRF_LSB = 0x08, + REG_PA_CONFIG = 0x09, + REG_PA_RAMP = 0x0A, + REG_DIO_MAPPING1 = 0x40, + REG_DIO_MAPPING2 = 0x41, + REG_VERSION = 0x42, + // FSK/OOK registers + REG_RX_CONFIG = 0x0D, + REG_RSSI_THRESH = 0x10, + REG_RX_BW = 0x12, + REG_OOK_PEAK = 0x14, + REG_OOK_FIX = 0x15, + REG_OOK_AVG = 0x16, + REG_AFC_FEI = 0x1A, + REG_PREAMBLE_DETECT = 0x1F, + REG_PREAMBLE_SIZE_MSB = 0x25, + REG_PREAMBLE_SIZE_LSB = 0x26, + REG_SYNC_CONFIG = 0x27, + REG_SYNC_VALUE1 = 0x28, + REG_SYNC_VALUE2 = 0x29, + REG_SYNC_VALUE3 = 0x2A, + REG_SYNC_VALUE4 = 0x2B, + REG_SYNC_VALUE5 = 0x2C, + REG_SYNC_VALUE6 = 0x2D, + REG_SYNC_VALUE7 = 0x2E, + REG_SYNC_VALUE8 = 0x2F, + REG_PACKET_CONFIG_1 = 0x30, + REG_PACKET_CONFIG_2 = 0x31, + REG_PAYLOAD_LENGTH_LSB = 0x32, + REG_FIFO_THRESH = 0x35, + REG_IMAGE_CAL = 0x3B, + // LoRa registers + REG_FIFO_ADDR_PTR = 0x0D, + REG_FIFO_TX_BASE_ADDR = 0x0E, + REG_FIFO_RX_BASE_ADDR = 0x0F, + REG_FIFO_RX_CURR_ADDR = 0x10, + REG_IRQ_FLAGS_MASK = 0x11, + REG_IRQ_FLAGS = 0x12, + REG_NB_RX_BYTES = 0x13, + REG_MODEM_STAT = 0x18, + REG_PKT_SNR_VALUE = 0x19, + REG_PKT_RSSI_VALUE = 0x1A, + REG_RSSI_VALUE = 0x1B, + REG_HOP_CHANNEL = 0x1C, + REG_MODEM_CONFIG1 = 0x1D, + REG_MODEM_CONFIG2 = 0x1E, + REG_SYMB_TIMEOUT_LSB = 0x1F, + REG_PREAMBLE_LEN_MSB = 0x20, + REG_PREAMBLE_LEN_LSB = 0x21, + REG_PAYLOAD_LENGTH = 0x22, + REG_HOP_PERIOD = 0x24, + REG_FIFO_RX_BYTE_ADDR = 0x25, + REG_MODEM_CONFIG3 = 0x26, + REG_FEI_MSB = 0x28, + REG_FEI_MIB = 0x29, + REG_FEI_LSB = 0x2A, + REG_DETECT_OPTIMIZE = 0x31, + REG_INVERT_IQ = 0x33, + REG_DETECT_THRESHOLD = 0x37, + REG_SYNC_WORD = 0x39, +}; + +enum SX127xOpMode : uint8_t { + MOD_LORA = 0x80, + ACCESS_FSK_REGS = 0x40, + ACCESS_LORA_REGS = 0x00, + MOD_OOK = 0x20, + MOD_FSK = 0x00, + ACCESS_LF_REGS = 0x08, + ACCESS_HF_REGS = 0x00, + MODE_CAD = 0x07, + MODE_RX_SINGLE = 0x06, + MODE_RX = 0x05, + MODE_RX_FS = 0x04, + MODE_TX = 0x03, + MODE_TX_FS = 0x02, + MODE_STDBY = 0x01, + MODE_SLEEP = 0x00, + MODE_MASK = 0x07, +}; + +enum SX127xPaConfig : uint8_t { + PA_PIN_BOOST = 0x80, + PA_PIN_RFO = 0x00, + PA_MAX_POWER = 0x70, +}; + +enum SX127xPaRamp : uint8_t { + CUTOFF_BR_X_2 = 0x40, + CUTOFF_BR_X_1 = 0x20, + GAUSSIAN_BT_0_3 = 0x60, + GAUSSIAN_BT_0_5 = 0x40, + GAUSSIAN_BT_1_0 = 0x20, + SHAPING_NONE = 0x00, + PA_RAMP_10 = 0x0F, + PA_RAMP_12 = 0x0E, + PA_RAMP_15 = 0x0D, + PA_RAMP_20 = 0x0C, + PA_RAMP_25 = 0x0B, + PA_RAMP_31 = 0x0A, + PA_RAMP_40 = 0x09, + PA_RAMP_50 = 0x08, + PA_RAMP_62 = 0x07, + PA_RAMP_100 = 0x06, + PA_RAMP_125 = 0x05, + PA_RAMP_250 = 0x04, + PA_RAMP_500 = 0x03, + PA_RAMP_1000 = 0x02, + PA_RAMP_2000 = 0x01, + PA_RAMP_3400 = 0x00, +}; + +enum SX127xDioMapping1 : uint8_t { + DIO0_MAPPING_00 = 0x00, + DIO0_MAPPING_01 = 0x40, + DIO0_MAPPING_10 = 0x80, + DIO0_MAPPING_11 = 0xC0, +}; + +enum SX127xRxConfig : uint8_t { + RESTART_ON_COLLISION = 0x80, + RESTART_NO_LOCK = 0x40, + RESTART_PLL_LOCK = 0x20, + AFC_AUTO_ON = 0x10, + AGC_AUTO_ON = 0x08, + TRIGGER_NONE = 0x00, + TRIGGER_RSSI = 0x01, + TRIGGER_PREAMBLE = 0x06, + TRIGGER_ALL = 0x07, +}; + +enum SX127xRxBw : uint8_t { + RX_BW_2_6 = 0x17, + RX_BW_3_1 = 0x0F, + RX_BW_3_9 = 0x07, + RX_BW_5_2 = 0x16, + RX_BW_6_3 = 0x0E, + RX_BW_7_8 = 0x06, + RX_BW_10_4 = 0x15, + RX_BW_12_5 = 0x0D, + RX_BW_15_6 = 0x05, + RX_BW_20_8 = 0x14, + RX_BW_25_0 = 0x0C, + RX_BW_31_3 = 0x04, + RX_BW_41_7 = 0x13, + RX_BW_50_0 = 0x0B, + RX_BW_62_5 = 0x03, + RX_BW_83_3 = 0x12, + RX_BW_100_0 = 0x0A, + RX_BW_125_0 = 0x02, + RX_BW_166_7 = 0x11, + RX_BW_200_0 = 0x09, + RX_BW_250_0 = 0x01, +}; + +enum SX127xOokPeak : uint8_t { + BIT_SYNC_ON = 0x20, + BIT_SYNC_OFF = 0x00, + OOK_THRESH_AVG = 0x10, + OOK_THRESH_PEAK = 0x08, + OOK_THRESH_FIXED = 0x00, + OOK_THRESH_STEP_6_0 = 0x07, + OOK_THRESH_STEP_5_0 = 0x06, + OOK_THRESH_STEP_4_0 = 0x05, + OOK_THRESH_STEP_3_0 = 0x04, + OOK_THRESH_STEP_2_0 = 0x03, + OOK_THRESH_STEP_1_5 = 0x02, + OOK_THRESH_STEP_1_0 = 0x01, + OOK_THRESH_STEP_0_5 = 0x00, +}; + +enum SX127xOokAvg : uint8_t { + OOK_THRESH_DEC_16 = 0xE0, + OOK_THRESH_DEC_8 = 0xC0, + OOK_THRESH_DEC_4 = 0xA0, + OOK_THRESH_DEC_2 = 0x80, + OOK_THRESH_DEC_1_8 = 0x60, + OOK_THRESH_DEC_1_4 = 0x40, + OOK_THRESH_DEC_1_2 = 0x20, + OOK_THRESH_DEC_1 = 0x00, + OOK_AVG_RESERVED = 0x10, +}; + +enum SX127xAfcFei : uint8_t { + AFC_AUTO_CLEAR_ON = 0x01, +}; + +enum SX127xPreambleDetect : uint8_t { + PREAMBLE_DETECTOR_ON = 0x80, + PREAMBLE_DETECTOR_OFF = 0x00, + PREAMBLE_DETECTOR_SIZE_SHIFT = 5, + PREAMBLE_DETECTOR_TOL_SHIFT = 0, +}; + +enum SX127xSyncConfig : uint8_t { + AUTO_RESTART_PLL_LOCK = 0x80, + AUTO_RESTART_NO_LOCK = 0x40, + AUTO_RESTART_OFF = 0x00, + PREAMBLE_55 = 0x20, + PREAMBLE_AA = 0x00, + SYNC_ON = 0x10, + SYNC_OFF = 0x00, +}; + +enum SX127xPacketConfig1 : uint8_t { + VARIABLE_LENGTH = 0x80, + FIXED_LENGTH = 0x00, + CRC_ON = 0x10, + CRC_OFF = 0x00, +}; + +enum SX127xPacketConfig2 : uint8_t { + CONTINUOUS_MODE = 0x00, + PACKET_MODE = 0x40, +}; + +enum SX127xFifoThresh : uint8_t { + TX_START_FIFO_EMPTY = 0x80, + TX_START_FIFO_LEVEL = 0x00, +}; + +enum SX127xImageCal : uint8_t { + AUTO_IMAGE_CAL_ON = 0x80, + IMAGE_CAL_START = 0x40, + IMAGE_CAL_RUNNING = 0x20, + TEMP_CHANGE = 0x08, + TEMP_THRESHOLD_20C = 0x06, + TEMP_THRESHOLD_15C = 0x04, + TEMP_THRESHOLD_10C = 0x02, + TEMP_THRESHOLD_5C = 0x00, + TEMP_MONITOR_OFF = 0x01, + TEMP_MONITOR_ON = 0x00, +}; + +enum SX127xIrqFlags : uint8_t { + RX_TIMEOUT = 0x80, + RX_DONE = 0x40, + PAYLOAD_CRC_ERROR = 0x20, + VALID_HEADER = 0x10, + TX_DONE = 0x08, + CAD_DONE = 0x04, + FHSS_CHANGE_CHANNEL = 0x02, + CAD_DETECTED = 0x01, +}; + +enum SX127xModemCfg1 : uint8_t { + BW_7_8 = 0x00, + BW_10_4 = 0x10, + BW_15_6 = 0x20, + BW_20_8 = 0x30, + BW_31_3 = 0x40, + BW_41_7 = 0x50, + BW_62_5 = 0x60, + BW_125_0 = 0x70, + BW_250_0 = 0x80, + BW_500_0 = 0x90, + CODING_RATE_4_5 = 0x02, + CODING_RATE_4_6 = 0x04, + CODING_RATE_4_7 = 0x06, + CODING_RATE_4_8 = 0x08, + IMPLICIT_HEADER = 0x01, + EXPLICIT_HEADER = 0x00, +}; + +enum SX127xModemCfg2 : uint8_t { + SPREADING_FACTOR_SHIFT = 4, + TX_CONTINOUS_MODE = 0x08, + RX_PAYLOAD_CRC_ON = 0x04, + RX_PAYLOAD_CRC_OFF = 0x00, +}; + +enum SX127xModemCfg3 : uint8_t { + LOW_DATA_RATE_OPTIMIZE_ON = 0x08, + MODEM_AGC_AUTO_ON = 0x04, +}; + +} // namespace sx127x +} // namespace esphome diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml new file mode 100644 index 0000000000..63adc2e91c --- /dev/null +++ b/tests/components/sx127x/common.yaml @@ -0,0 +1,45 @@ +spi: + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sx127x: + cs_pin: ${cs_pin} + rst_pin: ${rst_pin} + dio0_pin: ${dio0_pin} + pa_pin: BOOST + pa_power: 17 + pa_ramp: 40us + bitsync: true + bitrate: 4800 + bandwidth: 50_0kHz + frequency: 433920000 + modulation: FSK + deviation: 5000 + rx_start: true + rx_floor: -90 + packet_mode: true + payload_length: 8 + sync_value: [0x33, 0x33] + shaping: NONE + preamble_size: 2 + preamble_detect: 2 + preamble_errors: 8 + preamble_polarity: 0x55 + on_packet: + then: + - sx127x.send_packet: + data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + +button: + - platform: template + name: "SX127x Button" + on_press: + then: + - sx127x.set_mode_standby + - sx127x.run_image_cal + - sx127x.set_mode_tx + - sx127x.set_mode_sleep + - sx127x.set_mode_rx + - sx127x.send_packet: + data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] diff --git a/tests/components/sx127x/test.esp32-ard.yaml b/tests/components/sx127x/test.esp32-ard.yaml new file mode 100644 index 0000000000..71270462a2 --- /dev/null +++ b/tests/components/sx127x/test.esp32-ard.yaml @@ -0,0 +1,9 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO27 + miso_pin: GPIO19 + cs_pin: GPIO18 + rst_pin: GPIO23 + dio0_pin: GPIO26 + +<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-c3-ard.yaml b/tests/components/sx127x/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..36535a950d --- /dev/null +++ b/tests/components/sx127x/test.esp32-c3-ard.yaml @@ -0,0 +1,9 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO18 + miso_pin: GPIO19 + cs_pin: GPIO1 + rst_pin: GPIO2 + dio0_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-c3-idf.yaml b/tests/components/sx127x/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..36535a950d --- /dev/null +++ b/tests/components/sx127x/test.esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO18 + miso_pin: GPIO19 + cs_pin: GPIO1 + rst_pin: GPIO2 + dio0_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-idf.yaml b/tests/components/sx127x/test.esp32-idf.yaml new file mode 100644 index 0000000000..71270462a2 --- /dev/null +++ b/tests/components/sx127x/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO27 + miso_pin: GPIO19 + cs_pin: GPIO18 + rst_pin: GPIO23 + dio0_pin: GPIO26 + +<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp8266-ard.yaml b/tests/components/sx127x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..64c01edd44 --- /dev/null +++ b/tests/components/sx127x/test.esp8266-ard.yaml @@ -0,0 +1,9 @@ +substitutions: + clk_pin: GPIO5 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO1 + rst_pin: GPIO2 + dio0_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/sx127x/test.rp2040-ard.yaml b/tests/components/sx127x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..0af7b29790 --- /dev/null +++ b/tests/components/sx127x/test.rp2040-ard.yaml @@ -0,0 +1,9 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + rst_pin: GPIO6 + dio0_pin: GPIO7 + +<<: !include common.yaml