From d3e135e50809c783299f1cbda224bfbb1d7d4f79 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Thu, 10 Oct 2024 22:07:55 +0200 Subject: [PATCH 01/15] add optimization to suppress gcc warning about not being able to apply hardening --- crates/servicepoint_binding_c/examples/lang_c/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/servicepoint_binding_c/examples/lang_c/build.rs b/crates/servicepoint_binding_c/examples/lang_c/build.rs index db090d4..4f92e1d 100644 --- a/crates/servicepoint_binding_c/examples/lang_c/build.rs +++ b/crates/servicepoint_binding_c/examples/lang_c/build.rs @@ -12,5 +12,6 @@ fn main() { let mut cc = cc::Build::new(); cc.file("src/main.c"); cc.include(&sp_include); + cc.opt_level(2); cc.compile("lang_c"); } From 26bace8990cfb4c91b40006639733e75c80e316a Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Thu, 10 Oct 2024 23:17:19 +0200 Subject: [PATCH 02/15] add links to documentation, add some docs --- crates/servicepoint/src/command.rs | 65 +++++++++++++++++++---- crates/servicepoint/src/command_code.rs | 2 +- crates/servicepoint/src/connection.rs | 2 + crates/servicepoint/src/lib.rs | 4 ++ crates/servicepoint/src/origin.rs | 2 +- crates/servicepoint/src/packet.rs | 12 +++-- crates/servicepoint/src/pixel_grid.rs | 20 +++---- crates/servicepoint/src/primitive_grid.rs | 14 ++--- 8 files changed, 88 insertions(+), 33 deletions(-) diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index d799696..24cf84c 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -18,13 +18,36 @@ pub type Cp437Grid = PrimitiveGrid; /// /// This struct and associated functions implement the UDP protocol for the display. /// -/// To send a `Command`, use a `Connection`. +/// To send a [Command], use a [connection][crate::Connection]. +/// +/// # Available commands +/// +/// To send text, take a look at [Command::Cp437Data]. +/// +/// To draw pixels, the easiest command to use is [Command::BitmapLinearWin]. +/// +/// The other BitmapLinear-Commands operate on a region of pixel memory directly. +/// [Command::BitmapLinear] overwrites a region. +/// [Command::BitmapLinearOr], [Command::BitmapLinearAnd] and [Command::BitmapLinearXor] apply logical operations per pixel. +/// +/// Out of bounds operations may be truncated or ignored by the display. +/// +/// # Compression +/// +/// Some commands can contain compressed payloads. +/// To get started, use [CompressionCode::Uncompressed]. +/// +/// If you want to archive the best performance (e.g. latency), +/// you can try the different compression algorithms for your hardware and use case. +/// +/// In memory, the payload is not compressed in the [Command]. +/// Payload (de-)compression happens when converting the [Command] into a [Packet] or vice versa. /// /// # Examples /// /// ```rust /// # use servicepoint::{Brightness, Command, Connection, Packet}; -/// +/// # /// // create command /// let command = Command::Brightness(Brightness::MAX); /// @@ -56,6 +79,8 @@ pub enum Command { /// Show text on the screen. /// + /// The text is sent in the form of a 2D grid of characters. + /// ///
/// The library does not currently convert between UTF-8 and CP-437. /// Because Rust expects UTF-8 strings, it might be necessary to only send ASCII for now. @@ -72,7 +97,29 @@ pub enum Command { /// ``` Cp437Data(Origin, Cp437Grid), - /// Sets a window of pixels to the specified values + /// Overwrites a rectangular region of pixels. + /// + /// Origin coordinates must be divisible by 8. + /// + /// # Examples + /// + /// ```rust + /// # use servicepoint::{Command, CompressionCode, Grid, PixelGrid}; + /// # let connection = servicepoint::Connection::Fake; + /// # + /// let mut pixels = PixelGrid::max_sized(); + /// // draw something to the pixels here + /// # pixels.set(2, 5, true); + /// + /// // create command to send pixels + /// let command = Command::BitmapLinearWin( + /// servicepoint::Origin::new(0, 0), + /// pixels, + /// CompressionCode::Uncompressed + /// ); + /// + /// connection.send(command).expect("send failed"); + /// ``` BitmapLinearWin(Origin, PixelGrid, CompressionCode), /// Set the brightness of all tiles to the same value. @@ -95,7 +142,7 @@ pub enum Command { /// The screen will continuously overwrite more pixel data without regarding the offset, meaning /// once the starting row is full, overwriting will continue on column 0. /// - /// The contained `BitVec` is always uncompressed. + /// The contained [BitVec] is always uncompressed. BitmapLinear(Offset, SpBitVec, CompressionCode), /// Set pixel data according to an and-mask starting at the offset. @@ -103,7 +150,7 @@ pub enum Command { /// The screen will continuously overwrite more pixel data without regarding the offset, meaning /// once the starting row is full, overwriting will continue on column 0. /// - /// The contained `BitVec` is always uncompressed. + /// The contained [BitVec] is always uncompressed. BitmapLinearAnd(Offset, SpBitVec, CompressionCode), /// Set pixel data according to an or-mask starting at the offset. @@ -111,7 +158,7 @@ pub enum Command { /// The screen will continuously overwrite more pixel data without regarding the offset, meaning /// once the starting row is full, overwriting will continue on column 0. /// - /// The contained `BitVec` is always uncompressed. + /// The contained [BitVec] is always uncompressed. BitmapLinearOr(Offset, SpBitVec, CompressionCode), /// Set pixel data according to a xor-mask starting at the offset. @@ -119,7 +166,7 @@ pub enum Command { /// The screen will continuously overwrite more pixel data without regarding the offset, meaning /// once the starting row is full, overwriting will continue on column 0. /// - /// The contained `BitVec` is always uncompressed. + /// The contained [BitVec] is always uncompressed. BitmapLinearXor(Offset, SpBitVec, CompressionCode), /// Kills the udp daemon on the display, which usually results in a restart. @@ -166,7 +213,7 @@ pub enum Command { } #[derive(Debug)] -/// Err values for `Command::try_from`. +/// Err values for [Command::try_from]. #[derive(PartialEq)] pub enum TryFromPacketError { /// the contained command code does not correspond to a known command @@ -188,7 +235,7 @@ pub enum TryFromPacketError { impl TryFrom for Command { type Error = TryFromPacketError; - /// Try to interpret the `Packet` as one containing a `Command` + /// Try to interpret the [Packet] as one containing a [Command] fn try_from(packet: Packet) -> Result { let Packet { header: Header { diff --git a/crates/servicepoint/src/command_code.rs b/crates/servicepoint/src/command_code.rs index 25df1c9..4735e44 100644 --- a/crates/servicepoint/src/command_code.rs +++ b/crates/servicepoint/src/command_code.rs @@ -1,4 +1,4 @@ -/// The u16 command codes used for the `Commands`. +/// The u16 command codes used for the [Command]s. #[repr(u16)] #[derive(Debug, Copy, Clone)] pub(crate) enum CommandCode { diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index 032ffeb..40c9a5c 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -7,6 +7,8 @@ use crate::Packet; /// A connection to the display. /// +/// Used to send [Packets][Packet] or [Commands][crate::Command]. +/// /// # Examples /// ```rust /// let connection = servicepoint::Connection::open("172.23.42.29:2342") diff --git a/crates/servicepoint/src/lib.rs b/crates/servicepoint/src/lib.rs index d71e1fa..e646e24 100644 --- a/crates/servicepoint/src/lib.rs +++ b/crates/servicepoint/src/lib.rs @@ -1,5 +1,9 @@ //! Abstractions for the UDP protocol of the CCCB servicepoint display. //! +//! Your starting point is a [Connection] to the display. +//! With a connection, you can send [Command]s. +//! When received, the display will update the state of the pixels. +//! //! # Examples //! //! ```rust diff --git a/crates/servicepoint/src/origin.rs b/crates/servicepoint/src/origin.rs index 88758a5..0e7680a 100644 --- a/crates/servicepoint/src/origin.rs +++ b/crates/servicepoint/src/origin.rs @@ -11,7 +11,7 @@ pub struct Origin { } impl Origin { - /// Create a new `Origin` instance for the provided position. + /// Create a new [Origin] instance for the provided position. pub fn new(x: usize, y: usize) -> Self { Self { x, diff --git a/crates/servicepoint/src/packet.rs b/crates/servicepoint/src/packet.rs index 67c96b4..c09cb42 100644 --- a/crates/servicepoint/src/packet.rs +++ b/crates/servicepoint/src/packet.rs @@ -38,6 +38,8 @@ pub type Payload = Vec; /// /// Contents should probably only be used directly to use features not exposed by the library. /// +/// You may want to use [Command][crate::Command] instead. +/// /// # Examples /// /// Converting a packet to a command and back: @@ -46,7 +48,7 @@ pub type Payload = Vec; /// # use servicepoint::{Command, Packet}; /// # let command = Command::Clear; /// let packet: Packet = command.into(); -/// let command: Command = Command::try_from(packet).expect("could not read packet"); +/// let command: Command = Command::try_from(packet).expect("could not read command from packet"); /// ``` /// /// Converting a packet to bytes and back: @@ -98,9 +100,9 @@ impl From for Vec { impl TryFrom<&[u8]> for Packet { type Error = (); - /// Tries to interpret the bytes as a `Packet`. + /// Tries to interpret the bytes as a [Packet]. /// - /// returns: `Error` if slice is not long enough to be a `Packet` + /// returns: `Error` if slice is not long enough to be a [Packet] fn try_from(value: &[u8]) -> Result { if value.len() < size_of::
() { return Err(()); @@ -135,7 +137,7 @@ impl TryFrom> for Packet { } impl From for Packet { - /// Move the `Command` into a `Packet` instance for sending. + /// Move the [Command] into a [Packet] instance for sending. #[allow(clippy::cast_possible_truncation)] fn from(value: Command) -> Self { match value { @@ -210,7 +212,7 @@ impl From for Packet { } impl Packet { - /// Helper method for `BitMapLinear*`-Commands into `Packet` + /// Helper method for `BitMapLinear*`-Commands into [Packet] #[allow(clippy::cast_possible_truncation)] fn bitmap_linear_into_packet( command: CommandCode, diff --git a/crates/servicepoint/src/pixel_grid.rs b/crates/servicepoint/src/pixel_grid.rs index 4b8bcc7..746f2ac 100644 --- a/crates/servicepoint/src/pixel_grid.rs +++ b/crates/servicepoint/src/pixel_grid.rs @@ -13,14 +13,14 @@ pub struct PixelGrid { } impl PixelGrid { - /// Creates a new `PixelGrid` with the specified dimensions. + /// Creates a new [PixelGrid] with the specified dimensions. /// /// # Arguments /// /// - `width`: size in pixels in x-direction /// - `height`: size in pixels in y-direction /// - /// returns: `PixelGrid` initialized to all pixels off + /// returns: [PixelGrid] initialized to all pixels off /// /// # Panics /// @@ -40,14 +40,14 @@ impl PixelGrid { Self::new(PIXEL_WIDTH, PIXEL_HEIGHT) } - /// Loads a `PixelGrid` with the specified dimensions from the provided data. + /// Loads a [PixelGrid] with the specified dimensions from the provided data. /// /// # Arguments /// /// - `width`: size in pixels in x-direction /// - `height`: size in pixels in y-direction /// - /// returns: `PixelGrid` that contains a copy of the provided data + /// returns: [PixelGrid] that contains a copy of the provided data /// /// # Panics /// @@ -64,7 +64,7 @@ impl PixelGrid { } } - /// Iterate over all cells in `PixelGrid`. + /// Iterate over all cells in [PixelGrid]. /// /// Order is equivalent to the following loop: /// ``` @@ -80,7 +80,7 @@ impl PixelGrid { self.bit_vec.iter().by_refs() } - /// Iterate over all cells in `PixelGrid` mutably. + /// Iterate over all cells in [PixelGrid] mutably. /// /// Order is equivalent to the following loop: /// ``` @@ -107,7 +107,7 @@ impl PixelGrid { self.bit_vec.iter_mut() } - /// Iterate over all rows in `PixelGrid` top to bottom. + /// Iterate over all rows in [PixelGrid] top to bottom. pub fn iter_rows(&self) -> IterRows { IterRows { pixel_grid: self, @@ -117,7 +117,7 @@ impl PixelGrid { } impl Grid for PixelGrid { - /// Sets the value of the specified position in the `PixelGrid`. + /// Sets the value of the specified position in the [PixelGrid]. /// /// # Arguments /// @@ -139,7 +139,7 @@ impl Grid for PixelGrid { self.bit_vec[x + y * self.width] } - /// Sets the state of all pixels in the `PixelGrid`. + /// Sets the state of all pixels in the [PixelGrid]. /// /// # Arguments /// @@ -169,7 +169,7 @@ impl DataRef for PixelGrid { } impl From for Vec { - /// Turns a `PixelGrid` into the underlying `Vec`. + /// Turns a [PixelGrid] into the underlying [`Vec`]. fn from(value: PixelGrid) -> Self { value.bit_vec.into() } diff --git a/crates/servicepoint/src/primitive_grid.rs b/crates/servicepoint/src/primitive_grid.rs index d1a507e..2b02ca3 100644 --- a/crates/servicepoint/src/primitive_grid.rs +++ b/crates/servicepoint/src/primitive_grid.rs @@ -14,14 +14,14 @@ pub struct PrimitiveGrid { } impl PrimitiveGrid { - /// Creates a new `PrimitiveGrid` with the specified dimensions. + /// Creates a new [PrimitiveGrid] with the specified dimensions. /// /// # Arguments /// /// - width: size in x-direction /// - height: size in y-direction /// - /// returns: `PrimitiveGrid` initialized to default value. + /// returns: [PrimitiveGrid] initialized to default value. pub fn new(width: usize, height: usize) -> Self { Self { data: vec![Default::default(); width * height], @@ -30,9 +30,9 @@ impl PrimitiveGrid { } } - /// Loads a `PrimitiveGrid` with the specified dimensions from the provided data. + /// Loads a [PrimitiveGrid] with the specified dimensions from the provided data. /// - /// returns: `PrimitiveGrid` that contains a copy of the provided data + /// returns: [PrimitiveGrid] that contains a copy of the provided data /// /// # Panics /// @@ -47,7 +47,7 @@ impl PrimitiveGrid { } } - /// Iterate over all cells in `PrimitiveGrid`. + /// Iterate over all cells in [PrimitiveGrid]. /// /// Order is equivalent to the following loop: /// ``` @@ -63,7 +63,7 @@ impl PrimitiveGrid { self.data.iter() } - /// Iterate over all rows in `PrimitiveGrid` top to bottom. + /// Iterate over all rows in [PrimitiveGrid] top to bottom. pub fn iter_rows(&self) -> IterRows { IterRows { byte_grid: self, @@ -168,7 +168,7 @@ impl DataRef for PrimitiveGrid { } impl From> for Vec { - /// Turn into the underlying `Vec` containing the rows of bytes. + /// Turn into the underlying [`Vec`] containing the rows of bytes. fn from(value: PrimitiveGrid) -> Self { value.data } From 78b5d1180b3437881af84bb38b7ebec2e50d0bcb Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Thu, 10 Oct 2024 23:37:04 +0200 Subject: [PATCH 03/15] hide packets to reduce api surface for new users --- crates/servicepoint/src/command.rs | 8 +-- crates/servicepoint/src/compression.rs | 2 +- crates/servicepoint/src/connection.rs | 4 +- crates/servicepoint/src/lib.rs | 3 +- crates/servicepoint/src/packet.rs | 54 +++++++++++---------- crates/servicepoint_binding_c/src/packet.rs | 4 +- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index 24cf84c..537b017 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -1,8 +1,10 @@ use bitvec::prelude::BitVec; use crate::{ - command_code::CommandCode, compression::into_decompressed, Brightness, - BrightnessGrid, CompressionCode, Header, Origin, Packet, PixelGrid, Pixels, + command_code::CommandCode, + compression::into_decompressed, + packet::{Header, Packet}, + Brightness, BrightnessGrid, CompressionCode, Origin, PixelGrid, Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, }; @@ -46,7 +48,7 @@ pub type Cp437Grid = PrimitiveGrid; /// # Examples /// /// ```rust -/// # use servicepoint::{Brightness, Command, Connection, Packet}; +/// # use servicepoint::{Brightness, Command, Connection, packet::Packet}; /// # /// // create command /// let command = Command::Brightness(Brightness::MAX); diff --git a/crates/servicepoint/src/compression.rs b/crates/servicepoint/src/compression.rs index 2e78073..12c79d5 100644 --- a/crates/servicepoint/src/compression.rs +++ b/crates/servicepoint/src/compression.rs @@ -8,7 +8,7 @@ use flate2::{FlushCompress, FlushDecompress, Status}; #[cfg(feature = "compression_zstd")] use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; -use crate::{CompressionCode, Payload}; +use crate::{packet::Payload, CompressionCode}; pub(crate) fn into_decompressed( kind: CompressionCode, diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index 40c9a5c..255c15a 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -3,7 +3,7 @@ use std::net::{ToSocketAddrs, UdpSocket}; use log::{debug, info}; -use crate::Packet; +use crate::packet::Packet; /// A connection to the display. /// @@ -19,7 +19,7 @@ use crate::Packet; pub enum Connection { /// A real connection using the UDP protocol Udp(UdpSocket), - /// A fake connection for testing that does not actually send anything + /// A fake connection for testing that does not actually send anything. Fake, } diff --git a/crates/servicepoint/src/lib.rs b/crates/servicepoint/src/lib.rs index e646e24..4cceea8 100644 --- a/crates/servicepoint/src/lib.rs +++ b/crates/servicepoint/src/lib.rs @@ -47,7 +47,6 @@ pub use crate::connection::Connection; pub use crate::data_ref::DataRef; pub use crate::grid::Grid; pub use crate::origin::{Origin, Pixels, Tiles}; -pub use crate::packet::{Header, Packet, Payload}; pub use crate::pixel_grid::PixelGrid; pub use crate::primitive_grid::PrimitiveGrid; @@ -62,7 +61,7 @@ mod connection; mod data_ref; mod grid; mod origin; -mod packet; +pub mod packet; mod pixel_grid; mod primitive_grid; diff --git a/crates/servicepoint/src/packet.rs b/crates/servicepoint/src/packet.rs index c09cb42..cf0fa82 100644 --- a/crates/servicepoint/src/packet.rs +++ b/crates/servicepoint/src/packet.rs @@ -1,10 +1,34 @@ +//! Raw packet manipulation. +//! +//! Should probably only be used directly to use features not exposed by the library. +//! +//! # Examples +//! +//! Converting a packet to a command and back: +//! +//! ```rust +//! use servicepoint::{Command, packet::Packet}; +//! # let command = Command::Clear; +//! let packet: Packet = command.into(); +//! let command: Command = Command::try_from(packet).expect("could not read command from packet"); +//! ``` +//! +//! Converting a packet to bytes and back: +//! +//! ```rust +//! use servicepoint::{Command, packet::Packet}; +//! # let command = Command::Clear; +//! # let packet: Packet = command.into(); +//! let bytes: Vec = packet.into(); +//! let packet = Packet::try_from(bytes).expect("could not read packet from bytes"); +//! ``` + use std::mem::size_of; -use crate::command_code::CommandCode; use crate::compression::into_compressed; use crate::{ - Command, CompressionCode, Grid, Offset, Origin, PixelGrid, Pixels, Tiles, - TILE_SIZE, + command_code::CommandCode, Command, CompressionCode, Grid, Offset, Origin, + PixelGrid, Pixels, Tiles, TILE_SIZE, }; /// A raw header. @@ -13,8 +37,6 @@ use crate::{ /// payload, where applicable. /// /// Because the meaning of most fields depend on the command, there are no speaking names for them. -/// -/// Should probably only be used directly to use features not exposed by the library. #[derive(Copy, Clone, Debug, PartialEq)] pub struct Header { /// The first two bytes specify which command this packet represents. @@ -38,28 +60,8 @@ pub type Payload = Vec; /// /// Contents should probably only be used directly to use features not exposed by the library. /// -/// You may want to use [Command][crate::Command] instead. +/// You may want to use [Command] instead. /// -/// # Examples -/// -/// Converting a packet to a command and back: -/// -/// ```rust -/// # use servicepoint::{Command, Packet}; -/// # let command = Command::Clear; -/// let packet: Packet = command.into(); -/// let command: Command = Command::try_from(packet).expect("could not read command from packet"); -/// ``` -/// -/// Converting a packet to bytes and back: -/// -/// ```rust -/// # use servicepoint::{Command, Packet}; -/// # let command = Command::Clear; -/// # let packet: Packet = command.into(); -/// let bytes: Vec = packet.into(); -/// let packet = Packet::try_from(bytes).expect("could not read packet from bytes"); -/// ``` /// #[derive(Clone, Debug, PartialEq)] pub struct Packet { diff --git a/crates/servicepoint_binding_c/src/packet.rs b/crates/servicepoint_binding_c/src/packet.rs index eac41b8..5dc5820 100644 --- a/crates/servicepoint_binding_c/src/packet.rs +++ b/crates/servicepoint_binding_c/src/packet.rs @@ -7,7 +7,7 @@ use std::ptr::null_mut; use crate::SPCommand; /// The raw packet -pub struct SPPacket(pub(crate) servicepoint::Packet); +pub struct SPPacket(pub(crate) servicepoint::packet::Packet); /// Turns a `SPCommand` into a `SPPacket`. /// The `SPCommand` gets consumed. @@ -49,7 +49,7 @@ pub unsafe extern "C" fn sp_packet_try_load( length: usize, ) -> *mut SPPacket { let data = std::slice::from_raw_parts(data, length); - match servicepoint::Packet::try_from(data) { + match servicepoint::packet::Packet::try_from(data) { Err(_) => null_mut(), Ok(packet) => Box::into_raw(Box::new(SPPacket(packet))), } From 46cb48c126691f30b47c28470d16f40f679dea20 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 11 Oct 2024 00:08:13 +0200 Subject: [PATCH 04/15] convert between origin units --- crates/servicepoint/src/origin.rs | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/servicepoint/src/origin.rs b/crates/servicepoint/src/origin.rs index 0e7680a..16ba083 100644 --- a/crates/servicepoint/src/origin.rs +++ b/crates/servicepoint/src/origin.rs @@ -1,3 +1,4 @@ +use crate::TILE_SIZE; use std::marker::PhantomData; /// An origin marks the top left position of a window sent to the display. @@ -46,3 +47,69 @@ pub struct Tiles(); impl DisplayUnit for Pixels {} impl DisplayUnit for Tiles {} + +impl From<&Origin> for Origin { + fn from(value: &Origin) -> Self { + Self { + x: value.x * TILE_SIZE, + y: value.y * TILE_SIZE, + phantom_data: PhantomData, + } + } +} + +impl TryFrom<&Origin> for Origin { + type Error = (); + + fn try_from(value: &Origin) -> Result { + let (x, x_rem) = (value.x / TILE_SIZE, value.x % TILE_SIZE); + if x_rem != 0 { + return Err(()); + } + let (y, y_rem) = (value.y / TILE_SIZE, value.y % TILE_SIZE); + if y_rem != 0 { + return Err(()); + } + + Ok(Self { + x, + y, + phantom_data: PhantomData, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn origin_tile_to_pixel() { + let tile: Origin = Origin::new(1, 2); + let actual: Origin = Origin::from(&tile); + let expected: Origin = Origin::new(8, 16); + assert_eq!(actual, expected); + } + + #[test] + fn origin_pixel_to_tile() { + let pixel: Origin = Origin::new(8, 16); + let actual: Origin = Origin::try_from(&pixel).unwrap(); + let expected: Origin = Origin::new(1, 2); + assert_eq!(actual, expected); + } + + #[test] + #[should_panic] + fn origin_pixel_to_tile_fail_y() { + let pixel: Origin = Origin::new(8, 15); + let _: Origin = Origin::try_from(&pixel).unwrap(); + } + + #[test] + #[should_panic] + fn origin_pixel_to_tile_fail_x() { + let pixel: Origin = Origin::new(7, 16); + let _: Origin = Origin::try_from(&pixel).unwrap(); + } +} From 21cc7e3f121e55cb91c2d239d7e6ae9bf42fc90d Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 11 Oct 2024 21:37:28 +0200 Subject: [PATCH 05/15] fixup! hide packets to reduce api surface for new users --- crates/servicepoint/src/command.rs | 9 ++++++--- crates/servicepoint/src/packet.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index 537b017..6b821e9 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -492,9 +492,12 @@ impl Command { #[cfg(test)] mod tests { use crate::{ - bitvec::prelude::BitVec, command::TryFromPacketError, - command_code::CommandCode, origin::Pixels, Brightness, Command, - CompressionCode, Header, Origin, Packet, PixelGrid, PrimitiveGrid, + bitvec::prelude::BitVec, + command::TryFromPacketError, + command_code::CommandCode, + origin::Pixels, + packet::{Header, Packet}, + Brightness, Command, CompressionCode, Origin, PixelGrid, PrimitiveGrid, }; fn round_trip(original: Command) { diff --git a/crates/servicepoint/src/packet.rs b/crates/servicepoint/src/packet.rs index cf0fa82..a8f0831 100644 --- a/crates/servicepoint/src/packet.rs +++ b/crates/servicepoint/src/packet.rs @@ -316,7 +316,7 @@ impl Packet { #[cfg(test)] mod tests { - use crate::{Header, Packet}; + use super::*; #[test] fn round_trip() { From 03f74956950b2a5f9ac66d41e309df39b6b91995 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 11 Oct 2024 21:38:12 +0200 Subject: [PATCH 06/15] add function to load Cp437Grid from str --- crates/servicepoint/src/command.rs | 14 +-- crates/servicepoint/src/cp437.rs | 100 ++++++++++++++++++ crates/servicepoint/src/lib.rs | 4 +- .../servicepoint_binding_c/src/cp437_grid.rs | 8 +- 4 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 crates/servicepoint/src/cp437.rs diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index 6b821e9..6095bb3 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -4,18 +4,13 @@ use crate::{ command_code::CommandCode, compression::into_decompressed, packet::{Header, Packet}, - Brightness, BrightnessGrid, CompressionCode, Origin, PixelGrid, Pixels, - PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, + Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin, PixelGrid, + Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, }; /// Type alias for documenting the meaning of the u16 in enum values pub type Offset = usize; -/// A grid containing codepage 437 characters. -/// -/// The encoding is currently not enforced. -pub type Cp437Grid = PrimitiveGrid; - /// A low-level display command. /// /// This struct and associated functions implement the UDP protocol for the display. @@ -92,9 +87,8 @@ pub enum Command { /// /// ```rust /// # use servicepoint::{Command, Connection, Cp437Grid, Origin}; - /// # let connection = Connection::open("127.0.0.1:2342").unwrap(); - /// let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'].map(move |c| c as u8); - /// let grid = Cp437Grid::load(5, 2, &chars); + /// # let connection = Connection::Fake; + /// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap(); /// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap(); /// ``` Cp437Data(Origin, Cp437Grid), diff --git a/crates/servicepoint/src/cp437.rs b/crates/servicepoint/src/cp437.rs new file mode 100644 index 0000000..b0fd991 --- /dev/null +++ b/crates/servicepoint/src/cp437.rs @@ -0,0 +1,100 @@ +use crate::cp437::Cp437LoadError::InvalidChar; +use crate::{Grid, PrimitiveGrid}; + +/// A grid containing codepage 437 characters. +/// +/// The encoding is currently not enforced. +pub type Cp437Grid = PrimitiveGrid; + +#[derive(Debug)] +pub enum Cp437LoadError { + InvalidChar { index: usize, char: char }, +} + +impl Cp437Grid { + /// Load an ASCII-only [&str] into a [Cp437Grid] of specified width. + /// + /// # Panics + /// + /// - for width == 0 + /// - on empty strings + pub fn load_ascii( + value: &str, + width: usize, + wrap: bool, + ) -> Result { + assert!(width > 0); + assert!(!value.is_empty()); + + let mut chars = { + let mut x = 0; + let mut y = 0; + + for (index, char) in value.chars().enumerate() { + if !char.is_ascii() { + return Err(InvalidChar { index, char }); + } + + let is_lf = char == '\n'; + if is_lf || (wrap && x == width) { + y += 1; + x = 0; + if is_lf { + continue; + } + } + + x += 1; + } + + Cp437Grid::new(width, y + 1) + }; + + let mut x = 0; + let mut y = 0; + for char in value.chars().map(move |c| c as u8) { + let is_lf = char == b'\n'; + if is_lf || (wrap && x == width) { + y += 1; + x = 0; + if is_lf { + continue; + } + } + + if wrap || x < width { + chars.set(x, y, char); + } + x += 1; + } + + Ok(chars) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_ascii_nowrap() { + let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] + .map(move |c| c as u8); + let expected = Cp437Grid::load(5, 2, &chars); + + let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap(); + // comma will be removed because line is too long and wrap is off + assert_eq!(actual, expected); + } + + #[test] + fn load_ascii_wrap() { + let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] + .map(move |c| c as u8); + let expected = Cp437Grid::load(5, 2, &chars); + + let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap(); + // line break will be added + assert_eq!(actual, expected); + } +} diff --git a/crates/servicepoint/src/lib.rs b/crates/servicepoint/src/lib.rs index 4cceea8..4ed440f 100644 --- a/crates/servicepoint/src/lib.rs +++ b/crates/servicepoint/src/lib.rs @@ -41,9 +41,10 @@ pub use bitvec; use bitvec::prelude::{BitVec, Msb0}; pub use crate::brightness::{Brightness, BrightnessGrid}; -pub use crate::command::{Command, Cp437Grid, Offset}; +pub use crate::command::{Command, Offset}; pub use crate::compression_code::CompressionCode; pub use crate::connection::Connection; +pub use crate::cp437::Cp437Grid; pub use crate::data_ref::DataRef; pub use crate::grid::Grid; pub use crate::origin::{Origin, Pixels, Tiles}; @@ -58,6 +59,7 @@ mod command_code; mod compression; mod compression_code; mod connection; +mod cp437; mod data_ref; mod grid; mod origin; diff --git a/crates/servicepoint_binding_c/src/cp437_grid.rs b/crates/servicepoint_binding_c/src/cp437_grid.rs index a8d4684..781c345 100644 --- a/crates/servicepoint_binding_c/src/cp437_grid.rs +++ b/crates/servicepoint_binding_c/src/cp437_grid.rs @@ -40,7 +40,9 @@ pub unsafe extern "C" fn sp_cp437_grid_new( width: usize, height: usize, ) -> *mut SPCp437Grid { - Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::new(width, height)))) + Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::new( + width, height, + )))) } /// Loads a `SPCp437Grid` with the specified dimensions from the provided data. @@ -67,7 +69,9 @@ pub unsafe extern "C" fn sp_cp437_grid_load( data_length: usize, ) -> *mut SPCp437Grid { let data = std::slice::from_raw_parts(data, data_length); - Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::load(width, height, data)))) + Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::load( + width, height, data, + )))) } /// Clones a `SPCp437Grid`. From 942968fcf472668e89a0b59d2221fa30c91a60b1 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 11 Oct 2024 21:38:47 +0200 Subject: [PATCH 07/15] fix error message, add test --- crates/servicepoint/src/connection.rs | 13 +++++++++++++ crates/servicepoint/src/grid.rs | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index 255c15a..224f370 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -80,3 +80,16 @@ impl Connection { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::*; + + #[test] + fn send_fake() { + 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::Fake.send(packet).unwrap() + } +} diff --git a/crates/servicepoint/src/grid.rs b/crates/servicepoint/src/grid.rs index cb8f5e7..d367d98 100644 --- a/crates/servicepoint/src/grid.rs +++ b/crates/servicepoint/src/grid.rs @@ -79,12 +79,12 @@ pub trait Grid { assert!( x < self.width(), "cannot access index [{x}, {y}] because x is outside of bounds 0..{}", - self.width() + self.width() - 1 ); assert!( y < self.height(), - "cannot access byte [{x}, {y}] because y is outside of bounds 0..{}", - self.height() + "cannot access index [{x}, {y}] because y is outside of bounds 0..{}", + self.height() - 1 ); } } From 4dfb405792185b4650e61535b441aeca8da1d0cb Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 11 Oct 2024 21:42:26 +0200 Subject: [PATCH 08/15] update dependencies --- Cargo.lock | 84 +++++++++---------- .../examples/lang_c/include/servicepoint.h | 6 +- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9328e45..ff65df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.18" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1" dependencies = [ "jobserver", "libc", @@ -149,9 +149,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -236,9 +236,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -263,9 +263,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -281,9 +281,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "linux-raw-sys" @@ -353,15 +353,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "ppv-lite86" @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -428,9 +428,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rust-lzma" @@ -467,9 +467,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.36" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", @@ -518,9 +518,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -570,9 +570,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -587,9 +587,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -621,9 +621,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -634,9 +634,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "utf8parse" @@ -740,9 +740,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h b/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h index ce780ff..f431793 100644 --- a/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h +++ b/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h @@ -1,4 +1,4 @@ -/* Generated with cbindgen:0.26.0 */ +/* Generated with cbindgen:0.27.0 */ /* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ @@ -1355,5 +1355,5 @@ struct SPByteSlice sp_pixel_grid_unsafe_data_ref(struct SPPixelGrid *pixel_grid) size_t sp_pixel_grid_width(const struct SPPixelGrid *pixel_grid); #ifdef __cplusplus -} // extern "C" -#endif // __cplusplus +} // extern "C" +#endif // __cplusplus From f434b5bf830c99049332db1aef77b2e6c2f262b9 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 13:04:56 +0200 Subject: [PATCH 09/15] udp protocol as feature --- crates/servicepoint/Cargo.toml | 3 ++- crates/servicepoint/src/connection.rs | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/servicepoint/Cargo.toml b/crates/servicepoint/Cargo.toml index b68b538..515bb55 100644 --- a/crates/servicepoint/Cargo.toml +++ b/crates/servicepoint/Cargo.toml @@ -22,13 +22,14 @@ rust-lzma = { version = "0.6.0", optional = true } rand = { version = "0.8", optional = true } [features] -default = ["compression_lzma"] +default = ["compression_lzma", "protocol_udp"] compression_zlib = ["dep:flate2"] compression_bzip2 = ["dep:bzip2"] compression_lzma = ["dep:rust-lzma"] compression_zstd = ["dep:zstd"] all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"] rand = ["dep:rand"] +protocol_udp = [] [[example]] name = "random_brightness" diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index 224f370..76b3389 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -1,7 +1,11 @@ use std::fmt::Debug; -use std::net::{ToSocketAddrs, UdpSocket}; -use log::{debug, info}; +use log::debug; + +#[cfg(feature = "protocol_udp")] +use log::info; +#[cfg(feature = "protocol_udp")] +use std::net::{ToSocketAddrs, UdpSocket}; use crate::packet::Packet; @@ -18,6 +22,7 @@ use crate::packet::Packet; /// ``` pub enum Connection { /// A real connection using the UDP protocol + #[cfg(feature = "protocol_udp")] Udp(UdpSocket), /// A fake connection for testing that does not actually send anything. Fake, @@ -42,6 +47,7 @@ impl Connection { /// let connection = servicepoint::Connection::open("172.23.42.29: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")?; @@ -70,13 +76,17 @@ impl Connection { debug!("sending {packet:?}"); let data: Vec = packet.into(); match self { + #[cfg(feature = "protocol_udp")] Connection::Udp(socket) => { socket .send(&data) .map_err(SendError::IoError) .map(move |_| ()) // ignore Ok value } - Connection::Fake => Ok(()), + Connection::Fake => { + let _ = data; + Ok(()) + } } } } From c7764c49e15addc26dbce7ef48bbf3f01e697d48 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 16:18:27 +0200 Subject: [PATCH 10/15] add websocket binary message protocol --- Cargo.lock | 151 ++++++++++++++++++++++++ crates/servicepoint/Cargo.toml | 2 + crates/servicepoint/src/connection.rs | 164 +++++++++++++++++++++++--- 3 files changed, 301 insertions(+), 16 deletions(-) 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() + } } From 3d47b4110695988bea735a9b7ed864d0750bcc94 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 18:23:36 +0200 Subject: [PATCH 11/15] conversion between UTF-8 and CP-437 --- Cargo.lock | 1 + crates/servicepoint/Cargo.toml | 9 +- crates/servicepoint/examples/announce.rs | 20 ++-- crates/servicepoint/src/command.rs | 9 ++ crates/servicepoint/src/cp437.rs | 121 +++++++++++++++++++++++ crates/servicepoint/src/lib.rs | 2 +- crates/servicepoint/src/origin.rs | 7 ++ 7 files changed, 154 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90c56e3..4a84fab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,6 +617,7 @@ dependencies = [ "clap", "flate2", "log", + "once_cell", "rand", "rust-lzma", "tungstenite", diff --git a/crates/servicepoint/Cargo.toml b/crates/servicepoint/Cargo.toml index 09b9994..f66a607 100644 --- a/crates/servicepoint/Cargo.toml +++ b/crates/servicepoint/Cargo.toml @@ -21,9 +21,10 @@ 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 } +once_cell = { version = "1.20.2", optional = true } [features] -default = ["compression_lzma", "protocol_udp"] +default = ["compression_lzma", "protocol_udp", "cp437"] compression_zlib = ["dep:flate2"] compression_bzip2 = ["dep:bzip2"] compression_lzma = ["dep:rust-lzma"] @@ -32,15 +33,19 @@ all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", rand = ["dep:rand"] protocol_udp = [] protocol_websocket = ["dep:tungstenite"] +cp437 = ["dep:once_cell"] [[example]] name = "random_brightness" required-features = ["rand"] +[[example]] +name = "game_of_life" +required-features = ["rand"] + [dev-dependencies] # for examples clap = { version = "4.5", features = ["derive"] } -rand = "0.8" [lints] workspace = true \ No newline at end of file diff --git a/crates/servicepoint/examples/announce.rs b/crates/servicepoint/examples/announce.rs index 75b9ba6..ff95479 100644 --- a/crates/servicepoint/examples/announce.rs +++ b/crates/servicepoint/examples/announce.rs @@ -2,7 +2,7 @@ use clap::Parser; -use servicepoint::{Command, Connection, Cp437Grid, Grid, Origin}; +use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin}; #[derive(Parser, Debug)] struct Cli { @@ -31,19 +31,15 @@ fn main() { .expect("sending clear failed"); } - let max_width = cli.text.iter().map(|t| t.len()).max().unwrap(); + let text = cli.text.iter().fold(String::new(), move |str, line| { + let is_first = str.is_empty(); + str + if is_first { "" } else { "\n" } + line + }); - let mut chars = Cp437Grid::new(max_width, cli.text.len()); - for y in 0..cli.text.len() { - let row = &cli.text[y]; - - for (x, char) in row.chars().enumerate() { - let char = char.try_into().expect("invalid input char"); - chars.set(x, y, char); - } - } + let grid = CharGrid::from(&*text); + let cp437_grid = Cp437Grid::from(&grid); connection - .send(Command::Cp437Data(Origin::new(0, 0), chars)) + .send(Command::Cp437Data(Origin::new(0, 0), cp437_grid)) .expect("sending text failed"); } diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index 6095bb3..49c230d 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -86,6 +86,15 @@ pub enum Command { /// # Examples /// /// ```rust + /// # use servicepoint::{Command, Connection, Origin}; + /// # let connection = Connection::Fake; + /// use servicepoint::{CharGrid, Cp437Grid}; + /// let grid = CharGrid::from(&"Hello,\nWorld!"); + /// let grid = Cp437Grid::from(grid); + /// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed"); + /// ``` + /// + /// ```rust /// # use servicepoint::{Command, Connection, Cp437Grid, Origin}; /// # let connection = Connection::Fake; /// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap(); diff --git a/crates/servicepoint/src/cp437.rs b/crates/servicepoint/src/cp437.rs index b0fd991..dc40627 100644 --- a/crates/servicepoint/src/cp437.rs +++ b/crates/servicepoint/src/cp437.rs @@ -1,11 +1,15 @@ use crate::cp437::Cp437LoadError::InvalidChar; use crate::{Grid, PrimitiveGrid}; +use std::collections::HashMap; /// A grid containing codepage 437 characters. /// /// The encoding is currently not enforced. pub type Cp437Grid = PrimitiveGrid; +/// A grid containing UTF-8 characters. +pub type CharGrid = PrimitiveGrid; + #[derive(Debug)] pub enum Cp437LoadError { InvalidChar { index: usize, char: char }, @@ -72,6 +76,109 @@ impl Cp437Grid { } } +#[allow(unused)] // depends on features +pub use feature_cp437::*; + +#[cfg(feature = "cp437")] +mod feature_cp437 { + use super::*; + + /// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters + /// + /// Mostly follows CP437, except for: + /// * 0x0A & 0x0D are kept for use as line endings. + /// * 0x1A is used for SAUCE. + /// * 0x1B is used for ANSI escape sequences. + /// + /// These exclusions should be fine since most programs can't even use them + /// without issues. And this makes rendering simpler too. + /// + /// See + /// + /// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0 + #[rustfmt::skip] + const CP437_TO_UTF8: [char; 256] = [ + /* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '\r', '♫', '☼', + /* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔', '▲', '▼', + /* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/', + /* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + /* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + /* 5X */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',']', '^', '_', + /* 6X */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + /* 7X */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂', + /* 8X */ 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å', + /* 9X */ 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', + /* AX */ 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', + /* BX */ '░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', + /* CX */ '└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', + /* DX */ '╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', + /* EX */ 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', + /* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ', + ]; + + const UTF8_TO_CP437: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let pairs = CP437_TO_UTF8 + .iter() + .enumerate() + .map(move |(index, char)| (*char, index as u8)); + HashMap::from_iter(pairs) + }); + + const MISSING_CHAR_CP437: u8 = 0x3F; + + impl From<&Cp437Grid> for CharGrid { + fn from(value: &Cp437Grid) -> Self { + let mut grid = Self::new(value.width(), value.height()); + + for y in 0..grid.height() { + for x in 0..grid.width() { + let converted = CP437_TO_UTF8[value.get(x, y) as usize]; + grid.set(x, y, converted); + } + } + + grid + } + } + + impl From<&CharGrid> for Cp437Grid { + fn from(value: &CharGrid) -> Self { + let mut grid = Self::new(value.width(), value.height()); + + for y in 0..grid.height() { + for x in 0..grid.width() { + let char = value.get(x, y); + let converted = *UTF8_TO_CP437 + .get(&char) + .unwrap_or(&MISSING_CHAR_CP437); + grid.set(x, y, converted); + } + } + + grid + } + } + + impl From<&str> for CharGrid { + fn from(value: &str) -> Self { + let value = value.replace("\r\n", "\n"); + let lines = value.split('\n').collect::>(); + let width = + lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len())); + + let mut grid = Self::new(width, lines.len()); + for (y, line) in lines.iter().enumerate() { + for (x, char) in line.chars().enumerate() { + grid.set(x, y, char); + } + } + + grid + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -98,3 +205,17 @@ mod tests { assert_eq!(actual, expected); } } + +#[cfg(test)] +#[cfg(feature = "cp437")] +mod tests_feature_cp437 { + use crate::{CharGrid, Cp437Grid}; + + #[test] + fn round_trip_cp437() { + let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']); + let cp437 = Cp437Grid::from(&utf8); + let actual = CharGrid::from(&cp437); + assert_eq!(actual, utf8); + } +} diff --git a/crates/servicepoint/src/lib.rs b/crates/servicepoint/src/lib.rs index 4ed440f..fa5564e 100644 --- a/crates/servicepoint/src/lib.rs +++ b/crates/servicepoint/src/lib.rs @@ -44,7 +44,7 @@ pub use crate::brightness::{Brightness, BrightnessGrid}; pub use crate::command::{Command, Offset}; pub use crate::compression_code::CompressionCode; pub use crate::connection::Connection; -pub use crate::cp437::Cp437Grid; +pub use crate::cp437::{CharGrid, Cp437Grid}; pub use crate::data_ref::DataRef; pub use crate::grid::Grid; pub use crate::origin::{Origin, Pixels, Tiles}; diff --git a/crates/servicepoint/src/origin.rs b/crates/servicepoint/src/origin.rs index 16ba083..6c0f5d2 100644 --- a/crates/servicepoint/src/origin.rs +++ b/crates/servicepoint/src/origin.rs @@ -12,6 +12,13 @@ pub struct Origin { } impl Origin { + /// Top-left. Equivalent to `Origin::new(0, 0)`. + pub const ZERO: Self = Self { + x: 0, + y: 0, + phantom_data: PhantomData, + }; + /// Create a new [Origin] instance for the provided position. pub fn new(x: usize, y: usize) -> Self { Self { From 5f8f6914645f3b1e3761b29edf78f0d8f7520a22 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 18:38:43 +0200 Subject: [PATCH 12/15] add tests --- crates/servicepoint/src/brightness.rs | 34 +++++++++++++++++++++++++-- crates/servicepoint/src/packet.rs | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/servicepoint/src/brightness.rs b/crates/servicepoint/src/brightness.rs index 5401a9f..ad07aa6 100644 --- a/crates/servicepoint/src/brightness.rs +++ b/crates/servicepoint/src/brightness.rs @@ -77,8 +77,8 @@ impl From for Vec { } } -impl From for PrimitiveGrid { - fn from(value: PrimitiveGrid) -> Self { +impl From<&BrightnessGrid> for PrimitiveGrid { + fn from(value: &PrimitiveGrid) -> Self { let u8s = value .iter() .map(|brightness| (*brightness).into()) @@ -109,3 +109,33 @@ impl Distribution for Standard { Brightness(rng.gen_range(Brightness::MIN.0..=Brightness::MAX.0)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::DataRef; + + #[test] + fn brightness_from_u8() { + assert_eq!(Err(100), Brightness::try_from(100)); + assert_eq!(Ok(Brightness(1)), Brightness::try_from(1)) + } + + #[test] + #[cfg(feature = "rand")] + fn rand_brightness() { + let mut rng = rand::thread_rng(); + for _ in 0..100 { + let _: Brightness = rng.gen(); + } + } + + #[test] + fn to_u8_grid() { + let mut grid = BrightnessGrid::new(2, 2); + grid.set(1, 0, Brightness::MIN); + grid.set(0, 1, Brightness::MAX); + let actual = PrimitiveGrid::from(&grid); + assert_eq!(actual.data_ref(), &[11, 0, 11, 11]); + } +} diff --git a/crates/servicepoint/src/packet.rs b/crates/servicepoint/src/packet.rs index a8f0831..e53a7a1 100644 --- a/crates/servicepoint/src/packet.rs +++ b/crates/servicepoint/src/packet.rs @@ -331,7 +331,7 @@ mod tests { payload: vec![42u8; 23], }; let data: Vec = p.into(); - let p = Packet::try_from(&*data).unwrap(); + let p = Packet::try_from(data).unwrap(); assert_eq!( p, Packet { From 366aec054f874fe5e382e7e295de13eae7cae40a Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 19:33:49 +0200 Subject: [PATCH 13/15] close connection on drop --- crates/servicepoint/Cargo.toml | 4 ++++ crates/servicepoint/examples/websocket.rs | 27 +++++++++++++++++++++++ crates/servicepoint/src/connection.rs | 10 ++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 crates/servicepoint/examples/websocket.rs diff --git a/crates/servicepoint/Cargo.toml b/crates/servicepoint/Cargo.toml index f66a607..20ae963 100644 --- a/crates/servicepoint/Cargo.toml +++ b/crates/servicepoint/Cargo.toml @@ -43,6 +43,10 @@ required-features = ["rand"] name = "game_of_life" required-features = ["rand"] +[[example]] +name = "websocket" +required-features = ["protocol_websocket"] + [dev-dependencies] # for examples clap = { version = "4.5", features = ["derive"] } diff --git a/crates/servicepoint/examples/websocket.rs b/crates/servicepoint/examples/websocket.rs new file mode 100644 index 0000000..71be6a3 --- /dev/null +++ b/crates/servicepoint/examples/websocket.rs @@ -0,0 +1,27 @@ +//! Example for how to use the WebSocket connection + +use servicepoint::{ + Command, CompressionCode, Connection, Grid, Origin, PixelGrid, +}; + +fn main() { + // make connection mut + let mut connection = + Connection::open_websocket("ws://localhost:8080".parse().unwrap()) + .unwrap(); + + // use send_mut instead of send + connection.send_mut(Command::Clear).unwrap(); + + let mut pixels = PixelGrid::max_sized(); + pixels.fill(true); + + // use send_mut instead of send + connection + .send_mut(Command::BitmapLinearWin( + Origin::ZERO, + pixels, + CompressionCode::Lzma, + )) + .unwrap(); +} diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index dd2706a..c12f341 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -111,7 +111,6 @@ impl Connection { let request = ClientRequestBuilder::new(uri).into_client_request()?; let (sock, _) = connect(request)?; - Ok(Self::WebSocket(sock)) } @@ -208,6 +207,15 @@ impl Connection { } } +impl Drop for Connection { + fn drop(&mut self) { + #[cfg(feature = "protocol_websocket")] + if let Connection::WebSocket(sock) = self { + _ = sock.close(None); + } + } +} + #[cfg(test)] mod tests { use super::*; From 9b618d4f35200b4f8119a68631ea976cd3e06712 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 19:44:17 +0200 Subject: [PATCH 14/15] bump version to 0.9.0, cleanup readme --- Cargo.lock | 6 +-- Cargo.toml | 2 +- crates/servicepoint/README.md | 38 ++++++++++++------- crates/servicepoint_binding_c/Cargo.toml | 2 +- crates/servicepoint_binding_cs/Cargo.toml | 4 +- .../ServicePoint/ServicePoint.csproj | 2 +- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a84fab..038deb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "servicepoint" -version = "0.8.0" +version = "0.9.0" dependencies = [ "bitvec", "bzip2", @@ -626,7 +626,7 @@ dependencies = [ [[package]] name = "servicepoint_binding_c" -version = "0.8.0" +version = "0.9.0" dependencies = [ "cbindgen", "servicepoint", @@ -634,7 +634,7 @@ dependencies = [ [[package]] name = "servicepoint_binding_cs" -version = "0.8.0" +version = "0.9.0" dependencies = [ "csbindgen", "servicepoint", diff --git a/Cargo.toml b/Cargo.toml index 9efa925..9c0b7a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "0.8.0" +version = "0.9.0" [workspace.lints.rust] missing-docs = "warn" diff --git a/crates/servicepoint/README.md b/crates/servicepoint/README.md index daed5f3..71b4df7 100644 --- a/crates/servicepoint/README.md +++ b/crates/servicepoint/README.md @@ -9,6 +9,17 @@ In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wa Display" or "Airport Display". This crate contains a library for parsing, encoding and sending packets to this display via UDP. +## Installation + +```bash +cargo add servicepoint +``` +or +```toml +[dependencies] +servicepoint = "0.9.0" +``` + ## Examples ```rust @@ -23,7 +34,7 @@ fn main() { } ``` -More examples are available in the crate. +More examples are available in the crate. Execute `cargo run --example` for a list of available examples and `cargo run --example ` to run one. ## Note on stability @@ -32,22 +43,21 @@ This library is still in early development. You can absolutely use it, and it works, but expect minor breaking changes with every version bump. Please specify the full version including patch in your Cargo.toml until 1.0 is released. -## Installation - -```bash -cargo add servicepoint -``` - ## Features -This library has multiple compression libraries as optional dependencies. -If you do not need compression/decompression support you can disable those features. -In the likely case you only need one of them, you can include that one specifically. +This library has multiple optional dependencies. +You can choose to (not) include them by toggling the related features. -```toml -[dependencies] -servicepoint = { version = "0.8.0", default-features = false, features = ["compression-bz"] } -``` +| Name | Default | Description | +|--------------------|---------|--------------------------------------------| +| compression_zlib | false | Enable additional compression algo | +| compression_bzip2 | false | Enable additional compression algo | +| compression_lzma | true | Enable additional compression algo | +| compression_zstd | false | Enable additional compression algo | +| protocol_udp | true | Connection::Udp | +| protocol_websocket | false | Connection::WebSocket | +| rand | false | impl Distribution for Standard | +| cp437 | true | Conversion to and from CP-437 | ## Everything else diff --git a/crates/servicepoint_binding_c/Cargo.toml b/crates/servicepoint_binding_c/Cargo.toml index 8b3ed7c..5a3c37d 100644 --- a/crates/servicepoint_binding_c/Cargo.toml +++ b/crates/servicepoint_binding_c/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] cbindgen = "0.27.0" [dependencies.servicepoint] -version = "0.8.0" +version = "0.9.0" path = "../servicepoint" features = ["all_compressions"] diff --git a/crates/servicepoint_binding_cs/Cargo.toml b/crates/servicepoint_binding_cs/Cargo.toml index 798292d..261364e 100644 --- a/crates/servicepoint_binding_cs/Cargo.toml +++ b/crates/servicepoint_binding_cs/Cargo.toml @@ -13,8 +13,8 @@ test = false csbindgen = "1.9.3" [dependencies] -servicepoint_binding_c = { version = "0.8.0", path = "../servicepoint_binding_c" } -servicepoint = { version = "0.8.0", path = "../servicepoint" } +servicepoint_binding_c = { version = "0.9.0", path = "../servicepoint_binding_c" } +servicepoint = { version = "0.9.0", path = "../servicepoint" } [lints] workspace = true diff --git a/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj b/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj index b1831d7..7168cd0 100644 --- a/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj +++ b/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj @@ -11,7 +11,7 @@ ServicePoint - 0.8.0 + 0.9.0 Repository Authors None ServicePoint From 55524a69e2b2edeabd9f87849f99d3dc6999cbc1 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 19:49:45 +0200 Subject: [PATCH 15/15] fix doctest, enable all features for docs.rs --- crates/servicepoint/src/command.rs | 4 ++-- crates/servicepoint/src/connection.rs | 2 +- crates/servicepoint_binding_c/Cargo.toml | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index 49c230d..abe983c 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -89,8 +89,8 @@ pub enum Command { /// # use servicepoint::{Command, Connection, Origin}; /// # let connection = Connection::Fake; /// use servicepoint::{CharGrid, Cp437Grid}; - /// let grid = CharGrid::from(&"Hello,\nWorld!"); - /// let grid = Cp437Grid::from(grid); + /// let grid = CharGrid::from("Hello,\nWorld!"); + /// let grid = Cp437Grid::from(&grid); /// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed"); /// ``` /// diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index c12f341..5d4e902 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -90,7 +90,7 @@ impl Connection { /// /// # Examples /// - /// ```rust + /// ```no_run /// use tungstenite::http::Uri; /// use servicepoint::{Command, Connection}; /// let uri = "ws://localhost:8080".parse().unwrap(); diff --git a/crates/servicepoint_binding_c/Cargo.toml b/crates/servicepoint_binding_c/Cargo.toml index 5a3c37d..96633a6 100644 --- a/crates/servicepoint_binding_c/Cargo.toml +++ b/crates/servicepoint_binding_c/Cargo.toml @@ -23,3 +23,6 @@ features = ["all_compressions"] [lints] workspace = true + +[package.metadata.docs.rs] +all-features = true