# 二进制协议模拟设备：在电脑上扮演单片机，配合 App Inventor 二进制组件案例使用。
# 用法：python binary_device.py        （默认监听 8888 端口，Python 3，无需安装任何库）
#
# 帧格式：A5 5A + 数据长度 N（1 字节）+ N 个字节的数据 + 累加校验（前面所有字节相加取低 8 位）
# 命令（数据的第 1 个字节）：
#   01          读温湿度 → 回复 81 + 温度（2 字节，乘 10，有符号，高字节在前）+ 湿度（1 字节）
#   02 开关     开灯 01 / 关灯 00 → 回复 82 + 当前状态
# 连上后每 2 秒推送一次状态帧 90 + 序号，并轮流模拟三种网络情况：
#   一帧拆成两半分两次发、两帧粘在一起一次发、夹一个校验错误的坏帧
import argparse
import random
import socket
import threading
import time

PORT = 8888
FIXED = False


def frame(data):
    body = bytes([0xA5, 0x5A, len(data)]) + bytes(data)
    return body + bytes([sum(body) & 0xFF])


def hexs(b):
    return ' '.join('%02X' % x for x in b)


def reply_for(data, state):
    if not data:
        return None
    if data[0] == 0x01:
        temp = 235 if FIXED else int(round(random.uniform(-5, 35) * 10))
        hum = 45 if FIXED else random.randint(20, 90)
        t = temp & 0xFFFF
        return frame([0x81, t >> 8, t & 0xFF, hum])
    if data[0] == 0x02 and len(data) >= 2:
        state['led'] = data[1]
        return frame([0x82, state['led']])
    return None


def pusher(conn, lock, alive):
    seq = 0
    rounds = 0
    while alive[0]:
        time.sleep(2)
        seq += 1
        rounds += 1
        mode = rounds % 3
        try:
            with lock:
                if mode == 1:
                    f = frame([0x90, seq & 0xFF])
                    conn.sendall(f[:3])
                    time.sleep(0.05)
                    conn.sendall(f[3:])
                    print('推送（拆成两半）', hexs(f))
                elif mode == 2:
                    f1 = frame([0x90, seq & 0xFF])
                    seq += 1
                    f2 = frame([0x90, seq & 0xFF])
                    conn.sendall(f1 + f2)
                    print('推送（两帧粘在一起）', hexs(f1 + f2))
                else:
                    bad = bytearray(frame([0x90, seq & 0xFF]))
                    bad[-1] ^= 0xFF
                    conn.sendall(bytes(bad))
                    print('推送（校验错误的坏帧）', hexs(bad))
        except OSError:
            return


def serve(conn, addr):
    print('App 已连接', addr)
    lock = threading.Lock()
    alive = [True]
    state = {'led': 0}
    threading.Thread(target=pusher, args=(conn, lock, alive), daemon=True).start()
    buf = b''
    try:
        while True:
            chunk = conn.recv(1024)
            if not chunk:
                break
            buf += chunk
            while True:
                i = buf.find(b'\xA5\x5A')
                if i < 0:
                    buf = buf[-1:]
                    break
                buf = buf[i:]
                if len(buf) < 4 or len(buf) < buf[2] + 4:
                    break
                f, buf = buf[:buf[2] + 4], buf[buf[2] + 4:]
                ok = (sum(f[:-1]) & 0xFF) == f[-1]
                print('收到', hexs(f), '' if ok else '（校验不对，丢弃）')
                if ok:
                    r = reply_for(f[3:-1], state)
                    if r:
                        with lock:
                            conn.sendall(r)
                        print('回复', hexs(r))
    finally:
        alive[0] = False
        conn.close()
        print('App 已断开')


def main():
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(('0.0.0.0', PORT))
    srv.listen(1)
    print('模拟设备已启动，端口', PORT, '，本机 IP：', socket.gethostbyname(socket.gethostname()))
    while True:
        conn, addr = srv.accept()
        serve(conn, addr)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='二进制 TCP 模拟设备')
    parser.add_argument('--fixed', action='store_true', help='固定返回温度 23.5 ℃、湿度 45 %')
    FIXED = parser.parse_args().fixed
    main()
