add websocket binary message protocol

This commit is contained in:
Vinzenz Schroeter 2024-10-12 16:18:27 +02:00
parent f434b5bf83
commit c7764c49e1
3 changed files with 301 additions and 16 deletions

151
Cargo.lock generated
View file

@ -84,12 +84,27 @@ dependencies = [
"wyz", "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]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
[[package]] [[package]]
name = "bzip2" name = "bzip2"
version = "0.4.4" version = "0.4.4"
@ -193,6 +208,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "cpufeatures"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -202,6 +226,16 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "csbindgen" name = "csbindgen"
version = "1.9.3" version = "1.9.3"
@ -212,6 +246,22 @@ dependencies = [
"syn", "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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -244,12 +294,28 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -279,6 +345,23 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.6.0" version = "2.6.0"
@ -536,6 +619,7 @@ dependencies = [
"log", "log",
"rand", "rand",
"rust-lzma", "rust-lzma",
"tungstenite",
"zstd", "zstd",
] ]
@ -556,6 +640,17 @@ dependencies = [
"servicepoint_binding_c", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -598,6 +693,26 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"
@ -632,12 +747,42 @@ dependencies = [
"winnow", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@ -650,6 +795,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

@ -20,6 +20,7 @@ bzip2 = { version = "0.4", optional = true }
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }
rust-lzma = { version = "0.6.0", optional = true } rust-lzma = { version = "0.6.0", optional = true }
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
tungstenite = { version = "0.24.0", optional = true }
[features] [features]
default = ["compression_lzma", "protocol_udp"] default = ["compression_lzma", "protocol_udp"]
@ -30,6 +31,7 @@ compression_zstd = ["dep:zstd"]
all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"] all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"]
rand = ["dep:rand"] rand = ["dep:rand"]
protocol_udp = [] protocol_udp = []
protocol_websocket = ["dep:tungstenite"]
[[example]] [[example]]
name = "random_brightness" name = "random_brightness"

View file

@ -1,12 +1,5 @@
use std::fmt::Debug; 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; use crate::packet::Packet;
/// A connection to the display. /// A connection to the display.
@ -15,22 +8,54 @@ use crate::packet::Packet;
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// let connection = servicepoint::Connection::open("172.23.42.29:2342") /// let connection = servicepoint::Connection::open("127.0.0.1:2342")
/// .expect("connection failed"); /// .expect("connection failed");
/// connection.send(servicepoint::Command::Clear) /// connection.send(servicepoint::Command::Clear)
/// .expect("send failed"); /// .expect("send failed");
/// ``` /// ```
#[derive(Debug)]
pub enum Connection { 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")] #[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<std::net::TcpStream>,
>,
),
/// A fake connection for testing that does not actually send anything. /// A fake connection for testing that does not actually send anything.
///
/// This variant allows immutable send.
Fake, Fake,
/// A fake connection for testing that does not actually send anything.
///
/// This variant does not allow immutable send.
FakeMutableSend,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum SendError { pub enum SendError {
IoError(std::io::Error), IoError(std::io::Error),
#[cfg(feature = "protocol_websocket")]
WebsocketError(tungstenite::Error),
} }
impl Connection { 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. /// 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 /// # Errors
/// ///
/// Any errors resulting from binding the udp socket. /// Any errors resulting from binding the udp socket.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// let connection = servicepoint::Connection::open("172.23.42.29:2342") /// let connection = servicepoint::Connection::open("127.0.0.1:2342")
/// .expect("connection failed"); /// .expect("connection failed");
/// ``` /// ```
#[cfg(feature = "protocol_udp")] #[cfg(feature = "protocol_udp")]
pub fn open(addr: impl ToSocketAddrs + Debug) -> std::io::Result<Self> { pub fn open(
info!("connecting to {addr:?}"); addr: impl std::net::ToSocketAddrs + Debug,
let socket = UdpSocket::bind("0.0.0.0:0")?; ) -> std::io::Result<Self> {
log::info!("connecting to {addr:?}");
let socket = std::net::UdpSocket::bind("0.0.0.0:0")?;
socket.connect(addr)?; socket.connect(addr)?;
Ok(Self::Udp(socket)) 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<Self> {
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. /// 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 /// # Arguments
/// ///
/// - `packet`: the packet-like to send /// - `packet`: the packet-like to send
/// ///
/// returns: true if packet was sent, otherwise false /// returns: true if packet was sent, otherwise false
/// ///
/// # Panics
///
/// If the connection does not support immutable send, e.g. for [Connection::WebSocket].
///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # let connection = servicepoint::Connection::Fake; /// let connection = servicepoint::Connection::Fake;
/// // turn off all pixels on display /// // turn off all pixels on display
/// connection.send(servicepoint::Command::Clear) /// connection.send(servicepoint::Command::Clear)
/// .expect("send failed"); /// .expect("send failed");
/// ``` /// ```
pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> { pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> {
let packet = packet.into(); let packet = packet.into();
debug!("sending {packet:?}"); log::debug!("sending {packet:?}");
let data: Vec<u8> = packet.into(); let data: Vec<u8> = packet.into();
match self { match self {
#[cfg(feature = "protocol_udp")] #[cfg(feature = "protocol_udp")]
@ -87,6 +155,55 @@ impl Connection {
let _ = data; let _ = data;
Ok(()) 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<Packet>,
) -> Result<(), SendError> {
match self {
#[cfg(feature = "protocol_websocket")]
Connection::WebSocket(socket) => {
let packet = packet.into();
log::debug!("sending {packet:?}");
let data: Vec<u8> = 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<u8> = packet.into();
let _ = data;
Ok(())
}
_ => self.send(packet),
} }
} }
} }
@ -102,4 +219,19 @@ mod tests {
let packet = Packet::try_from(data).unwrap(); let packet = Packet::try_from(data).unwrap();
Connection::Fake.send(packet).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()
}
} }