Newer
Older
smart-home-server / devices / sensor / ld2420_radar.h
#ifndef LD2420_RADAR_H
#define LD2420_RADAR_H

#include <Arduino.h>

/*
    Упрощённая динамика активности.

    constant:
        Активность в среднем не растёт и не падает.

    increasing:
        Во второй половине 10-минутного окна активность заметно выше,
        чем в первой.

    decreasing:
        Во второй половине 10-минутного окна активность заметно ниже,
        чем в первой.

    variable:
        Активность слишком рваная/хаотичная, простая линейная трактовка
        не даёт хорошего описания.
*/
enum RadarActivityDynamics {
    RADAR_ACTIVITY_DYNAMICS_CONSTANT = 0,
    RADAR_ACTIVITY_DYNAMICS_INCREASING,
    RADAR_ACTIVITY_DYNAMICS_DECREASING,
    RADAR_ACTIVITY_DYNAMICS_VARIABLE
};

/*
    Конфигурация радара и алгоритмов обработки.

    Сейчас это именно практичная конфигурация под первый рабочий слой,
    без лишней "умности", которую можно нарастить позже.
*/
struct RadarConfig {
    /*
        UART-настройки для LD2420.
        Под текущую схему:
            OT1 радара -> RX ESP32
            RX  радара -> TX ESP32
    */
    int uart_rx_pin = 4;
    int uart_tx_pin = 15;
    uint32_t baud_rate = 115200;

    /*
        Если сырое присутствие перестало приходить, presence ещё немного
        удерживается, чтобы не дёргаться от кратких провалов.
    */
    uint32_t presence_hold_ms = 1500;

    /*
        Если слишком давно не было новых строк по UART, считаем радар
        неактуальным и помечаем online=false.
    */
    uint32_t stale_after_ms = 2000;

    /*
        Базовые фильтры Range.
    */
    uint8_t median_window_size = 5;
    uint16_t max_zone_step_per_sample = 4;

    /*
        EMA для дистанции и скорости.
    */
    float distance_ema_alpha = 0.35f;
    float speed_ema_alpha = 0.25f;

    /*
        Границы допустимых зон.
        Пока можно держать широкий диапазон, а потом сузить.
    */
    uint16_t min_valid_zone = 1;
    uint16_t max_valid_zone = 200;

    /*
        Преобразование Range -> distance_m
        Пока это приближённая калибровка.
        Позже подгонишь по реальным замерам.
    */
    float zone_to_meter_k = 0.7f;
    float zone_to_meter_b = 0.0f;

    /*
        Настройка расчёта текущей активности из скорости.
        Ниже этого значения считаем, что активность почти отсутствует.
    */
    float activity_min_speed_m_s = 0.03f;

    /*
        При такой скорости current activity уже выходит примерно к 10.
        Это не "физический предел", а просто верхняя точка шкалы.
    */
    float activity_max_speed_m_s = 1.20f;

    /*
        Если нужно логировать строки, которые не распарсились.
    */
    bool enable_debug_unknown_lines = false;
};

/*
    Итоговое упрощённое состояние, которое сейчас действительно нужно
    устройству и системе умного дома.

    online:
        Есть актуальные данные от радара.

    presence:
        Есть присутствие после фильтрации и hold-механизма.

    activity_score:
        Средняя активность за последнюю минуту, шкала 0..10.

    activity_score_current:
        Текущая активность, шкала 0..10.

    activity_score_dynamics:
        Характер динамики активности за последние 10 минут.

    distance_m:
        Отфильтрованная текущая дистанция до цели.
*/
struct RadarState {
    bool online = false;
    bool presence = false;

    uint8_t activity_score = 0;
    uint8_t activity_score_current = 0;
    RadarActivityDynamics activity_score_dynamics = RADAR_ACTIVITY_DYNAMICS_CONSTANT;

    float distance_m = -1.0f;

    /*
        Ниже — служебные поля, полезные для внутренней логики
        и отладки, но их не обязательно отдавать наружу.
    */
    bool raw_presence = false;
    bool stale = true;

    uint16_t raw_distance_zone = 0;
    uint16_t filtered_distance_zone = 0;

    float radial_speed_m_s = 0.0f;

    uint32_t last_on_ms = 0;
    uint32_t last_off_ms = 0;
    uint32_t last_range_ms = 0;
    uint32_t last_update_ms = 0;

    uint32_t line_counter = 0;
    uint32_t parse_error_counter = 0;
    uint32_t ignored_range_counter = 0;
};

/*
    Первый рабочий слой для LD2420 в текстовом UART-режиме.

    Что умеет:
    - читать ON / OFF / Range N
    - фильтровать Range
    - считать distance_m
    - считать текущую активность
    - считать усреднённую активность за минуту
    - оценивать динамику активности за 10 минут
    - отдавать данные как JSON-строку

    Что пока не умеет:
    - несколько целей
    - кошки/люди
    - углы и направления по комнате
    - fusion нескольких радаров
*/
class Ld2420Radar {
public:
    Ld2420Radar();

    void begin(HardwareSerial &uart_port, const RadarConfig &config);
    void update();

    const RadarState &get_state() const;
    String get_state_json() const;

    void set_config(const RadarConfig &config);
    const RadarConfig &get_config() const;

private:
    static const uint8_t max_supported_median_window = 9;

    /*
        10-минутная история активности:
        60 бакетов по 10 секунд.
        Этого достаточно, чтобы:
        - получить последнюю минуту как последние 6 бакетов
        - получить динамику за 10 минут как анализ всех 60 бакетов
    */
    static const uint8_t activity_bucket_count = 60;
    static const uint32_t activity_bucket_duration_ms = 10000UL;

    static const uint16_t ACTIVITY_HISTORY_SIZE = 120; // если update ~2 раза в секунду

    uint8_t activity_history[ACTIVITY_HISTORY_SIZE];
    uint16_t activity_history_index = 0;
    bool activity_history_filled = false;

    HardwareSerial *uart;
    RadarConfig config;
    RadarState state;

    String line_buffer;

    uint16_t zone_window[max_supported_median_window];
    uint8_t zone_window_count;
    uint8_t zone_window_index;

    bool has_filtered_zone;
    uint16_t last_filtered_zone;

    bool has_smoothed_distance;
    float smoothed_distance_m;

    bool has_smoothed_speed;
    float smoothed_speed_m_s;

    /*
        История активности.
        В каждом бакете храним усреднённую активность за 10 секунд.
    */
    uint8_t activity_buckets[activity_bucket_count];
    uint32_t activity_bucket_start_ms;
    uint32_t activity_bucket_accum_sum;
    uint16_t activity_bucket_accum_count;
    uint8_t activity_bucket_index;
    bool activity_history_filled;

    void reset_runtime_state();

    void read_uart_lines();
    void handle_line(const String &line);
    void handle_on_line();
    void handle_off_line();
    void handle_range_line(const String &line);

    void push_zone_sample(uint16_t zone);
    uint16_t get_median_zone() const;
    uint16_t apply_zone_step_limit(uint16_t candidate_zone);

    float zone_to_distance_m(uint16_t zone) const;
    float apply_distance_smoothing(float new_distance_m);
    void update_speed(float filtered_distance_m, uint32_t now_ms);

    void apply_presence_hold();
    void update_online_state();

    uint8_t speed_to_activity_score(float abs_speed_m_s) const;

    void update_activity_history();
    void commit_current_activity_bucket(uint32_t now_ms);
    void update_activity_outputs();

    RadarActivityDynamics calculate_activity_dynamics() const;
    const char *activity_dynamics_to_string(RadarActivityDynamics dynamics) const;

    bool is_zone_valid(uint16_t zone) const;
};

#endif