Compare commits

...

47 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
971e2a61f6 Fix file permissions 2021-10-26 09:47:52 +02:00
7ee7779461 Add tilt experiment 2021-10-25 23:58:20 +02:00
120d8d8c49 Add beat detection script 2021-10-25 23:58:05 +02:00
49f306226b Add untracked stuff to frontend 2021-10-25 18:52:10 +02:00
8b5d55025f Add pulse default sink name retrieval 2021-09-26 10:08:16 +02:00
36eb0bb51c Refactor guitar hero into modules 2021-09-25 22:40:28 +02:00
8573d30d02 Add beat detection Rust POC 2021-09-15 21:03:13 +02:00
95ae89cab7 Add aruco experiment code 2021-09-13 09:35:22 +02:00
d7ed085686 Autoformat guitar hero controller script 2021-09-10 14:35:36 +02:00
70671f6d54 Remove duplicate images 2021-09-10 13:12:05 +02:00
f987a876e8 Add Fritzing source files 2021-09-10 13:08:10 +02:00
aee36d4ccf Add MCU Documentation readme 2021-09-10 13:02:14 +02:00
fac05243aa Add guitar hero controller script 2021-09-07 08:55:21 +02:00
a2d7bb3211 Implement multiple fixtures 2021-09-03 00:50:56 +02:00
07ba32f0ee Move fixture definition to module 2021-09-03 00:39:04 +02:00
2dedc08636 Start backend 2021-09-01 16:37:28 +02:00
44c2604855 Add E1.31 example (too slow though) 2021-08-30 10:18:57 +02:00
f9e969e6e8 Cleanup 2021-08-30 10:18:31 +02:00
0260c3bb6b Cleanup 2021-08-30 10:11:46 +02:00
4f6666a874 Remove old code 2021-08-30 10:11:06 +02:00
d76bfcafe8 Cleanup 2021-08-30 10:10:33 +02:00
503690e9da Implement triple buffering 2021-08-30 09:33:28 +02:00
738301d113 Add MCU gitignore 2021-08-29 13:41:03 +02:00
8beaafc456 Move testing code to subfolder 2021-08-29 13:40:35 +02:00
34fe7f55d0 Increase baudrate 2021-08-29 12:54:33 +02:00
114 changed files with 41101 additions and 142 deletions

5
aruco/.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
rotate_180.mp4 filter=lfs diff=lfs merge=lfs -text
rotate_360.mp4 filter=lfs diff=lfs merge=lfs -text
rotate_540.mp4 filter=lfs diff=lfs merge=lfs -text
rotate_90.mp4 filter=lfs diff=lfs merge=lfs -text
tilt_180.mp4 filter=lfs diff=lfs merge=lfs -text

2
aruco/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
venv
.ipynb_checkpoints

125
aruco/angle_measure.py Normal file
View File

@@ -0,0 +1,125 @@
import math
import time
import serial
def clamp(x, ab):
(a, b) = ab
return max(a, min(b, x))
def rescale(x, from_limits, to_limits):
(a, b) = from_limits
x_0_1 = (x - a) / (b - a)
(c, d) = to_limits
return c + (d - c) * x_0_1
class MovingHead:
def __init__(self, start_addr):
self.start_addr = start_addr
self.pan = 0 # -3pi/2 to 3pi/2
self.tilt = 0 # -pi/2 to pi/2
self.speed = 0
self.dimmer = 0 # 0 to 1
self.rgbw = (0, 0, 0, 0)
def __str__(self):
return (
f"MovingHead({self.start_addr}): pan={self.pan!r}, "
f"tilt={self.tilt!r}, speed={self.speed!r}, "
f"dimmer={self.dimmer!r}, rgbw={self.rgbw!r}"
)
def render(self, dst):
pan = rescale(self.pan, (-1.5 * math.pi, 1.5 * math.pi), (255, 0))
pan = clamp(int(pan), (0, 255))
pan_fine = 0
tilt = rescale(self.tilt, (-0.5 * math.pi, 0.5 * math.pi), (0, 255))
tilt = clamp(int(tilt), (0, 255))
tilt_fine = 0
dimmer = clamp(7 + int(127 * self.dimmer), (7, 134))
(r, g, b, w) = self.rgbw
channels = [
pan,
pan_fine,
tilt,
tilt_fine,
self.speed,
dimmer,
r,
g,
b,
w,
0, # color mode
0, # auto jump speed
0, # control mode
0, # reset
]
offset = self.start_addr - 1
dst[offset : offset + len(channels)] = channels
if __name__ == "__main__":
lighting = MovingHead(43)
lighting.tilt = -0.5 * math.pi
lighting.rgbw = (0, 0, 0, 0xFF)
lighting.dimmer = 1
head = MovingHead(1)
head.rgbw = (0x00, 0x00, 0xFF, 0)
head.tilt = -0.5 * math.pi
head.dimmer = 1
dmx_data = bytearray(512)
with serial.Serial("/dev/ttyUSB0", 500_000) as ser:
def sync():
# wait for sync
while True:
b = ser.readline()
if b.strip() == b"Sync.":
return
print("syncing")
sync()
t0 = time.time()
left = -0.5 * math.pi
right = 0.5 * math.pi
while True:
now = time.time() - t0
if int(now) % 10 < 5:
head.tilt = left
head.rgbw = (0xFF, 0x00, 0x00, 0)
else:
head.tilt = right
head.rgbw = (0x00, 0xFF, 0x00, 0)
head.render(dmx_data)
lighting.render(dmx_data)
ser.write(dmx_data)
ser.flush()
response = ser.readline()
if response.strip() != b"Ack.":
print(f"received bad response: {response!r}")
sync()

1390
aruco/aruco.ipynb Normal file

File diff suppressed because it is too large Load Diff

BIN
aruco/formula.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

65
aruco/requirements.txt Normal file
View File

@@ -0,0 +1,65 @@
argon2-cffi==21.1.0
attrs==21.2.0
backcall==0.2.0
bleach==4.1.0
cffi==1.14.6
cycler==0.10.0
debugpy==1.4.3
decorator==5.1.0
defusedxml==0.7.1
entrypoints==0.3
ipykernel==6.4.1
ipython==7.27.0
ipython-genutils==0.2.0
ipywidgets==7.6.4
jedi==0.18.0
Jinja2==3.0.1
jsonschema==3.2.0
jupyter==1.0.0
jupyter-client==7.0.2
jupyter-console==6.4.0
jupyter-core==4.7.1
jupyterlab-pygments==0.1.2
jupyterlab-widgets==1.0.1
kiwisolver==1.3.2
MarkupSafe==2.0.1
matplotlib==3.4.3
matplotlib-inline==0.1.3
mistune==0.8.4
nbclient==0.5.4
nbconvert==6.1.0
nbformat==5.1.3
nest-asyncio==1.5.1
notebook==6.4.3
numpy==1.21.2
opencv-contrib-python==4.5.3.56
opencv-python==4.5.3.56
packaging==21.0
pandocfilters==1.4.3
parso==0.8.2
pexpect==4.8.0
pickleshare==0.7.5
Pillow==8.3.2
prometheus-client==0.11.0
prompt-toolkit==3.0.20
ptyprocess==0.7.0
pycparser==2.20
Pygments==2.10.0
pyparsing==2.4.7
pyrsistent==0.18.0
pyserial==3.5
pytesseract==0.3.8
python-dateutil==2.8.2
pyzmq==22.2.1
qtconsole==5.1.1
QtPy==1.11.0
scipy==1.7.1
Send2Trash==1.8.0
six==1.16.0
terminado==0.12.1
testpath==0.5.0
tornado==6.1
traitlets==5.1.0
wcwidth==0.2.5
webencodings==0.5.1
widgetsnbextension==3.5.1

BIN
aruco/rotate_180.mp4 LFS Normal file

Binary file not shown.

BIN
aruco/rotate_360.mp4 LFS Normal file

Binary file not shown.

BIN
aruco/rotate_540.mp4 LFS Normal file

Binary file not shown.

BIN
aruco/rotate_90.mp4 LFS Normal file

Binary file not shown.

BIN
aruco/tilt_180.mp4 LFS Normal file

Binary file not shown.

1
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

1203
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.43"
palette = "0.6.0"
rodio = "0.14.0"
serialport = "4.0.1"

108
backend/src/fixtures.rs Normal file
View File

@@ -0,0 +1,108 @@
use palette::{Pixel, Srgb};
// 9 Channel mode
pub struct MovingHead9CH {
start_addr: usize,
pub pan: u8,
pub tilt: u8,
pub dimmer: u8, // 0-7 off, 8-134 dim, 135-239 strobe, 240-255 "switch"?
pub rgb: Srgb,
pub w: u8,
pub speed: u8, // reversed
pub rst: u8, // 150-200
}
impl MovingHead9CH {
pub fn new(start_addr: usize) -> Self {
Self {
start_addr,
pan: 0,
tilt: 0,
dimmer: 0,
rgb: Srgb::new(0.0, 0.0, 0.0),
w: 0,
speed: 0,
rst: 0,
}
}
pub fn render(&self, destination: &mut [u8]) {
let [r, g, b]: [u8; 3] = self.rgb.into_format().into_raw();
destination[self.start_addr - 1..self.start_addr - 1 + 9].copy_from_slice(&[
self.pan,
self.tilt,
self.dimmer,
r,
g,
b,
self.w,
self.speed,
self.rst,
])
}
}
// 14 Channel mode
pub struct MovingHead14CH {
start_addr: usize,
pub pan: u8,
pub pan_fine: u8,
pub tilt: u8,
pub tilt_fine: u8,
pub speed: u8,
pub dimmer: u8,
pub rgb: Srgb,
pub w: u8,
pub color_mode: u8,
pub color_auto_jump_speed: u8,
pub control_mode: u8,
pub rst: u8,
}
impl MovingHead14CH {
pub fn new(start_addr: usize) -> Self {
Self {
start_addr,
pan: 0,
pan_fine: 0,
tilt: 0,
tilt_fine: 0,
speed: 0,
dimmer: 0, // 0-7 off, 8-134 dim, 135-239 strobe, 240-255 "switch"?
rgb: Srgb::new(0.0, 0.0, 0.0),
w: 0,
color_mode: 0, // 0-7 "select color" -> dmx?, 8-231 "built in"?, 232-255 "seven colors jumping"
color_auto_jump_speed: 0, // speed for "seven colors jumping" mode
control_mode: 0, /* 0-7 "custom control" -> dmx?, 8-63 "working in fast", 64-127 "working in slow"
128-191 "Sound1Active", 192-255 "Sound2Active" */
rst: 0, // 150-200
}
}
pub fn render(&self, destination: &mut [u8]) {
let [r, g, b]: [u8; 3] = self.rgb.into_format().into_raw();
destination[self.start_addr - 1..self.start_addr - 1 + 14].copy_from_slice(&[
self.pan,
self.pan_fine,
self.tilt,
self.tilt_fine,
self.speed,
self.dimmer,
r,
g,
b,
self.w,
self.color_mode,
self.color_auto_jump_speed,
self.control_mode,
self.rst,
]);
}
}

141
backend/src/main.rs Normal file
View File

@@ -0,0 +1,141 @@
use std::{
io, thread,
time::{Duration, Instant},
};
use anyhow::{anyhow, Result};
use palette::{Hsl, IntoColor, Srgb};
use serialport::SerialPort;
mod fixtures;
const FPS: u32 = 50;
enum MCUResponse {
Sync,
Ack,
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)),
}
}
fn main() -> Result<()> {
let frame_time = Duration::from_secs_f64(1.0 / FPS as f64);
let mut dmx_buffer = [0u8; 512];
let mut movingheads = [
fixtures::MovingHead14CH::new(1),
fixtures::MovingHead14CH::new(15),
fixtures::MovingHead14CH::new(29),
fixtures::MovingHead14CH::new(43),
];
for movinghead in movingheads.iter_mut() {
movinghead.dimmer = 134;
}
let mut ser = serialport::new("/dev/ttyUSB0", 500_000)
.timeout(Duration::from_millis(10))
.open()?;
println!("open");
// wait for initial sync
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => break,
_ => continue,
}
}
println!("sync");
let mut t = 0;
'main: loop {
let loop_start = Instant::now();
let dist = FPS as f32 / movingheads.len() as f32;
for (i, movinghead) in movingheads.iter_mut().enumerate() {
let hsl_color = Hsl::new(
360.0 * ((t % FPS) as f32 / FPS as f32) + i as f32 * dist,
1.0,
0.5,
);
movinghead.rgb = hsl_color.into_color();
movinghead.render(&mut dmx_buffer);
}
// write DMX data
let write_result = ser.write(&dmx_buffer);
if write_result.is_err() {
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => {
continue 'main;
}
_ => continue,
}
}
}
// get response
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Ack) => break,
Ok(MCUResponse::Info { num_pkts }) => {
println!("Info: {}", num_pkts);
continue;
}
Err(e) => {
println!("Error! {:?}", e);
continue;
}
Ok(MCUResponse::Sync) => {
continue 'main;
}
}
}
t += 1;
let loop_time = loop_start.elapsed();
if loop_time < frame_time {
thread::sleep(frame_time - loop_time);
} else {
println!("loop took too long!");
}
println!(
"loop time: {}ms busy, {}ms total",
loop_time.as_millis(),
loop_start.elapsed().as_millis()
);
}
}

14
beat_detection/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

17
beat_detection/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "beat_detection"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pulse = { version = "2.23.1", package = "libpulse-binding" }
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

@@ -0,0 +1,95 @@
use anyhow::{anyhow, Result};
use psimple::Simple;
use pulse::context::{Context, FlagSet as ContextFlagSet};
use pulse::mainloop::standard::{IterateResult, Mainloop};
use pulse::sample::Spec;
use pulse::stream::Direction;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
/*
Some manual poking around in PulseAudio to get the name of the default sink
*/
fn poll_mainloop(mainloop: &Rc<RefCell<Mainloop>>) -> Result<()> {
match mainloop.borrow_mut().iterate(true) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
return Err(anyhow!("Iterate state was not success"));
}
IterateResult::Success(_) => {
return Ok(());
}
}
}
fn get_pulse_default_sink() -> Result<String> {
let mainloop = Rc::new(RefCell::new(
Mainloop::new().ok_or(anyhow!("Failed to create mainloop"))?,
));
let ctx = Rc::new(RefCell::new(
Context::new(mainloop.borrow().deref(), "gib_default_sink")
.ok_or(anyhow!("Failed to create context"))?,
));
ctx.borrow_mut()
.connect(None, ContextFlagSet::NOFLAGS, None)?;
// Wait for context to be ready
loop {
poll_mainloop(&mainloop)?;
match ctx.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
return Err(anyhow!("Context was in failed/terminated state"));
}
_ => {}
}
}
let result = Rc::new(RefCell::new(None));
let cb_result_ref = result.clone();
ctx.borrow().introspect().get_server_info(move |info| {
*cb_result_ref.borrow_mut() = if let Some(ref sink_name) = info.default_sink_name {
Some(Ok(sink_name.to_string()))
} else {
Some(Err(()))
}
});
loop {
if let Some(result) = result.borrow().deref() {
return result
.to_owned()
.map_err(|_| anyhow!("Default sink name was empty"));
}
poll_mainloop(&mainloop)?;
}
}
/*
Get a PASimple instance which reads from the default sink monitor
*/
pub fn get_audio_reader(spec: &Spec) -> Result<Simple> {
let mut default_sink_name = get_pulse_default_sink()?;
default_sink_name.push_str(".monitor");
let simple = Simple::new(
None,
"piano_thingy",
Direction::Record,
Some(&default_sink_name),
"sample_yoinker",
&spec,
None,
None,
)?;
Ok(simple)
}

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!");
}
}
}

71
beat_detection/src/dsp.rs Normal file
View File

@@ -0,0 +1,71 @@
/*
Taken from Till's magic Arduino Sketch
*/
pub trait ZTransformFilter {
fn process(&mut self, sample: f32) -> f32;
}
// 20 - 200Hz Single Pole Bandpass IIR Filter
#[derive(Default)]
pub struct BassFilter {
xv: [f32; 3],
yv: [f32; 3],
}
impl ZTransformFilter for BassFilter {
fn process(&mut self, sample: f32) -> f32 {
self.xv[0] = self.xv[1];
self.xv[1] = self.xv[2];
self.xv[2] = sample / 3.0f32;
self.yv[0] = self.yv[1];
self.yv[1] = self.yv[2];
self.yv[2] = (self.xv[2] - self.xv[0])
+ (-0.7960060012f32 * self.yv[0])
+ (1.7903124146f32 * self.yv[1]);
self.yv[2]
}
}
// 10Hz Single Pole Lowpass IIR Filter
#[derive(Default)]
pub struct EnvelopeFilter {
xv: [f32; 2],
yv: [f32; 2],
}
impl ZTransformFilter for EnvelopeFilter {
fn process(&mut self, sample: f32) -> f32 {
self.xv[0] = self.xv[1];
self.xv[1] = sample / 50.0f32;
self.yv[0] = self.yv[1];
self.yv[1] = (self.xv[0] + self.xv[1]) + (0.9875119299f32 * self.yv[0]);
self.yv[1]
}
}
// 1.7 - 3.0Hz Single Pole Bandpass IIR Filter
#[derive(Default)]
pub struct BeatFilter {
xv: [f32; 3],
yv: [f32; 3],
}
impl ZTransformFilter for BeatFilter {
fn process(&mut self, sample: f32) -> f32 {
self.xv[0] = self.xv[1];
self.xv[1] = self.xv[2];
self.xv[2] = sample / 2.7f32;
self.yv[0] = self.yv[1];
self.yv[1] = self.yv[2];
self.yv[2] = (self.xv[2] - self.xv[0])
+ (-0.7169861741f32 * self.yv[0])
+ (1.4453653501f32 * self.yv[1]);
self.yv[2]
}
}

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

183
beat_detection/src/main.rs Normal file
View File

@@ -0,0 +1,183 @@
mod capture;
mod dmx_controller;
mod dsp;
mod fixtures;
mod layers;
mod util;
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
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 FPS: usize = 50;
enum BrightnessControl {
BeatDetection,
TapMetronome,
ConstantBrightness,
}
fn main() -> Result<()> {
// 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))
};
// output layers
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 active_brightness_control = BrightnessControl::BeatDetection;
let mut hsl_cycle = HSLCycle::new(Duration::from_secs(5));
let mut head_mover = HeadMover::new(Duration::from_secs_f32(0.5));
// sdl
let sdl = sdl2::init().unwrap();
let sdl_video = sdl.video().unwrap();
let window = sdl_video
.window("Beat Detectinator", 800, 600)
.resizable()
.build()?;
let mut canvas = window.into_canvas().build()?;
let texture_creator = canvas.texture_creator();
let texture_size = 250;
let mut texture = texture_creator.create_texture_target(
canvas.default_pixel_format(),
texture_size,
texture_size,
)?;
let frame_duration = Duration::from_secs_f64(1.0 / FPS as f64);
let mut event_pump = sdl.event_pump().unwrap();
'running: loop {
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => break 'running,
Event::KeyDown {
keycode: Some(Keycode::F1),
..
} => {
active_brightness_control = BrightnessControl::BeatDetection;
println!("Using automatic beat detection.")
}
Event::KeyDown {
keycode: Some(Keycode::F2),
..
} => {
active_brightness_control = BrightnessControl::TapMetronome;
println!("Using tap metronome.")
}
Event::KeyDown {
keycode: Some(Keycode::F3),
..
} => {
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);
}
}
}
beat_detector.tick(frame_duration);
tap_metronome.tick(frame_duration);
hsl_cycle.tick(frame_duration);
head_mover.tick(frame_duration);
{
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 {
Rect::new(((w - h) / 2) as i32, 0, h, h)
} else {
Rect::new(0, ((h - w) / 2) as i32, w, w)
};
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

@@ -0,0 +1,764 @@
# Blender v2.83.5 OBJ File: ''
# www.blender.org
mtllib untitled.mtl
o base_Cube.001
v 0.069532 0.101673 0.035000
v 0.069532 0.111109 0.035000
v 0.069532 0.101673 -0.035000
v 0.069532 0.111109 -0.035000
v 0.058653 0.101673 -0.035000
v 0.058653 0.111109 -0.035000
v 0.058653 0.101673 0.035000
v 0.058653 0.111109 0.035000
v 0.058653 0.189265 -0.035000
v 0.069532 0.189265 -0.035000
v 0.069532 0.189265 0.035000
v 0.058653 0.189265 0.035000
v -0.069532 0.101673 0.035000
v -0.069532 0.111109 0.035000
v -0.069532 0.101673 -0.035000
v -0.069532 0.111109 -0.035000
v -0.058653 0.101673 -0.035000
v -0.058653 0.111109 -0.035000
v -0.058653 0.101673 0.035000
v -0.058653 0.111109 0.035000
v -0.058653 0.189265 -0.035000
v -0.069532 0.189265 -0.035000
v -0.069532 0.189265 0.035000
v -0.058653 0.189265 0.035000
v 0.000000 0.101673 -0.035000
v 0.000000 0.111109 0.035000
v 0.000000 0.111109 -0.035000
v 0.000000 0.101673 0.035000
v 0.000000 0.000000 -0.080000
v 0.000000 0.092347 -0.080000
v 0.015607 0.000000 -0.078463
v 0.015607 0.092347 -0.078463
v 0.030615 0.000000 -0.073910
v 0.030615 0.092347 -0.073910
v 0.044446 0.000000 -0.066518
v 0.044446 0.092347 -0.066518
v 0.056569 0.000000 -0.056569
v 0.056569 0.092347 -0.056569
v 0.066518 0.000000 -0.044446
v 0.066518 0.092347 -0.044446
v 0.073910 0.000000 -0.030615
v 0.073910 0.092347 -0.030615
v 0.078463 0.000000 -0.015607
v 0.078463 0.092347 -0.015607
v 0.080000 0.000000 -0.000000
v 0.080000 0.092347 -0.000000
v 0.078463 0.000000 0.015607
v 0.078463 0.092347 0.015607
v 0.073910 0.000000 0.030615
v 0.073910 0.092347 0.030615
v 0.066518 0.000000 0.044446
v 0.066518 0.092347 0.044446
v 0.056569 0.000000 0.056569
v 0.056569 0.092347 0.056569
v 0.044446 0.000000 0.066518
v 0.044446 0.092347 0.066518
v 0.030615 0.000000 0.073910
v 0.030615 0.092347 0.073910
v 0.015607 0.000000 0.078463
v 0.015607 0.092347 0.078463
v -0.000000 0.000000 0.080000
v -0.000000 0.092347 0.080000
v -0.015607 0.000000 0.078463
v -0.015607 0.092347 0.078463
v -0.030615 0.000000 0.073910
v -0.030615 0.092347 0.073910
v -0.044446 0.000000 0.066518
v -0.044446 0.092347 0.066518
v -0.056569 0.000000 0.056569
v -0.056569 0.092347 0.056569
v -0.066518 0.000000 0.044446
v -0.066518 0.092347 0.044446
v -0.073910 0.000000 0.030615
v -0.073910 0.092347 0.030615
v -0.078463 0.000000 0.015607
v -0.078463 0.092347 0.015607
v -0.080000 0.000000 -0.000000
v -0.080000 0.092347 -0.000000
v -0.078463 0.000000 -0.015607
v -0.078463 0.092347 -0.015607
v -0.073910 0.000000 -0.030615
v -0.073910 0.092347 -0.030615
v -0.066518 0.000000 -0.044446
v -0.066518 0.092347 -0.044446
v -0.056568 0.000000 -0.056569
v -0.056568 0.092347 -0.056569
v -0.044446 0.000000 -0.066518
v -0.044446 0.092347 -0.066518
v -0.030615 0.000000 -0.073910
v -0.030615 0.092347 -0.073910
v -0.015607 0.000000 -0.078463
v -0.015607 0.092347 -0.078463
vt 0.375000 0.461557
vt 0.625000 0.461557
vt 0.625000 0.500000
vt 0.375000 0.500000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.336557 0.500000
vt 0.336557 0.750000
vt 0.625000 0.461557
vt 0.625000 0.500000
vt 0.129294 0.500000
vt 0.129294 0.750000
vt 0.625000 0.788443
vt 0.375000 0.788443
vt 0.375000 0.254294
vt 0.625000 0.254294
vt 0.663443 0.500000
vt 0.663443 0.750000
vt 0.625000 0.750000
vt 0.663443 0.500000
vt 0.663443 0.750000
vt 0.625000 0.788443
vt 0.625000 0.995706
vt 0.375000 0.995706
vt 0.870706 0.500000
vt 0.870706 0.750000
vt 0.375000 0.461557
vt 0.375000 0.500000
vt 0.625000 0.500000
vt 0.625000 0.461557
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.336557 0.500000
vt 0.336557 0.750000
vt 0.625000 0.500000
vt 0.625000 0.461557
vt 0.375000 0.788443
vt 0.625000 0.788443
vt 0.625000 0.750000
vt 0.663443 0.750000
vt 0.663443 0.500000
vt 0.663443 0.500000
vt 0.663443 0.750000
vt 0.625000 0.788443
vt 1.000000 0.500000
vt 1.000000 1.000000
vt 0.968750 1.000000
vt 0.968750 0.500000
vt 0.937500 1.000000
vt 0.937500 0.500000
vt 0.906250 1.000000
vt 0.906250 0.500000
vt 0.875000 1.000000
vt 0.875000 0.500000
vt 0.843750 1.000000
vt 0.843750 0.500000
vt 0.812500 1.000000
vt 0.812500 0.500000
vt 0.781250 1.000000
vt 0.781250 0.500000
vt 0.750000 1.000000
vt 0.750000 0.500000
vt 0.718750 1.000000
vt 0.718750 0.500000
vt 0.687500 1.000000
vt 0.687500 0.500000
vt 0.656250 1.000000
vt 0.656250 0.500000
vt 0.625000 1.000000
vt 0.625000 0.500000
vt 0.593750 1.000000
vt 0.593750 0.500000
vt 0.562500 1.000000
vt 0.562500 0.500000
vt 0.531250 1.000000
vt 0.531250 0.500000
vt 0.500000 1.000000
vt 0.500000 0.500000
vt 0.468750 1.000000
vt 0.468750 0.500000
vt 0.437500 1.000000
vt 0.437500 0.500000
vt 0.406250 1.000000
vt 0.406250 0.500000
vt 0.375000 1.000000
vt 0.375000 0.500000
vt 0.343750 1.000000
vt 0.343750 0.500000
vt 0.312500 1.000000
vt 0.312500 0.500000
vt 0.281250 1.000000
vt 0.281250 0.500000
vt 0.250000 1.000000
vt 0.250000 0.500000
vt 0.218750 1.000000
vt 0.218750 0.500000
vt 0.187500 1.000000
vt 0.187500 0.500000
vt 0.156250 1.000000
vt 0.156250 0.500000
vt 0.125000 1.000000
vt 0.125000 0.500000
vt 0.093750 1.000000
vt 0.093750 0.500000
vt 0.062500 1.000000
vt 0.062500 0.500000
vt 0.296822 0.485388
vt 0.250000 0.490000
vt 0.203179 0.485389
vt 0.158156 0.471731
vt 0.116663 0.449553
vt 0.080295 0.419706
vt 0.050447 0.383337
vt 0.028269 0.341844
vt 0.014612 0.296822
vt 0.010000 0.250000
vt 0.014611 0.203179
vt 0.028269 0.158156
vt 0.050447 0.116663
vt 0.080294 0.080294
vt 0.116663 0.050447
vt 0.158156 0.028269
vt 0.203178 0.014612
vt 0.250000 0.010000
vt 0.296822 0.014612
vt 0.341844 0.028269
vt 0.383337 0.050447
vt 0.419706 0.080294
vt 0.449553 0.116663
vt 0.471731 0.158156
vt 0.485388 0.203178
vt 0.490000 0.250000
vt 0.485388 0.296822
vt 0.471731 0.341844
vt 0.449553 0.383337
vt 0.419706 0.419706
vt 0.383337 0.449553
vt 0.341844 0.471731
vt 0.031250 1.000000
vt 0.031250 0.500000
vt 0.000000 1.000000
vt 0.000000 0.500000
vt 0.750000 0.490000
vt 0.796822 0.485388
vt 0.841844 0.471731
vt 0.883337 0.449553
vt 0.919706 0.419706
vt 0.949553 0.383337
vt 0.971731 0.341844
vt 0.985388 0.296822
vt 0.990000 0.250000
vt 0.985388 0.203178
vt 0.971731 0.158156
vt 0.949553 0.116663
vt 0.919706 0.080294
vt 0.883337 0.050447
vt 0.841844 0.028269
vt 0.796822 0.014612
vt 0.750000 0.010000
vt 0.703178 0.014612
vt 0.658156 0.028269
vt 0.616663 0.050447
vt 0.580294 0.080294
vt 0.550447 0.116663
vt 0.528269 0.158156
vt 0.514611 0.203179
vt 0.510000 0.250000
vt 0.514612 0.296822
vt 0.528269 0.341844
vt 0.550447 0.383337
vt 0.580295 0.419706
vt 0.616663 0.449553
vt 0.658156 0.471731
vt 0.703179 0.485389
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 1.0000 0.0000
vn -1.0000 0.0000 0.0000
vn 0.0980 0.0000 -0.9952
vn 0.2903 0.0000 -0.9569
vn 0.4714 0.0000 -0.8819
vn 0.6344 0.0000 -0.7730
vn 0.7730 0.0000 -0.6344
vn 0.8819 0.0000 -0.4714
vn 0.9569 0.0000 -0.2903
vn 0.9952 0.0000 -0.0980
vn 0.9952 0.0000 0.0980
vn 0.9569 0.0000 0.2903
vn 0.8819 0.0000 0.4714
vn 0.7730 0.0000 0.6344
vn 0.6344 0.0000 0.7730
vn 0.4714 0.0000 0.8819
vn 0.2903 0.0000 0.9569
vn 0.0980 0.0000 0.9952
vn -0.0980 0.0000 0.9952
vn -0.2903 0.0000 0.9569
vn -0.4714 0.0000 0.8819
vn -0.6344 0.0000 0.7730
vn -0.7730 0.0000 0.6344
vn -0.8819 0.0000 0.4714
vn -0.9569 0.0000 0.2903
vn -0.9952 0.0000 0.0980
vn -0.9952 0.0000 -0.0980
vn -0.9569 0.0000 -0.2903
vn -0.8819 0.0000 -0.4714
vn -0.7730 0.0000 -0.6344
vn -0.6344 0.0000 -0.7730
vn -0.4714 0.0000 -0.8819
vn -0.2903 0.0000 -0.9569
vn -0.0980 0.0000 -0.9952
usemtl None
s off
f 5/1/1 6/2/1 4/3/1 3/4/1
f 3/4/2 4/3/2 2/5/2 1/6/2
f 5/7/3 3/4/3 1/6/3 7/8/3
f 4/3/1 6/2/1 9/9/1 10/10/1
f 25/11/3 5/7/3 7/8/3 28/12/3
f 1/6/4 2/5/4 8/13/4 7/14/4
f 25/15/1 27/16/1 6/2/1 5/1/1
f 10/10/5 9/17/5 12/18/5 11/19/5
f 6/20/6 8/21/6 12/18/6 9/17/6
f 2/5/2 4/3/2 10/10/2 11/19/2
f 8/13/4 2/5/4 11/19/4 12/22/4
f 7/14/4 8/13/4 26/23/4 28/24/4
f 6/20/5 27/25/5 26/26/5 8/21/5
f 17/27/1 15/28/1 16/29/1 18/30/1
f 15/28/6 13/31/6 14/32/6 16/29/6
f 17/33/3 19/34/3 13/31/3 15/28/3
f 16/29/1 22/35/1 21/36/1 18/30/1
f 25/11/3 28/12/3 19/34/3 17/33/3
f 13/31/4 19/37/4 20/38/4 14/32/4
f 25/15/1 17/27/1 18/30/1 27/16/1
f 22/35/5 23/39/5 24/40/5 21/41/5
f 18/42/2 21/41/2 24/40/2 20/43/2
f 14/32/6 23/39/6 22/35/6 16/29/6
f 20/38/4 24/44/4 23/39/4 14/32/4
f 19/37/4 28/24/4 26/23/4 20/38/4
f 18/42/5 20/43/5 26/26/5 27/25/5
f 29/45/7 30/46/7 32/47/7 31/48/7
f 31/48/8 32/47/8 34/49/8 33/50/8
f 33/50/9 34/49/9 36/51/9 35/52/9
f 35/52/10 36/51/10 38/53/10 37/54/10
f 37/54/11 38/53/11 40/55/11 39/56/11
f 39/56/12 40/55/12 42/57/12 41/58/12
f 41/58/13 42/57/13 44/59/13 43/60/13
f 43/60/14 44/59/14 46/61/14 45/62/14
f 45/62/15 46/61/15 48/63/15 47/64/15
f 47/64/16 48/63/16 50/65/16 49/66/16
f 49/66/17 50/65/17 52/67/17 51/68/17
f 51/68/18 52/67/18 54/69/18 53/70/18
f 53/70/19 54/69/19 56/71/19 55/72/19
f 55/72/20 56/71/20 58/73/20 57/74/20
f 57/74/21 58/73/21 60/75/21 59/76/21
f 59/76/22 60/75/22 62/77/22 61/78/22
f 61/78/23 62/77/23 64/79/23 63/80/23
f 63/80/24 64/79/24 66/81/24 65/82/24
f 65/82/25 66/81/25 68/83/25 67/84/25
f 67/84/26 68/83/26 70/85/26 69/86/26
f 69/86/27 70/85/27 72/87/27 71/88/27
f 71/88/28 72/87/28 74/89/28 73/90/28
f 73/90/29 74/89/29 76/91/29 75/92/29
f 75/92/30 76/91/30 78/93/30 77/94/30
f 77/94/31 78/93/31 80/95/31 79/96/31
f 79/96/32 80/95/32 82/97/32 81/98/32
f 81/98/33 82/97/33 84/99/33 83/100/33
f 83/100/34 84/99/34 86/101/34 85/102/34
f 85/102/35 86/101/35 88/103/35 87/104/35
f 87/104/36 88/103/36 90/105/36 89/106/36
f 32/107/5 30/108/5 92/109/5 90/110/5 88/111/5 86/112/5 84/113/5 82/114/5 80/115/5 78/116/5 76/117/5 74/118/5 72/119/5 70/120/5 68/121/5 66/122/5 64/123/5 62/124/5 60/125/5 58/126/5 56/127/5 54/128/5 52/129/5 50/130/5 48/131/5 46/132/5 44/133/5 42/134/5 40/135/5 38/136/5 36/137/5 34/138/5
f 89/106/37 90/105/37 92/139/37 91/140/37
f 91/140/38 92/139/38 30/141/38 29/142/38
f 29/143/3 31/144/3 33/145/3 35/146/3 37/147/3 39/148/3 41/149/3 43/150/3 45/151/3 47/152/3 49/153/3 51/154/3 53/155/3 55/156/3 57/157/3 59/158/3 61/159/3 63/160/3 65/161/3 67/162/3 69/163/3 71/164/3 73/165/3 75/166/3 77/167/3 79/168/3 81/169/3 83/170/3 85/171/3 87/172/3 89/173/3 91/174/3
o head_Cylinder.003
v 0.000000 0.128498 0.043429
v 0.000000 0.139373 -0.043429
v 0.010167 0.129500 0.043429
v 0.008046 0.140165 -0.043429
v 0.019944 0.132465 0.043429
v 0.015782 0.142512 -0.043429
v 0.028954 0.137281 0.043429
v 0.022912 0.146323 -0.043429
v 0.036851 0.143762 0.043429
v 0.029161 0.151452 -0.043429
v 0.043332 0.151660 0.043429
v 0.034290 0.157701 -0.043429
v 0.048148 0.160670 0.043429
v 0.038101 0.164831 -0.043429
v 0.051114 0.170446 0.043429
v 0.040448 0.172567 -0.043429
v 0.052115 0.180613 0.043429
v 0.041241 0.180613 -0.043429
v 0.051114 0.190780 0.043429
v 0.040448 0.188659 -0.043429
v 0.048148 0.200557 0.043429
v 0.038101 0.196395 -0.043429
v 0.043332 0.209567 0.043429
v 0.034290 0.203525 -0.043429
v 0.036851 0.217464 0.043429
v 0.029161 0.209775 -0.043429
v 0.028954 0.223945 0.043429
v 0.022912 0.214903 -0.043429
v 0.019944 0.228761 0.043429
v 0.015782 0.218714 -0.043429
v 0.010167 0.231727 0.043429
v 0.008046 0.221061 -0.043429
v -0.000000 0.232728 0.043429
v -0.000000 0.221854 -0.043429
v -0.010167 0.231727 0.043429
v -0.008046 0.221061 -0.043429
v -0.019944 0.228761 0.043429
v -0.015782 0.218714 -0.043429
v -0.028954 0.223945 0.043429
v -0.022912 0.214903 -0.043429
v -0.036851 0.217464 0.043429
v -0.029161 0.209775 -0.043429
v -0.043332 0.209567 0.043429
v -0.034290 0.203525 -0.043429
v -0.048148 0.200557 0.043429
v -0.038101 0.196395 -0.043429
v -0.051114 0.190780 0.043429
v -0.040448 0.188659 -0.043429
v -0.052115 0.180613 0.043429
v -0.041241 0.180613 -0.043429
v -0.051114 0.170446 0.043429
v -0.040448 0.172567 -0.043429
v -0.048148 0.160670 0.043429
v -0.038101 0.164831 -0.043429
v -0.043332 0.151660 0.043429
v -0.034290 0.157701 -0.043429
v -0.036851 0.143762 0.043429
v -0.029161 0.151452 -0.043429
v -0.028953 0.137281 0.043429
v -0.022912 0.146323 -0.043429
v -0.019943 0.132465 0.043429
v -0.015782 0.142512 -0.043429
v -0.010167 0.129500 0.043429
v -0.008046 0.140165 -0.043429
v 0.000000 0.128498 -0.021164
v 0.010167 0.129500 -0.021164
v 0.019944 0.132465 -0.021164
v 0.028954 0.137281 -0.021164
v 0.036851 0.143762 -0.021164
v 0.043332 0.151660 -0.021164
v 0.048148 0.160670 -0.021164
v 0.051114 0.170446 -0.021164
v 0.052115 0.180613 -0.021164
v 0.051114 0.190780 -0.021164
v 0.048148 0.200557 -0.021164
v 0.043332 0.209567 -0.021164
v 0.036851 0.217464 -0.021164
v 0.028954 0.223945 -0.021164
v 0.019944 0.228761 -0.021164
v 0.010167 0.231727 -0.021164
v -0.000000 0.232728 -0.021164
v -0.010167 0.231727 -0.021164
v -0.019944 0.228761 -0.021164
v -0.028954 0.223945 -0.021164
v -0.036851 0.217464 -0.021164
v -0.043332 0.209567 -0.021164
v -0.048148 0.200557 -0.021164
v -0.051114 0.190780 -0.021164
v -0.052115 0.180613 -0.021164
v -0.051114 0.170446 -0.021164
v -0.048148 0.160670 -0.021164
v -0.043332 0.151660 -0.021164
v -0.036851 0.143762 -0.021164
v -0.028953 0.137281 -0.021164
v -0.019943 0.132465 -0.021164
v -0.010167 0.129500 -0.021164
vt 1.000000 0.871829
vt 1.000000 1.000000
vt 0.968750 1.000000
vt 0.968750 0.871829
vt 0.937500 1.000000
vt 0.937500 0.871829
vt 0.906250 1.000000
vt 0.906250 0.871829
vt 0.875000 1.000000
vt 0.875000 0.871829
vt 0.843750 1.000000
vt 0.843750 0.871829
vt 0.812500 1.000000
vt 0.812500 0.871829
vt 0.781250 1.000000
vt 0.781250 0.871829
vt 0.750000 1.000000
vt 0.750000 0.871829
vt 0.718750 1.000000
vt 0.718750 0.871829
vt 0.687500 1.000000
vt 0.687500 0.871829
vt 0.656250 1.000000
vt 0.656250 0.871829
vt 0.625000 1.000000
vt 0.625000 0.871829
vt 0.593750 1.000000
vt 0.593750 0.871829
vt 0.562500 1.000000
vt 0.562500 0.871829
vt 0.531250 1.000000
vt 0.531250 0.871829
vt 0.500000 1.000000
vt 0.500000 0.871829
vt 0.468750 1.000000
vt 0.468750 0.871829
vt 0.437500 1.000000
vt 0.437500 0.871829
vt 0.406250 1.000000
vt 0.406250 0.871829
vt 0.375000 1.000000
vt 0.375000 0.871829
vt 0.343750 1.000000
vt 0.343750 0.871829
vt 0.312500 1.000000
vt 0.312500 0.871829
vt 0.281250 1.000000
vt 0.281250 0.871829
vt 0.250000 1.000000
vt 0.250000 0.871829
vt 0.218750 1.000000
vt 0.218750 0.871829
vt 0.187500 1.000000
vt 0.187500 0.871829
vt 0.156250 1.000000
vt 0.156250 0.871829
vt 0.125000 1.000000
vt 0.125000 0.871829
vt 0.093750 1.000000
vt 0.093750 0.871829
vt 0.062500 1.000000
vt 0.062500 0.871829
vt 0.296822 0.485388
vt 0.250000 0.490000
vt 0.203179 0.485389
vt 0.158156 0.471731
vt 0.116663 0.449553
vt 0.080295 0.419706
vt 0.050447 0.383337
vt 0.028269 0.341844
vt 0.014612 0.296822
vt 0.010000 0.250000
vt 0.014611 0.203179
vt 0.028269 0.158156
vt 0.050447 0.116663
vt 0.080294 0.080295
vt 0.116663 0.050447
vt 0.158156 0.028269
vt 0.203178 0.014612
vt 0.250000 0.010000
vt 0.296822 0.014612
vt 0.341844 0.028269
vt 0.383337 0.050447
vt 0.419706 0.080294
vt 0.449553 0.116663
vt 0.471731 0.158156
vt 0.485388 0.203178
vt 0.490000 0.250000
vt 0.485388 0.296822
vt 0.471731 0.341844
vt 0.449553 0.383337
vt 0.419706 0.419706
vt 0.383337 0.449553
vt 0.341844 0.471731
vt 0.031250 1.000000
vt 0.031250 0.871829
vt 0.000000 1.000000
vt 0.000000 0.871829
vt 0.750000 0.490000
vt 0.796822 0.485388
vt 0.841844 0.471731
vt 0.883337 0.449553
vt 0.919706 0.419706
vt 0.949553 0.383337
vt 0.971731 0.341844
vt 0.985388 0.296822
vt 0.990000 0.250000
vt 0.985388 0.203178
vt 0.971731 0.158156
vt 0.949553 0.116663
vt 0.919706 0.080294
vt 0.883337 0.050447
vt 0.841844 0.028269
vt 0.796822 0.014612
vt 0.750000 0.010000
vt 0.703178 0.014612
vt 0.658156 0.028269
vt 0.616663 0.050447
vt 0.580294 0.080295
vt 0.550447 0.116663
vt 0.528269 0.158156
vt 0.514611 0.203179
vt 0.510000 0.250000
vt 0.514612 0.296822
vt 0.528269 0.341844
vt 0.550447 0.383337
vt 0.580295 0.419706
vt 0.616663 0.449553
vt 0.658156 0.471731
vt 0.703179 0.485389
vt 0.031250 0.500000
vt 0.000000 0.500000
vt 0.062500 0.500000
vt 0.093750 0.500000
vt 0.125000 0.500000
vt 0.156250 0.500000
vt 0.187500 0.500000
vt 0.218750 0.500000
vt 0.250000 0.500000
vt 0.281250 0.500000
vt 0.312500 0.500000
vt 0.343750 0.500000
vt 0.375000 0.500000
vt 0.406250 0.500000
vt 0.437500 0.500000
vt 0.468750 0.500000
vt 0.500000 0.500000
vt 0.531250 0.500000
vt 0.562500 0.500000
vt 0.593750 0.500000
vt 0.625000 0.500000
vt 0.656250 0.500000
vt 0.687500 0.500000
vt 0.718750 0.500000
vt 0.750000 0.500000
vt 0.781250 0.500000
vt 0.812500 0.500000
vt 0.843750 0.500000
vt 0.875000 0.500000
vt 0.906250 0.500000
vt 0.937500 0.500000
vt 0.968750 0.500000
vt 1.000000 0.500000
vn 0.0882 -0.8951 -0.4371
vn 0.2611 -0.8607 -0.4371
vn 0.4240 -0.7932 -0.4371
vn 0.5706 -0.6952 -0.4371
vn 0.6952 -0.5706 -0.4371
vn 0.7932 -0.4240 -0.4371
vn 0.8607 -0.2611 -0.4371
vn 0.8951 -0.0882 -0.4371
vn 0.8951 0.0882 -0.4371
vn 0.8607 0.2611 -0.4371
vn 0.7932 0.4240 -0.4371
vn 0.6952 0.5706 -0.4371
vn 0.5706 0.6952 -0.4371
vn 0.4240 0.7932 -0.4371
vn 0.2611 0.8607 -0.4371
vn 0.0882 0.8951 -0.4371
vn -0.0882 0.8951 -0.4371
vn -0.2611 0.8607 -0.4371
vn -0.4240 0.7932 -0.4371
vn -0.5706 0.6952 -0.4371
vn -0.6952 0.5706 -0.4371
vn -0.7932 0.4240 -0.4371
vn -0.8607 0.2611 -0.4371
vn -0.8951 0.0882 -0.4371
vn -0.8951 -0.0882 -0.4371
vn -0.8607 -0.2611 -0.4371
vn -0.7932 -0.4240 -0.4371
vn -0.6952 -0.5706 -0.4371
vn -0.5706 -0.6952 -0.4371
vn -0.4240 -0.7932 -0.4371
vn 0.0000 0.0000 -1.0000
vn -0.2611 -0.8607 -0.4371
vn -0.0882 -0.8951 -0.4371
vn 0.0000 0.0000 1.0000
vn -0.0980 -0.9952 0.0000
vn -0.2903 -0.9569 0.0000
vn -0.4714 -0.8819 0.0000
vn -0.6344 -0.7730 0.0000
vn -0.7730 -0.6344 0.0000
vn -0.8819 -0.4714 0.0000
vn -0.9569 -0.2903 0.0000
vn -0.9952 -0.0980 0.0000
vn -0.9952 0.0980 0.0000
vn -0.9569 0.2903 0.0000
vn -0.8819 0.4714 0.0000
vn -0.7730 0.6344 0.0000
vn -0.6344 0.7730 0.0000
vn -0.4714 0.8819 -0.0000
vn -0.2903 0.9569 0.0000
vn -0.0980 0.9952 -0.0000
vn 0.0980 0.9952 0.0000
vn 0.2903 0.9569 0.0000
vn 0.4714 0.8819 0.0000
vn 0.6344 0.7730 0.0000
vn 0.7730 0.6344 -0.0000
vn 0.8819 0.4714 0.0000
vn 0.9569 0.2903 -0.0000
vn 0.9952 0.0980 0.0000
vn 0.9952 -0.0980 0.0000
vn 0.9569 -0.2903 0.0000
vn 0.8819 -0.4714 0.0000
vn 0.7730 -0.6344 0.0000
vn 0.6344 -0.7730 0.0000
vn 0.4714 -0.8819 0.0000
vn 0.2903 -0.9569 0.0000
vn 0.0980 -0.9952 0.0000
usemtl None
s off
f 157/175/39 94/176/39 96/177/39 158/178/39
f 158/178/40 96/177/40 98/179/40 159/180/40
f 159/180/41 98/179/41 100/181/41 160/182/41
f 160/182/42 100/181/42 102/183/42 161/184/42
f 161/184/43 102/183/43 104/185/43 162/186/43
f 162/186/44 104/185/44 106/187/44 163/188/44
f 163/188/45 106/187/45 108/189/45 164/190/45
f 164/190/46 108/189/46 110/191/46 165/192/46
f 165/192/47 110/191/47 112/193/47 166/194/47
f 166/194/48 112/193/48 114/195/48 167/196/48
f 167/196/49 114/195/49 116/197/49 168/198/49
f 168/198/50 116/197/50 118/199/50 169/200/50
f 169/200/51 118/199/51 120/201/51 170/202/51
f 170/202/52 120/201/52 122/203/52 171/204/52
f 171/204/53 122/203/53 124/205/53 172/206/53
f 172/206/54 124/205/54 126/207/54 173/208/54
f 173/208/55 126/207/55 128/209/55 174/210/55
f 174/210/56 128/209/56 130/211/56 175/212/56
f 175/212/57 130/211/57 132/213/57 176/214/57
f 176/214/58 132/213/58 134/215/58 177/216/58
f 177/216/59 134/215/59 136/217/59 178/218/59
f 178/218/60 136/217/60 138/219/60 179/220/60
f 179/220/61 138/219/61 140/221/61 180/222/61
f 180/222/62 140/221/62 142/223/62 181/224/62
f 181/224/63 142/223/63 144/225/63 182/226/63
f 182/226/64 144/225/64 146/227/64 183/228/64
f 183/228/65 146/227/65 148/229/65 184/230/65
f 184/230/66 148/229/66 150/231/66 185/232/66
f 185/232/67 150/231/67 152/233/67 186/234/67
f 186/234/68 152/233/68 154/235/68 187/236/68
f 96/237/69 94/238/69 156/239/69 154/240/69 152/241/69 150/242/69 148/243/69 146/244/69 144/245/69 142/246/69 140/247/69 138/248/69 136/249/69 134/250/69 132/251/69 130/252/69 128/253/69 126/254/69 124/255/69 122/256/69 120/257/69 118/258/69 116/259/69 114/260/69 112/261/69 110/262/69 108/263/69 106/264/69 104/265/69 102/266/69 100/267/69 98/268/69
f 187/236/70 154/235/70 156/269/70 188/270/70
f 188/270/71 156/269/71 94/271/71 157/272/71
f 93/273/72 95/274/72 97/275/72 99/276/72 101/277/72 103/278/72 105/279/72 107/280/72 109/281/72 111/282/72 113/283/72 115/284/72 117/285/72 119/286/72 121/287/72 123/288/72 125/289/72 127/290/72 129/291/72 131/292/72 133/293/72 135/294/72 137/295/72 139/296/72 141/297/72 143/298/72 145/299/72 147/300/72 149/301/72 151/302/72 153/303/72 155/304/72
f 155/305/73 188/270/73 157/272/73 93/306/73
f 153/307/74 187/236/74 188/270/74 155/305/74
f 151/308/75 186/234/75 187/236/75 153/307/75
f 149/309/76 185/232/76 186/234/76 151/308/76
f 147/310/77 184/230/77 185/232/77 149/309/77
f 145/311/78 183/228/78 184/230/78 147/310/78
f 143/312/79 182/226/79 183/228/79 145/311/79
f 141/313/80 181/224/80 182/226/80 143/312/80
f 139/314/81 180/222/81 181/224/81 141/313/81
f 137/315/82 179/220/82 180/222/82 139/314/82
f 135/316/83 178/218/83 179/220/83 137/315/83
f 133/317/84 177/216/84 178/218/84 135/316/84
f 131/318/85 176/214/85 177/216/85 133/317/85
f 129/319/86 175/212/86 176/214/86 131/318/86
f 127/320/87 174/210/87 175/212/87 129/319/87
f 125/321/88 173/208/88 174/210/88 127/320/88
f 123/322/89 172/206/89 173/208/89 125/321/89
f 121/323/90 171/204/90 172/206/90 123/322/90
f 119/324/91 170/202/91 171/204/91 121/323/91
f 117/325/92 169/200/92 170/202/92 119/324/92
f 115/326/93 168/198/93 169/200/93 117/325/93
f 113/327/94 167/196/94 168/198/94 115/326/94
f 111/328/95 166/194/95 167/196/95 113/327/95
f 109/329/96 165/192/96 166/194/96 111/328/96
f 107/330/97 164/190/97 165/192/97 109/329/97
f 105/331/98 163/188/98 164/190/98 107/330/98
f 103/332/99 162/186/99 163/188/99 105/331/99
f 101/333/100 161/184/100 162/186/100 103/332/100
f 99/334/101 160/182/101 161/184/101 101/333/101
f 97/335/102 159/180/102 160/182/102 99/334/102
f 95/336/103 158/178/103 159/180/103 97/335/103
f 93/337/104 157/175/104 158/178/104 95/336/104

BIN
frontend/movinghead.blend Normal file

Binary file not shown.

View File

@@ -1,7 +1,6 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import * as Stats from 'stats.js'; import * as Stats from 'stats.js';
const canvas = document.getElementById('threejs-preview')! as HTMLCanvasElement; const canvas = document.getElementById('threejs-preview')! as HTMLCanvasElement;
@@ -11,16 +10,25 @@ const scene = new THREE.Scene();
scene.background = new THREE.Color(0x161618); scene.background = new THREE.Color(0x161618);
scene.fog = new THREE.FogExp2(0x161618, 0.002); scene.fog = new THREE.FogExp2(0x161618, 0.002);
// model loading
const objLoader = new OBJLoader(THREE.DefaultLoadingManager);
objLoader.load('assets/movinghead.obj', (objs: THREE.Group) => {
console.log('LOADER AYAYA');
objs.traverse((obj) => {
console.log(`traverse ${obj}`);
});
});
// camera controls
const camera = new THREE.PerspectiveCamera(75); const camera = new THREE.PerspectiveCamera(75);
camera.position.set(400, 200, 0); camera.position.set(400, 200, 0);
// controls
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.enableDamping = true;
controls.dampingFactor = 0.05; controls.dampingFactor = 0.05;
controls.screenSpacePanning = false; controls.screenSpacePanning = false;
// controls.maxPolarAngle = Math.PI / 2;
// dbg // dbg
const cylinder = new THREE.CylinderGeometry(0, 10, 30, 4, 1); const cylinder = new THREE.CylinderGeometry(0, 10, 30, 4, 1);

10
frontend/untitled.mtl Normal file
View File

@@ -0,0 +1,10 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl None
Ns 500
Ka 0.8 0.8 0.8
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2

1
guitarhero/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
venv

57
guitarhero/fixtures.py Normal file
View File

@@ -0,0 +1,57 @@
import math
from util import clamp, rescale
class MovingHead:
def __init__(self, start_addr):
self.start_addr = start_addr
self.pan = 0 # -pi/2 to pi/2
self.tilt = 0 # -3pi/2 to 3pi/2
self.speed = 0
self.dimmer = 0 # 0 to 1
self.rgbw = (0, 0, 0, 0)
def __str__(self):
return (
f"MovingHead({self.start_addr}): pan={self.pan!r}, "
f"tilt={self.tilt!r}, speed={self.speed!r}, "
f"dimmer={self.dimmer!r}, rgbw={self.rgbw!r}"
)
def render(self, dst):
pan = rescale(self.pan, (-1.5 * math.pi, 1.5 * math.pi), (255, 0))
pan = clamp(int(pan), (0, 255))
pan_fine = 0
tilt = rescale(self.tilt, (-0.5 * math.pi, 0.5 * math.pi), (0, 255))
tilt = clamp(int(tilt), (0, 255))
tilt_fine = 0
dimmer = clamp(7 + int(127 * self.dimmer), (7, 134))
(r, g, b, w) = self.rgbw
channels = [
pan,
pan_fine,
tilt,
tilt_fine,
self.speed,
dimmer,
r,
g,
b,
w,
0, # color mode
0, # auto jump speed
0, # control mode
0, # reset
]
offset = self.start_addr - 1
dst[offset : offset + len(channels)] = channels

139
guitarhero/main.py Normal file
View File

@@ -0,0 +1,139 @@
import math
import pygame
import serial
import time
import colorsys
from util import clamp, rescale
from fixtures import MovingHead
if __name__ == "__main__":
movingheads = [
MovingHead(1),
MovingHead(15),
MovingHead(29),
MovingHead(43),
]
# movingheads[0].rgbw = (59, 130, 246, 0) # blue
# movingheads[1].rgbw = (245, 158, 11, 0) # yellow
# movingheads[2].rgbw = (239, 68, 68, 0) # red
# movingheads[3].rgbw = (16, 185, 129, 0) # green
for head in movingheads:
head.rgbw = (0, 0, 0xFF, 0)
for head in movingheads:
head.pan = -0.5 * math.pi
dmx_data = bytearray(512)
# pygame initialization
pygame.init() # pylint: disable=no-member; wtf?
screen = pygame.display.set_mode((420, 69), pygame.RESIZABLE)
pygame.display.set_caption("meh")
pygame.joystick.init()
gamepad = pygame.joystick.Joystick(0)
gamepad.init()
clock = pygame.time.Clock()
# main loop
running = True # this is clearly not constant, see 17 lines below???
with serial.Serial("/dev/ttyUSB0", 500_000) as ser:
def sync():
# wait for sync
while True:
b = ser.readline()
if b.strip() == b"Sync.":
return
sync()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT: # pylint: disable=no-member;
running = False
# axes:
pitch = gamepad.get_axis(4) # 0 horizontal, -1 up, 1 down
whammy = gamepad.get_axis(3) # -1 default, 1 if max. pressed
# buttons:
button_state = [
gamepad.get_button(i)
for i in [
0, # green
1, # red
3, # yellow
2, # blue
4, # orange
]
]
# strumm bar
(_, bar) = gamepad.get_hat(0)
print(
f"pitch: {pitch:+0.2f}, "
f"whammy: {whammy:+0.2f}, "
f"buttons: {button_state}, "
f"bar: {bar: 1}"
)
# render
for head_id, button_id in enumerate([3, 2, 1, 0]):
movingheads[head_id].dimmer = 0 if button_state[button_id] else 1
pitch = max(0, min(1, -1 * pitch)) # 0 horizontal, 1 up
tilt = rescale(pitch, (0, 1), (-0.5 * math.pi, 0))
for head in movingheads:
head.tilt = tilt
r, g, b = colorsys.hls_to_rgb((time.time() / 12) % 1, 0.5, 1)
head.rgbw = (int(255 * r), int(255 * g), int(255 * b), 0)
ANGLE_HOME = -0.5 * math.pi
ANGLE_OUTER = 0.5235988
ANGLE_INNER = 0.2617994
movingheads[0].pan = rescale(
whammy, (-1, 1), (ANGLE_HOME, ANGLE_HOME + ANGLE_OUTER)
)
movingheads[1].pan = rescale(
whammy, (-1, 1), (ANGLE_HOME, ANGLE_HOME + ANGLE_INNER)
)
movingheads[2].pan = rescale(
whammy, (-1, 1), (ANGLE_HOME, ANGLE_HOME - ANGLE_INNER)
)
movingheads[3].pan = rescale(
whammy, (-1, 1), (ANGLE_HOME, ANGLE_HOME - ANGLE_OUTER)
)
for head in movingheads:
# print(head)
head.render(dmx_data)
ser.write(dmx_data)
ser.flush()
response = ser.readline()
if response.strip() != b"Ack.":
print(f"received bad response: {response!r}")
sync()
try:
clock.tick(50)
except KeyboardInterrupt:
running = False
pygame.quit() # pylint: disable=no-member;

View File

@@ -0,0 +1,2 @@
pygame==2.0.1
pyserial==3.5

11
guitarhero/util.py Normal file
View File

@@ -0,0 +1,11 @@
def clamp(x, ab):
(a, b) = ab
return max(a, min(b, x))
def rescale(x, from_limits, to_limits):
(a, b) = from_limits
x_0_1 = (x - a) / (b - a)
(c, d) = to_limits
return c + (d - c) * x_0_1

1
libpulse_aubio/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

204
libpulse_aubio/Cargo.lock generated Normal file
View File

@@ -0,0 +1,204 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "aubio"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "577cfca08ecb60eb4ad21f19507bc614229130112ac606cd493c0e5bb5318f54"
dependencies = [
"aubio-sys",
]
[[package]]
name = "aubio-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99ef2dfeaceccd0b8a6d72203409acc927d9eebc8180c5756099549c9f8f20a8"
dependencies = [
"cc",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
dependencies = [
"jobserver",
]
[[package]]
name = "jobserver"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
[[package]]
name = "libpulse-binding"
version = "2.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86835d7763ded6bc16b6c0061ec60214da7550dfcd4ef93745f6f0096129676a"
dependencies = [
"bitflags",
"libc",
"libpulse-sys",
"num-derive",
"num-traits",
"winapi",
]
[[package]]
name = "libpulse-simple-binding"
version = "2.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6a22538257c4d522bea6089d6478507f5d2589ea32150e20740aaaaaba44590"
dependencies = [
"libpulse-binding",
"libpulse-simple-sys",
"libpulse-sys",
]
[[package]]
name = "libpulse-simple-sys"
version = "1.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8b0fcb9665401cc7c156c337c8edc7eb4e797b9d3ae1667e1e9e17b29e0c7c"
dependencies = [
"libpulse-sys",
"pkg-config",
]
[[package]]
name = "libpulse-sys"
version = "1.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12950b69c1b66233a900414befde36c8d4ea49deec1e1f34e4cd2f586e00c7d"
dependencies = [
"libc",
"num-derive",
"num-traits",
"pkg-config",
"winapi",
]
[[package]]
name = "libpulse_aubio"
version = "0.1.0"
dependencies = [
"anyhow",
"aubio",
"libpulse-binding",
"libpulse-simple-binding",
]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "proc-macro2"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

12
libpulse_aubio/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "libpulse_aubio"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aubio = "0.2.1"
pulse = { version = "2.23.1", package = "libpulse-binding" }
psimple = { version = "2.23.0", package = "libpulse-simple-binding" }
anyhow = "1.0.44"

142
libpulse_aubio/src/main.rs Normal file
View File

@@ -0,0 +1,142 @@
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::stream::Direction;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
const BUF_SIZE: usize = 1024;
const HOP_SIZE: usize = 512;
const SAMPLE_RATE: u32 = 44100;
fn get_pulse_default_sink() -> Result<String> {
let mainloop = Rc::new(RefCell::new(
Mainloop::new().ok_or(anyhow!("Failed to create mainloop"))?,
));
let ctx = Rc::new(RefCell::new(
Context::new(mainloop.borrow().deref(), "gib_default_sink")
.ok_or(anyhow!("Failed to create context"))?,
));
ctx.borrow_mut()
.connect(None, ContextFlagSet::NOFLAGS, None)?;
// Wait for context to be ready
loop {
match mainloop.borrow_mut().iterate(false) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
return Err(anyhow!("Iterate state was not success"));
}
IterateResult::Success(_) => {}
}
match ctx.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
return Err(anyhow!("Context was in failed/terminated state"));
}
_ => {}
}
}
let result = Rc::new(RefCell::new(None));
let cb_result_ref = result.clone();
ctx.borrow().introspect().get_server_info(move |info| {
*cb_result_ref.borrow_mut() = if let Some(sink_name) = info.default_sink_name.clone() {
Some(Ok(sink_name.to_string()))
} else {
Some(Err(()))
}
});
while result.borrow().is_none() {
match mainloop.borrow_mut().iterate(false) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
return Err(anyhow!("Iterate state was not success"));
}
IterateResult::Success(_) => {}
}
}
let result = result.borrow().clone();
result
.unwrap()
.map_err(|_| anyhow!("Default sink name was empty"))
}
fn main() -> Result<()> {
println!("AYAYA");
let mut default_sink_name = get_pulse_default_sink()?;
default_sink_name.push_str(".monitor");
println!(
"gotted pulse default sink (hopefully): {}",
default_sink_name
);
let spec = Spec {
format: Format::F32le,
channels: 1,
rate: 44100,
};
assert!(spec.is_valid());
let s = Simple::new(
None,
"AAAAAAAA",
Direction::Record,
Some(&default_sink_name),
"BBBBBBBB",
&spec,
None,
None,
)?;
let mut tempo = aubio::Tempo::new(aubio::OnsetMode::SpecFlux, BUF_SIZE, HOP_SIZE, SAMPLE_RATE)?;
let mut onset = aubio::Onset::new(aubio::OnsetMode::Energy, BUF_SIZE, HOP_SIZE, SAMPLE_RATE)?;
let mut data = [0u8; 4 * BUF_SIZE];
let mut float_data = [0f32; BUF_SIZE];
loop {
s.read(&mut data)?;
for (i, f) in float_data.iter_mut().enumerate() {
let mut float_bytes = [0u8; 4];
float_bytes.copy_from_slice(&data[4 * i..4 * i + 4]);
*f = f32::from_le_bytes(float_bytes);
}
let r = tempo.do_result(&float_data)?;
if r > 0f32 {
println!(
"{}ms, {}s, frame {}, {} bpm, confidence {}",
tempo.get_last_ms(),
tempo.get_last_s(),
tempo.get_last(),
tempo.get_bpm(),
tempo.get_confidence()
);
}
/*
let r = onset.do_result(&float_data)?;
if r > 0f32 {
println!("onset i guess: {}", onset.get_last());
} else {
println!("nope")
}
*/
}
}

5
microcontroller/.gitignore vendored Normal file
View File

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

11
microcontroller/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Microcontroller
Essentially a makeshift USB-DMX-Interface.
Note that we're using a NodeMCU V3, which provides the 5 volts from USB on the `VU` Pin
## Breadboard Setup
![assets/breadboard.png](assets/breadboard.png)
## Stripboard Setup
![assets/stripboard.png](assets/stripboard.png)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

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

View File

@@ -0,0 +1,96 @@
/*
* E131_Test.ino - Simple sketch to listen for E1.31 data on an ESP32
* and print some statistics.
*
* Project: ESPAsyncE131 - Asynchronous E.131 (sACN) library for Arduino ESP8266 and ESP32
* Copyright (c) 2019 Shelby Merrick
* http://www.forkineye.com
*
* This program is provided free for you to use in any way that you wish,
* subject to the laws and regulations where you are using it. Due diligence
* is strongly suggested before using this code. Please give credit where due.
*
* The Author makes no warranty of any kind, express or implied, with regard
* to this program or the documentation contained in this document. The
* Author shall not be liable in any event for incidental or consequential
* damages in connection with, or arising out of, the furnishing, performance
* or use of these programs.
*
*/
#include <ESPAsyncE131.h>
#define UNIVERSE 1 // First DMX Universe to listen for
#define UNIVERSE_COUNT 1 // Total number of Universes to listen for, starting at UNIVERSE
const char ssid[] = "dmx"; // Replace with your SSID
const char passphrase[] = "DGqnu6p8ut6H2QQQ2LQH"; // Replace with your WPA2 passphrase
unsigned long packets_processed = 0;
unsigned long last_update;
void on_packet(e131_packet_t *packet, IPAddress address)
{
/*
Serial.printf("Universe %u / %u Channels | CH1: %u\n",
htons(packet->universe),
htons(packet->property_value_count) - 1,
packet->property_values[1]);
*/
packets_processed += 1;
}
// ESPAsyncE131 instance with callback
ESPAsyncE131 e131(on_packet);
void setup()
{
Serial.begin(500000);
delay(10);
// Make sure you're in station mode
WiFi.mode(WIFI_STA);
Serial.println("");
Serial.print(F("Connecting to "));
Serial.print(ssid);
if (passphrase != NULL)
WiFi.begin(ssid, passphrase);
else
WiFi.begin(ssid);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print(F("Connected with IP: "));
Serial.println(WiFi.localIP());
// Choose one to begin listening for E1.31 data
//if (e131.begin(E131_UNICAST)) // Listen via Unicast
if (e131.begin(E131_MULTICAST, UNIVERSE, UNIVERSE_COUNT)) // Listen via Multicast
Serial.println(F("Listening for data..."));
else
Serial.println(F("*** e131.begin failed ***"));
last_update = millis();
}
void loop()
{
unsigned long now = millis();
if (now - last_update > 1000)
{
last_update = now;
Serial.printf(
"%lu %u packets processed.\n",
now,
packets_processed);
}
}

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

View File

@@ -1,76 +1,162 @@
unsigned long tic_loop = 0; /*
Triple buffering data structure
Example writing:
----------------
DMXTripleBuffer buffer;
bytes_written = producer.write(
buffer.fill_buffer + buffer.fill_pos, // destination pointer
512 - buffer.fill_pos // maximum allowed
);
buffer.fill_pos += bytes_written;
if buffer.fill_pos == 512 {
buffer.on_fill_complete(); // swap buffers
buffer.fill_pos = 0; // reset fill pos
}
*/
struct InternalBuffer
{
char data[512] = {0};
bool is_fresh = false;
};
class DMXTripleBuffer
{
public:
DMXTripleBuffer()
{
fill_buffer = &_buffers[0];
drain_buffer = &_buffers[1];
off_buffer = &_buffers[2];
}
void on_fill_complete()
{
fill_pos = 0;
fill_buffer->is_fresh = true;
std::swap(fill_buffer, off_buffer);
}
void on_drain_complete()
{
drain_pos = 0;
drain_buffer->is_fresh = false;
if (off_buffer->is_fresh)
{
std::swap(drain_buffer, off_buffer);
}
}
InternalBuffer *fill_buffer, *drain_buffer, *off_buffer;
size_t fill_pos = 0, drain_pos = 0;
private:
InternalBuffer _buffers[3];
};
/*
Some globals
*/
DMXTripleBuffer buffer; // Triple buffer instance
bool packet_ready = true; // flag to write header
// send a "Sync." every second if no data is coming in
unsigned long time_since_last_sync; unsigned long time_since_last_sync;
const unsigned long SYNC_TIMEOUT = 1000; const unsigned long SYNC_TIMEOUT = 1000;
const unsigned int FRAME_TIME = 20; // 20 ms -> 50 FPS /*
setup
const size_t UNIVERSE_SIZE = 512; initialize both Serial connections and write the initial "Sync."
*/
byte channels_buffer[UNIVERSE_SIZE] = {0};
size_t bytes_read = 0;
void setup() void setup()
{ {
Serial.begin(115200); // USB Serial.begin(500000); // USB
Serial.setRxBufferSize(512); // to fit the full DMX packet. (default is 128)
while (!Serial) while (!Serial)
{ {
// spin until serial is up // spin until serial is up
} }
Serial1.begin(250000, SERIAL_8N2); // DMX
while (!Serial1)
{
// spin until serial1 is up
}
Serial.println(); Serial.println();
Serial.println("Sync."); Serial.println("Sync.");
time_since_last_sync = millis(); time_since_last_sync = millis();
Serial1.begin(250000, SERIAL_8N2); // DMX
} }
/*
loop
continuously poll Serial1 for writing and Serial for reading
only read/write as much as fits into the UART buffers to avoid blocking
since Serial1 is set to 250000 baud, it should be able to write 50 DMX packets per second
*/
void loop() void loop()
{ {
bool packet_ready = update_buffer();
// output
if (packet_ready) if (packet_ready)
{ {
send_packet(); send_dmx_header();
packet_ready = false;
} }
} size_t n = Serial1.availableForWrite();
void send_packet() size_t written = Serial1.write(
{ buffer.drain_buffer->data + buffer.drain_pos,
send_dmx_header(); std::min(n, 512 - buffer.drain_pos));
Serial1.write(channels_buffer, UNIVERSE_SIZE);
} buffer.drain_pos += written;
if (buffer.drain_pos == 512)
{
buffer.on_drain_complete();
packet_ready = true;
}
// input
n = Serial.available();
bool update_buffer()
{
unsigned long now = millis(); unsigned long now = millis();
size_t n = Serial.available();
if (!n) if (!n)
{ {
// nothing available to read if (now - time_since_last_sync > 1000)
if (now - time_since_last_sync > SYNC_TIMEOUT)
{ {
// re-sync buffer.fill_pos = 0;
bytes_read = 0;
Serial.println("Sync."); Serial.println("Sync.");
time_since_last_sync = now; time_since_last_sync = now;
} }
return;
return false;
} }
time_since_last_sync = now; time_since_last_sync = now;
int bytes_received = Serial.read(channels_buffer + bytes_read, std::min(n, UNIVERSE_SIZE - bytes_read)); size_t read = Serial.read(
bytes_read += bytes_received; buffer.fill_buffer->data + buffer.fill_pos,
std::min(n, 512 - buffer.fill_pos));
if (bytes_read == UNIVERSE_SIZE) buffer.fill_pos += read;
if (buffer.fill_pos == 512)
{ {
bytes_read = 0; buffer.on_fill_complete();
Serial.println("Ack."); Serial.println("Ack.");
return true;
}
else
{
return false;
} }
} }

View File

@@ -1,87 +0,0 @@
unsigned long tic_loop = 0;
const unsigned int FRAME_TIME = 25; // 20 ms -> 50 FPS
byte channels_buffer[512] = {0};
unsigned int bytes_to_write = 512;
const unsigned int START_ADDR = 10;
const unsigned int NUM_CHANNELS = 9;
byte dmx_data[] = {
0,
0,
134,
255,
0,
0,
0,
0,
0,
};
void setup()
{
Serial.begin(115200); // USB
Serial1.begin(250000, SERIAL_8N2); // DMX
tic_loop = millis();
for (int i = 0; i < NUM_CHANNELS; ++i)
{
channels_buffer[START_ADDR - 1 + i] = dmx_data[i];
}
}
void loop()
{
// update_buffer();
// this section gets executed at a maximum rate of around 40Hz
if ((millis() - tic_loop) > FRAME_TIME)
{
tic_loop = millis();
send_dmx_header();
Serial1.write(channels_buffer, bytes_to_write);
}
delay(1);
}
void update_buffer()
{
int n = Serial.available();
if (n < 1)
return;
n -= 1;
if (Serial.read() == n)
{
Serial.read(channels_buffer, n);
bytes_to_write = n;
}
else
{
// incomplete
while (Serial.available())
Serial.read();
}
}
void send_dmx_header()
{
Serial1.flush();
Serial1.begin(90000, SERIAL_8N2);
while (Serial1.available())
Serial1.read();
// send the break as a "slow" byte
Serial1.write(0);
// switch back to the original baud rate
Serial1.flush();
Serial1.begin(250000, SERIAL_8N2);
while (Serial1.available())
Serial1.read();
Serial1.write(0); // Start-Byte
}

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"]
}

Some files were not shown because too many files have changed in this diff Show More