#!/usr/bin/env python3
"""
Silent Voice BLE 客户端
========================
连接硬件设备，将数据发送到服务器

使用方法:
1. 安装依赖: pip install bleak requests
2. 运行: python ble_client.py --server https://tran.l9.pw
   或本地: python ble_client.py --server http://localhost:5000
"""

import asyncio
import argparse
import struct
import time
import sys

try:
    from bleak import BleakClient, BleakScanner
    import requests
except ImportError:
    print("请先安装依赖: pip install bleak requests")
    sys.exit(1)

# ===== 配置 =====
DEVICE_NAME = "SilentVoice"
IMU_CHAR_UUID = "19b10001-e8f2-537e-4f6c-d104768a1214"
CTRL_CHAR_UUID = "19b10002-e8f2-537e-4f6c-d104768a1214"

# 包格式: 19 bytes
FRAME_SIZE = 19
FRAME_FORMAT = '<H3h3hIB'  # uint16 + 3*int16 + 3*int16 + uint32 + uint8

# 全局状态
server_url = "http://localhost:5000"
last_frame_id = None
total_received = 0
total_dropped = 0
sample_buffer = []
last_send_time = time.time()
SEND_INTERVAL = 0.1  # 每100ms发送一批


def calc_checksum(data: bytes) -> int:
    return sum(data) & 0xFF


def parse_frame(data: bytes):
    """解析单帧数据"""
    global last_frame_id, total_received, total_dropped
    
    if len(data) != FRAME_SIZE:
        print(f"[警告] 数据长度错误: {len(data)} != {FRAME_SIZE}")
        return None
    
    # 校验
    expected_checksum = calc_checksum(data[:-1])
    actual_checksum = data[-1]
    if expected_checksum != actual_checksum:
        print(f"[警告] 校验失败")
        return None
    
    # 解析
    frame_id, ax, ay, az, gx, gy, gz, timestamp, _ = struct.unpack(FRAME_FORMAT, data)
    
    # 检查丢帧
    if last_frame_id is not None:
        expected = (last_frame_id + 1) & 0xFFFF
        if frame_id != expected:
            gap = (frame_id - last_frame_id) & 0xFFFF
            if gap > 1:
                total_dropped += gap - 1
    
    last_frame_id = frame_id
    total_received += 1
    
    return {
        'ax': ax / 1000.0,
        'ay': ay / 1000.0,
        'az': az / 1000.0,
        'gx': gx / 100.0,
        'gy': gy / 100.0,
        'gz': gz / 100.0,
        'timestamp': timestamp
    }


def send_to_server(samples):
    """发送数据到服务器"""
    global server_url
    try:
        response = requests.post(
            f"{server_url}/api/sensor/data",
            json={'samples': samples},
            timeout=1
        )
        return response.status_code == 200
    except Exception as e:
        print(f"[错误] 发送失败: {e}")
        return False


def notification_handler(sender, data):
    """BLE通知回调"""
    global sample_buffer, last_send_time
    
    sample = parse_frame(bytes(data))
    if sample:
        sample_buffer.append(sample)
        
        # 批量发送
        now = time.time()
        if now - last_send_time >= SEND_INTERVAL and sample_buffer:
            send_to_server(sample_buffer)
            sample_buffer = []
            last_send_time = now


async def find_device():
    """扫描设备"""
    print(f"🔍 扫描 {DEVICE_NAME} ...")
    
    devices = await BleakScanner.discover(timeout=10.0)
    for d in devices:
        if d.name and DEVICE_NAME in d.name:
            print(f"✅ 找到设备: {d.name} ({d.address})")
            return d.address
    
    return None


async def connect_and_run(address: str):
    """连接并运行"""
    global total_received, total_dropped, sample_buffer
    
    print(f"📡 连接 {address} ...")
    
    async with BleakClient(address) as client:
        if not client.is_connected:
            print("❌ 连接失败")
            return
        
        print("✅ 已连接!")
        print(f"📤 发送到: {server_url}")
        print("-" * 40)
        
        # 启动通知
        await client.start_notify(IMU_CHAR_UUID, notification_handler)
        
        # 发送开始采集命令
        await client.write_gatt_char(CTRL_CHAR_UUID, b'\x01')
        print("▶️ 开始采集")
        
        start_time = time.time()
        
        try:
            while True:
                await asyncio.sleep(2)
                
                elapsed = time.time() - start_time
                rate = total_received / elapsed if elapsed > 0 else 0
                
                print(f"📊 接收: {total_received} | 丢失: {total_dropped} | 速率: {rate:.1f} Hz")
                
        except KeyboardInterrupt:
            print("\n⏹️ 停止中...")
        
        # 停止采集
        try:
            await client.write_gatt_char(CTRL_CHAR_UUID, b'\x00')
        except:
            pass
        
        # 发送剩余数据
        if sample_buffer:
            send_to_server(sample_buffer)
        
        print("👋 已断开")


async def main():
    global server_url
    
    parser = argparse.ArgumentParser(description='Silent Voice BLE 客户端')
    parser.add_argument('--server', default='http://localhost:5000', 
                        help='服务器地址 (默认: http://localhost:5000)')
    args = parser.parse_args()
    
    server_url = args.server.rstrip('/')
    
    print("=" * 40)
    print("  Silent Voice BLE 客户端")
    print("=" * 40)
    print(f"服务器: {server_url}")
    print()
    
    # 测试服务器连接
    try:
        r = requests.get(f"{server_url}/api/status", timeout=5)
        if r.status_code == 200:
            print("✅ 服务器连接正常")
        else:
            print("⚠️ 服务器响应异常")
    except:
        print("⚠️ 无法连接服务器，继续尝试...")
    
    print()
    
    while True:
        address = await find_device()
        
        if address:
            try:
                await connect_and_run(address)
            except Exception as e:
                print(f"❌ 错误: {e}")
        else:
            print("❌ 未找到设备，5秒后重试...")
        
        await asyncio.sleep(5)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n👋 再见!")
