into packet can fail

This commit is contained in:
Vinzenz Schroeter 2025-03-21 14:56:31 +01:00
parent 08ed6a6fee
commit 44fe6961e7
22 changed files with 345 additions and 188 deletions

View file

@ -1,8 +1,10 @@
use crate::{
command_code::CommandCode, commands::TryFromPacketError,
compression::into_compressed, compression::into_decompressed, Bitmap,
CompressionCode, Grid, Header, Origin, Packet, Pixels, TypedCommand,
TILE_SIZE,
command_code::CommandCode,
commands::errors::{TryFromPacketError, TryIntoPacketError},
compression::into_compressed,
compression::into_decompressed,
Bitmap, CompressionCode, Grid, Header, Origin, Packet, Pixels,
TypedCommand, TILE_SIZE,
};
/// Overwrites a rectangular region of pixels.
@ -28,7 +30,7 @@ use crate::{
///
/// connection.send(command).expect("send failed");
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BitmapCommand {
/// where to start drawing the pixels
pub origin: Origin<Pixels>,
@ -38,15 +40,18 @@ pub struct BitmapCommand {
pub compression: CompressionCode,
}
impl From<BitmapCommand> for Packet {
fn from(value: BitmapCommand) -> Self {
impl TryFrom<BitmapCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: BitmapCommand) -> Result<Self, Self::Error> {
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 tile_x = (value.origin.x / TILE_SIZE).try_into()?;
let tile_w = (value.bitmap.width() / TILE_SIZE).try_into()?;
let pixel_h = value.bitmap.height().try_into()?;
let payload = into_compressed(value.compression, value.bitmap.into())
.ok_or(TryIntoPacketError::CompressionFailed)?;
let command = match value.compression {
CompressionCode::Uncompressed => {
CommandCode::BitmapLinearWinUncompressed
@ -61,16 +66,16 @@ impl From<BitmapCommand> for Packet {
CompressionCode::Zstd => CommandCode::BitmapLinearWinZstd,
};
Packet {
Ok(Packet {
header: Header {
command_code: command.into(),
a: tile_x,
b: value.origin.y as u16,
b: value.origin.y.try_into()?,
c: tile_w,
d: pixel_h,
},
payload,
}
})
}
}
@ -162,8 +167,11 @@ impl BitmapCommand {
mod tests {
use super::*;
use crate::command_code::CommandCode;
use crate::commands::tests::TestImplementsCommand;
use crate::*;
impl TestImplementsCommand for BitmapCommand {}
#[test]
fn command_code() {
assert_eq!(
@ -188,7 +196,8 @@ mod tests {
bitmap: Bitmap::new(8, 8).unwrap(),
compression: *compression,
}
.into();
.try_into()
.unwrap();
let Packet {
header,

View file

@ -1,6 +1,6 @@
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::TryFromPacketError, Packet, TypedCommand,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
@ -17,7 +17,7 @@ use std::fmt::Debug;
/// # #[allow(deprecated)]
/// connection.send(BitmapLegacyCommand).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[deprecated]
pub struct BitmapLegacyCommand;
@ -54,8 +54,10 @@ impl From<BitmapLegacyCommand> for TypedCommand {
#[allow(deprecated)]
mod tests {
use super::*;
use crate::commands::tests::round_trip;
use crate::Header;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::{Command, Header, TryIntoPacket};
impl TestImplementsCommand for BitmapLegacyCommand {}
#[test]
fn invalid_fields() {

View file

@ -1,8 +1,7 @@
use crate::compression::into_compressed;
use crate::{
command_code::CommandCode, commands::TryFromPacketError,
compression::into_decompressed, BitVec, CompressionCode, Header, Offset,
Packet, TypedCommand,
command_code::CommandCode, commands::errors::TryFromPacketError,
compression::into_compressed, compression::into_decompressed, BitVec,
CompressionCode, Header, Offset, Packet, TryIntoPacketError, TypedCommand,
};
/// Binary operations for use with the [BitVecCommand] command.
@ -31,7 +30,7 @@ pub enum BinaryOperation {
/// For example, [BinaryOperation::Or] can be used to turn on some pixels without affecting other pixels.
///
/// The contained [BitVec] is always uncompressed.
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, PartialEq, Debug, Eq)]
pub struct BitVecCommand {
/// where to start overwriting pixel data
pub offset: Offset,
@ -43,28 +42,31 @@ pub struct BitVecCommand {
pub compression: CompressionCode,
}
impl From<BitVecCommand> for Packet {
fn from(command: BitVecCommand) -> Self {
let command_code = match command.operation {
impl TryFrom<BitVecCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: BitVecCommand) -> Result<Self, Self::Error> {
let command_code = match value.operation {
BinaryOperation::Overwrite => CommandCode::BitmapLinear,
BinaryOperation::And => CommandCode::BitmapLinearAnd,
BinaryOperation::Or => CommandCode::BitmapLinearOr,
BinaryOperation::Xor => CommandCode::BitmapLinearXor,
};
let payload: Vec<_> = command.bitvec.into();
let length = payload.len() as u16;
let payload = into_compressed(command.compression, payload);
Packet {
let payload: Vec<_> = value.bitvec.into();
let length = payload.len().try_into()?;
let payload = into_compressed(value.compression, payload)
.ok_or(TryIntoPacketError::CompressionFailed)?;
Ok(Packet {
header: Header {
command_code: command_code.into(),
a: command.offset as u16,
a: value.offset.try_into()?,
b: length,
c: command.compression.into(),
c: value.compression.into(),
d: 0,
},
payload,
}
})
}
}
@ -135,9 +137,11 @@ impl From<BitVecCommand> for TypedCommand {
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::tests::round_trip;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::{commands, Bitmap, BitmapCommand, Origin};
impl TestImplementsCommand for BitVecCommand {}
#[test]
fn command_code() {
assert_eq!(
@ -193,7 +197,8 @@ mod tests {
compression: *compression,
operation: BinaryOperation::Overwrite,
}
.into();
.try_into()
.unwrap();
let Packet {
header,
mut payload,
@ -223,7 +228,8 @@ mod tests {
compression: CompressionCode::Uncompressed,
operation: BinaryOperation::Or,
}
.into();
.try_into()
.unwrap();
let Header {
command_code: command,
a: offset,
@ -255,7 +261,8 @@ mod tests {
compression: CompressionCode::Uncompressed,
operation: BinaryOperation::And,
}
.into();
.try_into()
.unwrap();
let Header {
command_code: command,
a: offset,
@ -287,7 +294,8 @@ mod tests {
compression: CompressionCode::Uncompressed,
operation: BinaryOperation::Xor,
}
.into();
.try_into()
.unwrap();
let Header {
command_code: command,
a: offset,

View file

@ -1,6 +1,7 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::TryFromPacketError, Brightness, Header, Packet, TypedCommand,
commands::errors::TryFromPacketError, Brightness, Header, Packet,
TypedCommand,
};
/// Set the brightness of all tiles to the same value.
@ -13,7 +14,7 @@ use crate::{
/// let command = BrightnessCommand { brightness: Brightness::MAX };
/// connection.send(command).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrightnessCommand {
/// the brightness to set all pixels to
pub brightness: Brightness,
@ -85,12 +86,14 @@ impl From<Brightness> for BrightnessCommand {
#[cfg(test)]
mod tests {
use crate::command_code::CommandCode;
use crate::commands::tests::round_trip;
use crate::commands::errors::TryFromPacketError;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::{
commands, Brightness, BrightnessCommand, Header, Packet,
TryFromPacketError, TypedCommand,
commands, Brightness, BrightnessCommand, Header, Packet, TypedCommand,
};
impl TestImplementsCommand for BrightnessCommand {}
#[test]
fn brightness_as_command() {
assert_eq!(

View file

@ -1,11 +1,11 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::TryFromPacketError, BrightnessGrid, ByteGrid, Header, Origin,
Packet, Tiles, TypedCommand,
commands::errors::TryFromPacketError, BrightnessGrid, ByteGrid, Header,
Origin, Packet, Tiles, TryIntoPacketError, TypedCommand,
};
/// Set the brightness of individual tiles in a rectangular area of the display.
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, PartialEq, Debug, Eq)]
pub struct BrightnessGridCommand {
/// which tile the brightness rectangle should start
pub origin: Origin<Tiles>,
@ -13,13 +13,15 @@ pub struct BrightnessGridCommand {
pub grid: BrightnessGrid,
}
impl From<BrightnessGridCommand> for Packet {
fn from(value: BrightnessGridCommand) -> Self {
Packet::origin_grid_to_packet(
impl TryFrom<BrightnessGridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: BrightnessGridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::CharBrightness,
)
)?)
}
}
@ -74,12 +76,15 @@ impl From<BrightnessGridCommand> for TypedCommand {
#[cfg(test)]
mod tests {
use crate::commands::tests::round_trip;
use crate::commands::errors::TryFromPacketError;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::{
commands, BrightnessGrid, BrightnessGridCommand, Origin, Packet,
TryFromPacketError, TypedCommand,
TypedCommand,
};
impl TestImplementsCommand for BrightnessGridCommand {}
#[test]
fn round_trip_char_brightness() {
round_trip(
@ -98,7 +103,7 @@ mod tests {
origin: Origin::ZERO,
grid,
};
let mut packet: Packet = command.into();
let mut packet: Packet = command.try_into().unwrap();
let slot = packet.payload.get_mut(1).unwrap();
*slot = 23;
assert_eq!(

View file

@ -1,7 +1,7 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::TryFromPacketError, CharGrid, Header, Origin, Packet, Tiles,
TypedCommand,
commands::errors::TryFromPacketError, CharGrid, Header, Origin, Packet,
Tiles, TryIntoPacketError, TypedCommand,
};
/// Show text on the screen.
@ -16,7 +16,7 @@ use crate::{
/// let grid = CharGrid::from("Hello,\nWorld!");
/// connection.send(CharGridCommand { origin: Origin::ZERO, grid }).expect("send failed");
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CharGridCommand {
/// which tile the text should start
pub origin: Origin<Tiles>,
@ -24,13 +24,15 @@ pub struct CharGridCommand {
pub grid: CharGrid,
}
impl From<CharGridCommand> for Packet {
fn from(value: CharGridCommand) -> Self {
Packet::origin_grid_to_packet(
impl TryFrom<CharGridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: CharGridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::Utf8Data,
)
)?)
}
}
@ -82,9 +84,11 @@ impl From<CharGridCommand> for TypedCommand {
#[cfg(test)]
mod tests {
use crate::commands::tests::round_trip;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::{CharGrid, CharGridCommand, Origin};
impl TestImplementsCommand for CharGridCommand {}
#[test]
fn round_trip_utf8_data() {
round_trip(

View file

@ -1,6 +1,6 @@
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::TryFromPacketError, Packet, TypedCommand,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
@ -12,7 +12,7 @@ use std::fmt::Debug;
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// connection.send(ClearCommand).unwrap();
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
/// ```
pub struct ClearCommand;
@ -43,8 +43,11 @@ impl From<ClearCommand> for TypedCommand {
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::tests::TestImplementsCommand;
use crate::Header;
impl TestImplementsCommand for ClearCommand {}
#[test]
fn round_trip() {
crate::commands::tests::round_trip(ClearCommand.into());

View file

@ -1,7 +1,7 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::TryFromPacketError, Cp437Grid, Header, Origin, Packet, Tiles,
TypedCommand,
commands::errors::TryFromPacketError, Cp437Grid, Header, Origin, Packet,
Tiles, TryIntoPacketError, TypedCommand,
};
/// Show text on the screen.
@ -27,7 +27,7 @@ use crate::{
/// connection.send(Cp437GridCommand{ origin: Origin::new(2, 2), grid }).unwrap();
/// ```
/// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cp437GridCommand {
/// which tile the text should start
pub origin: Origin<Tiles>,
@ -35,13 +35,15 @@ pub struct Cp437GridCommand {
pub grid: Cp437Grid,
}
impl From<Cp437GridCommand> for Packet {
fn from(value: Cp437GridCommand) -> Self {
Packet::origin_grid_to_packet(
impl TryFrom<Cp437GridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: Cp437GridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::Cp437Data,
)
)?)
}
}
@ -91,7 +93,9 @@ impl From<Cp437GridCommand> for TypedCommand {
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::tests::round_trip;
use crate::commands::tests::{round_trip, TestImplementsCommand};
impl TestImplementsCommand for Cp437GridCommand {}
#[test]
fn round_trip_cp437_data() {

43
src/commands/errors.rs Normal file
View file

@ -0,0 +1,43 @@
use crate::LoadBitmapError;
use std::num::TryFromIntError;
/// Err values for [crate::TypedCommand::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),
/// Some provided text was not valid UTF-8.
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
/// The bitmap contained in the payload could not be loaded
#[error(transparent)]
LoadBitmapFailed(#[from] LoadBitmapError),
}
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum TryIntoPacketError {
/// Compression of the payload failed.
#[error("The compression of the payload failed")]
CompressionFailed,
/// Conversion (probably to u16) failed
#[error(transparent)]
ConversionError(#[from] TryFromIntError),
}

View file

@ -1,6 +1,6 @@
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::TryFromPacketError, Packet, TypedCommand,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
@ -15,7 +15,7 @@ use std::fmt::Debug;
/// # let connection = FakeConnection;
/// connection.send(FadeOutCommand).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FadeOutCommand;
impl TryFrom<Packet> for FadeOutCommand {
@ -45,10 +45,11 @@ impl From<FadeOutCommand> for TypedCommand {
#[cfg(test)]
mod tests {
use crate::command_code::CommandCode;
use crate::commands::tests::round_trip;
use crate::{
FadeOutCommand, Header, Packet, TryFromPacketError, TypedCommand,
};
use crate::commands::errors::TryFromPacketError;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::{ClearCommand, FadeOutCommand, Header, Packet, TypedCommand};
impl TestImplementsCommand for FadeOutCommand {}
#[test]
fn round_trip_fade_out() {

View file

@ -1,6 +1,6 @@
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::TryFromPacketError, Packet, TypedCommand,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
@ -15,7 +15,7 @@ use std::fmt::Debug;
/// # let connection = FakeConnection;
/// connection.send(HardResetCommand).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HardResetCommand;
impl TryFrom<Packet> for HardResetCommand {
@ -46,9 +46,11 @@ impl From<HardResetCommand> for TypedCommand {
#[cfg(test)]
mod test {
use super::*;
use crate::commands::tests::round_trip;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::Header;
impl TestImplementsCommand for HardResetCommand {}
#[test]
fn round_trip_hard_reset() {
round_trip(HardResetCommand.into());

View file

@ -6,6 +6,7 @@ mod brightness_grid;
mod char_grid;
mod clear;
mod cp437_grid;
mod errors;
mod fade_out;
mod hard_reset;
mod typed;
@ -22,6 +23,7 @@ pub use brightness_grid::*;
pub use char_grid::*;
pub use clear::*;
pub use cp437_grid::*;
pub use errors::*;
pub use fade_out::*;
pub use hard_reset::*;
pub use typed::*;
@ -74,9 +76,12 @@ pub use typed::*;
/// # let connection = FakeConnection;
/// connection.send(command).unwrap();
/// ```
pub trait Command: Debug + Clone + PartialEq + Into<Packet> {}
pub trait Command:
Debug + Clone + Eq + TryInto<Packet> + TryFrom<Packet>
{
}
impl<T: Debug + Clone + PartialEq + Into<Packet>> Command for T {}
impl<T: Debug + Clone + Eq + TryInto<Packet> + TryFrom<Packet>> Command for T {}
fn check_command_code_only(
packet: Packet,
@ -121,8 +126,10 @@ fn check_command_code(
mod tests {
use crate::*;
pub(crate) trait TestImplementsCommand: Command {}
pub(crate) fn round_trip(original: TypedCommand) {
let packet: Packet = original.clone().into();
let packet: Packet = original.clone().try_into().unwrap();
let copy: TypedCommand = match TypedCommand::try_from(packet) {
Ok(command) => command,
Err(err) => panic!("could not reload {original:?}: {err:?}"),

View file

@ -1,14 +1,15 @@
use crate::{
command_code::CommandCode, BitVecCommand, BitmapCommand, BrightnessCommand,
BrightnessGridCommand, CharGridCommand, ClearCommand, Cp437GridCommand,
FadeOutCommand, HardResetCommand, Header, LoadBitmapError, Packet,
command_code::CommandCode, commands::errors::TryFromPacketError,
BitVecCommand, BitmapCommand, BrightnessCommand, BrightnessGridCommand,
CharGridCommand, ClearCommand, Cp437GridCommand, FadeOutCommand,
HardResetCommand, Header, Packet, TryIntoPacketError,
};
/// This enum contains all commands provided by the library.
/// This is useful in case you want one data type for all kinds of commands without using `dyn`.
///
/// Please look at the contained structs for documentation per command.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum TypedCommand {
Clear(ClearCommand),
@ -29,42 +30,11 @@ pub enum TypedCommand {
FadeOut(FadeOutCommand),
#[allow(deprecated)]
#[deprecated]
#[allow(deprecated)]
BitmapLegacy(crate::BitmapLegacyCommand),
}
/// Err values for [TypedCommand::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),
/// Some provided text was not valid UTF-8.
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
/// The bitmap contained in the payload could not be loaded
#[error(transparent)]
LoadBitmapFailed(#[from] LoadBitmapError),
}
impl TryFrom<Packet> for TypedCommand {
type Error = TryFromPacketError;
@ -136,27 +106,33 @@ impl TryFrom<Packet> for TypedCommand {
}
}
impl From<TypedCommand> for Packet {
fn from(command: TypedCommand) -> Self {
match command {
impl TryFrom<TypedCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: TypedCommand) -> Result<Self, Self::Error> {
Ok(match value {
TypedCommand::Clear(c) => c.into(),
TypedCommand::CharGrid(c) => c.into(),
TypedCommand::Cp437Grid(c) => c.into(),
TypedCommand::Bitmap(c) => c.into(),
TypedCommand::CharGrid(c) => c.try_into()?,
TypedCommand::Cp437Grid(c) => c.try_into()?,
TypedCommand::Bitmap(c) => c.try_into()?,
TypedCommand::Brightness(c) => c.into(),
TypedCommand::BrightnessGrid(c) => c.into(),
TypedCommand::BitVec(c) => c.into(),
TypedCommand::BrightnessGrid(c) => c.try_into()?,
TypedCommand::BitVec(c) => c.try_into()?,
TypedCommand::HardReset(c) => c.into(),
TypedCommand::FadeOut(c) => c.into(),
#[allow(deprecated)]
TypedCommand::BitmapLegacy(c) => c.into(),
}
})
}
}
#[cfg(test)]
mod tests {
use crate::{Header, Packet, TryFromPacketError, TypedCommand};
use crate::commands::errors::TryFromPacketError;
use crate::commands::tests::TestImplementsCommand;
use crate::{Header, Packet, TypedCommand};
impl TestImplementsCommand for TypedCommand {}
#[test]
fn error_invalid_command() {

View file

@ -5,6 +5,7 @@ use std::io::{Read, Write};
use bzip2::read::{BzDecoder, BzEncoder};
#[cfg(feature = "compression_zlib")]
use flate2::{FlushCompress, FlushDecompress, Status};
use log::error;
#[cfg(feature = "compression_zstd")]
use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder};
@ -67,28 +68,39 @@ pub(crate) fn into_decompressed(
}
}
#[allow(clippy::unwrap_used)]
pub(crate) fn into_compressed(
kind: CompressionCode,
payload: Payload,
) -> Payload {
) -> Option<Payload> {
match kind {
CompressionCode::Uncompressed => payload,
CompressionCode::Uncompressed => Some(payload),
#[cfg(feature = "compression_zlib")]
CompressionCode::Zlib => {
let mut compress =
flate2::Compress::new(flate2::Compression::fast(), true);
let mut buffer = [0u8; 10000];
match compress
.compress(&payload, &mut buffer, FlushCompress::Finish)
.expect("compress failed")
{
Status::Ok => panic!("buffer should be big enough"),
Status::BufError => panic!("BufError"),
Status::StreamEnd => {}
};
buffer[..compress.total_out() as usize].to_owned()
match compress.compress(
&payload,
&mut buffer,
FlushCompress::Finish,
) {
Ok(Status::Ok) => {
error!("buffer not big enough");
None
}
Ok(Status::BufError) => {
error!("Could not compress: {:?}", Status::BufError);
None
}
Ok(Status::StreamEnd) => {
Some(buffer[..compress.total_out() as usize].to_owned())
}
Err(_) => {
error!("compress returned err");
None
}
}
}
#[cfg(feature = "compression_bzip2")]
CompressionCode::Bzip2 => {
@ -96,21 +108,39 @@ pub(crate) fn into_compressed(
BzEncoder::new(&*payload, bzip2::Compression::fast());
let mut compressed = vec![];
match encoder.read_to_end(&mut compressed) {
Err(err) => panic!("could not compress payload: {}", err),
Ok(_) => compressed,
Err(err) => {
error!("Could not compress: {:?}", err);
None
}
Ok(_) => Some(compressed),
}
}
#[cfg(feature = "compression_lzma")]
CompressionCode::Lzma => lzma::compress(&payload, 6).unwrap(),
CompressionCode::Lzma => match lzma::compress(&payload, 6) {
Ok(payload) => Some(payload),
Err(e) => {
error!("Could not compress: {e:?}");
None
}
},
#[cfg(feature = "compression_zstd")]
CompressionCode::Zstd => {
let buf = Vec::with_capacity(payload.len());
let mut encoder =
ZstdEncoder::new(vec![], zstd::DEFAULT_COMPRESSION_LEVEL)
.expect("could not create encoder");
encoder
.write_all(&payload)
.expect("could not compress payload");
encoder.finish().expect("could not finish encoding")
match ZstdEncoder::new(buf, zstd::DEFAULT_COMPRESSION_LEVEL) {
Err(e) => {
error!("failed to create decoder: {e:?}");
return None;
}
Ok(encoder) => encoder,
};
if let Err(e) = encoder.write_all(&payload) {
error!("failed to decompress payload: {e:?}");
return None;
}
encoder.finish().ok()
}
}
}

View file

@ -21,7 +21,7 @@
/// };
/// ```
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionCode {
/// no compression
Uncompressed = 0x0,

View file

@ -1,16 +1,28 @@
use crate::{Connection, Packet};
use crate::{Connection, Packet, SendError};
use log::debug;
use std::{convert::Infallible, error::Error, fmt::Debug};
#[derive(Debug)]
/// A fake connection for testing that does not actually send anything.
pub struct FakeConnection;
impl Connection for FakeConnection {
// TODO: () does not implement Error+Debug, some placeholder is needed
type Error = std::io::Error;
type TransportError = Infallible;
fn send(&self, packet: impl Into<Packet>) -> Result<(), Self::Error> {
let data: Vec<u8> = packet.into().into();
fn send<P: TryInto<Packet>>(
&self,
packet: P,
) -> Result<
(),
SendError<<P as TryInto<Packet>>::Error, Self::TransportError>,
>
where
<P as TryInto<Packet>>::Error: Error + Debug,
{
let data: Vec<u8> = packet
.try_into()
.map(Into::<Vec<u8>>::into)
.map_err(SendError::IntoPacket)?;
debug!("Sending fake packet: {data:?}");
Ok(())
}

View file

@ -1,8 +1,7 @@
//! This module contains the [Connection] trait and all implementations provided in this library.
use crate::Packet;
use std::error::Error;
use std::fmt::Debug;
use std::{error::Error, fmt::Debug};
mod fake;
#[cfg(feature = "protocol_udp")]
@ -16,6 +15,18 @@ pub use udp::*;
#[cfg(feature = "protocol_websocket")]
pub use websocket::*;
/// An error that can happen when sending a command
#[derive(Debug, thiserror::Error)]
pub enum SendError<
IntoPacketError: Error + Debug,
TransportError: Error + Debug,
> {
#[error(transparent)]
Transport(TransportError),
#[error(transparent)]
IntoPacket(IntoPacketError),
}
/// A connection to the display.
///
/// Used to send [Packets][Packet] or [Commands][crate::Command].
@ -30,7 +41,7 @@ pub use websocket::*;
/// ```
pub trait Connection: Debug {
/// The error that can occur when sending a packet
type Error: Error + Debug;
type TransportError: Error + Debug;
/// Send something packet-like to the display. Usually this is in the form of a Command.
///
@ -49,5 +60,13 @@ pub trait Connection: Debug {
/// connection.send(servicepoint::ClearCommand)
/// .expect("send failed");
/// ```
fn send(&self, packet: impl Into<Packet>) -> Result<(), Self::Error>;
fn send<P: TryInto<Packet>>(
&self,
packet: P,
) -> Result<
(),
SendError<<P as TryInto<Packet>>::Error, Self::TransportError>,
>
where
<P as TryInto<Packet>>::Error: Error + Debug;
}

View file

@ -1,6 +1,5 @@
use crate::{Connection, Packet};
use std::fmt::Debug;
use std::net::UdpSocket;
use crate::{Connection, Packet, SendError};
use std::{error::Error, fmt::Debug, net::UdpSocket};
/// A connection using the UDP protocol.
///
@ -39,11 +38,26 @@ impl UdpConnection {
}
impl Connection for UdpConnection {
type Error = std::io::Error;
type TransportError = std::io::Error;
fn send(&self, packet: impl Into<Packet>) -> Result<(), Self::Error> {
let data: Vec<u8> = packet.into().into();
self.socket.send(&data).map(move |_| ()) // ignore Ok value
fn send<P: TryInto<Packet>>(
&self,
packet: P,
) -> Result<
(),
SendError<<P as TryInto<Packet>>::Error, Self::TransportError>,
>
where
<P as TryInto<Packet>>::Error: Error + Debug,
{
let data: Vec<u8> = packet
.try_into()
.map(Into::<Vec<u8>>::into)
.map_err(SendError::IntoPacket)?;
self.socket
.send(&data)
.map(move |_| ())
.map_err(SendError::Transport) // ignore Ok value
}
}

View file

@ -1,4 +1,5 @@
use crate::{Connection, Packet};
use crate::{Connection, Packet, SendError};
use std::{error::Error, fmt::Debug};
/// A connection using the WebSocket protocol.
///
@ -20,12 +21,27 @@ pub struct WebsocketConnection(
);
impl Connection for WebsocketConnection {
type Error = tungstenite::Error;
type TransportError = tungstenite::Error;
fn send(&self, packet: impl Into<Packet>) -> Result<(), Self::Error> {
let data: Vec<u8> = packet.into().into();
fn send<P: TryInto<Packet>>(
&self,
packet: P,
) -> Result<
(),
SendError<<P as TryInto<Packet>>::Error, Self::TransportError>,
>
where
<P as TryInto<Packet>>::Error: Error + Debug,
{
let data: Vec<u8> = packet
.try_into()
.map(Into::<Vec<u8>>::into)
.map_err(SendError::IntoPacket)?
.into();
let mut socket = self.0.lock().unwrap();
socket.send(tungstenite::Message::Binary(data.into()))
socket
.send(tungstenite::Message::Binary(data.into()))
.map_err(SendError::Transport)
}
}
@ -63,6 +79,6 @@ impl WebsocketConnection {
impl Drop for WebsocketConnection {
fn drop(&mut self) {
_ = self.0.try_lock().map(move |mut sock| sock.close(None));
drop(self.0.try_lock().map(move |mut sock| sock.close(None)));
}
}

View file

@ -13,7 +13,7 @@ impl<T: Sized + Default + Copy + Clone + Debug> Value for T {}
///
/// This structure can be used with any type that implements the [Value] trait.
/// You can also use the concrete type aliases provided in this crate, e.g. [CharGrid] and [ByteGrid].
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueGrid<T: Value> {
width: usize,
height: usize,

View file

@ -2,7 +2,7 @@ use crate::TILE_SIZE;
use std::marker::PhantomData;
/// An origin marks the top left position of a window sent to the display.
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Origin<Unit: DisplayUnit> {
/// position in the width direction
pub x: usize,
@ -44,11 +44,11 @@ impl<T: DisplayUnit> std::ops::Add<Origin<T>> for Origin<T> {
pub trait DisplayUnit {}
/// Marks something to be measured in number of pixels.
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Pixels();
/// Marks something to be measured in number of iles.
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Tiles();
impl DisplayUnit for Pixels {}

View file

@ -23,9 +23,8 @@
//! let packet = Packet::try_from(bytes).expect("could not read packet from bytes");
//! ```
use crate::command_code::CommandCode;
use crate::{Grid, Origin, Tiles};
use std::mem::size_of;
use crate::{command_code::CommandCode, Grid, Origin, Tiles};
use std::{mem::size_of, num::TryFromIntError};
/// A raw header.
///
@ -144,17 +143,17 @@ impl Packet {
origin: Origin<Tiles>,
grid: impl Grid<T> + Into<Payload>,
command_code: CommandCode,
) -> Packet {
Packet {
) -> Result<Packet, TryFromIntError> {
Ok(Packet {
header: Header {
command_code: command_code.into(),
a: origin.x as u16,
b: origin.y as u16,
c: grid.width() as u16,
d: grid.height() as u16,
a: origin.x.try_into()?,
b: origin.y.try_into()?,
c: grid.width().try_into()?,
d: grid.height().try_into()?,
},
payload: grid.into(),
}
})
}
pub(crate) fn command_code_only(c: CommandCode) -> Self {