#include "ld2420_radar.h"
#include <string.h>
#include <math.h>
#include <stdio.h>
/*
=========================================================
LD2420 Radar — реализация
Протокол: Energy mode (бинарный фрейм, fw >= v1.5.4)
Фрейм данных, 31 байт:
[0..3] Header F4 F3 F2 F1
[4..5] Length uint16_t LE
[6] Presence 0=нет 1=движение 2=стационарный
[7..8] Distance uint16_t LE (сантиметры)
[9..24] Gate[0..15] uint8_t
[25..26] Padding 00 00
[27..30] Footer F8 F7 F6 F5
Источник формата: ESPHome ld2420.cpp source
=========================================================
*/
/* -------- constexpr definitions ------------------------- */
constexpr uint8_t Ld2420Radar::HDR[4];
constexpr uint8_t Ld2420Radar::FTR[4];
/* -------- begin ----------------------------------------- */
void Ld2420Radar::begin(HardwareSerial& serial, const RadarConfig& config) {
_serial = &serial;
_cfg = config;
/* Ограничиваем окна */
if (_cfg.activity_avg_window_s < 1) _cfg.activity_avg_window_s = 1;
if (_cfg.activity_avg_window_s > ACT_AVG_MAX) _cfg.activity_avg_window_s = ACT_AVG_MAX;
if (_cfg.activity_trend_window_min < 1) _cfg.activity_trend_window_min = 1;
if (_cfg.activity_trend_window_min > TREND_MAX) _cfg.activity_trend_window_min = TREND_MAX;
if (_cfg.total_energy_max == 0) _cfg.total_energy_max = 1;
memset(_gate_energy, 0, sizeof(_gate_energy));
memset(_act_avg_buf, 0, sizeof(_act_avg_buf));
memset(_trend_buf, 0, sizeof(_trend_buf));
memset(_rx_buf, 0, sizeof(_rx_buf));
serial.begin(_cfg.baud_rate, SERIAL_8N1,
(int)_cfg.uart_rx_pin,
(int)_cfg.uart_tx_pin);
}
/* -------- update ---------------------------------------- */
void Ld2420Radar::update() {
if (!_serial) return;
while (_serial->available()) {
_feed_byte((uint8_t)_serial->read());
}
uint32_t now_ms = millis();
/* online */
_online = (_last_frame_ms > 0) &&
(now_ms - _last_frame_ms < _cfg.stale_after_ms);
/* presence hold */
if (_last_on_ms > 0 && (now_ms - _last_on_ms < _cfg.presence_hold_ms)) {
_presence = true;
} else {
_presence = false;
}
_tick_activity_avg(now_ms);
_tick_trend(now_ms);
}
/* -------- _feed_byte ------------------------------------- */
/*
Стратегия синхронизации:
1. Ищем заголовок F4 F3 F2 F1 побайтово.
2. После его захвата накапливаем оставшиеся (FRAME_SIZE - 4) байт.
3. Проверяем футер. Если неверен — сбрасываем и ищем снова.
*/
void Ld2420Radar::_feed_byte(uint8_t b) {
if (!_synced) {
/* Сдвигаем первые 4 байта в поисках заголовка */
_rx_buf[0] = _rx_buf[1];
_rx_buf[1] = _rx_buf[2];
_rx_buf[2] = _rx_buf[3];
_rx_buf[3] = b;
if (_rx_buf[0] == HDR[0] &&
_rx_buf[1] == HDR[1] &&
_rx_buf[2] == HDR[2] &&
_rx_buf[3] == HDR[3]) {
_synced = true;
_rx_idx = 4; // заголовок уже в буфере
}
return;
}
/* Накапливаем остаток фрейма */
_rx_buf[_rx_idx++] = b;
if (_rx_idx >= FRAME_SIZE) {
if (_validate_footer()) {
_process_frame();
} else {
if (_cfg.enable_debug_frames) {
Serial.print("[LD2420] bad footer: ");
for (uint8_t i = 0; i < FRAME_SIZE; i++) {
char hex[4];
snprintf(hex, sizeof(hex), "%02X ", _rx_buf[i]);
Serial.print(hex);
}
Serial.println();
}
}
/* В любом случае ищем следующий заголовок */
_synced = false;
_rx_idx = 0;
memset(_rx_buf, 0, sizeof(_rx_buf));
}
}
/* -------- _validate_footer ------------------------------ */
bool Ld2420Radar::_validate_footer() const {
return _rx_buf[FRAME_FOOTER_OFF + 0] == FTR[0] &&
_rx_buf[FRAME_FOOTER_OFF + 1] == FTR[1] &&
_rx_buf[FRAME_FOOTER_OFF + 2] == FTR[2] &&
_rx_buf[FRAME_FOOTER_OFF + 3] == FTR[3];
}
/* -------- _process_frame -------------------------------- */
void Ld2420Radar::_process_frame() {
uint32_t now_ms = millis();
_last_frame_ms = now_ms;
/* Поле Presence */
uint8_t presence_raw = _rx_buf[FRAME_PRESENCE_OFF];
bool has_target = (presence_raw == 0x01 || presence_raw == 0x02);
if (has_target) {
_last_on_ms = now_ms;
}
/* Дистанция (uint16_t LE, сантиметры → метры) */
uint16_t dist_cm = (uint16_t)(_rx_buf[FRAME_DIST_OFFSET]) |
(uint16_t)(_rx_buf[FRAME_DIST_OFFSET + 1]) << 8;
float raw_m = (float)dist_cm / 100.0f;
if (has_target && dist_cm > 0) {
if (_dist_ema < 0.0f) {
_dist_ema = raw_m;
} else {
_dist_ema = _cfg.distance_ema_alpha * raw_m
+ (1.0f - _cfg.distance_ema_alpha) * _dist_ema;
}
_distance_m = _dist_ema;
} else if (!has_target) {
_dist_ema = -1.0f;
_distance_m = 0.0f;
}
/* Энергии ворот Gate[0..15] */
uint32_t total_energy = 0;
for (uint8_t g = 0; g < LD2420_TOTAL_GATES; g++) {
_gate_energy[g] = _rx_buf[FRAME_GATE_OFFSET + g];
total_energy += _gate_energy[g];
}
/*
activity_score_current:
Суммарная энергия по всем воротам нормируется к 0–10.
total_energy_max соответствует score = 10.
*/
if (total_energy == 0) {
_activity_score_current = 0;
} else {
float score_f = (float)total_energy / (float)_cfg.total_energy_max * 10.0f;
if (score_f > 10.0f) score_f = 10.0f;
_activity_score_current = (uint8_t)(score_f + 0.5f);
}
if (_cfg.enable_debug_frames) {
Serial.print("[LD2420] pres=");
Serial.print(presence_raw);
Serial.print(" dist_cm=");
Serial.print(dist_cm);
Serial.print(" energy_sum=");
Serial.print(total_energy);
Serial.print(" score_cur=");
Serial.println(_activity_score_current);
}
}
/* -------- _tick_activity_avg ---------------------------- */
void Ld2420Radar::_tick_activity_avg(uint32_t now_ms) {
if (_act_avg_last_ms == 0) { _act_avg_last_ms = now_ms; return; }
if (now_ms - _act_avg_last_ms < 1000) return;
_act_avg_last_ms = now_ms;
uint8_t win = _cfg.activity_avg_window_s;
_act_avg_buf[_act_avg_idx] = _activity_score_current;
_act_avg_idx = (_act_avg_idx + 1) % win;
if (_act_avg_count < win) _act_avg_count++;
uint32_t sum = 0;
for (uint8_t i = 0; i < _act_avg_count; i++) sum += _act_avg_buf[i];
_activity_score = (uint8_t)(sum / _act_avg_count);
}
/* -------- _tick_trend ----------------------------------- */
void Ld2420Radar::_tick_trend(uint32_t now_ms) {
if (_trend_last_ms == 0) { _trend_last_ms = now_ms; return; }
if (now_ms - _trend_last_ms < 60000u) return;
_trend_last_ms = now_ms;
uint8_t win = _cfg.activity_trend_window_min;
_trend_buf[_trend_idx] = _activity_score;
_trend_idx = (_trend_idx + 1) % win;
if (_trend_count < win) _trend_count++;
_compute_trend();
}
/* -------- _compute_trend -------------------------------- */
void Ld2420Radar::_compute_trend() {
uint8_t n = _trend_count;
if (n < 2) { _trend_slope = 0; _trend_std_dev = 0.0f; return; }
uint8_t win = _cfg.activity_trend_window_min;
float sum_x = 0, sum_y = 0, sum_xx = 0, sum_xy = 0, mean_y = 0;
for (uint8_t i = 0; i < n; i++) {
/* Хронологический порядок: старый → новый */
uint8_t buf_i = (uint8_t)((_trend_idx - n + i + win) % win);
float x = (float)i;
float y = (float)_trend_buf[buf_i];
sum_x += x;
sum_y += y;
sum_xx += x * x;
sum_xy += x * y;
mean_y += y;
}
float fn = (float)n;
float denom = fn * sum_xx - sum_x * sum_x;
if (fabsf(denom) < 1e-6f) {
_trend_slope = 0;
} else {
float slope = (fn * sum_xy - sum_x * sum_y) / denom;
/* Значимый порог: ±0.30 балл/мин */
if (slope > 0.30f) _trend_slope = 1;
else if (slope < -0.30f) _trend_slope = -1;
else _trend_slope = 0;
}
/* Стандартное отклонение для детектирования "variable" */
mean_y /= fn;
float var = 0.0f;
for (uint8_t i = 0; i < n; i++) {
uint8_t buf_i = (uint8_t)((_trend_idx - n + i + win) % win);
float d = (float)_trend_buf[buf_i] - mean_y;
var += d * d;
}
_trend_std_dev = sqrtf(var / fn);
}
/* -------- get_activity_dynamics ------------------------- */
const char* Ld2420Radar::get_activity_dynamics() const {
if (_trend_count < 2) return "constant";
/*
"variable" = высокий разброс без чёткого тренда.
Порог StdDev > 2.5 при нулевом или слабом слоупе.
*/
if (_trend_std_dev > 2.5f && _trend_slope == 0) return "variable";
if (_trend_slope > 0) return "increasing";
else if (_trend_slope < 0) return "decreasing";
else return "constant";
}
/* -------- get_state_json -------------------------------- */
const char* Ld2420Radar::get_state_json() {
snprintf(_json_buf, sizeof(_json_buf),
"{"
"\"online\":%s,"
"\"presence\":%s,"
"\"activity_score\":%u,"
"\"activity_score_current\":%u,"
"\"activity_score_dynamics\":\"%s\","
"\"distance_m\":%.2f"
"}",
_online ? "true" : "false",
_presence ? "true" : "false",
(unsigned)_activity_score,
(unsigned)_activity_score_current,
get_activity_dynamics(),
_distance_m
);
return _json_buf;
}