This commit is contained in:
parent
2f7a2dfd62
commit
774f342f00
103 changed files with 101 additions and 14569 deletions
|
|
@ -1 +0,0 @@
|
|||
use flake
|
||||
11
.github/workflows/rust.yml
vendored
11
.github/workflows/rust.yml
vendored
|
|
@ -32,14 +32,3 @@ jobs:
|
|||
|
||||
- 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());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue