• Electron 串口通信


    1. 简介


    项目名称Node SerialPort
    主页https://serialport.io/
    开源协议MIT
    githubhttps://github.com/serialport/node-serialport
    github Star5.2k stars(2022-6-30)
    github Fork989 forks(2022-6-30)
    github git地址https://github.com/serialport/node-serialport.git

      通过一个例子来介绍一下Node的串口通信库【SerialPort】。同时也是Electron应用程序与硬件设备通过串口进行通信的例子。文末附有完整代码。本例不会涉及任何UI框架,只使用HTML、CSS、JavaScript编写代码。重点关注就是如何进行通信。

    2. 开发环境

    硬件:

    1. 单片机: Arduino UNO
    2. Arudino IDE版本: v 1.8.19
    3. OS: windows 10

    软件:

    1. Electron版本:v19.0.4
    2. Node版本:v14.19.3
    3. npm版本:v6.14.17
    4. VS Code版本:v1.68.1
    5. SerialPort版本:v10.4.0

    3. 程序说明

    本例子主要有以下功能:

    1. 遍历串口设备
    2. 打开与关闭设备
    3. 向设备发送数据
    4. 接收设备发送的数据

    请先看下方的程序示意图,讲解程序将根据示意图从上往下说明如何实现的。

    在这里插入图片描述

    先放上预加载脚本preload.js的代码,此脚本是渲染器进程与主进程IPC通信的关键。

    
    const {  contextBridge, ipcRenderer  } = require('electron')
    contextBridge.exposeInMainWorld('aeIo', {
        /**
         * 获取串口设备列表
         */
        async list() { return await ipcRenderer.invoke('aeio-list') },
        /**
         * 打开串口设备
         * @param {串口地址} path 
         * @param {波特率} baudRate 
         */
        open: (path, baudRate = 9600) => { ipcRenderer.send('aeio-open', path, baudRate) },
        /**
         * 关闭设备
         */
        close:()=>{ipcRenderer.send('aeio-close')},
        /**
         * 发送数据
         * @param {向arduino发送的数据} data 
         */
        write: (data) => { ipcRenderer.send('aeio-write', data) },
        /**
         * 从arduino接收数据
         * @param {*} callback 
         */
        read: (callback) => { ipcRenderer.on('aeio-read', callback) },
        /**
         * 接收消息 这个消息内容大部分都是main.js发出的,主要是用作提示
         * @param {*} callback 
         */
        message: (callback) => { ipcRenderer.on('aeio-message', callback) },
    });
    
    
    • 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

    3.1 遍历串口设备

    1. index.html
    
    <div><button id="mBtnList" onclick="getList()">获取串口设备列表</button>设备列表:<span id="dListInfo"></span></div>
    
    <script>
         /**
         * 向主进程发送获取串口设备的消息,并等待串口设备集合
         **/
         function getList() {
                window.aeIo.list().then((list) => {
                    let dListInfo = document.getElementById('dListInfo');
                    dListInfo.innerHTML = JSON.stringify(list);
                });
            }
    </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. main.js
    
    /**
     * 获取串口列表
     */
    ipcMain.handle('aeio-list', (ev, args) => {
        return SerialPort.list().then((info, err) => {
            if (err) {
                sendToRenderer(error(`设备列表获取失败:${err}`))
                return [];
            }
            //将集合返回给界面
            return info;
        })
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    SerialPort.list()方法会返回已连接的串口设备。返回的是一个集合。返回的数据集合中,每一个代表串口设备的对象都有一个path属性,这个属性的值就是要传给打开方法。

    3.2 打开与关闭设备

    1. index.html
    
        <div>串口设备:<input type="text" id="mInputDevPath" placeholder="请从设备列表上复制" width="200px" />波特率:<input type="text"
                id="mInputDevPort" placeholder="波特率" width="200px" value="115200" /><button id="mBtnOpen"
                onclick="openDevice()">打开设备</button><button id="mBtnClose" onclick="closeDevice()">关闭设备</button></div>
        <script>
            /**
             *  打开设备
             **/
            function openDevice() {
                let mInputDevPath = document.getElementById('mInputDevPath').value;// path 设备路径 我测试的时候是COM4
                let mInputDevPort = document.getElementById('mInputDevPort').value;// 波特率 我设置的是115200 如果这个和arduino设置的不一致是无法通信的
                window.aeIo.open(mInputDevPath, mInputDevPort);
            }
             /**
             *  关闭设备
             **/
            function closeDevice() {
                window.aeIo.close();
            }
        </script>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. main.js
    /**
     * 打开设备
     */
    let receiveData='';//接收到的数据
    ipcMain.on('aeio-open', (ev, ...args) => {
        console.log(args);
        const options = {
            path: args[0],
            baudRate: parseInt(args[1]),
        }
        //实例化设备之后此时已经打开设备了
        serialPort =  new SerialPort(options);
        //监听数据,当单片机发送数据时,会回调此方法
        serialPort.on('data',(data)=>{
            console.log(JSON.stringify(data.toString('utf8')));
            receiveData += data.toString('utf8');
            console.log(receiveData);
            //我是同过起始标志和结束标志来判断单片机是否已经将所有的数据发送完成
            //这个可以自定义开始与结束标志
            //本例中使用*表示结束
            if(receiveData.startsWith('I am Arduino')&&receiveData.endsWith('*')){
                read(receiveData);
                receiveData='';
            }
        })
    })
    /**
     * 关闭设备
     */
    ipcMain.on('aeio-close', (ev, ...args) => {
        if (serialPort!=null&&serialPort.isOpen) {
            serialPort.close((err) => {
                if(err){
                    error(`设备关闭失败:${err}`)
                }else{
                    error('设备已关闭');
                }
            });
        } else {
            error(`设备不存在,或设备未打开`)
        }
    })
    
    • 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

    说明:
    options对象:

    1. path属性:在获取串口设备方法返回的集合中,代表串口设备的对象也有一个path属性,这个属性值就是要传给options对象的path,本例中是通过输入框输入的,在实际开发中可以使用下拉列表选择串口设备。
    2. baudRate属性:波特率 ,这个值需要和Arduino设置的波特率一致。文末有Arduino的源码,请看注释。

    3.3 向设备发送数据

    1. index.js
        <div>数据:<input type="text" id="mInputData" placeholder="请输入要发送的数据" width="200px" /><button id="mBtnWrite"
                onclick="writeData()">发送数据</button></div>
        <script>
         /**
         * 发送数据
         **/
        function writeData() {
            let mInputData = document.getElementById('mInputData').value;
            if (mInputData.length === 0) {
                return;
            }
            window.aeIo.write(mInputData);
        }
        </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. main.js
    
    /**
     * 向arduino发送数据
     */
    ipcMain.on('aeio-write', (ev, ...args) => {
        if (serialPort!=null&&serialPort.isOpen) {
            let sendData = args[0]+"#";//以#号结尾 arduino 读取到#号的时候就代表 已经接收到全部数据了
            serialPort.write(Buffer.from(sendData),(err)=>{
                if(err){
                    error(`设备发送失败:${err}`)
                }else{
                    error('数据发送成功');
                }
            })
        } else {
            error(`设备不存在,或设备未打开,无法发送数据`)
        }
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. arduino.cc
    //loop中接收数据
    void loop() {
      // put your main code here, to run repeatedly:
      if (Serial.available() > 0) {
        // read the incoming byte:
        String data = Serial.readStringUntil('#');//遇到#号结束
    
        Serial.print("I am Arduino,I get data:"+data+"*"); 
      }
      delay(10);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.4 接收设备发送的数据

    接收设备的数据我们要反过来了,从arduino到主进程,再到渲染进程。

    1. arduino.cc
    //loop中接收数据
    void loop() {
      // put your main code here, to run repeatedly:
      if (Serial.available() > 0) {
        // read the incoming byte:
        String data = Serial.readStringUntil('#');//遇到#号结束
        //本例是当接收完数据,就把数据返回
        Serial.print("I am Arduino,I get data:"+data+"*"); //发送数据,并将接收到的数据返回过去
      }
      delay(10);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. main.js
    //监听数据,当单片机发送数据时,会回调此方法
    serialPort.on('data',(data)=>{
        console.log(JSON.stringify(data.toString('utf8')));
        receiveData += data.toString('utf8');
        console.log(receiveData);
        //我是同过起始标志和结束标志来判断单片机是否已经将所有的数据发送完成
        //这个可以自定义开始与结束标志
        //本例中使用*表示结束
        if(receiveData.startsWith('I am Arduino')&&receiveData.endsWith('*')){
            read(receiveData);
            receiveData='';
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. index.html
    /**
     * 接收数据
     **/
    window.aeIo.read((_event, value) => {
        dListInfo.innerHTML = `[${new Date()}]: ${value} <br/>` + dListInfo.innerHTML;
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.源码

    1. index.html
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>arduino-electron-io</title>
        <style>
            body {
                padding: 50px;
            }
    
            div {
                margin-top: 10px;
                margin-bottom: 10px;
            }
    
            button {
                margin-left: 5px;
                margin-right: 5px;
                background-color: rgb(14, 153, 247);
                color: white;
                border: 1px solid rgb(14, 153, 247);
                height: 32px;
            }
    
            .messageClass {
                border: 1px solid gray;
                background-color: black;
                color: white;
                font-size: 16px;
                height: 400px;
                margin-top: 50px;
                overflow-y: auto;
            }
        </style>
    </head>
    
    <body>
    
        <div><button id="mBtnList" onclick="getList()">获取串口设备列表</button>设备列表:<span id="dListInfo"></span></div>
        <div>串口设备:<input type="text" id="mInputDevPath" placeholder="请从设备列表上复制" width="200px" />波特率:<input type="text"
                id="mInputDevPort" placeholder="波特率" width="200px" value="115200" /><button id="mBtnOpen"
                onclick="openDevice()">打开设备</button><button id="mBtnClose" onclick="closeDevice()">关闭设备</button></div>
        <div>数据:<input type="text" id="mInputData" placeholder="请输入要发送的数据" width="200px" /><button id="mBtnWrite"
                onclick="writeData()">发送数据</button></div>
        <div id="mMessage" class="messageClass"></div>
        <script>
            let dListInfo = document.getElementById('mMessage');
            /**
             * 接收数据
             **/
            window.aeIo.read((_event, value) => {
                dListInfo.innerHTML = `[${new Date()}]: ${value} <br/>` + dListInfo.innerHTML;
            })
            /**
             * 接收消息提示
             **/
            window.aeIo.message((_event, value) => {
                dListInfo.innerHTML = `[${new Date()}]: ${value} <br/>` + dListInfo.innerHTML;
            })
            /**
             * 向主进程发送获取串口设备的消息,并等待串口设备集合
             **/
            function getList() {
                window.aeIo.list().then((list) => {
                    let dListInfo = document.getElementById('dListInfo');
                    dListInfo.innerHTML = JSON.stringify(list);
                });
            }
            /**
             *  打开设备
             **/
            function openDevice() {
                let mInputDevPath = document.getElementById('mInputDevPath').value;// path 设备路径 我测试的时候是COM4
                let mInputDevPort = document.getElementById('mInputDevPort').value;// 波特率 我设置的是115200 如果这个和arduino设置的不一致是无法通信的
                window.aeIo.open(mInputDevPath, mInputDevPort);
            }
             /**
             *  关闭设备
             **/
            function closeDevice() {
                window.aeIo.close();
            }
            /**
             * 发送数据
             **/
            function writeData() {
                let mInputData = document.getElementById('mInputData').value;
                if (mInputData.length === 0) {
                    return;
                }
                window.aeIo.write(mInputData);
            }
        </script>
    </body>
    
    </html>
    
    
    • 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
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    1. main.js
    
    const { app, BrowserWindow, ipcMain } = require('electron')
    const path = require('path')
    const { SerialPort } = require('serialport')
    let win
    const createWindow = () => {
        win = new BrowserWindow({
            width: 800,
            height: 600,
            webPreferences: {
                preload: path.join(__dirname, 'preload.js')
            }
        })
        win.maximize()
        win.webContents.openDevTools()
        win.loadFile('index.html')
    }
    
    let serialPort;
    app.whenReady().then(() => {
        createWindow()
    })
    
    /**
     * 获取串口列表
     */
    ipcMain.handle('aeio-list', (ev, args) => {
        return SerialPort.list().then((info, err) => {
            if (err) {
                sendToRenderer(error(`设备列表获取失败:${err}`))
                return [];
            }
            //将集合返回给界面
            return info;
        })
    })
    
    /**
     * 打开设备
     */
    let receiveData='';//接收到的数据
    ipcMain.on('aeio-open', (ev, ...args) => {
        console.log(args);
        const options = {
            path: args[0],
            baudRate: parseInt(args[1]),
        }
        //实例化设备之后此时已经打开设备了
        serialPort =  new SerialPort(options);
        //监听数据,当单片机发送数据时,会回调此方法
        serialPort.on('data',(data)=>{
            console.log(JSON.stringify(data.toString('utf8')));
            receiveData += data.toString('utf8');
            console.log(receiveData);
            //我是同过起始标志和结束标志来判断单片机是否已经将所有的数据发送完成
            //这个可以自定义开始与结束标志
            //本例中使用*表示结束
            if(receiveData.startsWith('I am Arduino')&&receiveData.endsWith('*')){
                read(receiveData);
                receiveData='';
            }
        })
    })
    /**
     * 关闭设备
     */
    ipcMain.on('aeio-close', (ev, ...args) => {
        if (serialPort!=null&&serialPort.isOpen) {
            serialPort.close((err) => {
                if(err){
                    error(`设备关闭失败:${err}`)
                }else{
                    error('设备已关闭');
                }
            });
        } else {
            error(`设备不存在,或设备未打开`)
        }
    })
    /**
     * 向arduino发送数据
     */
    ipcMain.on('aeio-write', (ev, ...args) => {
        if (serialPort!=null&&serialPort.isOpen) {
            let sendData = args[0]+"#";//以#号结尾 arduino 读取到#号的时候就代表 已经接收到全部数据了
            serialPort.write(Buffer.from(sendData),(err)=>{
                if(err){
                    error(`设备发送失败:${err}`)
                }else{
                    error('数据发送成功');
                }
            })
        } else {
            error(`设备不存在,或设备未打开,无法发送数据`)
        }
    })
    /**
     * 向渲染器进程发送消息
     * @param {*} channel 
     * @param {*} args 
     */
    function sendToRenderer(channel, args) {
        win.webContents.send(channel, args);
    }
    
    function read(data) {
        sendToRenderer('aeio-read', data)
    }
    function error(err) {
        sendToRenderer('aeio-message', err)
    }
    
    
    • 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
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    1. preload.js

    本文上半段已经展示了此源码。

    1. arduino.cc
    void setup() {
      // put your setup code here, to run once:
    Serial.begin(115200);//设置波特率
    Serial.setTimeout(10000);
    }
    
    //loop中接收数据
    void loop() {
      // put your main code here, to run repeatedly:
      if (Serial.available() > 0) {
        // read the incoming byte:
        String data = Serial.readStringUntil('#');//遇到#号结束
        //本例是当接收完数据,就把数据返回
        Serial.print("I am Arduino,I get data:"+data+"*"); //发送数据,并将接收到的数据返回过去
      }
      delay(10);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/c9cae57417c74fef962e3e7ac36bdaeb.jpeg#pic_center)
    
    
    • 1
    • 2
  • 相关阅读:
    CAD动态块制作
    字节算法大神熬了三个通宵整理的数据结构与算法笔记(万字长文)
    【服务器数据恢复】EMC Unity存储误操作删除数据卷的数据恢复案例
    二叉树基础总结
    OpenCV的二值化处理函数threshold()详解
    笔记本电脑识别不了刻录机,由于设备驱动程序的前一个实例仍在内存中,windows 无法加载这个硬件的设备驱动程序。 (代码 38)
    初识设计模式 - 访问者模式
    深入C++ Vector:解密vector的奥秘与底层模拟实现揭秘
    从设计、制造到封测,XSKY 智能存储助力半导体行业数字化转型
    11 Python 进程与线程编程
  • 原文地址:https://blog.csdn.net/Zhang_YingJie/article/details/125539401