diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d1f62c4..d8794a6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/.gitignore b/.gitignore index f48287d..79fb4fd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ out .direnv .envrc result -mutants.* \ No newline at end of file +mutants.* +tarpaulin-report.html diff --git a/Cargo.lock b/Cargo.lock index d0e0cf4..1187d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index b8c6fde..991edde 100644 --- a/Cargo.toml +++ b/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 \ No newline at end of file diff --git a/README.md b/README.md index 633e5c4..af45b8b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # servicepoint +[![Release](https://git.berlin.ccc.de/servicepoint/servicepoint/badges/release.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint/releases) [![crates.io](https://img.shields.io/crates/v/servicepoint.svg)](https://crates.io/crates/servicepoint) [![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint)](https://crates.io/crates/servicepoint) [![docs.rs](https://img.shields.io/docsrs/servicepoint)](https://docs.rs/servicepoint/latest/servicepoint/) [![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint)](./LICENSE) +[![CI](https://git.berlin.ccc.de/servicepoint/servicepoint/badges/workflows/rust.yml/badge.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint) In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point Display" or "Airport Display". @@ -12,24 +14,22 @@ This crate contains a library for parsing, encoding and sending packets to this The library itself is written in Rust, but can be used from multiple languages via [language bindings](#supported-language-bindings). -This project moved -to [git.berlin.ccc.de/servicepoint/servicepoint](https://git.berlin.ccc.de/servicepoint/servicepoint). -The [GitHub repository](https://github.com/cccb/servicepoint) remains available as a mirror. - ## Examples ```rust no_run +use std::net::UdpSocket; // everything you need is in the top-level use servicepoint::*; fn main() { - // establish connection - let connection = Connection::open("172.23.42.29:2342") - .expect("connection failed"); + // this should be the IP of the real display @CCCB + let destination = "172.23.42.29:2342"; - // clear screen content - connection.send(Command::Clear) - .expect("send failed"); + // establish connection + let connection = UdpSocket::bind(destination).expect("connection failed"); + + // clear screen content using the UdpSocketExt + connection.send_command(ClearCommand).expect("send failed"); } ``` @@ -46,7 +46,7 @@ or ```toml [dependencies] -servicepoint = "0.13.2" +servicepoint = "0.14.0" ``` ## Note on stability @@ -61,21 +61,32 @@ There should be no breaking changes in patch releases, but there may also be fea All of this means for you: please specify the full version including patch in your Cargo.toml until 1.0 is released. +Release notes are published [here](https://git.berlin.ccc.de/servicepoint/servicepoint/releases), please check them before updating. + +Currently, this crate requires Rust [v1.70](https://releases.rs/docs/1.70.0/) from June 2023. + ## Features This library has multiple optional dependencies. You can choose to (not) include them by toggling the related features. -| Name | Default | Description | Dependencies | -|--------------------|---------|----------------------------------------------|-----------------------------------------------------| -| protocol_udp | true | `Connection::Udp` | | -| cp437 | true | Conversion to and from CP-437 | [once_cell](https://crates.io/crates/once_cell) | -| compression_lzma | true | Enable additional compression algo | [rust-lzma](https://crates.io/crates/rust-lzma) | -| compression_zlib | false | Enable additional compression algo | [flate2](https://crates.io/crates/flate2) | -| compression_bzip2 | false | Enable additional compression algo | [bzip2](https://crates.io/crates/bzip2) | -| compression_zstd | false | Enable additional compression algo | [zstd](https://crates.io/crates/zstd) | -| protocol_websocket | false | `Connection::WebSocket` | [tungstenite](https://crates.io/crates/tungstenite) | -| rand | false | `impl Distribution 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 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. diff --git a/examples/announce.rs b/examples/announce.rs index 05d2b19..0eaa582 100644 --- a/examples/announce.rs +++ b/examples/announce.rs @@ -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"); } diff --git a/examples/brightness_tester.rs b/examples/brightness_tester.rs index ae69fa8..de4ba72 100644 --- a/examples/brightness_tester.rs +++ b/examples/brightness_tester.rs @@ -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"); } diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 9e63fe2..7f32398 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -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); } } diff --git a/examples/moving_line.rs b/examples/moving_line.rs index 540d461..17eaf8d 100644 --- a/examples/moving_line.rs +++ b/examples/moving_line.rs @@ -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); } } diff --git a/examples/random_brightness.rs b/examples/random_brightness.rs index a260064..7bf5ae5 100644 --- a/examples/random_brightness.rs +++ b/examples/random_brightness.rs @@ -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::().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); } diff --git a/examples/tiny_announce.rs b/examples/tiny_announce.rs new file mode 100644 index 0000000..34bfbfe --- /dev/null +++ b/examples/tiny_announce.rs @@ -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 +} diff --git a/examples/websocket.rs b/examples/websocket.rs deleted file mode 100644 index 9bc8d10..0000000 --- a/examples/websocket.rs +++ /dev/null @@ -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(); -} diff --git a/examples/wiping_clear.rs b/examples/wiping_clear.rs index 2ad88d7..ec126be 100644 --- a/examples/wiping_clear.rs +++ b/examples/wiping_clear.rs @@ -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); } diff --git a/flake.lock b/flake.lock index b07ad46..8d33f0e 100644 --- a/flake.lock +++ b/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": { diff --git a/flake.nix b/flake.nix index a13a828..abba3dd 100644 --- a/flake.nix +++ b/flake.nix @@ -137,6 +137,9 @@ clippy cargo-expand cargo-tarpaulin + cargo-semver-checks + cargo-show-asm + cargo-flamegraph ]; }) ]; diff --git a/generate-coverage b/generate-coverage new file mode 100755 index 0000000..860efbb --- /dev/null +++ b/generate-coverage @@ -0,0 +1,2 @@ +#/usr/bin/env bash +cargo tarpaulin --out Html --all-features diff --git a/src/bit_vec.rs b/src/bit_vec.rs deleted file mode 100644 index 2ece813..0000000 --- a/src/bit_vec.rs +++ /dev/null @@ -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; - -pub mod bitvec { - //! Re-export of the used library [mod@bitvec]. - pub use bitvec::prelude::*; -} diff --git a/src/brightness.rs b/src/brightness.rs index be57690..193feb2 100644 --- a/src/brightness.rs +++ b/src/brightness.rs @@ -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 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::(), rng.gen()); + // two so test failure is less likely + assert_ne!( + [rng.r#gen::(), rng.r#gen()], + [rng.r#gen(), rng.r#gen()] + ); } } diff --git a/src/command.rs b/src/command.rs deleted file mode 100644 index e3abd5e..0000000 --- a/src/command.rs +++ /dev/null @@ -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, CharGrid), - - /// Show text on the screen. - /// - /// The text is sent in the form of a 2D grid of [CP-437] encoded characters. - /// - ///
You probably want to use [Command::Utf8Data] instead
- /// - /// # 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, 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, 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, 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, - - ///
Untested
- /// - /// 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 for Command { - type Error = TryFromPacketError; - - /// Try to interpret the [Packet] as one containing a [Command] - fn try_from(packet: Packet) -> Result { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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::::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)) - ); - } -} diff --git a/src/command_code.rs b/src/command_code.rs index 0917ec9..76856ea 100644 --- a/src/command_code.rs +++ b/src/command_code.rs @@ -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 for u16 { } } +#[derive(Debug, thiserror::Error, Eq, PartialEq)] +#[error("The command code {0} is not known.")] +pub struct InvalidCommandCodeError(pub u16); + impl TryFrom 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 { @@ -97,7 +102,7 @@ impl TryFrom for CommandCode { value if value == CommandCode::Utf8Data as u16 => { Ok(CommandCode::Utf8Data) } - _ => Err(()), + _ => Err(InvalidCommandCodeError(value)), } } } diff --git a/src/commands/bitmap.rs b/src/commands/bitmap.rs new file mode 100644 index 0000000..92419e5 --- /dev/null +++ b/src/commands/bitmap.rs @@ -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, + /// how to compress the command when converting to packet + pub compression: CompressionCode, +} + +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: BitmapCommand) -> Result { + 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 for BitmapCommand { + type Error = TryFromPacketError; + + fn try_from(packet: Packet) -> Result { + 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 for TypedCommand { + fn from(command: BitmapCommand) -> Self { + Self::Bitmap(command) + } +} + +impl From 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 { + 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() + }, + ) + } +} diff --git a/src/commands/bitmap_legacy.rs b/src/commands/bitmap_legacy.rs new file mode 100644 index 0000000..67d2770 --- /dev/null +++ b/src/commands/bitmap_legacy.rs @@ -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 for BitmapLegacyCommand { + type Error = TryFromPacketError; + + fn try_from(value: Packet) -> Result { + if let Some(e) = + check_command_code_only(value, CommandCode::BitmapLegacy) + { + Err(e) + } else { + Ok(Self) + } + } +} + +#[allow(deprecated)] +impl From for Packet { + fn from(_: BitmapLegacyCommand) -> Self { + Packet::command_code_only(CommandCode::BitmapLegacy) + } +} + +#[allow(deprecated)] +impl From 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()); + } +} diff --git a/src/commands/bitvec.rs b/src/commands/bitvec.rs new file mode 100644 index 0000000..7ff79dd --- /dev/null +++ b/src/commands/bitvec.rs @@ -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 for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: BitVecCommand) -> Result { + 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 for BitVecCommand { + type Error = TryFromPacketError; + + fn try_from(packet: Packet) -> Result { + 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 for TypedCommand { + fn from(command: BitVecCommand) -> Self { + Self::BitVec(command) + } +} + +impl From 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, + }, + ) + } +} diff --git a/src/commands/brightness_grid.rs b/src/commands/brightness_grid.rs new file mode 100644 index 0000000..532ca46 --- /dev/null +++ b/src/commands/brightness_grid.rs @@ -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, +} + +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: BrightnessGridCommand) -> Result { + Ok(Packet::origin_grid_to_packet( + value.origin, + value.grid, + CommandCode::CharBrightness, + )?) + } +} + +impl From for BrightnessGridCommand { + fn from(grid: BrightnessGrid) -> Self { + Self { + grid, + origin: Origin::default(), + } + } +} + +impl TryFrom for BrightnessGridCommand { + type Error = TryFromPacketError; + + fn try_from(packet: Packet) -> Result { + 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 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) + ); + } +} diff --git a/src/commands/char_grid.rs b/src/commands/char_grid.rs new file mode 100644 index 0000000..84d3d90 --- /dev/null +++ b/src/commands/char_grid.rs @@ -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, +} + +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: CharGridCommand) -> Result { + Ok(Packet::origin_grid_to_packet( + value.origin, + value.grid, + CommandCode::Utf8Data, + )?) + } +} + +impl TryFrom for CharGridCommand { + type Error = TryFromPacketError; + + fn try_from(packet: Packet) -> Result { + 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 for TypedCommand { + fn from(command: CharGridCommand) -> Self { + Self::CharGrid(command) + } +} + +impl From 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) + ); + } +} diff --git a/src/commands/clear.rs b/src/commands/clear.rs new file mode 100644 index 0000000..186b212 --- /dev/null +++ b/src/commands/clear.rs @@ -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 for ClearCommand { + type Error = TryFromPacketError; + + fn try_from(value: Packet) -> Result { + if let Some(e) = check_command_code_only(value, CommandCode::Clear) { + Err(e) + } else { + Ok(Self) + } + } +} + +impl From for Packet { + fn from(_: ClearCommand) -> Self { + Packet::command_code_only(CommandCode::Clear) + } +} + +impl From 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) + ); + } +} diff --git a/src/commands/cp437_grid.rs b/src/commands/cp437_grid.rs new file mode 100644 index 0000000..f4aa6aa --- /dev/null +++ b/src/commands/cp437_grid.rs @@ -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. +/// +///
You probably want to use [Command::Utf8Data] instead
+/// +/// # 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, +} + +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: Cp437GridCommand) -> Result { + Ok(Packet::origin_grid_to_packet( + value.origin, + value.grid, + CommandCode::Cp437Data, + )?) + } +} + +impl TryFrom for Cp437GridCommand { + type Error = TryFromPacketError; + + fn try_from(packet: Packet) -> Result { + 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 for TypedCommand { + fn from(command: Cp437GridCommand) -> Self { + Self::Cp437Grid(command) + } +} + +impl From 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) + ); + } +} diff --git a/src/commands/errors.rs b/src/commands/errors.rs new file mode 100644 index 0000000..bdb9b62 --- /dev/null +++ b/src/commands/errors.rs @@ -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), +} diff --git a/src/commands/fade_out.rs b/src/commands/fade_out.rs new file mode 100644 index 0000000..c5a6a84 --- /dev/null +++ b/src/commands/fade_out.rs @@ -0,0 +1,103 @@ +use crate::{ + command_code::CommandCode, commands::check_command_code_only, + commands::errors::TryFromPacketError, Packet, TypedCommand, +}; +use std::fmt::Debug; + +///
Untested
+/// +/// 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 for FadeOutCommand { + type Error = TryFromPacketError; + + fn try_from(value: Packet) -> Result { + if let Some(e) = check_command_code_only(value, CommandCode::FadeOut) { + Err(e) + } else { + Ok(Self) + } + } +} + +impl From for Packet { + fn from(_: FadeOutCommand) -> Self { + Packet::command_code_only(CommandCode::FadeOut) + } +} + +impl From 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 + }) + )); + } +} diff --git a/src/commands/global_brightness.rs b/src/commands/global_brightness.rs new file mode 100644 index 0000000..a487c18 --- /dev/null +++ b/src/commands/global_brightness.rs @@ -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 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 for GlobalBrightnessCommand { + type Error = TryFromPacketError; + + fn try_from(packet: Packet) -> Result { + 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 for TypedCommand { + fn from(command: GlobalBrightnessCommand) -> Self { + Self::Brightness(command) + } +} + +impl From 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, + }, + ) + } +} diff --git a/src/commands/hard_reset.rs b/src/commands/hard_reset.rs new file mode 100644 index 0000000..a0753c9 --- /dev/null +++ b/src/commands/hard_reset.rs @@ -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 for HardResetCommand { + type Error = TryFromPacketError; + + fn try_from(value: Packet) -> Result { + if let Some(e) = check_command_code_only(value, CommandCode::HardReset) + { + Err(e) + } else { + Ok(Self) + } + } +} + +impl From for Packet { + fn from(_: HardResetCommand) -> Self { + Packet::command_code_only(CommandCode::HardReset) + } +} + +impl From 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) + )); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..6d04135 --- /dev/null +++ b/src/commands/mod.rs @@ -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 + TryFrom +{ +} + +impl + TryFrom> Command for T {} + +fn check_command_code_only( + packet: Packet, + code: CommandCode, +) -> Option { + 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); + } +} diff --git a/src/commands/typed.rs b/src/commands/typed.rs new file mode 100644 index 0000000..5314a7c --- /dev/null +++ b/src/commands/typed.rs @@ -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 for TypedCommand { + type Error = TryFromPacketError; + + /// Try to interpret the [Packet] as one containing a [`TypedCommand`] + fn try_from(packet: Packet) -> Result { + 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 for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: TypedCommand) -> Result { + 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())); + } +} diff --git a/src/compression.rs b/src/compression.rs index 2e78073..2d27fb0 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -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 { 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() } } } diff --git a/src/compression_code.rs b/src/compression_code.rs index d48fff2..7bcca38 100644 --- a/src/compression_code.rs +++ b/src/compression_code.rs @@ -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 for u16 { fn from(value: CompressionCode) -> Self { value as u16 @@ -38,7 +65,7 @@ impl From for u16 { } impl TryFrom for CompressionCode { - type Error = (); + type Error = InvalidCompressionCodeError; fn try_from(value: u16) -> Result { match value { @@ -61,7 +88,7 @@ impl TryFrom for CompressionCode { value if value == CompressionCode::Zstd as u16 => { Ok(CompressionCode::Zstd) } - _ => Err(()), + _ => Err(InvalidCompressionCodeError(value)), } } } diff --git a/src/connection.rs b/src/connection.rs index 417fd1d..880283e 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -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; - /// 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, - >, - >, - ), - - /// 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) -> 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 { - 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 { + 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 { - 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) -> Result<(), SendError> { - let packet = packet.into(); - log::debug!("sending {packet:?}"); - let data: Vec = 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) -> 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) -> Option<()> { + _ = self; // suppress unused warning + let packet = command.try_into().ok()?; + drop(Vec::from(packet)); + Some(()) } } diff --git a/src/constants.rs b/src/constants.rs index 9ea4376..ff97766 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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 diff --git a/src/containers/bit_vec.rs b/src/containers/bit_vec.rs new file mode 100644 index 0000000..f24f560 --- /dev/null +++ b/src/containers/bit_vec.rs @@ -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; + +pub mod bitvec { + //! Re-export of the used library [`::bitvec`]. + pub use ::bitvec::prelude::*; +} diff --git a/src/bitmap.rs b/src/containers/bitmap.rs similarity index 58% rename from src/bitmap.rs rename to src/containers/bitmap.rs index 331a878..5ded69e 100644 --- a/src/bitmap.rs +++ b/src/containers/bitmap.rs @@ -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 { + 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 { + 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 { + 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 { 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> { IterRows { bitmap: self, row: 0, @@ -185,7 +191,7 @@ impl Grid 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 for Vec { } } -impl From for BitVec { - /// Turns a [Bitmap] into the underlying [BitVec]. +impl From for DisplayBitVec { + /// Turns a [Bitmap] into the underlying [`DisplayBitVec`]. fn from(value: Bitmap) -> Self { value.bit_vec } } -impl From<&ValueGrid> for Bitmap { +impl TryFrom<&ValueGrid> 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) -> 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) -> Result { + 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 { } } -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 = 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); + } } diff --git a/src/brightness_grid.rs b/src/containers/brightness_grid.rs similarity index 68% rename from src/brightness_grid.rs rename to src/containers/brightness_grid.rs index 80cf327..3fd1b73 100644 --- a/src/brightness_grid.rs +++ b/src/containers/brightness_grid.rs @@ -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; 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 { + 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::>(); - ValueGrid::load(value.width(), value.height(), &u8s) + Self::from_raw_parts_unchecked(value.width(), value.height(), u8s) } } @@ -52,18 +58,17 @@ impl TryFrom for BrightnessGrid { .iter() .map(|b| Brightness::try_from(*b)) .collect::, _>>()?; - 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() ); } } diff --git a/src/byte_grid.rs b/src/containers/byte_grid.rs similarity index 56% rename from src/byte_grid.rs rename to src/containers/byte_grid.rs index 0a7fdae..5f8c98a 100644 --- a/src/byte_grid.rs +++ b/src/containers/byte_grid.rs @@ -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; diff --git a/src/char_grid.rs b/src/containers/char_grid.rs similarity index 84% rename from src/char_grid.rs rename to src/containers/char_grid.rs index d1a3fd7..24f62ff 100644 --- a/src/char_grid.rs +++ b/src/containers/char_grid.rs @@ -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; 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 { 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 { 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::>().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, ) -> Result { let s: Vec = 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 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 { - /// Converts a [CharGrid] into a [`Vec`]. + /// Converts a [`CharGrid`] into a [`Vec`]. /// /// Rows are not separated. /// @@ -223,7 +231,7 @@ impl From<&CharGrid> for Vec { /// let grid = CharGrid::load_utf8(width, height, grid.into()); /// ``` fn from(value: &CharGrid) -> Self { - String::from_iter(value.iter()).into_bytes() + value.iter().collect::().into_bytes() } } diff --git a/src/cp437_grid.rs b/src/containers/cp437_grid.rs similarity index 91% rename from src/cp437_grid.rs rename to src/containers/cp437_grid.rs index 00a7692..b74eba7 100644 --- a/src/cp437_grid.rs +++ b/src/containers/cp437_grid.rs @@ -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; +pub type Cp437Grid = ValueGrid; /// 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 diff --git a/src/data_ref.rs b/src/containers/data_ref.rs similarity index 100% rename from src/data_ref.rs rename to src/containers/data_ref.rs diff --git a/src/grid.rs b/src/containers/grid.rs similarity index 84% rename from src/grid.rs rename to src/containers/grid.rs index 68fe102..1e4a6b3 100644 --- a/src/grid.rs +++ b/src/containers/grid.rs @@ -31,6 +31,10 @@ pub trait Grid { /// returns: Value at position or None fn get_optional(&self, x: isize, y: isize) -> Option { 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 { /// 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 { 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 { 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})"); } } diff --git a/src/containers/mod.rs b/src/containers/mod.rs new file mode 100644 index 0000000..bdf0442 --- /dev/null +++ b/src/containers/mod.rs @@ -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, +}; diff --git a/src/value_grid.rs b/src/containers/value_grid.rs similarity index 72% rename from src/value_grid.rs rename to src/containers/value_grid.rs index 1f599d6..117406e 100644 --- a/src/value_grid.rs +++ b/src/containers/value_grid.rs @@ -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 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 { width: usize, height: usize, @@ -42,15 +42,18 @@ pub enum SetValueSeriesError { } impl ValueGrid { - /// 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 ValueGrid { } } - /// 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) -> 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 { - 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, - ) -> Result { + pub fn load(width: usize, height: usize, data: &[T]) -> Option { + 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) -> Option { + 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, + ) -> 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 ValueGrid { /// } /// } /// ``` - pub fn iter(&self) -> Iter { + pub fn iter(&self) -> impl Iterator { 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 { - IterGridRows { - byte_grid: self, - row: 0, - } + IterGridRows { grid: self, row: 0 } } /// Returns an iterator that allows modifying each value. @@ -200,29 +177,34 @@ impl ValueGrid { 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(&self, f: F) -> ValueGrid where TConverted: Value, @@ -233,22 +215,28 @@ impl ValueGrid { .iter() .map(|elem| f(*elem)) .collect::>(); - 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> { 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> { self.data .chunks_exact(self.width()) @@ -305,19 +293,27 @@ impl ValueGrid { }); } - 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 + 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 Grid for ValueGrid { - /// 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 From> for Vec { } } -/// 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, + grid: &'t ValueGrid, 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 { - 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, + row: usize, + column: usize, +} + +impl Iterator for EnumerateGrid<'_, T> { + type Item = (usize, usize, T); + + fn next(&mut self) -> Option { + 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 = 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::>(); + assert_eq!( + values, + vec![ + (0, 0, 0), + (1, 0, 1), + (0, 1, 2), + (1, 1, 3), + (0, 2, 4), + (1, 2, 5) + ] + ); } } diff --git a/src/cp437.rs b/src/cp437.rs index ed2f3f7..b374b1e 100644 --- a/src/cp437.rs +++ b/src/cp437.rs @@ -1,11 +1,10 @@ +//! Contains functions to convert between UTF-8 and Codepage 437. +//! +//! See + use crate::{CharGrid, Cp437Grid}; use std::collections::HashMap; -/// Contains functions to convert between UTF-8 and Codepage 437. -/// -/// See -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> = 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::>() }); -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 { - 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 { + 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 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); diff --git a/src/lib.rs b/src/lib.rs index c49481c..d9a72f8 100644 --- a/src/lib.rs +++ b/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; diff --git a/src/origin.rs b/src/origin.rs index 345b89e..8a55bb2 100644 --- a/src/origin.rs +++ b/src/origin.rs @@ -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 { /// position in the width direction pub x: usize, @@ -20,6 +20,7 @@ impl Origin { }; /// 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 std::ops::Add> for Origin { 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> for Origin { } } -impl TryFrom<&Origin> for Origin { - type Error = (); - - fn try_from(value: &Origin) -> Result { - 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 Default for Origin { + fn default() -> Self { + Self::ZERO } } @@ -99,24 +85,10 @@ mod tests { } #[test] - fn origin_pixel_to_tile() { - let pixel: Origin = Origin::new(8, 16); - let actual: Origin = Origin::try_from(&pixel).unwrap(); - let expected: Origin = Origin::new(1, 2); - assert_eq!(actual, expected); - } - - #[test] - #[should_panic] - fn origin_pixel_to_tile_fail_y() { - let pixel: Origin = Origin::new(8, 15); - let _: Origin = Origin::try_from(&pixel).unwrap(); - } - - #[test] - #[should_panic] - fn origin_pixel_to_tile_fail_x() { - let pixel: Origin = Origin::new(7, 16); - let _: Origin = Origin::try_from(&pixel).unwrap(); + fn origin_add() { + assert_eq!( + Origin::::new(4, 2), + Origin::new(1, 0) + Origin::new(3, 2) + ); } } diff --git a/src/packet.rs b/src/packet.rs index 099c50c..ad690a6 100644 --- a/src/packet.rs +++ b/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 = 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; /// /// 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 for Vec { /// 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 { if value.len() < size_of::
() { - return Err(()); + return Err(SliceSmallerThanHeader); } let header = { @@ -131,165 +112,48 @@ impl TryFrom<&[u8]> for Packet { } impl TryFrom> for Packet { - type Error = (); + type Error = SliceSmallerThanHeader; fn try_from(value: Vec) -> Result { Self::try_from(value.as_slice()) } } -impl From 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, - ) -> 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: 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::
() + 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( + pub(crate) fn origin_grid_to_packet( origin: Origin, grid: impl Grid + Into, command_code: CommandCode, - ) -> Packet { - Packet { + ) -> Result { + 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) + ); } }