
本文详解如何在raspberry pi os中通过udev + systemd可靠触发多个同id usb设备的独立python脚本实例,解决单服务冲突、串口发现失败及csv记录不一致问题。
本文详解如何在raspberry pi os中通过udev + systemd可靠触发多个同id usb设备的独立python脚本实例,解决单服务冲突、串口发现失败及csv记录不一致问题。
在Raspberry Pi OS中实现“即插即录”式多USB设备数据采集,关键在于避免服务实例竞争与精准绑定设备上下文。您当前的配置使用静态服务名 usb-trigger.service,导致 systemd 将后续触发视为重复请求而静默忽略——这是脚本“有时不执行”的根本原因。同时,Python脚本中轮询查找 /dev/ttyUSB* 的逻辑,在多设备并存时易因竞态或端口命名延迟(如 ttyUSB0/ttyUSB1 分配顺序不稳定)而失败。
✅ 正确方案:启用 systemd 多实例模板服务
1. 重命名服务为模板单元
将服务文件重命名为带 @ 符号的模板格式:
sudo mv /etc/systemd/system/usb-trigger.service /etc/systemd/system/usb-trigger@.service
2. 更新 udev 规则(自动注入设备内核名)
编辑 /etc/udev/rules.d/myrule.rules:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", ENV{SYSTEMD_WANTS}="usb-trigger@%k.service"
%k 由 udev 自动替换为设备内核名(如 ttyUSB0、ttyACM1),确保每个设备触发唯一实例。验证可用 udevadm info -n /dev/ttyUSB0 | grep ID_NAME 查看设备标识。
3. 修改服务定义,传递设备路径参数
更新 /etc/systemd/system/usb-trigger@.service:
[Unit] Description=USB Trigger Service for %I After=multi-user.target [Service] Type=exec ExecStart=/path/to/my/python/script/uart.py %I Restart=on-failure RestartSec=3 StandardOutput=journal StandardError=journal # 关键:确保服务在设备节点就绪后再启动 BindsTo=dev-%i.device After=dev-%i.device [Install] WantedBy=multi-user.target
- Type=exec 替代 oneshot,支持长期运行;
- BindsTo 和 After 确保服务严格依赖对应设备节点,避免 serial.Serial() 初始化失败;
- Restart 提升容错性(如端口短暂不可用)。
4. 重构 Python 脚本:移除轮询,直连指定端口
#!/usr/bin/env python3
import serial
import csv
import datetime
import sys
import argparse
import logging
import time
# 配置日志输出到 systemd journal
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stderr)]
)
def get_current_time():
now = datetime.datetime.now()
return now.strftime("%H:%M:%S.%f")[:-3] # 精确到毫秒
def main():
parser = argparse.ArgumentParser(description="Log USB device data to CSV")
parser.add_argument("device", help="Serial device path (e.g., ttyUSB0)")
args = parser.parse_args()
# 构造完整设备路径
port = f"/dev/{args.device}"
logging.info(f"Starting service for {port}")
# 生成唯一文件名
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"{timestamp}_{args.device}.csv"
try:
with serial.Serial(port=port, baudrate=460800, timeout=2) as ser, \
open(filename, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
logging.info(f"Connected to {port}, writing to {filename}")
# 初始化写入表头(示例)
writer.writerow(['Input_1', 'Input_2', 'Input_3', 'timestamp'])
csvfile.flush()
while True:
line = ser.readline().decode('ascii', errors='ignore').strip()
if line:
try:
# 假设数据格式为 "12.34,56.78,90.12"
values = [float(x) for x in line.split(',')]
if len(values) >= 3:
writer.writerow(values + [get_current_time()])
csvfile.flush() # 确保实时落盘
except (ValueError, IndexError):
pass # 忽略格式错误行
time.sleep(0.01)
except serial.SerialException as e:
logging.error(f"Serial error on {port}: {e}")
except OSError as e:
logging.error(f"OS error (device removed?): {e}")
except Exception as e:
logging.error(f"Unexpected error: {e}")
finally:
logging.info(f"Service for {port} terminated.")
if __name__ == "__main__":
main()
⚠️ 关键注意事项
- 权限保障:确保 pi 用户或服务运行用户对 /dev/ttyUSB* 有读写权限(sudo usermod -a -G dialout pi);
- udev 规则加载:修改后执行 sudo udevadm control --reload-rules && sudo udevadm trigger;
- 服务启用:sudo systemctl daemon-reload && sudo systemctl enable usb-trigger@.service;
-
调试技巧:
- 查看触发日志:journalctl -u usb-trigger@*.service -f
- 模拟触发:sudo udevadm trigger --subsystem-match=usb --action=add
- 验证设备属性:udevadm info -n /dev/ttyUSB0 | grep -E "(idVendor|idProduct|KERNEL)"
通过模板化服务 + 设备路径显式传递,每个 USB 设备获得独立、隔离的进程空间,彻底规避资源争抢与发现延迟问题。配合 systemd 的设备绑定机制,可稳定支撑 4 台及以上同 VID/PID 设备并发运行,每台设备生成专属 CSV 文件,实现工业级可靠的数据采集闭环。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











