Newer
Older
smart-home-server / devices / sensor / ld2420_radar.h
#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];
};