多传感器数据采集系统技术架构
1. 概述
本系统用于同时采集多个传感器的数据(如力觉、触觉、视觉等),并将数据按时间对齐后统一保存,以便后续用于遥操作、模型训练或数据分析。系统采用多线程架构,每个传感器拥有独立的采集线程,通过共享数据缓冲区和锁机制保证线程安全,并提供统一的启动、停止和保存接口。
2. 核心设计要点
2.1 类结构设计
- 采集控制器类 (例如
DataCollector)负责管理所有传感器的生命周期、数据汇聚与持久化。 - 在
__init__方法中:- 创建各传感器实例(如
self.force_sensor = ForceSensor())。 - 初始化共享数据缓冲区
self.data_buffer = {}(字典结构,键为传感器名称,值为该传感器的数据列表)。 - 初始化线程锁
self.lock = threading.Lock()。 - 初始化运行标志
self.running = False。 - 为每个传感器创建独立线程,线程目标为对应的数据采集方法。
- 创建各传感器实例(如
2.2 数据采集方法
为每个传感器定义一个采集方法(如 _collect_force_data),其结构为:
python
def _collect_force_data(self):
while self.running:
data = self.force_sensor.get_data() # 获取一帧数据
with self.lock:
self.data_buffer["force"].append(data) # 加锁写入
time.sleep(1.0 / freq) # 控制采样频率
- 每个采集方法独立运行,互不阻塞。
- 使用
while self.running控制循环,便于外部优雅停止。 - 写入共享缓冲区时必须加锁,避免数据损坏。
2.3 启动流程(start 方法)
- 清空或初始化
self.data_buffer,例如:self.data_buffer = {"force": [], "tactile": [], ...}。 - 设置
self.running = True。 - 启动所有传感器采集线程(线程已在
__init__中创建,只需确保线程未启动或使用标志控制)。
2.4 停止流程(stop 方法)
- 设置
self.running = False,各采集线程自然退出循环。 - 记录采集结束时间,计算总采集时长。
- 可选:等待所有线程结束(
thread.join())。
2.5 数据保存(save 方法)
- 复制一份当前
self.data_buffer的深拷贝(避免保存过程中缓冲区被修改)。 - 将拷贝的数据写入磁盘,常用格式:
.npz、.pkl、.csv或按传感器分开存储。 - 可同时保存采集参数(如时间戳、采样频率、传感器配置等)。
3. 关键注意事项
- 线程安全 :所有对
self.data_buffer的写操作必须使用锁;读操作(如保存时的拷贝)也建议加锁。 - 采样频率控制 :使用
time.sleep控制循环间隔,避免占用过高 CPU。 - 异常处理 :在采集循环中应捕获传感器
get_data可能抛出的异常,避免线程意外退出。 - 资源释放:停止采集后应合理关闭传感器连接(如串口、网络套接字)。
示例代码模板
python
import threading
import time
import copy
import pickle
from datetime import datetime
# 假设存在以下传感器类(用户需根据实际情况替换)
# from sensors import ForceSensor, TactileSensor, CameraSensor
class DataCollector:
"""
多传感器数据采集控制器
"""
def __init__(self, sensor_configs, freq=100):
"""
参数:
sensor_configs: dict, 例如 {'force': ForceSensor(port=6001), 'tactile': TactileSensor(port=6002)}
freq: int, 全局采集频率 (Hz),每个传感器按此频率独立采集
"""
self.sensor_configs = sensor_configs
self.freq = freq
self.sleep_interval = 1.0 / freq
# 共享数据缓冲区: { sensor_name: list_of_data_frames }
self.data_buffer = {}
self.lock = threading.Lock()
self.running = False
# 存储线程对象
self.threads = []
# 为每个传感器创建采集线程
for name, sensor in self.sensor_configs.items():
# 初始化缓冲区中的列表
self.data_buffer[name] = []
# 创建线程,target 使用 lambda 或 partial 传递传感器名
t = threading.Thread(target=self._collect_loop, args=(name, sensor))
t.daemon = True # 主程序退出时自动终止
self.threads.append(t)
def _collect_loop(self, sensor_name, sensor):
"""单个传感器的采集循环"""
while self.running:
try:
# 调用传感器的获取数据方法(需自行实现)
data = sensor.get_data()
# 可以添加时间戳:data['timestamp'] = time.time()
with self.lock:
self.data_buffer[sensor_name].append(data)
except Exception as e:
print(f"[{sensor_name}] 采集异常: {e}")
time.sleep(self.sleep_interval) # 不同的传感器频率是不同的,这里应该做一个 map 使用 name 去查就好了
def start(self):
"""启动所有传感器采集"""
if self.running:
print("采集已在运行中")
return
# 清空缓冲区(保留结构)
with self.lock:
for name in self.sensor_configs.keys():
self.data_buffer[name] = []
self.running = True
self.start_time = time.time()
# 启动所有线程
for t in self.threads:
if not t.is_alive():
t.start()
print(f"采集已启动,共 {len(self.threads)} 个传感器线程,采样频率 {self.freq} Hz")
def stop(self):
"""停止采集"""
if not self.running:
return
self.running = False
# 等待所有线程结束(可选)
for t in self.threads:
t.join(timeout=2.0)
self.end_time = time.time()
duration = self.end_time - self.start_time
print(f"采集已停止,总时长 {duration:.2f} 秒")
def save(self, filepath=None):
"""
将采集的数据保存到磁盘
参数:
filepath: 保存路径,若为 None 则自动生成
"""
if filepath is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = f"collected_data_{timestamp}.pkl"
# 加锁复制数据(深拷贝)
with self.lock:
data_to_save = copy.deepcopy(self.data_buffer)
# 附加元数据
metadata = {
"start_time": self.start_time,
"end_time": self.end_time,
"duration": self.end_time - self.start_time,
"frequency": self.freq,
"sensors": list(self.sensor_configs.keys())
}
save_obj = {
"data": data_to_save,
"metadata": metadata
}
# 保存为 pickle 文件
with open(filepath, "wb") as f:
pickle.dump(save_obj, f)
print(f"数据已保存至: {filepath} (大小: {len(str(save_obj))} bytes)")
# 可选:添加上下文管理器支持
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.stop()
# ========== 使用示例 ==========
if __name__ == "__main__":
# 模拟传感器类(实际使用时替换为真实传感器驱动)
class MockSensor:
def __init__(self, name):
self.name = name
self.counter = 0
def get_data(self):
self.counter += 1
return {"value": self.counter, "name": self.name}
# 配置传感器
sensors = {
"force": MockSensor("force_sensor"),
"tactile": MockSensor("tactile_sensor"),
# "camera": CameraSensor() # 实际使用时添加
}
# 创建采集器 (频率 50 Hz)
collector = DataCollector(sensors, freq=50)
# 启动采集
collector.start()
# 模拟采集 3 秒
time.sleep(3)
collector.stop()
# 保存数据
collector.save()
# 或者使用上下文管理器
# with DataCollector(sensors, freq=50) as collector:
# time.sleep(3)
# collector.save()
说明
- 传感器接口 :示例中使用了
MockSensor模拟真实传感器,实际使用时请替换为真实的传感器驱动类,并确保其具有get_data()方法(返回一帧数据,可以是字典或列表)。 - 频率控制 :每个传感器按相同频率独立采集。若某些传感器频率需要单独设置,可扩展配置字典为
{"name": sensor, "freq": 100}。 - 数据格式 :每个传感器的数据列表可包含任意 Python 对象,推荐每帧数据包含
timestamp字段以便后续时间对齐。 - 扩展性 :可轻松添加新的传感器,只需在
sensor_configs中增加条目即可。
如果需要针对特定传感器(如力觉、触觉、相机)的详细实现,请提供相关接口文档,我可以进一步补充。