二次开发
二次开发先从独立项目目录、dry-run、日志和停止方式开始,不要从修改系统核心文件开始。
Notebook 验证想法
↓
脚本封装功能
↓
Web / API 接入交互
↓
日志记录结果
↓
需要时再部署自启动
涉及底盘、云台、串口、ROS 2 或开机自启动的修改,都应先保留停止方式和恢复方式。
不建议第一次二次开发就修改 app.py、base_ctrl.py、cv_ctrl.py、ROS 2 驱动或下位机固件。先复制示例到自己的项目目录,用 dry-run、日志和低风险硬件验证思路。
二次开发原则
- 先确认当前功能运行在浏览器、JupyterLab、Jetson 主机、ROS 2 Docker 还是 ESP32 下位机。
- 先复制示例,不在原始文件中长期堆积实验代码。
- 运动类逻辑默认 dry-run,先输出计划,不发送真实运动指令。
- 接入真实控制前,先完成限幅、超时、停止方式和日志记录。
- 修改自启动前,先确认脚本可手动运行、可停止、可禁用。
- 修改前备份文件,记录命令、参数、日志和回退方法。
开发能力地图
| 开发对象 | 所在环境 | 常用入口 | 适合改什么 | 不建议改什么 |
|---|---|---|---|---|
| Web 控制端 | Jetson 主机 | ugv_jetson/app.py、templates/ | 页面、按钮、状态显示、独立 API | 第一次开发就替换主控制流程 |
| Notebook 示例 | Jetson 主机 / JupyterLab | tutorial_cn/ | 视觉识别、参数实验、图像显示 | 长时间无人值守或连续控制底盘 |
| 下位机 JSON | Jetson → ESP32 | Web 命令框、Python 串口调用 | LED、OLED、云台、低风险动作 | 未限幅的底盘运动 |
| ROS 2 节点 | ROS 2 Docker | ugv_ws | topic 订阅、状态监控、导航扩展 | 直接修改驱动核心 |
| 自启动任务 | Jetson 主机 | systemd、crontab | 数据记录、状态监控、独立 API | 开机后自动驱动底盘 |
| 前端二次开发 | 浏览器 / Flask | templates/、独立端口 | 状态面板、参数表单、低风险按钮 | 删除原页面安全逻辑 |
开发环境分层
| 场景 | 推荐环境 | 说明 |
|---|---|---|
| 访问 Web 页面、查看视频流 | Windows / 浏览器 | 不在这里运行 ROS 2 命令 |
| 编写 Notebook、视觉脚本、普通 Python 任务 | Jetson 主机 / JupyterLab | 适合上位机视觉和数据记录 |
检查 /dev/ttyTHS* 串口占用 | Jetson 主机 | 不建议在 ROS 2 Docker 中做主机串口释放判断 |
运行 ros2 topic、ros2 launch、Nav2、SLAM | ROS 2 Docker 容器 | 每个终端先加载 ROS 2 环境 |
| 修改或烧录 ESP32 固件 | 下位机开发环境 | 不属于普通上位机脚本修改 |
先确认自己当前在哪个环境,再执行命令。不要把 ROS 2 Docker 命令、Jetson 主机命令和 Windows 命令混用。
产品初始化与系统恢复
产品初始化用于恢复出厂系统、系统更新、替换 NVMe 固态硬盘,或自备 Jetson Orin Nano 核心板后重新配置环境。普通二次开发不需要先执行初始化。
系统烧录、镜像恢复和软件重装可能覆盖现有文件。操作前先备份项目代码、地图、Notebook、日志和自定义服务配置。
初始化分为两类:
| 场景 | 推荐处理方式 | 说明 |
|---|---|---|
| 恢复到官方预配置环境 | 使用 资料下载 中的系统镜像 | 适合恢复整机软件环境,通常需要拆下 NVMe 并使用硬盘烧录器 |
| 自备 Jetson Orin Nano 核心板 | 先通过 NVIDIA SDK Manager 安装系统,再安装上位机和 ROS 2 程序 | 适合开发者自备核心板或需要重新配置系统的情况 |
| 只修改了少量脚本或页面 | 优先使用备份文件或 Git 回退 | 不建议直接重装系统 |
初始化前备份
开始初始化前,至少备份以下内容:
~/ugv_jetson中修改过的 Web、Notebook、脚本和配置;/home/ws/ugv_ws中修改过的 ROS 2 包、地图和脚本;map.pgm、map.yaml、~/.ros/rtabmap.db等地图文件;~/ugv_projects/中的自定义项目、日志和数据;- 自定义
systemd、crontab、网络配置和账号信息。
核心板系统安装概览
给 Jetson Orin Nano 安装系统时,需要一台 Ubuntu 22.04 主机或虚拟机、NVIDIA 账号、黄色跳线帽、USB 数据线和稳定供电。SDK Manager 下载入口见 资料下载。
-
关闭 UGV 电源,用跳线帽或杜邦线短接
FC REC和GND,让核心板进入 Recovery Mode。
-
打开 UGV 电源,用 USB 数据线连接 Jetson 主板的 Type-C 数据口和 Ubuntu 22.04 主机。
-
在 Ubuntu 主机中运行
lsusb,确认能识别到 NVIDIA 设备。 -
安装并打开 NVIDIA SDK Manager,登录 NVIDIA 账号。


-
如果 SDK Manager 没有自动识别 Jetson 设备,先确认 Recovery Mode 和 USB 连接,再点击刷新。

-
选择 Jetson Orin Nano 对应设备和 JetPack 版本。开发套件场景选择 Developer Kit;Host Machine 组件按需取消勾选。

-
在组件选择页面保留 Jetson Linux,并按实际需要选择 Runtime / SDK 组件。确认协议后继续。

-
首次使用 SDK Manager 烧录时,按提示创建安装配置并继续。

-
在预配置页面选择开发板类型,
OEM Configuration可选择Pre-Config,并填写系统用户名和密码。
-
等待系统烧录完成。完成后再进行上位机软件和 ROS 2 工作空间恢复。

上位机软件恢复概览
上位机软件恢复有两种方式:
- 使用 资料下载 中的系统镜像,将已配置好的系统写入 NVMe 固态硬盘。该方式会覆盖硬盘原有数据,写入前先备份。
- 在已安装 Ubuntu 的 Jetson 系统中手动配置
ugv_jetson、ugv_ws、依赖、服务和工作目录。该方式适合需要保留自定义系统环境的开发场景,操作前先记录当前镜像版本、Python 依赖、ROS 2 环境和服务配置。
恢复完成后,按以下顺序检查:
- Web 页面和 JupyterLab 是否可访问;
- 摄像头、云台、LED、OLED 是否能执行低风险动作;
- 底盘串口是否正常,停止方式是否可用;
- ROS 2 Docker、
ugv_ws、雷达、TF、SLAM 和 Navigation 是否按 ROS 2 教程 验证。
开发前检查清单
- 当前功能运行在 Jetson 主机、ROS 2 Docker、浏览器还是 ESP32 下位机?
- 是否会占用底盘串口?
- 是否会占用摄像头?
- 是否会发布
/cmd_vel或 JSON 运动指令? - 是否已经有 dry-run?
- 是否已经有限幅、超时和停止逻辑?
- 是否有日志路径?
- 是否知道如何停止当前程序?
- 是否知道如何恢复原 Web / ROS 2 / 自启动配置?
- 是否已经备份要修改的文件?
源码结构导览
常见结构如下,以当前设备中的实际目录为准。
ugv_jetson/
├─ app.py
├─ base_ctrl.py
├─ cv_ctrl.py
├─ audio_ctrl.py
├─ templates/
├─ static/
├─ tutorial_cn/
├─ sounds/
└─ config.yaml
ugv_ws/
├─ src/ugv_main/ugv_bringup
├─ src/ugv_main/ugv_tools
├─ src/ugv_main/ugv_nav
├─ src/ugv_main/ugv_slam
├─ src/ugv_main/ugv_vision
├─ src/ugv_main/ugv_gazebo
└─ install/
| 文件或目录 | 推荐先看什么 | 适合扩展什么 | 不建议直接改什么 |
|---|---|---|---|
app.py | Web 路由、命令入口、自启动日志 | 独立接口、状态查询 | 主控制流程和安全停止逻辑 |
base_ctrl.py | JSON 发送方式、串口初始化 | 上层封装调用 | 串口协议核心和停止逻辑 |
cv_ctrl.py | 视觉模式、摄像头入口 | 识别参数和结果显示 | 相机初始化和线程核心 |
audio_ctrl.py | 音频播放入口 | 语音提示、状态播报 | 音频设备初始化逻辑 |
templates/ | 页面结构、按钮和表单 | 状态卡片、参数面板 | 删除原控制页面安全逻辑 |
config.yaml | 按钮、参数和反馈配置 | 复制后做参数实验 | 未备份时修改原配置 |
tutorial_cn/ | Notebook 示例 | 复制到个人目录后实验 | 在原示例中长期保存项目代码 |
sounds/ | 默认音频文件 | 新增提示音 | 删除默认音频 |
ugv_ws/src/ugv_main/ugv_tools | 行为控制和工具节点 | 自定义工具节点参考 | 直接改已验证工具入口 |
ugv_ws/src/ugv_main/ugv_nav | 导航参数和 launch 入口 | 导航扩展参考 | 未验证时修改 Nav2 参数 |
ugv_ws/src/ugv_main/ugv_vision | ROS 2 视觉节点 | 只读识别、结果发布 | 默认控制底盘 |
常用接口索引
| 类型 | 接口 / 文件 | 用途 | 风险 |
|---|---|---|---|
| Web API | /send_command | 调用已有 Web 命令 | 中 |
| 下位机 JSON | {"T": ...} | 控制底盘、LED、OLED、云台等 | 高 |
| ROS 2 topic | /voltage | 电压监控 | 低 |
| ROS 2 topic | /scan | 雷达数据 | 低 |
| ROS 2 topic | /odom | 里程计 | 中 |
| ROS 2 topic | /cmd_vel | 底盘速度控制 | 高 |
| ROS 2 topic | /robot_pose | 当前地图位姿 | 中 |
| ROS 2 topic | /goal_pose | 发布导航目标 | 高 |
| ROS 2 action | /behavior | 行为控制、保存点、发布点 | 高 |
| 文件 | map.pgm / map.yaml | Navigation 地图 | 中 |
| 日志 | ~/ugv_projects/<project>/logs/ | 自定义项目日志 | 低 |
高风险接口在接入前必须先完成停止方式、限幅、低速和小范围验证。
按目标选择开发路径
| 想实现的功能 | 推荐入口 | 先看哪个教程 | 二次开发重点 | 风险 |
|---|---|---|---|---|
| 修改颜色识别、运动检测、手势识别等视觉逻辑 | Notebook 原型开发 | 上位机教程 | 先可视化结果,再整理成脚本 | 中 |
| 给机器人增加一个 Web 按钮或参数入口 | Web 命令与接口调用 | 入门教程、上位机教程 | 先调用已有命令,再新增独立接口 | 中 |
| 制作自动拍照、巡检、数据记录任务 | 自定义 Python 任务脚本 | 上位机教程 | YAML 配置、dry-run、日志、停止方式 | 中 |
| 接入 ROS 2、订阅话题或写自定义节点 | ROS 2 二次开发 | ROS 2 教程 | 先只读 topic,再发布控制 | 高 |
| 扩展底盘、灯光、OLED、云台 JSON 指令 | 下位机 JSON / 硬件扩展 | 下位机教程 | 先测停止、LED、OLED,再测运动 | 高 |
| 开机运行自己的功能 | 部署、自启动与恢复 | 部署、自启动与恢复 | systemd / crontab、日志、禁用方式 | 高 |
| 记录长时间运行数据 | Notebook 或任务脚本 | 上位机教程 | CSV / JSONL 日志、时间戳、异常记录 | 低 |
| 处理突然断电、电压过低问题 | 电压记录与监控 | 入门教程、上位机教程 | 先观察 V_IN,再做日志或监控扩展 | 中 |
推荐开发流程
- 先完成基础验证。 确认 Web、JupyterLab、相机、底盘停止、低速运动和 ROS 2 基础功能是否正常。
- 复制示例,不直接改原文件。 将 Notebook、脚本或配置复制到自己的项目目录。
- 先 dry-run。 运动类功能先只输出将要执行的命令。
- 再低风险硬件验证。 先 LED、OLED、拍照,再云台,最后底盘。
- 记录参数和结果。 使用 CSV、JSONL 或 Markdown 记录时间、参数、现象和错误。
- 整理成脚本。 将验证通过的 Notebook 逻辑封装为
.py文件。 - 再接入 Web 或 ROS 2。 不要一开始就改 Web 主程序或 ROS 2 驱动。
- 最后考虑自启动。 确认有停止方式、日志和恢复方案后再加入 systemd 或 crontab。
路径 1:Notebook 原型开发
Notebook 适合算法验证、图像显示、参数调试和日志记录。摄像头读取、OpenCV 与 MediaPipe 的基础流程见 上位机教程。
适合场景:
- 视觉识别、手势识别、颜色阈值、图像显示和数据曲线调试;
- 单次动作建议、dry-run 输出、参数对比;
- 将实验结果记录为截图、CSV 或 Markdown。
不适合场景:
- 长时间无人值守任务;
- 开机自动运行任务;
- 直接连续控制底盘。
推荐做法:
- 将示例 Notebook 复制到
~/ugv_projects/<project>/notebooks/。 - 第一格写清楚实验目标、硬件状态、是否会控制底盘。
- 先显示图像、关键点、Mask、曲线或日志,不发送运动指令。
- 涉及底盘或云台时,先按 Notebook 中的安全底盘联动规则 处理。
- 验证通过后,把核心逻辑提取为
.py脚本。
DRY_RUN = True
def send_plan(name, command):
if DRY_RUN:
print("[DRY-RUN]", name, command)
return
raise RuntimeError("接入真实控制前,先完成停止方式和低速验证。")
send_plan("stop", {"T": 1, "L": 0, "R": 0})
观察点:
- Notebook 中能看到图像、识别框、关键点、曲线或文本状态;
- dry-run 能输出将要发送的 JSON 或速度参数;
- 运动类逻辑只在完成 底盘串口确认与占用排查 后再接入真实动作。
路径 2:Web 命令与接口调用
Web 命令和接口适合把已经验证过的功能接入按钮、参数框或远程调用入口。Web 控制页面、命令输入框和接口说明见 上位机教程。
适合场景:
- 给已有功能增加一个入口,例如语音提示、状态显示、拍照或参数切换;
- 通过
/send_command调用已有命令; - 用独立接口验证自己的参数和返回值。
不适合场景:
- 第一次开发就改原始
app.py主流程; - 在没有停止按钮和参数限幅时接入底盘运动;
- 让 Web 页面和 Notebook 同时打开同一个底盘串口。
推荐做法:
- 先使用低风险命令验证交互,例如语音、状态显示、视频质量、OLED 或 LED。
- 底盘 JSON、巡线参数和云台跟随参数放到进阶验证。
- 新增接口先运行在独立端口,不覆盖原 Web 主程序。
- 接入控制前增加参数限幅、dry-run、停止按钮和异常返回。
import requests
ROBOT_IP = "192.168.50.5"
WEB_PORT = 5000
DRY_RUN = True
def post_command(command):
url = f"http://{ROBOT_IP}:{WEB_PORT}/send_command"
if DRY_RUN:
print("[DRY-RUN] POST", url, {"command": command})
return {"dry_run": True, "command": command}
response = requests.post(url, data={"command": command}, timeout=2)
response.raise_for_status()
return response.text
post_command("audio -s hello")
观察点:
- 先验证接口返回值和日志;
- 再验证低风险外设;
- 运动类命令只输出预览,完成停止方式验证后再接入真实控制。
路径 3:自定义 Python 任务脚本
任务脚本适合把 Notebook 中已经验证的逻辑整理成可重复运行的应用,例如自动拍照、数据记录、巡检、识别结果保存或定时状态监控。
适合场景:
- 固定流程任务;
- 需要配置文件、日志和错误记录的任务;
- 需要在终端、SSH 或自启动服务中运行的任务。
不适合场景:
- 仍在频繁改参数和观察图像的算法调试;
- 缺少停止方式的连续运动任务;
- 未记录日志的无人值守任务。
推荐项目目录:
mkdir -p ~/ugv_projects/my_project/{scripts,configs,logs,data,assets}
配置文件示例:
project_name: my_project
dry_run: true
log_dir: ~/ugv_projects/my_project/logs
max_speed: 0.05
duration_sec: 0.3
脚本模板:
import json
import logging
import time
from pathlib import Path
import yaml
CONFIG_PATH = Path("~/ugv_projects/my_project/configs/config.yaml").expanduser()
def load_config(path):
with path.open("r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {}
config.setdefault("dry_run", True)
config.setdefault("log_dir", "~/ugv_projects/my_project/logs")
return config
def setup_logging(log_dir):
log_dir.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
filename=log_dir / "task.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logging.getLogger().addHandler(logging.StreamHandler())
class RobotClient:
def __init__(self, config):
self.config = config
def send_json(self, command):
if self.config["dry_run"]:
logging.info("[DRY-RUN] %s", json.dumps(command, ensure_ascii=False))
return
raise RuntimeError("接入真实发送接口前,先完成停止和低速验证。")
def safe_stop(self):
self.send_json({"T": 1, "L": 0, "R": 0})
def run_task(robot, config):
robot.send_json({"T": 1, "L": 0, "R": 0})
time.sleep(float(config.get("duration_sec", 0.3)))
def main():
config = load_config(CONFIG_PATH)
setup_logging(Path(config["log_dir"]).expanduser())
robot = RobotClient(config)
try:
run_task(robot, config)
except Exception:
logging.exception("task failed")
raise
finally:
robot.safe_stop()
if __name__ == "__main__":
main()
观察点:
dry_run: true时只输出命令和日志;- 日志文件中能看到启动、参数、命令、异常和停止;
- 确认脚本可手动停止后,再接入 Web、ROS 2 或自启动。
路径 4:自定义 Web 页面
自定义 Web 页面适合把任务脚本或已验证功能整理成按钮、状态卡片和参数输入框。先做独立页面或独立端口,验证后再考虑合入原 Web 主程序。
适合场景:
- 给任务脚本增加远程启动、停止和状态查看;
- 为参数调试增加表单;
- 把日志、状态或识别结果展示到页面。
不适合场景:
- 替换原始 Web 页面;
- 没有停止按钮的运动控制页面;
- 将未验证的视觉、底盘和自启动逻辑一次性接入。
独立 Flask API 模板:
from flask import Flask, jsonify, request
app = Flask(__name__)
DRY_RUN = True
MAX_SPEED = 0.10
MAX_DURATION = 0.5
def clamp(value, low, high):
return max(low, min(high, value))
@app.get("/api/status")
def status():
return jsonify({"ok": True, "dry_run": DRY_RUN})
@app.post("/api/stop")
def stop():
command = {"T": 1, "L": 0, "R": 0}
return jsonify({"dry_run": DRY_RUN, "command": command})
@app.post("/api/move")
def move():
data = request.get_json(silent=True) or {}
speed = clamp(float(data.get("speed", 0.0)), -MAX_SPEED, MAX_SPEED)
duration = clamp(float(data.get("duration", 0.2)), 0.0, MAX_DURATION)
command = {"T": 1, "L": speed, "R": speed, "duration": duration}
return jsonify({"dry_run": DRY_RUN, "command": command})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
观察点:
python flask_dev_api.py
curl http://127.0.0.1:5001/api/status
curl -X POST http://127.0.0.1:5001/api/stop
curl -X POST http://127.0.0.1:5001/api/move -H "Content-Type: application/json" -d '{"speed":0.05,"duration":0.2}'
确认独立接口、限幅和 dry-run 正常后,再考虑增加前端页面或合入主程序。
路径 5:ROS 2 二次开发
ROS 2 二次开发适合订阅传感器数据、编写自定义节点、记录 rosbag、扩展导航或与外部系统联动。ROS 2 的启动、topic、SLAM 和 Navigation 流程见 ROS 2 教程。
适合场景:
- 订阅
/voltage、雷达、IMU、图像、里程计或 TF 数据; - 编写只读监控节点;
- 记录 rosbag 复现问题;
- 在完成基础验证后发布控制消息。
不适合场景:
- 未完成 ROS 2 基础启动就修改 launch 或驱动;
- 在未确认底盘安全时发布速度;
- 同时让 Web 主程序、Notebook 和 ROS 2 抢占同一串口或摄像头资源。
推荐做法:
- 按 ROS 2 教程 进入 Docker 或 ROS 2 终端。
- 先运行
ros2 topic list和只读 echo。 - 自定义节点先订阅状态,不发布控制。
- 发布速度前,先确认底盘停止、低速测试和测试场地。
- 修改 launch、参数或驱动前,先复制文件并记录回退方式。
创建只读 ROS 2 节点
第一版自定义节点建议只订阅 topic,不发布控制消息,不发布 /cmd_vel。
cd /home/ws/ugv_ws/src
ros2 pkg create ugv_custom_monitor --build-type ament_python --dependencies rclpy std_msgs nav_msgs sensor_msgs
只读电压监控节点示例可命名为 voltage_monitor。如果当前 /voltage 的消息类型不同,先用 ros2 topic info /voltage 核对后再修改订阅类型。
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
class VoltageMonitor(Node):
def __init__(self):
super().__init__("voltage_monitor")
self.create_subscription(Float32, "/voltage", self.on_voltage, 10)
def on_voltage(self, msg):
self.get_logger().info(f"voltage: {msg.data:.2f} V")
def main(args=None):
rclpy.init(args=args)
node = VoltageMonitor()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()
在包的 setup.py 中配置 console script 后,构建并运行:
cd /home/ws/ugv_ws
colcon build --packages-select ugv_custom_monitor
source install/setup.bash
ros2 run ugv_custom_monitor voltage_monitor
观察点:
ros2 topic list能看到预期 topic;- 只读订阅节点能输出数据;
- rosbag 能记录并回放;
- 速度发布逻辑先使用 dry-run 或仿真输出。
ROS 2 Web 可视化与 API 接入
ROS 2 Web App / Vizanti 适合做浏览器端 ROS 2 可视化、远程状态查看、教学演示和轻量调试。它可以显示 RobotModel、TF、LaserScan、Map、Odometry、Path 和 Navigation 状态,也可以作为自定义 ROS 2 面板的入口。
它不替代上位机 Web 主页面,不替代 JupyterLab,也不替代 RViz 的完整调试能力。第一次调通 SLAM / Navigation 时仍建议优先使用 RViz。
启动 Vizanti
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 launch ugv_web_app bringup.launch.py host:=0.0.0.0
浏览器访问:
http://<UGV_IP>:5100
Vizanti 页面通常使用 5100 端口;rosbridge websocket 通常使用 5001。页面能打开但没有内容时,先检查是否有 ROS 2 topic,以及页面中是否已经添加显示组件。
| 目标 | 需要先启动 | Vizanti 中添加 |
|---|---|---|
| 查看机器人模型 | robot_state_publisher / robot_description | RobotModel |
| 查看 TF | /tf、/tf_static | TF |
| 查看雷达 | bringup / lidar | LaserScan /scan |
| 查看 2D 地图 | SLAM 或 Navigation | Map /map |
| 查看里程计 | bringup / base node | Odometry /odom |
| 查看导航路径 | Navigation | Path / Goal / Navigation 相关组件 |
ROS 2 topic 到 Web API 的接入边界
二次开发中可以把 ROS 2 topic、action 或 service 封装成 Web API,但建议按“只读 → dry-run → 限幅 → 真实控制”的顺序推进。
优先从只读状态开始,例如 /voltage、/odom、/robot_pose、/scan 的摘要信息。可先设计以下只读接口:
/api/status
/api/voltage
/api/pose
/api/ros_topics
涉及 /cmd_vel、/goal_pose、/behavior 或 /send_command 的 API 属于高风险接口。不要第一次就接真实底盘。必须先完成 dry-run、参数限幅、超时停止、日志记录和人工停止方式。
| 接口 | 风险 | 接入要求 |
|---|---|---|
/cmd_vel | 高 | 限速、限时、finally 停止、人工断电方式 |
/goal_pose | 高 | 先确认 Navigation 地图、AMCL、costmap 正常 |
/behavior | 高 | 先验证 save / pub 链路,不把回点作为默认保证 |
/send_command | 中 / 高 | 先低风险命令,再运动命令 |
/voltage | 低 | 可作为只读状态接口 |
Web AI / 外部 API 接入边界
Web AI 属于可选高级二次开发功能,依赖额外 AI 服务配置。AI 输出不能直接无保护地转换成底盘运动、导航目标或下位机 JSON 指令。
推荐顺序:
- 只做问答或状态解释;
- 只读查询 ROS 2 状态;
- dry-run 输出将要执行的动作;
- 人工确认后执行低风险动作;
- 涉及底盘或导航时必须有限幅、超时、停止和日志。
| 内容 | ROS 2 教程 | 二次开发 |
|---|---|---|
ugv_web_app 是什么 | 简短说明 | 详细说明 |
| 启动 Vizanti | 最小命令 | 可重复测试流程 |
| 页面空白为什么正常 | 简短说明 | 组件和 topic 对应关系 |
添加 RobotModel / TF / LaserScan / Map | 最小观察点 | 完整用途表 |
| 自定义 ROS 2 Web 面板 | 不展开 | 展开 |
| ROS 2 topic 接 Web API | 不展开 | 展开 |
/cmd_vel、/goal_pose、/behavior 接 API | 不展开 | 高风险开发说明 |
Web AI / ugv_chat_ai | 只跳转 | 可选高级功能 |
路径 6:下位机 JSON / 硬件扩展
下位机 JSON 和硬件扩展用于控制底盘、LED、OLED、云台或新增外设。先完成 下位机教程 中的低风险指令和停止方式验证,再做扩展。
建议顺序:
- 先测试状态、灯光、OLED 或音频提示。
- 再测试云台小角度动作。
- 最后测试底盘短时低速动作。
- 每一步都保留停止方式、日志和串口占用检查。
路径 7:部署、自启动与恢复
部署适合已经手动运行稳定的脚本。部署前,先确认脚本可手动运行、可停止、可记录日志、可禁用。
适合场景:
- 开机初始化状态;
- 自动启动数据记录;
- 启动独立 Web API;
- 周期性检查电压或系统状态。
不适合场景:
- 开机后自动驱动底盘;
- 无日志、无停止方式、无禁用方式的脚本;
- 依赖相机、串口或 ROS 2 但没有占用检查的脚本。
crontab 模板:
@reboot sleep 10 && /home/jetson/ugv_jetson/ugv-env/bin/python /home/jetson/ugv_projects/my_project/scripts/task.py >> /home/jetson/ugv_projects/my_project/logs/autorun.log 2>&1
禁用方法:执行 crontab -e,在对应行前添加 #,保存后重启。
systemd 模板:
[Unit]
Description=UGV custom task
After=network-online.target
[Service]
Type=simple
User=jetson
WorkingDirectory=/home/jetson/ugv_projects/my_project
ExecStart=/home/jetson/ugv_jetson/ugv-env/bin/python /home/jetson/ugv_projects/my_project/scripts/task.py
Restart=on-failure
RestartSec=5
StandardOutput=append:/home/jetson/ugv_projects/my_project/logs/systemd.log
StandardError=append:/home/jetson/ugv_projects/my_project/logs/systemd.err.log
[Install]
WantedBy=multi-user.target
常用管理命令:
sudo systemctl daemon-reload
sudo systemctl enable ugv-custom-task.service
sudo systemctl start ugv-custom-task.service
sudo systemctl status ugv-custom-task.service
sudo systemctl stop ugv-custom-task.service
sudo systemctl disable ugv-custom-task.service
systemd 和 crontab 二选一即可。不要让同一个脚本被两种方式重复启动。
观察点:
- 先手动运行脚本;
- 再用 systemd 或 crontab 启动一次;
- 检查日志路径、进程状态和停止命令;
- 重启后确认可禁用、可恢复。
最小可运行项目示例
项目 A:电压状态记录器
目标:定时读取电压状态并写入 CSV 或 JSONL 日志。
入口:JupyterLab、Jetson 主机 Python 脚本,或 ROS 2 只读节点。
不做什么:不控制底盘,不占用摄像头,不修改 Web 主程序。
观察点:日志文件按时间追加;电压字段可读;异常时有错误记录。
下一步:加入电压阈值提示或 Web 状态接口。
项目 B:独立 Web 状态接口
目标:使用独立端口输出自定义状态,例如电压、磁盘空间、任务状态或最近日志。
入口:Flask 独立 API,建议先使用 5001 等独立端口验证。
不做什么:不替换原 Web;不接入运动控制;不占用底盘串口。
观察点:/api/status 返回 JSON;终端有访问日志;异常时返回错误信息。
下一步:确认接口稳定后,再考虑增加只读状态卡片。
项目 C:拍照并保存结果
目标:读取摄像头画面,保存原图、时间戳和识别结果。
入口:Notebook 原型或 Jetson 主机 Python 脚本。
不做什么:不和 Web 视频、其它 Notebook 同时占用摄像头;不控制底盘。
观察点:图片文件可打开;文件名包含时间戳;摄像头被占用时有明确日志。
下一步:增加识别框、分类结果或定时采集。
项目 D:安全底盘动作模板
目标:整理一个带 dry-run、限幅、超时和停止逻辑的运动任务框架。
入口:任务脚本或独立 API。
不做什么:默认不发送真实底盘指令;未验证停止方式前不接入串口或 /cmd_vel。
观察点:dry-run 输出完整计划;速度和时间被限制在配置范围内;异常时会执行停止逻辑。
下一步:完成低速短时验证后,再接入真实控制入口。
项目 E:ROS 2 只读监控节点
目标:订阅 /voltage、/scan、/odom 或其它状态 topic,输出日志或保存 rosbag。
入口:ROS 2 Docker 容器中的 ugv_custom_monitor 包。
不做什么:不发布 /cmd_vel,不发布 /goal_pose,不修改 Nav2 参数。
观察点:节点能启动;topic 有输出;日志中有时间戳和状态值。
下一步:增加阈值提示、文件记录或 Web 只读状态显示。
修改前备份与版本管理
修改系统文件前,先创建备份目录并复制原文件。
mkdir -p ~/ugv_projects/backups
cp /home/jetson/ugv_jetson/app.py \
~/ugv_projects/backups/app.py.$(date +%Y%m%d_%H%M%S)
如果当前目录是 Git 仓库,先查看变更范围:
git status
git diff
git diff -- <file>
仅在当前目录是 Git 仓库、且确认要回退该文件时,才使用:
git checkout -- <file>
如果当前目录不是 Git 仓库,不要执行 Git 回退命令;先用复制备份的方式保护原文件。
Python 依赖与系统包管理
不要为了一个二次开发示例直接升级整套系统包。不要随意执行 apt upgrade。不要在不了解影响的情况下替换 OpenCV、NumPy、ROS 2 或系统 Python 相关依赖。
建议在个人项目目录中使用虚拟环境:
cd ~/ugv_projects/my_project
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip freeze > requirements.lock.txt
如果示例依赖系统摄像头、GPIO、ROS 2 或 Jetson 特定库,虚拟环境可能还需要额外配置;以当前项目实际依赖为准。
Web 前端二次开发边界
| 操作 | 建议 |
|---|---|
| 新增状态卡片、参数表单、低风险按钮 | 可以先在独立页面或独立端口验证 |
| 新增任务启动 / 停止接口 | 必须有停止按钮、日志和异常返回 |
| 修改底盘控制按钮 | 谨慎处理,必须保留限幅、停止和超时 |
| 删除原始控制页面或替换主程序 | 不建议 |
| 修改串口发送核心、停止逻辑、安全限幅 | 不建议第一次二次开发就修改 |
可以进行前端二次开发,但不应破坏原有停止逻辑、限幅逻辑和恢复方式。
外设和硬件扩展注意事项
新增外设前先确认供电、电流、接线和固定方式。
- 新增 USB 相机、OAK-D Lite 或其它 USB 外设时,注意供电和 USB 带宽;
- 新增外设不要遮挡雷达、相机或云台;
- 新增较重外设后,底盘重心、轮子打滑和 Navigation 表现可能变化;
- 如果外设改变了车体外形或遮挡雷达,可能需要重新建图或重新检查 Navigation;
- 不要在不确认引脚、电压和电流能力的情况下直接接外设。
常见风险与回退方法
| 风险 | 常见原因 | 回退方法 |
|---|---|---|
| 串口被占用 | Web、Notebook、ROS 2 同时打开底盘串口 | 停止多余程序,只保留一个控制入口 |
| 摄像头打不开 | Web 主程序或其它 Notebook 占用摄像头 | 释放摄像头资源后再运行视觉程序 |
| 底盘停不下 | 缺少 finally 停止、脚本异常退出 | 先发送停止指令,保留断电方式,之后补 safe_stop() |
| Web 页面异常 | 修改原始 app.py 或模板后无法恢复 | 用备份恢复,先在独立端口验证 |
| ROS 2 无 topic | 未进入容器、未加载环境、节点未启动 | 回到 ROS 2 教程完成基础检查 |
| 自启动循环失败 | systemd / crontab 重复启动或路径错误 | 禁用自启动,查看日志,手动运行脚本 |
| 电压偏低 | 长时间视觉、ROS 2 或底盘运动耗电 | 停止高负载任务,充电或更换电池 |
| 依赖升级后功能异常 | 直接升级系统包或核心 Python 依赖 | 使用依赖记录回退,避免全局升级 |
二次开发验收模板
部署或合入主程序前,先用该模板记录验证结果。
| 项目 | 内容 |
|---|---|
| 功能名称 | |
| 运行环境 | Jetson 主机 / ROS 2 Docker / 浏览器 / ESP32 下位机 |
| 是否控制底盘 | 是 / 否 |
| 是否占用摄像头 | 是 / 否 |
| 是否占用串口 | 是 / 否 |
| dry-run 输出 | |
| 日志路径 | |
| 停止方式 | |
| 异常恢复方式 | |
| 通过标准 | |
| 不通过时回退 |