#!/usr/bin/env python3
"""PC control and time synchronizer for the STM32 WSPR transmitter.

The firmware accepts ASCII commands on USART1:
  SET CALL CALLSIGN
  SET GRID GRID
  TIME HH:MM:SS
  AUTO ON

This tool configures the WSPR identity, then sends UTC time once per second.
Keep it running when using the PC as the time source.
"""

from __future__ import annotations

import argparse
import datetime as dt
import re
import sys
import time


TITLE = r"""
   _____ _______ __  __ ____ ___  ____       __        ______  ____  ____  
  / ___//_  __//  |/  // __ \__ \| | /      / /       / / __ \/ __ \/ __ \ 
  \__ \  / /  / /|_/ // /_/ /_/ /| |/ /____/ /  __  / / /_/ / /_/ / /_/ / 
 ___/ / / /  / /  / // ____/ __/ |   /____/ /__/ /_/ / ____/ _, _/ ____/  
/____/ /_/  /_/  /_//_/   /____/ |_|\_\   \____/\____/_/   /_/ |_/_/      
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Configure STM32 WSPR and send UTC time.")
    parser.add_argument("port", help="Serial port, for example COM7 or /dev/ttyUSB0")
    parser.add_argument("--baud", type=int, default=115200, help="Baud rate, default 115200")
    parser.add_argument("--no-auto", action="store_true", help="Do not send AUTO 1 at startup")
    parser.add_argument("--once", action="store_true", help="Send one TIME command and exit")
    parser.add_argument("--call", help="WSPR callsign, max 6 alphanumeric characters")
    parser.add_argument("--grid", help="4-character Maidenhead grid, for example OM85")
    parser.add_argument("--power", type=int, help="WSPR power in dBm, 0..60")
    parser.add_argument("--freq", type=int, help="Direct RF output frequency in Hz")
    return parser.parse_args()


def print_banner() -> None:
    print(TITLE)
    print("STM32 WSPR PC Terminal")
    print("Author: bi4loj")
    print()


def validate_call(callsign: str) -> str:
    callsign = callsign.strip().upper()
    if not re.fullmatch(r"[A-Z0-9]{3,6}", callsign) or not any(ch.isdigit() for ch in callsign):
        raise ValueError("callsign must be 3..6 alphanumeric chars and contain a digit")
    return callsign


def validate_grid(grid: str) -> str:
    grid = grid.strip().upper()
    if not re.fullmatch(r"[A-R]{2}[0-9]{2}", grid):
        raise ValueError("grid must be a 4-character Maidenhead locator, for example OM85")
    return grid


def prompt_value(label: str, validator, default: str | None = None) -> str:
    while True:
        suffix = f" [{default}]" if default else ""
        value = input(f"{label}{suffix}: ").strip()
        if not value and default:
            value = default
        try:
            return validator(value)
        except ValueError as exc:
            print(f"Invalid {label.lower()}: {exc}")


def read_available(ser) -> None:
    waiting = getattr(ser, "in_waiting", 0)
    if waiting:
        data = ser.read(waiting)
        if data:
            print(data.decode("ascii", errors="replace"), end="")


def send_command(ser, command: str, delay: float = 0.08) -> None:
    print(f"> {command}")
    ser.write((command + "\r\n").encode("ascii"))
    time.sleep(delay)
    read_available(ser)


def main() -> int:
    args = parse_args()
    print_banner()

    try:
        callsign = validate_call(args.call) if args.call else prompt_value("Callsign", validate_call, "BI4LOJ")
        grid = validate_grid(args.grid) if args.grid else prompt_value("Grid", validate_grid, "OM85")
    except ValueError as exc:
        print(f"configuration error: {exc}", file=sys.stderr)
        return 2

    if args.power is not None and not (0 <= args.power <= 60):
        print("configuration error: power must be 0..60 dBm", file=sys.stderr)
        return 2

    if args.freq is not None and not (2_500_000 <= args.freq <= 150_000_000):
        print("configuration error: freq must be 2500000..150000000 Hz", file=sys.stderr)
        return 2

    try:
        import serial
    except ImportError:
        print("pyserial is required: python -m pip install pyserial", file=sys.stderr)
        return 2

    try:
        with serial.Serial(args.port, args.baud, timeout=1) as ser:
            time.sleep(0.2)
            read_available(ser)
            send_command(ser, f"set call {callsign}")
            send_command(ser, f"set grid {grid}")
            if args.power is not None:
                send_command(ser, f"set power {args.power}")
            if args.freq is not None:
                send_command(ser, f"set freq {args.freq}")
            if not args.no_auto:
                send_command(ser, "auto on")
            else:
                send_command(ser, "auto off")
            send_command(ser, "show")

            while True:
                now = dt.datetime.now(dt.UTC)
                command = f"time {now:%H:%M:%S}"
                send_command(ser, command, delay=0.02)

                if args.once:
                    return 0

                sleep_s = 1.0 - (time.time() % 1.0)
                time.sleep(sleep_s)
    except serial.SerialException as exc:
        print(f"serial error: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
