技術情報

Electronでシリアル通信を行ってみる!

こんにちは、MSKです。
今回はElectronアプリでシリアル通信を行ってみたいと思います。
今回はパソコンに僕が作ったデバイスをつけて試しています。(僕は組み込みソフトウェアエンジニアなので、電子機器のソフトを作る方が専門だったりします・・・)

プロジェクト作成

使い勝手がよいので、Next.jsを使ったプロジェクトをそのまま使います。

Next.jsでElectronアプリのプロジェクトを立ち上げてみる!Next.jsを使ってElectronアプリを作ってみました。...

SerialPortのインストール

SerialPortをインストールします。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install serialport@9.2.8
npm i --save-dev @types/serialport
npm install serialport@9.2.8 npm i --save-dev @types/serialport
npm install serialport@9.2.8
npm i --save-dev @types/serialport

記事書いてる途中に別のプロジェクトでserialportをデフォルトでインストールしたら、10.x.xになっていました。
新しいバージョンは後で調べて更新する予定ではありますが、現段階では9.2.8を前提に書いていますので、ご注意ください。

接続されているCOMポートを取得する

libフォルダをelectron-srcの中に作って、その中にserialManager.tsを作成します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import SerialPort from 'serialport'
export const searchComPort = () => {
SerialPort.list().then(ports => {
ports.forEach( (port) => {
console.log(port.path);
console.log(port.pnpId);
console.log(port.manufacturer);
});
});
}
import SerialPort from 'serialport' export const searchComPort = () => { SerialPort.list().then(ports => { ports.forEach( (port) => { console.log(port.path); console.log(port.pnpId); console.log(port.manufacturer); }); }); }
import SerialPort from 'serialport'
export const searchComPort = () => {
    SerialPort.list().then(ports => {
        ports.forEach( (port) => {
            console.log(port.path);
            console.log(port.pnpId);
            console.log(port.manufacturer);
        });
    });
}

SerialPortのlistメソッドでComポートの一覧を取得できますので、それをforEachで全て表示しています。

あとはpages/index.tsxにボタンをつけて、mainプロセスから呼べるようにipcRendererでメッセージを送ります。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Native
import { join } from 'path'
import { format } from 'url'
// Packages
import { BrowserWindow, app, ipcMain, IpcMainEvent } from 'electron'
import isDev from 'electron-is-dev'
import prepareNext from 'electron-next'
import {searchComPort} from "./lib/serialManager";
// Prepare the renderer once the app is ready
app.on('ready', async () => {
await prepareNext('./renderer')
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: false,
preload: join(__dirname, 'preload.js'),
},
})
const url = isDev
? 'http://localhost:8000/'
: format({
pathname: join(__dirname, '../renderer/out/index.html'),
protocol: 'file:',
slashes: true,
})
mainWindow.webContents.openDevTools();
mainWindow.loadURL(url)
})
// Quit the app once all windows are closed
app.on('window-all-closed', app.quit)
ipcMain.on("com-port-get",(event: IpcMainEvent) => {
searchComPort().then((value:string[]) => {
event.sender.send("com-port-get-resp",value);
},(reason:string) => {
console.log("com-port-get failed : "+reason);
});
})
// Native import { join } from 'path' import { format } from 'url' // Packages import { BrowserWindow, app, ipcMain, IpcMainEvent } from 'electron' import isDev from 'electron-is-dev' import prepareNext from 'electron-next' import {searchComPort} from "./lib/serialManager"; // Prepare the renderer once the app is ready app.on('ready', async () => { await prepareNext('./renderer') const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: false, contextIsolation: false, preload: join(__dirname, 'preload.js'), }, }) const url = isDev ? 'http://localhost:8000/' : format({ pathname: join(__dirname, '../renderer/out/index.html'), protocol: 'file:', slashes: true, }) mainWindow.webContents.openDevTools(); mainWindow.loadURL(url) }) // Quit the app once all windows are closed app.on('window-all-closed', app.quit) ipcMain.on("com-port-get",(event: IpcMainEvent) => { searchComPort().then((value:string[]) => { event.sender.send("com-port-get-resp",value); },(reason:string) => { console.log("com-port-get failed : "+reason); }); })
// Native
import { join } from 'path'
import { format } from 'url'

// Packages
import { BrowserWindow, app, ipcMain, IpcMainEvent } from 'electron'
import isDev from 'electron-is-dev'
import prepareNext from 'electron-next'

import {searchComPort} from "./lib/serialManager";

// Prepare the renderer once the app is ready
app.on('ready', async () => {
  await prepareNext('./renderer')

  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: false,
      preload: join(__dirname, 'preload.js'),
    },
  })

  const url = isDev
    ? 'http://localhost:8000/'
    : format({
        pathname: join(__dirname, '../renderer/out/index.html'),
        protocol: 'file:',
        slashes: true,
      })
  mainWindow.webContents.openDevTools();
  mainWindow.loadURL(url)
})

// Quit the app once all windows are closed
app.on('window-all-closed', app.quit)

ipcMain.on("com-port-get",(event: IpcMainEvent) => {
  searchComPort().then((value:string[]) => {
    event.sender.send("com-port-get-resp",value);
  },(reason:string) => {
    console.log("com-port-get failed : "+reason);
  });
  
})

electron-srcのindex.tsにserialManagerのsearchComPortをインポートしておきます。
レンダラープロセスからメッセージが来た場合に反応するようにしておきます。
以降、シリアル通信の送信や受信はこのような形でメインプロセスで行い、それをレンダラープロセスから呼び出し、またはコールバックします。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect } from 'react'
import Link from 'next/link'
import Layout from '../components/Layout'
const IndexPage = () => {
useEffect(() => {
global.ipcRenderer.addListener('com-port-get-resp', (_event, args) => {
console.log(args);
});
}, [])
const onGetComPortClick = () => {
global.ipcRenderer.send('com-port-get');
}
return (
<Layout title="Home">
<button onClick={onGetComPortClick}>Get Com Port</button>
</Layout>
)
}
export default IndexPage
import { useEffect } from 'react' import Link from 'next/link' import Layout from '../components/Layout' const IndexPage = () => { useEffect(() => { global.ipcRenderer.addListener('com-port-get-resp', (_event, args) => { console.log(args); }); }, []) const onGetComPortClick = () => { global.ipcRenderer.send('com-port-get'); } return ( <Layout title="Home"> <button onClick={onGetComPortClick}>Get Com Port</button> </Layout> ) } export default IndexPage
import { useEffect } from 'react'
import Link from 'next/link'
import Layout from '../components/Layout'

const IndexPage = () => {
  useEffect(() => {
    global.ipcRenderer.addListener('com-port-get-resp', (_event, args) => {
      console.log(args);
    });
  }, [])

  const onGetComPortClick = () => {
    global.ipcRenderer.send('com-port-get');
  }

  return (
    <Layout title="Home">
      <button onClick={onGetComPortClick}>Get Com Port</button>
    </Layout>
  )
}

export default IndexPage

これで実行してみると、エラーが発生しました。
NODE_MODULE_VERSION 93. This version of Node.js requiresと出ています。
Nodeのバージョンがコンフリクトしているようです。
(僕がたまたま発生しただけみたいですが、備忘録をかねて解決した方法書いておきます。実際いろいろ試しました・・・)
次のコマンドでelectron-rebuildをインストールします。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm i -D electron-rebuild
npm i -D electron-rebuild
npm i -D electron-rebuild

インストールが終わったら、node_modules/.bin/electron-rebuildを実行します。
終わって、npm run devするとエラーが表示されず、コンソールに取得したCOMポートが表示されていると思います。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
COM1
USB\VID_XXXX&amp;PID_XXXX\xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
(メーカー名)
COM2
USB\VID_XXXX&amp;PID_XXXX\xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
(メーカー名)
COM1 USB\VID_XXXX&amp;PID_XXXX\xxxxxxxxxxxxxxxxxxxxxxxxxxxxx (メーカー名) COM2 USB\VID_XXXX&amp;PID_XXXX\xxxxxxxxxxxxxxxxxxxxxxxxxxxxx (メーカー名)
COM1
USB\VID_XXXX&amp;PID_XXXX\xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
(メーカー名)
COM2
USB\VID_XXXX&amp;PID_XXXX\xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
(メーカー名)

COMポートを開いてみる

Comの名前、ボーレートとFlowコントロールするかを指定してComポートを開きます。
(作ったデバイスではCTS・RTSを使ったFlowコントロールをEnableにしているので、今回はFlowコントロールするかをパラメータとして与えています。 )

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export const createComPort = (com:string,rate:number,flow:boolean) => {
return new Promise<string>((resolve,reject) => {
port = new SerialPort(com,{"baudRate":rate,"rtscts":flow},(error?: Error | null) => {
if(error){
console.log("create Serial failed : "+error.name+","+error.message);
reject("failed to create Serial");
} else {
console.log("create Serial Success");
resolve("success to create Serial");
}
});
});
}
export const createComPort = (com:string,rate:number,flow:boolean) => { return new Promise<string>((resolve,reject) => { port = new SerialPort(com,{"baudRate":rate,"rtscts":flow},(error?: Error | null) => { if(error){ console.log("create Serial failed : "+error.name+","+error.message); reject("failed to create Serial"); } else { console.log("create Serial Success"); resolve("success to create Serial"); } }); }); }
export const createComPort = (com:string,rate:number,flow:boolean)  => {
    return new Promise<string>((resolve,reject) => {
        port = new SerialPort(com,{"baudRate":rate,"rtscts":flow},(error?: Error | null) => {
            if(error){
                console.log("create Serial failed : "+error.name+","+error.message);
                reject("failed to create Serial");
            } else {
                console.log("create Serial Success");
                resolve("success to create Serial");
            }
        });
    });
}

SerialPortの第一引数がComの名前、第二引数がオプションになります。
今回はここでbaudRateとrtsctsを使用しています。
第三引数にコールバック関数を登録して、結果を受け取っています。

データの送受信をしてみる

作ったデバイスはメッセージを受信した場合、1秒後に同じメッセージを返すようにしました。(テスト用なので簡単なものにしました。)
そのため、先に受信する処理を作ります。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export const openComPortRecv = () => {
port.on("data",(data:any)=>{
console.log(String(data));
});
}
export const openComPortRecv = () => { port.on("data",(data:any)=>{ console.log(String(data)); }); }
export const openComPortRecv = ()  => {
    port.on("data",(data:any)=>{
      console.log(String(data));
    });
}

onメソッドを使って、第一引数に”data”、第二引数にコールバック関数を登録してComポートから受信したデータをコンソールに表示するようにしています。

次に送信処理を作ります。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export const writeComPort = (message:string) => {
return new Promise<string>((resolve,reject) => {
port.write(message,(error: Error | null | undefined) => {
if(error) {
console.log("write Serial failed : "+error.name+","+error.message);
reject("Serial Write failed");
} else {
console.log("write Serial Success");
resolve("Write Serial Done");
}
});
});
}
export const writeComPort = (message:string) => { return new Promise<string>((resolve,reject) => { port.write(message,(error: Error | null | undefined) => { if(error) { console.log("write Serial failed : "+error.name+","+error.message); reject("Serial Write failed"); } else { console.log("write Serial Success"); resolve("Write Serial Done"); } }); }); }
export const writeComPort = (message:string) => {
    return new Promise<string>((resolve,reject) => {
        port.write(message,(error: Error | null | undefined) => {
            if(error) {
                console.log("write Serial failed : "+error.name+","+error.message);
                reject("Serial Write failed");
            } else {
                console.log("write Serial Success");
                resolve("Write Serial Done");
            }
        });
    });
}

送信にはwriteメソッドを使います。
第一引数に送信するメッセージを、第二引数に結果を受け取るコールバック関数を登録します。
僕の場合、ビューにボタンをつけて、ボタンを押すとメインプロセスにメッセージを送り、そのメッセージを上の送信処理でデバイスに送るようにしました。
送信するとコンソールに送信したメッセージが表示されました。

最後に

結構簡単にNode.jsの機能を使ってシリアル通信を実現できました。
個人的にはFlowコントロールやボーレートの変更も簡単にできたことが嬉しかったです。

以上、「Electronでシリアル通信を行ってみる!」でした。
最後までご覧いただき、ありがとうございました。

ABOUT ME
MSK
九州在住の組み込み系エンジニアです。 2児の父親でもあります。 数学やプログラミングが趣味です。 最近RustとReact、結び目理論と曲面結び目理論にはまっています。