起因#
租住的公寓提供的是美的空调,很遗憾,并不支持智能家居的接入。
随着天气转热,每天又陷入了开关空调的微妙琐事之中。虽然大部分时间只是伸伸手,但依旧很打断上网的冲浪体验,加上这倒霉设备不能自动记住上次风速,所以开关后又要额外调节风速到自己喜欢的程度,对其不满便更是与日俱增起来。
终于在上周,上了一天班回来发现出门忘记关掉,更是心疼起电费。于是看了下架子上充当旁路由吃灰的树莓派,干脆折腾一下。
额外材料#
- 红外接收管(录制遥控器红外信号编码)
- 红外发射管(发射控制信号)
- 杜邦线(连接作用)
- 三极管(可选,看着说是可以增强信号,自己并没有用到)
软件安装#
我的设备及系统信息如下:
ubuntu@ubuntu:~$ uname -a
Linux ubuntu 5.15.0-1027-raspi #29-Ubuntu SMP PREEMPT Mon Apr 3 10:12:21 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux
先安装 lirc
相关的包
sudo apt update
sudo apt install lirc
安装完毕后配置 config
,这一步教程大多给的是 /boot/config.txt
位置,但是自己这边并非如此,注意甄别吧
sudo vi /boot/firmware/config.txt
# 追加如下内容
dtoverlay=gpio-ir,gpio_pin=18
dtoverlay=gpio-ir-tx,gpio_pin=17
修改 lirc 配置
sudo vi /etc/lirc/lirc_options.conf
# 修改之前
driver = devinput
device = auto
# 修改之后
driver = default
device = /dev/lirc0
操作完毕后,重启树莓派,令其生效。
硬件连接#
查询资料可知,树莓派的具体引脚定义如下:
红外接收管连接#
把红外接收管的凸起朝上,由左至右,引脚分别为数据,负极,正极。数据引脚用杜邦线连接到 GPIO18,负极接到地线,正极接到 3.3v 供电
红外发射管连接#
发射管的情况如图,长脚为正极,接入 GPIO17,短脚接入地线即可。
红外信号相关#
信号记录#
要控制空调,最初始的思路就是用红外管模拟遥控器按键信号,然后编码控制。
要模拟信号,我们需要先记录之。在终端执行 mode2 -m -d /dev/lirc1
,就可以开始记录。这时用遥控器按下一个按键,可以看到屏幕上类似下面的输出:
ubuntu@ubuntu:/boot/firmware$ mode2 -m -d /dev/lirc1
Using driver default on device /dev/lirc1
Trying device: /dev/lirc1
Using device: /dev/lirc1
16777215
4475 4380 576 1580 574 497
579 1580 582 1576 571 500
584 498 576 1578 570 497
585 491 579 1575 580 498
580 496 580 1575 580 1585
570 496 581 1582 571 1575
583 493 586 1582 566 1582
573 1576 579 1575 579 1575
579 1581 573 497 579 1577
578 496 580 498 579 497
582 503 574 497 575 498
579 499 579 1575 578 498
580 498 584 491 580 497
578 498 578 507 579 1586
567 504 573 1581 588 1561
578 1575 583 1571 580 1576
579 1576 573 5184 4422 4409
573 1581 549 524 580 1577
576 1579 577 497 577 502
556 1600 553 568 505 543
558 1586 570 497 575 502
558 1597 550 1606 552 521
555 1629 549 1600 529 542
532 1605 552 1600 555 1624
530 1602 555 1625 550 1578
553 542 530 1627 554 520
530 545 533 545 528 546
533 542 549 529 531 547
533 1623 528 547 528 547
531 545 532 545 556 521
532 542 531 1631 528 545
529 1623 531 1624 532 1623
531 1625 555 1600 528 1626
532 5224 4401 4434 525 1627
529 1624 527 550 529 1626
522 545 531 1624 531 545
532 1624 530 545 532 1624
531 1624 531 545 532 545
532 1624 530 1630 525 545
531 545 531 548 529 545
531 546 531 547 532 552
535 538 531 548 530 544
532 545 529 548 530 543
532 547 532 549 529 547
531 538 531 553 523 546
531 546 530 546 534 543
530 546 532 553 528 543
534 543 531 546 530 1626
526 1624 530 1628 526 547
530 1625 536 1623 524 17394-pulse
舍弃掉第一行的 16777215
与最后面的 17394-pulse
,剩余的数据便是我们要记录的当前按键代表的信息(比如我这里是 制冷,24 度,风速 1%)。
配置编写#
以此,我们便可以编写自己的配置:
sudo vi sudo vi /etc/lirc/lircd.conf.d/aircon.lircd.conf
# 内容如下:
begin remote
name aircon // 设备名称,可自行修改
flags RAW_CODES
eps 30
aeps 100
gap 19991
begin raw_codes
name c_25_1
4469 4386 578 1574 581 496 580 1577 577 1574 580 497 584 494 578 1574 580 497 580 497 580 1575 580 496 580 498 578 1575 580 1574 580 496 580 1575 579 1574 580 499 579 1575 579 1575 579 1575 580 1575 581 1573 580 1574 579 498 580 1577 577 498 579 497 582 508 570 493 580 497 581 496 579 1575 579 1575 580 498 579 497 580 496 556 521 579 498 581 496 580 497 579 497 580 1575 579 1577 578 1577 577 1575 579 1576 580 1576 580 5174 4449 4383 581 1574 555 520 579 1576 579 1577 577 501 577 496 579 1576 555 521 579 498 556 1598 580 499 578 497 556 1598 556 1599 555 521 556 1601 554 1599 579 501 554 1597 555 1599 579 1576 555 1599 578 1576 579 1575 556 521 579 1577 577 499 577 522 556 497 555 544 532 545 532 521 582 1575 554 1599 556 545 531 522 578 497 578 499 556 521 556 521 578 498 555 522 554 1600 578 1578 578 1576 554 1599 555 1623 531 1623 532 5201 4423 4408 555 1600 555 1623 531 545 531 1623 532 547 531 1622 555 521 532 1623 533 544 532 1623 532 1623 532 545 531 545 556 1599 531 1623 531 545 532 545 557 520 532 545 531 545 532 545 556 520 532 546 531 545 531 545 531 545 532 545 531 545 531 546 531 546 531 545 532 545 532 546 531 546 556 521 531 549 529 546 529 545 533 544 531 545 532 545 532 544 531 1623 532 1623 531 1629 526 546 531 1624 531 1623 531
name off
4465 4388 578 1574 580 496 583 1572 580 1577 580 496 578 497 580 1575 580 496 581 496 580 1574 581 496 581 496 581 1577 578 1574 581 502 574 1575 579 496 584 1571 581 1574 581 1574 581 1575 580 496 580 1575 580 1574 580 1575 583 494 582 495 580 497 580 498 578 1574 580 497 580 497 580 1575 580 1576 579 1574 580 498 582 495 580 497 581 497 580 499 578 497 581 497 578 497 582 1572 580 1576 581 1574 579 1576 579 1575 580 5179 4450 4382 579 1575 579 500 577 1575 580 1576 578 497 556 521 580 1577 580 496 580 497 579 1579 576 497 579 506 574 1576 552 1599 581 496 579 1576 579 498 581 1575 578 1579 576 1576 579 1577 578 498 579 1575 580 1575 579 1577 555 521 556 521 579 498 556 524 578 1576 577 498 579 498 556 1599 556 1599 579 1576 578 498 579 499 555 522 555 521 556 521 556 522 578 497 556 522 558 1597 579 1575 558 1597 555 1602 576 1576 579
end raw_codes
end remote
此处定义了两条指令:c_25_1 代表制冷,25 度,1% 风速;off 代表关闭空调。
保存文件,继续执行 sudo systemctl restart lircd
重启服务,让配置生效。
这时,就可以在终端中发送指令:irsend send_once aircon c_25_1
来完成空调的控制。
编码理解#
无脑的拷贝编码虽然方便,但是总有种雾里看花的不爽快感觉。那么尝试着分析一下具体含义。
注意,下面内容均基于《美的 R05D/BG 型号遥控器电控功能规格书》分析而来,感兴趣者请自行阅读原文
具体结构#
由搜集的资料可知,在只涉及 温度
,模式
,风力强弱(非具体风速)
的情况下,编码的结构大致为
LAA'BB'CC'SLAA'BB'CC'Z
其中,我们可以设定:
L 为引导码,对应信号 [4500, 4500]
S 为分隔码,对应信号 [540, 5200]
Z 为结束标识,对应信号 550
A 为固定的识别码,对应二进制序列 10110010
B 和风速有关,C 和温度以及模式有关
并且,二进制序列和信号编码存在互换关系:1 代表高电平,对应信号 [540, 1600]
,0 代表低电平,对应信号:[550, 550]
那么经过换算,上面的 A(识别码)便对应如此数据:540 1600 550 550 540 1600 540 1600 550 550 550 550 540 1600 550 550
。
数据整体分为两段,除开引导码和分隔码,两段数据的内容完全一致。每次按键,无论是为了调节风速还是温度,均包含上述信息。
编码区域关系#
注意,此处仅按照图中规格对照,暂不讨论实际情况
经由规格书可知大致存在如下关系:
那么可以编写数据 ABC
,内容如下:
10110010 10011111 01000000
可以对照翻译为
固定识别码 低风 制冷 24度
由于数据结构为 AA'BB'CC'
,其中 X' 代表 X 的反码,那么可扩充为:
10110010 01001101 10011111 01100000 01000000 10111111
按照上面的高低电平规则转换,可以得到 LAA'BB'CC'
的规则如下:
4500 4500 540 1600 550 550 540 1600 540 1600 550 550 550 550 540 1600 550 550 550 550 550 550 540 1600 550 550 550 550 540 1600 540 1600 550 550 540 1600 550 550 540 1600 550 550 550 550 540 1600 540 1600 540 1600 540 1600 540 1600 550 550 550 550 540 1600 540 1600 550 550 550 550 550 550 550 550 550 550 550 550 550 550 540 1600 550 550 550 550 550 550 550 550 550 550 550 550 550 550 540 1600 550 550 540 1600 540 1600 540 1600 540 1600 540 1600 540 1600
按照结构继续扩充,得到 LAA'BB'CC'SLAA'BB'CC'Z
的规则:
4500 4500 540 1600 550 550 540 1600 540 1600 550 550 550 550 540 1600 550 550 550 550 550 550 540 1600 550 550 550 550 540 1600 540 1600 550 550 540 1600 550 550 540 1600 550 550 550 550 540 1600 540 1600 540 1600 540 1600 540 1600 550 550 550 550 540 1600 540 1600 550 550 550 550 550 550 550 550 550 550 550 550 550 550 540 1600 550 550 550 550 550 550 550 550 550 550 550 550 550 550 540 1600 550 550 540 1600 540 5200 4500 4500 540 1600 540 1600 540 1600 540 1600 540 1600 550 550 540 1600 550 550 540 1600 540 1600 550 550 550 550 540 1600 550 550 550 550 550 550 540 1600 550 550 550 550 540 1600 540 1600 550 550 540 1600 550 550 540 1600 550 550 550 550 540 1600 540 1600 540 1600 540 1600 540 1600 550 550 550 550 540 1600 540 1600 550 550 550 550 550 550 550 550 550 550 550 550 550 550 540 1600 550 550 550 550 550 550 550 550 540 5200 4500 4500 550 550 550 550 550 550 540 1600 550 550 540 1600 540 1600 540 1600 540 1600 540 1600 540 1600 550
理解进阶#
至此,理解了大致规则后,觉得可以编写脚本自动生成各种组合了?
然而并不能。
查阅的资料发布时间是 2008 年,距今已经 15 年了。虽然部分规则可以对照得上,但是很多细节已经有了区别。
其中最显著的差异:我的空调是可以具体调节风速等级的,从 1% 到 100%。而在抓取了这部分的红外编码后,发现其格式如下:
LAA'BB'CC'SLAA'BB'CC'SLDEFNNGZ
可知,其数据长度从两端扩展成三段,并且第三段不再为 AA'BB'CC'
这种互为反码的结构。
于是,我使用 mode2 -m -d /dev/lirc1 | tee -a output.conf
命令,把同样模式和温度下,风速从 1 到 100 的编码全部记录,并用脚本转换为二进制序列,寻找其中规律
可以直接说结论,对于额外的数据来说,大致有如下规律:
额外块的第一行固定 11010101
第二行风速的百分比二进制(左侧填充0
)
第三行固定填充 0
第四行只有在100
风速的时候为 00000010
,其余时候填充 0
第五行固定填充 0
第六行自10
进制的214
开始填充,到255
变为从1
开始,到99
填充到56
,100
为特殊值,对应59
(实际均转换为二进制表达)
由于我个人只需要
温度
,模式
,风速
三个参数,加上资料的缺失,并未继续深入研究。
至此,可以勉强编写脚本来生成 1~100
风速,17~30
温度,冷/热
模式 的不同组合命令。
自动化探究#
虽然已经研究了很多,但是对实际需求的推进并不理想。毕竟,我需要的是足够方便地控制空调,而不是研究如何山寨一个空调遥控器。
那么继续扩展思路:
API 调用#
要实现一定程度上的智能 / 自动化,最简单,也最容易实现的方案就是暴露一个 API 接口,供给第三方服务调用之。
出于时间因素,这里使用 golang 做一个尽可能最小化的实现:
// main.go 文件
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
var isON = false
func setupRouter() *gin.Engine {
r := gin.Default()
r.GET("/aircon/:mode/:temp/:flow", func(c *gin.Context) {
mode := c.Params.ByName("mode")
temp := c.Params.ByName("temp")
flow := c.Params.ByName("flow")
cmd := mode + "_" + temp + "_" + flow
isON = true
IrsenAircon(cmd)
c.JSON(http.StatusOK, gin.H{"cmd": cmd})
})
r.GET("/aircon/off", func(c *gin.Context) {
IrsenAircon("off")
isON = false
c.JSON(http.StatusOK, gin.H{"cmd": "off"})
})
r.GET("/aircon/status", func(c *gin.Context) {
var result string
if isON {
result = "1"
} else {
result = "0"
}
c.String(http.StatusOK, result)
})
return r
}
func main() {
IrsenAircon("off")
r := setupRouter()
r.Run(":9997")
}
// aircon.go 文件
package main
import (
"fmt"
"log"
"os/exec"
)
func IrsenAircon(cmd string) {
cmdOutput, err := exec.Command("irsend", "send_once", "aircon", cmd).Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", cmdOutput)
}
将上述代码编译为树莓派 4B 可运行的二进制:
export GOARCH=arm64
export GOROOT_BOOTSTRAP=/usr/local/go
export GOOS=linux
go build
将编译好的二进制文件上传到树莓派即可。
这两个文件实现的内容很简单:在 9997 端口上暴露接口,通过 GET 请求拼接出不同的命令,传递给系统内的 irsend
二进制文件,发送对应的红外信号。
须知,当发送了含有当前场景信息的信号时,空调会直接被开启。所以并不存在单独的开启
命令,但是会存在固定的关闭
命令。
我的树莓派的内网 IP 为 192.168.2.224
。那么根据接口,有三组命令:
# 空调设置为 25度,制冷,风速1%
➜ Desktop curl http://192.168.2.224:9997/aircon/c/25/1
{"cmd":"c_25_1"}⏎
# 关闭空调
➜ Desktop curl http://192.168.2.224:9997/aircon/off
{"cmd":"off"}⏎
# 查询状态(0代表关闭,1代表开启)
➜ Desktop curl http://192.168.2.224:9997/aircon/status
0⏎
空调无法向树莓派回调自身的当前状态,我们也无法主动探测其当前的运行信息,所以一旦发射信号的操作被执行,我们就认为是成功,并且更新内部维护的开关状态。而为了近可能保持状态的准确性,在启动程序的时候先执行一次关闭空调的操作作为校准。
最后,使用 pm2 守护对应进程:
pm2 --name lirc-web start /mnt/sda/Downloads/lirc-web
# 按提示操作,设置自启动
pm2 startup
sudo env PATH=$PATH:/home/ubuntu/.nvm/versions/node/v18.13.0/bin /home/ubuntu/.nvm/versions/node/v18.13.0/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
「智能家居」#
虽然实现了编程接口,但是并不代表需求被全部解决。如果控制空调靠 在浏览器访问 url
或者 命令行中发起 curl
,那还不如使用遥控器。
由于 macOS 和 iPadOS 上均自带 家庭
App,可以进行智能家居的联动控制,那么只要将上面简陋的接口接入 HomeKit
,就可以进一步提升便捷程度。
HomeBridge#
我们使用 HomeBridge
应用来完成这一步骤。其官网的描述如下:
Homebridge 允许您与不支持 HomeKit 的智能家居设备集成。有超过 2,000 个 Homebridge 插件,支持数千种不同的智能配件。
不废话,直接 Docker-Compose
一把梭:
cd ~/docker/homebridge/
vi docker-compose.yml
# 编写如下内容:
version: '3'
services:
homebridge:
image: oznu/homebridge:ubuntu
container_name: homebridge
restart: always
network_mode: host
environment:
- HOMEBRIDGE_CONFIG_UI_PORT=10000
volumes:
- homebridge:/homebridge
volumes:
homebridge:
保存后执行 docker-compose up -d
等待镜像拉取完毕后,会自动开始运行。
此应用安装和运行时需要访问 NPM 仓库,如果你的网络存在问题,访问失败,会导致无法启动。请自行配置代理或者尝试换源等操作。不过即使服务启动失败,UI 的访问是不受影响的,所以仍可以自行在浏览器内查询日志
稍等一会儿,访问 http://<server ip>:10000
即可进入后台界面。
配置配件#
此处依旧按照最小化的需求场景去实现,即,将空调的复杂操作简化至两个 —— 开启 & 关闭。其中开启对应
25 度,制冷,风速 1%
,关闭为关闭空调。更复杂的实现请自行探索
要调用 HTTP 接口,我们需要安装插件:homebridge-http
。
安装完毕后在后台做如下配置:
{
"bridge": {
"name": "Homebridge",
"username": "1D:42:45:4B:E4:A4",
"port": 51177,
"pin": "XXX-XX-XXX",
"advertiser": "bonjour-hap"
},
"accessories": [
{
"accessory": "Http",
"name": "Media Aircon",
"switchHandling": "realtime",
"on_url": "http://192.168.2.224:9997/aircon/c/25/1",
"off_url": "http://192.168.2.224:9997/aircon/off",
"status_url": "http://192.168.2.224:9997/aircon/status"
}
],
"platforms": [
{
"name": "Config",
"port": 10000,
"platform": "config"
}
]
}
其中主要是 accessories
字段,新增对象。其中 on_url
off_url
status_url
分别代表开启,关闭,查询状态所使用的接口。当然,插件支持更多参数,具体的可查询其 GitHub 仓库说明。
配置完毕后,重启 HomeBridge 服务,打开家庭 App,扫描后台展示的二维码即可发现设备。
此时可正常获取状态,并且进行开关操作。
语音控制#
那么可不可以更进一步,比如躺在床上,不想抬手的时候,语音控制就非常有用了。
很简单,使用 快捷指令
即可完成。
当然,由于我们提供了 HTTP API,根本不需要复杂的方法,直接调用即可:
开启空调:
关闭空调:
至此,直接对 Siri 说出你的快捷指令名称就可以使用了
思考与总结#
本来是计划使用 Flutter 实现一套 App+Web 的前端应用来控制的。但是突然想起来苹果系的生态,便中途改了需求。但是也导致手机上(Android)没办法很方便的操作。不过总的来说搭配 HomeKit 这一套还是比较舒服的,算是有失有得。
由于时间原因,以及晚上又双叒叕去机场蹲着的缘故,代码方面写的不仅粗糙,且均遵循能跑就行的原则,后续至少要进一步做出优化。
目前来看,至少一下几个方向需要改进:
-
API 层,加入鉴权体系,配合
Cloudflare-Tunnel
穿透至外网,达成真正的远程控制,而不是局域网内部控制。 -
红外编码方面,完成更多常用组合,并且尝试使用相关的 lib ,直接发送红外编码数据,而不是粗暴调用外部二进制。
-
HomeBridge 层,需要拓展更多的功能,并且搭配自动指令实现更精细化的控制,而不是现在这种当作一个灯泡去处理(笑。
-
定时任务,在指定的时间段内自动开启 / 关闭:树莓派本身部署了
青龙面板
,简单写写脚本就可以完成。 -
外形修正:呃,工业风又不是不能用,实在没这个天赋,就算了。
总的来说,虽然其中波折不少,最后的实现也是匆匆忙忙草草率率,但是意外地,体验起来自身还算满意,至少倒也不算是浪费了时间。