split ruby gem into separate repository
All checks were successful
Rust / build (push) Successful in 5m1s
All checks were successful
Rust / build (push) Successful in 5m1s
This commit is contained in:
parent
2f7a2dfd62
commit
31dac283ef
|
@ -1 +0,0 @@
|
|||
use flake
|
23
.github/workflows/rust.yml
vendored
23
.github/workflows/rust.yml
vendored
|
@ -19,27 +19,22 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Update repos
|
||||
run: sudo apt-get update -qq
|
||||
- name: Install rust toolchain
|
||||
run: sudo apt-get install -qy cargo rust-clippy
|
||||
- name: install lzma
|
||||
run: sudo apt-get update && sudo apt-get install -y liblzma-dev
|
||||
run: sudo apt-get install -qy liblzma-dev
|
||||
- name: install ruby
|
||||
run: sudo apt-get install -qy ruby
|
||||
|
||||
- name: Run Clippy
|
||||
run: cargo clippy --all-targets --all-features
|
||||
- name: generate bindings
|
||||
run: ./generate-binding.sh
|
||||
- name: check that generated files did not change
|
||||
run: output=$(git status --porcelain) && [ -z "$output" ]
|
||||
|
||||
- name: build default features
|
||||
run: cargo build --all --verbose
|
||||
- name: build default features -- examples
|
||||
run: cargo build --examples --verbose
|
||||
- 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
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "servicepoint-binding-uniffi"]
|
||||
path = servicepoint-binding-uniffi
|
||||
url = https://git.berlin.ccc.de/servicepoint/servicepoint-binding-uniffi.git
|
|
@ -1,31 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
Contributions are accepted in any form (issues, documentation, feature requests, code, review, ...).
|
||||
|
||||
All creatures welcome.
|
||||
|
||||
If you have access, please contribute on the [CCCB Forgejo](https://git.berlin.ccc.de/servicepoint/servicepoint).
|
||||
Contributions on GitHub will be copied over and merged there.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Feel free to create a PR, even if your change is not done yet.
|
||||
|
||||
Mark your PR as a draft as long as you do not want it to be merged.
|
||||
|
||||
The main branch is supposed to be a working version, including language bindings,
|
||||
which means sometimes your PR may be merged into a temporary development branch.
|
||||
|
||||
Unit tests and documentation are required for the core library.
|
||||
|
||||
## Language bindings
|
||||
|
||||
Pull requests for your preferred language will be accepted.
|
||||
If there is no code generator, it should call the C ABI methods provided by `servicepoint_binding_c`.
|
||||
It should be able to send most of the basic commands in a way the simulator accepts, receiving is
|
||||
not required for the merge.
|
||||
|
||||
It is okay for the feature set of a language binding to lag behind the one of the rust crate.
|
||||
This also means you do not have to expose a feature to all the language bindings when adding something to the core.
|
||||
|
||||
If your change may break other language bindings, please note that in your PR description so someone can check them.
|
868
Cargo.lock
generated
868
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -1,17 +1,7 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/servicepoint",
|
||||
"crates/servicepoint_binding_c",
|
||||
"crates/servicepoint_binding_c/examples/lang_c",
|
||||
"crates/servicepoint_binding_uniffi"
|
||||
"uniffi-bindgen",
|
||||
"servicepoint-binding-uniffi"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.13.1"
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing-docs = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
thiserror = "2.0"
|
||||
|
|
57
README.md
57
README.md
|
@ -1,49 +1,34 @@
|
|||
# servicepoint
|
||||
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://docs.rs/servicepoint/latest/servicepoint/)
|
||||
[](./LICENSE)
|
||||
# ServicePoint
|
||||
|
||||
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point
|
||||
Display" or "Airport Display".
|
||||
This repository contains a library for parsing, encoding and sending packets to this display via UDP in multiple
|
||||
programming languages.
|
||||
|
||||
This project moved to [git.berlin.ccc.de/servicepoint/servicepoint](https://git.berlin.ccc.de/servicepoint/servicepoint).
|
||||
The [GitHub repository](https://github.com/cccb/servicepoint) remains available as a mirror.
|
||||
This crate contains C# bindings for the [servicepoint](https://git.berlin.ccc.de/servicepoint/servicepoint) library based on [servicepoint-binding-uniffi](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-uniffi).
|
||||
|
||||
Take a look at the contained crates for language specific information:
|
||||
Also take a look at the main project [README](https://git.berlin.ccc.de/servicepoint/servicepoint/src/branch/main/README.md) for more
|
||||
information.
|
||||
|
||||
| Crate | Languages | Readme |
|
||||
|-----------------------------|-----------------------------------|-----------------------------------------------------------------------------|
|
||||
| servicepoint | Rust | [servicepoint](crates/servicepoint/README.md) |
|
||||
| servicepoint_binding_c | C / C++ | [servicepoint_binding_c](crates/servicepoint_binding_c/README.md) |
|
||||
| servicepoint_binding_uniffi | C# / Python / Go / Kotlin / Swift | [servicepoint_binding_uniffi](crates/servicepoint_binding_uniffi/README.md) |
|
||||
## Note on stability
|
||||
|
||||
## Projects using the library
|
||||
This library is still in early development.
|
||||
You can absolutely use it, and it works, but expect minor breaking changes with every version bump.
|
||||
|
||||
- screen simulator (rust): [servicepoint-simulator](https://git.berlin.ccc.de/servicepoint/servicepoint-simulator)
|
||||
- A bunch of projects (C): [arfst23/ServicePoint](https://github.com/arfst23/ServicePoint), including
|
||||
- a CLI tool to display image files on the display or use the display as a TTY
|
||||
- a BSD games robots clone
|
||||
- a split-flap-display simulator
|
||||
- animations that play on the display
|
||||
- tanks game (C#): [servicepoint-tanks](https://github.com/kaesaecracker/cccb-tanks-cs)
|
||||
- cellular automata slideshow (rust): [servicepoint-life](https://github.com/kaesaecracker/servicepoint-life)
|
||||
- partial typescript implementation inspired by this library and browser stream: [cccb-servicepoint-browser](https://github.com/SamuelScheit/cccb-servicepoint-browser)
|
||||
- a CLI: [servicepoint-cli](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
|
||||
## Notes on differences to rust library
|
||||
|
||||
To add yourself to the list, open a pull request.
|
||||
- Performance will not be as good as the rust version:
|
||||
- most objects are reference counted.
|
||||
- objects with mutating methods will also have a MRSW lock
|
||||
- You will not get rust backtraces in release builds of the native code
|
||||
- Panic messages will work (PanicException)
|
||||
|
||||
You can also check out [awesome-servicepoint](https://github.com/stars/kaesaecracker/lists/awesome-servicepoint) for a bigger collection of projects, including some not related to this library.
|
||||
## Installation
|
||||
|
||||
If you have access, there is even more software linked in [the wiki](https://wiki.berlin.ccc.de/LED-Riesendisplay).
|
||||
Including this repository as a submodule and building from source is the recommended way of using the library.
|
||||
|
||||
## Contributing
|
||||
```bash
|
||||
git submodule add https://git.berlin.ccc.de/servicepoint/servicepoint.git
|
||||
git commit -m "add servicepoint submodule"
|
||||
```
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## What happened to servicepoint2?
|
||||
|
||||
After `servicepoint2` has been merged into `servicepoint`, `servicepoint2` will not continue to get any updates.
|
||||
Run `generate-binding.sh` to regenerate all bindings. This will also build `libservicepoint.so` (or equivalent on your
|
||||
platform).
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
# About the display
|
||||
|
||||
- Resolution: 352x160=56,320 pixels
|
||||
- Pixels are grouped into 44x20=880 tiles (8x8=64 pixels each)
|
||||
- Smallest addressable unit: row of pixels inside of a tile (8 pixels = 1 byte)
|
||||
- The brightness can only be set per tile
|
||||
- Screen content can be changed using a simple UDP protocol
|
||||
- Between each row of tiles, there is a gap of around 4 pixels size. This gap changes the aspect ratio of the display.
|
||||
|
||||
### Binary format
|
||||
|
||||
A UDP package sent to the display has a header size of 10 bytes.
|
||||
Each header value has a size of two bytes (unsigned 16 bit integer).
|
||||
Depending on the command, there can be a payload following the header.
|
||||
|
||||
To change screen contents, these commands are the most relevant:
|
||||
|
||||
1. Clear screen
|
||||
- command: `0x0002`
|
||||
- (rest does not matter)
|
||||
2. Send CP437 data: render specified text into rectangular region
|
||||
- command: `0x0003`
|
||||
- top left tile x
|
||||
- top left tile y
|
||||
- width in tiles
|
||||
- height in tiles
|
||||
- payload: (width in tiles * height in tiles) bytes
|
||||
- 1 byte = 1 character
|
||||
- each character is rendered into one tile (mono-spaced)
|
||||
- characters are encoded using code page 437
|
||||
3. Send bitmap window: set pixel states for a rectangular region
|
||||
- command: `0x0013`
|
||||
- top left tile x
|
||||
- top left _pixel_ y
|
||||
- width in tiles
|
||||
- height in _pixels_
|
||||
- payload: (width in tiles * height in pixels) bytes
|
||||
- network byte order
|
||||
- 1 bit = 1 pixel
|
||||
|
||||
There are other commands implemented as well, e.g. for changing the brightness.
|
|
@ -1,60 +0,0 @@
|
|||
[package]
|
||||
name = "servicepoint"
|
||||
version.workspace = true
|
||||
publish = true
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "A rust library for the CCCB Service Point Display."
|
||||
homepage = "https://docs.rs/crate/servicepoint"
|
||||
repository = "https://git.berlin.ccc.de/servicepoint/servicepoint"
|
||||
readme = "README.md"
|
||||
keywords = ["cccb", "cccb-servicepoint"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
bitvec = "1.0"
|
||||
flate2 = { version = "1.0", optional = true }
|
||||
bzip2 = { version = "0.5", optional = true }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
rust-lzma = { version = "0.6", optional = true }
|
||||
rand = { version = "0.8", optional = true }
|
||||
tungstenite = { version = "0.26", optional = true }
|
||||
once_cell = { version = "1.20", optional = true }
|
||||
thiserror.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["compression_lzma", "protocol_udp", "cp437"]
|
||||
compression_zlib = ["dep:flate2"]
|
||||
compression_bzip2 = ["dep:bzip2"]
|
||||
compression_lzma = ["dep:rust-lzma"]
|
||||
compression_zstd = ["dep:zstd"]
|
||||
all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"]
|
||||
rand = ["dep:rand"]
|
||||
protocol_udp = []
|
||||
protocol_websocket = ["dep:tungstenite"]
|
||||
cp437 = ["dep:once_cell"]
|
||||
|
||||
[[example]]
|
||||
name = "random_brightness"
|
||||
required-features = ["rand"]
|
||||
|
||||
[[example]]
|
||||
name = "game_of_life"
|
||||
required-features = ["rand"]
|
||||
|
||||
[[example]]
|
||||
name = "websocket"
|
||||
required-features = ["protocol_websocket"]
|
||||
|
||||
[dev-dependencies]
|
||||
# for examples
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
|
@ -1,67 +0,0 @@
|
|||
# servicepoint
|
||||
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://docs.rs/servicepoint/latest/servicepoint/)
|
||||
[](../../LICENSE)
|
||||
|
||||
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point
|
||||
Display" or "Airport Display".
|
||||
This crate contains a library for parsing, encoding and sending packets to this display via UDP.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cargo add servicepoint
|
||||
```
|
||||
or
|
||||
```toml
|
||||
[dependencies]
|
||||
servicepoint = "0.13.1"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```rust no_run
|
||||
fn main() {
|
||||
// establish connection
|
||||
let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
||||
.expect("connection failed");
|
||||
|
||||
// clear screen content
|
||||
connection.send(servicepoint::Command::Clear)
|
||||
.expect("send failed");
|
||||
}
|
||||
```
|
||||
|
||||
More examples are available in the crate.
|
||||
Execute `cargo run --example` for a list of available examples and `cargo run --example <name>` to run one.
|
||||
|
||||
## Note on stability
|
||||
|
||||
This library can be used for creative project or just to play around with the display.
|
||||
A decent coverage by unit tests prevents major problems and I also test this with my own projects, which mostly use up-to-date versions.
|
||||
|
||||
That being said, the API is still being worked on.
|
||||
Expect minor breaking changes with every version bump.
|
||||
Please specify the full version including patch in your Cargo.toml until 1.0 is released.
|
||||
|
||||
## Features
|
||||
|
||||
This library has multiple optional dependencies.
|
||||
You can choose to (not) include them by toggling the related features.
|
||||
|
||||
| Name | Default | Description |
|
||||
|--------------------|---------|--------------------------------------------|
|
||||
| compression_zlib | false | Enable additional compression algo |
|
||||
| compression_bzip2 | false | Enable additional compression algo |
|
||||
| compression_lzma | true | Enable additional compression algo |
|
||||
| compression_zstd | false | Enable additional compression algo |
|
||||
| protocol_udp | true | Connection::Udp |
|
||||
| protocol_websocket | false | Connection::WebSocket |
|
||||
| rand | false | impl Distribution<Brightness> for Standard |
|
||||
| cp437 | true | Conversion to and from CP-437 |
|
||||
|
||||
## Everything else
|
||||
|
||||
Look at the main project [README](https://git.berlin.ccc.de/servicepoint/servicepoint/src/branch/main/README.md) for further information.
|
|
@ -1,48 +0,0 @@
|
|||
//! An example for how to send text to the display.
|
||||
|
||||
use clap::Parser;
|
||||
use servicepoint::*;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "localhost:2342",
|
||||
help = "Address of the display"
|
||||
)]
|
||||
destination: String,
|
||||
#[arg(short, long, num_args = 1.., value_delimiter = '\n',
|
||||
help = "Text to send - specify multiple times for multiple lines")]
|
||||
text: Vec<String>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = true,
|
||||
help = "Clear screen before sending text"
|
||||
)]
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
/// example: `cargo run -- --text "Hallo" --text "CCCB"`
|
||||
fn main() {
|
||||
let mut cli = Cli::parse();
|
||||
if cli.text.is_empty() {
|
||||
cli.text.push("Hello, CCCB!".to_string());
|
||||
}
|
||||
|
||||
let connection = Connection::open(&cli.destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
if cli.clear {
|
||||
connection
|
||||
.send(Command::Clear)
|
||||
.expect("sending clear failed");
|
||||
}
|
||||
|
||||
let text = cli.text.join("\n");
|
||||
let grid = CharGrid::wrap_str(TILE_WIDTH, &text);
|
||||
connection
|
||||
.send(Command::Utf8Data(Origin::ZERO, grid))
|
||||
.expect("sending text failed");
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
//! Show a brightness level test pattern on screen
|
||||
|
||||
use clap::Parser;
|
||||
use servicepoint::*;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long, default_value = "localhost:2342")]
|
||||
destination: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let connection = Connection::open(cli.destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
let mut pixels = Bitmap::max_sized();
|
||||
pixels.fill(true);
|
||||
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
pixels,
|
||||
CompressionCode::Uncompressed,
|
||||
);
|
||||
connection.send(command).expect("send failed");
|
||||
|
||||
let max_brightness: u8 = Brightness::MAX.into();
|
||||
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
|
||||
let level = index as u8 % max_brightness;
|
||||
*byte = Brightness::try_from(level).unwrap();
|
||||
}
|
||||
|
||||
connection
|
||||
.send(Command::CharBrightness(Origin::ZERO, brightnesses))
|
||||
.expect("send failed");
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
//! A simple game of life implementation to show how to render graphics to the display.
|
||||
|
||||
use clap::Parser;
|
||||
use rand::{distributions, Rng};
|
||||
use servicepoint::*;
|
||||
use std::thread;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long, default_value = "localhost:2342")]
|
||||
destination: String,
|
||||
#[arg(short, long, default_value_t = 0.5f64)]
|
||||
probability: f64,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let connection = Connection::open(&cli.destination)
|
||||
.expect("could not connect to display");
|
||||
let mut field = make_random_field(cli.probability);
|
||||
|
||||
loop {
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
field.clone(),
|
||||
CompressionCode::Lzma,
|
||||
);
|
||||
connection.send(command).expect("could not send");
|
||||
thread::sleep(FRAME_PACING);
|
||||
field = iteration(field);
|
||||
}
|
||||
}
|
||||
|
||||
fn iteration(field: Bitmap) -> Bitmap {
|
||||
let mut next = field.clone();
|
||||
for x in 0..field.width() {
|
||||
for y in 0..field.height() {
|
||||
let old_state = field.get(x, y);
|
||||
let neighbors = count_neighbors(&field, x as i32, y as i32);
|
||||
|
||||
let new_state = matches!(
|
||||
(old_state, neighbors),
|
||||
(true, 2) | (true, 3) | (false, 3)
|
||||
);
|
||||
next.set(x, y, new_state);
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
fn count_neighbors(field: &Bitmap, x: i32, y: i32) -> i32 {
|
||||
let mut count = 0;
|
||||
for nx in x - 1..=x + 1 {
|
||||
for ny in y - 1..=y + 1 {
|
||||
if nx == x && ny == y {
|
||||
continue; // the cell itself does not count
|
||||
}
|
||||
|
||||
if nx < 0
|
||||
|| ny < 0
|
||||
|| nx >= field.width() as i32
|
||||
|| ny >= field.height() as i32
|
||||
{
|
||||
continue; // pixels outside the grid do not count
|
||||
}
|
||||
|
||||
if !field.get(nx as usize, ny as usize) {
|
||||
continue; // dead cells do not count
|
||||
}
|
||||
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
fn make_random_field(probability: f64) -> Bitmap {
|
||||
let mut field = Bitmap::max_sized();
|
||||
let mut rng = rand::thread_rng();
|
||||
let d = distributions::Bernoulli::new(probability).unwrap();
|
||||
for x in 0..field.width() {
|
||||
for y in 0..field.height() {
|
||||
field.set(x, y, rng.sample(d));
|
||||
}
|
||||
}
|
||||
field
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
//! A simple example for how to send pixel data to the display.
|
||||
|
||||
use clap::Parser;
|
||||
use servicepoint::*;
|
||||
use std::thread;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long, default_value = "localhost:2342")]
|
||||
destination: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let connection = Connection::open(Cli::parse().destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
let mut pixels = Bitmap::max_sized();
|
||||
for x_offset in 0..usize::MAX {
|
||||
pixels.fill(false);
|
||||
|
||||
for y in 0..PIXEL_HEIGHT {
|
||||
pixels.set((y + x_offset) % PIXEL_WIDTH, y, true);
|
||||
}
|
||||
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
pixels.clone(),
|
||||
CompressionCode::Lzma,
|
||||
);
|
||||
connection.send(command).expect("send failed");
|
||||
thread::sleep(FRAME_PACING);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
//! A simple example for how to set brightnesses for tiles on the screen.
|
||||
//! Continuously changes the tiles in a random window to random brightnesses.
|
||||
|
||||
use clap::Parser;
|
||||
use rand::Rng;
|
||||
use servicepoint::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long, default_value = "localhost:2342")]
|
||||
destination: String,
|
||||
#[arg(short, long, default_value_t = true)]
|
||||
enable_all: bool,
|
||||
#[arg(short, long, default_value_t = 100, allow_negative_numbers = false)]
|
||||
wait_ms: u64,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
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
|
||||
if cli.enable_all {
|
||||
let mut filled_grid = Bitmap::max_sized();
|
||||
filled_grid.fill(true);
|
||||
|
||||
let command = Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
filled_grid,
|
||||
CompressionCode::Lzma,
|
||||
);
|
||||
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();
|
||||
|
||||
// continuously update random windows to new random brightness
|
||||
loop {
|
||||
let min_size = 1;
|
||||
let x = rng.gen_range(0..TILE_WIDTH - min_size);
|
||||
let y = rng.gen_range(0..TILE_HEIGHT - min_size);
|
||||
|
||||
let w = rng.gen_range(min_size..=TILE_WIDTH - x);
|
||||
let h = rng.gen_range(min_size..=TILE_HEIGHT - y);
|
||||
|
||||
let origin = Origin::new(x, y);
|
||||
let mut luma = BrightnessGrid::new(w, h);
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
luma.set(x, y, rng.gen());
|
||||
}
|
||||
}
|
||||
|
||||
connection
|
||||
.send(Command::CharBrightness(origin, luma))
|
||||
.unwrap();
|
||||
std::thread::sleep(wait_duration);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
//! Example for how to use the WebSocket connection
|
||||
|
||||
use servicepoint::{
|
||||
Bitmap, Command, CompressionCode, Connection, Grid, Origin,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let connection =
|
||||
Connection::open_websocket("ws://localhost:8080".parse().unwrap())
|
||||
.unwrap();
|
||||
|
||||
connection.send(Command::Clear).unwrap();
|
||||
|
||||
let mut pixels = Bitmap::max_sized();
|
||||
pixels.fill(true);
|
||||
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
pixels,
|
||||
CompressionCode::Lzma,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
//! An example on how to modify the image on screen without knowing the current content.
|
||||
use clap::Parser;
|
||||
use servicepoint::*;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long, default_value = "localhost:2342")]
|
||||
destination: String,
|
||||
#[arg(short, long = "duration-ms", default_value_t = 5000)]
|
||||
time: u64,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let sleep_duration = Duration::max(
|
||||
FRAME_PACING,
|
||||
Duration::from_millis(cli.time / PIXEL_WIDTH as u64),
|
||||
);
|
||||
|
||||
let connection = Connection::open(cli.destination)
|
||||
.expect("could not connect to display");
|
||||
|
||||
let mut enabled_pixels = Bitmap::max_sized();
|
||||
enabled_pixels.fill(true);
|
||||
|
||||
for x_offset in 0..PIXEL_WIDTH {
|
||||
for y in 0..PIXEL_HEIGHT {
|
||||
enabled_pixels.set(x_offset % PIXEL_WIDTH, y, false);
|
||||
}
|
||||
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
enabled_pixels.clone(),
|
||||
CompressionCode::Lzma,
|
||||
))
|
||||
.expect("could not send command to display");
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/// A byte-packed vector of booleans.
|
||||
///
|
||||
/// The implementation is provided by [bitvec].
|
||||
/// This is an alias for the specific type of [bitvec::BitVec] used in this crate.
|
||||
pub type BitVec = bitvec::BitVec<u8, bitvec::Msb0>;
|
||||
|
||||
pub mod bitvec {
|
||||
//! Re-export of the used library [mod@bitvec].
|
||||
pub use bitvec::prelude::*;
|
||||
}
|
|
@ -1,379 +0,0 @@
|
|||
use crate::data_ref::DataRef;
|
||||
use crate::BitVec;
|
||||
use crate::*;
|
||||
use ::bitvec::order::Msb0;
|
||||
use ::bitvec::prelude::BitSlice;
|
||||
use ::bitvec::slice::IterMut;
|
||||
|
||||
/// A fixed-size 2D grid of booleans.
|
||||
///
|
||||
/// The values are stored in packed bytes (8 values per byte) in the same order as used by the display for storing pixels.
|
||||
/// This means that no conversion is necessary for sending the data to the display.
|
||||
/// The downside is that the width has to be a multiple of 8.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use servicepoint::Bitmap;
|
||||
/// let mut bitmap = Bitmap::new(8, 2);
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Bitmap {
|
||||
width: usize,
|
||||
height: usize,
|
||||
bit_vec: BitVec,
|
||||
}
|
||||
|
||||
impl Bitmap {
|
||||
/// Creates a new [Bitmap] with the specified dimensions.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `width`: size in pixels in x-direction
|
||||
/// - `height`: size in pixels in y-direction
|
||||
///
|
||||
/// returns: [Bitmap] initialized to all pixels off
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the width is not dividable by 8
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
assert_eq!(
|
||||
width % 8,
|
||||
0,
|
||||
"width must be a multiple of 8, but is {width}"
|
||||
);
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
bit_vec: BitVec::repeat(false, width * height),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new pixel grid with the size of the whole screen.
|
||||
#[must_use]
|
||||
pub fn max_sized() -> Self {
|
||||
Self::new(PIXEL_WIDTH, PIXEL_HEIGHT)
|
||||
}
|
||||
|
||||
/// Loads a [Bitmap] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `width`: size in pixels in x-direction
|
||||
/// - `height`: size in pixels in y-direction
|
||||
///
|
||||
/// returns: [Bitmap] that contains a copy of the provided data
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the dimensions and data size do not match exactly.
|
||||
/// - when the width is not dividable by 8
|
||||
#[must_use]
|
||||
pub fn load(width: usize, height: usize, data: &[u8]) -> Self {
|
||||
assert_eq!(width % 8, 0, "width must be a multiple of 8, but is {width}");
|
||||
assert_eq!(data.len(), height * width / 8, "data length must match dimensions, with 8 pixels per byte.");
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
bit_vec: BitVec::from_slice(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [Bitmap] with the specified width from the provided [BitVec] without copying it.
|
||||
///
|
||||
/// returns: [Bitmap] that contains the provided data.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the bitvec size is not dividable by the provided width
|
||||
/// - when the width is not dividable by 8
|
||||
#[must_use]
|
||||
pub fn from_bitvec(width: usize, bit_vec: BitVec) -> Self {
|
||||
assert_eq!(width % 8, 0, "width must be a multiple of 8, but is {width}");
|
||||
let len = bit_vec.len();
|
||||
let height = len / width;
|
||||
assert_eq!(0, len % width, "dimension mismatch - len {len} is not dividable by {width}");
|
||||
Self { width, height, bit_vec }
|
||||
}
|
||||
|
||||
/// Iterate over all cells in [Bitmap].
|
||||
///
|
||||
/// Order is equivalent to the following loop:
|
||||
/// ```
|
||||
/// # use servicepoint::{Bitmap, Grid};
|
||||
/// # let grid = Bitmap::new(8,2);
|
||||
/// for y in 0..grid.height() {
|
||||
/// for x in 0..grid.width() {
|
||||
/// grid.get(x, y);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter(&self) -> impl Iterator<Item = &bool> {
|
||||
self.bit_vec.iter().by_refs()
|
||||
}
|
||||
|
||||
/// Iterate over all cells in [Bitmap] mutably.
|
||||
///
|
||||
/// Order is equivalent to the following loop:
|
||||
/// ```
|
||||
/// # use servicepoint::{Bitmap, Grid};
|
||||
/// # let mut grid = Bitmap::new(8,2);
|
||||
/// # let value = false;
|
||||
/// for y in 0..grid.height() {
|
||||
/// for x in 0..grid.width() {
|
||||
/// grid.set(x, y, value);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use servicepoint::{Bitmap, Grid};
|
||||
/// # let mut grid = Bitmap::new(8,2);
|
||||
/// # let value = false;
|
||||
/// for (index, mut pixel) in grid.iter_mut().enumerate() {
|
||||
/// pixel.set(index % 2 == 0)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> {
|
||||
self.bit_vec.iter_mut()
|
||||
}
|
||||
|
||||
/// Iterate over all rows in [Bitmap] top to bottom.
|
||||
pub fn iter_rows(&self) -> IterRows {
|
||||
IterRows {
|
||||
bitmap: self,
|
||||
row: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid<bool> for Bitmap {
|
||||
/// Sets the value of the specified position in the [Bitmap].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `x` and `y`: position of the cell
|
||||
/// - `value`: the value to write to the cell
|
||||
///
|
||||
/// returns: old value of the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When accessing `x` or `y` out of bounds.
|
||||
fn set(&mut self, x: usize, y: usize, value: bool) {
|
||||
self.assert_in_bounds(x, y);
|
||||
self.bit_vec.set(x + y * self.width, value)
|
||||
}
|
||||
|
||||
fn get(&self, x: usize, y: usize) -> bool {
|
||||
self.assert_in_bounds(x, y);
|
||||
self.bit_vec[x + y * self.width]
|
||||
}
|
||||
|
||||
/// Sets the state of all pixels in the [Bitmap].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `this`: instance to write to
|
||||
/// - `value`: the value to set all pixels to
|
||||
fn fill(&mut self, value: bool) {
|
||||
self.bit_vec.fill(value);
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl DataRef<u8> for Bitmap {
|
||||
fn data_ref_mut(&mut self) -> &mut [u8] {
|
||||
self.bit_vec.as_raw_mut_slice()
|
||||
}
|
||||
|
||||
fn data_ref(&self) -> &[u8] {
|
||||
self.bit_vec.as_raw_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bitmap> for Vec<u8> {
|
||||
/// Turns a [Bitmap] into the underlying [`Vec<u8>`].
|
||||
fn from(value: Bitmap) -> Self {
|
||||
value.bit_vec.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bitmap> for BitVec {
|
||||
/// Turns a [Bitmap] into the underlying [BitVec].
|
||||
fn from(value: Bitmap) -> Self {
|
||||
value.bit_vec
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ValueGrid<bool>> for Bitmap {
|
||||
/// Converts a grid of [bool]s into a [Bitmap].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the width of `value` is not dividable by 8
|
||||
fn from(value: &ValueGrid<bool>) -> Self {
|
||||
let mut result = Self::new(value.width(), value.height());
|
||||
for (mut to, from) in result.iter_mut().zip(value.iter()) {
|
||||
*to = *from;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Bitmap> for ValueGrid<bool> {
|
||||
/// Converts a [Bitmap] into a grid of [bool]s.
|
||||
fn from(value: &Bitmap) -> Self {
|
||||
let mut result = Self::new(value.width(), value.height());
|
||||
for (to, from) in result.iter_mut().zip(value.iter()) {
|
||||
*to = *from;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IterRows<'t> {
|
||||
bitmap: &'t Bitmap,
|
||||
row: usize,
|
||||
}
|
||||
|
||||
impl<'t> Iterator for IterRows<'t> {
|
||||
type Item = &'t BitSlice<u8, Msb0>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.row >= self.bitmap.height {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = self.row * self.bitmap.width;
|
||||
let end = start + self.bitmap.width;
|
||||
self.row += 1;
|
||||
Some(&self.bitmap.bit_vec[start..end])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{BitVec, Bitmap, DataRef, Grid, ValueGrid};
|
||||
|
||||
#[test]
|
||||
fn fill() {
|
||||
let mut grid = Bitmap::new(8, 2);
|
||||
assert_eq!(grid.data_ref(), [0x00, 0x00]);
|
||||
|
||||
grid.fill(true);
|
||||
assert_eq!(grid.data_ref(), [0xFF, 0xFF]);
|
||||
|
||||
grid.fill(false);
|
||||
assert_eq!(grid.data_ref(), [0x00, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_set() {
|
||||
let mut grid = Bitmap::new(8, 2);
|
||||
assert!(!grid.get(0, 0));
|
||||
assert!(!grid.get(1, 1));
|
||||
|
||||
grid.set(5, 0, true);
|
||||
grid.set(1, 1, true);
|
||||
assert_eq!(grid.data_ref(), [0x04, 0x40]);
|
||||
|
||||
assert!(grid.get(5, 0));
|
||||
assert!(grid.get(1, 1));
|
||||
assert!(!grid.get(1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load() {
|
||||
let mut grid = Bitmap::new(8, 3);
|
||||
for x in 0..grid.width {
|
||||
for y in 0..grid.height {
|
||||
grid.set(x, y, (x + y) % 2 == 0);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
|
||||
|
||||
let data: Vec<u8> = grid.into();
|
||||
|
||||
let grid = Bitmap::load(8, 3, &data);
|
||||
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn out_of_bounds_x() {
|
||||
let vec = Bitmap::new(8, 2);
|
||||
vec.get(8, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn out_of_bounds_y() {
|
||||
let mut vec = Bitmap::new(8, 2);
|
||||
vec.set(1, 2, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
let grid = Bitmap::new(8, 2);
|
||||
assert_eq!(16, grid.iter().count())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_rows() {
|
||||
let grid = Bitmap::load(8, 2, &[0x04, 0x40]);
|
||||
let mut iter = grid.iter_rows();
|
||||
|
||||
assert_eq!(iter.next().unwrap().count_ones(), 1);
|
||||
assert_eq!(iter.next().unwrap().count_ones(), 1);
|
||||
assert_eq!(None, iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_mut() {
|
||||
let mut grid = Bitmap::new(8, 2);
|
||||
for (index, mut pixel) in grid.iter_mut().enumerate() {
|
||||
pixel.set(index % 2 == 0);
|
||||
}
|
||||
assert_eq!(grid.data_ref(), [0xAA, 0xAA]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_ref_mut() {
|
||||
let mut grid = Bitmap::new(8, 2);
|
||||
let data = grid.data_ref_mut();
|
||||
data[1] = 0x0F;
|
||||
assert!(grid.get(7, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_bitvec() {
|
||||
let mut grid = Bitmap::new(8, 2);
|
||||
grid.set(0, 0, true);
|
||||
let bitvec: BitVec = grid.into();
|
||||
assert_eq!(bitvec.as_raw_slice(), [0x80, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bool_grid() {
|
||||
let original = ValueGrid::load(
|
||||
8,
|
||||
1,
|
||||
&[true, false, true, false, true, false, true, false],
|
||||
);
|
||||
let converted = Bitmap::from(&original);
|
||||
let reconverted = ValueGrid::from(&converted);
|
||||
assert_eq!(original, reconverted);
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
#[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, Eq, Ord, PartialOrd)]
|
||||
pub struct Brightness(u8);
|
||||
|
||||
impl From<Brightness> for u8 {
|
||||
fn from(brightness: Brightness) -> Self {
|
||||
Self::from(&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);
|
||||
|
||||
/// Create a brightness value without returning an error for brightnesses above [Brightness::MAX].
|
||||
///
|
||||
/// returns: the specified value as a [Brightness], or [Brightness::MAX].
|
||||
pub fn saturating_from(value: u8) -> Brightness {
|
||||
if value > Brightness::MAX.into() {
|
||||
Brightness::MAX
|
||||
} else {
|
||||
Brightness(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Brightness {
|
||||
fn default() -> Self {
|
||||
Self::MAX
|
||||
}
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn brightness_from_u8() {
|
||||
assert_eq!(Err(100), Brightness::try_from(100));
|
||||
assert_eq!(Ok(Brightness(1)), Brightness::try_from(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "rand")]
|
||||
fn rand_brightness() {
|
||||
let mut rng = rand::thread_rng();
|
||||
for _ in 0..100 {
|
||||
let _: Brightness = rng.gen();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saturating_convert() {
|
||||
assert_eq!(Brightness::MAX, Brightness::saturating_from(100));
|
||||
assert_eq!(Brightness(5), Brightness::saturating_from(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "rand")]
|
||||
fn test() {
|
||||
let mut rng = rand::thread_rng();
|
||||
assert_ne!(rng.gen::<Brightness>(), rng.gen());
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
use crate::brightness::Brightness;
|
||||
use crate::grid::Grid;
|
||||
use crate::value_grid::ValueGrid;
|
||||
use crate::ByteGrid;
|
||||
|
||||
/// 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 = ValueGrid<Brightness>;
|
||||
|
||||
impl BrightnessGrid {
|
||||
/// Like [Self::load], but ignoring any out-of-range brightness values
|
||||
pub fn saturating_load(width: usize, height: usize, data: &[u8]) -> Self {
|
||||
ValueGrid::load(width, height, data).map(Brightness::saturating_from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrightnessGrid> for Vec<u8> {
|
||||
fn from(value: BrightnessGrid) -> Self {
|
||||
value
|
||||
.iter()
|
||||
.map(|brightness| (*brightness).into())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&BrightnessGrid> for ByteGrid {
|
||||
fn from(value: &BrightnessGrid) -> Self {
|
||||
let u8s = value
|
||||
.iter()
|
||||
.map(|brightness| (*brightness).into())
|
||||
.collect::<Vec<u8>>();
|
||||
ValueGrid::load(value.width(), value.height(), &u8s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ByteGrid> for BrightnessGrid {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: ByteGrid) -> Result<Self, Self::Error> {
|
||||
let brightnesses = value
|
||||
.iter()
|
||||
.map(|b| Brightness::try_from(*b))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(BrightnessGrid::load(
|
||||
value.width(),
|
||||
value.height(),
|
||||
&brightnesses,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::value_grid::ValueGrid;
|
||||
use crate::{Brightness, BrightnessGrid, DataRef, Grid};
|
||||
|
||||
#[test]
|
||||
fn to_u8_grid() {
|
||||
let mut grid = BrightnessGrid::new(2, 2);
|
||||
grid.set(1, 0, Brightness::MIN);
|
||||
grid.set(0, 1, Brightness::MAX);
|
||||
let actual = ValueGrid::from(&grid);
|
||||
assert_eq!(actual.data_ref(), &[11, 0, 11, 11]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saturating_load() {
|
||||
assert_eq!(
|
||||
BrightnessGrid::load(
|
||||
2,
|
||||
2,
|
||||
&[
|
||||
Brightness::MAX,
|
||||
Brightness::MAX,
|
||||
Brightness::MIN,
|
||||
Brightness::MAX
|
||||
]
|
||||
),
|
||||
BrightnessGrid::saturating_load(2, 2, &[255u8, 23, 0, 42])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
use crate::ValueGrid;
|
||||
|
||||
/// A 2d grid of bytes - see [ValueGrid].
|
||||
pub type ByteGrid = ValueGrid<u8>;
|
|
@ -1,298 +0,0 @@
|
|||
use crate::{Grid, SetValueSeriesError, TryLoadValueGridError, ValueGrid};
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
/// A grid containing UTF-8 characters.
|
||||
///
|
||||
/// To send a CharGrid to the display, use [Command::Utf8Data](crate::Command::Utf8Data).
|
||||
///
|
||||
/// Also see [ValueGrid] for the non-specialized operations and examples.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{CharGrid, Command, Connection, Origin};
|
||||
/// 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);
|
||||
/// ```
|
||||
pub type CharGrid = ValueGrid<char>;
|
||||
|
||||
impl CharGrid {
|
||||
/// Loads a [CharGrid] with the specified width from the provided text, wrapping to as many rows as needed.
|
||||
///
|
||||
/// The passed rows are extended with '\0' if needed.
|
||||
///
|
||||
/// returns: [CharGrid] that contains a copy of the provided data.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let grid = CharGrid::wrap_str(2, "abc\ndef");
|
||||
/// ```
|
||||
pub fn wrap_str(width: usize, text: &str) -> Self {
|
||||
let lines = text
|
||||
.split('\n')
|
||||
.flat_map(move |x| {
|
||||
x.chars()
|
||||
.collect::<Vec<char>>()
|
||||
.chunks(width)
|
||||
.map(|c| {
|
||||
let mut s = String::from_iter(c);
|
||||
s.push_str(&"\0".repeat(width - s.chars().count()));
|
||||
s
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
let height = lines.len();
|
||||
let mut result = Self::new(width, height);
|
||||
for (row, text_line) in lines.iter().enumerate() {
|
||||
result.set_row_str(row, text_line).unwrap()
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Copies a column from the grid as a String.
|
||||
///
|
||||
/// Returns [None] if x is out of bounds.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let grid = CharGrid::from("ab\ncd");
|
||||
/// let col = grid.get_col_str(0).unwrap(); // "ac"
|
||||
/// ```
|
||||
pub fn get_col_str(&self, x: usize) -> Option<String> {
|
||||
Some(String::from_iter(self.get_col(x)?))
|
||||
}
|
||||
|
||||
/// Copies a row from the grid as a String.
|
||||
///
|
||||
/// Returns [None] if y is out of bounds.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let grid = CharGrid::from("ab\ncd");
|
||||
/// let row = grid.get_row_str(0).unwrap(); // "ab"
|
||||
/// ```
|
||||
pub fn get_row_str(&self, y: usize) -> Option<String> {
|
||||
Some(String::from_iter(self.get_row(y)?))
|
||||
}
|
||||
|
||||
/// Overwrites a row in the grid with a str.
|
||||
///
|
||||
/// Returns [SetValueSeriesError] if y is out of bounds or `row` is not of the correct size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let mut grid = CharGrid::from("ab\ncd");
|
||||
/// grid.set_row_str(0, "ef").unwrap();
|
||||
/// ```
|
||||
pub fn set_row_str(
|
||||
&mut self,
|
||||
y: usize,
|
||||
value: &str,
|
||||
) -> Result<(), SetValueSeriesError> {
|
||||
self.set_row(y, value.chars().collect::<Vec<_>>().as_ref())
|
||||
}
|
||||
|
||||
/// Overwrites a column in the grid with a str.
|
||||
///
|
||||
/// Returns [SetValueSeriesError] if y is out of bounds or `row` is not of the correct size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let mut grid = CharGrid::from("ab\ncd");
|
||||
/// grid.set_col_str(0, "ef").unwrap();
|
||||
/// ```
|
||||
pub fn set_col_str(
|
||||
&mut self,
|
||||
x: usize,
|
||||
value: &str,
|
||||
) -> Result<(), SetValueSeriesError> {
|
||||
self.set_col(x, value.chars().collect::<Vec<_>>().as_ref())
|
||||
}
|
||||
|
||||
/// Loads a [CharGrid] with the specified dimensions from the provided UTF-8 bytes.
|
||||
///
|
||||
/// returns: [CharGrid] that contains the provided data, or [FromUtf8Error] if the data is invalid.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let grid = CharGrid::load_utf8(2, 2, [97u8, 98, 99, 100].to_vec());
|
||||
/// ```
|
||||
pub fn load_utf8(
|
||||
width: usize,
|
||||
height: usize,
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<CharGrid, LoadUtf8Error> {
|
||||
let s: Vec<char> = String::from_utf8(bytes)?.chars().collect();
|
||||
Ok(CharGrid::try_load(width, height, s)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LoadUtf8Error {
|
||||
#[error(transparent)]
|
||||
FromUtf8Error(#[from] FromUtf8Error),
|
||||
#[error(transparent)]
|
||||
TryLoadError(#[from] TryLoadValueGridError),
|
||||
}
|
||||
|
||||
impl From<&str> for CharGrid {
|
||||
fn from(value: &str) -> Self {
|
||||
let value = value.replace("\r\n", "\n");
|
||||
let mut lines = value.split('\n').collect::<Vec<_>>();
|
||||
let width = lines
|
||||
.iter()
|
||||
.fold(0, move |a, x| std::cmp::max(a, x.chars().count()));
|
||||
|
||||
while lines.last().is_some_and(move |line| line.is_empty()) {
|
||||
_ = lines.pop();
|
||||
}
|
||||
|
||||
let mut grid = Self::new(width, lines.len());
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
for (x, char) in line.chars().enumerate() {
|
||||
grid.set(x, y, char);
|
||||
}
|
||||
}
|
||||
|
||||
grid
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CharGrid {
|
||||
fn from(value: String) -> Self {
|
||||
CharGrid::from(&*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CharGrid> for String {
|
||||
fn from(grid: CharGrid) -> Self {
|
||||
String::from(&grid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CharGrid> for String {
|
||||
/// Converts a [CharGrid] into a [String].
|
||||
///
|
||||
/// Rows are separated by '\n'.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::CharGrid;
|
||||
/// let grid = CharGrid::from("ab\ncd");
|
||||
/// let string = String::from(grid);
|
||||
/// let grid = CharGrid::from(string);
|
||||
/// ```
|
||||
fn from(value: &CharGrid) -> Self {
|
||||
value
|
||||
.iter_rows()
|
||||
.map(String::from_iter)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CharGrid> for Vec<u8> {
|
||||
/// Converts a [CharGrid] into a [`Vec<u8>`].
|
||||
///
|
||||
/// Rows are not separated.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{CharGrid, Grid};
|
||||
/// let grid = CharGrid::from("ab\ncd");
|
||||
/// let height = grid.height();
|
||||
/// let width = grid.width();
|
||||
/// let grid = CharGrid::load_utf8(width, height, grid.into());
|
||||
/// ```
|
||||
fn from(value: &CharGrid) -> Self {
|
||||
String::from_iter(value.iter()).into_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CharGrid> for Vec<u8> {
|
||||
/// See [`From<&CharGrid>::from`].
|
||||
fn from(value: CharGrid) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn col_str() {
|
||||
let mut grid = CharGrid::new(2, 3);
|
||||
assert_eq!(grid.get_col_str(2), None);
|
||||
assert_eq!(grid.get_col_str(1), Some(String::from("\0\0\0")));
|
||||
assert_eq!(grid.set_col_str(1, "abc"), Ok(()));
|
||||
assert_eq!(grid.get_col_str(1), Some(String::from("abc")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_str() {
|
||||
let mut grid = CharGrid::new(2, 3);
|
||||
assert_eq!(grid.get_row_str(3), None);
|
||||
assert_eq!(grid.get_row_str(1), Some(String::from("\0\0")));
|
||||
assert_eq!(
|
||||
grid.set_row_str(1, "abc"),
|
||||
Err(SetValueSeriesError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 3
|
||||
})
|
||||
);
|
||||
assert_eq!(grid.set_row_str(1, "ab"), Ok(()));
|
||||
assert_eq!(grid.get_row_str(1), Some(String::from("ab")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_to_char_grid() {
|
||||
// conversion with .to_string() covers one more line
|
||||
let original = "Hello\r\nWorld!\n...\n".to_string();
|
||||
|
||||
let grid = CharGrid::from(original);
|
||||
assert_eq!(3, grid.height());
|
||||
assert_eq!("Hello\0\nWorld!\n...\0\0\0", String::from(grid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_bytes() {
|
||||
let grid = CharGrid::from("Hello\0\nWorld!\n...\0\0\0");
|
||||
let bytes: Vec<u8> = grid.clone().into();
|
||||
let copy =
|
||||
CharGrid::load_utf8(grid.width(), grid.height(), bytes).unwrap();
|
||||
assert_eq!(grid, copy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_string() {
|
||||
let grid = CharGrid::from("Hello\0\nWorld!\n...\0\0\0");
|
||||
let str: String = grid.clone().into();
|
||||
let copy = CharGrid::from(str);
|
||||
assert_eq!(grid, copy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_str() {
|
||||
let grid = CharGrid::wrap_str(2, "abc\ndef");
|
||||
assert_eq!(4, grid.height());
|
||||
assert_eq!("ab\nc\0\nde\nf\0", String::from(grid));
|
||||
}
|
||||
}
|
|
@ -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::Uncompressed].
|
||||
///
|
||||
/// If you want to archive the best performance (e.g. latency),
|
||||
/// you can try the different compression algorithms for your hardware and use case.
|
||||
///
|
||||
/// In memory, the payload is not compressed in the [Command].
|
||||
/// Payload (de-)compression happens when converting the [Command] into a [Packet] or vice versa.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use servicepoint::{Brightness, Command, Connection, Packet};
|
||||
/// #
|
||||
/// // create command
|
||||
/// let command = Command::Brightness(Brightness::MAX);
|
||||
///
|
||||
/// // 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. 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,
|
||||
|
||||
/// 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::{Command, Connection, Origin, CharGrid};
|
||||
/// # 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};
|
||||
/// # 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::{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};
|
||||
/// # 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::Uncompressed
|
||||
/// );
|
||||
///
|
||||
/// 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::{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),
|
||||
|
||||
/// 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::{Command, Connection};
|
||||
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
|
||||
/// connection.send(Command::HardReset).unwrap();
|
||||
/// ```
|
||||
HardReset,
|
||||
|
||||
/// <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,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
/// The u16 command codes used for the [Command]s.
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum CommandCode {
|
||||
Clear = 0x0002,
|
||||
Cp437Data = 0x0003,
|
||||
CharBrightness = 0x0005,
|
||||
Brightness = 0x0007,
|
||||
HardReset = 0x000b,
|
||||
FadeOut = 0x000d,
|
||||
#[deprecated]
|
||||
BitmapLegacy = 0x0010,
|
||||
BitmapLinear = 0x0012,
|
||||
BitmapLinearWinUncompressed = 0x0013,
|
||||
BitmapLinearAnd = 0x0014,
|
||||
BitmapLinearOr = 0x0015,
|
||||
BitmapLinearXor = 0x0016,
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
BitmapLinearWinZlib = 0x0017,
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
BitmapLinearWinBzip2 = 0x0018,
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
BitmapLinearWinLzma = 0x0019,
|
||||
Utf8Data = 0x0020,
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
BitmapLinearWinZstd = 0x001A,
|
||||
}
|
||||
|
||||
impl From<CommandCode> for u16 {
|
||||
/// returns the u16 command code corresponding to the enum value
|
||||
fn from(value: CommandCode) -> Self {
|
||||
value as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for CommandCode {
|
||||
type Error = ();
|
||||
|
||||
/// Returns the enum value for the specified `u16` or `Error` if the code is unknown.
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
value if value == CommandCode::Clear as u16 => {
|
||||
Ok(CommandCode::Clear)
|
||||
}
|
||||
value if value == CommandCode::Cp437Data as u16 => {
|
||||
Ok(CommandCode::Cp437Data)
|
||||
}
|
||||
value if value == CommandCode::CharBrightness as u16 => {
|
||||
Ok(CommandCode::CharBrightness)
|
||||
}
|
||||
value if value == CommandCode::Brightness as u16 => {
|
||||
Ok(CommandCode::Brightness)
|
||||
}
|
||||
value if value == CommandCode::HardReset as u16 => {
|
||||
Ok(CommandCode::HardReset)
|
||||
}
|
||||
value if value == CommandCode::FadeOut as u16 => {
|
||||
Ok(CommandCode::FadeOut)
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
value if value == CommandCode::BitmapLegacy as u16 => {
|
||||
Ok(CommandCode::BitmapLegacy)
|
||||
}
|
||||
value if value == CommandCode::BitmapLinear as u16 => {
|
||||
Ok(CommandCode::BitmapLinear)
|
||||
}
|
||||
value
|
||||
if value == CommandCode::BitmapLinearWinUncompressed as u16 =>
|
||||
{
|
||||
Ok(CommandCode::BitmapLinearWinUncompressed)
|
||||
}
|
||||
value if value == CommandCode::BitmapLinearAnd as u16 => {
|
||||
Ok(CommandCode::BitmapLinearAnd)
|
||||
}
|
||||
value if value == CommandCode::BitmapLinearOr as u16 => {
|
||||
Ok(CommandCode::BitmapLinearOr)
|
||||
}
|
||||
value if value == CommandCode::BitmapLinearXor as u16 => {
|
||||
Ok(CommandCode::BitmapLinearXor)
|
||||
}
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
value if value == CommandCode::BitmapLinearWinZstd as u16 => {
|
||||
Ok(CommandCode::BitmapLinearWinZstd)
|
||||
}
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
value if value == CommandCode::BitmapLinearWinLzma as u16 => {
|
||||
Ok(CommandCode::BitmapLinearWinLzma)
|
||||
}
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
value if value == CommandCode::BitmapLinearWinZlib as u16 => {
|
||||
Ok(CommandCode::BitmapLinearWinZlib)
|
||||
}
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
value if value == CommandCode::BitmapLinearWinBzip2 as u16 => {
|
||||
Ok(CommandCode::BitmapLinearWinBzip2)
|
||||
}
|
||||
value if value == CommandCode::Utf8Data as u16 => {
|
||||
Ok(CommandCode::Utf8Data)
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn clear() {
|
||||
assert_eq!(CommandCode::try_from(0x0002), Ok(CommandCode::Clear));
|
||||
assert_eq!(u16::from(CommandCode::Clear), 0x0002);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cp437_data() {
|
||||
assert_eq!(CommandCode::try_from(0x0003), Ok(CommandCode::Cp437Data));
|
||||
assert_eq!(u16::from(CommandCode::Cp437Data), 0x0003);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn char_brightness() {
|
||||
assert_eq!(CommandCode::try_from(0x0005), Ok(CommandCode::CharBrightness));
|
||||
assert_eq!(u16::from(CommandCode::CharBrightness), 0x0005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brightness() {
|
||||
assert_eq!(CommandCode::try_from(0x0007), Ok(CommandCode::Brightness));
|
||||
assert_eq!(u16::from(CommandCode::Brightness), 0x0007);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_reset() {
|
||||
assert_eq!(CommandCode::try_from(0x000b), Ok(CommandCode::HardReset));
|
||||
assert_eq!(u16::from(CommandCode::HardReset), 0x000b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fade_out() {
|
||||
assert_eq!(CommandCode::try_from(0x000d), Ok(CommandCode::FadeOut));
|
||||
assert_eq!(u16::from(CommandCode::FadeOut), 0x000d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn bitmap_legacy() {
|
||||
assert_eq!(CommandCode::try_from(0x0010), Ok(CommandCode::BitmapLegacy));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLegacy), 0x0010);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear() {
|
||||
assert_eq!(CommandCode::try_from(0x0012), Ok(CommandCode::BitmapLinear));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinear), 0x0012);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_and() {
|
||||
assert_eq!(CommandCode::try_from(0x0014), Ok(CommandCode::BitmapLinearAnd));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearAnd), 0x0014);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_xor() {
|
||||
assert_eq!(CommandCode::try_from(0x0016), Ok(CommandCode::BitmapLinearXor));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearXor), 0x0016);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
fn bitmap_win_zlib() {
|
||||
assert_eq!(CommandCode::try_from(0x0017), Ok(CommandCode::BitmapLinearWinZlib));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearWinZlib), 0x0017);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
fn bitmap_win_bzip2() {
|
||||
assert_eq!(CommandCode::try_from(0x0018), Ok(CommandCode::BitmapLinearWinBzip2));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearWinBzip2), 0x0018);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
fn bitmap_win_lzma() {
|
||||
assert_eq!(CommandCode::try_from(0x0019), Ok(CommandCode::BitmapLinearWinLzma));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearWinLzma), 0x0019);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
fn bitmap_win_zstd() {
|
||||
assert_eq!(CommandCode::try_from(0x001A), Ok(CommandCode::BitmapLinearWinZstd));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearWinZstd), 0x001A);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitmap_win_uncompressed() {
|
||||
assert_eq!(CommandCode::try_from(0x0013), Ok(CommandCode::BitmapLinearWinUncompressed));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearWinUncompressed), 0x0013);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_data() {
|
||||
assert_eq!(CommandCode::try_from(0x0020), Ok(CommandCode::Utf8Data));
|
||||
assert_eq!(u16::from(CommandCode::Utf8Data), 0x0020);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_or() {
|
||||
assert_eq!(CommandCode::try_from(0x0015), Ok(CommandCode::BitmapLinearOr));
|
||||
assert_eq!(u16::from(CommandCode::BitmapLinearOr), 0x0015);
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
#[allow(unused)]
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
use bzip2::read::{BzDecoder, BzEncoder};
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
use flate2::{FlushCompress, FlushDecompress, Status};
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder};
|
||||
|
||||
use crate::{CompressionCode, Payload};
|
||||
|
||||
pub(crate) fn into_decompressed(
|
||||
kind: CompressionCode,
|
||||
payload: Payload,
|
||||
) -> Option<Payload> {
|
||||
match kind {
|
||||
CompressionCode::Uncompressed => Some(payload),
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
CompressionCode::Zlib => {
|
||||
let mut decompress = flate2::Decompress::new(true);
|
||||
let mut buffer = [0u8; 10000];
|
||||
|
||||
let status = match decompress.decompress(
|
||||
&payload,
|
||||
&mut buffer,
|
||||
FlushDecompress::Finish,
|
||||
) {
|
||||
Err(_) => return None,
|
||||
Ok(status) => status,
|
||||
};
|
||||
|
||||
match status {
|
||||
Status::Ok => None,
|
||||
Status::BufError => None,
|
||||
Status::StreamEnd => Some(
|
||||
buffer[0..(decompress.total_out() as usize)].to_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
CompressionCode::Bzip2 => {
|
||||
let mut decoder = BzDecoder::new(&*payload);
|
||||
let mut decompressed = vec![];
|
||||
match decoder.read_to_end(&mut decompressed) {
|
||||
Err(_) => None,
|
||||
Ok(_) => Some(decompressed),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
CompressionCode::Lzma => match lzma::decompress(&payload) {
|
||||
Err(_) => None,
|
||||
Ok(decompressed) => Some(decompressed),
|
||||
},
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
CompressionCode::Zstd => {
|
||||
let mut decoder = match ZstdDecoder::new(&*payload) {
|
||||
Err(_) => return None,
|
||||
Ok(value) => value,
|
||||
};
|
||||
let mut decompressed = vec![];
|
||||
match decoder.read_to_end(&mut decompressed) {
|
||||
Err(_) => None,
|
||||
Ok(_) => Some(decompressed),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_compressed(
|
||||
kind: CompressionCode,
|
||||
payload: Payload,
|
||||
) -> Payload {
|
||||
match kind {
|
||||
CompressionCode::Uncompressed => 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()
|
||||
}
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
CompressionCode::Bzip2 => {
|
||||
let mut encoder =
|
||||
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,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
CompressionCode::Lzma => lzma::compress(&payload, 6).unwrap(),
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
CompressionCode::Zstd => {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
/// Specifies the kind of compression to use. Availability depends on features.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{Command, CompressionCode, Origin, Bitmap};
|
||||
/// // create command without payload compression
|
||||
/// # let pixels = Bitmap::max_sized();
|
||||
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Uncompressed);
|
||||
///
|
||||
/// // create command with payload compressed with lzma and appropriate header flags
|
||||
/// # let pixels = Bitmap::max_sized();
|
||||
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Lzma);
|
||||
/// ```
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CompressionCode {
|
||||
/// no compression
|
||||
Uncompressed = 0x0,
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
/// compress using flate2 with zlib header
|
||||
Zlib = 0x677a,
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
/// compress using bzip2
|
||||
Bzip2 = 0x627a,
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
/// compress using lzma
|
||||
Lzma = 0x6c7a,
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
/// compress using Zstandard
|
||||
Zstd = 0x7a73,
|
||||
}
|
||||
|
||||
impl From<CompressionCode> for u16 {
|
||||
fn from(value: CompressionCode) -> Self {
|
||||
value as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for CompressionCode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
value if value == CompressionCode::Uncompressed as u16 => {
|
||||
Ok(CompressionCode::Uncompressed)
|
||||
}
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
value if value == CompressionCode::Zlib as u16 => {
|
||||
Ok(CompressionCode::Zlib)
|
||||
}
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
value if value == CompressionCode::Bzip2 as u16 => {
|
||||
Ok(CompressionCode::Bzip2)
|
||||
}
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
value if value == CompressionCode::Lzma as u16 => {
|
||||
Ok(CompressionCode::Lzma)
|
||||
}
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
value if value == CompressionCode::Zstd as u16 => {
|
||||
Ok(CompressionCode::Zstd)
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn uncompressed() {
|
||||
assert_eq!(
|
||||
CompressionCode::try_from(0x0000),
|
||||
Ok(CompressionCode::Uncompressed)
|
||||
);
|
||||
assert_eq!(u16::from(CompressionCode::Uncompressed), 0x0000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_zlib")]
|
||||
fn zlib() {
|
||||
assert_eq!(
|
||||
CompressionCode::try_from(0x677a),
|
||||
Ok(CompressionCode::Zlib)
|
||||
);
|
||||
assert_eq!(u16::from(CompressionCode::Zlib), 0x677a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_bzip2")]
|
||||
fn bzip2() {
|
||||
assert_eq!(
|
||||
CompressionCode::try_from(0x627a),
|
||||
Ok(CompressionCode::Bzip2)
|
||||
);
|
||||
assert_eq!(u16::from(CompressionCode::Bzip2), 0x627a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_lzma")]
|
||||
fn lzma() {
|
||||
assert_eq!(
|
||||
CompressionCode::try_from(0x6c7a),
|
||||
Ok(CompressionCode::Lzma)
|
||||
);
|
||||
assert_eq!(u16::from(CompressionCode::Lzma), 0x6c7a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "compression_zstd")]
|
||||
fn zstd() {
|
||||
assert_eq!(
|
||||
CompressionCode::try_from(0x7a73),
|
||||
Ok(CompressionCode::Zstd)
|
||||
);
|
||||
assert_eq!(u16::from(CompressionCode::Zstd), 0x7a73);
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
use crate::packet::Packet;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// A connection to the display.
|
||||
///
|
||||
/// Used to send [Packets][Packet] or [Commands][crate::Command].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let connection = servicepoint::Connection::open("127.0.0.1:2342")
|
||||
/// .expect("connection failed");
|
||||
/// connection.send(servicepoint::Command::Clear)
|
||||
/// .expect("send failed");
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub enum Connection {
|
||||
/// A connection using the UDP protocol.
|
||||
///
|
||||
/// Use this when sending commands directly to the display.
|
||||
///
|
||||
/// Requires the feature "protocol_udp" which is enabled by default.
|
||||
#[cfg(feature = "protocol_udp")]
|
||||
Udp(std::net::UdpSocket),
|
||||
|
||||
/// A connection using the WebSocket protocol.
|
||||
///
|
||||
/// Note that you will need to forward the WebSocket messages via UDP to the display.
|
||||
/// You can use [servicepoint-websocket-relay] for this.
|
||||
///
|
||||
/// To create a new WebSocket automatically, use [Connection::open_websocket].
|
||||
///
|
||||
/// Requires the feature "protocol_websocket" which is disabled by default.
|
||||
///
|
||||
/// [servicepoint-websocket-relay]: https://github.com/kaesaecracker/servicepoint-websocket-relay
|
||||
#[cfg(feature = "protocol_websocket")]
|
||||
WebSocket(
|
||||
std::sync::Mutex<
|
||||
tungstenite::WebSocket<
|
||||
tungstenite::stream::MaybeTlsStream<std::net::TcpStream>,
|
||||
>,
|
||||
>,
|
||||
),
|
||||
|
||||
/// A fake connection for testing that does not actually send anything.
|
||||
Fake,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SendError {
|
||||
#[error("IO error occurred while sending")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[cfg(feature = "protocol_websocket")]
|
||||
#[error("WebSocket error occurred while sending")]
|
||||
WebsocketError(#[from] tungstenite::Error),
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Open a new UDP socket and connect to the provided host.
|
||||
///
|
||||
/// Note that this is UDP, which means that the open call can succeed even if the display is unreachable.
|
||||
///
|
||||
/// The address of the display in CCCB is `172.23.42.29:2342`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Any errors resulting from binding the udp socket.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let connection = servicepoint::Connection::open("127.0.0.1:2342")
|
||||
/// .expect("connection failed");
|
||||
/// ```
|
||||
#[cfg(feature = "protocol_udp")]
|
||||
pub fn open(
|
||||
addr: impl std::net::ToSocketAddrs + Debug,
|
||||
) -> std::io::Result<Self> {
|
||||
log::info!("connecting to {addr:?}");
|
||||
let socket = std::net::UdpSocket::bind("0.0.0.0:0")?;
|
||||
socket.connect(addr)?;
|
||||
Ok(Self::Udp(socket))
|
||||
}
|
||||
|
||||
/// Open a new WebSocket and connect to the provided host.
|
||||
///
|
||||
/// Requires the feature "protocol_websocket" which is disabled by default.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tungstenite::http::Uri;
|
||||
/// use servicepoint::{Command, Connection};
|
||||
/// let uri = "ws://localhost:8080".parse().unwrap();
|
||||
/// let mut connection = Connection::open_websocket(uri)
|
||||
/// .expect("could not connect");
|
||||
/// connection.send(Command::Clear)
|
||||
/// .expect("send failed");
|
||||
/// ```
|
||||
#[cfg(feature = "protocol_websocket")]
|
||||
pub fn open_websocket(
|
||||
uri: tungstenite::http::Uri,
|
||||
) -> tungstenite::Result<Self> {
|
||||
use tungstenite::{
|
||||
client::IntoClientRequest, connect, ClientRequestBuilder,
|
||||
};
|
||||
|
||||
log::info!("connecting to {uri:?}");
|
||||
|
||||
let request = ClientRequestBuilder::new(uri).into_client_request()?;
|
||||
let (sock, _) = connect(request)?;
|
||||
Ok(Self::WebSocket(std::sync::Mutex::new(sock)))
|
||||
}
|
||||
|
||||
/// Send something packet-like to the display. Usually this is in the form of a Command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `packet`: the packet-like to send
|
||||
///
|
||||
/// returns: true if packet was sent, otherwise false
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// let connection = servicepoint::Connection::Fake;
|
||||
/// // turn off all pixels on display
|
||||
/// connection.send(servicepoint::Command::Clear)
|
||||
/// .expect("send failed");
|
||||
/// ```
|
||||
pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> {
|
||||
let packet = packet.into();
|
||||
log::debug!("sending {packet:?}");
|
||||
let data: Vec<u8> = packet.into();
|
||||
match self {
|
||||
#[cfg(feature = "protocol_udp")]
|
||||
Connection::Udp(socket) => {
|
||||
socket
|
||||
.send(&data)
|
||||
.map_err(SendError::IoError)
|
||||
.map(move |_| ()) // ignore Ok value
|
||||
}
|
||||
#[cfg(feature = "protocol_websocket")]
|
||||
Connection::WebSocket(socket) => {
|
||||
let mut socket = socket.lock().unwrap();
|
||||
socket
|
||||
.send(tungstenite::Message::Binary(data.into()))
|
||||
.map_err(SendError::WebsocketError)
|
||||
}
|
||||
Connection::Fake => {
|
||||
let _ = data;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "protocol_websocket")]
|
||||
if let Connection::WebSocket(sock) = self {
|
||||
_ = sock.try_lock().map(move |mut sock| sock.close(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn send_fake() {
|
||||
let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
let packet = Packet::try_from(data).unwrap();
|
||||
Connection::Fake.send(packet).unwrap()
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
/// size of a single tile in one dimension
|
||||
pub const TILE_SIZE: usize = 8;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Display width in pixels
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, Bitmap};
|
||||
/// let grid = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||
/// ```
|
||||
pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
|
||||
|
||||
/// Display height in pixels
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, Bitmap};
|
||||
/// let grid = Bitmap::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, Bitmap};
|
||||
/// # let connection = servicepoint::Connection::Fake;
|
||||
/// # let pixels = Bitmap::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);
|
|
@ -1,115 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
/// Contains functions to convert between UTF-8 and Codepage 437.
|
||||
///
|
||||
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
|
||||
pub struct Cp437Converter;
|
||||
|
||||
/// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters
|
||||
///
|
||||
/// Mostly follows CP437, except 0x0A, which is kept for use as line ending.
|
||||
///
|
||||
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
|
||||
///
|
||||
/// Mostly copied from <https://github.com/kip93/cp437-tools>. License: GPL-3.0
|
||||
#[rustfmt::skip]
|
||||
const CP437_TO_UTF8: [char; 256] = [
|
||||
/* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '♪', '♫', '☼',
|
||||
/* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼',
|
||||
/* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/',
|
||||
/* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
|
||||
/* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||
/* 5X */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',']', '^', '_',
|
||||
/* 6X */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||
/* 7X */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂',
|
||||
/* 8X */ 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å',
|
||||
/* 9X */ 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ',
|
||||
/* AX */ 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»',
|
||||
/* BX */ '░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐',
|
||||
/* CX */ '└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧',
|
||||
/* DX */ '╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀',
|
||||
/* EX */ 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩',
|
||||
/* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ',
|
||||
];
|
||||
static UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
let pairs = CP437_TO_UTF8
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(index, char)| (*char, index as u8));
|
||||
HashMap::from_iter(pairs)
|
||||
});
|
||||
|
||||
impl Cp437Converter {
|
||||
const MISSING_CHAR_CP437: u8 = 0x3F; // '?'
|
||||
|
||||
/// Convert the provided bytes to UTF-8.
|
||||
pub fn cp437_to_str(cp437: &[u8]) -> String {
|
||||
cp437
|
||||
.iter()
|
||||
.map(move |char| Self::cp437_to_char(*char))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Convert a single CP-437 character to UTF-8.
|
||||
pub fn cp437_to_char(cp437: u8) -> char {
|
||||
CP437_TO_UTF8[cp437 as usize]
|
||||
}
|
||||
|
||||
/// Convert the provided text to CP-437 bytes.
|
||||
///
|
||||
/// Characters that are not available are mapped to '?'.
|
||||
pub fn str_to_cp437(utf8: &str) -> Vec<u8> {
|
||||
utf8.chars().map(Self::char_to_cp437).collect()
|
||||
}
|
||||
|
||||
/// Convert a single UTF-8 character to CP-437.
|
||||
pub fn char_to_cp437(utf8: char) -> u8 {
|
||||
*UTF8_TO_CP437
|
||||
.get(&utf8)
|
||||
.unwrap_or(&Self::MISSING_CHAR_CP437)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_feature_cp437 {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_str() {
|
||||
// test text from https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios
|
||||
let utf8 = r#"A quick brown fox jumps over the lazy dog.
|
||||
0123456789 ¿?¡!`'"., <>()[]{} &@%*^#$\/
|
||||
|
||||
* Wieniläinen sioux'ta puhuva ökyzombie diggaa Åsan roquefort-tacoja.
|
||||
* Ça me fait peur de fêter noël là, sur cette île bizarroïde où une mère et sa môme essaient de me tuer avec un gâteau à la cigüe brûlé.
|
||||
* Zwölf Boxkämpfer jagten Eva quer über den Sylter Deich.
|
||||
* El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.
|
||||
|
||||
┌─┬─┐ ╔═╦═╗ ╒═╤═╕ ╓─╥─╖
|
||||
│ │ │ ║ ║ ║ │ │ │ ║ ║ ║
|
||||
├─┼─┤ ╠═╬═╣ ╞═╪═╡ ╟─╫─╢
|
||||
└─┴─┘ ╚═╩═╝ ╘═╧═╛ ╙─╨─╜
|
||||
|
||||
░░░░░ ▐▀█▀▌ .·∙•○°○•∙·.
|
||||
▒▒▒▒▒ ▐ █ ▌ ☺☻ ♥♦♣♠ ♪♫☼
|
||||
▓▓▓▓▓ ▐▀█▀▌ $ ¢ £ ¥ ₧
|
||||
█████ ▐▄█▄▌ ◄►▲▼ ←→↑↓↕↨
|
||||
|
||||
⌠
|
||||
│dx ≡ Σ √x²ⁿ·δx
|
||||
⌡"#;
|
||||
|
||||
let cp437 = Cp437Converter::str_to_cp437(utf8);
|
||||
let actual = Cp437Converter::cp437_to_str(&cp437);
|
||||
assert_eq!(utf8, actual)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_invalid() {
|
||||
assert_eq!(
|
||||
Cp437Converter::cp437_to_char(Cp437Converter::char_to_cp437('😜')),
|
||||
'?'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
/// A grid containing codepage 437 characters.
|
||||
///
|
||||
/// The encoding is currently not enforced.
|
||||
pub type Cp437Grid = crate::value_grid::ValueGrid<u8>;
|
||||
|
||||
/// The error occurring when loading an invalid character
|
||||
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||
#[error(
|
||||
"The character {char:?} at position {index} is not a valid CP437 character"
|
||||
)]
|
||||
pub struct InvalidCharError {
|
||||
/// invalid character is at this position in input
|
||||
index: usize,
|
||||
/// the invalid character
|
||||
char: char,
|
||||
}
|
||||
|
||||
impl Cp437Grid {
|
||||
/// Load an ASCII-only [&str] into a [Cp437Grid] of specified width.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - for width == 0
|
||||
/// - on empty strings
|
||||
pub fn load_ascii(
|
||||
value: &str,
|
||||
width: usize,
|
||||
wrap: bool,
|
||||
) -> Result<Self, InvalidCharError> {
|
||||
assert!(width > 0);
|
||||
assert!(!value.is_empty());
|
||||
|
||||
let mut chars = {
|
||||
let mut x = 0;
|
||||
let mut y = 0;
|
||||
|
||||
for (index, char) in value.chars().enumerate() {
|
||||
if !char.is_ascii() {
|
||||
return Err(InvalidCharError { index, char });
|
||||
}
|
||||
|
||||
let is_lf = char == '\n';
|
||||
if is_lf || (wrap && x == width) {
|
||||
y += 1;
|
||||
x = 0;
|
||||
if is_lf {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
x += 1;
|
||||
}
|
||||
|
||||
Cp437Grid::new(width, y + 1)
|
||||
};
|
||||
|
||||
let mut x = 0;
|
||||
let mut y = 0;
|
||||
for char in value.chars().map(move |c| c as u8) {
|
||||
let is_lf = char == b'\n';
|
||||
if is_lf || (wrap && x == width) {
|
||||
y += 1;
|
||||
x = 0;
|
||||
if is_lf {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if wrap || x < width {
|
||||
chars.set(x, y, char);
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
|
||||
Ok(chars)
|
||||
}
|
||||
}
|
||||
|
||||
use crate::Grid;
|
||||
#[allow(unused)] // depends on features
|
||||
pub use feature_cp437::*;
|
||||
|
||||
#[cfg(feature = "cp437")]
|
||||
mod feature_cp437 {
|
||||
use super::*;
|
||||
use crate::{CharGrid, Cp437Converter};
|
||||
|
||||
impl From<&Cp437Grid> for CharGrid {
|
||||
fn from(value: &Cp437Grid) -> Self {
|
||||
value.map(Cp437Converter::cp437_to_char)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cp437Grid> for CharGrid {
|
||||
fn from(value: Cp437Grid) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CharGrid> for Cp437Grid {
|
||||
fn from(value: &CharGrid) -> Self {
|
||||
value.map(Cp437Converter::char_to_cp437)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CharGrid> for Cp437Grid {
|
||||
fn from(value: CharGrid) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn load_ascii_nowrap() {
|
||||
let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']
|
||||
.map(move |c| c as u8);
|
||||
let expected = Cp437Grid::load(5, 2, &chars);
|
||||
|
||||
let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap();
|
||||
// comma will be removed because line is too long and wrap is off
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_ascii_wrap() {
|
||||
let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']
|
||||
.map(move |c| c as u8);
|
||||
let expected = Cp437Grid::load(5, 2, &chars);
|
||||
|
||||
let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap();
|
||||
// line break will be added
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_ascii_invalid() {
|
||||
assert_eq!(
|
||||
Err(InvalidCharError {
|
||||
char: '🥶',
|
||||
index: 2
|
||||
}),
|
||||
Cp437Grid::load_ascii("?#🥶42", 3, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "cp437")]
|
||||
mod tests_feature_cp437 {
|
||||
use super::*;
|
||||
use crate::CharGrid;
|
||||
|
||||
#[test]
|
||||
fn round_trip_cp437() {
|
||||
let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']);
|
||||
let cp437 = Cp437Grid::from(utf8.clone());
|
||||
let actual = CharGrid::from(cp437);
|
||||
assert_eq!(actual, utf8);
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/// A trait for getting the underlying raw byte slices of data containers.
|
||||
///
|
||||
/// The expectation is that you can create an equal instance with this data given the additional
|
||||
/// metadata needed.
|
||||
pub trait DataRef<T> {
|
||||
/// Get the underlying bytes writable.
|
||||
///
|
||||
/// 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) -> &[T];
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
/// A two-dimensional grid of `T`
|
||||
pub trait Grid<T> {
|
||||
/// Sets the 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 set(&mut self, x: usize, y: usize, value: T);
|
||||
|
||||
/// Get 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;
|
||||
|
||||
/// Get the current value at the specified position if the position is inside of bounds
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// returns: Value at position or None
|
||||
fn get_optional(&self, x: isize, y: isize) -> Option<T> {
|
||||
if self.is_in_bounds(x, y) {
|
||||
Some(self.get(x as usize, y as usize))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the value at the specified position if the position is inside of bounds
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// returns: the old value or None
|
||||
fn set_optional(&mut self, x: isize, y: isize, value: T) -> bool {
|
||||
if self.is_in_bounds(x, y) {
|
||||
self.set(x as usize, y as usize, value);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets all cells in the grid to the specified value
|
||||
fn fill(&mut self, value: T);
|
||||
|
||||
/// the size in x-direction
|
||||
fn width(&self) -> usize;
|
||||
|
||||
/// the height in y-direction
|
||||
fn height(&self) -> usize;
|
||||
|
||||
/// Checks whether the specified signed position is in grid bounds
|
||||
fn is_in_bounds(&self, x: isize, y: isize) -> bool {
|
||||
x >= 0
|
||||
&& x < self.width() as isize
|
||||
&& y >= 0
|
||||
&& y < self.height() as isize
|
||||
}
|
||||
|
||||
/// Asserts that the specified unsigned position is in grid bounds.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When the specified position is out of bounds for this grid.
|
||||
fn assert_in_bounds(&self, x: usize, y: usize) {
|
||||
let width = self.width();
|
||||
assert!(x < width, "cannot access index [{x}, {y}] because x is outside of bounds [0..{width})");
|
||||
let height = self.height();
|
||||
assert!(y < height, "cannot access index [{x}, {y}] because x is outside of bounds [0..{height})");
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
//! Abstractions for the UDP protocol of the CCCB servicepoint display.
|
||||
//!
|
||||
//! Your starting point is a [Connection] to the display.
|
||||
//! With a connection, you can send [Command]s.
|
||||
//! When received, the display will update the state of its pixels.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ### Clear display
|
||||
//!
|
||||
//! ```rust
|
||||
//! use servicepoint::{Connection, Command};
|
||||
//!
|
||||
//! // establish a connection
|
||||
//! let connection = Connection::open("127.0.0.1:2342")
|
||||
//! .expect("connection failed");
|
||||
//!
|
||||
//! // turn off all pixels on display
|
||||
//! connection.send(Command::Clear)
|
||||
//! .expect("send failed");
|
||||
//! ```
|
||||
//!
|
||||
//! ### Set all pixels to on
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
|
||||
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
|
||||
//! // turn on all pixels in a grid
|
||||
//! let mut pixels = Bitmap::max_sized();
|
||||
//! pixels.fill(true);
|
||||
//!
|
||||
//! // create command to send pixels
|
||||
//! let command = Command::BitmapLinearWin(
|
||||
//! servicepoint::Origin::ZERO,
|
||||
//! pixels,
|
||||
//! CompressionCode::Uncompressed
|
||||
//! );
|
||||
//!
|
||||
//! // send command to display
|
||||
//! connection.send(command).expect("send failed");
|
||||
//! ```
|
||||
//!
|
||||
//! ### Send text
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap, CharGrid};
|
||||
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
|
||||
//! // create a text grid
|
||||
//! let mut grid = CharGrid::from("Hello\nCCCB?");
|
||||
//! // modify the grid
|
||||
//! grid.set(grid.width() - 1, 1, '!');
|
||||
//! // create the command to send the data
|
||||
//! let command = Command::Utf8Data(servicepoint::Origin::ZERO, grid);
|
||||
//! // send command to display
|
||||
//! connection.send(command).expect("send failed");
|
||||
//! ```
|
||||
|
||||
pub use crate::bit_vec::{bitvec, BitVec};
|
||||
pub use crate::bitmap::Bitmap;
|
||||
pub use crate::brightness::Brightness;
|
||||
pub use crate::brightness_grid::BrightnessGrid;
|
||||
pub use crate::byte_grid::ByteGrid;
|
||||
pub use crate::char_grid::CharGrid;
|
||||
pub use crate::command::{Command, Offset};
|
||||
pub use crate::compression_code::CompressionCode;
|
||||
pub use crate::connection::Connection;
|
||||
pub use crate::constants::*;
|
||||
pub use crate::cp437::Cp437Converter;
|
||||
pub use crate::cp437_grid::Cp437Grid;
|
||||
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::value_grid::{
|
||||
IterGridRows, SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
|
||||
};
|
||||
|
||||
mod bit_vec;
|
||||
mod bitmap;
|
||||
mod brightness;
|
||||
mod brightness_grid;
|
||||
mod byte_grid;
|
||||
mod char_grid;
|
||||
mod command;
|
||||
mod command_code;
|
||||
mod compression;
|
||||
mod compression_code;
|
||||
mod connection;
|
||||
mod constants;
|
||||
mod cp437_grid;
|
||||
mod data_ref;
|
||||
mod grid;
|
||||
mod origin;
|
||||
mod packet;
|
||||
mod value_grid;
|
||||
|
||||
#[cfg(feature = "cp437")]
|
||||
mod cp437;
|
||||
|
||||
// include README.md in doctest
|
||||
#[doc = include_str!("../README.md")]
|
||||
#[cfg(doctest)]
|
||||
pub struct ReadmeDocTests;
|
|
@ -1,122 +0,0 @@
|
|||
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)]
|
||||
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> {
|
||||
/// Top-left. Equivalent to `Origin::ZERO`.
|
||||
pub const ZERO: Self = Self {
|
||||
x: 0,
|
||||
y: 0,
|
||||
phantom_data: PhantomData,
|
||||
};
|
||||
|
||||
/// Create a new [Origin] instance for the provided position.
|
||||
pub fn new(x: usize, y: usize) -> Self {
|
||||
Self {
|
||||
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 {}
|
||||
|
||||
impl From<&Origin<Tiles>> for Origin<Pixels> {
|
||||
fn from(value: &Origin<Tiles>) -> Self {
|
||||
Self {
|
||||
x: value.x * TILE_SIZE,
|
||||
y: value.y * TILE_SIZE,
|
||||
phantom_data: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Origin<Pixels>> for Origin<Tiles> {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &Origin<Pixels>) -> Result<Self, Self::Error> {
|
||||
let (x, x_rem) = (value.x / TILE_SIZE, value.x % TILE_SIZE);
|
||||
if x_rem != 0 {
|
||||
return Err(());
|
||||
}
|
||||
let (y, y_rem) = (value.y / TILE_SIZE, value.y % TILE_SIZE);
|
||||
if y_rem != 0 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
x,
|
||||
y,
|
||||
phantom_data: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn origin_tile_to_pixel() {
|
||||
let tile: Origin<Tiles> = Origin::new(1, 2);
|
||||
let actual: Origin<Pixels> = Origin::from(&tile);
|
||||
let expected: Origin<Pixels> = Origin::new(8, 16);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_pixel_to_tile() {
|
||||
let pixel: Origin<Pixels> = Origin::new(8, 16);
|
||||
let actual: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
|
||||
let expected: Origin<Tiles> = Origin::new(1, 2);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn origin_pixel_to_tile_fail_y() {
|
||||
let pixel: Origin<Pixels> = Origin::new(8, 15);
|
||||
let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn origin_pixel_to_tile_fail_x() {
|
||||
let pixel: Origin<Pixels> = Origin::new(7, 16);
|
||||
let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
|
||||
}
|
||||
}
|
|
@ -1,358 +0,0 @@
|
|||
//! Raw packet manipulation.
|
||||
//!
|
||||
//! Should probably only be used directly to use features not exposed by the library.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Converting a packet to a command and back:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use servicepoint::{Command, Packet};
|
||||
//! # let command = Command::Clear;
|
||||
//! let packet: Packet = command.into();
|
||||
//! let command: Command = Command::try_from(packet).expect("could not read command from packet");
|
||||
//! ```
|
||||
//!
|
||||
//! Converting a packet to bytes and back:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use servicepoint::{Command, Packet};
|
||||
//! # let command = Command::Clear;
|
||||
//! # let packet: Packet = command.into();
|
||||
//! let bytes: Vec<u8> = packet.into();
|
||||
//! let packet = Packet::try_from(bytes).expect("could not read packet from bytes");
|
||||
//! ```
|
||||
|
||||
use crate::command_code::CommandCode;
|
||||
use crate::compression::into_compressed;
|
||||
use crate::{
|
||||
Bitmap, Command, CompressionCode, Grid, Offset, Origin, Pixels, Tiles,
|
||||
TILE_SIZE,
|
||||
};
|
||||
use std::mem::size_of;
|
||||
|
||||
/// A raw header.
|
||||
///
|
||||
/// The header specifies the kind of command, the size of the payload and where to display the
|
||||
/// payload, where applicable.
|
||||
///
|
||||
/// Because the meaning of most fields depend on the command, there are no speaking names for them.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct Header {
|
||||
/// The first two bytes specify which command this packet represents.
|
||||
pub command_code: u16,
|
||||
/// First command-specific value
|
||||
pub a: u16,
|
||||
/// Second command-specific value
|
||||
pub b: u16,
|
||||
/// Third command-specific value
|
||||
pub c: u16,
|
||||
/// Fourth command-specific value
|
||||
pub d: u16,
|
||||
}
|
||||
|
||||
/// The raw payload.
|
||||
///
|
||||
/// Should probably only be used directly to use features not exposed by the library.
|
||||
pub type Payload = Vec<u8>;
|
||||
|
||||
/// The raw packet.
|
||||
///
|
||||
/// Contents should probably only be used directly to use features not exposed by the library.
|
||||
///
|
||||
/// You may want to use [Command] instead.
|
||||
///
|
||||
///
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Packet {
|
||||
/// Meta-information for the packed command
|
||||
pub header: Header,
|
||||
/// The data for the packed command
|
||||
pub payload: Payload,
|
||||
}
|
||||
|
||||
impl From<Packet> for Vec<u8> {
|
||||
/// Turn the packet into raw bytes ready to send
|
||||
fn from(value: Packet) -> Self {
|
||||
let Packet {
|
||||
header:
|
||||
Header {
|
||||
command_code: mode,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
},
|
||||
payload,
|
||||
} = value;
|
||||
|
||||
let mut packet = vec![0u8; 10 + payload.len()];
|
||||
packet[0..=1].copy_from_slice(&u16::to_be_bytes(mode));
|
||||
packet[2..=3].copy_from_slice(&u16::to_be_bytes(a));
|
||||
packet[4..=5].copy_from_slice(&u16::to_be_bytes(b));
|
||||
packet[6..=7].copy_from_slice(&u16::to_be_bytes(c));
|
||||
packet[8..=9].copy_from_slice(&u16::to_be_bytes(d));
|
||||
|
||||
packet[10..].copy_from_slice(&payload);
|
||||
|
||||
packet
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Packet {
|
||||
type Error = ();
|
||||
|
||||
/// Tries to interpret the bytes as a [Packet].
|
||||
///
|
||||
/// returns: `Error` if slice is not long enough to be a [Packet]
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
if value.len() < size_of::<Header>() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let header = {
|
||||
let command_code = Self::u16_from_be_slice(&value[0..=1]);
|
||||
let a = Self::u16_from_be_slice(&value[2..=3]);
|
||||
let b = Self::u16_from_be_slice(&value[4..=5]);
|
||||
let c = Self::u16_from_be_slice(&value[6..=7]);
|
||||
let d = Self::u16_from_be_slice(&value[8..=9]);
|
||||
Header {
|
||||
command_code,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
}
|
||||
};
|
||||
let payload = value[10..].to_vec();
|
||||
|
||||
Ok(Packet { header, payload })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Packet {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::try_from(value.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
Self::origin_grid_to_packet(
|
||||
origin,
|
||||
grid,
|
||||
CommandCode::CharBrightness,
|
||||
)
|
||||
}
|
||||
Command::Brightness(brightness) => Packet {
|
||||
header: Header {
|
||||
command_code: CommandCode::Brightness.into(),
|
||||
a: 0x00000,
|
||||
b: 0x0000,
|
||||
c: 0x0000,
|
||||
d: 0x0000,
|
||||
},
|
||||
payload: 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) => Self::origin_grid_to_packet(
|
||||
origin,
|
||||
grid,
|
||||
CommandCode::Cp437Data,
|
||||
),
|
||||
Command::Utf8Data(origin, grid) => {
|
||||
Self::origin_grid_to_packet(origin, grid, CommandCode::Utf8Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: Header {
|
||||
command_code: command.into(),
|
||||
a: offset as u16,
|
||||
b: length,
|
||||
c: compression.into(),
|
||||
d: 0,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn bitmap_win_into_packet(
|
||||
origin: Origin<Pixels>,
|
||||
pixels: Bitmap,
|
||||
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: Header {
|
||||
command_code: command.into(),
|
||||
a: tile_x,
|
||||
b: origin.y as u16,
|
||||
c: tile_w,
|
||||
d: pixel_h,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method for creating empty packets only containing the command code
|
||||
fn command_code_only(code: CommandCode) -> Packet {
|
||||
Packet {
|
||||
header: Header {
|
||||
command_code: code.into(),
|
||||
a: 0x0000,
|
||||
b: 0x0000,
|
||||
c: 0x0000,
|
||||
d: 0x0000,
|
||||
},
|
||||
payload: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn u16_from_be_slice(slice: &[u8]) -> u16 {
|
||||
let mut bytes = [0u8; 2];
|
||||
bytes[0] = slice[0];
|
||||
bytes[1] = slice[1];
|
||||
u16::from_be_bytes(bytes)
|
||||
}
|
||||
|
||||
fn origin_grid_to_packet<T>(
|
||||
origin: Origin<Tiles>,
|
||||
grid: impl Grid<T> + Into<Payload>,
|
||||
command_code: CommandCode,
|
||||
) -> Packet {
|
||||
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,
|
||||
},
|
||||
payload: grid.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let p = Packet {
|
||||
header: Header {
|
||||
command_code: 0,
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4,
|
||||
},
|
||||
payload: vec![42u8; 23],
|
||||
};
|
||||
let data: Vec<u8> = p.into();
|
||||
let p = Packet::try_from(data).unwrap();
|
||||
assert_eq!(
|
||||
p,
|
||||
Packet {
|
||||
header: Header {
|
||||
command_code: 0,
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4
|
||||
},
|
||||
payload: vec![42u8; 23]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_small() {
|
||||
let data = vec![0u8; 4];
|
||||
assert_eq!(Packet::try_from(data.as_slice()), Err(()))
|
||||
}
|
||||
}
|
|
@ -1,590 +0,0 @@
|
|||
use std::fmt::Debug;
|
||||
use std::slice::{Iter, IterMut};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// A type that can be stored in a [ValueGrid], e.g. [char], [u8].
|
||||
pub trait Value: Sized + Default + Copy + Clone + Debug {}
|
||||
impl<T: Sized + Default + Copy + Clone + Debug> Value for T {}
|
||||
|
||||
/// A 2D grid of values.
|
||||
///
|
||||
/// The memory layout is the one the display expects in [Command]s.
|
||||
///
|
||||
/// 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)]
|
||||
pub struct ValueGrid<T: Value> {
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Vec<T>,
|
||||
}
|
||||
|
||||
/// Error type for methods that change a whole column or row at once
|
||||
#[derive(thiserror::Error, Debug, PartialEq)]
|
||||
pub enum SetValueSeriesError {
|
||||
#[error("The index {index} was out of bounds for size {size}")]
|
||||
/// The index {index} was out of bounds for size {size}
|
||||
OutOfBounds {
|
||||
/// the index where access was tried
|
||||
index: usize,
|
||||
/// the size in that direction
|
||||
size: usize,
|
||||
},
|
||||
#[error("The provided series was expected to have a length of {expected}, but was {actual}")]
|
||||
/// The provided series was expected to have a length of {expected}, but was {actual}
|
||||
InvalidLength {
|
||||
/// actual size of the provided series
|
||||
actual: usize,
|
||||
/// expected size
|
||||
expected: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Value> ValueGrid<T> {
|
||||
/// Creates a new [ValueGrid] with the specified dimensions.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - width: size in x-direction
|
||||
/// - height: size in y-direction
|
||||
///
|
||||
/// returns: [ValueGrid] initialized to default value.
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
data: vec![Default::default(); width * height],
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a [ValueGrid] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// returns: [ValueGrid] 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(),
|
||||
"dimension mismatch for data {data:?}"
|
||||
);
|
||||
Self {
|
||||
data: Vec::from(data),
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [ValueGrid] with the specified width from the provided data without copying it.
|
||||
///
|
||||
/// returns: [ValueGrid] that contains the provided data.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the data size is not dividable by the width.
|
||||
#[must_use]
|
||||
pub fn from_vec(width: usize, data: Vec<T>) -> Self {
|
||||
let len = data.len();
|
||||
let height = len / width;
|
||||
assert_eq!(0, len % width, "dimension mismatch - len {len} is not dividable by {width}");
|
||||
Self { data, width, height }
|
||||
}
|
||||
|
||||
/// Loads a [ValueGrid] with the specified width from the provided data, wrapping to as many rows as needed.
|
||||
///
|
||||
/// returns: [ValueGrid] that contains a copy of the provided data or [TryLoadValueGridError].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use servicepoint::ValueGrid;
|
||||
/// let grid = ValueGrid::wrap(2, &[0, 1, 2, 3, 4, 5]).unwrap();
|
||||
/// ```
|
||||
pub fn wrap(
|
||||
width: usize,
|
||||
data: &[T],
|
||||
) -> Result<Self, TryLoadValueGridError> {
|
||||
let len = data.len();
|
||||
if len % width != 0 {
|
||||
return Err(TryLoadValueGridError::InvalidDimensions);
|
||||
}
|
||||
Ok(Self::load(width, len / width, data))
|
||||
}
|
||||
|
||||
/// Loads a [ValueGrid] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// returns: [ValueGrid] that contains a copy of the provided data or [TryLoadValueGridError].
|
||||
pub fn try_load(
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Vec<T>,
|
||||
) -> Result<Self, TryLoadValueGridError> {
|
||||
if width * height != data.len() {
|
||||
return Err(TryLoadValueGridError::InvalidDimensions);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over all cells in [ValueGrid].
|
||||
///
|
||||
/// Order is equivalent to the following loop:
|
||||
/// ```
|
||||
/// # use servicepoint::{ByteGrid, Grid};
|
||||
/// # let grid = ByteGrid::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 [ValueGrid] top to bottom.
|
||||
pub fn iter_rows(&self) -> IterGridRows<T> {
|
||||
IterGridRows {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert between ValueGrid types.
|
||||
///
|
||||
/// See also [Iterator::map].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Use logic written for u8s and then convert to [Brightness] values for sending in a [Command].
|
||||
/// ```
|
||||
/// # fn foo(grid: &mut ByteGrid) {}
|
||||
/// # use servicepoint::{Brightness, BrightnessGrid, ByteGrid, Command, Origin, TILE_HEIGHT, TILE_WIDTH};
|
||||
/// let mut grid: ByteGrid = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||
/// foo(&mut grid);
|
||||
/// let grid: BrightnessGrid = grid.map(Brightness::saturating_from);
|
||||
/// let command = Command::CharBrightness(Origin::ZERO, grid);
|
||||
/// ```
|
||||
/// [Brightness]: [crate::Brightness]
|
||||
/// [Command]: [crate::Command]
|
||||
pub fn map<TConverted, F>(&self, f: F) -> ValueGrid<TConverted>
|
||||
where
|
||||
TConverted: Value,
|
||||
F: Fn(T) -> TConverted,
|
||||
{
|
||||
let data = self
|
||||
.data_ref()
|
||||
.iter()
|
||||
.map(|elem| f(*elem))
|
||||
.collect::<Vec<_>>();
|
||||
ValueGrid::load(self.width(), self.height(), &data)
|
||||
}
|
||||
|
||||
/// Copies a row from the grid.
|
||||
///
|
||||
/// Returns [None] if y is out of bounds.
|
||||
pub fn get_row(&self, y: usize) -> Option<Vec<T>> {
|
||||
self.data
|
||||
.chunks_exact(self.width())
|
||||
.nth(y)
|
||||
.map(|row| row.to_vec())
|
||||
}
|
||||
|
||||
/// Copies a column from the grid.
|
||||
///
|
||||
/// Returns [None] if x is out of bounds.
|
||||
pub fn get_col(&self, x: usize) -> Option<Vec<T>> {
|
||||
self.data
|
||||
.chunks_exact(self.width())
|
||||
.map(|row| row.get(x).copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Overwrites a column in the grid.
|
||||
///
|
||||
/// Returns [Err] if x is out of bounds or `col` is not of the correct size.
|
||||
pub fn set_col(
|
||||
&mut self,
|
||||
x: usize,
|
||||
col: &[T],
|
||||
) -> Result<(), SetValueSeriesError> {
|
||||
if col.len() != self.height() {
|
||||
return Err(SetValueSeriesError::InvalidLength {
|
||||
expected: self.height(),
|
||||
actual: col.len(),
|
||||
});
|
||||
}
|
||||
let width = self.width();
|
||||
if self
|
||||
.data
|
||||
.chunks_exact_mut(width)
|
||||
.zip(col.iter())
|
||||
.map(|(row, column_value)| {
|
||||
row.get_mut(x).map(move |cell| *cell = *column_value)
|
||||
})
|
||||
.all(|cell| cell.is_some())
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SetValueSeriesError::OutOfBounds {
|
||||
index: x,
|
||||
size: width,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Overwrites a row in the grid.
|
||||
///
|
||||
/// Returns [Err] if y is out of bounds or `row` is not of the correct size.
|
||||
pub fn set_row(
|
||||
&mut self,
|
||||
y: usize,
|
||||
row: &[T],
|
||||
) -> Result<(), SetValueSeriesError> {
|
||||
let width = self.width();
|
||||
if row.len() != width {
|
||||
return Err(SetValueSeriesError::InvalidLength {
|
||||
expected: width,
|
||||
actual: row.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let chunk = match self.data.chunks_exact_mut(width).nth(y) {
|
||||
Some(row) => row,
|
||||
None => {
|
||||
return Err(SetValueSeriesError::OutOfBounds {
|
||||
size: self.height(),
|
||||
index: y,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
chunk.copy_from_slice(row);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when loading a grid
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum TryLoadValueGridError {
|
||||
#[error("The provided dimensions do not match with the data size")]
|
||||
/// The provided dimensions do not match with the data size
|
||||
InvalidDimensions,
|
||||
}
|
||||
|
||||
impl<T: Value> Grid<T> for ValueGrid<T> {
|
||||
/// Sets the value of the cell at the specified position in the `ValueGrid.
|
||||
///
|
||||
/// # 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: Value> DataRef<T> for ValueGrid<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: Value> From<ValueGrid<T>> for Vec<T> {
|
||||
/// Turn into the underlying [`Vec<u8>`] containing the rows of bytes.
|
||||
fn from(value: ValueGrid<T>) -> Self {
|
||||
value.data
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator iver the rows in a [ValueGrid]
|
||||
pub struct IterGridRows<'t, T: Value> {
|
||||
byte_grid: &'t ValueGrid<T>,
|
||||
row: usize,
|
||||
}
|
||||
|
||||
impl<'t, T: Value> Iterator for IterGridRows<'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::{
|
||||
value_grid::{SetValueSeriesError, ValueGrid},
|
||||
*,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn fill() {
|
||||
let mut grid = ValueGrid::<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 = ValueGrid::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 = ValueGrid::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 = ValueGrid::load(2, 3, &data);
|
||||
assert_eq!(grid.data, [0, 1, 1, 2, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mut_data_ref() {
|
||||
let mut vec = ValueGrid::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 = ValueGrid::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 = ValueGrid::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 = ValueGrid::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 = ValueGrid::load(2, 2, &[0, 1, 2, 3]);
|
||||
vec.set(2, 1, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn out_of_bounds_y() {
|
||||
let vec = ValueGrid::load(2, 2, &[0, 1, 2, 3]);
|
||||
vec.get(1, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_mut() {
|
||||
let mut vec = ValueGrid::from_vec(3, vec![0, 1, 2, 3,4,5,6,7,8]);
|
||||
|
||||
let top_left = vec.get_ref_mut(0, 0);
|
||||
*top_left += 5;
|
||||
let somewhere = vec.get_ref_mut(2, 1);
|
||||
*somewhere = 42;
|
||||
|
||||
assert_eq!(None, vec.get_ref_mut_optional(3, 2));
|
||||
assert_eq!(None, vec.get_ref_mut_optional(2, 3));
|
||||
assert_eq!(Some(&mut 5), vec.get_ref_mut_optional(0, 0));
|
||||
assert_eq!(Some(&mut 42), vec.get_ref_mut_optional(2, 1));
|
||||
assert_eq!(Some(&mut 8), vec.get_ref_mut_optional(2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional() {
|
||||
let mut grid = ValueGrid::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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn col() {
|
||||
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(grid.get_col(0), Some(vec![0, 2, 4]));
|
||||
assert_eq!(grid.get_col(1), Some(vec![1, 3, 5]));
|
||||
assert_eq!(grid.get_col(2), None);
|
||||
assert_eq!(grid.set_col(0, &[5, 7, 9]), Ok(()));
|
||||
assert_eq!(
|
||||
grid.set_col(2, &[5, 7, 9]),
|
||||
Err(SetValueSeriesError::OutOfBounds { size: 2, index: 2 })
|
||||
);
|
||||
assert_eq!(
|
||||
grid.set_col(0, &[5, 7]),
|
||||
Err(SetValueSeriesError::InvalidLength {
|
||||
expected: 3,
|
||||
actual: 2
|
||||
})
|
||||
);
|
||||
assert_eq!(grid.get_col(0), Some(vec![5, 7, 9]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row() {
|
||||
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(grid.get_row(0), Some(vec![0, 1]));
|
||||
assert_eq!(grid.get_row(2), Some(vec![4, 5]));
|
||||
assert_eq!(grid.get_row(3), None);
|
||||
assert_eq!(grid.set_row(0, &[5, 7]), Ok(()));
|
||||
assert_eq!(grid.get_row(0), Some(vec![5, 7]));
|
||||
assert_eq!(
|
||||
grid.set_row(3, &[5, 7]),
|
||||
Err(SetValueSeriesError::OutOfBounds { size: 3, index: 3 })
|
||||
);
|
||||
assert_eq!(
|
||||
grid.set_row(2, &[5, 7, 3]),
|
||||
Err(SetValueSeriesError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 3
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap() {
|
||||
let grid = ValueGrid::wrap(2, &[0, 1, 2, 3, 4, 5]).unwrap();
|
||||
assert_eq!(grid.height(), 3);
|
||||
|
||||
let grid = ValueGrid::wrap(4, &[0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(grid.err(), Some(TryLoadValueGridError::InvalidDimensions));
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
[package]
|
||||
name = "servicepoint_binding_c"
|
||||
version.workspace = true
|
||||
publish = true
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "C bindings for the servicepoint crate."
|
||||
homepage = "https://docs.rs/crate/servicepoint_binding_c"
|
||||
repository = "https://git.berlin.ccc.de/servicepoint/servicepoint"
|
||||
readme = "README.md"
|
||||
links = "servicepoint"
|
||||
keywords = ["cccb", "cccb-servicepoint", "cbindgen"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.27.0"
|
||||
|
||||
[dependencies.servicepoint]
|
||||
version = "0.13.1"
|
||||
path = "../servicepoint"
|
||||
features = ["all_compressions"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
|
@ -1,63 +0,0 @@
|
|||
# servicepoint_binding_c
|
||||
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://docs.rs/servicepoint/latest/servicepoint/)
|
||||
[](../../LICENSE)
|
||||
|
||||
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall.
|
||||
It is called "Service Point Display" or "Airport Display".
|
||||
|
||||
This crate contains C bindings for the `servicepoint` library, enabling users to parse, encode and send packets to this display via UDP.
|
||||
|
||||
## Examples
|
||||
|
||||
```c++
|
||||
#include <stdio.h>
|
||||
#include "servicepoint.h"
|
||||
|
||||
int main(void) {
|
||||
SPConnection *connection = sp_connection_open("172.23.42.29:2342");
|
||||
if (connection == NULL)
|
||||
return 1;
|
||||
|
||||
SPBitmap *pixels = sp_bitmap_new(SP_PIXEL_WIDTH, SP_PIXEL_HEIGHT);
|
||||
sp_bitmap_fill(pixels, true);
|
||||
|
||||
SPCommand *command = sp_command_bitmap_linear_win(0, 0, pixels, Uncompressed);
|
||||
while (sp_connection_send_command(connection, sp_command_clone(command)));
|
||||
|
||||
sp_command_free(command);
|
||||
sp_connection_free(connection);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
A full example including Makefile is available as part of this crate.
|
||||
|
||||
## Note on stability
|
||||
|
||||
This library is still in early development.
|
||||
You can absolutely use it, and it works, but expect minor breaking changes with every version bump.
|
||||
Please specify the full version including patch in your Cargo.toml until 1.0 is released.
|
||||
|
||||
## Installation
|
||||
|
||||
Copy the header to your project and compile against.
|
||||
|
||||
You have the choice of linking statically (recommended) or dynamically.
|
||||
- The C example shows how to link statically against the `staticlib` variant.
|
||||
- When linked dynamically, you have to provide the `cdylib` at runtime in the _same_ version, as there are no API/ABI guarantees yet.
|
||||
|
||||
## Notes on differences to rust library
|
||||
|
||||
- function names are: `sp_` \<struct_name\> \<rust name\>.
|
||||
- Instances get consumed in the same way they do when writing rust code. Do not use an instance after an (implicit!) free.
|
||||
- Option<T> or Result<T, E> turn into nullable return values - check for NULL!
|
||||
- There are no specifics for C++ here yet. You might get a nicer header when generating directly for C++, but it should be usable.
|
||||
- Reading and writing to instances concurrently is not safe. Only reading concurrently is safe.
|
||||
- documentation is included in the header and available [online](https://docs.rs/servicepoint_binding_c/latest/servicepoint_binding_c/)
|
||||
|
||||
## Everything else
|
||||
|
||||
Look at the main project [README](https://git.berlin.ccc.de/servicepoint/servicepoint/src/branch/main/README.md) for further information.
|
|
@ -1,33 +0,0 @@
|
|||
//! Build script generating the header for the `servicepoint` C library.
|
||||
//!
|
||||
//! When the environment variable `SERVICEPOINT_HEADER_OUT` is set, the header is copied there from
|
||||
//! the out directory. This can be used to use the build script as a command line tool from other
|
||||
//! build tools.
|
||||
|
||||
use std::{env, fs::copy};
|
||||
|
||||
use cbindgen::{generate_with_config, Config};
|
||||
|
||||
fn main() {
|
||||
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
println!("cargo::rerun-if-changed={crate_dir}");
|
||||
|
||||
let config =
|
||||
Config::from_file(crate_dir.clone() + "/cbindgen.toml").unwrap();
|
||||
|
||||
let output_dir = env::var("OUT_DIR").unwrap();
|
||||
let header_file = output_dir.clone() + "/servicepoint.h";
|
||||
|
||||
generate_with_config(crate_dir, config)
|
||||
.unwrap()
|
||||
.write_to_file(&header_file);
|
||||
println!("cargo:include={output_dir}");
|
||||
|
||||
println!("cargo::rerun-if-env-changed=SERVICEPOINT_HEADER_OUT");
|
||||
if let Ok(header_out) = env::var("SERVICEPOINT_HEADER_OUT") {
|
||||
let header_copy = header_out + "/servicepoint.h";
|
||||
println!("cargo:warning=Copying header to {header_copy}");
|
||||
copy(header_file, &header_copy).unwrap();
|
||||
println!("cargo::rerun-if-changed={header_copy}");
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
language = "C"
|
||||
include_version = true
|
||||
cpp_compat = true
|
||||
|
||||
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
|
||||
|
||||
############################ Code Style Options ################################
|
||||
|
||||
braces = "SameLine"
|
||||
line_length = 80
|
||||
tab_width = 4
|
||||
documentation = true
|
||||
documentation_style = "auto"
|
||||
documentation_length = "full"
|
||||
line_endings = "LF"
|
||||
|
||||
############################# Codegen Options ##################################
|
||||
|
||||
style = "type"
|
||||
usize_is_size_t = true
|
||||
|
||||
# this is needed because otherwise the order in the C# bindings is different on different machines
|
||||
sort_by = "Name"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
|
||||
[parse.expand]
|
||||
all_features = true
|
||||
|
||||
[export]
|
||||
include = []
|
||||
exclude = []
|
||||
|
||||
[enum]
|
||||
rename_variants = "QualifiedScreamingSnakeCase"
|
|
@ -1,14 +0,0 @@
|
|||
[package]
|
||||
name = "lang_c"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.2"
|
||||
|
||||
[dependencies]
|
||||
servicepoint_binding_c = { path = "../.." }
|
|
@ -1,34 +0,0 @@
|
|||
CC := gcc
|
||||
|
||||
THIS_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
REPO_ROOT := $(THIS_DIR)/../../../..
|
||||
|
||||
build: out/lang_c
|
||||
|
||||
clean:
|
||||
rm -r out || true
|
||||
rm include/servicepoint.h || true
|
||||
cargo clean
|
||||
|
||||
run: out/lang_c
|
||||
out/lang_c
|
||||
|
||||
PHONY: build clean dependencies run
|
||||
|
||||
out/lang_c: dependencies src/main.c
|
||||
mkdir -p out || true
|
||||
${CC} src/main.c \
|
||||
-I include \
|
||||
-L $(REPO_ROOT)/target/release \
|
||||
-Wl,-Bstatic -lservicepoint_binding_c \
|
||||
-Wl,-Bdynamic -llzma \
|
||||
-o out/lang_c
|
||||
|
||||
dependencies: FORCE
|
||||
mkdir -p include || true
|
||||
# generate servicepoint header and binary to link against
|
||||
SERVICEPOINT_HEADER_OUT=$(THIS_DIR)/include cargo build \
|
||||
--manifest-path=$(REPO_ROOT)/crates/servicepoint_binding_c/Cargo.toml \
|
||||
--release
|
||||
|
||||
FORCE: ;
|
|
@ -1,17 +0,0 @@
|
|||
const SP_INCLUDE: &str = "DEP_SERVICEPOINT_INCLUDE";
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=src/main.c");
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
println!("cargo::rerun-if-env-changed={SP_INCLUDE}");
|
||||
|
||||
let sp_include =
|
||||
std::env::var_os(SP_INCLUDE).unwrap().into_string().unwrap();
|
||||
|
||||
// this builds a lib, this is only to check that the example compiles
|
||||
let mut cc = cc::Build::new();
|
||||
cc.file("src/main.c");
|
||||
cc.include(&sp_include);
|
||||
cc.opt_level(2);
|
||||
cc.compile("lang_c");
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,17 +0,0 @@
|
|||
#include <stdio.h>
|
||||
#include "servicepoint.h"
|
||||
|
||||
int main(void) {
|
||||
SPConnection *connection = sp_connection_open("localhost:2342");
|
||||
if (connection == NULL)
|
||||
return 1;
|
||||
|
||||
SPBitmap *pixels = sp_bitmap_new(SP_PIXEL_WIDTH, SP_PIXEL_HEIGHT);
|
||||
sp_bitmap_fill(pixels, true);
|
||||
|
||||
SPCommand *command = sp_command_bitmap_linear_win(0, 0, pixels, SP_COMPRESSION_CODE_UNCOMPRESSED);
|
||||
sp_connection_send_command(connection, command);
|
||||
|
||||
sp_connection_free(connection);
|
||||
return 0;
|
||||
}
|
|
@ -1,296 +0,0 @@
|
|||
//! C functions for interacting with [SPBitmap]s
|
||||
//!
|
||||
//! prefix `sp_bitmap_`
|
||||
|
||||
use servicepoint::{DataRef, Grid};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use crate::byte_slice::SPByteSlice;
|
||||
|
||||
/// A grid of pixels.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// Cp437Grid grid = sp_bitmap_new(8, 3);
|
||||
/// sp_bitmap_fill(grid, true);
|
||||
/// sp_bitmap_set(grid, 0, 0, false);
|
||||
/// sp_bitmap_free(grid);
|
||||
/// ```
|
||||
pub struct SPBitmap(pub(crate) servicepoint::Bitmap);
|
||||
|
||||
/// Creates a new [SPBitmap] with the specified dimensions.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `width`: size in pixels in x-direction
|
||||
/// - `height`: size in pixels in y-direction
|
||||
///
|
||||
/// returns: [SPBitmap] initialized to all pixels off. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when the width is not dividable by 8
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_bitmap_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> NonNull<SPBitmap> {
|
||||
let result = Box::new(SPBitmap(servicepoint::Bitmap::new(width, height)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Creates a new [SPBitmap] with a size matching the screen.
|
||||
///
|
||||
/// returns: [SPBitmap] initialized to all pixels off. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling [sp_bitmap_free].
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_new_screen_sized() -> NonNull<SPBitmap> {
|
||||
let result = Box::new(SPBitmap(servicepoint::Bitmap::max_sized()));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Loads a [SPBitmap] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `width`: size in pixels in x-direction
|
||||
/// - `height`: size in pixels in y-direction
|
||||
///
|
||||
/// returns: [SPBitmap] that contains a copy of the provided data. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `data` is NULL
|
||||
/// - when the dimensions and data size do not match exactly.
|
||||
/// - when the width is not dividable by 8
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `data` points to a valid memory location of at least `data_length` bytes in size.
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_bitmap_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_load(
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: *const u8,
|
||||
data_length: usize,
|
||||
) -> NonNull<SPBitmap> {
|
||||
assert!(!data.is_null());
|
||||
let data = std::slice::from_raw_parts(data, data_length);
|
||||
let result =
|
||||
Box::new(SPBitmap(servicepoint::Bitmap::load(width, height, data)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Clones a [SPBitmap].
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
/// - `bitmap` is not written to concurrently
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_bitmap_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_clone(
|
||||
bitmap: *const SPBitmap,
|
||||
) -> NonNull<SPBitmap> {
|
||||
assert!(!bitmap.is_null());
|
||||
let result = Box::new(SPBitmap((*bitmap).0.clone()));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPBitmap].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
/// - `bitmap` is not used concurrently or after bitmap call
|
||||
/// - `bitmap` was not passed to another consuming function, e.g. to create a [SPCommand]
|
||||
///
|
||||
/// [SPCommand]: [crate::SPCommand]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_free(bitmap: *mut SPBitmap) {
|
||||
assert!(!bitmap.is_null());
|
||||
_ = Box::from_raw(bitmap);
|
||||
}
|
||||
|
||||
/// Gets the current value at the specified position in the [SPBitmap].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bitmap`: instance to read from
|
||||
/// - `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
/// - when accessing `x` or `y` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
/// - `bitmap` is not written to concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_get(
|
||||
bitmap: *const SPBitmap,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> bool {
|
||||
assert!(!bitmap.is_null());
|
||||
(*bitmap).0.get(x, y)
|
||||
}
|
||||
|
||||
/// Sets the value of the specified position in the [SPBitmap].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bitmap`: instance to write to
|
||||
/// - `x` and `y`: position of the cell
|
||||
/// - `value`: the value to write to the cell
|
||||
///
|
||||
/// returns: old value of the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
/// - when accessing `x` or `y` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
/// - `bitmap` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_set(
|
||||
bitmap: *mut SPBitmap,
|
||||
x: usize,
|
||||
y: usize,
|
||||
value: bool,
|
||||
) {
|
||||
assert!(!bitmap.is_null());
|
||||
(*bitmap).0.set(x, y, value);
|
||||
}
|
||||
|
||||
/// Sets the state of all pixels in the [SPBitmap].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bitmap`: instance to write to
|
||||
/// - `value`: the value to set all pixels to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
/// - `bitmap` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_fill(bitmap: *mut SPBitmap, value: bool) {
|
||||
assert!(!bitmap.is_null());
|
||||
(*bitmap).0.fill(value);
|
||||
}
|
||||
|
||||
/// Gets the width in pixels of the [SPBitmap] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bitmap`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_width(bitmap: *const SPBitmap) -> usize {
|
||||
assert!(!bitmap.is_null());
|
||||
(*bitmap).0.width()
|
||||
}
|
||||
|
||||
/// Gets the height in pixels of the [SPBitmap] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bitmap`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_height(bitmap: *const SPBitmap) -> usize {
|
||||
assert!(!bitmap.is_null());
|
||||
(*bitmap).0.height()
|
||||
}
|
||||
|
||||
/// Gets an unsafe reference to the data of the [SPBitmap] instance.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid [SPBitmap]
|
||||
/// - the returned memory range is never accessed after the passed [SPBitmap] has been freed
|
||||
/// - the returned memory range is never accessed concurrently, either via the [SPBitmap] or directly
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitmap_unsafe_data_ref(
|
||||
bitmap: *mut SPBitmap,
|
||||
) -> SPByteSlice {
|
||||
assert!(!bitmap.is_null());
|
||||
let data = (*bitmap).0.data_ref_mut();
|
||||
SPByteSlice {
|
||||
start: NonNull::new(data.as_mut_ptr_range().start).unwrap(),
|
||||
length: data.len(),
|
||||
}
|
||||
}
|
|
@ -1,283 +0,0 @@
|
|||
//! C functions for interacting with [SPBitVec]s
|
||||
//!
|
||||
//! prefix `sp_bitvec_`
|
||||
|
||||
use crate::SPByteSlice;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// A vector of bits
|
||||
///
|
||||
/// # Examples
|
||||
/// ```C
|
||||
/// SPBitVec vec = sp_bitvec_new(8);
|
||||
/// sp_bitvec_set(vec, 5, true);
|
||||
/// sp_bitvec_free(vec);
|
||||
/// ```
|
||||
pub struct SPBitVec(servicepoint::BitVec);
|
||||
|
||||
impl From<servicepoint::BitVec> for SPBitVec {
|
||||
fn from(actual: servicepoint::BitVec) -> Self {
|
||||
Self(actual)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SPBitVec> for servicepoint::BitVec {
|
||||
fn from(value: SPBitVec) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SPBitVec {
|
||||
fn clone(&self) -> Self {
|
||||
SPBitVec(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [SPBitVec] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `size`: size in bits.
|
||||
///
|
||||
/// returns: [SPBitVec] with all bits set to false. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `size` is not divisible by 8.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_bitvec_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_new(size: usize) -> NonNull<SPBitVec> {
|
||||
let result = Box::new(SPBitVec(servicepoint::BitVec::repeat(false, size)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Interpret the data as a series of bits and load then into a new [SPBitVec] instance.
|
||||
///
|
||||
/// returns: [SPBitVec] instance containing data. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `data` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `data` points to a valid memory location of at least `data_length`
|
||||
/// bytes in size.
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_bitvec_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_load(
|
||||
data: *const u8,
|
||||
data_length: usize,
|
||||
) -> NonNull<SPBitVec> {
|
||||
assert!(!data.is_null());
|
||||
let data = std::slice::from_raw_parts(data, data_length);
|
||||
let result = Box::new(SPBitVec(servicepoint::BitVec::from_slice(data)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Clones a [SPBitVec].
|
||||
///
|
||||
/// returns: new [SPBitVec] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
/// - `bit_vec` is not written to concurrently
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_bitvec_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_clone(
|
||||
bit_vec: *const SPBitVec,
|
||||
) -> NonNull<SPBitVec> {
|
||||
assert!(!bit_vec.is_null());
|
||||
let result = Box::new((*bit_vec).clone());
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPBitVec].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `but_vec` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
/// - `bit_vec` is not used concurrently or after this call
|
||||
/// - `bit_vec` was not passed to another consuming function, e.g. to create a [SPCommand]
|
||||
///
|
||||
/// [SPCommand]: [crate::SPCommand]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_free(bit_vec: *mut SPBitVec) {
|
||||
assert!(!bit_vec.is_null());
|
||||
_ = Box::from_raw(bit_vec);
|
||||
}
|
||||
|
||||
/// Gets the value of a bit from the [SPBitVec].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bit_vec`: instance to read from
|
||||
/// - `index`: the bit index to read
|
||||
///
|
||||
/// returns: value of the bit
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
/// - when accessing `index` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
/// - `bit_vec` is not written to concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_get(
|
||||
bit_vec: *const SPBitVec,
|
||||
index: usize,
|
||||
) -> bool {
|
||||
assert!(!bit_vec.is_null());
|
||||
*(*bit_vec).0.get(index).unwrap()
|
||||
}
|
||||
|
||||
/// Sets the value of a bit in the [SPBitVec].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bit_vec`: instance to write to
|
||||
/// - `index`: the bit index to edit
|
||||
/// - `value`: the value to set the bit to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
/// - when accessing `index` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
/// - `bit_vec` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_set(
|
||||
bit_vec: *mut SPBitVec,
|
||||
index: usize,
|
||||
value: bool,
|
||||
) {
|
||||
assert!(!bit_vec.is_null());
|
||||
(*bit_vec).0.set(index, value)
|
||||
}
|
||||
|
||||
/// Sets the value of all bits in the [SPBitVec].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bit_vec`: instance to write to
|
||||
/// - `value`: the value to set all bits to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
/// - `bit_vec` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_fill(bit_vec: *mut SPBitVec, value: bool) {
|
||||
assert!(!bit_vec.is_null());
|
||||
(*bit_vec).0.fill(value)
|
||||
}
|
||||
|
||||
/// Gets the length of the [SPBitVec] in bits.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bit_vec`: instance to write to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_len(bit_vec: *const SPBitVec) -> usize {
|
||||
assert!(!bit_vec.is_null());
|
||||
(*bit_vec).0.len()
|
||||
}
|
||||
|
||||
/// Returns true if length is 0.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bit_vec`: instance to write to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_is_empty(bit_vec: *const SPBitVec) -> bool {
|
||||
assert!(!bit_vec.is_null());
|
||||
(*bit_vec).0.is_empty()
|
||||
}
|
||||
|
||||
/// Gets an unsafe reference to the data of the [SPBitVec] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `bit_vec`: instance to write to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is NULL
|
||||
///
|
||||
/// ## Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid [SPBitVec]
|
||||
/// - the returned memory range is never accessed after the passed [SPBitVec] has been freed
|
||||
/// - the returned memory range is never accessed concurrently, either via the [SPBitVec] or directly
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_bitvec_unsafe_data_ref(
|
||||
bit_vec: *mut SPBitVec,
|
||||
) -> SPByteSlice {
|
||||
assert!(!bit_vec.is_null());
|
||||
let data = (*bit_vec).0.as_raw_mut_slice();
|
||||
SPByteSlice {
|
||||
start: NonNull::new(data.as_mut_ptr_range().start).unwrap(),
|
||||
length: data.len(),
|
||||
}
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
//! C functions for interacting with [SPBrightnessGrid]s
|
||||
//!
|
||||
//! prefix `sp_brightness_grid_`
|
||||
|
||||
use crate::SPByteSlice;
|
||||
use servicepoint::{DataRef, Grid};
|
||||
use std::convert::Into;
|
||||
use std::intrinsics::transmute;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// see [servicepoint::Brightness::MIN]
|
||||
pub const SP_BRIGHTNESS_MIN: u8 = 0;
|
||||
/// see [servicepoint::Brightness::MAX]
|
||||
pub const SP_BRIGHTNESS_MAX: u8 = 11;
|
||||
/// Count of possible brightness values
|
||||
pub const SP_BRIGHTNESS_LEVELS: u8 = 12;
|
||||
|
||||
/// A grid containing brightness values.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```C
|
||||
/// SPConnection connection = sp_connection_open("127.0.0.1:2342");
|
||||
/// if (connection == NULL)
|
||||
/// return 1;
|
||||
///
|
||||
/// SPBrightnessGrid grid = sp_brightness_grid_new(2, 2);
|
||||
/// sp_brightness_grid_set(grid, 0, 0, 0);
|
||||
/// sp_brightness_grid_set(grid, 1, 1, 10);
|
||||
///
|
||||
/// SPCommand command = sp_command_char_brightness(grid);
|
||||
/// sp_connection_free(connection);
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct SPBrightnessGrid(pub(crate) servicepoint::BrightnessGrid);
|
||||
|
||||
/// Creates a new [SPBrightnessGrid] with the specified dimensions.
|
||||
///
|
||||
/// returns: [SPBrightnessGrid] initialized to 0. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_brightness_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> NonNull<SPBrightnessGrid> {
|
||||
let result = Box::new(SPBrightnessGrid(servicepoint::BrightnessGrid::new(
|
||||
width, height,
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Loads a [SPBrightnessGrid] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// returns: new [SPBrightnessGrid] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `data` is NULL
|
||||
/// - when the provided `data_length` does not match `height` and `width`
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `data` points to a valid memory location of at least `data_length`
|
||||
/// bytes in size.
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_brightness_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_load(
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: *const u8,
|
||||
data_length: usize,
|
||||
) -> NonNull<SPBrightnessGrid> {
|
||||
assert!(!data.is_null());
|
||||
let data = std::slice::from_raw_parts(data, data_length);
|
||||
let grid = servicepoint::ByteGrid::load(width, height, data);
|
||||
let grid = servicepoint::BrightnessGrid::try_from(grid)
|
||||
.expect("invalid brightness value");
|
||||
let result = Box::new(SPBrightnessGrid(grid));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Clones a [SPBrightnessGrid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to read from
|
||||
///
|
||||
/// returns: new [SPBrightnessGrid] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
/// - `brightness_grid` is not written to concurrently
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_brightness_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_clone(
|
||||
brightness_grid: *const SPBrightnessGrid,
|
||||
) -> NonNull<SPBrightnessGrid> {
|
||||
assert!(!brightness_grid.is_null());
|
||||
let result = Box::new((*brightness_grid).clone());
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPBrightnessGrid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
/// - `brightness_grid` is not used concurrently or after this call
|
||||
/// - `brightness_grid` was not passed to another consuming function, e.g. to create a [SPCommand]
|
||||
///
|
||||
/// [SPCommand]: [crate::SPCommand]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_free(
|
||||
brightness_grid: *mut SPBrightnessGrid,
|
||||
) {
|
||||
assert!(!brightness_grid.is_null());
|
||||
_ = Box::from_raw(brightness_grid);
|
||||
}
|
||||
|
||||
/// Gets the current value at the specified position.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to read from
|
||||
/// - `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// returns: value at position
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
/// - When accessing `x` or `y` out of bounds.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
/// - `brightness_grid` is not written to concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_get(
|
||||
brightness_grid: *const SPBrightnessGrid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> u8 {
|
||||
assert!(!brightness_grid.is_null());
|
||||
(*brightness_grid).0.get(x, y).into()
|
||||
}
|
||||
|
||||
/// Sets the value of the specified position in the [SPBrightnessGrid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to write to
|
||||
/// - `x` and `y`: position of the cell
|
||||
/// - `value`: the value to write to the cell
|
||||
///
|
||||
/// returns: old value of the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
/// - When accessing `x` or `y` out of bounds.
|
||||
/// - When providing an invalid brightness value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
/// - `brightness_grid` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_set(
|
||||
brightness_grid: *mut SPBrightnessGrid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
value: u8,
|
||||
) {
|
||||
assert!(!brightness_grid.is_null());
|
||||
let brightness = servicepoint::Brightness::try_from(value)
|
||||
.expect("invalid brightness value");
|
||||
(*brightness_grid).0.set(x, y, brightness);
|
||||
}
|
||||
|
||||
/// Sets the value of all cells in the [SPBrightnessGrid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to write to
|
||||
/// - `value`: the value to set all cells to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
/// - When providing an invalid brightness value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
/// - `brightness_grid` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_fill(
|
||||
brightness_grid: *mut SPBrightnessGrid,
|
||||
value: u8,
|
||||
) {
|
||||
assert!(!brightness_grid.is_null());
|
||||
let brightness = servicepoint::Brightness::try_from(value)
|
||||
.expect("invalid brightness value");
|
||||
(*brightness_grid).0.fill(brightness);
|
||||
}
|
||||
|
||||
/// Gets the width of the [SPBrightnessGrid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to read from
|
||||
///
|
||||
/// returns: width
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_width(
|
||||
brightness_grid: *const SPBrightnessGrid,
|
||||
) -> usize {
|
||||
assert!(!brightness_grid.is_null());
|
||||
(*brightness_grid).0.width()
|
||||
}
|
||||
|
||||
/// Gets the height of the [SPBrightnessGrid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to read from
|
||||
///
|
||||
/// returns: height
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_height(
|
||||
brightness_grid: *const SPBrightnessGrid,
|
||||
) -> usize {
|
||||
assert!(!brightness_grid.is_null());
|
||||
(*brightness_grid).0.height()
|
||||
}
|
||||
|
||||
/// Gets an unsafe reference to the data of the [SPBrightnessGrid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `brightness_grid`: instance to read from
|
||||
///
|
||||
/// returns: slice of bytes underlying the `brightness_grid`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `brightness_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `brightness_grid` points to a valid [SPBrightnessGrid]
|
||||
/// - the returned memory range is never accessed after the passed [SPBrightnessGrid] has been freed
|
||||
/// - the returned memory range is never accessed concurrently, either via the [SPBrightnessGrid] or directly
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_brightness_grid_unsafe_data_ref(
|
||||
brightness_grid: *mut SPBrightnessGrid,
|
||||
) -> SPByteSlice {
|
||||
assert!(!brightness_grid.is_null());
|
||||
assert_eq!(core::mem::size_of::<servicepoint::Brightness>(), 1);
|
||||
let data = (*brightness_grid).0.data_ref_mut();
|
||||
// this assumes more about the memory layout than rust guarantees. yikes!
|
||||
let data: &mut [u8] = transmute(data);
|
||||
SPByteSlice {
|
||||
start: NonNull::new(data.as_mut_ptr_range().start).unwrap(),
|
||||
length: data.len(),
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
//! FFI slice helper
|
||||
|
||||
use std::ptr::NonNull;
|
||||
|
||||
#[repr(C)]
|
||||
/// Represents a span of memory (`&mut [u8]` ) as a struct usable by C code.
|
||||
///
|
||||
/// You should not create an instance of this type in your C code.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - accesses to the memory pointed to with `start` is never accessed outside `length`
|
||||
/// - the lifetime of the `CByteSlice` does not outlive the memory it points to, as described in
|
||||
/// the function returning this type.
|
||||
/// - an instance of this created from C is never passed to a consuming function, as the rust code
|
||||
/// will try to free the memory of a potentially separate allocator.
|
||||
pub struct SPByteSlice {
|
||||
/// The start address of the memory
|
||||
pub start: NonNull<u8>,
|
||||
/// The amount of memory in bytes
|
||||
pub length: usize,
|
||||
}
|
|
@ -1,263 +0,0 @@
|
|||
//! C functions for interacting with [SPCharGrid]s
|
||||
//!
|
||||
//! prefix `sp_char_grid_`
|
||||
|
||||
use servicepoint::Grid;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// A C-wrapper for grid containing UTF-8 characters.
|
||||
///
|
||||
/// As the rust [char] type is not FFI-safe, characters are passed in their UTF-32 form as 32bit unsigned integers.
|
||||
///
|
||||
/// The encoding is enforced in most cases by the rust standard library
|
||||
/// and will panic when provided with illegal characters.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// CharGrid grid = sp_char_grid_new(4, 3);
|
||||
/// sp_char_grid_fill(grid, '?');
|
||||
/// sp_char_grid_set(grid, 0, 0, '!');
|
||||
/// sp_char_grid_free(grid);
|
||||
/// ```
|
||||
pub struct SPCharGrid(pub(crate) servicepoint::CharGrid);
|
||||
|
||||
impl Clone for SPCharGrid {
|
||||
fn clone(&self) -> Self {
|
||||
SPCharGrid(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [SPCharGrid] with the specified dimensions.
|
||||
///
|
||||
/// returns: [SPCharGrid] initialized to 0. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_char_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> NonNull<SPCharGrid> {
|
||||
let result =
|
||||
Box::new(SPCharGrid(servicepoint::CharGrid::new(width, height)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Loads a [SPCharGrid] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `data` is NULL
|
||||
/// - when the provided `data_length` does not match `height` and `width`
|
||||
/// - when `data` is not valid UTF-8
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `data` points to a valid memory location of at least `data_length`
|
||||
/// bytes in size.
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_char_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_load(
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: *const u8,
|
||||
data_length: usize,
|
||||
) -> NonNull<SPCharGrid> {
|
||||
assert!(data.is_null());
|
||||
let data = std::slice::from_raw_parts(data, data_length);
|
||||
let result = Box::new(SPCharGrid(
|
||||
servicepoint::CharGrid::load_utf8(width, height, data.to_vec())
|
||||
.unwrap(),
|
||||
));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Clones a [SPCharGrid].
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPCharGrid]
|
||||
/// - `char_grid` is not written to concurrently
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_char_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_clone(
|
||||
char_grid: *const SPCharGrid,
|
||||
) -> NonNull<SPCharGrid> {
|
||||
assert!(!char_grid.is_null());
|
||||
let result = Box::new((*char_grid).clone());
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPCharGrid].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPCharGrid]
|
||||
/// - `char_grid` is not used concurrently or after char_grid call
|
||||
/// - `char_grid` was not passed to another consuming function, e.g. to create a [SPCommand]
|
||||
///
|
||||
/// [SPCommand]: [crate::SPCommand]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_free(char_grid: *mut SPCharGrid) {
|
||||
assert!(!char_grid.is_null());
|
||||
_ = Box::from_raw(char_grid);
|
||||
}
|
||||
|
||||
/// Gets the current value at the specified position.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `char_grid`: instance to read from
|
||||
/// - `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
/// - when accessing `x` or `y` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPCharGrid]
|
||||
/// - `char_grid` is not written to concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_get(
|
||||
char_grid: *const SPCharGrid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> u32 {
|
||||
assert!(!char_grid.is_null());
|
||||
(*char_grid).0.get(x, y) as u32
|
||||
}
|
||||
|
||||
/// Sets the value of the specified position in the [SPCharGrid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `char_grid`: instance to write to
|
||||
/// - `x` and `y`: position of the cell
|
||||
/// - `value`: the value to write to the cell
|
||||
///
|
||||
/// returns: old value of the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
/// - when accessing `x` or `y` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPBitVec]
|
||||
/// - `char_grid` is not written to or read from concurrently
|
||||
///
|
||||
/// [SPBitVec]: [crate::SPBitVec]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_set(
|
||||
char_grid: *mut SPCharGrid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
value: u32,
|
||||
) {
|
||||
assert!(!char_grid.is_null());
|
||||
(*char_grid).0.set(x, y, char::from_u32(value).unwrap());
|
||||
}
|
||||
|
||||
/// Sets the value of all cells in the [SPCharGrid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `char_grid`: instance to write to
|
||||
/// - `value`: the value to set all cells to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPCharGrid]
|
||||
/// - `char_grid` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_fill(
|
||||
char_grid: *mut SPCharGrid,
|
||||
value: u32,
|
||||
) {
|
||||
assert!(!char_grid.is_null());
|
||||
(*char_grid).0.fill(char::from_u32(value).unwrap());
|
||||
}
|
||||
|
||||
/// Gets the width of the [SPCharGrid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `char_grid`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPCharGrid]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_width(
|
||||
char_grid: *const SPCharGrid,
|
||||
) -> usize {
|
||||
assert!(!char_grid.is_null());
|
||||
(*char_grid).0.width()
|
||||
}
|
||||
|
||||
/// Gets the height of the [SPCharGrid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `char_grid`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `char_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `char_grid` points to a valid [SPCharGrid]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_char_grid_height(
|
||||
char_grid: *const SPCharGrid,
|
||||
) -> usize {
|
||||
assert!(!char_grid.is_null());
|
||||
(*char_grid).0.height()
|
||||
}
|
|
@ -1,498 +0,0 @@
|
|||
//! C functions for interacting with [SPCommand]s
|
||||
//!
|
||||
//! prefix `sp_command_`
|
||||
|
||||
use std::ptr::{null_mut, NonNull};
|
||||
|
||||
use crate::{
|
||||
SPBitVec, SPBitmap, SPBrightnessGrid, SPCharGrid, SPCompressionCode,
|
||||
SPCp437Grid, SPPacket,
|
||||
};
|
||||
|
||||
/// A low-level display command.
|
||||
///
|
||||
/// This struct and associated functions implement the UDP protocol for the display.
|
||||
///
|
||||
/// To send a [SPCommand], use a [SPConnection].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// sp_connection_send_command(connection, sp_command_clear());
|
||||
/// sp_connection_send_command(connection, sp_command_brightness(5));
|
||||
/// ```
|
||||
///
|
||||
/// [SPConnection]: [crate::SPConnection]
|
||||
pub struct SPCommand(pub(crate) servicepoint::Command);
|
||||
|
||||
impl Clone for SPCommand {
|
||||
fn clone(&self) -> Self {
|
||||
SPCommand(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to turn a [SPPacket] into a [SPCommand].
|
||||
///
|
||||
/// The packet is deallocated in the process.
|
||||
///
|
||||
/// Returns: pointer to new [SPCommand] instance or NULL if parsing failed.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `packet` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - [SPPacket] points to a valid instance of [SPPacket]
|
||||
/// - [SPPacket] is not used concurrently or after this call
|
||||
/// - the result is checked for NULL
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_try_from_packet(
|
||||
packet: *mut SPPacket,
|
||||
) -> *mut SPCommand {
|
||||
let packet = *Box::from_raw(packet);
|
||||
match servicepoint::Command::try_from(packet.0) {
|
||||
Err(_) => null_mut(),
|
||||
Ok(command) => Box::into_raw(Box::new(SPCommand(command))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clones a [SPCommand] instance.
|
||||
///
|
||||
/// returns: new [SPCommand] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `command` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `command` points to a valid instance of [SPCommand]
|
||||
/// - `command` is not written to concurrently
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_clone(
|
||||
command: *const SPCommand,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!command.is_null());
|
||||
let result = Box::new((*command).clone());
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Set all pixels to the off state.
|
||||
///
|
||||
/// Does not affect brightness.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::Clear] instance. Will never return NULL.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// sp_connection_send_command(connection, sp_command_clear());
|
||||
/// ```
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_clear() -> NonNull<SPCommand> {
|
||||
let result = Box::new(SPCommand(servicepoint::Command::Clear));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Kills the udp daemon on the display, which usually results in a restart.
|
||||
///
|
||||
/// Please do not send this in your normal program flow.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::HardReset] instance. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_hard_reset() -> NonNull<SPCommand> {
|
||||
let result = Box::new(SPCommand(servicepoint::Command::HardReset));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// A yet-to-be-tested command.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::FadeOut] instance. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_fade_out() -> NonNull<SPCommand> {
|
||||
let result = Box::new(SPCommand(servicepoint::Command::FadeOut));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Set the brightness of all tiles to the same value.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::Brightness] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - When the provided brightness value is out of range (0-11).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_brightness(
|
||||
brightness: u8,
|
||||
) -> NonNull<SPCommand> {
|
||||
let brightness = servicepoint::Brightness::try_from(brightness)
|
||||
.expect("invalid brightness");
|
||||
let result =
|
||||
Box::new(SPCommand(servicepoint::Command::Brightness(brightness)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Set the brightness of individual tiles in a rectangular area of the display.
|
||||
///
|
||||
/// The passed [SPBrightnessGrid] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::CharBrightness] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `grid` points to a valid instance of [SPBrightnessGrid]
|
||||
/// - `grid` is not used concurrently or after this call
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_char_brightness(
|
||||
x: usize,
|
||||
y: usize,
|
||||
grid: *mut SPBrightnessGrid,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!grid.is_null());
|
||||
let byte_grid = *Box::from_raw(grid);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::CharBrightness(
|
||||
servicepoint::Origin::new(x, y),
|
||||
byte_grid.0,
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// 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 [SPBitVec] is always uncompressed.
|
||||
///
|
||||
/// The passed [SPBitVec] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::BitmapLinear] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is null
|
||||
/// - when `compression_code` is not a valid value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid instance of [SPBitVec]
|
||||
/// - `bit_vec` is not used concurrently or after this call
|
||||
/// - `compression` matches one of the allowed enum values
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_bitmap_linear(
|
||||
offset: usize,
|
||||
bit_vec: *mut SPBitVec,
|
||||
compression: SPCompressionCode,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!bit_vec.is_null());
|
||||
let bit_vec = *Box::from_raw(bit_vec);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::BitmapLinear(
|
||||
offset,
|
||||
bit_vec.into(),
|
||||
compression.try_into().expect("invalid compression code"),
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// 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 [SPBitVec] is always uncompressed.
|
||||
///
|
||||
/// The passed [SPBitVec] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::BitmapLinearAnd] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is null
|
||||
/// - when `compression_code` is not a valid value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid instance of [SPBitVec]
|
||||
/// - `bit_vec` is not used concurrently or after this call
|
||||
/// - `compression` matches one of the allowed enum values
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_bitmap_linear_and(
|
||||
offset: usize,
|
||||
bit_vec: *mut SPBitVec,
|
||||
compression: SPCompressionCode,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!bit_vec.is_null());
|
||||
let bit_vec = *Box::from_raw(bit_vec);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::BitmapLinearAnd(
|
||||
offset,
|
||||
bit_vec.into(),
|
||||
compression.try_into().expect("invalid compression code"),
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// 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 [SPBitVec] is always uncompressed.
|
||||
///
|
||||
/// The passed [SPBitVec] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::BitmapLinearOr] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is null
|
||||
/// - when `compression_code` is not a valid value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid instance of [SPBitVec]
|
||||
/// - `bit_vec` is not used concurrently or after this call
|
||||
/// - `compression` matches one of the allowed enum values
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_bitmap_linear_or(
|
||||
offset: usize,
|
||||
bit_vec: *mut SPBitVec,
|
||||
compression: SPCompressionCode,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!bit_vec.is_null());
|
||||
let bit_vec = *Box::from_raw(bit_vec);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::BitmapLinearOr(
|
||||
offset,
|
||||
bit_vec.into(),
|
||||
compression.try_into().expect("invalid compression code"),
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// 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 [SPBitVec] is always uncompressed.
|
||||
///
|
||||
/// The passed [SPBitVec] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::BitmapLinearXor] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bit_vec` is null
|
||||
/// - when `compression_code` is not a valid value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bit_vec` points to a valid instance of [SPBitVec]
|
||||
/// - `bit_vec` is not used concurrently or after this call
|
||||
/// - `compression` matches one of the allowed enum values
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_bitmap_linear_xor(
|
||||
offset: usize,
|
||||
bit_vec: *mut SPBitVec,
|
||||
compression: SPCompressionCode,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!bit_vec.is_null());
|
||||
let bit_vec = *Box::from_raw(bit_vec);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::BitmapLinearXor(
|
||||
offset,
|
||||
bit_vec.into(),
|
||||
compression.try_into().expect("invalid compression code"),
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Show codepage 437 encoded text on the screen.
|
||||
///
|
||||
/// The passed [SPCp437Grid] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::Cp437Data] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `grid` is null
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `grid` points to a valid instance of [SPCp437Grid]
|
||||
/// - `grid` is not used concurrently or after this call
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_cp437_data(
|
||||
x: usize,
|
||||
y: usize,
|
||||
grid: *mut SPCp437Grid,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!grid.is_null());
|
||||
let grid = *Box::from_raw(grid);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::Cp437Data(
|
||||
servicepoint::Origin::new(x, y),
|
||||
grid.0,
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Show UTF-8 encoded text on the screen.
|
||||
///
|
||||
/// The passed [SPCharGrid] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::Utf8Data] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `grid` is null
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `grid` points to a valid instance of [SPCharGrid]
|
||||
/// - `grid` is not used concurrently or after this call
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_utf8_data(
|
||||
x: usize,
|
||||
y: usize,
|
||||
grid: *mut SPCharGrid,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!grid.is_null());
|
||||
let grid = *Box::from_raw(grid);
|
||||
let result = Box::new(SPCommand(servicepoint::Command::Utf8Data(
|
||||
servicepoint::Origin::new(x, y),
|
||||
grid.0,
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Sets a window of pixels to the specified values.
|
||||
///
|
||||
/// The passed [SPBitmap] gets consumed.
|
||||
///
|
||||
/// Returns: a new [servicepoint::Command::BitmapLinearWin] instance. Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `bitmap` is null
|
||||
/// - when `compression_code` is not a valid value
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `bitmap` points to a valid instance of [SPBitmap]
|
||||
/// - `bitmap` is not used concurrently or after this call
|
||||
/// - `compression` matches one of the allowed enum values
|
||||
/// - the returned [SPCommand] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_command_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_bitmap_linear_win(
|
||||
x: usize,
|
||||
y: usize,
|
||||
bitmap: *mut SPBitmap,
|
||||
compression_code: SPCompressionCode,
|
||||
) -> NonNull<SPCommand> {
|
||||
assert!(!bitmap.is_null());
|
||||
let byte_grid = (*Box::from_raw(bitmap)).0;
|
||||
let result = Box::new(SPCommand(servicepoint::Command::BitmapLinearWin(
|
||||
servicepoint::Origin::new(x, y),
|
||||
byte_grid,
|
||||
compression_code
|
||||
.try_into()
|
||||
.expect("invalid compression code"),
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPCommand].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// SPCommand c = sp_command_clear();
|
||||
/// sp_command_free(c);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `command` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `command` points to a valid [SPCommand]
|
||||
/// - `command` is not used concurrently or after this call
|
||||
/// - `command` was not passed to another consuming function, e.g. to create a [SPPacket]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_command_free(command: *mut SPCommand) {
|
||||
assert!(!command.is_null());
|
||||
_ = Box::from_raw(command);
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
//! C functions for interacting with [SPConnection]s
|
||||
//!
|
||||
//! prefix `sp_connection_`
|
||||
|
||||
use std::ffi::{c_char, CStr};
|
||||
use std::ptr::{null_mut, NonNull};
|
||||
|
||||
use crate::{SPCommand, SPPacket};
|
||||
|
||||
/// A connection to the display.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// CConnection connection = sp_connection_open("172.23.42.29:2342");
|
||||
/// if (connection != NULL)
|
||||
/// sp_connection_send_command(connection, sp_command_clear());
|
||||
/// ```
|
||||
pub struct SPConnection(pub(crate) servicepoint::Connection);
|
||||
|
||||
/// Creates a new instance of [SPConnection].
|
||||
///
|
||||
/// returns: NULL if connection fails, or connected instance
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `host` is null or an invalid host
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_connection_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_connection_open(
|
||||
host: *const c_char,
|
||||
) -> *mut SPConnection {
|
||||
assert!(!host.is_null());
|
||||
let host = CStr::from_ptr(host).to_str().expect("Bad encoding");
|
||||
let connection = match servicepoint::Connection::open(host) {
|
||||
Err(_) => return null_mut(),
|
||||
Ok(value) => value,
|
||||
};
|
||||
|
||||
Box::into_raw(Box::new(SPConnection(connection)))
|
||||
}
|
||||
|
||||
/// Creates a new instance of [SPConnection] for testing that does not actually send anything.
|
||||
///
|
||||
/// returns: a new instance. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_connection_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_connection_fake() -> NonNull<SPConnection> {
|
||||
let result = Box::new(SPConnection(servicepoint::Connection::Fake));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Sends a [SPPacket] to the display using the [SPConnection].
|
||||
///
|
||||
/// The passed `packet` gets consumed.
|
||||
///
|
||||
/// returns: true in case of success
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `connection` is NULL
|
||||
/// - when `packet` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `connection` points to a valid instance of [SPConnection]
|
||||
/// - `packet` points to a valid instance of [SPPacket]
|
||||
/// - `packet` is not used concurrently or after this call
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_connection_send_packet(
|
||||
connection: *const SPConnection,
|
||||
packet: *mut SPPacket,
|
||||
) -> bool {
|
||||
assert!(!connection.is_null());
|
||||
assert!(!packet.is_null());
|
||||
let packet = Box::from_raw(packet);
|
||||
(*connection).0.send((*packet).0).is_ok()
|
||||
}
|
||||
|
||||
/// Sends a [SPCommand] to the display using the [SPConnection].
|
||||
///
|
||||
/// The passed `command` gets consumed.
|
||||
///
|
||||
/// returns: true in case of success
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `connection` is NULL
|
||||
/// - when `command` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `connection` points to a valid instance of [SPConnection]
|
||||
/// - `command` points to a valid instance of [SPPacket]
|
||||
/// - `command` is not used concurrently or after this call
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_connection_send_command(
|
||||
connection: *const SPConnection,
|
||||
command: *mut SPCommand,
|
||||
) -> bool {
|
||||
assert!(!connection.is_null());
|
||||
assert!(!command.is_null());
|
||||
let command = (*Box::from_raw(command)).0;
|
||||
(*connection).0.send(command).is_ok()
|
||||
}
|
||||
|
||||
/// Closes and deallocates a [SPConnection].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `connection` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `connection` points to a valid [SPConnection]
|
||||
/// - `connection` is not used concurrently or after this call
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_connection_free(connection: *mut SPConnection) {
|
||||
assert!(!connection.is_null());
|
||||
_ = Box::from_raw(connection);
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
//! re-exported constants for use in C
|
||||
|
||||
use servicepoint::CompressionCode;
|
||||
use std::time::Duration;
|
||||
|
||||
/// size of a single tile in one dimension
|
||||
pub const SP_TILE_SIZE: usize = 8;
|
||||
|
||||
/// Display tile count in the x-direction
|
||||
pub const SP_TILE_WIDTH: usize = 56;
|
||||
|
||||
/// Display tile count in the y-direction
|
||||
pub const SP_TILE_HEIGHT: usize = 20;
|
||||
|
||||
/// Display width in pixels
|
||||
pub const SP_PIXEL_WIDTH: usize = SP_TILE_WIDTH * SP_TILE_SIZE;
|
||||
|
||||
/// Display height in pixels
|
||||
pub const SP_PIXEL_HEIGHT: usize = SP_TILE_HEIGHT * SP_TILE_SIZE;
|
||||
|
||||
/// pixel count on whole screen
|
||||
pub const SP_PIXEL_COUNT: usize = SP_PIXEL_WIDTH * SP_PIXEL_HEIGHT;
|
||||
|
||||
/// Actual hardware limit is around 28-29ms/frame. Rounded up for less dropped packets.
|
||||
pub const SP_FRAME_PACING_MS: u128 = Duration::from_millis(30).as_millis();
|
||||
|
||||
/// Specifies the kind of compression to use.
|
||||
#[repr(u16)]
|
||||
pub enum SPCompressionCode {
|
||||
/// no compression
|
||||
Uncompressed = 0x0,
|
||||
/// compress using flate2 with zlib header
|
||||
Zlib = 0x677a,
|
||||
/// compress using bzip2
|
||||
Bzip2 = 0x627a,
|
||||
/// compress using lzma
|
||||
Lzma = 0x6c7a,
|
||||
/// compress using Zstandard
|
||||
Zstd = 0x7a73,
|
||||
}
|
||||
|
||||
impl TryFrom<SPCompressionCode> for CompressionCode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: SPCompressionCode) -> Result<Self, Self::Error> {
|
||||
CompressionCode::try_from(value as u16)
|
||||
}
|
||||
}
|
|
@ -1,285 +0,0 @@
|
|||
//! C functions for interacting with [SPCp437Grid]s
|
||||
//!
|
||||
//! prefix `sp_cp437_grid_`
|
||||
|
||||
use crate::SPByteSlice;
|
||||
use servicepoint::{DataRef, Grid};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// A C-wrapper for grid containing codepage 437 characters.
|
||||
///
|
||||
/// The encoding is currently not enforced.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```C
|
||||
/// Cp437Grid grid = sp_cp437_grid_new(4, 3);
|
||||
/// sp_cp437_grid_fill(grid, '?');
|
||||
/// sp_cp437_grid_set(grid, 0, 0, '!');
|
||||
/// sp_cp437_grid_free(grid);
|
||||
/// ```
|
||||
pub struct SPCp437Grid(pub(crate) servicepoint::Cp437Grid);
|
||||
|
||||
impl Clone for SPCp437Grid {
|
||||
fn clone(&self) -> Self {
|
||||
SPCp437Grid(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [SPCp437Grid] with the specified dimensions.
|
||||
///
|
||||
/// returns: [SPCp437Grid] initialized to 0. Will never return NULL.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_cp437_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> NonNull<SPCp437Grid> {
|
||||
let result =
|
||||
Box::new(SPCp437Grid(servicepoint::Cp437Grid::new(width, height)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Loads a [SPCp437Grid] with the specified dimensions from the provided data.
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `data` is NULL
|
||||
/// - when the provided `data_length` does not match `height` and `width`
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `data` points to a valid memory location of at least `data_length`
|
||||
/// bytes in size.
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_cp437_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_load(
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: *const u8,
|
||||
data_length: usize,
|
||||
) -> NonNull<SPCp437Grid> {
|
||||
assert!(data.is_null());
|
||||
let data = std::slice::from_raw_parts(data, data_length);
|
||||
let result = Box::new(SPCp437Grid(servicepoint::Cp437Grid::load(
|
||||
width, height, data,
|
||||
)));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Clones a [SPCp437Grid].
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
/// - `cp437_grid` is not written to concurrently
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_cp437_grid_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_clone(
|
||||
cp437_grid: *const SPCp437Grid,
|
||||
) -> NonNull<SPCp437Grid> {
|
||||
assert!(!cp437_grid.is_null());
|
||||
let result = Box::new((*cp437_grid).clone());
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPCp437Grid].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
/// - `cp437_grid` is not used concurrently or after cp437_grid call
|
||||
/// - `cp437_grid` was not passed to another consuming function, e.g. to create a [SPCommand]
|
||||
///
|
||||
/// [SPCommand]: [crate::SPCommand]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_free(cp437_grid: *mut SPCp437Grid) {
|
||||
assert!(!cp437_grid.is_null());
|
||||
_ = Box::from_raw(cp437_grid);
|
||||
}
|
||||
|
||||
/// Gets the current value at the specified position.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `cp437_grid`: instance to read from
|
||||
/// - `x` and `y`: position of the cell to read
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
/// - when accessing `x` or `y` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
/// - `cp437_grid` is not written to concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_get(
|
||||
cp437_grid: *const SPCp437Grid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> u8 {
|
||||
assert!(!cp437_grid.is_null());
|
||||
(*cp437_grid).0.get(x, y)
|
||||
}
|
||||
|
||||
/// Sets the value of the specified position in the [SPCp437Grid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `cp437_grid`: instance to write to
|
||||
/// - `x` and `y`: position of the cell
|
||||
/// - `value`: the value to write to the cell
|
||||
///
|
||||
/// returns: old value of the cell
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
/// - when accessing `x` or `y` out of bounds
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPBitVec]
|
||||
/// - `cp437_grid` is not written to or read from concurrently
|
||||
///
|
||||
/// [SPBitVec]: [crate::SPBitVec]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_set(
|
||||
cp437_grid: *mut SPCp437Grid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
value: u8,
|
||||
) {
|
||||
assert!(!cp437_grid.is_null());
|
||||
(*cp437_grid).0.set(x, y, value);
|
||||
}
|
||||
|
||||
/// Sets the value of all cells in the [SPCp437Grid].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `cp437_grid`: instance to write to
|
||||
/// - `value`: the value to set all cells to
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
/// - `cp437_grid` is not written to or read from concurrently
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_fill(
|
||||
cp437_grid: *mut SPCp437Grid,
|
||||
value: u8,
|
||||
) {
|
||||
assert!(!cp437_grid.is_null());
|
||||
(*cp437_grid).0.fill(value);
|
||||
}
|
||||
|
||||
/// Gets the width of the [SPCp437Grid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `cp437_grid`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_width(
|
||||
cp437_grid: *const SPCp437Grid,
|
||||
) -> usize {
|
||||
assert!(!cp437_grid.is_null());
|
||||
(*cp437_grid).0.width()
|
||||
}
|
||||
|
||||
/// Gets the height of the [SPCp437Grid] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `cp437_grid`: instance to read from
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_height(
|
||||
cp437_grid: *const SPCp437Grid,
|
||||
) -> usize {
|
||||
assert!(!cp437_grid.is_null());
|
||||
(*cp437_grid).0.height()
|
||||
}
|
||||
|
||||
/// Gets an unsafe reference to the data of the [SPCp437Grid] instance.
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `cp437_grid` is NULL
|
||||
///
|
||||
/// ## Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `cp437_grid` points to a valid [SPCp437Grid]
|
||||
/// - the returned memory range is never accessed after the passed [SPCp437Grid] has been freed
|
||||
/// - the returned memory range is never accessed concurrently, either via the [SPCp437Grid] or directly
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_cp437_grid_unsafe_data_ref(
|
||||
cp437_grid: *mut SPCp437Grid,
|
||||
) -> SPByteSlice {
|
||||
let data = (*cp437_grid).0.data_ref_mut();
|
||||
SPByteSlice {
|
||||
start: NonNull::new(data.as_mut_ptr_range().start).unwrap(),
|
||||
length: data.len(),
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
//! C API wrapper for the [servicepoint](https://docs.rs/servicepoint/latest/servicepoint/) crate.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Make sure to check out [this GitHub repo](https://github.com/arfst23/ServicePoint) as well!
|
||||
//!
|
||||
//! ```C
|
||||
//! #include <stdio.h>
|
||||
//! #include "servicepoint.h"
|
||||
//!
|
||||
//! int main(void) {
|
||||
//! SPConnection *connection = sp_connection_open("172.23.42.29:2342");
|
||||
//! if (connection == NULL)
|
||||
//! return 1;
|
||||
//!
|
||||
//! SPBitmap *pixels = sp_bitmap_new(SP_PIXEL_WIDTH, SP_PIXEL_HEIGHT);
|
||||
//! sp_bitmap_fill(pixels, true);
|
||||
//!
|
||||
//! SPCommand *command = sp_command_bitmap_linear_win(0, 0, pixels, Uncompressed);
|
||||
//! while (sp_connection_send_command(connection, sp_command_clone(command)));
|
||||
//!
|
||||
//! sp_command_free(command);
|
||||
//! sp_connection_free(connection);
|
||||
//! return 0;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use crate::bitmap::*;
|
||||
pub use crate::bitvec::*;
|
||||
pub use crate::brightness_grid::*;
|
||||
pub use crate::byte_slice::*;
|
||||
pub use crate::char_grid::*;
|
||||
pub use crate::command::*;
|
||||
pub use crate::connection::*;
|
||||
pub use crate::constants::*;
|
||||
pub use crate::cp437_grid::*;
|
||||
pub use crate::packet::*;
|
||||
|
||||
mod bitmap;
|
||||
mod bitvec;
|
||||
mod brightness_grid;
|
||||
mod byte_slice;
|
||||
mod char_grid;
|
||||
mod command;
|
||||
mod connection;
|
||||
mod constants;
|
||||
mod cp437_grid;
|
||||
mod packet;
|
|
@ -1,166 +0,0 @@
|
|||
//! C functions for interacting with [SPPacket]s
|
||||
//!
|
||||
//! prefix `sp_packet_`
|
||||
|
||||
use std::ptr::{null_mut, NonNull};
|
||||
|
||||
use crate::SPCommand;
|
||||
|
||||
/// The raw packet
|
||||
pub struct SPPacket(pub(crate) servicepoint::Packet);
|
||||
|
||||
/// Turns a [SPCommand] into a [SPPacket].
|
||||
/// The [SPCommand] gets consumed.
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `command` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - [SPCommand] points to a valid instance of [SPCommand]
|
||||
/// - [SPCommand] is not used concurrently or after this call
|
||||
/// - the returned [SPPacket] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_packet_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_packet_from_command(
|
||||
command: *mut SPCommand,
|
||||
) -> NonNull<SPPacket> {
|
||||
assert!(!command.is_null());
|
||||
let command = *Box::from_raw(command);
|
||||
let result = Box::new(SPPacket(command.0.into()));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Tries to load a [SPPacket] from the passed array with the specified length.
|
||||
///
|
||||
/// returns: NULL in case of an error, pointer to the allocated packet otherwise
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `data` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `data` points to a valid memory region of at least `length` bytes
|
||||
/// - `data` is not written to concurrently
|
||||
/// - the returned [SPPacket] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_packet_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_packet_try_load(
|
||||
data: *const u8,
|
||||
length: usize,
|
||||
) -> *mut SPPacket {
|
||||
assert!(!data.is_null());
|
||||
let data = std::slice::from_raw_parts(data, length);
|
||||
match servicepoint::Packet::try_from(data) {
|
||||
Err(_) => null_mut(),
|
||||
Ok(packet) => Box::into_raw(Box::new(SPPacket(packet))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a raw [SPPacket] from parts.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `command_code` specifies which command this packet contains
|
||||
/// - `a`, `b`, `c` and `d` are command-specific header values
|
||||
/// - `payload` is the optional data that is part of the command
|
||||
/// - `payload_len` is the size of the payload
|
||||
///
|
||||
/// returns: new instance. Will never return null.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `payload` is null, but `payload_len` is not zero
|
||||
/// - when `payload_len` is zero, but `payload` is nonnull
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `payload` points to a valid memory region of at least `payload_len` bytes
|
||||
/// - `payload` is not written to concurrently
|
||||
/// - the returned [SPPacket] instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling [sp_packet_free].
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_packet_from_parts(
|
||||
command_code: u16,
|
||||
a: u16,
|
||||
b: u16,
|
||||
c: u16,
|
||||
d: u16,
|
||||
payload: *const u8,
|
||||
payload_len: usize,
|
||||
) -> NonNull<SPPacket> {
|
||||
assert_eq!(payload.is_null(), payload_len == 0);
|
||||
|
||||
let payload = if payload.is_null() {
|
||||
vec![]
|
||||
} else {
|
||||
let payload = std::slice::from_raw_parts(payload, payload_len);
|
||||
Vec::from(payload)
|
||||
};
|
||||
|
||||
let packet = servicepoint::Packet {
|
||||
header: servicepoint::Header {
|
||||
command_code,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
},
|
||||
payload,
|
||||
};
|
||||
let result = Box::new(SPPacket(packet));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Clones a [SPPacket].
|
||||
///
|
||||
/// Will never return NULL.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `packet` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `packet` points to a valid [SPPacket]
|
||||
/// - `packet` is not written to concurrently
|
||||
/// - the returned instance is freed in some way, either by using a consuming function or
|
||||
/// by explicitly calling `sp_packet_free`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_packet_clone(
|
||||
packet: *const SPPacket,
|
||||
) -> NonNull<SPPacket> {
|
||||
assert!(!packet.is_null());
|
||||
let result = Box::new(SPPacket((*packet).0.clone()));
|
||||
NonNull::from(Box::leak(result))
|
||||
}
|
||||
|
||||
/// Deallocates a [SPPacket].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - when `packet` is NULL
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller has to make sure that:
|
||||
///
|
||||
/// - `packet` points to a valid [SPPacket]
|
||||
/// - `packet` is not used concurrently or after this call
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sp_packet_free(packet: *mut SPPacket) {
|
||||
assert!(!packet.is_null());
|
||||
_ = Box::from_raw(packet)
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
[package]
|
||||
name = "servicepoint_binding_uniffi"
|
||||
version.workspace = true
|
||||
publish = false
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "C bindings for the servicepoint crate."
|
||||
homepage = "https://docs.rs/crate/servicepoint_binding_c"
|
||||
repository = "https://git.berlin.ccc.de/servicepoint/servicepoint"
|
||||
#readme = "README.md"
|
||||
keywords = ["cccb", "cccb-servicepoint", "uniffi"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.25.3", features = ["build"] }
|
||||
|
||||
[dependencies]
|
||||
uniffi = { version = "0.25.3" }
|
||||
thiserror.workspace = true
|
||||
|
||||
[dependencies.servicepoint]
|
||||
version = "0.13.1"
|
||||
path = "../servicepoint"
|
||||
features = ["all_compressions"]
|
||||
|
||||
[dependencies.uniffi-bindgen-cs]
|
||||
git = "https://github.com/NordSecurity/uniffi-bindgen-cs"
|
||||
# tag="v0.8.3+v0.25.0"
|
||||
rev = "f68639fbc720b50ebe561ba75c66c84dc456bdce"
|
||||
optional = true
|
||||
|
||||
[dependencies.uniffi-bindgen-go]
|
||||
git = "https://github.com/NordSecurity/uniffi-bindgen-go.git"
|
||||
# tag = "0.2.2+v0.25.0"
|
||||
rev = "ba23bab72f1a9bcc39ce81924d3d9265598e017c"
|
||||
optional = true
|
||||
|
||||
[lints]
|
||||
#workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen"
|
||||
required-features = ["uniffi/cli"]
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen-cs"
|
||||
required-features = ["cs"]
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen-go"
|
||||
required-features = ["go"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cs = ["dep:uniffi-bindgen-cs"]
|
||||
go = ["dep:uniffi-bindgen-go"]
|
|
@ -1,90 +0,0 @@
|
|||
# ServicePoint
|
||||
|
||||
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point
|
||||
Display" or "Airport Display".
|
||||
|
||||
This crate contains bindings for multiple programming languages, enabling non-rust-developers to use the library.
|
||||
|
||||
Also take a look at the main project [README](https://git.berlin.ccc.de/servicepoint/servicepoint/src/branch/main/README.md) for more
|
||||
information.
|
||||
|
||||
## Note on stability
|
||||
|
||||
This library is still in early development.
|
||||
You can absolutely use it, and it works, but expect minor breaking changes with every version bump.
|
||||
|
||||
## Notes on differences to rust library
|
||||
|
||||
- Performance will not be as good as the rust version:
|
||||
- most objects are reference counted.
|
||||
- objects with mutating methods will also have a MRSW lock
|
||||
- You will not get rust backtraces in release builds of the native code
|
||||
- Panic messages will work (PanicException)
|
||||
|
||||
## Supported languages
|
||||
|
||||
| Language | Support level | Notes |
|
||||
|-----------|---------------|-------------------------------------------------------------------------------------------------|
|
||||
| .NET (C#) | Full | see dedicated section |
|
||||
| Ruby | Working | LD_LIBRARY_PATH has to be set, see example project |
|
||||
| Python | Tested once | Required project file not included. The shared library will be loaded from the script location. |
|
||||
| Go | untested | |
|
||||
| Kotlin | untested | |
|
||||
| Swift | untested | |
|
||||
|
||||
## Installation
|
||||
|
||||
Including this repository as a submodule and building from source is the recommended way of using the library.
|
||||
|
||||
```bash
|
||||
git submodule add https://git.berlin.ccc.de/servicepoint/servicepoint.git
|
||||
git commit -m "add servicepoint submodule"
|
||||
```
|
||||
|
||||
Run `generate-bindings.sh` to regenerate all bindings. This will also build `libservicepoint.so` (or equivalent on your
|
||||
platform).
|
||||
|
||||
For languages not fully supported, there will be no project file for the library, just the naked source file(s).
|
||||
If you successfully use a language, please open an issue or PR to add the missing ones.
|
||||
|
||||
## .NET (C#)
|
||||
|
||||
This is the best supported language.
|
||||
|
||||
F# is not tested. If there are usability or functionality problems, please open an issue.
|
||||
|
||||
Currently, the project file is hard-coded for Linux and will need tweaks for other platforms (e.g. `.dylib` instead of `.so`).
|
||||
|
||||
You do not have to compile or copy the rust crate manually, as building `ServicePoint.csproj` also builds it.
|
||||
|
||||
### Example
|
||||
|
||||
```csharp
|
||||
using System.Threading;
|
||||
using ServicePoint;
|
||||
|
||||
var connection = new Connection("127.0.0.1:2342");
|
||||
connection.Send(Command.Clear());
|
||||
|
||||
connection.Send(Command.Brightness(5));
|
||||
|
||||
var pixels = Bitmap.NewMaxSized();
|
||||
for (ulong offset = 0; offset < ulong.MaxValue; offset++)
|
||||
{
|
||||
pixels.Fill(false);
|
||||
|
||||
for (ulong y = 0; y < pixels.Height(); y++)
|
||||
pixels.Set((y + offset) % pixels.Width(), y, true);
|
||||
|
||||
connection.Send(Command.BitmapLinearWin(0, 0, pixels));
|
||||
Thread.Sleep(14);
|
||||
}
|
||||
```
|
||||
|
||||
A full example including project files is available as part of this crate.
|
||||
|
||||
### Why is there no NuGet-Package?
|
||||
|
||||
NuGet packages are not a good way to distribute native
|
||||
binaries ([relevant issue](https://github.com/dotnet/sdk/issues/33845)).
|
||||
Because of that, there is no NuGet package you can use directly.
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cargo build --release
|
||||
|
||||
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
TARGET_PATH="$(realpath "$SCRIPT_PATH"/../../target/release)"
|
||||
SERVICEPOINT_SO="$TARGET_PATH/libservicepoint_binding_uniffi.so"
|
||||
LIBRARIES_PATH="$SCRIPT_PATH/libraries"
|
||||
|
||||
echo "Source: $SERVICEPOINT_SO"
|
||||
echo "Output: $LIBRARIES_PATH"
|
||||
|
||||
BINDGEN="cargo run --features=uniffi/cli --bin uniffi-bindgen -- "
|
||||
BINDGEN_CS="cargo run --features=cs --bin uniffi-bindgen-cs -- "
|
||||
BINDGEN_GO="cargo run --features=go --bin uniffi-bindgen-go -- "
|
||||
COMMON_ARGS="--library $SERVICEPOINT_SO"
|
||||
|
||||
${BINDGEN} generate $COMMON_ARGS --language python --out-dir "$LIBRARIES_PATH/python"
|
||||
${BINDGEN} generate $COMMON_ARGS --language kotlin --out-dir "$LIBRARIES_PATH/kotlin"
|
||||
${BINDGEN} generate $COMMON_ARGS --language swift --out-dir "$LIBRARIES_PATH/swift"
|
||||
${BINDGEN} generate $COMMON_ARGS --language ruby --out-dir "$LIBRARIES_PATH/ruby/lib"
|
||||
${BINDGEN_CS} $COMMON_ARGS --out-dir "$LIBRARIES_PATH/csharp/ServicePoint"
|
||||
${BINDGEN_GO} $COMMON_ARGS --out-dir "$LIBRARIES_PATH/go/"
|
|
@ -1,4 +0,0 @@
|
|||
go
|
||||
kotlin
|
||||
python
|
||||
swift
|
|
@ -1,2 +0,0 @@
|
|||
bin
|
||||
obj
|
|
@ -1,2 +0,0 @@
|
|||
bin
|
||||
obj
|
|
@ -1,19 +0,0 @@
|
|||
using System.Threading;
|
||||
using ServicePoint;
|
||||
|
||||
var connection = new Connection("127.0.0.1:2342");
|
||||
connection.Send(Command.Clear());
|
||||
|
||||
connection.Send(Command.Brightness(5));
|
||||
|
||||
var pixels = Bitmap.NewMaxSized();
|
||||
for (ulong offset = 0; offset < ulong.MaxValue; offset++)
|
||||
{
|
||||
pixels.Fill(false);
|
||||
|
||||
for (ulong y = 0; y < pixels.Height(); y++)
|
||||
pixels.Set((y + offset) % pixels.Width(), y, true);
|
||||
|
||||
connection.Send(Command.BitmapLinearWin(0, 0, pixels));
|
||||
Thread.Sleep(14);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>ServicePoint.Example</RootNamespace>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../ServicePoint/ServicePoint.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,16 +0,0 @@
|
|||
namespace ServicePoint.Tests;
|
||||
|
||||
public class BitmapTests
|
||||
{
|
||||
[Fact]
|
||||
public void BasicFunctions()
|
||||
{
|
||||
var bitmap = new Bitmap(8, 2);
|
||||
Assert.False(bitmap.Get(0, 0));
|
||||
Assert.False(bitmap.Get(bitmap.Width() - 1, bitmap.Height() - 1));
|
||||
bitmap.Fill(true);
|
||||
Assert.True(bitmap.Get(1, 1));
|
||||
bitmap.Set(1, 1, false);
|
||||
Assert.False(bitmap.Get(1, 1));
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
namespace ServicePoint.Tests;
|
||||
|
||||
public class CharGridTests
|
||||
{
|
||||
[Fact]
|
||||
public void BasicFunctions()
|
||||
{
|
||||
var grid = new CharGrid(8, 2);
|
||||
Assert.Equal("\0", grid.Get(0, 0));
|
||||
Assert.Equal("\0", grid.Get(grid.Width() - 1, grid.Height() - 1));
|
||||
grid.Fill(" ");
|
||||
Assert.Equal(" ", grid.Get(1, 1));
|
||||
grid.Set(1, 1, "-");
|
||||
Assert.Equal("-", grid.Get(1, 1));
|
||||
Assert.Throws<PanicException>(() => grid.Get(8, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowAndCol()
|
||||
{
|
||||
var grid = new CharGrid(3, 2);
|
||||
Assert.Equal("\0\0\0", grid.GetRow(0));
|
||||
grid.Fill(" ");
|
||||
Assert.Equal(" ", grid.GetCol(1));
|
||||
Assert.Throws<CharGridException.OutOfBounds>(() => grid.GetCol(3));
|
||||
Assert.Throws<CharGridException.InvalidSeriesLength>(() => grid.SetRow(1, "Text"));
|
||||
grid.SetRow(1, "Foo");
|
||||
Assert.Equal("Foo", grid.GetRow(1));
|
||||
Assert.Equal(" o", grid.GetCol(2));
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
namespace ServicePoint.Tests;
|
||||
|
||||
public class CommandTests
|
||||
{
|
||||
private Connection _connection = Connection.NewFake();
|
||||
|
||||
[Fact]
|
||||
public void ClearSendable()
|
||||
{
|
||||
_connection.Send(Command.Clear());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrightnessSendable()
|
||||
{
|
||||
_connection.Send(Command.Brightness(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidBrightnessThrows()
|
||||
{
|
||||
Assert.Throws<ServicePointException.InvalidBrightness>(() => Command.Brightness(42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FadeOutSendable()
|
||||
{
|
||||
_connection.Send(Command.FadeOut());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HardResetSendable()
|
||||
{
|
||||
_connection.Send(Command.HardReset());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BitmapLinearWinSendable()
|
||||
{
|
||||
_connection.Send(Command.BitmapLinearWin(0, 0, Bitmap.NewMaxSized(), CompressionCode.Uncompressed));
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
namespace ServicePoint.Tests;
|
||||
|
||||
public class ConnectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void InvalidHostnameThrows()
|
||||
{
|
||||
Assert.Throws<ServicePointException.IoException>(() => new Connection(""));
|
||||
Assert.Throws<ServicePointException.IoException>(() => new Connection("-%6$§"));
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
global using Xunit;
|
||||
global using ServicePoint;
|
|
@ -1,27 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../ServicePoint/ServicePoint.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
using ServicePoint;
|
||||
|
||||
public static class ServicePointConstants
|
||||
{
|
||||
private static readonly Constants _instance = ServicepointBindingUniffiMethods.GetConstants();
|
||||
|
||||
public static readonly ulong PixelWidth = _instance.pixelWidth;
|
||||
public static readonly ulong PixelHeight = _instance.pixelHeight;
|
||||
public static readonly ulong PixelCount = _instance.pixelCount;
|
||||
public static readonly ulong TileWidth = _instance.tileWidth;
|
||||
public static readonly ulong TileHeight = _instance.tileHeight;
|
||||
public static readonly ulong TileSize = _instance.tileSize;
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>ServicePoint</PackageId>
|
||||
<Version>0.13.1</Version>
|
||||
<Authors>Repository Authors</Authors>
|
||||
<Company>None</Company>
|
||||
<Product>ServicePoint</Product>
|
||||
<PackageTags>CCCB</PackageTags>
|
||||
<Description>
|
||||
C# bindings for the rust crate servicepoint. You will need a suitable native shared library to use this.
|
||||
For documentation, see the rust documentation: https://docs.rs/servicepoint/latest/servicepoint/.
|
||||
Note that this library is still in early development. Breaking changes are expected before 1.0 is released.
|
||||
</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- generate C# bindings -->
|
||||
<Target Name="BuildBindings" Condition="'$(Configuration)'=='Release'" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="cargo build -p servicepoint_binding_uniffi --release"/>
|
||||
</Target>
|
||||
<Target Name="BuildBindings" Condition="'$(Configuration)'=='Debug'" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="cargo build -p servicepoint_binding_uniffi"/>
|
||||
</Target>
|
||||
|
||||
<!-- include native binary in output -->
|
||||
<ItemGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<Content Include="../../../../../target/debug/libservicepoint_binding_uniffi.so" CopyToOutputDirectory="Always">
|
||||
<Link>libservicepoint_binding_uniffi.so</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Configuration)'=='Release'">
|
||||
<Content Include="../../../../../target/release/libservicepoint_binding_uniffi.so" CopyToOutputDirectory="Always">
|
||||
<Link>libservicepoint_binding_uniffi.so</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- add README.md to package -->
|
||||
<None Include="../README.md" Pack="true" PackagePath="\"/>
|
||||
<!-- include link to source code at revision -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
File diff suppressed because it is too large
Load diff
|
@ -1,34 +0,0 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint", "ServicePoint\ServicePoint.csproj", "{53576D3C-E32E-49BF-BF10-2DB504E50CE1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint.Example", "ServicePoint.Example\ServicePoint.Example.csproj", "{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint.Tests", "ServicePoint.Tests\ServicePoint.Tests.csproj", "{9DC15508-A980-4135-9FC6-659FF54B4E5C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
LD_LIBRARY_PATH="../../../../../target/release:$LD_LIBRARY_PATH" ruby example.rb
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
uniffi_bindgen_cs::main().unwrap();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
uniffi_bindgen_go::main().unwrap();
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
use servicepoint::{DataRef, Grid};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Bitmap {
|
||||
pub(crate) actual: RwLock<servicepoint::Bitmap>,
|
||||
}
|
||||
|
||||
impl Bitmap {
|
||||
fn internal_new(actual: servicepoint::Bitmap) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
actual: RwLock::new(actual),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Bitmap {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(width: u64, height: u64) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Bitmap::new(
|
||||
width as usize,
|
||||
height as usize,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn new_max_sized() -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Bitmap::max_sized())
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn load(width: u64, height: u64, data: Vec<u8>) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Bitmap::load(
|
||||
width as usize,
|
||||
height as usize,
|
||||
&data,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
|
||||
Self::internal_new(other.actual.read().unwrap().clone())
|
||||
}
|
||||
|
||||
pub fn set(&self, x: u64, y: u64, value: bool) {
|
||||
self.actual
|
||||
.write()
|
||||
.unwrap()
|
||||
.set(x as usize, y as usize, value)
|
||||
}
|
||||
|
||||
pub fn get(&self, x: u64, y: u64) -> bool {
|
||||
self.actual.read().unwrap().get(x as usize, y as usize)
|
||||
}
|
||||
|
||||
pub fn fill(&self, value: bool) {
|
||||
self.actual.write().unwrap().fill(value)
|
||||
}
|
||||
pub fn width(&self) -> u64 {
|
||||
self.actual.read().unwrap().width() as u64
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u64 {
|
||||
self.actual.read().unwrap().height() as u64
|
||||
}
|
||||
|
||||
pub fn equals(&self, other: &Bitmap) -> bool {
|
||||
let a = self.actual.read().unwrap();
|
||||
let b = other.actual.read().unwrap();
|
||||
*a == *b
|
||||
}
|
||||
|
||||
pub fn copy_raw(&self) -> Vec<u8> {
|
||||
self.actual.read().unwrap().data_ref().to_vec()
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct BitVec {
|
||||
pub(crate) actual: RwLock<servicepoint::BitVec>,
|
||||
}
|
||||
|
||||
impl BitVec {
|
||||
fn internal_new(actual: servicepoint::BitVec) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
actual: RwLock::new(actual),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl BitVec {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(size: u64) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::BitVec::repeat(false, size as usize))
|
||||
}
|
||||
#[uniffi::constructor]
|
||||
pub fn load(data: Vec<u8>) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::BitVec::from_slice(&data))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
|
||||
Self::internal_new(other.actual.read().unwrap().clone())
|
||||
}
|
||||
|
||||
pub fn set(&self, index: u64, value: bool) {
|
||||
self.actual.write().unwrap().set(index as usize, value)
|
||||
}
|
||||
|
||||
pub fn get(&self, index: u64) -> bool {
|
||||
self.actual
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(index as usize)
|
||||
.is_some_and(move |bit| *bit)
|
||||
}
|
||||
|
||||
pub fn fill(&self, value: bool) {
|
||||
self.actual.write().unwrap().fill(value)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u64 {
|
||||
self.actual.read().unwrap().len() as u64
|
||||
}
|
||||
|
||||
pub fn equals(&self, other: &BitVec) -> bool {
|
||||
let a = self.actual.read().unwrap();
|
||||
let b = other.actual.read().unwrap();
|
||||
*a == *b
|
||||
}
|
||||
|
||||
pub fn copy_raw(&self) -> Vec<u8> {
|
||||
self.actual.read().unwrap().clone().into_vec()
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
use servicepoint::{Brightness, DataRef, Grid};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct BrightnessGrid {
|
||||
pub(crate) actual: RwLock<servicepoint::BrightnessGrid>,
|
||||
}
|
||||
|
||||
impl BrightnessGrid {
|
||||
fn internal_new(actual: servicepoint::BrightnessGrid) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
actual: RwLock::new(actual),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl BrightnessGrid {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(width: u64, height: u64) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::BrightnessGrid::new(
|
||||
width as usize,
|
||||
height as usize,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn load(width: u64, height: u64, data: Vec<u8>) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::BrightnessGrid::saturating_load(
|
||||
width as usize,
|
||||
height as usize,
|
||||
&data,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
|
||||
Self::internal_new(other.actual.read().unwrap().clone())
|
||||
}
|
||||
|
||||
pub fn set(&self, x: u64, y: u64, value: u8) {
|
||||
self.actual.write().unwrap().set(
|
||||
x as usize,
|
||||
y as usize,
|
||||
Brightness::saturating_from(value),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get(&self, x: u64, y: u64) -> u8 {
|
||||
self.actual
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(x as usize, y as usize)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn fill(&self, value: u8) {
|
||||
self.actual
|
||||
.write()
|
||||
.unwrap()
|
||||
.fill(Brightness::saturating_from(value))
|
||||
}
|
||||
pub fn width(&self) -> u64 {
|
||||
self.actual.read().unwrap().width() as u64
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u64 {
|
||||
self.actual.read().unwrap().height() as u64
|
||||
}
|
||||
|
||||
pub fn equals(&self, other: &BrightnessGrid) -> bool {
|
||||
let a = self.actual.read().unwrap();
|
||||
let b = other.actual.read().unwrap();
|
||||
*a == *b
|
||||
}
|
||||
|
||||
pub fn copy_raw(&self) -> Vec<u8> {
|
||||
self.actual
|
||||
.read()
|
||||
.unwrap()
|
||||
.data_ref()
|
||||
.iter()
|
||||
.map(u8::from)
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
use crate::cp437_grid::Cp437Grid;
|
||||
use servicepoint::{Grid, SetValueSeriesError};
|
||||
use std::convert::Into;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct CharGrid {
|
||||
pub(crate) actual: RwLock<servicepoint::CharGrid>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Error, thiserror::Error, Debug)]
|
||||
pub enum CharGridError {
|
||||
#[error("Exactly one character was expected, but {value:?} was provided")]
|
||||
StringNotOneChar { value: String },
|
||||
#[error("The provided series was expected to have a length of {expected}, but was {actual}")]
|
||||
InvalidSeriesLength { actual: u64, expected: u64 },
|
||||
#[error("The index {index} was out of bounds for size {size}")]
|
||||
OutOfBounds { index: u64, size: u64 },
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl CharGrid {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(width: u64, height: u64) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::CharGrid::new(
|
||||
width as usize,
|
||||
height as usize,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn load(data: String) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::CharGrid::from(&*data))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
|
||||
Self::internal_new(other.actual.read().unwrap().clone())
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&self,
|
||||
x: u64,
|
||||
y: u64,
|
||||
value: String,
|
||||
) -> Result<(), CharGridError> {
|
||||
let value = Self::str_to_char(value)?;
|
||||
self.actual
|
||||
.write()
|
||||
.unwrap()
|
||||
.set(x as usize, y as usize, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, x: u64, y: u64) -> String {
|
||||
self.actual
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(x as usize, y as usize)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn fill(&self, value: String) -> Result<(), CharGridError> {
|
||||
let value = Self::str_to_char(value)?;
|
||||
self.actual.write().unwrap().fill(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u64 {
|
||||
self.actual.read().unwrap().width() as u64
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u64 {
|
||||
self.actual.read().unwrap().height() as u64
|
||||
}
|
||||
|
||||
pub fn equals(&self, other: &CharGrid) -> bool {
|
||||
let a = self.actual.read().unwrap();
|
||||
let b = other.actual.read().unwrap();
|
||||
*a == *b
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> String {
|
||||
let grid = self.actual.read().unwrap();
|
||||
String::from(&*grid)
|
||||
}
|
||||
|
||||
pub fn set_row(&self, y: u64, row: String) -> Result<(), CharGridError> {
|
||||
self.actual
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_row(y as usize, &row.chars().collect::<Vec<_>>())
|
||||
.map_err(CharGridError::from)
|
||||
}
|
||||
|
||||
pub fn set_col(&self, x: u64, col: String) -> Result<(), CharGridError> {
|
||||
self.actual
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_row(x as usize, &col.chars().collect::<Vec<_>>())
|
||||
.map_err(CharGridError::from)
|
||||
}
|
||||
|
||||
pub fn get_row(&self, y: u64) -> Result<String, CharGridError> {
|
||||
self.actual
|
||||
.read()
|
||||
.unwrap()
|
||||
.get_row(y as usize)
|
||||
.map(String::from_iter)
|
||||
.ok_or(CharGridError::OutOfBounds {
|
||||
index: y,
|
||||
size: self.height(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_col(&self, x: u64) -> Result<String, CharGridError> {
|
||||
self.actual
|
||||
.read()
|
||||
.unwrap()
|
||||
.get_col(x as usize)
|
||||
.map(String::from_iter)
|
||||
.ok_or(CharGridError::OutOfBounds {
|
||||
index: x,
|
||||
size: self.width(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_cp437(&self) -> Arc<Cp437Grid> {
|
||||
Cp437Grid::internal_new(servicepoint::Cp437Grid::from(
|
||||
&*self.actual.read().unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl CharGrid {
|
||||
pub(crate) fn internal_new(actual: servicepoint::CharGrid) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
actual: RwLock::new(actual),
|
||||
})
|
||||
}
|
||||
|
||||
fn str_to_char(value: String) -> Result<char, CharGridError> {
|
||||
if value.len() != 1 {
|
||||
return Err(CharGridError::StringNotOneChar { value });
|
||||
}
|
||||
|
||||
let value = value.chars().nth(0).unwrap();
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetValueSeriesError> for CharGridError {
|
||||
fn from(e: SetValueSeriesError) -> Self {
|
||||
match e {
|
||||
SetValueSeriesError::OutOfBounds { index, size } => {
|
||||
CharGridError::OutOfBounds {
|
||||
index: index as u64,
|
||||
size: size as u64,
|
||||
}
|
||||
}
|
||||
SetValueSeriesError::InvalidLength { actual, expected } => {
|
||||
CharGridError::InvalidSeriesLength {
|
||||
actual: actual as u64,
|
||||
expected: expected as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
use crate::bitmap::Bitmap;
|
||||
use crate::bitvec::BitVec;
|
||||
use crate::brightness_grid::BrightnessGrid;
|
||||
use crate::char_grid::CharGrid;
|
||||
use crate::compression_code::CompressionCode;
|
||||
use crate::cp437_grid::Cp437Grid;
|
||||
use crate::errors::ServicePointError;
|
||||
use servicepoint::Origin;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Command {
|
||||
pub(crate) actual: servicepoint::Command,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
fn internal_new(actual: servicepoint::Command) -> Arc<Command> {
|
||||
Arc::new(Command { actual })
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Command {
|
||||
#[uniffi::constructor]
|
||||
pub fn clear() -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Command::Clear)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn brightness(brightness: u8) -> Result<Arc<Self>, ServicePointError> {
|
||||
servicepoint::Brightness::try_from(brightness)
|
||||
.map_err(move |value| ServicePointError::InvalidBrightness {
|
||||
value,
|
||||
})
|
||||
.map(servicepoint::Command::Brightness)
|
||||
.map(Self::internal_new)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn fade_out() -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Command::FadeOut)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn hard_reset() -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Command::HardReset)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn bitmap_linear_win(
|
||||
offset_x: u64,
|
||||
offset_y: u64,
|
||||
bitmap: &Arc<Bitmap>,
|
||||
compression: CompressionCode,
|
||||
) -> Arc<Self> {
|
||||
let origin = Origin::new(offset_x as usize, offset_y as usize);
|
||||
let bitmap = bitmap.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::BitmapLinearWin(
|
||||
origin,
|
||||
bitmap,
|
||||
servicepoint::CompressionCode::try_from(compression as u16)
|
||||
.unwrap(),
|
||||
);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn char_brightness(
|
||||
offset_x: u64,
|
||||
offset_y: u64,
|
||||
grid: &Arc<BrightnessGrid>,
|
||||
) -> Arc<Self> {
|
||||
let origin = Origin::new(offset_x as usize, offset_y as usize);
|
||||
let grid = grid.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::CharBrightness(origin, grid);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn bitmap_linear(
|
||||
offset: u64,
|
||||
bitmap: &Arc<BitVec>,
|
||||
compression: CompressionCode,
|
||||
) -> Arc<Self> {
|
||||
let bitmap = bitmap.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::BitmapLinear(
|
||||
offset as usize,
|
||||
bitmap,
|
||||
servicepoint::CompressionCode::try_from(compression as u16)
|
||||
.unwrap(),
|
||||
);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn bitmap_linear_and(
|
||||
offset: u64,
|
||||
bitmap: &Arc<BitVec>,
|
||||
compression: CompressionCode,
|
||||
) -> Arc<Self> {
|
||||
let bitmap = bitmap.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::BitmapLinearAnd(
|
||||
offset as usize,
|
||||
bitmap,
|
||||
servicepoint::CompressionCode::try_from(compression as u16)
|
||||
.unwrap(),
|
||||
);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn bitmap_linear_or(
|
||||
offset: u64,
|
||||
bitmap: &Arc<BitVec>,
|
||||
compression: CompressionCode,
|
||||
) -> Arc<Self> {
|
||||
let bitmap = bitmap.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::BitmapLinearOr(
|
||||
offset as usize,
|
||||
bitmap,
|
||||
servicepoint::CompressionCode::try_from(compression as u16)
|
||||
.unwrap(),
|
||||
);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn bitmap_linear_xor(
|
||||
offset: u64,
|
||||
bitmap: &Arc<BitVec>,
|
||||
compression: CompressionCode,
|
||||
) -> Arc<Self> {
|
||||
let bitmap = bitmap.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::BitmapLinearXor(
|
||||
offset as usize,
|
||||
bitmap,
|
||||
servicepoint::CompressionCode::try_from(compression as u16)
|
||||
.unwrap(),
|
||||
);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn cp437_data(
|
||||
offset_x: u64,
|
||||
offset_y: u64,
|
||||
grid: &Arc<Cp437Grid>,
|
||||
) -> Arc<Self> {
|
||||
let origin = Origin::new(offset_x as usize, offset_y as usize);
|
||||
let grid = grid.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::Cp437Data(origin, grid);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn utf8_data(
|
||||
offset_x: u64,
|
||||
offset_y: u64,
|
||||
grid: &Arc<CharGrid>,
|
||||
) -> Arc<Self> {
|
||||
let origin = Origin::new(offset_x as usize, offset_y as usize);
|
||||
let grid = grid.actual.read().unwrap().clone();
|
||||
let actual = servicepoint::Command::Utf8Data(origin, grid);
|
||||
Self::internal_new(actual)
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
|
||||
Self::internal_new(other.actual.clone())
|
||||
}
|
||||
|
||||
pub fn equals(&self, other: &Command) -> bool {
|
||||
self.actual == other.actual
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, uniffi::Enum)]
|
||||
pub enum CompressionCode {
|
||||
/// no compression
|
||||
Uncompressed = 0x0,
|
||||
/// compress using flate2 with zlib header
|
||||
Zlib = 0x677a,
|
||||
/// compress using bzip2
|
||||
Bzip2 = 0x627a,
|
||||
/// compress using lzma
|
||||
Lzma = 0x6c7a,
|
||||
/// compress using Zstandard
|
||||
Zstd = 0x7a73,
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::command::Command;
|
||||
use crate::errors::ServicePointError;
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Connection {
|
||||
actual: servicepoint::Connection,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Connection {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(host: String) -> Result<Arc<Self>, ServicePointError> {
|
||||
servicepoint::Connection::open(host)
|
||||
.map(|actual| Arc::new(Connection { actual }))
|
||||
.map_err(|err| ServicePointError::IoError {
|
||||
error: err.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn new_fake() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
actual: servicepoint::Connection::Fake,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&self, command: Arc<Command>) -> Result<(), ServicePointError> {
|
||||
self.actual.send(command.actual.clone()).map_err(|err| {
|
||||
ServicePointError::IoError {
|
||||
error: format!("{err:?}"),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#[derive(
|
||||
Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, uniffi::Record,
|
||||
)]
|
||||
pub struct Constants {
|
||||
pub tile_size: u64,
|
||||
pub tile_width: u64,
|
||||
pub tile_height: u64,
|
||||
pub pixel_width: u64,
|
||||
pub pixel_height: u64,
|
||||
pub pixel_count: u64,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn get_constants() -> Constants {
|
||||
Constants {
|
||||
tile_size: servicepoint::TILE_SIZE as u64,
|
||||
tile_width: servicepoint::TILE_WIDTH as u64,
|
||||
tile_height: servicepoint::TILE_HEIGHT as u64,
|
||||
pixel_width: servicepoint::PIXEL_WIDTH as u64,
|
||||
pixel_height: servicepoint::PIXEL_HEIGHT as u64,
|
||||
pixel_count: servicepoint::PIXEL_COUNT as u64,
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
use crate::char_grid::CharGrid;
|
||||
use servicepoint::{DataRef, Grid};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Cp437Grid {
|
||||
pub(crate) actual: RwLock<servicepoint::Cp437Grid>,
|
||||
}
|
||||
|
||||
impl Cp437Grid {
|
||||
pub(crate) fn internal_new(actual: servicepoint::Cp437Grid) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
actual: RwLock::new(actual),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Cp437Grid {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(width: u64, height: u64) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Cp437Grid::new(
|
||||
width as usize,
|
||||
height as usize,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn load(width: u64, height: u64, data: Vec<u8>) -> Arc<Self> {
|
||||
Self::internal_new(servicepoint::Cp437Grid::load(
|
||||
width as usize,
|
||||
height as usize,
|
||||
&data,
|
||||
))
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
|
||||
Self::internal_new(other.actual.read().unwrap().clone())
|
||||
}
|
||||
|
||||
pub fn set(&self, x: u64, y: u64, value: u8) {
|
||||
self.actual
|
||||
.write()
|
||||
.unwrap()
|
||||
.set(x as usize, y as usize, value)
|
||||
}
|
||||
|
||||
pub fn get(&self, x: u64, y: u64) -> u8 {
|
||||
self.actual.read().unwrap().get(x as usize, y as usize)
|
||||
}
|
||||
|
||||
pub fn fill(&self, value: u8) {
|
||||
self.actual.write().unwrap().fill(value)
|
||||
}
|
||||
pub fn width(&self) -> u64 {
|
||||
self.actual.read().unwrap().width() as u64
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u64 {
|
||||
self.actual.read().unwrap().height() as u64
|
||||
}
|
||||
|
||||
pub fn equals(&self, other: &Cp437Grid) -> bool {
|
||||
let a = self.actual.read().unwrap();
|
||||
let b = other.actual.read().unwrap();
|
||||
*a == *b
|
||||
}
|
||||
|
||||
pub fn copy_raw(&self) -> Vec<u8> {
|
||||
self.actual.read().unwrap().data_ref().to_vec()
|
||||
}
|
||||
|
||||
pub fn to_utf8(&self) -> Arc<CharGrid> {
|
||||
CharGrid::internal_new(servicepoint::CharGrid::from(
|
||||
&*self.actual.read().unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#[derive(uniffi::Error, thiserror::Error, Debug)]
|
||||
pub enum ServicePointError {
|
||||
#[error("An IO error occurred: {error}")]
|
||||
IoError { error: String },
|
||||
#[error("The specified brightness value {value} is out of range")]
|
||||
InvalidBrightness { value: u8 },
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
uniffi::setup_scaffolding!();
|
||||
|
||||
mod bitmap;
|
||||
mod bitvec;
|
||||
mod brightness_grid;
|
||||
mod char_grid;
|
||||
mod command;
|
||||
mod compression_code;
|
||||
mod connection;
|
||||
mod constants;
|
||||
mod cp437_grid;
|
||||
mod errors;
|
|
@ -1,3 +0,0 @@
|
|||
[bindings.csharp]
|
||||
namespace = "ServicePoint"
|
||||
access_modifier = "public"
|
6
example/example.sh
Executable file
6
example/example.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
|
||||
LD_LIBRARY_PATH="$SCRIPT_PATH/../target/release:$LD_LIBRARY_PATH" ruby "$SCRIPT_PATH/example.rb"
|
21
flake.lock
21
flake.lock
|
@ -1,25 +1,5 @@
|
|||
{
|
||||
"nodes": {
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1736429655,
|
||||
"narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739357830,
|
||||
|
@ -38,7 +18,6 @@
|
|||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
|
|
104
flake.nix
104
flake.nix
|
@ -3,17 +3,12 @@
|
|||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
||||
naersk = {
|
||||
url = "github:nix-community/naersk";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{
|
||||
self,
|
||||
nixpkgs,
|
||||
naersk,
|
||||
}:
|
||||
let
|
||||
lib = nixpkgs.lib;
|
||||
|
@ -33,101 +28,13 @@
|
|||
}
|
||||
);
|
||||
in
|
||||
rec {
|
||||
packages = forAllSystems (
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
makeWrapper
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
xe
|
||||
xz
|
||||
];
|
||||
makeExample =
|
||||
{
|
||||
package,
|
||||
example,
|
||||
features ? "",
|
||||
}:
|
||||
naersk'.buildPackage {
|
||||
pname = example;
|
||||
cargoBuildOptions =
|
||||
x:
|
||||
x
|
||||
++ [
|
||||
"--package"
|
||||
package
|
||||
];
|
||||
src = ./.;
|
||||
inherit nativeBuildInputs buildInputs;
|
||||
strictDeps = true;
|
||||
gitSubmodules = true;
|
||||
overrideMain = old: {
|
||||
preConfigure = ''
|
||||
cargo_build_options="$cargo_build_options --example ${example} ${
|
||||
if features == "" then "" else "--features " + features
|
||||
}"
|
||||
'';
|
||||
};
|
||||
};
|
||||
makePackage =
|
||||
package:
|
||||
let
|
||||
package-param = [
|
||||
"--package"
|
||||
package
|
||||
];
|
||||
in
|
||||
naersk'.buildPackage {
|
||||
pname = package;
|
||||
cargoBuildOptions = x: x ++ package-param;
|
||||
cargoTestOptions = x: x ++ package-param;
|
||||
src = ./.;
|
||||
doCheck = true;
|
||||
strictDeps = true;
|
||||
inherit nativeBuildInputs buildInputs;
|
||||
};
|
||||
in
|
||||
rec {
|
||||
servicepoint = makePackage "servicepoint";
|
||||
announce = makeExample {
|
||||
package = "servicepoint";
|
||||
example = "announce";
|
||||
};
|
||||
game-of-life = makeExample {
|
||||
package = "servicepoint";
|
||||
example = "game_of_life";
|
||||
features = "rand";
|
||||
};
|
||||
moving-line = makeExample {
|
||||
package = "servicepoint";
|
||||
example = "moving_line";
|
||||
};
|
||||
random-brightness = makeExample {
|
||||
package = "servicepoint";
|
||||
example = "random_brightness";
|
||||
features = "rand";
|
||||
};
|
||||
wiping-clear = makeExample {
|
||||
package = "servicepoint";
|
||||
example = "wiping_clear";
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
legacyPackages = packages;
|
||||
|
||||
devShells = forAllSystems (
|
||||
{ pkgs, system }:
|
||||
{
|
||||
default = pkgs.mkShell rec {
|
||||
inputsFrom = [ self.packages.${system}.servicepoint ];
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
(pkgs.symlinkJoin
|
||||
{
|
||||
(pkgs.symlinkJoin {
|
||||
name = "rust-toolchain";
|
||||
paths = with pkgs; [
|
||||
rustc
|
||||
|
@ -140,11 +47,10 @@
|
|||
];
|
||||
})
|
||||
ruby
|
||||
dotnet-sdk_8
|
||||
gcc
|
||||
gnumake
|
||||
xe
|
||||
xz
|
||||
pkg-config
|
||||
];
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
};
|
||||
}
|
||||
|
|
14
generate-binding.sh
Executable file
14
generate-binding.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
|
||||
cd "$SCRIPT_PATH"
|
||||
cargo build --release
|
||||
|
||||
TARGET_PATH="$(realpath "$SCRIPT_PATH"/target/release)"
|
||||
SERVICEPOINT_SO="$TARGET_PATH/libservicepoint_binding_uniffi.so"
|
||||
|
||||
echo "Source: $SERVICEPOINT_SO"
|
||||
|
||||
"$TARGET_PATH/uniffi-bindgen" generate --library "$SERVICEPOINT_SO" --language ruby --out-dir "$SCRIPT_PATH/lib"
|
1
servicepoint-binding-uniffi
Submodule
1
servicepoint-binding-uniffi
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 1169d9f1d294268f699e9c4e088cb1d97d6c36ce
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue