2026-06-08 11:56:42 +08:00
|
|
|
|
# -*-coding:utf-8 -*-
|
2026-06-06 14:40:07 +08:00
|
|
|
|
import ast
|
2026-06-05 09:34:29 +08:00
|
|
|
|
import numpy as np
|
|
|
|
|
|
import threading
|
2026-06-10 16:04:02 +08:00
|
|
|
|
import zmq
|
2026-06-05 09:34:29 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import queue
|
2026-06-06 14:40:07 +08:00
|
|
|
|
from typing import Dict
|
2026-06-07 11:05:24 +08:00
|
|
|
|
import datetime
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
2026-06-06 15:13:23 +08:00
|
|
|
|
from Zmq.dataBuffer import ParadigmRingBuffer
|
|
|
|
|
|
from Zmq.filterProcess import FilterRingBuffer
|
2026-06-06 14:40:07 +08:00
|
|
|
|
from PubLibrary.InifileHelper import IniRead
|
2026-06-06 09:16:49 +08:00
|
|
|
|
from logs.log import algo_log
|
2026-06-05 09:34:29 +08:00
|
|
|
|
|
2026-06-10 16:04:02 +08:00
|
|
|
|
zmqServer_host = str(IniRead('system', 'zmqServer_host', '127.0.0.1'))
|
2026-06-06 14:40:07 +08:00
|
|
|
|
|
2026-06-05 09:34:29 +08:00
|
|
|
|
class zmqServer(threading.Thread):
|
2026-06-06 09:16:49 +08:00
|
|
|
|
def __init__(self, host='0.0.0.0', cmd_port=8099, data_port=8100, device_info=None):
|
2026-06-05 09:34:29 +08:00
|
|
|
|
threading.Thread.__init__(self)
|
2026-06-06 14:40:07 +08:00
|
|
|
|
self.device_info = device_info
|
|
|
|
|
|
|
2026-06-10 16:04:02 +08:00
|
|
|
|
self.host = zmqServer_host
|
2026-06-09 10:57:28 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.cmd_port = cmd_port # 命令交互端口:收JSON命令 + 返JSON结果
|
|
|
|
|
|
self.data_port = data_port # 数据交互端口:收二进制原始脑电 + 返二进制滤波结果
|
2026-06-05 09:34:29 +08:00
|
|
|
|
self.running = False
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
|
|
|
|
|
# 原有业务状态变量
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.open_Impedance = False #当前系统处于阻抗检测状态
|
|
|
|
|
|
self.StartDecode = False
|
|
|
|
|
|
self.StartTrain = False
|
|
|
|
|
|
self.state_mode = None
|
|
|
|
|
|
self.currentLabel = -1
|
|
|
|
|
|
self.IsExitApp = False
|
2026-06-05 09:34:29 +08:00
|
|
|
|
self.daemon = True
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 双环形缓冲区
|
|
|
|
|
|
self.paradigmBuffer = ParadigmRingBuffer(
|
|
|
|
|
|
self.device_info['channel_nums'],
|
|
|
|
|
|
self.device_info['sample_rate'] * 10
|
|
|
|
|
|
)
|
|
|
|
|
|
self.filterBuffer = FilterRingBuffer(
|
|
|
|
|
|
self.device_info['channel_nums'],
|
|
|
|
|
|
self.device_info['sample_rate'] * 10
|
|
|
|
|
|
)
|
|
|
|
|
|
self.paradigmBufferLock = threading.Lock()
|
|
|
|
|
|
self.filterBufferLock = threading.Lock()
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# ZMQ上下文与套接字
|
2026-06-05 09:34:29 +08:00
|
|
|
|
self.context = zmq.Context()
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
|
|
|
|
|
# 8099命令端口:ROUTER
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.cmd_socket = self.context.socket(zmq.ROUTER)
|
2026-06-06 15:13:23 +08:00
|
|
|
|
self.cmd_socket.setsockopt(zmq.SocketOption.RCVHWM, 100)
|
|
|
|
|
|
self.cmd_socket.setsockopt(zmq.SocketOption.SNDHWM, 100)
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.cmd_socket.bind(f"tcp://{self.host}:{cmd_port}")
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 8100数据端口:ROUTER
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.data_socket = self.context.socket(zmq.ROUTER)
|
2026-06-06 15:13:23 +08:00
|
|
|
|
self.data_socket.setsockopt(zmq.SocketOption.RCVHWM, 500)
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.data_socket.setsockopt(zmq.SocketOption.SNDHWM, 100) # 添加发送高水位线
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.data_socket.bind(f"tcp://{self.host}:{data_port}")
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# Poller轮询器
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.poller = zmq.Poller()
|
|
|
|
|
|
self.poller.register(self.cmd_socket, zmq.POLLIN)
|
|
|
|
|
|
self.poller.register(self.data_socket, zmq.POLLIN)
|
2026-06-06 15:13:23 +08:00
|
|
|
|
|
2026-06-06 09:16:49 +08:00
|
|
|
|
# 业务变量
|
2026-06-05 09:34:29 +08:00
|
|
|
|
self.targetFreqs = []
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.changeTarget = False
|
|
|
|
|
|
self.labels = [0x01, 0x02, 0x03]
|
|
|
|
|
|
self.decoder_switch = False
|
|
|
|
|
|
self.decoder_class = None
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 客户端管理(单客户端场景)
|
|
|
|
|
|
self.cmd_clients = set()
|
|
|
|
|
|
self.data_clients = set()
|
|
|
|
|
|
self.current_data_client = None # 唯一数据客户端身份,用于发送滤波结果
|
2026-06-06 14:40:07 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 发送队列(双端口分离)
|
|
|
|
|
|
self.cmd_send_queue = queue.Queue() # 8099端口命令结果队列
|
|
|
|
|
|
self.data_send_queue = queue.Queue() # 8100端口滤波数据队列
|
|
|
|
|
|
|
|
|
|
|
|
# 范式buffer与事件检测参数
|
2026-06-06 14:40:07 +08:00
|
|
|
|
self.predict_event = 99
|
|
|
|
|
|
self.events = [1, 2, self.predict_event]
|
|
|
|
|
|
self.latency = 50
|
|
|
|
|
|
self.train_latency = 50
|
2026-06-07 11:05:24 +08:00
|
|
|
|
self.count_events = {}
|
|
|
|
|
|
self.epoch_finished = False
|
|
|
|
|
|
self.pack_contain_event = False
|
|
|
|
|
|
self.event_inner_idx = -1
|
|
|
|
|
|
self.interval_inited = False
|
2026-06-10 15:18:22 +08:00
|
|
|
|
self.last_epoch_finish_time = None
|
2026-06-06 14:40:07 +08:00
|
|
|
|
|
2026-06-06 17:08:09 +08:00
|
|
|
|
def reset_state(self):
|
|
|
|
|
|
"""清空采集器状态和缓存数据"""
|
|
|
|
|
|
with self.paradigmBufferLock:
|
|
|
|
|
|
self.paradigmBuffer.resetAllPara()
|
|
|
|
|
|
self.count_events = {}
|
|
|
|
|
|
self.epoch_finished = False
|
|
|
|
|
|
self.pack_contain_event = False
|
|
|
|
|
|
self.event_inner_idx = -1
|
|
|
|
|
|
self.interval_inited = False
|
|
|
|
|
|
|
2026-06-06 14:40:07 +08:00
|
|
|
|
def interval_init(self, decoder_class):
|
|
|
|
|
|
if decoder_class == 'ssmvep':
|
2026-06-09 10:57:28 +08:00
|
|
|
|
interval_epoch = ast.literal_eval(IniRead('system', 'SSMVEP_IntervalEpoch')) # [0.2, 2.2]
|
|
|
|
|
|
self.interval_epoch = [int(i * self.device_info['sample_rate']) for i in interval_epoch] # [50, 550]
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.train_epoch = [
|
|
|
|
|
|
int(self.interval_epoch[0]),
|
|
|
|
|
|
int(self.interval_epoch[1] + 0.1 * self.device_info['sample_rate'])
|
2026-06-09 10:57:28 +08:00
|
|
|
|
] # [50, 575]
|
|
|
|
|
|
self.latency = (self.interval_epoch[1] + 0.1 * self.device_info['sample_rate']) // 5 #115包, 575个点
|
|
|
|
|
|
self.train_latency = (self.train_epoch[1] + 0.1 * self.device_info['sample_rate']) // 5 #120包 600个点
|
2026-06-06 14:40:07 +08:00
|
|
|
|
|
|
|
|
|
|
elif decoder_class == 'mi':
|
2026-06-10 15:18:22 +08:00
|
|
|
|
interval_epoch = ast.literal_eval(IniRead('system', 'MI_IntervalEpoch')) # [0.5, 4.5]
|
|
|
|
|
|
self.interval_epoch = [int(i * self.device_info['sample_rate']) for i in interval_epoch] #[125, 1125]
|
2026-06-06 14:40:07 +08:00
|
|
|
|
self.train_epoch = self.interval_epoch.copy()
|
2026-06-10 15:18:22 +08:00
|
|
|
|
self.latency = self.interval_epoch[1] // 5 #225
|
|
|
|
|
|
self.train_latency = self.latency #225
|
2026-06-06 14:40:07 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"时间窗初始化完成: {interval_epoch}", level="INFO")
|
|
|
|
|
|
self.count_events: Dict[str, int] = {}
|
|
|
|
|
|
self.event_inner_idx = -1
|
|
|
|
|
|
self.epoch_finished = False
|
|
|
|
|
|
self.pack_contain_event = False
|
2026-06-06 14:40:07 +08:00
|
|
|
|
self.predict_event = 99
|
|
|
|
|
|
self.events = [1, 2, self.predict_event]
|
|
|
|
|
|
self.interval_inited = True
|
2026-06-05 09:34:29 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# -------------------------- 8099端口:命令结果广播 --------------------------
|
2026-06-05 09:34:29 +08:00
|
|
|
|
def broadcast_message(self, method, params):
|
2026-06-08 11:56:42 +08:00
|
|
|
|
"""
|
|
|
|
|
|
向所有8099端口客户端广播JSON格式的命令结果
|
|
|
|
|
|
用于:解码结果、训练状态、错误提示、进度通知等
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.cmd_send_queue.put((method, params))
|
|
|
|
|
|
|
|
|
|
|
|
def _process_cmd_send_queue(self):
|
|
|
|
|
|
"""处理8099端口发送队列,在主线程执行(保证ZMQ线程安全)"""
|
|
|
|
|
|
while not self.cmd_send_queue.empty():
|
|
|
|
|
|
method, params = self.cmd_send_queue.get()
|
|
|
|
|
|
if not self.cmd_clients:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
msg = {'method': method, 'params': params}
|
|
|
|
|
|
msg_bytes = json.dumps(msg).encode('utf-8')
|
|
|
|
|
|
|
2026-06-11 11:06:59 +08:00
|
|
|
|
if msg['method'] == 'beta_psd':
|
|
|
|
|
|
algo_log(f"发送命令结果: {msg}", level="DEBUG", record_once=True)
|
|
|
|
|
|
else:
|
|
|
|
|
|
algo_log(f"发送命令结果: {msg}", level="DEBUG")
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
|
|
|
|
|
# 广播到所有命令客户端
|
|
|
|
|
|
for client_id in list(self.cmd_clients):
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.cmd_socket.send_multipart([client_id, b"", msg_bytes])
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
algo_log(f"向命令客户端{client_id}发送失败: {e}", level="ERROR")
|
|
|
|
|
|
self.cmd_clients.discard(client_id)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
algo_log(f"命令结果打包失败: {e}", level="ERROR")
|
|
|
|
|
|
|
|
|
|
|
|
# -------------------------- 8100端口:滤波结果发送 --------------------------
|
|
|
|
|
|
def send_filtered_data(self, filtered_data):
|
|
|
|
|
|
"""
|
|
|
|
|
|
向8100端口客户端发送二进制格式的滤波结果
|
|
|
|
|
|
用于:上位机实时绘图的脑电波形数据
|
|
|
|
|
|
:param filtered_data: 滤波后数据,shape=(通道数, 50),float64格式
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.current_data_client is None:
|
|
|
|
|
|
algo_log("数据客户端未连接,跳过滤波数据发送", level="WARNING")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 转置为上位机需要的[50, 通道数]格式
|
2026-06-08 15:47:25 +08:00
|
|
|
|
filtered_data = filtered_data.T.astype(np.float64)
|
2026-06-08 11:56:42 +08:00
|
|
|
|
send_buf = filtered_data.tobytes()
|
2026-06-10 07:55:34 +08:00
|
|
|
|
algo_log(f"发送滤波数据,长度: {len(send_buf)}字节, filtered_data.shape: {filtered_data.shape}", level="DEBUG", record_once=True)
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.data_send_queue.put(send_buf)
|
|
|
|
|
|
|
|
|
|
|
|
def _process_data_send_queue(self):
|
|
|
|
|
|
"""处理8100端口发送队列,在主线程执行(保证ZMQ线程安全)"""
|
|
|
|
|
|
while not self.data_send_queue.empty():
|
|
|
|
|
|
send_buf = self.data_send_queue.get()
|
|
|
|
|
|
if self.current_data_client is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 标准ROUTER发送格式:[客户端ID, 空分隔帧, 数据帧]
|
|
|
|
|
|
self.data_socket.send_multipart([
|
|
|
|
|
|
self.current_data_client,
|
|
|
|
|
|
b"",
|
|
|
|
|
|
send_buf
|
|
|
|
|
|
])
|
2026-06-10 09:28:24 +08:00
|
|
|
|
algo_log(f"发送滤波数据成功,长度: {len(send_buf)}字节", level="DEBUG", record_once=True)
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
algo_log(f"发送滤波数据失败: {e}", level="ERROR")
|
|
|
|
|
|
# 客户端断开,重置身份
|
|
|
|
|
|
self.current_data_client = None
|
|
|
|
|
|
self.data_clients.clear()
|
2026-06-05 09:34:29 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# -------------------------- 命令端口消息处理 --------------------------
|
2026-06-06 09:16:49 +08:00
|
|
|
|
def _handle_cmd_message(self, frames):
|
2026-06-08 11:56:42 +08:00
|
|
|
|
"""处理8099端口JSON命令消息"""
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if len(frames) < 3:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"无效命令帧:长度不足3帧,实际{len(frames)}", level="ERROR")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
return
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
2026-06-06 09:16:49 +08:00
|
|
|
|
ident, _, message_bytes = frames[:3]
|
|
|
|
|
|
|
|
|
|
|
|
# 注册新的命令客户端
|
|
|
|
|
|
if ident not in self.cmd_clients:
|
|
|
|
|
|
self.cmd_clients.add(ident)
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"新命令客户端连接成功: {ident}", level="INFO")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 解析JSON命令
|
2026-06-06 09:16:49 +08:00
|
|
|
|
try:
|
|
|
|
|
|
message = json.loads(message_bytes.decode('utf-8'))
|
|
|
|
|
|
except json.JSONDecodeError:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"无效JSON命令: {message_bytes.hex()}", level="ERROR")
|
|
|
|
|
|
self.broadcast_message("error", {"code": 400, "message": "无效JSON格式"})
|
2026-06-06 14:40:07 +08:00
|
|
|
|
return
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
|
|
|
|
|
algo_log(f"收到命令: {message}", level="INFO")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
method = message.get("method")
|
|
|
|
|
|
params = message.get("params")
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 命令处理逻辑
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if method == "sync":
|
|
|
|
|
|
self.state_mode = 'sync'
|
2026-06-06 14:40:07 +08:00
|
|
|
|
elif method == "targetFreqs":
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if not isinstance(params, list):
|
2026-06-06 14:40:07 +08:00
|
|
|
|
algo_log(f"targetFreqs must be a list")
|
|
|
|
|
|
return
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if params != self.targetFreqs:
|
|
|
|
|
|
self.targetFreqs = params
|
|
|
|
|
|
self.changeTarget = True
|
2026-06-06 14:40:07 +08:00
|
|
|
|
elif method == "decoderClass":
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if not isinstance(params, str):
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"decoderClass必须是字符串")
|
2026-06-06 14:40:07 +08:00
|
|
|
|
return
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if params != self.decoder_class:
|
|
|
|
|
|
self.decoder_class = params
|
|
|
|
|
|
self.decoder_switch = True
|
2026-06-08 11:56:42 +08:00
|
|
|
|
elif method == "train":
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.state_mode = 'train'
|
2026-06-09 14:23:25 +08:00
|
|
|
|
resp = {
|
|
|
|
|
|
"method": "train_response",
|
|
|
|
|
|
"params": {
|
|
|
|
|
|
"code": 200,
|
|
|
|
|
|
"message": "ok"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
resp_bytes = json.dumps(resp, ensure_ascii=False).encode("utf-8")
|
|
|
|
|
|
self.cmd_socket.send_multipart([ident, b"", resp_bytes])
|
|
|
|
|
|
algo_log(f"train 命令已即时回复客户端 {ident}", level="DEBUG")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
algo_log(f"train 命令回复失败: {e}", level="ERROR")
|
|
|
|
|
|
return
|
2026-06-08 11:56:42 +08:00
|
|
|
|
elif method == "predict":
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.state_mode = 'predict'
|
|
|
|
|
|
if params == 1: #开始解码
|
|
|
|
|
|
self.StartDecode = True
|
|
|
|
|
|
elif params == 2: #停止解码
|
|
|
|
|
|
self.IsExitApp = True
|
|
|
|
|
|
self.running = False
|
2026-06-08 11:56:42 +08:00
|
|
|
|
elif method == "rest":
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.state_mode = 'rest'
|
2026-06-06 17:08:09 +08:00
|
|
|
|
elif method == "impedance":
|
|
|
|
|
|
if params == 1:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.open_Impedance = True
|
2026-06-06 17:08:09 +08:00
|
|
|
|
elif params == 2:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.open_Impedance = False
|
2026-06-06 14:40:07 +08:00
|
|
|
|
else:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
self.broadcast_message("error", {"code": 404, "message": f"未知命令: {method}"})
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# -------------------------- 数据端口消息处理 --------------------------
|
2026-06-06 09:16:49 +08:00
|
|
|
|
def _handle_data_message(self, frames):
|
2026-06-08 11:56:42 +08:00
|
|
|
|
"""处理8100端口二进制脑电数据消息"""
|
2026-06-08 17:06:27 +08:00
|
|
|
|
algo_log(f"收到数据帧,总帧数:{len(frames)}", level="DEBUG", record_once=True)
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 然后再进行解析
|
|
|
|
|
|
if len(frames) == 4:
|
|
|
|
|
|
# 你的上位机格式
|
|
|
|
|
|
ident, sender_ident, empty_sep, data_bytes = frames[:4]
|
|
|
|
|
|
elif len(frames) == 3:
|
|
|
|
|
|
# 标准格式
|
|
|
|
|
|
ident, empty_sep, data_bytes = frames[:3]
|
2026-06-09 19:11:21 +08:00
|
|
|
|
elif len(frames) == 2:
|
|
|
|
|
|
ident, data_bytes = frames[:2]
|
2026-06-08 11:56:42 +08:00
|
|
|
|
else:
|
2026-06-06 15:53:50 +08:00
|
|
|
|
return
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 注册新的数据客户端(单客户端场景,自动覆盖旧身份)
|
|
|
|
|
|
if ident not in self.data_clients:
|
|
|
|
|
|
self.data_clients.clear() # 单客户端,只保留最新连接
|
|
|
|
|
|
self.data_clients.add(ident)
|
|
|
|
|
|
self.current_data_client = ident
|
|
|
|
|
|
algo_log(f"新数据客户端连接成功: {ident}", level="INFO")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
try:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 精确长度校验
|
2026-06-08 16:07:09 +08:00
|
|
|
|
EXPECTED_BYTES = self.device_info['frame_points'] * self.device_info['channel_nums'] * np.dtype(np.float64).itemsize
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if len(data_bytes) != EXPECTED_BYTES:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"数据长度错误:期望{EXPECTED_BYTES}字节,实际{len(data_bytes)}字节", level="ERROR")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 零拷贝解析 + 维度转换
|
2026-06-08 15:47:25 +08:00
|
|
|
|
data_np = np.frombuffer(data_bytes, dtype=np.float64)
|
2026-06-06 14:40:07 +08:00
|
|
|
|
data_np = data_np.reshape(self.device_info['frame_points'], self.device_info['channel_nums'])
|
2026-06-06 09:16:49 +08:00
|
|
|
|
data_np = data_np.T.astype(np.float64)
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 写入滤波缓冲区
|
|
|
|
|
|
with self.filterBufferLock:
|
|
|
|
|
|
self.filterBuffer.appendBuffer(data_np)
|
2026-06-07 11:05:24 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 写入范式缓冲区
|
|
|
|
|
|
with self.paradigmBufferLock:
|
|
|
|
|
|
if self.interval_inited:
|
|
|
|
|
|
self.epoch_finished = self.detect_event(data_np)
|
|
|
|
|
|
if self.pack_contain_event:
|
|
|
|
|
|
self.paradigmBuffer.resetAllPara()
|
|
|
|
|
|
self.paradigmBuffer.appendBuffer(data_np)
|
2026-06-10 15:18:22 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
if self.epoch_finished:
|
2026-06-10 15:18:22 +08:00
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
|
|
time_diff_str = ""
|
|
|
|
|
|
# 计算与上一次Epoch完成的时间差
|
|
|
|
|
|
if self.last_epoch_finish_time is not None:
|
|
|
|
|
|
# 时间差 单位:秒,保留3位小数
|
|
|
|
|
|
delta_seconds = (now - self.last_epoch_finish_time).total_seconds()
|
|
|
|
|
|
time_diff_str = f" | 与上一次间隔: {delta_seconds:.3f} s"
|
|
|
|
|
|
|
|
|
|
|
|
# 拼接日志,增加时间差信息
|
|
|
|
|
|
log_msg = f"Epoch采集完成: {now.strftime('%H:%M:%S.%f')[:-3]}{time_diff_str}"
|
|
|
|
|
|
algo_log(log_msg, level="DEBUG")
|
|
|
|
|
|
|
|
|
|
|
|
# 更新上一次Epoch完成时间为当前时间
|
|
|
|
|
|
self.last_epoch_finish_time = now
|
2026-06-08 11:56:42 +08:00
|
|
|
|
else:
|
|
|
|
|
|
self.paradigmBuffer.appendBuffer(data_np)
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"数据处理失败: {str(e)}", level="ERROR")
|
2026-06-06 15:53:50 +08:00
|
|
|
|
if IniRead('system', 'algo_log_level', 'INFO') == 'DEBUG':
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# -------------------------- 事件检测 --------------------------
|
2026-06-07 11:05:24 +08:00
|
|
|
|
def detect_event(self, samples):
|
|
|
|
|
|
self.pack_contain_event = False
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 第65通道为事件通道
|
2026-06-09 10:57:28 +08:00
|
|
|
|
event = int(samples[-2][0])
|
|
|
|
|
|
# for idx, event in enumerate(events):
|
|
|
|
|
|
if event in self.events:
|
|
|
|
|
|
new_key = "".join(
|
|
|
|
|
|
[
|
|
|
|
|
|
str(event),
|
|
|
|
|
|
datetime.datetime.now().strftime("%Y-%m-%d \
|
|
|
|
|
|
-%H-%M-%S"),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
self.currentLabel = event
|
|
|
|
|
|
if event == self.predict_event:
|
|
|
|
|
|
self.count_events[new_key] = self.latency + 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.count_events[new_key] = self.train_latency + 1
|
|
|
|
|
|
self.event_inner_idx = self.device_info['frame_points'] - 1
|
|
|
|
|
|
# algo_log(f"事件检测到: {event},索引: {idx}", level="DEBUG")
|
|
|
|
|
|
self.pack_contain_event = True
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
|
|
|
|
|
# 倒计时并清理过期事件
|
2026-06-07 11:05:24 +08:00
|
|
|
|
drop_items = []
|
|
|
|
|
|
for key, value in self.count_events.items():
|
2026-06-08 11:56:42 +08:00
|
|
|
|
value -= 1
|
2026-06-07 11:05:24 +08:00
|
|
|
|
if value == 0:
|
|
|
|
|
|
drop_items.append(key)
|
|
|
|
|
|
self.count_events[key] = value
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
2026-06-07 11:05:24 +08:00
|
|
|
|
for key in drop_items:
|
|
|
|
|
|
del self.count_events[key]
|
2026-06-08 11:56:42 +08:00
|
|
|
|
|
2026-06-07 11:05:24 +08:00
|
|
|
|
if drop_items:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# -------------------------- 主循环 --------------------------
|
2026-06-05 09:34:29 +08:00
|
|
|
|
def run(self):
|
|
|
|
|
|
self.running = True
|
2026-06-09 14:23:25 +08:00
|
|
|
|
algo_log(f"ZMQ服务器启动成功 - host: {self.host}, 命令端口: {self.cmd_port}, 数据端口: {self.data_port}", level="INFO")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-05 09:34:29 +08:00
|
|
|
|
try:
|
|
|
|
|
|
while self.running:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 1. 处理两个端口的发送队列(必须在主线程执行)
|
|
|
|
|
|
self._process_cmd_send_queue()
|
|
|
|
|
|
self._process_data_send_queue()
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 2. 轮询监听两个端口的输入事件
|
2026-06-06 14:40:07 +08:00
|
|
|
|
socks = dict(self.poller.poll(50))
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 处理8099命令端口消息
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if self.cmd_socket in socks and socks[self.cmd_socket] == zmq.POLLIN:
|
|
|
|
|
|
frames = self.cmd_socket.recv_multipart()
|
|
|
|
|
|
self._handle_cmd_message(frames)
|
|
|
|
|
|
|
2026-06-10 17:53:01 +08:00
|
|
|
|
# 处理8100数据端口消息(排空积压,消除标签延迟)
|
2026-06-06 09:16:49 +08:00
|
|
|
|
if self.data_socket in socks and socks[self.data_socket] == zmq.POLLIN:
|
2026-06-10 17:53:01 +08:00
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
frames = self.data_socket.recv_multipart(zmq.NOBLOCK)
|
|
|
|
|
|
self._handle_data_message(frames)
|
|
|
|
|
|
except zmq.Again:
|
|
|
|
|
|
break
|
2026-06-05 09:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"服务器主循环异常: {e}", level="ERROR")
|
2026-06-05 09:34:29 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
self.running = False
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 优雅关闭所有资源
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.cmd_socket.close()
|
|
|
|
|
|
self.data_socket.close()
|
2026-06-05 09:34:29 +08:00
|
|
|
|
self.context.term()
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log("ZMQ服务器已关闭", level="INFO")
|
2026-06-06 09:16:49 +08:00
|
|
|
|
|
2026-06-05 09:34:29 +08:00
|
|
|
|
def stop(self):
|
|
|
|
|
|
"""显式关闭服务器"""
|
|
|
|
|
|
self.running = False
|
2026-06-06 09:16:49 +08:00
|
|
|
|
self.cmd_socket.close()
|
|
|
|
|
|
self.data_socket.close()
|
2026-06-05 09:34:29 +08:00
|
|
|
|
self.context.term()
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log(f"服务器已显式关闭 - 命令端口: {self.cmd_port}, 数据端口: {self.data_port}", level="INFO")
|
2026-06-05 09:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2026-06-08 11:56:42 +08:00
|
|
|
|
# 初始化并启动服务器
|
2026-06-05 09:34:29 +08:00
|
|
|
|
server = zmqServer()
|
2026-06-06 09:16:49 +08:00
|
|
|
|
server.start()
|
|
|
|
|
|
|
|
|
|
|
|
# 保持主线程运行
|
|
|
|
|
|
try:
|
|
|
|
|
|
while server.running:
|
|
|
|
|
|
threading.Event().wait(1)
|
|
|
|
|
|
except KeyboardInterrupt:
|
2026-06-08 11:56:42 +08:00
|
|
|
|
algo_log("收到键盘中断信号,正在停止服务器...", level="INFO")
|
|
|
|
|
|
server.stop()
|