JavaScript-物联网指南-全-

JavaScript 物联网指南(全)

原文:JavaScript on Things

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分.JavaScript 开发者对硬件的入门

这一部分的书将向你介绍嵌入式系统和电子电路的基础。在第一章(kindle_split_008.html#ch01)中,你将了解什么是嵌入式系统以及如何分析其组成部分。我们将花一些时间探讨 JavaScript“控制”硬件的含义,并检查 JavaScript 和电子学可以协同工作的不同方式。

你将在第二章中遇到 Arduino Uno R3 开发板,我们将从第二章到第七章(kindle_split_016.html#ch07)的所有实验中使用它。你将了解开发板的主要部分的功能以及它们如何与其他软件和硬件组件交互。你将使用 Arduino IDE 和 Johnny-Five Node.js 框架尝试一些基本的 LED 实验。

第三章 将会教授你电子电路的关键基础,深入探讨欧姆定律以及电压、电流和电阻之间的关系。你将在面包板上工作,构建包含多个 LED 的串联和并联电路。

当你完成这本书的这一部分后,你将掌握基本的嵌入式系统基础和核心电路概念。你将准备好开始构建带有不同输入和输出的小型 JavaScript 控制项目。

第一章.将 JavaScript 和硬件结合

本章涵盖

  • 涉及业余项目和“物联网”的组件和硬件

  • 嵌入式系统的常见组件

  • 使用 JavaScript 与嵌入式系统交互的不同方法

  • 开始构建所需的工具和材料

作为一位熟练的 JavaScript 网络开发者,你每天都在进行逻辑炼金术。但现在,你可以以新的方式运用你的软件开发技能,在现实世界中编程和控制事物。在本章中,你将了解不同项目和设备中涉及的硬件,并了解 JavaScript 和硬件如何协同工作。

我们被一些小魔法事物所包围,它们将物理世界与逻辑的、连接的和虚拟的世界融合在一起(图 1.1)。一个可以无线广播其位置的钥匙扣,这样你就可以用智能手机上的应用程序找到它。一个植物盆,当需要浇水时会发出抱怨的声音,或者更好的是,给你发送一个任性的短信。数十亿这样的物体闪烁、哔哔声、推文、自动调暗灯光、制作定制的茶,并在全球范围内执行它们的专业任务。

图 1.1. 哦,我们世界中的神奇事物!

构建这些设备很有趣。在用这些类型的物理小玩意儿进行创作时涉及的创造力,以及创新性家庭自酿项目的草根魅力——这些都是吸引网络开发者的东西。我们擅长原型设计,尝试新技术,并开辟自己的道路。

但开始可能会让人感到害怕。当我们看到所有的电线和组件,听到行话,站在硬件黑客社区的外面时,涉及的技能可能会感觉难以逾越,陌生。作为一名 JavaScript 开发者,当你初次尝试进入物理硬件的世界时,可能会遇到一些障碍——感知到的复杂性、信息过多且分散,以及硬件和软件概念的混淆。

我们将利用你的 JavaScript 知识作为优势,作为学习如何设计和构建构成“物联网”(IoT)以及激发硬件黑客灵感的事物的辅助工具。你将能够利用你的软件开发技能跳过一些干扰,快速集中精力学习你需要掌握的新技能。

为了了解我们所要经历的旅程,让我们首先看看你将学会构建的事物类型。让我们探讨当我们说事物硬件时,我们确切地指的是什么。

1.1. 硬件项目的结构

我们可以构建一个小装置,当环境变暖时自动打开风扇。这个微型独立气候控制系统将连续监测周围环境的温度。当它变得太热时,风扇就会打开。当它变得非常凉爽时,风扇就会关闭。

虽然我们不会因为发明这个诚然有些平凡的装置而赢得任何声望显赫的奖项,但它的基本成分与其他——更鼓舞人心——你将学会构建的事物是共通的。

1.1.1. 输入和输出

最重要的是——实际上也是唯一需要做的——我们的温度触发设备需要在太热时打开风扇,并在周围区域冷却后再次关闭风扇。由电机驱动的风扇是一个输出设备的例子。

为了获取关于周围环境温度的连续信息——以便设备可以决定何时打开或关闭风扇——我们需要从输入获取数据,在这种情况下是一个温度传感器(图 1.2)。

图 1.2. 自动风扇系统需要从温度传感器获取输入,并管理电机风扇的输出

图片描述

输入为系统提供数据,而传感器是一种提供有关物理环境数据的输入类型。在项目中你可以使用各种传感器:光传感器、热传感器、噪声传感器、振动传感器、蒸汽传感器、湿度传感器、气味传感器、运动传感器、火焰传感器——等等。有些,比如我们风扇的温度传感器,提供简单的数据——只是一个代表温度的单个值——而其他,比如 GPS 或加速度计,则产生更复杂的数据。

一个项目的输出代表其对使用者的净功能。闪烁的灯光、恼人的蜂鸣声、LCD 屏幕上的状态读数、机器人臂的横向移动——这些都是输出类型。对于这个项目,风扇是唯一的输出。

并非所有输入和输出都必然在物理世界中体现。当一位顾客在尝试在线订购产品时遇到错误(虚拟输入),可能会在坐在支持技术人员桌上的设备上点亮红灯(物理输出)。相反,土壤湿度的变化(物理传感器输入)可能会使花盆发送一条要求的信息(虚拟输出)。

1.1.2. 处理

我们的自动风扇也需要一个大脑,它能关注温度传感器的读数,并在温度过高时打开风扇。它需要的大脑实际上是一台微型计算机:一个处理器、一些内存以及处理输入和控制输出的能力。当处理器、内存和 I/O 功能包含在一个单一物理包中时,我们将这种芯片称为微控制器(图 1.3)。

图 1.3. 自动风扇需要一个大脑。一个流行的选择是微控制器,它将处理器、内存和 I/O 功能集成在一个单一组件中。

图片 1.3 的替代文本

微控制器(MCU)不如笔记本电脑中的通用处理器强大。大多数不能运行完整的操作系统(大多数,而不是所有,正如你将看到的),但它们便宜、可靠、小巧,功耗极低——这就是为什么它们在硬件项目和产品中如此普遍,就像我们虚构的自动风扇一样。

1.1.3. 电力、电路和系统

现在我们有了输入、输出和大脑——是时候将比特组合成一个系统了。我们需要使用一个或多个电子电路连接组件,并提供一些电力。构建一个系统既涉及电路设计,也涉及在物理空间中对组件的操作(图 1.4)。

图 1.4. 一个粗略的示意图,展示了风扇的输入、输出和微控制器如何在带电和电路的系统中连接。如果你对符号不熟悉,不要担心——在我们继续旅程的过程中,你将学习有关电路的知识。

图片 1.4 的替代文本

直接将电线连接到微控制器的小型引脚上需要焊料和非常稳定的手。更不用说我们最终会有一堆松散的部件尴尬地漂浮着。为了帮助硬件开发者,微控制器通常安装在物理开发板上(图 1.5)。除了其他功能外,板子使得将 I/O 设备连接到微控制器更加容易。

图 1.5. 基于微控制器的开发板使得连接输入和输出设备更加方便。

开发板有所帮助,但我们仍然有一堆松散的电线和组件。为了帮助整理这些,硬件开发者使用一种称为面包板的电路板原型工具(图 1.6)在物理空间中布置电路。

图 1.6. 面包板提供了一个电连接的网格,可以在其上原型化电子电路。

1.1.4. 逻辑和固件

我们硬件设计正在顺利进行,但你可能想知道微控制器是如何知道该做什么的。这里涉及到逻辑,如下面的列表所示:监听传感器,做出决定,发送指令来打开或关闭风扇。

列表 1.1. 温度触发风扇逻辑的伪代码
initialize temperatureSensor
initialize outputFan
initialize fanThreshold to 30 (celsius temperature)

loop main
    read temperatureSensor value into currentTemp
    if currentTemp is greater than fanThreshold
        if outputFan is off
            turn outputFan on
    else if currentTemp is less than or equal to fanThreshold
        if outputFan is on
            turn outputFan off

编程微控制器的占主导地位的语言长期以来一直是 C(或类似派生语言)。为微控制器编写 C 语言通常是平台特定的,并且可能相当底层。引用特定内存地址和位操作是常见的。

代码被编译成特定架构的汇编代码。为了将代码上传到项目中,它需要物理上传,或烧录,到微控制器的程序存储器

这种程序存储器通常是非易失性存储器——ROM,这种存储器允许微控制器“记住”程序,即使断电(与 RAM 相对,RAM 只有在供电时才保留其内容)。可用的程序空间有限,通常在几十千字节左右,这意味着在微控制器上运行的程序需要仔细优化。

一旦程序被烧录到微控制器中,它就作为微控制器的固件运行——当供电时,微控制器会连续运行程序,直到被编程为不同的内容(或以其他方式重置)。

对于习惯于高级逻辑的 JavaScript 开发者来说,这种低级的具体性可能让人望而却步。不用担心。这正是 JavaScript 能帮到我们的地方,它允许我们为基于微控制器的硬件编写程序,而无需使用 C 语言或一开始就陷入十六进制寄存器地址的繁琐细节。

将程序固件上传到微控制器的过程也因芯片技术的进步和爱好者友好型开发板的广泛可用性而变得容易得多(图 1.7)。

图 1.7. 非易失性程序存储器(EEPROM 和闪存)以及用户友好的板使得用固件编程微控制器变得更加容易。

EEPROM(电可擦可编程只读存储器),以众所周知的闪存介质为例,在微控制器中常用。这种可重写内存使得反复用不同逻辑重新编程微控制器成为可能。

开发板除了使 I/O 连接更容易外,还通过提供方便的接口来编程板上的微控制器(USB 非常常见),帮助硬件黑客。这减轻了对专用硬件编程设备的需求。如今,编程微控制器通常就像插入一个 USB 线缆并在 IDE 中点击一个按钮一样简单。

1.1.5. 包装和封装

我们风扇的设计几乎完成了。但我们可以通过将自动风扇装入一个漂亮的包装盒来将其提升到下一个层次——嵌入我们的系统到某个东西里面,这样它的电线和电路就会从视线中隐藏起来(图 1.8)。哇塞!

图 1.8. 完成并包装好的自动风扇是嵌入式系统的一个例子。输入和输出由基于微控制器的微型计算机处理,并由电源和电路支持。整个装置都隐藏在一个非常漂亮的盒子里,因为,为什么不呢?

1.1.6. 嵌入式系统

虽然术语嵌入式系统听起来可能有点正式或令人望而却步,但实际上并不太复杂。一个结合处理器、内存和 I/O 的小型计算机构成了大脑。正如你在我们的自动风扇中看到的那样,将输入、输出和微型计算机连接起来并为其供电,就创建了一个独立的系统。我们称之为嵌入式,因为它通常被隐藏在某个东西里面——一个包装盒、一个泰迪熊、洗衣机控制面板、一把雨伞。

虽然自动风扇、雨天会发光的雨伞和会发推文的泰迪熊看起来并不立即相似,但它们比你想象的要更多共同之处。这些例子,以及构成物联网的大多数硬件项目和设备,都可以被描述为嵌入式系统

现在我们来看看 JavaScript 如何融入这幅画面。

1.2. JavaScript 和硬件如何协同工作

当将 JavaScript 与嵌入式系统结合使用时,我们仍然以与其他类型硬件项目相同的方式构建电子电路。仍然有输入和输出、电线和组件。然而,我们不是使用汇编语言或 C 语言来定义项目微控制器或处理器做什么,而是使用 JavaScript。

有几种方法可以做到这一点,不同的方法用于使用 JavaScript 为硬件项目提供逻辑。这些方法根据 JavaScript 逻辑本身执行的位置进行分类:在独立的嵌入式系统之外的宿主计算机上,在嵌入式系统的微控制器上,或者完全在其他地方。

1.2.1. 主-客户端方法

为了绕过某些微控制器的限制,主机-客户端方法允许您在更强大的主机计算机上执行 JavaScript。当主机运行代码时,它与嵌入式硬件交换指令和数据,嵌入式硬件表现得像客户端(图 1.9)。

图 1.9. 使用 JavaScript 控制硬件的主机-客户端方法

图 1.10

许多微控制器存在限制,这影响了它们运行 JavaScript 的能力。程序内存有限,这意味着复杂的程序可能无法适应或必须进行大量优化。此外,许多廉价的微控制器采用 8 位或 16 位架构,运行速度相对于桌面计算机较低。大多数无法胜任运行操作系统的任务,从而排除了在芯片上直接运行 Node.js 或其他 JavaScript 运行时的可能性。

相反,主机-客户端方法涉及在主机计算机上执行 JavaScript 逻辑,例如您的笔记本电脑,它确实拥有运行完整操作系统所需的强大能力。主机机器能够运行 Node.js 并可以利用全球 JavaScript 软件生态系统(包括 npm 和网络)。

使这种设置工作的小技巧是让客户端硬件(如微控制器)和主机系统(您的笔记本电脑)使用一种相互理解的“语言”——一个通用的 API 进行相互通信(图 1.10)。

图 1.10. 为了在这种方法下实现主机计算机和客户端硬件之间的通信,它们双方都需要使用一个通用的 API。

图 1.10

要配置我们的自动风扇系统使用这种方法,我们首先需要通过将特殊固件上传到微控制器的程序内存中来准备嵌入式硬件。与用于控制风扇的特定、单一用途的程序不同,这个固件程序使微控制器能够与其他使用相同“语言”(API)的来源进行双向通信。也就是说,它将基于微控制器的硬件转变为客户端,全神贯注并准备好执行主机计算机的指令(图 1.11)。

图 1.11. 特定固件将微控制器转换为客户端。

图 1.11

硬件现在已准备好进行通信——下一步是使用主机计算机编写风扇的软件。为了硬件和软件能够相互理解,主机计算机需要用微控制器能够理解的语言发出指令。为了实现这一点,我们可以使用实现通用 API 的库或框架编写代码(图 1.12)。

图 1.12. 主机也需要使用通用的 API 进行通信。

图 1.12

主机通过物理、有线连接(通常是 USB)或无线(WiFi 或蓝牙)连接到客户端硬件。

然后,我们在宿主计算机上执行控制风扇的 JavaScript。宿主持续向客户端发送运行风扇的指令。客户端也可以向宿主发送消息,例如来自温度传感器的数据(图 1.13)。

图 1.13. 当主机执行 JavaScript 逻辑时,客户端和主机之间会持续交换指令和数据,使用一个共同的 API。

不要慌张,你不需要编写低级固件协议 API 软件!对于固件和 Node.js 框架,有直接、开源的选项来实现这些固件协议,因此你可以用最少的麻烦编写宿主端的 JavaScript 逻辑。

主客户端方法的好处是易于设置,并且支持许多平台。更重要的是,它让你能够访问整个 Node.js 生态系统,同时避免了廉价微控制器的性能和内存限制。缺点是客户端硬件没有主机就无法工作——它只能在主机计算机积极运行软件时才能执行其功能。

我们最终会无线化,但我们将从最简单的宿主-客户端选项——USB 连接——开始。这意味着在一段时间内,你的项目将物理连接到你的计算机上。

1.2.2. 嵌入式 JavaScript

嵌入式 JavaScript 中,控制项目的 JavaScript 逻辑直接在硬件的微控制器上运行。

许多微控制器无法本地运行 JavaScript,但有些可以。随着技术的进步,廉价微控制器变得越来越先进。现在可以在某些嵌入式处理器上直接运行 JavaScript 或优化的 JavaScript 变体。

每个嵌入式 JavaScript 平台都是硬件和软件成分的组合,协同工作。在硬件方面,能够本地运行代码的开发板基于更强大的(但仍然便宜)的芯片。

大多数平台还提供了一套软件工具来补充其硬件。可能有一个库或框架用于编写兼容的 JavaScript 代码,以及 CLI(命令行界面)或其他方法来准备代码并将其上传到微控制器。

Espruino (www.espruino.com) 是一个基于 JavaScript 的嵌入式平台的例子。Espruino 的 JavaScript 风格结合了优化的核心 JavaScript 和与硬件相关的 API。例如,你可以在基于网络的 IDE 中编写 Espruino Pico 板的代码,并通过 USB 上传到板子上(图 1.14)。为了使我们的自动风扇适应 Espruino 板,我们需要使用 Espruino 的 API 编写逻辑。

图 1.14. Espruino 平台结合了小型硬件板和 IDE 开发环境。

嵌入式 JavaScript 的另一个例子是 Tessel 2(tessel.io/),这是一个基于 Node.js 的开发平台。你可以使用tessel-cli npm 模块控制并部署代码到你的 Tessel——如果你喜欢,也可以无线部署,因为 Tessel 2 内置了 WiFi(图 1.15)。

图 1.15。Tessel 2 是一个开源平台,可以原生运行 Node.js。

图 1.15

能够在嵌入式硬件上直接运行 JavaScript 可以节能且自包含。项目是独立系统,可以独立运行。与需要固件将 JavaScript 转换为机器码的主机-客户端设置不同,通常在 JavaScript 和硬件之间有更少的抽象层。

这听起来很棒,你可能会想知道为什么我们不只用这种方法。有几个缺点。首先,目前硬件选项较少。此外,每个平台都有其特定的平台技术(软件、工具、方法),这些可能会在学习硬件基础知识时混淆视听。大多数平台也有一定的限制,要么是在 JavaScript 语言功能支持上,要么是在支持的输入和输出类型上。但这是一个鼓舞人心的方法,有着非常光明的未来。

1.2.3。其他硬件-JavaScript 组合

除了主机-客户端方法和运行嵌入式 JavaScript 之外,还有几种方法可以将 JavaScript 与硬件项目结合。

小型、单板计算机(SBCs)将主机和客户端合并为一个单元。基于云的服务使得在线编写 JavaScript 代码并无线部署到硬件成为可能。而且,浏览器本身的新兴、新功能和实验性特性可能为成千上万的网络开发者打开通往硬件世界的大门。

在小型计算机(SBCs)上运行 JavaScript

类似于树莓派系列和 BeagleBone Black 这样的单板计算机(SBCs)可以运行完整的操作系统环境(通常是 Linux),并且可以扩展到 Node.js。与 8 位或 16 位微控制器不同,SBCs 拥有更高性能的通用处理器。但许多 SBCs 也具有直接集成到同一板上的 I/O 引脚和功能(图 1.16)。

图 1.16。几款单板计算机(SBCs):Intel Galileo Gen 2(顶部)、Raspberry Pi 2 Model B(左下角)和 Raspberry Pi Zero(右下角)

图 1.16

使用 SBC 控制硬件项目结合了主机-客户端方法和运行嵌入式 JavaScript 的特点。处理器必须持续运行 JavaScript 逻辑以使项目工作(如主机-客户端模型),但整个包都包含在一块板上,感觉更像是一个独立的、嵌入式设置。

与运行嵌入式 JavaScript 逻辑的微控制器不同,SBC 上的处理器不运行单一用途的程序——它可以同时运行其他进程。

这些单板计算机正在变得越来越便宜。目前,有 5 美元的 Raspberry Pi Zero(如果你能弄到的话——它们因缺货而闻名)和带 WiFi 的 Pi Zero W,价格略高一些。低功耗微控制器硬件和真正的微型计算机之间的成本差异已经不再那么大了,这些微型计算机的处理器性能可以与平板电脑和智能手机相媲美。

虽然在支持 GPIO(通用输入/输出)的单板计算机上运行 JavaScript 给你提供了很多选项,但也有一些缺点。单板计算机的功耗并不像许多基于微控制器的板那么低——Raspberry Pi 2 Model B 消耗 4 瓦。我们将要查看的单板计算机确实支持 GPIO,但引脚映射和用法可能令人困惑,文档可能简略或技术性,这对于刚开始学习硬件黑客的人来说可能是一个挑战。你还需要准备好面对系统管理的障碍,因为单板计算机的 Linux 发行版,尤其是当与 Node.js 结合使用时,可能需要一些调试和耐心。

云服务与浏览器

这个最后涵盖硬件-JavaScript 组合的通用类别确实有些模糊。事物正在变化。非常快。目前,商业云服务在物联网领域的增长呈现出典型的曲棍球棒形状,而我们只是看到了让我们能够直接从浏览器本身与硬件交互的进步先锋。

云服务试图简化大规模管理物联网设备群组的复杂性。其中许多针对企业。例如,Resin.io (图 1.17) 构建、打包和部署容器化应用程序代码到预配置的设备,为你处理一些安全和自动化难题。

图 1.17. Resin.io 服务有助于简化将应用程序部署到和管理具备 Linux 功能的单板计算机的过程。

图 1.17 选项

然后还有浏览器本身,其中许多最前沿的硬件-JavaScript 组合才刚刚开始出现。一些浏览器已经允许你尝试 Web Bluetooth,这是一个 API,虽然目前不在标准轨道上,但可能是未来网络事物的预兆。正如其名所示,Web Bluetooth 允许你从浏览器内部使用 JavaScript 连接到并控制蓝牙低功耗(BLE)硬件。

另一个由谷歌推出的开源项目,物理网络,提出了一个简单的想法:赋予小型设备通过蓝牙低功耗(BLE)广播 URL 的能力。这样的信标可以将公交车站牌变成实时到达跟踪器,通过向包含该信息的网络应用广播 URL (图 1.18)。这是一个简单的概念,但非常灵活。

图 1.18. 在这个物理网应用的示例中,一个公交车站牌使用 BLE 信标每秒广播一个 URL(1);附近的人可以在他们的设备上扫描可用的信标并选择与公交车站相对应的信标(2);公交乘客的设备现在可以获取信标广播的 URL 并在浏览器中显示它(3)。

在 JavaScript 和硬件的所有结合中,这种变体——即网络与硬件的更深层次集成——是最不稳定的。它既引人入胜又不可预测。很可能会对使用 JavaScript 构建物联网产品的更多方式的需求导致这个领域令人头晕目眩的加速。

1.3. JavaScript 是否适合硬件?

因此,也许我们可以用 JavaScript 以各种方式对硬件进行黑客攻击,但我们真的应该这样做吗?这里是否有实用性,还是仅仅是一种自我放纵的客厅把戏?

几年前,当使用 JavaScript 与硬件结合的想法首次浮出水面时,并没有得到普遍的热情。有些人认为这是一种任意且不当的聪明,是一种我们真的需要在每个地方都使用 JavaScript 吗?的疲惫感。其他人则认为,在受限硬件上 JavaScript 的性能永远不会被接受用于任何非爱好用途。一些老派的顽固态度浮出水面,评论线程被充满激情的抨击所淹没,抨击的对象不是 C/C++以外的任何东西,而且持怀疑态度的人警告说,高级语言会掩盖新手对底层硬件细微之处的理解。

然而,仍有很多人保持开放的心态。为什么使用 JavaScript,当 C/C++已经足够好吗?这个问题在硬件领域早期范式转变中有一个奇怪的回声:为什么使用 C,当汇编语言已经足够好吗?

无论它是否令人惊叹,还是令人失望——我们现在不会争论这一点——JavaScript 是互联网的事实上的编程语言。人们都知道它,人们在使用它,它无处不在。JavaScript 的普遍性赋予它独特的潜力,成为数百万想要开始物联网开发的网络开发者的门户。

JavaScript 编程的某些方面非常适合硬件,特别是它在事件处理和异步处理方面的熟练程度。JavaScript 也是原型设计的良好工具,对于快速迭代来说是一个福音。

看到我们最终会走到哪里将会非常有趣。JavaScript 列车正从硬件站驶出,很多人都在跳上这趟列车。

1.4. 组装硬件工具包

您已经快速浏览了构成嵌入式系统的成分以及将硬件与 JavaScript 结合的方法。现在,让我们更具体地了解制作这些类型项目所需的物理硬件、配件和工具。然后我们将准备好一个基本的工具包,让您开始。

我们的项目将结合开发板和输入输出硬件。为了构建电路和连接系统,你需要支持电子组件、电线、电源和配件。再加上一些基本工具,你就可以出发了。

1.4.1. 开发板

开发板,也称为 原型板 或简称为 ,是结合微控制器或其他处理组件以及有用支持功能的物理开发平台(图 1.19)。它们是硬件黑客生活方式的基石。板的价格从几美元到高端 SBC 的 100 多美元不等。

图 1.19. 一些典型的基于微控制器的开发板,从左上角顺时针方向:德州仪器的 Tiva C-Series LaunchPad,Arduino Uno R3,Adafruit Trinket(5V 型号)和 Particle Photon

图片

板的核心是其大脑,即处理器、内存和 I/O 的组合。8 位或 16 位微控制器是像(大多数)Arduino 这样的简单、入门级原型板的中心(图 1.20)。具有更复杂 32 位微控制器的板可能能够运行嵌入式 JavaScript。

图 1.20. 这块 Arduino Uno 板由 8 位微控制器 AVR ATmega 328-P 驱动。

图片

并非所有板都是基于微控制器的。更强大的 SBC 由你通常在计算机主板上找到的组件供电。这些板的架构相应地更复杂,涉及一个或多个小型化系统芯片(SoC)和额外的互连,如 HDMI、音频或以太网。尽管 SBC 可能具有板上的物理 I/O 接口——例如 Raspberry Pi 就有——但它们的通用处理器同样可以用于非硬件中心的项目。

1.4.2. 输入和输出组件

哇,你可以连接到你的板上的传感器和设备真是太多了!这很有趣,但一开始可能会感到不知所措。有很多技术术语被提及,还有很多数字、值和规格需要吸收。随着你阅读这本书,你会学会找到自己的方向。

我们将要使用的输入和输出组件设计简单,可以直接插入面包板(也就是说,它们是 面包板友好型)。一些组件封装为 分线板。与开发板通过将微控制器的微小引脚连接到更方便的连接来简化 I/O 一样,分线板通过将它们的引脚连接到更方便的连接来简化与单用途传感器或输出设备的工作(图 1.21)。

图 1.21. 常见的输入和输出组件集合

图片

1.4.3. 其他电子组件

组装电子电路需要一系列支持电子组件。

虽然可能感觉有很多小零件,但像电阻器、电容器、二极管和晶体管这样的基本元件价格低廉,并且可以购买方便的入门套件(图 1.22)。我们将花时间来了解这些部件——很快它们就会像老朋友一样。

图 1.22. 这些常见的元件将帮助你构建功能性的电子电路。

1.4.4. 电源、电线和配件

你很快就会意识到,有好多方法可以为项目供电!

开发板可以通过 USB 供电,或者通过插入直流适配器(壁挂式适配器)来供电。在许多情况下,其他项目组件可以利用相同的电源(图 1.23)。

图 1.23. 一些电源和电路的电线和配件样本

电池对于使项目无电线以及在不同电压下提供额外电源很有用。有许多种电池夹具和夹持器,可以将电池连接到项目中。

为了连接东西,你需要电线。跳接线是预先切割的电线。一种特别方便的类型在每个端都有针脚,可以轻松滑入面包板和许多板上的 I/O 针脚。跳接线非常适合快速原型设计。另外,连接线通常卷在卷轴上,可以根据需要切割到特定长度。

1.4.5. 工具

一对尖嘴钳和一把精密螺丝刀在构建项目时非常有用。如果你需要切割或剥除连接线(预先切割的跳接线不需要切割或剥除),你将需要一把电线剥皮器——通常内置电线切割器。随着你的进步,你可能想拥有一台万用表,这是一种测量电压、电流和电阻的工具。

存储你的电子元件

当你开始构建项目时,你将会有很多小零件。你可以在五金店和爱好商店找到分格的存储盒或抽屉单元。专为鱼饵设计的盒子可以特别方便地用作电子元件的容器,因为它们的隔间很小,分隔板紧密贴合(图 1.24)。

图 1.24. 这个紧凑的鱼饵箱顶部有空间放置工具,底部有堆叠式、可拆卸容器用于存储元件。

是时候开始我们的旅程了。使用低功耗嵌入式硬件进行黑客攻击可以很有趣、富有创意和令人兴奋——它在商业世界中也越来越有用。网络开发者(就像你一样)已经拥有了可以在你的道路上成为很好的垫脚石的技术。你可以使用无处不在的网页语言 JavaScript 来开始你的旅程,并减少路边的干扰。

在我们的冒险中,您将获得理解使电子电路工作的基本关系的基本理解。不涉及数学?别担心,我也不。您将在旅途中遇到一些有用的角色:组件和模块、不同类型的板和软件。我们将尝试不同的组合,并学习如何在 LED 烧毁时擦干自己并再次尝试。

路途永无止境,视野无限。我们可能无法全部访问,但到这本书的结尾,您将准备好评估和使用尚未出现的未来技术。在您旅行的中途,您出发的道路可能已经发生了显著变化。但通过依赖一些常数作为您的指南针——硬件基础、JavaScript 的应用、网络技术——您将能够找到自己的路。

摘要

  • 从零开始学习嵌入式电子爱好可能会感到令人生畏,但您现有的 JavaScript 技能可以给您带来帮助。

  • 嵌入式系统将一个大脑——微控制器或低功耗处理器——与输入和输出结合在一个小封装中。

  • 微控制器将处理器、内存和 I/O 集成在一个芯片中。定义微控制器行为的逻辑——固件——通常被烧录到 MCU 的程序内存中。

  • 有几种方式 JavaScript 可以控制硬件:主机-客户端、嵌入式 JavaScript、SBC 上的 Node.js,甚至从浏览器内部。

  • 在主机-客户端设置中,Node.js 在主机计算机上执行,并通过消息协议(API)与微控制器交换指令和数据。该项目没有主机计算机就无法运行。

  • 一些受限制的微控制器被优化以直接在芯片上运行 JavaScript(或 JavaScript 的一个子集)(嵌入式 JavaScript)。

  • 单板计算机(SBCs)具有更复杂的处理器和附加功能,如 USB 端口或音频连接。这些设备通常可以运行完整的操作系统,并且通常表现得像小型计算机。许多设备允许您使用 Python、C++或 JavaScript 等高级语言控制 I/O 和行为。

  • 开发板是结合微控制器(或其他处理组件)和实用支持功能的平台。它们提供了方便的 I/O 引脚连接,允许快速原型设计项目。

  • 构建项目涉及一定数量的电子设备:开发板、输入和输出组件、基本电子元件如电阻和二极管、电源连接和基本工具。

第二章. 使用 Arduino 开始硬件之旅

本章涵盖

  • Arduino 是什么以及 Arduino Uno R3 开发板的功能

  • 将组件和电源连接到 Arduino Uno

  • 使用 Arduino IDE 编写和上传代码以使 LED 闪烁

  • 使用 Firmata 固件和 Johnny-Five Node.js 框架在主机-客户端设置中配置 Arduino Uno

  • 使用 JavaScript 控制 Arduino Uno 并使 LED 闪烁

Arduino. 它是一家公司。它是一个项目。它是硬件。它是一个用户社区。Arduino 是,嗯,它就是 Arduino,一个结合开源硬件和软件的广泛概念,旨在使初学者构建交互式设备变得容易(且成本低廉)。

与大多数开发板一样,Arduino 开发板具有微处理器、I/O 引脚、电源连接和其他标准功能。目前有大约十几种板型,包括图 2.1 中所示的 Uno。figure 2.1。每个 Arduino 开发板都有标准尺寸和布局,以便使用模块化 盾牌。盾牌是根据 Arduino 的形状制造的,提供额外的功能——如 WiFi 或 GPS——而这些功能本身并不由板子提供。(分线板是扩展开发板功能的另一种方式,但盾牌是专门针对 Arduino 的外形设计的。)

图 2.1. Arduino Uno 是 Arduino 最受欢迎的板型,我们将在接下来的几章中对其进行探讨。

Arduino:名字的含义是什么?

尽管所有 Arduino 硬件(和软件)都是开源的,这意味着你可以轻松地获得原理图,甚至可以不费太多力气就构建自己的板子,但只有由 Arduino 公司制造的“官方”板子才使用“Arduino”这个名字进行市场推广。

Arduino 兼容板 这个术语描述的是按照与官方 Arduino 板相同的设计规范制造的板子,但并不一定是由 Arduino(公司)生产的。

Genuino 是用于在美国以外市场销售的 Arduino 开发板的品牌名称。

几种产品,如 pcDuino 和 Netduino,使用 -duino 后缀来暗示它们的 Arduino 类似特性,并且两者都具备使用 Arduino 兼容盾牌的形态。pcDuino 允许你使用 Arduino 编程语言进行编程(尽管它也支持高级语言)。

| |

对于本章,你需要以下内容:

  • 1 Arduino Uno 版本 3 (R3)

  • 1 USB 线缆(USB A 到 USB B)

  • 1 标准 LED(任何颜色)

通常,编程 Arduino 是通过在 Arduino 的跨平台 IDE(集成开发环境)中编写 sketches(代码片段)来完成的,并将编译后的代码上传到板子的微控制器。sketch 中的代码是用 Arduino 编程语言编写的,这种语言类似于 C++,但增加了额外的硬件控制功能。IDE 负责编译代码并将其发送到板子,通常是通过 USB 连接。

Arduino 硬件非常受欢迎、价格低廉且经过良好测试。配置 Arduino 以使用允许你使用 Johnny-Five Node.js 框架控制的固件非常简单,因此它成为结合 JavaScript 和硬件的理想起点。

2.1. 了解 Arduino Uno

在所有 Arduino 板型中,Arduino Uno 是最受欢迎的。它已经在世界各地被硬件新手们测试过,不应该给我们带来任何大的惊喜。它是可靠且普遍的。

在 第一章 中,您看到了一些开发板的常见功能,如微控制器、I/O 引脚和电源连接。图 2.2 展示了这些以及其他 Arduino Uno 板的主要功能:

图 2.2. Arduino Uno 的主要部件

![02fig02_alt.jpg]

  • 微控制器 Uno 的 Atmel ATmega328P 微控制器具有 8 位处理器和 32 KB 的闪存来存储程序。记住,微控制器将处理器、内存和 I/O 处理能力结合到一个单一包中。

  • 编程和通信连接 USB 连接器允许您将 Uno 连接到您的计算机。您可以通过此连接将程序上传到 Uno,您还可以使用 USB 为电路板供电。

  • 数字 I/O 引脚 在 Uno 的 I/O 引脚中,有 14 个是 数字引脚,可以用作输入或输出。

  • 电源引脚 Uno 上的几个引脚提供了稳定的电源和地之间的访问。您可以使用这些引脚为您的项目供电。

  • 模拟输入引脚 Uno 的 6 个引脚能够处理 模拟输入。数字引脚只能读取或产生两种状态(高或低,稍后会有更多介绍),但模拟输入引脚使用 Uno 板上的 模拟-数字转换器 (ADC) 将模拟输入——不同的电压——转换为 0 到 1,023 之间的值。这对于从传感器获取数据很有用。模拟引脚也可以用作数字引脚。

  • 直流 (直流) 连接器 如果您不是通过 USB 为电路板供电,Uno 的直流柱状插头允许您将电路板插入直流电源适配器或其他直流电源。

  • 关于光 当电路板通电时,小型的 ON LED 灯会亮起。

  • 复位按钮 复位按钮会重新启动微控制器的固件,并将 Uno 的引脚恢复到默认水平,这大致相当于“重启”电路板。

2.1.1. 使用 Arduino Uno 创建您的第一个电路

在软件传统中,Hello World 程序是学习新语言或系统时的常见第一个练习——通常这些程序做一些微不足道的事情,比如将“Hello, world”打印到屏幕上。

使用 LED 的基本技巧是硬件黑客的“Hello World”。让一个 LED 点亮并闪烁已经成为众多硬件新手进入电子领域的第一步。

要点亮一个 LED,您需要构建一个 电路。电路提供了一条从电源到地之间的不间断路径,电子可以沿着这条路径移动。在这条路径上,电子可能会遇到并穿过组件,导致一些有趣的事情发生,比如点亮一个 LED。

从电源到地之间的任何间隙都会毁掉整个电路——电子无处可去。要点亮 LED,你需要通过填补电源和地之间的断开来完成电路。

要构建你的第一个电路,你需要

  • 1 个 Arduino Uno

  • 1 根 USB 线,用于将 Uno 连接到电脑的 USB 端口

  • 1 个标准 LED(任何颜色)

Arduino 板需要供电才能工作。为 Arduino 板供电有几种方法,但目前最简单的方法是将它连接到电脑的 USB 端口。使用随板子包装的电缆将 Arduino 插入 USB。如图 2.3 图所示的 ON LED 应该会亮起。

图 2.3. 当 Uno 有电时,ON LED 会亮起。

当板子接收到电源时,板上的几个电源引脚(图 2.4)会变得活跃。当活跃时,它们提供电压——即稳定的低压电源源——如果你想要的话(我们确实想要!),你可以将组件连接到它们。

图 2.4. Arduino Uno 的一些电源引脚。这些引脚在板子有电时都会供电。

你将在第三章中了解更多关于电压和功率的知识,但图 2.4 提供了对 Uno 一些基本电源引脚的快速查看。

创建 LED 电路

如图 2.5 所示的 LED 是发光二极管——一种在电流通过时将部分能量以光的形式散发的二极管。

图 2.5. 标准 LED 有一个阳极(正极)和一个阴极(负极)。通常,阳极比阴极长,这使得识别更容易。许多 LED 在负极(阴极)侧也有一个平坦的区域。

二极管 是一种只允许电流单向流动的电子元件——二极管不允许电流反向通过。正因为如此,在尝试将二极管插入任何东西之前,识别任何类型二极管的正极(阳极)和负极(阴极)非常重要。

Uno 上的开/关指示灯,如图 2.5 所示,也是一个 LED——一个表面贴装 LED。表面贴装元件常用于大量生产的机器制造板。它们需要焊接,并且它们微小的尺寸使得在手工项目中使用它们变得很棘手。相反,我们将坚持使用更大的、通孔元件(它们有适合通过的引脚、面包板或其他电路板的引线),就像图 2.5 中的 LED 一样。

断开连接!

在连接组件之前,务必确保将开发板从电源断开。如果不这样做,可能会损坏组件或板子。

现在你可以构建电路来点亮 LED 了。在继续之前,请确保你已经将 Arduino Uno 从 USB 或其他电源断开。将 LED 连接到 Arduino:

  1. 定位你的 LED 的阴极(负极)。如果需要帮助找到它,请参阅图 2.5。将 LED 的阴极引脚插入到板上标记为 GND 的电源部分中的一个引脚,如图图 2.6 所示。

    图 2.6. 使用 Arduino 点亮 LED。通过 LED 将电源(3.3 V)连接到地(GND)完成电路,消除任何间隙,允许电子流动。

  2. 定位 LED 的阳极(正极)并将其插入到电源部分标记为 3.3 V 的引脚。

将 LED 定位并连接好之后,用 USB 将 Arduino 连接到你的电脑上,以便它有电源。

哇!你使用了 LED 的引脚将电源连接到地,创建了一个电路并点亮了 LED(图 2.6)。

电压和 Arduino Uno

Arduino Uno 的典型工作电压是 5 V,你将在接下来的几章中构建的项目将主要涉及基于这种电压的电路。这可能会让你好奇为什么这个例子中的 LED 连接到 3.3 V 电源。直接将这种 LED 插入 5 V 的 Arduino 电源引脚会给它提供太多的能量——这并不特别危险,但你可能会烧毁你的 LED。随着我们的深入,你将了解更多关于这种事情的做法和原因,我将停止使用像能量这样的松散术语。

在未来的项目中,我们将使用电阻,这是一种抵抗电流流动的电子元件,将电路中的电流降低到对 LED 和其他元件不那么“粗暴”的水平。目前,我们将使用 3.3 V 电压,在 Arduino 的电流下,这对你的 LED 来说不会那么“粗暴”。

话虽如此,长时间不使用限流电阻给 LED 供电绝不是什么好主意,所以不要让你的 Arduino-LED 配置长时间插着电(你可能注意到 LED 亮的时间越长,触摸起来越热)。

通过将 3.3 V 引脚的电源通过 LED 连接到地,你完成了一个电路(图 2.7)。电源能够通过这个电路流动,并且作为一个很好的副作用,它使你的 LED 发光。

图 2.7. 基本 LED 电路的示意图。在传统的电路表示方法中,电源从正极流向负极。电源流入 LED 的正极(阳极)并从负极(阴极)流出。

你可以用任何低电压电源,如电池,构建基本的 LED 电路。这里没有逻辑问题,所以你实际上不需要开发板(尽管 Arduino 的稳定电源很方便)。但是,当你用 Arduino 编程做不同的事情时,事情就变得更有趣了——现在我们就试试看。

2.2. 使用 Arduino 工作流程

在我们将 JavaScript 引入 Arduino 混合之前,让我们使用典型的 Arduino 工作流程让你的 LED 闪烁。在我们在 Arduino 上堆叠一层 JavaScript 抽象之前,看看 Arduino 在原始形式下的工作方式是有帮助的。

2.2.1. Arduino Uno 的数字引脚

没有让 LED 闪烁的 Hello World 情况是不完整的。

但你不能使用之前练习中连接的引脚来使 LED 闪烁。3.3 V 电源引脚在板本身供电时始终会供电——LED 始终会亮,因为电路始终是完整的。我们真正需要做的是控制连接到 Arduino 的可编程数字引脚(在 Uno 上为 0 到 13 号引脚)的 LED——(图 2.8)。

图 2.8. Arduino Uno 的数字引脚

配置的数字引脚可以有两种状态之一:高电平或低电平。这种二进制逻辑可能对你这个程序员来说很熟悉——真/假、1/0、是的/不是的二分法是软件开发和数字架构的一般性主题。

在硬件层面,高低逻辑对应于电压。当数字引脚被配置为输出时,高电平状态意味着引脚正在提供电压,因此该引脚上有电源施加(在 Uno 的情况下,是 5 V 或接近 5 V)——它实际上是开启的。低电平意味着引脚上的电压在或接近 0 V——引脚实际上是关闭的。

将配置为输出的数字引脚编程为在高低电平之间循环,这是使连接的 LED 闪烁的关键。

数字输入

当一个数字引脚被配置为输入引脚时,高电平和低电平状态也在发挥作用,但方式稍微细腻一些。关于这一点,我们将在后面的章节中讨论数字传感器。

2.2.2. 草图和 Arduino IDE

要配置和编程 Uno 上的数字引脚,你需要编写一个草图并将其通过 Arduino IDE 上传到板子上。

访问 Arduino 软件页面(www.arduino.cc/en/Main/Software),下载适用于你的操作系统平台的最新版本的 Arduino IDE。它是免费的,支持 Windows、Mac OS 和 Linux。安装软件后,启动 IDE。你会看到类似于图 2.9 的内容。

图 2.9. 第一次启动 Arduino IDE 时,它将为你创建一个新的空草图。

什么是草图?

草图这个词只是代码程序的另一种说法。Arduino 将你的草图保存在所谓的草图簿(基本上是一个文件夹)。草图这个术语可以追溯到processing编程语言及其 IDE,Arduino 的 IDE 就是从那里继承下来的。

在 Arduino 编程语言的硬件支持函数库中,也可以看到 Arduino 血统的痕迹,这些函数是从wiring开发平台派生出来的,而wiring平台本身又是 processing 的一个分支。

Arduino 编程语言

在 Arduino IDE 中,你可以使用 Arduino 编程语言为你的 Arduino Uno(或其他 Arduino 兼容板)编写草图。让我们看看一些代码的示例片段,这些代码与我们将要使用的 JavaScript 看起来相当不同。

要使数字引脚准备好支持 LED,你需要在 Arduino 草图中将它配置为输出引脚——毕竟,LED 是一个输出组件。在 Arduino 编程语言中,将数字引脚配置为输出引脚看起来如下。

列表 2.1. 配置数字输出引脚的示例
pinMode(12, OUTPUT);                   *1*
  • 1 将(数字)引脚 12 配置为输出

要编程输出引脚,你可以使用 Arduino 编程语言内置的digitalWrite函数将其设置为HIGHLOW,如下一列表所示。

列表 2.2. 设置数字输出引脚
digitalWrite(12, HIGH);           *1*
digitalWrite(12, LOW);            *2*
  • 1 将引脚设置为高电平会给引脚施加电压(将其“打开”)

  • 2 将引脚设置为低电平会将电压降低(将其“关闭”)

2.2.3. 将 LED 连接到数字引脚

从上一个练习中取下 LED,将其阳极连接到 Uno 的 12 号引脚,将其阴极连接到附近的 GND 引脚。然后,从草图中将 12 号引脚设置为HIGH会提供电压并打开 LED,而将其设置为LOW则会关闭 LED。

但是你不应该这样做。记住,当设置为高电平时,数字引脚的输出电压是 5V。我们目前还没有使用电阻来管理电压,引脚的 5V 输出可能会压垮 LED 并可能烧毁它(图 2.10)。

图 2.10. 直接将 LED 插入 5V Uno 数字 I/O 引脚对 LED 来说压力很大。

幸运的是,Arduino Uno 在引脚 13 上有一个内置的 LED(图 2.11)。非常方便!每当数字引脚 13 被设置为HIGH时,板载 LED 就会亮起。对于这个实验,你不需要连接任何线路——相反,让我们专注于编程部分。

图 2.11. 当引脚 13 被设置为HIGH时,紧邻引脚 13 的小型表面贴装 LED(橙色)会亮起。

2.2.4. 编程 LED 闪烁

是时候将这些内容整合到一个草图中来控制 Arduino Uno 了。

Arduino 草图分为两部分:

  • setup——这是你可以放置草图设置代码的地方——我敢打赌你已经猜到了。这段代码在程序执行开始时只执行一次。为了设置 LED 闪烁草图,setup需要配置连接到 LED 的引脚(13)。即使你不会将外部组件物理连接到这个引脚,你仍然需要将其配置为数字输出引脚。

  • loop——在设置完成后,loop中的代码会反复执行,直到 Arduino 断电或重置。这部分草图需要交替设置引脚 13 为HIGH(施加 5V 电压)和LOW(无电压),以便 LED 闪烁。

在 Arduino IDE 中,创建一个新的草图来保存闪烁板载 LED 的代码,如下一列表所示。

列表 2.3. 闪烁 LED 的完整草图
void setup() {
  pinMode(13, OUTPUT);            *1*
}
void loop() {
  digitalWrite(13, HIGH);         *2*
  delay(500);                     *3*
  digitalWrite(13, LOW);          *4*
  delay(500);
}
  • 1 将引脚 13(带有内置 LED)配置为输出

  • 2 将引脚 13 设置为高电平以打开 LED

  • 3 等待 500 毫秒(半秒)

  • 4 将引脚 13 设置为 LOW 以关闭 LED

如果 Arduino 编程语言看起来像 C/C++,那你是对的,它基本上就是,除了少了几个类型和特性。Arduino 语言还提供了一个内置的硬件特定函数库,包括pinModedigitalWrite

示例草图

如果你不想将闪烁草图输入到 Arduino IDE 中,或者你只是想看到更多的示例草图,你可以在 IDE 中的文件>示例下找到一个类似的闪烁草图和更多其他草图。

上传闪烁草图

现在将你的 Arduino Uno 连接到电脑上的 USB 端口。从 IDE 中,你可以在草图窗口中点击 Verify 按钮(图 2.12)以确保你的代码没有错误,或者直接点击 Upload 按钮,它将验证、编译并将你的草图上传到板上的微控制器。如果你还没有保存草图,当你上传时,系统会提示你保存。

图 2.12. Arduino IDE 中的草图窗口

图片

如果一切顺利,你应该看到板载 LED 每 500 毫秒闪烁一次(图 2.13)。

图 2.13. 上传的草图应该使板载 LED 每 500 毫秒闪烁一次。

图片

虽然 Arduino 编程语言的语法比 JavaScript 低级,但它仍然是一种高级语言。你的草图内容首先被编译成微控制器可以原生执行的机器代码。然后,编译后的代码被写入微控制器上的非易失性程序内存。Uno 的微控制器将程序存储在闪存中,这是在存储卡和固态驱动器中常用的一种技术。

就像从数码相机中移除存储卡时照片不会消失一样,即使微控制器失去电源,草图也会保留在微控制器上。将此与易失性内存(如 RAM)进行比较,如果不供电,它会丢失其内容。你成功上传的草图将保留,直到被其他东西替换。

以这种方式永久存储在非易失性内存中的程序被称为固件。每次你将新的草图上传到 Arduino 时,你都在替换其固件。固件在你从 USB 连接中拔掉板子后不会“消失”。如果你将板子重新插入 USB,或者插入另一个电源,LED 将再次开始闪烁,因为固件仍然完好无损。

重置 Arduino 板

当 Arduino 在断开连接后恢复电源时,它将重置,将引脚恢复到默认行为,并重新启动微控制器的固件。程序内存中固件的执行将从开始处开始。从 Arduino 草图的角度来看,这意味着setup代码将再次运行。如果开发板上有什么东西运行失控,重置可以是一种拯救理智的策略。

你不需要从电源上拔下 Arduino Uno 板来重置它。你可以使用方便的板载 RESET 按钮——按住并保持一段时间——来完成相同的事情。

现在,你已经了解了 Arduino Uno 的基本布局,并学习了使用 IDE 编写和上传草图的基础知识。是时候学习 JavaScript 了!

2.3. 使用 JavaScript 控制 Arduino

使用 JavaScript 和主机-客户端配置与 Arduino 一起工作的流程与默认的 Arduino 工作流程不同。你将不再使用 Arduino IDE 编写和反复上传草图,而是最初上传一个草图,该草图将作为板子的固件保留在位。

之后,你将不再使用 Arduino IDE——相反,你将使用你选择的文本编辑器编写 JavaScript 代码,并在终端中使用node执行它。

这些是设置 Uno 的主机-客户端配置的步骤:

  1. 将包含兼容固件的草图上传到板子。

  2. 在你的计算机上安装 Johnny-Five Node.js 框架。

2.3.1. 将 Arduino 配置为客户端

回想一下第一章,主机-客户端方法涉及主机和客户端之间通过一个公共 API 进行通信。你将在接下来的几章中使用的 Node.js 框架——Johnny-Five——默认情况下使用名为 Firmata 的协议与板子通信。

Firmata 允许主机(计算机)和客户端(微控制器)以基于 MIDI 消息的格式相互交换消息。Firmata 协议指定了这些命令和数据消息应该是什么样子。Arduino 对 Firmata 的实现提供了你可以放在板子上的实际固件,使其“说”Firmata。它以 Arduino 草图的形式存在,你需要将其上传到板子(图 2.14)。

图 2.14。配置 Uno 的主机-客户端设置的第一步是上传一个 Firmata 草图,这将允许板子使用 Firmata 协议进行通信。

02fig14_alt.jpg

好消息:Firmata 足够流行,以至于你需要的 Firmata 草图都包含在 Arduino IDE 中。

将 Firmata 上传到 Uno

按照以下步骤上传正确的 Firmata 版本到你的 Uno,使其可以作为主机-客户端设置中的客户端使用:

  1. 将你的 Arduino Uno 连接到 USB。

  2. 启动 Arduino IDE。

  3. 访问 File > Examples > Firmata 菜单,并从列表中选择 StandardFirmataPlus(图 2.15)。

    图 2.15。从可用的示例草图中选择 StandardFirmataPlus

    02fig15_alt.jpg

  4. 通过点击上传图标将草图发送到 Uno。

为什么有这么多 Firmata?

Arduino IDE 中的 Examples > Firmata 菜单提供了相当丰富的 Firmata 草图选项。

这与新车上可选功能包有些类似。客户可以预先订购一辆带有特定组合选项的汽车或卡车——天窗、高级音响、地板垫——但你不能在单一车辆中组合所有选项。没有足够的空间放置三种类型的地板垫,而且只有少数消费者对最奢华的性能悬挂系统感兴趣。

同样,不同的 Firmata 草图实现了不同的功能组合,针对不同的 Arduino 硬件或用例进行了定制。StandardFirmataEthernet 添加了对以太网盾的支持。AnalogFirmata 尝试最大化同时可以通信的模拟输入数量。StandardFirmataPlus 是所有最受欢迎功能的一个很好的平衡,这就是我们将要使用的。

当你使用 Arduino IDE 制作闪烁 LED 时,你上传了一个草图,该草图成为板子的固件。这个固件是单功能的——它闪烁一个 LED。

相比之下,Firmata 固件没有特定的用途。相反,它是一个程序,允许板子与你要安装的 Node.js 框架进行通信。

你暂时完成了 Arduino IDE 的使用——上传 Firmata 草图后,你可以退出程序。

2.3.2. 安装 Johnny-Five Node.js 框架

Johnny-Five 是由 Bocoup 为控制主从配置的开发板创建的开源 Node.js 框架 (johnny-five.io)。Johnny-Five 特别注重机器人技术,但你可以用这个软件做很多事情。它比大多数硬件 JavaScript 框架存在的时间更长,具有清晰的 API 和一定的成熟度——这两者对于硬件初学者来说都是理想的选择。

要创建你的第一个 Johnny-Five 项目,为它创建一个目录并安装框架 npm 包,如下所示。

列表 2.4. 第一个 Johnny-Five 项目的准备
$ mkdir hello-world
$ cd hello-world
$ npm install johnny-five

安装 Johnny-Five:获取帮助

通常,Johnny-Five 只需一个 npm install 命令就可以愉快地安装,但安装过程确实会编译一些原生扩展,这有时可能会引起麻烦。

如果你在使用 Windows 时遇到问题,请尝试执行以下命令:

npm install johnny-five --msvs_version=2012

如果你遇到问题,这里有一些其他资源:

2.3.3. 使用 Johnny-Five 的 Hello World 闪烁 LED

你已经看到了在 Arduino 编程语言中闪烁 LED 脚本的样子。现在是时候编写一个 JavaScript 版本的 Hello World 闪烁 LED,以便了解 Johnny-Five。

在你安装 Johnny-Five 的 hello-world 目录中,创建一个名为 hello.js 的文件,并用你选择的文本编辑器打开它。在这个 JavaScript 文件中,你可以编写控制你的 Arduino Uno 的代码,如下一个列表所示。

列表 2.5. hello.js
const five = require('johnny-five');         *1*
const board = new five.Board();              *2*

board.on('ready', () => {                    *3*
  const led = new five.Led(13);              *4*
  led.blink(500);                            *5*
});
  • 1 需要 johnny-five 包

  • 2 初始化一个代表你的 Uno 的新 Board 对象

  • 3 等待板子触发 ready 事件

  • 4 在 13 号引脚(Uno 的内置 LED 引脚)上实例化一个 LED 对象

  • 5 使 LED 每 500 毫秒闪烁一次

保存文件。如果你的 Uno 还没有连接到电脑的 USB 端口,请将其插入。在终端中,使用cd命令切换到项目目录,并运行以下命令:

node hello.js

你会在终端中看到一些类似以下的内容。

列表 2.6。在终端中运行 hello.js
$ node hello.js
1457806454739 Device(s) /dev/cu.usbmodem1421
1457806454752 Connected /dev/cu.usbmodem1421
1457806458536 Repl Initialized

内置 LED 闪烁?就这样!你已经用 JavaScript 控制了 Arduino Uno。

2.3.4。Firmata、Johnny-Five 和主机-客户端方法

当脚本运行时,你会看到内置的 TX(发送)和 RX(接收)LED——在图 2.16 中标识——闪烁。这是因为主机和客户端正在使用 Firmata 协议交换串行消息。

图 2.16。当主脚本执行时,Uno 的 TX 和 RX(发送和接收)LED 将闪烁,因为主机和客户端之间正在交换通信。

要停止你的 Johnny-Five hello.js 程序的执行,在终端窗口中连续两次按 Ctrl-C。看看你的 Uno。根据你退出程序的时刻,13 号引脚的 LED 可能是关闭的,或者它可能持续开启。在任何情况下,它都不会再闪烁。

这是因为主机计算机上的程序已经停止执行,因此它已经停止向 Firmata 固件发送消息。板子处于表示程序成功通信的最后一条消息的状态。

现在 hello.js 脚本不再运行,按下并保持 Uno 的 RESET 按钮一会儿。Uno 的 13 号引脚 LED 将关闭并保持关闭。

回想一下,当重置时,Arduino Uno 会重置其引脚并重新启动固件程序。早些时候,当你的固件是从 Arduino IDE 上传的闪烁 LED 草图时,闪烁会继续。现在固件是 Firmata,它不会自己使 LED 闪烁,而是等待关于要做什么的指令。13 号引脚上的 LED 由引脚的硬件重置关闭,没有任何东西告诉它再次开始闪烁。

在主机-客户端方法中,客户端本身不执行任何操作——它必须持续由主机指导。在这个设置中,Uno 客户端就像一个木偶。它需要来自操纵者的持续输入——我们的主机执行的 JavaScript。

2.3.5。使用 Johnny-Five 结构化脚本

使用 Arduino IDE 编写的 Arduino 编程语言的 sketch 结构不同于 Johnny-Five 脚本的结构。

在 Arduino 编程语言中,你将你的代码分成setuplooploop中的代码会持续执行,直到板子被重置或固件被替换。为了使 LED 永远闪烁,你编写了以下代码。

列表 2.7。使用 Arduino 编程语言实现的 LED 闪烁
void loop() {
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
}

每次循环中的代码执行时,它会将引脚 13 设置为HIGH,等待 500 毫秒,然后将其设置为LOW,并再次等待 500 毫秒。然后循环再次执行(一次又一次……)。向引脚 13 提供的交替HIGHLOW电压,持续时间为 500 毫秒,使得 LED 看起来在闪烁。

然而,使用 Johnny-Five 你编写了以下代码。

列表 2.8. 使用 Johnny-Five 闪烁 LED
board.on('ready', () => {
  const led = new five.Led(13);
  led.blink(500);
});

板只触发一次ready事件(不用担心;即使在该事件触发后绑定到ready事件,它仍然会调用事件处理函数)。这意味着你传递的处理函数也只会执行一次——它不是一个循环。

另一个区别是 Johnny-Five 提供了一个更高级的 API。使用 Johnny-Five,你初始化一个代表 LED 组件的对象,而不是将引脚配置为通用数字输出。这让你可以访问Led类上的一些方便的方法和属性,包括blinkonofftoggle方法,以及你稍后将会遇到的更多有趣的方法。

你可以通过调用blink方法并传递一个定义闪烁周期的毫秒数的参数来代替通过设置引脚为HIGHLOW并在两者之间调用delay来操作 LED。在幕后,Johnny-Five 使用setInterval来处理闪烁的定时。LED 会闪烁,直到你告诉它停止。

适应闪烁的 LED

但我们如何让 LED 停止闪烁呢?这就是 JavaScript 隐喻开始发挥作用并为你提供与设备交互的绝佳选项的地方。在 Johnny-Five 中,Ledblink方法接受一个可选的第二个参数,即回调函数。每次 LED 闪烁开或关时,回调函数都会被调用。

列表 2.9. 在 hello.js 中使用 LED 回调
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const led = new five.Led(13);
  var blinkCount = 0;                                 *1*
  const blinkMax = 10;                                *2*

  led.blink(500, () => {                              *3*
    blinkCount++;
    console.log(`I have changed state ${blinkCount} times`);
    if (blinkCount >= blinkMax) {
      console.log('I shall stop blinking now');
      led.stop();                                     *4*
    }
  });
});
  • 1 跟踪 LED 闪烁开或关的次数

  • 2 配置 LED 总共闪烁的次数

  • 3 每次 LED 打开或关闭时,都会调用此回调函数

  • 4 当 blinkCount 达到 blinkMax 时,调用 stop 方法

重新尝试运行 hello.js:

$ node hello.js

你应该看到类似以下列表的输出。

列表 2.10. 运行修改后的 hello.js 脚本
1457808024073 Device(s) /dev/cu.usbmodem1421
1457808024079 Connected /dev/cu.usbmodem1421
1457808027867 Repl Initialized
>> I have changed state 1 times
I have changed state 2 times
I have changed state 3 times
I have changed state 4 times
I have changed state 5 times
I have changed state 6 times
I have changed state 7 times
I have changed state 8 times
I have changed state 9 times
I have changed state 10 times
I shall stop blinking now

此示例在特定次数的闪烁后停止 LED 闪烁,但你可以通过在Led.blink上使用回调执行各种其他操作。例如,你可以更改闪烁频率,或者简单地跟踪长时间内的总闪烁次数。

当你查看 Johnny-Five 代码时,它可能比低级的 Arduino 草图代码更自然、更熟悉。作为一个软件人员,硬件——特别是电子电路——可能对你来说是一个全新的世界。但有一些基础知识你需要了解。这就是我们接下来要讲的内容。

摘要

  • Arduino 板是学习硬件的常见起点,因为它们简单、广泛且经过充分测试。

  • 在典型的 Arduino 工作流程中,草图是在 Arduino 的跨平台 IDE 中编写的,并作为固件上传到板上。

  • 草图是用 Arduino 编程语言编写的,它与 C 或 C++类似。Arduino 草图由一个setup部分和一个loop部分组成。

  • 在主机-客户端设置中用 JavaScript 控制 Arduino 板,该板的微控制器需要运行能够使用与主机相同协议的固件。Firmata 就是这样一种协议,它可以作为一个草图上传到板上。

  • Johnny-Five Node.js 框架使用 Firmata 协议进行通信,但它也暴露了更高层次的 API。Led是用于控制硬件组件的 Johnny-Five 类的一个例子。

  • 用 Johnny-Five 编写的脚本可以从命令行使用node执行。主机上的代码必须持续执行并与客户端板通信——客户端不能独立运行。

  • 与 Arduino 草图中的setuploop部分相比,Johnny-Five 脚本的结构是事件驱动的,这对于 JavaScript 程序员来说是一个熟悉的设计模式。

第三章. 如何构建电路

本章涵盖

  • 使用欧姆定律在电路中操纵电压、电流和电阻

  • 在面包板上原型化基本电路

  • 并联电路和串联电路的区别

  • 关于 LED 的细节以及如何以几种有用的配置连接它们的详细信息

  • 识别和选择适合不同电路和组件的正确电阻

  • 计算串联电路和并联电路中的电阻

工具

对于本章,你需要

  • 1 个 Arduino Uno

  • 4 个标准红色 LED

  • 4 个 220V 电阻

  • 4 个 560V 电阻

  • 1 个 100V 电阻

  • 1 个按钮开关

  • 1 个 9V 电池

  • 1 个 9V 电池夹

  • 5 根红色和 2 根黑色跳线

  • 1 个半尺寸面包板

图片

设计和构建电路可能对你来说完全是新的,可能看起来令人畏惧。好消息是,只需要掌握几个核心概念。一旦你理解了电压、电流和电阻的相互作用——如欧姆定律所正式化的——你就已经走上了理解基本电路的道路。

传统上用来说明电压、电流和电阻的几个隐喻。最常用的类比是涉及储罐和管道的水压(水)系统。有效,但并不总是容易记住。让我们尝试不同的冒险。

3.1. 电压、电流和电阻

在高山深处,在一个不存在的地方的森林中,一群矮人意外地发现自己拥有无限的 jellyfish。矮人们,由于顽皮和淘气,开始寻找这些原本无动于衷的生物的幽默用途。他们发现,把 jellyfish 从悬崖上扔下去,看着它们溅入下面的湖中或弹跳到当地村庄的屋顶上,非常有趣。

附近的市民最初感到不便,但很快意识到坠落的无脊椎动物携带能量,可以是他们饼干工厂的免费能源来源——但前提是能够安全地利用这种冲击。因此,他们进行了观察,并在一段时间后开始理解和操纵电路的核心因素:电压、电流和电阻。

市民很快注意到,例如,悬崖越高越陡,扔到山谷底部湖中的 jellyfish 具有的能量就越多。较小的落差在 jellyfish 溅落时不会提供那么多的势能 (图 3.1)。

图 3.1. 高悬崖提供更多的“电压”,即电势。电压 类似于电的“压力”,推动电荷(jellyfish)从高势能位置向低势能位置移动。

电压 是两点之间势能差的测量。它类似于压力或张力或重力,因为电总是渴望从高电压移动到低电压。电压,以伏特为单位测量,是 势能,但仅凭电压,没有移动带电的电子(jellyfish),是无法造成任何破坏的 (图 3.2)。

图 3.2. 电压是势能。

要发生有趣的事情,jellyfish 需要被积极地扔过悬崖边缘,这是一个侏儒们非常乐意执行的任务。

市民们学会了通过在悬崖上标记一个位置并精确计算在一定时间内经过的 jellyfish 电流 来测量 jellyfish 电流 (图 3.3)。电流,即电荷的流动,以 安培 为单位测量,通常缩写为 amps

图 3.3. 电流,即电的流动,可以通过在定义的时间内计算通过悬崖上特定位置的电荷(jellyfish)数量来测量。

市民需要找到一种方法来管理 jellyfish 的电流,以免它压倒脆弱的饼干压机和烤箱。这是 jellyfish 电路控制的关键:电阻。电阻是材料抵抗电流流动的能力。它以 欧姆 为单位测量。

他们将 jellyfish 引导系统设计到悬崖面上 (图 3.4),将 jellyfish 流量限制在一个更合理的水平。对于靠近高悬崖(更多电压)的电路,这些系统必须更坚固,因为来自上面的 jellyfish 坠落压力巨大。

图 3.4. 市民通过将下落的 jellyfish 通过一系列管道引导来增加电路的电阻。增加电阻会降低电流。

市民发现的总结显示在 表 3.1 中。

表 3.1. 电压、电流和电阻
因素 它意味着什么 缩写为 以单位测量
电压 两个点之间电势差的差异,类似于电的“压力”。它是推动电荷通过电路的动力。 V 伏特
电流 电流:在定义的时间内,有多少电荷通过一个点。 I 安培(安培)
电阻 衡量材料抵抗电流流动的能力的测量。 R 欧姆(用 V 符号表示)

最后,镇民们完善了电路,水母帮助制作了一些周围最好的饼干。

有一个电源——一群小矮人——把水母扔过悬崖。悬崖越高,提供给电路的电压(势能)就越多。水母头部的电流(流动)朝向工厂机器。

为了将水母电流降低到可管理的水平,导流系统和管道增加了电阻

当水母将力量赋予制作饼干的机器并到达工厂的地面时,它们达到了电路中的最低势点。穿着喷气背包的小矮人像是一种泵,把疲惫的水母重新提升到悬崖上,然后再被扔下去。一次又一次... (图 3.5)。

图 3.5. 完整的小矮人和水母“电路”

电压、电流和电阻是基本电路的基本概念。下一步是了解这些因素如何相互关联,以及它们如何应用于现实世界的电路。

3.1.1. 欧姆定律

电压、电流和电阻以一致的方式相互关联。这三个因素中的每一个都像是一个杠杆:调整一个,就会影响其他。这些相互作用对镇民来说变得如此重要,以至于工厂开始生产展示这些关系的饼干(图 3.6)。

图 3.6. 镇民们的新标志性饼干显示了电压(V)、电流(I)和电阻(R)之间的关系。

拿着饼干的持有者可以咬掉他们想要确定的因子——然后看看它如何从其他两个因子中推导出来(图 3.7)。

图 3.7. 通过咬掉饼干上印有饼干食用者想要解决的因子的边缘,他们可以迅速看到他们需要解决的方程。例如,如果他们想要确定电阻(R),他们可以咬掉它,看到 R = 电压(V)除以电流(I)。

乔治·欧姆在 1820 年代就发现了电压、电阻和电流之间的这些关键关系,远在聪明的饼干镇民之前,这就是为什么欧姆定律以他的名字命名。如果你更喜欢非饼干的数学形式,这些是相关的方程:

V = I x R (voltage equals current times resistance)
I = V / R (current equals voltage divided by resistance)
R = V / I (resistance equals voltage divided by current)

“好吧,”你可能正在想,“但我如何在现实世界中应用这个?”

将欧姆定律应用于现实世界电路

设计和构建基本电路始于关键因素的正确平衡:电压、电流和电阻。

表 3.2 列举了如何在基本电路中调整电压、电流和电阻的一些常见示例。这些示例并不全面(例如,还有其他调整电路中电压的方法)但它们突出了我们在短期内将要做的事情,以确保我们的电路能够正确工作。

表 3.2. 在业余电子学中调整电压、电流和电阻
因素 关系 增加的常见方法 减少的常见方法
电压 V = IR 使用更高电压的电源。 使用更低电压的电源。
电流 I = V / R 通过移除电阻或使用电阻值较低的电阻来减少电阻。电流也可以通过提高电源电压来增加。 通过添加电阻或使用电阻值较高的电阻来增加电阻。电流也可以通过降低电源电压来减少。
电阻 R = V / I 添加电阻或使用电阻值较高的电阻。 移除电阻或使用电阻值较低的电阻。

在业余电子学黑客活动中出现的一个最常见的计算需求是:“给定一个电源 电压,我需要使用什么 电阻 来确保我的组件获得所需的 电流” (图 3.8)?

图 3.8. 一个常见的实际欧姆定律问题:在 6 V 电源电压的电路中,需要什么电阻值才能向 LED 提供 20 mA 的电流?

图片 03fig08_alt

电压通常由项目的电源——电池、USB 电源、直流适配器——定义,你知道你想要向电路中的组件提供特定的电流。因此,电压和电流被定义了,这意味着你需要求解 R,即电阻。

假设你知道你的电源电压将是 6 V,并且你有一个需要 20 mA (.02 A,或 20 千分之一安培) 电流的组件 (图 3.10)。求解 R(欧姆中的电阻)意味着将 V(伏特中的电压)除以 I(安培中的电流)因为 R = V / I:

R = 6 V / .02 A
so
R = 300 *V*
注意你的单位!

使用欧姆定律方程时,请确保保持单位的一致性。电流应始终以安培(A)为单位测量,电压以伏特(V)为单位,电阻以欧姆(Ω)为单位。伏特和欧姆通常比较直接,但当你处理业余电子学范围内的电流——通常是几十毫安——不要忘记将这些值以安培为单位表示,否则你会得到错误的结果:

300 (V) = 6 (V) / .02 (A)

but

300 (V) þ 6 (V) / 20 (mA)

好的!几乎准备好了。但在我们开始用这种新的理解拼凑电路之前,让我们成熟一点,保护自己免受一些潜在问题的困扰。

3.1.2. 问题与危险

在我们进行的电子黑客活动中,我们处理的是相当低的电压——5 V 或 3.3 V 是典型的例子——使用的是电流以十毫安(mA)计的元件。如果你做了点愚蠢的事情,这种电流和电压的组合不会把你扔到房间里(或者更糟)。但有一些事情要提防(并避免)。

避免电流过大

两个问题中的第一个出现在你向电路中的某个元件提供过多电流时(通常是通过使用过低的电阻值或完全忘记将其添加到电路中)。其中一部分能量可能被转化为可能期望的结果——例如 LED 的发光,但其余的能量也需要消耗掉。如果你向一个最大只能稳定(非闪烁)使用 20 mA 电流的 LED 提供 100 mA 的电流,那么从长远来看这不会很好。过多电流的第一个迹象通常是发热——LED 会开始变得触摸起来很热。在某个点上,它会被压垮并完全烧毁。

避免造成短路

第二个“糟糕”的情况可能更糟。如果一个电路中没有负载——也就是说,没有元件或电阻来抽取或抵抗电流——事情确实会变得很糟糕。如果有,比如说,一条直接从电池正极到负极的路径,那么电路中就没有电阻来限制电流。这是一个短路(图 3.9),并且会导致大量的电流非常快地通过电路放电。这种能量可以造成热量、火灾,甚至爆炸。

图 3.9. 短路没有负载来调节电流。短路的一个常见例子是将导线直接从电池的正极连接到负极(现实生活中不要这样做)。

图 3.9 的替代图片

开发板可以保护你免受最坏情况的影响。如果你短路了 Arduino 的电源到地(P.S.,不要这样做),你不会爆炸,因为板上的输出引脚有电流限制器(你的板可能已经烧焦了)。从其 5 V 引脚输出的最大电流是 450 mA;从单个 I/O 引脚输出的最大电流大约是 40 mA。如果你使用电池工作,你将不会得到这样的保护——你可能会引发一场令人遗憾的火花盛宴,或者更糟。所以要小心。

3.2. 构建电路

现在你已经接受了简报,让我们来实验!当你开始向电路中添加元件时,你需要一种方法来布置它们,而不会让你发疯。将电线拧在一起或在电路中用手指尝试固定几个元件是不切实际的(也许还有点风险)。

相反,面包板是搭建电路的绝佳基础。它们的作用有点像乐高基础板,提供了一个网格,可以将元件和导线插入其中。

3.2.1. 使用面包板来原型电路

用于电路原型设计的面包板是无焊点的,这意味着你可以直接将东西插入板中,而无需焊接。它们有各种形状和大小,但板上的连接方式是一致的。图 3.10 展示了一个半尺寸面包板的布局(全尺寸面包板就像两个半尺寸面包板首尾相连;它们的长度是两倍)。

图 3.10. 一个典型的面包板及其内部的连接

一个典型的面包板结合了两侧的水平端子排(一个听起来很专业的术语,实际上意味着“插入组件的位置”)和垂直电源轨(用于在电源和组件之间连接电源的孔)。每个孔在电源轨上:一个用于正极,一个用于负极。这些通常用红色(正极)和蓝色或黑色(负极)标记出来。电源轨的连接垂直贯穿整个板长。板的两侧的电源轨之间是不相连的。

端子排通常在每组的五个孔之间有一个缺口。一个十孔端子排——两组五个相连的孔通过一个缺口分隔——是一种常见的布局。五单元排中的每个孔在电上是相连的,但连接不会穿过缺口。

电源轨每行有两个孔:一个用于正极,一个用于负极。这些通常用红色(正极)和蓝色或黑色(负极)标记出来。电源轨的连接垂直贯穿整个板长。板的两侧的电源轨之间是不相连的。

让我们通过使用面包板重新布线上一章中的简单 LED 电路来感受这些连接是如何工作的。

3.2.2. 在面包板上布线简单的 LED 电路

你需要的东西

  • 1 个 Arduino Uno 和 USB 线

  • 跳线:两根红色,一根黑色

  • 1 个标准红色 LED

  • 1 220 V 电阻

首先,让我们回顾一下欧姆定律,以确定我们需要调整什么来使电路正常工作。以下是我们所知道的信息:

  • Arduino 将提供 5 V 的供电电压。

  • 我们应该通过 LED 的最大电流约为 20 mA(0.02 安培)——对于大多数标准 LED,20 mA 是一个通用的经验法则。

选择 LED 的电阻

因为我们有固定的电压和目标电流,所以可变值是电阻。需要什么电阻值才能创建电路?记住,

R = V / I

所以

R = 5 V / 0.02 A
R = 250 *V*

电阻有某些常见的电阻值,250 V 并不是常见的电阻值。计算所需的电阻值,结果发现没有这样的电阻,这种情况经常发生——不必担心。通常,经验法则是将电阻值向上取整到下一个常用电阻值(通常情况下,电阻过大比过小更安全)。

目前——相信我,我很快就会解释——我们将做相反的事情,并将电阻值向下取整到最接近的常用电阻值:220 V。

寻找合适的电阻

电阻以标准方式着色,以帮助识别。有两种条纹系统:四色电阻和五色电阻。四色电阻更为常见。

每个电阻都有代表电阻值前导数字的颜色带。四色带电阻有两个这样的颜色带,而五色带电阻有三个。

电阻的最后两个颜色带分别是其乘数带公差带

乘数带的颜色表示需要在前面数字带指示的值之后添加多少个零。换句话说,将前导数字乘以这个十的幂次给出电阻的

公差带的颜色表示电阻的准确性保证。对于我们所使用的电阻类型,+/-5% (金色)的公差是常见的。

四色带电阻有两个数字带。以下图中的四色带电阻编码如下:

  • 1 第一位数字:2 (红色)

  • 2 第二位数字:2 (红色)

  • 3 乘数:1 (棕色) = 101

  • 4 公差:+/- 5% (金色)

图片

电阻通过颜色带来表示它们的电阻和公差。

其值是

22 * 10¹ = 220 *V* @ +/-5%

图中的五色带电阻编码如下:

  • 1 第一位数字:1 (棕色)

  • 2 第二位数字:0 (黑色)

  • 3 第三位数字:0 (黑色)

  • 4 乘数:1 (棕色)

  • 5 公差:+/-5% (金色)

值是

100*10¹= 1000 *V* (or 1 k*V*) @ +/- 5%
电路图和电路图

有几种方式可以直观地表示电路,为其他构建者提供一个重建电路的“地图”。表示电路最正式的方式是通过电路图,如图 3.11 所示。电路图是一种使用标准化符号和记号的组件图形表示。它可以被看作是一种视觉上的、抽象的图形:电路图中组件的位置并不一定与它们在物理空间中的布局相对应。

图 3.11. 简单 LED 电路的电路图。电路图是电路的简洁和标准化的表示,但它们不显示如何在物理空间中定位组件。

图片

在图 3.11 中有很多物理实现电路的可能方式,因此接线图可以是一个有用的工具。图,如图 3.12(使用开源的 Fritzing 桌面应用程序创建),显示了电路的一种具体实现及其可能的布局(在这种情况下,在面包板上)。

图 3.12. 简单 LED 电路的接线图,显示了从其电路图实现电路的具体物理布局。此图是在开源的 Fritzing 桌面应用程序中创建的。

图片

电路图一开始可能会感觉数学化和抽象,但在电子社区中它们是普遍使用的。随着我们继续旅程,你会了解更多符号和惯例,它们会开始感觉更舒适。为了提高你对电路图的理解,养成将接线图与其源电路图进行比较的习惯。

正确连接:极性

LED 是极化的,这意味着它们需要以某种方式插入才能正确工作。正如你之前学到的,阳极是正极,阴极是负极。

电阻不是极化的,这意味着你插的方向无关紧要。它们以任何方向都工作得很好。

另一点要注意的是,尽管在这个练习中我会给你精确的面包板行和列坐标,但你也可以将组件插入任何行或列,只要最终连接成功(终端行水平连接;电源轨垂直连接)。

是时候构建电路了。参照图 3.12,首先将组件连接到终端行。将红色 LED 的阳极插入孔 5C(第 5 行,孔 C),其阴极插入孔 5D(下一行)。将电阻的一端插入孔 5B,靠近 LED 的阴极。

为电路供电

电路需要连接到电源。首先,你需要将终端行中的组件连接到电源轨——回想一下,终端行(出于良好原因!)与电源轨是隔离的。

正确插入:跳线颜色

如果你有一包彩色跳线,你可能想知道哪些颜色用于什么目的。按照惯例,电源连接通常使用红色(正极)和黑色(地)线。你可能看到的代表负电源连接的其他颜色包括白色或其他深色(棕色或紫色)。

绿色和黄色通常用于输入和输出连接,这些将在后面的章节中介绍。尽管使用红色和黑色/白色连接电源的广泛一致性,但不同的黑客使用不同的颜色组合来表示其他事物。简而言之:没有硬性规定,但请尽量在所实施的任何组合中保持一致性。

跳线不是极化的。它们可以任意方向连接。

使用红色跳线将孔 5B(与 LED 的阳极电连接)连接到电源轨的红色(正极)列中的孔。如果你不能决定最喜欢的孔,第四行向下应该很好。将电阻的自由端直接连接到负电源轨,如图图 3.12 所示。现在从正电源轨,通过红色电线到 LED,从 LED 出来,通过电阻回到负电源轨,形成了一条不间断的路径。

电流正在流动...现在是哪个方向?

我们使用开发板和嵌入式系统构建的项目类型使用直流(直流)电路。直流电路中的电流流向是单向的,通常表示为从正电源(最高势能)流向负极,或地(最低势能)。

从技术上讲,这并不正确——电流更准确地描述为从负极流向正极,即使是这种简化也是一种过度简化。但用正极到负极(+指向-)的方向绘制电路的传统做法已经根深蒂固,只要保持一致,用这种方式表示直流电流流动并没有固有的危害。有趣的事实:设想电流从正极流向负极的做法是由电学先驱本杰明·富兰克林确立的。

除了直流电(DC)之外,另一种电流流动类型是交流电(AC),其中电流的流动方向周期性地反转。家庭插座电源是交流电,每秒振荡方向 50 或 60 次,这取决于你所在的世界部分。

请拔掉 Arduino Uno 的电源!

无论如何,在连接元件或电路之前,请确保你的 Arduino Uno 已经从 USB 或墙式电源上拔掉。

Arduino Uno 可以从其 5V 电源引脚提供稳定的 5V 电压。使用一根红色跳线,一端插入 Arduino Uno 的 5V 引脚,另一端插入正电源轨的顶部行。从 Arduino Uno 的 GND 引脚用一根黑色跳线连接到负电源轨的顶部行。这样就完成了电路的布线。

现在将你的 Arduino Uno 插入 USB 或墙式电源。你的 LED 应该会亮起来!

电路故障排除

如果你的 LED 灯不亮,有几个地方需要检查。这种电路失败的最常见原因是它是开路,意味着从电路的正极到负极的路径中存在间隙。检查你的元件和电线位置,并确保电线牢固地插入了面包板的孔中。确保元件上暴露的金属端子没有相互接触。还要再次确认你的 Arduino Uno 的电源 LED 灯是亮的。

如果这些步骤不起作用,尝试用一个新的 LED 替换原来的 LED,以防 LED 灯已经损坏。有时,面包板的连接可能会出现问题(尽管这更常见于你已经使用很长时间的面包板)。作为最后的手段,尝试将元件连接到面包板的另一部分,或者尝试使用另一个面包板。

串联和并联电路

这个简单的 LED 电路(图 3.13)是一个串联电路。串联电路只有一条路径供电子——电流,那些侏儒世界中的流动水母——通过。因为只有一条可能的路径,所有的水母/电子都必须通过整个路径;它们不能走捷径或旁路。这意味着电路中所有点的电流都是相同的(图 3.13)。

图 3.13. 串联电路只有一条可能的路径供电荷(水母)流动——没有分支。在串联电路的任何一点,电流都是相同的,这意味着 1、2、3 和 4 点都有相同的电流。

图 3.13

当使用电阻器修改串联电路的电流时,你使用一个 200 伏的电阻器还是两个 100 伏的电阻器并不重要。电阻器的值相加并修改整个电路的电流(图 3.14)。

电压和串联电路

虽然在串联电路的任何一点上电流是相同的,但电压可能从一点到另一点不同。我们将在第四章中构建分压器时讨论这一点。

图 3.14. 串联电路中电阻器的值相加。

这些串联电路中的一个细节可能会让你感到困惑,那就是限流电阻器(限流电阻器是一种用来调节电路中电流的电阻器)的位置。

在图 3.14 中,电阻器连接在 LED 和地之间;也就是说,它们位于 LED 的“之后”。实际上,在串联电路中,电阻器相对于 LED 的位置并不重要。

放置在串联电路中的限流电阻器会影响整个电路的电流,无论其位置如何——记住,串联电路中所有点的电流是相同的。

3.2.3. 使用按钮扩展串联电路

你需要

  • 1 个 Arduino Uno 和 USB 线

  • 1 个按钮

  • 1 个标准红色 LED

  • 1 个 220 伏电阻器

  • 3 根跳线

在你焊接的简单 LED 串联电路中,所有电流都通过 LED,然后通过电阻器,最后到达地(图 3.14)。串联电路中的单个间隙会导致整个电路停止工作,因为它是电路唯一路径的间隙。这使得可以用一个开关来激活和关闭整个电路。

你可以通过向电路中添加按钮来了解这是如何工作的。按钮是一种只有在按下时才会完成连接的开关(有时按钮被称为瞬态开关)(图 3.15)。

图 3.15. 按钮两侧的引脚始终电连接,而共享同一侧的引脚只有在按钮按下时才连接。

在图 3.16 中,按钮连接到面包板上,方向使得“始终连接”的引脚跨越中间的凹槽。这意味着顶行的高亮连接始终电连接,底行的高亮连接也是如此。当按钮未激活(未按下)时,两行彼此隔离。然而,当按钮被按下时,按钮左侧的上下引脚之间以及右侧的上下引脚之间会建立连接。结果是,当按钮被按下时,两个高亮行在电上相互连接。

图 3.16. 一个连接到面包板的按钮,跨越中心缺口。当未按下时,按钮顶部和底部的引脚对是电连接的(水平)。当按下并保持时,按钮左右两侧的引脚对是电连接的(垂直)。

图 3.16

当按钮正确连接并按下并保持时,电路是闭合的,完成路径并允许电子通过电路流动。当释放时,电路是开路的——它有一个间隙,没有电流流动。你需要构建的电路图显示在图 3.17 中。

图 3.17. 更新电路的原理图,集成按钮

图 3.17

构建电路:按钮和 LED

要在图 3.18 中构建按钮电路,请按照以下步骤操作:

  1. 断开 Arduino Uno 的电源。

  2. 从面包板上移除 LED 和电阻。

  3. 使用一根黑色跳线,将 Arduino Uno 的 GND 连接到面包板右侧负电源轨的最上面一行。

  4. 将按钮连接到面包板上。你的按钮可能尺寸不同,更适合放在不同的行之间,这是可以的。

  5. 将 LED 的正极(较长的腿)插入与按钮底部两个腿同一行的槽位。

  6. 将 LED 的负极插入下一行。

  7. 将电阻从与 LED 负极同一行的槽位连接到负电源轨。你可以将连接左手电源轨到第 4 行的红色电线留在与之前练习相同的位置。

  8. 重新连接 Arduino Uno 的电源。

初始时 LED 不应该亮。按下按钮。LED 应该在你按下按钮的时间内亮起。

图 3.18. 带有按钮的改进 LED 电路的布线图

图 3.18 替代

3.2.4. 串联 LED

在串联电路探索之旅中还有一站。让我们构建一个包含多个串联 LED 的电路——也就是说,在一个只有一条路径的电路上有多个 LED。这就是我坦白之前章节中省略的 LED-电阻计算细节的地方。

为了唤醒你的记忆,计算是为了找到 5V 电路中 LED 的正确电阻值——目标电流 20mA:

R = 5 V / .02 A
R = 250 *V*

我们没有向上取整到下一个常见的电阻值,而是向下取整到 220V。

在 5V 电路中,220V 电阻对于 20mA 的 LED 来说绰绰有余的原因是,由于 LED 的特性,我们在计算合适的电阻值时不需要考虑完整的 5V。实际上,220V 略高。

存在一条相关定律,称为基尔霍夫电压定律(对那些了解的人来说是KVL)。它指出电路中的所有电压都必须平衡:产生的电压量必须与使用的电压量相同。输入电压,输出电压。

在 LED 串联电路中,如果电阻是电路中唯一“使用”一些电压的元件,那么 250 V 将是正确的近似电阻值。但事实并非如此。LED 有一个称为“正向电压降”的度量。这有点细节,但就我们的目的而言,它是 LED 在电路中消耗的大约电压量(图 3.19 和图 3.20)。

图 3.19。当电流穿过 LED 时,会消耗一些电压,或者说是“降下”。消耗的电压量称为 LED 的“正向电压”。它介于大约 1.8 V 到 3.5 V 之间,具体取决于 LED 的颜色——发出的光频率越高,电压降就越高。

图 3.20。回顾电压作为悬崖陡度或高度的比较,可以从不同的角度看到相同的串联 LED 电路。当水母电流通过 LED 时,1.8 V 被“消耗”,剩余电压降至 3.2 V(降低陡度)。然后,这 3.2 V 被限流电阻消耗。当水母达到最低电势点时,电路中的所有电压都已考虑在内。

虽然大多数日常标准 LED 的正向电流(为了简洁,你可以将其视为“大致应该接收的电流”)为 20 mA,但不同 LED 的正向电压是不同的,这主要与 LED 的颜色有关。

红色 LED 的正向电压大部分在 1.8 到 2 V 之间变化。如果电路中的 LED 的正向电压降为 1.8 V,我们可以从 5 V 的系统电压中减去这部分电压,这就是电阻需要考虑的电压:

5 V (supply voltage)
- 1.8 V (red LED forward voltage)
--------------------------------
= 3.2 V (remaining voltage)

既然我们想要达到 20 mA 的电流(0.02 A),根据欧姆定律得出的方程式是

R = 3.2 V / 0.02 A

或者

R = 160 V

220 V 的电阻值足够接近,可以非常合适——按照惯例,将其四舍五入到下一个常见的电阻值。现在,220 V 比 160 V 的电阻值要大,所以你可能想知道使用更高电阻值对 LED 和电路有什么影响。更高的电阻值和稳定的电压意味着电流会下降(因为,一如既往,根据欧姆定律):

I = V / R
I = 3.2 / 220 V
I = 0.0145 A

最后,LED 的电流小于 20 mA——大约 15 mA(0.0145 A)。提供给 LED 的电流与其亮度成正比:接收 15 mA 的 LED 不会像接收 20 mA 的 LED 那样耀眼。

如果在这个串联电路中再添加一个相同的红色 LED,情况会怎样?需要什么电阻值?我们需要考虑两个 LED 的电压降(图 3.21)。

图 3.21。两个 LED 中的每一个都会在电路中消耗一些电压,同样,剩余的电压由电阻消耗。

幸运的是,这是一个简单直接的数学问题。在从总电路电压中减去每个 LED 的电压降后,我们剩下 1.4 V:

5 V (supply)
- 1.8 V (LED 1)
- 1.8 V (LED 2)
--------------
= 1.4 V

R = 1.4 V / 0.02 A
R = 70 *V*

将电阻值四舍五入到下一个常用电阻值,一个 100V 电阻将非常适合,如图 3.22 中的电路图所示。

图 3.22. 两个 LED 灯通过一个 100V 电阻串联连接。当按下按钮时,电路中只有一条路径:通过第一个 LED,通过第二个 LED,通过电阻,然后回到地。

构建电路:两个 LED 灯串联
你将需要

  • 1 个 Arduino Uno 和 USB 线

  • 1 个按钮

  • 2 个标准红色 LED 灯

  • 1 个 100V 电阻

  • 3 根跳线

要构建图 3.23 中的电路,请按照以下步骤操作:

  1. 从电源上拔下你的 Arduino Uno。

  2. 从面包板上拔下 220V 电阻并收好。

  3. 将第二个 LED 的正极插入与第一个 LED 的负极相同的行(图 3.23 中的第 7 行)。

  4. 从负电源轨连接一个 100V 电阻到包含第二个 LED 负极的行(第 8 行)。

图 3.23. 两个 LED 灯串联的接线图

如果你将你的 Arduino Uno 连接到电源并按下按钮,两个 LED 灯应该都会亮起。它们是串联连接的,可以通过再次查看图 3.22 来更直观地理解。

那如果你添加第三个 LED 呢?这是一个陷阱问题:实际上你并不能做到。如果你想要产生可靠且明亮的灯光,这是不可能的。在第一个两个 LED 的电压下降后,只剩下 1.4V“剩余”电压。这几乎不足以点亮第三个 LED——LED 至少需要其电压下降值才能点亮。你可能能够使三个 LED 微弱地发光,但它们不会很稳定。

使用 5V 电源可以稳定地为两个以上的 LED 灯供电,但要这样做,你需要一个并联电路。

3.2.5. 并联电路和电流分配器

在串联电路中,电子只有一条路径可以走,但在并联电路中,有两条或更多可能的路径,或称为分支(图 3.24)。

图 3.24. 在并联电路中,电流可以走多条路径或分支。在这个例子中,有两个分支:每个分支都有自己的 LED 和电阻。

当电子通过电路并遇到道路分叉时,它们各自会做出选择哪条分支的决定。电流倾向于选择电阻较小的路径(图 3.25)。

图 3.25. 因为这个并联电路中的两个分支具有相同的电阻(100V),一半的电流将通过一个分支,另一半将通过另一个分支。

在计算并联电路中的电阻时,事情会变得有些奇怪且不符合直觉。再次查看图 3.25 中的并联电路。两个 100V 电阻提供的总电阻是多少?

串联电路的总电阻很容易计算——只需相加即可, voilà!你就得到了总电阻。一开始就假设并联电路的总电阻是 200 V 是很诱人的。不,或者你可能注意到,任何给定的电荷在电路中只通过一个电阻,而不是两个。所以这一定意味着总电阻是 100 V?抱歉,也不是。

正确答案是 50 V。

我知道,我知道。这感觉不太对劲,但这是真的:在并联电路中,总电阻总是小于最小电阻值。让我们用欧姆定律和一些深呼吸来探讨这怎么可能。在并联电路中计算等效电阻是新手电子黑客们面临的更具挑战性的概念之一,所以请不要还没开始就拔头发。

让我们把它拆开再组合起来。图 3.26 中显示的 5 V 串联电路和 100 V 电阻将消耗 50 mA 的电流(I),因为

.005 A (I) = 5 V (V) / 100 V (R)
图 3.26。这个串联电路的电流为 50 mA,因为 5 V / 100 V = 0.05 A。

图片

现在假设你复制相同的路径——5 V 和 100 V 电阻——并将其连接到电路中。第二条路径也将独立地消耗 50 mA,因为供电电压(5 V)和电阻(100 V)保持不变(图 3.27)。

图 3.27。并联电路的每个分支单独来看就像它自己的串联电路。我们可以用欧姆定律来验证这个分支也将消耗 50 mA。

图片

两个分支的总电流为 100 mA——电路的总电流消耗增加了(图 3.28)。

图 3.28。进入电路的总电流是 100 mA(点 A)。它平均分成两个分支——每个分支得到 50 mA(点 B、C)。电流重新汇合,在点 D 再次达到 100 mA。在并联电路中,每个分支的供电电压是恒定的,但电流是变化的。

图片

现在整体来看这个电路,其总电流是 100 mA(0.1 A),供电电压,一如既往,是 5 V。将这个值代入欧姆定律,

Total resistance (R) = 5 V (V) / .1 A (I)

R = 50 *V*

并联电路中每个电阻提供的电阻都会减小,因为并联电路中的每个分支都会增加电路中的总电流。电流在上升,而电压保持恒定:电阻在下降。

电流分配器

任何将电源电流分成多个分支的电路都称为电流分配器。以下图中的并联电路是电流分配器的一个例子:一些电流沿着一个分支流动,其余的沿着另一个分支流动。

图片

如果分支的电阻不相等,电流将按比例流过每个分支。

如果电荷发现自己处于道路的岔路口,并且两条可用的路径具有相同的电阻,电荷同样可能选择任何一条路线(就像图 3.28 中的并联电路)。但如果电阻不相等,更多的电荷会选择电阻较小的道路——也就是说,更多的电流会选择电阻较小的分支。

你可以通过单独查看每个分支并确定电路的总电流消耗来计算图中所示电路的总电阻:

Branch 1:
5 V / 200 *V* = .025 A (25 mA) because I = V / R

Branch 2:
5 V / 100 *V* = .05 A (50 mA) because I = V / R

  .025 A (branch 1)
+ .05 A  (branch 2)
=========================
= .075 A (75 mA) total current

R(Total) = 5 V / .075 A because R = V / I
R(Total) = 66.667 *V*

以这种方式计算总电阻,随着分支数量的增加可能会变得繁琐。我们不会设计带有许多不同电阻的复杂电流分配器,但如果你是好奇的类型,有一个公式可以计算任何并联电路中的等效(总)电阻:

1 / R(Total) = 1 / R1 + 1 / R2 + 1 / R3 ... + 1/Rn

一些并联电路的计算可能看起来毫无意义地复杂,但它确实有有用的应用。

这里的问题是:并联电路的每个分支都提供相同的电压。因此,如果我们把串联电路的 LED 添加更多分支(将其变成并联电路——图 3.29),每个分支都会得到“它自己的”5 V 来工作。这样,我们可以在同一电路中连接三个、四个甚至更多的 LED,而不会像串联电路那样电压耗尽。

图 3.29。在并联 LED 电路的原理图中,分支 A、B、C 和 D 都供电 5 V。每个分支接收电路总电流的 1/4。

构建电路:并联 LED
你需要的东西

  • 1 Arduino Uno 和 USB 线

  • 4 个标准红色 LED

  • 4 个 220 V 电阻

  • 6 根跳线

图 3.30 显示了相同电路的物理布局。要在同一电路中连接四个 LED,从一块干净的新面包板开始,按照以下步骤操作:

  1. 将四个 LED 的正极终端连接到 2、9、16 和 23 排的孔中。

  2. 将阴极端插入同一排但位于间隙的另一侧(这样它们就不会电连接)。

  3. 从板左侧的正电源轨运行四根红色跳线到每个 LED 的正极排。

  4. 将每个 LED 的阴极排连接到板右侧的负电源轨。

  5. 使用红色和黑色跳线将电源轨连接到 Arduino Uno 的 5 V 和 GND 引脚。

  6. 将 Arduino Uno 插入 USB 或直流电源。

图 3.30。并联连接 LED 可以使你在单个电路中连接更多元件,因为每个分支都得到相同的电压(5 V)。

如果一切顺利,所有四个 LED 都应该愉快地发光。

并联电路还有一个有用的特性。如果你要从第 2 行的 LED 和电源轨之间的红色电线移除,其他 3 个 LED 仍然会亮起。电路仍然有其他三个完整的路径可以使用。这与串联电路形成对比,在串联电路中,一个单独的间隙会阻止电流流向任何组件。

3.2.6. 使用电池为你的项目供电

到目前为止,你一直使用 Arduino Uno 板上的 5 V 电源为面包板供电,但还有其他提供电源的方法。一个(显然)的选项是使用电池。

单个 9 V 电池是一个方便的电源,在构建 LED 电路的情况下,它消除了对开发板的依赖。

构建电路:9 V 供电 LED
你需要

  • 1 个面包板

  • 9 V 电池和夹子,带电线

  • 4 个标准红色 LED

  • 4 根跳线

  • 4 个 560 V 电阻

使用 9 V 电池时,供电电压(显然)不同于 Arduino 的 5 V。这意味着我们需要为并联电路中的 LED 使用不同的电阻。

回想一下,在并联电路中,每个分支“获得”完整的 9 V 供电电压来工作,因此你可以使用欧姆定律计算每个分支所需的电阻:

R = 9 V / 20 mA
R = 450 *V*

560 V 电阻是最接近的常用电阻值,它将做得很好(你注意到有一些调整空间吗?只要向上取整?)。

断开面包板的电源轨与 Arduino 的连接,并将每个 220 V 电阻更换为更强大的 560 V 电阻。现在将电池盒的正负电线插入面包板的电源轨(图 3.31)。全部完成!

图 3.31. 使用 9 V 电池为并联 LED 电路供电需要更换一些电阻,并将 9 V 电池夹子连接到电源轨上。

摘要

  • 电压、电流和电阻之间的关系——如欧姆定律所正式化的——是理解基本电路的关键。

  • 面包板提供了一个有形且方便的原理图平台,具有标准连接模式,用于尝试电路。

  • 串联电路为电流流动提供一条单一的路径。并联电路有两个或更多路径。

  • LED 有一个称为正向电压降的特性。在计算串联连接的 LED 所需的正确电阻时,首先从供电电压中减去这个正向电压降。

  • 在串联电路中,电流在电路的所有点上都是相等的,而在并联电路中,所有分支的电压都是相等的。

  • 在串联电路中,计算电路中的总电阻很简单:将电阻值相加。

  • 具有多个路径的电路会将电路中的电流分割,称为电流分压器。可以使用电流分配公式计算出这种并联电路的总电阻。

第二部分. 项目基础:使用 Johnny-Five 进行输入和输出

这本书的这一部分是真正开始烹饪的地方:你将学习如何将传感器、输出和运动部件添加到项目中,在 Arduino Uno 板和 Johnny-Five 的帮助下进行一系列小实验。

在第四章中,你将了解所有关于输入(传感器)的内容,无论是模拟还是数字。你将尝试从简单的温度传感器和光敏电阻读取数据,并学习检测按钮按下。

第五章关注输出,基于你之前对 LED 的实验。你将超越闪烁 LED,到动画 LED 和全彩 RGB LED。你将在并行 LCD 模块上显示文本,并构建自己的“天气球”(简化的天气状况显示)。

如果你一直在等待机器人部分,第六章就是它!这一章全部关于运动:电机和伺服系统。我们将研究电机的工作原理以及如何供电和控制它们。在第六章结束时,你将使用一个经济实惠的机器人底盘套件构建一个简单的巡游机器人。

到这本书的这一部分结束时,你将已经调查了小型嵌入式项目中所有主要的简单输入和输出类型。你将能够从传感器读取环境数据,并输出光和声音。你将准备好构建更复杂、无线的项目。

第四章. 传感器和输入

本章涵盖

  • 传感器在项目中作为转换器的作用,将物理现象转换为电信号

  • 微控制器如何使用模数转换(ADC)来解释传入的模拟信号

  • 构建分压器电路来读取电阻传感器如光敏电阻

  • 使用 Johnny-Five 的通用Sensor类读取传感器数据并监听数据和变化

  • 利用 Johnny-Five 的特定组件ThermometerButton

  • 使用下拉电阻管理默认数字逻辑电平

要构建巧妙的设备,无论是温度控制的自动风扇还是更有趣的发明,你必须能够从真实物理世界中收集信息和输入。

对于这一章,你需要以下物品:

  • 1 Arduino Uno 和 USB 线

  • 1 光敏电阻

  • 1 4.7 kΩ 电阻

  • 1 TMP36 模拟温度传感器

  • 1 按钮开关

  • 1 10 kΩ 电阻

  • 黑色、红色和绿色跳线

  • 1 半尺寸面包板

模拟和数字传感器关注物理环境中的特定现象——温度、亮度、湿度、压力、振动——并且输出关于该现象强度变化的信息作为信号。在 第一章 中的自动风扇示例中,温度传感器将温度变化转换为微控制器固件可以读取和处理(图 4.1)的电气信号。

图 4.1. 来自 第一章 的温度控制风扇使用模拟温度传感器来收集关于周围环境温度变化的信息。

图片 04fig01_alt.jpg

这种从物理输入到电气输出的转换意味着传感器是一种 转换器,它可以将一种形式的能量转换为信号(或反之亦然)。

传感器和输入根据它们产生的信号类型进行分类:模拟,一个连续、平滑的值集,没有间隙,或数字,由离散、有限的值集组成(图 4.2)。

图 4.2. 类似于温度传感器(顶部)的模拟传感器将温度变化转换为平滑的模拟信号。类似于倾斜开关(底部)的数字输入可能只有两个离散的输出值:一个在正常方向(关闭,LOW,0 或 false)时,一个在反转(打开,HIGH,1,true)时。

图片 04fig02_alt.jpg

4.1. 与模拟传感器一起工作

我们的物理现实是模拟的:我们生活在一个无限的世界中。0 到 20 摄氏度之间有无限多的温度,有无限多的颜色,有无限多的声音频率。当然,作为人类,我们无法区分 280.3984 Hz 和 280.3985 Hz,但这两个不同的值确实存在。

模拟传感器 对这些模拟、现实世界的输入敏感。它们的输出是一个平滑的信号,通常是变化的电压,对应于它们所测量的强度(图 4.3)。

图 4.3. 模拟温度传感器的输出信号随温度变化在电压上变化。

图片 04fig03_alt.jpg

4.1.1. 模拟到数字转换

类似于风扇中的温度传感器的模拟传感器提供模拟信号,但我们的编程世界是数字的。模拟输入信号需要以某种方式采样和归一化——量化——为离散的数字值,以便它们可以用数字逻辑进行处理。

将信号样本从模拟转换为数字的这种转换需要一些硬件和处理。Arduino Uno 的微控制器在其六个引脚上提供了内置的 模拟到数字转换(ADC)功能。具有 ADC 支持的模拟输入引脚在 Uno 上以“A”为前缀(图 4.4)。

图 4.4. ADC 由 ATmega328P 微控制器在特定引脚上支持(在 Uno 上作为 A0-A5 提供)。ADC 将模拟信号转换为离散的数字值。

图片 04fig04_alt.jpg

从输入的模拟信号中可以导出的可能值的数量取决于微控制器上 ADC 硬件的带宽。Arduino Uno 上的 ATmega328P 为每个启用的引脚提供 10 位 ADC。这意味着它可以解析 1024(2¹⁰)个可能的数字值。0 V 的模拟输入将被解释为 0,5 V 为 1023,任何介于两者之间的值都按比例缩放到最接近的(整数)步进值。

模拟传感器对它敏感的物理刺激做出反应,将这种物理能量转换为电压变化的输出信号。该信号可以通过微控制器的模拟输入引脚进行采样,并使用 MCU 的模数转换(ADC)功能转换为数字值。为了看到这是如何工作的,让我们通过一个基本的模拟传感器进行实验。

4.1.2. 使用光敏电阻

光敏电阻,也称为光电池光敏电阻器(LDR),是一种简单的传感器。它的名字揭示了它实际上是一种电阻器,但其导电性取决于照射到它上的入射光的亮度——它是光电导的。

当光线较暗时,光敏电阻的电阻更高,在完全黑暗时达到约 10 kΩ。随着它所暴露的光量增加,它变得更加导电——其电阻降低——在光线相当明亮时降低到约 1 kΩ(图 4.5)。

图 4.5. 随着光照条件的变化,光敏电阻的电阻会变化。当更多的光照射到它时,它变得更加导电——也就是说,电阻更小。当光线较暗时,电阻更高。

电路中的电压

假设我们想要能够使用 Arduino Uno 的具有 ADC 功能的引脚 A0 来读取表示环境光值变化的信号。我们需要一个包含光敏电阻并连接到电源、地以及 Arduino Uno 的引脚 A0 的电路。

但图 4.6 所示的电路并不能解决问题。还记得第三章中提到的基尔霍夫电压定律(KVL)吗?电路中的所有电压都必须被消耗。在图 4.6 所示的电路中,唯一“消耗”电压的组件就是光敏电阻本身。

图 4.6. 在这样的电路中,引脚 A0 上不会出现任何有意义的电压变化。

这意味着无论光敏电阻在任何给定时刻的实际电阻值是多少,它仍然会消耗电路中的所有电压——这意味着 A0 的电压不会改变。

在第三章中,你也看到了在串联电路中电流在每个点都是相同的。图 4.7 所示的电路假设 LED 的正向电压降为 2 V,剩下 3 V 由电阻处理。电路中的总电阻为 200 Ω(两个 100 Ω电阻相加),所以

I = V / R

thus

0.015A = 3 V / 200 *V*
图 4.7. 在串联电路中,电流在所有点都是相同的。

电路中所有点的电流都将测量为 15 mA。

解决我们这里的光敏电阻问题的关键是电压在电路中的元件之间分配(图 4.8),并且它不是在每个点都恒定的。

图 4.8。在串联电路中,电压可以在不同的点有所不同。每个元件都会消耗,或者说下降电路可用电压的一部分。

让我们分解图 4.7 中的电路并分析它,看看电压去哪里了。

5 V 电源中的 2 V 被 LED 的正向电压下降消耗。这留下了 3 V 需要去...某个地方。它被分配给电路中剩余的元件。

分配给每个剩余的电阻元件(两个电阻,R[1]和 R[2])的电压是给定元件的电阻与电路总电阻的比率。

让我们计算分配给 R[1]的电压:

R1 resistance value =      100 V
Total circuit resistance = 200 V
Proportional voltage R1  = 100 V / 200 V = 1/2
Thus, voltage across R1  = (1/2) * 3 V remaining = 1.5 V

因为有两个电阻,并且它们的电阻相等,所以剩余的电压将平均分配给它们,即 50-50。如果电阻的值不同(例如,如果 R[1]是 300 V 而 R[2]是 100 V,它们分配的电压将分别是 1.125 V 和 0.375 V)。

在图 4.9 所示的电路中,两个电阻 R[1]和 R[2]具有相等的电阻——每个电阻提供电路总电阻的一半。因此,电路中剩余的电压(3.0 V)将平均分配给它们,即 50-50,每个电阻 1.5 V。

图 4.9。基尔霍夫电压定律指出,电路中所有电压的总和总是产生 0。也就是说,电路中的所有电压都需要被考虑(被消耗)。

一种更正式的说法是,“电路中的所有电压都需要被消耗”,即电路中的所有电压之和必须为 0。在图 4.9 中绕电路走一圈,我们从电源开始:+5 V。然后 R[1]下降其份额,1.5 V;LED 下降 2.0 V;R[2]下降 1.5 V。所有这些加起来等于 0:

(+5 V) + (–1.5 V) + (–2 V) + (–1.5 V) = 0 V

因为我们知道电路中每个元件消耗的电压量,我们可以推导出电路中不同点的电压(图 4.10)。

图 4.10。串联电路中不同点的电压根据电路中元件下降的电压量而变化。

因此,当电路中只有一个元件时,它将消耗所有的电压。如果你将光敏电阻与其他元件一起连接(图 4.11),它将提供电路中 100%的电阻(技术上,这并不完全正确,因为我们在微控制器中会遇到一个隐藏的电阻,但足够接近了)。

图 4.11。在电路中只有光敏电阻的情况下,没有参考点可以读取可测量的电压变化。

我们需要一种方法来创建一个具有参考点的电路,一个电压在可预测的方式下变化并可读取的点。

4.1.3. 分压器

分压器来拯救!分压器电路使得从光敏电阻中读取有用值成为可能。为了理解这一点,让我们首先看看什么是分压器以及它是如何工作的。

分压器是一种电路,它使用一对电阻将较高的供电电压转换为较低的输出电压。尽管电路的整体电压是供电电压(在我们的例子中是 5 V),但两个电阻之间的电压是一个新的输出电压(图 4.12)。

图 4.12。分压器在 V[out]处提供了一个新的、较低的电压,在两个电阻 R[1]和 R[2]之间可用。有几种方式可以绘制分压器的电路图;这里展示的两个电路在功能上是相同的。

图片

如果分压器中使用的两个电阻具有相同的电阻并且是电路中唯一的组件,那么直观地讲,图 4.13 中 V[out]处的电压将是原始供电电压的一半——R[1]会降低其相应的供电电压份额,即 50%,因为电流流过它。

图 4.13。当电路中的 R[1]和 R[2]具有相同的电阻时,V[out]是 V[in]的一半,因为每个电阻都会降低供电电压的 50%。

图片

图 4.14。V[out]是供电电压的 1/5(20%),因为 R[1]提供了电路总电阻的 80%。

图片

另一方面,如果 R[2]保持在 100 V,但 R[1]的电阻变为 400 V,那么 R[1]将降低 80%的可用电压(图 4.14),这样 V[out]处的输出电压现在仅为供电电压的 20%。

哇!电阻 R[1](光敏电阻)提供的电阻比例变化会反映在 V[out]处的电压变化上。这就是我们需要的参考点!

分压器公式

分压器电路的公式——即确定两个电阻之间 V[out]处电压的方法如下:

V(out) = V(in) * (R2 / (R1 + R2))

V[out]是 R[1]降低电路电压份额后剩余的供电电压。

我们将使用一个固定的 4.7 kΩ电阻作为 R2)。光敏电阻将扮演 R[1]的角色。其电阻范围在 1 kΩ到 10 kΩ之间,具体取决于环境亮度。

图 4.15。光敏电阻电路的电路图

图片

随着 R[1](光敏电阻)的电阻与电路总电阻成比例变化,A0 上的输出电压也会变化。我们可以用 Arduino Uno 测量这个电压,并观察光强的波动。

计算 V[out]的电压范围

您可以通过使用分压器公式来计算 V[out]处可能电压的范围。

在光敏电阻的最高电阻,即黑暗中,R[1]的电阻为 10 kΩ(记住,R[2]始终为 4.7 kΩ)。这种对黑暗的高电阻响应将产生该范围内的最低电压:

V(out) = V(in) * (R2 / (R1 + R2))

R1 = 10000 *V*
R2 = 4700 *V*

V(out)
  = 5 V * (4700 / 14700)
  = 5 V * .32
  = 1.6 V

当明亮时,R[1]可能的最小电阻是 1 kΩ,这将产生该范围内的最高电压:

V(out) = V(in) * (R2 / (R1 + R2))

R1 = 1000 *V*
R2 = 4700 *V*

V(out) = 5 V * (4700 / 5400)
  = 5 V * .825
  = 4.125 V

通过使用光敏电阻的电压分压器电路,你可以为 A0 引脚创建一个可读的输出电压信号,该信号从 1.6 V 变化到 4.125 V。

4.125 – 1.6 V = 2.525 V,或者说是总范围 0–5 V 的略多于 50%。这意味着你将获得一个相当宽的值范围——从(非常粗略地)大约328 (~1.6 V) 到845 (~4.125 V) 的可能 10 位(0–1023)范围内。

4.1.4. 接线和使用光敏电阻

你需要的东西

  • 1 个 Arduino Uno 和 USB 线

  • 1 个光敏电阻

  • 1 个 4.7 kΩ电阻(或 10 kΩ也可以)

  • 红色(2 根)、黑色(2 根)和绿色(1 根)的跳线

  • 1 个半尺寸面包板

就像你一直在使用的其他电阻一样,光敏电阻不是极化的,因此你不需要担心正负极的朝向。按照图 4.16 所示,将光电电池和 4.7 kΩ电阻连接到面包板上。

图 4.16. 光敏电阻电压分压器电路的接线图。连接到 A0 的跳线另一端需要连接到 V[out],在光敏电阻(R[1])和 4.7 kΩ电阻(R[2])之间。

电压分压器输出,V[out],在光电电池和电阻之间的任何终端排孔中都可以获得(回想一下,面包板每个终端排的五个孔在电学上是连接的)。

将绿色跳线的一端插入其中一个孔中,另一端插入到 Uno 的 A0 引脚上。从 Uno 的 5 V 电源和 GND 引脚连接到电源轨。现在你可以暂时把面包板放一边,我们将转向软件方面。

一旦光敏电阻电路通电(顺便说一句,目前还没有!),A0 引脚上将会出现一个变化的电压信号。那么我们如何使用我们手头的 Johnny-Five JavaScript 框架从这个信号中读取值呢?

使用 Johnny-Five 处理模拟传感器输入

Johnny-Five 的 API 包含了一组可以用来与你的开发板和组件交互的类。例如,你已经看到了Led。许多组件类,如Led,是为特定类型的设备设计的,例如AccelerometerServo。还有一些更通用的类,用途更广泛,包括一个可以与模拟传感器如Sensor光敏电阻一起使用的类。

保持 Johnny-Five API 更新

Johnny-Five,像许多其他开源软件一样,一直在不断进化。新功能和类被添加,API 也在不断演变。你可以在johnny-five.io/api上了解最新的 Johnny-Five API。

使用 Johnny-Five 的 Sensor 类

Sensor 类可以用来读取和处理模拟传感器的数据。首先,在 Johnny-Five Node.js 脚本中创建一个新的 Sensor 对象,如下一列表所示。

列表 4.1. 使用 Johnny-Five 实例化一个新的 Sensor 对象
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const sensor = new five.Sensor({
    pin: 'A0'                      *1*
  });
});
  • 1 当实例化 Sensor 时,“pin”是传递的选项对象中唯一的必需属性。

Sensor 的内部机制负责将指定的 pin 配置为模拟输入引脚,然后自动且连续地从该引脚读取 ADC 数据。

那么,你可以用 Sensor 对象做什么呢?你可以像以下列表所示那样记录它的值——valueSensor 对象上可用的几个参数之一,它保存最近读取的值。

列表 4.2. 记录传感器的值
board.on('ready', () => {
  const sensor = new five.Sensor({
    pin: 'A0'
  });
  console.log(sensor.value);            *1*
});
  • 1 这只会记录一次(就绪事件只会触发一次)。

但 列表 4.2 中的代码只会记录传感器的值一次——这用途有限。查看传感器随时间变化的数据要更有用。这是事件驱动 JavaScript 的用武之地。

Johnny-Five 中的传感器数据事件

Johnny-Five 中的不同对象提供了不同的事件,你的代码可以绑定到这些事件上。你可以使用回调函数来处理这些事件,当它们发生时。

例如,Sensor 类有一个 data 事件,每当读取引脚的值成功时都会触发,如下一列表所示。

列表 4.3. 使用 data 事件记录值
sensor.on('data', () => {               *1*
  console.log(sensor.value);
});
  • 1 每当从引脚成功读取值时,都会触发数据事件。
尝试使用光敏电阻

让我们用一些 Johnny-Five 代码来组合我们的电压分压器增强型光敏电阻电路,以检测周围光条件的变化。创建一个新文件,photoresistor.js,并填充以下代码。

列表 4.4. Photoresistor.js
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const sensor = new five.Sensor({
    pin: 'A0'
  });
  sensor.on('data', () => {
    console.log(sensor.value);
  });
});

将 Arduino 的 USB 线缆插入您的计算机——现在板子和面包板电路都有电了。执行光敏电阻脚本:

node photoresistor.js

一旦脚本开始运行,通过将手放在光敏电阻上或调暗灯光来增加和减少到达光敏电阻的光量。当你这样做的时候,你应该会看到记录输出的值发生变化,看起来如下所示。

列表 4.5. 光敏电阻数据记录输出
$ node photoresistor.js
1464612512404 Device(s) /dev/cu.usbmodem1421
1464612512416 Connected /dev/cu.usbmodem1421
1464612515883 Repl Initialized
>> 354
354
355
355
355
355
354
353
432
调整频率、比例和阈值

光敏电阻的值滚动得太快了。默认情况下,Sensor 将每 25 毫秒读取、计算和缩放 ADC 数据。这就是为什么你的光敏电阻值记录滚动得如此之快。你可以通过 freq 参数调整这些读取的频率,如下一列表所示。

列表 4.6. 传感器 freq 参数
const sensor = new five.Sensor({
  pin: 'A0',
  freq: 1000                         *1*
});
  • 1 值以毫秒为单位:这将每秒读取一次。

Johnny-Five 事件绑定和 this

Sensor 对象的事件将 this 绑定到 Sensor 对象,这意味着你可以编写如下代码:

sensor.on('data', function () {
  console.log(this.value); // `this` is bound to `sensor`
});

注意,由于使用了箭头函数作为匿名回调函数的绑定,以下代码将不起作用:

sensor.on('data', () => {
  console.log(this.value); // --> undefined
});

一种更优雅的方法是绑定到change事件而不是data事件。每当传感器的最新值与之前读取的值差异超过阈值时,都会触发change事件。

threshold值默认为1,这意味着——因为读取的传感器值是整数——任何值的改变都会触发change事件。thresholdSensor对象实例上的一个属性,可以在任何时刻更改以改变触发change事件的阈值。以下列表将此结合成一个脚本,该脚本会在传感器值变化5或更多时记录下来(记住,可能的值范围从 0 到 1023)。

列表 4.7. 记录变化的电阻值
var const = require('johnny-five');
var const = new five.Board();

board.on('ready', () => {
  const sensor = new five.Sensor({
    pin: 'A0'
  });
  console.log(sensor.value);             *1*
  sensor.threshold = 5;                  *2*
  sensor.on('change', () => {            *3*
    console.log(sensor.value);
  });
});
  • 1 记录传感器的初始值

  • 2 可以调整阈值属性;默认为 1

  • 3 当值变化≥阈值时触发 change

由于光敏电阻电路在 V[out]的实际电压将根据 R[2]中使用的电阻而变化,因此光敏电阻的值变化更相对(它变得更亮,它变得更暗),而不是绝对(以英尺烛光或测量的光强度)。其他类型的传感器提供校准后的输出电压,可以直接转换为固定单位,例如模拟温度传感器的情况下的摄氏度。

4.1.5. 使用模拟温度传感器

TMP36 是由 Analog Devices 制造的模拟温度传感器。它很丰富,价格便宜,易于使用。与其他模拟传感器一样,它提供变化的电压信号。不过,您不需要构建分压器来使用它:它在第三个引脚上提供变化的输出电压。您只需将传感器连接到+5 V 和地,然后读取第三个输出电压引脚上的电压(图 4.17)。

图 4.17. TMP 36 电路的原理图

04fig17.jpg

对于 TMP36 和其他类似模拟温度传感器,输出电压可以用来计算“真实”的温度值。TMP36 传感器的电压随温度线性增加——给定 TMP36 的电压读取值,可以通过将当前电压乘以 100 并减去 50 来获得摄氏温度。例如,如果传感器的输出电压为 0.7 V,

Temperature in Celsius = 0.7 V * 100 – 50 = 20

然而,要执行此计算,您需要将 ADC 10 位读取值转换回(近似)电压,并将算术运算放入您的代码中。为了使我们更方便,Johnny-Five 提供了Thermometer类,它支持多种不同类型的温度传感器。

构建电路:TMP36 温度传感器
你需要准备的东西

note.jpg

  • 1 Arduino Uno 和 USB 线

  • 1 TMP36 传感器

  • 1 半尺寸面包板

  • 红色(2)、黑色(2)和绿色(1)的跳线

TMP36 的电路如图 4.18 所示。图 4.18。使用红色和黑色跳线将 TMP36 连接到电源,然后使用绿色跳线将其输出(中间)引脚连接到 A0。

不要将 TMP36 反向插入!

TMP36 传感器不喜欢被反向插入。确保你在将其放入电路时检查传感器的平面侧。在给电路施加电源后,触摸 TMP36 传感器。如果它摸起来不舒服地热——哎呀!立即断开电源并检查传感器的方向。

图 4.18. 连接 TMP36 传感器

记录和检查 TMP36 数据

实例化一个 Thermometer 对象类似于使用 Sensor 类,但它需要有关正在使用的温度传感器(控制器)的信息。以下列表展示了记录温度的简单示例。

列表 4.8. temperature.js
var five = require('johnny-five');
var board = new five.Board();

board.on('ready', () => {
  const tmp36 = new five.Thermometer({
    controller: 'TMP36',                     *1*
    pin: 'A0'                                *2*
  });
  tmp36.on('data', () => {
    console.log(tmp36.celsius);              *3*
  });
});
  • 1 Thermometer 需要一个控制器参数——支持大约 15 种不同的温度传感器。

  • 2 与 Sensor 类似,Thermometer 需要知道它所在的引脚。

  • 3 celsius、fahrenheit 和 kelvin 都是 Thermometer 实例的属性。

使用 Johnny-Five REPL 与组件交互

当 Johnny-Five 脚本运行时,你可以在终端窗口中与之交互。或者,你 可以,如果所有那些 console.log 东西没有滚动过去的话。有时 console.log 就是你所需要的,但为了更方便的调试或检查,而无需重新启动脚本或更改代码,你可以利用 Johnny-Five 的 REPL(读取-评估-打印循环)。

如果你从 temperature.js 中移除了 console.logdata 事件处理程序并执行了脚本,你会看到如下所示的内容。

列表 4.9. Johnny-Five 的 REPL
$ node temperature.js
1464614001498 Device(s) /dev/cu.usbmodem1421
1464614001506 Connected /dev/cu.usbmodem1421
1464614004970 Repl Initialized
>>

你可以在双箭头提示符处输入 JavaScript 表达式,然后按键盘上的 Enter 键——这是一个交互式提示符。也许你想查看 TMP36 温度传感器的华氏值,如下列表所示。

列表 4.10. 如果你想从 Johnny-Five 组件获取信息...?
>> tmp36.fahrenheit
ReferenceError: temp36 is not defined
    at repl:1:1
    at REPLServer.defaultEval (repl.js:264:27)
    at bound (domain.js:287:14)
    at REPLServer.runBound [as eval] (domain.js:300:12)
    at REPLServer.<anonymous> (repl.js:431:12)
    at emitOne (events.js:77:13)
    at REPLServer.emit (events.js:169:7)
    at REPLServer.Interface._onLine (readline.js:211:10)
    at REPLServer.Interface._line (readline.js:550:8)
    at REPLServer.Interface._ttyWrite (readline.js:827:14)

这并不是游戏结束;我们只是错过了一步。“注入”某物到 Johnny-Five 的 REPL 范围内需要我们明确指出。

在你的 Johnny-Five 脚本中,你可以选择性地将你希望在脚本运行时可用于检查或操作的项目注入到 REPL 中,如下一列表所示。

列表 4.11. 注入到 REPL 中
board.repl.inject({         *1*
  tmp36: tmp36,             *2*
  foo: 'bar'                *3*
});
  • 1 事物作为键值对注入到 REPL 中。

  • 2 这使得 tmp36 对象引用可用(作为 tmp36)。

  • 3 这使得字符串 'bar' 可作为 foo 使用。

将字符串 'bar' 作为 foo 可用有点愚蠢,但重点是你可以从 REPL 内部使任何类型的值可用。然后你可以使用 REPL 作为与这些项目交互的控制台,如下列表所示。

列表 4.12. 修改 temperature.js 以使用 REPL 注入
var five = require('johnny-five');
var board = new five.Board();

board.on('ready', () => {
  const tmp36 = new five.Thermometer({
    controller: 'TMP36',
    pin: 'A0'
  });
  board.repl.inject({
    tmp36: tmp36
  });
});

现在,当你运行 node temperature.js 时,你将不会看到通过 tmp36 对象的数据记录,但一旦板子初始化完成,你将得到一个 REPL 提示符。然后你可以与 tmp36 对象交互。尝试输入以下内容:

>> tmp36.celsius

或者这样做:

>> tmp36.fahrenheit

这在调试或探索更复杂的组件时非常有用。

光敏电阻非常适合测量环境刺激的相对变化——它现在更亮,现在更暗,现在又亮了。TMP36 传感器非常适合测量作为固定单位的环境刺激——摄氏度或华氏度。两者都产生无限模拟分辨率的电压信号(至少在理论上是这样),可用于软件处理的值范围由微控制器的 ADC(在 Uno 的情况下为 10 位)定义。

然而,对于某些类型的环境感应,你不需要连续的值作为输入。对于这些类型的应用,你可以使用生成数字信号的组件。

4.2. 数字输入

在 Uno 上,引脚 0 到 13 是数字引脚。每个引脚都可以配置为输入引脚或输出引脚。当配置为输入时,数字引脚可以根据存在的电压判断其处于高逻辑状态还是低逻辑状态。

按钮和一些开关是提供简单数字信号的组件的好例子,因为它们是二进制的。按钮要么被按下,要么没有被按下:它是开启还是关闭。这对应于数字输入引脚,其状态也是二进制的(低或高)。

4.2.1. 使用按钮作为数字输入

我们的挑战是构建一个电路,当按下按钮(也称为瞬态开关)时,正确地使数字引脚处于高状态,否则处于低状态。这样,我们的软件就可以确定何时按下按钮,并利用这些信息做些事情。

按钮连接复习

当按钮按下时,共享按钮一侧的引脚才电连接。但按钮另一侧的引脚始终电连接。

0102fig01_alt.jpg

典型按钮的引脚连接

理解逻辑电平

Arduino 的微控制器根据读取时引脚上的电压确定给定的数字输入引脚是高还是低。正如你所预期的,在 Uno 的情况下,输入引脚上的 5 V 将使其逻辑上为高,而 0 V 使其逻辑上为低。然而,这里有一些你需要知道的细微差别,因为它将影响按钮电路的设计。

假设你在 Arduino 上有一个配置好的数字输入引脚,但还没有连接任何东西。如果你读取它的值,你期望引脚处于什么状态?结果,这是不可能预测的。可能是高,可能是低。如果你长时间读取,你会看到随机的高和低。这种未连接的行为被称为浮动

电压和逻辑电平

数字引脚不需要精确的 5 V 电压来读取 HIGH。同样,低电压,比如 0.8 V,将导致逻辑 LOW。每个微控制器都有一组电压范围,这些范围会导致 LOW 状态或 HIGH 状态,以及中间的噪声边缘

Arduino Uno 引脚上逻辑状态的电压范围。任何低于 1.5 V 的电压都是 LOW;任何高于 3 V 的电压都是 HIGH。介于 1.5 V 和 3 V 之间的电压处于噪声边缘,应避免使用。

对于 Arduino Uno 的 ATmega 328P,大约 0–1.5 V 的输入电压将导致 LOW 状态,而高于 3 V 的电压将读取 HIGH。

这是因为引脚的高阻抗,只需极小的电流就可以将引脚在逻辑电平之间移动。这是一件有用且高效的事情——引脚可以在不浪费大量电流的情况下检测连接组件的变化。然而,来自环境中的电气噪声或同一微控制器上其他引脚的干扰通常足以使引脚在 LOW 和 HIGH 之间随机切换。

使用下拉电阻连接按钮
你需要准备的东西

  • 1 个 Arduino Uno 和 USB 线

  • 1 个按钮

  • 1 个 10 kΩ电阻

  • 4 根跳线

  • 1 个半尺寸面包板

我们需要做的是构建一个安全的电路(一个没有短路风险的电路),当按钮未按下时,它能够建立一个可靠的“默认”逻辑电平——LOW 或 HIGH,但不会随机波动。实现这一点的其中一种方法是通过额外的电阻将输入引脚拉至地(0 V)。

当电路中存在下拉电阻时,引脚 2 和地之间始终存在连接,即使按钮未按下(图 4.19)。

图 4.19. 当组件(按钮)未被按下(连接)时,下拉电阻将数字引脚拉至逻辑 LOW。

这解决了浮空问题——如果按钮未按下时出现任何意外的杂散电流,它将通过电阻(图 4.19 的左侧)流向地。当按钮未按下时,引脚 2 将连续读取 LOW(0 V)。

当按钮被按下时(图 4.19 的右侧),电流以 5 V 的电压通过按钮,然后流入引脚 2 和地——引脚 2 将读取 HIGH。下拉电阻在这里还扮演另一个角色:它防止按钮按下时的短路,限制流过的电流。

伪装成电流分压器

当在电路中使用下拉电阻时,你实际上是在创建一个电流分压器,尽管这并不立即明显。记得从第三章中,电流分压是并联电路(图中的电路是并联电路,因为有一个以上的路径供单个电荷通过)的特性。当有多个路径可供选择时,电流将根据每条路径的电阻成比例地分裂。

使用下拉电阻与输入引脚结合实际上创建了一个电流分压器。

在内部,当引脚配置为输入(高阻抗状态)时,微控制器引脚电路包括一个高电阻(大约 100 kV 到几个兆欧姆)的电阻。这个内部电阻将扮演 R[2]的角色。对于 R[1],我们有 10 kV 的下拉电阻——高电阻,但比内部电阻(R[2])低一个数量级。

当电流通过电路(按钮被按下)时,大部分电流将通过 10 kV(R[1])电阻的路径,而不是内部电阻(R[2])的路径。

在电流分压器中,每条路径得到不同的电流分配,但两者得到相同的电压。到达引脚 2 的那一小部分电流在或接近 5 V,这将使引脚设置为 HIGH。

最终的接线图(图 4.20)并不复杂。请按照所示在面包板上连接按钮,确保在引脚 2 和地之间连接一个 10 kV 电阻。

图 4.20. 带有 10 kV 下拉电阻的按钮电路

Johnny-Five 的按钮类

Johnny-Five 的Button类负责配置数字输出引脚,并提供你希望在按钮中拥有的几个功能,包括监听按钮按下的能力,如下一列表所示。

列表 4.13. button.js
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const pushButton = new five.Button(2);
  pushButton.on('down', () => {
    console.log('I have been pressed!');
  });
});

尝试运行列表 4.13 中的代码。将你的 Arduino 连接到 USB 电源并运行脚本:

$ node button.js

一旦初始化了板子,每次按下按钮,你应该看到这个:

>> I have been pressed!

在本章中,你看到了几种基本输入的类型:两种类型的模拟传感器和带有按钮的数字输入。输入和传感器对于收集有关物理环境的信息至关重要,但当你实际上使用这些信息时,这会变得相当有趣——当你创建输出时。这就是你接下来将要做的。

概述

  • 模拟传感器生成具有(理论上)无限分辨率的电压信号,微控制器使用模数转换(ADC)将此信号转换为数字值等效。

  • Arduino Uno 有六个模拟输入引脚,提供 10 位 ADC(1024 个值)。它的 14 个数字引脚可以配置为输入或输出,并具有二进制逻辑电平:LOW 或 HIGH。

  • 串联电路中的每个元件都会接收到相同数量的电流,但电压是根据元件的电阻比例分配的。分压器利用这一原理将较高的输入电压转换为较低的输出电压,通过串联两个电阻来实现。

  • 分压器电路在参考点处创建一个可以检测电阻变化作为电压变化的点。这种方法可以用来读取光敏电阻等电阻传感器的数据。

  • Johnny-Five 或等效的更高级 JavaScript 框架中的 SensorThermometerButton 类可以通过抽象掉引脚配置和繁琐的计算,并提供相关事件来绑定,从而增加便利性。

  • 并联电路中的每个分支都会接收到相同的电压,但电流是根据分支的电阻比例分配的。电流分压器通过使用并联电阻来应用这一原理。

  • 使用下拉电阻是避免在数字输入断开或未活动时出现不确定逻辑电平浮动状态的一种方法。

第五章. 输出:让事物发生

本章涵盖

  • 掌握更高级的 LED 控制技术——动画 LED 和使用全色 RGB LED

  • 使用脉冲宽度调制(PWM)支持使数字输出信号表现得更像模拟输出信号

  • JavaScript 中位运算和二进制操作的基础

  • 集成第三方天气 API 以创建多色 LED “天气球”装置

  • 使用 Johnny-Five 和 Uno 连接和控制并行 LCD 模块

  • 结合多个输入和输出组件构建高级计时器设备

  • 使用压电元件和 Johnny-Five 制作噪音和播放曲调

现在是时候做一些嘈杂的、明亮的、闪烁的或富有表现力的东西了。你已经尝试了一些基本的 LED 技巧,但现在我们将更全面地探讨一些可以将输出集成到项目中的方法。

tool.jpg

对于本章,你需要以下物品:

  • Arduino Uno 和 USB 线

  • 2 个标准 LED,任何颜色

  • 1 个光敏电阻

  • 1 个共阴极 RGB LED

  • 3 个按钮

  • 1 个 16x2 并行 LCD 模块

  • 1 个旋转电位器

  • 1 个压电元件

  • 3 个 10 kΩ 电阻

  • 2 个 220 V 电阻

  • 23 根不同颜色的跳线

  • 1 个半尺寸面包板

0108fig01_alt.jpg

5.1. 点亮事物

LED 似乎能做很多不仅仅是开启或关闭的技巧。如果你看看你自己的电子设备中嵌入的电子元件,你会看到它们在闪烁或淡出。你可能甚至看到它们改变颜色。

这些常见的 LED 行为在技术上实际上是错觉(图 5.1)。LED 只能发射一种波长的光——它只能是一种颜色。然而,我们周围的 LED 确实看起来会变暗或呈现不同的色调。

图 5.1. LED 发射单一颜色,要么开启要么关闭。

05fig01_alt.jpg

是时候成为一个 LED 魔术师了——我们可以利用一种电子技术来使 LED 具有深度和吸引力。

5.1.1. 使用脉冲宽度调制(PWM)淡出 LED

好吧,所以一个 LED 在任何时刻只能处于开启或关闭状态。你已经让它闪烁了一次,这实际上只是一个周期性的开启和关闭。我们将使闪烁更加复杂,目的是欺骗人的眼睛。

你需要什么

  • 1 Arduino Uno 和 USB 线

  • 1 个面包板

  • 2 个标准 LED,任何你喜欢的颜色

  • 2 220 V 电阻

  • 黄色(2)和黑色(1)跳线

首先,构建图 5.2 中所示的电路。接下来,创建一个文件并添加以下列表中的代码。

列表 5.1. experiment-led.js
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const led1 = new five.Led(2);         *1*
  const led2 = new five.Led(3);         *2*

  board.repl.inject({
    led1: led1,                         *3*
    led2: led2
  });
});
  • 1 在引脚 2 上实例化一个 Led 对象

  • 2 在引脚 3 上实例化一个 Led 对象

  • 3 将 Led 对象实例注入到 REPL 中

图 5.2. LED 实验的接线图

这段代码实际上并没有做什么。它创建了两个Led对象,并将它们作为led1led2在 Johnny-Five 的 REPL 中可用。运行脚本:

$ node experiment-LED.js

一旦启动并运行,你将能够在 REPL 提示符处输入命令。首先——我知道这是我们之前已经走过的领域——让其中一个 LED 闪烁。在 REPL 提示符处输入以下内容并按 Enter 键:

>> led1.blink()

第一个 LED 现在应该以 100 毫秒的周期闪烁(亮 100 毫秒,灭 100 毫秒)——这是blink方法的默认相位长度(闪烁周期的速度),如果你不告诉它其他的话。

现在,我想让你用一只手小心地拿起 Arduino 和带有 LED 的面包板,在面前挥动。当你挥动得足够疯狂时,你会知道 LED 看起来不再闪烁,而是一条模糊的线条。

你在欺骗自己的眼睛。当事物移动得太快时,你的眼睛和大脑无法跟上。实际上,你的大脑为你连接点,并决定你看到的是一串连续的光线。如果你是摄影师,你可以把它想象成类似于快门速度——你自己的“快门速度”不够快,结果就是运动模糊。这就是为什么电影和电影可以在每秒 24 帧左右看起来流畅。

对于单个闪烁的检测也是如此。如果灯光闪烁得足够快,人类将完全看不到闪烁(图 5.3)。不同的人有不同的感知阈值——这解释了为什么我在老式的荧光灯或慢刷新频率的 CRT 周围会变得疯狂地暴躁,而我的同事们却一点也不在意。但每个人在约 100 Hz(每秒一百个周期)时都会可靠地失去分辨单个闪烁的能力。

图 5.3. 当 LED 快速移动时,我们失去了分辨单个闪烁的能力。

那么,让 LED 如此快速闪烁以至于闪烁变得不可见,看起来它似乎总是亮着的意义何在?我们难道不能只是稳定地打开 LED来达到同样的效果吗?

如果你玩弄 LED 开启(相对于关闭)的时间比例,会发生有趣的事情。如果你非常快地闪烁 LED,人类将无法感知到闪烁——我们已经讨论过这一点。但如果 LED 只有四分之一的时间开启(其余的三分之三是关闭的),它也会显得相当暗。

让它停止!

如果闪烁的 LED 让你感到烦躁,你可以始终使用Led.stop实例方法来停止闪烁:

> led1.stop()

根据命令发出的时间,结果可能是一个关闭的 LED 或一个稳定开启的 LED。如果 LED 是开启的,而你想要它关闭,使用这个:

> led1.off()

你需要使用这两种方法——你不能跳过off方法。stop将清除导致闪烁发生的间隔——off不会。

让我们实现这一点。尝试(在 REPL 中再次尝试):

>> led1.on()
>> led2.brightness(64)

brightness是 Johnny-Five Led对象上的一个实例方法,它接受一个 8 位值(0–255)。因此,64 的值是可能最亮值的四分之一。led2.brightness(64)调用结果就是 LED 有 75%的时间是关闭的,25%的时间是点亮的。所有这些开启和关闭操作都在人眼无法察觉的频率下进行。你会注意到led2现在看起来比led1暗,它更暗。

Uno 的微控制器在这里提供帮助。它为一种称为脉冲宽度调制(PWM)的技术提供硬件支持,这种技术比软件能轻易提供的开关操作更快。PWM 信号高电平(开启)的时间百分比称为占空比。一个四分之一时间高电平的输出被认为具有 25%的占空比(图 5.4)。

图 5.4. 25%占空比

PWM 硬件支持很常见,因为它非常有用,但支持情况因板而异,通常仅在特定引脚上可用。Arduino Uno 就是这样。PWM 仅在引脚 3、5、6、9、10 和 11 上可用。

不要担心,你不必记住这一点。如果你看看你的 Arduino,你会看到那些引脚编号旁边都印有波浪号(~)(图 5.5)。这表示在该引脚上支持 PWM。

图 5.5. 只有 Arduino Uno 的一些数字引脚支持 PWM。它们被标记为~

现在,回到 REPL 中,尝试这个:

>> led1.brightness(64)

这将不起作用。你会得到一个异常,开头可能是

Error: Pin Error: 2 is not a valid PWM pin (Led)

这就是 Johnny-Five 和firmata——软件包括了哪些引脚在哪些板上做什么的映射。

好的,我们在这次探索中已经看了很多东西。如果闪烁真的很快,人们无法分辨。将信号的占空比改变到 LED 中会使它的亮度看起来在变化。最后一件事,然后我们将用这个知识做点什么。

一定要使用支持 PWM 功能的引脚来处理需要 PWM 功能的特性

类似于brightness和其他你将在Led实例上需要的几个方法,需要组件连接到一个支持 PWM 的引脚。如果你尝试在一个不支持 PWM 的引脚上调用需要 PWM 的方法,Johnny-Five 会抛出一个错误。在设计电路时,考虑到哪些引脚支持 PWM 是一个好主意,这样可以节省时间和避免头疼。

在 REPL 中尝试以下操作:

>> led1.on()
>> led2.brightness(128)

128正好位于亮度范围的中间,所以你可能预期这两个 JavaScript 表达式会导致第二个 LED 的亮度是第一个 LED 的一半,而第一个 LED 是全亮。但除非你眯着眼睛看,两个 LED 看起来几乎一样。你可能能够看到一点点的区别,但并不多。

这是因为亮度更重要。brightness(128)确实会导致占空比为 50%——LED 只有一半时间处于开启状态——但大脑会偏向于亮度。换句话说,Johnny-Five 的 8 位亮度范围 0-255 看起来是非线性的,这完全是由于人类感知的原因。

5.1.2. 使用 PWM 动画化 LED

现在你已经知道如何打开或关闭 LED,闪烁它,并且假设它连接到一个支持 PWM 的引脚,你可以设置其亮度。为了补充这些小技巧,你还可以动画化LED 的亮度,使其看起来像脉冲、弹跳、呼吸或慢慢入睡——你的想象力是无限的。

Johnny-Five 包含一个Animation类,它提供了对 LED 亮度动画和伺服电机运动的精细控制(我们将在第六章中介绍伺服电机 chapter 6)。

在 Johnny-Five 中处理动画涉及几个步骤,如下一个列表所示。

列表 5.2. 在 Johnny-Five 中动画化组件的步骤
const pulsingLED = new five.Led(3);
const options    = { /* animation details */ };          *1*
const animation  = new five.Animation(pulsingLED);       *2*
animation.enqueue(options);                              *3*
  • 1 创建一个包含动画详细信息的选项对象——稍后将有更多介绍

  • 2 实例化一个 Animation 对象,并传递一个目标组件——目标将被动画化

  • 3 使用 enqueue 开始动画

让我们通过让 LED 看起来像脉冲来试一试。Johnny-Five 需要我们首先定义动画应该如何表现。

闪烁的 LED 应该使用吸引式缓动循环淡入淡出。缓动函数是在动画持续期间改变动画速率的函数。例如,一个缓动结束的动画开始时移动得很快,但随着时间的推移会变慢。

缓动函数通常是非线性函数,将正弦、立方、指数和其他曲线整合到方程中。in-out-sine 缓动创建的动画节奏类似于 图 5.6 中所示。缓动函数可以使动画更加逼真或赋予它不同的运动特性。当然,LED 不动,但它们亮度的变化确实可以被动画化。

图 5.6. inOutSine 是 Johnny-Five 通过其对 ease-component npm 包的依赖而提供的几十个缓动函数选项之一。

05fig06.jpg

inOutSine 缓动是我们用于脉冲的,因为亮度变化开始时较慢,然后在 LED 亮起中途加速。脉冲是一种 节拍器 动画,这意味着一旦动画向前运行,它应该然后反向运行,在其起点和终点之间来回 循环

我们还需要通过定义 关键帧 (图 5.7) 来告诉动画它在什么之间进行缓动。关键帧定义了动画应在其中填充中间帧的特定状态(如静止帧)。在关键帧之间创建这些中间帧或状态的过程称为 插值。对于这个简单的脉冲,关键帧很简单:完全关闭(亮度 0)和完全开启(亮度 255)。

图 5.7. 脉冲动画将以节拍器的方式在两个基本关键帧(亮度 0 和亮度 255)之间循环,使用 inOutSine 缓动。

05fig07_alt.jpg

最后,我们需要给动画一个 持续时间,以毫秒为单位。动画的每个部分——从暗到亮或从亮到暗——应该持续一秒钟。组合起来,动画选项看起来像以下代码。

列表 5.3. 脉冲动画选项
const options = {
  easing    : 'inOutSine',
  metronomic: true,
  loop      : true,
  keyFrames : [0, 255],
  duration  : 1000
};

让我们看看这会是什么样子,好吗?你可以使用上一组实验中的电路来完成这部分(你不会使用第一个 LED)。创建一个名为 animate-LED.js 的新文件。将 experiment-LED.js 中的内容粘贴到 animate-LED.js 中作为起点,并编辑代码以在引脚 3 上创建一个单独的 LED。在脉冲 Led 对象实例化之后添加 options 对象,如下所示。

列表 5.4. animate-led.js
const five  = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const pulsingLED = new five.Led(3);
  const options = {
    easing    : 'inOutSine',
    metronomic: true,
    loop      : true,
    keyFrames : [0, 255],
    duration  : 1000
  };
  // ...
});

下一步是步骤 2 和 3:创建一个 Animation 对象并使用 enqueue 使其运行。

列表 5.5. 动画实例化和入队
const five  = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const pulsingLED = new five.Led(3);
  const options = {
    easing    : 'inOutSine',
    metronomic: true,
    loop      : true,
    keyFrames : [0, 255],
    duration  : 1000
  };
  const animation = new five.Animation(pulsingLED);        *1*
  animation.enqueue(options);                              *2*
});
  • 1 传递目标——要动画化的东西。在这种情况下,LED(pulsingLED)。

  • 2 将动画入队并传递动画选项。

现在运行脚本:

$ node animate-LED.js

就这样了。嗯,差不多吧。我让你用困难的方式做了,因为实际上,以下列表中的代码做的是同样的事情。

列表 5.6. 更简单的方式来脉冲 LED
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const pulsingLED = new five.Led(3);
  pulsingLED.pulse(1000);                 *1*
});
  • 1 Led.prototype.pulse 接受一个以毫秒为单位的持续时间(默认 1000)。

在下面,pulse的实现与我们之前所做的是相似的,但它如此常见,以至于它被简化为 Johnny-Five 用户的方法。

闪烁 LED 可以是一种在不那么侵入性的情况下吸引注意力的好方法。使用我们所学的内容,你可以用几行代码制作一个简单的定时器——当时间到了,它就会开始闪烁,如下面的列表所示。

列表 5.7. 世界上最简单的定时器
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const pulsingLED = new five.Led(3);
  const timerLength = 10000;              *1*
  setTimeout(() => {                      *2*
    pulsingLED.pulse();
  }, timerLength);
});
  • 1 定义定时器长度(以毫秒为单位,这里为 10 秒)

  • 2 设置超时,在 10 秒后开始闪烁 LED

当然,这个定时器用途有限。你不能改变定时器的长度或开始时间,也不能启动一个新的定时器。别担心;我们很快就会让它变得更好。

5.1.3. 结合输入和 LED 输出

当然,如果输出是对某种有意义输入的响应,那么输出更有趣。输入和输出之间的连接是使物联网运转的原因。

其中一种耦合可以是光敏电阻和 LED 来创建亮度感知的夜灯。当光敏电阻的读数较低时,LED 可以更亮——也就是说,“夜灯”在“夜晚”变暗时打开。

你需要

  • 1 Arduino Uno 和 USB 线

  • 1 标准 LED,任何颜色(你可以重复使用上面的闪烁 LED)

  • 1 光敏电阻

  • 1 10 kV 电阻

  • 1 220 V 电阻

  • 黄色(1)、红色(2)、绿色(1)和黑色(1)跳线

  • 1 半尺寸面包板

将 LED 连接到面包板上的 3 号引脚(上一个例子中的闪烁 LED),但移除另一个 LED,并添加一个光敏电阻和一个 10 kV 电阻,如图 5.8 所示。

图 5.8. 自动夜灯的接线图

列表 5.8 展示了使用Light类(Johnny-Five 中用于处理光敏电阻的功能)制作光敏感夜灯的第一次尝试,ldr代表光敏电阻,是光敏电阻的另一种称呼。

列表 5.8. 一个简单的夜灯
const five  = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const nightlight = new five.Led(3);
  const ldr        = new five.Light({ pin : 'A0', freq: 500 });      *1*
  ldr.on('change', () => {
    nightlight.brightness((ldr.value >> 2) ^ 255);                   *2*
  });
});
  • 1 每秒检查两次光强度(每 500 毫秒,freq 的值)应该绰绰有余。

  • 2 将 LED 亮度设置为光敏电阻值的“相反”

如果你没有花很多时间在位运算符的世界里,以下这一行可能会让你感到眼花缭乱:

nightlight.brightness((photoresistor.value >> 2) ^ 255);

回想一下,Arduino Uno 的微控制器提供 10 位整数的 ADC 读数(0–1023)。同时,Led对象上的brightness方法期望一个 8 位数字(0–255)。

表达式photoresistor.value >> 2photoresistor.value向右移动两位。这意味着两个位从数字的右侧弹出,并且再也没有被听到过(图 5.9)。在photoresistor.value >> 2的情况下,进入的是一个 10 位数字,它的两个最低有效位被踢出去,返回的是一个 8 位数字。

图 5.9. 将 10 位数字向右移动 2 位得到 8 位数字。

您可以将 10 位数字视为对于 8 位输入过于精确;右移 2 位“四舍五入”到brightness可以使用的一个较低分辨率。

^ 255部分使用位运算的XOR(异或)运算符(^)来获取我所说的“相反”的 8 位数字,即左操作数的相反数。XOR在每一对位上进行比较,并且仅在评估的位不同时(一个为1,一个为0)在数字位返回1(图 5.10)。

图 5.10. XOR 运算比较每个二进制位,如果二进制位不同则产生 1。

表达式(photoresistor.value >> 2) ^ 255的意思是,首先将photoresistor.value右移 2 位(使其成为 8 位数字),然后XOR操作与 255 的结果。

各处进行位移动

对于通常在高层次代码中工作的软件开发者来说,位移动和位运算可能感觉陌生,但在与硬件一起工作时却无处不在。Mozilla 开发者网络有一个很好的 JavaScript 位运算符参考,其中包含了您想要了解的关键内容(mng.bz/CLvy)。

夜灯的第一版有一些不足之处,您可以通过运行它来看到:

$ node nightlight.js

首先,LED 灯总是以某种亮度亮着,即使光电电阻器的读数接近可能的最亮值。我们难道不希望 LED 只在变暗时才亮吗?在晴朗的日子里我们不需要它亮着。夜灯代码的第二版通过替换change回调函数内部的实现来解决这个问题,如下所示。

列表 5.9. 略微改进的夜灯
ldr.on('change', () => {
  if (ldr.level < 0.5) {                                  *1*
    nightlight.brightness((ldr.value >> 1) ^ 255);        *2*
  } else {
    nightlight.off();                                     *3*
  }
});
  • 1 灯实例有一个电平值,这是一个介于 0.0 和 1.0 之间的百分比。

  • 2 改变了 LED 亮度的计算

  • 3 当电平 >= 0.5 时,LED 关闭

在这个变体中,如果光电电阻器的level大于或等于0.5nightlight.off()),LED 将完全关闭。

如果photoresistor.level小于0.5(50%),我们知道它的 10 位value也必须小于 512(因为 511 是可能的 10 位值范围 0–1023 的中点)。这意味着photoresistor.value是一个介于 0 到 511 之间的整数(包括 0 和 511)。这意味着我们只需要右移一位就可以将value转换为 8 位数字(0–255)。

最终结果是,随着光电电阻器值从 511 减小到 0,LED 的亮度会逐渐增加。

但我们仍然可以做得更好。正如你在第四章中看到的,即使选择光电电阻电路电压分压器最佳可能的电阻值,你也不会得到从 0 到 5 V 的完整电压范围。如果夜灯能够根据它实际遇到的条件和读数进行校准,那就更好了,如下一列表所示。

列表 5.10. 自校准夜灯
const five  = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const nightlight = new five.Led(3);
  const ldr        = new five.Light({ pin : 'A0', freq: 500 });
  var dimmest = 1024,                                                          *1*
    brightest = 0;
  ldr.on('change', () => {
    if (ldr.value < dimmest)   { dimmest   = ldr.value; }                      *2*
    if (ldr.value > brightest) { brightest = ldr.value; }

    const relativeValue = five.Fn.scale(ldr.value, dimmest, brightest, 0, 511);*3*
    if (relativeValue <= 255) {
      nightlight.brightness((relativeValue >> 1) ^ 255);
    } else {
      nightlight.off();
    }
  });
});
  • 1 记录它看到的最低和最高读数

  • 2 如果遇到较低和较高的值,则更新最低和最高值

  • 3 将当前值缩放到最低和最高读数之间的 9 位数字

列表 5.10 中的夜灯代码进行了两项更改。一是它记录了夜灯脚本生命周期中看到的最低和最高实际值。然后它利用Board实例上的Fn对象可用的scale实用方法。

scale(value, fromLow, fromHigh, toLow, toHigh)函数接受一个value,该值当前存在于fromLowfromHigh(最暗和最亮的值)之间的范围内,并将其重新映射到toLowtoHigh0511)之间的范围,成比例地。

最终得到的数字将是一个 9 位数字(介于 0 和 511 之间)。现在我们可以继续进行,知道值<= 255位于观察到的值的下半部分。夜灯将在运行过程中继续调整自己(校准高低)。

5.1.4. 使用 RGB LED 实现全彩

好吧,这是对调暗 LED 的广泛覆盖,但那些似乎会改变颜色的 LED 呢?当然,单个 LED 只能始终是单一颜色——一个波长——但如果你在单个包装中将红色、绿色和蓝色 LED 组合在一起,并控制每个组件 LED 的亮度(占空比),!就能得到彩虹的所有颜色。

RGB LED 有四条腿。每条腿对应一个 LED(红色、绿色、蓝色)。第四条腿是阴极或阳极。常见的阴极 RGB LED——其中三个颜色 LED 共享一个单独的阴极腿——是我们将要使用的类型(图 5.11)。也有共享阳极腿的常见阳极 RGB LED。

图 5.11. 常见的阴极 RGB LED 在一个包装中包含三个组件 LED(红色、绿色、蓝色)。最长的腿是共享的阴极。

与普通 LED 一样,RGB LED 允许你使用 PWM 控制亮度,但现在有三个 LED 代表光的基本颜色。对于常见的阴极 LED,组件颜色 LED 的较高占空比会使该颜色更亮,而较低的占空比会使它变暗。

5.1.5. 制作你自己的“天气球”

我在俄勒冈州的波特兰长大,当我还是个孩子的时候,看到标准保险公司大厦顶部的天气球总是有点兴奋。天气球提供了对未来 24 小时左右天气预报的基本编码视觉表示。这个安装在杆上的天气球足够大,可以在整个城市中看到,它被灯光覆盖。

它只有六种可能的状态。预测的温度趋势由颜色表示:红色表示预计会变暖,白色表示变冷,绿色表示保持不变。如果预计会有降水,它会闪烁。简单,但非常清晰和有用。让我们做一个。

您需要准备的东西

  • Arduino Uno 和 USB 线

  • 1 个共阴极 RGB LED

  • 1 220 V 电阻

  • 红色(1)、绿色(1)、蓝色(1)和黑色(1)跳线

  • 1 个面包板

按照图 5.12 所示连接 RGB LED 电路。

图 5.12. 天气球的接线图

现在我们需要天气预报!Dark Sky 服务提供天气预报 API,在撰写本文时,每天最多提供 1,000 次免费查询结果。您需要注册一个 API 密钥,您可以在darksky.net/dev/register进行注册。您可能需要提供信用卡号码来注册服务。

一旦您有了开发者 API 密钥,请将其写下并保存在一个相对安全的地方。您还需要您的纬度和经度,您可以在mygeoposition.com/找到它们。

创建一个名为 weatherBall.js 的新文件。首先,让我们收集一些天气球设置,如下所示。

列表 5.11. 天气球的设置
const API_KEY = 'YOUR API KEY HERE';
const LAT     = '43.3463760';                           *1*
const LONG    = '-72.6395340';
const API_URL = 'https://api.darksky.net/forecast';     *2*
  • 1 我在佛蒙特州。将 LAT 和 LONG 值更改为您自己的位置。

  • 2 您不需要更改此 URL。

是时候处理我们的依赖项了:

$ npm install johnny-five request

我们将使用request包来调用 Dark Sky 的 API。在文件顶部添加requires,然后使用 Johnny-Five 实例化一个板。注意,在下一个列表中,Johnny-Five 为 RGB LED 提供了一个特殊的Led组件类:Led.RGB

列表 5.12. 实例化一个板
const five  = require('johnny-five');
const request = require('request');
// SETTINGS AS BEFORE

var board = new five.Board();
board.on('ready', () => {
  console.log('Powered by Dark Sky: https://darksky.net/poweredby/');
  const rgb = new five.Led.RGB({ pins: [3, 5, 6] });
  // Make request to API
});

接下来,我们需要通过在下一个列表中请求 Dark Sky API 的数据来获取一些关于预报的信息。

列表 5.13. 从 Dark Sky API 请求数据
//...
board.on('ready', () => {
  console.log('Powered by Dark Sky: https://darksky.net/poweredby/');
  const rgb        = new five.Led.RGB({ pins: [3, 5, 6] });
  const requestURL = `${API_URL}/${API_KEY}/${LAT},${LONG}`;         *1*

  request(requestURL, function (error, response, body) {
    if (error) {
      console.error(error);
    } else if (response.statusCode === 200) {                        *2*
      const forecast   = JSON.parse(body);                           *3*
      console.log(forecast);                                         *4*
    }
  });
});
  • 1 组合请求 URL

  • 2 如果响应返回 OK...

  • 3 将响应体解析为 JSON

  • 4 将预报对象记录到 REPL

假设一切顺利,您现在将有一些数据可以处理。让我们按照以下列表让天气球动起来。

列表 5.14. 让天气球动起来
request(requestURL, function (error, response, body) {
  if (error) {
    console.error(error);
  } else if (response.statusCode === 200) {
    const forecast   = JSON.parse(body);
    const daily      = forecast.daily.data;                                *1*
    const willBeDamp = daily[1].precipProbability > 0.2;                   *2*
    const tempDelta  = daily[1].temperatureMax - daily[0].temperatureMax;  *3*
    console.log(forecast);

    if (tempDelta > 4) {                                                   *4*
      rgb.color('#ff0000'); // warmer
    } else if (tempDelta < -4) {
      rgb.color('#ffffff'); // colder
    } else {
      rgb.color('#00ff00'); // about the same
    }
    if (willBeDamp) { rgb.strobe(1000); }                                  *5*
  }
});
  • 1 daily.data 是一个包含七个预报元素的数组。

  • 2 daily[1]是明天。有可能下雨吗?

  • 3 daily[0]是今天。温度变化了多少?

  • 4 根据温度变化设置 RGB LED 的颜色

  • 5 闪烁与闪烁相同——闪烁是闪烁的别名。

电路故障排除

为了最大灵活性,与 Johnny-Five 组件对象实例上的颜色相关的方法——在这种情况下,最重要的是 Led.prototype.color——接受几种不同的颜色格式,如下所示:

  • 十六进制(字符串)* 对于网络开发者来说很熟悉,RGB 十六进制值如 "#00ff00"(亮绿色)可以用来。它可以在或没有前导 # 的情况下使用。

  • CSS 颜色名称(字符串)* “red” 或 “darksalmon” 或 “lemonchiffon” 或任何其他有效的 CSS 颜色名称都适用。

  • R、G、B 值数组(数组)* 数组的每个元素都应是一个 8 位值,例如 [0x00, 0xff, 0x00][0, 255, 0]

  • RGB 值对象(对象)* 再次,应使用 8 位值,例如 { red: 0x00, green: 0xff, blue: 0x00}

johnny-five.io/api/led.rgb/ 上了解最新信息。

现在运行你的天气球:

$ node weatherBall.js

我们在前面示例中使用的传感器和输出只需要几根线,并且连接简单。在下一节中,你将遇到一个有很多线的组件,但只要你注意,它并不难处理。

5.2. 使用并行 LCD 显示器

LCD(液晶显示器)可以显示字符和形状,这使得它们成为众多项目的有用输出。它们的分辨率通常以它们可以显示的 字符 数来定义。我们将在以下实验中使用的 16x2 LCD 模块总共可以显示 32 个字符:每行 16 个。每个“字符”槽实际上是其自己的 5x7 点阵;非字符形状也可以表示。

LCD 模块提供不同的接口,包括几个串行选项(你将在第七章中学习串行通信)。图 5.13 中所示的模块具有 并行 接口。

图 5.13. 16x2 并行 LCD 模块由许多制造商提供。它们有 16 个引脚和一个 LED 背光。

当你看到本节中的接线图时,不要慌张。连接并行 LCD 需要一大把跳线,但与它们一起工作并不复杂。确保你在插拔东西时注意哪些线连接在哪里,你就能做得很好。诚然,改进本章前面提到的定时器项目需要比你在任何地方看到的更多的跳线,但这是一项很好的练习,可以组装更复杂的电路。

5.2.1. 使用 LCD 制作全功能定时器

你需要

  • 1 16x2 5 V 并行 LCD 模块,例如 SparkFun 的任何基本 16x2 5 V 并行 LCD 模块

  • 1 个标准 LED,任何颜色

  • 3 个按钮(瞬态开关)

  • 1 个旋转电位器

  • 1 个 220 V 电阻

  • 3 个 10 kV 电阻

  • 1 个压电片(可选)

  • 23 根跳线

  • 1 个半尺寸面包板

这个更智能的计时器将允许你使用按钮调整计时器的持续时间(即使计时器正在运行!)并在 LCD 上显示剩余时间。你还可以暂停和重新启动计时器。时间到时,LED 将闪烁以(温和地)吸引你的注意。

构建电路

完全构建的电路将看起来像 图 5.14。我们将将其分解为几个步骤以减少其威慑力。

图 5.14. 完全组装好的 LCD 计时器

连接和测试按钮

让我们从一些允许用户控制计时器的按钮开始。从左到右,图 图 5.15 中的按钮是

  1. 一个向下按钮(在以下代码中用 downButton 表示),它将从当前计时器中减去一秒

  2. 一个向上按钮 (upButton),它将向当前计时器添加一秒

  3. 一个切换启动按钮 (goButton),它将启动和暂停计时器

确保按钮跨越中心凹槽,并且方向如图 图 5.15 所示。每个按钮通过一个 10 kΩ 的下拉电阻连接到地,并通过跳线连接到 +5 V 电源轨。

创建一个名为 timer-advanced.js 的新文件,并首先添加以下代码以处理按钮按下。

图 5.15. 仅显示按钮的布线图:向下、向上和启动

列表 5.15. 在 timer-advanced.js 中测试按钮
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const downButton = new five.Button(2);
  const upButton   = new five.Button(3);
  const goButton   = new five.Button(4);
  downButton.on('press', () => {
    console.log('down');
  });
  upButton.on('press', () => {
    console.log('up');
  });
  goButton.on('press', () => {
    console.log('go');
  });
});

通过开始一些简单的按钮处理程序,你可以在继续到设备的其他部分之前验证你的按钮是否正确布线。运行脚本 (node timer-advanced.js) 并验证按下不同的按钮是否将正确的消息记录到控制台。

按钮故障排除

如果按下按钮没有给出你期望的结果,请确保按钮的输出端跳线正确连接到 Arduino——特别是,连接到面包板侧的连接应在按钮输出腿和电阻之间的孔中。

再次检查按钮的输入腿是否连接到电源,以及 10 kΩ 的下拉电阻是否将每个按钮连接到地。还请确保你的按钮方向正确——大多数按钮在一个方向上更适合跨越中心凹槽(正确的方向),但市面上有很多不同类型的按钮。

连接 LCD

在处理完按钮后,我们可以将 LCD 模块连接到面包板,连接到其大多数 16 个引脚(图 5.16)。

图 5.16. 16x2 并行 LCD 模块

按照 LCD 定位图(图 5.17)所示连接 LCD 到面包板,确保 LCD 模块的左侧引脚连接到面包板的左侧行,如图中布线图所示。LCD 的引脚编号为 1-16,从左侧开始编号。

LCD 模块引脚排列

大多数常见的 16x2 并行模块具有相同的引脚方向,当处于 LCD 定位图(图 5.17)所示的方位时,引脚 1 位于左侧,16 位于右侧。请务必检查您的 LCD 模块具有相同的引脚方向——如果不行,您可能需要将 LCD 翻转过来。

图 5.17. 包含电位器和 LCD 位置的布线图

接下来,将电位器连接到面包板上。

布线对比度控制电位器

与光敏电阻一样,电位器是一种可变电阻(图 5.18)。电位器有三个引脚:两个电源引脚和一个中间引脚。当旋转旋转电位器的旋钮(或在其他类型的电位器上滑动)时,其中间引脚上的电压会改变。电位器具有内部电压分压器,因此可以直接读取其输出电压——我们不需要像光敏电阻那样构建电压分压器。

图 5.18. 电位器是可变电阻。它们有多种形状和大小。

在这个电路中,电位器的中间引脚直接连接到 LCD 的引脚 3。LCD 板载电子设备读取该引脚上的电压以确定显示屏的对比度。因此,旋转电位器将调整对比度。

您的电位器可能大小或形状与 LCD 定位图(图 5.17)中所示的不同:这没关系。将其外引脚连接到电源和地——由于电位器不是极性的,所以您无法接反。任何方向都可以。接下来,将电位器的中间腿连接到 LCD 的引脚 3(记住,连接不会穿过中心缺口)。

完成 LCD 的连接

继续前进!LCD 有几个电源连接(图 5.19)。将 LCD 引脚 1、5 和 16 连接到地电源轨,将引脚 2 和 15 连接到源电源轨。

图 5.19. 显示面包板电源连接到 LCD 的布线图

连接到 LCD 引脚 1 和 2 的连接为 LCD 本身供电,而 LCD 引脚 15 和 16 为显示屏的 LED 背光供电。

引脚 5 是一个读/写(R/W)引脚。当它被拉到地时,将 LCD 置于写入模式,这在使用 LCD 时通常是想要的——写入它。

我们将在 LCD 上使用的其余引脚将直接连接到 Arduino 的引脚(图 5.20)。

图 5.20. 显示所有 LCD 连接的布线图

LCD 引脚 4 是寄存器选择(RS)引脚。在特定时间,Johnny-Five 的底层软件需要向 LCD 发送指令以告诉它如何行为。在其他时候,它需要发送用于显示的具体数据。此引脚允许它在指令和数据的不同内存寄存器之间切换。它应该连接到 Arduino 的引脚 7。

LCD 上的第 6 脚是使能(EN)引脚。它应该连接到 Arduino 的第 8 脚。向该引脚写入电压会提示 LCD 读取数据引脚上等待的数据。

数据引脚?那些是 LCD 上最后剩下的连接。LCD 上的第 7 到 14 脚是并行数据引脚(D0–D7),它们代表写入设备寄存器的数据位值。你只需要连接八个中的四个——D4 到 D7(LCD 引脚 11、12、13 和 14,分别连接到 Arduino 的 9、10、11 和 12 脚)。哇。有很多电线!

使用 Johnny-Five 控制 LCD

你可以通过使用 Johnny-Five 的LCD类来测试你的 LCD。默认情况下,Johnny-Five 的LCD对象构造函数会将 LCD 视为并行 LCD。它期望一个包含六个引脚编号的数组,这些引脚连接到 LCD 上的不同引脚:寄存器选择(RS)、使能(EN)和四个数据连接(D4、D5、D6 和 D7)。

与并行 LCD 交互的细节相当底层,但 Johnny-Five 为你抽象了其中很多。Johnny-Five 中有用的LCD方法包括以下这些:

  • cursor(row, column)—在显示文本之前定位光标

  • print(str)—在当前光标位置显示文本

  • clear()—清除 LCD 的内容

按照以下列表添加更多代码到你的 timer-advanced.js 脚本中,以进行测试。

列表 5.16. 测试 LCD
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const downButton = new five.Button(2);
  const upButton   = new five.Button(3);
  const goButton   = new five.Button(4);
  const lcd = new five.LCD([7, 8, 9, 10, 11, 12]);      *1*
  /** button handler functions... **/
  lcd.cursor(0, 0).print('hello, world');               *2*
  lcd.cursor(1, 0).print('hello, again');               *3*
});
  • 1 实例化一个 Johnny-Five LCD 对象

  • 2 将光标定位在第 0 行,第 0 个位置(左上角)并打印“hello, world”

  • 3 将“hello, again”写入 LCD 的第二行,第 0 个位置

运行脚本(node timer-advanced.js),一旦运行,将电位计的旋钮转动以调整 LCD 的对比度。

LCD 应在第一行显示“hello, world”,在第二行显示“hello, again”。

LCD 故障排除

如果你的 LCD 似乎无法正常工作,或者完全不工作,首先应该检查所有电线连接,并确保它们连接到 Arduino 的正确引脚上。

如果你的 LCD 没有点亮,请检查第 15 和 16 脚(背光电源)的电源连接。

确保 LCD 模块安装正确,并且引脚对齐准确。

编程定时器的逻辑

我们的 LCD 电路已经准备好了。现在是时候编程定时器了。我们将将其分解成几个部分,但定时器最终结构的概述如下所示。

列表 5.17. 定时器应用程序结构概述
const five = require('johnny-five');
const board = new five.Board();

// constants

board.on('ready', () => {
  // Initialize J5 components for buttons and LCD
  // Initialize some variables

  function init () {
    // initialize timer
  }

  function showRemaining () {
    // format remaining timer duration and
    // update LCD display
  }

  function adjustTime (delta) {
    // add or remove delta milliseconds
    // to/from timer duration
  }

  function start () {
    // start the timer
    // use setInterval to invoke tick()
  }

  function pause () {
    // pause the timer
  }

  function tick () {
    // update timer values internally
    // if timer is over, chime() and reset timer
    // otherwise, showRemaining()
  }

  function chime () {
    // pulse the indicator LED
  }

  // add button-press handlers

  // initialize the timer
});

从顶部开始,一个变量初始化部分设置了一些我们操作定时器所需的值,如下所示。将这些添加到 timer-advanced.js 中。

列表 5.18. 设置一些值
const five = require('johnny-five');
const board = new five.Board();

const DEFAULT_TIMER = 60000;                        *1*
const UPPER_LIMIT   = 99 * 60000;                   *2*
const LOWER_LIMIT   = 1000;                         *3*

board.on('ready', () => {
  const downButton = new five.Button(2);
  const upButton   = new five.Button(3);
  const goButton   = new five.Button(4);
  const lcd        = new five.LCD([7, 8, 9, 10, 11, 12]);
  const alertLED   = new five.Led(6);
  var remaining, timer, timeString, lastTimeString, timestamp, lastTimestamp;

  // ...
});
  • 1 60 秒是默认定时器长度,以毫秒为单位。如果你喜欢,可以更改它。

  • 2 定时器的上限是 99 分钟。

  • 3 你不能将定时器设置得短于 1 秒。

99 分钟的.upper limit 是基于在 LCD 上格式化剩余时间的方式(mm:ss)。更长的持续时间将无法适应。

说到显示格式,让我们开始吧。现在是时候添加一个初始化计时器并在 LCD 上显示当前计时器的剩余时间的函数了。接下来要添加的代码定义了 init()showRemaining() 函数。

列表 5.19. 初始化并显示剩余计时器时间
// ...
board.on('ready', () => {
  // ... components and variable initialization
  function init () {                                        *1*
    remaining      = DEFAULT_TIMER;
    lastTimeString = '00:00';
    timeString     = '';
    showRemaining();
  }

  function showRemaining () {                               *2*
    var minutes, seconds, minPad, secPad;
    minutes    = Math.floor(remaining / 60000);
    seconds    = Math.floor((remaining % 60000) / 1000);
    minPad     = (minutes < 10) ? '0' : '';
    secPad     = (seconds < 10) ? '0' : '';
    timeString = `${minPad}${minutes}:${secPad}${seconds}`;
    if (timeString != lastTimeString) {                     *3*
      lcd.cursor(0, 0).print(timeString);
    }
  }
  // ...
});
  • 1 初始化一个新的计时器和一些变量

  • 2 格式化并显示此计时器的剩余时间

  • 3 只有当格式化的字符串已更改时才更新 LCD

当计时器实际运行时,showRemaining 函数将被频繁调用。在更新 LCD 之前确保时间字符串已更改的检查(timeString != lastTimeString)将提高性能。当前计时器的剩余时间将显示在 LCD 的左上角(光标位置 0, 0)。

前进!当按下 upButtondownButton 时,应调整计时器的持续时间,分别增加或减少一秒。adjustTime() 函数(在列表 5.20 中)接受一个以毫秒为单位的 delta,并通过该值调整计时器的持续时间,确保总持续时间在范围内。

按钮处理程序需要注册以调用 adjustTime(),这也是初始化计时器(init())的好地方。

添加 adjustTime() 和按钮处理程序,如下所示。

列表 5.20. 添加时间调整处理函数
// ...
board.on('ready', () => {
  // ... variable and component initialization
  function init () { /* ... */ }
  function showRemaining () { /* ... */ }

  function adjustTime (delta) {
    remaining += delta;
    if (remaining < LOWER_LIMIT) {
      remaining = LOWER_LIMIT;
    } else if (remaining > UPPER_LIMIT) {
      remaining = UPPER_LIMIT;
    }
    showRemaining();                          *1*
  }

  downButton.on('press', () => { // remove a second
    adjustTime(-1000);
  });
  upButton.on('press', () => { // add a second
    adjustTime(1000);
  });

  init();                                     *2*
});
  • 1 计时器的持续时间已更改,因此 LCD 的显示需要更新。

  • 2 不要忘记初始化计时器。

现在,让我们连接 Go 按钮。该按钮应切换计时器(播放/暂停)。这意味着我们还需要激活计时器的逻辑——每 250 毫秒调用一次 tick() 函数——以及暂停它,如下一列表所示。

列表 5.21. 使计时器滴答
// ...
board.on('ready', () => {
  // ... variable and component initialization
  function init () { /* ... */ }
  function showRemaining () { /* ... */ }
  function adjustTime (delta) { /* ... */ }

  function start () {
    lcd.clear();                                *1*
    timestamp = Date.now();
    timer     = setInterval(tick, 250);         *2*
    tick();                                     *3*
  }

  function pause () {
    timer = clearInterval(timer);               *4*
    lcd.cursor(0, 9).print('PAUSED');           *5*
  }

  function tick () {
    lastTimestamp = timestamp;
    timestamp     = Date.now();
    remaining -= (timestamp - lastTimestamp);
    if (remaining <= 0) {
      timer = clearInterval(timer);
      init();
    }
    showRemaining();
  }

  downButton.on('press', () => { /* ... */ });
  upButton.on('press', () => { /* ... */ });
  goButton.on('press', () => {
    if (!timer) {                               *6*
      start();
    } else {
      pause();
    }
  });
  init();
});
  • 1 清除 LCD 上当前显示的内容

  • 2 设置一个间隔,每秒大约调用四次 tick

  • 3 启动计时器并发出 tick

  • 4 清除计时器间隔,使计时器停止滴答

  • 5 当再次通过 lcd.clear() 启动计时器时,"PAUSED" 将被擦除。

  • 6 在启动之前确保没有正在运行的计时器

现在,你可以开始和暂停计时器,但时间用完时会发生什么?我们需要某种东西来提醒用户他们的时间已到。

5.2.2. 添加视觉 LED“提示音”

作为此电路构建的最后一部分,你可以添加一个 LED,如图 5.21 所示。它应该通过一个 220 V 电阻连接到地,其阳极应连接到 Arduino 的 7 号引脚。

图 5.21. 带 LED 的完成布线图

我们需要对 starttick 函数进行一些调整,并添加一个新的 chime 函数,如下一列表所示。

列表 5.22. 添加视觉提示音
// ...
board.on('ready', () => {
  const downButton = new five.Button(2);
  const upButton   = new five.Button(3);
  const goButton   = new five.Button(4);
  const lcd        = new five.LCD([7, 8, 9, 10, 11, 12]);
  const alertLED   = new five.Led(6);                                      *1*
  var remaining, timer, timeString, lastTimeString, timestamp, lastTimestamp;

  function init () { /* ... */ }
  function showRemaining () { /* ... */ }
  function adjustTime (delta) { /* ... */ }

  function start () {
    alertLED.stop().off();                                                 *2*
    lcd.clear();
    timestamp = Date.now();
    timer     = setInterval(tick, 250);
    tick();
  }

  function pause () { /* ... */  }

  function tick () {
    lastTimestamp = timestamp;
    timestamp     = Date.now();
    remaining -= (timestamp - lastTimestamp);
    if (remaining <= 0) {                                                  *3*
      timer = clearInterval(timer);
      chime();                                                             *4*
      init();
    }
    showRemaining();
  }

  function chime () {
    alertLED.pulse();                                                      *5*
    lcd.cursor(0, 9).print('DONE!');                                       *6*
  }

  downButton.on('press', () => { /* ... */  });
  upButton.on('press', () => { /* ... */ });
  goButton.on('press', () => { /* ... */  });
  init();
});
  • 1 在 6 号引脚上实例化一个 Led 对象

  • 2 当计时器正在倒计时的时候,蜂鸣-LED 应该关闭。

  • 3 一些逻辑来判断时间是否结束,如果是,则清除间隔,等等

  • 4 chime()...

  • 5 闪烁 LED!时间到!

  • 6 也会显示“完成!”

因为当时间结束时,会再次调用init(),所以计时器可以重复使用而不需要重新启动程序(图 5.22)。试试看!

$ node timer-advanced.js
计时器的代码架构

计时器的逻辑全部发生在 Johnny-Five board对象的ready回调中,而且开始感觉难以控制。可能将逻辑封装在外部模块中会更优雅。你将在后面的章节中看到这方面的例子。

Figure 5.22. 计时器,正在计时!

5.3. 使用蜂鸣器制造噪音

首先,我不会试图说服你蜂鸣器(图 5.23)能发出美妙、悦耳的声音。它们可以根据简单的频率产生音调,但听起来很刺耳。尽管如此,你可以让它们演奏一首歌,而且它们很有趣,可以随意摆弄。如果你想将蜂鸣器添加到计时器中,你可以将 LED 换成蜂鸣器(如图 5.24 所示)。然后你就可以尽情玩耍,用你的蜂鸣器来烦扰朋友了!

Figure 5.23. 一些蜂鸣器有电线引脚,而另一些有腿。蜂鸣器有两个引脚:+ 和 -。

当电压施加到蜂鸣器上时,它会导致蜂鸣器内部表面的物理形状发生变化。蜂鸣器转换电能成机械能,而我们人类将这种机械能检测为声波。在特定频率下施加电压,会产生与不同音符相对应的振动。这种现象被称为逆压电效应——电能转换为机械运动。

蜂鸣器也可以用作传感器。作为输入使用时,蜂鸣器可以检测敲击或其他类型的振动。这展示了逆压电效应的反面,即压电效应

蜂鸣器:时序和频率

要让蜂鸣器发出一个音符,需要 PWM 和时序的结合。要让蜂鸣器发声,你需要以 50%的占空比给它施加电压——这会产生方波(开 关 开 关)。要让蜂鸣器演奏特定的音符,你需要调整这些 PWM 周期的频率。

例如,音符 A4,或 A440 是一个常见的参考调音音符。它被称为 A440,因为它以 440 Hz 振动。每 1/440 秒,有一个完整的波周期。在现实生活中,这些是平滑的模拟波。使用蜂鸣器,它们是方波数字波。通过每秒向蜂鸣器施加 440 次 50%占空比的电压,你可以近似 A4。

标准的 88 键钢琴键盘上包含的八度。A4(即 A440)是中音 C 以上的 A(第四八度的 A)。

音乐频率每八度翻倍,所以当你到达 A7 时,你看到的是 3520 Hz。

每个八度频率翻倍。A3(220 Hz)是 A4(440 Hz)频率的一半,而 A5(880 Hz)是 A4 的频率的两倍。A 音符有很好的圆形频率。中间 C(C4)的频率为 261.626 Hz,C5 的频率为 523.521 Hz,它们之间的音符也有可预测的频率,每个八度翻倍。

在 Johnny-Five 中,A4(A440)是通过每秒交替写入 HIGH 和 LOW 到压电片的引脚上,以相等的间隔 880 次来创建的。是的,880 次,因为一个完整的“波”(开-关)应该每秒发生 440 次。

A4 音符的频率为 440 Hz。一个完整的波周期是 1/440 秒。与自然、模拟声波不同,它们有平滑的曲线,压电片发出的是方波。

这里涉及到的频率远远超过了您可以使用内置 JavaScript 计时器(setIntervalsetTimeout)所能达到的分辨率,因此 Johnny-Five 依赖于一个名为nanotimer的 npm 包。这允许像setInterval一样的行为,但精度更高,粒度更细。

5.3.1. 将可发声的压电片铃声添加到计时器

根据您拥有的压电片类型,可以直接将其插入面包板,或者将它的电线插入面包板(图 5.24)。别忘了也要移除 220 V 电阻。

图 5.24. 替换 LED 的压电片接线图

在压电片上播放旋律

计时器的代码更改很小。在 timer-advanced.js 内部,实例化一个Piezo而不是Led,并从start()函数中移除对alertLED的引用,如下所示。

列表 5.23. 实例化一个压电片
var alertChime = new five.Piezo(6);

Johnny-Five 的Piezo类提供了一些方便的工具来使压电片播放旋律,处理复杂的时序和频率转换。音符通过它们的名称(如'e5')暴露出来,这样您就不需要记住频率。您可以将表示“旋律”的对象传递给play方法,其中包含如tempo(BPM)和song(音符及其持续时间的数组)等属性。

更新chime()函数以播放歌曲,如下所示。

列表 5.24. 新的chime()函数
function chime () {
  alertChime.play({
    tempo: 120,               *1*
    song: [
      ['e5', 1],              *2*
      ['g#5', 1],
      ['f#5', 1],
      ['b4', 2],
      ['e5', 1],
      ['f#5', 1],
      ['g#5', 1],
      ['e5', 2],
      ['g#5', 1],
      ['e5', 1],
      ['f#5', 1],
      ['b4', 2],
      ['b4', 1],
      ['f#5', 1],
      ['g#5', 1],
      ['e5', 2]
    ]
  });
  lcd.cursor(0, 9).print('DONE!');
}
  • 1 速度是一个可选属性。

  • 2 这将播放 e5 音符 1 个“拍”。

您需要亲自尝试以找出时间到调是什么!

摘要

  • 一个 LED 只能发出一种波长(颜色),在任何给定时间只能处于关闭或开启状态,但可以通过脉冲宽度调制(PWM)来欺骗眼睛,使其认为 LED 在不同的亮度下发光。

  • PWM 支持是硬件特性,并且只有开发板上的某些引脚支持 PWM。

  • LED 的亮度可以被“动画化”,以创建效果和传达信息。使用不同的缓动函数和提供不同的动画选项,可以用 Johnny-Five 生成不同的结果。

  • RGB LED 通过 PWM 将三个 LED(红、绿、蓝)以不同的亮度组合在一起,以创建不同的颜色。常见的共阴极 RGB LED 有三个组件 LED,它们共享一个共同的阴极引脚。

  • 并联 LCD 有很多连接。Johnny-Five 通过其LCD类提供了一个简化的 LCD 接口。

  • 滑动变阻器是另一种可变电阻,类似于光敏电阻。与光敏电阻不同,它们有自己的内部电压分压器,因此可以在第三个引脚上读取电压变化。

  • 马达石利用逆压电效应将不同频率的电压转换为机械运动,从而产生声波。

第六章:输出:让事物移动

本章涵盖

  • 电机的工作原理以及是什么使它们旋转

  • 电机感应特性以及如何安全构建电机电路

  • 在电路中使用二极管、电容器和晶体管来控制电机并保护组件

  • 如何使用伺服电机精确定位事物

  • 如何使用 H 桥电路和电机驱动器控制电机

  • 如何构建你的第一个基本、移动的机器人

到现在为止,你可能已经不耐烦地跺脚,想知道:“我们什么时候能做机器人?”好吧,你的船已经到了。是时候学习如何让事物移动了。

你即将了解的电机和伺服电机为机器人的基本运动控制提供基础。有很多东西要学习,但到本章结束时,你将构建一个基本的移动机器人。

让机器人移动涉及编排电子控制动作。电机旋转,使轮子滚动。伺服电机允许精确定位组件:摄像头、机械臂等。让我们开始行动吧。

对于本章,你需要以下物品:

  • Arduino Uno 和 USB 线

  • 1 个 9V 直流电机

  • 1 个 9V 电池和夹子

  • 1N4001 二极管

  • 1 个 N 沟道 MOSFET,例如 FQP30N06L

  • 1 个 100μF 电容器

  • 1 个 4.8V 微型伺服电机

  • 1 个德州仪器 SN754410 四倍半 H 驱动器

  • Actobotics Peewee Runt Rover 套件(或 2 个齿轮电机,2 个轮子,底盘)

  • 断开式雄性引脚头

  • 跳线

  • 1 个半尺寸面包板

6.1. 让电机旋转

电机将电能转化为机械能(图 6.1)。电流进入,运动出来,通常是旋转形式,如旋转轴。

图 6.1:典型的业余 DC 电机

电机将电能转化为动能的魔法核心?磁铁!

实际电流流动

回想一下,传统的电流表示法显示电流从正极流向负极,但在现实中,相反的情况更接近真相?电流的实际流动方向与本章涵盖的一些主题相关,因此概念图显示电流从负极流向正极。

6.1.1. 电机的工作原理

当电流通过导线时,也会产生磁场。磁场与电流方向成直角(图 6.2)。

图 6.2。导线中的电流在电流流动的垂直方向上产生磁场。

图片

在一根单独的直导线附近,由电流引起的磁场相对较弱。但可以通过将一段导线绕在(以及绕在绕在绕在)金属块上,来集中磁效应。当电流施加到导线的线圈上时,集体直角磁场具有足够的能量,可以将金属芯中先前无序排列的一些原子拉入南北方向对齐。而且,看!你已经通过使用电创造了磁铁:一个电磁铁(图 6.3)。

图 6.3。线圈中的电流产生磁场,磁化电磁铁的铁芯。

图片

如果你反转通过线圈导线的电流方向,电磁铁的极性也会翻转——南北磁极会互换位置(图 6.4)。切断导线的电流,磁场就会消散——你可以关闭你的磁铁了!

图 6.4。反转电流流动反转了电磁铁的极性。

图片

让我们设想我们的想象中的电磁铁被用于理论应用。在图 6.5 的左侧,一个未通电的电磁铁安装在一个轴上,并悬挂在两个固定磁铁之间。一旦电流施加到电磁铁上(图 6.5 的中间),电磁铁就会想要按照磁力指示的方式对齐——其北极指向具有朝内面对的南极的固定磁铁。它想要做的是旋转半圈来实现这一点,就像图 6.5 的右侧所示。当然,这个假设的电机不会工作:电池挡在中间,事情变得一团糟。

图 6.5。一个位于两个固定磁铁之间的轴上的电磁铁将想要通过旋转来对齐自己——按照磁力指示。

图片

没有进一步的干预,事情就会在这里停止——磁铁以愉快的方式排列。但如果我们能够再次在这个确切的位置交换电磁铁中的电流方向,运动就可以被迫继续,因为磁铁试图再次正确对齐自己。每半转反转电流极性可以使事物永远继续。事实上,这正是电机工作的原理。像图 6.5 中显示的那样连接的电机将很快变成一团电池和导线的混乱,所以真正的电机涉及一些设计改进。

图 6.6 展示了一个——仍然过于简化的——刷式直流电机。电机的壁包含固定磁铁。电源的正负极连接到两个静止的 刷子,当电机的轴转动并接触分环的不同部分时,它们“刷”上给定极性的电源。随着电机的转动,电流——因此,磁场——极性改变,保持一切运动。刷式电机非常常见,但无刷电机使用相同的一般概念——电和磁——来完成相同的事情。

图 6.6. 刷式电机的电磁铁连接到一个称为 换向器 的分环(A)。当电机旋转时,换向器和电磁铁一起旋转。固定位置的 刷子(B)连接到电源,并在它们下方旋转时“刷”上交替的极性,改变电机 电枢(C)上电磁铁的极性。

图片

6.1.2. 使用按钮开关控制电机

让我们通过构建一个基本的电路来逐步学习电机控制,该电路由按钮控制电机。稍后,我们将添加使用 Arduino Uno 和 Johnny-Five 控制电机的功能。

你需要的东西

图片

  • 1 个 9 V 电池

  • 1 个带引线的 9 V 电池夹

  • 1 个额定电压高达 9 V 的小型直流电机

  • 1 个 1N4001 二极管

  • 1 根跳线(黑色)

  • 1 个半尺寸面包板

电机接收电能,输出机械旋转。但反过来也是真的:如果电机的轴被转动(施加机械能),电机将 产生 电能——它充当 发电机。例如,水可以通过水力发电产生能量。流动的水物理地转动电机的轴(发电机),然后产生电能。所以电机也接收机械能,并输出电能(图 6.7)。这一系列现象——通过磁场相互作用产生电压或运动——被称为 电感。电机是一个 电感 元件。

图 6.7. 当电机被外部力量驱动时,会产生电流。

图片

诱导元件的特性与你的电路有关。比如说,你给电机施加电流,它愉快地旋转着。移除电流——关闭电源——电机将自行继续旋转一段时间。在这短暂的时间内,它将产生能量,值得注意的是,电机(或任何电感器)将在 相反方向 上产生电压(图 6.8)。

图 6.8. 当电机连接到电源时,电流从负极(低电位)流向正极(高电位)。电流流动使电机旋转。当从电机移除电流流动时,惯性会使电机短暂继续旋转。在这段时间内,电机充当发电机,产生与先前输入电压相反的电压。

这意味着电流可以在你的电路中“倒流”,并且负电压尖峰可能非常大(尽管时间非常短)。如果不进行干预,这种反向电压(也称为反向电动势反-EMF)可能会做坏事,比如损坏元件或导致真正的火花跳跃!

当电路闭合且电流通过电机(图 6.9)时,没有保护这种反向电压的电路可以正常工作,但当电机中的电流发生变化(图 6.10)时,这会在按钮释放(开关打开,断开电路)时发生变化,它可能会出现问题。

图 6.9. 在此处所示的未受保护的电路中,电流流动是通过一个按钮(开关)来控制的。当按下并保持开关时,电流按预期流动,为电机供电。电机消耗提供的电压。

在图 6.10 中短暂生成的较大负电压非常渴望找到通往+9 V 电压的路径,它可能会做出疯狂的事情,比如飞跃空中或通过其他非导电材料到达那里。我们必须保护我们的电路免受这种感应电压尖峰的影响。

图 6.10. 当按钮释放(开关打开)时,电机继续旋转一段时间。在这段时间内,它正在产生负电压。之前连接到电池正极(通过开关)的引线可能会积累一个非常大的负电位——比如数百伏,这比电池的+9 V 要低得多。

使用反冲二极管管理反向电压

我们必须管理这种反向电压情况,以确保电路的健康和安全。我们需要确保负电压不会被允许随意在电路中游荡,伤害无辜的元件。

有一种标准方法来完成这项任务,使用二极管(图 6.11)。你曾在第二章中简要地遇到过二极管——LED 就是二极管的一种。二极管是一种半导体元件,只允许电流单向通过它。

图 6.11. 二极管是半导体元件,只允许电流单向流动。

我们即将构建的按钮控制电机电路图中的二极管放置看起来是反的,并且它确实是(图 6.12)。

图 6.12. 基本按钮控制电机电路图,包括保护二极管

图片

二极管在电路中的方向(“反向”,或更技术性地,反向偏置)意味着电流将被阻止流过它——通常。但当出现反向电压情况,并且电路中的流动是颠倒的,二极管暂时变为正向偏置——它暂时调整为电流可以流过它的方向。在这些时候,它可以创建一个路径来“吸收”危险的负电压电流,并反复通过电机重新路由,直到负电压自然耗散(别担心,它会,而且很快)。

以这种方式使用的二极管被称为反激二极管,或吸收二极管(图 6.13)。

图 6.13. 如果在包含如电机这样的电感器的电路中使用反激二极管,它可以提供一个路径,让试图错误方向流动的积压负电压流过。它可以通过电机在回路中循环,直到它耗散。

图片

电路中电机的供电

电机这样的电感元件是电流消耗者,尤其是在它们被打开或关闭时(到现在你可能已经意识到,当电感器启动或关闭时,它们有一些有趣的特性)。单个 Arduino Uno 引脚可能的最大电流抽取仅为 20 mA——这还不够!此外,许多业余电机被评定为 6 V 或甚至 9 V——电压上超过了 Uno 能提供的。我们将为我们的电机使用一个稳定、独立的电源——9 V 电池,以确保我们的电机得到它需要的能量。

构建电路

为了使图 6.12 中所示的电机旋转,你需要从电源(正电池端子)到地(负电池端子)创建一个闭合回路。按下按钮将连接这个回路——当按钮被按下时,电机将运行。

按照图 6.12 所示构建电路,注意正确放置反激二极管。如果你放反了,它可能会非常热(至少),甚至可能爆炸或损坏电池。

一旦你构建了电路,你应该能够通过按下按钮来启动电机。那些小型的业余电机转速很快!

反向旋转电机

你的电机可能有红黑两根线引脚,这意味着有“正确”或“极化”的方式将电机插入电路。这有点误导:电机无论是哪种方向插入电路都很高兴——交换连接只会简单地反转电机旋转的方向。你可以尝试重新调整电机引脚在电路中的方向,看看是否能使其反向运行。通过改变电机中的电流方向来改变电机的方向是机器人控制的关键部分,你将在下面看到。

6.1.3. 使用 Johnny-Five 控制电机

我们第一次的电机实验有几个缺点:没有逻辑控制电机——只有我们自己的手指——电机要么全速运行要么关闭——中间没有其他选择。

Johnny-Five 有一个Motor组件类,可以让你对电机速度有更多的控制。如果你使用更复杂的集成电路或电机控制器,你将获得更多的控制——但让我们从基础开始。

你需要准备

  • 1 Arduino Uno 和 USB 线

  • 1 9V 电池

  • 1 9V 电池夹带引线

  • 1 小型 9V 直流电机

  • 1 1N4001 二极管

  • 1 N 沟道 MOSFET,例如 FQP30N06L

  • 4 跳线

  • 1 半尺寸面包板

在这个实验中,我们希望用可以由 Arduino 引脚输出控制的切换机制来替换人力推动的按钮开关。你已经知道电机需要的不仅仅是 Arduino 板载电源可以提供的——无论是电压还是电流——但任何来自 Arduino 引脚的输出信号都将为 5V。我们将得到一个结合两个独立电源的电路:9V(电池)和 5V(Arduino 逻辑)。

使用晶体管作为开关

MOSFET 是金属氧化物半导体场效应晶体管(whew,难怪人们只说MOSFET)。晶体管是半导体元件,扮演两种角色之一:放大或切换信号。关于晶体管还有很多要了解的,但就我们当前的情况而言,我们将使用 MOSFET 作为非常快速、可靠的开关。

当给 MOSFET 的引脚(图 6.14)施加小电压时,其他两个引脚()相连,电流可以在它们之间流动——MOSFET 被打开。Arduino 可以用较弱的 5V“手指”来“推动”门引脚,完成连接到的 9V 电路。也就是说,你可以使用低电压和低功率的信号来控制高电压和高功率的信号。结果,电机电路有两个输入电源:9V 电池和来自 Arduino 的 5V 输入。

图 6.14. 一个 MOSFET 及其电路符号。施加到门(G)引脚的小电压将在漏(D)和源(S)引脚之间建立连接。

MOSFET 引脚排列

尽管大多数此类(N 沟道)场效应晶体管(FETs)使用图 6.14 中显示的引脚排列,但请确保你查阅你的组件数据手册以确认哪些引脚是门、漏和源。

最后一点:你需要将两个电源连接到公共地。尽管输入电源是分开的——正电源轨仅用于 9 伏电源——但它们的接地是相连的。

构建电路

确保 MOSFET 的金属片朝向与 MOSFET 开关电机接线图(图 6.15)所示方向一致——金属片应朝向右侧。从顶部看,第一个引脚是栅极:这应该连接到 Arduino 的 6 号引脚。将最底部的引脚(源极)连接到地。MOSFET 的漏极引脚(中间引脚)应连接到电机的其中一个引脚和反向二极管,如图所示(二极管与电机并联)。

图 6.15. 晶体管开关电机控制电路图

图 6.15

当施加电压到栅极引脚时,电流将能够在源极和漏极引脚之间流动,从而使电机旋转。

带着电机去兜风

在 Johnny-Five 工作区域中创建一个名为 motor-basic.js 的 JavaScript 文件,并包含以下代码。此脚本在引脚 6 上实例化一个Motor,并将其手动控制可用。

列表 6.1. 电机测试驾驶
const five  = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const motor = new five.Motor({ pin: 6 });         *1*
  board.repl.inject({
    motor: motor                                    *2*
  });
});
  • 1 引脚 6 将使用 PWM 控制电机速度。

  • 2 你将从 REPL 中访问电机。

Johnny-Five 的Motor组件类还有更多功能,但我们从最基本的实例化开始:识别一个引脚来控制电机单方向的速度。这被称为非方向性电机

运行脚本,并在 REPL 中与motor交互:

$ node motor-basic.js

一旦初始化了板子和 REPL,你就可以通过在 REPL 中输入来实验可用的motor对象引用。以下是一些实用的方法:

  • speed(0-255)—使电机以给定速度旋转;例如,motor.speed(100)

  • stop()—停止电机;例如,motor.stop()

  • start()—使用之前设置的速度启动电机;例如,motor.start()

电机速度通过脉冲宽度调制(PWM)控制,所以有一个快速的开关(MOSFET)可以在高频下打开和关闭电路是好事。

进一步探索:制作自己的温度控制风扇

你现在在你的不断增长的构建者工具包中拥有了所有必要的工具,可以构建在第一章中首次提到的温度控制风扇。

挑战:不使用 REPL 来控制电机,而是将 TMP 36 传感器添加到电路中,并使用其值的变化来打开或关闭风扇,或改变其速度。如果你从硬纸板或卡纸上剪下一个风扇叶片并将其固定到电机的轴上,可能会更有趣!

6.2. 让伺服机构动起来

伺服机构是用于精确定位物体的装置,在机器人和其他需要精确移动物体的设备中不可或缺(图 6.16)。

图 6.16. 伺服机构和其基本部件。鸣管有不同形状和大小:圆盘、星星、单臂。

图 6.16

伺服的运动由直流电机提供动力,类似于我们在本章实验中使用的电机。但伺服需要一些额外的部件来完成其工作。一个齿轮组将电机的快速但弱旋转转换为较慢、更准确和更强(更高扭矩)的旋转。额外的内置电路监控输入信号,以告诉伺服它应该在什么角度定位自己,并允许伺服检测它是否处于正确的位置。

大多数伺服大约有 180 度的旋转范围。它们有一个“中性”位置(90 度,或“向上”),并且可以从该中性位置向任意方向旋转约 90 度(图 6.17)。180 度的旋转范围是最佳情况:价格低廉的低功耗伺服可能只有约 150 度的旋转范围。

图 6.17. 伺服(带连接的螺旋桨)定位在最小角度、中性角度和最大角度。不同的伺服有不同的实际角度范围。

伺服套件内的电路对伺服信号线上的编码信号做出响应。用于控制伺服位置的是一种特定的 PWM 信号。

伺服风味 PWM

通过其信号线发送 PWM 信号来控制伺服的位置,但这是 PWM 的一种特殊“风味”。伺服期望每 20 毫秒接收一个脉冲。脉冲持续的时间——信号为高电平的时间——决定了伺服的位置。脉冲持续时间越短,结果位置越偏向左侧。

通常,1.5 毫秒的脉冲会使伺服指向其中性方向(90°)。大约 1 毫秒的脉冲会使伺服指向左侧(0°),而 2 毫秒的脉冲将使其完全指向右侧(180°)。

PWM 脉冲的持续时间决定了伺服的角度。1.5 毫秒的脉冲将伺服定位在中性角度。较短的脉冲对应于更锐角,而较长的脉冲对应于钝角。

6.2.1. 使用 Johnny-Five 控制伺服

你需要准备

  • 1 个 Arduino Uno 和 USB 线

  • 1 个微型伺服(4.8 V)

  • 1 个 100 μF(微法拉)电容器

  • 如有需要,3 个雄性排针

  • 3 根跳线(红色、黑色、黄色)

  • 1 个面包板

用于连接伺服的排针

大多数伺服的线以三个端子的雌性连接器结束。

将这些插入面包板的最简单方法是获得一些断开排针。这些排针条带以 16 到 40 排针的行列提供,你可以折断所需数量的排针以连接特定组件。有些比其他更坚固,可能需要钳子来折断。

在 0.1 英寸间距(0.1 英寸间距使它们与面包板兼容)处寻找雄性排针。

雄性断开排针以条带形式提供。你需要“断开”(折断)所需数量的排针以适应特定组件。

就像电机一样,伺服器在移动时非常耗电,可能会在电路中引起电压波动。然而,如果你采取一些预防措施,可以直接从 Arduino Uno 的电源为低压微服(那些额定电压为 4.8 V 或以下的)供电。

使用去耦电容器保护电路

电容器 (图 6.18) 是一种类似于电池的被动电子元件:它们存储一定量的电荷,以称为 法拉 的单位来衡量(缩写为 F)。一个法拉很大;大多数业余电子电容器都是以微法、纳法或甚至皮法来衡量的。

图 6.18. 电容器有多种不同的封装和尺寸;电解电容器和陶瓷电容器相当常见。请注意:并非所有电容器都有极性。

如果电源和地之间有电压波动,充电电容器会变得不安。例如,如果电压突然下降到 4.5 V,电容器会放电一部分储存的电荷以“平滑”电压。

这意味着电容器可以充当一个小型的、提升的电池,根据需要喷出额外的电荷以保持电压稳定。以这种方式使用的电容器称为 去耦电容器,因为它可以 去耦 电路的其他部分,从而减少由组件引起的电压噪声。

小心使用电容器

当心!电容器是狡猾的小恶魔,在某些情况下可能会非常 危险。它们应得到额外的尊重和照顾。

一个重要的事情是,电容器可以在长时间内保持它们的电荷,即使没有电流通过它们。在这方面,它们又像电池一样。这意味着如果你不小心在电容器上完成电路——是的,你可以用手指造成这种情况——它可能会立即且猛烈地放电。如果你发现自己正在拆卸旧电视或电子闪光灯,一定要非常小心。如果那些强大的电容器意外地向你放电,你可能会去医院,或者更糟。

需要记住的一个更日常的事情是,电解电容器,就像这个实验中使用的电容器一样,是有极性的。它们不会容忍被反向插入,而且当以反向方向使用时,它们有一种相当顽固的习惯,会爆炸。在我们的低压、低电容世界中,这些爆炸相当温和,但它们仍然可能会吓到你,或者熔化你不想熔化的组件和电路板。

构建伺服电路

将你的伺服电机的电源、地线和控制线连接到面包板,如果需要可以使用引脚头(参见图 6.19)。不同的伺服电机制造商使用不同的电线颜色,但正极电源连接应该是红色电线。大多数伺服电机使用黑色作为负极引线,但有些使用棕色或类似颜色的电线。最后,信号线可能是白色、黄色、橙色甚至是蓝色。总之,红色电线是电源,最深的电线是地线,剩下的就是信号线(图 6.20)。

图 6.19. 带有去耦电容器的伺服电机接线图

图 6.20. 伺服电线的颜色各异,连接器也各不相同。应该有一条红色电线——这条电线连接到 Vcc(电源)。最深的电线——黑色或棕色的是地线。剩下的电线是信号线——在这里简写为 S——用于控制伺服电机的位置。

在伺服电机的电源连接处并联一个 100 μF 的电解电容器——也就是说,将电容器的阳极插入与伺服电机电源连接相同的面包板行,将电容器的阴极插入与伺服电机地连接相同的行。将伺服电机的信号线连接到 Uno 的 6 号引脚。最后,将伺服电机的电源连接到 Uno 的 5 V 电源,将伺服电机的地连接到 Uno 的 GND 引脚。

对伺服电机进行测试驾驶

你现在可能已经猜到了,Johnny-Five 有一个用于控制伺服电机的 Servo 类。确实如此!

创建一个新的文件,servo-basic.js,并添加以下 JavaScript。这与电机测试驾驶类似:它将在 REPL 中使 servo 可用。

列表 6.2. 伺服电机测试驾驶
const five  = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const servo = new five.Servo({ pin: 6 });           *1*
  board.repl.inject({
    servo: servo
  });
});
  • 1 伺服电机需要 PWM 控制信号——务必使用支持 PWM 的引脚!

运行脚本:

$ node servo-basic.js

现在,你可以使用 Servo 的方法:

  • to(deg)——将伺服电机移动到 deg 值(介于 0 和 180 之间)。例如,servo.to(50)

  • center()——将伺服电机移动到其中心/中性位置,默认为 90 度。例如,servo.center()

如果你尝试将伺服电机定位在其范围的极端位置——0 或 180 度或接近这些值——它可能会抗议,发出悲伤的磨擦声。这是因为伺服电机的 有效 范围小于 180 度。尝试实验以确定伺服电机可以舒适达到的最小和最大角度值。

这个简短的测试驾驶只是对伺服电机的一个简要介绍。如果下一节——构建机器人!——对你有吸引力,并且你想要深入研究机器人领域,那么在探索过程中你将遇到更多关于伺服电机的情况。

6.3. 构建你的第一个机器人!

是时候构建一个机器人了。要构建一个可以在轮子上移动的机器人,你需要一些基本部件(图 6.21):

  • 底盘—— 每个机器人都需要一个身体。底盘是一个可以安装电机、轮子和其他组件的结构。

  • 至少两个齿轮电机 机器人驱动电机需要一些齿轮减速以提供一些扭矩。否则,它们甚至无法越过最微小的障碍。齿轮电机的驱动轴通常(但不总是)与电机的旋转成直角。

  • 轮子或履带 这些将电机的运动转换为机器的运动。轮子适合安装在齿轮电机的驱动轴上。

  • 大脑 定义机器人逻辑的微控制器或处理器。

  • 电机电路 你的机器人需要一些组件来将其所需的运动转换为电机运动。

图 6.21. 一个基本的机器人需要一个底盘(身体)。两个或更多的齿轮电机连接到轮子或履带以推动机器人。当然,你还需要电路、大脑(微控制器)和电源。

图片

进入基础机器人领域可能会让人望而却步的一件事是各种部件的综合成本。底盘可能要花费几百美元(!),而现成的用于控制电机的防护罩、电路板或其他电路可能也要花费不少。

我们的第一辆巡游机器人将使用相当便宜的部件。你可以在网上和电子产品商店找到建议的 Actobotics Peewee Runt Rover 底盘套件,大约 16 美元。

你需要的东西

图片

  • 1 Arduino Uno 和 USB 线^([1])

    ¹

    如果你恰好有一根比随 Arduino 一起提供的 USB A 到 USB B 线更长的线,现在就是使用它的时候了。

  • 1 个 Actobotics Peewee Runt Rover 机器人套件或类似套件^([2])

    ²

    包含底盘、2 个齿轮电机和 2 个轮子。

  • 9 V 电池和夹子

  • 1 个德州仪器 SN754410 四倍半-H 驱动器

  • 15 个跳线

  • 双面胶带、电工胶带、电工胶带或其他粘合剂(可选)

  • 1 个半尺寸面包板

| |

建造你的第一个机器人的其他选项

Actobotics Peewee Runt Rover 套件是第一台机器人的便捷选择,因为它价格低廉,包括底盘、轮子、齿轮电机和支架——你需要的基本部件。但其他两轮底盘框架或套件也很好。SparkFun 的 Shadow 底盘是另一个经济实惠的选择(尽管你需要单独购买电机和轮子)。无论你最终得到什么,确保你有底盘、齿轮电机和轮子。以下关于构建巡游机器人的说明假设了 Peewee 套件,但构建不应与类似套件相差太多。

6.3.1. 机器人和电机

从我们最近的实验中,你已经看到了如何使用 PWM 控制电机的速度,以及如何通过交换其电源和地线连接来反转电机的方向。你还看到了电机电路比其他电路复杂一些:你需要二极管来保护电路的其他组件,并且你可能需要一个比开发板能提供的更强大的电源来为电机供电。

这会变得稍微复杂一些。一个移动的机器人至少需要两个电机才能转向,您需要能够以正向和反向方向运行这些电机(当然,不需要物理拔掉并交换引脚)。单个电路上的多个电机也应使用解耦电容器进行隔离,以防止一个电机引起的突然尖峰影响另一个电机或组件。细节开始累积。

使用 H 桥驱动器控制电机

H 桥是一种包含四个开关和中间负载的电路——在我们的例子中,负载是电机。在概念上,它大致呈字母H的形状,如图图 6.22 所示。

图 6.22. H 桥电路包含四个开关,电机位于中央。不同的开关状态组合可以使电流以不同的方向通过电机。

图片

H 桥中开关(即晶体管)的排列允许您在不改变电机物理接线的情况下控制电机的方向。关闭 1 号和 4 号开关(图 6.23,左侧)允许电流以一个方向流过电机,而关闭 2 号和 3 号开关(图 6.23,右侧)允许电机以相反的方向旋转。

图 6.23. 通过激活 H 桥中的不同开关,可以使电机正向或反向旋转。

图片

H 桥电路可以处于 16 种可能的状态(开关组合)中,其中图 6.23 中的两种是最显然有用的。其他几种是无害的,允许电机滑行(当电路中没有路径时)或制动电机(电机的两个引脚连接到相同的电压)。

有六种开关组合是坏消息,会导致所谓的“直通”情况,更简单地描述为短路(图 6.24)。这并不好,会损坏设备。

图 6.24. 关闭 H 桥两侧的开关会导致“直通”——短路!

图片

对于我们的机器人,我们需要为每个电机构建一个 H 桥电路,更不用说还要添加额外的保护二极管和电容器——这听起来像是要连接很多线路和复杂化。幸运的是,H 桥作为集成电路(IC)芯片价格低廉(图 6.25)。更好的是,许多低成本 IC 将 H 桥电路与内部二极管和其他设备一起封装,以保护您的电机电路,并防止直通状态。这些芯片被称为电机驱动器或简称驱动器

图 6.25. 像德州仪器 SN754410 这样的低成本电机驱动器,让您能够对内部 H 桥电路进行逻辑控制。它们还包含其他组件,如二极管,使电机控制更简单、更不易出错。SN754410 是双 H 桥——它可以控制两个电机。

图片

德州仪器 SN754410 四重半 H 桥驱动器只需花费几美元,可以从各种电子产品供应商处购买。是的,它的名字很长(提示:四重半 H 桥相当于两个完整的 H 桥电路),但它可以做我们为第一个巡游机器人驱动两个电机所需的一切。

在我们深入探讨电机驱动 IC 引脚和相关电路的细节之前,让我们开始构建我们第一个机器人的基础部件。

6.3.2. 构建机器人底盘基础

你需要将侧支撑(这可能需要一些力气)和面包板固定到机器人底盘的底部板(图 6.26)。如果你有 Peewee 套件,侧支撑是中间有较大、矩形孔的塑料件。

图 6.26. Peewee Runt Rover 底盘的底部板,带有连接的侧支撑和面包板。如果你使用的是不同的底盘,当然看起来会有些不同。

图片

将面包板固定到底盘上

大多数半尺寸面包板都有可以通过撕掉一块背纸来暴露的粘合剂,这可以用来将面包板粘附到机器人底盘上,以防止其移动。如果你不能永久地将面包板固定到机器人上——也许这是你唯一的面包板!——或者如果你的面包板没有粘合剂,你可以使用双面胶带、电工胶带、电工胶带或你选择的任何可移除粘合剂临时将面包板固定到机器人底盘上。你也可以使用线扣。

将面包板居中并固定到底盘上。板子中央的孔会露出一些面包板的粘合剂(如果你要永久性地固定面包板)。你可以剪下一块背纸,形状与孔相匹配,然后重新粘上去以覆盖暴露的粘性表面,如果你喜欢的话。暂时不要固定底盘的上板。

为机器人设计的齿轮电机有一个用于驱动轮子的齿轮输出轴,通常与电机的旋转轴成直角。轮子固定在输出轴上,朝向机器人外部,而电机的旋转轴向上(如在 Peewee 套件中,图 6.27),或者朝向后方(如在 SparkFun Shadow 底盘中)。在所有情况下,你都会希望将电机的线缆放在内部。

图 6.27. Peewee Runt Rover 底盘的上板,展示固定好的齿轮电机、轮子、开发板支撑和 Arduino Uno。确保电机的线缆朝向底盘内部。电机线缆可以穿过 Uno 下方并通过底盘板的中心孔。

图片

取出 Peewee 底盘的上板,并将齿轮电机安装到上面。然后,可以将电机的引线通过上板中间的圆形孔引下来,以便访问下面的面包板。Peewee 套件还附带两个开发板支架,形状为宽而浅的U形。现在应该将它们安装到上板上,并将 Arduino 放入其中,USB 连接朝向将成为机器人后部的方向(板子前后对称,所以你现在可以决定你的机器人后部在哪里)。

现在可以将轮子滑到齿轮电机轴上。此时,你应该有一个带有面包板和侧支架的底板,以及一个带有 Arduino、电机和轮子的上板。

6.3.3. 控制机器人的电机

如果将 SN754410 电机驱动器以顶部的半圆形凹槽朝上放置,引脚的顺序从 1 到 16,如图 6.28 所示。不同的引脚有不同的用途,并为芯片内部的不同部分提供连接。我们将在构建电路时介绍这些引脚。

图 6.28. SN754410 电机驱动器的引脚排列

SN754410 电源和使能连接

连接 SN754410 的第一步是将一些引脚连接到电源和地(图 6.29)。

图 6.29. SN754410 需要连接到 VCC2(物理引脚 8)上的电机电源和 VCC1(物理引脚 16)上的逻辑电源。两个使能引脚应直接连接到 5V 以将其设置为高电平。还有四个 GND 引脚,不出所料,需要连接到地。

驱动器需要连接到两个独立的电源。9V 电池将为电机本身供电(电机电源),而控制电机的逻辑将由来自 Uno 的 5V 供电(逻辑电源)。

此外,芯片上还有两个使能引脚:一个用于每个电机的驱动器。您需要通过将每个使能(EN)引脚——物理引脚 1 和 9——连接到 5V(即逻辑高电平)来“打开”每个驱动器。

按照电机电源布线图(图 6.30)所示连接到电源和使能引脚。请注意,地轨是连接在一起的(共享地),但电源是隔离的:左侧轨为电机电源(9V),右侧轨为逻辑电源(5V)。

图 6.30. 在机器人的面包板上布线 SN754410 的电源连接

SN754410 逻辑连接

在 SN754410 上,每个电机通过两个输入引脚进行控制。两个相应的输出引脚连接到电机(图 6.31)。

图 6.31. 两个电机驱动器各有两个输入引脚和两个输出引脚。

由于电机两个输入引脚上的不同逻辑电平组合,加上内部 H 桥开关和电路,每个电机都会发生不同的事情。对于每个电机,一个输入引脚可以用来控制方向,而第二个输入引脚可以用来控制电机的速度。让我们看看这可能是什么样子,检查第一个电机是如何控制的(第二个电机以相同方式控制)。

控制电机方向

我们将使用引脚 1A(SN54410 上的物理引脚 2)来控制第一个电机的方向(图 6.32)。

图 6.32。第一个电机控制器:两个输入引脚和两个输出引脚。我们将使用一个输入引脚来控制电机的方向,另一个来控制其速度。

当引脚 1A 设置为 HIGH,引脚 2A 设置为 LOW 时,9V 电流将通过两个输出引脚(1Y,2Y)流动,电机将正向旋转(图 6.33)。

图 6.33。当第一个输入设置为 HIGH,第二个输入设置为 LOW 时,电机将正向旋转。

类似地,当方向引脚 1A 设置为 LOW 时,只要引脚 2A 为 HIGH,电机就会以反向方向旋转(9V 电流反向流动)(图 6.34)。

图 6.34。当第一个输入为 LOW,第二个为 HIGH 时,电机以相反(反向)方向旋转。

换句话说,任何 1A 和 2A 具有相反逻辑电平的时候,电机都会根据引脚 1A 的逻辑电平来供电旋转。但如果两个输入引脚具有相同的逻辑电平,电机中就不会有电流流过(详细说明见表 6.1)。

表 6.1。电机驱动器方向控制表 1
1A(方向引脚)值 2A(速度引脚)值 结果
HIGH LOW 电机以方向 1(向前)旋转
电机中没有电流通过
LOW HIGH 电机以方向 2(向后)旋转
LOW LOW 电机中没有电流通过
控制电机速度

引脚 2A(SN54410 引脚 7)将控制第一个电机的速度,使用 PWM。假设引脚 1A,方向引脚,被设置为 LOW,表示电机旋转方向为反向,同时将 25%占空比的 PWM 应用于引脚 2A,速度引脚(图 6.35)。75%的时间内,1A 和 2A 将具有相同的逻辑电平(LOW/LOW),在此期间电机中没有电流流过。然而,25%的时间内,LOW/HIGH 的组合将允许电流流过,为电机供电。这导致了我们想要的结果:电机以 25%的速度设置反向旋转。

图 6.35。在引脚 1A 上设置方向为 LOW(反向)和引脚 2A 上设置 25%占空比(速度)时,电机将以 25%的时间在反向方向上供电。

同样的,正向方向的速度控制也是如此,但有一个小问题。假设 1A(方向)设置为 HIGH(正向),2A(速度)设置为 25%占空比的 PWM。我们希望电机以 25%的速度正向旋转,但实际上它将以 75%的速度正向旋转(图 6.36)。

图 6.36. 当 1A 引脚设置为 HIGH(正向)且 2A 引脚(速度)设置为 25%占空比时,电机将以 75%的时间在正向方向上供电。

为了解决这个问题,当电机方向为正向时,PWM 信号需要被反转。一些电机驱动器会为你自动处理这个问题,当电机设置为正向旋转时,自动反转 PWM。然而,SN54410 并不这样做,因此我们将在代码中考虑这一点。

完成电机电路

电路的其余部分涉及连接到 Arduino、9V 电机电源和两个电机(图 6.37)。Arduino 和电机的连接应通过顶壳板的中心孔进行。当你完成这些连接时,你可以将顶壳板放在底壳板上,并将 9V 电池放进去,但不要将部件完全组装在一起——你需要先测试电机。

图 6.37. 探索者完成后的电机电路图

使用 Johnny-Five 测试电机

在组装机器人之前,你应该测试电机电路是否按预期工作。在你的工作目录中创建一个名为 motor-test.js 的文件,并将以下代码放入其中。

列表 6.3. 电机驱动器测试驱动
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', function () {
  const motors = new five.Motors([
    { pins: { dir: 12, pwm: 11 }, invertPWM: true },
    { pins: { dir: 4, pwm: 5}, invertPWM: true }
  ]);

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

让我们聚焦于下一个列表中 Johnny-Five Motors对象的实例化。Motors是 Johnny-Five 组件集合类,类似于Leds。它可以同时控制多个Motor组件。

列表 6.4. 实例化电机
const motors = new five.Motors([
  { pins: { dir: 12, pwm: 11 }, invertPWM: true },        *1*
  { pins: { dir: 4, pwm: 5}, invertPWM: true }            *2*
]);
  • 1 第一台电机的选项和引脚

  • 2 第二台电机的选项和引脚

Motors构造函数传递一个包含每个Motor选项的数组。现在每个电机由两个引脚控制,Johnny-Five 需要知道哪个引脚做什么:一个用于方向(dir)和一个用于速度(pwm)。

记住,当电机正向运行时,PWM 信号需要被反转。硬件不会自动这样做,但你可以通过使用invertPWM选项让 Johnny-Five 知道需要这样做,Johnny-Five 会为你完成这个操作。

不要让你的机器人跑掉!

在测试脚本中运行电机之前,将你部分组装好的机器人从地面上抬起,使轮子悬在空中。否则它可能会跑掉!

运行脚本:

$ node motor-test.js

当板和 REPL 初始化后,输入以下命令并按 Enter 键:

> motors.forward(100)

现在看看你的电机。它们是向前缓慢运行吗?有很大可能性其中一个或两个电机正在错误的方向上旋转。没问题。停止脚本,断开电源,并交换电机引线和电机驱动器输出引脚之间的连接,对于任何运行方向错误的电机。然后再次尝试。

完成底盘构建

将小塑料圆珠固定到底板底部,前后各一个。当机器人不移动时,它就靠这些圆珠支撑。你确信电机按预期旋转吗?很好。现在你可以将顶板和底板拼装在一起(图 6.38)。

图 6.38. 从前面看完成的机器人

编写机器人软件

漫游车的软件将允许你使用键盘上的箭头键来控制机器人(并使用空格键停止它)。代码将包括几个脚本(模块)和一个额外的依赖项(npm keypress 模块)。让我们对项目的工 作区域进行一点更合理的组织:

  1. 创建一个名为 rover 的新目录,并 cd 到该新目录。

  2. 运行此命令:npm init --yes。这将使用一些合理的默认值初始化一个 package.json 文件。

  3. 运行此命令:npm install --save johnny-five keypress。这将安装所列的两个模块并将依赖信息保存到 package.json 中。

创建一个名为 Rover.js 的文件(列表 6.5)。此模块将包含一个定义漫游车基本运动的 JavaScript classRover 不了解传递给其构造函数的 Motors 对象中的特定引脚或电机配置;这些细节被抽象掉了。

列表 6.5. Rover
class Rover {                                 *1*
  constructor (motors) {                      *2*
    this.motors = motors;
  }

  forward () {
    console.log('Full speed ahead!');
    this.motors.forward(255);
  }

  backward () {
    console.log('Reverse!');
    this.motors.reverse(255);
  }

  left () {
    console.log('To the left!');
    this.motors[0].reverse(200);               *3*
    this.motors[1].forward(200);               *4*
  }

  right () {
    console.log('To the right!');
    this.motors[0].forward(200);
    this.motors[1].reverse(200);
  }

  stop () {
    this.motors.stop();
    console.log('Stopping motors...');
  }
}

module.exports = Rover;                        *5*
  • 1 使用类语法组织行为可以使代码更易读。

  • 2 构造函数创建对电机的引用(this.motors)。

  • 3 在 Motors 对象中,每个电机都可以用数组表示法访问。

  • 4 转向涉及一个电机向前运行,另一个电机向后运行。

  • 5 导出 Rover 类以供外部使用

现在机器人需要一个接口和一种控制它的方法。创建另一个名为 index.js 的文件,如下所示。

列表 6.6. index.js 的结构
// Require dependencies
const five     = require('johnny-five');
const board    = new five.Board();
const keypress = require('keypress');
const Rover    = require('./Rover');

board.on('ready', function () {
  // 1\. Instantiate Motors
  // 2\. Instantiate Rover, using Motors
  // 3\. Configure `keypress` to generate events on keypresses in the REPL
  // 4\. Listen for `keypress` events and invoke appropriate Rover methods
});

在前一个列表中的步骤 1 和 2 的基础上,让我们在下一个列表中实例化 MotorsRover

列表 6.7. index.js: 设置电机和漫游车
// ...

board.on('ready', function () {
  // 1\. Instantiate motors
  const motors = new five.Motors([
    { pins: { dir: 12, pwm: 11 }, invertPWM: true },           *1*
    { pins: { dir: 4, pwm: 5}, invertPWM: true }               *2*
  ]);

  // 2\. Instantiate Rover, with motors
  const rover = new Rover(motors);                             *3*

  // 3\. Configure `keypress` to generate events on keypresses in the REPL
  // 4\. Listen for `keypress` events and invoke appropriate Rover methods
});
  • 1 左电机细节

  • 2 右电机细节

  • 3 将电机传递给漫游车构造函数

要控制机器人,你需要监听相关的按键以获取转向输入。index.js 中的第三个任务是配置 keypress

列表 6.8. index.js: 设置 keypress
//...
board.on('ready', function () {
  // 1\. Instantiate motors (as before)
  // 2\. Instantiate Rover, with motors (as before)
  // 3\. Configure `keypress` to generate events on keypresses in the REPL
  keypress(process.stdin);                                                 *1*
  process.stdin.setEncoding('utf8');                                       *2*
  // 4\. Listen for `keypress` events and invoke appropriate Rover methods
});
  • 1 告诉 keypress 为 process.stdin(标准输入,通过你的键盘)生成事件

  • 2 明确字符编码。

最后,你需要监听和处理按键。

列表 6.9. index.js: 处理按键
//...
board.on('ready', function () {
  // 1\. Instantiate motors (as before)
  // 2\. Instantiate Rover, with motors (as before)
  // 3\. Configure `keypress` to generate events on keypresses in the REPL
  // 4\. Listen for `keypress` events and invoke appropriate Rover methods
  process.stdin.on('keypress', function (ch, key) {                       *1*

    if (!key) { return; }                                                 *2*

    switch (key.name) {
      case 'q':                                                           *3*
        rover.stop();
        console.log('Bye-bye!');
        process.exit();                                                   *4*
        break;
      case 'up':                                                          *5*
        rover.forward();
        break;
      case 'down':
        rover.backward();
        break;
      case 'left':
        rover.left();
        break;
      case 'right':
        rover.right();
        break;
      case 'space':                                                       *6*
        rover.stop();
        break;
      default:                                                            *7*
        return;
    }
  });
});
  • 1 监听按键事件。在这里你关心的是 key 参数。

  • 2 如果 key 中没有有用的内容,则返回(不执行任何操作)。

  • 3 按下 q 键将退出机器人。首先,停止电机。

  • 4 调用 process.exit() 将终止机器人的进程。

  • 5 ‘up’ 指的是键盘的上箭头(以及‘down’、‘left’等)。

  • 6 可以使用空格键停止机器人。

  • 7 key switch 语句的默认情况是不执行任何操作。

驾驶你的机器人

将你的机器人放置在一个有足够空间移动的地方,然后运行脚本:

$ node index.js

一旦初始化了板子和 REPL,你可以使用箭头键(上、下、左、右)来控制机器人移动,并使用空格键使其停止。

你可能会感到复杂的情绪,既有胜利的喜悦(太好了!第一个机器人!)又有限制(冒险受限于 USB 电缆的长度)。好消息是:随着我们继续前进,你将能够摆脱机器人的束缚,使它们更有趣。

在我们这样做之前,你将通过学习如何处理 串行 数据来完善输入和输出的主要主题。

概述

  • 电磁铁使电机旋转。给电机通电,它会将电能转换为机械能——开始旋转。转动电机的轴会使它充当发电机,将机械能转换为电能。

  • 使用电机构建电路需要额外的注意。电机电路通常结合多个电压,电机电源通常与低电压逻辑电路隔离。

  • 通过反转电机中电流的方向可以反转电机方向,并且可以使用 PWM 控制电机速度。

  • 晶体管,如 MOSFET,可以用作高速开关,使用低电压逻辑来切换高电压电路。

  • 二极管和电容器是两种可以帮助构建更安全电机电路的组件。

  • 二极管只允许电流单向通过它们。一个与电机并联且反向偏置的二极管可以保护电路,充当反激二极管。

  • 电容器存储电荷,可以与组件并联放置,以隔离电路并平滑电压变化。以这种方式使用时,它们被称为去耦电容器。

  • 伺服电机允许进行精确定位,将 PWM 信号转换为角度位置。大多数伺服电机的范围理论上为 180 度,但在廉价伺服电机上可用范围更窄。

  • H-bridge 电路提供了在多个方向上通过负载(电机)导电流的能力,允许你反转电机方向。H-bridge 与其他功能结合,封装在电机驱动 IC 中。

  • 基本巡游机器人结合了微控制器、电机驱动器、齿轮电机、轮子、电源和底盘。

第三部分. 更复杂的项目

本书这一部分通过串行通信和 Node.js 兼容的炫酷 Tessel 2 开发板,提高了难度并开启了新的可能性。

要使用更复杂的传感器和交换更复杂的数据,你需要掌握串行通信的工作原理,这是第七章 的主题。你将有机会实验一些有趣的传感器,包括加速度计、GPS 和指南针。在这个过程中,你将了解异步串行和同步串行的区别,并遇到 I²C 和 SPI 协议。你还将学习焊接技术。

在 第八章 中,你将使用 Tessel 2 开发板摆脱线的束缚,实现无线的项目。你将了解 Tessel,它可以在本地运行 Johnny-Five 脚本,并构建更多更复杂的实验,这些实验越来越多地使用第三方 npm 包。

随着你继续你的冒险,你可能会开始考虑自己要建造的东西。第九章 将介绍如何适应现有硬件并编写你自己的软件支持组件的步骤。你将破解遥控插座开关和 APDS-9960 手势传感器扩展板。

本书这一部分是朝着你的项目更加独立和复杂迈出的一步。你将通过 Tessel 的原生 Node.js 和板载 WiFi 实现对线的独立,并尝试使用串行通信使用更复杂的组件。

第七章. 串行通信

本章涵盖

  • 串行通信是什么,它能做什么,以及它在哪些地方被使用

  • 如何与异步串行组件(如 GPS 模块)一起工作

  • 焊接核心技能简介

  • 同步串行通信的基础以及业余电子爱好者中最流行的协议:SPI 和 I²C

  • 通过组合多个串行设备组件来构建更复杂的项目

对于本章,你需要以下物品:

  • 1 个 Arduino Uno 和 USB 线

  • 1 个 Adafruit Ultimate GPS 扩展板

  • 1 个 Adafruit HMC5883L 磁力计(指南针)扩展板

  • 1 个 Adafruit BMP180 多传感器扩展板

  • 1 个 Adafruit ADXL345 三轴加速度计扩展板

  • 1 个 16x2 并行 LCD 模块,或者可选的 I²C 启用 Grove RGB LCD 模块

  • 1 个旋转电位器(用于并行 LCD)

  • 断开式雄性插针

  • 焊接铁和焊接材料

  • 跳线

  • 2 个半尺寸面包板

在我们迄今为止的实验中,我们已经能够收集一些关于周围世界的有趣但简单的数据,例如温度(图 7.1)或环境光强度。通过监听基本数字信号(高 vs. 低)的变化,我们可以判断是否按下了按钮。

图 7.1. 使用简单的模拟传感器如 TMP36,你可以通过在不同时间点(A、B、C)采样信号的电压来获得单个数据点(温度)的值。

图片

但比单一流传模拟信号能传达的信息要多得多。事实上,许多有趣的数据相当复杂,需要更精细的协调(figure 7.2)。如果你想同时检测三个方向上的物理运动——比如从加速度计?从 GPS 芯片读取信息?精确的指南针坐标和航向?更复杂的数据需要更结构化、更复杂的组件间数字通信方法。

图 7.2. 加速度计、指南针和 GPS 模块等传感器产生的复杂数字数据需要更复杂的数据交换方法。

图片

7.1. 并行和串行通信数字数据

串行通信:这是一个单一的概念,有着无数的表现形式。其核心的单一概念非常简单:串行仅仅意味着信息——数据——是逐比特发送的,一个比特接着一个比特。这与并行通信形成对比,在并行通信中,多个比特同时发送(figure 7.3)。

图 7.3. 在串行通信(顶部)中,逐个比特按顺序发送。相比之下,并行通信(底部)同时传输多个比特。

图片

并行通信的好处可能相当明显:如果你可以打开闸门同时抛出一大批比特,为什么还要一次抛出一个比特呢?确实,并行通信可以非常快。但也有一些需要注意的问题。

实际上,你已经看到了并行通信的一个缺点。还记得第五章中的 LCD 计时器实验[kindle_split_013.html#ch05]吗?那个电路使用了一个并行 LCD 组件。回想一下电路。它的一个特点是需要一大群电线。其中四条电线负责将数据并行发送到 LCD,而这个电路只使用了 LCD 上可能的八个并行数据引脚中的四个。并行的第一个缺点:大量的电线。

并行硬件也比串行硬件需要更多的物理比特和部件。更多的电线导致更昂贵、更复杂的电路——更多的部件意味着有更多可能损坏的东西。

同时控制所有这些并行比特并确保它们同时到达同一地点也很棘手。最终,并行的复杂性可能比它的价值更大。而串行,尽管简单,已经足够快了。串行无处不在:HDMI、USB、以太网。串行是大量电子组件的数据交换方法。那么,让我们来谈谈串行。

7.2. 串行通信的基本原理

虽然串行通信的核心概念并不难理解——一次一位沿着电线传输——但实际上它发生的方式有很多。这些位应该以多快的速度移动?每个数据分组()由多少位组成?错误是如何检测和纠正的?如果有超过两个组件进行通信,数据是如何发送(寻址)到正确的组件的?是否有一个组件(主控)负责多个连接的组件?

在同一串行通信通道(或总线)上对话的所有组件,需要就如何进行对话达成一致(图 7.4)——它们需要使用相同的协议。有大量的串行协议,可能会让人感到不知所措。好消息是,在业余电子组件中只有少数几种是常用的。掌握基本异步串行、I²C,也许还有 SPI 的概念,你就能处于良好的状态。如果你特别谨慎,请注意,几乎所有低级复杂性都可以通过像 Johnny-Five 这样的库为你抽象出来。

图 7.4. 为了使用串行通信交换数据,设备需要知道如何相互交谈。

7.3. 异步串行通信

当人们提到串行而没有进一步说明时,他们通常指的是异步串行通信——两个设备在它们之间进行(TX)和(RX)数据传输(图 7.5)。然而,即使是这个简单的设置也需要一些规则。

图 7.5. 异步串行数据交换:两个连接的设备中的每一个都可以将数据(TX)发送到另一个组件,并从另一个组件接收(RX)数据。注意,一个设备的 TX 连接到另一个设备的 RX,反之亦然。像 Adafruit 的 GPS 扩展板这样的组件就是使用异步串行通信来传输数据的例子。

异步串行之所以被称为异步,是因为组件之间没有共享的管理时钟信号。每个设备都必须是自己时间的守护者。这没问题,但组件之间必须就每单位时间内发送多少位达成共识。换句话说,每个组件需要知道一个数据位将花费多长时间——例如,信号保持 HIGH 多长时间来表示单个 1 位值?(如果没有这个信息,接收器如何能够区分两个连续的 1 位和一个单一的、长时间的 1 位?)。位速度通常以每秒比特数(bps)或波特率来表示。

但等等,还有更多!数据不是以无尽的 1 和 0 的流发送的。相反,数据被封装在简短的段中,称为。发送的每个数据帧都由几个部分组成:数据本身(5-9 位),还有一个起始位,一个停止位(或两个),以及可能(但不常)一个奇偶校验位来帮助检测错误(图 7.6)。

图 7.6. 异步串行数据帧。一个起始位后跟 5-9 位数据位。可能使用奇偶校验位进行错误检测。然后一个或两个停止位表示帧的结束。

每个异步协议配置定义了其数据帧的具体结构以及波特率。例如,9600/8N1,这是一个常见的协议配置,表示数据速率为 9600 波特,8 位数据块,无奇偶校验位,一个停止位(图 7.7)。

图 7.7. 9600/8N1 的数据帧结构:起始位,8 个数据位,无奇偶校验位,一个停止位

Firmata,串行,以及 Johnny-Five 的工作方式的一些更多内容

Johnny-Five 的架构在暴露的、高级的 JS API 组件和实际写入和从连接组件读取数据的底层 I/O 实现之间做出了明确的区分。

Johnny-Five 假设兼容的开发板能够执行一组 I/O 操作。例如,Led 类内部的逻辑假设可以执行对引脚的数字写入(将其设置为 HIGH 或 LOW),但它并不关心 如何 执行该数字写入。相反,定义这些操作实际如何进行的责任在于兼容的 I/O 插件

当你实例化一个 Board 对象时,你可以选择声明要使用哪个 I/O 插件。例如,如果你想使用 Johnny-Five 与 Tessel 2 板(我们将在下一章中这样做)一起使用,你可以设置一个 io 选项属性。

列表 7.1. 使用 Johnny-Five 的不同 I/O 插件
const five = require('johnny-five');
const Tessel = require('tessel-io'); // A third-party I/O plugin module
 for J5
const board = new five.Board({
  io: new Tessel() // tell Johnny-Five to use this I/O plugin
});

你可能已经注意到,到目前为止,我们还没有在我们的 Board 实例化中提供 io 选项。如果未设置此选项,Johnny-Five 默认使用 Firmata 进行 I/O。默认的 Firmata I/O 层与一系列 Arduino 板兼容,包括你的 Uno。

当你在主机计算机上运行 Johnny-Five Node.js 脚本时,是 Firmata 将你的应用程序逻辑转换为发送到板子的 I/O 命令。它也在板上作为固件运行,并在相反方向进行转换:将数据从板子发送回主机计算机。

Firmata 的执行是异步串行操作的例子。Firmata 数据以 8 位块(8N1)的形式在主机计算机和 Arduino 板之间以快速波特率传输。当你执行主机计算机上的 Uno 兼容 Johnny-Five Node.js 程序时(“兼容”意味着它使用默认的 Firmata I/O),Firmata 数据将通过连接的 USB 线缆发送和接收。(毕竟,USB 代表通用 串行 总线!)在 Arduino 端,接收和发送串行 Firmata 数据由 Uno 单个板载通用异步接收/发送器 (UART) 处理(图 7.8)。

图 7.8. UART 是一种专门用于异步串行通信和并行到串行转换的硬件。

07fig08_alt.jpg

对于好奇者:尽管 Firmata 协议定义了消息的结构,并且 Firmata 实现处理这些消息的打包和处理,但通过计算机的 USB 端口进行串行数据交换的实际机械部分是由一个名为 node-serialport 的 npm 包支持的。

7.3.1. UARTs

通用异步接收/发送器 (UART) 是一种用于处理异步串行通信的硬件设备 (图 7.8)。UART 可以接收大量的并行数据——比如说,来自几个 I/O 引脚或其他来源的数据——对其进行处理,并以所需的串行协议输出。反之亦然:UART 也可以解码传入的串行数据并将其作为并行数据提供。并行输入,串行输出——反之亦然。UART 可以配置为使用不同类型的异步串行:不同的波特率等。这使得 UART 非常灵活,是很有用的硬件。

在这种情况下,术语 晶体管-晶体管逻辑 (TTL) 指的是通过 UART 进行异步串行通信的使用。在 TTL 中,逻辑 HIGH 被表示为微控制器的 Vcc(在 Uno 的情况下为 +5 V),而 LOW 被表示为 0 V——也就是说,电压范围始终限制在适合手头微控制器的电压范围内。

TTL 的多重含义

术语 晶体管-晶体管逻辑 (TTL) 通常指的是使用晶体管进行逻辑构建数字电路的方法——例如,AND 门、逻辑反相器、XOR 门等。在 TTL 组件中使用的标准电压看起来很熟悉:+5 V 表示逻辑 HIGH,0 V 表示 LOW。TTL 集成电路,尤其是与广受欢迎的德州仪器 7400 系列兼容的集成电路,在 1990 年代被广泛使用。尽管它们已被其他技术取代,用于大多数复杂的批量生产电子产品(例如,互补金属氧化物半导体,简称 CMOS),但它们仍然非常可用,并且对于爱好项目或更简单的嵌入式系统很有用。

可能会让人困惑,术语 TTL 也被应用于一种可以连接到其他设备或电路而无需任何额外翻译或接口的设备或电路。在这种情况下,它表示两个设备之间的信号将使用 HIGH (+5 V) 和 LOW (0 V) 逻辑电平进行逻辑通信。正是这种用法导致了将异步串行通信交替称为 TTL 串行 的惯例。

您的忠实 Arduino Uno 拥有一个 UART(更准确地说,它的 ATmega 328P 微控制器有一个 UART)。当在您的宿主计算机上执行 Johnny-Five 脚本时,Uno 的 UART 正在忙于接收和发送 Firmata 格式的消息。

这里存在潜在的冲突。假设你有一个想要在你的项目中使用的设备组件,它使用异步串行通信数据。但你的 Arduino Uno 的 UART 已经被用于 Firmata 通信所占用,这是为了使 Johnny-Five 程序能够运行。

幸运的是,有办法!当 UART 在硬件中实现时,它执行的工作确实更高效、更快,但也可以在软件中模拟。所谓的软件串行允许你通过那些在硬件级别通常不支持异步串行的微控制器引脚进行通信。软件串行可能会占用处理器资源,并且它的速度不如 UART 快,但它可以足够快并且能完成工作。

7.3.2. 使用 GPS 扩展板尝试软件串行

你需要准备的东西

  • 1 个面包板

  • 1 个 Adafruit Ultimate GPS 扩展板

  • 红色、黑色、黄色和白色跳线

  • Arduino Uno 和 USB 线

这里开始变得有趣——第一次连接 GPS 并看到传入的数据,感觉非常棒。使用 Arduino Uno 读取 GPS 数据过去感觉有点神秘——需要大量复制粘贴 Arduino 代码,并依赖于社区成员编写的低级库。它确实有效,但对于非 C 语言专家来说并不直观。

然而,如今使用 Johnny-Five 与 GPS 交互变得如此流畅,几乎感觉像魔法。你的小 GPS 芯片正在监听卫星!难以相信如此复杂的设备可以用几行清晰的 JavaScript 代码进行控制和采样。技术真酷。

GPS 数据和 NMEA 句子

许多 GPS 芯片输出的数据符合称为 NMEA 0183 的标准。国家海洋电子协会(NMEA)维护此标准,该标准定义了从你可能在海上航行船只上找到的硬件(如声纳、陀螺仪、海上雷达和 GPS)的数据结构。

NMEA 数据以 ASCII 字符形式通过异步串行进行通信——ASCII 字符是 7 位,非常适合数据字节——这些字符被组装成逗号分隔的NMEA 句子。尽管标准确实提到了特定的配置(4800 8N1),但 NMEA 数据可以通过各种配置发送。Adafruit 的 GPS 扩展板默认为 9600 波特率,但一些 GPS 模块可以传输得更快。

样本 NMEA 句子及其一些字段解释。从 GPS 传输的数据以符合 NMEA 标准的逗号分隔 ASCII 格式发送。

Johnny-Five 的GPS类使用软件串行从 GPS 硬件解析 NMEA 句子,并将它们组织成方便的属性,如latitude(纬度)和longitude(经度)。

好消息是,使用 GPS 扩展板构建电路只需要最少的连接。你会发现,辅助电子组件通常已经集成在扩展板上——电容器、电阻器等等——只留下电源和数据连接需要连接。

反过来,你现在需要学习焊接。Adafruit GPS 扩展板需要焊接在引脚上,以便你可以将其插入面包板(图 7.9)。焊接并不难,但像任何新的核心生活技能一样,你可能需要尝试几次才能熟练掌握。

图 7.9. 在你可以在面包板上使用它之前,GPS 需要焊接一些引脚。

07fig09_alt.jpg

7.3.3. 学习焊接!

焊料是一种金属合金,用于在金属碎片之间形成——熔接——永久连接(图 7.10)。焊料的熔点低于你的组件腿和连接,允许它在这些连接周围流动,液态,而不损坏它们。一旦它硬化——这几乎瞬间发生——你就得到了一个永久连接。它有点像导电金属胶水。焊料像电线一样,卷在卷轴上,细长。

图 7.10. 焊料像线一样卷在卷轴上。

07fig10.jpg

为了提供熔化焊料并形成这些连接所需的热量,你需要一个烙铁(图 7.11)。烙铁的价格从 10 美元(美国)到几百美元不等。不出所料,你得到的就是你付出的。便宜的烙铁表现得很便宜,不可调节。但如果你还没有准备好投资一个高质量的模型,一个便宜的烙铁也能很好地完成任务。

图 7.11. 一款普通的廉价烙铁。便宜的那些通常没有温度控制设置——它们只是保持一种热度。

07fig11_alt.jpg

确保你的烙铁有一个支架,当它不在你手中时可以放进去(图 7.12)。你还需要海绵来清洁烙铁尖端,在焊接之间以及给尖端上锡(稍后会有更多介绍)。好的焊接海绵是黄铜的,但普通的家用海绵也行。如果你使用纤维素家用海绵,确保在使用烙铁之前将其弄湿。

图 7.12. 当烙铁不用时,需要一个支架来固定它,并保护周围的东西免受烧毁。

07fig12_alt.jpg

组装你的焊接套件

如果必要的焊接材料清单看起来很繁琐(图 7.13),你可能考虑购买一个“学习焊接”套件,由在线电子产品零售商销售,其中包含你需要的多数物品,还有一个实践项目。

图 7.13. 开始焊接所需的关键物品

07fig13_alt.jpg

传统上,焊锡是由含有高比例铅的合金制成的。正如你可能知道的,铅对人类有害。自 2006 年以来,欧盟通过《有害物质限制指令》(RoHS)限制了电子中使用铅,这几乎消除了制造焊锡中使用铅。无铅焊锡无疑对人类和地球都有好处,但它也稍微有点难以操作——它不像含铅焊锡那样流动顺畅。无铅和含铅焊锡都可用;有些人认为学习使用含铅焊锡比使用无铅焊锡更容易。

你可能还想买一卷焊锡吸球(也称为脱焊线)。这种编织铜线充当化学真空吸尘器,用于去除不良焊点:重新加热焊点,并使用焊锡吸球吸取多余的焊锡。

如何焊接,一步一步来

在开始焊接之前,请完全阅读这些说明,这样你就准备好了,可以大显身手:

  1. 在开始之前,使用三爪夹具(图 7.14)或其它方法来固定你的工作台,并让你的双手自由——将排针插入面包板并将板子放置在适当位置也是一种选择。准备好你预计需要的工具和零件,这样你就不需要在之后手忙脚乱地拿着热焊锡枪了。

    图 7.14。在焊接之前固定你的组件。三爪夹具是完成这项工作的方便工具。

    图片

  2. 插上你的焊锡枪并让它加热几分钟。现在,特别是如果你有一个价格低廉的型号,再等几分钟——也就是说,要有耐心。使用不够热的焊锡枪是令人沮丧和烧焦电路板的原因。

  3. 准备(加锡)你的焊锡枪尖端。在焊锡枪的尖端熔化一点焊锡,然后在你的海绵上擦掉多余的焊锡。这应该在你的焊锡枪上留下一个闪亮的、明亮的尖端。如果你将焊锡接触到焊锡枪的尖端时焊锡不轻易熔化,那么焊锡枪不够热。等待几分钟再试一次。

  4. 用你的主手握住焊锡枪,用另一只手握住焊锡。将热焊锡枪的尖端压在排针和连接垫上大约一秒钟(图 7.15)。

    图 7.15。将焊锡枪的尖端放在金属连接垫和引脚上,以加热它们。

    图片

  5. 保持焊锡枪的位置,并将焊锡的末端涂在接合点的另一侧(图 7.16)。你不会直接用焊锡枪熔化焊锡——焊锡会与加热的引脚和板子连接接触。保持你的焊锡枪在引脚的底部;忽略将焊锡枪向上移动到焊锡的诱惑。

    图 7.16。加热的引脚会使焊锡熔化并围绕接合点流动。

    图片

  6. 焊料会熔化并围绕焊点流动。一旦你得到一个漂亮的火山形状的焊料堆(图 7.17),取下烙铁,哇!你会发现随着时间的推移,焊料似乎自然而然地“想要”流向那个火山形状。

    图 7.17. 一个良好的焊接连接看起来像一个小焊料火山。

焊接并不复杂,但要完全做对可能需要几轮。YouTube 和互联网是你的朋友:有大量的视频和教程教你如何焊接。这是一种如果你看到它在运动中可能更容易理解技能。SparkFun 的“通孔焊接”教程清晰易懂(mng.bz/cv1Z)。

焊接新手常见挑战

就像任何新的物理技能一样,焊接需要一段时间才能掌握。以下是一些可能让你慢下来的常见事情:

  • 你知道烙铁很热,但它似乎“不想”熔化焊料,或者说,“它就是不起作用” 确保你保持烙铁尖端镀锡,也就是说,涂上一点焊料。你可以通过尖端变得闪亮来判断尖端是否镀锡。如果你的烙铁尖端是黑色或哑光纹理,它已经氧化了,你需要重新镀锡。这个过程可能很繁琐:你需要让焊料熔化,这样它才能覆盖尖端,但未镀锡的尖端部分不会引起使焊料正确流动的化学反应。花点时间,耐心,正确地完成这个过程,使用海绵帮助引导焊料并擦掉多余的焊料。最好的办法是防止这种情况发生:在每焊接几个接头后检查烙铁,如果任何地方开始看起来氧化,就使用额外的焊料进行修补。注意,无铅焊料会使你的烙铁氧化得比含铅焊料快得多。

  • 烧黑、变形或烧焦的电路板 如果你不使用过多的热量,焊料不会损坏组件,但很容易因为将烙铁接触到电路板(而不是金属连接),使用过高的温度设置,长时间将烙铁对准组件,等等,而搞砸。通过练习你会变得更好。通常看起来烧焦的电路板或组件仍然可以正常工作,但有时你可能发现你真的烧毁了某个东西。慢慢来,仔细瞄准,并在便宜的组件上尝试你的第一次焊接任务,以防万一。

  • 焊接接头不工作 如果焊料不足(“冷焊”)的接头不会导电——重新插上烙铁再试一次。另一个常见问题是意外使用过多的焊料,并将连接焊接到相邻的连接上,导致电路出现各种问题。过多的焊料可以用焊料吸球清理。

  • 焊锡根本无法粘附到你试图粘附的东西上—— 焊接是一个化学过程。你不能在非金属表面如塑料上焊接,甚至一些金属也不可焊接。

7.3.4. 构建 GPS 电路

一旦将引脚头焊接到 GPS 扩展板上,构建电路就变得简单了。将 GPS 插入面包板。将电源和 GND 分别连接到 Arduino Uno 的 5V 和 GND 引脚。将板的 TX 引脚连接到 Arduino Uno 的 11 号引脚,RX 连接到 10 号引脚(图 7.18)。

图 7.18. GPS 电路布线图

使用 Johnny-Five 读取 GPS 数据

所需的代码简单得令人惊叹。在你的 Johnny-Five 工作目录中,创建一个名为 gps.js 的新文件,并添加以下代码。

列表 7.2. Johnny-Five GPS
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', function () {
  const gps = new five.GPS([11, 10]);           *1*

  board.repl.inject({
    gps: gps                                    *2*
  });
});
  • 1 软件串行仅在这些引脚——并且只有这些引脚——上支持 Uno。

  • 2 使 GPS 对象(gps)在 REPL 中可用

一旦 GPS 板连接到电源,它就会开始尝试获取卫星定位。直到它有定位,你将看到板载 LED 大约每秒闪烁一次。

GPS 内置了天线,但如果没有暴露在广阔的天空区域,它将无法获取卫星定位。很可能会需要你外出。幸运的是,这个要求可能不会太麻烦——拿起你的笔记本电脑、Arduino Uno 和面包板,到户外待几分钟。然而,当我测试这个代码时,外面是 24 华氏度(F)并且下着雪球。或者你可能正在使用台式电脑,这不太可能适合户外使用。

你可能在大窗户旁边,有清晰的视野看到天空时会有所帮助,或者你可能能够使用更长的 USB 线,这样你的 Arduino Uno 和面包板可以放在户外,而你的电脑在室内。

获取一个稳定的(“冷启动”)卫星定位大约需要 30-60 秒。

“但是等等,”你说,“我的手机可以在几秒钟内获取我的 GPS 位置……并且它可以在室内工作。怎么回事?”

你的智能手机使用一些额外的技巧来确定你的位置:它相对于本地手机塔和附近的 WiFi 网络的位置。实际上,为了你的手机能够获得真正的 GPS 锁定——卫星类型的——这可能需要与这些独立芯片一样长或更长的时间——长达几分钟。

一旦你的 GPS 芯片有了卫星定位,板载 LED 将停止快速闪烁,减慢到每 15 秒闪烁一次。你启动 gps.js 脚本的时间无关紧要——在芯片有定位之前或之后——但显然,直到定位稳定之前,位置数据将不可用:

$ node gps.js

假设你已经有了修复方案,现在你可以开始与你的 GPS 交互并记录数据,如下面的列表所示。

列表 7.3. 光敏电阻数据记录输出
1479058386733 Device(s) /dev/cu.usbmodem1411
1479058386761 Connected /dev/cu.usbmodem1411
1479058388413 Repl Initialized
>> gps.latitude
42.38
>>

好吧,太酷了——几乎太简单了!你可以尝试检查你的gps对象上的其他属性,比如longitude。有关你可以做什么的最新信息,请参阅 Johnny-Five GPS 文档(johnny-five.io/api/gps/)。

很好。你已经掌握了使用软件串行实现的基本异步串行连接。让我们看看串行还能做什么。

7.4. 同步串行通信

异步串行简单且有用,但并不适合所有情况。为了考虑到每个组件时钟之间可能存在的小差异,每个数据块都必须被起始位和停止位包围,也许还会加入一个奇偶校验位。这是一个相当大的开销:最佳情况(一个起始位,一个停止位,无奇偶校验),传输 8 位信息需要 10 位。UART 硬件复杂,软件实现较慢且占用处理器资源。在组件之间整理波特率和其他细节可能会很棘手。最后,异步串行实际上只适合让两个设备进行通信。

同步串行协议在数据连接之外还增加了一个共享时钟线,用于同步组件。由于时钟信号决定了数据传输的速度,因此不需要事先配置组件的数据速率。某些同步串行协议还使得在同一通信总线上多个设备之间的通信变得可能且方便。但也存在一些缺点;例如,你即将遇到的 SPI 和 I²C 协议都需要连接的线尽可能短——例如,你不能合理地使用 I²C 通过 20 英尺的电缆进行通信。

7.4.1. 串行外设接口(SPI)

串行外设接口(SPI)是一种同步串行协议,最初由摩托罗拉在 20 世纪 80 年代开发。它允许一个“主”设备控制并协调自身与一个或多个“从”设备之间的数据交换。对于一对一的主从连接,可以使用最少的三个线(图 7.19),但如果有多于一个的从设备,则需要四条或更多的线(图 7.20)。

图 7.19. 在一个简单的 SPI 配置中——一个主设备,一个从设备——数据交换可以使用最少的三个线:一个由主设备管理的共享时钟线(SCK)、主设备输出从设备输入(MOSI)和主设备输入从设备输出(MISO)。

图 7.19 的替代图片

图 7.20. 当添加更多设备时,至少需要一条额外的线——从设备选择(SS),尽管某些 SPI 配置可能需要为每个从设备一个 SS 线。

图 7.20 的替代图片

SPI 最大的优点是它允许实现真正快速的数据传输速率。接收硬件也比 UART 简单得多。SD 卡存储卡是 SPI 协议在实际应用中的流行例子。

SPI 组件对业余爱好者来说非常容易获得。例如,Johnny-Five 支持某些 SPI 气压计。此外,我们将在第十章(kindle_split_020.html#ch10)中使用具有 SPI 接口的 LCD 模块(诺基亚 5110)。但 SPI 组件并不像 I²C 这样的主流协议那么常见。这就是我们将关注的焦点。

关于术语的补充说明

我并不喜欢使用过时的主从术语来描述电子和计算机科学中的某些隐喻。但在串行协议中,这种术语却无处不在——例如,它在像MOSIMISO这样的缩写中根深蒂固。因此,我这里不情愿地使用主从语言,否则可能会导致混淆。

注意,在这些关于不同串行配置的讨论中,开发板上的串行硬件扮演着“主”的角色,而连接的设备则是“从”。

7.4.2. I²C

集成电路间通信(通常写作I²C,发音为“I-squared-C”)是由飞利浦半导体(现在是恩智浦)创建的一种同步串行协议。尽管它的速度并不比 SPI 快,但 I²C 通过灵活性和简单性来弥补这一点——只需两条线(图 7.21)!

图 7.21. 无论连接了多少个设备,使用 I²C 时始终只有两条线:SCL(时钟)和 SDA(数据)。

图 7.21

在 I²C 中,你可以在单条总线上连接多达,哦,大约一千个设备(如果你使用 10 位寻址,则是 1008 个;更常见的 7 位寻址允许最多 127 个)。这意味着你回到了异步串行的连接简单性——最少的线缆——但你得到了同步串行的好处——多个设备和无需预先配置。

所有连接组件的数据都通过单一的 SDA(串行数据)线传输。第二条线是 SCL(串行时钟),它像 SDA 一样被所有连接组件共享。将每个设备连接到 SDA 和 SCL 线(并给它们一些电源!),你就可以开始了。

I²C 设备如何相互通信

因为通常有很多 I²C 组件连接在同一个总线上,所以 I²C 消息必须更加结构化,并且需要某种机制来控制交通。总线上的一个或多个设备拥有这种权限。活动的主设备(总线上可以有多个主设备,但任何给定时间只能有一个主设备处于活动状态)生成时钟信号,并告知其他设备何时发送或接收数据。

为了开始这个过程,活动的主设备首先发送一个地址帧(以下图中左侧的第一个帧)以确定哪个连接的从设备需要执行后续操作。

图 0199

I²C 帧的高级结构

总线上的每个连接设备都有自己的、唯一的地址。通常,这是一个 7 位地址,完整的(8 位)地址帧由 7 位地址位后跟一个 R/W(读/写)位组成。R/W 位指定指示的设备应该读取还是写入数据。然后,被寻址的设备预期会发送一个单 ACK 位来指示所有系统正常工作。然后可以发送数据帧,每个帧后面跟着一个 ACK 位。所有这些都是在共享时钟信号的协调下进行的。

协议细节的输入输出稍微复杂一些——例如,你需要使用 10 位地址来发挥 1008 设备总线的最大作用(7 位数字不超过 127,每个设备都需要一个唯一的地址),而读取从从设备的过程的精确机制需要识别诸如从哪个内存寄存器读取等问题。如果你想了解更多,我推荐 Sparkfun 的 “I²C” 教程 (learn.sparkfun.com/tutorials/i2c)。

许多 I²C 设备带有硬件定义的地址。这意味着你不能更改设备的地址:如果你有另一个具有相同(硬连线)地址的设备,你将无法将它们放在同一个总线上。但有些设备具有可配置的地址。

7.4.3. 使用 I²C 磁力计制作数字罗盘

你需要准备的东西

  • 1 个 Arduino Uno 和 USB 线

  • 1 个 HMC5883L 磁力计(罗盘)5 V 兼容的解析板,例如 Adafruit 的

  • 1 个半尺寸面包板

  • 红色、黑色、黄色和白色跳线

Honeywell HMC5883L 是一种流行的 I²C 三轴 磁力计 芯片——即罗盘——可在 Adafruit 的 5 V 兼容解析板上找到。确定你的方向只需片刻,结合 Johnny-Five 的 Compass 类和一个以 Honeywell 芯片为中心的解析板。

你将再次需要练习你的焊接技能——解析板需要在将其插入面包板之前焊接到引脚头上 (图 7.22)。

图 7.22. 罗盘解析板,就像 GPS 解析板一样,需要焊接到引脚头上。

注意操作电压!

HMC5883L 解析板可以从各种电子分销商处获得,但请确保你选择的板子可以承受 5 V 的电压。许多板子,包括 Sparkfun 的变体,都是为 3.3 V 逻辑电压而设计的。将 3.3 V 罗盘连接到 Arduino 的 5 V 电源和引脚输出可能会烧毁芯片。Adafruit 的 HMC5883L 解析板有一个板载 电源调节器,这使得它可以从 3–5 V DC 供电——它是一个所谓的 “5 V 安全” 组件。尽管有方法使 3.3 V 组件在 5 V 板(如 Uno)上安全工作,但这需要一些额外的步骤和硬件,所以现在请确保你有一个 5 V 兼容的解析板。

与 GPS 解析板一样,一旦焊好引脚,这个电路的布线就很简单了:将解析板的 SDA 引脚连接到 Uno 的 A4 引脚,将 SCL 连接到 Uno 的 A5 引脚。将 VIN 连接到 Arduino 的 5 V 电源,并将地线引脚连接到 GND (图 7.23)。

图 7.23. 罗盘布线图

Arduino Uno 上的 I²C 硬件支持

注意,这个电路的引脚编号不是随意的:Uno 的 A4 和 A5 引脚支持硬件 I²C;其他引脚不支持。Uno 的 A4 引脚提供 SDA;A5 是 SCL。

Johnny-Five 的Compass类支持多种不同的芯片,因此在实例化时,你需要让它知道使用哪个特定的控制器;参见列表 7.4。Johnny-Five Compass文档标识了支持的组件(johnny-five.io/api/compass/)。你不需要定义罗盘连接到的引脚,因为连接 I²C 设备到 Uno 板只有一种方式(引脚 A4 和 A5)。

列表 7.4. 使用特定控制器实例化Compass
const compass = new five.Compass({ controller: 'HMC5883L' });

使用controller选项来区分不同支持的硬件是 Johnny-Five 组件类中的一种常见模式。你第一次在第四章中看到它,与 TMP36 模拟温度传感器一起。

在你的 Johnny-Five 工作目录中,创建一个名为 compass.js 的文件,如下所示。

列表 7.5. compass.js:使用 Johnny-Five 读取 HMC5883L 数据
const five = require('johnny-five');
const board = new five.Board();

board.on('ready', () => {
  const compass = new five.Compass({ controller: 'HMC5883L' });      *1*
  compass.on('change', () => {                                       *2*
    console.log(compass.bearing);                                    *3*
  });
});
  • 1 为 HMC588L 芯片实例化罗盘

  • 2 罗盘对象实例,像大多数其他 J5 组件对象一样,有一个 change 事件。

  • 3 罗盘属性是一个包含罗盘方向信息的对象。

现在运行脚本:

$ node compass.js

当板子和 REPL 初始化后,你会在终端看到如下输出:

$ node compass.js
1479310483561 Device(s) /dev/cu.usbmodem1411
1479310483570 Connected /dev/cu.usbmodem1411
1479310485230 Repl Initialized
>> { name: 'East', abbr: 'E', low: 84.38, high: 95.62, heading: 94 }
{ name: 'East', abbr: 'E', low: 84.38, high: 95.62, heading: 91 }

每个正在记录的bearing对象包含几个属性(abbrheading等)。如果这个数字罗盘有一个输出,它会更加出色——我们现在就来做。

7.5. 汇总:摇动切换多传感器小部件

这个下一个实验将几个 I²C 传感器组合在一起,并在 LCD 上显示输出。这里传感器足够多,一次性在 LCD 上显示所有输出比较困难,所以我们将会使用加速度计来创建一个摇动切换显示功能。通过摇动设备,你可以切换在 LCD 上显示的数据:一个显示温度和压力数据的屏幕,或者一个显示当前罗盘方向的屏幕。

A4 和 A5 是 Arduino Uno 上唯一的 I²C 兼容引脚,但这完全没问题——I²C 协议允许在单个总线上连接大量设备。也就是说,你可以只用这两个引脚来控制多个 I²C 设备。

你需要准备的东西

note.jpg

  • 1 Arduino Uno 和 USB 线

  • 1 HMC5883L(罗盘)5 V-friendly 分线板,例如 Adafruit 的

  • 1 16x2 LCD:并行或 Johnny-Five 兼容的 I²C LCD,例如 Grove 兼容的 JHD1313M1

  • Adafruit 5 V-ready BMP180 分线板(I²C 温度、大气压力传感器)

  • Adafruit ADXL345 三轴加速度计

  • 跳线

  • 2 半尺寸面包板或 1 个全尺寸面包板

混合和匹配传感器

这个实验的代码和接线细节基于供应清单中提到的物品,但你也可以混合使用其他 Johnny-Five 支持的 I²C 传感器:

  • 其他“多”传感器(结合温度、压力、高度计,有时还有湿度):BME 280、BMP 280、HTU21D 等。有关更多支持的 I²C 设备,请参阅 Johnny-Five 的“多”API 页面:johnny-five.io/api/multi/

  • 其他加速度计。有关更多支持的 I²C 加速度计,请参阅 Johnny-Five 的“加速度计”API 页面:johnny-five.io/api/accelerometer/

当然,你需要将代码示例修改为适用于你选择的硬件的正确控制器。

注意,这些传感器中的大多数都安装在需要你焊接引脚的扩展板上。

如果你手头有 Johnny-Five 支持的 I²C LCD,如 Grove RGB LCD 模块,那么你很幸运:连接 I²C LCD 非常简单。在这个电路中,并行 LCD 也可以正常工作,但正如你所知,它需要更多的电线。

7.5.1. 第 1 步:将指南针与 LCD 输出结合

图 7.24 中的接线图显示了如何在两个面包板上定位指南针和 LCD。(随着你向这个项目中添加传感器,单个面包板上将没有足够的空间。)

为什么在这个实验中不使用 GPS?

在这个多传感器项目中使用 GPS 模块似乎是个不错的想法。然而,Johnny-Five 对软件串行的支持相对较新,实现软件串行的一些复杂性意味着很难预测传感器将要传输多少数据(数据交换在这一点上不像 I²C 那样结构化)。正因为如此,在撰写本文时,将 GPS 与其他传感器或输出结合可能会引起 LCD 显示的垃圾数据问题和其他不可预测的行为。

图 7.24. 接线图,第 1 步。带有指南针扩展板的面包板上的电线排列是为了为以后要添加的传感器留出空间。

配置磁偏角以获得更好的精度

我喜欢用指南针方向做的一件事是,将其校正为我的本地磁偏角。指南针总是给出相对于磁北的读数,而磁北相对于真北(地理北)的位置因你所在地球表面的位置而异。在我的地区,真北大约比磁北西 14 度——也就是说,这里的磁偏角为-14.28 度。你可以通过访问www.magnetic-declination.com来找到你的磁偏角——我们将在设备的代码中使用它。

构建多传感器代码

创建一个名为 multi-sensor.js 的脚本,并从以下配置代码开始。

列表 7.6. multi-sensor.js:配置
const five           = require('johnny-five');

const DECLINATION    = -14.28;                   *1*
const UPDATE_FREQ_MS = 1000;                     *2*
var lastDisplay    = null;                       *3*

const board          = new five.Board();
  • 1 您当地的磁偏角:这应该是从www.magnetic-declination.com获取的值。

  • 2 更新 LCD 显示的频率

  • 3 跟踪 LCD 上最后显示的内容

接下来,添加一个辅助函数来校正罗盘读取的本地偏差。以下偏差校正函数中的算术将确保返回的值是一个有效的 0 到 360 度之间的度数测量值。

列表 7.7. multi-sensor.js: 校正偏差
function correctForDeclination (heading, declination) {                   *1*
  var corrected = heading + declination; // Recall: declination may be negative
  corrected += 360;                                                       *2*
  while (corrected >= 360) {                                              *3*
    corrected -= 360;
  }
  return corrected;
}
  • 1 此函数校正一个(数字)航向的本地偏差。

  • 2 确保校正后的值是正数

  • 3 从校正值中减去 360(度)的单位,直到它小于 360

现在,一个格式化辅助函数用于格式化从项目传感器中读取的 readings。在这个第一轮中,如下所示,它将格式化 heading 属性——我们很快会用罗盘的数据填充它。

列表 7.8. multi-sensor.js: 显示罗盘航向的格式
function formatDisplay (readings) {                                *1*
  var displayLine1, displayLine2;
  displayLine1 = 'HEADING: ';
  displayLine2 = Math.round(readings.heading) + ':circle:';        *2*
  return [displayLine1, displayLine2];
}
  • 1 读取一个读取对象,并返回在 LCD 的两条线上显示的内容

  • 2 :circle: 是一个可以在 LCD 上显示的特殊字符。

现在,让我们完成这一轮代码。

列表 7.9. multi-sensor.js: 显示罗盘航向
/* ... */
function correctForDeclination (heading, declination) { /** ... **/ }
function formatDisplay (readings) { /** ... **/ }

board.on('ready', () => {
  const compass        = new five.Compass({ controller: 'HMC5883L' });
  const lcd            = new five.LCD({ pins: [7, 8, 9, 5, 6, 12] });     *1*

  lcd.useChar('circle');                                                  *2*

  function update () {
    var display = formatDisplay({
      heading: correctForDeclination(compass.heading, DECLINATION),       *3*
    });
    if (!lastDisplay || (lastDisplay.join('') != display.join(''))) {     *4*
      lcd.clear();
      lcd.cursor(0, 0).print(display[0]);
      lcd.cursor(1, 0).print(display[1]);
      lastDisplay = display;                                              *5*
    }
  }

  board.loop(UPDATE_FREQ_MS, update);                                     *6*
});
  • 1 实例化一个并行 LCD

  • 2 告诉 LCD 使用特殊的圆圈字符(作为度数标记)

  • 3 校正罗盘读取的偏差,并将结果填充到 readings.heading 中

  • 4 显示值是否已更改?如果是,则更新 LCD。

  • 5 跟踪最后显示的内容

  • 6 设置一个循环,每 UPDATE_FREQ_MS 毫秒(本例中为 1000 毫秒)调用一次更新

通过每秒只检查一次数据(board.loop 频率)并且只有在数据发生变化时才重新打印到 LCD,可以提高性能并避免过度的 LCD 闪烁。

尝试一下:

$ node multi-sensor.js

你应该在 LCD 上看到罗盘航向——调整了磁偏差并四舍五入到最接近的整数度。它应该每秒更新一次,因为罗盘被重新定位。

7.5.2. 第 2 步:将多传感器添加到设备中

Johnny-Five 的 Multi 组件类旨在用于像 BMP180 这样的设备,这些设备将多个传感器组合成一个包。图 7.25 展示了 Adafruit 的 BMP180 分线板,它包含温度和大气压力传感器。像 GPS 和罗盘一样,BMP180 板在使用面包板之前需要焊接在引脚头上。

图 7.25. Adafruit 的 BMP180 分线板

一个 Multi 对象作为一个容器,用于各种传感器组件,并允许你以协调的方式与之交互。每个包含的传感器都映射到其适当的 J5 组件类。例如,以下实例

const multi          = new five.Multi({ controller: 'BMP180' });

将包含以下实例属性:

  • thermometer—一个指向 Johnny-Five Thermometer 实例的温度传感器

  • barometer—一个指向 Johnny-Five Barometer 实例的压力传感器

BMP180 使用 I²C 进行通信,这意味着你可以将其连接到已经由罗盘使用的共享 SDA 和 SCL 线。图 7.26)。

图 7.26. 添加 BMP180 扩展板到电路的布线图

7.5.3. 步骤 3:更新显示以显示温度和压力

同时显示温度、大气压力和罗盘方向会占用 LCD 有限的 16x2 字符空间。如果我们将显示分成两个屏幕,会更好:一个用于显示温度和压力,另一个用于显示罗盘方向。在我们的第一个版本中,将无法在两个屏幕之间切换——它只会显示温度和压力——但我们将在几分钟后回到这一点。

首先,在文件顶部附近添加一个额外的变量:

var altDisplay     = false;

这是一种简单的方法,帮助程序确定显示哪个屏幕——温度和压力,还是罗盘方向。

现在,更新显示格式化函数,如以下列表所示。

列表 7.10. multi-sensor.js:更新显示格式化
function formatDisplay (readings, altDisplay) {                           *1*
  var displayLine1, displayLine2;
  if (altDisplay) {                                                       *2*
    displayLine1 = 'HEADING: ';
    displayLine2 = Math.round(readings.heading) + ':circle:';
  } else {
    displayLine1 = 'TEMP/PRESSURE:';
    displayLine2 = readings.temperature.toFixed(1) + ':circle:F';
    displayLine2 += ' / ' + Math.round(10 * readings.pressure) + 'mb';    *3*
  }
  return [displayLine1, displayLine2];
}
  • 1 更新以接受 altDisplay 参数(布尔值)。

  • 2 此分支将在此代码版本中永远不会执行(它始终为假)。

  • 3 压力单位为 kPa;乘以 10 并四舍五入以获得更熟悉的毫巴

接下来,更新板的 ready 回调:实例化一个 Multi 传感器并将更多属性传递给显示格式化函数,如以下列表所示。

列表 7.11. multi-sensor.js:更新板的 ready 回调
board.on('ready', () => {
  const compass        = new five.Compass({ controller: 'HMC5883L' });
  const lcd            = new five.LCD({ controller: 'JHD1313M1' });
  const multi          = new five.Multi({ controller: 'BMP180' });        *1*

  lcd.useChar('circle');

  function update () {
    var display = formatDisplay({
      temperature: multi.thermometer.F,                                   *2*
      heading    : correctForDeclination(compass.heading, DECLINATION),
      pressure   : multi.barometer.pressure                               *3*
    },
    altDisplay);                                                          *4*
    if (!lastDisplay || (lastDisplay.join('') != display.join(''))) {
      lcd.clear();
      lcd.cursor(0, 0).print(display[0]);
      lcd.cursor(1, 0).print(display[1]);
      lastDisplay = display;
    }
  }

  board.loop(UPDATE_FREQ_MS, update);
});
  • 1 为 BMP180 实例化一个 Multi 对象

  • 2 从 BMP180 的温度值添加一个温度属性,单位为华氏度

  • 3 从 BMP180 的压力传感器添加一个压力属性

  • 4 传递(目前始终为假)的 altDisplay 值

现在尝试以下操作:

$ node multi-sensor.js

你应该在 LCD 屏幕上看到当前的温度和压力(但不是罗盘方向)。

7.5.4. 步骤 4:使用加速度计添加摇动切换显示功能

加速度计测量加速度和方向变化。通过监控加速度计的加速度——超过 1 G 的力——你可以创建一个摇动以更改显示的功能。

Adafruit 的 ADXL345 三轴加速度计扩展板(图 7.27)是一个 5 V 兼容的加速度计组件,提供 I²C 和 SPI 接口。尽管该板上的连接支持两种串行协议,但 Johnny-Five 对其的支持仅使用 I²C 接口(仅限)。

图 7.27. Adafruit 的 ADXL345 三轴加速度计扩展板。它可以连接到 I²C 总线(通过 SDA 和 SCL 引脚)或者,如图中所示,一些引脚作为双重功能使用,一个四线 SPI 设置(通过 CS、SDO、SDA 和 SCL 引脚)。

将加速度计添加到电路中(图 7.28),一旦它焊接在排针上,连接到面包板上的共享 SDA 和 SCL 行。

图 7.28. 添加加速度计到电路的布线图

Johnny-Five 加速度计类提供了对加速度计的支持(你猜对了,J5 组件类可能开始感觉过时了)。

为了实现摇动交换显示功能,我们将绑定到加速度计实例的加速度事件,该事件在设备加速度读数发生变化时触发,并查看它是否超过了一个大致的阈值,这表明有快速的摇动动作。

编辑以下代码。

首先,在文件顶部附近添加额外的变量来设置摇动阈值(以 G 为单位)并跟踪最后一次检测到的摇动,用于去抖动:

const SHAKE_THRESHOLD = 1.15;
var lastJiggleTime = null;

检测和去抖动“摇动”

一个加速度计对象的加速度事件会在加速度读数变化时触发。在粗略、非科学的实验中,我发现应该注册为摇动的合理阈值大约是 1.15 G。如果你发现这太敏感或需要太剧烈的摇动,你可以在代码中调整阈值。

真实的摇动动作会在几次设备读数期间产生波动加速度值。也就是说,在短时间内,会连续触发多个加速度事件,多次读取,读数超过阈值。如果我们每次加速度计报告的加速度超过阈值时都切换显示,那么显示会疯狂地来回闪烁。

相反,我们需要对交换进行去抖动,防止它过于频繁地发生。我们可以通过跟踪最后一次显示交换的时间,并确保我们不会比每秒一次更频繁地交换它来实现这一点。

ready回调函数内部,实例化加速度计:

const accel          = new five.Accelerometer({ controller: 'ADXL345' });

ready回调函数内部,添加一个加速度事件的处理程序,如下所示。

列表 7.12. multi-sensor.js: 加速度处理程序
accel.on('acceleration', () => {
  if (accel.acceleration > SHAKE_THRESHOLD) {                           *1*
    var jiggleTime = Date.now();
    if (!lastJiggleTime || jiggleTime > (lastJiggleTime + 1000)) {      *2*
      altDisplay     = !altDisplay;                                     *3*
      lastJiggleTime = Date.now();                                      *4*
      update();                                                         *5*
    }
  }
});
  • 1 如果当前加速度属性的值超过 G 阈值...

  • 2 如果在过去 1000 毫秒内没有交换显示...

  • 3 翻转 altDisplay 的值。

  • 4 记录交换发生的时间。

  • 5 更新显示。

完成了!运行。

$ node multi-sensor.js

初始时,你应该在 LCD 上看到温度和压力信息显示。拿起带有传感器的面包板,给它一个快速的摇动:你应该看到 LCD 切换到罗盘航向显示。

到现在为止,你可能已经开始注意到房间里有一头大象。你刚刚构建了一个类似定向仪的设备...它通过电缆连接到你的笔记本电脑或计算机。没有主机机器上的计算能力,Arduino 及其附加组件是无能为力的——这就是 Node.js 进程执行的地方。当然,这对于穿越森林的远足来说是一个非常荒谬的设置。同样,你在第六章中构建的机器人只能在其 USB 电缆允许的范围内移动。

是时候摆脱束缚了。在下一章中,我们将开始解放我们的项目,让它们自由发展。

替换 I²C LCD:一个案例研究

组合传感器电路的布线很拥挤,需要两个面包板。

如果你手头有一个 I²C 兼容的 LCD 显示器(我就是这样),你可以用那个代替并行显示器。我使用了一个 Grove 兼容的 I²C LCD(零件编号,JHD1313M1)。Grove 是由 Seeed Studio 制造的组件系统,所有组件都共享相同的连接器——但是,唉,这些连接器与引脚或面包板不兼容。你可以购买连接器适配器,但在我这个例子中,我进行了一些家庭式手术:我切掉了连接器,并将四根多股线(VCC、GND、SCL 和 SDA,就像任何其他 I²C 设备一样)焊接到了更容易插入面包板的实心连接线上。

结果电路释放了大量 Uno 引脚,整体上更加简单。I²C 是减少您需要支持的项目的组件的物理开发板 I/O 引脚数量的理想协议。

使用 Grove I²C LCD 组件的布线图。它可以与其他连接的传感器共享相同的 SDA 和 SCL 线。

对定向代码的唯一更改是在 LCD 的实例化上。而不是这样,

const lcd = new five.LCD({ pins: [7, 8, 9, 5, 6, 12] });

使用这个:

const lcd = new five.LCD({controller: 'JHD1313M1'});

摘要

  • 虽然简单的模拟信号足以满足基本传感器数据,但读取更高级组件的数据需要一种更结构化的信息交换方式。

  • 并行通信在本质上很快,但可能会很麻烦。串行通信——一次一个比特——是业余电子学中交换数据的首选方法。

  • 异步串行很常见,但需要两个设备事先配置协议特定设置,并且限制为两个设备。

  • TTL(晶体管-晶体管逻辑)是开发板上使用的异步串行类型。UART 硬件使得在板上实现快速 TTL 串行成为可能,但这种类型的串行通信也可以在软件中模拟(软件串行)。

  • 同步串行协议在通信总线上增加了一条共享时钟线。SPI 是一种需要三到四条(数据线和时钟线)线的协议;更受欢迎的 I²C 协议只需要两条线。

  • 可以将串行组件组合起来制作有趣且功能强大的项目。尽管只有两个设备可以通过异步(TTL)串行连接交换数据,但可以将许多设备添加到相同的 SPI 或 I²C 总线上,从而更有效地使用开发板的 I/O 引脚。

  • I²C 可以在一个总线上支持多达 1008 个设备(使用 10 位地址)。总线上的每个设备都需要一个唯一的地址。

  • 串行数据交换的支持通常在硬件级别提供(软件串行仿真是明显的例外)。在开发板上,这通常取决于微控制器或处理器提供的串行支持。

  • 串行连接仅在某些引脚上可用。对于 Arduino Uno,I²C 支持在引脚 A4(SDA)和 A5(SCL)上,SPI 在引脚 10(SS)、11(MOSI)、12(MISO)和 13(SCK)上。

第八章. 无线项目

本章涵盖

  • 为什么业余电子项目需要电线,以及如何摆脱它们

  • 在不同平台上使用 Johnny-Five 与不同的 I/O 插件

  • 配置和使用 Tessel 2 开发平台

  • 适应 3.3 V 逻辑电平和不同的引脚配置

  • 无线部署代码到 Tessel 2

  • 利用 Node.js 和 npm 生态系统为 Tessel 2 创建更复杂的软件

  • 使用电池让 Tessel 2 完全摆脱电线

工具图片

对于本章,你需要以下物品:

  • 1 个 Tessel 2 开发板

  • 1 根 USB A 到 USB micro 线

  • 1 个标准 LED 灯,任何颜色

  • 1 个 100 V 电阻

  • 1 个 Adafruit BMP180 多传感器扩展板

  • 以下任何一个:

    • 3 个公头引脚

    • 2 根 22 号规格、实心导线

    • 9 V 电池和夹子

  • 焊锡铁和配件

  • 1 个 USB 5 V 电源适配器或类似设备,为 Tessel 2 供电

  • 1 个 USB 电池(有时称为移动电源

  • 跳线

  • 1 个半尺寸面包板

  • 从第六章(kindle_split_014.html#ch06)中的巡游机器人(电机驱动电路和底盘)

0215fig01_alt.jpg

我们正在构建的东西变得越来越复杂和强大,但有一个限制因素:它们物理上连接到计算机。这些基于 Arduino Uno 的 Johnny-Five 项目缺乏物理独立性——它们完全依赖于主机计算机进行逻辑指令和电源。对于某些类型的项目,这种束缚的主机-客户端设置并不是问题。但为了进一步发展,为了创建 JavaScript 控制的、自主的、自包含的项目——为了摆脱电线——你需要扩大你的硬件视野。

Arduino Uno 上使用的 ATmega328P 微控制器过于受限,无法运行完整的操作系统或原生执行 JavaScript。如果你想用 JavaScript 控制 Uno,你必须使用外部且更强大的东西——一个主机——来代表板执行 JavaScript。

在主机上,JavaScript 程序中的逻辑需要转换为受限制的微控制器可以理解的指令。然后,这些指令需要传达给 Uno,它充当一个薄的客户端。同样,来自 Uno 的数据——传感器读数等——需要传达回主机进行处理(图 8.1)。

图 8.1. 这段来自第一章(kindle_split_008.html#ch01)的代码概述了自动风扇小部件的理论主机-客户端通信。在你迄今为止的实验中,Arduino Uno 充当了一个薄的客户端,而你的计算机作为主机。

08fig01_alt.jpg

在 Johnny-Five 中,格式化和交换主机和客户端之间的指令和数据是I/O 层的工作。

Arduino,包括 Uno,是 Johnny-Five 的特殊案例:Firmata 提供了 I/O 层,并在 Johnny-Five 中默认可用。也就是说,当您实例化一个Board对象时,除非您告诉它否则(使用选项),Firmata(通过 USB)将用于 I/O。您在之前的章节中一直在使用这种默认的 I/O。

8.1. 为什么您一直处于连接状态

到目前为止,您一直使用物理 USB 连接线为您的项目供电,因为 USB 连接提供了 I/O 交换和电源供应的双重功能。要断开连接,您需要以不同的方式满足这些要求——I/O 和电源,因此让我们深入了解内部的工作原理。

8.1.1. 数据交换、I/O 层和 I/O 插件

虽然 Johnny-Five 定义了 API 和组件的逻辑行为,但数据指令如何在 Johnny-Five 应用程序和硬件之间交换则留给I/O 层。这就是 Johnny-Five 保持平台无关性的方式:它将这些细节留给兼容的I/O 插件

到目前为止,您还没有使用任何 Johnny-Five I/O 插件,因为常见的 Arduino,包括 Uno,是 Johnny-Five 的特殊案例。当您实例化Board对象而不指定要使用的 I/O 时,Johnny-Five 默认通过 USB 使用 Firmata——这就是为什么它“直接工作”的原因。与其他平台的 I/O 插件不同,它们必须单独安装,Firmata 与 Johnny-Five 一起提供(技术上讲:它是一个依赖项)。这可能会让人感觉 Firmata 和琐碎的 I/O 细节是 Johnny-Five 代码的一部分,但实际上并非如此。当您在其他平台上尝试 Johnny-Five 时,您会看到您必须安装适当的 I/O 插件。关于这一点,稍后会有更多介绍。

在您迄今为止的 Arduino Uno 主机-客户端 Johnny-Five 设置中,USB 电缆充当了交换串行 Firmata 格式消息的脐带。这就是您一直处于物理连接状态的一个原因。

8.1.2. USB 作为电源

项目中的电路需要某种电源。您一直使用的 USB 连接提供稳定的 5V 电源。除了少数需要比开发板能提供的更多电流或电压的感应电路——如电机和伺服机构——之外,您一直依赖 USB 电缆供电以及数据交换。

开发板的直流电源

Arduino Uno 内置了一个直流圆筒形插孔,您可以使用它将 Arduino 连接到壁挂式直流适配器。这种“壁挂式电源适配器”无处不在,它将基于墙面的交流电转换为许多电子设备所需的直流电。

早期的壁挂式电源适配器具有圆筒形插孔,可以插入从答录机到您自己的开发板(如 Uno)的任何设备。

![0218fig01_alt.jpg]

Uno 有一个直流输入圆筒形插孔用于供电

大多数开发板(包括 Uno)都有内置的 电压调节器 用于其电源输入,这意味着可以通过直流电源为 Uno 提供从 9 V 到超过 20 V 的电压,并且它会将其调节到所需的 5 V。

电压调节器需要提供比其目标输出电压更高的电压。就像 LED 一样,调节器本身存在正向电压降——板上的功率调节电路消耗了一些输入电压。在 Uno 的例子中,为了得到稳定的 5 V,至少需要 7 V 的输入电压。

任何具有中心正极性、5.5 mm x 2.5 mm 插头(最常见的尺寸)的 9 V–12 V DC 适配器都适用于为 Uno 提供电力。

此符号表示电源适配器具有中心正极性。也就是说,插头尖端具有正极性,而套筒是负极。大多数直流电源适配器都具有中心正极性——这正是你为 Uno 和通常其他开发板供电所需要的那种。

并非所有适配器上都打印有极性符号。这很遗憾,因为你不能假设某个适配器具有中心正极性——没有默认值,尽管中心正极性更为常见。

我手头备有直流电源适配器和 5 V USB 充电器,以便在项目中使用。这些适配器早已从它们原始的消费电子产品中分离出来,在为各种原型和项目提供电力方面变得非常方便。

8.1.3. 无线项目通信选项

正如你所见,项目使用线缆有两个主要原因:数据交换和电力。我们将稍后解决电力问题,但首先让我们看看创建不需要物理连接数据连接的项目的方法。

无线主机-客户端设置

某些具有受限微控制器的开发板提供了无线通信功能,如 WiFi 或蓝牙。

执行 JavaScript 逻辑需要一个主机,但主机和客户端之间可以通过无线方式交换数据和指令——从而消除了物理 USB 数据连接的需求(图 8.2)。

图 8.2. 与 Uno 一样,这些板具有受限的微控制器,但两者都可以无线通信。

例如,Blend Micro 板可以通过使用 blend-micro-io I/O 插件和板上的 BLEFirmata 固件无线地与 Johnny-Five 一起使用。你将在 第十二章 中遇到另一个具有 BLE 功能的小型板,Espruino 的 Puck.js。

嵌入式 JavaScript

另一类开发板具有从受限到中等复杂性的微控制器,并针对运行 JavaScript、JavaScript 的某些子集或类似 JavaScript 的代码进行了优化。这些板上的微控制器不适合运行完整的操作系统,但往往具有高效能、低成本和小型化——所有这些都是嵌入式系统的理想特性。

工作流程因平台而异。在某些情况下,您编写的 JavaScript 可能会被编译成其他东西(更底层和高效的代码)然后再被烧录到设备上。在其他情况下,板上的微控制器可能能够直接执行 JavaScript 的某些子集——这意味着您使用 JavaScript 编写代码,但您可能无法访问每种语言的所有功能(图 8.3)。我们将在第十章中重新探讨受限的嵌入式 JavaScript 平台。

图 8.3. 这类设备将受限的微控制器与优化的 JavaScript 和类似 JavaScript 的运行时版本相结合。我们将在第十章中探讨 Pico 和 Element。

图片

使用微型计算机(SBC)控制客户端

单板计算机(SBC)能够运行真正的多任务操作系统,并在处理能力和板载外围设备支持方面提供多种选择。它们可以做很多事情,包括运行 Node.js,但相应地,它们比其他嵌入式项目开发板更耗电、更昂贵,体积也更大。

在单板计算机(SBC)中,计算机和开发板之间的界限可能变得模糊:一些,如流行的 Raspberry Pi 系列,将通用计算功能与类似开发板的 I/O 结合在一块板上。这算作嵌入式逻辑执行,还是作为主机-客户端设置的微型变体(Pi 的处理器作为主机来控制 I/O)?事情变得更加复杂,因为您可以使用 Raspberry Pi 控制连接到 Pi 自身 USB 端口的一个 Arduino——一个微型主机-客户端设置。我们将在第十一章中探讨更多 SBC。

8.2. 使用 Tessel 2 实现无线的项目

Tessel 2 (tessel.io/) 是一个以 Node.js 和 npm 软件包管理器为中心的开源开发平台(硬件和软件都是开源的)。除了您已经习惯的基本 I/O 类型——数字、模拟、PWM、I²C 等等——Tessel 2 还提供了一些高级外围设备,如 USB 端口、以太网,以及——欢呼!——WiFi。(Tessel 2 是目前唯一可用的 Tessel 型号,所以我会通常直接称之为 Tessel。)

图 8.4. Tessel 2 开源开发板

图片

Tessel 是一个有趣且实用的硬件设备。根据“能够运行真实操作系统”的标准,它将属于 SBC 类别的设备——它预装了 OpenWrt,这是一个常见于路由器的 Linux 发行版。

但开发工作流程更像是主机-客户端设置和嵌入式 JavaScript 设备:你在电脑上编写代码,并将其部署到 Tessel 上,而不是在 Tessel 上编写代码。尽管 Tessel 运行 OpenWrt 并附带一些我们将要检查的实用软件,但它比像 Raspberry Pi 这样的 SBC 平台更受限制。它只有 64 MB 的 RAM 和 32 MB 的 Flash 空间用于程序。这比基于 ATmega328P 的板如 Uno 上可用的空间大得多,但与典型的桌面计算机相比,它不在同一个水平上。

在使用 Johnny-Five 为 Uno 开发项目后,你会发现 Tessel 的许多人体工程学方面都很熟悉。确实,如果你习惯于使用 Node.js 进行开发,那么与 Tessel 一起工作的机制会感觉非常熟悉。

Tessel 与 Arduino Uno 之间有一个重要的区别需要注意:Tessel 在 3.3 V 下运行,而 Uno 是 5 V。

Tessel 2 是 3.3 V

Tessel 2 在 3.3 V 下运行。你需要考虑到这一点来设计电路,并确保使用 3.3 V 兼容的组件。将 5 V 组件或电源插入 Tessel 2 的引脚可能会损坏电路。不用担心:我们会随着进展逐步介绍细节。

8.3. 设置你的 Tessel

你需要准备的东西

![note.jpg]

  • 1 个 Tessel 2

  • 1 条 USB 线:USB A 到 USB micro

要让 Tessel 准备好冒险,你需要用 USB 线将其连接到你的笔记本电脑上,就像与 Uno 一样。但是有一些很大的不同。首先,你将要编写的 JavaScript 代码将上传到 Tessel 本身并运行——Tessel 不需要主机来为它思考。此外,Tessel 有 WiFi,所以一旦设置好,你就不需要一直将其插入 USB——你可以无线部署到它上面。

Tessel 和 Node.js LTS

Tessel 支持 Node.js 的长期支持(LTS)版本,截至写作时是 6.11(到你阅读时,它可能已经大大前进)。本书中的代码示例将假设至少是 6.11 版本,示例脚本将使用该版本中可用的语言功能。比当前 LTS 版本更新的 Node.js 版本可能不与 Tessel 兼容。

注意,Tessel 上运行的 Node.js 版本可能与你系统的 Node.js 版本不同(并且更旧)。一旦你的 Tessel 配置完成,你可以使用t2 version命令查看它正在运行的 Node.js 版本以及固件版本(稍后将有更多关于t2工具的介绍)。

8.3.1. 配置 Tessel

以下说明可能会让设置步骤感觉比较长,但在大多数情况下这个过程只需要几分钟。

步骤 1:安装 CLI

安装用于从你的电脑控制 Tessel 的命令行界面(CLI)。这应该作为一个全局 npm 包安装,你需要在终端中输入以下命令:

$ npm install -g t2-cli

模块安装完成后,您将能够在终端中使用 t2 命令来控制您的 Tessel。

t2t2-cli

npm 模块名为 t2-cli,但它提供的命令是 t2

第 2 步:连接、查找并重命名您的 Tessel

将您的 Tessel 上的 USB 微型端口连接到电脑上的 USB 端口。这将给 Tessel 提供电源,您应该会看到 LED 闪烁(这需要大约 30 秒)。

使用以下命令在终端中查找 Tessel:

$ t2 list

您应该看到类似以下的内容。

列表 8.1. t2 list
$ t2 list
INFO Searching for nearby Tessels...
     USB    Tessel-02A397D5D8A4

哈哈!但 Tessel-02A397D5D8A4 并不是很有吸引力。幸运的是,使用此命令重命名您的 Tessel 简单得就像做饼一样:

t2 rename <name>

我决定将我的 Tessel 命名为 sweetbirch。您可以选择自己的名字。

列表 8.2. t2 rename
$ t2 rename sweetbirch
INFO Looking for your Tessel...
INFO Connected to Tessel-02A397D5D8A4.
INFO Renaming Tessel-02A397D5D8A4 to sweetbirch
INFO Changed name of device Tessel-02A397D5D8A4 to sweetbirch
第 3 步:将 Tessel 连接到 WiFi 并配置

使用以下命令将您的 Tessel 连接到 WiFi 网络:

$ t2 wifi -n <network-name> -p <password>
Tessel 的 WiFi 兼容性

通常,使用单个命令就可以轻松地将 Tessel 连接到典型的 2.4 GHz 家庭 WiFi 网络。但请注意,Tessels 目前还不兼容 5 GHz 网络。如果您遇到问题,请访问 Tessel 的 WiFi 连接设置页面 (tessel.github.io/t2-start/wifi.html) 获取更多信息。

最后,配置 Tessel 以便您可以通过 WiFi 从您的电脑发送代码到它:

$ t2 provision

配置后的 t2 list 命令输出应显示 Tessel 既可以通过 USB 连接,也可以通过 WiFi(局域网)可用,如以下列表所示。

列表 8.3. 配置后的 t2 list
$ t2 list
INFO Searching for nearby Tessels...
     USB    sweetbirch
     LAN    sweetbirch
第 4 步:更新您的 Tessel

Tessel 的固件会不时发布更新。请确保您拥有最新版本,通过运行以下命令:

$ t2 update

如果有可用的更新,此过程可能需要几分钟。

确保您的 Tessel 仍然连接到 USB 并已启动。要在您的 Tessel 上运行 LED 闪烁代码,首先按照以下列表初始化项目。

列表 8.4. 初始化闪烁 LED Tessel 项目
$ t2 init
INFO Initializing new Tessel project for JavaScript...
INFO Created ".npmrc".
INFO Created ".tesselinclude".
INFO Created "package.json".

t2 init 命令

t2-cli npm 包的新版本要求您在将项目代码部署到您的 Tessel 之前,在项目目录中运行 t2 init 命令。您只需这样做一次(每个项目一次)。如果您忘记运行 t2 init,您将收到一条有用的消息:

$ t2 run index.js
INFO Looking for your Tessel...
INFO Connected to sweetbirch.
WARN This project is missing an ".npmrc" file!
WARN To prepare your project for deployment, use the command:
WARN
WARN t2 init
WARN
WARN Once complete, retry:
WARN
WARN t2 run index.js

8.3.2. “Hello World” LED 在 Tessel 上闪烁

您可以直接使用 JavaScript 控制 Tessel,这正是 Tessel 的用途。创建一个名为 hello-tessel.js 的文件,并在其中添加以下列表中的 LED 闪烁代码。此脚本将使 Tessel 的内置板载 LED 闪烁。

列表 8.5. hello-tessel.js
const tessel = require('tessel');     *1*

tessel.led[2].on();                   *2*

setInterval(function () {
  tessel.led[2].toggle();
  tessel.led[3].toggle();
}, 100);                              *3*

console.log("I'm blinking! (Press CTRL + C to stop)");
  • 1 导入 tessel 硬件接口

  • 2 首先打开板载 LED 之一

  • 3 每 100 毫秒切换 LED

现在,你可以使用t2 run <file>命令在 Tessel 上运行 LED 闪烁代码,如下所示。在代码部署并开始运行后,你应该看到 Tessel 的两个板载 LED 快速闪烁。

列表 8.6. t2 run
$ t2 run hello-tessel.js --lan
INFO Looking for your Tessel...
INFO Connected to sweetbirch.
INFO Building project.
INFO Writing project to RAM on sweetbirch (89.088 kB)...
INFO Deployed.
INFO Running hello-tessel.js...
I'm blinking! (Press CTRL + C to stop)
--lan标志

Johnny-Five 脚本如果通过 LAN(WiFi)连接部署到 REPL(使用console.log())会表现得更好。t2 run命令的--lan标志指定应该使用 WiFi 连接(而不是有线usb连接)。

理解“Hello Tessel”示例

在 hello-tessel.js 代码中有些看起来熟悉的东西,但一些细节是 Tessel 特有的(与使用 Johnny-Five 控制 Uno 相比)。

代码中的第一件事是引入一个名为tessel的库的要求:

var tessel = require('tessel');

留意观察者可能会注意到这一点很奇怪:你从未使用npm install安装过这样的库,或者以其他方式使其在你的电脑上的代码中可用。Tessel 2 硬件 API 文档(mng.bz/Ror5)解释了这个require语句:

当你在 Tessel 2 上执行的脚本中require('tessel')时,这会加载一个与 Tessel 2 硬件接口的库,包括引脚、端口和 LED...

这里一个重要的部分是短语“在 Tessel 2 上执行”。这是真的:如果你尝试在自己的电脑上用 Node.js 运行 hello-tessel.js 脚本,你不会走得太远——你会遇到一个关于缺少模块的错误。你也不会在 npm 上找到tessel包。相反,tessel是一个预安装在 Tessel 板上的 JavaScript API 库,因此对在 Tessel 2 上运行的脚本可用。

通过tessel对象开启和切换 Tessel 板上的 LED 灯,看起来有点像使用 Johnny-Five 的Led对象,但又不完全一样:

tessel.led[2].on();
tessel.led[2].toggle();
tessel.led[3].toggle();

tessel对象让你可以访问 Tessel 板上的端口和引脚,暴露了一个硬件 API 来与之交互。这是一个比 Johnny-Five 更底层的 API:你可以读取和写入数字、模拟和串行数据,但没有更高层次的对象来封装,比如温度传感器。Tessel 2 硬件 API 文档提供了更多相关信息(mng.bz/Ror5)。

8.3.3. 使用 Tessel 闪烁外部 LED

你需要

![note.jpg]

  • 1 个 Tessel 2

  • 1 个 USB 到 USB-micro 线缆

  • 1 个标准 LED

  • 1 个 100 V 电阻

  • 跳线

  • 1 个全尺寸面包板

按照古老的闪烁 LED“Hello World”传统,让我们进一步深入 Tessel,使用这个经典的第一个电路。这个例子不是闪烁板载 LED,而是使用外部连接的 LED。虽然这个实验范围有限,但它会介绍你如何进行 Tessel 项目的工作流程和结构。

Tessel 项目结构

与你用 Uno 编写的单次、独立脚本不同,你将把每个 Tessel 实验结构化为一个合适的 Node.js 项目:

  • 每个实验都将有自己的工作目录。

  • 你将使用 package.json 文件来管理依赖项。

  • 实验的主要(入口)脚本将命名为 index.js。

如果你熟悉使用 Node.js 编写软件,这个结构看起来会很熟悉。

设置 Johnny-Five Tessel 项目

在编写任何代码之前,设置项目的工作区域。你需要创建一个目录并使用 npm init 命令设置 package.json 文件。打开终端并执行以下命令:

mkdir t2-blink
cd t2-blink
npm init -y

运行 npm init 通常会引导你通过一系列交互式问题来设置项目。添加 -y 标志将跳过这些步骤并使用默认设置创建 package.json 文件。如果你不关心项目的特定配置细节,这是一个更快开始的方式。一旦你运行了 npm init,你将有一个 package.json 文件。

连接 3.3 V LED 电路

虽然你之前已经构建了一个基本的 LED 闪烁电路,但为了调整 Tessel 的 3.3 V 供电电压,需要进行一些算术运算。电路与你在 第三章 中构建的 Uno 的基本 LED 电路相似(图 8.5),但你将需要一个不同的限流电阻来适应 Tessel 的 3.3 V 逻辑。

图 8.5. Uno 的原始基本 LED 电路。Uno 工作电压为 5.5 V,而 Tessel 是 3.3 V 设备。电路需要调整以适应 Tessel 的不同电压。

图片

在 第三章 中,你通过考虑 Uno 的工作电压(5 V)和 LED 正向电压的近似值(约 1.8 V)来计算串联电路中单个 LED 所需的限流电阻。你得到了以下结果:

5.0 V (supply voltage)
– 1.8 V (red LED forward voltage)
--------------------------------
= 3.2 V ("remaining" voltage in the circuit)

为 LED 设定 20 mA 的电流,然后你使用了欧姆定律:

Resistance (R) = Voltage (V) / Current (I)
            R  = 3.2 V        / 0.02 A
------------------------------------------
               = 160 *V*

160 V 不是一个常见的电阻值,所以你将其四舍五入到 220 V(这是一个常见值)。

调整 Tessel 上的 3.3 V 供电值:

3.3 V (supply voltage)
– 1.8 V (red LED forward voltage)
--------------------------------
= 1.5 V ("remaining" voltage in the circuit)

Resistance (R) = Voltage (V) / Current (I)
            R  = 1.5 V        / 0.02 A
------------------------------------------
               = 75 *V*

75 V 不是一个常见的电阻值,但 100 V 是——一个 100 V 的电阻在这里就足够了!在 5 V 电路中,所需的电阻值较低是有道理的:LED 电压降后剩余的电压较少。

按照 Tessel LED 连接图(图 8.6)所示连接电路。

图 8.6. 将 LED 的阳极连接到 Tessel 的端口 A,引脚 5,将阴极连接到端口 A 的 GND 连接。

图片

编写 LED 闪烁代码

在 第二章 中,使用 Johnny-Five 为 Arduino Uno 编写的闪烁 LED 实验使用了以下代码。

列表 8.7. led.js
const five = require('johnny-five');
const board = new five.Board();        *1*

board.on('ready', () => {              *2*
  const led = new five.Led('13');      *3*
  led.blink(500);                      *4*
});
  • 1 使用无非默认选项的 Board 对象实例化

  • 2 当板子准备好...

  • 3 实例化一个连接到引脚 13 的 Led 对象

  • 4 闪烁 LED

要使此代码与 Tessel 一起工作,不需要做太多更改。你需要考虑两件事:

  • Tessel 需要不同的 I/O 层支持才能与 Johnny-Five 协同工作。您需要确保您的Board对象使用tessel-io进行 I/O(而不是默认的firmata)。

  • 需要更新的 LED 连接到的引脚编号。

首先在您的t2-blink工作目录中运行以下命令:

$ npm install --save johnny-five tessel-io

这将本地安装johnny-fivetessel-io包,并将依赖项保存到 package.json 文件中。您已经熟悉johnny-five包。tessel-io是 Johnny-Five 为 Tessel 提供的 I/O 插件。

在同一目录下创建一个名为 index.js 的文件,并添加以下代码。

列表 8.8. index.js
const five = require('johnny-five');
const Tessel = require('tessel-io');          *1*
const board = new five.Board({
  io: new Tessel()                            *2*
});

board.on('ready', () => {
  const led = new five.Led('A5');             *3*
  led.blink(500);
});
  • 1 需要 tessel-io 包

  • 2 告诉板使用 tessel-io 进行 I/O

  • 3 LED 连接到 Tessel 的端口 A,引脚 5。

这段代码与 Uno 兼容代码非常相似。唯一的不同是 I/O 插件和引脚编号。请使用t2 run命令尝试运行它:

$ t2 run index.js

您应该看到类似于以下列表的输出,并且 LED 应该开始闪烁。

列表 8.9. 在 Tessel 上运行 LED 闪烁代码
$ t2 run index.js
INFO Looking for your Tessel...
INFO Connected to sweetbirch.
INFO Building project.
INFO Writing project to RAM on sweetbirch (746.496 kB)...
INFO Deployed.
INFO Running index.js...
1484926245789 Device(s) Tessel 2 (Tessel-02A397D5D8A4)
1484926245946 Connected Tessel 2 (Tessel-02A397D5D8A4)
1484926246003 Repl Initialized
>>

8.3.4. 探索 Tessel 的引脚和功能

与 Arduino Uno 类似,Tessel 上的不同引脚执行不同的功能。Tessel 上有 16 个 I/O 引脚,分布在两个端口之间(图 8.7)。

图 8.7. Tessel 的引脚分为两个“端口”,每个端口有 8 个 I/O 引脚。

引脚编号为 A0–A7(在端口 A 上)和 B0–B7(在端口 B 上)。每个物理端口顶部两个引脚提供电源连接(GND 和 3.3 V)。其他功能,如 I²C、SPI 和 UART(异步/TTL 串行),在特定引脚上得到支持(图 8.8)。

图 8.8. Tessel 2 引脚及其功能。任何编号的引脚都可以用作数字 I/O。

尽管所有引脚都可以用作数字 I/O 引脚,但模拟输入(ADC)在引脚 A4 和 A7 以及所有端口 B 引脚上可用。您还可以看到一些引脚支持中断(这意味着它们可以用于“监听”按钮按下或其他需要准确检测从低电平到高电平或反之的变动的应用)。一些引脚支持 PWM,但并非所有引脚都支持。

引脚 B7 有一个我们尚未遇到的功能:模拟输出。该引脚可以提供数字到模拟转换(DAC)。我们不会直接探索这个功能,但在 Tessel 上它是可用的。

8.4. Tessel 上的无线路由项目

嘿,等等!LED 示例仍然需要连接。接下来,您将开始移除一些电线。

首先,您需要将 Tessel 从您的计算机上断开连接,但使用电源插座供电。

您需要准备

  • 1 Tessel 2

  • 5 V–12 V USB 充电器

  • 1 LED

  • 1 100 V 电阻

  • 1 半尺寸面包板

| |

Tessel 的墙电源

如果您的 Tessel 没有附带 USB 直流适配器,您可以使用 5 V USB 充电器(如平板电脑或手机充电器)为 Tessel 供电。

与 Arduino Uno 的直流输入插孔一样,Tessel 的 USB 微型连接也具有电源调节器,可以将输入电压调节到所需的 3.3 V。标准的 5 V(USB 电压)正好适合输入电压。

提供直流 5 V 电压的 USB 墙上适配器很常见。它们特别受欢迎,因为手机和平板电脑的充电器。

只是为了证明你可以做到,你将稍微修改 LED 闪烁代码,使其 LED 闪烁。你可以这样做,因为 LED 连接到 Tessel 上的一个 PWM 功能引脚。

要进行此更改,请在 index.js 中找到以下行:

led.blink(500);

用这个替换它:

led.pulse(500);

现在,将你的 Tessel 插入任何你喜欢的电源插座,只要它在你电脑所在的同一 WiFi 网络范围内。在尝试向其部署代码之前,请给它一些时间来启动。

现在,回到你的电脑上,你可以尝试以稍微不同的方式部署。在你的工作目录中,执行以下命令:

t2 push index.js

t2 push 命令与 t2 run 命令不同。使用 t2 run 时,Node.js 进程将在 Tessel 上执行,直到主机计算机终止该进程(通常通过 Ctrl-C)。使用 t2 push 时,程序将被烧录到 Tessel 上,并且只要 Tessel 有电就会一直运行。如果你拔掉 Tessel 并再次插上,它将恢复执行程序。

现在,你有一个在某处闪烁的 LED,但这并不特别令人兴奋。是时候构建一个功能更强大、可以连接到 Tessel 上 Node.js 生态系统的项目了。你可以将 Tessel 用作独立的气象站:从一个或多个传感器读取数据,并通过运行本地 Web 服务器来提供这些数据(图 8.9)。

图 8.9. Tessel 作为迷你气象站的神经中枢。运行在 Tessel 上的 Node.js 应用程序将读取 BMP180 多传感器数据,并运行 Web 服务器,以便同一 WiFi 网络上的其他计算机可以在 Web 浏览器中查看天气信息。

8.4.1. 无线数据:远程气象站

你需要

  • 1 个 Tessel 2

  • 1 根 USB-micro 到 USB-A 线缆

  • 1 个 5 V–12 V USB 充电器

  • 1 个 Adafruit BMP180 I²C 多传感器

  • 跳线

  • 1 个半尺寸面包板

在 第七章 中使用的 Adafruit I²C 传感器有一个很酷的功能:它们在 5 V 和 3.3 V 下都很高兴。它们的分线板每个都包含电压调节器硬件,可以处理电平转换——也就是说,它们可以处理 3.3 V 和 5 V 微控制器的不同逻辑电平电压。你可以像使用 Arduino Uno 一样轻松地使用这些传感器。

电路很简单:将 BMP180 I²C 传感器按照 图 8.10 所示连接到 Tessel。

图 8.10. 将 BMP180 I²C 传感器连接到电源(A 端口的 GND 和 3.3 V 引脚)、SCL(A0)和 SDA(A1)。

在你的终端应用程序中,设置一个新的工作项目目录,并像 LED 示例一样安装依赖项:

mkdir t2-weather
cd t2-weather
npm init -y
npm install --save johnny-five tessel-io

创建一个名为 index.js 的文件并添加以下代码。

列表 8.10. index.js
const five = require('johnny-five');
const Tessel = require('tessel-io');

const board = new five.Board({
  io: new Tessel()                                         *1*
});

board.on('ready', () => {
  const weatherSensor = new five.Multi({
    controller: 'BMP180'
  });
  weatherSensor.thermometer.on('change', function () {     *2*
    console.log(this.F);                                   *3*
  });
});
  • 1 不要忘记指定 tessel-io 作为 I/O 层。

  • 2 当传感器的温度实例上触发变化事件时...

  • 3 ...记录当前的华氏温度

与之前的 LED-闪烁代码一样,将 I/O 抽象化到 Johnny-Five 核心组件中使得这段代码看起来几乎与你在 Arduino Uno 上使用 I²C 传感器的方式相同。实际上,这里唯一的区别在于创建 Board 对象时传递了不同的 I/O 插件选项。你不需要与引脚号打交道,因为 board 已经知道 Tessel 上哪些引脚支持 I²C(并且它将默认使用端口 A)。

上述代码通过 weatherSensor 访问 thermometer 实例。一个用于控制器 BMP180Multi 对象实例包含 thermometerbarometer 属性,这些属性是代表 BMP180 的温度和压力传感器的组件类实例的引用。每个多传感器组件独立生成事件,事件也会汇总到 Multi 实例中:

weatherSensor.on('change', () => {
  // This will get invoked any time ANY of the multi component's sensors
  // have a change
});

weatherSensor.barometer.on('change', () => {
  // This gets invoked only when the barometer's reading changes
});

Johnny-Five(如 ButtonsLeds)中的其他 collection 组件类以类似的方式运行。

由于目前你的 Tessel 通过 USB 连接,使用 t2 run 在 Tessel 上尝试运行脚本:

$ t2 run index.js --lan

你应该看到类似这样的内容:

$ t2 run index.js --lan
INFO Looking for your Tessel...
INFO Connected to sweetbirch.
INFO Building project.
INFO Writing project to RAM on sweetbirch (440.832 kB)...
INFO Deployed.
INFO Running index.js...
1487876926840 Device(s) Tessel 2 (sweetbirch)
1487876926995 Connected Tessel 2 (sweetbirch)
1487876927051 Repl Initialized
>> 72.14
72.32
72.14
72.32

要使用传感器的读数做一些有用的事情——一些更直观和可视化的东西——你将利用 Tessel 支持 npm 包和执行更复杂的 Node.js 代码的能力——让我们看看如何构建一个网络应用程序。

在 Tessel 上构建更复杂的应用程序

要从 Tessel 服务器上提供网页内容,你需要创建一些额外的文件。至少,你需要从一个基本的 HTML 文档开始。

在 t2-weather 目录内创建一个名为“app”的目录。在该目录内创建一个 index.html 文件,如下面的目录结构所示。

列表 8.11. 到目前为止的项目目录和文件结构
t2-weather/
 app
 index.html
 index.js
 node_modules
 package.json

打开 index.html 并添加以下内容。

列表 8.12. index.html
<!DOCTYPE html>
<html lang="en">
 <head>
  <title>Current Conditions</title>
 </head>
 <body>
  <h1>Current Conditions</h1>
  <p>Data coming soon!</p>
 </body>
</html>

当你在 Tessel 上执行代码时,Tessel 会知道复制并使用你正在运行的脚本以及为项目安装的 Node.js 模块。但是,如果你需要使用这些项目之外的资源,你需要明确地告诉它。你可以通过在你的项目根目录中放置一个 .tesselinclude 文件来实现这一点。

创建一个 .tesselinclude 文件并添加以下内容:

app/

.tesselinclude 中的每一行都是一个 glob,一个用于匹配文件的模式。例如,app/ 匹配 app 目录中的所有文件。这将确保当 Tessel 部署时,它会复制 app 目录中的所有文件。

添加 .tesselinclude 之后,你的文件结构应该看起来像这样。

列表 8.13. 包含 .tesselinclude 的项目目录和文件结构
t2-weather/
 .tesselinclude
 app
 index.html
 index.js
 node_modules
 package.json

接下来,你将分两步构建一个 Web 应用程序来显示气象站数据:

  1. 你将通过将 Express (expressjs.com/) Web 应用程序框架与内置的 Node.js 模块相结合来设置一个基本的静态 Web 服务器。这将提供基本的网页,它将成为气象站数据的容器。

  2. 你将在 Tessel(即网络服务器)上设置一个 socket.IO 服务器,并从客户端(从浏览器中运行的 JavaScript)连接到它。你还将使用一些 CSS 和结构化标记来美化 HTML。

设置静态 Web 服务器

你将通过在你的应用程序代码中启动一个基本的静态 Web 服务器来开始简单。在这里,“静态”意味着 Web 服务器将交付资产(如 HTML、图像、JavaScript 等),而不会对它们执行任何动态的服务器端处理——它只会从指定的目录中交付请求的文件。

首先,安装 express Web 框架。确保你处于 t2-weather 目录中,并在终端中执行以下命令:

$ npm install --save express

现在回到 index.js 脚本,将以下代码添加到文件顶部。

列表 8.14. index.js
const five = require('johnny-five');
const Tessel = require('tessel-io');
const express = require('express');                          *1*

const path = require('path');                                *2*
const http = require('http');
const os = require('os');

var app = express();                                         *3*
app.use(express.static(path.join(__dirname, '/app')));       *4*
var server = new http.Server(app);                           *5*

const board = new five.Board({
  io: new Tessel()
});

board.on('ready', () => {
  const weatherSensor = new five.Multi({
    controller: 'BMP180'
  });                                                       *6*
  server.listen(3000, () => {                               *7*
    console.log(`http://${os.networkInterfaces().wlan0[0].address}:3000`);
  });
});
  • 1 需要 Express

  • 2 需要一些内置的 Node.js 模块

  • 3 实例化一个新的 Express 应用程序

  • 4 告诉应用程序从 app/ 目录中提供静态资产

  • 5 创建一个 HTTP 服务器并将应用程序传递给它

  • 6 从上一个版本中省略或注释掉 console.log 行

  • 7 使服务器在端口 3000 上监听请求

由于它们内置在 Node.js 中,因此没有必要使用 npm install 安装 ospathhttp 模块。

尝试一下!

$ t2 run index.js --lan

你应该看到以下结果。

列表 8.15. 运行 index.js
$ t2 run index.js
INFO Looking for your Tessel...
INFO Connected to sweetbirch.
INFO Building project.
INFO Writing project to RAM on sweetbirch (933.376 kB)...      *1*
INFO Deployed.
INFO Running index.js...
1487884455497 Device(s) Tessel 2 (sweetbirch)
1487884455635 Connected Tessel 2 (sweetbirch)
1487884455691 Repl Initialized
>> http://192.168.1.16:3000                                    *2*
  • 1 随着你添加更多的依赖项,应用程序正在变得更大。

  • 2 这是 Tessel 的内部地址,并且正在积极监听 HTTP 端口。

你现在可以打开一个 Web 浏览器并查看控制台日志中记录的 URL(确保你使用输出中显示的 URL——而不是前面示例输出中显示的 URL)。

Tessel 的 IP 地址

为了使这个实验工作,你的 Tessel 需要连接到与你的计算机相同的网络。这可能是 WiFi 网络,如设置部分中讨论的那样,尽管 Tessel 也有一个以太网端口。

连接到网络会导致 Tessel 被分配自己的 IP 地址,你需要知道这个地址才能访问其上运行的 Web 服务器。在 index.js 代码中的以下行方便地显示了这一点:

console.log(`http://${os.networkInterfaces().wlan0[0].address}:3000`);

你可以将终端应用程序中的输出复制并粘贴到浏览器的 URL 框中。在浏览器中访问输出 URL 将渲染当前的 index.html 文件 (图 8.11)。

图 8.11. 没有什么花哨的!在浏览器中导航到 Tessel 的 IP 地址和端口会显示 index.html 文件,但目前它还没有做什么。

使用 Socket.IO 显示实时数据

对于一个简单的 Web 应用,一个选项是在提供的 HTML 文档的标记中包含当前的传感器值。这是一个完全有效的方法,并且遵循传统的 HTTP 模型:浏览器负责请求服务器更多的数据,形式为完整的文档(网页)请求。副作用是,用户必须在浏览器中重新加载页面才能看到新的读取值。

有更好的方法!

WebSocket API 是一个网络标准,允许客户端(浏览器)和服务器通过单个 TCP socket进行异步消息交换。每个都可以向对方推送消息,从而实现近乎实时数据的交换。

WebSocket 被许多浏览器支持,但并非所有浏览器。为了填补这些空白,你将使用 Socket.IO,这是一个 API,当浏览器支持 WebSocket 时(当浏览器支持时),它会使用 WebSocket,但它有一长串其他回退传输来模拟 WebSocket 行为。简而言之,它使得使用 WebSocket 功能变得简单且无忧。

使用 Socket.IO,当传感器读取值发生变化时,socket 服务器可以将新数据“推送”到客户端(假设客户端正在监听并使用更新的数据)。最终效果是,你可以在浏览器中频繁更新传感器读取值,而无需重新加载页面。

第一步是安装socket.io npm 包:

npm install --save socket.io

接下来,使用以下代码更新 index.js 的内容。

列表 8.16. index.js
const five     = require('johnny-five');
const Tessel   = require('tessel-io');
const express  = require('express');
const SocketIO = require('socket.io');                    *1*

const path = require('path');
const http = require('http');
const os   = require('os');

const app    = new express();
const server = new http.Server(app);
const socket = new SocketIO(server);                      *2*
app.use(express.static(path.join(__dirname, '/app')));

const board = new five.Board({ io: new Tessel() });

board.on('ready', () => {
  const weatherSensor = new five.Multi({
    controller: 'BMP180',
    freq: 5000                                            *3*
  });

  socket.on('connection', client => {                     *4*
    weatherSensor.on('change', () => {
      client.emit('weather', {
        temperature: weatherSensor.thermometer.F,
        pressure: (weatherSensor.barometer.pressure * 10)
      });
    });
  });

  server.listen(3000, () => {
    console.log(`http://${os.networkInterfaces().wlan0[0].address}:3000`);
  });
});
  • 1 需要 socket.io 库

  • 2 创建 socket.io 服务器

  • 3 让 Johnny-Five 将传感器读取频率降低到五秒

  • 4 为客户端请求连接时注册回调

将传感器读取频率降低到每五秒一次,可以保持性能在合理范围内,并为客户端通过 socket 接收新数据的频率设定一个合理的阈值。每五秒是 socket 服务器触发更新的最大频率,因为只有当传感器读取值发生变化时,weather事件才会被触发。

让我们更仔细地检查以下列表中的 Socket.IO connection处理。

列表 8.17. index.js
// Register a callback for when a client (browser) tries to connect.
// The callback is passed a reference to the `client`
socket.on('connection', client => {
  // Listen for change events on the J5 Multi (representing the BMP180)
  weatherSensor.on('change', () => {
    // Emit a `weather` event on the client
    // And pass an object representing current sensor values
    // The client can listen for this event and handle it accordingly
    client.emit('weather', {
      // This is the temperature in Fahrenheit;
      // change to `C` if you'd prefer Celsius
      temperature: weatherSensor.thermometer.F,
      // Multiplying by 10 converts the sensor's readings in kilopascals
      // to more commonly used millibar units
      pressure: (weatherSensor.barometer.pressure * 10)
    });
  });
});
代码非生产级

这些示例中的代码被简化到最小,以保持清晰和简洁。这对于在家原型设计来说是不错的,但 index.js 中的代码并不是为生产准备的。在一个“真实”的代码库中,你想要确保 socket 服务器不会随意接受每个传入的连接,要强制执行最大连接数,等等。此外,尽管 index.html 标记包含有效的 HTML,但它缺少一些可访问性和润色细节,并且在 CSS 中可以更加关注支持旧版浏览器和多样化的浏览环境(如移动设备)。

如果没有利用它的客户端,socket 服务器将不会太有用。首先,你需要通过添加一些结构化标记来准备 HTML 以显示传感器值,如下一个列表所示。

列表 8.18. index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <script src="/socket.io/socket.io.js"></script>                    *1*
  <link rel="stylesheet" href="style.css" type="text/css" />         *2*
  <title>Current Conditions</title>
</head>
<body>

  <main role="main">                                                 *3*
    <h1>Current Conditions</h1>
    <div class="row">
      <div class="col">
        <div class="data">
          <h2>Temperature</h2>
          <span class="data--value" id="temperature">--.--</span>    *4*
          <small class="data--unit">F</small>
        </div>
      </div>
      <div class="col">
        <div class="data">
          <h2>Pressure</h2>
          <span class="data--value" id="pressure">---.--</span>
          <small class="data--unit">mBar</small>
        </div>
      </div>
    </div>
  </main>

 </body>
</html>
  • 1 包含客户端 socket.io JavaScript

  • 2 你将很快创建这个 CSS 文件。

  • 3 在某些容器元素中标记内容,这些元素将渲染为行

  • 4 .data—value spans 将会被填充上传感器数据。

这一行可能让你感到困惑:

<script src="/socket.io/socket.io.js"></script>

那个 JavaScript 文件“神奇地”是从哪里来的?

在 index.js 中,你通过提供一个 server 实例启动了 socket.io,这使得客户端代码可以通过服务器自动在 /socket.io/socket.io.js 上提供。

关于这一行:

<link rel="stylesheet" href="style.css" type="text/css" />

你现在将创建那个样式表。在 t2-weather 目录中,导航到 app 子目录并创建一个名为 style.css 的文件。向其中添加以下代码。

列表 8.19. style.css
html {
  font-family    : "Helvetica Neue", "Helvetica", "Arial", san-serif;
}
.row {
 display         : flex;
 justify-content : center;
 max-width       : 48rem;
 margin          : auto;
}
.row .col {
 margin          : auto;
 padding         : 2rem;
}
h1, h2 {
 margin          : 0;
 text-align      : center;
}
h2 {
 font-size       : 1.5rem;
}
small {
 color           : #999;
 font-size       : 0.65em;
}
.data {
 padding         : 1.5rem;
 background-color: #eee;
 border-radius   : 10px;
 font-size       : 3rem;
}
.data--value {
 font-weight     : bold;
}
.connected { /* Make font color green to show that websockets updating works
     */
 color           : #093;
}

CSS flexbox

这里的 CSS 使用了“flexible box”布局模式,也就是 flexbox。Flexbox 允许你安排和定位元素,而不会因为浮动和塌陷边距的混淆。Flexbox 越来越受到良好的支持,但如果你在使用 Internet Explorer,布局可能会有些奇怪(然而,Edge 浏览器确实支持 flexbox)。如果这一切听起来很陌生,不要担心。这里的 CSS 只是样式化网页,并不关键于理解项目的功能。

列表 8.20. 项目的完整文件和目录结构
t2-weather/
 .tesselinclude
 app
 index.html
 style.css
 index.js
 node_modules
 package.json

现在,最后的关键部分。客户端需要连接到 socket 服务器,监听相关事件(在这种情况下是 weather),并以某种有用的方式响应它们。回到 index.html,并在 </body> 标签之前添加以下代码到文件中。

列表 8.21. index.html
<script>
  window.addEventListener('DOMContentLoaded', function () {       *1*
    var socket = io();                                            *2*
    socket.on('weather', updateData);                             *3*
  });
  function updateData (data) {                                    *4*
    ['temperature', 'pressure'].forEach(function (dataPoint) {
      document.getElementById(dataPoint).innerHTML =
       data[dataPoint].toFixed(2);
    });
    document.querySelectorAll('.data--value').forEach(function (el) {
      el.classList.add('connected');
    });
  }
</script>
  • 1 当页面的 DOM 完全加载...

  • 2 创建 Socket.IO 客户端。

  • 3 注册天气事件的回调函数。

  • 4 处理天气事件的处理器函数:解析更新后的数据并更新页面显示

让我们更仔细地看看前面的代码。

当 DOM 加载完成后,你需要创建一个 Socket.IO 客户端,并告诉它在发生 weather 事件时调用回调函数 (updateData):

window.addEventListener('DOMContentLoaded', function () {
  var socket = io();
  socket.on('weather', updateData);
});
io()函数是从哪里来的?

io 函数,用于初始化 Socket.IO 客户端,通过包含 /socket.io/socket.io.js 脚本来全局提供。

updateData 函数接受更新的 data 对象,并更新页面的 DOM 以包含新值,如下面的列表所示。

列表 8.22. updateData() 详细信息
// `data` is an object containing a `temperature` and `pressure` properties
function updateData (data) {
  ['temperature', 'pressure'].forEach(function (dataPoint) {
    document.getElementById(dataPoint).innerHTML = data[dataPoint].toFixed(2);
  });
  document.querySelectorAll('.data--value').forEach(function (el) {
    el.classList.add('connected');
  });
}

首先,更新具有 IDs temperaturepressure 的 HTML 元素——它们的 HTML 内容分别设置为温度和压力传感器的值:

['temperature', 'pressure'].forEach(function (dataPoint) {
  document.getElementById(dataPoint).innerHTML = data[dataPoint].toFixed(2);
});

传感器值被格式化为小数点后显示两位数字。

然后,作为一个视觉提示,额外的类connected被添加到#temperature#pressure元素上(CSS 将其设置为绿色文本),以显示数据正在通过套接字接收:

document.querySelectorAll('.data--value').forEach(function (el) {
  el.classList.add('connected');
});

DOM 操作代码,jQuery 风格

为了避免过度依赖客户端,index.html 中的 JavaScript 直接使用浏览器 DOM API;例如,document.querySelectorAll()。但很多人更熟悉 jQuery 风格的语法,这使得 DOM 遍历和操作更易于阅读和直观。

如果你喜欢,可以使用 jQuery。为此,你需要添加一个<script>标签来包含它。使用他们 CDN 托管的代码(code.jquery.com/)是推荐的,这样你就不必将另一个文件添加到应用目录中。

更新后的 index.html 脚本可能看起来像这样:

$(function () {
  var socket = io();
  socket.on('weather', updateData);
});
function updateData (data) {
  $('#temperature').html(data.temperature.toFixed(2));
  $('#pressure').html(data.pressure.toFixed(2));
  $('.data--value').addClass('connected');
}

准备好了!你可以将连接到墙式电源的 Tessel 放置在无线网络范围内的任何地方。然后,再次从你的电脑上,输入这个命令:

t2 run index.js --lan

打开浏览器到控制台指示的 URL(图 8.12)。

图 8.12. 温度和压力值在浏览器中实时更新,通过 websockets。

![08fig12_alt.jpg]

t2 push deployment for full independence

记住,当使用t2 run部署项目时,Tessel 仍然对你的电脑有一些依赖:只要没有从你的电脑终止,该进程就会在 Tessel 上运行。使用t2 push,Tessel 可以达到完全自主。

记下你的 Tessel 当前的 IP 地址和 Web 服务器端口。如果你需要提醒,再次运行t2 run index.js并复制记录的 IP 地址和端口组合到你可以访问的地方。通过t2 push部署时,你将无法再看到console.log的输出。

现在,运行这个命令:

$ t2 push index.js

你应该看到以下输出。

列表 8.23. t2 push 输出
$ t2 push index.js
INFO Looking for your Tessel...
INFO Connected to sweetbirch.
INFO Building project.
INFO Writing project to Flash on sweetbirch (1400.832 kB)...
INFO Deployed.
INFO Your Tessel may now be untethered.
INFO The application will run whenever Tessel boots up.
INFO      To remove this application, use "t2 erase".
INFO Running index.js...

给 Tessel 几秒钟的时间来启动网络服务器并开始工作,然后在你的浏览器中查看气象站网页(在 Tessel 的 IP 地址,端口 3000)。

即使你的 Tessel 失去电力,一旦恢复电力并启动,它将开始运行气象站脚本。Tessel 不再需要你的电脑了。

让它停止

你可以使用t2 erase让 Tessel 停止运行气象站程序。

8.5. 使用电池为项目供电

还剩下一根线:一个为 Tessel 及其电路提供电力的墙式连接。

为项目供电有多种选择,如墙式电源、碱性或可充电家用电池、多种锂离子电池或太阳能板。这些选项可能会让人感到不知所措。让我们稍微分析一下。

首先,究竟需要为哪些设备供电?对于像气象站这样的项目,Tessel 本身(开发板、微控制器、处理器)需要供电,Tessel 随后以 3.3V 的电压为 BMP180 扩展板供电。在这种情况下,自然地会想到为开发板供电,并假设开发板将作为整个电路及其组件的电源。

当项目电路对电源需求不大时,这是一个很好的概念,但当项目包含电机或其他电流消耗大的组件时,情况就不同了。在第六章中,漫游机器人的电路包含两个直流电机,所需的电流超过了 Arduino Uno 引脚可以直接提供的电流。那个项目包括一个辅助电源来驱动电机:一个 9V 碱性电池(图 8.13)。

图 8.13。在第六章中,用于机器人的双电机设置使用了外部电源为直流电机供电:一个 9V 电池。

图片

在任何情况下,你选择的为项目提供电源的设备都需要为整个项目供电——有时所有电流都会流经开发板,有时则不会,这取决于电路中的内容。

对于你第一次尝试完全使用电池供电的自由,我们将使用一个USB 移动电源。也许你手头已经有一个:这些小巧的电源包通常用于为移动手机和平板电脑提供额外的电量。它们通过 USB 提供稳定的 5V 电流,并且可以用 USB 壁式充电器轻松充电。它们作为移动设备的备用电池非常棒,同时也非常适合为可以运行 5V 电源的项目供电。

电池容量

给定电池中的“电量”是以电流随时间的变化来衡量的。电池的容量是以其额定电压下的安时(Ah)来衡量的,或者在家庭电池的背景下,是毫安时(mAh)。一个 1.5V、容量为 500mAh 的电池应该能够在 1.5V 下提供 100mA 的稳定电流 5 小时。或者提供 10mA 的稳定电流 50 小时。

当然,事情并没有那么简单。电池的实际容量受许多变量影响:放电率(较高的放电率会导致总容量降低)、环境温度(电池不喜欢寒冷)、电池充电后的时间(电池会自然且缓慢地放电)、电池的化学成分等等。

消费者 1.5AA 电池的容量可以从 400mAh 到 3000mAh 以上不等,这取决于它们的化学成分和其他因素。这是一个很大的范围!

我目前拥有的 USB 移动电源中最强大的一个提供 8000mAh(8 安时)。在另一端,我的一些小型锂聚合物(LiPo)电池只有几百 mAh。

Tessel(不是特别低功耗的板)和电机的功耗需要更高容量的电池,如 USB 砖块,而将 Arduino Nano(一个小巧高效的 Arduino 板)和低功耗温度传感器结合在一起的项目可以在较低容量的 LiPo 上运行良好。

8.5.1. 使用 Tessel 的电池供电机器人

你需要什么

注意

  • 1 Tessel 2

  • 1 从第六章构建的漫游机器人底盘

  • 1 个带有 USB 微型连接的 USB“电源银行”电池

  • 3 个女性接插件引脚一段实心导线(22 号实心导线是理想的)(可选)

  • 焊接枪和材料

通过进行一些小的修改,你可以在第六章中用 Arduino Uno 构建的机器人可以摆脱(线缆)。这不会感觉更像一个真正的机器人吗?

为了解放机器人,你需要做以下事情:

  1. 在 Tessel 上焊接一些连接,为电机提供一个 5 V 电源(可选)

  2. 将电路中的 Uno 替换为 Tessel

  3. 初始化一个项目工作目录,将漫游机器人代码复制到新项目中,并调整电机驱动引脚的引脚号

为电机供电

在第六章中,漫游机器人的电机由 9 V 电池供电。Tessel 的 3.3 V 工作电压太低,无法为电机供电——而且,引脚上的电流限制也会造成问题。但 Tessel 板提供了访问 5 V 电源的途径,有足够的电流来完成工作(图 8.14)。关键是访问它——你需要做一些焊接工作。

图 8.14. Tessel 可以从这里突出显示的电源引脚提供 5 V 电源。

图片

最灵活的选项是将女性接插件焊接在 Tessel 上的三个电源引脚上,从而产生可重复使用的“插座”,你可以将跳线电缆插入其中(8.15)。

焊接女性接插件(图 8.16)与焊接男性断开式接插件略有不同:你将板翻转过来,并在底部焊接引脚。使用一块胶带将引脚固定在顶部,以防你翻转板时掉落。

图 8.15. 将女性接插件焊接在 5 V 电源引脚上提供了方便的可重复使用的插槽,用于跳线。

图片

图 8.16. 要将女性接插件焊接在板上,将板翻转并焊接在底部。你需要一块胶带将引脚固定在顶部,以便你在焊接时保持位置。

图片

或者,你也可以将导线焊接在+5 V 和 GND 引脚上(图 8.17)。然而,这会导致永久性连接的导线。

图 8.17. 将实心导线焊接在电源引脚上也是一种选择。

图片

如果这看起来太麻烦,你可以选择为这个项目使用两个电源:现有的 9 V 电池用于电机电源轨,USB 电池为 Tessel 供电——下一节提供了每个的接线图。

不要从 3.3 V 电源给电机供电

Tessel 的 3.3 V 电源引脚既不能提供电机所需的电压也不能提供电流。不要尝试直接从 Tessel 的 3.3 V 引脚给电机供电。

(重新)构建机器人电路

取下由 Uno 供电的机器人底盘的顶部(图 8.18),以便可以访问内部,并将电路更改为使用 Tessel 而不是 Uno。如果你已经将连接焊接到 Tessel 的 5 V 电源,则按照图 8.19 所示构建电路。如果你选择继续使用 9 V 电池,则按照图 8.20 所示布线电路。

图 8.18。通过将第六章的漫游机器人中的 Uno 替换为 Tessel,做一些布线调整,并将 USB 电池插入 Tessel,你可以创建一个无需接线的漫游机器人。

图 8.19。使用 Tessel 及其 5 V 电源的漫游机器人电路。Tessel 的 5 V 电源需要为两个电源轨供电——确保使用额外的(红色)跳线(靠近顶部)将电源轨的正列连接在一起。

图 8.20。使用两个电源的漫游电路:Tessel 的 3.3 V 和 9 V 电池为电机供电。

更新漫游代码

创建一个工作目录并安装一些依赖项。在终端中(确保你不在 t2-weather 或任何其他现有项目目录内),输入以下命令:

mkdir t2-rover
cd t2-rover
npm init -y
npm install --save johnny-five tessel-io keypress

keypress包用于捕获键盘输入,因此可以使用箭头键控制机器人。

将两个机器人脚本复制到 t2-rover 目录中——index.js 和 Rover.js(或翻回第六章以找到源代码)。在文本编辑器中打开 index.js 文件并做出以下更改:

  1. 在文件顶部添加requiretessel-io

    const Tessel = require('tessel-io');
    
  2. 更新board实例化以指定 I/O 插件:

    const board = new five.Board({ io: new Tessel() });
    
  3. 更新电机引脚:

    const motors = new five.Motors([
      { pins: { dir: 'A4', pwm: 'A5' }, invertPWM: true },
      { pins: { dir: 'B4', pwm: 'B5' }, invertPWM: true }
    ]);
    

就这样!你的机器人已经准备就绪,无需接线。将 Tessel 板放置在之前放置 Uno 的漫游机器人上。插入 USB 电源(如果你使用的是 9 V 电池,也请插入)并使用胶带或魔术贴将其固定在机器人底盘上。确保 Tessel 已完全启动并在局域网中可见(使用t2 list确保)。然后运行以下命令:

$ t2 run index.js --lan

你应该能够使用箭头键从你的电脑上控制你的机器人,并且漫游机器人应该能够比用 USB 线连接时走得更远。

通过本章的实验,你看到了如何开始让你的项目摆脱物理束缚。现在你已经超越了基础。是时候给你更多的工具来发明你自己的实验,创造以前从未创造过的事物。

电压和电机驱动器

基于 Arduino Uno 的机器人使用了 5V 逻辑电平,电机由电池提供 9V 的电力。记住,这里有两个电源——一个用于电机驱动器的逻辑,另一个用于为电机本身供电。

在基于 Tessel 的设置中,电机驱动器连接到 3.3V 逻辑电平,电机只获得 5V 的电压(除非你使用 9V 电池)。电机驱动器在 3.3V 逻辑电平上工作,因为 3.3V 的高电平足以在 5V 电机驱动器设备上注册为逻辑高电平——很多时候你可以使用 5V 逻辑电平与 3.3V 电平一起使用,因为 5V 设备的有效高电压范围通常包括 3.3V。

图片

3.3V 逻辑有时与具有 5V 逻辑电平的组件兼容。5V 逻辑电平设备解释为高电平的电压范围通常包括 3.3V。也就是说,一个+3.3V 的信号(来自 3.3V 设备的逻辑高电平)将在 5V 设备上被解释为高电平,因为它在其高电压范围内。

你会注意到,在 5V 电压下,电机不如在 9V 电压下那样灵活。这是可以预料的。但它们仍然足够好,可以四处移动。

摘要

  • 项目需要有线连接的两个主要原因:数据和通信交换以及电力。

  • 使用 Johnny-Five 与 Arduino Uno 结合使用是主机-客户端设置的例子。主机计算机和 Uno 之间必须始终存在连接。一些受限制的硬件,如 Photon Particle,可以无线与 Johnny-Five 通信,但仍需要一个主机计算机来进行思考。

  • Tessel 2 是一个开源平台,它直接在板上运行嵌入式 Linux 和 Node.js。Tessel 可以在没有主机计算机的情况下独立执行代码。

  • Tessel 2 在 3.3V 电压下运行。它有两个 8 针端口,并支持不同引脚上的不同功能。

  • 对于大多数项目来说,编写 Johnny-Five 脚本所需的适应主要是针对 Tessel 与 Uno(或在不同平台之间移植)使用tessel-io I/O 插件,并更改组件的引脚编号。

  • 由于 Tessel 可以原生运行 Node.js 并支持 npm 包,因此可以在 Tessel 上创建复杂的高级应用程序并直接执行它们。

  • 为项目供电有许多选择。到目前为止,你已经使用了墙上的电源、碱性电池和 USB 电源宝。USB 电池很方便,因为它们有多个用途,包括为移动设备充电,而且使用起来也很简单。

第九章. 创建你自己的东西

本章涵盖

  • 使用现有的、低电压的消费电子产品创建自己的混合体的无限可能性

  • 创建你自己的发明步骤:原型设计、迭代和调试

  • 使用光电耦合器隔离电路

  • 分析数据表和其他软件和固件实现,为组件创建自己的软件支持

  • 使用自定义 Johnny-Five 插件和 J5 Collection 混入封装组件行为

  • 管理复杂的传感器:处理复杂数据和管理配置

对于本章,你需要以下内容:

  • 1 个 Tessel 2

  • 1 套遥控插座

  • 1 个 SparkFun 3.3 V APDS-9960 分线板

  • 焊接铁和配件

  • 用于调试电路的按钮和/或 LED(可选)

  • 6 个光电耦合器(光隔离器),例如 4N25

  • 6 个 100 V 电阻

  • 1 个全尺寸面包板

  • 跳接线

  • 1 个万用表

  • 0.1″公头断开式引脚

  • 22AWG 实心线(可选,用于焊接遥控电路)

硬件工具包(第九章)(可选,未显示 22AWG 实心线)

你已经远远超越了点亮 LED 的世界,可能正渴望开始构建以前未曾构建过的电子设备,规划自己的路线。让我们通过一个完整示例项目的全过程进行一次巡礼——重混一个遥控插座开关——从构思、原型设计、故障排除、迭代和改进。你将不再依赖于现有的电路图、说明书和软件,而是将在项目的某些部分开辟自己的道路。

本章中的项目将让你接触到比早期实验更多的软件复杂性。根据你的 JavaScript 熟练程度,这可能会达到一个甜蜜点,或者可能会觉得超出了你现在的承受范围。如果太多,那也没关系:总是可以从现有的组件支持中组合项目。

现实世界的项目有起有落。需要一点勇气才能跳入更复杂的项目,面对未知。最终使其工作是非常甜蜜的回报,我希望你能学会享受这种冒险。

9.1. 消费电子产品黑客技术

在我的电子专业知识增长过程中,最令人兴奋的时刻之一(如果你能原谅这个糟糕的双关语)是我意识到自己拥有足够的基本知识来摆弄现有的消费电子产品:使它们变得更好,或者发明创造性的混合体——挠一挠可能出现的任何发明家的痒处。如果你家里有一个闲置的电子小玩意儿已经执行了相同的功能,为什么还要设计、购买零件并费力地构建电路呢?

9.1.1. 修改射频控制的插座开关

在房间里四处走动以打开(或关闭)几个单独的灯具可能会很麻烦,尤其是如果它们的开关位置不方便。无线控制的插座插头开关可以很方便。大约 20 美元就可以买到一套带有遥控器的三个(图 9.1)。

图 9.1. 在五金店和家居中心,遥控插座套装既便宜又容易买到。遥控单元由低压电池供电。

![09fig01.jpg]

拿着电池供电的遥控器,你可以单独打开或关闭每个已插入的开关(以及它们插入的灯)。这比在房间里物理地绕一圈要好。但还有一些事情可以改进。

至少用我有的这个型号,不可能同时打开或关闭所有开关(以及它们的灯)。每个都必须单独切换。

更有趣的是,如果能够以其他方式,比如自动化的方式触发灯光,那岂不是很好?也许你希望灯光在变暗时打开,或者从家里的另一个房间,或者使用你梦想中的其他形式的输入。

好消息!这些小遥控器内部的电路既简单又强大——低压,低电流。它们很容易理解,如果你弄错了什么,可能也不会对你造成太大的伤害。让我们开始破解。使用遥控器的内部组件,你可以组装出自己的个性化设备。

务必小心!

虽然许多低压、电池供电的消费电子产品电路是良性的,但每个设备都不同,你需要运用常识。

除非你是合格的电工,不要打开或更改遥控器系统的插座组件——将你的破解限制在遥控器单元上。插座插入墙壁,我在电子探索中有一个政策,那就是永远不接触主电源——这让我远离了医院。

在处理电子设备时,请取出电池。并且始终,始终远离电容器。即使没有连接到电源,它们也能保持电荷很长时间。

拆卸遥控器单元

你需要拆卸遥控器组件。你的遥控器单元可能略有不同,但为了拆卸我拥有的遥控器,我取下了两个小螺丝。在盒子里,我只找到了几样东西(图 9.2):

  • 3 V 的硬币电池(在这个遥控器中是锂 CR2032)

  • 可以按压的物理按钮,它们都连接在一个柔软的塑料床上

  • 一个包含按钮触点、电池触点和状态 LED(以及其他东西,如无线电发射器)的小电路板

  • 一个可伸缩的天线,连接到电路板上

图 9.2. 拆卸后的遥控器

![09fig02_alt.jpg]

每个按钮在电路板上都对应一个垫片,其形状像锯齿状。当上面的按钮被按下时,它会在锯齿状的两边之间建立连接,完成电路。你可以通过使用跳线并触摸锯齿状的一端到两边来自己连接电路,就像一个单独按下的按钮一样(图 9.3)。

图 9.3. 当按钮被按下时,它会与下面的锯齿状触点的两边接触。你可以通过用跳线连接两边来重现这种效果。

增加遥控电路

锯齿状的一侧连接到地。你需要确定哪一侧是那一侧,以便你可以正确地连接你自己的“按钮”(也就是说,不是反的)。最好的方法是使用万用表,但如果你现在不想购买万用表,你可以尝试将 LED 的引脚触摸到按钮触点的每一侧,看看它何时亮起——在那个时刻,LED 的阴极接触的那一侧就是接地侧(图 9.4)。记住,如果你不小心让 LED 流过太多电流或反向连接,你可能会损坏 LED。

图 9.4. 没有万用表?你可能可以使用 LED 来确定按钮触点的哪一侧是接地侧。LED 是一种二极管——只有当它在电路中正确对齐时才会允许电流流动。当它亮起时,这意味着它的阴极位于按钮的接地侧。

掌握万用表

万用表,可以测量电压、电流和电阻,是电子爱好者工具箱中的必备工具。它们价格低廉——可承受的业余品质的万用表价格低于 20 美元。

万用表允许你测量电阻、电流和电压。

在我们当前的实验中,你可以通过观察锯齿状两边的电压电位差来确定按钮接触垫片的哪一侧连接到地。为此,将万用表设置为直流电压设置。我选择了 20 V——根据其电池,设备应在 3 V 左右运行。20 V 设置可以显示高达 20 V 的电压读数;设备的电压略高于我的 2 V 设置,无法使用。

将万用表设置为适当的直流电压设置时,当接地探针位于按钮触点的接地侧时,读数将是正数。

当你将万用表的探针接触到按钮触点的每一侧时,你应该在设备上看到电压读数。如果数字是负数,交换探针。一旦你看到正数读数——我的大约是 2.5 V——你就找到了正极(红色探针连接)和负极(黑色探针连接)。为每个按钮连接做笔记。

控制器一开始可能看起来令人畏惧,但万用表是检查电路和调试电路的好帮手。SparkFun 的“如何使用万用表”教程是一个很好的教程:mng.bz/9f03

下一步可能需要一些独创性,具体取决于你设备电路板的细节:你需要将电线连接到每个按钮的 Z 字形两侧(每个按钮两根电线)。随后,你将能够通过控制这些电线之间的连接和断开来开启和关闭遥控开关。

如果你很幸运,可以直接将电线焊接在按钮的接触垫上(图 9.5)。在我的情况下,遥控器的印刷电路板(PCB)——一块专门为该产品制造的板,上面有丝网印刷的连接和连接不同元件的走线——这使得任务变得具有挑战性。尽管按钮接触是导电的,但焊锡不会粘附在其上。尽管我尽力了,但我所取得的成果只是在电线上涂抹焊锡,弄脏了它们。使用电工胶带将电线固定在垫子上既令人沮丧又麻烦,而且总是从我手中脱落。

图 9.5. 理想的情况是将电线直接焊接在每个按钮接触垫的两侧。

最后,通过使用我的万用表,我找到了电路板上的孔,这些孔对应于按钮接触点。这些孔仍然不能焊接焊锡,但它们作为锚点,我可以将电线插入下面的面包板中(图 9.6)。

图 9.6. 我自制的布线解决方案,利用了特定电路板的结构

结果表明,电路板上的每个按钮都有自己的独立接地连接(蓝色电线——图 9.6 中的顶部六根)。但所有按钮都通过走线连接到单个共享的正电源(黄色电线——图 9.6 中的底部)。要激活任何一个按钮,必须通过将按钮的特定接地连接连接到共享的正电源来完成电路。

学习在发明时保持耐心

当你在开辟新道路并发明自己的东西时,每个项目中至少有一个点事情不会按预期进行(说实话,可能更多)。这可能是一种失望。无论是焊接电线到连接点的问题,还是无法插入面包板的组件,你都需要富有创造力和思考才能克服它。

尽量不要急躁或慌乱——当然,这比听起来容易!这都是经验的一部分。尽量使用合适的工具来完成工作,即使这意味着将你的项目放在一边几天,等待你在线订购的工具或部件通过邮寄到达。即使你手头有必要的材料,有时休息一下也是一个好主意。

这还只是开始!随着你继续学习,你会学会预见某些问题。你也会构建你的工具集,所以当你需要修复故障或进行故障排除时,你将有更多的选择。你还将学会从头开始构建项目真正需要多长时间:这可能会比你想象的要长!

如果你想要实验,你可以将几个瞬态开关(按钮)连接到按钮线上,并使用这些开关来建立连接,从而打开或关闭遥控器开关(图 9.7)。

图 9.7. 如果你愿意,你可以连接一个或多个你的有线按钮到按钮上以测试它。

09fig07.jpg

到目前为止,这已经是一个有趣的练习,用于理解事物是如何工作的,但你可能已经意识到这并没有完成任何有用的任务。它仍然需要人类手指的戳击来操作控件。相反,让我们设计它,以便你可以根据你想要的任何输入或逻辑来控制设备。

使用光电耦合器来隔离电路

当与其他电子设备中的电路进行接口时,最好将目标设备的电源和电路组件与你的开发板和电路隔离。你不想让遥控器中的电流或电压的不可预测性损坏开发板电路中的电子设备,反之亦然。你希望你的开发板及其连接的设备能够控制通过遥控器的电流流动(为不同的按钮建立和断开连接),但你不想实际上连接到遥控器的电路(图 9.8)。

图 9.8. 对于遥控器上的每个按钮,你希望能够使用 Tessel 作为类似开关的控制器来按下和释放按钮。你希望当在点 1 和点 2(输入端)之间施加电流时,电流能够在点 3 和点 4(输出端,遥控器端)之间流动,但你不想让两个电路在物理上连接。

09fig08_alt.jpg

这就是光电耦合器组件(字面意义上)发光的地方。当电流在光电耦合器组件输入端的两个引脚之间流动时,允许电流在输出端的两个引脚之间流动。也就是说,输出端的开关是关闭的(图 9.9)。如果你觉得这个概念有点像晶体管的工作方式——一个小的输入电流允许电流在两个其他引脚之间流动——你是正确的。但在这个案例中,输入电流通过不“接触”它来激活输出晶体管。相反,内部 LED 由流经输入引脚的电流供电。输出端的晶体管是光敏的,当光线照射到它时,会激活(关闭开关)。

图 9.9. 电流施加到光电耦合器的输入端会激活内部红外 LED。LED 发出的光照射到光敏晶体管上,激活它并允许电流在输出端流动。

09fig09_alt.jpg

正如你看到的一些其他组件一样,光电耦合器有许多不同的名称:你可能会看到它们被称为光电耦合器光电隔离器

9.2. 使用 Johnny-Five 组件插件控制遥控器开关

你第一次尝试远程控制遥控器(哦,它变得很抽象了!)将涉及使用现有的 Johnny-Five 功能,但以新的方式组合它们。

在这个第一次迭代中,你将使其能够通过 Tessel 上运行的应用逻辑控制每个开/关开关——虚拟地按下按钮。

9.2.1. 开关项目原型设计

而不是一次性连接所有光耦合器和开关——在我的情况下是六个光耦合器和相关组件——从电路的一个部分开始并确保它工作可能是有帮助的。然后你可以编写一些不够完善的代码来检查事物是否按预期运行,然后再进行任何精细调整。

这个原型设计过程可以帮助你在深入之前,在小规模上验证你的方法并排除问题。

原型设计硬件

按照图 9.10 所示连接第一组开/关按钮。图 9.10。黄色电线应连接到正按钮连接,黑色电线连接到负极(在我的情况下,所有黄色电线都连接到相同的共享正极源)。确保光耦合器上指示引脚 1 的圆点方向正确。将两个光耦合器的引脚 2 连接到地。

图 9.10. 第一组开/关按钮的布线图

图 9.10 的替代图片

通过一个 100 欧姆电阻将第一个光耦合器的引脚 1 连接到 Tessel 的引脚 A0,将引脚 2 连接到 GND。这里需要限流电阻,因为光耦合器内部有一个 LED。将光耦合器的引脚 5 连接到第一个开关的按钮的正极,将引脚 4 连接到该按钮的地线。

以类似的方式连接第二个光耦合器,但将其连接到 Tessel(源)侧的引脚 A1 和第一个开关的按钮的正极和地线。

原型设计软件

这里有一些需要考虑的事情。在遥控器上“按下按钮”意味着什么?按钮按下所建立的连接不会永远保持连接,也不是瞬间的脉冲——想想人类手指按下然后释放按钮。事实上,在我的遥控器上,我过去注意到,如果我按得太快——释放得太快——开关并不总是响应。这里涉及到一点按下并保持。

你可以通过软件通过启用按钮的连接并通过光电耦合器保持一段时间,比如半秒钟,然后再关闭它来模仿这一点。通过查看实际代码可能更容易理解这一点。让我们开始吧。

首先,设置项目:

mkdir remote-switches
cd remote-switches
npm init -y

接下来,安装标准依赖项并创建一个 index.js 文件:

npm install --save johnny-five tessel-io
touch index.js

与使用高级的 Johnny-Five 组件类如 Led 不同,你将直接写入数字值——HIGH(1)或 LOW(0)到 A0 和 A1 引脚。一个引脚可以被配置为数字输出引脚,如下所示:

board.pinMode('A0', five.Pin.OUTPUT);

一旦配置完成,就可以像这样写入:

board.digitalWrite('A0', 1); // set HIGH (3.3V on Tessel)
board.digitalWrite('A0', 0); // set LOW (0V)

要“按下”第一个开关的按钮,你需要将引脚 A0 设置为 HIGH——“打开”光电耦合器,允许电流通过第一个按钮。500 毫秒后,你再次将 A0 设置为 LOW,断开电路连接并“释放”按钮。

将以下原型切换代码添加到 index.js 中。

列表 9.1. 原型按钮按下代码
const five = require('johnny-five');
const Tessel = require('tessel-io');

const board = new five.Board({ io: new Tessel() });

const switchPins = {                                   *1*
  on: 'A0',
  off: 'A1'
};
const pressDuration = 500;                             *2*

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

  board.pinMode(switchPins.on, five.Pin.OUTPUT);       *3*
  board.pinMode(switchPins.off, five.Pin.OUTPUT);

  const pressButton = function (pin) {
    board.digitalWrite(pin, 1);                        *4*
    setTimeout(() => {
      board.digitalWrite(pin, 0);
    }, pressDuration);                                 *5*
  };
  const turnOn = function () {
    pressButton(switchPins.on);
  };
  const turnOff = function () {
    pressButton(switchPins.off);
  };

  board.repl.inject({                                  *6*
    turnOn: turnOn,
    turnOff: turnOff
  });
});
  • 1 这两个引脚连接到两个光电耦合器的源端。

  • 2 “按下并保持”按钮 500 毫秒以确保开关激活

  • 3 将 A0 和 A1 配置为数字输出引脚

  • 4 要按下按钮,首先将相关的光电耦合器引脚设置为 HIGH。

  • 5 然后在将引脚再次设置为 LOW 之前,为 pressDuration(500 毫秒)设置超时。

  • 6 在原型设计阶段,将 turnOn 和 turnOff 方法提供给 REPL 使用。

尝试一下。通过局域网将代码部署到 Tessel(你需要通过局域网部署,以便 REPL 能够工作):

$ t2 run index.js --lan

尝试从 REPL 调用 turnOn()turnOff() 函数。注意,这两个函数都返回 undefined——这是正常的,不是错误。

1492612088185 Available Tessel 2 (sweetbirch)
1492612088341 Connected Tessel 2 (sweetbirch)
1492612088415 Repl Initialized
>> turnOn()
undefined
>> turnOff()
undefined

这样做应该会“按下”远程控制器的开/关按钮,并打开/关闭相关的墙壁插座。

构建电路的其余部分

一旦你更有信心你的第一个电路正在按预期工作,就可以像 图 9.11 所示那样扩展它。确保注意每个光电耦合器的方向。

图 9.11. 完成的开关/光电耦合器接线图。光电耦合器的输出应该连接到按钮接触线。

9.2.2. 编写 RemoteSwitch 插件

现在你已经完善了电路,是时候完善代码了。你可以在应用程序的主模块(index.js)中封装每个三个开/关开关组合的行为,而不是有一个杂乱无章的代码集合。通过移除组件特定的代码,你可以在 index.js 中简单地实例化一个 RemoteSwitch 对象,用于每个开/关对(或者你的远程控制器有多少个开关对)并随意打开或关闭它们。

让我们一步步地创建一个可重用的 Johnny-Five 组件插件。

Johnny-Five 组件插件与组件类

内置 组件类LedMotor 是 Johnny-Five 的核心部分。创建一个新的组件类需要对 Johnny-Five 代码库本身进行更改。另一方面,组件插件 可以创建,而无需对 Johnny-Five 进行任何修改——因此术语 插件。组件插件的结构与组件类代码的结构有许多相似之处,但这两者——组件类与组件插件——是不同的东西,所以不要混淆(你正在制作一个 插件)!

组件插件结构基础

在 remote-switches 目录中创建一个新的文件名为 RemoteSwitch.js,用于包含插件模块。模块的结构将开始如下所示。这个骨架结构遵循 Johnny-Five 组件插件约定,使其模块化和灵活。

列表 9.2. RemoteSwitch.js:起点
module.exports = function (five) {                                 *1*
  return (function () {
    function RemoteSwitch (opts) { /* ... */ }                     *2*
    RemoteSwitch.prototype.toggle = function (turnOn) {};          *3*
    RemoteSwitch.prototype.on = function () {};
    RemoteSwitch.prototype.off = function () {};
    return RemoteSwitch;
  }());
};
  • 1 消费模块提供对 Johnny-Five(five)的引用。

  • 2 这将初始化一个新的 RemoteSwitch 对象(这是它的构造函数)。

  • 3 你将添加三个原型方法来控制开关组件。

RemoteSwitch 模块没有直接依赖 Johnny-Five。相反,它将其导出函数的参数作为一个 Johnny-Five 对象引用。这样,插件就可以在包含它的任何 Johnny-Five 对象之上工作。传递的 Johnny-Five 对象可能包含配置了 Tessel I/O 插件的 board,或者它可能有一个带有 Firmata I/O 层的 Arduino,或者可能是其他东西——这样插件就不必关心物流或有效的引脚编号等任何事情。传递的 Johnny-Five 对象将负责这些,让你可以自由处理手头的逻辑。

插件需要有一个构造函数,它可以接受一些选项,执行它需要进行的任何设置,并注册到 Johnny-Five。当你完成时,RemoteSwitch 的实例化将看起来像这样:

const switch1 = new RemoteSwitch({ pins: { on: 'A0', off: 'A1' } });

你接下来将工作在这个构造函数上。

编写插件代码

将以下代码添加到 RemoteSwitch.js

列表 9.3. RemoteSwitch.js:构造函数
module.exports = function (five) {
  return (function () {
    function RemoteSwitch (opts) {
      if (!(this instanceof RemoteSwitch)) {                            *1*
        return new RemoteSwitch(opts);
      }
      five.Board.Component.call(this, opts = five.Board.Options(opts)); *2*

      // opts.pins should contain two properties, `on` and `off`,
      // defining their pin numbers, respectively
      this.pins     = opts.pins;
      this.duration = 500;
      this.isOn     = undefined;                                        *3*
      this.isActive = false;                                            *4*

      this.io.pinMode(this.pins.on, this.io.MODES.OUTPUT);              *5*
      this.io.pinMode(this.pins.off, this.io.MODES.OUTPUT);
    }
    // ...
  }());
};
  • 1 这个(样板)模式确保函数使用“new”关键字调用。

  • 2 注册组件

  • 3 在一开始,技术上你不知道开关是开启还是关闭。

  • 4 当开关的一个按钮被激活(按下)时,isActive 为 true。

  • 5 将开关的开启和关闭引脚配置为数字输出引脚。

现在是时候添加切换开关开启和关闭的方法了。

列表 9.4. RemoteSwitch.js:原型方法
module.exports = function (five) {
  return (function () {
    function RemoteSwitch (opts) { /* we already wrote this */ };

    RemoteSwitch.prototype.toggle = function (turnOn) {
      if (this.isActive) { return false; }                      *1*
      this.isActive = true;                                     *2*
      if (typeof turnOn === 'undefined') {
        turnOn = !this.isOn;                                    *3*
      }
      const pin = (turnOn) ? this.pins.on : this.pins.off;      *4*
      this.io.digitalWrite(pin, 1);
      setTimeout(() => {
        this.io.digitalWrite(pin, 0);
        this.isActive = false;                                  *5*
        this.isOn = !!turnOn;                                   *6*
      }, this.duration);
    };
    RemoteSwitch.prototype.on = function () {
      this.toggle(true);
    };
    RemoteSwitch.prototype.off = function () {
      this.toggle(false);
    };
  }());
};
  • 1 如果开关已经处于激活(忙碌)状态,则不会激活开关

  • 2 表示开关当前处于激活状态

  • 3 如果缺少 turnOn,则确定切换行为

  • 4 确定要激活开关的哪个引脚(开启或关闭)

  • 5 开关不再处于激活状态;isActive 应该再次设置为 false

  • 6 跟踪开关的当前状态

您会注意到一切都与 toggle 方法有关。单个 toggle 参数 turnOn 定义了会发生什么。如果是真值,开关会打开。如果是假值,开关会关闭。如果是 缺失的 (undefined),开关将从当前状态切换。为了支持该功能,开关的当前状态存储在 isOn 属性中。

为了防止远程设备尝试同时发送两个信号,使用 isActive 属性作为标志。如果 isActive 为真,则 toggle 不会写入任何引脚。

将 index.js 重构为使用 RemoteSwitch

您现在可以将 index.js 更新为包含以下代码。

列表 9.5. 重构后的 index.js
const five = require('johnny-five');
const Tessel = require('tessel-io');
const RemoteSwitch = require('./RemoteSwitch')(five);         *1*

const board = new five.Board({ io: new Tessel() });

board.on('ready', () => {
  const switch1 = new RemoteSwitch({
    pins : { on: 'A0', off: 'A1' }
  });
  const switch2 = new RemoteSwitch({
    pins: { on: 'A2', off: 'A3' }
  });
  const switch3 = new RemoteSwitch({
    pins: { on: 'A4', off: 'A5' }
  });
  board.repl.inject({
    switch1: switch1,
    switch2: switch2,
    switch3: switch3
  });
});
  • 1 导入 RemoteSwitch 模块并将其传递五个参考

尝试一下:

t2 run index.js --lan

一旦它开始运行,您就可以在 REPL 中与开关对象交互:

1492618175517 Available Tessel 2 (sweetbirch)
1492618175671 Connected Tessel 2 (sweetbirch)
1492618175746 Repl Initialized
>> switch1.on()
undefined
>> switch1.off()
undefined
>> switch2.on()
undefined
迭代:排队和回调

目前,如果 isActive 为真,则 RemoteSwitch 实例上的 toggle 方法不会做任何事情——也就是说,它不允许多个同时的开关激活。缺点是,按照目前的编写方式,如果在开关已经激活时发生,toggle 命令将被有效地丢弃并忽略。假设出于某种原因,您想多次闪烁开关,并在 index.js 中有如下代码来尝试完成这个任务:

for (var i = 0; i < 10; i++) {
  switch1.toggle();
}

开关确实会按预期切换一次。但随后的九次 toggle 调用会立即发生——在第一次调用激活开关之后(因此,当 isActivetrue 时)。它们会被忽略。因此,开关只会切换一次,而不是十次。

您可以通过在开关激活时加入简单的 FIFO 排队(先进先出——类似于“按接收顺序回答电话”)来修复这个问题。这只需要几行代码。

当你在那里时,你还可以向 RemoteSwitch 的方法添加一些 回调 支持,如下一列表所示。这与其他 J5 组件保持一致,并使得在开关命令(开/关/切换)完成时注册一个要调用的函数成为可能。目前这只是一个额外的功能,但将来会很有用。

列表 9.6. 完整的 RemoteSwitch 插件
module.exports = function (five) {
  return (function () {
    function RemoteSwitch (opts) {
      if (!(this instanceof RemoteSwitch)) {
        return new RemoteSwitch(opts);
      }
      five.Board.Component.call(this, opts = five.Board.Options(opts));

      this.pins     = opts.pins;
      this.duration = opts.duration || 500;
      this.isOn     = undefined;
      this.isActive = false;
      this.queue    = [];                                               *1*

      this.io.pinMode(this.pins.on, this.io.MODES.OUTPUT);
      this.io.pinMode(this.pins.off, this.io.MODES.OUTPUT);
    }

    RemoteSwitch.prototype.toggle = function (turnOn, callback) {       *2*
      if (this.isActive) {                                              *3*
        this.queue.push([turnOn, callback]);
        return;
      }
      this.isActive = true;
      if (typeof turnOn === 'undefined') {
        turnOn = !this.isOn;
      }
      const pin = (turnOn) ? this.pins.on : this.pins.off;
      this.io.digitalWrite(pin, 1);
      setTimeout(() => {
        this.io.digitalWrite(pin, 0);
        this.isActive = false;
        this.isOn = !!turnOn;
        if (typeof callback === 'function') {                           *4*
          callback();
        }
        if (this.queue.length) {                                        *5*
          this.toggle.apply(this, this.queue.shift());
        }
      }, this.duration);
    };

    RemoteSwitch.prototype.on = function (callback) {                   *6*
      this.toggle(true, callback);
    };
    RemoteSwitch.prototype.off = function (callback) {
      this.toggle(false, callback);
    };
    return RemoteSwitch;
  }());
};
  • 1 实例化一个空数组来存储排队的“命令”

  • 2 现在接受回调

  • 3 如果开关忙碌,则将项目推入队列

  • 4 切换动作完成:如果有回调,则调用

  • 5 如果队列中有下一个项目,则调用 toggle

  • 6 开关和关闭处理函数接受一个回调参数并将其传递给切换

现在如果您将以下代码添加到 index.js 中,它应该按预期工作:

for (var i = 0; i < 10; i++) {
  switch1.toggle(); // This will cause the switch to toggle 10 times
}
您的硬件可能不同

根据我们编写的软件,开关应该切换 10 次。然而,值得注意的是,可能存在硬件限制,这可能会阻止这种情况的发生。例如,你的遥控器的电子设备可能不允许开关频繁切换。

到目前为止,你已经黑入了消费电子产品,构建了一个小型原型,并将其扩展为一个更精致的电路,其中包含一个定制的 Johnny-Five 组件插件用于输出(开关)。

你可以用你之前看到的一些输入设备来触发你的RemoteSwitch组件。你可以使用光敏电阻,当房间变暗时触发开关。你可以使用运动或接近传感器,当你的猫经过时打开它们(并在一定时间后自动关闭)。但是,当你一心想要用 Johnny-Five 不支持的一种输入方式来控制开关时会发生什么?那时怎么办?

9.3. 编写复杂硬件的软件

有这样一个吸引我注意的 I²C 设备。Avago APDS-9960 设备(图 9.12)包含多个传感器:一个 RGB 环境光传感器、一个接近传感器和一个复杂的手势传感器。SparkFun 销售基于该芯片的廉价(约 15 美元)分线板。真 neat!一个手势传感器!这是一个控制你远程开关灯的有趣方式。为什么还要按按钮,当你可以像魔术师一样在空中滑动呢?

图 9.12. APDS-9960 将三个传感器集成在一个封装中,包括一个手势传感器。SparkFun 提供了一块使芯片工作更简单的分线板。

问题在于,当我偶然发现 APDS-9960 时,并没有现成的 Johnny-Five 支持。没有手势传感器的组件类。这意味着你需要创建那个支持!当你想使用的软件不存在时,你有两个选择:替换另一个有支持的硬件,或者自己创建那个支持。

9.3.1. 项目:为 APDS-9660 手势传感器添加 Johnny-Five 支持

APDS-9960 将考验你的能力(它是一个复杂的设备),但你为它创建的插件的高级结构将与RemoteSwitch相似。以下的项目流程基于真实经验,并突出了在创建硬件新支持时遇到的一些常见阶段、挫折和结果。

要构建支持,你需要做以下事情:

  1. 定义目标和范围

  2. 收集关于 APDS-9960 的信息并进行研究

  3. 构建一个快速的原型

  4. 定义 API 界面和插件生命周期

  5. 编写插件的代码

  6. 测试插件是否工作

  7. 完成项目,将本章早些时候的RemoteSwitch与 APDS-9960 插件集成

手势传感器插件的目标

在插上电线、焊接或编写代码之前,首先要问自己你想要完成什么。

你想要为 APDS-9960 手势传感器创建一个 Johnny-Five 组件插件。一个合理的 API,与其它 Johnny-Five 组件保持一致,应该包括暴露手势事件,这些事件可以被应用程序代码监听和响应。

范围之外的内容是什么?鉴于 APDS-9960 手势传感器的复杂性,不要尝试支持其它的传感器。同样,为了节省开发时间和减少麻烦,你将直接硬编码许多默认设置和功能支持,这些可以在以后通过其他方式管理,例如通过传递给插件的选项。

设计代码以便在多个硬件平台上使用是一个良好的目标,同时也要考虑到未来的灵活性。编写一个符合 Johnny-Five 组件插件约定的模块——例如RemoteSwitch——将有助于实现跨平台的目标。注意常量的管理方式,关注 API 表面的一致性,并保持方法模块化,将有助于使软件更容易修改和扩展。

你将基于 Tessel 编写这个手势传感器支持。没有理由认为它不会在其他(3.3 V)Johnny-Five 兼容平台上工作,但在这个项目中你不会花费时间去进行超出 Tessel 的测试。

收集有关 APDS-9960 的信息

在明确了目标和范围之后,是时候收集信息了。有许多问题需要回答:

  • 物理硬件和协议 设备的工作电压是多少?有哪些引脚和连接?它使用哪种通信协议?

  • 通信 从设备读取数据和写入数据的具体细节是什么?

  • 配置和设置 初始化设备、设置默认值和启用所需功能需要采取哪些步骤?

  • 数据 设备产生了哪些类型的数据?你如何解释和处理这些数据?

在探索之旅的第一站,你需要阅读 SparkFun 的 APDS-9960 连接指南和文档(mng.bz/MapU)。SparkFun 为 APDS-9960 提供的分线板为你处理了一些硬件层面的繁琐工作——跳线和电源连接。结果板可以像你遇到的其他 I²C 组件一样布线。但有一个额外的连接:一个中断能力的引脚。我们稍后会回到这一点。另一个非常重要的细节是,这个设备在 3.3 V 下运行——这对 Tessel 来说完全没问题,但不要尝试将其连接到 5 V 的 Arduino。

接下来,获取 APDS-9960 的数据表并进行扫描(mng.bz/by50)。好消息是,APDS-9960 的数据表相当出色,作为数据表来说。但请注意,它仍然是一个非平凡设备的数据表:如果初次浏览让你感到眼花缭乱,请不要慌张。完整阅读第 1 页——这是一个很好的总结。从数据表中,你可以获取到设备硬编码的 I²C 地址等重要细节(它是0x39;见第 8 页)。

通过审查现有的 APDS-9960 软件和固件支持,你可以获得很大的优势。SparkFun 的 Shawn Hymel 编写了一个完整且出色的开源 Arduino 库(仅适用于 3.3 V Arduino!),它支持设备的每个功能(mng.bz/8gE7)。它非常出色:易于阅读且注释详尽。Arduino 库和数据表之间的交叉引用详细说明了与设备一起工作的其他一些细节。

如何使用 APDS-9960

在生成手势数据之前,APDS-9960 硬件需要初始化并启用其手势模式。从概念上讲,这是一个两步的过程。

首先,有一个设置阶段,在这个阶段,默认值和设置被写入 APDS-9960 上的多个不同寄存器。随后,有一个启用阶段,在这个阶段,手势模式被激活(而不是设备其他传感器的模式),并且一些特定于手势的设置被写入更多的寄存器(图 9.13)。

图 9.13。在这个简化的部分状态机表示中,插件对象实例最初处于new状态。通过setup方法,它进入initialized状态,此时 APDS-9960 设备已初始化但未主动感应手势。enable方法启用了设备上的手势模式。

用一个非常粗略、不准确的比喻来说:设置就像打开芯片并启动它,而启用就像在设备上启动一个特定手势的应用程序。尽管你的实现将仅支持手势感应——这意味着启用总是在设置之后立即发生,无需用户干预——保持这两个阶段的不同将使以后添加额外的传感器支持更容易。

在设置和启用过程中,需要写入哪些寄存器和什么值?所有这些信息都在数据表中;需要时间、耐心和细致的注意力来组织它们。

设置和启用完成后,APDS-9960 将积极感应手势运动。当设备感应到运动并开始产生数据时,它将将中断引脚拉低,表示已检测到手势。也就是说,连接到分线板中断引脚的 Tessel 引脚上的电压将从 3.3 V 变为 0 V。从高到低的所谓下降沿是软件应该从设备读取手势数据的信号。

手势数据表示由一系列 4 字节的数据集组成,每个数据集包含每个方向(上、下、左、右)的 1 字节(一个从 0 到 255 的值)。通过分析这些值在每个方向上的变化,可以推导出手势的整体方向。

当设备检测到手势动作时,它会将中断引脚拉低,然后开始将数据集存入内存寄存器。设备上为此预留了 128 字节的空间——最多可以存储 32 个 4 字节的数据读取——数据以 FIFO(先进先出)的方式放入这些寄存器中(图 9.14)。

图 9.14。有 128 字节的 FIFO RAM 可用,起始内存地址为 0xFC,用于存储手势数据。在这个例子中,FIFO 队列中有三个数据集可用。一个数据集由四个字节组成,每个字节代表不同方向的数据——上、下、左和右。

当控制设备——你的 Tessel——从这些寄存器中读取数据字节时,它就会释放空间,更多的数据可以被推入 FIFO RAM。这个过程会持续进行——控制器读取,设备将更多数据放入 FIFO——直到没有更多的手势数据进入,FIFO 被清空。然后,数据可以被控制设备处理(图 9.15)。

技术上讲:中断

技术上讲,当检测到手势数据时,设备将触发一个中断,因为你在启用步骤中将其配置为这样做。APDS-9960 具有高度可配置性,手势中断是一个可选功能。更技术性地,它将在 FIFO RAM 中放入四个数据集(样本)后触发中断。确切的中断触发时间,是的,也是可配置的。

图 9.15。随着研究的深入,该插件的有限状态机变得更加详细。当检测到中断时,插件实例进入读取周期阶段,然后进入处理阶段,最后返回到激活监听状态。

构建一个概念验证原型

在着手插件实现之前,需要进行一次合理性检查。你将建立一个工作区,连接芯片,并确保可以与它建立 I²C 通信。这将使你在进入更注重细节的开发阶段时更有信心。

连接 APDS-9960 扩展板很简单,但首先需要在其上焊接引脚头。按照图 9.16 所示连接。中断引脚连接到 Tessel 的 A2 引脚,这是一个具有中断能力的引脚。

图 9.16。APDS-9960 扩展板的布线图

创建一个新的项目工作区并安装这些依赖项:

mkdir gesture
cd gesture
npm init -y
npm install johnny-five tessel-io

同时也将 RemoteSwitch.js 文件复制到手势目录中,你稍后会再次使用它。

创建和共享电路和布线图

为了创建本书中使用的布线图,我使用了开源的 Fritzing 软件 (fritzing.org/home/),它可在 Mac、Windows 和 Linux 平台上使用。它包含各种部件,包括你可以用来构建图表的板子和组件。此外,像 SparkFun 和 AdaFruit 这样的部件制造商经常为他们的产品提供 Fritzing 部件。例如,我能在 SparkFun 的 Fritzing_Parts 存储库中找到一个 APDS-9960 的部件 (mng.bz/Hsa2)。

使用 Fritzing 可以创建原理图以及图表,尽管我发现它有点挑剔。在原理图和 PCB 设计软件领域,Autodesk 的 EAGLE 是一个更强大的选择。不出所料,它有一个学习曲线,但它被广泛使用,如果你想尝试的话,还有一个跨平台的免费版本 (www.autodesk.com/products/eagle/free-download)。

KiCad EDA 是另一个跨平台的开源原理图和 PCB 设计软件选项 (kicad-pcb.org/)。

如果你热衷于尝试模拟电子电路——不是高级的断出板和微控制器,而是像电容器、逻辑门、晶体管和变压器这样的基础知识——你可能想尝试 iCircuit App ($9.99),适用于 iOS、Android 和 Windows(桌面和手机)。这不是一个静态绘图或制图应用。相反,它是一个实时模拟引擎,允许你在更改电路时看到真正发生的事情。

APDS-9960 的许多寄存器之一是一个只读的 DEVICE_ID 寄存器,地址为 0x92(数据手册第 25 页)。当你从该地址读取一个字节时,你应该始终得到值 0xAB (图 9.17)。这对于你进一步操作芯片来说并不有用,但它是一个方便的方式来确保 I²C 通信正在工作,并且你确实连接到了 APDS-9960。

图 9.17. 在 APDS-9960 的地址 0x92 上的 DEVICE_ID 寄存器中的值应该始终是 0xAB(二进制中的 10101011)。

创建一个名为 i2c-test.js 的文件,并将以下代码添加到其中。

列表 9.7. 测试与 APDS-9960 的连接
const five = require('johnny-five');
const Tessel = require('tessel-io');
const board = new five.Board({ io: new Tessel() });

board.on('ready', () => {
  board.i2cConfig({ address: 0x39 });                                      *1*
  board.i2cReadOnce(0x39, 0x92, 1, data => {                               *2*
    if (data[0] !== 0xAB) { // DEVICE_ID register should return 0xAB       *3*
      throw new Error('Unable to establish connection with APDS9960');
    } else {
      console.log('Connected to APDS-9960!');
    }
  });
});
  • 1 使用从机地址 0x39 开始 I²C 通信。这是 APDS-9960 的硬编码 I²C 地址。

  • 2 从地址 0x39 的设备 0x92 寄存器中读取 1 个字节

  • 3 如果该寄存器不包含数据 0xAB,则表示出了问题。

默认 I²C 接口

Tessel 有两个 I²C 接口。Johnny-Five 通过 tessel-io,如果没有在 board.i2cConfig() 传递的选项中指定,将自动使用 A 端口的接口。

Johnny-Five 的 I²C 功能

i2cReadOnce(address, register, bytesToRead, handler(arrayOfBytes)) 方法从指定的 register 寄存器开始读取给定的 bytesToRead 字节数。读取完成后,回调函数被调用并传递一个字节数组。

i2cReadOnce 与相关方法 i2cRead 不同。正如其名所示,i2cReadOnce 只会 读取一次,而 i2cRead 将会 持续 从指定的地址/寄存器组合中读取。如果你想要反复从相同的寄存器(们)读取以观察变化,i2cRead 会很有用。在你的情况下,你将使用 APDS-9960 的中断功能来通知你何时有新的手势数据,而不是使用 i2cRead 进行轮询。

这些 I²C 实用方法的实际底层实现——这些方法也出人意料地包括向 I²C 设备写入的能力——取决于活动的 I/O 插件。在 Tessel 的情况下,这是由tessel-io处理的;对于 Arduino,这里将是由 Firmata 来完成繁重的工作。

在你的连接 Tessel 和 APDS-9960 电路板上尝试这段测试代码:

$ t2 run i2c-test.js --lan

一旦你成功看到日志消息“已连接到 APDS-9960!”就到了继续前进并构建组件插件的时候了。

编写 APDS9960 插件

创建一个名为 APDS9960.js 的文件,并从以下列表中显示的代码开始。为了完成插件的代码,你将填充这些部分。

列表 9.8。APDS9960.js 的起点
// Dependencies
const Emitter = require('events').EventEmitter;
const util    = require('util');

/** CONSTANTS HERE **/

module.exports = function (five) {
  return (function () {
    /**
     * @param {Object} opts Options: pin, address
     * pin denotes interrupt pin connection
     *
     * Sample initialization:
     * var gesture = new APDS9960('A2');
     * var gesture = new APDS9960({ pin: 'A2'});
     * gesture.on('up', () => { ...do something ...});
     */
    function APDS9960 (opts) {
      // Constructor: Set up instance properties and kick off initialization
    }

    // Extend Node.js' EventEmitter class so that our object can emit events
    util.inherits(APDS9960, Emitter);

    /* Reset this instance's current gesture data */
    APDS9960.prototype.resetGesture = function () { };

    /* `setup` and `enable` are invoked from the constructor */
    APDS9960.prototype.setup = function (callback) { };
    APDS9960.prototype.enable = function () { };

    /* When interrupt is pulled LOW, `readGesture` reads data out of the
     * FIFO until the data are exhausted, then invokes `processGesture`
     * and `decodeGesture` to process the resulting data
     */
    APDS9960.prototype.readGesture = function () { };

    /* `processGesture` performs some computations over read data and
     * determines some ratios and deltas in the directional samples.
     */
    APDS9960.prototype.processGesture = function () { };

    /* Using `deltas` computed by `processGesture`, "decodes" the
     * information into a gesture (up, down, left, right) when possible
     * and emits events.
     */
    APDS9960.prototype.decodeGesture = function () { };

    return APDS9960;
  }());
};

高级 API 中的方法表示完整的状态机(图 9.18)。现在你需要实现这些方法。

图 9.18。该插件的有限状态机现在已完整。下一步是实现它。

常量和配置设置

你将首先定义一些常量,因为有很多。但不要慌张。APDS9960.js 中的常量定义了寄存器地址、位掩码、默认值以及一些其他配置位和组件。

确定需要写入哪些寄存器以设置和启用设备是一项注重细节的练习,需要参考数据手册(并在可能的情况下查看其他软件实现)。

图 9.19 展示了在设置和启用过程中写入的一些寄存器。一些寄存器的值被设置为简单的合理默认值,例如 GPENTH,其设置为 40 的二进制表示。其他寄存器被禁用——GOFFSET_U 被设置为 0x00。还有一些是位掩码,一次设置多个标志——配置值(GCONF1GCONF2)。

图 9.19。设置和启用过程中写入的 APDS-9960 寄存器的样本

使用位掩码管理功能配置

APDS-9960 插件使用 掩码 来管理 APDS-9960 上的配置设置——几个配置值通常包含在一个字节中,不同的位位置对应不同功能的值。几个掩码标志通过按位或运算组合成多功能的字节。

例如,地址为 0xA3 的寄存器(GCONF2)包含三个功能的配置(图 9.20)。

图 9.20. GCONF 寄存器中的不同位范围对应不同的设置。例如,位置 6 和 5 的两个位设置手势增益。位 7 未使用。

再次提醒你,MDN 关于位运算符和掩码的出色文章(mng.bz/CLvy)。

按照以下列表填写常量。

列表 9.9. APDS9960 常量
const REGISTERS = {
  ENABLE   : 0x80, // Enable different sensors/features (p.20)
  WTIME    : 0x83, // Wait time config value (p.21)
  PPULSE   : 0x8E, // Proximity pulse count and length (p.23)
  CONFIG2  : 0x90, // Second configuration register (p.24), for LED boost
  DEVICE_ID: 0x92, // Contains device ID (0xAB) (p.25)
  GPENTH   : 0xA0, // Entry proximity threshold for gestures (p.27)
  GEXTH    : 0xA1, // Exit proximity threshold for gestures (p.28)
  GCONF1   : 0xA2, // Gesture config 1: gesture detection masking (p.28)
  GCONF2   : 0xA3, // G config 2: gain, LED drive, gesture wait time (p.29)
  GOFFSET_U: 0xA4, // Gesture offset (up) (p.30)
  GOFFSET_D: 0xA5, // Gesture offset (down) (p.30)
  GPULSE   : 0xA6, // Gesture Pulse count and length (p.31)
  GOFFSET_L: 0xA7, // Gesture offset (left) (p.30)
  GOFFSET_R: 0xA9, // Gesture offset (right) (p.31)
  GCONF4   : 0xAB, // Gesture config 4: interrupts, mode enable (p.32)
  GFLVL    : 0xAE, // Gesture FIFO level: # of datasets in FIFO (p.32)
  GSTATUS  : 0xAF, // Gesture status; bit 0 indicates available data (p.33)
  GFIFO_U  : 0xFC, // 1st FIFO register in (RAM)—read data from here (p.33)
};

const FLAGS = {
  GFIFOTH  : 0b10000000, /* FIFO threshold: trigger interrupt after
                            4 datasets in FIFO (GCONF1 <7:6> p.28) */
  GGAIN    : 0b01000000, /* Gesture gain control:
                            4x (GCONF2 <6:5> p.29) */
  GLDRIVE  : 0b00000000, /* Gesture LED drive strength:
                          * 100mA (GCONF2 <4:3> p.29) */
  GWTIME   : 0b00000001, /* Gesture wait time:
                            2.8ms (GCONF2 <2:0> p.29) */
  GPLEN    : 0b11000000, /* Gesture pulse length:
                            32μs (GPULSE <7 :6> p.31) */
  GPULSE   : 0b00001001, /* Gesture pulse count:
                            10 (9 + 1) (GPULSE <5:0> p.31) */
  GVALID   : 0b00000001, /* GSTATUS register value
                            indicates valid data if 0th bit is 1 */
  PPLEN    : 0b10000000, /* Proximity pulse length:
                            16μs (PPULSE <7 :6> p.23) */
  PPULSE   : 0b10001001, /* Proximity pulse count:
                            10 (9 + 1) (PPULSE <5:0> p.23) */
  LED_BOOST: 0b00110000, /* LED drive boost:
                            300% (CONFIG2 <5:4> p.24) */
  GIEN     : 0b00000010, /* Gesture interrupt enable:
                            yes (GCONF4 <1> p.32) */
  GMODE    : 0b00000001, /* Gesture mode:
                            yes! (GCONF4 <0> p.32) */
  ENABLE   : 0b01001101, /* Enable features:
                            Gesture, Wait, Proximity, Power on
                            (ENABLE, p.20) */
};

// During setup, (value) is written to each register (key)
const SETUP_DEFAULTS = {
  ENABLE   : 0x00,          /* Disable all things,
                              effectively turning the chip off (p. 20) */
  GPENTH   : 40,            // Entry proximity threshold
  GEXTH    : 30,            // Exit proximity threshold
  GCONF1   : FLAGS.GFIFOTH, // FIFO interrupt threshold
  GCONF2   : FLAGS.GGAIN | FLAGS.GLDRIVE | FLAGS.GWTIME, // Gesture gain,
   LED drive, wait time
  GOFFSET_U: 0x00,          // no offset
  GOFFSET_D: 0x00,          // no offset
  GOFFSET_L: 0x00,          // no offset
  GOFFSET_R: 0x00,          // no offset
  GPULSE   : FLAGS.GPLEN | FLAGS.GPULSE // pulse count and length,
};

// During enable, each (value) is written to register (key)
const ENABLE_VALUES = {
  WTIME  : 0xFF,                       /* Wait time between cycles in
                                          low-power mode: 2.78ms (p. 21) */
  PPULSE : FLAGS.PPLEN | FLAGS.PPULSE, // Proximity pulse length and count
  CONFIG2: FLAGS.LED_BOOST,
  GCONF4 : FLAGS.GIEN | FLAGS.GMODE,
  ENABLE : FLAGS.ENABLE
};

// For processing read data
const GESTURE_THRESHOLD_OUT = 30;
const GESTURE_SENSITIVITY = 10;
技术上讲:它是如何实际工作的

APDS-9960 通过检测内置红外 LED 反射回的能量变化来感应“手势”。这个 LED 的配置细节——驱动它的功率、脉冲次数以及每个脉冲在检测周期中的持续时间——在定义的配置值中经常出现。

9.3.2. 实现构造函数和初始化方法

接下来,你将完善构造函数以及初始化和启用设备进入手势模式的方法。

构造函数的基本结构与 RemoteSwitch 构造函数相同。构造函数还启动了设置和启用,如下一列表所示。

列表 9.10. APDS9960:构造函数和手势数据重置
function APDS9960 (opts) {
  if (!(this instanceof APDS9960)) {
    return new APDS9960(opts);
  }
  five.Board.Component.call(this, opts = five.Board.Options(opts));        *1*
  this.interruptState = 1; // Interrupt is active LOW                      *2*
  opts.address        = opts.address || I2C_ADDR;                          *3*
  this.address        = opts.address;                                      *4*
  this.io.i2cConfig(opts); // Get I2C comms started for the device         *5*

  this.io.i2cReadOnce(this.address, REGISTERS.DEVICE_ID, 1, data => {
    if (data[0] !== DEVICE_ID) { // DEVICE_ID register should return 0xAB
      throw new Error('Unable to establish connection with APDS9960');
    }
  });
  this.resetGesture();                                                     *6*
  this.setup(this.enable);                                                 *7*
}

util.inherits(APDS9960, Emitter);

APDS9960.prototype.resetGesture = function () {
  this.gestureData = {
    raw: [],
    deltas: {},
    movements: { // A gesture can have movements along more than one axis
      vertical  : false,
      horizontal: false,
      up        : false,
      down      : false,
      left      : false,
      right     : false
    },
    valid: false, // Was gesture decoding successful?
    direction: undefined
  };
};
  • 1 将组件注册到活动板上

  • 2 中断将从高电平拉到低电平以激活;初始值为高电平(1)

  • 3 准备 I²C 地址以传递给 i2cConfig

  • 4 同时保留组件对象上的 I²C 地址

  • 5 this.io 是对活动板实例的引用。

  • 6 重置(初始化)手势保持数据对象

  • 7 启动设置(随后是启用)

接下来是初始化方法:setupenable

列表 9.11. APDS9960:填写 setupenable
APDS9960.prototype.setup = function (callback) {
  for (var rKey in SETUP_DEFAULTS) {                                       *1*
    this.io.i2cWrite(this.address,
      REGISTERS[rKey], [SETUP_DEFAULTS[rKey]]);
  }
  if (typeof callback === 'function') {
    callback.call(this);                                                   *2*
  }
};

APDS9960.prototype.enable = function () {
  // Set up interrupt handling
  this.io.pinMode(this.pin, this.io.MODES.INPUT);                          *3*
  // Interrupts from device are active LOW—when pin goes LOW we should act
  this.io.digitalRead(this.pin, data => {                                  *4*
    if (data !== this.interruptState && data === 0) {                      *5*
      this.readGesture();
    }
    this.interruptState = data;
  });
  for (var rKey in ENABLE_VALUES) {                                        *6*
    this.io.i2cWrite(this.address,
      REGISTERS[rKey], [ENABLE_VALUES[rKey]]);
  }
};
  • 1 将设备设置的默认值(SETUP_DEFAULTS)写入各个寄存器

  • 2 调用回调(在这个例子中是 enable)

  • 3 将连接的中断配置为数字输入引脚

  • 4 从中断引脚连续读取值

  • 5 当中断从高电平变为低电平时,调用 readGesture

  • 6 与设置类似,为手势模式特定的功能写入配置

读取传感器数据

在初始化和设置代码就绪后,让我们着手插件的核心部分:读取和处理手势数据。当中断引脚变为低电平时,readGesture 被调用,从 APDS-9960 读取数据。

列表 9.12. 读取手势数据
APDS9960.prototype.readGesture = function () {
  // GSTATUS value determines whether valid data is available (p.33)
  this.io.i2cReadOnce(this.address, REGISTERS.GSTATUS, 1, status => {
    if (status & FLAGS.GVALID === FLAGS.GVALID) {
      // There should be valid data in the FIFO
      // GFLVL will report how many datasets are in the FIFO (p.32)
      this.io.i2cReadOnce(this.address, REGISTERS.GFLVL, 1, fifoLevel => {
        // Read the number of 4-byte samples indicated by sampleCount
        // And split them out into their directional components
        this.io.i2cReadOnce(this.address,
          REGISTERS.GFIFO_U, (fifoLevel * 4), rawData => {
            for (var i = 0; i < rawData.length; i += 4) {
              this.gestureData.raw.push({
                up   : rawData[i],
                down : rawData[i + 1],
                left : rawData[i + 2],
                right: rawData[i + 3]
              });
            }
            return this.readGesture(); // Keep reading data...
          });
      });
    } else { // No (more) data to gather about this gesture
      this.processGesture();
      this.decodeGesture();
      this.resetGesture();
    }
  });
};

首先,readGestureGSTATUS寄存器读取一个字节。如果该寄存器中的值最低位(0 位)设置为1,那么你就可以开始了:可以从 FIFO 中读取有效数据。但是有多少数据?GFLVL(手势 FIFO 级别)寄存器的值将告诉你此刻 FIFO 中有多少数据集样本可用。然后它继续从 FIFO 寄存器中读取这么多样本,这些寄存器从GFIFO_U(地址 0xFC)开始。

回想一下,每个数据集是四个字节(每个方向一个字节),所以需要从 FIFO 中读取的总字节数是fifoLevel * 4readGestures再次使用i2cReadOnce方法,这次读取fifoLevel * 4个字节而不是一个字节。然后它遍历每个完整的数据集,将对应于每个方向的单独字节放入raw数据结构中,以供后续处理。然后它再次调用自己(递归地)以查看是否有更多数据可供读取(图 9.21)。

图 9.21。读取周期首先检查GSTATUS寄存器中的 0 位是否为1。如果是,则读取GFLVL以查看有多少样本可用(0101,即 5),然后从 FIFO 中读取指示的数量。循环继续,直到GSTATUS<0>0

图 9.21

这个循环会一直重复,直到GSTATUS寄存器的值指示没有剩余的有效数据可读取——第一个条件失败,执行继续在else子句中。当数据读取周期完成时,开始处理这些数据。

处理和解码手势数据

您的 APDS9960 插件中的方法区分了处理解码手势数据(图 9.22)。

图 9.22。processData对数据进行算术运算;decodeData从计算结果中派生出手势方向并触发事件。最后,在实例上重置手势数据对象,以便它准备好收集下一个手势的数据。

图 9.22

在处理步骤中,会发生数学运算。完整的原始样本集合被过滤,只包括每个方向上的值都超过定义的阈值常数的读取值。然后,在每个轴(上下和左右)上计算读取值随时间变化的比率(变化量)。

列表 9.13。处理手势数据
APDS9960.prototype.processGesture = function () {
  const raw = this.gestureData.raw;
  const directionDelta = function (el1, el2, dir1, dir2) {                 *1*
    var el2r = ((el2[dir1] - el2[dir2]) * 100) / (el2[dir1] + el2[dir2]);
    var el1r = ((el1[dir1] - el1[dir2]) * 100) / (el1[dir1] + el1[dir2]);
    return el2r - el1r;
  };
  const exceedsThreshold = raw.filter(sample => {                          *2*
    return (sample.up > GESTURE_THRESHOLD_OUT &&
            sample.down > GESTURE_THRESHOLD_OUT &&
            sample.left > GESTURE_THRESHOLD_OUT &&
            sample.right > GESTURE_THRESHOLD_OUT);
  });
  if (!exceedsThreshold.length || raw.length < 4) {                        *3*
    // If not enough data or none exceed threshold, nothing to do
    // This will result in gesture data being ignored and discarded
    return false;
  }

  const first = exceedsThreshold[0];
  const last = exceedsThreshold[exceedsThreshold.length - 1];
  const deltas = {
    upDown: directionDelta(first, last, 'up', 'down'),
    leftRight: directionDelta(first, last, 'left', 'right')
  };
  this.gestureData.deltas = deltas;                                        *4*
};
  • 1 通过比较不同方向读取值的比率来计算变化量

  • 2 过滤样本,只包括那些读取值超过阈值的样本

  • 3 确保存在有效数据可处理

  • 4 最终,计算出一些变化量

最后,在下述列表中的解码步骤将processGesture方法计算出的变化量转换为整体手势的派生方向。然后它触发相应的事件————或者,如果它无法确定一个清晰的单个手势方向,它将触发一个通用的gesture事件,如下一列表所示。

列表 9.14。解码手势数据
APDS9960.prototype.decodeGesture = function () {
  const deltas = this.gestureData.deltas;
  const verticalMotion = Math.abs(deltas.upDown);
  const horizontalMotion = Math.abs(deltas.leftRight);
  if (verticalMotion > GESTURE_SENSITIVITY) { // Determine meaningful
   movement on vertical axis
    this.gestureData.valid = true;
    this.gestureData.movements.vertical = true;
    this.gestureData.movements.up = (deltas.upDown >= 0);
    this.gestureData.movements.down = (deltas.upDown < 0);
  }
  if (horizontalMotion > GESTURE_SENSITIVITY) { // Determine meaningful
   movement on horizontal axis
    this.gestureData.valid = true;
    this.gestureData.movements.horizontal = true;
    this.gestureData.movements.left = (deltas.leftRight >= 0);
    this.gestureData.movements.right = (deltas.leftRight < 0);
  }
  if (this.gestureData.valid) {
    if (verticalMotion > horizontalMotion) {
      this.gestureData.direction = (this.gestureData.movements.up) ?
        'up' : 'down';
    } else {
      this.gestureData.direction = (this.gestureData.movements.left) ?
      'left' : 'right';
    }
  }
  // Emit a directional event if there is a direction
  if (this.gestureData.direction) {
    this.emit(this.gestureData.direction, this.gestureData);
  }
  // Always emit a generic gesture event, even if decoding failed
  this.emit('gesture', this.gestureData);
};

这样,插件的代码就完成了!

9.3.3. 集成手势传感器和远程开关

你现在可以编辑 index.js 来添加一些快速测试代码,如下所示。在你进行大组合步骤——连接远程开关之前,你可以测试不同的滑动方向事件并将它们记录到控制台。

列表 9.15. APDS9960 测试驱动
const five = require('johnny-five');
const Tessel = require('tessel-io');
const Gesture = require('./APDS9960')(five);

const board = new five.Board({ io: new Tessel() });

board.on('ready', () => {
  const gesture = new Gesture({ pin: 'A2'});
  gesture.on('right', () => console.log('right'));
  gesture.on('left', () => console.log('left'));
  gesture.on('up', () => console.log('up'));
  gesture.on('down', () => console.log('down'));
});

在 Tessel 上运行 index.js (t2 run index.js --lan) 并尝试将你的手移至手势传感器上方——最佳距离大约八英寸(20 厘米)。

结合 APDS9960 和 RemoteSwitch

你已经拥有了使用手势传感器和 RemoteSwitch 控制单个开关开/关组合的原料。例如,你可以做如下所示的事情。

列表 9.16. 使用手势传感器控制单个开关的 APDS9960
const five = require('johnny-five');
const Tessel = require('tessel-io');
const Gesture = require('./APDS9960')(five);
const RemoteSwitch = require('./RemoteSwitch')(five);

const board = new five.Board({ io: new Tessel() });

board.on('ready', () => {
  const gesture = new Gesture({ pin: 'A2'});
  const switch1 = new RemoteSwitch({ pins : { on: 'A3', off: 'A4' } });
  gesture.on('right', () => switch1.on());
  gesture.on('left', () => switch2.on());
});

到目前为止,一切顺利。但最初提出的目标之一不是能够一次性打开或关闭所有开关吗?好消息是,你即将实现这一点。另一个消息是,你需要再迈出一步才能实现这一点。

协调多个开关的挑战

RemoteSwitch 的设计考虑到了需要错开开关激活的需求:它将当其中一个按钮已经激活时到达的“命令”排队,将它们放入一个 FIFO 队列,并在不再激活时执行下一个排队的命令。此外,它将在完成一个命令后调用提供的回调函数。因此,你可以放心地执行以下列表中的代码,不用担心开关会相互干扰。

列表 9.17. 由 RemoteSwitch 实例管理的按钮对有一个队列
switch1.on();   // Happens right away
switch1.off();  // Gets queued
switch1.on();   // Gets queued
switch1.off(() => console.log('hi!')); // Gets queued; logs 'hi!' to the
 console when it's done

然而,遗憾的是,存在一个缺点:每个开关对之间的排队是单独管理的。不同的 RemoteSwitch 实例之间互不相识;请参见以下列表。

列表 9.18. 每个 RemoteSwitch 都有自己的队列
switch1.on();   // Happens right away
switch1.off();  // Gets queued in `switch1`'s queue
switch2.on();   // `switch2`'s queue is empty...happens right away (uh oh)
switch3.on();   // `switch3`'s queue is empty...happens right away (oh dear)

很可能 switch2switch3 会尝试在 switch1 仍然忙碌时激活——相当于同时按下遥控器上的多个按钮。这可能不是什么好事。RemoteSwitch 是这样设计的,每个开关对之间互不相识。这是一个很好的硬件抽象的提示,但我们的现实是,多个开关对正在共享单个遥控设备上的同一发射器。

使用集合控制多个开关

哎呀,项目中的另一个转折点。你可以拆分 RemoteSwitch 并重新编写它以处理多组开关及其混合队列。或者,你可以在应用程序特定逻辑中编写一些管理多个开关对的代码。

这两种选择都有缺点。在不破坏现有 API 或过度复杂化它的前提下适应 RemoteSwitch 会很麻烦(如果您是从零开始,这个选项看起来会更吸引人)。将相关逻辑直接放入主应用程序代码中会显得很丑陋且分散注意力。经过一番思考,我决定采用一种实用——虽然有些拼凑——的第三种方案,该方案利用了 Johnny-Five 的 Collection 混合。

内置 集合类Motors(您在第六章中看到过)利用 Johnny-Five 的 Collection 混合,该混合提供了在单个容器-like 对象内管理多个组件的功能。您可以使用此混合提供的某些功能来创建一个可以管理多个 RemoteSwitch 对象的组件。完成后,您将能够在主应用程序模块中编写如下列表所示的代码。

列表 9.19. 使用 RemoteSwitches
const switches = new RemoteSwitches([
  new RemoteSwitch({ pins : { on: 'A3', off: 'A4' } }),
  new RemoteSwitch({ pins: { on: 'A5', off: 'A6' } }),
  new RemoteSwitch({ pins: { on: 'A7', off: 'B0' } })
]);
// You can act on all switches at once...
switches.on(); // Turn all switches on
// Or a single switch...
switches.off(1); // Turn the second switch off

在您的当前工作目录中创建一个名为 RemoteSwitches.js 的文件,并添加以下代码。

列表 9.20. RemoteSwitches
const util    = require('util');

module.exports = function (five, RemoteSwitch) {
  return (function () {
    function RemoteSwitches (opts) {
      if (!(this instanceof RemoteSwitches)) {
        return new RemoteSwitches(opts);
      }
      // RemoteSwitch is the "type" of each individual component object
      // that will be managed by this RemoteSwitches instance
      Object.defineProperty(this, 'type', { value: RemoteSwitch });
      // Make it go: register and initialize the collection component objects
      five.Collection.call(this, opts);
      this.isActive = false; 1((CO11-1))
      this.queue = [];
    }
    // Use the Collection mixin
    util.inherits(RemoteSwitches, five.Collection);

    // The nuts-and-bolts logic for (de-)activating a given switch
    // Note that this is not on the prototype (inaccessible externally)
    const write = function (whichSwitch, turnOn) {
      if (this.isActive) {
        this.queue.push([whichSwitch, turnOn]);
        return;
      }
      this.isActive = true;
      // An individual RemoteSwitch object's "toggle" method
      // is invoked
      whichSwitch.toggle.call(whichSwitch, turnOn, () => {
        this.isActive = false;
        if (this.queue.length) {
          write.apply(this, this.queue.shift());
        }
      });
    };

    // Prototype methods take optional `idx` argument to designate which
    // switch to activate. If not provided, all switches will be affected.
    RemoteSwitches.prototype.toggle = function (idx, turnOn) {
      if (typeof idx !== 'undefined' && this[idx]) {
        write.call(this, this[idx], turnOn);
      } else {
        this.each(whichSwitch => write.call(this, whichSwitch, turnOn));
      }
    };

    RemoteSwitches.prototype.on = function (idx) { this.toggle(idx, true); };
    RemoteSwitches.prototype.off = function (idx) { this.toggle(idx, false); };
    return RemoteSwitches;
  }());
};

9.3.4. 整合整个项目

所有组件现在都已准备就绪,可以创建一个综合项目,将软件和电路模块结合成一个手势控制遥控器。

按照如图 9.23 所示的方式组合两个电路——遥控和手势。光电耦合器的输出端应连接到遥控器的按钮触点。请注意,光电耦合器现在连接到 Tessel 的不同引脚,以腾出 APDS-9960 的空间。

图 9.23. 显示 APDS-9960 分线板和按钮/光电耦合器电路组合的布线图。

使用全尺寸面包板

如果您使用的是全尺寸面包板,如图 9.23 所示,请确保按照板长中部的说明连接电源轨:电源轨连接在中部有断裂。

您可以将全尺寸面包板想象成两个半尺寸面包板拼接在一起。

最终确定软件

您的 gesture 目录现在应包含以下内容:

  • APDS9960.js_ 手势传感器插件

  • RemoteSwitch.js (单个) 远程开关插件

  • RemoteSwitches.js 远程开关集合

  • index.js 应用逻辑

为了为 APDS-9960 连接腾出空间,光电耦合器的连接必须向下移动几个引脚(参见图 9.23)——这些更新的引脚编号在 index.js 的最终版本中已考虑,如下一列表所示。

列表 9.21. 在 index.js 中整合所有内容
const five = require('johnny-five');
const Tessel = require('tessel-io');
const Gesture = require('./APDS9960')(five);
const RemoteSwitch = require('./RemoteSwitch')(five);
const RemoteSwitches = require('./RemoteSwitches')(five, RemoteSwitch);    *1*

const board = new five.Board({ io: new Tessel() });

board.on('ready', () => {
  const gesture = new Gesture({ pin: 'A2'});
  const switches = new RemoteSwitches([
    new RemoteSwitch({ pins : { on: 'A3', off: 'A4' } }),
    new RemoteSwitch({ pins: { on: 'A5', off: 'A6' } }),
    new RemoteSwitch({ pins: { on: 'A7', off: 'B0' } })
  ]);
  gesture.on('up', () => switches.on());                                   *2*
  gesture.on('down', () => switches.off());
  gesture.on('right', () => switches.on(1));
  gesture.on('left', () => switches.off(1));
});
  • 1 需要 RemoteSwitches 模块

  • 2 您当然可以更改哪些手势对应哪些开关行为。

这是一项大量工作!但这里有一些稳固的结果。你无疑在你的电子黑客经验之柱上刻下了几个胜利的凹槽。当然,到处都是电线,这是我们稍后将要解决的问题,当我们探讨不同外形尺寸的项目外壳时。

说到外形尺寸,你一直非常关注 Arduino Uno 和 Tessel,两者都使用 Johnny-Five。但 JavaScript 控制的硬件选项还有很多。是时候认识一些其他玩家了。

摘要

  • 电池供电、低压消费电子产品通常可以被重新用于你自己的项目中的部件和组件(前提是你小心操作!)。光电耦合器组件可以帮助隔离这些电子产品中的电路与你的微控制器电路。

  • 发明需要独创性,但也需要坚持和耐心。它通常需要创造性思维来解决意外的问题。

  • 数据表可能数据密集到令人难以承受,但它们是至关重要的,随着时间的推移,你会学会如何快速在其中找到关键信息——比如内存寄存器地址和配置步骤。

  • 即使是个人爱好项目,也可以从有组织的开发方法中受益:确定目标和范围、研究、原型设计和迭代。

  • 将行为封装成模块化、组件级别的块是良好的开发实践,特别是对于抽象和跨平台支持。使用 Johnny-Five,你可以创建组件插件,你也利用了Collection混入。

第四部分. 在其他环境中使用 JavaScript 与硬件

本书的这一部分探讨了你可以使用 JavaScript 控制硬件的其他环境,并展望了未来。

你将从第十章开始,研究高度受限设备上的 JavaScript 和类似 JavaScript 的环境,使用 Espruino Pico 和 Kinoma Element 设备进行一些实验原型设计。

第十章 10 和第十一章 11 都介绍了一套可重复使用的步骤,用于快速了解新平台。在第十一章 11 中,我们将把注意力转向更强大的硬件:具有板载 I/O 功能的一般用途单板计算机(SBC)。你将使用 Raspberry Pi 3 和 BeagleBone Black 开始,并将一些 Johnny-Five 实验适配到这两个平台上。

第十二章提供了物联网生态系统其他部分的品尝,并检查了在网页浏览器内可能实现的内容。你将使用云服务打包和部署 Johnny-Five 应用程序到 BeagleBone Black,并使用 Espruino Puck.js 设备探索 Web 蓝牙和物理网络的尖端。

当你完成本书的这一部分时,你的 JavaScript on Things 工具箱将装备齐全,你将准备好进入充满勇气和灵感的 JavaScript 和嵌入式系统世界。

第十章. JavaScript 和受限硬件

本章涵盖

  • JavaScript 能力的嵌入式硬件平台与主机-客户端和单板计算机(SBC)平台相比

  • 熟悉新开发平台的步骤

  • 检查两个代表性的嵌入式 JavaScript 平台:Espruino Pico 和 Kinoma Element

  • 使用 Espruino Pico 开发项目

  • 使用 Nokia 5110 LCD 显示屏和 Espruino Graphics库制作文本和形状

  • 重复使用可靠的组件:以新的方式在不同的平台上使用 BMP180 多传感器和 HMC5883L 指南针

  • 对 Kinoma Element 的案例研究视角

在本书的前半部分,通过使用连接的 Arduino Uno——一个主机-客户端设置——展示了电子基础知识。然而,在过去的几章中,你已经遇到了 Tessel 2,它具有在 OpenWrt 操作系统内原生运行 Node.js 的能力——这是一个单板计算机(SBC)设置。

现在我们将探讨第三类 JavaScript 控制的平台:具有对 JavaScript(或通常是类似 JavaScript 的东西)的原生支持的受限嵌入式硬件。为了在如此有限的硬件资源上完成这一壮举,这些平台往往依赖于高度优化的自定义 JavaScript 引擎。

这些设备正在快速发展,进入(和退出)市场的速度比印刷品能捕捉到的要快。就在此刻,Espruino 平台——我们很快就会看到 Espruino Pico——似乎保持着强劲的动力 (图 10.1)。Kinoma Element——也将在我们的调查中——已经处于预发布状态有一段时间了。尽管 Kinoma 的嵌入式运行时被吹捧为支持大多数 ECMAScript 6 功能的早期先驱,但 Element 产品可能最终不会起飞。由于变化如此之快,很难说。

图 10.1. 两个嵌入式 JavaScript 平台:Espruino Pico 和 Kinoma Element

对于将物联网硬件和软件信息印刷在纸上的情况,其过时几乎是肯定的,这就是为什么本章更多地关注嵌入式 JavaScript 平台上的常见任务和谜题。具体产品和平台来来去去,但有一些常见的研发步骤可以帮助你快速熟悉你选择的产品。

对于本章,你需要以下物品:

  • 1 个 Espruino Pico

  • 1 个 Kinoma Element

  • 1 个 USB 微型线缆

  • 18 个(每条 9 个)0.1″ 阳性断开式引脚

  • 1 个 USB 2.0 A 到 USB A 雌性(也称为 USB 延长线)线缆

  • 1 个 Adafruit BMP180 I²C 多传感器扩展板

  • 1 个诺基亚 5110 84x48 LCD 显示模块

  • 1 个 100 V 电阻

  • 1 个 Adafruit HMC5883L 磁力计(指南针)扩展板

  • 1 个全尺寸面包板

  • 跳线

10.1. Espruino Pico 平台

Espruino Pico 的内存和计算能力比 Tessel 2 要少。没有 WiFi 和 USB 外设支持,那么为什么还要使用它呢?因为它在其它方面表现出色:它更便宜,它更小巧,它更可靠,它节能——这些都是低功耗嵌入式平台的标志。

Espruino 既可以描述硬件家族本身,也可以描述预装在 Espruino 设备上的固件运行时解释器。Espruino-解释器支持大多数 JavaScript 功能,但并非全部。例如,你不能省略分号,正则表达式也不受支持。

很重要要区分 JavaScript 和 JavaScript 式的 Node.js:这不是 Node.js,所以你不能使用 Johnny-Five 或任何 npm 模块。

相反,Espruino 提供了自己的 JavaScript API 用于与硬件 I/O 交互 (www.espruino.com/Reference#software). 你现在已经有足够的经验,API 的某些方面可能听起来很熟悉——例如,有一个 analogRead 函数,它接受一个引脚号作为参数。还有 Espruino 特有的模块,它们封装了特定电子组件的行为,正如你将看到的。

在更深入地检查 Pico 之前,你需要将其设置好,并运行一个 Hello World LED 闪烁脚本。

10.1.1. 设置 Pico

Pico 需要焊接在排针上(图 10.2)。您的 Pico 可能附带排针,如果没有,您将需要两根九针的排针。

图 10.2。Pico 有 18 个引脚(每行 9 个),间距为 0.1 英寸——与面包板兼容。它们需要焊接在排针上。Pico 板的一端设计成可以直接插入 USB 端口。

图片

Pico 可以直接插入 USB 端口。一些版本的 Pico 有一个额外的 USB 微型连接,但您可能只能使用 USB A 连接。这可能很方便,因为您可以直接将 Pico 插入电脑,但如果您想在面包板上使用 Pico(当您想要尝试任何 I/O 引脚时这是必要的)就会有点棘手。Pico 设计成连接到 USB A 母接头。您可以使用市场上销售的 USB 延长线来获得 Pico 需要的 USB A 到 USB A 母接头连接(图 10.3)。

图 10.3。您可以使用 USB 延长线将 Pico 连接到您的电脑,使 Pico 可以放置在面包板上。Pico 的 USB 端插入 USB A 母接头。

图片

Espruino 家族的发展变化非常快,在这里包括详尽的设置说明似乎是愚蠢的。相反,请访问espruino.com开始使用。

这些是基本步骤,在将 Pico 插入 USB 后:

  1. 为您的平台准备 Pico:

    1. Mac 用户可能不需要做任何事情。

    2. Windows 用户可能需要驱动程序。

    3. Linux 用户可能需要调整权限。

  2. 安装 Espruino IDE Chrome 应用(如果您还没有安装 Chrome 浏览器,还需要安装 Chrome 浏览器)。

  3. 启动 Chrome 应用,连接并更新 Pico 的固件。

为了实验 Pico,您将使用 Espruino 的基于 Web 的 IDE(Chrome 应用)——这意味着您将在 Chrome 应用内连接到、与 Pico 通信并部署代码到 Pico。

图 10.4。Espruino Chrome 应用 IDE

图片

在应用界面的左侧,您会看到一个类似终端的控制台区域。一旦连接到 Pico,您可以直接在这里输入表达式,有点像 Node.js 解释器或 Johnny-Five REPL。右侧是一个可以编写脚本的区域。

10.1.2. Hello World LED 闪烁

让我们通过 LED 闪烁来试一试。在这个实验中,您将使用 Pico 的板载 LED 之一,因此您可以直接将 Pico 插入 USB 端口,或者使用 USB 延长线将其放在面包板上:选择由您决定。

Espruino 脚本在全局级别提供了一系列变量,这些变量与平台功能和引脚相关。这包括用于 Pico 内置红色和绿色 LED 的变量LED1LED2(图 10.5)。

图 10.5. 此实验将使 Pico 的板载 LED(一个红色,一个绿色)交替闪烁。LED 的访问通过全局变量 LED1LED2 提供。

10fig05.jpg

启动 Espruino IDE Chrome 应用程序并连接到 Pico。将以下列表中的代码输入屏幕的代码组合区域(右侧)并点击中间的发送到 Espruino 图标(图 10.6)。

列表 10.1. 点亮 Pico 的 LED
var ledStatus = false;
function toggleLED () {
  if (!ledStatus) {
    digitalWrite(LED1, 1);               *1*
    digitalWrite(LED2, 0);
  } else {
    digitalWrite(LED1, 0);
    digitalWrite(LED2, 1);
  }
  ledStatus = !ledStatus;
  setTimeout(toggleLED, 500);            *2*
}

toggleLED();                             *3*
  • 1 LED1(板载红色 LED)和 LED2(绿色)在 Pico 脚本中可用。

  • 2 使用 setTimeout 每隔 500 毫秒调用自身函数

  • 3 开始切换

图 10.6. 将 LED-闪烁代码输入到 Espruino IDE 中(字体大小增加以提高可见性)

10fig06_alt.jpg

此示例使用 Espruino 的 digitalWrite 函数交替设置 LED 为高电平和低电平。一旦将代码部署到 Pico 上,你应该看到 Pico 的红色和绿色 LED 依次闪烁。你还会在 IDE 窗口的左侧看到一些输出。

10.2. 了解新平台

现在你已经动手实践了,让我们回顾一下。你是如何知道 Pico 有两个内置 LED(红色和绿色),以及你是如何知道存在变量 LED1LED2 的?到现在为止,像 digitalWrite 这样的函数名称表示向数字输出写入逻辑电平这样的约定可能看起来很合理。但它可能仍然显得有些神奇或随机。从哪里开始呢?

当面对一个新的平台时,你可以应用一系列侦探步骤来让你迅速上手。你现在将解决这些问题来了解 Pico,但你也可以在未来重新使用它们来评估不同的嵌入式平台:

  1. 发现平台的核心特性

  2. 查找引脚图或硬件图

  3. 了解配置和开发工作流程

  4. 查找示例和简单教程,并动手实践

  5. 使用参考 API 文档

让我们看看这些中的每一个。

10.2.1. 发现平台的核心特性

在你甚至接触到新的开发板或平台之前,你可能想要对其整体功能有一个大致的了解。

通常,关键细节会在制造商或供应商的网站上总结。在 Pico 的情况下,Pico 的网页上列出的特性列表为我们提供了信息:它是一个 3.3 V 设备,外形小巧(33 毫米 x 15 毫米),由 STM32F401CDU6 微控制器供电(不,我的猫并没有只是走过我的键盘;ST——制造商——有非浪漫但非常精确的命名规范),并且使用 ARM Cortex M4 处理器(www.espruino.com/Pico)。

在关键特性列表中还有关于功耗的要点(图 10.7)。即使这里引用的具体数字没有给你留下深刻印象,你也能看出他们正在强调其节能特性。

图 10.7. Espruino Pico 的关键特性,列在 Espruino 的网站上

10fig07_alt.jpg

该设备上有 22 个 GPIO 引脚,包括 3 个 I²C 接口和 3 个(硬件)SPI 接口——对于这么小的设备来说,这已经很不错了。如果你点击进入微控制器的数据手册(mng.bz/i7r8),你可以看到这要归功于 STM32F401D/E 系列微控制器(首页有关于通信接口的部分)。

有几个其他特性也值得注意。一个是向 5 V 逻辑的普遍性致敬——“所有 GPIO 都能承受 5 伏(Arduino 兼容)”——这对于我们这些经常在这两者之间切换的人来说是一种善意。输出始终是 3.3 V,但 5 V 输入不会让 Pico 心烦。

另一个值得注意的细节是:尽管 18 个引脚(两排各 9 个)的间距为面包板标准的 0.1 英寸,但一个短端上的 8 个引脚间距仅为 0.05 英寸(图 10.8)。在探索中你不会使用这些引脚,因为它们不容易插入面包板,但你可以获得物理垫片来使其成为可能。

图 10.8. Pico 的 8 个 I/O 引脚间距为 0.05 英寸:太窄,无法插入面包板。

图片

当然,这个特定功能列表没有提到这是一个由 JavaScript 驱动的设备,这确实很重要,但你可以从 Espruino 的主页(www.espruino.com)中清楚地看出这一点。

ARM Cortex M 微控制器和嵌入式 JavaScript

Espruino Pico 和 Kinoma Element 都基于 ARM Cortex M 系列的微控制器。正如 ATmega 微控制器被用于各种 Arduino 兼容的主机-客户端类板(如 Arduino Uno 及其同类),ARM Cortex M 微控制器在包括 Element 和 Pico 在内的嵌入式平台类别中非常受欢迎。ARM 的网站声称,已有数十亿个设备使用了 Cortex M 系列的产品。

Cortex M 系列的 32 位微控制器在低功耗下(大多数预测性并不便宜)优于 8 位 ATmegas。嵌入式 JavaScript(或类似 JavaScript)的运行时需要比 8 位 ATmega 提供的更多处理能力。

随着你继续尝试新的平台,你可能会继续遇到 Cortex M 变体。

另一个值得了解的是平台的经济和许可模式。硬件或软件(或两者)是开源的还是专有的?如果你考虑在商业上使用平台、扩展硬件或软件,或以其他方式为平台做出贡献,这可能很重要。(Espruino 平台是完全开源的。)

10.2.2. 寻找引脚图

也许是因为我对地图的喜爱,但找到和分析板上的引脚图通常是事情真正变得清晰的时候。这些图显示了哪些引脚可以做什么:通信接口、PWM、电源引脚等等。

从 Pico 的图示 (图 10.9) 中,我们可以注意几个要点(在 Espruino 的 Pico 文档页面查看更大尺寸的详细内容:www.espruino.com/Pico)。首先,引脚编号不是连续的;它们跳来跳去,你会在你将使用的两个侧面都找到 A 和 B 引脚。你还可以看到几乎每个 GPIO 引脚都支持 PWM。最后,你可以看到哪些引脚具有硬件支持的 I²C 和 SPI,以及哪些可以支持 ADC(模拟到数字转换)。

图 10.9. Espruino Pico 引脚图细节

10fig09_alt.jpg

合理的整体引脚布局(硬件设计)和高质量的引脚图可以带来更好的开发体验。

10.2.3. 了解配置和工作流程

代码是如何编写的?如何部署的?设备是如何管理、配置和更新的?它是否支持你的操作系统?配置过程是否令人沮丧且繁琐?这可能是一个持续头痛的迹象。

在 Pico 的情况下,我们采取了推荐的路线,使用 Chrome 应用 IDE。这可能很方便——代码编写、设备管理和部署都在一个地方,但如果你是那种对自家的编辑器或 IDE 写 JavaScript 有偏爱的人,这可能会让你抓狂。

获取高级软件结构的感觉:是否有插件或其他模块化组件?是否有面向硬件的通用 API?Espruino 两者都有。

10.2.4. 寻找示例和教程

接下来是逐步通过一些 Hello World 示例,你已经用 Pico 做过这个了。当实验一个新的平台时,找出完成一些常见任务的方法,比如闪烁 LED、从模拟传感器读取数据、与显示屏一起工作以及控制 I²C 设备。理想情况下,你将在这一步亲自动手,感受在开发过程中与平台交互的真实感觉。

一旦你理解了整体的大致情况,并看过(尝试过)一些应用示例,查阅参考文档可以帮助你填补细节。

10.2.5. 使用参考 API 文档

如果你浏览 Espruino 的 API 文档(10.10),你会看到熟悉的 JavaScript 类——StringMathBooleanJSON——以及与硬件相关的 Espruino 特定类:I2CSPIWLAN。《全局》部分列出了 Espruino 脚本可用的硬件函数,如 digitalWrite(),以及一些标准的 JavaScript 全局功能,如 setTimeout()eval()

还有一个页面列出了 Espruino 可用的模块以及如何使用它们 (www.espruino.com/Modules)。随着我们对 Pico 的深入了解,我们将进一步探讨这一点。

当你更多地了解一个平台并积累经验时,你会发现自己在从寻找现成示例、概念和教程的学习阶段,过渡到参考阶段,在这个阶段你正在寻找如何完成特定任务的详细信息。

图 10.10. Espruino 的 API 文档在www.espruino.com/Reference

10.3. 在 Pico 上进行实验

接下来,你将尝试使用 Pico 进行一些实验。首先,你将重新访问 BMP180 天气多传感器,但你会将其与 Nokia 5110 LCD 显示组件结合使用,构建一个外观漂亮的、自包含的迷你气象站。

10.3.1. Pico 和 BMP180 多传感器

你需要什么

  • 1 Espruino Pico

  • 1 全尺寸面包板

  • 1 USB 延长线

  • 1 BMP180 扩展板

  • 跳线

由于你正在使用 Web IDE,使用 Espruino 模块与使用 Espruino 代码中的require()语句一样简单,只需指定你想要的模块名称。这些模块为不同类型的组件提供了封装支持。而且,太好了,有一个现有的模块用于你信任的 BMP180 温度和压力多传感器。该模块被称为BMP085,因为它也与类似的 BMP085 传感器兼容。一旦导入模块,你就可以使用它提供的 API 与 BMP180 传感器进行交互。让我们看看这会是什么样子。

BMP085 Espruino 模块

这个实验将记录从 I²C BMP180 传感器获取的当前温度(摄氏度)和压力(帕斯卡)。

从 BMP180 读取数据的代码并不复杂,正如你在列表 10.2 中看到的。它使用了公开的I2C1全局变量来配置一个 I²C 接口,然后将其传递给BMP085模块的connect方法。

正如你所看到的,Johnny-Five 提供了几个用于执行连续、周期性操作(如传感器读取)的结构,例如board.loop。但在这里你并没有使用 Johnny-Five。相反,你将遵循 Espruino 的约定,它使用setInterval进行重复的 I/O 操作。

列表 10.2. 使用 BMP085 Espruino 模块
I2C1.setup({ scl: B6, sda: B7});                                *1*
var bmp = require('BMP085').connect(I2C1);                      *2*
setInterval(function () {
  bmp.getPressure(function (readings) {                         *3*
    console.log('Pressure: ' + readings.pressure + ' Pa');
    console.log('Temperature: ' + readings.temperature + ' C');
  });
}, 1000);                                                       *4*
  • 1 使用 B6 引脚作为 SCL 和 B7 引脚作为 SDA 设置 Pico 的第一个 I²C 接口(I²C1)

  • 2 需要 BMP085 模块并调用其 connect 函数,使用 I²C1 接口

  • 3 实例化的 bmp 对象的 getPressure 方法是异步的;注册一个回调...

  • 4 每秒(1000 毫秒)执行一次传感器读取

你如何知道 B6 和 B7 引脚分别支持 I²C SCL 和 SDA?从引脚分配(图 10.11)中可以看出。

图 10.11. Espruino Pico 和 BMP180 传感器的接线图

构建 BMP180 电路

为了构建这个,你需要在全尺寸面包板上放置 Pico 并使用 USB 延长线。构建图 10.11 中所示的电路。

目前,设置可能看起来有点混乱。“为什么要把 BMP180 传感器放在 Pico 那么远的地方?”你可能合理地想知道。这里有一个方法:产生的间隙将允许在未来的实验中扩展此电路以容纳诺基亚 5110 LCD 组件。

全尺寸面包板连接

如果这是你第一次使用全尺寸面包板,请注意,从电气连接的角度来看,它实际上是由两个半尺寸面包板首尾相连组成的。全尺寸面包板的一个问题是,电源轨在板的中部有断开连接(图 10.12)。

图 10.12. 不要忘记!全尺寸面包板上的电源轨在板的中部有断开连接。

部署代码

将 代码清单 10.2 中的代码输入到 IDE 的右侧,然后点击发送到 Espruino 的向上箭头图标以在 Pico 上执行代码。记录的压力和温度将出现在 IDE 窗口的控制台/终端(左侧)侧(图 10.13)。

图 10.13. 一旦将 BMP180 脚本上传到 Pico,你应该每秒在屏幕左侧看到一次输出日志。

10.3.2. Pico 和诺基亚 5110 LCD

你需要

  • 1 个 Espruino Pico

  • 1 个诺基亚 5110 48x84 LCD 开发板

  • 1 个全尺寸面包板

  • 1 个 100 V 电阻

  • 跳线

诺基亚 5110 48x84 像素显示屏(图 10.14)最初用于流行的诺基亚 51xx 系列手机,这些手机始于 20 世纪 90 年代末(顺便说一句,它们是很好的手机——它们以耐用性、出色的电池寿命和易用性而闻名)。诺基亚 5110 LCD 单元在网上可以以低至 6 美元的价格找到,但它们在每件约 10 美元的地方更容易找到。它们是很好的小部件:48x84 像素并不是无限的面积,但它比我们迄今为止看到的 16x2 多得多。有空间绘制、动画和做有趣的事情。

图 10.14. 诺基亚 5110 LCD 具有由其飞利浦半导体 PCD8544 驱动器提供的 48x84 像素分辨率和 SPI 接口。显示屏在此处是正立的。

该显示屏由名为 PCD8544 的飞利浦半导体驱动器控制,这种组件通常具有那种不令人难忘的方式。PCD8544 为显示屏提供 SPI 接口,而且(太好了!)还有一个 Espruino 模块用于此控制器。

你将首先使用诺基亚 5110 独自创建一个视觉倒计时计时器,然后你将结合 BMP180 来制作一个小型气象站。

诺基亚 5110/PCD8544 引脚图

不同的 PCD8544/Nokia 5110 模块有不同的引脚图!在尝试遵循图 10.15 中的布线图之前,请检查你板上的引脚标记。图 10.15 的连接应该印在板上。

电路图中所使用的布局基于 SparkFun 产品页面上可用的 5110 变体(mng.bz/IId1),连接方式如图所示。值得注意的是,SparkFun 模块的引脚排列与 Espruino 网站上“Pico LCD 显示 Hello World”教程中假设的引脚排列不同(mng.bz/604s),但 SparkFun 记录的布局似乎更为常见。

本章中的接线图基于诺基亚 5110 的 SparkFun 版本,并假设引脚排列如图所示。检查您的 5110 的引脚排列,并在它们不同的情况下调整电路中的连接。

请参阅表 10.1 了解具体哪些 LCD 模块引脚连接到 Pico 的哪些引脚。

将 LCD 连接到 Pico

保持 BMP180 从上一个实验中的连接——你将在一分钟后再次使用它——并将诺基亚 5110 连接到全尺寸面包板上的空闲部分,如图图 10.15(并在表 10.1 中总结)所示。

图 10.15. 将诺基亚 5110 LCD 组件添加到电路中的接线图

表 10.1.诺基亚 5110 LCD 连接
LCD 模块引脚 LCD 引脚功能 连接到 Pico 引脚 图中电线颜色
VCC 或 Vin 3.3 V 电源 3.3 V 红色
GND GND 黑色
CE 或 SCE SPI 芯片选择 B14 蓝色
RST 复位 B15 白色
DC 或 D/C 数据/命令 B13 橙色
MOSI 或 DN SPI 主出,从入 B10 绿色
SCK 或 SCLK SPI 时钟 B1 黄色
LED LED 背光 3.3 V 电源 3.3 V,通过 100 Ω 电阻 红色

接线图和美观

如果你研究图 10.15,你会注意到一些细节,这些细节既指向清晰度,也指向美观,无论是在电路图本身还是在结果电路中。

例如,注意电源连接(红色电线)。BMP180 和诺基亚 5110 的背光 LED 现在共享一个正电源连接。LCD 的背光 LED 电源通过一个 100 Ω 电阻连接——它是一个 LED,所以这个 100 Ω 的电阻值对于 3.3 V 电路来说是一个很好的选择。

请记住,全尺寸面包板的电源轨在面包板长边的中部有断开(全尺寸面包板实际上相当于两个半尺寸板粘在一起)。因此,图中多出的短地线:它将地电源连接到板的另一半地轨,桥接了连接间隙。

当你与电路图打交道时,你经常会看到为了保持电路外观“整洁”而做出的折衷方案,比如那个分割的接地连接。另一个例子在图 10.15 中,是诺基亚 5110 的 D/C(数据/命令模式)的橙色电线;连接被分成两段,这样它就不会在视觉上与其他组件或电线重叠。其他连接在使用单独的电线完成连接之前,被桥接在面包板的中央缺口上。

实现相同结果电路的方法有很多。如果你想要节省时间或所需的电线,每个多线连接都可以用一根单独的电线来完成。

同样的电路,没有为了整洁而做折衷方案

使用诺基亚 5110 制作视觉倒计时器

为了熟悉诺基亚 5110 和 Espruino 的Graphics功能,这个实验创建了一个 10 秒的定时器,它使用 LCD 上的动画进度条显示进度(图 10.16)。当然,你可以在代码中调整定时器的持续时间。定时器通过按下 Pico 的内置小按钮启动。

图 10.16. 定时器的显示。填充的矩形是“动画”的,并且随着时间的流逝向右增长。

Espruino PCD8544 模块

要为定时器编写程序,你将使用 Espruino PCD8544 模块。代码首先设置一些变量,创建一个初始化函数来设置定时器(10.3),如下所示。

列表 10.3. 设置定时器
var lcd;
var progress      = 0; // Current timer progress
var frameDuration = 200; // ms (5fps)
var timerSeconds  = 10;
var timerLength   = timerSeconds * 1000 / frameDuration;       *1*
var timerActive   = false;                                     *2*

function onInit () {
  SPI1.setup({ sck: B3, mosi: B5 });                           *3*
  lcd = require('PCD8544').connect(SPI1, B13, B14, B15);       *4*
}

onInit();                                                      *5*
  • 1 确定定时器持续多长时间(“滴答”或“帧”数)

  • 2 跟踪定时器是否正在运行

  • 3 为 LCD 配置 SPI 接口

  • 4 使用 PCD8544 Espruino 模块实例化表示 LCD 的对象

  • 5 调用 onInit()函数开始工作

接下来,你需要找到一种让用户激活定时器的方法。你可以使用 Pico 的板载按钮作为触发器,通过使用一些 Espruino 全局优点:setWatch(function, pin, options)函数和虚拟BTN引脚来启动定时器。

setWatch()函数提供了中断类似的行为,允许你注册一个回调,当监视引脚的值发生变化时被调用。在列表 10.4 中,setWatch()用于持续监视BTN。在启动定时器之前,代码确保没有其他定时器正在运行,然后重置定时器的progress并开始操作。

中断类似的行为?

setWatch函数提供了中断类似的行为。技术细节在 Espruino 的 API 文档中解释了 setWatch(mng.bz/EE71):“内部,中断将引脚状态变化的时间写入队列,而提供给setWatch的函数只从主消息循环中执行。”

列表 10.4. 启动定时器
// variables
function onInit() { /* ... */ }
setWatch (function (e) {
  if (!timerActive) {
    progress = 0;
    setInterval(draw, frameDuration);           *1*
    timerActive = true;
  }
},
  BTN,                                          *2*
  { repeat: true });                            *3*
  • 1 每个 frameDuration(200 ms)调用一次 draw 函数

  • 2 setWatch 的第二个参数指定要监视的引脚——Pico 的板载按钮。

  • 3 第三个参数是选项,这里指定监视应无限期进行(重复:true)。

计时器通过重复调用名为draw的函数在计算间隔内运行。但draw函数是什么?你需要编写它!你的draw函数的任务将包括增加计时器的进度并在 LCD 屏幕上渲染其比例进度。

PCD8544 模块的connect方法返回的对象——在您的代码中分配给变量lcd——提供了一些 LCD 特定的方法,如flip(),它将缓冲区当前内容显示在屏幕上,以及setContrast()——该方法做的是它听起来会做的事情。此外,该对象继承自 Espruino 的Graphics库(www.espruino.com/Graphics),为你提供了将文本字符串渲染到屏幕上以及绘制线条和形状的工具。

清单 10.5 中的draw函数使用drawRect(x1, y1, x2, y2)方法绘制表示总计时时间的框的轮廓。然后使用fillRect(x1, y1, x2, y2)绘制一个代表到目前为止经过时间的填充进度条。draw函数真正需要做的数学计算就是确定填充矩形应该有多宽——LCD 可用的 84 个水平像素中有多少代表已经过去的时间比例。这是计算并分配给rightEdge变量的。总结一下:绘制一个空矩形——进度条的轮廓,垂直居中于屏幕上,然后在其中绘制一个计算宽度的填充矩形。

清单 10.5. 绘制计时器
// ...
function draw () {
  progress++;
  if (progress > timerLength) {                                  *1*
    clearInterval();
    timerActive = false;
  }

  var rightEdge = Math.floor((progress / timerLength) * 84) - 1; *2*
  lcd.clear();
  lcd.drawRect(0, 19, 83, 27);                                   *3*
  lcd.fillRect(0, 19, rightEdge, 27);                            *4*
  lcd.flip();                                                    *5*
}
  • 1 如果计时器完成,关闭它并停止

  • 2 计算填充进度条的右侧 x 轴位置

  • 3 使用 drawRect(x1, y1, x2, y2)绘制一个垂直居中、高度为 8 px 的空框

  • 4 使用 fillRect(x1, y1, x2, y2)来绘制表示到目前为止进度的填充框

  • 5 将所有内容绘制到 LCD 屏幕上

将计时器代码粘贴到 Espruino IDE 的右侧,连接到 Pico,并上传代码。按下 Pico 的按钮以开始计时。

如果你想调整代码,你可以改变计时器的持续时间,或者当计时器完成时,让 LCD 显示一条消息,例如。

10.3.3. 使用 Pico 构建高效能的气象设备

你在构建迷你气象设备方面变得越来越专业,现在又有一个新设备可以加入你的装备库。通过将你的老朋友 BMP180 传感器与诺基亚 5110 显示屏结合,你可以组装成一个独立、格式良好的低功耗气象设备(图 10.17)。

图 10.17. 天气小工具的输出将显示温度和空气压力,格式良好。

如果你跟随着前两个实验,你已经有了所需的电路:BMP180 和诺基亚 5110 连接到 Pico 上的全尺寸面包板上(图 10.18)。你将依赖 Espruino Graphics 库的一些更多功能,以便你可以绘制矢量字体和更多形状来格式化数据显示,并将生成的代码部署到 Pico 的闪存中,以便 Pico 在提供电源时可以独立运行程序。

图 10.18. 使用 Espruino 的 Graphics 支持创建形状和绘制文本字符串

与计时器一样,你将首先设置一些变量和一个初始化函数,如列表 10.6 所示。如果对当地海拔(以米为单位)进行调整,BMP180 的压力读数将更加准确。getSeaLevel 方法,可在 BMP085 模块的 connect() 函数返回的对象上使用,为你提供了一个方便的方式来执行海拔校正。请注意,这里使用 getPressure 方法同时读取压力和温度。确保将下一列表中的 altitude 变量的值调整为你的当地海拔(以米为单位)。

列表 10.6. 设置天气小工具
var altitude = 300; // Local altitude in meters: CHANGE ME!
var lcd;

function onInit () {
  clearInterval();
  I2C1.setup({ scl: B6, sda: B7});
  var bmp180 = require('BMP085').connect(I2C1);
  SPI1.setup({ sck: B3, mosi: B5 });
  lcd = require('PCD8544').connect(SPI1, B13, B14, B15, function () {    *1*
    setInterval(function () {
      bmp180.getPressure(function (readings) {
        draw(readings.temperature,
             bmp180.getSeaLevel(readings.pressure, altitude));           *2*
      });
    }, 1000);
  });
}

onInit();                                                                *3*
  • 1 一旦设置好 LCD,然后启动 setInterval。

  • 2 使用当前温度和调整后的海拔压力调用 draw。

  • 3 不要忘记调用 onInit 函数!

直线、圆和带有 Espruino 图形的文本

与计时器一样,你需要编写 draw 函数。列表 10.7 中的代码使用了来自 Graphics 库的更多形状绘制方法:drawLine(x1, y1, x2, y2)drawCircle(x, y, radius) (图 10.18)。它还利用了一些获取尺寸的方法:例如 getWidth()getHeight(),它们分别返回显示器的可用区域,以像素为单位,对于 x 和 y 轴。最后,stringWidth(str) 使用当前字体设置计算给定字符串的像素宽度。

让我们谈谈字体。有一个可用的位图字体,其字符大小为 4 x 6 像素。要使用位图字体,你使用 setFontBitmap() 方法使其成为活动字体。然而,在这个例子中,你将使用矢量字体。矢量字体可以用于各种大小——它可以缩放。setFontVector(size) 方法将活动字体设置为具有 size 像素高度的缩放矢量字体。

在下面的draw函数中有很多数字。它看起来有点挑剔,但这里的一切都是简单的像素运算,用于定位文本和形状的元素。请注意,绘图函数中的字符串“mb”(毫巴的缩写)是“手工调整字距的”,因为我发现用这种字体大小一次性绘制字符串会使字母难以辨认。

列表 10.7. 渲染天气显示
function draw (temperature, pressure) {
  lcd.clear();
  // Convert temperature to Fahrenheit and format to one decimal place
  var tempString = (temperature * (9 / 5) + 32).toFixed(1);
  // Convert pressure from pascals to millibars and format to one decimal
   place
  var pressString = (pressure / 100).toFixed(1);

  // Draw a vertically centered line across the display
  lcd.drawLine(0, (lcd.getHeight() / 2), lcd.getWidth(),
   (lcd.getHeight() / 2));

  // Set the active font to 18 pixels high
  lcd.setFontVector(18);
  // Calculate the pixel width of the temperature value at 18px font
  var tempWidth  = lcd.stringWidth(tempString);
  // Calculate the pixel width of the pressure value at 18px font
  var pressWidth = lcd.stringWidth(pressString);
  // The temperature will be horizontally centered
  // Determine the x coordinate for where the value should be displayed
  var xTemp      = ((lcd.getWidth() - tempWidth) / 2);

  // Render the temperature at point (xTemp, 2)
  lcd.drawString(tempString, xTemp, 2);
  // Render a degree symbol (circle) of radius 2px
  // at point (xTemp + tempWidth + 4, 5)
  // The center of the circle will be 4px to the right of the
  // end of the temperature value string
  lcd.drawCircle(xTemp + tempWidth + 4, 5, 2);
  // Render the pressure value left-aligned, 2px below vertical center
  lcd.drawString(pressString, 0, (lcd.getHeight() / 2 + 2));

  // Set a smaller font for the unit characters
  lcd.setFontVector(8);
  // Draw an "F" to denote Fahrenheit
  lcd.drawString('F', xTemp + tempWidth + 2, 12);
  // Draw "mb" (millibar) string.
  lcd.drawString('m', pressWidth + 3, (lcd.getHeight() / 2 + 12));
  lcd.drawString('b', pressWidth + 12, (lcd.getHeight() / 2 + 12));
  lcd.flip();
}

将天气小部件的所有代码放入 Espruino IDE 的代码部分,并使用“发送到 Espruino”图标在 Pico 上运行代码。直到从电脑的 USB 端口拔掉它,它将显示温度和压力。

但你可以做得更好!在 IDE 的左侧,输入命令save()并按 Enter 键。这将把代码闪存到 Pico 上。现在,每当 Pico 有电时,它将恢复运行此代码。尝试通过将 Pico 插入 USB 电源,如手机充电器,来测试它。

功耗和 LCD 的背光

为了使天气小部件更加节能,你可能考虑断开 LCD 的 LED 背光连接电源。在黑暗的房间里你将无法阅读 LCD,但它将消耗更少的电力。

10.4. 在 Kinoma Element 平台上进行实验

为了重复平台探索的过程,我们将简要地看看另一个嵌入式 JavaScript 平台:Kinoma Element。

Element 是一个小型、由 JavaScript 驱动的物联网平台,拥有 16 个可编程引脚(图 10.19)。像 Espruino Pico 一样,它价格低廉——Element 的价格大约是 20 美元或稍多一点。同样,像 Pico 一样,它没有其更加强大(且昂贵)的兄弟产品的所有功能——你不会在板上找到 USB、以太网、SD 卡或其他外围设备,但它以一个高效的小包装提供了物联网产品所需的基本组件。此外,它还内置了 WiFi 支持。

图 10.19. Kinoma Element

10.4.1. Element 的核心功能

芯片制造商 Marvell 生产 Element(mng.bz/w1lR),它具有 Marvell MW302 系统级芯片(SoC),该芯片使用 200 MHz 的 ARM Cortex M4。板、外壳、JavaScript 运行时和框架软件都是开源的。

要在只有 512 KB RAM 的情况下原生运行 JavaScript,Element 使用一种称为 XIP(原地执行)的技术。Element 运行 FreeRTOS,这是一个精简和最小化的开源操作系统。板的供电电压是 3.3 V。

Element 被封装在一个外壳中,这使得它看起来更像是一个成品设备(外壳设计也是开源的)。它没有专门的电源引脚,你可以根据需要将 Element 的任何 16 个引脚(每侧 8 个)配置为 3.3 V 或地。

Kinoma 使用自己的(Apache 许可的)JavaScript 引擎 XS6,除了少数例外,声称与 ES6 兼容。请注意,Element 的 IDE 软件适用于 Mac 和 Windows(beta),但未提供 Linux 支持。

10.4.2. 引脚布局和硬件图

Element 的引脚布局相当简单(图 10.20)。在 I/O 功能支持方面,它比 Pico 简单;例如,有两个 I²C 接口,但没有 SPI 支持。另一方面,确定哪些引脚做什么要简单得多,编号也容易跟随。

图 10.20. Kinoma Element 的引脚布局图

10.4.3. 配置、管理、工作流程

Element 是一个整洁、自包含的包,不需要任何焊接或准备。跳线可以直接插入其中。

配置和工作流程与 Pico 类似,因为有一个 IDE 用于配置、编写和部署。如果您手头有 Element,可以查看快速入门指南以获取详细信息(mng.bz/84cS),但以下是一般步骤:

  1. 下载并安装 Kinoma Code IDE。

  2. 在您的 WiFi 网络上设置 Element。

  3. 应用固件更新。

在 Kinoma Code IDE(图 10.21)中开发的 Element 代码项目可以通过 USB 或 WiFi b/g/n 部署。设置涉及将 Element 连接到您的本地 WiFi 网络。它将被分配自己的 IP 地址。

图 10.21. Kinoma Code IDE 软件

Element 的应用项目具有一定的结构。例如,每个项目都必须包含一个 project.json 文件,该文件定义了入口点——将在设备上执行的脚本。入口点默认为 main.js。

与 Espruino 类似,Kinoma 提供了一些全局对象来帮助您与硬件交互,并且有模块封装组件行为的概念。用于控制硬件组件的 Kinoma 模块被称为闪烁灯库(BLLs),它们涉及通过(内置的)引脚模块与硬件交互。您可以使用 CommonJS 风格的require语句将其他 JS 文件拉入您的项目,以及任何内置或自定义的 BLL 模块(但请记住,这并不是 Node.js:您不能使用 npm 模块)。

10.4.4. 示例和教程

Kinoma 网站上有一些 Element 的代码示例:mng.bz/1BaB。查看闪烁 LED 的示例代码(mng.bz/5t61),可以立即看出 Kinoma 项目的结构比某些其他平台更为正式(图 10.22)。闪烁 LED 需要通过一个 project.json 文件来定义项目,一个 main.js(入口点)来初始化板子并配置 LED 的引脚,以及一个 led.js BLL 模块,它通过toggle方法提供闪烁的逻辑支持。(项目中的.project 文件和 XML 文件似乎是用于 Kinoma 网站特定的构建和元数据支持。)

图 10.22. 闪烁 LED 的 Kinoma 示例项目的源代码。Kinoma 项目的结构比 Espruino 项目更为复杂。

你还需要在聚会上自带 LED 灯和电阻,因为板上没有明显的 LED 灯可以使用。这个项目 main.js 文件中的代码假设你使用的是 9 号和 10 号引脚(接地),但并未提供接线图。

你将在稍后了解更多关于 Kinoma main.js 脚本中代码的样子。

10.4.5. API 参考

Kinoma 模块提供了硬件交互的 API。最相关的模块是Pins,它提供了基本的 I/O 支持,包括你预期的类型:数字和模拟输入输出;脉冲宽度调制(PWM);串行(例如 I²C)。你可以在 Kinoma 网站上找到程序员指南(mng.bz/w1lR)。

10.4.6. 案例研究项目:实时更新的指南针读数

你需要准备的东西

  • 1 Kinoma Element

  • 1 USB A to USB micro 线缆

  • 1 面包板

  • 1 HMC5883L 磁力计扩展板

  • 跳接线

Element 内置 WiFi 和 Web 实用库,比 Pico 更明显地可以作为 Web 服务器使用。

在这个实验中,你将查看使用 Element 创建项目的高级过程,制作一个自定义 BLL 来支持 HMC5883L I²C 磁力计(指南针)。你将使用 Kinoma 的 WebSocket 模块在 Element 上运行 WebSocket 服务器,当指南针方向改变时可以发出变化。最后,你将构建一个 HTML 文档,该文档将连接到 Element 的 WebSocket 服务器,并在指南针方向改变时更新(图 10.23)。

图 10.23. 浏览器显示的细节,显示指南针方向。指南针方向将实时更新,无需刷新浏览器。在这种情况下,当前方向是 190.62 度——稍微偏西。

HMC5883L 模块将被连接到面包板上。通过旋转面包板,你可以改变磁力计的方向,并在浏览器中实时看到更新的方向。

WebSocket 浏览器支持

你之前已经遇到过 WebSocket 协议。在第八章中,你使用了 Tessel 2 气象站应用程序中的 socket.IO 来显示实时更新的温度和压力。Socket.IO 为支持 WebSocket 的浏览器使用 WebSocket,对于不支持 WebSocket 的浏览器,它将回退到其他方法。

在这个例子中,你将使用 WebSocket 的正确用法:不支持 WebSocket 的浏览器中应用程序将无法工作。如今,浏览器中缺少 WebSocket 支持的情况极为罕见,因此你不太可能遇到问题。

构建电路

将 HMC5883L 放置在面包板上,并按图 10.24 所示连接到 Element。断出板上的 SDA 和 SCL 引脚连接到 Element 的 13 号和 14 号引脚,如图图 10.20 中的引脚图所示,它们支持 I²C。Element 上的任何引脚都可以配置为电源或地;这里使用 11 号和 12 号引脚是因为它们靠近 I²C 引脚。

图 10.24. Kinoma Element 和 HMC5883L

项目结构

Element 实时指南针项目由四个文件组成(图 10.25):

  • 包含 Kinoma 项目元数据的 package.json 文件

  • 一个 main.js 文件,作为应用程序的主要模块(入口点)

  • 一个 HMC5883L.js 文件,这是用于指南针的自定义 Kinoma BLL 模块

  • 一个 index.html 文件,这是客户端代码——你在网页浏览器中查看它

图 10.25. Element 实时指南针项目的结构。main.js 文件提供应用程序逻辑和 WebSocket 服务器,依赖于 HMC5883L.js 中的 BLL 支持与磁力计交互。project.json 文件定义了项目,使用 Kinoma 约定。Index.html 在您的计算机上的网页浏览器中运行,并显示实时更新的指南针方向。

首先,创建 package.json 文件并输入一些元数据,如下一列表所示。

列表 10.8. project.json
{
  "id": "compass.websockets.lyza.com",       *1*
  "title": "hmc5883L",
  "Element": {
    "main": "main"                           *2*
  }
}
  • 1 ID 字符串应根据文档中的说明采用“点域名样式”。

  • 2 应用程序的入口点将是 main.js。

创建应用程序的结构

Kinoma 项目应用程序模块——在这个例子中是 main.js——有一个通用结构。它们需要导出一个default函数,该函数实现一些事件处理器,例如onLaunch()onQuit()。启动处理器配置板上的引脚并启动应用程序。

该模块的基本结构如下。

列表 10.9. main.js 的结构
import Pins from 'pins';                                                   *1*
import { WebSocketServer } from 'websocket';                               *2*

var formatMsg = heading => JSON.stringify({ heading: heading.toFixed(2) });*3*
var main = {
  onLaunch () {                                                            *4*
    Pins.configure({
      // Configure the HMC5883L via a custom BLL module that still needs
       to be written
    }, success => {                                                        *5*
      if (success) {
        // Set up a WebSocket server
        // read compass headings and emit changes
      } else {
        // Handle failure with the built-in `trace` function
        trace('Failed to configure\n');
      }
    });
  }
};

export default main;
  • 1 这是内置的 Pins 模块,Kinoma 应用中的许多功能都依赖于它。

  • 2 Kinoma 有一个内置的 websocket 模块;你需要使用其中的 WebSocketServer。

  • 3 这是一个用于格式化指南针头部的便利函数(尚未使用)。

  • 4 onLaunch()将在启动时自动调用。

  • 5 Pins.configure 接受一个回调函数,当它完成时调用。

一旦自定义 HMC5883L 模块准备就绪,你将返回并填写 main.js 中的空白。

HMC5883L 的自定义 BLL

BLL(或闪烁灯库)封装了与 Kinoma 兼容的组件行为在一个模块中。BLL 模块需要执行某些操作。根据 Kinoma 文档,BLL 至少需要导出一个定义其使用的引脚类型的 pins 对象、一个 configure 函数和一个 close 函数。

下面的列表显示了完成的 HMC5883L BLL 模块的部分内容。

列表 10.10. BLL 代码细节
// From datasheet: various register addresses for the device
var registers = {
  CRA: 0x00,
  CRB: 0x01,
  MODE: 0x02,
  READ: 0x03,
};

/* ... */

// Required export object
// Configure the pins involved as I2C at address 0x1E (from datasheet)
exports.pins = {
  compass: {type: 'I2C', address: 0x1E }
};

// Required export function
exports.configure = function () {
  this.compass.init(); // Get I2C going
  // Derived from Johnny-Five Compass class support for HMC5883Ls
  this.compass.writeByteDataSMB(registers.CRA, 0x70);                     *1*
  this.compass.writeByteDataSMB(registers.CRB, 0x40);
  this.compass.writeByteDataSMB(registers.MODE, 0x00);
};

// Required export function
exports.close = function () {
  this.compass.close(); // Cleanup; boilerplate
};

// Can be invoked repeatedly to read data from sensor
exports.read = function () {
  // Derived from Johnny-Five Compass class, again
  var bytes = this.compass.readBlockDataSMB(registers.READ, 6, 'Array');  *2*
  var data = {                                                            *3*
    x: int16(bytes[0], bytes[1]),                                         *4*
    y: int16(bytes[4], bytes[5]),
    z: int16(bytes[2], bytes[3])
  };
  return toHeading(data.x, data.y);                                       *5*
};
  • 1 writeByteDataSMB() 由 Kinoma I²C API 提供;它从特定的寄存器地址读取。

  • 2 readBlockDataSMB(),同样来自 API,用于从 READ 寄存器获取六个字节的数组。

  • 3 HMC5883L 的数据由三个轴的每个轴的两个字节组成。

  • 4 int16() 是一个实用函数,用于从两个字节中创建一个 16 位整数(实现未显示)。

  • 5 toHeading() 使用数学从数据中推导出航向(实现未显示)。

HMC5883L Kinoma BLL 模块的贡献者

我自己拼凑了这个 BLL,作为探索 BLL 模块和 I²C 在 Element 上如何工作的一个尝试,但代码大量借鉴了现有的工作。它实际上是 Johnny-Five 对该传感器的支持逻辑的移植(mng.bz/TxHV),由 Johnny-Five 的发明者 Rick Waldron 编写。Johnny-Five 的代码反过来依赖于 Arduino 的早期实现(mng.bz/nB4V),而 Arduino 的实现又依赖于该设备的数据表(mng.bz/j67k)。

这种复杂的血统在开源软件中是常见的,但作为提醒,始终检查你的许可证并确保你遵守它们。此外,对你的工作的灵感来源表示感谢。

列表 10.10 只显示了完成的 BLL 的一部分。完整的 5883L BLL 可以在本书的代码仓库中找到。如果你想构建这个实验,你需要它:将其放置在其他项目文件相同的目录中。

完成应用程序代码

在完成 BLL 后,main.js 中依赖于 BLL 的部分可以填充。如下一个列表所示,onLaunch 处理程序通过传递一个设置对象到 Pins.configure() 来在 Element 上设置指南针传感器。

列表 10.11. 配置引脚
/* ... */
var main = {
  onLaunch () {
    Pins.configure({
      compass: {
        require: 'HMC5883L',                    *1*
        pins: {
          compass: { sda: 13, clock: 14 },
          ground: { pin: 12, type: 'Ground' },
          power: { pin: 11, type: 'Power' }
        }
      },
    }, success => {   } );                      *2*
  }
};
  • 1 自定义 BLL 模块,按文件名,无扩展名

  • 2 回调函数

你还需要将一个回调函数作为第二个参数传递给 Pins.configure()

在那个回调函数中,你首先需要启动一个 WebSocket 服务器,如下一个列表所示。此代码使用了内置的 Kinoma WebSocketServer API(WebSocketServer 在 列表 10.9 中是必需的)。

列表 10.12. WebSocket 服务器设置
/* ... */
var main = {
  onLaunch () {
    Pins.configure({
      /* ... */
    }, success => {
      if (success) {
        const clients = new Set();                         *1*
        const wss = new WebSocketServer(80);               *2*
        let lastResult = 0;                                *3*

        wss.onStart = client => { // When a client (browser) connects
          clients.add(client);
          // Immediately send the latest compass heading
          client.send(formatMsg(lastResult));
          // Clean up when closing later
          client.onclose = () => clients.remove(client);
        };
      }
    });
  }
};
  • 1 支持 ES6 功能,因此可以使用 Set

  • 2 在 Element 的端口 80 上启动 WebSocket 服务器

  • 3 保存最后的指南针航向读取

列表 10.12 中的代码在客户端连接时发出初始航向读取,但航向读取最初是如何获得的,以及当读取值发生变化时客户端如何接收更新?以下列表中显示的 main.js 的最后几行代码负责处理这些事情。

列表 10.13. 读取和更新航向
/* ... */
var main = {
  onLaunch () {
    Pins.configure({
      /* ... */
    }, success => {
      if (success) {
        /* ... */
        Pins.repeat('/compass/read', 500, result => {       *1*
          if (Math.abs(result - lastResult) >= 0.5) {       *2*
            clients.forEach(recipient => {                  *3*
              recipient.send(formatMsg(result));
            });
          }
          lastResult = result;
        });
      }
    });
  }
};
  • 1 每 500 次读取

  • 2 如果新的结果(航向,以度为单位)与上次结果相差某个阈值...

  • 3 ...它已经发生了有意义的改变。将新值发送给每个已连接的客户端。

BLL 中的函数作为路径

BLL 中的函数通过路径在外部引用:

Pins.repeat('/compass/read', 500, result => { });

在这里,/compass/read 是 BLL 模块中 read 函数的“路径”。这里的代码每 500 毫秒重复调用一次 read,并且一个回调函数接收最新读取操作的 result。请参见 列表 10.13 中的演示。

客户端代码(HTML)

最后,您需要一个 HTML 页面作为 WebSocket 客户端,并实时显示航向。此页面可以在浏览器中查看。

列表 10.14. 显示航向的客户端代码(HTML 页面)
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Live Compass Heading</title>
    <style>
      #compass {                                                 *1*
        text-align: center;
        font-size: 2em;
        font-family: "Helvetica", sans-serif;
        margin: 2em;
      }
    </style>
    <script>
      window.addEventListener('load', function () {              *2*
        var ws = new WebSocket('ws://10.0.0.17:80');             *3*
        ws.onmessage = function (message) {                      *4*
          var data = JSON.parse(message.data);
          document.getElementById('direction').innerHTML = data.heading;
        };
      });
    </script>
  </head>
  <body>
    <div id="compass">
      <label for="direction">Compass Heading</label><div id="direction">...
       </div>
    </div>
  </body>
</html>
  • 1 航向显示的样式

  • 2 在加载时连接到 WebSocket 服务器

  • 3 重要!您需要将其更改为您自己的 Element 的 IP 地址。

  • 4 当新数据到来时,更新 #direction 元素中的 HTML 以显示新的航向

部署航向代码

通过 Kinoma Code IDE 将代码部署到 Element。请注意,在这个例子中,index.html 文件不是从 Element 服务器上提供的。相反,您需要在 Element 运行航向应用程序后,在浏览器中打开该文件。如果您还没有这样做,请参阅 Element 快速入门指南 (mng.bz/84cS),了解如何从 Kinoma Code IDE 连接到并部署代码到您的 Element 的逐步说明。

一旦代码部署并运行,您就可以在浏览器中打开 index.html 文件,并旋转连接有罗盘的面包板,以实时查看显示更新。

摘要

  • 嵌入式 JavaScript 平台使用优化的硬件和固件来执行 JavaScript 的子集。Kinoma 和 Espruino 都维护自己的开源 JavaScript 引擎(分别称为 KinomaJS 和 Espruino JavaScript),以实现这一点。

  • 嵌入式 JS 平台通常具有更复杂的处理器——通常是 32 位——但仍然对内存和程序空间有显著的限制。

  • Espruino 的开源产品系列包括 Pico,这是一个尺寸较小的开发板。Espruino 项目可以利用 Espruino 特定的模块来处理不同类型的组件。

  • Kinoma Element 是另一个由 JavaScript 驱动的开源设备。为 Element 创建项目涉及使用称为 BLL(闪烁灯库)的组件模块。

  • 虽然有许多平台选项,但你可以通过以下步骤加快学习新平台的速度:了解核心细节、查找硬件和引脚信息、理解工作流程、尝试示例和寻找 API 文档。

第十一章. 使用 Node.js 和微型计算机构建

本章涵盖

  • 在单板计算机(SBC)平台上开始 Node.js 硬件开发

  • 收集组件并设置 Raspberry Pi 3 Model B 系统

  • 在 Raspberry Pi 上 GPIO 的工作原理,以及使用 JavaScript 控制它的不同选项

  • 将 Johnny-Five 气象站应用程序适配到多个不同的平台——Tessel 2、Raspberry Pi、Arduino 和 BeagleBone Black

  • 使用 GPIO 丰富的 BeagleBone Black 开源 SBC

单板计算机(SBCs)是小型动力强大的设备,将通用计算与嵌入式系统的特性相结合(图 11.1)。这些小巧的计算机将众多外围设备和好东西打包进一个小巧的封装中:多个 USB 端口、蓝牙、WiFi、以太网——这些都是你从台式电脑上期望的功能。但它们有几个特性非常适合嵌入式应用:它们的尺寸减小、价格较低、GPIO 支持以及相对的电力效率(虽然它们不像更简单的嵌入式平台那样节能,但它们确实比台式电脑兄弟需要更少的电力)。

图 11.1. 单板计算机(SBCs),从左到右:带有 Arduino 扩展板的 Intel Edison 模块、Raspberry Pi 2 Model B 和 BeagleBone Black

这不是你第一次在这本书中看到 SBCs,但让我们广泛地回顾一下 SBC 是什么。这个术语没有正式的定义,但 SBC 平台通常具有以下特点:

  • 运行高级操作系统;在大多数情况下,如果你选择的话,可以安装不同的操作系统(通常是 Linux,但并非总是如此)

  • 提供类似桌面电脑的通用功能,例如支持 USB 外设、显示器、声音等

  • 提供 GPIO 选项,尽管这些有时会排在平台其他功能的后面

在这个类别中,Raspberry Pi 平台是当之无愧的巨头,这是一系列广受欢迎的单板计算机。因此,我们将在本章的大部分内容中深入探讨 Node.js 和 Raspberry Pi 3 Model B。但我们也将对 BeagleBone Black 板进行简要的浏览,作为本 SBC 类别中平台的第二个示例。

对于本章,你需要以下内容:

  • 1 Raspberry Pi 3 Model B 和 5 V 电源

  • 1 microSD 卡和适配器

  • 1 Adafruit T-Cobbler、SparkFun Pi Wedge 或类似产品,或一套公对母跳线

  • 1 标准 LED,任何颜色

  • 1 100 V 电阻

  • 1 Adafruit BMP180 多传感器扩展板

  • 跳线

  • 1 BeagleBone Black

  • 1 Arduino Uno

  • 1 半尺寸面包板

Tessel 2 和单板计算机的定义

Tessel 2 几乎是一个独立的设备类别。它的 USB 外设、网络功能和高级操作系统(OpenWrt Linux)似乎暗示了单板计算机(SBC)的领域。然而,它的有限 RAM 和闪存存储,以及缺乏类似桌面的外设支持(如显示器),表明它是为嵌入式应用而设计的。

那么它是不是一个单板计算机?我倾向于认为是有条件的“是”,但如果你对 SBC 的定义明确包括能够插入显示器并用作台式计算机的能力,那么就不是!

11.1. 与微型计算机一起工作

单板计算机当然比它们更简单的开发板兄弟更复杂。它们也可以相应地更复杂,需要设置和配置。在许多情况下,你将在软件和硬件领域都面临大量的选择。不过,别慌张:使用预刷了 NOOBS(新开箱即用软件)的 SD 卡将 Raspberry Pi 3 从零开始到全速运行可以非常简单。但你需要卷起你的 Linux 衣袖,花些时间在终端上,以整理出一个舒适、以 Node.js 为中心的流程。

别紧张。如果我们检查的任何 SBC(Raspberry Pi 3 和 BeagleBone Black)的设置步骤出现问题,你总是可以从头开始,而不会造成不可修复的损害。

单板计算机、嵌入式系统和(Debian)Linux

在过去几年中,针对嵌入式和移动设备的 Linux 项目和发行版经历了大爆炸。例如,Tessel 运行的是最初为路由器开发的精简版 Linux:OpenWrt。

当我们深入研究高性能 SBC 时,你会看到需要做出决定——大多数平台都愿意运行多个不同的 Linux 发行版。

使用 Raspberry Pi,我们将坚持默认和最常见的选择:基于 Debian 的 Raspbian Linux。为了保持一致性,本章末尾的 BeagleBone Black 探索将使用 Debian Linux 重新刷新板子,而不是使用它随附的默认(Ångström)。

Debian 版本以《玩具总动员》的角色命名。在撰写本文时,Debian 稳定版是第 9 版(Stretch),但大多数嵌入式和单板计算机平台仍在使用第 8 版(Jessie)的构建。有时可以看到第 7 版(Wheezy)的构建,尽管它们变得越来越不常见。

11.1.1. Raspberry Pi 平台

Raspberry Pi (图 11.2) 到处都是。Arduino 平台对于简单的开发板来说是什么,Pi 平台对于单板计算机来说就是什么:无处不在。

图 11.2. Raspberry Pi 2 Model B(左侧)、Raspberry Pi Zero(中心前景)和 Raspberry Pi 3 Model B(在盒子里,附带 SparkFun Pi Wedge)

图片

随着每个按顺序编号的 Pi 代(到目前为止有 1、2 和 3 代),该平台变得更加强大、高效、稳定,并且功能丰富。例外的是 Pi Zero 系列,它甚至更小、更便宜——但以牺牲一些性能和功能为代价。

尽管 Raspberry Pi 平台在功能和灵活性方面很强大,但并不总是电子初学者的理想选择。你可以用 Pi 做的众多事情可能会分散注意力,处理 Linux 管理和其他配置细节的任务也是如此。很容易陷入谷歌搜索的歧途,在论坛、项目想法和选项中浪费数小时。

此外,Pi 引脚排列相当复杂,有多个混淆的编号和命名系统。尽管引脚数量众多,但一些 GPIO 支持完全缺失。例如,没有板载的模数转换器(ADC)。其他关键的 GPIO 功能也有限。在 Raspberry Pi 3 Model B 的 40 个 GPIO 引脚中(图 11.3),只有两个是 PWM 功能的。

图 11.3. Raspberry Pi 3 Model B

另一方面,您已经对基础知识有了足够的了解,一些潜在的陷阱可能不再那么令人生畏。Raspbian——Pi 的默认基于 Debian 的操作系统——被广泛使用,合理且可靠。由于有很多人使用 Pi,因此有许多资源旨在帮助即使是初学者也能得到帮助,而且有大量的论坛、维基、Stack Overflow 和博客文章等,以帮助您了解每一个细节。

整理您的套件

以下部分提供了两种不同的设置基于 Raspberry Pi 3 Model B 的系统选项:

  • 传统配置——配置 Raspberry Pi 的传统且更适合初学者的方式是将它视为台式计算机——插入 USB 鼠标、USB 键盘和 HDMI 显示器,然后直接工作。

  • 无头配置——如果您不想为 Pi 配备那么多外围设备,您可以选择将其视为嵌入式系统。如果这个想法吸引您,那么无头配置部分就是为您准备的!

对于初次设置,来自所有主要在线电子零售商的 Pi 入门套件——虽然成本更高,但是一个不错的选择(与单独购买 Pi 板及其支持组件相比)。以下是在套件中寻找的一些事项:

  • 预装 NOOBs 或 Raspbian 的 microSD 卡——最好还带有 SD 卡适配器,这样您就可以使用标准尺寸的卡读卡器/写卡器来更新卡的内容

  • 外壳(机箱)——这为您的 Pi 提供了物理稳定性、保护,在某些情况下,还提供了优雅的外观。

  • 电源供应——Pi 的电源连接是 USB 微型,因此您可以使用 5 V USB 设备充电器作为替代。然而,请注意,并非所有手机充电器都能满足 Pi 的电流需求:Pi 网站推荐一个可以提供 2.5 A 或更多电流的适配器。

  • 提供更方便访问 Pi 的 GPIO 引脚的硬件——这些硬件以 Adafruit Cobbler、SparkFun Wedge 和其他类似选项的形式出现(图 11.4)。

图 11.4. 构建的 Raspberry Pi 3 Model B SparkFun 套件,带有 Pi Wedge,此处显示连接到面包板。同时显示的是 Adafruit T-Cobbler(右侧),它提供了与 Wedge 相同的功能。

11fig04_alt

套件可能提供其他有用的配件——例如 SparkFun 套件包括 USB microSD 卡读卡器——但它们有一个缺点,就是经常包括你可能已经拥有的东西:面包板、跳线、LED 等。

一块单独的 Raspberry Pi 3 Model B 主板大约是 40 美元,而完整的套件大约是 90 美元。Adafruit 和 SparkFun 目前提供的 Pi 3 入门套件包含本节所需的所有部件。

在 Pi 上制作 GPIO 连接

Pi 的引脚是公的——如果你直接连接到它们,你需要公对母的跳线,你可能会得到一团糟和挫败感,因为 Pi 本身没有丝印信息。

有第三方硬件组件旨在使 Pi 的 GPIO 更容易使用。SparkFun 的 Pi Wedge 和 Adafruit 的 T-Cobbler 是两个例子:这些分线将引脚组织成更直观的分组(带有丝印提示)并提供面包板兼容的形态。你的 Pi 套件可能包含它们,或者可以单独购买。

0339fig01_alt

组装好的 Adafruit T-Cobbler 和 SparkFun Pi Wedge(带有连接的 40 引脚电缆)

FTDI(未来技术设备国际)

FTDI 制造芯片,允许嵌入式设备和计算机之间进行异步串行通信。FTDI 芯片将来自设备的 TTL 或 RS-232 信号(RS-232 是另一种异步串行协议)转换为计算机可以理解的 USB 信号,反之亦然。FTDI 连接可以用来监控设备的串行输出。在某些情况下,它们也可以用来编程或控制设备。

SparkFun 的 Pi Wedge 包括一个 FTDI 接口。你还需要一根线或一个分线板来连接到 FTDI 引脚(一个带有迷你 USB 连接器的 SparkFun FTDI 分线板连接到 Pi Wedge,如图 11.4 所示)。

你需要什么

note

接下来是关于传统桌面式 Pi 设置和无头设置的步骤。你只需要选择其中一种选项,但无论你选择哪条路径,你都需要以下内容:

  • 1 块 Raspberry Pi 3 Model B

  • 1 个 5V USB 微型电源

  • 1 个外壳(可选但推荐)

  • 1 个 Adafruit T-Cobbler、SparkFun Pi Wedge 或类似产品;或者一套公对母跳线

11.1.2. 配置选项 1:传统方式

你需要什么

note

除了上一节中列出的部件外,你还需要以下内容:

  • 1 张预装了 NOOBS 或 Raspbian OS(带桌面)的 microSD 卡

  • 1 个 USB 键盘

  • 1 个 USB 鼠标

  • 1 个显示器

  • 1 条 HDMI 电缆用于显示器

此配置选项涉及连接外围设备和电源,并遵循屏幕上的说明。插入预先闪存的 NOOBS microSD 卡并通电。NOOBS 给你安装几个操作系统的选择。请选择第一个选项——Raspbian。安装过程需要几分钟。

一旦安装并配置了操作系统,你将能够启动到 PIXEL(Pi 改进的 Xwindows 环境,轻量级)环境。设置 WiFi 很简单:首先点击右上角菜单栏中的 WiFi 图标(图 11.5)。

图 11.5. PIXEL 桌面。注意右上角的 WiFi 图标。

如果你希望在以后能够登录到你的 Pi,而无需直接在 Pi 上工作,你需要启用 SSH。为此,请使用“首选项”>“Raspberry Pi 配置”菜单选项。导航到“接口”选项卡,并点击 SSH 旁边的“启用”选项。点击“确定”以应用更改。

目前就到这里!如果你遇到任何困难,请前往 Raspberry Pi 软件指南(mng.bz/P8Hu),它有插图且用户友好。

11.1.3. 配置选项 2:无头

这种更精简、直接到核心的方法消除了对外围设备和电缆的需求。另一方面,这是一种不太常见的做法,它需要在终端中花费更多时间。Raspbian 是不断发展的,但以下步骤在 2017 年中期能够可靠地工作。

如果你已经使用桌面方法设置了 Pi,你可以跳过这一组步骤。你可以跳到 11.8 节。

你需要什么

除了 11.1 节末尾列出的部件外,你还需要以下部件:

  • 1 个 microSD 卡

  • 1 个 SD 卡适配器

  • 1 个 SD 卡读写器(或具有内置读卡器的计算机)

  • 1 条以太网电缆

由于你将无头运行,你需要配置 Pi,以便以某种方式与之通信,因为你将缺少键盘和显示器。

首先,你需要创建一个可启动的 SD 卡,这样你就可以通过有线以太网连接到 Pi 进行 ssh。然后你将登录并使用命令行配置 WiFi。

创建可启动的 Raspbian 磁盘镜像

你需要在 microSD 卡上安装操作系统,这样 Pi 才能启动:

  1. 从 Raspbian 下载页面(www.raspberrypi.org/downloads/raspbian/)下载 Raspbian(不是 NOOBS)(图 11.6)。

    图 11.6. 从 Raspberry Pi 的 Raspbian 下载页面下载 Raspbian 的完整版本。

    大型下载完成后,解压生成的 zip 文件。如果一切顺利,你应该会得到一个 IMG 文件(一个可启动的磁盘镜像)。

    大型的 Raspbian zip 文件

    如 Raspbian 下载页面所示(图 11.6),生成的 zip 文件很大,您默认的解压工具可能无法完成解压任务(我的就不行)。如页面建议,如果遇到问题,请尝试 Mac 上的 The Unarchiver 或 Windows 上的 7Zip。

  2. 创建可引导的 microSD 卡:

    1. 安装免费的应用程序 Etcher(etcher.io/),适用于 Windows、Mac 或 Linux。这将允许您将 Raspbian IMG 文件烧录到 microSD 卡上。

    2. 将 microSD 卡插入 SD 卡适配器,然后将整个装置插入您的计算机或 SD 卡读卡器/写入器。

    3. 启动 Etcher 并按照步骤将 IMG 文件放置到 microSD 卡上(图 11.7)。

    图 11.7. Etcher 是一个简单的实用程序:选择磁盘镜像文件,选择驱动器(它通常自动为您选择),然后开始。

    图片 11fig07_alt

  3. 向 microSD 卡添加文件以启用 SSH:

    1. 当 Etcher 完成操作——创建磁盘镜像需要几分钟时间——microSD 卡将从您的计算机中软弹出(卸载)。拔掉它,重新插上,然后打开引导分区(这可能是你能看到的唯一分区)。

    2. 创建一个名为“ssh”的空文件——没有扩展名——并将其放置在引导分区的顶层。这将启用 Pi 上的 SSH,否则在 Raspbian 中默认是禁用的。

  4. 安装操作系统:

    1. 从您的计算机中弹出 SD 卡适配器,从适配器中取出 microSD 卡,并将其插入 Pi 3。

    2. 引导 Pi。

  5. 通过以太网与您的 Pi 建立通信:

    1. 使用以太网线将 Pi 3 的以太网接口直接连接到您的无线路由器。

    2. 查找您的 Pi 的 IP 地址。您的 Pi 3 应该会自动分配一个 IP 地址(通过 DHCP),但您需要找出这个 IP 地址是什么。有很多种方法可以做到这一点。在 Google 上搜索“IP 扫描器”或“LAN 扫描器”,您将找到各种平台上的大量免费工具,或者您可以使用命令行工具。我使用 Mac 上的 LanScan(图 11.8)。目的是确定分配给您的 Pi 的 IP 地址。

    图 11.8. 使用 LanScan for Mac,我可以看到 Raspberry Pi 的以太网接口被分配了 IP 地址 192.168.1.13。

    图片 11fig08_alt

  6. 一旦您获得了 Pi 的 IP 地址,打开终端并输入以下命令:

    $ ssh pi@<your Pi's IP>
    
默认 Pi 用户名和密码

Raspberry Pi 的默认用户名是pi,密码是raspberry。更改pi用户的密码是个好主意。您可以通过在命令提示符下输入passwd并遵循屏幕上的说明来完成此操作。现在就做。

在 Pi 上配置 WiFi

最后一步是为你的树莓派配置 WiFi,这样你就不必使用有线以太网连接。这涉及到对树莓派的wpa_supplicant设置进行操作。有时如果你直接编辑配置文件,可能会很棘手和令人沮丧。我发现最安全的方法是使用 wpa_cli 命令行工具:

树莓派 3 的 WiFi 支持

树莓派 3,就像 Tessel 2 一样,不支持 5 GHz WiFi 网络。

  1. 如果你还没有,请通过 SSH 连接到你的树莓派。

  2. 通过输入以下命令启动一个交互式的 wpa_cli 会话:

    $ sudo wpa_cli
    

    这将使你进入交互模式。你可以在>提示符下输入后续命令。

  3. 扫描可用的无线网络,以确保你的树莓派可以看到所需的 WiFi 网络。要这样做,输入以下内容:

    > scan
    

    然后输入以下内容,以查看扫描结果:

    > scan_results
    

    你的网络显示了吗?如果没有,检查你的路由器设置,确保它是一个兼容的 WiFi 网络(不是 5 GHz)。如果是,太好了!继续。

  4. 执行以下每个命令以在树莓派的wpa_supplicant配置中添加、配置和启用所需的 WiFi 网络连接:

    > add_network 0
    > set_network 0 ssid "<your network's SSID>"
    > set_network 0 pwk "<your network's password>"
    > enable network 0
    > save_config
    
  5. 按 Ctrl-C 退出 wpa_cli。

  6. 为了验证它是否成功,输入以下内容:

    $ ifconfig wlan0
    

    如果一切正常,你应该会看到一个分配的 IP 地址(图 11.9)。

图 11.9. 配置 WiFi 后,树莓派现在有两个 IP 地址——一个用于以太网接口,一个用于其 WLAN(WiFi)接口。

11fig09_alt.jpg

如果你喜欢,可以拔掉以太网连接。树莓派现在将自动连接到在此配置的 WiFi 网络,每次启动时都会连接。方便!

如果你需要重新开始

如果你的树莓派运行不正常,或者你感到困惑或卡壳,重新开始并不太难。关闭树莓派电源,弹出 microSD 卡,然后用你想要的操作系统重新刷机:NOOBS 提供用户友好的可视化设置;Raspbian;或者任何其他你选择的兼容操作系统。从更新后的 SD 卡启动将给你一个全新的开始。

11.2. 学习关于树莓派 3

现在你已经配置了具有 WiFi 连接的树莓派,让我们首先应用第十章中概述的平台学习步骤,以更好地了解树莓派 3 平台的整体情况。

11.2.1. 核心功能

单板计算机(SBCs)在处理能力上大大提升,与它们额外的功能相匹配。ARM 32 位 Cortex M 系列的微控制器是许多可以运行嵌入式 JavaScript 的平台的核心。使用完整的单板计算机,你将进一步提高处理器的性能。这些 32 位和 64 位处理器通常具有多个核心、3D 图形加速、更高的时钟速度和复杂的子系统。

树莓派 3 配备了四核 64 位 ARM(A8)CPU,运行频率为 1.2 GHz。这与 Arduino 的 20 MHz 8 位 ATmega 相比相去甚远(尽管这种比较可能不太恰当)。产品信息页上还列出了其他规格,包括 1 GB 的 RAM、板载 WiFi 和蓝牙、4 个 USB 端口、视频和立体声音频输出、HDMI 接口等(图 11.10)。更多信息请访问树莓派 3 型号 B 产品页面

图 11.10. 树莓派网站上的树莓派 3 型号 B 规格亮点

在 GPIO 方面,情况比较复杂。有多个 SPI 和 I²C 接口,引脚总数也很多。如前所述,没有 ADC 支持,PWM 也有限制。附加配件和某些外围设备可能需要连接到一些引脚上,这意味着根据你的配置,你可能无法访问所有 40 个引脚的 I/O。

由于树莓派 3 面向通用市场,其产品页面上的核心功能列表没有提到其 GPIO 逻辑电平电压为 3.3 V,但你应该知道:树莓派 3 是一个 3.3 V 的设备。

严肃地说,树莓派是一个 3.3 V 的设备

不要将 5 V 电源连接到树莓派的任何引脚上,否则你可能会发现你的树莓派已经无法使用了。

11.2.2. GPIO 功能与引脚配置

树莓派的引脚配置很复杂。首先,引脚数量很多(40 个),有许多不同的引脚编号和命名方案,而且某个引脚可能已经被某个组件、过程或外围设备占用。例如,SPI 或 I²C 引脚这样的相关连接组并不一定在物理上是相邻的。pinout.xyz 网站是了解树莓派引脚细节的好资源。

以单个树莓派 GPIO 引脚的多种面貌为例,物理引脚 33(图 11.11)也被称为 BCM 13(博通引脚编号),其主要功能名称为 PWM1(它是具有 PWM 功能的引脚之一),以及 WiringPi 引脚 23。它还有几个功能性的别名,如AVEOUT VID 9。而且,它甚至可能用于 GPIO?如果你将并行外部显示器连接到树莓派上,它就是那些引脚之一。如果你想要使用 JTAG 调试接口或 SMI(二级存储接口)设备,它也可能是那些连接中使用的引脚之一。

图 11.11. pinout.xyz 是一个专门提供树莓派引脚信息网站。这个来自 pinout.xyz 的细节显示了物理引脚 33 的多个别名。

SBC 和平台特定硬件术语

看起来每个 SBC 平台都在物联网词汇中添加了更多的术语。就像 Arduino 兼容的扩展板被称为盾牌一样,适合树莓派的板通常被称为帽子,而与 BeagleBone Black 兼容的板则被称为披风

11.2.3. 配置和工作流程

在 Pi 平台上,你可以做的和如何做几乎是没有边界的——毕竟,它是一个完整的计算机。让我们集中精力,关注以 Node.js 为中心的电子黑客配置的配置和工作流程选项。

到这一步,你应该有一个正在工作的 Pi——无论是作为独立计算机还是无头设备。为了使配置和工作流程更加稳定,你还需要完成以下三个步骤:

  1. 确保 Pi 的软件是最新的。

  2. 在 Pi 上安装一个可接受的 Node.js 版本。

  3. 找出一种方法来编写代码并将其上传到 Pi 的文件系统。

更新 Pi 的软件

确保 Pi 拥有最新的软件更新:

  1. 通过 ssh 连接到 Pi(无头)或使用终端(桌面)。确保 Pi 已连接到互联网。

  2. 运行以下命令:

    $ sudo apt update
    

    这可能需要几分钟才能完成。

  3. 运行以下命令:

    $ sudo apt full-upgrade
    

    这可能需要几分钟才能完成其任务。

升级 Pi 上的 Node.js

仍然登录到 Pi 或终端窗口中,尝试运行以下命令:

$ node --version

这将输出 Pi 上当前的 Node.js 版本。在撰写本文时,预安装的 Node.js 版本是 v0.10.29,这已经过时了。将其升级到 LTS(长期支持),这样我们就不需要在项目中遇到兼容性或安全问题:

  1. 如果你还没有运行完整的系统更新(如前文所示),首先运行以下命令:

    $ sudo apt update
    
  2. 通过执行以下命令下载并运行目标版本的 Node.js 设置脚本:

    $ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
    

    在撰写本文时,LTS 版本是 v6.x。在你阅读本文时,这可能已经发生了变化,因此你可以将 URL 中的 setup_6.x 部分替换为适当的重大版本号。更多信息请参阅 NodeSource 二进制分发仓库 (github.com/nodesource/distributions)。

  3. 安装 Node.js:

    $ sudo apt install -y nodejs
    
  4. 通过运行以下命令验证是否成功:

    $ node --version
    
管理 Pi 上的文件

你的 Pi 项目中的 JavaScript 文件和资源将在 Pi 上执行,并需要在 Pi 的文件系统中存在。不出所料,有无数种方法可以将你的文件上传到 Pi。以下是一些选项:

  • 在 Pi 本身上编写文件。你可以使用基于终端的编辑器,如 vinano。或者,如果你为 Pi 配置了桌面环境,你可以使用预安装的 Leafpad 应用程序等 GUI 编辑器。或者,你可以安装任何数量的附加文本编辑应用程序。

  • 使用实用程序将文件从你的计算机复制到 Pi。你可以使用 Unix 命令行工具 scp(安全复制)来移动文件,或者使用支持它的 GUI 应用程序,例如。

  • 在 Pi 上设置一个文件服务器,以便你可以从网络上的其他计算机作为远程共享访问它。实现这一目标的一种可能方法就是使用 Samba (SMB) 服务器。

在 Pi 上配置 Samba (SMB) 服务器

让你的 Pi 文件易于访问的一种方法是在 Pi 上设置一个文件服务器。以下步骤设置了一个名为projects的 Samba(SMB)共享,该共享可以被pi用户读写访问。一旦配置完成,这个共享应该会出现在你的系统 Finder 或文件资源管理器中,作为一个网络驱动器。请注意,这些说明假设你对 Linux 命令行有一定的了解:

  1. pi用户(无头)的身份使用ssh连接到 Pi,或使用终端应用程序(桌面)。

  2. 确保你处于pi用户的家目录,通过输入以下命令:

    $pwd
    

    你应该看到以下输出:

    /home/pi
    
  3. 创建一个目录来保存项目文件:

    $ mkdir projects
    
  4. 安装 Samba:

    $ sudo apt install samba
    
  5. 通过输入以下命令并按照提示操作,为pi用户设置 Samba 密码:

    $ sudo smbpasswd -a pi
    
  6. Samba 不使用系统密码;它维护自己的密码。

  7. 编辑 Samba 配置文件。首先备份到你的家目录,以防万一:

    $ sudo cp /etc/samba/smb.conf ~/
    
  8. 现在编辑配置:

    $ sudo vi /etc/samba/smb.conf
    
  9. (如果你更喜欢nano而不是vi,可以使用不同的编辑器。)滚动到文件的底部并添加以下行,包括=字符周围的空格:

    [projects]
        path = /home/pi/projects
        valid users = pi
        read only = No
    
  10. 保存文件并退出。

  11. 重新启动 Samba 服务:

    $ sudo service smbd restart
    
  12. 如果喜欢,检查你的配置:

    $ testparm
    

你现在应该能够从同一网络上的其他计算机连接到 SMB 共享(在 Windows 文件资源管理器中使用映射网络驱动器或在 Mac 上使用连接到服务器或 Cmd-K)。连接字符串采用以下形式:

smb://user@host/sharename

你最终应该得到类似以下内容:

smb://pi@<your Pi's IP>/projects

Samba 共享有时可能不完美,你可能需要调整权限或用户元数据才能使其正常工作。如果你需要支持,网上有大量的用户论坛和帮助文章。

创建一个“项目”区域

在配置 Samba 服务器时,在侧边栏中创建名为“projects”的目录是步骤之一。即使你不在 Pi 上设置共享,也请创建一个目录来整理你即将进行的代码实验。本章的其余部分将假设存在一个~/projects目录(家目录内的“projects”目录)。

11.2.4. 示例和教程

在 Raspberry Pi 上闪烁 LED 没有一种唯一的方法——选项几乎数不胜数。在这里,你将尝试几种方法,强调 JavaScript 选项。

你需要的东西

note.jpg

  • 1 个配置好的 Raspberry Pi 3

  • 1 个 SparkFun Pi Wedge,Adafruit T-Cobbler 或类似产品;或公对母跳线

  • 1 个标准 LED,任何颜色

  • 1 个 100V 电阻

  • 1 个面包板

  • 跳线

正确使用sudo的方式

由于 Raspbian 中与 GPIO 交互的权限较为保守,你可能需要使用sudo执行以下代码示例。例如,

$ sudo ./blink.sh

或者

$ sudo node index.js

如果没有sudo,你可能得到如下权限错误:

./blink.sh: line 4: /sys/class/gpio/gpio4/direction: Permission denied
./blink.sh: line 5: /sys/class/gpio/gpio4/value: Permission denied
./blink.sh: line 7: echo: write error: Operation not permitted

尝试只使用 sudo 来执行需要它的命令,例如执行这些脚本时。不要使用 sudo 来安装 npm 模块或创建文件等。如果你这样做,可能会创建具有奇怪权限的东西。如果有疑问,先尝试在不使用 sudo 的情况下执行一些操作,看看是否可行。

构建 LED 电路

以下每个示例都使用相同的物理电路配置:LED 的阳极应连接到 Pi 的物理引脚 7(WiringPi 引脚 7,BCM 引脚 4)。阴极应连接到 GND 引脚。

接线图显示了直接连接到 Pi 的连接(图 11.12)、Adafruit 的 T-Cobbler(图 11.13)和 SparkFun 的 Pi Wedge(图 11.14)。

图 11.12. 使用公对母跳线直接连接 LED 到 Pi 引脚的接线图

图 11.13. 使用 Adafruit 的 T-Cobbler 的接线图

图 11.14. 使用 SparkFun 的 Pi Wedge 的接线图

使用 sysfs 闪烁 LED

Sysfs 是一个 Linux 伪文件系统,它将连接的设备和系统的配置组织成文件系统层次结构。sysfs 导出到用户空间中的每个实体都由一个目录表示。Sysfs 挂载在 /sys 上,并且从 /sys/class/gpio 中可以控制 Pi 的 GPIO 引脚。

对于你想要使用的每个引脚,你需要执行以下操作:

  1. 导出引脚。这是通过将引脚号写入 /sys/class/gpio/export 文件并使用 BCM 编号方案来完成的。这将创建一个用于引脚的目录(<pin_directory>)。

  2. 配置引脚,例如通过将值 'in''out' 写入 /sys/class/gpio/<pin_directory>/direction。

  3. 与引脚交互,例如通过从 /sys/class/gpio/<pin_directory>/value 文件中读取或向其写入值。

  4. 清理。通过将引脚号写入 /sys/class/gpio/unexport 文件来取消导出引脚。

通过示例可能更容易理解。闪烁连接到物理引脚 7、BCM GPIO 4 的 LED 一次——打开然后再次关闭——可以通过使用 shell 脚本来实现,如下面的列表所示。

要尝试 sysfs,在你的 ~/projects 目录内创建一个“blink-sysfs”目录。

列表 11.1. 一个用于闪烁连接的 LED 一次的 bash 脚本
#! /bin/bash
# Export the pin so we can work with it
echo "4" > /sys/class/gpio/export
# Set the pin up as an output pin
echo "out" > /sys/class/gpio/gpio4/direction
# Turn on the LED by setting the pin to HIGH ("1")
echo "1" > /sys/class/gpio/gpio4/value
# Do nothing for one second
sleep 1
# Turn off the pin by setting it to LOW ("0")
echo "0" > /sys/class/gpio/gpio4/value
sleep 1
# Unexport the pin to clean up after ourselves
echo "4" > /sys/class/gpio/unexport

要运行 blink.sh 脚本,你需要使文件可执行。你可以在 ~/projects/blink-sysfs 目录内运行以下命令来完成:

$ chmod +x ./blink.sh

现在通过输入以下内容来尝试:

$ sudo ./blink.sh
使用 sysfs 和 Node.js 闪烁 LED

最后,这些只是文件系统操作,因此你也可以使用 Node.js 的内置 fs 模块来完成,如下一个列表所示。

列表 11.2. 使用 sysfs 和 Node.js 闪烁 LED
const fs = require('fs');
const sysfsPath = '/sys/class/gpio';
const ledPin = '4';                              *1*
const blinkTotal = 10;

var blinkCount = 0;
var ledStatus = false;

// Export and configure the pin as an output
fs.writeFileSync(`${sysfsPath}/export`, ledPin);
fs.writeFileSync(`${sysfsPath}/gpio${ledPin}/direction`, 'out');

var blinker = setInterval(() => {
  if (ledStatus) {
    // The LED is on. Turn it off.
    fs.writeFileSync(`${sysfsPath}/gpio${ledPin}/value`, '0');
    blinkCount++; // This completes one blink cycle
    if (blinkCount >= blinkTotal) {
      console.log('All done blinking');
      // Clean up after ourselves
      fs.writeFileSync(`${sysfsPath}/unexport`, '4');
      clearInterval(blinker);
    }
  } else {
    // The LED is off. Turn it on.
    fs.writeFileSync(`${sysfsPath}/gpio${ledPin}/value`, '1');
  }
  ledStatus = !ledStatus; // The LED has swapped status
}, 1000);
  • 1 注意,在这个例子中,引脚号和所有写入的值都是字符串。

通过以下命令尝试:

$ sudo node index.js

别忘了取消导出

当你完成时,真的需要在 gpio 目录中 unexport 对象,否则下次你想使用该引脚时可能会遇到问题。当你尝试运行 Node.js 闪烁脚本时,如果看到类似这样的错误,很可能是因为引脚没有被正确地取消导出:

Error: EBUSY: resource
busy or locked, write

你可以在终端中执行以下命令来手动清理:

$ echo "4" > /sys/class/gpio/unexport

你可以用一本百科全书来填满所有你可以用 sysfs 做的事情,但让我们继续前进。了解 sysfs 是好的,但直接与之工作需要耐心,并且涉及一个学习曲线。

WiringPi

WiringPi 是一个抽象包装器,试图使引脚编号更合理,并且它暴露了一个更熟悉的 Arduino 风格的 API。它是用 C 编写的,但 Ruby 和 Python 库很受欢迎。有一个 npm 包提供了 Node.js 绑定:wiring-pi

如果你想尝试一下,在你的 ~/projects 区域创建一个名为 blink-wiring-pi 的目录。在该目录内,运行以下命令:

$ npm install wiring-pi

然后创建一个名为 index.js 的文件,其内容如下所示。在这个脚本中,status 值在 01 之间切换,分别用于关闭和打开 LED。

列表 11.3. index.js
const wpi = require('wiring-pi');
const ledPin = 7;                             *1*
const blinkTotal = 10;
var blinkCount = 0;
var status = 1;                               *2*

wpi.setup('wpi');
wpi.pinMode(ledPin, wpi.OUTPUT);              *3*

var blinker = setInterval(() => {
  wpi.digitalWrite(ledPin, status);           *4*
  if (!status) {
    blinkCount++;
    if (blinkCount >= blinkTotal) {
      console.log('All done blinking!');
      clearInterval(blinker);
    }
  }
  status = +!status;                          *5*
}, 1000);
  • 1 与之前相同的物理引脚,但使用 WiringPi 编号,实际上它们是 JavaScript 数字(不是字符串)

  • 2 注意状态是一个数字(0 或 1),而不是之前的布尔值。

  • 3 pinMode() 和 OUTPUT 常量与 Arduino 语言 API 相呼应。

  • 4 这里的状态值要么是 1(高)要么是 0(低)。

  • 5 取反状态值的布尔等效值并将其再次转换为数字(+ 操作符)

要运行脚本,使用以下命令:

$ sudo node index.js`
Johnny-Five 与 raspi-io I/O 插件

为了完成闪烁的盛宴,我们将回到我们的老朋友 Johnny-Five。就像 tessel-io Johnny-Five I/O 插件使得使用 Johnny-Five 与 Tessel 成为可能一样,npm 包 raspi-io 允许你在 Raspberry Pi 上使用 J5。

在 ~/projects 目录中创建一个名为 blink-j5 的目录。

在 blink-j5 目录内,运行以下命令来安装 Johnny-Five 和 raspi-io 包:

$ npm install johnny-five raspi-io

对于你的下一个技巧,你将使用你尝试过的第一个 LED 脚本之一——从很久以前的 第二章——并将其修改为在 Pi 上工作,如下所示。所需更改仅包括和使用 raspi-io 插件来提供 I/O,以及更改 LED 的引脚编号。就是这样!

列表 11.4. 使用 Johnny-Five 闪烁 LED
const five = require('johnny-five');
const Raspi = require('raspi-io');                 *1*

const board = new five.Board({io: new Raspi()      *2*
});

board.on('ready', () => {
  const led = new five.Led(7);                     *3*
  var blinkCount = 0;
  const blinkMax = 10;

  led.blink(500, () => {
    blinkCount++;
    console.log(`I have changed state ${blinkCount} times`);
    if (blinkCount >= blinkMax) {
      console.log('I shall stop blinking now');
      led.stop();
    }
  });
});
  • 1 需要 raspi-io I/O 插件模块

  • 2 使用 Raspi 对象进行 io

  • 3 使用 WiringPi 编号方案

Raspi-io 支持多种 Pi 引脚编号方案。作为 JavaScript Number 值传递的引脚自动被认为是 WiringPi 编号。但你也可以使用物理引脚编号和功能名称(如 GPIO4)。有关更多详细信息,请参阅插件的文档(github.com/nebrius/raspi-io)。

11.2.5. API 文档

由于有无数种方式可以使用 Pi 控制硬件,因此没有单一的 API 文档来源。相反,我们将使用 Johnny-Five、Node.js 和 raspi-io I/O 插件,因此在我们探索时,您需要将那些 API 的文档网站放在手边。

11.3. 为不同平台编写 Johnny-Five 应用程序

如 列表 11.4 所示,将 Johnny-Five 应用程序调整到不同的平台(例如从 Arduino Uno 迁移到 Raspberry Pi)可以非常简单。通常这只是一个选择正确的 I/O 插件和在代码中更新一些引脚编号的问题。

在本节接下来的几个实验中,您将调整第八章中为 Tessel 创建的基于 BMP180 的实时更新天气应用(图 11.15)。遵循您对单板计算机和 Raspberry Pi 3 的具体探索,您将首先在 Pi 3 上实现气象站,然后使其在 Arduino Uno 上工作。

图 11.15. 小型天气应用提供实时更新的温度和压力数据,可以在同一网络上的任何计算机上的浏览器中查看。

图片

您可以在 GitHub 上的本书源代码库中找到天气站应用的 Tessel 版本的源代码。

11.3.1. 为 Pi 3 调整小型气象站

您需要

图片

  • 1 个配置好的 Raspberry Pi 3

  • 1 个 SparkFun Pi Wedge、Adafruit T-Cobbler 或类似的;或公对母跳线

  • 1 个 BMP180 多传感器开发板

  • 1 个面包板

  • 跳线

作为复习,天气应用有两个主要组件:

  • 服务器代码——这包括用于处理 I/O 和读取传感器数据的 Johnny-Five 代码,一个静态 web 服务器(使用 express),以及一个 socket.IO 服务器,它发出表示天气数据更新的事件(socket.IO 客户端可以监听这些事件)。

  • 客户端代码——这是以单个 HTML 页面——index.html 的形式存在的,将由 express 静态 web 服务器提供。一旦在浏览器中加载,index.html 就会作为客户端连接到 socket.IO 服务器,以便接收和显示天气数据,而无需用户刷新页面。

这种模式——I/O 处理和基于浏览器的前端结合——可以重用来构建许多不同类型的物联网应用。这是一个值得保留在您口袋里的有用模式。

构建电路

首先,您需要通过将 BMP180 开发板连接到您的 Pi 来构建电路。与闪烁的 LED 一样,具体操作将取决于您的设置。提供了直接连接到 Pi 的布线图(图 11.16)、SparkFun Pi Wedge (图 11.17)和 Adafruit 的 T-Cobbler (图 11.18)。

图 11.16. BMP180 的布线图,显示直接连接到 Pi

图片

图 11.17. BMP180 和 SparkFun Pi Wedge 的布线图

图 11.18. BMP180 和 Adafruit T-Cobbler 的接线图

Raspi-io 和 I²C

BMP180 是 I²C。Raspi-io 支持 I²C,但要启用它,您需要在安装 raspi-io 后重启(如果您还没有重启的话)。(提示:sudo reboot 是一个方便的命令。)

测试 BMP180

在适配 Tessel 的天气站软件之前,您将编写一个基本的脚本来将 BMP180 温度和压力数据记录到控制台。这将确认 Raspbian、raspi-io、I²C 和 BMP180 正在和谐地一起工作:

  1. 建立工作区域。在您的 Pi 项目目录中创建一个名为“weather”的目录:

    $ mkdir weather
    
  2. 在天气目录中,运行以下命令:

    $ npm install johnny-five raspi-io
    
  3. 创建一个 index.js 文件,并用下一列表中的代码填充。

列表 11.5. 测试 Pi、Johnny-Five、raspi-io 和 BMP180
const five = require('johnny-five');
const Raspi = require('raspi-io');

const board = new five.Board({
  io: new Raspi()                     *1*
});

board.on('ready', () => {
  const bmp180 = new five.Multi({
    controller: 'BMP180'
  });
  bmp180.on('change', () => {
    var temperature = bmp180.thermometer.fahrenheit.toFixed(2);
    var pressure    = bmp180.barometer.pressure.toFixed(2);
    console.log(`${temperature}°F | ${pressure}kPa`);
  });
});
  • 1 再次使用 raspi-io 插件进行 I/O

在前面的列表中,请注意,在 Multi 传感器组件(分配给 bmp180 变量)的实例化中,您不必指定 BMP180 连接到的引脚。Raspi-io “知道” Raspberry Pi 上具有 I²C 功能的引脚在哪里,并为您配置接口!这很方便。

尝试一下。仍然在天气目录中,运行以下命令:

$ sudo node index.js

您应该看到温度和压力数据记录到控制台,如下所示。

列表 11.6. 测试 BMP180 脚本的示例输出
>> 77.54°F | 98.05kPa
77.54°F | 98.05kPa
77.54°F | 98.05kPa
77.54°F | 98.05kPa
77.54°F | 98.06kPa
77.54°F | 98.05kPa
77.72°F | 98.06kPa
77.72°F | 98.06kPa
77.54°F | 98.05kPa
进行 Pi 特定更改

现在您将对 Pi 进行一些特定更改。创建一个新的工作区域,一个名为“pi-weather”的目录。将书中 GitHub 仓库中的原始天气应用程序源文件复制到该目录中,但省略 .tesselinclude。您最终应该得到以下结构。

列表 11.7. 项目目录和文件结构
pi-weather/
 app
 index.html
 style.css
 index.js
 package.json
不要复制 node_modules 目录

如果您不小心在 pi-weather 目录中(从之前与 Tessel 的实验中复制)得到了一个 node_modules 目录,请在继续之前将其删除:

$ rm -rf node_modules

代码中有两个地方需要更改:

  • package.json—您需要更新依赖项。

  • index.js—您需要使用 raspi-io 进行 I/O。

更新 package.json 依赖项

首先编辑 package.json。它应该包含部分 dependencies 对象,其外观应类似于以下列表,尽管您的版本号可能不同。删除 tessel-io 依赖项,因为您不需要它用于 Raspberry Pi 版本。

列表 11.8. Tessel 项目的 package.json 依赖项
"dependencies": {
  "express": "⁴.14.1",
  "johnny-five": "⁰.10.4",
  "socket.io": "¹.7.3",         *1*
  "tessel-io": "⁰.9.0"          *2*
}
  • 1 如果 tessel-io 条目是最后一个,别忘了删除尾随的逗号。

  • 2 删除此行。

现在安装剩余的依赖项:

$ npm install

然后使用 --save 标志添加一个新的依赖项—raspi-io—并将更改写入 package.json 中的 dependencies

$ npm install --save raspi-io
更新 index.js

在 index.js 中需要做的更改很简单。

  1. 替换此行,

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

    使用这个:

    const Raspi   = require('raspi-io');
    
  2. board 实例化更新为使用 raspi-io 而不是 tessel-io,更改此行,

    const board = new five.Board({ io: new Tessel() });
    

    看起来像这样:

    const board = new five.Board({ io: new Raspi() });
    

生成的 index.js 内容应如下所示。

列表 11.9. 兼容 Pi 的 index.js 版本
const five     = require('johnny-five');
const Raspi   = require('raspi-io');
const express  = require('express');
const SocketIO = require('socket.io');

const path = require('path');
const http = require('http');
const os   = require('os');

const app    = new express();
const server = new http.Server(app);
const socket = new SocketIO(server);
app.use(express.static(path.join(__dirname, '/app')));

const board = new five.Board({ io: new Raspi() });

board.on('ready', () => {
  const weatherSensor = new five.Multi({
    controller: 'BMP180',
    freq: 5000
  });

  socket.on('connection', client => {
    weatherSensor.on('change', () => {
      client.emit('weather', {
        temperature: weatherSensor.thermometer.F,
        pressure: (weatherSensor.barometer.pressure * 10)
      });
    });
  });

  server.listen(3000, () => {
    console.log(`http://${os.networkInterfaces().wlan0[0].address}:3000`);
  });
});

在 app/index.html 中不需要做任何修改,因为那是客户端代码——它在用户的浏览器中运行,并且不受平台变化的影响。

在天气目录中使用此命令运行应用程序:

$ sudo node index.js

一旦服务器代码初始化,它将输出可以访问 Pi 同一网络上的天气显示的 URL(如下所示列表)。将您的计算机浏览器指向记录的 URL 以查看天气站的实际运行情况。

列表 11.10. 启动天气站应用程序时的示例输出
pi@raspberrypi:~/projects/weather $ sudo node index.js
1499532864338 Available RaspberryPi-IO
1499532864960 Connected RaspberryPi-IO
1499532864984 Repl Initialized
>> http://192.168.1.16:3000

11.3.2. 将迷你天气站适配到 Arduino Uno

使天气应用程序在其他 Johnny-Five 支持的平台上运行同样简单直接。将天气应用程序适配到连接到您自己的计算机的 Arduino Uno 上,而不是 Pi 上,是快速的工作。

你需要

note

  • 1 Arduino Uno

  • 1 面板

  • 1 BMP180 多传感器扩展板

  • 跳线

  1. 将应用程序的 Pi 版本——天气目录及其内容(不包括 node_modules 目录),复制到您的计算机上。

  2. 编辑 package.json。移除 raspi-io 依赖项;Arduino 平台的 I/O 支持已内置到 Johnny-Five 中,不需要 I/O 插件。

  3. 安装依赖项:

    $ npm install
    
  4. 编辑 index.js 文件:

    1. 移除 raspi-iorequire 语句。

    2. 修改 board 实例化。移除对 Raspi 的引用,使其如下所示:

      const board = new five.Board();
      
  5. 将 BMP180 按照图 11.19(图 11.19)所示连接到 Arduino Uno。

图 11.19. BMP180 和 Arduino Uno 的接线图

11fig19_alt

将 Uno 连接到您的计算机并运行应用程序。这里不需要 sudo

$ node index.js

11.4. 将 Raspberry Pi 作为主机使用

记住,Raspberry Pi 3 是一台“真正的”计算机,所以没有理由你不能将其作为主机在主机-客户端设置中使用,就像你用自己的计算机一样,配备自己的 Arduino。

你需要

note

  • 1 配置好的 Raspberry Pi 3

  • 1 Arduino Uno

  • 1 面板

  • 1 BMP180 多传感器扩展板

  • 跳线

  1. 将 Arduino Uno 版本的天气应用程序代码(同样不包括 node_modules 目录)复制到 Pi 上。

  2. 安装依赖项:

    $ npm install
    
  3. 将连接到 BMP180(如图 11.23(图 11.23)所示)的 Arduino Uno 插入 Pi 的四个 USB 端口之一。

  4. 运行应用程序:

    $ node index.js
    

    (这里不需要使用 sudo,因为你没有使用 Pi 的 GPIO。)

使用 Raspberry Pi 3 和 Tessel 2 的天气站

没有任何技术原因阻止你将 Tessel 2 版本的天气应用程序部署到 Tessel 上(而不是从你的电脑上部署)。然而,首先,你需要在 Pi 上安装 t2-cli 并从它那里配置 Tessel,具体操作请参考 Tessel 的“安装 Tessel 2”页面 (tessel.github.io/t2-start/)。

最终,Raspberry Pi 平台是一个充满选择和选项的广阔世界。Pi 3 本身就是一个真正的计算机,它具有作为主机在主机-客户端设置中的能力,这对于不想处理 Pi 的复杂 GPIO 来说特别方便。但如果你确实想深入研究 Pi 的板载 GPIO,当然有无数种方法可以完成任务。

11.5. 案例研究:BeagleBone Black

Pi 并非市场上唯一的游戏——还有许多其他的单板计算机(SBC)平台。为了了解它们之间的共性和差异,让我们简要地浏览一下 BeagleBone Black (图 11.20),它是 BeagleBoard 家族中的一员。

图 11.20. BeagleBone Black 单板计算机

11.5.1. 了解 BeagleBone Black

比较任何两个 SBC 常常是苹果和橙子的比较;不同的平台针对不同的应用。Pi 3 在 CPU 和外围设备竞赛中获胜(多核 CPU、更高的时钟速度、更多的 USB 端口等),但 BeagleBone Black 在 GPIO 功能方面超过了 Pi,并且在空闲时更加节能。BeagleBone Black 的 Rev C 版本还增加了 RAM:它有 4 GB。

另一个重要的区别是,尽管你可以在 Raspberry Pi 上运行的 Linux 发行版是开源的,但其硬件并非如此。Pi 由 Raspberry Pi 基金会独家制造,并包含一些闭源组件。相比之下,BeagleBoard 是开源硬件。

一块 BeagleBone Black 的价格大约是 55 美元。

核心功能

如前所述,BeagleBone Black 的 CPU 是单核,运行速度为 1.0 GHz,比 Pi 3 的 1.2 GHz 慢。然而,它们都是 ARM v8 芯片,所以总体上并不太相似。如果你选择 Rev C 版本,你将获得 4 GB 的 RAM(旧版本有 512 MB)。与 Pi 一样,BeagleBone Black 有一个 microSD 插槽,但它还内置了 4 GB 的 eMMC 闪存存储。它有一个 USB 端口,一个 mini-HDMI 接口和一个以太网接口 (图 11.21)。

图 11.21. BeagleBone Black 在 BeagleBoard 网站上的信息页面

GPIO 功能和引脚配置

BeagleBone Black 拥有丰富的 GPIO。有 92 个(九十二个!)引脚。它支持 4 个 UART,65 个具有中断能力的数字 I/O 引脚,8 个 PWM 引脚和 7 个模拟输入。而 Pi 是一个同时做 GPIO 的计算机,而 BeagleBone 则更像是一个以 GPIO 为主的冠军,同时也能进行计算。

BeagleBone Black 和非以太网连接选项

基本版的 BeagleBone Black 没有内置 WiFi,但你可以使用第三方适配器添加支持。你可以在嵌入式 Linux Wiki 的 BeagleBone Black 页面上找到支持的 WiFi 适配器列表(mng.bz/Uj9d)。

另一种网络方法允许你通过 USB 连接 ssh 到你的 BeagleBone Black。这可能需要你安装一些驱动程序或调整一些其他配置;有关更多详细信息,请参阅 BeagleBoard 网站的“入门”页面(beagleboard.org/getting-started)。

BeagleBone Black 的引脚分为两组 扩展头,每组有 46 个引脚。由于板的直流电源连接位于顶部,扩展头 P9 位于左侧,P8 位于右侧(至于 P1–P7 发生了什么:不知道)。与 Pi 类似,BeagleBone Black 的许多引脚可以扮演多个角色。

你将使用另一个 Johnny-Five I/O 插件来控制 BeagleBone Black 的 GPIO。图 11.22 展示了 beaglebone-io Johnny-Five I/O 插件支持的引脚及其功能。要使用 beaglebone-io 引用引脚,你需要在引脚的物理编号前加上头文件编号,例如 P9_11 表示 P9 头上的第 11 个引脚。由于你将只使用 I²C 接口,因此你不需要提供任何引脚编号——beaglebone-io 将自动使用引脚 P9_19(SCL)和 P9_20(SDA)。

BeagleBone Black 电压

BeagleBone Black 的模拟输入引脚仅接受高达 1.8 V 的输入电压。

重要的是要注意,图 11.22 中显示的引脚功能并不代表 BeagleBone Black 在这些引脚上提供的所有硬件支持,而是 beaglebone-io 插件提供的支持(用于 Johnny-Five 脚本)。

图 11.22. 通过 beaglebone-io Johnny-Five I/O 插件可用的 BeagleBone Black 引脚功能

图 11.22

例如,这里没有显示 UART/TTL 串行支持,但 BeagleBone Black 确实有几个 UART。有时,寻找相关的引脚图可能会因为引脚实际功能取决于你如何使用它们而变得复杂。

BBB GPIO 超越 Johnny-Five

你计划在其他非 Johnny-Five 的环境中使用你的 BeagleBone Black 吗?你需要寻找一个更完整的引脚图。网上有大量的其他 BeagleBone Black 引脚图。BeagleBone Black 有 96 个引脚,每个引脚可以扮演五到六个不同的角色,并使用不同的命名约定进行引用。这可能会导致一些相当令人眼花缭乱的图表。请耐心一些:这真的是一大堆需要视觉上吸收的信息。

配置和工作流程

一种配置 BeagleBone Black 的方法与本章前面提到的 Raspberry Pi 3 设置非常相似,只是没有 WiFi 设置(没有 WiFi 可配置)。

该板附带Ångström,一个嵌入式 Linux 发行版,但此设置过程将其替换为 Debian。有关创建可启动的 microSD(步骤在此处简化)的更多详细信息,请参阅之前的 Raspberry Pi 3 设置第 11.4 节:

  1. beagleboard.org/latest-images下载最新的 BeagleBone Debian 镜像。

  2. 使用 Etcher 应用程序将镜像烧录到 SD 卡上。

  3. 您不想反复从 SD 卡启动,所以您需要将操作系统镜像刷写到板载 eMMC 闪存中。这需要额外的、稍微有些麻烦的步骤:需要编辑一个配置文件。在您用 Debian 镜像刷写 SD 卡后,您的电脑可能无法读取它(我的电脑就是这种情况)。取而代之的是,您可以一次性直接从 SD 卡启动 BeagleBone Black,以便编辑那个配置文件:

    1. 将带有 Debian 镜像的 microSD 卡插入 BeagleBone Black。

    2. 使用以太网线将 BeagleBone Black 直接连接到您的路由器,然后为 BeagleBone Black 插上电源。BeagleBone Black 需要一两分钟的时间来启动并获取网络上的 IP 地址。

    3. 启动您的局域网或 IP 扫描工具以确定 BeagleBone Black 的 IP 地址。

    4. 从您的电脑上,在终端中,使用debian用户(ssh在此处使用的 Debian 镜像中默认启用)连接到 BBB:

      $ ssh debian@<BBB IP>
      

      该用户有一个默认密码,首次登录时会显示给您。

    5. 编辑相关的配置文件:

      $ sudo vi /boot/uEnv.txt
      

      (或者如果您不喜欢vi,可以使用您喜欢的编辑器)。在文件的底部附近找到以下行并取消注释它(移除#):

      #cmdline=init=/opt/scripts/tools/eMMC/init-eMMC-flasher-v3.sh
      

      保存并退出文件。取消注释该行将允许 BeagleBone Black 在启动时运行一个脚本,该脚本将复制(刷写)SD 卡的内容到内置的 eMMC。

  4. 现在,关闭 BeagleBone Black 的电源。在 SD 卡仍然插入的情况下,按住 BOOT/USER 按钮(图 11.23)并重新连接电源。继续按住按钮几秒钟,直到所有板载 LED 灯瞬间亮起。然后您可以松开按钮。

    图 11.23. BeagleBone Black 上的 BOOT/USER 按钮

  5. eMMC 刷写过程需要一段时间——BeagleBoard 的网站表示需要 30-45 分钟。您可以通过所有 BeagleBone Black 的蓝色 LED 灯都熄灭来判断何时完成(坦白说,从它默认的闪烁中解脱出来是个令人欢迎的休息!)。

  6. 关闭 BeagleBone Black 的电源并弹出 SD 卡。再次打开电源。

  7. 从您的电脑上,以debian用户身份使用ssh连接到 BeagleBone Black。

  8. 运行此命令:

    $ lsb_release -a
    

    您应该看到类似以下内容的输出:

    No LSB modules are available.
    Distributor ID:    Debian
    Description:    Debian GNU/Linux 8.7 (jessie)
    Release:    8.7
    Codename:    jessie
    
  9. 要查看您拥有的 Node.js 版本,请运行此命令:

    $ node --version
    

    我得到了v4.8.0

  10. 如果您想更新 Node.js 版本,欢迎您这样做,但 v4.8.x 已经足够支持本节中其余的代码示例。

示例和教程

接下来,你将使用 Johnny-Five 和beaglebone-io插件来给 BeagleBone Black 进行一次“Hello World”的 LED 式旋转,你可以访问板载 LED,因此这是一个快速的任务(无需电路)。

通过 SSH 连接到你的 BeagleBone Black,并创建一个工作区域(一个项目目录或类似)。在这个目录内,安装 Johnny-Five 和beaglebone-io I/O 插件:

$npm install johnny-five beaglebone-io

创建一个 index.js 文件,并添加以下列表的内容。

列表 11.11. 在 BeagleBone Black 上闪烁 LED
var five = require('johnny-five');
var BeagleBone = require('beaglebone-io');

var board = new five.Board({
  io: new BeagleBone()                 *1*
});

board.on('ready', function () {
  var led = new five.Led();            *2*
  led.blink(500);
});
  • 1 使用 beaglebone-io 插件

  • 2 这里没有给出引脚编号:beaglebone-io 将自动使用板载 LED。

现在运行它!与 Pi 一样,这里你需要sudo

$ sudo node index.js

你现在应该能看到 BeagleBone Black 的一个蓝色 LED 每 500 毫秒闪烁一次。

API 文档

再次强调,你在这里选择的是 Johnny-Five 路线。beaglebone-io插件的文档包含了关于引脚支持和插件使用细节的重要信息(github.com/julianduque/beaglebone-io)。

11.5.2. 为 BeagleBone 适配天气站

你需要的东西

  • 1 配置好网络连接的 BeagleBone Black 和电源

  • 1 面包板

  • 1 BMP180 多传感器扩展板

  • 跳线

到现在为止,天气应用的总体适配模式已经变得熟悉:

  1. 按照图 11.24 所示连接 BMP180 到 BeagleBone Black。

    图 11.24. BMP180 和 BeagleBone Black 的接线图

  2. 在 BeagleBone Black 上创建一个天气目录,并将原始(Tessel 变体)的天气站源代码复制进去,不包括 node_modules。

  3. 编辑 package.json 以移除tessel-io依赖项。

  4. 运行$ npm install来安装依赖项。

  5. 运行$npm install --save beaglebone-io来安装beaglebone-io插件并将其保存到 package.json。

  6. 编辑 index.js:

    1. 移除tessel-io依赖项,并用beaglebone-io替换:

      const BeagleBone = require('beaglebone-io');
      
    2. board实例化改为使用beaglebone-io插件:

      const board = new five.Board({
        io: new BeagleBone()
      });
      

BeagleBone Black 还需要你几个快速技巧。

事实上,BeagleBone Black 默认情况下已经在端口3000上运行了一个服务器。它没有 WiFi,所以没有wlan0网络接口。为了解决这个问题,按照以下方式编辑server.listen()的代码。

列表 11.12. 更新的server.listen()
server.listen(4000, () => {                                               *1*
  console.log(`http://${os.networkInterfaces().eth0[0].address}:4000`);   *2*
});
  • 1 将端口号更改为其他值;4000 也可以。

  • 2 这里使用eth0而不是wlan0,并且更新端口号。

完成了!让它这样:

$ sudo node index.js

现在在你的电脑上的浏览器中打开登录的 URL。

将 I/O 细节隔离到 I/O 插件中使得 Johnny-Five 应用程序在不同平台之间相对便携。确实会冒出一些细节,比如 BeagleBone Black 上不可用的端口3000,但总体来说,在平台之间移动事物通常不会太糟糕。如果你愿意,你还可以运行天气应用程序的 Arduino 兼容变体,使用 BeagleBone Black 作为主机。SBCs 为你提供了一整个选项星座。

摘要

  • 单板计算机(SBCs)增加了大量功能和通用优点,但它们比更受限制的平台消耗更多电力,配置和管理也更复杂。

  • Raspberry Pi 3 是第三代 Raspberry Pi 平台,它可以用于通用计算以及嵌入式应用。Raspbian 是基于 Debian 的 Linux 操作系统,专门针对 Pi 系列进行了优化。

  • SBCs 和 Node.js 开发的常见设置步骤包括刷写(或以其他方式安装或升级)操作系统、配置网络、更新 Node.js 版本以及建立文件系统工作流程。

  • Sysfs 是一个 Linux 伪文件系统,它允许通过虚拟目录和文件与连接的组件和硬件进行交互。

  • 在 Raspberry Pi 上黑客攻击硬件有无数种方法,几乎涵盖了你能想到的任何编程语言的框架和库。

  • WiringPi 是 Raspberry Pi GPIO 的一个流行抽象,它用 C 语言编写,但还有其他几种语言的库。其他不是基于 WiringPi 的框架(包括raspi-io插件)支持 WiringPi 的引脚编号方案,因为它的 Arduino 模拟清晰度可能比其他编号方案更不令人困惑。

  • raspi-iobeaglebone-io I/O 插件分别在 Raspberry Pi 和 BeagleBone Black 平台上支持 Johnny-Five 兼容性。

  • 将 Johnny-Five 应用程序适配为跨平台通常涉及交换 I/O 插件和更新引脚编号。通常这些是唯一必要的更改。

  • Raspberry Pi 也可以用于主机-客户端设置,充当主机。例如,它可以控制连接的 Arduino Uno(客户端)。

  • BeagleBone Black 是一个开源的单板计算机,其功能与 Raspberry Pi 3 大致相同,但更专注于嵌入式和 GPIO 应用。

第十二章. 在云端、浏览器之外

本章涵盖

  • 使用基于云的服务(resin.io)在设备群中部署和管理应用程序

  • 用于与硬件交互的尖端 Web 平台技术,包括 Web 蓝牙和通用传感器 API

  • 使用开放的 Eddystone 协议和蓝牙低功耗(BLE)信标构建物理 Web

  • 使用 Web 蓝牙和 Puck.js 从网页控制硬件

  • 从 BLE 设备读取数据和向其发送命令

对于本章,你需要以下内容:

  • 1 BeagleBone Black 和 5 V 电源

  • 1 Espruino Puck.js

  • 1 Adafruit BMP180 多传感器扩展板

  • 1 半尺寸面包板

  • 跳线

本章开启了一些宏伟的视野。然而,这种规模的宏伟与有限的篇幅相冲突。这里仅触及的一些主题是整个专业领域;关于每个主题的学习内容足以填满书籍、书架,甚至整个图书馆:安全性;网络标准流程;蓝牙和蓝牙低功耗(BLE)架构的复杂性;大规模部署和管理物联网设备。

因此,本章并不是学习旅程的终点,而是一个跳板,为后续的冒险做准备。它的一半揭开了基于云的物联网服务世界的面纱。另一半戴着未来主义者的帽子,探索了我们今天在网页和浏览器上能做什么,以及明天可能到来的是什么。

代码尚未准备好用于生产

本章中的代码尚未准备好用于生产环境。为了追求清晰和简洁,这里牺牲了安全性、性能、优雅降级和可访问性。

12.1. 物联网与云

本书主要关注说明核心电子原理和用 JavaScript 控制硬件。但云是物联网不可或缺的一部分——没有互联网就没有物联网。商业物联网云服务为发明家和企业家提供了将他们的物联网愿景变为现实的方法,提供支持服务,如数据存储、部署工具、RESTful API、分析、安全性、测试、基准测试、调试、监控、开发框架——哇!结果证明物联网是复杂的。而且,涉及到的术语真是太多了!

物联网云服务的概念本身已经模糊不清,而一些物联网公司的产品并不局限于软件。事实上,一些公司结合了物理硬件平台和他们的云服务,创造了一个从上到下的完整方案(图 12.1)。

图 12.1. Particle(www.particle.io)和三星的 ARTIK 服务(www.artik.io)是所谓的“端到端”物联网解决方案。

例如,Particle 制造了 Electron 板,该板具有板载 2G/3G 连接性。您可以通过蜂窝网络将代码部署到 Electron 板,并从 Electron 板读取数据——当然,这依赖于 Particle 的专有云服务进行部署和 I/O。在这种情况下,公司——Particle——提供了硬件(Electron 设备)、软件(您使用他们的 API 编程 Particle 板)和云基础设施(您使用他们的集中式基于网页的工具将代码部署到您的 Particle 设备群组)。

另一个所谓端到端产品提供的例子是三星的 ARTIK 平台。你可以单独使用 ARTIK 硬件“模块”之一——它们的 5 系列、7 系列和 10 系列板都是具备能力的 SBC,随 Fedora Linux 发行版发货——但硬件本身只是它们集成生态系统的一部分。他们希望你会选择使用他们的云服务。

这是一个深不可测的领域,如果你没有熟悉的标志来作为你航行的参考,可能会感到不知所措,而且有大量的术语和商业术语。为了减轻这种新颖的冲击,我们不会从头开始一个项目,而是将我们的老朋友,具有浏览器界面的迷你气象站,适配到 BeagleBone Black 上。

我们将使用相同的气象站应用程序代码——进行一些小的调整——但不是直接在设备上操作来管理操作系统和运行应用程序代码,我们将让 resin.io 为我们完成这项工作。

12.2. 使用 resin.io 进行容器化部署

你需要的东西

  • 1 块 BeagleBone Black 和电源

  • 1 根以太网线

  • 1 张 microSD 卡和适配器

  • 1 块面包板

  • 1 块 BMP180 多传感器扩展板

  • 跳线

Resin.io (resin.io/)是一个提供将互联网连接的、已配置的 Linux 物联网设备进行容器化部署和管理的服务。这听起来有些复杂——这就是我之前警告过的一些术语——所以让我们从它试图解决的实际问题角度来解释它。

回想一下第十一章,SBC(如 BeagleBone Black)通常能够运行各种 Linux 版本,但为了这种灵活性所付出的代价是,Linux 的安装和管理可能会给基于 SBC 的工作流程增加一些开销(和痛苦)。从你首选的开发环境(如你的笔记本电脑)将代码文件上传到设备上可能是一项繁琐的工作。管理环境设置和配置可能是一个头疼的问题。你可能渴望迭代性地工作,使用熟悉的软件开发方法和工具(如 Git 进行版本控制)并与其他开发者协作。逐一设置所有这些可能需要时间和精力,或者甚至可能让你完全不知所措。

现在想象一下,如果你的物联网应用程序不仅需要在单个 BeagleBone(或 Pi,或其他 SBC)上运行,还需要在可能散布在州、省、国家地理分布的整个机队上运行。跟踪设备、保持同步、向每个设备发送正确的代码版本、监控设备故障、推送操作系统或安全更新——手动完成所有这些工作不会很好地扩展。然后还有所有那些由真实用户使用的软件和硬件产品的要求:分析(特定设备的使用量是多少?)、安全性(让我们确保用户的心率通过安全连接上传!),等等。

这里有很多事情在进行中。为了完成其中许多事情,resin.io(以及一些其他类似的服务)采用了几个关键策略:

  • 容器化 这里关键的想法是,你希望同样的应用程序在每一台设备上以相同的方式运行。Resin.io 使用 Docker 容器来干净、可靠地打包应用程序及其依赖项。你的应用程序容器将被部署到每个已配置的设备上。

  • 配置 某个设备需要能够获取 resin.io,识别自己,并接收应用程序更新。为了实现这一点,你需要从 resin.io 下载一个自定义磁盘镜像,并从它启动每个设备。一旦设备成功配置,它将出现在关联的 resin.io 应用程序的网页仪表板上,正如你很快就会看到的。

  • 版本控制集成 将代码推送到指定的 Git 仓库的特定远程位置会自动触发应用程序在所有连接的、已配置的设备上的重新部署。设置 resin.io 项目的部分工作是定义要使用哪个 Git 仓库作为应用程序源。

容器,容器,容器!

现在在互联网上,你几乎无法避免遇到某人在说关于容器的事情(通常含糊不清,但几乎总是赞誉有加)。你会读到容器是应用程序部署、安全性、性能、确保世界和平等问题的最佳解决方案,但更难找到的是关于容器究竟是什么以及它在实际中做什么的解释。

容器既用于封装和隔离应用程序及其正常运行所需的组件(依赖项、设置等)。单个服务器或计算设备可以同时运行多个独立的容器,而不会相互干扰。同样,相同的容器可以被部署到许多不同的计算机上,因为容器包含了定义应用程序环境及其所有依赖项所需的所有东西——因此,你可以确信应用程序将在不同的设备上以相同的方式运行。

Resin.io 使用 Docker 容器。Docker 是特定容器化技术平台的名字,也是创建该平台的公司。Docker 是目前行业中最受欢迎的容器技术。

一旦设置了 resin.io 项目和其设备,您就可以迭代地开发您的应用程序。当您将更改推送到您的 resin.io Git 远程仓库时,resin.io 会使用更新的代码重新构建应用程序的容器,并将该容器部署到所有连接的、已配置的应用程序设备上,无论它们在哪里(图 12.2)。

图 12.2. 运行应用程序的设备通过在每个设备上安装和启动定制的 resin.io OS 镜像进行配置。将应用程序代码推送到特定项目的 resin.io Git 远程仓库会触发应用程序容器的重新构建和重新部署到所有连接的、已配置的设备。

快速掌握 Git

您的 resin.io 应用程序代码将在 Git 仓库中管理——您需要在您的计算机上安装 Git。git-scm 网站上的“安装 Git”部分记录了如何在多个不同的平台上完成此操作(git-scm.com/book/en/v2/Getting-Started-Installing-Git)。

虽然构建 resin.io 应用程序的说明中包含了您在 BeagleBone Black 上启动应用程序所需的全部 Git 命令,但了解 Git 的基本知识应该是每个开发者心理工具箱的一部分。学习 Git 基础知识只需要几分钟(尽管,说实话,精通 Git 需要一生的时间)。GitHub 有一个交互式教程(try.github.io/levels/1/challenges/1),还有大量的其他在线 Git 教育资源——其中许多是免费的。

12.2.1. 创建 resin.io 应用程序

您将为您的气象站软件创建一个 resin.io 应用程序,用于部署到您的 BeagleBone Black。Resin.io 提供了一个免费层,允许最多五个设备的部署和管理。前往resin.io/创建一个账户(图 12.3)。

图 12.3. 在 resin.io 上注册账户。

接下来,创建一个应用程序。系统会提示您输入应用程序的名称和设备类型。您可以根据喜好命名,我将其命名为beagleweather。在设备类型字段中,从长长的选项列表中选择BeagleBone Black(图 12.4)。

没有 BeagleBone Black?

没有手头的 BeagleBone Black?如果您愿意,可以使用 Raspberry Pi 代替。您需要使用 Pi 版本的天气应用程序代码(在书籍 GitHub 仓库的第十一章文件夹中找到)——带有raspi-io I/O 插件——但其他步骤应该相同。哦,当然,确保在选择 resin.io 应用程序的设备类型时选择Raspberry Pi 3而不是BeagleBone Black

图 12.4. 应用程序创建步骤的详细图,显示了支持的设备类型列表

12fig04.jpg

12.2.2. 配置 BeagleBone Black

Resin.io 为每个项目生成一个定制的操作系统镜像。resin.io 操作系统是一个轻量级的 Linux。它可以运行你的应用程序的 Docker 容器,并且还负责一些日常维护工作,如配置设备和监视部署的更新。

一旦你定义了你的 resin.io 应用程序,你可以继续下载生成的操作系统镜像(如图 12.5 所示提供了链接)并在你的设备上安装它(或者在我们的情况下,单个设备)。有关此处步骤的更多详细信息,请参阅第十一章 11.2.3 节:

  1. 下载操作系统镜像。

  2. 使用 Etcher 应用程序,将 IMG 文件烧录到 microSD 卡上。

  3. 将 microSD 卡插入 BeagleBone Black,并将 BeagleBone Black 的以太网接口连接到你的路由器。

  4. 按住 USER/BOOT 按钮(图 12.6),并将电源插入 BeagleBone Black。继续按住按钮,直到 LED 灯疯狂闪烁。然后释放按钮。

图 12.5. resin.io 项目仪表板在为 BeagleBone Black 设备配置过程中的详细图。该设备由 resin.io 自动命名为“red-night”,正在安装特定于应用程序的 resin.io 操作系统镜像。

12fig05_alt.jpg

图 12.6. 按住 BeagleBone Black 的 BOOT/USER 按钮从 SD 卡启动。

12fig06_alt.jpg

回到你的电脑上,几秒钟后你应该能在 resin.io 应用程序的仪表板上看到设备。从仪表板,你可以跟踪设备在配置过程中的进度。

12.2.3. 适配天气应用程序软件

要在配置好的 BeagleBone Black 上运行天气应用程序软件,还需要进行几个额外的步骤:

  1. 为应用程序设置和配置 Git 仓库。

  2. 配置应用程序的 Docker 容器。

  3. 定义一个脚本以启动应用程序(在 package.json 中)。

  4. 对软件本身进行一些调整。

  5. 提交并推送到 GitHub。

  6. 为应用程序提供一个公开的 URL。

初始化 Git 仓库

首先,你需要建立一个工作区域。在你的电脑上创建一个目录来存放项目(例如beagleweather?)。

在这个目录内,通过运行以下命令初始化一个 Git 仓库:

$ git init

要部署你的应用程序,你需要能够将你的仓库的 master 分支推送到 resin.io。你需要添加一个 resin.io 特定的 Git 远程,以便以后可以推送到 resin.io。resin.io 方便地显示了你需要运行的精确命令来设置此远程:在你的应用程序仪表板的右上角找到它。在你的本地仓库中执行显示的命令(图 12.7)。

图 12.7. resin.io 应用程序仪表板的详细图,显示了添加应用程序的 resin.io Git 远程的命令位置

你可以通过运行此命令来查看你仓库的所有远程:

$ git remote -v

你应该看到类似以下输出:

resin    <your resin username>@git.resin.io:<your resin username>/
 beagleweather.git (fetch)
resin    <your resin username>@git.resin.io:<your resin username>/
 beagleweather.git (push)

接下来,将原始 BeagleBone 天气应用程序的源文件从复制到你的新工作 Git 项目目录中——index.js、package.json 以及应用目录及其内容。

定义 Docker 应用容器

现在运行在你 BeagleBone Black 上的应用程序的基础 resin.io OS 由一个精简的 Linux 系统组成,并包含一些有用的支持工具。但你需要创建一个将在你的应用设备上运行的 Docker 容器,并告诉它如何表现以及做什么。

在你的项目目录中创建一个名为 Dockerfile.template 的文件(“template”扩展名允许在文件中使用某些方便的变量)并添加以下列表中的内容。列表的大部分是样板代码,直接来自 resin.io 的文档。

列表 12.1. Dockerfile.template
# base-image for node on any machine using a template variable
FROM resin/%%RESIN_MACHINE_NAME%%-node:6                                   *1*

# Defines our working directory in container
WORKDIR /usr/src/app

# Copies the package.json first for better cache on later pushes
COPY package.json package.json

# This install npm dependencies on the resin.io build server,
# making sure to clean up the artifacts it creates in order to reduce the
 image size.
RUN JOBS=MAX npm install --production --unsafe-perm && npm cache clean &&
 rm -rf /tmp/*

# This will copy all files in our root to the working  directory in the
 container
COPY . ./

# Enable systemd init system in container
ENV INITSYSTEM on

# server.js will run when container starts up on the device
CMD ["npm", "start"]                                                       *2*
  • 1 使用带有 Node.js,版本 6 的基础镜像

  • 2 你需要定义一个 npm start 脚本来启动你的应用程序。

更多关于 resin.io 主机名、基础镜像和标签的信息

在 resin.io 的 Dockerfile 中,你可以指定关于设备、功能和 Linux 发行版和版本的大量详细信息。这里的内容太多,但如果你对此类内容感兴趣,resin.io 的文档非常详尽 (docs.resin.io/raspberrypi/nodejs/getting-started/)。

添加 npm start 脚本

这行代码出现在 Dockerfile.template 配置的末尾:

CMD ["npm", "start"]

这告诉构建器在容器启动后运行命令 npm start。你还没有 start 脚本,但你可以通过编辑 package.json 来轻松添加一个。

编辑 package.json 中的 scripts 字段,使其看起来像以下列表。

列表 12.2. package.json 脚本
"scripts": {
  "start": "node index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

现在,当构建过程运行 npm start 命令时,它将产生与执行 node index.js 相同的效果——它将启动你的天气应用程序。

调整应用代码

在完成之前,你需要对 index.js 进行两个小的修改。

Johnny-Five 的 REPL 和 resin.io 不兼容。你可以通过向板子实例化选项对象中添加一个属性 (repl: false) 来轻松禁用 REPL。

列表 12.3. 禁用 Johnny-Five REPL
const board = new five.Board({
  io: new BeagleBone(),
  repl: false
});

server.listen() 中为 Web 服务器定义的现有端口(4000)将正常工作,但你将执行一个很酷的技巧,所以将端口更改为 80

列表 12.4. 将 Web 服务器端口更改为 80
server.listen(80, () => {
  console.log(`http://${os.networkInterfaces().eth0[0].address}:80`);
});
提交和推送

将项目文件添加到 Git 中(确保你处于项目的顶级目录):

$ git add index.js package.json Dockerfile.template app/
$ git commit -m "first commit"

现在,推送以触发到你的 BeagleBone Black 的部署:

$ git push resin master

第一次这样做时,可能需要几分钟(之后会更快)。您可以在 resin.io 仪表板上跟踪进度(图 12.8)。您将能够在计算机上的浏览器中查看应用,并使用其本地 IP 地址。

图 12.8. 部署期间的 Resin.io 应用仪表板

12fig08_alt.jpg

要迭代您的应用,您可以在本地仓库中做出更改,提交,并根据需要频繁地推送。

为您的应用提供公共 URL

您已经为您的设备构建了许多基于 Web 的界面,但到目前为止,您只能从同一网络访问您的 Web 应用。Resin.io 有一个很酷的功能,可以为设备生成一个公共 URL,允许外部访问该设备的 80 端口(现在您知道为什么您更改了端口号了!)。

要为整个应用启用公共 URL,请从应用的仪表板中的操作部分进入(图 12.9)。

图 12.9. 从应用的操作选项卡中启用应用的公共 URL

12fig09_alt.jpg

接下来,通过导航到设备部分并点击您想要管理的设备名称(在我的情况下是red-night)来进入设备特定的视图(图 12.10)。

图 12.10. 通过导航到设备特定的操作选项卡来查看设备的公共 URL。

12fig10_alt.jpg

再次前往操作部分——这些操作仅针对此设备,而不是整个应用。您应该能够看到——并且,方便的是,可以点击——在您的 BeagleBone Black 上运行的应用的公共 URL(图 12.11)。

图 12.11. 运行在 BeagleBone Black 上的气象站应用,通过由 resin.io 生成的公共 URL 访问

12fig11_alt.jpg

12.3. 硬件和网页浏览器

气象站应用利用了一个相对现代的 Web API,WebSocket(更准确地说,它使用socket.IO,该库在支持 WebSocket 的浏览器中使用 WebSocket API)。此 API 用于在客户端(浏览器)和服务器(在本例中运行在 BeagleBone Black 上)之间保持连接。服务器能够发出更新的传感器数据,客户端可以“听到”并相应地处理。但这仍然是间接的——浏览器依赖于服务器来处理与 BeagleBone Black 的 I/O 和附加传感器的实际交互。是否有可能在浏览器本身直接与硬件交互?

答案是复杂的——是“视情况而定”和“在某些浏览器中”的结合,还包含大量的“再稍等一会儿”。

毫无疑问,与几年前相比,网络在物理能力上要强大得多。不久前,您必须创建一个本地应用才能访问移动设备的位置服务或其摄像头,或发送推送通知。现在所有这些都可以在浏览器中完成。

网络平台是一个(庞大的)技术集合和 API。当然,有那些明星技术——HTML、ECMAScript 和 CSS(尽管技术上 CSS 不是一个单一的东西,而是一系列 模块)——但还有成百上千的其他部分:Web Workers、WebGL、XMLHttpRequest、Web Audio、WebSocket、WebRTC(我可以一直列举下去)。不同的技术和 API 在标准化过程中处于不同的阶段,有些甚至根本不在标准轨道上。

12.3.1. Web Bluetooth API

Web Bluetooth API 允许通过通用属性配置文件(GATT)与蓝牙低功耗(BLE)设备进行交互。在 W3C(万维网联盟,负责制定网络标准的主体)上有一个活跃的 Web Bluetooth 社区小组,并且它在 Chrome、Opera 和 Android 浏览器的一些版本中得到了实现。但它既不是标准,甚至还没有进入标准轨道(尽管它正朝着这个方向前进)。这很复杂。

Web Bluetooth 也遇到了典型的网络硬件 API 的障碍:安全问题。不需要天才就能想象出一些物理设备暴露在网络上可能带来的安全噩梦。到目前为止,Mozilla(Firefox)没有追求 Web Bluetooth 的实现,因为它对(当前的)安全模型不满意。

但别让这听起来很悲观,这些挑战对于提议的标准来说并不典型。只是很难预测 Web Bluetooth 在六个月或两年后会是什么样子。而且你今天就可以在 Chrome 中使用它。

12.3.2. 通用传感器 API

Web Bluetooth 不是镇上唯一的初出茅庐的、与硬件相关的网络 API(尽管它目前拥有最完整的浏览器实现)。W3C 的设备和传感器工作组(www.w3.org/2009/dap/) 负责创建与设备硬件、传感器、服务以及如相机、麦克风、接近传感器、原生地址簿、日历和原生消息应用等应用程序交互的 API。

该小组的通用传感器 API(www.w3.org/TR/generic-sensor/),目前处于草案阶段,定义了一个抽象的 Sensor 基类,它本身并不做任何事情,但旨在被特定组件的 API 扩展。一个例子是环境光传感器 API(也是一个草案,www.w3.org/TR/ambient-light/)),它定义了一个基于 Sensor 的接口,用于与环境光传感器交互。如果你审查了规范细节,可能不会感到惊讶,因为通用和其他传感器 API 的编辑之一是 Rick Waldron——Johnny-Five 的发明者。API 的组件行为封装确实与 Johnny-Five 抽象行为的方式相协调。

这些 API 并不关心硬件检测和连接的细节,更多的是定义一个高级 API 来与组件交互。因此,早期实现的周围光传感器依赖于内置硬件的存在(通常是设备的摄像头),以及浏览器相应地公开该硬件。周围光传感器在 Chrome 和 Microsoft Edge 中可用(在标志后面——您必须在浏览器的设置中明确启用它)。

12.3.3. 物理网

物理网是一种发现服务,极大地简化了通过简单的蓝牙低功耗信标与对象交互,利用了网络给予我们的一项伟大礼物:URL。

我们在很久以前(第一章)简要地探讨了物理网。当时设想的是具有信标功能的公交车站场景。信标不断广播一个 URL,该 URL 对应的页面可以追踪下一班公交车的到达情况(图 12.12)。

图 12.12. 在物理网中,附着或嵌入在物理对象上的低功耗蓝牙(BLE)信标广播与该设备交互相关的 URL。附近的用户可以检测并与之互动,在浏览器中打开其关联的网页。

图 12.12

或者,假设您正在穿过一个雕塑花园。其中一些作品附近或上面有信标,广播一个指向包含该作品及其艺术家信息的网页的 URL。在您漫步时,您可以看到附近的信标,并选择与之互动,访问它所宣传的 URL。

谷歌正在推广物理网及其相关的开放 BLE 协议 Eddystone。因为技术要求非常直接——一个 BLE 设备只需使用某种协议广播一个 URL——所以硬件需求最小,信标电池可以持续很长时间。现在可用的 Android 和 iOS 应用程序允许您找到您附近的任何信标。

12.4. 使用 Puck.js 探索蓝牙低功耗

Espruino 制造了一种设备,让您可以立即——今天!现在!——实验 Web Bluetooth 和物理网。Puck.js 是一个按钮形状的设备,运行 Espruino 解释器和通过 BLE 通信(图 12.13)。您将利用它来亲身体验 Web Bluetooth 和物理网。

图 12.13. Puck.js 是一个 BLE 信标设备,它像 Pico 一样运行 Espruino JavaScript 解释器。

图 12.13

12.4.1. 核心功能

Puck.js 是基于一款超低功耗的 Nordic Semiconductor SoC(系统级芯片)构建的,该芯片包括蓝牙低功耗和 NFC 支持,以及一个与 Pico 不太相似的 ARM Cortex M4 CPU。尽管 Pico 和 Puck.js 都运行相同的底层 Espruino 软件,但 Puck.js 与 Pico 的通信方式不同:通过 BLE 而不是 Pico 的直接 USB 连接。

Puck.js 的板载组件包括内置磁力计、温度计和三个 LED(红色、绿色、蓝色)。红色 LED 还可以用作环境光传感器。

Puck.js 是一个由纽扣电池供电的 3.3V 设备。它大致相当于 Pico,尽管有些限制:时钟速度较慢,内存略少,I/O 引脚较少(图 12.14)。

图 12.14. Espruino 网站上列出的 Puck.js 的一些功能

Puck.js 有一个灵活的硅胶外壳。整个设备可以作为一个大按钮操作,当您按下它时会产生触觉点击声(并激活其内置按钮)。

12.4.2. GPIO 功能和引脚排布

Puck.js 具有各种 GPIO 功能,包括 I²C、PWM、SPI 和 ADC(图 12.15)。在这些实验中,您将使用其板载硬件(仅限板载硬件)。

图 12.15. Espruino.com 上 Puck.js 引脚排布的细节。引脚 D11 可以感应电容输入。任何引脚都提供 I²C、SPI 和 USART 支持(每个引脚支持一个硬件接口,但对于 I²C 和 SPI,软件支持是无限的)。

12.4.3. 配置和工作流程

前往 Espruino 的 Puck.js 入门指南:www.espruino.com/Puck.js+Quick+Start。您需要拆卸设备以移除保护电池标签。

之后会发生什么取决于您的开发平台操作系统和 Web 蓝牙的当前状态。Mac OS 上的 Chrome 支持它——这很简单。Linux 用户可能需要额外一步,可能需要在 Chrome 中启用一个或两个标志。Windows(在 Chrome 中)的 Web 蓝牙支持可能即将到来(2017 年中期)。请参阅入门指南以获取最新信息。

如果您跟随了第十一章中的 Pico 实验,您可能已经安装了 Espruino Chrome 应用 IDE。如果没有,不要担心,因为您实际上可以使用位于www.espruino.com/ide的基于 Web 的 IDE 来使用 Puck.js(无需安装)。Web IDE(图 12.16)与 Chrome 应用 IDE 几乎一模一样。

图 12.16. Espruino 的基于 Web 的 IDE

如果您无法获得 Web 蓝牙支持...

在本章后面的部分,进行基于 Web 的 LED 和远程门铃实验需要支持 Web 蓝牙的浏览器。对于基本的 Hello World LED 闪烁,以及 Physical Web 示例,您不需要 Web 蓝牙支持——对于这些,Puck.js 可以在 IDE 内部进行控制。

如果您没有 Web 蓝牙支持,您将无法使用位于www.espruino.com/ide的基于 Web 的 IDE。相反,您需要安装 Web IDE(有关详细信息,请参阅 Puck.js 的入门指南:www.espruino.com/Puck.js+Quick+Start)。

还记得从 第十一章 中提到的,Espruino 代码编写涉及使用 Espruino 模块——封装的行为和支持,可以导入到 Espruino 脚本中。

有三种主要方式让 Puck.js 做某事:

  • 一旦连接,Puck.js 将执行 IDE 界面左侧输入的任何命令,类似于 REPL 或控制台。

  • IDE 右侧编写的代码可以使用发送到 Espruino 的图标上传。

  • 数据和命令可以通过 Web Bluetooth 发送到 Puck.js 并从 Puck.js 接收。这是在浏览器执行上下文中完成的,依赖于 Espruino 提供的少量客户端 JavaScript 库。(这种方法是 Puck.js 独有的,与其他 Espruino 板不同。)

12.4.4. 示例、教程和 API 文档

启动 IDE 并连接到你的 Puck.js,这样你就可以尝试 Hello World LED 闪烁并探索 Puck.js 的几个功能。

尝试在 IDE 的左侧输入以下命令之一。按下 Enter 后,命令将通过 BLE 发送到 Puck.js 并在那里执行:

  • 使用 LED1.set() 打开板载红色 LED。

  • 使用 LED1.reset() 关闭板载红色 LED。

  • 尝试使用其他两个 LED:LED2(绿色)和 LED3(蓝色)执行前面的命令。

  • 红色 LED 还可以作为环境光传感器。确保所有 LED 都已关闭(LEDx.reset()),然后尝试使用 Puck.light() 来获取环境光读数。尝试用手遮挡 Puck.js 并第二次发送 Puck.light() 命令,以查看差异。

  • 尝试 E.getTemperature() 获取温度读数。E 是 Espruino 的实用类。温度应准确到约 +/-1 度(摄氏度)。

Espruino 的 API 文档(www.espruino.com/Reference)涵盖了所有 Espruino 设备可用的 API,以及 Puck 全局对象上公开的 Puck 特定功能。

12.4.5. 从网页控制 LED

你需要

  • 1 个 Puck.js

  • 1 个支持 Web Bluetooth 的网络浏览器

正如你所见,你可以通过使用 IDE 控制 Puck.js:发送命令或编写脚本并将其部署到设备。这与 Espruino Pico 的流程类似。

但还有另一种方法:你可以在浏览器中的自己的代码中控制 Puck.js。在这个实验中,你将构建一个网页,允许用户通过点击浏览器中的按钮来打开和关闭 Puck.js 的红色 LED(图 12.17)。

图 12.17. 在支持 Web Bluetooth 的浏览器中,可以通过网页控制 Puck.js 的 LED。

页面的 JavaScript 需要与 Puck.js 配对并与之通信(发送命令)使用 Web Bluetooth。

Espruino 通过提供一个小的客户端库(一个 JavaScript 文件)来简化这一部分,你可以在我们的页面上使用它。该库抽象了 Web Bluetooth API 的细节,提供了一个简单的接口,你可以使用它来配对并与 Puck.js 交互。

你可以在www.puck-js.com/puck.js找到这个客户端库。你需要在项目的 index.html 页面中包含它。

Web Bluetooth:仍然好奇?

如果你好奇 Web Bluetooth 在底层是如何工作的,你可以阅读 puck.js JavaScript 文件的源代码,它有很好的注释:www.puck-js.com/puck.js

puck-js.com 的 JavaScript 库为你提供了连接和与 Puck.js 通信的实用工具,但你仍然需要编写自己的特定逻辑来处理按钮点击并向 Puck.js 发送命令来打开和关闭 LED。

设置项目结构

首先,你需要建立一个工作区域——创建一个名为 led-toggle 的目录。然后,在该目录内运行以下命令:

$ npm install express

这就是唯一的依赖项目。

接下来,在 index.js 中创建一个应用程序入口点,它为 app/中的资源启动一个超级基本的静态 Web 服务器,如下面的列表所示。

列表 12.5. index.js
const express = require('express');
const path    = require('path');
const http    = require('http');

const app    = new express();
const server = new http.Server(app);

app.use(express.static(path.join(__dirname, '/app')));
server.listen(3000);

现在是时候创建应用程序的 HTML 页面了。为此,在项目中创建一个名为 app 的目录,并添加 index.html,其内容如下所示。

列表 12.6. index.html
<html>
 <head>
  <title>Puck.js LED Toggle</title>
  <style>
    h1 {
      font-family: "Helvetica";
    }
    button {
      display: block;
      width: 6em;
      height: 4em;
      margin: 2em;
      background-color: #eee;
      border: 1px solid #ccc;
      font-size: 1.75em;
    }
  </style>
 </head>
  <body>
  <h1>Web Bluetooth Puck.js Toggle</h1>
  <div id="message"></div>                                        *1*
  <button id="onButton">ON</button>
  <button id="offButton">OFF</button>
  <script src="https://www.puck-js.com/puck.js"></script>         *2*
  <script>                                                        *3*
    // ... TBD
  </script>
  </body>
</html>
  • 1 用于在 Web Bluetooth 不受支持时存储消息的容器

  • 2 提供一个 Puck 对象,用于通过 Web Bluetooth 与 Puck.js 通信

  • 3 应用程序客户端逻辑:你需要编写它!

HTML 页面本身不做任何事情:它包含一些 CSS,并包含来自 puck-js.com 站点的脚本,该脚本允许你使用 Web Bluetooth 与 Puck.js 通信。它还包括 ON 和 OFF 按钮的标记,但目前它们还没有做任何事情。

创建 LED 切换逻辑

让我们讨论一下 ON 和 OFF 按钮的点击处理器。当按钮被点击时,你需要向 Puck 发送一个命令来打开或关闭它的红色 LED。正如你在第 12.4.4 节中看到的,这是 Puck.js 需要执行的命令来打开红色 LED:

LED1.set();

这是你需要通过puck.js客户端库作为信使发送到 Puck.js 的命令。该命令需要作为字符串发送到 Puck.js,包括\n(换行符)字符。这是生成的命令字符串:

'LED1.set();\n'

要将此发送到 Puck.js,你将使用Puck对象的write()方法,这是在页面 JavaScript 中全局可用的,因为你包含了客户端的puck.js库(图 12.18)。

Puck.write('LED1.set();\n');
图 12.18. 你的浏览器执行的 JavaScript 在 puck.js 库提供的 Puck 对象上调用 write() 方法。该库使用 Web Bluetooth 将字符串发送到物理 Puck.js,并对其进行评估。字符串命令末尾的 \n 让 Puck.js 知道命令已完成;这几乎就像在虚拟 REPL 中输入并按 Enter 键一样。

Puck.js 究竟是什么意思?

浏览器中有一个 Puck 对象,你可以在 Puck.js IDE 中向其发送命令,而 Puck.js 和 puck.js 完全是不同的事物。哎呀!

命名约定确实有点让人头昏脑胀。以下是一个总结:

  • Puck.js——Puck.js 物理设备本身

  • puck.js——Espruino 提供的客户端 JavaScript 库,用于在浏览器中通过 Web Bluetooth(BLE)与 Puck.js 设备通信

  • Puck 对象——令人困惑的是,这取决于代码执行的上下文,有两种(完全不同)的事物:

    • 在 IDE 或直接在 Puck.js 上运行的脚本中——它是 Espruino 的 Puck 全局类 (www.espruino.com/Reference#Puck),它为 Puck.js 添加了一些特定的硬件交互功能——也就是说,这些功能在其他 Espruino 板上不可用(例如使用 Puck.light() 从 Puck.js 的环境光传感器读取数据)

    • 在浏览器中,假设包含了 puck.js 客户端库——提供了一些通过 Web Bluetooth 与 Puck.js 通信的方法(例如 Puck.write()Puck.connect())。请记住,任何发送的命令都在 Puck.js 本身上的 Espruino 解释器中评估。

这意味着在浏览器脚本中——假设包含了 puck.js 客户端库——以下语句是有效的:

Puck.write('Puck.light();\n');

Puck.write() 在浏览器上下文中执行,这意味着它指的是包含的 puck.js 库提供的对象。但是,它通过 write() 发送的命令在 Puck.js 本身上评估:Puck.light() 中的 Puck 对象是对全局 Espruino Puck 对象的引用。呼!

以下列表定义了在 index.html 中的 <script> 标签之间放置的内容:点击事件监听器和发送到 Puck.js 的命令。

列表 12.7. 用于切换 LED 的事件监听器
window.addEventListener('load', () => {
  if ('bluetooth' in window.navigator) {
    const onButton = window.document.getElementById('onButton');
    const offButton = window.document.getElementById('offButton');
    onButton.addEventListener('click', () => Puck.write('LED1.set();\n'));
    offButton.addEventListener('click', () => Puck.write('LED1.reset();\n'));
  } else {
    const mEl = window.document.getElementById('message');
    mEl.innerHTML = "Looks like your browser doesn't support Bluetooth!";
  }
});

列表 12.7 中的模拟功能检测——if ('bluetooth' in window.navigator)——诚然是笨拙且天真的。仅仅因为浏览器暴露了 navigator.bluetooth,并不意味着它正确实现了 Puck.js 所需的功能。在 JavaScript 代码中,有一个名为 checkIf-Supported() 的函数,它提供了一个更正确、更全面的浏览器支持检查。不幸的是,该函数并未暴露给 Puck 对象——它不在你能够访问的任何作用域中——因此你不能直接调用它。

尝试一下!使用 node index.js 启动网络服务器,并打开一个支持 Web Bluetooth 的浏览器,访问 localhost:3000。

当你第一次点击按钮时,你会看到一个配对请求弹出,类似于图 12.19。一旦配对完成,你应该能够点击 ON 和 OFF,并看到 Puck.js 的红色 LED 灯亮起和熄灭。

图 12.19. 当你第一次点击按钮时,你会被提示与 Puck.js 配对。

12.4.6. 物理网和 Puck.js

你需要什么

  • 1 Puck.js

  • 1 运行 Android 或 iOS 的移动设备

物理网由使用特定格式——Eddystone——的信标组成,用于广播相关的 URL,并且这些广播可以被运行在移动设备上的应用程序接收。

Puck.js 可以很容易地作为物理网兼容的信标。要启动这个过程,你需要做以下事情:

  • 在移动设备上安装物理网发现实用程序,或在 Chrome 中启用此功能。

  • 确定你希望 Puck.js 广播的 URL。

  • 让 Puck.js 开始作为兼容的信标进行广告。

Eddystone 协议

Eddystone 是由 Google 创建的一个开放的 BLE 信标协议。物理网信标使用此格式来广播它们相关的 URL,并且客户端应用程序检测这些 Eddystone 信标。

Eddystone 非常简单。Eddystone 设备可以发送的信息种类很少——帧类型——,其中最相关的是Eddystone-URL

Eddystone-URL的长度限制

Eddystone-URL URL 的最大长度为 17。这很紧凑。但它并不像听起来那么受限。还有一个单独的字节用于存储 URL 的方案前缀的表示(例如 https://www.,http://等)——这些字符不计入 17 个字符。此外,常见的顶级域名(.com,.org 等)可以用一个字符表示,留下 16 个字符。假设开发者会使用 URL 缩短器(如goo.gl)来最小化 URL 长度。

URL www.lyza.com 在其正常形式下有 20 个字符长,但它只需要 17 个可用字节中的 5 个。

将 Puck.js 配置为信标

将 Puck.js 设置为 Eddystone 兼容的物理网信标几乎令人难以置信地简单。有一个ble_eddystone Espruino 模块正等着你!启动 Web IDE,连接到你的 Puck.js,并在 IDE 的左侧输入以下命令:

require("ble_eddystone").advertise("https://www.lyza.com");

(当然,你可以随意用你喜欢的任何 URL 替换我的域名 URL。)

断开 IDE 与 Puck.js 的连接,以便它能够以 Eddystone 格式开始广播。

在你的移动设备上启用物理网发现

你可以使用运行 Android 或 iOS 的设备检测你的 Puck.js 物理网信标(图 12.20);说明可以在物理网网站上找到(google.github.io/physical-web/try-physical-web)。一旦配置完成,你的移动设备应该能够看到你的 Puck.js 信标。

图 12.20. 通过 iOS 上的 Chrome 小部件支持物理网络。Puck.js 已配置为使用 Eddystone 协议广播 URL www.lyza.com

是时候以一声巨响(好吧,至少是一声叮当声)离开了。我们的最后一个实验将结合 Web Bluetooth、Web Audio 和从 Puck.js 发送的数据。

12.4.7. 基于网络的蓝牙门铃

您需要什么

  • 1 Puck.js

  • 1 具有 Web Bluetooth 功能的网络浏览器

此实验在按下配对的 Puck.js 按钮时,在浏览器中播放(高质量)声音并显示视觉警报。将其视为基于网络的门铃。它利用 Web Audio API——另一个出色的网络 API——来加载和播放(公有领域)的铃声。

应用逻辑使用客户端 puck.js 库通过 Web Bluetooth 连接到 Puck.js,配置 Puck.js 监控其板载按钮,并解析 Puck.js 发出的数据——字符串输出将表示按钮按下。

设置项目结构

首先建立一个工作区域。创建一个目录(“门铃”)并在该目录中安装 express 作为依赖项:

$ npm install express

创建 index.js,静态网络服务器。您可以在 列表 12.5 中重用代码,该代码将在端口 3000 上运行静态网络服务器。

创建 HTML 和事件监听器

在门铃内部创建一个应用目录。添加一个包含以下内容的 index.html 文件。

列表 12.8. index.html
<html>
 <head>
  <title>Puck.js Remote Chime</title>
  <style>
    body {
      max-width: 90%;
      font-family: "Times New Roman";
      margin: 1em auto;
      color: #111;
      background-color: transparent;
      transition: background-color 0.5s ease-in-out;
    }
    .ding {
      background-color: #e60a62;
      transition: all 0.1s ease-in-out;
    }
    button {
      width: 100%;
      height: 100%;
      border: 5px solid #e60a62;
      font-family: "Times New Roman";
      text-transform: lowercase;
      font-variant: small-caps;
      background-color: transparent;
      font-size: 3em;
      font-weight: 600;
      cursor: pointer;
    }
    button:hover {
      color: #fff;
      border-color: #b5084d;
      background-color: #f62c7d;
    }
    .active {
      opacity: 0;
      transition: all 1s;
    }
  </style>
 </head>
  <body>
    <button id="goButton">Turn it on</button>
    <script src="https://www.puck-js.com/puck.js"></script>
    <script src="PuckChime.js"></script>
    <script>
      // ... add event listeners
    </script>
  </body>
</html>

如果现在在浏览器中查看 index.html,您将看到 图 12.21 中所示的内容,但此时它还没有任何功能。

图 12.21. 目前,index.html 显示了一个非常大的按钮,但它没有任何功能。

该按钮(#goButton)最终将有一个点击事件处理程序,将启用门铃。您可能想知道为什么需要这个额外的点击步骤——为什么不在页面加载时直接激活门铃?出于隐私和权限的原因,在浏览器允许蓝牙配对之前,需要有一个明确的用户界面操作——如果您在没有用户输入的情况下尝试这样做,您将得到一个错误。

index.html 文件包含一段 CSS。其中一些样式是为了格式化大的“开启”按钮,但也有样式在门铃响起时使屏幕闪烁,并在门铃激活后淡出“开启”按钮(.active 类)。

您现在可以在以下列表中的 <script> 内容中填写代码。此代码假设存在一个 PuckChime 类,其创建我们将在下一部分介绍。

列表 12.9. “开启”按钮的点击处理程序
window.addEventListener('load', () => {
  const onButton = window.document.getElementById('goButton');
  onButton.addEventListener('click', function () {
    var chime = new PuckChime();
    chime.init().then(() => {                  *1*
      onButton.classList.add('active');        *2*
    });
  });
});
  • 1 一旦 chime.init() Promise 解决,您就会知道 BLE Puck.js 铃声已成功设置。

  • 2 您不再需要按钮,因此可以添加活动类使其淡出。

编写 PuckChime

与之前在 LED 控制网页中的点击事件处理器中发送单行命令给 Puck.js 相比,BLE 门铃的逻辑更为复杂。

将代码封装在新的文件 app/PuckChime.js 中的 PuckChime 类中是有意义的。PuckChime 的 API 接口在下面的列表中显示。你将在接下来的几个步骤中完成它。

列表 12.10. PuckChime 的 API 接口
class PuckChime {
  constructor () {

  }

  init () {
    /**
     * - establish connection to Puck.js
     * - reset Puck.js
     * - send command to Puck.js: observe builtin button for presses
     * - invoke `chime()` as aural/visual confirm when successful
     */
  }

  connect () {
    /**
     * - connect to Puck.js with BLE
     * - add an event handler for Puck.js `data` events
     */
  }

  send (cmd) {
    // format and send `cmd` to Puck.js
  }

  reset () {
    // send a `reset` command to Puck.js and wait 1.5 seconds for it to "take"
  }

  watchButton () {
    /**
     * send a command to Puck.js to watch its button for presses
     * and log (over Bluetooth) a string when button is pressed
     */
  }

  parseData (data) {
    /**
     * `data` event handler for incoming data chunks from Puck.js
     * - append `data` to buffer
     * - parse buffer into lines (split on `\n`)
     * - send each line (`cmd`) to `parseCommand()`
     */
  }

  parseCommand (cmd) {
    // if `cmd` is `CHIME`, invoke `chime()`
  }

  chime () {
    // play a chime sound and make visual chime
  }
}

让我们看看构造函数和 init() 方法如何在下一个列表中实现。

列表 12.11. PuckChime:构造函数和初始化方法
constructor () {
  this.connection = null;                              *1*
  this.dataBuffer = '';                                *2*
  this.sound = new Sound('/chime.mp3');                *3*
}

init () {
  return this.connect().then(() => {
    return new Promise((resolve, reject) => {
      this.reset()
      .then(this.watchButton.bind(this))
      .then(() => {
        this.chime();                                  *4*
        resolve();
      });
    });
  });
}
  • 1 保持与 Puck.js 的连接;它最初为 null,直到连接

  • 2 用于存储和解析从 Puck.js 收到的数据的缓冲区

  • 3 Sound 是一个方便的类,用于使用 Web Audio API 加载和播放声音。

  • 4 在 init 的工作完成后,调用 chime() 一次

构造函数通过实例化一个 Sound 对象来准备声音—chime.mp3。如果 Sound 看起来像是一个突然出现的神奇类,而 chime.mp3 是一个神秘来源的文件,那么你是对的!请稍等;稍后会有更多关于它们的细节。

init 方法返回一个 Promise,当以下步骤完成时解析:

  1. 与 Puck.js 建立连接

  2. Puck.js 已重置

  3. 指示 Puck.js 监视其按钮的按下

只有当所有这些事情都完成时,init() 返回的 Promise 才会解析;chime() 也作为确认被调用(门铃在准备就绪时会响一次)。

继续前进,connect()send(cmd)reset()watchButton() 方法与 Puck.js 通信,每个方法都返回一个 Promise。这些方法依赖于 Espruino 提供的客户端 puck.js 通信代码:

  • connect()—连接到 Puck.js 并为 Puck.js 的 data 事件添加一个事件处理器 (parseData)

  • send(cmd)—适当地格式化字符串 cmd 并将其发送到 Puck.js,通过将回调导向的 connection.write() 方法包装在 Promise 中以保持一致性

  • reset()—向 Puck.js 发送重置命令,并在解析它返回的 Promise 之前等待 1.5 秒,以便 Puck.js 再次准备好

  • watchButton()—向 Puck.js 发送更复杂的命令以设置对其内置按钮的监视

这些方法在下面的列表中进行了详细说明。

列表 12.12. 与 Puck.js 通信的方法
connect () {
  return new Promise ((resolve, reject) => {
    Puck.connect(connection => {
      this.connection = connection;
      this.connection.on('data', this.parseData.bind(this));
      resolve(this.connection);
    });
  });
}

send (cmd) {
  cmd = `\x10${cmd}\n`;
  return new Promise ((resolve, reject) => {
    this.connection.write(cmd, () => { resolve(cmd); });
  });
}

reset () {
  return new Promise((resolve, reject) => {
    this.send('reset()').then(() => { setTimeout(resolve, 1500); });
  });
}

watchButton () {
  const cb = "function() { Bluetooth.println('CHIME'); LED1.set();
   setTimeout(() => LED1.reset(), 250);}";
  const opts = "{repeat:true,debounce:250,edge:'rising'}";
  const cmd = `setWatch(${cb},BTN,${opts});`;
  return this.send(cmd);
}

让我们仔细看看 watchButton() 发送的命令——格式化后相当复杂——并更好地了解发送和接收 Puck.js 数据和命令的情况。

正如你在 第 12.4.5 节 的网络控制 LED 示例中看到的,在从浏览器发送命令到 Puck.js 之前,需要将命令格式化为字符串。在更简单的 LED 实验中,这是通过调用 Puck.write()(见 图 12.18)来完成的。

在这个更复杂的情况下,数据双向传输时,你将建立一个持久连接(在 connect() 方法中)。一旦建立连接,就使用 connection.write() 向 Puck.js 发送命令。通过 connection 上的发出 data 事件从 Puck.js 接收数据(图 12.22),这些事件由注册的 data 事件处理器 parseData() 处理。我们稍后会讨论这一点。

图 12.22. 在门铃示例中的双向通信中,使用 puck.js 客户端库在浏览器和 Puck.js 之间建立持久连接。可以通过连接向 Puck.js 发送命令,并且 Puck.js 通过蓝牙输出的任何输出都会在连接上触发一个 data 事件。

PuckChime 对象向 Puck.js 发送两个命令:在初始化阶段发送一个 reset() 命令以清除 Puck.js 中的任何蜘蛛网或异常,然后在 watchButton() 中发送一个更复杂的命令。在该方法中构建的命令,去除了所有的字符串性,并忽略了换行符约束,在以下列表中展开。它使用了 Espruino 全局 setWatch(<callback>, <pin>, [<options>]) 函数。

列表 12.13. Puck.js 命令,展开
setWatch(
  function () {                        *1*
    Bluetooth.println('CHIME');        *2*
    LED1.set();                        *3*
    setTimeout(() => {                 *4*
      LED1.reset();
    }, 250);
  },
  BTN,                                 *5*
  {
    repeat: true,                      *6*
    debounce: 250,                     *7*
    edge: 'rising'                     *8*
  }
);
  • 1 setWatch() 的第一个参数:一个回调函数

  • 2 通过蓝牙记录字符串 CHIME;这将触发一个 data 事件

  • 3 打开红色 LED

  • 4 250 毫秒后再次关闭红色 LED

  • 5 要监视的引脚

  • 6 重复监视这个引脚

  • 7 对引脚(按钮)进行去抖动,以 250 毫秒为间隔,避免每次按下时触发多次或干扰之前的按下

  • 8 触发上升沿——从 LOW 到 HIGH 的转换

在 Puck.js 上注册的按钮按下回调通过蓝牙输出一个字符串,并且还会短暂点亮板载的红色 LED 作为视觉反馈。

当在蓝牙上输出某些内容时,Puck.js 会随时发出数据。这发生在按钮监视回调中执行 Bluetooth.println('CHIME') 时,但并非来自 Puck.js 的所有内容都将是一个 CHIME 命令。例如,在建立连接后,会自动生成几行调试和版本输出。这些行与你的逻辑无关,因此 parseCommand(cmd) 确保你有一个实际的 CHIME 字符串匹配。

在将命令输入到 parseCommand() 之前,你必须从其他传入的数据中解析出“命令”。数据以块的形式传入,因此 parseData() 处理器必须保持一个简单的缓冲区,并将传入的字符串数据拆分成行——由 \n(换行符)字符分隔。这些行分别传递给 parseCommand(),以查看它们是否确实代表一个有效的命令——CHIME 是你唯一的有效命令。请参见以下列表。

列表 12.14. 从 Puck.js 解析数据
parseData (data) {
  this.dataBuffer += data;
  var cmdEndIndex = this.dataBuffer.indexOf('\n');
  while (~cmdEndIndex) {                                                   *1*
    var cmd = this.dataBuffer.substr(0, cmdEndIndex).replace(/\W/g, '');   *2*
    this.parseCommand(cmd);                                                *3*
    this.dataBuffer = this.dataBuffer.substr(cmdEndIndex + 1);             *4*
    cmdEndIndex = this.dataBuffer.indexOf('\n');
  }
}

parseCommand (cmd) {
  if (cmd.match('CHIME')) {
    this.chime();
  }
}
  • 1 -1 是唯一一个在位运算非(~)操作符面前产生 0(false)的值。

  • 2 移除任何非字母数字字符,以防控制字符被挤入

  • 3 将此行传递给 parseCommand() 以查看它是否意味着什么

  • 4 从数据缓冲区的开头剪切当前命令并查看是否有更多行

PuckChime 类中的最后一个方法是 chime() 本身。在构造函数中实例化的 Sound 对象被播放(使用 play()),并且添加了一个类 — .ding — 到 body 元素上,然后在 500 毫秒后移除,如下所示。.ding 类通过暂时改变整个页面的背景颜色,在浏览器中创建一个视觉门铃声。

列表 12.15。门铃本身
chime () {
  window.document.body.classList.add('ding');
  this.sound.play();
  window.setTimeout(() => {
    window.document.body.classList.remove('ding');
  }, 500);
}
Web Audio 和 Sound 类

Sound 是一个 JavaScript 类,它封装了在构造函数中传递给它的 url 处加载和播放声音文件。它使用 Web Audio API。其源代码在 列表 12.16 中重现;您可以将它放在 PuckChime.js 文件的顶部。或者,您可以在本书的 GitHub 仓库中找到整个 PuckChime.js 源代码,包括 Sound 类。

在托管 PuckChime.js 源代码的同一目录中,您还可以找到 chime.mp3 声音文件——或者您也可以使用自己的声音文件(如果您提供了不同的文件名,别忘了更新 PuckChime 构造函数中的 Sound 实例化)。

列表 12.16。Sound
class Sound {
  constructor (url) {
    // Context in which to do anything related to audio.
    // It is prefixed with `webkit` in some browsers
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    this.url = url;
    this.context = new AudioContext();
    this.buffer = null;
  }
  /**
   * Using XMLHttpRequest, Load the audio file at this.url
   * decode and store it in this.buffer
   * @return Promise resolving to this.buffer
   */
  load () {
    return new Promise((resolve, reject) => {
      if (this.buffer) { resolve(this.buffer); }
      var request = new window.XMLHttpRequest();
      request.open('GET', this.url, true);
      request.responseType = 'arraybuffer';
      request.onload = () => {
        this.context.decodeAudioData(request.response, soundBuffer => {
          this.buffer = soundBuffer;
          resolve(this.buffer);
        });
      };
      request.send();
    });
  }
  /**
   * Load an AudioBuffer, then create an AudioBufferSourceNode to play it.
   * Connect the AudioBufferSourceNode to the destination (output)
   */
  play () {
    this.load().then(buffer => {
      // Create a new AudioBufferSourceNode which can play sound from
      // a buffer (AudioBuffer object)
      const source = this.context.createBufferSource();
      source.buffer = buffer;
      // Connect the AudioBufferSourceNode to the destination
      // (e.g. your laptop's speakers)
      source.connect(this.context.destination);
      source.start(0);
    });
  }
}

Web Audio API 功能强大,相应地也有些复杂。MDN 的 Web Audio API 文档非常全面:developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API

尝试门铃

概括来说,Puck.js 门铃项目应包含以下文件:

  • index.js — 一个简单的网络服务器。

  • app/index.html — 包含大“开启”按钮的样式和标记,以及按钮的点击事件处理程序以初始化门铃(PuckChime 对象)。它还包括与 Puck.js 通信的 Puck.js 客户端 JavaScript 库,以及 PuckChime.js。

  • app/PuckChime.js — 包含 SoundPuckChime 类。

  • app/chime.mp3(或您选择的另一个声音文件)—按门铃时播放的声音。

启动网络服务器:

$ node index.js

打开您的浏览器到 localhost:3000。同时打开浏览器中的 Web 检查器控制台,查看任何记录的消息或错误。当您点击“开启”按钮时,应该会弹出一个配对请求(图 12.23)。

图 12.23。点击“开启”按钮将提示与 Puck.js 配对。

配对后,初始化过程需要几秒钟——您将在控制台中看到一些日志。如果您听到门铃声音播放并且屏幕短暂地闪烁粉红色,那么您就会知道它已经完成并且成功(图 12.24)。

图 12.24。当按下 Puck.js 的按钮时,铃声响起,屏幕闪烁着亮粉色——甚至可以说它非常明亮——短暂地。

12.5。拓展 JavaScript 和硬件的边界

这个 BLE 门铃实验很好地说明了在物理设备和 JavaScript 世界中的前沿在哪里。Puck.js 运行 Espruino 解释器,它几乎但并不完全是完整的 JavaScript——有优化措施使其能够在这样的受限硬件上执行 JavaScript。Web Bluetooth 在一些浏览器中工作,但也有一些不足之处。在撰写本文时,你必须在每次页面刷新时重新配对,这真是个麻烦。你可能会发现它在调试困难的情况下间歇性地不可靠。

但 Puck.js 的存在,以及 Web Bluetooth 在某些浏览器中的实现,都是非常引人注目的,而且这比 12 或 18 个月前有了巨大的飞跃。Johnny-Five(在 2017 年迎来了五周年纪念日)持续的热度和 I/O 插件增长表明,JavaScript 开发者对这些领域的兴趣依然强烈。这种对将 JavaScript 和其他网络技术与其他物理设备融合的兴趣也体现在物联网硬件的 Node.js 兼容云管理选项激增上,特别是在能够运行 Linux 的 SBC 设备类别中。

建立电子黑客能力并不意味着要深入而僵化地树立一个标志。JavaScript 不必是你的锤子,你的教条式单一方法。相反,它可以作为一个探索的范例,一个熟悉的眼镜,通过它来审视不熟悉的事物。JavaScript 确实可以带你到达你需要去的地方。但对于其他情况,一个开放和好奇的心态是无价的。

因此,我鼓励你不仅通过 JavaScript 继续学习,而且在你越来越熟悉之后,也要进一步探索。了解更多关于不同串行协议的工作原理,深入了解位操作,学习编写基于 C 的固件。尽管说 C 语言很简单可能会显得有些轻率,但它确实是可以接触的:许多人发现 Arduino 编程语言(非常粗略地说,基本上是 C++ 加上一些额外的硬件控制功能)是通往 C++ 熟练程度的有帮助的入门途径。

尝试。构建。提问。了解技术。故意破坏。知道即使这是你第一次构建电路和与物理 I/O 一起工作,你也能想出如何构建你梦想中的事物。快乐黑客!

摘要

  • 近年来,针对管理部署物联网应用的云服务激增,许多服务针对企业用户。许多这些针对具备 Linux 功能的单板计算机(SBC)的服务提供对 Node.js 的支持。

  • 容器化是一种将应用程序及其依赖项从环境变化的变幻莫测中隔离出来的方法,它是物联网应用部署的流行选择。

  • Web Bluetooth API 尚未正式纳入官方网络标准轨道,但已在几个浏览器中存在实现。其中一些功能尚未完善,而且安全和权限模型仍然存在争议。

  • Espruino 提供了一个小的客户端 JavaScript 库,该库使用 Web Bluetooth 与 Puck.js 进行通信。

  • 通用传感器 API 及其进一步的基于此的传感器 API,例如环境光传感器 API,正处于标准定义的早期阶段,但它们正在积极开发中。

  • 要参与物理网,一个 BLE 设备可以使用开放的 Eddystone 协议来广播一个 URL。附近拥有兼容发现软件的用户可以浏览并与之交互这些信标。

posted @ 2025-11-15 13:05  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报