在阅读本篇文章之前,你已经阅读了:
本篇视频
本篇学习内容
讲解demo1里服务器端代码、界面代码、NodeMCU代码
阶段性作业:自行实现demo1效果demo1总览 在之前的demo1跑起来 里面已经介绍了demo1跑起来的效果了,这篇不再演示,而是直接讲解代码。(PS:图片中的IP地址根据实际情况变化,图里只是简单地举例。) 整个demo1的整体代码流程图如下所示: 首先服务器端的代码要跑起来,整个代码就是由Express手脚架(generator)一健生成的。然后NodeMCU通电跑程序时,首先连接WIFI,连接WIFI成功后会连接TCP服务器,成功后会不断地发送tick
数值,我用这个当作心跳(在之前的课程里:亲手实现demo0.1 里有讨论过为什么要引入心跳机制),然后一直等待接收控制指令,执行开关灯。 服务器端跑起来后,可以访问网页界面,点击网页界面的按钮,服务器端程序接收到控制指令,进而转发到对应的硬件里,硬件接收到就执行开关灯。
demo1界面代码 demo1的界面极其简单,就是一个下拉框一个表格,两个按钮。界面代码在demo1/myapp/views/index.html
,里面有引用的静态文件如<script src="/javascripts/index.js"></script>
对应的位置就是在demo1/myapp/public/javascripts/index.js
,这个对应着界面的逻辑代码,就是由它来获取在线设备数据,渲染到界面,点击按钮控制在线设备开关灯等业务。 为了获取在线设备列表,界面每次向服务器端发出一个请求(轮询),服务端会返回所有的在线设备,格式就是数组,数组晨的每个设备的数据格式对应着{addr:'...',id:'...'}
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function getData ( ) { var httpRequest = null ; if (window .XMLHttpRequest) { httpRequest = new XMLHttpRequest(); } else if (window .ActiveXObject) { httpRequest = new ActiveXObject("Microsoft.XMLHTTP" ); } httpRequest.onreadystatechange = ()=> { if ( httpRequest.readyState === 4 ){ var selectItems = document .getElementsByClassName('equipment-select-item' ) for (var i = 0 ; i < selectItems.length; i++) { selectItems[i].remove() } var tableItems = document .getElementsByClassName('equipment-table-item' ) for (var i = 0 ; i < tableItems.length; i++) { tableItems[i].remove() } var responseData = JSON .parse(httpRequest.responseText) responseData.forEach(equipment => { addSelectorData(equipment) addTableData(equipment) }) } }; httpRequest.open('GET' , '/equipmentArray' ); httpRequest.send(); } getData() setInterval (() => { getData() }, 1000 );
JavaScript能控制界面的变化,本质就是操作DOM对象。新增了一个在线设备,我给下拉框添加一个选择,所以我就创建对应的
option
元素挂到
select
元素之下,所以下拉框就多了这个选项:
1 2 3 4 5 6 7 8 9 function addSelectorData (equipment ) { var selector = document .getElementById('dev-selector' ); var option = document .createElement('option' ); option.className = 'equipment-select-item' option.innerText = equipment.addr+' - ' +equipment.id selector.append(option) }
给按钮绑定一个事件,当点击开灯按钮时就会向服务器端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function postData (equipment,actionString ) { if (!equipment){ return console .log('没设备,不可发送指令' ) } var httpRequest = null ; if (window .XMLHttpRequest) { httpRequest = new XMLHttpRequest(); } else if (window .ActiveXObject) { httpRequest = new ActiveXObject("Microsoft.XMLHTTP" ); } var params = 'action=' +actionString+'&addr=' +equipment.addr+'&id=' +equipment.id httpRequest.open('POST' , '/' ); httpRequest.setRequestHeader('Content-type' , 'application/x-www-form-urlencoded' ); httpRequest.send(params); } document .getElementById('open' ).onclick = ()=> { var equipment = getEquipmentInfo() postData(equipment,'open' ) }
demo1里NodeMCU代码 NodeMCU里的代码,就是不断尝试连接WIFI,连接成功后不断尝试连接TCP服务器,连接成功发送它的唯一ID,然后进入死循环,一直等待服务器端的控制指令,若有就执行开关灯,若无就发送tick
值心跳(数字不断增加)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 while (client.connected()) { if (client.available()) { String line = client.readStringUntil('\n' ); if (line == "1" ) { Serial.println("command:open led." ); digitalWrite(LED_BUILTIN, LOW); client.print("OK" ); } else if (line == "0" ) { Serial.println("command:close led." ); digitalWrite(LED_BUILTIN, HIGH); client.print("OK" ); } } else { Serial.print("tem:" ); Serial.println(tick); client.print(tick); tick++; delay(1000 ); } }
在实际使用时,设备往往拥有一个唯一ID,这个ID用来结合业务逻辑的,比如说我把ID值为001001作为广州市里的某一块设备,ID值002001作为中山市里的一块设备,当广州市的板子坏了,需要替换时,我依旧把ID写成001001,那么它就能继续它的业务逻辑。
demo1服务器端代码 整个程序的入口启动文件就是myapp/bin/www
,界面的代码是myapp/views/index.html
。HTTP服务器就是由Express框架生成的,我们额外添加了TCP服务器的代码myapp/bin/tcp-server.js
。 当浏览器打开网页时,发起HTTP请求(GET /),服务器代码进入到myapp/routes/index.js
发送界面文件:
1 2 3 4 router.get('/' , function (req, res, next ) { res.sendFile('index.html' ,{root :path.join(__dirname , '../views' )}); });
当使用硬件连接到服务器时,硬件发出的第一条数据成为其设备id,所有数据都会存放到
socket.lastValue
中。为了防止不同的读者使用同一个设备ID操作时数据串扰,所以我还额外添加了IP地址以区分。在demo0.1直接使用一个tcpClient变量存放对象,所以它只能支持一个设备,现在改进后使用一个数组存放多个设备就支持多个设备了。所有超过心跳时间没有发数据的都会删除设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 socket.on("data" ,data => { let str = addr+" receive: " + data.toString('ascii' )socket.lastValue = data.toString('ascii' ) console .log(str) if (!socket.id){ socket.id = data.toString('ascii' ) socket.addr = addr addEquipment(socket) } })
1 2 3 4 5 6 7 8 const equipmentArray = []function addEquipment (socket ) { deleteEquipment(socket.id,socket.addr) equipmentArray.push(socket) }
浏览器发出POST请求后,由服务器端代码myapp/routes/index.js
进行处理,找到对应的设备,并发送控制命令,此时硬件接收到命令进行开关灯。根据POST里的body数据:req.body.addr
,req.body.id
,req.body.action
找到对应的设备,转发控制命令数据:
1 2 3 4 5 6 7 8 9 10 router.post('/' ,function (req, res, next ) { let addr = req.body.addr let id = req.body.id let action = req.body.action if (action === 'open' || action === 'close' ){ tcpServer.sentCommand(id,addr,action) } res.json(req.body); })
界面向服务器端发送的值是
open
字体符串,服务器端根据id与IP地址找到那个硬件(虽然代码上写的是数组可兼容多个设备,但理论上是唯一的,只有一个设备。),并向硬件转发时发的是
1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function sentCommand (id,addr,command ) { let equipments = findEquipment(id,addr) if (equipments.length === 0 ){ return ; } if (command === 'open' ){ equipments.forEach((socket )=> { socket.write("1" , 'ascii' ) }) } else if (command === 'close' ){ equipments.forEach((socket )=> { socket.write("0" , 'ascii' ) }) } }
补充说明 JS代码里时不时可以看到声明变量时使用的是const
与let
来取代var
,那是ES6引入的语法,用来解决var
本身的一些缺陷,具体可自行搜索学习。
阶段性作业 光听课以为自己什么都懂,其实只有自己实践过才是真的懂,在实践的过程中才会发现自己不懂的地方。所以请读者自行实现demo1效果,有什么问题请提出来,我会全部解答。
奔向demo2 demo1尽量追求简单入门,所以界面不好看(帅是第一生产力),性能也不足。先说说demo1的问题:
HTTP轮询性能低,时效性差(每一秒轮询代表着极限情况下是有可能2秒才得到最新数据)。
界面丑(优化并会引入图表库Echart,实现数据可视化)
不能显示历史数据(引入数据库)
为了解决以上问题,我们正式奔向demo2。