Compare commits
57 commits
59137b6357
...
e509e067dd
Author | SHA1 | Date | |
---|---|---|---|
|
e509e067dd | ||
![]() |
663adde30a | ||
![]() |
4984197d95 | ||
![]() |
8e47d3c966 | ||
![]() |
b08701c342 | ||
![]() |
473bbbc3f9 | ||
![]() |
8ddbaeaaa6 | ||
![]() |
de8a1a2fe8 | ||
![]() |
114385868a | ||
![]() |
cecccb3f28 | ||
![]() |
4e3d1ce208 | ||
![]() |
e985140417 | ||
![]() |
ff56215c06 | ||
![]() |
75d24f6587 | ||
![]() |
62d1666ec2 | ||
![]() |
b6f5f74fa4 | ||
![]() |
fe67160974 | ||
![]() |
739675a9f5 | ||
![]() |
b3bf57301a | ||
![]() |
1cf37413e6 | ||
![]() |
cbab86bd93 | ||
![]() |
b69e7df635 | ||
![]() |
fe1aa3ebd1 | ||
![]() |
373d0efe55 | ||
![]() |
eba1a6a6be | ||
![]() |
617c37c713 | ||
![]() |
ff886ca27d | ||
![]() |
61f83a7042 | ||
![]() |
9f239ec71d | ||
![]() |
5ba01ec4cc | ||
![]() |
3384cc4ee9 | ||
![]() |
2c3d31f649 | ||
![]() |
fbd42d7c47 | ||
![]() |
05ab631eb6 | ||
![]() |
bf2b320c81 | ||
![]() |
2d72ee05a7 | ||
![]() |
5e38ced392 | ||
![]() |
44fe6961e7 | ||
![]() |
08ed6a6fee | ||
![]() |
4ccbd57ba8 | ||
![]() |
28f0bd5903 | ||
![]() |
18db901fb5 | ||
![]() |
b178b48834 | ||
![]() |
7cd26cd50e | ||
![]() |
c8a38870d1 | ||
![]() |
d6229ece87 | ||
![]() |
03f84c337f | ||
![]() |
2ff49aaf7a | ||
![]() |
159abd36d9 | ||
![]() |
8022b65991 | ||
![]() |
e3fc56c200 | ||
![]() |
9bff9bd346 | ||
![]() |
427dd93088 | ||
![]() |
d195f6100a | ||
![]() |
c66e6db498 | ||
![]() |
b691ef33f8 | ||
![]() |
111f35b242 |
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ out
|
|||
.envrc
|
||||
result
|
||||
mutants.*
|
||||
tarpaulin-report.html
|
||||
|
|
288
Cargo.lock
generated
288
Cargo.lock
generated
|
@ -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",
|
||||
|
|
55
Cargo.toml
55
Cargo.toml
|
@ -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
|
96
README.md
96
README.md
|
@ -1,9 +1,11 @@
|
|||
# servicepoint
|
||||
|
||||
[](https://git.berlin.ccc.de/servicepoint/servicepoint/releases)
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://crates.io/crates/servicepoint)
|
||||
[](https://docs.rs/servicepoint/latest/servicepoint/)
|
||||
[](./LICENSE)
|
||||
[](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.
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
47
examples/tiny_announce.rs
Normal 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
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
12
flake.lock
12
flake.lock
|
@ -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": {
|
||||
|
|
|
@ -137,6 +137,9 @@
|
|||
clippy
|
||||
cargo-expand
|
||||
cargo-tarpaulin
|
||||
cargo-semver-checks
|
||||
cargo-show-asm
|
||||
cargo-flamegraph
|
||||
];
|
||||
})
|
||||
];
|
||||
|
|
2
generate-coverage
Executable file
2
generate-coverage
Executable file
|
@ -0,0 +1,2 @@
|
|||
#/usr/bin/env bash
|
||||
cargo tarpaulin --out Html --all-features
|
|
@ -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::*;
|
||||
}
|
|
@ -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()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
968
src/command.rs
968
src/command.rs
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
244
src/commands/bitmap.rs
Normal 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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
83
src/commands/bitmap_legacy.rs
Normal file
83
src/commands/bitmap_legacy.rs
Normal 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
350
src/commands/bitvec.rs
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
158
src/commands/brightness_grid.rs
Normal file
158
src/commands/brightness_grid.rs
Normal 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
147
src/commands/char_grid.rs
Normal 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
94
src/commands/clear.rs
Normal 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
152
src/commands/cp437_grid.rs
Normal 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
54
src/commands/errors.rs
Normal 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
103
src/commands/fade_out.rs
Normal 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
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
198
src/commands/global_brightness.rs
Normal file
198
src/commands/global_brightness.rs
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
77
src/commands/hard_reset.rs
Normal file
77
src/commands/hard_reset.rs
Normal 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
143
src/commands/mod.rs
Normal 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
133
src/commands/typed.rs
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
10
src/containers/bit_vec.rs
Normal 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::*;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>;
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
21
src/containers/mod.rs
Normal 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,
|
||||
};
|
|
@ -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)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
82
src/cp437.rs
82
src/cp437.rs
|
@ -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);
|
||||
|
|
74
src/lib.rs
74
src/lib.rs
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
269
src/packet.rs
269
src/packet.rs
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue