一 智能照明管理系统概述 背景概述
深圳市酷图软件开发有限公司成立于2013年,专注设备智能控制与远程管理解决方案,面向照明、安防、家电等领域,为各类电子产品开发无线连网模块、手机APP及后台管理功能。
基于过往的项目经验、客户需求和行业趋势,我们规划了一套智能照明管理系统,目标是将家庭、写字楼、酒店等各类区域的照明系统连接到云端,实现集中管理、自动控制,以达到提升照明效果、提高管理效率、降低能源消耗的目标。
- 首先我们需要一个网关,通过有线或无线方式与灯具连接起来,然后将网关连接到云平台。
- 我们在云平台上实现设备连接、消息传递,数据处理与存储。
-
我们针对不同的应用场景开发手机APP、PC客户端与Web页面,实现对系统的控制和管理。
- PC客户端——用于较大规模的系统,安装在监控中心的PC上,由管理人员对系统进行监控。
- 手机客户端——用于家庭;用于酒店、写字楼等环境,由用户对自己所在区域的照明进行控制。
- Web页面——通过浏览器对照明系统进行监控(替代PC客户端),查看报表,管理登录用户等。
网关硬件采用Dragonboard 410c开发板。此开发板集成了四核ARM® Cortex® A53处理器,单核频率可达1.2GHz,通信方面支持WiFi、蓝牙和GPS,并提供了多种扩展接口连接外设。这保障了网关可以应对规模较大的照明系统,并提供多样化的控制方式。
网关软件采用专为物联网应用设计的Ubuntu Core操作系统。此系统最大的特点是采用Snappy包的方式管理应用,应用程序和其依赖的运行库打包在一起,便于快速布署和更新;此外,应用独立运行于各自的沙箱环境中,可以有效地实现数据隔离,保障系统的安全性。
云平台选择AWS。AWS的IoT服务采用MQTT协议实现与设备的安全连接和消息传递,并在此基础上提供了设备影子和规则引擎——设备影子用于保存和同步设备状态,规则引擎用于与文件存储、数据库、推送通知、Lambda函数等其他云服务的整合。这令我们可以专注照明的业务逻辑,实现快速开发。
开发技术
系统各部分的程序代码统一采用JavaScript语言编写,JavaScript无需编译,同时拥有丰富的第三方库(npm),可以有效提升开发效率。同时JavaScript的事件驱动特性非常适合于物联网应用。
本项目开放源代码,代码托管于开源中国码云平台:http://git.oschina.net/erabbit/OpenIoT
二 在Dragonboard 410c上安装Ubuntu Core
材料清单
我们将要下载Ubuntu Core的镜像文件,烧写到SD卡上,然后放到Dragonboard上启动并进行初始化设置。除Dragonboard外,还需要准备以下物品:
- 电脑(用于烧写SD卡,Windows/Linux/Mac都可以,本文以Macbook为例)
- Micro SD卡和读卡器(MicroSD卡没有特殊要求,我们使用的是SanDisk 4GB,Class4型号的)
- USB接口的键盘
- HDMI视频线及显示器
-
可访问Internet的WiFi连接
注册Ubuntu One账号
最新的Ubuntu Core操作系统默认已不再提供本地登录账号,需要到Ubuntu网站上注册一个Ubuntu One账号(访问各种Ubuntu服务的统一账号)并上传公钥文件。待系统安装好以后,使用私钥文件以SSH方式登录。公钥和私钥对可以在电脑上用ssh-keygen命令生成。
下载镜像文件
到这里下载最新的Ubuntu Core 16镜像: http://releases.ubuntu.com/ubuntu-core/16/
其中包含了适用于Raspberry Pi等多种硬件的文件,我们选择ubuntu-core-16-dragonboard-410c.img.xz。
下载完成以后检查一下文件的MD5:$ md5 ~/Downloads/ubuntu-core-16-dragonboard-410c.img.xz MD5 (/Users/Tom/Downloads/ubuntu-core-16-dragonboard-410c.img.xz) = 2aa8f5b404826818e2de63e947b0bae7
将以上的值与文件http://releases.ubuntu.com/ubuntu-core/16/MD5SUMS中的值对比一下,确认一致,以免网络问题导致下载的文件损坏。 烧写SD卡 -
将Micro SD卡放进读卡器,插到电脑USB口上,然后使用diskUtil命令查看读卡器对应的设备,此处是/dev/disk2:
$ diskutil list /dev/disk0 (internal, physical): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme *251.0 GB disk0 1: EFI EFI 209.7 MB disk0s1 2: Apple_CoreStorage Macintosh HD 250.1 GB disk0s2 3: Apple_Boot Recovery HD 650.0 MB disk0s3 /dev/disk1 (internal, virtual): #: TYPE NAME SIZE IDENTIFIER 0: Macintosh HD +249.8 GB disk1 Logical Volume on disk0s2 36C243A9-C568-4875-B987-5026D4C20FE7 Unencrypted /dev/disk2 (external, physical): #: TYPE NAME SIZE IDENTIFIER 0: FDisk_partition_scheme *4.0 GB disk2 1: DOS_FAT_32 NO NAME 4.0 GB disk2s1
-
卸载磁盘
$ diskutil unmountDisk /dev/disk2 Unmount of all volumes on disk2 was successful
-
烧写镜像
$ xzcat ~/Downloads/ubuntu-core-16-dragonboard-410c.img.xz | sudo dd of=/dev/disk2 bs=32m 0+10366 records in 0+10366 records out 679297024 bytes transferred in 153.468353 secs (4426300 bytes/sec)
- 注意32后面的单位是小写的m
-
烧录过程中命令行会卡住几分钟,不显示进度信息
完成后再查看一下磁盘列表:
$ diskutil list /dev/disk0 (internal, physical): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme *251.0 GB disk0 1: EFI EFI 209.7 MB disk0s1 2: Apple_CoreStorage Macintosh HD 250.1 GB disk0s2 3: Apple_Boot Recovery HD 650.0 MB disk0s3 /dev/disk1 (internal, virtual): #: TYPE NAME SIZE IDENTIFIER 0: Macintosh HD +249.8 GB disk1 Logical Volume on disk0s2 36C243A9-C568-4875-B987-5026D4C20FE7 Unencrypted /dev/disk2 (external, physical): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme *4.0 GB disk2 1: DEA0BA2C-CBDD-4805-B4F9-F428251C3E98 1.0 MB disk2s1 2: 098DF793-D712-413D-9D4E-89D711772228 1.0 MB disk2s2 3: A053AA7F-40B8-4B1C-BA08-2F68AC71A4F4 1.0 MB disk2s3 4: E1A6A689-0C8D-4CC6-B4E8-55A4320FBD8A 1.0 MB disk2s4 5: 303E6AC3-AF15-4C54-9E9B-D9A8FBECF401 1.0 MB disk2s5 6: 400FFDCD-22E0-47E7-9A23-F16ED9382388 2.1 MB disk2s6 7: 20117F86-E985-4357-B9EE-374BC1D8487D 1.0 MB disk2s7 8: Microsoft Basic Data system-boot 134.2 MB disk2s8 9: Linux Filesystem 535.6 MB disk2s9
- 最后,再次将磁盘从电脑上卸载,取下读卡器,拿出SD卡。 启动和初始化
- 将烧写好的SD卡装入Drgonboard 410c的卡槽,将板背面的拨码开关S6拨到0110(第2、3位为ON,其他位OFF),连接键盘、显示器,然后接通电源。
- 按照提示输入WiFi的SSID和密码,以连接网络。
-
输入注册Ubuntu One账号的Email地址,完成初始化,记录下屏幕上显示的IP地址。
SSH登录 在电脑上确认能够ping通Dragonboard的IP地址,然后使用SSH命令登录,用-i参数指定私钥文件。
三 连接AWS IoT服务
管理控制台
要使用AWS的IoT服务连接和管理设备,需要先登录AWS管理控制台,创建设备对象、生成证书和设置访问策略。其中证书需要下载到本地,放到设备中,连接时需要。
设备连接
AWS IoT服务使用MQTT协议连接设备与云端,MQTT基于发布/订阅模型,是目前IoT领域设备与云端通信的主流消息协议。
AWS已经将MQTT客户端封装进了设备SDK当中,在设备侧引入aws-iot-device-sdk,配置好证书、设备ID及IoT服务所在的区域即可实现连接。
sdk提供了多种开发语言的版本,我们使用的是JavaScript:
var awsIot = require('aws-iot-device-sdk'); var thingShadows = awsIot.thingShadow({ keyPath: path + '/certs/private.pem.key', certPath: path + '/certs/certificate.pem.crt', caPath: path + '/certs/root-CA.crt', clientId: gateway.id, region: 'ap-northeast-1', }); var thingShadowConnected = false; var clientTokenUpdate; thingShadows.on('connect', function() { thingShadows.register(gateway.id); thingShadowConnected = true; setTimeout(gateway.reportState, 5000); });设备影子
AWS IoT在MQTT的基础上定义了“设备影子”服务,用于管理设备状态。“设备影子”的内容实际上就是一个JSON文档,其具体字段由开发者自己定义,系统跟踪字段值的变化。在本应用中,灯的开关状态就是通过设备影子管理的,主要流程如下:
- 当网关检测到灯状态变化后,即发布report消息;
- 云端收到report消息,即更新设备影子中灯的当前状态;
- 用户通过Web页面设置灯的状态,发布desired消息;
- 云端收到desired消息,通过对比其与当前状态的差别,发布delta消息;
-
网关收到delta消息,根据消息内容设置灯的状态。 ``` ... reportState: function() {
var curState = new Object(); for(var light of gateway.lights) { curState[light.id] = { power: light.power }; } var state = { "state": { "reported": curState } }; clientTokenUpdate = thingShadows.update(gateway.id, state); if (clientTokenUpdate) console.log('updated shadow: ' + JSON.stringify(state)); else console.log('update shadow failed, operation still in progress');
} ...
thingShadows.on('delta', function(thingName, stateObject) { console.log(received delta on ${thingName}: ${JSON.stringify(stateObject)}); var deltaState = stateObject.state; for(var lightId in deltaState) { var lightState = deltaState[lightId]; lightState.id = lightId; gateway.onCommand(commands.light.power, lightState); } }); ```
规则引擎
AWS云上有丰富的计算、存储、网络资源,IoT服务通过规则引擎与其他各项服务建立联系。
我们在规则引擎当中,以类似SQL的语法定义规则,筛选设备消息。当符合指定条件的消息到达后,系统就就会将其插入数据库、发送通知或触发Lambda函数的执行。
四 打包和布署Snap应用
在开发板上测试
网关程序使用JavaScript开发,由Node.js解释和运行,前期是在PC上开发的,打包之前需要先移植到开发板上。
由于JavaScript/Node.js本身是跨平台的,所以网关代码基本不需要修改,在开发板上安装好Node.js环境即可运行。
Ubuntu Core作为一个“Snappy Only”的系统,并不能像其他系统一样随意安装程序,而是需要先安装一个名为“classic”的snap,然后在这个snap提供的环境中安装Node.js。
$ sudo snap install classic $ sudo classic (classic)$ sudo apt-get install nodejs打包
当在classic环境中测试没有问题后,就需要使用Snapcraft工具将网关程序和Node.js环境打成Snap包,关键是编写snapcraft.yaml文件:
name: lighting-gw version: "0.1.0" summary: Connect local lighing devices to cloud description: A smartlighting gateway connected to AWS IoT confinement: devmode apps: lighting-gw: command: bin/lighting-gw plugs: [serial-port, network, home] parts: lighting-gw: plugin: nodejs source: . node-engine: 6.5.0
此文件定义了应用代码和可执行程序的路径,所需的系统资源(串口、网络),以及Node.js的版本。编写好以后,执行snapcraft命令即可生成“.snap”文件。
每个“.snap”中都可以包含自己的Node.js/Python/JRE版本,相互之间不影响。
安装Snap应用前需要先退出classic模式,然后使用snap命令安装:
sudo snap install --devmode --force-dangerous lighting-gw_0.1.0_arm64_api.snap
也可以将“.snap”文件复制到其他运行Ubuntu Core的Dragonboard上,然后使用以上命令进行安装,在目标板上并不需要安装classic和Node.js,因为snap包中已经包含了其运行所需的一切。
文件访问Snap运行时的文件系统是只读的,因此安装目录下的文件无法修改,应用的数据要放在专门的数据路径下,此路径可以在Snap环境中使用“SNAP_DATA”环境变量获取。
var path = process.env.SNAP_DATA;
五 使用手机APP进行本地控制
HTTP Restful API
为了在不连接云平台的情况下仍能在局域网中控制设备,网关上集成了HTTP服务,提供API接口供手机APP调用。
HTTP API采用express框架:
var app = express(); var router = express.Router(); router.route('/lights') .get(function(req, res) { var devices = new Array(); for(var light of host.lights) { var device = new Object(); device.id = light.id; device.uid = light.uid; device.power = light.power devices.push(device); } res.status(200).json(devices); }); app.use('/api', router);Android APP
public class MainActivity extends Activity { ... @Override public void onLightPowerChanged(boolean isOn) { String curLights = curLights(); if(curLights != null) Light.setLightPower(curLights, isOn ? Light.LIGHT_POWER_ON : Light.LIGHT_POWER_OFF, apiHandler); }
class Light { ... static String serverIp = "192.168.0.100"; static String getUrl(String suffix) { return "http://" + serverIp + ":9000/api" + suffix; } public static void setLightPower(String lightId, int power, Handler handler) { String url = getUrl("/command/light/power"); HttpRequestThread thread = new HttpRequestThread(url, handler, MSG_LIGHT_POWER); thread.addParam("id", lightId); thread.addParam("operation", (power == LIGHT_POWER_ON) ? "on" : "off"); thread.start(); }