1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 08:41:59 +00:00

Merge remote-tracking branch 'upstream/dev' into integration

# Conflicts:
#	esphome/components/bedjet/bedjet_hub.cpp
#	esphome/components/esp8266/preferences.cpp
#	esphome/components/radon_eye_ble/radon_eye_listener.cpp
#	esphome/components/radon_eye_rd200/radon_eye_rd200.cpp
This commit is contained in:
J. Nick Koston
2026-01-05 17:48:09 -10:00
15 changed files with 719 additions and 105 deletions

View File

@@ -395,6 +395,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet
esphome/components/rd03d/* @jasstrong
esphome/components/resampler/speaker/* @kahrendt
esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz

View File

@@ -194,7 +194,7 @@ bool BedJetHub::discover_characteristics_() {
result = false;
} else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
char uuid_buf[esp32_ble::UUID_STR_LEN];
char uuid_buf[espbt::UUID_STR_LEN];
ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
descr->uuid.to_str(uuid_buf));
result = false;

View File

@@ -127,8 +127,8 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
return true;
}
// Stack buffer size - 16 words covers up to 15 words of data (60 bytes)
// which handles virtually all real-world preferences without heap allocation
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
// This handles virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_WORDS = 16;
class ESP8266PreferenceBackend : public ESPPreferenceBackend {

View File

@@ -1,7 +1,6 @@
#include "radon_eye_listener.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <vector>
#ifdef USE_ESP32
@@ -11,18 +10,11 @@ namespace radon_eye_ble {
static const char *const TAG = "radon_eye_ble";
bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (not device.get_name().empty()) {
// Vector containing the prefixes to search for
std::vector<std::string> prefixes = {"FR:R", "FR:I", "FR:H"};
// Check if the device name starts with any of the prefixes
if (std::any_of(prefixes.begin(), prefixes.end(),
[&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) {
// Device found
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(),
device.address_str_to(addr_buf));
}
// Radon Eye devices have names starting with "FR:"
if (device.get_name().starts_with("FR:")) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(),
device.address_str_to(addr_buf));
}
return false;
}

View File

@@ -1,6 +1,8 @@
#include "radon_eye_rd200.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include <cstring>
#ifdef USE_ESP32
namespace esphome {
@@ -8,6 +10,22 @@ namespace radon_eye_rd200 {
static const char *const TAG = "radon_eye_rd200";
static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V1 =
esp32_ble_tracker::ESPBTUUID::from_raw("00001523-1212-efde-1523-785feabcd123");
static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V1 =
esp32_ble_tracker::ESPBTUUID::from_raw("00001524-1212-efde-1523-785feabcd123");
static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V1 =
esp32_ble_tracker::ESPBTUUID::from_raw("00001525-1212-efde-1523-785feabcd123");
static const uint8_t WRITE_COMMAND_V1 = 0x50;
static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V2 =
esp32_ble_tracker::ESPBTUUID::from_raw("00001523-0000-1000-8000-00805f9b34fb");
static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V2 =
esp32_ble_tracker::ESPBTUUID::from_raw("00001524-0000-1000-8000-00805f9b34fb");
static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V2 =
esp32_ble_tracker::ESPBTUUID::from_raw("00001525-0000-1000-8000-00805f9b34fb");
static const uint8_t WRITE_COMMAND_V2 = 0x40;
void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
@@ -24,6 +42,22 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->parent()->get_service(SERVICE_UUID_V1) != nullptr) {
service_uuid_ = SERVICE_UUID_V1;
sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V1;
sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V1;
write_command_ = WRITE_COMMAND_V1;
} else if (this->parent()->get_service(SERVICE_UUID_V2) != nullptr) {
service_uuid_ = SERVICE_UUID_V2;
sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V2;
sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V2;
write_command_ = WRITE_COMMAND_V2;
} else {
ESP_LOGW(TAG, "No supported device has been found, disconnecting");
parent()->set_enabled(false);
break;
}
this->read_handle_ = 0;
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_read_characteristic_uuid_);
if (chr == nullptr) {
@@ -35,7 +69,6 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
this->read_handle_ = chr->handle;
// Write a 0x50 to the write characteristic.
auto *write_chr = this->parent()->get_characteristic(service_uuid_, sensors_write_characteristic_uuid_);
if (write_chr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN];
@@ -46,81 +79,106 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
this->write_handle_ = write_chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
write_query_message_();
request_read_values_();
esp_err_t status =
esp_ble_gattc_register_for_notify(gattc_if, this->parent()->get_remote_bda(), this->read_handle_);
if (status) {
ESP_LOGW(TAG, "Error registering for sensor notify, status=%d", status);
}
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.status != ESP_GATT_OK) {
ESP_LOGE(TAG, "write descr failed, error status = %x", param->write.status);
break;
}
if (param->read.handle == this->read_handle_) {
read_sensors_(param->read.value, param->read.value_len);
ESP_LOGV(TAG, "Write descr success, writing 0x%02X at write_handle=%d", this->write_command_,
this->write_handle_);
esp_err_t status =
esp_ble_gattc_write_char(gattc_if, this->parent()->get_conn_id(), this->write_handle_, sizeof(write_command_),
(uint8_t *) &write_command_, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error writing 0x%02x command, status=%d", write_command_, status);
}
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.is_notify) {
ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive notify value, %d bytes", param->notify.value_len);
} else {
ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive indicate value, %d bytes", param->notify.value_len);
}
read_sensors_(param->notify.value, param->notify.value_len);
break;
}
default:
break;
}
}
void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) {
if (value_len < 20) {
ESP_LOGD(TAG, "Invalid read");
if (value_len < 1) {
ESP_LOGW(TAG, "Unexpected empty message");
return;
}
// Example data
// [13:08:47][D][radon_eye_rd200:107]: result bytes: 5010 85EBB940 00000000 00000000 2200 2500 0000
ESP_LOGV(TAG, "result bytes: %02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X %02X%02X %02X%02X",
value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], value[8], value[9],
value[10], value[11], value[12], value[13], value[14], value[15], value[16], value[17], value[18],
value[19]);
uint8_t command = value[0];
if (value[0] != 0x50) {
// This isn't a sensor reading.
if ((command == WRITE_COMMAND_V1 && value_len < 20) || (command == WRITE_COMMAND_V2 && value_len < 68)) {
ESP_LOGW(TAG, "Unexpected command 0x%02X message length %d", command, value_len);
return;
}
// Example data V1:
// 501085EBB9400000000000000000220025000000
// Example data V2:
// 4042323230313033525532303338330652443230304e56322e302e3200014a00060a00080000000300010079300000e01108001c00020000003822005c8f423fa4709d3f
ESP_LOGV(TAG, "radon sensors raw bytes");
ESP_LOG_BUFFER_HEX_LEVEL(TAG, value, value_len, ESP_LOG_VERBOSE);
// Convert from pCi/L to Bq/m³
constexpr float convert_to_bwpm3 = 37.0;
RadonValue radon_value;
radon_value.chars[0] = value[2];
radon_value.chars[1] = value[3];
radon_value.chars[2] = value[4];
radon_value.chars[3] = value[5];
float radon_now = radon_value.number * convert_to_bwpm3;
if (is_valid_radon_value_(radon_now)) {
radon_sensor_->publish_state(radon_now);
float radon_now; // in Bq/m³
float radon_day; // in Bq/m³
float radon_month; // in Bq/m³
if (command == WRITE_COMMAND_V1) {
// Use memcpy to avoid unaligned memory access
float temp;
memcpy(&temp, value + 2, sizeof(float));
radon_now = temp * convert_to_bwpm3;
memcpy(&temp, value + 6, sizeof(float));
radon_day = temp * convert_to_bwpm3;
memcpy(&temp, value + 10, sizeof(float));
radon_month = temp * convert_to_bwpm3;
} else if (command == WRITE_COMMAND_V2) {
// Use memcpy to avoid unaligned memory access
uint16_t temp;
memcpy(&temp, value + 33, sizeof(uint16_t));
radon_now = temp;
memcpy(&temp, value + 35, sizeof(uint16_t));
radon_day = temp;
memcpy(&temp, value + 37, sizeof(uint16_t));
radon_month = temp;
} else {
ESP_LOGW(TAG, "Unexpected command value: 0x%02X", command);
return;
}
radon_value.chars[0] = value[6];
radon_value.chars[1] = value[7];
radon_value.chars[2] = value[8];
radon_value.chars[3] = value[9];
float radon_day = radon_value.number * convert_to_bwpm3;
if (this->radon_sensor_ != nullptr) {
this->radon_sensor_->publish_state(radon_now);
}
radon_value.chars[0] = value[10];
radon_value.chars[1] = value[11];
radon_value.chars[2] = value[12];
radon_value.chars[3] = value[13];
float radon_month = radon_value.number * convert_to_bwpm3;
if (is_valid_radon_value_(radon_month)) {
ESP_LOGV(TAG, "Radon Long Term based on month");
radon_long_term_sensor_->publish_state(radon_month);
} else if (is_valid_radon_value_(radon_day)) {
ESP_LOGV(TAG, "Radon Long Term based on day");
radon_long_term_sensor_->publish_state(radon_day);
if (this->radon_long_term_sensor_ != nullptr) {
if (radon_month > 0) {
ESP_LOGV(TAG, "Radon Long Term based on month");
this->radon_long_term_sensor_->publish_state(radon_month);
} else {
ESP_LOGV(TAG, "Radon Long Term based on day");
this->radon_long_term_sensor_->publish_state(radon_day);
}
}
ESP_LOGV(TAG,
@@ -135,49 +193,23 @@ void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) {
parent()->set_enabled(false);
}
bool RadonEyeRD200::is_valid_radon_value_(float radon) { return radon > 0.0 and radon < 37000; }
void RadonEyeRD200::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void RadonEyeRD200::write_query_message_() {
ESP_LOGV(TAG, "writing 0x50 to write service");
int request = 0x50;
auto status = esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
this->write_handle_, sizeof(request), (uint8_t *) &request,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending write request for sensor, status=%d", status);
}
}
void RadonEyeRD200::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
this->read_handle_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
void RadonEyeRD200::dump_config() {
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
}
RadonEyeRD200::RadonEyeRD200()
: PollingComponent(10000),
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
sensors_write_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(WRITE_CHARACTERISTIC_UUID)),
sensors_read_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(READ_CHARACTERISTIC_UUID)) {}
RadonEyeRD200::RadonEyeRD200() : PollingComponent(10000) {}
} // namespace radon_eye_rd200
} // namespace esphome

View File

@@ -14,10 +14,6 @@
namespace esphome {
namespace radon_eye_rd200 {
static const char *const SERVICE_UUID = "00001523-1212-efde-1523-785feabcd123";
static const char *const WRITE_CHARACTERISTIC_UUID = "00001524-1212-efde-1523-785feabcd123";
static const char *const READ_CHARACTERISTIC_UUID = "00001525-1212-efde-1523-785feabcd123";
class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode {
public:
RadonEyeRD200();
@@ -32,25 +28,17 @@ class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
protected:
bool is_valid_radon_value_(float radon);
void read_sensors_(uint8_t *value, uint16_t value_len);
void write_query_message_();
void request_read_values_();
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};
uint8_t write_command_;
uint16_t read_handle_;
uint16_t write_handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_write_characteristic_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_read_characteristic_uuid_;
union RadonValue {
char chars[4];
float number;
};
};
} // namespace radon_eye_rd200

View File

@@ -0,0 +1,50 @@
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_THROTTLE
CODEOWNERS = ["@jasstrong"]
DEPENDENCIES = ["uart"]
MULTI_CONF = True
CONF_RD03D_ID = "rd03d_id"
CONF_TRACKING_MODE = "tracking_mode"
rd03d_ns = cg.esphome_ns.namespace("rd03d")
RD03DComponent = rd03d_ns.class_("RD03DComponent", cg.Component, uart.UARTDevice)
TrackingMode = rd03d_ns.enum("TrackingMode", is_class=True)
TRACKING_MODES = {
"single": TrackingMode.SINGLE_TARGET,
"multi": TrackingMode.MULTI_TARGET,
}
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(RD03DComponent),
cv.Optional(CONF_TRACKING_MODE): cv.enum(TRACKING_MODES, lower=True),
cv.Optional(CONF_THROTTLE): cv.positive_time_period_milliseconds,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"rd03d",
require_tx=False,
require_rx=True,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_TRACKING_MODE in config:
cg.add(var.set_tracking_mode(config[CONF_TRACKING_MODE]))
if CONF_THROTTLE in config:
cg.add(var.set_throttle(config[CONF_THROTTLE]))

View File

@@ -0,0 +1,39 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_TARGET, DEVICE_CLASS_OCCUPANCY
from . import CONF_RD03D_ID, RD03DComponent
DEPENDENCIES = ["rd03d"]
MAX_TARGETS = 3
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_RD03D_ID): cv.use_id(RD03DComponent),
cv.Optional(CONF_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
),
}
).extend(
{
cv.Optional(f"target_{i + 1}"): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
)
for i in range(MAX_TARGETS)
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_RD03D_ID])
if target_config := config.get(CONF_TARGET):
sens = await binary_sensor.new_binary_sensor(target_config)
cg.add(hub.set_target_binary_sensor(sens))
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
sens = await binary_sensor.new_binary_sensor(target_config)
cg.add(hub.set_target_binary_sensor(i, sens))

View File

@@ -0,0 +1,243 @@
#include "rd03d.h"
#include "esphome/core/log.h"
#include <cmath>
namespace esphome::rd03d {
static const char *const TAG = "rd03d";
// Delay before sending configuration commands to allow radar to initialize
static constexpr uint32_t SETUP_TIMEOUT_MS = 100;
// Data frame format (radar -> host)
static constexpr uint8_t FRAME_HEADER[] = {0xAA, 0xFF, 0x03, 0x00};
static constexpr uint8_t FRAME_FOOTER[] = {0x55, 0xCC};
// Command frame format (host -> radar)
static constexpr uint8_t CMD_FRAME_HEADER[] = {0xFD, 0xFC, 0xFB, 0xFA};
static constexpr uint8_t CMD_FRAME_FOOTER[] = {0x04, 0x03, 0x02, 0x01};
// RD-03D tracking mode commands
static constexpr uint16_t CMD_SINGLE_TARGET = 0x0080;
static constexpr uint16_t CMD_MULTI_TARGET = 0x0090;
// Decode coordinate/speed value from RD-03D format
// Per datasheet: MSB=1 means positive, MSB=0 means negative
static constexpr int16_t decode_value(uint8_t low_byte, uint8_t high_byte) {
int16_t value = ((high_byte & 0x7F) << 8) | low_byte;
if ((high_byte & 0x80) == 0) {
value = -value;
}
return value;
}
void RD03DComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up RD-03D...");
this->set_timeout(SETUP_TIMEOUT_MS, [this]() { this->apply_config_(); });
}
void RD03DComponent::dump_config() {
ESP_LOGCONFIG(TAG, "RD-03D:");
if (this->tracking_mode_.has_value()) {
ESP_LOGCONFIG(TAG, " Tracking Mode: %s",
*this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi");
}
if (this->throttle_ > 0) {
ESP_LOGCONFIG(TAG, " Throttle: %ums", this->throttle_);
}
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Target Count", this->target_count_sensor_);
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
#endif
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
ESP_LOGCONFIG(TAG, " Target %d:", i + 1);
#ifdef USE_SENSOR
LOG_SENSOR(" ", "X", this->targets_[i].x);
LOG_SENSOR(" ", "Y", this->targets_[i].y);
LOG_SENSOR(" ", "Speed", this->targets_[i].speed);
LOG_SENSOR(" ", "Distance", this->targets_[i].distance);
LOG_SENSOR(" ", "Resolution", this->targets_[i].resolution);
LOG_SENSOR(" ", "Angle", this->targets_[i].angle);
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Presence", this->target_presence_[i]);
#endif
}
}
void RD03DComponent::loop() {
while (this->available()) {
uint8_t byte = this->read();
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
// Check if we're looking for frame header
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
if (byte == FRAME_HEADER[this->buffer_pos_]) {
this->buffer_[this->buffer_pos_++] = byte;
} else if (byte == FRAME_HEADER[0]) {
// Start over if we see a potential new header
this->buffer_[0] = byte;
this->buffer_pos_ = 1;
} else {
this->buffer_pos_ = 0;
}
continue;
}
// Accumulate data bytes
this->buffer_[this->buffer_pos_++] = byte;
// Check if we have a complete frame
if (this->buffer_pos_ == FRAME_SIZE) {
// Validate footer
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
this->process_frame_();
} else {
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
this->buffer_[FRAME_SIZE - 1]);
}
this->buffer_pos_ = 0;
}
}
}
void RD03DComponent::process_frame_() {
// Apply throttle if configured
if (this->throttle_ > 0) {
uint32_t now = millis();
if (now - this->last_publish_time_ < this->throttle_) {
return;
}
this->last_publish_time_ = now;
}
uint8_t target_count = 0;
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
// Calculate offset for this target's data
// Header is 4 bytes, each target is 8 bytes
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
// Extract raw bytes for this target
uint8_t x_low = this->buffer_[offset + 0];
uint8_t x_high = this->buffer_[offset + 1];
uint8_t y_low = this->buffer_[offset + 2];
uint8_t y_high = this->buffer_[offset + 3];
uint8_t speed_low = this->buffer_[offset + 4];
uint8_t speed_high = this->buffer_[offset + 5];
uint8_t res_low = this->buffer_[offset + 6];
uint8_t res_high = this->buffer_[offset + 7];
// Decode values per RD-03D format
int16_t x = decode_value(x_low, x_high);
int16_t y = decode_value(y_low, y_high);
int16_t speed = decode_value(speed_low, speed_high);
uint16_t resolution = (res_high << 8) | res_low;
// Check if target is present (non-zero coordinates)
bool target_present = (x != 0 || y != 0);
if (target_present) {
target_count++;
}
#ifdef USE_SENSOR
this->publish_target_(i, x, y, speed, resolution);
#endif
#ifdef USE_BINARY_SENSOR
if (this->target_presence_[i] != nullptr) {
this->target_presence_[i]->publish_state(target_present);
}
#endif
}
#ifdef USE_SENSOR
if (this->target_count_sensor_ != nullptr) {
this->target_count_sensor_->publish_state(target_count);
}
#endif
#ifdef USE_BINARY_SENSOR
if (this->target_binary_sensor_ != nullptr) {
this->target_binary_sensor_->publish_state(target_count > 0);
}
#endif
}
#ifdef USE_SENSOR
void RD03DComponent::publish_target_(uint8_t target_num, int16_t x, int16_t y, int16_t speed, uint16_t resolution) {
TargetSensor &target = this->targets_[target_num];
// Publish X coordinate (mm)
if (target.x != nullptr) {
target.x->publish_state(x);
}
// Publish Y coordinate (mm)
if (target.y != nullptr) {
target.y->publish_state(y);
}
// Publish speed (convert from cm/s to mm/s)
if (target.speed != nullptr) {
target.speed->publish_state(static_cast<float>(speed) * 10.0f);
}
// Publish resolution (mm)
if (target.resolution != nullptr) {
target.resolution->publish_state(resolution);
}
// Calculate and publish distance (mm)
if (target.distance != nullptr) {
float distance = std::hypot(static_cast<float>(x), static_cast<float>(y));
target.distance->publish_state(distance);
}
// Calculate and publish angle (degrees)
// Angle is measured from the Y axis (radar forward direction)
if (target.angle != nullptr) {
if (x == 0 && y == 0) {
target.angle->publish_state(0);
} else {
float angle = std::atan2(static_cast<float>(x), static_cast<float>(y)) * 180.0f / M_PI;
target.angle->publish_state(angle);
}
}
}
#endif
void RD03DComponent::send_command_(uint16_t command, const uint8_t *data, uint8_t data_len) {
// Send header
this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
// Send length (command word + data)
uint16_t len = 2 + data_len;
this->write_byte(len & 0xFF);
this->write_byte((len >> 8) & 0xFF);
// Send command word (little-endian)
this->write_byte(command & 0xFF);
this->write_byte((command >> 8) & 0xFF);
// Send data if any
if (data != nullptr && data_len > 0) {
this->write_array(data, data_len);
}
// Send footer
this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
ESP_LOGD(TAG, "Sent command 0x%04X with %d bytes of data", command, data_len);
}
void RD03DComponent::apply_config_() {
if (this->tracking_mode_.has_value()) {
uint16_t mode_cmd = (*this->tracking_mode_ == TrackingMode::SINGLE_TARGET) ? CMD_SINGLE_TARGET : CMD_MULTI_TARGET;
this->send_command_(mode_cmd);
}
}
} // namespace esphome::rd03d

View File

@@ -0,0 +1,93 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include <array>
namespace esphome::rd03d {
static constexpr uint8_t MAX_TARGETS = 3;
static constexpr uint8_t FRAME_HEADER_SIZE = 4;
static constexpr uint8_t FRAME_FOOTER_SIZE = 2;
static constexpr uint8_t TARGET_DATA_SIZE = 8;
static constexpr uint8_t FRAME_SIZE =
FRAME_HEADER_SIZE + (MAX_TARGETS * TARGET_DATA_SIZE) + FRAME_FOOTER_SIZE; // 30 bytes
enum class TrackingMode : uint8_t {
SINGLE_TARGET = 0,
MULTI_TARGET = 1,
};
#ifdef USE_SENSOR
struct TargetSensor {
sensor::Sensor *x{nullptr};
sensor::Sensor *y{nullptr};
sensor::Sensor *speed{nullptr};
sensor::Sensor *distance{nullptr};
sensor::Sensor *resolution{nullptr};
sensor::Sensor *angle{nullptr};
};
#endif
class RD03DComponent : public Component, public uart::UARTDevice {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
#ifdef USE_SENSOR
void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; }
void set_x_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].x = sensor; }
void set_y_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].y = sensor; }
void set_speed_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].speed = sensor; }
void set_distance_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].distance = sensor; }
void set_resolution_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].resolution = sensor; }
void set_angle_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].angle = sensor; }
#endif
#ifdef USE_BINARY_SENSOR
void set_target_binary_sensor(binary_sensor::BinarySensor *sensor) { this->target_binary_sensor_ = sensor; }
void set_target_binary_sensor(uint8_t target, binary_sensor::BinarySensor *sensor) {
this->target_presence_[target] = sensor;
}
#endif
// Configuration setters (called from code generation)
void set_tracking_mode(TrackingMode mode) { this->tracking_mode_ = mode; }
void set_throttle(uint32_t throttle) { this->throttle_ = throttle; }
protected:
void apply_config_();
void send_command_(uint16_t command, const uint8_t *data = nullptr, uint8_t data_len = 0);
void process_frame_();
#ifdef USE_SENSOR
void publish_target_(uint8_t target_num, int16_t x, int16_t y, int16_t speed, uint16_t resolution);
#endif
#ifdef USE_SENSOR
std::array<TargetSensor, MAX_TARGETS> targets_{};
sensor::Sensor *target_count_sensor_{nullptr};
#endif
#ifdef USE_BINARY_SENSOR
std::array<binary_sensor::BinarySensor *, MAX_TARGETS> target_presence_{};
binary_sensor::BinarySensor *target_binary_sensor_{nullptr};
#endif
// Configuration (only sent if explicitly set)
optional<TrackingMode> tracking_mode_{};
uint32_t throttle_{0};
uint32_t last_publish_time_{0};
std::array<uint8_t, FRAME_SIZE> buffer_{};
uint8_t buffer_pos_{0};
};
} // namespace esphome::rd03d

View File

@@ -0,0 +1,105 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ANGLE,
CONF_DISTANCE,
CONF_RESOLUTION,
CONF_SPEED,
CONF_X,
CONF_Y,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_SPEED,
STATE_CLASS_MEASUREMENT,
UNIT_DEGREES,
UNIT_MILLIMETER,
)
from . import CONF_RD03D_ID, RD03DComponent
DEPENDENCIES = ["rd03d"]
CONF_TARGET_COUNT = "target_count"
MAX_TARGETS = 3
UNIT_MILLIMETER_PER_SECOND = "mm/s"
TARGET_SCHEMA = cv.Schema(
{
cv.Optional(CONF_X): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Y): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETER_PER_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_SPEED,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RESOLUTION): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ANGLE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_RD03D_ID): cv.use_id(RD03DComponent),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)})
async def to_code(config):
hub = await cg.get_variable(config[CONF_RD03D_ID])
if target_count_config := config.get(CONF_TARGET_COUNT):
sens = await sensor.new_sensor(target_count_config)
cg.add(hub.set_target_count_sensor(sens))
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
if x_config := target_config.get(CONF_X):
sens = await sensor.new_sensor(x_config)
cg.add(hub.set_x_sensor(i, sens))
if y_config := target_config.get(CONF_Y):
sens = await sensor.new_sensor(y_config)
cg.add(hub.set_y_sensor(i, sens))
if speed_config := target_config.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(hub.set_speed_sensor(i, sens))
if distance_config := target_config.get(CONF_DISTANCE):
sens = await sensor.new_sensor(distance_config)
cg.add(hub.set_distance_sensor(i, sens))
if resolution_config := target_config.get(CONF_RESOLUTION):
sens = await sensor.new_sensor(resolution_config)
cg.add(hub.set_resolution_sensor(i, sens))
if angle_config := target_config.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(hub.set_angle_sensor(i, sens))

View File

@@ -0,0 +1,59 @@
rd03d:
id: rd03d_radar
sensor:
- platform: rd03d
rd03d_id: rd03d_radar
target_count:
name: Target Count
target_1:
x:
name: Target-1 X
y:
name: Target-1 Y
speed:
name: Target-1 Speed
angle:
name: Target-1 Angle
distance:
name: Target-1 Distance
resolution:
name: Target-1 Resolution
target_2:
x:
name: Target-2 X
y:
name: Target-2 Y
speed:
name: Target-2 Speed
angle:
name: Target-2 Angle
distance:
name: Target-2 Distance
resolution:
name: Target-2 Resolution
target_3:
x:
name: Target-3 X
y:
name: Target-3 Y
speed:
name: Target-3 Speed
angle:
name: Target-3 Angle
distance:
name: Target-3 Distance
resolution:
name: Target-3 Resolution
binary_sensor:
- platform: rd03d
rd03d_id: rd03d_radar
target:
name: Presence
target_1:
name: Target-1 Presence
target_2:
name: Target-2 Presence
target_3:
name: Target-3 Presence

View File

@@ -0,0 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
<<: !include common.yaml