Blog:
使用 Node.js 和 Express 框架通过网页访问 Colibri VF61 GPIO
这篇文章适合那些想要快速开发嵌入式 Linux 网络应用,控制硬件 GPIO,从而使得用户能够远程控制和监控系统。这里的应用开发使用了 Toradex 的 Colibri VF61 CoM (Computer on Module)、Iris 载板以及带有开关和 LED 的 PCB 板,如图 1 所示。文章的主要目的是向读者展示开发使用文件系统控制 GPIO 的 Node 代码、创建用户有好的界面、以及运行基于 Express 框架使用 AJAX 通客户端进行通信的网络服务器。为了理解客户端应用,建议先了解 HTML+CSS、jQuery 和 AJAX 知识。如果读者只想要大致了解 Node,则可以跳过这一部分,专注于服务器端应用。这里用的所有代码都可以从下面的 Github 上下载: nodeGPIOdemo, WebNodeGPIOdemo and WebNodeMultiGPIOdemo.
Node.js 是服务器端的运行环境,用 JavaScript 开发服务器端应用。因为 Linux 使用文件系统访问硬件功能,Node 带有处理文件系统操作的模块,所以可以开发应用访问系统的 GPIO。Node 也提供诸如 HTTP 的模块,可以用于开发网络服务器。但是 Express 框架更加容易使用,这也是我们选择它的理由。
JavaScript 是由客户端解释的开发语言,这意味着其由用户浏览器处理。结合 HTML 和 CSS,JavaScript 能够创建用户互动的网页,即交互设计。由于开发 JavaScript 比较耗时,有一个跨浏览器的库 jQuery 能够简化任务,甚至是处理和使用 AJAX 方法,其是被用于通服务器之间异步通信的技术,以及动态改变网页内容。
Colibri VF61 module模块具有多达 99 个引脚可以配置为 GPIO。因为我们配合使用 Iris 载板,有 8 个 GPIO 可以直接使用,我们将会使用其中的 6 个作演示,另外 18 个引脚也可以进一步配置为 GPIO。Iris 上的 13 引脚到 15 引脚接到低电平触发的开关,16 引脚到 18 引脚连接高电平触发的 LED。你可以参考 Colibri VF61技术手册 和 Iris 底板的 技术手册 配置更多的引脚为 GPIO。如果你对使用的引脚有疑问,可以查阅表格 1,更详细的说明可以参考上面手册的链接,以及 VF61 芯片和 Linux 所使用引脚名称的 网页说明。
表 1:Iris、SODIMM 接口、Vyrid 和 Kernel 引脚对应关系
Iris (x16) |
SODIMM (x1) |
Vybrid | Kernel | Iris (x16) |
SODIMM (x1) |
Vybrid | Kernel | |
13 | 98 | ptc1 | 46 | 16 | 101 | ptc2 | 47 | |
14 | 133 | ptd9 | 88 | 17 | 97 | ptc5 | 50 | |
15 | 103 | ptc3 | 48 | 18 | 85 | ptc8 | 53 |
配置环境
在这篇中,你可以从 这里下载所使用过的 Colibri_VF_LinuxConsoleImageV2.5 镜像。这里介绍了如何烧写模块镜像,或者你也可以使用 OpenEmbedded 编译自己的定制化镜像 。
然后将目标板连接至网络。如果你按照本文的步骤操作,但是无法将目标板连接至你所在的局域网,请参考 如何为嵌入式 Linux 应用开发配置网络。 或者使用 connman设置静态 IP 地址,然后你可以方便地通过 SSH 或者浏览器访问设备。
通过 SSH 访问你的开发板。
leonardo@leonardo:/tmp/leo$ ssh root@192.168.0.180 root@colibri-vf:~#
首先需要在你的开发板上安装 Node.js 。通过执行下面的命令进行安装,并验证是否正常工作:
root@colibri-vf:~# opkg update root@colibri-vf:~# opkg install nodejs root@colibri-vf:~# node > process.exit() root@colibri-vf:~#js
尽管你需要做的仅仅是在终端中执行你的代码,有一个工具 nodemon ,它可以监控源代码中的变化,并自动重启服务,免除在开发的时候,你需要按 ctrl+c,手动重启的麻烦。接下来还会需要 Express 框架,我们会先安装。我们也会安装body-parser 中间件,用于解析 JSON 字符串。下面的步骤安装 nodemon、Express 和 body-parser。在这之前需要安装 tar 和 npm (node package manager),这需要一定的时间。
root@colibri-vf:~# opkg install tar root@colibri-vf:~# curl -L https://www.npmjs.com/install.sh | sh root@colibri-vf:~# npm install express root@colibri-vf:~# npm install body-parser root@colibri-vf:~# npm install -g nodemon
GPIO 介绍以及在终端中操作
在登录开发板后,首先需配置使用的 GPIO。默认情况下,在复位以后所有的 GPIO 都禁用输出缓存以及内部的上拉/下拉电阻,引脚处于高阻态。
引脚需要被导出后才能通过 sysfs 操作。这可以通过向 /sys/class/gpio/export 文件写入对应的引脚编号。
你可以分别向 export 和 unexport 文件写入需要使用的或者从内核释放的引脚编号。需要注意的是,如果导出一个已经被使用的引脚将会遇到错误。gpiochipN 是 GPIO 控制器,每一个管理 32 个引脚。如果你想知道引脚对应哪一个控制器,可以通过下面的公式计算得到:
pin_number = gpioN[pin] + 32*N
例如,pin 88 使用的是 controler 2 的 pin 24,gpio2[24]。更简单的方式是查阅 模块手册的 GPIO Pad 和 GPIO Port 信息知道 VF61 引脚对应的关系。
我们导出需要使用的 GPIO,查看 /sys/class/gpio 目录
root@colibri-vf:~# echo 46 > /sys/class/gpio/export root@colibri-vf:~# ls /sys/class/gpio/ export gpio46 gpiochip128 gpiochip64 unexport gpiochip0 gpiochip32 gpiochip96
现在你可以看到由导出 GPIO 的文件夹,查看其中的内容:
root@colibri-vf:~# ls -l /sys/class/gpio/gpio46/ -rw-r--r-- 1 root root 4096 Jan 19 18:40 active_low lrwxrwxrwx 1 root root 0 Jan 19 18:40 device -> ../../../4004a000.gpio -rw-r--r-- 1 root root 4096 Jan 19 18:40 direction -rw-r--r-- 1 root root 4096 Jan 19 18:40 edge drwxr-xr-x 2 root root 0 Jan 19 18:40 power lrwxrwxrwx 1 root root 0 Jan 19 18:40 subsystem -> ../../../../../../../class/gpio -rw-r--r-- 1 root root 4096 Jan 19 18:39 uevent -rw-r--r-- 1 root root 4096 Jan 19 18:40 value
所有的操作都是通过读和写完成,只需要在 direction 文件写入 in 或者 out 就可以配置引脚。在系统复位后,引脚被导出并配置为输入。文件 value 用于读取或者设置引脚的状态值,即使在输出状态下读取也会返回 0/1 。在设置引脚时,任何 0 意外的数字都会被当做 1。
有些时候 GPIO 系统关于激活状态和系统读取的逻辑值有不同的定义。例如,所有文中用到的开关是低电平触,这表示如果从引脚上读取 0,那么按键是被按下,或者说触发。这在编程的时候可能会带来混淆,因此可以通过向 active_low 文件写入 1 来翻转读取的逻辑值。那么无论什么时候你读取或者写入 1 的时候,引脚的逻辑状态都是 0 。
部分引脚提供 edge 文件,它们可以配置产生中断。文件的状态可以是 none、rising、falling 或者 both,配置不同产生中断的 edge 。使用该功能的方法是轮询文件,直到有中断产生。现在我们不会在这里详细介绍该功能,更多关于如何使用中断的资料可以从 这里获取。
使用 Node.js 处理 GPIO
现在我们开始编写代码。我们假设你从 0 开始,在的用户目录或者项目目录中创建一个文件,命名为 server.js 。我在支持 Remote System Explorer 的电脑上使用 Eclipse IDE 编辑文件。你也可以选择其他的 IDE 或者是文本编辑器。打开文件,首先需要 export File System 模块。最好分配引脚编号以及常量,便于维护。
/* Modules */ var fs = require('fs'); /* VF61 GPIO pins */ const LED1 = '47', //you need to add the other pins from the table 1 /* Constants */ const HIGH = 1, LOW = 0;
接下来创建配置、读取和写 GPIO 的函数。由于这只是一个指导说明,不需要关心错误处理。如果你需要一个稳健的代码,还是强烈推荐增加该功能,甚至是创建模块以便重用代码。继续完成剩余的部分。
function cfGPIO(pin, direction){ /*---------- export pin if not exported and configure the pin direction --------*/ fs.access('/sys/class/gpio/gpio' + pin, fs.F_OK, function(err){ if(err){ console.log('exporting GPIO' + pin); fs.writeFileSync('/sys/class/gpio/export', pin); } console.log('Configuring GPIO' + pin + ' as ' + direction); fs.writeFileSync('/sys/class/gpio/gpio' + pin + '/direction', direction); }); } function rdGPIO(pin){ /*you need to read the value file and return it*/ } function wrGPIO(pin, value){ /*you need to write value to the file*/ }
rdGPIO 和 wrGPIO 比较简单:它们仅封转了读取和写入文件系统的函数,并将参数组合,所以主要部分的代码看起来会更加简洁。cfGPIO 函数首先检查引脚是否已经导出,如果有需要也可以由其导出,然后配置引脚方向。检查部分是至关重要的,如果导出一个已经被导出的引脚将会出现错误。使用 sync 函数也需要注意:因为 Node 是异步的,如果不强制使用同步函数,会存在读取或者写入操作在发生引脚配置之前或者正在配置引脚的时候的风险,这同样也会导致错误。
接下来针对每一个引脚调用配置函数 - 我们会使用最简单的方法,如果由多个引脚,你可以使用循环的方法。我们主要关心应用的功能,用一些基本的操作作为开始:轮询开关,并将状态复制到 LED。setInterval 函数会周期性得调用我们的应用:第一个参数是调用的函数,第二个是时间,单位毫秒,在我们的应用里可以当做一个软件去抖动功能。注意 setImmediate 的使用,由于 Node 的异步特性,需要用其确保在代码开始轮询前引脚被正确配置。
//setImmediate(function cfgOurPins(){ cfGPIO(LED1, 'out'); //you should complete it for the other GPIO pins setInterval(copySwToLed, 50); //});
在运行应用前需要重启,我们可以检查引脚是是否被正确地导出和配置,以及其他的部分是否正确执行。检查输出的信息以及测试硬件。
root@colibri-vf:~# reboot root@colibri-vf:~# nodemon server.js
在实际操作的一些建议:
- 按下按键检查系统是否正常工作
- 更改延时的时间,调整到较高的值,如 1000,观察按键时间和 LED 变换之间的延时
- 你可以使用 cfGPIO 函数配置 active_low 文件,然后代码中需要在将开关状态发送至 LED 之前将其翻转
- 如果你需要健壮的代码,在代码里添加错误处理
- 为 GPIO 函数和方法创建模块
在开发板上开发网络服务器和网络界面
接下来我将使用由 Express 安装的 debug module,在终端里显示日志信息。为了使用调试功能, DEBUG 变量需要在代码里设置为使用的名字,或者 * 显示所有的调试信息。该部分的代码也可以在这里从 GitHub 上 下载。
DEBUG=myserver nodemon server.js --ignore index.js
现在需要开发 Express 网络服务器。我们需要 Express 和调试相关的模块,将 IP 地址和侦听端口添加到代码中。为了给我们的应用提供网页,我们只需要指向一个包括 CSS、JavaScript、图片等文件的目录,侦听特定的 IP 和端口。常量 __dirname 指向当前目录。修改的代码如下:
/* server.js */ /* Modules */ var fs = require('fs'); var express = require('express'); var bodyParser = require('body-parser'); var app = express(); var debug = require('debug')('myserver'); /* Constants */ const HIGH = 1, LOW = 0, IP_ADDR = '192.168.0.180', PORT_ADDR = 3000; //Using Express to create a server app.use(express.static(__dirname)); app.listen(PORT_ADDR, IP_ADDR, function () { debug('Express server listening at http://%s:%s %s', host, port, family); });
客户端的代码也需要编写。服务器默认会查找 index.html 文件,该文件也需要一些修改。我们将会使用 JavaScript/jQuery 创建 client.js 文件。第一个版本的 index.html 会被显示,主要由输入和标签,这在接下来被用作控制 LED 的开关。在文件的头部,jQuery 和 client.js 已经包含进来了。这个网站适合 初学者学习使用网络开发语言、方法和库。
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Colibri VF61 node.js webserver</title> <!-- Add jQuery library --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script type="text/javascript" src="client.js"></script> </head> <body> <h1>Access to the VF61 GPIO using Node.js and AJAX</h1> <form> <input type="checkbox" class="btn" id="btn1"> <label for="btn1" id="btn1l" style="color:red">OFF</label> </form> </body> </html>
如果你打开浏览器,访问服务器在侦听的 IP,你可以看到如图 2 的响应。你可以点击勾选框,但是不会有任何事情发生。
图 2:第一个网页应用设计
客户端的 client.js 可以按照下面的方法为页面添加响应以及同服务器通信。首先,jQuery 获得所有 btn 类元素的点击事件。然后检查按键是否以及被按下,并相应地改变标签,设置发送给服务器的 btn_status 对象的按键 id 和属性。最后,使用 jQuery 发送 AJAX HTTP POST 到服务器,包含先前提到的 btn_status 对象,其被编码成 JSON 字符串。
/* client.js */ $(function(){ $(".btn").click(function clickHandling(){ var btn_status = {id:"", val:""}; if(this.checked){ $(this).siblings().html("ON").css("color","green"); btn_status.id = $(this).attr("id"); btn_status.val = "on"; } else{ $(this).siblings().html("OFF").css("color","red"); btn_status.id = $(this).attr("id"); btn_status.val = "off"; } $.post("/gpio", btn_status, function (data, status){ if(status == "success"){ console.log(data); } },"json"); }); });
在实际使用之前,需要在服务器侧 (server.js) 添加支持接收 HTTP POST 和根据接收到的数据做出响应的功能。同时还需要移除开关轮询部分的功能,否者两者会有冲突,无法获得预期效果。第一步需要设置 body-parser,这必须在设置事务之前完成。然后设置事务,其名字必须和客户端应用中的一致,但是你可以选择你想要的名字,除此之外还可设置多个事务。然后设置处理接收到的 POST 的函数:获取服务器收到的值,相应地改变 LED,并向服务器发送响应,例如其可以发送包含需要客户端确认的数据,确保按照预期的执行。
/* server.js */ app.use(bodyParser.urlencoded({ //must come before routing extended: true })); app.route('/gpio') .post(function (req, res) { var serverResponse = {status:''}; var btn = req.body.id, val = req.body.val; if(val == 'on'){ // call wrGPIO to write 1 to them serverResponse.status = 'LEDs turned on.'; res.send(serverResponse); } else{ // call wrGPIO to write 0 to them serverResponse.status = 'LEDs turned off.'; res.send(serverResponse); } });
现在网络可以正常响应,你也可以控制 LED。采用 AJAX 同 Node 服务器通信,这就是最简单的方法实现从网页控制 GPIO 输出。下面是你可以尝试的一些事情:
- 为服务器响应添加状态属性,在客户端查看,例如“成功”或者“失败”,并可以在服务器侧的 GPIO 函数增加错误处理函数
- 使用 setInterval 函数在客户端轮询开关状态。间歇性发送 POST 请求,报告目前开关状态。如果你没法实现,也不用担心,在接下来文章中我们也将会开发一个能够轮询开关状态并更新 UI 的应用
设计和使用多个 LED/开关
这个代码或许由实际参考价值,但是界面却并不怎么友好,例如 图 3 是在智能手机上使用我们应用的截图,通过 Chrome 仿真终端模式。我们将会修改代码使用 Bootstrap 框架,其主要基于 CSS,并且对移动端友好。你也可以使用例如 PhoneGap其他框架,为移动设备创建和我们类似的应用,或者使用其他工具自己设计 UI。
首先我们要修改 HTML 文件来使用 Bootstrap 框架。尽管看起来会有许多的变动,但这不会影响应用的工作,只是改变外观。在文件头部,我们需要添加 meta tag 来提供移动端支持,以及 CSS 和 JavaScript 库文件的链接。在 body 部分,添加 div 容器,其提供支持相应以及固定宽度的 div,我们的页面将会限制在里面。在内部,由于 Bootstrap 需要使用网格系统,因此添加row div - 在 div 内,增加 4 宽度的列 div,总共为 12,由 Bootstrap 提供。
相比于我的应用, 主要的不同 是我们不再使用勾选框和标签作为输入,而替换为按键:配置位于类属性中,btn 和 btn-block 是 Bootstrap 类,true_btn 则用于区分不同的按键所对应的 LED。同样,这可以使得 style/label 变更更加容易,因为没有必要寻找便签和勾选框的对应关系。
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- mobile first --> <title>Colibri VF61 node.js webserver</title> <!-- Add jQuery library --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <!-- Using bootstrap --> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script type="text/javascript" src="client.js"></script> </head> <body> <div class="container"> <h1>Access to the VF61 GPIO using Node.js and AJAX</h1> <div class="row"> <div class="col-sm-4"> <input type="button" class="btn btn-block true_btn" id="LED1"> </div> </div> </div> </body> </html>
如果你现在运行应用,你将会发现在 UI 上有很多的变化,但是和预期的一样会无法工作。在修复之前,我们需要做以下的事情:
- 在 row div 添加两个按键,改变 id 索引 - 这些按键将会是 LED 指示器/控制器
- 在 LED 指示器后面添加另外一行,再添加 3 个按键,分别使用 SW1、SW2、SW3 id - 这是开关状态指示器
- 在 LED 指示器前面,添加全款的行,id “MSG”,值是“application running” - 这只是我们的应用正在运行
- 在每行前面或者内部添加头部信息,标记 LED 和开关,方便用户区别
在做这些变更之前,我可以查看 HTML代码。从这里开始,那些文件将不需要进一步的变更。现在我们需要修改服务器来适应新的 UI 设计。修改配置引脚的函数 - 循环查询作为 LED 或者开关的 GPIO,并相应地配置,所以这可以适用于任何数量的引脚。同时为所有使用的 GPIO 创建对象。下面代码是从前面修改而来的:
/* server.js */ /* VF61 GPIO pins */ const GPIO = { LED1:'47', LED2:'50', LED3:'53', SW1:'46', SW2:'88', SW3:'48'}; setImmediate(function cfgOurPins(){ for(io in GPIO){ //for every GPIO pin if(io.indexOf('LED') != -1){ cfGPIO(GPIO[io], 'out'); } else if(io.indexOf('SW') != -1){ cfGPIO(GPIO[io], 'in'); } } });
接下来,处理 POST 的部分代码需要重写。按照惯例,我们假设如果客户端想要服务器发送当前 GPIO 的状态,其将会发送 getGPIO 作为 id。那么服务器会读取所有 GPIO 状态值,并发送到客户端。否者,客户端将会发送引脚 id 以及需要写入的值 - 这种情况下,服务器会改变 GPIO 的状态。
现在,服务器端的代码已经完成,你可以在 这里查看。但是为了是应用能够正常工作,还需要一些修改:JavaScript/jQuery 客户端的代码同样需要修改,这将是我们接下来要做的。首先,重命名 clickHandling 函数,将其移到我们主要代码外面,无论什么时候点击 true_btn 类按键,btnHandling 函数都会被调用 - 这能够帮助我们区分哪一个按键需要翻转 LED 而哪一个不用。我们同样在 changeLedState 函数中添加 AJAX POST 请求,所以我们的代码可以更加容易理解,这部分的代码也可以被重用:
不使用 Bootstrap 的主要不同是检查按键。btn-success 或者 btn-danger 类分别应用于 Green 或 Red 按键,我们使用这个不仅可以告诉用户,对应的 LED 是否开启/关闭,也可以在我们的代码中使用 indexOf 方法检查 LED,当传递的字符串不匹配时,其会返回 -1。通过调用 changeLedState,我们可以更改按键类。并告诉服务器翻转 LED 状态。
我们还需要另外一个函数,在加载页面后立即读取 GPIO 状态,使得第一个 LED 操作不会出错。为此,需要 getGPIO 和 updateCurrentStatus 函数。getGPIO 不会返回值,而是传递一个回调函数; 该函数可以使用 getGPIO 发送的 id 而获得的返回的 GPIO 状态。updateCurrentStatus 只读取服务器返回的 GPIO 状态,以及 /sys/class/gpio/gpiox/active_low 翻转 GPIO,现在我们不需要这个功能。最后,需要在 main 函数中调用更新按键的状态。
/* client.js */ $(function main(){ getGPIO(updateCurrentStatus); $(".true_btn").click(btnHandling); }); function getGPIO(callback){ /* Gets the current GPIO status*/ $.post("/gpio", {id:'getGPIO'}, function (data, status){ if(status == "success"){ callback(data); } },"json"); } function updateCurrentStatus(gpio_status){ /* Updates the page GPIO status*/ if(gpio_status.status == 'readgpio'){ for (next_pin in gpio_status.gpio){ if(next_pin.indexOf("SW") != -1){ gpio_status.gpio[next_pin] = !gpio_status.gpio[next_pin]; } if(gpio_status.gpio[next_pin]){ $("#" + next_pin).attr("class","btn btn-block btn-success").val(next_pin + ":ON"); } else{ $("#" + next_pin).attr("class","btn btn-block btn-danger").val(next_pin + ":OFF"); } if(next_pin == curr_led){ $("#" + next_pin).addClass("active"); } } } }
我们在 0.2 秒内轮询 GPIO,为每一个开关添加角色,所以不会有空余的时间。为此,我们会使用 setInterval 间歇性地调用 getGPIO 以及回调,其名称为 swAction,实现开关的功能:
- SW1 更改选中的 LED
- SW2 翻转选中的 LED
- SW3 通过停止 setInterval 暂停应用 - MSG 按键可以恢复应用
- 在应用暂停的时候,如果点击 MSG 按键,将会调用 resumeApp,其最终调用 setInterval 恢复轮询
- 同样也需要设置两个全局变量:一个指向当前选择的 LED,另外一个表明总共 LED 数量
根据上面的说明,做相应的变更。
现在我们使用 Node 通过用户友好的网页 UI 访问 GPIO 已经完成。你在 这里查看客户端的最终代码。查看图 4 和 5,可以看到相比开始的界面,这个更加好看。有一个视频展示了应用如何工作。
图 5:在大屏幕浏览器上的用户界面友好的应用
文章包含了大量我们使用到的概念的链接,你可以随时查看。我同样也希望,这篇文章对您有所帮助。
本博文最开始使用葡萄牙语在 Embarcados.com 发表,请参考这里。