# -*- coding: utf-8 -*-
# 本地串口桥：pyserial 独占串口，网页通过 WebSocket 收发。
# 背景：Web Serial 的写入经过浏览器进程中转，偶发把一条 AT 命令在中途"顿"开，
# 设备的行空闲定时器会把两截各报一个 ERROR,1（命令未执行）。pyserial 一次 write()
# 把整条命令交给内核，字节流物理连续——桥接后网页与桌面工具共用同一条写入路径。
#
# 用法: python scripts/serial_ws_bridge.py [串口号] [波特率] [WS端口]
#   默认: COM16 115200 8765，网页侧填 ws://127.0.0.1:8765
#
# 协议（JSON 文本帧）:
#   浏览器 → 桥: {"t":"w","d":"AT+NOTA=192,...\r\n"}   原样写入串口（含 CRLF）
#   桥 → 浏览器: {"t":"o","port":"COM16","baud":115200}  串口已打开（握手完成）
#                {"t":"l","d":"+NOTA:7,..."}            串口收到的一行
#                {"t":"e","d":"..."}                    错误（随后关闭连接）

import asyncio
import json
import sys
import threading
import time

import serial
import websockets

WS_HOST = '127.0.0.1'


def now():
    return time.strftime('%H:%M:%S')


class Bridge:
    def __init__(self, port, baud):
        self.port_name = port
        self.baud = baud
        self.ser = None
        self.loop = None
        self.lines = None

    def _serial_reader(self):
        # 后台线程：阻塞读串口，按行拆分后投递到 asyncio 队列
        buf = b''
        try:
            while self.ser and self.ser.is_open:
                n = self.ser.in_waiting
                chunk = self.ser.read(n if n > 0 else 1)
                if not chunk:
                    continue
                buf += chunk
                while b'\n' in buf:
                    line, buf = buf.split(b'\n', 1)
                    text = line.decode(errors='ignore').rstrip('\r').strip()
                    if text:
                        asyncio.run_coroutine_threadsafe(self.lines.put(text), self.loop)
        except Exception as e:
            print(f'[{now()}] 串口读取结束: {e}')
            asyncio.run_coroutine_threadsafe(self.lines.put(None), self.loop)

    async def _pump_lines(self, ws):
        while True:
            line = await self.lines.get()
            if line is None:
                await ws.close(code=1011, reason='serial read error')
                return
            await ws.send(json.dumps({'t': 'l', 'd': line}))

    async def handler(self, ws):
        print(f'[{now()}] 浏览器已连接，打开串口 {self.port_name}@{self.baud}')
        try:
            self.ser = serial.Serial(self.port_name, self.baud, timeout=0.05)
        except Exception as e:
            await ws.send(json.dumps({'t': 'e', 'd': f'打开串口失败: {e}'}))
            await ws.close()
            return
        try:
            self.ser.setDTR(True)
            self.ser.setRTS(True)
        except Exception:
            pass
        self.lines = asyncio.Queue()
        self.loop = asyncio.get_running_loop()
        threading.Thread(target=self._serial_reader, daemon=True).start()
        await ws.send(json.dumps({'t': 'o', 'port': self.port_name, 'baud': self.baud}))
        print(f'[{now()}] 串口已打开，桥接就绪')
        pump = asyncio.create_task(self._pump_lines(ws))
        try:
            async for raw in ws:
                try:
                    msg = json.loads(raw)
                except Exception:
                    continue
                if msg.get('t') == 'w':
                    data = str(msg.get('d', ''))
                    await asyncio.to_thread(self.ser.write, data.encode('utf-8', errors='ignore'))
        except websockets.ConnectionClosed:
            pass
        finally:
            pump.cancel()
            if self.ser and self.ser.is_open:
                self.ser.close()
            print(f'[{now()}] 连接断开，串口已关闭')


async def main():
    port = sys.argv[1] if len(sys.argv) > 1 else 'COM16'
    baud = int(sys.argv[2]) if len(sys.argv) > 2 else 115200
    ws_port = int(sys.argv[3]) if len(sys.argv) > 3 else 8765
    bridge = Bridge(port, baud)
    async with websockets.serve(bridge.handler, WS_HOST, ws_port):
        print(f'[{now()}] 串口桥已启动: ws://{WS_HOST}:{ws_port} → {port}@{baud}')
        await asyncio.Future()


if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print('已退出')
