"""
Радар-экран для HMMD-mmWave: наглядная визуализация присутствия цели для демонстрации детям.
Читает обычный текстовый вывод прошивки из Serial Monitor (main.cpp, режим по умолчанию —
НЕ 'r'/сырой дамп) и рисует классический вид радара: сектор обзора ±60°, кольца дальности
до 2.1 м и дугу на дистанции до цели.

Запуск:
    pip install -r requirements.txt
    python3 radar_view.py [/dev/ttyUSB0]

Важно про угол: датчик HMMD отдаёт по UART только ДИСТАНЦИЮ до цели, без направления
(азимута) — см. THEORY.md, раздел 3 ("Протокол — что подтверждено, а что нет"). Поэтому
цель здесь рисуется как дуга через весь сектор обзора на измеренной дистанции, а не как
точка в конкретном направлении — так и есть на самом деле, никакой угол не выдумывается.
Сектор ±60° — это угол обзора датчика по документации поставщика (см. THEORY.md, раздел 2),
а не измеренное направление на цель.
"""
import re
import sys
from collections import deque

import matplotlib.pyplot as plt
import numpy as np
import serial
from matplotlib.animation import FuncAnimation

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
BAUD = 115200  # должно совпадать с Serial.begin() в main.cpp

# Должно совпадать с kMaxDistanceM в main.cpp — дальше этой отметки радар цель не учитывает.
MAX_DISTANCE_M = 2.10
HALF_FOV_DEG = 60  # угол обзора датчика ±60° по документации (THEORY.md, раздел 2) — не измерение

# Строки, которые печатает main.cpp в обычном режиме (не 'r'):
#   [TARGET] дистанция=1.05 м формат=ASCII isAlive=1
#   [EMPTY]  зона свободна  формат=ASCII isAlive=1
#   [WARN] Нет данных от радара — ...
TARGET_RE = re.compile(r"\[TARGET\]\s+дистанция=([\d.]+)\s*м")
EMPTY_RE = re.compile(r"\[EMPTY\]")
WARN_RE = re.compile(r"\[WARN\]")

TRAIL_LEN = 6  # сколько последних дистанций подсвечивать угасающим следом, для ощущения "живости"


def open_port(port: str) -> serial.Serial:
    try:
        return serial.Serial(port, BAUD, timeout=0.1)
    except serial.SerialException as e:
        print(f"Не удалось открыть {port}: {e}")
        print("Проверьте порт (ls /dev/ttyUSB*) и что прошивка запущена в обычном режиме (не 'r').")
        sys.exit(1)


def read_lines(ser: serial.Serial) -> list[str]:
    if ser.in_waiting == 0:
        return []
    raw = ser.read(ser.in_waiting).decode("utf-8", errors="ignore")
    return [line.strip() for line in raw.split("\n") if line.strip()]


def main():
    ser = open_port(PORT)
    print(f"Подключено к {PORT} @ {BAUD}.")
    print("Если экран не оживает — проверьте, что Serial Monitor прошивки закрыт (порт занят только одной программой).")

    fig = plt.figure(figsize=(7, 7))
    fig.suptitle("HMMD mmWave — радар присутствия (вид сверху)")
    ax = fig.add_subplot(111, projection="polar")

    # Радар смотрит "вверх" (0° экрана) и рисуется только в верхнем полукруге.
    ax.set_theta_zero_location("N")
    ax.set_theta_direction(-1)
    ax.set_thetamin(-HALF_FOV_DEG)
    ax.set_thetamax(HALF_FOV_DEG)
    ax.set_ylim(0, MAX_DISTANCE_M)
    ax.set_rlabel_position(135)
    ax.set_facecolor("#06210f")
    fig.patch.set_facecolor("#06210f")
    ax.tick_params(colors="#5fff8f")
    ax.spines["polar"].set_color("#5fff8f")
    ax.grid(color="#5fff8f", alpha=0.3)
    ax.set_title("Зона контроля: 0–%.2f м, сектор ±%d°" % (MAX_DISTANCE_M, HALF_FOV_DEG),
                 color="#5fff8f", pad=20)

    theta = np.linspace(np.radians(-HALF_FOV_DEG), np.radians(HALF_FOV_DEG), 200)

    # Дуга текущей цели (пусто, пока нет данных).
    (target_line,) = ax.plot([], [], color="#ff3b3b", lw=4, alpha=0.9, zorder=5)
    # Угасающий след последних измерений — создаёт ощущение "живого" сканирования.
    trail_lines = [ax.plot([], [], color="#ff3b3b", lw=3)[0] for _ in range(TRAIL_LEN)]
    for i, ln in enumerate(trail_lines):
        ln.set_alpha(0.35 * (1 - i / TRAIL_LEN))

    status_text = ax.text(0, 0, "", transform=ax.transAxes, ha="left", va="top",
                           fontsize=11, color="#5fff8f")

    trail: deque[float] = deque(maxlen=TRAIL_LEN)
    state = {"text": "Ожидание данных...", "alive": False}

    def update(_frame):
        for line in read_lines(ser):
            m = TARGET_RE.search(line)
            if m:
                dist = float(m.group(1))
                trail.appendleft(dist)
                state["text"] = f"ЦЕЛЬ ОБНАРУЖЕНА\nдистанция: {dist:.2f} м"
                state["alive"] = True
            elif EMPTY_RE.search(line):
                trail.clear()
                state["text"] = "Зона свободна"
                state["alive"] = True
            elif WARN_RE.search(line):
                trail.clear()
                state["text"] = "НЕТ СВЯЗИ С ДАТЧИКОМ"
                state["alive"] = False

        if trail:
            target_line.set_data(theta, np.full_like(theta, trail[0]))
            for i, ln in enumerate(trail_lines):
                if i < len(trail):
                    ln.set_data(theta, np.full_like(theta, trail[i]))
                else:
                    ln.set_data([], [])
        else:
            target_line.set_data([], [])
            for ln in trail_lines:
                ln.set_data([], [])

        color = "#5fff8f" if state["alive"] else "#ff3b3b"
        status_text.set_text(state["text"])
        status_text.set_color(color)

        return [target_line, status_text, *trail_lines]

    ani = FuncAnimation(fig, update, interval=100, blit=False, cache_frame_data=False)
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()
