编程语言研究书库

用手机开机器人 — systemd 自启、网页摇杆与 launcher 服务

用手机开机器人 — systemd 自启、网页摇杆与 launcher 服务

这一章讲什么: 上一章机器人已经能动了,但每开一次机要开两个 SSH 窗口、敲两行命令—— 这不是产品,是调试。本章把它变成「开机 → 绿灯亮 → 掏手机 → 开走」: ① 用 systemd 让电机服务开机自启,加一颗 LED 当「就绪」信号; ② 用 HTML/CSS/JavaScript 写一个手机网页应用,核心是自回中摇杆; ③ 给 Mosquitto 开 WebSocket 口,让浏览器里的网页直接连 MQTT; ④ 写一个 launcher 服务,网页上的按钮就能启动行为、关掉机器人。 读完你会有一个不再依赖 SSH 的完整遥控闭环。

1. 先看现象:SSH 不是给用户用的

书里先把痛点摆出来:手动驾驶很好玩,但登录、输命令启动、Ctrl+C 停止这套流程, 又慢又容易打错字1。买个蓝牙手柄也行,但作者指了指你口袋:「你手里的手机就是 一个无线触摸屏摇杆」——有 Wi-Fi、能开热点、能读手指动作1。

于是目标被定义成一句话:开机,等机器人说「我好了」,然后拿手机指它。 「说」这个动作用一颗 LED 完成——机器人没有屏幕,状态要用光来表达。 这个「无头设备的自我表达」问题,是本章前半的主线。

2. 顶层全景:四块新零件

 ① systemd    unit 模板 + deploy_services.py —— 服务开机自启/崩溃自重启
 ② 就绪信号   电机服务点亮 HAT 上的 LED(绿=活着),退出时 atexit 熄灭
 ③ 网页应用   index.html 菜单 + manual_drive.html 摇杆 + mqtt_client.js 连接
 ④ launcher   launcher_service.py:订阅 launcher/#,调 systemctl 启停行为、poweroff

 图说:①② 让机器人「自己会开场」;③④ 让你「不用 SSH 也能收场」。

3. 核心原理之一:systemd——让程序开机自己起来

systemd 是 Raspberry Pi OS 用来自动启动程序的工具:管程序的生死、何时跑、 以哪个用户跑、在哪个目录启动、把日志收进 journal;systemctl 是操控它的命令2。 第 6 章装 Mosquitto 时已经用过它,本章把它用到自己的代码上。

每个被 systemd 管的程序有一份 unit file(单元文件),写在 deploy/service_template.j2:

[Unit]
Description={{ service_name }}
After=mosquitto.service

[Service]
Type=simple
Restart={{ restart }}
WorkingDirectory=/home/{{ pi_user }}/
ExecStart=/usr/local/bin/robotpython -u {{ command }}
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

逐节拆开,每一行都有明确职责:

设置干什么为什么
After=mosquitto.service排在 Mosquitto 之后启动broker 先就绪,服务连得上 MQTT3
Restart={{ restart }}退出后怎么办电机服务填 always(崩了自动拉起);行为填 no(只手动启动)3
WorkingDirectory程序的工作目录代码都在用户主目录下
ExecStart启动命令用 robotpython 跑;-u 让日志不缓冲、实时可见3
KillSignal=SIGINT停止时发什么信号让 Python 的 atexit 能跑——停车、灭灯都靠它3
WantedBy=multi-user.target挂在哪个启动目标下开机进入多用户模式时就启动3

模板里 {{ 双括号 }} 是 PyInfra 的占位符,部署时替换成真实服务名、命令、重启策略。 deploy/deploy_services.py 把「填模板 → 写 unit → 重载 systemd → 按需重启」封装成 一个 deploy_service() 函数,参数四样:服务名、命令、是否自启、代码有没有变4。

两个必学的运维命令(以后天天用):

  • systemctl status <服务名> —— 看死活;
  • journalctl -u <服务名> —— 看完整日志,-f 实时跟(第 4 章 pyinfra 排错那套 在这里升级成「服务级日志」)5。

4. 核心原理之二:LED 就绪灯——无头设备的「我好了」

服务能自启之后,新问题:你在房间另一头,怎么知道它好了?给它一盏灯。

改动只有两处:服务连上 broker 后点亮 HAT 的第一颗 LED 为绿色 (board.leds.set_rgb(0, 0, 255, 0)——RGB 三个分量,绿满、红蓝为零); 退出处理器从只停车扩展为「停车 + 清灯」6。

 开机 → systemd 拉起电机服务 → 连上 broker → LED 变绿 = 「可以给我发消息了」
 关机/崩溃 → atexit:停车、灭灯

 图说:一颗灯,把「服务状态」这个本来要 SSH 才能看到的信息,变成房间任何角落可见。

部署、关机、拔掉墙电、只用电池再开机——绿灯亮,整条链路就自证成功了6。 这是嵌入式系统的经典模式:状态外化。机器人没有屏幕,灯、蜂鸣器、 later 网页仪表盘, 都是同一个思路的放大。

5. 核心原理之三:网页应用三件套——结构、样子、行为

手机端不装 App,用网页当界面——浏览器人人都有,跨平台零安装。 三件套分工书里给得很干净7:

技术管什么
HTML(超文本标记语言)页面的结构:文字、按钮、链接
CSS(层叠样式表)样子:颜色、字体、位置、大小
JS(JavaScript)行为:点击发生什么、和 MQTT 对话

HTML 的树。 页面由「开标签…闭标签」嵌套成树:<html> 包整页,<head> 装页面信息 (标题、样式链接),<body> 装看得见的内容(<h1> 大标题、<ul>/<li> 列表、 <a href=…> 链接)。浏览器开发者工具里能看到这棵树,它有个名字:DOM (Document Object Model,文档对象模型)8。理解「页面 = 一棵可以编程的树」, 是本章所有 JavaScript 的前提。

CSS 的级联。 样式写在单独的 .css 文件里供多页共用;选择器按元素名匹配 (body {…} 作用于 body 及其所有后代——这就是「层叠 cascade」), 2em 表示「父元素字号的 2 倍」,随屏幕缩放9。两行关键 HTML 别忘了: <link rel="stylesheet" href="styles.css"> 引样式; <meta name="viewport" content="width=device-width, initial-scale=1"> 让手机按真实 宽度渲染而不是缩小成「桌面微缩版」10。

本地预览闭环:电脑上 python -m http.server --directory robot_control, 浏览器开 localhost:8000,改文件刷新即见——网页先在电脑上调好,再上机器人, 这个顺序省无数麻烦11。

6. 核心原理之四:摇杆——用别人的组件,读手指的位置

自回中摇杆不自造,用现成组件 bobboteck/JoyStick(GitHub),PyInfra 下载 joy.js 到 robot_control/libs/12。页面里三行关键:

<div id="joydiv" style="width:50vh;height:50vh;"></div>
<script type="module">
  var joystick = new JoyStick('joydiv', {}, function(stickData) {
      console.log(stickData.x, stickData.y);
  });
</script>
  • <div id="joydiv"> 是给摇杆占的「坑」;50vh = 视口高度的 50%,摇杆因此占半屏, 任何手机上尺寸都对13;
  • type="module" 表示这段脚本是 ES 模块(能 import 别的文件——后面接 MQTT 必需);
  • 回调函数在摇杆被触摸或移动时触发,stickData.x、stickData.y 是相对中心的偏移, 范围 -100 到 10014。

调试姿势:桌面浏览器右键 → Inspect → Console,拖动摇杆,数值在控制台滚动15。 排错要点:404 查 libs/joy.js(libs 带 s);摇杆看不见查 style 里的尺寸16。

7. 核心原理之五:WebSocket——浏览器进 MQTT 的唯一通道

网页里的 JavaScript 有安全限制,不能直接开普通网络连接——连 MQTT 的唯一通道是 WebSocket:一种在浏览器和服务器之间保持长连接的协议17。 Mosquitto 默认不开它,配置 deploy/robot_mosquitto.conf:

listener 1883
protocol mqtt

listener 9001
protocol websockets

注意两个细节:① 一旦给了配置文件,1883 要显式重新声明(书里明说了); ② 不开密码文件、不允许匿名连接,Mosquitto 就不启用 WebSocket——这两行是绑定的18。 配好后 systemctl status mosquitto 的日志里应看到 Loading config file /etc/mosquitto/conf.d/robot.conf19。

网页侧装 mqtt.js(ESM 版,import 用)20,再用一个 .env.js 存机器人地址和 MQTT 账号(主机名 / robot / robot)。这个文件含口令,绝不能进 Git——书里给了 .gitignore 操作,并再一次挂账:「这个应用没有任何安全机制,共享 Wi-Fi 上用要慎重, 第 18 章处理」21。

主线例子走完:手指到轮子的全链路

 手指上推       stickData.y = -59(屏幕坐标,y 向下为正)
 JS 换算        speed = y/100 = -0.59 → 取反约定后 = 前进 0.59
                direction = x/100(左负右正)
 合成两个轮速    [speed + direction, speed - direction]
                例:推上略右 → [0.59+0.2, 0.59-0.2] = [0.79, 0.39]  → 右缓弯
 JSON 打包      JSON.stringify([...]) → "[0.79,0.39]"
 WebSocket 发出  ws://learnrob3.local:9001 → broker → 电机服务
 电机执行        set_motor_wheels:左 79%、右 39%
 手指松开        摇杆自回中 → [0.0, 0.0]...  不对,服务 1 秒无消息自动停

 图说:从皮肤到轮子,中间隔着 6 站,每一站都已经在前面章节建好了——这一章只是把它们接起来。

注意换算的两个设计:摇杆的 ±100 除以 100 变成电机认识的 ±1;「速度 ± 转向」合成两轮速 (speed + direction, speed - direction)——推满上 = 直行,斜推 = 差速弯, 逻辑两行程22。这套消息格式和第 6 章行为脚本用的完全一样——这正是 MQTT 服务层的回报:网页和 Python 行为说同一种语言。

8. 核心原理之六:launcher——让网页指挥 systemd

最后一跃:让网页能启动行为、关掉机器人。新服务 robot/launcher_service.py, 新武器一个——subprocess:Python 里启动别的程序的库23。四个消息处理:

topic消息体动作
launcher/start服务名systemctl start <服务名>
launcher/stop服务名systemctl stop <服务名>
launcher/poweroff空poweroff 关机

行为也注册成 systemd 服务,但自启参数设 False——它只在被点名时启动 (Restart=no + enabled=False 的组合)24。页面上一个 <button id="start">, JavaScript 给按钮挂事件监听器(addEventListener):点击 → 发布 launcher/start + behavior_path.service → launcher 收到 → systemctl 拉起行为25。 关机页同样,只是按钮文案换成「Turn off」,并且单独一页——确认动作单独放一页, 误触就少一次26。

至此闭环完成。书里那句总结值得原样记住:

「现在你只需要在调试时才开 SSH 了。」27

顺带一提:你手动开车时会不自觉地修正跑偏——左轮快了就多推一点右—— 书里点破这是人的闭环控制,机器人自己的闭环(传感器反馈)要从下一章开始装28。

9. 作者的判断与证据

  • 判断: 网页当 App,而不是原生 App 或手柄。理由:浏览器零安装、跨平台、 能直接长 MQTT;代价(无安全机制、WebSocket 配置麻烦)书里如实承认721。
  • 判断: 自回中摇杆 + 「速度 ± 转向」的合成式,而不是左右两个推杆——单手操作友好, 且与两轮差速的消息格式天然对齐22。
  • 判断: launcher 服务是「最小权限的遥控器」——它只暴露 systemctl 的三个动作, 而不是给网页一个 shell。书里没展开安全论证,但架构本身就是论证23。
  • 事实层: unit 模板各行语义、WebSocket 9001 端口、Mosquitto 不启用 WS 的两个前置条件、 .local 在 Android 上不被支持(要查 IP:ip addr show wlan0)——都可上机复核1829。

10. 边界与局限

  • 安全近乎裸奔。 口令 robot/robot 明文、WebSocket 无加密(wss 没讲)、 网页应用无认证——书里两次主动挂账给第 18 章。家用 Wi-Fi 玩玩可以,公共网络不行。
  • 网页控制的延迟没测。 摇杆到轮子的延迟受 Wi-Fi 质量影响,本书没有任何量化测量; 对「遥控车」够用,对高速行为不够。
  • 前端教学是脚手架级别。 HTML/CSS/JS 三件套只够搭起这个应用;样式、布局、 组件化的深水区一律指向 Further reading30。
  • 行为启动是「一次性」的。 launcher 能 start/stop,但没有「行为状态查询」—— 网页不知道行为跑没跑,这个洞到第 8 章的仪表盘才部分补上。

11. 可带走的

  1. systemd unit 五要素:After(依赖顺序)、Restart(always/no)、WorkingDirectory、 ExecStart、KillSignal=SIGINT(给 atexit 留活路)3。
  2. 看服务:systemctl status 看死活,journalctl -u 看日志,-f 实时跟5。
  3. 无头设备的状态要外化:一颗 LED = 服务的「我好了」;退出时 atexit 清灯6。
  4. 网页 = 结构(HTML 树/DOM)+ 样子(CSS 级联)+ 行为(JS 模块);先本地 python -m http.server 调好再上机器人811。
  5. 浏览器连 MQTT 只有 WebSocket 一条路;Mosquitto 要显式开 9001, 且必须配密码文件 + 禁匿名才生效18。
  6. 摇杆 ±100 ÷ 100 → ±1;两轮速 = 速度 ± 转向——网页与行为共用同一套消息格式22。
  7. .env.js 进 .gitignore;这个应用没有安全机制,共享网络别用21。
  8. launcher 模式:网页发消息 → 一个薄服务替它按 systemctl——只暴露需要的动作23。
  9. 危险动作(关机)单独一页 = 防误触26。

12. 原文地图

主题原书位置
为什么用手机当手柄text/67-fm-technical-requirements.txt:28(搜「touchscreen joystick」)
systemd 与 unit 模板text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:34(搜「unit file」) · text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:50(搜「KillSignal=SIGINT」)
deploy_service 与状态命令text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:166(搜「systemctl status」) · text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:180(搜「journalctl」)
LED 就绪灯text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:213(搜「board.leds.set_rgb」)
三件套与 DOMtext/69-fm-building-a-phone-app.txt:13(搜「web-page-based app」) · text/69-fm-building-a-phone-app.txt:88(搜「DOM (Document Object Model)」)
viewport 与 CSS 级联text/69-fm-building-a-phone-app.txt:129(搜「initial-scale=1」)
本地预览与上架机器人text/69-fm-building-a-phone-app.txt:66(搜「http.server」) · text/69-fm-building-a-phone-app.txt:175(搜「web_server」)
摇杆组件与 ±100text/69-fm-building-a-phone-app.txt:316(搜「stickData.x and stickData.y」)
WebSocket 配置text/70-fm-connecting-the-phone-app-to-mqtt.txt:40(搜「WebSockets are the only option」) · text/70-fm-connecting-the-phone-app-to-mqtt.txt:52(搜「listener 9001」)
mqtt.js 与 .env.jstext/70-fm-connecting-the-phone-app-to-mqtt.txt:132(搜「.env.js contains secrets」) · text/70-fm-connecting-the-phone-app-to-mqtt.txt:168(搜「no security mechanism」)
摇杆到轮子的换算text/70-fm-connecting-the-phone-app-to-mqtt.txt:196(搜「speed + direction」)
Android 要 IPtext/70-fm-connecting-the-phone-app-to-mqtt.txt:224(搜「Android doesn't support」)
launcher 服务text/71-fm-adding-more-control-options-to-the-web-app.txt:58(搜「subprocess module is」) · text/71-fm-adding-more-control-options-to-the-web-app.txt:91(搜「auto_start is set to False」)
关机页text/71-fm-adding-more-control-options-to-the-web-app.txt:155(搜「Turn off」)
章节总结text/72-fm-summary.txt:5(搜「only need an SSH session when」) · text/72-fm-summary.txt:21(搜「correcting for veer」)

Footnotes

  1. 出处:「Technical requirements」第 28 段(text/67-fm-technical-requirements.txt:28,搜「touchscreen joystick」)。原文:SSH 敲命令启停又慢又易错;游戏手柄要花钱;口袋里的手机自带 Wi-Fi、热点、触摸屏——「把它当成一个无线触摸屏摇杆」。 ↩ ↩2

  2. 出处:「Making services start when the Raspberry Pi starts」第 22 段(text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:22,搜「automatically launch programs」)。原文 Note:systemd 用于自动启动程序;控制生命周期、查看状态;能控制何时跑、怎么跑、以哪个用户、在哪个目录启动,并把日志收为 journal;systemctl 是操控命令。 ↩

  3. 出处:「Making services start when the Raspberry Pi starts」第 34 段(text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:34,搜「unit file」)与第 44 段(搜「KillSignal=SIGINT」)。原文:unit 文件按 [节] 组织;After=mosquitto.service 保证 broker 先起;Restart 用模板变量(always/no);WorkingDirectory 用用户主目录;ExecStart 用 robotpython -u 保证日志不缓冲;KillSignal=SIGINT 确保 atexit 代码能运行;WantedBy=multi-user.target 让开机启动。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  4. 出处:「Making services start when the Raspberry Pi starts」第 79 段(text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:79,搜「deploy_service」)。原文:函数四参(服务名/命令/自启/变更标记);写 /etc/systemd/system/<名>.service;changed 时 daemon_reload 并重启;enabled=自启,running=立即启动。 ↩

  5. 出处:「Making services start when the Raspberry Pi starts」第 180 段(text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:180,搜「journalctl」)。原文:journalctl -u <服务> 看完整日志,方向键滚动、Shift+G 到末尾、q 退出;-f 实时跟随,Ctrl+C 停止。 ↩ ↩2

  6. 出处:「Making services start when the Raspberry Pi starts」第 213 段(text/68-fm-making-services-start-when-the-raspberry-pi-star.txt:213,搜「board.leds.set_rgb」)。原文:exit_handler 停车 + leds.clear();连上 broker 后 set_rgb(0, 0, 255, 0) 点绿第一颗 LED;断电仅用电池重开,绿灯亮即自动运行成功。 ↩ ↩2 ↩3

  7. 出处:「Building a phone app」第 13 段(text/69-fm-building-a-phone-app.txt:13,搜「web-page-based app」)。原文:建基于网页的 App,三工具——HTML 呈现结构(文字、输入、按钮、链接),CSS 管样式布局(颜色、字体、位置、大小),JS 写页面上的代码并对接 MQTT 服务。 ↩ ↩2

  8. 出处:「Building a phone app」第 88 段(text/69-fm-building-a-phone-app.txt:88,搜「DOM (Document Object Model)」)。原文:元素开闭标签嵌套成树;有子元素可展开;容器是枝、无子元素是叶;这棵树叫 DOM。 ↩ ↩2

  9. 出处:「Building a phone app」第 99 段(text/69-fm-building-a-phone-app.txt:99,搜「Cascading Style Sheets」)。原文:CSS 指定元素大小、颜色、字体;样式会级联到子元素;body 设字体全家生效;h1 用 2em(em = 父元素字号倍数)放大居中。 ↩

  10. 出处:「Building a phone app」第 129 段(text/69-fm-building-a-phone-app.txt:129,搜「initial-scale=1」)。原文:link 标签引 styles.css;meta viewport 让手机视图与桌面同比例;手机上字太小多半是漏了这一行。 ↩

  11. 出处:「Building a phone app」第 66 段(text/69-fm-building-a-phone-app.txt:66,搜「http.server」)。原文:python -m http.server --directory robot_control 本地起服务,浏览器开 0.0.0.0:8000;VS Code 可点链接直接开;在电脑上调样式极有用。 ↩ ↩2

  12. 出处:「Building a phone app」第 255 段(text/69-fm-building-a-phone-app.txt:255,搜「JoyStick」)。原文:用 bobboteck/JoyStick 组件(GitHub);PyInfra 建 libs 目录并下载 joy.js。 ↩

  13. 出处:「Building a phone app」第 299 段(text/69-fm-building-a-phone-app.txt:299,搜「50vh」)。原文:div 是容器;50vh = 视口高度的 50%,摇杆因此占半屏;id=joydiv 供 JS 引用。 ↩

  14. 出处:「Building a phone app」第 316 段(text/69-fm-building-a-phone-app.txt:316,搜「stickData.x and stickData.y」)。原文:回调在触摸/移动时触发;stickData.x/y 是相对中心的偏移,-100 到 100;type=module 才能 import。 ↩

  15. 出处:「Building a phone app」第 333 段(text/69-fm-building-a-phone-app.txt:333,搜「Inspect」)。原文:右键 Inspect 开开发者面板,Console 里拖动摇杆可见数值滚动。 ↩

  16. 出处:「Connecting the phone app to MQTT」第 18 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:18,搜「404 errors」)。原文排错:404 查 libs/joy.js(注意 s);SSH 里 ls robot_control/libs;摇杆不可见查 style 属性;无输出查回调函数是否传给 JoyStick。 ↩

  17. 出处:「Connecting the phone app to MQTT」第 40 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:40,搜「WebSockets are the only option」)。原文:网页的 JS 不能直接连网络服务,WebSocket 让它和机器人保持长连接、收发消息;这是网页直连 MQTT 的唯一选项;Mosquitto 默认不启用。 ↩

  18. 出处:「Connecting the phone app to MQTT」第 52 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:52,搜「listener 9001」)。原文:加配置文件后必须显式重开 1883;WS 端口开在 9001;不设密码文件、不关匿名连接,Mosquitto 就不启用 WebSocket。 ↩ ↩2 ↩3

  19. 出处:「Connecting the phone app to MQTT」第 235 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:235,搜「Loading config file」)。原文:systemctl status mosquitto 应见 Active: active (running) 和 Loading config file /etc/mosquitto/conf.d/robot.conf;问题查 deploy/robot_mosquitto.conf 并重跑 pyinfra;内部日志 sudo tail /var/log/mosquitto/mosquitto.log。 ↩

  20. 出处:「Connecting the phone app to MQTT」第 113 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:113,搜「mqtt.esm.js」)。原文:下载 mqtt.js 的 ESM 变体(unpkg mqtt@5.7.0)以便 import。 ↩

  21. 出处:「Connecting the phone app to MQTT」第 168 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:168,搜「no security mechanism」)。原文 Note:.env.js 含口令,别进 Git(echo .env.js >> .gitignore);应用无安全机制/认证/密码,本书范围之外,共享 Wi-Fi 上要认真考虑,第 18 章处理。 ↩ ↩2 ↩3

  22. 出处:「Connecting the phone app to MQTT」第 196 段(text/70-fm-connecting-the-phone-app-to-mqtt.txt:196,搜「speed + direction」)。原文:x、y 各除 100 得 -1..1;speed+direction 与 speed-direction 合成两轮速;JSON.stringify 打包;topic 与格式和第 6 章行为完全一致。 ↩ ↩2 ↩3

  23. 出处:「Adding more control options to the web app」第 58 段(text/71-fm-adding-more-control-options-to-the-web-app.txt:58,搜「subprocess module is」)。原文:subprocess 是启动其他程序的 Python 库;launcher/start、stop 把消息体当服务名传给 systemctl;poweroff 忽略消息体。 ↩ ↩2 ↩3

  24. 出处:「Adding more control options to the web app」第 91 段(text/71-fm-adding-more-control-options-to-the-web-app.txt:91,搜「auto_start is set to False」)。原文:行为也注册成服务,但 auto_start=False,只被 systemctl 点名时启动;Tip:拆成服务后,修改和排错可以瞄准系统的具体部分。 ↩

  25. 出处:「Adding more control options to the web app」第 124 段(text/71-fm-adding-more-control-options-to-the-web-app.txt:124,搜「addEventListener」)。原文:button id=start;getElementById 找到按钮,addEventListener 监听点击,发布 launcher/start + behavior_path.service。 ↩

  26. 出处:「Adding more control options to the web app」第 155 段(text/71-fm-adding-more-control-options-to-the-web-app.txt:155,搜「Turn off」)。原文:关机页复制行为页改几处;Turn off 按钮发 launcher/poweroff;危险动作单独一页,误触更少。 ↩ ↩2

  27. 出处:「Summary」第 5 段(text/72-fm-summary.txt:5,搜「only need an SSH session when」)。原文:「You now only need an SSH session when debugging!」。 ↩

  28. 出处:「Summary」第 21 段(text/72-fm-summary.txt:21,搜「correcting for veer」)。原文:手动驾驶要练一阵;你会不自觉地用手修正跑偏(电机表现略不同)。 ↩

  29. 出处:「Serving HTML from the Raspberry Pi」第 192 段(text/69-fm-building-a-phone-app.txt:192,搜「ip addr show wlan0」)。原文:Android 不认 .local,要查 IP——inet 后四个数即地址(如 192.168.0.137),换网络会变。 ↩

  30. 出处:「Challenges」第 7 段(text/73-fm-challenges.txt:7,搜「w3schools」)。原文:样式改进指 w3schools CSS 教程(「深不见底的兔子洞」);安全挑战指 systemd EnvFile 方案,第 18 章细讲。 ↩

On this page