2月に「Webフロントエンド技術で作る体験型コンテンツ勉強会」たるものに参加させていただき、DMXの便利さを痛感してから、ずーっと欲しかった、DMXコントローラーと調光ユニット(ディマー)を、ついに手に入れることができたので、LED電球を調光できるElectronアプリをつくってみました。
ハード構成
PCとDMXコントローラーはUSBで接続。
DMXコントローラーとディマーはDMXケーブルで接続。
と、とてもシンプルな構成です。
アプリ構成
いつも通り、Next.jsのElectronテンプレートを使ってElectronアプリを作りました。
いまだ、この問題が解決していないため、
electron-nextは自分のリポジトリのものを使っています。
Node.jsからのDMXを出力する部分は、node-dmxを使いました。
node-dmxは内部でnode-serialportを使っているため、Electronと合わせて使うためには、electron-rebuildを使って、バージョンの整合性をとる必要があります。
参考URL
ソースコード(抜粋)
package.json
{ "private": true, "main": "main/index.js", "productName": "ElectronDMX", "scripts": { "clean": "rimraf dist main renderer/out renderer/.next", "dev": "npm run build-electron && electron .", "build-renderer": "next build renderer", "build-electron": "tsc -p electron-src", "build": "npm run build-renderer && npm run build-electron", "pack-app": "npm run build && electron-builder --dir", "dist": "npm run build && electron-builder", "type-check": "tsc -p ./renderer/tsconfig.json && tsc -p ./electron-src/tsconfig.json", "postinstall": "electron-rebuild -f -w serialport" }, "dependencies": { "dmx": "^0.2.5", "electron-is-dev": "^1.2.0", "electron-next": "git+https://github.com/kimizuka/electron-next#master", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@electron/rebuild": "^3.6.0", "@types/node": "^14.18.63", "@types/react": "^16.14.52", "@types/react-dom": "^16.9.24", "electron": "^27.1.2", "electron-builder": "^24.9.1", "next": "latest", "rimraf": "^3.0.2", "sass": "^1.77.1", "typescript": "^4.9.5" }, "build": { "asar": true, "files": [ "main", "renderer/out" ] } }
electron-src/index.ts
// 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'; const port = '/dev/tty.usbserial-XXXXXXXX'; // ls -l /dev/tty.usb* などでコントローラのポートを調べる const DMX = require('dmx'); const dmx = new DMX(); const universe = dmx.addUniverse('dmx', 'enttec-usb-dmx-pro', port); // 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: true, preload: join(__dirname, 'preload.js'), }, }); const url = isDev ? 'http://localhost:8000/' : format({ pathname: join(__dirname, '../renderer/out/index.html'), protocol: 'file:', slashes: true, }); mainWindow.loadURL(url); }); // Quit the app once all windows are closed app.on('window-all-closed', app.quit); // listen the channel `message` and resend the received message to the renderer process ipcMain.on('sendValue', (_evt: IpcMainEvent, value: number) => { universe.update({ 2: value }); // チャンネルを合わせる必要あり });
electron-src/preload.ts
import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('electron', { sendValue: (value: number) => ipcRenderer.send('sendValue', value) });
renderer/pages/index.tsx
import style from './index.module.scss'; import { useEffect, useState } from 'react'; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace interface Window { electron: { sendValue: (value: number) => void; }; } } export default function IndexPage() { const [ value, setValue ] = useState(0); useEffect(() => { window.electron.sendValue(value); }, [value]); return ( <main className={ style.wrapper }> <label> <input type="range" min={ 0 } max={ 100 } step={ 1 } value={ value } onChange={ (evt) => setValue(Number(evt.target.value)) } /> <input type="text" readOnly={ true } value={ value } /> </label> </main> ); };
renderer/pages/index.module.scss
.wrapper { display: flex; align-items: center; justify-content: center; position: fixed; inset: 0; label { display: flex; align-items: center; justify-content: center; transform: scale(2); > * { + * { margin-left: 8px; } } [type='range'] { cursor: pointer; } [type='text'] { width: 40px; text-align: center; } } }
ざざざっと書くとこんな感じです。
アプリ上のスライダーの値をIPC通信でメインプロセスに渡して、その値をDMXコントローラに渡しています。
その際、僕の使っているディマーのスタートチャンネルが2だったため、1チャンネルに接続しているLED電球の調光のために2チャンネルに値を渡していますが、
universe.update({ 2: value });
を、
universe.updateAll(value);
にすれば、すべてのチャンネルを一括で調光できます。
DEMO
アプリ上のスライダーでLED電球を調光できています。
今回は以上です。