道阻且长

道阻且长

问君西游何时还,畏途巉岩不可攀。
twitter
github

基于树莓派+红外管的伪智能家居实现

起因#

租住的公寓提供的是美的空调,很遗憾,并不支持智能家居的接入。

随着天气转热,每天又陷入了开关空调的微妙琐事之中。虽然大部分时间只是伸伸手,但依旧很打断上网的冲浪体验,加上这倒霉设备不能自动记住上次风速,所以开关后又要额外调节风速到自己喜欢的程度,对其不满便更是与日俱增起来。

终于在上周,上了一天班回来发现出门忘记关掉,更是心疼起电费。于是看了下架子上充当旁路由吃灰的树莓派,干脆折腾一下。

额外材料#

  • 红外接收管(录制遥控器红外信号编码)
  • 红外发射管(发射控制信号)
  • 杜邦线(连接作用)
  • 三极管(可选,看着说是可以增强信号,自己并没有用到)

软件安装#

我的设备及系统信息如下:

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

操作完毕后,重启树莓派,令其生效。

硬件连接#

查询资料可知,树莓派的具体引脚定义如下:

image

红外接收管连接#

image

把红外接收管的凸起朝上,由左至右,引脚分别为数据,负极,正极。数据引脚用杜邦线连接到 GPIO18,负极接到地线,正极接到 3.3v 供电

红外发射管连接#

image

发射管的情况如图,长脚为正极,接入 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

数据整体分为两段,除开引导码和分隔码,两段数据的内容完全一致。每次按键,无论是为了调节风速还是温度,均包含上述信息。

编码区域关系#

注意,此处仅按照图中规格对照,暂不讨论实际情况

经由规格书可知大致存在如下关系:

image

那么可以编写数据 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填充到56100为特殊值,对应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,扫描后台展示的二维码即可发现设备。

image

此时可正常获取状态,并且进行开关操作。

语音控制#

那么可不可以更进一步,比如躺在床上,不想抬手的时候,语音控制就非常有用了。

很简单,使用 快捷指令 即可完成。

当然,由于我们提供了 HTTP API,根本不需要复杂的方法,直接调用即可:

开启空调:

image

关闭空调:

image

至此,直接对 Siri 说出你的快捷指令名称就可以使用了

思考与总结#

本来是计划使用 Flutter 实现一套 App+Web 的前端应用来控制的。但是突然想起来苹果系的生态,便中途改了需求。但是也导致手机上(Android)没办法很方便的操作。不过总的来说搭配 HomeKit 这一套还是比较舒服的,算是有失有得。

由于时间原因,以及晚上又双叒叕去机场蹲着的缘故,代码方面写的不仅粗糙,且均遵循能跑就行的原则,后续至少要进一步做出优化。

目前来看,至少一下几个方向需要改进:

  • API 层,加入鉴权体系,配合 Cloudflare-Tunnel 穿透至外网,达成真正的远程控制,而不是局域网内部控制。

  • 红外编码方面,完成更多常用组合,并且尝试使用相关的 lib ,直接发送红外编码数据,而不是粗暴调用外部二进制。

  • HomeBridge 层,需要拓展更多的功能,并且搭配自动指令实现更精细化的控制,而不是现在这种当作一个灯泡去处理(笑。

  • 定时任务,在指定的时间段内自动开启 / 关闭:树莓派本身部署了青龙面板,简单写写脚本就可以完成。

  • 外形修正:呃,工业风又不是不能用,实在没这个天赋,就算了。

总的来说,虽然其中波折不少,最后的实现也是匆匆忙忙草草率率,但是意外地,体验起来自身还算满意,至少倒也不算是浪费了时间。

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.