利用前端语言实现跨平台应用开发似乎是大势所趋,跨平台并不是一个新的概念,“一次编译、到处运行”是老牌服务端跨平台语言Java的一个基本特性。随着时代的发展,无论是后端开发语言还是前端开发语言,一切都在朝着减少工作量,降低工作成本的方向发展。
和后端开发语言不同,利用前端语言实现跨平台有先天的优势,比如后端语言Java跨平台需要将源代码编译为class字节码文件后,再放进 Java 虚拟机运行;而前端语言JavaScript是直接将源代码放进JavaScript解释器运行。这就使得以JavaScript为跨平台语言开发的应用,可移植性非常强大。
目前跨平台技术按照解决方案分类,主要分为 Web 跨平台、容器跨平台、小程序跨平台。这里,我们主要以小程序跨端为例,测试对比IoT小程序和其他小程序在开发和应用上的优缺点。说到小程序,大家肯定想到微信小程序,实际在各大互联网公司:支付宝、百度、头条等等都有自己的小程序,小程序跨平台和Web跨平台十分类似,都是基于前端语言实现,小程序跨平台的优势在于可以调用系统底层能力,例如:蓝牙、相机等,性能方面也优于Web跨平台。
IoT小程序和大多数小程序一样,它是一套跨平台应用显示框架,它利用JS语言低门槛和API标准化大幅度降低了IoT应用的研发难度,其官方框架介绍如下:
IoT小程序在前端框架能力、应用框架能力、图形框架能力都进行了适配和优化。那么接下来,我们按照其官方步骤搭建开发环境,然后结合中央空调数据采集和状态显示的实际应用场景开发物联网小程序应用。
一、IoT小程序开发环境搭建
IoT小程序开发环境搭建一共分为四步,对于前端开发来说,安装NodeJS、配置cnpm、安装VSCode都是轻车熟路,不需要细讲,唯一不同的是按照官方说明安装IoT小程序的模拟器和VSCode开发插件HaaS UI,前期开发环境准备完毕,运行Demo查看一下效果,然后就可以进行IoT小程序应用开发了。
搭建开发环境,安装HaaS UI插件和运行新建项目,出现一下界面说明开发环境搭建成功,就可以进行IoT小程序开发了:
二、开发展示中央空调采集数据和运行状态的IoT小程序应用
中央空调的维保单位会对中央空调进行定期维护保养,定期的维护保养可排出故障隐患,减少事故发生,降低运行费用,延长设备的使用寿命,同时保障正常的工作时序。除了定期的维护保养外,还需要实时监测中央空调的运行参数(温度、累计排污量、不锈钢_腐蚀率等)和运行状态,及时发现中央空调运行过程中某些参数低于或高于报警值的问题,以便及时定位诊断中央空调存在的问题,然后进行相应的维护保养操作。
中央空调的数据采集和展示是典型的物联网应用架构,在中央空调端部署采集终端,通过Modbus通信协议采集中央空调设备参数,然后再由采集终端通过MQTT消息发送的我们的云端服务器,云端服务器接收到MQTT消息后转发到消息队列Kafka中,由云服务器上的自定义服务应用订阅Kafka主题,再存储到我们时序数据库中。
下图展示了物联网应用的整体架构和IoT小程序在物联网架构中的位置:
IoT小程序框架作为跨平台应用显示框架,顾名思义,其在物联网应用中主要作为显示框架开发。在传统应用中,我们使用微信小程序实现采集数据和运行状态的展示。而IoT小程序支持部署在AliOS Things、Ubuntu、Linux、MacOS、Window等系统中,这就使得我们可以灵活的将IoT小程序部署到多种设备终端中运行。
下面将以阿里云ASP-80智显面板为例,把展示中央空调采集数据和运行状态的IoT小程序部署在阿里云ASP-80智显面板中。
我们将从IoT小程序提供的前端框架能力、应用框架能力、图形框架能力来规划相应的功能开发。
- 前端框架能力
IoT小程序采用Vue.js(v2.6.12)开源框架,实现了W3C标准的标签和样式子集;定义了四个应用生命周期,分别是:onLaunch,onShow,onHide,onDestroy;定义了十四个前端基础组件,除了基础的CSS样式支持外,还提供了对Less的支持;Net网络请求通过框架内置的JSAPI实现。
为了快速熟悉IoT小程序框架的开发方式,我们将在VSCode中导入官方公版案例,并以公版案例为基础框架开发我们想要的功能。
简单实现通过网络请求获取中央空调采集数据并展示:
- 在VSCode编辑器中导入从IoT小程序官网下载的公版案例,下载地址。
- 因为IoT小程序前端框架使用的是Vue.js框架,所以在新增页面时也是按照Vue.js框架的模式,将页面添加到pages目录。我们是空调项目的IoT小程序,所以这里在pages目录下新增air-conditioning目录用于存放空调IoT小程序相关前端代码。
- 在app.json中配置新增的页面,修改pages项,增加"air-conditioning": “pages/air-conditioning/index.vue”。
{"pages": {
......"air-conditioning": "pages/air-conditioning/index.vue",
......},"options": {"style": {"theme": "theme-dark"}}
}
- 在air-conditioning目录下新增index.vue前端页面代码,用于展示空调的采集数据是否正常及历史曲线图。设计需要开发的界面如下,页面的元素有栅格布局、Tabs 标签页、Radio单选框、日期选择框、曲线图表等元素。
- 首先是实现Tabs标签页,IoT小程序没有Tabs组件,只能自己设置多个Text组件自定义样式并添加click事件来实现。
<div class="tab-list"><fl-icon name="back" class="nav-back" @click="onBack" /><textv-for="(item, index) in scenes":key="index":class="'tab-item' + (index === selectedIndex ? ' tab-item-selected' : '')"@click="tabSelected(index)">{{ item }}</text></div>
......data() {return {scenes: ["设备概览", "实时数据", "数据统计", "状态统计"],selectedIndex: 0};},
......
6、添加采集数据显示列表,在其他小程序框架中,尤其是以Vue.js为基础框架的小程序框架,这里有成熟的组件,而IoT小程序也是需要自己来实现。
<template><div class="scene-wrapper" v-if="current"><div class="label-temperature-wrapper top-title"><div class="label-temperature-wrapper left-text"><text class="label-temperature">设备编码:</text><text class="label-temperature-unit">97306000000000005{{content}}</text></div><div class="label-temperature-wrapper right-text"><text class="label-temperature">数据日期:</text><text class="label-temperature-unit">2023-03-11 23:59:59{{content}}</text></div></div><div class="main-wrapper"><div class="section"><div class="demo-block icon-block"><div class="icons-item" v-for="(value, key, index) in IconTypes" :key="index"><div class="label-title-wrapper"><text class="label-title left-text">电导率</text><text class="label-title-unit right-text" style="padding-right: 5px;">正常</text></div><div class="label-zhibiao-wrapper"><text class="label-zhibiao">当前值:</text><text class="label-zhibiao-unit">56.36{{content}}</text></div><div class="label-zhibiao-wrapper" style="margin-bottom: 10px;"><text class="label-zhibiao">目标值:</text><text class="label-zhibiao-unit">63.32{{content}}</text></div></div></div></div></div></div>
</template>
在开发过程中发现,IoT小程序对样式的支持不是很全面,本来想将
7. 界面实现之后,需要发送数据请求,来查询采集数据并显示在界面上。IoT小程序通过框架内置JSAPI的Net网络提供网络请求工具。目前从官方文档和代码中来看,官方框架只提供了http请求,没有提供物联网中常用的WebSocket和MQTT工具,估计需要自定义扩展系统JSAPI实现其他网络请求。
created() {const http = $falcon.jsapi.httphttp.request({url: 'http://服务域名/device/iot/query/data/point',data: {'deviceId': '97306000000000005','rangeType': 'mo','lastPoint': '1','beginDateTime': '2023-02-10+16:09:42','endDateTime': '2023-03-12+16:09:42'},header: {'Accept': 'application/json;charset=UTF-8','Accept-Encoding': 'gzip, deflate, br','Content-Type': 'application/json;charset=UTF-8','Authorization': '有效token'}}, (response) => {console.log(response)var obj = JSON.parse(response.result)console.log(obj.success)console.log(JSON.parse(obj.data))});},
按照官方要求编写http请求,发现默认未开启https请求:Protocol “https” not supported or disabled in libcurl。切换为http请求,返回数据为乱码,设置Accept-Encoding和Accept为application/json;charset=UTF-8仍然无效,且返回数据为JSON字符串,需要自己手动使用JSON.parse()进行转换,对于习惯于应用成熟框架的人来说,十分不友好。想了解更多关于 $falcon.jsapi.http的相关配置和实现,但是官方文档只有寥寥几句,没有详细的说明如何使用和配置,以及http请求中遇到一些常见问题的解决方式。
8. IoT小程序框架提供画布组件,原则上来讲可以实现常用的曲线图表功能,但是如果使用其基础能力从零开始开发一套图表系统,耗时又耗力,所以这里尝试引入常用的图表组件库ECharts,使用ECharts在IoT小程序上显示曲线图表。
- 执行cnpm install echarts --save安装echarts组件
cnpm install echarts --save
- 新建echarts配置文件,按需引入
// 加载echarts,注意引入文件的路径
import echarts from 'echarts/lib/echarts'// 再引入你需要使用的图表类型,标题,提示信息等
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/pie'
import 'echarts/lib/component/legend'
import 'echarts/lib/component/title'
import 'echarts/lib/component/tooltip'export default echarts
- 新增echarts组件ChartDemo.vue
<template><div ref="chartDemo" style="height:200px;" ></div>
</template>
<script>import echarts from '@/utils/echarts-config.js'const ChartDemo = {name: 'ChartDemo',data() {return {chart: null}},watch: {option: {handler(newValue, oldValue) {this.chart.setOption(newValue)},deep: true}},mounted() {this.chart = echarts.init(this.$refs.chartDemo)},methods: {setOption(option) {this.chart && this.chart.setOption(option)},throttle(func, wait, options) {let time, context, argslet previous = 0if (!options) options = {}const later = function() {previous = options.leading === false ? 0 : new Date().getTime()time = nullfunc.apply(context, args)if (!time) context = args = null}const throttled = function() {const now = new Date().getTime()if (!previous && options.leading === false) previous = nowconst remaining = wait - (now - previous)context = thisargs = argumentsif (remaining <= 0 || remaining > wait) {if (time) {clearTimeout(time)time = null}previous = nowfunc.apply(context, args)if (!time) context = args = null} else if (!time && options.trailing !== false) {time = setTimeout(later, remaining)}}return throttled}}}export default ChartDemo
</script>
- 在base-page.js中注册全局组件
......
import ChartDemo from './components/ChartDemo.vue';
export class BasePage extends $falcon.Page {constructor() {super()}beforeVueInstantiate(Vue) {......Vue.component('ChartDemo', ChartDemo);}
}
- 新建空调采集数据展示页history-charts.vue,用于展示Echarts图表
<template><div class="scene-wrapper" v-if="current"><div class="brightness-wrap"><ChartBlock ref="chart2"></ChartBlock></div></div>
</template><script>
let option2 = {title: {text: '某站点用户访问来源',subtext: '纯属虚构',left: 'center'},tooltip: {trigger: 'item',formatter: '{a} <br/>{b} : {c} ({d}%)'},legend: {orient: 'vertical',left: 'left',data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']},series: [{name: '访问来源',type: 'pie',radius: '55%',center: ['50%', '60%'],data: [{ value: 335, name: '直接访问' },{ value: 310, name: '邮件营销' },{ value: 234, name: '联盟广告' },{ value: 135, name: '视频广告' },{ value: 1548, name: '搜索引擎' }],emphasis: {itemStyle: {shadowBlur: 10,shadowOffsetX: 0,shadowColor: 'rgba(0, 0, 0, 0.5)'}}}]
}export default {props:{current:{type:Boolean,default:false}},data() {return {};},methods: {},mounted: function() {this.$refs.chart2.setOption(option2)}
};
</script>
-
执行HaaS UI: Build-Debug ,显示打包成功
-
执行HaaS UI: Simulator ,显示“当前HaaS UI: Simulator任务正在执行,请稍后再试”
本来想在模拟器上看一下Echarts显示效果,但是执行HaaS UI: Simulator时一直显示任务正在执行。然后以为是系统进程占用,但是重启、关闭进程等操作一系列操作下来,仍然显示此提示,最后将Echarts代码删除,恢复到没有Echarts的状态,又可以执行了。这里不清楚是否是IoT小程序不支持引入第三方图表组件,从官方文档中没有找到答案。后来又使用echarts的封装组件v-charts进行了尝试,结果依然不能展示。
如果不能使用第三方组件,那么只能使用IoT官方小程序提供的画布组件来自己实现图表功能,官方提供的画布曲线图示例。
9. 通过IoT小程序提供的组件分别实现显示中央空调采集数据的实时数据、数据统计、状态统计图表。
-实现实时数据折线图
<template><div class="scene-wrapper" v-show="current"><div class="main-wrapper"><div class="label-temperature-wrapper top-title"><div class="label-temperature-wrapper left-text"><text class="label-temperature">设备编码:</text><text class="label-temperature-unit">97306000000000005</text></div><div class="label-temperature-wrapper right-text"><text class="label-temperature">数据日期:</text><text class="label-temperature-unit">2023-03-11 23:59:59</text></div></div><canvas ref="c2" class="canvas" width="650" height="300"></canvas></div></div>
</template><script>export default {name: "canvas",props: {},data() {return {};},mounted() {this.c2();},methods: {c2() {let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c2) : this.$refs.c1.getContext("2d");let arr = [{key:'01:00',value:61.68},{key:'02:00',value:83.68},{key:'03:00',value:56.68},{key:'04:00',value:86.68},{key:'05:00',value:53.68},{key:'06:00',value:41.68},{key:'07:00',value:33.68}];this.drawStat(ctx, arr);},//该函数用来绘制折线图drawStat(ctx, arr) {//画布的款高var cw = 700;var ch = 300;//内间距paddingvar padding = 35;//原点,bottomRight:X轴终点,topLeft:Y轴终点var origin = {x:padding,y:ch-padding};var bottomRight = {x:cw-padding,y:ch-padding};var topLeft = {x:padding,y:padding};ctx.strokeStyle='#FF9500';ctx.fillStyle='#FF9500';//绘制X轴ctx.beginPath();ctx.moveTo(origin.x,origin.y);ctx.lineTo(bottomRight.x,bottomRight.y);//绘制X轴箭头ctx.lineTo(bottomRight.x-10,bottomRight.y-5);ctx.moveTo(bottomRight.x,bottomRight.y);ctx.lineTo(bottomRight.x-10,bottomRight.y+5);//绘制Y轴ctx.moveTo(origin.x,origin.y);ctx.lineTo(topLeft.x,topLeft.y);//绘制Y轴箭头ctx.lineTo(topLeft.x-5,topLeft.y+10);ctx.moveTo(topLeft.x,topLeft.y);ctx.lineTo(topLeft.x+5,topLeft.y+10);//设置字号var color = '#FF9500';ctx.fillStyle=color;ctx.font = "13px scans-serif";//设置字体//绘制X方向刻度//计算刻度可使用的总宽度var avgWidth = (cw - 2*padding - 50)/(arr.length-1);for(var i=0;i<arr.length;i++){//循环绘制所有刻度线if(i > 0){//移动刻度起点ctx.moveTo(origin.x+i*avgWidth,origin.y);//绘制到刻度终点ctx.lineTo(origin.x+i*avgWidth,origin.y-10);}//X轴说明文字:1月,2月...var txtWidth = 35;ctx.fillText(arr[i].key,origin.x+i*avgWidth-txtWidth/2 + 10,origin.y+20);}//绘制Y方向刻度//最大刻度maxvar max = 0;for(var i=0;i<arr.length;i++){if(arr[i].value>max){max=arr[i].value;}}console.log(max);/*var max = Math.max.apply(this,arr);console.log(max);*/var avgValue=Math.floor(max/5);var avgHeight = (ch-padding*2-50)/5;for(var i=1;i<arr.length;i++){//绘制Y轴刻度ctx.moveTo(origin.x,origin.y-i*avgHeight);ctx.lineTo(origin.x+10,origin.y-i*avgHeight);//绘制Y轴文字var txtWidth = 40;ctx.fillText(avgValue*i,origin.x-txtWidth-5,origin.y-i*avgHeight+6);}//绘制折线for(var i=0;i<arr.length;i++){var posY = origin.y - Math.floor(arr[i].value/max*(ch-2*padding-50));if(i==0){ctx.moveTo(origin.x+i*avgWidth,posY);}else{ctx.lineTo(origin.x+i*avgWidth,posY);}//具体金额文字ctx.fillText(arr[i].value,origin.x+i*avgWidth,posY)}ctx.stroke();//绘制折线上的小圆点ctx.beginPath();for(var i=0;i<arr.length;i++){var posY = origin.y - Math.floor(arr[i].value/max*(ch-2*padding-50));ctx.arc(origin.x+i*avgWidth,posY,4,0,Math.PI*2);//圆心,半径,画圆ctx.closePath();}ctx.fill();}}};</script>
-数据统计图表
<template><div class="scene-wrapper" v-show="current"><div class="main-wrapper"><div class="label-temperature-wrapper top-title"><div class="label-temperature-wrapper left-text"><text class="label-temperature">设备编码:</text><text class="label-temperature-unit">97306000000000005</text></div><div class="label-temperature-wrapper right-text"><text class="label-temperature">数据日期:</text><text class="label-temperature-unit">2023-03-13 20:29:36</text></div></div><canvas ref="c1" class="canvas" width="650" height="300"></canvas></div></div>
</template>
<script>export default {name: "canvas",props: {},data() {return {};},mounted() {this.c1();},methods: {c1() {let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c1) : this.$refs.c1.getContext("2d");this.draw(ctx);},draw(ctx){var x0=30,//x轴0处坐标y0=280,//y轴0处坐标x1=700,//x轴顶处坐标y1=30,//y轴顶处坐标dis=30;//先绘制X和Y轴ctx.beginPath();ctx.lineWidth=1; ctx.strokeStyle='#FF9500';ctx.fillStyle='#FF9500';ctx.moveTo(x0,y1);//笔移动到Y轴的顶部ctx.lineTo(x0,y0);//绘制Y轴ctx.lineTo(x1,y0);//绘制X轴ctx.stroke();//绘制虚线和Y轴值 var yDis = y0-y1;var n=1;ctx.fillText(0,x0-20,y0);//x,y轴原点显示0while(yDis>dis){ctx.beginPath();//每隔30划一个虚线ctx.setLineDash([2,2]);//实线和空白的比例ctx.moveTo(x1,y0-dis);ctx.lineTo(x0,y0-dis);ctx.fillText(dis,x0-20,y0-dis);//每隔30划一个虚线dis+=30;ctx.stroke();}var xDis=30,//设定柱子之前的间距width=40;//设定每个柱子的宽度//绘制柱状和在顶部显示值for(var i=0;i<12;i++){//假设有8个月ctx.beginPath();var color = '#' + Math.random().toString(16).substr(2, 6).toUpperCase();//随机颜色ctx.fillStyle=color;ctx.font = "13px scans-serif";//设置字体var height = Math.round(Math.random()*220+20);//在一定范围内随机高度var rectX=x0+(width+xDis)*i,//柱子的x位置rectY=height;//柱子的y位置ctx.color='#FF9500';ctx.fillText((i+1)+'月份',rectX,y0+15);//绘制最下面的月份稳住ctx.fillRect(rectX,y0, width, -height);//绘制一个柱状ctx.fillText(rectY,rectX+10,280-rectY-5);//显示柱子的值}},}};
</script>
-状态统计图表
<template><div class="scene-wrapper" v-show="current"><div class="main-wrapper"><div class="label-temperature-wrapper top-title"><div class="label-temperature-wrapper left-text"><text class="label-temperature">设备编码:</text><text class="label-temperature-unit">97306000000000005</text></div><div class="label-temperature-wrapper right-text"><text class="label-temperature">数据日期:</text><text class="label-temperature-unit">2023-03-13 20:29:36</text></div></div><canvas ref="c3" class="canvas" width="600" height="300"></canvas></div></div>
</template>
<script>export default {name: "canvas",props: {},data() {return {};},mounted() {this.c3();},methods: {c3() {let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c3) : this.$refs.c3.getContext("2d");this.drawPie(ctx);},drawPie(pen){//假数据var deg = Math.PI / 180var arr = [{name: "开机",time: 8000,color: '#7CFF00'},{name: "关机",time: 1580,color: '#737F9C'},{name: "空闲",time: 5790,color: '#0ECC9B'},{name: "故障",time: 4090,color: '#893FCD'},{name: "报警",time: 2439,color: '#EF4141'},];//总价pen.translate(30,-120);arr.tatol = 0;for (let i = 0; i < arr.length; i++) {arr.tatol = arr.tatol + arr[i].time}var stardeg = 0arr.forEach(el => {pen.beginPath()var r1 = 115pen.fillStyle = el.colorpen.strokeStyle='#209AAD';pen.font = "15px scans-serif";//求出每个time的占比var angle = (el.time / arr.tatol) * 360//利用占比来画圆弧pen.arc(300, 300, r1, stardeg * deg, (stardeg + angle) * deg)//将圆弧与圆心相连接,形成扇形pen.lineTo(300, 300)var r2 = r1+10;if(el.name === '关机' || el.name === '空闲'){r2 = r1+30}//给每个扇形添加数组的namevar y1 = 300 + Math.sin((stardeg + angle) * deg-angle*deg/2 ) *( r2)var x1 = 300 + Math.cos((stardeg + angle) * deg-angle*deg/2 ) * (r2)pen.fillText(`${el.name}`, x1, y1)stardeg = stardeg + anglepen.fill()pen.stroke()});},}};
</script>
三、将IoT小程序更新到ASP-80智显面板查看运行效果
将IoT小程序更新到ASP-80智显面板,在硬件设备上查看IoT应用运行效果。如果是使用PC端初次连接,那么需要安装相关驱动和配置,否则无法使用VSCode直接更新IoT小程序到ASP-80智显面板。
- 如果使用Win10将IoT小程序包更新到ASP-80智显面板上,必须用到CH340串口驱动,第一次通过TypeC数据线连接设备,PC端设备管理器的端口处不显示端口,这时需要下载Windows版本的CH340串口驱动下载链接 。
- 将下载的驱动文件CH341SER.ZIP解压并安装之后,再次查看PC端设备管理器端口就有了USB Serial CH340端口。
- 使用SourceCRT连接ASP-80智显面板,按照官方文档说明,修改配置文件,连接好WiFi无线网,下一步通过VSCode直接更新IoT小程序到ASP-80智显面板上查看测试。
4. 所有准备工作就绪后,点击VSCode的上传按钮HaaS UI: Device,将应用打包并上传至ASP-80智显面板。在选择ip地址框的时候,输入我们上一步获取到的ip地址192.168.1.112,其他参数保持默认即可,上传成功后,VSCode控制台提示安装app成功。
- IoT小程序安装成功之后就可以在ASP-80智显面板上查看运行效果了。
综上所述,IoT小程序框架在跨系统平台(AliOS Things、Ubuntu、Linux、MacOS、Window等)方面提供了非常优秀的基础能力,应用的更新升级提供了多种方式,在实际业务开发过程中可以灵活选择。IoT小程序框架通过JSAPI提供了调用系统底层应用的能力,同时提供了自定义JSAPI扩展封装的方法,这样就足够业务开发通过自定义的方式满足特殊的业务需求。
虽然多家互联网公司都提供了小程序框架,但在128M 128M这样的低资源设备里输出,IoT小程序是比较领先的,它不需要另外下载APP作为小程序的容器,降低了资源的消耗,这一点是其他小程序框架所不能比拟的。
但是在前端框架方面,实用组件太少。其他小程序已发展多年,基于基础组件封装并开源的前端组件应用场景非常丰富,对于中小企业来说,习惯于使用成熟的开源组件,如果使用IoT小程序开发物联网应用可能需要耗费一定的人力物力。既然是基于Vue.js的框架,却没有提供引入其他优秀组件的文档说明和示例,不利于物联网应用的快速开发,希望官方能够完善文档,详细说明IoT小程序开发框架配置项,将来能够提供更多的实用组件。