Compare commits

...

57 commits

Author SHA1 Message Date
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
51 changed files with 3034 additions and 2128 deletions

View file

@ -28,7 +28,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y liblzma-dev run: sudo apt-get update && sudo apt-get install -y liblzma-dev
- name: Run Clippy - name: Run Clippy
run: cargo clippy --all-targets --all-features run: cargo clippy --all-features
- name: no features -- test (without doctest) - name: no features -- test (without doctest)
run: cargo test --lib --no-default-features run: cargo test --lib --no-default-features

3
.gitignore vendored
View file

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

288
Cargo.lock generated
View file

@ -58,6 +58,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]] [[package]]
name = "bitvec" name = "bitvec"
version = "1.0.1" version = "1.0.1"
@ -70,52 +76,30 @@ dependencies = [
"wyz", "wyz",
] ]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]] [[package]]
name = "bzip2" name = "bzip2"
version = "0.5.1" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b89e7c29231c673a61a46e722602bcd138298f6b9e81e71119693534585f5c" checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
dependencies = [ dependencies = [
"bzip2-sys", "bzip2-sys",
] ]
[[package]] [[package]]
name = "bzip2-sys" name = "bzip2-sys"
version = "0.1.12+1.0.8" version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9" checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [ dependencies = [
"cc", "cc",
"libc",
"pkg-config", "pkg-config",
] ]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.14" version = "1.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -130,9 +114,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.29" version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -140,9 +124,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.29" version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -152,9 +136,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.28" version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -174,15 +158,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -192,48 +167,16 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
@ -241,24 +184,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "generic-array" name = "getrandom"
version = "0.14.7" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"typenum", "cfg-if",
"version_check", "libc",
"wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "r-efi",
"wasi 0.14.2+wasi-0.2.4",
] ]
[[package]] [[package]]
@ -267,104 +212,88 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.32" version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [ dependencies = [
"getrandom 0.3.2",
"libc", "libc",
] ]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.4" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [ dependencies = [
"adler2", "adler2",
] ]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.3" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.93" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]] [[package]]
name = "radium" name = "radium"
version = "0.7.0" version = "0.7.0"
@ -398,7 +327,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.16",
] ]
[[package]] [[package]]
@ -413,7 +342,7 @@ dependencies = [
[[package]] [[package]]
name = "servicepoint" name = "servicepoint"
version = "0.13.2" version = "0.14.0"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"bzip2", "bzip2",
@ -424,21 +353,9 @@ dependencies = [
"rand", "rand",
"rust-lzma", "rust-lzma",
"thiserror", "thiserror",
"tungstenite",
"zstd", "zstd",
] ]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -453,9 +370,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.98" version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -470,59 +387,29 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.11" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.11" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "tungstenite"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.16" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
@ -536,18 +423,21 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@ -621,6 +511,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "wyz" name = "wyz"
version = "0.5.1" version = "0.5.1"
@ -632,19 +531,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [ dependencies = [
"byteorder",
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.35" version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -653,27 +551,27 @@ dependencies = [
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe",
] ]
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "7.2.1" version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [ dependencies = [
"zstd-sys", "zstd-sys",
] ]
[[package]] [[package]]
name = "zstd-sys" name = "zstd-sys"
version = "2.0.13+zstd.1.5.6" version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "servicepoint" name = "servicepoint"
version = "0.13.2" version = "0.14.0"
publish = true publish = true
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@ -9,6 +9,7 @@ homepage = "https://docs.rs/crate/servicepoint"
repository = "https://git.berlin.ccc.de/servicepoint/servicepoint" repository = "https://git.berlin.ccc.de/servicepoint/servicepoint"
readme = "README.md" readme = "README.md"
keywords = ["cccb", "cccb-servicepoint"] keywords = ["cccb", "cccb-servicepoint"]
rust-version = "1.70.0"
[lib] [lib]
crate-type = ["rlib"] crate-type = ["rlib"]
@ -21,20 +22,17 @@ bzip2 = { version = "0.5", optional = true }
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }
rust-lzma = { version = "0.6", optional = true } rust-lzma = { version = "0.6", optional = true }
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
tungstenite = { version = "0.26", optional = true }
once_cell = { version = "1.20", optional = true } once_cell = { version = "1.20", optional = true }
thiserror = "2.0" thiserror = "2.0"
[features] [features]
default = ["compression_lzma", "protocol_udp", "cp437"] default = ["compression_lzma", "cp437"]
compression_zlib = ["dep:flate2"] compression_zlib = ["dep:flate2"]
compression_bzip2 = ["dep:bzip2"] compression_bzip2 = ["dep:bzip2"]
compression_lzma = ["dep:rust-lzma"] compression_lzma = ["dep:rust-lzma"]
compression_zstd = ["dep:zstd"] compression_zstd = ["dep:zstd"]
all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"] all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"]
rand = ["dep:rand"] rand = ["dep:rand"]
protocol_udp = []
protocol_websocket = ["dep:tungstenite"]
cp437 = ["dep:once_cell"] cp437 = ["dep:once_cell"]
[[example]] [[example]]
@ -45,16 +43,55 @@ required-features = ["rand"]
name = "game_of_life" name = "game_of_life"
required-features = ["rand"] required-features = ["rand"]
[[example]]
name = "websocket"
required-features = ["protocol_websocket"]
[dev-dependencies] [dev-dependencies]
# for examples # for examples
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
[lints.rust] [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"
[lints.clippy]
## 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] [package.metadata.docs.rs]
all-features = true 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

View file

@ -1,9 +1,11 @@
# 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".
@ -12,24 +14,22 @@ This crate contains a library for parsing, encoding and sending packets to this
The library itself is written in Rust, but can be used from multiple languages The library itself is written in Rust, but can be used from multiple languages
via [language bindings](#supported-language-bindings). via [language bindings](#supported-language-bindings).
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.
## Examples ## Examples
```rust no_run ```rust no_run
use std::net::UdpSocket;
// everything you need is in the top-level // everything you need is in the top-level
use servicepoint::*; use servicepoint::*;
fn main() { fn main() {
// establish connection // this should be the IP of the real display @CCCB
let connection = Connection::open("172.23.42.29:2342") let destination = "172.23.42.29:2342";
.expect("connection failed");
// clear screen content // establish connection
connection.send(Command::Clear) let connection = UdpSocket::bind(destination).expect("connection failed");
.expect("send failed");
// clear screen content using the UdpSocketExt
connection.send_command(ClearCommand).expect("send failed");
} }
``` ```
@ -46,7 +46,7 @@ or
```toml ```toml
[dependencies] [dependencies]
servicepoint = "0.13.2" servicepoint = "0.14.0"
``` ```
## Note on stability ## Note on stability
@ -61,21 +61,32 @@ There should be no breaking changes in patch releases, but there may also be fea
All of this means for you: please specify the full version including patch in your Cargo.toml until 1.0 is released. 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 ## Features
This library has multiple optional dependencies. This library has multiple optional dependencies.
You can choose to (not) include them by toggling the related features. You can choose to (not) include them by toggling the related features.
| Name | Default | Description | Dependencies | | Name | Default | Description | Dependencies |
|--------------------|---------|----------------------------------------------|-----------------------------------------------------| |-------------------|---------|----------------------------------------------|-------------------------------------------------|
| protocol_udp | true | `Connection::Udp` | | | cp437 | true | Conversion to and from CP-437 | [once_cell](https://crates.io/crates/once_cell) |
| 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_lzma | true | Enable additional compression algo | [rust-lzma](https://crates.io/crates/rust-lzma) | | compression_zlib | false | Enable additional compression algorithm | [flate2](https://crates.io/crates/flate2) |
| compression_zlib | false | Enable additional compression algo | [flate2](https://crates.io/crates/flate2) | | compression_bzip2 | false | Enable additional compression algorithm | [bzip2](https://crates.io/crates/bzip2) |
| compression_bzip2 | false | Enable additional compression algo | [bzip2](https://crates.io/crates/bzip2) | | compression_zstd | false | Enable additional compression algorithm | [zstd](https://crates.io/crates/zstd) |
| compression_zstd | false | Enable additional compression algo | [zstd](https://crates.io/crates/zstd) | | rand | false | `impl Distribution<Brightness> for Standard` | [rand](https://crates.io/crates/rand) |
| protocol_websocket | false | `Connection::WebSocket` | [tungstenite](https://crates.io/crates/tungstenite) |
| 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.14.0", 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 ## Supported language bindings
@ -91,17 +102,14 @@ You can choose to (not) include them by toggling the related features.
## Projects using the library ## Projects using the library
- screen simulator (rust): [servicepoint-simulator](https://git.berlin.ccc.de/servicepoint/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
- a CLI tool to display image files on the display or use the display as a TTY - [servicepoint-life](https://git.berlin.ccc.de/vinzenz/servicepoint-life): a cellular automata slideshow written in rust
- a BSD games robots clone - [servicepoint-cli](https://git.berlin.ccc.de/servicepoint/servicepoint-cli): a CLI that can:
- a split-flap-display simulator - share (stream) your screen
- animations that play on the display - send image files with dithering
- tanks game (C#): [servicepoint-tanks](https://github.com/kaesaecracker/cccb-tanks-cs) - clear the display
- cellular automata slideshow (rust): [servicepoint-life](https://github.com/kaesaecracker/servicepoint-life) - ...
- partial typescript implementation inspired by this library and browser
stream: [cccb-servicepoint-browser](https://github.com/SamuelScheit/cccb-servicepoint-browser)
- a CLI, can also share your screen: [servicepoint-cli](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
To add yourself to the list, open a pull request. To add yourself to the list, open a pull request.
@ -110,10 +118,28 @@ 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). 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 BSD games robots clone
- a split-flap-display simulator
- animations that play on the display
## 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,7 +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, CharGridCommand, ClearCommand, UdpSocketExt, TILE_WIDTH,
};
use std::net::UdpSocket;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -31,18 +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(&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::wrap_str(TILE_WIDTH, &text); let command: CharGridCommand = CharGrid::wrap_str(TILE_WIDTH, &text).into();
connection connection
.send(Command::Utf8Data(Origin::ZERO, grid)) .send_command(command)
.expect("sending text failed"); .expect("sending text failed");
} }

View file

@ -1,7 +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 {
@ -11,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 =
.expect("could not connect to display"); UdpSocket::bind(cli.destination).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::default(),
);
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");
} }

View file

@ -2,8 +2,8 @@
use clap::Parser; use clap::Parser;
use rand::{distributions, Rng}; use rand::{distributions, Rng};
use servicepoint::*; use servicepoint::{Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING};
use std::thread; use std::{net::UdpSocket, thread};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -16,17 +16,13 @@ struct Cli {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let connection = Connection::open(&cli.destination) let connection = UdpSocket::bind(&cli.destination)
.expect("could not connect to display"); .expect("could not connect to display");
let mut field = make_random_field(cli.probability); let mut field = make_random_field(cli.probability);
loop { loop {
let command = Command::BitmapLinearWin( let command = BitmapCommand::from(field.clone());
Origin::ZERO, connection.send_command(command).expect("could not send");
field.clone(),
CompressionCode::default(),
);
connection.send(command).expect("could not send");
thread::sleep(FRAME_PACING); thread::sleep(FRAME_PACING);
field = iteration(field); field = iteration(field);
} }
@ -39,10 +35,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);
} }
} }

View file

@ -1,8 +1,11 @@
//! A simple example for how to send pixel data to the display. //! A simple example for how to send pixel data to the display.
use clap::Parser; use clap::Parser;
use servicepoint::*; use servicepoint::{
use std::thread; Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT,
PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -11,23 +14,19 @@ struct Cli {
} }
fn main() { fn main() {
let connection = Connection::open(Cli::parse().destination) let connection = UdpSocket::bind(Cli::parse().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();
for x_offset in 0..usize::MAX { for x_offset in 0..usize::MAX {
pixels.fill(false); bitmap.fill(false);
for y in 0..PIXEL_HEIGHT { for y in 0..PIXEL_HEIGHT {
pixels.set((y + x_offset) % PIXEL_WIDTH, y, true); bitmap.set((y + x_offset) % PIXEL_WIDTH, y, true);
} }
let command = Command::BitmapLinearWin( let command = BitmapCommand::from(bitmap.clone());
Origin::ZERO, connection.send_command(command).expect("send failed");
pixels.clone(),
CompressionCode::default(),
);
connection.send(command).expect("send failed");
thread::sleep(FRAME_PACING); thread::sleep(FRAME_PACING);
} }
} }

View file

@ -3,8 +3,12 @@
use clap::Parser; use clap::Parser;
use rand::Rng; use rand::Rng;
use servicepoint::*; use servicepoint::{
use std::time::Duration; Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand,
GlobalBrightnessCommand, Grid, Origin, UdpSocketExt,
TILE_HEIGHT, TILE_WIDTH,
};
use std::{net::UdpSocket, time::Duration};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -19,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);
@ -28,17 +32,14 @@ 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 = Command::BitmapLinearWin( let command = BitmapCommand::from(filled_grid);
Origin::ZERO, connection.send_command(command).expect("send failed");
filled_grid,
CompressionCode::default(),
);
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::thread_rng();
connection.send(Command::Brightness(rng.gen())).unwrap(); let command: GlobalBrightnessCommand = rng.r#gen::<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 {
@ -54,12 +55,12 @@ fn main() {
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.r#gen());
} }
} }
connection connection
.send(Command::CharBrightness(origin, luma)) .send_command(BrightnessGridCommand { origin, grid: luma })
.unwrap(); .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(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,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::default(),
))
.unwrap();
}

View file

@ -1,8 +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 clap::Parser; use clap::Parser;
use servicepoint::*; use servicepoint::{
use std::thread; Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT,
use std::time::Duration; PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread, time::Duration};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
@ -20,8 +23,8 @@ 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 =
.expect("could not connect to display"); UdpSocket::bind(cli.destination).expect("could not connect to display");
let mut enabled_pixels = Bitmap::max_sized(); let mut enabled_pixels = Bitmap::max_sized();
enabled_pixels.fill(true); enabled_pixels.fill(true);
@ -31,12 +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( .send_command(command)
Origin::ZERO,
enabled_pixels.clone(),
CompressionCode::default(),
))
.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": 1736429655, "lastModified": 1745925850,
"narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=", "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce", "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -22,11 +22,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1739357830, "lastModified": 1746183838,
"narHash": "sha256-9xim3nJJUFbVbJCz48UP4fGRStVW5nv4VdbimbKxJ3I=", "narHash": "sha256-kwaaguGkAqTZ1oK0yXeQ3ayYjs8u/W7eEfrFpFfIDFA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0ff09db9d034a04acd4e8908820ba0b410d7a33a", "rev": "bf3287dac860542719fe7554e21e686108716879",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -137,6 +137,9 @@
clippy clippy
cargo-expand cargo-expand
cargo-tarpaulin cargo-tarpaulin
cargo-semver-checks
cargo-show-asm
cargo-flamegraph
]; ];
}) })
]; ];

2
generate-coverage Executable file
View file

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

View file

@ -1,10 +0,0 @@
/// A byte-packed vector of booleans.
///
/// The implementation is provided by [bitvec].
/// This is an alias for the specific type of [bitvec::BitVec] used in this crate.
pub type BitVec = bitvec::BitVec<u8, bitvec::Msb0>;
pub mod bitvec {
//! Re-export of the used library [mod@bitvec].
pub use bitvec::prelude::*;
}

View file

@ -9,15 +9,16 @@ use rand::{
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// # use servicepoint::{Brightness, Command, Connection}; /// # use servicepoint::*;
/// let b = Brightness::MAX; /// let b = Brightness::MAX;
/// let val: u8 = b.into(); /// let val: u8 = b.into();
/// ///
/// let b = Brightness::try_from(7).unwrap(); /// let b = Brightness::try_from(7).unwrap();
/// # let connection = Connection::Fake; /// # let connection = FakeConnection;
/// let result = connection.send(Command::Brightness(b)); /// let result = connection.send_command(GlobalBrightnessCommand::from(b));
/// ``` /// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct Brightness(u8); pub struct Brightness(u8);
impl From<Brightness> for u8 { impl From<Brightness> for u8 {
@ -50,9 +51,10 @@ impl Brightness {
/// lowest possible brightness value, 0 /// lowest possible brightness value, 0
pub const MIN: Brightness = Brightness(0); pub const MIN: Brightness = Brightness(0);
/// Create a brightness value without returning an error for brightnesses above [Brightness::MAX]. /// Create a brightness value without returning an error for brightnesses above [`Brightness::MAX`].
/// ///
/// returns: the specified value as a [Brightness], or [Brightness::MAX]. /// returns: the specified value as a [Brightness], or [`Brightness::MAX`].
#[must_use]
pub fn saturating_from(value: u8) -> Brightness { pub fn saturating_from(value: u8) -> Brightness {
if value > Brightness::MAX.into() { if value > Brightness::MAX.into() {
Brightness::MAX Brightness::MAX
@ -90,7 +92,7 @@ mod tests {
fn rand_brightness() { fn rand_brightness() {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
for _ in 0..100 { for _ in 0..100 {
let _: Brightness = rng.gen(); let _: Brightness = rng.r#gen();
} }
} }
@ -104,6 +106,10 @@ mod tests {
#[cfg(feature = "rand")] #[cfg(feature = "rand")]
fn test() { fn test() {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
assert_ne!(rng.gen::<Brightness>(), rng.gen()); // two so test failure is less likely
assert_ne!(
[rng.r#gen::<Brightness>(), rng.r#gen()],
[rng.r#gen(), rng.r#gen()]
);
} }
} }

View file

@ -1,968 +0,0 @@
use crate::command_code::CommandCode;
use crate::compression::into_decompressed;
use crate::*;
/// Type alias for documenting the meaning of the u16 in enum values
pub type Offset = usize;
/// A low-level display command.
///
/// This struct and associated functions implement the UDP protocol for the display.
///
/// To send a [Command], use a [connection][crate::Connection].
///
/// # Available commands
///
/// To send text, take a look at [Command::Cp437Data].
///
/// To draw pixels, the easiest command to use is [Command::BitmapLinearWin].
///
/// The other BitmapLinear-Commands operate on a region of pixel memory directly.
/// [Command::BitmapLinear] overwrites a region.
/// [Command::BitmapLinearOr], [Command::BitmapLinearAnd] and [Command::BitmapLinearXor] apply logical operations per pixel.
///
/// Out of bounds operations may be truncated or ignored by the display.
///
/// # Compression
///
/// Some commands can contain compressed payloads.
/// To get started, use [CompressionCode::default].
///
/// If you want to archive the best performance (e.g. latency),
/// you can try the different compression algorithms for your hardware and use case.
///
/// In memory, the payload is not compressed in the [Command].
/// Payload (de-)compression happens when converting the [Command] into a [Packet] or vice versa.
///
/// # Examples
///
/// ```rust
/// use servicepoint::{Brightness, Command, Connection, Packet};
/// #
/// // create command
/// let command = Command::Brightness(Brightness::MAX);
///
/// // turn command into Packet
/// let packet: Packet = command.clone().into();
///
/// // read command from packet
/// let round_tripped = Command::try_from(packet).unwrap();
///
/// // round tripping produces exact copy
/// assert_eq!(command, round_tripped);
///
/// // send command
/// # let connection = Connection::Fake;
/// 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::Fake;
/// connection.send(Command::Clear).unwrap();
/// ```
Clear,
/// Show text on the screen.
///
/// The text is sent in the form of a 2D grid of UTF-8 encoded characters (the default encoding in rust).
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, Connection, Origin, CharGrid};
/// # let connection = Connection::Fake;
/// let grid = CharGrid::from("Hello,\nWorld!");
/// connection.send(Command::Utf8Data(Origin::ZERO, grid)).expect("send failed");
/// ```
Utf8Data(Origin<Tiles>, CharGrid),
/// Show text on the screen.
///
/// The text is sent in the form of a 2D grid of [CP-437] encoded characters.
///
/// <div class="warning">You probably want to use [Command::Utf8Data] instead</div>
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, Connection, Origin, CharGrid, Cp437Grid};
/// # let connection = Connection::Fake;
/// let grid = CharGrid::from("Hello,\nWorld!");
/// let grid = Cp437Grid::from(&grid);
/// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed");
/// ```
///
/// ```rust
/// # use servicepoint::{Command, Connection, Cp437Grid, Origin};
/// # let connection = Connection::Fake;
/// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
/// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
/// ```
/// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437
Cp437Data(Origin<Tiles>, Cp437Grid),
/// Overwrites a rectangular region of pixels.
///
/// Origin coordinates must be divisible by 8.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
/// # let connection = servicepoint::Connection::Fake;
/// #
/// let mut pixels = Bitmap::max_sized();
/// // draw something to the pixels here
/// # pixels.set(2, 5, true);
///
/// // create command to send pixels
/// let command = Command::BitmapLinearWin(
/// servicepoint::Origin::ZERO,
/// pixels,
/// CompressionCode::default()
/// );
///
/// 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::Fake;
/// 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::Fake;
/// 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::Fake;
/// 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::Fake;
/// // this sends a packet that does nothing
/// # #[allow(deprecated)]
/// connection.send(Command::BitmapLegacy).unwrap();
/// ```
#[deprecated]
BitmapLegacy,
}
/// Err values for [Command::try_from].
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum TryFromPacketError {
/// the contained command code does not correspond to a known command
#[error("The command code {0:?} does not correspond to a known command")]
InvalidCommand(u16),
/// the expected payload size was n, but size m was found
#[error("the expected payload size was {0}, but size {1} was found")]
UnexpectedPayloadSize(usize, usize),
/// Header fields not needed for the command have been used.
///
/// Note that these commands would usually still work on the actual display.
#[error("Header fields not needed for the command have been used")]
ExtraneousHeaderValues,
/// The contained compression code is not known. This could be of disabled features.
#[error("The compression code {0:?} does not correspond to a known compression algorithm.")]
InvalidCompressionCode(u16),
/// Decompression of the payload failed. This can be caused by corrupted packets.
#[error("The decompression of the payload failed")]
DecompressionFailed,
/// The given brightness value is out of bounds
#[error("The given brightness value {0} is out of bounds.")]
InvalidBrightness(u8),
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
}
impl TryFrom<Packet> for Command {
type Error = TryFromPacketError;
/// Try to interpret the [Packet] as one containing a [Command]
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header: Header {
command_code, a, ..
},
..
} = packet;
let command_code = match CommandCode::try_from(command_code) {
Err(()) => {
return Err(TryFromPacketError::InvalidCommand(command_code));
}
Ok(value) => value,
};
match command_code {
CommandCode::Clear => {
Self::packet_into_command_only(packet, Command::Clear)
}
CommandCode::Brightness => Self::packet_into_brightness(&packet),
CommandCode::HardReset => {
Self::packet_into_command_only(packet, Command::HardReset)
}
CommandCode::FadeOut => {
Self::packet_into_command_only(packet, Command::FadeOut)
}
CommandCode::Cp437Data => Self::packet_into_cp437(&packet),
CommandCode::CharBrightness => {
Self::packet_into_char_brightness(&packet)
}
CommandCode::Utf8Data => Self::packet_into_utf8(&packet),
#[allow(deprecated)]
CommandCode::BitmapLegacy => Ok(Command::BitmapLegacy),
CommandCode::BitmapLinear => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinear(a as Offset, vec, compression))
}
CommandCode::BitmapLinearAnd => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinearAnd(a as Offset, vec, compression))
}
CommandCode::BitmapLinearOr => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinearOr(a as Offset, vec, compression))
}
CommandCode::BitmapLinearXor => {
let (vec, compression) =
Self::packet_into_linear_bitmap(packet)?;
Ok(Command::BitmapLinearXor(a as Offset, vec, compression))
}
CommandCode::BitmapLinearWinUncompressed => {
Self::packet_into_bitmap_win(
packet,
CompressionCode::Uncompressed,
)
}
#[cfg(feature = "compression_zlib")]
CommandCode::BitmapLinearWinZlib => {
Self::packet_into_bitmap_win(packet, CompressionCode::Zlib)
}
#[cfg(feature = "compression_bzip2")]
CommandCode::BitmapLinearWinBzip2 => {
Self::packet_into_bitmap_win(packet, CompressionCode::Bzip2)
}
#[cfg(feature = "compression_lzma")]
CommandCode::BitmapLinearWinLzma => {
Self::packet_into_bitmap_win(packet, CompressionCode::Lzma)
}
#[cfg(feature = "compression_zstd")]
CommandCode::BitmapLinearWinZstd => {
Self::packet_into_bitmap_win(packet, CompressionCode::Zstd)
}
}
}
}
impl Command {
fn packet_into_bitmap_win(
packet: Packet,
compression: CompressionCode,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a: tiles_x,
b: pixels_y,
c: tile_w,
d: pixel_h,
},
payload,
} = packet;
let payload = match into_decompressed(compression, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(decompressed) => decompressed,
};
Ok(Command::BitmapLinearWin(
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize),
Bitmap::load(
tile_w as usize * TILE_SIZE,
pixel_h as usize,
&payload,
),
compression,
))
}
/// Helper method for checking that a packet is empty and only contains a command code
fn packet_into_command_only(
packet: Packet,
command: Command,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
if !payload.is_empty() {
Err(TryFromPacketError::UnexpectedPayloadSize(0, payload.len()))
} else if a != 0 || b != 0 || c != 0 || d != 0 {
Err(TryFromPacketError::ExtraneousHeaderValues)
} else {
Ok(command)
}
}
/// Helper method for Packets into `BitmapLinear*`-Commands
fn packet_into_linear_bitmap(
packet: Packet,
) -> Result<(BitVec, CompressionCode), TryFromPacketError> {
let Packet {
header:
Header {
b: length,
c: sub,
d: reserved,
..
},
payload,
} = packet;
if reserved != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
let sub = match CompressionCode::try_from(sub) {
Err(()) => {
return Err(TryFromPacketError::InvalidCompressionCode(sub));
}
Ok(value) => value,
};
let payload = match into_decompressed(sub, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(value) => value,
};
if payload.len() != length as usize {
return Err(TryFromPacketError::UnexpectedPayloadSize(
length as usize,
payload.len(),
));
}
Ok((BitVec::from_vec(payload), sub))
}
fn packet_into_char_brightness(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a: x,
b: y,
c: width,
d: height,
},
payload,
} = packet;
let grid = ByteGrid::load(*width as usize, *height as usize, payload);
let grid = match BrightnessGrid::try_from(grid) {
Ok(grid) => grid,
Err(val) => return Err(TryFromPacketError::InvalidBrightness(val)),
};
Ok(Command::CharBrightness(
Origin::new(*x as usize, *y as usize),
grid,
))
}
fn packet_into_brightness(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
if payload.len() != 1 {
return Err(TryFromPacketError::UnexpectedPayloadSize(
1,
payload.len(),
));
}
if *a != 0 || *b != 0 || *c != 0 || *d != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
match Brightness::try_from(payload[0]) {
Ok(b) => Ok(Command::Brightness(b)),
Err(_) => Err(TryFromPacketError::InvalidBrightness(payload[0])),
}
}
fn packet_into_cp437(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
Ok(Command::Cp437Data(
Origin::new(*a as usize, *b as usize),
Cp437Grid::load(*c as usize, *d as usize, payload),
))
}
fn packet_into_utf8(
packet: &Packet,
) -> Result<Command, TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
let payload: Vec<_> =
String::from_utf8(payload.clone())?.chars().collect();
Ok(Command::Utf8Data(
Origin::new(*a as usize, *b as usize),
CharGrid::load(*c as usize, *d as usize, &payload),
))
}
}
#[cfg(test)]
mod tests {
use crate::command::TryFromPacketError;
use crate::command_code::CommandCode;
use crate::{
BitVec, Bitmap, Brightness, BrightnessGrid, CharGrid, Command,
CompressionCode, Cp437Grid, Header, Origin, Packet, Pixels,
};
fn round_trip(original: Command) {
let packet: Packet = original.clone().into();
let copy: Command = match Command::try_from(packet) {
Ok(command) => command,
Err(err) => panic!("could not reload {original:?}: {err:?}"),
};
assert_eq!(copy, original);
}
fn all_compressions<'t>() -> &'t [CompressionCode] {
&[
CompressionCode::Uncompressed,
#[cfg(feature = "compression_lzma")]
CompressionCode::Lzma,
#[cfg(feature = "compression_bzip2")]
CompressionCode::Bzip2,
#[cfg(feature = "compression_zlib")]
CompressionCode::Zlib,
#[cfg(feature = "compression_zstd")]
CompressionCode::Zstd,
]
}
#[test]
fn round_trip_clear() {
round_trip(Command::Clear);
}
#[test]
fn round_trip_hard_reset() {
round_trip(Command::HardReset);
}
#[test]
fn round_trip_fade_out() {
round_trip(Command::FadeOut);
}
#[test]
fn round_trip_brightness() {
round_trip(Command::Brightness(Brightness::try_from(6).unwrap()));
}
#[test]
#[allow(deprecated)]
fn round_trip_bitmap_legacy() {
round_trip(Command::BitmapLegacy);
}
#[test]
fn round_trip_char_brightness() {
round_trip(Command::CharBrightness(
Origin::new(5, 2),
BrightnessGrid::new(7, 5),
));
}
#[test]
fn round_trip_cp437_data() {
round_trip(Command::Cp437Data(Origin::new(5, 2), Cp437Grid::new(7, 5)));
}
#[test]
fn round_trip_utf8_data() {
round_trip(Command::Utf8Data(Origin::new(5, 2), CharGrid::new(7, 5)));
}
#[test]
fn round_trip_bitmap_linear() {
for compression in all_compressions().iter().copied() {
round_trip(Command::BitmapLinear(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearAnd(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearOr(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearXor(
23,
BitVec::repeat(false, 40),
compression,
));
round_trip(Command::BitmapLinearWin(
Origin::ZERO,
Bitmap::max_sized(),
compression,
));
}
}
#[test]
fn error_invalid_command() {
let p = Packet {
header: Header {
command_code: 0xFF,
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::InvalidCommand(0xFF))
))
}
#[test]
fn error_extraneous_header_values_clear() {
let p = Packet {
header: Header {
command_code: CommandCode::Clear.into(),
a: 0x05,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_extraneous_header_values_brightness() {
let p = Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0x00,
b: 0x13,
c: 0x37,
d: 0x00,
},
payload: vec![5],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_extraneous_header_hard_reset() {
let p = Packet {
header: Header {
command_code: CommandCode::HardReset.into(),
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x01,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_extraneous_header_fade_out() {
let p = Packet {
header: Header {
command_code: CommandCode::FadeOut.into(),
a: 0x10,
b: 0x00,
c: 0x00,
d: 0x01,
},
payload: vec![],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
))
}
#[test]
fn error_unexpected_payload() {
let p = Packet {
header: Header {
command_code: CommandCode::FadeOut.into(),
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![5, 7],
};
let result = Command::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::UnexpectedPayloadSize(0, 2))
))
}
#[test]
fn error_decompression_failed_win() {
for compression in all_compressions().iter().copied() {
let p: Packet = Command::BitmapLinearWin(
Origin::new(16, 8),
Bitmap::new(8, 8),
compression,
)
.into();
let Packet {
header,
mut payload,
} = p;
// mangle it
for byte in payload.iter_mut() {
*byte -= *byte / 2;
}
let p = Packet { header, payload };
let result = Command::try_from(p);
if compression != CompressionCode::Uncompressed {
assert_eq!(result, Err(TryFromPacketError::DecompressionFailed))
} else {
assert!(result.is_ok());
}
}
}
#[test]
fn error_decompression_failed_and() {
for compression in all_compressions().iter().copied() {
let p: Packet = Command::BitmapLinearAnd(
0,
BitVec::repeat(false, 8),
compression,
)
.into();
let Packet {
header,
mut payload,
} = p;
// mangle it
for byte in payload.iter_mut() {
*byte -= *byte / 2;
}
let p = Packet { header, payload };
let result = Command::try_from(p);
if compression != CompressionCode::Uncompressed {
assert_eq!(result, Err(TryFromPacketError::DecompressionFailed))
} else {
// when not compressing, there is no way to detect corrupted data
assert!(result.is_ok());
}
}
}
#[test]
fn unexpected_payload_size_brightness() {
assert_eq!(
Command::try_from(Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0,
b: 0,
c: 0,
d: 0,
},
payload: vec!()
}),
Err(TryFromPacketError::UnexpectedPayloadSize(1, 0))
);
assert_eq!(
Command::try_from(Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0,
b: 0,
c: 0,
d: 0,
},
payload: vec!(0, 0)
}),
Err(TryFromPacketError::UnexpectedPayloadSize(1, 2))
);
}
#[test]
fn error_reserved_used() {
let Packet { header, payload } = Command::BitmapLinear(
0,
BitVec::repeat(false, 8),
CompressionCode::Uncompressed,
)
.into();
let Header {
command_code: command,
a: offset,
b: length,
c: sub,
d: _reserved,
} = header;
let p = Packet {
header: Header {
command_code: command,
a: offset,
b: length,
c: sub,
d: 69,
},
payload,
};
assert_eq!(
Command::try_from(p),
Err(TryFromPacketError::ExtraneousHeaderValues)
);
}
#[test]
fn error_invalid_compression() {
let Packet { header, payload } = Command::BitmapLinear(
0,
BitVec::repeat(false, 8),
CompressionCode::Uncompressed,
)
.into();
let Header {
command_code: command,
a: offset,
b: length,
c: _sub,
d: reserved,
} = header;
let p = Packet {
header: Header {
command_code: command,
a: offset,
b: length,
c: 42,
d: reserved,
},
payload,
};
assert_eq!(
Command::try_from(p),
Err(TryFromPacketError::InvalidCompressionCode(42))
);
}
#[test]
fn error_unexpected_size() {
let Packet { header, payload } = Command::BitmapLinear(
0,
BitVec::repeat(false, 8),
CompressionCode::Uncompressed,
)
.into();
let Header {
command_code: command,
a: offset,
b: length,
c: compression,
d: reserved,
} = header;
let p = Packet {
header: Header {
command_code: command,
a: offset,
b: 420,
c: compression,
d: reserved,
},
payload,
};
assert_eq!(
Command::try_from(p),
Err(TryFromPacketError::UnexpectedPayloadSize(
420,
length as usize,
))
);
}
#[test]
fn origin_add() {
assert_eq!(
Origin::<Pixels>::new(4, 2),
Origin::new(1, 0) + Origin::new(3, 2)
);
}
#[test]
fn packet_into_char_brightness_invalid() {
let grid = BrightnessGrid::new(2, 2);
let command = Command::CharBrightness(Origin::ZERO, grid);
let mut packet: Packet = command.into();
let slot = packet.payload.get_mut(1).unwrap();
*slot = 23;
assert_eq!(
Command::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(23))
);
}
#[test]
fn packet_into_brightness_invalid() {
let mut packet: Packet = Command::Brightness(Brightness::MAX).into();
let slot = packet.payload.get_mut(0).unwrap();
*slot = 42;
assert_eq!(
Command::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(42))
);
}
}

View file

@ -1,7 +1,8 @@
/// The u16 command codes used for the [Command]s. /// The u16 command codes used for the [Command]s.
#[repr(u16)] #[repr(u16)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(crate) enum CommandCode { #[allow(missing_docs)]
pub enum CommandCode {
Clear = 0x0002, Clear = 0x0002,
Cp437Data = 0x0003, Cp437Data = 0x0003,
CharBrightness = 0x0005, CharBrightness = 0x0005,
@ -33,8 +34,12 @@ impl From<CommandCode> for 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 { impl TryFrom<u16> for CommandCode {
type Error = (); type Error = InvalidCommandCodeError;
/// Returns the enum value for the specified `u16` or `Error` if the code is unknown. /// Returns the enum value for the specified `u16` or `Error` if the code is unknown.
fn try_from(value: u16) -> Result<Self, Self::Error> { fn try_from(value: u16) -> Result<Self, Self::Error> {
@ -97,7 +102,7 @@ impl TryFrom<u16> for CommandCode {
value if value == CommandCode::Utf8Data as u16 => { value if value == CommandCode::Utf8Data as u16 => {
Ok(CommandCode::Utf8Data) Ok(CommandCode::Utf8Data)
} }
_ => Err(()), _ => Err(InvalidCommandCodeError(value)),
} }
} }
} }

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

@ -0,0 +1,244 @@
use crate::{
command_code::{CommandCode, InvalidCommandCodeError},
commands::errors::{TryFromPacketError, TryIntoPacketError},
compression::into_compressed,
compression::into_decompressed,
Bitmap, CompressionCode, 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)]
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> {
assert_eq!(value.origin.x % 8, 0);
assert_eq!(value.bitmap.width() % 8, 0);
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 payload = into_compressed(value.compression, value.bitmap.into())
.ok_or(TryIntoPacketError::CompressionFailed)?;
let command = match value.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,
};
Ok(Packet {
header: Header {
command_code: command.into(),
a: tile_x,
b: value.origin.y.try_into()?,
c: tile_w,
d: pixel_h,
},
payload,
})
}
}
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)?;
match code {
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)
}
_ => {
Err(InvalidCommandCodeError(packet.header.command_code).into())
}
}
}
}
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 packet_into_bitmap_win(
packet: Packet,
compression: CompressionCode,
) -> Result<Self, 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,
};
let bitmap = Bitmap::load(
tile_w as usize * TILE_SIZE,
pixel_h as usize,
&payload,
)?;
Ok(Self {
origin: Origin::new(
tiles_x as usize * TILE_SIZE,
pixels_y as usize,
),
bitmap,
compression,
})
}
}
#[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: vec![],
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,
mut payload,
} = p;
// mangle it
for byte in &mut payload {
*byte -= *byte / 2;
}
let p = Packet { header, 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()
},
)
}
}

View file

@ -0,0 +1,83 @@
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
/// # #[allow(deprecated)]
/// connection.send_command(BitmapLegacyCommand).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[deprecated]
pub struct BitmapLegacyCommand;
#[allow(deprecated)]
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)
}
}
}
#[allow(deprecated)]
impl From<BitmapLegacyCommand> for Packet {
fn from(_: BitmapLegacyCommand) -> Self {
Packet::command_code_only(CommandCode::BitmapLegacy)
}
}
#[allow(deprecated)]
impl From<BitmapLegacyCommand> for TypedCommand {
fn from(command: BitmapLegacyCommand) -> Self {
Self::BitmapLegacy(command)
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::{
commands::tests::{round_trip, 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: vec![],
}),
Err(TryFromPacketError::ExtraneousHeaderValues)
);
}
#[test]
fn round_trip_bitmap_legacy() {
round_trip(BitmapLegacyCommand.into());
}
}

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

@ -0,0 +1,350 @@
use crate::{
command_code::CommandCode, command_code::InvalidCommandCodeError,
commands::errors::TryFromPacketError, compression::into_compressed,
compression::into_decompressed, DisplayBitVec, CompressionCode, Header,
Offset, Packet, TryIntoPacketError, TypedCommand,
};
/// Binary operations for use with the [`BitVecCommand`] command.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[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)]
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> {
let command_code = match value.operation {
BinaryOperation::Overwrite => CommandCode::BitmapLinear,
BinaryOperation::And => CommandCode::BitmapLinearAnd,
BinaryOperation::Or => CommandCode::BitmapLinearOr,
BinaryOperation::Xor => CommandCode::BitmapLinearXor,
};
let payload: Vec<_> = value.bitvec.into();
let length = payload.len().try_into()?;
let payload = into_compressed(value.compression, payload)
.ok_or(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,
})
}
}
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 = match into_decompressed(compression, payload) {
None => return Err(TryFromPacketError::DecompressionFailed),
Some(value) => value,
};
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::{round_trip, TestImplementsCommand},
compression_code::InvalidCompressionCodeError,
Bitmap, BitmapCommand, Origin, PIXEL_WIDTH,
};
impl TestImplementsCommand for BitVecCommand {}
#[test]
fn command_code() {
assert_eq!(
BitVecCommand::try_from(Packet {
payload: vec![],
header: Header {
command_code: CommandCode::Brightness.into(),
..Default::default()
}
}),
Err(InvalidCommandCodeError(CommandCode::Brightness.into()).into())
);
}
#[test]
fn round_trip_bitmap_linear() {
for compression in CompressionCode::ALL {
for operation in [
BinaryOperation::Overwrite,
BinaryOperation::And,
BinaryOperation::Or,
BinaryOperation::Xor,
] {
round_trip(
BitVecCommand {
offset: 23,
bitvec: DisplayBitVec::repeat(false, 40),
compression: *compression,
operation,
}
.into(),
);
}
round_trip(
BitmapCommand {
origin: Origin::ZERO,
bitmap: Bitmap::max_sized(),
compression: *compression,
}
.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,
mut payload,
} = p;
// mangle it
for byte in &mut payload {
*byte -= *byte / 2;
}
let p = Packet { header, 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,
},
)
}
}

View file

@ -0,0 +1,158 @@
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)]
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 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;
if payload.len() != expected_size {
return Err(TryFromPacketError::UnexpectedPayloadSize {
actual: payload.len(),
expected: expected_size,
});
}
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::{round_trip, TestImplementsCommand},
},
Brightness, BrightnessGrid, BrightnessGridCommand, Origin, Packet,
TypedCommand,
};
impl TestImplementsCommand for BrightnessGridCommand {}
#[test]
fn round_trip_char_brightness() {
round_trip(
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.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: packet.payload[..5].to_vec(),
};
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 5,
expected: 6
}),
BrightnessGridCommand::try_from(packet)
);
}
}

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

@ -0,0 +1,147 @@
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)]
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<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 payload: Vec<_> =
String::from_utf8(payload.clone())?.chars().collect();
let expected = width as usize * height as usize;
if payload.len() != expected {
return Err(TryFromPacketError::UnexpectedPayloadSize {
expected,
actual: payload.len(),
});
}
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::{round_trip, TestImplementsCommand},
CharGrid, CharGridCommand, Origin, Packet, TryFromPacketError,
};
impl TestImplementsCommand for CharGridCommand {}
#[test]
fn round_trip_utf8_data() {
round_trip(
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: packet.payload[..5].to_vec(),
};
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 5,
expected: 6
}),
CharGridCommand::try_from(packet)
);
}
}

94
src/commands/clear.rs Normal file
View file

@ -0,0 +1,94 @@
use crate::{
command_code::CommandCode,
commands::{check_command_code_only, errors::TryFromPacketError},
Packet, TypedCommand,
};
use std::fmt::Debug;
/// Set all pixels to the off state. Does not affect brightness.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// connection.send_command(ClearCommand).unwrap();
#[derive(Debug, Clone, PartialEq, Eq)]
/// ```
pub struct ClearCommand;
impl TryFrom<Packet> for ClearCommand {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
if let Some(e) = check_command_code_only(value, CommandCode::Clear) {
Err(e)
} else {
Ok(Self)
}
}
}
impl From<ClearCommand> for Packet {
fn from(_: ClearCommand) -> Self {
Packet::command_code_only(CommandCode::Clear)
}
}
impl From<ClearCommand> for TypedCommand {
fn from(command: ClearCommand) -> Self {
Self::Clear(command)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command_code::InvalidCommandCodeError;
use crate::commands::tests::TestImplementsCommand;
use crate::Header;
impl TestImplementsCommand for ClearCommand {}
#[test]
fn round_trip() {
crate::commands::tests::round_trip(ClearCommand.into());
}
#[test]
fn extraneous_header_values() {
let p = Packet {
header: Header {
command_code: CommandCode::Clear.into(),
a: 0x05,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![],
};
let result = TypedCommand::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
));
}
#[test]
fn invalid_command_code() {
let p = Packet {
header: Header {
command_code: CommandCode::HardReset.into(),
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x00,
},
payload: vec![],
};
assert_eq!(
Err(InvalidCommandCodeError(CommandCode::HardReset.into()).into()),
ClearCommand::try_from(p)
);
}
}

152
src/commands/cp437_grid.rs Normal file
View file

@ -0,0 +1,152 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::errors::TryFromPacketError, Cp437Grid, Header, Origin, Packet,
Tiles, TryIntoPacketError, TypedCommand,
};
/// Show text on the screen.
///
/// The text is sent in the form of a 2D grid of [CP-437] encoded characters.
///
/// <div class="warning">You probably want to use [Command::Utf8Data] instead</div>
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// let grid = CharGrid::from("Hello,\nWorld!");
/// let grid = Cp437Grid::from(&grid);
/// connection.send_command(Cp437GridCommand{ origin: Origin::ZERO, grid }).expect("send failed");
/// ```
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
/// connection.send_command(Cp437GridCommand{ origin: Origin::new(2, 2), grid }).unwrap();
/// ```
/// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cp437GridCommand {
/// the text to send to the display
pub grid: Cp437Grid,
/// which tile the text should start
pub origin: Origin<Tiles>,
}
impl TryFrom<Cp437GridCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: Cp437GridCommand) -> Result<Self, Self::Error> {
Ok(Packet::origin_grid_to_packet(
value.origin,
value.grid,
CommandCode::Cp437Data,
)?)
}
}
impl TryFrom<Packet> for Cp437GridCommand {
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::Cp437Data)?;
let expected = width as usize * height as usize;
if payload.len() != expected {
return Err(TryFromPacketError::UnexpectedPayloadSize {
expected,
actual: payload.len(),
});
}
Ok(Self {
origin: Origin::new(origin_x as usize, origin_y as usize),
grid: Cp437Grid::from_raw_parts_unchecked(
width as usize,
height as usize,
payload,
),
})
}
}
impl From<Cp437GridCommand> for TypedCommand {
fn from(command: Cp437GridCommand) -> Self {
Self::Cp437Grid(command)
}
}
impl From<Cp437Grid> for Cp437GridCommand {
fn from(grid: Cp437Grid) -> Self {
Self {
grid,
origin: Origin::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::tests::{round_trip, TestImplementsCommand};
impl TestImplementsCommand for Cp437GridCommand {}
#[test]
fn round_trip_cp437_data() {
round_trip(
Cp437GridCommand {
origin: Origin::new(5, 2),
grid: Cp437Grid::new(7, 5),
}
.into(),
);
}
#[test]
fn into_command() {
let mut grid = Cp437Grid::new(2, 3);
grid.iter_mut()
.enumerate()
.for_each(|(index, value)| *value = index as u8);
assert_eq!(
Cp437GridCommand::from(grid.clone()),
Cp437GridCommand {
grid,
origin: Origin::default(),
},
)
}
#[test]
fn invalid_size() {
let command: Cp437GridCommand = Cp437Grid::new(2, 3).into();
let packet: Packet = command.try_into().unwrap();
let packet = Packet {
header: packet.header,
payload: packet.payload[..5].to_vec(),
};
assert_eq!(
Err(TryFromPacketError::UnexpectedPayloadSize {
actual: 5,
expected: 6
}),
Cp437GridCommand::try_from(packet)
);
}
}

54
src/commands/errors.rs Normal file
View file

@ -0,0 +1,54 @@
use crate::{
command_code::InvalidCommandCodeError,
compression_code::InvalidCompressionCodeError, LoadBitmapError,
};
use std::num::TryFromIntError;
/// Err values for [`crate::TypedCommand::try_from`].
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum TryFromPacketError {
/// the contained command code does not correspond to a known command
#[error(transparent)]
InvalidCommand(#[from] InvalidCommandCodeError),
/// the expected payload size was n, but size m was found
#[error(
"the expected payload size was {actual}, but size {expected} was found"
)]
UnexpectedPayloadSize {
/// size of the provided payload
actual: usize,
/// expected size according to command or header values
expected: 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(transparent)]
InvalidCompression(#[from] InvalidCompressionCodeError),
/// 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),
/// Some provided text was not valid UTF-8.
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
/// The bitmap contained in the payload could not be loaded
#[error(transparent)]
LoadBitmapFailed(#[from] LoadBitmapError),
}
/// An error that can occur when parsing a raw packet as a command
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum TryIntoPacketError {
/// Compression of the payload failed.
#[error("The compression of the payload failed")]
CompressionFailed,
/// Conversion (probably to u16) failed
#[error(transparent)]
ConversionError(#[from] TryFromIntError),
}

103
src/commands/fade_out.rs Normal file
View file

@ -0,0 +1,103 @@
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
/// <div class="warning">Untested</div>
///
/// Slowly decrease brightness until off or something like that?
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// connection.send_command(FadeOutCommand).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FadeOutCommand;
impl TryFrom<Packet> for FadeOutCommand {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
if let Some(e) = check_command_code_only(value, CommandCode::FadeOut) {
Err(e)
} else {
Ok(Self)
}
}
}
impl From<FadeOutCommand> for Packet {
fn from(_: FadeOutCommand) -> Self {
Packet::command_code_only(CommandCode::FadeOut)
}
}
impl From<FadeOutCommand> for TypedCommand {
fn from(command: FadeOutCommand) -> Self {
Self::FadeOut(command)
}
}
#[cfg(test)]
mod tests {
use crate::{
command_code::CommandCode,
commands::{
errors::TryFromPacketError,
tests::{round_trip, TestImplementsCommand},
},
FadeOutCommand, Header, Packet, TypedCommand,
};
impl TestImplementsCommand for FadeOutCommand {}
#[test]
fn round_trip_fade_out() {
round_trip(FadeOutCommand.into());
}
#[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 = TypedCommand::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 = TypedCommand::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::UnexpectedPayloadSize {
expected: 0,
actual: 2
})
));
}
}

View file

@ -0,0 +1,198 @@
use crate::{
command_code::CommandCode, commands::check_command_code,
commands::errors::TryFromPacketError, Brightness, Header, Packet,
TypedCommand,
};
/// Set the brightness of all tiles to the same value.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// let command = GlobalBrightnessCommand { brightness: Brightness::MAX };
/// connection.send_command(command).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobalBrightnessCommand {
/// the brightness to set all pixels to
pub brightness: Brightness,
}
impl From<GlobalBrightnessCommand> for Packet {
fn from(command: GlobalBrightnessCommand) -> Self {
Self {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0x00000,
b: 0x0000,
c: 0x0000,
d: 0x0000,
},
payload: vec![command.brightness.into()],
}
}
}
impl TryFrom<Packet> for GlobalBrightnessCommand {
type Error = TryFromPacketError;
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
let Packet {
header:
Header {
command_code,
a,
b,
c,
d,
},
payload,
} = packet;
check_command_code(command_code, CommandCode::Brightness)?;
if payload.len() != 1 {
return Err(TryFromPacketError::UnexpectedPayloadSize {
expected: 1,
actual: payload.len(),
});
}
if a != 0 || b != 0 || c != 0 || d != 0 {
return Err(TryFromPacketError::ExtraneousHeaderValues);
}
match Brightness::try_from(payload[0]) {
Ok(brightness) => Ok(Self { brightness }),
Err(_) => Err(TryFromPacketError::InvalidBrightness(payload[0])),
}
}
}
impl From<GlobalBrightnessCommand> for TypedCommand {
fn from(command: GlobalBrightnessCommand) -> Self {
Self::Brightness(command)
}
}
impl From<Brightness> for GlobalBrightnessCommand {
fn from(brightness: Brightness) -> Self {
GlobalBrightnessCommand { brightness }
}
}
#[cfg(test)]
mod tests {
use crate::{
command_code::CommandCode,
commands::{
errors::TryFromPacketError,
tests::{round_trip, TestImplementsCommand},
},
Brightness, GlobalBrightnessCommand, Header, Packet, TypedCommand,
};
impl TestImplementsCommand for GlobalBrightnessCommand {}
#[test]
fn brightness_as_command() {
assert_eq!(
GlobalBrightnessCommand {
brightness: Brightness::MAX
},
Brightness::MAX.into()
);
}
#[test]
fn round_trip_brightness() {
round_trip(
GlobalBrightnessCommand {
brightness: Brightness::try_from(6).unwrap(),
}
.into(),
);
}
#[test]
fn error_extraneous_header_values() {
let p = Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0x00,
b: 0x13,
c: 0x37,
d: 0x00,
},
payload: vec![5],
};
let result = TypedCommand::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
));
}
#[test]
fn unexpected_payload_size_brightness() {
assert_eq!(
TypedCommand::try_from(Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0,
b: 0,
c: 0,
d: 0,
},
payload: vec!()
}),
Err(TryFromPacketError::UnexpectedPayloadSize {
expected: 1,
actual: 0
})
);
assert_eq!(
TypedCommand::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 {
expected: 1,
actual: 2
})
);
}
#[test]
fn packet_into_brightness_invalid() {
let mut packet: Packet = GlobalBrightnessCommand {
brightness: Brightness::MAX,
}
.into();
let slot = packet.payload.get_mut(0).unwrap();
*slot = 42;
assert_eq!(
TypedCommand::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(42))
);
}
#[test]
fn into_command() {
assert_eq!(
GlobalBrightnessCommand::from(Brightness::MIN),
GlobalBrightnessCommand {
brightness: Brightness::MIN,
},
)
}
}

View file

@ -0,0 +1,77 @@
use crate::{
command_code::CommandCode, commands::check_command_code_only,
commands::errors::TryFromPacketError, Packet, TypedCommand,
};
use std::fmt::Debug;
/// 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::*;
/// # let connection = FakeConnection;
/// connection.send_command(HardResetCommand).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HardResetCommand;
impl TryFrom<Packet> for HardResetCommand {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
if let Some(e) = check_command_code_only(value, CommandCode::HardReset)
{
Err(e)
} else {
Ok(Self)
}
}
}
impl From<HardResetCommand> for Packet {
fn from(_: HardResetCommand) -> Self {
Packet::command_code_only(CommandCode::HardReset)
}
}
impl From<HardResetCommand> for TypedCommand {
fn from(command: HardResetCommand) -> Self {
Self::HardReset(command)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::commands::tests::{round_trip, TestImplementsCommand};
use crate::Header;
impl TestImplementsCommand for HardResetCommand {}
#[test]
fn round_trip_hard_reset() {
round_trip(HardResetCommand.into());
}
#[test]
fn error_extraneous_header() {
let p = Packet {
header: Header {
command_code: CommandCode::HardReset.into(),
a: 0x00,
b: 0x00,
c: 0x00,
d: 0x01,
},
payload: vec![],
};
let result = TypedCommand::try_from(p);
assert!(matches!(
result,
Err(TryFromPacketError::ExtraneousHeaderValues)
));
}
}

143
src/commands/mod.rs Normal file
View file

@ -0,0 +1,143 @@
mod bitmap;
mod bitmap_legacy;
mod bitvec;
mod brightness_grid;
mod char_grid;
mod clear;
mod cp437_grid;
mod errors;
mod fade_out;
mod global_brightness;
mod hard_reset;
mod typed;
use crate::command_code::{CommandCode, InvalidCommandCodeError};
use crate::{Header, Packet};
use std::fmt::Debug;
pub use bitmap::*;
pub use bitmap_legacy::*;
pub use bitvec::*;
pub use brightness_grid::*;
pub use char_grid::*;
pub use clear::*;
pub use cp437_grid::*;
pub use errors::*;
pub use fade_out::*;
pub use global_brightness::*;
pub use hard_reset::*;
pub use typed::*;
/// This trait represents a command that can be sent to the display.
///
/// To send a [Command], use a [connection][crate::Connection].
///
/// # Available commands
///
/// To send text, take a look at [`Cp437GridCommand`].
///
/// To draw pixels, the easiest command to use is [`BitmapCommand`].
///
/// The other BitmapLinear-Commands operate on a region of pixel memory directly.
/// [`BitVecCommand`] overwrites a region or applies a logical operation per pixel with [`BinaryOperation`].
///
/// Out of bounds operations may be truncated or ignored by the display.
///
/// # Compression
///
/// Some commands can contain compressed payloads.
/// To get started, use [`CompressionCode::default()`].
///
/// 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::*;
///
/// // create command
/// let command = GlobalBrightnessCommand{ brightness: Brightness::MAX };
///
/// // turn command into Packet
/// let packet: Packet = command.clone().into();
///
/// // read command from packet
/// let round_tripped = TypedCommand::try_from(packet).unwrap();
///
/// // round tripping produces exact copy
/// assert_eq!(round_tripped, TypedCommand::from(command.clone()));
///
/// // send command
/// # let connection = FakeConnection;
/// connection.send_command(command).unwrap();
/// ```
pub trait Command:
Debug + Clone + Eq + TryInto<Packet> + TryFrom<Packet>
{
}
impl<T: Debug + Clone + Eq + TryInto<Packet> + TryFrom<Packet>> Command for T {}
fn check_command_code_only(
packet: Packet,
code: CommandCode,
) -> Option<TryFromPacketError> {
let Packet {
header:
Header {
command_code: _,
a,
b,
c,
d,
},
payload,
} = packet;
if packet.header.command_code != u16::from(code) {
Some(InvalidCommandCodeError(packet.header.command_code).into())
} else if !payload.is_empty() {
Some(TryFromPacketError::UnexpectedPayloadSize {
expected: 0,
actual: payload.len(),
})
} else if a != 0 || b != 0 || c != 0 || d != 0 {
Some(TryFromPacketError::ExtraneousHeaderValues)
} else {
None
}
}
fn check_command_code(
actual: u16,
expected: CommandCode,
) -> Result<(), InvalidCommandCodeError> {
if actual == u16::from(expected) {
Ok(())
} else {
Err(InvalidCommandCodeError(actual))
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[allow(
unused,
reason = "false positive, used in submodules that check if structs impl Command"
)]
pub(crate) trait TestImplementsCommand: Command {}
pub(crate) fn round_trip(original: TypedCommand) {
let packet: Packet = original.clone().try_into().unwrap();
let copy: TypedCommand = match TypedCommand::try_from(packet) {
Ok(command) => command,
Err(err) => panic!("could not reload {original:?}: {err:?}"),
};
assert_eq!(copy, original);
}
}

133
src/commands/typed.rs Normal file
View file

@ -0,0 +1,133 @@
use crate::{
command_code::CommandCode, commands::errors::TryFromPacketError,
BitVecCommand, BitmapCommand, BrightnessGridCommand, CharGridCommand,
ClearCommand, Cp437GridCommand, FadeOutCommand, GlobalBrightnessCommand,
HardResetCommand, Packet, TryIntoPacketError,
};
/// This enum contains all commands provided by the library.
/// This is useful in case you want one data type for all kinds of commands without using `dyn`.
///
/// Please look at the contained structs for documentation per command.
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(missing_docs)]
#[allow(deprecated)]
pub enum TypedCommand {
Clear(ClearCommand),
CharGrid(CharGridCommand),
Cp437Grid(Cp437GridCommand),
Bitmap(BitmapCommand),
Brightness(GlobalBrightnessCommand),
BrightnessGrid(BrightnessGridCommand),
BitVec(BitVecCommand),
HardReset(HardResetCommand),
FadeOut(FadeOutCommand),
#[deprecated]
BitmapLegacy(crate::BitmapLegacyCommand),
}
impl TryFrom<Packet> for TypedCommand {
type Error = TryFromPacketError;
/// Try to interpret the [Packet] as one containing a [`TypedCommand`]
fn try_from(packet: Packet) -> Result<Self, Self::Error> {
Ok(match CommandCode::try_from(packet.header.command_code)? {
CommandCode::Clear => {
TypedCommand::Clear(crate::ClearCommand::try_from(packet)?)
}
CommandCode::Brightness => TypedCommand::Brightness(
crate::GlobalBrightnessCommand::try_from(packet)?,
),
CommandCode::HardReset => TypedCommand::HardReset(
crate::HardResetCommand::try_from(packet)?,
),
CommandCode::FadeOut => {
TypedCommand::FadeOut(crate::FadeOutCommand::try_from(packet)?)
}
CommandCode::Cp437Data => TypedCommand::Cp437Grid(
crate::Cp437GridCommand::try_from(packet)?,
),
CommandCode::CharBrightness => TypedCommand::BrightnessGrid(
crate::BrightnessGridCommand::try_from(packet)?,
),
CommandCode::Utf8Data => TypedCommand::CharGrid(
crate::CharGridCommand::try_from(packet)?,
),
#[allow(deprecated)]
CommandCode::BitmapLegacy => TypedCommand::BitmapLegacy(
crate::BitmapLegacyCommand::try_from(packet)?,
),
CommandCode::BitmapLinear
| CommandCode::BitmapLinearOr
| CommandCode::BitmapLinearAnd
| CommandCode::BitmapLinearXor => {
TypedCommand::BitVec(crate::BitVecCommand::try_from(packet)?)
}
CommandCode::BitmapLinearWinUncompressed => {
TypedCommand::Bitmap(crate::BitmapCommand::try_from(packet)?)
}
#[cfg(feature = "compression_zlib")]
CommandCode::BitmapLinearWinZlib => {
TypedCommand::Bitmap(crate::BitmapCommand::try_from(packet)?)
}
#[cfg(feature = "compression_bzip2")]
CommandCode::BitmapLinearWinBzip2 => {
TypedCommand::Bitmap(crate::BitmapCommand::try_from(packet)?)
}
#[cfg(feature = "compression_lzma")]
CommandCode::BitmapLinearWinLzma => {
TypedCommand::Bitmap(crate::BitmapCommand::try_from(packet)?)
}
#[cfg(feature = "compression_zstd")]
CommandCode::BitmapLinearWinZstd => {
TypedCommand::Bitmap(crate::BitmapCommand::try_from(packet)?)
}
})
}
}
impl TryFrom<TypedCommand> for Packet {
type Error = TryIntoPacketError;
fn try_from(value: TypedCommand) -> Result<Self, Self::Error> {
Ok(match value {
TypedCommand::Clear(c) => c.into(),
TypedCommand::CharGrid(c) => c.try_into()?,
TypedCommand::Cp437Grid(c) => c.try_into()?,
TypedCommand::Bitmap(c) => c.try_into()?,
TypedCommand::Brightness(c) => c.into(),
TypedCommand::BrightnessGrid(c) => c.try_into()?,
TypedCommand::BitVec(c) => c.try_into()?,
TypedCommand::HardReset(c) => c.into(),
TypedCommand::FadeOut(c) => c.into(),
#[allow(deprecated)]
TypedCommand::BitmapLegacy(c) => c.into(),
})
}
}
#[cfg(test)]
mod tests {
use crate::{
command_code::InvalidCommandCodeError,
commands::tests::TestImplementsCommand, Header, Packet, TypedCommand,
};
impl TestImplementsCommand for TypedCommand {}
#[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 = TypedCommand::try_from(p);
assert_eq!(result, Err(InvalidCommandCodeError(0xFF).into()));
}
}

View file

@ -1,10 +1,11 @@
#[allow(unused)]
use std::io::{Read, Write};
#[cfg(feature = "compression_bzip2")] #[cfg(feature = "compression_bzip2")]
use bzip2::read::{BzDecoder, BzEncoder}; use bzip2::read::{BzDecoder, BzEncoder};
#[cfg(feature = "compression_zlib")] #[cfg(feature = "compression_zlib")]
use flate2::{FlushCompress, FlushDecompress, Status}; use flate2::{FlushCompress, FlushDecompress, Status};
#[allow(unused, reason = "used depending on enabled features")]
use log::error;
#[allow(unused, reason = "used depending on enabled features")]
use std::io::{Read, Write};
#[cfg(feature = "compression_zstd")] #[cfg(feature = "compression_zstd")]
use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder};
@ -21,21 +22,31 @@ pub(crate) fn into_decompressed(
let mut decompress = flate2::Decompress::new(true); let mut decompress = flate2::Decompress::new(true);
let mut buffer = [0u8; 10000]; let mut buffer = [0u8; 10000];
let status = match decompress.decompress( match decompress.decompress(
&payload, &payload,
&mut buffer, &mut buffer,
FlushDecompress::Finish, FlushDecompress::Finish,
) { ) {
Err(_) => return None, Ok(Status::Ok) => {
Ok(status) => status, error!("input not big enough");
}; None
}
match status { Ok(Status::BufError) => {
Status::Ok => None, error!("output buffer is too small");
Status::BufError => None, None
Status::StreamEnd => Some( }
buffer[0..(decompress.total_out() as usize)].to_owned(), Ok(Status::StreamEnd) =>
), {
#[allow(
clippy::cast_possible_truncation,
reason = "can never be larger than the fixed buffer size"
)]
Some(buffer[..decompress.total_out() as usize].to_owned())
}
Err(e) => {
error!("failed to decompress data: {e}");
None
}
} }
} }
#[cfg(feature = "compression_bzip2")] #[cfg(feature = "compression_bzip2")]
@ -43,24 +54,36 @@ pub(crate) fn into_decompressed(
let mut decoder = BzDecoder::new(&*payload); let mut decoder = BzDecoder::new(&*payload);
let mut decompressed = vec![]; let mut decompressed = vec![];
match decoder.read_to_end(&mut decompressed) { match decoder.read_to_end(&mut decompressed) {
Err(_) => None,
Ok(_) => Some(decompressed), Ok(_) => Some(decompressed),
Err(e) => {
error!("failed to decompress data: {e}");
None
}
} }
} }
#[cfg(feature = "compression_lzma")] #[cfg(feature = "compression_lzma")]
CompressionCode::Lzma => match lzma::decompress(&payload) { CompressionCode::Lzma => match lzma::decompress(&payload) {
Err(_) => None, Err(e) => {
Ok(decompressed) => Some(decompressed), error!("failed to decompress data: {e}");
None
}
Ok(result) => Some(result),
}, },
#[cfg(feature = "compression_zstd")] #[cfg(feature = "compression_zstd")]
CompressionCode::Zstd => { CompressionCode::Zstd => {
let mut decoder = match ZstdDecoder::new(&*payload) { let mut decoder = match ZstdDecoder::new(&*payload) {
Err(_) => return None,
Ok(value) => value, Ok(value) => value,
Err(e) => {
error!("failed to create zstd decoder: {e}");
return None;
}
}; };
let mut decompressed = vec![]; let mut decompressed = vec![];
match decoder.read_to_end(&mut decompressed) { match decoder.read_to_end(&mut decompressed) {
Err(_) => None, Err(e) => {
error!("failed to decompress data: {e}");
None
}
Ok(_) => Some(decompressed), Ok(_) => Some(decompressed),
} }
} }
@ -70,24 +93,41 @@ pub(crate) fn into_decompressed(
pub(crate) fn into_compressed( pub(crate) fn into_compressed(
kind: CompressionCode, kind: CompressionCode,
payload: Payload, payload: Payload,
) -> Payload { ) -> Option<Payload> {
match kind { match kind {
CompressionCode::Uncompressed => payload, CompressionCode::Uncompressed => Some(payload),
#[cfg(feature = "compression_zlib")] #[cfg(feature = "compression_zlib")]
CompressionCode::Zlib => { CompressionCode::Zlib => {
let mut compress = let mut compress =
flate2::Compress::new(flate2::Compression::fast(), true); flate2::Compress::new(flate2::Compression::fast(), true);
let mut buffer = [0u8; 10000]; let mut buffer = [0u8; 10000];
match compress match compress.compress(
.compress(&payload, &mut buffer, FlushCompress::Finish) &payload,
.expect("compress failed") &mut buffer,
{ FlushCompress::Finish,
Status::Ok => panic!("buffer should be big enough"), ) {
Status::BufError => panic!("BufError"), Ok(Status::Ok) => {
Status::StreamEnd => {} error!("output buffer not big enough");
}; None
buffer[..compress.total_out() as usize].to_owned() }
Ok(Status::BufError) => {
error!("Could not compress with buffer error");
None
}
Ok(Status::StreamEnd) =>
{
#[allow(
clippy::cast_possible_truncation,
reason = "can never be larger than the fixed buffer size"
)]
Some(buffer[..compress.total_out() as usize].to_owned())
}
Err(e) => {
error!("failed to compress data: {e}");
None
}
}
} }
#[cfg(feature = "compression_bzip2")] #[cfg(feature = "compression_bzip2")]
CompressionCode::Bzip2 => { CompressionCode::Bzip2 => {
@ -95,21 +135,39 @@ pub(crate) fn into_compressed(
BzEncoder::new(&*payload, bzip2::Compression::fast()); BzEncoder::new(&*payload, bzip2::Compression::fast());
let mut compressed = vec![]; let mut compressed = vec![];
match encoder.read_to_end(&mut compressed) { match encoder.read_to_end(&mut compressed) {
Err(err) => panic!("could not compress payload: {}", err), Err(e) => {
Ok(_) => compressed, error!("failed to compress data: {e}");
None
}
Ok(_) => Some(compressed),
} }
} }
#[cfg(feature = "compression_lzma")] #[cfg(feature = "compression_lzma")]
CompressionCode::Lzma => lzma::compress(&payload, 6).unwrap(), CompressionCode::Lzma => match lzma::compress(&payload, 6) {
Ok(payload) => Some(payload),
Err(e) => {
error!("failed to compress data: {e}");
None
}
},
#[cfg(feature = "compression_zstd")] #[cfg(feature = "compression_zstd")]
CompressionCode::Zstd => { CompressionCode::Zstd => {
let buf = Vec::with_capacity(payload.len());
let mut encoder = let mut encoder =
ZstdEncoder::new(vec![], zstd::DEFAULT_COMPRESSION_LEVEL) match ZstdEncoder::new(buf, zstd::DEFAULT_COMPRESSION_LEVEL) {
.expect("could not create encoder"); Err(e) => {
encoder error!("failed to create zstd encoder: {e}");
.write_all(&payload) return None;
.expect("could not compress payload"); }
encoder.finish().expect("could not finish encoding") Ok(encoder) => encoder,
};
if let Err(e) = encoder.write_all(&payload) {
error!("failed to compress data: {e}");
return None;
}
encoder.finish().ok()
} }
} }
} }

View file

@ -3,17 +3,25 @@
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use servicepoint::{Command, CompressionCode, Origin, Bitmap}; /// # use servicepoint::*;
/// // create command without payload compression /// // create command without payload compression
/// # let pixels = Bitmap::max_sized(); /// # let pixels = Bitmap::max_sized();
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Uncompressed); /// _ = BitmapCommand {
/// origin: Origin::ZERO,
/// bitmap: pixels,
/// compression: CompressionCode::Uncompressed
/// };
/// ///
/// // create command with payload compressed with lzma and appropriate header flags /// // create command with payload compressed with lzma and appropriate header flags
/// # let pixels = Bitmap::max_sized(); /// # let pixels = Bitmap::max_sized();
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Lzma); /// _ = BitmapCommand {
/// origin: Origin::ZERO,
/// bitmap: pixels,
/// compression: CompressionCode::Lzma
/// };
/// ``` /// ```
#[repr(u16)] #[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionCode { pub enum CompressionCode {
/// no compression /// no compression
Uncompressed = 0x0, Uncompressed = 0x0,
@ -31,6 +39,25 @@ pub enum CompressionCode {
Zstd = 0x7a73, Zstd = 0x7a73,
} }
impl CompressionCode {
/// All available compression codes (depending on features).
pub const ALL: &'static [CompressionCode] = &[
Self::Uncompressed,
#[cfg(feature = "compression_zlib")]
Self::Zlib,
#[cfg(feature = "compression_bzip2")]
Self::Bzip2,
#[cfg(feature = "compression_lzma")]
Self::Lzma,
#[cfg(feature = "compression_zstd")]
Self::Zstd,
];
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
#[error("The compression code {0} is not known.")]
pub struct InvalidCompressionCodeError(pub u16);
impl From<CompressionCode> for u16 { impl From<CompressionCode> for u16 {
fn from(value: CompressionCode) -> Self { fn from(value: CompressionCode) -> Self {
value as u16 value as u16
@ -38,7 +65,7 @@ impl From<CompressionCode> for u16 {
} }
impl TryFrom<u16> for CompressionCode { impl TryFrom<u16> for CompressionCode {
type Error = (); type Error = InvalidCompressionCodeError;
fn try_from(value: u16) -> Result<Self, Self::Error> { fn try_from(value: u16) -> Result<Self, Self::Error> {
match value { match value {
@ -61,7 +88,7 @@ impl TryFrom<u16> for CompressionCode {
value if value == CompressionCode::Zstd as u16 => { value if value == CompressionCode::Zstd as u16 => {
Ok(CompressionCode::Zstd) Ok(CompressionCode::Zstd)
} }
_ => Err(()), _ => Err(InvalidCompressionCodeError(value)),
} }
} }
} }

View file

@ -1,175 +1,40 @@
use crate::packet::Packet; use crate::Packet;
use std::fmt::Debug; use std::net::{Ipv4Addr, ToSocketAddrs};
use std::{convert::TryInto, net::UdpSocket};
/// A connection to the display. /// Provides servicepoint specific extensions for `UdpSocket`
/// pub trait UdpSocketExt {
/// Used to send [Packets][Packet] or [Commands][crate::Command]. /// Creates a `UdpSocket` that can be used so send to the specified addr.
/// fn bind_connect(addr: impl ToSocketAddrs) -> std::io::Result<UdpSocket>;
/// # 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. /// Serializes the command and sends it through the socket
/// fn send_command(&self, command: impl TryInto<Packet>) -> Option<()>;
/// 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)] impl UdpSocketExt for UdpSocket {
pub enum SendError { fn bind_connect(addr: impl ToSocketAddrs) -> std::io::Result<UdpSocket> {
#[error("IO error occurred while sending")] let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
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)?; socket.connect(addr)?;
Ok(Self::Udp(socket)) Ok(socket)
} }
/// Open a new WebSocket and connect to the provided host. fn send_command(&self, command: impl TryInto<Packet>) -> Option<()> {
/// let packet = command.try_into().ok()?;
/// Requires the feature "protocol_websocket" which is disabled by default. let vec: Vec<_> = packet.into();
/// self.send(&vec).ok()?;
/// # Examples Some(())
///
/// ```no_run
/// use tungstenite::http::Uri;
/// use servicepoint::{Command, Connection};
/// let uri = "ws://localhost:8080".parse().unwrap();
/// let mut connection = Connection::open_websocket(uri)
/// .expect("could not connect");
/// connection.send(Command::Clear)
/// .expect("send failed");
/// ```
#[cfg(feature = "protocol_websocket")]
pub fn open_websocket(
uri: tungstenite::http::Uri,
) -> tungstenite::Result<Self> {
use tungstenite::{
client::IntoClientRequest, connect, ClientRequestBuilder,
};
log::info!("connecting to {uri:?}");
let request = ClientRequestBuilder::new(uri).into_client_request()?;
let (sock, _) = connect(request)?;
Ok(Self::WebSocket(std::sync::Mutex::new(sock)))
}
/// Send something packet-like to the display. Usually this is in the form of a Command.
///
/// # Arguments
///
/// - `packet`: the packet-like to send
///
/// returns: true if packet was sent, otherwise false
///
/// # Examples
///
/// ```rust
/// let connection = servicepoint::Connection::Fake;
/// // turn off all pixels on display
/// connection.send(servicepoint::Command::Clear)
/// .expect("send failed");
/// ```
pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> {
let packet = packet.into();
log::debug!("sending {packet:?}");
let data: Vec<u8> = packet.into();
match self {
#[cfg(feature = "protocol_udp")]
Connection::Udp(socket) => {
socket
.send(&data)
.map_err(SendError::IoError)
.map(move |_| ()) // ignore Ok value
}
#[cfg(feature = "protocol_websocket")]
Connection::WebSocket(socket) => {
let mut socket = socket.lock().unwrap();
socket
.send(tungstenite::Message::Binary(data.into()))
.map_err(SendError::WebsocketError)
}
Connection::Fake => {
let _ = data;
Ok(())
}
}
} }
} }
impl Drop for Connection { /// A fake connection for testing that does not actually send anything.
fn drop(&mut self) { pub struct FakeConnection;
#[cfg(feature = "protocol_websocket")]
if let Connection::WebSocket(sock) = self { impl FakeConnection {
_ = sock.try_lock().map(move |mut sock| sock.close(None)); /// Serializes the command, but does not actually send it as this is the fake connection
} pub fn send_command(&self, command: impl TryInto<Packet>) -> Option<()> {
} _ = self; // suppress unused warning
} let packet = command.try_into().ok()?;
drop(Vec::from(packet));
#[cfg(test)] Some(())
mod tests {
use super::*;
#[test]
fn send_fake() {
let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let packet = Packet::try_from(data).unwrap();
Connection::Fake.send(packet).unwrap()
} }
} }

View file

@ -52,19 +52,19 @@ pub const PIXEL_COUNT: usize = PIXEL_WIDTH * PIXEL_HEIGHT;
/// ///
/// ```rust /// ```rust
/// # use std::time::Instant; /// # use std::time::Instant;
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, Bitmap}; /// # use servicepoint::*;
/// # let connection = servicepoint::Connection::Fake; /// # let connection = FakeConnection;
/// # let pixels = Bitmap::max_sized(); /// # let pixels = Bitmap::max_sized();
/// loop { /// loop {
/// let start = Instant::now(); /// let start = Instant::now();
/// ///
/// // Change pixels here /// // Change pixels here
/// ///
/// connection.send(Command::BitmapLinearWin( /// connection.send_command(BitmapCommand {
/// Origin::new(0,0), /// origin: Origin::new(0,0),
/// pixels, /// bitmap: pixels,
/// CompressionCode::default() /// compression: CompressionCode::default()
/// )) /// })
/// .expect("send failed"); /// .expect("send failed");
/// ///
/// // warning: will crash if resulting duration is negative, e.g. when resuming from standby /// // warning: will crash if resulting duration is negative, e.g. when resuming from standby

10
src/containers/bit_vec.rs Normal file
View file

@ -0,0 +1,10 @@
/// A byte-packed vector of booleans.
///
/// The implementation is provided by [bitvec].
/// This is an alias for the specific type of [`bitvec::BitVec`] used in this crate.
pub type DisplayBitVec = bitvec::BitVec<u8, bitvec::Msb0>;
pub mod bitvec {
//! Re-export of the used library [`::bitvec`].
pub use ::bitvec::prelude::*;
}

View file

@ -1,9 +1,7 @@
use crate::data_ref::DataRef; use crate::{
use crate::BitVec; DisplayBitVec, DataRef, Grid, ValueGrid, PIXEL_HEIGHT, PIXEL_WIDTH,
use crate::*; };
use ::bitvec::order::Msb0; use ::bitvec::{order::Msb0, prelude::BitSlice, slice::IterMut};
use ::bitvec::prelude::BitSlice;
use ::bitvec::slice::IterMut;
/// A fixed-size 2D grid of booleans. /// A fixed-size 2D grid of booleans.
/// ///
@ -22,100 +20,106 @@ use ::bitvec::slice::IterMut;
pub struct Bitmap { pub struct Bitmap {
width: usize, width: usize,
height: usize, height: usize,
bit_vec: BitVec, bit_vec: DisplayBitVec,
} }
impl Bitmap { impl Bitmap {
/// Creates a new [Bitmap] with the specified dimensions. /// Creates a new [`Bitmap`] with the specified dimensions.
/// The initial state of the contained pixels is false.
///
/// The width has to be a multiple of [`crate::TILE_SIZE`], otherwise this function returns None.
/// ///
/// # Arguments /// # Arguments
/// ///
/// - `width`: size in pixels in x-direction /// - `width`: size in pixels in x-direction
/// - `height`: size in pixels in y-direction /// - `height`: size in pixels in y-direction
/// #[must_use]
/// returns: [Bitmap] initialized to all pixels off pub fn new(width: usize, height: usize) -> Option<Self> {
/// assert!(width < isize::MAX as usize);
/// # Panics assert!(height < isize::MAX as usize);
/// if width % 8 != 0 {
/// - when the width is not dividable by 8 return None;
pub fn new(width: usize, height: usize) -> Self {
assert_eq!(
width % 8,
0,
"width must be a multiple of 8, but is {width}"
);
Self {
width,
height,
bit_vec: BitVec::repeat(false, width * height),
} }
Some(Self::new_unchecked(width, height))
} }
/// Creates a new pixel grid with the size of the whole screen. /// Creates a new pixel grid with the size of the whole screen.
#[must_use] #[must_use]
pub fn max_sized() -> Self { pub fn max_sized() -> Self {
Self::new(PIXEL_WIDTH, PIXEL_HEIGHT) Self::new_unchecked(PIXEL_WIDTH, PIXEL_HEIGHT)
}
#[must_use]
fn new_unchecked(width: usize, height: usize) -> Self {
Self {
width,
height,
bit_vec: DisplayBitVec::repeat(false, width * height),
}
} }
/// Loads a [Bitmap] with the specified dimensions from the provided data. /// Loads a [Bitmap] with the specified dimensions from the provided data.
/// ///
/// The data cannot be loaded on the following cases:
/// - when the dimensions and data size do not match exactly.
/// - when the width is not dividable by 8
///
/// In those cases, an Err is returned.
///
/// Otherwise, this returns a [Bitmap] that contains a copy of the provided data
///
/// # Arguments /// # Arguments
/// ///
/// - `width`: size in pixels in x-direction /// - `width`: size in pixels in x-direction
/// - `height`: size in pixels in y-direction /// - `height`: size in pixels in y-direction
/// pub fn load(
/// returns: [Bitmap] that contains a copy of the provided data width: usize,
/// height: usize,
/// # Panics data: &[u8],
/// ) -> Result<Self, LoadBitmapError> {
/// - when the dimensions and data size do not match exactly. assert!(width < isize::MAX as usize);
/// - when the width is not dividable by 8 assert!(height < isize::MAX as usize);
#[must_use] if width % 8 != 0 {
pub fn load(width: usize, height: usize, data: &[u8]) -> Self { return Err(LoadBitmapError::InvalidWidth);
assert_eq!( }
width % 8, if data.len() != height * width / 8 {
0, return Err(LoadBitmapError::InvalidDataSize);
"width must be a multiple of 8, but is {width}" }
); Ok(Self {
assert_eq!(
data.len(),
height * width / 8,
"data length must match dimensions, with 8 pixels per byte."
);
Self {
width, width,
height, height,
bit_vec: BitVec::from_slice(data), bit_vec: DisplayBitVec::from_slice(data),
} })
} }
/// Creates a [Bitmap] with the specified width from the provided [BitVec] without copying it. /// Creates a [Bitmap] with the specified width from the provided [`DisplayBitVec`] without copying it.
/// ///
/// returns: [Bitmap] that contains the provided data. /// The data cannot be loaded on the following cases:
/// /// - when the data size is not divisible by the width (incomplete rows)
/// # Panics
///
/// - when the bitvec size is not dividable by the provided width
/// - when the width is not dividable by 8 /// - when the width is not dividable by 8
#[must_use] ///
pub fn from_bitvec(width: usize, bit_vec: BitVec) -> Self { /// In those cases, an Err is returned.
assert_eq!( /// Otherwise, this returns a [Bitmap] that contains the provided data.
width % 8, pub fn from_bitvec(
0, width: usize,
"width must be a multiple of 8, but is {width}" bit_vec: DisplayBitVec,
); ) -> Result<Self, LoadBitmapError> {
if width % 8 != 0 {
return Err(LoadBitmapError::InvalidWidth);
}
let len = bit_vec.len(); let len = bit_vec.len();
let height = len / width; let height = len / width;
assert_eq!( assert!(width < isize::MAX as usize);
0, assert!(height < isize::MAX as usize);
len % width, if len % width != 0 {
"dimension mismatch - len {len} is not dividable by {width}" return Err(LoadBitmapError::InvalidDataSize);
); }
Self {
Ok(Self {
width, width,
height, height,
bit_vec, bit_vec,
} })
} }
/// Iterate over all cells in [Bitmap]. /// Iterate over all cells in [Bitmap].
@ -123,7 +127,7 @@ impl Bitmap {
/// Order is equivalent to the following loop: /// Order is equivalent to the following loop:
/// ``` /// ```
/// # use servicepoint::{Bitmap, Grid}; /// # use servicepoint::{Bitmap, Grid};
/// # let grid = Bitmap::new(8,2); /// # let grid = Bitmap::new(8, 2).unwrap();
/// for y in 0..grid.height() { /// for y in 0..grid.height() {
/// for x in 0..grid.width() { /// for x in 0..grid.width() {
/// grid.get(x, y); /// grid.get(x, y);
@ -139,7 +143,7 @@ impl Bitmap {
/// Order is equivalent to the following loop: /// Order is equivalent to the following loop:
/// ``` /// ```
/// # use servicepoint::{Bitmap, Grid}; /// # use servicepoint::{Bitmap, Grid};
/// # let mut grid = Bitmap::new(8,2); /// # let mut grid = Bitmap::new(8, 2).unwrap();
/// # let value = false; /// # let value = false;
/// for y in 0..grid.height() { /// for y in 0..grid.height() {
/// for x in 0..grid.width() { /// for x in 0..grid.width() {
@ -151,18 +155,20 @@ impl Bitmap {
/// # Example /// # Example
/// ``` /// ```
/// # use servicepoint::{Bitmap, Grid}; /// # use servicepoint::{Bitmap, Grid};
/// # let mut grid = Bitmap::new(8,2); /// # let mut grid = Bitmap::new(8, 2).unwrap();
/// # let value = false; /// # let value = false;
/// for (index, mut pixel) in grid.iter_mut().enumerate() { /// for (index, mut pixel) in grid.iter_mut().enumerate() {
/// pixel.set(index % 2 == 0) /// pixel.set(index % 2 == 0)
/// } /// }
/// ``` /// ```
#[must_use]
#[allow(clippy::iter_without_into_iter)]
pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> { pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> {
self.bit_vec.iter_mut() self.bit_vec.iter_mut()
} }
/// Iterate over all rows in [Bitmap] top to bottom. /// Iterate over all rows in [Bitmap] top to bottom.
pub fn iter_rows(&self) -> IterRows { pub fn iter_rows(&self) -> impl Iterator<Item = &BitSlice<u8, Msb0>> {
IterRows { IterRows {
bitmap: self, bitmap: self,
row: 0, row: 0,
@ -185,7 +191,7 @@ impl Grid<bool> for Bitmap {
/// When accessing `x` or `y` out of bounds. /// When accessing `x` or `y` out of bounds.
fn set(&mut self, x: usize, y: usize, value: bool) { fn set(&mut self, x: usize, y: usize, value: bool) {
self.assert_in_bounds(x, y); self.assert_in_bounds(x, y);
self.bit_vec.set(x + y * self.width, value) self.bit_vec.set(x + y * self.width, value);
} }
fn get(&self, x: usize, y: usize) -> bool { fn get(&self, x: usize, y: usize) -> bool {
@ -229,25 +235,25 @@ impl From<Bitmap> for Vec<u8> {
} }
} }
impl From<Bitmap> for BitVec { impl From<Bitmap> for DisplayBitVec {
/// Turns a [Bitmap] into the underlying [BitVec]. /// Turns a [Bitmap] into the underlying [`DisplayBitVec`].
fn from(value: Bitmap) -> Self { fn from(value: Bitmap) -> Self {
value.bit_vec value.bit_vec
} }
} }
impl From<&ValueGrid<bool>> for Bitmap { impl TryFrom<&ValueGrid<bool>> for Bitmap {
type Error = ();
/// Converts a grid of [bool]s into a [Bitmap]. /// Converts a grid of [bool]s into a [Bitmap].
/// ///
/// # Panics /// Returns Err if the width of `value` is not dividable by 8
/// fn try_from(value: &ValueGrid<bool>) -> Result<Self, Self::Error> {
/// - when the width of `value` is not dividable by 8 let mut result = Self::new(value.width(), value.height()).ok_or(())?;
fn from(value: &ValueGrid<bool>) -> Self {
let mut result = Self::new(value.width(), value.height());
for (mut to, from) in result.iter_mut().zip(value.iter()) { for (mut to, from) in result.iter_mut().zip(value.iter()) {
*to = *from; *to = *from;
} }
result Ok(result)
} }
} }
@ -262,7 +268,8 @@ impl From<&Bitmap> for ValueGrid<bool> {
} }
} }
pub struct IterRows<'t> { #[must_use]
struct IterRows<'t> {
bitmap: &'t Bitmap, bitmap: &'t Bitmap,
row: usize, row: usize,
} }
@ -282,13 +289,28 @@ impl<'t> Iterator for IterRows<'t> {
} }
} }
/// Errors that can happen when loading a bitmap.
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum LoadBitmapError {
/// The provided width is not divisible by 8.
#[error("The provided width is not divisible by 8.")]
InvalidWidth,
/// The provided data has an incorrect size for the provided dimensions.
#[error(
"The provided data has an incorrect size for the provided dimensions."
)]
InvalidDataSize,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{BitVec, Bitmap, DataRef, Grid, ValueGrid}; use crate::{
DisplayBitVec, Bitmap, DataRef, Grid, LoadBitmapError, ValueGrid,
};
#[test] #[test]
fn fill() { fn fill() {
let mut grid = Bitmap::new(8, 2); let mut grid = Bitmap::new(8, 2).unwrap();
assert_eq!(grid.data_ref(), [0x00, 0x00]); assert_eq!(grid.data_ref(), [0x00, 0x00]);
grid.fill(true); grid.fill(true);
@ -300,7 +322,7 @@ mod tests {
#[test] #[test]
fn get_set() { fn get_set() {
let mut grid = Bitmap::new(8, 2); let mut grid = Bitmap::new(8, 2).unwrap();
assert!(!grid.get(0, 0)); assert!(!grid.get(0, 0));
assert!(!grid.get(1, 1)); assert!(!grid.get(1, 1));
@ -315,7 +337,7 @@ mod tests {
#[test] #[test]
fn load() { fn load() {
let mut grid = Bitmap::new(8, 3); let mut grid = Bitmap::new(8, 3).unwrap();
for x in 0..grid.width { for x in 0..grid.width {
for y in 0..grid.height { for y in 0..grid.height {
grid.set(x, y, (x + y) % 2 == 0); grid.set(x, y, (x + y) % 2 == 0);
@ -326,33 +348,33 @@ mod tests {
let data: Vec<u8> = grid.into(); let data: Vec<u8> = grid.into();
let grid = Bitmap::load(8, 3, &data); let grid = Bitmap::load(8, 3, &data).unwrap();
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]); assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
} }
#[test] #[test]
#[should_panic] #[should_panic]
fn out_of_bounds_x() { fn out_of_bounds_x() {
let vec = Bitmap::new(8, 2); let vec = Bitmap::new(8, 2).unwrap();
vec.get(8, 1); vec.get(8, 1);
} }
#[test] #[test]
#[should_panic] #[should_panic]
fn out_of_bounds_y() { fn out_of_bounds_y() {
let mut vec = Bitmap::new(8, 2); let mut vec = Bitmap::new(8, 2).unwrap();
vec.set(1, 2, false); vec.set(1, 2, false);
} }
#[test] #[test]
fn iter() { fn iter() {
let grid = Bitmap::new(8, 2); let grid = Bitmap::new(8, 2).unwrap();
assert_eq!(16, grid.iter().count()) assert_eq!(16, grid.iter().count());
} }
#[test] #[test]
fn iter_rows() { fn iter_rows() {
let grid = Bitmap::load(8, 2, &[0x04, 0x40]); let grid = Bitmap::load(8, 2, &[0x04, 0x40]).unwrap();
let mut iter = grid.iter_rows(); let mut iter = grid.iter_rows();
assert_eq!(iter.next().unwrap().count_ones(), 1); assert_eq!(iter.next().unwrap().count_ones(), 1);
@ -362,7 +384,7 @@ mod tests {
#[test] #[test]
fn iter_mut() { fn iter_mut() {
let mut grid = Bitmap::new(8, 2); let mut grid = Bitmap::new(8, 2).unwrap();
for (index, mut pixel) in grid.iter_mut().enumerate() { for (index, mut pixel) in grid.iter_mut().enumerate() {
pixel.set(index % 2 == 0); pixel.set(index % 2 == 0);
} }
@ -371,7 +393,7 @@ mod tests {
#[test] #[test]
fn data_ref_mut() { fn data_ref_mut() {
let mut grid = Bitmap::new(8, 2); let mut grid = Bitmap::new(8, 2).unwrap();
let data = grid.data_ref_mut(); let data = grid.data_ref_mut();
data[1] = 0x0F; data[1] = 0x0F;
assert!(grid.get(7, 1)); assert!(grid.get(7, 1));
@ -379,9 +401,9 @@ mod tests {
#[test] #[test]
fn to_bitvec() { fn to_bitvec() {
let mut grid = Bitmap::new(8, 2); let mut grid = Bitmap::new(8, 2).unwrap();
grid.set(0, 0, true); grid.set(0, 0, true);
let bitvec: BitVec = grid.into(); let bitvec: DisplayBitVec = grid.into();
assert_eq!(bitvec.as_raw_slice(), [0x80, 0x00]); assert_eq!(bitvec.as_raw_slice(), [0x80, 0x00]);
} }
@ -391,9 +413,57 @@ mod tests {
8, 8,
1, 1,
&[true, false, true, false, true, false, true, false], &[true, false, true, false, true, false, true, false],
); )
let converted = Bitmap::from(&original); .unwrap();
let converted = Bitmap::try_from(&original).unwrap();
let reconverted = ValueGrid::from(&converted); let reconverted = ValueGrid::from(&converted);
assert_eq!(original, reconverted); assert_eq!(original, reconverted);
} }
#[test]
fn load_invalid_width() {
let data = DisplayBitVec::repeat(false, 7 * 3).into_vec();
assert_eq!(
Bitmap::load(7, 3, &data),
Err(LoadBitmapError::InvalidWidth)
);
}
#[test]
fn load_invalid_size() {
let data = DisplayBitVec::repeat(false, 8 * 4).into_vec();
assert_eq!(
Bitmap::load(8, 3, &data),
Err(LoadBitmapError::InvalidDataSize)
);
}
#[test]
fn from_vec_invalid_width() {
let data = DisplayBitVec::repeat(false, 7 * 3);
assert_eq!(
Bitmap::from_bitvec(7, data),
Err(LoadBitmapError::InvalidWidth)
);
}
#[test]
fn from_vec_invalid_size() {
let data = DisplayBitVec::repeat(false, 7 * 4);
assert_eq!(
Bitmap::from_bitvec(8, data),
Err(LoadBitmapError::InvalidDataSize)
);
}
#[test]
fn from_vec() {
let orig = Bitmap::new(8, 3).unwrap();
assert_eq!(Bitmap::from_bitvec(8, orig.bit_vec.clone()).unwrap(), orig);
}
#[test]
fn new_invalid_width() {
assert_eq!(Bitmap::new(7, 2), None);
}
} }

View file

@ -1,27 +1,33 @@
use crate::brightness::Brightness; use crate::{Brightness, ByteGrid, Grid, ValueGrid};
use crate::grid::Grid;
use crate::value_grid::ValueGrid;
use crate::ByteGrid;
/// A grid containing brightness values. /// A grid containing brightness values.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Connection, Grid, Origin}; /// # use servicepoint::*;
/// let mut grid = BrightnessGrid::new(2,2); /// let mut grid = BrightnessGrid::new(2,2);
/// grid.set(0, 0, Brightness::MIN); /// grid.set(0, 0, Brightness::MIN);
/// grid.set(1, 1, Brightness::MIN); /// grid.set(1, 1, Brightness::MIN);
/// ///
/// # let connection = Connection::Fake; /// # let connection = FakeConnection;
/// connection.send(Command::CharBrightness(Origin::new(3, 7), grid)).unwrap() /// connection.send_command(BrightnessGridCommand {
/// origin: Origin::new(3, 7),
/// grid
/// }).unwrap()
/// ``` /// ```
pub type BrightnessGrid = ValueGrid<Brightness>; pub type BrightnessGrid = ValueGrid<Brightness>;
impl BrightnessGrid { impl BrightnessGrid {
/// Like [Self::load], but ignoring any out-of-range brightness values /// Like [`Self::load`], but ignoring any out-of-range brightness values
pub fn saturating_load(width: usize, height: usize, data: &[u8]) -> Self { #[must_use]
ValueGrid::load(width, height, data).map(Brightness::saturating_from) pub fn saturating_load(
width: usize,
height: usize,
data: &[u8],
) -> Option<Self> {
ValueGrid::load(width, height, data)
.map(move |grid| grid.map(Brightness::saturating_from))
} }
} }
@ -40,7 +46,7 @@ impl From<&BrightnessGrid> for ByteGrid {
.iter() .iter()
.map(|brightness| (*brightness).into()) .map(|brightness| (*brightness).into())
.collect::<Vec<u8>>(); .collect::<Vec<u8>>();
ValueGrid::load(value.width(), value.height(), &u8s) Self::from_raw_parts_unchecked(value.width(), value.height(), u8s)
} }
} }
@ -52,18 +58,17 @@ impl TryFrom<ByteGrid> for BrightnessGrid {
.iter() .iter()
.map(|b| Brightness::try_from(*b)) .map(|b| Brightness::try_from(*b))
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(BrightnessGrid::load( Ok(Self::from_raw_parts_unchecked(
value.width(), value.width(),
value.height(), value.height(),
&brightnesses, brightnesses,
)) ))
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::value_grid::ValueGrid; use crate::{Brightness, BrightnessGrid, DataRef, Grid, ValueGrid};
use crate::{Brightness, BrightnessGrid, DataRef, Grid};
#[test] #[test]
fn to_u8_grid() { fn to_u8_grid() {
@ -86,8 +91,9 @@ mod tests {
Brightness::MIN, Brightness::MIN,
Brightness::MAX Brightness::MAX
] ]
), )
BrightnessGrid::saturating_load(2, 2, &[255u8, 23, 0, 42]) .unwrap(),
BrightnessGrid::saturating_load(2, 2, &[255u8, 23, 0, 42]).unwrap()
); );
} }
} }

View file

@ -1,4 +1,4 @@
use crate::ValueGrid; use crate::ValueGrid;
/// A 2d grid of bytes - see [ValueGrid]. /// A 2d grid of bytes - see [`ValueGrid`].
pub type ByteGrid = ValueGrid<u8>; pub type ByteGrid = ValueGrid<u8>;

View file

@ -3,28 +3,29 @@ use std::string::FromUtf8Error;
/// A grid containing UTF-8 characters. /// A grid containing UTF-8 characters.
/// ///
/// To send a CharGrid to the display, use [Command::Utf8Data](crate::Command::Utf8Data). /// To send a `CharGrid` to the display, use a [`crate::CharGridCommand`].
/// ///
/// Also see [ValueGrid] for the non-specialized operations and examples. /// Also see [`ValueGrid`] for the non-specialized operations and examples.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use servicepoint::{CharGrid, Command, Connection, Origin}; /// # use servicepoint::*;
/// let grid = CharGrid::from("You can\nload multiline\nstrings directly"); /// let grid = CharGrid::from("You can\nload multiline\nstrings directly");
/// assert_eq!(grid.get_row_str(1), Some("load multiline\0\0".to_string())); /// assert_eq!(grid.get_row_str(1), Some("load multiline\0\0".to_string()));
/// ///
/// # let connection = Connection::Fake; /// # let connection = FakeConnection;
/// let command = Command::Utf8Data(Origin::ZERO, grid); /// let command = CharGridCommand { origin: Origin::ZERO, grid };
/// connection.send_command(command).unwrap()
/// ``` /// ```
pub type CharGrid = ValueGrid<char>; pub type CharGrid = ValueGrid<char>;
impl CharGrid { impl CharGrid {
/// Loads a [CharGrid] with the specified width from the provided text, wrapping to as many rows as needed. /// Loads a [`CharGrid`] with the specified width from the provided text, wrapping to as many rows as needed.
/// ///
/// The passed rows are extended with '\0' if needed. /// The passed rows are extended with '\0' if needed.
/// ///
/// returns: [CharGrid] that contains a copy of the provided data. /// returns: [`CharGrid`] that contains a copy of the provided data.
/// ///
/// # Examples /// # Examples
/// ///
@ -32,6 +33,7 @@ impl CharGrid {
/// # use servicepoint::CharGrid; /// # use servicepoint::CharGrid;
/// let grid = CharGrid::wrap_str(2, "abc\ndef"); /// let grid = CharGrid::wrap_str(2, "abc\ndef");
/// ``` /// ```
#[must_use]
pub fn wrap_str(width: usize, text: &str) -> Self { pub fn wrap_str(width: usize, text: &str) -> Self {
let lines = text let lines = text
.split('\n') .split('\n')
@ -50,7 +52,9 @@ impl CharGrid {
let height = lines.len(); let height = lines.len();
let mut result = Self::new(width, height); let mut result = Self::new(width, height);
for (row, text_line) in lines.iter().enumerate() { for (row, text_line) in lines.iter().enumerate() {
result.set_row_str(row, text_line).unwrap() #[allow(clippy::unwrap_used)]
// we calculated the width before setting
result.set_row_str(row, text_line).unwrap();
} }
result result
} }
@ -66,6 +70,7 @@ impl CharGrid {
/// let grid = CharGrid::from("ab\ncd"); /// let grid = CharGrid::from("ab\ncd");
/// let col = grid.get_col_str(0).unwrap(); // "ac" /// let col = grid.get_col_str(0).unwrap(); // "ac"
/// ``` /// ```
#[must_use]
pub fn get_col_str(&self, x: usize) -> Option<String> { pub fn get_col_str(&self, x: usize) -> Option<String> {
Some(String::from_iter(self.get_col(x)?)) Some(String::from_iter(self.get_col(x)?))
} }
@ -81,13 +86,14 @@ impl CharGrid {
/// let grid = CharGrid::from("ab\ncd"); /// let grid = CharGrid::from("ab\ncd");
/// let row = grid.get_row_str(0).unwrap(); // "ab" /// let row = grid.get_row_str(0).unwrap(); // "ab"
/// ``` /// ```
#[must_use]
pub fn get_row_str(&self, y: usize) -> Option<String> { pub fn get_row_str(&self, y: usize) -> Option<String> {
Some(String::from_iter(self.get_row(y)?)) Some(String::from_iter(self.get_row(y)?))
} }
/// Overwrites a row in the grid with a str. /// Overwrites a row in the grid with a str.
/// ///
/// Returns [SetValueSeriesError] if y is out of bounds or `row` is not of the correct size. /// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
/// ///
/// # Examples /// # Examples
/// ///
@ -106,7 +112,7 @@ impl CharGrid {
/// Overwrites a column in the grid with a str. /// Overwrites a column in the grid with a str.
/// ///
/// Returns [SetValueSeriesError] if y is out of bounds or `row` is not of the correct size. /// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
/// ///
/// # Examples /// # Examples
/// ///
@ -123,9 +129,9 @@ impl CharGrid {
self.set_col(x, value.chars().collect::<Vec<_>>().as_ref()) self.set_col(x, value.chars().collect::<Vec<_>>().as_ref())
} }
/// Loads a [CharGrid] with the specified dimensions from the provided UTF-8 bytes. /// Loads a [`CharGrid`] with the specified dimensions from the provided UTF-8 bytes.
/// ///
/// returns: [CharGrid] that contains the provided data, or [FromUtf8Error] if the data is invalid. /// returns: [`CharGrid`] that contains the provided data, or [`FromUtf8Error`] if the data is invalid.
/// ///
/// # Examples /// # Examples
/// ///
@ -139,7 +145,9 @@ impl CharGrid {
bytes: Vec<u8>, bytes: Vec<u8>,
) -> Result<CharGrid, LoadUtf8Error> { ) -> Result<CharGrid, LoadUtf8Error> {
let s: Vec<char> = String::from_utf8(bytes)?.chars().collect(); let s: Vec<char> = String::from_utf8(bytes)?.chars().collect();
Ok(CharGrid::try_load(width, height, s)?) CharGrid::load(width, height, &s).ok_or(LoadUtf8Error::TryLoadError(
TryLoadValueGridError::InvalidDimensions,
))
} }
} }
@ -187,7 +195,7 @@ impl From<CharGrid> for String {
} }
impl From<&CharGrid> for String { impl From<&CharGrid> for String {
/// Converts a [CharGrid] into a [String]. /// Converts a [`CharGrid`] into a [String].
/// ///
/// Rows are separated by '\n'. /// Rows are separated by '\n'.
/// ///
@ -209,7 +217,7 @@ impl From<&CharGrid> for String {
} }
impl From<&CharGrid> for Vec<u8> { impl From<&CharGrid> for Vec<u8> {
/// Converts a [CharGrid] into a [`Vec<u8>`]. /// Converts a [`CharGrid`] into a [`Vec<u8>`].
/// ///
/// Rows are not separated. /// Rows are not separated.
/// ///
@ -223,7 +231,7 @@ impl From<&CharGrid> for Vec<u8> {
/// let grid = CharGrid::load_utf8(width, height, grid.into()); /// let grid = CharGrid::load_utf8(width, height, grid.into());
/// ``` /// ```
fn from(value: &CharGrid) -> Self { fn from(value: &CharGrid) -> Self {
String::from_iter(value.iter()).into_bytes() value.iter().collect::<String>().into_bytes()
} }
} }

View file

@ -1,9 +1,9 @@
use crate::Grid; use crate::{Grid, ValueGrid};
/// A grid containing codepage 437 characters. /// A grid containing codepage 437 characters.
/// ///
/// The encoding is currently not enforced. /// The encoding is currently not enforced.
pub type Cp437Grid = crate::value_grid::ValueGrid<u8>; pub type Cp437Grid = ValueGrid<u8>;
/// The error occurring when loading an invalid character /// The error occurring when loading an invalid character
#[derive(Debug, PartialEq, thiserror::Error)] #[derive(Debug, PartialEq, thiserror::Error)]
@ -18,7 +18,7 @@ pub struct InvalidCharError {
} }
impl Cp437Grid { impl Cp437Grid {
/// Load an ASCII-only [&str] into a [Cp437Grid] of specified width. /// Load an ASCII-only [&str] into a [`Cp437Grid`] of specified width.
/// ///
/// # Panics /// # Panics
/// ///
@ -86,7 +86,7 @@ mod tests {
fn load_ascii_nowrap() { fn load_ascii_nowrap() {
let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']
.map(move |c| c as u8); .map(move |c| c as u8);
let expected = Cp437Grid::load(5, 2, &chars); let expected = Cp437Grid::load(5, 2, &chars).unwrap();
let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap(); let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap();
// comma will be removed because line is too long and wrap is off // comma will be removed because line is too long and wrap is off
@ -97,7 +97,7 @@ mod tests {
fn load_ascii_wrap() { fn load_ascii_wrap() {
let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']
.map(move |c| c as u8); .map(move |c| c as u8);
let expected = Cp437Grid::load(5, 2, &chars); let expected = Cp437Grid::load(5, 2, &chars).unwrap();
let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap(); let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap();
// line break will be added // line break will be added

View file

@ -31,6 +31,10 @@ pub trait Grid<T> {
/// returns: Value at position or None /// returns: Value at position or None
fn get_optional(&self, x: isize, y: isize) -> Option<T> { fn get_optional(&self, x: isize, y: isize) -> Option<T> {
if self.is_in_bounds(x, y) { if self.is_in_bounds(x, y) {
#[expect(
clippy::cast_sign_loss,
reason = "is_in_bounds already checks this"
)]
Some(self.get(x as usize, y as usize)) Some(self.get(x as usize, y as usize))
} else { } else {
None None
@ -46,6 +50,10 @@ pub trait Grid<T> {
/// returns: the old value or None /// returns: the old value or None
fn set_optional(&mut self, x: isize, y: isize, value: T) -> bool { fn set_optional(&mut self, x: isize, y: isize, value: T) -> bool {
if self.is_in_bounds(x, y) { if self.is_in_bounds(x, y) {
#[expect(
clippy::cast_sign_loss,
reason = "is_in_bounds already checks this"
)]
self.set(x as usize, y as usize, value); self.set(x as usize, y as usize, value);
true true
} else { } else {
@ -63,6 +71,10 @@ pub trait Grid<T> {
fn height(&self) -> usize; fn height(&self) -> usize;
/// Checks whether the specified signed position is in grid bounds /// Checks whether the specified signed position is in grid bounds
#[expect(
clippy::cast_possible_wrap,
reason = "implementing types only allow 0..isize::MAX"
)]
fn is_in_bounds(&self, x: isize, y: isize) -> bool { fn is_in_bounds(&self, x: isize, y: isize) -> bool {
x >= 0 x >= 0
&& x < self.width() as isize && x < self.width() as isize
@ -79,6 +91,6 @@ pub trait Grid<T> {
let width = self.width(); let width = self.width();
assert!(x < width, "cannot access index [{x}, {y}] because x is outside of bounds [0..{width})"); assert!(x < width, "cannot access index [{x}, {y}] because x is outside of bounds [0..{width})");
let height = self.height(); let height = self.height();
assert!(y < height, "cannot access index [{x}, {y}] because x is outside of bounds [0..{height})"); assert!(y < height, "cannot access index [{x}, {y}] because y is outside of bounds [0..{height})");
} }
} }

21
src/containers/mod.rs Normal file
View file

@ -0,0 +1,21 @@
mod bit_vec;
mod bitmap;
mod brightness_grid;
mod byte_grid;
mod char_grid;
mod cp437_grid;
mod data_ref;
mod grid;
mod value_grid;
pub use bit_vec::{bitvec, DisplayBitVec};
pub use bitmap::*;
pub use brightness_grid::BrightnessGrid;
pub use byte_grid::ByteGrid;
pub use char_grid::CharGrid;
pub use cp437_grid::Cp437Grid;
pub use data_ref::DataRef;
pub use grid::Grid;
pub use value_grid::{
IterGridRows, SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
};

View file

@ -1,19 +1,19 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::slice::{Iter, IterMut}; use std::slice::{Iter, IterMut};
use crate::*; use crate::{DataRef, Grid};
/// A type that can be stored in a [ValueGrid], e.g. [char], [u8]. /// A type that can be stored in a [`ValueGrid`], e.g. [char], [u8].
pub trait Value: Sized + Default + Copy + Clone + Debug {} pub trait Value: Sized + Default + Copy + Clone + Debug {}
impl<T: Sized + Default + Copy + Clone + Debug> Value for T {} impl<T: Sized + Default + Copy + Clone + Debug> Value for T {}
/// A 2D grid of values. /// A 2D grid of values.
/// ///
/// The memory layout is the one the display expects in [Command]s. /// The memory layout is the one the display expects in [`crate::Command`]s.
/// ///
/// This structure can be used with any type that implements the [Value] trait. /// This structure can be used with any type that implements the [Value] trait.
/// You can also use the concrete type aliases provided in this crate, e.g. [CharGrid] and [ByteGrid]. /// You can also use the concrete type aliases provided in this crate, e.g. [`crate::CharGrid`] and [`crate::ByteGrid`].
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueGrid<T: Value> { pub struct ValueGrid<T: Value> {
width: usize, width: usize,
height: usize, height: usize,
@ -42,15 +42,18 @@ pub enum SetValueSeriesError {
} }
impl<T: Value> ValueGrid<T> { impl<T: Value> ValueGrid<T> {
/// Creates a new [ValueGrid] with the specified dimensions. /// Creates a new [`ValueGrid`] with the specified dimensions.
/// ///
/// # Arguments /// # Arguments
/// ///
/// - width: size in x-direction /// - width: size in x-direction
/// - height: size in y-direction /// - height: size in y-direction
/// ///
/// returns: [ValueGrid] initialized to default value. /// returns: [`ValueGrid`] initialized to default value.
#[must_use]
pub fn new(width: usize, height: usize) -> Self { pub fn new(width: usize, height: usize) -> Self {
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
Self { Self {
data: vec![Default::default(); width * height], data: vec![Default::default(); width * height],
width, width,
@ -58,91 +61,68 @@ impl<T: Value> ValueGrid<T> {
} }
} }
/// Loads a [ValueGrid] with the specified dimensions from the provided data. /// Loads a [`ValueGrid`] with the specified dimensions from the provided data.
/// ///
/// returns: [ValueGrid] that contains a copy of the provided data /// returns: [`ValueGrid`] that contains a copy of the provided data,
/// /// or None if the dimensions do not match the data size.
/// # Panics
///
/// - when the dimensions and data size do not match exactly.
#[must_use] #[must_use]
pub fn load(width: usize, height: usize, data: &[T]) -> Self { pub fn load(width: usize, height: usize, data: &[T]) -> Option<Self> {
assert_eq!( assert!(width < isize::MAX as usize);
width * height, assert!(height < isize::MAX as usize);
data.len(),
"dimension mismatch for data {data:?}"
);
Self {
data: Vec::from(data),
width,
height,
}
}
/// Creates a [ValueGrid] with the specified width from the provided data without copying it.
///
/// returns: [ValueGrid] that contains the provided data.
///
/// # Panics
///
/// - when the data size is not dividable by the width.
#[must_use]
pub fn from_vec(width: usize, data: Vec<T>) -> Self {
let len = data.len();
let height = len / width;
assert_eq!(
0,
len % width,
"dimension mismatch - len {len} is not dividable by {width}"
);
Self {
data,
width,
height,
}
}
/// Loads a [ValueGrid] with the specified width from the provided data, wrapping to as many rows as needed.
///
/// returns: [ValueGrid] that contains a copy of the provided data or [TryLoadValueGridError].
///
/// # Examples
///
/// ```
/// # use servicepoint::ValueGrid;
/// let grid = ValueGrid::wrap(2, &[0, 1, 2, 3, 4, 5]).unwrap();
/// ```
pub fn wrap(
width: usize,
data: &[T],
) -> Result<Self, TryLoadValueGridError> {
let len = data.len();
if len % width != 0 {
return Err(TryLoadValueGridError::InvalidDimensions);
}
Ok(Self::load(width, len / width, data))
}
/// Loads a [ValueGrid] with the specified dimensions from the provided data.
///
/// returns: [ValueGrid] that contains a copy of the provided data or [TryLoadValueGridError].
pub fn try_load(
width: usize,
height: usize,
data: Vec<T>,
) -> Result<Self, TryLoadValueGridError> {
if width * height != data.len() { if width * height != data.len() {
return Err(TryLoadValueGridError::InvalidDimensions); return None;
} }
Some(Self {
Ok(Self { data: Vec::from(data),
data,
width, width,
height, height,
}) })
} }
/// Iterate over all cells in [ValueGrid]. /// Creates a [`ValueGrid`] with the specified width from the provided data,
/// wrapping to as many rows as needed,
/// without copying the vec.
///
/// returns: [`ValueGrid`] that contains the provided data,
/// or None if the data size is not divisible by the width.
///
/// # Examples
///
/// ```
/// # use servicepoint::ValueGrid;
/// let grid = ValueGrid::from_vec(2, vec![0, 1, 2, 3, 4, 5]).unwrap();
/// ```
#[must_use]
pub fn from_vec(width: usize, data: Vec<T>) -> Option<Self> {
let len = data.len();
let height = len / width;
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
if len % width != 0 {
return None;
}
Some(Self {
width,
height,
data,
})
}
#[must_use]
pub(crate) fn from_raw_parts_unchecked(
width: usize,
height: usize,
data: Vec<T>,
) -> Self {
debug_assert_eq!(data.len(), width * height);
Self {
width,
height,
data,
}
}
/// Iterate over all cells in [`ValueGrid`].
/// ///
/// Order is equivalent to the following loop: /// Order is equivalent to the following loop:
/// ``` /// ```
@ -154,16 +134,13 @@ impl<T: Value> ValueGrid<T> {
/// } /// }
/// } /// }
/// ``` /// ```
pub fn iter(&self) -> Iter<T> { pub fn iter(&self) -> impl Iterator<Item = &T> {
self.data.iter() self.data.iter()
} }
/// Iterate over all rows in [ValueGrid] top to bottom. /// Iterate over all rows in [`ValueGrid`] top to bottom.
pub fn iter_rows(&self) -> IterGridRows<T> { pub fn iter_rows(&self) -> IterGridRows<T> {
IterGridRows { IterGridRows { grid: self, row: 0 }
byte_grid: self,
row: 0,
}
} }
/// Returns an iterator that allows modifying each value. /// Returns an iterator that allows modifying each value.
@ -200,29 +177,34 @@ impl<T: Value> ValueGrid<T> {
y: isize, y: isize,
) -> Option<&mut T> { ) -> Option<&mut T> {
if self.is_in_bounds(x, y) { if self.is_in_bounds(x, y) {
#[expect(
clippy::cast_sign_loss,
reason = "is_in_bounds already checks this"
)]
Some(&mut self.data[x as usize + y as usize * self.width]) Some(&mut self.data[x as usize + y as usize * self.width])
} else { } else {
None None
} }
} }
/// Convert between ValueGrid types. /// Convert between `ValueGrid` types.
/// ///
/// See also [Iterator::map]. /// See also [`Iterator::map`].
/// ///
/// # Examples /// # Examples
/// ///
/// Use logic written for u8s and then convert to [Brightness] values for sending in a [Command]. /// Use logic written for u8s and then convert to [Brightness] values for sending in a [Command].
/// ``` /// ```
/// # fn foo(grid: &mut ByteGrid) {} /// # fn foo(grid: &mut ByteGrid) {}
/// # use servicepoint::{Brightness, BrightnessGrid, ByteGrid, Command, Origin, TILE_HEIGHT, TILE_WIDTH}; /// # use servicepoint::*;
/// let mut grid: ByteGrid = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT); /// let mut grid: ByteGrid = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT);
/// foo(&mut grid); /// foo(&mut grid);
/// let grid: BrightnessGrid = grid.map(Brightness::saturating_from); /// let grid: BrightnessGrid = grid.map(Brightness::saturating_from);
/// let command = Command::CharBrightness(Origin::ZERO, grid); /// let command = BrightnessGridCommand { origin: Origin::ZERO, grid };
/// ``` /// ```
/// [Brightness]: [crate::Brightness] /// [Brightness]: [crate::Brightness]
/// [Command]: [crate::Command] /// [Command]: [crate::Command]
#[must_use]
pub fn map<TConverted, F>(&self, f: F) -> ValueGrid<TConverted> pub fn map<TConverted, F>(&self, f: F) -> ValueGrid<TConverted>
where where
TConverted: Value, TConverted: Value,
@ -233,22 +215,28 @@ impl<T: Value> ValueGrid<T> {
.iter() .iter()
.map(|elem| f(*elem)) .map(|elem| f(*elem))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
ValueGrid::load(self.width(), self.height(), &data) ValueGrid {
width: self.width(),
height: self.height(),
data,
}
} }
/// Copies a row from the grid. /// Copies a row from the grid.
/// ///
/// Returns [None] if y is out of bounds. /// Returns [None] if y is out of bounds.
#[must_use]
pub fn get_row(&self, y: usize) -> Option<Vec<T>> { pub fn get_row(&self, y: usize) -> Option<Vec<T>> {
self.data self.data
.chunks_exact(self.width()) .chunks_exact(self.width())
.nth(y) .nth(y)
.map(|row| row.to_vec()) .map(<[T]>::to_vec)
} }
/// Copies a column from the grid. /// Copies a column from the grid.
/// ///
/// Returns [None] if x is out of bounds. /// Returns [None] if x is out of bounds.
#[must_use]
pub fn get_col(&self, x: usize) -> Option<Vec<T>> { pub fn get_col(&self, x: usize) -> Option<Vec<T>> {
self.data self.data
.chunks_exact(self.width()) .chunks_exact(self.width())
@ -305,19 +293,27 @@ impl<T: Value> ValueGrid<T> {
}); });
} }
let chunk = match self.data.chunks_exact_mut(width).nth(y) { let Some(chunk) = self.data.chunks_exact_mut(width).nth(y) else {
Some(row) => row, return Err(SetValueSeriesError::OutOfBounds {
None => { size: self.height(),
return Err(SetValueSeriesError::OutOfBounds { index: y,
size: self.height(), });
index: y,
})
}
}; };
chunk.copy_from_slice(row); chunk.copy_from_slice(row);
Ok(()) Ok(())
} }
/// Enumerates all values in the grid.
pub fn enumerate(
&self,
) -> impl Iterator<Item = (usize, usize, T)> + use<'_, T> {
EnumerateGrid {
grid: self,
column: 0,
row: 0,
}
}
} }
/// Errors that can occur when loading a grid /// Errors that can occur when loading a grid
@ -329,7 +325,7 @@ pub enum TryLoadValueGridError {
} }
impl<T: Value> Grid<T> for ValueGrid<T> { impl<T: Value> Grid<T> for ValueGrid<T> {
/// Sets the value of the cell at the specified position in the `ValueGrid. /// Sets the value of the cell at the specified position in the grid.
/// ///
/// # Arguments /// # Arguments
/// ///
@ -390,9 +386,10 @@ impl<T: Value> From<ValueGrid<T>> for Vec<T> {
} }
} }
/// An iterator iver the rows in a [ValueGrid] /// An iterator iver the rows in a [`ValueGrid`]
#[must_use]
pub struct IterGridRows<'t, T: Value> { pub struct IterGridRows<'t, T: Value> {
byte_grid: &'t ValueGrid<T>, grid: &'t ValueGrid<T>,
row: usize, row: usize,
} }
@ -400,24 +397,46 @@ impl<'t, T: Value> Iterator for IterGridRows<'t, T> {
type Item = Iter<'t, T>; type Item = Iter<'t, T>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.byte_grid.height { if self.row >= self.grid.height {
return None; return None;
} }
let start = self.row * self.byte_grid.width; let start = self.row * self.grid.width;
let end = start + self.byte_grid.width; let end = start + self.grid.width;
let result = self.byte_grid.data[start..end].iter(); let result = self.grid.data[start..end].iter();
self.row += 1; self.row += 1;
Some(result) Some(result)
} }
} }
pub struct EnumerateGrid<'t, T: Value> {
grid: &'t ValueGrid<T>,
row: usize,
column: usize,
}
impl<T: Value> Iterator for EnumerateGrid<'_, T> {
type Item = (usize, usize, T);
fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.grid.height {
return None;
}
let result =
Some((self.column, self.row, self.grid.get(self.column, self.row)));
self.column += 1;
if self.column == self.grid.width {
self.column = 0;
self.row += 1;
}
result
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{SetValueSeriesError, ValueGrid, *};
value_grid::{SetValueSeriesError, ValueGrid},
*,
};
#[test] #[test]
fn fill() { fn fill() {
@ -456,7 +475,7 @@ mod tests {
let data: Vec<u8> = grid.into(); let data: Vec<u8> = grid.into();
let grid = ValueGrid::load(2, 3, &data); let grid = ValueGrid::load(2, 3, &data).unwrap();
assert_eq!(grid.data, [0, 1, 1, 2, 2, 3]); assert_eq!(grid.data, [0, 1, 1, 2, 2, 3]);
} }
@ -468,7 +487,7 @@ mod tests {
data_ref.copy_from_slice(&[1, 2, 3, 4]); data_ref.copy_from_slice(&[1, 2, 3, 4]);
assert_eq!(vec.data, [1, 2, 3, 4]); assert_eq!(vec.data, [1, 2, 3, 4]);
assert_eq!(vec.get(1, 0), 2) assert_eq!(vec.get(1, 0), 2);
} }
#[test] #[test]
@ -495,7 +514,7 @@ mod tests {
#[test] #[test]
fn iter_rows() { fn iter_rows() {
let vec = ValueGrid::load(2, 3, &[0, 1, 1, 2, 2, 3]); let vec = ValueGrid::load(2, 3, &[0, 1, 1, 2, 2, 3]).unwrap();
for (y, row) in vec.iter_rows().enumerate() { for (y, row) in vec.iter_rows().enumerate() {
for (x, val) in row.enumerate() { for (x, val) in row.enumerate() {
assert_eq!(*val, (x + y) as u8); assert_eq!(*val, (x + y) as u8);
@ -506,20 +525,21 @@ mod tests {
#[test] #[test]
#[should_panic] #[should_panic]
fn out_of_bounds_x() { fn out_of_bounds_x() {
let mut vec = ValueGrid::load(2, 2, &[0, 1, 2, 3]); let mut vec = ValueGrid::load(2, 2, &[0, 1, 2, 3]).unwrap();
vec.set(2, 1, 5); vec.set(2, 1, 5);
} }
#[test] #[test]
#[should_panic] #[should_panic]
fn out_of_bounds_y() { fn out_of_bounds_y() {
let vec = ValueGrid::load(2, 2, &[0, 1, 2, 3]); let vec = ValueGrid::load(2, 2, &[0, 1, 2, 3]).unwrap();
vec.get(1, 2); vec.get(1, 2);
} }
#[test] #[test]
fn ref_mut() { fn ref_mut() {
let mut vec = ValueGrid::from_vec(3, vec![0, 1, 2, 3, 4, 5, 6, 7, 8]); let mut vec =
ValueGrid::from_vec(3, vec![0, 1, 2, 3, 4, 5, 6, 7, 8]).unwrap();
let top_left = vec.get_ref_mut(0, 0); let top_left = vec.get_ref_mut(0, 0);
*top_left += 5; *top_left += 5;
@ -535,7 +555,7 @@ mod tests {
#[test] #[test]
fn optional() { fn optional() {
let mut grid = ValueGrid::load(2, 2, &[0, 1, 2, 3]); let mut grid = ValueGrid::load(2, 2, &[0, 1, 2, 3]).unwrap();
grid.set_optional(0, 0, 5); grid.set_optional(0, 0, 5);
grid.set_optional(-1, 0, 8); grid.set_optional(-1, 0, 8);
grid.set_optional(0, 8, 42); grid.set_optional(0, 8, 42);
@ -547,7 +567,7 @@ mod tests {
#[test] #[test]
fn col() { fn col() {
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]); let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.get_col(0), Some(vec![0, 2, 4])); assert_eq!(grid.get_col(0), Some(vec![0, 2, 4]));
assert_eq!(grid.get_col(1), Some(vec![1, 3, 5])); assert_eq!(grid.get_col(1), Some(vec![1, 3, 5]));
assert_eq!(grid.get_col(2), None); assert_eq!(grid.get_col(2), None);
@ -568,7 +588,7 @@ mod tests {
#[test] #[test]
fn row() { fn row() {
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]); let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.get_row(0), Some(vec![0, 1])); assert_eq!(grid.get_row(0), Some(vec![0, 1]));
assert_eq!(grid.get_row(2), Some(vec![4, 5])); assert_eq!(grid.get_row(2), Some(vec![4, 5]));
assert_eq!(grid.get_row(3), None); assert_eq!(grid.get_row(3), None);
@ -589,10 +609,32 @@ mod tests {
#[test] #[test]
fn wrap() { fn wrap() {
let grid = ValueGrid::wrap(2, &[0, 1, 2, 3, 4, 5]).unwrap(); let grid = ValueGrid::from_vec(2, vec![0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.height(), 3); assert_eq!(grid.height(), 3);
let grid = ValueGrid::wrap(4, &[0, 1, 2, 3, 4, 5]); let grid = ValueGrid::from_vec(4, vec![0, 1, 2, 3, 4, 5]);
assert_eq!(grid.err(), Some(TryLoadValueGridError::InvalidDimensions)); assert_eq!(grid, None);
}
#[test]
fn load_invalid_size() {
assert_eq!(ValueGrid::load(2, 2, &[1, 2, 3]), None);
}
#[test]
fn enumerate() {
let grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
let values = grid.enumerate().collect::<Vec<_>>();
assert_eq!(
values,
vec![
(0, 0, 0),
(1, 0, 1),
(0, 1, 2),
(1, 1, 3),
(0, 2, 4),
(1, 2, 5)
]
);
} }
} }

View file

@ -1,11 +1,10 @@
//! Contains functions to convert between UTF-8 and Codepage 437.
//!
//! See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
use crate::{CharGrid, Cp437Grid}; use crate::{CharGrid, Cp437Grid};
use std::collections::HashMap; use std::collections::HashMap;
/// Contains functions to convert between UTF-8 and Codepage 437.
///
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
pub struct Cp437Converter;
/// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters /// 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. /// Mostly follows CP437, except 0x0A, which is kept for use as line ending.
@ -34,47 +33,47 @@ const CP437_TO_UTF8: [char; 256] = [
]; ];
static UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> = static UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
once_cell::sync::Lazy::new(|| { once_cell::sync::Lazy::new(|| {
let pairs = CP437_TO_UTF8 CP437_TO_UTF8
.iter() .iter()
.enumerate() .enumerate()
.map(move |(index, char)| (*char, index as u8)); .map(
HashMap::from_iter(pairs) #[allow(clippy::cast_possible_truncation)]
move |(index, char)| (*char, index as u8),
)
.collect::<HashMap<_, _>>()
}); });
impl Cp437Converter { const MISSING_CHAR_CP437: u8 = 0x3F; // '?'
const MISSING_CHAR_CP437: u8 = 0x3F; // '?'
/// Convert the provided bytes to UTF-8. /// Convert the provided bytes to UTF-8.
pub fn cp437_to_str(cp437: &[u8]) -> String { #[must_use]
cp437 pub fn cp437_to_str(cp437: &[u8]) -> String {
.iter() cp437.iter().map(move |char| cp437_to_char(*char)).collect()
.map(move |char| Self::cp437_to_char(*char)) }
.collect()
}
/// Convert a single CP-437 character to UTF-8. /// Convert a single CP-437 character to UTF-8.
pub fn cp437_to_char(cp437: u8) -> char { #[must_use]
CP437_TO_UTF8[cp437 as usize] pub fn cp437_to_char(cp437: u8) -> char {
} CP437_TO_UTF8[cp437 as usize]
}
/// Convert the provided text to CP-437 bytes. /// Convert the provided text to CP-437 bytes.
/// ///
/// Characters that are not available are mapped to '?'. /// Characters that are not available are mapped to '?'.
pub fn str_to_cp437(utf8: &str) -> Vec<u8> { #[must_use]
utf8.chars().map(Self::char_to_cp437).collect() 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. /// Convert a single UTF-8 character to CP-437.
pub fn char_to_cp437(utf8: char) -> u8 { #[must_use]
*UTF8_TO_CP437 pub fn char_to_cp437(utf8: char) -> u8 {
.get(&utf8) *UTF8_TO_CP437.get(&utf8).unwrap_or(&MISSING_CHAR_CP437)
.unwrap_or(&Self::MISSING_CHAR_CP437)
}
} }
impl From<&Cp437Grid> for CharGrid { impl From<&Cp437Grid> for CharGrid {
fn from(value: &Cp437Grid) -> Self { fn from(value: &Cp437Grid) -> Self {
value.map(Cp437Converter::cp437_to_char) value.map(cp437_to_char)
} }
} }
@ -86,7 +85,7 @@ impl From<Cp437Grid> for CharGrid {
impl From<&CharGrid> for Cp437Grid { impl From<&CharGrid> for Cp437Grid {
fn from(value: &CharGrid) -> Self { fn from(value: &CharGrid) -> Self {
value.map(Cp437Converter::char_to_cp437) value.map(char_to_cp437)
} }
} }
@ -125,22 +124,19 @@ mod tests_feature_cp437 {
dx Σ x²·δx dx Σ x²·δx
"#; "#;
let cp437 = Cp437Converter::str_to_cp437(utf8); let cp437 = str_to_cp437(utf8);
let actual = Cp437Converter::cp437_to_str(&cp437); let actual = cp437_to_str(&cp437);
assert_eq!(utf8, actual) assert_eq!(utf8, actual);
} }
#[test] #[test]
fn convert_invalid() { fn convert_invalid() {
assert_eq!( assert_eq!(cp437_to_char(char_to_cp437('😜')), '?');
Cp437Converter::cp437_to_char(Cp437Converter::char_to_cp437('😜')),
'?'
);
} }
#[test] #[test]
fn round_trip_cp437() { fn round_trip_cp437() {
let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']); let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']).unwrap();
let cp437 = Cp437Grid::from(utf8.clone()); let cp437 = Cp437Grid::from(utf8.clone());
let actual = CharGrid::from(cp437); let actual = CharGrid::from(cp437);
assert_eq!(actual, utf8); assert_eq!(actual, utf8);

View file

@ -9,97 +9,83 @@
//! ### Clear display //! ### Clear display
//! //!
//! ```rust //! ```rust
//! use servicepoint::{Connection, Command}; //! use std::net::UdpSocket;
//! use servicepoint::*;
//! //!
//! // establish a connection //! // establish a connection
//! let connection = Connection::open("127.0.0.1:2342") //! let connection = UdpSocket::bind("127.0.0.1:2342")
//! .expect("connection failed"); //! .expect("connection failed");
//! //!
//! # let connection = FakeConnection; // do not fail tests
//! // turn off all pixels on display //! // turn off all pixels on display
//! connection.send(Command::Clear) //! connection.send_command(ClearCommand)
//! .expect("send failed"); //! .expect("send failed");
//! ``` //! ```
//! //!
//! ### Set all pixels to on //! ### Set all pixels to on
//! //!
//! ```rust //! ```rust
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap}; //! # use std::net::UdpSocket;
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed"); //! # use servicepoint::*;
//! # let connection = FakeConnection;
//! // turn on all pixels in a grid //! // turn on all pixels in a grid
//! let mut pixels = Bitmap::max_sized(); //! let mut pixels = Bitmap::max_sized();
//! pixels.fill(true); //! pixels.fill(true);
//! //!
//! // create command to send pixels //! // create command to send pixels
//! let command = Command::BitmapLinearWin( //! let command = BitmapCommand {
//! servicepoint::Origin::ZERO, //! origin: Origin::ZERO,
//! pixels, //! bitmap: pixels,
//! CompressionCode::default() //! compression: CompressionCode::default()
//! ); //! };
//! //!
//! // send command to display //! // send command to display
//! connection.send(command).expect("send failed"); //! connection.send_command(command).expect("send failed");
//! ``` //! ```
//! //!
//! ### Send text //! ### Send text
//! //!
//! ```rust //! ```rust
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap, CharGrid}; //! # use std::net::UdpSocket;
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed"); //! # use servicepoint::*;
//! # let connection = FakeConnection;
//! // create a text grid //! // create a text grid
//! let mut grid = CharGrid::from("Hello\nCCCB?"); //! let mut grid = CharGrid::from("Hello\nCCCB?");
//! // modify the grid //! // modify the grid
//! grid.set(grid.width() - 1, 1, '!'); //! grid.set(grid.width() - 1, 1, '!');
//! // create the command to send the data //! // create the command to send the data
//! let command = Command::Utf8Data(servicepoint::Origin::ZERO, grid); //! let command = CharGridCommand { origin: Origin::ZERO, grid };
//! // send command to display //! // send command to display
//! connection.send(command).expect("send failed"); //! connection.send_command(command).expect("send failed");
//! ``` //! ```
pub use crate::bit_vec::{bitvec, BitVec};
pub use crate::bitmap::Bitmap;
pub use crate::brightness::Brightness; pub use crate::brightness::Brightness;
pub use crate::brightness_grid::BrightnessGrid; pub use crate::command_code::CommandCode;
pub use crate::byte_grid::ByteGrid; pub use crate::commands::*;
pub use crate::char_grid::CharGrid;
pub use crate::command::{Command, Offset};
pub use crate::compression_code::CompressionCode; pub use crate::compression_code::CompressionCode;
pub use crate::connection::Connection; pub use crate::connection::*;
pub use crate::constants::*; pub use crate::constants::*;
pub use crate::cp437_grid::Cp437Grid; pub use crate::containers::*;
pub use crate::data_ref::DataRef;
pub use crate::grid::Grid;
pub use crate::origin::{Origin, Pixels, Tiles}; pub use crate::origin::{Origin, Pixels, Tiles};
pub use crate::packet::{Header, Packet, Payload}; pub use crate::packet::{Header, Packet, Payload};
pub use crate::value_grid::{
IterGridRows, SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
};
mod bit_vec;
mod bitmap;
mod brightness; mod brightness;
mod brightness_grid;
mod byte_grid;
mod char_grid;
mod command;
mod command_code; mod command_code;
mod commands;
mod compression; mod compression;
mod compression_code; mod compression_code;
mod connection; mod connection;
mod constants; mod constants;
mod cp437_grid; mod containers;
mod data_ref; #[cfg(feature = "cp437")]
mod grid; pub mod cp437;
mod origin; mod origin;
mod packet; mod packet;
mod value_grid;
#[cfg(feature = "cp437")]
mod cp437;
#[cfg(feature = "cp437")]
pub use crate::cp437::Cp437Converter;
// include README.md in doctest // include README.md in doctest
#[doc = include_str!("../README.md")] #[doc = include_str!("../README.md")]
#[cfg(doctest)] #[cfg(doctest)]
pub struct ReadmeDocTests; pub struct ReadmeDocTests;
/// Type alias for documenting the meaning of the u16 in enum values
pub type Offset = usize;

View file

@ -2,7 +2,7 @@ use crate::TILE_SIZE;
use std::marker::PhantomData; use std::marker::PhantomData;
/// An origin marks the top left position of a window sent to the display. /// An origin marks the top left position of a window sent to the display.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Origin<Unit: DisplayUnit> { pub struct Origin<Unit: DisplayUnit> {
/// position in the width direction /// position in the width direction
pub x: usize, pub x: usize,
@ -20,6 +20,7 @@ impl<Unit: DisplayUnit> Origin<Unit> {
}; };
/// Create a new [Origin] instance for the provided position. /// Create a new [Origin] instance for the provided position.
#[must_use]
pub fn new(x: usize, y: usize) -> Self { pub fn new(x: usize, y: usize) -> Self {
Self { Self {
x, x,
@ -44,11 +45,11 @@ impl<T: DisplayUnit> std::ops::Add<Origin<T>> for Origin<T> {
pub trait DisplayUnit {} pub trait DisplayUnit {}
/// Marks something to be measured in number of pixels. /// Marks something to be measured in number of pixels.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Pixels(); pub struct Pixels();
/// Marks something to be measured in number of iles. /// Marks something to be measured in number of iles.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Tiles(); pub struct Tiles();
impl DisplayUnit for Pixels {} impl DisplayUnit for Pixels {}
@ -65,24 +66,9 @@ impl From<&Origin<Tiles>> for Origin<Pixels> {
} }
} }
impl TryFrom<&Origin<Pixels>> for Origin<Tiles> { impl<Unit: DisplayUnit> Default for Origin<Unit> {
type Error = (); fn default() -> Self {
Self::ZERO
fn try_from(value: &Origin<Pixels>) -> Result<Self, Self::Error> {
let (x, x_rem) = (value.x / TILE_SIZE, value.x % TILE_SIZE);
if x_rem != 0 {
return Err(());
}
let (y, y_rem) = (value.y / TILE_SIZE, value.y % TILE_SIZE);
if y_rem != 0 {
return Err(());
}
Ok(Self {
x,
y,
phantom_data: PhantomData,
})
} }
} }
@ -99,24 +85,10 @@ mod tests {
} }
#[test] #[test]
fn origin_pixel_to_tile() { fn origin_add() {
let pixel: Origin<Pixels> = Origin::new(8, 16); assert_eq!(
let actual: Origin<Tiles> = Origin::try_from(&pixel).unwrap(); Origin::<Pixels>::new(4, 2),
let expected: Origin<Tiles> = Origin::new(1, 2); Origin::new(1, 0) + Origin::new(3, 2)
assert_eq!(actual, expected); );
}
#[test]
#[should_panic]
fn origin_pixel_to_tile_fail_y() {
let pixel: Origin<Pixels> = Origin::new(8, 15);
let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
}
#[test]
#[should_panic]
fn origin_pixel_to_tile_fail_x() {
let pixel: Origin<Pixels> = Origin::new(7, 16);
let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
} }
} }

View file

@ -7,29 +7,24 @@
//! Converting a packet to a command and back: //! Converting a packet to a command and back:
//! //!
//! ```rust //! ```rust
//! use servicepoint::{Command, Packet}; //! use servicepoint::{Command, Packet, TypedCommand};
//! # let command = Command::Clear; //! # let command = servicepoint::ClearCommand;
//! let packet: Packet = command.into(); //! let packet: Packet = command.into();
//! let command: Command = Command::try_from(packet).expect("could not read command from packet"); //! let command = TypedCommand::try_from(packet).expect("could not read command from packet");
//! ``` //! ```
//! //!
//! Converting a packet to bytes and back: //! Converting a packet to bytes and back:
//! //!
//! ```rust //! ```rust
//! use servicepoint::{Command, Packet}; //! use servicepoint::{Command, Packet};
//! # let command = Command::Clear; //! # let command = servicepoint::ClearCommand;
//! # let packet: Packet = command.into(); //! # let packet: Packet = command.into();
//! let bytes: Vec<u8> = packet.into(); //! let bytes: Vec<u8> = packet.into();
//! let packet = Packet::try_from(bytes).expect("could not read packet from bytes"); //! let packet = Packet::try_from(bytes).expect("could not read packet from bytes");
//! ``` //! ```
use crate::command_code::CommandCode; use crate::{command_code::CommandCode, Grid, Origin, Tiles};
use crate::compression::into_compressed; use std::{mem::size_of, num::TryFromIntError};
use crate::{
Bitmap, Command, CompressionCode, Grid, Offset, Origin, Pixels, Tiles,
TILE_SIZE,
};
use std::mem::size_of;
/// A raw header. /// A raw header.
/// ///
@ -37,7 +32,10 @@ use std::mem::size_of;
/// payload, where applicable. /// payload, where applicable.
/// ///
/// Because the meaning of most fields depend on the command, there are no speaking names for them. /// Because the meaning of most fields depend on the command, there are no speaking names for them.
#[derive(Copy, Clone, Debug, PartialEq)] ///
/// The contained values are in platform endian-ness and may need to be converted before sending.
#[derive(Copy, Clone, Debug, PartialEq, Default)]
#[repr(C)]
pub struct Header { pub struct Header {
/// The first two bytes specify which command this packet represents. /// The first two bytes specify which command this packet represents.
pub command_code: u16, pub command_code: u16,
@ -60,9 +58,7 @@ pub type Payload = Vec<u8>;
/// ///
/// Contents should probably only be used directly to use features not exposed by the library. /// Contents should probably only be used directly to use features not exposed by the library.
/// ///
/// You may want to use [Command] instead. /// You may want to use [`crate::Command`] or [`crate::TypedCommand`] instead.
///
///
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Packet { pub struct Packet {
/// Meta-information for the packed command /// Meta-information for the packed command
@ -74,40 +70,25 @@ pub struct Packet {
impl From<Packet> for Vec<u8> { impl From<Packet> for Vec<u8> {
/// Turn the packet into raw bytes ready to send /// Turn the packet into raw bytes ready to send
fn from(value: Packet) -> Self { fn from(value: Packet) -> Self {
let Packet { let mut vec = vec![0u8; value.size()];
header: value.serialize_to(vec.as_mut_slice());
Header { vec
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
} }
} }
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
#[error("The provided slice is smaller than the header size, so it cannot be read as a packet.")]
pub struct SliceSmallerThanHeader;
impl TryFrom<&[u8]> for Packet { impl TryFrom<&[u8]> for Packet {
type Error = (); type Error = SliceSmallerThanHeader;
/// Tries to interpret the bytes as a [Packet]. /// Tries to interpret the bytes as a [Packet].
/// ///
/// returns: `Error` if slice is not long enough to be a [Packet] /// returns: `Error` if slice is not long enough to be a [Packet]
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() < size_of::<Header>() { if value.len() < size_of::<Header>() {
return Err(()); return Err(SliceSmallerThanHeader);
} }
let header = { let header = {
@ -131,165 +112,48 @@ impl TryFrom<&[u8]> for Packet {
} }
impl TryFrom<Vec<u8>> for Packet { impl TryFrom<Vec<u8>> for Packet {
type Error = (); type Error = SliceSmallerThanHeader;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> { fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice()) Self::try_from(value.as_slice())
} }
} }
impl From<Command> for Packet {
/// Move the [Command] into a [Packet] instance for sending.
#[allow(clippy::cast_possible_truncation)]
fn from(value: Command) -> Self {
match value {
Command::Clear => Self::command_code_only(CommandCode::Clear),
Command::FadeOut => Self::command_code_only(CommandCode::FadeOut),
Command::HardReset => {
Self::command_code_only(CommandCode::HardReset)
}
#[allow(deprecated)]
Command::BitmapLegacy => {
Self::command_code_only(CommandCode::BitmapLegacy)
}
Command::CharBrightness(origin, grid) => {
Self::origin_grid_to_packet(
origin,
grid,
CommandCode::CharBrightness,
)
}
Command::Brightness(brightness) => Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0x00000,
b: 0x0000,
c: 0x0000,
d: 0x0000,
},
payload: vec![brightness.into()],
},
Command::BitmapLinearWin(origin, pixels, compression) => {
Self::bitmap_win_into_packet(origin, pixels, compression)
}
Command::BitmapLinear(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinear,
offset,
compression,
bits.into(),
)
}
Command::BitmapLinearAnd(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinearAnd,
offset,
compression,
bits.into(),
)
}
Command::BitmapLinearOr(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinearOr,
offset,
compression,
bits.into(),
)
}
Command::BitmapLinearXor(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinearXor,
offset,
compression,
bits.into(),
)
}
Command::Cp437Data(origin, grid) => Self::origin_grid_to_packet(
origin,
grid,
CommandCode::Cp437Data,
),
Command::Utf8Data(origin, grid) => {
Self::origin_grid_to_packet(origin, grid, CommandCode::Utf8Data)
}
}
}
}
impl Packet { impl Packet {
/// Helper method for `BitmapLinear*`-Commands into [Packet] /// Serialize packet into pre-allocated buffer.
#[allow(clippy::cast_possible_truncation)] ///
fn bitmap_linear_into_packet( /// returns false if the buffer is too small before writing any values.
command: CommandCode, pub fn serialize_to(&self, slice: &mut [u8]) -> bool {
offset: Offset, if slice.len() < self.size() {
compression: CompressionCode, return false;
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,
} }
let Packet {
header:
Header {
command_code,
a,
b,
c,
d,
},
payload,
} = self;
slice[0..=1].copy_from_slice(&u16::to_be_bytes(*command_code));
slice[2..=3].copy_from_slice(&u16::to_be_bytes(*a));
slice[4..=5].copy_from_slice(&u16::to_be_bytes(*b));
slice[6..=7].copy_from_slice(&u16::to_be_bytes(*c));
slice[8..=9].copy_from_slice(&u16::to_be_bytes(*d));
slice[10..].copy_from_slice(payload);
true
} }
#[allow(clippy::cast_possible_truncation)] /// Returns the amount of bytes this packet takes up when serialized.
fn bitmap_win_into_packet( #[must_use]
origin: Origin<Pixels>, pub fn size(&self) -> usize {
pixels: Bitmap, size_of::<Header>() + self.payload.len()
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 { fn u16_from_be_slice(slice: &[u8]) -> u16 {
@ -299,20 +163,30 @@ impl Packet {
u16::from_be_bytes(bytes) u16::from_be_bytes(bytes)
} }
fn origin_grid_to_packet<T>( pub(crate) fn origin_grid_to_packet<T>(
origin: Origin<Tiles>, origin: Origin<Tiles>,
grid: impl Grid<T> + Into<Payload>, grid: impl Grid<T> + Into<Payload>,
command_code: CommandCode, command_code: CommandCode,
) -> Packet { ) -> Result<Packet, TryFromIntError> {
Packet { Ok(Packet {
header: Header { header: Header {
command_code: command_code.into(), command_code: command_code.into(),
a: origin.x as u16, a: origin.x.try_into()?,
b: origin.y as u16, b: origin.y.try_into()?,
c: grid.width() as u16, c: grid.width().try_into()?,
d: grid.height() as u16, d: grid.height().try_into()?,
}, },
payload: grid.into(), payload: grid.into(),
})
}
pub(crate) fn command_code_only(c: CommandCode) -> Self {
Self {
header: Header {
command_code: c.into(),
..Default::default()
},
payload: vec![],
} }
} }
} }
@ -353,6 +227,9 @@ mod tests {
#[test] #[test]
fn too_small() { fn too_small() {
let data = vec![0u8; 4]; let data = vec![0u8; 4];
assert_eq!(Packet::try_from(data.as_slice()), Err(())) assert_eq!(
Packet::try_from(data.as_slice()),
Err(SliceSmallerThanHeader)
);
} }
} }