351 lines
13 KiB
Python
351 lines
13 KiB
Python
from esphome import automation, pins
|
|
import esphome.codegen as cg
|
|
from esphome.components import spi
|
|
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
|
import esphome.config_validation as cv
|
|
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
|
|
from esphome.core import 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_DEVIATION = "deviation"
|
|
CONF_DIO0_PIN = "dio0_pin"
|
|
CONF_MODULATION = "modulation"
|
|
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 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="5kHz"): cv.All(
|
|
cv.frequency, cv.float_range(min=0, max=100000)
|
|
),
|
|
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
|
|
cv.Required(CONF_FREQUENCY): cv.All(
|
|
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
|
),
|
|
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,
|
|
synchronous=True,
|
|
)
|
|
@automation.register_action(
|
|
"sx127x.set_mode_tx",
|
|
SetModeTxAction,
|
|
NO_ARGS_ACTION_SCHEMA,
|
|
synchronous=True,
|
|
)
|
|
@automation.register_action(
|
|
"sx127x.set_mode_rx",
|
|
SetModeRxAction,
|
|
NO_ARGS_ACTION_SCHEMA,
|
|
synchronous=True,
|
|
)
|
|
@automation.register_action(
|
|
"sx127x.set_mode_sleep",
|
|
SetModeSleepAction,
|
|
NO_ARGS_ACTION_SCHEMA,
|
|
synchronous=True,
|
|
)
|
|
@automation.register_action(
|
|
"sx127x.set_mode_standby",
|
|
SetModeStandbyAction,
|
|
NO_ARGS_ACTION_SCHEMA,
|
|
synchronous=True,
|
|
)
|
|
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,
|
|
synchronous=True,
|
|
)
|
|
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:
|
|
# Generate static array in flash to avoid RAM copy
|
|
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
|
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
|
cg.add(var.set_data_static(arr, len(data)))
|
|
return var
|