Compare commits

..

22 Commits

Author SHA1 Message Date
90db3e61f5 Update webserial default code 2023-06-23 15:50:43 +02:00
4cb1f6ea4d Init webui 2023-06-23 15:50:17 +02:00
162103e54b Refactor default code, Improve stdlib, Implement Flower 2022-12-29 15:15:59 +01:00
85ea070632 Make code persistent 2022-12-29 14:26:27 +01:00
2dba389d48 Add bulma / styling 2022-12-29 13:53:51 +01:00
7720005194 Fix editor not resizing 2022-12-29 13:05:02 +01:00
9eac3510fa Refactor code store to contain non-transpiled TypeScript 2022-12-29 12:59:26 +01:00
cac24e64b1 Fix vite building with absolute paths 2022-12-28 02:38:26 +01:00
1de834020a Implement fixtures and library 2022-12-28 02:30:36 +01:00
60b2b56d8f Implement editor poc 2022-12-27 23:46:04 +01:00
cd16208634 Implement slider poc 2022-12-25 17:33:54 +01:00
d85bf03011 Implement WebSerial I/O 2022-12-25 03:33:43 +01:00
444ca860fa Init WebSerial 2022-12-25 02:12:07 +01:00
0d13c7604f Add hot reload example 2022-12-21 16:53:06 +01:00
6f6e0fd665 Implement DMX serial output 2022-03-22 21:35:16 +01:00
62636fa2f9 Add pult frontend stuff 2022-02-19 11:47:50 +01:00
763aa23ca8 Update 2021-10-28 03:07:03 +02:00
5a71aa2cf0 Cleanup 2021-10-28 01:32:37 +02:00
30a4f83d32 Refactor 2021-10-28 01:31:19 +02:00
9f07172f60 Implement beat detection -> dmx output 2021-10-27 22:44:51 +02:00
b0fecde639 Add sphere movement notebook 2021-10-27 13:57:59 +02:00
1ebe1d0323 Fix minor beat_detection warnings 2021-10-27 13:57:46 +02:00
75 changed files with 36170 additions and 195 deletions

View File

@@ -11,3 +11,7 @@ psimple = { version = "2.23.0", package = "libpulse-simple-binding" }
anyhow = "1.0.44"
sdl2 = "0.35.1"
ringbuffer = "0.8.3"
num = "0.4.0"
serialport = "4.0.1"
palette = "0.6.0"
rand = "0.8.4"

View File

@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use psimple::Simple;
use pulse::context::{Context, FlagSet as ContextFlagSet};
use pulse::mainloop::standard::{IterateResult, Mainloop};
use pulse::sample::{Format, Spec};
use pulse::sample::Spec;
use pulse::stream::Direction;
use std::cell::RefCell;
use std::ops::Deref;

View File

@@ -0,0 +1,113 @@
use std::{
io,
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
};
use anyhow::{anyhow, Result};
use serialport::SerialPort;
use crate::fixtures::{DMXFixture, MovingHead};
const FPS: u32 = 50;
enum MCUResponse {
Sync,
Ack,
#[allow(dead_code)]
Info {
num_pkts: u32,
},
}
fn poll_response(ser: &mut dyn SerialPort) -> Result<MCUResponse> {
let mut read_buffer = vec![0u8; 32];
let bytes_read;
loop {
match ser.read(read_buffer.as_mut_slice()) {
Ok(t) => {
bytes_read = t;
break;
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => continue,
Err(e) => Err(e),
}?
}
let response = std::str::from_utf8(&read_buffer[..bytes_read])?;
match response.trim() {
"Sync." => Ok(MCUResponse::Sync),
"Ack." => Ok(MCUResponse::Ack),
s if s.starts_with("Info") => Ok(MCUResponse::Info { num_pkts: 69 }),
s => Err(anyhow!("Unknown response: \"{}\"", s)),
}
}
pub fn controller_thread(
running: Arc<Mutex<bool>>,
movingheads: Arc<Mutex<[MovingHead; 4]>>,
) -> Result<()> {
let frame_time = Duration::from_secs_f64(1.0 / FPS as f64);
let mut dmx_buffer = [0u8; 512];
let mut ser = serialport::new("/dev/ttyUSB0", 500_000)
.timeout(Duration::from_millis(10))
.open()?;
// wait for initial sync
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => break,
_ => continue,
}
}
'main: loop {
{
let running = running.lock().unwrap();
if !*running {
break Ok(());
}
}
let loop_start = Instant::now();
{
let movingheads = movingheads.lock().unwrap();
for head in movingheads.iter() {
head.render(&mut dmx_buffer);
}
}
let write_result = ser.write(&dmx_buffer);
if write_result.is_err() {
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => continue 'main,
_ => continue,
}
}
}
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Ack) => break,
Ok(MCUResponse::Info { .. }) | Err(_) => continue,
Ok(MCUResponse::Sync) => continue 'main,
}
}
let loop_time = loop_start.elapsed();
if loop_time < frame_time {
thread::sleep(frame_time - loop_time);
} else {
println!("loop took too long!");
}
}
}

View File

@@ -0,0 +1,53 @@
use std::f32::consts::{FRAC_PI_2, PI};
use crate::util::rescale;
pub struct MovingHead {
start_addr: usize,
pub pan: f32, // -3pi/2 to 3pi/2
pub tilt: f32, // -pi/2 to pi/2
pub dimmer: f32, // 0 to 1
pub rgbw: (u8, u8, u8, u8),
pub speed: u8, // reversed
}
impl MovingHead {
pub fn new(start_addr: usize) -> Self {
Self {
start_addr,
pan: 0f32,
tilt: 0f32,
speed: 0u8,
dimmer: 0f32,
rgbw: (0u8, 0u8, 0u8, 0u8),
}
}
}
pub trait DMXFixture {
fn render(&self, dst: &mut [u8]);
}
impl DMXFixture for MovingHead {
fn render(&self, dst: &mut [u8]) {
let pan = rescale(self.pan, (-1.5 * PI, 1.5 * PI), (255.0, 0.0)) as u8;
let pan_fine = 0;
let tilt = rescale(self.tilt, (-1.0 * FRAC_PI_2, FRAC_PI_2), (0.0, 255.0)) as u8;
let tilt_fine = 0;
let dimmer = (7 + (127.0 * self.dimmer) as u8).clamp(7, 134);
let (r, g, b, w) = self.rgbw;
let offset = self.start_addr - 1;
let channels = [
pan, pan_fine, tilt, tilt_fine, self.speed, dimmer, r, g, b, w, 0, 0, 0, 0,
];
dst[offset..offset + channels.len()].copy_from_slice(&channels);
}
}

View File

@@ -0,0 +1,288 @@
use std::{
sync::{Arc, Mutex},
thread::{self, JoinHandle},
};
use anyhow::Result;
use pulse::sample::{Format, Spec};
use ringbuffer::{ConstGenericRingBuffer, RingBufferExt, RingBufferWrite};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::Color,
rect::{Point, Rect},
};
use crate::{
capture,
dsp::{self, ZTransformFilter},
};
use super::OutputLayer;
const SAMPLE_RATE: usize = 5000;
const PULSE_UPDATES_PER_SECOND: usize = 50;
const BUFFER_SIZE: usize = SAMPLE_RATE / PULSE_UPDATES_PER_SECOND;
const POINT_COUNT: usize = SAMPLE_RATE / 200;
const POINT_BUFFER_SIZE: usize = POINT_COUNT.next_power_of_two();
const MIN_MAX_SAMPLE_COUNT: usize = 10;
pub struct BeatDetectinator {
join_handle: Option<JoinHandle<Result<()>>>,
shared_state: Arc<Mutex<BeatDetectinatorSharedState>>,
}
struct BeatDetectinatorSharedState {
running: bool,
brightness: f32,
threshold: f32,
point_buf: ConstGenericRingBuffer<f32, POINT_BUFFER_SIZE>,
}
impl BeatDetectinator {
pub fn new() -> Self {
let shared_state = Arc::new(Mutex::new(BeatDetectinatorSharedState {
running: true,
brightness: 0.0,
threshold: 1.5,
point_buf: Default::default(),
}));
{
shared_state.lock().unwrap().point_buf.fill_default();
}
let join_handle = {
let shared_state = shared_state.clone();
Some(thread::spawn(move || audio_loop(shared_state)))
};
println!("Audio thread started.");
Self {
join_handle,
shared_state,
}
}
}
impl Drop for BeatDetectinator {
fn drop(&mut self) {
{
self.shared_state.lock().unwrap().running = false;
}
match self.join_handle.take().unwrap().join().unwrap() {
Ok(_) => println!("Audio thread stopped."),
Err(e) => println!("Audio thread died: {:?}", e),
}
}
}
fn audio_loop(shared_state: Arc<Mutex<BeatDetectinatorSharedState>>) -> Result<()> {
let spec = Spec {
format: Format::F32le,
rate: SAMPLE_RATE as u32,
channels: 1,
};
assert!(spec.is_valid());
let reader = capture::get_audio_reader(&spec)?;
let mut buffer = [0u8; 4 * BUFFER_SIZE];
let mut bass_filter = dsp::BassFilter::default();
let mut envelope_filter = dsp::EnvelopeFilter::default();
let mut beat_filter = dsp::BeatFilter::default();
let mut j = 0;
loop {
{
if shared_state.lock().unwrap().running == false {
break Ok(());
}
}
reader.read(&mut buffer)?;
for i in 0..BUFFER_SIZE {
let mut float_bytes = [0u8; 4];
float_bytes.copy_from_slice(&buffer[4 * i..4 * i + 4]);
j += 1;
let sample = f32::from_le_bytes(float_bytes);
let mut value = bass_filter.process(sample);
if value < 0f32 {
value = -value;
}
let envelope = envelope_filter.process(value);
if j == 200 {
let beat = beat_filter.process(envelope);
shared_state.lock().unwrap().point_buf.push(beat);
j = 0;
}
}
}
}
impl OutputLayer for BeatDetectinator {
fn tick(&mut self, _dt: std::time::Duration) {}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, texture_size: u32) {
let min_y = 0f32;
let max_y = 3f32;
let beat = {
self.shared_state
.lock()
.unwrap()
.point_buf
.back()
.unwrap()
.clone()
};
let get_y = |y: &f32| {
let mut y = y.clone();
if y <= 0f32 {
y = 0f32;
}
y = (1f32 + y).log2();
let y = (y - min_y) / (max_y - min_y);
((1f32 - y) * texture_size as f32) as u32
};
// background
let v;
let (threshold, points) = {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.brightness = if beat > shared_state.threshold {
1f32
} else {
0.75f32 * shared_state.brightness
};
v = (255f32 * shared_state.brightness) as u8;
(
shared_state.threshold.clone(),
shared_state.point_buf.to_vec(),
)
};
canvas.set_draw_color(Color::RGB(v, v, v));
canvas.clear();
// zero
canvas.set_draw_color(Color::RGB(0, 128, 0));
let y = get_y(&0f32);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// min / max lines
canvas.set_draw_color(Color::RGB(255, 0, 0));
let min_beat = points
.iter()
.skip(points.len() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a < b { a } else { b })
.unwrap();
let x = texture_size - MIN_MAX_SAMPLE_COUNT as u32 * 10;
let y = get_y(min_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
let max_beat = points
.iter()
.skip(points.len() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a > b { a } else { b })
.unwrap();
let y = get_y(max_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// threshhold line
canvas.set_draw_color(Color::RGB(0, 0, 255));
let y = get_y(&threshold);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// values
canvas.set_draw_color(Color::RGB(0, 255, 0));
for (i, beat) in points.iter().skip(points.len() - POINT_COUNT).enumerate() {
let x = 10 * i;
let y = get_y(beat);
canvas
.fill_rect(Rect::new((x + 1) as i32, (y - 1) as i32, 8, 3))
.unwrap();
}
}
fn on_sdl_event(&mut self, event: &Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Up),
..
} => {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.threshold += 0.01f32;
println!("threshold: {:.2}", shared_state.threshold);
}
Event::KeyDown {
keycode: Some(Keycode::Down),
..
} => {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.threshold -= 0.01f32;
println!("threshold: {:.2}", shared_state.threshold);
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
let shared_state = self.shared_state.lock().unwrap();
for head in output.iter_mut() {
head.dimmer = 0.2 + 0.8 * shared_state.brightness;
}
}
}

View File

@@ -0,0 +1,24 @@
use super::OutputLayer;
pub struct ConstantBrightness {
pub brightness: f32,
}
impl OutputLayer for ConstantBrightness {
fn tick(&mut self, _dt: std::time::Duration) {}
fn draw_sdl(
&self,
_canvas: &mut sdl2::render::Canvas<sdl2::video::Window>,
_texture_size: u32,
) {
}
fn on_sdl_event(&mut self, _event: &sdl2::event::Event) {}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
for head in output.iter_mut() {
head.dimmer = self.brightness;
}
}
}

View File

@@ -0,0 +1,53 @@
use std::time::Duration;
use palette::{Hsl, IntoColor, Pixel, Srgb};
use sdl2::{pixels::Color, rect::Rect};
use crate::layers::OutputLayer;
pub struct HSLCycle {
cycle_millis: usize,
time: usize,
rgb: [u8; 3],
}
impl HSLCycle {
pub fn new(cycle_length: Duration) -> Self {
Self {
cycle_millis: cycle_length.as_millis() as usize,
time: 0,
rgb: [0, 0, 0],
}
}
}
impl OutputLayer for HSLCycle {
fn tick(&mut self, dt: std::time::Duration) {
self.time += dt.as_millis() as usize;
self.time %= self.cycle_millis;
let hsl: Srgb = Hsl::new(
360.0 * (self.time as f32 / self.cycle_millis as f32),
1.0,
0.5,
)
.into_color();
self.rgb = hsl.into_format().into_raw();
}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, _texture_size: u32) {
let [r, g, b] = self.rgb;
canvas.set_draw_color(Color::RGB(r, g, b));
canvas.fill_rect(Rect::new(0, 0, 5, 5)).unwrap();
}
fn on_sdl_event(&mut self, _event: &sdl2::event::Event) {}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
let [r, g, b] = self.rgb;
for head in output.iter_mut() {
head.rgbw = (r, g, b, 0);
}
}
}

View File

@@ -0,0 +1,21 @@
pub mod beat_detector;
pub mod constant_brightness;
pub mod hsl_cycle;
pub mod movement;
pub mod tap;
use std::time::Duration;
use sdl2::{event::Event, render::Canvas, video::Window};
use crate::fixtures::MovingHead;
pub trait OutputLayer {
fn tick(&mut self, dt: Duration);
fn draw_sdl(&self, canvas: &mut Canvas<Window>, texture_size: u32);
fn on_sdl_event(&mut self, event: &Event);
fn update_dmx(&self, output: &mut [MovingHead; 4]);
}

View File

@@ -0,0 +1,78 @@
use std::{
f32::consts::PI,
time::{Duration, Instant},
};
use rand::random;
use sdl2::{event::Event, keyboard::Keycode};
use crate::util::rescale;
use super::OutputLayer;
pub struct HeadMover {
movement_duration: Duration,
last_timestamp: Instant,
targets: [(f32, f32); 4],
}
impl HeadMover {
pub fn new(movement_duration: Duration) -> Self {
Self {
movement_duration,
last_timestamp: Instant::now() - movement_duration,
targets: Default::default(),
}
}
}
fn sample_random_point() -> (f32, f32) {
let pan = rescale(random::<f32>(), (0.0, 1.0), (-0.5 * PI, 0.5 * PI));
let tilt = random::<f32>().acos();
(pan, tilt)
}
impl OutputLayer for HeadMover {
fn tick(&mut self, _dt: Duration) {
let now = Instant::now();
if now - self.last_timestamp >= self.movement_duration {
for target in self.targets.iter_mut() {
*target = sample_random_point();
}
self.last_timestamp = now;
}
}
fn draw_sdl(
&self,
_canvas: &mut sdl2::render::Canvas<sdl2::video::Window>,
_texture_size: u32,
) {
}
fn on_sdl_event(&mut self, event: &sdl2::event::Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Return),
..
} => {
let now = Instant::now();
for target in self.targets.iter_mut() {
*target = sample_random_point();
}
self.last_timestamp = now;
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
for (head, target) in output.iter_mut().zip(self.targets.iter()) {
let (pan, tilt) = target;
head.pan = *pan;
head.tilt = *tilt;
}
}
}

View File

@@ -0,0 +1,102 @@
use std::{
collections::VecDeque,
time::{Duration, Instant},
};
use sdl2::{event::Event, keyboard::Keycode, pixels::Color};
use super::OutputLayer;
pub struct TapMetronome {
max_time: Duration,
timestamps: VecDeque<Instant>,
seconds_per_beat: Option<f32>,
brightness: f32,
decay: f32,
}
impl TapMetronome {
pub fn new(max_time: Duration) -> Self {
Self {
max_time,
timestamps: VecDeque::new(),
seconds_per_beat: None,
brightness: 0.0,
decay: 1.0,
}
}
}
impl OutputLayer for TapMetronome {
fn tick(&mut self, _dt: std::time::Duration) {
let now = Instant::now();
if let (Some(stamp), Some(spb)) = (self.timestamps.back(), self.seconds_per_beat) {
let dt = (now - *stamp).as_secs_f32();
let beat_offset = dt % spb;
self.brightness = (-1.0 * self.decay * beat_offset).exp()
} else {
self.brightness = 0.0;
}
}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, _texture_size: u32) {
let v = (255.0 * self.brightness) as u8;
canvas.set_draw_color(Color::RGB(v, v, v));
canvas.clear();
}
fn on_sdl_event(&mut self, event: &sdl2::event::Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Space),
..
} => {
let now = Instant::now();
while let Some(stamp) = self.timestamps.front() {
if now - *stamp < self.max_time {
break;
}
self.timestamps.pop_front();
}
self.timestamps.push_back(now);
if self.timestamps.len() >= 2 {
let dt = *self.timestamps.back().unwrap() - *self.timestamps.front().unwrap();
let spb = dt.as_secs_f32() / (self.timestamps.len() - 1) as f32;
println!("Detected BPM: {:.2}", 60.0 / spb);
self.seconds_per_beat = Some(spb);
} else {
self.seconds_per_beat = None;
}
}
Event::KeyDown {
keycode: Some(Keycode::Down),
..
} => {
self.decay += 0.1;
println!("Decay: {:.3}", self.decay)
}
Event::KeyDown {
keycode: Some(Keycode::Up),
..
} => {
self.decay -= 0.1;
println!("Decay: {:.3}", self.decay)
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
for head in output.iter_mut() {
head.dimmer = 0.2 + 0.8 * self.brightness;
}
}
}

View File

@@ -1,49 +1,61 @@
mod capture;
mod dmx_controller;
mod dsp;
mod fixtures;
mod layers;
mod util;
use anyhow::Result;
use pulse::sample::{Format, Spec};
use ringbuffer::{ConstGenericRingBuffer, RingBuffer, RingBufferExt, RingBufferWrite};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::Color,
rect::{Point, Rect},
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
use crate::dsp::ZTransformFilter;
use anyhow::Result;
use layers::{
beat_detector::BeatDetectinator, constant_brightness::ConstantBrightness, hsl_cycle::HSLCycle,
movement::HeadMover, tap::TapMetronome, OutputLayer,
};
use sdl2::{event::Event, keyboard::Keycode, pixels::Color, rect::Rect};
use crate::dmx_controller::controller_thread;
use crate::fixtures::MovingHead;
const SAMPLE_RATE: usize = 5000;
const FPS: usize = 50;
const BUFFER_SIZE: usize = SAMPLE_RATE / FPS;
const POINT_COUNT: usize = SAMPLE_RATE / 200;
const POINT_BUFFER_SIZE: usize = POINT_COUNT.next_power_of_two();
const MIN_MAX_SAMPLE_COUNT: usize = 10;
enum BrightnessControl {
BeatDetection,
TapMetronome,
ConstantBrightness,
}
fn main() -> Result<()> {
let spec = Spec {
format: Format::F32le,
rate: SAMPLE_RATE as u32,
channels: 1,
// dmx thread
let dmx_running = Arc::new(Mutex::new(true));
let movingheads = Arc::new(Mutex::new([
MovingHead::new(1),
MovingHead::new(15),
MovingHead::new(29),
MovingHead::new(43),
]));
let dmx_thread = {
let dmx_running = dmx_running.clone();
let movingheads = movingheads.clone();
thread::spawn(move || controller_thread(dmx_running, movingheads))
};
assert!(spec.is_valid());
let reader = capture::get_audio_reader(&spec)?;
let mut buffer = [0u8; 4 * BUFFER_SIZE];
// output layers
let mut bass_filter = dsp::BassFilter::default();
let mut envelope_filter = dsp::EnvelopeFilter::default();
let mut beat_filter = dsp::BeatFilter::default();
let mut j = 0;
let mut beat_detector = BeatDetectinator::new();
let mut tap_metronome = TapMetronome::new(Duration::from_secs(2));
let mut constant_brightness = ConstantBrightness { brightness: 1.0 };
let mut threshold = 1.5f32;
let mut active_brightness_control = BrightnessControl::BeatDetection;
let mut brightness = 0f32;
let mut beat_toggle = false;
let mut hsl_cycle = HSLCycle::new(Duration::from_secs(5));
let mut head_mover = HeadMover::new(Duration::from_secs_f32(0.5));
// sdl
@@ -56,15 +68,14 @@ fn main() -> Result<()> {
.build()?;
let mut canvas = window.into_canvas().build()?;
let texture_creator = canvas.texture_creator();
let texture_size = 10 * POINT_COUNT as u32;
let texture_size = 250;
let mut texture = texture_creator.create_texture_target(
canvas.default_pixel_format(),
texture_size,
texture_size,
)?;
let mut point_buf = ConstGenericRingBuffer::<f32, POINT_BUFFER_SIZE>::new();
point_buf.fill_default();
let frame_duration = Duration::from_secs_f64(1.0 / FPS as f64);
let mut event_pump = sdl.event_pump().unwrap();
@@ -76,172 +87,78 @@ fn main() -> Result<()> {
keycode: Some(Keycode::Escape),
..
} => break 'running,
Event::KeyDown {
keycode: Some(Keycode::Up),
keycode: Some(Keycode::F1),
..
} => {
threshold += 0.01f32;
println!("threshold: {:.2}", threshold);
active_brightness_control = BrightnessControl::BeatDetection;
println!("Using automatic beat detection.")
}
Event::KeyDown {
keycode: Some(Keycode::Down),
keycode: Some(Keycode::F2),
..
} => {
threshold -= 0.01f32;
println!("threshold: {:.2}", threshold);
active_brightness_control = BrightnessControl::TapMetronome;
println!("Using tap metronome.")
}
Event::KeyDown {
keycode: Some(k), ..
keycode: Some(Keycode::F3),
..
} => {
println!("{}", k)
active_brightness_control = BrightnessControl::ConstantBrightness;
println!("Using constant brightness.")
}
event => {
match active_brightness_control {
BrightnessControl::BeatDetection => beat_detector.on_sdl_event(&event),
BrightnessControl::TapMetronome => tap_metronome.on_sdl_event(&event),
BrightnessControl::ConstantBrightness => {
constant_brightness.on_sdl_event(&event)
}
}
hsl_cycle.on_sdl_event(&event);
head_mover.on_sdl_event(&event);
}
_ => {}
}
}
reader.read(&mut buffer)?;
beat_detector.tick(frame_duration);
tap_metronome.tick(frame_duration);
hsl_cycle.tick(frame_duration);
head_mover.tick(frame_duration);
for i in 0..BUFFER_SIZE {
let mut float_bytes = [0u8; 4];
float_bytes.copy_from_slice(&buffer[4 * i..4 * i + 4]);
j += 1;
let sample = f32::from_le_bytes(float_bytes);
let mut value = bass_filter.process(sample);
if value < 0f32 {
value = -value;
}
let envelope = envelope_filter.process(value);
if j == 200 {
let beat = beat_filter.process(envelope);
point_buf.push(beat);
canvas.with_texture_canvas(&mut texture, |canvas| {
let min_y = 0f32;
let max_y = 3f32;
let get_y = |y: &f32| {
let mut y = y.clone();
if y <= 0f32 {
y = 0f32;
}
y = (1f32 + y).log2();
let mut y = (y - min_y) / (max_y - min_y);
((1f32 - y) * texture_size as f32) as u32
};
// background
let prev_brightness = brightness;
brightness = if beat > threshold {
1f32
} else {
0.75f32 * brightness
};
if brightness - prev_brightness > 0.5 {
println!("beat. {}", if beat_toggle {"-"} else {"|"});
beat_toggle ^= true;
}
let v = (255f32 * brightness) as u8;
canvas.set_draw_color(Color::RGB(v, v, v));
canvas.clear();
// zero
canvas.set_draw_color(Color::RGB(0, 128, 0));
let y = get_y(&0f32);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// min / max lines
canvas.set_draw_color(Color::RGB(255, 0, 0));
let min_beat = point_buf
.iter()
.skip(point_buf.capacity() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a < b { a } else { b })
.unwrap();
let x = texture_size - MIN_MAX_SAMPLE_COUNT as u32 * 10;
let y = get_y(min_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
let max_beat = point_buf
.iter()
.skip(point_buf.capacity() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a > b { a } else { b })
.unwrap();
let y = get_y(max_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// threshhold line
canvas.set_draw_color(Color::RGB(0, 0, 255));
let y = get_y(&threshold);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// values
canvas.set_draw_color(Color::RGB(0, 255, 0));
for (i, beat) in point_buf
.iter()
.skip(point_buf.capacity() - POINT_COUNT)
.enumerate()
{
let x = 10 * i;
let y = get_y(beat);
canvas
.draw_rect(Rect::new((x + 1) as i32, (y - 1) as i32, 8, 3))
.unwrap();
}
})?;
j = 0;
{
let mut output = movingheads.lock().unwrap();
match active_brightness_control {
BrightnessControl::BeatDetection => beat_detector.update_dmx(&mut *output),
BrightnessControl::TapMetronome => tap_metronome.update_dmx(&mut *output),
BrightnessControl::ConstantBrightness => {
constant_brightness.update_dmx(&mut *output)
}
}
hsl_cycle.update_dmx(&mut *output);
head_mover.update_dmx(&mut *output);
}
canvas.set_draw_color(Color::RGB(0, 0, 0));
canvas.clear();
canvas.with_texture_canvas(&mut texture, |canvas| {
match active_brightness_control {
BrightnessControl::BeatDetection => beat_detector.draw_sdl(canvas, texture_size),
BrightnessControl::TapMetronome => tap_metronome.draw_sdl(canvas, texture_size),
BrightnessControl::ConstantBrightness => {
constant_brightness.draw_sdl(canvas, texture_size)
}
}
hsl_cycle.draw_sdl(canvas, texture_size);
head_mover.draw_sdl(canvas, texture_size);
})?;
let (w, h) = canvas.window().drawable_size();
let target_rect = if w > h {
@@ -252,7 +169,15 @@ fn main() -> Result<()> {
canvas.copy(&texture, None, Some(target_rect)).unwrap();
canvas.present();
thread::sleep(frame_duration);
}
{
let mut dmx_running = dmx_running.lock().unwrap();
*dmx_running = false;
}
dmx_thread.join().unwrap()?;
Ok(())
}

View File

@@ -0,0 +1,8 @@
use num::traits::float::Float;
pub fn rescale<T: Float>(x: T, from: (T, T), to: (T, T)) -> T {
let (a, b) = from;
let (c, d) = to;
c + (d - c) * (x - a) / (b - a)
}

View File

@@ -1,4 +1,5 @@
.vscode
.mypy_cache
venv
build
build
__pycache__

View File

@@ -5,24 +5,25 @@ import colorsys
import sys
channels = [
192, # pan
0, # tilt
134, # dimmer
255, # R
0x88, # G
0, # B
0, # W
1, # movement speed
0, # RST
255, # dimmer
0, # R
0, # G
0, # B
0, # W
0, # A
0, # UV
0, # Strobe
0, # function
0, # function speed
]
start_addr = 10
start_addr = 1
with serial.Serial("/dev/ttyUSB0", 500000) as ser:
payload = bytearray(512)
FPS = 30
FPS = 50
if len(sys.argv) > 1:
FPS = int(sys.argv[1])
@@ -46,9 +47,9 @@ with serial.Serial("/dev/ttyUSB0", 500000) as ser:
r, g, b = colorsys.hls_to_rgb(t, 0.5, 1)
channels[3] = int(255 * r)
channels[4] = int(255 * g)
channels[5] = int(255 * b)
channels[1] = int(255 * r)
channels[2] = int(255 * g)
channels[3] = int(255 * b)
payload[(start_addr - 1) : (start_addr - 1 + len(channels))] = channels

View File

@@ -0,0 +1,69 @@
import importlib
import time
import os
import sys
import traceback
import serial
import scene
start_addr = 1
with serial.Serial("/dev/ttyUSB0", 500000) as ser:
payload = bytearray(512)
FPS = 50
if len(sys.argv) > 1:
FPS = int(sys.argv[1])
FRAME_TIME = 1 / FPS
t = 0
def sync():
# wait for sync
while True:
b = ser.readline()
if b.strip() == b"Sync.":
return
sync()
print("initial sync.")
last_edit_timestamp = os.stat(scene.__file__).st_mtime_ns
while True:
loop_start = time.time()
try:
timestamp = os.stat(scene.__file__).st_mtime_ns
if timestamp > last_edit_timestamp:
print("[importlib.reload]")
importlib.reload(scene)
last_edit_timestamp = timestamp
scene.display(t, payload)
except Exception:
traceback.print_exc()
ser.write(payload)
ser.flush()
response = ser.readline()
if response.strip() != b"Ack.":
print(f"received bad response: {response!r}")
sync()
continue
t += FRAME_TIME
t %= 1
loop_time = time.time() - loop_start
if loop_time < FRAME_TIME:
time.sleep(FRAME_TIME - loop_time)
else:
print("loop took too long!")
print(f"loop time: {1000 * loop_time:0.2f}ms busy, {1000 * (time.time() - loop_start):0.2f}ms total")
# print(ser.read_all())

29
microcontroller/scene.py Normal file
View File

@@ -0,0 +1,29 @@
import colorsys
channels = [
255, # dimmer
0, # R
0, # G
0, # B
0, # W
0, # A
0, # UV
0, # Strobe
0, # function
0, # function speed
]
start_addr = 1
def display(t, payload):
r, g, b = colorsys.hls_to_rgb(t, 0.5, 1)
#channels[1] = int(255 * r)
#channels[2] = int(255 * g)
#channels[3] = int(255 * b)
channels[4] = 0
channels[5] = 0
channels[6] = 255
channels[7] = 0
payload[(start_addr - 1) : (start_addr - 1 + len(channels))] = channels

153
pult/backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,153 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

1
pult/backend/frontend Symbolic link
View File

@@ -0,0 +1 @@
../frontend/build

239
pult/backend/main.py Normal file
View File

@@ -0,0 +1,239 @@
import asyncio
from typing import *
from queue import Queue
from threading import Thread, Lock
import time
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field, ValidationError
import serial
app = FastAPI()
class Slider:
def __init__(self):
self.value = 0
self.owner: Optional[WebSocket] = None
self.release_timer: Optional[asyncio.Task] = None
self.release_event = asyncio.Event()
def cancel_release_timer(self):
if self.release_timer is not None:
self.release_timer.cancel()
self.release_timer = None
def reset_release_timer(self):
self.cancel_release_timer()
self.release_timer = asyncio.create_task(self._release_timer())
async def _release_timer(self):
await asyncio.sleep(1)
self.release_event.set()
dmx_state = [Slider() for _ in range(8)]
class GrabAction(BaseModel):
action_type: Literal["grab"]
slider: int
class ReleaseAction(BaseModel):
action_type: Literal["release"]
slider: int
class MoveAction(BaseModel):
action_type: Literal["move"]
slider: int
new_value: int
class ClientAction(BaseModel):
action: Union[GrabAction, ReleaseAction, MoveAction] = Field(
..., discriminator="action_type"
)
class SocketManager:
def __init__(self):
self.sockets = set()
async def on_connect(self, ws: WebSocket):
self.sockets.add(ws)
await self.push_state(ws)
def on_disconnect(self, ws: WebSocket):
self.sockets.remove(ws)
for slider in dmx_state:
if slider.owner == ws:
slider.owner = None
async def on_action(
self, ws: WebSocket, action: Union[GrabAction, ReleaseAction, MoveAction]
):
slider = dmx_state[action.slider]
if action.action_type == "grab":
print(f"grab {action.slider}")
if slider.owner is None:
slider.owner = ws
slider.reset_release_timer()
elif action.action_type == "release":
print(f"release {action.slider}")
if slider.owner == ws:
slider.owner = None
slider.cancel_release_timer()
elif action.action_type == "move":
print(f"move {action.slider} -> {action.new_value}")
if slider.owner == ws:
slider.value = action.new_value
slider.reset_release_timer()
await self.push_all()
async def push_state(self, ws: WebSocket):
response = []
for slider in dmx_state:
value = slider.value
if slider.owner == ws:
status = "owned"
elif slider.owner is not None:
status = "locked"
else:
status = "open"
response.append({"value": value, "status": status})
await ws.send_json(response)
async def push_all(self):
await asyncio.gather(*[self.push_state(ws) for ws in self.sockets])
async def watch_auto_release(self):
async def _watch(slider):
while True:
await slider.release_event.wait()
print("resetteroni")
slider.release_event.clear()
slider.owner = slider.release_timer = None
await self.push_all()
await asyncio.gather(*[_watch(slider) for slider in dmx_state])
socket_manager = SocketManager()
@app.websocket("/ws")
async def ws_handler(ws: WebSocket):
await ws.accept()
await socket_manager.on_connect(ws)
try:
while True:
data = await ws.receive_json()
try:
action = ClientAction.parse_obj(data)
await socket_manager.on_action(ws, action.action)
except ValidationError as e:
print(e)
except WebSocketDisconnect as e:
pass
finally:
socket_manager.on_disconnect(ws)
app.mount("/", StaticFiles(directory="frontend", html=True))
dmx_data_lock = Lock()
dmx_data = [0 for _ in range(len(dmx_state))]
async def dmx_watcher():
while True:
with dmx_data_lock:
for (i, slider) in enumerate(dmx_state):
dmx_data[i] = slider.value
await asyncio.sleep(1/50)
class DmxWriter(Thread):
def __init__(self):
super().__init__()
self.running = True
def run(self):
FPS = 50
FRAME_TIME = 1 / FPS
with serial.Serial("/dev/ttyUSB0", 500_000) as ser:
payload = bytearray(512)
def sync():
# wait for sync
while True:
b = ser.readline()
if b.strip() == b"Sync.":
return
sync()
print("initial sync.")
while self.running:
loop_start = time.time()
with dmx_data_lock:
for (i, value) in enumerate(dmx_data):
payload[i] = value
ser.write(payload)
ser.flush()
response = ser.readline()
if response.strip() != b"Ack.":
print(f"received bad response: {response!r}")
sync()
continue
loop_time = time.time() - loop_start
if loop_time < FRAME_TIME:
time.sleep(FRAME_TIME - loop_time)
else:
print("loop took too long!")
print(f"loop time: {1000 * loop_time:0.2f}ms busy, {1000 * (time.time() - loop_start):0.2f}ms total")
def stop(self):
self.running = False
dmx_writer = DmxWriter()
@app.on_event("startup")
async def on_startup():
asyncio.create_task(socket_manager.watch_auto_release())
asyncio.create_task(dmx_watcher())
dmx_writer.start()
@app.on_event("shutdown")
async def on_shutdown():
print("shutdown")
dmx_writer.stop()
dmx_writer.join()

View File

@@ -0,0 +1,11 @@
anyio==3.5.0
asgiref==3.5.0
click==8.0.3
fastapi==0.73.0
h11==0.13.0
idna==3.3
pydantic==1.9.0
sniffio==1.2.0
starlette==0.17.1
typing-extensions==4.1.1
uvicorn==0.17.5

23
pult/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

46
pult/frontend/README.md Normal file
View File

@@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

28331
pult/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@fontsource/roboto": "^4.5.3",
"@mui/icons-material": "^5.4.1",
"@mui/material": "^5.4.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.0",
"@types/node": "^16.11.24",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"typescript": "^4.5.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

107
pult/frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,107 @@
import React, { useState, useMemo, createContext, useContext } from "react";
import { createTheme, ThemeProvider } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import blueGrey from "@mui/material/colors/blueGrey"
import teal from "@mui/material/colors/teal";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Brightness4Icon from "@mui/icons-material/Brightness4";
import Container from "@mui/material/Container";
import CssBaseline from "@mui/material/CssBaseline";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Sliders from "./Sliders";
// Light / Dark mode
type ThemeMode = 'system' | 'light' | 'dark';
const ThemeModeContext = createContext((_: ThemeMode) => { })
const App: React.FC = () => {
const systemDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const [themeMode, setThemeMode] = useState<ThemeMode>('system');
const theme = useMemo(
() => createTheme({
palette: {
mode: themeMode === 'system' ? (systemDarkMode ? 'dark' : 'light') : themeMode,
primary: blueGrey,
secondary: teal,
}
}),
[themeMode, systemDarkMode],
);
return <ThemeModeContext.Provider value={setThemeMode}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Layout />
</ThemeProvider>
</ThemeModeContext.Provider>
}
// Layout
const Layout: React.FC = () => {
return <>
<TopBar />
<main>
<Box sx={{
pt: 8,
pb: 6,
}}>
<Container>
<Sliders />
</Container>
</Box>
</main>
</>
}
// Top Bar
const TopBar: React.FC = () => {
const setThemeMode = useContext(ThemeModeContext);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
return <AppBar position="relative">
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
DMX Controllinator
</Typography>
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) }}
color="inherit"
>
<Brightness4Icon />
</IconButton>
<Menu
open={open}
anchorEl={anchorEl}
onClose={() => { setAnchorEl(null) }}
>
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('light'); }}>Light</MenuItem>
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('system'); }}>System</MenuItem>
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('dark'); }}>Dark</MenuItem>
</Menu>
<Button variant="contained" href="docs">
API
</Button>
</Toolbar>
</AppBar >
}
// Content
export default App;

View File

@@ -0,0 +1,120 @@
import MuiSlider from "@mui/material/Slider";
import React from "react";
import { useState, useEffect, useRef } from "react";
type StateItem = {
value: number,
status: "open" | "owned" | "locked",
};
type State = Array<StateItem>;
type ClientAction = {
action_type: "grab" | "release",
slider: number,
} | {
action_type: "move",
slider: number,
new_value: number,
}
const Sliders: React.FC = () => {
const ws = useRef<WebSocket>();
const reconnectInterval = useRef<number>();
const [state, setState] = useState<State>();
const connect = () => {
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
if (ws.current !== undefined && ws.current.readyState !== 3) return;
const wsURL = new URL("ws", window.location.href);
wsURL.protocol = wsURL.protocol.replace("http", "ws");
ws.current = new WebSocket(wsURL.href);
ws.current.onmessage = (ev) => {
setState(JSON.parse(ev.data));
}
}
useEffect(() => {
connect();
reconnectInterval.current = window.setInterval(connect, 1000);
return () => {
if (reconnectInterval.current !== undefined) window.clearInterval(reconnectInterval.current);
if (ws.current !== undefined) ws.current.close();
};
}, []);
const cb = (action: ClientAction) => {
if (ws.current !== undefined && ws.current.readyState !== 3) {
ws.current.send(JSON.stringify({
action: action,
}));
}
}
return <>
{state?.map((item, index) => <Slider key={index} item={item} index={index} cb={cb} />)}
</>
}
const styleOverride = {
"& .MuiSlider-track": { transition: "none" },
"& .MuiSlider-thumb": { transition: "none" },
};
const Slider: React.FC<{
item: StateItem,
index: number,
cb: (action: ClientAction) => void,
}> = ({ item, index, cb }) => {
const disabled = item.status === "locked";
const [value, setValue] = useState(item.value);
useEffect(() => {
if (item.status !== "owned") setValue(item.value);
}, [item]);
const onChange = (n: number) => {
setValue(n);
cb({
action_type: "move",
slider: index,
new_value: n,
});
};
const onGrab = () => {
cb({
action_type: "grab",
slider: index,
});
};
const onRelease = () => {
cb({
action_type: "release",
slider: index,
});
};
return <MuiSlider
min={0}
max={255}
sx={disabled ? styleOverride : {}}
disabled={disabled}
value={value}
onChange={(_, n) => { onChange(n as number) }}
onMouseDown={onGrab}
onTouchStart={onGrab}
onMouseUp={onRelease}
onTouchEnd={onRelease}
/>
};
export default Sliders;

View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

1
pult/frontend/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

3
sphere_movement/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
venv
.vscode
.ipynb_checkpoints

100
sphere_movement/slerp.ipynb Normal file

File diff suppressed because one or more lines are too long

24
webserial/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
webserial/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
webserial/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

12
webserial/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Light Maymays</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2448
webserial/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
webserial/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "webserial",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.0",
"@tsconfig/svelte": "^3.0.0",
"@types/w3c-web-serial": "^1.0.3",
"bulma": "^0.9.4",
"monaco-editor": "^0.34.1",
"sass": "^1.57.1",
"svelte": "^3.54.0",
"svelte-check": "^2.10.0",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
}
}

28
webserial/src/App.svelte Normal file
View File

@@ -0,0 +1,28 @@
<script lang="ts">
import Monaco from "./editor/Monaco.svelte";
import EvalLoop from "./eval/EvalLoop.svelte";
import SerialManager from "./serial/SerialManager.svelte";
</script>
<main>
<Monaco />
<SerialManager />
<EvalLoop />
</main>
<style lang="scss">
:global(body) {
margin: 0;
height: 100vh;
}
:global(#app) {
height: 100%;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
</style>

9
webserial/src/app.scss Normal file
View File

@@ -0,0 +1,9 @@
/* Import only what you need from Bulma */
@import "bulma/sass/utilities/_all";
@import "bulma/sass/base/_all";
@import "bulma/sass/elements/_all";
@import "bulma/sass/form/_all";
@import "bulma/sass/components/_all";
@import "bulma/sass/grid/_all";
@import "bulma/sass/helpers/_all";
@import "bulma/sass/layout/_all";

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { dmxData } from "./store";
$: dbg = new Array(10).fill(0).map((_, i) => `${i}: ${$dmxData[i].toString().padStart(3, " ")}`).join(" ");
</script>
<pre>dbg: "{dbg}"</pre>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { dmxData } from "../store";
export let address: number;
let dimmer = 255,
r = 0,
g = 0,
b = 0,
w = 0,
a = 0,
uv = 0,
strobe = 0;
$: {
$dmxData[address - 1 + 0] = dimmer;
$dmxData[address - 1 + 1] = r;
$dmxData[address - 1 + 2] = g;
$dmxData[address - 1 + 3] = b;
$dmxData[address - 1 + 4] = w;
$dmxData[address - 1 + 5] = a;
$dmxData[address - 1 + 6] = uv;
$dmxData[address - 1 + 7] = strobe;
$dmxData = $dmxData;
}
</script>
<div>
<h3>Par @ {address}</h3>
<input bind:value={dimmer} type="range" min="0" max="255" />
<input bind:value={r} type="range" min="0" max="255" />
<input bind:value={g} type="range" min="0" max="255" />
<input bind:value={b} type="range" min="0" max="255" />
<input bind:value={w} type="range" min="0" max="255" />
<input bind:value={a} type="range" min="0" max="255" />
<input bind:value={uv} type="range" min="0" max="255" />
</div>

View File

@@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export const dmxData = writable(new Uint8Array(512));

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import * as monaco from "monaco-editor";
import { onMount } from "svelte";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import defaultCode from "./defaultCode.ts?raw";
import defaultEnv from "./defaultEnv.d.ts?raw";
import { code } from "./code";
let divEl: HTMLDivElement = null;
let editor: monaco.editor.IStandaloneCodeEditor;
onMount(async () => {
// @ts-ignore
self.MonacoEnvironment = {
getWorker: function (_moduleId: any, label: string) {
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
editor = monaco.editor.create(divEl, {
value: $code,
language: "typescript",
automaticLayout: true,
});
let lib =
monaco.languages.typescript.typescriptDefaults.addExtraLib(
defaultEnv
);
editor.onKeyUp((_e) => ($code = editor.getValue()));
return () => {
lib.dispose();
editor.dispose();
};
});
function resetCode() {
$code = defaultCode;
editor.setValue($code);
}
</script>
<nav class="navbar">
<div class="navbar-brand">
<div class="navbar-item"><strong>DMX Memes</strong></div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<button class="button is-danger" on:click={resetCode}>Reset to default</button>
<button class="button is-danger" on:click={() => localStorage.clear()}>Clear local storage</button>
</div>
</div>
</div>
</nav>
<div class="parent">
<div class="editor" bind:this={divEl} />
</div>
<style lang="scss">
.parent {
flex-grow: 1;
}
.editor {
height: 100%;
resize: both;
}
</style>

View File

@@ -0,0 +1,9 @@
import { writable } from "svelte/store";
import defaultCode from "./defaultCode.ts?raw";
const stored = localStorage.getItem("dmxCode") || defaultCode;
/** the TypeScript code as seen in the editor */
export const code = writable(stored);
code.subscribe((value) => localStorage.setItem("dmxCode", value))

View File

@@ -0,0 +1,166 @@
// Connect the interface with the button below and modify the code here.
// The output should update in real time as you type :)
// Fixtures library. TODO move this into a separate file
class Fixture {
startAddress: number
constructor(startAddress: number) {
this.startAddress = startAddress;
}
/**
* Write to a DMX channel
* @param address Address offset (1 = start address)
* @param value between 0 and 255
*/
setChannel(address: number, value: number) {
ctx.set(this.startAddress + address - 1, value);
}
}
interface GenericRGBW {
/**
* Set the brightness
*
* @param value between 0 and 1
* @param strobe enable strobe (turn off if unsupported)
*/
setBrightness(value: number, strobe?: boolean): void
setRGBW(rgb: [number, number, number], w?: number): void
}
class Par extends Fixture implements GenericRGBW {
setBrightness(value: number, strobe?: boolean) {
if (strobe || false) {
this.setChannel(1, 255);
this.setChannel(8, value * 255);
} else {
this.setChannel(1, value * 255);
this.setChannel(8, 0);
}
}
setRGBW(rgb: [number, number, number], w?: number) {
let [r, g, b] = rgb;
this.setChannel(2, r);
this.setChannel(3, g);
this.setChannel(4, b);
this.setChannel(5, w || 0);
}
setAUV(a: number, uv: number) {
this.setChannel(6, a);
this.setChannel(7, uv);
}
}
class MovingHead extends Fixture implements GenericRGBW {
setBrightness(value: number, strobe?: boolean) {
let val = (strobe || false)
? 135 + (239 - 135) * value
: 8 + (134 - 8) * value;
this.setChannel(6, val);
}
setRGBW(rgb: [number, number, number], w?: number) {
let [r, g, b] = rgb;
this.setChannel(7, r);
this.setChannel(8, g);
this.setChannel(9, b);
this.setChannel(10, w || 0);
}
/**
* Rotate the moving head. Pan and tilt are in radians.
* @param pan between -1.5pi and 1.5pi
* @param tilt between -0.5pi and 0.5pi
* @param speed between 0 (fast) and 255 (slow)
*/
setPanTilt(pan: number, tilt: number, speed?: number) {
let panRough = (pan + 3 * Math.PI / 2) / (Math.PI * 3) * 256
panRough = Math.max(0, Math.min(255, panRough))
let tiltRough = (tilt + Math.PI / 2) / (Math.PI) * 256
tiltRough = Math.max(0, Math.min(255, tiltRough))
// TODO
let panFine = 0
let tiltFine = 0
this.setChannel(1, panRough);
this.setChannel(2, panFine);
this.setChannel(3, tiltRough);
this.setChannel(4, tiltFine);
this.setChannel(5, speed || 0);
}
}
// class Flower extends Fixture implements GenericRGBW {
//
// setBrightness(value: number, strobe?: boolean): void {
// // dimmer seems unsupported :(
// this.setChannel(7, (strobe || false) ? 255 * value : 0);
// }
//
// setRGBW(rgb: [number, number, number], w?: number): void {
// const [r, g, b] = rgb;
// this.setChannel(1, r);
// this.setChannel(2, g);
// this.setChannel(3, b);
// this.setChannel(4, w || 0);
// }
//
// setAP(a: number, p: number) {
// this.setChannel(5, a);
// this.setChannel(6, p);
// }
//
// /**
// * Set the rotation speed
// * @param speed Between -1 (clockwise) and 1 (counterclockwise)
// */
// setRotation(direction: number) {
// const val = (direction < 0)
// ? /* clockwise */ lib.remap(direction, [0, -1], [0, 128], true)
// : /* counterclockwise */ lib.remap(direction, [0, 1], [129, 255], true);
//
// this.setChannel(8, val);
// }
//
// setMacro(pattern: number, speed: number) {
// this.setChannel(9, pattern);
// this.setChannel(10, speed);
// }
//
// }
// ******************
// * CODE GOES HERE *
// ******************
let color = lib.hsl2rgb(360 * (t / 7), 100, 50);
const pars = [
new Par(1),
new Par(41),
];
const heads = [
new MovingHead(120),
new MovingHead(140),
new MovingHead(160),
new MovingHead(100),
];
for (let par of pars) {
par.setBrightness(1);
par.setRGBW(color);
}
for (let head of heads) {
head.setBrightness(1);
head.setRGBW(color);
}

41
webserial/src/editor/defaultEnv.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
type Context = {
/**
* Set DMX address `address` to `value`
* @param address between 1 and 511
* @param value between 0 and 255
*/
set(address: number, value: number): void,
};
/** The global Context object */
declare const ctx: Context;
/** The current time (in seconds, e.g. 1.25 after 1250 milliseconds) */
declare const t: number;
type Lib = {
/**
* Converts from the HSL color space to RGB
*
* Outputs [r, g, b] between 0 and 255 each
*
* @param h in degrees, i.e. 0 to 360
* @param s between 0 and 100
* @param l between 0 and 100
*/
hsl2rgb(h: number, s: number, l: number): [number, number, number],
clamp(value: number, boundaries: [number, number]),
/**
* Map a value from one range to the other
* @param value to be remapped. Does not need to be in the source range.
* @param from [lower, upper] boundaries of source range.
* @param to [lower, upper] boundaries of target range.
* @param clamp clamp the output to the target range boundaries
*/
remap(value: number, from: [number, number], to: [number, number], clamp?: boolean),
}
/** The standard library */
declare const lib: Lib;

View File

@@ -0,0 +1,49 @@
<script lang="ts">
/// <reference path="../editor/defaultEnv.d.ts" />
import { onMount } from "svelte";
import { code } from "../editor/code";
import { dmxData } from "../dmx/store";
import { lib } from "./lib";
import TS from "typescript";
import tsOptions from "./tsOptions";
$: transpiled = TS.transpile($code, tsOptions);
let startTime: number;
let payload = new Uint8Array(512);
let ctx: Context = {
set(address: number, value: number) {
let rounded = Math.round(value);
payload[address - 1] = Math.max(0, Math.min(255, rounded));
},
};
let result: string;
function dmxFrame() {
payload = new Uint8Array(512);
const t = (performance.now() - startTime) / 1000;
try {
let ret = Function("ctx", "t", "lib", transpiled).call({}, ctx, t, lib);
result = JSON.stringify(ret);
} catch (err) {
result = JSON.stringify(err);
}
$dmxData = payload;
}
const FPS = 50;
const FRAME_TIME = 1000 / FPS; // milliseconds
onMount(() => {
startTime = performance.now();
let int = setInterval(dmxFrame, FRAME_TIME);
return () => {
clearInterval(int);
};
});
</script>
<!-- <pre>{result}</pre> -->

42
webserial/src/eval/lib.ts Normal file
View File

@@ -0,0 +1,42 @@
/// <reference path="../editor/defaultEnv.d.ts" />
function hsl2rgb(h, s, l): [number, number, number] {
// https://stackoverflow.com/a/44134328
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
};
return [f(0), f(8), f(4)];
}
function clamp(value: number, boundaries: [number, number]) {
let [l, r] = boundaries;
return Math.max(l, Math.min(value, r));
}
const _clamp = clamp;
function remap(value: number, from: [number, number], to: [number, number], clamp?: boolean) {
const [a, b] = from;
const normalized = (value - a) / (b - a);
const [c, d] = to;
let remapped = c + (d - c) * normalized;
if (clamp) {
remapped = _clamp(remapped, to)
};
return remapped;
}
export const lib: Lib = {
hsl2rgb: hsl2rgb,
clamp: clamp,
remap: remap,
}

View File

@@ -0,0 +1,7 @@
import type { CompilerOptions } from "typescript"
const tsOptions: CompilerOptions = {
strict: true
}
export default tsOptions

8
webserial/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import App from './App.svelte'
import "./app.scss";
const app = new App({
target: document.getElementById('app'),
})
export default app

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import { onMount } from "svelte";
import { dmxData } from "../dmx/store";
export let port: SerialPort;
export let fps: number = 50;
const FRAME_TIME = 1000 / fps; // milliseconds
let lines = [];
let linesResolve: (value: string | PromiseLike<string>) => void = null;
function readLine(): Promise<string> {
if (lines.length > 0) {
return Promise.resolve(lines.shift());
} else {
// lines is empty, register callbacks
return new Promise((resolve) => {
linesResolve = resolve;
});
}
}
const handleLine = (line: string) => {
if (linesResolve !== null) {
linesResolve(line);
linesResolve = null;
} else {
lines.push(line);
}
};
let running = true;
async function readLoop() {
console.log("runner started");
await port.open({ baudRate: 500_000 });
let reader = port.readable.getReader();
let data = "";
try {
while (running) {
const { value, done } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
data += chunk;
const lines = data.split("\r\n");
data = lines.pop();
for (let line of lines) {
handleLine(line);
}
}
} finally {
reader.releaseLock();
}
await port.close();
console.log("runner stopped");
}
let readLoopHandle: Promise<void> = null;
async function sync() {
while (running) {
const line = await readLine();
if (line === "Sync.") {
return;
}
}
}
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
let lastLoopTime: number = null;
async function writeLoop() {
await sync();
while (running) {
const loopStart = performance.now();
// write out payload
let writer = port.writable.getWriter();
try {
await writer.write($dmxData);
} finally {
writer.releaseLock();
}
// read response
const response = await readLine();
if (response !== "Ack.") {
console.log(`received bad response: "${response}"`);
lastLoopTime = null;
await sync();
continue;
}
const loopTime = performance.now() - loopStart;
if (loopTime < FRAME_TIME) {
await sleep(FRAME_TIME - loopTime);
} else {
console.warn(
`loop took too long (+${(loopTime - FRAME_TIME).toFixed(
2
)} ms)`
);
}
lastLoopTime = loopTime;
}
}
let writeLoopHandle: Promise<void> = null;
onMount(() => {
console.log("mount");
readLoopHandle = readLoop();
writeLoopHandle = writeLoop();
return () => {
console.log("unmount");
running = false;
if (linesResolve !== null) {
linesResolve("");
}
};
});
</script>
<span>
{#if lastLoopTime !== null}
{lastLoopTime.toFixed(2).padStart(5)}
{:else}
syncing
{/if}
</span>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import SerialConnection from "./SerialConnection.svelte";
let port: SerialPort = null;
async function connect() {
port = await navigator.serial.requestPort();
port.addEventListener("disconnect", disconnect);
}
async function disconnect() {
port = null;
}
</script>
<section class="section bg-footer">
<div class="container">
{#if navigator.serial}
<div class="box parent">
<span class="left">Serial available &#x1f680;</span>
<span class="middle">
{#if port !== null}
<button class="button is-warning" on:click={disconnect}
>disconnect</button
>
{:else}
<button class="button is-primary" on:click={connect}
>connect</button
>
{/if}
</span>
<span class="right">
{#if port !== null}
<SerialConnection {port} />
{:else}
Not connected
{/if}
</span>
</div>
{:else}
<div class="notification is-danger text-center">
<p>
Looks like your browser does not support WebSerial &#x1f622;
</p>
<p>
Check <a
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
>here</a
> for a list of compatible browsers
</p>
</div>
{/if}
</div>
</section>
<style lang="scss">
.parent {
display: flex;
flex-direction: row;
}
.left,
.middle,
.right {
flex: 1;
}
.middle {
text-align: center;
}
.right {
text-align: right;
}
.text-center {
text-align: center;
}
.bg-footer {
background-color: hsl(0, 0%, 98%);
}
</style>

2
webserial/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

20
webserial/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}

8
webserial/vite.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
base: '',
})

24
webui/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
webui/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
webui/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

12
webui/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2274
webui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
webui/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.4",
"@tsconfig/svelte": "^4.0.1",
"svelte": "^3.58.0",
"svelte-check": "^3.3.1",
"tslib": "^2.5.0",
"typescript": "^5.1.3",
"vite": "^4.3.9"
},
"dependencies": {
"monaco-editor": "^0.39.0"
}
}

45
webui/src/App.svelte Normal file
View File

@@ -0,0 +1,45 @@
<script lang="ts">
import Editor from "./Editor.svelte";
import { parseFunctionInfo } from "./analysis";
const moduleCode = "export function run(x: number): number {\n return x + 69;\n}\n";
// const blob = new Blob([moduleCode], { type: "text/javascript" });
// const url = URL.createObjectURL(blob);
// const module = import(/* @vite-ignore */ url);
let text = moduleCode;
$: info = parseFunctionInfo(text, "run");
</script>
<main>
<div class="yeet">
<Editor bind:content={text} />
</div>
{#if info !== null}
<h3>Args:</h3>
<ul>
{#each info.arguments as arg}
<li>{arg.name}: {arg.type}</li>
{/each}
</ul>
<h3>Return:</h3>
{info.returnType}
{/if}
<!--
{#await module then m}
<pre>{JSON.stringify(m)}</pre>
<pre>{JSON.stringify(m.run())}</pre>
{/await}
-->
</main>
<style>
.yeet {
width: 40vw;
height: 400px;
display: flex;
}
</style>

69
webui/src/Editor.svelte Normal file
View File

@@ -0,0 +1,69 @@
<script lang="ts" context="module">
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// @ts-ignore
self.MonacoEnvironment = {
getWorker: function (_moduleId: any, label: string) {
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
</script>
<script lang="ts">
// Adapted from https://gist.github.com/KTibow/77da4597dcb22cf80be525df284e6d72
import { onMount } from "svelte";
export let content: string;
let editor;
let divEl: HTMLElement;
onMount(async () => {
const monaco = await import("monaco-editor");
editor = monaco.editor.create(divEl, {
value: content,
language: "typescript",
scrollBeyondLastLine: false,
theme: "vs-dark",
});
editor.onDidChangeModelContent(() => {
content = editor.getValue();
});
return () => {
editor.dispose();
};
});
</script>
<div class="container">
<div bind:this={divEl} class="editor" />
</div>
<svelte:window
on:resize={() => {
editor.layout({ width: 0, height: 0 });
window.requestAnimationFrame(() => {
const rect = divEl.parentElement.getBoundingClientRect();
editor.layout({ width: rect.width, height: rect.height });
});
}}
/>
<style>
.container {
flex-grow: 1;
}
.editor {
width: 100%;
height: 100%;
text-align: left;
}
</style>

24
webui/src/analysis.ts Normal file
View File

@@ -0,0 +1,24 @@
import ts from 'typescript';
export function parseFunctionInfo(sourceCode: string, functionName: string): { arguments: { name: string, type: string }[], returnType: string } | null {
const sourceFile = ts.createSourceFile('temp.ts', sourceCode, ts.ScriptTarget.ES2015, true);
let functionInfo: { arguments: { name: string, type: string }[], returnType: string } | null = null;
for (const node of sourceFile.statements) {
if (ts.isFunctionDeclaration(node) && node.name && node.name.text === functionName) {
const argumentTypes: { name: string, type: string }[] = node.parameters.map(parameter => {
const name = parameter.name.getText();
const type = parameter.type ? sourceCode.substring(parameter.type.pos, parameter.type.end) : 'any';
return { name, type };
});
const returnType = node.type ? sourceCode.substring(node.type.pos, node.type.end) : 'any';
functionInfo = { arguments: argumentTypes, returnType };
break;
}
}
return functionInfo;
}

80
webui/src/app.css Normal file
View File

@@ -0,0 +1,80 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

8
webui/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import './app.css'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app'),
})
export default app

2
webui/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

7
webui/svelte.config.js Normal file
View File

@@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

20
webui/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
webui/tsconfig.node.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

7
webui/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
})