Published on

用 ⌚📱💻 控制寝室空调

Authors
  • Name

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


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

硬件

准备

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

准备材料

接线

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

接线结果

软件

硬件部署好后,需要在树莓派上控制红外模块进行发射与接收。这部分用到了 LIRC (Linux Infrared Remote Control) [3] 所提供的若干硬件控制命令。

$ sudo pacman -S lirc

修改启动参数

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

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

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

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

接收与发射

📡 接收

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

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

执行这个命令,会从设备 /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[5: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 Book 和 Vue 的文档。今天就现学现用,用 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[6]进行服务器的开机自动启动:

$ 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. ir-ctl(1) ↩︎ ↩︎

  6. https://systemd.io ↩︎