diff --git a/devices/sensor/ld2420_radar.cpp b/devices/sensor/ld2420_radar.cpp index d0013e3..44ea14c 100644 --- a/devices/sensor/ld2420_radar.cpp +++ b/devices/sensor/ld2420_radar.cpp @@ -2,9 +2,6 @@ Ld2420Radar::Ld2420Radar() { uart = nullptr; - new_data_available = false; - calibration_mode = false; - reset_runtime_state(); } @@ -30,91 +27,50 @@ reset_runtime_state(); - state.timestamp_ms = millis(); - state.last_update_ms = state.timestamp_ms; + uint32_t now_ms = millis(); + state.last_update_ms = now_ms; + activity_bucket_start_ms = now_ms; } void Ld2420Radar::update() { - state.timestamp_ms = millis(); - read_uart_lines(); apply_presence_hold(); - update_stale_flag(); - update_direction(); - update_activity_score(); - update_confidence(); -} - -bool Ld2420Radar::has_new_data() const { - return new_data_available; -} - -void Ld2420Radar::clear_new_data_flag() { - new_data_available = false; + update_online_state(); + update_activity_history(); + update_activity_outputs(); } const RadarState &Ld2420Radar::get_state() const { return state; } -String Ld2420Radar::get_state_json() { +String Ld2420Radar::get_state_json() const { String json; - json.reserve(512); + json.reserve(192); json += "{"; - json += "\"timestamp_ms\":"; - json += String(state.timestamp_ms); - - json += ",\"online\":"; + json += "\"online\":"; json += (state.online ? "true" : "false"); - json += ",\"stale\":"; - json += (state.stale ? "true" : "false"); - json += ",\"presence\":"; json += (state.presence ? "true" : "false"); - json += ",\"raw_presence\":"; - json += (state.raw_presence ? "true" : "false"); - - json += ",\"raw_distance_zone\":"; - json += String(state.raw_distance_zone); - - json += ",\"median_distance_zone\":"; - json += String(state.median_distance_zone); - - json += ",\"filtered_distance_zone\":"; - json += String(state.filtered_distance_zone); - - json += ",\"distance_m\":"; - json += String(state.distance_m, 3); - - json += ",\"radial_speed_m_s\":"; - json += String(state.radial_speed_m_s, 3); - - json += ",\"direction\":\""; - json += direction_to_string(state.direction); - json += "\""; - json += ",\"activity_score\":"; json += String(state.activity_score); - json += ",\"confidence\":"; - json += String(state.confidence, 3); + json += ",\"activity_score_current\":"; + json += String(state.activity_score_current); - json += ",\"line_counter\":"; - json += String(state.line_counter); + json += ",\"activity_score_dynamics\":\""; + json += activity_dynamics_to_string(state.activity_score_dynamics); + json += "\""; - json += ",\"parse_error_counter\":"; - json += String(state.parse_error_counter); - - json += ",\"ignored_range_counter\":"; - json += String(state.ignored_range_counter); + json += ",\"distance_m\":"; + json += String(state.distance_m, 2); json += "}"; - state.state_json_cache = json; - return state.state_json_cache; + return json; } void Ld2420Radar::set_config(const RadarConfig &new_config) { @@ -132,41 +88,10 @@ return config; } -void Ld2420Radar::set_calibration_mode(bool enabled) { - calibration_mode = enabled; -} - -bool Ld2420Radar::is_calibration_mode() const { - return calibration_mode; -} - -String Ld2420Radar::get_calibration_line() const { - String line; - line.reserve(128); - - line += "raw="; - line += String(state.raw_distance_zone); - - line += " median="; - line += String(state.median_distance_zone); - - line += " filtered="; - line += String(state.filtered_distance_zone); - - line += " distance="; - line += String(state.distance_m, 3); - - line += " speed="; - line += String(state.radial_speed_m_s, 3); - - return line; -} - void Ld2420Radar::reset_runtime_state() { state = RadarState(); line_buffer = ""; - new_data_available = false; zone_window_count = 0; zone_window_index = 0; @@ -182,6 +107,16 @@ has_smoothed_speed = false; smoothed_speed_m_s = 0.0f; + + for (uint8_t i = 0; i < activity_bucket_count; i++) { + activity_buckets[i] = 0; + } + + activity_bucket_start_ms = 0; + activity_bucket_accum_sum = 0; + activity_bucket_accum_count = 0; + activity_bucket_index = 0; + activity_history_filled = false; } void Ld2420Radar::read_uart_lines() { @@ -214,7 +149,6 @@ return; } - state.online = true; state.line_counter++; state.last_update_ms = millis(); @@ -244,13 +178,11 @@ void Ld2420Radar::handle_on_line() { state.raw_presence = true; state.last_on_ms = state.last_update_ms; - new_data_available = true; } void Ld2420Radar::handle_off_line() { state.raw_presence = false; state.last_off_ms = state.last_update_ms; - new_data_available = true; } void Ld2420Radar::handle_range_line(const String &line) { @@ -275,10 +207,10 @@ state.raw_distance_zone = raw_zone; push_zone_sample(raw_zone); - uint16_t median_zone = get_median_zone(); - state.median_distance_zone = median_zone; + uint16_t median_zone = get_median_zone(); uint16_t filtered_zone = apply_zone_step_limit(median_zone); + state.filtered_distance_zone = filtered_zone; float raw_distance_m = zone_to_distance_m(filtered_zone); @@ -290,14 +222,10 @@ state.last_range_ms = now_ms; /* - Если пришёл Range, это уже полезный признак присутствия. - Даже если отдельная строка ON отсутствует, будем считать, - что радар видит цель. + Если пришёл Range, считаем это достаточным признаком присутствия. */ state.raw_presence = true; state.last_on_ms = now_ms; - - new_data_available = true; } void Ld2420Radar::push_zone_sample(uint16_t zone) { @@ -352,6 +280,16 @@ return candidate_zone; } +float Ld2420Radar::zone_to_distance_m(uint16_t zone) const { + float distance_m = static_cast(zone) * config.zone_to_meter_k + config.zone_to_meter_b; + + if (distance_m < 0.0f) { + distance_m = 0.0f; + } + + return distance_m; +} + float Ld2420Radar::apply_distance_smoothing(float new_distance_m) { if (!has_smoothed_distance) { has_smoothed_distance = true; @@ -393,43 +331,6 @@ state.radial_speed_m_s = smoothed_speed_m_s; } -void Ld2420Radar::update_direction() { - if (!state.presence && !state.raw_presence) { - state.direction = RADAR_DIRECTION_UNKNOWN; - return; - } - - if (state.radial_speed_m_s > config.speed_epsilon_m_s) { - state.direction = RADAR_DIRECTION_LEAVING; - } else if (state.radial_speed_m_s < -config.speed_epsilon_m_s) { - state.direction = RADAR_DIRECTION_APPROACHING; - } else { - state.direction = RADAR_DIRECTION_STATIC; - } -} - -void Ld2420Radar::update_activity_score() { - if (!state.presence && !state.raw_presence) { - state.activity_score = 0; - return; - } - - float abs_speed_m_s = fabs(state.radial_speed_m_s); - uint8_t score = 1; - - if (abs_speed_m_s >= 0.03f) score = 2; - if (abs_speed_m_s >= 0.07f) score = 3; - if (abs_speed_m_s >= 0.12f) score = 4; - if (abs_speed_m_s >= 0.20f) score = 5; - if (abs_speed_m_s >= 0.30f) score = 6; - if (abs_speed_m_s >= 0.45f) score = 7; - if (abs_speed_m_s >= 0.65f) score = 8; - if (abs_speed_m_s >= 0.90f) score = 9; - if (abs_speed_m_s >= 1.20f) score = 10; - - state.activity_score = score; -} - void Ld2420Radar::apply_presence_hold() { uint32_t now_ms = millis(); @@ -442,97 +343,217 @@ state.presence = true; } else { state.presence = false; - state.activity_score = 0; state.radial_speed_m_s = 0.0f; - state.direction = RADAR_DIRECTION_UNKNOWN; } } -void Ld2420Radar::update_stale_flag() { +void Ld2420Radar::update_online_state() { uint32_t now_ms = millis(); + state.stale = !(state.last_update_ms > 0 && (now_ms - state.last_update_ms) < config.stale_after_ms); + state.online = !state.stale; } -void Ld2420Radar::update_confidence() { - float conf = 0.0f; +uint8_t Ld2420Radar::speed_to_activity_score(float abs_speed_m_s) const { + if (!state.presence) { + return 0; + } + + if (abs_speed_m_s <= config.activity_min_speed_m_s) { + return 1; + } + + if (abs_speed_m_s >= config.activity_max_speed_m_s) { + return 10; + } + + float normalized = + (abs_speed_m_s - config.activity_min_speed_m_s) / + (config.activity_max_speed_m_s - config.activity_min_speed_m_s); + + int score = 1 + static_cast(normalized * 9.0f + 0.5f); + + if (score < 1) score = 1; + if (score > 10) score = 10; + + return static_cast(score); +} + +void Ld2420Radar::update_activity_history() { uint32_t now_ms = millis(); - if (state.online) { - conf += 0.25f; + if (activity_bucket_start_ms == 0) { + activity_bucket_start_ms = now_ms; } - if (!state.stale) { - conf += 0.25f; + /* + Каждый update складываем текущий score в накопитель текущего 10-секундного окна. + */ + uint8_t current_score = speed_to_activity_score(fabs(state.radial_speed_m_s)); + state.activity_score_current = current_score; + + activity_bucket_accum_sum += current_score; + activity_bucket_accum_count++; + + /* + Если прошло 10 секунд — фиксируем бакет. + Если прошло сразу несколько бакетов подряд, тоже корректно продвигаем окно. + */ + while ((now_ms - activity_bucket_start_ms) >= activity_bucket_duration_ms) { + commit_current_activity_bucket(activity_bucket_start_ms + activity_bucket_duration_ms); + } +} + +void Ld2420Radar::commit_current_activity_bucket(uint32_t next_bucket_start_ms) { + uint8_t average_score = 0; + + if (activity_bucket_accum_count > 0) { + average_score = static_cast( + activity_bucket_accum_sum / activity_bucket_accum_count + ); } - if (state.presence) { - conf += 0.20f; + activity_buckets[activity_bucket_index] = average_score; + + activity_bucket_index++; + if (activity_bucket_index >= activity_bucket_count) { + activity_bucket_index = 0; + activity_history_filled = true; } - if (state.filtered_distance_zone > 0) { - conf += 0.15f; + activity_bucket_accum_sum = 0; + activity_bucket_accum_count = 0; + activity_bucket_start_ms = next_bucket_start_ms; +} + +void Ld2420Radar::update_activity_outputs() { + /* + activity_score = средняя активность за последнюю минуту. + Так как бакет = 10 секунд, берём последние 6 бакетов. + */ + uint8_t minute_bucket_count = 6; + uint32_t sum = 0; + uint8_t count = 0; + + for (uint8_t i = 0; i < minute_bucket_count; i++) { + int index = static_cast(activity_bucket_index) - 1 - i; + if (index < 0) { + index += activity_bucket_count; + } + + /* + Если история ещё не заполнена, не читаем незаписанные хвосты. + */ + if (!activity_history_filled) { + if (index >= static_cast(activity_bucket_index)) { + continue; + } + } + + sum += activity_buckets[index]; + count++; } - if (zone_window_count >= 3) { - conf += 0.15f; + if (count > 0) { + state.activity_score = static_cast(sum / count); + } else { + state.activity_score = state.activity_score_current; } - if (state.parse_error_counter > 0) { - conf -= 0.10f; + state.activity_score_dynamics = calculate_activity_dynamics(); +} + +RadarActivityDynamics Ld2420Radar::calculate_activity_dynamics() const { + /* + Для оценки динамики за 10 минут используем 60 бакетов по 10 секунд. + Делим их на две половины: + - первые 5 минут + - последние 5 минут + + Если разница средних заметна — increasing/decreasing. + Если внутри ряда слишком большой разброс — variable. + Иначе constant. + */ + uint8_t available_count = activity_history_filled ? activity_bucket_count : activity_bucket_index; + + if (available_count < 12) { + return RADAR_ACTIVITY_DYNAMICS_CONSTANT; } - if (state.last_update_ms > 0 && (now_ms - state.last_update_ms) > config.stale_after_ms) { - conf -= 0.20f; + uint8_t half = available_count / 2; + + float first_sum = 0.0f; + float second_sum = 0.0f; + + for (uint8_t i = 0; i < half; i++) { + int index = static_cast(activity_bucket_index) - available_count + i; + while (index < 0) { + index += activity_bucket_count; + } + first_sum += activity_buckets[index]; } - if (conf < 0.0f) conf = 0.0f; - if (conf > 1.0f) conf = 1.0f; + for (uint8_t i = half; i < available_count; i++) { + int index = static_cast(activity_bucket_index) - available_count + i; + while (index < 0) { + index += activity_bucket_count; + } + second_sum += activity_buckets[index]; + } - state.confidence = conf; + float first_avg = first_sum / half; + float second_avg = second_sum / (available_count - half); + float delta = second_avg - first_avg; + + /* + Проверка на "variable": + если средний размах скачков между соседними бакетами большой, + динамика считается рваной. + */ + float diff_sum = 0.0f; + uint8_t diff_count = 0; + + for (uint8_t i = 1; i < available_count; i++) { + int prev_index = static_cast(activity_bucket_index) - available_count + (i - 1); + int curr_index = static_cast(activity_bucket_index) - available_count + i; + + while (prev_index < 0) prev_index += activity_bucket_count; + while (curr_index < 0) curr_index += activity_bucket_count; + + diff_sum += abs(static_cast(activity_buckets[curr_index]) - static_cast(activity_buckets[prev_index])); + diff_count++; + } + + float avg_diff = (diff_count > 0) ? (diff_sum / diff_count) : 0.0f; + + if (avg_diff >= 2.5f && fabs(delta) < 2.0f) { + return RADAR_ACTIVITY_DYNAMICS_VARIABLE; + } + + if (delta >= 1.5f) { + return RADAR_ACTIVITY_DYNAMICS_INCREASING; + } + + if (delta <= -1.5f) { + return RADAR_ACTIVITY_DYNAMICS_DECREASING; + } + + return RADAR_ACTIVITY_DYNAMICS_CONSTANT; +} + +const char *Ld2420Radar::activity_dynamics_to_string(RadarActivityDynamics dynamics) const { + switch (dynamics) { + case RADAR_ACTIVITY_DYNAMICS_INCREASING: + return "increasing"; + case RADAR_ACTIVITY_DYNAMICS_DECREASING: + return "decreasing"; + case RADAR_ACTIVITY_DYNAMICS_VARIABLE: + return "variable"; + default: + return "constant"; + } } bool Ld2420Radar::is_zone_valid(uint16_t zone) const { return zone >= config.min_valid_zone && zone <= config.max_valid_zone; -} - -float Ld2420Radar::zone_to_distance_m(uint16_t zone) const { - float distance_m = static_cast(zone) * config.zone_to_meter_k + config.zone_to_meter_b; - - if (distance_m < 0.0f) { - distance_m = 0.0f; - } - - return distance_m; -} - -const char *Ld2420Radar::direction_to_string(RadarDirection direction) const { - switch (direction) { - case RADAR_DIRECTION_STATIC: - return "static"; - case RADAR_DIRECTION_APPROACHING: - return "approaching"; - case RADAR_DIRECTION_LEAVING: - return "leaving"; - default: - return "unknown"; - } -} - -String Ld2420Radar::escape_json_string(const String &value) const { - String out; - out.reserve(value.length() + 8); - - for (size_t i = 0; i < value.length(); i++) { - char c = value.charAt(i); - - if (c == '\"') { - out += "\\\""; - } else if (c == '\\') { - out += "\\\\"; - } else { - out += c; - } - } - - return out; } \ No newline at end of file diff --git a/devices/sensor/ld2420_radar.h b/devices/sensor/ld2420_radar.h index 31dad63..0c48bdf 100644 --- a/devices/sensor/ld2420_radar.h +++ b/devices/sensor/ld2420_radar.h @@ -4,80 +4,147 @@ #include /* - Перечисление направления движения цели относительно радара. - Здесь мы оцениваем только радиальную составляющую: - - approaching -> объект приближается к радару - - leaving -> объект удаляется от радара - - static -> заметного движения по дистанции нет - - unknown -> данных недостаточно + Упрощённая динамика активности. + + constant: + Активность в среднем не растёт и не падает. + + increasing: + Во второй половине 10-минутного окна активность заметно выше, + чем в первой. + + decreasing: + Во второй половине 10-минутного окна активность заметно ниже, + чем в первой. + + variable: + Активность слишком рваная/хаотичная, простая линейная трактовка + не даёт хорошего описания. */ -enum RadarDirection { - RADAR_DIRECTION_UNKNOWN = 0, - RADAR_DIRECTION_STATIC, - RADAR_DIRECTION_APPROACHING, - RADAR_DIRECTION_LEAVING +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: - Есть ли признак того, что UART от радара вообще живой. - - stale: - Данные давно не обновлялись, значит состояние устарело. + Есть актуальные данные от радара. presence: - Отфильтрованное присутствие с удержанием presence_hold_ms. - - raw_presence: - Сырое присутствие из последних сообщений ON/OFF/Range. - - raw_distance_zone: - Какой Range пришёл от радара напрямую. - - median_distance_zone: - После медианного фильтра по окну последних измерений. - - filtered_distance_zone: - После ограничения скачка между соседними измерениями. - - distance_m: - Отфильтрованная и откалиброванная дистанция в метрах. - - radial_speed_m_s: - Оценка радиальной скорости в м/с. + Есть присутствие после фильтрации и hold-механизма. activity_score: - Грубая активность 0..10 на основе модуля скорости. + Средняя активность за последнюю минуту, шкала 0..10. - confidence: - Оценка качества текущего состояния сенсора, а не вероятность - обнаружения человека. + activity_score_current: + Текущая активность, шкала 0..10. - state_json_cache: - Кэш JSON-строки. Формируется по запросу через get_state_json(). + activity_score_dynamics: + Характер динамики активности за последние 10 минут. + + distance_m: + Отфильтрованная текущая дистанция до цели. */ struct RadarState { - uint32_t timestamp_ms = 0; - bool online = false; - bool stale = true; - bool presence = false; - bool raw_presence = false; - - uint16_t raw_distance_zone = 0; - uint16_t median_distance_zone = 0; - uint16_t filtered_distance_zone = 0; - - float distance_m = -1.0f; - float radial_speed_m_s = 0.0f; - - RadarDirection direction = RADAR_DIRECTION_UNKNOWN; uint8_t activity_score = 0; - float confidence = 0.0f; + 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; @@ -87,185 +154,57 @@ uint32_t line_counter = 0; uint32_t parse_error_counter = 0; uint32_t ignored_range_counter = 0; - - String state_json_cache; }; /* - Настройки драйвера радара. + Первый рабочий слой для LD2420 в текстовом UART-режиме. - uart_rx_pin / uart_tx_pin: - Пины ESP32, на которые заведён UART радара. + Что умеет: + - читать ON / OFF / Range N + - фильтровать Range + - считать distance_m + - считать текущую активность + - считать усреднённую активность за минуту + - оценивать динамику активности за 10 минут + - отдавать данные как JSON-строку - baud_rate: - Скорость UART. Для твоего экземпляра сейчас 115200. - - presence_hold_ms: - Сколько миллисекунд удерживать presence=true после исчезновения - сырых сообщений о присутствии. - - stale_after_ms: - Через сколько миллисекунд без данных считать сенсор stale. - - median_window_size: - Размер окна медианного фильтра. - Для простоты поддерживаются значения до 9. - - max_zone_step_per_sample: - Максимально допустимый скачок filtered_distance_zone за один шаг. - Если медиана прыгнула сильнее, скачок будет ограничен. - - distance_ema_alpha: - Коэффициент EMA для дистанции. Чем больше, тем быстрее реакция - и меньше сглаживание. - - speed_ema_alpha: - Коэффициент EMA для скорости. - - speed_epsilon_m_s: - Порог, ниже которого скорость считается статической. - - min_valid_zone / max_valid_zone: - Диапазон допустимых Range. Всё вне диапазона отбрасывается. - - zone_to_meter_k: - Коэффициент перевода зоны в метры. - - zone_to_meter_b: - Смещение перевода зоны в метры: - distance_m = zone * k + b - - enable_debug_unknown_lines: - Если true, можно будет смотреть строки, которые не распарсились. -*/ -struct RadarConfig { - int uart_rx_pin = 4; - int uart_tx_pin = 15; - uint32_t baud_rate = 115200; - - uint32_t presence_hold_ms = 1500; - uint32_t stale_after_ms = 2000; - - uint8_t median_window_size = 5; - uint16_t max_zone_step_per_sample = 4; - - float distance_ema_alpha = 0.35f; - float speed_ema_alpha = 0.25f; - float speed_epsilon_m_s = 0.08f; - - uint16_t min_valid_zone = 1; - uint16_t max_valid_zone = 200; - - float zone_to_meter_k = 0.7f; - float zone_to_meter_b = 0.0f; - - bool enable_debug_unknown_lines = false; -}; - -/* - Класс первого сенсорного слоя для HLK-LD2420 в текстовом режиме UART. - - Назначение класса: - - читать строки вида ON / OFF / Range N - - фильтровать поток - - хранить актуальное состояние радара - - отдавать состояние как структуру и как JSON - - Этот слой намеренно не занимается: - - Wi-Fi / HTTP / MQTT - - объединением нескольких радаров - - распознаванием людей/котов - - логикой комнаты/дома + Что пока не умеет: + - несколько целей + - кошки/люди + - углы и направления по комнате + - fusion нескольких радаров */ class Ld2420Radar { public: Ld2420Radar(); - /* - Инициализация драйвера. - - uart_port: - Ссылка на UART-порт, например Serial2. - - config: - Пользовательская конфигурация. - */ void begin(HardwareSerial &uart_port, const RadarConfig &config); - - /* - Обновление состояния радара. - Должен вызываться часто в loop(). - */ void update(); - /* - Есть ли новые осмысленные данные после последнего чтения. - Полезно, если позже захочешь отправлять обновления только по событию. - */ - bool has_new_data() const; - - /* - Сброс флага новых данных после обработки. - */ - void clear_new_data_flag(); - - /* - Получить текущее состояние сенсора. - */ const RadarState &get_state() const; + String get_state_json() const; - /* - Получить JSON-строку текущего состояния. - - Формат, например: - { - "timestamp_ms":123, - "online":true, - ... - } - */ - String get_state_json(); - - /* - Обновить конфигурацию на лету. - Полезно для будущей интеграции с системой настроек. - */ void set_config(const RadarConfig &config); - - /* - Получить текущую конфигурацию. - */ const RadarConfig &get_config() const; - /* - Включить/выключить режим калибровочного лога. - Если включён, можно удобно печатать короткие строки для ручной - калибровки зависимости Range -> distance. - */ - void set_calibration_mode(bool enabled); - - /* - Узнать, включён ли режим калибровки. - */ - bool is_calibration_mode() const; - - /* - Сформировать компактную строку для режима калибровки. - Например: - raw=57 median=56 filtered=55 distance=3.24 speed=-0.12 - */ - String get_calibration_line() 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; + HardwareSerial *uart; RadarConfig config; RadarState state; String line_buffer; - bool new_data_available; - bool calibration_mode; uint16_t zone_window[max_supported_median_window]; uint8_t zone_window_count; @@ -280,6 +219,17 @@ 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(); @@ -291,20 +241,24 @@ 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 update_direction(); - void update_activity_score(); void apply_presence_hold(); - void update_stale_flag(); - void update_confidence(); + 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; - float zone_to_distance_m(uint16_t zone) const; - - const char *direction_to_string(RadarDirection direction) const; - String escape_json_string(const String &value) const; }; #endif \ No newline at end of file diff --git a/devices/sensor/sensor.ino b/devices/sensor/sensor.ino index a9b4bb2..6c0c5cd 100644 --- a/devices/sensor/sensor.ino +++ b/devices/sensor/sensor.ino @@ -32,12 +32,6 @@ static const uint32_t print_interval_ms = 500; uint32_t last_print_ms = 0; -/* - Интервал печати калибровочной строки радара. -*/ -static const uint32_t calibration_print_interval_ms = 250; -uint32_t last_calibration_print_ms = 0; - /* ========================= @@ -46,25 +40,59 @@ */ void setup_radar_config() { + /* + Подключение радара: + OT1 радара -> GPIO4 ESP32 (RX) + RX радара -> GPIO15 ESP32 (TX) + */ radar_config.uart_rx_pin = 4; radar_config.uart_tx_pin = 15; radar_config.baud_rate = 115200; + /* + Удержание presence после кратковременного исчезновения сырых данных. + */ radar_config.presence_hold_ms = 1500; + + /* + Если давно не было новых строк от радара, online станет false. + */ radar_config.stale_after_ms = 2000; + /* + Базовые фильтры Range. + */ radar_config.median_window_size = 5; radar_config.max_zone_step_per_sample = 4; + + /* + EMA-сглаживание дистанции и скорости. + */ radar_config.distance_ema_alpha = 0.35f; radar_config.speed_ema_alpha = 0.25f; - radar_config.speed_epsilon_m_s = 0.08f; + /* + Пока широкий диапазон допустимых зон. + Позже подрежешь под корпус, сектор и реальную геометрию установки. + */ radar_config.min_valid_zone = 1; radar_config.max_valid_zone = 200; + /* + Стартовая калибровка перевода зоны в метры. + Потом желательно подогнать по реальным замерам. + */ radar_config.zone_to_meter_k = 0.7f; radar_config.zone_to_meter_b = 0.0f; + /* + Шкала текущей активности. + Ниже activity_min_speed_m_s движение почти не учитывается. + Около activity_max_speed_m_s текущая активность выходит к 10. + */ + radar_config.activity_min_speed_m_s = 0.03f; + radar_config.activity_max_speed_m_s = 1.20f; + radar_config.enable_debug_unknown_lines = false; } @@ -127,9 +155,7 @@ void init_radar() { setup_radar_config(); - radar.begin(Serial2, radar_config); - radar.set_calibration_mode(false); Serial.println("Radar initialized"); } @@ -192,18 +218,23 @@ uint32_t now_ms = millis(); - if (!radar.is_calibration_mode()) { - if (now_ms - last_print_ms >= print_interval_ms) { - last_print_ms = now_ms; + if (now_ms - last_print_ms >= print_interval_ms) { + last_print_ms = now_ms; - Serial.println(radar.get_state_json()); - Serial.println(light_sensor.get_state_json()); - Serial.println(climate_sensor.get_state_json()); - } - } else { - if (now_ms - last_calibration_print_ms >= calibration_print_interval_ms) { - last_calibration_print_ms = now_ms; - Serial.println(radar.get_calibration_line()); - } + /* + Каждый сенсор отдаёт свой JSON независимо. + Радар теперь отдаёт упрощённую структуру вида: + { + "online": true, + "presence": true, + "activity_score": 5, + "activity_score_current": 4, + "activity_score_dynamics": "increasing", + "distance_m": 2.3 + } + */ + Serial.println(radar.get_state_json()); + Serial.println(light_sensor.get_state_json()); + Serial.println(climate_sensor.get_state_json()); } } \ No newline at end of file diff --git a/docs/devices/sensor.md b/docs/devices/sensor.md new file mode 100644 index 0000000..fa41167 --- /dev/null +++ b/docs/devices/sensor.md @@ -0,0 +1,60 @@ +### `GET /status` +У все х датчиков есть поле `online`, если датчик на связи и устройство получает с него данные, всегда будет `true`. Устройство так же имеет rgb индикатор, однако управления им не предоставляется, он может быть использован самим устройством например для самодиагностики или индикации пользователю о текущем состоянии устройства. Значения индикатора аналогичны устройству **button** + +#### Пример запроса состояния сенсоров + +```json +{ + "status": "ok", + "indicators": "nowifi", + "sensors": { + "light": { + "online": true, // true | false + "level": 1, // 0...10 ( Уровень освещения ) + "lux": 160.15, // Уровень освещения в lux + "percent": 11, // Уровень освещения в % + }, + "temperature": { + "online": true, // true | false + "current": 27.42, // Текущая температура в цельсиях + "dynamics": "decreasing", // constant, increasing, decreasing, variable ( Характер динамики температуры за 10 минут ) + "dynamics_val": -2.11, // среднее изменения температуры в минуту, за период 10 минут в цельсиях. + }, + "pressure": { + "online": true, // true | false + "current": 995.36, // Текущее давление в hpa + "dynamics": "decreasing", // constant, increasing, decreasing, variable ( Характер динамики давления за 10 минут ) + "dynamics_val": -2.11, // среднее изменения давления в минуту, за период 10 минут в hpa. + }, + "humidity": { + "online": true, // true | false + "current": 41.32, // Текущая влажность в % + "dynamics": "increasing", // constant, increasing, decreasing, variable ( Характер динамики влажности за 10 минут ) + "dynamics_val": 0.11, // среднее изменения температуры в минуту, за период 10 минут в процентах. + }, + "radar": { + "online": true, // true | false + "presence": true, // true | false ( Присутствие ) + "activity_score": 5, // 0...10 ( Оценка активности в комнате за последнюю минуту ) + "activity_score_current": 4, // 0...10 ( Оценка активности в комнате в момент запроса ) + "activity_score_dynamics": "increasing", // constant, increasing, decreasing, variable ( Характер динамики активности за 10 минут ) + "distance_m": 2.3, // 0...~7 ( Дистанция от датчика до человека в комнате ) + }, + "microphone": { + "online": true, + "current_noise": 65, // dBi ( Уровень шума в момент запроса ) + "noise_level": 4, // 0...10 ( Средний уровень шума за минуту ) + "noise_level_dbi": 65, // dBi ( Средний уровень шума за минуту в dBi ) + "noise_dynamics": "constant", // constant, increasing, decreasing, variable ( Характер динамики шума за последние 10 минут ) + } + } +} + +``` +--- + +## Events + +#### `presence_changed` +Событие генерируется при изменении присутствия в помещении. Например, "в помещении больше никто не присутствует" или "в помещении возникло присутствие". В качестве данных будут переданы данные радара +