Merge branch '20250707-ld2450-clean-up' into integration

This commit is contained in:
J. Nick Koston 2025-07-07 07:18:12 -05:00
commit da5fb6e24f
No known key found for this signature in database
30 changed files with 1739 additions and 284 deletions

View File

@ -443,6 +443,7 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes

View File

@ -1,6 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import display, i2c
from esphome.components.esp32 import CONF_CPU_FREQUENCY
import esphome.config_validation as cv
from esphome.const import (
CONF_FULL_UPDATE_EVERY,
@ -13,7 +14,9 @@ from esphome.const import (
CONF_PAGES,
CONF_TRANSFORM,
CONF_WAKEUP_PIN,
PLATFORM_ESP32,
)
import esphome.final_validate as fv
DEPENDENCIES = ["i2c", "esp32"]
AUTO_LOAD = ["psram"]
@ -120,6 +123,18 @@ CONFIG_SCHEMA = cv.All(
)
def _validate_cpu_frequency(config):
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
raise cv.Invalid(
"Inkplate requires 240MHz CPU frequency (set in esp32 component)"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])

View File

@ -13,13 +13,13 @@ from esphome.const import (
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
ResetButton = ld2450_ns.class_("ResetButton", button.Button)
FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button)
RestartButton = ld2450_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
ResetButton,
FactoryResetButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
@ -38,7 +38,7 @@ async def to_code(config):
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_reset_button(b))
cg.add(ld2450_component.set_factory_reset_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2450_ID])

View File

@ -0,0 +1,9 @@
#include "factory_reset_button.h"
namespace esphome {
namespace ld2450 {
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace ld2450
} // namespace esphome

View File

@ -6,9 +6,9 @@
namespace esphome {
namespace ld2450 {
class ResetButton : public button::Button, public Parented<LD2450Component> {
class FactoryResetButton : public button::Button, public Parented<LD2450Component> {
public:
ResetButton() = default;
FactoryResetButton() = default;
protected:
void press_action() override;

View File

@ -1,9 +0,0 @@
#include "reset_button.h"
namespace esphome {
namespace ld2450 {
void ResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace ld2450
} // namespace esphome

View File

@ -18,11 +18,10 @@ namespace esphome {
namespace ld2450 {
static const char *const TAG = "ld2450";
static const char *const NO_MAC = "08:05:04:03:02:01";
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
enum BaudRateStructure : uint8_t {
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
@ -33,14 +32,13 @@ enum BaudRateStructure : uint8_t {
BAUD_RATE_460800 = 8
};
// Zone type struct
enum ZoneTypeStructure : uint8_t {
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
enum PeriodicDataStructure : uint8_t {
enum PeriodicData : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
TARGET_SPEED = 8,
@ -48,12 +46,12 @@ enum PeriodicDataStructure : uint8_t {
};
enum PeriodicDataValue : uint8_t {
HEAD = 0xAA,
END = 0x55,
HEADER = 0xAA,
FOOTER = 0x55,
CHECK = 0x00,
};
enum AckDataStructure : uint8_t {
enum AckData : uint8_t {
COMMAND = 6,
COMMAND_STATUS = 7,
};
@ -61,11 +59,11 @@ enum AckDataStructure : uint8_t {
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
uint8_t value;
const uint8_t value;
};
struct Uint8ToString {
uint8_t value;
const uint8_t value;
const char *str;
};
@ -75,6 +73,13 @@ constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
{DIRECTION_APPROACHING, "Approaching"},
{DIRECTION_MOVING_AWAY, "Moving away"},
{DIRECTION_STATIONARY, "Stationary"},
{DIRECTION_NA, "NA"},
};
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
{ZONE_DISABLED, "Disabled"},
{ZONE_DETECTION, "Detection"},
@ -104,28 +109,35 @@ template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t v
return ""; // Not found
}
// LD2450 serial command header & footer
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
// LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0xFF;
static const uint8_t CMD_DISABLE_CONF = 0xFE;
static const uint8_t CMD_VERSION = 0xA0;
static const uint8_t CMD_MAC = 0xA5;
static const uint8_t CMD_RESET = 0xA2;
static const uint8_t CMD_RESTART = 0xA3;
static const uint8_t CMD_BLUETOOTH = 0xA4;
static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
static const uint8_t CMD_MULTI_TARGET_MODE = 0x90;
static const uint8_t CMD_QUERY_TARGET_MODE = 0x91;
static const uint8_t CMD_SET_BAUD_RATE = 0xA1;
static const uint8_t CMD_QUERY_ZONE = 0xC1;
static const uint8_t CMD_SET_ZONE = 0xC2;
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
static constexpr uint8_t CMD_RESET = 0xA2;
static constexpr uint8_t CMD_RESTART = 0xA3;
static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90;
static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91;
static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
static constexpr uint8_t CMD_QUERY_ZONE = 0xC1;
static constexpr uint8_t CMD_SET_ZONE = 0xC2;
// Header & Footer size
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
// Command Header & Footer
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
// Data Header & Footer
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00};
static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
for (int i = 0; i < 4; i++) {
for (uint8_t i = 0; i < 4; i++) {
uint16_t val = values[i] & 0xFFFF;
bytes[i * 2] = val & 0xFF; // Store low byte first (little-endian)
bytes[i * 2 + 1] = (val >> 8) & 0xFF; // Store high byte second
@ -166,18 +178,13 @@ static inline float calculate_angle(float base, float hypotenuse) {
return angle_degrees;
}
static inline std::string get_direction(int16_t speed) {
static const char *const APPROACHING = "Approaching";
static const char *const MOVING_AWAY = "Moving away";
static const char *const STATIONARY = "Stationary";
if (speed > 0) {
return MOVING_AWAY;
static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) {
if (header_footer[i] != buffer[i]) {
return false; // Mismatch in header/footer
}
}
if (speed < 0) {
return APPROACHING;
}
return STATIONARY;
return true; // Valid header/footer
}
void LD2450Component::setup() {
@ -192,84 +199,93 @@ void LD2450Component::setup() {
}
void LD2450Component::dump_config() {
ESP_LOGCONFIG(TAG, "LD2450:");
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGCONFIG(TAG,
"LD2450:\n"
" Firmware version: %s\n"
" MAC address: %s\n"
" Throttle: %u ms",
version.c_str(), mac_str.c_str(), this->throttle_);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_);
#endif
#ifdef USE_SWITCH
LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_);
LOG_SWITCH(" ", "MultiTargetSwitch", this->multi_target_switch_);
#endif
#ifdef USE_BUTTON
LOG_BUTTON(" ", "ResetButton", this->reset_button_);
LOG_BUTTON(" ", "RestartButton", this->restart_button_);
ESP_LOGCONFIG(TAG, "Binary Sensors:");
LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_);
LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_);
LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_);
ESP_LOGCONFIG(TAG, "Sensors:");
LOG_SENSOR(" ", "MovingTargetCount", this->moving_target_count_sensor_);
LOG_SENSOR(" ", "StillTargetCount", this->still_target_count_sensor_);
LOG_SENSOR(" ", "TargetCount", this->target_count_sensor_);
for (sensor::Sensor *s : this->move_x_sensors_) {
LOG_SENSOR(" ", "NthTargetXSensor", s);
LOG_SENSOR(" ", "TargetX", s);
}
for (sensor::Sensor *s : this->move_y_sensors_) {
LOG_SENSOR(" ", "NthTargetYSensor", s);
}
for (sensor::Sensor *s : this->move_speed_sensors_) {
LOG_SENSOR(" ", "NthTargetSpeedSensor", s);
LOG_SENSOR(" ", "TargetY", s);
}
for (sensor::Sensor *s : this->move_angle_sensors_) {
LOG_SENSOR(" ", "NthTargetAngleSensor", s);
LOG_SENSOR(" ", "TargetAngle", s);
}
for (sensor::Sensor *s : this->move_distance_sensors_) {
LOG_SENSOR(" ", "NthTargetDistanceSensor", s);
LOG_SENSOR(" ", "TargetDistance", s);
}
for (sensor::Sensor *s : this->move_resolution_sensors_) {
LOG_SENSOR(" ", "NthTargetResolutionSensor", s);
LOG_SENSOR(" ", "TargetResolution", s);
}
for (sensor::Sensor *s : this->move_speed_sensors_) {
LOG_SENSOR(" ", "TargetSpeed", s);
}
for (sensor::Sensor *s : this->zone_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneTargetCountSensor", s);
}
for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s);
LOG_SENSOR(" ", "ZoneTargetCount", s);
}
for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneMovingTargetCountSensor", s);
LOG_SENSOR(" ", "ZoneMovingTargetCount", s);
}
for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
LOG_SENSOR(" ", "ZoneStillTargetCount", s);
}
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_);
LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_);
ESP_LOGCONFIG(TAG, "Text Sensors:");
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
LOG_TEXT_SENSOR(" ", "Mac", this->mac_text_sensor_);
for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s);
LOG_TEXT_SENSOR(" ", "Direction", s);
}
#endif
#ifdef USE_NUMBER
ESP_LOGCONFIG(TAG, "Numbers:");
LOG_NUMBER(" ", "PresenceTimeout", this->presence_timeout_number_);
for (auto n : this->zone_numbers_) {
LOG_NUMBER(" ", "ZoneX1Number", n.x1);
LOG_NUMBER(" ", "ZoneY1Number", n.y1);
LOG_NUMBER(" ", "ZoneX2Number", n.x2);
LOG_NUMBER(" ", "ZoneY2Number", n.y2);
LOG_NUMBER(" ", "ZoneX1", n.x1);
LOG_NUMBER(" ", "ZoneY1", n.y1);
LOG_NUMBER(" ", "ZoneX2", n.x2);
LOG_NUMBER(" ", "ZoneY2", n.y2);
}
#endif
#ifdef USE_SELECT
LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_);
LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_);
ESP_LOGCONFIG(TAG, "Selects:");
LOG_SELECT(" ", "BaudRate", this->baud_rate_select_);
LOG_SELECT(" ", "ZoneType", this->zone_type_select_);
#endif
#ifdef USE_NUMBER
LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_);
#ifdef USE_SWITCH
ESP_LOGCONFIG(TAG, "Switches:");
LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_);
LOG_SWITCH(" ", "MultiTarget", this->multi_target_switch_);
#endif
#ifdef USE_BUTTON
ESP_LOGCONFIG(TAG, "Buttons:");
LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
LOG_BUTTON(" ", "Restart", this->restart_button_);
#endif
ESP_LOGCONFIG(TAG,
" Throttle: %ums\n"
" MAC Address: %s\n"
" Firmware version: %s",
this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
}
void LD2450Component::loop() {
while (this->available()) {
this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH);
this->readline_(this->read());
}
}
@ -304,7 +320,7 @@ void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_
this->zone_type_ = zone_type;
int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
for (int i = 0; i < MAX_ZONES; i++) {
for (uint8_t i = 0; i < MAX_ZONES; i++) {
this->zone_config_[i].x1 = zone_parameters[i * 4];
this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
@ -318,15 +334,15 @@ void LD2450Component::send_set_zone_command_() {
uint8_t cmd_value[26] = {};
uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
uint8_t area_config[24] = {};
for (int i = 0; i < MAX_ZONES; i++) {
for (uint8_t i = 0; i < MAX_ZONES; i++) {
int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
this->zone_config_[i].y2};
ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
}
std::memcpy(cmd_value, zone_type_bytes, 2);
std::memcpy(cmd_value + 2, area_config, 24);
std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
this->set_config_mode_(true);
this->send_command_(CMD_SET_ZONE, cmd_value, 26);
this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
this->set_config_mode_(false);
}
@ -342,14 +358,14 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
}
// Extract, store and publish zone details LD2450 buffer
void LD2450Component::process_zone_(uint8_t *buffer) {
void LD2450Component::process_zone_() {
uint8_t index, start;
for (index = 0; index < MAX_ZONES; index++) {
start = 12 + index * 8;
this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start);
this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2);
this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
#ifdef USE_NUMBER
// only one null check as all coordinates are required for a single zone
if (this->zone_numbers_[index].x1 != nullptr) {
@ -395,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() {
// Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending command %02X", command);
// frame header
this->write_array(CMD_FRAME_HEADER, 4);
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
// frame header bytes
this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
// length bytes
int len = 2;
uint8_t len = 2;
if (command_value != nullptr) {
len += command_value_len;
}
this->write_byte(lowbyte(len));
this->write_byte(highbyte(len));
// command
this->write_byte(lowbyte(command));
this->write_byte(highbyte(command));
uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00};
this->write_array(len_cmd, sizeof(len_cmd));
// command value bytes
if (command_value != nullptr) {
for (int i = 0; i < command_value_len; i++) {
for (uint8_t i = 0; i < command_value_len; i++) {
this->write_byte(command_value[i]);
}
}
// footer
this->write_array(CMD_FRAME_END, 4);
// frame footer bytes
this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
// FIXME to remove
delay(50); // NOLINT
}
@ -423,26 +437,23 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
// LD2450 Radar data message:
// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
// Header Target 1 Target 2 Target 3 End
void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
void LD2450Component::handle_periodic_data_() {
// Early throttle check - moved before any processing to save CPU cycles
if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
return;
}
if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Invalid message length");
if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Invalid length");
return;
}
if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header
ESP_LOGE(TAG, "Invalid message header");
if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
ESP_LOGE(TAG, "Invalid header/footer");
return;
}
if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer
ESP_LOGE(TAG, "Invalid message footer");
return;
}
// Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
this->last_periodic_millis_ = App.get_loop_component_start_time();
int16_t target_count = 0;
@ -450,13 +461,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
int16_t moving_target_count = 0;
int16_t start = 0;
int16_t val = 0;
uint8_t index = 0;
int16_t tx = 0;
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
int16_t angle = 0;
std::string direction{};
uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false;
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
@ -468,7 +479,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
is_moving = false;
sensor::Sensor *sx = this->move_x_sensors_[index];
if (sx != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
tx = val;
if (this->cached_target_data_[index].x != val) {
sx->publish_state(val);
@ -479,7 +490,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
start = TARGET_Y + index * 8;
sensor::Sensor *sy = this->move_y_sensors_[index];
if (sy != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
ty = val;
if (this->cached_target_data_[index].y != val) {
sy->publish_state(val);
@ -490,7 +501,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
start = TARGET_RESOLUTION + index * 8;
sensor::Sensor *sr = this->move_resolution_sensors_[index];
if (sr != nullptr) {
val = (buffer[start + 1] << 8) | buffer[start];
val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
if (this->cached_target_data_[index].resolution != val) {
sr->publish_state(val);
this->cached_target_data_[index].resolution = val;
@ -499,7 +510,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#endif
// SPEED
start = TARGET_SPEED + index * 8;
val = ld2450::decode_speed(buffer[start], buffer[start + 1]);
val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
ts = val;
if (val) {
is_moving = true;
@ -532,7 +543,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
}
}
// ANGLE
angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td));
angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
@ -547,14 +558,19 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#endif
#ifdef USE_TEXT_SENSOR
// DIRECTION
direction = get_direction(ts);
if (td == 0) {
direction = "NA";
direction = DIRECTION_NA;
} else if (ts > 0) {
direction = DIRECTION_MOVING_AWAY;
} else if (ts < 0) {
direction = DIRECTION_APPROACHING;
} else {
direction = DIRECTION_STATIONARY;
}
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
if (this->cached_target_data_[index].direction != direction) {
tsd->publish_state(direction);
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
this->cached_target_data_[index].direction = direction;
}
}
@ -678,117 +694,139 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#endif
}
bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]);
if (len < 10) {
ESP_LOGE(TAG, "Invalid ack length");
bool LD2450Component::handle_ack_data_() {
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
if (this->buffer_pos_ < 10) {
ESP_LOGE(TAG, "Invalid length");
return true;
}
if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header
ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]);
if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
return true;
}
if (buffer[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Invalid ack status");
if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Invalid status");
return true;
}
if (buffer[8] || buffer[9]) {
ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]);
if (this->buffer_data_[8] || this->buffer_data_[9]) {
ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
return true;
}
switch (buffer[COMMAND]) {
case lowbyte(CMD_ENABLE_CONF):
ESP_LOGV(TAG, "Enable conf command");
switch (this->buffer_data_[COMMAND]) {
case CMD_ENABLE_CONF:
ESP_LOGV(TAG, "Enable conf");
break;
case lowbyte(CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Disable conf command");
case CMD_DISABLE_CONF:
ESP_LOGV(TAG, "Disabled conf");
break;
case lowbyte(CMD_SET_BAUD_RATE):
ESP_LOGV(TAG, "Baud rate change command");
case CMD_SET_BAUD_RATE:
ESP_LOGV(TAG, "Baud rate change");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str());
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
}
#endif
break;
case lowbyte(CMD_VERSION):
this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
case CMD_QUERY_VERSION: {
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
this->version_[4], this->version_[3], this->version_[2]);
ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(this->version_);
this->version_text_sensor_->publish_state(version);
}
#endif
break;
case lowbyte(CMD_MAC):
if (len < 20) {
}
case CMD_QUERY_MAC_ADDRESS: {
if (this->buffer_pos_ < 20) {
return false;
}
this->mac_ = format_mac_address_pretty(&buffer[10]);
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
if (this->bluetooth_on_) {
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
}
std::string mac_str =
mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
this->mac_text_sensor_->publish_state(mac_str);
}
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
this->bluetooth_switch_->publish_state(this->bluetooth_on_);
}
#endif
break;
case lowbyte(CMD_BLUETOOTH):
ESP_LOGV(TAG, "Bluetooth command");
}
case CMD_BLUETOOTH:
ESP_LOGV(TAG, "Bluetooth");
break;
case lowbyte(CMD_SINGLE_TARGET_MODE):
ESP_LOGV(TAG, "Single target conf command");
case CMD_SINGLE_TARGET_MODE:
ESP_LOGV(TAG, "Single target conf");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(false);
}
#endif
break;
case lowbyte(CMD_MULTI_TARGET_MODE):
ESP_LOGV(TAG, "Multi target conf command");
case CMD_MULTI_TARGET_MODE:
ESP_LOGV(TAG, "Multi target conf");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(true);
}
#endif
break;
case lowbyte(CMD_QUERY_TARGET_MODE):
ESP_LOGV(TAG, "Query target tracking mode command");
case CMD_QUERY_TARGET_MODE:
ESP_LOGV(TAG, "Query target tracking mode");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(buffer[10] == 0x02);
this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
}
#endif
break;
case lowbyte(CMD_QUERY_ZONE):
ESP_LOGV(TAG, "Query zone conf command");
this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16);
case CMD_QUERY_ZONE:
ESP_LOGV(TAG, "Query zone conf");
this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16);
this->publish_zone_type();
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
}
#endif
if (buffer[10] == 0x00) {
if (this->buffer_data_[10] == 0x00) {
ESP_LOGV(TAG, "Zone: Disabled");
}
if (buffer[10] == 0x01) {
if (this->buffer_data_[10] == 0x01) {
ESP_LOGV(TAG, "Zone: Area detection");
}
if (buffer[10] == 0x02) {
if (this->buffer_data_[10] == 0x02) {
ESP_LOGV(TAG, "Zone: Area filter");
}
this->process_zone_(buffer);
this->process_zone_();
break;
case lowbyte(CMD_SET_ZONE):
ESP_LOGV(TAG, "Set zone conf command");
case CMD_SET_ZONE:
ESP_LOGV(TAG, "Set zone conf");
this->query_zone_info();
break;
default:
break;
}
@ -796,55 +834,57 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
}
// Read LD2450 buffer data
void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) {
void LD2450Component::readline_(int readch) {
if (readch < 0) {
return;
return; // No data available
}
if (this->buffer_pos_ < len - 1) {
buffer[this->buffer_pos_++] = readch;
buffer[this->buffer_pos_] = 0;
if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
this->buffer_data_[this->buffer_pos_++] = readch;
this->buffer_data_[this->buffer_pos_] = 0;
} else {
// We should never get here, but just in case...
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
this->buffer_pos_ = 0;
}
if (this->buffer_pos_ < 4) {
return;
return; // Not enough data to process yet
}
if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) {
ESP_LOGV(TAG, "Handle periodic radar data");
this->handle_periodic_data_(buffer, this->buffer_pos_);
if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
this->handle_periodic_data_();
this->buffer_pos_ = 0; // Reset position index for next frame
} else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 &&
buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) {
ESP_LOGV(TAG, "Handle command ack data");
if (this->handle_ack_data_(buffer, this->buffer_pos_)) {
this->buffer_pos_ = 0; // Reset position index for next frame
} else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
if (this->handle_ack_data_()) {
this->buffer_pos_ = 0; // Reset position index for next message
} else {
ESP_LOGV(TAG, "Command ack data invalid");
ESP_LOGV(TAG, "Ack Data incomplete");
}
}
}
// Set Config Mode - Pre-requisite sending commands
void LD2450Component::set_config_mode_(bool enable) {
uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
const uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
}
// Set Bluetooth Enable/Disable
void LD2450Component::set_bluetooth(bool enable) {
this->set_config_mode_(true);
uint8_t enable_cmd_value[2] = {0x01, 0x00};
uint8_t disable_cmd_value[2] = {0x00, 0x00};
this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
// Set Baud rate
void LD2450Component::set_baud_rate(const std::string &state) {
this->set_config_mode_(true);
uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@ -885,12 +925,12 @@ void LD2450Component::factory_reset() {
void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
// Get LD2450 firmware version
void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
// Get LD2450 mac address
void LD2450Component::get_mac_() {
uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(CMD_MAC, cmd_value, 2);
this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
}
// Query for target tracking mode

View File

@ -38,10 +38,18 @@ namespace ld2450 {
// Constants
static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec.
static const uint8_t MAX_LINE_LENGTH = 60; // Max characters for serial buffer
static const uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer
static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
enum Direction : uint8_t {
DIRECTION_APPROACHING = 0,
DIRECTION_MOVING_AWAY = 1,
DIRECTION_STATIONARY = 2,
DIRECTION_NA = 3,
DIRECTION_UNDEFINED = 4,
};
// Target coordinate struct
struct Target {
int16_t x;
@ -67,19 +75,22 @@ struct ZoneOfNumbers {
#endif
class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
SUB_SENSOR(target_count)
SUB_SENSOR(still_target_count)
SUB_SENSOR(moving_target_count)
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(target)
SUB_BINARY_SENSOR(moving_target)
SUB_BINARY_SENSOR(still_target)
SUB_BINARY_SENSOR(target)
#endif
#ifdef USE_SENSOR
SUB_SENSOR(moving_target_count)
SUB_SENSOR(still_target_count)
SUB_SENSOR(target_count)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
SUB_TEXT_SENSOR(mac)
SUB_TEXT_SENSOR(version)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(presence_timeout)
#endif
#ifdef USE_SELECT
SUB_SELECT(baud_rate)
@ -90,12 +101,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
SUB_SWITCH(multi_target)
#endif
#ifdef USE_BUTTON
SUB_BUTTON(reset)
SUB_BUTTON(factory_reset)
SUB_BUTTON(restart)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(presence_timeout)
#endif
public:
void setup() override;
@ -138,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable);
void handle_periodic_data_(uint8_t *buffer, uint8_t len);
bool handle_ack_data_(uint8_t *buffer, uint8_t len);
void process_zone_(uint8_t *buffer);
void readline_(int readch, uint8_t *buffer, uint8_t len);
void handle_periodic_data_();
bool handle_ack_data_();
void process_zone_();
void readline_(int readch);
void get_version_();
void get_mac_();
void query_target_tracking_mode_();
@ -159,13 +167,14 @@ class LD2450Component : public Component, public uart::UARTDevice {
uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0;
uint16_t timeout_ = 5;
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t zone_type_ = 0;
bool bluetooth_on_{false};
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
std::string version_{};
std::string mac_{};
// Change detection - cache previous values to avoid redundant publishes
// All values are initialized to sentinel values that are outside the valid sensor ranges
@ -176,8 +185,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
int16_t speed = std::numeric_limits<int16_t>::min(); // -32768, outside practical sensor range
uint16_t resolution = std::numeric_limits<uint16_t>::max(); // 65535, unlikely resolution value
uint16_t distance = std::numeric_limits<uint16_t>::max(); // 65535, outside range of 0 to ~8990
Direction direction = DIRECTION_UNDEFINED; // Undefined, will differ from any real direction
float angle = NAN; // NAN, safe sentinel for floats
std::string direction = ""; // Empty string, will differ from any real direction
} cached_target_data_[MAX_TARGETS];
struct CachedZoneData {

View File

@ -11,7 +11,7 @@ static const char *const TAG = "nextion";
void Nextion::setup() {
this->is_setup_ = false;
this->ignore_is_setup_ = true;
this->connection_state_.ignore_is_setup_ = true;
// Wake up the nextion
this->send_command_("bkcmd=0");
@ -23,16 +23,16 @@ void Nextion::setup() {
// Reboot it
this->send_command_("rest");
this->ignore_is_setup_ = false;
this->connection_state_.ignore_is_setup_ = false;
}
bool Nextion::send_command_(const std::string &command) {
if (!this->ignore_is_setup_ && !this->is_setup()) {
if (!this->connection_state_.ignore_is_setup_ && !this->is_setup()) {
return false;
}
#ifdef USE_NEXTION_COMMAND_SPACING
if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) {
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
return false;
}
@ -48,7 +48,7 @@ bool Nextion::send_command_(const std::string &command) {
}
bool Nextion::check_connect_() {
if (this->is_connected_)
if (this->connection_state_.is_connected_)
return true;
// Check if the handshake should be skipped for the Nextion connection
@ -56,7 +56,7 @@ bool Nextion::check_connect_() {
// Log the connection status without handshake
ESP_LOGW(TAG, "Connected (no handshake)");
// Set the connection status to true
this->is_connected_ = true;
this->connection_state_.is_connected_ = true;
// Return true indicating the connection is set
return true;
}
@ -64,7 +64,7 @@ bool Nextion::check_connect_() {
if (this->comok_sent_ == 0) {
this->reset_(false);
this->ignore_is_setup_ = true;
this->connection_state_.ignore_is_setup_ = true;
this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating
if (this->exit_reparse_on_start_) {
this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
@ -72,7 +72,7 @@ bool Nextion::check_connect_() {
this->send_command_("connect");
this->comok_sent_ = App.get_loop_component_start_time();
this->ignore_is_setup_ = false;
this->connection_state_.ignore_is_setup_ = false;
return false;
}
@ -101,9 +101,9 @@ bool Nextion::check_connect_() {
return false;
}
this->ignore_is_setup_ = true;
this->connection_state_.ignore_is_setup_ = true;
ESP_LOGI(TAG, "Connected");
this->is_connected_ = true;
this->connection_state_.is_connected_ = true;
ESP_LOGN(TAG, "connect: %s", response.c_str());
@ -127,7 +127,7 @@ bool Nextion::check_connect_() {
ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
}
this->ignore_is_setup_ = false;
this->connection_state_.ignore_is_setup_ = false;
this->dump_config();
return true;
}
@ -158,7 +158,7 @@ void Nextion::dump_config() {
ESP_LOGCONFIG(TAG,
" Wake On Touch: %s\n"
" Exit reparse: %s",
YESNO(this->auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->max_commands_per_loop_);
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
@ -221,7 +221,7 @@ void Nextion::add_buffer_overflow_event_callback(std::function<void()> &&callbac
}
void Nextion::update_all_components() {
if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return;
for (auto *binarysensortype : this->binarysensortype_) {
@ -239,7 +239,7 @@ void Nextion::update_all_components() {
}
bool Nextion::send_command(const char *command) {
if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return false;
if (this->send_command_(command)) {
@ -250,7 +250,7 @@ bool Nextion::send_command(const char *command) {
}
bool Nextion::send_command_printf(const char *format, ...) {
if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return false;
char buffer[256];
@ -291,12 +291,12 @@ void Nextion::print_queue_members_() {
#endif
void Nextion::loop() {
if (!this->check_connect_() || this->is_updating_)
if (!this->check_connect_() || this->connection_state_.is_updating_)
return;
if (this->nextion_reports_is_setup_ && !this->sent_setup_commands_) {
this->ignore_is_setup_ = true;
this->sent_setup_commands_ = true;
if (this->connection_state_.nextion_reports_is_setup_ && !this->connection_state_.sent_setup_commands_) {
this->connection_state_.ignore_is_setup_ = true;
this->connection_state_.sent_setup_commands_ = true;
this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command.
if (this->brightness_.has_value()) {
@ -314,19 +314,19 @@ void Nextion::loop() {
this->set_wake_up_page(this->wake_up_page_);
}
this->ignore_is_setup_ = false;
this->connection_state_.ignore_is_setup_ = false;
}
this->process_serial_(); // Receive serial data
this->process_nextion_commands_(); // Process nextion return commands
if (!this->nextion_reports_is_setup_) {
if (!this->connection_state_.nextion_reports_is_setup_) {
if (this->started_ms_ == 0)
this->started_ms_ = App.get_loop_component_start_time();
if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
ESP_LOGD(TAG, "Manual ready set");
this->nextion_reports_is_setup_ = true;
this->connection_state_.nextion_reports_is_setup_ = true;
}
}
@ -669,7 +669,7 @@ void Nextion::process_nextion_commands_() {
case 0x88: // system successful start up
{
ESP_LOGD(TAG, "System start: %zu", to_process_length);
this->nextion_reports_is_setup_ = true;
this->connection_state_.nextion_reports_is_setup_ = true;
break;
}
case 0x89: { // start SD card upgrade
@ -1052,7 +1052,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
* @param command
*/
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
if ((!this->is_setup() && !this->ignore_is_setup_) || command.empty())
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
return;
if (this->send_command_(command)) {
@ -1095,7 +1095,7 @@ void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &va
bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format,
...) {
if ((!this->is_setup() && !this->ignore_is_setup_))
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_))
return false;
char buffer[256];
@ -1120,7 +1120,7 @@ bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string
* @param ... The format arguments
*/
bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) {
if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return false;
char buffer[256];
@ -1159,7 +1159,7 @@ void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name,
void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name,
const std::string &variable_name_to_send, int32_t state_value,
bool is_sleep_safe) {
if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
return;
this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%" PRId32, variable_name_to_send.c_str(),
@ -1187,7 +1187,7 @@ void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name,
void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name,
const std::string &variable_name_to_send,
const std::string &state_value, bool is_sleep_safe) {
if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
return;
this->add_no_result_to_queue_with_printf_(variable_name, "%s=\"%s\"", variable_name_to_send.c_str(),
@ -1204,7 +1204,7 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia
* @param component Pointer to the Nextion component that will handle the response.
*/
void Nextion::add_to_get_queue(NextionComponentBase *component) {
if ((!this->is_setup() && !this->ignore_is_setup_))
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_))
return;
#ifdef USE_NEXTION_MAX_QUEUE_SIZE
@ -1244,7 +1244,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
* @param buffer_size The buffer data
*/
void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return;
RAMAllocator<nextion::NextionQueue> allocator;
@ -1285,7 +1285,7 @@ void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = write
ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20")
void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); }
bool Nextion::is_updating() { return this->is_updating_; }
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
} // namespace nextion
} // namespace esphome

View File

@ -1302,7 +1302,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
* @return true if the Nextion display is connected and ready to receive commands
* @return false if the display is not yet connected or connection was lost
*/
bool is_connected() { return this->is_connected_; }
bool is_connected() { return this->connection_state_.is_connected_; }
protected:
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
@ -1336,21 +1336,28 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
bool remove_from_q_(bool report_empty = true);
/**
* @brief
* Sends commands ignoring of the Nextion has been setup.
* @brief Status flags for Nextion display state management
*
* Uses bitfields to pack multiple boolean states into a single byte,
* saving 5 bytes of RAM compared to individual bool variables.
*/
bool ignore_is_setup_ = false;
struct {
uint8_t is_connected_ : 1; ///< Connection established with Nextion display
uint8_t sent_setup_commands_ : 1; ///< Initial setup commands have been sent
uint8_t ignore_is_setup_ : 1; ///< Temporarily ignore setup state for special operations
uint8_t nextion_reports_is_setup_ : 1; ///< Nextion has reported successful initialization
uint8_t is_updating_ : 1; ///< TFT firmware update is currently in progress
uint8_t auto_wake_on_touch_ : 1; ///< Display should wake automatically on touch (default: true)
uint8_t reserved_ : 2; ///< Reserved bits for future flag additions
} connection_state_{}; ///< Zero-initialized status flags (all start as false)
bool nextion_reports_is_setup_ = false;
void process_nextion_commands_();
void process_serial_();
bool is_updating_ = false;
uint16_t touch_sleep_timeout_ = 0;
uint8_t wake_up_page_ = 255;
#ifdef USE_NEXTION_CONF_START_UP_PAGE
uint8_t start_up_page_ = 255;
#endif // USE_NEXTION_CONF_START_UP_PAGE
bool auto_wake_on_touch_ = true;
bool exit_reparse_on_start_ = false;
bool skip_connection_handshake_ = false;
@ -1472,11 +1479,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void reset_(bool reset_nextion = true);
std::string command_data_;
bool is_connected_ = false;
const uint16_t startup_override_ms_ = 8000;
const uint16_t max_q_age_ms_ = 8000;
uint32_t started_ms_ = 0;
bool sent_setup_commands_ = false;
};
} // namespace nextion

View File

@ -38,7 +38,7 @@ void Nextion::sleep(bool sleep) {
// Protocol reparse mode
bool Nextion::set_protocol_reparse_mode(bool active_mode) {
ESP_LOGV(TAG, "Reparse mode: %s", YESNO(active_mode));
this->ignore_is_setup_ = true; // if not in reparse mode setup will fail, so it should be ignored
this->connection_state_.ignore_is_setup_ = true; // if not in reparse mode setup will fail, so it should be ignored
bool all_commands_sent = true;
if (active_mode) { // Sets active protocol reparse mode
all_commands_sent &= this->send_command_("recmod=1");
@ -48,10 +48,10 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) {
all_commands_sent &= this->send_command_("recmod=0"); // Sending recmode=0 twice is recommended
all_commands_sent &= this->send_command_("recmod=0");
}
if (!this->nextion_reports_is_setup_) { // No need to connect if is already setup
if (!this->connection_state_.nextion_reports_is_setup_) { // No need to connect if is already setup
all_commands_sent &= this->send_command_("connect");
}
this->ignore_is_setup_ = false;
this->connection_state_.ignore_is_setup_ = false;
return all_commands_sent;
}
@ -191,7 +191,7 @@ void Nextion::set_backlight_brightness(float brightness) {
}
void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) {
this->auto_wake_on_touch_ = auto_wake_on_touch;
this->connection_state_.auto_wake_on_touch_ = auto_wake_on_touch;
this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0);
}

View File

@ -16,8 +16,8 @@ bool Nextion::upload_end_(bool successful) {
} else {
ESP_LOGE(TAG, "Upload failed");
this->is_updating_ = false;
this->ignore_is_setup_ = false;
this->connection_state_.is_updating_ = false;
this->connection_state_.ignore_is_setup_ = false;
uint32_t baud_rate = this->parent_->get_baud_rate();
if (baud_rate != this->original_baud_rate_) {

View File

@ -152,7 +152,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse));
ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
if (this->is_updating_) {
if (this->connection_state_.is_updating_) {
ESP_LOGW(TAG, "Upload in progress");
return false;
}
@ -162,7 +162,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
return false;
}
this->is_updating_ = true;
this->connection_state_.is_updating_ = true;
if (exit_reparse) {
ESP_LOGD(TAG, "Exit reparse mode");
@ -203,7 +203,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
#endif // USE_ESP8266
if (!begin_status) {
this->is_updating_ = false;
this->connection_state_.is_updating_ = false;
ESP_LOGD(TAG, "Connection failed");
return false;
} else {
@ -254,7 +254,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// The Nextion will ignore the upload command if it is sleeping
ESP_LOGV(TAG, "Wake-up");
this->ignore_is_setup_ = true;
this->connection_state_.ignore_is_setup_ = true;
this->send_command_("sleep=0");
this->send_command_("dim=100");
delay(250); // NOLINT

View File

@ -155,7 +155,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse));
ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
if (this->is_updating_) {
if (this->connection_state_.is_updating_) {
ESP_LOGW(TAG, "Upload in progress");
return false;
}
@ -165,7 +165,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
return false;
}
this->is_updating_ = true;
this->connection_state_.is_updating_ = true;
if (exit_reparse) {
ESP_LOGD(TAG, "Exit reparse mode");
@ -246,7 +246,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// The Nextion will ignore the upload command if it is sleeping
ESP_LOGV(TAG, "Wake-up");
this->ignore_is_setup_ = true;
this->connection_state_.ignore_is_setup_ = true;
this->send_command_("sleep=0");
this->send_command_("dim=100");
vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT

View File

@ -0,0 +1,317 @@
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_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import TimePeriod
MULTI_CONF = True
CODEOWNERS = ["@swoboda1337"]
DEPENDENCIES = ["spi"]
CONF_SX126X_ID = "sx126x_id"
CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate"
CONF_CODING_RATE = "coding_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_DEVIATION = "deviation"
CONF_DIO1_PIN = "dio1_pin"
CONF_HW_VERSION = "hw_version"
CONF_MODULATION = "modulation"
CONF_ON_PACKET = "on_packet"
CONF_PA_POWER = "pa_power"
CONF_PA_RAMP = "pa_ramp"
CONF_PAYLOAD_LENGTH = "payload_length"
CONF_PREAMBLE_DETECT = "preamble_detect"
CONF_PREAMBLE_SIZE = "preamble_size"
CONF_RST_PIN = "rst_pin"
CONF_RX_START = "rx_start"
CONF_RF_SWITCH = "rf_switch"
CONF_SHAPING = "shaping"
CONF_SPREADING_FACTOR = "spreading_factor"
CONF_SYNC_VALUE = "sync_value"
CONF_TCXO_VOLTAGE = "tcxo_voltage"
CONF_TCXO_DELAY = "tcxo_delay"
sx126x_ns = cg.esphome_ns.namespace("sx126x")
SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice)
SX126xListener = sx126x_ns.class_("SX126xListener")
SX126xBw = sx126x_ns.enum("SX126xBw")
SX126xPacketType = sx126x_ns.enum("SX126xPacketType")
SX126xTcxoCtrl = sx126x_ns.enum("SX126xTcxoCtrl")
SX126xRampTime = sx126x_ns.enum("SX126xRampTime")
SX126xPulseShape = sx126x_ns.enum("SX126xPulseShape")
SX126xLoraCr = sx126x_ns.enum("SX126xLoraCr")
BW = {
"4_8kHz": SX126xBw.SX126X_BW_4800,
"5_8kHz": SX126xBw.SX126X_BW_5800,
"7_3kHz": SX126xBw.SX126X_BW_7300,
"9_7kHz": SX126xBw.SX126X_BW_9700,
"11_7kHz": SX126xBw.SX126X_BW_11700,
"14_6kHz": SX126xBw.SX126X_BW_14600,
"19_5kHz": SX126xBw.SX126X_BW_19500,
"23_4kHz": SX126xBw.SX126X_BW_23400,
"29_3kHz": SX126xBw.SX126X_BW_29300,
"39_0kHz": SX126xBw.SX126X_BW_39000,
"46_9kHz": SX126xBw.SX126X_BW_46900,
"58_6kHz": SX126xBw.SX126X_BW_58600,
"78_2kHz": SX126xBw.SX126X_BW_78200,
"93_8kHz": SX126xBw.SX126X_BW_93800,
"117_3kHz": SX126xBw.SX126X_BW_117300,
"156_2kHz": SX126xBw.SX126X_BW_156200,
"187_2kHz": SX126xBw.SX126X_BW_187200,
"234_3kHz": SX126xBw.SX126X_BW_234300,
"312_0kHz": SX126xBw.SX126X_BW_312000,
"373_6kHz": SX126xBw.SX126X_BW_373600,
"467_0kHz": SX126xBw.SX126X_BW_467000,
"7_8kHz": SX126xBw.SX126X_BW_7810,
"10_4kHz": SX126xBw.SX126X_BW_10420,
"15_6kHz": SX126xBw.SX126X_BW_15630,
"20_8kHz": SX126xBw.SX126X_BW_20830,
"31_3kHz": SX126xBw.SX126X_BW_31250,
"41_7kHz": SX126xBw.SX126X_BW_41670,
"62_5kHz": SX126xBw.SX126X_BW_62500,
"125_0kHz": SX126xBw.SX126X_BW_125000,
"250_0kHz": SX126xBw.SX126X_BW_250000,
"500_0kHz": SX126xBw.SX126X_BW_500000,
}
CODING_RATE = {
"CR_4_5": SX126xLoraCr.LORA_CR_4_5,
"CR_4_6": SX126xLoraCr.LORA_CR_4_6,
"CR_4_7": SX126xLoraCr.LORA_CR_4_7,
"CR_4_8": SX126xLoraCr.LORA_CR_4_8,
}
MOD = {
"LORA": SX126xPacketType.PACKET_TYPE_LORA,
"FSK": SX126xPacketType.PACKET_TYPE_GFSK,
}
TCXO_VOLTAGE = {
"1_6V": SX126xTcxoCtrl.TCXO_CTRL_1_6V,
"1_7V": SX126xTcxoCtrl.TCXO_CTRL_1_7V,
"1_8V": SX126xTcxoCtrl.TCXO_CTRL_1_8V,
"2_2V": SX126xTcxoCtrl.TCXO_CTRL_2_2V,
"2_4V": SX126xTcxoCtrl.TCXO_CTRL_2_4V,
"2_7V": SX126xTcxoCtrl.TCXO_CTRL_2_7V,
"3_0V": SX126xTcxoCtrl.TCXO_CTRL_3_0V,
"3_3V": SX126xTcxoCtrl.TCXO_CTRL_3_3V,
"NONE": SX126xTcxoCtrl.TCXO_CTRL_NONE,
}
RAMP = {
"10us": SX126xRampTime.PA_RAMP_10,
"20us": SX126xRampTime.PA_RAMP_20,
"40us": SX126xRampTime.PA_RAMP_40,
"80us": SX126xRampTime.PA_RAMP_80,
"200us": SX126xRampTime.PA_RAMP_200,
"800us": SX126xRampTime.PA_RAMP_800,
"1700us": SX126xRampTime.PA_RAMP_1700,
"3400us": SX126xRampTime.PA_RAMP_3400,
}
SHAPING = {
"GAUSSIAN_BT_0_3": SX126xPulseShape.GAUSSIAN_BT_0_3,
"GAUSSIAN_BT_0_5": SX126xPulseShape.GAUSSIAN_BT_0_5,
"GAUSSIAN_BT_0_7": SX126xPulseShape.GAUSSIAN_BT_0_7,
"GAUSSIAN_BT_1_0": SX126xPulseShape.GAUSSIAN_BT_1_0,
"NONE": SX126xPulseShape.NO_FILTER,
}
RunImageCalAction = sx126x_ns.class_(
"RunImageCalAction", automation.Action, cg.Parented.template(SX126x)
)
SendPacketAction = sx126x_ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeTxAction = sx126x_ns.class_(
"SetModeTxAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeRxAction = sx126x_ns.class_(
"SetModeRxAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeSleepAction = sx126x_ns.class_(
"SetModeSleepAction", automation.Action, cg.Parented.template(SX126x)
)
SetModeStandbyAction = sx126x_ns.class_(
"SetModeStandbyAction", automation.Action, cg.Parented.template(SX126x)
)
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):
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_MODULATION] == "LORA":
if config[CONF_BANDWIDTH] not in lora_bws:
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
if config[CONF_PREAMBLE_SIZE] > 0 and 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] in lora_bws:
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with FSK")
if config[CONF_PREAMBLE_DETECT] > len(config[CONF_SYNC_VALUE]):
raise cv.Invalid("Preamble detection length must be <= sync value length")
return config
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SX126x),
cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000),
cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
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.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22),
cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP),
cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4),
cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535),
cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_RX_START, default=True): cv.boolean,
cv.Required(CONF_RF_SWITCH): 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),
cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE),
cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All(
cv.positive_time_period_microseconds,
cv.Range(max=TimePeriod(microseconds=262144000)),
),
},
)
.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_DIO1_PIN in config:
dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN])
cg.add(var.set_dio1_pin(dio1_pin))
rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN])
cg.add(var.set_rst_pin(rst_pin))
busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
cg.add(var.set_busy_pin(busy_pin))
cg.add(var.set_bandwidth(config[CONF_BANDWIDTH]))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
cg.add(var.set_hw_version(config[CONF_HW_VERSION]))
cg.add(var.set_deviation(config[CONF_DEVIATION]))
cg.add(var.set_modulation(config[CONF_MODULATION]))
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_bitrate(config[CONF_BITRATE]))
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_size(config[CONF_PREAMBLE_SIZE]))
cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT]))
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_start(config[CONF_RX_START]))
cg.add(var.set_rf_switch(config[CONF_RF_SWITCH]))
cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE]))
cg.add(var.set_tcxo_delay(config[CONF_TCXO_DELAY]))
NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(SX126x),
}
)
@automation.register_action(
"sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx126x.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(SX126x),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
)
@automation.register_action(
"sx126x.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

View File

@ -0,0 +1,62 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sx126x/sx126x.h"
namespace esphome {
namespace sx126x {
template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->run_image_cal(); }
};
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX126x> {
public:
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const std::vector<uint8_t> &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<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_tx(); }
};
template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_rx(); }
};
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_sleep(); }
};
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> {
public:
void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); }
};
} // namespace sx126x
} // namespace esphome

View File

@ -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_SX126X_ID, SX126x, SX126xListener, sx126x_ns
SX126xTransport = sx126x_ns.class_(
"SX126xTransport", PacketTransport, PollingComponent, SX126xListener
)
CONFIG_SCHEMA = transport_schema(SX126xTransport).extend(
{
cv.GenerateID(CONF_SX126X_ID): cv.use_id(SX126x),
}
)
async def to_code(config):
var, _ = await new_packet_transport(config)
sx126x = await cg.get_variable(config[CONF_SX126X_ID])
cg.add(var.set_parent(sx126x))

View File

@ -0,0 +1,26 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "sx126x_transport.h"
namespace esphome {
namespace sx126x {
static const char *const TAG = "sx126x_transport";
void SX126xTransport::setup() {
PacketTransport::setup();
this->parent_->register_listener(this);
}
void SX126xTransport::update() {
PacketTransport::update();
this->updated_ = true;
this->resend_data_ = true;
}
void SX126xTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->transmit_packet(buf); }
void SX126xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sx126x/sx126x.h"
#include "esphome/components/packet_transport/packet_transport.h"
#include <vector>
namespace esphome {
namespace sx126x {
class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener {
public:
void setup() override;
void update() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; }
size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); }
};
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,523 @@
#include "sx126x.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sx126x {
static const char *const TAG = "sx126x";
static const uint16_t RAMP[8] = {10, 20, 40, 80, 200, 800, 1700, 3400};
static const uint32_t BW_HZ[31] = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400, 29300, 39000, 46900,
58600, 78200, 93800, 117300, 156200, 187200, 234300, 312000, 373600, 467000, 7810,
10420, 15630, 20830, 31250, 41670, 62500, 125000, 250000, 500000};
static const uint8_t BW_LORA[10] = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830, LORA_BW_31250,
LORA_BW_41670, LORA_BW_62500, LORA_BW_125000, LORA_BW_250000, LORA_BW_500000};
static const uint8_t BW_FSK[21] = {
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
static constexpr uint32_t SWITCHING_DELAY_US = 1;
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
// OCP (Over Current Protection) values
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
// LoRa low data rate optimization threshold
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
uint8_t SX126x::read_fifo_(uint8_t offset, std::vector<uint8_t> &packet) {
this->wait_busy_();
this->enable();
this->transfer_byte(RADIO_READ_BUFFER);
this->transfer_byte(offset);
uint8_t status = this->transfer_byte(0x00);
for (uint8_t &byte : packet) {
byte = this->transfer_byte(0x00);
}
this->disable();
return status;
}
void SX126x::write_fifo_(uint8_t offset, const std::vector<uint8_t> &packet) {
this->wait_busy_();
this->enable();
this->transfer_byte(RADIO_WRITE_BUFFER);
this->transfer_byte(offset);
for (const uint8_t &byte : packet) {
this->transfer_byte(byte);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->transfer_byte(opcode);
uint8_t status = this->transfer_byte(0x00);
for (int32_t i = 0; i < size; i++) {
data[i] = this->transfer_byte(0x00);
}
this->disable();
return status;
}
void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->transfer_byte(opcode);
for (int32_t i = 0; i < size; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->write_byte(RADIO_READ_REGISTER);
this->write_byte((reg >> 8) & 0xFF);
this->write_byte((reg >> 0) & 0xFF);
this->write_byte(0x00);
for (int32_t i = 0; i < size; i++) {
data[i] = this->transfer_byte(0x00);
}
this->disable();
}
void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
this->wait_busy_();
this->enable();
this->write_byte(RADIO_WRITE_REGISTER);
this->write_byte((reg >> 8) & 0xFF);
this->write_byte((reg >> 0) & 0xFF);
for (int32_t i = 0; i < size; i++) {
this->transfer_byte(data[i]);
}
this->disable();
delayMicroseconds(SWITCHING_DELAY_US);
}
void SX126x::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
// setup pins
this->busy_pin_->setup();
this->rst_pin_->setup();
this->dio1_pin_->setup();
// start spi
this->spi_setup();
// configure rf
this->configure();
}
void SX126x::configure() {
uint8_t buf[8];
// toggle chip reset
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
this->rst_pin_->digital_write(false);
delayMicroseconds(RESET_DELAY_LOW_US);
this->rst_pin_->digital_write(true);
delayMicroseconds(RESET_DELAY_HIGH_US);
// wakeup
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
// config tcxo
if (this->tcxo_voltage_ != TCXO_CTRL_NONE) {
uint32_t delay = this->tcxo_delay_ >> 6;
buf[0] = this->tcxo_voltage_;
buf[1] = (delay >> 16) & 0xFF;
buf[2] = (delay >> 8) & 0xFF;
buf[3] = (delay >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
buf[0] = 0x7F;
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
}
// clear errors
buf[0] = 0x00;
buf[1] = 0x00;
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
// rf switch
if (this->rf_switch_) {
buf[0] = 0x01;
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
}
// check silicon version to make sure hw is ok
this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16);
if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) {
this->mark_failed();
return;
}
// setup packet type
buf[0] = this->modulation_;
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
// calibrate image
this->run_image_cal();
// set frequency
uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ;
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
buf[3] = (uint8_t) (freq & 0xFF);
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
// configure pa
int8_t pa_power = this->pa_power_;
if (this->hw_version_ == "sx1261") {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
if (pa_power == 15) {
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
} else {
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
}
pa_power = std::max(pa_power, (int8_t) -3);
pa_power = std::min(pa_power, (int8_t) 14);
buf[0] = OCP_80MA;
this->write_register_(REG_OCP, buf, 1);
} else {
// the following values were taken from section 13.1.14.1 table 13-21
// in rev 2.1 of the datasheet
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
pa_power = std::max(pa_power, (int8_t) -3);
pa_power = std::min(pa_power, (int8_t) 22);
buf[0] = OCP_140MA;
this->write_register_(REG_OCP, buf, 1);
}
buf[0] = pa_power;
buf[1] = this->pa_ramp_;
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
// configure modem
if (this->modulation_ == PACKET_TYPE_LORA) {
// set modulation params
float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_];
buf[0] = this->spreading_factor_;
buf[1] = BW_LORA[this->bandwidth_ - SX126X_BW_7810];
buf[2] = this->coding_rate_;
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
// set packet params and sync word
this->set_packet_params_(this->payload_length_);
if (this->sync_value_.size() == 2) {
this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
}
} else {
// set modulation params
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_;
uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ;
buf[0] = (bitrate >> 16) & 0xFF;
buf[1] = (bitrate >> 8) & 0xFF;
buf[2] = (bitrate >> 0) & 0xFF;
buf[3] = this->shaping_;
buf[4] = BW_FSK[this->bandwidth_ - SX126X_BW_4800];
buf[5] = (fdev >> 16) & 0xFF;
buf[6] = (fdev >> 8) & 0xFF;
buf[7] = (fdev >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
// set packet params and sync word
this->set_packet_params_(this->payload_length_);
if (!this->sync_value_.empty()) {
this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
}
}
// switch to rx or sleep
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
}
size_t SX126x::get_max_packet_size() {
if (this->payload_length_ > 0) {
return this->payload_length_;
}
return 255;
}
void SX126x::set_packet_params_(uint8_t payload_length) {
uint8_t buf[9];
if (this->modulation_ == PACKET_TYPE_LORA) {
buf[0] = (this->preamble_size_ >> 8) & 0xFF;
buf[1] = (this->preamble_size_ >> 0) & 0xFF;
buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00;
buf[3] = payload_length;
buf[4] = (this->crc_enable_) ? 0x01 : 0x00;
buf[5] = 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6);
} else {
uint16_t preamble_size = this->preamble_size_ * 8;
buf[0] = (preamble_size >> 8) & 0xFF;
buf[1] = (preamble_size >> 0) & 0xFF;
buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
buf[3] = this->sync_value_.size() * 8;
buf[4] = 0x00;
buf[5] = 0x00;
buf[6] = payload_length;
buf[7] = this->crc_enable_ ? 0x06 : 0x01;
buf[8] = 0x00;
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
}
}
SX126xError SX126x::transmit_packet(const std::vector<uint8_t> &packet) {
if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX126xError::INVALID_PARAMS;
}
if (packet.empty() || packet.size() > this->get_max_packet_size()) {
ESP_LOGE(TAG, "Packet size out of range");
return SX126xError::INVALID_PARAMS;
}
SX126xError ret = SX126xError::NONE;
this->set_mode_standby(STDBY_XOSC);
if (this->payload_length_ == 0) {
this->set_packet_params_(packet.size());
}
this->write_fifo_(0x00, packet);
this->set_mode_tx();
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio1_pin_->digital_read()) {
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX126xError::TIMEOUT;
break;
}
}
uint8_t buf[2];
buf[0] = 0xFF;
buf[1] = 0xFF;
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
return ret;
}
void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, rssi, snr);
}
this->packet_trigger_->trigger(packet, rssi, snr);
}
void SX126x::loop() {
if (!this->dio1_pin_->digital_read()) {
return;
}
uint16_t status;
uint8_t buf[3];
uint8_t rssi;
int8_t snr;
this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2);
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
status = (buf[0] << 8) | buf[1];
if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) {
if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) {
this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3);
if (this->modulation_ == PACKET_TYPE_LORA) {
rssi = buf[0];
snr = buf[1];
} else {
rssi = buf[2];
snr = 0;
}
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2);
this->packet_.resize(buf[0]);
this->read_fifo_(buf[1], this->packet_);
this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f);
}
}
}
void SX126x::run_image_cal() {
// the following values were taken from section 9.2.1 table 9-2
// in rev 2.1 of the datasheet
uint8_t buf[2] = {0, 0};
if (this->frequency_ > 900000000) {
buf[0] = 0xE1;
buf[1] = 0xE9;
} else if (this->frequency_ > 850000000) {
buf[0] = 0xD7;
buf[1] = 0xD8;
} else if (this->frequency_ > 770000000) {
buf[0] = 0xC1;
buf[1] = 0xC5;
} else if (this->frequency_ > 460000000) {
buf[0] = 0x75;
buf[1] = 0x81;
} else if (this->frequency_ > 425000000) {
buf[0] = 0x6B;
buf[1] = 0x6F;
}
if (buf[0] > 0 && buf[1] > 0) {
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
}
}
void SX126x::set_mode_rx() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// set timeout to 0
buf[0] = 0x00;
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
// switch to continuous mode rx
buf[0] = 0xFF;
buf[1] = 0xFF;
buf[2] = 0xFF;
this->write_opcode_(RADIO_SET_RX, buf, 3);
}
void SX126x::set_mode_tx() {
uint8_t buf[8];
// configure irq params
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
buf[0] = (irq >> 8) & 0xFF;
buf[1] = (irq >> 0) & 0xFF;
buf[2] = (irq >> 8) & 0xFF;
buf[3] = (irq >> 0) & 0xFF;
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
// switch to single mode tx
buf[0] = 0x00;
buf[1] = 0x00;
buf[2] = 0x00;
this->write_opcode_(RADIO_SET_TX, buf, 3);
}
void SX126x::set_mode_sleep() {
uint8_t buf[1];
buf[0] = 0x05;
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
}
void SX126x::set_mode_standby(SX126xStandbyMode mode) {
uint8_t buf[1];
buf[0] = mode;
this->write_opcode_(RADIO_SET_STANDBY, buf, 1);
}
void SX126x::wait_busy_() {
// wait if the device is busy, the maximum delay is only be a few ms
// with most commands taking only a few us
uint32_t start = millis();
while (this->busy_pin_->digital_read()) {
if (millis() - start > BUSY_TIMEOUT_MS) {
ESP_LOGE(TAG, "Wait busy timeout");
this->mark_failed();
break;
}
}
}
void SX126x::dump_config() {
ESP_LOGCONFIG(TAG, "SX126x:");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" BUSY Pin: ", this->busy_pin_);
LOG_PIN(" RST Pin: ", this->rst_pin_);
LOG_PIN(" DIO1 Pin: ", this->dio1_pin_);
ESP_LOGCONFIG(TAG,
" HW Version: %15s\n"
" Frequency: %" PRIu32 " Hz\n"
" Bandwidth: %" PRIu32 " Hz\n"
" PA Power: %" PRId8 " dBm\n"
" PA Ramp: %" PRIu16 " us\n"
" Payload Length: %" PRIu32 "\n"
" CRC Enable: %s\n"
" Rx Start: %s",
this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_, RAMP[this->pa_ramp_],
this->payload_length_, TRUEFALSE(this->crc_enable_), TRUEFALSE(this->rx_start_));
if (this->modulation_ == PACKET_TYPE_GFSK) {
const char *shaping = "NONE";
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_0_7) {
shaping = "GAUSSIAN_BT_0_7";
} else if (this->shaping_ == GAUSSIAN_BT_1_0) {
shaping = "GAUSSIAN_BT_1_0";
}
ESP_LOGCONFIG(TAG,
" Modulation: FSK\n"
" Deviation: %" PRIu32 " Hz\n"
" Shaping: %s\n"
" Preamble Size: %" PRIu16 "\n"
" Preamble Detect: %" PRIu16 "\n"
" Bitrate: %" PRIu32 "b/s",
this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_);
} else if (this->modulation_ == PACKET_TYPE_LORA) {
const char *cr = "4/8";
if (this->coding_rate_ == LORA_CR_4_5) {
cr = "4/5";
} else if (this->coding_rate_ == LORA_CR_4_6) {
cr = "4/6";
} else if (this->coding_rate_ == LORA_CR_4_7) {
cr = "4/7";
}
ESP_LOGCONFIG(TAG,
" Modulation: LORA\n"
" Spreading Factor: %" PRIu8 "\n"
" Coding Rate: %s\n"
" Preamble Size: %" PRIu16,
this->spreading_factor_, cr, this->preamble_size_);
}
if (!this->sync_value_.empty()) {
ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str());
}
if (this->is_failed()) {
ESP_LOGE(TAG, "Configuring SX126x failed");
}
}
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,140 @@
#pragma once
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "sx126x_reg.h"
#include <utility>
#include <vector>
namespace esphome {
namespace sx126x {
enum SX126xBw : uint8_t {
// FSK
SX126X_BW_4800,
SX126X_BW_5800,
SX126X_BW_7300,
SX126X_BW_9700,
SX126X_BW_11700,
SX126X_BW_14600,
SX126X_BW_19500,
SX126X_BW_23400,
SX126X_BW_29300,
SX126X_BW_39000,
SX126X_BW_46900,
SX126X_BW_58600,
SX126X_BW_78200,
SX126X_BW_93800,
SX126X_BW_117300,
SX126X_BW_156200,
SX126X_BW_187200,
SX126X_BW_234300,
SX126X_BW_312000,
SX126X_BW_373600,
SX126X_BW_467000,
// LORA
SX126X_BW_7810,
SX126X_BW_10420,
SX126X_BW_15630,
SX126X_BW_20830,
SX126X_BW_31250,
SX126X_BW_41670,
SX126X_BW_62500,
SX126X_BW_125000,
SX126X_BW_250000,
SX126X_BW_500000,
};
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
class SX126xListener {
public:
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
};
class SX126x : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
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_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
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_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; }
void set_mode_rx();
void set_mode_tx();
void set_mode_standby(SX126xStandbyMode mode);
void set_mode_sleep();
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
void set_pa_power(int8_t power) { this->pa_power_ = power; }
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
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<uint8_t> &sync_value) { this->sync_value_ = sync_value; }
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; }
void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; }
void run_image_cal();
void configure();
SX126xError transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() const { return this->packet_trigger_; };
protected:
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
uint8_t read_fifo_(uint8_t offset, std::vector<uint8_t> &packet);
void write_fifo_(uint8_t offset, const std::vector<uint8_t> &packet);
void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
void write_register_(uint16_t reg, uint8_t *data, uint8_t size);
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr);
void wait_busy_();
Trigger<std::vector<uint8_t>, float, float> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, float>()};
std::vector<SX126xListener *> listeners_;
std::vector<uint8_t> packet_;
std::vector<uint8_t> sync_value_;
InternalGPIOPin *busy_pin_{nullptr};
InternalGPIOPin *dio1_pin_{nullptr};
InternalGPIOPin *rst_pin_{nullptr};
std::string hw_version_;
char version_[16];
SX126xBw bandwidth_{SX126X_BW_125000};
uint32_t bitrate_{0};
uint32_t deviation_{0};
uint32_t frequency_{0};
uint32_t payload_length_{0};
uint32_t tcxo_delay_{0};
uint16_t preamble_detect_{0};
uint16_t preamble_size_{0};
uint8_t tcxo_voltage_{0};
uint8_t coding_rate_{0};
uint8_t modulation_{PACKET_TYPE_LORA};
uint8_t pa_ramp_{0};
uint8_t shaping_{0};
uint8_t spreading_factor_{0};
int8_t pa_power_{0};
bool crc_enable_{false};
bool rx_start_{false};
bool rf_switch_{false};
};
} // namespace sx126x
} // namespace esphome

View File

@ -0,0 +1,163 @@
#pragma once
#include "esphome/core/hal.h"
namespace esphome {
namespace sx126x {
static const uint32_t XTAL_FREQ = 32000000;
enum SX126xOpCode : uint8_t {
RADIO_GET_STATUS = 0xC0,
RADIO_WRITE_REGISTER = 0x0D,
RADIO_READ_REGISTER = 0x1D,
RADIO_WRITE_BUFFER = 0x0E,
RADIO_READ_BUFFER = 0x1E,
RADIO_SET_SLEEP = 0x84,
RADIO_SET_STANDBY = 0x80,
RADIO_SET_FS = 0xC1,
RADIO_SET_TX = 0x83,
RADIO_SET_RX = 0x82,
RADIO_SET_RXDUTYCYCLE = 0x94,
RADIO_SET_CAD = 0xC5,
RADIO_SET_TXCONTINUOUSWAVE = 0xD1,
RADIO_SET_TXCONTINUOUSPREAMBLE = 0xD2,
RADIO_SET_PACKETTYPE = 0x8A,
RADIO_GET_PACKETTYPE = 0x11,
RADIO_SET_RFFREQUENCY = 0x86,
RADIO_SET_TXPARAMS = 0x8E,
RADIO_SET_PACONFIG = 0x95,
RADIO_SET_CADPARAMS = 0x88,
RADIO_SET_BUFFERBASEADDRESS = 0x8F,
RADIO_SET_MODULATIONPARAMS = 0x8B,
RADIO_SET_PACKETPARAMS = 0x8C,
RADIO_GET_RXBUFFERSTATUS = 0x13,
RADIO_GET_PACKETSTATUS = 0x14,
RADIO_GET_RSSIINST = 0x15,
RADIO_GET_STATS = 0x10,
RADIO_RESET_STATS = 0x00,
RADIO_SET_DIOIRQPARAMS = 0x08,
RADIO_GET_IRQSTATUS = 0x12,
RADIO_CLR_IRQSTATUS = 0x02,
RADIO_CALIBRATE = 0x89,
RADIO_CALIBRATEIMAGE = 0x98,
RADIO_SET_REGULATORMODE = 0x96,
RADIO_GET_ERROR = 0x17,
RADIO_CLR_ERROR = 0x07,
RADIO_SET_TCXOMODE = 0x97,
RADIO_SET_TXFALLBACKMODE = 0x93,
RADIO_SET_RFSWITCHMODE = 0x9D,
RADIO_SET_STOPRXTIMERONPREAMBLE = 0x9F,
RADIO_SET_LORASYMBTIMEOUT = 0xA0,
};
enum SX126xRegister : uint16_t {
REG_VERSION_STRING = 0x0320,
REG_GFSK_SYNCWORD = 0x06C0,
REG_LORA_SYNCWORD = 0x0740,
REG_OCP = 0x08E7,
};
enum SX126xStandbyMode : uint8_t {
STDBY_RC = 0x00,
STDBY_XOSC = 0x01,
};
enum SX126xPacketType : uint8_t {
PACKET_TYPE_GFSK = 0x00,
PACKET_TYPE_LORA = 0x01,
PACKET_TYPE_LRHSS = 0x03,
};
enum SX126xFskBw : uint8_t {
FSK_BW_4800 = 0x1F,
FSK_BW_5800 = 0x17,
FSK_BW_7300 = 0x0F,
FSK_BW_9700 = 0x1E,
FSK_BW_11700 = 0x16,
FSK_BW_14600 = 0x0E,
FSK_BW_19500 = 0x1D,
FSK_BW_23400 = 0x15,
FSK_BW_29300 = 0x0D,
FSK_BW_39000 = 0x1C,
FSK_BW_46900 = 0x14,
FSK_BW_58600 = 0x0C,
FSK_BW_78200 = 0x1B,
FSK_BW_93800 = 0x13,
FSK_BW_117300 = 0x0B,
FSK_BW_156200 = 0x1A,
FSK_BW_187200 = 0x12,
FSK_BW_234300 = 0x0A,
FSK_BW_312000 = 0x19,
FSK_BW_373600 = 0x11,
FSK_BW_467000 = 0x09,
};
enum SX126xLoraBw : uint8_t {
LORA_BW_7810 = 0x00,
LORA_BW_10420 = 0x08,
LORA_BW_15630 = 0x01,
LORA_BW_20830 = 0x09,
LORA_BW_31250 = 0x02,
LORA_BW_41670 = 0x0A,
LORA_BW_62500 = 0x03,
LORA_BW_125000 = 0x04,
LORA_BW_250000 = 0x05,
LORA_BW_500000 = 0x06,
};
enum SX126xLoraCr : uint8_t {
LORA_CR_4_5 = 0x01,
LORA_CR_4_6 = 0x02,
LORA_CR_4_7 = 0x03,
LORA_CR_4_8 = 0x04,
};
enum SX126xIrqMasks : uint16_t {
IRQ_RADIO_NONE = 0x0000,
IRQ_TX_DONE = 0x0001,
IRQ_RX_DONE = 0x0002,
IRQ_PREAMBLE_DETECTED = 0x0004,
IRQ_SYNCWORD_VALID = 0x0008,
IRQ_HEADER_VALID = 0x0010,
IRQ_HEADER_ERROR = 0x0020,
IRQ_CRC_ERROR = 0x0040,
IRQ_CAD_DONE = 0x0080,
IRQ_CAD_ACTIVITY_DETECTED = 0x0100,
IRQ_RX_TX_TIMEOUT = 0x0200,
IRQ_RADIO_ALL = 0xFFFF,
};
enum SX126xTcxoCtrl : uint8_t {
TCXO_CTRL_1_6V = 0x00,
TCXO_CTRL_1_7V = 0x01,
TCXO_CTRL_1_8V = 0x02,
TCXO_CTRL_2_2V = 0x03,
TCXO_CTRL_2_4V = 0x04,
TCXO_CTRL_2_7V = 0x05,
TCXO_CTRL_3_0V = 0x06,
TCXO_CTRL_3_3V = 0x07,
TCXO_CTRL_NONE = 0xFF,
};
enum SX126xPulseShape : uint8_t {
NO_FILTER = 0x00,
GAUSSIAN_BT_0_3 = 0x08,
GAUSSIAN_BT_0_5 = 0x09,
GAUSSIAN_BT_0_7 = 0x0A,
GAUSSIAN_BT_1_0 = 0x0B,
};
enum SX126xRampTime : uint8_t {
PA_RAMP_10 = 0x00,
PA_RAMP_20 = 0x01,
PA_RAMP_40 = 0x02,
PA_RAMP_80 = 0x03,
PA_RAMP_200 = 0x04,
PA_RAMP_800 = 0x05,
PA_RAMP_1700 = 0x06,
PA_RAMP_3400 = 0x07,
};
} // namespace sx126x
} // namespace esphome

View File

@ -3,6 +3,9 @@ i2c:
scl: 16
sda: 17
esp32:
cpu_frequency: 240MHz
display:
- platform: inkplate6
id: inkplate_display

View File

@ -0,0 +1,40 @@
spi:
clk_pin: ${clk_pin}
mosi_pin: ${mosi_pin}
miso_pin: ${miso_pin}
sx126x:
dio1_pin: ${dio1_pin}
cs_pin: ${cs_pin}
busy_pin: ${busy_pin}
rst_pin: ${rst_pin}
pa_power: 3
bandwidth: 125_0kHz
crc_enable: true
frequency: 433920000
modulation: LORA
rx_start: true
hw_version: sx1262
rf_switch: true
sync_value: [0x14, 0x24]
preamble_size: 8
spreading_factor: 7
coding_rate: CR_4_6
tcxo_voltage: 1_8V
tcxo_delay: 5ms
on_packet:
then:
- lambda: |-
ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str());
button:
- platform: template
name: "SX126x Button"
on_press:
then:
- sx126x.set_mode_standby
- sx126x.run_image_cal
- sx126x.set_mode_sleep
- sx126x.set_mode_rx
- sx126x.send_packet:
data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]

View File

@ -0,0 +1,10 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO27
miso_pin: GPIO19
cs_pin: GPIO18
rst_pin: GPIO23
busy_pin: GPIO25
dio1_pin: GPIO26
<<: !include common.yaml

View File

@ -0,0 +1,10 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO18
miso_pin: GPIO19
cs_pin: GPIO1
rst_pin: GPIO2
busy_pin: GPIO4
dio1_pin: GPIO3
<<: !include common.yaml

View File

@ -0,0 +1,10 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO18
miso_pin: GPIO19
cs_pin: GPIO1
rst_pin: GPIO2
busy_pin: GPIO4
dio1_pin: GPIO3
<<: !include common.yaml

View File

@ -0,0 +1,10 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO27
miso_pin: GPIO19
cs_pin: GPIO18
rst_pin: GPIO23
busy_pin: GPIO25
dio1_pin: GPIO26
<<: !include common.yaml

View File

@ -0,0 +1,10 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO13
miso_pin: GPIO12
cs_pin: GPIO1
rst_pin: GPIO2
busy_pin: GPIO4
dio1_pin: GPIO3
<<: !include common.yaml

View File

@ -0,0 +1,10 @@
substitutions:
clk_pin: GPIO2
mosi_pin: GPIO3
miso_pin: GPIO4
cs_pin: GPIO5
rst_pin: GPIO6
busy_pin: GPIO8
dio1_pin: GPIO7
<<: !include common.yaml