Published on

用 ⌚📱💻 控制寝室空调

Authors
  • Name

上学期有若干次到实验室后才想起寝室空调还开着,因为实验室到寝室有将近一公里,我又懒得专门回去关空调,最终的结果通常是空调白白开到晚上。一直觉得可以用树莓派连接一下边缘设备和空调,但是一直没空去做。在七夕这天,为了解决这个问题,我终于对寝室的空调遥控器下手了。


本文部分参考自这篇博客[1]

硬件

准备

除了一台树莓派,还需要红外发射/接收模块(另外为了连接红外模块,还需要有面包板和跳线)。

准备材料

接线

树莓派的引脚信息可以在这个网站[2]看到。我在这里给发射模块接了 5V 的电源,信号输入端接在引脚 GPIO17 上;给接收模块接了 3V 的电源(引脚 17),信号输出端接在 GPIO18 上。

WARNING

要注意的是,引脚标号和 GPIO 标号是两个概念,比如红外发射模块需要将其 DATA/IN 接口接到 GPIO17 上,如果误接到引脚 17 (3V3 Power) 上,就会造成红外发射模块一直发射。我在某次尝试中就这样误接了,然后发现红外接收模块一直闪红灯、发射模块变得很烫。

接线结果

软件

硬件部署好后,需要在树莓派上控制红外模块进行发射与接收。参考的博客[1:1]中使用的是 LIRC [3],然而在我的 case 下并没法成功。我转而使用 v4l-utils 包所提供的 ir-ctl 功能对红外信号进行录制和重放。

$ sudo pacman -S v4l-utils

修改启动参数

WARNING

如果树莓派系统是 archlinuxarm[4],需要安装的是 ARMv7 版本而非 aarch64 版本。因为在 alarm 的安装指南[5]上提到:

ARMv7 Installation

Use this installation if you require any of the vendor's kernel hacks, overlays, or closed-source GPU blobs and utilities.

我们需要使用启动参数中的 dtoverlay 项,而 aarch64 的 alarm 并不支持这些启动参数。

树莓派需要修改启动参数来启用对红外接收与发射模块的控制。上面的红外接收模块的信号端接在了引脚 18 上,红外发射模块的信号端接在了引脚 17 上,所以在 /boot/config.txt 中添加:

# Infrared
dtoverlay=gpio-ir,gpio_pin=18
dtoverlay=gpio-ir-tx,gpio_pin=17

INFO

根据一篇树莓派论坛上的帖子,某次内核更新后,启动参数中的 lirc-rpi 不再有用,取而代之的是 gpio-ir,也就是以上两行中使用的。更多信息可以看看这篇帖子[6]

这两行中,gpio-ir 表示红外接收模块,其对应的引脚是 18;gpio-ir-tx 表示红外发射模块,其对应的引脚是 17。

/boot/config.txt 的键值文档可以在树莓派的 /boot/overlays/README (在 Arch Linux ARM for Raspberry Pi[4:1] 上有这个文件,别的发行版不清楚) 查阅。

接收与发射

📡 接收

被参考的博客[1:2]中在这部分用了 LIRC 提供的 irrecord 命令。我在实际使用时没什么问题,但是后面尝试发射这里录入的信号时会发射失败。最后我用了 ir-ctl[7] 命令来录:

$ sudo ir-ctl --receive --device /dev/lirc1 -1 --mode2

WARNING

有一个坑是个别品牌(如 TCL 的某型号)的空调遥控器在改变当前空调状态时,会发射不止一串控制信号。如果使用上面的命令,会造成仅仅录制到了第一串控制信号,而空调并不会识别这单个一串信号。如果发现录制的信号用发射模块发射后,接收模块(我们接在 GPIO18 上的)能够接收到信号,但是空调无论如何都没有反应,则去掉以上的 -1 flag 再录入。

要是想看空调遥控器在按下一个按键时会发射几串信号,那么把 --mode2 flag 也去掉,即:

$ sudo ir-ctl --receive --device /dev/lirc1

看看输出有几行,就是几串信号。

执行这个命令,会从设备 /dev/lirc1 等待第一串接收到的红外信号,以 mode2 格式输出到 stdout。上面的命令执行后,对着接收模块按一下开机键,终端会输出一串类似下面的信号:

pulse 4391
space 4466
pulse 492
...
pulse 469

这串信号就是开机后的空调状态的控制信号。把这串信号以纯文本原样存储在一个文件里就可以被用于后续读入并发送。比如,我希望存储一个制冷、自动风力、27 摄氏度的空调状态控制信号:

$ sudo ir-ctl --receive --device /dev/lirc1  -1 --mode2 | tee cold-auto-27

将把 mode2 格式的控制信号存储在文件 code-auto-27 中。存储的信号被用于后面读取并发射。

🚀发射!

仍然用 ir-ctl 命令(正如其 manpage[7:1] 所言,这是 a swiss-knife tool to handle raw IR and to set lirc options):

$ sudo ir-ctl --send ./cold-auto-27

如果各模块接线正常且发射器与空调之间没有明显遮挡(其实我用大功率发射模块的测试中尽管有明显遮挡也没关系),此时应当能听到空调清脆的“滴”声。

💦 Tedious work

为了进行方便的索引,把录制的各种信号文件组织成了 <模式>/<风力>/<温度> 的文件结构:

.
├── cold/
│  ├── auto/
│  │  ├── 17
│  │  ├── 24
│  │  ├── 27
│  ├── high/
│  │  ├── 17
...
└── off

用 ⌚📱💻 控制寝室空调

如果只能在树莓派上用命令行控制空调也太麻烦了(而且一点也不酷)。以前用 Apple Watch 的邮件应用时注意到邮件里的链接能点开,这说明 Watch OS 内置了个浏览器,所以用网页应用控制前面这段电路就是个不错的通用解决方案(“通用”是指“有浏览器就能用”)。

昨天是七夕,在寝室看了一天 The Rust BookVue 的文档。今天就现学现用,用 Vue (虽然后来知道其实没必要)写了个能看能用的前端,并用 Rust 写了服务器后端,跑在树莓派上反向代理到了公网。

前端

这部分就是五个按钮:分别控制 <模式>/<风力>/<温度> 状态的三个按钮加上一个发送键和一个关机键。预期用法是我在页面上配置好状态后用发送/关机键给后端 POST 一个空调状态。

后端

先用 PHP 写了个后端用来测试,如果接收到 POST 到的空调状态,就调用 sudo ir-ctl 发送对应的信号:

<?php
$data = json_decode(file_get_contents("php://input"), true);

if ($data["on"]) {
    $mode = $data["state"]["mode"];
    $strength = $data["state"]["strength"];
    $temperature = $data["state"]["temperature"];

    exec("sudo ir-ctl --send /path/to/infrared/air-conditioner/$mode/$strength/$temperature");
} else {
    exec("sudo ir-ctl --send /path/to/infrared/air-conditioner/off");
}
?>

直接在终端运行 PHP 内置的服务器来测试:

$ php -S 0:8899 -t frontend/dist
[...date...] PHP 8.0.10 Development Server (http://0:8899) started

然后用手机打开页面设置状态并按发送键,空调并没有发出“滴”声,因为终端出现了 sudo prompt。

[sudo] password for gy:

输入密码后空调才有反应。这可不行呀,我总不能每次重启树莓派都上来手动输一次密码吧。想要绕过这个问题其实也不难,最无脑的方法就是直接用 root 跑这个服务器呗,然而这样做会引入没人想面对的安全问题(Disclaimer: 其实没人关心我的树莓派,但这不影响我希望它可以足够安全)。

权限问题

ir-ctl 需要 root 权限才能执行,又不想用 root 跑我的后端,怎么办?我的树莓派是用 sudo 来管理权限的,可以修改 /etc/sudoers 文件,仅允许用户无密码执行 sudo ir-ctl。在 /etc/sudoers 里添加:

Cmnd_Alias INFRARED_CTRL = /usr/bin/irsend, /usr/bin/irsimsend, \
                           /usr/bin/irrecord, /usr/bin/irsimreceive, \
                           /usr/bin/ir-ctl
# users in group infrared can control infrared devices via INFRARED_CTRL
# command group
%infrared ALL = NOPASSWD: INFRARED_CTRL

然后把当前用户添加到 infrared 组:

$ sudo usermod -aG infrared gy

重启后检查用户所在组,以及 sudo ir-ctl 是否需要输入密码:

$ groups
wheel gy informant infrared

$ sudo ir-ctl
Usage: ir-ctl [OPTION...] --features
  or:  ir-ctl [OPTION...] --receive [save to file]
  or:  ir-ctl [OPTION...] --send [file to send]
  or:  ir-ctl [OPTION...] --scancode [scancode to send]
  or:  ir-ctl [OPTION...] --keycode [keycode to send]
  or:  ir-ctl [OPTION...] [to set lirc option]
Try `ir-ctl --help' or `ir-ctl --usage' for more information.

可以看到 sudo prompt 已经不再出现了,这时候再运行服务器,手机按下状态发送键后,立刻就听到了空调的“滴”声。

部署

最后,用底裤D[8]进行服务器的开机自动启动:

$ cat ~/.config/systemd/user/acremote.service
[Unit]
Description=acremote.service: I run acremote backend.

[Service]
Type=simple
WorkingDirectory=%h/.config/infrared/air-conditioner/frontend/dist
ExecStart=%h/.cargo/bin/acremote-backend --listen-port 12682 --server-root .
Restart=always
RestartSec=3s

[Install]
WantedBy=default.target

$ systemctl --user enable --now acremote
Created symlink /home/gy/.config/systemd/user/default.target.wants/acremote.service → /home/gy/.config/systemd/user/acremote.service

把这个后端的端口(此处 12682)反向代理到有公网 IP 的服务器上,就完成了!

完成

最终效果


  1. https://segmentfault.com/... ↩︎ ↩︎ ↩︎

  2. https://pinout.xyz/# ↩︎

  3. https://wiki.archlinux.org/... ↩︎

  4. https://archlinuxarm.org/... ↩︎ ↩︎

  5. https://archlinuxarm.org/... ↩︎

  6. https://forums.raspberrypi.com/... ↩︎

  7. ir-ctl(1) ↩︎ ↩︎

  8. https://systemd.io ↩︎