返回 登录
7

基于 JavaScript 开发 IoT 入门指南

原文:JavaScript for Microcontrollers and IoT
译者:蒋春华
审校/责编:屠敏,关注物联网、移动开发领域,寻求报道或投稿请发邮件tumin@csdn.net


如果你想入门学习如何在一个在小型 IoT 设备(例如微控制器平台)上运行 JavaScript,那么这篇文章就是为你量身定制的。

JavaScript 不仅是 Web 时代的通用语言,现在还延伸到了令人难以置信的其它地方。它是一个对初学者友好的语言(初始版本在 10 天内构建完成),现在已经无处不在:服务器、工作站、数据库,甚至还包括物联网设备。不过,由于某些微控制器固有的低端特性,使得初学者使用 JavaScript 变得有些困难。在这篇文章中,我们将会先走马观花般地看看在小型设备上运行 JavaSript 有哪些选择,然后将会挑选其中一个并让它运行在某个 JavaScript 引擎上。现在,我们一起来探索 JavaScript 的旅行吧!

简介

我始终坚信,某些事即使有实现的可能,也并不意味着它就应当被实现。我认为这也同样适用于 JavaScript。抛开 JavaScript 自身的缺陷,我认为它是一个相当合适的编程语言。不过,这不意味着它适用于所有的地方。因此,我先事先阐明我的观点:为自己的工作挑选编程语言不仅仅是一个纯粹的选择或尝试。编程语言仅仅是一个工具,工具在被设计时就有特定的使用场景和应用程序。例如,你也可以拿一个锤子来订螺丝钉(而不是钉子),其结果可能会不尽如意,甚至可能干了坏事儿。这同样适用于编程语言。JavaScript 是一个动态的、允许用户在各个版本间快速迭代的、可以完成小型自动化任务和脚本的语言(你可能在其它网站上经常看到这段话),也是一个初学者和熟悉 Java 语法的人均容易上手的语言。请先记住这个观点,我们会在下一节中对其进行阐释,现在只需要记住:将编程语言当做一个工具,并“为合适的工作挑选合适的工具”。

如果你不熟悉微控制器,你可以把它理解成一个非常老的系统:RAM 容量很小,CPU 速度非常慢,“磁盘”容量也非常小。当然,微控制器的的尺寸也各不相同,大一些的微控制器可以跑更大的系统,不过它们的目标基本是相同的:微控制器通常被设计用于提供专用的编程功能,且保持成本低廉、尺寸小巧。更大的系统,例如你的智能手机中所使用的系统,功能更加强大,但是却更加昂贵。当我们在 IoT 热潮中看到有越来越多的微控制器时请不要那么吃惊,因为它们一方面比大型系统更加便宜,另一方面由于越来越成熟的技术使其也变得更加强大和复杂。因此,现在某些价格低廉的产品(例如千篇一律的物联网水壶)也具有强大的编程能力。

依然对微控制器没什么概念?我们现在来直观感受一下。在本文中,我们是使用的微控制器是一个 32 位的 120MHz 的 CPU,其 RAM 是 128KB,Flash 存储空间是 1MB。更让人哭笑不得的是,这已经是相当强大的微控制器了,有许多 8 位的微控制器,它们只有 0.375KB 的 ROM,16 字节的 RAM!这些微控制器主要用于超低功耗、超低成本的应用中。我们在本文中需要做的事(运行 JavaScript 引擎)需要更多强大的功能。

需要说明的是,时至今日,微控制器的定义已经有些模糊。在上世纪九十年代,微控制器通常指的是集成了少量 RAM 和 ROM 的小型嵌入式 CPU。如今,大家谈论的都是片上系统(SoC)。片上系统是用于描述集成了 CPU、RAM、ROM 甚至一些外设(例如 GPS 接收器)的高度耦合的硬件设计的另一种方式。我们在本文中将交替使用这两种术语。



为什么选择 JavaScript?

当提到嵌入式开发时,你通常会想到如何在硬件层面下手。例如,如果你需要读取某种传感器,你会将传感器的值用某个公式进行转换,然后将其结果显示在某个地方。你会选择带有模数转换器(ADC)且能驱动显示设备的微控制器。对于所有满足你功能的微控制器,越小价格越便宜。当然,越小的控制器,你在编程的时候受到的限制就越多。对于某些非常小的设备,你可能需要使用汇编语言;如果你随后打算切换到其它平台上去,则会加大程序移植的难度。通常,你需要挑选一个满足你的需求的较小的、但不是最小的平台。你应当考虑到将来的扩展性或者新的需求,因此通常需要在硬件层预留一些空间。

当你使用稍大的微控制器时,一些大门正在为你开启。第一道大门通常是 C 语言。使用 C 代替汇编,主要有两点好处:可移植性和易用性(包括可读性和易于维护性)。C 语言被设计为”可移植的汇编语言“,是大多数小型微控制器(除极小的微控制器之外)的理想编程语言。当大小和功能继续增加的时候,更多大门开始打开:C++、操作系统(FreeRTOS, Nuttx 等),甚至包括以太网或者WiFi连接。伴随着这些选择,可以使用的编程语言越来越多。某些编程语言,由于它们自身你的语法特性,不能被提前编译成机器代码。JavaScript 就是其中之一。那么,我们首先需要问自己的是”JavaScript 引擎能运行在我们的平台上吗?“。答案是:能!今后将在随后看到。

与汇编、C、C++ 相比,更上层语言最大的优势在于它们的安全性。在 macOS 上面,一点细微(off-by-one)的错误通常都会导致段错误;在微控制器上面,它甚至会造成整个系统级崩溃。在微控制器上,程序的调试也非常受限,所以任何可以有助于编写安全代码的方法都是非常有帮助的。更上层的语言在这一方面的好处非常明显。

再来看看在微控制器中以这种方式运行代码的另一个好处。通常,微控制器会从 ROM 中读取代码,而 ROM 中的数据是通过特殊的方法写入的,因此改变它的值非常不方便。这限制了代码更新或者程序不同版本之间的快速迭代。解释器可以读取 RAM 或者 ROM 上的程序,使无需烧写 flash 就能更新代码成为可能。当然,这需要你自己权衡利弊:系统重启后 RAM 的数据不能恢复,因此程序需要每次重新加载;由于 RAM 大小受限,因此程序也必须非常小。上层语言富有表现力的特性在这方面具有很多的帮助:只需要几行简单的代码,就能表达复杂的行为和逻辑。

这些好处在大多数高级、解释型或实时编译的语言间均存在,但是 JavaScript 自身还有一些优势。首先,JavaScript 有大量的社区以及大量的库。虽然由于缺少系统级别的支持(主要是 Node.js 或者浏览器)大量的库不能工作在微控制器上,但是依然有一部分库是可以的,并且非常有用。由于语法的相似性,C 和 C++ 程序员可以很快速地学会 JavaScript,因此习惯嵌入式编程的开发者在阅读 JavaScript 代码库的时候只会遇到少量的麻烦。此外,回忆一下前面的简介,我们提到 JavaScript 被设计用于为通用自动化任务和快速重复任务编写小脚本。微控制器通常也用于这种场景中 —— 为自动化任务或数据上报任务编写小片段与硬件设备交互!

总结一下我们考虑使用 JavaScript 或者其它流行解释型语言为微控制器编程的主要原因:

  • 上层,安全:没有 off-by-one 崩溃;更容易处理错误、复杂的数据类型和类型转换;更具有表线性。
  • 快速迭代:可以从 RAM 中加载脚本,不需要每次测试时都烧写。可以远程更新。
  • 大量的社区和库(大多数需要调整后才能运行到嵌入式平台上):bundler 和 minifier 可用于确保代码最小化。
  • C、C++ 以及 Java 相似的语法。

缺点

当然,选择 JavaScript 也有一些缺点。首先,JavaScript 在设计时就未考虑运行在小型、内存受限的设备上。因此,语言本身的构建器会消耗大量内存。大多数 JavaScript 解释器都至少需要 150KB 的 ROM 和 32KB 的 RAM。如果你的设备的资源比这还小,那还是继续使用 C/C++ 吧,或者使用为小型系统而设计的脚本语言,例如 Lua

你可能还会发现,让 JavaScript 引擎保持在一个合理的尺寸会限制某些高级的语言特性,我们将在后续的例子中讨论它。

最后,即使使用了 JavaScript 或者其它解释语言,你也逃不掉 C 或者 C++。除非你使用的是一个你所挑选的 JavaScript 引擎所完全支持的平台,否则将平台与 JavaScript 集成在一起需要手工进行调节。

在下决定之前,需要仔细权衡优势和劣势。在这里,我们这样做是只为了好玩。那么,我们需要在微控制器上使用 JavaScript 吗?YES!看看它能带我们遨游到哪里吧!

选择

我们将简单看看在硬件方面和软件方面有哪些选择。对于硬件方面,我们主要看两个选择:近期非常流行的 ESP8266 以及 Particle Photon。这二者都相当强劲,且均支持 WiFi 连接。太酷了!而且它们还非常便宜:ESP8266 价格低至 7 美金(注:淘宝 18 元就能买到了!);Particle Photon 当前也只需要 19 美金。当然,如果你计划使用大量设备,价格甚至更低。

还有一些其它的选择,例如更新的 ESP32、32 位的 PIC、Espruino、使用 ARM CPU 的 ST 开发板,甚至包括 Arduino,例如基于 Intel Curie 的 Arduino 101。说明一下,大多数 Arduino 开发板都不够强大,无法运行任何 JavaScript 引擎,而 Arduino 101 是个例外。

还有另一个非常有意思的选择 —— 树莓派 Zero W,它比上面所提到的都强大很多,封装了一个 1GHz 的 ARM CPU,512 MB 的 RAM 和 WiFi 连接,且价格低至 10 美金。由于具有如此高的配置,树莓派 Zero 可以运行 Linux。因此,你可以使用 Node.js 或者其它强大的 JavaScript 引擎。我们主要关注不足以运行 Node 和 Linux 的更低端设备。

在这一系列文章中,我们挑选的是 Particle Paoton 和 ESP8266,因为它们非常易于使用。

在软件方面,我们主要看一看 JerryscriptEspruino。Jerryscript 是由三星为小型嵌入式设备而开发的 JavaScript 引擎;Espruino 是微控制器 Espruino 套件的一个 JavaScript 引擎。这两个引擎的主要目标都是在兼容 ECMAScript 5.1 的同时保持代码简洁,使用尽可能少的 RAM 和 ROM。还有一些其它的选择,例如 DuktapeV7 以及当前正在积极开发的 MuJS

在本文中,我们主要将精力集中在运行 JerryScript 的 Particle Photon 上面。今后的文章中,我们再看 ESP8266 和 Espruino。

开始动手!

Particle Photon



Particle Photon 是一个带有 WiFi 功能的 SoC,具有大量的 I/O 引脚,支持若干有线协议(I2C、CAN、SPI 和 USB),它将一个 120MHz 的 ARM Cortex-M3 微控制器与一个 Cypress WiFi 芯片结合在一起,携带了 1MB 的 flash 存储空间和 128 KB 的 RAM。它支持低功耗模式,是需要使用 WiFi 连接的嵌入式理想产品。

收到 Photon 后,我脑海中的第一个印象是它太精致了,包装非常棒,PCB 一看就是经过精心设计的。Photon 出厂已经烧写了默认的 Particle 的固件,我们可以直接使用 Particle 的工具与之交互,编程和配置都非常方便。因此,当你将 Photon 从盒子中拿出的那一刻起,你就可以直接将它连接到你的 PC 中,然后开始骇客开发了。衷心感谢 Particle 的小伙伴所做的优秀工作。



Particle 提供的软件套件也非常丰富,包括一个基于 web 的可支持远程烧写的编辑器、一个可安装在本地的 IDE 以及一个可以直接通过 USB 便可与 Photon 直接通信的命令行客户端。这使得开发起来非常简单。

工具套件使用 GCC 作为它的编译器,支持 C 和 C++。Particle 支持 Wiring API —— 由 Arduino 普及的一个 C++ API。Photon 中的所有硬件元素都可以通过这些 API 进行访问。一些上层组件,例如 WiFi 和 TCP/UDP 套接字,也可以通过它进行访问。换句话说,与低端设备不同的是,你不需要在运行你的代码前进行硬件的初始化和配置工作,只需要在正确的地方调用你的函数即可。

如果你对 Wiring API 不熟悉的话,我们简单瞟一眼 —— 用户必须提供两个基本的函数:setuploop

// Called once when the microcontroller boots
void setup() {
    // YOUR CODE
}
// Called periodically by the firmware
void loop() {
    // YOUR CODE
}

Particle 提供的基础固件会在完成设备的基本初始化后调用setup,因此你可以在此时运行用户代码(执行你的代码所需的初始化操作),但是某些组件可能还不可用(例如 WiFi)。这个函数只会被调用一次。

当调用函数setup后,固件会继续执行,周期性地调用函数looploop被两次调用的时间间隔是不固定的,但是是尽可能快的。底层固件在loop调用之间会执行一些系统级别的工作(检查缓冲、发送心跳报文、管理外设等)。Particle 在固件中包含了一个非常小的 OS,因此你可以专心做顶层实现。

需要注意的是,虽然使用的编译器是 GCC,但是有许多 C++ 的功能是不支持的,这是由于某些 C++ 构造器会影响二进制镜像的最终大小。特别地,异常全部被禁止了。你应该尽量少使用模板代码,尤其是一个模板需要实例化多个版本的代码。简单地使用 std::vector 这类功能是没有问题的。不过,由于向量的每个模板实例化都会引入大量的机器代码,所以如果股你的代码依赖于std::vector<char>std::vector<int>std::vector<long>的话,你的向量代码在编译时会有三个不同的版本。这对于大一些的系统来说没有影响,但是它将快速耗尽小型设备上面的 ROM 空间。因此在写 C++ 代码时请一定要考虑这些因素。

Photon 还有非常棒的一点是 WiFi 连接可以完全由固件来管理。将SYSTEM_MODE(AUTOMATIC);放在你的main.cpp(该文件就是setuploop定义之处)的开头处,系统会自动尝试连接到 WiFi 芯片所存储的网络。这段存储空间在重启后依然有效,所以如果你的设备掉电了,它依然会在供电恢复的时候自动重连。WiFi 证书既可以使用控制台设置,也可以使用 SoftAP 模式设置。在 SoftAP 模式时,Photon 会成为一个开放的 WiFi 访问点,你可以使用任何 WiFi 设备访问它。在这种模式下,有一个特殊的 API 可用于设置证书。Particle 还提供了一个 app,所以我们可以非常方便地设置证书。

Particle/Wiring API 包含了很多有用的调用,具体能实现哪些功能请查看文档

为了方便入门,我们创建了一个公共的 Docker 镜像,它里面已经预装了所有必要的东西,因此你可以立即测试你自己的代码。Docker 镜像也可用于将它所编译的固件烧写到 Photon 中。不过,如果要使用这个功能,你必须使用 Linux 主机,这是因为 Docker 镜像是直接与 Linux USB 接口通信将固件发送到 Photon 的。下面,我将向你展示如何在 Linux 主机上对其进行设置。如果你使用的是 Windows 或者 MacOS,可以使用虚拟机,并将 USB 端口移动到 Linux 客户端,然后你可以在该客户端运行 Docker。另外,你还可以选择另一条路,使用 Docker 编译固件,然后使用 Particle 命令行工具烧写固件。我在下面也会展示如何使用该方法。

Photon 开发入门

尽管我们会向你展示如何使用我们为本文所准备的 Docker 镜像,但是你也可以不使用 Docker,直接按照 Particle 的文档在你的 PC 中一步一步设置本地环境。

首先,请确保已安装了 Docker。然后,下载我们的镜像:

sudo docker pull sebadoom/jerryphoton

通过该镜像,你可以简单快速地编译本地文件。换句话说,当你需要编译代码时,不需要每次都重新编译镜像。这是通过 Docker 的卷(volume)实现的 —— 可以无缝地将本地主机目录挂载到容器内部。我们先验证一下这个功能。在一个本地目录中克隆我们的 Hello World 仓库:

git clone TODO

cd TODO

然后编译!

./compile.sh

这个脚本会使用 sudo 权限来运行 Docker。如果你的系统不需要使用 root 权限,可以编辑、修改该文件。

这看起来好像也没干啥,但是我们的这个简单的 Hello World 实际上是与 JerryScript 一起编译的!具体细节我们将会随后谈论,这里不过多涉及,因为还没有暴露任何 Particle 功能给 JerryScript。你可以在工程的dist目录找到编译生成的固件。

由于脚本会将参数--rm传递给 Docker,容器将会在运行命令后自动销毁,所以我们不需要时候手动清除容器。现在我们将演示如何烧写刚刚编译的固件。

使用 Docker 镜像烧写固件(Linux 主机)

如果你在 Linux 主机上运行我们的 Docker 镜像,则烧写刚才所编译的固件非常简单:

  1. 首先,你需要使用 USB 数据线将 Photon 连接到 PC 上。
  2. 让 Photon 进入设备固件升级(DFU)模式。DFU 模式是一个特殊的模式 —— 即使先前的固件被破坏,也可以使用该模式来升级固件。这应该归功于烧写到 Photon 中的 bootloader。要进入 DFU 模式,先按住SETUP按钮不放,然后按下并松开 RESET 按钮,当主 LED 开始闪烁黄色时再松开SETUP按钮。
  3. 使用下面的脚本运行 Docker 容器:
./compile-and-flash.sh

如果你看了该脚本的话,你可能会注意到,我们将访问 USB 设备的权限完全放给了容器。这是必要的,因为容器需要向 Photon 发送固件。几秒后,Photon 将会重启,你看到蓝色 LED 等开始闪烁。这就是我们的 Hello World 在运行!

在主机中使用 CLI 烧写(任何主机)

如果你更喜欢手动烧写固件,则可以使用 Particle 的命令行工具来完成。

1.安装particle-cliparticle命令是一个 Node.js 应用,因此必须现在你的系统中安装 Node:

npm install -g particle-cli

2.编译固件:

./compile.sh

3.让 Photon 进入设备固件升级(DFU)模式。DFU 模式是一个特殊的模式 —— 即使先前的固件被破坏,也可以使用该模式来升级固件。这应该归功于烧写到 Photon 中的 bootloader。要进入 DFU 模式,先按住SETUP按钮不放,然后按下并松开RESET按钮,当主 LED 开始闪烁黄色时再松开SETUP按钮。

4.运行下面的命令:

particle flash --usb dist/firmware.bin

命令particle flash也可用于远程(通过 WiFi 连接到云)烧写设备。这需要 Photon 运行在非 DFU 模式。如果你想了解该功能的话,需要使用命令particle loginparticle setup将设备关联到云端。

我们的 Hello World 现在运行在 Photon 上面了。使用我们的 Docker 镜像的好处是,我们可以确保 JerryScript 也被一起编译了。

现在我们开始讨论 JerryScript。

JerryScript

既然已经将 Photon 开发环境搭建好并让其运行起来了,现在是时候深入了解 JerryScript 了。我们假设你对解释器不熟悉,因此简单介绍下它的工作流程。首先,你需要调用初始化函数来设置解释器,这个函数会创建一个新的解释器实例。然后,你可以调用另一个的函数解释并执行脚本,或者你可以先调用并解释一个函数(会让脚本准备就绪),然后调用另一个函数运行它。当你不需要解释器后,调用去初始化/销毁函数。到现在为止,一直都还 OK,但是当解释器或者本地环境需要与对方交互时,这就变得有些麻烦了,而要使用脚本,这是绕不开点坎。例如,在本文中,我们已经将某些 Particle API 集成到了我们的 JavaScript 环境中,因此你可以开箱即用!

JerryScript 自己的编译系统与 Particle 的编译系统是相互独立的。幸运的是,JerryScript 中已经包含了所有需要集成到两个编译系统中的所有文件。事实上,当你在前面运行 Dcoker 镜像编译脚本的时候,你已经使用了这个集成环境了。如果你对细节感兴趣,可以看看 JerryScript/Particle 中的 README 文件

正如我们在前面所提到的,JavaScript 设计初衷并不是用于内存受限的系统,因此某些构建器会使用大量内存。幸运的是,JerryScript 允许我们很方便地关闭 JavaScript 的部分功能,以减小代码尺寸和内存占用量。为了实现该功能,JerryScript 定义了 profile。你可以换在主 JerryScript 目录的子目录 jerry-code/profiles 中找到这些 profile。在运行某些测试代码的时候,我发现大量的 ECMAScript 5.1 功能都可以在 Photon 中被使能。在我们的 Docker 镜像中,我们已经设置了一个默认的 JerryScript profile,禁止了如下功能:

  • 数据(Date)对象
  • 所有的 ECMAScript 2015 功能
  • 数学(Math)对象
  • 正则表示式
  • Unicode 转换

可能在进行某些修改后也可以使能这些功能,不过我们没有做这样的测试。事实上,我们发现使能其它所有单一功能都会导致代码超出 Photon ROM 的限制。1MB 的 flash 存储空间是与固件共享的,所以解释器可用的空间远小于 1MB。尽管如此,功能已经非常健全了。

如果你想要切换为其它 profile 文件,可以看看在示例目录下面的文件 Makefile.particle,它的第 47 行就是用来设置 profile 的。

在进入我们的例子之前,让我们先看看 JerryScript API。

运行一个脚本

下面这个简单的例子演示了如何使用 JerryScript 运行一个脚本:

/* Initialize the global JerryScript instance */
jerry_init(JERRY_INIT_EMPTY);
/* Run a script, passed in as a char array (JavaScript code) */
jerry_value_t result = jerry_eval((const jerry_char_t*) script, scriptlen, false);    
/* Check the return value, which is a JavaScript value */
if(jerry_value_has_error_flag(result)) {
    /* Handle error */
} else {
    /* Do something with the value here */
}
/* Release the result, this is necessary to avoid leaks */
jerry_release_value(result);
/* Free all JerryScript memory */
jerry_cleanup();

为了尽可能地增加可移植性,JerryScript 使用 C 语言完成(没有 C++ 功能)。如果你熟悉 C 编程,你可能已看出这些 API 的古怪之处了:jerry_init 没有返回指向新构建的实例的指针,也没有任何函数在参数中获取该实例。相反,JerryScript 开发者选择了使用一个全局实例。不幸的是,这有一些缺陷。尤其是,当你需要在同一个线程中运行多个不同的 JerryScript 实例时,需要在调用 JerryScript 函数之前在它们之间进行切换。我个人认为这是一个相当笨的办法。与其它嵌入式 JavaScript 引擎相比,我讲它当做一个缺陷。当只需要一个 JerryScript 实例时,传递给函数的参数少了一个。

C/JAVASCRIPT 交互

要使嵌入式引擎变得有意义,我们需要想办法使其与系统进行交互。大多数嵌入式引擎都提供了在内部调用 C 代码的方法,以及在 C 中调用部分脚本的方法。

如果要在 JavaScript 中调用 C 函数,则必须在 JavaScript 函数中关联 C 函数:

static void my_c_function() {

    printf("Hello!\n");

}

static jerry_value_t 

my_c_function_wrapper(const jerry_value_t func,

                      const jerry_value_t thiz,

                      const jerry_value_t *args,

                      const jerry_length_t argscount) {

    my_c_function();

    return jerry_create_undefined();

}

void setup() {

    jerry_init(JERRY_INIT_EMPTY);

    // Create a JavaScript function from a C function wrapper

    jerry_value_t func = jerry_create_external_function(my_c_function_wrapper);

    // Create a JavaScript string to use as method name

    jerry_value_t prop = jerry_create_string((const jerry_char_t*)"myCFunction");

    // Get JerryScript's global object (what you would call window in a browser)

    jerry_value_t global = jerry_get_global_object();

    // Bind the 'func' function inside the 'global' object to the 'prop' name

    jerry_set_property(global, prop, func);

    // Release all temporaries

    jerry_release_value(global);

    jerry_release_value(prop);

    jerry_release_value(func);

    // (...)

}

当这段代码执行后,由 JerryScript 执行的脚本可以调用 myCFunction(),然后 my_c_function 会被调用。你可能已经注意到,这段代码中有很多 jerry_release_value。这是由于 JerryScript 使用了一个引用计数系统。无论什么时候,当创建一个对象后,或者从一个函数返回对象后,它们的引用计数都会增加。当再将这些对象传回给 JerryScript 后(例如调用 jerry_set_property),它们的引用计数会再次增加。对象是由 JerryScript 负责管理的,当我们不再需要的时候,我们必须调用 jerry_release_value 来减小其引用计数。如果不这样做将导致内存泄露。这是在 C 编程中非常典型的一种做法。如果 JerryScript API 是封装在 C++ 中的话,我们可以创建一个类似智能指针(smart-pointer-like)的对象来方便地处理引用计数。不过,JerryScript 是由 C 语言编写的(考虑到它的可移植性),所以你必须按照我们上面的那种方法来手工处理引用计数。想必用 C 写过代码的人都知道这种非常常规的做法,就像 malloc 和 free 需要配对调用一样。

在 C 中调用 JavaScript 代码就简单多了,其中最简单的方法是按照展示的那样对一个字符串进行求值即可。

/* Run a script, passed in as a char array (JavaScript code) */

jerry_value_t result = jerry_eval((const jerry_char_t*) script, scriptlen, false);    

/* Check the return value, which is a JavaScript value */

if(jerry_value_has_error_flag(result)) {

    /* Handle error */

} else {

    /* Do something with the value here */

}

另一种做法是使用函数 jerry_call_function。通过该函数,我们可以按照类似 JavaScript 代码调用 C 代码的方法将所有的参数传递给 JavaScript 函数:

// This is how jerry_call_function looks

jerry_value_t

jerry_call_function (const jerry_value_t func_obj_val,

                     const jerry_value_t this_val,

                     const jerry_value_t args_p[],

                     jerry_size_t args_count);

// For instance, to call our myCFunction from before we could do:

void call_javascript_function(void) {

    jerry_value_t global = jerry_get_global_object();

    jerry_value_t prop = jerry_create_string((const jerry_char_t*)"myCFunction");

    jerry_value_t func = jerry_get_property(global, prop);

    jerry_value_t undefined = jerry_create_undefined();

    jerry_value_t result = jerry_call_function(func, undefined, NULL, 0);

    // Do something with the result

    jerry_release_value(result);

    jerry_release_value(undefined);

    jerry_release_value(func);

    jerry_release_value(prop);

    jerry_release_value(global);

}

理解这些知识后,我们就知道如何使用 JerryScript 编写一些游泳的与 Photon API 交互的函数了。事实上,我们目前编写的可交互的函数只包括:GPIO 引脚、日志、等待、TCP 套接字、定时器和一些系统调用(例如进入DFU 模式)。至于这些函数是如何实现的,请查看在例子中所包含的 jerryphoton mini-lib。

举例

至此,我们已经对 Particle Photon 和 JerryScript 都有足够的认识了,然后可以看一些示例了。现在我们会尽量简单地进行一些说明,再后续文章中再深入学习。我们的示例基本都由三部分组成:

  • 设置 JerryScript,以 1Hz 的频率(每秒1次)运行一个开/关蓝色 LED 的简单脚本。
  • 使能 USB 日志(让 USB 端口进入串行模式)
  • 设置一个监听 TCP 套接字,可以通过 WiFi 将 JavaScript 脚本发送到 Photon 中。

第一部分已经在前面第一次烧写例子的过程中解释过了,开关蓝色 LED 是由 JavaScript 代码完成的!下面是代码:

count = 0;

photon.pin.mode(7, 'OUTPUT');

setInterval(function() {

   photon.pin(7, !photon.pin(7));

   photon.log.trace('Hello from JavaScript! ' + count.toString());

   ++count;

}, 1000);

这段代码位于文件main.cpp中第第 36 行。我们只是将一个 C 字符串传递给了 JerryScript!

你可能会注意到,这段代码有一个全局对象 photon,该对象是我们的 jerryphoto 库使用 JerryScript 暴露出的对象。该对象可用于与某些 Particle API 进行交互。你可能会认为setInterval是 JerryScript 提供的,但其实该函数是由 jerryphoton 库提供的。

下面是一些你可以使用的方法的简短说明列表:

  • photon.log.trace/info/warn/error(string): 通过 USB 输出字符串。格式化操作必须子啊 JavaScript 脚本内完成。
  • photon.delay(number): 调用 Particle 的函数delay(等待 N 毫秒)。
  • photon.pin.mode(string): 调用 Particle 的函数pinMode。mode 可以是 “OUTPUT”、”INPUT”、”INPUT_PULLUP”、”INPUT_PULLDOWN”。各个模式的含义请参考 Particle 文档。
  • photon.pin(number): 调用 Particle 的函数digitalRead并返回一个布尔值。
  • photon.pin(number, boolean): 调用 Particle 的函数digitalWrite并将 PIN 设置为所传递的布尔值。
  • photon.process(): 调用 Particle 的函数process。 需要在阻塞调用中保持 WiFi 连接的活动性。
  • photon.dfu(boolean): 调用函数System.dfu,并传递相应的参数。它会强制进入 DFU 模式。脚本会在此刻停止执行。
  • photon.TCPClient(): 创建一个 Particle TCPClient 对象,在封装在 JavaScript 对象中返回。返回对象接受 6 个调用:connectedconnectwriteavailablereadstop。该函数可以使用new或者常规方法进行调用,二者均会返回一个新的 TCPClient。Particle API 和该对象返回的 API 是有区别的,我们在下面会解释。
  • setInterval/setTimeout(function, number): 用于模仿浏览器的同名函数。不支持将使用字符串传递脚本,只接受一个函数和一个数字(毫秒)作为参数。函数clearInterval现在还不可用,不过支持该函数的大多数代码已经准备就绪了。

API photon.TCPClient 与 Particle 中的对应 API 有一些不同。

  • TCPClient.read(number): 读取数据并返回一个 JerryScript 字符串。如果没有数据,则返回空字符串。你也可以可选地指定需要读取的最大字节数。
  • TCPClient.write(string): 向套接字写入字符串。

其它的TCPClient方法与 Particle API 完全一样。

尽管只与 Particle API 做了非常有限的对比,但这足以说明我们的示例是如何工作的。我们看看示例源码录下的文件jerryphoto.cpp。jerryphoton 的共用 API 是非常简单的:

  • jerryphoton::js::instance(): 获取单个js实例,该实例对象是对 JerryScript 的封装。获取单个而非多个实例与 JerryScript 的设计方法有关:全局实例在调用之前必须先进行切换,因此我们也只获取了单个实例,以保持 API 的稳定性。
  • js.instantiated(): 检查示例是否被创建了(可以控制对内存的使用)。
  • js.eval(const chart* script, size_t size = 0)将 JerryScript 当做一个字符串。字符串既可以以0x0终止,也可以将字符串的长度传递给函数(可选)。如果该长度是 0,函数则认为字符串是以\0终止的。 该函数会计算字符串的值。当完成字符串处理后,解释器内部会记录所有产生的状态改变。换句话说,重复调用函数eval会保护状态改变。
  • js.loop(): 一个必须被周期性调用的函数。因此,你必须在你的工程的loop函数中调用该函数。该函数用于管理定时器。

在开始正式玩耍 JavaScript 脚本之前,我们再看看另外的一些内容。

使用 USB 输出日志



你可能已经注意到了,我们在前面多次提到过 USB 日志。当与 PC 相连时,Photon 会被识别为一个串行接口。事实上,Photon 可以作为各种 USB 设备,例如鼠标和键盘,不过我们这里只会使用它的 USB 串行接口功能。通过该接口,我们可以将 Photon 的数据发送到 PC 上面,以方便进行调试。在我们的例子中,所有的日志都会被推送到这个串行接口。

在使用 USB 数据线将 Photon 连接到 PC 后,你还需要一个串口终端应用程序。如果你使用的是 Linux 或者 MacOS(homebrew),则可以使用minicom

minicom -D /dev/ttyACM0 #macOS should be /dev/tty.usbserial or similar

在 Linux 上,你如果想以普通用户身份运行 minicom 访问串口设备,则需要将你自己的用户名添加到uucp组中。

对于 Windows,你可以使用 PuTTY 或者 Realterm

完成这步操作后,你的终端中就会显示日志。注意,但设备在 DFU 模式时,不会显示任何数据。

上传一个新脚本

我们的例子里面自带了一个无需烧写 flash 就能向设备上传 JavaScript 脚本的方法。你只需要连接到 Photon 的 IP 地址和端口 65550 并发送脚本即可,然后断开连接。断开连接后,脚本会立即被解释并执行。为了简化这个过程,我们提供了在例程里面提供了一个脚本upload-script.sh

./upload-script.sh <PHOTON-IP-ADDRESS>

该脚本使用nc(GNU Netcat)来向设备上传脚本,你也可以使用其它任何工具来打开套接字、发送数据。默认上传的脚本是test.js,不过你也可以编辑upload-script.sh来上传其它任何脚本文件。注意,脚本是在 RAM 中被解释的,所以你需要限制你所上传的脚本的大小。

脚本test.js会使用对象photon.TCPClient尝试连接到另一个端口为 3000 的主机中。你可以使用 nc(监听模式:nc -lk 3000)来与运行在 Photon 中的程序传输数据。

var s = photon.TCPClient();
s.connect('i5-4590-LIN', 3000);
s.write('Hello from JS!');
while(s.connected()) {
    photon.process();
    if(s.available()) {
        photon.log.trace(s.read());
    }
}
s.stop();
photon.log.trace('END');

如果你希望尝试一下的话,可以这样操作:

  • test.js中设置用于监听连接的 PC 的 IP 地址或者主机名
  • 在 PC 的防火墙中打开端口 3000
  • 运行nc
nc -lk 3000

上传脚本:

./upload-script.sh <PHOTON-IP-ADDRESS>

然后你就能在运行nc的控制台中看到Hello from JS!你可以在这里输入一些内容,然后敲下回车,然后会在运行Minicom的终端中看到对应的消息。

你可以使用这个示例让 Photon 通过 WiFi 与远程主机通信!特别说明,当前所有的通信都是明码传输的,没有任何加密保护,我们将在该系列后续文章中修复该问题。

Auth0 中的 Javascript

Auth0 内部,我们大量地使用了 JavaScript。在你的 JavaScript web app 中使用我们的认证和授权服务是非常简单的。下面是使用 ECMAScript 2015 功能和 Auth0.js库 的一个简单示例。借助它,你可以使用 TCPServer 并检查 JWT 的正确性来提供一个授权 API。

这个一个认证并授权用户访问 API 的客户端脚本。它也更新了 DOM,以显示一些用户数据。你可以将它作为 HTML 页面的一部分发送给 Photon。

const auth0 = new window.auth0.WebAuth({
    clientID: "YOUR-AUTH0-CLIENT-ID",
    domain: "YOUR-AUTH0-DOMAIN",
    scope: "openid email profile YOUR-ADDITIONAL-SCOPES",
    audience: "YOUR-API-AUDIENCES", // See https://auth0.com/docs/api-auth
    responseType: "token id_token",
    redirectUri: "http://localhost:9000" //YOUR-REDIRECT-URL
});
function logout() {
    localStorage.removeItem('id_token');
    localStorage.removeItem('access_token');
    window.location.href = "/";
}
function showProfileInfo(profile) {
    var btnLogin = document.getElementById('btn-login');
    var btnLogout = document.getElementById('btn-logout');
    var avatar = document.getElementById('avatar');
    document.getElementById('nickname').textContent = profile.nickname;
    btnLogin.style.display = "none";
    avatar.src = profile.picture;
    avatar.style.display = "block";
    btnLogout.style.display = "block";
}
function retrieveProfile() {
    var idToken = localStorage.getItem('id_token');
    if (idToken) {
        try {
            const profile = jwt_decode(idToken);
            showProfileInfo(profile);
        } catch (err) {
            alert('There was an error getting the profile: ' + err.message);
        }
    }
}
auth0.parseHash(window.location.hash, (err, result) => {
    if(err || !result) {
        // Handle error
        return;
    }
    // You can use the ID token to get user information in the frontend.
    localStorage.setItem('id_token', result.idToken);
    // You can use this token to interact with server-side APIs.
    localStorage.setItem('access_token', result.accessToken);
    retrieveProfile();
});
function afterLoad() {
    // buttons
    var btnLogin = document.getElementById('btn-login');
    var btnLogout = document.getElementById('btn-logout');
    btnLogin.addEventListener('click', function () {
        auth0.authorize();
    });
    btnLogout.addEventListener('click', function () {
        logout();
    });
    retrieveProfile();
}
window.addEventListener('load', afterLoad);

你可以在 这里 拿到完整的示例程序,并创建一个免费的账户。

在本文中,我们先介绍了在微控制器上进行 JavaScript 开发的状态,并选择了 JerryScript —— 一个由三星开发的简单 JavaScript 引擎。我们介绍了如何执行脚本,如何在 C 和 JavaScript 之间通信。尽管 JerryScript 有一些瑕疵(例如为多个实例使用同一个全局对象),但是它使用起来真的是太方便了。在今后的开发过程中,JerryScript 可能会允许将脚本离线编译成字节流,然后可以直接烧写到 ROM 中。Duktape、V7 以及其它 JavaScript 都支持该功能。

在硬件方面,我们发现 Particle Photon 使用起来真是简单得难以置信。文档和开发工具都非常完美。Particle 实现的 Wiring API 简洁、高效,我们在将这些函数引入到 JavaScript 解释器中时没有碰到任何麻烦。我们的示例可以远程上传并执行 JavaScript、使用引脚、TCP 客户端套接字和定时器。

我们也确实发现在编译某些 JerryScript 功能时的内存收到了一些限制。不过单单 1MB 的 flash,再除去 Particle 固件提供的所有功能所占据的空间,这已经非常不错了。1 MB 的空间足够我们来测试了,但是如果运行一个更复杂的应用的话是否还足够呢?下面我们将继续深入探讨!

2017年7月1日(星期六),「【在线峰会】一天掌握物联网全栈开发之道」将在 CSDN 学院召开,集结来自阿里、中国科学院、WRTnode、Ruff、ThoughtWorks、叶帆科技等一线物联网领域的技术专家,基于 JavaScript、Python,从整体架构、技术栈、应用开发平台到实战经验与安全方案,希望通过一天的时间,帮助开发者快速掌握物联网全栈开发之道,也为所有对物联网感兴趣的软件开发者、嵌入式开发人员以及希望从互联网技术背景转入物联网的开发人员提供一个良好的学习晋升平台。目前在线峰会火热报名中,详情点击注册参会

评论