feat: parse DMX on ESP32, relay colors via OSC on host

This commit is contained in:
Luca 2025-06-08 01:34:54 +02:00
parent c6bb7aa1e3
commit 1138424c04
8 changed files with 1269 additions and 41 deletions

118
Cargo.lock generated
View File

@ -64,6 +64,12 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -132,11 +138,21 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "dmxusb"
version = "0.1.0"
dependencies = [
"clap",
"rosc",
"serialport",
]
@ -162,6 +178,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.172"
@ -197,6 +219,18 @@ dependencies = [
"libc",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nix"
version = "0.26.4"
@ -208,6 +242,22 @@ dependencies = [
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
@ -220,6 +270,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.95"
@ -238,12 +294,43 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rosc"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd37602e1513794e952274082d074e8d31aa7f64d047e3acb746c91db40600a5"
dependencies = [
"byteorder",
"nom",
"time",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serialport"
version = "4.7.2"
@ -300,6 +387,37 @@ dependencies = [
"syn",
]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "unescaper"
version = "0.1.6"

View File

@ -5,4 +5,5 @@ edition = "2021"
[dependencies]
clap = { version = "4.5.39", features = ["derive"] }
rosc = "0.11.4"
serialport = "4.7.2"

View File

@ -1,28 +1,37 @@
use clap::Parser;
use serialport::{DataBits, FlowControl, Parity, SerialPort, StopBits};
use rosc::encoder;
use rosc::{OscMessage, OscPacket, OscType};
use std::io::{self, ErrorKind};
use std::io::{self, Write};
use std::net::{IpAddr, Ipv6Addr, TcpListener};
use std::thread;
use std::time::Duration;
const BAUD_RATE: u32 = 250000;
const BAUD_RATE: u32 = 115_200;
const MAX_SLOTS: usize = 513;
const MICROS_PER_SLOT: u64 = 44;
const BREAK_MICROS: u64 = 88;
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, conflicts_with = "port")]
list_ports: bool,
#[arg(short = 'h', long, default_value_t = IpAddr::V6(Ipv6Addr::LOCALHOST), conflicts_with = "list_ports")]
osc_host: IpAddr,
#[arg(
short = 'p',
long,
default_value_t = 9996,
conflicts_with = "list_ports"
)]
osc_port: u16,
#[arg(required_unless_present = "list_ports")]
port: Option<String>,
}
type Packet = [u8; MAX_SLOTS];
fn main() {
let args = Args::parse();
@ -34,48 +43,60 @@ fn main() {
return;
}
let listener = TcpListener::bind((args.osc_host, args.osc_port)).unwrap();
let (mut osc, _) = listener.accept().unwrap();
let mut serial = serialport::new(args.port.unwrap(), BAUD_RATE)
.data_bits(DataBits::Eight)
.flow_control(FlowControl::None)
.parity(Parity::None)
.stop_bits(StopBits::Two)
.timeout(Duration::from_micros(MICROS_PER_SLOT))
.open()
.unwrap();
// ignore first (possibly incomplete) packet
let _ = read_packet(&mut serial).unwrap();
println!("{:#?}", read_packet(&mut serial).unwrap());
}
fn read_packet(serial: &mut Box<dyn SerialPort>) -> Result<Packet, io::Error> {
let mut buf = [0; MAX_SLOTS];
let mut pos = 0;
let mut color = [0; 3];
let mut strobe = false;
loop {
match serial.read(&mut buf[pos..]) {
Ok(bytes_read) => {
pos += bytes_read;
if pos == MAX_SLOTS {
break;
match serial.read_exact(&mut color) {
Ok(()) => {
if color[0] == 0 && color[1] == 0 && color[2] == 0 {
continue;
}
continue;
}
Err(err) if err.kind() == ErrorKind::Interrupted => continue,
Err(err) if err.kind() == ErrorKind::TimedOut => {
if pos > 0 {
break;
}
println!("{:#?}", color);
thread::sleep(Duration::from_micros(MICROS_PER_SLOT));
continue;
let _ = send_value(&mut osc, "dmx_red".into(), color[0] as f32 / 255.0);
let _ = send_value(&mut osc, "dmx_green".into(), color[1] as f32 / 255.0);
let _ = send_value(&mut osc, "dmx_blue".into(), color[2] as f32 / 255.0);
if color[0] == 255 && color[1] == 255 && color[2] == 255 {
if !strobe {
let _ = send_value(&mut osc, "dmx_strobe".into(), true);
}
strobe = true;
} else {
if strobe {
let _ = send_value(&mut osc, "dmx_strobe".into(), false);
}
strobe = false;
}
}
Err(_) => {
thread::sleep(Duration::from_micros(BREAK_MICROS));
}
Err(err) => return Err(err),
}
}
Ok(buf)
}
fn send_value<W, T>(w: &mut W, addr: String, v: T) -> io::Result<()>
where
W: Write,
T: Into<OscType>,
{
let buf = encoder::encode_tcp(&OscPacket::Message(OscMessage {
addr,
args: vec![v.into()],
}))
.unwrap();
w.write_all(&buf[..])?;
w.flush()
}

View File

@ -0,0 +1,8 @@
[build]
target = "riscv32imc-unknown-none-elf"
[target.riscv32imc-unknown-none-elf]
runner = "espflash flash"
rustflags = [
"-C", "link-arg=-Tlinkall.x",
]

1
uart-relay/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1004
uart-relay/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
uart-relay/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "uart-relay"
version = "0.1.0"
edition = "2021"
[dependencies]
esp-hal = { version = "0.23.1", features = ["esp32c3"] }
panic-halt = "1.0.0"

67
uart-relay/src/main.rs Normal file
View File

@ -0,0 +1,67 @@
#![no_std]
#![no_main]
use esp_hal::uart::{self, DataBits, Parity, StopBits, UartRx};
use esp_hal::usb_serial_jtag::UsbSerialJtag;
use esp_hal::{main, Async};
use panic_halt as _;
const BAUD_RATE: u32 = 250_000;
const MAX_SLOTS: usize = 513;
const START_CODE: u8 = 0;
type Packet = [u8; MAX_SLOTS];
#[main]
fn main() -> ! {
let peripherals = esp_hal::init(esp_hal::Config::default());
let mut rx = UartRx::new(
peripherals.UART0,
uart::Config::default()
.with_baudrate(BAUD_RATE)
.with_data_bits(DataBits::_8)
.with_parity(Parity::None)
.with_stop_bits(StopBits::_2)
.with_rx_timeout(1), // slots
)
.unwrap()
.with_rx(peripherals.GPIO3)
.into_async();
let mut usb_serial = UsbSerialJtag::new(peripherals.USB_DEVICE);
// ignore first (possibly incomplete) packet
let _ = read_packet(&mut rx).unwrap();
loop {
if let Ok(packet) = read_packet(&mut rx) {
if packet[0] == START_CODE {
let _ = usb_serial.write_bytes(&packet[1..4]);
}
}
}
}
fn read_packet(rx: &mut UartRx<'_, Async>) -> Result<Packet, uart::Error> {
let mut buf = [0; MAX_SLOTS];
let mut pos = 0;
loop {
match rx.read_buffered_bytes(&mut buf[pos..]) {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
pos += bytes_read;
}
Err(err) => return Err(err),
}
}
Ok(buf)
}