上位机教程
目标
本章介绍 Jetson Orin Nano 上位机的常用功能,包括 JupyterLab、Python、Web 控制、数据记录和启动自动化;摄像头、OpenCV 和 MediaPipe 内容见 摄像头与视觉处理。
Web 页面在上位机中的作用
| 功能区域 | 上位机侧作用 |
|---|---|
| 状态信息区 | 汇总 CPU、RAM、温度、FPS、电压、文件占用等状态 |
| 摄像头画面区 | 显示实时画面和识别结果 |
| 媒体快捷操作区 | 处理拍照、录像、缩放、WebRTC 等媒体功能 |
| 文件管理区 | 查看照片和视频保存结果 |
| 视觉与自动功能区 | 触发识别、追踪、巡线、MediaPipe 等功能 |
| Web 命令行区 | 输入调试命令或二次开发命令 |
| JupyterLab 入口 | 打开 Notebook、Python 示例和开发环境 |
Web 控制端入口
Web 控制端运行在 Jetson Orin Nano / Jetson Orin NX 上,上位机项目目录为 ~/ugv_jetson。
正常使用时,在浏览器中打开 Web 页面即可完成底盘、云台、灯光、摄像头、拍照录像和视觉功能操作。只有在排查服务启动、修改页面功能或二次开发时,才需要查看以下文件和接口。
开发者补充:Web 控制端文件和接口
Web 控制端相关文件如下:
| 文件 | 作用 |
|---|---|
app.py | Web 主程序入口,创建 Flask 应用和 SocketIO 服务,监听 0.0.0.0:5000。 |
templates/index.html | Web 控制端主页面,包含视频画面、底盘控制、云台控制、视觉功能、拍照录像、文件列表和设置入口。 |
templates/control.js | 读取 /config,按 config.yaml 中的命令编号发送控制消息。 |
templates/main.js | WebRTC 相关前端逻辑。 |
config.yaml | 保存机器人类型、速度参数、命令编号、反馈字段编号、视频参数和视觉参数。 |
base_ctrl.py | 封装串口 JSON 指令发送、灯光、OLED、云台和底盘控制。 |
cv_ctrl.py | 处理摄像头、拍照、录像、OpenCV、MediaPipe、目标位置偏移和巡线等视觉逻辑。 |
audio_ctrl.py | 处理提示音、上传音频、播放音频和 TTS 相关功能。 |
app.py 提供以下接口:
| 接口 | 调用方式 | 用途 |
|---|---|---|
/ | 浏览器访问 | 打开 templates/index.html。 |
/config | GET | 返回 config.yaml,供前端读取命令编号和状态字段。 |
/video_feed | GET | 返回 MJPEG 视频流,页面中通过 <img src="/video_feed"> 显示画面。 |
/send_command | POST 表单字段 command | Web 命令行入口,将字符串交给 cmdline_ctrl() 解析。 |
SocketIO /json | emit('json', jsonData) | 发送 JSON 控制指令到 base.base_json_ctrl()。 |
SocketIO /ctrl | send(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 use 或 Port 5000 is in use by another program,表示 5000 端口已经被其它进程占用。常见情况是 Web 主程序已经在后台运行,此时不要重复启动 app.py,直接在浏览器访问 http://<机器人IP>:5000 检查页面。
如果浏览器仍然打不开页面,优先检查:
- 电脑是否连接到 UGV 热点、同一路由器 WiFi。 或是否通过网线 / USB 数据线连接到 Jetson。
- 浏览器中填写的 IP 是否与 OLED 屏幕显示一致。
- URL 是否包含端口
:5000。 - 终端是否仍显示
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 是运行在 Jetson Orin Nano 上的浏览器开发环境,可以新建 Notebook、打开 Console、使用 Terminal、查看文件和编辑脚本。它看起来像网页,但代码不是在浏览器里运行,而是在 UGV 的上位机中运行。
为什么 UGV 上位机要用 JupyterLab
UGV Rover PT Jetson Orin ROS2 Kit 的 Web 控制、视觉识别、相机取流、ROS 2、SLAM 和导航等高层功能都运行在 Jetson Orin Nano 上。JupyterLab 提供了一个交互式控制入口:电脑浏览器连接到机器人后,可以直接在上位机上运行 Python,逐步验证 LED、云台、底盘和相机等功能。
Notebook、Console、Terminal 的区别
| 工具 | 用途 |
|---|---|
| Notebook | 保存完整控制实验流程,适合逐步运行、记录结果和复现实验。 |
| Console | 临时验证变量、参数和函数,适合先检查 LED PWM、云台角度等参数。 |
| Terminal | 查看项目文件、确认依赖、运行脚本和查看日志。 |
JupyterLab 界面速览
下图为 JupyterLab 首页。左侧是文件浏览器,右侧是 Launcher 启动页。

| 区域 | 作用 |
|---|---|
| 文件浏览器 | 浏览 Jetson 上位机中的项目目录、脚本、配置文件和媒体资源。 |
| Launcher | 创建 Notebook、Console、Terminal、文本文件或 Python 文件。 |
| Notebook | 按单元格保存和运行 Python 控制实验,便于记录过程和复现实验。 |
| Console | 临时执行 Python 语句,用于快速验证参数、变量和函数。 |
| Terminal | 执行系统命令、查看文件路径、运行脚本和查看日志。 |
| Kernel 选择窗口 | 为 Notebook 或 Console 选择实际运行代码的 Python 环境。 |
| Notebook 工具栏 | 保存文件、添加单元格、运行代码、停止执行、重启 Kernel 和切换单元格类型。 |
| 菜单 / Kernel | 管理文件、运行单元,并在 Notebook 或 Console 卡住时中断或重启内核。 |
JupyterLab 基础流程
打开 JupyterLab
JupyterLab 的访问方式见入门教程:
新建第一个 Notebook
前置知识
什么是 Kernel?
Kernel 是 Notebook 或 Console 实际运行代码的环境。第一次学习本节时,直接选择 Python 3 (ipykernel)。
已有 Kernel 选项介绍
| kernel选项 | 作用 |
|---|---|
Start Preferred Kernel | 启动当前 Notebook 推荐的默认 Kernel。 |
Python 3 (ipykernel) | 运行 Python 代码的常用 Kernel,适合本教程中的 Notebook、Console 和 Python 示例。 |
Use No Kernel / No Kernel | 只打开 Notebook,不连接运行环境;适合查看或编辑内容,不适合运行代码。 |
Use Kernel from Preferred Session | 复用 JupyterLab 为当前 Notebook 匹配到的已有会话,适合继续之前的实验。 |
Use Kernel from Other Session | 连接到其它已打开 Notebook 或 Console 的 Kernel;会共用该会话中的变量和运行状态。 |
ugv_jupyterlab_control_test.ipynb、Untitled6.ipynb 等文件名 | 表示当前已经存在的 Notebook 会话;选择后会连接到对应会话。 |
如果代码状态混乱,可通过 Notebook 工具栏或菜单中的重启 Kernel 功能重新开始。
什么是 Cell?
JupyterLab 中,Notebook 由多个 Cell(单元格)组成。每个 Cell 可以保存一段代码或 Markdown 文本,并可单独运行或编辑。本教程后文使用 Cell 1、Cell 2 表示需要按顺序新建并运行的代码单元。
-
在 Notebook 区域双击
Python 3 (ipykernel)。 -
在左侧文件浏览器中选中新建的 Notebook。 右键重命名为:
ugv_jupyterlab_control_test.ipynb
-
确认 Notebook 工具栏右侧显示
Python 3 (ipykernel)。备注Notebook 工具栏介绍
Notebook 打开后,页面上方会显示工具栏。常用按钮如下图所示:
按钮 / 区域 作用 保存
保存当前 Notebook。 新增单元格
在当前位置新增一个单元格。 剪切 / 复制 / 粘贴
剪切、复制或粘贴当前单元格。 运行
运行当前选中的单元格。 停止
停止正在执行的单元格。 重启 Kernel
重启当前 Kernel,适合代码卡住或变量状态混乱时使用。 重启并运行
重启 Kernel 后继续运行后续单元格,运行前请确认代码内容安全。 Code下拉框切换单元格类型,例如 Python 代码单元或 Markdown 文本单元。 Python 3 (ipykernel)显示当前 Notebook 连接的 Kernel。 -
在 Cell 1 中输入:
print("Hello UGV Rover!") -
按 Shift + Enter 运行 Cell 1。
-
测试结束后保存 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。

打开后,在图示位置粘贴代码:
粘贴以下内容后,按 Shift + Enter 执行。
LED_PWM = 64
GIMBAL_STEP = 20
print("LED_PWM =", LED_PWM)
print("GIMBAL_STEP =", GIMBAL_STEP)
执行结束后,可关闭 Console 标签页;如果 Console 长时间没有返回输入提示,可通过 JupyterLab 菜单中断对应 Kernel。
如果能看到新的 In [ ]: 输入提示,说明 Console 内核工作正常。
开发者补充:项目目录和控制文件检查
这一步用于确认后续章节需要用到的上位机项目文件已经存在。实际开发时,如果项目路径不正确,后续 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 低亮度点亮、云台回中和云台小角度转动。看到这些输出只说明控制计划已生成,还没有真正向下位机发送控制指令。
挑战任务
目标:从 Web 页面记录 V_IN,并在 JupyterLab 中换算、判断和绘制电压趋势。
本任务不直接访问底盘串口,也不需要启动 ROS 2。只需要从 Web 页面状态区读取 V_IN 数值,并在 Notebook 中完成电压换算、状态判断、日志保存和趋势图显示。
前置条件:
- 已打开 Web 控制页面;
- Web 页面状态区能看到
V_IN; - JupyterLab 能正常打开;
- 当前任务不需要停止 Web 主程序。
完成标准: Notebook 能把 Web 页面 Cell 1:手动输入 Web 页面 运行后应输出换算后的电压。不要把 Cell 2:判断电压状态。 运行后应输出当前状态和建议。这里不会发送底盘停止指令,也不会触发语音提醒。 Cell 3:保存电压记录。 运行后,当前目录会生成或更新 Cell 4:显示状态卡片。 运行后会显示一个状态卡片,包含电压、状态、建议和原始 Cell 5:绘制电压变化趋势。 运行后会读取 V_IN 显示值换算为实际电压;能输出状态和建议;当前目录生成 battery_log.csv;能显示电压状态卡片和电压变化趋势图。展开电压记录代码
V_IN。1112 理解成 1112V。battery_log.csv。多次记录不同时间的 V_IN 后,可以用它绘制趋势图。V_IN 页面显示值。battery_log.csv 并绘制电压变化趋势。若只有一条记录,图中只会显示一个点;多记录几次后趋势会更明显。
本任务使用手动输入方式,是为了先理解 V_IN、电压换算、状态判断和可视化记录。Web 状态自动读取、ROS 2 /voltage 和串口直读会在后续对应章节中说明。
第一节不要直接使用串口读取电压。Web 主程序和 ROS 2 bringup 都可能读取底盘反馈,重复打开串口可能造成串口占用冲突。
摄像头读取、OpenCV、MediaPipe 和视觉应用内容见 摄像头与视觉处理。
数据记录与可视化调试
前置知识
为什么要记录数据
调试 UGV 时,状态、指令、反馈和日志可以帮助复现问题。先记录低风险数据,再分析底盘或视觉问题。
读取项目文件
只做查看,不直接修改。
开发者补充:常见项目文件
| 文件 / 目录 | 作用 |
|---|---|
app.py | Web 主程序和页面接口。 |
base_ctrl.py | 下位机通信和底盘、云台、灯光封装。 |
cv_ctrl.py | 相机取流和视觉处理。 |
config.yaml | 产品型号、模块和运行参数。 |
sounds/ | 音频文件目录。 |
- 在 JupyterLab 文件浏览器中进入上位机项目目录。
- 打开需要查看的文件或目录。
- 只读取内容。 不直接修改控制脚本或配置文件。
- 查看结束后关闭文件标签页。 JupyterLab 中应能打开对应文件或目录。若文件不存在,应先确认项目路径和当前镜像版本。
开发者补充:查看 config.yaml 配置
config.yaml 用于保存命令编号、视频分辨率、颜色阈值、速度参数和产品运行参数。排查配置是否存在或字段是否变化时,可以只读查看,不要求在基础流程中执行。
- Cell 1:读取配置文件。
- 粘贴以下代码。 按实机确认
CONFIG_PATH。 - 运行代码。 查看配置顶层字段。
- 查看结束后关闭文件标签页或停止当前单元格。 不要在未备份时写回配置。
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。
-
确认
CAMERA_SOURCE可用。 继续使用前面单帧读取测试得到的摄像头编号。展开查看确认方法
如果还没有完成单帧读取测试,请运行下面代码,确认能读取并显示一帧画面。确认成功后,继续使用同一个
CAMERA_SOURCE。Cell 1:导入库、参数配置和中心裁剪函数。
import cv2import matplotlib.pyplot as pltCAMERA_CANDIDATES = [-1, 0]FRAME_WIDTH = 640FRAME_HEIGHT = 480WARMUP_FRAMES = 20ZOOM = 1.5def center_crop_zoom(frame, zoom=1.0):if zoom <= 1.0:return frameheight, width = frame.shape[:2]crop_width = int(width / zoom)crop_height = int(height / zoom)start_x = (width - crop_width) // 2start_y = (height - crop_height) // 2cropped = 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 = Noneframe = Nonecamera_source = Nonefor 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()continuecap = test_capframe = test_framecamera_source = sourcebreakif frame is None:raise RuntimeError("无法读取摄像头画面")for _ in range(WARMUP_FRAMES):ok, warmup_frame = cap.read()if ok:frame = warmup_frameCAMERA_SOURCE = camera_sourceprint("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() -
Cell 1:导入库和参数。
import cv2import timefrom pathlib import Pathfrom IPython.display import Video, displayFRAME_DIR = Path("timelapse_frames")VIDEO_PATH = Path("timelapse.mp4")FRAME_COUNT = 5CAPTURE_INTERVAL = 1.0FPS = 2FRAME_DIR.mkdir(exist_ok=True)print("图片保存目录:", FRAME_DIR.resolve())print("视频输出文件:", VIDEO_PATH.resolve())print("拍摄张数:", FRAME_COUNT)print("拍摄间隔:", CAPTURE_INTERVAL, "秒")print("视频 FPS:", FPS)运行成功后,Notebook 会输出图片保存目录、视频文件路径、拍摄张数和视频帧率。此时不会打开摄像头。
-
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)continueimage_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.jpg、frame_001.jpg这样的图片。拍摄过程中可以缓慢移动画面中的物体,或让场景自然变化。 -
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,说明前面的拍摄步骤没有成功。 -
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 = 0try:for image_path in image_files:frame = cv2.imread(str(image_path))if frame is None:print("跳过无法读取的图片:", image_path)continueif frame.shape[:2] != (height, width):frame = cv2.resize(frame, (width, height))writer.write(frame)written_count += 1finally: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()报错,说明当前环境的视频编码支持需要检查。 -
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 服务管理。优先确认服务是否已启动,再判断是否需要恢复默认状态。
开发者补充:查看启动状态
- 查看 Web 主程序状态。
- 查看 JupyterLab 服务状态。
- 确认是否有重复启动项。
- 恢复默认状态时,先保留可登录入口,再停止临时测试脚本。
crontab -l
启动自动化不建议加入底盘前进、转向、巡线或连续动作脚本。需要时只保留 Web、自启动和提示音这类低风险任务。