# -*- coding: utf-8 -*-
"""Duga — ЗНАКОМСТВО ПО ПАРОЛЮ НА ЖИВЫХ ПЛАТАХ.

=====================================================================
ЧТО ЭТОТ ОПЫТ ДОКАЗЫВАЕТ, А ЧТО НЕТ
=====================================================================
ДОКАЗЫВАЕТ: две настоящие рации, стоя на столе, договариваются об
общем ключе через настоящий эфир, имея из общего только восемь цифр;
после этого каждая знает другую и помнит её после выключения.

НЕ ДОКАЗЫВАЕТ, что шифр стойкий. Стойкость проверяется не опытом, а
сверкой с официальными примерами — она сделана отдельно
(ШИФР_ПРОВЕРКА.bat, КЛЮЧИ_ПРОВЕРКА.bat, КОНВЕРТ_ПРОВЕРКА.bat). Здесь
проверяется другое: что написанное работает на железе, а не только
на компьютере.

=====================================================================
ПОЧЕМУ ПАРОЛЬ ЗАДАЁТСЯ ПО ПРОВОДУ, А НЕ ПАЛЬЦАМИ
=====================================================================
Пальцами его будет вводить человек — для того меню и делается. Но
проверку надо прогонять много раз подряд и видеть ЧИСЛА, а не глаза.
Провод — это хозяин: у кого плата в руках, тот и так может её
перепрошить. В эфир из пароля не уходит ни один байт (см. доводы в
duga_znakomstvo_parol.h).

=====================================================================
ЗАПУСК
=====================================================================
    py -3 znakomstvo_opyt.py
    py -3 znakomstvo_opyt.py --parol=47190283
"""
import io
import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stdout.reconfigure(line_buffering=True)

try:
    import serial
    from serial.tools import list_ports
except ImportError:
    print("нет модуля pyserial"); sys.exit(1)

FEND, FESC, TFEND, TFESC = 0xC0, 0xDB, 0xDC, 0xDD
CMD_NASTROYKA   = 0xA5
CMD_ZNAKOMSTVO  = 0xAD
N_SBORKA   = 34
N_NOMER    = 45
N_KNIZHKA  = 73
N_ZNAKOMYH = 74
N_ZNAK_SHAG = 76
N_SCHET    = 77

ШАГИ = {0: "тихо", 1: "зовёт", 2: "отвечает", 3: "подтвердила",
        4: "ВЫШЛО", 5: "не вышло", 6: "нет случайности"}
КНИЖКА = {0: "цела", 1: "заведена впервые", 252: "не та раскладка",
          253: "обрублена", 254: "испорчена"}

ит = []


def шаг(имя, вышло, пояснение=""):
    ит.append((имя, bool(вышло)))
    print("  %s %s%s" % ("✅" if вышло else "🔴", имя,
                         ("   — " + пояснение) if пояснение else ""))


def открыть(порт):
    п = serial.Serial()
    п.port = порт
    п.baudrate = 115200
    п.timeout = 0.6
    п.write_timeout = 3.0
    п.dtr = True
    п.rts = False
    п.open()
    return п


def кадр(cmd, тело=b""):
    v = bytearray([FEND, cmd])
    for b in тело:
        if b == FEND:   v += bytes([FESC, TFEND])
        elif b == FESC: v += bytes([FESC, TFESC])
        else:           v.append(b)
    v.append(FEND)
    return bytes(v)


def кадры(сырое):
    из, тек, в_кадре, экр = [], bytearray(), False, False
    for b in сырое:
        if b == FEND:
            if в_кадре and тек:
                из.append(bytes(тек))
            тек, в_кадре, экр = bytearray(), True, False
            continue
        if not в_кадре:
            continue
        if экр:
            if b == TFEND: b = FEND
            elif b == TFESC: b = FESC
            экр = False
        elif b == FESC:
            экр = True
            continue
        тек.append(b)
    return из


def настройка(п, номер, попыток=3):
    for _ in range(попыток):
        try:
            п.reset_input_buffer()
            п.write(кадр(CMD_NASTROYKA, bytes([номер])))
        except Exception:
            return None
        time.sleep(0.35)
        try:
            сырое = п.read(8192)
        except Exception:
            return None
        for k in кадры(сырое):
            if k and k[0] == CMD_NASTROYKA and len(k) >= 6 and k[1] == номер:
                v = int.from_bytes(k[2:6], "big")
                return None if v == 0xFFFFFFFF else v
    return None


def знакомство(п, парольные_цифры=None):
    """Спросить состояние или начать знакомство. Возвращает (шаг, знакомых)."""
    тело = парольные_цифры.encode("ascii") if парольные_цифры else b""
    for _ in range(3):
        try:
            п.reset_input_buffer()
            п.write(кадр(CMD_ZNAKOMSTVO, тело))
        except Exception:
            return None
        time.sleep(0.35)
        try:
            сырое = п.read(8192)
        except Exception:
            return None
        for k in кадры(сырое):
            if k and k[0] == CMD_ZNAKOMSTVO and len(k) >= 3:
                return (k[1], k[2])
    return None


def какая(п_инфо):
    имя = ((п_инфо.description or "") + " " + str(п_инфо.hwid or "")).upper()
    if "239A" in имя or "ADAFRUIT" in имя: return "T114"
    if "303A" in имя: return "V4"
    if any(x in имя for x in ("10C4", "CP210", "SILICON LABS", "1A86", "CH340")): return "V3"
    return None


def главное():
    пароль = "47190283"
    for а in sys.argv[1:]:
        if а.startswith("--parol="):
            пароль = а.split("=", 1)[1].strip()
    if len(пароль) != 8 or not пароль.isdigit():
        print("пароль — ровно восемь цифр"); return 1

    print("=" * 70)
    print("DUGA — ЗНАКОМСТВО ПО ПАРОЛЮ НА ЖИВЫХ ПЛАТАХ")
    print("=" * 70)
    print("  пароль на этот раз: %s (одноразовый — так и задумано)" % пароль)
    print()

    платы = []
    for пи in list_ports.comports():
        вид = какая(пи)
        if not вид:
            continue
        try:
            с = открыть(пи.device)
        except Exception:
            continue
        time.sleep(1.6)
        сб = настройка(с, N_SBORKA)
        if сб is None:
            с.close(); continue
        платы.append({"порт": пи.device, "вид": вид, "с": с, "сборка": сб,
                      "номер": настройка(с, N_NOMER)})

    for п in платы:
        print("  %-6s %-5s сборка %s, номер %s"
              % (п["порт"], п["вид"], п["сборка"], п["номер"]))
    шаг("наших плат на проводе хотя бы две", len(платы) >= 2, "нашлось %d" % len(платы))
    if len(платы) < 2:
        return итог()

    умеют = [п for п in платы if знакомство(п["с"]) is not None]
    шаг("платы умеют знакомиться по паролю (команда 0xAD)", len(умеют) >= 2,
        "умеют %d из %d — остальным нужна прошивка посвежее" % (len(умеют), len(платы)))
    if len(умеют) < 2:
        return итог()

    а, б = умеют[0], умеют[1]
    print()
    print("ЗНАКОМИМ %s/%s  и  %s/%s" % (а["номер"], а["вид"], б["номер"], б["вид"]))
    print("-" * 70)

    было_а = настройка(а["с"], N_ZNAKOMYH) or 0
    было_б = настройка(б["с"], N_ZNAKOMYH) or 0
    print("     знакомых было: %s — %d, %s — %d" % (а["вид"], было_а, б["вид"], было_б))

    книга_а = настройка(а["с"], N_KNIZHKA)
    книга_б = настройка(б["с"], N_KNIZHKA)
    шаг("книжка ключей поднялась на обеих",
        книга_а in (0, 1) and книга_б in (0, 1),
        "%s: %s, %s: %s" % (а["вид"], КНИЖКА.get(книга_а, книга_а),
                            б["вид"], КНИЖКА.get(книга_б, книга_б)))

    # Пароль уходит на обе, и почти одновременно: чем ближе, тем
    # раньше сойдётся. Расхождение в доли секунды роли не играет —
    # зов повторяется каждые три секунды.
    знакомство(а["с"], пароль)
    знакомство(б["с"], пароль)
    print("     пароль задан обеим, жду...")

    вышло_а = вышло_б = False
    for сек in range(1, 46):
        time.sleep(1.0)
        оа = знакомство(а["с"])
        об = знакомство(б["с"])
        if оа is None or об is None:
            continue
        if сек % 5 == 0 or (оа[0] >= 4 and об[0] >= 4):
            print("     %2d c: %s — %s (знакомых %d), %s — %s (знакомых %d)"
                  % (сек, а["вид"], ШАГИ.get(оа[0], оа[0]), оа[1],
                     б["вид"], ШАГИ.get(об[0], об[0]), об[1]))
        if оа[0] in (4, 5) and об[0] in (4, 5):
            вышло_а = (оа[0] == 4)
            вышло_б = (об[0] == 4)
            break

    шаг("обе рации говорят «вышло»", вышло_а and вышло_б)

    стало_а = настройка(а["с"], N_ZNAKOMYH) or 0
    стало_б = настройка(б["с"], N_ZNAKOMYH) or 0
    шаг("у каждой прибавился знакомый",
        стало_а == было_а + 1 and стало_б == было_б + 1,
        "стало: %s — %d, %s — %d" % (а["вид"], стало_а, б["вид"], стало_б))

    # Счётчик посылок обязан быть далеко впереди нуля: бронь.
    сч_а = настройка(а["с"], N_SCHET)
    шаг("счётчик закрытых посылок забронирован во флеше",
        сч_а is not None and сч_а > 0,
        "следующая посылка №%s" % сч_а)

    print()
    print("ПЕРЕЖИВЁТ ЛИ ЗНАКОМСТВО ВЫКЛЮЧЕНИЕ")
    print("-" * 70)
    print("     Это проверяется только руками: выньте и воткните обе платы,")
    print("     потом запустите ЗНАКОМСТВО_ПОСМОТРЕТЬ.bat — там должно быть")
    print("     столько же знакомых, сколько сейчас (%d и %d)." % (стало_а, стало_б))

    for п in платы:
        try: п["с"].close()
        except Exception: pass
    return итог()


def итог():
    print()
    print("=" * 70)
    вышло = sum(1 for _, в in ит if в)
    print("ИТОГ: %d из %d" % (вышло, len(ит)))
    for имя, в in ит:
        if not в: print("  не вышло: %s" % имя)
    print("=" * 70)
    return 0 if вышло == len(ит) else 1


if __name__ == "__main__":
    sys.exit(главное())
