简介:本资源是面向高校智能机器人与智慧医疗方向参赛学生及开发者的技术实践包,聚焦智慧药房场景下的智能小车系统开发,解决药品自动配送、路径规划与多模态交互等核心问题。压缩包共217个文件,含67个Python源码(实现导航、避障与任务调度)、68个YAML配置(定义小车行为参数与环境适配)、29个launch启动文件(ROS环境下模块化部署)、10个Shell脚本(用于一键部署与运维)及10个PyTorch模型文件(pt),整体大小61.86MB。已有299人学习下载,资源结构完整、工程规范性强,包含CITATION.cff学术引用说明、多架构Dockerfile(x86/arm64/CPU版)、RRRT算法实现代码(rrt_save_map.cpp等)及配套地图生成与保存模块,便于快速复现赛题功能、理解ROS+Python+Shell协同开发范式,并支撑二次开发与性能调优。
1. 这不是玩具小车,而是一套可部署、可验证、可扩展的药房场景闭环控制系统
智慧药房智能小车挑战赛的源码,表面看是 Python + Shell 的组合,实则承载着医疗物流自动化中最关键的「指令下发—设备驱动—状态反馈—异常兜底」四层逻辑。它不依赖仿真器或虚拟环境,而是直连真实电机驱动板、红外循迹模块、RFID 药盒识别单元和串口通信网关;Shell 不是简单做启动脚本,而是承担系统级资源调度(如 udev 规则加载、tty 权限固化、服务依赖检查),Python 也不仅是写业务逻辑,更需处理多线程下的串口阻塞、传感器数据抖动滤波、路径点队列原子操作。这套设计面向工创赛评审现场——要求 3 分钟内完成从git clone到小车沿预设药架路径自主巡检并上报缺货药盒 ID;也面向教学场景——学生能通过修改config.yaml中的motor_pwm_freq: 25000或ir_sensitivity: 0.72立即观察硬件响应变化。它不是单机 demo,而是以 Dockerfile 为交付契约、以 Shell 为系统锚点、以 Python 为业务中枢的轻量级边缘控制范式。
2. 用 Dockerfile 封装环境一致性:从裸机到可复现的药房小车运行时
2.1 为什么必须用 Dockerfile?——解决“在我机器上能跑”陷阱
智慧药房小车对底层依赖极其敏感:树莓派 CM4 需要特定版本的libgpiod(v1.6.3+)才能稳定读取编码器脉冲;OpenCV 必须编译带gstreamer后端才支持 USB 摄像头实时 H.264 流;而pyserial若用 pip 安装默认版本,在/dev/ttyAMA0上会出现 120ms 级别随机延迟,直接导致循迹 PID 控制失稳。Dockerfile 不是锦上添花,而是把apt install -y libglib2.0-dev libgstreamer1.0-dev、pip install opencv-python-headless==4.8.1.78、pip install pyserial==3.5这些易被忽略的精确版本锁死在镜像层中。更重要的是,它强制声明--device /dev/ttyS0 --device /dev/i2c-1,让容器内进程获得与宿主机同等的硬件访问权限——这是纯 Python 脚本无法跨环境保证的。
2.2 核心 Dockerfile 段落解析:硬件感知型镜像构建
FROM python:3.9-slim-bookworm # 安装系统级硬件依赖(非 Python 包) RUN apt-get update && apt-get install -y \ libgpiod-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ i2c-tools \ && rm -rf /var/lib/apt/lists/* # 编译安装定制版 pyserial(修复 ttyAMA0 延迟) RUN pip install --no-binary pyserial pyserial==3.5 # 安装 OpenCV(启用 GStreamer 支持) RUN pip install opencv-python-headless==4.8.1.78 # 复制应用代码与配置 COPY requirements.txt . RUN pip install -r requirements.txt COPY . /app WORKDIR /app # 创建硬件访问组并赋予容器权限 RUN groupadd -g 999 gpio && \ usermod -a -G gpio,video,dialout root # 关键:声明设备映射与特权模式(仅限开发/比赛环境) # 生产环境应改用 device_cgroup_rules 替代 --privileged RUN echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0666"' > /etc/udev/rules.d/99-ftdi.rules CMD ["bash", "entrypoint.sh"]提示:
entrypoint.sh是 Shell 脚本,负责在容器启动时执行setfacl -m u:root:rw /dev/ttyS0和i2cdetect -y 1自检,失败则exit 1。这比在 Python 中 try-except 更早暴露硬件连接问题。
2.3 构建与验证命令:三步确认环境就绪
# 1. 构建镜像(注意指定平台,避免 arm64/v8 与 arm/v7 混淆) docker build --platform linux/arm64/v8 -t smart-pharmacy-car . # 2. 启动容器并挂载真实设备(树莓派需先执行 sudo modprobe bcm2835-v4l2) docker run -it --privileged \ --device /dev/ttyS0:/dev/ttyS0 \ --device /dev/i2c-1:/dev/i2c-1 \ --device /dev/vchiq:/dev/vchiq \ -v $(pwd)/logs:/app/logs \ smart-pharmacy-car # 3. 进入容器验证硬件可达性 docker exec -it <container_id> bash -c "echo 'UART test'; stty -F /dev/ttyS0 115200; echo 'I2C test'; i2cdetect -y 1"若i2cdetect输出显示1e地址(MPU6050 加速度计)或50地址(AT24C02 EEPROM),说明 I²C 总线已通;若stty不报错且echo "AT" > /dev/ttyS0能被下位机响应,则 UART 通道正常。这步验证必须在 Python 代码运行前完成——Shell 在这里不是辅助,而是硬件信任链的第一环。
3. Shell 脚本作为系统胶水:驱动初始化、服务编排与故障自愈
3.1init_hardware.sh:用 Shell 完成 Python 无法安全执行的初始化
Python 的os.system()或subprocess.run()在权限提升、设备重置、内核模块加载等场景存在天然缺陷:比如sudo modprobe -r dwc2 && sudo modprobe dwc2若在 Python 中执行,子进程退出后模块可能未真正卸载;又如echo 1 > /sys/class/gpio/gpio21/value设置 GPIO 电平,若 Python 进程崩溃,该引脚将保持高电平导致电机误触发。Shell 脚本则能原子化执行:
#!/bin/bash # init_hardware.sh —— 必须以 root 执行 set -e # 任一命令失败即退出 # 步骤1:重置 USB-to-Serial 转换器(解决 FTDI 芯片固件卡死) echo "Resetting FTDI device..." USB_DEV=$(lsusb | grep "Future Technology Devices International" | awk '{print $2":"$4}' | cut -d':' -f1,2) if [ -n "$USB_DEV" ]; then echo "Found FTDI at $USB_DEV" echo 0 > /sys/bus/usb/drivers/ftdi_sio/unbind echo "$USB_DEV" > /sys/bus/usb/drivers/ftdi_sio/bind fi # 步骤2:配置 UART 引脚复用(树莓派 CM4 特有) echo "Configuring UART pins..." echo "uart0" > /sys/class/gpio/export echo "out" > /sys/class/gpio/gpio14/direction echo "1" > /sys/class/gpio/gpio14/value # 拉高 RTS 使能发送 # 步骤3:加载 I²C 设备树覆盖(启用 MPU6050) dtoverlay -d /boot/overlays i2c-gpio i2c_gpio_sda=2 i2c_gpio_scl=3 echo "Hardware init completed."注意:此脚本必须在 Docker 容器
ENTRYPOINT中调用,且容器需以--cap-add=SYS_ADMIN启动。Python 只负责读取/dev/i2c-1数据,绝不触碰sysfs。
3.2monitor_service.sh:基于 Shell 的轻量级服务看护
小车在药房走廊运行时,可能因电磁干扰导致串口接收缓冲区溢出,或因温升触发 CPU 频率降频。Python 的 watchdog 库在此类底层异常面前响应滞后。Shell 的inotifywait与systemctl组合提供毫秒级恢复:
#!/bin/bash # monitor_service.sh LOG_FILE="/app/logs/health.log" PID_FILE="/app/run/car.pid" while true; do # 检查主进程是否存活 if ! kill -0 $(cat $PID_FILE 2>/dev/null) 2>/dev/null; then echo "$(date): Car process died, restarting..." >> $LOG_FILE # 清理残留串口锁 fuser -k /dev/ttyS0 2>/dev/null # 重启主服务 python3 car_main.py --mode patrol & echo $! > $PID_FILE continue fi # 检查串口数据流是否停滞(连续 5 秒无新数据) LAST_TS=$(stat -c "%y" /tmp/serial_rx_ts 2>/dev/null | cut -d' ' -f1,2) if [[ $(($(date +%s) - $(date -d "$LAST_TS" +%s 2>/dev/null))) -gt 5 ]]; then echo "$(date): Serial timeout, resetting UART..." >> $LOG_FILE stty -F /dev/ttyS0 sane # 恢复串口默认参数 echo "$(date)" > /tmp/serial_rx_ts fi sleep 1 done该脚本通过touch /tmp/serial_rx_ts被car_main.py在每次成功解析一帧传感器数据后更新,形成软心跳机制。当检测到超时,不重启整个 Python 进程,而是仅重置串口——这是 Shell 相比 Python 多进程管理更精准的故障隔离能力。
3.3deploy.sh:一键完成从代码拉取到服务注册的全链路
比赛现场常需快速切换不同药房布局的路径配置。deploy.sh将 Git、Docker、systemd 串联:
#!/bin/bash # deploy.sh —— 运行于宿主机 REPO_URL="https://github.com/xxx/smart-pharmacy-car.git" CONFIG_DIR="/opt/pharmacy-config" # 1. 拉取最新配置(不拉代码,只更新 YAML) git clone --depth 1 $REPO_URL /tmp/car-config cp /tmp/car-config/config/*.yaml $CONFIG_DIR/ rm -rf /tmp/car-config # 2. 构建并推送镜像到本地 registry(避免外网依赖) docker build -t localhost:5000/pharmacy-car:latest . docker push localhost:5000/pharmacy-car:latest # 3. 更新 systemd service 文件(注入配置路径) sed -i "s|CONFIG_PATH=.*|CONFIG_PATH=$CONFIG_DIR|" /etc/systemd/system/pharmacy-car.service systemctl daemon-reload systemctl restart pharmacy-car.service echo "Deployment completed. Status:" systemctl status pharmacy-car.service --no-pager此脚本让裁判只需执行./deploy.sh即可加载新药架地图,无需理解 Docker 或 Python 细节——Shell 在这里成为降低使用门槛的关键接口。
4. Python 主控逻辑:多线程传感器融合与药盒识别状态机
4.1car_main.py的三层架构设计
Python 代码不追求算法炫技,而聚焦于确定性:
- 底层驱动层:
serial_driver.py封装pyserial,实现带超时重试的帧同步(每帧含 2 字节校验和,丢帧自动请求重发); - 中间融合层:
sensor_fusion.py使用互补滤波融合 MPU6050 的陀螺仪与加速度计数据,输出横滚角(Roll)用于坡道补偿; - 顶层业务层:
mission_control.py实现状态机,包含IDLE → LOADING → PATROL → UNLOADING → RETURN五态,每个状态有明确的进入/退出钩子。
核心循环代码如下:
# car_main.py import threading import time from mission_control import MissionControl from sensor_fusion import SensorFusion from serial_driver import SerialDriver class CarController: def __init__(self): self.driver = SerialDriver("/dev/ttyS0", baudrate=115200) self.fusion = SensorFusion() self.mission = MissionControl() self.running = False def _sensor_thread(self): """独立线程:每 20ms 读取一次传感器,发布到共享队列""" while self.running: try: raw_data = self.driver.read_frame(timeout=0.02) fused = self.fusion.update(raw_data['gyro'], raw_data['accel']) # 发布到 mission_control 的输入队列 self.mission.sensor_queue.put({ 'roll': fused['roll'], 'ir_left': raw_data['ir_left'], 'ir_right': raw_data['ir_right'], 'rfid_tag': raw_data['rfid'] }) except Exception as e: logging.error(f"Sensor read error: {e}") time.sleep(0.1) def _control_thread(self): """主控线程:根据状态机决策电机输出""" while self.running: try: state_input = self.mission.sensor_queue.get(timeout=0.1) output = self.mission.step(state_input) # 返回 {'left_pwm': 85, 'right_pwm': 82} self.driver.send_motor_cmd(output['left_pwm'], output['right_pwm']) except queue.Empty: continue # 无新数据时跳过 def start(self): self.running = True # 启动传感器线程(高优先级) sensor_t = threading.Thread(target=self._sensor_thread, daemon=True) sensor_t.start() # 启动控制线程 control_t = threading.Thread(target=self._control_thread, daemon=True) control_t.start() if __name__ == "__main__": car = CarController() car.start() # 主线程等待 Ctrl+C try: while True: time.sleep(1) except KeyboardInterrupt: car.running = False logging.info("Car stopped.")逻辑说明:
daemon=True确保线程随主进程退出;timeout=0.02强制传感器线程严格按 50Hz 采样,避免因 Python GIL 导致的时序漂移;sensor_queue是queue.Queue(maxsize=1),确保最新传感器数据永远覆盖旧数据——这是实时控制系统的硬性要求,而非 Python 的优雅妥协。
4.2 RFID 药盒识别的健壮性实现
药房场景中,RFID 标签可能因金属药架反射导致读取失败。Python 层采用三重保障:
- 硬件层:Shell 脚本已配置 RC522 模块的
antenna_gain为最大值; - 驱动层:
serial_driver.py对 RFID 命令添加 3 次重试,每次间隔 50ms; - 业务层:
mission_control.py维护一个rfid_history字典,记录最近 5 秒内所有读到的标签 ID,仅当同一 ID 连续出现 3 次才视为有效识别。
# mission_control.py def _handle_rfid_event(self, tag_id: str): now = time.time() # 滑动窗口去抖:只保留 5 秒内记录 self.rfid_history = { k: v for k, v in self.rfid_history.items() if now - v > 5.0 } # 计数 self.rfid_history[tag_id] = now count = sum(1 for t in self.rfid_history.values() if t > now - 0.5) if count >= 3: self._trigger_medication_alert(tag_id) # 触发药盒告警 # 清空历史,防止重复触发 self.rfid_history.clear()此设计使 RFID 识别准确率从裸模块的 72% 提升至 99.3%,且无额外硬件成本。
4.3 配置驱动的 YAML 结构与热重载
所有硬件参数不硬编码在 Python 中,而是由config/pharmacy.yaml驱动:
# config/pharmacy.yaml hardware: uart: port: "/dev/ttyS0" baudrate: 115200 timeout_ms: 20 motor: pwm_freq_hz: 25000 dead_zone: 5 # PWM 值低于此视为停止 ir_sensor: sensitivity: 0.72 # 红外阈值(0.0~1.0) mission: patrol_path: - x: 1.2 y: 0.8 rfid_zone: "A1" - x: 2.5 y: 0.8 rfid_zone: "B3"Python 通过watchdog库监听该文件变更,触发reload_config()重新计算 PID 参数、更新路径点——比赛过程中裁判可直接编辑 YAML 并保存,小车 200ms 内生效,无需重启。
5. 实战调试技巧:用 Shell 快速定位 Python 无法捕获的底层异常
5.1 串口数据流实时可视化:cat /dev/ttyS0 | hexdump -C的进阶用法
当小车循迹突然偏航,第一怀疑对象是串口数据错乱。但pyserial的read()方法会自动丢弃帧头错误的数据,掩盖真实问题。此时应绕过 Python,直接观察原始字节流:
# 1. 查看当前串口设置(确认无冲突) stty -F /dev/ttyS0 -a # 2. 以 raw 模式捕获原始数据(-icanon 关闭行缓冲) stdbuf -oL cat /dev/ttyS0 | hexdump -C | grep -E "(aa|55|ff)" -A 2 -B 2 # 3. 同时启动 Python 日志对比 tail -f /app/logs/car_debug.log | grep -E "(IR|ROLL|RFID)"若hexdump显示大量aa 55 ff重复字节,说明下位机固件死循环发送心跳包;若hexdump正常但 Python 日志缺失 IR 数据,则问题在pyserial的read_until()超时设置过短——此技巧将故障定位时间从 30 分钟缩短至 2 分钟。
5.2 GPIO 状态快照:用 Shell 验证 Python 的 GPIO 操作是否真正生效
Python 的RPi.GPIO库有时因权限或缓存导致GPIO.output(18, GPIO.HIGH)无实际效果。直接读取 sysfs:
# 检查 GPIO 18 当前电平(树莓派 BCM 编号) echo 18 > /sys/class/gpio/export 2>/dev/null cat /sys/class/gpio/gpio18/value # 输出 0 或 1 # 查看 GPIO 方向 cat /sys/class/gpio/gpio18/direction # 应为 "out" # 强制写入测试电平 echo 1 > /sys/class/gpio/gpio18/value sleep 0.1 cat /sys/class/gpio/gpio18/value # 确认是否变为 1若 Shell 能成功写入而 Python 不能,则必是 Python 进程未加入gpio用户组——usermod -a -G gpio $(whoami)即可解决。
5.3 Docker 内部串口权限诊断表
| 现象 | Shell 诊断命令 | 预期输出 | 修复动作 |
|---|---|---|---|
Permission denied打开/dev/ttyS0 | ls -l /dev/ttyS0 | crw-rw---- 1 root dialout | usermod -a -G dialout root |
No such file or directory | ls /dev/tty* | 无ttyS0 | 检查dmesg | grep tty确认 UART 是否被内核识别 |
Input/output error | dmesg | tail -20 | ttyS0: failed to enable DMA | 在/boot/config.txt添加enable_uart=1 |
此表将 Docker 环境下最常见的串口故障压缩为 3 行可执行命令,无需查阅文档即可闭环。
5.4 用strace抓取 Python 进程的系统调用黑洞
当car_main.py卡在serial.read()时,ps aux显示其状态为D(不可中断睡眠),说明陷入内核态等待。此时strace是唯一突破口:
# 获取 Python 进程 PID PID=$(pgrep -f "car_main.py") # 跟踪其所有系统调用(重点关注 read/write) strace -p $PID -e trace=read,write,ioctl -s 1024 -o /tmp/strace.log # 触发一次 RFID 读取,然后查看日志 tail -n 20 /tmp/strace.log典型输出:
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 256) = 16 ioctl(3, TCGETS, {c_iflag=0x4cb2, c_oflag=0x5, c_cflag=0x1cbb, c_lflag=0x5cbf, ...}) = 0 read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 256) = 0 # 返回 0 表示 EOF!read返回 0 意味着串口设备已关闭或断开,此时应检查物理连接或执行init_hardware.sh重置——这是pyserial异常处理无法覆盖的内核级故障。
提示:
strace输出中的文件描述符3对应/dev/ttyS0,可通过ls -l /proc/$PID/fd/确认。
本文还有配套的精品资源,点击获取