This commit is contained in:
Kai Vogelgesang 2021-11-09 11:25:24 +01:00
parent c956ff08e5
commit d0ba6312c2
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
12 changed files with 422 additions and 360 deletions

View File

@ -12,57 +12,28 @@ import 'core-js/stable';
import 'regenerator-runtime/runtime';
import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
import { resolveHtmlPath } from './util';
export default class AppUpdater {
constructor() {
log.transports.file.level = 'info';
autoUpdater.logger = log;
autoUpdater.checkForUpdatesAndNotify();
}
}
import rust from 'rust_native_module';
let mainWindow: BrowserWindow | null = null;
ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
console.log(msgTemplate(arg));
event.reply('ipc-example', msgTemplate('pong'));
});
let beat_tracker = rust.getBeatTracker();
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
// 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 isDevelopment =
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
if (isDevelopment) {
require('electron-debug')();
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS'];
return installer
.default(
extensions.map((name) => installer[name]),
forceDownload
)
.catch(console.log);
};
const createWindow = async () => {
if (isDevelopment) {
await installExtensions();
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
@ -81,6 +52,8 @@ const createWindow = async () => {
},
});
mainWindow.removeMenu();
mainWindow.loadURL(resolveHtmlPath('index.html'));
mainWindow.on('ready-to-show', () => {
@ -98,18 +71,11 @@ const createWindow = async () => {
mainWindow = null;
});
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
// Open urls in the user's browser
mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
// Remove this if your app does not use auto updates
// eslint-disable-next-line
new AppUpdater();
};
/**

View File

@ -3,23 +3,19 @@ const rust = require('rust_native_module');
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
myPing() {
ipcRenderer.send('ipc-example', 'ping');
send(channel, data) {
const validChannels = ['beat-tracking'];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on(channel, func) {
const validChannels = ['ipc-example'];
const validChannels = ['tick', 'beat-tracking'];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
once(channel, func) {
const validChannels = ['ipc-example'];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.once(channel, (event, ...args) => func(...args));
}
},
},
rustding: rust
});

View File

@ -0,0 +1,33 @@
import { MovingHeadState } from 'rust_native_module';
import { Pattern, Time } from './proto';
export class ChaserPattern implements Pattern {
render(time: Time): Array<MovingHeadState> {
let head_number = Math.ceil(time.beat_relative) % 4;
let template: MovingHeadState = {
startAddress: 0,
pan: 0,
tilt: 0,
brightness: {
type: 'dimmer',
value: 0.2,
},
rgbw: [255, 0, 0, 0],
speed: 0
}
let result = [];
for (let [i, startAddress] of [1, 15, 29, 43].entries()) {
result[i] = template;
result[i].startAddress = startAddress;
if (i === head_number) {
result[i].brightness = { type: 'dimmer', value: 1 };
}
}
return result;
}
}

View File

@ -0,0 +1,10 @@
import { MovingHeadState } from "rust_native_module";
export type Time = {
absolute: number,
beat_relative: number,
};
export interface Pattern {
render(time: Time): Array<MovingHeadState>;
}

View File

@ -1,62 +0,0 @@
/*
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
* See https://github.com/webpack-contrib/sass-loader#imports
*/
body {
position: relative;
color: white;
height: 100vh;
background: linear-gradient(
200.96deg,
#fedc2a -29.09%,
#dd5789 51.77%,
#7a2c9e 129.35%
);
font-family: sans-serif;
overflow-y: hidden;
display: flex;
justify-content: center;
align-items: center;
}
button {
background-color: white;
padding: 10px 20px;
border-radius: 10px;
border: none;
appearance: none;
font-size: 1.3rem;
box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12),
0px 18px 88px -4px rgba(24, 39, 75, 0.14);
transition: all ease-in 0.1s;
cursor: pointer;
opacity: 0.9;
}
button:hover {
transform: scale(1.05);
opacity: 1;
}
li {
list-style: none;
}
a {
text-decoration: none;
height: fit-content;
width: fit-content;
margin: 10px;
}
a:hover {
opacity: 1;
text-decoration: none;
}
.Hello {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
}

View File

@ -1,23 +1,19 @@
import { MemoryRouter as Router, Switch, Route } from 'react-router-dom';
import icon from '../../assets/icon.svg';
import { IpcRenderer } from 'electron/renderer';
import './App.css';
import rust from 'rust_native_module';
const deinelib = ((window as any).electron.rustding) as (typeof rust);
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
function tap() {
ipcRenderer.send("beat-tracking", "tap");
}
const Hello = () => {
const {hello} = deinelib;
return (
<div>
<div className="Hello">
<img width="200px" alt="icon" src={icon} />
</div>
<h1>electron-react-boilerplate</h1>
<div className="Hello">
<p>Rust says {hello()}</p>
</div>
<button onClick={tap}>Tap</button>
</div>
);
};

View File

@ -2,17 +2,9 @@
<html>
<head>
<meta charset="utf-8" />
<title>Hello Electron React!</title>
<title>Abnormal krass episch wylde Lightshow</title>
</head>
<body>
<div id="root"></div>
</body>
<script>
window.electron.ipcRenderer.once('ipc-example', (arg) => {
// eslint-disable-next-line no-console
console.log(arg);
});
window.electron.ipcRenderer.myPing();
</script>
</html>

View File

@ -1,31 +1,42 @@
type Result<T> =
declare module rust_native_module {
type Result<T> =
{ type: "success" } & T
| { type: "error", message: string };
type Brightness =
type Option<T> =
{ type: "some", value: T }
| { type: "none" }
type Brightness =
{ type: "off" }
| { type: "switch" }
| { type: "dimmer", value: number }
| { type: "strobe", value: number };
type MovingHeadState = {
start_address: number, // [0, 512]
type MovingHeadState = {
startAddress: number, // [0, 512]
pan: number, // [-3pi/2, 3pi/2]
tilt: number, // [-pi/2, pi/2]
brightness: Brightness,
rgbw: [number, number, number, number], // RGBW, [0, 255]
speed: number, // [255, 0]
}
}
type OutputHandle = {
set: (heads: [MovingHeadState, MovingHeadState, MovingHeadState, MovingHeadState]) => Result<{}>,
type OutputHandle = {
set: (heads: Array<MovingHeadState>) => Result<{}>,
close: () => Result<{}>,
}
}
type BeatTrackerHandle = {
tap: () => void,
getProgress: () => Option<number>,
}
declare module rust_native_module {
function listPorts(): Array<string>;
function openOutput(): Result<OutputHandle>;
function getBeatTracker(): Result<BeatTrackerHandle>;
}
export = rust_native_module;

View File

@ -0,0 +1,47 @@
use std::time::{Duration, Instant};
pub struct Metronome {
taps: Vec<Instant>,
beat_interval: Option<u128>,
timeout: Duration,
}
impl Metronome {
pub fn new(timeout: Duration) -> Self {
Self {
taps: Vec::new(),
beat_interval: None,
timeout,
}
}
pub fn tap(&mut self) {
let now = Instant::now();
if let Some(t) = self.taps.last() {
if now - *t > self.timeout {
self.taps.clear();
self.beat_interval = None;
}
}
self.taps.push(now);
let n = self.taps.len();
if n >= 2 {
let dt = *self.taps.last().unwrap() - *self.taps.first().unwrap();
let interval_millis = dt.as_millis() / (n - 1) as u128;
self.beat_interval = Some(interval_millis);
}
}
pub fn current_beat_progress(&self) -> Option<f64> {
if self.beat_interval.is_none() {
return None;
}
let now = Instant::now();
let relative_millis = (now - *self.taps.last().unwrap()).as_millis();
Some(relative_millis as f64 / self.beat_interval.unwrap() as f64)
}
}

View File

@ -0,0 +1,67 @@
mod metronome;
use std::{cell::RefCell, time::Duration};
use metronome::Metronome;
use neon::prelude::*;
type BoxedTracker = JsBox<RefCell<Metronome>>;
impl Finalize for Metronome {}
pub fn get_beat_tracker(mut cx: FunctionContext) -> JsResult<JsObject> {
let obj = cx.empty_object();
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)
}
pub fn tap(mut cx: FunctionContext) -> JsResult<JsValue> {
let this = cx.this();
let boxed_tracker = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedTracker, _>(&mut cx)?;
boxed_tracker.borrow_mut().tap();
Ok(JsUndefined::new(&mut cx).upcast())
}
pub fn get_progress(mut cx: FunctionContext) -> JsResult<JsObject> {
let this = cx.this();
let boxed_tracker = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedTracker, _>(&mut cx)?;
let obj = cx.empty_object();
let type_string;
match boxed_tracker.borrow().current_beat_progress() {
Some(progress) => {
type_string = cx.string("some".to_string());
let progress = cx.number(progress);
obj.set(&mut cx, "value", progress)?;
}
None => {
type_string = cx.string("none".to_string());
}
}
obj.set(&mut cx, "type", type_string)?;
Ok(obj)
}

View File

@ -1,199 +1,13 @@
pub mod output;
use std::cell::RefCell;
use output::controller::Controller;
use output::fixtures::{Brightness, DMXFixture, MovingHead};
type BoxedController = JsBox<RefCell<Controller>>;
impl Finalize for Controller {}
mod beat_tracking;
mod output;
use neon::prelude::*;
fn list_ports(mut cx: FunctionContext) -> JsResult<JsArray> {
let array = cx.empty_array();
if let Ok(ports) = serialport::available_ports() {
for (i, port) in ports.iter().enumerate() {
let port_name = cx.string(&port.port_name);
array.set(&mut cx, i as u32, port_name)?;
}
}
Ok(array)
}
fn open_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let path = cx.argument::<JsString>(0)?;
let path = path.value(&mut cx);
let obj = cx.empty_object();
let success_string;
match Controller::new(path) {
Ok(controller) => {
success_string = cx.string("success");
let boxed_controller = cx.boxed(RefCell::new(controller));
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)?;
let close_function = JsFunction::new(&mut cx, close_output)?;
obj.set(&mut cx, "close", close_function)?;
}
Err(e) => {
success_string = cx.string("error");
let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?;
}
}
obj.set(&mut cx, "type", success_string)?;
Ok(obj)
}
fn set_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let arg = cx.argument::<JsArray>(0)?;
let mut moving_heads = Vec::with_capacity(4);
for i in 0..4 {
// why isn't neon-serde maintained anymore? T_T
let head = arg
.get(&mut cx, i)?
.downcast_or_throw::<JsObject, _>(&mut cx)?;
moving_heads.push(MovingHead {
start_address: head
.get(&mut cx, "startAddress")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as usize,
pan: head
.get(&mut cx, "pan")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
tilt: head
.get(&mut cx, "tilt")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
brightness: {
let brightness = head
.get(&mut cx, "brightness")?
.downcast_or_throw::<JsObject, _>(&mut cx)?;
match brightness
.get(&mut cx, "type")?
.downcast_or_throw::<JsString, _>(&mut cx)?
.value(&mut cx)
.as_str()
{
"off" => Brightness::Off,
"dimmer" => Brightness::Dimmer(
brightness
.get(&mut cx, "value")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
),
"strobe" => Brightness::Strobe(
brightness
.get(&mut cx, "value")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
),
"switch" => Brightness::Switch,
s => {
let err = cx.string(format!("Invalid brightness type: {}", s));
return cx.throw(err);
}
}
},
rgbw: {
let rgbw = head
.get(&mut cx, "rgbw")?
.downcast_or_throw::<JsArray, _>(&mut cx)?;
let mut vec = Vec::new();
for i in 0..4 {
let v = rgbw
.get(&mut cx, i)?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as u8;
vec.push(v);
}
(vec[0], vec[1], vec[2], vec[3])
},
speed: head
.get(&mut cx, "speed")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as u8,
})
}
let this = cx.this();
let boxed_controller = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedController, _>(&mut cx)?;
let boxed_controller = boxed_controller.borrow_mut();
{
let mut state = boxed_controller.state.lock().unwrap();
for head in moving_heads {
head.render(&mut state.data);
}
}
let obj = cx.empty_object();
let success_text = cx.string("success".to_string());
obj.set(&mut cx, "type", success_text)?;
Ok(obj)
}
fn close_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let this = cx.this();
let boxed_controller = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedController, _>(&mut cx)?;
let obj = cx.empty_object();
let success_string;
match boxed_controller.borrow_mut().stop() {
Ok(()) => {
success_string = cx.string("success");
}
Err(e) => {
success_string = cx.string("error");
let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?;
}
}
obj.set(&mut cx, "type", success_string)?;
Ok(obj)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("listPorts", list_ports)?;
cx.export_function("openOutput", open_output)?;
cx.export_function("listPorts", output::list_ports)?;
cx.export_function("openOutput", output::open_output)?;
cx.export_function("getBeatTracker", beat_tracking::get_beat_tracker)?;
Ok(())
}

View File

@ -1,2 +1,194 @@
pub mod fixtures;
pub mod controller;
mod controller;
mod fixtures;
use std::cell::RefCell;
use controller::Controller;
use fixtures::{Brightness, DMXFixture, MovingHead};
type BoxedController = JsBox<RefCell<Controller>>;
impl Finalize for Controller {}
use neon::prelude::*;
pub fn list_ports(mut cx: FunctionContext) -> JsResult<JsArray> {
let array = cx.empty_array();
if let Ok(ports) = serialport::available_ports() {
for (i, port) in ports.iter().enumerate() {
let port_name = cx.string(&port.port_name);
array.set(&mut cx, i as u32, port_name)?;
}
}
Ok(array)
}
pub fn open_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let path = cx.argument::<JsString>(0)?;
let path = path.value(&mut cx);
let obj = cx.empty_object();
let success_string;
match Controller::new(path) {
Ok(controller) => {
success_string = cx.string("success");
let boxed_controller = cx.boxed(RefCell::new(controller));
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)?;
let close_function = JsFunction::new(&mut cx, close_output)?;
obj.set(&mut cx, "close", close_function)?;
}
Err(e) => {
success_string = cx.string("error");
let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?;
}
}
obj.set(&mut cx, "type", success_string)?;
Ok(obj)
}
fn set_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let arg = cx.argument::<JsArray>(0)?;
let mut moving_heads = Vec::with_capacity(4);
for i in 0..arg.len(&mut cx) {
// why isn't neon-serde maintained anymore? T_T
let head = arg
.get(&mut cx, i)?
.downcast_or_throw::<JsObject, _>(&mut cx)?;
moving_heads.push(MovingHead {
start_address: head
.get(&mut cx, "startAddress")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as usize,
pan: head
.get(&mut cx, "pan")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
tilt: head
.get(&mut cx, "tilt")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
brightness: {
let brightness = head
.get(&mut cx, "brightness")?
.downcast_or_throw::<JsObject, _>(&mut cx)?;
match brightness
.get(&mut cx, "type")?
.downcast_or_throw::<JsString, _>(&mut cx)?
.value(&mut cx)
.as_str()
{
"off" => Brightness::Off,
"dimmer" => Brightness::Dimmer(
brightness
.get(&mut cx, "value")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
),
"strobe" => Brightness::Strobe(
brightness
.get(&mut cx, "value")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
),
"switch" => Brightness::Switch,
s => {
let err = cx.string(format!("Invalid brightness type: {}", s));
return cx.throw(err);
}
}
},
rgbw: {
let rgbw = head
.get(&mut cx, "rgbw")?
.downcast_or_throw::<JsArray, _>(&mut cx)?;
let mut vec = Vec::new();
for i in 0..4 {
let v = rgbw
.get(&mut cx, i)?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as u8;
vec.push(v);
}
(vec[0], vec[1], vec[2], vec[3])
},
speed: head
.get(&mut cx, "speed")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as u8,
})
}
let this = cx.this();
let boxed_controller = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedController, _>(&mut cx)?;
let boxed_controller = boxed_controller.borrow_mut();
{
let mut state = boxed_controller.state.lock().unwrap();
for head in moving_heads {
head.render(&mut state.data);
}
}
let obj = cx.empty_object();
let success_text = cx.string("success".to_string());
obj.set(&mut cx, "type", success_text)?;
Ok(obj)
}
fn close_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let this = cx.this();
let boxed_controller = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedController, _>(&mut cx)?;
let obj = cx.empty_object();
let success_string;
match boxed_controller.borrow_mut().stop() {
Ok(()) => {
success_string = cx.string("success");
}
Err(e) => {
success_string = cx.string("error");
let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?;
}
}
obj.set(&mut cx, "type", success_string)?;
Ok(obj)
}