微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

我们用 JavaScript Hack 了家里的咖啡机

2017-07-26 20:30 伯乐在线


(点击上方公众号,可快速关注)


编译:伯乐在线/乔永琪

如有好文章投稿,请点击 → 这里了解详情



除了区块链,物联网或智能家电大约是当下最时髦的话题。若能将已有设备改装成物联网版本,就无需购买智能家电了。我们决定就这么做,用 JavaScript 构建一个网络驱动的咖啡机,并在超文本咖啡壶控制协议(Hyper Text Coffee Pot Control Protocol)创建 19 年后的今天,重新使用此协议。



讲故事时间


我和室友有几个通过 Amazon Echo Dots 控制的网络驱动设备。虽然很便捷,但还不能做到在床上就能触发咖啡机。


幸运的是,室友有一台闲置的德龙 Latissima 咖啡机,他也舍得此咖啡机;而且此咖啡机有一些电子按钮,整个系统貌似可编程。


为何采用 JavaScript?室友并非开发人员,而我已有三年没写过一行 C++ 代码了。此外,个人觉得 JavaScript 用起来很舒服。硬件和 JavaScript 均是事件驱动,其中心问题均是像“此控件是否被按下?”。但 JavaScript 硬件编程文档很少,这对我们构成一个不小的挑战。


硬件选择


确定采用 JavaScript 进行硬件编程之后,接下来需要考虑的是选取何种硬件:


Espruino



Espruino是一个开源的固件,有了它便可以在 ESP8266,或其它低功耗的微控制器上运行 JavaScript。


Espruino 自带一个基于网页的 IDE,方便程序员编写 JavaScript,并将其部署在硬件上。同时,它还拥有一套类似 Node.js 的模块系统,当然 并不是要让 Node.js 运行在微控制器上。


家里仅有的 Espruino 板是 Espruino Pico,而且不带WiFi,为此我必须焊接一个 ESP8266,不过谁有时间做这些?我们需要的只是咖啡!


Tessel

 


另一个选项是名为 Tessel 的开源硬件项目,此项目中的一部分便是 Tessel 2。硬件不仅能被 JavaScript 所控制,而且完全支持 Node.js 运行,还能存储整个程序,无需与电脑进行连接。



Johnny-Five



第三个选项是 Johnny-Five,它实际上是一个 npm 模块,可以与多个微控制器进行通信。初始化阶段,借助 Johnny-Five 的 I/O plug-ins, 采用 Firmata 协议,便可与 Arduinos,或者其它平台进行通信。例如,Particle Photon, Raspberry PI 甚至 Tessel 2。


我们的选择


我们决定用 Tessel 搭配 Johnny-Five。通过这种方式,写的代码与平台独立,但可以直接运行;无需链接计算机,或者在云端运行部分代码。


相信我,我是一名工程师


现在,我们已经知道了要用的工具,接下来开始我们的咖啡机操控之旅。考虑到硬件并没有一个版本控制系统,我们先看一眼这个漂亮的、正在工作的咖啡机,并记住它。



那我们开始吧!我们俩都不是电子工程师,而且虽然在大学期间学过两个学期,但什么都不记得了。



注意:我们工作的硬件,通电后会很烫。从这个角度看,我们做的一些事,既不智能,也不安全。倘若你要做同样的事情,风险自己承担。



在没有任何提示线索的情况下,如何操控硬件呢?


我们采用一种最浅显的方式,拆解咖啡机,一探究竟。



然后看到的就是这些,哪些是我们关心的呢?我的意思是,我们并不打算更改煮咖啡的方式,仅是触发煮咖啡的工序,以及打开/关闭咖啡机。


幸运的是,看起来貌似行得通。细看你会发现,带有按钮的控制面板,以及控制煮咖啡的流程的微控制器,是由一个普通的可插拔电缆进行连接。



倘若我们能够模拟控制板和微控制器之间的信号,想必便能控制咖啡机。此刻,仅需要找出此类信号。


我们该做些什么呢?


为了鉴别发送了何种信号,首先先看控制板:



可以看到如下有意思的控件:


  • 带有八个管脚的插头

  • 七个LEDs

  • 六个按钮开关


我们认为,八个管脚中至少需要一个用来提供电量。我们发现,八根电线中标记为红色的那一条它可能负责提供电源。那么剩下的七个管脚呢?按照程序员的思维方式,我们采用二进制的标签,表示这七个 LED 和六个开关的状态,因此需要为 LED 准备三个管脚,为开关准备三个管脚。最后一个管脚我们用来做什么呢?


基于假设,首个管脚为电源,那我们为其连上电源,来探究接通余下的管脚会发生什么。为此,将原始电线的一端连在控制板上,将一束跳线连到另一端。通过此种方式,便可将七个未知管脚连入 Tessel 管脚中。



对于电源,有两种可选项 3.3V 和 5V。首先尝试 3.3V,很少有 LED 被点亮。接着我们尝试 5V,成功了。有一半成功了,七个 LED 中有三个被点亮,这也是一种进步。


Tessel 带有模拟和数字管脚,这意味着除了数字信号(高/低,也叫 0/1)之外,我们还可以测量这些管脚的电压。我们先尝试测量余下七个管脚的电压。


我们为 Tessel 写了一个很小的脚本,借助于 Johnny-Five 初始化连接主板,并在模拟模式下创建管脚实例(模式:2)。接着将这些管脚放入 Johnny-Five 的 REPL 中,这样便可测量任何我们想要的值。


analogread.js

 

const five = require('johnny-five');

const Tessel = require('tessel-io');

const board = new five.Board({

  io: new Tessel()

});

board.on('ready', function () {

  const p2 = new five.Pin({

    pin: 'b1',

    mode: 2

  });

  const p3 = new five.Pin({

    pin: 'b2',

    mode: 2

  });

  const p4 = new five.Pin({

    pin: 'b3',

    mode: 2

  });

  const p5 = new five.Pin({

    pin: 'b4',

    mode: 2

  });

  const p6 = new five.Pin({

    pin: 'b5',

    mode: 2

  });

  const p7 = new five.Pin({

    pin: 'b6',

    mode: 2

  });

  const p8 = new five.Pin({

    pin: 'b7',

    mode: 2

  });

  const pins = [p2, p3, p4, p5, p6, p7, p8];

  board.repl.inject({ pins: pins });

});


不幸的是,模拟读数并不能得到我们想要的结论。测量值波动很大,无法有效提取信息;或许数值读数能帮到我们:


analogread.js

const p2 = new five.Pin({

  pin: 'b1'/*,

  mode: 2 */

});

const p3 = new five.Pin({

  pin: 'b2'/*,

  mode: 2 */

});

const p4 = new five.Pin({

  pin: 'b3'/*,

  mode: 2 */

});

const p5 = new five.Pin({

  pin: 'b4'/*,

  mode: 2 */

});

const p6 = new five.Pin({

  pin: 'b5'/*,

  mode: 2 */

});

const p7 = new five.Pin({

  pin: 'b6'/*,

  mode: 2 */

});

const p8 = new five.Pin({

  pin: 'b7'/*,

  mode: 2 */

});


借助这样的配置,采用一个比特,便可探究是否控制了 LED 或者找出一些规律。确实如此,对数字管脚分别设置 HIGH 和 LOW,我们发现管脚 4-6 可以打开/关掉 LED4,LED5,LED7。但不能打开剩余的LEDs。但至少我们知晓有 4 个管脚与 LED 相连。剩下的呢?



或许它们是按钮!我们将剩余的管脚初始化为按钮,尝试其是否工作:


analogread.js

oard.repl.inject({ pins: pins });

  const btn2 = new five.Button('b1');

  const btn3 = new five.Button('b2');

  const btn7 = new five.Button('b6');

  const btn8 = new five.Button('b7');

  const buttons = [btn2, btn3, btn7, btn8];

  buttons.forEach(btn => {

    btn.on('press', () => console.log('Pressed button no.%d', btn.pin));

    btn.on('release', () => console.log('Released button no.%d', btn.pin));

  });

});


这也凸显了 JavaScript 和硬件相结合的一个优势。相信每位 JavaScript 开发人员此刻都会感到非常自在。我们有一个 EventEmitter 类,其基于 Button 类拥有两个可监听事件 ‘release’ 和 ‘press’。


我们确实测到了一些东西!按下最上面的两个开关,检测到 btn7 和 btn8 触发了这些事件;按下右下角两个按钮,同样也能检测到 btn7 和 btn8 触发了这些事件。将 p3 设置为 HIGH,能检测到剩余的两个按钮的行为。



那么现在我们都知道了些什么呢?


  • 管脚 1 为电源,打开 LED4, LED5, LED7

  • 管脚 4-6 能够关闭 LED4,LED5,LED7

  • 管脚 7-8 对 S1-S6 开关能做出反应

  • 管脚 2 能操纵开关 3 和开关 4

  • 管脚 3 能操纵开关 1 和开关 2


但这远远不够,如何分辨按下的是哪个按钮?我们能够触动或使按钮失效,但更想知道的是这些按钮是何时被按下的。经过5个小时的低效尝试,须改变策略了。



重新绘制电路板



于是我们采用一种更加系统的方式,试着理清主板上的数字电路。为此,我们为主板拍照,将其放大,并追踪其上的每一条线。接着采用万用表测量两点之间的电阻,验证两点之间是否存在连接。倘若存在某个连接,使用一个叫 Fritzing 的工具,便可在图表上标记这条连接。Fritzing 是一个很棒的开源工具,用以标记硬件设置。本图表忽略了很多东西,以至于看起来像一个电阻器。最后呈现出来的是一个很漂亮的图表。



我知道你想说什么,这看起来比之前还糟糕。但事实上却不尽然。首先,我们很容易就找出了 LED 连接。但最重要的是须知晓 LED 如何允许电流仅按某一方向通过。而在图表上进行标记,有助于我们追踪电流,以便更好地理解整个电路。


除了管脚 1 之外,管脚 2 和 3 均为电源!这就解释了为何剩余 LED 无法点亮。那开关呢?


经历了 8 个小时缓慢的进展,盯着这个小板子看,我们意识到一直以来自己都忽略了一件事。



这三个元件并不是电阻器,它们是二极管!!二极管允许 LED(发光二极管)中电流只沿着一方向流动。因此,我们更新了图表,移除了 LED,并对图表进行了清理。



终于解决了困惑我们已久的协议:



有人想来杯咖啡吗?


如何运用此协议煮咖啡呢?考虑到开关依赖于两个管脚之间传输的电流,模拟此协议势必太过复杂。但有一个更简单的方式,用继电器!


继电器与开关类似,但是通过微控制器,可以数字化地操控这些继电器。考虑到我们已知控制板的所有电路,可以自行创建一个相同的电路,用继电器对开关做简单替换。


我们仅有四个继电器,因此只模拟右边三个开关,它们分别控制大杯咖啡、浓咖啡、电源。这些正是我们需要的!同样,我们分别接了三个 LED 作为视觉反馈,表明一切正常运行。


初次布线看上去不是特别整洁,如下:



没有足够的跳线,因此决定将继电器倒置放入面包板(breadboard)中。


接着检测布线。不过在打开咖啡机前,我们要写点代码。快速实现一个小脚本,借助 Johnny-Five 的内嵌类 Relay,手动控制三个继电器,将其放入 REPL 中。


analogread.js

const five = require('johnny-five');

const Tessel = require('tessel-io');

const board = new five.Board({

  io: new Tessel()

});

board.on('ready', () => {

  const espresso = new five.Relay({

    pin: 'a4',

    type: 'NO'

  });

  const grande = new five.Relay({

    pin: 'a5',

    type: 'NO'

  });

  const power = new five.Relay({

    pin: 'a6',

    type: 'NO'

  });

  espresso.close();

  grande.close();

  power.close();

  board.repl.inject({

    espresso: espresso,

    grande: grande,

    power: power

  });

});


写好代码,倒一些水到咖啡机中,通过设备背面的主开关,打开咖啡机,为代码通电。倘若一切正常,运行 power.open() 便可打开设备。需要加热 20 秒,直到 LED 停止闪动。接着开始煮咖啡,其实第一次测试我们用的是水。


成功了!10个小时过后,太阳渐渐升起,我们终于得到了咖啡。或者说是热水。因为我们忘记买 Nespresso 胶囊了,不管怎样第一步成功了。



互联网化的咖啡壶


太棒了,仅需要手提电脑连接 Tessel,Tessel 连接咖啡机,就可以这样煮咖啡了。然后写点代码至 REPL 中。此刻是时候将其改装为互联网驱动的咖啡壶了。


幸运的是,19年前的今天,一些聪明的人们,开发了一个足以改变生活的协议。超文本咖啡壶控制协议(Hyper Text Coffee Pot Control Protocol),或简写为 HTCPCP,用以控制互联网驱动的咖啡壶。这难道不是我们应该在 2017 年重新启用这个已被遗忘的协议,用以控制我们的咖啡机的一个信号吗?


什么是超文本咖啡壶控制协议?


或许你注意到 HTCPCP 是 1998 年的一个愚人节玩笑。那是因为它建议将 HTTP 状态代码 418-I’am a teapot,作为 RFC 文件的一部分。此状态代码时至今日依旧不是一个实际的标准,仅作为许多软件“复活节小彩蛋”。以谷歌为例:google.com/teapot


当然此协议远不止这一个状态代码,我们来探究一番:


1 基于HTTP

2 它提议除了 POST 方法外,BREW 方法也可以用来发布煮咖啡的命令

3 定义了 coffee:// URI规则,借此访问不同的咖啡壶

4 甚至有国际化本 URI 规则,强烈建议你阅读此部分

5 推荐一个新的头字段 Accept-Additions,指定你想要的调味品,比如牛奶、威士忌、或者南瓜拿铁

6 清晰地定义了 Content-Type,以及可传递的信息

7 其它事项


换言之,它命中注定要被实现。


在正式开始之前,我们需要讨论一下本 RFC 文件的不确定性以及缺点。


文件两次提及,需使用不同的两个 Content-Type。其一为 application/coffee-pot-command,其二为 message/coffeepot。 application/coffee-pot-command 作为 Content-Type 发送给咖啡机,message/coffeepot 作为回应类型,因此我们不能同时使用这两个值。


另外,与1997年的标准咖啡壶,如今的咖啡机所支持的咖啡种类繁多。因为需要修改 URI 规则解决这些差异:coffee://host/pot-{identifier}/{coffee type}。这尤为重要,因为 application/coffee-pot-command 仅支持 start 和 stop 这两个官方值。


现在我们来实现它


418 – 我是个茶壶



考虑到 HTCPCP 基于 HTTP,以及便捷和集成,我们采用 Node.js 和 http module,实现一个标准HTTP服务器。为了保持 Tessel 代码的轻量级,决定不使用任何诸如 Express 的库,而是使用 URL 这种纯粹的方式。


同样,与硬件交互的逻辑代码放入独有的文件中,以后便可容易拿来为其它咖啡机所用,采用同样的服务器逻辑。


latissima.js

const EventEmittter = require('events');

const { Board, Relay, Pin } = require('johnny-five');

const Promise = require('bluebird');

const Tessel = require('tessel-io');

const PRESS_DURATION = 500;

class Latissima extends EventEmittter {

  // Board

  board = undefined;

  // Relays

  espressoRelay = undefined;

  grandeRelay = undefined;

  powerRelay = undefined;

  // Others

  // The coffee machine is automatically on when you flip the master switch

  isOn = true;

  // No this isn't a teapot

  isTeapot = false;

  

  // Available Additions

  additions = [];

  // Coffee Types

  static Types = {

    espresso: 1,

    grande: 2

  };

  constructor(debug = false) {

    super();

    this.board = new Board({

      io: new Tessel(),

      repl: debug,

      debug: debug

    });

    this.board.on('ready', () => {

      this.initializePins();

      this.emit('ready');

    });

  }

  initializePins() {

    this.espressoRelay = new Relay({

      pin: 'a4',

      type: 'NO'

    });

    this.espressoRelay.close();

    this.grandeRelay = new Relay({

      pin: 'a5',

      type: 'NO'

    });

    this.grandeRelay.close();

    this.powerRelay = new Relay({

      pin: 'a6',

      type: 'NO'

    });

    this.powerRelay.close();

  }

  pressPower() {

    if (!this.isOn) {

      // it needs to heat up for ~20 seconds

      // TODO: Remove this when we can read if the machine is on

      setTimeout(() => {

        this.isOn = true;

      }, 21 * 1000)

    } else {

      this.isOn = false;

    }

    return this.pressButton(this.powerRelay);

  }

  // Convinience function to press different coffee types

  press(type) {

    switch(type) {

      case Latissima.Types.espresso:

        return this.pressButton(this.espressoRelay);

      case Latissima.Types.grande:

        return this.pressButton(this.grandeRelay);

      default:

        return Promise.reject(new Error("Could not find Type"));

    }

  }

  // Emulate the push of a button by opening

  // the respective relay, wait for a certain

  // time and closing it again

  pressButton(relay) {

    return new Promise((resolve, reject) => {

      relay.open();

      setTimeout(() => {

        relay.close();

        resolve();

      }, PRESS_DURATION);

    });

  }

}

module.exports = { Latissima };


此处用到的某些 JavaScript 特性并没有包含在 Tessel Node 版本中,因此借助 TypeScript 将其转译为 ES5 兼容的代码。


作为服务器,我们决定实现以下功能:


1. GET, POST 以及 BREW方法实现


我们接纳了 BREW 方法,并对其做了实现。传统的 GET 用来获取咖啡机状态,而 POST 则用来发布命令


index.js

 

if (req.method === 'GET') {

  console.log('GET');

  res.setHeader('Safe', 'yes');

  res.setHeader('Content-Type', 'message/coffeepot');

  if (req.url.indexOf('/pot-0/') === 0) {

    console.log('TYPE INFO');

    // TODO: Implement status of coffee production

    let data = [];

    res.write(data.join('n'));

  } else if (req.url.indexOf('/pot-0') === 0) {

    let data = ['isOn='+coffeeMachine.isOn]

    res.write(data.join('n'));

  } else {

    res.statusCode = 404;

  }

  return res.end();

}

if (req.method === 'POST' || req.method === 'BREW') {


2. Content-Type 校验


仅接受 application/coffee-pot-command 以及 text/plain,作为 POST 以及 BREW 请求的合格的 Content type。随后还要用 Alexa 为集成添加 text/plain。


index.js

 

function hasCorrectContentType(req) {

  return req.headers['content-type'] === 'application/coffee-pot-command'

    || req.headers['content-type'] ===  'text/plain';

}


3. 增加 Accept-Additions 支持


仅有四个继电器,我们是无法操纵所有的咖啡机功能的。因此,咖啡机不支持任何附加功能。有人订阅了咖啡机并不支持的额外服务时,我们对头字段和行为更正添加了验证,以下是额外服务清单。


index.js

 

function getAdditionsRequested(req) {

  let header = req.headers['accept-additions'];

  let milkType = ['Cream', 'Half-and-half', 'Whole-milk', 'Part-Skim', 'Skim', 'Non-Dairy'];

  let syrupType = ['Vanilla', 'Almond', 'Raspberry', 'Chocolate'];

  let alcoholType = ['Whisky', 'Rum', 'Kahlua', 'Aquavit'];

  let spiceType = []; /** NO LIST DEFINIED IN RFC */

  let validTypes = [

    '*',

    ...milkType,

    ...syrupType,

    ...alcoholType,

    ...spiceType

  ];

  if (!header) {

    return [];

  }

  return header.split(';')

    .map(type => type.trim())

    .filter(type => validTypes.indexOf(type) !== -1);

}


4. 418 – 我是个茶壶


显然,我们需要实现一些事无巨细地检查,检查连接服务器的咖啡壶是不是一个伪装的茶壶。在此情形下,我们需要返回一个恰当的状态码。


index.js

 

function handleRequests(req, res) {

  if (coffeeMachine.isTeapot) {

    res.statusCode = 418;

    res.statusMessage = "I'm a teapot";

    res.write("I'm a teapot");

    return res.end();

  }


没有实现的部分:


目前无法通过编程检测咖啡机的状态。LED 虽能显示各种状态,但我们不得不解决在 Tessel 2 中检测状态的问题。而且我们不能恰当地实现 Safe 头字段或者 GET 状态。


尽管实现了一个 coffee:// 规则的基本版本,但我们并没有做全部的实现,而且做了必要的修改。否则,我们需要花费大量时间,还需要借助其它诸如 Amazon Alexa 的平台。


为了使 Tessel 便于访问,我们额外安装了 localtunnel module,创建了一个可外部访问 URL。


一切正常,你可以看到一个成功运行的 HTCPCP 协议:


Brew a coffee via HTTP using JavaScript and a hacked coffee machine(https://www.youtube.com/watch?v=L_kzjHtkGPE)


所有代码均存储在 GitHub 中


Implementation of the HTCPCP for DeLonghi Latissima using Tessel 2(https://github.com/dkundel/htcpcp-delonghi)


IFTTC – If This Then Coffee


此刻已拥有一个正常运行的 HTTP 服务器,由此操纵咖啡机,接下来该连接 Alexa 了。最理想的方式是,创建 Smart Home skill,将其与家里的其它智能设备进行关联。然而,时间宝贵,因此,我们采用一种更快的方式:IFTTT (If-This-Then-That)。


幸运的是,Alexa 支持创建简单的 skills,借助于 IFTTT 便可进行 HTTP POST 请求。这样我们便可操控咖啡机!



对此我们提供了三种 skills。第一种是在待机状态下启动咖啡机;第二种,煮常规咖啡;第三种,煮浓咖啡。



最棒的是,做起来跟说出来一样容易,仅仅三分钟,即完成了对 Alexa 的集成,具体请看:


Brew a coffee using Amazon Alexa and JavaScript(https://www.youtube.com/watch?v=JHUi334R9zw)


我们依旧沉浸在取得初级版本的兴奋当中,但接下来还有很多工作要做,计划如下:


  • 制定出借助编程判定咖啡机状态的方案

  • 添加更多的继电器,以便实现咖啡机的所有功能

  • 利用传感器判断咖啡机中是否有杯子

  • 找出如何制作爱尔兰咖啡的方案,也就是如何在咖啡中添加威士忌

  • 为所有设备做一个套件,以便整齐地摆放在咖啡机顶部

  • 集成 Twillio,这样即使距离超远,没有 Alexa,也可以触发煮咖啡命令

  • 解决必须手动添置新的 Nespresso 咖啡包的问题


每过一天这个清单都会变长,一些酷炫的点子还会接着冒出来,我猜你或许也有不错的点子,如果有,请发推特至 @dkundel。期待听到你的消息!


结论


现今的设备大都是封闭的,以至于很难再打开一探究竟,能做这样一个小项目真的很棒。也不得不说借助 Tessel 2,Johnny-Five 以及 JavaScript ,开展一些工作是一件很有意思的事情。本次是对硬件操纵的首次尝试,我在家里拥有一整套 Sparkfun 的 Johnny-Five Inventor’s Kit,此刻迫不及待,计划着将 kit 中的剩余部件应用于更多项目中。


个人认为,JavaScript 面向事件的方式可以很好的同硬件进行匹配。当然,你若计划创建一个能效、空间利用更高的项目,还须回到 C++。但与本例相似的硬件操纵项目,采用此种方式也是相当不错的。


总之,这个是一个很有趣的项目,最终我们将硬件操纵与 HTCPCP RFC 的读取以及实现集成起来。希望这能激发你在硬件操纵领域的兴趣,并为之积极探索。如果你在做了这方面工作,劳烦知晓一声。或许你们当中的有人愿意尝试操纵面包机。



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

 
前端大全 更多文章 前端模板的原理与实现 让我们一起学习JavaScript闭包吧 WebStorm 2017.2 发布:更好地支持 Webpack 总结个人使用过的移动端布局方法 详解 JavaScript 闭包
猜您喜欢 Python 日志功能的实现 Ruby 2.4 的一些新特性 双11前来根震动棒?网络购物APP实力榜,提前体会上上下下的感觉 图像风格化技术(1) 作为一名数据科学家Python需要掌握到什么程度?