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
- name: Run Clippy
run: cargo clippy --all-targets --all-features
run: cargo clippy --all-features
- name: no features -- test (without doctest)
run: cargo test --lib --no-default-features

1
.gitignore vendored
View file

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

288
Cargo.lock generated
View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "servicepoint"
version = "0.13.2"
version = "0.14.0"
publish = true
edition = "2021"
license = "GPL-3.0-or-later"
@ -9,6 +9,7 @@ homepage = "https://docs.rs/crate/servicepoint"
repository = "https://git.berlin.ccc.de/servicepoint/servicepoint"
readme = "README.md"
keywords = ["cccb", "cccb-servicepoint"]
rust-version = "1.70.0"
[lib]
crate-type = ["rlib"]
@ -21,20 +22,17 @@ bzip2 = { version = "0.5", optional = true }
zstd = { version = "0.13", optional = true }
rust-lzma = { version = "0.6", optional = true }
rand = { version = "0.8", optional = true }
tungstenite = { version = "0.26", optional = true }
once_cell = { version = "1.20", optional = true }
thiserror = "2.0"
[features]
default = ["compression_lzma", "protocol_udp", "cp437"]
default = ["compression_lzma", "cp437"]
compression_zlib = ["dep:flate2"]
compression_bzip2 = ["dep:bzip2"]
compression_lzma = ["dep:rust-lzma"]
compression_zstd = ["dep:zstd"]
all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"]
rand = ["dep:rand"]
protocol_udp = []
protocol_websocket = ["dep:tungstenite"]
cp437 = ["dep:once_cell"]
[[example]]
@ -45,16 +43,55 @@ required-features = ["rand"]
name = "game_of_life"
required-features = ["rand"]
[[example]]
name = "websocket"
required-features = ["protocol_websocket"]
[dev-dependencies]
# for examples
clap = { version = "4.5", features = ["derive"] }
[lints.rust]
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]
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
[![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 Total Downloads](https://img.shields.io/crates/d/servicepoint)](https://crates.io/crates/servicepoint)
[![docs.rs](https://img.shields.io/docsrs/servicepoint)](https://docs.rs/servicepoint/latest/servicepoint/)
[![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint)](./LICENSE)
[![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
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
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
```rust no_run
use std::net::UdpSocket;
// everything you need is in the top-level
use servicepoint::*;
fn main() {
// establish connection
let connection = Connection::open("172.23.42.29:2342")
.expect("connection failed");
// this should be the IP of the real display @CCCB
let destination = "172.23.42.29:2342";
// clear screen content
connection.send(Command::Clear)
.expect("send failed");
// establish connection
let connection = UdpSocket::bind(destination).expect("connection failed");
// clear screen content using the UdpSocketExt
connection.send_command(ClearCommand).expect("send failed");
}
```
@ -46,7 +46,7 @@ or
```toml
[dependencies]
servicepoint = "0.13.2"
servicepoint = "0.14.0"
```
## 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.
Release notes are published [here](https://git.berlin.ccc.de/servicepoint/servicepoint/releases), please check them before updating.
Currently, this crate requires Rust [v1.70](https://releases.rs/docs/1.70.0/) from June 2023.
## Features
This library has multiple optional dependencies.
You can choose to (not) include them by toggling the related features.
| Name | Default | Description | Dependencies |
|--------------------|---------|----------------------------------------------|-----------------------------------------------------|
| protocol_udp | true | `Connection::Udp` | |
| cp437 | true | Conversion to and from CP-437 | [once_cell](https://crates.io/crates/once_cell) |
| compression_lzma | true | Enable additional compression algo | [rust-lzma](https://crates.io/crates/rust-lzma) |
| compression_zlib | false | Enable additional compression algo | [flate2](https://crates.io/crates/flate2) |
| compression_bzip2 | false | Enable additional compression algo | [bzip2](https://crates.io/crates/bzip2) |
| compression_zstd | false | Enable additional compression algo | [zstd](https://crates.io/crates/zstd) |
| protocol_websocket | false | `Connection::WebSocket` | [tungstenite](https://crates.io/crates/tungstenite) |
| rand | false | `impl Distribution<Brightness> for Standard` | [rand](https://crates.io/crates/rand) |
| Name | Default | Description | Dependencies |
|-------------------|---------|----------------------------------------------|-------------------------------------------------|
| cp437 | true | Conversion to and from CP-437 | [once_cell](https://crates.io/crates/once_cell) |
| compression_lzma | true | Enable additional compression algorithm | [rust-lzma](https://crates.io/crates/rust-lzma) |
| compression_zlib | false | Enable additional compression algorithm | [flate2](https://crates.io/crates/flate2) |
| compression_bzip2 | false | Enable additional compression algorithm | [bzip2](https://crates.io/crates/bzip2) |
| compression_zstd | false | Enable additional compression algorithm | [zstd](https://crates.io/crates/zstd) |
| rand | false | `impl Distribution<Brightness> for Standard` | [rand](https://crates.io/crates/rand) |
Es an example, if you only want zlib compression:
```toml
[dependencies]
servicepoint = { version = "0.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
@ -91,17 +102,14 @@ You can choose to (not) include them by toggling the related features.
## Projects using the library
- screen simulator (rust): [servicepoint-simulator](https://git.berlin.ccc.de/servicepoint/servicepoint-simulator)
- A bunch of projects (C): [arfst23/ServicePoint](https://github.com/arfst23/ServicePoint), including
- a CLI tool to display image files on the display or use the display as a TTY
- a BSD games robots clone
- a split-flap-display simulator
- animations that play on the display
- tanks game (C#): [servicepoint-tanks](https://github.com/kaesaecracker/cccb-tanks-cs)
- cellular automata slideshow (rust): [servicepoint-life](https://github.com/kaesaecracker/servicepoint-life)
- partial typescript implementation inspired by this library and browser
stream: [cccb-servicepoint-browser](https://github.com/SamuelScheit/cccb-servicepoint-browser)
- a CLI, can also share your screen: [servicepoint-cli](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
- [servicepoint-simulator](https://git.berlin.ccc.de/servicepoint/servicepoint-simulator): a screen simulator written in rust
- [servicepoint-tanks](https://git.berlin.ccc.de/vinzenz/servicepoint-tanks): a multiplayer game written in C# with a second screen in the browser written in React/Typescript
- [servicepoint-life](https://git.berlin.ccc.de/vinzenz/servicepoint-life): a cellular automata slideshow written in rust
- [servicepoint-cli](https://git.berlin.ccc.de/servicepoint/servicepoint-cli): a CLI that can:
- share (stream) your screen
- send image files with dithering
- clear the display
- ...
To add yourself to the list, open a pull request.
@ -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).
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
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.
use clap::Parser;
use servicepoint::*;
use servicepoint::{
CharGrid, CharGridCommand, ClearCommand, UdpSocketExt, TILE_WIDTH,
};
use std::net::UdpSocket;
#[derive(Parser, Debug)]
struct Cli {
@ -31,18 +34,18 @@ fn main() {
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");
if cli.clear {
connection
.send(Command::Clear)
.send_command(ClearCommand)
.expect("sending clear failed");
}
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
.send(Command::Utf8Data(Origin::ZERO, grid))
.send_command(command)
.expect("sending text failed");
}

View file

@ -1,7 +1,11 @@
//! Show a brightness level test pattern on screen
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)]
struct Cli {
@ -11,27 +15,23 @@ struct Cli {
fn main() {
let cli = Cli::parse();
let connection = Connection::open(cli.destination)
.expect("could not connect to display");
let connection =
UdpSocket::bind(cli.destination).expect("could not connect to display");
let mut pixels = Bitmap::max_sized();
pixels.fill(true);
let mut bitmap = Bitmap::max_sized();
bitmap.fill(true);
let command = Command::BitmapLinearWin(
Origin::ZERO,
pixels,
CompressionCode::default(),
);
connection.send(command).expect("send failed");
connection
.send_command(BitmapCommand::from(bitmap))
.expect("send failed");
let max_brightness: u8 = Brightness::MAX.into();
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
let level = index as u8 % max_brightness;
let level = (index % u8::MAX as usize) as u8 % max_brightness;
*byte = Brightness::try_from(level).unwrap();
}
connection
.send(Command::CharBrightness(Origin::ZERO, brightnesses))
.expect("send failed");
let command: BrightnessGridCommand = brightnesses.into();
connection.send_command(command).expect("send failed");
}

View file

@ -2,8 +2,8 @@
use clap::Parser;
use rand::{distributions, Rng};
use servicepoint::*;
use std::thread;
use servicepoint::{Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING};
use std::{net::UdpSocket, thread};
#[derive(Parser, Debug)]
struct Cli {
@ -16,17 +16,13 @@ struct Cli {
fn main() {
let cli = Cli::parse();
let connection = Connection::open(&cli.destination)
let connection = UdpSocket::bind(&cli.destination)
.expect("could not connect to display");
let mut field = make_random_field(cli.probability);
loop {
let command = Command::BitmapLinearWin(
Origin::ZERO,
field.clone(),
CompressionCode::default(),
);
connection.send(command).expect("could not send");
let command = BitmapCommand::from(field.clone());
connection.send_command(command).expect("could not send");
thread::sleep(FRAME_PACING);
field = iteration(field);
}
@ -39,10 +35,8 @@ fn iteration(field: Bitmap) -> Bitmap {
let old_state = field.get(x, y);
let neighbors = count_neighbors(&field, x as i32, y as i32);
let new_state = matches!(
(old_state, neighbors),
(true, 2) | (true, 3) | (false, 3)
);
let new_state =
matches!((old_state, neighbors), (true, 2 | 3) | (false, 3));
next.set(x, y, new_state);
}
}

View file

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

View file

@ -3,8 +3,12 @@
use clap::Parser;
use rand::Rng;
use servicepoint::*;
use std::time::Duration;
use servicepoint::{
Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand,
GlobalBrightnessCommand, Grid, Origin, UdpSocketExt,
TILE_HEIGHT, TILE_WIDTH,
};
use std::{net::UdpSocket, time::Duration};
#[derive(Parser, Debug)]
struct Cli {
@ -19,7 +23,7 @@ struct Cli {
fn main() {
let cli = Cli::parse();
let connection = Connection::open(cli.destination)
let connection = UdpSocket::bind_connect(cli.destination)
.expect("could not connect to display");
let wait_duration = Duration::from_millis(cli.wait_ms);
@ -28,17 +32,14 @@ fn main() {
let mut filled_grid = Bitmap::max_sized();
filled_grid.fill(true);
let command = Command::BitmapLinearWin(
Origin::ZERO,
filled_grid,
CompressionCode::default(),
);
connection.send(command).expect("send failed");
let command = BitmapCommand::from(filled_grid);
connection.send_command(command).expect("send failed");
}
// set all pixels to the same random brightness
let mut rng = rand::thread_rng();
connection.send(Command::Brightness(rng.gen())).unwrap();
let command: GlobalBrightnessCommand = rng.r#gen::<Brightness>().into();
connection.send_command(command).unwrap();
// continuously update random windows to new random brightness
loop {
@ -54,12 +55,12 @@ fn main() {
for y in 0..h {
for x in 0..w {
luma.set(x, y, rng.gen());
luma.set(x, y, rng.r#gen());
}
}
connection
.send(Command::CharBrightness(origin, luma))
.send_command(BrightnessGridCommand { origin, grid: luma })
.unwrap();
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.
use clap::Parser;
use servicepoint::*;
use std::thread;
use std::time::Duration;
use servicepoint::{
Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT,
PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread, time::Duration};
#[derive(Parser, Debug)]
struct Cli {
@ -20,8 +23,8 @@ fn main() {
Duration::from_millis(cli.time / PIXEL_WIDTH as u64),
);
let connection = Connection::open(cli.destination)
.expect("could not connect to display");
let connection =
UdpSocket::bind(cli.destination).expect("could not connect to display");
let mut enabled_pixels = Bitmap::max_sized();
enabled_pixels.fill(true);
@ -31,12 +34,9 @@ fn main() {
enabled_pixels.set(x_offset % PIXEL_WIDTH, y, false);
}
let command = BitmapCommand::from(enabled_pixels.clone());
connection
.send(Command::BitmapLinearWin(
Origin::ZERO,
enabled_pixels.clone(),
CompressionCode::default(),
))
.send_command(command)
.expect("could not send command to display");
thread::sleep(sleep_duration);
}

View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1736429655,
"narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=",
"lastModified": 1745925850,
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community",
"repo": "naersk",
"rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce",
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github"
},
"original": {
@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1739357830,
"narHash": "sha256-9xim3nJJUFbVbJCz48UP4fGRStVW5nv4VdbimbKxJ3I=",
"lastModified": 1746183838,
"narHash": "sha256-kwaaguGkAqTZ1oK0yXeQ3ayYjs8u/W7eEfrFpFfIDFA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0ff09db9d034a04acd4e8908820ba0b410d7a33a",
"rev": "bf3287dac860542719fe7554e21e686108716879",
"type": "github"
},
"original": {

View file

@ -137,6 +137,9 @@
clippy
cargo-expand
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
///
/// ```
/// # use servicepoint::{Brightness, Command, Connection};
/// # use servicepoint::*;
/// let b = Brightness::MAX;
/// let val: u8 = b.into();
///
/// let b = Brightness::try_from(7).unwrap();
/// # let connection = Connection::Fake;
/// let result = connection.send(Command::Brightness(b));
/// # let connection = FakeConnection;
/// let result = connection.send_command(GlobalBrightnessCommand::from(b));
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct Brightness(u8);
impl From<Brightness> for u8 {
@ -50,9 +51,10 @@ impl Brightness {
/// lowest possible brightness value, 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 {
if value > Brightness::MAX.into() {
Brightness::MAX
@ -90,7 +92,7 @@ mod tests {
fn rand_brightness() {
let mut rng = rand::thread_rng();
for _ in 0..100 {
let _: Brightness = rng.gen();
let _: Brightness = rng.r#gen();
}
}
@ -104,6 +106,10 @@ mod tests {
#[cfg(feature = "rand")]
fn test() {
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.
#[repr(u16)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(crate) enum CommandCode {
#[allow(missing_docs)]
pub enum CommandCode {
Clear = 0x0002,
Cp437Data = 0x0003,
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 {
type Error = ();
type Error = InvalidCommandCodeError;
/// Returns the enum value for the specified `u16` or `Error` if the code is unknown.
fn try_from(value: u16) -> Result<Self, Self::Error> {
@ -97,7 +102,7 @@ impl TryFrom<u16> for CommandCode {
value if value == CommandCode::Utf8Data as u16 => {
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")]
use bzip2::read::{BzDecoder, BzEncoder};
#[cfg(feature = "compression_zlib")]
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")]
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 buffer = [0u8; 10000];
let status = match decompress.decompress(
match decompress.decompress(
&payload,
&mut buffer,
FlushDecompress::Finish,
) {
Err(_) => return None,
Ok(status) => status,
};
match status {
Status::Ok => None,
Status::BufError => None,
Status::StreamEnd => Some(
buffer[0..(decompress.total_out() as usize)].to_owned(),
),
Ok(Status::Ok) => {
error!("input not big enough");
None
}
Ok(Status::BufError) => {
error!("output buffer is too small");
None
}
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")]
@ -43,24 +54,36 @@ pub(crate) fn into_decompressed(
let mut decoder = BzDecoder::new(&*payload);
let mut decompressed = vec![];
match decoder.read_to_end(&mut decompressed) {
Err(_) => None,
Ok(_) => Some(decompressed),
Err(e) => {
error!("failed to decompress data: {e}");
None
}
}
}
#[cfg(feature = "compression_lzma")]
CompressionCode::Lzma => match lzma::decompress(&payload) {
Err(_) => None,
Ok(decompressed) => Some(decompressed),
Err(e) => {
error!("failed to decompress data: {e}");
None
}
Ok(result) => Some(result),
},
#[cfg(feature = "compression_zstd")]
CompressionCode::Zstd => {
let mut decoder = match ZstdDecoder::new(&*payload) {
Err(_) => return None,
Ok(value) => value,
Err(e) => {
error!("failed to create zstd decoder: {e}");
return None;
}
};
let mut decompressed = vec![];
match decoder.read_to_end(&mut decompressed) {
Err(_) => None,
Err(e) => {
error!("failed to decompress data: {e}");
None
}
Ok(_) => Some(decompressed),
}
}
@ -70,24 +93,41 @@ pub(crate) fn into_decompressed(
pub(crate) fn into_compressed(
kind: CompressionCode,
payload: Payload,
) -> Payload {
) -> Option<Payload> {
match kind {
CompressionCode::Uncompressed => payload,
CompressionCode::Uncompressed => Some(payload),
#[cfg(feature = "compression_zlib")]
CompressionCode::Zlib => {
let mut compress =
flate2::Compress::new(flate2::Compression::fast(), true);
let mut buffer = [0u8; 10000];
match compress
.compress(&payload, &mut buffer, FlushCompress::Finish)
.expect("compress failed")
{
Status::Ok => panic!("buffer should be big enough"),
Status::BufError => panic!("BufError"),
Status::StreamEnd => {}
};
buffer[..compress.total_out() as usize].to_owned()
match compress.compress(
&payload,
&mut buffer,
FlushCompress::Finish,
) {
Ok(Status::Ok) => {
error!("output buffer not big enough");
None
}
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")]
CompressionCode::Bzip2 => {
@ -95,21 +135,39 @@ pub(crate) fn into_compressed(
BzEncoder::new(&*payload, bzip2::Compression::fast());
let mut compressed = vec![];
match encoder.read_to_end(&mut compressed) {
Err(err) => panic!("could not compress payload: {}", err),
Ok(_) => compressed,
Err(e) => {
error!("failed to compress data: {e}");
None
}
Ok(_) => Some(compressed),
}
}
#[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")]
CompressionCode::Zstd => {
let buf = Vec::with_capacity(payload.len());
let mut encoder =
ZstdEncoder::new(vec![], zstd::DEFAULT_COMPRESSION_LEVEL)
.expect("could not create encoder");
encoder
.write_all(&payload)
.expect("could not compress payload");
encoder.finish().expect("could not finish encoding")
match ZstdEncoder::new(buf, zstd::DEFAULT_COMPRESSION_LEVEL) {
Err(e) => {
error!("failed to create zstd encoder: {e}");
return None;
}
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
///
/// ```rust
/// # use servicepoint::{Command, CompressionCode, Origin, Bitmap};
/// # use servicepoint::*;
/// // create command without payload compression
/// # 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
/// # let pixels = Bitmap::max_sized();
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Lzma);
/// _ = BitmapCommand {
/// origin: Origin::ZERO,
/// bitmap: pixels,
/// compression: CompressionCode::Lzma
/// };
/// ```
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionCode {
/// no compression
Uncompressed = 0x0,
@ -31,6 +39,25 @@ pub enum CompressionCode {
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 {
fn from(value: CompressionCode) -> Self {
value as u16
@ -38,7 +65,7 @@ impl From<CompressionCode> for u16 {
}
impl TryFrom<u16> for CompressionCode {
type Error = ();
type Error = InvalidCompressionCodeError;
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
@ -61,7 +88,7 @@ impl TryFrom<u16> for CompressionCode {
value if value == CompressionCode::Zstd as u16 => {
Ok(CompressionCode::Zstd)
}
_ => Err(()),
_ => Err(InvalidCompressionCodeError(value)),
}
}
}

View file

@ -1,175 +1,40 @@
use crate::packet::Packet;
use std::fmt::Debug;
use crate::Packet;
use std::net::{Ipv4Addr, ToSocketAddrs};
use std::{convert::TryInto, net::UdpSocket};
/// A connection to the display.
///
/// Used to send [Packets][Packet] or [Commands][crate::Command].
///
/// # Examples
/// ```rust
/// let connection = servicepoint::Connection::open("127.0.0.1:2342")
/// .expect("connection failed");
/// connection.send(servicepoint::Command::Clear)
/// .expect("send failed");
/// ```
#[derive(Debug)]
pub enum Connection {
/// A connection using the UDP protocol.
///
/// Use this when sending commands directly to the display.
///
/// Requires the feature "protocol_udp" which is enabled by default.
#[cfg(feature = "protocol_udp")]
Udp(std::net::UdpSocket),
/// Provides servicepoint specific extensions for `UdpSocket`
pub trait UdpSocketExt {
/// Creates a `UdpSocket` that can be used so send to the specified addr.
fn bind_connect(addr: impl ToSocketAddrs) -> std::io::Result<UdpSocket>;
/// A connection using the WebSocket protocol.
///
/// Note that you will need to forward the WebSocket messages via UDP to the display.
/// You can use [servicepoint-websocket-relay] for this.
///
/// To create a new WebSocket automatically, use [Connection::open_websocket].
///
/// Requires the feature "protocol_websocket" which is disabled by default.
///
/// [servicepoint-websocket-relay]: https://github.com/kaesaecracker/servicepoint-websocket-relay
#[cfg(feature = "protocol_websocket")]
WebSocket(
std::sync::Mutex<
tungstenite::WebSocket<
tungstenite::stream::MaybeTlsStream<std::net::TcpStream>,
>,
>,
),
/// A fake connection for testing that does not actually send anything.
Fake,
/// Serializes the command and sends it through the socket
fn send_command(&self, command: impl TryInto<Packet>) -> Option<()>;
}
#[derive(Debug, thiserror::Error)]
pub enum SendError {
#[error("IO error occurred while sending")]
IoError(#[from] std::io::Error),
#[cfg(feature = "protocol_websocket")]
#[error("WebSocket error occurred while sending")]
WebsocketError(#[from] tungstenite::Error),
}
impl Connection {
/// Open a new UDP socket and connect to the provided host.
///
/// Note that this is UDP, which means that the open call can succeed even if the display is unreachable.
///
/// The address of the display in CCCB is `172.23.42.29:2342`.
///
/// # Errors
///
/// Any errors resulting from binding the udp socket.
///
/// # Examples
/// ```rust
/// let connection = servicepoint::Connection::open("127.0.0.1:2342")
/// .expect("connection failed");
/// ```
#[cfg(feature = "protocol_udp")]
pub fn open(
addr: impl std::net::ToSocketAddrs + Debug,
) -> std::io::Result<Self> {
log::info!("connecting to {addr:?}");
let socket = std::net::UdpSocket::bind("0.0.0.0:0")?;
impl UdpSocketExt for UdpSocket {
fn bind_connect(addr: impl ToSocketAddrs) -> std::io::Result<UdpSocket> {
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
socket.connect(addr)?;
Ok(Self::Udp(socket))
Ok(socket)
}
/// Open a new WebSocket and connect to the provided host.
///
/// Requires the feature "protocol_websocket" which is disabled by default.
///
/// # Examples
///
/// ```no_run
/// use tungstenite::http::Uri;
/// use servicepoint::{Command, Connection};
/// let uri = "ws://localhost:8080".parse().unwrap();
/// let mut connection = Connection::open_websocket(uri)
/// .expect("could not connect");
/// connection.send(Command::Clear)
/// .expect("send failed");
/// ```
#[cfg(feature = "protocol_websocket")]
pub fn open_websocket(
uri: tungstenite::http::Uri,
) -> tungstenite::Result<Self> {
use tungstenite::{
client::IntoClientRequest, connect, ClientRequestBuilder,
};
log::info!("connecting to {uri:?}");
let request = ClientRequestBuilder::new(uri).into_client_request()?;
let (sock, _) = connect(request)?;
Ok(Self::WebSocket(std::sync::Mutex::new(sock)))
}
/// Send something packet-like to the display. Usually this is in the form of a Command.
///
/// # Arguments
///
/// - `packet`: the packet-like to send
///
/// returns: true if packet was sent, otherwise false
///
/// # Examples
///
/// ```rust
/// let connection = servicepoint::Connection::Fake;
/// // turn off all pixels on display
/// connection.send(servicepoint::Command::Clear)
/// .expect("send failed");
/// ```
pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> {
let packet = packet.into();
log::debug!("sending {packet:?}");
let data: Vec<u8> = packet.into();
match self {
#[cfg(feature = "protocol_udp")]
Connection::Udp(socket) => {
socket
.send(&data)
.map_err(SendError::IoError)
.map(move |_| ()) // ignore Ok value
}
#[cfg(feature = "protocol_websocket")]
Connection::WebSocket(socket) => {
let mut socket = socket.lock().unwrap();
socket
.send(tungstenite::Message::Binary(data.into()))
.map_err(SendError::WebsocketError)
}
Connection::Fake => {
let _ = data;
Ok(())
}
}
fn send_command(&self, command: impl TryInto<Packet>) -> Option<()> {
let packet = command.try_into().ok()?;
let vec: Vec<_> = packet.into();
self.send(&vec).ok()?;
Some(())
}
}
impl Drop for Connection {
fn drop(&mut self) {
#[cfg(feature = "protocol_websocket")]
if let Connection::WebSocket(sock) = self {
_ = sock.try_lock().map(move |mut sock| sock.close(None));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn send_fake() {
let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let packet = Packet::try_from(data).unwrap();
Connection::Fake.send(packet).unwrap()
/// A fake connection for testing that does not actually send anything.
pub struct FakeConnection;
impl FakeConnection {
/// 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));
Some(())
}
}

View file

@ -52,19 +52,19 @@ pub const PIXEL_COUNT: usize = PIXEL_WIDTH * PIXEL_HEIGHT;
///
/// ```rust
/// # use std::time::Instant;
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, Bitmap};
/// # let connection = servicepoint::Connection::Fake;
/// # use servicepoint::*;
/// # let connection = FakeConnection;
/// # let pixels = Bitmap::max_sized();
/// loop {
/// let start = Instant::now();
///
/// // Change pixels here
///
/// connection.send(Command::BitmapLinearWin(
/// Origin::new(0,0),
/// pixels,
/// CompressionCode::default()
/// ))
/// connection.send_command(BitmapCommand {
/// origin: Origin::new(0,0),
/// bitmap: pixels,
/// compression: CompressionCode::default()
/// })
/// .expect("send failed");
///
/// // 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::BitVec;
use crate::*;
use ::bitvec::order::Msb0;
use ::bitvec::prelude::BitSlice;
use ::bitvec::slice::IterMut;
use crate::{
DisplayBitVec, DataRef, Grid, ValueGrid, PIXEL_HEIGHT, PIXEL_WIDTH,
};
use ::bitvec::{order::Msb0, prelude::BitSlice, slice::IterMut};
/// A fixed-size 2D grid of booleans.
///
@ -22,100 +20,106 @@ use ::bitvec::slice::IterMut;
pub struct Bitmap {
width: usize,
height: usize,
bit_vec: BitVec,
bit_vec: DisplayBitVec,
}
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
///
/// - `width`: size in pixels in x-direction
/// - `height`: size in pixels in y-direction
///
/// returns: [Bitmap] initialized to all pixels off
///
/// # Panics
///
/// - when the width is not dividable by 8
pub fn new(width: usize, height: usize) -> Self {
assert_eq!(
width % 8,
0,
"width must be a multiple of 8, but is {width}"
);
Self {
width,
height,
bit_vec: BitVec::repeat(false, width * height),
#[must_use]
pub fn new(width: usize, height: usize) -> Option<Self> {
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
if width % 8 != 0 {
return None;
}
Some(Self::new_unchecked(width, height))
}
/// Creates a new pixel grid with the size of the whole screen.
#[must_use]
pub fn max_sized() -> Self {
Self::new(PIXEL_WIDTH, PIXEL_HEIGHT)
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.
///
/// 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
///
/// - `width`: size in pixels in x-direction
/// - `height`: size in pixels in y-direction
///
/// returns: [Bitmap] that contains a copy of the provided data
///
/// # Panics
///
/// - when the dimensions and data size do not match exactly.
/// - when the width is not dividable by 8
#[must_use]
pub fn load(width: usize, height: usize, data: &[u8]) -> Self {
assert_eq!(
width % 8,
0,
"width must be a multiple of 8, but is {width}"
);
assert_eq!(
data.len(),
height * width / 8,
"data length must match dimensions, with 8 pixels per byte."
);
Self {
pub fn load(
width: usize,
height: usize,
data: &[u8],
) -> Result<Self, LoadBitmapError> {
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
if width % 8 != 0 {
return Err(LoadBitmapError::InvalidWidth);
}
if data.len() != height * width / 8 {
return Err(LoadBitmapError::InvalidDataSize);
}
Ok(Self {
width,
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.
///
/// # Panics
///
/// - when the bitvec size is not dividable by the provided width
/// The data cannot be loaded on the following cases:
/// - when the data size is not divisible by the width (incomplete rows)
/// - when the width is not dividable by 8
#[must_use]
pub fn from_bitvec(width: usize, bit_vec: BitVec) -> Self {
assert_eq!(
width % 8,
0,
"width must be a multiple of 8, but is {width}"
);
///
/// In those cases, an Err is returned.
/// Otherwise, this returns a [Bitmap] that contains the provided data.
pub fn from_bitvec(
width: usize,
bit_vec: DisplayBitVec,
) -> Result<Self, LoadBitmapError> {
if width % 8 != 0 {
return Err(LoadBitmapError::InvalidWidth);
}
let len = bit_vec.len();
let height = len / width;
assert_eq!(
0,
len % width,
"dimension mismatch - len {len} is not dividable by {width}"
);
Self {
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
if len % width != 0 {
return Err(LoadBitmapError::InvalidDataSize);
}
Ok(Self {
width,
height,
bit_vec,
}
})
}
/// Iterate over all cells in [Bitmap].
@ -123,7 +127,7 @@ impl Bitmap {
/// Order is equivalent to the following loop:
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # let grid = Bitmap::new(8,2);
/// # let grid = Bitmap::new(8, 2).unwrap();
/// for y in 0..grid.height() {
/// for x in 0..grid.width() {
/// grid.get(x, y);
@ -139,7 +143,7 @@ impl Bitmap {
/// Order is equivalent to the following loop:
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # let mut grid = Bitmap::new(8,2);
/// # let mut grid = Bitmap::new(8, 2).unwrap();
/// # let value = false;
/// for y in 0..grid.height() {
/// for x in 0..grid.width() {
@ -151,18 +155,20 @@ impl Bitmap {
/// # Example
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # let mut grid = Bitmap::new(8,2);
/// # let mut grid = Bitmap::new(8, 2).unwrap();
/// # let value = false;
/// for (index, mut pixel) in grid.iter_mut().enumerate() {
/// pixel.set(index % 2 == 0)
/// }
/// ```
#[must_use]
#[allow(clippy::iter_without_into_iter)]
pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> {
self.bit_vec.iter_mut()
}
/// Iterate over all rows in [Bitmap] top to bottom.
pub fn iter_rows(&self) -> IterRows {
pub fn iter_rows(&self) -> impl Iterator<Item = &BitSlice<u8, Msb0>> {
IterRows {
bitmap: self,
row: 0,
@ -185,7 +191,7 @@ impl Grid<bool> for Bitmap {
/// When accessing `x` or `y` out of bounds.
fn set(&mut self, x: usize, y: usize, value: bool) {
self.assert_in_bounds(x, y);
self.bit_vec.set(x + y * self.width, value)
self.bit_vec.set(x + y * self.width, value);
}
fn get(&self, x: usize, y: usize) -> bool {
@ -229,25 +235,25 @@ impl From<Bitmap> for Vec<u8> {
}
}
impl From<Bitmap> for BitVec {
/// Turns a [Bitmap] into the underlying [BitVec].
impl From<Bitmap> for DisplayBitVec {
/// Turns a [Bitmap] into the underlying [`DisplayBitVec`].
fn from(value: Bitmap) -> Self {
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].
///
/// # Panics
///
/// - when the width of `value` is not dividable by 8
fn from(value: &ValueGrid<bool>) -> Self {
let mut result = Self::new(value.width(), value.height());
/// Returns Err if the width of `value` is not dividable by 8
fn try_from(value: &ValueGrid<bool>) -> Result<Self, Self::Error> {
let mut result = Self::new(value.width(), value.height()).ok_or(())?;
for (mut to, from) in result.iter_mut().zip(value.iter()) {
*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,
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)]
mod tests {
use crate::{BitVec, Bitmap, DataRef, Grid, ValueGrid};
use crate::{
DisplayBitVec, Bitmap, DataRef, Grid, LoadBitmapError, ValueGrid,
};
#[test]
fn fill() {
let mut grid = Bitmap::new(8, 2);
let mut grid = Bitmap::new(8, 2).unwrap();
assert_eq!(grid.data_ref(), [0x00, 0x00]);
grid.fill(true);
@ -300,7 +322,7 @@ mod tests {
#[test]
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(1, 1));
@ -315,7 +337,7 @@ mod tests {
#[test]
fn load() {
let mut grid = Bitmap::new(8, 3);
let mut grid = Bitmap::new(8, 3).unwrap();
for x in 0..grid.width {
for y in 0..grid.height {
grid.set(x, y, (x + y) % 2 == 0);
@ -326,33 +348,33 @@ mod tests {
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]);
}
#[test]
#[should_panic]
fn out_of_bounds_x() {
let vec = Bitmap::new(8, 2);
let vec = Bitmap::new(8, 2).unwrap();
vec.get(8, 1);
}
#[test]
#[should_panic]
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);
}
#[test]
fn iter() {
let grid = Bitmap::new(8, 2);
assert_eq!(16, grid.iter().count())
let grid = Bitmap::new(8, 2).unwrap();
assert_eq!(16, grid.iter().count());
}
#[test]
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();
assert_eq!(iter.next().unwrap().count_ones(), 1);
@ -362,7 +384,7 @@ mod tests {
#[test]
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() {
pixel.set(index % 2 == 0);
}
@ -371,7 +393,7 @@ mod tests {
#[test]
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();
data[1] = 0x0F;
assert!(grid.get(7, 1));
@ -379,9 +401,9 @@ mod tests {
#[test]
fn to_bitvec() {
let mut grid = Bitmap::new(8, 2);
let mut grid = Bitmap::new(8, 2).unwrap();
grid.set(0, 0, true);
let bitvec: BitVec = grid.into();
let bitvec: DisplayBitVec = grid.into();
assert_eq!(bitvec.as_raw_slice(), [0x80, 0x00]);
}
@ -391,9 +413,57 @@ mod tests {
8,
1,
&[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);
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::grid::Grid;
use crate::value_grid::ValueGrid;
use crate::ByteGrid;
use crate::{Brightness, ByteGrid, Grid, ValueGrid};
/// A grid containing brightness values.
///
/// # Examples
///
/// ```rust
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Connection, Grid, Origin};
/// # use servicepoint::*;
/// let mut grid = BrightnessGrid::new(2,2);
/// grid.set(0, 0, Brightness::MIN);
/// grid.set(1, 1, Brightness::MIN);
///
/// # let connection = Connection::Fake;
/// connection.send(Command::CharBrightness(Origin::new(3, 7), grid)).unwrap()
/// # let connection = FakeConnection;
/// connection.send_command(BrightnessGridCommand {
/// origin: Origin::new(3, 7),
/// grid
/// }).unwrap()
/// ```
pub type BrightnessGrid = ValueGrid<Brightness>;
impl BrightnessGrid {
/// Like [Self::load], but ignoring any out-of-range brightness values
pub fn saturating_load(width: usize, height: usize, data: &[u8]) -> Self {
ValueGrid::load(width, height, data).map(Brightness::saturating_from)
/// Like [`Self::load`], but ignoring any out-of-range brightness values
#[must_use]
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()
.map(|brightness| (*brightness).into())
.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()
.map(|b| Brightness::try_from(*b))
.collect::<Result<Vec<_>, _>>()?;
Ok(BrightnessGrid::load(
Ok(Self::from_raw_parts_unchecked(
value.width(),
value.height(),
&brightnesses,
brightnesses,
))
}
}
#[cfg(test)]
mod tests {
use crate::value_grid::ValueGrid;
use crate::{Brightness, BrightnessGrid, DataRef, Grid};
use crate::{Brightness, BrightnessGrid, DataRef, Grid, ValueGrid};
#[test]
fn to_u8_grid() {
@ -86,8 +91,9 @@ mod tests {
Brightness::MIN,
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;
/// A 2d grid of bytes - see [ValueGrid].
/// A 2d grid of bytes - see [`ValueGrid`].
pub type ByteGrid = ValueGrid<u8>;

View file

@ -3,28 +3,29 @@ use std::string::FromUtf8Error;
/// 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
///
/// ```rust
/// # use servicepoint::{CharGrid, Command, Connection, Origin};
/// # use servicepoint::*;
/// let grid = CharGrid::from("You can\nload multiline\nstrings directly");
/// assert_eq!(grid.get_row_str(1), Some("load multiline\0\0".to_string()));
///
/// # let connection = Connection::Fake;
/// let command = Command::Utf8Data(Origin::ZERO, grid);
/// # let connection = FakeConnection;
/// let command = CharGridCommand { origin: Origin::ZERO, grid };
/// connection.send_command(command).unwrap()
/// ```
pub type CharGrid = ValueGrid<char>;
impl CharGrid {
/// Loads a [CharGrid] with the specified width from the provided text, wrapping to as many rows as needed.
/// Loads a [`CharGrid`] with the specified width from the provided text, wrapping to as many rows as needed.
///
/// The passed rows are extended with '\0' if needed.
///
/// returns: [CharGrid] that contains a copy of the provided data.
/// returns: [`CharGrid`] that contains a copy of the provided data.
///
/// # Examples
///
@ -32,6 +33,7 @@ impl CharGrid {
/// # use servicepoint::CharGrid;
/// let grid = CharGrid::wrap_str(2, "abc\ndef");
/// ```
#[must_use]
pub fn wrap_str(width: usize, text: &str) -> Self {
let lines = text
.split('\n')
@ -50,7 +52,9 @@ impl CharGrid {
let height = lines.len();
let mut result = Self::new(width, height);
for (row, text_line) in lines.iter().enumerate() {
result.set_row_str(row, text_line).unwrap()
#[allow(clippy::unwrap_used)]
// we calculated the width before setting
result.set_row_str(row, text_line).unwrap();
}
result
}
@ -66,6 +70,7 @@ impl CharGrid {
/// let grid = CharGrid::from("ab\ncd");
/// let col = grid.get_col_str(0).unwrap(); // "ac"
/// ```
#[must_use]
pub fn get_col_str(&self, x: usize) -> Option<String> {
Some(String::from_iter(self.get_col(x)?))
}
@ -81,13 +86,14 @@ impl CharGrid {
/// let grid = CharGrid::from("ab\ncd");
/// let row = grid.get_row_str(0).unwrap(); // "ab"
/// ```
#[must_use]
pub fn get_row_str(&self, y: usize) -> Option<String> {
Some(String::from_iter(self.get_row(y)?))
}
/// Overwrites a row in the grid with a str.
///
/// Returns [SetValueSeriesError] if y is out of bounds or `row` is not of the correct size.
/// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
///
/// # Examples
///
@ -106,7 +112,7 @@ impl CharGrid {
/// 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
///
@ -123,9 +129,9 @@ impl CharGrid {
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
///
@ -139,7 +145,9 @@ impl CharGrid {
bytes: Vec<u8>,
) -> Result<CharGrid, LoadUtf8Error> {
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 {
/// Converts a [CharGrid] into a [String].
/// Converts a [`CharGrid`] into a [String].
///
/// Rows are separated by '\n'.
///
@ -209,7 +217,7 @@ impl From<&CharGrid> for String {
}
impl From<&CharGrid> for Vec<u8> {
/// Converts a [CharGrid] into a [`Vec<u8>`].
/// Converts a [`CharGrid`] into a [`Vec<u8>`].
///
/// Rows are not separated.
///
@ -223,7 +231,7 @@ impl From<&CharGrid> for Vec<u8> {
/// let grid = CharGrid::load_utf8(width, height, grid.into());
/// ```
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.
///
/// 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
#[derive(Debug, PartialEq, thiserror::Error)]
@ -18,7 +18,7 @@ pub struct InvalidCharError {
}
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
///
@ -86,7 +86,7 @@ mod tests {
fn load_ascii_nowrap() {
let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']
.map(move |c| c as u8);
let expected = Cp437Grid::load(5, 2, &chars);
let expected = Cp437Grid::load(5, 2, &chars).unwrap();
let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap();
// comma will be removed because line is too long and wrap is off
@ -97,7 +97,7 @@ mod tests {
fn load_ascii_wrap() {
let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']
.map(move |c| c as u8);
let expected = Cp437Grid::load(5, 2, &chars);
let expected = Cp437Grid::load(5, 2, &chars).unwrap();
let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap();
// line break will be added

View file

@ -31,6 +31,10 @@ pub trait Grid<T> {
/// returns: Value at position or None
fn get_optional(&self, x: isize, y: isize) -> Option<T> {
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))
} else {
None
@ -46,6 +50,10 @@ pub trait Grid<T> {
/// returns: the old value or None
fn set_optional(&mut self, x: isize, y: isize, value: T) -> bool {
if self.is_in_bounds(x, y) {
#[expect(
clippy::cast_sign_loss,
reason = "is_in_bounds already checks this"
)]
self.set(x as usize, y as usize, value);
true
} else {
@ -63,6 +71,10 @@ pub trait Grid<T> {
fn height(&self) -> usize;
/// 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 {
x >= 0
&& x < self.width() as isize
@ -79,6 +91,6 @@ pub trait Grid<T> {
let width = self.width();
assert!(x < width, "cannot access index [{x}, {y}] because x is outside of bounds [0..{width})");
let height = self.height();
assert!(y < height, "cannot access index [{x}, {y}] because x is outside of bounds [0..{height})");
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::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 {}
impl<T: Sized + Default + Copy + Clone + Debug> Value for T {}
/// A 2D grid of values.
///
/// The memory layout is the one the display expects in [Command]s.
/// 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.
/// You can also use the concrete type aliases provided in this crate, e.g. [CharGrid] and [ByteGrid].
#[derive(Debug, Clone, PartialEq)]
/// You can also use the concrete type aliases provided in this crate, e.g. [`crate::CharGrid`] and [`crate::ByteGrid`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueGrid<T: Value> {
width: usize,
height: usize,
@ -42,15 +42,18 @@ pub enum SetValueSeriesError {
}
impl<T: Value> ValueGrid<T> {
/// Creates a new [ValueGrid] with the specified dimensions.
/// Creates a new [`ValueGrid`] with the specified dimensions.
///
/// # Arguments
///
/// - width: size in x-direction
/// - height: size in y-direction
///
/// returns: [ValueGrid] initialized to default value.
/// returns: [`ValueGrid`] initialized to default value.
#[must_use]
pub fn new(width: usize, height: usize) -> Self {
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
Self {
data: vec![Default::default(); width * height],
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
///
/// # Panics
///
/// - when the dimensions and data size do not match exactly.
/// returns: [`ValueGrid`] that contains a copy of the provided data,
/// or None if the dimensions do not match the data size.
#[must_use]
pub fn load(width: usize, height: usize, data: &[T]) -> Self {
assert_eq!(
width * height,
data.len(),
"dimension mismatch for data {data:?}"
);
Self {
data: Vec::from(data),
width,
height,
}
}
/// Creates a [ValueGrid] with the specified width from the provided data without copying it.
///
/// returns: [ValueGrid] that contains the provided data.
///
/// # Panics
///
/// - when the data size is not dividable by the width.
#[must_use]
pub fn from_vec(width: usize, data: Vec<T>) -> Self {
let len = data.len();
let height = len / width;
assert_eq!(
0,
len % width,
"dimension mismatch - len {len} is not dividable by {width}"
);
Self {
data,
width,
height,
}
}
/// Loads a [ValueGrid] with the specified width from the provided data, wrapping to as many rows as needed.
///
/// returns: [ValueGrid] that contains a copy of the provided data or [TryLoadValueGridError].
///
/// # Examples
///
/// ```
/// # use servicepoint::ValueGrid;
/// let grid = ValueGrid::wrap(2, &[0, 1, 2, 3, 4, 5]).unwrap();
/// ```
pub fn wrap(
width: usize,
data: &[T],
) -> Result<Self, TryLoadValueGridError> {
let len = data.len();
if len % width != 0 {
return Err(TryLoadValueGridError::InvalidDimensions);
}
Ok(Self::load(width, len / width, data))
}
/// Loads a [ValueGrid] with the specified dimensions from the provided data.
///
/// returns: [ValueGrid] that contains a copy of the provided data or [TryLoadValueGridError].
pub fn try_load(
width: usize,
height: usize,
data: Vec<T>,
) -> Result<Self, TryLoadValueGridError> {
pub fn load(width: usize, height: usize, data: &[T]) -> Option<Self> {
assert!(width < isize::MAX as usize);
assert!(height < isize::MAX as usize);
if width * height != data.len() {
return Err(TryLoadValueGridError::InvalidDimensions);
return None;
}
Ok(Self {
data,
Some(Self {
data: Vec::from(data),
width,
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:
/// ```
@ -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()
}
/// 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> {
IterGridRows {
byte_grid: self,
row: 0,
}
IterGridRows { grid: self, row: 0 }
}
/// Returns an iterator that allows modifying each value.
@ -200,29 +177,34 @@ impl<T: Value> ValueGrid<T> {
y: isize,
) -> Option<&mut T> {
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])
} else {
None
}
}
/// Convert between ValueGrid types.
/// Convert between `ValueGrid` types.
///
/// See also [Iterator::map].
/// See also [`Iterator::map`].
///
/// # Examples
///
/// Use logic written for u8s and then convert to [Brightness] values for sending in a [Command].
/// ```
/// # fn foo(grid: &mut ByteGrid) {}
/// # use servicepoint::{Brightness, BrightnessGrid, ByteGrid, Command, Origin, TILE_HEIGHT, TILE_WIDTH};
/// # use servicepoint::*;
/// let mut grid: ByteGrid = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT);
/// foo(&mut grid);
/// let grid: BrightnessGrid = grid.map(Brightness::saturating_from);
/// let command = Command::CharBrightness(Origin::ZERO, grid);
/// let command = BrightnessGridCommand { origin: Origin::ZERO, grid };
/// ```
/// [Brightness]: [crate::Brightness]
/// [Command]: [crate::Command]
#[must_use]
pub fn map<TConverted, F>(&self, f: F) -> ValueGrid<TConverted>
where
TConverted: Value,
@ -233,22 +215,28 @@ impl<T: Value> ValueGrid<T> {
.iter()
.map(|elem| f(*elem))
.collect::<Vec<_>>();
ValueGrid::load(self.width(), self.height(), &data)
ValueGrid {
width: self.width(),
height: self.height(),
data,
}
}
/// Copies a row from the grid.
///
/// Returns [None] if y is out of bounds.
#[must_use]
pub fn get_row(&self, y: usize) -> Option<Vec<T>> {
self.data
.chunks_exact(self.width())
.nth(y)
.map(|row| row.to_vec())
.map(<[T]>::to_vec)
}
/// Copies a column from the grid.
///
/// Returns [None] if x is out of bounds.
#[must_use]
pub fn get_col(&self, x: usize) -> Option<Vec<T>> {
self.data
.chunks_exact(self.width())
@ -305,19 +293,27 @@ impl<T: Value> ValueGrid<T> {
});
}
let chunk = match self.data.chunks_exact_mut(width).nth(y) {
Some(row) => row,
None => {
return Err(SetValueSeriesError::OutOfBounds {
size: self.height(),
index: y,
})
}
let Some(chunk) = self.data.chunks_exact_mut(width).nth(y) else {
return Err(SetValueSeriesError::OutOfBounds {
size: self.height(),
index: y,
});
};
chunk.copy_from_slice(row);
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
@ -329,7 +325,7 @@ pub enum TryLoadValueGridError {
}
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
///
@ -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> {
byte_grid: &'t ValueGrid<T>,
grid: &'t ValueGrid<T>,
row: usize,
}
@ -400,24 +397,46 @@ impl<'t, T: Value> Iterator for IterGridRows<'t, T> {
type Item = Iter<'t, T>;
fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.byte_grid.height {
if self.row >= self.grid.height {
return None;
}
let start = self.row * self.byte_grid.width;
let end = start + self.byte_grid.width;
let result = self.byte_grid.data[start..end].iter();
let start = self.row * self.grid.width;
let end = start + self.grid.width;
let result = self.grid.data[start..end].iter();
self.row += 1;
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)]
mod tests {
use crate::{
value_grid::{SetValueSeriesError, ValueGrid},
*,
};
use crate::{SetValueSeriesError, ValueGrid, *};
#[test]
fn fill() {
@ -456,7 +475,7 @@ mod tests {
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]);
}
@ -468,7 +487,7 @@ mod tests {
data_ref.copy_from_slice(&[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]
@ -495,7 +514,7 @@ mod tests {
#[test]
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 (x, val) in row.enumerate() {
assert_eq!(*val, (x + y) as u8);
@ -506,20 +525,21 @@ mod tests {
#[test]
#[should_panic]
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);
}
#[test]
#[should_panic]
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);
}
#[test]
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);
*top_left += 5;
@ -535,7 +555,7 @@ mod tests {
#[test]
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(-1, 0, 8);
grid.set_optional(0, 8, 42);
@ -547,7 +567,7 @@ mod tests {
#[test]
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(1), Some(vec![1, 3, 5]));
assert_eq!(grid.get_col(2), None);
@ -568,7 +588,7 @@ mod tests {
#[test]
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(2), Some(vec![4, 5]));
assert_eq!(grid.get_row(3), None);
@ -589,10 +609,32 @@ mod tests {
#[test]
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);
let grid = ValueGrid::wrap(4, &[0, 1, 2, 3, 4, 5]);
assert_eq!(grid.err(), Some(TryLoadValueGridError::InvalidDimensions));
let grid = ValueGrid::from_vec(4, vec![0, 1, 2, 3, 4, 5]);
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 std::collections::HashMap;
/// Contains functions to convert between UTF-8 and Codepage 437.
///
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
pub struct Cp437Converter;
/// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters
///
/// Mostly follows CP437, except 0x0A, which is kept for use as line ending.
@ -34,47 +33,47 @@ const CP437_TO_UTF8: [char; 256] = [
];
static UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
once_cell::sync::Lazy::new(|| {
let pairs = CP437_TO_UTF8
CP437_TO_UTF8
.iter()
.enumerate()
.map(move |(index, char)| (*char, index as u8));
HashMap::from_iter(pairs)
.map(
#[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.
pub fn cp437_to_str(cp437: &[u8]) -> String {
cp437
.iter()
.map(move |char| Self::cp437_to_char(*char))
.collect()
}
/// Convert the provided bytes to UTF-8.
#[must_use]
pub fn cp437_to_str(cp437: &[u8]) -> String {
cp437.iter().map(move |char| cp437_to_char(*char)).collect()
}
/// Convert a single CP-437 character to UTF-8.
pub fn cp437_to_char(cp437: u8) -> char {
CP437_TO_UTF8[cp437 as usize]
}
/// Convert a single CP-437 character to UTF-8.
#[must_use]
pub fn cp437_to_char(cp437: u8) -> char {
CP437_TO_UTF8[cp437 as usize]
}
/// Convert the provided text to CP-437 bytes.
///
/// Characters that are not available are mapped to '?'.
pub fn str_to_cp437(utf8: &str) -> Vec<u8> {
utf8.chars().map(Self::char_to_cp437).collect()
}
/// Convert the provided text to CP-437 bytes.
///
/// Characters that are not available are mapped to '?'.
#[must_use]
pub fn str_to_cp437(utf8: &str) -> Vec<u8> {
utf8.chars().map(char_to_cp437).collect()
}
/// Convert a single UTF-8 character to CP-437.
pub fn char_to_cp437(utf8: char) -> u8 {
*UTF8_TO_CP437
.get(&utf8)
.unwrap_or(&Self::MISSING_CHAR_CP437)
}
/// Convert a single UTF-8 character to CP-437.
#[must_use]
pub fn char_to_cp437(utf8: char) -> u8 {
*UTF8_TO_CP437.get(&utf8).unwrap_or(&MISSING_CHAR_CP437)
}
impl From<&Cp437Grid> for CharGrid {
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 {
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
"#;
let cp437 = Cp437Converter::str_to_cp437(utf8);
let actual = Cp437Converter::cp437_to_str(&cp437);
assert_eq!(utf8, actual)
let cp437 = str_to_cp437(utf8);
let actual = cp437_to_str(&cp437);
assert_eq!(utf8, actual);
}
#[test]
fn convert_invalid() {
assert_eq!(
Cp437Converter::cp437_to_char(Cp437Converter::char_to_cp437('😜')),
'?'
);
assert_eq!(cp437_to_char(char_to_cp437('😜')), '?');
}
#[test]
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 actual = CharGrid::from(cp437);
assert_eq!(actual, utf8);

View file

@ -9,97 +9,83 @@
//! ### Clear display
//!
//! ```rust
//! use servicepoint::{Connection, Command};
//! use std::net::UdpSocket;
//! use servicepoint::*;
//!
//! // establish a connection
//! let connection = Connection::open("127.0.0.1:2342")
//! let connection = UdpSocket::bind("127.0.0.1:2342")
//! .expect("connection failed");
//!
//! # let connection = FakeConnection; // do not fail tests
//! // turn off all pixels on display
//! connection.send(Command::Clear)
//! connection.send_command(ClearCommand)
//! .expect("send failed");
//! ```
//!
//! ### Set all pixels to on
//!
//! ```rust
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
//! # use std::net::UdpSocket;
//! # use servicepoint::*;
//! # let connection = FakeConnection;
//! // turn on all pixels in a grid
//! let mut pixels = Bitmap::max_sized();
//! pixels.fill(true);
//!
//! // create command to send pixels
//! let command = Command::BitmapLinearWin(
//! servicepoint::Origin::ZERO,
//! pixels,
//! CompressionCode::default()
//! );
//! let command = BitmapCommand {
//! origin: Origin::ZERO,
//! bitmap: pixels,
//! compression: CompressionCode::default()
//! };
//!
//! // send command to display
//! connection.send(command).expect("send failed");
//! connection.send_command(command).expect("send failed");
//! ```
//!
//! ### Send text
//!
//! ```rust
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap, CharGrid};
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
//! # use std::net::UdpSocket;
//! # use servicepoint::*;
//! # let connection = FakeConnection;
//! // create a text grid
//! let mut grid = CharGrid::from("Hello\nCCCB?");
//! // modify the grid
//! grid.set(grid.width() - 1, 1, '!');
//! // create the command to send the data
//! let command = Command::Utf8Data(servicepoint::Origin::ZERO, grid);
//! let command = CharGridCommand { origin: Origin::ZERO, grid };
//! // 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_grid::BrightnessGrid;
pub use crate::byte_grid::ByteGrid;
pub use crate::char_grid::CharGrid;
pub use crate::command::{Command, Offset};
pub use crate::command_code::CommandCode;
pub use crate::commands::*;
pub use crate::compression_code::CompressionCode;
pub use crate::connection::Connection;
pub use crate::connection::*;
pub use crate::constants::*;
pub use crate::cp437_grid::Cp437Grid;
pub use crate::data_ref::DataRef;
pub use crate::grid::Grid;
pub use crate::containers::*;
pub use crate::origin::{Origin, Pixels, Tiles};
pub use crate::packet::{Header, Packet, Payload};
pub use crate::value_grid::{
IterGridRows, SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
};
mod bit_vec;
mod bitmap;
mod brightness;
mod brightness_grid;
mod byte_grid;
mod char_grid;
mod command;
mod command_code;
mod commands;
mod compression;
mod compression_code;
mod connection;
mod constants;
mod cp437_grid;
mod data_ref;
mod grid;
mod containers;
#[cfg(feature = "cp437")]
pub mod cp437;
mod origin;
mod packet;
mod value_grid;
#[cfg(feature = "cp437")]
mod cp437;
#[cfg(feature = "cp437")]
pub use crate::cp437::Cp437Converter;
// include README.md in doctest
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
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;
/// 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> {
/// position in the width direction
pub x: usize,
@ -20,6 +20,7 @@ impl<Unit: DisplayUnit> Origin<Unit> {
};
/// Create a new [Origin] instance for the provided position.
#[must_use]
pub fn new(x: usize, y: usize) -> Self {
Self {
x,
@ -44,11 +45,11 @@ impl<T: DisplayUnit> std::ops::Add<Origin<T>> for Origin<T> {
pub trait DisplayUnit {}
/// Marks something to be measured in number of pixels.
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Pixels();
/// Marks something to be measured in number of iles.
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Tiles();
impl DisplayUnit for Pixels {}
@ -65,24 +66,9 @@ impl From<&Origin<Tiles>> for Origin<Pixels> {
}
}
impl TryFrom<&Origin<Pixels>> for Origin<Tiles> {
type Error = ();
fn try_from(value: &Origin<Pixels>) -> Result<Self, Self::Error> {
let (x, x_rem) = (value.x / TILE_SIZE, value.x % TILE_SIZE);
if x_rem != 0 {
return Err(());
}
let (y, y_rem) = (value.y / TILE_SIZE, value.y % TILE_SIZE);
if y_rem != 0 {
return Err(());
}
Ok(Self {
x,
y,
phantom_data: PhantomData,
})
impl<Unit: DisplayUnit> Default for Origin<Unit> {
fn default() -> Self {
Self::ZERO
}
}
@ -99,24 +85,10 @@ mod tests {
}
#[test]
fn origin_pixel_to_tile() {
let pixel: Origin<Pixels> = Origin::new(8, 16);
let actual: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
let expected: Origin<Tiles> = Origin::new(1, 2);
assert_eq!(actual, expected);
}
#[test]
#[should_panic]
fn origin_pixel_to_tile_fail_y() {
let pixel: Origin<Pixels> = Origin::new(8, 15);
let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
}
#[test]
#[should_panic]
fn origin_pixel_to_tile_fail_x() {
let pixel: Origin<Pixels> = Origin::new(7, 16);
let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap();
fn origin_add() {
assert_eq!(
Origin::<Pixels>::new(4, 2),
Origin::new(1, 0) + Origin::new(3, 2)
);
}
}

View file

@ -7,29 +7,24 @@
//! Converting a packet to a command and back:
//!
//! ```rust
//! use servicepoint::{Command, Packet};
//! # let command = Command::Clear;
//! use servicepoint::{Command, Packet, TypedCommand};
//! # let command = servicepoint::ClearCommand;
//! 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:
//!
//! ```rust
//! use servicepoint::{Command, Packet};
//! # let command = Command::Clear;
//! # let command = servicepoint::ClearCommand;
//! # let packet: Packet = command.into();
//! let bytes: Vec<u8> = packet.into();
//! let packet = Packet::try_from(bytes).expect("could not read packet from bytes");
//! ```
use crate::command_code::CommandCode;
use crate::compression::into_compressed;
use crate::{
Bitmap, Command, CompressionCode, Grid, Offset, Origin, Pixels, Tiles,
TILE_SIZE,
};
use std::mem::size_of;
use crate::{command_code::CommandCode, Grid, Origin, Tiles};
use std::{mem::size_of, num::TryFromIntError};
/// A raw header.
///
@ -37,7 +32,10 @@ use std::mem::size_of;
/// payload, where applicable.
///
/// Because the meaning of most fields depend on the command, there are no speaking names for them.
#[derive(Copy, Clone, Debug, PartialEq)]
///
/// 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 {
/// The first two bytes specify which command this packet represents.
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.
///
/// You may want to use [Command] instead.
///
///
/// You may want to use [`crate::Command`] or [`crate::TypedCommand`] instead.
#[derive(Clone, Debug, PartialEq)]
pub struct Packet {
/// Meta-information for the packed command
@ -74,40 +70,25 @@ pub struct Packet {
impl From<Packet> for Vec<u8> {
/// Turn the packet into raw bytes ready to send
fn from(value: Packet) -> Self {
let Packet {
header:
Header {
command_code: mode,
a,
b,
c,
d,
},
payload,
} = value;
let mut packet = vec![0u8; 10 + payload.len()];
packet[0..=1].copy_from_slice(&u16::to_be_bytes(mode));
packet[2..=3].copy_from_slice(&u16::to_be_bytes(a));
packet[4..=5].copy_from_slice(&u16::to_be_bytes(b));
packet[6..=7].copy_from_slice(&u16::to_be_bytes(c));
packet[8..=9].copy_from_slice(&u16::to_be_bytes(d));
packet[10..].copy_from_slice(&payload);
packet
let mut vec = vec![0u8; value.size()];
value.serialize_to(vec.as_mut_slice());
vec
}
}
#[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 {
type Error = ();
type Error = SliceSmallerThanHeader;
/// Tries to interpret the bytes as a [Packet].
///
/// returns: `Error` if slice is not long enough to be a [Packet]
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() < size_of::<Header>() {
return Err(());
return Err(SliceSmallerThanHeader);
}
let header = {
@ -131,165 +112,48 @@ impl TryFrom<&[u8]> for Packet {
}
impl TryFrom<Vec<u8>> for Packet {
type Error = ();
type Error = SliceSmallerThanHeader;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}
impl From<Command> for Packet {
/// Move the [Command] into a [Packet] instance for sending.
#[allow(clippy::cast_possible_truncation)]
fn from(value: Command) -> Self {
match value {
Command::Clear => Self::command_code_only(CommandCode::Clear),
Command::FadeOut => Self::command_code_only(CommandCode::FadeOut),
Command::HardReset => {
Self::command_code_only(CommandCode::HardReset)
}
#[allow(deprecated)]
Command::BitmapLegacy => {
Self::command_code_only(CommandCode::BitmapLegacy)
}
Command::CharBrightness(origin, grid) => {
Self::origin_grid_to_packet(
origin,
grid,
CommandCode::CharBrightness,
)
}
Command::Brightness(brightness) => Packet {
header: Header {
command_code: CommandCode::Brightness.into(),
a: 0x00000,
b: 0x0000,
c: 0x0000,
d: 0x0000,
},
payload: vec![brightness.into()],
},
Command::BitmapLinearWin(origin, pixels, compression) => {
Self::bitmap_win_into_packet(origin, pixels, compression)
}
Command::BitmapLinear(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinear,
offset,
compression,
bits.into(),
)
}
Command::BitmapLinearAnd(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinearAnd,
offset,
compression,
bits.into(),
)
}
Command::BitmapLinearOr(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinearOr,
offset,
compression,
bits.into(),
)
}
Command::BitmapLinearXor(offset, bits, compression) => {
Self::bitmap_linear_into_packet(
CommandCode::BitmapLinearXor,
offset,
compression,
bits.into(),
)
}
Command::Cp437Data(origin, grid) => Self::origin_grid_to_packet(
origin,
grid,
CommandCode::Cp437Data,
),
Command::Utf8Data(origin, grid) => {
Self::origin_grid_to_packet(origin, grid, CommandCode::Utf8Data)
}
}
}
}
impl Packet {
/// Helper method for `BitmapLinear*`-Commands into [Packet]
#[allow(clippy::cast_possible_truncation)]
fn bitmap_linear_into_packet(
command: CommandCode,
offset: Offset,
compression: CompressionCode,
payload: Vec<u8>,
) -> Packet {
let length = payload.len() as u16;
let payload = into_compressed(compression, payload);
Packet {
header: Header {
command_code: command.into(),
a: offset as u16,
b: length,
c: compression.into(),
d: 0,
},
payload,
/// Serialize packet into pre-allocated buffer.
///
/// returns false if the buffer is too small before writing any values.
pub fn serialize_to(&self, slice: &mut [u8]) -> bool {
if slice.len() < self.size() {
return false;
}
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)]
fn bitmap_win_into_packet(
origin: Origin<Pixels>,
pixels: Bitmap,
compression: CompressionCode,
) -> Packet {
debug_assert_eq!(origin.x % 8, 0);
debug_assert_eq!(pixels.width() % 8, 0);
let tile_x = (origin.x / TILE_SIZE) as u16;
let tile_w = (pixels.width() / TILE_SIZE) as u16;
let pixel_h = pixels.height() as u16;
let payload = into_compressed(compression, pixels.into());
let command = match compression {
CompressionCode::Uncompressed => {
CommandCode::BitmapLinearWinUncompressed
}
#[cfg(feature = "compression_zlib")]
CompressionCode::Zlib => CommandCode::BitmapLinearWinZlib,
#[cfg(feature = "compression_bzip2")]
CompressionCode::Bzip2 => CommandCode::BitmapLinearWinBzip2,
#[cfg(feature = "compression_lzma")]
CompressionCode::Lzma => CommandCode::BitmapLinearWinLzma,
#[cfg(feature = "compression_zstd")]
CompressionCode::Zstd => CommandCode::BitmapLinearWinZstd,
};
Packet {
header: Header {
command_code: command.into(),
a: tile_x,
b: origin.y as u16,
c: tile_w,
d: pixel_h,
},
payload,
}
}
/// Helper method for creating empty packets only containing the command code
fn command_code_only(code: CommandCode) -> Packet {
Packet {
header: Header {
command_code: code.into(),
a: 0x0000,
b: 0x0000,
c: 0x0000,
d: 0x0000,
},
payload: vec![],
}
/// Returns the amount of bytes this packet takes up when serialized.
#[must_use]
pub fn size(&self) -> usize {
size_of::<Header>() + self.payload.len()
}
fn u16_from_be_slice(slice: &[u8]) -> u16 {
@ -299,20 +163,30 @@ impl Packet {
u16::from_be_bytes(bytes)
}
fn origin_grid_to_packet<T>(
pub(crate) fn origin_grid_to_packet<T>(
origin: Origin<Tiles>,
grid: impl Grid<T> + Into<Payload>,
command_code: CommandCode,
) -> Packet {
Packet {
) -> Result<Packet, TryFromIntError> {
Ok(Packet {
header: Header {
command_code: command_code.into(),
a: origin.x as u16,
b: origin.y as u16,
c: grid.width() as u16,
d: grid.height() as u16,
a: origin.x.try_into()?,
b: origin.y.try_into()?,
c: grid.width().try_into()?,
d: grid.height().try_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]
fn too_small() {
let data = vec![0u8; 4];
assert_eq!(Packet::try_from(data.as_slice()), Err(()))
assert_eq!(
Packet::try_from(data.as_slice()),
Err(SliceSmallerThanHeader)
);
}
}