はじめに
ElectronとVueを使ってデスクトップアプリを作っていますが、画面の操作を、タッチパネル以外に、物理的なボタンやセンサーを使って操作したい事がたまにあります。
今回はそんなセンサーの値をElectronに送って、画面を動かす方法をM5Stackを使って作っていきたいと思います。
また、M5StackはBluetoothやWifiなど、無線での操作もできると思いますが、今回はUSBを使って、有線でシリアルポートの値を受け取ってみたいと思います。
この記事はボリュームが大きくなってしまったので、2回に分かれています。
ー M5Stack編 ー
ー Electron編 ー(この記事です)
環境
- vue : 3.2.13
- electron : 13.0.0
- serialport: 10.4.0
- pixi.js: 6.3.0
- Arduino IDE
- WebStorm
- Windows10 Home
M5Stackとセンサー
- M5Stack M5Go
- M5Stack用 ToF測距センサユニット
- M5Stack用 回転角ユニット
- LEGO Technicsパーツ色々
M5Stack編で、回転と距離が取れている状態まで作っておきます。
この時、USBのポート番号と、通信速度をメモっておきます。前回からの続きなので、
ポート番号はCOM3、通信速度は115200bpsとなります。
Electronの準備
こちらも、「WebStormでVue3のElectronアプリを作る」を参考に、Electronが起動する状態まで作っておきます。
パッケージの追加
シリアル通信がNode.jsで出来る serialport
と Canvasアニメーションが出来る PIXI.js
をインストールします。
> npm install serialport
> npm install pixi.js
M5Stackからの値を受け取る
M5Stack編では、値をシリアルモニタに表示する所まで作っていますので、この値を、今度はElectronで受け取ってみたいと思います。
src
> background.js
を開きます。
'use strict'
import {app, protocol, BrowserWindow} from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'
import {SerialPort} from "serialport"; //<--追加
const isDevelopment = process.env.NODE_ENV !== 'production'
// 追加 --->
const port = new SerialPort({
path:'COM3',
baudRate:115200
})
// <--- 追加
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
async function createWindow() {
const win = new BrowserWindow({
// ...省略... //
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
}
// 追加 --->
port.on('data',(data)=>{
console.log(data)
})
// <--- 追加
// ...省略... //
portの情報を上記のように追記してビルドすると、こんなエラーが。
色々調べたところ、package.json
と同階層にある、vue.config.js
を編集することで、エラーがでなくなりました。もしファイルがなければ新規で作ります。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 追加 --->
pluginOptions: {
electronBuilder: {
externals: ['serialport'],
nodeIntegration:true,
}
}
// <--- 追加
})
上記を追記し、再度ビルドすると、コンソール 画面に情報が表示されるようになりました。
nodeIntegration:true,
こちらに関しては、後ほど解説するIPC通信で必要になりますので、記述しています。
これをtrue
にすることはセキュリティ的に推奨されていないようですが、今回はサンプルという事でtrue
にしています。
もし、false
のまま使いたい場合は、contextBridge
モジュールを活用した方法があるようです。
data
が <Buffer ... >
から始まるデータになっているので、String型に変換します。
// ...省略... //
port.on('data',(data)=>{
console.log(String(data))
})
// ...省略... //
IPC通信を使ってデータを送る
Electron までデータが来ているので、このデータをさらにHTML側に送りたいと思います。
Electron側をメインプロセス、HTML側をレンダープロセスと呼び、このプロセス間の通信を行うために、IPC通信を使います。
'use strict'
// ...省略... //
let win= null // 追加
async function createWindow() {
win= new BrowserWindow({ // 関数の外に変数を定義
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
}
port.on('data',(data)=>{
// 追加 --->
let ary = String(data).split('_') // データを分ける
let obj = {
// Number型に変換しないと、uint8Arrayという型になる
dist:Number(ary[0]), // 距離
angle:Number(ary[1]) // 回転
}
win.send('serial-update',obj) // HTML側にデータを送る
// <--- 追加
})
// ...省略... //
background.js
を開き、BrowserWindow
の変数を関数の外にだして、シリアルポートでも操作できるようにします。
HTML側にデータを送るところは、win.send('serial-update',obj)
の部分になります。
第1引数のserial-update
という名前は任意です。次のHTML側のデータを受け取る時にこの名前が必要になります。第2引数に送りたいデータを入れます。
HTML側でデータを受け取る
Electron側の設定
IPC通信 をHTML側でも使えるように、background.js
に記述を追加します。
'use strict'
// ...省略... //
let win= null
async function createWindow() {
win= new BrowserWindow({
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
enableRemoteModule:true, // 追加
})
// ...省略... //
}
// ...省略... //
これで、HTML側でもElectronの機能が使えるようになります。
HTML側の設定
また、今回はvue
で作っていますので、IPC通信 の部分をMixin で作って、必要なコンポーネントに読み込む形にしたいと思います。
<script>
import electron from "electron";
export default {
name: "MixinIpc",
data(){
return{
ipcRenderer:electron.ipcRenderer,
}
},
mounted() {
//Serial読み込み結果
this.ipcRenderer.on('serial-update',(event,args)=>{
console.log(args)
})
}
}
</script>
components
フォルダに MixinIPC.vue
として保存します。
次に、このMixinIPC
コンポーネントをApp.vue
に読み込みます。
<script>
import MixinIPC from "@/components/MixinIPC"
export default {
name: 'App',
mixins:[MixinIPC],
}
</script>
上記を追記後、ビルドしてみると、デベロッパーツールのコンソールに値が出力されているのがわかります。
あとは、この値をビジュアル的に見せて行けば、終了です。
ビジュアルの準備
今回のセンサーの値を使って、画面を制御したいので、PixiJSを使い、簡単な図形を動かしたいと思います。
App.vue
にPixiを配置します。今回はとりあえず、四角の図形を配置します。
<template>
<div>
<canvas ref="pixicvs"></canvas>
</div>
</template>
<script>
import MixinIPC from "@/components/MixinIPC"
const PIXI = require('pixi.js') // 追加
export default {
name: 'App',
mixins: [MixinIPC],
// 追加 --->
data(){
return{
pixiApp:null,
cvs:null
}
},
mounted() {
//setting canvas
this.cvs = this.$refs.pixicvs
this.cvs.getContext('webgl',{
preserveDrawingBuffer:true,
stencil:true
})
//setting pixi
this.pixiApp = new PIXI.Application({
antialias:true,
autoDensity:true,
width:800,height:600,
view:this.cvs,
backgroundColor:0xff0000
})
// make square
let square = new PIXI.Sprite()
let g = new PIXI.Graphics()
g.beginFill(0xffff00)
g.drawRect(-100,-100,200,200)
g.endFill()
square.addChild(g)
square.x = this.pixiApp.screen.width/2
square.y = this.pixiApp.screen.height/2
this.pixiApp.stage.addChild(square)
}
// <--- 追加
}
</script>
値の整形
ビジュアルができたので、この図形を動かすために、値の調整をしていきます。
ToFセンサーの値
センサーに手を近づけると拡大、遠くすると縮小、といった感じで、距離の値を拡大縮小に使いたいので、図形の拡大率は0.5~1.5 の間で変換したいと思います。
M5Stackから来たデータで、ToFセンサーの値は、一番近い時で30、遠い時で300くらいが安定していましたので、この範囲の値の時に拡大縮小をしたいと思います。
<script>
import electron from "electron";
export default {
name: "MixinIpc",
data(){
return{
ipcRenderer:electron.ipcRenderer,
// 追加 --->
tofObj:{
range:0,
input_min:30,input_max:300,
output_min:0.5,output_max:1.5,
}
// <--- 追加
}
},
mounted() {
//Serial読み込み結果
this.ipcRenderer.on('serial-update',(event,args)=>{
// 追加 --->
// 指定範囲内に値を収める 外れた値は最大か最小のどちらかで留まる
this.tofObj.range = Math.max(this.tofObj.input_min,
Math.min(args.dist,this.tofObj.input_max))
console.log(this.distRange) // 0.5~1.5で収まった値が返ってくる
// <--- 追加
})
},
// 追加 --->
methods:{
// ある指定範囲(input)内の値を新しい指定範囲(output)内の値に変換して返す
map(obj){
const input_diff = obj.input_max - obj.range
const input_range = obj.input_max - obj.input_min
const output_range = obj.output_max - obj.output_min
const percent = input_diff / input_range
const output_diff = percent * output_range
const result = obj.output_max - output_diff
return result
}
},
computed:{
// ToFセンサーの値を変換
distRange(){ return this.map(this.tofObj) },
}
// <--- 追加
}
</script>
ANGLEセンサーの値
つまみを回すと、図形を回転させたいので、0~360 の間で変換したいと思います。
M5Stackから来たデータで、ANGLEセンサーの値は、0~4095の間で取得できたので、この範囲で回転させたいと思います。ToFセンサーの値の取得でも使った関数を使いまわして、追記していきます。
<script>
import electron from "electron";
export default {
name: "MixinIpc",
data(){
return{
ipcRenderer:electron.ipcRenderer,
tofObj:{
range:0,
input_min:30,input_max:300,
output_min:0.5,output_max:1.5,
},
// 追加 --->
angleObj:{
range:0,
input_min:0,input_max:4095,
output_min:0,output_max:360,
},
// <--- 追加
}
},
mounted() {
//Serial読み込み結果
this.ipcRenderer.on('serial-update',(event,args)=>{
// 指定範囲内に値を収める 外れた値は最大か最小のどちらかで留まる
this.tofObj.range = Math.max(this.tofObj.input_min,
Math.min(args.dist,this.tofObj.input_max))
console.log('距離:'+this.distRange) // 0.5~1.5で収まった値が返ってくる
// 追加 --->
this.angleObj.range = args.angle
console.log('回転'+this.angleRange) // 0~360で収まった値が返ってくる
// <--- 追加
})
},
methods:{
// ある指定範囲(input)内の値を新しい指定範囲(output)内の値に変換して返す
map(obj){
const input_diff = obj.input_max - obj.range
const input_range = obj.input_max - obj.input_min
const output_range = obj.output_max - obj.output_min
const percent = input_diff / input_range
const output_diff = percent * output_range
const result = obj.output_max - output_diff
return result
}
},
computed:{
// ToFセンサーの値を変換
distRange(){ return this.map(this.tofObj) },
// ANGLEセンサーの値を変換
angleRange(){ return this.map(this.angleObj) } // 追加
}
}
</script>
ビルドすると、コンソール画面に距離と回転が指定範囲内で推移しているのがわかります。
PixiJSに値を渡す
では、先ほど取得した値をPixiJS に反映してみます。使うのは distRange
と angleRange
です。
//...省略...//
<script>
import MixinIPC from "@/components/MixinIPC"
const PIXI = require('pixi.js')
export default {
//...省略...//
mounted() {
//...省略...//
this.pixiApp.stage.addChild(square)
// 追加 --->
this.pixiApp.ticker.add(() => {
square.rotation = -this.rotRange * (Math.PI / 180)
square.scale.x = square.scale.y = 2 - this.distRange
})
// <--- 追加
}
}
</script>
先ほど作った square
スプライトの回転と拡大に distRange
と angleRange
を代入します。
回転は、つまみの動きと square
の動きを合わせるために、マイナスの値にしています。また、回転の値は、角度を指定するのではなく、ラジアンで指定するので、変換しています。
拡大縮小は、近づいたときに値も square
も大きくなりたいので、2から引いて、0.5~1.5の値を反転しています。
ビルドしてみると、回転と拡大縮小が動いているのがわかると思います。コンソール画面は変換前の値がでたままですが、動きに合わせて数値も変っているのがわかると思います。
こうしてセンサーの値を画面に反映させることで、色々と面白いものが作れるのでは、と思います。
ありがとうございました。
参考URL
Node SrialPort
Electronでserialportを使おうとするとあれこれエラー
IPCによるプロセス間通信(ipcMain, ipcRenderer, 設定)
指定範囲へ数値の再マッピング【JavaScript】