#pragma once
#include <Arduino.h>
#include <HardwareSerial.h>
/*
=========================================================
LD2420 Radar
Модуль: LD2420 v2.1
Режим: Energy mode (бинарный фрейм)
При старте ESP32 сам переводит радар в Energy mode и
записывает пороги чувствительности через командный протокол.
Командный протокол (config mode):
Header: FD FC FB FA
Footer: 04 03 02 01
Фрейм данных Energy mode (31 байт):
[0..3] Header F4 F3 F2 F1
[4..5] Length uint16 LE
[6] Presence 0=нет 1=движение 2=стационарный
[7..8] Distance uint16 LE (сантиметры)
[9..24] Gate[0..15] uint8 — энергия каждого ворота
[25..26] Padding
[27..30] Footer F8 F7 F6 F5
=========================================================
*/
static constexpr uint8_t LD2420_TOTAL_GATES = 16;
struct RadarConfig {
/* ---- UART ---- */
uint8_t uart_rx_pin = 4;
uint8_t uart_tx_pin = 15;
uint32_t baud_rate = 115200;
/* ---- Присутствие и онлайн ---- */
uint32_t presence_hold_ms = 1500;
uint32_t stale_after_ms = 2000;
/* ---- Дистанция ---- */
float distance_ema_alpha = 0.6f;
/* ---- Параметры, записываемые в радар при старте ---- */
/*
Максимальная зона детекции (0–15).
Каждая зона ≈ 0.7 м, итого:
5 ворот ≈ 3.5 м
8 ворот ≈ 5.6 м
12 ворот ≈ 8.4 м (максимум)
*/
uint8_t min_gate = 2; // игнорировать ворота ближе этого (0-14)
uint8_t max_gate = 8;
/*
Таймаут исчезновения присутствия внутри самого радара (секунды).
Радар не выдаст OFF пока не пройдёт это время без движения.
Рекомендуется 5–30 с. Мы дополнительно держим presence
в прошивке через presence_hold_ms.
*/
uint16_t radar_timeout_s = 5;
/*
Пороги чувствительности — одинаковые для всех ворот.
Чем выше значение, тем менее чувствителен радар.
move_threshold: порог обнаружения движения
still_threshold: порог обнаружения стационарного присутствия
Заводские значения Hi-Link:
gate 0-1: move=60000, still=40000 (очень близко — шумно)
gate 2+: move=250, still=200
Рекомендуемый старт для комнаты без ложных срабатываний:
move=500, still=300
Если пропускает реального человека — уменьшай.
Если ложные срабатывания — увеличивай.
*/
/*
Пороги присутствия для каждого ворота (g0–g15).
Presence вычисляется в прошивке — поле pres из фрейма игнорируется.
Ворото считается активным если его энергия > порога.
presence = true если активных ворот >= presence_min_active_gates.
Как настраивать:
1. Включи enable_debug_frames=true, посмотри [GATES] пустой комнаты
2. Порог ворота = максимум_шума_этого_ворота * 2
3. g0 и g1 всегда 0 — не участвуют в решении
*/
uint32_t presence_thresholds[LD2420_TOTAL_GATES] = {
0, // g0 шум антенны, игнорируем
0, // g1 фоновый шум, игнорируем
800, // g2 ~1.4 м
500, // g3 ~2.1 м
15000, // g4 ~2.8 м (высокий: помехи в этой зоне)
25000, // g5 ~3.5 м (высокий: помехи)
15000, // g6 ~4.2 м (высокий: помехи)
2000, // g7 ~4.9 м
2000, // g8 ~5.6 м
500, // g9
500, // g10
500, // g11
500, // g12
500, // g13
500, // g14
500, // g15
};
/* Минимальное кол-во активных ворот для presence=true */
uint8_t presence_min_active_gates = 1;
/* Пороги для записи в модуль (внутренняя логика радара) */
uint32_t move_threshold = 10000;
uint32_t still_threshold = 8000;
/* ---- activity_score ---- */
/*
Максимальная суммарная энергия по всем воротам
соответствующая score = 10.
Откалибруй: включи enable_debug_frames, посмотри
energy_sum при активном движении и подставь это значение.
*/
uint32_t total_energy_max = 3000;
uint8_t activity_avg_window_s = 60;
uint8_t activity_trend_window_min = 10;
/* ---- Отладка ---- */
bool enable_debug_frames = false;
};
/*
=========================================================
Ld2420Radar — публичный интерфейс
=========================================================
*/
class Ld2420Radar {
public:
Ld2420Radar() = default;
/*
Инициализация: открывает UART, входит в config mode,
записывает Energy mode + пороги + max_gate, выходит.
Возвращает true если конфигурация применена успешно.
*/
bool begin(HardwareSerial& serial, const RadarConfig& config);
void update();
const char* get_state_json();
bool is_online() const { return _online; }
bool is_presence() const { return _presence; }
uint8_t get_activity_score() const { return _activity_score; }
uint8_t get_activity_score_current() const { return _activity_score_current; }
float get_distance_m() const { return _distance_m; }
const char* get_activity_dynamics() const;
uint16_t get_gate_energy(uint8_t g) const {
return (g < LD2420_TOTAL_GATES) ? _gate_energy[g] : 0;
}
/* true если конфигурация была успешно записана в радар */
bool is_configured() const { return _configured; }
private:
HardwareSerial* _serial = nullptr;
RadarConfig _cfg;
bool _configured = false;
/* ====================================================
КОМАНДНЫЙ ПРОТОКОЛ (config mode)
==================================================== */
static constexpr uint8_t CMD_HDR[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static constexpr uint8_t CMD_FTR[4] = {0x04, 0x03, 0x02, 0x01};
static constexpr uint16_t CMD_ENABLE_CONFIG = 0x00FF;
static constexpr uint16_t CMD_DISABLE_CONFIG = 0x00FE;
static constexpr uint16_t CMD_SET_SYS_MODE = 0x0012;
static constexpr uint16_t CMD_WRITE_THRESH = 0x0007;
static constexpr uint16_t CMD_SET_MAX_GATE = 0x0012; // тот же 0x0012, param 0x0001
static constexpr uint16_t CMD_RESTART = 0x0068;
static constexpr uint16_t SYS_MODE_ENERGY = 0x0004;
static constexpr uint16_t REG_MAX_GATE = 0x0001;
static constexpr uint16_t REG_TIMEOUT = 0x0004;
static constexpr uint16_t GATE_MOVE_REG_BASE = 0x0010; // 0x0010..0x001F
static constexpr uint16_t GATE_STILL_REG_BASE = 0x0020; // 0x0020..0x002F
static constexpr uint32_t ACK_TIMEOUT_MS = 300;
static constexpr uint8_t MAX_RETRIES = 3;
/* Отправить команду и дождаться ACK. Возвращает true при успехе. */
bool _send_cmd(const uint8_t* payload, uint8_t payload_len);
/* Ждать ACK-фрейм (header FD FC FB FA) до таймаута. */
bool _wait_ack();
/* Сбросить входной буфер UART. */
void _flush_rx();
/* Вся последовательность конфигурации. */
bool _configure();
/* ====================================================
ПАРСЕР ENERGY-ФРЕЙМОВ
==================================================== */
static constexpr uint8_t FRAME_SIZE = 45;
static constexpr uint8_t FRAME_PRESENCE = 6;
static constexpr uint8_t FRAME_DIST = 7;
static constexpr uint8_t FRAME_GATES = 9;
static constexpr uint8_t FRAME_FOOTER_OFF = 41;
static constexpr uint8_t DATA_HDR[4] = {0xF4, 0xF3, 0xF2, 0xF1};
static constexpr uint8_t DATA_FTR[4] = {0xF8, 0xF7, 0xF6, 0xF5};
uint8_t _rx_buf[FRAME_SIZE] = {};
uint8_t _rx_idx = 0;
bool _synced = false;
void _feed_byte(uint8_t b);
bool _validate_data_footer() const;
void _process_frame();
/* ---- Состояние ---- */
uint16_t _gate_energy[LD2420_TOTAL_GATES] = {};
float _dist_ema = -1.0f;
float _distance_m = 0.0f;
uint32_t _last_frame_ms = 0;
uint32_t _last_on_ms = 0;
bool _online = false;
bool _presence = false;
uint8_t _activity_score_current = 0;
static constexpr uint8_t ACT_AVG_MAX = 60;
uint8_t _act_avg_buf[ACT_AVG_MAX] = {};
uint8_t _act_avg_idx = 0;
uint8_t _act_avg_count = 0;
uint32_t _act_avg_last_ms = 0;
uint8_t _activity_score = 0;
void _tick_activity_avg(uint32_t now_ms);
static constexpr uint8_t TREND_MAX = 10;
uint8_t _trend_buf[TREND_MAX] = {};
uint8_t _trend_idx = 0;
uint8_t _trend_count = 0;
uint32_t _trend_last_ms = 0;
int8_t _trend_slope = 0;
float _trend_std_dev = 0.0f;
void _tick_trend(uint32_t now_ms);
void _compute_trend();
char _json_buf[200];
};