Merge branch 'fix-brightness'
This commit is contained in:
commit
a484e6a976
35 changed files with 1594 additions and 553 deletions
14
.github/workflows/rust.yml
vendored
14
.github/workflows/rust.yml
vendored
|
|
@ -16,9 +16,17 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build crates
|
||||
|
||||
- name: build default features
|
||||
run: cargo build --all --verbose
|
||||
- name: Build
|
||||
- name: build default features -- examples
|
||||
run: cargo build --examples --verbose
|
||||
- name: Run tests
|
||||
- name: test default features
|
||||
run: cargo test --all --verbose
|
||||
|
||||
- name: build all features
|
||||
run: cargo build --all-features --verbose
|
||||
- name: build all features -- examples
|
||||
run: cargo build --all-features --examples --verbose
|
||||
- name: test all features
|
||||
run: cargo test --all --all-features --verbose
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/servicepoint",
|
||||
"crates/servicepoint_binding_c",
|
||||
"crates/servicepoint_binding_cs",
|
||||
"crates/*",
|
||||
"crates/servicepoint_binding_c/examples/lang_c"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing-docs = "warn"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ flate2 = { version = "1.0", optional = true }
|
|||
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 }
|
||||
|
||||
[features]
|
||||
default = ["compression_lzma"]
|
||||
|
|
@ -27,6 +28,11 @@ 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"]
|
||||
|
||||
[[example]]
|
||||
name = "random_brightness"
|
||||
required-features = ["rand"]
|
||||
|
||||
[dev-dependencies]
|
||||
# for examples
|
||||
|
|
@ -34,4 +40,4 @@ clap = { version = "4.5", features = ["derive"] }
|
|||
rand = "0.8"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
workspace = true
|
||||
|
|
@ -46,7 +46,7 @@ In the likely case you only need one of them, you can include that one specifica
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
servicepoint = { version = "0.6.0", default-features = false, features = ["compression-bz"] }
|
||||
servicepoint = { version = "0.7.0", default-features = false, features = ["compression-bz"] }
|
||||
```
|
||||
|
||||
## Everything else
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use clap::Parser;
|
||||
|
||||
use servicepoint::{ByteGrid, Command, Connection, Grid, Origin};
|
||||
use servicepoint::{Command, Connection, Cp437Grid, Grid, Origin};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
|
|
@ -22,14 +22,18 @@ fn main() {
|
|||
cli.text.push("Hello, CCCB!".to_string());
|
||||
}
|
||||
|
||||
let connection = Connection::open(&cli.destination).unwrap();
|
||||
let connection = Connection::open(&cli.destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
if cli.clear {
|
||||
connection.send(Command::Clear).unwrap();
|
||||
connection
|
||||
.send(Command::Clear)
|
||||
.expect("sending clear failed");
|
||||
}
|
||||
|
||||
let max_width = cli.text.iter().map(|t| t.len()).max().unwrap();
|
||||
|
||||
let mut chars = ByteGrid::new(max_width, cli.text.len());
|
||||
let mut chars = Cp437Grid::new(max_width, cli.text.len());
|
||||
for y in 0..cli.text.len() {
|
||||
let row = &cli.text[y];
|
||||
|
||||
|
|
@ -40,6 +44,6 @@ fn main() {
|
|||
}
|
||||
|
||||
connection
|
||||
.send(Command::Cp437Data(Origin(0, 0), chars))
|
||||
.unwrap();
|
||||
.send(Command::Cp437Data(Origin::new(0, 0), chars))
|
||||
.expect("sending text failed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
use clap::Parser;
|
||||
|
||||
use servicepoint::Command::BitmapLinearWin;
|
||||
use servicepoint::*;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
|
@ -13,25 +12,26 @@ struct Cli {
|
|||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let connection = Connection::open(cli.destination).unwrap();
|
||||
let connection = Connection::open(cli.destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
let mut pixels = PixelGrid::max_sized();
|
||||
pixels.fill(true);
|
||||
|
||||
connection
|
||||
.send(BitmapLinearWin(
|
||||
Origin(0, 0),
|
||||
pixels,
|
||||
CompressionCode::Uncompressed,
|
||||
))
|
||||
.expect("send failed");
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::new(0, 0),
|
||||
pixels,
|
||||
CompressionCode::Uncompressed,
|
||||
);
|
||||
connection.send(command).expect("send failed");
|
||||
|
||||
let mut brightnesses = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||
let max_brightness = usize::from(u8::from(Brightness::MAX));
|
||||
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
|
||||
*byte = (index % u8::MAX as usize) as u8;
|
||||
*byte = Brightness::try_from((index % max_brightness) as u8).unwrap();
|
||||
}
|
||||
|
||||
connection
|
||||
.send(Command::CharBrightness(Origin(0, 0), brightnesses))
|
||||
.send(Command::CharBrightness(Origin::new(0, 0), brightnesses))
|
||||
.expect("send failed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,17 @@ struct Cli {
|
|||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let connection = Connection::open(&cli.destination).unwrap();
|
||||
let connection = Connection::open(&cli.destination)
|
||||
.expect("could not connect to display");
|
||||
let mut field = make_random_field(cli.probability);
|
||||
|
||||
loop {
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin(0, 0),
|
||||
field.clone(),
|
||||
CompressionCode::Lzma,
|
||||
))
|
||||
.expect("could not send");
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::new(0, 0),
|
||||
field.clone(),
|
||||
CompressionCode::Lzma,
|
||||
);
|
||||
connection.send(command).expect("could not send");
|
||||
thread::sleep(FRAME_PACING);
|
||||
field = iteration(field);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ struct Cli {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
let connection = Connection::open(Cli::parse().destination).unwrap();
|
||||
let connection = Connection::open(Cli::parse().destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
let mut pixels = PixelGrid::max_sized();
|
||||
for x_offset in 0..usize::MAX {
|
||||
|
|
@ -22,13 +23,13 @@ fn main() {
|
|||
for y in 0..PIXEL_HEIGHT {
|
||||
pixels.set((y + x_offset) % PIXEL_WIDTH, y, true);
|
||||
}
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin(0, 0),
|
||||
pixels.clone(),
|
||||
CompressionCode::Lzma,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::new(0, 0),
|
||||
pixels.clone(),
|
||||
CompressionCode::Lzma,
|
||||
);
|
||||
connection.send(command).expect("send failed");
|
||||
thread::sleep(FRAME_PACING);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ struct Cli {
|
|||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let connection = Connection::open(cli.destination).unwrap();
|
||||
let connection = Connection::open(cli.destination)
|
||||
.expect("could not connect to display");
|
||||
let wait_duration = Duration::from_millis(cli.wait_ms);
|
||||
|
||||
// put all pixels in on state
|
||||
|
|
@ -30,8 +31,11 @@ fn main() {
|
|||
let mut filled_grid = PixelGrid::max_sized();
|
||||
filled_grid.fill(true);
|
||||
|
||||
let command =
|
||||
BitmapLinearWin(Origin(0, 0), filled_grid, CompressionCode::Lzma);
|
||||
let command = BitmapLinearWin(
|
||||
Origin::new(0, 0),
|
||||
filled_grid,
|
||||
CompressionCode::Lzma,
|
||||
);
|
||||
connection.send(command).expect("send failed");
|
||||
}
|
||||
|
||||
|
|
@ -48,8 +52,8 @@ fn main() {
|
|||
let w = rng.gen_range(min_size..=TILE_WIDTH - x);
|
||||
let h = rng.gen_range(min_size..=TILE_HEIGHT - y);
|
||||
|
||||
let origin = Origin(x, y);
|
||||
let mut luma = ByteGrid::new(w, h);
|
||||
let origin = Origin::new(x, y);
|
||||
let mut luma = BrightnessGrid::new(w, h);
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ fn main() {
|
|||
Duration::from_millis(cli.time / PIXEL_WIDTH as u64),
|
||||
);
|
||||
|
||||
let connection = Connection::open(cli.destination).unwrap();
|
||||
let connection = Connection::open(cli.destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
let mut enabled_pixels = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||
enabled_pixels.fill(true);
|
||||
|
|
@ -38,7 +39,7 @@ fn main() {
|
|||
|
||||
connection
|
||||
.send(Command::BitmapLinearAnd(0, bit_vec, CompressionCode::Lzma))
|
||||
.unwrap();
|
||||
.expect("could not send command to display");
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
111
crates/servicepoint/src/brightness.rs
Normal file
111
crates/servicepoint/src/brightness.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use crate::{Grid, PrimitiveGrid};
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
use rand::{
|
||||
distributions::{Distribution, Standard},
|
||||
Rng,
|
||||
};
|
||||
|
||||
/// A display brightness value, checked for correct value range
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::{Brightness, Command, Connection};
|
||||
/// let b = Brightness::MAX;
|
||||
/// let val: u8 = b.into();
|
||||
///
|
||||
/// let b = Brightness::try_from(7).unwrap();
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// let result = connection.send(Command::Brightness(b));
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Brightness(u8);
|
||||
|
||||
/// A grid containing brightness values.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Connection, Grid, Origin};
|
||||
/// let mut grid = BrightnessGrid::new(2,2);
|
||||
/// grid.set(0, 0, Brightness::MIN);
|
||||
/// grid.set(1, 1, Brightness::MIN);
|
||||
///
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// connection.send(Command::CharBrightness(Origin::new(3, 7), grid)).unwrap()
|
||||
/// ```
|
||||
pub type BrightnessGrid = PrimitiveGrid<Brightness>;
|
||||
|
||||
impl From<Brightness> for u8 {
|
||||
fn from(brightness: Brightness) -> Self {
|
||||
brightness.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Brightness {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
if value > Self::MAX.0 {
|
||||
Err(value)
|
||||
} else {
|
||||
Ok(Brightness(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Brightness {
|
||||
/// highest possible brightness value, 11
|
||||
pub const MAX: Brightness = Brightness(11);
|
||||
/// lowest possible brightness value, 0
|
||||
pub const MIN: Brightness = Brightness(0);
|
||||
}
|
||||
|
||||
impl Default for Brightness {
|
||||
fn default() -> Self {
|
||||
Self::MAX
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrightnessGrid> for Vec<u8> {
|
||||
fn from(value: PrimitiveGrid<Brightness>) -> Self {
|
||||
value
|
||||
.iter()
|
||||
.map(|brightness| (*brightness).into())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrightnessGrid> for PrimitiveGrid<u8> {
|
||||
fn from(value: PrimitiveGrid<Brightness>) -> Self {
|
||||
let u8s = value
|
||||
.iter()
|
||||
.map(|brightness| (*brightness).into())
|
||||
.collect::<Vec<u8>>();
|
||||
PrimitiveGrid::load(value.width(), value.height(), &u8s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PrimitiveGrid<u8>> for BrightnessGrid {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: PrimitiveGrid<u8>) -> Result<Self, Self::Error> {
|
||||
let brightnesses = value
|
||||
.iter()
|
||||
.map(|b| Brightness::try_from(*b))
|
||||
.collect::<Result<Vec<Brightness>, _>>()?;
|
||||
Ok(BrightnessGrid::load(
|
||||
value.width(),
|
||||
value.height(),
|
||||
&brightnesses,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
impl Distribution<Brightness> for Standard {
|
||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Brightness {
|
||||
Brightness(rng.gen_range(Brightness::MIN.0..=Brightness::MAX.0))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,183 +1,168 @@
|
|||
use bitvec::prelude::BitVec;
|
||||
|
||||
use crate::command_code::CommandCode;
|
||||
use crate::compression::{into_compressed, into_decompressed};
|
||||
use crate::{
|
||||
ByteGrid, CompressionCode, Grid, Header, Packet, PixelGrid, SpBitVec,
|
||||
TILE_SIZE,
|
||||
command_code::CommandCode, compression::into_decompressed, Brightness,
|
||||
BrightnessGrid, CompressionCode, Header, Origin, Packet, PixelGrid, Pixels,
|
||||
PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE,
|
||||
};
|
||||
|
||||
/// An origin marks the top left position of a window sent to the display.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Origin(pub usize, pub usize);
|
||||
|
||||
impl std::ops::Add<Origin> for Origin {
|
||||
type Output = Origin;
|
||||
|
||||
fn add(self, rhs: Origin) -> Self::Output {
|
||||
let Origin(x1, y1) = self;
|
||||
let Origin(x2, y2) = rhs;
|
||||
Origin(x1 + x2, y1 + y2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for documenting the meaning of the u16 in enum values
|
||||
pub type Offset = usize;
|
||||
|
||||
/// Type alias for documenting the meaning of the u16 in enum values
|
||||
pub type Brightness = u8;
|
||||
/// A grid containing codepage 437 characters.
|
||||
///
|
||||
/// The encoding is currently not enforced.
|
||||
pub type Cp437Grid = PrimitiveGrid<u8>;
|
||||
|
||||
/// A command to send to the display.
|
||||
/// A low-level display command.
|
||||
///
|
||||
/// This struct and associated functions implement the UDP protocol for the display.
|
||||
///
|
||||
/// To send a `Command`, use a `Connection`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{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::open("127.0.0.1:2342").unwrap();
|
||||
/// connection.send(command).unwrap();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Command {
|
||||
/// Set all pixels to the off state
|
||||
/// Set all pixels to the off state. Does not affect brightness.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Command, Connection};
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// connection.send(Command::Clear).unwrap();
|
||||
/// ```
|
||||
Clear,
|
||||
/// Kills the udp daemon, usually results in a reboot of the display.
|
||||
HardReset,
|
||||
/// Slowly decrease brightness until off? Untested.
|
||||
FadeOut,
|
||||
/// Set the brightness of tiles
|
||||
CharBrightness(Origin, ByteGrid),
|
||||
/// Set the brightness of all tiles
|
||||
|
||||
/// Show text on the screen.
|
||||
///
|
||||
/// <div class="warning">
|
||||
/// 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.
|
||||
/// </div>
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```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);
|
||||
/// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
|
||||
/// ```
|
||||
Cp437Data(Origin<Tiles>, Cp437Grid),
|
||||
|
||||
/// Sets a window of pixels to the specified values
|
||||
BitmapLinearWin(Origin<Pixels>, PixelGrid, CompressionCode),
|
||||
|
||||
/// Set the brightness of all tiles to the same value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Brightness, Command, Connection};
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// let command = Command::Brightness(Brightness::MAX);
|
||||
/// connection.send(command).unwrap();
|
||||
/// ```
|
||||
Brightness(Brightness),
|
||||
#[deprecated]
|
||||
/// Legacy command code, gets ignored by the real display.
|
||||
BitmapLegacy,
|
||||
/// Set pixel data starting at the offset.
|
||||
|
||||
/// 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, SpBitVec, 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, SpBitVec, 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, SpBitVec, 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, SpBitVec, CompressionCode),
|
||||
/// Show text on the screen. Note that the byte data has to be CP437 encoded.
|
||||
Cp437Data(Origin, ByteGrid),
|
||||
/// Sets a window of pixels to the specified values
|
||||
BitmapLinearWin(Origin, PixelGrid, CompressionCode),
|
||||
}
|
||||
|
||||
impl From<Command> for Packet {
|
||||
/// Move the `Command` into a `Packet` instance for sending.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn from(value: Command) -> Self {
|
||||
match value {
|
||||
Command::Clear => Command::command_code_only(CommandCode::Clear),
|
||||
Command::FadeOut => {
|
||||
Command::command_code_only(CommandCode::FadeOut)
|
||||
}
|
||||
Command::HardReset => {
|
||||
Command::command_code_only(CommandCode::HardReset)
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
Command::BitmapLegacy => {
|
||||
Command::command_code_only(CommandCode::BitmapLegacy)
|
||||
}
|
||||
Command::CharBrightness(Origin(x, y), grid) => Packet(
|
||||
Header(
|
||||
CommandCode::CharBrightness.into(),
|
||||
x as u16,
|
||||
y as u16,
|
||||
grid.width() as u16,
|
||||
grid.height() as u16,
|
||||
),
|
||||
grid.into(),
|
||||
),
|
||||
Command::Brightness(brightness) => Packet(
|
||||
Header(
|
||||
CommandCode::Brightness.into(),
|
||||
0x00000,
|
||||
0x0000,
|
||||
0x0000,
|
||||
0x0000,
|
||||
),
|
||||
vec![brightness],
|
||||
),
|
||||
Command::BitmapLinearWin(origin, pixels, compression) => {
|
||||
bitmap_win_into_packet(origin, pixels, compression)
|
||||
}
|
||||
Command::BitmapLinear(offset, bits, compression) => {
|
||||
Command::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinear,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::BitmapLinearAnd(offset, bits, compression) => {
|
||||
Command::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinearAnd,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::BitmapLinearOr(offset, bits, compression) => {
|
||||
Command::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinearOr,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::BitmapLinearXor(offset, bits, compression) => {
|
||||
Command::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinearXor,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::Cp437Data(Origin(x, y), grid) => Packet(
|
||||
Header(
|
||||
CommandCode::Cp437Data.into(),
|
||||
x as u16,
|
||||
y as u16,
|
||||
grid.width() as u16,
|
||||
grid.height() as u16,
|
||||
),
|
||||
grid.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 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::{Command, Connection};
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// connection.send(Command::HardReset).unwrap();
|
||||
/// ```
|
||||
HardReset,
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn bitmap_win_into_packet(
|
||||
origin: Origin,
|
||||
pixels: PixelGrid,
|
||||
compression: CompressionCode,
|
||||
) -> Packet {
|
||||
let Origin(pixel_x, pixel_y) = origin;
|
||||
debug_assert_eq!(pixel_x % 8, 0);
|
||||
debug_assert_eq!(pixels.width() % 8, 0);
|
||||
/// <div class="warning">Untested</div>
|
||||
///
|
||||
/// Slowly decrease brightness until off or something like that?
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Command, Connection};
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// connection.send(Command::FadeOut).unwrap();
|
||||
/// ```
|
||||
FadeOut,
|
||||
|
||||
let tile_x = (pixel_x / TILE_SIZE) as u16;
|
||||
let tile_w = (pixels.width() / TILE_SIZE) as u16;
|
||||
let pixel_h = pixels.height() as u16;
|
||||
let payload = into_compressed(compression, pixels.into());
|
||||
let command = match 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(command.into(), tile_x, pixel_y as u16, tile_w, pixel_h),
|
||||
payload,
|
||||
)
|
||||
/// Legacy command code, gets ignored by the real display.
|
||||
///
|
||||
/// Might be useful as a noop package.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Command, Connection};
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// // this sends a packet that does nothing
|
||||
/// # #[allow(deprecated)]
|
||||
/// connection.send(Command::BitmapLegacy).unwrap();
|
||||
/// ```
|
||||
#[deprecated]
|
||||
BitmapLegacy,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -196,6 +181,8 @@ pub enum TryFromPacketError {
|
|||
InvalidCompressionCode(u16),
|
||||
/// Decompression of the payload failed. This can be caused by corrupted packets.
|
||||
DecompressionFailed,
|
||||
/// The given brightness value is out of bounds
|
||||
InvalidBrightness(u8),
|
||||
}
|
||||
|
||||
impl TryFrom<Packet> for Command {
|
||||
|
|
@ -203,7 +190,7 @@ impl TryFrom<Packet> for Command {
|
|||
|
||||
/// Try to interpret the `Packet` as one containing a `Command`
|
||||
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
|
||||
let Packet(Header(command_u16, a, b, c, d), _) = packet;
|
||||
let Packet(Header(command_u16, a, _, _, _), _) = packet;
|
||||
let command_code = match CommandCode::try_from(command_u16) {
|
||||
Err(()) => {
|
||||
return Err(TryFromPacketError::InvalidCommand(command_u16));
|
||||
|
|
@ -212,47 +199,19 @@ impl TryFrom<Packet> for Command {
|
|||
};
|
||||
|
||||
match command_code {
|
||||
CommandCode::Clear => match Self::check_command_only(packet) {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(Command::Clear),
|
||||
},
|
||||
CommandCode::Brightness => {
|
||||
let Packet(header, payload) = packet;
|
||||
if payload.len() != 1 {
|
||||
return Err(TryFromPacketError::UnexpectedPayloadSize(
|
||||
1,
|
||||
payload.len(),
|
||||
));
|
||||
}
|
||||
|
||||
let Header(_, a, b, c, d) = header;
|
||||
if a != 0 || b != 0 || c != 0 || d != 0 {
|
||||
Err(TryFromPacketError::ExtraneousHeaderValues)
|
||||
} else {
|
||||
Ok(Command::Brightness(payload[0]))
|
||||
}
|
||||
CommandCode::Clear => {
|
||||
Self::packet_into_command_only(packet, Command::Clear)
|
||||
}
|
||||
CommandCode::HardReset => match Self::check_command_only(packet) {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(Command::HardReset),
|
||||
},
|
||||
CommandCode::FadeOut => match Self::check_command_only(packet) {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(Command::FadeOut),
|
||||
},
|
||||
CommandCode::Cp437Data => {
|
||||
let Packet(_, payload) = packet;
|
||||
Ok(Command::Cp437Data(
|
||||
Origin(a as usize, b as usize),
|
||||
ByteGrid::load(c as usize, d as usize, &payload),
|
||||
))
|
||||
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 => {
|
||||
let Packet(_, payload) = packet;
|
||||
Ok(Command::CharBrightness(
|
||||
Origin(a as usize, b as usize),
|
||||
ByteGrid::load(c as usize, d as usize, &payload),
|
||||
))
|
||||
Self::packet_into_char_brightness(&packet)
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
CommandCode::BitmapLegacy => Ok(Command::BitmapLegacy),
|
||||
|
|
@ -316,7 +275,7 @@ impl Command {
|
|||
};
|
||||
|
||||
Ok(Command::BitmapLinearWin(
|
||||
Origin(tiles_x as usize * TILE_SIZE, pixels_y as usize),
|
||||
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize),
|
||||
PixelGrid::load(
|
||||
tile_w as usize * TILE_SIZE,
|
||||
pixel_h as usize,
|
||||
|
|
@ -326,42 +285,18 @@ impl Command {
|
|||
))
|
||||
}
|
||||
|
||||
/// Helper method for `BitMapLinear*`-Commands into `Packet`
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn bitmap_linear_into_packet(
|
||||
command: CommandCode,
|
||||
offset: Offset,
|
||||
compression: CompressionCode,
|
||||
payload: Vec<u8>,
|
||||
) -> Packet {
|
||||
let length = payload.len() as u16;
|
||||
let payload = into_compressed(compression, payload);
|
||||
Packet(
|
||||
Header(
|
||||
command.into(),
|
||||
offset as u16,
|
||||
length,
|
||||
compression.into(),
|
||||
0,
|
||||
),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
/// Helper method for creating empty packets only containing the command code
|
||||
fn command_code_only(code: CommandCode) -> Packet {
|
||||
Packet(Header(code.into(), 0x0000, 0x0000, 0x0000, 0x0000), vec![])
|
||||
}
|
||||
|
||||
/// Helper method for checking that a packet is empty and only contains a command code
|
||||
fn check_command_only(packet: Packet) -> Option<TryFromPacketError> {
|
||||
fn packet_into_command_only(
|
||||
packet: Packet,
|
||||
command: Command,
|
||||
) -> Result<Command, TryFromPacketError> {
|
||||
let Packet(Header(_, a, b, c, d), payload) = packet;
|
||||
if !payload.is_empty() {
|
||||
Some(TryFromPacketError::UnexpectedPayloadSize(0, payload.len()))
|
||||
Err(TryFromPacketError::UnexpectedPayloadSize(0, payload.len()))
|
||||
} else if a != 0 || b != 0 || c != 0 || d != 0 {
|
||||
Some(TryFromPacketError::ExtraneousHeaderValues)
|
||||
Err(TryFromPacketError::ExtraneousHeaderValues)
|
||||
} else {
|
||||
None
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -391,16 +326,63 @@ impl Command {
|
|||
}
|
||||
Ok((BitVec::from_vec(payload), sub))
|
||||
}
|
||||
|
||||
fn packet_into_char_brightness(
|
||||
packet: &Packet,
|
||||
) -> Result<Command, TryFromPacketError> {
|
||||
let Packet(Header(_, x, y, width, height), payload) = packet;
|
||||
|
||||
let grid =
|
||||
PrimitiveGrid::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(_, 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(_, 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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bitvec::prelude::BitVec;
|
||||
|
||||
use crate::command::TryFromPacketError;
|
||||
use crate::command_code::CommandCode;
|
||||
use crate::{
|
||||
ByteGrid, Command, CompressionCode, Header, Origin, Packet, PixelGrid,
|
||||
bitvec::prelude::BitVec, command::TryFromPacketError,
|
||||
command_code::CommandCode, origin::Pixels, Brightness, Command,
|
||||
CompressionCode, Header, Origin, Packet, PixelGrid, PrimitiveGrid,
|
||||
};
|
||||
|
||||
fn round_trip(original: Command) {
|
||||
|
|
@ -443,7 +425,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn round_trip_brightness() {
|
||||
round_trip(Command::Brightness(6));
|
||||
round_trip(Command::Brightness(Brightness::try_from(6).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -454,12 +436,18 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn round_trip_char_brightness() {
|
||||
round_trip(Command::CharBrightness(Origin(5, 2), ByteGrid::new(7, 5)));
|
||||
round_trip(Command::CharBrightness(
|
||||
Origin::new(5, 2),
|
||||
PrimitiveGrid::new(7, 5),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_cp437_data() {
|
||||
round_trip(Command::Cp437Data(Origin(5, 2), ByteGrid::new(7, 5)));
|
||||
round_trip(Command::Cp437Data(
|
||||
Origin::new(5, 2),
|
||||
PrimitiveGrid::new(7, 5),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -486,7 +474,7 @@ mod tests {
|
|||
compression,
|
||||
));
|
||||
round_trip(Command::BitmapLinearWin(
|
||||
Origin(0, 0),
|
||||
Origin::new(0, 0),
|
||||
PixelGrid::max_sized(),
|
||||
compression,
|
||||
));
|
||||
|
|
@ -572,7 +560,7 @@ mod tests {
|
|||
fn error_decompression_failed_win() {
|
||||
for compression in all_compressions().to_owned() {
|
||||
let p: Packet = Command::BitmapLinearWin(
|
||||
Origin(16, 8),
|
||||
Origin::new(16, 8),
|
||||
PixelGrid::new(8, 8),
|
||||
compression,
|
||||
)
|
||||
|
|
@ -696,6 +684,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn origin_add() {
|
||||
assert_eq!(Origin(4, 2), Origin(1, 0) + Origin(3, 2));
|
||||
assert_eq!(
|
||||
Origin::<Pixels>::new(4, 2),
|
||||
Origin::new(1, 0) + Origin::new(3, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
/// Specifies the kind of compression to use. Availability depends on features.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Command, CompressionCode, Origin, PixelGrid};
|
||||
/// // create command without payload compression
|
||||
/// # let pixels = PixelGrid::max_sized();
|
||||
/// _ = Command::BitmapLinearWin(Origin::new(0, 0), pixels, CompressionCode::Uncompressed);
|
||||
///
|
||||
/// // create command with payload compressed with lzma and appropriate header flags
|
||||
/// # let pixels = PixelGrid::max_sized();
|
||||
/// _ = Command::BitmapLinearWin(Origin::new(0, 0), pixels, CompressionCode::Lzma);
|
||||
/// ```
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CompressionCode {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ use log::{debug, info};
|
|||
use crate::Packet;
|
||||
|
||||
/// A connection to the display.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use servicepoint::Command;
|
||||
/// let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
||||
/// .expect("connection failed");
|
||||
/// connection.send(Command::Clear)
|
||||
/// .expect("send failed");
|
||||
/// ```
|
||||
pub struct Connection {
|
||||
socket: UdpSocket,
|
||||
}
|
||||
|
|
@ -46,21 +55,12 @@ impl Connection {
|
|||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
||||
/// let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
||||
/// .expect("connection failed");
|
||||
///
|
||||
/// // turn off all pixels
|
||||
/// # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
||||
/// # let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
||||
/// # .expect("connection failed");
|
||||
/// // turn off all pixels on display
|
||||
/// connection.send(Command::Clear)
|
||||
/// .expect("send failed");
|
||||
///
|
||||
/// // turn on all pixels
|
||||
/// let mut pixels = PixelGrid::max_sized();
|
||||
/// pixels.fill(true);
|
||||
///
|
||||
/// // send pixels to display
|
||||
/// connection.send(Command::BitmapLinearWin(servicepoint::Origin(0, 0), pixels, CompressionCode::Uncompressed))
|
||||
/// .expect("send failed");
|
||||
/// ```
|
||||
pub fn send(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
///
|
||||
/// The expectation is that you can create an equal instance with this data given the additional
|
||||
/// metadata needed.
|
||||
pub trait DataRef {
|
||||
pub trait DataRef<T> {
|
||||
/// Get the underlying bytes writable.
|
||||
fn data_ref_mut(&mut self) -> &mut [u8];
|
||||
///
|
||||
/// Note that depending on the struct this is implemented on, writing invalid values here might
|
||||
/// lead to panics later in the lifetime of the program or on the receiving side.
|
||||
fn data_ref_mut(&mut self) -> &mut [T];
|
||||
|
||||
/// Get the underlying bytes read-only.
|
||||
fn data_ref(&self) -> &[u8];
|
||||
fn data_ref(&self) -> &[T];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,55 @@
|
|||
//! Abstractions for the UDP protocol of the CCCB servicepoint display.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
||||
//!
|
||||
//! let connection = servicepoint::Connection::open("127.0.0.1:2342")
|
||||
//! .expect("connection failed");
|
||||
//!
|
||||
//! // turn off all pixels on display
|
||||
//! connection.send(Command::Clear)
|
||||
//! .expect("send failed");
|
||||
//! ```
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
||||
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
|
||||
//! // turn on all pixels in a grid
|
||||
//! let mut pixels = PixelGrid::max_sized();
|
||||
//! pixels.fill(true);
|
||||
//!
|
||||
//! // create command to send pixels
|
||||
//! let command = Command::BitmapLinearWin(
|
||||
//! servicepoint::Origin::new(0, 0),
|
||||
//! pixels,
|
||||
//! CompressionCode::Uncompressed
|
||||
//! );
|
||||
//!
|
||||
//! // send command to display
|
||||
//! connection.send(command).expect("send failed");
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub use bitvec;
|
||||
use bitvec::prelude::{BitVec, Msb0};
|
||||
|
||||
pub use crate::byte_grid::ByteGrid;
|
||||
pub use crate::command::{Brightness, Command, Offset, Origin};
|
||||
pub use crate::brightness::{Brightness, BrightnessGrid};
|
||||
pub use crate::command::{Command, Cp437Grid, Offset};
|
||||
pub use crate::compression_code::CompressionCode;
|
||||
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;
|
||||
|
||||
type SpBitVec = BitVec<u8, Msb0>;
|
||||
|
||||
mod byte_grid;
|
||||
mod brightness;
|
||||
mod command;
|
||||
mod command_code;
|
||||
mod compression;
|
||||
|
|
@ -24,28 +57,84 @@ mod compression_code;
|
|||
mod connection;
|
||||
mod data_ref;
|
||||
mod grid;
|
||||
mod origin;
|
||||
mod packet;
|
||||
mod pixel_grid;
|
||||
mod primitive_grid;
|
||||
|
||||
/// size of a single tile in one dimension
|
||||
pub const TILE_SIZE: usize = 8;
|
||||
|
||||
/// tile count in the x-direction
|
||||
/// Display tile count in the x-direction
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Cp437Grid, TILE_HEIGHT, TILE_WIDTH};
|
||||
/// let grid = Cp437Grid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||
/// ```
|
||||
pub const TILE_WIDTH: usize = 56;
|
||||
|
||||
/// tile count in the y-direction
|
||||
/// Display tile count in the y-direction
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Cp437Grid, TILE_HEIGHT, TILE_WIDTH};
|
||||
/// let grid = Cp437Grid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||
/// ```
|
||||
pub const TILE_HEIGHT: usize = 20;
|
||||
|
||||
/// screen width in pixels
|
||||
/// Display width in pixels
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, PixelGrid};
|
||||
/// let grid = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||
/// ```
|
||||
pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
|
||||
|
||||
/// screen height in pixels
|
||||
/// Display height in pixels
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, PixelGrid};
|
||||
/// let grid = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||
/// ```
|
||||
pub const PIXEL_HEIGHT: usize = TILE_HEIGHT * TILE_SIZE;
|
||||
|
||||
/// pixel count on whole screen
|
||||
pub const PIXEL_COUNT: usize = PIXEL_WIDTH * PIXEL_HEIGHT;
|
||||
|
||||
/// Actual hardware limit is around 28-29ms/frame. Rounded up for less dropped packets.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::time::Instant;
|
||||
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, PixelGrid};
|
||||
/// # let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
||||
/// # .expect("connection failed");
|
||||
/// # let pixels = PixelGrid::max_sized();
|
||||
/// loop {
|
||||
/// let start = Instant::now();
|
||||
///
|
||||
/// // Change pixels here
|
||||
///
|
||||
/// connection.send(Command::BitmapLinearWin(
|
||||
/// Origin::new(0,0),
|
||||
/// pixels,
|
||||
/// CompressionCode::Lzma
|
||||
/// ))
|
||||
/// .expect("send failed");
|
||||
///
|
||||
/// // warning: will crash if resulting duration is negative, e.g. when resuming from standby
|
||||
/// std::thread::sleep(FRAME_PACING - start.elapsed());
|
||||
/// # break; // prevent doctest from hanging
|
||||
/// }
|
||||
/// ```
|
||||
pub const FRAME_PACING: Duration = Duration::from_millis(30);
|
||||
|
||||
// include README.md in doctest
|
||||
|
|
|
|||
48
crates/servicepoint/src/origin.rs
Normal file
48
crates/servicepoint/src/origin.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
/// An origin marks the top left position of a window sent to the display.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Origin<Unit: DisplayUnit> {
|
||||
/// position in the width direction
|
||||
pub x: usize,
|
||||
/// position in the height direction
|
||||
pub y: usize,
|
||||
phantom_data: PhantomData<Unit>,
|
||||
}
|
||||
|
||||
impl<Unit: DisplayUnit> Origin<Unit> {
|
||||
/// Create a new `Origin` instance for the provided position.
|
||||
pub fn new(x: usize, y: usize) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
phantom_data: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DisplayUnit> std::ops::Add<Origin<T>> for Origin<T> {
|
||||
type Output = Origin<T>;
|
||||
|
||||
fn add(self, rhs: Origin<T>) -> Self::Output {
|
||||
Origin {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
phantom_data: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DisplayUnit {}
|
||||
|
||||
/// Marks something to be measured in number of pixels.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Pixels();
|
||||
|
||||
/// Marks something to be measured in number of iles.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Tiles();
|
||||
|
||||
impl DisplayUnit for Pixels {}
|
||||
|
||||
impl DisplayUnit for Tiles {}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
use std::mem::size_of;
|
||||
|
||||
use crate::command_code::CommandCode;
|
||||
use crate::compression::into_compressed;
|
||||
use crate::{
|
||||
Command, CompressionCode, Grid, Offset, Origin, PixelGrid, Pixels,
|
||||
TILE_SIZE,
|
||||
};
|
||||
|
||||
/// A raw header. Should probably not be used directly.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Header(pub u16, pub u16, pub u16, pub u16, pub u16);
|
||||
|
|
@ -58,6 +65,151 @@ impl TryFrom<&[u8]> for Packet {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Command> for Packet {
|
||||
/// Move the `Command` into a `Packet` instance for sending.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn from(value: Command) -> Self {
|
||||
match value {
|
||||
Command::Clear => Self::command_code_only(CommandCode::Clear),
|
||||
Command::FadeOut => Self::command_code_only(CommandCode::FadeOut),
|
||||
Command::HardReset => {
|
||||
Self::command_code_only(CommandCode::HardReset)
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
Command::BitmapLegacy => {
|
||||
Self::command_code_only(CommandCode::BitmapLegacy)
|
||||
}
|
||||
Command::CharBrightness(origin, grid) => Packet(
|
||||
Header(
|
||||
CommandCode::CharBrightness.into(),
|
||||
origin.x as u16,
|
||||
origin.y as u16,
|
||||
grid.width() as u16,
|
||||
grid.height() as u16,
|
||||
),
|
||||
grid.into(),
|
||||
),
|
||||
Command::Brightness(brightness) => Packet(
|
||||
Header(
|
||||
CommandCode::Brightness.into(),
|
||||
0x00000,
|
||||
0x0000,
|
||||
0x0000,
|
||||
0x0000,
|
||||
),
|
||||
vec![brightness.into()],
|
||||
),
|
||||
Command::BitmapLinearWin(origin, pixels, compression) => {
|
||||
Self::bitmap_win_into_packet(origin, pixels, compression)
|
||||
}
|
||||
Command::BitmapLinear(offset, bits, compression) => {
|
||||
Self::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinear,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::BitmapLinearAnd(offset, bits, compression) => {
|
||||
Self::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinearAnd,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::BitmapLinearOr(offset, bits, compression) => {
|
||||
Self::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinearOr,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::BitmapLinearXor(offset, bits, compression) => {
|
||||
Self::bitmap_linear_into_packet(
|
||||
CommandCode::BitmapLinearXor,
|
||||
offset,
|
||||
compression,
|
||||
bits.into(),
|
||||
)
|
||||
}
|
||||
Command::Cp437Data(origin, grid) => Packet(
|
||||
Header(
|
||||
CommandCode::Cp437Data.into(),
|
||||
origin.x as u16,
|
||||
origin.y as u16,
|
||||
grid.width() as u16,
|
||||
grid.height() as u16,
|
||||
),
|
||||
grid.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
/// Helper method for `BitMapLinear*`-Commands into `Packet`
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn bitmap_linear_into_packet(
|
||||
command: CommandCode,
|
||||
offset: Offset,
|
||||
compression: CompressionCode,
|
||||
payload: Vec<u8>,
|
||||
) -> Packet {
|
||||
let length = payload.len() as u16;
|
||||
let payload = into_compressed(compression, payload);
|
||||
Packet(
|
||||
Header(
|
||||
command.into(),
|
||||
offset as u16,
|
||||
length,
|
||||
compression.into(),
|
||||
0,
|
||||
),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn bitmap_win_into_packet(
|
||||
origin: Origin<Pixels>,
|
||||
pixels: PixelGrid,
|
||||
compression: CompressionCode,
|
||||
) -> Packet {
|
||||
debug_assert_eq!(origin.x % 8, 0);
|
||||
debug_assert_eq!(pixels.width() % 8, 0);
|
||||
|
||||
let tile_x = (origin.x / TILE_SIZE) as u16;
|
||||
let tile_w = (pixels.width() / TILE_SIZE) as u16;
|
||||
let pixel_h = pixels.height() as u16;
|
||||
let payload = into_compressed(compression, pixels.into());
|
||||
let command = match 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(command.into(), tile_x, origin.y as u16, tile_w, pixel_h),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
/// Helper method for creating empty packets only containing the command code
|
||||
fn command_code_only(code: CommandCode) -> Packet {
|
||||
Packet(Header(code.into(), 0x0000, 0x0000, 0x0000, 0x0000), vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Header, Packet};
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ impl Grid<bool> for PixelGrid {
|
|||
}
|
||||
}
|
||||
|
||||
impl DataRef for PixelGrid {
|
||||
impl DataRef<u8> for PixelGrid {
|
||||
fn data_ref_mut(&mut self) -> &mut [u8] {
|
||||
self.bit_vec.as_raw_mut_slice()
|
||||
}
|
||||
|
|
|
|||
322
crates/servicepoint/src/primitive_grid.rs
Normal file
322
crates/servicepoint/src/primitive_grid.rs
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
use std::slice::{Iter, IterMut};
|
||||
|
||||
use crate::{DataRef, Grid};
|
||||
|
||||
pub trait PrimitiveGridType: Sized + Default + Copy + Clone {}
|
||||
impl<T: Sized + Default + Copy + Clone> PrimitiveGridType for T {}
|
||||
|
||||
/// A 2D grid of bytes
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PrimitiveGrid<T: PrimitiveGridType> {
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T: PrimitiveGridType> PrimitiveGrid<T> {
|
||||
/// 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.
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
data: vec![Default::default(); width * height],
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a `PrimitiveGrid` with the specified dimensions from the provided data.
|
||||
///
|
||||
/// returns: `PrimitiveGrid` that contains a copy of the provided data
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the dimensions and data size do not match exactly.
|
||||
#[must_use]
|
||||
pub fn load(width: usize, height: usize, data: &[T]) -> Self {
|
||||
assert_eq!(width * height, data.len());
|
||||
Self {
|
||||
data: Vec::from(data),
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all cells in `PrimitiveGrid`.
|
||||
///
|
||||
/// Order is equivalent to the following loop:
|
||||
/// ```
|
||||
/// # use servicepoint::{PrimitiveGrid, Grid};
|
||||
/// # let grid = PrimitiveGrid::<u8>::new(2,2);
|
||||
/// for y in 0..grid.height() {
|
||||
/// for x in 0..grid.width() {
|
||||
/// grid.get(x, y);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter(&self) -> Iter<T> {
|
||||
self.data.iter()
|
||||
}
|
||||
|
||||
/// Iterate over all rows in `PrimitiveGrid` top to bottom.
|
||||
pub fn iter_rows(&self) -> IterRows<T> {
|
||||
IterRows {
|
||||
byte_grid: self,
|
||||
row: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator that allows modifying each value.
|
||||
///
|
||||
/// The iterator yields all cells from top left to bottom right.
|
||||
pub fn iter_mut(&mut self) -> IterMut<T> {
|
||||
self.data.iter_mut()
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the current value at the specified position.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `x` and `y`: position of the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When accessing `x` or `y` out of bounds.
|
||||
pub fn get_ref_mut(&mut self, x: usize, y: usize) -> &mut T {
|
||||
self.assert_in_bounds(x, y);
|
||||
&mut self.data[x + y * self.width]
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the current value at the specified position if position is in bounds.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `x` and `y`: position of the cell
|
||||
///
|
||||
/// returns: Reference to cell or None
|
||||
pub fn get_ref_mut_optional(
|
||||
&mut self,
|
||||
x: isize,
|
||||
y: isize,
|
||||
) -> Option<&mut T> {
|
||||
if self.is_in_bounds(x, y) {
|
||||
Some(&mut self.data[x as usize + y as usize * self.width])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveGridType> Grid<T> for PrimitiveGrid<T> {
|
||||
/// Sets the value of the cell at the specified position in the `PrimitiveGrid.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `x` and `y`: position of the cell
|
||||
/// * `value`: the value to write to the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When accessing `x` or `y` out of bounds.
|
||||
fn set(&mut self, x: usize, y: usize, value: T) {
|
||||
self.assert_in_bounds(x, y);
|
||||
self.data[x + y * self.width] = value;
|
||||
}
|
||||
|
||||
/// Gets the current value at the specified position.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When accessing `x` or `y` out of bounds.
|
||||
fn get(&self, x: usize, y: usize) -> T {
|
||||
self.assert_in_bounds(x, y);
|
||||
self.data[x + y * self.width]
|
||||
}
|
||||
|
||||
fn fill(&mut self, value: T) {
|
||||
self.data.fill(value);
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveGridType> DataRef<T> for PrimitiveGrid<T> {
|
||||
/// Get the underlying byte rows mutable
|
||||
fn data_ref_mut(&mut self) -> &mut [T] {
|
||||
self.data.as_mut_slice()
|
||||
}
|
||||
|
||||
/// Get the underlying byte rows read only
|
||||
fn data_ref(&self) -> &[T] {
|
||||
self.data.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveGridType> From<PrimitiveGrid<T>> for Vec<T> {
|
||||
/// Turn into the underlying `Vec<u8>` containing the rows of bytes.
|
||||
fn from(value: PrimitiveGrid<T>) -> Self {
|
||||
value.data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IterRows<'t, T: PrimitiveGridType> {
|
||||
byte_grid: &'t PrimitiveGrid<T>,
|
||||
row: usize,
|
||||
}
|
||||
|
||||
impl<'t, T: PrimitiveGridType> Iterator for IterRows<'t, T> {
|
||||
type Item = Iter<'t, T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.row >= self.byte_grid.height {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = self.row * self.byte_grid.width;
|
||||
let end = start + self.byte_grid.width;
|
||||
let result = self.byte_grid.data[start..end].iter();
|
||||
self.row += 1;
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{DataRef, Grid, PrimitiveGrid};
|
||||
|
||||
#[test]
|
||||
fn fill() {
|
||||
let mut grid = PrimitiveGrid::<usize>::new(2, 2);
|
||||
assert_eq!(grid.data, [0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
grid.fill(42);
|
||||
assert_eq!(grid.data, [42; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_set() {
|
||||
let mut grid = PrimitiveGrid::new(2, 2);
|
||||
assert_eq!(grid.get(0, 0), 0);
|
||||
assert_eq!(grid.get(1, 1), 0);
|
||||
|
||||
grid.set(0, 0, 42);
|
||||
grid.set(1, 0, 23);
|
||||
assert_eq!(grid.data, [42, 23, 0, 0]);
|
||||
|
||||
assert_eq!(grid.get(0, 0), 42);
|
||||
assert_eq!(grid.get(1, 0), 23);
|
||||
assert_eq!(grid.get(1, 1), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load() {
|
||||
let mut grid = PrimitiveGrid::new(2, 3);
|
||||
for x in 0..grid.width {
|
||||
for y in 0..grid.height {
|
||||
grid.set(x, y, (x + y) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(grid.data, [0, 1, 1, 2, 2, 3]);
|
||||
|
||||
let data: Vec<u8> = grid.into();
|
||||
|
||||
let grid = PrimitiveGrid::load(2, 3, &data);
|
||||
assert_eq!(grid.data, [0, 1, 1, 2, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mut_data_ref() {
|
||||
let mut vec = PrimitiveGrid::new(2, 2);
|
||||
|
||||
let data_ref = vec.data_ref_mut();
|
||||
data_ref.copy_from_slice(&[1, 2, 3, 4]);
|
||||
|
||||
assert_eq!(vec.data, [1, 2, 3, 4]);
|
||||
assert_eq!(vec.get(1, 0), 2)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
let mut vec = PrimitiveGrid::new(2, 2);
|
||||
vec.set(1, 1, 5);
|
||||
|
||||
let mut iter = vec.iter();
|
||||
assert_eq!(*iter.next().unwrap(), 0);
|
||||
assert_eq!(*iter.next().unwrap(), 0);
|
||||
assert_eq!(*iter.next().unwrap(), 0);
|
||||
assert_eq!(*iter.next().unwrap(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_mut() {
|
||||
let mut vec = PrimitiveGrid::new(2, 3);
|
||||
for (index, cell) in vec.iter_mut().enumerate() {
|
||||
*cell = index as u8;
|
||||
}
|
||||
|
||||
assert_eq!(vec.data_ref(), [0, 1, 2, 3, 4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_rows() {
|
||||
let vec = PrimitiveGrid::load(2, 3, &[0, 1, 1, 2, 2, 3]);
|
||||
for (y, row) in vec.iter_rows().enumerate() {
|
||||
for (x, val) in row.enumerate() {
|
||||
assert_eq!(*val, (x + y) as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn out_of_bounds_x() {
|
||||
let mut vec = PrimitiveGrid::load(2, 2, &[0, 1, 2, 3]);
|
||||
vec.set(2, 1, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn out_of_bounds_y() {
|
||||
let vec = PrimitiveGrid::load(2, 2, &[0, 1, 2, 3]);
|
||||
vec.get(1, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_mut() {
|
||||
let mut vec = PrimitiveGrid::load(2, 2, &[0, 1, 2, 3]);
|
||||
|
||||
let top_left = vec.get_ref_mut(0, 0);
|
||||
*top_left += 5;
|
||||
|
||||
assert_eq!(None, vec.get_ref_mut_optional(2, 2));
|
||||
assert_eq!(Some(&mut 5), vec.get_ref_mut_optional(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional() {
|
||||
let mut grid = PrimitiveGrid::load(2, 2, &[0, 1, 2, 3]);
|
||||
grid.set_optional(0, 0, 5);
|
||||
grid.set_optional(-1, 0, 8);
|
||||
grid.set_optional(0, 8, 42);
|
||||
assert_eq!(grid.data, [5, 1, 2, 3]);
|
||||
|
||||
assert_eq!(grid.get_optional(0, 0), Some(5));
|
||||
assert_eq!(grid.get_optional(0, 8), None);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue