diff --git a/boilerbloat/src/main/backend.ts b/boilerbloat/src/main/backend.ts new file mode 100644 index 0000000..e38a816 --- /dev/null +++ b/boilerbloat/src/main/backend.ts @@ -0,0 +1,120 @@ + +import { ipcMain } from 'electron'; +import { blackout } from '../patterns/blackout'; +import { Pattern, PatternOutput, Time } from '../patterns/proto'; +import { TestPattern } from '../patterns/test'; +import rust, { BeatTrackerHandle, MovingHeadState, OutputHandle } from 'rust_native_module'; + +type AppState = { + patterns: { [key: string]: PatternOutput }, + selectedPattern: string | null, +}; + +class Backend { + + beatTracker: BeatTrackerHandle; + dmxOutput: OutputHandle; + + patterns: Map; + patternOutputs: Map; + selectedPattern: string; + + constructor() { + + // beat tracking + + let beatTracker = rust.getBeatTracker(); + + if (beatTracker.type !== 'success') { + throw new Error("could not initialize beat tracking"); + } + + this.beatTracker = beatTracker.value; + ipcMain.on('beat-tracking', async (_, arg) => { + if (arg === 'tap') { + this.beatTracker.tap(); + } + }); + + // output + + let dmxOutput = rust.openOutput("/dev/ttyUSB0"); + + if (dmxOutput.type !== 'success') { + throw new Error("could not open DMX output"); + } + + this.dmxOutput = dmxOutput.value; + + // patterns + + this.patterns = new Map(); + this.patterns.set("test", new TestPattern()); + + this.patternOutputs = new Map(); + this.selectedPattern = "test"; + + let time: Time = { + absolute: 0, + beat_relative: null + } + + for (let [patternId, pattern] of this.patterns.entries()) { + this.patternOutputs.set(patternId, pattern.render(time)); + } + + } + + getState(): AppState { + let result: AppState = { + patterns: {}, + selectedPattern: this.selectedPattern, + } + + for (let [patternId, patternOutput] of this.patternOutputs) { + result.patterns[patternId] = patternOutput; + } + + return result; + } + + update() { + let date = new Date(); + let time: Time = { + absolute: date.getTime() / 1000, + beat_relative: null, + } + + // render all patterns + + if (!this.patterns) { + console.log("big oof?"); + throw new Error("???"); + } + + for (let [patternId, pattern] of this.patterns.entries()) { + this.patternOutputs.set(patternId, pattern.render(time)); + } + + // write selected pattern + + let output: Array = blackout; + + if (this.selectedPattern !== null) { + let selectedPatternOutput = this.patternOutputs.get(this.selectedPattern) + + if (selectedPatternOutput) { + output = selectedPatternOutput; + } + } + + this.dmxOutput.set(output); + } + + close() { + this.dmxOutput.close(); + } + +} + +export default Backend; diff --git a/boilerbloat/src/main/main.ts b/boilerbloat/src/main/main.ts index dc7dbb7..aa08394 100644 --- a/boilerbloat/src/main/main.ts +++ b/boilerbloat/src/main/main.ts @@ -11,28 +11,20 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; import path from 'path'; -import { app, BrowserWindow, shell, ipcMain } from 'electron'; +import { app, BrowserWindow, ipcMain, shell } from 'electron'; import { resolveHtmlPath } from './util'; +import Backend from './backend'; -import rust from 'rust_native_module'; +let backend = new Backend(); + +let mainLoop = setInterval(() => { backend.update(); }, 20); + +ipcMain.handle('poll', () => { + return backend.getState(); +}); let mainWindow: BrowserWindow | null = null; -let beat_tracker = rust.getBeatTracker(); - -// TODO @thamma why does this not infer that beat_tracker has tap? -if (beat_tracker.type === 'success') { - - console.log('Beat Tracker started.'); - - ipcMain.on('beat-tracking', async (_, arg) => { - if (arg === 'tap') { - // see here - (beat_tracker as any).tap(); - } - }); -} - const createWindow = async () => { const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'assets') @@ -99,5 +91,9 @@ app // dock icon is clicked and there are no other windows open. if (mainWindow === null) createWindow(); }); + app.on('quit', () => { + clearInterval(mainLoop); + backend.close(); + }) }) .catch(console.log); diff --git a/boilerbloat/src/main/menu.ts b/boilerbloat/src/main/menu.ts deleted file mode 100644 index c8c5755..0000000 --- a/boilerbloat/src/main/menu.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { - app, - Menu, - shell, - BrowserWindow, - MenuItemConstructorOptions, -} from 'electron'; - -interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { - selector?: string; - submenu?: DarwinMenuItemConstructorOptions[] | Menu; -} - -export default class MenuBuilder { - mainWindow: BrowserWindow; - - constructor(mainWindow: BrowserWindow) { - this.mainWindow = mainWindow; - } - - buildMenu(): Menu { - if ( - process.env.NODE_ENV === 'development' || - process.env.DEBUG_PROD === 'true' - ) { - this.setupDevelopmentEnvironment(); - } - - const template = - process.platform === 'darwin' - ? this.buildDarwinTemplate() - : this.buildDefaultTemplate(); - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - - return menu; - } - - setupDevelopmentEnvironment(): void { - this.mainWindow.webContents.on('context-menu', (_, props) => { - const { x, y } = props; - - Menu.buildFromTemplate([ - { - label: 'Inspect element', - click: () => { - this.mainWindow.webContents.inspectElement(x, y); - }, - }, - ]).popup({ window: this.mainWindow }); - }); - } - - buildDarwinTemplate(): MenuItemConstructorOptions[] { - const subMenuAbout: DarwinMenuItemConstructorOptions = { - label: 'Electron', - submenu: [ - { - label: 'About ElectronReact', - selector: 'orderFrontStandardAboutPanel:', - }, - { type: 'separator' }, - { label: 'Services', submenu: [] }, - { type: 'separator' }, - { - label: 'Hide ElectronReact', - accelerator: 'Command+H', - selector: 'hide:', - }, - { - label: 'Hide Others', - accelerator: 'Command+Shift+H', - selector: 'hideOtherApplications:', - }, - { label: 'Show All', selector: 'unhideAllApplications:' }, - { type: 'separator' }, - { - label: 'Quit', - accelerator: 'Command+Q', - click: () => { - app.quit(); - }, - }, - ], - }; - const subMenuEdit: DarwinMenuItemConstructorOptions = { - label: 'Edit', - submenu: [ - { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, - { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, - { type: 'separator' }, - { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, - { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, - { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, - { - label: 'Select All', - accelerator: 'Command+A', - selector: 'selectAll:', - }, - ], - }; - const subMenuViewDev: MenuItemConstructorOptions = { - label: 'View', - submenu: [ - { - label: 'Reload', - accelerator: 'Command+R', - click: () => { - this.mainWindow.webContents.reload(); - }, - }, - { - label: 'Toggle Full Screen', - accelerator: 'Ctrl+Command+F', - click: () => { - this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); - }, - }, - { - label: 'Toggle Developer Tools', - accelerator: 'Alt+Command+I', - click: () => { - this.mainWindow.webContents.toggleDevTools(); - }, - }, - ], - }; - const subMenuViewProd: MenuItemConstructorOptions = { - label: 'View', - submenu: [ - { - label: 'Toggle Full Screen', - accelerator: 'Ctrl+Command+F', - click: () => { - this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); - }, - }, - ], - }; - const subMenuWindow: DarwinMenuItemConstructorOptions = { - label: 'Window', - submenu: [ - { - label: 'Minimize', - accelerator: 'Command+M', - selector: 'performMiniaturize:', - }, - { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, - { type: 'separator' }, - { label: 'Bring All to Front', selector: 'arrangeInFront:' }, - ], - }; - const subMenuHelp: MenuItemConstructorOptions = { - label: 'Help', - submenu: [ - { - label: 'Learn More', - click() { - shell.openExternal('https://electronjs.org'); - }, - }, - { - label: 'Documentation', - click() { - shell.openExternal( - 'https://github.com/electron/electron/tree/main/docs#readme' - ); - }, - }, - { - label: 'Community Discussions', - click() { - shell.openExternal('https://www.electronjs.org/community'); - }, - }, - { - label: 'Search Issues', - click() { - shell.openExternal('https://github.com/electron/electron/issues'); - }, - }, - ], - }; - - const subMenuView = - process.env.NODE_ENV === 'development' || - process.env.DEBUG_PROD === 'true' - ? subMenuViewDev - : subMenuViewProd; - - return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; - } - - buildDefaultTemplate() { - const templateDefault = [ - { - label: '&File', - submenu: [ - { - label: '&Open', - accelerator: 'Ctrl+O', - }, - { - label: '&Close', - accelerator: 'Ctrl+W', - click: () => { - this.mainWindow.close(); - }, - }, - ], - }, - { - label: '&View', - submenu: - process.env.NODE_ENV === 'development' || - process.env.DEBUG_PROD === 'true' - ? [ - { - label: '&Reload', - accelerator: 'Ctrl+R', - click: () => { - this.mainWindow.webContents.reload(); - }, - }, - { - label: 'Toggle &Full Screen', - accelerator: 'F11', - click: () => { - this.mainWindow.setFullScreen( - !this.mainWindow.isFullScreen() - ); - }, - }, - { - label: 'Toggle &Developer Tools', - accelerator: 'Alt+Ctrl+I', - click: () => { - this.mainWindow.webContents.toggleDevTools(); - }, - }, - ] - : [ - { - label: 'Toggle &Full Screen', - accelerator: 'F11', - click: () => { - this.mainWindow.setFullScreen( - !this.mainWindow.isFullScreen() - ); - }, - }, - ], - }, - { - label: 'Help', - submenu: [ - { - label: 'Learn More', - click() { - shell.openExternal('https://electronjs.org'); - }, - }, - { - label: 'Documentation', - click() { - shell.openExternal( - 'https://github.com/electron/electron/tree/main/docs#readme' - ); - }, - }, - { - label: 'Community Discussions', - click() { - shell.openExternal('https://www.electronjs.org/community'); - }, - }, - { - label: 'Search Issues', - click() { - shell.openExternal('https://github.com/electron/electron/issues'); - }, - }, - ], - }, - ]; - - return templateDefault; - } -} diff --git a/boilerbloat/src/main/preload.js b/boilerbloat/src/main/preload.js index 925f2b0..7556abe 100644 --- a/boilerbloat/src/main/preload.js +++ b/boilerbloat/src/main/preload.js @@ -1,21 +1,5 @@ const { contextBridge, ipcRenderer } = require('electron'); -const rust = require('rust_native_module'); contextBridge.exposeInMainWorld('electron', { - ipcRenderer: { - send(channel, data) { - const validChannels = ['beat-tracking']; - if (validChannels.includes(channel)) { - ipcRenderer.send(channel, data); - } - }, - on(channel, func) { - const validChannels = ['tick', 'beat-tracking']; - if (validChannels.includes(channel)) { - // Deliberately strip event as it includes `sender` - ipcRenderer.on(channel, (event, ...args) => func(...args)); - } - }, - }, - rustding: rust + ipcRenderer: ipcRenderer, }); diff --git a/boilerbloat/src/patterns/blackout.ts b/boilerbloat/src/patterns/blackout.ts new file mode 100644 index 0000000..b57ca18 --- /dev/null +++ b/boilerbloat/src/patterns/blackout.ts @@ -0,0 +1,15 @@ +import { MovingHeadState } from "rust_native_module"; + +export const blackout: Array = [] + +for (let startAddress of [1, 15, 29, 43]) { + blackout.push({ + startAddress: startAddress, + pan: 0, + tilt: 0, + brightness: { type: 'off' }, + rgbw: [0, 0, 0, 0], + speed: 1, + reset: false, + }) +} diff --git a/boilerbloat/src/patterns/chaser.ts b/boilerbloat/src/patterns/chaser.ts index 4e9330a..42e464f 100644 --- a/boilerbloat/src/patterns/chaser.ts +++ b/boilerbloat/src/patterns/chaser.ts @@ -1,8 +1,13 @@ import { MovingHeadState } from 'rust_native_module'; -import { Pattern, Time } from './proto'; +import { Pattern, PatternOutput, Time } from './proto'; export class ChaserPattern implements Pattern { - render(time: Time): Array { + render(time: Time): PatternOutput { + + if (time.beat_relative === null) { + return null; + } + let head_number = Math.ceil(time.beat_relative) % 4; let template: MovingHeadState = { diff --git a/boilerbloat/src/patterns/proto.ts b/boilerbloat/src/patterns/proto.ts index e466dba..1d6d4c9 100644 --- a/boilerbloat/src/patterns/proto.ts +++ b/boilerbloat/src/patterns/proto.ts @@ -2,9 +2,11 @@ import { MovingHeadState } from "rust_native_module"; export type Time = { absolute: number, - beat_relative: number, // + beat_relative: number | null, }; +export type PatternOutput = Array | null; + export interface Pattern { - render(time: Time): Array; + render(time: Time): PatternOutput; } diff --git a/boilerbloat/src/patterns/stage.ts b/boilerbloat/src/patterns/stage.ts new file mode 100644 index 0000000..681260b --- /dev/null +++ b/boilerbloat/src/patterns/stage.ts @@ -0,0 +1 @@ +export const startAddresses = [1, 15, 29, 43] diff --git a/boilerbloat/src/patterns/test.ts b/boilerbloat/src/patterns/test.ts new file mode 100644 index 0000000..8da1d3b --- /dev/null +++ b/boilerbloat/src/patterns/test.ts @@ -0,0 +1,39 @@ +import { MovingHeadState } from "rust_native_module"; +import { Pattern, PatternOutput, Time } from "./proto"; +import { startAddresses } from "./stage"; + +export class TestPattern implements Pattern { + + rgbw = [ + [255, 0, 0, 0], + [0, 255, 0, 0], + [0, 0, 255, 0], + [0, 0, 0, 255], + ] + + render(time: Time): PatternOutput { + let t = time.absolute % this.rgbw.length; + let second = Math.floor(t); + let brightness = 1 - (t % 1); + + return startAddresses.map((startAddress) => { + + let [r, g, b, w] = this.rgbw[second]; + + let state: MovingHeadState = { + startAddress: startAddress, + pan: 0, + tilt: 0, + brightness: { + type: "dimmer", + value: brightness, + }, + rgbw: [r, g, b, w], + speed: 1, + reset: false + } + return state; + }) + } + +} diff --git a/boilerbloat/src/renderer/App.tsx b/boilerbloat/src/renderer/App.tsx index a5dc85b..f0d89fc 100644 --- a/boilerbloat/src/renderer/App.tsx +++ b/boilerbloat/src/renderer/App.tsx @@ -2,6 +2,7 @@ import { MemoryRouter as Router, Switch, Route } from 'react-router-dom'; import { IpcRenderer } from 'electron/renderer'; import './App.css'; +import { useEffect, useState } from 'react'; const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; @@ -9,20 +10,35 @@ function tap() { ipcRenderer.send("beat-tracking", "tap"); } -const Hello = () => { +const Frontend: React.FC = () => { - return ( + const [state, setState] = useState(); + + const pollMain = async () => { + const reply = await ipcRenderer.invoke("poll"); + setState(reply); + } + + useEffect(() => { + const interval = setInterval(pollMain, 20); + return () => clearInterval(interval); + }); + + return <> +
+ State: {state ? JSON.stringify(state) : "undef"} +
- ); + ; }; export default function App() { return ( - + ); diff --git a/rust_native_module/index.d.ts b/rust_native_module/index.d.ts index cc61140..99a7363 100644 --- a/rust_native_module/index.d.ts +++ b/rust_native_module/index.d.ts @@ -1,6 +1,6 @@ declare module rust_native_module { type Result = - { type: "success" } & T + { type: "success", value: T } | { type: "error", message: string }; type Option = @@ -26,8 +26,8 @@ declare module rust_native_module { } type OutputHandle = { - set: (heads: Array) => Result<{}>, - close: () => Result<{}>, + set: (heads: Array) => Result, + close: () => Result, } type BeatTrackerHandle = { @@ -36,7 +36,7 @@ declare module rust_native_module { } function listPorts(): Array; - function openOutput(): Result; + function openOutput(port: string): Result; function getBeatTracker(): Result; } diff --git a/rust_native_module/src/beat_tracking/mod.rs b/rust_native_module/src/beat_tracking/mod.rs index 5d4260a..ca3e6cd 100644 --- a/rust_native_module/src/beat_tracking/mod.rs +++ b/rust_native_module/src/beat_tracking/mod.rs @@ -11,19 +11,22 @@ impl Finalize for Metronome {} pub fn get_beat_tracker(mut cx: FunctionContext) -> JsResult { let obj = cx.empty_object(); + let value_obj = cx.empty_object(); + + let boxed_tracker = cx.boxed(RefCell::new(Metronome::new(Duration::from_secs(2)))); + value_obj.set(&mut cx, "_rust_ptr", boxed_tracker)?; + + let tap_function = JsFunction::new(&mut cx, tap)?; + value_obj.set(&mut cx, "tap", tap_function)?; + + let get_progress_function = JsFunction::new(&mut cx, get_progress)?; + value_obj.set(&mut cx, "getProgress", get_progress_function)?; + + obj.set(&mut cx, "value", value_obj)?; let success_string = cx.string("success".to_string()); obj.set(&mut cx, "type", success_string)?; - let boxed_tracker = cx.boxed(RefCell::new(Metronome::new(Duration::from_secs(2)))); - obj.set(&mut cx, "_rust_ptr", boxed_tracker)?; - - let tap_function = JsFunction::new(&mut cx, tap)?; - obj.set(&mut cx, "tap", tap_function)?; - - let get_progress_function = JsFunction::new(&mut cx, get_progress)?; - obj.set(&mut cx, "getProgress", get_progress_function)?; - Ok(obj) } diff --git a/rust_native_module/src/output/mod.rs b/rust_native_module/src/output/mod.rs index fde3a83..55dcbab 100644 --- a/rust_native_module/src/output/mod.rs +++ b/rust_native_module/src/output/mod.rs @@ -29,31 +29,33 @@ pub fn open_output(mut cx: FunctionContext) -> JsResult { let path = path.value(&mut cx); let obj = cx.empty_object(); - let success_string; + let value_obj = cx.empty_object(); match Controller::new(path) { Ok(controller) => { - success_string = cx.string("success"); + let success_string = cx.string("success"); + obj.set(&mut cx, "type", success_string)?; let boxed_controller = cx.boxed(RefCell::new(controller)); - obj.set(&mut cx, "_rust_ptr", boxed_controller)?; + value_obj.set(&mut cx, "_rust_ptr", boxed_controller)?; let set_output_function = JsFunction::new(&mut cx, set_output)?; - obj.set(&mut cx, "set", set_output_function)?; + value_obj.set(&mut cx, "set", set_output_function)?; let close_function = JsFunction::new(&mut cx, close_output)?; - obj.set(&mut cx, "close", close_function)?; + value_obj.set(&mut cx, "close", close_function)?; + + obj.set(&mut cx, "value", value_obj)?; } Err(e) => { - success_string = cx.string("error"); + let success_string = cx.string("error"); + obj.set(&mut cx, "type", success_string)?; let error_message = cx.string(e.to_string()); obj.set(&mut cx, "message", error_message)?; } } - obj.set(&mut cx, "type", success_string)?; - Ok(obj) } diff --git a/rust_native_module/test.js b/rust_native_module/test.js index 4b5e419..47ac5a8 100644 --- a/rust_native_module/test.js +++ b/rust_native_module/test.js @@ -2,63 +2,65 @@ const mod = require('.') const o = mod.openOutput("/dev/ttyUSB0") -if (o.type !== "success") { - return -} +if (o.type == 'success') { -const movingHeads = [ - { - startAddress: 1, - pan: 0, - tilt: 0, - brightness: { - type: "dimmer", - value: 1, + const output = o.value; + + const movingHeads = [ + { + startAddress: 1, + pan: 0, + tilt: 0, + brightness: { + type: "dimmer", + value: 1, + }, + rgbw: [255, 0, 0, 0], + speed: 0, + reset: false, }, - rgbw: [255, 0, 0, 0], - speed: 0, - reset: false, - }, - { - startAddress: 15, - pan: 0, - tilt: 0, - brightness: { - type: "dimmer", - value: 1, + { + startAddress: 15, + pan: 0, + tilt: 0, + brightness: { + type: "dimmer", + value: 1, + }, + rgbw: [255, 0, 0, 0], + speed: 0, + reset: false, }, - rgbw: [255, 0, 0, 0], - speed: 0, - reset: false, - }, - { - startAddress: 29, - pan: 0, - tilt: 0, - brightness: { - type: "dimmer", - value: 1, + { + startAddress: 29, + pan: 0, + tilt: 0, + brightness: { + type: "dimmer", + value: 1, + }, + rgbw: [255, 0, 0, 0], + speed: 0, + reset: false, }, - rgbw: [255, 0, 0, 0], - speed: 0, - reset: false, - }, - { - startAddress: 43, - pan: 0, - tilt: 0, - brightness: { - type: "dimmer", - value: 1, - }, - rgbw: [0, 255, 0, 0], - speed: 1, - reset: false, - } -]; + { + startAddress: 43, + pan: 0, + tilt: 0, + brightness: { + type: "dimmer", + value: 1, + }, + rgbw: [0, 255, 0, 0], + speed: 1, + reset: false, + } + ]; -r = o.set(movingHeads); -console.log(r); + let r = output.set(movingHeads); + console.log(r); -o.close(); + output.close(); + +} \ No newline at end of file