跳到主要内容

上位机教程

Web 页面在上位机中的作用

功能区域上位机侧作用
状态信息区汇总 CPU、RAM、温度、FPS、电压、文件占用等状态
摄像头画面区显示实时画面和识别结果
媒体快捷操作区处理拍照、录像、缩放、WebRTC 等媒体功能
文件管理区查看照片和视频保存结果
视觉与自动功能区触发识别、追踪、巡线、MediaPipe 等功能
Web 命令行区输入调试命令或二次开发命令
JupyterLab 入口打开 Notebook、Python 示例和开发环境

Web 控制端入口

Web 控制端运行在 Jetson Orin Nano / Jetson Orin NX 上,上位机项目目录为 ~/ugv_jetson

正常使用时,在浏览器中打开 Web 页面即可完成底盘、云台、灯光、摄像头、拍照录像和视觉功能操作。只有在排查服务启动、修改页面功能或二次开发时,才需要查看以下文件和接口。

开发者补充:Web 控制端文件和接口

Web 控制端相关文件如下:

文件作用
app.pyWeb 主程序入口,创建 Flask 应用和 SocketIO 服务,监听 0.0.0.0:5000
templates/index.htmlWeb 控制端主页面,包含视频画面、底盘控制、云台控制、视觉功能、拍照录像、文件列表和设置入口。
templates/control.js读取 /config,按 config.yaml 中的命令编号发送控制消息。
templates/main.jsWebRTC 相关前端逻辑。
config.yaml保存机器人类型、速度参数、命令编号、反馈字段编号、视频参数和视觉参数。
base_ctrl.py封装串口 JSON 指令发送、灯光、OLED、云台和底盘控制。
cv_ctrl.py处理摄像头、拍照、录像、OpenCV、MediaPipe、目标位置偏移和巡线等视觉逻辑。
audio_ctrl.py处理提示音、上传音频、播放音频和 TTS 相关功能。

app.py 提供以下接口:

接口调用方式用途
/浏览器访问打开 templates/index.html
/configGET返回 config.yaml,供前端读取命令编号和状态字段。
/video_feedGET返回 MJPEG 视频流,页面中通过 <img src="/video_feed"> 显示画面。
/send_commandPOST 表单字段 commandWeb 命令行入口,将字符串交给 cmdline_ctrl() 解析。
SocketIO /jsonemit('json', jsonData)发送 JSON 控制指令到 base.base_json_ctrl()
SocketIO /ctrlsend(JSON.stringify({A,B,C}))发送页面功能按钮命令,例如拍照、录像、视觉模式和灯光控制。

Web 主程序不会自动在电脑上打开浏览器页面。确认 Web 服务可用时,应先在电脑浏览器访问:

http://<机器人IP>:5000
提示

页面无法访问,或需要在终端查看启动日志时,可手动启动 Web 主程序。在 ~/ugv_jetson 目录执行:

cd ~/ugv_jetson
source ugv-env/bin/activate
python app.py

如果终端输出 Address already in usePort 5000 is in use by another program,表示 5000 端口已经被其它进程占用。常见情况是 Web 主程序已经在后台运行,此时不要重复启动 app.py,直接在浏览器访问 http://<机器人IP>:5000 检查页面。

如果浏览器仍然打不开页面,优先检查:

  1. 电脑是否连接到 UGV 热点、同一路由器 WiFi。 或是否通过网线 / USB 数据线连接到 Jetson。
  2. 浏览器中填写的 IP 是否与 OLED 屏幕显示一致。
  3. URL 是否包含端口 :5000
  4. 终端是否仍显示 Port 5000 is in use。 若显示该信息,先按“已有 Web 服务运行”处理;只有在调试端口占用问题时,才进一步检查或停止后台进程。
开发者补充:串口配置与开机启动

app.py 中默认使用 Jetson Orin Nano 串口:

base = BaseController('/dev/ttyTHS1', 115200)

Jetson Orin NX 可使用以下串口配置:

# base = BaseController('/dev/ttyTHS0', 115200)

这里展示的是 Web 主程序的示例配置。Notebook 中准备执行真实底盘动作时,仍应按后文 底盘串口确认与占用排查 逐个验证候选串口,不要直接照抄这里的串口值。

autorun.sh 会向当前账号 crontab 添加 Web 主程序开机任务,路径为 ~/ugv_jetson/app.py。不要用 sudo ./autorun.sh 执行该脚本;脚本内已经检查并拒绝 sudo 执行。JupyterLab 通过 create_jupyter_service.py 生成 ugv_jupyter.service 后交给 systemd 管理。

Python 与 JupyterLab 基础

JupyterLab 界面速览

下图为 JupyterLab 首页。左侧是文件浏览器,右侧是 Launcher 启动页。

JupyterLab 首页整体界面

区域作用
文件浏览器浏览 Jetson 上位机中的项目目录、脚本、配置文件和媒体资源。
Launcher创建 Notebook、Console、Terminal、文本文件或 Python 文件。
Notebook按单元格保存和运行 Python 控制实验,便于记录过程和复现实验。
Console临时执行 Python 语句,用于快速验证参数、变量和函数。
Terminal执行系统命令、查看文件路径、运行脚本和查看日志。
Kernel 选择窗口为 Notebook 或 Console 选择实际运行代码的 Python 环境。
Notebook 工具栏保存文件、添加单元格、运行代码、停止执行、重启 Kernel 和切换单元格类型。
菜单 / Kernel管理文件、运行单元,并在 Notebook 或 Console 卡住时中断或重启内核。

JupyterLab 基础流程

打开 JupyterLab

JupyterLab 的访问方式见入门教程:

新建第一个 Notebook

  1. 在 Notebook 区域双击 Python 3 (ipykernel)

  2. 在左侧文件浏览器中选中新建的 Notebook。 右键重命名为:

    ugv_jupyterlab_control_test.ipynb

    rename

  3. 确认 Notebook 工具栏右侧显示 Python 3 (ipykernel)

    备注

    Notebook 工具栏介绍

    Notebook 打开后,页面上方会显示工具栏。常用按钮如下图所示:

    Notebook 工具栏

    按钮 / 区域作用
    保存 保存保存当前 Notebook。
    添加代码 新增单元格在当前位置新增一个单元格。
    剪切复制粘贴 剪切 / 复制 / 粘贴剪切、复制或粘贴当前单元格。
    运行 运行运行当前选中的单元格。
    停止 停止停止正在执行的单元格。
    重启 Kernel 重启 Kernel重启当前 Kernel,适合代码卡住或变量状态混乱时使用。
    重启并运行 重启并运行重启 Kernel 后继续运行后续单元格,运行前请确认代码内容安全。
    单元格类型 Code 下拉框切换单元格类型,例如 Python 代码单元或 Markdown 文本单元。
    Kernel 状态 Python 3 (ipykernel)显示当前 Notebook 连接的 Kernel。
  4. 在 Cell 1 中输入:

    print("Hello UGV Rover!")
  5. Shift + Enter 运行 Cell 1。

  6. 测试结束后保存 Notebook。 如果单元格长时间运行,可点击工具栏中的停止按钮,或在菜单中选择 Kernel > Interrupt Kernel

    运行成功后,Cell 1 下方会显示:

    Hello UGV Rover!

    如果单元格左侧出现类似 In [1]: 的编号,且下方输出内容与上面一致,说明 Notebook 已经连接到 Python Kernel,并能正常执行代码。

使用 Console 快速验证 Python

注意

Console 适合临时验证变量、参数和小段代码,例如 LED PWM、云台角度等;不适合保存完整实验流程,也不要在 Console 中直接发送硬件控制指令。正式测试请回到 Notebook 中运行带安全保护的代码。

在 Launcher 的 Console 区域双击 Python 3 (ipykernel) 打开 Console。

Console

打开后,在图示位置粘贴代码:

Console 输入位置

粘贴以下内容后,按 Shift + Enter 执行。

LED_PWM = 64
GIMBAL_STEP = 20

print("LED_PWM =", LED_PWM)
print("GIMBAL_STEP =", GIMBAL_STEP)

执行结束后,可关闭 Console 标签页;如果 Console 长时间没有返回输入提示,可通过 JupyterLab 菜单中断对应 Kernel。

如果能看到新的 In [ ]: 输入提示,说明 Console 内核工作正常。

console2

开发者补充:项目目录和控制文件检查
提示

这一步用于确认后续章节需要用到的上位机项目文件已经存在。实际开发时,如果项目路径不正确,后续 LED、云台、底盘等控制示例也无法继续运行。

在 Notebook 的 Cell 2 中输入以下代码,确认当前 Python 环境和上位机项目目录是否存在。上位机项目目录为 ~/ugv_jetson

import sys
from pathlib import Path

print("Python 版本:")
print(sys.version)

PROJECT_DIR = Path("~/ugv_jetson").expanduser()

print("\n项目目录:", PROJECT_DIR)
print("项目目录是否存在:", PROJECT_DIR.exists())

上述输出应显示 ~/ugv_jetson 存在。

项目目录: /home/<当前账号>/ugv_jetson
项目目录是否存在: True

如需进一步确认下位机控制封装文件是否存在,可在 Notebook 中检查 base_ctrl.py

control_file = PROJECT_DIR / "base_ctrl.py"
print(control_file, "是否存在:", control_file.exists())

预演打印待发送控制动作

提示

dry-run 只显示计划,不会点亮 LED、不会转动云台,也不会让底盘运动。本节只用于熟悉 JupyterLab 和 Python 的基本操作;真实硬件控制放到后续章节分开讲解。

在正式控制硬件前,先用 dry-run 打印准备执行的动作,确认参数含义。

DRY_RUN = True

control_plan = [
{"name": "低亮度点亮 LED", "action": "lights_ctrl", "args": [64, 64]},
{"name": "云台回中", "action": "gimbal_base_ctrl", "args": [2, 2, 0]},
{"name": "云台小角度转动", "action": "gimbal_ctrl", "args": [20, 0, 0, 0]},
]

for index, step in enumerate(control_plan, start=1):
print(f"{index}. {step['name']}")
print(f" 函数:{step['action']}")
print(f" 参数:{step['args']}")

输出会列出 3 个待执行动作,包括 LED 低亮度点亮、云台回中和云台小角度转动。看到这些输出只说明控制计划已生成,还没有真正向下位机发送控制指令。

摄像头读取、OpenCV、MediaPipe 和视觉应用内容见 摄像头与视觉处理

数据记录与可视化调试

读取项目文件

只做查看,不直接修改。

开发者补充:常见项目文件
文件 / 目录作用
app.pyWeb 主程序和页面接口。
base_ctrl.py下位机通信和底盘、云台、灯光封装。
cv_ctrl.py相机取流和视觉处理。
config.yaml产品型号、模块和运行参数。
sounds/音频文件目录。
  1. 在 JupyterLab 文件浏览器中进入上位机项目目录。
  2. 打开需要查看的文件或目录。
  3. 只读取内容。 不直接修改控制脚本或配置文件。
  4. 查看结束后关闭文件标签页。 JupyterLab 中应能打开对应文件或目录。若文件不存在,应先确认项目路径和当前镜像版本。
开发者补充:查看 config.yaml 配置

config.yaml 用于保存命令编号、视频分辨率、颜色阈值、速度参数和产品运行参数。排查配置是否存在或字段是否变化时,可以只读查看,不要求在基础流程中执行。

  1. Cell 1:读取配置文件。
  2. 粘贴以下代码。 按实机确认 CONFIG_PATH
  3. 运行代码。 查看配置顶层字段。
  4. 查看结束后关闭文件标签页或停止当前单元格。 不要在未备份时写回配置。
from pathlib import Path
import yaml

CONFIG_PATH = Path("~/ugv_jetson/config.yaml").expanduser()

if not CONFIG_PATH.exists():
raise FileNotFoundError(CONFIG_PATH)

with CONFIG_PATH.open("r", encoding="utf-8") as f:
config = yaml.safe_load(f)

print("配置顶层字段:", list(config.keys()))

Notebook 应输出配置文件的顶层字段列表。若提示文件不存在,应先确认项目目录和配置文件路径。

注意

修改 config.yaml 前先复制备份。不了解字段含义时,只查看,不写回。

延时摄影

延时摄影可以拆成四个环节:定时拍照、保存图片序列、合成视频、验证视频。本节只使用固定摄像头完成安全版流程,不控制底盘,不控制灯光,也不做移动拍摄。

Web 端的 timelapse 可以包含底盘移动、停止、补光和拍照等联动。Notebook 本节只拆解数据流程:固定摄像头,定时保存多张图片,再合成 timelapse.mp4

  1. 确认 CAMERA_SOURCE 可用。 继续使用前面单帧读取测试得到的摄像头编号。

    展开查看确认方法

    如果还没有完成单帧读取测试,请运行下面代码,确认能读取并显示一帧画面。确认成功后,继续使用同一个 CAMERA_SOURCE

    Cell 1:导入库、参数配置和中心裁剪函数。

    import cv2
    import matplotlib.pyplot as plt

    CAMERA_CANDIDATES = [-1, 0]
    FRAME_WIDTH = 640
    FRAME_HEIGHT = 480
    WARMUP_FRAMES = 20
    ZOOM = 1.5

    def center_crop_zoom(frame, zoom=1.0):
    if zoom <= 1.0:
    return frame

    height, width = frame.shape[:2]
    crop_width = int(width / zoom)
    crop_height = int(height / zoom)
    start_x = (width - crop_width) // 2
    start_y = (height - crop_height) // 2

    cropped = frame[start_y:start_y + crop_height, start_x:start_x + crop_width]
    return cv2.resize(cropped, (width, height), interpolation=cv2.INTER_LINEAR)

    Cell 2:检测可用摄像头并读取一帧画面。

    cap = None
    frame = None
    camera_source = None

    for source in CAMERA_CANDIDATES:
    test_cap = cv2.VideoCapture(source)
    test_cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
    test_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)

    ok, test_frame = test_cap.read()
    if not ok:
    test_cap.release()
    continue

    cap = test_cap
    frame = test_frame
    camera_source = source
    break

    if frame is None:
    raise RuntimeError("无法读取摄像头画面")

    for _ in range(WARMUP_FRAMES):
    ok, warmup_frame = cap.read()
    if ok:
    frame = warmup_frame

    CAMERA_SOURCE = camera_source
    print("CAMERA_SOURCE =", CAMERA_SOURCE)

    Cell 3:执行中心裁剪、显示图像,并释放摄像头资源。

    try:
    display_frame = center_crop_zoom(frame, ZOOM)
    frame_rgb = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB)
    plt.imshow(frame_rgb)
    plt.axis("off")
    plt.show()
    finally:
    if cap is not None:
    cap.release()
  2. Cell 1:导入库和参数。

    import cv2
    import time
    from pathlib import Path

    from IPython.display import Video, display

    FRAME_DIR = Path("timelapse_frames")
    VIDEO_PATH = Path("timelapse.mp4")

    FRAME_COUNT = 5
    CAPTURE_INTERVAL = 1.0
    FPS = 2

    FRAME_DIR.mkdir(exist_ok=True)

    print("图片保存目录:", FRAME_DIR.resolve())
    print("视频输出文件:", VIDEO_PATH.resolve())
    print("拍摄张数:", FRAME_COUNT)
    print("拍摄间隔:", CAPTURE_INTERVAL, "秒")
    print("视频 FPS:", FPS)

    运行成功后,Notebook 会输出图片保存目录、视频文件路径、拍摄张数和视频帧率。此时不会打开摄像头。

  3. Cell 2:使用已确认的 CAMERA_SOURCE 定时拍摄图片。

    if "CAMERA_SOURCE" not in globals():
    raise RuntimeError("请先完成前面的单帧读取测试,确认 CAMERA_SOURCE 可用。")

    cap = cv2.VideoCapture(CAMERA_SOURCE)

    if not cap.isOpened():
    raise RuntimeError("无法打开摄像头,请先检查摄像头资源释放与恢复。")

    saved_files = []

    try:
    for index in range(FRAME_COUNT):
    ok, frame = cap.read()

    if not ok:
    print("第", index, "张读取失败,跳过。")
    time.sleep(CAPTURE_INTERVAL)
    continue

    image_path = FRAME_DIR / f"frame_{index:03d}.jpg"
    cv2.imwrite(str(image_path), frame)
    saved_files.append(image_path)

    print("保存:", image_path)
    time.sleep(CAPTURE_INTERVAL)

    finally:
    cap.release()
    print("摄像头已释放。")
    print("保存数量:", len(saved_files))

    运行成功后,timelapse_frames 目录中应生成多张 frame_000.jpgframe_001.jpg 这样的图片。拍摄过程中可以缓慢移动画面中的物体,或让场景自然变化。

  4. Cell 3:查看图片序列。

    image_files = sorted(FRAME_DIR.glob("frame_*.jpg"))

    print("图片数量:", len(image_files))

    if not image_files:
    raise RuntimeError("没有找到延时摄影图片,请先运行拍摄 Cell。")

    preview_count = min(5, len(image_files))

    for image_path in image_files[:preview_count]:
    print(image_path)

    运行成功后,Notebook 会列出已经保存的图片文件。如果图片数量为 0,说明前面的拍摄步骤没有成功。

  5. Cell 4:合成视频并验证 VideoWriter。

    image_files = sorted(FRAME_DIR.glob("frame_*.jpg"))

    if not image_files:
    raise RuntimeError("没有可用于合成视频的图片。")

    first_frame = cv2.imread(str(image_files[0]))

    if first_frame is None:
    raise RuntimeError("无法读取第一张图片。")

    height, width = first_frame.shape[:2]

    fourcc = cv2.VideoWriter_fourcc(*"mp4v")
    writer = cv2.VideoWriter(str(VIDEO_PATH), fourcc, FPS, (width, height))

    if not writer.isOpened():
    raise RuntimeError("无法创建视频文件,请检查 OpenCV 视频编码支持。")

    written_count = 0

    try:
    for image_path in image_files:
    frame = cv2.imread(str(image_path))

    if frame is None:
    print("跳过无法读取的图片:", image_path)
    continue

    if frame.shape[:2] != (height, width):
    frame = cv2.resize(frame, (width, height))

    writer.write(frame)
    written_count += 1

    finally:
    writer.release()

    print("写入视频帧数:", written_count)
    print("视频文件:", VIDEO_PATH.resolve())
    print("视频是否存在:", VIDEO_PATH.exists())
    print("视频大小:", VIDEO_PATH.stat().st_size if VIDEO_PATH.exists() else 0, "bytes")

    运行成功后,应生成 timelapse.mp4。如果视频大小为 0,或 writer.isOpened() 报错,说明当前环境的视频编码支持需要检查。

  6. Cell 5:验证并预览视频。

    if not VIDEO_PATH.exists():
    raise RuntimeError("视频文件不存在,请先运行视频合成 Cell。")

    print("视频文件:", VIDEO_PATH.resolve())
    print("视频大小:", VIDEO_PATH.stat().st_size, "bytes")

    video_cap = cv2.VideoCapture(str(VIDEO_PATH))

    print("视频是否可打开:", video_cap.isOpened())
    print("视频帧数:", int(video_cap.get(cv2.CAP_PROP_FRAME_COUNT)))
    print("视频 FPS:", video_cap.get(cv2.CAP_PROP_FPS))

    ok, frame = video_cap.read()
    print("能否读取第一帧:", ok)
    print("第一帧尺寸:", None if frame is None else frame.shape)

    video_cap.release()

    display(Video(str(VIDEO_PATH), embed=True))

    如果 视频是否可打开True视频帧数 大于 0,并且能读取第一帧,说明视频文件基本正常。

    如果 JupyterLab 文件浏览器直接打开 timelapse.mp4 时提示 not UTF-8 encoded,不要按文本方式打开它。请使用本 Cell 预览,或将视频下载到电脑后用视频播放器打开。

音频与启动自动化

音频功能

audio_ctrl.py 负责播放提示音、随机播放目录中的音频、设置音量、TTS 语音播报和停止播放。

常见检查点:

  • 播放一段短提示音;
  • 播放目录中的随机音频;
  • 调整音量后再测试;
  • 停止正在播放的音频。

音频测试不应触发底盘、云台或灯光动作。

启动自动化

启动自动化主要包括 Web 主程序开机启动和 JupyterLab 服务管理。优先确认服务是否已启动,再判断是否需要恢复默认状态。

开发者补充:查看启动状态
  1. 查看 Web 主程序状态。
  2. 查看 JupyterLab 服务状态。
  3. 确认是否有重复启动项。
  4. 恢复默认状态时,先保留可登录入口,再停止临时测试脚本。
crontab -l

启动自动化不建议加入底盘前进、转向、巡线或连续动作脚本。需要时只保留 Web、自启动和提示音这类低风险任务。