JS 遇上 IOT

3,468 阅读9分钟
原文链接: zhuanlan.zhihu.com

1995年,当工作于 Netscape 的 Brendan Eich着手为Netscape Navigator 2.0 开发一个称之为 LiveScript 的脚本语言时,没有人会想到avascript将在今天的互联网软件开发中发挥重要作用。如今,Javascript已经在越来越多的领域攻城略地,web工程构建,后端服务器开发,三维图像,AR,VR等等等。甚至,在近几年我们也惊喜的发现已经JS可以用来开发硬件设备。今天,就跟大家聊聊用JS进行简单物联网开发的心得与体会。

凡是可以用 JavaScript 来写的应用,最终都会用 JavaScript ——Atwood定律

什么是IOT

我们总说IOT,那到底什么是IOT?IOT是Internet of Things的缩写,字面翻译是“物体组成的因特网”,准确的翻译应该为“物联网”。物联网(Internet of Things)又称传感网,简要讲就是互联网从人向物的延伸。

其实物联网可以从两个方向进行拆分,即由“物”向“网”,或者是由“网”向“物”。由物向网可以理解为人跨越空间和自身条件的局限对物体进行感知的过程。由“网”向“物”是人跨越空间对物体进行控制的过程。

IoT应用开发平台简介

在IoT应用开发领域中,大家熟知的开发平台主要有如下几类:

  • 嵌入式操作系统,包括VxWorks、FreeRTOS、LiteOS等;
  • 极客硬件平台,包括树莓派、Arduino等;
  • JavaScript IoT应用开发平台,包括Ruff、Tessel、JerryScript、Johnny-Five等。

嵌入式操作系统,从功能的角度上来说,能够满足目前的绝大多数需求。但是:

  • 其入门门槛极高,开发者想要成为优秀的嵌入式开发工程师,需要学习大量软硬件知识。相较于软件行业,嵌入式领域的人才数量受到了限制。
  • 嵌入式领域在开发方法上已经大幅度落后于整个行业的发展。敏捷软件开发方法以及精益创业的理念,受到工具所限,在嵌入式领域极少得到应用,所以该领域在工程方法上发展缓慢。
  • 这些操作系统的编程概念通常属于专用领域,所以知识很难在行业中共享,开发者在行业中流动也相对困难,造成的结果是,嵌入式领域对于现代软件开发理念的理解也整体上落后于软件行业。

极客硬件平台,其初衷是降低开发门槛,让更多开发者得以进入到硬件开发领域中。但是:

  • 它只是在操作方面的入门难度上在努力,而开发真正困难的部分在编程概念。对于大多数软件开发者而言,难点在于硬件中的编程概念。各种各样的接口及参数,这是软件开发者难于理解和掌握的。
  • 更关键的因素是,这些平台只解决了原型开发的问题。开发者即便能够通过它实现了一个产品原型,也很难将它用到真正的产品中。应用到产品中,往往要重新设计硬件,这些平台的优势就荡然无存了。

二者最本质的复杂度在于其编程模型,对于软件开发者来说,GPIO、I2C之类硬件接口完全是另一种语言,除了要了解接口的编程方法,还要针对每个硬件,阅读其数据手册,了解参数细节。

目前为止,诸位会想,IoT行业对软件工程师简直犹如另一个世界,一点都不友好。是的,很多人都是这么想的,于是,有人想用更高级的语言改变这个世界,这其中最为活跃的便是JavaScript社区。

JavaScript IoT应用开发平台

JavaScript IoT应用开发平台,其建设初衷是让开发者能够用JavaScript开发IoT应用,一方面可以更好地构建抽象,另一方面,可以将比较现代的开发方式引入到硬件研发中。JavaScript IoT应用开发平台目前主要分为几大类:

  • 在硬件上运行JavaScript,如JerryScript、Espruino等;
  • 提供硬件抽象能力,比如Tessel、Johnny-Five、Cylon.js等;
  • 面向生产的能力,如Ruff。

Ruff的优势

Ruff与Arduino相比更贴近网络,由于Arduino的诞生较早,标准开发板并没有网络通信方式,虽然可以通过扩展的形式添加,但是上手略微复杂。而Ruff天生支持Wifi通信,使用Ruff进行http通信和使用普通nodejs的http通信方式没有什么区别,上手极为简单。当然,由于Arduino发布较早,而且一开始就为模块化开发设计,感觉Arduino的第三发模块相对简单,就像搭积木一样一层层安装即可,而ruff的硬件模块相对较少,生态和Arduino相比不是那么成熟完善。

Ruff与树莓派相比更加贴近物联网开发。树莓派的本质是个浓缩的但是相对完整的操作系统,你在树莓派上可以干任何事情,可以浏览网页,可以编写web服务端程序,当然也可以直接编写树莓派的I/O接口(定时启动咖啡机、给狗狗喂食。。。)而Ruff的功能相对简单很多,简单的可以认为Ruff的功能是单片机,部署各web server还有可能实现,但是想访问浏览器等图形界面就不可能实现了,但是它的核心目标就是针对硬件来编程,功耗更低效率更高。总而言之是两者设计初衷的差异。树莓派要做电脑,如果太弱了,很多功能做不了,而 Ruff 开发套件是为了做硬件应用,太强了反而不能体现真实的场景。

Ruff上手

Ruff的入门上手极为简单。通过官网教程我们可以迅速的使用Javascript写出个简单的点亮LED小灯的程序。

'use strict';

$.ready(function (error) {
    if (error) {
        console.log(error);
        return;
    }

    $('#led-r').turnOn();  //点亮小灯
});

$.end(function () {
    $('#led-r').turnOff();
});

然后连接Ruff自带wifi热点。

通过无线网络,使用命令 rap deploy -s 将程序下载到开发板。整个过程极为简单,自然。不比配置个前端工程复杂太多。

Ruff拥抱Internet

如果ruff只能简单的开发小灯亮灭这样的简单程序,只能作为个玩具。而只用真正接入到广阔的互联网中才能实现真正意义上的“物联网”。后面将介绍如何将ruff接入阿里云物联网套件,实现信息的上报与获取,实现由“物”向“网”的信息感知和由“网”向“物”的远程控制。

阿里云物联网套件是阿里云专门为物联网领域的开发人员推出的,其目的是帮助开发者搭建安全性能强大的数据通道,方便终端(如传感器、执行器、嵌入式设备或智能家电等等)和云端的双向通信。全球多节点部署让海量设备全球范围都可以安全低延时接入阿里云IoT Hub,安全上提供多重防护保障设备云端安全,性能上能够支撑亿级设备长连接,百万消息并发。物联网套件还提供了一站式托管服务,数据从采集到计算到存储,用户无需购买服务器部署分布式架构,用户只需在web上配置规则即可实现采集+计算+存储等全栈服务。总而言之,基于物联网套件提供的服务,物联网开发者可以快速搭建稳定可靠的物联网平台。

当然各位看官可以访问阿里云物联网套件的产品详情页来探索物联网套件的强大功能,阿里云 - 物联网套件 - 产品详情

从架构图上我们也可以看出,红色的消息发布通道即为我们前文谈到的由“物”向“网”感知过程,而蓝色的订阅消息服务则是我们前文谈到的由“网”向“物”的控制过程。

阿里云物联网套件目前没有官方的JS SDK。但是物联网套件使用的MQTT协议是通用的物联网通信协议,我们可以根据Java的SDK,接入阿里云产品的通用Openapi。

ruff程序代码示例:

'use strict';

var fs = require('fs');
var os = require('os')
var mqtt = require('mqtt')

var productKey = '填写在阿里云物联网套件中申请的productKey'
var deviceName = '填写在阿里云物联网套件中申请的deviceName'
var deviceSecret = '填写在阿里云物联网套件中申请的deviceSecret'
var targetServer = "tcp://" + productKey + ".iot-as-mqtt.cn-shanghai.aliyuncs.com:1883"
var port = 1883
var host = productKey + '.iot-as-mqtt.cn-shanghai.aliyuncs.com'

var clientId = os.hostname();
var timestamp = (new Date()).valueOf()
var mqttClientId = clientId + "|securemode=3,signmethod=hmacsha1,timestamp=" + timestamp + "|";
var mqttUsername = deviceName + "&" + productKey
var content = 'clientId' + clientId + 'deviceName' + deviceName + 'productKey' + productKey + 'timestamp' + timestamp

// var forge = require('forge')
// var hmac = forge.hmac.create();
// hmac.start('sha1', deviceSecret);
// hmac.update(content);
// var mqttPassword = hmac.digest().toHex();
// console.log(mqttPassword)
// 目前ruff上无法使用crypto等包,可以自行实现一个hmac sha1加密
var mqttPassword = '生成的秘文';

var puburl = "/" + productKey + "/" + deviceName + "/update"
var suburl = "/" + productKey + "/" + deviceName + "/get"

var tsl_options = {
  port: port,
  host: host,
  rejectUnauthorized: false,
  keepalive: 100,
  clientId: mqttClientId,
  username: mqttUsername,
  password: mqttPassword
}


$.ready(function (error) {
  if (error) {
    console.log(error);
    return;
  }

  var mqttClient = mqtt.connect(targetServer, tsl_options)

   mqttClient.on('connect', function () {
    console.log('********** Connected **********')
    //当按键被按下时,通过mqtt协议,向阿里云mns服务发送消息
    
    $('#button').on('push', function () {
      var data = { ts: (new Date()).valueOf(), deviceName: deviceName }
      mqttClient.publish(puburl, JSON.stringify(data))
      console.log(JSON.stringify(data))
    });
    //level 0:最多一次的传输
    //level 1:至少一次的传输
    //level 2:只有一次的传输
    mqttClient.subscribe(suburl, {qos:1})
   
    //当接受到消息时控制小红灯亮灭
    mqttClient.on('message', function (topic, message) {
      var msg = message.toString()
      console.log('您接收到的消息为: ' + msg)
      if(msg=='turn_on_led'){
        $('#led-r').turnOn();
        setTimeout(function(){
          $('#led-r').turnOff();
        }, 500)
      }
    })
  })
  mqttClient.on('error', function (error) {
    console.log(error)
  })

});

服务端代码示例:

const RPCClient = require('@alicloud/pop-core').RPCClient;
const MNSClient = require('@alicloud/mns');
const Base64 = require('js-base64').Base64;

var iotClient = new RPCClient({
  accessKeyId: '阿里云accessKeyId',
  secretAccessKey: '阿里云accessKeySecret',
  endpoint: 'https://iot.cn-shanghai.aliyuncs.com',
  apiVersion: '2017-04-20'
});

var mnsClient = new MNSClient('阿里云账户id', {
  region: 'cn-shanghai',
  accessKeyId: '阿里云accessKeyId',
  accessKeySecret: '阿里云accessKeySecret',
});

const queueName = 'aliyun-iot-xGEDKBE*****' //开通阿里云物联网套件生成MNS消息队列
//从队列中消费消息
setInterval(async ()=>{
  try{
    var receiveRes = await mnsClient.receiveMessage(queueName)
    var deleteRes = await mnsClient.deleteMessage(queueName, receiveRes.body.ReceiptHandle);
    var payload = JSON.parse(Base64.decode(receiveRes.body.MessageBody))
    var data = Base64.decode(payload.payload)
    console.log('从IoT设备接受到的数据为:' + data)
  } catch(err) {
    console.log(err)
  }
}, 500)


const productKey = '**********'    //阿里云物联网套件productKey
const deviceName = '**********'    //阿里云物联网套件deviceName

const iotClientParams = {
  ProductKey: productKey,
  TopicFullName: `/${productKey}/${deviceName}/get`,
  MessageContent: Base64.encode('turn_on_led'),
}

//向ruff发送消息
setInterval(async ()=>{
  try{
    var sedRes = await iotClient.request('Pub', iotClientParams)
  }catch(err){
    console.log(err)
  }
}, 2000)
  

*附注:服务端程序无需购买阿里云的ECS,在本地即可测试,物联网套件和MNS按量服务,非生产环境下几乎不会产生费用,注册阿里云账号即可使用。以上代码示例写的比较简单,可以实现简单的物联网设备与服务端程序的双向通信,抛砖引玉,期待大家一起实践起来!

最后,也发一条广告,阿里云前端团队不仅仅是前端!加入我们吧,用手中的JS探索星辰大海!