服务器网页版上位机设计 - 03 - 上位机 (完结)

服务器网页版上位机设计 03 上位机 (完结)

本设计主要涉及三个方面: 服务器,网页版,上位机.

书接上回,介绍完网页页面的设计,现在来说说上位机的功能设计.

也就是js文件的内容编写.

1.获取html对象

  • 在网页设计时,设置了4个按钮,现在为它们添加回调函数.

  • 首先需要在js中获取html的元素.有多种方法,我选择类似css类选择器的方法,这是为了和我的css筛选方式相同.

参考: << 【JavaScript】 JS中获取HTML元素值的三种方法_L5Butterfly的博客-CSDN博客_js 获取元素值 >>

  • 1.js文件中添加如下代码:
// 通过选择器获取一个元素
const f1 =  document.querySelector('div.d11 > form');
const f2 =  document.querySelector('div.d12 > div.d21 > form');
const f3 =  document.querySelector('div.d12 > div.d22 > form');
  • 然后就可以通过这3个对象获取元素了.想打印看看是什么内容?可以直接在浏览器控制台中输入代码,这个控制台其实就是js环境,类似python,逐行编译.
  • 输入f1就能看到提示,第一行代表有创建这个变量.第二和第三行代表输入历史值,我之前测试时有输入过,现在快捷显示.然后左边的页面能看到<form>标签元素高亮了,发现并没有包裹所有表单元素,因为我们没有设置大小,也不需要设置.

在这里插入图片描述

  • 当你按下回车,会发现打印的就是html内的文本内容(没想到居然就是文本内容),再把鼠标移动到个别元素上,会再次高亮个别元素.

在这里插入图片描述

  • 那么怎么方便明了的获取<form>内的元素呢?可以直接通过html中设置的name元素,获取到个别元素.不需要再通过选择器等方法获取了.而且很关键的是,这个name元素是字符串的形式,是支持中文的.用起来也很像访问字典数据.
  • 例如,输入f1["波特率"]就能获取到波特率的元素.非常方便.接下来就可以通过这个方法获取按钮元素,然后添加回调函数了.

在这里插入图片描述

2.添加回调函数

  • js中对html进行操作,这一行为有个专业术语:DOM,(Document Object Model) 译为文档对象模型.

参考: << HTML DOM 教程 | 菜鸟教程 (runoob.com) >>

  • 想介绍的东西都有多,不过不跑题了,就说添加回调函数,也就是按下按键时,触发的函数.关键词是onclick .有两种方法,一种是直接为onclick属性赋值,另一个是指定为onclick赋值.(我一开始用后者,回来才知道前者,所以都介绍一下.)

参考: << HTML DOM 事件 | 菜鸟教程 (runoob.com) >>

<< HTML DOM addEventListener() 方法 | 菜鸟教程 (runoob.com) >>

// 直接赋值,简单方便
f1["端口号"].onclick = function(){};
// 指定赋值,灵活
f1["端口号"].addEventListener("click", function(){});
  • 这里选择使用前者,方便快捷.添加代码前,先规划好每个按钮的不同状态,一开始要先将框架定下来,之后再一点点添加内容.这样代码写起来才不会乱.

f1[“开关键”] -> 打开 / 关闭

f2[“按钮”] -> 开始读取 / 停止读取

f3[“按钮”] -> 开始发送 / 停止发送

f1[“端口号”] -> 显示当前端口ID

(本来想做个选择端口的功能的,但是打开端口时就已经包含了选择功能,虽然可以分开,但是因为无法识别端口名字,只能获取到端口id,所以选择端口只能看着一堆数字也分辨不出来.所以干脆就不分开了.然后这个端口按钮就只是用来查看已经连接的端口id)

  • 设计好大概框架后,就可以写代码了,在1.js中继续添加如下代码:
// 按钮的回调函数
f1["端口号"].onclick = function()
{
    if (f1["开关键"].value == "打开")
    {
        alert(`未打开串口`); // 弹窗:提示没有打开串口
    }
    else if (f1["开关键"].value == "关闭") 
    {
        alert(`已打开串口\n${2048}`); // 弹窗:反馈已打开串口的信息
    }
    else
    {
        new Error(this); // 意料之外,报错
    }
};
f1["开关键"].onclick = function()
{
    if (f1["开关键"].value == "打开")
    {
        console.log(`开启串口`); // 以下调用 开启串口 函数

        f1["开关键"].value = "关闭"; // 执行完毕,最后修改按钮文本
    }
    else if (f1["开关键"].value == "关闭") 
    {
        console.log(`关闭串口`); // 以下调用 关闭串口 函数

        f1["开关键"].value = "打开"; // 执行完毕,最后修改按钮文本
    }
    else
    {
        new Error(this); // 意料之外,报错
    }
};
f2["按钮"].onclick = function()
{
    if (f2["按钮"].value == "开始接收")
    {
        console.log(`开始接收`); // 以下调用 开始接收 函数

        f2["按钮"].value = "停止接收"; // 执行完毕,最后修改按钮文本
    }
    else if (f2["按钮"].value == "停止接收") 
    {
        console.log(`停止接收`); // 以下调用 停止接收 函数

        f2["按钮"].value = "开始接收"; // 执行完毕,最后修改按钮文本
    }
    else
    {
        new Error(this); // 意料之外,报错
    }
};
f3["按钮"].onclick = function()
{
    if (f3["按钮"].value == "开始发送")
    {
        console.log(`开始发送`); // 以下调用 开始发送 函数

        f3["按钮"].value = "停止发送"; // 执行完毕,最后修改按钮文本
    }
    else if (f3["按钮"].value == "停止发送") 
    {
        console.log(`停止发送`); // 以下调用 停止发送 函数

        f3["按钮"].value = "开始发送"; // 执行完毕,最后修改按钮文本
    }
    else
    {
        new Error(this); // 意料之外,报错
    } 
};
  • 上面代码中还用到了new Error(this),这个是反馈行号用的,可以自行在控制台输入查看效果.

参考: << js获取当前代码行号_60rzvvbj的博客-CSDN博客_js获取当前行数 >>

(其实在浏览器中,随便打印输出点东西就可以了,因为控制台会显示执行的行号,还能快捷跳转查看)

3.Web Serial API

  • 一哟一哟,终于开始正片内容了.我们将开始网页访问串口的功能实现.以下内容写在2.js文件中.在上一篇讲网页设计时已经完成脚本导入的步骤了.所以现在2个文件可以互相访问.没错,并不是只有1.js能访问2.js,而是二者可以互相访问,这也是使用requirejs的一大好处.
  • 推荐一系列教程,大部分其实都是源自官方的手册翻译.大同小异.

推荐参考:

<< Web Serial API,web端通过串口与硬件通信 - 掘金 (juejin.cn) >>

<< 什么,网页也能直接与硬件通信?Web Serial API!|8月更文挑战 - 掘金 (juejin.cn) >>

<< Web Serial API - Web APIs | MDN (mozilla.org) >>

<< Web Serial API (wicg.github.io) >>

  • 在开始前还需要先准备一下串口工具,因为手头可能没有能提供串口连接的硬件设备.使用虚拟串口做实验更加方便.使用VSPD创建虚拟串口,然后网页版上位机连接一个,串口调试助手连接一个.

推荐参考:

<< Virtual Serial Port Driver 10 破解版 - 星光的博客 (starxg.com) >> (半天找不到免安装,就安装一下吧)

<< 友善串口调试助手下载与安装 - 知乎 (zhihu.com) >> (找了个可以免安装的)

3.1.异步&延时

  • 这里介绍一个重要概念,异步.为了防止程序堵塞,浏览器的串口模块是需要异步使用的.使用关键词asyncawait ,使用的时候只需要注意两点即可,1)不能用返回值,2)await必须在async函数内使用.

参考: << JavaScript 入门笔记 - 下 - 函数语法_L建豪 忄YH的博客-CSDN博客 >>

  • 在串口通讯时,也很难避免会用到延时等待.在js中很特殊,没有直接的sleep等待函数.只有一个创建异步的setTimeout函数,能创建一个x毫秒后执行的异步函数.不过可以自行嵌套创建.

参考: << javascript里的sleep()方法_clschen的博客-CSDN博客_js sleep() >>

  • 2.js中添加如下内容:
// 延时函数,异步中调用
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
// 调用时要加上 await,代表只能在异步函数 async 中使用.
// await sleep(1000);
  • 如果是需要多段延时等待的话,sleep使用会很方便,如果只是一段的话,还是直接使用setTimeout会更加直接,也不需要使用await关键词.

3.2.打开串口

推荐参考: << Web Serial API (wicg.github.io) >> 4.4 open() method ;

  • 2.js中添加如下内容:
/* 说明: 全局变量
*/
let port_g = null; // 储存端口对象
let reader_g = null; // 储存读对象
let writer_g = null; // 储存写对象

/* 说明: 选择端口,并打开
*  参数: 波特率
*/
async function open(baudRate)
{
    if (!("serial" in navigator)) // 保护措施
    {
        console.log(`本浏览器不支持串口功能.`);
        return; // 跳过剩余内容
    }

    console.log(`选择端口`);
    try
    {
        console.log(`选择成功`);
        port_g = await navigator.serial.requestPort();
    }
    catch
    {
        console.log(`选择失败`);
        port_g = null;
    }

    if (port_g != null) // 保护措施
    {
        console.log(`打开端口`);
        await port_g.open({
            baudRate: baudRate, 	    // 波特率
            dataBits: 8, 				// 数据位
            stopBits: 1, 				// 停止位
            parity: 'none', 			// 奇偶校验
            bufferSize = 255,			// 缓冲区大小
            flowControl: 'none'			// 流控模式
        });
    }
}

  • 需要注意的有3点: 1)选择端口和打开端口都是带有await关键词的异步函数,所以要使用async函数.2)选择端口时需要使用try关键词,因为如果没有在弹窗中选择端口时,会引发错误.3)open()函数的传参是字典,其中波特率是没有默认值的,其他参数都有默认值.不写也可以.
  • 然后在1.js文件内的按钮回调函数中调用.
...
f1["开关键"].onclick = function()
{
    if (f1["开关键"].value == "打开")
    {
        console.log(`开启串口`); // 以下调用 开启串口 函数

        open(f1["波特率"].options[f1["波特率"].selectedIndex].value); // 取出波特率,然后打开串口
        
        f1["开关键"].value = "关闭"; // 执行完毕,最后修改按钮文本
    }
...
  • 这个时候发现一个bug,虽然不选择端口后控制台不会报错,但是按钮的文本却发生了改变.如果打开失败,应该是不改变文本.

在这里插入图片描述

  • 如果是以前c语言的思维,应该有个返回值,成功返回真,失败返回假.但是因为异步函数的原因,返回值并不好用.结合js的特点,用传入函数的方式解决.
  • 2.js文件修改成如下:
async function open(baudRate, fun)
{
...
    if (port_g != null) // 保护措施
    {
        console.log(`打开端口`);
        await port_g.open({
            baudRate: baudRate, 	    // 波特率
            dataBits: 8, 				// 数据位
            parity: 'none', 			// 奇偶校验
            stopBits: 1, 				// 停止位
            flowControl: 'none'			// 流控模式
        });
    }

    if (port_g != null) // 保护措施
    {
        fun(); // 执行完毕,最后附带执行
    }
}
  • 1.js文件修改成如下:
...
f1["开关键"].onclick = function()
{
    if (f1["开关键"].value == "打开")
    {
        console.log(`开启串口`); // 以下调用 开启串口 函数

        open(f1["波特率"].options[f1["波特率"].selectedIndex].value, ()=>
        {
            f1["开关键"].value = "关闭"; // 执行完毕,最后修改按钮文本
        }); // 取出波特率,然后打开串口
    }
...
  • 这样就完美许多了,而且很灵活,fun()函数的调用甚至可以根据需要而变动.

3.3.端口信息

推荐参考: << Web Serial API (wicg.github.io) >> 4.3 getInfo() method ;

<< JavaScript中判断一个对象是否为一个类的实例_oldjwu的博客-CSDN博客_js 判断是否是实例 >>

  • 2.js中添加如下内容:
/* 说明: 获取端口信息
*  参数: 无
*/
function getInfo(fun)
{
    if (port_g.constructor == SerialPort) // 保护措施,判断对象是否为类的实例
    {
        let usbVendorId = port_g.getInfo()["usbVendorId"]; // 供应商 ID
        if (usbVendorId == undefined)
        {
            usbVendorId = "未定义"; // 虚拟串口或部分杂牌usb设备可能没有设置这个参数
        }

        let usbProductId = port_g.getInfo()["usbProductId"]; // 产品 ID
        if (usbProductId == undefined)
        {
            usbProductId = "未定义";
        }

        fun(usbVendorId, usbProductId);
    }
}
  • 获取usb设备id的主要作用其实是用在打开端口上.目前我的设计是选择授权哪个端口然后就打开哪个端口.官方推荐的设计是,可以授权多个端口,只要网页不关闭授权就有效.然后记录ID,在打开端口时可以通过ID过滤器选择打开哪个端口.(我个人感觉这样好麻烦啊,还是整一对一比较方便,一般人使用应该不会有一拖多的情况吧.)
  • 这2个usb设备id都可以在pc的设备管理器-端口(COM和LPT)中查看得到.不过因为我的是虚拟串口,且我忘记是哪个选项了,所以没找到.如果你有硬件设备的话可以找找.验证验证.

在这里插入图片描述

  • 另外注意,getInfo()是不需要await关键字的,所以可以不需要async.如果要加上也可以.这里我还是继续采用fun()传参的方式.
  • 1.js文件修改成如下:(箭头函数真方便)
...
f1["端口号"].onclick = function()
{
    if (f1["开关键"].value == "打开")
    {
        alert(`未打开串口`); // 提示没有打开串口
    }
    else if (f1["开关键"].value == "关闭") 
    {
        getInfo((usbVendorId, usbProductId)=>
        {
            alert(`已打开串口:
            供应商 ID : ${usbVendorId}
            产品 ID   : ${usbProductId}`); // 反馈已打开串口的信息
        });
    }
...

3.4.写数据

推荐参考: << Web Serial API (wicg.github.io) >> 4.6 writable attribute ;

  • 2.js中添加如下内容:
/* 说明: 写数据
*  参数: 数据
*/
async function write(data, fun)
{
    if (port_g.constructor == SerialPort) // 保护措施
    {
        const encoder = new TextEncoder(); // 创建实例
        writer_g = port_g.writable.getWriter(); // 创建写对象
        writer_g.write(encoder.encode(data)); // 开始发送
        // writer_g.close(); // 写终止,停止发送内容,这里用不到,循环发送时才可能用到,后面讲解
        writer_g.releaseLock(); // 写完毕,允许稍后关闭端口
        writer_g = null; // 清空变量

        console.log(data);
        fun(); // 执行完毕,额外执行
    }
}
  • 1.js文件修改成如下:
... 
f3["按钮"].onclick = function()
{
    if (f3["按钮"].value == "开始发送")
    {
        console.log(`开始发送`); // 以下调用 开始发送 函数
        f3["按钮"].value = "停止发送"; // 开始执行,修改按钮文本

        write(f3["发送"].value, ()=>
        {
            console.log(`停止发送`);
            f3["按钮"].value = "开始发送"; // 执行完毕,重置文本
        });
    }
    else if (f3["按钮"].value == "停止发送") 
    {
        console.log(`停止发送`); // 以下调用 停止发送 函数

        // 暂不写手动关闭发送的功能,所以不可以手动将按钮从"停止发送"改为"开始发送".

        // f3["按钮"].value = "开始发送";  // 使用 disabled 属性禁用按钮,或是直接屏蔽这一行,也能起到同样效果
    }
...
  • 注意,记得调用writer_g.releaseLock(),如果没有调用这个函数,将无法关闭端口.而调用这个之前写任务必须结束,如果没有结束,需要调用writer_g.close()函数来手动结束写任务.不过如果只是发送一段数据,一般不会用得到.应该是在循环的 写数据流 中会用到.大概.
  • 总得来说简单的写数据实现挺容易的,以下是效果图.网页端选择COM1.

在这里插入图片描述

3.5.读数据

推荐参考: << Web Serial API (wicg.github.io) >> 4.5 readable attribute

  • 2.js中添加如下内容:
/* 说明: 读数据
*  参数: 最大读取时间
*/
async function read(time, fun)
{
    let data = []; // 储存接收数据

    if (port_g.constructor == SerialPort) // 保护措施
    {
        if (port_g.readable) // 判断是否可用,只执行一次,不使用 while
        {
            reader_g = port_g.readable.getReader(); // 创建读对象

            setTimeout(()=>
            {
                if (reader_g != null)
                {
                    reader_g.cancel(); // 读终止,停止读任务,
                }
            },time); // 达到最大读取时间后手动关闭

            console.log(`读取开始`);
            try 
            {
                while (true) 
                {
                    const { value, done } = await reader_g.read(); // 执行读任务
                    if (done) // 读取成功或失败时会返回,不然一直阻塞在死循环
                    {
                        console.log(`读取失败,一般是手动关闭了读任务`);
                        break;
                    }
                    else
                    {
                        for(let i=0; i<value.length; i++) // 连续读取到的数据可能是二维数组或一维数组,解包成一维后再组合数组
                        {
                            data = data.concat(value[i]); // 读取成功,保存读取内容,组合数组
                        }
                    }
                }
            }
            catch (error) 
            {
                console.log(`读取失败,一般是端口断开连接`);
            } 
            finally 
            {
                console.log(`读取结束`);
                reader_g.releaseLock(); // 允许稍后关闭端口
                reader_g = null; // 清空变量
            }
        }

        console.log(data);
        fun(data); // 执行完毕,额外执行
    }
}
  • 注意注意注意,和写数据类似,读数据,同样需要调用releaseLock(),以便之后能关闭端口.而调用前提是读任务已经结束.和写任务不一样,读任务是在while中一直循环的,因为经常不知道要读取的长度,( 不过在一些情况下是固定长度读取的,比如自定义协议之类的 )为此,必须要手动调用cancel()关闭读任务,使得其能执行break退出while.

注意注意注意,关闭任务使用的是close(),关闭读任务使用的是cancel();

  • 自定义情况下手动关闭读任务,最常见的就是设定时间,超时关闭.这时用到前面所说的setTimeout()函数了.

我之前学习时时一直没找到cancel(),导致我一直无法正常关闭端口,在这个上面栽了大跟头.可能是我教程没看清,所以特意重点说明.

  • 另一点需要注意的,读取到的数据是数组,而且每次读取到的长度不一定,且可能是二维数组,所以遍历一维后使用concat()组合数组的方式保存数据.再另外,读取的内容是字符ASCII格式的十进制形式.

  • 1.js文件修改成如下:

...
f2["按钮"].onclick = function()
{
    if (f2["按钮"].value == "开始接收")
    {
        console.log(`开始接收`); // 以下调用 开始接收 函数
        f2["按钮"].value = "停止接收"; // 开始执行,修改按钮文本

        read(3000, (data)=>
        {
            f2["接收"].value = data; // 将接收内容显示在多行文本框
            f2["按钮"].value = "开始接收"; // 执行完毕,最后修改按钮文本
        });
    }
    else if (f2["按钮"].value == "停止接收") 
    {
        console.log(`停止接收`); // 以下调用 停止接收 函数

        if (reader_g != null) // 手动关闭
        {
            reader_g.cancel(); // 读终止,停止读任务,
        }

        // f2["按钮"].value = "开始接收"; // 不需要这里执行,上面执行
    }
...
  • 现在,点击读取按钮后,就会读取3秒,超时会自动关闭读取,改变文本.也可以提前手动关闭读取.
  • 以下是试验效果,我读取了2次,第一次读取等待超时,第二次读取提前手动关闭,49,50,51是123的ASCII值,13是换行符.

在这里插入图片描述

3.6.关闭串口

推荐参考: << Web Serial API (wicg.github.io) >> 4.9 close() method ;

  • 2.js中添加如下内容:
/* 说明: 关闭串口
*  参数: 无
*/
async function close(fun)
{
    if (port_g.constructor == SerialPort) // 保护措施
    {
        if (reader_g != null) // 如果没有清空对象
        {
            reader_g.cancel(); // 就关闭任务
        }
        if (writer_g != null) // 同上
        {
            writer_g.close();
        }

        await sleep(200); // 等待一会,确保读写任务关闭,

        await port_g.close(); // 关闭串口

        fun(); // 执行完毕
    }
}
  • 注意,关闭端口之前一定要先关闭读写任务.为了确保异步函数执行结束,就调用延时等待,也是上面介绍过的.起到阻塞性的延时等待.

注意注意注意,关闭任务使用的是close(),关闭读任务使用的是cancel();而关闭串口使用的又是close();逆天,为什么要这样区别命名啊?

  • 1.js文件修改成如下:
...
    else if (f1["开关键"].value == "关闭") 
    {
        console.log(`关闭串口`); // 以下调用 关闭串口 函数

        close(()=>
        {
            f1["开关键"].value = "打开"; // 执行完毕,最后修改按钮文本
        }); // 关闭端口
    }
...
  • 最后效果如下图,开始接收后就立刻关闭串口,接收理想的提前结束.

在这里插入图片描述

4.总结

  • 好了,以上就算是这个系列的全部内容了.从零开始,完整的介绍了一个web上位机小项目.希望能帮到你.

也算是填了一个我以前的坑.之前也写过一个python+qt的上位机,用于图像接受的,参加智能车比赛时用.本来打算也写个教程的.不过后来嫌弃太简单就没写了(就是懒).那个项目唯一折腾我的就是不知道怎么在qt里显示连贯图片,也就是视频.找了许久才找对方向.其他东西都没什么特别的.

  • 现在工作后愈发不想奋斗,只想摸鱼.下班就想玩游戏和睡觉.如果工作中有什么有趣的项目,而且有闲情逸致的话,再记录.
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值