diff --git a/Cargo.lock b/Cargo.lock index 9328e45..038deb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,12 +84,27 @@ 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.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + [[package]] name = "bzip2" version = "0.4.4" @@ -132,9 +147,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.18" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1" dependencies = [ "jobserver", "libc", @@ -149,9 +164,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -159,9 +174,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -171,9 +186,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -193,6 +208,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -202,6 +226,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 = "csbindgen" version = "1.9.3" @@ -212,6 +246,22 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[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 = "equivalent" version = "1.0.1" @@ -236,20 +286,36 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -263,9 +329,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -280,10 +346,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "indexmap" -version = "2.5.0" +name = "http" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", @@ -320,9 +403,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "linux-raw-sys" @@ -353,15 +436,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "ppv-lite86" @@ -374,9 +457,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -428,9 +511,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -440,9 +523,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -451,9 +534,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rust-lzma" @@ -467,9 +550,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.36" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", @@ -518,30 +601,32 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "servicepoint" -version = "0.8.0" +version = "0.9.0" dependencies = [ "bitvec", "bzip2", "clap", "flate2", "log", + "once_cell", "rand", "rust-lzma", + "tungstenite", "zstd", ] [[package]] name = "servicepoint_binding_c" -version = "0.8.0" +version = "0.9.0" dependencies = [ "cbindgen", "servicepoint", @@ -549,13 +634,24 @@ dependencies = [ [[package]] name = "servicepoint_binding_cs" -version = "0.8.0" +version = "0.9.0" dependencies = [ "csbindgen", "servicepoint", "servicepoint_binding_c", ] +[[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" @@ -570,9 +666,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -587,9 +683,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -598,6 +694,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.19" @@ -621,9 +737,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -633,10 +749,40 @@ dependencies = [ ] [[package]] -name = "unicode-ident" -version = "1.0.12" +name = "tungstenite" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" @@ -650,6 +796,12 @@ 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" @@ -740,9 +892,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9efa925..9c0b7a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "0.8.0" +version = "0.9.0" [workspace.lints.rust] missing-docs = "warn" diff --git a/crates/servicepoint/Cargo.toml b/crates/servicepoint/Cargo.toml index b68b538..20ae963 100644 --- a/crates/servicepoint/Cargo.toml +++ b/crates/servicepoint/Cargo.toml @@ -20,24 +20,36 @@ bzip2 = { version = "0.4", optional = true } zstd = { version = "0.13", optional = true } rust-lzma = { version = "0.6.0", optional = true } rand = { version = "0.8", optional = true } +tungstenite = { version = "0.24.0", optional = true } +once_cell = { version = "1.20.2", optional = true } [features] -default = ["compression_lzma"] +default = ["compression_lzma", "protocol_udp", "cp437"] compression_zlib = ["dep:flate2"] compression_bzip2 = ["dep:bzip2"] compression_lzma = ["dep:rust-lzma"] compression_zstd = ["dep:zstd"] all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"] rand = ["dep:rand"] +protocol_udp = [] +protocol_websocket = ["dep:tungstenite"] +cp437 = ["dep:once_cell"] [[example]] name = "random_brightness" required-features = ["rand"] +[[example]] +name = "game_of_life" +required-features = ["rand"] + +[[example]] +name = "websocket" +required-features = ["protocol_websocket"] + [dev-dependencies] # for examples clap = { version = "4.5", features = ["derive"] } -rand = "0.8" [lints] workspace = true \ No newline at end of file diff --git a/crates/servicepoint/README.md b/crates/servicepoint/README.md index daed5f3..71b4df7 100644 --- a/crates/servicepoint/README.md +++ b/crates/servicepoint/README.md @@ -9,6 +9,17 @@ In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wa Display" or "Airport Display". This crate contains a library for parsing, encoding and sending packets to this display via UDP. +## Installation + +```bash +cargo add servicepoint +``` +or +```toml +[dependencies] +servicepoint = "0.9.0" +``` + ## Examples ```rust @@ -23,7 +34,7 @@ fn main() { } ``` -More examples are available in the crate. +More examples are available in the crate. Execute `cargo run --example` for a list of available examples and `cargo run --example ` to run one. ## Note on stability @@ -32,22 +43,21 @@ This library is still in early development. You can absolutely use it, and it works, but expect minor breaking changes with every version bump. Please specify the full version including patch in your Cargo.toml until 1.0 is released. -## Installation - -```bash -cargo add servicepoint -``` - ## Features -This library has multiple compression libraries as optional dependencies. -If you do not need compression/decompression support you can disable those features. -In the likely case you only need one of them, you can include that one specifically. +This library has multiple optional dependencies. +You can choose to (not) include them by toggling the related features. -```toml -[dependencies] -servicepoint = { version = "0.8.0", default-features = false, features = ["compression-bz"] } -``` +| Name | Default | Description | +|--------------------|---------|--------------------------------------------| +| compression_zlib | false | Enable additional compression algo | +| compression_bzip2 | false | Enable additional compression algo | +| compression_lzma | true | Enable additional compression algo | +| compression_zstd | false | Enable additional compression algo | +| protocol_udp | true | Connection::Udp | +| protocol_websocket | false | Connection::WebSocket | +| rand | false | impl Distribution for Standard | +| cp437 | true | Conversion to and from CP-437 | ## Everything else diff --git a/crates/servicepoint/examples/announce.rs b/crates/servicepoint/examples/announce.rs index 75b9ba6..ff95479 100644 --- a/crates/servicepoint/examples/announce.rs +++ b/crates/servicepoint/examples/announce.rs @@ -2,7 +2,7 @@ use clap::Parser; -use servicepoint::{Command, Connection, Cp437Grid, Grid, Origin}; +use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin}; #[derive(Parser, Debug)] struct Cli { @@ -31,19 +31,15 @@ fn main() { .expect("sending clear failed"); } - let max_width = cli.text.iter().map(|t| t.len()).max().unwrap(); + let text = cli.text.iter().fold(String::new(), move |str, line| { + let is_first = str.is_empty(); + str + if is_first { "" } else { "\n" } + line + }); - let mut chars = Cp437Grid::new(max_width, cli.text.len()); - for y in 0..cli.text.len() { - let row = &cli.text[y]; - - for (x, char) in row.chars().enumerate() { - let char = char.try_into().expect("invalid input char"); - chars.set(x, y, char); - } - } + let grid = CharGrid::from(&*text); + let cp437_grid = Cp437Grid::from(&grid); connection - .send(Command::Cp437Data(Origin::new(0, 0), chars)) + .send(Command::Cp437Data(Origin::new(0, 0), cp437_grid)) .expect("sending text failed"); } diff --git a/crates/servicepoint/examples/websocket.rs b/crates/servicepoint/examples/websocket.rs new file mode 100644 index 0000000..71be6a3 --- /dev/null +++ b/crates/servicepoint/examples/websocket.rs @@ -0,0 +1,27 @@ +//! Example for how to use the WebSocket connection + +use servicepoint::{ + Command, CompressionCode, Connection, Grid, Origin, PixelGrid, +}; + +fn main() { + // make connection mut + let mut connection = + Connection::open_websocket("ws://localhost:8080".parse().unwrap()) + .unwrap(); + + // use send_mut instead of send + connection.send_mut(Command::Clear).unwrap(); + + let mut pixels = PixelGrid::max_sized(); + pixels.fill(true); + + // use send_mut instead of send + connection + .send_mut(Command::BitmapLinearWin( + Origin::ZERO, + pixels, + CompressionCode::Lzma, + )) + .unwrap(); +} diff --git a/crates/servicepoint/src/brightness.rs b/crates/servicepoint/src/brightness.rs index 5401a9f..ad07aa6 100644 --- a/crates/servicepoint/src/brightness.rs +++ b/crates/servicepoint/src/brightness.rs @@ -77,8 +77,8 @@ impl From for Vec { } } -impl From for PrimitiveGrid { - fn from(value: PrimitiveGrid) -> Self { +impl From<&BrightnessGrid> for PrimitiveGrid { + fn from(value: &PrimitiveGrid) -> Self { let u8s = value .iter() .map(|brightness| (*brightness).into()) @@ -109,3 +109,33 @@ impl Distribution for Standard { Brightness(rng.gen_range(Brightness::MIN.0..=Brightness::MAX.0)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::DataRef; + + #[test] + fn brightness_from_u8() { + assert_eq!(Err(100), Brightness::try_from(100)); + assert_eq!(Ok(Brightness(1)), Brightness::try_from(1)) + } + + #[test] + #[cfg(feature = "rand")] + fn rand_brightness() { + let mut rng = rand::thread_rng(); + for _ in 0..100 { + let _: Brightness = rng.gen(); + } + } + + #[test] + fn to_u8_grid() { + let mut grid = BrightnessGrid::new(2, 2); + grid.set(1, 0, Brightness::MIN); + grid.set(0, 1, Brightness::MAX); + let actual = PrimitiveGrid::from(&grid); + assert_eq!(actual.data_ref(), &[11, 0, 11, 11]); + } +} diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index d799696..abe983c 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -1,30 +1,50 @@ use bitvec::prelude::BitVec; use crate::{ - command_code::CommandCode, compression::into_decompressed, Brightness, - BrightnessGrid, CompressionCode, Header, Origin, Packet, PixelGrid, Pixels, - PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, + command_code::CommandCode, + compression::into_decompressed, + packet::{Header, Packet}, + Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin, PixelGrid, + Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, }; /// Type alias for documenting the meaning of the u16 in enum values pub type Offset = usize; -/// A grid containing codepage 437 characters. -/// -/// The encoding is currently not enforced. -pub type Cp437Grid = PrimitiveGrid; - /// A low-level display command. /// /// This struct and associated functions implement the UDP protocol for the display. /// -/// To send a `Command`, use a `Connection`. +/// To send a [Command], use a [connection][crate::Connection]. +/// +/// # Available commands +/// +/// To send text, take a look at [Command::Cp437Data]. +/// +/// To draw pixels, the easiest command to use is [Command::BitmapLinearWin]. +/// +/// The other BitmapLinear-Commands operate on a region of pixel memory directly. +/// [Command::BitmapLinear] overwrites a region. +/// [Command::BitmapLinearOr], [Command::BitmapLinearAnd] and [Command::BitmapLinearXor] apply logical operations per pixel. +/// +/// Out of bounds operations may be truncated or ignored by the display. +/// +/// # Compression +/// +/// Some commands can contain compressed payloads. +/// To get started, use [CompressionCode::Uncompressed]. +/// +/// If you want to archive the best performance (e.g. latency), +/// you can try the different compression algorithms for your hardware and use case. +/// +/// In memory, the payload is not compressed in the [Command]. +/// Payload (de-)compression happens when converting the [Command] into a [Packet] or vice versa. /// /// # Examples /// /// ```rust -/// # use servicepoint::{Brightness, Command, Connection, Packet}; -/// +/// # use servicepoint::{Brightness, Command, Connection, packet::Packet}; +/// # /// // create command /// let command = Command::Brightness(Brightness::MAX); /// @@ -56,6 +76,8 @@ pub enum Command { /// Show text on the screen. /// + /// The text is sent in the form of a 2D grid of characters. + /// ///
/// The library does not currently convert between UTF-8 and CP-437. /// Because Rust expects UTF-8 strings, it might be necessary to only send ASCII for now. @@ -64,15 +86,45 @@ pub enum Command { /// # Examples /// /// ```rust + /// # use servicepoint::{Command, Connection, Origin}; + /// # let connection = Connection::Fake; + /// use servicepoint::{CharGrid, Cp437Grid}; + /// let grid = CharGrid::from("Hello,\nWorld!"); + /// let grid = Cp437Grid::from(&grid); + /// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed"); + /// ``` + /// + /// ```rust /// # use servicepoint::{Command, Connection, Cp437Grid, Origin}; - /// # let connection = Connection::open("127.0.0.1:2342").unwrap(); - /// let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'].map(move |c| c as u8); - /// let grid = Cp437Grid::load(5, 2, &chars); + /// # let connection = Connection::Fake; + /// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap(); /// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap(); /// ``` Cp437Data(Origin, Cp437Grid), - /// Sets a window of pixels to the specified values + /// Overwrites a rectangular region of pixels. + /// + /// Origin coordinates must be divisible by 8. + /// + /// # Examples + /// + /// ```rust + /// # use servicepoint::{Command, CompressionCode, Grid, PixelGrid}; + /// # let connection = servicepoint::Connection::Fake; + /// # + /// let mut pixels = PixelGrid::max_sized(); + /// // draw something to the pixels here + /// # pixels.set(2, 5, true); + /// + /// // create command to send pixels + /// let command = Command::BitmapLinearWin( + /// servicepoint::Origin::new(0, 0), + /// pixels, + /// CompressionCode::Uncompressed + /// ); + /// + /// connection.send(command).expect("send failed"); + /// ``` BitmapLinearWin(Origin, PixelGrid, CompressionCode), /// Set the brightness of all tiles to the same value. @@ -95,7 +147,7 @@ pub enum Command { /// 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. + /// The contained [BitVec] is always uncompressed. BitmapLinear(Offset, SpBitVec, CompressionCode), /// Set pixel data according to an and-mask starting at the offset. @@ -103,7 +155,7 @@ pub enum Command { /// 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. + /// The contained [BitVec] is always uncompressed. BitmapLinearAnd(Offset, SpBitVec, CompressionCode), /// Set pixel data according to an or-mask starting at the offset. @@ -111,7 +163,7 @@ pub enum Command { /// 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. + /// The contained [BitVec] is always uncompressed. BitmapLinearOr(Offset, SpBitVec, CompressionCode), /// Set pixel data according to a xor-mask starting at the offset. @@ -119,7 +171,7 @@ pub enum Command { /// 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. + /// The contained [BitVec] is always uncompressed. BitmapLinearXor(Offset, SpBitVec, CompressionCode), /// Kills the udp daemon on the display, which usually results in a restart. @@ -166,7 +218,7 @@ pub enum Command { } #[derive(Debug)] -/// Err values for `Command::try_from`. +/// Err values for [Command::try_from]. #[derive(PartialEq)] pub enum TryFromPacketError { /// the contained command code does not correspond to a known command @@ -188,7 +240,7 @@ pub enum TryFromPacketError { impl TryFrom for Command { type Error = TryFromPacketError; - /// Try to interpret the `Packet` as one containing a `Command` + /// Try to interpret the [Packet] as one containing a [Command] fn try_from(packet: Packet) -> Result { let Packet { header: Header { @@ -443,9 +495,12 @@ impl Command { #[cfg(test)] mod tests { use crate::{ - bitvec::prelude::BitVec, command::TryFromPacketError, - command_code::CommandCode, origin::Pixels, Brightness, Command, - CompressionCode, Header, Origin, Packet, PixelGrid, PrimitiveGrid, + bitvec::prelude::BitVec, + command::TryFromPacketError, + command_code::CommandCode, + origin::Pixels, + packet::{Header, Packet}, + Brightness, Command, CompressionCode, Origin, PixelGrid, PrimitiveGrid, }; fn round_trip(original: Command) { diff --git a/crates/servicepoint/src/command_code.rs b/crates/servicepoint/src/command_code.rs index 25df1c9..4735e44 100644 --- a/crates/servicepoint/src/command_code.rs +++ b/crates/servicepoint/src/command_code.rs @@ -1,4 +1,4 @@ -/// The u16 command codes used for the `Commands`. +/// The u16 command codes used for the [Command]s. #[repr(u16)] #[derive(Debug, Copy, Clone)] pub(crate) enum CommandCode { diff --git a/crates/servicepoint/src/compression.rs b/crates/servicepoint/src/compression.rs index 2e78073..12c79d5 100644 --- a/crates/servicepoint/src/compression.rs +++ b/crates/servicepoint/src/compression.rs @@ -8,7 +8,7 @@ use flate2::{FlushCompress, FlushDecompress, Status}; #[cfg(feature = "compression_zstd")] use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; -use crate::{CompressionCode, Payload}; +use crate::{packet::Payload, CompressionCode}; pub(crate) fn into_decompressed( kind: CompressionCode, diff --git a/crates/servicepoint/src/connection.rs b/crates/servicepoint/src/connection.rs index 032ffeb..5d4e902 100644 --- a/crates/servicepoint/src/connection.rs +++ b/crates/servicepoint/src/connection.rs @@ -1,29 +1,61 @@ use std::fmt::Debug; -use std::net::{ToSocketAddrs, UdpSocket}; -use log::{debug, info}; - -use crate::Packet; +use crate::packet::Packet; /// A connection to the display. /// +/// Used to send [Packets][Packet] or [Commands][crate::Command]. +/// /// # Examples /// ```rust -/// let connection = servicepoint::Connection::open("172.23.42.29:2342") +/// 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 real connection using the UDP protocol - Udp(UdpSocket), - /// A fake connection for testing that does not actually send anything + /// A connection using the UDP protocol. + /// + /// Use this when sending commands directly to the display. + /// + /// Requires the feature "protocol_udp" which is enabled by default. + #[cfg(feature = "protocol_udp")] + Udp(std::net::UdpSocket), + + /// A connection using the WebSocket protocol. + /// + /// Note that you will need to forward the WebSocket messages via UDP to the display. + /// You can use [servicepoint-websocket-relay] for this. + /// + /// To create a new WebSocket automatically, use [Connection::open_websocket]. + /// + /// Requires the feature "protocol_websocket" which is disabled by default. + /// + /// [servicepoint-websocket-relay]: https://github.com/kaesaecracker/servicepoint-websocket-relay + #[cfg(feature = "protocol_websocket")] + WebSocket( + tungstenite::WebSocket< + tungstenite::stream::MaybeTlsStream, + >, + ), + + /// A fake connection for testing that does not actually send anything. + /// + /// This variant allows immutable send. Fake, + + /// A fake connection for testing that does not actually send anything. + /// + /// This variant does not allow immutable send. + FakeMutableSend, } #[derive(Debug)] pub enum SendError { IoError(std::io::Error), + #[cfg(feature = "protocol_websocket")] + WebsocketError(tungstenite::Error), } impl Connection { @@ -31,24 +63,110 @@ impl Connection { /// /// 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("172.23.42.29:2342") + /// let connection = servicepoint::Connection::open("127.0.0.1:2342") /// .expect("connection failed"); /// ``` - pub fn open(addr: impl ToSocketAddrs + Debug) -> std::io::Result { - info!("connecting to {addr:?}"); - let socket = UdpSocket::bind("0.0.0.0:0")?; + #[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")?; socket.connect(addr)?; Ok(Self::Udp(socket)) } + /// Open a new WebSocket and connect to the provided host. + /// + /// Requires the feature "protocol_websocket" which is disabled by default. + /// + /// # Examples + /// + /// ```no_run + /// use tungstenite::http::Uri; + /// use servicepoint::{Command, Connection}; + /// let uri = "ws://localhost:8080".parse().unwrap(); + /// let mut connection = Connection::open_websocket(uri) + /// .expect("could not connect"); + /// connection.send_mut(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(sock)) + } + /// Send something packet-like to the display. Usually this is in the form of a Command. /// + /// This variant can only be used for connections that support immutable send, e.g. [Connection::Udp]. + /// + /// If you want to be able to switch the protocol, you should use [Self::send_mut] instead. + /// + /// # Arguments + /// + /// - `packet`: the packet-like to send + /// + /// returns: true if packet was sent, otherwise false + /// + /// # Panics + /// + /// If the connection does not support immutable send, e.g. for [Connection::WebSocket]. + /// + /// # 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 + } + Connection::Fake => { + let _ = data; + Ok(()) + } + #[allow(unreachable_patterns)] // depends on features + _ => { + panic!("Connection {:?} does not support immutable send", self) + } + } + } + + /// Send something packet-like to the display. Usually this is in the form of a Command. + /// + /// This variant has to be used for connections that do not support immutable send, e.g. [Connection::WebSocket]. + /// + /// If you want to be able to switch the protocol, you should use this variant. + /// /// # Arguments /// /// - `packet`: the packet-like to send @@ -58,23 +176,70 @@ impl Connection { /// # Examples /// /// ```rust - /// # let connection = servicepoint::Connection::Fake; + /// let mut connection = servicepoint::Connection::FakeMutableSend; /// // turn off all pixels on display - /// connection.send(servicepoint::Command::Clear) + /// connection.send_mut(servicepoint::Command::Clear) /// .expect("send failed"); /// ``` - pub fn send(&self, packet: impl Into) -> Result<(), SendError> { - let packet = packet.into(); - debug!("sending {packet:?}"); - let data: Vec = packet.into(); + pub fn send_mut( + &mut self, + packet: impl Into, + ) -> Result<(), SendError> { match self { - Connection::Udp(socket) => { + #[cfg(feature = "protocol_websocket")] + Connection::WebSocket(socket) => { + let packet = packet.into(); + log::debug!("sending {packet:?}"); + let data: Vec = packet.into(); socket - .send(&data) - .map_err(SendError::IoError) - .map(move |_| ()) // ignore Ok value + .send(tungstenite::Message::Binary(data)) + .map_err(SendError::WebsocketError) } - Connection::Fake => Ok(()), + Connection::FakeMutableSend => { + let packet = packet.into(); + log::debug!("sending {packet:?}"); + let data: Vec = packet.into(); + let _ = data; + Ok(()) + } + _ => self.send(packet), } } } + +impl Drop for Connection { + fn drop(&mut self) { + #[cfg(feature = "protocol_websocket")] + if let Connection::WebSocket(sock) = self { + _ = sock.close(None); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::*; + + #[test] + fn send_fake() { + let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let packet = Packet::try_from(data).unwrap(); + Connection::Fake.send(packet).unwrap() + } + + #[test] + fn send_fake_mutable() { + 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::FakeMutableSend.send_mut(packet).unwrap() + } + + #[test] + #[should_panic] + fn send_fake_mutable_panic() { + 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::FakeMutableSend.send(packet).unwrap() + } +} diff --git a/crates/servicepoint/src/cp437.rs b/crates/servicepoint/src/cp437.rs new file mode 100644 index 0000000..dc40627 --- /dev/null +++ b/crates/servicepoint/src/cp437.rs @@ -0,0 +1,221 @@ +use crate::cp437::Cp437LoadError::InvalidChar; +use crate::{Grid, PrimitiveGrid}; +use std::collections::HashMap; + +/// A grid containing codepage 437 characters. +/// +/// The encoding is currently not enforced. +pub type Cp437Grid = PrimitiveGrid; + +/// A grid containing UTF-8 characters. +pub type CharGrid = PrimitiveGrid; + +#[derive(Debug)] +pub enum Cp437LoadError { + InvalidChar { index: usize, char: char }, +} + +impl Cp437Grid { + /// Load an ASCII-only [&str] into a [Cp437Grid] of specified width. + /// + /// # Panics + /// + /// - for width == 0 + /// - on empty strings + pub fn load_ascii( + value: &str, + width: usize, + wrap: bool, + ) -> Result { + assert!(width > 0); + assert!(!value.is_empty()); + + let mut chars = { + let mut x = 0; + let mut y = 0; + + for (index, char) in value.chars().enumerate() { + if !char.is_ascii() { + return Err(InvalidChar { index, char }); + } + + let is_lf = char == '\n'; + if is_lf || (wrap && x == width) { + y += 1; + x = 0; + if is_lf { + continue; + } + } + + x += 1; + } + + Cp437Grid::new(width, y + 1) + }; + + let mut x = 0; + let mut y = 0; + for char in value.chars().map(move |c| c as u8) { + let is_lf = char == b'\n'; + if is_lf || (wrap && x == width) { + y += 1; + x = 0; + if is_lf { + continue; + } + } + + if wrap || x < width { + chars.set(x, y, char); + } + x += 1; + } + + Ok(chars) + } +} + +#[allow(unused)] // depends on features +pub use feature_cp437::*; + +#[cfg(feature = "cp437")] +mod feature_cp437 { + use super::*; + + /// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters + /// + /// Mostly follows CP437, except for: + /// * 0x0A & 0x0D are kept for use as line endings. + /// * 0x1A is used for SAUCE. + /// * 0x1B is used for ANSI escape sequences. + /// + /// These exclusions should be fine since most programs can't even use them + /// without issues. And this makes rendering simpler too. + /// + /// See + /// + /// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0 + #[rustfmt::skip] + const CP437_TO_UTF8: [char; 256] = [ + /* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '\r', '♫', '☼', + /* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔', '▲', '▼', + /* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/', + /* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + /* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + /* 5X */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',']', '^', '_', + /* 6X */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + /* 7X */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂', + /* 8X */ 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å', + /* 9X */ 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', + /* AX */ 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', + /* BX */ '░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', + /* CX */ '└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', + /* DX */ '╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', + /* EX */ 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', + /* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ', + ]; + + const UTF8_TO_CP437: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let pairs = CP437_TO_UTF8 + .iter() + .enumerate() + .map(move |(index, char)| (*char, index as u8)); + HashMap::from_iter(pairs) + }); + + const MISSING_CHAR_CP437: u8 = 0x3F; + + impl From<&Cp437Grid> for CharGrid { + fn from(value: &Cp437Grid) -> Self { + let mut grid = Self::new(value.width(), value.height()); + + for y in 0..grid.height() { + for x in 0..grid.width() { + let converted = CP437_TO_UTF8[value.get(x, y) as usize]; + grid.set(x, y, converted); + } + } + + grid + } + } + + impl From<&CharGrid> for Cp437Grid { + fn from(value: &CharGrid) -> Self { + let mut grid = Self::new(value.width(), value.height()); + + for y in 0..grid.height() { + for x in 0..grid.width() { + let char = value.get(x, y); + let converted = *UTF8_TO_CP437 + .get(&char) + .unwrap_or(&MISSING_CHAR_CP437); + grid.set(x, y, converted); + } + } + + grid + } + } + + impl From<&str> for CharGrid { + fn from(value: &str) -> Self { + let value = value.replace("\r\n", "\n"); + let lines = value.split('\n').collect::>(); + let width = + lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len())); + + let mut grid = Self::new(width, lines.len()); + for (y, line) in lines.iter().enumerate() { + for (x, char) in line.chars().enumerate() { + grid.set(x, y, char); + } + } + + grid + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_ascii_nowrap() { + let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] + .map(move |c| c as u8); + let expected = Cp437Grid::load(5, 2, &chars); + + let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap(); + // comma will be removed because line is too long and wrap is off + assert_eq!(actual, expected); + } + + #[test] + fn load_ascii_wrap() { + let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] + .map(move |c| c as u8); + let expected = Cp437Grid::load(5, 2, &chars); + + let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap(); + // line break will be added + assert_eq!(actual, expected); + } +} + +#[cfg(test)] +#[cfg(feature = "cp437")] +mod tests_feature_cp437 { + use crate::{CharGrid, Cp437Grid}; + + #[test] + fn round_trip_cp437() { + let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']); + let cp437 = Cp437Grid::from(&utf8); + let actual = CharGrid::from(&cp437); + assert_eq!(actual, utf8); + } +} diff --git a/crates/servicepoint/src/grid.rs b/crates/servicepoint/src/grid.rs index cb8f5e7..d367d98 100644 --- a/crates/servicepoint/src/grid.rs +++ b/crates/servicepoint/src/grid.rs @@ -79,12 +79,12 @@ pub trait Grid { assert!( x < self.width(), "cannot access index [{x}, {y}] because x is outside of bounds 0..{}", - self.width() + self.width() - 1 ); assert!( y < self.height(), - "cannot access byte [{x}, {y}] because y is outside of bounds 0..{}", - self.height() + "cannot access index [{x}, {y}] because y is outside of bounds 0..{}", + self.height() - 1 ); } } diff --git a/crates/servicepoint/src/lib.rs b/crates/servicepoint/src/lib.rs index d71e1fa..fa5564e 100644 --- a/crates/servicepoint/src/lib.rs +++ b/crates/servicepoint/src/lib.rs @@ -1,5 +1,9 @@ //! Abstractions for the UDP protocol of the CCCB servicepoint display. //! +//! Your starting point is a [Connection] to the display. +//! With a connection, you can send [Command]s. +//! When received, the display will update the state of the pixels. +//! //! # Examples //! //! ```rust @@ -37,13 +41,13 @@ pub use bitvec; use bitvec::prelude::{BitVec, Msb0}; pub use crate::brightness::{Brightness, BrightnessGrid}; -pub use crate::command::{Command, Cp437Grid, Offset}; +pub use crate::command::{Command, Offset}; pub use crate::compression_code::CompressionCode; pub use crate::connection::Connection; +pub use crate::cp437::{CharGrid, Cp437Grid}; pub use crate::data_ref::DataRef; pub use crate::grid::Grid; pub use crate::origin::{Origin, Pixels, Tiles}; -pub use crate::packet::{Header, Packet, Payload}; pub use crate::pixel_grid::PixelGrid; pub use crate::primitive_grid::PrimitiveGrid; @@ -55,10 +59,11 @@ mod command_code; mod compression; mod compression_code; mod connection; +mod cp437; mod data_ref; mod grid; mod origin; -mod packet; +pub mod packet; mod pixel_grid; mod primitive_grid; diff --git a/crates/servicepoint/src/origin.rs b/crates/servicepoint/src/origin.rs index 88758a5..6c0f5d2 100644 --- a/crates/servicepoint/src/origin.rs +++ b/crates/servicepoint/src/origin.rs @@ -1,3 +1,4 @@ +use crate::TILE_SIZE; use std::marker::PhantomData; /// An origin marks the top left position of a window sent to the display. @@ -11,7 +12,14 @@ pub struct Origin { } impl Origin { - /// Create a new `Origin` instance for the provided position. + /// Top-left. Equivalent to `Origin::new(0, 0)`. + pub const ZERO: Self = Self { + x: 0, + y: 0, + phantom_data: PhantomData, + }; + + /// Create a new [Origin] instance for the provided position. pub fn new(x: usize, y: usize) -> Self { Self { x, @@ -46,3 +54,69 @@ pub struct Tiles(); impl DisplayUnit for Pixels {} impl DisplayUnit for Tiles {} + +impl From<&Origin> for Origin { + fn from(value: &Origin) -> Self { + Self { + x: value.x * TILE_SIZE, + y: value.y * TILE_SIZE, + phantom_data: PhantomData, + } + } +} + +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, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn origin_tile_to_pixel() { + let tile: Origin = Origin::new(1, 2); + let actual: Origin = Origin::from(&tile); + let expected: Origin = Origin::new(8, 16); + assert_eq!(actual, expected); + } + + #[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(); + } +} diff --git a/crates/servicepoint/src/packet.rs b/crates/servicepoint/src/packet.rs index 67c96b4..e53a7a1 100644 --- a/crates/servicepoint/src/packet.rs +++ b/crates/servicepoint/src/packet.rs @@ -1,10 +1,34 @@ +//! Raw packet manipulation. +//! +//! Should probably only be used directly to use features not exposed by the library. +//! +//! # Examples +//! +//! Converting a packet to a command and back: +//! +//! ```rust +//! use servicepoint::{Command, packet::Packet}; +//! # let command = Command::Clear; +//! let packet: Packet = command.into(); +//! let command: Command = Command::try_from(packet).expect("could not read command from packet"); +//! ``` +//! +//! Converting a packet to bytes and back: +//! +//! ```rust +//! use servicepoint::{Command, packet::Packet}; +//! # let command = Command::Clear; +//! # let packet: Packet = command.into(); +//! let bytes: Vec = packet.into(); +//! let packet = Packet::try_from(bytes).expect("could not read packet from bytes"); +//! ``` + use std::mem::size_of; -use crate::command_code::CommandCode; use crate::compression::into_compressed; use crate::{ - Command, CompressionCode, Grid, Offset, Origin, PixelGrid, Pixels, Tiles, - TILE_SIZE, + command_code::CommandCode, Command, CompressionCode, Grid, Offset, Origin, + PixelGrid, Pixels, Tiles, TILE_SIZE, }; /// A raw header. @@ -13,8 +37,6 @@ use crate::{ /// payload, where applicable. /// /// Because the meaning of most fields depend on the command, there are no speaking names for them. -/// -/// Should probably only be used directly to use features not exposed by the library. #[derive(Copy, Clone, Debug, PartialEq)] pub struct Header { /// The first two bytes specify which command this packet represents. @@ -38,26 +60,8 @@ pub type Payload = Vec; /// /// Contents should probably only be used directly to use features not exposed by the library. /// -/// # Examples +/// You may want to use [Command] instead. /// -/// Converting a packet to a command and back: -/// -/// ```rust -/// # use servicepoint::{Command, Packet}; -/// # let command = Command::Clear; -/// let packet: Packet = command.into(); -/// let command: Command = Command::try_from(packet).expect("could not read packet"); -/// ``` -/// -/// Converting a packet to bytes and back: -/// -/// ```rust -/// # use servicepoint::{Command, Packet}; -/// # let command = Command::Clear; -/// # let packet: Packet = command.into(); -/// let bytes: Vec = packet.into(); -/// let packet = Packet::try_from(bytes).expect("could not read packet from bytes"); -/// ``` /// #[derive(Clone, Debug, PartialEq)] pub struct Packet { @@ -98,9 +102,9 @@ impl From for Vec { impl TryFrom<&[u8]> for Packet { type Error = (); - /// Tries to interpret the bytes as a `Packet`. + /// Tries to interpret the bytes as a [Packet]. /// - /// returns: `Error` if slice is not long enough to be a `Packet` + /// returns: `Error` if slice is not long enough to be a [Packet] fn try_from(value: &[u8]) -> Result { if value.len() < size_of::
() { return Err(()); @@ -135,7 +139,7 @@ impl TryFrom> for Packet { } impl From for Packet { - /// Move the `Command` into a `Packet` instance for sending. + /// Move the [Command] into a [Packet] instance for sending. #[allow(clippy::cast_possible_truncation)] fn from(value: Command) -> Self { match value { @@ -210,7 +214,7 @@ impl From for Packet { } impl Packet { - /// Helper method for `BitMapLinear*`-Commands into `Packet` + /// Helper method for `BitMapLinear*`-Commands into [Packet] #[allow(clippy::cast_possible_truncation)] fn bitmap_linear_into_packet( command: CommandCode, @@ -312,7 +316,7 @@ impl Packet { #[cfg(test)] mod tests { - use crate::{Header, Packet}; + use super::*; #[test] fn round_trip() { @@ -327,7 +331,7 @@ mod tests { payload: vec![42u8; 23], }; let data: Vec = p.into(); - let p = Packet::try_from(&*data).unwrap(); + let p = Packet::try_from(data).unwrap(); assert_eq!( p, Packet { diff --git a/crates/servicepoint/src/pixel_grid.rs b/crates/servicepoint/src/pixel_grid.rs index 4b8bcc7..746f2ac 100644 --- a/crates/servicepoint/src/pixel_grid.rs +++ b/crates/servicepoint/src/pixel_grid.rs @@ -13,14 +13,14 @@ pub struct PixelGrid { } impl PixelGrid { - /// Creates a new `PixelGrid` with the specified dimensions. + /// Creates a new [PixelGrid] with the specified dimensions. /// /// # Arguments /// /// - `width`: size in pixels in x-direction /// - `height`: size in pixels in y-direction /// - /// returns: `PixelGrid` initialized to all pixels off + /// returns: [PixelGrid] initialized to all pixels off /// /// # Panics /// @@ -40,14 +40,14 @@ impl PixelGrid { Self::new(PIXEL_WIDTH, PIXEL_HEIGHT) } - /// Loads a `PixelGrid` with the specified dimensions from the provided data. + /// Loads a [PixelGrid] with the specified dimensions from the provided data. /// /// # Arguments /// /// - `width`: size in pixels in x-direction /// - `height`: size in pixels in y-direction /// - /// returns: `PixelGrid` that contains a copy of the provided data + /// returns: [PixelGrid] that contains a copy of the provided data /// /// # Panics /// @@ -64,7 +64,7 @@ impl PixelGrid { } } - /// Iterate over all cells in `PixelGrid`. + /// Iterate over all cells in [PixelGrid]. /// /// Order is equivalent to the following loop: /// ``` @@ -80,7 +80,7 @@ impl PixelGrid { self.bit_vec.iter().by_refs() } - /// Iterate over all cells in `PixelGrid` mutably. + /// Iterate over all cells in [PixelGrid] mutably. /// /// Order is equivalent to the following loop: /// ``` @@ -107,7 +107,7 @@ impl PixelGrid { self.bit_vec.iter_mut() } - /// Iterate over all rows in `PixelGrid` top to bottom. + /// Iterate over all rows in [PixelGrid] top to bottom. pub fn iter_rows(&self) -> IterRows { IterRows { pixel_grid: self, @@ -117,7 +117,7 @@ impl PixelGrid { } impl Grid for PixelGrid { - /// Sets the value of the specified position in the `PixelGrid`. + /// Sets the value of the specified position in the [PixelGrid]. /// /// # Arguments /// @@ -139,7 +139,7 @@ impl Grid for PixelGrid { self.bit_vec[x + y * self.width] } - /// Sets the state of all pixels in the `PixelGrid`. + /// Sets the state of all pixels in the [PixelGrid]. /// /// # Arguments /// @@ -169,7 +169,7 @@ impl DataRef for PixelGrid { } impl From for Vec { - /// Turns a `PixelGrid` into the underlying `Vec`. + /// Turns a [PixelGrid] into the underlying [`Vec`]. fn from(value: PixelGrid) -> Self { value.bit_vec.into() } diff --git a/crates/servicepoint/src/primitive_grid.rs b/crates/servicepoint/src/primitive_grid.rs index d1a507e..2b02ca3 100644 --- a/crates/servicepoint/src/primitive_grid.rs +++ b/crates/servicepoint/src/primitive_grid.rs @@ -14,14 +14,14 @@ pub struct PrimitiveGrid { } impl PrimitiveGrid { - /// Creates a new `PrimitiveGrid` with the specified dimensions. + /// Creates a new [PrimitiveGrid] with the specified dimensions. /// /// # Arguments /// /// - width: size in x-direction /// - height: size in y-direction /// - /// returns: `PrimitiveGrid` initialized to default value. + /// returns: [PrimitiveGrid] initialized to default value. pub fn new(width: usize, height: usize) -> Self { Self { data: vec![Default::default(); width * height], @@ -30,9 +30,9 @@ impl PrimitiveGrid { } } - /// Loads a `PrimitiveGrid` with the specified dimensions from the provided data. + /// Loads a [PrimitiveGrid] with the specified dimensions from the provided data. /// - /// returns: `PrimitiveGrid` that contains a copy of the provided data + /// returns: [PrimitiveGrid] that contains a copy of the provided data /// /// # Panics /// @@ -47,7 +47,7 @@ impl PrimitiveGrid { } } - /// Iterate over all cells in `PrimitiveGrid`. + /// Iterate over all cells in [PrimitiveGrid]. /// /// Order is equivalent to the following loop: /// ``` @@ -63,7 +63,7 @@ impl PrimitiveGrid { self.data.iter() } - /// Iterate over all rows in `PrimitiveGrid` top to bottom. + /// Iterate over all rows in [PrimitiveGrid] top to bottom. pub fn iter_rows(&self) -> IterRows { IterRows { byte_grid: self, @@ -168,7 +168,7 @@ impl DataRef for PrimitiveGrid { } impl From> for Vec { - /// Turn into the underlying `Vec` containing the rows of bytes. + /// Turn into the underlying [`Vec`] containing the rows of bytes. fn from(value: PrimitiveGrid) -> Self { value.data } diff --git a/crates/servicepoint_binding_c/Cargo.toml b/crates/servicepoint_binding_c/Cargo.toml index 8b3ed7c..96633a6 100644 --- a/crates/servicepoint_binding_c/Cargo.toml +++ b/crates/servicepoint_binding_c/Cargo.toml @@ -17,9 +17,12 @@ crate-type = ["staticlib", "cdylib", "rlib"] cbindgen = "0.27.0" [dependencies.servicepoint] -version = "0.8.0" +version = "0.9.0" path = "../servicepoint" features = ["all_compressions"] [lints] workspace = true + +[package.metadata.docs.rs] +all-features = true diff --git a/crates/servicepoint_binding_c/examples/lang_c/build.rs b/crates/servicepoint_binding_c/examples/lang_c/build.rs index db090d4..4f92e1d 100644 --- a/crates/servicepoint_binding_c/examples/lang_c/build.rs +++ b/crates/servicepoint_binding_c/examples/lang_c/build.rs @@ -12,5 +12,6 @@ fn main() { let mut cc = cc::Build::new(); cc.file("src/main.c"); cc.include(&sp_include); + cc.opt_level(2); cc.compile("lang_c"); } diff --git a/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h b/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h index ce780ff..f431793 100644 --- a/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h +++ b/crates/servicepoint_binding_c/examples/lang_c/include/servicepoint.h @@ -1,4 +1,4 @@ -/* Generated with cbindgen:0.26.0 */ +/* Generated with cbindgen:0.27.0 */ /* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ @@ -1355,5 +1355,5 @@ struct SPByteSlice sp_pixel_grid_unsafe_data_ref(struct SPPixelGrid *pixel_grid) size_t sp_pixel_grid_width(const struct SPPixelGrid *pixel_grid); #ifdef __cplusplus -} // extern "C" -#endif // __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/crates/servicepoint_binding_c/src/cp437_grid.rs b/crates/servicepoint_binding_c/src/cp437_grid.rs index a8d4684..781c345 100644 --- a/crates/servicepoint_binding_c/src/cp437_grid.rs +++ b/crates/servicepoint_binding_c/src/cp437_grid.rs @@ -40,7 +40,9 @@ pub unsafe extern "C" fn sp_cp437_grid_new( width: usize, height: usize, ) -> *mut SPCp437Grid { - Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::new(width, height)))) + Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::new( + width, height, + )))) } /// Loads a `SPCp437Grid` with the specified dimensions from the provided data. @@ -67,7 +69,9 @@ pub unsafe extern "C" fn sp_cp437_grid_load( data_length: usize, ) -> *mut SPCp437Grid { let data = std::slice::from_raw_parts(data, data_length); - Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::load(width, height, data)))) + Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::load( + width, height, data, + )))) } /// Clones a `SPCp437Grid`. diff --git a/crates/servicepoint_binding_c/src/packet.rs b/crates/servicepoint_binding_c/src/packet.rs index eac41b8..5dc5820 100644 --- a/crates/servicepoint_binding_c/src/packet.rs +++ b/crates/servicepoint_binding_c/src/packet.rs @@ -7,7 +7,7 @@ use std::ptr::null_mut; use crate::SPCommand; /// The raw packet -pub struct SPPacket(pub(crate) servicepoint::Packet); +pub struct SPPacket(pub(crate) servicepoint::packet::Packet); /// Turns a `SPCommand` into a `SPPacket`. /// The `SPCommand` gets consumed. @@ -49,7 +49,7 @@ pub unsafe extern "C" fn sp_packet_try_load( length: usize, ) -> *mut SPPacket { let data = std::slice::from_raw_parts(data, length); - match servicepoint::Packet::try_from(data) { + match servicepoint::packet::Packet::try_from(data) { Err(_) => null_mut(), Ok(packet) => Box::into_raw(Box::new(SPPacket(packet))), } diff --git a/crates/servicepoint_binding_cs/Cargo.toml b/crates/servicepoint_binding_cs/Cargo.toml index 798292d..261364e 100644 --- a/crates/servicepoint_binding_cs/Cargo.toml +++ b/crates/servicepoint_binding_cs/Cargo.toml @@ -13,8 +13,8 @@ test = false csbindgen = "1.9.3" [dependencies] -servicepoint_binding_c = { version = "0.8.0", path = "../servicepoint_binding_c" } -servicepoint = { version = "0.8.0", path = "../servicepoint" } +servicepoint_binding_c = { version = "0.9.0", path = "../servicepoint_binding_c" } +servicepoint = { version = "0.9.0", path = "../servicepoint" } [lints] workspace = true diff --git a/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj b/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj index b1831d7..7168cd0 100644 --- a/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj +++ b/crates/servicepoint_binding_cs/ServicePoint/ServicePoint.csproj @@ -11,7 +11,7 @@ ServicePoint - 0.8.0 + 0.9.0 Repository Authors None ServicePoint