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

View file

@ -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"

View file

@ -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<std::net::TcpStream>,
>,
),
/// 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<Self> {
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<Self> {
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<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.
///
/// 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<Packet>) -> Result<(), SendError> {
let packet = packet.into();
debug!("sending {packet:?}");
log::debug!("sending {packet:?}");
let data: Vec<u8> = 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<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();
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()
}
}