This commit is contained in:
2025-01-21 03:09:10 +00:00
parent 33aafc7102
commit 3204ec5ccb
61 changed files with 0 additions and 105770 deletions
View File
View File
View File
View File
-243
View File
@@ -1,243 +0,0 @@
# main.py
import network
import socket
import struct
from machine import Timer, ADC, Pin
import array
import gc
import math
import time
# 配置信息
SSID = "Obscura"
PASSWORD = "Obscura2024"
WS_HOST = "222.186.10.253"
WS_PORT = 6002
SERIAL_NUMBER = "1101-5042b37b50-42b695"
class HighSpeedCollector:
def __init__(self):
# 移除 ADC 初始化
# 添加模拟数据生成所需的变量
self.phase = [0, 0, 0, 0] # 四个通道的相位
self.frequencies = [1, 2, 3, 4] # 四个不同频率的正弦波
# 保持其他初始化不变
self.buffers = [
array.array('H', [0] * 1000), # buffer 1
array.array('H', [0] * 1000) # buffer 2
]
self.current_buffer = 0
self.buffer_index = 0
# 网络相关
self.sock = None
self.send_buffer = bytearray(4096) # 发送缓冲区
self.buffer_view = memoryview(self.send_buffer)
self.is_connected = False
# 采样定时器
self.sample_timer = Timer(0)
def connect_wifi(self):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
pass
def connect_ws(self):
print("开始连接WebSocket服务器...")
print(f"正在连接到 {WS_HOST}:{WS_PORT}")
try:
self.sock = socket.socket()
self.sock.connect((WS_HOST, WS_PORT))
print("Socket连接成功")
# WebSocket握手
key = "dGhlIHNhbXBsZSBub25jZQ=="
handshake = "GET / HTTP/1.1\r\nHost: " + WS_HOST + ":" + str(WS_PORT) + "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: " + key + "\r\nSec-WebSocket-Version: 13\r\n\r\n"
print("发送WebSocket握手请求...")
self.sock.send(handshake.encode())
response = self.sock.recv(1024)
print("收到服务器响应:", response)
# 发送序列号
print(f"发送设备序列号: {SERIAL_NUMBER}")
sn_frame = self._create_ws_frame(SERIAL_NUMBER.encode(), 0x81)
self.sock.send(sn_frame)
self.is_connected = True
print("WebSocket连接建立成功!")
except Exception as e:
print("WebSocket连接失败:", str(e))
self.is_connected = False
raise e
def _create_ws_frame(self, data, opcode=0x82):
"""创建WebSocket帧"""
data_len = len(data)
if data_len < 126:
self.buffer_view[0] = opcode
self.buffer_view[1] = data_len
offset = 2
else:
self.buffer_view[0] = opcode
self.buffer_view[1] = 126
self.buffer_view[2] = (data_len >> 8) & 0xFF
self.buffer_view[3] = data_len & 0xFF
offset = 4
self.buffer_view[offset:offset+data_len] = data
return self.buffer_view[:offset+data_len]
def _sample(self, _):
"""模拟生成传感器数据"""
if self.buffer_index >= 1000:
return
# 生成4个通道的模拟数据
for i in range(4):
# 生成正弦波,范围在0-4095之间
value = int(2048 + 2047 * math.sin(self.phase[i]))
self.buffers[self.current_buffer][self.buffer_index] = value
# 每100个样本打印一次
if self.buffer_index % 100 == 0:
print(f"生成数据 - 通道{i+1}: {value}")
# 更新相位
self.phase[i] += 0.1 * self.frequencies[i]
if self.phase[i] >= 2 * math.pi:
self.phase[i] -= 2 * math.pi
self.buffer_index += 1
# 缓冲区满时切换
if self.buffer_index >= 1000:
print(f"缓冲区 {self.current_buffer} 已满,切换到缓冲区 {1 - self.current_buffer}")
self.current_buffer = 1 - self.current_buffer
self.buffer_index = 0
def _send_data(self, _):
"""发送数据回调"""
print("\n=== 开始发送数据 ===")
if not self.is_connected:
print("WebSocket未连接,尝试重新连接...")
try:
self.connect_ws()
except Exception as e:
print(f"重连失败: {e}")
return
try:
# 获取待发送的缓冲区
buffer_to_send = self.buffers[1 - self.current_buffer]
print(f"准备发送缓冲区 {1 - self.current_buffer} 的数据")
# 打包数据:时间戳 + 1000个采样点
timestamp = time.ticks_ms() & 0xFFFF
print(f"当前时间戳: {timestamp}")
# 分块发送数据(每250个采样点一包)
for i in range(0, 1000, 250):
chunk = buffer_to_send[i:i+250]
print(f"\n发送第 {i//250 + 1} 块数据")
try:
packed_data = struct.pack(
'<HH250H',
timestamp, # 时间戳
i, # 块索引
*chunk # 数据
)
frame = self._create_ws_frame(packed_data)
bytes_sent = self.sock.send(frame)
print(f"发送数据包成功 - 发送字节: {bytes_sent}")
except Exception as e:
print(f"数据发送错误: {e}")
self.is_connected = False
return
print("=== 数据发送完成 ===\n")
except Exception as e:
print('发送数据过程出错:', str(e))
self.is_connected = False
def start(self):
"""启动高速采样"""
print("启动采样定时器...")
try:
# 只启动采样定时器
self.sample_timer.init(
freq=1000, # 1kHz采样率
mode=Timer.PERIODIC,
callback=self._sample
)
print("采样定时器启动成功")
except Exception as e:
print(f"采样定时器启动失败: {e}")
print("定时器启动完成")
def stop(self):
"""停止采样"""
self.sample_timer.deinit()
if self.sock:
self.sock.close()
def main():
print("=== 程序开始启动 ===")
gc.enable()
try:
print("初始化采集器...")
collector = HighSpeedCollector()
print("开始连接WiFi...")
collector.connect_wifi()
print("开始连接WebSocket服务器...")
collector.connect_ws()
print("启动数据采集...")
collector.start()
print("数据采集已启动,进入主循环")
last_send_time = time.ticks_ms()
send_interval = 100 # 每100ms发送一次数据
while True:
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) >= send_interval:
print("\n=== 检查发送时间 ===")
collector._send_data(None)
last_send_time = current_time
if gc.mem_free() < 5000:
gc.collect()
except Exception as e:
print("程序出错:", str(e))
raise e
except KeyboardInterrupt:
print("程序被手动中断")
finally:
if 'collector' in locals():
collector.stop()
print("=== 程序已停止 ===")
# 直接调用main(),不使用if __name__ == '__main__'
print("正在启动主程序...")
main()
-268
View File
@@ -1,268 +0,0 @@
# main.py
import network
import socket
import struct
from machine import Timer, ADC, Pin
import array
import gc
import math
import time
# 配置信息
SSID = "Obscura"
PASSWORD = "Obscura2024"
WS_HOST = "222.186.10.253"
WS_PORT = 6002
SERIAL_NUMBER = "1101-5042b37b50-42b695"
class HighSpeedCollector:
def __init__(self):
# 移除 ADC 初始化
# 添加模拟数据生成所需的变量
self.phase = [0, 0, 0, 0] # 四个通道的相位
self.frequencies = [1, 2, 3, 4] # 四个不同频率的正弦波
# 保持其他初始化不变
self.buffers = [
array.array('H', [0] * 1000), # 250个采样点 * 4个通道
array.array('H', [0] * 1000)
]
self.current_buffer = 0
self.buffer_index = 0
self.buffer_ready = False # 添加标志位表示缓冲区是否准备好发送
# 网络相关
self.sock = None
self.send_buffer = bytearray(4096) # 发送缓冲区
self.buffer_view = memoryview(self.send_buffer)
self.is_connected = False
# 采样定时器
self.sample_timer = Timer(0)
# 添加性能统计相关的属性初始化
self.total_bytes_sent = 0
self.qps_counter = 0
self.qps_start_time = 0
self.last_qps_print_time = 0
self.qps_print_interval = 1000
self.running = False
def connect_wifi(self):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
pass
def connect_ws(self):
print("开始连接WebSocket服务器...")
print(f"正在连接到 {WS_HOST}:{WS_PORT}")
try:
self.sock = socket.socket()
self.sock.connect((WS_HOST, WS_PORT))
print("Socket连接成功")
# WebSocket握手
key = "dGhlIHNhbXBsZSBub25jZQ=="
handshake = "GET / HTTP/1.1\r\nHost: " + WS_HOST + ":" + str(WS_PORT) + "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: " + key + "\r\nSec-WebSocket-Version: 13\r\n\r\n"
print("发送WebSocket握手请求...")
self.sock.send(handshake.encode())
response = self.sock.recv(1024)
print("收到服务器响应:", response)
# 发送序列号
print(f"发送设备序列号: {SERIAL_NUMBER}")
sn_frame = self._create_ws_frame(SERIAL_NUMBER.encode(), 0x81)
self.sock.send(sn_frame)
self.is_connected = True
print("WebSocket连接建立成功!")
except Exception as e:
print("WebSocket连接失败:", str(e))
self.is_connected = False
raise e
def _create_ws_frame(self, data, opcode=0x82):
"""创建WebSocket帧"""
data_len = len(data)
if data_len < 126:
self.buffer_view[0] = opcode
self.buffer_view[1] = data_len
offset = 2
else:
self.buffer_view[0] = opcode
self.buffer_view[1] = 126
self.buffer_view[2] = (data_len >> 8) & 0xFF
self.buffer_view[3] = data_len & 0xFF
offset = 4
self.buffer_view[offset:offset+data_len] = data
return self.buffer_view[:offset+data_len]
def _sample(self, _):
"""模拟生成传感器数据"""
if self.buffer_index >= 1000:
return
# 为4个通道生成数据
for channel in range(4):
# 计算当前通道的数据在buffer中的位置
buffer_pos = self.buffer_index * 4 + channel
# 生成不同频率的正弦波
value = int(2048 + 2047 * math.sin(self.phase[channel]))
self.buffers[self.current_buffer][buffer_pos] = value
# 更新每个通道的相位
self.phase[channel] += 0.1 * self.frequencies[channel]
if self.phase[channel] >= 2 * math.pi:
self.phase[channel] -= 2 * math.pi
self.buffer_index += 1
# 缓冲区满时切换 (注意:现在是4000个点,因为每个采样点包含4个通道)
if self.buffer_index >= 250: # 修改为250,因为每个点有4个值
self.buffer_ready = True
self.current_buffer = 1 - self.current_buffer
self.buffer_index = 0
def _send_data(self, _):
"""发送数据回调"""
current_time = time.ticks_ms()
# 初始化性能统计时间
if self.qps_start_time == 0:
self.qps_start_time = current_time
self.last_qps_print_time = current_time
if not self.buffer_ready: # 如果缓冲区未准备好,直接返回
return
if not self.is_connected:
try:
self.connect_ws()
except Exception as e:
print(f"重连失败: {e}")
return
try:
buffer_to_send = self.buffers[1 - self.current_buffer]
# 一次性打包所有数据
for i in range(0, 1000, 250):
chunk = buffer_to_send[i:i+250]
# 确保chunk长度正确
if len(chunk) != 250:
print(f"警告:数据块长度异常: {len(chunk)}")
continue
# 打包数据
packed_data = struct.pack(
'<HH250H',
current_time & 0xFFFF,
i,
*chunk
)
# 创建完整的WebSocket帧并发送
frame = self._create_ws_frame(packed_data)
bytes_sent = self.sock.send(frame)
# 更新统计数据
self.total_bytes_sent += bytes_sent
self.qps_counter += 1
# 打印性能统计信息
if time.ticks_diff(current_time, self.last_qps_print_time) >= self.qps_print_interval:
elapsed_seconds = time.ticks_diff(current_time, self.qps_start_time) / 1000
qps = self.qps_counter / elapsed_seconds if elapsed_seconds > 0 else 0
throughput = self.total_bytes_sent / elapsed_seconds / 1024 if elapsed_seconds > 0 else 0
self.last_qps_print_time = current_time
self.buffer_ready = False # 重置缓冲区状态
except Exception as e:
print('发送数据过程出错:', str(e))
self.is_connected = False
def start(self):
"""启动高速采样"""
print("启动采样定时器...")
self.running = True # 设置运行标志
try:
self.sample_timer.init(
freq=1000,
mode=Timer.PERIODIC,
callback=self._sample
)
print("采样定时器启动成功")
except Exception as e:
print(f"采样定时器启动失败: {e}")
self.running = False
def stop(self):
"""停止采样"""
print("正在停止采集器...")
self.running = False # 清除运行标志
self.sample_timer.deinit()
if self.sock:
try:
self.sock.close()
except:
pass
print("采集器已停止")
def main():
print("=== 程序开始启动 ===")
gc.enable()
collector = None
try:
print("初始化集器...")
collector = HighSpeedCollector()
print("开始连接WiFi...")
collector.connect_wifi()
print("始连接WebSocket服务器...")
collector.connect_ws()
print("启动数据采集...")
collector.start()
print("数据采集已启动,进入主循环")
last_send_time = time.ticks_ms()
send_interval = 20 # 减少发送间隔到20ms
while collector.running:
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) >= send_interval:
collector._send_data(None)
last_send_time = current_time
except KeyboardInterrupt:
print("\n程序被手动中断")
except Exception as e:
print("程序出错:", str(e))
finally:
if collector:
collector.stop()
print("=== 程序已停止 ===")
# 主程序入口
print("正在启动主程序...")
try:
main()
except KeyboardInterrupt:
print("\n程序已被手动终止")
-268
View File
@@ -1,268 +0,0 @@
# main.py
import network
import socket
import struct
from machine import Timer, ADC, Pin
import array
import gc
import math
import time
# 配置信息
SSID = "Obscura"
PASSWORD = "Obscura2024"
WS_HOST = "222.186.10.253"
WS_PORT = 6002
SERIAL_NUMBER = "1101-5042b37b50-42b695"
class HighSpeedCollector:
def __init__(self):
# 移除 ADC 初始化
# 添加模拟数据生成所需的变量
self.phase = [0, 0, 0, 0] # 四个通道的相位
self.frequencies = [1, 2, 3, 4] # 四个不同频率的正弦波
# 保持其他初始化不变
self.buffers = [
array.array('H', [0] * 1000), # 250个采样点 * 4个通道
array.array('H', [0] * 1000)
]
self.current_buffer = 0
self.buffer_index = 0
self.buffer_ready = False # 添加标志位表示缓冲区是否准备好发送
# 网络相关
self.sock = None
self.send_buffer = bytearray(4096) # 发送缓冲区
self.buffer_view = memoryview(self.send_buffer)
self.is_connected = False
# 采样定时器
self.sample_timer = Timer(0)
# 添加性能统计相关的属性初始化
self.total_bytes_sent = 0
self.qps_counter = 0
self.qps_start_time = 0
self.last_qps_print_time = 0
self.qps_print_interval = 1000
self.running = False
def connect_wifi(self):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
pass
def connect_ws(self):
print("开始连接WebSocket服务器...")
print(f"正在连接到 {WS_HOST}:{WS_PORT}")
try:
self.sock = socket.socket()
self.sock.connect((WS_HOST, WS_PORT))
print("Socket连接成功")
# WebSocket握手
key = "dGhlIHNhbXBsZSBub25jZQ=="
handshake = "GET / HTTP/1.1\r\nHost: " + WS_HOST + ":" + str(WS_PORT) + "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: " + key + "\r\nSec-WebSocket-Version: 13\r\n\r\n"
print("发送WebSocket握手请求...")
self.sock.send(handshake.encode())
response = self.sock.recv(1024)
print("收到服务器响应:", response)
# 发送序列号
print(f"发送设备序列号: {SERIAL_NUMBER}")
sn_frame = self._create_ws_frame(SERIAL_NUMBER.encode(), 0x81)
self.sock.send(sn_frame)
self.is_connected = True
print("WebSocket连接建立成功!")
except Exception as e:
print("WebSocket连接失败:", str(e))
self.is_connected = False
raise e
def _create_ws_frame(self, data, opcode=0x82):
"""创建WebSocket帧"""
data_len = len(data)
if data_len < 126:
self.buffer_view[0] = opcode
self.buffer_view[1] = data_len
offset = 2
else:
self.buffer_view[0] = opcode
self.buffer_view[1] = 126
self.buffer_view[2] = (data_len >> 8) & 0xFF
self.buffer_view[3] = data_len & 0xFF
offset = 4
self.buffer_view[offset:offset+data_len] = data
return self.buffer_view[:offset+data_len]
def _sample(self, _):
"""模拟生成传感器数据"""
if self.buffer_index >= 1000:
return
# 为4个通道生成数据
for channel in range(4):
# 计算当前通道的数据在buffer中的位置
buffer_pos = self.buffer_index * 4 + channel
# 生成不同频率的正弦波
value = int(2048 + 2047 * math.sin(self.phase[channel]))
self.buffers[self.current_buffer][buffer_pos] = value
# 更新每个通道的相位
self.phase[channel] += 0.1 * self.frequencies[channel]
if self.phase[channel] >= 2 * math.pi:
self.phase[channel] -= 2 * math.pi
self.buffer_index += 1
# 缓冲区满时切换 (注意:现在是4000个点,因为每个采样点包含4个通道)
if self.buffer_index >= 250: # 修改为250,因为每个点有4个值
self.buffer_ready = True
self.current_buffer = 1 - self.current_buffer
self.buffer_index = 0
def _send_data(self, _):
"""发送数据回调"""
current_time = time.ticks_ms()
# 初始化性能统计时间
if self.qps_start_time == 0:
self.qps_start_time = current_time
self.last_qps_print_time = current_time
if not self.buffer_ready: # 如果缓冲区未准备好,直接返回
return
if not self.is_connected:
try:
self.connect_ws()
except Exception as e:
print(f"重连失败: {e}")
return
try:
buffer_to_send = self.buffers[1 - self.current_buffer]
# 一次性打包所有数据
for i in range(0, 1000, 250):
chunk = buffer_to_send[i:i+250]
# 确保chunk长度正确
if len(chunk) != 250:
print(f"警告:数据块长度异常: {len(chunk)}")
continue
# 打包数据
packed_data = struct.pack(
'<HH250H',
current_time & 0xFFFF,
i,
*chunk
)
# 创建完整的WebSocket帧并发送
frame = self._create_ws_frame(packed_data)
bytes_sent = self.sock.send(frame)
# 更新统计数据
self.total_bytes_sent += bytes_sent
self.qps_counter += 1
# 打印性能统计信息
if time.ticks_diff(current_time, self.last_qps_print_time) >= self.qps_print_interval:
elapsed_seconds = time.ticks_diff(current_time, self.qps_start_time) / 1000
qps = self.qps_counter / elapsed_seconds if elapsed_seconds > 0 else 0
throughput = self.total_bytes_sent / elapsed_seconds / 1024 if elapsed_seconds > 0 else 0
self.last_qps_print_time = current_time
self.buffer_ready = False # 重置缓冲区状态
except Exception as e:
print('发送数据过程出错:', str(e))
self.is_connected = False
def start(self):
"""启动高速采样"""
print("启动采样定时器...")
self.running = True # 设置运行标志
try:
self.sample_timer.init(
freq=1000,
mode=Timer.PERIODIC,
callback=self._sample
)
print("采样定时器启动成功")
except Exception as e:
print(f"采样定时器启动失败: {e}")
self.running = False
def stop(self):
"""停止采样"""
print("正在停止采集器...")
self.running = False # 清除运行标志
self.sample_timer.deinit()
if self.sock:
try:
self.sock.close()
except:
pass
print("采集器已停止")
def main():
print("=== 程序开始启动 ===")
gc.enable()
collector = None
try:
print("初始化集器...")
collector = HighSpeedCollector()
print("开始连接WiFi...")
collector.connect_wifi()
print("始连接WebSocket服务器...")
collector.connect_ws()
print("启动数据采集...")
collector.start()
print("数据采集已启动,进入主循环")
last_send_time = time.ticks_ms()
send_interval = 20 # 减少发送间隔到20ms
while collector.running:
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) >= send_interval:
collector._send_data(None)
last_send_time = current_time
except KeyboardInterrupt:
print("\n程序被手动中断")
except Exception as e:
print("程序出错:", str(e))
finally:
if collector:
collector.stop()
print("=== 程序已停止 ===")
# 主程序入口
print("正在启动主程序...")
try:
main()
except KeyboardInterrupt:
print("\n程序已被手动终止")
-209
View File
@@ -1,209 +0,0 @@
# main.py
import network
import socket
import struct
from machine import Timer, ADC, Pin
import array
import gc
import time
import random
# 配置信息
SSID = "Obscura"
PASSWORD = "Obscura2024"
UDP_HOST = "222.186.10.253"
UDP_PORT = 6002
SERIAL_NUMBER = "AD01-b088e8e9d3-64f663"
class HighSpeedCollector:
def __init__(self):
# 增加缓冲区大小以测试性能极限
self.buffers = [
array.array('H', [0] * 1000), # 增加到1000
array.array('H', [0] * 1000) # 增加到1000
]
self.current_buffer = 0
self.buffer_index = 0
self.buffer_ready = False
# UDP相关
self.sock = None
self.is_connected = False
self.sample_timer = Timer(0)
self.running = False
# 初始化器,添加合适的衰减和位宽配置
self.adc1 = ADC(Pin(0))
self.adc1.atten(ADC.ATTN_11DB) # 设置衰减为11dB
self.adc1.width(ADC.WIDTH_12BIT) # 设置12位分辨率
self.adc2 = ADC(Pin(1))
self.adc2.atten(ADC.ATTN_11DB)
self.adc2.width(ADC.WIDTH_12BIT)
self.adc3 = ADC(Pin(2))
self.adc3.atten(ADC.ATTN_11DB)
self.adc3.width(ADC.WIDTH_12BIT)
self.adc4 = ADC(Pin(3))
self.adc4.atten(ADC.ATTN_11DB)
self.adc4.width(ADC.WIDTH_12BIT)
self.qps = 0 # 新增QPS计数器
self.packets_this_second = 0 # 每秒发送的数据包数
def connect_wifi(self):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
gc.collect()
time.sleep(1)
def setup_udp(self):
"""初始化UDP连接"""
try:
gc.collect()
if self.sock:
try:
self.sock.close()
except:
pass
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置非阻塞模式
self.sock.setblocking(False)
# 首次连接时发送序列号
self.sock.sendto(SERIAL_NUMBER.encode(), (UDP_HOST, UDP_PORT))
print(f"首次连接,发送序列号: {SERIAL_NUMBER}")
self.is_connected = True
except Exception as e:
print("UDP初始化失败:", str(e))
self.is_connected = False
self.sock = None
def _sample(self, _):
"""采集传感器数据"""
if self.buffer_index >= 250: # 修改为250 (1000/4)
return
# 读取4个ADC通道的数据
pos = self.buffer_index * 4
self.buffers[self.current_buffer][pos] = random.randint(2000, 3500)
self.buffers[self.current_buffer][pos + 1] = random.randint(2500, 3000)
self.buffers[self.current_buffer][pos + 2] = random.randint(2000, 3000)
self.buffers[self.current_buffer][pos + 3] = random.randint(1000, 2000)
self.buffer_index += 1
if self.buffer_index >= 250:
self.buffer_ready = True
self.current_buffer = 1 - self.current_buffer
self.buffer_index = 0
def _send_data(self, _):
"""通过UDP发送数据"""
if not self.is_connected:
print("UDP未连接,尝试重新连接...")
self.setup_udp()
if not self.is_connected:
print("UDP重连失败")
return
try:
# 检查是否有请求序列号的消息
try:
data, addr = self.sock.recvfrom(32) # 非阻塞接收
if data == b'REQUEST_SERIAL':
print(f"收到序列号请求,发送序列号: {SERIAL_NUMBER}")
self.sock.sendto(SERIAL_NUMBER.encode(), addr)
return
except:
pass # 没有收到数据继续正常发送
# 如果buffer没有准备好,直接返回
if not self.buffer_ready:
return
buffer_to_send = self.buffers[1 - self.current_buffer]
current_time = time.ticks_ms()
packed_data = struct.pack(
'<HH250H',
current_time & 0xFFFF,
0,
*buffer_to_send[:250]
)
self.sock.sendto(packed_data, (UDP_HOST, UDP_PORT))
print(f"发送数据包成功 - 时间戳: {current_time}, 数据点数: {len(buffer_to_send[:250])}")
self.buffer_ready = False
self.packets_this_second += 1
except Exception as e:
print('发送数据错误:', str(e))
self.is_connected = False
self.sock = None
def start(self):
"""启动采样"""
self.running = True
try:
self.sample_timer.init(
freq=500,
mode=Timer.PERIODIC,
callback=self._sample
)
print("采样定时器启动成功") # 添加调试信息
except Exception as e:
print(f"采样定时器启动失败: {e}")
self.running = False
def stop(self):
"""停止采样"""
self.running = False
self.sample_timer.deinit()
if self.sock:
try:
self.sock.close()
except:
pass
def main():
gc.enable()
collector = None
try:
print("初始化采集器...") # 添加调试信息
collector = HighSpeedCollector()
collector.connect_wifi()
collector.setup_udp()
collector.start()
last_send_time = time.ticks_ms()
send_interval = 50 # 修改为50ms
print("进入主循环...") # 添加调试信息
while collector.running:
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) >= send_interval:
collector._send_data(None)
last_send_time = current_time
time.sleep_ms(10)
except Exception as e:
print("程序出错:", str(e))
finally:
if collector:
collector.stop()
if __name__ == "__main__":
main()
-158
View File
@@ -1,158 +0,0 @@
# main.py
import network
import socket
import struct
from machine import Timer, ADC, Pin
import array
import gc
import time
import random
# 配置信息
SSID = "Obscura"
PASSWORD = "Obscura2024"
UDP_HOST = "222.186.10.253"
UDP_PORT = 6002
SERIAL_NUMBER = "ES01-21ef678d9b-efe5fb"
class HighSpeedCollector:
def __init__(self):
# 移除缓冲区相关代码
self.sock = None
self.is_connected = False
self.sample_timer = Timer(0)
self.running = False
# ADC配置
self.adc = ADC(Pin(3))
self.adc.atten(ADC.ATTN_11DB)
self.adc.width(ADC.WIDTH_12BIT)
def connect_wifi(self):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
gc.collect()
time.sleep(1)
def setup_udp(self):
"""初始化UDP连接"""
try:
gc.collect()
if self.sock:
try:
self.sock.close()
except:
pass
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置非阻塞模式
self.sock.setblocking(False)
# 首次连接时发送序列号
self.sock.sendto(SERIAL_NUMBER.encode(), (UDP_HOST, UDP_PORT))
print(f"首次连接,发送序列号: {SERIAL_NUMBER}")
self.is_connected = True
except Exception as e:
print("UDP初始化失败:", str(e))
self.is_connected = False
self.sock = None
def _sample_and_send(self, _):
"""直接采样并发送数据"""
if not self.is_connected:
return
try:
# 检查是否有序列号请求
try:
data, addr = self.sock.recvfrom(1024)
if data == b'REQUEST_SERIAL':
self.sock.sendto(SERIAL_NUMBER.encode(), addr)
return
except:
pass
# 使用相对时间戳,从0开始计数
if not hasattr(self, 'start_time'):
self.start_time = time.ticks_ms()
# 计算相对时间戳(毫秒)
relative_ms = time.ticks_diff(time.ticks_ms(), self.start_time)
# 取模确保在16位范围内
timestamp = relative_ms % 65536
# 直接采集50个数据点
samples = array.array('H', [self.adc.read() for _ in range(50)])
packed_data = struct.pack(
'<HH50H',
timestamp,
0, # chunk_index
*samples
)
self.sock.sendto(packed_data, (UDP_HOST, UDP_PORT))
except Exception as e:
print('发送数据错误:', str(e))
self.is_connected = False
self.sock = None
def start(self):
"""启动采样和发送"""
self.running = True
try:
self.sample_timer.init(
freq=20, # 每秒20次采样和发送
mode=Timer.PERIODIC,
callback=self._sample_and_send
)
print("采样定时器启动成功 - 频率: 20Hz")
except Exception as e:
print(f"采样定时器启动失败: {e}")
self.running = False
def stop(self):
"""停止采样"""
self.running = False
self.sample_timer.deinit()
if self.sock:
try:
self.sock.close()
except:
pass
def main():
gc.enable()
collector = None
try:
print("初始化采集器...")
collector = HighSpeedCollector()
collector.connect_wifi()
collector.setup_udp()
collector.start()
print("进入主循环...")
while collector.running:
gc.collect() # 定期进行垃圾回收
time.sleep(0.1) # 添加短暂延时,减少CPU使用
except Exception as e:
print("程序出错:", str(e))
finally:
if collector:
collector.stop()
if __name__ == "__main__":
main()
-685
View File
@@ -1,685 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, List
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket: Dict[str, websockets.WebSocketClientProtocol] = {} # serial_number -> WebSocketClientProtocol
self.experiment_id: Dict[str, str] = {} # serial_number -> experiment_id
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
# 添加Redis批量处理
self.redis_batch_size = 100
self.redis_batches: Dict[str, List] = {}
self.redis_batch_locks: Dict[str, asyncio.Lock] = {}
# 添加实验状态缓存
self.experiment_status: Dict[str, Dict[str, str]] = {} # experiment_id -> {serial_number: status}
self.status_check_interval = 5 # 每5秒检查一次状态
# 添加状态消息缓存
self.status_message_cache: Dict[str, bool] = {} # experiment_id:serial_number -> has_printed
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每个通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接保持活跃"""
try:
# 检查现有连接是否可用
if (serial_number in self.websocket and
self.experiment_id.get(serial_number) == experiment_id and
not self.websocket[serial_number].closed):
return True
async with self.reconnect_lock:
# 关闭旧连接
await self.close_websocket(serial_number)
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}/{serial_number}"
print(f"建立新的WebSocket连接: {uri}")
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 添加心跳机制
ping_timeout=10,
close_timeout=5
)
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await websocket.send(json.dumps(init_message))
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "success":
self.websocket[serial_number] = websocket
self.experiment_id[serial_number] = experiment_id
print(f"WebSocket连接成功建立: {serial_number}")
return True
await websocket.close()
return False
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
return False
async def close_websocket(self, serial_number):
"""安全关闭特定设备的WebSocket连接"""
if serial_number in self.websocket:
try:
await self.websocket[serial_number].close()
except Exception:
pass
del self.websocket[serial_number]
del self.experiment_id[serial_number]
async def batch_redis_write(self, stream_key: str, data_point: dict):
"""批量写入Redis"""
if stream_key not in self.redis_batches:
self.redis_batches[stream_key] = []
self.redis_batch_locks[stream_key] = asyncio.Lock()
async with self.redis_batch_locks[stream_key]:
self.redis_batches[stream_key].append(data_point)
if len(self.redis_batches[stream_key]) >= self.redis_batch_size:
batch = self.redis_batches[stream_key]
self.redis_batches[stream_key] = []
# 使用pipeline批量写入
pipe = self.redis_client.pipeline()
for point in batch:
pipe.xadd(stream_key, point)
await pipe.execute()
async def check_experiment_status(self, experiment_id: str, serial_number: str) -> bool:
"""检查实验状态"""
try:
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
# 切换到db199
await self.redis_client.select(199)
status_key = f"experiment_status:{experiment_id}:{serial_number}"
status = await self.redis_client.get(status_key)
# 切回db200(用于其他操作)
await self.redis_client.select(200)
return status == "active"
except Exception as e:
print(f"检查实验状态时出错: {e}")
return False
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# Redis stream 处理 - 无论实验状态如何都保存数据
stream_key = f"experiment:{experiment_id}:{serial_number}"
channel_data = {
sensor_name: channels[channel_num]
for channel_num, sensor_name in sensors_map.items()
if channel_num < len(channels)
}
# Redis批量写入
await self.batch_redis_write(stream_key, {
'data': json.dumps({
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
})
})
# 检查实验是否处于活跃状态
is_active = await self.check_experiment_status(experiment_id, serial_number)
if not is_active:
# 使用组合键检查是否已经打印过停止消息
cache_key = f"{experiment_id}:{serial_number}"
if not self.status_message_cache.get(cache_key):
print(f"实验ID: {experiment_id}, 设备: {serial_number} 已停止,仅保存数据")
self.status_message_cache[cache_key] = True
return
else:
# 如果实验重新激活,清除缓存
cache_key = f"{experiment_id}:{serial_number}"
self.status_message_cache.pop(cache_key, None)
# 以下是WebSocket相关操作,只在实验活跃时执行
websocket = self.websocket.get(serial_number)
if not websocket or websocket.closed:
print(f"重新建立WebSocket连接: {serial_number}")
connected = await self.ensure_websocket_connected(experiment_id, serial_number)
if connected:
websocket = self.websocket.get(serial_number)
else:
print(f"无法建立WebSocket连接: {serial_number}")
return
try:
# 降采样处理
data_length = len(next(iter(channel_data.values())))
DOWNSAMPLE_RATE = max(5, data_length // 1000) # 动态调整降采样率
# 使用numpy进行高效降采样
import numpy as np
downsampled_data = {}
for sensor_name, data in channel_data.items():
data_array = np.array(data)
downsampled_data[sensor_name] = data_array[::DOWNSAMPLE_RATE].tolist()
# 计算时间戳序列
base_timestamp = timestamp
sample_interval = self.SAMPLE_INTERVAL_MS * DOWNSAMPLE_RATE
timestamps = [
base_timestamp + (i * sample_interval)
for i in range(len(next(iter(downsampled_data.values()))))
]
# 构建批量数据点
data_points = []
for i in range(len(timestamps)):
point_data = {
sensor_name: values[i]
for sensor_name, values in downsampled_data.items()
if i < len(values)
}
data_points.append({
'sensor_data': point_data,
'timestamp': timestamps[i]
})
# 批量发送数据
if websocket and not websocket.closed:
message = {
'type': 'data_batch',
'data_points': data_points,
'serial_number': serial_number,
'total_points': len(data_points)
}
# print(f"发送数据: {message}")
await websocket.send(json.dumps(message))
except websockets.exceptions.ConnectionClosed:
print(f"发送数据时连接断开,将在下次发送时重连: {serial_number}")
await self.close_websocket(serial_number)
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭所有 WebSocket 连接
if self.websocket:
# 创建所有序列号的副本,因为在关闭过程中字典会被修改
serial_numbers = list(self.websocket.keys())
for serial_number in serial_numbers:
await self.close_websocket(serial_number)
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭所有 WebSocket 连接
if self.websocket:
serial_numbers = list(self.websocket.keys())
for serial_number in serial_numbers:
await self.close_websocket(serial_number)
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-82
View File
@@ -1,82 +0,0 @@
import socket
import threading
import base64
import hashlib
import json
from datetime import datetime
class WebSocketServer:
def __init__(self, host='0.0.0.0', port=6002):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((host, port))
self.server.listen(5)
print(f"WebSocket服务器启动在 {host}:{port}")
def handle_client(self, client_socket, address):
try:
# 处理WebSocket握手
data = client_socket.recv(1024).decode('utf-8')
if "GET" in data:
key = ''
for line in data.split('\r\n'):
if "Sec-WebSocket-Key" in line:
key = line.split(': ')[1]
break
if key:
response_key = base64.b64encode(
hashlib.sha1(
(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
).digest()
).decode('utf-8')
response = (
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Accept: {response_key}\r\n\r\n"
)
client_socket.send(response.encode())
print(f"客户端 {address} 已连接")
# 简化后的数据接收循环
while True:
try:
# 接收原始数据
frame = client_socket.recv(4096)
if not frame:
break
# 直接打印接收到的原始数据
print(f"接收到的原始数据: {frame}")
except Exception as e:
print(f"数据接收错误: {e}")
break
except Exception as e:
print(f"连接处理错误: {e}")
finally:
client_socket.close()
print(f"客户端 {address} 已断开连接")
def start(self):
try:
while True:
client_socket, address = self.server.accept()
client_thread = threading.Thread(
target=self.handle_client,
args=(client_socket, address)
)
client_thread.start()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
self.server.close()
if __name__ == "__main__":
server = WebSocketServer()
server.start()
-255
View File
@@ -1,255 +0,0 @@
import socket
import threading
import base64
import hashlib
import asyncio
import websockets
import json
from datetime import datetime
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, Set
class WebSocketServer:
def __init__(self, host='0.0.0.0', port=6002):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((host, port))
self.server.listen(5)
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, str] = {} # serial_number -> experiment_id
# 缓存每个实验的WebSocket连接
self.experiment_websockets: Dict[str, websockets.WebSocketClientProtocol] = {}
print(f"WebSocket服务器启动在 {host}:{port}")
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
async def update_active_experiments(self):
"""更新活跃实验的设备映射"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
db = self.db_client["lab"]
try:
# 查找所有活跃的实验会话
active_sessions = db.experiment_sessions.find({"end_time": None})
# 清空当前映射
self.active_experiments.clear()
# 更新映射
async for session in active_sessions:
experiment_id = str(session["experiment_id"])
# 遍历会话中的所有设备
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
self.active_experiments[serial_number] = experiment_id
print(f"活跃实验映射已更新: {self.active_experiments}")
except Exception as e:
print(f"更新活跃实验时出错: {e}")
# 打印更详细的错误信息
import traceback
traceback.print_exc()
async def get_experiment_websocket(self, experiment_id: str) -> websockets.WebSocketClientProtocol:
"""获取或创建到实验WebSocket端点的连接"""
if experiment_id not in self.experiment_websockets:
# 直接连接到前端使用的WebSocket端点
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}"
try:
websocket = await websockets.connect(uri)
self.experiment_websockets[experiment_id] = websocket
print(f"Created new WebSocket connection for experiment {experiment_id}")
except Exception as e:
print(f"Error creating WebSocket connection: {e}")
return None
return self.experiment_websockets[experiment_id]
async def forward_data(self, data_frame: str):
"""转发数据到对应的实验WebSocket"""
try:
# 解析接收到的数据
data = json.loads(data_frame)
serial_number = data.get('serial_number')
if not serial_number:
print("Missing serial number in data")
return
# 如果serial_number不在映射中,更新活跃实验映射
if serial_number not in self.active_experiments:
await self.update_active_experiments()
# 获取experiment_id
experiment_id = self.active_experiments.get(serial_number)
if not experiment_id:
print(f"No active experiment found for serial number: {serial_number}")
return
# 获取或创建WebSocket连接
websocket = await self.get_experiment_websocket(experiment_id)
if websocket:
try:
await websocket.send(json.dumps(data))
print(f"Data forwarded to experiment {experiment_id}")
except websockets.exceptions.ConnectionClosed:
# 如果连接已关闭,删除缓存的连接
del self.experiment_websockets[experiment_id]
# 重试一次
websocket = await self.get_experiment_websocket(experiment_id)
if websocket:
await websocket.send(json.dumps(data))
except json.JSONDecodeError:
print("Invalid JSON data received")
except Exception as e:
print(f"Error forwarding data: {e}")
def handle_client(self, client_socket, address):
"""处理客户端连接"""
# 为每个客户端创建新的事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 处理WebSocket握手
data = client_socket.recv(1024).decode('utf-8')
if "GET" in data:
key = ''
for line in data.split('\r\n'):
if "Sec-WebSocket-Key" in line:
key = line.split(': ')[1]
break
if key:
response_key = base64.b64encode(
hashlib.sha1(
(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
).digest()
).decode('utf-8')
response = (
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Accept: {response_key}\r\n\r\n"
)
client_socket.send(response.encode())
print(f"客户端 {address} 已连接")
# 初始化活跃实验映射
loop.run_until_complete(self.update_active_experiments())
# 数据接收循环
while True:
try:
frame = client_socket.recv(4096)
if not frame:
break
decoded_data = self.decode_websocket_frame(frame)
if decoded_data:
try:
json_data = json.loads(decoded_data)
print(f"解析后的数据: {json_data}")
# 使用当前事件循环运行异步任务
loop.run_until_complete(self.forward_data(decoded_data))
except json.JSONDecodeError as e:
print(f"JSON 解析错误: {e}")
except Exception as e:
print(f"数据接收错误: {e}")
break
except Exception as e:
print(f"连接处理错误: {e}")
finally:
client_socket.close()
print(f"客户端 {address} 已断开连接")
loop.close()
def decode_websocket_frame(self, frame):
"""解析WebSocket帧"""
try:
if len(frame) < 2:
return None
# 检查是否是文本帧
opcode = frame[0] & 0x0f
if opcode != 0x1: # 0x1 表示文本帧
return None
# 获取掩码位和数据长度
has_mask = frame[1] & 0x80
payload_length = frame[1] & 0x7f
# 确定数据长度和起始位置
header_length = 2
if payload_length == 126:
header_length += 2
payload_length = int.from_bytes(frame[2:4], byteorder='big')
elif payload_length == 127:
header_length += 8
payload_length = int.from_bytes(frame[2:10], byteorder='big')
# 如果有掩码,处理掩码
if has_mask:
mask = frame[header_length:header_length + 4]
header_length += 4
# 解码数据
payload = bytearray()
for i in range(payload_length):
payload.append(frame[header_length + i] ^ mask[i % 4])
decoded_data = payload.decode('utf-8')
else:
# 如果没有掩码,直接获取数据
decoded_data = frame[header_length:header_length + payload_length].decode('utf-8')
return decoded_data
except Exception as e:
print(f"Error decoding WebSocket frame: {e}")
print(f"Raw frame: {frame}")
return None
async def cleanup(self):
"""清理资源"""
# 关闭所有WebSocket连接
for websocket in self.experiment_websockets.values():
await websocket.close()
# 关闭MongoDB连接
if self.db_client:
self.db_client.close()
def start(self):
"""启动服务器"""
try:
print("正在启动WebSocket服务器...")
print("等待客户端连接...")
while True:
client_socket, address = self.server.accept()
client_thread = threading.Thread(
target=self.handle_client,
args=(client_socket, address)
)
client_thread.start()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
self.loop.run_until_complete(self.cleanup())
finally:
self.server.close()
self.loop.close()
if __name__ == "__main__":
server = WebSocketServer()
server.start()
-510
View File
@@ -1,510 +0,0 @@
import socket
import threading
import base64
import hashlib
import asyncio
import websockets
import json
from datetime import datetime
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, Set
import redis
from redis.exceptions import ConnectionError
from redis.commands.json.path import Path
import numpy as np
import struct
import time
from redis import asyncio as aioredis
class WebSocketServer:
def __init__(self, host='0.0.0.0', port=6002):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((host, port))
self.server.listen(5)
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, str] = {} # serial_number -> experiment_id
# 缓存每个实验的WebSocket连接
self.experiment_websockets: Dict[str, websockets.WebSocketClientProtocol] = {}
print(f"WebSocket服务器启动在 {host}:{port}")
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
# Redis连接
self.redis_client = None
# 添加客户端序列号映射
self.client_serials: Dict[socket.socket, str] = {} # socket -> serial_number
self.frame_buffer = bytearray() # 添加帧缓冲区
self.frame_length = 0 # 添加帧长度记录
async def init_redis(self):
"""初始化Redis异步连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
"redis://222.186.10.253:6379",
password='Obscura@2024',
db=199,
decode_responses=True
)
async def update_active_experiments(self):
"""更新活跃实验的设备映射"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
db = self.db_client["lab"]
try:
# 查找所有活跃的实验会话
active_sessions = db.experiment_sessions.find({"end_time": None})
# 清空当前映射
self.active_experiments.clear()
# 更新映射
async for session in active_sessions:
experiment_id = str(session["experiment_id"])
# 遍历会话中的所有设备
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
self.active_experiments[serial_number] = experiment_id
print(f"活跃实验映射已更新: {self.active_experiments}")
except Exception as e:
print(f"更新活跃实验时出错: {e}")
# 印更详细的错误信息
import traceback
traceback.print_exc()
async def get_experiment_websocket(self, experiment_id: str) -> websockets.WebSocketClientProtocol:
"""获取或创建到实验WebSocket端点的连接"""
try:
if experiment_id not in self.experiment_websockets or \
self.experiment_websockets[experiment_id].closed:
# 移除旧连接
if experiment_id in self.experiment_websockets:
del self.experiment_websockets[experiment_id]
# 创建新连接
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}"
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 每20秒发送一次ping
ping_timeout=10, # ping超时时间
close_timeout=10, # 关闭超时时间
max_size=None, # 不限制消息大小
extra_headers={
'Connection': 'keep-alive',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache'
}
)
self.experiment_websockets[experiment_id] = websocket
print(f"为实验 {experiment_id} 创建了新的WebSocket连")
except Exception as e:
print(f"创建WebSocket连接失败: {e}")
return None
return self.experiment_websockets[experiment_id]
except Exception as e:
print(f"获取WebSocket连接时出错: {e}")
return None
async def process_and_forward_data(self, data: dict):
"""处理并转发数据"""
try:
serial_number = data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
return
experiment_id = self.active_experiments[serial_number]
websocket = await self.get_experiment_websocket(experiment_id)
if not websocket:
return
# 获取所有通道数据
channel_data = data.get('channel_data', {})
total_points = len(next(iter(channel_data.values())))
step = total_points // 7 # 将数据分成7份
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
# 存储数据到Redis DB199
try:
stream_key = f"experiment:{experiment_id}:{serial_number}"
timestamp = data.get('timestamp')
point_data = {
'channel_data': {
'humidity': channel_data['channel1'],
'temperature': channel_data['channel2'],
'pressure': channel_data['channel3'],
'light': channel_data['channel4']
},
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': total_points
}
# 使用异步方式存储数据
await self.redis_client.xadd(
stream_key,
{'data': json.dumps(point_data)}
)
except Exception as e:
print(f"Redis存储错误: {e}")
import traceback
traceback.print_exc()
# 发送数据到WebSocket(保持原有的分7次发送逻辑)
for i in range(7):
idx = i * step
# 构造包含四个通道当前数据点的消息
message = {
'sensor_data': {
'humidity': channel_data['channel1'][idx],
'temperature': channel_data['channel2'][idx],
'pressure': channel_data['channel3'][idx],
'light': channel_data['channel4'][idx]
},
'timestamp': timestamp,
'serial_number': serial_number,
'chunk_index': i,
'data_points': 1
}
try:
await websocket.send(json.dumps(message))
print(f"发送第 {i+1}/7 组数据到实验 {experiment_id}")
print(f"时间戳: {message['timestamp']}")
print(f"数据: humidity={message['sensor_data']['humidity']}, "
f"temperature={message['sensor_data']['temperature']}, "
f"pressure={message['sensor_data']['pressure']}, "
f"light={message['sensor_data']['light']}")
await asyncio.sleep(0.01)
except websockets.exceptions.WebSocketException as e:
print(f"WebSocket发送错误: {e}")
if experiment_id in self.experiment_websockets:
del self.experiment_websockets[experiment_id]
break
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def decode_micropython_data(self, frame):
"""解析来自MicroPython的数据帧"""
try:
print(f"收到原始数据: 长度={len(frame)} bytes")
print(f"数据内容: {frame[:20]}...")
if len(frame) < 4:
print(f"数据长度过短: {len(frame)} bytes")
return None
expected_length = 504 # 2(timestamp) + 2(chunk_index) + 250*2(data)
if len(frame) != expected_length:
print(f"数据长度不正确: {len(frame)} bytes,期望 {expected_length} bytes")
if len(frame) >= 4:
try:
timestamp, chunk_index = struct.unpack('<HH', frame[:4])
print(f"帧头解析: timestamp={timestamp}, chunk_index={chunk_index}")
except Exception as e:
print(f"帧头解析失败: {e}")
return None
try:
# 解析数据
timestamp, chunk_index, *data = struct.unpack('<HH250H', frame)
# 将250个数据点分配给4个传感器
# 假设数据是按照传感器1、传感器2、传感器3、传感器4的顺序交错排列
sensor1_data = data[0::4] # 每4个点取第1个
sensor2_data = data[1::4] # 每4个点取第2个
sensor3_data = data[2::4] # 每4个点取第3个
sensor4_data = data[3::4] # 每4个点取第4个
# 处理所有数据点
channel_data = {
'channel1': sensor1_data,
'channel2': sensor2_data,
'channel3': sensor3_data,
'channel4': sensor4_data
}
# 使用每个传感器的第一个数据点作为当前值
sensor_data = {
'humidity': sensor1_data[0] if sensor1_data else 0,
'temperature': sensor2_data[0] if sensor2_data else 0,
'pressure': sensor3_data[0] if sensor3_data else 0,
'light': sensor4_data[0] if sensor4_data else 0
}
print(f"数据分布: 传感器1={len(sensor1_data)}点, 传感器2={len(sensor2_data)}点, "
f"传感器3={len(sensor3_data)}点, 传感器4={len(sensor4_data)}点")
return {
'timestamp': int(time.time() * 1000),
'chunk_index': chunk_index,
'sensor_data': sensor_data,
'channel_data': channel_data,
'data_points': len(data)
}
except Exception as e:
print(f"数据解析错误: {e}")
print(f"数据内容: {frame.hex()}")
return None
except Exception as e:
print(f"处理数据时出错: {e}")
import traceback
traceback.print_exc()
return None
async def handle_serial_number(self, client_socket: socket.socket, serial_number: str):
"""理客户端发送的序列号"""
try:
# 存储客户端序列号映射
self.client_serials[client_socket] = serial_number
print(f"收到客户端序列号: {serial_number}")
# 更新活跃实验映射
await self.update_active_experiments()
# 检查序列号是否匹配到实验
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]
print(f"序列号 {serial_number} 匹配到实验 {experiment_id}")
return True
else:
print(f"序列号 {serial_number} 未匹配到活跃实验")
return False
except Exception as e:
print(f"处理序列号错误: {e}")
return False
async def request_serial_number(self, client_socket: socket.socket):
"""请求客户端发送序列号"""
try:
request = {
"type": "request_serial",
"message": "请提供设��序列号"
}
frame = self._create_ws_frame(json.dumps(request).encode())
client_socket.send(frame)
except Exception as e:
print(f"请求列号误: {e}")
async def handle_client(self, client_socket, address):
"""处理客户端连接"""
print(f"新客户端连接: {address}")
serial_received = False
try:
# 设置socket为非阻塞模式
client_socket.setblocking(False)
# 处理WebSocket握手
data = await self.loop.sock_recv(client_socket, 1024)
data = data.decode('utf-8')
if "GET" in data:
key = ''
for line in data.split('\r\n'):
if "Sec-WebSocket-Key" in line:
key = line.split(': ')[1]
break
if key:
response_key = base64.b64encode(
hashlib.sha1(
(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
).digest()
).decode('utf-8')
response = (
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Accept: {response_key}\r\n\r\n"
)
await self.loop.sock_sendall(client_socket, response.encode())
print(f"WebSocket握手成功: {address}")
while True:
try:
frame = await self.loop.sock_recv(client_socket, 4096)
if not frame:
print(f"客户端断开连接: {address}")
break
# 处理数据帧
decoded_data = self.decode_websocket_frame(frame)
if not decoded_data:
continue
# 如果还没有收到序列号
if not serial_received:
try:
# 尝试将解码后的数据转换为字符串
serial_number = decoded_data.decode('utf-8')
print(f"收到序列号: {serial_number}")
serial_received = await self.handle_serial_number(client_socket, serial_number)
if not serial_received:
await self.request_serial_number(client_socket)
except UnicodeDecodeError:
# 如果无法解码为字符串,说明可能是数据帧
if len(decoded_data) == 504: # 检查是否是数据帧
serial_received = True # 假设之前已经收到过序列号
# 如果已经收到序列号,处理数据帧
if serial_received:
# 检查数据长度是否符合预期
if len(decoded_data) == 504:
parsed_data = await self.decode_micropython_data(decoded_data)
if parsed_data:
if client_socket in self.client_serials:
parsed_data['serial_number'] = self.client_serials[client_socket]
await self.process_and_forward_data(parsed_data)
else:
print(f"跳过非数据帧: 长度={len(decoded_data)}")
except (BlockingIOError, InterruptedError):
await asyncio.sleep(0.001)
continue
except Exception as e:
print(f"接收数据错误: {e}")
break
except Exception as e:
print(f"连接处理错误: {e}")
finally:
if client_socket in self.client_serials:
del self.client_serials[client_socket]
client_socket.close()
print(f"客户端连接关闭: {address}")
def decode_websocket_frame(self, frame):
"""解析WebSocket帧"""
try:
# 将新数据添加到缓冲区
self.frame_buffer.extend(frame)
# 如果缓冲区数据不足以构成完整帧头,等待更多数据
if len(self.frame_buffer) < 2:
return None
# 获取帧头信息
has_mask = self.frame_buffer[1] & 0x80
payload_length = self.frame_buffer[1] & 0x7f
# 计算帧头长度
header_length = 2
if payload_length == 126:
if len(self.frame_buffer) < 4:
return None
header_length += 2
payload_length = int.from_bytes(self.frame_buffer[2:4], byteorder='big')
elif payload_length == 127:
if len(self.frame_buffer) < 10:
return None
header_length += 8
payload_length = int.from_bytes(self.frame_buffer[2:10], byteorder='big')
# 如果有掩码,增加掩码长度
if has_mask:
header_length += 4
# 计算完整帧长度
total_length = header_length + payload_length
# 如果数据不完整,等待更多数据
if len(self.frame_buffer) < total_length:
return None
# 提取完整帧数据
frame_data = self.frame_buffer[:total_length]
# 更新缓冲区,移除已处理的数据
self.frame_buffer = self.frame_buffer[total_length:]
# 处理掩码
if has_mask:
mask = frame_data[header_length-4:header_length]
payload = bytearray()
data = frame_data[header_length:total_length]
for i in range(payload_length):
payload.append(data[i] ^ mask[i % 4])
return bytes(payload)
else:
return frame_data[header_length:total_length]
except Exception as e:
print(f"WebSocket帧解析错误: {e}")
import traceback
traceback.print_exc()
self.frame_buffer.clear() # 清空缓冲区
return None
async def cleanup(self):
"""清理资源"""
# 关闭所有WebSocket连接
for websocket in self.experiment_websockets.values():
await websocket.close()
# 关闭MongoDB连接
if self.db_client:
self.db_client.close()
def start(self):
"""启动服务器"""
try:
print("正在启动WebSocket服务器...")
print("等待客户端连接...")
# 创建事件循环
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
# 设置服务器socket为非阻塞模式
self.server.setblocking(False)
async def server_loop():
while True:
try:
client_socket, address = await self.loop.sock_accept(self.server)
print(f"接受新连接: {address}")
# 创建新的任务处理客户
self.loop.create_task(self.handle_client(client_socket, address))
except (BlockingIOError, InterruptedError):
await asyncio.sleep(0.1)
except Exception as e:
print(f"接受连接错误: {e}")
# 运行服务器循环
self.loop.run_until_complete(server_loop())
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
self.server.close()
if self.loop:
self.loop.close()
if __name__ == "__main__":
server = WebSocketServer()
server.start()
-406
View File
@@ -1,406 +0,0 @@
import socket
import threading
import base64
import hashlib
import asyncio
import websockets
import json
from datetime import datetime
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, Set
import redis
from redis.exceptions import ConnectionError
from redis.commands.json.path import Path
import numpy as np
import struct
import time
from redis import asyncio as aioredis
from bson.objectid import ObjectId
class WebSocketServer:
def __init__(self, host='0.0.0.0', port=6002):
self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.server.bind((host, port))
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, str] = {} # serial_number -> experiment_id
# 缓存每个实验的WebSocket连接
self.experiment_websockets: Dict[str, websockets.WebSocketClientProtocol] = {}
print(f"WebSocket服务器启动在 {host}:{port}")
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
# Redis连接
self.redis_client = None
# 添加客户端序列号映射
self.client_serials: Dict[socket.socket, str] = {} # socket -> serial_number
self.frame_buffer = bytearray() # 添加帧缓冲区
self.frame_length = 0 # 添加帧长度记录
async def init_redis(self):
"""初始化Redis异步连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
"redis://222.186.10.253:6379",
password='Obscura@2024',
db=199,
decode_responses=True
)
async def update_active_experiments(self):
"""更新活跃实验的设备映射"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
db = self.db_client["lab"]
try:
# 查找所有活跃的实验会话
active_sessions = db.experiment_sessions.find({"end_time": None})
# 清空当前映射
self.active_experiments.clear()
# 更新映射
async for session in active_sessions:
experiment_id = str(session["experiment_id"])
# 遍历会话中的所有设备
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
self.active_experiments[serial_number] = experiment_id
print(f"活跃实验映射已更新: {self.active_experiments}")
except Exception as e:
print(f"更新活跃实验时出错: {e}")
# 印更详细的错误信息
import traceback
traceback.print_exc()
async def get_experiment_websocket(self, experiment_id: str) -> websockets.WebSocketClientProtocol:
"""获取或创建到实验WebSocket端点的连接"""
try:
if experiment_id not in self.experiment_websockets or \
self.experiment_websockets[experiment_id].closed:
# 移除旧连接
if experiment_id in self.experiment_websockets:
del self.experiment_websockets[experiment_id]
# 创建新连接
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}"
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 每20秒发送一次ping
ping_timeout=10, # ping超时时间
close_timeout=10, # 关闭超时时间
max_size=None, # 不限制消息大小
extra_headers={
'Connection': 'keep-alive',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache'
}
)
self.experiment_websockets[experiment_id] = websocket
print(f"为实验 {experiment_id} 创建了新的WebSocket连")
except Exception as e:
print(f"创建WebSocket连接失败: {e}")
return None
return self.experiment_websockets[experiment_id]
except Exception as e:
print(f"获取WebSocket连接时出错: {e}")
return None
async def process_and_forward_data(self, data: dict):
"""处理并转发数据"""
try:
serial_number = data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
return
experiment_id = self.active_experiments[serial_number]
# 获取实验会话信息以匹配传感器
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
db = self.db_client["lab"]
# 查找对应的实验会话
session = await db.experiment_sessions.find_one({
"experiment_id": ObjectId(experiment_id),
"end_time": None
})
if not session:
print(f"未找到活跃的实验会话: {experiment_id}")
return
# 获取设备的传感器配置
device_config = next(
(device for device in session.get("devices", [])
if device.get("serial_number") == serial_number),
None
)
if not device_config:
print(f"未找到设备配置: {serial_number}")
return
# 创建channel到sensor_name的映射
sensor_mapping = {
sensor["index"]: sensor["sensor_name"]
for sensor in device_config.get("sensors", [])
}
# 获取WebSocket连接
websocket = await self.get_experiment_websocket(experiment_id)
if not websocket:
return
# 获取所有通道数据并按配置转换
channel_data = data.get('channel_data', {})
total_points = len(next(iter(channel_data.values())))
step = total_points // 7
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
# 存储数据到Redis DB199
try:
stream_key = f"experiment:{experiment_id}:{serial_number}"
timestamp = data.get('timestamp')
point_data = {
'channel_data': {
sensor_mapping.get(channel): channel_data[channel]
for channel in channel_data.keys()
if channel in sensor_mapping
},
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': total_points
}
# 使用异步方式存储数据
await self.redis_client.xadd(
stream_key,
{'data': json.dumps(point_data)}
)
except Exception as e:
print(f"Redis存储错误: {e}")
import traceback
traceback.print_exc()
# 发送数据到WebSocket时使用映射后的传感器名称
for i in range(7):
idx = i * step
# 使用sensor_mapping转换channel名称
message = {
'sensor_data': {
sensor_mapping.get(channel, channel): channel_data[channel][idx]
for channel in channel_data.keys()
if channel in sensor_mapping
},
'timestamp': data.get('timestamp'),
'serial_number': serial_number,
'chunk_index': i,
'data_points': 1
}
try:
await websocket.send(json.dumps(message))
print(f"发送第 {i+1}/7 组数据到实验 {experiment_id}")
print(f"时间戳: {message['timestamp']}")
print(f"数据: {message['sensor_data']}")
await asyncio.sleep(0.01)
except websockets.exceptions.WebSocketException as e:
print(f"WebSocket发送错误: {e}")
if experiment_id in self.experiment_websockets:
del self.experiment_websockets[experiment_id]
break
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def decode_micropython_data(self, frame):
"""解析来自MicroPython的数据帧"""
try:
print(f"收到原始数据: 长度={len(frame)} bytes")
print(f"数据内容: {frame[:20]}...")
if len(frame) < 4:
print(f"数据长度过短: {len(frame)} bytes")
return None
expected_length = 504 # 2(timestamp) + 2(chunk_index) + 250*2(data)
if len(frame) != expected_length:
print(f"数据长度不正确: {len(frame)} bytes,期望 {expected_length} bytes")
if len(frame) >= 4:
try:
timestamp, chunk_index = struct.unpack('<HH', frame[:4])
print(f"帧头解析: timestamp={timestamp}, chunk_index={chunk_index}")
except Exception as e:
print(f"帧头解析失败: {e}")
return None
try:
# 解析数据
timestamp, chunk_index, *data = struct.unpack('<HH250H', frame)
# 将250个数据点分配给4个传感器
# 假设数据是按照传感器1、传感器2、传感器3、传感器4的顺序交错排列
sensor1_data = data[0::4] # 每4个点取第1个
sensor2_data = data[1::4] # 每4个点取第2个
sensor3_data = data[2::4] # 每4个点取第3个
sensor4_data = data[3::4] # 每4个点取第4个
# 处理所有数据点
channel_data = {
'channel1': sensor1_data,
'channel2': sensor2_data,
'channel3': sensor3_data,
'channel4': sensor4_data
}
# 使用每个传感器的第一个数据点作为当前值
sensor_data = {
'humidity': sensor1_data[0] if sensor1_data else 0,
'temperature': sensor2_data[0] if sensor2_data else 0,
'pressure': sensor3_data[0] if sensor3_data else 0,
'light': sensor4_data[0] if sensor4_data else 0
}
print(f"数据分布: 传感器1={len(sensor1_data)}点, 传感器2={len(sensor2_data)}点, "
f"传感器3={len(sensor3_data)}点, 传感器4={len(sensor4_data)}点")
return {
'timestamp': int(time.time() * 1000),
'chunk_index': chunk_index,
'sensor_data': sensor_data,
'channel_data': channel_data,
'data_points': len(data)
}
except Exception as e:
print(f"数据解析错误: {e}")
print(f"数据内容: {frame.hex()}")
return None
except Exception as e:
print(f"处理数据时出错: {e}")
import traceback
traceback.print_exc()
return None
async def handle_serial_number(self, client_socket: socket.socket, serial_number: str):
"""理客户端发送的序列号"""
try:
# 存储客户端序列号映射
self.client_serials[client_socket] = serial_number
print(f"收到客户端序列号: {serial_number}")
# 更新活跃实验映射
await self.update_active_experiments()
# 检查序列号是否匹配到实验
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]
print(f"序列号 {serial_number} 匹配到实验 {experiment_id}")
return True
else:
print(f"序列号 {serial_number} 未匹配到活跃实验")
return False
except Exception as e:
print(f"处理序列号错误: {e}")
return False
async def request_serial_number(self, client_socket: socket.socket):
"""请求客户端发送序列号"""
try:
request = {
"type": "request_serial",
"message": "请提供设序列号"
}
frame = self._create_ws_frame(json.dumps(request).encode())
client_socket.send(frame)
except Exception as e:
print(f"请求列号误: {e}")
async def handle_client_data(self, data, addr):
"""处理客户端数据"""
try:
# 如果是序列号
try:
serial_number = data.decode('utf-8')
print(f"收到来自 {addr} 的序列号: {serial_number}")
# 更新活跃实验映射
await self.update_active_experiments()
return
except UnicodeDecodeError:
pass
# 如果是数据包
if len(data) == 504: # 检查数据长度
parsed_data = await self.decode_micropython_data(data)
if parsed_data:
# 获取该地址对应的序列号
serial_number = self.client_serials.get(addr)
if serial_number:
parsed_data['serial_number'] = serial_number
await self.process_and_forward_data(parsed_data)
except Exception as e:
print(f"处理客户端数据错误: {e}")
import traceback
traceback.print_exc()
def start(self):
"""启动服务器"""
try:
print("正在启动UDP服务器...")
print("等待客户端数据...")
# 创建事件循环
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
# 设置服务器socket为非阻塞模式
self.server.setblocking(False)
async def server_loop():
while True:
try:
data, addr = await self.loop.sock_recvfrom(self.server, 4096)
if data:
# 创建新的任务处理数据
self.loop.create_task(self.handle_client_data(data, addr))
except (BlockingIOError, InterruptedError):
await asyncio.sleep(0.1)
except Exception as e:
print(f"接收数据错误: {e}")
# 运行服务器循环
self.loop.run_until_complete(server_loop())
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
self.server.close()
if self.loop:
self.loop.close()
if __name__ == "__main__":
server = WebSocketServer()
server.start()
-652
View File
@@ -1,652 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket = None
self.experiment_id = None
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 获取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每���通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接已建立"""
try:
# 检查序列号是否还有活跃实验
await self.update_active_experiments()
if serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 已不存在于活跃实验,不再重WebSocket")
if self.websocket:
await self.close_websocket()
return False
# 获取最新的experiment_id
current_experiment_id = self.active_experiments[serial_number]["experiment_id"]
if current_experiment_id != experiment_id:
print(f"实验ID已更改: {experiment_id} -> {current_experiment_id}")
experiment_id = current_experiment_id
# 检查现有连接是否可用
if (self.websocket and
self.experiment_id == experiment_id and
not self.websocket.closed):
return True
# 使用和冷却时间控制重连
async with self.reconnect_lock:
current_time = time.time()
if current_time - self.last_connect_attempt < self.RECONNECT_COOLDOWN:
print("重连冷却中...")
return False
if self.reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
print("达到最大重试次数,等待冷却...")
await asyncio.sleep(self.RECONNECT_COOLDOWN)
self.reconnect_attempts = 0
return False
# 更新最后连接尝试时间
self.last_connect_attempt = current_time
self.reconnect_attempts += 1
# 确保关闭旧连接
await self.close_websocket()
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}"
print(f"尝试建立WebSocket连接: {uri} (尝试次数: {self.reconnect_attempts})")
try:
self.websocket = await websockets.connect(
uri,
ping_interval=None,
close_timeout=5
)
self.experiment_id = experiment_id
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await self.websocket.send(json.dumps(init_message))
response = await self.websocket.recv()
print(f"连接成功,服务器响应: {response}")
# 重置重连计数器
self.reconnect_attempts = 0
return True
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
import traceback
traceback.print_exc()
return False
async def close_websocket(self):
"""安全关闭WebSocket连接"""
if self.websocket:
try:
await self.websocket.close()
except Exception:
pass
self.websocket = None
self.experiment_id = None
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
# 确保WebSocket连接已建立
if not await self.ensure_websocket_connected(experiment_id, serial_number):
print("WebSocket连接未就,跳过数据发送")
return
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# 构造Redis stream key
stream_key = f"experiment:{experiment_id}:{serial_number}"
# 构造完整的数据点,只包含已配置的传感器
channel_data = {}
for channel_num, sensor_name in sensors_map.items():
if channel_num < len(channels):
channel_data[sensor_name] = channels[channel_num]
point_data = {
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
}
# 存储到Redis Stream
try:
await self.redis_client.xadd(
stream_key,
{'data': json.dumps(point_data)}
)
except Exception as e:
print(f"Redis存储错误: {e}")
import traceback
traceback.print_exc()
# 逐条发送数据到WebSocket
downsample_rate = 5
# 添加连接计数器
connection_attempts = 0
max_attempts = 3 # 最大重试次数
for i in range(0, parsed_data['sample_count'], downsample_rate):
sensor_data = {}
for channel_num, sensor_name in sensors_map.items():
if channel_num < len(channels):
sensor_data[sensor_name] = channels[channel_num][i]
# 使用正确的时间间隔计算时间戳
point_timestamp = timestamp + (i * self.SAMPLE_INTERVAL_MS)
message = {
'sensor_data': sensor_data,
'timestamp': point_timestamp,
'serial_number': serial_number,
'chunk_index': i,
'data_points': 1
}
try:
# 添加超时控制
await asyncio.wait_for(
self.websocket.send(json.dumps(message)),
timeout=5.0
)
await asyncio.sleep(0.001)
except (websockets.exceptions.WebSocketException, asyncio.TimeoutError) as e:
print(f"发送数据失败: {e}")
connection_attempts += 1
if connection_attempts > max_attempts:
print("达到最大重试次数,停止重连")
break
# 确保旧连接被关闭
if self.websocket:
try:
await self.websocket.close()
except Exception:
pass
self.websocket = None
# 重新建立连接
try:
await self.ensure_websocket_connected(
self.experiment_id,
parsed_data['serial_number']
)
if not self.websocket:
print("重连失败,停止处理")
break
except Exception as conn_err:
print(f"重连失败: {conn_err}")
break
# print(f"\n成功发送所有 {parsed_data['sample_count'] // downsample_rate} 个数据点")
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收到的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis连接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭 WebSocket 连接
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭 WebSocket
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-652
View File
@@ -1,652 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket = None
self.experiment_id = None
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 获取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每���通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接已建立"""
try:
# 检查序列号是否还有活跃实验
await self.update_active_experiments()
if serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 已不存在于活跃实验,不再重WebSocket")
if self.websocket:
await self.close_websocket()
return False
# 获取最新的experiment_id
current_experiment_id = self.active_experiments[serial_number]["experiment_id"]
if current_experiment_id != experiment_id:
print(f"实验ID已更改: {experiment_id} -> {current_experiment_id}")
experiment_id = current_experiment_id
# 检查现有连接是否可用
if (self.websocket and
self.experiment_id == experiment_id and
not self.websocket.closed):
return True
# 使用和冷却时间控制重连
async with self.reconnect_lock:
current_time = time.time()
if current_time - self.last_connect_attempt < self.RECONNECT_COOLDOWN:
print("重连冷却中...")
return False
if self.reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
print("达到最大重试次数,等待冷却...")
await asyncio.sleep(self.RECONNECT_COOLDOWN)
self.reconnect_attempts = 0
return False
# 更新最后连接尝试时间
self.last_connect_attempt = current_time
self.reconnect_attempts += 1
# 确保关闭旧连接
await self.close_websocket()
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}"
print(f"尝试建立WebSocket连接: {uri} (尝试次数: {self.reconnect_attempts})")
try:
self.websocket = await websockets.connect(
uri,
ping_interval=None,
close_timeout=5
)
self.experiment_id = experiment_id
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await self.websocket.send(json.dumps(init_message))
response = await self.websocket.recv()
print(f"连接成功,服务器响应: {response}")
# 重置重连计数器
self.reconnect_attempts = 0
return True
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
import traceback
traceback.print_exc()
return False
async def close_websocket(self):
"""安全关闭WebSocket连接"""
if self.websocket:
try:
await self.websocket.close()
except Exception:
pass
self.websocket = None
self.experiment_id = None
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
# 确保WebSocket连接已建立
if not await self.ensure_websocket_connected(experiment_id, serial_number):
print("WebSocket连接未就,跳过数据发送")
return
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# 构造Redis stream key
stream_key = f"experiment:{experiment_id}:{serial_number}"
# 构造完整的数据点,只包含已配置的传感器
channel_data = {}
for channel_num, sensor_name in sensors_map.items():
if channel_num < len(channels):
channel_data[sensor_name] = channels[channel_num]
point_data = {
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
}
# 存储到Redis Stream
try:
await self.redis_client.xadd(
stream_key,
{'data': json.dumps(point_data)}
)
except Exception as e:
print(f"Redis存储错误: {e}")
import traceback
traceback.print_exc()
# 逐条发送数据到WebSocket
downsample_rate = 5
# 添加连接计数器
connection_attempts = 0
max_attempts = 3 # 最大重试次数
for i in range(0, parsed_data['sample_count'], downsample_rate):
sensor_data = {}
for channel_num, sensor_name in sensors_map.items():
if channel_num < len(channels):
sensor_data[sensor_name] = channels[channel_num][i]
# 使用正确的时间间隔计算时间戳
point_timestamp = timestamp + (i * self.SAMPLE_INTERVAL_MS)
message = {
'sensor_data': sensor_data,
'timestamp': point_timestamp,
'serial_number': serial_number,
'chunk_index': i,
'data_points': 1
}
try:
# 添加超时控制
await asyncio.wait_for(
self.websocket.send(json.dumps(message)),
timeout=5.0
)
await asyncio.sleep(0.001)
except (websockets.exceptions.WebSocketException, asyncio.TimeoutError) as e:
print(f"发送数据失败: {e}")
connection_attempts += 1
if connection_attempts > max_attempts:
print("达到最大重试次数,停止重连")
break
# 确保旧连接被关闭
if self.websocket:
try:
await self.websocket.close()
except Exception:
pass
self.websocket = None
# 重新建立连接
try:
await self.ensure_websocket_connected(
self.experiment_id,
parsed_data['serial_number']
)
if not self.websocket:
print("重连失败,停止处理")
break
except Exception as conn_err:
print(f"重连失败: {conn_err}")
break
# print(f"\n成功发送所有 {parsed_data['sample_count'] // downsample_rate} 个数据点")
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收到的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis连接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭 WebSocket 连接
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭 WebSocket
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-638
View File
@@ -1,638 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, List
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket: Dict[str, websockets.WebSocketClientProtocol] = {} # serial_number -> WebSocketClientProtocol
self.experiment_id: Dict[str, str] = {} # serial_number -> experiment_id
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
# 添加Redis批量处理
self.redis_batch_size = 100
self.redis_batches: Dict[str, List] = {}
self.redis_batch_locks: Dict[str, asyncio.Lock] = {}
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定���实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 取通道配���
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每个通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接保持活跃"""
try:
# 检查现有连接是否可用
if (serial_number in self.websocket and
self.experiment_id.get(serial_number) == experiment_id and
not self.websocket[serial_number].closed):
return True
async with self.reconnect_lock:
# 关闭旧连接
await self.close_websocket(serial_number)
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}/{serial_number}"
print(f"建立新的WebSocket连接: {uri}")
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 添加心跳机制
ping_timeout=10,
close_timeout=5
)
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await websocket.send(json.dumps(init_message))
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "success":
self.websocket[serial_number] = websocket
self.experiment_id[serial_number] = experiment_id
print(f"WebSocket连接成功建立: {serial_number}")
return True
await websocket.close()
return False
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
return False
async def close_websocket(self, serial_number):
"""安全关闭特定设备的WebSocket连接"""
if serial_number in self.websocket:
try:
await self.websocket[serial_number].close()
except Exception:
pass
del self.websocket[serial_number]
del self.experiment_id[serial_number]
async def batch_redis_write(self, stream_key: str, data_point: dict):
"""批量写入Redis"""
if stream_key not in self.redis_batches:
self.redis_batches[stream_key] = []
self.redis_batch_locks[stream_key] = asyncio.Lock()
async with self.redis_batch_locks[stream_key]:
self.redis_batches[stream_key].append(data_point)
if len(self.redis_batches[stream_key]) >= self.redis_batch_size:
batch = self.redis_batches[stream_key]
self.redis_batches[stream_key] = []
# 使用pipeline批量写入
pipe = self.redis_client.pipeline()
for point in batch:
pipe.xadd(stream_key, point)
await pipe.execute()
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
# print(f"处理数据,序列号: {serial_number}")
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# Redis stream 处理
stream_key = f"experiment:{experiment_id}:{serial_number}"
channel_data = {
sensor_name: channels[channel_num]
for channel_num, sensor_name in sensors_map.items()
if channel_num < len(channels)
}
# Redis批量写入
await self.batch_redis_write(stream_key, {
'data': json.dumps({
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
})
})
# WebSocket发送任务 - 降采样处理
websocket = self.websocket.get(serial_number)
if not websocket or websocket.closed:
print(f"重新建立WebSocket连接: {serial_number}")
connected = await self.ensure_websocket_connected(experiment_id, serial_number)
if connected:
websocket = self.websocket.get(serial_number)
else:
print(f"无法建立WebSocket连接: {serial_number}")
return
try:
# 对每个通道进行降采样
downsampled_data = {}
# 优化降采样策略
data_length = len(next(iter(channel_data.values())))
DOWNSAMPLE_RATE = max(5, data_length // 1000) # 动态调整降采样率
# 使用numpy进行高效降采样
import numpy as np
for sensor_name, data in channel_data.items():
data_array = np.array(data)
downsampled_data[sensor_name] = data_array[::DOWNSAMPLE_RATE].tolist()
# 计算降采样后的时间戳
base_timestamp = timestamp
sample_interval = self.SAMPLE_INTERVAL_MS * DOWNSAMPLE_RATE
# 单次发送数据
if websocket and not websocket.closed:
for i in range(len(next(iter(downsampled_data.values())))):
current_timestamp = base_timestamp + (i * sample_interval)
# 构建单个数据点
point_data = {
sensor_name: values[i]
for sensor_name, values in downsampled_data.items()
if i < len(values)
}
# 发送单个数据点
message = {
'type': 'data',
'sensor_data': point_data,
'timestamp': current_timestamp,
'serial_number': serial_number,
'data_points': 1
}
await websocket.send(json.dumps(message))
# 可选:添加小延迟以防止发送过快
await asyncio.sleep(0.001) # 1ms延迟
except websockets.exceptions.ConnectionClosed:
print(f"发送数据时连接断开,将在下次发送时重连: {serial_number}")
await self.close_websocket(serial_number)
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭 WebSocket 连接
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭 WebSocket
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-638
View File
@@ -1,638 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, List
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket: Dict[str, websockets.WebSocketClientProtocol] = {} # serial_number -> WebSocketClientProtocol
self.experiment_id: Dict[str, str] = {} # serial_number -> experiment_id
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
# 添加Redis批量处理
self.redis_batch_size = 100
self.redis_batches: Dict[str, List] = {}
self.redis_batch_locks: Dict[str, asyncio.Lock] = {}
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每个通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接保持活跃"""
try:
# 检查现有连接是否可用
if (serial_number in self.websocket and
self.experiment_id.get(serial_number) == experiment_id and
not self.websocket[serial_number].closed):
return True
async with self.reconnect_lock:
# 关闭旧连接
await self.close_websocket(serial_number)
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}/{serial_number}"
print(f"建立新的WebSocket连接: {uri}")
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 添加心跳机制
ping_timeout=10,
close_timeout=5
)
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await websocket.send(json.dumps(init_message))
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "success":
self.websocket[serial_number] = websocket
self.experiment_id[serial_number] = experiment_id
print(f"WebSocket连接成功建立: {serial_number}")
return True
await websocket.close()
return False
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
return False
async def close_websocket(self, serial_number):
"""安全关闭特定设备的WebSocket连接"""
if serial_number in self.websocket:
try:
await self.websocket[serial_number].close()
except Exception:
pass
del self.websocket[serial_number]
del self.experiment_id[serial_number]
async def batch_redis_write(self, stream_key: str, data_point: dict):
"""批量写入Redis"""
if stream_key not in self.redis_batches:
self.redis_batches[stream_key] = []
self.redis_batch_locks[stream_key] = asyncio.Lock()
async with self.redis_batch_locks[stream_key]:
self.redis_batches[stream_key].append(data_point)
if len(self.redis_batches[stream_key]) >= self.redis_batch_size:
batch = self.redis_batches[stream_key]
self.redis_batches[stream_key] = []
# 使用pipeline批量写入
pipe = self.redis_client.pipeline()
for point in batch:
pipe.xadd(stream_key, point)
await pipe.execute()
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# Redis stream 处理
stream_key = f"experiment:{experiment_id}:{serial_number}"
channel_data = {
sensor_name: channels[channel_num]
for channel_num, sensor_name in sensors_map.items()
if channel_num < len(channels)
}
# Redis批量写入
await self.batch_redis_write(stream_key, {
'data': json.dumps({
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
})
})
# WebSocket处理
websocket = self.websocket.get(serial_number)
if not websocket or websocket.closed:
print(f"重新建立WebSocket连接: {serial_number}")
connected = await self.ensure_websocket_connected(experiment_id, serial_number)
if connected:
websocket = self.websocket.get(serial_number)
else:
print(f"无法建立WebSocket连接: {serial_number}")
return
try:
# 降采样处理
data_length = len(next(iter(channel_data.values())))
DOWNSAMPLE_RATE = max(5, data_length // 1000) # 动态调整降采样率
# 使用numpy进行高效降采样
import numpy as np
downsampled_data = {}
for sensor_name, data in channel_data.items():
data_array = np.array(data)
downsampled_data[sensor_name] = data_array[::DOWNSAMPLE_RATE].tolist()
# 计算时间戳序列
base_timestamp = timestamp
sample_interval = self.SAMPLE_INTERVAL_MS * DOWNSAMPLE_RATE
timestamps = [
base_timestamp + (i * sample_interval)
for i in range(len(next(iter(downsampled_data.values()))))
]
# 构建批量数据点
data_points = []
for i in range(len(timestamps)):
point_data = {
sensor_name: values[i]
for sensor_name, values in downsampled_data.items()
if i < len(values)
}
data_points.append({
'sensor_data': point_data,
'timestamp': timestamps[i]
})
# 批量发送数据
if websocket and not websocket.closed:
message = {
'type': 'data_batch',
'data_points': data_points,
'serial_number': serial_number,
'total_points': len(data_points)
}
# print(f"发送数据: {message}")
await websocket.send(json.dumps(message))
except websockets.exceptions.ConnectionClosed:
print(f"发送数据时连接断开,将在下次发送时重连: {serial_number}")
await self.close_websocket(serial_number)
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭 WebSocket 连接
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭 WebSocket
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-638
View File
@@ -1,638 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, List
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket: Dict[str, websockets.WebSocketClientProtocol] = {} # serial_number -> WebSocketClientProtocol
self.experiment_id: Dict[str, str] = {} # serial_number -> experiment_id
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
# 添加Redis批量处理
self.redis_batch_size = 100
self.redis_batches: Dict[str, List] = {}
self.redis_batch_locks: Dict[str, asyncio.Lock] = {}
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每个通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接保持活跃"""
try:
# 检查现有连接是否可用
if (serial_number in self.websocket and
self.experiment_id.get(serial_number) == experiment_id and
not self.websocket[serial_number].closed):
return True
async with self.reconnect_lock:
# 关闭旧连接
await self.close_websocket(serial_number)
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}/{serial_number}"
print(f"建立新的WebSocket连接: {uri}")
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 添加心跳机制
ping_timeout=10,
close_timeout=5
)
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await websocket.send(json.dumps(init_message))
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "success":
self.websocket[serial_number] = websocket
self.experiment_id[serial_number] = experiment_id
print(f"WebSocket连接成功建立: {serial_number}")
return True
await websocket.close()
return False
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
return False
async def close_websocket(self, serial_number):
"""安全关闭特定设备的WebSocket连接"""
if serial_number in self.websocket:
try:
await self.websocket[serial_number].close()
except Exception:
pass
del self.websocket[serial_number]
del self.experiment_id[serial_number]
async def batch_redis_write(self, stream_key: str, data_point: dict):
"""批量写入Redis"""
if stream_key not in self.redis_batches:
self.redis_batches[stream_key] = []
self.redis_batch_locks[stream_key] = asyncio.Lock()
async with self.redis_batch_locks[stream_key]:
self.redis_batches[stream_key].append(data_point)
if len(self.redis_batches[stream_key]) >= self.redis_batch_size:
batch = self.redis_batches[stream_key]
self.redis_batches[stream_key] = []
# 使用pipeline批量写入
pipe = self.redis_client.pipeline()
for point in batch:
pipe.xadd(stream_key, point)
await pipe.execute()
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# Redis stream 处理
stream_key = f"experiment:{experiment_id}:{serial_number}"
channel_data = {
sensor_name: channels[channel_num]
for channel_num, sensor_name in sensors_map.items()
if channel_num < len(channels)
}
# Redis批量写入
await self.batch_redis_write(stream_key, {
'data': json.dumps({
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
})
})
# WebSocket处理
websocket = self.websocket.get(serial_number)
if not websocket or websocket.closed:
print(f"重新建立WebSocket连接: {serial_number}")
connected = await self.ensure_websocket_connected(experiment_id, serial_number)
if connected:
websocket = self.websocket.get(serial_number)
else:
print(f"无法建立WebSocket连接: {serial_number}")
return
try:
# 降采样处理
data_length = len(next(iter(channel_data.values())))
DOWNSAMPLE_RATE = max(5, data_length // 1000) # 动态调整降采样率
# 使用numpy进行高效降采样
import numpy as np
downsampled_data = {}
for sensor_name, data in channel_data.items():
data_array = np.array(data)
downsampled_data[sensor_name] = data_array[::DOWNSAMPLE_RATE].tolist()
# 计算时间戳序列
base_timestamp = timestamp
sample_interval = self.SAMPLE_INTERVAL_MS * DOWNSAMPLE_RATE
timestamps = [
base_timestamp + (i * sample_interval)
for i in range(len(next(iter(downsampled_data.values()))))
]
# 构建批量数据点
data_points = []
for i in range(len(timestamps)):
point_data = {
sensor_name: values[i]
for sensor_name, values in downsampled_data.items()
if i < len(values)
}
data_points.append({
'sensor_data': point_data,
'timestamp': timestamps[i]
})
# 批量发送数据
if websocket and not websocket.closed:
message = {
'type': 'data_batch',
'data_points': data_points,
'serial_number': serial_number,
'total_points': len(data_points)
}
# print(f"发送数据: {message}")
await websocket.send(json.dumps(message))
except websockets.exceptions.ConnectionClosed:
print(f"发送数据时连接断开,将在下次发送时重连: {serial_number}")
await self.close_websocket(serial_number)
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭 WebSocket 连接
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭 WebSocket
if self.websocket:
await self.close_websocket()
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-685
View File
@@ -1,685 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, List
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket: Dict[str, websockets.WebSocketClientProtocol] = {} # serial_number -> WebSocketClientProtocol
self.experiment_id: Dict[str, str] = {} # serial_number -> experiment_id
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
# 添加Redis批量处理
self.redis_batch_size = 100
self.redis_batches: Dict[str, List] = {}
self.redis_batch_locks: Dict[str, asyncio.Lock] = {}
# 添加实验状态缓存
self.experiment_status: Dict[str, Dict[str, str]] = {} # experiment_id -> {serial_number: status}
self.status_check_interval = 5 # 每5秒检查一次状态
# 添加状态消息缓存
self.status_message_cache: Dict[str, bool] = {} # experiment_id:serial_number -> has_printed
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每个通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接保持活跃"""
try:
# 检查现有连接是否可用
if (serial_number in self.websocket and
self.experiment_id.get(serial_number) == experiment_id and
not self.websocket[serial_number].closed):
return True
async with self.reconnect_lock:
# 关闭旧连接
await self.close_websocket(serial_number)
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}/{serial_number}"
print(f"建立新的WebSocket连接: {uri}")
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 添加心跳机制
ping_timeout=10,
close_timeout=5
)
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await websocket.send(json.dumps(init_message))
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "success":
self.websocket[serial_number] = websocket
self.experiment_id[serial_number] = experiment_id
print(f"WebSocket连接成功建立: {serial_number}")
return True
await websocket.close()
return False
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
return False
async def close_websocket(self, serial_number):
"""安全关闭特定设备的WebSocket连接"""
if serial_number in self.websocket:
try:
await self.websocket[serial_number].close()
except Exception:
pass
del self.websocket[serial_number]
del self.experiment_id[serial_number]
async def batch_redis_write(self, stream_key: str, data_point: dict):
"""批量写入Redis"""
if stream_key not in self.redis_batches:
self.redis_batches[stream_key] = []
self.redis_batch_locks[stream_key] = asyncio.Lock()
async with self.redis_batch_locks[stream_key]:
self.redis_batches[stream_key].append(data_point)
if len(self.redis_batches[stream_key]) >= self.redis_batch_size:
batch = self.redis_batches[stream_key]
self.redis_batches[stream_key] = []
# 使用pipeline批量写入
pipe = self.redis_client.pipeline()
for point in batch:
pipe.xadd(stream_key, point)
await pipe.execute()
async def check_experiment_status(self, experiment_id: str, serial_number: str) -> bool:
"""检查实验状态"""
try:
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
# 切换到db199
await self.redis_client.select(199)
status_key = f"experiment_status:{experiment_id}:{serial_number}"
status = await self.redis_client.get(status_key)
# 切回db200(用于其他操作)
await self.redis_client.select(200)
return status == "active"
except Exception as e:
print(f"检查实验状态时出错: {e}")
return False
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# Redis stream 处理 - 无论实验状态如何都保存数据
stream_key = f"experiment:{experiment_id}:{serial_number}"
channel_data = {
sensor_name: channels[channel_num]
for channel_num, sensor_name in sensors_map.items()
if channel_num < len(channels)
}
# Redis批量写入
await self.batch_redis_write(stream_key, {
'data': json.dumps({
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
})
})
# 检查实验是否处于活跃状态
is_active = await self.check_experiment_status(experiment_id, serial_number)
if not is_active:
# 使用组合键检查是否已经打印过停止消息
cache_key = f"{experiment_id}:{serial_number}"
if not self.status_message_cache.get(cache_key):
print(f"实验ID: {experiment_id}, 设备: {serial_number} 已停止,仅保存数据")
self.status_message_cache[cache_key] = True
return
else:
# 如果实验重新激活,清除缓存
cache_key = f"{experiment_id}:{serial_number}"
self.status_message_cache.pop(cache_key, None)
# 以下是WebSocket相关操作,只在实验活跃时执行
websocket = self.websocket.get(serial_number)
if not websocket or websocket.closed:
print(f"重新建立WebSocket连接: {serial_number}")
connected = await self.ensure_websocket_connected(experiment_id, serial_number)
if connected:
websocket = self.websocket.get(serial_number)
else:
print(f"无法建立WebSocket连接: {serial_number}")
return
try:
# 降采样处理
data_length = len(next(iter(channel_data.values())))
DOWNSAMPLE_RATE = max(5, data_length // 1000) # 动态调整降采样率
# 使用numpy进行高效降采样
import numpy as np
downsampled_data = {}
for sensor_name, data in channel_data.items():
data_array = np.array(data)
downsampled_data[sensor_name] = data_array[::DOWNSAMPLE_RATE].tolist()
# 计算时间戳序列
base_timestamp = timestamp
sample_interval = self.SAMPLE_INTERVAL_MS * DOWNSAMPLE_RATE
timestamps = [
base_timestamp + (i * sample_interval)
for i in range(len(next(iter(downsampled_data.values()))))
]
# 构建批量数据点
data_points = []
for i in range(len(timestamps)):
point_data = {
sensor_name: values[i]
for sensor_name, values in downsampled_data.items()
if i < len(values)
}
data_points.append({
'sensor_data': point_data,
'timestamp': timestamps[i]
})
# 批量发送数据
if websocket and not websocket.closed:
message = {
'type': 'data_batch',
'data_points': data_points,
'serial_number': serial_number,
'total_points': len(data_points)
}
# print(f"发送数据: {message}")
await websocket.send(json.dumps(message))
except websockets.exceptions.ConnectionClosed:
print(f"发送数据时连接断开,将在下次发送时重连: {serial_number}")
await self.close_websocket(serial_number)
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭所有 WebSocket 连接
if self.websocket:
# 创建所有序列号的副本,因为在关闭过程中字典会被修改
serial_numbers = list(self.websocket.keys())
for serial_number in serial_numbers:
await self.close_websocket(serial_number)
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭所有 WebSocket 连接
if self.websocket:
serial_numbers = list(self.websocket.keys())
for serial_number in serial_numbers:
await self.close_websocket(serial_number)
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-685
View File
@@ -1,685 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict, List
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
import signal
class UDPServer(asyncio.DatagramProtocol):
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
self.active_experiments_lock = asyncio.Lock() # 添加锁来保护活跃实验映射
# WebSocket连接
self.websocket: Dict[str, websockets.WebSocketClientProtocol] = {} # serial_number -> WebSocketClientProtocol
self.experiment_id: Dict[str, str] = {} # serial_number -> experiment_id
self.reconnect_lock = asyncio.Lock() # 添加重连锁
self.last_connect_attempt = 0 # 添加最后连接尝试时间
self.RECONNECT_COOLDOWN = 30 # 重连冷却时间(秒)
self.MAX_RECONNECT_ATTEMPTS = 3 # 最大重连次数
self.reconnect_attempts = 0 # 重连计数器
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 5 # 每5秒检查一次
# 添加时间戳相关属性
self.last_timestamp = None
self.timestamp_offset = 0
# 添加实验更新相关的属性
self.last_experiment_update = 0
self.EXPERIMENT_UPDATE_INTERVAL = 1 # 实验更新间隔(秒)
self.SAMPLE_INTERVAL_MS = 1 # 修改为1毫秒,而不是之前的4毫秒
self._shutdown = False
self._shutdown_event = asyncio.Event()
# 添加定时查询相关的属性
self.experiment_check_task = None
# 添加Redis批量处理
self.redis_batch_size = 100
self.redis_batches: Dict[str, List] = {}
self.redis_batch_locks: Dict[str, asyncio.Lock] = {}
# 添加实验状态缓存
self.experiment_status: Dict[str, Dict[str, str]] = {} # experiment_id -> {serial_number: status}
self.status_check_interval = 5 # 每5秒检查一次状态
# 添加状态消息缓存
self.status_message_cache: Dict[str, bool] = {} # experiment_id:serial_number -> has_printed
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""增量更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
async with self.active_experiments_lock:
# print("开始查询活跃实验...")
# 查找所有活跃的实验会话
cursor = db.experiment_sessions.find({"end_time": None})
count = await db.experiment_sessions.count_documents({"end_time": None})
# print(f"找到 {count} 个活跃实验会话")
# 保留当前的映射
current_mapping = self.active_experiments.copy()
# 增量更新映射
async for session in cursor:
experiment_id = str(session["experiment_id"])
# print(f"处理实验会话: {experiment_id}")
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# print(f"检查设备: {serial_number}")
# 构建传感器映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1
sensors_map[channel_num] = sensor.get("sensor_name")
# 更新映射
current_mapping[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
# print(f"更新设备映射: {serial_number} -> {experiment_id}")
# 检查映射是否有变化
if current_mapping != self.active_experiments:
# print(f"活跃实验映射已更新: {current_mapping}")
self.active_experiments = current_mapping
return True
else:
# print("没有发现新的实验映射")
return False
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
return False
def connection_made(self, transport):
self.transport = transport
print(f"UDP服务器启动在 {self.server_address[0]}:{self.server_address[1]}")
# 启动定时查询任务
self.experiment_check_task = asyncio.create_task(self.periodic_experiment_check())
async def periodic_experiment_check(self):
"""定期检查活跃实验"""
while not self._shutdown:
try:
# print("执行定期实验检查...")
await self.update_active_experiments()
await asyncio.sleep(self.EXPERIMENT_CHECK_INTERVAL)
except Exception as e:
print(f"定期检查实验时出错: {e}")
await asyncio.sleep(1) # 出错时短暂等待后重试
def datagram_received(self, data, addr):
"""处理接收到的UDP数据"""
try:
# 检查是否是序列号
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text):
print(f"检测到序列号: {text}")
asyncio.create_task(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据
task = asyncio.create_task(self.handle_sensor_data(data, addr))
# 添加任务完成回调
task.add_done_callback(self._handle_task_result)
except Exception as e:
print(f"数据处理错误: {e}")
def _handle_task_result(self, task):
"""处理任务完成的回调"""
try:
# 获取任务结果,如果有异常会在这里抛出
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常的
except Exception as e:
print(f"任务执行出错: {e}")
import traceback
traceback.print_exc()
def request_serial_number(self, addr):
"""向设备请求序列号"""
try:
# 修改为与 boot.py 中匹配的请求命令
self.transport.sendto(b'REQUEST_SERIAL', addr)
except Exception as e:
print(f"请求序列号失败: {e}")
async def decode_micropython_data(self, data, addr):
try:
# 包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 获取设备的序列号
serial_number = self.device_serials.get(addr)
if not serial_number:
print(f"收到未知设备数据,请求序列号: {addr}")
self.request_serial_number(addr)
return None
# 使用锁保护读取操作
async with self.active_experiments_lock:
experiment_info = self.active_experiments.get(serial_number)
if not experiment_info:
# print(f"设备 {serial_number} 未找到实验,等待下次检查...")
return None
# 取通道配置
sensors_map = experiment_info.get("sensors_map", {})
if not sensors_map:
print(f"设备 {serial_number} 的传感器映射为空")
return None
num_channels = len(sensors_map)
if num_channels == 0:
print(f"设备 {serial_number} 的通道配置为空")
return None
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值,不再使用0值作为停止条件
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
sensor_data.append(value)
# 检查数据长度是否是通道数的整数倍
if len(sensor_data) % num_channels != 0:
print(f"设备 {serial_number} 的数据长度 {len(sensor_data)} 不是通道数 {num_channels} 的整数倍")
return None
# 将数据重组为对应数的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
# 检查每个通道是否都有数据
if not all(len(ch) > 0 for ch in channels):
print(f"设备 {serial_number} 的某些通道没有数据")
return None
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0]),
'serial_number': serial_number
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接保持活跃"""
try:
# 检查现有连接是否可用
if (serial_number in self.websocket and
self.experiment_id.get(serial_number) == experiment_id and
not self.websocket[serial_number].closed):
return True
async with self.reconnect_lock:
# 关闭旧连接
await self.close_websocket(serial_number)
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}/{serial_number}"
print(f"建立新的WebSocket连接: {uri}")
try:
websocket = await websockets.connect(
uri,
ping_interval=20, # 添加心跳机制
ping_timeout=10,
close_timeout=5
)
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await websocket.send(json.dumps(init_message))
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "success":
self.websocket[serial_number] = websocket
self.experiment_id[serial_number] = experiment_id
print(f"WebSocket连接成功建立: {serial_number}")
return True
await websocket.close()
return False
except Exception as e:
print(f"WebSocket连接失败: {e}")
return False
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
return False
async def close_websocket(self, serial_number):
"""安全关闭特定设备的WebSocket连接"""
if serial_number in self.websocket:
try:
await self.websocket[serial_number].close()
except Exception:
pass
del self.websocket[serial_number]
del self.experiment_id[serial_number]
async def batch_redis_write(self, stream_key: str, data_point: dict):
"""批量写入Redis"""
if stream_key not in self.redis_batches:
self.redis_batches[stream_key] = []
self.redis_batch_locks[stream_key] = asyncio.Lock()
async with self.redis_batch_locks[stream_key]:
self.redis_batches[stream_key].append(data_point)
if len(self.redis_batches[stream_key]) >= self.redis_batch_size:
batch = self.redis_batches[stream_key]
self.redis_batches[stream_key] = []
# 使用pipeline批量写入
pipe = self.redis_client.pipeline()
for point in batch:
pipe.xadd(stream_key, point)
await pipe.execute()
async def check_experiment_status(self, experiment_id: str, serial_number: str) -> bool:
"""检查实验状态"""
try:
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
# 切换到db199
await self.redis_client.select(199)
status_key = f"experiment_status:{experiment_id}:{serial_number}"
status = await self.redis_client.get(status_key)
# 切回db200(用于其他操作)
await self.redis_client.select(200)
return status == "active"
except Exception as e:
print(f"检查实验状态时出错: {e}")
return False
async def process_and_forward_data(self, parsed_data):
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# Redis stream 处理 - 无论实验状态如何都保存数据
stream_key = f"experiment:{experiment_id}:{serial_number}"
channel_data = {
sensor_name: channels[channel_num]
for channel_num, sensor_name in sensors_map.items()
if channel_num < len(channels)
}
# Redis批量写入
await self.batch_redis_write(stream_key, {
'data': json.dumps({
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
})
})
# 检查实验是否处于活跃状态
is_active = await self.check_experiment_status(experiment_id, serial_number)
if not is_active:
# 使用组合键检查是否已经打印过停止消息
cache_key = f"{experiment_id}:{serial_number}"
if not self.status_message_cache.get(cache_key):
print(f"实验ID: {experiment_id}, 设备: {serial_number} 已停止,仅保存数据")
self.status_message_cache[cache_key] = True
return
else:
# 如果实验重新激活,清除缓存
cache_key = f"{experiment_id}:{serial_number}"
self.status_message_cache.pop(cache_key, None)
# 以下是WebSocket相关操作,只在实验活跃时执行
websocket = self.websocket.get(serial_number)
if not websocket or websocket.closed:
print(f"重新建立WebSocket连接: {serial_number}")
connected = await self.ensure_websocket_connected(experiment_id, serial_number)
if connected:
websocket = self.websocket.get(serial_number)
else:
print(f"无法建立WebSocket连接: {serial_number}")
return
try:
# 降采样处理
data_length = len(next(iter(channel_data.values())))
DOWNSAMPLE_RATE = max(5, data_length // 1000) # 动态调整降采样率
# 使用numpy进行高效降采样
import numpy as np
downsampled_data = {}
for sensor_name, data in channel_data.items():
data_array = np.array(data)
downsampled_data[sensor_name] = data_array[::DOWNSAMPLE_RATE].tolist()
# 计算时间戳序列
base_timestamp = timestamp
sample_interval = self.SAMPLE_INTERVAL_MS * DOWNSAMPLE_RATE
timestamps = [
base_timestamp + (i * sample_interval)
for i in range(len(next(iter(downsampled_data.values()))))
]
# 构建批量数据点
data_points = []
for i in range(len(timestamps)):
point_data = {
sensor_name: values[i]
for sensor_name, values in downsampled_data.items()
if i < len(values)
}
data_points.append({
'sensor_data': point_data,
'timestamp': timestamps[i]
})
# 批量发送数据
if websocket and not websocket.closed:
message = {
'type': 'data_batch',
'data_points': data_points,
'serial_number': serial_number,
'total_points': len(data_points)
}
# print(f"发送数据: {message}")
await websocket.send(json.dumps(message))
except websockets.exceptions.ConnectionClosed:
print(f"发送数据时连接断开,将在下次发送时重连: {serial_number}")
await self.close_websocket(serial_number)
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收的序列号"""
try:
# 检查是否是的序列号
is_new_serial = addr not in self.device_serials or self.device_serials[addr] != serial_number
# 更新设备地址映射
self.device_serials[addr] = serial_number
self.serial_request_intervals.pop(addr, None)
# 只在新序列号且未匹配到实验时才更新
if is_new_serial and serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未匹配到实验,正在更新...")
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 已匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未找到匹配的活跃实验")
except Exception as e:
print(f"处理序列号错误: {e}")
async def handle_sensor_data(self, data, addr):
"""处理传感器数据"""
try:
# 解析数据
parsed_data = await self.decode_micropython_data(data, addr)
if not parsed_data:
return
# 处理和转发数据
await self.process_and_forward_data(parsed_data)
except asyncio.CancelledError:
# 优雅地处理取消
print("传感器数据处理被取消")
raise
except Exception as e:
print(f"处理传感器数据时出错: {e}")
import traceback
traceback.print_exc()
finally:
# 确保在任务结束时进行清理
pass
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
async def close(self):
"""关闭服务器并清理资源"""
try:
# 关闭所有 WebSocket 连接
if self.websocket:
# 创建所有序列号的副本,因为在关闭过程中字典会被修改
serial_numbers = list(self.websocket.keys())
for serial_number in serial_numbers:
await self.close_websocket(serial_number)
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
async def shutdown(self):
"""优雅关闭服务器"""
if self._shutdown:
return
print("\n正在关闭服务器...")
self._shutdown = True
# 取消定时查询任务
if self.experiment_check_task:
self.experiment_check_task.cancel()
try:
await self.experiment_check_task
except asyncio.CancelledError:
pass
# 关闭所有连接和资源
try:
# 关闭所有 WebSocket 连接
if self.websocket:
serial_numbers = list(self.websocket.keys())
for serial_number in serial_numbers:
await self.close_websocket(serial_number)
# 关闭数据库连接
if self.db_client:
self.db_client.close()
# 关闭 Redis 连接
if self.redis_client:
await self.redis_client.close()
# 关闭传输层
if self.transport:
self.transport.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
# 确保事件循环停止
loop = asyncio.get_event_loop()
loop.stop()
async def main():
"""主函数"""
server = UDPServer()
loop = asyncio.get_event_loop()
def signal_handler():
"""信号处理函数"""
print("\n收到终止信号")
asyncio.create_task(server.shutdown())
# 注册信号处理器
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)
try:
# 创建服务器
transport, _ = await loop.create_datagram_endpoint(
lambda: server,
local_addr=server.server_address
)
# 等待关闭信号
await server._shutdown_event.wait()
except Exception as e:
print(f"服务器运行出错: {e}")
finally:
# 确保清理资源
await server.shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
finally:
print("程序已退出")
-437
View File
@@ -1,437 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Monitor</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.chart-wrapper {
margin-bottom: 20px;
}
.chart-container {
height: 300px;
position: relative;
}
.custom-data-panel {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.custom-data-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.custom-data-item {
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 基础字体设置 */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #333;
line-height: 1.6;
}
/* 设备信息样式 */
.device-name {
font-size: 2rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.device-serial {
font-size: 1rem;
color: #7f8c8d;
font-weight: 400;
}
/* 数据面板样式 */
.custom-data-panel h5 {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 1rem;
}
.custom-data-item {
background: linear-gradient(to bottom right, #ffffff, #f8f9fa);
border: 1px solid #e9ecef;
}
.custom-data-name {
font-size: 0.875rem;
color: #6c757d;
font-weight: 500;
margin-bottom: 0.25rem;
}
.custom-data-value {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
}
.custom-data-unit {
font-size: 0.75rem;
color: #6c757d;
margin-left: 0.25rem;
}
/* 图表标题样式 */
.card-title {
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
}
.text-muted {
font-size: 0.875rem;
color: #6c757d !important;
}
/* 卡片样式优化 */
.card {
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
}
/* 响应式字体大小 */
@media (max-width: 768px) {
.device-name {
font-size: 1.5rem;
}
.custom-data-value {
font-size: 1rem;
}
.card-title {
font-size: 1rem;
}
}
</style>
</head>
<body>
<div class="container mt-4">
<div id="deviceInfo" class="mb-4">
<h3 class="device-name"></h3>
<p class="device-serial"></p>
</div>
<div id="customDataPanel" class="custom-data-panel">
<h5>Data</h5>
<div class="custom-data-list"></div>
</div>
<div id="chartsContainer"></div>
</div>
<script>
const API_BASE_URL = 'https://dev.obscura.work/lab';
const maxDataPoints = 50;
let charts = {};
let chartDatasets = {};
let formulaConfigs = [];
// 从URL获取参数
const urlParams = new URLSearchParams(window.location.search);
const experimentId = urlParams.get('experimentId');
const serialNumber = urlParams.get('serialNumber');
if (!experimentId || !serialNumber) {
document.body.innerHTML = '<div class="container mt-4"><div class="alert alert-danger">Invalid link</div></div>';
} else {
initDeviceMonitor();
}
async function initDeviceMonitor() {
try {
// 获取设备信息
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices/public`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
const devices = await response.json();
const device = devices.find(d => d.serial_number === serialNumber);
if (!device) {
throw new Error('Device not found');
}
// 更新设备信息显示
document.querySelector('.device-name').textContent = device.device_name;
document.querySelector('.device-serial').textContent = `Serial Number: ${device.serial_number}`;
// // 创建设备映射
deviceNameMap = devices.reduce((acc, device) => {
acc[device.serial_number] = device.device_name;
return acc;
}, {});
// 设置公式配置
formulaConfigs = device.sensors.reduce((acc, sensor) => {
if (sensor.formulas) {
sensor.formulas.forEach(formula => {
acc.push({
...formula,
sensorName: sensor.sensor_name
});
});
}
return acc;
}, []);
// 连接WebSocket
connectWebSocket();
}
} catch (error) {
console.error('Failed to initialize device monitor:', error);
document.body.innerHTML = `
<div class="container mt-4">
<div class="alert alert-danger">Failed to initialize device monitor: ${error.message}</div>
</div>
`;
}
}
// 统一的图表配置
function getChartConfig(deviceName, sensorName, dataset) {
return {
type: 'line',
data: {
labels: dataset.labels,
datasets: [{
label: `${deviceName} - ${sensorName}`,
data: dataset.values,
borderColor: '#6366F1',
borderWidth: 3,
fill: false,
tension: 0.3,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false
},
x: {
grid: {
display: false
}
}
},
animation: {
duration: 0
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
}
}
}
};
}
// 统一的数据集初始化
function initChartDataset(value) {
return {
labels: Array(maxDataPoints).fill(''),
values: Array(maxDataPoints).fill(null),
historicalMin: value,
historicalMax: value
};
}
// 创建图表容器
function createChartContainer(deviceName, sensorName, chartId) {
const chartWrapper = document.createElement('div');
chartWrapper.className = 'chart-wrapper mb-4';
chartWrapper.innerHTML = `
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="card-title mb-0">Device: ${deviceName}</h5>
<small class="text-muted">Sensor: ${sensorName}</small>
</div>
</div>
<div class="chart-container">
<canvas id="chart_${chartId}"></canvas>
</div>
</div>
</div>
`;
return chartWrapper;
}
// 更新图表数据
function updateChartData(chartId, value) {
const dataset = chartDatasets[chartId];
dataset.values.shift();
dataset.values.push(value);
// 更新历史极值
if (dataset.historicalMin === undefined || dataset.historicalMax === undefined) {
dataset.historicalMin = value;
dataset.historicalMax = value;
} else {
dataset.historicalMin = Math.min(dataset.historicalMin, value);
dataset.historicalMax = Math.max(dataset.historicalMax, value);
}
// 更新图表显示
const range = dataset.historicalMax - dataset.historicalMin;
const padding = range * 0.1;
const chart = charts[chartId];
chart.data.datasets[0].data = dataset.values;
chart.options.scales.y.min = dataset.historicalMin - padding;
chart.options.scales.y.max = dataset.historicalMax + padding;
chart.update('none');
}
// 处理自定义数据显示
function updateCustomData(deviceName, sensorName, value, formula) {
try {
const result = eval(`const value = ${value}; ${formula.formula}`);
const elementId = `formula-${deviceName}-${sensorName}-${formula.data_name}`;
const customDataList = document.querySelector('#customDataPanel .custom-data-list');
let resultElement = document.getElementById(elementId);
if (!resultElement) {
resultElement = document.createElement('div');
resultElement.className = 'custom-data-item';
resultElement.id = elementId;
resultElement.style.width = '150px'; // 设置固定宽度
resultElement.innerHTML = `
<div class="custom-data-name" style="font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${deviceName} - ${formula.data_name}
</div>
<div class="custom-data-value">
${Number(result).toFixed(2)}
<span class="custom-data-unit" style="font-size: 0.8em;">${formula.data_unit || ''}</span>
</div>
`;
customDataList.appendChild(resultElement);
} else {
const valueElement = resultElement.querySelector('.custom-data-value');
valueElement.innerHTML = `
${Number(result).toFixed(2)}
<span class="custom-data-unit" style="font-size: 0.8em;">${formula.data_unit || ''}</span>
`;
}
} catch (error) {
console.error(`Calculation formula error ${formula.data_name}:`, error);
}
}
let deviceNameMap = {};
let wsConnections = {};
// WebSocket连接和数据处理相关函数
function connectWebSocket() {
const wsUrl = `wss://dev.obscura.work/lab/ws/${experimentId}/${serialNumber}`;
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === "ping") {
ws.send(JSON.stringify({
type: "pong",
timestamp: Date.now()
}));
return;
}
// 处理数据批次
if (data.type === "data_batch" && data.data_points) {
const deviceId = data.serial_number;
const deviceName = deviceNameMap[deviceId] || deviceId;
// 按时间戳排序
const sortedDataPoints = data.data_points.sort((a, b) => a.timestamp - b.timestamp);
// 获取所有传感器名称
const sensorNames = new Set();
sortedDataPoints.forEach(point => {
Object.keys(point.sensor_data).forEach(name => sensorNames.add(name));
});
// 处理每个传感器数据
sensorNames.forEach(sensorName => {
const chartId = `${deviceId}_${sensorName}`;
// 创建或获取图表
if (!charts[chartId]) {
const container = createChartContainer(deviceName, sensorName, chartId);
document.getElementById('chartsContainer').appendChild(container);
const firstValue = sortedDataPoints[0].sensor_data[sensorName];
chartDatasets[chartId] = initChartDataset(firstValue);
const canvas = document.getElementById(`chart_${chartId}`);
charts[chartId] = new Chart(
canvas.getContext('2d'),
getChartConfig(deviceName, sensorName, chartDatasets[chartId])
);
}
// 更新数据
sortedDataPoints.forEach(point => {
const value = point.sensor_data[sensorName];
if (value !== undefined) {
updateChartData(chartId, value);
// 更新自定义数据
formulaConfigs.forEach(formula => {
if (formula.sensorName === sensorName) {
updateCustomData(deviceName, sensorName, value, formula);
}
});
}
});
});
}
} catch (error) {
console.error('Failed to process WebSocket message:', error);
}
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-292
View File
@@ -1,292 +0,0 @@
# Device routes
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional
from motor.motor_asyncio import AsyncIOMotorClient
from bson import ObjectId
from pydantic import BaseModel, Field
from contextlib import asynccontextmanager
from datetime import datetime
import uuid
# Database Configuration
MONGODB_URL = "mongodb://lab:[email protected]:27017/lab"
# 更新 FastAPI 实例化
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行
await connect_to_mongo()
yield
# 关闭时执行
await close_mongo_connection()
app = FastAPI(lifespan=lifespan)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# MongoDB setup
class PyObjectId(ObjectId):
"""
自定义ObjectId类,用于在Pydantic模型中处理MongoDB的ObjectId
"""
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v, handler):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
@classmethod
def __get_pydantic_json_schema__(cls, _schema_cache, **_kwargs):
return {
'type': 'string',
'description': 'ObjectId',
'pattern': r'^[0-9a-fA-F]{24}$'
}
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(
type='string',
description='ObjectId',
pattern=r'^[0-9a-fA-F]{24}$'
)
class SensorModel(BaseModel):
"""传感器模型"""
index: str
sensor_name: str
sensor_type: str
unit: str
class DeviceModel(BaseModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None)
device_name: str
device_type: str
device_number: int
serial_numbers: list = Field(default_factory=list)
sensors: list[SensorModel] = Field(default_factory=list)
class Config:
populate_by_name = True
arbitrary_types_allowed = True
json_encoders = {ObjectId: str}
def dict(self, *args, **kwargs):
"""确保返回的字典包含 _id 字段"""
kwargs["by_alias"] = True
return super().dict(*args, **kwargs)
# Database connection
class Database:
"""数据库连接管理类"""
client: AsyncIOMotorClient = None
db = Database()
async def get_database():
"""获取数据库连接"""
if db.client is None:
await connect_to_mongo()
return db.client["lab"]
async def connect_to_mongo():
try:
db.client = AsyncIOMotorClient(MONGODB_URL)
# 验证连接
await db.client.admin.command('ping')
print("Successfully connected to MongoDB")
except Exception as e:
print(f"Could not connect to MongoDB: {e}")
raise
async def close_mongo_connection():
db.client.close()
@app.post("/devices/devices")
async def create_device(device: DeviceModel):
"""
创建新的设备
参数:
device: 设备模型,包含设备信息和传感器列表
返回:
dict: 包含创建成功消息和新设备ID
"""
db = await get_database()
# 验证传感器数量是否与通道数匹配
if len(device.sensors) != device.device_number:
raise HTTPException(
status_code=400,
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
)
device_dict = device.model_dump(exclude={"id"})
result = await db.devices.insert_one(device_dict)
return {
"message": "设备创建成功",
"id": str(result.inserted_id)
}
@app.get("/devices/devices")
async def get_devices():
"""
获取所有设备列表
返回:
List[dict]: 设备列表,包含所有设备的信息
"""
db = await get_database()
devices = []
async for device in db.devices.find():
# 确保 _id 被正确序列化
device["_id"] = str(device["_id"])
devices.append(device)
return devices
# 添加删除路由
@app.delete("/devices/devices/{device_id}")
async def delete_device(device_id: str):
"""
删除指定的设备
参数:
device_id: 设备的ObjectId字符串
返回:
dict: 包含删除操作的结果信息
"""
try:
db = await get_database()
result = await db.devices.delete_one({"_id": ObjectId(device_id)})
if result.deleted_count == 0:
return {"message": "未找到指定设备"}
return {"message": "设备删除成功"}
except Exception as e:
return {"message": f"删除设备失败: {str(e)}"}
# 添加生成序列号的函数
def generate_serial_number(device_type, batch_number, device_id):
"""生成序列号
格式: XX00-UUID(10位)-设备ID后6位
"""
# 获取前缀(设备类型前两位大写)
prefix = f"{device_type[:2].upper()}{batch_number:02d}"
# 生成UUID并取前10位
uuid_part = str(uuid.uuid4()).replace('-', '')[:10]
# 获取设备ID的后6位作为校验码
checksum = str(device_id)[-6:]
return f"{prefix}-{uuid_part}-{checksum}"
# 添加生成序列号的路由
@app.post("/devices/devices/{device_id}/serials")
async def generate_device_serials(device_id: str):
"""为设备生成10个新的序列号"""
try:
db = await get_database()
# 获取设备信息
device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not device:
return {"message": "设备不存在"}
# 获取当前序列号数量作为批次号
current_batch = len(device.get("serial_numbers", [])) // 10 + 1
# 生成10个新序列号
new_serials = []
for _ in range(10):
serial = generate_serial_number(
device["device_type"],
current_batch,
device_id # 传入设备ID
)
new_serials.append({
"serial": serial,
"status": "available",
"created_at": datetime.now().isoformat()
})
# 更新设备记录
result = await db.devices.update_one(
{"_id": ObjectId(device_id)},
{"$push": {"serial_numbers": {"$each": new_serials}}}
)
return {
"message": "序列号生成成功",
"serials": new_serials
}
except Exception as e:
return {"message": f"生成序列号失败: {str(e)}"}
# 添加获取序列号的路由
@app.get("/devices/devices/{device_id}/serials")
async def get_device_serials(device_id: str):
"""获取设备的所有序列号"""
try:
db = await get_database()
device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not device:
return {"message": "设备不存在"}
return {
"device_id": str(device["_id"]),
"device_name": device["device_name"],
"serial_numbers": device.get("serial_numbers", [])
}
except Exception as e:
return {"message": f"获取序列号失败: {str(e)}"}
# 添加更新设备的路由
@app.put("/devices/devices/{device_id}")
async def update_device(device_id: str, device: DeviceModel):
"""更新设备信息"""
try:
db = await get_database()
# 验证传感器数量
if len(device.sensors) != device.device_number:
raise HTTPException(
status_code=400,
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
)
# 更新设备信息
update_data = device.model_dump(exclude={"id"})
result = await db.devices.update_one(
{"_id": ObjectId(device_id)},
{"$set": update_data}
)
if result.modified_count == 0:
raise HTTPException(status_code=404, detail="设备不存在")
return {"message": "设备更新成功"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=6001)
File diff suppressed because it is too large Load Diff
-430
View File
@@ -1,430 +0,0 @@
# Device routes
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional
from motor.motor_asyncio import AsyncIOMotorClient
from bson import ObjectId
from pydantic import BaseModel, Field
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
import uuid
from redis import asyncio as aioredis
import asyncio
# Database Configuration
MONGODB_URL = "mongodb://lab:[email protected]:27017/lab"
# 更新 FastAPI 实例化
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行
await connect_to_mongo()
await init_redis()
yield
# 关闭时执行
await close_mongo_connection()
app = FastAPI(lifespan=lifespan)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# MongoDB setup
class PyObjectId(ObjectId):
"""
自定义ObjectId类,用于在Pydantic模型中处理MongoDB的ObjectId
"""
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v, handler):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
@classmethod
def __get_pydantic_json_schema__(cls, _schema_cache, **_kwargs):
return {
'type': 'string',
'description': 'ObjectId',
'pattern': r'^[0-9a-fA-F]{24}$'
}
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(
type='string',
description='ObjectId',
pattern=r'^[0-9a-fA-F]{24}$'
)
class SensorModel(BaseModel):
"""传感器模型"""
index: str
sensor_name: str
sensor_type: str
unit: str
class DeviceModel(BaseModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None)
device_name: str
device_type: str
device_number: int
serial_numbers: list = Field(default_factory=list)
sensors: list[SensorModel] = Field(default_factory=list)
class Config:
populate_by_name = True
arbitrary_types_allowed = True
json_encoders = {ObjectId: str}
def dict(self, *args, **kwargs):
"""确保返回的字典包含 _id 字段"""
kwargs["by_alias"] = True
return super().dict(*args, **kwargs)
# Database connection
class Database:
"""数据库连接管理类"""
client: AsyncIOMotorClient = None
db = Database()
async def get_database():
"""获取数据库连接"""
if db.client is None:
await connect_to_mongo()
return db.client["lab"]
async def connect_to_mongo():
try:
db.client = AsyncIOMotorClient(MONGODB_URL)
# 验证连接
await db.client.admin.command('ping')
print("Successfully connected to MongoDB")
except Exception as e:
print(f"Could not connect to MongoDB: {e}")
raise
async def close_mongo_connection():
db.client.close()
@app.post("/devices/devices")
async def create_device(device: DeviceModel):
"""
创建新的设备
参数:
device: 设备模型,包含设备信息和传感器列表
返回:
dict: 包含创建成功消息和新设备ID
"""
db = await get_database()
# 验证传感器数量是否与通道数匹配
if len(device.sensors) != device.device_number:
raise HTTPException(
status_code=400,
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
)
device_dict = device.model_dump(exclude={"id"})
result = await db.devices.insert_one(device_dict)
return {
"message": "设备创建成功",
"id": str(result.inserted_id)
}
@app.get("/devices/devices")
async def get_devices():
"""
获取所有设备列表
返回:
List[dict]: 设备列表,包含所有设备的信息
"""
db = await get_database()
devices = []
async for device in db.devices.find():
# 确保 _id 被正确序列化
device["_id"] = str(device["_id"])
devices.append(device)
return devices
# 添加删除路由
@app.delete("/devices/devices/{device_id}")
async def delete_device(device_id: str):
"""
删除指定的设备
参数:
device_id: 设备的ObjectId字符串
返回:
dict: 包含删除操作的结果信息
"""
try:
db = await get_database()
result = await db.devices.delete_one({"_id": ObjectId(device_id)})
if result.deleted_count == 0:
return {"message": "未找到指定设备"}
return {"message": "设备删除成功"}
except Exception as e:
return {"message": f"删除设备失败: {str(e)}"}
# 添加生成序列号的函数
def generate_serial_number(device_type, batch_number, device_id):
"""生成序列号
格式: XX00-UUID(10位)-设备ID后6位
"""
# 获取前缀(设备类型前两位大写)
prefix = f"{device_type[:2].upper()}{batch_number:02d}"
# 生成UUID并取前10位
uuid_part = str(uuid.uuid4()).replace('-', '')[:10]
# 获取设备ID的后6位作为校验码
checksum = str(device_id)[-6:]
return f"{prefix}-{uuid_part}-{checksum}"
# 添加生成序列号的路由
@app.post("/devices/devices/{device_id}/serials")
async def generate_device_serials(device_id: str):
"""为设备生成10个新的序列号"""
try:
db = await get_database()
# 获取设备信息
device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not device:
return {"message": "设备不存在"}
# 获取当前序列号数量作为批次号
current_batch = len(device.get("serial_numbers", [])) // 10 + 1
# 生成10个新序列号
new_serials = []
for _ in range(10):
serial = generate_serial_number(
device["device_type"],
current_batch,
device_id # 传入设备ID
)
new_serials.append({
"serial": serial,
"status": "available",
"created_at": datetime.now().isoformat()
})
# 更新设备记录
result = await db.devices.update_one(
{"_id": ObjectId(device_id)},
{"$push": {"serial_numbers": {"$each": new_serials}}}
)
return {
"message": "序列号生成成功",
"serials": new_serials
}
except Exception as e:
return {"message": f"生成序列号失败: {str(e)}"}
# 添加获取序列号的路由
@app.get("/devices/devices/{device_id}/serials")
async def get_device_serials(device_id: str):
"""获取设备的所有序列号"""
try:
db = await get_database()
device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not device:
return {"message": "设备不存在"}
return {
"device_id": str(device["_id"]),
"device_name": device["device_name"],
"serial_numbers": device.get("serial_numbers", [])
}
except Exception as e:
return {"message": f"获取序列号失败: {str(e)}"}
# 添加更新设备的路由
@app.put("/devices/devices/{device_id}")
async def update_device(device_id: str, device: DeviceModel):
"""更新设备信息"""
try:
db = await get_database()
# 首先获取现有设备数据,特别是serial_numbers
existing_device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not existing_device:
raise HTTPException(status_code=404, detail="设备不存在")
# 验证传感器数量
if len(device.sensors) != device.device_number:
raise HTTPException(
status_code=400,
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
)
# 只更新允许修改的字段,保留原有的serial_numbers
update_data = {
"device_name": device.device_name,
"device_type": device.device_type,
"device_number": device.device_number,
"sensors": [sensor.dict() for sensor in device.sensors],
"serial_numbers": existing_device.get("serial_numbers", []) # 保留原有的序列号
}
result = await db.devices.update_one(
{"_id": ObjectId(device_id)},
{"$set": update_data}
)
return {"message": "设备更新成功"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# 添加Redis连接配置
REDIS_URL = "redis://:Obscura@[email protected]:6379"
redis_client = None
# 用于存储上一次检查时的stream长度
last_stream_lengths = {}
# 在全局变量中添加设备活跃时间记录
device_last_active = {}
# 初始化Redis连接
async def init_redis():
global redis_client
if redis_client is None:
redis_client = await aioredis.from_url(REDIS_URL, db=200)
return redis_client
@app.get("/devices/devices/online")
async def get_online_devices():
"""获取在线设备列表"""
try:
redis = await init_redis()
db = await get_database()
keys = [key.decode('utf-8') for key in await redis.keys("experiment:*")]
online_devices = {}
current_time = datetime.now()
# 用于临时存储序列号的活跃状态
serial_active_status = {}
stream_lengths = {} # 添加存储stream长度的字典
print("\n=== 检查 Stream 活跃状态 ===")
# 检查所有streams
for key in keys:
try:
serial_number = key.split(":")[-1]
current_length = await redis.xlen(key)
last_length = last_stream_lengths.get(key, 0)
# 记录当前stream长度
stream_lengths[key] = {
'current': current_length,
'last': last_length
}
# 如果该序列号还未被标记为活跃,且当前stream有更新
if not serial_active_status.get(serial_number, False) and current_length != last_length:
serial_active_status[serial_number] = True
device_last_active[serial_number] = current_time
last_stream_lengths[key] = current_length
except Exception as e:
print(f"处理key {key} 时出错: {str(e)}")
continue
# 处理在线设备
for serial_number, is_active in serial_active_status.items():
last_active_time = device_last_active.get(serial_number)
# 如果设备在过去5分钟内有活动,则认为它在线
if last_active_time and (current_time - last_active_time) <= timedelta(minutes=5):
device = await db.devices.find_one({
"serial_numbers.serial": serial_number
})
if device:
online_devices[serial_number] = {
"device_id": str(device["_id"]),
"device_name": device["device_name"],
"device_type": device["device_type"],
"device_number": device["device_number"],
"serial_number": serial_number,
"last_active": last_active_time.isoformat(),
"sensors": device["sensors"],
"status": "active" if is_active else "idle",
"stream_info": { # 添加stream信息
k: v for k, v in stream_lengths.items()
if k.endswith(serial_number)
}
}
print("\n=== 在线设备统计 ===")
print(f"在线设备数量: {len(online_devices)}")
for device in online_devices.values():
print(f"\n设备: {device['device_name']} ({device['serial_number']})")
print(f" - 状态: {device['status']}")
print(f" - 最后活跃: {device['last_active']}")
print(" - Stream信息:")
for stream_key, info in device['stream_info'].items():
print(f" * {stream_key}: 当前={info['current']}, 上次={info['last']}")
response_data = {
"online_count": len(online_devices),
"devices": list(online_devices.values())
}
return response_data
except Exception as e:
print(f"获取在线设备出错: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取在线设备失败: {str(e)}")
# 修改清理函数
async def cleanup_old_records():
while True:
current_time = datetime.now()
# 清理stream长度记录
keys_to_remove = []
for key in last_stream_lengths.keys():
if current_time - last_stream_lengths.get(key, {}).get('last_check', current_time) > timedelta(minutes=10):
keys_to_remove.append(key)
for key in keys_to_remove:
last_stream_lengths.pop(key, None)
# 清理过期的设备活跃记录
serials_to_remove = []
for serial, last_active in device_last_active.items():
if current_time - last_active > timedelta(minutes=10):
serials_to_remove.append(serial)
for serial in serials_to_remove:
device_last_active.pop(serial, None)
await asyncio.sleep(300) # 每5分钟清理一次
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=6001)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-60
View File
@@ -1,60 +0,0 @@
# Lab 项目文档
## 项目结构
### 后端代码
- `boot.py` - 采集器代码
- 负责数据采集
- 将采集的数据发送给 data.py
- `data.py` - 数据处理代码
- 接收来自 boot.py 的数据
- 处理数据
- 将处理后的数据发送给 main.py
- `main.py` - 服务端代码
- 处理前端请求
- 管理数据库操作
- 提供 API 接口
- `device.py` - 设备注册代码
- 处理设备注册逻辑
- 管理设备信息
### Web界面
- lab.html: 主界面
- device.html: 设备管理界面
- device-monitor.html: 单个设备数据监控界面
## 系统流程
1. 数据采集流程:
- boot.py 采集数据
- 数据传输至 data.py
- data.py 处理数据
- 处理后的数据发送至 main.py
2. 设备管理流程:
- 通过 device.html 注册设备
- device.py 处理注册请求
- 设备信息存储到数据库
3. 用户操作流程:
- 用户通过 lab.html 进行项目管理
- 用户通过 device.html 注册设备
- main.py 处理用户请求
- 返回处理结果
## 开发说明
请确保按照以下顺序启动服务:
1. 启动数据库服务
2. 运行 main.py
3. 运行 boot.py
4. 运行 data.py
## 技术栈
- 后端:Python
- 前端:HTML, JavaScript
- 数据库:MongoDB
-209
View File
@@ -1,209 +0,0 @@
# main.py
import network
import socket
import struct
from machine import Timer, ADC, Pin
import array
import gc
import time
import random
# 配置信息
SSID = "Obscura"
PASSWORD = "Obscura2024"
UDP_HOST = "222.186.10.253"
UDP_PORT = 6002
SERIAL_NUMBER = "AD01-b088e8e9d3-64f663"
class HighSpeedCollector:
def __init__(self):
# 增加缓冲区大小以测试性能极限
self.buffers = [
array.array('H', [0] * 1000), # 增加到1000
array.array('H', [0] * 1000) # 增加到1000
]
self.current_buffer = 0
self.buffer_index = 0
self.buffer_ready = False
# UDP相关
self.sock = None
self.is_connected = False
self.sample_timer = Timer(0)
self.running = False
# 初始化器,添加合适的衰减和位宽配置
self.adc1 = ADC(Pin(0))
self.adc1.atten(ADC.ATTN_11DB) # 设置衰减为11dB
self.adc1.width(ADC.WIDTH_12BIT) # 设置12位分辨率
self.adc2 = ADC(Pin(1))
self.adc2.atten(ADC.ATTN_11DB)
self.adc2.width(ADC.WIDTH_12BIT)
self.adc3 = ADC(Pin(2))
self.adc3.atten(ADC.ATTN_11DB)
self.adc3.width(ADC.WIDTH_12BIT)
self.adc4 = ADC(Pin(3))
self.adc4.atten(ADC.ATTN_11DB)
self.adc4.width(ADC.WIDTH_12BIT)
self.qps = 0 # 新增QPS计数器
self.packets_this_second = 0 # 每秒发送的数据包数
def connect_wifi(self):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
gc.collect()
time.sleep(1)
def setup_udp(self):
"""初始化UDP连接"""
try:
gc.collect()
if self.sock:
try:
self.sock.close()
except:
pass
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置非阻塞模式
self.sock.setblocking(False)
# 首次连接时发送序列号
self.sock.sendto(SERIAL_NUMBER.encode(), (UDP_HOST, UDP_PORT))
print(f"首次连接,发送序列号: {SERIAL_NUMBER}")
self.is_connected = True
except Exception as e:
print("UDP初始化失败:", str(e))
self.is_connected = False
self.sock = None
def _sample(self, _):
"""采集传感器数据"""
if self.buffer_index >= 250: # 修改为250 (1000/4)
return
# 读取4个ADC通道的数据
pos = self.buffer_index * 4
self.buffers[self.current_buffer][pos] = random.randint(2000, 3500)
self.buffers[self.current_buffer][pos + 1] = random.randint(2500, 3000)
self.buffers[self.current_buffer][pos + 2] = random.randint(2000, 3000)
self.buffers[self.current_buffer][pos + 3] = random.randint(1000, 2000)
self.buffer_index += 1
if self.buffer_index >= 250:
self.buffer_ready = True
self.current_buffer = 1 - self.current_buffer
self.buffer_index = 0
def _send_data(self, _):
"""通过UDP发送数据"""
if not self.is_connected:
print("UDP未连接,尝试重新连接...")
self.setup_udp()
if not self.is_connected:
print("UDP重连失败")
return
try:
# 检查是否有请求序列号的消息
try:
data, addr = self.sock.recvfrom(32) # 非阻塞接收
if data == b'REQUEST_SERIAL':
print(f"收到序列号请求,发送序列号: {SERIAL_NUMBER}")
self.sock.sendto(SERIAL_NUMBER.encode(), addr)
return
except:
pass # 没有收到数据继续正常发送
# 如果buffer没有准备好,直接返回
if not self.buffer_ready:
return
buffer_to_send = self.buffers[1 - self.current_buffer]
current_time = time.ticks_ms()
packed_data = struct.pack(
'<HH250H',
current_time & 0xFFFF,
0,
*buffer_to_send[:250]
)
self.sock.sendto(packed_data, (UDP_HOST, UDP_PORT))
print(f"发送数据包成功 - 时间戳: {current_time}, 数据点数: {len(buffer_to_send[:250])}")
self.buffer_ready = False
self.packets_this_second += 1
except Exception as e:
print('发送数据错误:', str(e))
self.is_connected = False
self.sock = None
def start(self):
"""启动采样"""
self.running = True
try:
self.sample_timer.init(
freq=500,
mode=Timer.PERIODIC,
callback=self._sample
)
print("采样定时器启动成功") # 添加调试信息
except Exception as e:
print(f"采样定时器启动失败: {e}")
self.running = False
def stop(self):
"""停止采样"""
self.running = False
self.sample_timer.deinit()
if self.sock:
try:
self.sock.close()
except:
pass
def main():
gc.enable()
collector = None
try:
print("初始化采集器...") # 添加调试信息
collector = HighSpeedCollector()
collector.connect_wifi()
collector.setup_udp()
collector.start()
last_send_time = time.ticks_ms()
send_interval = 50 # 修改为50ms
print("进入主循环...") # 添加调试信息
while collector.running:
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) >= send_interval:
collector._send_data(None)
last_send_time = current_time
time.sleep_ms(10)
except Exception as e:
print("程序出错:", str(e))
finally:
if collector:
collector.stop()
if __name__ == "__main__":
main()
-388
View File
@@ -1,388 +0,0 @@
import socket
import asyncio
import json
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Dict
import struct
from bson.objectid import ObjectId
import time
from redis import asyncio as aioredis
import websockets
class UDPServer:
def __init__(self, host='0.0.0.0', port=6002):
self.server_address = (host, port)
self.loop = asyncio.get_event_loop()
self.transport = None
# MongoDB 连接
self.mongodb_url = "mongodb://lab:[email protected]:27017/lab"
self.db_client = None
self.redis_client = None
# 缓存活跃实验的设备映射
self.active_experiments: Dict[str, dict] = {} # serial_number -> {experiment_id, sensors_map}
# WebSocket连接
self.websocket = None
self.experiment_id = None
# 添加设备序列号映射
self.device_serials: Dict[tuple, str] = {} # (ip, port) -> serial_number
# 添加重试相关的属性
self.serial_request_intervals = {} # (ip, port) -> last_request_time
self.experiment_check_intervals = {} # serial_number -> last_check_time
self.REQUEST_INTERVAL = 1 # 请求序列号的间隔(秒)
self.EXPERIMENT_CHECK_INTERVAL = 1 # 检查实验的间隔(秒)
print(f"UDP服务器启动在 {host}:{port}")
async def init_mongodb(self):
"""初始化MongoDB连接"""
if not self.db_client:
self.db_client = AsyncIOMotorClient(self.mongodb_url)
async def init_redis(self):
"""初始化Redis连接"""
if not self.redis_client:
self.redis_client = await aioredis.from_url(
'redis://222.186.10.253:6379',
password='Obscura@2024',
db=200,
decode_responses=True
)
async def update_active_experiments(self):
"""更新活跃实验的设备映射"""
if not self.db_client:
await self.init_mongodb()
db = self.db_client["lab"]
try:
# 查找所有活跃的实验会话
active_sessions = db.experiment_sessions.find({"end_time": None})
# 清空当前映射
self.active_experiments.clear()
# 更新映射
async for session in active_sessions:
experiment_id = str(session["experiment_id"])
# 遍历会话中的所有设备
for device in session.get("devices", []):
serial_number = device.get("serial_number")
if serial_number:
# 创建通道到传感器名称的映射
sensors_map = {}
for sensor in device.get("sensors", []):
channel_index = sensor.get("index")
if channel_index and channel_index.startswith("channel"):
channel_num = int(channel_index[7:]) - 1 # channel1 -> 0
sensors_map[channel_num] = sensor.get("sensor_name")
self.active_experiments[serial_number] = {
"experiment_id": experiment_id,
"sensors_map": sensors_map
}
print(f"活跃实验映射已更新: {self.active_experiments}")
except Exception as e:
print(f"更新活跃实验时出错: {e}")
import traceback
traceback.print_exc()
def connection_made(self, transport):
self.transport = transport
print("UDP服务器已准备好接收数据...")
def datagram_received(self, data, addr):
"""处理接收到的UDP数据包"""
try:
# 检查是否是序列号(纯文本)还是传感器数据
try:
text = data.decode('utf-8').strip()
if text and all(c.isprintable() for c in text): # 检查是否都是可打印字符
print(f"检测到序列号: {text}")
asyncio.ensure_future(self.handle_serial_number(text, addr))
return
except UnicodeDecodeError:
pass
# 处理传感器数据时添加序列号
parsed_data = self.decode_micropython_data(data, addr)
if parsed_data:
# 从地址映射中获取序列号
serial_number = self.device_serials.get(addr)
if serial_number:
if serial_number not in self.active_experiments:
# 检查是否需要重新查找实验
current_time = time.time()
last_check = self.experiment_check_intervals.get(serial_number, 0)
if current_time - last_check >= self.EXPERIMENT_CHECK_INTERVAL:
print(f"序列号 {serial_number} 未匹配到实验,重新检查...")
asyncio.ensure_future(self.update_active_experiments())
self.experiment_check_intervals[serial_number] = current_time
parsed_data['serial_number'] = serial_number
print(f"传感器数据解析成功 (序列号: {serial_number}):")
asyncio.ensure_future(self.process_and_forward_data(parsed_data))
else:
# 检查是否需要重新请求序列号
current_time = time.time()
last_request = self.serial_request_intervals.get(addr, 0)
if current_time - last_request >= self.REQUEST_INTERVAL:
print(f"未找到地址 {addr} 对应的序列号,发送请求...")
self.transport.sendto(b'REQUEST_SERIAL', addr)
self.serial_request_intervals[addr] = current_time
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
def decode_micropython_data(self, data, addr):
"""解码从MicroPython设备接收的数据"""
try:
# 解包头部信息(前4字节)
timestamp, chunk_index = struct.unpack('<HH', data[:4])
# 解析传感器数据
sensor_data = []
data_bytes = data[4:] # 跳过头部4字节
# 每2字节解析一个数值
for i in range(0, len(data_bytes), 2):
if i + 2 > len(data_bytes):
break
value, = struct.unpack('<H', data_bytes[i:i+2])
if value == 0: # 遇到0值停止解析
break
sensor_data.append(value)
# 获取设备的通道数
serial_number = self.device_serials.get(addr)
num_channels = 0
if serial_number and serial_number in self.active_experiments:
num_channels = len(self.active_experiments[serial_number]["sensors_map"])
if num_channels == 0:
print("警告:未找到通道配置,使用默认值4")
num_channels = 4
# 将数据重组为对应数量的通道
channels = [[] for _ in range(num_channels)]
for i in range(0, len(sensor_data), num_channels):
if i + num_channels > len(sensor_data):
break
for ch in range(num_channels):
channels[ch].append(sensor_data[i + ch])
print(f"数据解析成功: 时间戳={timestamp}, chunk_index={chunk_index}, 通道数={num_channels}, 每通道数据点={len(channels[0])}")
return {
'timestamp': timestamp,
'chunk_index': chunk_index,
'channels': channels,
'sample_count': len(channels[0])
}
except Exception as e:
print(f"数据解析错误: {e}")
import traceback
traceback.print_exc()
return None
async def ensure_websocket_connected(self, experiment_id: str, serial_number: str):
"""确保WebSocket连接已建立"""
try:
# 检查序列号是否还有活跃实验
await self.update_active_experiments()
if serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 已不存在活跃实验,不再重连WebSocket")
if self.websocket:
await self.websocket.close()
self.websocket = None
self.experiment_id = None
return
# 获取最新的experiment_id
current_experiment_id = self.active_experiments[serial_number]["experiment_id"]
if current_experiment_id != experiment_id:
print(f"实验ID已更改: {experiment_id} -> {current_experiment_id}")
experiment_id = current_experiment_id
if (self.websocket is None or
self.experiment_id != experiment_id or
self.websocket.closed):
uri = f"ws://dev.obscura.work/lab/ws/{experiment_id}"
print(f"\n建立新的WebSocket连接: {uri}")
try:
self.websocket = await websockets.connect(uri)
self.experiment_id = experiment_id
# 发送初始连接消息
init_message = {
"type": "connect",
"serial_number": serial_number,
"experiment_id": experiment_id
}
await self.websocket.send(json.dumps(init_message))
response = await self.websocket.recv()
print(f"连接成功,服务器响应: {response}")
except Exception as e:
print(f"WebSocket连接失败: {e}")
self.websocket = None
raise
except Exception as e:
print(f"确保WebSocket连接时出错: {e}")
import traceback
traceback.print_exc()
async def process_and_forward_data(self, parsed_data):
"""处理并逐条转发数据"""
try:
serial_number = parsed_data.get('serial_number')
if not serial_number or serial_number not in self.active_experiments:
print(f"序列号 {serial_number} 未在活跃实验中")
return
experiment_info = self.active_experiments[serial_number]
experiment_id = experiment_info["experiment_id"]
sensors_map = experiment_info["sensors_map"]
# 确保WebSocket连接已建立
await self.ensure_websocket_connected(experiment_id, serial_number)
if not self.websocket:
print("WebSocket连接未建立,无法发送数据")
return
# 确保Redis连接已初始化
if not self.redis_client:
await self.init_redis()
channels = parsed_data['channels']
timestamp = parsed_data['timestamp']
# 构造Redis stream key
stream_key = f"experiment:{experiment_id}:{serial_number}"
# 构造完整的数据点,只包含已配置的传感器
channel_data = {}
for channel_num, sensor_name in sensors_map.items():
if channel_num < len(channels):
channel_data[sensor_name] = channels[channel_num]
point_data = {
'channel_data': channel_data,
'timestamp': timestamp,
'serial_number': serial_number,
'total_points': parsed_data['sample_count']
}
# 存储到Redis Stream
try:
await self.redis_client.xadd(
stream_key,
{'data': json.dumps(point_data)}
)
except Exception as e:
print(f"Redis存储错误: {e}")
import traceback
traceback.print_exc()
# 逐条发送到WebSocket
downsample_rate = 7
for i in range(0, parsed_data['sample_count'], downsample_rate):
sensor_data = {}
for channel_num, sensor_name in sensors_map.items():
if channel_num < len(channels):
sensor_data[sensor_name] = channels[channel_num][i]
message = {
'sensor_data': sensor_data,
'timestamp': timestamp + i,
'serial_number': serial_number,
'chunk_index': i,
'data_points': 1
}
try:
await self.websocket.send(json.dumps(message))
await asyncio.sleep(0.1) # 添加小延迟,避免发送过快
except websockets.exceptions.WebSocketException as e:
print(f"发送数据失败,重置连接: {e}")
self.websocket = None
break
print(f"\n成功发送所有 {parsed_data['sample_count'] // downsample_rate} 个数据点")
except Exception as e:
print(f"数据处理错误: {e}")
import traceback
traceback.print_exc()
async def handle_serial_number(self, serial_number: str, addr):
"""处理接收到的序列号"""
try:
# 保存设备地址和序列号的映射关系
self.device_serials[addr] = serial_number
# 清除该地址的请求间隔记录
self.serial_request_intervals.pop(addr, None)
await self.update_active_experiments()
if serial_number in self.active_experiments:
experiment_id = self.active_experiments[serial_number]["experiment_id"]
print(f"序列号 {serial_number} 匹配到实验 {experiment_id}")
else:
print(f"序列号 {serial_number} 未匹配到活跃实验")
# 记录检查时间
self.experiment_check_intervals[serial_number] = time.time()
except Exception as e:
print(f"处理序列号错误: {e}")
def start(self):
"""启动UDP服务器"""
print("正在启动UDP服务器...")
# 初始化数据库连接
self.loop.run_until_complete(self.init_mongodb())
# 初始化Redis连接
self.loop.run_until_complete(self.init_redis())
# 更新活跃实验
self.loop.run_until_complete(self.update_active_experiments())
listen = self.loop.create_datagram_endpoint(
lambda: self,
local_addr=self.server_address
)
self.transport, _ = self.loop.run_until_complete(listen)
try:
self.loop.run_forever()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
if self.redis_client:
self.redis_client.close()
if self.transport:
self.transport.close()
self.loop.close()
def stop(self):
"""停止服务器"""
print("\n服务器正在关闭...")
if self.websocket:
asyncio.create_task(self.websocket.close())
if self.transport:
self.transport.close()
self.loop.close()
if __name__ == "__main__":
server = UDPServer()
server.start()
-1125
View File
File diff suppressed because it is too large Load Diff
-292
View File
@@ -1,292 +0,0 @@
# Device routes
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional
from motor.motor_asyncio import AsyncIOMotorClient
from bson import ObjectId
from pydantic import BaseModel, Field
from contextlib import asynccontextmanager
from datetime import datetime
import uuid
# Database Configuration
MONGODB_URL = "mongodb://lab:[email protected]:27017/lab"
# 更新 FastAPI 实例化
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行
await connect_to_mongo()
yield
# 关闭时执行
await close_mongo_connection()
app = FastAPI(lifespan=lifespan)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# MongoDB setup
class PyObjectId(ObjectId):
"""
自定义ObjectId类,用于在Pydantic模型中处理MongoDB的ObjectId
"""
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v, handler):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
@classmethod
def __get_pydantic_json_schema__(cls, _schema_cache, **_kwargs):
return {
'type': 'string',
'description': 'ObjectId',
'pattern': r'^[0-9a-fA-F]{24}$'
}
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(
type='string',
description='ObjectId',
pattern=r'^[0-9a-fA-F]{24}$'
)
class SensorModel(BaseModel):
"""传感器模型"""
index: str
sensor_name: str
sensor_type: str
unit: str
class DeviceModel(BaseModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None)
device_name: str
device_type: str
device_number: int
serial_numbers: list = Field(default_factory=list)
sensors: list[SensorModel] = Field(default_factory=list)
class Config:
populate_by_name = True
arbitrary_types_allowed = True
json_encoders = {ObjectId: str}
def dict(self, *args, **kwargs):
"""确保返回的字典包含 _id 字段"""
kwargs["by_alias"] = True
return super().dict(*args, **kwargs)
# Database connection
class Database:
"""数据库连接管理类"""
client: AsyncIOMotorClient = None
db = Database()
async def get_database():
"""获取数据库连接"""
if db.client is None:
await connect_to_mongo()
return db.client["lab"]
async def connect_to_mongo():
try:
db.client = AsyncIOMotorClient(MONGODB_URL)
# 验证连接
await db.client.admin.command('ping')
print("Successfully connected to MongoDB")
except Exception as e:
print(f"Could not connect to MongoDB: {e}")
raise
async def close_mongo_connection():
db.client.close()
@app.post("/devices/devices")
async def create_device(device: DeviceModel):
"""
创建新的设备
参数:
device: 设备模型,包含设备信息和传感器列表
返回:
dict: 包含创建成功消息和新设备ID
"""
db = await get_database()
# 验证传感器数量是否与通道数匹配
if len(device.sensors) != device.device_number:
raise HTTPException(
status_code=400,
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
)
device_dict = device.model_dump(exclude={"id"})
result = await db.devices.insert_one(device_dict)
return {
"message": "设备创建成功",
"id": str(result.inserted_id)
}
@app.get("/devices/devices")
async def get_devices():
"""
获取所有设备列表
返回:
List[dict]: 设备列表,包含所有设备的信息
"""
db = await get_database()
devices = []
async for device in db.devices.find():
# 确保 _id 被正确序列化
device["_id"] = str(device["_id"])
devices.append(device)
return devices
# 添加删除路由
@app.delete("/devices/devices/{device_id}")
async def delete_device(device_id: str):
"""
删除指定的设备
参数:
device_id: 设备的ObjectId字符串
返回:
dict: 包含删除操作的结果信息
"""
try:
db = await get_database()
result = await db.devices.delete_one({"_id": ObjectId(device_id)})
if result.deleted_count == 0:
return {"message": "未找到指定设备"}
return {"message": "设备删除成功"}
except Exception as e:
return {"message": f"删除设备失败: {str(e)}"}
# 添加生成序列号的函数
def generate_serial_number(device_type, batch_number, device_id):
"""生成序列号
格式: XX00-UUID(10位)-设备ID后6位
"""
# 获取前缀(设备类型前两位大写)
prefix = f"{device_type[:2].upper()}{batch_number:02d}"
# 生成UUID并取前10位
uuid_part = str(uuid.uuid4()).replace('-', '')[:10]
# 获取设备ID的后6位作为校验码
checksum = str(device_id)[-6:]
return f"{prefix}-{uuid_part}-{checksum}"
# 添加生成序列号的路由
@app.post("/devices/devices/{device_id}/serials")
async def generate_device_serials(device_id: str):
"""为设备生成10个新的序列号"""
try:
db = await get_database()
# 获取设备信息
device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not device:
return {"message": "设备不存在"}
# 获取当前序列号数量作为批次号
current_batch = len(device.get("serial_numbers", [])) // 10 + 1
# 生成10个新序列号
new_serials = []
for _ in range(10):
serial = generate_serial_number(
device["device_type"],
current_batch,
device_id # 传入设备ID
)
new_serials.append({
"serial": serial,
"status": "available",
"created_at": datetime.now().isoformat()
})
# 更新设备记录
result = await db.devices.update_one(
{"_id": ObjectId(device_id)},
{"$push": {"serial_numbers": {"$each": new_serials}}}
)
return {
"message": "序列号生成成功",
"serials": new_serials
}
except Exception as e:
return {"message": f"生成序列号失败: {str(e)}"}
# 添加获取序列号的路由
@app.get("/devices/devices/{device_id}/serials")
async def get_device_serials(device_id: str):
"""获取设备的所有序列号"""
try:
db = await get_database()
device = await db.devices.find_one({"_id": ObjectId(device_id)})
if not device:
return {"message": "设备不存在"}
return {
"device_id": str(device["_id"]),
"device_name": device["device_name"],
"serial_numbers": device.get("serial_numbers", [])
}
except Exception as e:
return {"message": f"获取序列号失败: {str(e)}"}
# 添加更新设备的路由
@app.put("/devices/devices/{device_id}")
async def update_device(device_id: str, device: DeviceModel):
"""更新设备信息"""
try:
db = await get_database()
# 验证传感器数量
if len(device.sensors) != device.device_number:
raise HTTPException(
status_code=400,
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
)
# 更新设备信息
update_data = device.model_dump(exclude={"id"})
result = await db.devices.update_one(
{"_id": ObjectId(device_id)},
{"$set": update_data}
)
if result.modified_count == 0:
raise HTTPException(status_code=404, detail="设备不存在")
return {"message": "设备更新成功"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=6001)
-4155
View File
File diff suppressed because it is too large Load Diff
-2369
View File
File diff suppressed because it is too large Load Diff