Hao
Hi, I am Hao (👋): a coder, a woodworker, a blogger, and a father.
使用树莓派、Alexa和Siri打造智能家居系统
July 17, 2017

引言

我目前的住址是唯一硅谷圣塔克拉拉某条街道的一间公寓。公寓不大,但是有很多墙壁开关和墙壁插头。其中某些开关可以控制某些插头是否通电。美国的公寓一般在客厅没有屋顶灯,一般情况下美国人把落地灯连接到墙壁插座上,然后通过能控制它的开关来开关灯。

听起来好像很方便,但实际上造成了巨大麻烦。插座的位置固定,所以落地灯的位置也必须靠近插座,否则就得使用难看的复杂的延长线。第二,由于开关在门口,所以想关灯只能走到门口去。

另外一个生活上亟须解决的痛点是,我经常两手拎着电脑包、健身包、外卖等等,站在门口一筹莫展。因为钥匙在电脑包内层里,想要开门要放下某个包,然后抓出钥匙。

现代生活往往导致家庭里有很多电器,而我尤其如此。我有一个电视、音响、投影仪、PS4、Apple TV等等。每次想看一个电影尤其复杂——要开电视,选择正确的HDMI接口,打开音响,选择音响信号输入,打开Apple TV…

这就是为什么我开始这个智能家居项目。每个人对智能家居的理解不同。有些人认为用手机遥控足够好,有些人则要求智能家居需要真正“智能“。而我认为使用手机来遥控各种电器并不能省事多少,而太过”智能“的系统又非常困难。我希望搭建一个以语音系统为基础,可以扩展的智能家居系统。

为了解决上述需求痛点,在项目开始的时候,我列下了一些目标。

需求、目标

硬件购买清单

基础设施设置

最后的方案是以 Amazon Echo Dot 和 Apple 的内置语音助手 Siri 为控制输入、以树莓派搭建控制中转中心、ESP8266无线模块为控制终端来搭建整个系统。本部分的设置主要为参考,随版本更新可能发生变化。

树莓派

  1. 在美亚上下单,两天后收到。
  2. 下载 Raspbian 系统,注意官方网站上的说明让用户使用软件把镜像文件烧录到U盘上,但这是错误的。只需要解压缩下载得到的压缩包,并把全部内容拷贝到U盘即可。
  3. 树莓派连接显示器、鼠标、键盘,插入U盘,连接树莓派电源(5V mini USB 接口),树莓派会自动启动。
  4. 根据屏幕提示,安装 Raspbian Jessie 系统。
  5. 重启进入系统,设置 Wifi 连接,并允许 SSH 连接。为了方便,把计算机名称改为 pi,并修改初始密码。
  6. 测试 SSH 连接:$ ssh pi@pi.local
  7. 安装 Node.js
    • $ sudo apt-get update
    • $ curl -sLS https://apt.adafruit.com/add | sudo bash
    • $ sudo apt-get install node
  8. 安装 Vim:$ sudo apt-get vim

使用 Homebridge 连接 Siri

目前市场上支持 Siri 的设备非稀少即昂贵。Homebridge 是一个基于 Node.js 的第三方 HomeKit 插件,能伪装出一系列支持 Siri 的设备。

安装 Homebridge:

$ sudo npm install -g --unsafe-perm homebridge

安装 homebridge-cmdswitch2。这个插件可以允许 Homebridge 使用命令行来控制设备的开关。

进入 ~/.homebridge 新建一个 config.json 根据示例文件 弄一个简易的配置文件,然后启动 Homebridge 服务。

打开 iPhone 的 Home app,添加 Accessory,选择 Homebridge。

Amazon Echo Dot

Amazon 有一套 Skills 系统,允许添加自定义的 Skill,但缺点是需要每次说”Alexa, tell x to do something”。非常奇怪。因此,放弃 Skills,使用 Fauxmo。这是一个 Node.js 插件,把自己伪装成一个 WeMo 的灯泡,但是除了“灯泡”这个属性之外,其他的东西都是可以自定义的,比如名字。

新建一个目录,进入后安装 fauxmojs:

$ npm install fauxmojs ip --save

使用下列示例代码新建一个简单的配置文件,它跟 Homebridge 的文件差不多。主要就是在 handler() 方法中执行具体的控制命令。具体内容稍后再添加。

const FauxMo = require('fauxmojs');
const ip = require('ip');
const { spawnSync } = require('child_process');
const request = require('request');

let fauxMo = new FauxMo({
  ipAddress: ip.address(),
  devices: [{
    name: 'Living Room Light',
    port: 11000,
    handler(action) {
      if (action === 'on') {
        // do something
      } else if (action === 'off') {
        // do something else
      }
    }
  }]
});

console.log('started...');

ESP8266 NodeMCU 模块

开发该模块首先必备一份针脚地图,注意 D4 的并非 GPIO4。

NodeMCU pin map

下载安装 Arduino IDE for Mac。下载安装 NodeMCU 的驱动。这个比较难找,如果 Mac 系统是最新的话,可能要费一些功夫。但还是附上链接,并不保证一定有用。

使用一条 USB 转 mini USB 数据线(注意,需要一条数据线而不是普通的充电线),连接 Macbook 和 模块。

打开 Arduino IDE 软件。在菜单栏 Library 里安装 NodeMCU 的模块。重启 IDE。在 Port 处应该有两个,一个是什么什么蓝牙(Bluetooth),另外一个是类似 SLAB_USBtoUART 这样的名字,选择。在菜单栏,选择一个简单的示例文件,点击“上传”,此时模块有一个蓝色信号灯狂闪,上传结束后会立即执行或者报错。IDE 右上角有一个按钮可以打开控制台,但注意需要选择代码中正确的频道才能看见,否则可能是乱码。

Etekcity 生产的遥控插座转接头是一个神器,美亚上30美元五个,平均一个六美元。而现有最便宜的支持 Siri 的单口插座则要至少20美元一个。

这个插座的原理就是使用遥控器来控制继电器,从而达到切断电流的目的。我们则要使用射频(RF)嗅探器来截获遥控器的射频信号码,然后使用发射器来达到跟遥控器相同的目的。

插座头的设置非常简单,插入插座中,按下旁边的按钮,红色LED灯闪烁,按下遥控器的某一个键,红灯闪烁三下表明已经记录,然后这个键就被指定给了这个插座。这样把五个插座头都配置好按钮,放在一边。接下来连接射频(RF)发射和接收器。

发射器和接收器都有电源(VCC),接地(GND)和数据 (DATA)接口,并在模块上有标注。发射器可能还有天线接口,暂时忽略之。使用跳线,把两个设备连接到树莓派的 GPIO 针口上。电源需要连接到3.3V上。

在树莓派上安装 wiringPi

新建一个目录,克隆 rf_pi

$ git clone https://github.com/n8henrie/rf_pi.git
$ cd rf_pi

# 编辑 send.cpp 文件,修改 GPIO 为真实的连接号码。如果发射器连接到了 GPIO 3,则需要修改为 3
$ vim send.cpp

$ make

开始录制射频信号:

$ ./RFSniffer

依次按遥控器上的按钮,屏幕上则会出现该按钮的代码。记住这些按钮对应的号码。

测试信号,把插座插到墙上,然后执行:

$ ./send 123456

如果一切正常,插座应该切换有电/断电模式。

整合到语音控制

对于 Siri,把下列代码添加到 ~/homebridge/config.json 中:

{
  "on_cmd": "/home/pi/workspace/rf_pi/send 5272835",
  "off_cmd": "/home/pi/workspace/rf_pi/send 5272836"
}

对于 Alexa,把下列代码添加到刚才建好的 index.js 中:

const { spawnSync } from 'child_process';

// ...
if (action === 'on') {
  spawnSync('/home/pi/workspace/rf_pi/send', ['5272835'], { encoding: 'utf8'});
} else if (action === 'off') {
  spawnSync('/home/pi/workspace/rf_pi/send', ['5272844'], { encoding: 'utf8'});
}

测试:“Hey Siri, turn on the living room light“以及”Alexa, turn on the living room light“。

空调/风扇

家用电器中使用射频控制的非常少,大多还是频率为38kHz的红外信号。以空调为例。控制原理跟射频差不多,基本也是:解惑信号 -> 记录信号 -> 模拟发射信号。

使用 NodeMCU 模块直接录制红外信号:把红外(IR)接收器模块连接到 NodeMCU 模块上:VCC - 3.3V, GND - GND, Data - D5,注意 D5 是 GPIO 14 针脚。

使用IRremoteESP8266红外包。下载至<path>/Arduino/Libraries目录中。重启 IDE。打开示例文件中名为 IRrecvDumpV2.ino 的文件。打开控制台(Serial Monitor),选择频道为 115200(或者实际频道),上传代码。

把空调遥控器对准红外接收器,按一下电源键。这里有个小技巧,按键的时候一定要快速,不要拖沓。每次录制出来的数据可能不同,最好多记录几次,待会测试的时候需要。IRremoteESP8266 包内置了一些知名品牌的红外信号格式,但是大多数可能还是显示“UNKNOWN”。比如:

uint16_t acPowerRawData[37] = {8500,4300, 550,1600, 550,1600, 550,1600, 500,600, 550,1600, 550,600, 550,1600, 550,1600, 500,4300, 550,1600, 500,1650, 550,550, 550,1600, 550,550, 550,550, 550,550, 550,550, 600};

按照下图连接红外发射器、三极管。

IR sending

Copyright: markszabo

不过根据我的实际操作来看,此图的连线略有复杂。在面包板上直接使用左下角的几个针脚(3.3V, GND, D8)的话,连跳线都不需要。

打开一个新的 Aruduino IDE 窗口,使用示例代码上传。把代码中的 rawData 变量换成实际录制的数组。

irsend.sendRaw(acPowerRawData, 37, 38);

这一行代码发送了红外信号。第二个参数是数组的长度。第三个参数是红外频率,一般家用电器为 38 kHz

对于整合来说,与射频类似,把控制命令放在对应的配置文件中。

门锁

市场上的智能门锁有几个缺陷:

  1. 昂贵,动辄200美元
  2. 不支持 Siri,而用一个 app 开锁不见得比钥匙方便多少
  3. 需要拆掉现有的门锁

门锁的转动需要舵机来实现,订购 Futaba S3003 标准舵机,5V供电。测量家用门锁的尺寸,订购乐高模块。

把舵机分别连接至 ESP8266 模块的 5V电源、接地线(GND)和数据针脚(D2)上。一般红色为正极,黑色为地线,白色或黄色为数据(DATA)。

把 ESP8266 连接至 Mac,打开 Arduino IDE。新建一个文件。输入下列代码:

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <Servo.h> 

#define DELAY_BETWEEN_COMMANDS 1000
#define LOCK_DOOR 24
#define UNLOCK_DOOR 134

Servo myservo;

const char* ssid = "";
const char* password = "";
const char* hname = "lock";
int state = LOCK_DOOR;

ESP8266WebServer server(80);

const int led = BUILTIN_LED;

void setup(void){
  myservo.attach(4); // GPIO 4 D2
  myservo.write(LOCK_DOOR);
  
  pinMode(led, OUTPUT);
  digitalWrite(led, 1);
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  WiFi.hostname(hname);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin(hname)) {
    Serial.println("MDNS Responder Started");
  }

  server.on("/door_lock", [](){
    myservo.write(LOCK_DOOR);
    state = LOCK_DOOR;
    server.send(200, "text/plain", "Door locked");
  });

  server.on("/door_unlock", [](){
    myservo.write(UNLOCK_DOOR);
    state = UNLOCK_DOOR;
    server.send(200, "text/plain", "Door unlocked");
  });

  server.begin();
  Serial.println("HTTP Server Started");
}

void loop(void){
  server.handleClient();
}

其中,LOCK_DOORUNLOCK_DOOR 为两个储存舵机转动角度的常量。需要测试后替换为真实的“开”和“关”的角度。在 ssidpassword 填入家用 Wifi 的信息。上传运行代码。

在浏览器中打开下列两个 URL 来测试门锁是否正常工作:

一切正常后,整合至 Siri。

本文全部代码在 Github 上托管