M5Stackに繋いだセンサーの値を、Electronに送ってみる２ー Electron編 ー

# はじめに
ElectronとVueを使ってデスクトップアプリを作っていますが、画面の操作を、タッチパネル以外に、物理的なボタンやセンサーを使って操作したい事がたまにあります。
今回はそんなセンサーの値をElectronに送って、画面を動かす方法をM5Stackを使って作っていきたいと思います。
また、M5StackはBluetoothやWifiなど、無線での操作もできると思いますが、今回はUSBを使って、有線でシリアルポートの値を受け取ってみたいと思います。
この記事はボリュームが大きくなってしまったので、２回に分かれています。
[ー M5Stack編 ー](https://qiita.com/kaluna/items/c96be59cf4f15534d3b1)
[ー 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](https://www.switch-science.com/catalog/7363/)
- [M5Stack用 ToF測距センサユニット](https://www.switch-science.com/catalog/5219/)
- [M5Stack用 回転角ユニット](https://www.switch-science.com/catalog/6551/)
- LEGO Technicsパーツ色々
![写真 2022-04-28 16 15 23.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/7515caf6-d4f4-d58a-7ca3-455f93b47e53.jpeg)
**M5Stack編**で、回転と距離が取れている状態まで作っておきます。
この時、USBのポート番号と、通信速度をメモっておきます。前回からの続きなので、
ポート番号は**COM3**、通信速度は**115200bps**となります。
# Electronの準備
こちらも、「[WebStormでVue3のElectronアプリを作る](https://qiita.com/kaluna/items/256d1e32fe73986dc266)」を参考に、Electronが起動する状態まで作っておきます。
## パッケージの追加
シリアル通信がNode.jsで出来る `serialport` と Canvasアニメーションが出来る `PIXI.js` をインストールします。
```shell:powershell
> npm install serialport
> npm install pixi.js
```
# M5Stackからの値を受け取る
**M5Stack編**では、値をシリアルモニタに表示する所まで作っていますので、この値を、今度はElectronで受け取ってみたいと思います。
`src` > `background.js` を開きます。
```javascript: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**の情報を上記のように追記してビルドすると、こんなエラーが。
![electron01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/21adaa9f-6b2d-df57-d4e6-386eb108ce4e.png)
色々調べたところ、`package.json`と同階層にある、`vue.config.js` を編集することで、エラーがでなくなりました。もしファイルがなければ新規で作ります。
```javascript:vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 追加 --->
  pluginOptions: {
    electronBuilder: {
      externals: ['serialport'],
      nodeIntegration:true,
    }
  }
  // <--- 追加
})
```
上記を追記し、再度ビルドすると、**コンソール** 画面に情報が表示されるようになりました。
```javascript
nodeIntegration:true,
```
こちらに関しては、後ほど解説する**IPC通信**で必要になりますので、記述しています。
これを`true` にすることはセキュリティ的に推奨されていないようですが、今回はサンプルという事で`true` にしています。
もし、`false` のまま使いたい場合は、`contextBridge` モジュールを活用した方法があるようです。
![electron02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/3fa1d038-9b47-95a6-9ee9-aba658117dfd.png)
`data` が `<Buffer ... >` から始まるデータになっているので、String型に変換します。

```javascript:background.js
// ...省略... //

port.on('data',(data)=>{
  console.log(String(data))
})

// ...省略... //
```
![electron03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/2c578820-498d-0b3d-533a-adc034595a70.png)
距離と角度が送られているのが確認できました。
# IPC通信を使ってデータを送る
**Electron**　までデータが来ているので、このデータをさらに**HTML側**に送りたいと思います。
Electron側をメインプロセス、HTML側をレンダープロセスと呼び、このプロセス間の通信を行うために、**IPC通信**を使います。

```javascript: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,
  })
  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` に記述を追加します。
```javascript:backgroungd.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** で作って、必要なコンポーネントに読み込む形にしたいと思います。
```vue:MixinIPC.vue
<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` に読み込みます。
```vue:App.vue
<script>
import MixinIPC from "@/components/MixinIPC"
export default {
  name: 'App',
  mixins:[MixinIPC],
}
</script>
```
上記を追記後、ビルドしてみると、デベロッパーツールのコンソールに値が出力されているのがわかります。
あとは、この値をビジュアル的に見せて行けば、終了です。
![electron04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/39f080c7-6c1e-fd1b-213b-f9cb8fab575f.png)
## ビジュアルの準備
今回のセンサーの値を使って、画面を制御したいので、[PixiJS](https://pixijs.com/)を使い、簡単な図形を動かしたいと思います。
`App.vue` にPixiを配置します。今回はとりあえず、四角の図形を配置します。
```vue:App.vue
<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>
```
![electron05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/feb5e998-32dc-16c6-44f9-bfb70b77a185.png)
ビルドすると、上記の図形が表示されます。
## 値の整形
ビジュアルができたので、この図形を動かすために、値の調整をしていきます。
### ToFセンサーの値
センサーに手を近づけると拡大、遠くすると縮小、といった感じで、距離の値を拡大縮小に使いたいので、図形の拡大率は**0.5～1.5** の間で変換したいと思います。
M5Stackから来たデータで、ToFセンサーの値は、一番近い時で**30**、遠い時で**300**くらいが安定していましたので、この範囲の値の時に拡大縮小をしたいと思います。

```vue:MixinIPC.vue
<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センサーの値の取得でも使った関数を使いまわして、追記していきます。

```vue:MixinIPC.vue
<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>
```
ビルドすると、**コンソール画面**に距離と回転が指定範囲内で推移しているのがわかります。
![electron06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/61b344c3-81a3-67a6-7914-5d0f932565c0.png)
## PixiJSに値を渡す
では、先ほど取得した値を**PixiJS** に反映してみます。使うのは　`distRange` と `angleRange` です。
```vue:App.vue
//...省略...//
<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**の値を反転しています。
![electron07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49845/c65297af-22bb-cf27-f058-01b4c312be56.png)
ビルドしてみると、回転と拡大縮小が動いているのがわかると思います。コンソール画面は変換前の値がでたままですが、動きに合わせて数値も変っているのがわかると思います。

こうしてセンサーの値を画面に反映させることで、色々と面白いものが作れるのでは、と思います。

ありがとうございました。
# 参考URL
[Node SrialPort](https://serialport.io/)
[Electronでserialportを使おうとするとあれこれエラー](https://drroot.page/wp/?p=201)
[IPCによるプロセス間通信(ipcMain, ipcRenderer, 設定)](https://www.wakuwakubank.com/posts/773-electron-ipc/)
[指定範囲へ数値の再マッピング【JavaScript】](https://siomin.vercel.app/2020/4/re-mapping/)