From 68106d8aed48b560b2a4f1bf57bced56eb8a274e Mon Sep 17 00:00:00 2001 From: lizhao Date: Wed, 10 Jun 2026 10:57:30 +0800 Subject: [PATCH] nuitka package test --- .gitignore | 2 + nuitka_3in1_package.sh | 55 ++++++ requirements.txt | 52 +++++ system_test.py | 422 ----------------------------------------- 4 files changed, 109 insertions(+), 422 deletions(-) create mode 100644 nuitka_3in1_package.sh create mode 100644 requirements.txt delete mode 100644 system_test.py diff --git a/.gitignore b/.gitignore index 021d662..914eda2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ __pycache__/ # Distribution / packaging build/ dist/ +dist_nuitka/ upperHost_stim/ +.vscode/ #!upperHost_stim/MI_headless.py #!upperHost_stim/ssmvep_headless.py .env diff --git a/nuitka_3in1_package.sh b/nuitka_3in1_package.sh new file mode 100644 index 0000000..35d9577 --- /dev/null +++ b/nuitka_3in1_package.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Git Bash 中文 UTF-8 兼容配置(通用版,无报错) +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 + +echo "========================" +echo "Nuitka 打包脚本 - 优化稳定版" +echo "适配:PyTorch2.0.0 + CUDA11.7 + 脑电解码项目" +echo "========================" + +# ===================== 自定义配置区 ===================== +PY_FILE="runDecoder.py" # 主程序文件 +OUT_DIR="dist_nuitka" # 输出文件夹 +MODEL_DIR="online_Models" # 模型文件夹 +# ======================================================== + +# 检查主文件是否存在 +if [ ! -f "${PY_FILE}" ]; then + echo "错误:未找到主文件 ${PY_FILE},请检查路径!" + read -n 1 -s -r -p "按任意键退出" + exit 1 +fi + +echo "开始打包:${PY_FILE}" +echo "输出目录:${OUT_DIR}" + +# Nuitka 核心打包命令(无错误、无冗余、全依赖) +python -m nuitka \ +--standalone \ +--msvc=latest \ +--windows-console-mode=force \ +--module-parameter=torch-disable-jit=yes \ +--enable-plugin=no-qt \ +--include-package=numpy \ +--include-module=numpy.core._multiarray_umath \ +--include-package=scipy \ +--no-deployment-flag=self-execution \ +--include-data-dir="${MODEL_DIR}=${MODEL_DIR}" \ +--output-dir="${OUT_DIR}" \ +--remove-output \ +"${PY_FILE}" + +# 打包结果判断 +if [ $? -eq 0 ]; then + echo -e "\n========================" + echo "✅ 打包成功!" + echo "📦 产物路径:${OUT_DIR}/${PY_FILE%.py}.exe" + echo "========================" +else + echo -e "\n❌ 打包失败!" +fi + +# Git Bash 兼容的暂停 +read -n 1 -s -r -p "按任意键退出..." +echo \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0f04eb4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,52 @@ +Bottleneck==1.4.2 +brotlicffi==1.2.0.0 +certifi==2026.5.20 +cffi==2.0.0 +charset-normalizer==3.4.4 +contourpy==1.3.2 +cycler==0.12.1 +einops==0.8.2 +filelock==3.20.3 +fonttools==4.63.0 +gmpy2==2.2.2 +idna==3.11 +Jinja2==3.1.6 +joblib==1.5.3 +kiwisolver==1.5.0 +MarkupSafe==3.0.2 +matplotlib==3.10.9 +mkl_fft==1.3.11 +mkl_random==1.2.8 +mkl-service==2.5.2 +mpmath==1.3.0 +networkx==3.4.2 +Nuitka==4.1.1 +numexpr==2.14.1 +numpy==1.24.3 +packaging==26.0 +pandas==2.3.3 +pillow==12.2.0 +pip==26.0.1 +pycparser==3.0 +pyparsing==3.3.2 +pyserial==3.5 +PySocks==1.7.1 +python-dateutil==2.9.0.post0 +pytz==2026.1.post1 +pyzmq==27.1.0 +requests==2.33.1 +scikit-learn==1.7.1 +scipy==1.15.3 +setuptools==82.0.1 +six==1.17.0 +sympy==1.14.0 +threadpoolctl==3.5.0 +torch==2.0.0 +torchaudio==2.0.0 +torchsummary==1.5.1 +torchvision==0.15.0 +typing_extensions==4.15.0 +tzdata==2026.2 +urllib3==2.7.0 +wheel==0.46.3 +win_inet_pton==1.1.0 diff --git a/system_test.py b/system_test.py deleted file mode 100644 index 41b96b1..0000000 --- a/system_test.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- coding: utf-8 -*- -""" -ZMQ 脑电数据测试工具【语法错误修复版】 -修复点: -1. dataclass 可变列表默认值报错 -2. threading.Thread daemon 参数语法错误 -适配:Python3.10、全链路 float64、ZMQ DEALER<->ROUTER -端口:8099(命令) / 8100(数据) -""" -import zmq -import time -import threading -import numpy as np -import matplotlib.pyplot as plt -import json -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Union, Tuple -from matplotlib.animation import FuncAnimation - -# ===================== 1. 配置管理 ===================== -@dataclass(frozen=True) # 冻结配置类 -class TestConfig: - # 网络配置 - SERVER_IP: str = "127.0.0.1" - CMD_PORT: int = 8099 - DATA_PORT: int = 8100 - - # 硬件与时序 - SAMPLE_RATE: int = 250 - FRAME_INTERVAL_MS: int = 20 - SEND_INTERVAL: float = FRAME_INTERVAL_MS / 1000 - CHANNEL_NUMS: int = 66 - FRAME_POINTS: int = 5 - FILTER_OUT_CHAN: int = 64 - FILTER_FRAME_POINTS: int = 50 - - # 数据类型 & 字节数 (float64 8字节) - DATA_DTYPE: np.dtype = np.float64 - RAW_FRAME_BYTES: int = CHANNEL_NUMS * FRAME_POINTS * 8 # 66*5*8 = 2640 - FILTER_FRAME_BYTES: int = FILTER_OUT_CHAN * FILTER_FRAME_POINTS * 8 # 25600 - - # 事件通道索引 - EVENT_CHANNEL_IDX: int = -2 - - # 列表类型 使用 default_factory 规避可变默认值报错 - EVENT_TAGS: List[int] = field(default_factory=lambda: [1, 2, 99]) - SIM_SIGNAL_FREQ: List[float] = field(default_factory=lambda: [8.0, 9.0]) - - # 仿真噪声 - NOISE_STD: float = 0.25 - - # 可视化配置 - PLOT_TARGET_CHAN: int = 0 - PLOT_WINDOW_LEN: int = 400 - PLOT_REFRESH_INTERVAL: int = 50 - - # 日志限流 - FRAME_ERR_INTERVAL: float = 3.0 - - # ZMQ 配置 - SEND_RETRY_MAX: int = 3 - SEND_RETRY_SLEEP: float = 0.01 - ZMQ_HWM: int = 1000 - -# 初始化全局配置 -CONFIG = TestConfig() - -# ===================== 2. 全局状态管理 ===================== -class GlobalState: - def __init__(self): - self.run_flag: bool = True - self.last_frame_err_time: float = 0.0 - -GLOBAL_STATE = GlobalState() - -# ===================== 3. Matplotlib 中文初始化 ===================== -def init_matplotlib(): - # Windows 黑体,Linux/Mac 自行替换字体 - plt.rcParams['font.sans-serif'] = ['SimHei'] - plt.rcParams['axes.unicode_minus'] = False # 修复负号乱码 - -init_matplotlib() - -# ===================== 4. ZMQ DEALER 客户端 ===================== -class ZmqDealerClient: - """适配 ROUTER 的 DEALER 客户端,高频流式数据专用""" - def __init__(self, server_ip: str, port: int): - self.ctx: zmq.Context = zmq.Context() - self.socket: zmq.Socket = self.ctx.socket(zmq.DEALER) - self._configure_socket() - self.socket.connect(f"tcp://{server_ip}:{port}") - - def _configure_socket(self): - """套接字参数配置""" - self.socket.setsockopt(zmq.RCVHWM, CONFIG.ZMQ_HWM) - self.socket.setsockopt(zmq.SNDHWM, CONFIG.ZMQ_HWM) - self.socket.setsockopt(zmq.RCVTIMEO, 0) - self.socket.setsockopt(zmq.SNDTIMEO, 0) - - def send_json(self, data: Dict) -> bool: - """发送JSON命令,带重试机制""" - try: - payload = json.dumps(data, ensure_ascii=False).encode("utf-8") - except Exception as e: - print(f"[JSON序列化失败] {e}") - return False - - for _ in range(CONFIG.SEND_RETRY_MAX): - try: - self.socket.send_multipart([b"", payload]) - return True - except zmq.Again: - time.sleep(CONFIG.SEND_RETRY_SLEEP) - except Exception as e: - print(f"[JSON发送异常] {e}") - time.sleep(CONFIG.SEND_RETRY_SLEEP) - print(f"[JSON发送重试失败]") - return False - - def send_bytes(self, data: bytes) -> bool: - """发送二进制脑电数据,带重试""" - for _ in range(CONFIG.SEND_RETRY_MAX): - try: - self.socket.send_multipart([b"", data]) - return True - except zmq.Again: - time.sleep(CONFIG.SEND_RETRY_SLEEP) - except Exception as e: - print(f"[二进制发送异常] {e}") - time.sleep(CONFIG.SEND_RETRY_SLEEP) - print(f"[二进制发送重试失败]") - return False - - def recv_json(self) -> Optional[Dict]: - """接收JSON命令响应(标准3帧)""" - try: - frames = self.socket.recv_multipart() - if len(frames) < 3: - self._log_frame_err(f"帧数异常: {len(frames)}") - return None - payload = frames[2].decode("utf-8") - return json.loads(payload) - except json.JSONDecodeError: - self._log_frame_err("JSON解析失败") - return None - except Exception as e: - self._log_frame_err(f"接收异常: {e}") - return None - - def recv_bytes(self) -> Optional[bytes]: - """接收滤波数据,兼容3/4帧格式""" - try: - frames = self.socket.recv_multipart() - frame_len = len(frames) - if frame_len == 3: - payload = frames[2] - elif frame_len == 4: - payload = frames[3] - else: - self._log_frame_err(f"帧数异常: {frame_len}") - return None - - if len(payload) != CONFIG.FILTER_FRAME_BYTES: - self._log_frame_err(f"字节不匹配: 期望{CONFIG.FILTER_FRAME_BYTES}, 实际{len(payload)}") - return None - return payload - except Exception as e: - self._log_frame_err(f"数据接收异常: {e}") - return None - - def _log_frame_err(self, msg: str): - """日志限流,防止刷屏""" - now = time.time() - if now - GLOBAL_STATE.last_frame_err_time > CONFIG.FRAME_ERR_INTERVAL: - print(f"[帧异常] {msg}") - GLOBAL_STATE.last_frame_err_time = now - - def close(self): - """优雅释放ZMQ资源""" - try: - self.socket.close(linger=0) - self.ctx.term() - except Exception as e: - print(f"[资源释放异常] {e}") - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - -# ===================== 5. 仿真脑电数据生成 ===================== -def generate_raw_eeg_frame(add_event: bool = False) -> np.ndarray: - """生成单帧float64仿真脑电数据""" - t = np.linspace( - 0, CONFIG.FRAME_POINTS / CONFIG.SAMPLE_RATE, - CONFIG.FRAME_POINTS, endpoint=False - ) - eeg_frame = np.zeros( - (CONFIG.CHANNEL_NUMS, CONFIG.FRAME_POINTS), - dtype=CONFIG.DATA_DTYPE - ) - - # 模拟脑电信号 + 高斯噪声 - for freq in CONFIG.SIM_SIGNAL_FREQ: - eeg_frame[:CONFIG.FILTER_OUT_CHAN] += np.sin(2 * np.pi * freq * t) - eeg_frame[:CONFIG.FILTER_OUT_CHAN] += np.random.normal( - 0, CONFIG.NOISE_STD, - size=(CONFIG.FILTER_OUT_CHAN, CONFIG.FRAME_POINTS) - ) - - # 事件通道处理 - eeg_frame[CONFIG.EVENT_CHANNEL_IDX] = 0.0 - if add_event: - event_pos = np.random.randint(0, CONFIG.FRAME_POINTS) - eeg_frame[CONFIG.EVENT_CHANNEL_IDX, event_pos] = np.random.choice(CONFIG.EVENT_TAGS) - - # 预留通道置0 - eeg_frame[-1] = 0.0 - return eeg_frame - -# ===================== 6. 后台工作线程 ===================== -def start_cmd_response_thread(cmd_client: ZmqDealerClient): - """命令响应接收线程""" - print("[线程-命令接收] 已启动") - while GLOBAL_STATE.run_flag: - msg = cmd_client.recv_json() - if msg: - print(f"\n【命令响应】{json.dumps(msg, ensure_ascii=False, indent=2)}") - time.sleep(0.01) - print("[线程-命令接收] 已退出") - -def start_raw_eeg_send_thread(data_client: ZmqDealerClient): - """原始脑电发送线程(20ms/帧)""" - print(f"[线程-原始数据发送] 20ms/帧 | 单帧{CONFIG.RAW_FRAME_BYTES}字节 | float64") - frame_count = 0 - while GLOBAL_STATE.run_flag: - insert_event = (frame_count % 20 == 0) - eeg_frame = generate_raw_eeg_frame(add_event=insert_event) - frame_bytes = eeg_frame.tobytes() - - # 字节校验 - if len(frame_bytes) != CONFIG.RAW_FRAME_BYTES: - print(f"[字节警告] 期望{CONFIG.RAW_FRAME_BYTES}, 实际{len(frame_bytes)}") - time.sleep(CONFIG.SEND_INTERVAL) - frame_count += 1 - continue - - data_client.send_bytes(frame_bytes) - frame_count += 1 - time.sleep(CONFIG.SEND_INTERVAL) - print("[线程-原始数据发送] 已退出") - -def start_filter_data_recv_thread(data_client: ZmqDealerClient, plot_queue: List[np.ndarray]): - """滤波数据接收线程""" - print(f"[线程-滤波数据接收] 单包{CONFIG.FILTER_FRAME_BYTES}字节 | float64") - while GLOBAL_STATE.run_flag: - raw_bytes = data_client.recv_bytes() - if not raw_bytes: - time.sleep(0.01) - continue - - try: - filter_arr = np.frombuffer(raw_bytes, dtype=CONFIG.DATA_DTYPE) - filter_arr = filter_arr.reshape(CONFIG.FILTER_FRAME_POINTS, CONFIG.FILTER_OUT_CHAN) - plot_queue.append(filter_arr[:, CONFIG.PLOT_TARGET_CHAN]) - except Exception as e: - print(f"[滤波数据解析异常] {e}") - continue - print("[线程-滤波数据接收] 已退出") - -# ===================== 7. 实时波形可视化 ===================== -def start_wave_visualization(plot_queue: List[np.ndarray]): - """启动实时滤波波形绘图""" - fig, ax = plt.subplots(figsize=(14, 4)) - x_axis = np.arange(0, CONFIG.PLOT_WINDOW_LEN) - wave_data = np.zeros(CONFIG.PLOT_WINDOW_LEN, dtype=CONFIG.DATA_DTYPE) - line, = ax.plot(x_axis, wave_data, color="#2E86AB", linewidth=1.2) - - ax.set_title( - f"实时滤波脑电波形 | 通道 {CONFIG.PLOT_TARGET_CHAN} | {CONFIG.SAMPLE_RATE}Hz | float64", - fontsize=12 - ) - ax.set_ylim(-3.0, 3.0) - ax.grid(True, alpha=0.3, linestyle="--") - plt.tight_layout() - - def update_plot(_): - nonlocal wave_data - if plot_queue: - new_wave = plot_queue.pop(0) - wave_data = np.roll(wave_data, -len(new_wave)) - wave_data[-len(new_wave)] = new_wave - line.set_ydata(wave_data) - return (line,) - - ani = FuncAnimation( - fig, update_plot, - interval=CONFIG.PLOT_REFRESH_INTERVAL, - blit=True, - cache_frame_data=False - ) - plt.show() - -# ===================== 8. 全量业务测试用例 ===================== -def run_full_test_cases(cmd_client: ZmqDealerClient): - """全覆盖 zmqServer 所有命令:sync/targetFreqs/decoderClass/impedance/train/predict/rest""" - print("\n" + "="*60) - print("开始执行全量命令测试用例") - print("="*60) - time.sleep(2) - - # 1. 同步命令 - print("\n[用例 1] 发送 sync 命令") - cmd_client.send_json({"method": "sync", "params": {}}) - time.sleep(1) - - # 2. 设置目标频率 - print("\n[用例 2] 发送 targetFreqs = [8.0, 9.0]") - cmd_client.send_json({"method": "targetFreqs", "params": [8.0, 9.0]}) - time.sleep(1) - - # 3. 切换解码器 - print("\n[用例 3] 切换解码器为 ssmvep") - cmd_client.send_json({"method": "decoderClass", "params": "ssmvep"}) - time.sleep(2) - print("\n[用例 3-2] 切换解码器为 mi") - cmd_client.send_json({"method": "decoderClass", "params": "mi"}) - time.sleep(2) - - # 4. 阻抗检测开关 - print("\n[用例 4] 开启阻抗检测 impedance=1") - cmd_client.send_json({"method": "impedance", "params": 1}) - time.sleep(1) - print("\n[用例 4-2] 关闭阻抗检测 impedance=2") - cmd_client.send_json({"method": "impedance", "params": 2}) - time.sleep(1) - - # 5. 训练模式 - print("\n[用例 5] 启动训练 train,标签=1") - cmd_client.send_json({"method": "train", "params": 1}) - time.sleep(3) - - # # 6. 休息模式 - # print("\n[用例 6] 切换 rest 休息模式") - # cmd_client.send_json({"method": "rest", "params": {}}) - # time.sleep(1) - - # 7. 启动解码 - print("\n[用例 7] 启动解码 predict=1") - cmd_client.send_json({"method": "predict", "params": 1}) - time.sleep(4) - - # # 8. 非法命令(异常测试) - # print("\n[用例 8] 发送非法命令 test_cmd_illegal") - # cmd_client.send_json({"method": "test_cmd_illegal", "params": {}}) - # time.sleep(1) - - # # 9. 停止解码 - # print("\n[用例 9] 停止解码 predict=2") - # cmd_client.send_json({"method": "predict", "params": 2}) - # time.sleep(2) - - print("\n" + "="*60) - print("所有测试用例执行完毕") - print("="*60) - -# ===================== 主程序入口(修复线程语法) ===================== -if __name__ == "__main__": - print("="*60) - print("ZMQ 脑电仿真测试工具 启动") - print(f"命令端口: {CONFIG.CMD_PORT} | 数据端口: {CONFIG.DATA_PORT}") - print(f"原始帧{CONFIG.RAW_FRAME_BYTES}字节 | 滤波帧{CONFIG.FILTER_FRAME_BYTES}字节 | float64") - print("="*60) - - try: - with ZmqDealerClient(CONFIG.SERVER_IP, CONFIG.CMD_PORT) as cmd_client, \ - ZmqDealerClient(CONFIG.SERVER_IP, CONFIG.DATA_PORT) as data_client: - - plot_queue = [] - - # ========== 重点修复:线程语法,daemon 移出 args ========== - # 命令接收线程 - t_cmd = threading.Thread( - target=start_cmd_response_thread, - args=(cmd_client,), # 单元素元组保留逗号 - daemon=True - ) - # 原始数据发送线程 - t_eeg = threading.Thread( - target=start_raw_eeg_send_thread, - args=(data_client,), - daemon=True - ) - # 滤波数据接收线程 - t_filter = threading.Thread( - target=start_filter_data_recv_thread, - args=(data_client, plot_queue), - daemon=True - ) - - # 启动线程 - t_cmd.start() - t_eeg.start() - t_filter.start() - - # 执行测试用例 - run_full_test_cases(cmd_client) - - # 启动可视化(阻塞主线程) - print("\n[提示] 波形窗口已启动,关闭窗口 / Ctrl+C 退出程序") - start_wave_visualization(plot_queue) - - except KeyboardInterrupt: - print("\n\n[用户中断] 接收到 Ctrl+C,准备退出...") - except Exception as e: - print(f"\n[程序异常] {e}") - finally: - # 停止所有后台线程 - GLOBAL_STATE.run_flag = False - time.sleep(0.2) - print("程序已安全退出") \ No newline at end of file