Command is now a trait
Some checks failed
Rust / build (pull_request) Failing after 1m4s

This commit is contained in:
Vinzenz Schroeter 2025-03-07 22:51:32 +01:00
commit c66e6db498
33 changed files with 1705 additions and 1196 deletions

View file

@ -28,7 +28,7 @@ fn main() {
.expect("connection failed");
// clear screen content
connection.send(Command::Clear)
connection.send(command::Clear)
.expect("send failed");
}
```

View file

@ -36,13 +36,16 @@ fn main() {
if cli.clear {
connection
.send(Command::Clear)
.send(command::Clear)
.expect("sending clear failed");
}
let text = cli.text.join("\n");
let grid = CharGrid::wrap_str(TILE_WIDTH, &text);
let command = command::Utf8Data {
origin: Origin::ZERO,
grid: CharGrid::wrap_str(TILE_WIDTH, &text),
};
connection
.send(Command::Utf8Data(Origin::ZERO, grid))
.send(command)
.expect("sending text failed");
}

View file

@ -14,14 +14,14 @@ fn main() {
let connection = connection::Udp::open(cli.destination)
.expect("could not connect to display");
let mut pixels = Bitmap::max_sized();
pixels.fill(true);
let mut bitmap = Bitmap::max_sized();
bitmap.fill(true);
let command = Command::BitmapLinearWin(
Origin::ZERO,
pixels,
CompressionCode::default(),
);
let command = command::BitmapLinearWin {
origin: Origin::ZERO,
bitmap,
compression: CompressionCode::default(),
};
connection.send(command).expect("send failed");
let max_brightness: u8 = Brightness::MAX.into();
@ -31,7 +31,9 @@ fn main() {
*byte = Brightness::try_from(level).unwrap();
}
connection
.send(Command::CharBrightness(Origin::ZERO, brightnesses))
.expect("send failed");
let command = command::CharBrightness {
origin: Origin::ZERO,
grid: brightnesses,
};
connection.send(command).expect("send failed");
}

View file

@ -22,11 +22,11 @@ fn main() {
let mut field = make_random_field(cli.probability);
loop {
let command = Command::BitmapLinearWin(
Origin::ZERO,
field.clone(),
CompressionCode::default(),
);
let command = command::BitmapLinearWin {
origin: Origin::ZERO,
bitmap: field.clone(),
compression: CompressionCode::default(),
};
connection.send(command).expect("could not send");
thread::sleep(FRAME_PACING);
field = iteration(field);

View file

@ -14,19 +14,19 @@ fn main() {
let connection = connection::Udp::open(Cli::parse().destination)
.expect("could not connect to display");
let mut pixels = Bitmap::max_sized();
let mut bitmap = Bitmap::max_sized();
for x_offset in 0..usize::MAX {
pixels.fill(false);
bitmap.fill(false);
for y in 0..PIXEL_HEIGHT {
pixels.set((y + x_offset) % PIXEL_WIDTH, y, true);
bitmap.set((y + x_offset) % PIXEL_WIDTH, y, true);
}
let command = Command::BitmapLinearWin(
Origin::ZERO,
pixels.clone(),
CompressionCode::default(),
);
let command = command::BitmapLinearWin {
bitmap: bitmap.clone(),
compression: CompressionCode::default(),
origin: Origin::ZERO,
};
connection.send(command).expect("send failed");
thread::sleep(FRAME_PACING);
}

View file

@ -4,7 +4,6 @@
use clap::Parser;
use rand::Rng;
use servicepoint::*;
use std::net::UdpSocket;
use std::time::Duration;
#[derive(Parser, Debug)]
@ -29,17 +28,17 @@ fn main() {
let mut filled_grid = Bitmap::max_sized();
filled_grid.fill(true);
let command = Command::BitmapLinearWin(
Origin::ZERO,
filled_grid,
CompressionCode::default(),
);
let command = command::BitmapLinearWin {
origin: Origin::ZERO,
bitmap: filled_grid,
compression: CompressionCode::default(),
};
connection.send(command).expect("send failed");
}
// set all pixels to the same random brightness
let mut rng = rand::thread_rng();
connection.send(Command::Brightness(rng.gen())).unwrap();
connection.send(rng.gen::<Brightness>()).unwrap();
// continuously update random windows to new random brightness
loop {
@ -60,7 +59,7 @@ fn main() {
}
connection
.send(Command::CharBrightness(origin, luma))
.send(command::CharBrightness { origin, grid: luma })
.unwrap();
std::thread::sleep(wait_duration);
}

View file

@ -1,24 +1,21 @@
//! Example for how to use the WebSocket connection
use servicepoint::connection::Websocket;
use servicepoint::{
Bitmap, Command, CompressionCode, Connection, Grid, Origin,
};
use servicepoint::*;
fn main() {
let connection =
Websocket::open("ws://localhost:8080".parse().unwrap()).unwrap();
let uri = "ws://localhost:8080".parse().unwrap();
let connection = Websocket::open(uri).unwrap();
connection.send(Command::Clear).unwrap();
connection.send(command::Clear).unwrap();
let mut pixels = Bitmap::max_sized();
pixels.fill(true);
connection
.send(Command::BitmapLinearWin(
Origin::ZERO,
pixels,
CompressionCode::default(),
))
.unwrap();
let command = command::BitmapLinearWin {
origin: Origin::ZERO,
bitmap: pixels,
compression: CompressionCode::default(),
};
connection.send(command).unwrap();
}

View file

@ -32,12 +32,13 @@ fn main() {
enabled_pixels.set(x_offset % PIXEL_WIDTH, y, false);
}
let command = command::BitmapLinearWin {
origin: Origin::ZERO,
bitmap: enabled_pixels.clone(),
compression: CompressionCode::default(),
};
connection
.send(Command::BitmapLinearWin(
Origin::ZERO,
enabled_pixels.clone(),
CompressionCode::default(),
))
.send(command)
.expect("could not send command to display");
thread::sleep(sleep_duration);
}

View file

@ -15,7 +15,7 @@ use rand::{
///
/// let b = Brightness::try_from(7).unwrap();
/// # let connection = connection::Fake;
/// let result = connection.send(Command::Brightness(b));
/// let result = connection.send(b);
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Brightness(u8);

View file

@ -8,13 +8,16 @@ use crate::ByteGrid;
/// # Examples
///
/// ```rust
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Connection, Grid, Origin, connection};
/// # use servicepoint::*;
/// let mut grid = BrightnessGrid::new(2,2);
/// grid.set(0, 0, Brightness::MIN);
/// grid.set(1, 1, Brightness::MIN);
///
/// # let connection = connection::Fake;
/// connection.send(Command::CharBrightness(Origin::new(3, 7), grid)).unwrap()
/// connection.send(command::CharBrightness {
/// origin: Origin::new(3, 7),
/// grid
/// }).unwrap()
/// ```
pub type BrightnessGrid = ValueGrid<Brightness>;

View file

@ -10,12 +10,13 @@ use std::string::FromUtf8Error;
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, CharGrid, Command, Connection, Origin};
/// # use servicepoint::*;
/// let grid = CharGrid::from("You can\nload multiline\nstrings directly");
/// assert_eq!(grid.get_row_str(1), Some("load multiline\0\0".to_string()));
///
/// # let connection = connection::Fake;
/// let command = Command::Utf8Data(Origin::ZERO, grid);
/// let command = command::Utf8Data { origin: Origin::ZERO, grid };
/// connection.send(command).unwrap()
/// ```
pub type CharGrid = ValueGrid<char>;

View file

@ -1,968 +0,0 @@
use crate::command_code::CommandCode;
use crate::compression::into_decompressed;
use crate::*;
/// Type alias for documenting the meaning of the u16 in enum values
pub type Offset = usize;
/// A low-level display command.
///
/// This struct and associated functions implement the UDP protocol for the display.
///
/// 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::default].
///
/// 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::{connection, Brightness, Command, Connection, Packet};
/// #
/// // create command
/// let command = Command::Brightness(Brightness::MAX);
///
/// // turn command into Packet
/// let packet: Packet = command.clone().into();
///
/// // read command from packet
/// let round_tripped = Command::try_from(packet).unwrap();
///
/// // round tripping produces exact copy
/// assert_eq!(command, round_tripped);
///
/// // send command
/// # let connection = connection::Fake;
/// connection.send(command).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
/// Set all pixels to the off state. Does not affect brightness.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, Command, Connection};
/// # let connection = connection::Fake;
/// connection.send(Command::Clear).unwrap();
/// ```
Clear,
/// Show text on the screen.
///
/// The text is sent in the form of a 2D grid of UTF-8 encoded characters (the default encoding in rust).
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = connection::Fake;
/// let grid = CharGrid::from("Hello,\nWorld!");
/// connection.send(Command::Utf8Data(Origin::ZERO, grid)).expect("send failed");
/// ```
Utf8Data(Origin<Tiles>, CharGrid),
/// Show text on the screen.
///
/// The text is sent in the form of a 2D grid of [CP-437] encoded characters.
///
/// <div class="warning">You probably want to use [Command::Utf8Data] instead</div>
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, Connection, Origin, CharGrid, Cp437Grid, connection};
/// # let connection = connection::Fake;
/// let grid = CharGrid::from("Hello,\nWorld!");
/// let grid = Cp437Grid::from(&grid);
/// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed");
/// ```
///
/// ```rust
/// # use servicepoint::{connection, Command, Connection, Cp437Grid, Origin};
/// # let connection = connection::Fake;
/// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
/// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
/// ```
/// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437
Cp437Data(Origin<Tiles>, Cp437Grid),
/// Overwrites a rectangular region of pixels.
///
/// Origin coordinates must be divisible by 8.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, CompressionCode, Grid, Bitmap, Connection};
/// # let connection = servicepoint::connection::Fake;
/// #
/// let mut pixels = Bitmap::max_sized();
/// // draw something to the pixels here
/// # pixels.set(2, 5, true);
///
/// // create command to send pixels
/// let command = Command::BitmapLinearWin(
/// servicepoint::Origin::ZERO,
/// pixels,
/// CompressionCode::default()
/// );
///
/// connection.send(command).expect("send failed");
/// ```
BitmapLinearWin(Origin<Pixels>, Bitmap, CompressionCode),
/// Set the brightness of all tiles to the same value.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, Brightness, Command, Connection};
/// # let connection = connection::Fake;
/// let command = Command::Brightness(Brightness::MAX);
/// connection.send(command).unwrap();
/// ```
Brightness(Brightness),
/// Set the brightness of individual tiles in a rectangular area of the display.
CharBrightness(Origin<Tiles>, BrightnessGrid),
/// Set pixel data starting at the pixel offset on screen.
///
/// 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.
BitmapLinear(Offset, BitVec, CompressionCode),
/// Set pixel data according to an and-mask starting at the offset.
///
/// 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.
BitmapLinearAnd(Offset, BitVec, CompressionCode),
/// Set pixel data according to an or-mask starting at the offset.
///
/// 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.
BitmapLinearOr(Offset, BitVec, CompressionCode),
/// Set pixel data according to a xor-mask starting at the offset.
///
/// 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.
BitmapLinearXor(Offset, BitVec, CompressionCode),
/// Kills the udp daemon on the display, which usually results in a restart.
///
/// Please do not send this in your normal program flow.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, Command, Connection};
/// # let connection = connection::Fake;
/// connection.send(Command::HardReset).unwrap();
/// ```
HardReset,
/// <div class="warning">Untested</div>
///
/// Slowly decrease brightness until off or something like that?
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, Command, Connection};
/// # let connection = connection::Fake;
/// connection.send(Command::FadeOut).unwrap();
/// ```
FadeOut,
/// Legacy command code, gets ignored by the real display.
///
/// Might be useful as a noop package.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, Command, Connection};
/// # let connection = connection::Fake;
/// // this sends a packet that does nothing
/// # #[allow(deprecated)]
/// connection.send(Command::BitmapLegacy).unwrap();
/// ```
#[deprecated]
BitmapLegacy,
}
/// Err values for [Command::try_from].
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum TryFromPacketError {
/// the contained command code does not correspond to a known command
#[error("The command code {0:?} does not correspond to a known command")]
InvalidCommand(u16),
/// the expected payload size was n, but size m was found
#[error("the expected payload size was {0}, but size {1} was found")]
UnexpectedPayloadSize(usize, usize),
/// Header fields not needed for the command have been used.
///
/// Note that these commands would usually still work on the actual display.
#[error("Header fields not needed for the command have been used")]
ExtraneousHeaderValues,
/// The contained compression code is not known. This could be of disabled features.
#[error("The compression code {0:?} does not correspond to a known compression algorithm.")]
InvalidCompressionCode(u16),
/// Decompression of the payload failed. This can be caused by corrupted packets.
#[error("The decompression of the payload failed")]
DecompressionFailed,
/// The given brightness value is out of bounds
#[error("The given brightness value {0} is out of bounds.")]
InvalidBrightness(u8),
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
}
impl TryFrom<Packet> for Command {
type Error = TryFromPacketError;
/// Try to interpret the [Packet] as one containing a [Command]
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header: Header {
command_code, a, ..
},
..
} = packet;
let command_code = match CommandCode::try_from(command_code) {
Err(()) => {
return Err(TryFromPacketError::InvalidCommand(command_code));
}
Ok(value) => value,
};
match command_code {
CommandCode::Clear => {
Self::packet_into_command_only(packet, Command::Clear)
}
CommandCode::Brightness => Self::packet_into_brightness(&packet),
CommandCode::HardReset => {
Self::packet_into_command_only(packet, Command::HardReset)
}
CommandCode::FadeOut => {
Self::packet_into_command_only(packet, Command::FadeOut)
}
CommandCode::Cp437Data => Self::packet_into_cp437(&packet),
CommandCode::CharBrightness => {
Self::packet_into_char_brightness(&packet)
}
CommandCode::Utf8Data => Self::packet_into_utf8(&packet),
#[allow(deprecated)]
CommandCode::BitmapLegacy => Ok(Command::BitmapLegacy),
CommandCode::BitmapLinear => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinear(a as Offset, vec, compression))
}
CommandCode::BitmapLinearAnd => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinearAnd(a as Offset, vec, compression))
}
CommandCode::BitmapLinearOr => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinearOr(a as Offset, vec, compression))
}
CommandCode::BitmapLinearXor => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinearXor(a as Offset, vec, compression))
}
CommandCode::BitmapLinearWinUncompressed => {
Self::packet_into_bitmap_win(
packet,
CompressionCode::Uncompressed,
)
}
#[cfg(feature = "compression_zlib")]
CommandCode::BitmapLinearWinZlib => {
Self::packet_into_bitmap_win(packet, CompressionCode::Zlib)
}
#[cfg(feature = "compression_bzip2")]
CommandCode::BitmapLinearWinBzip2 => {
Self::packet_into_bitmap_win(packet, CompressionCode::Bzip2)
}
#[cfg(feature = "compression_lzma")]
CommandCode::BitmapLinearWinLzma => {
Self::packet_into_bitmap_win(packet, CompressionCode::Lzma)
}
#[cfg(feature = "compression_zstd")]
CommandCode::BitmapLinearWinZstd => {
Self::packet_into_bitmap_win(packet, CompressionCode::Zstd)
}
}
}
}
impl Command {
fn packet_into_bitmap_win(
packet: Packet,
compression: CompressionCode,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a: tiles_x,
b: pixels_y,
c: tile_w,
d: pixel_h,
},
payload,
} = packet;
let payload = match into_decompressed(compression, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(decompressed) => decompressed,
};
Ok(Command::BitmapLinearWin(
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize),
Bitmap::load(
tile_w as usize * TILE_SIZE,
pixel_h as usize,
&payload,
),
compression,
))
}
/// Helper method for checking that a packet is empty and only contains a command code
fn packet_into_command_only(
packet: Packet,
command: Command,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
if !payload.is_empty() {
Err(TryFromPacketError::UnexpectedPayloadSize(0, payload.len()))
} else if a != 0 || b != 0 || c != 0 || d != 0 {
Err(TryFromPacketError::ExtraneousHeaderValues)
} else {
Ok(command)
}
}
/// Helper method for Packets into `BitmapLinear*`-Commands
fn packet_into_linear_bitmap(
packet: Packet,
) -> Result<(BitVec, CompressionCode), TryFromPacketError> {
let Packet {
header:
Header {
b: length,
c: sub,
d: reserved,
..
},
payload,
} = packet;
if reserved != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
let sub = match CompressionCode::try_from(sub) {
Err(()) => {
return Err(TryFromPacketError::InvalidCompressionCode(sub));
}
Ok(value) => value,
};
let payload = match into_decompressed(sub, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(value) => value,
};
if payload.len() != length as usize {
return Err(TryFromPacketError::UnexpectedPayloadSize(
length as usize,
payload.len(),
));
}
Ok((BitVec::from_vec(payload), sub))
}
fn packet_into_char_brightness(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a: x,
b: y,
c: width,
d: height,
},
payload,
} = packet;
let grid = ByteGrid::load(*width as usize, *height as usize, payload);
let grid = match BrightnessGrid::try_from(grid) {
Ok(grid) => grid,
Err(val) => return Err(TryFromPacketError::InvalidBrightness(val)),
};
Ok(Command::CharBrightness(
Origin::new(*x as usize, *y as usize),
grid,
))
}
fn packet_into_brightness(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
if payload.len() != 1 {
return Err(TryFromPacketError::UnexpectedPayloadSize(
1,
payload.len(),
));
}
if *a != 0 || *b != 0 || *c != 0 || *d != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
match Brightness::try_from(payload[0]) {
Ok(b) => Ok(Command::Brightness(b)),
Err(_) => Err(TryFromPacketError::InvalidBrightness(payload[0])),
}
}
fn packet_into_cp437(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
Ok(Command::Cp437Data(
Origin::new(*a as usize, *b as usize),
Cp437Grid::load(*c as usize, *d as usize, payload),
))
}
fn packet_into_utf8(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
let payload: Vec<_> =
String::from_utf8(payload.clone())?.chars().collect();
Ok(Command::Utf8Data(
Origin::new(*a as usize, *b as usize),
CharGrid::load(*c as usize, *d as usize, &payload),
))
}
}
#[cfg(test)]
mod tests {
use crate::command::TryFromPacketError;
use crate::command_code::CommandCode;
use crate::{
BitVec, Bitmap, Brightness, BrightnessGrid, CharGrid, Command,
CompressionCode, Cp437Grid, Header, Origin, Packet, Pixels,
};
fn round_trip(original: Command) {
let packet: Packet = original.clone().into();
let copy: Command = match Command::try_from(packet) {
Ok(command) => command,
Err(err) => panic!("could not reload {original:?}: {err:?}"),
};
assert_eq!(copy, original);
}
fn all_compressions<'t>() -> &'t [CompressionCode] {
&[
CompressionCode::Uncompressed,
#[cfg(feature = "compression_lzma")]
CompressionCode::Lzma,
#[cfg(feature = "compression_bzip2")]
CompressionCode::Bzip2,
#[cfg(feature = "compression_zlib")]
CompressionCode::Zlib,
#[cfg(feature = "compression_zstd")]
CompressionCode::Zstd,
]
}
#[test]
fn round_trip_clear() {
round_trip(Command::Clear);
}
#[test]
fn round_trip_hard_reset() {
round_trip(Command::HardReset);
}
#[test]
fn round_trip_fade_out() {
round_trip(Command::FadeOut);
}
#[test]
fn round_trip_brightness() {
round_trip(Command::Brightness(Brightness::try_from(6).unwrap()));
}
#[test]
#[allow(deprecated)]
fn round_trip_bitmap_legacy() {
round_trip(Command::BitmapLegacy);
}
#[test]
fn round_trip_char_brightness() {
round_trip(Command::CharBrightness(
Origin::new(5, 2),
BrightnessGrid::new(7, 5),
));
}
#[test]
fn round_trip_cp437_data() {
round_trip(Command::Cp437Data(Origin::new(5, 2), Cp437Grid::new(7, 5)));
}
#[test]
fn round_trip_utf8_data() {
round_trip(Command::Utf8Data(Origin::new(5, 2), CharGrid::new(7, 5)));
}
#[test]
fn round_trip_bitmap_linear() {
for compression in all_compressions().iter().copied() {
round_trip(Command::BitmapLinear(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearAnd(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearOr(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearXor(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearWin(
Origin::ZERO,
Bitmap::max_sized(),
compression,
));
}
}
#[test]
fn error_invalid_command() {
let p = Packet {
header: Header {
command_code: 0xFF,
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::InvalidCommand(0xFF))
))
}
#[test]
fn error_extraneous_header_values_clear() {
let p = Packet {
header: Header {
command_code: CommandCode::Clear.into(),
a: 0x05,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_extraneous_header_values_brightness() {
let p = Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0x00,
b: 0x13,
c: 0x37,
d: 0x00,
},
payload: vec![5],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_extraneous_header_hard_reset() {
let p = Packet {
header: Header {
command_code: CommandCode::HardReset.into(),
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x01,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_extraneous_header_fade_out() {
let p = Packet {
header: Header {
command_code: CommandCode::FadeOut.into(),
a: 0x10,
b: 0x00,
c: 0x00,
d: 0x01,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_unexpected_payload() {
let p = Packet {
header: Header {
command_code: CommandCode::FadeOut.into(),
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![5, 7],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::UnexpectedPayloadSize(0, 2))
))
}
#[test]
fn error_decompression_failed_win() {
for compression in all_compressions().iter().copied() {
let p: Packet = Command::BitmapLinearWin(
Origin::new(16, 8),
Bitmap::new(8, 8),
compression,
)
.into();
let Packet {
header,
mut payload,
} = p;
// mangle it
for byte in payload.iter_mut() {
*byte -= *byte / 2;
}
let p = Packet { header, payload };
let result = Command::try_from(p);
if compression != CompressionCode::Uncompressed {
assert_eq!(result, Err(TryFromPacketError::DecompressionFailed))
} else {
assert!(result.is_ok());
}
}
}
#[test]
fn error_decompression_failed_and() {
for compression in all_compressions().iter().copied() {
let p: Packet = Command::BitmapLinearAnd(
0,
BitVec::repeat(false, 8),
compression,
)
.into();
let Packet {
header,
mut payload,
} = p;
// mangle it
for byte in payload.iter_mut() {
*byte -= *byte / 2;
}
let p = Packet { header, payload };
let result = Command::try_from(p);
if compression != CompressionCode::Uncompressed {
assert_eq!(result, Err(TryFromPacketError::DecompressionFailed))
} else {
// when not compressing, there is no way to detect corrupted data
assert!(result.is_ok());
}
}
}
#[test]
fn unexpected_payload_size_brightness() {
assert_eq!(
Command::try_from(Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0,
b: 0,
c: 0,
d: 0,
},
payload: vec!()
}),
Err(TryFromPacketError::UnexpectedPayloadSize(1, 0))
);
assert_eq!(
Command::try_from(Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0,
b: 0,
c: 0,
d: 0,
},
payload: vec!(0, 0)
}),
Err(TryFromPacketError::UnexpectedPayloadSize(1, 2))
);
}
#[test]
fn error_reserved_used() {
let Packet { header, payload } = Command::BitmapLinear(
0,
BitVec::repeat(false, 8),
CompressionCode::Uncompressed,
)
.into();
let Header {
command_code: command,
a: offset,
b: length,
c: sub,
d: _reserved,
} = header;
let p = Packet {
header: Header {
command_code: command,
a: offset,
b: length,
c: sub,
d: 69,
},
payload,
};
assert_eq!(
Command::try_from(p),
Err(TryFromPacketError::ExtraneousHeaderValues)
);
}
#[test]
fn error_invalid_compression() {
let Packet { header, payload } = Command::BitmapLinear(
0,
BitVec::repeat(false, 8),
CompressionCode::Uncompressed,
)
.into();
let Header {
command_code: command,
a: offset,
b: length,
c: _sub,
d: reserved,
} = header;
let p = Packet {
header: Header {
command_code: command,
a: offset,
b: length,
c: 42,
d: reserved,
},
payload,
};
assert_eq!(
Command::try_from(p),
Err(TryFromPacketError::InvalidCompressionCode(42))
);
}
#[test]
fn error_unexpected_size() {
let Packet { header, payload } = Command::BitmapLinear(
0,
BitVec::repeat(false, 8),
CompressionCode::Uncompressed,
)
.into();
let Header {
command_code: command,
a: offset,
b: length,
c: compression,
d: reserved,
} = header;
let p = Packet {
header: Header {
command_code: command,
a: offset,
b: 420,
c: compression,
d: reserved,
},
payload,
};
assert_eq!(
Command::try_from(p),
Err(TryFromPacketError::UnexpectedPayloadSize(
420,
length as usize,
))
);
}
#[test]
fn origin_add() {
assert_eq!(
Origin::<Pixels>::new(4, 2),
Origin::new(1, 0) + Origin::new(3, 2)
);
}
#[test]
fn packet_into_char_brightness_invalid() {
let grid = BrightnessGrid::new(2, 2);
let command = Command::CharBrightness(Origin::ZERO, grid);
let mut packet: Packet = command.into();
let slot = packet.payload.get_mut(1).unwrap();
*slot = 23;
assert_eq!(
Command::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(23))
);
}
#[test]
fn packet_into_brightness_invalid() {
let mut packet: Packet = Command::Brightness(Brightness::MAX).into();
let slot = packet.payload.get_mut(0).unwrap();
*slot = 42;
assert_eq!(
Command::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(42))
);
}
}

View file

@ -0,0 +1,51 @@
use crate::{
command::check_command_code_only, command::TryFromPacketError,
command_code::CommandCode, Packet, TypedCommand,
};
use std::fmt::Debug;
/// Legacy command code, gets ignored by the real display.
///
/// Might be useful as a noop package.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = connection::Fake;
/// // this sends a packet that does nothing
/// # #[allow(deprecated)]
/// connection.send(command::BitmapLegacy).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
#[deprecated]
pub struct BitmapLegacy;
#[allow(deprecated)]
impl TryFrom<Packet> for BitmapLegacy {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
if let Some(e) =
check_command_code_only(value, CommandCode::BitmapLegacy)
{
Err(e)
} else {
Ok(Self)
}
}
}
#[allow(deprecated)]
impl From<BitmapLegacy> for Packet {
fn from(_: BitmapLegacy) -> Self {
Packet::command_code_only(CommandCode::BitmapLegacy)
}
}
#[allow(deprecated)]
impl From<BitmapLegacy> for TypedCommand {
fn from(command: BitmapLegacy) -> Self {
Self::BitmapLegacy(command)
}
}

View file

@ -0,0 +1,91 @@
use crate::{
command::TryFromPacketError, command_code::CommandCode,
compression::into_decompressed, BitVec, CompressionCode, Header, Offset,
Packet, TypedCommand,
};
/// Set pixel data starting at the pixel offset on screen.
///
/// 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.
#[derive(Clone, PartialEq, Debug)]
pub struct BitmapLinear {
/// where to start overwriting pixel data
pub offset: Offset,
/// the pixels to send to the display as one long row
pub bitvec: BitVec,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl From<BitmapLinear> for Packet {
fn from(bitmap: BitmapLinear) -> Self {
Packet::bitmap_linear_into_packet(
CommandCode::BitmapLinear,
bitmap.offset,
bitmap.compression,
bitmap.bitvec.into(),
)
}
}
impl TryFrom<Packet> for BitmapLinear {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let (offset, bitvec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Self {
offset,
bitvec,
compression,
})
}
}
impl From<BitmapLinear> for TypedCommand {
fn from(command: BitmapLinear) -> Self {
Self::BitmapLinear(command)
}
}
impl BitmapLinear {
/// Helper method for Packets into `BitmapLinear*`-Commands
pub(crate) fn packet_into_linear_bitmap(
packet: Packet,
) -> Result<(Offset, BitVec, CompressionCode), TryFromPacketError> {
let Packet {
header:
Header {
a: offset,
b: length,
c: sub,
d: reserved,
..
},
payload,
} = packet;
if reserved != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
let sub = match CompressionCode::try_from(sub) {
Err(()) => {
return Err(TryFromPacketError::InvalidCompressionCode(sub));
}
Ok(value) => value,
};
let payload = match into_decompressed(sub, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(value) => value,
};
if payload.len() != length as usize {
return Err(TryFromPacketError::UnexpectedPayloadSize(
length as usize,
payload.len(),
));
}
Ok((offset as Offset, BitVec::from_vec(payload), sub))
}
}

View file

@ -0,0 +1,52 @@
use crate::{
command::{BitmapLinear, TryFromPacketError},
command_code::CommandCode,
BitVec, CompressionCode, Offset, Packet, TypedCommand,
};
/// Set pixel data according to an and-mask starting at the offset.
///
/// 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.
#[derive(Clone, PartialEq, Debug)]
pub struct BitmapLinearAnd {
/// where to start overwriting pixel data
pub offset: Offset,
/// the pixels to send to the display as one long row
pub bitvec: BitVec,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl TryFrom<Packet> for BitmapLinearAnd {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let (offset, bitvec, compression) =
BitmapLinear::packet_into_linear_bitmap(packet)?;
Ok(Self {
offset,
bitvec,
compression,
})
}
}
impl From<BitmapLinearAnd> for Packet {
fn from(bitmap: BitmapLinearAnd) -> Self {
Packet::bitmap_linear_into_packet(
CommandCode::BitmapLinearAnd,
bitmap.offset,
bitmap.compression,
bitmap.bitvec.into(),
)
}
}
impl From<BitmapLinearAnd> for TypedCommand {
fn from(command: BitmapLinearAnd) -> Self {
Self::BitmapLinearAnd(command)
}
}

View file

@ -0,0 +1,52 @@
use crate::{
command::{BitmapLinear, TryFromPacketError},
command_code::CommandCode,
BitVec, CompressionCode, Offset, Packet, TypedCommand,
};
/// Set pixel data according to an or-mask starting at the offset.
///
/// 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.
#[derive(Clone, PartialEq, Debug)]
pub struct BitmapLinearOr {
/// where to start overwriting pixel data
pub offset: Offset,
/// the pixels to send to the display as one long row
pub bitvec: BitVec,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl TryFrom<Packet> for BitmapLinearOr {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let (offset, bitvec, compression) =
BitmapLinear::packet_into_linear_bitmap(packet)?;
Ok(Self {
offset,
bitvec,
compression,
})
}
}
impl From<BitmapLinearOr> for Packet {
fn from(bitmap: BitmapLinearOr) -> Self {
Packet::bitmap_linear_into_packet(
CommandCode::BitmapLinearOr,
bitmap.offset,
bitmap.compression,
bitmap.bitvec.into(),
)
}
}
impl From<BitmapLinearOr> for TypedCommand {
fn from(command: BitmapLinearOr) -> Self {
Self::BitmapLinearOr(command)
}
}

View file

@ -0,0 +1,157 @@
use crate::{
command::TryFromPacketError, command_code::CommandCode,
compression::into_compressed, compression::into_decompressed, Bitmap,
CompressionCode, Grid, Header, Origin, Packet, Pixels, TypedCommand,
TILE_SIZE,
};
/// Overwrites a rectangular region of pixels.
///
/// Origin coordinates must be divisible by 8.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = connection::Fake;
/// #
/// let mut bitmap = Bitmap::max_sized();
/// // draw something to the pixels here
/// # bitmap.set(2, 5, true);
///
/// // create command to send pixels
/// let command = command::BitmapLinearWin {
/// bitmap,
/// origin: Origin::ZERO,
/// compression: CompressionCode::Uncompressed
/// };
///
/// connection.send(command).expect("send failed");
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct BitmapLinearWin {
/// where to start drawing the pixels
pub origin: Origin<Pixels>,
/// the pixels to send
pub bitmap: Bitmap,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl From<BitmapLinearWin> for Packet {
fn from(value: BitmapLinearWin) -> Self {
assert_eq!(value.origin.x % 8, 0);
assert_eq!(value.bitmap.width() % 8, 0);
let tile_x = (value.origin.x / TILE_SIZE) as u16;
let tile_w = (value.bitmap.width() / TILE_SIZE) as u16;
let pixel_h = value.bitmap.height() as u16;
let payload = into_compressed(value.compression, value.bitmap.into());
let command = match value.compression {
CompressionCode::Uncompressed => {
CommandCode::BitmapLinearWinUncompressed
}
#[cfg(feature = "compression_zlib")]
CompressionCode::Zlib => CommandCode::BitmapLinearWinZlib,
#[cfg(feature = "compression_bzip2")]
CompressionCode::Bzip2 => CommandCode::BitmapLinearWinBzip2,
#[cfg(feature = "compression_lzma")]
CompressionCode::Lzma => CommandCode::BitmapLinearWinLzma,
#[cfg(feature = "compression_zstd")]
CompressionCode::Zstd => CommandCode::BitmapLinearWinZstd,
};
Packet {
header: Header {
command_code: command.into(),
a: tile_x,
b: value.origin.y as u16,
c: tile_w,
d: pixel_h,
},
payload,
}
}
}
impl TryFrom<Packet> for BitmapLinearWin {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let code = CommandCode::try_from(packet.header.command_code).map_err(
|_| TryFromPacketError::InvalidCommand(packet.header.command_code),
)?;
match code {
CommandCode::BitmapLinearWinUncompressed => {
Self::packet_into_bitmap_win(
packet,
CompressionCode::Uncompressed,
)
}
#[cfg(feature = "compression_zlib")]
CommandCode::BitmapLinearWinZlib => {
Self::packet_into_bitmap_win(packet, CompressionCode::Zlib)
}
#[cfg(feature = "compression_bzip2")]
CommandCode::BitmapLinearWinBzip2 => {
Self::packet_into_bitmap_win(packet, CompressionCode::Bzip2)
}
#[cfg(feature = "compression_lzma")]
CommandCode::BitmapLinearWinLzma => {
Self::packet_into_bitmap_win(packet, CompressionCode::Lzma)
}
#[cfg(feature = "compression_zstd")]
CommandCode::BitmapLinearWinZstd => {
Self::packet_into_bitmap_win(packet, CompressionCode::Zstd)
}
_ => Err(TryFromPacketError::InvalidCommand(
packet.header.command_code,
)),
}
}
}
impl From<BitmapLinearWin> for TypedCommand {
fn from(command: BitmapLinearWin) -> Self {
Self::BitmapLinearWin(command)
}
}
impl BitmapLinearWin {
fn packet_into_bitmap_win(
packet: Packet,
compression: CompressionCode,
) -> Result<Self, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a: tiles_x,
b: pixels_y,
c: tile_w,
d: pixel_h,
},
payload,
} = packet;
let payload = match into_decompressed(compression, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(decompressed) => decompressed,
};
Ok(Self {
origin: Origin::new(
tiles_x as usize * TILE_SIZE,
pixels_y as usize,
),
bitmap: Bitmap::load(
tile_w as usize * TILE_SIZE,
pixel_h as usize,
&payload,
),
compression,
})
}
}

View file

@ -0,0 +1,52 @@
use crate::{
command::{BitmapLinear, TryFromPacketError},
command_code::CommandCode,
BitVec, CompressionCode, Offset, Packet, TypedCommand,
};
/// Set pixel data according to a xor-mask starting at the offset.
///
/// 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.
#[derive(Clone, PartialEq, Debug)]
pub struct BitmapLinearXor {
/// where to start overwriting pixel data
pub offset: Offset,
/// the pixels to send to the display as one long row
pub bitvec: BitVec,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl TryFrom<Packet> for BitmapLinearXor {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let (offset, bitvec, compression) =
BitmapLinear::packet_into_linear_bitmap(packet)?;
Ok(Self {
offset,
bitvec,
compression,
})
}
}
impl From<BitmapLinearXor> for Packet {
fn from(bitmap: BitmapLinearXor) -> Self {
Packet::bitmap_linear_into_packet(
CommandCode::BitmapLinearXor,
bitmap.offset,
bitmap.compression,
bitmap.bitvec.into(),
)
}
}
impl From<BitmapLinearXor> for TypedCommand {
fn from(command: BitmapLinearXor) -> Self {
Self::BitmapLinearXor(command)
}
}

View file

@ -0,0 +1,58 @@
use crate::{
command::TryFromPacketError, command_code::CommandCode, BrightnessGrid,
ByteGrid, Header, Origin, Packet, Tiles, TypedCommand,
};
/// Set the brightness of individual tiles in a rectangular area of the display.
#[derive(Clone, PartialEq, Debug)]
pub struct CharBrightness {
/// which tile the brightness rectangle should start
pub origin: Origin<Tiles>,
/// the brightness values per tile
pub grid: BrightnessGrid,
}
impl From<CharBrightness> for Packet {
fn from(value: CharBrightness) -> Self {
Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::CharBrightness,
)
}
}
impl TryFrom<Packet> for CharBrightness {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header:
Header {
command_code: _,
a: x,
b: y,
c: width,
d: height,
},
payload,
} = packet;
let grid = ByteGrid::load(width as usize, height as usize, &*payload);
let grid = match BrightnessGrid::try_from(grid) {
Ok(grid) => grid,
Err(val) => return Err(TryFromPacketError::InvalidBrightness(val)),
};
Ok(Self {
grid,
origin: Origin::new(x as usize, y as usize),
})
}
}
impl From<CharBrightness> for TypedCommand {
fn from(command: CharBrightness) -> Self {
Self::CharBrightness(command)
}
}

41
src/command/clear.rs Normal file
View file

@ -0,0 +1,41 @@
use crate::{
command::check_command_code_only, command::TryFromPacketError,
command_code::CommandCode, Packet, TypedCommand,
};
use std::fmt::Debug;
/// Set all pixels to the off state. Does not affect brightness.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{connection, Command, Connection, command};
/// # let connection = connection::Fake;
/// connection.send(command::Clear).unwrap();
#[derive(Debug, Clone, PartialEq)]
/// ```
pub struct Clear;
impl TryFrom<Packet> for Clear {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
if let Some(e) = check_command_code_only(value, CommandCode::Clear) {
Err(e)
} else {
Ok(Self)
}
}
}
impl From<Clear> for Packet {
fn from(_: Clear) -> Self {
Packet::command_code_only(CommandCode::Clear)
}
}
impl From<Clear> for TypedCommand {
fn from(command: Clear) -> Self {
Self::Clear(command)
}
}

Some files were not shown because too many files have changed in this diff Show more