#!/usr/bin/env python3 """ TEM 下位机模拟器 — 用于 App 调试 用法: python tools/mock_device.py python tools/mock_device.py --host 0.0.0.0 --port 4321 python tools/mock_device.py --channels 3 --freq 2 # 3通道, 2Hz 在 App 连接界面将 IP 改为本机局域网 IP(或 10.0.2.2 若在 Android 模拟器中)。 """ import socket import struct import threading import time import math import random import argparse import sys from datetime import datetime # ── 协议常量 ────────────────────────────────────────────────────────────────── MAGIC = b'\x68\x68\xff\xff' FRAME_FLAG = 0xFE class FC: SETUP_REQ = 0x01 CONTINUOUS_REQ = 0x02 SINGLE_REQ = 0x03 STOP_REQ = 0x04 ACTIVE_REQ = 0x05 SPLITFRAME_REQ = 0x08 SETUP_ACK = 0x81 CONTINUOUS_ACK = 0x82 SINGLE_ACK = 0x83 STOP_ACK = 0x84 DATA_ACK = 0x85 _FC_NAME = { 0x01: 'SETUP_REQ', 0x02: 'CONTINUOUS_REQ', 0x03: 'SINGLE_REQ', 0x04: 'STOP_REQ', 0x05: 'ACTIVE_REQ', 0x08: 'SPLITFRAME_REQ', 0x81: 'SETUP_ACK', 0x82: 'CONTINUOUS_ACK', 0x83: 'SINGLE_ACK', 0x84: 'STOP_ACK', 0x85: 'DATA_ACK', } # sendFreq code → Hz SEND_FREQ_HZ = { 0x00: 0.5, 0x01: 1.0, 0x02: 2.0, 0x03: 4.0, 0x04: 8.0, 0x05: 12.5, 0x06: 16.0, 0x07: 25.0, 0x08: 32.0, 0x09: 50.0, 0x0a: 64.0, } AMP_GAIN = [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 1.0] # ── 帧构建 ────────────────────────────────────────────────────────────────── def build_frame(func: int, payload: bytes) -> bytes: header = MAGIC + struct.pack(' bytes: payload = bytearray(54) payload[0] = 0x01 if ok else 0x02 if reason: encoded = reason.encode('ascii', errors='replace')[:31] payload[22:22 + len(encoded)] = encoded return build_frame(func_ack, bytes(payload)) def build_data_frame(cfg: dict, phase: float) -> bytes: """ 构建 DATA_ACK 帧,包含 54 字节元数据 + channelNum*sampleDepth 个 int32 ADC 采样。 phase: 当前帧的相位偏移(秒),用于生成连续正弦波形。 """ ch_num = cfg['channelNum'] depth = cfg['sampleDepth'] amp_idx = cfg['ampRatio'] acc_num = max(cfg['accNum'], 1) gain = AMP_GAIN[amp_idx] if amp_idx < len(AMP_GAIN) else 1.0 # 54 字节元数据(与 parser.ts parseMetadata 对齐) meta = bytearray(54) meta[0] = 1 # devId struct.pack_into('= 10: # 搜索帧头 idx = -1 for i in range(len(self.buf) - 3): if self.buf[i:i+4] == b'\x68\x68\xff\xff': idx = i break if idx == -1: self.buf = bytearray(self.buf[-3:]) if len(self.buf) >= 3 else bytearray() break if idx > 0: self.buf = self.buf[idx:] if len(self.buf) < 10: break payload_len = struct.unpack_from(' 400 * 1024: # 异常长度,跳过此帧头 self.buf = self.buf[4:] continue total = 10 + payload_len if len(self.buf) < total: break func = self.buf[5] payload = bytes(self.buf[10:total]) frames.append((func, payload)) self.buf = self.buf[total:] return frames # ── 客户端会话 ────────────────────────────────────────────────────────────── class Session: def __init__(self, sock: socket.socket, addr, cli_cfg: dict): self.sock = sock self.addr = addr self.parser = FrameParser() self.phase = 0.0 # 当前正弦相位(秒) self.running = False # 连续采集中? self._lock = threading.Lock() # 当前设备配置(可被 SETUP_REQ 覆盖) self.cfg = { 'channelNum': cli_cfg.get('channels', 6), 'sendFreq': cli_cfg.get('freq_code', 0x01), 'sampleFreq': 0x03, 'sampleDepth': cli_cfg.get('depth', 256), 'accNum': 1, 'ampRatio': 3, # 1× 'sourceMode': 0x00, } def _ts(self): return datetime.now().strftime('%H:%M:%S.%f')[:-3] def log(self, msg: str, direction: str = ' '): tag = f'{direction} [{self.addr[0]}:{self.addr[1]}]' print(f'[{self._ts()}] {tag} {msg}') def send(self, data: bytes) -> bool: try: self.sock.sendall(data) return True except OSError: return False # ── 连续发送线程 ────────────────────────────────────────────────────── def _data_loop(self): freq_hz = SEND_FREQ_HZ.get(self.cfg['sendFreq'], 1.0) interval = 1.0 / freq_hz self.log(f'开始连续推送 @ {freq_hz} Hz(间隔 {interval*1000:.0f} ms)', '⇄') while self.running: t0 = time.monotonic() frame = build_data_frame(self.cfg, self.phase) if not self.send(frame): break payload_size = len(frame) - 10 self.log(f'DATA_ACK ch={self.cfg["channelNum"]} ' f'depth={self.cfg["sampleDepth"]} ' f'payload={payload_size}B', '→') self.phase += interval elapsed = time.monotonic() - t0 sleep_t = interval - elapsed if sleep_t > 0: time.sleep(sleep_t) self.log('连续推送已停止', '⇄') def start_continuous(self): if self.running: return self.running = True t = threading.Thread(target=self._data_loop, daemon=True) t.start() def stop_continuous(self): self.running = False # ── 帧处理 ──────────────────────────────────────────────────────────── def _parse_setup(self, payload: bytes): if len(payload) < 14: return self.cfg['channelNum'] = max(1, min(6, payload[0])) self.cfg['sendFreq'] = payload[1] self.cfg['sampleFreq'] = payload[2] depth = struct.unpack_from(' 0 else 256 acc_flags = struct.unpack_from('> 2) & 0x3FFF self.cfg['accNum'] = acc_num if acc_num > 0 else 1 self.cfg['ampRatio'] = payload[7] self.cfg['sourceMode'] = payload[13] if len(payload) > 13 else 0 prefix = '' if len(payload) >= 54: raw = payload[38:54] prefix = raw.split(b'\x00', 1)[0].decode('ascii', errors='replace') freq_hz = SEND_FREQ_HZ.get(self.cfg['sendFreq'], 1.0) self.log( f'SETUP ch={self.cfg["channelNum"]} ' f'freq={freq_hz}Hz depth={self.cfg["sampleDepth"]} ' f'acc={self.cfg["accNum"]} amp=0x{self.cfg["ampRatio"]:02X} ' f'prefix="{prefix}"', '←' ) def handle(self, func: int, payload: bytes): name = _FC_NAME.get(func, f'0x{func:02X}') if func == FC.SETUP_REQ: self._parse_setup(payload) self.send(build_ack(FC.SETUP_ACK)) self.log('SETUP_ACK OK', '→') elif func == FC.CONTINUOUS_REQ: self.log('CONTINUOUS_REQ', '←') self.send(build_ack(FC.CONTINUOUS_ACK)) self.log('CONTINUOUS_ACK OK', '→') self.start_continuous() elif func == FC.SINGLE_REQ: self.log('SINGLE_REQ', '←') self.send(build_ack(FC.SINGLE_ACK)) self.log('SINGLE_ACK OK', '→') frame = build_data_frame(self.cfg, self.phase) self.send(frame) self.log(f'DATA_ACK (单次) {len(frame)-10}B', '→') self.phase += 1.0 elif func == FC.STOP_REQ: self.log('STOP_REQ', '←') self.stop_continuous() self.send(build_ack(FC.STOP_ACK)) self.log('STOP_ACK OK', '→') elif func == FC.ACTIVE_REQ: pass # keepalive,无需回应 elif func == FC.SPLITFRAME_REQ: self.log(f'SPLITFRAME_REQ(未实现,忽略)', '←') else: self.log(f'未知帧 0x{func:02X} ({len(payload)}B payload)', '←') # ── 主循环 ──────────────────────────────────────────────────────────── def run(self): self.log('已连接', '✓') try: while True: data = self.sock.recv(4096) if not data: break for func, payload in self.parser.feed(data): self.handle(func, payload) except OSError: pass finally: self.stop_continuous() self.sock.close() self.log('已断开', '✗') # ── 服务器主入口 ──────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description='TEM 下位机模拟器', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 连接方式: Android 模拟器 → App IP 填 10.0.2.2 真机(同 WiFi) → App IP 填本机局域网 IP(运行 ipconfig 查看) Expo Web/桌面 → App IP 填 127.0.0.1 """, ) parser.add_argument('--host', default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)') parser.add_argument('--port', type=int, default=4321, help='监听端口 (默认: 4321)') parser.add_argument('--channels', type=int, default=6, help='默认通道数 1-6 (默认: 6)') parser.add_argument('--freq', type=float, default=1.0, help='默认发送频率 Hz (默认: 1.0)') parser.add_argument('--depth', type=int, default=256, help='默认采样深度 (默认: 256)') args = parser.parse_args() # 找到最接近的 freq code freq_code = min(SEND_FREQ_HZ, key=lambda k: abs(SEND_FREQ_HZ[k] - args.freq)) actual_freq = SEND_FREQ_HZ[freq_code] cli_cfg = { 'channels': min(6, max(1, args.channels)), 'freq_code': freq_code, 'depth': max(16, args.depth), } srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((args.host, args.port)) srv.listen(5) print(f'\n TEM 下位机模拟器') print(f' ─────────────────────────────────────────') print(f' 监听地址 : {args.host}:{args.port}') print(f' 默认通道数: {cli_cfg["channels"]}') print(f' 默认频率 : {actual_freq} Hz') print(f' 默认采样深: {cli_cfg["depth"]} pts') print(f' ─────────────────────────────────────────') print(f' 在 App 连接界面将 IP 改为本机局域网 IP') print(f' Android 模拟器请填 10.0.2.2') print(f' Ctrl+C 停止\n') try: while True: conn, addr = srv.accept() conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) session = Session(conn, addr, cli_cfg) t = threading.Thread(target=session.run, daemon=True) t.start() except KeyboardInterrupt: print('\n模拟器已停止') finally: srv.close() if __name__ == '__main__': main()