├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── demo ├── effects.toml ├── jump.toml └── smiley.toml ├── examples └── hello-world.rs ├── rustfmt.toml └── src ├── ble.rs ├── lib.rs ├── main.rs ├── protocol.rs ├── usb_hid.rs └── util.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: cargo 5 | directory: / 6 | schedule: 7 | interval: daily 8 | groups: 9 | minor-updates: 10 | applies-to: version-updates 11 | update-types: 12 | - minor 13 | - patch 14 | 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: daily 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | - workflow_dispatch 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | CARGO_INCREMENTAL: 0 13 | CARGO_TERM_COLOR: always 14 | RUSTFLAGS: -C link-arg=-s 15 | 16 | jobs: 17 | format: 18 | name: Check rust format 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 45 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Setup rust 24 | run: | 25 | rustup toolchain install nightly --profile minimal --component rustfmt --no-self-update 26 | - name: Run cargo fmt 27 | run: cargo +nightly fmt --check 28 | 29 | test: 30 | name: ${{ matrix.cmd.name }} (Rust ${{ matrix.rust }}) ${{ matrix.features }} 31 | runs-on: ubuntu-latest 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | rust: 36 | - stable 37 | features: 38 | - --no-default-features 39 | - 40 | - -F cli 41 | cmd: 42 | - name: Test 43 | run: cargo test --locked 44 | - name: Clippy 45 | run: cargo clippy --locked --tests 46 | run2: -D warnings 47 | timeout-minutes: 45 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: Setup rust 51 | run: | 52 | rustup toolchain install ${{ matrix.rust }} --profile minimal --no-self-update 53 | - name: Install build dependencies 54 | run: sudo apt-get install -y libudev-dev libdbus-1-dev 55 | - name: ${{ matrix.cmd.name }} 56 | run: ${{ matrix.cmd.run }} ${{ matrix.features }} -- ${{ matrix.cmd.run2 }} 57 | 58 | build: 59 | name: Build for ${{ matrix.target.name }} 60 | runs-on: ${{ matrix.target.runs-on }} 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | target: 65 | - name: Linux (x86_64) 66 | runs-on: ubuntu-latest 67 | target: x86_64-unknown-linux-gnu 68 | pre-build: | 69 | sudo apt-get install -y libudev-dev libdbus-1-dev 70 | - name: Windows (x86_64) 71 | runs-on: windows-latest 72 | target: x86_64-pc-windows-msvc 73 | ext: .exe 74 | - name: MacOS (x86_64) 75 | runs-on: macos-latest 76 | target: x86_64-apple-darwin 77 | - name: MacOS (arm64) 78 | runs-on: macos-latest 79 | target: aarch64-apple-darwin 80 | timeout-minutes: 45 81 | # env: 82 | # RUSTFLAGS: -C target-feature=+crt-static 83 | steps: 84 | - uses: actions/checkout@v4 85 | - name: Setup rust 86 | run: | 87 | rustup toolchain install stable --target ${{ matrix.target.target }} --profile minimal --no-self-update 88 | - name: Install build dependencies 89 | run: ${{ matrix.target.pre-build }} 90 | if: matrix.target.pre-build 91 | - name: Build for ${{ matrix.target.name }} 92 | run: cargo build --locked --release --target ${{ matrix.target.target }} --no-default-features -F cli 93 | - name: Check file 94 | run: | 95 | file target/${{ matrix.target.target }}/release/badgemagic${{ matrix.target.ext }} 96 | stat target/${{ matrix.target.target }}/release/badgemagic${{ matrix.target.ext }} 97 | mv target/${{ matrix.target.target }}/release/badgemagic${{ matrix.target.ext }} badgemagic.${{ matrix.target.target }}${{ matrix.target.ext }} 98 | - name: Run for ${{ matrix.target.name }} 99 | run: ./badgemagic.${{ matrix.target.target }}${{ matrix.target.ext }} --help 100 | - uses: actions/upload-artifact@v4 101 | with: 102 | name: badgemagic.${{ matrix.target.target }}${{ matrix.target.ext }} 103 | path: badgemagic.${{ matrix.target.target }}${{ matrix.target.ext }} 104 | if-no-files-found: error 105 | 106 | ready: 107 | name: All required checks passed 108 | needs: 109 | - format 110 | - test 111 | - build 112 | runs-on: ubuntu-latest 113 | steps: 114 | - run: date 115 | 116 | release: 117 | name: Create release 118 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 119 | permissions: 120 | contents: write 121 | needs: 122 | - format 123 | - test 124 | - build 125 | runs-on: ubuntu-latest 126 | timeout-minutes: 45 127 | steps: 128 | - uses: actions/download-artifact@v4 129 | with: 130 | pattern: badgemagic.* 131 | merge-multiple: true 132 | - name: List artifacts 133 | run: find -exec ls -ld {} + 134 | - uses: actions/github-script@v7 135 | id: upload-release-asset 136 | with: 137 | script: | 138 | const fs = require('fs'); 139 | const { env } = process; 140 | 141 | const { data: release } = await github.rest.repos.createRelease({ 142 | owner: context.repo.owner, 143 | repo: context.repo.repo, 144 | tag_name: `commit-${env.GITHUB_SHA.slice(0, 7)}`, 145 | target_commitish: env.GITHUB_SHA, 146 | draft: true, 147 | generate_release_notes: true, 148 | }); 149 | console.log('release:', release.id); 150 | 151 | const artifacts = fs.readdirSync('.'); 152 | console.log('artifacts:', artifacts); 153 | 154 | for (const name of artifacts) { 155 | const data = fs.readFileSync(name); 156 | await github.rest.repos.uploadReleaseAsset({ 157 | owner: context.repo.owner, 158 | repo: context.repo.repo, 159 | release_id: release.id, 160 | name, 161 | data, 162 | }); 163 | } 164 | 165 | await github.rest.repos.updateRelease({ 166 | owner: context.repo.owner, 167 | repo: context.repo.repo, 168 | release_id: release.id, 169 | draft: false, 170 | }); 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*.toml 3 | !/Cargo.toml 4 | !/rustfmt.toml 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.4" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 55 | dependencies = [ 56 | "windows-sys 0.52.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys 0.52.0", 67 | ] 68 | 69 | [[package]] 70 | name = "anyhow" 71 | version = "1.0.95" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 74 | 75 | [[package]] 76 | name = "async-trait" 77 | version = "0.1.83" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 80 | dependencies = [ 81 | "proc-macro2", 82 | "quote", 83 | "syn", 84 | ] 85 | 86 | [[package]] 87 | name = "autocfg" 88 | version = "1.3.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 91 | 92 | [[package]] 93 | name = "az" 94 | version = "1.2.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" 97 | 98 | [[package]] 99 | name = "backtrace" 100 | version = "0.3.73" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 103 | dependencies = [ 104 | "addr2line", 105 | "cc", 106 | "cfg-if", 107 | "libc", 108 | "miniz_oxide", 109 | "object", 110 | "rustc-demangle", 111 | ] 112 | 113 | [[package]] 114 | name = "badgemagic" 115 | version = "0.1.0" 116 | dependencies = [ 117 | "anyhow", 118 | "base64", 119 | "btleplug", 120 | "clap", 121 | "embedded-graphics", 122 | "hidapi", 123 | "serde", 124 | "serde_json", 125 | "time", 126 | "tokio", 127 | "toml", 128 | "uuid", 129 | "zerocopy", 130 | ] 131 | 132 | [[package]] 133 | name = "base64" 134 | version = "0.22.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 137 | 138 | [[package]] 139 | name = "bitflags" 140 | version = "2.6.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 143 | 144 | [[package]] 145 | name = "block2" 146 | version = "0.5.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" 149 | dependencies = [ 150 | "objc2", 151 | ] 152 | 153 | [[package]] 154 | name = "bluez-async" 155 | version = "0.7.2" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "5ce7d4413c940e8e3cb6afc122d3f4a07096aca259d286781128683fc9f39d9b" 158 | dependencies = [ 159 | "async-trait", 160 | "bitflags", 161 | "bluez-generated", 162 | "dbus", 163 | "dbus-tokio", 164 | "futures", 165 | "itertools", 166 | "log", 167 | "serde", 168 | "serde-xml-rs", 169 | "thiserror", 170 | "tokio", 171 | "uuid", 172 | ] 173 | 174 | [[package]] 175 | name = "bluez-generated" 176 | version = "0.3.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "4d1c659dbc82f0b8ca75606c91a371e763589b7f6acf36858eeed0c705afe367" 179 | dependencies = [ 180 | "dbus", 181 | ] 182 | 183 | [[package]] 184 | name = "btleplug" 185 | version = "0.11.6" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "82837101dad9a257a3ffe35fbc2ef4df9a27aecbe5343ce83ac233bcab283394" 188 | dependencies = [ 189 | "async-trait", 190 | "bitflags", 191 | "bluez-async", 192 | "dashmap 6.1.0", 193 | "dbus", 194 | "futures", 195 | "jni", 196 | "jni-utils", 197 | "log", 198 | "objc2", 199 | "objc2-core-bluetooth", 200 | "objc2-foundation", 201 | "once_cell", 202 | "static_assertions", 203 | "thiserror", 204 | "tokio", 205 | "tokio-stream", 206 | "uuid", 207 | "windows", 208 | ] 209 | 210 | [[package]] 211 | name = "byteorder" 212 | version = "1.5.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 215 | 216 | [[package]] 217 | name = "bytes" 218 | version = "1.6.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" 221 | 222 | [[package]] 223 | name = "cc" 224 | version = "1.0.98" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 227 | 228 | [[package]] 229 | name = "cesu8" 230 | version = "1.1.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 233 | 234 | [[package]] 235 | name = "cfg-if" 236 | version = "1.0.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 239 | 240 | [[package]] 241 | name = "clap" 242 | version = "4.5.23" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 245 | dependencies = [ 246 | "clap_builder", 247 | "clap_derive", 248 | ] 249 | 250 | [[package]] 251 | name = "clap_builder" 252 | version = "4.5.23" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 255 | dependencies = [ 256 | "anstream", 257 | "anstyle", 258 | "clap_lex", 259 | "strsim", 260 | ] 261 | 262 | [[package]] 263 | name = "clap_derive" 264 | version = "4.5.18" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 267 | dependencies = [ 268 | "heck", 269 | "proc-macro2", 270 | "quote", 271 | "syn", 272 | ] 273 | 274 | [[package]] 275 | name = "clap_lex" 276 | version = "0.7.4" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 279 | 280 | [[package]] 281 | name = "colorchoice" 282 | version = "1.0.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 285 | 286 | [[package]] 287 | name = "combine" 288 | version = "4.6.7" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 291 | dependencies = [ 292 | "bytes", 293 | "memchr", 294 | ] 295 | 296 | [[package]] 297 | name = "crossbeam-utils" 298 | version = "0.8.21" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 301 | 302 | [[package]] 303 | name = "dashmap" 304 | version = "5.5.3" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" 307 | dependencies = [ 308 | "cfg-if", 309 | "hashbrown", 310 | "lock_api", 311 | "once_cell", 312 | "parking_lot_core", 313 | ] 314 | 315 | [[package]] 316 | name = "dashmap" 317 | version = "6.1.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 320 | dependencies = [ 321 | "cfg-if", 322 | "crossbeam-utils", 323 | "hashbrown", 324 | "lock_api", 325 | "once_cell", 326 | "parking_lot_core", 327 | ] 328 | 329 | [[package]] 330 | name = "dbus" 331 | version = "0.9.7" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 334 | dependencies = [ 335 | "futures-channel", 336 | "futures-util", 337 | "libc", 338 | "libdbus-sys", 339 | "winapi", 340 | ] 341 | 342 | [[package]] 343 | name = "dbus-tokio" 344 | version = "0.7.6" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" 347 | dependencies = [ 348 | "dbus", 349 | "libc", 350 | "tokio", 351 | ] 352 | 353 | [[package]] 354 | name = "deranged" 355 | version = "0.3.11" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 358 | dependencies = [ 359 | "powerfmt", 360 | ] 361 | 362 | [[package]] 363 | name = "either" 364 | version = "1.13.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 367 | 368 | [[package]] 369 | name = "embedded-graphics" 370 | version = "0.8.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" 373 | dependencies = [ 374 | "az", 375 | "byteorder", 376 | "embedded-graphics-core", 377 | "float-cmp", 378 | "micromath", 379 | ] 380 | 381 | [[package]] 382 | name = "embedded-graphics-core" 383 | version = "0.4.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" 386 | dependencies = [ 387 | "az", 388 | "byteorder", 389 | ] 390 | 391 | [[package]] 392 | name = "equivalent" 393 | version = "1.0.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 396 | 397 | [[package]] 398 | name = "float-cmp" 399 | version = "0.9.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 402 | dependencies = [ 403 | "num-traits", 404 | ] 405 | 406 | [[package]] 407 | name = "futures" 408 | version = "0.3.31" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 411 | dependencies = [ 412 | "futures-channel", 413 | "futures-core", 414 | "futures-executor", 415 | "futures-io", 416 | "futures-sink", 417 | "futures-task", 418 | "futures-util", 419 | ] 420 | 421 | [[package]] 422 | name = "futures-channel" 423 | version = "0.3.31" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 426 | dependencies = [ 427 | "futures-core", 428 | "futures-sink", 429 | ] 430 | 431 | [[package]] 432 | name = "futures-core" 433 | version = "0.3.31" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 436 | 437 | [[package]] 438 | name = "futures-executor" 439 | version = "0.3.31" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 442 | dependencies = [ 443 | "futures-core", 444 | "futures-task", 445 | "futures-util", 446 | ] 447 | 448 | [[package]] 449 | name = "futures-io" 450 | version = "0.3.31" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 453 | 454 | [[package]] 455 | name = "futures-macro" 456 | version = "0.3.31" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 459 | dependencies = [ 460 | "proc-macro2", 461 | "quote", 462 | "syn", 463 | ] 464 | 465 | [[package]] 466 | name = "futures-sink" 467 | version = "0.3.31" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 470 | 471 | [[package]] 472 | name = "futures-task" 473 | version = "0.3.31" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 476 | 477 | [[package]] 478 | name = "futures-util" 479 | version = "0.3.31" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 482 | dependencies = [ 483 | "futures-channel", 484 | "futures-core", 485 | "futures-io", 486 | "futures-macro", 487 | "futures-sink", 488 | "futures-task", 489 | "memchr", 490 | "pin-project-lite", 491 | "pin-utils", 492 | "slab", 493 | ] 494 | 495 | [[package]] 496 | name = "gimli" 497 | version = "0.29.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 500 | 501 | [[package]] 502 | name = "hashbrown" 503 | version = "0.14.5" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 506 | 507 | [[package]] 508 | name = "heck" 509 | version = "0.5.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 512 | 513 | [[package]] 514 | name = "hermit-abi" 515 | version = "0.3.9" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 518 | 519 | [[package]] 520 | name = "hidapi" 521 | version = "2.6.3" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" 524 | dependencies = [ 525 | "cc", 526 | "cfg-if", 527 | "libc", 528 | "pkg-config", 529 | "windows-sys 0.48.0", 530 | ] 531 | 532 | [[package]] 533 | name = "indexmap" 534 | version = "2.2.6" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 537 | dependencies = [ 538 | "equivalent", 539 | "hashbrown", 540 | ] 541 | 542 | [[package]] 543 | name = "is_terminal_polyfill" 544 | version = "1.70.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 547 | 548 | [[package]] 549 | name = "itertools" 550 | version = "0.10.5" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 553 | dependencies = [ 554 | "either", 555 | ] 556 | 557 | [[package]] 558 | name = "itoa" 559 | version = "1.0.11" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 562 | 563 | [[package]] 564 | name = "jni" 565 | version = "0.19.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" 568 | dependencies = [ 569 | "cesu8", 570 | "combine", 571 | "jni-sys", 572 | "log", 573 | "thiserror", 574 | "walkdir", 575 | ] 576 | 577 | [[package]] 578 | name = "jni-sys" 579 | version = "0.3.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 582 | 583 | [[package]] 584 | name = "jni-utils" 585 | version = "0.1.1" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" 588 | dependencies = [ 589 | "dashmap 5.5.3", 590 | "futures", 591 | "jni", 592 | "log", 593 | "once_cell", 594 | "static_assertions", 595 | "uuid", 596 | ] 597 | 598 | [[package]] 599 | name = "libc" 600 | version = "0.2.155" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 603 | 604 | [[package]] 605 | name = "libdbus-sys" 606 | version = "0.2.5" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 609 | dependencies = [ 610 | "pkg-config", 611 | ] 612 | 613 | [[package]] 614 | name = "lock_api" 615 | version = "0.4.12" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 618 | dependencies = [ 619 | "autocfg", 620 | "scopeguard", 621 | ] 622 | 623 | [[package]] 624 | name = "log" 625 | version = "0.4.22" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 628 | 629 | [[package]] 630 | name = "memchr" 631 | version = "2.7.2" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 634 | 635 | [[package]] 636 | name = "micromath" 637 | version = "2.1.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" 640 | 641 | [[package]] 642 | name = "miniz_oxide" 643 | version = "0.7.4" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 646 | dependencies = [ 647 | "adler", 648 | ] 649 | 650 | [[package]] 651 | name = "mio" 652 | version = "1.0.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 655 | dependencies = [ 656 | "hermit-abi", 657 | "libc", 658 | "wasi", 659 | "windows-sys 0.52.0", 660 | ] 661 | 662 | [[package]] 663 | name = "num-conv" 664 | version = "0.1.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 667 | 668 | [[package]] 669 | name = "num-traits" 670 | version = "0.2.19" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 673 | dependencies = [ 674 | "autocfg", 675 | ] 676 | 677 | [[package]] 678 | name = "objc-sys" 679 | version = "0.3.5" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" 682 | 683 | [[package]] 684 | name = "objc2" 685 | version = "0.5.2" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" 688 | dependencies = [ 689 | "objc-sys", 690 | "objc2-encode", 691 | ] 692 | 693 | [[package]] 694 | name = "objc2-core-bluetooth" 695 | version = "0.2.2" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" 698 | dependencies = [ 699 | "bitflags", 700 | "objc2", 701 | "objc2-foundation", 702 | ] 703 | 704 | [[package]] 705 | name = "objc2-encode" 706 | version = "4.0.3" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" 709 | 710 | [[package]] 711 | name = "objc2-foundation" 712 | version = "0.2.2" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" 715 | dependencies = [ 716 | "bitflags", 717 | "block2", 718 | "libc", 719 | "objc2", 720 | ] 721 | 722 | [[package]] 723 | name = "object" 724 | version = "0.36.1" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 727 | dependencies = [ 728 | "memchr", 729 | ] 730 | 731 | [[package]] 732 | name = "once_cell" 733 | version = "1.20.2" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 736 | 737 | [[package]] 738 | name = "parking_lot_core" 739 | version = "0.9.10" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 742 | dependencies = [ 743 | "cfg-if", 744 | "libc", 745 | "redox_syscall", 746 | "smallvec", 747 | "windows-targets 0.52.5", 748 | ] 749 | 750 | [[package]] 751 | name = "pin-project-lite" 752 | version = "0.2.14" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 755 | 756 | [[package]] 757 | name = "pin-utils" 758 | version = "0.1.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 761 | 762 | [[package]] 763 | name = "pkg-config" 764 | version = "0.3.30" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 767 | 768 | [[package]] 769 | name = "powerfmt" 770 | version = "0.2.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 773 | 774 | [[package]] 775 | name = "proc-macro2" 776 | version = "1.0.92" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 779 | dependencies = [ 780 | "unicode-ident", 781 | ] 782 | 783 | [[package]] 784 | name = "quote" 785 | version = "1.0.36" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 788 | dependencies = [ 789 | "proc-macro2", 790 | ] 791 | 792 | [[package]] 793 | name = "redox_syscall" 794 | version = "0.5.3" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 797 | dependencies = [ 798 | "bitflags", 799 | ] 800 | 801 | [[package]] 802 | name = "rustc-demangle" 803 | version = "0.1.24" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 806 | 807 | [[package]] 808 | name = "ryu" 809 | version = "1.0.18" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 812 | 813 | [[package]] 814 | name = "same-file" 815 | version = "1.0.6" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 818 | dependencies = [ 819 | "winapi-util", 820 | ] 821 | 822 | [[package]] 823 | name = "scopeguard" 824 | version = "1.2.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 827 | 828 | [[package]] 829 | name = "serde" 830 | version = "1.0.217" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 833 | dependencies = [ 834 | "serde_derive", 835 | ] 836 | 837 | [[package]] 838 | name = "serde-xml-rs" 839 | version = "0.6.0" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" 842 | dependencies = [ 843 | "log", 844 | "serde", 845 | "thiserror", 846 | "xml-rs", 847 | ] 848 | 849 | [[package]] 850 | name = "serde_derive" 851 | version = "1.0.217" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 854 | dependencies = [ 855 | "proc-macro2", 856 | "quote", 857 | "syn", 858 | ] 859 | 860 | [[package]] 861 | name = "serde_json" 862 | version = "1.0.134" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" 865 | dependencies = [ 866 | "itoa", 867 | "memchr", 868 | "ryu", 869 | "serde", 870 | ] 871 | 872 | [[package]] 873 | name = "serde_spanned" 874 | version = "0.6.7" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 877 | dependencies = [ 878 | "serde", 879 | ] 880 | 881 | [[package]] 882 | name = "slab" 883 | version = "0.4.9" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 886 | dependencies = [ 887 | "autocfg", 888 | ] 889 | 890 | [[package]] 891 | name = "smallvec" 892 | version = "1.13.2" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 895 | 896 | [[package]] 897 | name = "socket2" 898 | version = "0.5.7" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 901 | dependencies = [ 902 | "libc", 903 | "windows-sys 0.52.0", 904 | ] 905 | 906 | [[package]] 907 | name = "static_assertions" 908 | version = "1.1.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 911 | 912 | [[package]] 913 | name = "strsim" 914 | version = "0.11.1" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 917 | 918 | [[package]] 919 | name = "syn" 920 | version = "2.0.93" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" 923 | dependencies = [ 924 | "proc-macro2", 925 | "quote", 926 | "unicode-ident", 927 | ] 928 | 929 | [[package]] 930 | name = "thiserror" 931 | version = "1.0.69" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 934 | dependencies = [ 935 | "thiserror-impl", 936 | ] 937 | 938 | [[package]] 939 | name = "thiserror-impl" 940 | version = "1.0.69" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 943 | dependencies = [ 944 | "proc-macro2", 945 | "quote", 946 | "syn", 947 | ] 948 | 949 | [[package]] 950 | name = "time" 951 | version = "0.3.37" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 954 | dependencies = [ 955 | "deranged", 956 | "num-conv", 957 | "powerfmt", 958 | "serde", 959 | "time-core", 960 | ] 961 | 962 | [[package]] 963 | name = "time-core" 964 | version = "0.1.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 967 | 968 | [[package]] 969 | name = "tokio" 970 | version = "1.42.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" 973 | dependencies = [ 974 | "backtrace", 975 | "libc", 976 | "mio", 977 | "pin-project-lite", 978 | "socket2", 979 | "windows-sys 0.52.0", 980 | ] 981 | 982 | [[package]] 983 | name = "tokio-stream" 984 | version = "0.1.17" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 987 | dependencies = [ 988 | "futures-core", 989 | "pin-project-lite", 990 | "tokio", 991 | "tokio-util", 992 | ] 993 | 994 | [[package]] 995 | name = "tokio-util" 996 | version = "0.7.11" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 999 | dependencies = [ 1000 | "bytes", 1001 | "futures-core", 1002 | "futures-sink", 1003 | "pin-project-lite", 1004 | "tokio", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "toml" 1009 | version = "0.8.19" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1012 | dependencies = [ 1013 | "serde", 1014 | "serde_spanned", 1015 | "toml_datetime", 1016 | "toml_edit", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "toml_datetime" 1021 | version = "0.6.8" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1024 | dependencies = [ 1025 | "serde", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "toml_edit" 1030 | version = "0.22.20" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 1033 | dependencies = [ 1034 | "indexmap", 1035 | "serde", 1036 | "serde_spanned", 1037 | "toml_datetime", 1038 | "winnow", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "unicode-ident" 1043 | version = "1.0.12" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1046 | 1047 | [[package]] 1048 | name = "utf8parse" 1049 | version = "0.2.1" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1052 | 1053 | [[package]] 1054 | name = "uuid" 1055 | version = "1.11.0" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" 1058 | 1059 | [[package]] 1060 | name = "walkdir" 1061 | version = "2.5.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1064 | dependencies = [ 1065 | "same-file", 1066 | "winapi-util", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "wasi" 1071 | version = "0.11.0+wasi-snapshot-preview1" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1074 | 1075 | [[package]] 1076 | name = "winapi" 1077 | version = "0.3.9" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1080 | dependencies = [ 1081 | "winapi-i686-pc-windows-gnu", 1082 | "winapi-x86_64-pc-windows-gnu", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "winapi-i686-pc-windows-gnu" 1087 | version = "0.4.0" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1090 | 1091 | [[package]] 1092 | name = "winapi-util" 1093 | version = "0.1.8" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 1096 | dependencies = [ 1097 | "windows-sys 0.52.0", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "winapi-x86_64-pc-windows-gnu" 1102 | version = "0.4.0" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1105 | 1106 | [[package]] 1107 | name = "windows" 1108 | version = "0.57.0" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 1111 | dependencies = [ 1112 | "windows-core", 1113 | "windows-targets 0.52.5", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "windows-core" 1118 | version = "0.57.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" 1121 | dependencies = [ 1122 | "windows-implement", 1123 | "windows-interface", 1124 | "windows-result", 1125 | "windows-targets 0.52.5", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "windows-implement" 1130 | version = "0.57.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" 1133 | dependencies = [ 1134 | "proc-macro2", 1135 | "quote", 1136 | "syn", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "windows-interface" 1141 | version = "0.57.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" 1144 | dependencies = [ 1145 | "proc-macro2", 1146 | "quote", 1147 | "syn", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "windows-result" 1152 | version = "0.1.2" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 1155 | dependencies = [ 1156 | "windows-targets 0.52.5", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "windows-sys" 1161 | version = "0.48.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1164 | dependencies = [ 1165 | "windows-targets 0.48.5", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "windows-sys" 1170 | version = "0.52.0" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1173 | dependencies = [ 1174 | "windows-targets 0.52.5", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "windows-targets" 1179 | version = "0.48.5" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1182 | dependencies = [ 1183 | "windows_aarch64_gnullvm 0.48.5", 1184 | "windows_aarch64_msvc 0.48.5", 1185 | "windows_i686_gnu 0.48.5", 1186 | "windows_i686_msvc 0.48.5", 1187 | "windows_x86_64_gnu 0.48.5", 1188 | "windows_x86_64_gnullvm 0.48.5", 1189 | "windows_x86_64_msvc 0.48.5", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "windows-targets" 1194 | version = "0.52.5" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1197 | dependencies = [ 1198 | "windows_aarch64_gnullvm 0.52.5", 1199 | "windows_aarch64_msvc 0.52.5", 1200 | "windows_i686_gnu 0.52.5", 1201 | "windows_i686_gnullvm", 1202 | "windows_i686_msvc 0.52.5", 1203 | "windows_x86_64_gnu 0.52.5", 1204 | "windows_x86_64_gnullvm 0.52.5", 1205 | "windows_x86_64_msvc 0.52.5", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "windows_aarch64_gnullvm" 1210 | version = "0.48.5" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1213 | 1214 | [[package]] 1215 | name = "windows_aarch64_gnullvm" 1216 | version = "0.52.5" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1219 | 1220 | [[package]] 1221 | name = "windows_aarch64_msvc" 1222 | version = "0.48.5" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1225 | 1226 | [[package]] 1227 | name = "windows_aarch64_msvc" 1228 | version = "0.52.5" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1231 | 1232 | [[package]] 1233 | name = "windows_i686_gnu" 1234 | version = "0.48.5" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1237 | 1238 | [[package]] 1239 | name = "windows_i686_gnu" 1240 | version = "0.52.5" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1243 | 1244 | [[package]] 1245 | name = "windows_i686_gnullvm" 1246 | version = "0.52.5" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1249 | 1250 | [[package]] 1251 | name = "windows_i686_msvc" 1252 | version = "0.48.5" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1255 | 1256 | [[package]] 1257 | name = "windows_i686_msvc" 1258 | version = "0.52.5" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1261 | 1262 | [[package]] 1263 | name = "windows_x86_64_gnu" 1264 | version = "0.48.5" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1267 | 1268 | [[package]] 1269 | name = "windows_x86_64_gnu" 1270 | version = "0.52.5" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1273 | 1274 | [[package]] 1275 | name = "windows_x86_64_gnullvm" 1276 | version = "0.48.5" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1279 | 1280 | [[package]] 1281 | name = "windows_x86_64_gnullvm" 1282 | version = "0.52.5" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1285 | 1286 | [[package]] 1287 | name = "windows_x86_64_msvc" 1288 | version = "0.48.5" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1291 | 1292 | [[package]] 1293 | name = "windows_x86_64_msvc" 1294 | version = "0.52.5" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1297 | 1298 | [[package]] 1299 | name = "winnow" 1300 | version = "0.6.18" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 1303 | dependencies = [ 1304 | "memchr", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "xml-rs" 1309 | version = "0.8.20" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" 1312 | 1313 | [[package]] 1314 | name = "zerocopy" 1315 | version = "0.8.14" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" 1318 | dependencies = [ 1319 | "zerocopy-derive", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "zerocopy-derive" 1324 | version = "0.8.14" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" 1327 | dependencies = [ 1328 | "proc-macro2", 1329 | "quote", 1330 | "syn", 1331 | ] 1332 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "badgemagic" 3 | version = "0.1.0" 4 | authors = ["Martin Michaelis "] 5 | edition = "2021" 6 | description = "Badge Magic with LEDs - Library and CLI" 7 | homepage = "https://badgemagic.fossasia.org" 8 | repository = "https://github.com/fossasia/badgemagic-rs" 9 | license = "MIT OR Apache-2.0" 10 | publish = false 11 | 12 | [[bin]] 13 | name = "badgemagic" 14 | required-features = ["cli"] 15 | 16 | [[example]] 17 | name = "hello-world" 18 | required-features = ["embedded-graphics", "usb-hid"] 19 | 20 | [features] 21 | default = ["embedded-graphics", "usb-hid"] 22 | 23 | cli = [ 24 | "embedded-graphics", 25 | "serde", 26 | "usb-hid", 27 | "ble", 28 | "dep:base64", 29 | "dep:clap", 30 | "dep:serde_json", 31 | "dep:toml", 32 | ] 33 | 34 | embedded-graphics = ["dep:embedded-graphics"] 35 | serde = ["dep:serde"] 36 | usb-hid = ["dep:hidapi"] 37 | ble = ["dep:btleplug", "dep:uuid", "dep:tokio"] 38 | 39 | [dependencies] 40 | anyhow = "1.0.95" 41 | base64 = { version = "0.22.1", optional = true } 42 | clap = { version = "4.5.23", features = ["derive"], optional = true } 43 | embedded-graphics = { version = "0.8.1", optional = true } 44 | hidapi = { version = "2.6.3", optional = true } 45 | btleplug = { version = "0.11.6", optional = true } 46 | uuid = { version = "1.11.0", optional = true } 47 | tokio = { version = "1.39.2", features = ["rt"], optional = true } 48 | serde = { version = "1.0.217", features = ["derive"], optional = true } 49 | serde_json = { version = "1.0.134", optional = true } 50 | time = "0.3.37" 51 | toml = { version = "0.8.19", optional = true } 52 | zerocopy = { version = "0.8.14", features = ["derive"] } 53 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2024 Martin Michaelis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](../../actions/workflows/ci.yaml/badge.svg)](../../actions/workflows/ci.yaml) 2 | 3 | # Badge Magic in Rust 4 | 5 | Library and CLI to configure LED badges. 6 | 7 | ## Installation 8 | 9 | As of now there are no proper releases (with version numbers) of this tool. 10 | 11 | The latest commit on the main branch just gets build and released automatically. 12 | 13 | Download the prebuild program for one of the following operating systems: 14 | 15 | - [Linux (GNU / 64 bit)](../../releases/latest/download/badgemagic.x86_64-unknown-linux-gnu) 16 | - [Windows (64 bit)](../../releases/latest/download/badgemagic.x86_64-pc-windows-msvc.exe) 17 | - [MacOS (Intel)](../../releases/latest/download/badgemagic.x86_64-apple-darwin) 18 | - [MacOS (M1, etc.)](../../releases/latest/download/badgemagic.aarch64-apple-darwin) 19 | 20 | ```sh 21 | # After the download rename the file to `badgemagic` 22 | mv badgemagic. badgemagic 23 | 24 | # Make the program executable (linux / macOS only) 25 | chmod +x badgemagic 26 | 27 | # Test that it works 28 | ./badgemagic --help 29 | ``` 30 | 31 | > 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. 32 | 33 | 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: 34 | 35 | ```sh 36 | cargo install --git https://github.com/fossasia/badgemagic-rs --features cli 37 | badgemagic --help 38 | ``` 39 | 40 | Or clone the repo and run the CLI: 41 | ```sh 42 | git clone https://github.com/fossasia/badgemagic-rs 43 | cd badgemagic-rs 44 | cargo run --features cli -- --help 45 | ``` 46 | 47 | ## Usage 48 | 49 | Execute the `badgemagic` tool and pass the file name of your configuration file alongside the mode of transport (USB or Bluetooth Low Energy). 50 | Depending on how you installed the tool: 51 | 52 | ```sh 53 | # Downloaded from release page 54 | ./badgemagic config.toml 55 | 56 | # Installed with cargo install 57 | badgemagic config.toml 58 | 59 | # Run from git repository 60 | cargo run --features cli -- config.toml 61 | ``` 62 | 63 | The above command will read your configuration from a file named `config.toml` in the current directory. 64 | The transport mode can be either `--transport usb` or `--transport ble` for transferring the message via Bluetooth Low Energy. 65 | Usage of BLE on macOS requires special permissions, which is explained in more detail [here](https://github.com/deviceplug/btleplug#macos). 66 | 67 | ## Configuration 68 | 69 | You can have a look at the example configurations in the [`demo` directory](demo). 70 | 71 | The TOML configuration consists of up to 8 message sections starting with `[[message]]`. 72 | 73 | Each message can have the following options: 74 | ```toml 75 | [[message]] 76 | # Enable blink mode 77 | blink = true 78 | 79 | # Show a dotted border arround the display 80 | border = true 81 | 82 | # Set the update speed of the animations (0 to 7) 83 | speed = 6 84 | 85 | # Set the display animation (left, right, up, down, center, fast, drop, curtain, laser) 86 | mode = "left" 87 | 88 | # The text to show on the display 89 | text = "Lorem ipsum dolor sit amet." 90 | ``` 91 | 92 | You can omit options you don't need: 93 | ```toml 94 | [[message]] 95 | mode = "center" 96 | text = "Hello" 97 | ``` 98 | 99 | If you want you can "draw" images as ASCII art (`_` = Off, `X` = On): 100 | ```toml 101 | [[message]] 102 | mode = "center" 103 | bitstring = """ 104 | ___XXXXX___ 105 | __X_____X__ 106 | _X_______X_ 107 | X__XX_XX__X 108 | X__XX_XX__X 109 | X_________X 110 | X_XX___XX_X 111 | X__XXXXX__X 112 | _X__XXX__X_ 113 | __X_____X__ 114 | ___XXXXX___ 115 | """ 116 | ``` 117 | 118 | 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. 119 | 120 | ## License 121 | 122 | Licensed under either of 123 | 124 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 125 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 126 | 127 | at your option. 128 | 129 | ### Contribution 130 | 131 | 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. 132 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "cli")] 3 | cli::generate_version_info(); 4 | } 5 | 6 | #[cfg(feature = "cli")] 7 | mod cli { 8 | use std::{env, fs, path::PathBuf, process::Command, str}; 9 | 10 | pub fn generate_version_info() { 11 | let pkg_version = env::var("CARGO_PKG_VERSION").expect("missing package version"); 12 | let git_version = git_version(); 13 | let git_prefix = git_version 14 | .is_some() 15 | .then_some("commit-") 16 | .unwrap_or_default(); 17 | let git_version = git_version.as_deref().unwrap_or("unknown"); 18 | let version = format!("{pkg_version}-git.{git_prefix}{git_version}"); 19 | 20 | let out: PathBuf = env::var_os("OUT_DIR").expect("build output path").into(); 21 | fs::write( 22 | out.join("cli.rs"), 23 | format!("pub const VERSION: &str = {version:?};\n"), 24 | ) 25 | .expect("write cli.rs"); 26 | } 27 | 28 | fn git_version() -> Option { 29 | let output = Command::new("git") 30 | .arg("describe") 31 | .arg("--always") 32 | .arg("--dirty=-modified") 33 | .output() 34 | .ok()?; 35 | if output.status.success() { 36 | Some(str::from_utf8(&output.stdout).ok()?.trim().into()) 37 | } else { 38 | None 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demo/effects.toml: -------------------------------------------------------------------------------- 1 | [[message]] 2 | blink = true 3 | border = true 4 | mode = "center" 5 | text = "Hello" 6 | 7 | [[message]] 8 | mode = "laser" 9 | text = "Laser" 10 | 11 | [[message]] 12 | mode = "drop" 13 | text = "Drop" 14 | 15 | [[message]] 16 | mode = "curtain" 17 | text = "Curtain" 18 | 19 | [[message]] 20 | mode = "left" 21 | text = "Move message left" 22 | 23 | [[message]] 24 | mode = "up" 25 | text = "Move up" 26 | 27 | [[message]] 28 | blink = true 29 | border = true 30 | speed = 6 31 | mode = "left" 32 | text = "Combine them all and add some speed... might be a bit overwhelming" 33 | -------------------------------------------------------------------------------- /demo/jump.toml: -------------------------------------------------------------------------------- 1 | [[message]] 2 | speed = 6 3 | mode = "fast" 4 | bitstring = """ 5 | X__________________________________________X____X__________________________________________X____X___________________XXXX___________________X____X__________________________________________X 6 | _X________________________________________X______X__________________XXXX__________________X______X_________________X____X_________________X______X__________________XXXX__________________X_ 7 | __X_________________XXXX_________________X________X________________X____X________________X________X_____________X__X____X__X_____________X________X________________X____X________________X__ 8 | ___________________X____X__________________________________________X____X________________________________________X__XXXX__X________________________________________X____X___________________ 9 | ___________________X____X___________________________________________XXXX__________________________________________XXXXXXXX__________________________________________XXXX____________________ 10 | ____________________XXXX_______________________________________XXXXXXXXXXXXXX________________________________________X_________________________________________XXXXXXXXXXXXXX_______________ 11 | __________________XXXXXXXX___________________________________________X______________________________________________X_X______________________________________________X______________________ 12 | _________________X___X____X_________________________________________X_X____________________________________________X___X____________________________________________X_X_____________________ 13 | __X_____________X___X_X____X_____________X________X________________X___X_________________X________X_______________X_____X________________X________X________________X___X_________________X__ 14 | _X_________________X___X__________________X______X________________X_____X_________________X______X________________________________________X______X________________X_____X_________________X_ 15 | X_________________X_____X__________________X____X__________________________________________X____X__________________________________________X____X__________________________________________X 16 | """ 17 | -------------------------------------------------------------------------------- /demo/smiley.toml: -------------------------------------------------------------------------------- 1 | [[message]] 2 | mode = "center" 3 | bitstring = """ 4 | ___XXXXX___ 5 | __X_____X__ 6 | _X_______X_ 7 | X__XX_XX__X 8 | X__XX_XX__X 9 | X_________X 10 | X_XX___XX_X 11 | X__XXXXX__X 12 | _X__XXX__X_ 13 | __X_____X__ 14 | ___XXXXX___ 15 | """ 16 | -------------------------------------------------------------------------------- /examples/hello-world.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic)] 2 | 3 | use anyhow::Result; 4 | use badgemagic::{ 5 | embedded_graphics::{ 6 | geometry::Point, mono_font::MonoTextStyle, pixelcolor::BinaryColor, text::Text, 7 | }, 8 | protocol::{Mode, PayloadBuffer, Style}, 9 | usb_hid::Device, 10 | util::DrawableLayoutExt, 11 | }; 12 | 13 | fn main() -> Result<()> { 14 | let mut payload = PayloadBuffer::new(); 15 | 16 | payload.add_message_drawable( 17 | Style::default().mode(Mode::Center), 18 | &Text::new( 19 | "Hello", 20 | Point::new(0, 8), 21 | MonoTextStyle::new( 22 | &embedded_graphics::mono_font::iso_8859_1::FONT_6X9, 23 | BinaryColor::On, 24 | ), 25 | ), 26 | ); 27 | 28 | payload.add_message_drawable( 29 | Style::default().mode(Mode::Center), 30 | &Text::new( 31 | "Hello", 32 | Point::new(0, 5), 33 | MonoTextStyle::new( 34 | &embedded_graphics::mono_font::iso_8859_1::FONT_4X6, 35 | BinaryColor::On, 36 | ), 37 | ) 38 | .z_stack(Text::new( 39 | "World", 40 | Point::new(23, 8), 41 | MonoTextStyle::new( 42 | &embedded_graphics::mono_font::iso_8859_1::FONT_4X6, 43 | BinaryColor::On, 44 | ), 45 | )), 46 | ); 47 | 48 | Device::single()?.write(payload)?; 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true 2 | imports_granularity = "Crate" 3 | use_field_init_shorthand = true 4 | -------------------------------------------------------------------------------- /src/ble.rs: -------------------------------------------------------------------------------- 1 | //! Connect to an LED badge via Bluetooth Low Energy (BLE) 2 | 3 | use std::time::Duration; 4 | 5 | use anyhow::{Context, Result}; 6 | use btleplug::{ 7 | api::{bleuuid, Central as _, Manager as _, Peripheral as _, ScanFilter, WriteType}, 8 | platform::{Manager, Peripheral}, 9 | }; 10 | use tokio::time; 11 | use uuid::Uuid; 12 | 13 | use crate::protocol::PayloadBuffer; 14 | 15 | /// `0000fee0-0000-1000-8000-00805f9b34fb` 16 | const BADGE_SERVICE_UUID: Uuid = bleuuid::uuid_from_u16(0xfee0); 17 | /// `0000fee1-0000-1000-8000-00805f9b34fb` 18 | const BADGE_CHAR_UUID: Uuid = bleuuid::uuid_from_u16(0xfee1); 19 | 20 | const BADGE_BLE_DEVICE_NAME: &str = "LSLED"; 21 | const BLE_CHAR_CHUNK_SIZE: usize = 16; 22 | 23 | /// A discovered BLE device 24 | pub struct Device { 25 | peripheral: Peripheral, 26 | } 27 | 28 | impl Device { 29 | /// Return a list of all BLE devies as a string representation. 30 | pub async fn list_all() -> Result> { 31 | // Run device scan 32 | let manager = Manager::new().await.context("create BLE manager")?; 33 | let adapters = manager 34 | .adapters() 35 | .await 36 | .context("enumerate bluetooth adapters")?; 37 | let adapter = adapters.first().context("no bluetooth adapter found")?; 38 | 39 | adapter 40 | .start_scan(ScanFilter { 41 | // don't filter by service 42 | services: Vec::new(), 43 | }) 44 | .await 45 | .context("bluetooth scan start")?; 46 | time::sleep(Duration::from_secs(2)).await; 47 | 48 | let mut devices = Vec::new(); 49 | for peripheral in adapter 50 | .peripherals() 51 | .await 52 | .context("enumerating bluetooth devices")? 53 | { 54 | let device = async { 55 | let props = peripheral 56 | .properties() 57 | .await? 58 | .context("missing device info")?; 59 | 60 | Ok(format!( 61 | "{}: name={:?} services={:?}", 62 | props.address, props.local_name, props.services 63 | )) 64 | }; 65 | devices.push(device.await.unwrap_or_else(|err: anyhow::Error| { 66 | format!("{} failed to collect info: {err:?}", peripheral.address()) 67 | })); 68 | } 69 | 70 | Ok(devices) 71 | } 72 | 73 | /// Return all supported devices that are found in two seconds. 74 | /// 75 | /// Returns all badges that are in BLE range and are in Bluetooth transfer mode. 76 | pub async fn enumerate() -> Result> { 77 | Self::enumerate_duration(Duration::from_secs(2)).await 78 | } 79 | 80 | /// Return all supported devices that are found in the given duration. 81 | /// 82 | /// Returns all badges that are in BLE range and are in Bluetooth transfer mode. 83 | /// # Panics 84 | /// This function panics if it is unable to access the Bluetooth adapter. 85 | pub async fn enumerate_duration(scan_duration: Duration) -> Result> { 86 | // Run device scan 87 | let manager = Manager::new().await.context("create BLE manager")?; 88 | let adapters = manager 89 | .adapters() 90 | .await 91 | .context("enumerate bluetooth adapters")?; 92 | let adapter = adapters.first().context("no bluetooth adapter found")?; 93 | 94 | adapter 95 | .start_scan(ScanFilter { 96 | services: vec![BADGE_SERVICE_UUID], 97 | }) 98 | .await 99 | .context("bluetooth scan start")?; 100 | time::sleep(scan_duration).await; 101 | 102 | // Filter for badge devices 103 | let mut led_badges = vec![]; 104 | for p in adapter 105 | .peripherals() 106 | .await 107 | .context("enumerating bluetooth devices")? 108 | { 109 | if let Some(badge) = Self::from_peripheral(p).await { 110 | led_badges.push(badge); 111 | } 112 | } 113 | 114 | Ok(led_badges) 115 | } 116 | 117 | async fn from_peripheral(peripheral: Peripheral) -> Option { 118 | // The existance of the service with the correct UUID 119 | // exists is already checked by the scan filter. 120 | // But we also need to check the device name to make sure 121 | // we're talking to a badge as some devices that are not led badges 122 | // also use the same service UUID. 123 | let props = peripheral.properties().await.ok()??; 124 | let local_name = props.local_name.as_ref()?; 125 | 126 | if local_name == BADGE_BLE_DEVICE_NAME { 127 | Some(Self { peripheral }) 128 | } else { 129 | None 130 | } 131 | } 132 | 133 | /// Return the single supported device 134 | /// 135 | /// This function returns an error if no device could be found 136 | /// or if multiple devices would match. 137 | pub async fn single() -> Result { 138 | let mut devices = Self::enumerate() 139 | .await 140 | .context("enumerating badges")? 141 | .into_iter(); 142 | let device = devices.next().context("no device found")?; 143 | anyhow::ensure!(devices.next().is_none(), "multiple devices found"); 144 | Ok(device) 145 | } 146 | 147 | /// Write a payload to the device. 148 | /// 149 | /// This function connects to the device, writes the payload and disconnects. 150 | /// When the device went out of range between discovering it 151 | /// and writing the payload, an error is returned. 152 | /// # Panics 153 | /// This functions panics if the BLE device does not have the expected badge characteristic. 154 | pub async fn write(&self, payload: PayloadBuffer) -> Result<()> { 155 | self.peripheral 156 | .connect() 157 | .await 158 | .context("bluetooth device connect")?; 159 | 160 | let result = self.write_connected(payload).await; 161 | let disconnect_result = self.peripheral.disconnect().await; 162 | 163 | if result.is_ok() { 164 | // Write succesful, return disconnect result 165 | Ok(disconnect_result?) 166 | } else { 167 | // Write failed, return write result and ignore disconnect result 168 | result 169 | } 170 | } 171 | 172 | async fn write_connected(&self, payload: PayloadBuffer) -> Result<()> { 173 | // Get characteristic 174 | self.peripheral 175 | .discover_services() 176 | .await 177 | .context("discovering services")?; 178 | let characteristics = self.peripheral.characteristics(); 179 | let badge_char = characteristics 180 | .iter() 181 | .find(|c| c.uuid == BADGE_CHAR_UUID) 182 | .context("badge characteristic not found")?; 183 | 184 | // Write payload 185 | let bytes = payload.into_padded_bytes(); 186 | let data = bytes.as_ref(); 187 | 188 | anyhow::ensure!( 189 | data.len() % BLE_CHAR_CHUNK_SIZE == 0, 190 | "Payload size must be a multiple of {} bytes", 191 | BLE_CHAR_CHUNK_SIZE 192 | ); 193 | 194 | // the device will brick itself if the payload is too long (more then 8192 bytes) 195 | anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)"); 196 | 197 | for chunk in data.chunks(BLE_CHAR_CHUNK_SIZE) { 198 | self.peripheral 199 | .write(badge_char, chunk, WriteType::WithoutResponse) 200 | .await 201 | .context("writing payload chunk")?; 202 | } 203 | 204 | Ok(()) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic)] 2 | #![allow(clippy::missing_errors_doc)] 3 | 4 | pub mod protocol; 5 | 6 | #[cfg(feature = "usb-hid")] 7 | pub mod usb_hid; 8 | 9 | #[cfg(feature = "ble")] 10 | pub mod ble; 11 | 12 | #[cfg(feature = "embedded-graphics")] 13 | pub mod util; 14 | 15 | #[cfg(feature = "embedded-graphics")] 16 | pub use embedded_graphics; 17 | 18 | #[cfg(feature = "cli")] 19 | #[doc(hidden)] 20 | pub mod cli { 21 | include!(concat!(env!("OUT_DIR"), "/cli.rs")); 22 | } 23 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic)] 2 | 3 | use std::{fs, path::PathBuf}; 4 | 5 | use anyhow::{Context, Result}; 6 | use badgemagic::{ 7 | ble::Device as BleDevice, 8 | protocol::{Mode, PayloadBuffer, Speed, Style}, 9 | usb_hid::Device as UsbDevice, 10 | }; 11 | use base64::Engine; 12 | use clap::{Parser, ValueEnum}; 13 | use embedded_graphics::{ 14 | geometry::Point, 15 | image::{Image, ImageRawLE}, 16 | mono_font::{iso_8859_1::FONT_6X9, MonoTextStyle}, 17 | pixelcolor::BinaryColor, 18 | text::Text, 19 | Drawable, Pixel, 20 | }; 21 | use serde::Deserialize; 22 | 23 | #[derive(Parser)] 24 | /// Upload a configuration with up to 8 messages to an LED badge 25 | #[clap( 26 | version = badgemagic::cli::VERSION, 27 | author, 28 | help_template = "\ 29 | {before-help}{name} {version} 30 | {author-with-newline} 31 | {about-with-newline} 32 | {usage-heading} {usage} 33 | 34 | {all-args}{after-help} 35 | ", 36 | )] 37 | struct Args { 38 | /// File format of the config file (toml, json) 39 | #[clap(long)] 40 | format: Option, 41 | 42 | /// Transport protocol to use 43 | #[clap(long)] 44 | transport: TransportProtocol, 45 | 46 | /// List all devices visible to a transport and exit 47 | #[clap(long)] 48 | list_devices: bool, 49 | 50 | /// Path to TOML configuration file 51 | #[clap(required_unless_present = "list_devices")] 52 | config: Option, 53 | } 54 | 55 | #[derive(Clone, Deserialize, ValueEnum)] 56 | #[serde(rename_all = "kebab-case")] 57 | enum TransportProtocol { 58 | Usb, 59 | Ble, 60 | } 61 | 62 | #[derive(Deserialize)] 63 | #[serde(deny_unknown_fields)] 64 | struct Config { 65 | #[serde(rename = "message")] 66 | messages: Vec, 67 | } 68 | 69 | #[derive(Deserialize)] 70 | struct Message { 71 | #[serde(default)] 72 | blink: bool, 73 | 74 | #[serde(default)] 75 | border: bool, 76 | 77 | #[serde(default)] 78 | speed: Speed, 79 | 80 | #[serde(default)] 81 | mode: Mode, 82 | 83 | #[serde(flatten)] 84 | content: Content, 85 | } 86 | 87 | #[derive(Deserialize)] 88 | #[serde(deny_unknown_fields, untagged)] 89 | enum Content { 90 | Text { text: String }, 91 | Bitstring { bitstring: String }, 92 | BitmapBase64 { width: u32, bitmap_base64: String }, 93 | BitmapFile { width: u32, bitmap_file: PathBuf }, 94 | // TODO: implement png 95 | // PngFile { png_file: PathBuf }, 96 | } 97 | 98 | fn main() -> Result<()> { 99 | let mut args = Args::parse(); 100 | 101 | if args.list_devices { 102 | return list_devices(&args.transport); 103 | } 104 | 105 | let payload = gnerate_payload(&mut args)?; 106 | 107 | write_payload(&args.transport, payload) 108 | } 109 | 110 | fn list_devices(transport: &TransportProtocol) -> Result<()> { 111 | let devices = match transport { 112 | TransportProtocol::Usb => UsbDevice::list_all(), 113 | TransportProtocol::Ble => tokio::runtime::Builder::new_current_thread() 114 | .enable_all() 115 | .build()? 116 | .block_on(async { BleDevice::list_all().await }), 117 | }?; 118 | 119 | eprintln!( 120 | "found {} {} devices", 121 | devices.len(), 122 | transport.to_possible_value().unwrap().get_name(), 123 | ); 124 | for device in devices { 125 | println!("- {device}"); 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | fn gnerate_payload(args: &mut Args) -> Result { 132 | let config_path = args.config.take().unwrap_or_default(); 133 | let config = fs::read_to_string(&config_path) 134 | .with_context(|| format!("load config: {config_path:?}"))?; 135 | let config: Config = { 136 | let extension = args 137 | .format 138 | .as_deref() 139 | .map(AsRef::as_ref) 140 | .or(config_path.extension()) 141 | .context("missing file extension for config file")?; 142 | match extension.to_str().unwrap_or_default() { 143 | "json" => serde_json::from_str(&config).context("parse config")?, 144 | "toml" => toml::from_str(&config).context("parse config")?, 145 | _ => anyhow::bail!("unsupported config file extension: {extension:?}"), 146 | } 147 | }; 148 | 149 | let mut payload = PayloadBuffer::new(); 150 | 151 | for message in config.messages { 152 | let mut style = Style::default(); 153 | if message.blink { 154 | style = style.blink(); 155 | } 156 | if message.border { 157 | style = style.border(); 158 | } 159 | style = style.speed(message.speed).mode(message.mode); 160 | match message.content { 161 | Content::Text { text } => { 162 | let text = Text::new( 163 | &text, 164 | Point::new(0, 7), 165 | MonoTextStyle::new(&FONT_6X9, BinaryColor::On), 166 | ); 167 | payload.add_message_drawable(style, &text); 168 | } 169 | Content::Bitstring { bitstring } => { 170 | let lines: Vec<_> = bitstring.trim().lines().collect(); 171 | 172 | anyhow::ensure!( 173 | lines.len() == 11, 174 | "expected 11 lines in bitstring, found {} lines", 175 | lines.len() 176 | ); 177 | let width = lines[0].len(); 178 | if lines.iter().any(|l| l.len() != width) { 179 | anyhow::bail!( 180 | "lines should have the same length, got: {:?}", 181 | lines.iter().map(|l| l.len()).collect::>() 182 | ); 183 | } 184 | let mut buffer = payload.add_message(style, (width + 7) / 8); 185 | 186 | for (y, line) in lines.iter().enumerate() { 187 | for (x, c) in line.chars().enumerate() { 188 | match c { 189 | '_' => { 190 | // off 191 | } 192 | 'X' => { 193 | Pixel( 194 | Point::new(x.try_into().unwrap(), y.try_into().unwrap()), 195 | BinaryColor::On, 196 | ) 197 | .draw(&mut buffer) 198 | .unwrap(); 199 | } 200 | _ => anyhow::bail!("invalid bit value for bit ({x}, {y}): {c:?}"), 201 | } 202 | } 203 | } 204 | } 205 | Content::BitmapBase64 { 206 | width, 207 | bitmap_base64: bitmap, 208 | } => { 209 | let data = if bitmap.ends_with('=') { 210 | base64::engine::general_purpose::STANDARD 211 | } else { 212 | base64::engine::general_purpose::STANDARD_NO_PAD 213 | } 214 | .decode(bitmap) 215 | .context("decode bitmap")?; 216 | let image_raw = ImageRawLE::::new(&data, width); 217 | let image = Image::new(&image_raw, Point::zero()); 218 | payload.add_message_drawable(style, &image); 219 | } 220 | Content::BitmapFile { width, bitmap_file } => { 221 | let data = fs::read(bitmap_file).context("load bitmap")?; 222 | let image_raw = ImageRawLE::::new(&data, width); 223 | let image = Image::new(&image_raw, Point::zero()); 224 | payload.add_message_drawable(style, &image); 225 | } 226 | } 227 | } 228 | 229 | Ok(payload) 230 | } 231 | 232 | fn write_payload( 233 | transport: &TransportProtocol, 234 | payload: PayloadBuffer, 235 | ) -> Result<(), anyhow::Error> { 236 | match transport { 237 | TransportProtocol::Usb => UsbDevice::single()?.write(payload), 238 | TransportProtocol::Ble => tokio::runtime::Builder::new_current_thread() 239 | .enable_all() 240 | .build()? 241 | .block_on(async { BleDevice::single().await?.write(payload).await }), 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | //! Protocol used to update the badge 2 | 3 | use std::num::TryFromIntError; 4 | 5 | #[cfg(feature = "embedded-graphics")] 6 | use embedded_graphics::{ 7 | draw_target::DrawTarget, 8 | geometry::{Dimensions, Point, Size}, 9 | pixelcolor::BinaryColor, 10 | prelude::Pixel, 11 | primitives::Rectangle, 12 | Drawable, 13 | }; 14 | use time::OffsetDateTime; 15 | use zerocopy::{BigEndian, FromBytes, Immutable, IntoBytes, KnownLayout, U16}; 16 | 17 | /// Message style configuration 18 | /// ``` 19 | /// use badgemagic::protocol::{Mode, Style}; 20 | /// # ( 21 | /// Style::default().blink().border().mode(Mode::Center) 22 | /// # ); 23 | /// ``` 24 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 25 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 26 | #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] 27 | #[must_use] 28 | pub struct Style { 29 | #[cfg_attr(feature = "serde", serde(default))] 30 | blink: bool, 31 | 32 | #[cfg_attr(feature = "serde", serde(default))] 33 | border: bool, 34 | 35 | #[cfg_attr(feature = "serde", serde(default))] 36 | speed: Speed, 37 | 38 | #[cfg_attr(feature = "serde", serde(default))] 39 | mode: Mode, 40 | } 41 | 42 | impl Style { 43 | /// Enable blink mode 44 | /// 45 | /// The message will blink. 46 | /// ``` 47 | /// use badgemagic::protocol::Style; 48 | /// # ( 49 | /// Style::default().blink() 50 | /// # ); 51 | /// ``` 52 | pub fn blink(mut self) -> Self { 53 | self.blink = true; 54 | self 55 | } 56 | 57 | /// Show a dotted border arround the display. 58 | /// ``` 59 | /// use badgemagic::protocol::Style; 60 | /// # ( 61 | /// Style::default().blink() 62 | /// # ); 63 | /// ``` 64 | pub fn border(mut self) -> Self { 65 | self.border = true; 66 | self 67 | } 68 | 69 | /// Set the update speed of the animations. 70 | /// 71 | /// The animation will jump to the next pixel at the specified frame rate. 72 | /// ``` 73 | /// use badgemagic::protocol::{Speed, Style}; 74 | /// # ( 75 | /// Style::default().speed(Speed::Fps1_2) 76 | /// # ); 77 | /// ``` 78 | pub fn speed(mut self, speed: Speed) -> Self { 79 | self.speed = speed; 80 | self 81 | } 82 | 83 | /// Set the display animation. 84 | /// ``` 85 | /// use badgemagic::protocol::{Mode, Style}; 86 | /// # ( 87 | /// Style::default().mode(Mode::Curtain) 88 | /// # ); 89 | /// ``` 90 | /// 91 | /// Show text centered, without an animation: 92 | /// ``` 93 | /// use badgemagic::protocol::{Mode, Style}; 94 | /// # ( 95 | /// Style::default().mode(Mode::Center) 96 | /// # ); 97 | /// ``` 98 | pub fn mode(mut self, mode: Mode) -> Self { 99 | self.mode = mode; 100 | self 101 | } 102 | } 103 | 104 | /// Animation update speed 105 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 106 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 107 | #[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))] 108 | pub enum Speed { 109 | /// 1.2 FPS 110 | Fps1_2, 111 | 112 | /// 1.3 FPS 113 | Fps1_3, 114 | 115 | /// 2 FPS 116 | Fps2, 117 | 118 | /// 2.4 FPS 119 | Fps2_4, 120 | 121 | /// 2.8 FPS 122 | #[default] 123 | Fps2_8, 124 | 125 | /// 4.5 FPS 126 | Fps4_5, 127 | 128 | /// 7.5 FPS 129 | Fps7_5, 130 | 131 | /// 15 FPS 132 | Fps15, 133 | } 134 | 135 | impl From for u8 { 136 | fn from(value: Speed) -> Self { 137 | value as u8 138 | } 139 | } 140 | 141 | impl TryFrom for Speed { 142 | type Error = TryFromIntError; 143 | 144 | fn try_from(value: u8) -> Result { 145 | Ok(match value { 146 | 0 => Self::Fps1_2, 147 | 1 => Self::Fps1_3, 148 | 2 => Self::Fps2, 149 | 3 => Self::Fps2_4, 150 | 4 => Self::Fps2_8, 151 | 5 => Self::Fps4_5, 152 | 6 => Self::Fps7_5, 153 | 7 => Self::Fps15, 154 | _ => return Err(u8::try_from(-1).unwrap_err()), 155 | }) 156 | } 157 | } 158 | 159 | /// Message display mode 160 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 161 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 162 | #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] 163 | pub enum Mode { 164 | /// Scroll thorugh the message from left to right 165 | #[default] 166 | Left, 167 | 168 | /// Scroll through the message from right to left 169 | Right, 170 | 171 | /// Enter from the bottom, move up 172 | Up, 173 | 174 | /// Enter from the top, move down 175 | Down, 176 | 177 | /// Center the text, no animation 178 | Center, 179 | 180 | /// Fast mode for animations 181 | /// 182 | /// Will leave a 4 pixel gap between screens: 183 | /// Place a 44x11 pixel screen every 48 pixels 184 | Fast, 185 | 186 | /// Drop rows of pixels from the top 187 | Drop, 188 | 189 | /// Open a curtain and reveal the message 190 | Curtain, 191 | 192 | /// A laser will reveal the message from left to right 193 | Laser, 194 | } 195 | 196 | const MSG_PADDING_ALIGN: usize = 64; 197 | 198 | const MAGIC: [u8; 6] = *b"wang\0\0"; 199 | 200 | #[derive(FromBytes, IntoBytes, Immutable, KnownLayout)] 201 | #[repr(C)] 202 | struct Header { 203 | magic: [u8; 6], 204 | blink: u8, 205 | border: u8, 206 | speed_and_mode: [u8; 8], 207 | message_length: [U16; 8], 208 | _padding_1: [u8; 6], 209 | timestamp: Timestamp, 210 | _padding_2: [u8; 20], 211 | } 212 | 213 | #[derive(FromBytes, IntoBytes, Immutable, KnownLayout)] 214 | #[repr(C)] 215 | struct Timestamp { 216 | year: u8, 217 | month: u8, 218 | day: u8, 219 | hour: u8, 220 | minute: u8, 221 | second: u8, 222 | } 223 | 224 | impl Timestamp { 225 | fn new(ts: OffsetDateTime) -> Self { 226 | Self { 227 | #[allow(clippy::cast_possible_truncation)] // clippy does not understand `rem_euclid(100) <= 100` 228 | year: ts.year().rem_euclid(100) as u8, 229 | month: ts.month() as u8, 230 | day: ts.day(), 231 | hour: ts.hour(), 232 | minute: ts.minute(), 233 | second: ts.second(), 234 | } 235 | } 236 | 237 | fn now() -> Self { 238 | Self::new(OffsetDateTime::now_utc()) 239 | } 240 | } 241 | 242 | /// Buffer to create a payload 243 | /// 244 | /// A payload consits of up to 8 messages 245 | /// ``` 246 | /// # #[cfg(feature = "embedded-graphics")] 247 | /// # fn main() { 248 | /// # use badgemagic::protocol::{PayloadBuffer, Style}; 249 | /// use badgemagic::embedded_graphics::{ 250 | /// geometry::{Point, Size}, 251 | /// pixelcolor::BinaryColor, 252 | /// primitives::{PrimitiveStyle, Rectangle, Styled}, 253 | /// }; 254 | /// 255 | /// let mut buffer = PayloadBuffer::new(); 256 | /// buffer.add_message_drawable( 257 | /// Style::default(), 258 | /// &Styled::new( 259 | /// Rectangle::new(Point::new(2, 2), Size::new(4, 7)), 260 | /// PrimitiveStyle::with_fill(BinaryColor::On), 261 | /// ), 262 | /// ); 263 | /// # } 264 | /// # #[cfg(not(feature = "embedded-graphics"))] 265 | /// # fn main() {} 266 | /// ``` 267 | pub struct PayloadBuffer { 268 | num_messages: u8, 269 | data: Vec, 270 | } 271 | 272 | impl Default for PayloadBuffer { 273 | fn default() -> Self { 274 | Self::new() 275 | } 276 | } 277 | 278 | impl PayloadBuffer { 279 | /// Create a new empty buffer 280 | #[must_use] 281 | pub fn new() -> Self { 282 | Self { 283 | num_messages: 0, 284 | data: Header { 285 | magic: MAGIC, 286 | blink: 0, 287 | border: 0, 288 | speed_and_mode: [0; 8], 289 | message_length: [0.into(); 8], 290 | _padding_1: [0; 6], 291 | timestamp: Timestamp::now(), 292 | _padding_2: [0; 20], 293 | } 294 | .as_bytes() 295 | .into(), 296 | } 297 | } 298 | 299 | fn header_mut(&mut self) -> &mut Header { 300 | Header::mut_from_prefix(&mut self.data).unwrap().0 301 | } 302 | 303 | /// Return the current number of messages 304 | pub fn num_messages(&mut self) -> usize { 305 | self.num_messages as usize 306 | } 307 | 308 | /// Add a messages containing the specified `content` 309 | /// 310 | /// ## Panics 311 | /// This method panics if it is unable to draw the content. 312 | #[cfg(feature = "embedded-graphics")] 313 | pub fn add_message_drawable( 314 | &mut self, 315 | style: Style, 316 | content: &(impl Drawable + Dimensions), 317 | ) -> O { 318 | #[allow(clippy::cast_possible_wrap)] 319 | fn saturating_usize_to_isize(n: usize) -> isize { 320 | usize::min(n, isize::MAX as usize) as isize 321 | } 322 | 323 | fn add(a: i32, b: u32) -> usize { 324 | let result = a as isize + saturating_usize_to_isize(b as usize); 325 | result.try_into().unwrap_or_default() 326 | } 327 | 328 | let bounds = content.bounding_box(); 329 | let width = add(bounds.top_left.x, bounds.size.width); 330 | let mut message = self.add_message(style, (width + 7) / 8); 331 | content.draw(&mut message).unwrap() 332 | } 333 | 334 | /// Add a message with `count * 8` columns 335 | /// 336 | /// The returned `MessageBuffer` can be used as an `embedded_graphics::DrawTarget` 337 | /// with the `embedded_graphics` feature. 338 | /// 339 | /// ## Panics 340 | /// Panics if the supported number of messages is reached. 341 | pub fn add_message(&mut self, style: Style, count: usize) -> MessageBuffer { 342 | let index = self.num_messages as usize; 343 | assert!( 344 | index < 8, 345 | "maximum number of supported messages reached: {index} messages", 346 | ); 347 | self.num_messages += 1; 348 | 349 | let header = self.header_mut(); 350 | 351 | if style.blink { 352 | header.blink |= 1 << index; 353 | } 354 | if style.border { 355 | header.border |= 1 << index; 356 | } 357 | header.speed_and_mode[index] = ((style.speed as u8) << 4) | style.mode as u8; 358 | header.message_length[index] = count.try_into().unwrap(); 359 | 360 | let start = self.data.len(); 361 | self.data.resize(start + count * 11, 0); 362 | MessageBuffer(FromBytes::mut_from_bytes(&mut self.data[start..]).unwrap()) 363 | } 364 | 365 | /// Get the current payload as bytes (without padding) 366 | #[must_use] 367 | pub fn as_bytes(&self) -> &[u8] { 368 | &self.data 369 | } 370 | 371 | /// Convert the payload buffe into bytes (with padding) 372 | #[allow(clippy::missing_panics_doc)] // should never panic 373 | #[must_use] 374 | pub fn into_padded_bytes(self) -> impl AsRef<[u8]> { 375 | let mut data = self.data; 376 | 377 | let prev_len = data.len(); 378 | 379 | // pad msg to align to 64 bytes 380 | data.resize( 381 | (data.len() + (MSG_PADDING_ALIGN - 1)) & !(MSG_PADDING_ALIGN - 1), 382 | 0, 383 | ); 384 | 385 | // validate alignment 386 | assert_eq!(data.len() % 64, 0); 387 | assert!(prev_len <= data.len()); 388 | 389 | data 390 | } 391 | } 392 | 393 | /// A display buffer for a single message. 394 | /// 395 | /// Can be used as an `embedded_graphics::DrawTarget`. 396 | pub struct MessageBuffer<'a>(&'a mut [[u8; 11]]); 397 | 398 | impl MessageBuffer<'_> { 399 | /// Set the state of the pixel at point (`x`, `y`) 400 | /// 401 | /// Returns `None` if the pixel was out of bounds. 402 | pub fn set(&mut self, (x, y): (usize, usize), state: State) -> Option<()> { 403 | let byte = self.0.get_mut(x / 8)?.get_mut(y)?; 404 | let bit = 0x80 >> (x % 8); 405 | match state { 406 | State::Off => { 407 | *byte &= !bit; 408 | } 409 | State::On => { 410 | *byte |= bit; 411 | } 412 | } 413 | Some(()) 414 | } 415 | 416 | #[cfg(feature = "embedded-graphics")] 417 | fn set_embedded_graphics(&mut self, point: Point, color: BinaryColor) -> Option<()> { 418 | let x = point.x.try_into().ok()?; 419 | let y = point.y.try_into().ok()?; 420 | self.set((x, y), color.into()) 421 | } 422 | } 423 | 424 | /// State of a pixel 425 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 426 | pub enum State { 427 | #[default] 428 | Off, 429 | On, 430 | } 431 | 432 | impl From for State { 433 | fn from(value: bool) -> Self { 434 | if value { 435 | Self::On 436 | } else { 437 | Self::Off 438 | } 439 | } 440 | } 441 | 442 | #[cfg(feature = "embedded-graphics")] 443 | impl From for State { 444 | fn from(value: BinaryColor) -> Self { 445 | match value { 446 | BinaryColor::Off => Self::Off, 447 | BinaryColor::On => Self::On, 448 | } 449 | } 450 | } 451 | 452 | #[cfg(feature = "embedded-graphics")] 453 | impl Dimensions for MessageBuffer<'_> { 454 | fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle { 455 | Rectangle::new( 456 | Point::zero(), 457 | Size::new((self.0.len() * 8).try_into().unwrap(), 11), 458 | ) 459 | } 460 | } 461 | 462 | #[cfg(feature = "embedded-graphics")] 463 | impl DrawTarget for MessageBuffer<'_> { 464 | type Color = BinaryColor; 465 | 466 | type Error = std::convert::Infallible; 467 | 468 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 469 | where 470 | I: IntoIterator>, 471 | { 472 | for Pixel(point, color) in pixels { 473 | #[allow(clippy::manual_assert)] 474 | if self.set_embedded_graphics(point, color).is_none() { 475 | panic!( 476 | "tried to draw pixel outside the display area (x: {}, y: {})", 477 | point.x, point.y 478 | ); 479 | } 480 | } 481 | Ok(()) 482 | } 483 | } 484 | 485 | #[cfg(test)] 486 | mod test { 487 | use std::ops::Range; 488 | 489 | use super::Speed; 490 | 491 | #[test] 492 | fn speed_to_u8_and_back() { 493 | const VALID_SPEED_VALUES: Range = 1..8; 494 | for i in u8::MIN..u8::MAX { 495 | if let Ok(speed) = Speed::try_from(i) { 496 | assert_eq!(u8::from(speed), i); 497 | } else { 498 | assert!(!VALID_SPEED_VALUES.contains(&i)); 499 | } 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/usb_hid.rs: -------------------------------------------------------------------------------- 1 | //! Connect to an LED badge via USB HID 2 | 3 | use std::sync::Arc; 4 | 5 | use anyhow::{Context, Result}; 6 | use hidapi::{DeviceInfo, HidApi, HidDevice}; 7 | 8 | use crate::protocol::PayloadBuffer; 9 | 10 | enum DeviceType { 11 | // rename if we add another device type 12 | TheOnlyOneWeSupportForNow, 13 | } 14 | 15 | impl DeviceType { 16 | fn new(info: &DeviceInfo) -> Option { 17 | Some(match (info.vendor_id(), info.product_id()) { 18 | (0x0416, 0x5020) => Self::TheOnlyOneWeSupportForNow, 19 | _ => return None, 20 | }) 21 | } 22 | } 23 | 24 | /// A discovered USB device 25 | pub struct Device { 26 | api: Arc, 27 | info: DeviceInfo, 28 | type_: DeviceType, 29 | } 30 | 31 | impl Device { 32 | /// Return a list of all usb devies as a string representation 33 | pub fn list_all() -> Result> { 34 | let api = HidApi::new().context("create hid api")?; 35 | let devices = api.device_list(); 36 | 37 | Ok(devices 38 | .map(|info| { 39 | format!( 40 | "{:?}: vendor_id={:#06x} product_id={:#06x} manufacturer={:?} product={:?}", 41 | info.path(), 42 | info.vendor_id(), 43 | info.product_id(), 44 | info.manufacturer_string(), 45 | info.product_string(), 46 | ) 47 | }) 48 | .collect()) 49 | } 50 | 51 | /// Return all supported devices 52 | pub fn enumerate() -> Result> { 53 | let api = HidApi::new().context("create hid api")?; 54 | let api = Arc::new(api); 55 | 56 | let devices = api.device_list(); 57 | let devices = devices 58 | .filter_map(|info| { 59 | DeviceType::new(info).map(|type_| Device { 60 | api: api.clone(), 61 | info: info.clone(), 62 | type_, 63 | }) 64 | }) 65 | .collect(); 66 | 67 | Ok(devices) 68 | } 69 | 70 | /// Return the single supported device 71 | /// 72 | /// This function returns an error if no device could be found 73 | /// or if multiple devices would match. 74 | pub fn single() -> Result { 75 | let mut devices = Self::enumerate()?.into_iter(); 76 | let device = devices.next().context("no device found")?; 77 | anyhow::ensure!(devices.next().is_none(), "multiple devices found"); 78 | Ok(device) 79 | } 80 | 81 | /// Write a payload to the device 82 | pub fn write(&self, payload: PayloadBuffer) -> Result<()> { 83 | let device = self.info.open_device(&self.api).context("open device")?; 84 | match self.type_ { 85 | DeviceType::TheOnlyOneWeSupportForNow => { 86 | write_raw(&device, payload.into_padded_bytes().as_ref()) 87 | } 88 | } 89 | } 90 | } 91 | 92 | fn write_raw(device: &HidDevice, data: &[u8]) -> Result<()> { 93 | anyhow::ensure!(data.len() % 64 == 0, "payload not padded to 64 bytes"); 94 | 95 | // the device will brick itself if the payload is too long (more then 8192 bytes) 96 | anyhow::ensure!(data.len() <= 8192, "payload too long (max 8192 bytes)"); 97 | 98 | // just to be sure 99 | assert!(data.len() <= 8192); 100 | 101 | let n = device.write(data).context("write payload")?; 102 | 103 | anyhow::ensure!( 104 | n == data.len(), 105 | "incomplete write: {n} of {} bytes", 106 | data.len() 107 | ); 108 | 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Graphics utilities 2 | 3 | use embedded_graphics::Drawable; 4 | 5 | use self::layout::ZStack; 6 | 7 | /// Drawable layout extension 8 | pub trait DrawableLayoutExt: Drawable + Sized { 9 | /// Draw a 10 | fn z_stack(self, other: T) -> ZStack { 11 | ZStack(self, other) 12 | } 13 | } 14 | 15 | impl DrawableLayoutExt for T where T: Drawable {} 16 | 17 | pub mod layout { 18 | //! Types used by `DrawableLayoutExt ` 19 | 20 | use embedded_graphics::{ 21 | geometry::{Dimensions, Point}, 22 | primitives::Rectangle, 23 | Drawable, 24 | }; 25 | 26 | pub struct ZStack(pub(super) A, pub(super) B); 27 | 28 | impl Dimensions for ZStack 29 | where 30 | A: Dimensions, 31 | B: Dimensions, 32 | { 33 | fn bounding_box(&self) -> Rectangle { 34 | let a = self.0.bounding_box(); 35 | let b = self.1.bounding_box(); 36 | let left = i32::min(a.top_left.x, b.top_left.x); 37 | let top = i32::min(a.top_left.y, b.top_left.y); 38 | let right = i32::max(a.bottom_right().unwrap().x, b.bottom_right().unwrap().x); 39 | let bottom = i32::max(a.bottom_right().unwrap().y, b.bottom_right().unwrap().y); 40 | Rectangle::with_corners(Point::new(left, top), Point::new(right, bottom)) 41 | } 42 | } 43 | 44 | impl Drawable for ZStack 45 | where 46 | A: Drawable, 47 | B: Drawable, 48 | { 49 | type Color = A::Color; 50 | 51 | type Output = (A::Output, B::Output); 52 | 53 | fn draw(&self, target: &mut D) -> std::prelude::v1::Result 54 | where 55 | D: embedded_graphics::prelude::DrawTarget, 56 | { 57 | let a = self.0.draw(target)?; 58 | let b = self.1.draw(target)?; 59 | Ok((a, b)) 60 | } 61 | } 62 | } 63 | --------------------------------------------------------------------------------