"""
Живая стрелка компаса для GY-271: наглядная визуализация азимута для демонстрации детям.
Периодически посылает команду 'r' в прошивку (main.cpp) и парсит её текстовый ответ,
поворачивая стрелку на экране в реальном времени.

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

До калибровки (команда 'c' в Serial Monitor) азимут не считается, и стрелка
не появится — это ожидаемо, см. THEORY.md, раздел 4 "Калибровка Hard-Iron:
без неё никак". После калибровки стрелка должна стабильно указывать в одну
сторону, даже если поворачивать саму плату в руках.
"""
import re
import sys
import time

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

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

POLL_INTERVAL_S = 0.3  # как часто посылать команду 'r'

# Строка, которую печатает main.cpp в printReading() при команде 'r':
#   X:120 Y:-340 Z:50 | Азимут: 87.3° - ВОСТОК
HEADING_RE = re.compile(r"Азимут:\s*([\d.]+)")
NOT_CALIBRATED_RE = re.compile(r"не откалибровано")


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


def read_lines(ser) -> 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("Если стрелка не появляется — откалибруйте датчик командой 'c' в Serial Monitor.")

    fig = plt.figure(figsize=(5, 5))
    fig.patch.set_facecolor("#06210f")
    fig.suptitle("GY-271 — азимут", color="#5fff8f")
    ax = fig.add_subplot(projection="polar")
    ax.set_facecolor("#06210f")
    ax.set_theta_zero_location("N")
    ax.set_theta_direction(-1)  # по часовой стрелке, как на компасе
    ax.set_ylim(0, 1)
    ax.set_yticks([])
    ax.tick_params(colors="#5fff8f")
    for label, angle in (("С", 0), ("В", 90), ("Ю", 180), ("З", 270)):
        ax.text(angle * 3.14159 / 180, 1.15, label, color="#5fff8f",
                 ha="center", va="center", fontsize=12)

    arrow, = ax.plot([0, 0], [0, 1], color="#ff3b3b", linewidth=3)
    status_text = fig.text(0.5, 0.02, "Ожидание калибровки...", ha="center",
                            color="#5fff8f", fontsize=11)

    state = {"last_poll": 0.0}

    def update(_frame):
        now = time.monotonic()
        if now - state["last_poll"] >= POLL_INTERVAL_S:
            ser.write(b"r")
            state["last_poll"] = now

        for line in read_lines(ser):
            if NOT_CALIBRATED_RE.search(line):
                status_text.set_text("Не откалибровано — введите 'c' в Serial Monitor")
                continue

            match = HEADING_RE.search(line)
            if not match:
                continue

            heading_deg = float(match.group(1))
            heading_rad = heading_deg * 3.14159 / 180
            arrow.set_data([heading_rad, heading_rad], [0, 1])
            status_text.set_text(f"Азимут: {heading_deg:.1f}°")

        return [arrow, status_text]

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


if __name__ == "__main__":
    main()
