This commit is contained in:
		
							parent
							
								
									2f7a2dfd62
								
							
						
					
					
						commit
						b87200cbee
					
				
					 103 changed files with 111 additions and 14572 deletions
				
			
		|  | @ -1 +0,0 @@ | ||||||
| use flake |  | ||||||
							
								
								
									
										21
									
								
								.github/workflows/rust.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/rust.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -25,21 +25,14 @@ jobs: | ||||||
|       - name: Install rust toolchain |       - name: Install rust toolchain | ||||||
|         run: sudo apt-get install -qy cargo rust-clippy |         run: sudo apt-get install -qy cargo rust-clippy | ||||||
|       - name: install lzma |       - 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 |       - name: generate bindings | ||||||
|         run: cargo clippy --all-targets --all-features |         run: ./generate-binding.sh | ||||||
|  |       - name: check that generated files did not change | ||||||
|  |         run: output=$(git status --porcelain) && [ -z "$output" ] | ||||||
|          |          | ||||||
|       - name: build default features |       - name: build default features | ||||||
|         run: cargo build --all --verbose |         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] | [workspace] | ||||||
| resolver = "2" | resolver = "2" | ||||||
| members = [ | members = [ | ||||||
|     "crates/servicepoint", |     "uniffi-bindgen", | ||||||
|     "crates/servicepoint_binding_c", |     "servicepoint-binding-uniffi" | ||||||
|     "crates/servicepoint_binding_c/examples/lang_c", |  | ||||||
|     "crates/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 | # 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 | 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". | 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).  | 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). | ||||||
| The [GitHub repository](https://github.com/cccb/servicepoint) remains available as a mirror. |  | ||||||
| 
 | 
 | ||||||
| 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                                                                      | | ## Note on stability | ||||||
| |-----------------------------|-----------------------------------|-----------------------------------------------------------------------------| |  | ||||||
| | 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) | |  | ||||||
| 
 | 
 | ||||||
| ## 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) | ## Notes on differences to rust library | ||||||
| - 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) |  | ||||||
| 
 | 
 | ||||||
| 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). | Run `generate-binding.sh` to regenerate all bindings. This will also build `libservicepoint.so` (or equivalent on your | ||||||
| 
 | platform). | ||||||
| ## What happened to servicepoint2? |  | ||||||
| 
 |  | ||||||
| After `servicepoint2` has been merged into `servicepoint`, `servicepoint2` will not continue to get any updates. |  | ||||||
|  |  | ||||||
|  | @ -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 +0,0 @@ | ||||||
| 
 |  | ||||||
|  | @ -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
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							|  | @ -1,25 +1,5 @@ | ||||||
| { | { | ||||||
|   "nodes": { |   "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": { |     "nixpkgs": { | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1739357830, |         "lastModified": 1739357830, | ||||||
|  | @ -38,7 +18,6 @@ | ||||||
|     }, |     }, | ||||||
|     "root": { |     "root": { | ||||||
|       "inputs": { |       "inputs": { | ||||||
|         "naersk": "naersk", |  | ||||||
|         "nixpkgs": "nixpkgs" |         "nixpkgs": "nixpkgs" | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										106
									
								
								flake.nix
									
										
									
									
									
								
							
							
						
						
									
										106
									
								
								flake.nix
									
										
									
									
									
								
							|  | @ -3,17 +3,12 @@ | ||||||
| 
 | 
 | ||||||
|   inputs = { |   inputs = { | ||||||
|     nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; |     nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; | ||||||
|     naersk = { |  | ||||||
|       url = "github:nix-community/naersk"; |  | ||||||
|       inputs.nixpkgs.follows = "nixpkgs"; |  | ||||||
|     }; |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   outputs = |   outputs = | ||||||
|     inputs@{ |     inputs@{ | ||||||
|       self, |       self, | ||||||
|       nixpkgs, |       nixpkgs, | ||||||
|       naersk, |  | ||||||
|     }: |     }: | ||||||
|     let |     let | ||||||
|       lib = nixpkgs.lib; |       lib = nixpkgs.lib; | ||||||
|  | @ -33,101 +28,13 @@ | ||||||
|           } |           } | ||||||
|         ); |         ); | ||||||
|     in |     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 ( |       devShells = forAllSystems ( | ||||||
|         { pkgs, system }: |         { pkgs, system }: | ||||||
|         { |         { | ||||||
|           default = pkgs.mkShell rec { |           default = pkgs.mkShell { | ||||||
|             inputsFrom = [ self.packages.${system}.servicepoint ]; |  | ||||||
|             packages = with pkgs; [ |             packages = with pkgs; [ | ||||||
|               (pkgs.symlinkJoin |               (pkgs.symlinkJoin { | ||||||
|               { |  | ||||||
|                 name = "rust-toolchain"; |                 name = "rust-toolchain"; | ||||||
|                 paths = with pkgs; [ |                 paths = with pkgs; [ | ||||||
|                   rustc |                   rustc | ||||||
|  | @ -140,11 +47,10 @@ | ||||||
|                 ]; |                 ]; | ||||||
|               }) |               }) | ||||||
|               ruby |               ruby | ||||||
|               dotnet-sdk_8 |               xe | ||||||
|               gcc |               xz | ||||||
|               gnumake |               pkg-config | ||||||
|             ]; |             ]; | ||||||
|             LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; |  | ||||||
|             RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; |             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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter