下位机教程
目标
ESP32 下位机负责电机、编码器、IMU、OLED、LED、舵机、底盘反馈和下位机通信等实时控制任务。
前置知识
什么是下位机
下位机是更靠近硬件执行层的控制器。本产品中 ESP32 负责接收上位机指令,并控制电机、舵机、LED、OLED,同时读取编码器、IMU 和底盘反馈。
Web 页面按钮不会直接驱动硬件。浏览器中的底盘、云台、灯光和状态查看操作会先进入上位机控制逻辑,再通过 JSON 指令或串口通信下发到底层。Web 页面入口和媒体、视觉功能说明见 上位机教程。
Web 页面 / Python 控制逻辑
↓
JSON 指令 / 串口通信
↓
ESP32 下位机
↓
电机、云台、灯光、OLED、传感器反馈
开发者补充:下位机串口配置
app.py 通过 GPIO UART 与 ESP32 下位机通信。串口配置如下:
| 上位机环境 | 串口 | 波特率 |
|---|---|---|
| Jetson Orin Nano | /dev/ttyTHS0 | 115200 |
| Jetson Orin NX | /dev/ttyTHS1 | 115200 |
其中 /dev/ttyTHS0 是 Jetson Orin Nano 默认串口,/dev/ttyTHS1 用于 Jetson Orin NX。若镜像改动过串口映射,按系统中列出的串口节点检查。
单条低风险 JSON 指令,例如 OLED 和 Head Light / Base Light,建议优先通过 Web 命令输入框测试,由 Web 主程序统一转发,不需要手动释放串口。
涉及底盘连续动作、定时停止、多步动作序列时,建议使用 Jupyter Notebook 或 Python 脚本编排。串口节点和占用进程检查在 JupyterLab Terminal / SSH 终端中执行;ROS 2 相关操作请参考 ROS 2 教程。
若 Notebook 直接使用 BaseController 打开底盘串口,请先完成 串口占用检查与释放。
Web端JSON 指令控制下位机
如果只是通过 Web 命令输入框发送 base -c ... 指令,由 Web 主程序统一转发,不需要手动释放串口。
只有 Notebook / Python 脚本直接使用 BaseController(...) 打开底盘串口时,才需要做串口占用检查。
前置知识
什么是 JSON 指令
JSON 是一种结构化文本格式,适合把“要做什么”和“参数是多少”写清楚。上位机通过 GPIO UART 把 JSON 指令发送给 ESP32 下位机。
常见 JSON 指令包含 T 字段,用于表示指令类型。例如 {"T":1,"L":0.2,"R":0.2} 表示速度闭环运动控制:T=1 是速度控制指令,L 和 R 分别表示左右侧车轮目标线速度,单位为 m/s。
Web端命令输入框特点
Web端命令输入框适合发送单条 JSON 指令,例如:
base -c {"T":1,"L":0.03,"R":0.03}
base -c ... 只转发一次指令,不会持续补发;默认配置下,底盘约 1~2 秒没有收到新的运动指令后会自动停止。因此它适合短时点动测试,例如确认车轮方向或停止指令。
需要持续运动时,请使用 Web 页面底盘按钮 / 键盘控制,或在后面的 Notebook / Python 示例中按固定周期发送运动指令。
不要为了让底盘持续运动而直接调大速度、PWM、PID 或随意修改心跳时间。车轮不动、卡住、异响或发热时,应先停止并检查供电、串口和机械结构。
| 控制方式 | 指令发送方式 | 适合用途 |
|---|---|---|
Web 命令输入框 base -c ... | 单次发送 JSON | 低风险点动测试、OLED、灯光、停止指令 |
| Web 底盘按钮 / 键盘控制 | 周期性发送运动指令 | Web 页面内持续运动控制 |
| Notebook / Python | 由代码控制发送周期 | 多步动作、自动停止、数据记录和调试 |
Web 命令输入框 JSON 写法
Web 命令输入框会按空格拆分命令。发送 base -c ... 时,JSON 内不要加入多余空格,建议使用紧凑写法,便于直接复制。
base -c {"T":1,"L":0,"R":0}
base -c {"T":3,"lineNum":0,"Text":"UGV TEST"}
base -c {"T":132,"IO4":64,"IO5":64}
不要写成下面这样带空格的 JSON:
base -c {"T": 1, "L": 0, "R": 0}
基础运动控制
CMD_SPEED_CTRL - 速度闭环控制
base -c {"T":1,"L":0,"R":0}
用于控制左右侧车轮目标线速度,也可用于停止底盘。
- 参数:
T=1:速度闭环控制。L:左侧车轮目标线速度,单位为m/s。R:右侧车轮目标线速度,单位为m/s。
- 推荐起步值:
- 停止:
L=0,R=0。 - 架空低速测试:
L=0.03,R=0.03。 - 地面低速测试:
L=0.03~0.05,R=0.03~0.05。
- 停止:
- 取值范围 / 参考说明:
L和R的正负号表示运动方向;- 具体可用速度范围与电机、减速器、轮径和供电状态有关,首次测试请从较小速度逐步增加。
- 现象:
L和R同为正值时,底盘向一个方向移动;L和R同为负值时,底盘向反方向移动;L和R一正一负时,底盘原地转向;L=0,R=0时,底盘停止。
CMD_PWM_INPUT - PWM 开环控制
base -c {"T":11,"L":64,"R":64}
- 参数:
T=11:PWM 开环控制。L:左侧电机 PWM。R:右侧电机 PWM。
- 取值范围 / 参考范围:
L/R=-255~255;- 正负号表示方向;
- 绝对值越大,输出越强。
- 现象:
- 对应侧电机按 PWM 方向转动;
- 无闭环速度调节,低速和负载变化时表现可能不稳定。
PWM 开环控制不按目标线速度闭环调节,不建议作为首次运动测试。若必须排查电机驱动,请架空底盘,并从短时、低绝对值开始。
CMD_SET_MOTOR_PID - 设置底盘 PID
PID 是闭环控制参数,用于让实际电机速度接近目标速度。P 影响响应力度,I 用于修正长期误差,D 用于抑制变化趋势。PID 参数不合适时,可能出现抖动、响应迟钝、跑偏或电机持续受力。
base -c {"T":2,"P":20,"I":2000,"D":0,"L":255}
普通通信验证不要修改 PID。车轮不动、轮子偏斜、机械卡滞、供电不足或电机发热时,不要先改 PID。
云台控制
Web 页面中的云台方向按钮和键盘控制,主要表现为改变云台目标角度。按一次或按住时,云台会转向新的目标角度;达到目标角度后会停下。因此不要把 Web 页面方向按钮理解成“持续运动直到发送 T=135 停止”。
CMD_GIMBAL_CTRL_SIMPLE - 云台角度控制
base -c {"T":133,"X":0,"Y":0,"SPD":200,"ACC":10}
控制云台转到指定角度,可用于回中或小角度转动。
- 参数:
X为水平角度,Y为俯仰角度,SPD为速度参数,ACC为加速度参数。 - 推荐起步值:小角度、非零速度和加速度,例如
SPD=200,ACC=10。 - 取值范围 / 参考范围:
X=-180~180,Y=-30~90。SPD=0、ACC=0表示最快速度 / 加速度,初次测试不要使用最快。
每次动作前检查线缆和机械限位。
- 现象:云台按目标角度短时移动,达到目标角度后停下。
开发者补充:云台连续控制和 UI 控制接口
T=134 和 T=135 属于云台连续控制相关指令。它们与 Web 页面方向按钮的实际体验不完全相同。需要验证连续控制时,应单独测试,并确认停止方式。
base -c {"T":134,"X":45,"Y":45,"SX":300,"SY":300}
base -c {"T":135}
T=141 是底层 UI 控制接口,X/Y 可为 -1、0、1,SPD 为速度。普通操作优先使用 Web 页面云台按钮或 T=133 小角度验证。
base -c {"T":141,"X":0,"Y":0,"SPD":300}
灯光控制
Head Light / Base Light 控制
Web 页面中的 Head Light Ctrl 和 Base Light Ctrl 对应下位机两路灯光输出。底层指令仍使用 T=132,通过 IO4 和 IO5 设置两路输出亮度。
base -c {"T":132,"IO4":64,"IO5":64}
base -c {"T":132,"IO4":0,"IO5":0}
- 参数:
IO4、IO5为两路 PWM 输出。 - 推荐起步值:
64这类低亮度值。 - 取值范围 / 参考范围:
0~255,0表示关闭,255表示最大输出。
关闭灯光重新发送 JSON,把对应亮度设置为 0。不要长时间高亮;如果外接设备,需要额外确认供电和电流能力。
OLED 显示控制
CMD_OLED_CTRL - OLED 指定行显示
base -c {"T":3,"lineNum":0,"Text":"UGV TEST"}
base -c {"T":-3}
lineNum=0..3,其中0是最上方;Text为显示文本。建议使用短英文或数字,文本过长可能换行并影响其它行。UGV TEST可能只短暂显示,随后被 Web 主程序刷新为默认界面;默认界面中的STA/AP是当前网络模式显示,不是 OLED 文本指令切换网络模式。需要恢复默认显示时发送T=-3。
如果 Web 主程序仍在运行,OLED 通常会在几秒后自动刷新为网络状态界面。需要手动恢复默认显示时,在 Web 命令输入框发送:
base -c {"T":-3}
如果已经停止 Web 主程序并切换到 Notebook 直连串口,使用 base.base_default_oled() 恢复默认显示;需要继续显示 STA / AP、IP 和运行时间等动态状态时,重新启动 Web 主程序或重启设备。
挑战任务
用 Web 命令输入框完成一次低风险下位机通信验证:让 OLED 显示测试文本,短时间点亮灯光,然后恢复默认显示并关闭灯光。
目标: 使用 Web 命令输入框发送低风险 JSON 指令,确认上位机可以把指令转发到 ESP32 下位机。
完成标准: OLED 短暂显示指定文本,灯光亮度发生变化,测试结束后灯光关闭,OLED 恢复默认显示。 在 Web 页面底部找到命令输入框。 显示 OLED 文本。 低亮度点亮 Head Light / Base Light 对应输出。 关闭灯光。 恢复 OLED 默认显示。 现象: 排查:展开操作步骤
UGV TEST,随后可能恢复为默认界面并显示 STA 或 AP 等网络状态;
开发者补充:正常操作不需要修改的 JSON 指令
| 指令 | 用途 | 为什么不放主线 | 修改风险 |
|---|---|---|---|
T=13 | ROS 侧封装调用的线速度 / 角速度指令 | ROS 2 操作放在 ROS 2 教程 | 与 Web 或 Notebook 串口控制混用会增加排查难度。 |
T=136 | 设置运动心跳时间 | 普通操作通常保持默认 | 设置过长会降低安全性,设置过短可能导致运动断续。 |
T=138、T=139 | 设置 / 读取左右速度比率 | 只用于轻微跑偏校正 | 不用于解决机械卡死、车轮不动或电机发热。 |
T=126、T=130、T=131 | IMU 与底盘反馈读取 | 更适合 Notebook / Python 串口读取 | Web 命令输入框附近不一定显示返回内容。 |
T=142 | 设置连续反馈额外间隔 | 调整反馈频率时使用 | 设置不当会影响串口输出频率。 |
T=143 | 串口回显开关 | 通信排查时使用 | 长期开启会增加串口输出。 |
T=127、T=128、T=129 | IMU 校准 / 偏移量 | 当前普通功能验证不需要执行 | 修改偏移可能影响 IMU 数据。 |
T=4、T=900 | 产品和模块类型 | 开机流程会按配置发送 | 修改错误会影响后续模块控制。 |
T=401~408 | WiFi 设置 | 网络配置类操作 | 修改前要确认恢复方式。 |
T=501、T=210、T=502 | 舵机 ID、力矩、中位设置 | 舵机配置类操作 | 可能影响云台识别、保持力和回中。 |
T=300~306 | ESP-NOW 组网 / 广播控制 | 不属于本页主线测试 | 可能影响设备配对或广播控制。 |
什么是心跳时间
心跳时间是下位机等待新的运动指令的超时时间。若在设定时间内没有收到新的运动指令,下位机会停止运动,用于避免上位机断开或程序卡住后底盘持续运动。
base -c {"T":136,"cmd":3000}
其中 cmd=3000 表示约 3000 ms。普通操作通常不需要修改该值。
什么是串口回显
串口回显是让下位机把收到的指令再输出回来,用于确认指令是否到达下位机。它适合排查通信问题,不适合长期打开。
base -c {"T":143,"cmd":1}
base -c {"T":143,"cmd":0}
cmd=1 表示开启回显,cmd=0 表示关闭回显。Web 命令输入框不一定显示回显内容,建议在 Notebook / Python 串口读取流程中观察。
机械臂相关泛用指令存在于通用指令集中,但 UGV Rover PT 页面不展开机械臂控制流程。
使用 Notebook 操作下位机
读取反馈、直接发送运动指令或运行 Python 脚本时,会直接打开底盘串口。进入这些步骤前,先完成目标串口确认和占用释放。
串口占用检查与释放
确认目标串口
在 Jetson 终端或 JupyterLab Terminal 中列出当前存在的硬件串口:
ls /dev/ttyTHS*
如果输出中有多个串口,例如:
/dev/ttyTHS1 /dev/ttyTHS2
只选择准备给底盘通信使用的那个串口,不需要逐个释放所有串口。如果当前设备没有 /dev/ttyTHS0,不要继续使用 /dev/ttyTHS0。
下面以 /dev/ttyTHS1 为例。后续命令中的 PORT 和 Notebook 代码中的 SERIAL_PORT 应保持一致;如果你的设备使用其它串口,请替换为实际串口。
PORT=/dev/ttyTHS1
串口能打开只说明程序可以访问该串口,不代表它一定能控制底盘。后续低速短时动作有效后,再把该串口固定用于真实动作。
释放目标底盘串口
查看目标串口是否被占用:
sudo fuser -v $PORT
如果有输出,说明该串口正在被某个进程占用。准备切换到 Notebook 直连时,先尝试温和释放:
sudo fuser -TERM -k $PORT
sleep 2
sudo fuser -v $PORT
如果最后一条命令没有输出,说明串口已经释放,可以进入 Notebook 操作。
如果仍然有输出,再执行强制释放:
sudo fuser -KILL -k $PORT
sleep 1
sudo fuser -v $PORT
最后一次 sudo fuser -v $PORT 没有输出,才表示目标串口已经空闲。
sudo fuser -TERM -k $PORT 只会结束正在占用 $PORT 指向串口的进程。它比 sudo killall -9 python 更精确,不会主动结束所有 Python 程序。
如果需要确认占用者是谁,可以先通过 sudo fuser -v $PORT 找到 PID,再用 ps -fp <PID> -ww 查看该进程的完整命令。
如果继续使用 Web 页面、Web 命令输入框或 Web 视频流,不需要释放串口。只有准备切换到 Notebook / Python 直接打开底盘串口时,才释放目标串口。
不要使用 sudo killall -9 python 释放串口。它可能同时结束 Web 主程序、JupyterLab、Notebook Kernel、jtop 和其它 Python 脚本。
需要释放串口时,只释放目标串口对应的占用进程。
Notebook 连接下位机
完成串口释放后,回到 Jupyter Notebook,新建代码单元格运行下面代码。不要把这段 Python 代码粘贴到 Terminal。
这段代码会进入 ~/ugv_jetson 项目目录,导入 BaseController,打开刚才释放的目标串口,并先发送停止指令。
from pathlib import Path
import os
import sys
import time
PROJECT_DIR = Path("~/ugv_jetson").expanduser()
os.chdir(PROJECT_DIR)
if str(PROJECT_DIR) not in sys.path:
sys.path.insert(0, str(PROJECT_DIR))
from base_ctrl import BaseController
SERIAL_PORT = "/dev/ttyTHS1" # 与前面释放的 PORT 保持一致
BAUDRATE = 115200
base = BaseController(SERIAL_PORT, BAUDRATE)
base.send_command({"T": 1, "L": 0, "R": 0})
time.sleep(0.1)
print("下位机串口已连接,并已发送停止指令。")
运行后应显示 下位机串口已连接,并已发送停止指令。。本示例只发送停止指令,车轮不会转动;如果出现串口报错,请确认 PORT 和 SERIAL_PORT 使用同一个目标串口。
Notebook 通用控制函数
前一段代码中已经运行过:
base = BaseController(SERIAL_PORT, BAUDRATE)
这里的 base 是 Notebook 中保存的下位机控制对象,已经打开 SERIAL_PORT 指向的底盘串口。后续发送运动、OLED、灯光、云台和读取反馈时,都通过这个 base 对象完成。
同一个 Notebook 中只创建一次 BaseController(...)。后续代码复用 base 发送指令,不要反复打开同一个串口;如果前一个连接没有关闭,可能导致串口占用或打开失败。
def send_cmd(cmd):
base.send_command(cmd)
time.sleep(0.05)
def stop():
base.send_command({"T": 1, "L": 0, "R": 0})
time.sleep(0.1)
stop()
print("通用控制函数已准备完成。")
运行后应显示 通用控制函数已准备完成。。如果提示 NameError: name 'base' is not defined,说明前面的 Notebook 连接下位机 代码单元格没有成功运行。请先回到前一节,确认 base = BaseController(SERIAL_PORT, BAUDRATE) 已经执行完成。
低速运动测试
本节从 Notebook 直接向下位机发送 JSON,不经过 Web 页面。Web 页面按钮和键盘控制请参考前面的 Web 端 JSON 控制部分。方向不确定时先架空底盘。
运行本小节任意代码前,必须先完成 Notebook 连接下位机 和 Notebook 通用控制函数。如果提示 send_cmd 或 stop 未定义,请回到这两个小节按顺序运行代码。
停止运动
stop()
print("已发送停止指令。")
低速前进 0.3 秒
TEST_SPEED = 0.03
ACTION_TIME = 0.3
try:
print("低速前进")
send_cmd({"T": 1, "L": TEST_SPEED, "R": TEST_SPEED})
time.sleep(ACTION_TIME)
finally:
stop()
print("已停止")
低速原地左转 0.3 秒
try:
print("低速原地左转")
send_cmd({"T": 1, "L": -TEST_SPEED, "R": TEST_SPEED})
time.sleep(ACTION_TIME)
finally:
stop()
print("已停止")
低速原地右转 0.3 秒
try:
print("低速原地右转")
send_cmd({"T": 1, "L": TEST_SPEED, "R": -TEST_SPEED})
time.sleep(ACTION_TIME)
finally:
stop()
print("已停止")
挑战任务:让 UGV 走出一个小 L 形路线
目标: 使用 Notebook 中已经定义好的 send_cmd() 和 stop(),组合出一个简单的低速路线动作。
UGV 将依次执行:
低速前进 → 停止 → 原地左转 → 停止 → 再低速前进 → 停止
本任务不追求精准 90° 转角,也不要求固定距离。重点是理解每个动作段都要短时运行,并在每段后发送停止指令。 运行下面代码前,先完成 Notebook 连接下位机 和 Notebook 通用控制函数。否则会出现 按顺序运行下面几个代码单元格。若某一段动作不符合预期,先停在当前单元格排查,不要继续运行后面的动作。 Cell 1:设置路线参数 Cell 2:第 1 段低速前进 Cell 3:第 2 段原地左转 Cell 4:第 3 段再次低速前进 Cell 5:确认停止展开 Notebook 代码
NameError: name 'send_cmd' is not defined 或 NameError: name 'stop' is not defined。
现象:
- UGV 先向前移动一小段;
- 停止后原地向左转一小段;
- 再向新的方向前进一小段;
- 每一段动作结束后都会停止。
可调整参数:
L_ROUTE_SPEED:路线速度,首次保持0.03;L_ROUTE_TIME:前进动作时间,首次保持0.3;TURN_TIME:转向动作时间,首次保持0.3。
不要一开始同时调大速度和时间。需要微调路线时,优先小幅调整 L_ROUTE_TIME 或 TURN_TIME,每次只改一个参数。如果 0.03 没有看到明显转动,不要马上大幅增加速度,先参考 JSON 指令发出后车轮不动怎么办? 检查。
如果出现一侧轮子不动、方向相反、抖动、异响、发热或系统重启,请立即停止测试,检查供电、电机、线缆、参数和下位机反馈。
Notebook 中读取下位机反馈
读取类 JSON 更适合在 Notebook / Python 直连串口时观察返回数据。Web 命令输入框可以发送读取指令,但页面不一定会在输入框附近显示返回内容。
运行本小节代码前,必须先完成 Notebook 连接下位机 和 Notebook 通用控制函数。如果提示 send_cmd、stop 或 base 未定义,请回到这两个小节按顺序运行代码。
编码器用于估计轮子转动和行驶距离,IMU 用于感知角速度、加速度和姿态变化。ROS 2 中的里程计、IMU topic 检查请参考 ROS 2 教程。
单次读取底盘反馈
send_cmd({"T": 130})
data = base.feedback_data()
print(data)
返回内容通常类似:
{'T': 1001, 'L': 0, 'R': 0, 'ax': 92, 'ay': -440, 'az': 8306, 'gx': 2, 'gy': -7, 'gz': 12, 'mx': -622, 'my': 457, 'mz': 2498, 'odl': 0, 'odr': 2, 'v': 1197}
| 参数 | 含义 |
|---|---|
T | 返回数据类型,1001 表示底盘反馈数据。 |
L / R | 左右侧轮速或运动反馈值,静止时通常接近 0。 |
ax / ay / az | 加速度计三轴原始值。 |
gx / gy / gz | 陀螺仪三轴原始值。 |
mx / my / mz | 磁力计三轴原始值,容易受周围磁场影响。 |
odl / odr | 左右侧编码器 / 里程计相关值,轮子转动时会变化。 |
v | 底盘电压反馈值,单位为 0.01V,例如 1197 约为 11.97V。 |
如果输出为 None 或没有明显数据,请确认串口连接、底盘供电和反馈状态。
连续反馈读取
连续反馈适合短时间观察数据变化。读取完成后要关闭连续反馈。
try:
send_cmd({"T": 131, "cmd": 1})
for _ in range(20):
data = base.feedback_data()
print(data)
time.sleep(0.1)
finally:
send_cmd({"T": 131, "cmd": 0})
print("已关闭连续反馈。")
读取 IMU 数据
send_cmd({"T": 126})
imu_data = base.feedback_data()
print(imu_data)
地面打滑、轮径参数、轮距参数和 IMU 安装方向都会影响里程计。发现数据异常时,不要只改算法参数,也要先检查机械和传感器安装。
OLED、灯光与云台封装函数
本节只使用前面创建的 base 对象和 Python 封装函数,不再重复 Web 命令输入框操作。
运行本小节代码前,必须先完成 Notebook 连接下位机 和 Notebook 通用控制函数。如果提示 base 未定义,请先回到前面的连接步骤。
OLED 显示
try:
base.base_oled(0, "UGV TEST")
time.sleep(1)
finally:
base.base_default_oled()
运行后,OLED 第 0 行会短暂显示 UGV TEST。随后 base.base_default_oled() 会发送恢复默认显示指令,OLED 会回到下位机默认显示界面。
默认显示界面可能包含网络模式、状态信息或其它系统默认内容,例如 STA、AP 等。但不影响后续的使用,如果需要恢复之前的OLED显示界面,可以重启设备再重新设置。
灯光控制
try:
base.lights_ctrl(64, 64)
time.sleep(1)
finally:
base.lights_ctrl(0, 0)
云台小角度控制
本示例使用 gimbal_ctrl(x, y, speed, acceleration),它会通过 T=133 云台角度控制 指令让云台转到指定角度。x 为水平角度,y 为俯仰角度,speed 为速度参数,acceleration 为加速度参数。先使用小角度、非最快速度测试,避免线缆或结构件干涉。
try:
base.gimbal_ctrl(0, 0, 200, 10)
time.sleep(1)
base.gimbal_ctrl(10, 0, 200, 10)
time.sleep(1)
base.gimbal_ctrl(0, 0, 200, 10)
finally:
base.gimbal_ctrl(0, 0, 200, 10)
不要用手强行掰动云台。若云台抖动、卡住或线缆被拉扯,请立即停止控制。
结束 Notebook 下位机控制
完成 Notebook 控制后,先恢复到安全状态:
运行下面代码前,需要已经完成 Notebook 连接下位机 和 Notebook 通用控制函数。
try:
stop()
base.lights_ctrl(0, 0)
base.base_default_oled()
finally:
base.gimbal_dev_close()
print("已停止底盘、关闭灯光,并关闭串口。")
gimbal_dev_close() 用于关闭当前串口连接。函数名称保留封装中的命名,不需要修改源码。
如果后续要重新使用 Web 页面控制底盘,需要重新启动 Web 主程序,或重启设备。不要让 Web 主程序和 Notebook 同时打开同一个底盘串口。
开发者补充:自定义串口客户端
本页主线使用 BaseController 封装操作下位机。只有需要开发自定义串口通信逻辑时,才参考下面的自定义串口客户端模板。
该模板默认 dry_run,只查看或生成 JSON 指令,不直接打开串口;进入真实发送前再完成 串口占用检查与释放。
- 先保持
dry_run=True运行脚本。 确认不会控制硬件。 - 确认是否需要真实发送。 只查看 JSON 或 dry-run 时,不需要释放串口。
- 真实发送前检查串口占用。 按 串口占用检查与释放 处理。
- 确认串口可用后。 才改为
dry_run=False。 - 任何异常退出都必须发送停止指令。
import json
import time
from dataclasses import dataclass
import serial
@dataclass
class SerialConfig:
port: str = "/dev/ttyTHS1" # 按串口检查结果修改
baudrate: int = 115200
timeout_s: float = 1.0
dry_run: bool = True
class LowerComputerClient:
def __init__(self, config: SerialConfig):
self.config = config
self.ser = None
if not config.dry_run:
self.ser = serial.Serial(config.port, config.baudrate, timeout=config.timeout_s)
def send(self, command: dict):
line = json.dumps(command, ensure_ascii=False) + "\n"
if self.config.dry_run:
print("[DRY-RUN]", line.strip())
return
self.ser.write(line.encode("utf-8"))
def read_line(self):
if self.config.dry_run:
return {"status": "dry-run"}
raw = self.ser.readline().decode("utf-8", errors="replace").strip()
if not raw:
return None
return json.loads(raw)
def stop(self):
self.send({"T": 1, "L": 0, "R": 0})
def close(self):
if self.ser:
self.ser.close()
def main():
client = LowerComputerClient(SerialConfig(dry_run=True))
try:
client.send({"T": 3, "lineNum": 0, "Text": "UGV TEST"})
client.send({"T": 132, "IO4": 64, "IO5": 64})
time.sleep(0.5)
client.send({"T": 132, "IO4": 0, "IO5": 0})
print(client.read_line())
finally:
client.stop()
client.close()
if __name__ == "__main__":
main()