LoginSignup
0
0

More than 1 year has passed since last update.

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

Posted at

はじめに

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とセンサー

Electronの準備

こちらも、「WebStormでVue3のElectronアプリを作る」を参考に、Electronが起動する状態まで作っておきます。

パッケージの追加

シリアル通信がNode.jsで出来る serialport と Canvasアニメーションが出来る PIXI.js をインストールします。

powershell
> npm install serialport
> npm install pixi.js

M5Stackからの値を受け取る

M5Stack編では、値をシリアルモニタに表示する所まで作っていますので、この値を、今度はElectronで受け取ってみたいと思います。
src > background.js を開きます。

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
色々調べたところ、package.jsonと同階層にある、vue.config.js を編集することで、エラーがでなくなりました。もしファイルがなければ新規で作ります。

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 モジュールを活用した方法があるようです。
electron02.png
data<Buffer ... > から始まるデータになっているので、String型に変換します。

background.js
// ...省略... //

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

// ...省略... //

electron03.png
距離と角度が送られているのが確認できました。

IPC通信を使ってデータを送る

Electron までデータが来ているので、このデータをさらにHTML側に送りたいと思います。
Electron側をメインプロセス、HTML側をレンダープロセスと呼び、このプロセス間の通信を行うために、IPC通信を使います。

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 に記述を追加します。

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 で作って、必要なコンポーネントに読み込む形にしたいと思います。

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 に読み込みます。

App.vue
<script>
import MixinIPC from "@/components/MixinIPC"
export default {
  name: 'App',
  mixins:[MixinIPC],
}
</script>

上記を追記後、ビルドしてみると、デベロッパーツールのコンソールに値が出力されているのがわかります。
あとは、この値をビジュアル的に見せて行けば、終了です。
electron04.png

ビジュアルの準備

今回のセンサーの値を使って、画面を制御したいので、PixiJSを使い、簡単な図形を動かしたいと思います。
App.vue にPixiを配置します。今回はとりあえず、四角の図形を配置します。

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
ビルドすると、上記の図形が表示されます。

値の整形

ビジュアルができたので、この図形を動かすために、値の調整をしていきます。

ToFセンサーの値

センサーに手を近づけると拡大、遠くすると縮小、といった感じで、距離の値を拡大縮小に使いたいので、図形の拡大率は0.5~1.5 の間で変換したいと思います。
M5Stackから来たデータで、ToFセンサーの値は、一番近い時で30、遠い時で300くらいが安定していましたので、この範囲の値の時に拡大縮小をしたいと思います。

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

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

PixiJSに値を渡す

では、先ほど取得した値をPixiJS に反映してみます。使うのは distRangeangleRange です。

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 スプライトの回転と拡大に distRangeangleRange を代入します。
回転は、つまみの動きと square の動きを合わせるために、マイナスの値にしています。また、回転の値は、角度を指定するのではなく、ラジアンで指定するので、変換しています。
拡大縮小は、近づいたときに値も square も大きくなりたいので、2から引いて、0.5~1.5の値を反転しています。
electron07.png
ビルドしてみると、回転と拡大縮小が動いているのがわかると思います。コンソール画面は変換前の値がでたままですが、動きに合わせて数値も変っているのがわかると思います。

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

ありがとうございました。

参考URL

Node SrialPort
Electronでserialportを使おうとするとあれこれエラー
IPCによるプロセス間通信(ipcMain, ipcRenderer, 設定)
指定範囲へ数値の再マッピング【JavaScript】

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0