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 (бинарный фрейм, fw >= v1.5.4)

    Формат фрейма данных (Energy mode, 31 байт):
      Offset  Size  Поле
       0       4    Header:   F4 F3 F2 F1
       4       2    Length:   uint16_t LE  (обычно 0x0023)
       6       1    Presence: 0=нет, 1=движение, 2=стационарный
       7       2    Distance: uint16_t LE, сантиметры
       9      16    Gate[0..15]: uint8_t, энергия каждых ворот
      25       2    Padding:  00 00
      27       4    Footer:   F8 F7 F6 F5
    Итого: 31 байт
    =========================================================
*/

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;

    /*  Удерживать presence после пропадания данных  */
    uint32_t presence_hold_ms = 1500;

    /*  Считать online пока данные свежее stale_after_ms  */
    uint32_t stale_after_ms = 2000;

    /*  EMA для дистанции (0 < α ≤ 1; больше = быстрее, меньше = плавнее)  */
    float    distance_ema_alpha = 0.35f;

    /*
        Расстояние одних ворот в метрах.
        Для LD2420 при стандартной конфигурации ≈ 0.70 м.
        Подгони по реальным замерам.
    */
    float    gate_size_m = 0.70f;

    /*
        Максимальная суммарная энергия по всем воротам,
        соответствующая activity_score_current = 10.
        Калибруй под реальную комнату и типичное движение.
    */
    uint32_t total_energy_max = 2000;

    /*  Окно усреднения activity_score, секунды (1–60)  */
    uint8_t  activity_avg_window_s    = 60;

    /*  Окно тренда для dynamics, минуты (1–10)  */
    uint8_t  activity_trend_window_min = 10;

    bool     enable_debug_frames = false;
};


/*
    =========================================================
    Ld2420Radar — публичный интерфейс
    =========================================================
*/

class Ld2420Radar {
public:
    Ld2420Radar() = default;

    /*  Вызвать один раз в setup()  */
    void begin(HardwareSerial& serial, const RadarConfig& config);

    /*  Вызывать каждый loop()  */
    void update();

    /*  JSON-снимок состояния (статический буфер, валиден до следующего вызова)  */
    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; }

    /*  "constant" | "increasing" | "decreasing" | "variable"  */
    const char* get_activity_dynamics() const;

    /*  Сырые уровни энергии ворот 0–15 из последнего фрейма  */
    uint8_t get_gate_energy(uint8_t gate) const {
        return (gate < LD2420_TOTAL_GATES) ? _gate_energy[gate] : 0;
    }

private:
    /* ---- Конфигурация и железо ---- */
    HardwareSerial* _serial = nullptr;
    RadarConfig     _cfg;

    /* ---- Бинарный парсер фрейма ---- */
    static constexpr uint8_t FRAME_SIZE         = 31;
    static constexpr uint8_t FRAME_PRESENCE_OFF = 6;
    static constexpr uint8_t FRAME_DIST_OFFSET  = 7;
    static constexpr uint8_t FRAME_GATE_OFFSET  = 9;
    static constexpr uint8_t FRAME_FOOTER_OFF   = 27;

    /*  Байты заголовка  */
    static constexpr uint8_t HDR[4] = {0xF4, 0xF3, 0xF2, 0xF1};
    /*  Байты футера  */
    static constexpr uint8_t 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_footer() const;
    void _process_frame();

    /* ---- Данные последнего фрейма ---- */
    uint8_t  _gate_energy[LD2420_TOTAL_GATES] = {};
    float    _dist_ema    = -1.0f;
    float    _prev_dist_m = -1.0f;

    /* ---- Онлайн / присутствие ---- */
    uint32_t _last_frame_ms = 0;
    uint32_t _last_on_ms    = 0;
    bool     _online    = false;
    bool     _presence  = false;

    /* ---- Дистанция ---- */
    float _distance_m = 0.0f;

    /* ---- activity_score_current ---- */
    uint8_t _activity_score_current = 0;

    /* ---- activity_score (скользящее среднее 1 сэмпл/с, окно 60 с) ---- */
    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);

    /* ---- Тренд активности (1 точка/мин, окно 10 мин) ---- */
    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;    //  1 = рост, 0 = плоско, -1 = спад
    float    _trend_std_dev = 0.0f;
    void _tick_trend(uint32_t now_ms);
    void _compute_trend();

    /* ---- JSON-буфер ---- */
    char _json_buf[200];
};