六、Arduino 到前端:第二部分

本章深入探讨了 Arduino 组件如何与网页元素交互。您将使用模拟和数字数据、JavaScript 数据结构以及对数据的简单计算。在本章结束时,您将创建一个使用电位计的交互式应用程序来回答基于 web 的问题,使用数据进行计算,并在 Arduino 上将其可视化。

模拟和数字信号

Arduinos 具有数字和模拟引脚,可以发送和接收模拟或数字信号。模拟信号是连续可变的;数字信号以固定的单位计数。图 6-1 显示了一个模拟和数字信号。

A453258_1_En_6_Fig1_HTML.jpg

图 6-1

An analog and digital signal

一个数字信号有有限数量的值,有限数量的范围内的步骤。模拟信号是连续可变的。因此,数字信号可以记录 11 或 12,而模拟信号可以记录 11 和 12 之间的任何数字。

Arduino 既有模拟引脚,也有数字引脚。analogRead 功能允许您从模拟引脚读入数据,analogWrite 功能允许您使用该数据控制其他元件,例如 LED 的亮度。

analogRead 函数的范围是 0 到 1023,analogWrite 函数的范围是 0 到 255。read 函数将输入电压映射为 0 到 1023 之间的值。必须映射读取的数据,使其符合模拟写入的范围。

应用程序

在本章中,您将创建一个事件反馈应用程序。它可以让你从一个活动的参与者那里得到反馈,并计算出这个活动有多成功。屏幕上会出现一些问题,这些问题将使用物理电位计来回答。然后,这些数据将被用于计算活动的成功程度,显示在网页上,并被发送回 Arduino。

Set Up The Arduino

事件计量应用电路将由两个电位计和一个按钮组成。电位计用于回答问题,按钮用于提交数据。你需要两个电位计、一个 Arduino、一个按钮和导线。组成如图 6-2 所示。

A453258_1_En_6_Fig2_HTML.jpg

图 6-2

Components for the application: 1. Breadboard, 2. 2 x potentiometers, 3. Button, 4. Arduino Uno

如图 6-3 所示,将组件连接到 Arduino。

A453258_1_En_6_Fig3_HTML.jpg

图 6-3

The setup for the circuits The Arduino Code

打开 Arduino IDE 并创建一个名为 chapter_06 的新草图,然后复制清单 6-1 中的代码。

const int analogInA0 = A0;
const int analogInA1 = A1;
const int pushButton = 2;

bool lastButtonState = 0;

int a0Value = 0;
int a0LastValue = 0;

int a1Value = 0;
int a1LastValue = 0;
String a0String = "A0";
String a1String = "A1";
String pushButtonString = "BP";

void setup(){
  Serial.begin(9600);
  pinMode(pushButton, INPUT_PULLUP);
}
void loop(){
  int buttonStateUp = digitalRead(pushButton);

  a0Value = analogRead(analogInA0);
  a1Value = analogRead(analogInA1);

  a0Value = map(a0Value, 0, 1023, 0, 10);
  a1Value = map(a1Value, 0, 1023, 0, 10);

  a0LastValue = CheckValue(a0Value, a0LastValue, a0String);
  a1LastValue = CheckValue(a1Value, a1LastValue, a1String);
  if(lastButtonState != buttonStateUp){
      lastButtonState = buttonStateUp;
      if(buttonStateUp == false){
          Serial.println(pushButtonString  + a0Value + "," + a1Value);
      }

  }
}
int CheckValue(int aValue, int aLastValue, String aString){
  if(aValue != aLastValue){
    Serial.println(aString + aValue);
    aLastValue = aValue;
  }
  return aLastValue;
}

Listing 6-1
Chapter_06.ino code

将 Arduino 连接到电脑,编译并上传代码。

代码解释

在这张草图中,有两个电位计连接到模拟输出 A0 和 A1。还有一个按钮连接到数字输出 2。

该代码向服务器发送两种类型的数据,一种类型是旋转电位计,另一种类型是按下按钮。在每个循环中,来自每个电位计的值被发送到一个名为 CheckValue 的函数。

该函数检查值是否已经改变。如果有,它会将新值发送到串行端口。该函数在一个循环中被调用两次,以检查每个电位计。每个电位计都有一个标识符字符串,该字符串与其值一起发送到串行端口。为了检查该值是否已经改变,该函数需要前一个循环中的电位计值以及当前循环中的值。

在每次循环中还会检查按钮的状态。如果它发生变化,并且变化是被按下,则每个电位计的当前值与数据连接到按钮按下的标识符一起被发送到串行端口。表 6-1 详细解释了代码。

表 6-1

Chapter_06.ino explained

| const int analogInA0 = A0;``const int analogInA1 = A1;T2】 | 有三个变量保存电位计和按钮的引脚编号参考。 | | bool lastButtonState = 0; | 这个变量保存按钮的状态,0 代表向上,1 代表向下。 | | int a0Value = 0;``int a0LastValue = 0;``int a1Value = 0;T3】 | 这些变量将保存电位计的当前值和最终值。 | | String a0String = "A0";``String a1String = "A1";T2】 | 每个交互式组件都有一个参考字符串。 | | int buttonStateUp = digitalRead(pushButton); | 按钮的当前状态在每个循环开始时被记录。 | | a0Value = analogRead(analogInA0); a1Value = analogRead(analogInA1); | 每个电位计的值被读入一个变量。 | | a0Value = map(a0Value, 0, 1023, 0, 10); a1Value = map(a1Value, 0, 1023, 0, 10); | 这些值被映射。这是因为电位计值介于 0 和 1023 之间,对于应用,它们需要介于 0 和 10 之间。 | | a0LastValue = CheckValue(a0Value, a0LastValue, a0String); a1LastValue = CheckValue(a1Value, a1LastValue, a1String); | 电位计的值被发送到一个名为 CheckValue 的函数。检查值传递了三个参数:当前值、最后一个值和标识符字符串 | | int CheckValue(int aValue, int aLastValue, String aString){``if(aValue != aLastValue){``Serial.println(aString + aValue);``aLastValue = aValue;``}``return aLastValue;T6】 | CheckValue()函数有一个 if 语句,用于检查新值是否不同于旧值(使用不等于)。如果不同,则调用 Serial.println()函数,传递字符串标识符(“A0”或 A1”)。最后一个值被更改为当前值,并从函数中返回。 | | if(lastButtonState != buttonStateUp){ | 这个 if 语句检查按钮的状态是否已经改变。 | | lastButtonState = buttonStateUp; | 如果按钮状态已经改变,则更新变量 lastButtonState 以反映该改变。 | | if(buttonStateUp == false){``Serial.println(pushButtonString  + a0Value + "," + a1Value);T2】 | 如果 buttonStateUp 为 false,则意味着按钮被按下;如果它是向下的,那么标识符“BP”与两个电位计的值连接,这段数据通过串行端口发送。 |

注意与 JavaScript 函数不同,函数的返回值需要在。ino 文件。到目前为止,有两个功能,设置和循环。这两个都不返回任何内容,所以它们以关键字 void 开头。在本章中,将调用函数 CheckValue。它返回一个整数,所以当它被定义时,int 关键字出现在函数名之前:int CheckValue(int aValue,int aLastValue,String aString)。

Node.js 应用程序

当电位计转动时,网络应用程序将收到数据。web 应用程序将需要计算出哪个电位计被转动,然后更新网页。图 6-4 显示了应用程序的外观。

A453258_1_En_6_Fig4_HTML.jpg

图 6-4

The event feedback front end

应用程序的目录结构如下所示:

/chapter_06
    /node_modules
    /public
        /css
            main.css
        /javascript
            main.js
    /views
        index.ejs
    index.js

Set Up The Node.js Server

本章中的 Node.js 服务器的设置方式与前几章相同:

  1. 创建一个新文件夹来存放应用程序。我叫我的章 _06。
  2. 打开命令提示符(Windows 操作系统)或终端窗口(Mac)并导航到新创建的文件夹。
  3. 当你在正确的目录键入 npm init 创建一个新的应用程序;您可以按下 return 键浏览每个问题,或者对它们进行更改。
  4. 您现在可以开始添加必要的库;要在命令行下载 Express.js,请键入 npm install express@4.15.3 - save。
  5. 然后安装 ejs,键入 npm install ejs@2.5.6 - save。
  6. 下载完成后,安装串口。在 Mac 上键入 npm 安装 serial port @ 4 . 0 . 7–save,在 Windows PC 上键入 npm 安装 serial port @ 4 . 0 . 7–build-from-source。
  7. 然后最后安装 socket.io,输入 npm install socket.io@1.7.3 - save。

Create a Node.js Server

Node.js 服务器类似于前面的章节。主要区别在于,来自 Arduino 的数据可以通过电位计或按钮发送。这意味着数据必须根据输入进行清理,并发送给正确的函数。在应用程序的根目录下,创建一个名为 index.js 的文件,并复制清单 6-2 中的代码。

var http = require('http');
var express = require('express');
var app = express();
var server = http.createServer(app);
var io = require('socket.io')(server);
var SerialPort = require('serialport');
var serialport = new SerialPort('<add in the serial port for your Arduino>', {
    parser: SerialPort.parsers.readline('\n')
});
app.engine('ejs', require('ejs').__express);
app.set('view engine', 'ejs');

app.use(express.static(__dirname + '/public'));

app.get('/', function (req, res){
    res.render('index');
});

serialport.on('open', function(){
    console.log('serial port opened');
});

io.on('connection', function(socket){
    console.log('socket.io connection');
    serialport.on('data', function(data){
        console.log(data);

        var dataKey = data.slice(0,2);
        var dataString = data.slice(2);
        dataString = dataString.replace(/(\r\n|\n|\r)/gm,"");

        if(dataKey === "BP"){
            var dataArray = dataString.split(",");
            console.log(dataArray);
            socket.emit("button-data", dataArray);
        } else {
            var dataObject = {
                dataKey: dataKey,
            dataString: dataString
        }
        console.log(dataObject);
        socket.emit("bar-data", dataObject);
    }
   });
    socket.on('disconnect', function(){
        console.log('disconnected');
    });
});
server.listen(3000, function(){
    console.log('listening on port 3000...');
});

Listing 6-2
index.js code

删除并将你的串口添加到新的 serial port()函数中。

代码解释

服务器需要处理两种不同类型的数据,电位计数据和按钮数据。创建了两个套接字,每个套接字对应一种数据类型,ID 分别为“条形数据”和“按钮数据”

这段代码中使用了一些新的函数和概念。它们与转动电位计或按下按钮有关,并使用这些数据创建数据结构。

控制台日志有助于了解哪些数据被发送到前端。表 6-2 解释 index.js。

表 6-2

index.js explained

| serialport.on('data', function(data){ console.log(data); | 通过串行端口传递的数据可能来自电位计或按钮。如果电位计连接到 A0,数据将类似于“A03”,其中“A0”是标识符,“3”是电位计的数据。如果是连接到 A1 的电位计,它将类似于“A13”如果按钮被按下,那么数据将类似于“BP2,4”“BP”是按钮的标识符,“2,4”是按钮传递的数据。 | | var dataKey = data.slice(0,2); | slice()是一个删除 JavaScript 字符串元素的 JavaScript 函数。当函数被传递(0,2)时,它从字符串的索引 0 开始切片,并切片两个字符。这两个切分字符存储在 dataKey 变量中。 | | var dataString = data.slice(2); | 通过向 slice 函数传递一个参数,将返回从该索引点到字符串末尾的字符。 | | dataString = dataString.replace(/(\r\n&#124;\n&#124;\r)/gm,""); | JavaScript 中的 replace 函数可以用另一个字符替换某个字符。在这种情况下,字符串末尾有一个新行,需要用空字符串""替换。第一个参数是正则表达式(\r\n|\n|\r)/它在字符串上查找任何类型的回车符或换行符,而替换它的第二个参数是空字符串" "。 | | if(dataKey === "BP"){``...``} else {``...T4】 | 数据键定义了 Arduino 上的交互内容。有一个 if/else 语句来检查组件。if 语句检查 dataKey 变量包含的内容。如果是“BP ”,则表示按钮已被按下,如果不是,则表示电位计已被转动。 | | var dataArray = dataString.split(","); | 函数的作用是:将一个字符串分割成一个数组。参数“”告诉 split 根据什么来拆分字符串,在本例中是一个逗号。这里的结果将是一个数组,其中包含电位计的两个数字;比如[ '3 ',' 4' ]。 | | socket.emit("button-data", dataArray); | 这会将数组发送到一个名为“button-data”的套接字。 | | var dataObject = {``dataKey: dataKey,``dataString: dataStringT3】 | 如果使用 else 语句,则数据来自电位计。在这种情况下,您需要传递一个标识符来显示哪个电位计被转动过,以及数据。创建一个名为 dataObject 的对象,该对象可以与标识符和数据一起传递到前端。 | | socket.emit("bar-data", dataObject); | 然后,这些数据通过一个名为“bar-data”的套接字发送出去。 |

注意正则表达式用于搜索字符串中的不同字符。它们由描述搜索模式的已定义集合符号组成。例如,使用\n 匹配换行符,使用\r 匹配回车符。

Create The Front End

本章中的前端将做许多事情;它将向 Arduino 用户提供反馈,并显示之前用户所说的信息。首先,您将创建一个提供 Arduino 输入反馈的网页。在应用程序的根目录下创建一个 views 文件夹,并在其中创建一个文件。从清单 6-3 中复制 HTML。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>data</title>
    <link href="/css/main.css" rel="stylesheet" type="text/css">
</head>
<body>
    <header>
        <h1>EVENT METRICS</h1>
        <h2>getting information through an Arduino</h2>
    </header>
    <div id="content">
        <h2>AT TONIGHTS EVENT DID YOU ...</H2>
        <p>Answer the questions by turning the knobs, to submit your answer press the button.</p>
        <div id="bar-A0" class="container">
            <div  class="bar">
                <p>talk to someone new?</p>
                <div class="response-container">
                    <p class="flex-item">not really</p>
                    <p class="flex-item">loads</p>
                </div>

                <svg width="400" height="20" viewBox="0 0 400 20">
                    <rect id="A0"
                    x="0"
                    y="0"
                    fill="#6BCAE2"
                    width="320"
                    height="20"/>
                </svg>
            </div>
            <div class="text-block">
                <h3>Current Input<h3>
                <p></p>
            </div>
            <div class="text-block-response hidden">
                <h3>Thanks<h3>
            </div>
        </div>
        <div id="bar-A1" class="container">
            <div  class="bar">
                <p>find out something new new?</p>
                <div class="response-container">
                    <p class="flex-item">not really</p>
                    <p class="flex-item">loads</p>
                </div>
                <svg width="400" height="20" viewBox="0 0 400 20">
                    <rect id="A1"
                    x="0"
                    y="0"
                    fill="#6BCAE2"
                    width="320"
                    height="20"/>
                </svg>

            </div>
            <div class="text-block">
                <h3>Current Input<h3>
                <p></p>
            </div>
            <div class="text-block-response hidden">
                <h3>Thanks<h3>
                <p></p>
            </div>
        </div>
    </div>
    <script src="/socket.io/socket.io.js"></script>
    <script src="javascript/main.js"></script>
</body>
</html>
Listing 6-3index.ejs code

代码解释

服务器需要处理两种不同类型的数据,电位计数据和按钮数据。创建了两个套接字,每个套接字对应一种数据类型,ID 分别为“条形数据”和“按钮数据”

前端需要向用户反馈他们与 Arduino 的交互;这包括关于电位计值的信息和反馈,以让他们知道按钮按压已经被记录。

要注意的主要事情是有两个非常相似的 HTML 块,每个电位计一个。它们都有相同的结构,有一个类“容器”、“栏”、“文本块”和“文本块响应”以及一个 SVG。每个模块还具有 id 参考,作为其可视化电位计的参考。表 6-3 详细介绍了 index.ejs 中的代码。

表 6-3

index.ejs explained

| <div id="bar-A0" class="container"> <div id="bar-A1" class="container"> | 每个电位计都有相似的 HTML 块,所以它们得到正确的数据;每个都有一个与电位计相关的 id。 | | <div class="text-block-response hidden"> | 在每个容器块中,都有一个隐藏了类的 div。只有当有人按下按钮时,它的内容才会显示,所以它有第二个名为 hidden 的类,用于隐藏 div。 |

Add Style

在应用程序的根目录下创建一个公共文件夹,并在其中创建一个名为 CSS 的文件夹;创建 main.css 文件。将清单 6-4 中的 CSS 复制到新创建的文件中。

*{
  margin: 0;
  padding: 0;
}
body{
  font-family: Verdana, Arial, sans-serif;
}
h2{
  font-size: 18px;
}
h3{
  font-size: 16px;
}
p{
  font-size: 14px;
}
header{
  background: #FE8402;
  color: white;
}
header h1{
  padding-top: 25px;
}
header h2{
  padding-bottom: 45px;
}
header h1,header h2, header h3, header p{
    padding-left: 12px;
}
#content{
  margin: 22px;
}
.container{
    display: flex;
    flex-direction: row;
    margin-top: 20px;
}
.response-container{
  display: flex;
}
.flex-item:nth-child(2){
  margin-left: 305px;
}
.bar > p{
  font-weight: bold;
}

.text-block{
    background: #6BCAE2;
    color: white;
    width: 145px;
    height: 45px;
    margin-top: 22px;
    margin-bottom: 10px;
    margin-right: 10px;
    padding: 10px;
    font-size: 16px;
}
.text-block-response{
    background: #FE8402;
    color: white;
    width: 90px;
    height: 45px;
    margin-top: 22px;
    margin-bottom: 10px;
    margin-right: 10px;
    padding: 10px;
    font-size: 16px;
}
.text-block p{
  padding-left: 0px;
  padding-top: 12px;
}
.container svg{
  border: #FE8402 solid 1px;
  padding: 10px;
  margin: 10px 10px 10px 0;
}
.hidden{
  visibility: hidden;
}
Listing 6-4
main.css

代码解释

CSS 与前面的章节相似,使用 Flexbox 作为主要内容。表 6-4 解释了 main.css 中的一些 CSS。

表 6-4

main.css explained

| header h1,header h2, header h3, header p{} | 这将只选择标题中的 h1、h2、h3 和 p 标签。 | | .flex-item:nth-child(2){``margin-left: 305px;``}``<div class="response-container">``<p class="flex-item">not really</p>``<p class="flex-item">loads</p>T6】 | nth-child 是一个 CSS 命令,选择某个位置有元素或类名的子元素。在这种情况下,有两个带有“flex-item”类的 p 标签;n-child(2)挑第二个,给它左边 305 像素的边距;这是它坐在酒吧的尽头。 | | .bar > p{``font-weight: bold;T2】 | CSS 中的>字符选择它前面元素的子元素,而不是孙元素。在这种情况下,它将选取类“bar”的 div 中的段落,但不选取 div 中任何作为“bar”的子元素的段落。 | | .hidden{``visibility: hidden;T2】 | hidden 类使用可见性来隐藏包含该类的任何元素。然后可以用 JavaScript 添加和删除它,以隐藏和取消隐藏一个 HTML 块。 |

Make The Page Interactive

最后添加代码,使前端的元素具有交互性。在 public 文件夹中创建一个名为 JavaScript 的新文件夹,并创建一个名为 main.js 的文件。将清单 6-5 中的代码复制到 main.js 中。

(function(){
    var socket = io();
    socket.on("bar-data", function(data){
        var current = data.dataKey;
        var svgBar = document.getElementById(current);  
        var newWidth = data.dataString * 40;
        svgBar.setAttribute("width", newWidth);
        currentInputValue(data);
        addRemoveClass("add");
    });
    socket.on("button-data", function(data){
        addRemoveClass("remove");
    });
    function addRemoveClass(action){
        var buttonResponse = document.getElementById("bar-A0").getElementsByClassName("text-block-response")[0];

        buttonResponse.classList[action]("hidden");
        buttonResponse = document.getElementById("bar-A1").getElementsByClassName("text-block-response")[0];
        buttonResponse.classList[action]("hidden");
    }

    function currentInputValue(data){
        var targetP = document.getElementById("bar-" + data.dataKey).getElementsByClassName("text-block")[0].getElementsByTagName("p")[0];
        targetP.innerHTML = data.dataString;
    }
})();

Listing 6-5
main.js code

代码解释

CSS 与前面的章节相似,使用 Flexbox 作为主要内容。

JavaScript 将从两个插座接收数据,“bar-data”和“button-data ”,这取决于是转动电位计还是按下按钮。当按钮被按下时,需要给用户一个通知。当转动电位计时,用户将看到数据以条形和数字的形式显示。

当电位计转动时,向前端发送一个包含电位计标识符和值的对象。表格 6-5 详细描述了 main.js 中的代码。

表 6-5

main.js explained

| socket.on("bar-data", function(data){ var current = data.dataKey; | socket.on 函数做的第一件事是获取电位计的密钥,并将其存储在一个变量中。 | | var svgBar = document.getElementById(current); | 然后将正确的 SVG 条存储在一个变量中。 | | svgBar.setAttribute("width", newWidth); | 宽度属性在 svg 上更新。 | | currentInputValue(data); | 调用更新电位计文本值的函数。 | | addRemoveClass("add"); | 调用隐藏按钮被按下通知的函数。 | | socket.on("button-data", function(data){``addRemoveClass("remove");T2】 | 当按钮被按下时,调用一个函数从通知块中删除隐藏的类。 | | The AddRemoveClass(action) function | 这个函数添加或删除隐藏在 div 中的类,该类通知用户他们已经按下了按钮。 | | var buttonResponse = document.getElementById("bar-A0").getElementsByClassName("text-block-response")[0]; | 这将选择 id 为“bar-A0”的元素中类名为 text-block-response“bar-A0”的第一个元素。 | | buttonResponse.classList[action]("hidden") ; | classList 是一个 JavaScript 函数,可以在元素中添加或删除类名。当函数被用来添加或删除一个类时,这些关键字作为参数动作被传递给 AddRemoveClass 函数,并被传递给方括号[]中的 classList 函数。 | | The currentInputValue(data) function | 该函数首先存储它想要更改的元素,然后将其 innerHTML 更改为来自电位计的当前数据字符串, |

请注意,您可能已经注意到了调用“document . getelementsbyclassname”末尾的[0],这是因为它返回一个数组,类似于具有该类名的所有元素的对象。要访问数组中您想要的元素,您可以使用括号通过它在数组中的位置来请求它。即使在您进行的调用中只返回了一个元素,它仍然需要被它在数组中的位置引用,也就是位置 0。

注意您通常会看到这样调用的函数:

button response . class list . add(" hidden ");或者 button response . class list . remove(" hidden ");

add 和 remove 关键字写在“.”之后。在这段代码中,单词“add”或“remove”是作为参数发送给函数的字符串。因为它们是字符串,而不是关键字,所以不能加在点之后,而需要放在方括号中。方括号用于任何传递字符串的函数,当它需要一个关键字时。

您可以通过在控制台窗口中导航到应用程序,然后键入 nodemon index.js 或 node index.js 来启动应用程序,从而检查到目前为止页面的外观。确保 Arduino 已连接到您的电脑,并打开浏览器,然后转到 http://localhost:3000/查看应用程序的运行情况。

扩展应用程序

现在你有了数据,你可以用它做很多事情。该应用程序将被扩展到包括一个晚上的总体评级。

这些计算可以在服务器或前端进行。在这种情况下,它们将在前端执行。如果刷新页面,数据将会丢失,因为它只存储在浏览器的本地。

您可以设置连接到 Node.js 服务器的数据库并存储数据,这不在本书讨论范围之内;在附录 b 中有一些关于在哪里找到建立数据库的信息的信息。

Arduino 的设置是相同的,Node.js 服务器的代码也是如此。

计算出每个问题选择 5 或以上的人的百分比。

Update The Code

要更新应用程序,打开清单 6-3 中的 index.ejs 文件,并复制到粗体的 HTML 中。

<!DOCTYPE html>
<html>
...
            <div class="text-block">
                <h3>Current Input<h3>
                <p></p>
            </div>
            <div class="text-block-response hidden">
                <h3>Thanks<h3>
                <p></p>
            </div>
        </div>
        <div>
            <h2><span id="percent">0</span>%</h2s>
            <p>positive event</p>
        <div>
    </div>
    <script src="/socket.io/socket.io.js"></script>
    <script src="javascript/main.js"></script>
</body>
</html>

添加的 div 将显示正面反馈的百分比。span 有一个 id,这样它就可以被 JavaScript 访问并以百分比更新。

接下来打开清单 6-5 中的 main.js 并添加粗体代码。

(function(){
    var socket = io();
    var totalClickCounter = 0;
    var accumulatorArrayA0 = [0,0,0,0,0,0,0,0,0,0,0];
    var accumulatorArrayA1 = [0,0,0,0,0,0,0,0,0,0,0];
    socket.on("bar-data", function(data){
        var current = data.dataKey;
        var svgBar = document.getElementById(current);
        var newWidth = data.dataString * 40;
        svgBar.setAttribute("width", newWidth)
        currentInputValue(data);
        addRemoveClass("add");
    });
    socket.on("button-data", function(data){
        var percetageSpan = document.getElementById('percent');
        totalClickCounter = totalClickCounter + 2;
        accumulatorArrayA0[data[0]] = accumulatorArrayA0[data[0]] + 1;
        accumulatorArrayA1[data[1]] = accumulatorArrayA1[data[1]] + 1;

        var positiveTotal1 = sumPositiveResponses(accumulatorArrayA0);
        var positiveTotal2 = sumPositiveResponses(accumulatorArrayA1);
        var positiveTotals = positiveTotal1 + positiveTotal2;
        var positivePercentage = (positiveTotals/totalClickCounter) * 100;
        percent.innerHTML = Math.floor(positivePercentage)
        addRemoveClass("remove");
    });
    function sumPositiveResponses(dataArray){    
      var positiveTotal = 0;
      for(var i = 5; i< dataArray.length; i++){
        positiveTotal = positiveTotal + dataArray[i];
      }
      return positiveTotal;
    }

    function addRemoveClass(action){
        var buttonResponse = document.getElementById("bar-A0").getElementsByClassName("text-block-response")[0];
        buttonResponse.classList[action]("hidden");
        buttonResponse = document.getElementById("bar-A1").getElementsByClassName("text-block-response")[0];
        buttonResponse.classList[action]("hidden");
    }

    function currentInputValue(data){
        var targetP = document.getElementById("bar-" + data.dataKey).getElementsByClassName("text-block")[0].getElementsByTagName("p")[0];
        targetP.innerHTML = data.dataString;
    }
})();

代码解释表 6-6 解释 main.js 中的代码。

表 6-6

The updated main.js explained

| var totalClickCounter = 0; | 创建一个计数器来记录按钮被按下的次数。 | | var accumulatorArrayA0 = [0,0,0,0,0,0,0,0,0,0,0]; var accumulatorArrayA1 = [0,0,0,0,0,0,0,0,0,0,0]; | 有两个数组:一个用于 11 个元素的每个问题,一个用于电位计上的每个可能的选择。 | | totalClickCounter = totalClickCounter + 2; | 每点击一次按钮,计数器就增加 2,因为百分比需要通过两个问题计算出来。 | | accumulatorArrayA0[data[0]] = accumulatorArrayA0[data[0]] + 1; accumulatorArrayA1[data[1]] = accumulatorArrayA1[data[1]] + 1; | 来自每个问题的数据被添加到适当的数组中。如果有人在第一个问题中输入了 3,那么数组中的第三个位置将被更新 1。 | | var positiveTotal1 = sumPositiveResponses(accumulatorArrayA0); var positiveTotal2 = sumPositiveResponses(accumulatorArrayA1); | 调用一个函数来累加数组中五个或更多的元素。 | | var positiveTotals = positiveTotal1 + positiveTotal2; | 两个问题的总数相加。 | | var positivePercentage = (positiveTotals/totalClickCounter) * 100; console.log(Math.floor(positivePercentage)); | 百分比算出来了。 | | percent.innerHTML = Math.floor(positivePercentage) | index.ejs 上的跨度用百分比更新。Math.floor()是一个 JavaScript 函数,它确保值是整数;如果没有这个,你可能会得到一个长浮点数。 |

最后,更新 css,打开清单 6-4 中的 main.css 文件,将加法 CSS 添加到底部。

#responses{
  color: black;
  font-weight: normal;
  margin: 12px;
}
#responses .text-block{
    background-color: white;
    border: #6BCAE2 solid 2px;
    color: black;
    width: 260px;
    height: 48px;
    margin-top: 22px;
    margin-bottom: 10px;
    margin-right: 10px;
    padding: 10px;
    font-size: 14px;
    font-weight: normal;
}

现在,如果您重新启动服务器,您应该会根据 Arduino 的输入看到百分比变化。

在 Arduino 上可视化数据

现在,您有了一个数字来表示人们享受活动的程度,您也可以使用附加到 Arduino 的组件来表示这个数字。本章的最后一节更新了 Arduino 电路和代码,使 LED 的亮度取决于享受等级。

使用 serialport.write()函数将百分比从 Node.js 服务器发送回 Arduino。Arduino 然后处理这些数据,这样它就可以用来点亮 LED。

Update The Front-End Javascript

前端使用的 JavaScript 需要更新,以便将计算出的百分比发送到服务器。这使用了 socket.emit 函数。

打开更新后的 main.js 文件,在 socket.on()函数中添加粗体代码,id 为“button-data”

socket.on("button-data", function(data){
    var percetageSpan = document.getElementById('percent');
    totalClickCounter = totalClickCounter + 2;

    accumulatorArrayA0[data[0]] = accumulatorArrayA0[data[0]] + 1;
    accumulatorArrayA1[data[1]] = accumulatorArrayA1[data[1]] + 1;
    var positiveTotal1 = sumPositiveResponses(accumulatorArrayA0);
    var positiveTotal2 = sumPositiveResponses(accumulatorArrayA1);
    var positiveTotals = positiveTotal1 + positiveTotal2;
    var positivePercentage = (positiveTotals/totalClickCounter) * 100;
    positivePercentage = Math.floor(positivePercentage);
    percent.innerHTML = positivePercentage;
    socket.emit('percentData', positivePercentage);    
    addRemoveClass("remove");
});

代码解释

6-7 解释了更新后的 main.js 文件中的代码。

表 6-7

The updated main.js explained

| positivePercentage = Math.floor(positivePercentage); | positivePercentage 变量被更新以保存 Math.floor()函数的结果。这就是为什么 Math.floor()函数不需要被调用两次。socket.emit 发送的值需要它以及 innerHTML。 | | socket.emit('percentData', Math.floor(positivePercentage)); | 套接字发出函数 id 是“percentData ”,正的百分比数被发送给任何与该 id 匹配的 socket.on 函数。 |

Update The Node.js Server

打开 index.js 文件,列出 6-2 ,用粗体代码更新 io.on()函数。

io.on('connection', function(socket){
    console.log('socket.io connection');
    serialport.on('data', function(data){
            // console.log(data);
            var dataKey = data.slice(0,2);

            var dataString = data.slice(2);
            // console.log(dataString);
            dataString = dataString.replace(/(\r\n|\n|\r)/gm, "");

            if(dataKey === "BP"){
                var dataArray = dataString.split(",");
                // console.log(dataArray);
                socket.emit("button-data", dataArray);
            } else {
                var dataObject = {
                    dataKey: dataKey,
                    dataString: dataString
                }
                // console.log(dataObject);
                socket.emit("bar-data", dataObject);
            }

    });

     socket.on('percentData', function(data){
        serialport.write(data + 'T');
    });
    socket.on('disconnect', function(){
        console.log('disconnected');
    });
});

socket.on()函数连接到 main.js 中的 socket.emit()函数。当接收到新数据时,它通过添加了终止字符“T”的串行端口发送。

Update The Arduino

Arduino 电路和代码需要更新。对于该电路,您需要以下内容:

1 x LED
1 x 220 ohm resister

6-5 显示了如何将 LED 添加到电路中。

A453258_1_En_6_Fig5_HTML.jpg

图 6-5

The updated circuit

更新电路时记得拔掉 Arduino。LED 的阳极(较长的正极引脚)连接到 220 欧姆电阻,该电阻连接到数字引脚 9。LED 的阴极(较短的负极引脚)接地。

Update The Arduino

打开清单 6-1 中的 chapter_06.ino 代码,用粗体代码更新它。

const int analogInA0 = A0;
const int analogInA1 = A1;
const int pushButton = 2;

const int ledPin = 9;

bool lastButtonState = 0;

int a0Value = 0;
int a0LastValue = 0;

int a1Value = 0;
int a1LastValue = 0;

String a0String = "A0";
String a1String = "A1";
String pushButtonString = "BP";

int serverValueRemapped = 0;

char charRead;

String inputString ="";

void setup(){
  Serial.begin(9600);
  pinMode(pushButton, INPUT_PULLUP);
}
void loop(){
  int buttonStateUp = digitalRead(pushButton);
  a0Value = analogRead(analogInA0);
  a1Value = analogRead(analogInA1);
  a0Value = map(a0Value, 0, 1023, 0, 10);
  a1Value = map(a1Value, 0, 1023, 0, 10);
  a0LastValue = CheckValue(a0Value, a0LastValue, a0String);
  a1LastValue = CheckValue(a1Value, a1LastValue, a1String);
  if(lastButtonState != buttonStateUp){
   lastButtonState = buttonStateUp;
    if(buttonStateUp == false){
       Serial.println(pushButtonString  + a0Value + "," + a1Value);
    }
  }
  checkSerialRead();
}

void checkSerialRead(){

    if (Serial.available()) {
    charRead = Serial.read();
    if(charRead != 'T'){
      inputString += charRead;
    } else {
      int convertedString = inputString.toInt();
      serverValueRemapped = map(convertedString, 0, 100, 0, 255);
      analogWrite(ledPin, serverValueRemapped);
      inputString = "";
    }

  }

}

int CheckValue(int aValue, int aLastValue, String aString){
  if(aValue != aLastValue){
    Serial.println(aString + aValue);
    aLastValue = aValue;
  }
  return aLastValue;
}

您必须停止服务器才能将代码上传到 Arduino。验证代码,然后上传到 Arduino。重新启动服务器并刷新浏览器。现在灯光的亮度应该反映百分比。

代码解释

添加了一个名为 void checkSerialRead()的新函数,该函数在循环中被调用,并检查是否有来自服务器的新串行数据。它使用第 5 章中使用的 Serial.available 和 Serial.read()函数,解析新数据并计算出 LED 的值。表 6-8 更详细地解释了更新后的 chapter_06.ino 文件中的代码。

表 6-8

updated chapter_06.ino explained

| int serverValueRemapped = 0; | 添加了一个新变量,它将保存来自服务器的重新映射值。 | | char charRead; String inputString =""; | 有两个变量用于解析来自服务器的数据。 | | checkSerialRead(); | 在每个循环中都调用 checkSerialRead()函数。 | | void checkSerialRead(){} | checkSerialRead()函数无效,因为它不返回值。 | | int convertedString = inputString.toInt(); | 数据字符串被转换为整数。 | | serverValueRemapped = map(convertedString, 0, 100, 0, 255); | 使用 map()函数将前端的值转换为可用于点亮 LED 的值。 | | analogWrite(ledPin, serverValueRemapped); | 新值用于向 LED 发送一个值。 |

摘要

在本章中,您已经使用模拟和数字数据以及 Arduino 创建了一个系统,以了解人们对某项活动的喜爱程度。数据必须被映射和解析,这样它才能与前端一起工作。你也开始用数据提问,并把答案可视化。下一章更进一步,开始使用这些数据来回答更多的问题,并使用 JavaScript 库 D3.js 将它们可视化。