├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── dev.rs └── longstrip.rs └── src ├── connection.rs ├── error.rs ├── lib.rs ├── packet.rs └── protocol ├── id.rs ├── message.rs ├── mod.rs ├── packet_type.rs ├── pixel_config.rs └── timecode.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .vscode/ 4 | .idea/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ddp-rs" 3 | version = "1.0.0" 4 | edition = "2021" 5 | license = "MIT" 6 | keywords = ["distributed", "display", "protocol", "ddp", "pixel"] 7 | categories = ["network-programming", "multimedia"] 8 | description = "Distributed Display Protocol (DDP) in Rust" 9 | repository = "https://github.com/coral/ddp-rs" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | serde = { version = "1.0", features = ["derive"] } 15 | thiserror = "1.0.40" 16 | serde_json = "1.0" 17 | crossbeam = "0.8.2" 18 | dashmap = "5.4.0" 19 | log = "0.4.17" 20 | 21 | [dev-dependencies] 22 | anyhow = "1.0.40" 23 | colorgrad = "0.6.2" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Coral 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Distributed Display Protocol (DDP) in Rust 2 | 3 | This package allows you to write pixel data to a LED strip over [Distributed Display Protocol (DDP)](http://www.3waylabs.com/ddp/) by 3waylabs. 4 | 5 | You can use this to stream pixel data to [WLED](https://github.com/Aircoookie/WLED) or any other DDP capable reciever. 6 | 7 | ## Example 8 | 9 | ```rust 10 | use anyhow::Result; 11 | use ddp_rs::connection; 12 | use ddp_rs::protocol; 13 | 14 | fn main() -> Result<()> { 15 | 16 | let mut conn = connection::DDPConnection::try_new 17 | ( 18 | "192.168.1.40:4048", // The IP address of the device followed by :4048 19 | protocol::PixelConfig::default(), // Default is RGB, 8 bits ber channel 20 | protocol::ID::Default, 21 | std::net::UdpSocket::bind("0.0.0.0:6969") 22 | .unwrap() // can be any unused port on 0.0.0.0, but protocol recommends 4048 23 | )?; 24 | 25 | // loop sets some colors for the first 6 pixels to see if it works 26 | for i in 0u8..100u8{ 27 | let high = 10u8.overflowing_mul(i).0; 28 | 29 | // loop through some colors 30 | 31 | let temp: usize = conn.write(&[ 32 | high/*red value*/, 0/*green value*/, 0/*blue value*/, 33 | high/*red value*/, 0/*green value*/, 0/*blue value*/, 34 | 0/*red value*/, high/*green value*/, 0/*blue value*/, 35 | 0/*red value*/, high/*green value*/, 0/*blue value*/, 36 | 0/*red value*/, 0/*green value*/, high/*blue value*/, 37 | 0/*red value*/, 0/*green value*/, high/*blue value*/ 38 | ])?; 39 | 40 | std::thread::sleep(std::time::Duration::from_millis(10)); 41 | // this crate is non blocking, so with out the sleep, it will send them all instantly 42 | 43 | println!("sent {temp} packets"); 44 | } 45 | 46 | Ok(()) 47 | } 48 | ``` 49 | 50 | or try it by running `cargo run --example dev` 51 | 52 | ## Why? 53 | 54 | I wish I could tell you. I've gone back and forth on these bespoke LED protocols and DDP seems like the most "sane" one although the "specification" leaves some to be desired. [TPM2.net](https://gist.github.com/jblang/89e24e2655be6c463c56) was another possible protocol which [i started to implement](https://github.com/coral/tpm2net) but stopped after I realized how bad it is. Artnet and E1.31 is great but then you have framerate problem (approx 40-44 FPS) to maintain backwards compatbility with DMX. 55 | 56 | DDP sits in the middle here as "sane" but not perfect, hence why I implemented it for whatever it is I'm doing. It doesn't mandate a framerate, it's spec agnostic to if you send it over UDP or TCP (although I suspect most vendors only accept UDP) and it's open ended in that it relies on JSON for messaging. On top of that the author shoved so much data into the 10 byte header it's almost impressive. Only drawback is that clients needs to implement JSON parsing if they want to be smart but that's tablestakes at this point for anything connected. 57 | 58 | For any future "i'm going to invent my own LED protocol" people out there, take note from the people in broadcast video instead of your jank ham radio serial protocol. I like the "freeform pixel struture" but there would probably be value to a more structured "session" where you standardize on how to communicate pixel size etc. 59 | 60 | ## Is it trash? 61 | 62 | Most definitely. I've only tested it with WLED so if you come across some other expensive controller that supports DDP (such as the [Minleon NDB Pro](https://minleonusa.com/product/ndb-pro/)), please try it and let me know. 63 | 64 | ## Contributing 65 | 66 | m8 just open a PR with some gucchimucchi code and I'll review it. 67 | 68 | ![KADSBUGGEL](https://raw.githubusercontent.com/coral/fluidsynth2/master/kadsbuggel.png) 69 | 70 | ## Contributors 71 | 72 | - [coral](https://www.youtube.com/@coral1), main clown of this library 73 | - [paulwrath1223](https://github.com/paulwrath1223) coming in with a [hot potato PR](https://github.com/coral/ddp-rs/pull/1). Absolute legendary PR right there. 74 | -------------------------------------------------------------------------------- /examples/dev.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ddp_rs::connection; 3 | use ddp_rs::protocol; 4 | 5 | fn main() -> Result<()> { 6 | let mut conn = connection::DDPConnection::try_new( 7 | "192.168.1.40:4048", // The IP address of the device followed by :4048 8 | protocol::PixelConfig::default(), // Default is RGB, 8 bits ber channel 9 | protocol::ID::Default, 10 | std::net::UdpSocket::bind("0.0.0.0:4048").unwrap(), // can be any unused port on 0.0.0.0, but protocol recommends 4048 11 | )?; 12 | 13 | // loop sets some colors for the first 6 pixels to see if it works 14 | for i in 0u8..100u8 { 15 | let high = 10u8.overflowing_mul(i).0; 16 | 17 | // loop through some colors 18 | 19 | let temp: usize = conn.write(&vec![ 20 | high, /*red value*/ 21 | 0, /*green value*/ 22 | 0, /*blue value*/ 23 | high, /*red value*/ 24 | 0, /*green value*/ 25 | 0, /*blue value*/ 26 | 0, /*red value*/ 27 | high, /*green value*/ 28 | 0, /*blue value*/ 29 | 0, /*red value*/ 30 | high, /*green value*/ 31 | 0, /*blue value*/ 32 | 0, /*red value*/ 33 | 0, /*green value*/ 34 | high, /*blue value*/ 35 | 0, /*red value*/ 36 | 0, /*green value*/ 37 | high, /*blue value*/ 38 | ])?; 39 | 40 | std::thread::sleep(std::time::Duration::from_millis(10)); 41 | // this crate is non blocking, so with out the sleep, it will send them all instantly 42 | 43 | println!("sent {temp} packets"); 44 | } 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/longstrip.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use colorgrad::Color; 3 | use ddp_rs::connection; 4 | use ddp_rs::protocol; 5 | 6 | // Testing a longer LED strip with offset 7 | 8 | fn main() -> Result<()> { 9 | let mut conn = connection::DDPConnection::try_new( 10 | "10.0.1.184:4048", 11 | protocol::PixelConfig::default(), 12 | protocol::ID::Default, 13 | std::net::UdpSocket::bind("0.0.0.0:4048").unwrap(), 14 | )?; 15 | 16 | let clr = argen(1200)?; 17 | 18 | loop { 19 | conn.write_offset(&clr, 100)?; 20 | std::thread::sleep(std::time::Duration::from_millis(10)); 21 | } 22 | } 23 | 24 | fn argen(length: u32) -> Result> { 25 | let mut vec = Vec::new(); 26 | let g = colorgrad::CustomGradient::new() 27 | .colors(&[ 28 | Color::from_rgba8(255, 0, 0, 255), 29 | Color::from_rgba8(0, 255, 0, 255), 30 | ]) 31 | .build()?; 32 | 33 | for i in 0..length { 34 | let color = g.at(i as f64 / length as f64).to_rgba8(); 35 | vec.push(color[0] as u8); 36 | vec.push(color[1] as u8); 37 | vec.push(color[2] as u8); 38 | } 39 | 40 | Ok(vec) 41 | } 42 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DDPError; 2 | use crate::error::DDPError::CrossBeamError; 3 | use crate::packet::Packet; 4 | use crate::protocol; 5 | use crossbeam::channel::{unbounded, Receiver, TryRecvError}; 6 | use std::net::{SocketAddr, UdpSocket}; 7 | 8 | const MAX_DATA_LENGTH: usize = 480 * 3; 9 | 10 | /// Represents a connection to a DDP display 11 | #[derive(Debug)] 12 | pub struct DDPConnection { 13 | pub pixel_config: protocol::PixelConfig, 14 | pub id: protocol::ID, 15 | 16 | sequence_number: u8, 17 | socket: UdpSocket, 18 | addr: SocketAddr, 19 | 20 | pub receiver_packet: Receiver, 21 | 22 | // Since the buffer is hot path, we can reuse it to avoid allocations per packet 23 | buffer: [u8; 1500], 24 | } 25 | 26 | impl DDPConnection { 27 | /// Writes pixel data to the display 28 | /// 29 | /// You send the data 30 | pub fn write(&mut self, data: &[u8]) -> Result { 31 | let mut h = protocol::Header::default(); 32 | 33 | h.packet_type.push(false); 34 | h.pixel_config = self.pixel_config; 35 | h.id = self.id; 36 | 37 | self.slice_send(&mut h, data) 38 | } 39 | 40 | /// Writes pixel data to the display with offset 41 | /// 42 | /// You send the data with offset 43 | pub fn write_offset(&mut self, data: &[u8], offset: u32) -> Result { 44 | let mut h = protocol::Header::default(); 45 | 46 | h.packet_type.push(false); 47 | h.pixel_config = self.pixel_config; 48 | h.id = self.id; 49 | h.offset = offset; 50 | 51 | self.slice_send(&mut h, data) 52 | } 53 | 54 | /// Allows you to send JSON messages to display 55 | /// This is useful for things like setting the brightness 56 | /// or changing the display mode 57 | /// 58 | /// You provide a Message (either typed or untyped) and it will be sent to the display 59 | pub fn write_message(&mut self, msg: protocol::message::Message) -> Result { 60 | let mut h = protocol::Header::default(); 61 | h.packet_type.push(false); 62 | h.id = msg.get_id(); 63 | let msg_data: Vec = msg.try_into()?; 64 | h.length = msg_data.len() as u16; 65 | 66 | self.slice_send(&mut h, &msg_data) 67 | } 68 | 69 | fn slice_send( 70 | &mut self, 71 | header: &mut protocol::Header, 72 | data: &[u8], 73 | ) -> Result { 74 | let mut offset = header.offset as usize; 75 | let mut sent = 0; 76 | 77 | let num_iterations = (data.len() + MAX_DATA_LENGTH - 1) / MAX_DATA_LENGTH; 78 | let mut iter = 0; 79 | 80 | while offset < data.len() { 81 | iter += 1; 82 | 83 | if iter == num_iterations { 84 | header.packet_type.push(true); 85 | } 86 | 87 | header.sequence_number = self.sequence_number; 88 | 89 | let chunk_end = std::cmp::min(offset + MAX_DATA_LENGTH, data.len()); 90 | let chunk = &data[offset..chunk_end]; 91 | header.length = chunk.len() as u16; 92 | let len = self.assemble_packet(*header, chunk); 93 | 94 | // Send to socket 95 | sent += self.socket.send_to(&self.buffer[0..len], self.addr)?; 96 | 97 | // Increment sequence number 98 | if self.sequence_number > 15 { 99 | self.sequence_number = 1; 100 | } else { 101 | self.sequence_number += 1; 102 | } 103 | offset += MAX_DATA_LENGTH; 104 | header.offset = offset as u32; 105 | } 106 | 107 | Ok(sent) 108 | } 109 | 110 | pub fn get_incoming(&self) -> Result { 111 | match self.receiver_packet.try_recv() { 112 | Ok(packet) => Ok(packet), 113 | Err(TryRecvError::Empty) => Err(DDPError::NothingToReceive), 114 | Err(e2) => Err(CrossBeamError(e2)), 115 | } 116 | } 117 | 118 | pub fn try_new( 119 | addr: A, 120 | pixel_config: protocol::PixelConfig, 121 | id: protocol::ID, 122 | socket: UdpSocket, 123 | ) -> Result 124 | where 125 | A: std::net::ToSocketAddrs, 126 | { 127 | let socket_addr: SocketAddr = addr 128 | .to_socket_addrs()? 129 | .next() 130 | .ok_or(DDPError::NoValidSocketAddr)?; 131 | let (_s, recv) = unbounded(); 132 | 133 | Ok(DDPConnection { 134 | addr: socket_addr, 135 | pixel_config, 136 | id, 137 | socket, 138 | receiver_packet: recv, 139 | sequence_number: 1, 140 | buffer: [0u8; 1500], 141 | }) 142 | } 143 | 144 | // doing this to avoid allocations per frame 145 | // micro optimization, but it's a hot path 146 | // esp running this embedded 147 | #[inline(always)] 148 | fn assemble_packet(&mut self, header: protocol::Header, data: &[u8]) -> usize { 149 | let header_bytes: usize = if header.packet_type.timecode { 150 | let header_bytes: [u8; 14] = header.into(); 151 | self.buffer[0..14].copy_from_slice(&header_bytes); 152 | 14usize 153 | } else { 154 | let header_bytes: [u8; 10] = header.into(); 155 | self.buffer[0..10].copy_from_slice(&header_bytes); 156 | 10usize 157 | }; 158 | self.buffer[header_bytes..(header_bytes + data.len())].copy_from_slice(data); 159 | 160 | header_bytes + data.len() 161 | } 162 | } 163 | 164 | #[cfg(test)] 165 | mod tests { 166 | use super::*; 167 | use crate::protocol::{PixelConfig, ID}; 168 | use crossbeam::channel::unbounded; 169 | use std::thread; 170 | 171 | #[test] 172 | // Test sending to a loopback device 173 | fn test_conn() { 174 | let data_to_send = &vec![255, 0, 0, 255, 0, 0, 255, 0, 0]; 175 | let (s, r) = unbounded(); 176 | 177 | thread::spawn(move || { 178 | let socket = UdpSocket::bind("127.0.0.1:4048").unwrap(); 179 | 180 | let mut buf = [0; 1500]; 181 | let (amt, _) = socket.recv_from(&mut buf).unwrap(); 182 | let buf = &mut buf[..amt]; 183 | 184 | s.send(buf.to_vec()).unwrap(); 185 | }); 186 | 187 | let mut conn = DDPConnection::try_new( 188 | "127.0.0.1:4048", 189 | PixelConfig::default(), 190 | ID::Default, 191 | UdpSocket::bind("0.0.0.0:4049").unwrap(), 192 | ) 193 | .unwrap(); 194 | 195 | // Test simple send 196 | conn.write(data_to_send).unwrap(); 197 | std::thread::sleep(std::time::Duration::from_millis(10)); 198 | let recv_data = r.recv().unwrap(); 199 | assert_eq!( 200 | &vec![ 201 | 0x41, 0x01, 0x0D, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xFF, 0x00, 0x00, 0xFF, 202 | 0x00, 0x00, 0xFF, 0x00, 0x00 203 | ], 204 | &recv_data 205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum DDPError { 5 | #[error("socket error")] 6 | Disconnect(#[from] std::io::Error), 7 | #[error("No valid socket addr found")] 8 | NoValidSocketAddr, 9 | #[error("parse error")] 10 | ParseError(#[from] serde_json::Error), 11 | #[error("invalid sender, did you forget to connect() ( data from {from:?} - {data:?})")] 12 | UnknownClient { 13 | from: std::net::SocketAddr, 14 | data: Vec, 15 | }, 16 | #[error("Invalid packet")] 17 | InvalidPacket, 18 | #[error("There are no packets waiting to be read. This error should be handled explicitly")] 19 | NothingToReceive, 20 | #[error("Error receiving packet: {0}")] 21 | CrossBeamError(#[from] crossbeam::channel::TryRecvError), 22 | } 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | pub mod error; 3 | pub mod packet; 4 | pub mod protocol; 5 | -------------------------------------------------------------------------------- /src/packet.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::{message::Message, Header}; 2 | 3 | #[derive(Debug, PartialEq, Clone)] 4 | /// Packet is our internal representation of a recieved packet 5 | /// Used to parse messages sent back by displays 6 | /// This struct does allocate! 7 | pub struct Packet { 8 | /// The packet header 9 | pub header: Header, 10 | /// Raw data, if you're getting pixels this is the one you want 11 | pub data: Vec, 12 | /// For anything that's messaging, we try to parse it or cast it to string here 13 | pub parsed: Option, 14 | } 15 | 16 | impl Packet { 17 | pub fn from_data(h: Header, d: &[u8]) -> Packet { 18 | Packet { 19 | header: h, 20 | data: d.to_vec(), 21 | parsed: None, 22 | } 23 | } 24 | 25 | pub fn from_bytes(bytes: &[u8]) -> Self { 26 | let header_bytes = &bytes[0..14]; 27 | let header = Header::from(header_bytes); 28 | let mut start_index: usize = 10; 29 | if header.packet_type.timecode { 30 | start_index = 14; 31 | } 32 | let data = &bytes[start_index..]; 33 | 34 | let mut parsed: Option = None; 35 | 36 | if header.packet_type.reply { 37 | // Try to parse the data into typed structs in the spec 38 | parsed = match match header.id { 39 | crate::protocol::ID::Control => match serde_json::from_slice(data) { 40 | Ok(v) => Some(Message::Control(v)), 41 | Err(_) => None, 42 | }, 43 | crate::protocol::ID::Config => match serde_json::from_slice(data) { 44 | Ok(v) => Some(Message::Config(v)), 45 | Err(_) => None, 46 | }, 47 | crate::protocol::ID::Status => match serde_json::from_slice(data) { 48 | Ok(v) => Some(Message::Status(v)), 49 | Err(_) => None, 50 | }, 51 | _ => None, 52 | } { 53 | // Worked, return the typed struct 54 | Some(v) => Some(v), 55 | 56 | // OK, no bueno, lets try just untyped JSON 57 | None => match header.id { 58 | crate::protocol::ID::Control 59 | | crate::protocol::ID::Config 60 | | crate::protocol::ID::Status => match serde_json::from_slice(data) { 61 | // JSON Value it is 62 | Ok(v) => Some(Message::Parsed((header.id, v))), 63 | // Ok we're really screwed, lets just return the raw data as a string 64 | Err(_) => match std::str::from_utf8(data) { 65 | Ok(v) => Some(Message::Unparsed((header.id, v.to_string()))), 66 | // I guess it's... just bytes? 67 | Err(_) => None, 68 | }, 69 | }, 70 | _ => None, 71 | }, 72 | } 73 | } 74 | Packet { 75 | header, 76 | data: data.to_vec(), 77 | parsed, 78 | } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn test_json() { 88 | { 89 | let data = vec![ 90 | 0x44, 0x00, 0x0D, 0xFA, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8E, 0x7b, 0x0a, 0x20, 0x20, 91 | 0x20, 0x20, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x3a, 0x0a, 0x20, 0x20, 92 | 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x67, 93 | 0x77, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x2e, 0x62, 0x2e, 0x63, 0x2e, 0x64, 0x22, 0x2c, 94 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x69, 0x70, 0x22, 0x3a, 95 | 0x20, 0x22, 0x61, 0x2e, 0x62, 0x2e, 0x63, 0x2e, 0x64, 0x22, 0x2c, 0x0a, 0x20, 0x20, 96 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x6d, 0x22, 0x3a, 0x20, 0x22, 0x61, 97 | 0x2e, 0x62, 0x2e, 0x63, 0x2e, 0x64, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 98 | 0x20, 0x20, 0x20, 0x22, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x22, 0x3a, 0x0a, 0x20, 0x20, 99 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 100 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 101 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x22, 0x3a, 102 | 0x20, 0x33, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 103 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x3a, 0x20, 104 | 0x31, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 105 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x73, 0x22, 0x3a, 0x20, 0x34, 0x2c, 0x0a, 106 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 107 | 0x20, 0x20, 0x22, 0x74, 0x73, 0x22, 0x3a, 0x20, 0x32, 0x0a, 0x20, 0x20, 0x20, 0x20, 108 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0a, 0x20, 0x20, 0x20, 109 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 110 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 111 | 0x6c, 0x22, 0x3a, 0x20, 0x37, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 112 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x6f, 0x72, 0x74, 113 | 0x22, 0x3a, 0x20, 0x35, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 114 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x73, 0x22, 0x3a, 0x20, 115 | 0x38, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 116 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x73, 0x22, 0x3a, 0x20, 0x36, 0x0a, 0x20, 117 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 118 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 119 | 0x0a, 0x7d, 120 | ]; 121 | let packet = Packet::from_bytes(&data); 122 | 123 | assert_eq!(packet.header.length, 398); 124 | 125 | match packet.parsed { 126 | Some(p) => match p { 127 | Message::Config(c) => { 128 | assert_eq!(c.config.gw.unwrap(), "a.b.c.d"); 129 | assert_eq!(c.config.nm.unwrap(), "a.b.c.d"); 130 | assert_eq!(c.config.ports.len(), 2); 131 | } 132 | _ => panic!("not the right packet parsed"), 133 | }, 134 | None => panic!("Packet parsing failed"), 135 | } 136 | } 137 | } 138 | 139 | #[test] 140 | fn test_untyped() { 141 | { 142 | let data = vec![ 143 | 0x44, 0x00, 0x0D, 0xFA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x7B, 0x22, 0x68, 0x65, 144 | 0x6C, 0x6C, 0x6F, 0x22, 0x3A, 0x20, 0x22, 0x6F, 0x6B, 0x22, 0x7D, 145 | ]; 146 | let packet = Packet::from_bytes(&data); 147 | 148 | match packet.parsed { 149 | Some(p) => match p { 150 | Message::Parsed((_, p)) => { 151 | assert_eq!(p["hello"], "ok"); 152 | } 153 | _ => panic!("not the right packet parsed"), 154 | }, 155 | None => panic!("Packet parsing failed"), 156 | } 157 | } 158 | } 159 | 160 | #[test] 161 | fn test_unparsed() { 162 | { 163 | let data = vec![ 164 | 0x44, 0x00, 0x0D, 0xFA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x53, 0x4C, 0x49, 0x43, 165 | 0x4B, 0x44, 0x45, 0x4E, 0x49, 0x53, 0x34, 0x30, 0x30, 0x30, 166 | ]; 167 | let packet = Packet::from_bytes(&data); 168 | 169 | match packet.parsed { 170 | Some(p) => match p { 171 | Message::Unparsed((_, p)) => { 172 | assert_eq!(p, "SLICKDENIS4000"); 173 | } 174 | _ => panic!("not the right packet parsed"), 175 | }, 176 | None => panic!("Packet parsing failed"), 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/protocol/id.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Default)] 3 | pub enum ID { 4 | Reserved, 5 | #[default] 6 | Default, 7 | Custom(u8), 8 | Control, 9 | Config, 10 | Status, 11 | DMX, 12 | Broadcast, 13 | } 14 | 15 | impl From for ID { 16 | fn from(value: u8) -> Self { 17 | match value { 18 | 0 => ID::Reserved, 19 | 1 => ID::Default, 20 | 2..=246 => ID::Custom(value), 21 | 249 => ID::Control, 22 | 250 => ID::Config, 23 | 251 => ID::Status, 24 | 254 => ID::DMX, 25 | 255 => ID::Broadcast, 26 | _ => ID::Reserved, 27 | } 28 | } 29 | } 30 | 31 | impl Into for ID { 32 | fn into(self) -> u8 { 33 | match self { 34 | ID::Reserved => 0, 35 | ID::Default => 1, 36 | ID::Custom(value) if (2..=246).contains(&value) => value, 37 | ID::Control => 249, 38 | ID::Config => 250, 39 | ID::Status => 251, 40 | ID::DMX => 254, 41 | ID::Broadcast => 255, 42 | ID::Custom(_) => 1, 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_id_parse() { 53 | // Custom 54 | let id = ID::from(212); 55 | assert_eq!(id, ID::Custom(212)); 56 | 57 | // Default 58 | let id = ID::from(1); 59 | assert_eq!(id, ID::Default); 60 | 61 | // Status 62 | let id = ID::from(251); 63 | assert_eq!(id, ID::Status); 64 | 65 | // Reserved 66 | let id = ID::from(0); 67 | assert_eq!(id, ID::Reserved); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/protocol/message.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::ID; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::Value; 4 | 5 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 6 | pub struct StatusRoot { 7 | pub status: Status, 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 11 | pub struct Status { 12 | pub update: Option, 13 | pub state: Option, 14 | pub man: Option, 15 | #[serde(rename = "mod")] 16 | pub model: Option, 17 | pub ver: Option, 18 | pub mac: Option, 19 | pub push: Option, 20 | pub ntp: Option, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 24 | pub struct ConfigRoot { 25 | pub config: Config, 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 29 | pub struct Config { 30 | pub ip: Option, 31 | pub nm: Option, 32 | pub gw: Option, 33 | pub ports: Vec, 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 37 | pub struct Port { 38 | pub port: u32, 39 | pub ts: u32, 40 | pub l: u32, 41 | pub ss: u32, 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 45 | pub struct ControlRoot { 46 | pub control: Control, 47 | } 48 | 49 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 50 | pub struct Control { 51 | pub fx: Option, 52 | pub int: Option, 53 | pub spd: Option, 54 | pub dir: Option, 55 | pub colors: Option>, 56 | pub save: Option, 57 | pub power: Option, 58 | } 59 | 60 | #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Clone)] 61 | pub struct Color { 62 | pub r: u32, 63 | pub g: u32, 64 | pub b: u32, 65 | } 66 | 67 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 68 | pub enum Message { 69 | Control(ControlRoot), 70 | Status(StatusRoot), 71 | Config(ConfigRoot), 72 | Parsed((ID, Value)), 73 | Unparsed((ID, String)), 74 | } 75 | 76 | impl TryInto> for Message { 77 | type Error = serde_json::Error; 78 | 79 | fn try_into(self) -> Result, Self::Error> { 80 | match self { 81 | Message::Control(c) => serde_json::to_vec(&c), 82 | Message::Status(s) => serde_json::to_vec(&s), 83 | Message::Config(c) => serde_json::to_vec(&c), 84 | Message::Parsed((_, v)) => serde_json::to_vec(&v), 85 | Message::Unparsed((_, s)) => Ok(s.as_bytes().to_vec()), 86 | } 87 | } 88 | } 89 | 90 | impl Message { 91 | pub fn get_id(&self) -> ID { 92 | match self { 93 | Message::Control(_) => ID::Control, 94 | Message::Status(_) => ID::Status, 95 | Message::Config(_) => ID::Config, 96 | Message::Parsed((i, _)) => *i, 97 | Message::Unparsed((i, _)) => *i, 98 | } 99 | } 100 | } 101 | 102 | impl Into for Message { 103 | fn into(self) -> ID { 104 | match self { 105 | Message::Control(_) => crate::protocol::ID::Control, 106 | Message::Status(_) => crate::protocol::ID::Status, 107 | Message::Config(_) => crate::protocol::ID::Config, 108 | Message::Parsed((i, _)) => i, 109 | Message::Unparsed((i, _)) => i, 110 | } 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::*; 117 | 118 | #[test] 119 | fn test_id_into() { 120 | let msg = Message::Parsed((ID::Config, Value::Null)); 121 | let id: ID = msg.get_id(); 122 | assert_eq!(id, ID::Config); 123 | 124 | let vm: Vec = msg.try_into().unwrap(); 125 | assert_eq!(vm, b"null"); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | // http://www.3waylabs.com/ddp/ 2 | 3 | pub mod packet_type; 4 | pub use packet_type::*; 5 | 6 | pub mod pixel_config; 7 | pub use pixel_config::{PixelConfig, PixelFormat}; 8 | 9 | pub mod id; 10 | pub use id::ID; 11 | 12 | pub mod message; 13 | 14 | pub mod timecode; 15 | use timecode::TimeCode; 16 | 17 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Default)] 18 | pub struct Header { 19 | pub packet_type: PacketType, 20 | pub sequence_number: u8, 21 | pub pixel_config: PixelConfig, 22 | pub id: ID, 23 | pub offset: u32, 24 | pub length: u16, 25 | pub time_code: TimeCode, //technically supported, although untested and relies on user to handle 26 | } 27 | 28 | impl Into<[u8; 10]> for Header { 29 | fn into(self) -> [u8; 10] { 30 | // Define a byte array with the size of the header 31 | let mut buffer: [u8; 10] = [0u8; 10]; 32 | 33 | // Write the packet type field to the buffer 34 | 35 | let packet_type_byte: u8 = self.packet_type.into(); 36 | buffer[0] = packet_type_byte; 37 | 38 | // Write the sequence number field to the buffer 39 | buffer[1] = self.sequence_number; 40 | 41 | // Write the pixel config field to the buffer 42 | buffer[2] = self.pixel_config.into(); 43 | 44 | // Write the id field to the buffer 45 | buffer[3] = self.id.into(); 46 | 47 | // Write the offset field to the buffer 48 | let offset_bytes = self.offset.to_be_bytes(); 49 | buffer[4..8].copy_from_slice(&offset_bytes); 50 | 51 | // Write the length field to the buffer 52 | let length_bytes = self.length.to_be_bytes(); 53 | buffer[8..10].copy_from_slice(&length_bytes); 54 | 55 | // Return a slice of the buffer representing the entire header 56 | buffer 57 | } 58 | } 59 | impl Into<[u8; 14]> for Header { 60 | fn into(self) -> [u8; 14] { 61 | // Define a byte array with the size of the header 62 | let mut buffer = [0u8; 14]; 63 | 64 | // Write the packet type field to the buffer 65 | 66 | let packet_type_byte: u8 = self.packet_type.into(); 67 | buffer[0] = packet_type_byte; 68 | 69 | // Write the sequence number field to the buffer 70 | buffer[1] = self.sequence_number; 71 | 72 | // Write the pixel config field to the buffer 73 | buffer[2] = self.pixel_config.into(); 74 | 75 | // Write the id field to the buffer 76 | buffer[3] = self.id.into(); 77 | 78 | // Write the offset field to the buffer 79 | let offset_bytes: [u8; 4] = self.offset.to_be_bytes(); 80 | buffer[4..8].copy_from_slice(&offset_bytes); 81 | 82 | // Write the length field to the buffer 83 | let length_bytes: [u8; 2] = self.length.to_be_bytes(); 84 | buffer[8..10].copy_from_slice(&length_bytes); 85 | 86 | let time_code: [u8; 4] = self.time_code.to_bytes(); 87 | buffer[10..14].copy_from_slice(&time_code); 88 | 89 | // Return a slice of the buffer representing the entire header 90 | buffer 91 | } 92 | } 93 | 94 | impl<'a> From<&'a [u8]> for Header { 95 | fn from(bytes: &'a [u8]) -> Self { 96 | // Extract the packet type field from the buffer 97 | let packet_type = PacketType::from(bytes[0]); 98 | 99 | // Extract the sequence number field from the buffer 100 | let sequence_number = bytes[1]; 101 | 102 | // Extract the pixel config field from the buffer 103 | let pixel_config = PixelConfig::from(bytes[2]); 104 | 105 | // Extract the id field from the buffer 106 | let id = ID::from(bytes[3]); 107 | 108 | // Extract the offset field from the buffer 109 | let offset = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); 110 | 111 | // Extract the length field from the buffer 112 | let length = u16::from_be_bytes([bytes[8], bytes[9]]); 113 | 114 | if packet_type.timecode && bytes.len() >= 14 { 115 | let time_code = TimeCode::from_4_bytes([bytes[10], bytes[11], bytes[12], bytes[13]]); 116 | 117 | Header { 118 | packet_type, 119 | sequence_number, 120 | pixel_config, 121 | id, 122 | offset, 123 | length, 124 | time_code, 125 | } 126 | } else { 127 | Header { 128 | packet_type, 129 | sequence_number, 130 | pixel_config, 131 | id, 132 | offset, 133 | length, 134 | time_code: TimeCode(None), 135 | } 136 | } 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use super::*; 143 | 144 | #[test] 145 | fn test_parsing() { 146 | // Normal 147 | { 148 | let data: [u8; 10] = [65, 6, 10, 1, 0, 0, 0, 0, 0, 3]; 149 | let header = Header::from(&data[..]); 150 | 151 | assert_eq!( 152 | header.packet_type, 153 | PacketType { 154 | version: 1, 155 | timecode: false, 156 | storage: false, 157 | reply: false, 158 | query: false, 159 | push: true 160 | } 161 | ); 162 | assert_eq!(header.sequence_number, 6); 163 | assert_eq!(header.length, 3); 164 | assert_eq!(header.offset, 0); 165 | } 166 | 167 | // oddity 168 | { 169 | let data: [u8; 10] = [255, 12, 13, 1, 0, 0, 0x99, 0xd5, 0x01, 0x19]; 170 | let header = Header::from(&data[..]); 171 | 172 | assert_eq!( 173 | header.packet_type, 174 | PacketType { 175 | version: 3, 176 | timecode: true, 177 | storage: true, 178 | reply: true, 179 | query: true, 180 | push: true 181 | } 182 | ); 183 | 184 | assert_eq!(header.sequence_number, 12); 185 | assert_eq!( 186 | header.pixel_config, 187 | PixelConfig { 188 | data_type: pixel_config::DataType::RGB, 189 | data_size: PixelFormat::Pixel24Bits, 190 | customer_defined: false 191 | } 192 | ); 193 | assert_eq!(header.length, 281); 194 | assert_eq!(header.offset, 39381); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/protocol/packet_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 2 | #[allow(dead_code)] 3 | pub struct PacketType { 4 | pub version: u8, // 0x40 ( 2 bit value, can be a value between 1-4 depending on version mask, ) 5 | pub timecode: bool, // 0x10 6 | pub storage: bool, // 0x08 7 | pub reply: bool, // 0x04 8 | pub query: bool, //0x02 9 | pub push: bool, //0x01 10 | } 11 | 12 | impl PacketType { 13 | pub fn push(&mut self, push: bool) { 14 | self.push = push; 15 | } 16 | } 17 | 18 | const VERSION_MASK: u8 = 0xc0; 19 | //const RESERVED: u8 = 0x20; 20 | const TIMECODE: u8 = 0x10; 21 | const STORAGE: u8 = 0x08; 22 | const REPLY: u8 = 0x04; 23 | const QUERY: u8 = 0x02; 24 | const PUSH: u8 = 0x01; 25 | 26 | impl Default for PacketType { 27 | fn default() -> Self { 28 | Self { 29 | version: 1, 30 | timecode: false, 31 | storage: false, 32 | reply: false, 33 | query: false, 34 | push: false, 35 | } 36 | } 37 | } 38 | 39 | impl From for PacketType { 40 | fn from(byte: u8) -> Self { 41 | let version = match byte & VERSION_MASK { 42 | 0x00 => 0, 43 | 0x40 => 1, 44 | 0x80 => 2, 45 | 0xc0 => 3, 46 | _ => 0, 47 | }; 48 | let timecode = byte & TIMECODE == TIMECODE; 49 | let storage = byte & STORAGE == STORAGE; 50 | let reply = byte & REPLY == REPLY; 51 | let query = byte & QUERY == QUERY; 52 | let push = byte & PUSH == PUSH; 53 | 54 | PacketType { 55 | version, 56 | timecode, 57 | storage, 58 | reply, 59 | query, 60 | push, 61 | } 62 | } 63 | } 64 | 65 | impl Into for PacketType { 66 | fn into(self) -> u8 { 67 | let mut byte: u8 = 0; 68 | let v = match self.version { 69 | 1 => self.version, 70 | 2 => self.version, 71 | 3 => self.version, 72 | 4 => self.version, 73 | _ => 0, 74 | }; 75 | byte |= v << 6; 76 | // Set the flag bits 77 | if self.timecode { 78 | byte |= TIMECODE 79 | }; 80 | if self.storage { 81 | byte |= STORAGE 82 | }; 83 | if self.reply { 84 | byte |= REPLY 85 | }; 86 | if self.query { 87 | byte |= QUERY 88 | }; 89 | if self.push { 90 | byte |= PUSH 91 | }; 92 | 93 | byte 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | 101 | #[test] 102 | fn test_packet_type_parse() { 103 | let byte: u8 = 0b00010110; 104 | let packet_type = PacketType::from(byte); 105 | 106 | assert_eq!( 107 | packet_type, 108 | PacketType { 109 | version: 0, 110 | timecode: true, 111 | storage: false, 112 | reply: true, 113 | query: true, 114 | push: false, 115 | } 116 | ); 117 | } 118 | 119 | #[test] 120 | fn test_packet_type_into_u8() { 121 | let packet_type = PacketType { 122 | version: 1, 123 | timecode: true, 124 | storage: false, 125 | reply: true, 126 | query: true, 127 | push: false, 128 | }; 129 | 130 | let byte: u8 = packet_type.into(); 131 | assert_eq!(byte, 0x56); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/protocol/pixel_config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Copy)] 2 | #[repr(u8)] 3 | #[allow(dead_code)] 4 | pub enum DataType { 5 | Undefined, 6 | RGB, 7 | HSL, 8 | RGBW, 9 | Grayscale, 10 | } 11 | 12 | #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Copy)] 13 | #[repr(u8)] 14 | #[allow(dead_code)] 15 | pub enum PixelFormat { 16 | Undefined, 17 | Pixel1Bits, 18 | Pixel4Bits, 19 | Pixel8Bits, 20 | Pixel16Bits, 21 | Pixel24Bits, 22 | Pixel32Bits, 23 | } 24 | 25 | #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Copy)] 26 | #[allow(dead_code)] 27 | pub struct PixelConfig { 28 | pub data_type: DataType, 29 | pub data_size: PixelFormat, 30 | pub customer_defined: bool, 31 | } 32 | 33 | impl From for PixelConfig { 34 | fn from(byte: u8) -> Self { 35 | let data_type = match (byte >> 3) & 0x07 { 36 | 0 => DataType::Undefined, 37 | 1 => DataType::RGB, 38 | 2 => DataType::HSL, 39 | 3 => DataType::RGBW, 40 | 4 => DataType::Grayscale, 41 | _ => DataType::Undefined, 42 | }; 43 | 44 | let data_size = match (byte & 0x07) as usize { 45 | 0 => PixelFormat::Undefined, 46 | 1 => PixelFormat::Pixel1Bits, 47 | 2 => PixelFormat::Pixel4Bits, 48 | 3 => PixelFormat::Pixel8Bits, 49 | 4 => PixelFormat::Pixel16Bits, 50 | 5 => PixelFormat::Pixel24Bits, 51 | 6 => PixelFormat::Pixel32Bits, 52 | _ => PixelFormat::Undefined, 53 | }; 54 | 55 | let customer_defined = (byte >> 7) != 0; 56 | 57 | PixelConfig { 58 | data_type, 59 | data_size, 60 | customer_defined, 61 | } 62 | } 63 | } 64 | 65 | impl Into for PixelConfig { 66 | fn into(self) -> u8 { 67 | let mut byte = 0u8; 68 | 69 | byte |= match self.data_type { 70 | DataType::Undefined => 0, 71 | DataType::RGB => 1, 72 | DataType::HSL => 2, 73 | DataType::RGBW => 3, 74 | DataType::Grayscale => 4, 75 | } << 3; 76 | 77 | byte |= match self.data_size { 78 | PixelFormat::Undefined => 0, 79 | PixelFormat::Pixel1Bits => 1, 80 | PixelFormat::Pixel4Bits => 2, 81 | PixelFormat::Pixel8Bits => 3, 82 | PixelFormat::Pixel16Bits => 4, 83 | PixelFormat::Pixel24Bits => 5, 84 | PixelFormat::Pixel32Bits => 6, 85 | }; 86 | 87 | if self.customer_defined { 88 | byte |= 0x80; 89 | } 90 | 91 | byte 92 | } 93 | } 94 | 95 | impl Default for PixelConfig { 96 | fn default() -> Self { 97 | Self { 98 | data_type: DataType::RGB, 99 | data_size: PixelFormat::Pixel24Bits, 100 | customer_defined: Default::default(), 101 | } 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use super::*; 108 | 109 | #[test] 110 | fn test_pixel_config_from_u8() { 111 | // WLED oddities 112 | { 113 | let byte = 0x0A; 114 | let pixel_config = PixelConfig::from(byte); 115 | 116 | assert_eq!(pixel_config.data_type, DataType::RGB); 117 | assert_eq!(pixel_config.data_size, PixelFormat::Pixel4Bits); 118 | assert!(!pixel_config.customer_defined); 119 | } 120 | 121 | // RGB 24 122 | { 123 | let byte = 0x0D; 124 | let pixel_config = PixelConfig::from(byte); 125 | 126 | assert_eq!(pixel_config.data_type, DataType::RGB); 127 | assert_eq!(pixel_config.data_size, PixelFormat::Pixel24Bits); 128 | assert!(!pixel_config.customer_defined); 129 | } 130 | 131 | // RGBW 32 132 | { 133 | let byte = 0x1E; 134 | let pixel_config = PixelConfig::from(byte); 135 | 136 | assert_eq!(pixel_config.data_type, DataType::RGBW); 137 | assert_eq!(pixel_config.data_size, PixelFormat::Pixel32Bits); 138 | assert!(!pixel_config.customer_defined); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/protocol/timecode.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Default)] 2 | pub struct TimeCode(pub Option); 3 | 4 | impl TimeCode { 5 | pub fn from_4_bytes(bytes: [u8; 4]) -> Self { 6 | TimeCode(Some(u32::from_be_bytes(bytes))) 7 | } 8 | 9 | pub fn to_bytes(&self) -> [u8; 4] { 10 | self.0.unwrap_or(0u32).to_be_bytes() 11 | } 12 | } 13 | --------------------------------------------------------------------------------