Implemeeeeeent

This commit is contained in:
Kai Vogelgesang 2021-11-10 00:38:22 +01:00
parent a5dbe91bc1
commit 1bc7c6ab27
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
14 changed files with 301 additions and 406 deletions

View File

@ -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<string, Pattern>;
patternOutputs: Map<string, PatternOutput>;
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<MovingHeadState> = 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;

View File

@ -11,28 +11,20 @@
import 'core-js/stable'; import 'core-js/stable';
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
import path from 'path'; import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron'; import { app, BrowserWindow, ipcMain, shell } from 'electron';
import { resolveHtmlPath } from './util'; 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 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 createWindow = async () => {
const RESOURCES_PATH = app.isPackaged const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets') ? path.join(process.resourcesPath, 'assets')
@ -99,5 +91,9 @@ app
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow(); if (mainWindow === null) createWindow();
}); });
app.on('quit', () => {
clearInterval(mainLoop);
backend.close();
})
}) })
.catch(console.log); .catch(console.log);

View File

@ -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;
}
}

View File

@ -1,21 +1,5 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
const rust = require('rust_native_module');
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
ipcRenderer: { ipcRenderer: 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
}); });

View File

@ -0,0 +1,15 @@
import { MovingHeadState } from "rust_native_module";
export const blackout: Array<MovingHeadState> = []
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,
})
}

View File

@ -1,8 +1,13 @@
import { MovingHeadState } from 'rust_native_module'; import { MovingHeadState } from 'rust_native_module';
import { Pattern, Time } from './proto'; import { Pattern, PatternOutput, Time } from './proto';
export class ChaserPattern implements Pattern { export class ChaserPattern implements Pattern {
render(time: Time): Array<MovingHeadState> { render(time: Time): PatternOutput {
if (time.beat_relative === null) {
return null;
}
let head_number = Math.ceil(time.beat_relative) % 4; let head_number = Math.ceil(time.beat_relative) % 4;
let template: MovingHeadState = { let template: MovingHeadState = {

View File

@ -2,9 +2,11 @@ import { MovingHeadState } from "rust_native_module";
export type Time = { export type Time = {
absolute: number, absolute: number,
beat_relative: number, // beat_relative: number | null,
}; };
export type PatternOutput = Array<MovingHeadState> | null;
export interface Pattern { export interface Pattern {
render(time: Time): Array<MovingHeadState>; render(time: Time): PatternOutput;
} }

View File

@ -0,0 +1 @@
export const startAddresses = [1, 15, 29, 43]

View File

@ -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;
})
}
}

View File

@ -2,6 +2,7 @@ import { MemoryRouter as Router, Switch, Route } from 'react-router-dom';
import { IpcRenderer } from 'electron/renderer'; import { IpcRenderer } from 'electron/renderer';
import './App.css'; import './App.css';
import { useEffect, useState } from 'react';
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
@ -9,20 +10,35 @@ function tap() {
ipcRenderer.send("beat-tracking", "tap"); ipcRenderer.send("beat-tracking", "tap");
} }
const Hello = () => { const Frontend: React.FC = () => {
return ( const [state, setState] = useState<any>();
const pollMain = async () => {
const reply = await ipcRenderer.invoke("poll");
setState(reply);
}
useEffect(() => {
const interval = setInterval(pollMain, 20);
return () => clearInterval(interval);
});
return <>
<div>
State: {state ? JSON.stringify(state) : "undef"}
</div>
<div> <div>
<button onClick={tap}>Tap</button> <button onClick={tap}>Tap</button>
</div> </div>
); </>;
}; };
export default function App() { export default function App() {
return ( return (
<Router> <Router>
<Switch> <Switch>
<Route path="/" component={Hello} /> <Route path="/" component={Frontend} />
</Switch> </Switch>
</Router> </Router>
); );

View File

@ -1,6 +1,6 @@
declare module rust_native_module { declare module rust_native_module {
type Result<T> = type Result<T> =
{ type: "success" } & T { type: "success", value: T }
| { type: "error", message: string }; | { type: "error", message: string };
type Option<T> = type Option<T> =
@ -26,8 +26,8 @@ declare module rust_native_module {
} }
type OutputHandle = { type OutputHandle = {
set: (heads: Array<MovingHeadState>) => Result<{}>, set: (heads: Array<MovingHeadState>) => Result<never>,
close: () => Result<{}>, close: () => Result<never>,
} }
type BeatTrackerHandle = { type BeatTrackerHandle = {
@ -36,7 +36,7 @@ declare module rust_native_module {
} }
function listPorts(): Array<string>; function listPorts(): Array<string>;
function openOutput(): Result<OutputHandle>; function openOutput(port: string): Result<OutputHandle>;
function getBeatTracker(): Result<BeatTrackerHandle>; function getBeatTracker(): Result<BeatTrackerHandle>;
} }

View File

@ -11,19 +11,22 @@ impl Finalize for Metronome {}
pub fn get_beat_tracker(mut cx: FunctionContext) -> JsResult<JsObject> { pub fn get_beat_tracker(mut cx: FunctionContext) -> JsResult<JsObject> {
let obj = cx.empty_object(); 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()); let success_string = cx.string("success".to_string());
obj.set(&mut cx, "type", success_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) Ok(obj)
} }

View File

@ -29,31 +29,33 @@ pub fn open_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let path = path.value(&mut cx); let path = path.value(&mut cx);
let obj = cx.empty_object(); let obj = cx.empty_object();
let success_string; let value_obj = cx.empty_object();
match Controller::new(path) { match Controller::new(path) {
Ok(controller) => { 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)); 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)?; 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)?; 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) => { 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()); let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?; obj.set(&mut cx, "message", error_message)?;
} }
} }
obj.set(&mut cx, "type", success_string)?;
Ok(obj) Ok(obj)
} }

View File

@ -2,11 +2,11 @@ const mod = require('.')
const o = mod.openOutput("/dev/ttyUSB0") const o = mod.openOutput("/dev/ttyUSB0")
if (o.type !== "success") { if (o.type == 'success') {
return
}
const movingHeads = [ const output = o.value;
const movingHeads = [
{ {
startAddress: 1, startAddress: 1,
pan: 0, pan: 0,
@ -55,10 +55,12 @@ const movingHeads = [
speed: 1, speed: 1,
reset: false, reset: false,
} }
]; ];
r = o.set(movingHeads); let r = output.set(movingHeads);
console.log(r); console.log(r);
o.close(); output.close();
}