#include "ld2420_radar.h"
#include <string.h>
#include <math.h>
#include <stdio.h>
/*
=========================================================
LD2420 Radar — реализация
Energy mode + конфигурация через командный протокол
=========================================================
*/
/* ---- constexpr definitions ----------------------------- */
constexpr uint8_t Ld2420Radar::CMD_HDR[4];
constexpr uint8_t Ld2420Radar::CMD_FTR[4];
constexpr uint8_t Ld2420Radar::DATA_HDR[4];
constexpr uint8_t Ld2420Radar::DATA_FTR[4];
/* ====================================================
ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ КОМАНДНОГО ПРОТОКОЛА
==================================================== */
/*
Формат команды:
CMD_HDR (4) | length (2, LE) | payload (N) | CMD_FTR (4)
length = кол-во байт payload
*/
bool Ld2420Radar::_send_cmd(const uint8_t* payload, uint8_t payload_len) {
/* Заголовок */
_serial->write(CMD_HDR, 4);
/* Длина payload (uint16 LE) */
_serial->write((uint8_t)(payload_len & 0xFF));
_serial->write((uint8_t)(payload_len >> 8));
/* Payload */
_serial->write(payload, payload_len);
/* Футер */
_serial->write(CMD_FTR, 4);
_serial->flush();
return _wait_ack();
}
/*
Ждём ACK — фрейм начинающийся с FD FC FB FA.
Не парсим полностью, просто проверяем что он пришёл.
*/
bool Ld2420Radar::_wait_ack() {
uint32_t deadline = millis() + ACK_TIMEOUT_MS;
uint8_t hdr_match = 0;
while (millis() < deadline) {
if (!_serial->available()) { delay(1); continue; }
uint8_t b = _serial->read();
if (b == CMD_HDR[hdr_match]) {
hdr_match++;
if (hdr_match == 4) {
/* Заголовок найден — ACK получен, читаем остаток */
uint32_t t = millis() + 50;
while (millis() < t) {
if (_serial->available()) _serial->read();
}
return true;
}
} else {
hdr_match = (b == CMD_HDR[0]) ? 1 : 0;
}
}
return false;
}
void Ld2420Radar::_flush_rx() {
uint32_t t = millis() + 100;
while (millis() < t) {
if (_serial->available()) _serial->read();
else delay(1);
}
}
/* ====================================================
КОНФИГУРАЦИЯ РАДАРА
==================================================== */
bool Ld2420Radar::_configure() {
Serial.println("[LD2420] Starting configuration...");
/* ---- 1. Войти в config mode ---- */
/* Payload: CMD(2) + protocol_version(2) = FF 00 02 00 */
{
uint8_t p[] = {0xFF, 0x00, 0x02, 0x00};
bool ok = false;
for (uint8_t i = 0; i < MAX_RETRIES && !ok; i++) {
_flush_rx();
ok = _send_cmd(p, sizeof(p));
}
if (!ok) {
Serial.println("[LD2420] ERROR: cannot enter config mode");
return false;
}
Serial.println("[LD2420] Config mode entered");
}
/* ---- 2. Установить Energy mode ---- */
/*
CMD_WRITE_SYS_PARAM = 0x0012
Payload: 12 00 | param_id(2) | param_value(4)
param_id для mode = 0x0000
Energy mode value = 0x0004
*/
{
uint8_t p[] = {
0x12, 0x00, // команда SET_SYS_PARAM
0x00, 0x00, // param: system mode
0x04, 0x00, 0x00, 0x00 // value: Energy mode
};
bool ok = false;
for (uint8_t i = 0; i < MAX_RETRIES && !ok; i++) ok = _send_cmd(p, sizeof(p));
if (!ok) Serial.println("[LD2420] WARN: set Energy mode failed");
else Serial.println("[LD2420] Energy mode set");
}
/* ---- 3. Установить max_gate ---- */
/*
CMD_WRITE_SYS_PARAM = 0x0012
param: 0x0001 = max gate
*/
{
uint8_t g = _cfg.max_gate;
if (g > 15) g = 15;
uint8_t p[] = {
0x12, 0x00,
0x01, 0x00, // param: max gate
g, 0x00, 0x00, 0x00
};
bool ok = false;
for (uint8_t i = 0; i < MAX_RETRIES && !ok; i++) ok = _send_cmd(p, sizeof(p));
if (!ok) Serial.println("[LD2420] WARN: set max_gate failed");
else {
Serial.print("[LD2420] max_gate set to ");
Serial.println(g);
}
}
/* ---- 4. Установить таймаут ---- */
/*
param: 0x0004 = timeout
*/
{
uint16_t t = _cfg.radar_timeout_s;
uint8_t p[] = {
0x12, 0x00,
0x04, 0x00, // param: timeout
(uint8_t)(t & 0xFF), (uint8_t)(t >> 8), 0x00, 0x00
};
bool ok = false;
for (uint8_t i = 0; i < MAX_RETRIES && !ok; i++) ok = _send_cmd(p, sizeof(p));
if (!ok) Serial.println("[LD2420] WARN: set timeout failed");
else {
Serial.print("[LD2420] timeout set to ");
Serial.print(t);
Serial.println(" s");
}
}
/* ---- 5. Записать пороги для каждого ворота ---- */
/*
CMD_WRITE_ABD_PARAM = 0x0007
Payload: 07 00 | gate_move_reg(2) | move_val(4) | gate_still_reg(2) | still_val(4)
Записываем оба порога одной командой на каждое ворото.
*/
Serial.println("[LD2420] Writing gate thresholds...");
bool thresh_ok = true;
for (uint8_t gate = 0; gate <= _cfg.max_gate; gate++) {
uint16_t move_reg = (uint16_t)(GATE_MOVE_REG_BASE + gate);
uint16_t still_reg = (uint16_t)(GATE_STILL_REG_BASE + gate);
uint32_t mv = _cfg.move_threshold;
uint32_t sv = _cfg.still_threshold;
uint8_t p[] = {
0x07, 0x00,
(uint8_t)(move_reg & 0xFF), (uint8_t)(move_reg >> 8),
(uint8_t)(mv & 0xFF), (uint8_t)((mv >> 8) & 0xFF),
(uint8_t)((mv >> 16) & 0xFF), (uint8_t)((mv >> 24) & 0xFF),
(uint8_t)(still_reg & 0xFF), (uint8_t)(still_reg >> 8),
(uint8_t)(sv & 0xFF), (uint8_t)((sv >> 8) & 0xFF),
(uint8_t)((sv >> 16) & 0xFF), (uint8_t)((sv >> 24) & 0xFF),
};
bool ok = false;
for (uint8_t i = 0; i < MAX_RETRIES && !ok; i++) ok = _send_cmd(p, sizeof(p));
if (!ok) {
Serial.print("[LD2420] WARN: threshold gate ");
Serial.print(gate);
Serial.println(" failed");
thresh_ok = false;
}
}
if (thresh_ok) Serial.println("[LD2420] Thresholds written");
/* ---- 6. Выйти из config mode и перезапустить ---- */
{
uint8_t p[] = {0xFE, 0x00};
bool ok = false;
for (uint8_t i = 0; i < MAX_RETRIES && !ok; i++) ok = _send_cmd(p, sizeof(p));
if (!ok) Serial.println("[LD2420] WARN: disable config failed");
else Serial.println("[LD2420] Config mode exited");
}
/* Небольшая пауза — радар применяет настройки */
delay(500);
Serial.println("[LD2420] Configuration complete");
return true;
}
/* ====================================================
BEGIN
==================================================== */
bool 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;
serial.begin(_cfg.baud_rate, SERIAL_8N1,
(int)_cfg.uart_rx_pin,
(int)_cfg.uart_tx_pin);
/* Даём радару время проснуться */
delay(1000);
_flush_rx();
_configured = _configure();
return _configured;
}
/* ====================================================
UPDATE
==================================================== */
void Ld2420Radar::update() {
if (!_serial) return;
while (_serial->available()) {
_feed_byte((uint8_t)_serial->read());
}
uint32_t now_ms = millis();
_online = (_last_frame_ms > 0) &&
(now_ms - _last_frame_ms < _cfg.stale_after_ms);
_presence = (_last_on_ms > 0) &&
(now_ms - _last_on_ms < _cfg.presence_hold_ms);
_tick_activity_avg(now_ms);
_tick_trend(now_ms);
}
/* ====================================================
ПАРСЕР ENERGY-ФРЕЙМОВ
==================================================== */
void Ld2420Radar::_feed_byte(uint8_t b) {
if (!_synced) {
/* Сдвигаем окно поиска заголовка */
_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] == DATA_HDR[0] &&
_rx_buf[1] == DATA_HDR[1] &&
_rx_buf[2] == DATA_HDR[2] &&
_rx_buf[3] == DATA_HDR[3]) {
_synced = true;
_rx_idx = 4;
}
return;
}
_rx_buf[_rx_idx++] = b;
if (_rx_idx >= FRAME_SIZE) {
if (_validate_data_footer()) {
_process_frame();
} else if (_cfg.enable_debug_frames) {
Serial.print("[LD2420] bad footer at: ");
Serial.printf("%02X %02X %02X %02X\n",
_rx_buf[FRAME_FOOTER_OFF], _rx_buf[FRAME_FOOTER_OFF+1],
_rx_buf[FRAME_FOOTER_OFF+2], _rx_buf[FRAME_FOOTER_OFF+3]);
}
_synced = false;
_rx_idx = 0;
}
}
bool Ld2420Radar::_validate_data_footer() const {
return _rx_buf[FRAME_FOOTER_OFF + 0] == DATA_FTR[0] &&
_rx_buf[FRAME_FOOTER_OFF + 1] == DATA_FTR[1] &&
_rx_buf[FRAME_FOOTER_OFF + 2] == DATA_FTR[2] &&
_rx_buf[FRAME_FOOTER_OFF + 3] == DATA_FTR[3];
}
void Ld2420Radar::_process_frame() {
uint32_t now_ms = millis();
_last_frame_ms = now_ms;
/* Presence — поле из фрейма только для debug, решение принимаем сами */
uint8_t pres_raw = _rx_buf[FRAME_PRESENCE];
/* Distance */
uint16_t dist_cm = (uint16_t)_rx_buf[FRAME_DIST] |
(uint16_t)_rx_buf[FRAME_DIST + 1] << 8;
float raw_m = (float)dist_cm * 0.01f;
/* дистанция обновляется ниже после вычисления has_target */
/* Gate energies + activity_score_current */
/* Каждое ворото — uint16_t LE, 2 байта */
/* Суммируем только от min_gate до max_gate — исключаем шум ближней зоны */
uint32_t total_energy = 0;
uint8_t gate_min = _cfg.min_gate;
uint8_t gate_max = (_cfg.max_gate < LD2420_TOTAL_GATES - 1)
? _cfg.max_gate : LD2420_TOTAL_GATES - 1;
for (uint8_t g = 0; g < LD2420_TOTAL_GATES; g++) {
uint8_t lo = _rx_buf[FRAME_GATES + g * 2];
uint8_t hi = _rx_buf[FRAME_GATES + g * 2 + 1];
_gate_energy[g] = (uint16_t)lo | ((uint16_t)hi << 8);
if (g >= gate_min && g <= gate_max) {
total_energy += _gate_energy[g];
}
}
/*
Вычисляем presence самостоятельно по энергиям ворот.
Ворото активно если его энергия > presence_thresholds[g].
Пороги g0 и g1 = 0 — они всегда исключены.
*/
uint8_t active_gates = 0;
for (uint8_t g = _cfg.min_gate; g <= gate_max; g++) {
if (_cfg.presence_thresholds[g] > 0 &&
_gate_energy[g] > _cfg.presence_thresholds[g]) {
active_gates++;
}
}
bool has_target = (active_gates >= _cfg.presence_min_active_gates);
if (has_target) _last_on_ms = now_ms;
/* Дистанция: обновляем только если has_target и dist >= min_gate */
{
float min_dist_m = (float)_cfg.min_gate * 0.70f;
bool dist_valid = has_target && dist_cm > 0 && raw_m >= min_dist_m;
if (dist_valid) {
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;
}
/* has_target=true но dist невалидна — держим последнее значение */
}
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.printf("[LD2420] pres_raw=%u active_gates=%u dist_cm=%u energy=%lu score=%u\n",
pres_raw, active_gates, dist_cm, (unsigned long)total_energy, _activity_score_current);
}
}
/* ====================================================
ТИКИ НАКОПЛЕНИЯ
==================================================== */
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);
}
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();
}
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 sx = 0, sy = 0, sxx = 0, sxy = 0, mean_y = 0;
for (uint8_t i = 0; i < n; i++) {
uint8_t idx = (uint8_t)((_trend_idx - n + i + win) % win);
float x = (float)i, y = (float)_trend_buf[idx];
sx += x; sy += y; sxx += x*x; sxy += x*y; mean_y += y;
}
float fn = (float)n, denom = fn * sxx - sx * sx;
if (fabsf(denom) < 1e-6f) {
_trend_slope = 0;
} else {
float slope = (fn * sxy - sx * sy) / denom;
_trend_slope = (slope > 0.30f) ? 1 : (slope < -0.30f) ? -1 : 0;
}
mean_y /= fn;
float var = 0.0f;
for (uint8_t i = 0; i < n; i++) {
uint8_t idx = (uint8_t)((_trend_idx - n + i + win) % win);
float d = (float)_trend_buf[idx] - mean_y;
var += d * d;
}
_trend_std_dev = sqrtf(var / fn);
}
const char* Ld2420Radar::get_activity_dynamics() const {
if (_trend_count < 2) return "constant";
if (_trend_std_dev > 2.5f && _trend_slope == 0) return "variable";
if (_trend_slope > 0) return "increasing";
if (_trend_slope < 0) return "decreasing";
return "constant";
}
/* ====================================================
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;
}