Compare commits
51 Commits
c79abb1be9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
90db3e61f5
|
|||
|
4cb1f6ea4d
|
|||
|
162103e54b
|
|||
|
85ea070632
|
|||
|
2dba389d48
|
|||
|
7720005194
|
|||
|
9eac3510fa
|
|||
|
cac24e64b1
|
|||
|
1de834020a
|
|||
|
60b2b56d8f
|
|||
|
cd16208634
|
|||
|
d85bf03011
|
|||
|
444ca860fa
|
|||
|
0d13c7604f
|
|||
|
6f6e0fd665
|
|||
|
62636fa2f9
|
|||
|
763aa23ca8
|
|||
|
5a71aa2cf0
|
|||
|
30a4f83d32
|
|||
|
9f07172f60
|
|||
|
b0fecde639
|
|||
|
1ebe1d0323
|
|||
|
971e2a61f6
|
|||
|
7ee7779461
|
|||
|
120d8d8c49
|
|||
|
49f306226b
|
|||
|
8b5d55025f
|
|||
|
36eb0bb51c
|
|||
|
8573d30d02
|
|||
|
95ae89cab7
|
|||
|
d7ed085686
|
|||
|
70671f6d54
|
|||
|
f987a876e8
|
|||
|
aee36d4ccf
|
|||
|
fac05243aa
|
|||
|
a2d7bb3211
|
|||
|
07ba32f0ee
|
|||
|
2dedc08636
|
|||
|
44c2604855
|
|||
|
f9e969e6e8
|
|||
|
0260c3bb6b
|
|||
|
4f6666a874
|
|||
|
d76bfcafe8
|
|||
|
503690e9da
|
|||
|
738301d113
|
|||
|
8beaafc456
|
|||
|
34fe7f55d0
|
|||
|
74526fa07d
|
|||
|
406c628810
|
|||
|
3237a9bf6a
|
|||
|
3fea0fe57c
|
5
aruco/.gitattributes
vendored
Normal file
5
aruco/.gitattributes
vendored
Normal 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
2
aruco/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
venv
|
||||
.ipynb_checkpoints
|
||||
125
aruco/angle_measure.py
Normal file
125
aruco/angle_measure.py
Normal 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
1390
aruco/aruco.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
BIN
aruco/formula.png
Normal file
BIN
aruco/formula.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
65
aruco/requirements.txt
Normal file
65
aruco/requirements.txt
Normal 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
BIN
aruco/rotate_180.mp4
LFS
Normal file
Binary file not shown.
BIN
aruco/rotate_360.mp4
LFS
Normal file
BIN
aruco/rotate_360.mp4
LFS
Normal file
Binary file not shown.
BIN
aruco/rotate_540.mp4
LFS
Normal file
BIN
aruco/rotate_540.mp4
LFS
Normal file
Binary file not shown.
BIN
aruco/rotate_90.mp4
LFS
Normal file
BIN
aruco/rotate_90.mp4
LFS
Normal file
Binary file not shown.
BIN
aruco/tilt_180.mp4
LFS
Normal file
BIN
aruco/tilt_180.mp4
LFS
Normal file
Binary file not shown.
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target
|
||||
1203
backend/Cargo.lock
generated
Normal file
1203
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
backend/Cargo.toml
Normal file
12
backend/Cargo.toml
Normal 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
108
backend/src/fixtures.rs
Normal 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
141
backend/src/main.rs
Normal 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
14
beat_detection/.gitignore
vendored
Normal 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
17
beat_detection/Cargo.toml
Normal 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"
|
||||
95
beat_detection/src/capture.rs
Normal file
95
beat_detection/src/capture.rs
Normal 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)
|
||||
}
|
||||
113
beat_detection/src/dmx_controller.rs
Normal file
113
beat_detection/src/dmx_controller.rs
Normal 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
71
beat_detection/src/dsp.rs
Normal 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]
|
||||
}
|
||||
}
|
||||
53
beat_detection/src/fixtures.rs
Normal file
53
beat_detection/src/fixtures.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
288
beat_detection/src/layers/beat_detector.rs
Normal file
288
beat_detection/src/layers/beat_detector.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
beat_detection/src/layers/constant_brightness.rs
Normal file
24
beat_detection/src/layers/constant_brightness.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
beat_detection/src/layers/hsl_cycle.rs
Normal file
53
beat_detection/src/layers/hsl_cycle.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
beat_detection/src/layers/mod.rs
Normal file
21
beat_detection/src/layers/mod.rs
Normal 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]);
|
||||
}
|
||||
78
beat_detection/src/layers/movement.rs
Normal file
78
beat_detection/src/layers/movement.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
102
beat_detection/src/layers/tap.rs
Normal file
102
beat_detection/src/layers/tap.rs
Normal 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
183
beat_detection/src/main.rs
Normal 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(())
|
||||
}
|
||||
8
beat_detection/src/util.rs
Normal file
8
beat_detection/src/util.rs
Normal 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)
|
||||
}
|
||||
764
frontend/assets/movinghead.obj
Normal file
764
frontend/assets/movinghead.obj
Normal 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
BIN
frontend/movinghead.blend
Normal file
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
|
||||
|
||||
import * as Stats from 'stats.js';
|
||||
|
||||
const canvas = document.getElementById('threejs-preview')! as HTMLCanvasElement;
|
||||
@@ -11,16 +10,25 @@ const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x161618);
|
||||
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);
|
||||
camera.position.set(400, 200, 0);
|
||||
|
||||
// controls
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = false;
|
||||
// controls.maxPolarAngle = Math.PI / 2;
|
||||
|
||||
// dbg
|
||||
const cylinder = new THREE.CylinderGeometry(0, 10, 30, 4, 1);
|
||||
|
||||
10
frontend/untitled.mtl
Normal file
10
frontend/untitled.mtl
Normal 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
1
guitarhero/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
venv
|
||||
57
guitarhero/fixtures.py
Normal file
57
guitarhero/fixtures.py
Normal 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
139
guitarhero/main.py
Normal 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;
|
||||
2
guitarhero/requirements.txt
Normal file
2
guitarhero/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pygame==2.0.1
|
||||
pyserial==3.5
|
||||
11
guitarhero/util.py
Normal file
11
guitarhero/util.py
Normal 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
1
libpulse_aubio/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target
|
||||
204
libpulse_aubio/Cargo.lock
generated
Normal file
204
libpulse_aubio/Cargo.lock
generated
Normal 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
12
libpulse_aubio/Cargo.toml
Normal 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
142
libpulse_aubio/src/main.rs
Normal 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
5
microcontroller/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.vscode
|
||||
.mypy_cache
|
||||
venv
|
||||
build
|
||||
__pycache__
|
||||
11
microcontroller/README.md
Normal file
11
microcontroller/README.md
Normal 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
|
||||

|
||||
|
||||
## Stripboard Setup
|
||||

|
||||
BIN
microcontroller/assets/breadboard.fzz
Normal file
BIN
microcontroller/assets/breadboard.fzz
Normal file
Binary file not shown.
BIN
microcontroller/assets/breadboard.png
Normal file
BIN
microcontroller/assets/breadboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
BIN
microcontroller/assets/stripboard.fzz
Normal file
BIN
microcontroller/assets/stripboard.fzz
Normal file
Binary file not shown.
BIN
microcontroller/assets/stripboard.png
Normal file
BIN
microcontroller/assets/stripboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
@@ -1,24 +1,76 @@
|
||||
import serial
|
||||
import time
|
||||
import colorsys
|
||||
|
||||
import sys
|
||||
|
||||
channels = [
|
||||
192, # pan
|
||||
0, # tilt
|
||||
134, # dimmer
|
||||
255, # R
|
||||
255, # G
|
||||
255, # B
|
||||
0, # W
|
||||
1, # movement speed
|
||||
0, # RST
|
||||
255, # dimmer
|
||||
0, # R
|
||||
0, # G
|
||||
0, # B
|
||||
0, # W
|
||||
0, # A
|
||||
0, # UV
|
||||
0, # Strobe
|
||||
0, # function
|
||||
0, # function speed
|
||||
]
|
||||
|
||||
ser = serial.Serial("/dev/ttyUSB0", 115200)
|
||||
start_addr = 1
|
||||
|
||||
payload = bytearray()
|
||||
payload.extend(5 * [0])
|
||||
payload.extend(channels)
|
||||
with serial.Serial("/dev/ttyUSB0", 500000) as ser:
|
||||
|
||||
ser.write(payload)
|
||||
ser.flush()
|
||||
payload = bytearray(512)
|
||||
|
||||
print(ser.read_all())
|
||||
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.")
|
||||
|
||||
while True:
|
||||
|
||||
loop_start = time.time()
|
||||
|
||||
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)
|
||||
|
||||
payload[(start_addr - 1) : (start_addr - 1 + len(channels))] = channels
|
||||
|
||||
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())
|
||||
|
||||
96
microcontroller/e131/main.ino
Normal file
96
microcontroller/e131/main.ino
Normal 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);
|
||||
}
|
||||
}
|
||||
69
microcontroller/hotreload.py
Normal file
69
microcontroller/hotreload.py
Normal 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
29
microcontroller/scene.py
Normal 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
|
||||
@@ -1,54 +1,162 @@
|
||||
unsigned long tic_loop = 0;
|
||||
const unsigned int FRAME_TIME = 25; // 20 ms -> 50 FPS
|
||||
/*
|
||||
Triple buffering data structure
|
||||
|
||||
byte channels_buffer[512] = {0};
|
||||
byte read_buffer[9] = {0};
|
||||
size_t bytes_read = 0;
|
||||
Example writing:
|
||||
----------------
|
||||
|
||||
const unsigned int START_ADDR = 10;
|
||||
const unsigned int NUM_CHANNELS = 9;
|
||||
DMXTripleBuffer buffer;
|
||||
|
||||
bytes_written = producer.write(
|
||||
buffer.fill_buffer + buffer.fill_pos, // destination pointer
|
||||
512 - buffer.fill_pos // maximum allowed
|
||||
);
|
||||
|
||||
unsigned int bytes_to_write = START_ADDR + NUM_CHANNELS;
|
||||
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;
|
||||
const unsigned long SYNC_TIMEOUT = 1000;
|
||||
|
||||
/*
|
||||
setup
|
||||
|
||||
initialize both Serial connections and write the initial "Sync."
|
||||
*/
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200); // USB
|
||||
|
||||
while (!Serial.available());
|
||||
Serial.println();
|
||||
Serial.println("INIT");
|
||||
Serial.begin(500000); // USB
|
||||
Serial.setRxBufferSize(512); // to fit the full DMX packet. (default is 128)
|
||||
while (!Serial)
|
||||
{
|
||||
// spin until serial is up
|
||||
}
|
||||
|
||||
Serial1.begin(250000, SERIAL_8N2); // DMX
|
||||
tic_loop = millis();
|
||||
while (!Serial1)
|
||||
{
|
||||
// spin until serial1 is up
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
Serial.println("Sync.");
|
||||
time_since_last_sync = millis();
|
||||
}
|
||||
|
||||
/*
|
||||
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()
|
||||
{
|
||||
update_buffer();
|
||||
|
||||
// this section gets executed at a maximum rate of around 40Hz
|
||||
if ((millis() - tic_loop) > FRAME_TIME)
|
||||
// output
|
||||
|
||||
if (packet_ready)
|
||||
{
|
||||
tic_loop = millis();
|
||||
|
||||
send_dmx_header();
|
||||
Serial1.write(channels_buffer, bytes_to_write);
|
||||
packet_ready = false;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
size_t n = Serial1.availableForWrite();
|
||||
|
||||
void update_buffer()
|
||||
{
|
||||
if (!Serial.available()) return;
|
||||
Serial.read(read_buffer + bytes_read, 1);
|
||||
Serial.print(".");
|
||||
size_t written = Serial1.write(
|
||||
buffer.drain_buffer->data + buffer.drain_pos,
|
||||
std::min(n, 512 - buffer.drain_pos));
|
||||
|
||||
bytes_read += 1;
|
||||
if (bytes_read == NUM_CHANNELS) {
|
||||
bytes_read = 0;
|
||||
memcpy(channels_buffer + START_ADDR - 1, read_buffer, NUM_CHANNELS);
|
||||
Serial.println();
|
||||
Serial.println("Updated.");
|
||||
buffer.drain_pos += written;
|
||||
|
||||
if (buffer.drain_pos == 512)
|
||||
{
|
||||
buffer.on_drain_complete();
|
||||
packet_ready = true;
|
||||
}
|
||||
|
||||
// input
|
||||
|
||||
n = Serial.available();
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
if (!n)
|
||||
{
|
||||
if (now - time_since_last_sync > 1000)
|
||||
{
|
||||
buffer.fill_pos = 0;
|
||||
Serial.println("Sync.");
|
||||
time_since_last_sync = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
time_since_last_sync = now;
|
||||
|
||||
size_t read = Serial.read(
|
||||
buffer.fill_buffer->data + buffer.fill_pos,
|
||||
std::min(n, 512 - buffer.fill_pos));
|
||||
|
||||
buffer.fill_pos += read;
|
||||
|
||||
if (buffer.fill_pos == 512)
|
||||
{
|
||||
buffer.on_fill_complete();
|
||||
Serial.println("Ack.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,15 +164,10 @@ 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
|
||||
}
|
||||
@@ -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
153
pult/backend/.gitignore
vendored
Normal 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
1
pult/backend/frontend
Symbolic link
@@ -0,0 +1 @@
|
||||
../frontend/build
|
||||
239
pult/backend/main.py
Normal file
239
pult/backend/main.py
Normal 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()
|
||||
11
pult/backend/requirements.txt
Normal file
11
pult/backend/requirements.txt
Normal 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
23
pult/frontend/.gitignore
vendored
Normal 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
46
pult/frontend/README.md
Normal 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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
28331
pult/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
pult/frontend/package.json
Normal file
48
pult/frontend/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
22
pult/frontend/public/index.html
Normal file
22
pult/frontend/public/index.html
Normal 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>
|
||||
3
pult/frontend/public/robots.txt
Normal file
3
pult/frontend/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
107
pult/frontend/src/App.tsx
Normal file
107
pult/frontend/src/App.tsx
Normal 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;
|
||||
120
pult/frontend/src/Sliders.tsx
Normal file
120
pult/frontend/src/Sliders.tsx
Normal 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;
|
||||
15
pult/frontend/src/index.tsx
Normal file
15
pult/frontend/src/index.tsx
Normal 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
1
pult/frontend/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
26
pult/frontend/tsconfig.json
Normal file
26
pult/frontend/tsconfig.json
Normal 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
3
sphere_movement/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
venv
|
||||
.vscode
|
||||
.ipynb_checkpoints
|
||||
100
sphere_movement/slerp.ipynb
Normal file
100
sphere_movement/slerp.ipynb
Normal file
File diff suppressed because one or more lines are too long
24
webserial/.gitignore
vendored
Normal file
24
webserial/.gitignore
vendored
Normal 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
3
webserial/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
||||
47
webserial/README.md
Normal file
47
webserial/README.md
Normal 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
12
webserial/index.html
Normal 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
2448
webserial/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
webserial/package.json
Normal file
25
webserial/package.json
Normal 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
28
webserial/src/App.svelte
Normal 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
9
webserial/src/app.scss
Normal 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";
|
||||
7
webserial/src/dmx/Debug.svelte
Normal file
7
webserial/src/dmx/Debug.svelte
Normal 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>
|
||||
38
webserial/src/dmx/fixtures/par.svelte
Normal file
38
webserial/src/dmx/fixtures/par.svelte
Normal 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>
|
||||
3
webserial/src/dmx/store.ts
Normal file
3
webserial/src/dmx/store.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const dmxData = writable(new Uint8Array(512));
|
||||
75
webserial/src/editor/Monaco.svelte
Normal file
75
webserial/src/editor/Monaco.svelte
Normal 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>
|
||||
9
webserial/src/editor/code.ts
Normal file
9
webserial/src/editor/code.ts
Normal 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))
|
||||
166
webserial/src/editor/defaultCode.ts
Normal file
166
webserial/src/editor/defaultCode.ts
Normal 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
41
webserial/src/editor/defaultEnv.d.ts
vendored
Normal 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;
|
||||
49
webserial/src/eval/EvalLoop.svelte
Normal file
49
webserial/src/eval/EvalLoop.svelte
Normal 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
42
webserial/src/eval/lib.ts
Normal 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,
|
||||
}
|
||||
7
webserial/src/eval/tsOptions.ts
Normal file
7
webserial/src/eval/tsOptions.ts
Normal 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
8
webserial/src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import App from './App.svelte'
|
||||
import "./app.scss";
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
|
||||
export default app
|
||||
133
webserial/src/serial/SerialConnection.svelte
Normal file
133
webserial/src/serial/SerialConnection.svelte
Normal 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>
|
||||
83
webserial/src/serial/SerialManager.svelte
Normal file
83
webserial/src/serial/SerialManager.svelte
Normal 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 🚀</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 😢
|
||||
</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
2
webserial/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
7
webserial/svelte.config.js
Normal file
7
webserial/svelte.config.js
Normal 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
20
webserial/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
8
webserial/tsconfig.node.json
Normal file
8
webserial/tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
8
webserial/vite.config.ts
Normal file
8
webserial/vite.config.ts
Normal 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
24
webui/.gitignore
vendored
Normal 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?
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user