mirror of
https://github.com/fossasia/badgemagic-rs
synced 2025-06-23 15:23:58 +00:00
Initial commit
This commit is contained in:
commit
97d5561e81
17 changed files with 1967 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
/*.toml
|
||||
!/Cargo.toml
|
||||
!/rustfmt.toml
|
610
Cargo.lock
generated
Normal file
610
Cargo.lock
generated
Normal file
|
@ -0,0 +1,610 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "az"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973"
|
||||
|
||||
[[package]]
|
||||
name = "badgemagic"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"clap",
|
||||
"embedded-graphics",
|
||||
"hidapi",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"toml",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-graphics"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0"
|
||||
dependencies = [
|
||||
"az",
|
||||
"byteorder",
|
||||
"embedded-graphics-core",
|
||||
"float-cmp",
|
||||
"micromath",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-graphics-core"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044"
|
||||
dependencies = [
|
||||
"az",
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hidapi"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e58251020fe88fe0dae5ebcc1be92b4995214af84725b375d08354d0311c23c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "micromath"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815"
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.5",
|
||||
"windows_aarch64_msvc 0.52.5",
|
||||
"windows_i686_gnu 0.52.5",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.5",
|
||||
"windows_x86_64_gnu 0.52.5",
|
||||
"windows_x86_64_gnullvm 0.52.5",
|
||||
"windows_x86_64_msvc 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ff33f391015ecab21cd092389215eb265ef9496a9a07b6bee7d3529831deda"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
48
Cargo.toml
Normal file
48
Cargo.toml
Normal file
|
@ -0,0 +1,48 @@
|
|||
[package]
|
||||
name = "badgemagic"
|
||||
version = "0.1.0"
|
||||
authors = ["Martin Michaelis <code@mgjm.de>"]
|
||||
edition = "2021"
|
||||
description = "Badge Magic with LEDs - Library and CLI"
|
||||
homepage = "https://badgemagic.fossasia.org"
|
||||
repository = "https://github.com/fossasia/badgemagic-rs"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "badgemagic"
|
||||
required-features = ["cli"]
|
||||
|
||||
[[example]]
|
||||
name = "hello-world"
|
||||
required-features = ["embedded-graphics", "usb-hid"]
|
||||
|
||||
[features]
|
||||
default = ["embedded-graphics", "usb-hid"]
|
||||
|
||||
cli = [
|
||||
"embedded-graphics",
|
||||
"serde",
|
||||
"usb-hid",
|
||||
"dep:base64",
|
||||
"dep:clap",
|
||||
"dep:serde_json",
|
||||
"dep:toml",
|
||||
]
|
||||
|
||||
embedded-graphics = ["dep:embedded-graphics"]
|
||||
serde = ["dep:serde"]
|
||||
usb-hid = ["dep:hidapi"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
clap = { version = "4.5.4", features = ["derive"], optional = true }
|
||||
embedded-graphics = { version = "0.8.1", optional = true }
|
||||
hidapi = { version = "2.6.1", optional = true }
|
||||
serde = { version = "1.0.203", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1.0.117", optional = true }
|
||||
time = "0.3.36"
|
||||
toml = { version = "0.8.13", optional = true }
|
||||
zerocopy = { version = "0.7.34", features = ["derive"] }
|
||||
|
176
LICENSE-APACHE
Normal file
176
LICENSE-APACHE
Normal file
|
@ -0,0 +1,176 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
19
LICENSE-MIT
Normal file
19
LICENSE-MIT
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright 2024 Martin Michaelis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
128
README.md
Normal file
128
README.md
Normal file
|
@ -0,0 +1,128 @@
|
|||
[](../../actions/workflows/ci.yaml)
|
||||
|
||||
# Badge Magic in Rust
|
||||
|
||||
Library and CLI to configure LED badges.
|
||||
|
||||
## Installation
|
||||
|
||||
As of now there are no proper releases (with version numbers) of this tool.
|
||||
|
||||
The latest commit on the main branch just gets build and released automatically.
|
||||
|
||||
Download the prebuild program for one of the following operating systems:
|
||||
|
||||
- [Linux (GNU / 64 bit)](../../releases/latest/download/badgemagic.x86_64-unknown-linux-gnu)
|
||||
- [Windows (64 bit)](../../releases/latest/download/badgemagic.x86_64-pc-windows-msvc.exe)
|
||||
- [MacOS (Intel)](../../releases/latest/download/badgemagic.x86_64-apple-darwin)
|
||||
- [MacOS (M1, etc.)](../../releases/latest/download/badgemagic.aarch64-apple-darwin)
|
||||
|
||||
```sh
|
||||
# After the download rename the file to `badgemagic`
|
||||
mv badgemagic.<target> badgemagic
|
||||
|
||||
# Make the program executable (linux / macOS only)
|
||||
chmod +x badgemagic
|
||||
|
||||
# Test that it works
|
||||
./badgemagic --help
|
||||
```
|
||||
|
||||
> Note: The windows and macOS build is not actively tested. Please try it out and report back whether it worked or any problems that might occour.
|
||||
|
||||
If your system is not listed above (Linux / Windows on ARM, musl, etc.) or you want to do it anyway, you can install this tool from source:
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/fossasia/badgemagic-rs --features cli
|
||||
badgemagic --help
|
||||
```
|
||||
|
||||
Or clone the repo and run the CLI:
|
||||
```sh
|
||||
git clone https://github.com/fossasia/badgemagic-rs
|
||||
cd badgemagic-rs
|
||||
cargo run --features cli -- --help
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Execute the `badgemagic` tool and pass the file name of your configuration file. Depending on how you installed the tool:
|
||||
|
||||
```sh
|
||||
# Downloaded from release page
|
||||
./badgemagic config.toml
|
||||
|
||||
# Installed with cargo install
|
||||
badgemagic config.toml
|
||||
|
||||
# Run from git repository
|
||||
cargo run --features cli -- config.toml
|
||||
```
|
||||
|
||||
The above command will read your configuration from a file named `config.toml` in the current directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can have a look at the example configurations in the [`demo` directory](demo).
|
||||
|
||||
The TOML configuration consists of up to 8 message sections starting with `[[message]]`.
|
||||
|
||||
Each message can have the following options:
|
||||
```toml
|
||||
[[message]]
|
||||
# Enable blink mode
|
||||
blink = true
|
||||
|
||||
# Show a dotted border arround the display
|
||||
border = true
|
||||
|
||||
# Set the update speed of the animations (0 to 7)
|
||||
speed = 6
|
||||
|
||||
# Set the display animation (left, right, up, down, center, fast, drop, curtain, laser)
|
||||
mode = "left"
|
||||
|
||||
# The text to show on the display
|
||||
text = "Lorem ipsum dolor sit amet."
|
||||
```
|
||||
|
||||
You can omit options you don't need:
|
||||
```toml
|
||||
[[message]]
|
||||
mode = "center"
|
||||
text = "Hello"
|
||||
```
|
||||
|
||||
If you want you can "draw" images as ASCII art (`_` = Off, `X` = On):
|
||||
```toml
|
||||
[[message]]
|
||||
mode = "center"
|
||||
bitstring = """
|
||||
___XXXXX___
|
||||
__X_____X__
|
||||
_X_______X_
|
||||
X__XX_XX__X
|
||||
X__XX_XX__X
|
||||
X_________X
|
||||
X_XX___XX_X
|
||||
X__XXXXX__X
|
||||
_X__XXX__X_
|
||||
__X_____X__
|
||||
___XXXXX___
|
||||
"""
|
||||
```
|
||||
|
||||
You just replace the `text` option with `bitstring`. All other options (e.g. `border`, `blink`) still work and can be combined with a custom image.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
|
41
build.rs
Normal file
41
build.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
fn main() {
|
||||
#[cfg(feature = "cli")]
|
||||
cli::generate_version_info();
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
mod cli {
|
||||
use std::{env, fs, path::PathBuf, process::Command, str};
|
||||
|
||||
pub fn generate_version_info() {
|
||||
let pkg_version = env::var("CARGO_PKG_VERSION").expect("missing package version");
|
||||
let git_version = git_version();
|
||||
let git_prefix = git_version
|
||||
.is_some()
|
||||
.then_some("commit-")
|
||||
.unwrap_or_default();
|
||||
let git_version = git_version.as_deref().unwrap_or("unknown");
|
||||
let version = format!("{pkg_version}-git.{git_prefix}{git_version}");
|
||||
|
||||
let out: PathBuf = env::var_os("OUT_DIR").expect("build output path").into();
|
||||
fs::write(
|
||||
out.join("cli.rs"),
|
||||
format!("pub const VERSION: &str = {version:?};\n"),
|
||||
)
|
||||
.expect("write cli.rs");
|
||||
}
|
||||
|
||||
fn git_version() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.arg("describe")
|
||||
.arg("--always")
|
||||
.arg("--dirty=-modified")
|
||||
.output()
|
||||
.ok()?;
|
||||
if output.status.success() {
|
||||
Some(str::from_utf8(&output.stdout).ok()?.trim().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
32
demo/effects.toml
Normal file
32
demo/effects.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[[message]]
|
||||
blink = true
|
||||
border = true
|
||||
mode = "center"
|
||||
text = "Hello"
|
||||
|
||||
[[message]]
|
||||
mode = "laser"
|
||||
text = "Laser"
|
||||
|
||||
[[message]]
|
||||
mode = "drop"
|
||||
text = "Drop"
|
||||
|
||||
[[message]]
|
||||
mode = "curtain"
|
||||
text = "Curtain"
|
||||
|
||||
[[message]]
|
||||
mode = "left"
|
||||
text = "Move message left"
|
||||
|
||||
[[message]]
|
||||
mode = "up"
|
||||
text = "Move up"
|
||||
|
||||
[[message]]
|
||||
blink = true
|
||||
border = true
|
||||
speed = 6
|
||||
mode = "left"
|
||||
text = "Combine them all and add some speed... might be a bit overwhelming"
|
16
demo/jump.toml
Normal file
16
demo/jump.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[[message]]
|
||||
speed = 6
|
||||
mode = "fast"
|
||||
bitstring = """
|
||||
X__________________________________________X____X__________________________________________X____X___________________XXXX___________________X____X__________________________________________X
|
||||
_X________________________________________X______X__________________XXXX__________________X______X_________________X____X_________________X______X__________________XXXX__________________X_
|
||||
__X_________________XXXX_________________X________X________________X____X________________X________X_____________X__X____X__X_____________X________X________________X____X________________X__
|
||||
___________________X____X__________________________________________X____X________________________________________X__XXXX__X________________________________________X____X___________________
|
||||
___________________X____X___________________________________________XXXX__________________________________________XXXXXXXX__________________________________________XXXX____________________
|
||||
____________________XXXX_______________________________________XXXXXXXXXXXXXX________________________________________X_________________________________________XXXXXXXXXXXXXX_______________
|
||||
__________________XXXXXXXX___________________________________________X______________________________________________X_X______________________________________________X______________________
|
||||
_________________X___X____X_________________________________________X_X____________________________________________X___X____________________________________________X_X_____________________
|
||||
__X_____________X___X_X____X_____________X________X________________X___X_________________X________X_______________X_____X________________X________X________________X___X_________________X__
|
||||
_X_________________X___X__________________X______X________________X_____X_________________X______X________________________________________X______X________________X_____X_________________X_
|
||||
X_________________X_____X__________________X____X__________________________________________X____X__________________________________________X____X__________________________________________X
|
||||
"""
|
15
demo/smiley.toml
Normal file
15
demo/smiley.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[[message]]
|
||||
mode = "center"
|
||||
bitstring = """
|
||||
___XXXXX___
|
||||
__X_____X__
|
||||
_X_______X_
|
||||
X__XX_XX__X
|
||||
X__XX_XX__X
|
||||
X_________X
|
||||
X_XX___XX_X
|
||||
X__XXXXX__X
|
||||
_X__XXX__X_
|
||||
__X_____X__
|
||||
___XXXXX___
|
||||
"""
|
51
examples/hello-world.rs
Normal file
51
examples/hello-world.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
#![warn(clippy::all, clippy::pedantic)]
|
||||
|
||||
use anyhow::Result;
|
||||
use badgemagic::{
|
||||
embedded_graphics::{
|
||||
geometry::Point, mono_font::MonoTextStyle, pixelcolor::BinaryColor, text::Text,
|
||||
},
|
||||
protocol::{Mode, PayloadBuffer, Style},
|
||||
usb_hid::Device,
|
||||
util::DrawableLayoutExt,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut payload = PayloadBuffer::new();
|
||||
|
||||
payload.add_message_drawable(
|
||||
Style::default().mode(Mode::Center),
|
||||
&Text::new(
|
||||
"Hello",
|
||||
Point::new(0, 8),
|
||||
MonoTextStyle::new(
|
||||
&embedded_graphics::mono_font::iso_8859_1::FONT_6X9,
|
||||
BinaryColor::On,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
payload.add_message_drawable(
|
||||
Style::default().mode(Mode::Center),
|
||||
&Text::new(
|
||||
"Hello",
|
||||
Point::new(0, 5),
|
||||
MonoTextStyle::new(
|
||||
&embedded_graphics::mono_font::iso_8859_1::FONT_4X6,
|
||||
BinaryColor::On,
|
||||
),
|
||||
)
|
||||
.z_stack(Text::new(
|
||||
"World",
|
||||
Point::new(23, 8),
|
||||
MonoTextStyle::new(
|
||||
&embedded_graphics::mono_font::iso_8859_1::FONT_4X6,
|
||||
BinaryColor::On,
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
Device::single()?.write(payload)?;
|
||||
|
||||
Ok(())
|
||||
}
|
3
rustfmt.toml
Normal file
3
rustfmt.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
format_code_in_doc_comments = true
|
||||
imports_granularity = "Crate"
|
||||
use_field_init_shorthand = true
|
19
src/lib.rs
Normal file
19
src/lib.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
#![warn(clippy::all, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub mod protocol;
|
||||
|
||||
#[cfg(feature = "usb-hid")]
|
||||
pub mod usb_hid;
|
||||
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
pub mod util;
|
||||
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
pub use embedded_graphics;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
#[doc(hidden)]
|
||||
pub mod cli {
|
||||
include!(concat!(env!("OUT_DIR"), "/cli.rs"));
|
||||
}
|
183
src/main.rs
Normal file
183
src/main.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
#![warn(clippy::all, clippy::pedantic)]
|
||||
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use badgemagic::{
|
||||
protocol::{Mode, PayloadBuffer, Speed, Style},
|
||||
usb_hid::Device,
|
||||
};
|
||||
use base64::Engine;
|
||||
use clap::Parser;
|
||||
use embedded_graphics::{
|
||||
geometry::Point,
|
||||
image::{Image, ImageRawLE},
|
||||
mono_font::{iso_8859_1::FONT_6X9, MonoTextStyle},
|
||||
pixelcolor::BinaryColor,
|
||||
text::Text,
|
||||
Drawable, Pixel,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Parser)]
|
||||
/// Upload a configuration with up to 8 messages to an LED badge
|
||||
#[clap(
|
||||
version = badgemagic::cli::VERSION,
|
||||
author,
|
||||
help_template = "\
|
||||
{before-help}{name} {version}
|
||||
{author-with-newline}
|
||||
{about-with-newline}
|
||||
{usage-heading} {usage}
|
||||
|
||||
{all-args}{after-help}
|
||||
",
|
||||
)]
|
||||
struct Args {
|
||||
/// File format of the config file (toml, json)
|
||||
#[clap(long)]
|
||||
format: Option<String>,
|
||||
|
||||
/// Path to TOML configuration file
|
||||
config: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Config {
|
||||
#[serde(rename = "message")]
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Message {
|
||||
#[serde(default)]
|
||||
blink: bool,
|
||||
|
||||
#[serde(default)]
|
||||
border: bool,
|
||||
|
||||
#[serde(default)]
|
||||
speed: Speed,
|
||||
|
||||
#[serde(default)]
|
||||
mode: Mode,
|
||||
|
||||
#[serde(flatten)]
|
||||
content: Content,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, untagged)]
|
||||
enum Content {
|
||||
Text { text: String },
|
||||
Bitstring { bitstring: String },
|
||||
BitmapBase64 { width: u32, bitmap_base64: String },
|
||||
BitmapFile { width: u32, bitmap_file: PathBuf },
|
||||
// TODO: implement png
|
||||
// PngFile { png_file: PathBuf },
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
let config = fs::read_to_string(&args.config)
|
||||
.with_context(|| format!("load config: {:?}", args.config))?;
|
||||
|
||||
let config: Config = {
|
||||
let extension = args
|
||||
.format
|
||||
.as_deref()
|
||||
.map(AsRef::as_ref)
|
||||
.or(args.config.extension())
|
||||
.context("missing file extension for config file")?;
|
||||
match extension.to_str().unwrap_or_default() {
|
||||
"json" => serde_json::from_str(&config).context("parse config")?,
|
||||
"toml" => toml::from_str(&config).context("parse config")?,
|
||||
_ => anyhow::bail!("unsupported config file extension: {extension:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
let mut payload = PayloadBuffer::new();
|
||||
|
||||
for message in config.messages {
|
||||
let mut style = Style::default();
|
||||
if message.blink {
|
||||
style = style.blink();
|
||||
}
|
||||
if message.border {
|
||||
style = style.border();
|
||||
}
|
||||
style = style.speed(message.speed).mode(message.mode);
|
||||
match message.content {
|
||||
Content::Text { text } => {
|
||||
let text = Text::new(
|
||||
&text,
|
||||
Point::new(0, 7),
|
||||
MonoTextStyle::new(&FONT_6X9, BinaryColor::On),
|
||||
);
|
||||
payload.add_message_drawable(style, &text);
|
||||
}
|
||||
Content::Bitstring { bitstring } => {
|
||||
let lines: Vec<_> = bitstring.trim().lines().collect();
|
||||
|
||||
anyhow::ensure!(
|
||||
lines.len() == 11,
|
||||
"expected 11 lines in bitstring, found {} lines",
|
||||
lines.len()
|
||||
);
|
||||
let width = lines[0].len();
|
||||
if lines.iter().any(|l| l.len() != width) {
|
||||
anyhow::bail!(
|
||||
"lines should have the same length, got: {:?}",
|
||||
lines.iter().map(|l| l.len()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
let mut buffer = payload.add_message(style, (width + 7) / 8);
|
||||
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
for (x, c) in line.chars().enumerate() {
|
||||
match c {
|
||||
'_' => {
|
||||
// off
|
||||
}
|
||||
'X' => {
|
||||
Pixel(
|
||||
Point::new(x.try_into().unwrap(), y.try_into().unwrap()),
|
||||
BinaryColor::On,
|
||||
)
|
||||
.draw(&mut buffer)
|
||||
.unwrap();
|
||||
}
|
||||
_ => anyhow::bail!("invalid bit value for bit ({x}, {y}): {c:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::BitmapBase64 {
|
||||
width,
|
||||
bitmap_base64: bitmap,
|
||||
} => {
|
||||
let data = if bitmap.ends_with('=') {
|
||||
base64::engine::general_purpose::STANDARD
|
||||
} else {
|
||||
base64::engine::general_purpose::STANDARD_NO_PAD
|
||||
}
|
||||
.decode(bitmap)
|
||||
.context("decode bitmap")?;
|
||||
let image_raw = ImageRawLE::<BinaryColor>::new(&data, width);
|
||||
let image = Image::new(&image_raw, Point::zero());
|
||||
payload.add_message_drawable(style, &image);
|
||||
}
|
||||
Content::BitmapFile { width, bitmap_file } => {
|
||||
let data = fs::read(bitmap_file).context("load bitmap")?;
|
||||
let image_raw = ImageRawLE::<BinaryColor>::new(&data, width);
|
||||
let image = Image::new(&image_raw, Point::zero());
|
||||
payload.add_message_drawable(style, &image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Device::single()?.write(payload)?;
|
||||
|
||||
Ok(())
|
||||
}
|
469
src/protocol.rs
Normal file
469
src/protocol.rs
Normal file
|
@ -0,0 +1,469 @@
|
|||
//! Protocol used to update the badge
|
||||
|
||||
use std::{convert::Infallible, num::TryFromIntError};
|
||||
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
use embedded_graphics::{
|
||||
draw_target::DrawTarget,
|
||||
geometry::{Dimensions, Point, Size},
|
||||
pixelcolor::BinaryColor,
|
||||
prelude::Pixel,
|
||||
primitives::Rectangle,
|
||||
Drawable,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use zerocopy::{AsBytes, BigEndian, FromBytes, FromZeroes, U16};
|
||||
|
||||
/// Message style configuration
|
||||
/// ```
|
||||
/// use badgemagic::protocol::{Mode, Style};
|
||||
/// # (
|
||||
/// Style::default().blink().border().mode(Mode::Center)
|
||||
/// # );
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
|
||||
#[must_use]
|
||||
pub struct Style {
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
blink: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
border: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
speed: Speed,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Enable blink mode
|
||||
///
|
||||
/// The message will blink.
|
||||
/// ```
|
||||
/// use badgemagic::protocol::Style;
|
||||
/// # (
|
||||
/// Style::default().blink()
|
||||
/// # );
|
||||
/// ```
|
||||
pub fn blink(mut self) -> Self {
|
||||
self.blink = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a dotted border arround the display.
|
||||
/// ```
|
||||
/// use badgemagic::protocol::Style;
|
||||
/// # (
|
||||
/// Style::default().blink()
|
||||
/// # );
|
||||
/// ```
|
||||
pub fn border(mut self) -> Self {
|
||||
self.border = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the update speed of the animations.
|
||||
///
|
||||
/// The animation will jump to the next pixel at the specified frame rate.
|
||||
/// ```
|
||||
/// use badgemagic::protocol::{Speed, Style};
|
||||
/// # (
|
||||
/// Style::default().speed(Speed::Fps1_2)
|
||||
/// # );
|
||||
/// ```
|
||||
pub fn speed(mut self, speed: Speed) -> Self {
|
||||
self.speed = speed;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the display animation.
|
||||
/// ```
|
||||
/// use badgemagic::protocol::{Mode, Style};
|
||||
/// # (
|
||||
/// Style::default().mode(Mode::Curtain)
|
||||
/// # );
|
||||
/// ```
|
||||
///
|
||||
/// Show text centered, without an animation:
|
||||
/// ```
|
||||
/// use badgemagic::protocol::{Mode, Style};
|
||||
/// # (
|
||||
/// Style::default().mode(Mode::Center)
|
||||
/// # );
|
||||
/// ```
|
||||
pub fn mode(mut self, mode: Mode) -> Self {
|
||||
self.mode = mode;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Animation update speed
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))]
|
||||
pub enum Speed {
|
||||
/// 1.2 FPS
|
||||
Fps1_2,
|
||||
|
||||
/// 1.3 FPS
|
||||
Fps1_3,
|
||||
|
||||
/// 2 FPS
|
||||
Fps2,
|
||||
|
||||
/// 2.4 FPS
|
||||
Fps2_4,
|
||||
|
||||
/// 2.8 FPS
|
||||
#[default]
|
||||
Fps2_8,
|
||||
|
||||
/// 4.5 FPS
|
||||
Fps4_5,
|
||||
|
||||
/// 7.5 FPS
|
||||
Fps7_5,
|
||||
|
||||
/// 15 FPS
|
||||
Fps15,
|
||||
}
|
||||
|
||||
impl From<Speed> for u8 {
|
||||
fn from(value: Speed) -> Self {
|
||||
value as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Speed {
|
||||
type Error = TryFromIntError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
0 => Self::Fps1_2,
|
||||
1 => Self::Fps1_3,
|
||||
2 => Self::Fps2,
|
||||
3 => Self::Fps2_4,
|
||||
4 => Self::Fps2_8,
|
||||
5 => Self::Fps4_5,
|
||||
6 => Self::Fps7_5,
|
||||
7 => Self::Fps15,
|
||||
_ => return Err(u8::try_from(-1).unwrap_err()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Message display mode
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||
pub enum Mode {
|
||||
/// Scroll thorugh the message from left to right
|
||||
#[default]
|
||||
Left,
|
||||
|
||||
/// Scroll through the message from right to left
|
||||
Right,
|
||||
|
||||
/// Enter from the bottom, move up
|
||||
Up,
|
||||
|
||||
/// Enter from the top, move down
|
||||
Down,
|
||||
|
||||
/// Center the text, no animation
|
||||
Center,
|
||||
|
||||
/// Fast mode for animations
|
||||
///
|
||||
/// Will leave a 4 pixel gap between screens:
|
||||
/// Place a 44x11 pixel screen every 48 pixels
|
||||
Fast,
|
||||
|
||||
/// Drop rows of pixels from the top
|
||||
Drop,
|
||||
|
||||
/// Open a curtain and reveal the message
|
||||
Curtain,
|
||||
|
||||
/// A laser will reveal the message from left to right
|
||||
Laser,
|
||||
}
|
||||
|
||||
const MSG_PADDING_ALIGN: usize = 64;
|
||||
|
||||
const MAGIC: [u8; 6] = *b"wang\0\0";
|
||||
|
||||
#[derive(FromZeroes, FromBytes, AsBytes)]
|
||||
#[repr(C)]
|
||||
struct Header {
|
||||
magic: [u8; 6],
|
||||
blink: u8,
|
||||
border: u8,
|
||||
speed_and_mode: [u8; 8],
|
||||
message_length: [U16<BigEndian>; 8],
|
||||
_padding_1: [u8; 6],
|
||||
timestamp: Timestamp,
|
||||
_padding_2: [u8; 20],
|
||||
}
|
||||
|
||||
#[derive(FromZeroes, FromBytes, AsBytes)]
|
||||
#[repr(C)]
|
||||
struct Timestamp {
|
||||
year: u8,
|
||||
month: u8,
|
||||
day: u8,
|
||||
hour: u8,
|
||||
minute: u8,
|
||||
second: u8,
|
||||
}
|
||||
|
||||
impl Timestamp {
|
||||
fn new(ts: OffsetDateTime) -> Self {
|
||||
Self {
|
||||
#[allow(clippy::cast_possible_truncation)] // clippy does not understand `rem_euclid(100) <= 100`
|
||||
year: ts.year().rem_euclid(100) as u8,
|
||||
month: ts.month() as u8,
|
||||
day: ts.day(),
|
||||
hour: ts.hour(),
|
||||
minute: ts.minute(),
|
||||
second: ts.second(),
|
||||
}
|
||||
}
|
||||
|
||||
fn now() -> Self {
|
||||
Self::new(OffsetDateTime::now_utc())
|
||||
}
|
||||
}
|
||||
|
||||
/// Buffer to create a payload
|
||||
///
|
||||
/// A payload consits of up to 8 messages
|
||||
/// ```
|
||||
/// # #[cfg(feature = "embedded-graphics")]
|
||||
/// # fn main() {
|
||||
/// # use badgemagic::protocol::{PayloadBuffer, Style};
|
||||
/// use badgemagic::embedded_graphics::{
|
||||
/// geometry::{Point, Size},
|
||||
/// pixelcolor::BinaryColor,
|
||||
/// primitives::{PrimitiveStyle, Rectangle, Styled},
|
||||
/// };
|
||||
///
|
||||
/// let mut buffer = PayloadBuffer::new();
|
||||
/// buffer.add_message_drawable(
|
||||
/// Style::default(),
|
||||
/// &Styled::new(
|
||||
/// Rectangle::new(Point::new(2, 2), Size::new(4, 7)),
|
||||
/// PrimitiveStyle::with_fill(BinaryColor::On),
|
||||
/// ),
|
||||
/// );
|
||||
/// # }
|
||||
/// # #[cfg(not(feature = "embedded-graphics"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub struct PayloadBuffer {
|
||||
num_messages: u8,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default for PayloadBuffer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PayloadBuffer {
|
||||
/// Create a new empty buffer
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
num_messages: 0,
|
||||
data: Header {
|
||||
magic: MAGIC,
|
||||
blink: 0,
|
||||
border: 0,
|
||||
speed_and_mode: [0; 8],
|
||||
message_length: [0.into(); 8],
|
||||
_padding_1: [0; 6],
|
||||
timestamp: Timestamp::now(),
|
||||
_padding_2: [0; 20],
|
||||
}
|
||||
.as_bytes()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn header_mut(&mut self) -> &mut Header {
|
||||
Header::mut_from_prefix(&mut self.data).unwrap()
|
||||
}
|
||||
|
||||
/// Return the current number of messages
|
||||
pub fn num_messages(&mut self) -> usize {
|
||||
self.num_messages as usize
|
||||
}
|
||||
|
||||
/// Add a messages containing the specified `content`
|
||||
///
|
||||
/// ## Panics
|
||||
/// This method panics if it is unable to draw the content.
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
pub fn add_message_drawable<O>(
|
||||
&mut self,
|
||||
style: Style,
|
||||
content: &(impl Drawable<Color = BinaryColor, Output = O> + Dimensions),
|
||||
) -> O {
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
fn saturating_usize_to_isize(n: usize) -> isize {
|
||||
usize::min(n, isize::MAX as usize) as isize
|
||||
}
|
||||
|
||||
fn add(a: i32, b: u32) -> usize {
|
||||
let result = a as isize + saturating_usize_to_isize(b as usize);
|
||||
result.try_into().unwrap_or_default()
|
||||
}
|
||||
|
||||
let bounds = content.bounding_box();
|
||||
let width = add(bounds.top_left.x, bounds.size.width);
|
||||
let mut message = self.add_message(style, (width + 7) / 8);
|
||||
content.draw(&mut message).unwrap()
|
||||
}
|
||||
|
||||
/// Add a message with `count * 8` columns
|
||||
///
|
||||
/// The returned `MessageBuffer` can be used as an `embedded_graphics::DrawTarget`
|
||||
/// with the `embedded_graphics` feature.
|
||||
///
|
||||
/// ## Panics
|
||||
/// Panics if the supported number of messages is reached.
|
||||
pub fn add_message(&mut self, style: Style, count: usize) -> MessageBuffer {
|
||||
let index = self.num_messages as usize;
|
||||
assert!(
|
||||
index < 8,
|
||||
"maximum number of supported messages reached: {index} messages",
|
||||
);
|
||||
self.num_messages += 1;
|
||||
|
||||
let header = self.header_mut();
|
||||
|
||||
if style.blink {
|
||||
header.blink |= 1 << index;
|
||||
}
|
||||
if style.border {
|
||||
header.border |= 1 << index;
|
||||
}
|
||||
header.speed_and_mode[index] = ((style.speed as u8) << 4) | style.mode as u8;
|
||||
header.message_length[index] = count.try_into().unwrap();
|
||||
|
||||
let start = self.data.len();
|
||||
self.data.resize(start + count * 11, 0);
|
||||
MessageBuffer(FromBytes::mut_slice_from(&mut self.data[start..]).unwrap())
|
||||
}
|
||||
|
||||
/// Get the current payload as bytes (without padding)
|
||||
#[must_use]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Convert the payload buffe into bytes (with padding)
|
||||
#[allow(clippy::missing_panics_doc)] // should never panic
|
||||
#[must_use]
|
||||
pub fn into_padded_bytes(self) -> impl AsRef<[u8]> {
|
||||
let mut data = self.data;
|
||||
|
||||
let prev_len = data.len();
|
||||
|
||||
// pad msg to align to 64 bytes
|
||||
data.resize(
|
||||
(data.len() + (MSG_PADDING_ALIGN - 1)) & !(MSG_PADDING_ALIGN - 1),
|
||||
0,
|
||||
);
|
||||
|
||||
// validate alignment
|
||||
assert_eq!(data.len() % 64, 0);
|
||||
assert!(prev_len <= data.len());
|
||||
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
/// A display buffer for a single message.
|
||||
///
|
||||
/// Can be used as an `embedded_graphics::DrawTarget`.
|
||||
pub struct MessageBuffer<'a>(&'a mut [[u8; 11]]);
|
||||
|
||||
impl MessageBuffer<'_> {
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
fn set(&mut self, point: Point, color: BinaryColor) -> Option<()> {
|
||||
let byte = self
|
||||
.0
|
||||
.get_mut(usize::try_from(point.x / 8).ok()?)?
|
||||
.get_mut(usize::try_from(point.y).ok()?)?;
|
||||
|
||||
let bit = 0x80 >> (point.x % 8);
|
||||
match color {
|
||||
BinaryColor::Off => {
|
||||
*byte &= !bit;
|
||||
}
|
||||
BinaryColor::On => {
|
||||
*byte |= bit;
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
impl Dimensions for MessageBuffer<'_> {
|
||||
fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle {
|
||||
Rectangle::new(
|
||||
Point::zero(),
|
||||
Size::new((self.0.len() * 8).try_into().unwrap(), 11),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "embedded-graphics")]
|
||||
impl DrawTarget for MessageBuffer<'_> {
|
||||
type Color = BinaryColor;
|
||||
|
||||
type Error = Infallible;
|
||||
|
||||
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||
where
|
||||
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||
{
|
||||
for Pixel(point, color) in pixels {
|
||||
#[allow(clippy::manual_assert)]
|
||||
if self.set(point, color).is_none() {
|
||||
panic!(
|
||||
"tried to draw pixel outside the display area (x: {}, y: {})",
|
||||
point.x, point.y
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::ops::Range;
|
||||
|
||||
use super::Speed;
|
||||
|
||||
#[test]
|
||||
fn speed_to_u8_and_back() {
|
||||
const VALID_SPEED_VALUES: Range<u8> = 1..8;
|
||||
for i in u8::MIN..u8::MAX {
|
||||
if let Ok(speed) = Speed::try_from(i) {
|
||||
assert_eq!(u8::from(speed), i);
|
||||
} else {
|
||||
assert!(!VALID_SPEED_VALUES.contains(&i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
91
src/usb_hid.rs
Normal file
91
src/usb_hid.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
//! Connect to an LED badge via USB HID
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use hidapi::{DeviceInfo, HidApi, HidDevice};
|
||||
|
||||
use crate::protocol::PayloadBuffer;
|
||||
|
||||
enum DeviceType {
|
||||
// rename if we add another device type
|
||||
TheOnlyOneWeSupportForNow,
|
||||
}
|
||||
|
||||
impl DeviceType {
|
||||
fn new(info: &DeviceInfo) -> Option<Self> {
|
||||
Some(match (info.vendor_id(), info.product_id()) {
|
||||
(0x0416, 0x5020) => Self::TheOnlyOneWeSupportForNow,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A discovered USB device
|
||||
pub struct Device {
|
||||
api: Arc<HidApi>,
|
||||
info: DeviceInfo,
|
||||
type_: DeviceType,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Return all supported devices
|
||||
pub fn enumerate() -> Result<Vec<Self>> {
|
||||
let api = HidApi::new().context("create hid api")?;
|
||||
let api = Arc::new(api);
|
||||
|
||||
let devices = api.device_list();
|
||||
let devices = devices
|
||||
.filter_map(|info| {
|
||||
DeviceType::new(info).map(|type_| Device {
|
||||
api: api.clone(),
|
||||
info: info.clone(),
|
||||
type_,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
/// Return the single supported device
|
||||
///
|
||||
/// This function returns an error if no device could be found
|
||||
/// or if multiple devices would match.
|
||||
pub fn single() -> Result<Self> {
|
||||
let mut devices = Self::enumerate()?.into_iter();
|
||||
let device = devices.next().context("no device found")?;
|
||||
anyhow::ensure!(devices.next().is_none(), "multiple devices found");
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
/// Write a payload to the device
|
||||
pub fn write(&self, payload: PayloadBuffer) -> Result<()> {
|
||||
let device = self.info.open_device(&self.api).context("open device")?;
|
||||
match self.type_ {
|
||||
DeviceType::TheOnlyOneWeSupportForNow => {
|
||||
write_raw(&device, payload.into_padded_bytes().as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_raw(device: &HidDevice, data: &[u8]) -> Result<()> {
|
||||
anyhow::ensure!(data.len() % 64 == 0, "payload not padded to 64 bytes");
|
||||
|
||||
// the device will brick itself if the payload is too long (more then 8192 bytes)
|
||||
anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)");
|
||||
|
||||
// just to be sure
|
||||
assert!(data.len() <= 8192);
|
||||
|
||||
let n = device.write(data).context("write payload")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
n == data.len(),
|
||||
"incomplete write: {n} of {} bytes",
|
||||
data.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
62
src/util.rs
Normal file
62
src/util.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
//! Graphics utilities
|
||||
|
||||
use embedded_graphics::Drawable;
|
||||
|
||||
use self::layout::ZStack;
|
||||
|
||||
/// Drawable layout extension
|
||||
pub trait DrawableLayoutExt: Drawable + Sized {
|
||||
/// Draw a
|
||||
fn z_stack<T>(self, other: T) -> ZStack<Self, T> {
|
||||
ZStack(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DrawableLayoutExt for T where T: Drawable {}
|
||||
|
||||
pub mod layout {
|
||||
//! Types used by `DrawableLayoutExt `
|
||||
|
||||
use embedded_graphics::{
|
||||
geometry::{Dimensions, Point},
|
||||
primitives::Rectangle,
|
||||
Drawable,
|
||||
};
|
||||
|
||||
pub struct ZStack<A, B>(pub(super) A, pub(super) B);
|
||||
|
||||
impl<A, B> Dimensions for ZStack<A, B>
|
||||
where
|
||||
A: Dimensions,
|
||||
B: Dimensions,
|
||||
{
|
||||
fn bounding_box(&self) -> Rectangle {
|
||||
let a = self.0.bounding_box();
|
||||
let b = self.1.bounding_box();
|
||||
let left = i32::min(a.top_left.x, b.top_left.x);
|
||||
let top = i32::min(a.top_left.y, b.top_left.y);
|
||||
let right = i32::max(a.bottom_right().unwrap().x, b.bottom_right().unwrap().x);
|
||||
let bottom = i32::max(a.bottom_right().unwrap().y, b.bottom_right().unwrap().y);
|
||||
Rectangle::with_corners(Point::new(left, top), Point::new(right, bottom))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Drawable for ZStack<A, B>
|
||||
where
|
||||
A: Drawable,
|
||||
B: Drawable<Color = A::Color>,
|
||||
{
|
||||
type Color = A::Color;
|
||||
|
||||
type Output = (A::Output, B::Output);
|
||||
|
||||
fn draw<D>(&self, target: &mut D) -> std::prelude::v1::Result<Self::Output, D::Error>
|
||||
where
|
||||
D: embedded_graphics::prelude::DrawTarget<Color = Self::Color>,
|
||||
{
|
||||
let a = self.0.draw(target)?;
|
||||
let b = self.1.draw(target)?;
|
||||
Ok((a, b))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue