Compare commits

...

124 commits

Author SHA1 Message Date
vinzenz 04f8884a14 Merge pull request 'update version to 0.15.2' (#13) from version into main
All checks were successful
Rust / build (push) Successful in 1m59s
Reviewed-on: #13
2025-07-03 19:25:17 +02:00
Vinzenz Schroeter a07a52267d update version to 0.15.2
All checks were successful
Rust / build (pull_request) Successful in 1m59s
2025-07-03 19:22:28 +02:00
vinzenz 78dfc74cf2 Merge pull request 'usize only grid, fix header only command parsing' (#12) from next into main
All checks were successful
Rust / build (push) Successful in 1m58s
Reviewed-on: #12
2025-07-03 19:20:58 +02:00
Vinzenz Schroeter 7d63abe3f8 fix UnexpectedPayloadSize { actual: 0, expected: 0 } when loading a command only packet
All checks were successful
Rust / build (pull_request) Successful in 1m58s
2025-07-03 19:17:55 +02:00
Vinzenz Schroeter eff700620d usize only grid 2025-07-03 19:17:55 +02:00
vinzenz 1fb0daeafb Merge pull request 'derive(Hash), make Grid inherent' (#11) from next into main
All checks were successful
Rust / build (push) Successful in 1m54s
Reviewed-on: #11
2025-06-27 17:59:18 +02:00
Vinzenz Schroeter 68233a99e4 update dependencies and flake
All checks were successful
Rust / build (pull_request) Successful in 1m57s
2025-06-27 17:57:02 +02:00
Vinzenz Schroeter 2bf8ef1f4e version 0.15.1
All checks were successful
Rust / build (pull_request) Successful in 1m57s
2025-06-27 17:52:25 +02:00
Vinzenz Schroeter 163090c980 fix doc links, make some Grid inherent 2025-06-27 17:52:24 +02:00
Vinzenz Schroeter 54cd538a7a derive Hash on all commands 2025-06-27 17:52:24 +02:00
vinzenz 1b19d04701 Merge pull request 'next' (#6) from next into main
All checks were successful
Rust / build (push) Successful in 1m55s
Reviewed-on: #6
2025-05-24 13:34:38 +02:00
Vinzenz Schroeter 020c75c487 fix tests fail depending on features
All checks were successful
Rust / build (pull_request) Successful in 1m54s
2025-05-24 13:31:30 +02:00
Vinzenz Schroeter c80449777d add tests for round tripping a reference
Some checks failed
Rust / build (pull_request) Failing after 1m17s
2025-05-24 10:39:31 +02:00
Vinzenz Schroeter 97559fe8b0 more conversion functions for references
Some checks failed
Rust / build (pull_request) Failing after 1m17s
mostly with fewer copies
2025-05-24 10:22:55 +02:00
Vinzenz Schroeter 68ee892795 add tests, suppress false positive warnings
Some checks failed
Rust / build (pull_request) Failing after 1m28s
2025-05-22 18:35:12 +02:00
Vinzenz Schroeter 5b4ba29c4c update dependencies
Some checks failed
Rust / build (pull_request) Failing after 1m45s
2025-05-21 23:32:10 +02:00
Vinzenz Schroeter 5891379880 update random, bump version 2025-05-21 23:32:10 +02:00
Vinzenz Schroeter 2ac305bf88 clippy fixes
Some checks failed
Rust / build (pull_request) Failing after 5m10s
2025-05-21 23:02:01 +02:00
Vinzenz Schroeter df3d54624a make small commands : Copy 2025-05-21 21:24:02 +02:00
Vinzenz Schroeter 5d3aec596d compress based on reference 2025-05-21 21:21:49 +02:00
Vinzenz Schroeter ffb1d23554 finish fewer_copies example 2025-05-21 20:15:02 +02:00
Vinzenz Schroeter cd6d25bde8 wip make it possible to send with fewer copies 2025-05-16 00:48:18 +02:00
Vinzenz Schroeter 0cded79c56 accept inexact sizes
Some checks failed
Rust / build (pull_request) Failing after 1m6s
2025-05-11 16:46:49 +02:00
Vinzenz Schroeter d979d46d3e make BinaryOperation: Copy
Some checks failed
Rust / build (pull_request) Failing after 1m1s
2025-05-05 18:42:53 +02:00
Vinzenz Schroeter 4f397a8a88 add example for own command 2025-05-05 18:42:39 +02:00
Vinzenz Schroeter 5188fee641 make packet payload optional 2025-05-04 13:05:03 +02:00
vinzenz 7f41875c2a Merge pull request 'fixes for 0.14.0' (#4) from next into main
All checks were successful
Rust / build (push) Successful in 1m56s
Reviewed-on: #4
2025-05-04 12:02:22 +02:00
Vinzenz Schroeter 66930db97f version 0.14.1
All checks were successful
Rust / build (pull_request) Successful in 1m53s
2025-05-04 11:43:04 +02:00
Vinzenz Schroeter 71dcaf5392 fix display now stops UTF-8 strings at first null byte, replace \0 with space
All checks were successful
Rust / build (pull_request) Successful in 1m53s
2025-05-03 20:51:30 +02:00
Vinzenz Schroeter e5ec598f13 fix examples use UdpSocket::bind instead of bind_connect
All checks were successful
Rust / build (pull_request) Successful in 1m53s
2025-05-03 11:44:15 +02:00
vinzenz e509e067dd Merge pull request 'make everything a trait, dont panic' (#3) from next into main
All checks were successful
Rust / build (push) Successful in 1m54s
Reviewed-on: #3
2025-05-03 11:15:52 +02:00
Vinzenz Schroeter 663adde30a update dependencies
All checks were successful
Rust / build (pull_request) Successful in 1m52s
2025-05-03 11:08:17 +02:00
Vinzenz Schroeter 4984197d95 rename BitVecU8Msb0 to DisplayBitVec 2025-05-03 11:08:17 +02:00
Vinzenz Schroeter 8e47d3c966 update README 2025-05-03 11:08:17 +02:00
Vinzenz Schroeter b08701c342 remove SendCommandExt 2025-05-03 10:54:19 +02:00
Vinzenz Schroeter 473bbbc3f9 improve compression error logging 2025-05-03 09:35:48 +02:00
Vinzenz Schroeter 8ddbaeaaa6 v0.14.0
All checks were successful
Rust / build (pull_request) Successful in 2m6s
2025-05-01 19:55:39 +02:00
Vinzenz Schroeter de8a1a2fe8 remove connections
All checks were successful
Rust / build (pull_request) Successful in 2m6s
2025-05-01 19:43:32 +02:00
Vinzenz Schroeter 114385868a make CommandCode pub
All checks were successful
Rust / build (pull_request) Successful in 2m19s
2025-04-12 21:27:15 +02:00
Vinzenz Schroeter cecccb3f28 make BinaryOperation repr(u8)
All checks were successful
Rust / build (pull_request) Successful in 2m20s
2025-04-12 18:17:32 +02:00
Vinzenz Schroeter 4e3d1ce208 copy packet to slice 2025-04-12 18:17:32 +02:00
Vinzenz Schroeter e985140417 make packet repr(C)
Some checks failed
Rust / build (pull_request) Failing after 1m0s
2025-04-12 11:20:44 +02:00
Vinzenz Schroeter ff56215c06 rename bitvec to fix cbindgen duplicate name
Some checks failed
Rust / build (pull_request) Failing after 1m1s
2025-04-12 10:18:20 +02:00
Vinzenz Schroeter 75d24f6587 see post for details
All checks were successful
Rust / build (pull_request) Successful in 2m23s
2025-04-10 20:14:50 +02:00
Vinzenz Schroeter 62d1666ec2 update flake
All checks were successful
Rust / build (pull_request) Successful in 2m17s
2025-04-06 11:43:15 +02:00
Vinzenz Schroeter b6f5f74fa4 fix warnings, add instructions to get smallest possible binary 2025-04-06 11:40:15 +02:00
Vinzenz Schroeter fe67160974 fix test
All checks were successful
Rust / build (pull_request) Successful in 2m17s
2025-03-27 18:49:42 +01:00
Vinzenz Schroeter 739675a9f5 rename BrightnessCommand to GlobalBrightnessCommand, name fields in error struct
Some checks failed
Rust / build (pull_request) Failing after 1m17s
2025-03-27 18:43:09 +01:00
Vinzenz Schroeter b3bf57301a add unit tests for into command 2025-03-27 18:13:20 +01:00
Vinzenz Schroeter 1cf37413e6 implement From<X> for XCommand
All checks were successful
Rust / build (pull_request) Successful in 2m15s
2025-03-25 22:20:44 +01:00
Vinzenz Schroeter cbab86bd93 fix broken example
All checks were successful
Rust / build (pull_request) Successful in 2m15s
2025-03-25 21:50:50 +01:00
Vinzenz Schroeter b69e7df635 fix warning when no compression is used
Some checks failed
Rust / build (pull_request) Failing after 2m13s
2025-03-25 21:46:18 +01:00
Vinzenz Schroeter fe1aa3ebd1 do not use () as Err, clean up error handling
Some checks failed
Rust / build (pull_request) Failing after 1m15s
2025-03-25 21:42:17 +01:00
Vinzenz Schroeter 373d0efe55 remove TryFrom<&Origin<Pixels>> for Origin<Tiles> 2025-03-25 21:38:36 +01:00
Vinzenz Schroeter eba1a6a6be this one is different depending on the rust version
Some checks failed
Rust / build (pull_request) Failing after 1m15s
2025-03-25 20:35:41 +01:00
Vinzenz Schroeter 617c37c713 do not run clippy on examples and tests in CI
Some checks failed
Rust / build (pull_request) Failing after 59s
2025-03-25 20:24:01 +01:00
Vinzenz Schroeter ff886ca27d more clippy fixes and/or whitelists
Some checks failed
Rust / build (pull_request) Failing after 1m5s
2025-03-25 20:17:01 +01:00
Vinzenz Schroeter 61f83a7042 more clippy fixes and/or whitelists 2025-03-25 20:01:04 +01:00
Vinzenz Schroeter 9f239ec71d more clippy fixes and/or whitelists
Some checks failed
Rust / build (pull_request) Failing after 1m6s
2025-03-25 19:55:15 +01:00
Vinzenz Schroeter 5ba01ec4cc cargo clippy --fix 2025-03-25 19:20:55 +01:00
Vinzenz Schroeter 3384cc4ee9 even stricter lints, first fixes 2025-03-25 19:18:21 +01:00
Vinzenz Schroeter 2c3d31f649 remove redundant CP437 ZST 2025-03-25 19:11:39 +01:00
Vinzenz Schroeter fbd42d7c47 add script to generate coverage report 2025-03-25 18:55:17 +01:00
Vinzenz Schroeter 05ab631eb6 make Origin::ZERO the Default::default()
Some checks failed
Rust / build (pull_request) Failing after 1m7s
2025-03-25 18:48:23 +01:00
Vinzenz Schroeter bf2b320c81 add missing docs 2025-03-25 18:47:53 +01:00
Vinzenz Schroeter 2d72ee05a7 add more must_use annotations 2025-03-25 18:42:38 +01:00
Vinzenz Schroeter 5e38ced392 reorder fields by importance 2025-03-25 18:11:31 +01:00
Vinzenz Schroeter 44fe6961e7 into packet can fail 2025-03-21 14:56:31 +01:00
Vinzenz Schroeter 08ed6a6fee add a bunch of lints and change more panics to result/option
Some checks failed
Rust / build (pull_request) Failing after 1m7s
2025-03-12 22:45:29 +01:00
Vinzenz Schroeter 4ccbd57ba8 add missing docs, clippy
Some checks failed
Rust / build (pull_request) Failing after 1m5s
2025-03-08 18:23:05 +01:00
Vinzenz Schroeter 28f0bd5903 add tests, fix bug
Some checks failed
Rust / build (pull_request) Failing after 1m4s
2025-03-08 18:13:51 +01:00
Vinzenz Schroeter 18db901fb5 do not panic in ValueGrid 2025-03-08 17:50:22 +01:00
Vinzenz Schroeter b178b48834 add tests 2025-03-08 17:37:13 +01:00
Vinzenz Schroeter 7cd26cd50e move tests to the module they test 2025-03-08 17:09:26 +01:00
Vinzenz Schroeter c8a38870d1 brightness to command not packet, move docs, clippy 2025-03-08 14:26:14 +01:00
Vinzenz Schroeter d6229ece87 adjust mod structure
Some checks failed
Rust / build (pull_request) Failing after 1m4s
2025-03-08 12:09:07 +01:00
Vinzenz Schroeter 03f84c337f adjust tests for higher coverage 2025-03-08 11:56:49 +01:00
Vinzenz Schroeter 2ff49aaf7a move TypedCommand to own mod 2025-03-08 11:47:25 +01:00
Vinzenz Schroeter 159abd36d9 fix docs
Some checks failed
Rust / build (pull_request) Failing after 1m3s
2025-03-08 11:41:56 +01:00
Vinzenz Schroeter 8022b65991 move containers into own mod 2025-03-08 11:32:12 +01:00
Vinzenz Schroeter e3fc56c200 rename commands, add suffix and export on top level 2025-03-08 11:25:29 +01:00
Vinzenz Schroeter 9bff9bd346 do not panic if bitmap has invalid parameters 2025-03-08 10:06:25 +01:00
Vinzenz Schroeter 427dd93088 merge BitmapLinear-Commands
Some checks failed
Rust / build (pull_request) Failing after 1m5s
2025-03-08 00:39:08 +01:00
Vinzenz Schroeter d195f6100a rename modules 2025-03-08 00:38:10 +01:00
Vinzenz Schroeter c66e6db498 Command is now a trait
Some checks failed
Rust / build (pull_request) Failing after 1m4s
2025-03-07 22:51:32 +01:00
Vinzenz Schroeter b691ef33f8 Connection is now a trait 2025-03-06 23:50:08 +01:00
Vinzenz Schroeter 111f35b242 fix panic message 2025-03-02 13:28:23 +01:00
Vinzenz Schroeter 59137b6357 version 0.13.2
All checks were successful
Rust / build (push) Successful in 2m10s
2025-02-17 22:42:55 +01:00
vinzenz de9a09e171 Merge pull request 'split language bindings into separate repositories' (#2) from split into main
Some checks failed
Rust / build (push) Has been cancelled
Reviewed-on: #2
2025-02-17 22:42:11 +01:00
Vinzenz Schroeter 6a19c5bc6c update README
All checks were successful
Rust / build (pull_request) Successful in 2m11s
2025-02-17 22:37:48 +01:00
Vinzenz Schroeter a1316b0271 impl Default for CompressionCode
also more feature-agnostic examples
2025-02-17 22:32:45 +01:00
Vinzenz Schroeter c534929089 format code 2025-02-17 21:35:05 +01:00
Vinzenz Schroeter 319ef4572a move all of cp437 feature code into one unit 2025-02-17 21:35:05 +01:00
Vinzenz Schroeter 62a9969cb5 CI check --no-default-features works 2025-02-17 21:35:05 +01:00
Vinzenz Schroeter 0604609fdf update dependencies 2025-02-17 21:13:51 +01:00
Vinzenz Schroeter 04aa4aa95a split language bindings into separate repositories
All checks were successful
Rust / build (pull_request) Successful in 2m22s
2025-02-16 17:36:08 +01:00
Vinzenz Schroeter 2f7a2dfd62 Merge branch 'next'
All checks were successful
Rust / build (push) Successful in 7m22s
2025-02-15 12:19:00 +01:00
Vinzenz Schroeter bb86d23248 update version to 0.13.1
Some checks failed
Rust / build (pull_request) Failing after 7m12s
2025-02-15 12:15:09 +01:00
Vinzenz Schroeter fa3afeaf33 add wiki link to README
All checks were successful
Rust / build (pull_request) Successful in 7m26s
2025-02-15 12:10:45 +01:00
Vinzenz Schroeter 24862e096e update flake 2025-02-15 10:03:24 +01:00
Vinzenz Schroeter 073d982d57 simplify flake 2025-02-15 10:03:24 +01:00
Vinzenz Schroeter e6a7a8e163 cargo-mutants reduce misses to 16 2025-02-15 09:52:47 +01:00
Vinzenz Schroeter 8cb7a9bbfb create grids without copying data 2025-02-15 09:52:47 +01:00
Vinzenz Schroeter 753a66136e prepare move to forgejo 2025-02-08 14:52:45 +01:00
Vinzenz Schroeter 8f1aac797b misc metadata tweaks 2025-02-08 14:49:04 +01:00
Vinzenz Schroeter fc6389b587
fix link in README
All checks were successful
Rust / build (push) Successful in 7m37s
2025-02-08 09:55:03 +01:00
Vinzenz Schroeter d6a4d807be
Merge pull request #23 from cccb/forgejo-pipeline
All checks were successful
Rust / build (push) Successful in 7m39s
change pipeline to also run on forgejo, add note about move to README
2025-02-07 22:43:07 +01:00
Vinzenz Schroeter c0e3f2d748 fix license link 2025-02-07 22:39:15 +01:00
Vinzenz Schroeter 2e30dfffab change note for cccb-servicepoint-browser as it does not actually use the library 2025-02-07 22:38:30 +01:00
Vinzenz Schroeter 81fd341da2 fix new clippy warnings 2025-02-07 22:35:11 +01:00
Vinzenz Schroeter 2c6c854969 change pipeline to also run on forgejo, add note about move to README 2025-02-07 22:28:40 +01:00
Vinzenz Schroeter c5cd8648ba
Merge pull request #22 from SamuelScheit/patch-1
Some checks failed
Rust / build (push) Failing after 38s
add cccb-servicepoint-browser
2025-02-04 08:27:34 +01:00
Samuel Scheit 60e0d014ba
add cccb-servicepoint-browser 2025-01-31 11:19:32 +01:00
Vinzenz Schroeter b6fa1b161e Merge branch 'utf8'
Some checks failed
Rust / build (push) Has been cancelled
2025-01-16 21:33:01 +01:00
Vinzenz Schroeter 7e1fb6cc99 version 0.13.0 2025-01-16 20:51:56 +01:00
Vinzenz Schroeter 04cda144ed add ValueGrid::wrap and CharGrid::wrap_str, more examples
add examples
2025-01-12 15:22:54 +01:00
Vinzenz Schroeter 2a6005fff9 expose new command to uniffi API 2025-01-12 15:22:54 +01:00
Vinzenz Schroeter 2f2160f246 expose CharGrid to C API 2025-01-12 15:22:54 +01:00
Vinzenz Schroeter 4f83aa3d5c group cp437 functions 2025-01-12 15:22:54 +01:00
Vinzenz Schroeter dea176d0d9 fix warnings, minor docs changes 2025-01-12 15:22:54 +01:00
Vinzenz Schroeter 8320ee2d80 restructure api 2025-01-12 15:22:54 +01:00
Vinzenz Schroeter efaa52faa1 first CMD_UTF8_DATA implementation
UTF8 now works
2025-01-12 15:22:54 +01:00
Vinzenz Schroeter 38316169e9 update dependencies and flake
update dependencies
2025-01-12 15:22:54 +01:00
Vinzenz Schroeter 24e0eaaf07 sp_packet_from_parts, sp_bitmap_new_screen_sized 2024-11-23 23:47:41 +01:00
126 changed files with 5678 additions and 14784 deletions

View file

@ -1 +0,0 @@
use flake

View file

@ -9,24 +9,36 @@ on:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
# Make sure CI fails on all warnings, including Clippy lints
RUSTFLAGS: "-Dwarnings"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: build default features - name: Update repos
run: cargo build --all --verbose run: sudo apt-get update -qq
- name: build default features -- examples - name: Install rust toolchain
run: cargo build --examples --verbose run: sudo apt-get install -qy cargo rust-clippy
- name: test default features - name: install lzma
run: cargo test --all --verbose run: sudo apt-get update && sudo apt-get install -y liblzma-dev
- name: build all features - name: Run Clippy
run: cargo build --all-features --verbose run: cargo clippy --all-features
- name: build all features -- examples
run: cargo build --all-features --examples --verbose - name: no features -- test (without doctest)
- name: test all features run: cargo test --lib --no-default-features
run: cargo test --all --all-features --verbose
- name: default features -- test
run: cargo test --all
- name: default features -- examples
run: cargo build --examples
- name: all features -- test
run: cargo test --all --all-features
- name: all features -- examples
run: cargo build --examples --all-features

2
.gitignore vendored
View file

@ -4,3 +4,5 @@ out
.direnv .direnv
.envrc .envrc
result result
mutants.*
tarpaulin-report.html

View file

@ -4,6 +4,9 @@ Contributions are accepted in any form (issues, documentation, feature requests,
All creatures welcome. 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 ## Pull requests
Feel free to create a PR, even if your change is not done yet. Feel free to create a PR, even if your change is not done yet.

1375
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,102 @@
[workspace] [package]
resolver = "2" name = "servicepoint"
members = [ version = "0.15.2"
"crates/servicepoint", publish = true
"crates/servicepoint_binding_c", edition = "2021"
"crates/servicepoint_binding_c/examples/lang_c", license = "GPL-3.0-or-later"
"crates/servicepoint_binding_uniffi" 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"]
rust-version = "1.70.0"
[workspace.package] [lib]
version = "0.12.0" crate-type = ["rlib"]
[workspace.lints.rust] [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.9", optional = true }
once_cell = { version = "1.20", optional = true }
thiserror = "2.0"
inherent = "1.0"
[features]
default = ["compression_lzma", "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"]
cp437 = ["dep:once_cell"]
[[example]]
name = "random_brightness"
required-features = ["rand"]
[[example]]
name = "game_of_life"
required-features = ["rand"]
[[example]]
name = "own_command"
required-features = ["rand"]
[dev-dependencies]
# for examples
clap = { version = "4.5", features = ["derive"] }
[lints.rust]
missing-docs = "warn" missing-docs = "warn"
deprecated-safe = "warn"
future-incompatible = "warn"
keyword-idents = "warn"
let-underscore = "warn"
nonstandard-style = "warn"
refining_impl_trait_reachable = "warn"
rust-2024-compatibility = "warn"
[workspace.dependencies] [lints.clippy]
thiserror = "1.0.69" ## Categories
complexity = {level = "warn", priority = -1 }
perf = {level = "warn", priority = -1 }
style = {level = "warn", priority = -1 }
pedantic = {level = "warn", priority = -1 }
## Blacklist
unwrap_used = "warn"
expect_used = "warn"
panic = "warn"
incompatible_msrv = "forbid"
allow_attributes_without_reason = "warn"
## Whitelist
# Too many false positives as often a module only contains one struct that is re-exported at top-level
module_name_repetitions = "allow"
# The pretty detailed exception types should be enough for now
missing_errors_doc = "allow"
# The few places where a panic is triggered in code are inspected and should never panic
missing_panics_doc = "allow"
# Does not work for all types, but should probably be fixed at some point
iter_without_into_iter = "allow"
[lints.rustdoc]
private_doc_tests = "warn"
unescaped_backticks = "warn"
[package.metadata.docs.rs]
all-features = true
[profile.size-optimized]
inherits = "release"
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary

141
README.md
View file

@ -1,40 +1,145 @@
# servicepoint # servicepoint
[![Release](https://git.berlin.ccc.de/servicepoint/servicepoint/badges/release.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint/releases)
[![crates.io](https://img.shields.io/crates/v/servicepoint.svg)](https://crates.io/crates/servicepoint) [![crates.io](https://img.shields.io/crates/v/servicepoint.svg)](https://crates.io/crates/servicepoint)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint)](https://crates.io/crates/servicepoint) [![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint)](https://crates.io/crates/servicepoint)
[![docs.rs](https://img.shields.io/docsrs/servicepoint)](https://docs.rs/servicepoint/latest/servicepoint/) [![docs.rs](https://img.shields.io/docsrs/servicepoint)](https://docs.rs/servicepoint/latest/servicepoint/)
[![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint)](../../LICENSE) [![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint)](./LICENSE)
[![CI](https://git.berlin.ccc.de/servicepoint/servicepoint/badges/workflows/rust.yml/badge.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint)
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.
Take a look at the contained crates for language specific information: This crate contains a library for parsing, encoding and sending packets to this display via UDP.
The library itself is written in Rust, but can be used from multiple languages
via [language bindings](#supported-language-bindings).
| Crate | Languages | Readme | ## Examples
|-----------------------------|-----------------------------------|-------------------------------------------------------------------------|
| servicepoint | Rust | [servicepoint](crates/servicepoint/README.md) | ```rust no_run
| servicepoint_binding_c | C / C++ | [servicepoint_binding_c](crates/servicepoint_binding_c/README.md) | use std::net::UdpSocket;
| servicepoint_binding_uniffi | C# / Python / Go / Kotlin / Swift | [servicepoint_binding_cs](crates/servicepoint_binding_uniffi/README.md) | // everything you need is in the top-level
use servicepoint::{ClearCommand, UdpSocketExt};
fn main() {
// this should be the IP of the real display @CCCB
let destination = "172.23.42.29:2342";
// establish connection
let connection = UdpSocket::bind_connect(destination).expect("connection failed");
// clear screen content using the UdpSocketExt
connection.send_command(ClearCommand).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.
## Installation
```bash
cargo add servicepoint
```
or
```toml
[dependencies]
servicepoint = "0.15.2"
```
## 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 breaking changes with every minor version bump.
There should be no breaking changes in patch releases, but there may also be features hiding in those.
All of this means for you: please specify the full version including patch in your Cargo.toml until 1.0 is released.
Release notes are published [here](https://git.berlin.ccc.de/servicepoint/servicepoint/releases), please check them before updating.
Currently, this crate requires Rust [v1.70](https://releases.rs/docs/1.70.0/) from June 2023.
## Features
This library has multiple optional dependencies.
You can choose to (not) include them by toggling the related features.
| Name | Default | Description | Dependencies |
|-------------------|---------|----------------------------------------------|-------------------------------------------------|
| cp437 | true | Conversion to and from CP-437 | [once_cell](https://crates.io/crates/once_cell) |
| compression_lzma | true | Enable additional compression algorithm | [rust-lzma](https://crates.io/crates/rust-lzma) |
| compression_zlib | false | Enable additional compression algorithm | [flate2](https://crates.io/crates/flate2) |
| compression_bzip2 | false | Enable additional compression algorithm | [bzip2](https://crates.io/crates/bzip2) |
| compression_zstd | false | Enable additional compression algorithm | [zstd](https://crates.io/crates/zstd) |
| rand | false | `impl Distribution<Brightness> for Standard` | [rand](https://crates.io/crates/rand) |
Es an example, if you only want zlib compression:
```toml
[dependencies]
servicepoint = { version = "0.15.2", default-features = false, features = ["compression_zlib"] }
```
If you are looking at features to minimize binary size: take a look at the `tiny_announce`-example!
## Supported language bindings
| Language | Support level | Repo |
|-----------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| .NET (C#) | Full | [servicepoint-binding-csharp](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-csharp) contains bindings and a `.csproj` to reference |
| C | Full | [servicepoint-binding-c](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-c) contains a header and a library to link against |
| Ruby | Working | [servicepoint-binding-ruby](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-ruby) contains bindings |
| Python | Unsupported | bindings can be generated from [servicepoint-binding-uniffi](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-uniffi), tested once |
| Go | Unsupported | bindings can be generated from [servicepoint-binding-uniffi](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-uniffi) |
| Kotlin | Unsupported | bindings can be generated from [servicepoint-binding-uniffi](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-uniffi) |
| Swift | Unsupported | bindings can be generated from [servicepoint-binding-uniffi](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-uniffi) |
## Projects using the library ## Projects using the library
- screen simulator (rust): [servicepoint-simulator](https://github.com/kaesaecracker/servicepoint-simulator) - [servicepoint-simulator](https://git.berlin.ccc.de/servicepoint/servicepoint-simulator): a screen simulator written in rust
- A bunch of projects (C): [arfst23/ServicePoint](https://github.com/arfst23/ServicePoint), including - [servicepoint-tanks](https://git.berlin.ccc.de/vinzenz/servicepoint-tanks): a multiplayer game written in C# with a second screen in the browser written in React/Typescript
- [servicepoint-life](https://git.berlin.ccc.de/vinzenz/servicepoint-life): a cellular automata slideshow written in rust
- [servicepoint-cli](https://git.berlin.ccc.de/servicepoint/servicepoint-cli): a CLI that can:
- share (stream) your screen
- send image files with dithering
- clear the display
- ...
To add yourself to the list, open a pull request.
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.
If you have access, there is even more software linked in [the wiki](https://wiki.berlin.ccc.de/LED-Riesendisplay).
Some more related projects:
- [cccb-servicepoint-browser](https://github.com/SamuelScheit/cccb-servicepoint-browser): a partial typescript implementation inspired by this library and browser stream
- [arfst23/ServicePoint](https://github.com/arfst23/ServicePoint): a bunch of projects in C that [used to](https://zerforschen.plus/posts/tiny-binaries-rust/) use the C bindings
- a CLI tool to display image files on the display or use the display as a TTY - a CLI tool to display image files on the display or use the display as a TTY
- a BSD games robots clone - a BSD games robots clone
- a split-flap-display simulator - a split-flap-display simulator
- animations that play on the display - 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)
To add yourself to the list, open a pull request.
## Contributing ## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). You are welcome to contribute, see [CONTRIBUTING.md](CONTRIBUTING.md).
## What happened to servicepoint2? ## History
After `servicepoint2` has been merged into `servicepoint`, `servicepoint2` will not continue to get any updates. ### Move to Forgejo
This project moved
to [git.berlin.ccc.de/servicepoint/servicepoint](https://git.berlin.ccc.de/servicepoint/servicepoint).
The [GitHub repository](https://github.com/cccb/servicepoint) remains available as a mirror.
### What happened to servicepoint2?
`servicepoint2` was a fork of `servicepoint`. Since `servicepoint2` has been merged into `servicepoint`, `servicepoint2` did not get any updates.

View file

@ -1,59 +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://github.com/cccb/servicepoint"
readme = "README.md"
[lib]
crate-type = ["rlib"]
[dependencies]
log = "0.4"
bitvec = "1.0"
flate2 = { version = "1.0", optional = true }
bzip2 = { version = "0.4", optional = true }
zstd = { version = "0.13", optional = true }
rust-lzma = { version = "0.6.0", optional = true }
rand = { version = "0.8", optional = true }
tungstenite = { version = "0.24.0", optional = true }
once_cell = { version = "1.20.2", 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

View file

@ -1,64 +0,0 @@
# servicepoint
[![crates.io](https://img.shields.io/crates/v/servicepoint.svg)](https://crates.io/crates/servicepoint)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint)](https://crates.io/crates/servicepoint)
[![docs.rs](https://img.shields.io/docsrs/servicepoint)](https://docs.rs/servicepoint/latest/servicepoint/)
[![GPLv3 licensed](https://img.shields.io/crates/l/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.12.0"
```
## 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 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.
## 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://github.com/cccb/servicepoint/blob/main/README.md) for further information.

View file

@ -1,35 +0,0 @@
//! A simple example for how to send pixel data to the display.
use std::thread;
use clap::Parser;
use servicepoint::*;
#[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);
}
}

View file

@ -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();
}

View file

@ -1,298 +0,0 @@
use bitvec::order::Msb0;
use bitvec::prelude::BitSlice;
use bitvec::slice::IterMut;
use crate::{BitVec, DataRef, Grid, PIXEL_HEIGHT, PIXEL_WIDTH};
/// A grid of pixels stored in packed bytes.
#[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);
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);
assert_eq!(data.len(), height * width / 8);
Self {
width,
height,
bit_vec: BitVec::from_slice(data),
}
}
/// 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 {
fn from(value: Bitmap) -> Self {
value.bit_vec
}
}
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::{Bitmap, DataRef, Grid};
#[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));
}
}

View file

@ -1,177 +0,0 @@
use crate::{Grid, PrimitiveGrid};
#[cfg(feature = "rand")]
use rand::{
distributions::{Distribution, Standard},
Rng,
};
/// A display brightness value, checked for correct value range
///
/// # Examples
///
/// ```
/// # use servicepoint::{Brightness, Command, Connection};
/// let b = Brightness::MAX;
/// let val: u8 = b.into();
///
/// let b = Brightness::try_from(7).unwrap();
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
/// let result = connection.send(Command::Brightness(b));
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Brightness(u8);
/// A grid containing brightness values.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Connection, Grid, Origin};
/// let mut grid = BrightnessGrid::new(2,2);
/// grid.set(0, 0, Brightness::MIN);
/// grid.set(1, 1, Brightness::MIN);
///
/// # let connection = Connection::open("127.0.0.1:2342").unwrap();
/// connection.send(Command::CharBrightness(Origin::new(3, 7), grid)).unwrap()
/// ```
pub type BrightnessGrid = PrimitiveGrid<Brightness>;
impl BrightnessGrid {
/// Like [Self::load], but ignoring any out-of-range brightness values
pub fn saturating_load(width: usize, height: usize, data: &[u8]) -> Self {
PrimitiveGrid::load(width, height, data).map(Brightness::saturating_from)
}
}
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
}
}
impl From<BrightnessGrid> for Vec<u8> {
fn from(value: PrimitiveGrid<Brightness>) -> Self {
value
.iter()
.map(|brightness| (*brightness).into())
.collect()
}
}
impl From<&BrightnessGrid> for PrimitiveGrid<u8> {
fn from(value: &PrimitiveGrid<Brightness>) -> Self {
let u8s = value
.iter()
.map(|brightness| (*brightness).into())
.collect::<Vec<u8>>();
PrimitiveGrid::load(value.width(), value.height(), &u8s)
}
}
impl TryFrom<PrimitiveGrid<u8>> for BrightnessGrid {
type Error = u8;
fn try_from(value: PrimitiveGrid<u8>) -> Result<Self, Self::Error> {
let brightnesses = value
.iter()
.map(|b| Brightness::try_from(*b))
.collect::<Result<Vec<_>, _>>()?;
Ok(BrightnessGrid::load(
value.width(),
value.height(),
&brightnesses,
))
}
}
#[cfg(feature = "rand")]
impl Distribution<Brightness> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Brightness {
Brightness(rng.gen_range(Brightness::MIN.0..=Brightness::MAX.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DataRef;
#[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 to_u8_grid() {
let mut grid = BrightnessGrid::new(2, 2);
grid.set(1, 0, Brightness::MIN);
grid.set(0, 1, Brightness::MAX);
let actual = PrimitiveGrid::from(&grid);
assert_eq!(actual.data_ref(), &[11, 0, 11, 11]);
}
#[test]
fn saturating_convert() {
assert_eq!(Brightness::MAX, Brightness::saturating_from(100));
assert_eq!(Brightness(5), Brightness::saturating_from(5));
}
#[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]));
}
}

View file

@ -1,129 +0,0 @@
use crate::primitive_grid::SeriesError;
use crate::{Grid, PrimitiveGrid};
/// A grid containing UTF-8 characters.
pub type CharGrid = PrimitiveGrid<char>;
impl CharGrid {
/// Copies a column from the grid as a String.
///
/// Returns [None] if x is out of bounds.
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.
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 [SeriesError] if y is out of bounds or `row` is not of the correct size.
pub fn set_row_str(
&mut self,
y: usize,
value: &str,
) -> Result<(), SeriesError> {
self.set_row(y, value.chars().collect::<Vec<_>>().as_ref())
}
/// Overwrites a column in the grid with a str.
///
/// Returns [SeriesError] if y is out of bounds or `row` is not of the correct size.
pub fn set_col_str(
&mut self,
x: usize,
value: &str,
) -> Result<(), SeriesError> {
self.set_col(x, value.chars().collect::<Vec<_>>().as_ref())
}
}
impl From<&str> for CharGrid {
fn from(value: &str) -> Self {
let value = value.replace("\r\n", "\n");
let mut lines = value
.split('\n')
.map(move |line| line.trim_end())
.collect::<Vec<_>>();
let width =
lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len()));
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(value: &CharGrid) -> Self {
value
.iter_rows()
.map(move |chars| {
chars
.collect::<String>()
.replace('\0', " ")
.trim_end()
.to_string()
})
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Grid;
#[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(SeriesError::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() {
let original = "Hello\r\nWorld!\n...\n";
let grid = CharGrid::from(original);
assert_eq!(3, grid.height());
let actual = String::from(&grid);
assert_eq!("Hello\nWorld!\n...", actual);
}
}

View file

@ -1,934 +0,0 @@
use crate::{
command_code::CommandCode,
compression::into_decompressed,
packet::{Header, Packet},
Bitmap, Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin,
Pixels, PrimitiveGrid, BitVec, Tiles, TILE_SIZE,
};
/// 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::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 [CP-437] encoded characters.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, Connection, Origin};
/// # let connection = Connection::Fake;
/// use servicepoint::{CharGrid, Cp437Grid};
/// 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),
}
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)
}
#[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 =
PrimitiveGrid::load(*width as usize, *height as usize, payload);
let grid = match BrightnessGrid::try_from(grid) {
Ok(grid) => grid,
Err(val) => return Err(TryFromPacketError::InvalidBrightness(val)),
};
Ok(Command::CharBrightness(
Origin::new(*x as usize, *y as usize),
grid,
))
}
fn packet_into_brightness(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
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),
))
}
}
#[cfg(test)]
mod tests {
use crate::{
bitvec::prelude::BitVec,
command::TryFromPacketError,
command_code::CommandCode,
origin::Pixels,
packet::{Header, Packet},
Bitmap, Brightness, BrightnessGrid, Command, CompressionCode, Origin,
PrimitiveGrid,
};
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),
PrimitiveGrid::new(7, 5),
));
}
#[test]
fn round_trip_cp437_data() {
round_trip(Command::Cp437Data(
Origin::new(5, 2),
PrimitiveGrid::new(7, 5),
));
}
#[test]
fn round_trip_bitmap_linear() {
for compression in all_compressions().to_owned() {
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().to_owned() {
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().to_owned() {
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))
);
}
}

View file

@ -1,99 +0,0 @@
/// The u16 command codes used for the [Command]s.
#[repr(u16)]
#[derive(Debug, Copy, Clone)]
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,
#[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)
}
_ => Err(()),
}
}
}

View file

@ -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::{packet::Payload, CompressionCode};
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")
}
}
}

View file

@ -1,67 +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(()),
}
}
}

View file

@ -1,180 +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))
.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::*;
use crate::packet::*;
#[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()
}
}

View file

@ -1,257 +0,0 @@
//! Conversion between UTF-8 and CP-437.
//!
//! Most of the functionality is only available with feature "cp437" enabled.
use crate::{Grid, PrimitiveGrid};
use std::collections::HashMap;
/// A grid containing codepage 437 characters.
///
/// The encoding is currently not enforced.
pub type Cp437Grid = PrimitiveGrid<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)
}
}
#[allow(unused)] // depends on features
pub use feature_cp437::*;
#[cfg(feature = "cp437")]
mod feature_cp437 {
use super::*;
use crate::CharGrid;
/// 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]
pub 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)
});
const MISSING_CHAR_CP437: u8 = 0x3F; // '?'
impl From<&Cp437Grid> for CharGrid {
fn from(value: &Cp437Grid) -> Self {
value.map(cp437_to_char)
}
}
impl From<&CharGrid> for Cp437Grid {
fn from(value: &CharGrid) -> Self {
value.map(char_to_cp437)
}
}
impl From<CharGrid> for Cp437Grid {
fn from(value: CharGrid) -> Self {
Cp437Grid::from(&value)
}
}
/// Convert the provided bytes to UTF-8.
pub fn cp437_to_str(cp437: &[u8]) -> String {
cp437.iter().map(move |char| 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(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(&MISSING_CHAR_CP437)
}
}
#[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);
let actual = CharGrid::from(&cp437);
assert_eq!(actual, utf8);
}
#[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 , sur cette île bizarroïde 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 = str_to_cp437(utf8);
let actual = cp437_to_str(&*cp437);
assert_eq!(utf8, actual)
}
#[test]
fn convert_invalid() {
assert_eq!(cp437_to_char(char_to_cp437('😜')), '?');
}
}

View file

@ -1,149 +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 the pixels.
//!
//! # Examples
//!
//! ```rust
//! use servicepoint::{Command, CompressionCode, Grid, Bitmap};
//!
//! let connection = servicepoint::Connection::open("127.0.0.1:2342")
//! .expect("connection failed");
//!
//! // turn off all pixels on display
//! connection.send(Command::Clear)
//! .expect("send failed");
//! ```
//!
//! ```rust
//! # use servicepoint::{Command, CompressionCode, Grid, 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");
//! ```
use std::time::Duration;
pub use bitvec;
pub use crate::bitmap::Bitmap;
pub use crate::brightness::{Brightness, BrightnessGrid};
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::cp437::Cp437Grid;
pub use crate::data_ref::DataRef;
pub use crate::grid::Grid;
pub use crate::origin::{Origin, Pixels, Tiles};
pub use crate::primitive_grid::{PrimitiveGrid, SeriesError};
/// An alias for the specific type of [bitvec::prelude::BitVec] used.
pub type BitVec = bitvec::prelude::BitVec<u8, bitvec::prelude::Msb0>;
mod bitmap;
mod brightness;
mod char_grid;
mod command;
mod command_code;
mod compression;
mod compression_code;
mod connection;
pub mod cp437;
mod data_ref;
mod grid;
mod origin;
pub mod packet;
mod primitive_grid;
/// 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);
// include README.md in doctest
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDocTests;

View file

@ -1,355 +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::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::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 std::mem::size_of;
use crate::compression::into_compressed;
use crate::{
command_code::CommandCode, Bitmap, Command, CompressionCode, Grid, Offset,
Origin, Pixels, Tiles, TILE_SIZE,
};
/// 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,
),
}
}
}
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(()))
}
}

View file

@ -1,28 +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://github.com/cccb/servicepoint"
readme = "README.md"
links = "servicepoint"
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
cbindgen = "0.27.0"
[dependencies.servicepoint]
version = "0.12.0"
path = "../servicepoint"
features = ["all_compressions"]
[lints]
workspace = true
[package.metadata.docs.rs]
all-features = true

View file

@ -1,63 +0,0 @@
# servicepoint_binding_c
[![crates.io](https://img.shields.io/crates/v/servicepoint_binding_c.svg)](https://crates.io/crates/servicepoint)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint_binding_c)](https://crates.io/crates/servicepoint)
[![docs.rs](https://img.shields.io/docsrs/servicepoint_binding_c)](https://docs.rs/servicepoint/latest/servicepoint/)
[![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint_binding_c)](../../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://github.com/cccb/servicepoint/blob/main/README.md) for further information.

View file

@ -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}");
}
}

View file

@ -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"

View file

@ -1,14 +0,0 @@
[package]
name = "lang_c"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
test = false
[build-dependencies]
cc = "1.0"
[dependencies]
servicepoint_binding_c = { path = "../.." }

View file

@ -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: ;

View file

@ -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");
}

View file

@ -1,18 +0,0 @@
#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, SP_COMPRESSION_CODE_UNCOMPRESSED);
while (sp_connection_send_command(connection, sp_command_clone(command)));
sp_command_free(command);
sp_connection_free(connection);
return 0;
}

View file

@ -1,283 +0,0 @@
//! C functions for interacting with [SPBitmap]s
//!
//! prefix `sp_bitmap_`
use std::ptr::NonNull;
use servicepoint::{DataRef, Grid};
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))
}
/// 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(),
}
}

View file

@ -1,284 +0,0 @@
//! C functions for interacting with [SPBitVec]s
//!
//! prefix `sp_bitvec_`
use std::ptr::NonNull;
use crate::SPByteSlice;
use servicepoint::bitvec::prelude::{BitVec, Msb0};
/// 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(BitVec<u8, Msb0>);
impl From<BitVec<u8, Msb0>> for SPBitVec {
fn from(actual: BitVec<u8, Msb0>) -> Self {
Self(actual)
}
}
impl From<SPBitVec> for BitVec<u8, Msb0> {
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(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(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(),
}
}

View file

@ -1,322 +0,0 @@
//! C functions for interacting with [SPBrightnessGrid]s
//!
//! prefix `sp_brightness_grid_`
use crate::SPByteSlice;
use servicepoint::{Brightness, DataRef, Grid, PrimitiveGrid};
use std::convert::Into;
use std::intrinsics::transmute;
use std::ptr::NonNull;
/// see [Brightness::MIN]
pub const SP_BRIGHTNESS_MIN: u8 = 0;
/// see [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 = PrimitiveGrid::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 =
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 =
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::<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(),
}
}

View file

@ -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,
}

View file

@ -1,476 +0,0 @@
//! C functions for interacting with [SPCommand]s
//!
//! prefix `sp_command_`
use std::ptr::{null_mut, NonNull};
use servicepoint::{Brightness, Origin};
use crate::{
SPBitVec, SPBitmap, SPBrightnessGrid, 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 =
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(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 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(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(
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);
}

View file

@ -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);
}

View file

@ -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)
}
}

View file

@ -1,286 +0,0 @@
//! C functions for interacting with [SPCp437Grid]s
//!
//! prefix `sp_cp437_grid_`
use std::ptr::NonNull;
use crate::SPByteSlice;
use servicepoint::{DataRef, Grid};
/// 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(),
}
}

View file

@ -1,46 +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::bitvec::*;
pub use crate::bitmap::*;
pub use crate::brightness_grid::*;
pub use crate::byte_slice::*;
pub use crate::command::*;
pub use crate::connection::*;
pub use crate::constants::*;
pub use crate::cp437_grid::*;
pub use crate::packet::*;
mod bitvec;
mod bitmap;
mod brightness_grid;
mod byte_slice;
mod command;
mod connection;
mod constants;
mod cp437_grid;
mod packet;

View file

@ -1,109 +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::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::Packet::try_from(data) {
Err(_) => null_mut(),
Ok(packet) => Box::into_raw(Box::new(SPPacket(packet))),
}
}
/// 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 `sp_packet_free` 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)
}

View file

@ -1,60 +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://github.com/cccb/servicepoint"
#readme = "README.md"
[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.12.0"
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.1+v0.25.0"
rev = "a77dc0462dc18d53846c758155ab4e0a42e5b240"
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"]

View file

@ -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://github.com/cccb/servicepoint/blob/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://github.com/cccb/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.

View file

@ -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/"

View file

@ -1,4 +0,0 @@
go
kotlin
python
swift

View file

@ -1,2 +0,0 @@
bin
obj

View file

@ -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);
}

View file

@ -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>

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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$§"));
}
}

View file

@ -1,2 +0,0 @@
global using Xunit;
global using ServicePoint;

View file

@ -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>

View file

@ -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;
}

View file

@ -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.12.0</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>

View file

@ -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

View file

@ -1,3 +0,0 @@
source 'https://rubygems.org'
gem 'servicepoint', path: '..'

View file

@ -1,19 +0,0 @@
PATH
remote: ..
specs:
servicepoint (0.0.0)
ffi
GEM
remote: https://rubygems.org/
specs:
ffi (1.17.0-x86_64-linux-gnu)
PLATFORMS
x86_64-linux
DEPENDENCIES
servicepoint!
BUNDLED WITH
2.3.27

View file

@ -1,25 +0,0 @@
require_relative "../lib/servicepoint_binding_uniffi"
include ServicepointBindingUniffi
connection = Connection.new("172.23.42.29:2342")
pixels = Bitmap.new_max_sized
x_offset = 0
loop do
pixels.fill(false)
(0..((pixels.height) -1)).each do |y|
pixels.set((y + x_offset) % pixels.width, y, true);
end
command = Command.bitmap_linear_win(0, 0, pixels, CompressionCode::UNCOMPRESSED)
connection.send(command)
sleep 0.0005
x_offset += 1
end

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
LD_LIBRARY_PATH="../../../../../target/release:$LD_LIBRARY_PATH" ruby example.rb

View file

@ -1,13 +0,0 @@
Gem::Specification.new do |s|
s.name = "servicepoint"
s.version = "0.12.0"
s.summary = ""
s.description = ""
s.authors = ["kaesaecracker"]
s.email = ""
s.files = ["lib/servicepoint_binding_uniffi.rb"]
s.homepage =
"https://rubygems.org/gems/hola"
s.license = "MIT"
s.add_dependency 'ffi'
end

View file

@ -1,3 +0,0 @@
fn main() {
uniffi_bindgen_cs::main().unwrap();
}

View file

@ -1,3 +0,0 @@
fn main() {
uniffi_bindgen_go::main().unwrap();
}

View file

@ -1,3 +0,0 @@
fn main() {
uniffi::uniffi_bindgen_main()
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -1,163 +0,0 @@
use servicepoint::{Grid, SeriesError};
use std::convert::Into;
use std::sync::{Arc, RwLock};
use crate::cp437_grid::Cp437Grid;
#[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<SeriesError> for CharGridError {
fn from(e: SeriesError) -> Self {
match e {
SeriesError::OutOfBounds { index, size } => {
CharGridError::OutOfBounds {
index: index as u64,
size: size as u64,
}
}
SeriesError::InvalidLength { actual, expected } => {
CharGridError::InvalidSeriesLength {
actual: actual as u64,
expected: expected as u64,
}
}
}
}
}

View file

@ -1,162 +0,0 @@
use crate::bitmap::Bitmap;
use crate::bitvec::BitVec;
use crate::brightness_grid::BrightnessGrid;
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 clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.clone())
}
pub fn equals(&self, other: &Command) -> bool {
self.actual == other.actual
}
}

View file

@ -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,
}

View file

@ -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:?}"),
}
})
}
}

View file

@ -1,21 +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,
}
}

View file

@ -1,77 +0,0 @@
use servicepoint::{DataRef, Grid};
use std::sync::{Arc, RwLock};
use crate::char_grid::CharGrid;
#[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()))
}
}

View file

@ -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 },
}

View file

@ -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 cp437_grid;
mod errors;
mod constants;

View file

@ -1,3 +0,0 @@
[bindings.csharp]
namespace = "ServicePoint"
access_modifier = "public"

View file

@ -1,8 +1,10 @@
//! An example for how to send text to the display. //! An example for how to send text to the display.
use clap::Parser; use clap::Parser;
use servicepoint::{
use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin}; CharGrid, CharGridCommand, ClearCommand, UdpSocketExt, TILE_WIDTH,
};
use std::net::UdpSocket;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -32,20 +34,18 @@ fn main() {
cli.text.push("Hello, CCCB!".to_string()); cli.text.push("Hello, CCCB!".to_string());
} }
let connection = Connection::open(&cli.destination) let connection = UdpSocket::bind_connect(&cli.destination)
.expect("could not connect to display"); .expect("could not connect to display");
if cli.clear { if cli.clear {
connection connection
.send(Command::Clear) .send_command(ClearCommand)
.expect("sending clear failed"); .expect("sending clear failed");
} }
let text = cli.text.join("\n"); let text = cli.text.join("\n");
let grid = CharGrid::from(text); let command: CharGridCommand = CharGrid::wrap_str(TILE_WIDTH, &text).into();
let grid = Cp437Grid::from(grid);
connection connection
.send(Command::Cp437Data(Origin::ZERO, grid)) .send_command(command)
.expect("sending text failed"); .expect("sending text failed");
} }

View file

@ -1,8 +1,11 @@
//! Show a brightness level test pattern on screen //! Show a brightness level test pattern on screen
use clap::Parser; use clap::Parser;
use servicepoint::{
use servicepoint::*; Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand,
DataRef, Grid, UdpSocketExt, TILE_HEIGHT, TILE_WIDTH,
};
use std::net::UdpSocket;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -12,27 +15,23 @@ struct Cli {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let connection = Connection::open(cli.destination) let connection = UdpSocket::bind_connect(cli.destination)
.expect("could not connect to display"); .expect("could not connect to display");
let mut pixels = Bitmap::max_sized(); let mut bitmap = Bitmap::max_sized();
pixels.fill(true); bitmap.fill(true);
let command = Command::BitmapLinearWin( connection
Origin::ZERO, .send_command(BitmapCommand::from(bitmap))
pixels, .expect("send failed");
CompressionCode::Uncompressed,
);
connection.send(command).expect("send failed");
let max_brightness: u8 = Brightness::MAX.into(); let max_brightness: u8 = Brightness::MAX.into();
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT); let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() { for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
let level = index as u8 % max_brightness; let level = (index % u8::MAX as usize) as u8 % max_brightness;
*byte = Brightness::try_from(level).unwrap(); *byte = Brightness::try_from(level).unwrap();
} }
connection let command: BrightnessGridCommand = brightnesses.into();
.send(Command::CharBrightness(Origin::ZERO, brightnesses)) connection.send_command(command).expect("send failed");
.expect("send failed");
} }

54
examples/fewer_copies.rs Normal file
View file

@ -0,0 +1,54 @@
//! An example on how to modify the image on screen without knowing the current content.
use clap::Parser;
use servicepoint::{
Bitmap, BitmapCommand, CompressionCode, Grid, Origin, Packet, UdpSocketExt,
FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread, 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 = UdpSocket::bind_connect(cli.destination)
.expect("could not connect to display");
let mut command = BitmapCommand {
compression: CompressionCode::Uncompressed,
bitmap: Bitmap::max_sized(),
origin: Origin::ZERO,
};
command.bitmap.fill(true);
let mut buf = [0u8; 10000];
for x_offset in 0..PIXEL_WIDTH {
for y in 0..PIXEL_HEIGHT {
command.bitmap.set((x_offset + y) % PIXEL_WIDTH, y, false);
}
let packet: Packet = Packet::try_from(&command)
.expect("could not turn command into packet");
let size = packet
.serialize_to(&mut buf)
.expect("failed to serialize packet");
connection
.send(&buf[..size])
.expect("could not send command to display");
thread::sleep(sleep_duration);
}
}

View file

@ -1,11 +1,9 @@
//! A simple game of life implementation to show how to render graphics to the display. //! A simple game of life implementation to show how to render graphics to the display.
use std::thread;
use clap::Parser; use clap::Parser;
use rand::{distributions, Rng}; use rand::Rng;
use servicepoint::{Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING};
use servicepoint::*; use std::{net::UdpSocket, thread};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -18,19 +16,14 @@ struct Cli {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let connection = Connection::open(&cli.destination) let connection = UdpSocket::bind_connect(&cli.destination)
.expect("could not connect to display"); .expect("could not connect to display");
let mut field = make_random_field(cli.probability);
let mut command = BitmapCommand::from(make_random_field(cli.probability));
loop { loop {
let command = Command::BitmapLinearWin( connection.send_command(&command).expect("could not send");
Origin::ZERO,
field.clone(),
CompressionCode::Lzma,
);
connection.send(command).expect("could not send");
thread::sleep(FRAME_PACING); thread::sleep(FRAME_PACING);
field = iteration(field); command.bitmap = iteration(command.bitmap);
} }
} }
@ -41,10 +34,8 @@ fn iteration(field: Bitmap) -> Bitmap {
let old_state = field.get(x, y); let old_state = field.get(x, y);
let neighbors = count_neighbors(&field, x as i32, y as i32); let neighbors = count_neighbors(&field, x as i32, y as i32);
let new_state = matches!( let new_state =
(old_state, neighbors), matches!((old_state, neighbors), (true, 2 | 3) | (false, 3));
(true, 2) | (true, 3) | (false, 3)
);
next.set(x, y, new_state); next.set(x, y, new_state);
} }
} }
@ -80,8 +71,8 @@ fn count_neighbors(field: &Bitmap, x: i32, y: i32) -> i32 {
fn make_random_field(probability: f64) -> Bitmap { fn make_random_field(probability: f64) -> Bitmap {
let mut field = Bitmap::max_sized(); let mut field = Bitmap::max_sized();
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let d = distributions::Bernoulli::new(probability).unwrap(); let d = rand::distr::Bernoulli::new(probability).unwrap();
for x in 0..field.width() { for x in 0..field.width() {
for y in 0..field.height() { for y in 0..field.height() {
field.set(x, y, rng.sample(d)); field.set(x, y, rng.sample(d));

33
examples/moving_line.rs Normal file
View file

@ -0,0 +1,33 @@
//! A simple example for how to send pixel data to the display.
use clap::Parser;
use servicepoint::{
Bitmap, BitmapCommand, CompressionCode, Grid, UdpSocketExt, FRAME_PACING,
PIXEL_HEIGHT, PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread};
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long, default_value = "localhost:2342")]
destination: String,
}
fn main() {
let connection = UdpSocket::bind_connect(Cli::parse().destination)
.expect("could not connect to display");
let mut bitmap = Bitmap::max_sized();
for x_offset in 0..usize::MAX {
bitmap.fill(false);
for y in 0..PIXEL_HEIGHT {
bitmap.set((y + x_offset) % PIXEL_WIDTH, y, true);
}
let mut command = BitmapCommand::from(bitmap.clone());
command.compression = CompressionCode::Uncompressed;
connection.send_command(command).expect("send failed");
thread::sleep(FRAME_PACING);
}
}

49
examples/own_command.rs Normal file
View file

@ -0,0 +1,49 @@
//! An example on how to use the provided infrastructure to implement custom commands.
use rand::Rng;
use servicepoint::{
Brightness, GlobalBrightnessCommand, Header, Packet, UdpSocketExt,
};
use std::{fmt::Debug, net::UdpSocket};
/// Command that sets the brightness to zero globally.
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ZeroBrightnessCommand;
impl Into<Packet> for ZeroBrightnessCommand {
fn into(self) -> Packet {
GlobalBrightnessCommand::from(Brightness::MIN).into()
}
}
/// Command that turns into a random packet.
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct FuzzyCommand;
impl TryInto<Packet> for FuzzyCommand {
type Error = ();
fn try_into(self) -> Result<Packet, Self::Error> {
let mut rng = rand::rng();
Ok(Packet {
payload: None,
header: Header {
command_code: rng.random(),
a: rng.random(),
b: rng.random(),
c: rng.random(),
d: rng.random(),
},
})
}
}
fn main() {
let connection = UdpSocket::bind_connect("172.23.42.29:2342")
.expect("could not connect to display");
for _ in 0..100 {
connection.send_command(FuzzyCommand).unwrap()
}
connection.send_command(ZeroBrightnessCommand).unwrap();
}

View file

@ -1,13 +1,14 @@
//! A simple example for how to set brightnesses for tiles on the screen. //! A simple example for how to set brightnesses for tiles on the screen.
//! Continuously changes the tiles in a random window to random brightnesses. //! Continuously changes the tiles in a random window to random brightnesses.
use std::time::Duration;
use clap::Parser; use clap::Parser;
use rand::Rng; use rand::Rng;
use servicepoint::{
use servicepoint::Command::{BitmapLinearWin, Brightness, CharBrightness}; Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand,
use servicepoint::*; GlobalBrightnessCommand, Grid, Origin, UdpSocketExt, TILE_HEIGHT,
TILE_WIDTH,
};
use std::{net::UdpSocket, time::Duration};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -22,7 +23,7 @@ struct Cli {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let connection = Connection::open(cli.destination) let connection = UdpSocket::bind_connect(cli.destination)
.expect("could not connect to display"); .expect("could not connect to display");
let wait_duration = Duration::from_millis(cli.wait_ms); let wait_duration = Duration::from_millis(cli.wait_ms);
@ -31,37 +32,36 @@ fn main() {
let mut filled_grid = Bitmap::max_sized(); let mut filled_grid = Bitmap::max_sized();
filled_grid.fill(true); filled_grid.fill(true);
let command = BitmapLinearWin( let command = BitmapCommand::from(filled_grid);
Origin::ZERO, connection.send_command(command).expect("send failed");
filled_grid,
CompressionCode::Lzma,
);
connection.send(command).expect("send failed");
} }
// set all pixels to the same random brightness // set all pixels to the same random brightness
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
connection.send(Brightness(rng.gen())).unwrap(); let command: GlobalBrightnessCommand = rng.random::<Brightness>().into();
connection.send_command(command).unwrap();
// continuously update random windows to new random brightness // continuously update random windows to new random brightness
loop { loop {
let min_size = 1; let min_size = 1;
let x = rng.gen_range(0..TILE_WIDTH - min_size); let x = rng.random_range(0..TILE_WIDTH - min_size);
let y = rng.gen_range(0..TILE_HEIGHT - min_size); let y = rng.random_range(0..TILE_HEIGHT - min_size);
let w = rng.gen_range(min_size..=TILE_WIDTH - x); let w = rng.random_range(min_size..=TILE_WIDTH - x);
let h = rng.gen_range(min_size..=TILE_HEIGHT - y); let h = rng.random_range(min_size..=TILE_HEIGHT - y);
let origin = Origin::new(x, y); let origin = Origin::new(x, y);
let mut luma = BrightnessGrid::new(w, h); let mut luma = BrightnessGrid::new(w, h);
for y in 0..h { for y in 0..h {
for x in 0..w { for x in 0..w {
luma.set(x, y, rng.gen()); luma.set(x, y, rng.random());
} }
} }
connection.send(CharBrightness(origin, luma)).unwrap(); connection
.send_command(BrightnessGridCommand { origin, grid: luma })
.unwrap();
std::thread::sleep(wait_duration); std::thread::sleep(wait_duration);
} }
} }

47
examples/tiny_announce.rs Normal file
View file

@ -0,0 +1,47 @@
//! An example for how to send text to the display - but optimized for minimal binary size.
//!
//! See [zerforschen.plus/posts/tiny-binaries-rust](https://zerforschen.plus/posts/tiny-binaries-rust/)
//! for details.
//!
//! The bulk of optimizations are compiler options, though there are some code changes that together
//! make a huge difference.
//!
//! To build this example inside this repository for the smallest possible size, you can run:
//! ```sh
//! RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none" \
//! cargo build \
//! --example=tiny_announce \
//! --profile=size-optimized \
//! --no-default-features --features=protocol_udp \
//! -Zbuild-std="core,std,alloc,proc_macro,panic_abort" \
//! -Zbuild-std-features="panic_immediate_abort"
//!```
//!
//! This requires unstable rust.
#![no_main]
use servicepoint::{CharGrid, CharGridCommand, ClearCommand, UdpSocketExt};
use std::net::{SocketAddr, UdpSocket};
/// This is the entry point of the example.
/// `#![no_main]` is used to remove the default rust main
/// Because we use `#![no_main]`, this is a C-style main function.
#[unsafe(no_mangle)]
pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize {
let addr = SocketAddr::from(([172, 23, 42, 29], 2342));
let connection = UdpSocket::bind_connect(addr).unwrap();
connection.send_command(ClearCommand).unwrap();
let grid = CharGrid::from_vec(
5,
vec!['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'],
)
.unwrap();
connection
.send_command(CharGridCommand::from(grid))
.unwrap();
0
}

View file

@ -1,10 +1,11 @@
//! An example on how to modify the image on screen without knowing the current content. //! An example on how to modify the image on screen without knowing the current content.
use std::thread;
use std::time::Duration;
use clap::Parser; use clap::Parser;
use servicepoint::{
use servicepoint::*; Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT,
PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread, time::Duration};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -22,7 +23,7 @@ fn main() {
Duration::from_millis(cli.time / PIXEL_WIDTH as u64), Duration::from_millis(cli.time / PIXEL_WIDTH as u64),
); );
let connection = Connection::open(cli.destination) let connection = UdpSocket::bind_connect(cli.destination)
.expect("could not connect to display"); .expect("could not connect to display");
let mut enabled_pixels = Bitmap::max_sized(); let mut enabled_pixels = Bitmap::max_sized();
@ -33,8 +34,9 @@ fn main() {
enabled_pixels.set(x_offset % PIXEL_WIDTH, y, false); enabled_pixels.set(x_offset % PIXEL_WIDTH, y, false);
} }
let command = BitmapCommand::from(enabled_pixels.clone());
connection connection
.send(Command::BitmapLinearWin(Origin::ZERO, enabled_pixels.clone(), CompressionCode::Lzma)) .send_command(command)
.expect("could not send command to display"); .expect("could not send command to display");
thread::sleep(sleep_duration); thread::sleep(sleep_duration);
} }

View file

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1721727458, "lastModified": 1745925850,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -22,16 +22,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1730963269, "lastModified": 1750838302,
"narHash": "sha256-rz30HrFYCHiWEBCKHMffHbMdWJ35hEkcRVU0h7ms3x0=", "narHash": "sha256-aVkL3/yu50oQzi2YuKo0ceiCypVZpZXYd2P2p1FMJM4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "83fb6c028368e465cd19bb127b86f971a5e41ebc", "rev": "7284e2decc982b81a296ab35aa46e804baaa1cfe",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixos-24.05", "ref": "nixos-25.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View file

@ -1,8 +1,8 @@
{ {
description = "Flake for servicepoint-simulator"; description = "Flake for the servicepoint library.";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05"; nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
naersk = { naersk = {
url = "github:nix-community/naersk"; url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -23,35 +23,28 @@
"aarch64-darwin" "aarch64-darwin"
"x86_64-darwin" "x86_64-darwin"
]; ];
forAllSystems = lib.genAttrs supported-systems; forAllSystems =
make-rust-toolchain-core = f:
pkgs: lib.genAttrs supported-systems (
pkgs.symlinkJoin { system:
name = "rust-toolchain-core"; f rec {
paths = with pkgs; [ pkgs = nixpkgs.legacyPackages.${system};
rustc inherit system;
cargo }
rustPlatform.rustcSrc );
];
};
in in
rec { rec {
packages = forAllSystems ( packages = forAllSystems (
system: { pkgs, ... }:
let let
pkgs = nixpkgs.legacyPackages."${system}"; naersk' = pkgs.callPackage naersk { };
rust-toolchain-core = make-rust-toolchain-core pkgs;
naersk' = pkgs.callPackage naersk {
cargo = rust-toolchain-core;
rustc = rust-toolchain-core;
};
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
pkg-config pkg-config
makeWrapper makeWrapper
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
xe xe
lzma xz
]; ];
makeExample = makeExample =
{ {
@ -69,9 +62,8 @@
package package
]; ];
src = ./.; src = ./.;
nativeBuildInputs = nativeBuildInputs; inherit nativeBuildInputs buildInputs;
strictDeps = true; strictDeps = true;
buildInputs = buildInputs;
gitSubmodules = true; gitSubmodules = true;
overrideMain = old: { overrideMain = old: {
preConfigure = '' preConfigure = ''
@ -95,9 +87,8 @@
cargoTestOptions = x: x ++ package-param; cargoTestOptions = x: x ++ package-param;
src = ./.; src = ./.;
doCheck = true; doCheck = true;
nativeBuildInputs = nativeBuildInputs;
strictDeps = true; strictDeps = true;
buildInputs = buildInputs; inherit nativeBuildInputs buildInputs;
}; };
in in
rec { rec {
@ -130,29 +121,27 @@
legacyPackages = packages; legacyPackages = packages;
devShells = forAllSystems ( devShells = forAllSystems (
system: { pkgs, system }:
let
pkgs = nixpkgs.legacyPackages."${system}";
rust-toolchain = pkgs.symlinkJoin {
name = "rust-toolchain";
paths = with pkgs; [
(make-rust-toolchain-core pkgs)
rustfmt
clippy
cargo-expand
cargo-tarpaulin
];
};
in
{ {
default = pkgs.mkShell rec { default = pkgs.mkShell rec {
inputsFrom = [ self.packages.${system}.servicepoint ]; inputsFrom = [ self.packages.${system}.servicepoint ];
packages = with pkgs; [ packages = with pkgs; [
rust-toolchain (pkgs.symlinkJoin
ruby {
dotnet-sdk_8 name = "rust-toolchain";
gcc paths = with pkgs; [
gnumake rustc
cargo
rustPlatform.rustcSrc
rustfmt
clippy
cargo-expand
cargo-tarpaulin
cargo-semver-checks
cargo-show-asm
cargo-flamegraph
];
})
]; ];
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; 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}";
@ -160,6 +149,6 @@
} }
); );
formatter = forAllSystems (system: nixpkgs.legacyPackages."${system}".nixfmt-rfc-style); formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style);
}; };
} }

2
generate-coverage Executable file
View file

@ -0,0 +1,2 @@
#/usr/bin/env bash
cargo tarpaulin --out Html --all-features

115
src/brightness.rs Normal file
View file

@ -0,0 +1,115 @@
#[cfg(feature = "rand")]
use rand::{
distr::{Distribution, StandardUniform},
Rng,
};
/// A display brightness value, checked for correct value range
///
/// # Examples
///
/// ```
/// # use servicepoint::*;
/// let b = Brightness::MAX;
/// let val: u8 = b.into();
///
/// let b = Brightness::try_from(7).unwrap();
/// # let connection = FakeConnection;
/// let result = connection.send_command(GlobalBrightnessCommand::from(b));
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
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`].
#[must_use]
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 StandardUniform {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Brightness {
Brightness(rng.random_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::rng();
for _ in 0..100 {
let _: Brightness = rng.random();
}
}
#[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::rng();
// two so test failure is less likely
assert_ne!(
[rng.random::<Brightness>(), rng.random()],
[rng.random(), rng.random()]
);
}
}

252
src/command_code.rs Normal file
View file

@ -0,0 +1,252 @@
/// The u16 command codes used for the [`crate::Command`]s.
#[repr(u16)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub 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
}
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
#[error("The command code {0} is not known.")]
pub struct InvalidCommandCodeError(pub u16);
impl TryFrom<u16> for CommandCode {
type Error = InvalidCommandCodeError;
/// 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(InvalidCommandCodeError(value)),
}
}
}
#[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);
}
}

328
src/commands/bitmap.rs Normal file
View file

@ -0,0 +1,328 @@
use crate::{
command_code::{CommandCode, InvalidCommandCodeError},
commands::errors::{TryFromPacketError, TryIntoPacketError},
compression::{compress, decompress, CompressionError},
Bitmap, CompressionCode, DataRef, Grid, Header, Origin, Packet, Pixels,
TypedCommand, TILE_SIZE,
};
/// Overwrites a rectangular region of pixels.
///
/// Origin coordinates must be divisible by 8.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// #
/// let mut bitmap = Bitmap::max_sized();
/// // draw something to the pixels here
/// # bitmap.set(2, 5, true);
///
/// // create command to send pixels
/// let command = BitmapCommand {
/// bitmap,
/// origin: Origin::ZERO,
/// compression: CompressionCode::Uncompressed
/// };
///
/// connection.send_command(command).expect("send failed");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BitmapCommand {
/// the pixels to send
pub bitmap: Bitmap,
/// where to start drawing the pixels
pub origin: Origin<Pixels>,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl TryFrom<&BitmapCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: &BitmapCommand) -> Result<Self, Self::Error> {
let tile_x = (value.origin.x / TILE_SIZE).try_into()?;
let tile_w = (value.bitmap.width() / TILE_SIZE).try_into()?;
let pixel_h = value.bitmap.height().try_into()?;
let command =
BitmapCommand::command_code_for_compression(value.compression);
let data_ref = value.bitmap.data_ref();
let payload = match compress(value.compression, data_ref) {
Ok(payload) => payload,
Err(CompressionError::NoCompression) => data_ref.to_vec(),
Err(_) => return Err(TryIntoPacketError::CompressionFailed),
};
Ok(Packet {
header: Header {
command_code: command.into(),
a: tile_x,
b: value.origin.y.try_into()?,
c: tile_w,
d: pixel_h,
},
payload: Some(payload),
})
}
}
impl TryFrom<BitmapCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: BitmapCommand) -> Result<Self, Self::Error> {
Packet::try_from(&value)
}
}
impl TryFrom<Packet> for BitmapCommand {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let code = CommandCode::try_from(packet.header.command_code)?;
let compression = BitmapCommand::compression_for_command_code(code)
.ok_or(InvalidCommandCodeError(packet.header.command_code))?;
let Packet {
header:
Header {
command_code: _,
a: tiles_x,
b: pixels_y,
c: tile_w,
d: pixel_h,
},
payload,
} = packet;
let expected = tile_w as usize * pixel_h as usize;
let payload =
payload.ok_or(TryFromPacketError::UnexpectedPayloadSize {
actual: 0,
expected,
})?;
let payload = match decompress(compression, &payload) {
Ok(payload) => payload,
Err(CompressionError::NoCompression) => payload,
Err(_) => return Err(TryFromPacketError::DecompressionFailed),
};
let bitmap = Bitmap::load(
tile_w as usize * TILE_SIZE,
pixel_h as usize,
&payload,
)?;
let origin =
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize);
Ok(Self {
bitmap,
origin,
compression,
})
}
}
impl From<BitmapCommand> for TypedCommand {
fn from(command: BitmapCommand) -> Self {
Self::Bitmap(command)
}
}
impl From<Bitmap> for BitmapCommand {
fn from(bitmap: Bitmap) -> Self {
Self {
bitmap,
origin: Origin::default(),
compression: CompressionCode::default(),
}
}
}
impl BitmapCommand {
fn command_code_for_compression(
compression_code: CompressionCode,
) -> CommandCode {
match compression_code {
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,
}
}
fn compression_for_command_code(
command_code: CommandCode,
) -> Option<CompressionCode> {
Some(match command_code {
CommandCode::BitmapLinearWinUncompressed => {
CompressionCode::Uncompressed
}
#[cfg(feature = "compression_zlib")]
CommandCode::BitmapLinearWinZlib => CompressionCode::Zlib,
#[cfg(feature = "compression_bzip2")]
CommandCode::BitmapLinearWinBzip2 => CompressionCode::Bzip2,
#[cfg(feature = "compression_lzma")]
CommandCode::BitmapLinearWinLzma => CompressionCode::Lzma,
#[cfg(feature = "compression_zstd")]
CommandCode::BitmapLinearWinZstd => CompressionCode::Zstd,
_ => return None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
command_code::CommandCode, commands::tests::TestImplementsCommand,
};
impl TestImplementsCommand for BitmapCommand {}
#[test]
fn command_code() {
assert_eq!(
BitmapCommand::try_from(Packet {
payload: None,
header: Header {
command_code: CommandCode::Brightness.into(),
..Default::default()
}
}),
Err(InvalidCommandCodeError(CommandCode::Brightness.into()).into())
);
}
#[test]
fn error_decompression_failed_win() {
for compression in CompressionCode::ALL {
let p: Packet = BitmapCommand {
origin: Origin::new(16, 8),
bitmap: Bitmap::new(8, 8).unwrap(),
compression: *compression,
}
.try_into()
.unwrap();
let Packet { header, payload } = p;
let mut payload = payload.unwrap();
// mangle it
for byte in &mut payload {
*byte -= *byte / 2;
}
let p = Packet {
header,
payload: Some(payload),
};
let result = TypedCommand::try_from(p);
if *compression != CompressionCode::Uncompressed {
assert_eq!(
result,
Err(TryFromPacketError::DecompressionFailed)
);
} else {
assert!(result.is_ok());
}
}
}
#[test]
fn into_command() {
let mut bitmap = Bitmap::max_sized();
bitmap.fill(true);
assert_eq!(
BitmapCommand::from(bitmap.clone()),
BitmapCommand {
bitmap,
origin: Origin::default(),
compression: CompressionCode::default()
},
)
}
#[test]
fn into_packet_out_of_range() {
let mut cmd = BitmapCommand::from(Bitmap::max_sized());
cmd.origin.x = usize::MAX;
assert!(matches!(
Packet::try_from(cmd),
Err(TryIntoPacketError::ConversionError(_))
));
}
#[test]
fn into_packet_invalid_alignment() {
let cmd = BitmapCommand {
bitmap: Bitmap::max_sized(),
compression: CompressionCode::Uncompressed,
origin: Origin::new(5, 0),
};
let packet = Packet::try_from(cmd).unwrap();
assert_eq!(
packet.header,
Header {
command_code: 19,
a: 0,
b: 0,
c: 56,
d: 160
}
);
let cmd = BitmapCommand {
bitmap: Bitmap::max_sized(),
compression: CompressionCode::Uncompressed,
origin: Origin::new(11, 0),
};
let packet = Packet::try_from(cmd).unwrap();
assert_eq!(
packet.header,
Header {
command_code: 19,
a: 1,
b: 0,
c: 56,
d: 160
}
);
}
#[test]
fn round_trip() {
for compression in CompressionCode::ALL {
crate::commands::tests::round_trip(
BitmapCommand {
origin: Origin::ZERO,
bitmap: Bitmap::max_sized(),
compression: *compression,
}
.into(),
);
}
}
#[test]
fn round_trip_ref() {
for compression in CompressionCode::ALL {
crate::commands::tests::round_trip_ref(
&BitmapCommand {
origin: Origin::ZERO,
bitmap: Bitmap::max_sized(),
compression: *compression,
}
.into(),
);
}
}
}

View file

@ -0,0 +1,87 @@
#![allow(deprecated, reason = "this implements the deprecated functionality")]
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
/// Legacy command code, gets ignored by the real display.
///
/// Might be useful as a noop package.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// // this sends a packet that does nothing
/// connection.send_command(BitmapLegacyCommand).unwrap();
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[deprecated]
pub struct BitmapLegacyCommand;
impl TryFrom<Packet> for BitmapLegacyCommand {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
if let Some(e) =
check_command_code_only(value, CommandCode::BitmapLegacy)
{
Err(e)
} else {
Ok(Self)
}
}
}
impl From<BitmapLegacyCommand> for Packet {
fn from(value: BitmapLegacyCommand) -> Self {
Packet::from(&value)
}
}
impl From<&BitmapLegacyCommand> for Packet {
fn from(_: &BitmapLegacyCommand) -> Self {
Packet::command_code_only(CommandCode::BitmapLegacy)
}
}
impl From<BitmapLegacyCommand> for TypedCommand {
fn from(command: BitmapLegacyCommand) -> Self {
Self::BitmapLegacy(command)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{commands::tests::TestImplementsCommand, Header};
impl TestImplementsCommand for BitmapLegacyCommand {}
#[test]
fn invalid_fields() {
assert_eq!(
BitmapLegacyCommand::try_from(Packet {
header: Header {
command_code: CommandCode::BitmapLegacy.into(),
a: 1,
..Default::default()
},
payload: None,
}),
Err(TryFromPacketError::ExtraneousHeaderValues)
);
}
#[test]
fn round_trip() {
crate::commands::tests::round_trip(BitmapLegacyCommand.into());
}
#[test]
fn round_trip_ref() {
crate::commands::tests::round_trip_ref(&BitmapLegacyCommand.into());
}
}

417
src/commands/bitvec.rs Normal file
View file

@ -0,0 +1,417 @@
use crate::{
command_code::{CommandCode, InvalidCommandCodeError},
commands::errors::TryFromPacketError,
compression::{compress, decompress, CompressionError},
CompressionCode, DisplayBitVec, Header, Offset, Packet, TryIntoPacketError,
TypedCommand,
};
/// Binary operations for use with the [`BitVecCommand`] command.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Hash)]
#[repr(u8)]
pub enum BinaryOperation {
/// r := a
#[default]
Overwrite,
/// r := a && b
And,
/// r := a || b
Or,
/// r := (a || b) && (a != b)
Xor,
}
/// 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 [`BinaryOperation`] will be applied on the display comparing old and sent bit.
///
/// `new_bit = old_bit op sent_bit`
///
/// For example, [`BinaryOperation::Or`] can be used to turn on some pixels without affecting other pixels.
///
/// The contained [`DisplayBitVec`] is always uncompressed.
#[derive(Clone, PartialEq, Debug, Eq, Hash)]
pub struct BitVecCommand {
/// the pixels to send to the display as one long row
pub bitvec: DisplayBitVec,
/// where to start overwriting pixel data
pub offset: Offset,
/// The operation to apply on the display per bit comparing old and new state.
pub operation: BinaryOperation,
/// how to compress the command when converting to packet
pub compression: CompressionCode,
}
impl TryFrom<BitVecCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: BitVecCommand) -> Result<Self, Self::Error> {
Packet::try_from(&value)
}
}
impl TryFrom<&BitVecCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: &BitVecCommand) -> Result<Self, Self::Error> {
let command_code = match value.operation {
BinaryOperation::Overwrite => CommandCode::BitmapLinear,
BinaryOperation::And => CommandCode::BitmapLinearAnd,
BinaryOperation::Or => CommandCode::BitmapLinearOr,
BinaryOperation::Xor => CommandCode::BitmapLinearXor,
};
let data_ref = value.bitvec.as_raw_slice();
let length = data_ref.len().try_into()?;
let payload = match compress(value.compression, data_ref) {
Ok(payload) => payload,
Err(CompressionError::NoCompression) => data_ref.to_vec(),
Err(_) => return Err(TryIntoPacketError::CompressionFailed),
};
Ok(Packet {
header: Header {
command_code: command_code.into(),
a: value.offset.try_into()?,
b: length,
c: value.compression.into(),
d: 0,
},
payload: Some(payload),
})
}
}
impl TryFrom<Packet> for BitVecCommand {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header:
Header {
command_code,
a: offset,
b: expected_len,
c: sub,
d: reserved,
..
},
payload,
} = packet;
let command_code = CommandCode::try_from(command_code)?;
let operation = match command_code {
CommandCode::BitmapLinear => BinaryOperation::Overwrite,
CommandCode::BitmapLinearAnd => BinaryOperation::And,
CommandCode::BitmapLinearOr => BinaryOperation::Or,
CommandCode::BitmapLinearXor => BinaryOperation::Xor,
_ => {
return Err(InvalidCommandCodeError(command_code.into()).into());
}
};
if reserved != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
let compression = CompressionCode::try_from(sub)?;
let payload =
payload.ok_or(TryFromPacketError::UnexpectedPayloadSize {
expected: expected_len as usize,
actual: 0,
})?;
let payload = match decompress(compression, &payload) {
Ok(payload) => payload,
Err(CompressionError::NoCompression) => payload.clone(),
Err(_) => return Err(TryFromPacketError::DecompressionFailed),
};
if payload.len() != expected_len as usize {
return Err(TryFromPacketError::UnexpectedPayloadSize {
expected: expected_len as usize,
actual: payload.len(),
});
}
Ok(Self {
offset: offset as Offset,
bitvec: DisplayBitVec::from_vec(payload),
compression,
operation,
})
}
}
impl From<BitVecCommand> for TypedCommand {
fn from(command: BitVecCommand) -> Self {
Self::BitVec(command)
}
}
impl From<DisplayBitVec> for BitVecCommand {
fn from(bitvec: DisplayBitVec) -> Self {
Self {
bitvec,
operation: BinaryOperation::default(),
offset: Offset::default(),
compression: CompressionCode::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
commands, commands::tests::TestImplementsCommand,
compression_code::InvalidCompressionCodeError, PIXEL_WIDTH,
};
impl TestImplementsCommand for BitVecCommand {}
#[test]
fn command_code() {
assert_eq!(
BitVecCommand::try_from(Packet {
payload: None,
header: Header {
command_code: CommandCode::Brightness.into(),
..Default::default()
}
}),
Err(InvalidCommandCodeError(CommandCode::Brightness.into()).into())
);
}
#[test]
fn round_trip() {
for compression in CompressionCode::ALL {
for operation in [
BinaryOperation::Overwrite,
BinaryOperation::And,
BinaryOperation::Or,
BinaryOperation::Xor,
] {
crate::commands::tests::round_trip(
BitVecCommand {
offset: 23,
bitvec: DisplayBitVec::repeat(false, 40),
compression: *compression,
operation,
}
.into(),
);
}
}
}
#[test]
fn round_trip_ref() {
for compression in CompressionCode::ALL {
for operation in [
BinaryOperation::Overwrite,
BinaryOperation::And,
BinaryOperation::Or,
BinaryOperation::Xor,
] {
crate::commands::tests::round_trip(
BitVecCommand {
offset: 23,
bitvec: DisplayBitVec::repeat(false, 40),
compression: *compression,
operation,
}
.into(),
);
}
}
}
#[test]
fn error_decompression_failed_and() {
for compression in CompressionCode::ALL {
let p: Packet = commands::BitVecCommand {
offset: 0,
bitvec: DisplayBitVec::repeat(false, 8),
compression: *compression,
operation: BinaryOperation::Overwrite,
}
.try_into()
.unwrap();
let Packet { header, payload } = p;
let mut payload = payload.unwrap();
// mangle it
for byte in &mut payload {
*byte -= *byte / 2;
}
let p = Packet {
header,
payload: Some(payload),
};
let result = TypedCommand::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 error_reserved_used() {
let Packet { header, payload } = commands::BitVecCommand {
offset: 0,
bitvec: DisplayBitVec::repeat(false, 8),
compression: CompressionCode::Uncompressed,
operation: BinaryOperation::Or,
}
.try_into()
.unwrap();
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!(
TypedCommand::try_from(p),
Err(TryFromPacketError::ExtraneousHeaderValues)
);
}
#[test]
fn error_invalid_compression() {
let Packet { header, payload } = commands::BitVecCommand {
offset: 0,
bitvec: DisplayBitVec::repeat(false, 8),
compression: CompressionCode::Uncompressed,
operation: BinaryOperation::And,
}
.try_into()
.unwrap();
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!(
TypedCommand::try_from(p),
Err(InvalidCompressionCodeError(42).into())
);
}
#[test]
fn error_unexpected_size() {
let Packet { header, payload } = commands::BitVecCommand {
offset: 0,
bitvec: DisplayBitVec::repeat(false, 8),
compression: CompressionCode::Uncompressed,
operation: BinaryOperation::Xor,
}
.try_into()
.unwrap();
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!(
TypedCommand::try_from(p),
Err(TryFromPacketError::UnexpectedPayloadSize {
expected: 420,
actual: length as usize,
})
);
}
#[test]
fn into_command() {
let mut bitvec = DisplayBitVec::repeat(true, PIXEL_WIDTH);
bitvec.fill(true);
assert_eq!(
BitVecCommand::from(bitvec.clone()),
BitVecCommand {
bitvec,
offset: 0,
compression: CompressionCode::default(),
operation: BinaryOperation::Overwrite,
},
)
}
#[test]
fn into_packet_invalid_alignment() {
let mut cmd = BitVecCommand::from(DisplayBitVec::repeat(false, 32));
cmd.offset = 5;
cmd.compression = CompressionCode::Uncompressed;
let packet = Packet::try_from(cmd).unwrap();
assert_eq!(
packet.header,
Header {
command_code: 18,
a: 5,
b: 4,
c: 0,
d: 0
}
);
let cmd = BitVecCommand {
bitvec: DisplayBitVec::repeat(false, 32),
offset: 11,
operation: BinaryOperation::Overwrite,
compression: CompressionCode::Uncompressed,
};
let packet = Packet::try_from(cmd).unwrap();
assert_eq!(
packet.header,
Header {
command_code: 18,
a: 11,
b: 4,
c: 0,
d: 0
}
);
}
}

View file

@ -0,0 +1,201 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::errors::TryFromPacketError, BrightnessGrid, ByteGrid, Header,
Origin, Packet, Tiles, TryIntoPacketError, TypedCommand,
};
/// Set the brightness of individual tiles in a rectangular area of the display.
#[derive(Clone, PartialEq, Debug, Eq, Hash)]
pub struct BrightnessGridCommand {
/// the brightness values per tile
pub grid: BrightnessGrid,
/// which tile the brightness rectangle should start
pub origin: Origin<Tiles>,
}
impl TryFrom<BrightnessGridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: BrightnessGridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::CharBrightness,
)?)
}
}
impl TryFrom<&BrightnessGridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: &BrightnessGridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_as_packet(
value.origin,
&value.grid,
CommandCode::CharBrightness,
)?)
}
}
impl From<BrightnessGrid> for BrightnessGridCommand {
fn from(grid: BrightnessGrid) -> Self {
Self {
grid,
origin: Origin::default(),
}
}
}
impl TryFrom<Packet> for BrightnessGridCommand {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header:
Header {
command_code,
a: x,
b: y,
c: width,
d: height,
},
payload,
} = packet;
check_command_code(command_code, CommandCode::CharBrightness)?;
let expected_size = width as usize * height as usize;
let payload = match payload {
None => {
return Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 0,
expected: expected_size,
})
}
Some(payload) if payload.len() != expected_size => {
return Err(TryFromPacketError::UnexpectedPayloadSize {
actual: payload.len(),
expected: expected_size,
})
}
Some(payload) => payload,
};
let grid = ByteGrid::from_raw_parts_unchecked(
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(Self {
grid,
origin: Origin::new(x as usize, y as usize),
})
}
}
impl From<BrightnessGridCommand> for TypedCommand {
fn from(command: BrightnessGridCommand) -> Self {
Self::BrightnessGrid(command)
}
}
#[cfg(test)]
mod tests {
use crate::{
commands::{errors::TryFromPacketError, tests::TestImplementsCommand},
Brightness, BrightnessGrid, BrightnessGridCommand, Origin, Packet,
TypedCommand,
};
impl TestImplementsCommand for BrightnessGridCommand {}
#[test]
fn round_trip() {
crate::commands::tests::round_trip(
BrightnessGridCommand {
origin: Origin::new(5, 2),
grid: BrightnessGrid::new(7, 5),
}
.into(),
);
}
#[test]
fn round_trip_ref() {
crate::commands::tests::round_trip_ref(
&BrightnessGridCommand {
origin: Origin::new(5, 2),
grid: BrightnessGrid::new(7, 5),
}
.into(),
);
}
#[test]
fn packet_into_char_brightness_invalid() {
let grid = BrightnessGrid::new(2, 2);
let command = BrightnessGridCommand {
origin: Origin::ZERO,
grid,
};
let mut packet: Packet = command.try_into().unwrap();
let slot = packet.payload.as_mut().unwrap().get_mut(1).unwrap();
*slot = 23;
assert_eq!(
TypedCommand::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(23))
);
}
#[test]
fn into_command() {
let mut grid = BrightnessGrid::new(2, 3);
grid.iter_mut().enumerate().for_each(|(index, value)| {
*value = Brightness::saturating_from(index as u8)
});
assert_eq!(
BrightnessGridCommand::from(grid.clone()),
BrightnessGridCommand {
grid,
origin: Origin::default(),
},
)
}
#[test]
fn invalid_size() {
let command: BrightnessGridCommand = BrightnessGrid::new(2, 3).into();
let packet: Packet = command.try_into().unwrap();
let packet = Packet {
header: packet.header,
payload: Some(packet.payload.as_ref().unwrap()[..5].to_vec()),
};
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 5,
expected: 6
}),
BrightnessGridCommand::try_from(packet)
);
}
#[test]
fn missing_payload() {
let command: BrightnessGridCommand = BrightnessGrid::new(2, 3).into();
let mut packet: Packet = command.try_into().unwrap();
packet.payload = None;
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 0,
expected: 6
}),
BrightnessGridCommand::try_from(packet)
);
}
}

192
src/commands/char_grid.rs Normal file
View file

@ -0,0 +1,192 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::errors::TryFromPacketError, CharGrid, Header, Origin, Packet,
Tiles, TryIntoPacketError, TypedCommand,
};
/// 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::*;
/// # let connection = FakeConnection;
/// let grid = CharGrid::from("Hello,\nWorld!");
/// connection.send_command(CharGridCommand { origin: Origin::ZERO, grid }).expect("send failed");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CharGridCommand {
/// the text to send to the display
pub grid: CharGrid,
/// which tile the text should start on
pub origin: Origin<Tiles>,
}
impl TryFrom<CharGridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: CharGridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::Utf8Data,
)?)
}
}
impl TryFrom<&CharGridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: &CharGridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_as_packet(
value.origin,
&value.grid,
CommandCode::Utf8Data,
)?)
}
}
impl TryFrom<Packet> for CharGridCommand {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header:
Header {
command_code,
a: origin_x,
b: origin_y,
c: width,
d: height,
},
payload,
} = packet;
check_command_code(command_code, CommandCode::Utf8Data)?;
let expected = width as usize * height as usize;
let payload = match payload {
None => {
return Err(TryFromPacketError::UnexpectedPayloadSize {
expected,
actual: 0,
})
}
Some(payload) if payload.len() != expected => {
return Err(TryFromPacketError::UnexpectedPayloadSize {
expected,
actual: payload.len(),
})
}
Some(payload) => {
String::from_utf8(payload.clone())?.chars().collect()
}
};
Ok(Self {
origin: Origin::new(origin_x as usize, origin_y as usize),
grid: CharGrid::from_raw_parts_unchecked(
width as usize,
height as usize,
payload,
),
})
}
}
impl From<CharGridCommand> for TypedCommand {
fn from(command: CharGridCommand) -> Self {
Self::CharGrid(command)
}
}
impl From<CharGrid> for CharGridCommand {
fn from(grid: CharGrid) -> Self {
Self {
grid,
origin: Origin::default(),
}
}
}
#[cfg(test)]
mod tests {
use crate::{
commands::tests::TestImplementsCommand, CharGrid, CharGridCommand,
Origin, Packet, TryFromPacketError,
};
impl TestImplementsCommand for CharGridCommand {}
#[test]
fn round_trip() {
crate::commands::tests::round_trip(
CharGridCommand {
origin: Origin::new(5, 2),
grid: CharGrid::new(7, 5),
}
.into(),
);
}
#[test]
fn round_trip_ref() {
crate::commands::tests::round_trip_ref(
&CharGridCommand {
origin: Origin::new(5, 2),
grid: CharGrid::new(7, 5),
}
.into(),
);
}
#[test]
#[cfg(feature = "cp437")]
fn into_command() {
let mut grid = CharGrid::new(2, 3);
grid.iter_mut().enumerate().for_each(|(index, value)| {
*value = crate::cp437::cp437_to_char(index as u8)
});
assert_eq!(
CharGridCommand::from(grid.clone()),
CharGridCommand {
grid,
origin: Origin::default(),
},
)
}
#[test]
fn invalid_size() {
let command: CharGridCommand = CharGrid::new(2, 3).into();
let packet: Packet = command.try_into().unwrap();
let packet = Packet {
header: packet.header,
payload: Some(packet.payload.as_ref().unwrap()[..5].to_vec()),
};
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 5,
expected: 6
}),
CharGridCommand::try_from(packet)
);
}
#[test]
fn missing_payload() {
let command: CharGridCommand = CharGrid::new(2, 3).into();
let mut packet: Packet = command.try_into().unwrap();
packet.payload = None;
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 0,
expected: 6
}),
CharGridCommand::try_from(packet)
);
}
}

Some files were not shown because too many files have changed in this diff Show more