diff --git a/Cargo.lock b/Cargo.lock index ff65df9..90c56e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,12 +84,27 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + [[package]] name = "bzip2" version = "0.4.4" @@ -193,6 +208,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -202,6 +226,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csbindgen" version = "1.9.3" @@ -212,6 +246,22 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -244,12 +294,28 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -279,6 +345,23 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + [[package]] name = "indexmap" version = "2.6.0" @@ -536,6 +619,7 @@ dependencies = [ "log", "rand", "rust-lzma", + "tungstenite", "zstd", ] @@ -556,6 +640,17 @@ dependencies = [ "servicepoint_binding_c", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -598,6 +693,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.19" @@ -632,12 +747,42 @@ dependencies = [ "winnow", ] +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" @@ -650,6 +795,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/crates/servicepoint/Cargo.toml b/crates/servicepoint/Cargo.toml index 515bb55..09b9994 100644 --- a/crates/servicepoint/Cargo.toml +++ b/crates/servicepoint/Cargo.toml @@ -20,6 +20,7 @@ bzip2 = { version = "0.4", optional = true } zstd = { version = "0.13", optional = true } rust-lzma = { version = "0.6.0", optional = true } rand = { version = "0.8", optional = true } +tungstenite = { version = "0.24.0", optional = true } [features] default = ["compression_lzma", "protocol_udp"] @@ -30,6 +31,7 @@ compression_zstd = ["dep:zstd"] all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"] rand = ["dep:rand"] protocol_udp = [] +protocol_websocket = ["dep:tungstenite"] [[example]] name = "random_brightness" diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index 76b3389..dd2706a 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -1,12 +1,5 @@ use std::fmt::Debug; -use log::debug; - -#[cfg(feature = "protocol_udp")] -use log::info; -#[cfg(feature = "protocol_udp")] -use std::net::{ToSocketAddrs, UdpSocket}; - use crate::packet::Packet; /// A connection to the display. @@ -15,22 +8,54 @@ use crate::packet::Packet; /// /// # Examples /// ```rust -/// let connection = servicepoint::Connection::open("172.23.42.29:2342") +/// let connection = servicepoint::Connection::open("127.0.0.1:2342") /// .expect("connection failed"); /// connection.send(servicepoint::Command::Clear) /// .expect("send failed"); /// ``` +#[derive(Debug)] pub enum Connection { - /// A real connection using the UDP protocol + /// A connection using the UDP protocol. + /// + /// Use this when sending commands directly to the display. + /// + /// Requires the feature "protocol_udp" which is enabled by default. #[cfg(feature = "protocol_udp")] - Udp(UdpSocket), + Udp(std::net::UdpSocket), + + /// A connection using the WebSocket protocol. + /// + /// Note that you will need to forward the WebSocket messages via UDP to the display. + /// You can use [servicepoint-websocket-relay] for this. + /// + /// To create a new WebSocket automatically, use [Connection::open_websocket]. + /// + /// Requires the feature "protocol_websocket" which is disabled by default. + /// + /// [servicepoint-websocket-relay]: https://github.com/kaesaecracker/servicepoint-websocket-relay + #[cfg(feature = "protocol_websocket")] + WebSocket( + tungstenite::WebSocket< + tungstenite::stream::MaybeTlsStream, + >, + ), + /// A fake connection for testing that does not actually send anything. + /// + /// This variant allows immutable send. Fake, + + /// A fake connection for testing that does not actually send anything. + /// + /// This variant does not allow immutable send. + FakeMutableSend, } #[derive(Debug)] pub enum SendError { IoError(std::io::Error), + #[cfg(feature = "protocol_websocket")] + WebsocketError(tungstenite::Error), } impl Connection { @@ -38,42 +63,85 @@ impl Connection { /// /// Note that this is UDP, which means that the open call can succeed even if the display is unreachable. /// + /// The address of the display in CCCB is `172.23.42.29:2342`. + /// /// # Errors /// /// Any errors resulting from binding the udp socket. /// /// # Examples /// ```rust - /// let connection = servicepoint::Connection::open("172.23.42.29:2342") + /// let connection = servicepoint::Connection::open("127.0.0.1:2342") /// .expect("connection failed"); /// ``` #[cfg(feature = "protocol_udp")] - pub fn open(addr: impl ToSocketAddrs + Debug) -> std::io::Result { - info!("connecting to {addr:?}"); - let socket = UdpSocket::bind("0.0.0.0:0")?; + pub fn open( + addr: impl std::net::ToSocketAddrs + Debug, + ) -> std::io::Result { + log::info!("connecting to {addr:?}"); + let socket = std::net::UdpSocket::bind("0.0.0.0:0")?; socket.connect(addr)?; Ok(Self::Udp(socket)) } + /// Open a new WebSocket and connect to the provided host. + /// + /// Requires the feature "protocol_websocket" which is disabled by default. + /// + /// # Examples + /// + /// ```rust + /// use tungstenite::http::Uri; + /// use servicepoint::{Command, Connection}; + /// let uri = "ws://localhost:8080".parse().unwrap(); + /// let mut connection = Connection::open_websocket(uri) + /// .expect("could not connect"); + /// connection.send_mut(Command::Clear) + /// .expect("send failed"); + /// ``` + #[cfg(feature = "protocol_websocket")] + pub fn open_websocket( + uri: tungstenite::http::Uri, + ) -> tungstenite::Result { + use tungstenite::{ + client::IntoClientRequest, connect, ClientRequestBuilder, + }; + + log::info!("connecting to {uri:?}"); + + let request = ClientRequestBuilder::new(uri).into_client_request()?; + let (sock, _) = connect(request)?; + + Ok(Self::WebSocket(sock)) + } + /// Send something packet-like to the display. Usually this is in the form of a Command. /// + /// This variant can only be used for connections that support immutable send, e.g. [Connection::Udp]. + /// + /// If you want to be able to switch the protocol, you should use [Self::send_mut] instead. + /// /// # Arguments /// /// - `packet`: the packet-like to send /// /// returns: true if packet was sent, otherwise false /// + /// # Panics + /// + /// If the connection does not support immutable send, e.g. for [Connection::WebSocket]. + /// /// # Examples /// /// ```rust - /// # let connection = servicepoint::Connection::Fake; + /// let connection = servicepoint::Connection::Fake; /// // turn off all pixels on display /// connection.send(servicepoint::Command::Clear) /// .expect("send failed"); /// ``` pub fn send(&self, packet: impl Into) -> Result<(), SendError> { let packet = packet.into(); - debug!("sending {packet:?}"); + log::debug!("sending {packet:?}"); let data: Vec = packet.into(); match self { #[cfg(feature = "protocol_udp")] @@ -87,6 +155,55 @@ impl Connection { let _ = data; Ok(()) } + #[allow(unreachable_patterns)] // depends on features + _ => { + panic!("Connection {:?} does not support immutable send", self) + } + } + } + + /// Send something packet-like to the display. Usually this is in the form of a Command. + /// + /// This variant has to be used for connections that do not support immutable send, e.g. [Connection::WebSocket]. + /// + /// If you want to be able to switch the protocol, you should use this variant. + /// + /// # Arguments + /// + /// - `packet`: the packet-like to send + /// + /// returns: true if packet was sent, otherwise false + /// + /// # Examples + /// + /// ```rust + /// let mut connection = servicepoint::Connection::FakeMutableSend; + /// // turn off all pixels on display + /// connection.send_mut(servicepoint::Command::Clear) + /// .expect("send failed"); + /// ``` + pub fn send_mut( + &mut self, + packet: impl Into, + ) -> Result<(), SendError> { + match self { + #[cfg(feature = "protocol_websocket")] + Connection::WebSocket(socket) => { + let packet = packet.into(); + log::debug!("sending {packet:?}"); + let data: Vec = packet.into(); + socket + .send(tungstenite::Message::Binary(data)) + .map_err(SendError::WebsocketError) + } + Connection::FakeMutableSend => { + let packet = packet.into(); + log::debug!("sending {packet:?}"); + let data: Vec = packet.into(); + let _ = data; + Ok(()) + } + _ => self.send(packet), } } } @@ -102,4 +219,19 @@ mod tests { let packet = Packet::try_from(data).unwrap(); Connection::Fake.send(packet).unwrap() } + + #[test] + fn send_fake_mutable() { + let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let packet = Packet::try_from(data).unwrap(); + Connection::FakeMutableSend.send_mut(packet).unwrap() + } + + #[test] + #[should_panic] + fn send_fake_mutable_panic() { + let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let packet = Packet::try_from(data).unwrap(); + Connection::FakeMutableSend.send(packet).unwrap() + } }