Implemeeeeeent

This commit is contained in:
2021-11-10 00:38:22 +01:00
parent a5dbe91bc1
commit 1bc7c6ab27
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 '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);

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

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 { Pattern, Time } from './proto';
import { Pattern, PatternOutput, Time } from './proto';
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 template: MovingHeadState = {

View File

@@ -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<MovingHeadState> | null;
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 './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<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>
<button onClick={tap}>Tap</button>
</div>
);
</>;
};
export default function App() {
return (
<Router>
<Switch>
<Route path="/" component={Hello} />
<Route path="/" component={Frontend} />
</Switch>
</Router>
);