diff --git a/.github/workflows/ebds.yml b/.github/workflows/ebds.yml new file mode 100644 index 0000000..014e405 --- /dev/null +++ b/.github/workflows/ebds.yml @@ -0,0 +1,102 @@ +name: ebds + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + rustfmt-clippy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt, clippy + + - name: Run rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + - name: Run clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --tests + + test: + runs-on: ${{matrix.os}} + strategy: + matrix: + os: [ubuntu-latest] + target: + - debian: null + cross: null + rust: null + llvm_version: ["5.0", "9.0"] + main_tests: [1] + release_build: [0, 1] + no_default_features: [0, 1] + # FIXME: There are no pre-built static libclang libraries, so the + # `static` feature is not testable atm. + feature_runtime: [0, 1] + feature_extra_asserts: [0] + + include: + # Test with extra asserts + docs just with latest llvm versions to + # prevent explosion + - os: ubuntu-latest + llvm_version: "9.0" + release_build: 0 + no_default_features: 0 + feature_extra_asserts: 1 + + steps: + - uses: actions/checkout@v3 + + - name: Install multiarch packages + if: matrix.target.debian + run: | + sudo apt-get install binfmt-support qemu-user-static gcc-${{matrix.target.cross}} g++-${{matrix.target.cross}} + source /etc/lsb-release + sudo tee /etc/apt/sources.list </dev/null + deb [arch=${{matrix.target.debian}}] http://ports.ubuntu.com/ubuntu-ports/ $DISTRIB_CODENAME main + deb [arch=${{matrix.target.debian}}] http://ports.ubuntu.com/ubuntu-ports/ $DISTRIB_CODENAME-updates main + deb [arch=${{matrix.target.debian}}] http://ports.ubuntu.com/ubuntu-ports/ $DISTRIB_CODENAME-backports main + deb [arch=${{matrix.target.debian}}] http://ports.ubuntu.com/ubuntu-ports/ $DISTRIB_CODENAME-security main + EOF + sudo dpkg --add-architecture ${{matrix.target.debian}} + sudo apt-get update + sudo apt-get install libc6:${{matrix.target.debian}} libstdc++6:${{matrix.target.debian}} + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: ${{matrix.target.rust}} + - name: Install libtinfo + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install libtinfo5 + - name: Run all the tests (debug) + env: + GITHUB_ACTIONS_OS: ${{matrix.os}} + RUST_TARGET: ${{matrix.target.rust}} + run: cargo test --all + - name: Run all the tests (release) + env: + GITHUB_ACTIONS_OS: ${{matrix.os}} + RUST_TARGET: ${{matrix.target.rust}} + run: cargo test --all --release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2c209ee --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ebds" +version = "0.1.0" +edition = "2021" +authors = ["EBDS Rust Developers"] +description = "Messages and related types for implementing the EBDS serial communication protocol" +keywords = ["no-std", "serial", "ebds", "bill-acceptor", "bill-validator"] +categories = ["no-std"] +repository = "https://github.com/ebds-rs/ebds" +license = "MIT" + +[dependencies] +bitfield = "0.14" +log = { version = "0.4", default-features = false } +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +serialport = { version = "4.2", default-features = false } +arbitrary = { version = "1", optional = true } +parking_lot = "0.12" + +[features] +default = ["sc", "usd"] +e2e = [] +s2k = [] +sc = [] +std = [] +arbitrary = ["arbitrary/derive"] + +# Currency sets +amd = [] +aud = [] +cad = [] +cny = [] +gbp = [] +jpy = [] +mxn = [] +usd = [] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..750f959 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright © 2023 EBDS Rust developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b124b56 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# EBDS Serial Protocol + +This crate implements the EBDS serial protocol messages, and related types for communication with bill acceptor unit devices. + +The currently supported messages are implemented in the various modules in this crate, along with some common types used across multiple messages. + +If adding a new message, please follow the existing pattern of placing `...Command` (host-initiated) messages in `/command.rs` files, and `...Reply` (device-initiated) messages in `/reply.rs` files. + +There are some exceptions to the general rule, e.g. when the types in the documenation do not follow the `Command/Reply` naming convention. + +In those cases, the suffix is omitted to aid in readability when comparing with the EBDS specification. + +## Macros + +Some simple macros exist for implementing traits over the various message types. All message types should implement `MessageOps`, and all reply types should implement `OmnibusReplyOps`. + +`MessageOps` can be implemented with the helper macro `impl_message_ops!`, e.g. for a new `SomeNewReply` message: + +```rust +use crate::impl_message_ops; + +pub struct SomeNewReply { + // For the example, we are just using a number for the length. + // In real implementations, please add a constant to the `len` module. + buf: [u8; 11], +} + +impl_message_ops!(SomeNewReply); +``` + +This will implement the `MessageOps` trait for `SomeNewReply`, and provide all of the associated functions. Traits are how Rust does polymorphism, similar to Go's `interface` and C++'s `template`, with important differences. + +All of the macro implementations live in `src/macros.rs`. + +## Using with `std` + +This library is `no-std` compatible by default. To use `std`-only features, add the `std` feature to the dependency: + +```toml +ebds = { version = "0.1", features = ["std"] } +``` diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..ece4c8c --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,461 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach", +] + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "derive_arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cdeb9ec472d588e539a818b2dee436825730da08ad0017c4b1a17676bdc8b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ebds" +version = "0.1.0" +dependencies = [ + "arbitrary", + "bitfield", + "log", + "parking_lot", + "serde", + "serde_json", + "serialport", +] + +[[package]] +name = "ebds-fuzz" +version = "0.0.0" +dependencies = [ + "ebds", + "libfuzzer-sys", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serialport" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353dc2cbfc67c9a14a89a1292a9d8e819bd51066b083e08c1974ba08e3f48c62" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags 2.0.2", + "cfg-if", + "mach2", + "nix", + "regex", + "scopeguard", + "winapi", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..149c270 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "ebds-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.ebds] +path = ".." +features = ["arbitrary", "std"] + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "fuzz_method" +path = "fuzz_targets/fuzz_method.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_omnibus_reply" +path = "fuzz_targets/fuzz_omnibus_reply.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_omnibus_reply_from_bytes" +path = "fuzz_targets/fuzz_omnibus_reply_from_bytes.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_build_message" +path = "fuzz_targets/fuzz_build_message.rs" +test = false +doc = false diff --git a/fuzz/dictionary/method.dict b/fuzz/dictionary/method.dict new file mode 100644 index 0000000..104540f --- /dev/null +++ b/fuzz/dictionary/method.dict @@ -0,0 +1,31 @@ +"ACCEPT" +"accept" +"ACCept" +"STOP" +"stop" +"stOP" +"DISPENSE" +"dispense" +"disPENse" +"STACK" +"stack" +"stACK" +"REJECT" +"reject" +"reJEct" +"STATUS" +"status" +"stAtus" +"ESCROW_FULL" +"escrow_full" +"escrOW_fULl" +"RESET" +"reset" +"reSet" +"SHUTDOWN" +"shutdown" +"SHutDown" +"UNKNOWN" +"unknown" +"UNknowN" + diff --git a/fuzz/dictionary/omnibus_reply.dict b/fuzz/dictionary/omnibus_reply.dict new file mode 100644 index 0000000..7897eaf --- /dev/null +++ b/fuzz/dictionary/omnibus_reply.dict @@ -0,0 +1 @@ +"\x02\x0b\x20\x00\x00\x00\x00\x00\x00\x03\x2b" diff --git a/fuzz/fuzz_targets/fuzz_build_message.rs b/fuzz/fuzz_targets/fuzz_build_message.rs new file mode 100644 index 0000000..17a21b3 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_build_message.rs @@ -0,0 +1,17 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +use ebds::{MessageVariant, len}; + +fuzz_target!(|data: &[u8]| { + // Omnibus Reply's length is the minimum for any reply message + if data.len() < len::OMNIBUS_REPLY {return} + + let msg = match MessageVariant::from_buf(data) { + Ok(msg) => msg, + Err(_err) => return, + }; + + assert!(msg.as_omnibus_reply().len() <= data.len()); +}); diff --git a/fuzz/fuzz_targets/fuzz_method.rs b/fuzz/fuzz_targets/fuzz_method.rs new file mode 100644 index 0000000..381a4dc --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_method.rs @@ -0,0 +1,24 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +use ebds::Method; + +fuzz_target!(|data: &[u8]| { + let upper_data_str = std::str::from_utf8(data).unwrap_or("").to_uppercase(); + let method = Method::from(data); + + match upper_data_str.as_str() { + "ACCEPT" => assert_eq!(method, Method::Accept), + "STOP" => assert_eq!(method, Method::Stop), + "DISPENSE" => assert_eq!(method, Method::Dispense), + "STACK" => assert_eq!(method, Method::Stack), + "REJECT" => assert_eq!(method, Method::Reject), + "STATUS" => assert_eq!(method, Method::Status), + "ESCROW_FULL" => assert_eq!(method, Method::EscrowFull), + "RESET" => assert_eq!(method, Method::Reset), + "SHUTDOWN" => assert_eq!(method, Method::Shutdown), + "UNKNOWN" => assert_eq!(method, Method::Unknown), + _ => assert_eq!(method, Method::Unknown), + } +}); diff --git a/fuzz/fuzz_targets/fuzz_omnibus_reply.rs b/fuzz/fuzz_targets/fuzz_omnibus_reply.rs new file mode 100644 index 0000000..82ba213 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_omnibus_reply.rs @@ -0,0 +1,9 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +use ebds::{OmnibusReply, MessageOps, MessageType, len::OMNIBUS_REPLY}; + +fuzz_target!(|reply: OmnibusReply| { + assert_eq!(reply.buf().len(), OMNIBUS_REPLY); +}); diff --git a/fuzz/fuzz_targets/fuzz_omnibus_reply_from_bytes.rs b/fuzz/fuzz_targets/fuzz_omnibus_reply_from_bytes.rs new file mode 100644 index 0000000..7233ee1 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_omnibus_reply_from_bytes.rs @@ -0,0 +1,14 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +use ebds::{OmnibusReply, MessageOps, MessageType, len}; + +fuzz_target!(|data: &[u8]| { + if data.len() < len::OMNIBUS_REPLY {return} + + let mut reply = OmnibusReply::new(); + if let Err(_err) = reply.from_buf(data) {return} + + assert_eq!(reply.buf().len(), len::OMNIBUS_REPLY); +}); diff --git a/src/advanced_bookmark_mode.rs b/src/advanced_bookmark_mode.rs new file mode 100644 index 0000000..8cb4eec --- /dev/null +++ b/src/advanced_bookmark_mode.rs @@ -0,0 +1,5 @@ +mod command; +mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/advanced_bookmark_mode/command.rs b/src/advanced_bookmark_mode/command.rs new file mode 100644 index 0000000..e849688 --- /dev/null +++ b/src/advanced_bookmark_mode/command.rs @@ -0,0 +1,135 @@ +use crate::{ + bool_enum, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::ADVANCED_BOOKMARK_MODE_COMMAND, ExtendedCommand, ExtendedCommandOps, MessageOps, + MessageType, +}; + +mod index { + pub const STATUS: usize = 7; +} + +bool_enum!( + AdvancedBookmarkStatus, + r"Whether the Advance Bookmark Mode is enabled on the device" +); + +/// Advanced Bookmark Mode - Command (Subtype 0x0D) +/// +/// The Advanced bookmark message is used to enable/disable a special mode of the device. This is +/// available on newer firmware. When a device is in advanced bookmark mode, it will allow the very next +/// document to be processed by the host even if it does not have any value. If the document meets certain +/// size restrictions, the host can decide to accept or return the document. If stacked, the document will be +/// reported as “No Value.” +/// +/// To prevent accidentally stacking a valid document (banknote or barcode), the device automatically +/// reject all validated documents. +/// +/// **WARNING** Even though the device will auto reject a valid note in this mode, there is no guarantee that +/// the device will reject ALL valid notes in this mode because the device may possibly not detect the +/// document as a valid banknote; the burden is placed on the host implementation to ensure no miscounts +/// occur when using this message. This mode is designed for attended services and allows the attendant to +/// insert a slip or receipt that should be placed with the currency. +/// +/// The device will automatically leave the mode if any of the following conditions are met: +/// +/// * Power is lost +/// * Host informs the device to leave the mode. +/// * Calibration mode is entered. +/// * A document is stacked +/// +/// The most important one is the last one: “A document is stacked.” This means the mode does not persist +/// and will only be enabled for a single document. Once this document is stacked, the device will resume +/// normal operations. +/// +/// If this mode is enabled along with the normal bookmark mode, then advanced bookmark mode takes +/// precedence; all valid notes and barcodes will automatically be rejected. +/// +/// The host must enable this feature every time a custom bookmark is to be accepted. This is done with the +/// following message. (The same message structure can also be used to disable the feature). +/// +/// The Advanced Bookmark Mode Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Status | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +/// | Value | 0x02 | 0x0B | 0x7n | 0x04 | nn | nn | nn | 0x00/01 | 0x03 | zz | +/// +/// The Status byte tells the device to enable (0x01) or disable (0x00) the mode. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AdvancedBookmarkModeCommand { + buf: [u8; ADVANCED_BOOKMARK_MODE_COMMAND], +} + +impl AdvancedBookmarkModeCommand { + /// Creates a new [AdvancedBookmarkModeCommand]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; ADVANCED_BOOKMARK_MODE_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::AdvancedBookmark); + + message + } + + /// Gets the status of Advanced Bookmark Mode. + pub fn status(&self) -> AdvancedBookmarkStatus { + self.buf[index::STATUS].into() + } + + /// Sets the status of Advanced Bookmark Mode. + pub fn set_status(&mut self, status: AdvancedBookmarkStatus) { + self.buf[index::STATUS] = status.into(); + } +} + +impl_default!(AdvancedBookmarkModeCommand); +impl_message_ops!(AdvancedBookmarkModeCommand); +impl_extended_ops!(AdvancedBookmarkModeCommand); +impl_omnibus_extended_command!(AdvancedBookmarkModeCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_advanced_bookmark_command_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0a, 0x70, 0x0d, + // Data + 0x00, 0x00, 0x00, 0x01, + // ETX | Checksum + 0x03, 0x76, + ]; + + let mut msg = AdvancedBookmarkModeCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::AdvancedBookmark); + assert_eq!(msg.status(), AdvancedBookmarkStatus::Set); + + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0a, 0x70, 0x0d, + // Data + 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x77, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::AdvancedBookmark); + assert_eq!(msg.status(), AdvancedBookmarkStatus::Unset); + + Ok(()) + } +} diff --git a/src/advanced_bookmark_mode/reply.rs b/src/advanced_bookmark_mode/reply.rs new file mode 100644 index 0000000..daed87f --- /dev/null +++ b/src/advanced_bookmark_mode/reply.rs @@ -0,0 +1,140 @@ +use crate::std; +use std::fmt; + +use crate::{ + bool_enum, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply, + len::ADVANCED_BOOKMARK_MODE_REPLY, ExtendedCommand, ExtendedCommandOps, MessageOps, + MessageType, OmnibusReplyOps, +}; + +mod index { + pub const ACKNAK: usize = 10; +} + +bool_enum!( + AdvancedBookmarkAckNak, + r" +Whether the Advance Bookmark Mode is enabled on the device. + +May also indicate the device was busy (NAKs when stacking or powering up). +" +); + +/// Advanced Bookmark Mode - Reply (Subtype 0x0D) +/// +/// The device will respond with an ACK or NAK message. +/// +/// The Advanced Bookmark Mode Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ACK/NAK | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | +/// | Value | 0x02 | 0x0D | 0x7n | 0x04 | nn | nn | nn | nn | nn | nn | 0x00/01 | 0x03 | zz | +/// +/// If the device ACKs the message with 0x01, then the mode has been entered. The device may NAK the +/// message if it is currently busy (processing a note or powering up). +/// +/// When the device stacks a document in this mode, the Standard Omnibus Reply (section 7.1.2) is +/// reported with the stacked bit set and no value reported in data byte 2. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AdvancedBookmarkModeReply { + buf: [u8; ADVANCED_BOOKMARK_MODE_REPLY], +} + +impl AdvancedBookmarkModeReply { + /// Creates a new [AdvancedBookmarkModeReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; ADVANCED_BOOKMARK_MODE_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::AdvancedBookmark); + + message + } + + /// Gets the ACKNAK reply of [AdvancedBookmarkModeReply]. + pub fn mode_acknak(&self) -> AdvancedBookmarkAckNak { + self.buf[index::ACKNAK].into() + } + + /// Sets the status of [AdvancedBookmarkModeReply]. + pub fn set_mode_acknak(&mut self, acknak: AdvancedBookmarkAckNak) { + self.buf[index::ACKNAK] = acknak.into(); + } +} + +impl_default!(AdvancedBookmarkModeReply); +impl_message_ops!(AdvancedBookmarkModeReply); +impl_omnibus_extended_reply!(AdvancedBookmarkModeReply); +impl_extended_ops!(AdvancedBookmarkModeReply); + +impl fmt::Display for AdvancedBookmarkModeReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Subtype: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, ModeAckNak: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.extended_command(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + self.mode_acknak(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_advanced_bookmark_reply_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0d, 0x70, 0x0d, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ACK/NAK + 0x01, + // ETX | Checksum + 0x03, 0x71, + ]; + + let mut msg = AdvancedBookmarkModeReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::AdvancedBookmark); + assert_eq!(msg.mode_acknak(), AdvancedBookmarkAckNak::Set); + + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0d, 0x70, 0x0d, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ACK/NAK + 0x00, + // ETX | Checksum + 0x03, 0x70, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::AdvancedBookmark); + assert_eq!(msg.mode_acknak(), AdvancedBookmarkAckNak::Unset); + + Ok(()) + } +} diff --git a/src/aux_command.rs b/src/aux_command.rs new file mode 100644 index 0000000..7c3f5aa --- /dev/null +++ b/src/aux_command.rs @@ -0,0 +1,100 @@ +use crate::std; +use std::fmt; + +use crate::MessageOps; + +/// Auxilliary Commmands (Type 6): The Auxiliary Commands are used to provide functionality outside the scope of the Omnibus command +/// in the previous sections. These commands can be specific to a certain code base, so be sure to check the +/// compatibility icons before each section. + +/// Developers: add additional types from the specification as needed +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AuxCommand { + QuerySoftwareCrc = 0x00, + QueryBootPartNumber = 0x06, + QueryApplicationPartNumber = 0x07, + QueryVariantName = 0x08, + QueryVariantPartNumber = 0x09, + QueryDeviceCapabilities = 0x0d, + QueryApplicationId = 0x0e, + QueryVariantId = 0x0f, + SoftReset = 0x7f, + Reserved = 0xff, +} + +impl From for AuxCommand { + fn from(b: u8) -> Self { + match b { + 0x00 => Self::QuerySoftwareCrc, + 0x06 => Self::QueryBootPartNumber, + 0x07 => Self::QueryApplicationPartNumber, + 0x08 => Self::QueryVariantName, + 0x09 => Self::QueryVariantPartNumber, + 0x0d => Self::QueryDeviceCapabilities, + 0x0e => Self::QueryApplicationId, + 0x0f => Self::QueryVariantId, + 0x7f => Self::SoftReset, + _ => Self::Reserved, + } + } +} + +impl From for &'static str { + fn from(a: AuxCommand) -> Self { + match a { + AuxCommand::QuerySoftwareCrc => "QuerySoftwareCrc", + AuxCommand::QueryBootPartNumber => "QueryBootPartNumber", + AuxCommand::QueryApplicationPartNumber => "QueryApplicationPartNumber", + AuxCommand::QueryVariantName => "QueryVariantName", + AuxCommand::QueryVariantPartNumber => "QueryVariantPartNumber", + AuxCommand::QueryDeviceCapabilities => "QueryDeviceCapabilities", + AuxCommand::QueryApplicationId => "QueryApplicationId", + AuxCommand::QueryVariantId => "QueryVariantId", + AuxCommand::SoftReset => "SoftReset", + AuxCommand::Reserved => "Reserved", + } + } +} + +impl From<&AuxCommand> for &'static str { + fn from(a: &AuxCommand) -> Self { + (*a).into() + } +} + +impl From for u8 { + fn from(a: AuxCommand) -> Self { + a as u8 + } +} + +impl From<&AuxCommand> for u8 { + fn from(a: &AuxCommand) -> Self { + (*a).into() + } +} + +impl fmt::Display for AuxCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +pub(crate) mod index { + pub const COMMAND: usize = 5; +} + +pub trait AuxCommandOps: MessageOps { + /// Gets the auxilliary command sub-type. + fn aux_command(&self) -> AuxCommand { + assert!(self.buf().len() > index::COMMAND); + + self.buf()[index::COMMAND].into() + } + + /// Sets the auxilliary command sub-type. + fn set_aux_command(&mut self, aux_command: AuxCommand) { + self.buf_mut()[index::COMMAND] = aux_command.into(); + } +} diff --git a/src/banknote.rs b/src/banknote.rs new file mode 100644 index 0000000..f5e3140 --- /dev/null +++ b/src/banknote.rs @@ -0,0 +1,854 @@ +#[cfg(not(feature = "std"))] +use alloc::string::{String, ToString}; + +use crate::std; +use std::fmt; + +use crate::Currency; + +pub type ISOCode = Currency; + +/// A three character ASCII coded decimal value +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BaseValue(u16); + +impl BaseValue { + pub const LEN: usize = 3; + + pub const fn default() -> Self { + Self(0) + } +} + +impl From for u16 { + fn from(b: BaseValue) -> Self { + b.0 + } +} + +impl From<&BaseValue> for u16 { + fn from(b: &BaseValue) -> Self { + (*b).into() + } +} + +impl From for f32 { + fn from(b: BaseValue) -> Self { + b.0 as f32 + } +} + +impl From<&BaseValue> for f32 { + fn from(b: &BaseValue) -> Self { + (*b).into() + } +} + +impl From<&[u8]> for BaseValue { + fn from(b: &[u8]) -> Self { + if b.len() < Self::LEN { + Self(0) + } else { + // try to parse decimal value from the byte buffer + // any failure results in a default (0) value + let val = std::str::from_utf8(b[..Self::LEN].as_ref()) + .unwrap_or("0") + .parse::() + .unwrap_or(0); + Self(val) + } + } +} + +impl From<[u8; N]> for BaseValue { + fn from(b: [u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&[u8; N]> for BaseValue { + fn from(b: &[u8; N]) -> Self { + b.as_ref().into() + } +} + +/// An ASCII coded sign value for the Exponent. +/// This field is either a “+” or a “-“ +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Sign { + Positive, + Negative, +} + +impl Sign { + pub const LEN: usize = 1; + + pub const fn default() -> Self { + Self::Positive + } +} + +impl From for Sign { + fn from(b: u8) -> Self { + match b { + b'+' => Self::Positive, + b'-' => Self::Negative, + _ => Self::Positive, + } + } +} + +impl From for &'static str { + fn from(sign: Sign) -> Self { + match sign { + Sign::Negative => "-", + Sign::Positive => "+", + } + } +} + +/// ASCII coded decimal value for the power of ten +/// that the base is to either be multiplied by (if Sign +/// is “+”) or divided by (if Sign is “-“) +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Exponent(u8); + +impl Exponent { + pub const LEN: usize = 2; + + pub const fn default() -> Self { + Self(1) + } +} + +impl From for Exponent { + fn from(b: u8) -> Self { + Self(b) + } +} + +impl From for u8 { + fn from(e: Exponent) -> Self { + e.0 + } +} + +impl From<&Exponent> for u8 { + fn from(e: &Exponent) -> Self { + (*e).into() + } +} + +impl From for f32 { + fn from(e: Exponent) -> Self { + e.0 as f32 + } +} + +impl From<&Exponent> for f32 { + fn from(e: &Exponent) -> Self { + (*e).into() + } +} + +impl From<&[u8]> for Exponent { + fn from(b: &[u8]) -> Self { + if b.len() < Exponent::LEN { + Exponent(1) + } else { + let exp = std::str::from_utf8(b[..Exponent::LEN].as_ref()) + .unwrap_or("1") + .parse::() + .unwrap_or(1); + Self(exp) + } + } +} + +impl From<[u8; N]> for Exponent { + fn from(b: [u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&[u8; N]> for Exponent { + fn from(b: &[u8; N]) -> Self { + b.as_ref().into() + } +} + +/// A single character binary field that encodes the +/// orientation of the bank note. +/// +/// * 0x00 = Right Edge, Face Up +/// * 0x01 = Right Edge, Face Down +/// * 0x02 = Left Edge, Face Up +/// * 0x03 = Left Edge, Face Down +/// +/// Note: In general, this field is only correct if the +/// Extended orientation bit is set in device +/// capabilities map. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BanknoteOrientation { + RightEdgeFaceUp = 0x00, + RightEdgeFaceDown = 0x01, + LeftEdgeFaceUp = 0x02, + LeftEdgeFaceDown = 0x03, +} + +impl BanknoteOrientation { + pub const LEN: usize = 1; + pub const MASK: u8 = 0b11; + + pub const fn default() -> Self { + Self::RightEdgeFaceUp + } +} + +impl From for BanknoteOrientation { + fn from(b: u8) -> Self { + match b & Self::MASK { + 0x00 => Self::RightEdgeFaceUp, + 0x01 => Self::RightEdgeFaceDown, + 0x02 => Self::LeftEdgeFaceUp, + 0x03 => Self::LeftEdgeFaceDown, + // Computationally unreachable, but add the default case in the off chance the laws of + // computation break + // + // Avoid the use of the `unreachable` macro because it panics if ever hit, and there is + // a sane default here + _ => Self::default(), + } + } +} + +impl From for &'static str { + fn from(b: BanknoteOrientation) -> Self { + match b { + BanknoteOrientation::RightEdgeFaceUp => "Right edge face up", + BanknoteOrientation::RightEdgeFaceDown => "Right edge face down", + BanknoteOrientation::LeftEdgeFaceUp => "Left edge face up", + BanknoteOrientation::LeftEdgeFaceDown => "Left edge face down", + } + } +} + +impl From<&BanknoteOrientation> for &'static str { + fn from(b: &BanknoteOrientation) -> Self { + (*b).into() + } +} + +impl fmt::Display for BanknoteOrientation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bstr: &'static str = self.into(); + write!(f, "{bstr}") + } +} + +macro_rules! ascii_tuple_struct { + ($name:ident, $base:tt, $doc:tt) => { + #[doc = $doc] + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct $name($base); + + impl $name { + pub const LEN: usize = std::mem::size_of::<$base>(); + + pub const fn default() -> Self { + Self(0) + } + + pub fn to_string(&self) -> String { + std::str::from_utf8(&[self.0]).unwrap_or("").to_string() + } + } + + impl From<$base> for $name { + fn from(b: $base) -> Self { + Self(b) + } + } + + impl From<$name> for String { + fn from(n: $name) -> String { + n.to_string() + } + } + + impl From<&$name> for String { + fn from(n: &$name) -> String { + (*n).into() + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_string()) + } + } + }; +} + +ascii_tuple_struct!( + NoteType, + u8, + r" + An ASCII letter that documents the note type. + + This corresponds to the data in the variant identity card. +" +); + +ascii_tuple_struct!( + NoteSeries, + u8, + r" + An ASCII letter that documents the note series. + + This corresponds to the data in the variant identity card. +" +); + +ascii_tuple_struct!( + NoteCompatibility, + u8, + r" + An ASCII letter that documents the revision of the recognition core used. + + This corresponds to the data in the variant identity card. +" +); + +ascii_tuple_struct!( + NoteVersion, + u8, + r" + An ASCII letter that documents the version of the note's recognition criteria. + + This corresponds to the data in the variant identity card. +" +); + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BanknoteClassification { + /// Sent for any the following: + /// + /// - In response to a Host Query Extended Note Specification Command (i.e. host requests a note table element). + /// - In response to a note escrowed or stacked event while device is in extended note mode and classification is + /// - Supported by the device but disabled. + /// - NOT supported by the device. + DisabledOrNotSupported = 0x00, + /// Class 1 (unidentified banknote) + Unidentified = 0x01, + /// Class 2 (suspected counterfeit) + SuspectedCounterfeit = 0x02, + /// Class 3 (suspected zero value note) + SuspectedZero = 0x03, + /// Class 4 (genuine banknote) + Genuine = 0x04, +} + +impl BanknoteClassification { + pub const fn default() -> Self { + Self::DisabledOrNotSupported + } +} + +impl From for BanknoteClassification { + fn from(b: u8) -> Self { + match b { + 0x00 => Self::DisabledOrNotSupported, + 0x01 => Self::Unidentified, + 0x02 => Self::SuspectedCounterfeit, + 0x03 => Self::SuspectedZero, + 0x04 => Self::Genuine, + _ => { + log::trace!("Unknown banknote classification: 0x{b:x}"); + Self::default() + } + } + } +} + +impl From for &'static str { + fn from(b: BanknoteClassification) -> Self { + match b { + BanknoteClassification::DisabledOrNotSupported => "Disabled or not supported", + BanknoteClassification::Unidentified => "Unidentified", + BanknoteClassification::SuspectedCounterfeit => "Suspected counterfeit", + BanknoteClassification::SuspectedZero => "Suspected zero", + BanknoteClassification::Genuine => "Genuine", + } + } +} + +impl From<&BanknoteClassification> for &'static str { + fn from(b: &BanknoteClassification) -> Self { + (*b).into() + } +} + +impl fmt::Display for BanknoteClassification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +/// The banknote value +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Banknote { + /// The banknote value. + pub(crate) value: f32, + /// The banknote ISO code. + pub(crate) iso_code: ISOCode, + /// The banknote type. + pub(crate) note_type: NoteType, + /// The banknote series. + pub(crate) note_series: NoteSeries, + /// The banknote compatibility. + pub(crate) note_compatibility: NoteCompatibility, + /// The banknote version. + pub(crate) note_version: NoteVersion, + /// The banknote classification, see [BanknoteClassification]. + pub(crate) banknote_classification: BanknoteClassification, +} + +impl Banknote { + pub const fn new( + value: f32, + iso_code: ISOCode, + note_type: NoteType, + note_series: NoteSeries, + note_compatibility: NoteCompatibility, + note_version: NoteVersion, + banknote_classification: BanknoteClassification, + ) -> Self { + Self { + value, + iso_code, + note_type, + note_series, + note_compatibility, + note_version, + banknote_classification, + } + } + + pub const fn default() -> Self { + Self { + value: 0f32, + iso_code: ISOCode::default(), + note_type: NoteType::default(), + note_series: NoteSeries::default(), + note_compatibility: NoteCompatibility::default(), + note_version: NoteVersion::default(), + banknote_classification: BanknoteClassification::default(), + } + } + + /// Get the banknote value. + pub fn value(&self) -> f32 { + self.value + } + + /// Set the banknote value. + pub fn set_value(&mut self, value: u32) { + self.value = value as f32; + } + + /// Sets the [Banknote] value, consumes and returns the [Banknote]. + pub fn with_value(mut self, value: u32) -> Self { + self.set_value(value); + self + } + + /// Get the banknote ISO code. + pub fn iso_code(&self) -> ISOCode { + self.iso_code + } + + /// Set the banknote ISO code. + pub fn set_iso_code(&mut self, iso_code: ISOCode) { + self.iso_code = iso_code; + } + + /// Get the banknote type. + pub fn note_type(&self) -> NoteType { + self.note_type + } + + /// Set the banknote type. + pub fn set_note_type(&mut self, note_type: NoteType) { + self.note_type = note_type; + } + + /// Get the banknote series. + pub fn note_series(&self) -> NoteSeries { + self.note_series + } + + /// Set the banknote series. + pub fn set_note_series(&mut self, note_series: NoteSeries) { + self.note_series = note_series; + } + + /// Get the banknote compatibility. + pub fn note_compatibility(&self) -> NoteCompatibility { + self.note_compatibility + } + + /// Set the banknote compatibility. + pub fn set_note_compatibility(&mut self, note_compatibility: NoteCompatibility) { + self.note_compatibility = note_compatibility; + } + + /// Get the banknote version. + pub fn note_version(&self) -> NoteVersion { + self.note_version + } + + /// Set the banknote version. + pub fn set_note_version(&mut self, note_version: NoteVersion) { + self.note_version = note_version; + } + + /// Get the banknote classification. + pub fn banknote_classification(&self) -> BanknoteClassification { + self.banknote_classification + } + + /// Set the banknote classification. + pub fn set_banknote_classification(&mut self, banknote_classification: BanknoteClassification) { + self.banknote_classification = banknote_classification; + } +} + +impl fmt::Display for Banknote { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Value: {} ISO Code: {} Note Type: {} Note Series: {} Note Compatibility: {} Note Version: {} Banknote Classification: {}", + self.value as u64, + self.iso_code, + self.note_type, + self.note_series, + self.note_compatibility, + self.note_version, + self.banknote_classification, + ) + } +} + +/// Extended note table item +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NoteTableItem { + pub(crate) note_index: usize, + pub(crate) banknote: Banknote, +} + +impl NoteTableItem { + /// Creates a new [NoteTableItem]. + pub const fn new(note_index: usize, banknote: Banknote) -> Self { + Self { + note_index, + banknote, + } + } + + /// Creates a default (null) [NoteTableItem] + pub const fn default() -> Self { + Self { + note_index: 0, + banknote: Banknote::default(), + } + } + + /// Get whether the [NoteTableItem] is null, indicating the end of the note table + pub fn is_null(&self) -> bool { + self == &Self::default() + } + + /// Get the note index. + pub fn note_index(&self) -> usize { + self.note_index + } + + /// Set the note index. + pub fn set_note_index(&mut self, note_index: usize) { + self.note_index = note_index; + } + + /// Get a reference to the banknote. + pub fn banknote(&self) -> &Banknote { + &self.banknote + } + + /// Get a mutable reference to the banknote. + pub fn banknote_mut(&mut self) -> &mut Banknote { + &mut self.banknote + } + + /// Set the banknote. + pub fn set_banknote(&mut self, banknote: Banknote) { + self.banknote = banknote; + } +} + +impl fmt::Display for NoteTableItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Index: {} {}", self.note_index, self.banknote) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_iso_code() { + let iso_aud = ISOCode::AUD; + let iso_amd = ISOCode::AMD; + let iso_cad = ISOCode::CAD; + let iso_eur = ISOCode::EUR; + let iso_gbp = ISOCode::GBP; + let iso_mxn = ISOCode::MXN; + let iso_cny = ISOCode::CNY; + let iso_usd = ISOCode::USD; + let iso_xxx = ISOCode::XXX; + + assert_eq!(<&'static str>::from(iso_aud), "AUD"); + assert_eq!(<&'static str>::from(iso_amd), "AMD"); + assert_eq!(<&'static str>::from(iso_cad), "CAD"); + assert_eq!(<&'static str>::from(iso_eur), "EUR"); + assert_eq!(<&'static str>::from(iso_gbp), "GBP"); + assert_eq!(<&'static str>::from(iso_mxn), "MXN"); + assert_eq!(<&'static str>::from(iso_cny), "CNY"); + assert_eq!(<&'static str>::from(iso_usd), "USD"); + assert_eq!(<&'static str>::from(iso_xxx), "XXX"); + + assert_eq!(ISOCode::from("AUD"), iso_aud); + assert_eq!(ISOCode::from("AMD"), iso_amd); + assert_eq!(ISOCode::from("CAD"), iso_cad); + assert_eq!(ISOCode::from("EUR"), iso_eur); + assert_eq!(ISOCode::from("GBP"), iso_gbp); + assert_eq!(ISOCode::from("MXN"), iso_mxn); + assert_eq!(ISOCode::from("CNY"), iso_cny); + assert_eq!(ISOCode::from("USD"), iso_usd); + assert_eq!(ISOCode::from("XXX"), iso_xxx); + assert_eq!(ISOCode::from(""), iso_xxx); + + assert_eq!(ISOCode::from(b"AUD"), iso_aud); + assert_eq!(ISOCode::from(b"AMD"), iso_amd); + assert_eq!(ISOCode::from(b"CAD"), iso_cad); + assert_eq!(ISOCode::from(b"EUR"), iso_eur); + assert_eq!(ISOCode::from(b"GBP"), iso_gbp); + assert_eq!(ISOCode::from(b"MXN"), iso_mxn); + assert_eq!(ISOCode::from(b"CNY"), iso_cny); + assert_eq!(ISOCode::from(b"USD"), iso_usd); + assert_eq!(ISOCode::from(b"XXX"), iso_xxx); + assert_eq!(ISOCode::from(b""), iso_xxx); + + for i in 0..=u8::MAX { + // Check that values that are too short are parsed as the default value + assert_eq!(ISOCode::from([i]), ISOCode::default()); + + for j in 0..=u8::MAX { + // Check that values that are too short are parsed as the default value + assert_eq!(ISOCode::from([i, j]), ISOCode::default()); + + for k in 0..=u8::MAX { + let iso_str = &[i, j, k]; + // Check that all values outside the valid range are parsed as the default + // value + match iso_str.as_ref() { + b"AUD" | b"AMD" | b"CAD" | b"EUR" | b"GBP" | b"JPY" | b"MXN" | b"CNY" + | b"USD" => continue, + _ => assert_eq!(ISOCode::from(iso_str), ISOCode::default()), + } + } + } + } + } + + #[test] + fn test_base_value() { + let base_value = BaseValue(42); + + assert_eq!(u16::from(base_value), 42); + assert_eq!(f32::from(base_value), 42.0); + assert_eq!(BaseValue::from(b"042"), base_value); + + // Check that values that are too short get parsed as the default value + for i in 0..=u8::MAX { + assert_eq!(BaseValue::from([i]), BaseValue::default()); + + for j in 0..=u8::MAX { + assert_eq!(BaseValue::from([i, j]), BaseValue::default()); + } + } + + // Check that values that are too long only parse the first BaseValue::LEN bytes + assert_eq!(BaseValue::from(b"042f"), base_value); + } + + #[test] + fn test_sign() { + let sign_pos = Sign::Positive; + let sign_neg = Sign::Negative; + + assert_eq!(Sign::from(b'+'), sign_pos); + assert_eq!(Sign::from(b'-'), sign_neg); + + for i in 0..=u8::MAX { + let b = i as u8; + if b != b'-' { + assert_eq!(Sign::from(b), sign_pos); + } + } + + assert_eq!(<&'static str>::from(sign_pos), "+"); + assert_eq!(<&'static str>::from(sign_neg), "-"); + } + + #[test] + fn test_exponent() { + let exp_max = Exponent(99); + let exp_def = Exponent(1); + let exp_min = Exponent(0); + + assert_eq!(Exponent::default(), exp_def); + assert_eq!(Exponent::from(b"99"), exp_max); + assert_eq!(Exponent::from(b"00"), exp_min); + + // Check that values that are too short are parsed as the default value + assert_eq!(Exponent::from([]), exp_def); + for i in 0..=u8::MAX { + // Check that values that are too short are parsed as the default value + assert_eq!(Exponent::from([i]), exp_def); + + for j in 0..=u8::MAX { + if i == b'+' && j >= b'0' && j <= b'9' { + assert_eq!( + Exponent::from([i, j]), + Exponent(std::str::from_utf8(&[j]).unwrap().parse::().unwrap()) + ); + } else if i < b'0' || i > b'9' || j < b'0' || j > b'9' { + // Check that values outside the valid range are parsed as the default value + assert_eq!( + Exponent::from([i, j]), + exp_def, + "i: {i}, j: {j}, string({})", + std::str::from_utf8(&[i, j]).unwrap() + ); + } + } + } + } + + #[test] + fn test_banknote_orientation() { + assert_eq!( + BanknoteOrientation::from(0x00), + BanknoteOrientation::RightEdgeFaceUp + ); + assert_eq!( + BanknoteOrientation::from(0x01), + BanknoteOrientation::RightEdgeFaceDown + ); + assert_eq!( + BanknoteOrientation::from(0x02), + BanknoteOrientation::LeftEdgeFaceUp + ); + assert_eq!( + BanknoteOrientation::from(0x03), + BanknoteOrientation::LeftEdgeFaceDown + ); + + // Check that values outside the base range are parsed as their masked values + for i in 0x04..=u8::MAX { + assert_eq!( + BanknoteOrientation::from(i), + BanknoteOrientation::from(i & BanknoteOrientation::MASK) + ); + } + } + + #[test] + fn test_banknote_classification() { + assert_eq!( + BanknoteClassification::from(0x00), + BanknoteClassification::DisabledOrNotSupported + ); + assert_eq!( + BanknoteClassification::from(0x01), + BanknoteClassification::Unidentified + ); + assert_eq!( + BanknoteClassification::from(0x02), + BanknoteClassification::SuspectedCounterfeit + ); + assert_eq!( + BanknoteClassification::from(0x03), + BanknoteClassification::SuspectedZero + ); + assert_eq!( + BanknoteClassification::from(0x04), + BanknoteClassification::Genuine + ); + + for i in 0x05..=u8::MAX { + assert_eq!( + BanknoteClassification::from(i), + BanknoteClassification::default() + ); + } + } + + #[test] + fn test_ascii_tuples() { + let ascii_table = [ + b' ', b'!', b'"', b'#', b'$', b'%', b'&', b'\'', b'(', b')', b'*', b'+', b',', b'-', + b'.', b'/', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', b';', + b'<', b'=', b'>', b'?', b'@', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', + b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', + b'X', b'Y', b'Z', b'[', b'\\', b']', b'^', b'_', b'`', b'a', b'b', b'c', b'd', b'e', + b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', + b't', b'u', b'v', b'w', b'x', b'y', b'z', b'{', b'|', b'}', b'~', + ]; + + for c in 0..=u8::MAX { + if c >= 32 && c <= 126 { + let ascii_index = (c - 32) as usize; + let ascii_val = ascii_table[ascii_index]; + + // Check that printable ASCII characters are parsed as non-empty strings + assert_eq!(NoteType::from(c).to_string().as_bytes()[0], ascii_val); + assert_eq!(NoteSeries::from(c).to_string().as_bytes()[0], ascii_val); + assert_eq!( + NoteCompatibility::from(c).to_string().as_bytes()[0], + ascii_val + ); + assert_eq!(NoteVersion::from(c).to_string().as_bytes()[0], ascii_val); + } else if c < 128 { + // Check that non-printable ASCII characters are parsed as their unicode values + assert!(!NoteType::from(c).to_string().is_empty()); + assert!(!NoteSeries::from(c).to_string().is_empty()); + assert!(!NoteCompatibility::from(c).to_string().is_empty()); + assert!(!NoteVersion::from(c).to_string().is_empty()); + } else { + // Check that all other values are parsed as an empty string + assert!(NoteType::from(c).to_string().is_empty()); + assert!(NoteSeries::from(c).to_string().is_empty()); + assert!(NoteCompatibility::from(c).to_string().is_empty()); + assert!(NoteVersion::from(c).to_string().is_empty()); + } + } + } +} diff --git a/src/cash.rs b/src/cash.rs new file mode 100644 index 0000000..a7bded6 --- /dev/null +++ b/src/cash.rs @@ -0,0 +1,439 @@ +#[cfg(not(feature = "std"))] +use alloc::string::String; + +use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; + +use crate::std; +use std::fmt; + +use crate::{ + banknote::NoteTableItem, + denomination::{Denomination, StandardDenomination, StandardDenominationFlag}, + method::Method, + OPEN_BRACE, CLOSE_BRACE, +}; + +/// Container for cash inserted into the bill acceptor +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize)] +pub struct CashInsertionEvent { + pub event: Method, + pub amount: u32, +} + +impl CashInsertionEvent { + pub const fn new(event: Method, amount: u32) -> Self { + Self { event, amount } + } + + pub fn event(&self) -> Method { + self.event + } + + pub fn amount(&self) -> u32 { + self.amount + } +} + +impl Default for CashInsertionEvent { + fn default() -> Self { + Self { + event: Method::Accept, + amount: 0, + } + } +} + +impl fmt::Display for CashInsertionEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"event\":\"{}\",\"amount\":{}{CLOSE_BRACE}", + self.event, self.amount + ) + } +} + +impl Serialize for CashInsertionEvent { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut cash_insertion = serializer.serialize_struct("CashInsertionEvent", 2)?; + + cash_insertion.serialize_field("event", &self.event)?; + cash_insertion.serialize_field("amount", &self.amount)?; + + cash_insertion.end() + } +} + +/// ISO 4217 codes: +/// +/// Developers: add more codes as needed +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize)] +#[serde(field_identifier, rename_all = "UPPERCASE")] +pub enum Currency { + /// Australian dollar + AUD = 36, + /// Armenian dram + AMD = 51, + /// Canadian dollar + CAD = 124, + /// Japanese yen + JPY = 392, + /// Euro + EUR = 978, + /// Great British pound + GBP = 826, + /// Mexican peso + MXN = 484, + /// Chinese renminbi + CNY = 156, + /// United States dollar + USD = 840, + /// No currency + XXX = 999, +} + +impl Currency { + /// The length of the ASCII string, not the internal representation. + pub const LEN: usize = 3; + + /// Gets the [Denomination] based on [Currency] and a base [StandardDenomination]. + pub fn denomination_value_base(&self, denom: StandardDenomination) -> Denomination { + let denom_flag: StandardDenominationFlag = denom.into(); + match *self { + Currency::USD => match denom_flag { + StandardDenominationFlag::Denom1 => Denomination::One, + StandardDenominationFlag::Denom2 => Denomination::Two, + StandardDenominationFlag::Denom3 => Denomination::Five, + StandardDenominationFlag::Denom4 => Denomination::Ten, + StandardDenominationFlag::Denom5 => Denomination::Twenty, + StandardDenominationFlag::Denom6 => Denomination::Fifty, + StandardDenominationFlag::Denom7 => Denomination::Hundred, + _ => Denomination::Zero, + }, + Currency::CAD => match denom_flag { + StandardDenominationFlag::Denom2 => Denomination::Five, + StandardDenominationFlag::Denom3 => Denomination::Ten, + StandardDenominationFlag::Denom4 => Denomination::Twenty, + StandardDenominationFlag::Denom5 => Denomination::Fifty, + StandardDenominationFlag::Denom6 => Denomination::Hundred, + _ => Denomination::Zero, + }, + Currency::GBP => match denom_flag { + StandardDenominationFlag::Denom1 => Denomination::One, + StandardDenominationFlag::Denom2 => Denomination::Five, + StandardDenominationFlag::Denom3 => Denomination::Ten, + StandardDenominationFlag::Denom4 => Denomination::Twenty, + StandardDenominationFlag::Denom5 => Denomination::Fifty, + _ => Denomination::Zero, + }, + Currency::AMD => match denom_flag { + StandardDenominationFlag::Denom1 => Denomination::Thousand, + StandardDenominationFlag::Denom2 => Denomination::TwoThousand, + StandardDenominationFlag::Denom3 => Denomination::FiveThousand, + StandardDenominationFlag::Denom4 => Denomination::TenThousand, + StandardDenominationFlag::Denom5 => Denomination::TwentyThousand, + StandardDenominationFlag::Denom6 => Denomination::FiftyThousand, + StandardDenominationFlag::Denom7 => Denomination::HundredThousand, + _ => Denomination::Zero, + }, + _ => Denomination::Zero, + } + } + + /// Gets the [Denomination] based on [Currency] and an extended [NoteTableItem]. + pub fn denomination_value_extended(&self, note: &NoteTableItem) -> Denomination { + let code: &'static str = note.banknote().iso_code().into(); + let curr_str: &'static str = self.into(); + + if curr_str == code { + Denomination::from(note.banknote().value() as u32) + } else { + Denomination::Zero + } + } + + /// Creates the default variant for [Currency]. + pub const fn default() -> Self { + Self::XXX + } +} + +impl From<&str> for Currency { + fn from(currency: &str) -> Self { + match currency { + "USD" => Self::USD, + "CNY" => Self::CNY, + "GBP" => Self::GBP, + "JPY" => Self::JPY, + "EUR" => Self::EUR, + "AUD" => Self::AUD, + "CAD" => Self::CAD, + "MXN" => Self::MXN, + "AMD" => Self::AMD, + _ => Self::XXX, + } + } +} + +impl From<&String> for Currency { + fn from(currency: &String) -> Self { + currency.as_str().into() + } +} + +impl From for Currency { + fn from(currency: String) -> Self { + (¤cy).into() + } +} + +impl From for &'static str { + fn from(c: Currency) -> &'static str { + match c { + Currency::USD => "USD", + Currency::CNY => "CNY", + Currency::GBP => "GBP", + Currency::JPY => "JPY", + Currency::EUR => "EUR", + Currency::AUD => "AUD", + Currency::CAD => "CAD", + Currency::MXN => "MXN", + Currency::AMD => "AMD", + _ => "XXX", + } + } +} + +impl From<&Currency> for &'static str { + fn from(c: &Currency) -> Self { + (*c).into() + } +} + +impl From<&[u8]> for Currency { + fn from(b: &[u8]) -> Self { + if b.len() < Self::LEN { + Self::default() + } else { + let iso = std::str::from_utf8(b[..Self::LEN].as_ref()).unwrap_or("XXX"); + iso.into() + } + } +} + +impl From<[u8; N]> for Currency { + fn from(b: [u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&[u8; N]> for Currency { + fn from(b: &[u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From for Currency { + fn from(b: u16) -> Self { + match b { + 36 => Currency::AUD, + 51 => Currency::AMD, + 124 => Currency::CAD, + 392 => Currency::JPY, + 978 => Currency::EUR, + 826 => Currency::GBP, + 484 => Currency::MXN, + 156 => Currency::CNY, + 840 => Currency::USD, + _ => Currency::XXX, + } + } +} + +impl From for u16 { + fn from(c: Currency) -> Self { + c as u16 + } +} + +impl From<&Currency> for u16 { + fn from(c: &Currency) -> Self { + (*c).into() + } +} + +impl fmt::Display for Currency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +impl Serialize for Currency { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Self::USD => serializer.serialize_unit_variant("Currency", 0, "USD"), + Self::CNY => serializer.serialize_unit_variant("Currency", 1, "CNY"), + Self::GBP => serializer.serialize_unit_variant("Currency", 2, "GBP"), + Self::JPY => serializer.serialize_unit_variant("Currency", 3, "JPY"), + Self::EUR => serializer.serialize_unit_variant("Currency", 4, "EUR"), + Self::AUD => serializer.serialize_unit_variant("Currency", 5, "AUD"), + Self::CAD => serializer.serialize_unit_variant("Currency", 6, "CAD"), + Self::MXN => serializer.serialize_unit_variant("Currency", 7, "MXN"), + Self::AMD => serializer.serialize_unit_variant("Currency", 8, "AMD"), + Self::XXX => serializer.serialize_unit_variant("Currency", 9, "XXX"), + } + } +} + +/// Bill acceptor configuration +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub struct BillAcceptorConfig { + /// The currency being accepted + currency: Currency, +} + +impl BillAcceptorConfig { + /// Create a new BillAcceptorConfig + pub const fn new(currency: Currency) -> Self { + Self { currency } + } + + pub fn currency(&self) -> Currency { + self.currency + } +} + +impl Default for BillAcceptorConfig { + fn default() -> Self { + Self { + currency: Currency::USD, + } + } +} + +impl fmt::Display for BillAcceptorConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{OPEN_BRACE}\"currency\":\"{}\"{CLOSE_BRACE}", self.currency) + } +} + +/// Request to dispense bills +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub struct DispenseRequest { + /// Amount to dispense + amount: u32, + /// Currency to dispense + currency: Currency, +} + +impl DispenseRequest { + /// Create a new DispenseRequest + pub const fn new(amount: u32, currency: Currency) -> Self { + Self { amount, currency } + } +} + +impl fmt::Display for DispenseRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"amount\":{},\"currency\":\"{}\"{CLOSE_BRACE}", + self.amount, self.currency + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, Result}; + + #[test] + fn test_cash_insertion_event_serde() -> Result<()> { + let insertion_event = CashInsertionEvent { + event: Method::Dispense, + amount: 42, + }; + let expected = "{\"event\":\"DISPENSE\",\"amount\":42}"; + + assert_eq!(serde_json::to_string(&insertion_event)?, expected); + assert_eq!( + serde_json::from_str::(expected)?, + insertion_event + ); + + Ok(()) + } + + #[test] + fn test_currency_serde() -> Result<()> { + assert_eq!(serde_json::to_string(&Currency::USD)?, "\"USD\""); + assert_eq!(serde_json::to_string(&Currency::CNY)?, "\"CNY\""); + assert_eq!(serde_json::to_string(&Currency::GBP)?, "\"GBP\""); + assert_eq!(serde_json::to_string(&Currency::JPY)?, "\"JPY\""); + assert_eq!(serde_json::to_string(&Currency::AUD)?, "\"AUD\""); + assert_eq!(serde_json::to_string(&Currency::EUR)?, "\"EUR\""); + assert_eq!(serde_json::to_string(&Currency::CAD)?, "\"CAD\""); + assert_eq!(serde_json::to_string(&Currency::MXN)?, "\"MXN\""); + assert_eq!(serde_json::to_string(&Currency::AMD)?, "\"AMD\""); + assert_eq!(serde_json::to_string(&Currency::XXX)?, "\"XXX\""); + + assert_eq!(serde_json::from_str::("\"USD\"")?, Currency::USD); + assert_eq!(serde_json::from_str::("\"CNY\"")?, Currency::CNY); + assert_eq!(serde_json::from_str::("\"GBP\"")?, Currency::GBP); + assert_eq!(serde_json::from_str::("\"JPY\"")?, Currency::JPY); + assert_eq!(serde_json::from_str::("\"EUR\"")?, Currency::EUR); + assert_eq!(serde_json::from_str::("\"AUD\"")?, Currency::AUD); + assert_eq!(serde_json::from_str::("\"CAD\"")?, Currency::CAD); + assert_eq!(serde_json::from_str::("\"MXN\"")?, Currency::MXN); + assert_eq!(serde_json::from_str::("\"AMD\"")?, Currency::AMD); + assert_eq!(serde_json::from_str::("\"XXX\"")?, Currency::XXX); + + assert_eq!(Currency::from("WHaT_a_W3ird_CurRen$Y"), Currency::XXX); + + Ok(()) + } + + #[test] + fn test_bill_acceptor_config_serde() -> Result<()> { + let acceptor_cfg = BillAcceptorConfig { + currency: Currency::USD, + }; + let expected = "{\"currency\":\"USD\"}"; + + assert_eq!(serde_json::to_string(&acceptor_cfg)?, expected); + assert_eq!( + serde_json::from_str::(expected)?, + acceptor_cfg + ); + + Ok(()) + } + + #[test] + fn test_dispense_request_serde() -> Result<()> { + let dispense_req = DispenseRequest { + amount: 42, + currency: Currency::USD, + }; + let expected = "{\"amount\":42,\"currency\":\"USD\"}"; + + assert_eq!(serde_json::to_string(&dispense_req)?, expected); + assert_eq!( + serde_json::from_str::(expected)?, + dispense_req + ); + + Ok(()) + } +} diff --git a/src/clear_audit_data.rs b/src/clear_audit_data.rs new file mode 100644 index 0000000..2063039 --- /dev/null +++ b/src/clear_audit_data.rs @@ -0,0 +1,5 @@ +mod reply; +mod request; + +pub use reply::*; +pub use request::*; diff --git a/src/clear_audit_data/reply.rs b/src/clear_audit_data/reply.rs new file mode 100644 index 0000000..a634c12 --- /dev/null +++ b/src/clear_audit_data/reply.rs @@ -0,0 +1,269 @@ +use crate::std; +use std::fmt; + +use crate::{ + bool_enum, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply, + len::{CLEAR_AUDIT_DATA_REQUEST_ACK, CLEAR_AUDIT_DATA_REQUEST_RESULTS}, + ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, OmnibusReplyOps, +}; + +bool_enum!( + ClearAuditAckNak, + r"Whether the device is able to perform the Clear Audit Data Request." +); + +bool_enum!( + ClearAuditPassFail, + r"Whether the device successfully proccessed the Clear Audit Data Request." +); + +mod index { + pub const ACKNAK: usize = 10; + pub const PASS_FAIL: usize = 10; +} + +/// Clear Audit Data - Request Acknowledgement (Subtype 0x1D) +/// +/// The [ClearAuditDataRequestAck] reply is an immediate response to a request to perform a clear of the audit data on the SC Advanced. +/// +/// Since the command needs to clear large sections of memory, the command may take a few seconds to +/// complete. +/// +/// The device will inform the host that the operation is complete by posting back a completion +/// response at later time. However, the device will post an acknowledgement of the host request immediately +/// +/// The Clear Audit Data Request Acknowledgement is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ACK/NAK | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | +/// | Value | 0x02 | 0x0D | 0x7n | 0x1D | nn | nn | nn | nn | nn | nn | 0x00/01 | 0x03 | zz | +/// +/// If for any reason the device is unable to honor the host request, a NAK will be sent to the host +/// (represented by 0x00 for byte 10). The device will NAK the host in the following situations: +/// +/// * Device is in power up mode. +/// * A current transaction underway. If a document has been inserted and is being processed, the +/// device will NAK all Clear Audit Data Request +/// * Device is in calibration mode. +/// * The device is currently servicing another type 7 message request. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ClearAuditDataRequestAck { + buf: [u8; CLEAR_AUDIT_DATA_REQUEST_ACK], +} + +impl ClearAuditDataRequestAck { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; CLEAR_AUDIT_DATA_REQUEST_ACK], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::ClearAuditDataRequest); + + message + } + + /// Gets the ACKNAK data field. + /// + /// If for any reason the device is unable to honor the host request, a NAK will be sent to the host + /// (represented by 0x00 for byte 10). The device will NAK the host in the following situations: + /// + /// - Device is in power up mode. + /// - A current transaction underway. If a document has been inserted and is being processed, the device will NAK all Clear Audit Data Request + /// - Device is in calibration mode. + /// - The device is currently servicing another type 7 message request. + pub fn audit_acknak(&self) -> ClearAuditAckNak { + self.buf[index::ACKNAK].into() + } +} + +impl_default!(ClearAuditDataRequestAck); +impl_message_ops!(ClearAuditDataRequestAck); +impl_omnibus_extended_reply!(ClearAuditDataRequestAck); +impl_extended_ops!(ClearAuditDataRequestAck); + +impl fmt::Display for ClearAuditDataRequestAck { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Subtype: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, AuditAckNak: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.extended_command(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + self.audit_acknak(), + ) + } +} + +/// Clear Audit Data - Request Results (Subtype 0x1D) +/// +/// The [ClearAuditDataRequestResults] reply contains the results of the clear audit data process on the SC Advanced. +/// +/// If the device ACKs the original request, it will process that request and issue a completion response. +/// +/// This response will be issued as a reply to a general host omnibus command. The message will contain a data +/// byte that will tell the host if the operation passed or failed. +/// +/// The Clear Audit Data Request Results is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | Pass/Fail | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:---------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | +/// | Value | 0x02 | 0x0D | 0x7n | 0x1D | nn | nn | nn | nn | nn | nn | 0x00/01 | 0x03 | zz | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ClearAuditDataRequestResults { + buf: [u8; CLEAR_AUDIT_DATA_REQUEST_RESULTS], +} + +impl ClearAuditDataRequestResults { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; CLEAR_AUDIT_DATA_REQUEST_RESULTS], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::ClearAuditDataRequest); + + message + } + + /// Gets the Pass/Fail data field. + pub fn pass_fail(&self) -> ClearAuditPassFail { + self.buf[index::PASS_FAIL].into() + } +} + +impl_default!(ClearAuditDataRequestResults); +impl_message_ops!(ClearAuditDataRequestResults); +impl_omnibus_extended_reply!(ClearAuditDataRequestResults); +impl_extended_ops!(ClearAuditDataRequestResults); + +impl fmt::Display for ClearAuditDataRequestResults { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Subtype: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, PassFail: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.extended_command(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + self.pass_fail(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_clear_audit_data_request_ack_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0d, 0x70, 0x1d, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ACK/NAK + 0x01, + // ETX | Checksum + 0x03, 0x61, + ]; + + let mut msg = ClearAuditDataRequestAck::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!( + msg.extended_command(), + ExtendedCommand::ClearAuditDataRequest + ); + assert_eq!(msg.audit_acknak(), ClearAuditAckNak::Set); + + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0d, 0x70, 0x1d, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ACK/NAK + 0x00, + // ETX | Checksum + 0x03, 0x60, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!( + msg.extended_command(), + ExtendedCommand::ClearAuditDataRequest + ); + assert_eq!(msg.audit_acknak(), ClearAuditAckNak::Unset); + + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_clear_audit_data_request_results_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0d, 0x70, 0x1d, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Pass/Fail + 0x11, + // ETX | Checksum + 0x03, 0x71, + ]; + + let mut msg = ClearAuditDataRequestResults::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!( + msg.extended_command(), + ExtendedCommand::ClearAuditDataRequest + ); + assert_eq!(msg.pass_fail(), ClearAuditPassFail::Set); + + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0d, 0x70, 0x1d, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Pass/Fail + 0x10, + // ETX | Checksum + 0x03, 0x70, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!( + msg.extended_command(), + ExtendedCommand::ClearAuditDataRequest + ); + assert_eq!(msg.pass_fail(), ClearAuditPassFail::Unset); + + Ok(()) + } +} diff --git a/src/clear_audit_data/request.rs b/src/clear_audit_data/request.rs new file mode 100644 index 0000000..312164c --- /dev/null +++ b/src/clear_audit_data/request.rs @@ -0,0 +1,73 @@ +use crate::{ + impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::CLEAR_AUDIT_DATA_REQUEST, ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, +}; + +/// Clear Audit Data - Request (Subtype 0x1D) +/// +/// The [ClearAuditDataRequest] command allows the host to perform a clear of the audit data on the SC Advanced. +/// +/// This command will clear all audit information except for the lifetime audit section; these will +/// be protected and will not be cleared by this command. +/// +/// The Clear Audit Data Request is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +/// | Value | 0x02 | 0x09 | 0x7n | 0x1D | nn | nn | nn | 0x03 | zz | +/// +/// Since the command needs to clear large sections of memory, the command may take a few seconds to +/// complete. +pub struct ClearAuditDataRequest { + buf: [u8; CLEAR_AUDIT_DATA_REQUEST], +} + +impl ClearAuditDataRequest { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; CLEAR_AUDIT_DATA_REQUEST], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::ClearAuditDataRequest); + + message + } +} + +impl_default!(ClearAuditDataRequest); +impl_message_ops!(ClearAuditDataRequest); +impl_extended_ops!(ClearAuditDataRequest); +impl_omnibus_extended_command!(ClearAuditDataRequest); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_clear_audit_data_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x09, 0x70, 0x1d, + // Data + 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x64, + ]; + + let mut msg = ClearAuditDataRequest::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!( + msg.extended_command(), + ExtendedCommand::ClearAuditDataRequest + ); + + Ok(()) + } +} diff --git a/src/denomination.rs b/src/denomination.rs new file mode 100644 index 0000000..1656229 --- /dev/null +++ b/src/denomination.rs @@ -0,0 +1,266 @@ +#[cfg(not(feature = "std"))] +use alloc::string::String; + +use crate::std; +use std::fmt; + +/// Cash denominations +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Denomination { + Zero = 0, + One = 1, + Two = 2, + Five = 5, + Ten = 10, + Twenty = 20, + Fifty = 50, + Hundred = 100, + TwoHundred = 200, + FiveHundred = 500, + Thousand = 1000, + TwoThousand = 2000, + FiveThousand = 5000, + TenThousand = 10_000, + TwentyThousand = 20_000, + FiftyThousand = 50_000, + HundredThousand = 100_000, +} + +impl From for &'static str { + fn from(d: Denomination) -> Self { + match d { + Denomination::Zero => "Zero", + Denomination::One => "One", + Denomination::Two => "Two", + Denomination::Five => "Five", + Denomination::Ten => "Ten", + Denomination::Twenty => "Twenty", + Denomination::Fifty => "Fifty", + Denomination::Hundred => "Hundred", + Denomination::TwoHundred => "Two hundred", + Denomination::FiveHundred => "Five hundred", + Denomination::Thousand => "Thousand", + Denomination::TwoThousand => "Two thousand", + Denomination::FiveThousand => "Five thousand", + Denomination::TenThousand => "Ten thousand", + Denomination::TwentyThousand => "Twenty thousand", + Denomination::FiftyThousand => "Fifty thousand", + Denomination::HundredThousand => "Hundred thousand", + } + } +} + +impl From<&Denomination> for &'static str { + fn from(d: &Denomination) -> Self { + (*d).into() + } +} + +impl fmt::Display for Denomination { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +impl From for Denomination { + fn from(digit: u32) -> Self { + match digit { + 0 => Self::Zero, + 1 => Self::One, + 2 => Self::Two, + 5 => Self::Five, + 10 => Self::Ten, + 20 => Self::Twenty, + 50 => Self::Fifty, + 100 => Self::Hundred, + 200 => Self::TwoHundred, + 500 => Self::FiveHundred, + 1000 => Self::Thousand, + 2000 => Self::TwoThousand, + 5000 => Self::FiveThousand, + 10_000 => Self::TenThousand, + 20_000 => Self::TwentyThousand, + 50_000 => Self::FiftyThousand, + 100_000 => Self::HundredThousand, + _ => Self::Zero, + } + } +} + +impl From for u32 { + fn from(d: Denomination) -> Self { + d as u32 + } +} + +bitfield! { + /// Enable/disable note denominations while in base note mode + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct StandardDenomination(u8); + u8; + pub one, set_one: 0; + pub two, set_two: 1; + pub three, set_three: 2; + pub four, set_four: 3; + pub five, set_five: 4; + pub six, set_six: 5; + pub seven, set_seven: 6; +} + +mod bitmask { + pub const DENOMINATION: u8 = 0b111_1111; +} + +impl StandardDenomination { + /// Creates a [Denomination] with all denominations set. + pub const fn all() -> Self { + Self(bitmask::DENOMINATION) + } + + /// Creates a [Denomination] with no denominations set. + pub const fn none() -> Self { + Self(0) + } + + /// Converts from the [ExceptionStatus](crate::status::ExceptionStatus) `note_value` field. + pub const fn from_note_value(note_value: u8) -> Self { + match note_value { + 0b000 => Self::none(), + 0b001..=0b111 => Self(1 << (note_value - 1)), + _ => Self::none(), + } + } + + /// Sets all denomintations. + pub fn set_all(&mut self) { + self.0 |= bitmask::DENOMINATION; + } + + /// Inverts all the denomination bits. + pub fn set_inverted(&mut self) { + self.0 ^= bitmask::DENOMINATION; + } + + /// Inverts all the denomination bits. + pub fn invert(&self) -> Self { + Self(self.0 ^ bitmask::DENOMINATION) + } +} + +fn denom_delimiter(has_denom: bool) -> &'static str { + if has_denom { + "," + } else { + "" + } +} + +impl fmt::Display for StandardDenomination { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut dis = String::new(); + let mut has_denom = false; + + if self.one() { + dis += "Denom1"; + has_denom = true; + } + if self.two() { + dis = dis + denom_delimiter(has_denom) + "Denom2"; + has_denom = true; + } + if self.three() { + dis = dis + denom_delimiter(has_denom) + "Denom3"; + has_denom = true; + } + if self.four() { + dis = dis + denom_delimiter(has_denom) + "Denom4"; + has_denom = true; + } + if self.five() { + dis = dis + denom_delimiter(has_denom) + "Denom5"; + has_denom = true; + } + if self.six() { + dis = dis + denom_delimiter(has_denom) + "Denom6"; + has_denom = true; + } + if self.seven() { + dis = dis + denom_delimiter(has_denom) + "Denom7"; + has_denom = true; + } + + if has_denom { + write!(f, "{}", dis) + } else { + write!(f, "None") + } + } +} + +impl From for u8 { + fn from(d: StandardDenomination) -> Self { + d.0 + } +} + +impl From<&StandardDenomination> for u8 { + fn from(d: &StandardDenomination) -> Self { + d.0 + } +} + +impl From for StandardDenomination { + fn from(b: u8) -> Self { + Self(b & bitmask::DENOMINATION) + } +} + +impl From for StandardDenomination { + fn from(f: StandardDenominationFlag) -> Self { + Self(f as u8) + } +} + +/// Bit flags for [StandardDenomination]s. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum StandardDenominationFlag { + Denom1 = 0b000_0001, + Denom2 = 0b000_0010, + Denom3 = 0b000_0100, + Denom4 = 0b000_1000, + Denom5 = 0b001_0000, + Denom6 = 0b010_0000, + Denom7 = 0b100_0000, + Zero = 0b000_0000, +} + +impl StandardDenominationFlag { + pub const fn default() -> Self { + Self::Zero + } +} + +impl From for StandardDenominationFlag { + fn from(d: StandardDenomination) -> Self { + // matches lowest value first + if d.one() { + Self::Denom1 + } else if d.two() { + Self::Denom2 + } else if d.three() { + Self::Denom3 + } else if d.four() { + Self::Denom4 + } else if d.five() { + Self::Denom5 + } else if d.six() { + Self::Denom6 + } else if d.seven() { + Self::Denom7 + } else { + Self::Zero + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..40358c8 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,533 @@ +#[cfg(not(feature = "std"))] +use alloc::string::String; + +use crate::std; +use std::{fmt, result}; + +use serde::{Deserialize, Serialize}; + +pub type Result = result::Result; +pub type JsonRpcResult = result::Result; + +/// Result status for HAL function calls +#[repr(u16)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum HalResult { + Success = 0, + Failure = 1, + LogicFailure = 2, + RuntimeFailure = 3, + InvalidArgumentFailure = 4, + OutOfRangeFailure = 5, + NakResponse = 6, + IllegalCall = 7, + AsyncInterrupted = 8, + InsufficientException = 9, + TimeOut = 10, + ErrorInvalidArguments = 4105, + InvalidHandle = 4112, + IncompleteArguments = 4113, +} + +impl From for HalResult { + fn from(res: u16) -> Self { + match res { + 0 => Self::Success, + 1 => Self::Failure, + 2 => Self::LogicFailure, + 3 => Self::RuntimeFailure, + 4 => Self::InvalidArgumentFailure, + 5 => Self::OutOfRangeFailure, + 6 => Self::NakResponse, + 7 => Self::IllegalCall, + 8 => Self::AsyncInterrupted, + 9 => Self::InsufficientException, + 10 => Self::TimeOut, + 4105 => Self::ErrorInvalidArguments, + 4112 => Self::InvalidHandle, + 4113 => Self::IncompleteArguments, + _ => Self::Failure, + } + } +} + +impl From for &'static str { + fn from(res: HalResult) -> Self { + match res { + HalResult::Success => "success", + HalResult::Failure => "failure", + HalResult::LogicFailure => "logic failure", + HalResult::RuntimeFailure => "runtime failure", + HalResult::InvalidArgumentFailure => "invalid argument failure", + HalResult::OutOfRangeFailure => "out of range failure", + HalResult::NakResponse => "NAK response", + HalResult::IllegalCall => "illegal call", + HalResult::AsyncInterrupted => "async interrupted", + HalResult::InsufficientException => "insufficient exception", + HalResult::TimeOut => "time out", + HalResult::ErrorInvalidArguments => "error invalid arguments", + HalResult::InvalidHandle => "invalid handle", + HalResult::IncompleteArguments => "incomplete arguments", + } + } +} + +impl From<&HalResult> for &'static str { + fn from(res: &HalResult) -> &'static str { + (*res).into() + } +} + +impl fmt::Display for HalResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = <&'static str>::from(self); + let code = (*self) as u16; + + write!(f, "{s}: {code}") + } +} + +/// HAL error codes +#[repr(i16)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum HalError { + LicenseKeyAuthFailure = -18, + BillPresentInEscrow = -15, + BillNotPresentInEscrow = -14, + BillReject = -13, + PrintImageErr = -12, + OpenSerialPortFailure = -11, + BufferOverflow = -10, + DeviceTimeout = -9, + LibraryInternalErr = -8, + DeviceAlreadyOpen = -7, + DeviceNotReady = -6, + Cancelled = -5, + InvalidData = -3, + DeviceBusy = -2, + DeviceFailure = -1, + RequestFieldInvalid = 1, + InternalErr = 2, + InternalValidationErr = 3, + ComponentNotImplemented = 4, + PreconditionFailed = 5, + ApplicationTimeout = 6, + InvalidDenomination = 7, + ApplicationBusy = 8, + DeviceCommErr = 9, + FirmwareErr = 10, + PhysicalTamper = 11, + SystemErr = 12, + MethodNotImplemented = 13, + DecodingErr = 14, +} + +impl From for &'static str { + fn from(err: HalError) -> Self { + match err { + HalError::LicenseKeyAuthFailure => "license key auth failure", + HalError::BillPresentInEscrow => "bill present in escrow", + HalError::BillNotPresentInEscrow => "bill not present in escrow", + HalError::BillReject => "bill reject", + HalError::PrintImageErr => "print image err", + HalError::OpenSerialPortFailure => "open serial port failure", + HalError::BufferOverflow => "buffer overflow", + HalError::DeviceTimeout => "device timeout", + HalError::LibraryInternalErr => "library internal err", + HalError::DeviceAlreadyOpen => "device already open", + HalError::DeviceNotReady => "device not ready", + HalError::Cancelled => "cancelled", + HalError::InvalidData => "invalid data", + HalError::DeviceBusy => "device busy", + HalError::DeviceFailure => "device failure", + HalError::RequestFieldInvalid => "request field invalid", + HalError::InternalErr => "internal err", + HalError::InternalValidationErr => "internal validation err", + HalError::ComponentNotImplemented => "component not implemented", + HalError::PreconditionFailed => "precondition failed", + HalError::ApplicationTimeout => "application timeout", + HalError::InvalidDenomination => "invalid denomination", + HalError::ApplicationBusy => "application busy", + HalError::DeviceCommErr => "device comm err", + HalError::FirmwareErr => "firmware err", + HalError::PhysicalTamper => "physical tamper", + HalError::SystemErr => "system err", + HalError::MethodNotImplemented => "method not implemented", + HalError::DecodingErr => "decoding err", + } + } +} + +impl From<&HalError> for &'static str { + fn from(err: &HalError) -> Self { + (*err).into() + } +} + +impl fmt::Display for HalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s: &'static str = self.into(); + let code = (*self) as i32; + + write!(f, "{s}: {code}") + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum JsonRpcErrorCode { + HalResult(HalResult), + HalError(HalError), + IoError(String), + JsonError(String), + SerialError(String), + GenericError(i64), + Stop, +} + +impl From for JsonRpcErrorCode { + fn from(err: HalResult) -> Self { + Self::HalResult(err) + } +} + +impl From for JsonRpcErrorCode { + fn from(err: HalError) -> Self { + Self::HalError(err) + } +} + +#[cfg(feature = "std")] +impl From for JsonRpcErrorCode { + fn from(err: std::io::Error) -> Self { + Self::IoError(format!("{err}")) + } +} + +impl From for JsonRpcErrorCode { + fn from(err: serde_json::Error) -> Self { + Self::JsonError(format!("{err}")) + } +} + +impl From for JsonRpcErrorCode { + fn from(err: i64) -> Self { + Self::GenericError(err) + } +} + +impl fmt::Display for JsonRpcErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::HalResult(err) => write!(f, "Hal result: {err}"), + Self::HalError(err) => write!(f, "Hal error: {err}"), + Self::IoError(err) => write!(f, "I/O error: {err}"), + Self::JsonError(err) => write!(f, "JSON error: {err}"), + Self::SerialError(err) => write!(f, "Serial error: {err}"), + Self::GenericError(err) => write!(f, "Generic error: {err}"), + Self::Stop => write!(f, "Stop"), + } + } +} + +/// Basic error type for JSON-RPC messages +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct JsonRpcError { + /// Error code (if present) + pub(crate) code: JsonRpcErrorCode, + /// Error message (if present) + pub(crate) message: String, +} + +impl JsonRpcError { + /// Create a JsonRpcError + pub fn new(code: C, message: S) -> Self + where + C: Into, + S: Into, + { + Self { + code: code.into(), + message: message.into(), + } + } + + /// Create a JsonRpcError with a generic failure code + pub fn failure(message: S) -> Self + where + S: Into, + { + Self::new(-1i64, message) + } + + pub fn stop() -> Self { + Self::new(JsonRpcErrorCode::Stop, "") + } + + /// Get the error code + pub fn code(&self) -> &JsonRpcErrorCode { + &self.code + } + + /// Set the error code + pub fn set_code(&mut self, code: C) + where + C: Into, + { + self.code = code.into(); + } + + /// Get the error message + pub fn message(&self) -> &str { + &self.message + } + + /// Set the error message + pub fn set_message(&mut self, message: S) + where + S: Into, + { + self.message = message.into(); + } +} + +impl fmt::Display for JsonRpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "code[{}]: {}", self.code, self.message) + } +} + +impl From for JsonRpcError { + fn from(res: HalResult) -> Self { + match res { + HalResult::Success => Self { + code: res.into(), + message: String::new(), + }, + HalResult::InvalidHandle => Self { + code: HalError::PreconditionFailed.into(), + message: "Device handle is invalid".into(), + }, + HalResult::ErrorInvalidArguments => Self { + code: HalError::InternalValidationErr.into(), + message: "One of the passed arguments is NULL".into(), + }, + HalResult::IncompleteArguments => Self { + code: HalError::InternalValidationErr.into(), + message: "One of the passed arguments is incomplete (wrong length)".into(), + }, + hal_result => Self { + code: hal_result.into(), + message: String::new(), + }, + } + } +} + +#[cfg(feature = "std")] +impl From for std::io::Error { + fn from(err: JsonRpcError) -> Self { + Self::new(std::io::ErrorKind::Other, format!("{err}")) + } +} + +#[cfg(feature = "std")] +impl From for JsonRpcError { + fn from(err: std::io::Error) -> Self { + Self { + code: err.into(), + message: String::new(), + } + } +} + +impl From for JsonRpcError { + fn from(err: serde_json::Error) -> Self { + Self { + code: err.into(), + message: String::new(), + } + } +} + +impl From for JsonRpcError { + fn from(err: std::str::Utf8Error) -> Self { + Self::failure(format!("{err}")) + } +} + +/// Basic error type for serial communication +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] +pub struct Error { + code: ErrorCode, + message: String, +} + +impl Error { + /// Create a generic failure Error + pub fn failure(message: S) -> Self + where + S: Into, + { + Self { + code: ErrorCode::Failure, + message: message.into(), + } + } + + /// Create a serial port failure Error + pub fn serial(message: S) -> Self + where + S: Into, + { + Self { + code: ErrorCode::SerialPort, + message: message.into(), + } + } + + /// Create a JSON-RPC failure Error + pub fn json_rpc(message: S) -> Self + where + S: Into, + { + Self { + code: ErrorCode::JsonRpc, + message: message.into(), + } + } + + /// Get the error code + pub fn code(&self) -> ErrorCode { + self.code + } + + /// Get the error message + pub fn message(&self) -> &str { + self.message.as_str() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "code: {}, message: {}", self.code, self.message) + } +} + +#[cfg(feature = "std")] +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self { + code: ErrorCode::Failure, + message: format!("I/O error: {}", err), + } + } +} + +impl From for Error { + fn from(err: std::str::Utf8Error) -> Self { + Self { + code: ErrorCode::Failure, + message: format!("Utf8 error: {}", err), + } + } +} + +impl From for Error { + fn from(err: serialport::Error) -> Self { + Self { + code: ErrorCode::SerialPort, + message: format!("Serial port error: {err}"), + } + } +} + +#[cfg(feature = "std")] +impl From> for Error { + fn from(err: std::sync::mpsc::SendError) -> Self { + Self::failure(format!("failed to send an item to the queue: {err}")) + } +} + +impl From<&Error> for JsonRpcError { + fn from(err: &Error) -> Self { + Self::new(err.code(), err.message()) + } +} + +impl From for JsonRpcError { + fn from(err: Error) -> Self { + Self::from(&err) + } +} + +impl From for Error { + fn from(err: JsonRpcError) -> Self { + Self::from(&err) + } +} + +impl From<&JsonRpcError> for Error { + fn from(err: &JsonRpcError) -> Self { + Self::json_rpc(format!("code[{}]: {}", err.code(), err.message())) + } +} + +/// Error codes for returned errors from acceptor device +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +// FIXME: fill out with more error codes +pub enum ErrorCode { + /// Generic failure code + Failure = -1, + /// Failure code originating from the serial port connection + SerialPort = -2, + /// JSON-RPC failure code + JsonRpc = -3, +} + +impl From for &'static str { + fn from(e: ErrorCode) -> Self { + match e { + ErrorCode::Failure => "failure", + ErrorCode::SerialPort => "serial port", + ErrorCode::JsonRpc => "JSON-RPC", + } + } +} + +impl From<&ErrorCode> for &'static str { + fn from(e: &ErrorCode) -> Self { + (*e).into() + } +} + +impl From for JsonRpcErrorCode { + fn from(e: ErrorCode) -> Self { + JsonRpcErrorCode::SerialError(format!("{e}")) + } +} + +impl From<&ErrorCode> for JsonRpcErrorCode { + fn from(e: &ErrorCode) -> Self { + (*e).into() + } +} + +impl From for ErrorCode { + fn from(_e: JsonRpcErrorCode) -> Self { + Self::JsonRpc + } +} + +impl From<&JsonRpcErrorCode> for ErrorCode { + fn from(_e: &JsonRpcErrorCode) -> Self { + Self::JsonRpc + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} diff --git a/src/extended_command.rs b/src/extended_command.rs new file mode 100644 index 0000000..efd4b2d --- /dev/null +++ b/src/extended_command.rs @@ -0,0 +1,96 @@ +use crate::std; +use std::fmt; + +use crate::MessageOps; + +/// Extended Commands (Type 7): The extended commands utilize message type 7 to provide functionality outside of the standard +/// omnibus commands. The use of message type 7 is complicated by the fact that it can be used by either +/// the host or device at anytime. +/// +/// Developers: add additional types from the specification as needed +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ExtendedCommand { + ExtendedBarcodeReply = 0x1, + ExtendedNoteSpecification = 0x2, + SetExtendedNoteInhibits = 0x3, + SetEscrowTimeout = 0x4, + QueryValueTable = 0x6, + NoteRetrieved = 0xb, + AdvancedBookmark = 0xd, + ClearAuditDataRequest = 0x1d, + Reserved = 0xff, +} + +impl From for ExtendedCommand { + fn from(b: u8) -> Self { + match b { + 0x1 => ExtendedCommand::ExtendedBarcodeReply, + 0x2 => ExtendedCommand::ExtendedNoteSpecification, + 0x3 => ExtendedCommand::SetExtendedNoteInhibits, + 0x4 => ExtendedCommand::SetEscrowTimeout, + 0x6 => ExtendedCommand::QueryValueTable, + 0xb => ExtendedCommand::NoteRetrieved, + 0xd => ExtendedCommand::AdvancedBookmark, + 0x1d => ExtendedCommand::ClearAuditDataRequest, + // Missing values are either specified and unneeded, or unspecified and RFU + _ => ExtendedCommand::Reserved, + } + } +} + +impl From for u8 { + fn from(e: ExtendedCommand) -> Self { + e as u8 + } +} + +impl From<&ExtendedCommand> for u8 { + fn from(e: &ExtendedCommand) -> Self { + (*e).into() + } +} + +impl From for &'static str { + fn from(e: ExtendedCommand) -> Self { + match e { + ExtendedCommand::ExtendedBarcodeReply => "ExtendedBarcodeReply", + ExtendedCommand::ExtendedNoteSpecification => "ExtendedNoteSpecification", + ExtendedCommand::SetExtendedNoteInhibits => "SetExtendedNoteInhibits", + ExtendedCommand::SetEscrowTimeout => "SetEscrowTimeout / ExtendedCoupon", + ExtendedCommand::QueryValueTable => "QueryValueTable", + ExtendedCommand::NoteRetrieved => "NoteRetrieved", + ExtendedCommand::AdvancedBookmark => "AdvancedBookmark", + ExtendedCommand::ClearAuditDataRequest => "ClearAuditDataRequest", + ExtendedCommand::Reserved => "Reserved", + } + } +} + +impl From<&ExtendedCommand> for &'static str { + fn from(e: &ExtendedCommand) -> Self { + (*e).into() + } +} + +impl fmt::Display for ExtendedCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +mod index { + pub const SUBTYPE: usize = 3; +} + +pub trait ExtendedCommandOps: MessageOps { + /// Get the extended command sub-type + fn extended_command(&self) -> ExtendedCommand { + self.buf()[index::SUBTYPE].into() + } + + /// Set the extended command sub-type + fn set_extended_command(&mut self, ext_cmd: ExtendedCommand) { + self.buf_mut()[index::SUBTYPE] = ext_cmd.into(); + } +} diff --git a/src/extended_note_inhibits.rs b/src/extended_note_inhibits.rs new file mode 100644 index 0000000..8cb4eec --- /dev/null +++ b/src/extended_note_inhibits.rs @@ -0,0 +1,5 @@ +mod command; +mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/extended_note_inhibits/command.rs b/src/extended_note_inhibits/command.rs new file mode 100644 index 0000000..e511bc1 --- /dev/null +++ b/src/extended_note_inhibits/command.rs @@ -0,0 +1,314 @@ +use crate::std; + +use crate::{ + error::{Error, Result}, + impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::SET_EXTENDED_NOTE_INHIBITS_BASE, + ExtendedCommand, ExtendedCommandOps, ExtendedNoteReporting, MessageOps, MessageType, + OmnibusCommandOps, +}; + +/// CFSC device extended note enable byte length, see section 7.5.3 +pub const CFSC_ENABLE_LEN: usize = 8; +/// SC device extended note enable byte length, see section 7.5.3 +pub const SC_ENABLE_LEN: usize = 19; + +mod bitmask { + pub const ENABLE_NOTE: u8 = 0b111_1111; +} + +mod index { + pub const ENABLE_NOTE: usize = 7; +} + +bitfield! { + /// Represents enabled notes in the extended note table. + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct EnableNote(u8); + u8; + pub note1, set_note1: 0; + pub note2, set_note2: 1; + pub note3, set_note3: 2; + pub note4, set_note4: 3; + pub note5, set_note5: 4; + pub note6, set_note6: 5; + pub note7, set_note7: 6; +} + +impl EnableNote { + pub const LEN: usize = 7; + + /// Creates an [EnableNote] with no bits set. + pub const fn none() -> Self { + Self(0) + } + + /// Creates an [EnableNote] with all bits set. + pub const fn all() -> Self { + Self(bitmask::ENABLE_NOTE) + } + + /// Get the length of the [EnableNote] bitfield. + pub const fn len() -> usize { + Self::LEN + } + + /// Sets an index to enable. + /// + /// Valid range is [1, 7] (inclusive). + pub fn set_index(&mut self, index: usize) -> Result<()> { + match index { + 1 => self.set_note1(true), + 2 => self.set_note2(true), + 3 => self.set_note3(true), + 4 => self.set_note4(true), + 5 => self.set_note5(true), + 6 => self.set_note6(true), + 7 => self.set_note7(true), + _ => return Err(Error::failure("invalid enable index")), + } + Ok(()) + } +} + +impl From<&[bool]> for EnableNote { + fn from(b: &[bool]) -> Self { + let mut inner = 0u8; + // only allow a max of + let end = std::cmp::min(b.len(), Self::len()); + for (i, &set) in b[..end].iter().enumerate() { + let bit = if set { 1 } else { 0 }; + inner |= bit << i; + } + Self(inner) + } +} + +impl From<&[bool; N]> for EnableNote { + fn from(b: &[bool; N]) -> Self { + b.as_ref().into() + } +} + +impl From<[bool; N]> for EnableNote { + fn from(b: [bool; N]) -> Self { + (&b).into() + } +} + +impl From for EnableNote { + fn from(b: u8) -> Self { + Self(b & bitmask::ENABLE_NOTE) + } +} + +impl From<&EnableNote> for u8 { + fn from(e: &EnableNote) -> u8 { + e.0 + } +} + +impl From for u8 { + fn from(e: EnableNote) -> u8 { + (&e).into() + } +} + +/// Set Extended Note Inhibits - Request (Subtype 0x03) +/// +/// This command is used to control the acceptance of bank notes on a note type basis. It is only used when +/// the device is running in extended note mode (section 4.2.2). +/// +/// The generic parameter is the sum of +/// [SET_EXTENDED_NOTE_INHIBITS_BASE](crate::len::SET_EXTENDED_NOTE_INHIBITS_BASE), +/// and the number of enable note bytes (either [CFSC_ENABLE_LEN] or [SC_ENABLE_LEN]). +/// +/// The Set Extended Note Inhibits is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Enable 1 | ... | Enable N | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:--------:|:------:|:--------:|:------:|:------:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... | LL - 3 | LL - 2 | LL - 1 | +/// | Value | 0x02 | LL | 0x7n | 0x03 | nn | nn | nn | nn | nn | nn | 0x03 | zz | +/// +/// | **CFSC** | +/// |:--------:| +/// +/// Supports up to 50 denomination types, therefore the command requires 8 extended data bytes. +/// +/// This will make the message length 0x11: +/// +/// | Byte | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | +/// |:---------|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| +/// | Enable 1 | Note 7 | Note 6 | Note 5 | Note 4 | Note 3 | Note 2 | Note 1 | +/// | Enable 2 | Note 14 | Note 13 | Note 12 | Note 11 | Note 10 | Note 9 | Note 8 | +/// | Enable 3 | Note 21 | Note 20 | Note 19 | Note 18 | Note 17 | Note 16 | Note 15 | +/// | Enable 4 | Note 28 | Note 27 | Note 26 | Note 25 | Note 24 | Note 23 | Note 22 | +/// | Enable 5 | Note 35 | Note 34 | Note 33 | Note 32 | Note 31 | Note 30 | Note 29 | +/// | Enable 6 | Note 42 | Note 41 | Note 40 | Note 39 | Note 38 | Note 37 | Note 36 | +/// | Enable 7 | Note 49 | Note 48 | Note 47 | Note 46 | Note 45 | Note 44 | Note 43 | +/// | Enable 8 | - | - | - | - | - | - | Note 50 | +/// +/// | **SC Adv** | **SCR** | +/// |:----------:|:-------:| +/// +/// Supports up to 128 denomination types, therefore the command requires 19 extended +/// data bytes. +/// +/// This will make the message length 0x1C: +/// +/// | Byte | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | +/// |:----------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| +/// | Enable 1 | Note 7 | Note 6 | Note 5 | Note 4 | Note 3 | Note 2 | Note 1 | +/// | Enable 2 | Note 14 | Note 13 | Note 12 | Note 11 | Note 10 | Note 9 | Note 8 | +/// | Enable 3 | Note 21 | Note 20 | Note 19 | Note 18 | Note 17 | Note 16 | Note 15 | +/// | Enable 4 | Note 28 | Note 27 | Note 26 | Note 25 | Note 24 | Note 23 | Note 22 | +/// | Enable 5 | Note 35 | Note 34 | Note 33 | Note 32 | Note 31 | Note 30 | Note 29 | +/// | Enable 6 | Note 42 | Note 41 | Note 40 | Note 39 | Note 38 | Note 37 | Note 36 | +/// | Enable 7 | Note 49 | Note 48 | Note 47 | Note 46 | Note 45 | Note 44 | Note 43 | +/// | Enable 8 | Note 56 | Note 55 | Note 54 | Note 53 | Note 52 | Note 51 | Note 50 | +/// | Enable 9 | Note 63 | Note 62 | Note 61 | Note 60 | Note 59 | Note 58 | Note 57 | +/// | Enable 10 | Note 70 | Note 69 | Note 68 | Note 67 | Note 66 | Note 65 | Note 64 | +/// | Enable 11 | Note 77 | Note 76 | Note 75 | Note 74 | Note 73 | Note 72 | Note 71 | +/// | Enable 12 | Note 84 | Note 83 | Note 82 | Note 81 | Note 80 | Note 79 | Note 78 | +/// | Enable 13 | Note 91 | Note 90 | Note 89 | Note 88 | Note 87 | Note 86 | Note 85 | +/// | Enable 14 | Note 98 | Note 97 | Note 98 | Note 97 | Note 96 | Note 95 | Note 94 | +/// | Enable 15 | Note 105 | Note 104 | Note 103 | Note 102 | Note 101 | Note 100 | Note 99 | +/// | Enable 16 | Note 112 | Note 111 | Note 110 | Note 109 | Note 108 | Note 107 | Note 106 | +/// | Enable 17 | Note 119 | Note 118 | Note 117 | Note 116 | Note 115 | Note 114 | Note 113 | +/// | Enable 18 | Note 126 | Note 125 | Note 124 | Note 123 | Note 122 | Note 121 | Note 120 | +/// | Enable 19 | - | - | - | - | - | Note 128 | Note 127 | +/// +/// If the bit equals 1 then the note is enabled. +pub struct SetExtendedNoteInhibits { + buf: [u8; M], +} + +impl SetExtendedNoteInhibits { + /// The length of enable note bytes (N - [SET_EXTENDED_NOTE_INHIBITS_BASE]). + pub const ENABLE_NOTE_LEN: usize = N; + + /// Creates a new [SetExtendedNoteInhibits] message. + pub fn new() -> Self { + assert!( + M == SET_EXTENDED_NOTE_INHIBITS_BASE + CFSC_ENABLE_LEN + || M == SET_EXTENDED_NOTE_INHIBITS_BASE + SC_ENABLE_LEN + ); + + let mut message = Self { buf: [0u8; M] }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_note(ExtendedNoteReporting::Set); + message.set_extended_command(ExtendedCommand::SetExtendedNoteInhibits); + + message + } + + /// Get the table of enabled note bytes. + pub fn enabled_notes(&self) -> [EnableNote; N] { + let mut ret = [EnableNote::none(); N]; + + for (¬e, set_note) in self.buf + [index::ENABLE_NOTE..index::ENABLE_NOTE + Self::ENABLE_NOTE_LEN] + .iter() + .zip(ret.iter_mut()) + { + *set_note = EnableNote::from(note); + } + + ret + } + + /// Sets the enable note bytes + /// + /// Example: `notes[0]` sets `Enable 1`, `notes[1]` sets `Enable 2` etc. + /// + /// Note: maximum of [ENABLE_NOTE_LEN](Self::ENABLE_NOTE_LEN) [EnableNote]s can be set, any extra supplied are ignored. + pub fn set_enabled_notes(&mut self, notes: &[EnableNote]) { + let max_len = std::cmp::min(notes.len(), Self::ENABLE_NOTE_LEN); + + for (i, note) in notes[..max_len].iter().enumerate() { + self.buf[index::ENABLE_NOTE + i] = note.into(); + } + } +} + +pub const CFSC_ENABLE_FULL_LEN: usize = SET_EXTENDED_NOTE_INHIBITS_BASE + CFSC_ENABLE_LEN; +pub const SC_ENABLE_FULL_LEN: usize = SET_EXTENDED_NOTE_INHIBITS_BASE + SC_ENABLE_LEN; + +pub type SetExtendedNoteInhibitsCFSC = + SetExtendedNoteInhibits; +pub type SetExtendedNoteInhibitsSC = SetExtendedNoteInhibits; + +impl_default!(SetExtendedNoteInhibits, M, N); +impl_message_ops!(SetExtendedNoteInhibits, M, N); +impl_extended_ops!(SetExtendedNoteInhibits, M, N); +impl_omnibus_extended_command!(SetExtendedNoteInhibits, M, N); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_set_extended_note_inhibits_from_bytes() -> Result<()> { + + // CFSC note table + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x11, 0x70, 0x03, + // Data + 0x00, 0x00, 0x00, + // Enable + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x63, + ]; + + let mut msg = SetExtendedNoteInhibitsCFSC::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::SetExtendedNoteInhibits); + + let exp_enabled = [ + EnableNote::from(1), EnableNote::none(), EnableNote::none(), EnableNote::none(), + EnableNote::none(), EnableNote::none(), EnableNote::none(), EnableNote::none(), + ]; + + assert_eq!(msg.enabled_notes(), exp_enabled); + + // SC note table + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x1c, 0x70, 0x03, + // Data + 0x00, 0x00, 0x00, + // Enable + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x6e, + ]; + + let mut msg = SetExtendedNoteInhibitsSC::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::SetExtendedNoteInhibits); + + let exp_enabled = [ + EnableNote::from(1), EnableNote::none(), EnableNote::none(), EnableNote::none(), + EnableNote::none(), EnableNote::none(), EnableNote::none(), EnableNote::none(), + EnableNote::none(), EnableNote::none(), EnableNote::none(), EnableNote::none(), + EnableNote::none(), EnableNote::none(), EnableNote::none(), EnableNote::none(), + EnableNote::none(), EnableNote::none(), EnableNote::none(), + ]; + + assert_eq!(msg.enabled_notes(), exp_enabled); + + Ok(()) + } +} diff --git a/src/extended_note_inhibits/reply.rs b/src/extended_note_inhibits/reply.rs new file mode 100644 index 0000000..d584888 --- /dev/null +++ b/src/extended_note_inhibits/reply.rs @@ -0,0 +1,126 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_extended_reply_ops, impl_message_ops, impl_omnibus_extended_reply, + len::EXTENDED_NOTE_INHIBITS_REPLY_ALT, ExtendedCommand, ExtendedReplyOps, MessageOps, + MessageType, OmnibusReply, +}; + +mod index { + pub const DATA: usize = 4; +} + +/// Set Extended Note Inhibits - Reply (Subtype 0x03) +/// Represents a reply for a [SetExtendedNoteInhibits](crate::SetExtendedNoteInhibits) command. +/// +/// The device will reply with a standard Omnibus reply detailed in section 7.1.2 with no extended data. +pub type ExtendedNoteInhibitsReply = OmnibusReply; + +/// Represents an alternate reply for a [SetExtendedNoteInhibits](crate::SetExtendedNoteInhibits) command. +/// +/// In some firmware, an alternate reply is given. This reply also contains no extended data. +/// +/// The Extended Note Inhibits Alternate Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | +/// | Value | 0x02 | 0x0C | 0x7n | 0x03 | nn | nn | nn | nn | nn | nn | 0x03 | zz | +/// +/// **WARNING** In order to avoid possible confusion processing the extended note data, this command should +/// only be sent when the device is in the idle state. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ExtendedNoteInhibitsReplyAlt { + buf: [u8; EXTENDED_NOTE_INHIBITS_REPLY_ALT], +} + +impl ExtendedNoteInhibitsReplyAlt { + /// Creates a new [ExtendedNoteInhibitsReplyAlt] message. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; EXTENDED_NOTE_INHIBITS_REPLY_ALT], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::SetExtendedNoteInhibits); + + message + } +} + +impl_default!(ExtendedNoteInhibitsReplyAlt); +impl_message_ops!(ExtendedNoteInhibitsReplyAlt); +impl_omnibus_extended_reply!(ExtendedNoteInhibitsReplyAlt); +impl_extended_reply_ops!(ExtendedNoteInhibitsReplyAlt); + +impl From for ExtendedNoteInhibitsReplyAlt { + fn from(msg: ExtendedNoteInhibitsReply) -> Self { + use crate::index as omnibus_index; + + let msg_buf = msg.buf(); + let msg_etx_index = msg.etx_index(); + + let mut res = Self::new(); + let res_etx_index = res.etx_index(); + let res_buf = res.buf_mut(); + + res_buf[index::DATA..res_etx_index] + .copy_from_slice(msg_buf[omnibus_index::DATA..msg_etx_index].as_ref()); + + res.calculate_checksum(); + + res + } +} + +impl fmt::Display for ExtendedNoteInhibitsReplyAlt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, ExtendedCommand: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.extended_command(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_extended_note_inhibits_reply_alt_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0c, 0x70, 0x03, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x7f, + ]; + + let mut msg = ExtendedNoteInhibitsReplyAlt::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!( + msg.extended_command(), + ExtendedCommand::SetExtendedNoteInhibits + ); + + Ok(()) + } +} diff --git a/src/extended_note_specification.rs b/src/extended_note_specification.rs new file mode 100644 index 0000000..5610716 --- /dev/null +++ b/src/extended_note_specification.rs @@ -0,0 +1,5 @@ +mod query; +mod reply; + +pub use query::*; +pub use reply::*; diff --git a/src/extended_note_specification/query.rs b/src/extended_note_specification/query.rs new file mode 100644 index 0000000..b635a72 --- /dev/null +++ b/src/extended_note_specification/query.rs @@ -0,0 +1,90 @@ +use crate::{ + impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::QUERY_EXTENDED_NOTE_SPECIFICATION, ExtendedCommand, ExtendedCommandOps, + ExtendedNoteReporting, MessageOps, MessageType, OmnibusCommandOps, +}; + +mod index { + pub const NOTE_INDEX: usize = 7; +} + +/// Extended Note Specification - Query (Subtype 0x02) +/// +/// This message serves two purposes; one purpose for this message is to allow the host to query the +/// extended note details for a specified index. The other use is by the device to inform the host when a +/// bank note has reached the escrow position or been stacked. +/// +/// The Query Extended Note Specification message is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Index | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:-----:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +/// | Value | 0x02 | 0x0A | 0x7n | 0x02 | nn | nn | nn | nn | 0x03 | zz | +/// +/// The first extended data byte is the Index. This index value starts at `1` and represents the index of the +/// extended note table data in the device. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryExtendedNoteSpecification { + buf: [u8; QUERY_EXTENDED_NOTE_SPECIFICATION], +} + +impl QueryExtendedNoteSpecification { + /// Create a new [QueryExtendedNoteSpecification] message + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_EXTENDED_NOTE_SPECIFICATION], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::ExtendedNoteSpecification); + message.set_extended_note(ExtendedNoteReporting::Set); + + message + } + + /// Get the note index being queried + pub fn note_index(&self) -> usize { + self.buf[index::NOTE_INDEX] as usize + } + + /// Set the note index being queried + pub fn set_note_index(&mut self, index: usize) { + self.buf[index::NOTE_INDEX] = index as u8; + } +} + +impl_default!(QueryExtendedNoteSpecification); +impl_message_ops!(QueryExtendedNoteSpecification); +impl_extended_ops!(QueryExtendedNoteSpecification); +impl_omnibus_extended_command!(QueryExtendedNoteSpecification); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_extended_note_specification_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0a, 0x70, 0x02, + // Data + 0x00, 0x00, 0x00, + // Index + 0x01, + // ETX | Checksum + 0x03, 0x79, + ]; + + let mut msg = QueryExtendedNoteSpecification::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::ExtendedNoteSpecification); + assert_eq!(msg.note_index(), 1); + + Ok(()) + } +} diff --git a/src/extended_note_specification/reply.rs b/src/extended_note_specification/reply.rs new file mode 100644 index 0000000..fb425ed --- /dev/null +++ b/src/extended_note_specification/reply.rs @@ -0,0 +1,312 @@ +use crate::std; +use std::fmt; + +use crate::{ + banknote::*, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply, + len::EXTENDED_NOTE_REPLY, status::*, ExtendedCommand, ExtendedCommandOps, MessageOps, + MessageType, OmnibusReplyOps, +}; + +impl From<&ExtendedNoteReply> for Banknote { + fn from(reply: &ExtendedNoteReply) -> Self { + let base_value: f32 = reply.base_value().into(); + let exponent: f32 = reply.exponent().into(); + + let value = match reply.sign() { + Sign::Positive => base_value * 10f32.powf(exponent), + Sign::Negative => base_value * 10f32.powf(-exponent), + }; + + Self::new( + value, + reply.iso_code(), + reply.note_type(), + reply.note_series(), + reply.note_compatibility(), + reply.note_version(), + reply.banknote_classification(), + ) + } +} + +impl From for Banknote { + fn from(reply: ExtendedNoteReply) -> Self { + Self::from(&reply) + } +} + +impl From<&ExtendedNoteReply> for NoteTableItem { + fn from(e: &ExtendedNoteReply) -> NoteTableItem { + Self::new(e.note_index(), e.into()) + } +} + +impl From for NoteTableItem { + fn from(e: ExtendedNoteReply) -> NoteTableItem { + (&e).into() + } +} + +impl From<&ExtendedNoteReply> for DocumentStatus { + fn from(reply: &ExtendedNoteReply) -> Self { + let status = DocumentStatus::default().with_standard_denomination(reply.note_value()); + + match reply.banknote_classification() { + BanknoteClassification::Genuine | BanknoteClassification::DisabledOrNotSupported => { + status.with_accepted_note_table_item(AcceptedNoteTableItem::new( + reply.into(), + reply.orientation(), + )) + } + _ => status, + } + } +} + +mod index { + use super::{BaseValue, Exponent, ISOCode}; + + pub const EXTENDED: usize = 10; + pub const NOTE_INDEX: usize = EXTENDED; + pub const ISO_CODE: usize = EXTENDED + 1; + pub const ISO_CODE_END: usize = ISO_CODE + ISOCode::LEN; + pub const BASE_VALUE: usize = EXTENDED + 4; + pub const BASE_VALUE_END: usize = BASE_VALUE + BaseValue::LEN; + pub const SIGN: usize = EXTENDED + 7; + pub const EXPONENT: usize = EXTENDED + 8; + pub const EXPONENT_END: usize = EXPONENT + Exponent::LEN; + pub const ORIENTATION: usize = EXTENDED + 10; + pub const NOTE_TYPE: usize = EXTENDED + 11; + pub const NOTE_SERIES: usize = EXTENDED + 12; + pub const NOTE_COMPATIBILITY: usize = EXTENDED + 13; + pub const NOTE_VERSION: usize = EXTENDED + 14; + pub const BANKNOTE_CLASSIFICATION: usize = EXTENDED + 15; +} + +/// Extended Note Specification - Reply (Subtype 0x02) +/// +/// ExtendedNoteReply represents a message sent from the device back to the host +/// +/// The reply contains 18 additional bytes of data that describe the bank note in great detail. This message +//// can be sent from the device for two reasons: +/// +/// * Response to a host’s query extended note command +/// * Device is running in extended note mode and a valid banknote has either reached escrow or +/// been stacked. +/// +/// The Extended Note Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ExtData 0 | ... | ExtData 17 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:---------:|:---:|:----------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ... | 27 | 28 | 29 | +/// | Value | 0x02 | 0x1E | 0x7n | 0x03 | nn | nn | nn | nn | nn | nn | nn | nn | nn | 0x03 | zz | +/// +/// If this is a reply to the Host Query Command, the index will match the same value sent from the host. +/// Otherwise, the index value is not used and set to 0x00 for any escrowed or stacked notes. +/// +/// | Field | Byte Offset | Field Description | Sample Value
(2000 Yen Note) | +/// |:------------------------|:-----------:|:-----------------------------------------------------|:----------------------------------:| +/// | Index | 0 | Not used for escrow or stacked notes | 0x00 | +/// | ISO Code | 1..3 | A three character ASCII currecny code,
see | "JPY" | +/// | Base Value | 4..6 | A three character ASCII coded decimal value | "002" | +/// | Sign | 7 | An ASCII coded sign value for the Exponent ("+"/"-") | "+" | +/// | Exponent | 8..9 | An ASCII coded decimal power of ten to [multiply "+", divide "-"] the Base Value | "03" | +/// | Orientation | 10 | A single character binary field that encodes the orientation of the bank note.

0x00 = Right Edge, Face Up
0x01 = Right Edge, Face Down
0x02 = Left Edge, Face Up
0x03 = Left Edge, Face Down

Note: in general this field is only correct if the Extended Orientation bit is set in the device capabilities map.| 0x00 | +/// | Type | 11 | An ASCII letter that documents the note type.
This corresponds to the data in the variant identity card. | "A" | +/// | Series | 12 | An ASCII letter that documents the note series.
This corresponds to the data in the variant identity card. | "A" | +/// | Compatibility | 13 | An ASCII letter that documents the revision of the compatibility core used.
This corresponds to the data in the variant identity card. | "B" | +/// | Version | 14 | An ASCII letter that documents the version of the note's recognition criteria.
This corresponds to the data in the variant identity card. | "A" | +/// | Banknote Classification | 15 | 0x00 = Sent for any of the following:
  • In response to a Host Query Extended Note Specification Command (i.e. host requests a note table element).
  • In response to a note escrowed or stacked event while device is in extended note mode and classification is:
    • Supported by the device but disabled.
    • NOT supported by the device.

**SC Adv Classification**
**SCR Classification**
0x01 = Class 1 (unidentified banknote)
0x02 = Class 2 (suspected counterfeit)
0x03 = Class 3 (suspected zero value note)
0x04 = Class 4 (genuine banknote) | 0x00 | +/// | Reserved | 16..17 | Bytes reserved for future use | N/A | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ExtendedNoteReply { + buf: [u8; EXTENDED_NOTE_REPLY], +} + +impl ExtendedNoteReply { + /// Create a new ExtendedNoteReply message + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; EXTENDED_NOTE_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::ExtendedNoteSpecification); + + message + } + + /// Get the note index (invalid for escrowed and stacked notes). + pub fn note_index(&self) -> usize { + self.buf[index::NOTE_INDEX] as usize + } + + /// Get the ISO 4217 code + pub fn iso_code(&self) -> ISOCode { + self.buf[index::ISO_CODE..index::ISO_CODE_END] + .as_ref() + .into() + } + + /// Get the note's base value + pub fn base_value(&self) -> BaseValue { + self.buf[index::BASE_VALUE..index::BASE_VALUE_END] + .as_ref() + .into() + } + + /// Get the note's sign + pub fn sign(&self) -> Sign { + self.buf[index::SIGN].into() + } + + /// Get the note's exponent + pub fn exponent(&self) -> Exponent { + self.buf[index::EXPONENT..index::EXPONENT_END] + .as_ref() + .into() + } + + /// Get the note's orientation + pub fn orientation(&self) -> BanknoteOrientation { + self.buf[index::ORIENTATION].into() + } + + /// Get the note's type + pub fn note_type(&self) -> NoteType { + self.buf[index::NOTE_TYPE].into() + } + + /// Get the note's series + pub fn note_series(&self) -> NoteSeries { + self.buf[index::NOTE_SERIES].into() + } + + /// Get the note's compatibility + pub fn note_compatibility(&self) -> NoteCompatibility { + self.buf[index::NOTE_COMPATIBILITY].into() + } + + /// Get the note's version + pub fn note_version(&self) -> NoteVersion { + self.buf[index::NOTE_VERSION].into() + } + + /// Get the note's banknote classification + pub fn banknote_classification(&self) -> BanknoteClassification { + self.buf[index::BANKNOTE_CLASSIFICATION].into() + } + + /// Check if the reply is null + pub fn is_null(&self) -> bool { + let mut res = true; + self.buf[index::EXTENDED..self.etx_index()] + .iter() + .for_each(|&b| { + if res && b != 0 { + res = false + } + }); + res + } +} + +impl_default!(ExtendedNoteReply); +impl_message_ops!(ExtendedNoteReply); +impl_extended_ops!(ExtendedNoteReply); +impl_omnibus_extended_reply!(ExtendedNoteReply); + +impl fmt::Display for ExtendedNoteReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, Banknote: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + Banknote::from(self), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_extended_note_reply_from_bytes() -> Result<()> { + let mut msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x1e, 0x70, 0x02, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Index + 0x00, + // ISO code + 0x00, 0x00, 0x00, + // Base value + 0x00, 0x00, 0x00, + // Sign + 0x00, + // Exponent + 0x00, 0x00, + // Orientation + 0x00, + // Note type + 0x00, + // Note series + 0x00, + // Note compatibility + 0x00, + // Note version + 0x00, + // Banknote classification + 0x00, + // Reserved + 0x00, 0x00, + // ETX | Checksum + 0x03, 0x36, + ]; + + msg_bytes[index::ISO_CODE..index::ISO_CODE_END].copy_from_slice(b"JPY".as_ref()); + msg_bytes[index::BASE_VALUE..index::BASE_VALUE_END].copy_from_slice(b"002".as_ref()); + msg_bytes[index::SIGN] = b'+'; + msg_bytes[index::EXPONENT..index::EXPONENT_END].copy_from_slice(b"03".as_ref()); + msg_bytes[index::ORIENTATION] = BanknoteOrientation::RightEdgeFaceUp as u8; + msg_bytes[index::NOTE_TYPE] = b'A'; + msg_bytes[index::NOTE_SERIES] = b'A'; + msg_bytes[index::NOTE_COMPATIBILITY] = b'B'; + msg_bytes[index::NOTE_VERSION] = b'A'; + msg_bytes[index::BANKNOTE_CLASSIFICATION] = BanknoteClassification::DisabledOrNotSupported as u8; + + let mut msg = ExtendedNoteReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::ExtendedNoteSpecification); + assert_eq!(msg.note_index(), 0); + assert_eq!(msg.iso_code(), ISOCode::JPY); + assert_eq!(msg.base_value(), BaseValue::from(b"002")); + assert_eq!(msg.sign(), Sign::Positive); + assert_eq!(msg.exponent(), Exponent::from(b"03")); + assert_eq!(msg.orientation(), BanknoteOrientation::RightEdgeFaceUp); + assert_eq!(msg.note_type(), NoteType::from(b'A')); + assert_eq!(msg.note_series(), NoteSeries::from(b'A')); + assert_eq!(msg.note_compatibility(), NoteCompatibility::from(b'B')); + assert_eq!(msg.note_version(), NoteVersion::from(b'A')); + assert_eq!(msg.banknote_classification(), BanknoteClassification::DisabledOrNotSupported); + + assert_eq!(Banknote::from(msg).value(), 2000.0); + + Ok(()) + } +} diff --git a/src/extended_reply.rs b/src/extended_reply.rs new file mode 100644 index 0000000..92e0f73 --- /dev/null +++ b/src/extended_reply.rs @@ -0,0 +1,86 @@ +use crate::{ + CodeRevision, DeviceState, DeviceStatus, ExceptionStatus, ExtendedCommand, MessageOps, + MiscDeviceState, ModelNumber, +}; + +mod index { + pub const SUBTYPE: usize = 3; + pub const DEVICE_STATE: usize = SUBTYPE + 1; + pub const DEVICE_STATUS: usize = SUBTYPE + 2; + pub const EXCEPTION_STATUS: usize = SUBTYPE + 3; + pub const MISC_DEVICE_STATE: usize = SUBTYPE + 4; + pub const MODEL_NUMBER: usize = SUBTYPE + 5; + pub const CODE_REVISION: usize = SUBTYPE + 6; +} + +pub trait ExtendedReplyOps: MessageOps { + /// Get the extended command sub-type + fn extended_command(&self) -> ExtendedCommand { + self.buf()[index::SUBTYPE].into() + } + + /// Set the extended command sub-type + fn set_extended_command(&mut self, ext_cmd: ExtendedCommand) { + self.buf_mut()[index::SUBTYPE] = ext_cmd.into(); + } + + /// Get the device state data field + fn device_state(&self) -> DeviceState { + self.buf()[index::DEVICE_STATE].into() + } + + /// Set the device state data field + fn set_device_state(&mut self, device_state: DeviceState) { + self.buf_mut()[index::DEVICE_STATE] = device_state.into(); + } + + /// Get the device status data field + fn device_status(&self) -> DeviceStatus { + self.buf()[index::DEVICE_STATUS].into() + } + + /// Set the device status data field + fn set_device_status(&mut self, device_status: DeviceStatus) { + self.buf_mut()[index::DEVICE_STATUS] = device_status.into(); + } + + /// Get the exception status data field + fn exception_status(&self) -> ExceptionStatus { + self.buf()[index::EXCEPTION_STATUS].into() + } + + /// Set the exception status data field + fn set_exception_status(&mut self, exception_status: ExceptionStatus) { + self.buf_mut()[index::EXCEPTION_STATUS] = exception_status.into(); + } + + /// Get the miscellaneous device status data field + fn misc_device_state(&self) -> MiscDeviceState { + self.buf()[index::MISC_DEVICE_STATE].into() + } + + /// Set the miscellaneous device status data field + fn set_misc_device_state(&mut self, misc_device_state: MiscDeviceState) { + self.buf_mut()[index::MISC_DEVICE_STATE] = misc_device_state.into(); + } + + /// Get the model number data field + fn model_number(&self) -> ModelNumber { + self.buf()[index::MODEL_NUMBER].into() + } + + /// Set the model number data field + fn set_model_number(&mut self, model_number: ModelNumber) { + self.buf_mut()[index::MODEL_NUMBER] = model_number.into(); + } + + /// Get the code revision data field + fn code_revision(&self) -> CodeRevision { + self.buf()[index::CODE_REVISION].into() + } + + /// Set the code revision data field + fn set_code_revision(&mut self, code_revision: CodeRevision) { + self.buf_mut()[index::CODE_REVISION] = code_revision.into() + } +} diff --git a/src/flash_download.rs b/src/flash_download.rs new file mode 100644 index 0000000..e0497eb --- /dev/null +++ b/src/flash_download.rs @@ -0,0 +1,107 @@ +use crate::MessageOps; + +mod baud_rate; +mod message_7bit; +mod message_8bit; +mod reply_7bit; +mod reply_8bit; +mod start_download; + +pub use baud_rate::*; +pub use message_7bit::*; +pub use message_8bit::*; +pub use reply_7bit::*; +pub use reply_8bit::*; +pub use start_download::*; + +pub trait FlashDownloadMessage: MessageOps { + /// Gets whether this is the initial polling message for a firmware download session. + fn is_initial_poll(&self) -> bool { + false + } + + /// Gets the packet number of the [FlashDownloadMessage]. + /// + /// Represents the last successfully received packet number. + fn packet_number(&self) -> u16; + + /// Sets the packet number of the [FlashDownloadMessage]. + fn set_packet_number(&mut self, n: u16); + + /// Increments the packet number by one. + /// + /// If the packet number reaches u16::MAX (65_535), any additional increments will overflow, + /// starting the count back at zero. + /// + /// It isn't clear that is what the vendor intends, but that is the behavior in C. + /// + /// Without overflow, this limits the firmware size to: + /// + /// * 8-bit protocol: ~4MB (4_194_240 = 65_535 * 64) + /// * 7-bit protocol: ~2MB (2_097_120 = 65_535 * 32) + fn increment_packet_number(&mut self) -> u16 { + // FIXME: the desired behavior of an increment past the max is unclear. + // + // C behavior is to overflow, restarting at 0, but it isn't obvious that's what CPI + // intends. + // + // FWIW their firmware files all appear to be below the limit. + let packet_number = self.packet_number().overflowing_add(1).0; + self.set_packet_number(packet_number); + packet_number + } + + /// Gets the data bytes as a 32-byte array. + /// + /// Performs the conversion from the seven-bit protocol encoding (nibble-per-byte); + fn data(&self) -> [u8; DATA_LEN]; + + /// Gets a reference to the data bytes, as the raw protocol encoding. + /// + /// For 7-bit messages, this means each byte contains the significant bits in the lower nibble + /// of the byte. + /// + /// For 8-bit messages, there is no special encoding. + fn data_ref(&self) -> &[u8]; + + /// Sets the data bytes from a user-supplied array. + /// + /// **NOTE** user must supply: + /// + /// * 7-bit protocol: 32-byte array + /// * 8-bit protocol, 32-byte message: 32-byte array + /// * 8-bit protocol, 64-byte message: 64-byte array + /// + /// Performs the conversion: + /// + /// * 7-bit protocol: nibble-per-byte encoding + /// * 8-bit protocol: no conversion + fn set_data(&mut self, data: &[u8]); +} + +pub trait FlashDownloadReply: MessageOps { + /// Gets the packet number of the [FlashDownloadReply]. + /// + /// Represents the last successfully received packet number. + fn packet_number(&self) -> u16; + + /// Sets the packet number of the [FlashDownloadReply]. + fn set_packet_number(&mut self, n: u16); + + /// Gets whether the device experienced power loss during firmware download. + /// + /// A true value indicates the host should begin firmware download from the first packet. + fn power_loss(&self) -> bool { + // A value of -1 (0xffff) indicates the device experienced power loss + self.packet_number() == 0xffff + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SerialProtocol { + /// 7-bit serial protocol, 7 data bits, Parity: even, 1 stop bits + _7bit, + /// 8-bit serial protocol, 8 data bits, Parity: none, 1 stop bits + _8bit, +} diff --git a/src/flash_download/baud_rate.rs b/src/flash_download/baud_rate.rs new file mode 100644 index 0000000..e215a43 --- /dev/null +++ b/src/flash_download/baud_rate.rs @@ -0,0 +1,388 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, + len::{BAUD_CHANGE_REPLY, BAUD_CHANGE_REQUEST}, + MessageOps, MessageType, +}; + +#[allow(dead_code)] +mod index { + pub const DATA0: usize = 3; + pub const BAUD_RATE: usize = 3; +} + +/// Represents the acceptable values for host-device serial baud rates. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BaudRate { + _9600 = 0x01, + _19200 = 0x02, + _38400 = 0x03, + _115200 = 0x04, + Reserved = 0xff, +} + +impl From for BaudRate { + fn from(b: u8) -> Self { + match b { + 0x01 => Self::_9600, + 0x02 => Self::_19200, + 0x03 => Self::_38400, + 0x04 => Self::_115200, + _ => Self::Reserved, + } + } +} + +impl From for BaudRate { + fn from(b: u32) -> Self { + match b { + 9_600 => Self::_9600, + 19_200 => Self::_19200, + 38_400 => Self::_38400, + 115_200 => Self::_115200, + _ => Self::_9600, + } + } +} + +impl From for u8 { + fn from(b: BaudRate) -> Self { + b as u8 + } +} + +impl From<&BaudRate> for u8 { + fn from(b: &BaudRate) -> Self { + (*b).into() + } +} + +impl From for u32 { + fn from(b: BaudRate) -> Self { + match b { + BaudRate::_9600 => 9_600, + BaudRate::_19200 => 19_200, + BaudRate::_38400 => 38_400, + BaudRate::_115200 => 115_200, + _ => 9_600, + } + } +} + +impl From<&BaudRate> for u32 { + fn from(b: &BaudRate) -> Self { + (*b).into() + } +} + +impl From for &'static str { + fn from(b: BaudRate) -> Self { + match b { + BaudRate::_9600 => "9600", + BaudRate::_19200 => "19200", + BaudRate::_38400 => "38400", + BaudRate::_115200 => "115200", + BaudRate::Reserved => "Reserved", + } + } +} + +impl From<&BaudRate> for &'static str { + fn from(b: &BaudRate) -> Self { + (*b).into() + } +} + +impl fmt::Display for BaudRate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let b: &str = self.into(); + write!(f, "{b}") + } +} + +/// Setting Baud Rate (Optional) +/// +/// An optional feature available on selected versions of firmware is the ability to perform a "Fast Serial +/// Download". To support this feature, the serial port settings need to be adjusted prior to downloading +/// packets. The serial port settings shall be reverted back to the original values after the last packet has +/// been acknowledged by the device. +/// +/// | STX | LEN | CTRL | DATA0 | ETX | CHK | +/// |------|------|------|-------|------|------| +/// | 0x02 | 0x06 | 0x5n | baud | 0x03 | zz | +/// +/// | Data0 | Description | +/// |-------|-----------------------------------------------------------| +/// | 0x01 | Baud Rate 9600; Data Bits 8, Parity None; Stop Bit 0ne | +/// | 0x02 | Baud Rate 19,200; Data Bits 8, Parity None; Stop Bit 0ne | +/// | 0x03 | Baud Rate 38,400; Data Bits 8, Parity None; Stop Bit 0ne | +/// | 0x04 | Baud Rate 115,200; Data Bits 8, Parity None; Stop Bit 0ne | +/// | Other | Reserved | +/// +/// **Warning** If the device firmware does not support the fast serial download feature, the device will not +/// respond to the baud rate change request. This means the host will be required to perform the download +/// using the original algorithm. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BaudRateChangeRequest { + buf: [u8; BAUD_CHANGE_REQUEST], +} + +impl BaudRateChangeRequest { + /// Creates a new [BaudRateChangeRequest] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; BAUD_CHANGE_REQUEST], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } + + /// Gets the [BaudRate] for the [BaudRateChangeRequest]. + pub fn baud_rate(&self) -> BaudRate { + self.buf[index::DATA0].into() + } + + /// Sets the [BaudRate] for the [BaudRateChangeRequest]. + pub fn set_baud_rate(&mut self, baud_rate: BaudRate) { + match baud_rate { + BaudRate::Reserved => { + self.buf[index::DATA0] = BaudRate::_9600.into(); + } + _ => { + self.buf[index::DATA0] = baud_rate.into(); + } + } + } +} + +impl_default!(BaudRateChangeRequest); +impl_message_ops!(BaudRateChangeRequest); + +impl fmt::Display for BaudRateChangeRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, BaudRate: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.baud_rate(), + ) + } +} + +/// Setting Baud Rate Reply (Optional) +/// +/// If supported, the device will respond back to the command with an ACK (toggle bits will be the same) +/// and the Data 0 response will equal the requested value. (ex. if the host requests 19,200, the device will +/// respond with 19,200). If the device supports fast download but cannot support the requested baud rate, +/// the device will NAK the request but transmit the maximum supported value (ex. Host requests 115,200 +/// on SC Advance unit, device will NAK and return 38,400). +/// +/// | STX | LEN | CTRL | DATA0 | ETX | CHK | +/// |------|------|------|-------|------|------| +/// | 0x02 | 0x06 | 0x5n | baud | 0x03 | zz | +/// +/// | Data0 | Description | +/// |-------|-----------------------------------------------------------| +/// | 0x01 | Baud Rate 9600; Data Bits 8, Parity None; Stop Bit 0ne | +/// | 0x02 | Baud Rate 19,200; Data Bits 8, Parity None; Stop Bit 0ne | +/// | 0x03 | Baud Rate 38,400; Data Bits 8, Parity None; Stop Bit 0ne | +/// | 0x04 | Baud Rate 115,200; Data Bits 8, Parity None; Stop Bit 0ne | +/// | Other | Reserved | +/// +/// **Warning** If the device firmware does not support the fast serial download feature, the device will not +/// respond to the baud rate change request. This means the host will be required to perform the download +/// using the original algorithm. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BaudRateChangeReply { + buf: [u8; BAUD_CHANGE_REPLY], +} + +impl BaudRateChangeReply { + /// Creates a new [BaudRateChangeReply] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; BAUD_CHANGE_REQUEST], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } + + /// Gets the [BaudRate] for the [BaudRateChangeReply]. + pub fn baud_rate(&self) -> BaudRate { + self.buf[index::DATA0].into() + } + + /// Sets the [BaudRate] for the [BaudRateChangeReply]. + pub fn set_baud_rate(&mut self, baud_rate: BaudRate) { + match baud_rate { + BaudRate::Reserved => { + self.buf[index::DATA0] = BaudRate::_9600.into(); + } + _ => { + self.buf[index::DATA0] = baud_rate.into(); + } + } + } +} + +impl_default!(BaudRateChangeReply); +impl_message_ops!(BaudRateChangeReply); +impl_omnibus_nop_reply!(BaudRateChangeReply); + +impl fmt::Display for BaudRateChangeReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, BaudRate: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.baud_rate(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_baud_rate_change_request_from_bytes() -> Result<()> { + let mut msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (9,600) + 0x01, + // ETX | Checksum + 0x03, 0x57, + ]; + + let mut msg = BaudRateChangeRequest::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_9600); + + msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (19,200) + 0x02, + // ETX | Checksum + 0x03, 0x54, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_19200); + + msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (38,400) + 0x03, + // ETX | Checksum + 0x03, 0x55, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_38400); + + msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (115,200) + 0x04, + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_115200); + + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_baud_rate_change_reply_from_bytes() -> Result<()> { + let mut msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (9,600) + 0x01, + // ETX | Checksum + 0x03, 0x57, + ]; + + let mut msg = BaudRateChangeReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_9600); + + msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (19,200) + 0x02, + // ETX | Checksum + 0x03, 0x54, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_19200); + + msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (38,400) + 0x03, + // ETX | Checksum + 0x03, 0x55, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_38400); + + msg_bytes = [ + // STX | LEN | Message type + 0x02, 0x06, 0x50, + // Baud rate (115,200) + 0x04, + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.baud_rate(), BaudRate::_115200); + + Ok(()) + } +} diff --git a/src/flash_download/message_7bit.rs b/src/flash_download/message_7bit.rs new file mode 100644 index 0000000..929073b --- /dev/null +++ b/src/flash_download/message_7bit.rs @@ -0,0 +1,171 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, + len::{FLASH_DATA_PACKET, FLASH_DOWNLOAD_MESSAGE_7BIT}, + seven_bit_u16, seven_bit_u8, u16_seven_bit, u8_seven_bit, MessageOps, MessageType, +}; + +use super::FlashDownloadMessage; + +mod index { + pub const PACKET0: usize = 3; + //pub const PACKET1: usize = 4; + //pub const PACKET2: usize = 5; + pub const PACKET3: usize = 6; + // The spec is inconsistent with its numbering scheme. + // Correct here to zero-index for consistency + pub const DATA0_HI: usize = 7; + pub const DATA31_LO: usize = 70; +} + +/// 7-Bit protocol (Original Algorithm) +/// +/// Starting at the beginning of the file, the host sends 32 byte blocks of data to the device (Note: the file is +/// required to be a multiple of 32 bytes long and the MEI firmware files are configured as such). +/// Downloading is completed through the Download Data command shown below. Each data byte contains +/// only 4 bits of data located in the least significant nibble +/// +/// The Flash Download Message (7-bit) is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Packet #0 | Packet #1 | Packet #2 | Packet #3 | Data 0 Hi | Data 0 Lo | ... | Data 31 Hi | Data 31 Lo | ETX | CHK | +/// |:------|:----:|:----:|:----:|:---------:|:---------:|:---------:|:---------:|:---------:|:---------:|:----:|:----------:|:----------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | 69 | 70 | 71 | 72 | +/// | Value | 0x02 | 0x49 | 0x5n | 0x0n | 0x0n | 0x0n | 0x0n | 0x0n | 0x0n | 0x0n | 0x0n | 0x0n | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FlashDownloadMessage7bit { + buf: [u8; FLASH_DOWNLOAD_MESSAGE_7BIT], +} + +impl FlashDownloadMessage7bit { + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FLASH_DOWNLOAD_MESSAGE_7BIT], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } +} + +impl_default!(FlashDownloadMessage7bit); +impl_message_ops!(FlashDownloadMessage7bit); + +impl FlashDownloadMessage for FlashDownloadMessage7bit { + fn packet_number(&self) -> u16 { + seven_bit_u16(self.buf[index::PACKET0..=index::PACKET3].as_ref()) + } + + fn set_packet_number(&mut self, n: u16) { + self.buf[index::PACKET0..=index::PACKET3].copy_from_slice(u16_seven_bit(n).as_ref()); + } + + fn data(&self) -> [u8; FLASH_DATA_PACKET] { + let mut ret = [0u8; FLASH_DATA_PACKET]; + for (i, b) in self.buf[index::DATA0_HI..=index::DATA31_LO] + .chunks_exact(2) + .enumerate() + { + ret[i] = seven_bit_u8(b); + } + ret + } + + fn data_ref(&self) -> &[u8] { + self.buf[index::DATA0_HI..=index::DATA31_LO].as_ref() + } + + fn set_data(&mut self, data: &[u8]) { + assert_eq!(data.len(), FLASH_DATA_PACKET); + + for (i, &b) in data.iter().enumerate() { + let start = index::DATA0_HI + (i * 2); + let end = start + 2; + + self.buf[start..end].copy_from_slice(u8_seven_bit(b).as_ref()); + } + } +} + +impl fmt::Display for FlashDownloadMessage7bit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, PacketNumber: {}, Data: {:x?}", + self.acknak(), + self.device_type(), + self.message_type(), + self.packet_number(), + self.data(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + fn flash_download_message_fns() { + let mut msg = FlashDownloadMessage7bit::new(); + + let data_packet = [ + 0x54, 0x01, 0x03, 0x04, 0x00, 0xa0, 0x00, 0x20, 0x00, 0x7f, 0x5a, 0x01, 0xc0, 0x1f, + 0x00, 0x00, 0x59, 0xae, 0x00, 0x20, 0x03, 0x00, 0x07, 0x32, 0x38, 0x36, 0x31, 0x30, + 0x31, 0x31, 0x30, 0x33, + ]; + + let expected_encoding = [ + 0x05, 0x04, 0x00, 0x01, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x07, 0x0f, 0x05, 0x0a, 0x00, 0x01, 0x0c, 0x00, 0x01, 0x0f, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x09, 0x0a, 0x0e, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x07, 0x03, 0x02, 0x03, 0x08, 0x03, 0x06, 0x03, 0x01, 0x03, 0x00, + 0x03, 0x01, 0x03, 0x01, 0x03, 0x00, 0x03, 0x03, + ]; + + msg.set_data(data_packet.as_ref()); + + assert_eq!(msg.data_ref(), expected_encoding.as_ref()); + assert_eq!(msg.data(), data_packet); + } + + #[test] + #[rustfmt::skip] + fn flash_download_message_7bit_from_buf() -> Result<()> { + let data_packet = [ + 0x54, 0x01, 0x03, 0x04, 0x00, 0xa0, 0x00, 0x20, 0x00, 0x7f, 0x5a, 0x01, 0xc0, 0x1f, + 0x00, 0x00, 0x59, 0xae, 0x00, 0x20, 0x03, 0x00, 0x07, 0x32, 0x38, 0x36, 0x31, 0x30, + 0x31, 0x31, 0x30, 0x33, + ]; + + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x49, 0x50, + // Packet number + 0x01, 0x02, 0x03, 0x04, + // Data + 0x05, 0x04, 0x00, 0x01, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x07, 0x0f, 0x05, 0x0a, 0x00, 0x01, 0x0c, 0x00, 0x01, 0x0f, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x09, 0x0a, 0x0e, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x07, 0x03, 0x02, 0x03, 0x08, 0x03, 0x06, 0x03, 0x01, 0x03, 0x00, + 0x03, 0x01, 0x03, 0x01, 0x03, 0x00, 0x03, 0x03, + // ETX | Checksum + 0x03, 0x15, + ]; + + let mut msg = FlashDownloadMessage7bit::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.packet_number(), 0x1234); + assert_eq!(msg.data_ref(), msg_bytes[index::DATA0_HI..=index::DATA31_LO].as_ref()); + assert_eq!(msg.data(), data_packet); + + Ok(()) + } +} diff --git a/src/flash_download/message_8bit.rs b/src/flash_download/message_8bit.rs new file mode 100644 index 0000000..0a14451 --- /dev/null +++ b/src/flash_download/message_8bit.rs @@ -0,0 +1,269 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, + len::{ + FLASH_DATA_PACKET, FLASH_DATA_PACKET_64, FLASH_DOWNLOAD_MESSAGE_8BIT_32, + FLASH_DOWNLOAD_MESSAGE_8BIT_64, + }, + MessageOps, MessageType, +}; + +use super::FlashDownloadMessage; + +mod index { + pub const PACKET0: usize = 3; + pub const PACKET1: usize = 4; + pub const DATA0: usize = 5; + pub const DATA31: usize = 36; + pub const DATA63: usize = 68; +} + +/// Flash Download Message - 8-Bit protocol (Fast Serial Algorithm) (64-byte packet) +/// +/// Starting at the beginning of the file, the host sends 64 byte blocks of data to the device. Downloading is +/// completed through the Download Data command shown below. The full 8-bits can be used for packet +/// numbers and data packets. +/// +/// **Warning** The file is required to be a multiple of 32 bytes long therefore it is possible for the final +/// packet to only contain the remaining 32 data bytes. Do not pad the message with empty values. +/// +/// The Flash Download Message (8-bit, 64-byte packet) is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Packet #0 | Packet #1 | Data 0 | ... | Data 63 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:---------:|:---------:|:------:|:----:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | ... | 68 | 69 | 70 | +/// | Value | 0x02 | 0x47 | 0x5n | 0xnn | 0xnn | 0xnn | 0xnn | 0xnn | 0x03 | zz | +/// +/// **Note**: the 16-bit packet numbers are stored in little-endian format (least-significant byte first) +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FlashDownloadMessage8bit_64 { + buf: [u8; FLASH_DOWNLOAD_MESSAGE_8BIT_64], +} + +impl FlashDownloadMessage8bit_64 { + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FLASH_DOWNLOAD_MESSAGE_8BIT_64], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } +} + +impl_default!(FlashDownloadMessage8bit_64); +impl_message_ops!(FlashDownloadMessage8bit_64); + +impl FlashDownloadMessage for FlashDownloadMessage8bit_64 { + fn packet_number(&self) -> u16 { + u16::from_le_bytes([self.buf[index::PACKET0], self.buf[index::PACKET1]]) + } + + fn set_packet_number(&mut self, n: u16) { + self.buf[index::PACKET0..=index::PACKET1].copy_from_slice(n.to_le_bytes().as_ref()); + } + + fn increment_packet_number(&mut self) -> u16 { + // FIXME: the desired behavior of an increment past the max is unclear. + // + // C behavior is to overflow, restarting at 0, but it isn't obvious that's what CPI + // intends. + // + // FWIW their firmware files all appear to be below the limit. + let packet_number = self.packet_number().overflowing_add(1).0; + self.set_packet_number(packet_number); + packet_number + } + + fn data(&self) -> [u8; FLASH_DATA_PACKET_64] { + // The unwrap is safe here, and can never panic because the slice is guaranteed to be the + // correct length. + self.buf[index::DATA0..=index::DATA63].try_into().unwrap() + } + + fn data_ref(&self) -> &[u8] { + self.buf[index::DATA0..=index::DATA63].as_ref() + } + + fn set_data(&mut self, data: &[u8]) { + assert_eq!(data.len(), FLASH_DATA_PACKET_64); + + self.buf[index::DATA0..=index::DATA63].copy_from_slice(data); + } +} + +impl fmt::Display for FlashDownloadMessage8bit_64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, PacketNumber: {}, Data: {:x?}", + self.acknak(), + self.device_type(), + self.message_type(), + self.packet_number(), + self.data(), + ) + } +} + +/// Flash Download Message - 8-Bit protocol (Fast Serial Algorithm) (32-byte packet) +/// +/// Starting at the beginning of the file, the host sends 64 byte blocks of data to the device. Downloading is +/// completed through the Download Data command shown below. The full 8-bits can be used for packet +/// numbers and data packets. +/// +/// **Warning** The file is required to be a multiple of 32 bytes long therefore it is possible for the final +/// packet to only contain the remaining 32 data bytes. Do not pad the message with empty values. +/// +/// The Flash Download Message (8-bit, 32-byte packet) is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Packet #0 | Packet #1 | Data 0 | ... | Data 31 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:---------:|:---------:|:------:|:----:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | ... | 36 | 37 | 38 | +/// | Value | 0x02 | 0x27 | 0x5n | 0xnn | 0xnn | 0xnn | 0xnn | 0xnn | 0x03 | zz | +/// +/// **Note**: the 16-bit packet numbers are stored in little-endian format (least-significant byte first) +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FlashDownloadMessage8bit_32 { + buf: [u8; FLASH_DOWNLOAD_MESSAGE_8BIT_32], +} + +impl FlashDownloadMessage8bit_32 { + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FLASH_DOWNLOAD_MESSAGE_8BIT_32], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } +} + +impl_default!(FlashDownloadMessage8bit_32); +impl_message_ops!(FlashDownloadMessage8bit_32); + +impl FlashDownloadMessage for FlashDownloadMessage8bit_32 { + fn packet_number(&self) -> u16 { + u16::from_le_bytes([self.buf[index::PACKET0], self.buf[index::PACKET1]]) + } + + fn set_packet_number(&mut self, n: u16) { + self.buf[index::PACKET0..=index::PACKET1].copy_from_slice(n.to_le_bytes().as_ref()); + } + + fn increment_packet_number(&mut self) -> u16 { + // FIXME: the desired behavior of an increment past the max is unclear. + // + // C behavior is to overflow, restarting at 0, but it isn't obvious that's what CPI + // intends. + // + // FWIW their firmware files all appear to be below the limit. + let packet_number = self.packet_number().overflowing_add(1).0; + self.set_packet_number(packet_number); + packet_number + } + + fn data(&self) -> [u8; FLASH_DATA_PACKET] { + // The unwrap is safe here, and can never panic because the slice is guaranteed to be the + // correct length. + self.buf[index::DATA0..=index::DATA31].try_into().unwrap() + } + + fn data_ref(&self) -> &[u8] { + self.buf[index::DATA0..=index::DATA31].as_ref() + } + + fn set_data(&mut self, data: &[u8]) { + assert_eq!(data.len(), FLASH_DATA_PACKET); + + self.buf[index::DATA0..=index::DATA31].copy_from_slice(data); + } +} + +impl fmt::Display for FlashDownloadMessage8bit_32 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, PacketNumber: {}, Data: {:x?}", + self.acknak(), + self.device_type(), + self.message_type(), + self.packet_number(), + self.data(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn flash_download_message_8bit_64_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x47, 0x50, + // Packet number (little-endian) + 0x34, 0x12, + // Data + 0x54, 0x01, 0x03, 0x04, 0x00, 0xa0, 0x00, 0x20, 0x00, 0x7f, 0x5a, 0x01, 0xc0, 0x1f, 0x00, 0x00, + 0x59, 0xae, 0x00, 0x20, 0x03, 0x00, 0x07, 0x32, 0x38, 0x36, 0x31, 0x30, 0x31, 0x31, 0x30, 0x33, + 0x54, 0x01, 0x03, 0x04, 0x00, 0xa0, 0x00, 0x20, 0x00, 0x7f, 0x5a, 0x01, 0xc0, 0x1f, 0x00, 0x00, + 0x59, 0xae, 0x00, 0x20, 0x03, 0x00, 0x07, 0x32, 0x38, 0x36, 0x31, 0x30, 0x31, 0x31, 0x03, 0x33, + // ETX | Checksum + 0x03, 0x02, + ]; + + let mut msg = FlashDownloadMessage8bit_64::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.packet_number(), 0x1234); + assert_eq!(msg.data_ref(), msg_bytes[index::DATA0..=index::DATA63].as_ref()); + assert_eq!(msg.data().as_ref(), msg_bytes[index::DATA0..=index::DATA63].as_ref()); + + assert_eq!(msg.increment_packet_number(), 0x1235); + assert_eq!(msg.packet_number(), 0x1235); + + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn flash_download_message_8bit_32_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x27, 0x50, + // Packet number (little-endian) + 0x34, 0x12, + // Data + 0x54, 0x01, 0x03, 0x04, 0x00, 0xa0, 0x00, 0x20, 0x00, 0x7f, 0x5a, 0x01, 0xc0, 0x1f, 0x00, 0x00, + 0x59, 0xae, 0x00, 0x20, 0x03, 0x00, 0x07, 0x32, 0x38, 0x36, 0x31, 0x30, 0x31, 0x31, 0x30, 0x33, + // ETX | Checksum + 0x03, 0x95, + ]; + + let mut msg = FlashDownloadMessage8bit_32::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.packet_number(), 0x1234); + assert_eq!(msg.data_ref(), msg_bytes[index::DATA0..=index::DATA31].as_ref()); + assert_eq!(msg.data().as_ref(), msg_bytes[index::DATA0..=index::DATA31].as_ref()); + + assert_eq!(msg.increment_packet_number(), 0x1235); + assert_eq!(msg.packet_number(), 0x1235); + + Ok(()) + } +} diff --git a/src/flash_download/reply_7bit.rs b/src/flash_download/reply_7bit.rs new file mode 100644 index 0000000..3508459 --- /dev/null +++ b/src/flash_download/reply_7bit.rs @@ -0,0 +1,123 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::FLASH_DOWNLOAD_REPLY_7BIT, + seven_bit_u16, u16_seven_bit, MessageOps, MessageType, +}; + +use super::FlashDownloadReply; + +mod index { + pub const PACKET0: usize = 3; + //pub const PACKET1: usize = 4; + //pub const PACKET2: usize = 5; + pub const PACKET3: usize = 6; +} + +/// Flash Download Reply - 7-bit protocol (Original algorithm) +/// +/// The Packet Number reported by the device is the last successfully received packet number. +/// The ACK/NAK of the reply is important. +/// +/// * If the device ACKs the packet, the host should step to the next packet. +/// +/// * If the device NAKs the packet, then the host needs to resynchronize with the device. This is +/// accomplished by changing the block number to the value contained in the reply plus one. An +/// example is shown below. +/// +/// Example: +/// +/// ```rust +/// let rpn = [0x1, 0x2, 0x3, 0x4]; +/// let hi = ((rpn[0] & 0xf) << 4) | (rpn[1] & 0xf); +/// let lo = ((rpn[2] & 0xf) << 4) | (rpn[3] & 0xf); +/// let packet_num = u16::from_be_bytes([hi, lo]); +/// assert_eq!(packet_num, 0x12_34); +/// +/// let reply_block_num = packet_num + 1; +/// ``` +/// +/// The Flash Download Reply (7-bit) is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Packet #0 | Packet #1 | Packet #2 | Packet #3 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:---------:|:---------:|:---------:|:---------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +/// | Value | 0x02 | 0x09 | 0x5n | 0x0n | 0x0n | 0x0n | 0x0n | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FlashDownloadReply7bit { + buf: [u8; FLASH_DOWNLOAD_REPLY_7BIT], +} + +impl FlashDownloadReply7bit { + /// Creates a new [FlashDownloadReply7bit] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FLASH_DOWNLOAD_REPLY_7BIT], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } +} + +impl_default!(FlashDownloadReply7bit); +impl_message_ops!(FlashDownloadReply7bit); +impl_omnibus_nop_reply!(FlashDownloadReply7bit); + +impl FlashDownloadReply for FlashDownloadReply7bit { + fn packet_number(&self) -> u16 { + // In the 7-bit protocol, packet numbers are stored as 16-bit big-endian numbers encoded as 4-byte slices with the + // significant bits in the lower nibble of each byte + seven_bit_u16(self.buf[index::PACKET0..=index::PACKET3].as_ref()) + } + + fn set_packet_number(&mut self, n: u16) { + // In the 7-bit protocol, packet numbers are stored as 16-bit big-endian numbers encoded as 4-byte slices with the + // significant bits in the lower nibble of each byte + self.buf[index::PACKET0..=index::PACKET3].copy_from_slice(u16_seven_bit(n).as_ref()); + } +} + +impl fmt::Display for FlashDownloadReply7bit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, PacketNumber: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.packet_number(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn flash_download_reply_7bit_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x09, 0x50, + // Packet number + 0x01, 0x02, 0x03, 0x04, + // ETX | Checksum + 0x03, 0x5d, + ]; + + let mut msg = FlashDownloadReply7bit::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.packet_number(), 0x1234); + + Ok(()) + } +} diff --git a/src/flash_download/reply_8bit.rs b/src/flash_download/reply_8bit.rs new file mode 100644 index 0000000..cbf5d0c --- /dev/null +++ b/src/flash_download/reply_8bit.rs @@ -0,0 +1,108 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::FLASH_DOWNLOAD_REPLY_8BIT, + MessageOps, MessageType, +}; + +use super::FlashDownloadReply; + +mod index { + pub const PACKET0: usize = 3; + pub const PACKET1: usize = 4; +} + +/// Flash Download Reply - 8-bit protocol (Fast serial algorithm) +/// +/// The Packet Number reported by the device is the last successfully received packet number in little +/// endian format. +/// +/// The ACK/NAK of the reply is important. +/// +/// * If the device ACKs the packet, the host should step to the next packet. +/// +/// * If the device NAKs the packet, then the host needs to resynchronize with the device. This is +/// accomplished by changing the block number to the value contained in the reply plus one. +/// +/// The Flash Download Reply (8-bit) is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Packet #0 | Packet #1 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:---------:|:---------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +/// | Value | 0x02 | 0x07 | 0x5n | 0xnn | 0xnn | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FlashDownloadReply8bit { + buf: [u8; FLASH_DOWNLOAD_REPLY_8BIT], +} + +impl FlashDownloadReply8bit { + /// Creates a new [FlashDownloadReply8bit] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FLASH_DOWNLOAD_REPLY_8BIT], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } +} + +impl_default!(FlashDownloadReply8bit); +impl_message_ops!(FlashDownloadReply8bit); +impl_omnibus_nop_reply!(FlashDownloadReply8bit); + +impl FlashDownloadReply for FlashDownloadReply8bit { + fn packet_number(&self) -> u16 { + // In the 8-bit protocol, packet numbers are stored as 16-bit little endian values + u16::from_le_bytes([self.buf[index::PACKET0], self.buf[index::PACKET1]]) + } + + fn set_packet_number(&mut self, n: u16) { + // In the 8-bit protocol, packet numbers are stored as 16-bit little endian values + self.buf[index::PACKET0..=index::PACKET1].copy_from_slice(n.to_le_bytes().as_ref()); + } +} + +impl fmt::Display for FlashDownloadReply8bit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, PacketNumber: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.packet_number(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn flash_download_reply_8bit_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x07, 0x50, + // Packet number (little-endian) + 0x34, 0x12, + // ETX | Checksum + 0x03, 0x71, + ]; + + let mut msg = FlashDownloadReply8bit::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.packet_number(), 0x1234); + + Ok(()) + } +} diff --git a/src/flash_download/start_download.rs b/src/flash_download/start_download.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/flash_download/start_download.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/flash_download/start_download/command.rs b/src/flash_download/start_download/command.rs new file mode 100644 index 0000000..bbf1a69 --- /dev/null +++ b/src/flash_download/start_download/command.rs @@ -0,0 +1,94 @@ +use crate::{ + impl_default, impl_message_ops, len::START_DOWNLOAD_COMMAND, ExtendedNoteReporting, MessageOps, + MessageType, +}; + +mod index { + pub const DATA2: usize = 5; +} + +/// Host Start Download Command +/// +/// The host command shall start the device in Download mode and data 2 contains the option of either +/// being all zeros or having the value 0x10 depending on whether or not extended note mode is supported +/// with the device configuration. (See 4.2.2 for more details on extended note mode option). +/// +/// | STX | LEN | CTRL | DATA0 | DATA1 | DATA2 | ETX | CHK | +/// |------|------|------|-------|-------|---------|------|------| +/// | 0x02 | 0x08 | 0x5n | 0x00 | 0x00 | 0x00/10 | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct StartDownloadCommand { + buf: [u8; START_DOWNLOAD_COMMAND], +} + +impl StartDownloadCommand { + /// Creates a new [StartDownloadCommand] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; START_DOWNLOAD_COMMAND], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } + + /// Gets whether extended note mode is supported with the device configuration. + pub fn extended_note(&self) -> ExtendedNoteReporting { + // Shift the data byte right four bits (0x01 = 0x10 >> 4) to convert to the bool_enum impl + (self.buf[index::DATA2] >> 4).into() + } + + /// Sets whether extended note mode is supported with the device configuration. + pub fn set_extended_note(&mut self, extended: ExtendedNoteReporting) { + let b: u8 = extended.into(); + // Shift the data byte left four bits (0x10 = 0x01 << 4) to convert from the bool_enum impl + self.buf[index::DATA2] = b << 4; + } +} + +impl_default!(StartDownloadCommand); +impl_message_ops!(StartDownloadCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn start_download_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x50, + // Data + 0x00, 0x00, 0x10, + // ETX | Checksum + 0x03, 0x48, + ]; + + let mut msg = StartDownloadCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.extended_note(), ExtendedNoteReporting::Set); + + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x50, + // Data + 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x58, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.extended_note(), ExtendedNoteReporting::Unset); + + Ok(()) + } +} diff --git a/src/flash_download/start_download/reply.rs b/src/flash_download/start_download/reply.rs new file mode 100644 index 0000000..13edf18 --- /dev/null +++ b/src/flash_download/start_download/reply.rs @@ -0,0 +1,118 @@ +use crate::std; +use std::fmt; + +use crate::{ + bool_enum, impl_default, impl_message_ops, impl_omnibus_nop_reply, len::START_DOWNLOAD_REPLY, + MessageOps, MessageType, +}; + +mod index { + pub const DATA3: usize = 6; +} + +bool_enum!( + DownloadReady, + "Indicates whether the device is ready to enter the firmware downloading phase." +); + +/// Host Start Download Reply +/// +/// Data 3 contains the status of the device. If the Flash Download bit (denoted by 0x02) is not set, the +/// device is not yet in download mode and the Start Download command must be resent. When that bit is +/// set, the device is expecting the host to enter the downloading phase of the download process. +/// +/// | STX | LEN | CTRL | DATA0 | DATA1 | DATA2 | DATA3 | DATA4 | DATA5 | ETX | CHK | +/// |------|------|------|-------|-------|-------|---------|-------|-------|------|------| +/// | 0x02 | 0x0B | 0x5n | nn | nn | nn | 0x00/02 | nn | nn | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct StartDownloadReply { + buf: [u8; START_DOWNLOAD_REPLY], +} + +impl StartDownloadReply { + /// Creates a new [StartDownloadReply] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; START_DOWNLOAD_REPLY], + }; + + msg.init(); + msg.set_message_type(MessageType::FirmwareDownload); + + msg + } + + /// Gets whether the device is ready to enter the firmware download phase. + pub fn download_ready(&self) -> DownloadReady { + // Shift the Data 3 byte right 1 bit to convert to the bool_enum impl + // 0x01 = 0x02 >> 1; + ((self.buf[index::DATA3] & 0b10) >> 1).into() + } + + /// Sets whether the device is ready to enter the firmware download phase. + pub fn set_download_ready(&mut self, ready: DownloadReady) { + let b: u8 = ready.into(); + // Shift the bool_enum value left 1 bit to convert to the Data 3 byte value + // 0x02 = 0x01 << 1; + self.buf[index::DATA3] = b << 1; + } +} + +impl_default!(StartDownloadReply); +impl_message_ops!(StartDownloadReply); +impl_omnibus_nop_reply!(StartDownloadReply); + +impl fmt::Display for StartDownloadReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, DownloadReady: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.download_ready(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn start_download_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0b, 0x50, + // Data + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x59, + ]; + + let mut msg = StartDownloadReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.download_ready(), DownloadReady::Set); + + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0b, 0x50, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x5b, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::FirmwareDownload); + assert_eq!(msg.download_ready(), DownloadReady::Unset); + + Ok(()) + } +} diff --git a/src/hardware.rs b/src/hardware.rs new file mode 100644 index 0000000..29c73d2 --- /dev/null +++ b/src/hardware.rs @@ -0,0 +1,632 @@ +#[cfg(not(feature = "std"))] +use alloc::string::String; + +use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; + +use crate::std; +use std::fmt; + +use crate::{ + cash::Currency, + jsonrpc::{CLOSE_BRACE, OPEN_BRACE}, + status::{DeviceState, DeviceStateFlags}, +}; + +pub const ENV_BAU_DEVICE: &str = "SERIAL_PATH_BAU"; +pub const ENV_CDU_DEVICE: &str = "SERIAL_PATH_CPU"; +pub const DEFAULT_BAU_DEV_PATH: &str = "/dev/bau"; +pub const DEFAULT_CDU_DEV_PATH: &str = "/dev/cdu"; + +/// HardwareComponent is a list of possible hardware components used on the platform +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum HardwareComponent { + /// Cash dispenser unit + CDU, + /// Electronic payment process + EPP, + /// Stores interface unit + SIU, + /// Remote pickup unit + RPU, + /// Magnetic card reader + MCR, + /// Bill acceptor unit + BAU, + /// Bill acceptor 2? + BA2, + /// Bar code scanner? + BCS, + /// Camera + CAM, + /// Universal power supply + UPS, +} + +impl HardwareComponent { + pub const fn default() -> Self { + Self::BAU + } +} + +impl From for &'static str { + fn from(h: HardwareComponent) -> Self { + match h { + HardwareComponent::CDU => "CDU", + HardwareComponent::EPP => "EPP", + HardwareComponent::SIU => "SIU", + HardwareComponent::RPU => "RPU", + HardwareComponent::MCR => "MCR", + HardwareComponent::BAU => "BAU", + HardwareComponent::BA2 => "BA2", + HardwareComponent::BCS => "BCS", + HardwareComponent::CAM => "CAM", + HardwareComponent::UPS => "UPS", + } + } +} + +impl From<&HardwareComponent> for &'static str { + fn from(h: &HardwareComponent) -> Self { + (*h).into() + } +} + +impl fmt::Display for HardwareComponent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +/// HardwareState represents the different states hardware can be in +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize)] +#[serde(field_identifier, rename_all = "UPPERCASE")] +pub enum HardwareState { + /// Everything is running properly + OK, + /// The hardware is missing + Missing, + /// The hardware emitted a warning + Warning, + /// The hardware emitted an error + Error, +} + +impl HardwareState { + pub const fn default() -> Self { + Self::OK + } +} + +impl Serialize for HardwareState { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Self::OK => serializer.serialize_unit_variant("HardwareState", 0, "OK"), + Self::Missing => serializer.serialize_unit_variant("HardwareState", 1, "MISSING"), + Self::Warning => serializer.serialize_unit_variant("HardwareState", 2, "WARNING"), + Self::Error => serializer.serialize_unit_variant("HardwareState", 3, "ERROR"), + } + } +} + +impl From for HardwareState { + fn from(dev_state: DeviceState) -> Self { + Self::from(DeviceStateFlags::from(dev_state)) + } +} + +impl From for HardwareState { + fn from(dev_state: DeviceStateFlags) -> Self { + match dev_state { + DeviceStateFlags::Disconnected | DeviceStateFlags::CashBoxRemoved => Self::Missing, + DeviceStateFlags::PowerUp + | DeviceStateFlags::Initialize + | DeviceStateFlags::Download + | DeviceStateFlags::Idle + | DeviceStateFlags::HostDisabled + | DeviceStateFlags::BusyCalculation + | DeviceStateFlags::Escrowed + | DeviceStateFlags::Accepting + | DeviceStateFlags::Stacking + | DeviceStateFlags::Returning + | DeviceStateFlags::Paused + | DeviceStateFlags::Calibration + | DeviceStateFlags::Dispensing + | DeviceStateFlags::FloatingDown + | DeviceStateFlags::IdleInEscrowSession + | DeviceStateFlags::HostDisabledInEscrowSession + | DeviceStateFlags::PatternRecovering => Self::OK, + DeviceStateFlags::Cheated + | DeviceStateFlags::StackerFull + | DeviceStateFlags::TransportOpened + | DeviceStateFlags::EscrowStorageFull + | DeviceStateFlags::UnknownDocumentsDetected => Self::Warning, + DeviceStateFlags::Jammed + | DeviceStateFlags::Failure + | DeviceStateFlags::Stalled + | DeviceStateFlags::DisabledAndJammed + | DeviceStateFlags::Disabled => Self::Error, + _ => Self::Error, + } + } +} + +impl From for &'static str { + fn from(h: HardwareState) -> Self { + match h { + HardwareState::OK => "OK", + HardwareState::Missing => "MISSING", + HardwareState::Warning => "WARNING", + HardwareState::Error => "ERROR", + } + } +} + +impl From<&HardwareState> for &'static str { + fn from(h: &HardwareState) -> Self { + (*h).into() + } +} + +impl fmt::Display for HardwareState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +/// BillAcceptorStatusDetails represents detailed information about the bill acceptor hardware +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct BillAcceptorStatusDetails { + // Was the cashbox removed, or has the field not been set + cashbox_removed: Option, + // The firmware version + firmware_version: Option, + // ISO string for the type of currency + currency: Option, + // Is the bill acceptor jammed, or has the field not been set + jammed: Option, +} + +impl BillAcceptorStatusDetails { + /// Create a new BillAcceptorStatusDetails + pub const fn new( + cashbox_removed: Option, + firmware_version: Option, + currency: Option, + jammed: Option, + ) -> Self { + Self { + cashbox_removed, + firmware_version, + currency, + jammed, + } + } + + pub const fn default() -> Self { + Self { + cashbox_removed: None, + firmware_version: None, + currency: None, + jammed: None, + } + } + + /// Builder function to include cashbox removed status + pub fn with_cashbox_removed(mut self, cashbox_removed: bool) -> Self { + self.cashbox_removed = Some(cashbox_removed); + self + } + + /// Builder function to include firmware version + pub fn with_firmware_version(mut self, firmware_version: &str) -> Self { + self.firmware_version = Some(firmware_version.into()); + self + } + + /// Builder function to include currency status + pub fn with_currency(mut self, currency: Currency) -> Self { + self.currency = Some(currency); + self + } + + /// Builder function to include jammed status + pub fn with_jammed(mut self, jammed: bool) -> Self { + self.jammed = Some(jammed); + self + } + + /// Get whether the cashbox is removed + /// + /// If none is set, returns false + pub fn cashbox_removed(&self) -> bool { + if let Some(ret) = self.cashbox_removed { + ret + } else { + false + } + } + + /// Set whether the cashbox is removed + pub fn set_cashbox_removed(&mut self, removed: bool) { + self.cashbox_removed = Some(removed); + } + + /// Unset the cashbox removed status + pub fn unset_cashbox_removed(&mut self) { + self.cashbox_removed = None; + } + + /// Get the firmware version + /// + /// If none is set, returns the empty string + pub fn firmware_version(&self) -> &str { + if let Some(ret) = self.firmware_version.as_ref() { + ret + } else { + "" + } + } + + /// Set the firmware version + pub fn set_firmware_version(&mut self, version: S) + where + S: Into, + { + self.firmware_version = Some(version.into()); + } + + /// Unset the firmware version + pub fn unset_firmware_version(&mut self) { + self.firmware_version = None + } + + /// Get the BAU currency + /// + /// If none is set, returns the default currency (USD) + pub fn currency(&self) -> Currency { + if let Some(ret) = self.currency { + ret + } else { + Currency::USD + } + } + + /// Set the BAU currency + pub fn set_currency(&mut self, currency: Currency) { + self.currency = Some(currency); + } + + /// Unset the BAU currency + pub fn unset_currency(&mut self) { + self.currency = None; + } + + /// Get whether the BAU is jammed + /// + /// If none is set, returns false + pub fn jammed(&self) -> bool { + if let Some(ret) = self.jammed { + ret + } else { + false + } + } + + /// Set whether the BAU is jammed + pub fn set_jammed(&mut self, jammed: bool) { + self.jammed = Some(jammed); + } + + /// Unset the jammed status + pub fn unset_jammed(&mut self) { + self.jammed = None; + } +} + +impl fmt::Display for BillAcceptorStatusDetails { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{OPEN_BRACE}")?; + if let Some(ret) = self.cashbox_removed { + write!(f, "\"cashbox_removed\":{}", ret)?; + } + if let Some(ret) = self.firmware_version.as_ref() { + write!(f, ", \"firmware_version\":\"{}\"", ret)?; + } + if let Some(ret) = self.currency { + write!(f, ", \"currency\":\"{}\"", ret)?; + } + if let Some(ret) = self.jammed { + write!(f, ", \"jammed\":{}", ret)?; + } + write!(f, "{CLOSE_BRACE}") + } +} + +impl Serialize for BillAcceptorStatusDetails { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut bau_status = serializer.serialize_struct("BillAcceptorStatusDetails", 4)?; + + bau_status.serialize_field("cashbox_removed", &self.cashbox_removed)?; + bau_status.serialize_field("firmware_version", &self.firmware_version)?; + bau_status.serialize_field("currency", &self.currency)?; + bau_status.serialize_field("jammed", &self.jammed)?; + + bau_status.end() + } +} + +/// HardwareStatusDetails represents the type of hardware details are provided for in +/// HardwareStatus +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum HardwareStatusDetails { + NULL, + BAU, +} + +impl Default for HardwareStatusDetails { + fn default() -> Self { + Self::NULL + } +} + +impl From for &'static str { + fn from(h: HardwareStatusDetails) -> Self { + match h { + HardwareStatusDetails::NULL => "NULL", + HardwareStatusDetails::BAU => "BAU", + } + } +} + +impl From<&HardwareStatusDetails> for &'static str { + fn from(h: &HardwareStatusDetails) -> &'static str { + (*h).into() + } +} + +impl fmt::Display for HardwareStatusDetails { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +/// HardwareStatus represents basics information about the current status of hardware +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct HardwareStatus { + // Hardware component type + component: HardwareComponent, + // Current state of the hardware + state: HardwareState, + // Basic description of the hardware status + description: String, + // Details of the BAU status + details: BillAcceptorStatusDetails, +} + +impl HardwareStatus { + pub const fn new( + component: HardwareComponent, + state: HardwareState, + description: String, + details: BillAcceptorStatusDetails, + ) -> Self { + Self { + component, + state, + description, + details, + } + } + + pub const fn default() -> Self { + Self { + component: HardwareComponent::default(), + state: HardwareState::default(), + description: String::new(), + details: BillAcceptorStatusDetails::default(), + } + } + + /// Get the hardware component + pub fn component(&self) -> HardwareComponent { + self.component + } + + /// Set the hardware component + pub fn set_component(&mut self, component: HardwareComponent) { + self.component = component; + } + + /// Get the hardware state + pub fn state(&self) -> HardwareState { + self.state + } + + /// Set the hardware state + pub fn set_state(&mut self, state: HardwareState) { + self.state = state; + } + + /// Set the "worst" state for the HardwareStatus + /// (e.g. [HardwareState::Error](HardwareState) takes precedence over [HardwareState::OK](HardwareState)) + pub fn set_priority_state(&mut self, proposed: HardwareState) { + if (proposed as u32) > (self.state as u32) { + self.state = proposed; + } + } + + /// Get the hardware description + pub fn description(&self) -> &str { + &self.description + } + + /// Set the hardware description + pub fn set_description(&mut self, description: S) + where + S: Into, + { + self.description = description.into(); + } + + /// Get the BAU status details + pub fn details(&self) -> &BillAcceptorStatusDetails { + &self.details + } + + /// Get a mutable reference to the BAU status details + pub fn details_mut(&mut self) -> &mut BillAcceptorStatusDetails { + &mut self.details + } + + /// Set the BAU status details + pub fn set_details(&mut self, details: BillAcceptorStatusDetails) { + self.details = details; + } +} + +impl fmt::Display for HardwareStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let component = self.component(); + let state = self.state(); + let description = self.description(); + let details = self.details(); + + write!(f, "{OPEN_BRACE}\"component\":\"{component}\",\"state\":{state},\"description\":\"{description}\",\"details\":{details}{CLOSE_BRACE}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, Result}; + + #[test] + fn test_hardware_component_serde() -> Result<()> { + assert_eq!(serde_json::to_string(&HardwareComponent::CDU)?, "\"CDU\""); + assert_eq!(serde_json::to_string(&HardwareComponent::EPP)?, "\"EPP\""); + assert_eq!(serde_json::to_string(&HardwareComponent::SIU)?, "\"SIU\""); + assert_eq!(serde_json::to_string(&HardwareComponent::RPU)?, "\"RPU\""); + assert_eq!(serde_json::to_string(&HardwareComponent::MCR)?, "\"MCR\""); + assert_eq!(serde_json::to_string(&HardwareComponent::BAU)?, "\"BAU\""); + assert_eq!(serde_json::to_string(&HardwareComponent::BA2)?, "\"BA2\""); + assert_eq!(serde_json::to_string(&HardwareComponent::BCS)?, "\"BCS\""); + assert_eq!(serde_json::to_string(&HardwareComponent::CAM)?, "\"CAM\""); + assert_eq!(serde_json::to_string(&HardwareComponent::UPS)?, "\"UPS\""); + + assert_eq!( + serde_json::from_str::("\"CDU\"")?, + HardwareComponent::CDU + ); + assert_eq!( + serde_json::from_str::("\"EPP\"")?, + HardwareComponent::EPP + ); + assert_eq!( + serde_json::from_str::("\"SIU\"")?, + HardwareComponent::SIU + ); + assert_eq!( + serde_json::from_str::("\"RPU\"")?, + HardwareComponent::RPU + ); + assert_eq!( + serde_json::from_str::("\"MCR\"")?, + HardwareComponent::MCR + ); + assert_eq!( + serde_json::from_str::("\"BAU\"")?, + HardwareComponent::BAU + ); + assert_eq!( + serde_json::from_str::("\"BA2\"")?, + HardwareComponent::BA2 + ); + assert_eq!( + serde_json::from_str::("\"BCS\"")?, + HardwareComponent::BCS + ); + assert_eq!( + serde_json::from_str::("\"CAM\"")?, + HardwareComponent::CAM + ); + assert_eq!( + serde_json::from_str::("\"UPS\"")?, + HardwareComponent::UPS + ); + + Ok(()) + } + + #[test] + fn test_hardware_state_serde() -> Result<()> { + assert_eq!(serde_json::to_string(&HardwareState::OK)?, "\"OK\""); + assert_eq!( + serde_json::to_string(&HardwareState::Missing)?, + "\"MISSING\"" + ); + assert_eq!( + serde_json::to_string(&HardwareState::Warning)?, + "\"WARNING\"" + ); + assert_eq!(serde_json::to_string(&HardwareState::Error)?, "\"ERROR\""); + + Ok(()) + } + + #[test] + fn test_bill_acceptor_status_details_serde() -> Result<()> { + let bau_status_filled = BillAcceptorStatusDetails { + cashbox_removed: Some(true), + firmware_version: Some("version-1.0".into()), + currency: Some(Currency::USD), + jammed: Some(false), + }; + + let expected = "{\"cashbox_removed\":true,\"firmware_version\":\"version-1.0\",\"currency\":\"USD\",\"jammed\":false}"; + + assert_eq!(serde_json::to_string(&bau_status_filled)?, expected); + + let des_filled: BillAcceptorStatusDetails = serde_json::from_str(expected)?; + + assert_eq!(des_filled, bau_status_filled); + + let bau_status_sparse = BillAcceptorStatusDetails { + cashbox_removed: None, + firmware_version: Some("version-1.0".into()), + currency: Some(Currency::USD), + jammed: None, + }; + + let expected = "{\"cashbox_removed\":null,\"firmware_version\":\"version-1.0\",\"currency\":\"USD\",\"jammed\":null}"; + + assert_eq!(serde_json::to_string(&bau_status_sparse)?, expected); + + let sparse_json = "{\"firmware_version\":\"version-1.0\",\"currency\":\"USD\"}"; + let des_sparse: BillAcceptorStatusDetails = serde_json::from_str(sparse_json)?; + assert_eq!(des_sparse, bau_status_sparse); + + Ok(()) + } +} + +#[cfg(feature = "std")] +/// Get the device path from the environment, or return the default path +pub fn get_device_path(env_key: &str, default_path: &str) -> String { + std::env::var(env_key).unwrap_or(default_path.into()) +} + +#[cfg(not(feature = "std"))] +pub fn get_device_path(_env_key: &str, default_path: &str) -> String { + default_path.into() +} diff --git a/src/jsonrpc.rs b/src/jsonrpc.rs new file mode 100644 index 0000000..2ef11a4 --- /dev/null +++ b/src/jsonrpc.rs @@ -0,0 +1,465 @@ +#[cfg(not(feature = "std"))] +use alloc::string::String; + +use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; + +use crate::std; +use std::fmt; + +use crate::{ + cash::{BillAcceptorConfig, CashInsertionEvent, DispenseRequest}, + error::{JsonRpcError, JsonRpcResult as Result}, + hardware::HardwareStatus, + method::Method, +}; + +pub const JRPC_VERSION: &str = "2.0"; +pub const OPEN_BRACE: &str = "{"; +pub const CLOSE_BRACE: &str = "}"; + +/// HAL payload types +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum HalPayload { + Empty(()), + Error(JsonRpcError), + CashInsertionEvent(CashInsertionEvent), + DispenseRequest(DispenseRequest), + BillAcceptorConfig(BillAcceptorConfig), + HardwareStatus(HardwareStatus), +} + +impl Default for HalPayload { + fn default() -> Self { + Self::Empty(()) + } +} + +impl fmt::Display for HalPayload { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Empty(()) => write!(f, "()"), + Self::Error(inner) => write!(f, "{inner}"), + Self::CashInsertionEvent(inner) => write!(f, "{inner}"), + Self::DispenseRequest(inner) => write!(f, "{inner}"), + Self::BillAcceptorConfig(inner) => write!(f, "{inner}"), + Self::HardwareStatus(inner) => write!(f, "{inner}"), + } + } +} + +/// JSON-RPC ID +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum JsonRpcId { + Integer(u64), + String(String), +} + +impl fmt::Display for JsonRpcId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Integer(id) => write!(f, "{id}"), + Self::String(id) => write!(f, "{id}"), + } + } +} + +/// JSON-RPC message carrying a HAL payload +#[repr(C)] +#[derive(Debug, Deserialize, Serialize)] +pub struct JsonRpcMessage { + /// JSON-RPC version string (should always be "2.0") + jsonrpc: String, + /// JSON-RPC ID (typically 1) + id: Option, + /// JSON-RPC method being called/responded to + method: Option, + /// HAL message payload + data: Option, +} + +impl JsonRpcMessage { + /// Create a new JsonRpcMessage with the provided method and payload + pub fn new(method: Method, data: HalPayload) -> Self { + Self { + jsonrpc: JRPC_VERSION.into(), + id: Some(JsonRpcId::Integer(1)), + method: Some(method), + data: Some(data), + } + } + + /// Create a JsonRpcMessage from a method and payload + /// + /// Validates the method-payload pair, and returns the JsonRpcMessage + /// Returns error for an invalid method-payload pair + pub fn create(method: Method, payload: HalPayload) -> Result { + let msg = match (method, &payload) { + (Method::Accept, HalPayload::BillAcceptorConfig(_)) + | (Method::Stop, _) + | (Method::Dispense, _) + | (Method::Reject, HalPayload::CashInsertionEvent(_)) + | (Method::Stack, HalPayload::CashInsertionEvent(_)) + | (Method::Status, HalPayload::HardwareStatus(_)) + | (_, HalPayload::Empty(_)) => Self::new(method, payload), + _ => return Err(JsonRpcError::failure("invalid method-payload combination")), + }; + + Ok(msg) + } + + pub fn jsonrpc(&self) -> &str { + self.jsonrpc.as_str() + } + + pub fn id(&self) -> u64 { + if let Some(&JsonRpcId::Integer(id)) = self.id.as_ref() { + id + } else { + 1 + } + } + + pub fn set_id(&mut self, id: u64) { + self.id = Some(JsonRpcId::Integer(id)); + } + + /// Get the JSON-RPC method + pub fn method(&self) -> Method { + self.method.unwrap_or(Method::default()) + } + + /// Get the JSON-RPC data (HalPayload) + pub fn data(&self) -> Option<&HalPayload> { + self.data.as_ref() + } +} + +impl Default for JsonRpcMessage { + fn default() -> Self { + Self::new(Method::default(), HalPayload::default()) + } +} + +impl fmt::Display for JsonRpcMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let jsonrpc = self.jsonrpc(); + let id = match self.id.as_ref() { + Some(JsonRpcId::Integer(id)) => format!("{}", id), + Some(JsonRpcId::String(id)) => id.clone(), + None => "1".into(), + }; + let method = if let Some(method) = self.method.as_ref() { + format!("\"method\":\"{method}\",") + } else { + String::new() + }; + let data = if let Some(data) = self.data.as_ref() { + format!("\"data\":{data}") + } else { + String::new() + }; + write!( + f, + "{OPEN_BRACE}\"json-rpc\":\"{jsonrpc}\", \"id\":\"{id}\",{method} {data}{CLOSE_BRACE}", + ) + } +} + +impl From for JsonRpcMessage { + fn from(msg: JsonRpcRequest) -> Self { + Self { + jsonrpc: msg.jsonrpc, + id: msg.id, + method: msg.method, + data: msg.params, + } + } +} + +impl From for JsonRpcMessage { + fn from(msg: JsonRpcResponse) -> Self { + Self { + jsonrpc: msg.jsonrpc, + id: msg.id, + method: None, + data: msg.result, + } + } +} + +/// JSON-RPC message carrying a HAL payload +#[repr(C)] +#[derive(Clone, Debug, Deserialize)] +pub struct JsonRpcRequest { + /// JSON-RPC version string (should always be "2.0") + jsonrpc: String, + /// JSON-RPC ID (typically 1) + id: Option, + /// JSON-RPC method being called/responded to + method: Option, + /// HAL message payload + params: Option, +} + +impl JsonRpcRequest { + /// Create a new JsonRpcMessage with the provided method and payload + pub fn new(method: Method, params: HalPayload) -> Self { + Self { + jsonrpc: JRPC_VERSION.into(), + id: Some(JsonRpcId::Integer(1)), + method: Some(method), + params: Some(params), + } + } + + pub fn new_with_no_id(method: Method, params: HalPayload) -> Self { + Self { + jsonrpc: JRPC_VERSION.into(), + id: None, + method: Some(method), + params: Some(params), + } + } + + pub fn jsonrpc(&self) -> &str { + self.jsonrpc.as_str() + } + + /// Gets the JSON-RPC ID value. + pub fn id(&self) -> u64 { + if let Some(&JsonRpcId::Integer(id)) = self.id.as_ref() { + id + } else { + 1 + } + } + + /// Sets the JSON-RPC ID value. + pub fn set_id(&mut self, id: u64) { + self.id = Some(JsonRpcId::Integer(id)); + } + + /// Unsets the JSON-RPC ID value. + pub fn unset_id(&mut self) -> Option { + self.id.take() + } + + /// Get the JSON-RPC method + pub fn method(&self) -> Method { + self.method.unwrap_or(Method::default()) + } + + /// Get the JSON-RPC request parameters (HalPayload) + pub fn params(&self) -> Option<&HalPayload> { + self.params.as_ref() + } +} + +impl Default for JsonRpcRequest { + fn default() -> Self { + Self::new(Method::default(), HalPayload::default()) + } +} + +impl fmt::Display for JsonRpcRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let jsonrpc = self.jsonrpc(); + let id = if let Some(id) = self.id.as_ref() { + format!(", \"id\":\"{id}\"") + } else { + String::new() + }; + let method = if let Some(method) = self.method.as_ref() { + format!(", \"method\":\"{method}\"") + } else { + String::new() + }; + let params = if let Some(params) = self.params.as_ref() { + format!(", \"params\":{params}") + } else { + String::new() + }; + write!( + f, + "{OPEN_BRACE}\"json-rpc\":\"{jsonrpc}\"{id}{method}{params}{CLOSE_BRACE}", + ) + } +} + +impl Serialize for JsonRpcRequest { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let mut request = serializer.serialize_struct("JsonRpcRequest", 4)?; + + request.serialize_field("jsonrpc", &self.jsonrpc)?; + + if let Some(id) = self.id.as_ref() { + request.serialize_field("id", id)?; + } + if let Some(method) = self.method.as_ref() { + request.serialize_field("method", method)?; + } + if let Some(params) = self.params.as_ref() { + request.serialize_field("params", params)?; + } + + request.end() + } +} + +/// JSON-RPC message carrying a HAL payload +#[repr(C)] +#[derive(Debug, Deserialize, Serialize)] +pub struct JsonRpcResponse { + /// JSON-RPC version string (should always be "2.0") + jsonrpc: String, + /// JSON-RPC ID (typically 1) + id: Option, + /// HAL message payload + result: Option, +} + +impl JsonRpcResponse { + /// Create a new JsonRpcMessage with the provided payload + pub fn new(result: HalPayload) -> Self { + Self { + jsonrpc: JRPC_VERSION.into(), + id: Some(JsonRpcId::Integer(1)), + result: Some(result), + } + } + + /// Create a new JsonRpcMessage with the provided ID and payload + pub fn new_with_id(id: u64, result: HalPayload) -> Self { + Self { + jsonrpc: JRPC_VERSION.into(), + id: Some(JsonRpcId::Integer(id)), + result: Some(result), + } + } + + /// Create a new JsonRpcMessage with the provided ID and no payload + pub fn new_with_id_only(id: u64) -> Self { + Self { + jsonrpc: JRPC_VERSION.into(), + id: Some(JsonRpcId::Integer(id)), + result: None, + } + } + + pub fn jsonrpc(&self) -> &str { + self.jsonrpc.as_str() + } + + pub fn id(&self) -> u64 { + if let Some(&JsonRpcId::Integer(id)) = self.id.as_ref() { + id + } else { + 1 + } + } + + pub fn set_id(&mut self, id: u64) { + self.id = Some(JsonRpcId::Integer(id)); + } + + /// Get the JSON-RPC response result (HalPayload) + pub fn result(&self) -> Option<&HalPayload> { + self.result.as_ref() + } +} + +impl Default for JsonRpcResponse { + fn default() -> Self { + Self::new(HalPayload::default()) + } +} + +impl fmt::Display for JsonRpcResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let jsonrpc = self.jsonrpc(); + let id = match self.id.as_ref() { + Some(JsonRpcId::Integer(id)) => format!("{}", id), + Some(JsonRpcId::String(id)) => id.clone(), + None => "1".into(), + }; + let result = if let Some(result) = self.result.as_ref() { + format!(", \"result\":{result}") + } else { + String::new() + }; + write!( + f, + "{OPEN_BRACE}\"json-rpc\":\"{jsonrpc}\", \"id\":\"{id}\"{result}{CLOSE_BRACE}", + ) + } +} + +#[cfg(test)] +mod tests { + #[cfg(not(feature = "std"))] + use alloc::string::ToString; + + use super::*; + use crate::{ + cash::Currency, + error::JsonRpcError, + hardware::{BillAcceptorStatusDetails, HardwareComponent, HardwareState}, + }; + use serde_json::{self, Result}; + + #[test] + fn test_json_rpc_id_serde() -> Result<()> { + let id = JsonRpcId::Integer(42); + let expected = "42"; + + assert_eq!(serde_json::to_string(&id)?, expected); + assert_eq!(serde_json::from_str::(expected)?, id); + + Ok(()) + } + + #[test] + fn test_json_rpc_hal_payload_serde() -> Result<()> { + let payload = HalPayload::Error(JsonRpcError::new(-32700, "Parse error")); + let expected = "{\"code\":{\"GenericError\":-32700},\"message\":\"Parse error\"}"; + + assert_eq!(serde_json::to_string(&payload)?, expected); + assert_eq!(serde_json::from_str::(expected)?, payload); + + let payload = HalPayload::CashInsertionEvent(CashInsertionEvent::new(Method::Accept, 42)); + let expected = "{\"event\":\"ACCEPT\",\"amount\":42}"; + + assert_eq!(serde_json::to_string(&payload)?, expected); + assert_eq!(serde_json::from_str::(expected)?, payload); + + let payload = HalPayload::DispenseRequest(DispenseRequest::new(42, Currency::USD)); + let expected = "{\"amount\":42,\"currency\":\"USD\"}"; + + assert_eq!(serde_json::to_string(&payload)?, expected); + assert_eq!(serde_json::from_str::(expected)?, payload); + + let payload = HalPayload::BillAcceptorConfig(BillAcceptorConfig::new(Currency::USD)); + let expected = "{\"currency\":\"USD\"}"; + + assert_eq!(serde_json::to_string(&payload)?, expected); + assert_eq!(serde_json::from_str::(expected)?, payload); + + let payload = HalPayload::HardwareStatus(HardwareStatus::new( + HardwareComponent::BAU, + HardwareState::OK, + "BAU is operating properly".to_string(), + BillAcceptorStatusDetails::default().with_firmware_version("1.0"), + )); + let expected = "{\"component\":\"BAU\",\"state\":\"OK\",\"description\":\"BAU is operating properly\",\"details\":{\"cashbox_removed\":null,\"firmware_version\":\"1.0\",\"currency\":null,\"jammed\":null}}"; + + assert_eq!(serde_json::to_string(&payload)?, expected); + assert_eq!(serde_json::from_str::(expected)?, payload); + + Ok(()) + } +} diff --git a/src/len.rs b/src/len.rs new file mode 100644 index 0000000..8a4dfeb --- /dev/null +++ b/src/len.rs @@ -0,0 +1,81 @@ +pub const OMNIBUS_COMMAND: usize = 8; +pub const OMNIBUS_REPLY: usize = 11; + +pub const AUX_COMMAND: usize = 8; +pub const AUX_REPLY: usize = 11; + +pub const CALIBRATE_COMMAND: usize = 8; +pub const CALIBRATE_REPLY: usize = 11; + +pub const START_DOWNLOAD_COMMAND: usize = 8; +pub const START_DOWNLOAD_REPLY: usize = 11; + +pub const BAUD_CHANGE_REQUEST: usize = 6; +pub const BAUD_CHANGE_REPLY: usize = 6; + +pub const FLASH_DATA_PACKET: usize = 32; +pub const FLASH_DATA_PACKET_64: usize = 64; + +pub const FLASH_DOWNLOAD_MESSAGE_7BIT: usize = 73; +pub const FLASH_DOWNLOAD_REPLY_7BIT: usize = 9; + +pub const FLASH_DOWNLOAD_MESSAGE_8BIT_64: usize = 71; +pub const FLASH_DOWNLOAD_MESSAGE_8BIT_32: usize = 39; +pub const FLASH_DOWNLOAD_REPLY_8BIT: usize = 7; + +pub const QUERY_DEVICE_CAPABILITIES_COMMAND: usize = 8; +pub const QUERY_DEVICE_CAPABILITIES_REPLY: usize = 11; + +pub const QUERY_VARIANT_NAME_COMMAND: usize = 8; +pub const QUERY_VARIANT_NAME_REPLY: usize = 37; + +pub const QUERY_EXTENDED_NOTE_SPECIFICATION: usize = 10; +pub const EXTENDED_NOTE_REPLY: usize = 30; + +pub const SET_EXTENDED_NOTE_INHIBITS_BASE: usize = 9; +pub const EXTENDED_NOTE_INHIBITS_REPLY: usize = 11; +pub const EXTENDED_NOTE_INHIBITS_REPLY_ALT: usize = 12; + +pub const QUERY_VALUE_TABLE_COMMAND: usize = 9; +pub const QUERY_VALUE_TABLE_REPLY: usize = 82; + +pub const APPLICATION_ID_COMMAND: usize = 8; +pub const APPLICATION_ID_REPLY: usize = 14; + +pub const ADVANCED_BOOKMARK_MODE_COMMAND: usize = 10; +pub const ADVANCED_BOOKMARK_MODE_REPLY: usize = 13; + +pub const QUERY_SOFTWARE_CRC_COMMAND: usize = 8; +pub const QUERY_SOFTWARE_CRC_REPLY: usize = 11; + +pub const QUERY_BOOT_PART_NUMBER_COMMAND: usize = 8; +pub const QUERY_BOOT_PART_NUMBER_REPLY: usize = 14; + +pub const QUERY_APPLICATION_PART_NUMBER_COMMAND: usize = 8; +pub const QUERY_APPLICATION_PART_NUMBER_REPLY: usize = 14; + +pub const QUERY_APPLICATION_ID_COMMAND: usize = 8; +pub const QUERY_APPLICATION_ID_REPLY: usize = 14; + +pub const QUERY_VARIANT_PART_NUMBER_COMMAND: usize = 8; +pub const QUERY_VARIANT_PART_NUMBER_REPLY: usize = 14; + +pub const QUERY_VARIANT_ID_COMMAND: usize = 8; +pub const QUERY_VARIANT_ID_REPLY: usize = 14; + +pub const CLEAR_AUDIT_DATA_REQUEST: usize = 9; +pub const CLEAR_AUDIT_DATA_REQUEST_ACK: usize = 13; +pub const CLEAR_AUDIT_DATA_REQUEST_RESULTS: usize = 13; + +pub const SOFT_RESET: usize = 8; + +pub const SET_ESCROW_TIMEOUT_COMMAND: usize = 11; +pub const SET_ESCROW_TIMEOUT_REPLY: usize = 12; + +pub const NOTE_RETRIEVED_COMMAND: usize = 10; +pub const NOTE_RETRIEVED_REPLY: usize = 13; +pub const NOTE_RETRIEVED_EVENT: usize = 13; + +pub const METADATA: usize = 5; +pub const MIN_MESSAGE: usize = 5; +pub const MAX_MESSAGE: usize = 255; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c252c23 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,689 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +#[macro_use(format)] +extern crate alloc; + +#[macro_use(bitfield)] +extern crate bitfield; + +#[cfg(not(feature = "std"))] +pub(crate) use core as std; +#[cfg(feature = "std")] +pub(crate) use std; + +use std::{fmt, ops::Not}; + +/// Banknote types used across multiple messages +pub mod banknote; +/// Cash types used across multiple messages +pub mod cash; +/// Denomination types used +pub mod denomination; +/// Library error types +pub mod error; +/// Hardware status and related types +pub mod hardware; +/// JSON-RPC message and related types +pub mod jsonrpc; +/// Logging convenience helpers +pub mod logging; +mod macros; +/// JSON-RPC and device method types +pub mod method; +/// Device status types +pub mod status; + +pub use banknote::*; +pub use cash::*; +pub use denomination::*; +pub use error::*; +pub use hardware::*; +pub use jsonrpc::*; +pub use logging::*; +pub use method::*; +pub use status::*; + +/// Advanced Bookmark Mode - Extended (Type 0x07, Subtype 0x0D) +pub mod advanced_bookmark_mode; +/// Generic types for Auxilliary Command/Reply messages - Auxilliary (Type 0x06) +pub mod aux_command; +/// Clear Audit Data - Extended (Type 0x07, Subtype 0x1D) +pub mod clear_audit_data; +/// Generic types for Extended Command messages - Extended (Type 0x07) +pub mod extended_command; +/// Extended Note Inhibits - Extended (Type 0x07, Subtype 0x03) +pub mod extended_note_inhibits; +/// Extended Note Specification - Extended (Type 0x07, Subtype 0x02) +pub mod extended_note_specification; +/// Generic types for Extended Reply messages - Extended (Type 0x07) +pub mod extended_reply; +/// Flash Download - (Type 0x05) +pub mod flash_download; +/// Total message lengths for various messages +/// +/// IMPORTANT: this is the total byte length of the packet, +/// to get the length of data bytes, subtract 5 from these constants. +/// +/// Example: +/// +/// ```rust +/// # use ebds::{OmnibusCommand, MessageOps, len::{OMNIBUS_COMMAND, METADATA}}; +/// let message = OmnibusCommand::new(); +/// +/// assert_eq!(message.len(), OMNIBUS_COMMAND); +/// assert_eq!(message.data_len(), OMNIBUS_COMMAND - METADATA); +/// ``` +pub mod len; +/// Note Retrieved - Extended (Type 0x07, Subtype 0x0B) +pub mod note_retrieved; +/// Omnibus - Command (Type 0x01), Reply (Type 0x02) +pub mod omnibus; +/// Part number type definitions, used across multiple messages +pub mod part_number; +/// Query Application ID - Auxilliary (Type 0x06, Subtype 0x0E) +pub mod query_application_id; +/// Query Application Part Number - Auxilliary (Type 0x06, Subtype 0x07) +pub mod query_application_part_number; +/// Query Boot Part Number - Auxilliary (Type 0x06, Subtype 0x06) +pub mod query_boot_part_number; +/// Query Device Capabilities - Auxilliary (Type 0x06, Subtype 0x0D) +pub mod query_device_capabilities; +/// Query Software CRC - Auxilliary (Type 0x06, Subtype 0x00) +pub mod query_software_crc; +/// Query Value Table - Extended (Type 0x07, Subtype 0x06) +pub mod query_value_table; +/// Query Variant ID - Auxilliary (Type 0x06, Subtype 0x0F) +pub mod query_variant_id; +/// Query Variant Name - Auxilliary (Type 0x06, Subtype 0x08) +pub mod query_variant_name; +/// Query Variant Part Number - Auxilliary (Type 0x06, Subtype 0x09) +pub mod query_variant_part_number; +/// Set Escrow Timeout - Extended (Type 0x07, Subtype 0x04) +pub mod set_escrow_timeout; +/// Soft Reset - Auxilliary (Type 0x06, Subtype 0x7F) +pub mod soft_reset; +/// Message variant for building messages from raw bytes +pub mod variant; + +pub use advanced_bookmark_mode::*; +pub use aux_command::*; +pub use clear_audit_data::*; +pub use extended_command::*; +pub use extended_note_inhibits::*; +pub use extended_note_specification::*; +pub use extended_reply::*; +pub use flash_download::*; +pub use note_retrieved::*; +pub use omnibus::*; +pub use part_number::*; +pub use query_application_id::*; +pub use query_application_part_number::*; +pub use query_boot_part_number::*; +pub use query_device_capabilities::*; +pub use query_software_crc::*; +pub use query_value_table::*; +pub use query_variant_id::*; +pub use query_variant_name::*; +pub use query_variant_part_number::*; +pub use set_escrow_timeout::*; +pub use soft_reset::*; +pub use variant::*; + +pub use crate::error::{Error, JsonRpcError, JsonRpcResult, Result}; + +/// Start byte for EBDS packet +pub const STX: u8 = 0x02; +/// End byte for EBDS packet +pub const ETX: u8 = 0x03; +/// Magic byte for Special Interrupt Mode (not supported). +pub const ENQ: u8 = 0x05; +/// Constant for the environment variable defining the default Currency set +pub const ENV_CURRENCY: &str = "BAU_CURRENCY"; + +#[cfg(feature = "usd")] +pub const DEFAULT_CURRENCY: Currency = Currency::USD; +#[cfg(feature = "cny")] +pub const DEFAULT_CURRENCY: Currency = Currency::CNY; +#[cfg(feature = "gbp")] +pub const DEFAULT_CURRENCY: Currency = Currency::GBP; +#[cfg(feature = "jpy")] +pub const DEFAULT_CURRENCY: Currency = Currency::JPY; +#[cfg(feature = "aud")] +pub const DEFAULT_CURRENCY: Currency = Currency::AUD; +#[cfg(feature = "cad")] +pub const DEFAULT_CURRENCY: Currency = Currency::CAD; +#[cfg(feature = "mxn")] +pub const DEFAULT_CURRENCY: Currency = Currency::MXN; +#[cfg(feature = "amd")] +pub const DEFAULT_CURRENCY: Currency = Currency::AMD; + +#[cfg(feature = "std")] +pub fn bau_currency() -> Currency { + std::env::var(ENV_CURRENCY) + .unwrap_or(format!("{DEFAULT_CURRENCY}")) + .into() +} + +#[cfg(not(feature = "std"))] +pub fn bau_currency() -> Currency { + DEFAULT_CURRENCY +} + +/// Calculate the XOR checksum of a byte range +/// +/// This range should be the first non-control byte (LEN-byte), +/// through the first byte before ETX +pub fn checksum(data: &[u8]) -> u8 { + let mut sum = 0u8; + data.iter().for_each(|&b| sum ^= b); + sum +} + +// Under the 7-bit protocol, constructs a 16-bit number from a 4-byte slice. +// +// Each byte stores the significant bits in the lower nibble (4-bits), +// most significant nibble first (big-endian). +pub(crate) fn seven_bit_u16(b: &[u8]) -> u16 { + debug_assert_eq!(b.len(), 4); + + let hi = ((b[0] & 0xf) << 4) | (b[1] & 0xf); + let lo = ((b[2] & 0xf) << 4) | (b[3] & 0xf); + + u16::from_be_bytes([hi, lo]) +} + +// Under the 7-bit protocol, transforms a 16-bit number +// into a 4-byte slice. +// +// Each byte stores the significant bits in the lower nibble (4-bits), +// most significant nibble first (big-endian). +pub(crate) fn u16_seven_bit(n: u16) -> [u8; 4] { + let b = n.to_be_bytes(); + [b[0] >> 4, b[0] & 0xf, b[1] >> 4, b[1] & 0xf] +} + +// Under the 7-bit protocol, constructs a 8-bit number from a 2-byte slice. +// +// Each byte stores the significant bits in the lower nibble (4-bits), +// most significant nibble first (big-endian). +pub(crate) fn seven_bit_u8(b: &[u8]) -> u8 { + debug_assert_eq!(b.len(), 2); + + ((b[0] & 0xf) << 4) | (b[1] & 0xf) +} + +// Under the 7-bit protocol, transforms a 8-bit number into a 2-byte slice. +// +// Each byte stores the significant bits in the lower nibble (4-bits), +// most significant nibble first (big-endian). +pub(crate) fn u8_seven_bit(n: u8) -> [u8; 2] { + [n >> 4, n & 0xf] +} + +bitfield! { + /// Control field for EBDS messages + pub struct Control(u8); + u8; + /// AckNak bit for message acknowledgement, see [AckNak](AckNak) + pub acknak, set_acknak: 0; + /// Device type, see [DeviceType](DeviceType) + pub device_type, set_device_type: 3, 1; + /// Message type, see [MessageType](MessageType) + pub message_type, set_message_type: 6, 4; +} + +impl From for Control { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From for u8 { + fn from(c: Control) -> Self { + c.0 + } +} + +impl From<&Control> for u8 { + fn from(c: &Control) -> Self { + c.0 + } +} + +/// Set the ACK field in the control byte +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AckNak { + Ack = 0b0, + Nak = 0b1, +} + +impl Not for AckNak { + type Output = AckNak; + + fn not(self) -> Self::Output { + Self::from(!(self as u8)) + } +} + +impl fmt::Display for AckNak { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +impl From for &'static str { + fn from(a: AckNak) -> &'static str { + match a { + AckNak::Ack => "ACK", + AckNak::Nak => "NAK", + } + } +} + +impl From<&AckNak> for &'static str { + fn from(a: &AckNak) -> Self { + (*a).into() + } +} + +impl From for AckNak { + fn from(b: bool) -> Self { + match b { + false => Self::Ack, + true => Self::Nak, + } + } +} + +impl From for AckNak { + fn from(b: u8) -> Self { + match b & bitmask::ACK_NAK { + 0b0 => Self::Ack, + 0b1 => Self::Nak, + _ => unreachable!("invalid AckNak"), + } + } +} + +impl From for bool { + fn from(a: AckNak) -> bool { + a == AckNak::Nak + } +} + +/// Device type control bits +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DeviceType { + /// Bill acceptor device + BillAcceptor = 0b000, + /// Bill recycler device + BillRecycler = 0b001, + /// All other 3-bit values reserved for future use + Reserved = 0b111, +} + +impl fmt::Display for DeviceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let dev_str: &'static str = self.into(); + write!(f, "{}", dev_str) + } +} + +impl From for &'static str { + fn from(d: DeviceType) -> Self { + match d { + DeviceType::BillAcceptor => "BillAcceptor", + DeviceType::BillRecycler => "BillRecycler", + DeviceType::Reserved => "Reserved", + } + } +} + +impl From<&DeviceType> for &'static str { + fn from(d: &DeviceType) -> Self { + (*d).into() + } +} + +impl From for DeviceType { + fn from(b: u8) -> Self { + match b & bitmask::DEVICE_TYPE { + 0b000 => Self::BillAcceptor, + 0b001 => Self::BillRecycler, + _ => Self::Reserved, + } + } +} + +/// Various message types for different device interactions +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MessageType { + /// Generic omnibus command message, see [OmnibusCommand](crate::OmnibusCommand) + OmnibusCommand = 0b001, + /// Generic omnibus reply message, see [OmnibusReply](crate::OmnibusReply) + OmnibusReply = 0b010, + /// Generic omnibus bookmark message, see [OmnibusBookmark](crate::OmnibusBookmark) + OmnibusBookmark = 0b011, + /// Calibrate message, see [Calibrate](crate::Calibrate) + Calibrate = 0b100, + /// Firmware download message (response and reply), see [FirmwareDownload](crate::flash_download) + FirmwareDownload = 0b101, + /// Auxilliary command, see [AuxCommand](crate::AuxCommand) + AuxCommand = 0b110, + /// Extended message, see [Extended](crate::ExtendedCommand) + Extended = 0b111, + /// Variant to represent reserved values + Reserved = 0xff, +} + +impl fmt::Display for MessageType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +impl From for &'static str { + fn from(m: MessageType) -> &'static str { + match m { + MessageType::OmnibusCommand => "OmnibusCommand", + MessageType::OmnibusReply => "OmnibusReply", + MessageType::OmnibusBookmark => "OmnibusBookmark", + MessageType::Calibrate => "Calibrate", + MessageType::FirmwareDownload => "FirmwareDownload", + MessageType::AuxCommand => "AuxCommand", + MessageType::Extended => "Extended", + MessageType::Reserved => "Reserved", + } + } +} + +impl From<&MessageType> for &'static str { + fn from(m: &MessageType) -> Self { + (*m).into() + } +} + +impl From for MessageType { + fn from(b: u8) -> Self { + match b & bitmask::MESSAGE_TYPE { + 0b001 => Self::OmnibusCommand, + 0b010 => Self::OmnibusReply, + 0b011 => Self::OmnibusBookmark, + 0b100 => Self::Calibrate, + 0b101 => Self::FirmwareDownload, + 0b110 => Self::AuxCommand, + 0b111 => Self::Extended, + _ => Self::Reserved, + } + } +} + +pub(crate) mod index { + pub const STX: usize = 0; + pub const LEN: usize = 1; + pub const CONTROL: usize = 2; + pub const DATA: usize = 3; + pub const EXT_SUBTYPE: usize = 3; +} + +pub(crate) mod bitmask { + pub const ACK_NAK: u8 = 0b1; + pub const DEVICE_TYPE: u8 = 0b111; + pub const MESSAGE_TYPE: u8 = 0b111; +} + +/// Generic functions for all EBDS message types +pub trait MessageOps { + /// Initialize common message fields + fn init(&mut self) { + let len = self.len(); + let etx_index = self.etx_index(); + let buf = self.buf_mut(); + + buf[index::STX] = STX; + buf[index::LEN] = len as u8; + buf[etx_index] = ETX; + } + + /// Get a reference to the message buffer. + fn buf(&self) -> &[u8]; + + /// Get a mutable reference to the message buffer. + fn buf_mut(&mut self) -> &mut [u8]; + + /// Get the length of the entire message. + fn len(&self) -> usize { + self.buf().len() + } + + /// Gets whether the message buffer is empty (all zeros) + fn is_empty(&self) -> bool { + let mut ret = 0; + self.buf().iter().for_each(|&b| ret ^= b); + ret == 0 + } + + /// Get the length of data bytes. + fn data_len(&self) -> usize { + self.len() - len::METADATA + } + + /// Get the ETX index. + fn etx_index(&self) -> usize { + self.buf().len() - 2 + } + + /// Get the checksum index. + fn chk_index(&self) -> usize { + self.buf().len() - 1 + } + + /// Get the ACKNAK control field. + fn acknak(&self) -> AckNak { + Control(self.buf()[index::CONTROL]).acknak().into() + } + + /// Set the ACKNAK control field. + fn set_acknak(&mut self, acknak: AckNak) { + let mut control = Control(self.buf()[index::CONTROL]); + control.set_acknak(acknak.into()); + self.buf_mut()[index::CONTROL] = control.into(); + } + + /// Switches the current ACKNAK control field value. + fn switch_acknak(&mut self) { + self.set_acknak(!self.acknak()) + } + + /// Get the device type control field. + fn device_type(&self) -> DeviceType { + Control(self.buf()[index::CONTROL]).device_type().into() + } + + /// Set the device type control field + fn set_device_type(&mut self, device_type: DeviceType) { + let mut control = Control(self.buf()[index::CONTROL]); + control.set_device_type(device_type as u8); + self.buf_mut()[index::CONTROL] = control.into(); + } + + /// Get the message type control field + fn message_type(&self) -> MessageType { + Control(self.buf()[index::CONTROL]).message_type().into() + } + + /// Set the message type control field + fn set_message_type(&mut self, message_type: MessageType) { + let mut control = Control(self.buf()[index::CONTROL]); + control.set_message_type(message_type as u8); + self.buf_mut()[index::CONTROL] = control.into(); + } + + /// Get the current checksum value + /// + /// Note: to ensure validity, call [calculate_checksum](Self::calculate_checksum) first + fn checksum(&self) -> u8 { + self.buf()[self.chk_index()] + } + + fn checksum_bytes(&self) -> &[u8] { + self.buf()[index::LEN..self.etx_index()].as_ref() + } + + /// Calculate the message checksum + fn calculate_checksum(&mut self) -> u8 { + let csum = checksum(self.checksum_bytes()); + let csum_index = self.chk_index(); + self.buf_mut()[csum_index] = csum; + csum + } + + /// Validate the message checksum + /// + /// Calculates the checksum of the buffer, and checks for a match against the current checksum. + fn validate_checksum(&self) -> Result<()> { + let expected = checksum(self.checksum_bytes()); + let current = self.buf()[self.chk_index()]; + + if expected == current { + Ok(()) + } else { + Err(Error::failure(format!( + "invalid checksum, expected: {expected}, have: {current}" + ))) + } + } + + /// Get the message as a byte buffer + /// + /// Note: calculates the checksum, and sets the checksum byte. + /// + /// To get the buffer without calculating the checksum, use [as_bytes_unchecked](Self::as_bytes_unchecked) + fn as_bytes(&mut self) -> &[u8] { + self.calculate_checksum(); + self.buf() + } + + /// Get a mutable reference to the byte buffer + fn as_bytes_mut(&mut self) -> &mut [u8] { + self.buf_mut() + } + + /// Get the message as a byte buffer + /// + /// Note: does not perform checksum calculation, caller must call + /// [calculate_checksum](Self::calculate_checksum) prior to calling this function. + fn as_bytes_unchecked(&self) -> &[u8] { + self.buf() + } + + /// Deserializes a message type from a byte buffer. + /// + /// Returns `Ok(())` on success, and `Err(_)` for an invalid buffer. + // FIXME: separate into another trait, and find a way to generically create message types + #[allow(clippy::wrong_self_convention)] + fn from_buf(&mut self, buf: &[u8]) -> Result<()> { + if buf.len() < self.len() { + return Err(Error::failure("invalid reply length")); + } + + let stx = buf[index::STX]; + if stx != STX { + return Err(Error::failure(format!( + "invalid STX byte, expected: {STX}, have: {stx}" + ))); + } + + let msg_len = buf[index::LEN] as usize; + + if msg_len != self.len() { + return Err(Error::failure("invalid reply length")); + } + + let etx = buf[msg_len - 2]; + if etx != ETX { + return Err(Error::failure(format!( + "invalid ETX byte, expected: {ETX}, have: {etx}" + ))); + } + + validate_checksum(buf[..msg_len].as_ref())?; + + let control = Control::from(buf[index::CONTROL]); + let msg_type = MessageType::from(control.message_type()); + let exp_msg_type = self.message_type(); + + if msg_type != exp_msg_type { + return Err(Error::failure(format!( + "invalid message type, expected: {exp_msg_type}, have: {msg_type}" + ))); + } + + self.buf_mut().copy_from_slice(buf[..msg_len].as_ref()); + + Ok(()) + } +} + +/// Validates a checksum matches the expected value. +/// +/// Returns `Ok(())` on a match, `Err(_)` otherwise. +pub fn validate_checksum(buf: &[u8]) -> Result<()> { + let len = buf.len(); + + if !(len::MIN_MESSAGE..=len::MAX_MESSAGE).contains(&len) { + return Err(Error::failure("invalid message length")); + } + + let etx_index = len - 2; + let chk_index = len - 1; + + let expected = checksum(buf[index::LEN..etx_index].as_ref()); + let current = buf[chk_index]; + + if expected == current { + Ok(()) + } else { + Err(Error::failure(format!( + "invalid checksum, expected: {expected}, have: {current}" + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_u16_seven_bit() { + let expected = 0x1234; + let expected_bytes = [0x1, 0x2, 0x3, 0x4]; + + assert_eq!(seven_bit_u16(expected_bytes.as_ref()), expected); + assert_eq!(u16_seven_bit(expected), expected_bytes); + + assert_eq!( + u16_seven_bit(seven_bit_u16(expected_bytes.as_ref())), + expected_bytes + ); + assert_eq!(seven_bit_u16(u16_seven_bit(expected).as_ref()), expected); + + for num in 0..u16::MAX { + let n = num as u16; + assert_eq!(seven_bit_u16(u16_seven_bit(n).as_ref()), n); + } + } + + #[test] + fn test_u8_seven_bit() { + let expected = 0x54; + let expected_bytes = [0x5, 0x4]; + + assert_eq!(u8_seven_bit(expected), expected_bytes); + assert_eq!(seven_bit_u8(expected_bytes.as_ref()), expected); + } +} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..8f2bb19 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,41 @@ +pub const BAU_LOG_PREFIX: &str = "BILL ACCEPTOR"; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LogLevel { + Off = 0, + Critical, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for LogLevel { + fn from(level: u32) -> Self { + match level { + 0 => Self::Off, + 1 => Self::Critical, + 2 => Self::Error, + 3 => Self::Warn, + 4 => Self::Info, + 5 => Self::Debug, + 6 => Self::Trace, + _ => Self::Off, + } + } +} + +impl From for log::LevelFilter { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::Off => log::LevelFilter::Off, + LogLevel::Trace => log::LevelFilter::Trace, + LogLevel::Debug => log::LevelFilter::Debug, + LogLevel::Info => log::LevelFilter::Info, + LogLevel::Warn => log::LevelFilter::Warn, + LogLevel::Critical | LogLevel::Error => log::LevelFilter::Error, + } + } +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..80bb05a --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,577 @@ +/// Creates an named boolean-like enum (set or unset enums). +/// +/// Implements utility traits for converting from/to basic types. +#[macro_export] +macro_rules! bool_enum { + ($name:ident, $doc:tt) => { + #[doc = $doc] + #[repr(u8)] + #[derive(Clone, Copy, Debug, PartialEq)] + pub enum $name { + /// The field is unset + Unset = 0b0, + /// The field is set + Set = 0b1, + } + + impl From for $name { + fn from(b: bool) -> Self { + match b { + false => Self::Unset, + true => Self::Set, + } + } + } + + impl From for $name { + fn from(b: u8) -> Self { + match (b & 0b1) { + 0b0 => Self::Unset, + 0b1 => Self::Set, + _ => Self::Unset, + } + } + } + + impl From<$name> for bool { + fn from(n: $name) -> Self { + n == $name::Set + } + } + + impl From<&$name> for bool { + fn from(n: &$name) -> Self { + (*n).into() + } + } + + impl From<$name> for u8 { + fn from(n: $name) -> Self { + (n == $name::Set) as u8 + } + } + + impl From<&$name> for u8 { + fn from(n: &$name) -> Self { + (*n).into() + } + } + + impl From<&$name> for &'static str { + fn from(name: &$name) -> Self { + let set: bool = name.into(); + if set { + "set" + } else { + "unset" + } + } + } + + impl From<$name> for &'static str { + fn from(name: $name) -> Self { + (&name).into() + } + } + + impl $crate::std::fmt::Display for $name { + fn fmt(&self, f: &mut $crate::std::fmt::Formatter<'_>) -> $crate::std::fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } + } + }; + + ($name:ident) => { + bool_enum!($name, ""); + }; +} + +/// Implements the [MessageOps](crate::MessageOps) trait for a named type. +#[macro_export] +macro_rules! impl_message_ops { + ($name:ident) => { + impl $crate::MessageOps for $name { + fn buf(&self) -> &[u8] { + self.buf.as_ref() + } + + fn buf_mut(&mut self) -> &mut [u8] { + self.buf.as_mut() + } + } + }; + + ($name:ident, $full_len:ident, $enable_len:ident) => { + impl $crate::MessageOps + for $name<$full_len, $enable_len> + { + fn buf(&self) -> &[u8] { + self.buf.as_ref() + } + + fn buf_mut(&mut self) -> &mut [u8] { + self.buf.as_mut() + } + } + }; +} + +/// Implements the defaults for the [OmnibusCommandOps](crate::OmnibusCommandOps) trait for a +/// named type. +#[macro_export] +macro_rules! impl_omnibus_command_ops { + ($name:ident) => { + impl $crate::OmnibusCommandOps for $name {} + }; +} + +/// Implements the defaults for the [OmnibusReplyOps](crate::OmnibusReplyOps) trait for a +/// named type. +#[macro_export] +macro_rules! impl_omnibus_reply_ops { + ($name:ident) => { + impl $crate::OmnibusReplyOps for $name {} + }; +} + +/// Implements the defaults for the [ExtendedCommandOps](crate::ExtendedCommandOps) trait for a +/// named type in the Extended messages subset. +#[macro_export] +macro_rules! impl_extended_ops { + ($name:ident) => { + impl $crate::ExtendedCommandOps for $name {} + }; + + ($name:ident, $full_len:ident, $enable_len:ident) => { + impl $crate::ExtendedCommandOps + for $name<$full_len, $enable_len> + { + } + }; +} + +/// Implements the defaults for the [ExtendedReplyOps](crate::ExtendedReplyOps) trait for a +/// named type in the Extended messages subset. +#[macro_export] +macro_rules! impl_extended_reply_ops { + ($name:ident) => { + impl $crate::ExtendedReplyOps for $name {} + }; + + ($name:ident, $full_len:ident, $enable_len:ident) => { + impl $crate::ExtendedReplyOps + for $name<$full_len, $enable_len> + { + } + }; +} + +/// Implements the defaults for the [AuxCommandOps](crate::AuxCommandOps) trait for a +/// named type in the Auxilliary messages subset. +#[macro_export] +macro_rules! impl_aux_ops { + ($name:ident) => { + impl $crate::AuxCommandOps for $name {} + }; +} + +/// Implements the defaults for the [OmnibusCommandOps](crate::OmnibusCommandOps) trait for a +/// named type that is in the subset of Extended Commands. +#[macro_export] +macro_rules! impl_omnibus_extended_command { + ($name:ident) => { + impl $crate::OmnibusCommandOps for $name { + fn denomination(&self) -> $crate::StandardDenomination { + use $crate::{omnibus::command::index, MessageOps}; + self.buf()[index::DENOMINATION + 1].into() + } + + fn set_denomination(&mut self, denomination: $crate::StandardDenomination) { + use $crate::{omnibus::command::index, MessageOps}; + self.buf_mut()[index::DENOMINATION + 1] = denomination.into(); + } + + fn operational_mode(&self) -> $crate::OperationalMode { + use $crate::{omnibus::command::index, MessageOps}; + self.buf()[index::OPERATIONAL_MODE + 1].into() + } + + fn set_operational_mode(&mut self, operational_mode: $crate::OperationalMode) { + use $crate::{omnibus::command::index, MessageOps}; + self.buf_mut()[index::OPERATIONAL_MODE + 1] = operational_mode.into(); + } + + fn configuration(&self) -> $crate::Configuration { + use $crate::{omnibus::command::index, MessageOps}; + self.buf()[index::CONFIGURATION + 1].into() + } + + fn set_configuration(&mut self, configuration: $crate::Configuration) { + use $crate::{omnibus::command::index, MessageOps}; + self.buf_mut()[index::CONFIGURATION + 1] = configuration.into(); + } + } + }; + + ($name:ident, $full_len:ident, $enable_len:ident) => { + impl $crate::OmnibusCommandOps + for $name<$full_len, $enable_len> + { + fn denomination(&self) -> $crate::StandardDenomination { + use $crate::{omnibus::command::index, MessageOps}; + self.buf()[index::DENOMINATION + 1].into() + } + + fn set_denomination(&mut self, denomination: $crate::StandardDenomination) { + use $crate::{omnibus::command::index, MessageOps}; + self.buf_mut()[index::DENOMINATION + 1] = denomination.into(); + } + + fn operational_mode(&self) -> $crate::OperationalMode { + use $crate::{omnibus::command::index, MessageOps}; + self.buf()[index::OPERATIONAL_MODE + 1].into() + } + + fn set_operational_mode(&mut self, operational_mode: $crate::OperationalMode) { + use $crate::{omnibus::command::index, MessageOps}; + self.buf_mut()[index::OPERATIONAL_MODE + 1] = operational_mode.into(); + } + + fn configuration(&self) -> $crate::Configuration { + use $crate::{omnibus::command::index, MessageOps}; + self.buf()[index::CONFIGURATION + 1].into() + } + + fn set_configuration(&mut self, configuration: $crate::Configuration) { + use $crate::{omnibus::command::index, MessageOps}; + self.buf_mut()[index::CONFIGURATION + 1] = configuration.into(); + } + } + }; +} + +/// Implements the defaults for the [OmnibusReplyOps](crate::OmnibusReplyOps) trait for a +/// named type that is in the subset of Extended Replies. +#[macro_export] +macro_rules! impl_omnibus_extended_reply { + ($name:ident) => { + impl $crate::OmnibusReplyOps for $name { + fn device_state(&self) -> $crate::DeviceState { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf()[index::DEVICE_STATE + 1].into() + } + + fn set_device_state(&mut self, device_state: $crate::DeviceState) { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf_mut()[index::DEVICE_STATE + 1] = device_state.into(); + } + + fn device_status(&self) -> $crate::DeviceStatus { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf()[index::DEVICE_STATUS + 1].into() + } + + fn set_device_status(&mut self, device_status: $crate::DeviceStatus) { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf_mut()[index::DEVICE_STATUS + 1] = device_status.into(); + } + + fn exception_status(&self) -> $crate::ExceptionStatus { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf()[index::EXCEPTION_STATUS + 1].into() + } + + fn set_exception_status(&mut self, exception_status: $crate::ExceptionStatus) { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf_mut()[index::EXCEPTION_STATUS + 1] = exception_status.into(); + } + + fn misc_device_state(&self) -> $crate::MiscDeviceState { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf()[index::MISC_DEVICE_STATE + 1].into() + } + + fn set_misc_device_state(&mut self, misc_device_state: $crate::MiscDeviceState) { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf_mut()[index::MISC_DEVICE_STATE + 1] = misc_device_state.into(); + } + + fn model_number(&self) -> $crate::ModelNumber { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf()[index::MODEL_NUMBER + 1].into() + } + + fn set_model_number(&mut self, model_number: $crate::ModelNumber) { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf_mut()[index::MODEL_NUMBER + 1] = model_number.into(); + } + + fn code_revision(&self) -> $crate::CodeRevision { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf()[index::CODE_REVISION + 1].into() + } + + fn set_code_revision(&mut self, code_revision: $crate::CodeRevision) { + use $crate::{omnibus::reply::index, MessageOps}; + self.buf_mut()[index::CODE_REVISION + 1] = code_revision.into(); + } + } + }; +} + +/// Sets all [OmnibusReplyOps](crate::OmnibusReplyOps) functions to `unimplemented` for an [AuxCommand](crate::AuxCommand) reply type. +/// +/// Intended to allow generalization over AuxCommand reply types as [OmnibusReplyOps](crate::OmnibusReplyOps) in contexts +/// where calling the trait functions is not intended. For example, in [MessageVariant](crate::MessageVariant) where each +/// variant needs to implement the [OmnibusReplyOps](crate::OmnibusReplyOps) trait, but it is not necessary to actually +/// call the trait functions. +#[macro_export] +macro_rules! impl_omnibus_nop_reply { + ($name:ident) => { + impl $crate::OmnibusReplyOps for $name { + fn device_state(&self) -> $crate::DeviceState { + 0u8.into() + } + + fn set_device_state(&mut self, _device_state: $crate::DeviceState) {} + + fn idling(&self) -> $crate::Idling { + 0u8.into() + } + + fn set_idling(&mut self, _idling: $crate::Idling) {} + + fn accepting(&self) -> $crate::Accepting { + 0u8.into() + } + + fn set_accepting(&mut self, _accepting: $crate::Accepting) {} + + fn escrowed_state(&self) -> $crate::EscrowedState { + 0u8.into() + } + + fn set_escrowed_state(&mut self, _escrowed_state: $crate::EscrowedState) {} + + fn stacking(&self) -> $crate::Stacking { + 0u8.into() + } + + fn set_stacking(&mut self, _stacking: $crate::Stacking) {} + + fn stacked_event(&self) -> $crate::StackedEvent { + 0u8.into() + } + + fn set_stacked_event(&mut self, _stacked_event: $crate::StackedEvent) {} + + fn returning(&self) -> $crate::Returning { + 0u8.into() + } + + fn set_returning(&mut self, _returning: $crate::Returning) {} + + fn returned_event(&self) -> $crate::ReturnedEvent { + 0u8.into() + } + + fn set_returned_event(&mut self, _returned_event: $crate::ReturnedEvent) {} + + fn device_status(&self) -> $crate::DeviceStatus { + 0u8.into() + } + + fn set_device_status(&mut self, _device_status: $crate::DeviceStatus) {} + + fn cheated(&self) -> $crate::Cheated { + 0u8.into() + } + + fn set_cheated(&mut self, _cheated: $crate::Cheated) {} + + fn rejected(&self) -> $crate::Rejected { + 0u8.into() + } + + fn set_rejected(&mut self, _rejected: $crate::Rejected) {} + + fn jammed(&self) -> $crate::Jammed { + 0u8.into() + } + + fn set_jammed(&mut self, _jammed: $crate::Jammed) {} + + fn stacker_full(&self) -> $crate::StackerFull { + 0u8.into() + } + + fn set_stacker_full(&mut self, _stacker_full: $crate::StackerFull) {} + + fn cassette_attached(&self) -> $crate::CassetteAttached { + 0u8.into() + } + + fn set_cassette_attached(&mut self, _cassette_attached: $crate::CassetteAttached) {} + + fn cash_box_status(&self) -> $crate::CashBoxStatus { + 0u8.into() + } + + fn paused(&self) -> $crate::Paused { + 0u8.into() + } + + fn set_paused(&mut self, _paused: $crate::Paused) {} + + fn calibration(&self) -> $crate::Calibration { + 0u8.into() + } + + fn set_calibration(&mut self, _calibration: $crate::Calibration) {} + + fn exception_status(&self) -> $crate::ExceptionStatus { + 0u8.into() + } + + fn set_exception_status(&mut self, _exception_status: $crate::ExceptionStatus) {} + + fn power_up(&self) -> $crate::PowerUpStatus { + 0u8.into() + } + + fn set_power_up(&mut self, _power_up: $crate::PowerUpStatus) {} + + fn invalid_command(&self) -> $crate::InvalidCommand { + 0u8.into() + } + + fn set_invalid_command(&mut self, _invalid_command: $crate::InvalidCommand) {} + + fn failure(&self) -> $crate::Failure { + 0u8.into() + } + + fn set_failure(&mut self, _failure: $crate::Failure) {} + + fn note_value(&self) -> $crate::StandardDenomination { + 0u8.into() + } + + fn set_note_value(&mut self, _note_value: $crate::StandardDenomination) {} + + fn transport_open(&self) -> $crate::TransportOpen { + 0u8.into() + } + + fn set_transport_open(&mut self, _transport_open: $crate::TransportOpen) {} + + fn misc_device_state(&self) -> $crate::MiscDeviceState { + 0u8.into() + } + + fn set_misc_device_state(&mut self, _misc_device_state: $crate::MiscDeviceState) {} + + fn stalled(&self) -> $crate::Stalled { + 0u8.into() + } + + fn set_stalled(&mut self, _stalled: $crate::Stalled) {} + + fn flash_download(&self) -> $crate::FlashDownload { + 0u8.into() + } + + fn set_flash_download(&mut self, _flash_download: $crate::FlashDownload) {} + + fn pre_stack(&self) -> $crate::PreStack { + 0u8.into() + } + + fn set_pre_stack(&mut self, _pre_stack: $crate::PreStack) {} + + fn raw_barcode(&self) -> $crate::RawBarcode { + 0u8.into() + } + + fn set_raw_barcode(&mut self, _raw_barcode: $crate::RawBarcode) {} + + fn device_capabilities(&self) -> $crate::DeviceCapabilities { + 0u8.into() + } + + fn set_device_capabilities( + &mut self, + _device_capabilities: $crate::DeviceCapabilities, + ) { + } + + fn disabled(&self) -> $crate::Disabled { + 0u8.into() + } + + fn set_disabled(&mut self, _disabled: $crate::Disabled) {} + + fn model_number(&self) -> $crate::ModelNumber { + 0u8.into() + } + + fn set_model_number(&mut self, _model_number: $crate::ModelNumber) {} + + fn code_revision(&self) -> $crate::CodeRevision { + 0u8.into() + } + + fn set_code_revision(&mut self, _code_revision: $crate::CodeRevision) {} + } + }; +} + +#[macro_export] +macro_rules! impl_from_for_omnibus_reply { + ($name:ident) => { + impl From<&$name> for $crate::OmnibusReply { + fn from(reply: &$name) -> Self { + use $crate::OmnibusReplyOps; + + let mut msg = Self::new(); + + msg.set_device_state(reply.device_state()); + msg.set_device_status(reply.device_status()); + msg.set_exception_status(reply.exception_status()); + msg.set_misc_device_state(reply.misc_device_state()); + msg.set_model_number(reply.model_number()); + msg.set_code_revision(reply.code_revision()); + + msg + } + } + + impl From<$name> for $crate::OmnibusReply { + fn from(reply: $name) -> Self { + (&reply).into() + } + } + }; +} + +/// Implements the Default trait for a named type with a `Self::new()` function. +#[macro_export] +macro_rules! impl_default { + ($name:ident) => { + impl Default for $name { + fn default() -> Self { + Self::new() + } + } + }; + + ($name:ident, $full_len:ident, $enable_len:ident) => { + impl Default + for $name<$full_len, $enable_len> + { + fn default() -> Self { + Self::new() + } + } + }; +} diff --git a/src/method.rs b/src/method.rs new file mode 100644 index 0000000..2ff7e87 --- /dev/null +++ b/src/method.rs @@ -0,0 +1,219 @@ +use serde::{Deserialize, Serialize, Serializer}; + +use crate::std; +use std::fmt; + +/// Method performed by the hardware +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize)] +#[serde(field_identifier, rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Method { + /// Accept bills + Accept, + /// Stop current action + Stop, + /// Dispense bills + Dispense, + /// Stack bills + Stack, + /// Reject bills + Reject, + /// Get current status + Status, + /// Report escrow full + EscrowFull, + /// Reset the device + Reset, + /// Shutdown the socket connection + Shutdown, + /// Unknown method + Unknown = 0xff, +} + +impl Default for Method { + fn default() -> Self { + Self::Unknown + } +} + +impl Serialize for Method { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Self::Accept => serializer.serialize_unit_variant("Method", 0, "ACCEPT"), + Self::Stop => serializer.serialize_unit_variant("Method", 1, "STOP"), + Self::Dispense => serializer.serialize_unit_variant("Method", 2, "DISPENSE"), + Self::Stack => serializer.serialize_unit_variant("Method", 3, "STACK"), + Self::Reject => serializer.serialize_unit_variant("Method", 4, "REJECT"), + Self::Status => serializer.serialize_unit_variant("Method", 5, "STATUS"), + Self::EscrowFull => serializer.serialize_unit_variant("Method", 6, "ESCROW_FULL"), + Self::Reset => serializer.serialize_unit_variant("Method", 7, "RESET"), + Self::Shutdown => serializer.serialize_unit_variant("Method", 8, "SHUTDOWN"), + Self::Unknown => serializer.serialize_unit_variant("Method", 0xff, "UNKNOWN"), + } + } +} + +impl From for &'static str { + fn from(m: Method) -> Self { + match m { + Method::Accept => "ACCEPT", + Method::Stop => "STOP", + Method::Dispense => "DISPENSE", + Method::Stack => "STACK", + Method::Reject => "REJECT", + Method::Status => "STATUS", + Method::EscrowFull => "ESCROW_FULL", + Method::Reset => "RESET", + Method::Shutdown => "SHUTDOWN", + Method::Unknown => "UNKNOWN", + } + } +} + +impl From<&Method> for &'static str { + fn from(m: &Method) -> Self { + (*m).into() + } +} + +impl From<&str> for Method { + fn from(s: &str) -> Self { + match s.to_uppercase().as_str() { + "ACCEPT" => Self::Accept, + "STOP" => Self::Stop, + "DISPENSE" => Self::Dispense, + "STACK" => Self::Stack, + "REJECT" => Self::Reject, + "STATUS" => Self::Status, + "ESCROW_FULL" => Self::EscrowFull, + "RESET" => Self::Reset, + "SHUTDOWN" => Self::Shutdown, + _ => Self::Unknown, + } + } +} + +impl From<&[u8]> for Method { + fn from(b: &[u8]) -> Self { + std::str::from_utf8(b).unwrap_or("").into() + } +} + +impl From<[u8; N]> for Method { + fn from(b: [u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&[u8; N]> for Method { + fn from(b: &[u8; N]) -> Self { + b.as_ref().into() + } +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, Result}; + + #[test] + fn test_method_serde() -> Result<()> { + assert_eq!(serde_json::to_string(&Method::Accept)?, "\"ACCEPT\""); + assert_eq!(serde_json::to_string(&Method::Stop)?, "\"STOP\""); + assert_eq!(serde_json::to_string(&Method::Dispense)?, "\"DISPENSE\""); + assert_eq!(serde_json::to_string(&Method::Stack)?, "\"STACK\""); + assert_eq!(serde_json::to_string(&Method::Reject)?, "\"REJECT\""); + assert_eq!(serde_json::to_string(&Method::Status)?, "\"STATUS\""); + assert_eq!(serde_json::to_string(&Method::Shutdown)?, "\"SHUTDOWN\""); + assert_eq!( + serde_json::to_string(&Method::EscrowFull)?, + "\"ESCROW_FULL\"" + ); + + assert_eq!( + serde_json::from_str::("\"ACCEPT\"")?, + Method::Accept + ); + assert_eq!(serde_json::from_str::("\"STOP\"")?, Method::Stop); + assert_eq!( + serde_json::from_str::("\"DISPENSE\"")?, + Method::Dispense + ); + assert_eq!(serde_json::from_str::("\"STACK\"")?, Method::Stack); + assert_eq!( + serde_json::from_str::("\"REJECT\"")?, + Method::Reject + ); + assert_eq!( + serde_json::from_str::("\"STATUS\"")?, + Method::Status + ); + assert_eq!( + serde_json::from_str::("\"ESCROW_FULL\"")?, + Method::EscrowFull + ); + assert_eq!( + serde_json::from_str::("\"SHUTDOWN\"")?, + Method::Shutdown + ); + + Ok(()) + } + + #[test] + fn test_method_from_str() { + // Check that upper-, lower-, and mixed-case method strings are parse correctly + assert_eq!(Method::from("ACCEPT"), Method::Accept); + assert_eq!(Method::from("accept"), Method::Accept); + assert_eq!(Method::from("ACCept"), Method::Accept); + + assert_eq!(Method::from("STOP"), Method::Stop); + assert_eq!(Method::from("stop"), Method::Stop); + assert_eq!(Method::from("stOP"), Method::Stop); + + assert_eq!(Method::from("DISPENSE"), Method::Dispense); + assert_eq!(Method::from("dispense"), Method::Dispense); + assert_eq!(Method::from("disPENse"), Method::Dispense); + + assert_eq!(Method::from("STACK"), Method::Stack); + assert_eq!(Method::from("stack"), Method::Stack); + assert_eq!(Method::from("stACK"), Method::Stack); + + assert_eq!(Method::from("REJECT"), Method::Reject); + assert_eq!(Method::from("reject"), Method::Reject); + assert_eq!(Method::from("reJEct"), Method::Reject); + + assert_eq!(Method::from("STATUS"), Method::Status); + assert_eq!(Method::from("status"), Method::Status); + assert_eq!(Method::from("stAtus"), Method::Status); + + assert_eq!(Method::from("ESCROW_FULL"), Method::EscrowFull); + assert_eq!(Method::from("escrow_full"), Method::EscrowFull); + assert_eq!(Method::from("escrOW_fULl"), Method::EscrowFull); + + assert_eq!(Method::from("RESET"), Method::Reset); + assert_eq!(Method::from("reset"), Method::Reset); + assert_eq!(Method::from("reSet"), Method::Reset); + + assert_eq!(Method::from("SHUTDOWN"), Method::Shutdown); + assert_eq!(Method::from("shutdown"), Method::Shutdown); + assert_eq!(Method::from("SHutDown"), Method::Shutdown); + + assert_eq!(Method::from("UNKNOWN"), Method::Unknown); + assert_eq!(Method::from("unknown"), Method::Unknown); + assert_eq!(Method::from("UNknowN"), Method::Unknown); + + // All other strings should be parsed as unknown + // Fuzz tests are better suited to exhaustively test random strings + assert_eq!(Method::from("?@R@UI@(H"), Method::Unknown); + } +} diff --git a/src/note_retrieved.rs b/src/note_retrieved.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/note_retrieved.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/note_retrieved/command.rs b/src/note_retrieved/command.rs new file mode 100644 index 0000000..2ce8526 --- /dev/null +++ b/src/note_retrieved/command.rs @@ -0,0 +1,116 @@ +use crate::{ + bool_enum, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::NOTE_RETRIEVED_COMMAND, ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, +}; + +mod index { + pub const STATUS: usize = 7; +} + +bool_enum!( + Status, + "Whether to enable/disable Note Retrieved functionality" +); + +/// Note Retrieved - Command (Subtype 0x0B) +/// +/// [NoteRetrievedCommand] represents a message sent to enable/disable Note Retrieved functionality. +/// +/// The note retrieved message is used to turn on optional functionality in the device. Some software has +/// the ability to report to the host when the customer has retrieved a returned or rejected note from the +/// device. Once a document is returned/rejected by the device, the document will sit partially in the device +/// in a manner that is convenient for the customer. This message allows the host to detect the moment the +/// customer removes the note from the mouth of the device. +/// +/// The host must enable this feature every time the device powers up. This is done with the following +/// message. (The same message structure can also be used to disable the feature). +/// +/// **Warning** The note retrieved event is disabled by default and is required to be turned on explicitly by +/// the host every time the device powers up. +/// +/// The Note Retrieved Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Status | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +/// | Value | 0x02 | 0x0A | 0x7n | 0x0B | nn | nn | nn | 0x00/01 | 0x03 | zz | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NoteRetrievedCommand { + buf: [u8; NOTE_RETRIEVED_COMMAND], +} + +impl NoteRetrievedCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; NOTE_RETRIEVED_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::NoteRetrieved); + + message + } + + /// Gets the [Status] data field. + pub fn status(&self) -> Status { + self.buf[index::STATUS].into() + } + + /// Sets the [Status] data field. + pub fn set_status(&mut self, status: Status) { + self.buf[index::STATUS] = status.into(); + } +} + +impl_default!(NoteRetrievedCommand); +impl_message_ops!(NoteRetrievedCommand); +impl_omnibus_extended_command!(NoteRetrievedCommand); +impl_extended_ops!(NoteRetrievedCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_note_retrieved_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type | Subtype + 0x02, 0x0a, 0x70, 0x0b, + // Data + 0x00, 0x00, 0x00, + // Status + 0x01, + // ETX | Checksum + 0x03, 0x70, + ]; + + let mut msg = NoteRetrievedCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::NoteRetrieved); + assert_eq!(msg.status(), Status::Set); + + let msg_bytes = [ + // STX | LEN | Message Type | Subtype + 0x02, 0x0a, 0x70, 0x0b, + // Data + 0x00, 0x00, 0x00, + // Status + 0x00, + // ETX | Checksum + 0x03, 0x71, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::NoteRetrieved); + assert_eq!(msg.status(), Status::Unset); + + Ok(()) + } +} diff --git a/src/note_retrieved/reply.rs b/src/note_retrieved/reply.rs new file mode 100644 index 0000000..fb06d92 --- /dev/null +++ b/src/note_retrieved/reply.rs @@ -0,0 +1,234 @@ +use crate::std; +use std::fmt; + +use crate::{ + bool_enum, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply, + len::{NOTE_RETRIEVED_EVENT, NOTE_RETRIEVED_REPLY}, + ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, OmnibusReplyOps, +}; + +pub const EVENT: u8 = 0x7f; + +bool_enum!( + RetrieveAckNak, + "Indicates success(0x01) / failure(0x00) of the NoteRetrievedCommand" +); + +pub(crate) mod index { + pub const ACKNAK: usize = 10; + pub const EVENT: usize = 10; +} + +/// Note Retrieved - Reply (Subtype 0x0B) +/// +/// NoteRetrievedReply represents an immediate reply to the +/// [NoteRetrievedCommand](crate::NoteRetrievedCommand). +/// +/// The device will respond to the enable/disable command with an ACK or NAK. +/// +/// The device ACKs with 0x01 if it can honor the hosts request to either enable or disable. The device will +/// NAK the command if it is not supported for the current configuration. (ex. BNF is attached). +/// +/// The Note Retrieved Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ACK/NAK | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | +/// | Value | 0x02 | 0x0D | 0x7n | 0x0B | nn | nn | nn | nn | nn | nn | 0x01/00 | 0x03 | zz | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NoteRetrievedReply { + buf: [u8; NOTE_RETRIEVED_REPLY], +} + +impl NoteRetrievedReply { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; NOTE_RETRIEVED_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::NoteRetrieved); + + message + } + + pub fn retrieved_acknak(&self) -> RetrieveAckNak { + self.buf[index::ACKNAK].into() + } + + pub fn set_retrieved_acknak(&mut self, acknak: RetrieveAckNak) { + self.buf[index::ACKNAK] = acknak.into() + } +} + +impl_default!(NoteRetrievedReply); +impl_message_ops!(NoteRetrievedReply); +impl_omnibus_extended_reply!(NoteRetrievedReply); +impl_extended_ops!(NoteRetrievedReply); + +impl fmt::Display for NoteRetrievedReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, Retrieved AckNak: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + self.retrieved_acknak(), + ) + } +} + +/// Note Retrieved - Event (Subtype 0x0B) +/// +/// If the functionality has been enabled, the device will send out a message each time the note is removed +/// after a return/reject. +/// +/// The Note Retrieved Event is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | Event | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:-----:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | +/// | Value | 0x02 | 0x0D | 0x7n | 0x0B | nn | nn | nn | nn | nn | nn | 0x7F | 0x03 | zz | +/// +/// The `0x7F` for the `Event byte signifies that the note has been removed by the user. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NoteRetrievedEvent { + buf: [u8; NOTE_RETRIEVED_EVENT], +} + +impl NoteRetrievedEvent { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; NOTE_RETRIEVED_EVENT], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::NoteRetrieved); + message.buf[index::EVENT] = EVENT; + + message + } + + pub fn retrieved_event(&self) -> u8 { + self.buf[index::EVENT] + } +} + +impl_default!(NoteRetrievedEvent); +impl_message_ops!(NoteRetrievedEvent); +impl_omnibus_extended_reply!(NoteRetrievedEvent); +impl_extended_ops!(NoteRetrievedEvent); + +impl fmt::Display for NoteRetrievedEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, Retrieved Event: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + self.retrieved_event(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_note_retrieved_reply_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type | Subtype + 0x02, 0x0d, 0x70, 0x0b, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ACK/NAK + 0x01, + // ETX | Checksum + 0x03, 0x77, + ]; + + let mut msg = NoteRetrievedReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::NoteRetrieved); + assert_eq!(msg.retrieved_acknak(), RetrieveAckNak::Set); + + let msg_bytes = [ + // STX | LEN | Message Type | Subtype + 0x02, 0x0d, 0x70, 0x0b, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ACK/NAK + 0x00, + // ETX | Checksum + 0x03, 0x76, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::NoteRetrieved); + assert_eq!(msg.retrieved_acknak(), RetrieveAckNak::Unset); + + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_note_retrieved_event_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type | Subtype + 0x02, 0x0d, 0x70, 0x0b, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Event + 0x7f, + // ETX | Checksum + 0x03, 0x09, + ]; + + let mut msg = NoteRetrievedEvent::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::NoteRetrieved); + assert_eq!(msg.retrieved_event(), 0x7f); + + let msg_bytes = [ + // STX | LEN | Message Type | Subtype + 0x02, 0x0d, 0x70, 0x0b, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Event (any non-0x7f value is invalid) + 0x7e, + // ETX | Checksum + 0x03, 0x08, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.retrieved_event(), 0x7e); + + Ok(()) + } +} diff --git a/src/omnibus.rs b/src/omnibus.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/omnibus.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/omnibus/command.rs b/src/omnibus/command.rs new file mode 100644 index 0000000..1a02047 --- /dev/null +++ b/src/omnibus/command.rs @@ -0,0 +1,547 @@ +use bitfield::bitfield; + +use crate::{ + bool_enum, impl_default, impl_message_ops, impl_omnibus_command_ops, + len::{FLASH_DATA_PACKET, OMNIBUS_COMMAND}, + FlashDownloadMessage, MessageOps, MessageType, StandardDenomination, +}; + +bitfield! { + /// Operational Mode - Omnibus Command: data byte 1 + /// + /// [OperationalMode] controls details about the operational mode of the device. + /// It also contains the very important command bits that control what to do with a note in escrow. + /// + /// It is a bitfield representing the following settings: + /// + /// * Special Interrupt Mode: bit 0 (**Deprecated** **Obsolete** **Unimplemented**) + /// * High Security Mode: bit 1 (**Deprecated**: Enabling is deprecated/unimplemented, defaults to high acceptance mode) + /// * [OrientationControl]: bits 2..3 + /// * [EscrowMode]: bit 4 + /// * [DocumentStack]: bit 5 + /// * [DocumentReturn]: bit 6 + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct OperationalMode(u8); + u8; + /// This field controls the acceptance of bank notes based on the orientation of those + /// notes as they enter the device. Note that note orientations can also be controlled by + /// a configuration coupon or on some models, “DIP” switches. In all cases, the most + /// accommodating of the settings is used. See the Controlling the Orientation of Notes + /// section (5.2.3) for more details on controlling the orientation of note acceptance. + pub orientation_control, set_orientation_control: 3, 2; + /// This mode determines how documents are handled after the documents have been + /// validated. Note that documents that are unable to be validated are always rejected. + /// This concept is discussed in detail in section 4.3. + pub escrow_mode, set_escrow_mode: 4; + /// If a document is in escrow, stack it in the cash box. Note that this + /// command is only valid if Escrow mode is enabled and a document is in + /// escrow. This command and the Document Return command are + /// mutually exclusive. + pub document_stack, set_document_stack: 5; + /// If a document is in escrow, return it to the consumer. Note that this + /// command is only valid if Escrow mode is enabled and a document is in + /// escrow. This command and the Document Stack command are mutually + /// exclusive. + pub document_return, set_document_return: 6; +} + +impl From for u8 { + fn from(o: OperationalMode) -> Self { + o.0 + } +} + +impl From<&OperationalMode> for u8 { + fn from(o: &OperationalMode) -> Self { + o.0 + } +} + +impl From for OperationalMode { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +/// Controls which bill orientation is accepted +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum OrientationControl { + /// Accept notes fed right edge first, face up only + OneWay = 0b00, + /// Accept notes face up only + TwoWay = 0b01, + /// Accept notes fed any way + FourWay = 0b10, +} + +impl From for OrientationControl { + fn from(b: u8) -> Self { + match b & bitmask::ORIENTATION { + 0b00 => Self::OneWay, + 0b01 => Self::TwoWay, + 0b10 | 0b11 => Self::FourWay, + _ => unreachable!("invalid OrientationControl"), + } + } +} + +impl From for u8 { + fn from(o: OrientationControl) -> Self { + o as u8 + } +} + +impl From<&OrientationControl> for u8 { + fn from(o: &OrientationControl) -> Self { + (*o).into() + } +} + +bool_enum!( + EscrowMode, + r" + Determines how documents are handled after validation + + Unset: + **Deprecated/Obsolete**: Escrow is disabled + + Set: + Escrow mode is enabled +" +); + +bool_enum!( + DocumentStack, + r" + If a document is in escrow, stack it in the cash box + + This command is mutually exclusive with [DocumentReturn](DocumentReturn) + + Unset: + No-op + + Set: + Stack a document in the cash box. Only valid if escrow mode enabled +" +); + +bool_enum!( + DocumentReturn, + r" + If a document is in escrow, return it to the consumer + + This command is mutually exclusive with [DocumentStack](DocumentStack) + + Unset: + No-op + + Set: + Return a document to the consumer. Only valid if escrow mode enabled +" +); + +bitfield! { + /// Configuration - Omnibus Command: data byte 2 + /// + /// [Configuration] contains configuration settings for the device. + /// + /// It is a bitfield representing the following settings: + /// + /// * [NoPush]: bit 0 + /// * [Barcode]: bit 1 + /// * [PowerUp]: bits 2..3 + /// * [ExtendedNoteReporting]: bit 4 + /// * [ExtendedCouponReporting]: bit 5 + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Configuration(u8); + u8; + pub no_push, set_no_push: 0; + pub barcode, set_barcode: 1; + pub power_up, set_power_up: 3, 2; + pub extended_note, set_extended_note: 4; + pub extended_coupon, set_extended_coupon: 5; +} + +impl From for u8 { + fn from(c: Configuration) -> Self { + c.0 + } +} + +impl From<&Configuration> for u8 { + fn from(c: &Configuration) -> Self { + c.0 + } +} + +impl From for Configuration { + fn from(b: u8) -> Self { + Self(b & 0b11_1111) + } +} + +bool_enum!( + NoPush, + r" + There are times when the device is unable to give credit for a note due to a + problem in transporting the document, and the document cannot be returned + to the customer. In these cases, this bit determines how such documents should + be handled. + + Unset: + Recommended: Push non-credit notes into the stacker, and continue operating. + + Set: + Do not push non-credit notes. Stall the device with the document still in the path. A + manager/technician level intervention is required. +" +); + +bool_enum!( + Barcode, + r" + A barcode voucher is a document with a unique bar-coded identity number + encoded into it. These identity numbers are referenced against an external + database by the host to determine the validity and value of the voucher. + Notes: Barcode vouchers must be inserted “face up” with CFSC devices. + + Unset: + Barcode vouchers are disabled. + + Set: + Barcode vouchers are enabled. +" +); + +/// Values that represent acceptor device power up policy. +/// That define device behavior on power up with bill in trace. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PowerUp { + /// Post - Escrow : The procedure will complete and the document will be stacked. However, no value will be reported to the host. + A = 0b00, + /// Escrow : The device will go out of order and hold the document at the escrow position. + /// Post - Escrow : The procedure will complete and the document will be stacked. + B = 0b01, + /// Escrow : The device will go out of order and hold the document at the escrow position. + /// Post - Escrow : The procedure will complete and the document will be stacked.However, no value will be reported to the host. + C = 0b10, + Reserved = 0b11, +} + +impl From for PowerUp { + fn from(b: u8) -> Self { + match b & bitmask::POWER_UP { + 0b00 => Self::A, + 0b01 => Self::B, + 0b10 => Self::B, + _ => Self::Reserved, + } + } +} + +bool_enum!( + ExtendedNoteReporting, + r" + Whether to use extended note reporting for bank note values. + + Unset: + Use non-extended note reporting. Notes are reported as the generic Denom1 through 7. + + Set: + Use extended note reporting. Notes are reported to the host via the Extended Omnibus. + + - Note Reply packets. See Section 7.5.2 (Subtype 0x02) + Extended Note Specification Message for details. + + - Notes are enabled / inhibited individually via the Set Note + Inhibits command. See (Subtype 0x03) Set Extended Note Inhibits for details. + + - This bit is also associated with enabling / disabling the device. + See Section 4.9 Disabling the Device and Inhibiting Notes. +" +); + +bool_enum!( + ExtendedCouponReporting, + r" + Handling for MEI/CPI coupon vouchers. + + Unset: + Recommended: No special handling of generic coupons. MEI™ Generic Coupons (if + supported) are reported the same as a bank note of the same value. + Free vend coupons are not supported. + + Set: + Enable detailed reporting of MEI Generic Coupons. The host + receives details on the type and identification of generic coupons fed + into the device. See the (Subtype 0x04) Set Escrow Timeout / + Extended Coupon response (7.5.4) for more details about the device’s + message when a coupon is detected. +" +); + +pub(crate) mod index { + use crate::index::DATA; + + pub const DENOMINATION: usize = DATA; + pub const OPERATIONAL_MODE: usize = DATA + 1; + pub const CONFIGURATION: usize = DATA + 2; +} + +mod bitmask { + pub const ORIENTATION: u8 = 0b11; + pub const POWER_UP: u8 = 0b11; +} + +/// Omnibus Command - (Type 1) +/// +/// The concept behind the omnibus command is simple. The host sends a packet to the device with +/// virtually everything needed to control a bill acceptor, and the device responds with a packet with +/// virtually everything needed by the host. Thus in theory, only one command is needed. In practice, the +/// sophistication of the command set long ago reached the point where it was not feasible to fit in all the +/// data all the time. Thus the auxiliary and extended commands were created. Despite this, the omnibus +/// command remains the very core of EBDS and the most frequently used command. +/// +/// The Omnibus Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | Data 2 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 4 | 5 | 6 | 7 | 8 | +/// | Value | 0x02 | 0x09 | 0x1n | nn | nn | nn | 0x03 | zz | +/// +/// The data may vary, and is represented by the `nn`. +/// +/// Finally, the checksum is denoted with a `zz`. +/// +/// This convention will be used throughout the documentation. +/// +/// The data bytes are bitfields used to represent: +/// +/// * Data byte 0: the [StandardDenomination]s enabled by the device +/// * Data byte 1: the [OperationalMode] settings +/// * Data byte 2: the [Configuration] settings +/// +/// For more detailed information about the meaning of the data fields, see the EBDS Protocol +/// Specification: sections 7.1.1.[1-3]. +#[repr(C)] +pub struct OmnibusCommand { + buf: [u8; OMNIBUS_COMMAND], +} + +impl OmnibusCommand { + /// Create a new OmnibusCommand message + pub fn new() -> Self { + let mut command = Self { + buf: [0u8; OMNIBUS_COMMAND], + }; + + command.init(); + command.set_message_type(MessageType::OmnibusCommand); + command.set_escrow_mode(EscrowMode::Set); + + command + } +} + +pub trait OmnibusCommandOps: MessageOps { + /// Get the denomination data byte (data byte 0) + fn denomination(&self) -> StandardDenomination { + self.buf()[index::DENOMINATION].into() + } + + /// Set the denomination data byte + fn set_denomination(&mut self, denomination: StandardDenomination) { + self.buf_mut()[index::DENOMINATION] = denomination.into(); + } + + /// Get the operational mode data byte (data byte 1) + fn operational_mode(&self) -> OperationalMode { + self.buf()[index::OPERATIONAL_MODE].into() + } + + /// Set the operational mode data byte (data byte 1) + fn set_operational_mode(&mut self, op: OperationalMode) { + self.buf_mut()[index::OPERATIONAL_MODE] = op.into(); + } + + /// Get the orientation control data field + fn orientation_control(&self) -> OrientationControl { + self.operational_mode().orientation_control().into() + } + + /// Set the orientation control data field + fn set_orientation_control(&mut self, orientation: OrientationControl) { + let mut op = self.operational_mode(); + op.set_orientation_control(orientation as u8); + self.set_operational_mode(op); + } + + /// Get the escrow mode data field + fn escrow_mode(&self) -> EscrowMode { + self.operational_mode().escrow_mode().into() + } + + /// Set the escrow mode data field + fn set_escrow_mode(&mut self, escrow_mode: EscrowMode) { + let mut op = self.operational_mode(); + op.set_escrow_mode(escrow_mode.into()); + self.set_operational_mode(op); + } + + /// Get the document stack data field + fn document_stack(&self) -> DocumentStack { + self.operational_mode().document_stack().into() + } + + /// Set the document stack data field + fn set_document_stack(&mut self, document_stack: DocumentStack) { + let mut op = self.operational_mode(); + op.set_document_stack(document_stack.into()); + self.set_operational_mode(op); + } + + /// Get the document return data field + fn document_return(&self) -> DocumentReturn { + self.operational_mode().document_return().into() + } + + /// Set the document return data field + fn set_document_return(&mut self, document_return: DocumentReturn) { + let mut op = self.operational_mode(); + op.set_document_return(document_return.into()); + self.set_operational_mode(op); + } + + /// Get the device configuration setting data byte (data byte 2) + fn configuration(&self) -> Configuration { + self.buf()[index::CONFIGURATION].into() + } + + /// Set the device configuration setting data byte (data byte 2) + fn set_configuration(&mut self, cfg: Configuration) { + self.buf_mut()[index::CONFIGURATION] = cfg.into(); + } + + /// Get the no push data field + fn no_push(&self) -> NoPush { + self.configuration().no_push().into() + } + + /// Set the no push data field + fn set_no_push(&mut self, no_push: NoPush) { + let mut cfg = self.configuration(); + cfg.set_no_push(no_push.into()); + self.set_configuration(cfg); + } + + /// Get the barcode data field + fn barcode(&self) -> Barcode { + self.configuration().barcode().into() + } + + /// Set the barcode data field + fn set_barcode(&mut self, barcode: Barcode) { + let mut cfg = self.configuration(); + cfg.set_barcode(barcode.into()); + self.set_configuration(cfg); + } + + /// Get the power up data field + fn power_up(&self) -> PowerUp { + self.configuration().power_up().into() + } + + /// Set the power up data field + fn set_power_up(&mut self, power_up: PowerUp) { + let mut cfg = self.configuration(); + cfg.set_power_up(power_up as u8); + self.set_configuration(cfg); + } + + /// Get the extended note reporting data field + fn extended_note(&self) -> ExtendedNoteReporting { + self.configuration().extended_note().into() + } + + /// Set the extended note data field + fn set_extended_note(&mut self, extended_note: ExtendedNoteReporting) { + let mut cfg = self.configuration(); + cfg.set_extended_note(extended_note.into()); + self.set_configuration(cfg); + } + + /// Get the extended coupon reporting data field + fn extended_coupon(&self) -> ExtendedCouponReporting { + self.configuration().extended_coupon().into() + } + + /// Set the extended coupon data field + fn set_extended_coupon(&mut self, extended_coupon: ExtendedCouponReporting) { + let mut cfg = self.configuration(); + cfg.set_extended_coupon(extended_coupon.into()); + self.set_configuration(cfg); + } +} + +impl_default!(OmnibusCommand); +impl_message_ops!(OmnibusCommand); +impl_omnibus_command_ops!(OmnibusCommand); + +// Implements FlashDownloadMessage to allow using OmnibusCommand in +// `AcceptorDeviceHandle::poll_flash_download` +impl FlashDownloadMessage for OmnibusCommand { + fn is_initial_poll(&self) -> bool { + true + } + + fn packet_number(&self) -> u16 { + 0xffff + } + + fn set_packet_number(&mut self, _n: u16) {} + + fn increment_packet_number(&mut self) -> u16 { + 0xffff + } + + fn data(&self) -> [u8; FLASH_DATA_PACKET] { + [0u8; FLASH_DATA_PACKET] + } + + fn data_ref(&self) -> &[u8] { + self.buf() + } + + fn set_data(&mut self, _data: &[u8]) {} +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_omnibus_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x10, + // Data + 0x7f, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x67, + ]; + + let mut msg = OmnibusCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::OmnibusCommand); + assert_eq!(msg.denomination(), StandardDenomination::all()); + assert_eq!(msg.operational_mode(), OperationalMode::from(0)); + assert_eq!(msg.configuration(), Configuration::from(0)); + + Ok(()) + } +} diff --git a/src/omnibus/reply.rs b/src/omnibus/reply.rs new file mode 100644 index 0000000..8109b75 --- /dev/null +++ b/src/omnibus/reply.rs @@ -0,0 +1,531 @@ +use crate::std; +use std::fmt; + +use crate::{ + banknote::*, impl_default, impl_from_for_omnibus_reply, impl_message_ops, + impl_omnibus_reply_ops, len::OMNIBUS_REPLY, status::*, AdvancedBookmarkModeReply, + ClearAuditDataRequestAck, ClearAuditDataRequestResults, ExtendedNoteInhibitsReplyAlt, + ExtendedNoteReply, MessageOps, MessageType, NoteRetrievedEvent, NoteRetrievedReply, + QueryApplicationIdReply, QueryApplicationPartNumberReply, QueryBootPartNumberReply, + QueryDeviceCapabilitiesReply, QueryValueTableReply, QueryVariantIdReply, QueryVariantNameReply, + QueryVariantPartNumberReply, SetEscrowTimeoutReply, StandardDenomination, +}; + +pub(crate) mod index { + use crate::index::DATA; + + pub const DEVICE_STATE: usize = DATA; + pub const DEVICE_STATUS: usize = DATA + 1; + pub const EXCEPTION_STATUS: usize = DATA + 2; + pub const MISC_DEVICE_STATE: usize = DATA + 3; + pub const MODEL_NUMBER: usize = DATA + 4; + pub const CODE_REVISION: usize = DATA + 5; +} + +/// Omnibus Reply - (Type 2) +/// +/// [OmnibusReply] represents a message sent from the device back to the hostl +/// +/// The most common reply to an [OmnibusCommand](crate::OmnibusCommand) is the standard reply. +/// +/// However, if barcode vouchers, extended note, extended coupon reporting is enabled in the standard +/// omnibus command, or if the unit is an SCR, then other reply formats are possible. +/// +/// These replies are detailed in sections 7.5.1, 7.5.2, 7.5.4, and 7.5.15 respectively. +/// +/// There are also other circumstances that may result in the device responding back +/// to the host with a different type of message. These special cases will be described in a future section +/// when the associated feature is enabled. +/// +/// The Omnibus Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:------:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | +/// | Value | 0x02 | 0x09 | 0x1n | nn | nn | nn | nn | nn | nn | 0x03 | zz | +/// +/// The data bytes are bitfields representing device information: +/// +/// * Data byte 0: [DeviceState] +/// * Data byte 1: [DeviceStatus] +/// * Data byte 2: [ExceptionStatus] +/// * Data byte 3: [MiscDeviceState] +/// * Data byte 4: [ModelNumber] +/// * Data byte 5: [CodeRevision] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct OmnibusReply { + buf: [u8; OMNIBUS_REPLY], +} + +impl OmnibusReply { + /// Create a new OmnibusReply message + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; OMNIBUS_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::OmnibusReply); + + message + } +} + +impl_from_for_omnibus_reply!(AdvancedBookmarkModeReply); +impl_from_for_omnibus_reply!(ClearAuditDataRequestAck); +impl_from_for_omnibus_reply!(ClearAuditDataRequestResults); +impl_from_for_omnibus_reply!(ExtendedNoteReply); +impl_from_for_omnibus_reply!(ExtendedNoteInhibitsReplyAlt); +impl_from_for_omnibus_reply!(NoteRetrievedReply); +impl_from_for_omnibus_reply!(NoteRetrievedEvent); +impl_from_for_omnibus_reply!(QueryValueTableReply); +impl_from_for_omnibus_reply!(SetEscrowTimeoutReply); +impl_from_for_omnibus_reply!(QueryBootPartNumberReply); +impl_from_for_omnibus_reply!(QueryApplicationPartNumberReply); +impl_from_for_omnibus_reply!(QueryVariantNameReply); +impl_from_for_omnibus_reply!(QueryVariantPartNumberReply); +impl_from_for_omnibus_reply!(QueryDeviceCapabilitiesReply); +impl_from_for_omnibus_reply!(QueryApplicationIdReply); +impl_from_for_omnibus_reply!(QueryVariantIdReply); + +impl fmt::Display for OmnibusReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + ) + } +} + +pub trait OmnibusReplyOps: MessageOps { + /// Get the device state data field + fn device_state(&self) -> DeviceState { + self.buf()[index::DEVICE_STATE].into() + } + + /// Set the device state data field + fn set_device_state(&mut self, device_state: DeviceState) { + self.buf_mut()[index::DEVICE_STATE] = device_state.into(); + } + + /// Get the idling device state data field + fn idling(&self) -> Idling { + self.device_state().idling().into() + } + + /// Get the idling device state data field + fn set_idling(&mut self, idling: Idling) { + let mut state = self.device_state(); + state.set_idling(idling.into()); + self.set_device_state(state); + } + + /// Get the accepting device state data field + fn accepting(&self) -> Accepting { + self.device_state().accepting().into() + } + + /// Get the accepting device state data field + fn set_accepting(&mut self, accepting: Accepting) { + let mut state = self.device_state(); + state.set_accepting(accepting.into()); + self.set_device_state(state); + } + + /// Get the escrowed state device state data field + fn escrowed_state(&self) -> EscrowedState { + self.device_state().escrowed_state().into() + } + + /// Get the escrowed state device state data field + fn set_escrowed_state(&mut self, escrowed_state: EscrowedState) { + let mut state = self.device_state(); + state.set_escrowed_state(escrowed_state.into()); + self.set_device_state(state); + } + + /// Get the stacking device state data field + fn stacking(&self) -> Stacking { + self.device_state().stacking().into() + } + + /// Get the stacking device state data field + fn set_stacking(&mut self, stacking: Stacking) { + let mut state = self.device_state(); + state.set_stacking(stacking.into()); + self.set_device_state(state); + } + + /// Get the stacked event device state data field + fn stacked_event(&self) -> StackedEvent { + self.device_state().stacked_event().into() + } + + /// Get the stacked event device state data field + fn set_stacked_event(&mut self, stacked_event: StackedEvent) { + let mut state = self.device_state(); + state.set_stacked_event(stacked_event.into()); + self.set_device_state(state); + } + + /// Get the returning device state data field + fn returning(&self) -> Returning { + self.device_state().returning().into() + } + + /// Get the returning device state data field + fn set_returning(&mut self, returning: Returning) { + let mut state = self.device_state(); + state.set_returning(returning.into()); + self.set_device_state(state); + } + + /// Get the returned event device state data field + fn returned_event(&self) -> ReturnedEvent { + self.device_state().returned_event().into() + } + + /// Get the returned event device state data field + fn set_returned_event(&mut self, returned_event: ReturnedEvent) { + let mut state = self.device_state(); + state.set_returned_event(returned_event.into()); + self.set_device_state(state); + } + + /// Get the device status data field + fn device_status(&self) -> DeviceStatus { + self.buf()[index::DEVICE_STATUS].into() + } + + fn set_device_status(&mut self, device_status: DeviceStatus) { + self.buf_mut()[index::DEVICE_STATUS] = device_status.into(); + } + + /// Get the cheated device status data field + fn cheated(&self) -> Cheated { + self.device_status().cheated().into() + } + + /// Set the cheated device status data field + fn set_cheated(&mut self, cheated: Cheated) { + let mut status = self.device_status(); + status.set_cheated(cheated.into()); + self.set_device_status(status); + } + + /// Get the rejected device status data field + fn rejected(&self) -> Rejected { + self.device_status().rejected().into() + } + + /// Set the rejected device status data field + fn set_rejected(&mut self, rejected: Rejected) { + let mut status = self.device_status(); + status.set_rejected(rejected.into()); + self.set_device_status(status); + } + + /// Get the jammed device status data field + fn jammed(&self) -> Jammed { + self.device_status().jammed().into() + } + + /// Set the jammed device status data field + fn set_jammed(&mut self, jammed: Jammed) { + let mut status = self.device_status(); + status.set_jammed(jammed.into()); + self.set_device_status(status); + } + + /// Get the stacker full device status data field + fn stacker_full(&self) -> StackerFull { + self.device_status().stacker_full().into() + } + + /// Set the stacker full device status data field + fn set_stacker_full(&mut self, stacker_full: StackerFull) { + let mut status = self.device_status(); + status.set_stacker_full(stacker_full.into()); + self.set_device_status(status); + } + + /// Get the cassette attached device status data field + fn cassette_attached(&self) -> CassetteAttached { + self.device_status().cassette_attached().into() + } + + /// Set the cassette attached device status data field + fn set_cassette_attached(&mut self, cassette_attached: CassetteAttached) { + let mut status = self.device_status(); + status.set_cassette_attached(cassette_attached.into()); + self.set_device_status(status); + } + + /// Get the status of the cash box + fn cash_box_status(&self) -> CashBoxStatus { + let status = self.device_status(); + + if status.stacker_full() { + CashBoxStatus::Full + } else if status.cassette_attached() { + CashBoxStatus::Attached + } else { + CashBoxStatus::Removed + } + } + + /// Get the paused device status data field + fn paused(&self) -> Paused { + self.device_status().paused().into() + } + + /// Set the paused device status data field + fn set_paused(&mut self, paused: Paused) { + let mut status = self.device_status(); + status.set_paused(paused.into()); + self.set_device_status(status); + } + + /// Get the calibration in progress device status data field + fn calibration(&self) -> Calibration { + self.device_status().calibration().into() + } + + /// Set the calibration in progress device status data field + fn set_calibration(&mut self, calibration: Calibration) { + let mut status = self.device_status(); + status.set_calibration(calibration.into()); + self.set_device_status(status); + } + + /// Get the exception status data field + fn exception_status(&self) -> ExceptionStatus { + self.buf()[index::EXCEPTION_STATUS].into() + } + + fn set_exception_status(&mut self, exception_status: ExceptionStatus) { + self.buf_mut()[index::EXCEPTION_STATUS] = exception_status.into(); + } + + /// Get the power up status data field + fn power_up(&self) -> PowerUpStatus { + self.exception_status().power_up().into() + } + + /// Set the power up status data field + fn set_power_up(&mut self, power_up: PowerUpStatus) { + let mut ex = self.exception_status(); + ex.set_power_up(power_up.into()); + self.set_exception_status(ex); + } + + /// Get the invalid command data field + fn invalid_command(&self) -> InvalidCommand { + self.exception_status().invalid_command().into() + } + + /// Set the invalid command data field + fn set_invalid_command(&mut self, invalid_command: InvalidCommand) { + let mut ex = self.exception_status(); + ex.set_invalid_command(invalid_command.into()); + self.set_exception_status(ex); + } + + /// Get the failure data field + fn failure(&self) -> Failure { + self.exception_status().failure().into() + } + + /// Set the failure data field + fn set_failure(&mut self, failure: Failure) { + let mut ex = self.exception_status(); + ex.set_failure(failure.into()); + self.set_exception_status(ex); + } + + /// Get the note value data field + fn note_value(&self) -> StandardDenomination { + self.exception_status().note_value().into() + } + + /// Set the note value data field + fn set_note_value(&mut self, note_value: StandardDenomination) { + let mut ex = self.exception_status(); + ex.set_note_value(note_value.into()); + self.set_exception_status(ex); + } + + /// Get the transport open data field + fn transport_open(&self) -> TransportOpen { + self.exception_status().transport_open().into() + } + + /// Set the transport open data field + fn set_transport_open(&mut self, transport_open: TransportOpen) { + let mut ex = self.exception_status(); + ex.set_transport_open(transport_open.into()); + self.set_exception_status(ex); + } + + /// Get the miscellaneous device status data field + fn misc_device_state(&self) -> MiscDeviceState { + self.buf()[index::MISC_DEVICE_STATE].into() + } + + fn set_misc_device_state(&mut self, misc_device_state: MiscDeviceState) { + self.buf_mut()[index::MISC_DEVICE_STATE] = misc_device_state.into(); + } + + /// Get the stalled data field + fn stalled(&self) -> Stalled { + self.misc_device_state().stalled().into() + } + + /// Set the stalled data field + fn set_stalled(&mut self, stalled: Stalled) { + let mut misc = self.misc_device_state(); + misc.set_stalled(stalled.into()); + self.set_misc_device_state(misc); + } + + /// Get the flash download data field + fn flash_download(&self) -> FlashDownload { + self.misc_device_state().flash_download().into() + } + + /// Set the flash download data field + fn set_flash_download(&mut self, flash_download: FlashDownload) { + let mut misc = self.misc_device_state(); + misc.set_flash_download(flash_download.into()); + self.set_misc_device_state(misc); + } + + /// Get the pre-stack data field + fn pre_stack(&self) -> PreStack { + self.misc_device_state().pre_stack().into() + } + + /// Set the pre-stack data field + fn set_pre_stack(&mut self, pre_stack: PreStack) { + let mut misc = self.misc_device_state(); + misc.set_pre_stack(pre_stack.into()); + self.set_misc_device_state(misc); + } + + /// Get the raw barcode data field + fn raw_barcode(&self) -> RawBarcode { + self.misc_device_state().raw_barcode().into() + } + + /// Set the raw barcode data field + fn set_raw_barcode(&mut self, raw_barcode: RawBarcode) { + let mut misc = self.misc_device_state(); + misc.set_raw_barcode(raw_barcode.into()); + self.set_misc_device_state(misc); + } + + /// Get the device capabilities data field + fn device_capabilities(&self) -> DeviceCapabilities { + self.misc_device_state().device_capabilities().into() + } + + /// Set the device capabilities data field + fn set_device_capabilities(&mut self, device_capabilities: DeviceCapabilities) { + let mut misc = self.misc_device_state(); + misc.set_device_capabilities(device_capabilities.into()); + self.set_misc_device_state(misc); + } + + /// Get the disabled data field + fn disabled(&self) -> Disabled { + self.misc_device_state().disabled().into() + } + + /// Set the disabled data field + fn set_disabled(&mut self, disabled: Disabled) { + let mut misc = self.misc_device_state(); + misc.set_disabled(disabled.into()); + self.set_misc_device_state(misc); + } + + /// Get the model number data field + fn model_number(&self) -> ModelNumber { + self.buf()[index::MODEL_NUMBER].into() + } + + /// Set the model number data field + fn set_model_number(&mut self, model_number: ModelNumber) { + self.buf_mut()[index::MODEL_NUMBER] = model_number.into(); + } + + /// Get the code revision data field + fn code_revision(&self) -> CodeRevision { + self.buf()[index::CODE_REVISION].into() + } + + /// Set the code revision data field + fn set_code_revision(&mut self, code_revision: CodeRevision) { + self.buf_mut()[index::CODE_REVISION] = code_revision.into() + } +} + +impl_default!(OmnibusReply); +impl_message_ops!(OmnibusReply); +impl_omnibus_reply_ops!(OmnibusReply); + +impl From<&OmnibusReply> for Banknote { + fn from(reply: &OmnibusReply) -> Self { + use crate::bau_currency; + + let note_value = bau_currency().denomination_value_base(reply.note_value()); + Self::default().with_value(note_value.into()) + } +} + +impl From<&dyn OmnibusReplyOps> for DocumentStatus { + fn from(reply: &dyn OmnibusReplyOps) -> Self { + Self::default().with_standard_denomination(reply.note_value()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_omnibus_reply_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0b, 0x20, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x2b, + ]; + + let mut msg = OmnibusReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::OmnibusReply); + assert_eq!(msg.device_state(), DeviceState::from(0)); + assert_eq!(msg.device_status(), DeviceStatus::from(0)); + assert_eq!(msg.exception_status(), ExceptionStatus::from(0)); + assert_eq!(msg.misc_device_state(), MiscDeviceState::from(0)); + assert_eq!(msg.model_number(), ModelNumber::from(0)); + assert_eq!(msg.code_revision(), CodeRevision::from(0)); + + Ok(()) + } +} diff --git a/src/part_number.rs b/src/part_number.rs new file mode 100644 index 0000000..41b9642 --- /dev/null +++ b/src/part_number.rs @@ -0,0 +1,449 @@ +use crate::std; +use std::fmt; + +#[cfg(not(feature = "std"))] +use alloc::string::String; + +/// Check digit for the [ProjectNumber]. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct CheckDigit(u8); + +impl CheckDigit { + /// The length (in bytes) of the [CheckDigit]. + pub const LEN: usize = 1; + + /// Converts [CheckDigit] to a u8. + pub const fn as_u8(&self) -> u8 { + self.0 + } +} + +impl From for CheckDigit { + /// Parse the byte as an ASCII string. + /// + /// If a direct conversion from a u8 is wanted, use: + /// + /// let num = 0x0u8; + /// let _ = CheckDigit(num); + fn from(b: u8) -> Self { + let digit = std::str::from_utf8(&[b]) + .unwrap_or("") + .parse::() + .unwrap_or(0xff); + + Self(digit) + } +} + +impl From for u8 { + fn from(c: CheckDigit) -> Self { + c.0 + } +} + +impl From<&CheckDigit> for u8 { + fn from(c: &CheckDigit) -> Self { + (*c).into() + } +} + +/// The Application Part Number type. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PartNumberType { + Type1 = 1, + Type2 = 2, + Variant = 3, + Unknown = 0x00, +} + +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. Please see the following table for expected values. +/// +/// | Project Number (5-6 bytes) | Check Digit (0-1 bytes) | Version (3 bytes) | Description | +/// |----------------------------|-------------------------|-------------------|-------------| +/// | 28000...28599 | | | Type 1 Application Part Number (Requires Check Digit) **CFSC Only** | +/// | 286000...289999 | | | Type 2 Application Part Number (No Check Digit) | +/// | | 0...9 | | Check digit (Not applicable for Type 2 Application Part Numbers) **CFSC Only** | +/// | | | 000...999 | Formatted as V1.23 | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ProjectNumber { + number: u32, + check_digit: CheckDigit, + part_type: PartNumberType, +} + +impl ProjectNumber { + /// The length (in bytes) of the [ProjectNumber]. + /// + /// The length represents the ASCII string length, not the internal representation. + pub const LEN: usize = 6; + /// The length (in bytes) of a Type 1 Application Part Number. + pub const TYPE_1_LEN: usize = 5; + /// The length (in bytes) of a Type 2 Application Part Number. + pub const TYPE_2_LEN: usize = 6; + /// The length (in bytes) of a Variant Application Part Number. + pub const VARIANT_LEN: usize = 5; + /// The index of the Checksum Digit (invalid for Type 2 Application Part Number). + pub const CHECK_DIGIT_IDX: usize = 5; + + /// Creates a Type 1 [ProjectNumber] from a number and [CheckDigit]. + pub const fn type1(number: u32, check_digit: CheckDigit) -> Self { + Self { + number, + check_digit, + part_type: PartNumberType::Type1, + } + } + + /// Creates a Type 2 [ProjectNumber]. + pub const fn type2(number: u32) -> Self { + Self { + number, + check_digit: CheckDigit(0xff), + part_type: PartNumberType::Type2, + } + } + + /// Creates a Variant [ProjectNumber] from a number and [CheckDigit]. + pub const fn variant(number: u32, check_digit: CheckDigit) -> Self { + Self { + number, + check_digit, + part_type: PartNumberType::Variant, + } + } + + /// Creates a zeroed [ProjectNumber]. + pub const fn zero() -> Self { + Self { + number: 0, + check_digit: CheckDigit(0x00), + part_type: PartNumberType::Unknown, + } + } + + /// Gets the application part number. + pub const fn number(&self) -> u32 { + self.number + } + + /// Gets the check digit. + pub const fn check_digit(&self) -> CheckDigit { + self.check_digit + } + + /// Gets the [PartNumberType]. + pub const fn part_type(&self) -> PartNumberType { + self.part_type + } +} + +impl From<&[u8]> for ProjectNumber { + fn from(b: &[u8]) -> Self { + if b.len() < Self::LEN { + return Self::zero(); + } + + let type1: u32 = std::str::from_utf8(b[..Self::TYPE_1_LEN].as_ref()) + .unwrap_or("") + .parse::() + .unwrap_or(0); + + let type2: u32 = std::str::from_utf8(b[..Self::TYPE_2_LEN].as_ref()) + .unwrap_or("") + .parse::() + .unwrap_or(0); + + if (28_000..=28_599).contains(&type1) { + Self::type1(type1, CheckDigit::from(b[Self::CHECK_DIGIT_IDX])) + } else if (49000..=49999).contains(&type1) || (51_000..=52_999).contains(&type1) { + Self::variant(type1, CheckDigit::from(b[Self::CHECK_DIGIT_IDX])) + } else if (286_000..=289_999).contains(&type2) { + Self::type2(type2) + } else { + Self::zero() + } + } +} + +impl fmt::Display for ProjectNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.part_type() { + PartNumberType::Type1 => write!( + f, + "{} check digit: {}", + self.number, + self.check_digit.as_u8() + ), + PartNumberType::Type2 => write!(f, "{}", self.number), + PartNumberType::Variant => write!( + f, + "{} check digit: {}", + self.number, + self.check_digit.as_u8() + ), + PartNumberType::Unknown => write!(f, "Unknown"), + } + } +} + +/// The Boot Version number. +/// +/// Formatted as the 3-digit ASCII string divided by one hundred. +/// +/// Example: +/// +/// ```rust +/// # use ebds::PartVersion; +/// let version = PartVersion::from(b"123"); +/// let formatted_version = version.as_string(); +/// assert_eq!(formatted_version, "V1.23"); +/// ``` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PartVersion(u16); + +impl PartVersion { + /// The length (in bytes) of the [PartVersion]. + /// + /// The length represents the ASCII string length, not the internal representation. + pub const LEN: usize = 3; + + pub fn as_string(&self) -> String { + format!("{self}") + } +} + +impl From<&[u8]> for PartVersion { + fn from(b: &[u8]) -> Self { + let version = std::str::from_utf8(b) + .unwrap_or("") + .parse::() + .unwrap_or(0); + + if version > 999 { + Self(0) + } else { + Self(version) + } + } +} + +impl From<[u8; N]> for PartVersion { + fn from(b: [u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&[u8; N]> for PartVersion { + fn from(b: &[u8; N]) -> Self { + b.as_ref().into() + } +} + +impl fmt::Display for PartVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "V{:.2}", (self.0 as f32) / 100f32) + } +} + +/// The boot part number from the device firmware. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BootPartNumber { + project_number: ProjectNumber, + version: PartVersion, +} + +impl BootPartNumber { + /// The length (in bytes) of the [BootPartNumber]. + pub const LEN: usize = 9; + + /// Creates a new [BootPartNumber]. + pub const fn new(project_number: ProjectNumber, version: PartVersion) -> Self { + Self { + project_number, + version, + } + } + + pub const fn default() -> Self { + Self { + project_number: ProjectNumber::zero(), + version: PartVersion(0), + } + } +} + +impl From<&[u8]> for BootPartNumber { + fn from(b: &[u8]) -> Self { + if b.len() < Self::LEN { + Self::default() + } else { + let len = std::cmp::min(b.len(), Self::LEN); + + let project_number: ProjectNumber = b[..ProjectNumber::LEN].as_ref().into(); + let version: PartVersion = b[ProjectNumber::LEN..len].as_ref().into(); + + Self { + project_number, + version, + } + } + } +} + +impl fmt::Display for BootPartNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Project number: {}, Version: {}", + self.project_number, self.version + ) + } +} + +/// The application part number from the device firmware. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ApplicationPartNumber { + project_number: ProjectNumber, + version: PartVersion, +} + +impl ApplicationPartNumber { + /// The length (in bytes) of the [ApplicationPartNumber]. + pub const LEN: usize = 9; + + /// Creates a new [ApplicationPartNumber]. + pub const fn new(project_number: ProjectNumber, version: PartVersion) -> Self { + Self { + project_number, + version, + } + } + + pub const fn default() -> Self { + Self { + project_number: ProjectNumber::zero(), + version: PartVersion(0), + } + } +} + +impl From<&[u8]> for ApplicationPartNumber { + fn from(b: &[u8]) -> Self { + if b.len() < Self::LEN { + Self::default() + } else { + let len = std::cmp::min(b.len(), Self::LEN); + + let project_number: ProjectNumber = b[..ProjectNumber::LEN].as_ref().into(); + let version: PartVersion = b[ProjectNumber::LEN..len].as_ref().into(); + + Self { + project_number, + version, + } + } + } +} + +impl fmt::Display for ApplicationPartNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Project number: {}, Version: {}", + self.project_number, self.version + ) + } +} + +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. Please see the following table for expected values. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct VariantPartNumber { + project_number: ProjectNumber, + version: PartVersion, +} + +impl VariantPartNumber { + /// The length (in bytes) of the [VariantPartNumber]. + pub const LEN: usize = 9; + + /// Creates a new [VariantPartNumber]. + pub const fn new(project_number: ProjectNumber, version: PartVersion) -> Self { + Self { + project_number, + version, + } + } + + pub const fn default() -> Self { + Self { + project_number: ProjectNumber::zero(), + version: PartVersion(0), + } + } +} + +impl From<&[u8]> for VariantPartNumber { + fn from(b: &[u8]) -> Self { + if b.len() < Self::LEN { + Self::default() + } else { + let len = std::cmp::min(b.len(), Self::LEN); + + let project_number: ProjectNumber = b[..ProjectNumber::LEN].as_ref().into(); + let version: PartVersion = b[ProjectNumber::LEN..len].as_ref().into(); + + Self { + project_number, + version, + } + } + } +} + +impl fmt::Display for VariantPartNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Project number: {}, Version: {}", + self.project_number, self.version + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn boot_version_parsing() { + let version = PartVersion::from(b"123"); + let formatted_version = version.as_string(); + assert_eq!(formatted_version, "V1.23"); + + let version = PartVersion::from(b"23"); + let formatted_version = version.as_string(); + assert_eq!(formatted_version, "V0.23"); + + let version = PartVersion::from(b"3"); + let formatted_version = version.as_string(); + assert_eq!(formatted_version, "V0.03"); + + let version = PartVersion::from(b""); + let formatted_version = version.as_string(); + assert_eq!(formatted_version, "V0.00"); + + // Number is out of range + let version = PartVersion::from(b"999888777"); + let formatted_version = version.as_string(); + assert_eq!(formatted_version, "V0.00"); + } +} diff --git a/src/query_application_id.rs b/src/query_application_id.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_application_id.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_application_id/command.rs b/src/query_application_id/command.rs new file mode 100644 index 0000000..9ca96d9 --- /dev/null +++ b/src/query_application_id/command.rs @@ -0,0 +1,76 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::QUERY_APPLICATION_ID_COMMAND, AuxCommand, + AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Application ID - Command (Subtype 0x0E) +/// +/// This command is used to return the software part number of the actual application component of the +/// device firmware. +/// +/// This is only applicable when the device has been loaded with a combine file (a file that +/// contains both the application and the variant) because the (Subtype 0x07) Query Acceptor Application +/// Part Number command will return the part number of the combine file, not the underlying component’s +/// part number. +/// +/// The device capabilities map (section 7.4.14) has an entry as to whether or not the device +/// supports this command. +/// +/// The Query Application ID Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x0E | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryApplicationIdCommand { + buf: [u8; QUERY_APPLICATION_ID_COMMAND], +} + +impl QueryApplicationIdCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_APPLICATION_ID_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryApplicationId); + + message + } +} + +impl_default!(QueryApplicationIdCommand); +impl_message_ops!(QueryApplicationIdCommand); +impl_aux_ops!(QueryApplicationIdCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_application_id_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x0e, + // ETX | Checksum + 0x03, 0x66, + ]; + + let mut msg = QueryApplicationIdCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryApplicationId); + + Ok(()) + } +} diff --git a/src/query_application_id/reply.rs b/src/query_application_id/reply.rs new file mode 100644 index 0000000..4b36bbe --- /dev/null +++ b/src/query_application_id/reply.rs @@ -0,0 +1,153 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::QUERY_APPLICATION_ID_REPLY, + ApplicationPartNumber, MessageOps, MessageType, PartVersion, ProjectNumber, +}; + +mod index { + pub const PROJECT_NUM: usize = 3; + pub const VERSION: usize = 9; +} + +/// Query Application ID - Reply (Subtype 0x0E) +/// +/// This is the response to a query for the software part number of the application component of the device firmware. +/// +/// The data returned by the device takes the form of an ASCII string that is 9 bytes long. +/// +/// The Query Application ID Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | ... | Data 8 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:----:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | ... | 11 | 12 | 13 | +/// | Value | 0x02 | 0x0E | 0x6n | nn | nn | nn | nn | 0x03 | zz | +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// See [ProjectNumber](crate::ProjectNumber) for formatting details. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryApplicationIdReply { + buf: [u8; QUERY_APPLICATION_ID_REPLY], +} + +impl QueryApplicationIdReply { + /// Creates a new [QueryApplicationIdReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_APPLICATION_ID_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Gets the [ApplicationPartNumber]. + pub fn application_part_number(&self) -> ApplicationPartNumber { + self.buf[index::PROJECT_NUM..self.etx_index()] + .as_ref() + .into() + } + + /// Gets the [ProjectNumber] parsed from the raw byte buffer. + /// + /// On invalid ranges, returns a zeroed [ProjectNumber]. + pub fn project_number(&self) -> ProjectNumber { + self.buf[index::PROJECT_NUM..index::VERSION].as_ref().into() + } + + /// Gets the [PartVersion]. + pub fn version(&self) -> PartVersion { + self.buf[index::VERSION..self.etx_index()].as_ref().into() + } +} + +impl_default!(QueryApplicationIdReply); +impl_message_ops!(QueryApplicationIdReply); +impl_omnibus_nop_reply!(QueryApplicationIdReply); + +impl fmt::Display for QueryApplicationIdReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, ApplicationPartNumber: {}, ProjectNumber: {}, Version: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.application_part_number(), + self.project_number(), + self.version(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CheckDigit; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_application_id_reply_from_buf() -> Result<()> { + // Type 1 Application Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'0', b'0', b'0', + // Check Digit (in ASCII) + b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x54, + ]; + + let mut msg = QueryApplicationIdReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type1(28_000, CheckDigit::from(b'0')); + let exp_part_version = PartVersion::from(b"123"); + let exp_app_part_number = ApplicationPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.application_part_number(), exp_app_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + // Type 2 Application Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'6', b'0', b'0', b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type2(286_000); + let exp_part_version = PartVersion::from(b"123"); + let exp_app_part_number = ApplicationPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.application_part_number(), exp_app_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + Ok(()) + } +} diff --git a/src/query_application_part_number.rs b/src/query_application_part_number.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_application_part_number.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_application_part_number/command.rs b/src/query_application_part_number/command.rs new file mode 100644 index 0000000..3c497bc --- /dev/null +++ b/src/query_application_part_number/command.rs @@ -0,0 +1,68 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::QUERY_APPLICATION_PART_NUMBER_COMMAND, + AuxCommand, AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Application Part Number - Command (Subtype 0x07) +/// +/// This command is used to return the software part number of the boot component of the device +/// firmware. +/// +/// The Query Application Part Number Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x07 | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryApplicationPartNumberCommand { + buf: [u8; QUERY_APPLICATION_PART_NUMBER_COMMAND], +} + +impl QueryApplicationPartNumberCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_APPLICATION_PART_NUMBER_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryApplicationPartNumber); + + message + } +} + +impl_default!(QueryApplicationPartNumberCommand); +impl_message_ops!(QueryApplicationPartNumberCommand); +impl_aux_ops!(QueryApplicationPartNumberCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_application_part_number_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x07, + // ETX | Checksum + 0x03, 0x6f, + ]; + + let mut msg = QueryApplicationPartNumberCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryApplicationPartNumber); + + Ok(()) + } +} diff --git a/src/query_application_part_number/reply.rs b/src/query_application_part_number/reply.rs new file mode 100644 index 0000000..6ce1a14 --- /dev/null +++ b/src/query_application_part_number/reply.rs @@ -0,0 +1,154 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, + len::QUERY_APPLICATION_PART_NUMBER_REPLY, ApplicationPartNumber, MessageOps, MessageType, + PartVersion, ProjectNumber, +}; + +mod index { + pub const PROJECT_NUM: usize = 3; + pub const VERSION: usize = 9; +} + +/// Query Application Part Number - Reply (Subtype 0x07) +/// +/// This is the response to a query for the software part number of the boot component of the device firmware. +/// +/// The data returned by the device takes the form of an ASCII string that is 9 bytes long. +/// +/// The Query Application Part Number Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | ... | Data 8 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:----:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | ... | 11 | 12 | 13 | +/// | Value | 0x02 | 0x0E | 0x6n | nn | nn | nn | nn | 0x03 | zz | +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// See [ProjectNumber](crate::ProjectNumber) for formatting details. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryApplicationPartNumberReply { + buf: [u8; QUERY_APPLICATION_PART_NUMBER_REPLY], +} + +impl QueryApplicationPartNumberReply { + /// Creates a new [QueryApplicationPartNumberReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_APPLICATION_PART_NUMBER_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Gets the [ApplicationPartNumber]. + pub fn application_part_number(&self) -> ApplicationPartNumber { + self.buf[index::PROJECT_NUM..self.etx_index()] + .as_ref() + .into() + } + + /// Gets the [ProjectNumber] parsed from the raw byte buffer. + /// + /// On invalid ranges, returns a zeroed [ProjectNumber]. + pub fn project_number(&self) -> ProjectNumber { + self.buf[index::PROJECT_NUM..index::VERSION].as_ref().into() + } + + /// Gets the [PartVersion]. + pub fn version(&self) -> PartVersion { + self.buf[index::VERSION..self.etx_index()].as_ref().into() + } +} + +impl_default!(QueryApplicationPartNumberReply); +impl_message_ops!(QueryApplicationPartNumberReply); +impl_omnibus_nop_reply!(QueryApplicationPartNumberReply); + +impl fmt::Display for QueryApplicationPartNumberReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, ApplicationPartNumber: {}, ProjectNumber: {}, Version: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.application_part_number(), + self.project_number(), + self.version(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CheckDigit; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_application_part_number_reply_from_buf() -> Result<()> { + // Type 1 Application Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'0', b'0', b'0', + // Check Digit (in ASCII) + b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x54, + ]; + + let mut msg = QueryApplicationPartNumberReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type1(28_000, CheckDigit::from(b'0')); + let exp_part_version = PartVersion::from(b"123"); + let exp_app_part_number = ApplicationPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.application_part_number(), exp_app_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + // Type 2 Application Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'6', b'0', b'0', b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type2(286_000); + let exp_part_version = PartVersion::from(b"123"); + let exp_app_part_number = ApplicationPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.application_part_number(), exp_app_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + Ok(()) + } +} diff --git a/src/query_boot_part_number.rs b/src/query_boot_part_number.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_boot_part_number.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_boot_part_number/command.rs b/src/query_boot_part_number/command.rs new file mode 100644 index 0000000..b52f9c2 --- /dev/null +++ b/src/query_boot_part_number/command.rs @@ -0,0 +1,68 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::QUERY_BOOT_PART_NUMBER_COMMAND, AuxCommand, + AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Boot Part Number - Command (Subtype 0x06) +/// +/// This command is used to return the software part number of the boot component of the device +/// firmware. +/// +/// The Query Boot Part Number Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x06 | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryBootPartNumberCommand { + buf: [u8; QUERY_BOOT_PART_NUMBER_COMMAND], +} + +impl QueryBootPartNumberCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_BOOT_PART_NUMBER_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryBootPartNumber); + + message + } +} + +impl_default!(QueryBootPartNumberCommand); +impl_message_ops!(QueryBootPartNumberCommand); +impl_aux_ops!(QueryBootPartNumberCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_boot_part_number_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x06, + // ETX | Checksum + 0x03, 0x6e, + ]; + + let mut msg = QueryBootPartNumberCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryBootPartNumber); + + Ok(()) + } +} diff --git a/src/query_boot_part_number/reply.rs b/src/query_boot_part_number/reply.rs new file mode 100644 index 0000000..e0ddbde --- /dev/null +++ b/src/query_boot_part_number/reply.rs @@ -0,0 +1,153 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::QUERY_BOOT_PART_NUMBER_REPLY, + BootPartNumber, MessageOps, MessageType, PartVersion, ProjectNumber, +}; + +mod index { + pub const PROJECT_NUM: usize = 3; + pub const VERSION: usize = 9; +} + +/// Query Boot Part Number - Reply (Subtype 0x06) +/// +/// This is the response to a query for the software part number of the boot component of the device firmware. +/// +/// The data returned by the device takes the form of an ASCII string that is 9 bytes long. +/// +/// The Query Boot Part Number Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | ... | Data 8 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:----:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | ... | 11 | 12 | 13 | +/// | Value | 0x02 | 0x0E | 0x6n | nn | nn | nn | nn | 0x03 | zz | +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// See [ProjectNumber](crate::ProjectNumber) for formatting details. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryBootPartNumberReply { + buf: [u8; QUERY_BOOT_PART_NUMBER_REPLY], +} + +impl QueryBootPartNumberReply { + /// Creates a new [QueryBootPartNumberReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_BOOT_PART_NUMBER_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Gets the [BootPartNumber]. + pub fn boot_part_number(&self) -> BootPartNumber { + self.buf[index::PROJECT_NUM..self.etx_index()] + .as_ref() + .into() + } + + /// Gets the [ProjectNumber] parsed from the raw byte buffer. + /// + /// On invalid ranges, returns a zeroed [ProjectNumber]. + pub fn project_number(&self) -> ProjectNumber { + self.buf[index::PROJECT_NUM..index::VERSION].as_ref().into() + } + + /// Gets the [PartVersion]. + pub fn version(&self) -> PartVersion { + self.buf[index::VERSION..self.etx_index()].as_ref().into() + } +} + +impl_default!(QueryBootPartNumberReply); +impl_message_ops!(QueryBootPartNumberReply); +impl_omnibus_nop_reply!(QueryBootPartNumberReply); + +impl fmt::Display for QueryBootPartNumberReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, BootPartNumber: {}, ProjectNumber: {}, Version: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.boot_part_number(), + self.project_number(), + self.version(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CheckDigit; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_boot_part_number_reply_from_buf() -> Result<()> { + // Type 1 Boot Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'0', b'0', b'0', + // Check Digit (in ASCII) + b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x54, + ]; + + let mut msg = QueryBootPartNumberReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type1(28_000, CheckDigit::from(b'0')); + let exp_part_version = PartVersion::from(b"123"); + let exp_boot_part_number = BootPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.boot_part_number(), exp_boot_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + // Type 2 Boot Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'6', b'0', b'0', b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type2(286_000); + let exp_part_version = PartVersion::from(b"123"); + let exp_boot_part_number = BootPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.boot_part_number(), exp_boot_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + Ok(()) + } +} diff --git a/src/query_device_capabilities.rs b/src/query_device_capabilities.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_device_capabilities.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_device_capabilities/command.rs b/src/query_device_capabilities/command.rs new file mode 100644 index 0000000..225bced --- /dev/null +++ b/src/query_device_capabilities/command.rs @@ -0,0 +1,87 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, impl_omnibus_command_ops, + len::QUERY_DEVICE_CAPABILITIES_COMMAND, AuxCommand, AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Device Capabilities - Command (Subtype 0x0D) +/// +/// This command is used to query the device capabilities. In general, this command should only be sent to +/// devices that have indicated support by setting the DeviceCaps bit in a standard poll reply (see section +/// 7.1.2.4.) +/// +/// The Query Device Capabilities Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x0D | 0x03 | zz | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryDeviceCapabilitiesCommand { + buf: [u8; QUERY_DEVICE_CAPABILITIES_COMMAND], +} + +impl QueryDeviceCapabilitiesCommand { + /// Create a new [QueryDeviceCapabilitiesCommand] message + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_DEVICE_CAPABILITIES_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryDeviceCapabilities); + + message + } +} + +impl_default!(QueryDeviceCapabilitiesCommand); +impl_message_ops!(QueryDeviceCapabilitiesCommand); +impl_aux_ops!(QueryDeviceCapabilitiesCommand); +impl_omnibus_command_ops!(QueryDeviceCapabilitiesCommand); + +impl fmt::Display for QueryDeviceCapabilitiesCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Command: {}, Checksum: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.aux_command(), + self.checksum(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_device_capabilities_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x0d, + // ETX | Checksum + 0x03, 0x65, + ]; + + let mut msg = QueryDeviceCapabilitiesCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryDeviceCapabilities); + + Ok(()) + } +} diff --git a/src/query_device_capabilities/reply.rs b/src/query_device_capabilities/reply.rs new file mode 100644 index 0000000..e6bca47 --- /dev/null +++ b/src/query_device_capabilities/reply.rs @@ -0,0 +1,416 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::QUERY_DEVICE_CAPABILITIES_REPLY, + MessageOps, MessageType, CLOSE_BRACE, OPEN_BRACE, +}; + +mod index { + pub const CAP0: usize = 3; + pub const CAP1: usize = 4; + pub const CAP2: usize = 5; + pub const CAP3: usize = 6; + pub const CAP4: usize = 7; + pub const CAP5: usize = 8; +} + +bitfield! { + /// First set of device capabilties + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Cap0(u8); + u8; + /// **OBSOLETE** Extended PUP mode is supported. + pub extended_pup_mode, _: 0; + /// Extended orientation handling is supported. + pub extended_orientation, _: 1; + /// [QueryApplicationId](crate::QueryApplicationIdCommand) and [QueryVariantId](crate::QueryVariantIdCommand) are supported + pub application_and_variant_id, _: 2; + /// QueryBNFStatus is supported. + pub bnf_status, _: 3; + /// Test documents are supported. + pub test_documents, _: 4; + /// Set Bezel is supported + pub bezel, _: 5; + /// Easitrax is supported (with Query Asset Number). + pub easitrax, _: 6; +} + +impl fmt::Display for Cap0 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"extended_pup_mode\":{},\"extended_orientation\":{},\"application_and_variant_id\":{},\"bnf_status\":{},\"test_documents\":{},\"bezel\":{},\"easitrax\":{}{CLOSE_BRACE}", + self.extended_pup_mode(), + self.extended_orientation(), + self.application_and_variant_id(), + self.bnf_status(), + self.test_documents(), + self.bezel(), + self.easitrax(), + ) + } +} + +impl From for Cap0 { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From<&Cap0> for u8 { + fn from(c: &Cap0) -> Self { + c.0 + } +} + +impl From for u8 { + fn from(c: Cap0) -> Self { + (&c).into() + } +} + +bitfield! { + /// Second set of device capabilties + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Cap1(u8); + u8; + /// Note Retrieved is supported. + pub note_retrieved, _: 0; + /// Advanced Bookmark mode is supported. (see 7.5.9) + pub advanced_bookmark, _: 1; + /// Device capable of ABDS download. + pub abds_download, _: 2; + /// Device supports Clear Audit Command. (see 7.5.23) + pub clear_audit, _: 3; + /// Multi-note escrow is supported. + pub multi_note_escrow, _: 4; + /// 32-bit Unix timestamp is supported. + pub unix_timestamp_32bit, _: 5; +} + +impl fmt::Display for Cap1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"note_retrieved\":{},\"advanced_bookmark\":{},\"abds_download\":{},\"clear_audit\":{},\"multi_note_escrow\":{},\"unix_timestamp_32bit\":{}{CLOSE_BRACE}", + self.note_retrieved(), + self.advanced_bookmark(), + self.abds_download(), + self.clear_audit(), + self.multi_note_escrow(), + self.unix_timestamp_32bit(), + ) + } +} + +impl From for Cap1 { + fn from(b: u8) -> Self { + Self(b & 0b11_1111) + } +} + +impl From<&Cap1> for u8 { + fn from(c: &Cap1) -> Self { + c.0 + } +} + +impl From for u8 { + fn from(c: Cap1) -> Self { + (&c).into() + } +} + +bitfield! { + /// Third set of device capabilties + /// + /// Note: **SCR Classification** If banknote classification is supported (i.e. Cap Byte 3, bit 1 is set), all denomination + /// recycling bits will be set to 0. + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Cap2(u8); + u8; + /// 1 Denomination recycling is supported. + pub one_denom_recycling, _: 0; + /// 2 Denomination recycling is supported. + pub two_denom_recycling, _: 1; + /// 3 Denomination recycling is supported. + pub three_denom_recycling, _: 2; + /// 4 Denomination recycling is supported. + pub four_denom_recycling, _: 3; + /// **Retail Only** Improperly Seated Head Detection is supported. + pub improperly_seated_head_detection, _: 4; + /// **SCR** Host Controlled Recycler Inventory (Mixed Denomintion Recycling) is supported. + pub mixed_denom_recycling, _: 6; +} + +impl fmt::Display for Cap2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"one_denom_recycling\":{},\"two_denom_recycling\":{},\"three_denom_recycling\":{},\"four_denom_recycling\":{},\"improperly_seated_head_detection\":{},\"mixed_denom_recycling\":{}{CLOSE_BRACE}", + self.one_denom_recycling(), + self.two_denom_recycling(), + self.three_denom_recycling(), + self.four_denom_recycling(), + self.improperly_seated_head_detection(), + self.mixed_denom_recycling(), + ) + } +} + +impl From for Cap2 { + fn from(b: u8) -> Self { + Self(b & 0b101_1111) + } +} + +impl From<&Cap2> for u8 { + fn from(c: &Cap2) -> Self { + c.0 + } +} + +impl From for u8 { + fn from(c: Cap2) -> Self { + (&c).into() + } +} + +bitfield! { + /// Fourth set of device capabilties + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Cap3(u8); + u8; + /// Customer Configuration Options Set/Query (Msg 6 Subtypes 0x25 and 0x26) are supported. + pub customer_config, _: 0; + /// **SCR Classification** Banknote classification is supported. + pub banknote_classification, _: 1; +} + +impl fmt::Display for Cap3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"customer_config\":{},\"banknote_classification\":{}{CLOSE_BRACE}", + self.customer_config(), + self.banknote_classification(), + ) + } +} + +impl From for Cap3 { + fn from(b: u8) -> Self { + Self(b & 0x000_0011) + } +} + +impl From<&Cap3> for u8 { + fn from(c: &Cap3) -> Self { + c.0 + } +} + +impl From for u8 { + fn from(c: Cap3) -> Self { + (&c).into() + } +} + +bitfield! { + /// Fifth set of device capabilties + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Cap4(u8); + u8; + /// RFU + pub reserved, _: 6, 0; +} + +impl fmt::Display for Cap4 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"reserved\": 0b{:08b}{CLOSE_BRACE}", + self.reserved(), + ) + } +} + +impl From for Cap4 { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From<&Cap4> for u8 { + fn from(c: &Cap4) -> Self { + c.0 + } +} + +impl From for u8 { + fn from(c: Cap4) -> Self { + (&c).into() + } +} + +bitfield! { + /// Sixth set of device capabilties + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct Cap5(u8); + u8; + /// RFU + pub reserved, _: 6, 0; +} + +impl fmt::Display for Cap5 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"reserved\": 0b{:08b}{CLOSE_BRACE}", + self.reserved() + ) + } +} + +impl From for Cap5 { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From<&Cap5> for u8 { + fn from(c: &Cap5) -> Self { + c.0 + } +} + +impl From for u8 { + fn from(c: Cap5) -> Self { + (&c).into() + } +} + +/// Query Device Capabilities - Reply (Subtype 0x0D) +/// +/// This the reply for the query of device capabilities: +/// [QueryDeviceCapabilitiesCommand](crate::QueryDeviceCapabilitiesCommand). +/// +/// The Query Device Capabilities Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Cap 0 | Cap 1 | Cap 2 | Cap 3 | Cap 4 | Cap 5 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +/// | Value | 0x02 | 0x0B | 0x6n | nn | nn | nn | nn | nn | nn | 0x03 | zz | +/// +/// The `Cap` fields are bitfields describing device capabilities: +/// +/// * [Cap0] +/// * [Cap1] +/// * [Cap2] +/// * [Cap3] +/// * [Cap4] - RFU +/// * [Cap5] - RFU +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryDeviceCapabilitiesReply { + buf: [u8; QUERY_DEVICE_CAPABILITIES_REPLY], +} + +impl QueryDeviceCapabilitiesReply { + /// Create a new [QueryDeviceCapabilitiesReply] message + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_DEVICE_CAPABILITIES_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Get the first set of capabilities + pub fn cap0(&self) -> Cap0 { + self.buf[index::CAP0].into() + } + + /// Get the second set of capabilities + pub fn cap1(&self) -> Cap1 { + self.buf[index::CAP1].into() + } + + /// Get the third set of capabilities + pub fn cap2(&self) -> Cap2 { + self.buf[index::CAP2].into() + } + + /// Get the fourth set of capabilities + pub fn cap3(&self) -> Cap3 { + self.buf[index::CAP3].into() + } + + /// Get the fifth set of capabilities + pub fn cap4(&self) -> Cap4 { + self.buf[index::CAP4].into() + } + + /// Get the sixth set of capabilities + pub fn cap5(&self) -> Cap5 { + self.buf[index::CAP5].into() + } +} + +impl_default!(QueryDeviceCapabilitiesReply); +impl_message_ops!(QueryDeviceCapabilitiesReply); +impl_omnibus_nop_reply!(QueryDeviceCapabilitiesReply); + +impl fmt::Display for QueryDeviceCapabilitiesReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Capability set 0: {}, Capability set 1: {}, Capability set 2: {}, Capability set 3: {}, Capability set 4: {}, Capability set 5: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.cap0(), + self.cap1(), + self.cap2(), + self.cap3(), + self.cap4(), + self.cap5(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_device_capabilities_reply_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0b, 0x60, + // Capabilities + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x6b, + ]; + + let mut msg = QueryDeviceCapabilitiesReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + assert_eq!(msg.cap0(), Cap0::from(0)); + assert_eq!(msg.cap1(), Cap1::from(0)); + assert_eq!(msg.cap2(), Cap2::from(0)); + assert_eq!(msg.cap3(), Cap3::from(0)); + assert_eq!(msg.cap4(), Cap4::from(0)); + assert_eq!(msg.cap5(), Cap5::from(0)); + + Ok(()) + } +} diff --git a/src/query_software_crc.rs b/src/query_software_crc.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_software_crc.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_software_crc/command.rs b/src/query_software_crc/command.rs new file mode 100644 index 0000000..cdb01ac --- /dev/null +++ b/src/query_software_crc/command.rs @@ -0,0 +1,41 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::QUERY_SOFTWARE_CRC_COMMAND, AuxCommand, + AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Software CRC - Command (Subtype 0x00) +/// +/// | **S2K** | **CFSC** | **SC Adv** | **SCR** | +/// |:-------:|:--------:|:----------:|:-------:| +/// +/// This command is used to query the device for the 16 bit CRC of the flash contents. +/// +/// The Query Software CRC Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x00 | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QuerySoftwareCrcCommand { + buf: [u8; QUERY_SOFTWARE_CRC_COMMAND], +} + +impl QuerySoftwareCrcCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_SOFTWARE_CRC_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QuerySoftwareCrc); + + message + } +} + +impl_default!(QuerySoftwareCrcCommand); +impl_message_ops!(QuerySoftwareCrcCommand); +impl_aux_ops!(QuerySoftwareCrcCommand); diff --git a/src/query_software_crc/reply.rs b/src/query_software_crc/reply.rs new file mode 100644 index 0000000..3f2ec17 --- /dev/null +++ b/src/query_software_crc/reply.rs @@ -0,0 +1,95 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, impl_omnibus_nop_reply, + len::QUERY_SOFTWARE_CRC_REPLY, seven_bit_u16, u16_seven_bit, AuxCommand, AuxCommandOps, + MessageOps, MessageType, +}; + +mod index { + pub const CRC_BEGIN: usize = 3; + pub const CRC_END: usize = 7; +} + +/// Query Software CRC - Reply (Subtype 0x00) +/// +/// | **S2K** | **CFSC** | **SC Adv** | **SCR** | +/// |:-------:|:--------:|:----------:|:-------:| +/// +/// The Query Software CRC Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | CRC 1 | CRC 2 | CRC 3 | CRC 4 | N/A | N/A | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-----:|:-----:|:-----:|:-----:|:----:|:----:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x0n | 0x0n | 0x0n | 0x0n | 0x00 | 0x00 | 0x03 | zz | +/// +/// The 16 bit CRC data is sent in bytes 3 through 6, four bits at a time. +/// +/// This may be extracted as shown below: +/// +/// Example: +/// +/// ```rust +/// # use ebds::MessageOps; +/// let mut reply_crc = ebds::QuerySoftwareCrcReply::new(); +/// +/// let exp_crc = 0x1234; +/// reply_crc.set_crc(exp_crc); +/// +/// let crc_bytes = reply_crc.buf(); +/// +/// let hi = ((crc_bytes[3] & 0xf) << 4) | (crc_bytes[4] & 0xf); +/// let lo = ((crc_bytes[5] & 0xf) << 4) | (crc_bytes[6] & 0xf); +/// +/// let crc = u16::from_be_bytes([hi, lo]); +/// +/// assert_eq!(exp_crc, crc); +/// ``` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QuerySoftwareCrcReply { + buf: [u8; QUERY_SOFTWARE_CRC_REPLY], +} + +impl QuerySoftwareCrcReply { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_SOFTWARE_CRC_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QuerySoftwareCrc); + + message + } + + /// Gets the CRC-16 from the [QuerySoftwareCrcReply]. + pub fn crc(&self) -> u16 { + seven_bit_u16(self.buf[index::CRC_BEGIN..index::CRC_END].as_ref()) + } + + /// Sets the CRC-16 from the [QuerySoftwareCrcReply]. + pub fn set_crc(&mut self, crc: u16) { + self.buf[index::CRC_BEGIN..index::CRC_END].copy_from_slice(u16_seven_bit(crc).as_ref()); + } +} + +impl_default!(QuerySoftwareCrcReply); +impl_message_ops!(QuerySoftwareCrcReply); +impl_omnibus_nop_reply!(QuerySoftwareCrcReply); +impl_aux_ops!(QuerySoftwareCrcReply); + +impl fmt::Display for QuerySoftwareCrcReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, CRC: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.crc(), + ) + } +} diff --git a/src/query_value_table.rs b/src/query_value_table.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_value_table.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_value_table/command.rs b/src/query_value_table/command.rs new file mode 100644 index 0000000..2c92a46 --- /dev/null +++ b/src/query_value_table/command.rs @@ -0,0 +1,70 @@ +use crate::{ + impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::QUERY_VALUE_TABLE_COMMAND, ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, +}; + +/// Query Value Table - Command (Subtype 0x06) +/// +/// This command sends a request to the device for the entire note table. The device will respond with a +/// message containing all known denominations. The purpose of this message is to allow the host to know +/// the exact denomination of a note that is usually only reported as Note Index Value “X”. +/// +/// **WARNING** This message is only compatible with variants of a single currency and running in Non-Extended Mode (4.2.1). +/// +/// The Query Value Table Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +/// | Value | 0x02 | 0x09 | 0x7n | 0x06 | nn | nn | nn | 0x03 | zz | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryValueTableCommand { + buf: [u8; QUERY_VALUE_TABLE_COMMAND], +} + +impl QueryValueTableCommand { + /// Creates a new [QueryValueTableCommand]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VALUE_TABLE_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::QueryValueTable); + + message + } +} + +impl_default!(QueryValueTableCommand); +impl_message_ops!(QueryValueTableCommand); +impl_omnibus_extended_command!(QueryValueTableCommand); +impl_extended_ops!(QueryValueTableCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_value_table_command_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x09, 0x70, 0x06, + // Data + 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x7f, + ]; + + let mut msg = QueryValueTableCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::QueryValueTable); + + Ok(()) + } +} diff --git a/src/query_value_table/reply.rs b/src/query_value_table/reply.rs new file mode 100644 index 0000000..b7d1ef7 --- /dev/null +++ b/src/query_value_table/reply.rs @@ -0,0 +1,379 @@ +use crate::std; +use std::fmt; + +use crate::{ + banknote::*, impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply, + len::QUERY_VALUE_TABLE_REPLY, ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, + OmnibusReplyOps, +}; + +/// Represents a denomination in non-extended mode. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BaseDenomination { + /// Note value index reported starting with '1' and ending with '7'. + note_index: usize, + /// A three character ASCII currency code. + iso_code: ISOCode, + /// A three character ASCII decimal value. + base_value: BaseValue, + /// An ASCII coded sign (+/-) to be used with the exponent. + sign: Sign, + /// ASCII coded decimal value for the power of ten that the base is multiplied/divided. + exponent: Exponent, +} + +impl BaseDenomination { + pub const LEN: usize = 10; + + /// Gets the index of the [BaseDenomination]. + pub fn note_index(&self) -> usize { + self.note_index + } + + /// Gets the ISO code of the [BaseDenomination]. + pub fn iso_code(&self) -> ISOCode { + self.iso_code + } + + /// Gets the base value of the [BaseDenomination]. + pub fn base_value(&self) -> BaseValue { + self.base_value + } + + /// Gets the sign of the [BaseDenomination]. + pub fn sign(&self) -> Sign { + self.sign + } + + /// Gets the exponent of the [BaseDenomination]. + pub fn exponent(&self) -> Exponent { + self.exponent + } + + /// Gets the full value of the [BaseDenomination]. + /// + /// Convenience function for `base_value * 10^([+-]exponent)`. + pub fn value(&self) -> f32 { + let base_value: f32 = self.base_value.into(); + let exponent: f32 = self.exponent.into(); + + match self.sign { + Sign::Positive => base_value * 10f32.powf(exponent), + Sign::Negative => base_value * 10f32.powf(-exponent), + } + } +} + +impl From<&[u8]> for BaseDenomination { + fn from(b: &[u8]) -> Self { + Self { + note_index: b[index::DENOM_INDEX] as usize, + iso_code: b[index::DENOM_ISO..index::DENOM_ISO_END].as_ref().into(), + base_value: b[index::DENOM_BASE_VALUE..index::DENOM_BASE_VALUE_END] + .as_ref() + .into(), + sign: b[index::DENOM_SIGN].into(), + exponent: b[index::DENOM_EXPONENT..index::DENOM_EXPONENT_END] + .as_ref() + .into(), + } + } +} + +impl From<[u8; N]> for BaseDenomination { + fn from(b: [u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&[u8; N]> for BaseDenomination { + fn from(b: &[u8; N]) -> Self { + b.as_ref().into() + } +} + +impl From<&BaseDenomination> for Banknote { + fn from(b: &BaseDenomination) -> Self { + Self::new( + b.value(), + b.iso_code(), + NoteType::default(), + NoteSeries::default(), + NoteCompatibility::default(), + NoteVersion::default(), + BanknoteClassification::Genuine, + ) + } +} + +impl From for Banknote { + fn from(b: BaseDenomination) -> Banknote { + (&b).into() + } +} + +impl From<&BaseDenomination> for NoteTableItem { + fn from(b: &BaseDenomination) -> Self { + NoteTableItem::new(b.note_index(), b.into()) + } +} + +impl From for NoteTableItem { + fn from(b: BaseDenomination) -> Self { + (&b).into() + } +} + +impl fmt::Display for BaseDenomination { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let open_brace = "{"; + let note_index = self.note_index; + let iso_code = self.iso_code; + let base_value: u16 = self.base_value.into(); + let sign: &str = self.sign.into(); + let exponent: u8 = self.exponent.into(); + let close_brace = "}"; + write!(f, + "{open_brace}\"note_index\":{note_index}, \"iso_code\":{iso_code}, \"base_value\":{base_value}, \"sign\":{sign}, \"exponent\":{exponent}{close_brace}") + } +} + +mod index { + use crate::{BaseDenomination, BaseValue, Exponent, ISOCode}; + + pub const DENOM: usize = 10; + pub const DENOM0: usize = DENOM; + pub const DENOM1: usize = DENOM0 + BaseDenomination::LEN; + pub const DENOM2: usize = DENOM1 + BaseDenomination::LEN; + pub const DENOM3: usize = DENOM2 + BaseDenomination::LEN; + pub const DENOM4: usize = DENOM3 + BaseDenomination::LEN; + pub const DENOM5: usize = DENOM4 + BaseDenomination::LEN; + pub const DENOM6: usize = DENOM5 + BaseDenomination::LEN; + pub const DENOM6_END: usize = DENOM6 + BaseDenomination::LEN; + + pub const DENOM_INDEX: usize = 0; + pub const DENOM_ISO: usize = 1; + pub const DENOM_ISO_END: usize = DENOM_ISO + ISOCode::LEN; + pub const DENOM_BASE_VALUE: usize = 4; + pub const DENOM_BASE_VALUE_END: usize = DENOM_BASE_VALUE + BaseValue::LEN; + pub const DENOM_SIGN: usize = 7; + pub const DENOM_EXPONENT: usize = 8; + pub const DENOM_EXPONENT_END: usize = DENOM_EXPONENT + Exponent::LEN; +} + +/// Query Value Table - Reply (Subtype 0x06) +/// +/// This message is a reply to [QueryValueTableCommand](crate::QueryValueTableCommand), +/// and contains the note table in the extended data. +/// +/// The Query Value Table Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | +/// | Value | 0x02 | 0x0C | 0x7n | 0x06 | nn | nn | nn | nn | nn | nn | 0x03 | zz | +/// +/// +/// | Field | Byte Offset | Field Description | Sample Value
(2000 Yen) | +/// |:-----------|:-----------:|:----------------------------------------------------------------------------------------------:|:--------------------------:| +/// | Index | 0 | Note Value Index reported starting with ‘1’ and ending with the final index ‘7’. | 0x03 | +/// | ISO Code | 1..3 | A three character ASCII currency code. See ISO_4217 at the Wikipedia for details. | “JPY” | +/// | Base Value | 4..6 | A three character ASCII coded decimal value. | “002” | +/// | Sign | 7 | An ASCII coded sign value for the Exponent.
This field is either a “+” or a “-“. | “+” | +/// | Exponent | 8..9 | ASCII coded decimal value for the power of ten that the base is (multiplied “+”, divided “-“). | "03" | +/// +/// In this example: Note Value = `002 x 10^03 = 2 x 1000 = ¥2000`. +/// +/// If a Value Index does not have a corresponding denomination value, then all fields will be `0x00` following +/// the Value Index. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryValueTableReply { + buf: [u8; QUERY_VALUE_TABLE_REPLY], +} + +impl QueryValueTableReply { + /// Creates a new [QueryValueTableReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VALUE_TABLE_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::QueryValueTable); + + message + } + + /// Get the [BaseDenomination] 0. + pub fn denom0(&self) -> BaseDenomination { + self.buf[index::DENOM0..index::DENOM1].as_ref().into() + } + + /// Get the [BaseDenomination] 1. + pub fn denom1(&self) -> BaseDenomination { + self.buf[index::DENOM1..index::DENOM2].as_ref().into() + } + + /// Get the [BaseDenomination] 2. + pub fn denom2(&self) -> BaseDenomination { + self.buf[index::DENOM2..index::DENOM3].as_ref().into() + } + + /// Get the [BaseDenomination] 3. + pub fn denom3(&self) -> BaseDenomination { + self.buf[index::DENOM3..index::DENOM4].as_ref().into() + } + + /// Get the [BaseDenomination] 4. + pub fn denom4(&self) -> BaseDenomination { + self.buf[index::DENOM4..index::DENOM5].as_ref().into() + } + + /// Get the [BaseDenomination] 5. + pub fn denom5(&self) -> BaseDenomination { + self.buf[index::DENOM5..index::DENOM6].as_ref().into() + } + + /// Get the [BaseDenomination] 6. + pub fn denom6(&self) -> BaseDenomination { + self.buf[index::DENOM6..index::DENOM6_END].as_ref().into() + } +} + +impl_default!(QueryValueTableReply); +impl_message_ops!(QueryValueTableReply); +impl_omnibus_extended_reply!(QueryValueTableReply); +impl_extended_ops!(QueryValueTableReply); + +impl fmt::Display for QueryValueTableReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Subtype: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, Denom0: {}, Denom1: {}, Denom2: {}, Denom3: {}, Denom4: {}, Denom5: {}, Denom6: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.extended_command(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + self.denom0(), + self.denom1(), + self.denom2(), + self.denom3(), + self.denom4(), + self.denom5(), + self.denom6(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_value_table_reply_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x52, 0x70, 0x06, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Denom 0 + 0x01, b'U', b'S', b'D', b'0', b'0', b'1', b'+', b'0', b'0', + // Denom 1 + 0x02, b'U', b'S', b'D', b'0', b'0', b'2', b'+', b'0', b'0', + // Denom 2 + 0x03, b'U', b'S', b'D', b'0', b'0', b'5', b'+', b'0', b'0', + // Denom 3 + 0x04, b'U', b'S', b'D', b'0', b'0', b'1', b'+', b'0', b'1', + // Denom 4 + 0x05, b'U', b'S', b'D', b'0', b'0', b'2', b'+', b'0', b'1', + // Denom 5 + 0x06, b'U', b'S', b'D', b'0', b'0', b'5', b'+', b'0', b'1', + // Denom 6 + 0x07, b'U', b'S', b'D', b'0', b'0', b'1', b'+', b'0', b'2', + // ETX | Checksum + 0x03, 0x7f, + ]; + + let mut msg = QueryValueTableReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::QueryValueTable); + + let exp_denom0 = BaseDenomination::from([0x01, b'U', b'S', b'D', b'0', b'0', b'1', b'+', b'0', b'0']); + let exp_denom1 = BaseDenomination::from([0x02, b'U', b'S', b'D', b'0', b'0', b'2', b'+', b'0', b'0']); + let exp_denom2 = BaseDenomination::from([0x03, b'U', b'S', b'D', b'0', b'0', b'5', b'+', b'0', b'0']); + let exp_denom3 = BaseDenomination::from([0x04, b'U', b'S', b'D', b'0', b'0', b'1', b'+', b'0', b'1']); + let exp_denom4 = BaseDenomination::from([0x05, b'U', b'S', b'D', b'0', b'0', b'2', b'+', b'0', b'1']); + let exp_denom5 = BaseDenomination::from([0x06, b'U', b'S', b'D', b'0', b'0', b'5', b'+', b'0', b'1']); + let exp_denom6 = BaseDenomination::from([0x07, b'U', b'S', b'D', b'0', b'0', b'1', b'+', b'0', b'2']); + + assert_eq!(msg.denom0(), exp_denom0); + assert_eq!(msg.denom0().note_index(), 1); + assert_eq!(msg.denom0().iso_code(), ISOCode::USD); + assert_eq!(msg.denom0().base_value(), BaseValue::from(b"001")); + assert_eq!(msg.denom0().sign(), Sign::Positive); + assert_eq!(msg.denom0().exponent(), Exponent::from(b"00")); + assert_eq!(Banknote::from(msg.denom0()).value(), 1.0); + + assert_eq!(msg.denom1(), exp_denom1); + assert_eq!(msg.denom1().note_index(), 2); + assert_eq!(msg.denom1().iso_code(), ISOCode::USD); + assert_eq!(msg.denom1().base_value(), BaseValue::from(b"002")); + assert_eq!(msg.denom1().sign(), Sign::Positive); + assert_eq!(msg.denom1().exponent(), Exponent::from(b"00")); + assert_eq!(Banknote::from(msg.denom1()).value(), 2.0); + + assert_eq!(msg.denom2(), exp_denom2); + assert_eq!(msg.denom2().note_index(), 3); + assert_eq!(msg.denom2().iso_code(), ISOCode::USD); + assert_eq!(msg.denom2().base_value(), BaseValue::from(b"005")); + assert_eq!(msg.denom2().sign(), Sign::Positive); + assert_eq!(msg.denom2().exponent(), Exponent::from(b"00")); + assert_eq!(Banknote::from(msg.denom2()).value(), 5.0); + + assert_eq!(msg.denom3(), exp_denom3); + assert_eq!(msg.denom3().note_index(), 4); + assert_eq!(msg.denom3().iso_code(), ISOCode::USD); + assert_eq!(msg.denom3().base_value(), BaseValue::from(b"001")); + assert_eq!(msg.denom3().sign(), Sign::Positive); + assert_eq!(msg.denom3().exponent(), Exponent::from(b"01")); + assert_eq!(Banknote::from(msg.denom3()).value(), 10.0); + + assert_eq!(msg.denom4(), exp_denom4); + assert_eq!(msg.denom4().note_index(), 5); + assert_eq!(msg.denom4().iso_code(), ISOCode::USD); + assert_eq!(msg.denom4().base_value(), BaseValue::from(b"002")); + assert_eq!(msg.denom4().sign(), Sign::Positive); + assert_eq!(msg.denom4().exponent(), Exponent::from(b"01")); + assert_eq!(Banknote::from(msg.denom4()).value(), 20.0); + + assert_eq!(msg.denom5(), exp_denom5); + assert_eq!(msg.denom5().note_index(), 6); + assert_eq!(msg.denom5().iso_code(), ISOCode::USD); + assert_eq!(msg.denom5().base_value(), BaseValue::from(b"005")); + assert_eq!(msg.denom5().sign(), Sign::Positive); + assert_eq!(msg.denom5().exponent(), Exponent::from(b"01")); + assert_eq!(Banknote::from(msg.denom5()).value(), 50.0); + + assert_eq!(msg.denom6(), exp_denom6); + assert_eq!(msg.denom6().note_index(), 7); + assert_eq!(msg.denom6().iso_code(), ISOCode::USD); + assert_eq!(msg.denom6().base_value(), BaseValue::from(b"001")); + assert_eq!(msg.denom6().sign(), Sign::Positive); + assert_eq!(msg.denom6().exponent(), Exponent::from(b"02")); + assert_eq!(Banknote::from(msg.denom6()).value(), 100.0); + + Ok(()) + } +} diff --git a/src/query_variant_id.rs b/src/query_variant_id.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_variant_id.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_variant_id/command.rs b/src/query_variant_id/command.rs new file mode 100644 index 0000000..403c4bd --- /dev/null +++ b/src/query_variant_id/command.rs @@ -0,0 +1,74 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::QUERY_VARIANT_ID_COMMAND, AuxCommand, + AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Variant ID Number - Command (Subtype 0x0F) +/// +/// This command is used to return the software part number of the actual variant component of the device firmware. +/// +/// This is only applicable when the device has been loaded with a combine file (a file that +/// contains both the application and the variant) because the (Subtype 0x09) Query Acceptor Variant Part +/// Number command will return the part number of the combine file, not the underlying component’s part +/// number. +/// +/// The device capabilities map (section 7.4.14) has an entry as to whether or not the device supports this command. +/// +/// The Query Variant ID Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x0F | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryVariantIdCommand { + buf: [u8; QUERY_VARIANT_ID_COMMAND], +} + +impl QueryVariantIdCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VARIANT_ID_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryVariantId); + + message + } +} + +impl_default!(QueryVariantIdCommand); +impl_message_ops!(QueryVariantIdCommand); +impl_aux_ops!(QueryVariantIdCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_variant_id_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x0f, + // ETX | Checksum + 0x03, 0x67, + ]; + + let mut msg = QueryVariantIdCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryVariantId); + + Ok(()) + } +} diff --git a/src/query_variant_id/reply.rs b/src/query_variant_id/reply.rs new file mode 100644 index 0000000..4ab3f8f --- /dev/null +++ b/src/query_variant_id/reply.rs @@ -0,0 +1,152 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::QUERY_VARIANT_ID_REPLY, + MessageOps, MessageType, PartVersion, ProjectNumber, VariantPartNumber, +}; + +mod index { + pub const PROJECT_NUM: usize = 3; + pub const VERSION: usize = 9; +} + +/// Query Variant ID - Reply (Subtype 0x0F) +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// The Query Variant ID Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | ... | Data 8 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:----:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | ... | 11 | 12 | 13 | +/// | Value | 0x02 | 0x0E | 0x6n | nn | nn | nn | nn | 0x03 | zz | +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// See [ProjectNumber](crate::ProjectNumber) for formatting details. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryVariantIdReply { + buf: [u8; QUERY_VARIANT_ID_REPLY], +} + +impl QueryVariantIdReply { + /// Creates a new [QueryVariantIdReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VARIANT_ID_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Gets the [VariantPartNumber]. + pub fn variant_part_number(&self) -> VariantPartNumber { + self.buf[index::PROJECT_NUM..self.etx_index()] + .as_ref() + .into() + } + + /// Gets the [ProjectNumber] parsed from the raw byte buffer. + /// + /// On invalid ranges, returns a zeroed [ProjectNumber]. + pub fn project_number(&self) -> ProjectNumber { + self.buf[index::PROJECT_NUM..index::VERSION].as_ref().into() + } + + /// Gets the [PartVersion]. + pub fn version(&self) -> PartVersion { + self.buf[index::VERSION..self.etx_index()].as_ref().into() + } +} + +impl_default!(QueryVariantIdReply); +impl_message_ops!(QueryVariantIdReply); +impl_omnibus_nop_reply!(QueryVariantIdReply); + +impl fmt::Display for QueryVariantIdReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, VariantPartNumber: {}, ProjectNumber: {}, Version: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.variant_part_number(), + self.project_number(), + self.version(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CheckDigit; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_variant_id_reply_from_buf() -> Result<()> { + // Type 1 Variant Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'0', b'0', b'0', + // Check Digit (in ASCII) + b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x54, + ]; + + let mut msg = QueryVariantIdReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type1(28_000, CheckDigit::from(b'0')); + let exp_part_version = PartVersion::from(b"123"); + let exp_variant_part_number = VariantPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.variant_part_number(), exp_variant_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + // Type 2 Variant Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'6', b'0', b'0', b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type2(286_000); + let exp_part_version = PartVersion::from(b"123"); + let exp_variant_part_number = VariantPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.variant_part_number(), exp_variant_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + Ok(()) + } +} diff --git a/src/query_variant_name.rs b/src/query_variant_name.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_variant_name.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_variant_name/command.rs b/src/query_variant_name/command.rs new file mode 100644 index 0000000..936179d --- /dev/null +++ b/src/query_variant_name/command.rs @@ -0,0 +1,86 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, impl_omnibus_command_ops, + len::QUERY_VARIANT_NAME_COMMAND, AuxCommand, AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Variant Name - Command (Subtype 0x08) +/// +/// This command is used to return the name of the variant component of the firmware. The variant +/// software determines which bank notes are accepted by the device and the name of the variant, +/// identifies the country of origin of those bank notes. +/// +/// The Query Variant Name Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x08 | 0x03 | zz | +pub struct QueryVariantNameCommand { + buf: [u8; QUERY_VARIANT_NAME_COMMAND], +} + +impl QueryVariantNameCommand { + /// Creates a new [QueryVariantNameCommand] + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VARIANT_NAME_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryVariantName); + + message + } +} + +impl_default!(QueryVariantNameCommand); +impl_message_ops!(QueryVariantNameCommand); +impl_omnibus_command_ops!(QueryVariantNameCommand); +impl_aux_ops!(QueryVariantNameCommand); + +impl fmt::Display for QueryVariantNameCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, Command: {}, Checksum: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.aux_command(), + self.checksum(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_boot_part_number_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x08, + // ETX | Checksum + 0x03, 0x60, + ]; + + let mut msg = QueryVariantNameCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryVariantName); + + Ok(()) + } +} diff --git a/src/query_variant_name/reply.rs b/src/query_variant_name/reply.rs new file mode 100644 index 0000000..6d0cf69 --- /dev/null +++ b/src/query_variant_name/reply.rs @@ -0,0 +1,137 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::QUERY_VARIANT_NAME_REPLY, + MessageOps, MessageType, +}; + +mod index { + pub const DATA: usize = 3; +} + +/// Query Variant Name - Reply (Subtype 0x08) +/// +/// Represents the currency variant name currently in use by the firmware. +/// +/// The data returned by the device takes the form of an ASCII string that is either 32 bytes long or is +/// terminated by a non-printable character (`0x00`). +/// +/// The Query Variant Name Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | ... | Data 31 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:---:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | ... | 34 | 35 | 36 | +/// | Value | 0x02 | 0x25 | 0x6n | nn | nn | nn | nn | 0x03 | zz | +/// +/// The names of the currencies supported are represented as three character ISO codes. If more than one +/// currency is supported, they are separated by underscore `_` characters. For example `USD_CAD` would +/// signify a mixed U.S.A./Canadian bill set. +/// +/// For further information on currency descriptors, please see . +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryVariantNameReply { + buf: [u8; QUERY_VARIANT_NAME_REPLY], +} + +impl QueryVariantNameReply { + /// Creates a new [QueryVariantNameReply] + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VARIANT_NAME_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Gets the variant name from the [QueryVariantNameReply]. + pub fn variant_name(&self) -> &str { + let etx_index = self.etx_index(); + let buf = self.buf(); + + let name = std::str::from_utf8(buf[index::DATA..etx_index].as_ref()).unwrap_or("Unknown"); + let end = if let Some(i) = name.find('\0') { + i + } else { + name.len() + }; + + &name[..end] + } +} + +impl_default!(QueryVariantNameReply); +impl_message_ops!(QueryVariantNameReply); +impl_omnibus_nop_reply!(QueryVariantNameReply); + +impl fmt::Display for QueryVariantNameReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AckNak: {}, DeviceType: {}, MessageType: {}, VariantName: {}, Checksum: {}", + self.acknak(), + self.device_type(), + self.message_type(), + self.variant_name(), + self.checksum(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_variant_name_reply_from_buf() -> Result<()> { + // Variant name - short + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x25, 0x60, + // Data (name in ASCII) + b'S', b'C', b'N', b'L', b' ', + b'6', b'6', b'7', b'0', b'R', + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', + // ETX | Checksum + 0x03, 0x22, + ]; + + let mut msg = QueryVariantNameReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + assert_eq!(msg.variant_name(), "SCNL 6670R"); + + // Variant name - full + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x25, 0x60, + // Data (name in ASCII) + b'S', b'C', b'N', b'L', b' ', + b'6', b'6', b'7', b'0', b'R', b' ', + b'P', b'l', b'u', b's', b' ', + b's', b'o', b'm', b'e', b' ', + b'l', b'o', b'n', b'g', b' ', + b's', b'u', b'f', b'f', b'i', b'x', + // ETX | Checksum + 0x03, 0x11, + ]; + + let mut msg = QueryVariantNameReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + assert_eq!(msg.variant_name(), "SCNL 6670R Plus some long suffix"); + + Ok(()) + } +} diff --git a/src/query_variant_part_number.rs b/src/query_variant_part_number.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/query_variant_part_number.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/query_variant_part_number/command.rs b/src/query_variant_part_number/command.rs new file mode 100644 index 0000000..95040e3 --- /dev/null +++ b/src/query_variant_part_number/command.rs @@ -0,0 +1,67 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::QUERY_VARIANT_PART_NUMBER_COMMAND, + AuxCommand, AuxCommandOps, MessageOps, MessageType, +}; + +/// Query Variant Part Number - Command (Subtype 0x09) +/// +/// This command is used to return the software part number of the file containing the variant component of the device firmware. +/// +/// The Query Variant Part Number Command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x00 | 0x00 | 0x09 | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryVariantPartNumberCommand { + buf: [u8; QUERY_VARIANT_PART_NUMBER_COMMAND], +} + +impl QueryVariantPartNumberCommand { + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VARIANT_PART_NUMBER_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::QueryVariantPartNumber); + + message + } +} + +impl_default!(QueryVariantPartNumberCommand); +impl_message_ops!(QueryVariantPartNumberCommand); +impl_aux_ops!(QueryVariantPartNumberCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_variant_part_number_command_from_buf() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x08, 0x60, + // Data + 0x00, 0x00, + // Command + 0x09, + // ETX | Checksum + 0x03, 0x61, + ]; + + let mut msg = QueryVariantPartNumberCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::QueryVariantPartNumber); + + Ok(()) + } +} diff --git a/src/query_variant_part_number/reply.rs b/src/query_variant_part_number/reply.rs new file mode 100644 index 0000000..4aa4a1d --- /dev/null +++ b/src/query_variant_part_number/reply.rs @@ -0,0 +1,164 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_message_ops, impl_omnibus_nop_reply, len::QUERY_VARIANT_PART_NUMBER_REPLY, + MessageOps, MessageType, PartVersion, ProjectNumber, VariantPartNumber, +}; + +mod index { + pub const PROJECT_NUM: usize = 3; + pub const VERSION: usize = 9; +} + +/// Query Variant Part Number - Reply (Subtype 0x09) +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// **WARNING** If the device was loaded with a combined file (a file that contains both the +/// application and the variant) this command will return the part number for the combined file, not the +/// actual part number of the underlying variant component. In this case, (Subtype 0x0F) Query Acceptor +/// Variant ID can be used to retrieve the part number of the variant component. +/// +/// The Query Variant Part Number Reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Data 0 | Data 1 | ... | Data 8 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:----:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | ... | 11 | 12 | 13 | +/// | Value | 0x02 | 0x0E | 0x6n | nn | nn | nn | nn | 0x03 | zz | +/// +/// The part number is composed of a project number (5-6 digits) and version number (3 digits) with an +/// optional Check sum digit in the middle. +/// +/// See [ProjectNumber](crate::ProjectNumber) for formatting details. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct QueryVariantPartNumberReply { + buf: [u8; QUERY_VARIANT_PART_NUMBER_REPLY], +} + +impl QueryVariantPartNumberReply { + /// Creates a new [QueryVariantPartNumberReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; QUERY_VARIANT_PART_NUMBER_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + + message + } + + /// Gets the [VariantPartNumber]. + pub fn variant_part_number(&self) -> VariantPartNumber { + self.buf[index::PROJECT_NUM..self.etx_index()] + .as_ref() + .into() + } + + /// Gets the [ProjectNumber] parsed from the raw byte buffer. + /// + /// On invalid ranges, returns a zeroed [ProjectNumber]. + pub fn project_number(&self) -> ProjectNumber { + self.buf[index::PROJECT_NUM..index::VERSION].as_ref().into() + } + + /// Gets the [PartVersion]. + pub fn version(&self) -> PartVersion { + self.buf[index::VERSION..self.etx_index()].as_ref().into() + } +} + +impl_default!(QueryVariantPartNumberReply); +impl_message_ops!(QueryVariantPartNumberReply); +impl_omnibus_nop_reply!(QueryVariantPartNumberReply); + +impl fmt::Display for QueryVariantPartNumberReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + " + AckNak: {}, + DeviceType: {}, + MessageType: {}, + VariantPartNumber: {}, + ProjectNumber: {}, + Version: {}, + ", + self.acknak(), + self.device_type(), + self.message_type(), + self.variant_part_number(), + self.project_number(), + self.version(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CheckDigit; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_query_variant_part_number_reply_from_buf() -> Result<()> { + // Type 1 Variant Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'0', b'0', b'0', + // Check Digit (in ASCII) + b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x54, + ]; + + let mut msg = QueryVariantPartNumberReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type1(28_000, CheckDigit::from(b'0')); + let exp_part_version = PartVersion::from(b"123"); + let exp_variant_part_number = VariantPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.variant_part_number(), exp_variant_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + // Type 2 Variant Part Number + let msg_bytes = [ + // STX | LEN | Message Type + 0x02, 0x0e, 0x60, + // Project number (in ASCII) + b'2', b'8', b'6', b'0', b'0', b'0', + // Version (in ASCII) + b'1', b'2', b'3', + // ETX | Checksum + 0x03, 0x52, + ]; + + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + + let exp_project_number = ProjectNumber::type2(286_000); + let exp_part_version = PartVersion::from(b"123"); + let exp_variant_part_number = VariantPartNumber::new(exp_project_number, exp_part_version); + + assert_eq!(msg.variant_part_number(), exp_variant_part_number); + assert_eq!(msg.project_number(), exp_project_number); + assert_eq!(msg.version(), exp_part_version); + assert_eq!(msg.version().as_string().as_str(), "V1.23"); + + Ok(()) + } +} diff --git a/src/set_escrow_timeout.rs b/src/set_escrow_timeout.rs new file mode 100644 index 0000000..c74c787 --- /dev/null +++ b/src/set_escrow_timeout.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod reply; + +pub use command::*; +pub use reply::*; diff --git a/src/set_escrow_timeout/command.rs b/src/set_escrow_timeout/command.rs new file mode 100644 index 0000000..d248eeb --- /dev/null +++ b/src/set_escrow_timeout/command.rs @@ -0,0 +1,111 @@ +use crate::{ + impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_command, + len::SET_ESCROW_TIMEOUT_COMMAND, ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, +}; + +mod index { + pub const NOTES: usize = 7; + pub const BARCODES: usize = 8; +} + +const TIMEOUT_MASK: u8 = 0x7f; + +/// This command is generally used to set the escrow timeout of the device. However, it can also serve an +/// alternative function in reporting a special coupon if that mode is enabled (Section 7.1.1.3). +/// +/// This command is generally used to set the escrow timeout of the device. However, it can also serve an +/// alternative function in reporting a special coupon if that mode is enabled (Section 7.1.1.3). +/// SCR Classification When classification mode is enabled in the device, the device will suppress the +/// EBDS escrow timeout. This means that if communications are lost with the host, then the SCR will keep +/// the notes at escrow until communications are restored and the host makes the escrow decision. +/// +/// The Set Escrow command is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Notes | Barcode | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:-----:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +/// | Value | 0x02 | 0x0B | 0x7n | 0x04 | nn | nn | nn | nn | nn | 0x03 | zz | +pub struct SetEscrowTimeoutCommand { + buf: [u8; SET_ESCROW_TIMEOUT_COMMAND], +} + +impl SetEscrowTimeoutCommand { + /// Creates a new [SetEscrowTimeoutCommand]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; SET_ESCROW_TIMEOUT_COMMAND], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::SetEscrowTimeout); + + message + } + + /// Gets the timeout for bank notes. + pub fn notes_timeout(&self) -> u8 { + self.buf[index::NOTES] & TIMEOUT_MASK + } + + /// Sets the timeout for bank notes. + /// + /// 1-127 sets the timeout in seconds. + /// + /// 0 disables the timeout. + pub fn set_notes_timeout(&mut self, secs: u8) { + self.buf[index::NOTES] = secs & TIMEOUT_MASK; + } + + /// Sets the timeout for barcodes. + /// + /// 1-127 sets the timeout in seconds. + /// + /// 0 disables the timeout. + pub fn set_barcodes_timeout(&mut self, secs: u8) { + self.buf[index::BARCODES] = secs & TIMEOUT_MASK; + } + + /// Gets the timeout for barcodes. + pub fn barcodes_timeout(&self) -> u8 { + self.buf[index::BARCODES] & TIMEOUT_MASK + } +} + +impl_default!(SetEscrowTimeoutCommand); +impl_message_ops!(SetEscrowTimeoutCommand); +impl_extended_ops!(SetEscrowTimeoutCommand); +impl_omnibus_extended_command!(SetEscrowTimeoutCommand); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_set_escrow_timeout_command_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0b, 0x70, 0x04, + // Data + 0x00, 0x00, 0x00, + // Notes + 0x01, + // Barcode + 0x02, + // ETX | Checksum + 0x03, 0x7c, + ]; + + let mut msg = SetEscrowTimeoutCommand::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::SetEscrowTimeout); + assert_eq!(msg.notes_timeout(), 1); + assert_eq!(msg.barcodes_timeout(), 2); + + Ok(()) + } +} diff --git a/src/set_escrow_timeout/reply.rs b/src/set_escrow_timeout/reply.rs new file mode 100644 index 0000000..cf13c5d --- /dev/null +++ b/src/set_escrow_timeout/reply.rs @@ -0,0 +1,105 @@ +use crate::std; +use std::fmt; + +use crate::{ + impl_default, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply, + len::SET_ESCROW_TIMEOUT_REPLY, ExtendedCommand, ExtendedCommandOps, MessageOps, MessageType, + OmnibusReplyOps, +}; + +/// This command is generally used to set the escrow timeout of the device. However, it can also serve an +/// alternative function in reporting a special coupon if that mode is enabled (Section 7.1.1.3). +/// +/// The Notes and Barcode fields set the timeout for bank notes and barcodes in seconds. This is a value +/// from 1 through 127 seconds, or zero to disable the timeout. By default, both timeouts are disabled in +/// most software implementations. +/// +/// The reply contains no extended data. +/// +/// The Set Escrow reply is formatted as follows: +/// +/// | Name | STX | LEN | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ETX | CHK | +/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | +/// | Value | 0x02 | 0x0C | 0X7n | 0x04 | nn | nn | nn | nn | nn | nn | 0x03 | zz | +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SetEscrowTimeoutReply { + buf: [u8; SET_ESCROW_TIMEOUT_REPLY], +} + +impl SetEscrowTimeoutReply { + /// Creates a new [SetEscrowTimeoutReply]. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; SET_ESCROW_TIMEOUT_REPLY], + }; + + message.init(); + message.set_message_type(MessageType::Extended); + message.set_extended_command(ExtendedCommand::SetEscrowTimeout); + + message + } +} + +impl_default!(SetEscrowTimeoutReply); +impl_message_ops!(SetEscrowTimeoutReply); +impl_extended_ops!(SetEscrowTimeoutReply); +impl_omnibus_extended_reply!(SetEscrowTimeoutReply); + +impl fmt::Display for SetEscrowTimeoutReply { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + " + AckNak: {}, + DeviceType: {}, + MessageType: {}, + Subtype: {}, + DeviceState: {}, + DeviceStatus: {}, + ExceptionStatus: {}, + MiscDeviceState: {}, + ModelNumber: {}, + CodeRevision: {}, + ", + self.acknak(), + self.device_type(), + self.message_type(), + self.extended_command(), + self.device_state(), + self.device_status(), + self.exception_status(), + self.misc_device_state(), + self.model_number(), + self.code_revision(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_set_escrow_timeout_reply_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x0c, 0x70, 0x04, + // Data + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ETX | Checksum + 0x03, 0x78, + ]; + + let mut msg = SetEscrowTimeoutReply::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::Extended); + assert_eq!(msg.extended_command(), ExtendedCommand::SetEscrowTimeout); + + Ok(()) + } +} diff --git a/src/soft_reset.rs b/src/soft_reset.rs new file mode 100644 index 0000000..c85496a --- /dev/null +++ b/src/soft_reset.rs @@ -0,0 +1,82 @@ +use crate::{ + impl_aux_ops, impl_default, impl_message_ops, len::SOFT_RESET, AuxCommand, AuxCommandOps, + MessageOps, MessageType, +}; + +mod index { + pub const DATA: usize = 3; + pub const DATA_END: usize = DATA + 2; +} + +/// Acceptor Soft Reset: (Subtype 0x7F) +/// +/// This command is used to reset the device. There is not necessarily a reply to this command, but some +/// data may be sent by the device. The host system should ignore all data sent by the device for at least +/// one second. Further, the device may take as much as fifteen seconds to return to normal operation after +/// being reset and the host should poll, once per second, for at least fifteen seconds until the device +/// replies. +/// +/// The acceptor soft reset command takes the form: +/// +/// | Name | STX | LEN | CTRL | Data A | Data B | Command | ETX | CHK | +/// |:------|:----:|:----:|:----:|:------:|:------:|:-------:|:----:|:---:| +/// | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// | Value | 0x02 | 0x08 | 0x6n | 0x7F | 0x7F | 0x7F | 0x03 | zz | +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SoftReset { + buf: [u8; SOFT_RESET], +} + +impl SoftReset { + /// Creates a new [SoftReset] message. + pub fn new() -> Self { + let mut message = Self { + buf: [0u8; SOFT_RESET], + }; + + message.init(); + message.set_message_type(MessageType::AuxCommand); + message.set_aux_command(AuxCommand::SoftReset); + message.set_reset_data(); + + message + } + + fn set_reset_data(&mut self) { + self.buf[index::DATA..index::DATA_END].copy_from_slice([0x7f, 0x7f].as_ref()); + } +} + +impl_default!(SoftReset); +impl_message_ops!(SoftReset); +impl_aux_ops!(SoftReset); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Result; + + #[test] + #[rustfmt::skip] + fn test_soft_reset_from_bytes() -> Result<()> { + let msg_bytes = [ + // STX | LEN | Message type | Subtype + 0x02, 0x08, 0x60, + // Data + 0x7f, 0x7f, + // Command + 0x7f, + // ETX | Checksum + 0x03, 0x17, + ]; + + let mut msg = SoftReset::new(); + msg.from_buf(msg_bytes.as_ref())?; + + assert_eq!(msg.message_type(), MessageType::AuxCommand); + assert_eq!(msg.aux_command(), AuxCommand::SoftReset); + + Ok(()) + } +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..a1bcb00 --- /dev/null +++ b/src/status.rs @@ -0,0 +1,1028 @@ +use crate::std::fmt; + +use crate::{bool_enum, StandardDenomination, OPEN_BRACE, CLOSE_BRACE}; + +mod document_status; + +pub use document_status::*; + +bitfield! { + /// DeviceState describes the current state of the device + /// + /// The variants describe the bitfield values of data byte 0 in OmnibusReply messages + /// + /// The variants ending in `-ing` are transient states, and should only be used for information + /// purposes. + /// + /// The `Stacked`, `Returned`, and `Rejected` bits are mutually exclusive and will never be sent in + /// the same message. + /// + /// DeviceState is a bitfield, representing the following: + /// + /// * [Idling]: bit 0 + /// * [Accepting]: bit 1 + /// * [EscrowedState]: bit 2 + /// * [Stacking]: bit 3 + /// * [StackedEvent]: bit 4 + /// * [Returning]: bit 5 + /// * [ReturnedEvent]: bit 6 + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct DeviceState(u8); + u8; + /// The device is idling. Not processing a document. + pub idling, set_idling: 0; + /// The device is drawing in a document. + pub accepting, set_accepting: 1; + /// There is a valid document in escrow. + pub escrowed_state, set_escrowed_state: 2; + /// The device is stacking a document. + pub stacking, set_stacking: 3; + /// The device has stacked a document. + pub stacked_event, set_stacked_event: 4; + /// The device is returning a document to the customer. + pub returning, set_returning: 5; + /// The device has returned a document to the customer. + pub returned_event, set_returned_event: 6; +} + +impl DeviceState { + /// Creates a [DeviceState] with no bits set. + pub const fn none() -> Self { + Self(0) + } + + /// If all seven state bits are zero, the device is out of service. + pub fn out_of_service(&self) -> bool { + (self.0 & 0b111_1111) == 0 + } + + /// If all seven state bits are zero, the device is out of service. + pub fn host_disabled(&self) -> bool { + self.0 == DeviceStateFlags::HostDisabled as u8 + } +} + +impl From for DeviceState { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From for u8 { + fn from(d: DeviceState) -> Self { + (&d).into() + } +} + +impl From<&DeviceState> for u8 { + fn from(d: &DeviceState) -> Self { + d.0 + } +} + +impl From<&mut DeviceState> for u8 { + fn from(d: &mut DeviceState) -> Self { + (&*d).into() + } +} + +impl fmt::Display for DeviceState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}idling:{}, accepting:{}, escrowed_state:{}, stacking:{}, stacked_event:{}, returning:{}, returned_event:{}{CLOSE_BRACE}", + self.idling(), + self.accepting(), + self.escrowed_state(), + self.stacking(), + self.stacked_event(), + self.returning(), + self.returned_event(), + ) + } +} + +/// Values that represent device states +/// +/// Represents semantic values for a combination of bitfield settings in [DeviceState]. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DeviceStateFlags { + Disconnected = 0, + PowerUp, + Initialize, + Download, + Idle, + HostDisabled, + BusyCalculation, + Escrowed, + Accepting, + Stacking, + Returning, + Cheated, + Jammed, + StackerFull, + Paused, + Calibration, + Failure, + Stalled, + CashBoxRemoved, + TransportOpened, + Dispensing, + FloatingDown, + Disabled, + EscrowStorageFull, + IdleInEscrowSession, + HostDisabledInEscrowSession, + UnknownDocumentsDetected, + PatternRecovering, + DisabledAndJammed, + Unknown = 0xff, +} + +impl From for DeviceStateFlags { + fn from(f: u8) -> Self { + match f { + 0 => Self::Disconnected, + 1 => Self::PowerUp, + 2 => Self::Initialize, + 3 => Self::Download, + 4 => Self::Idle, + 5 => Self::HostDisabled, + 6 => Self::BusyCalculation, + 7 => Self::Escrowed, + 8 => Self::Accepting, + 9 => Self::Stacking, + 10 => Self::Returning, + 11 => Self::Cheated, + 12 => Self::Jammed, + 13 => Self::StackerFull, + 14 => Self::Paused, + 15 => Self::Calibration, + 16 => Self::Failure, + 17 => Self::Stalled, + 18 => Self::CashBoxRemoved, + 19 => Self::TransportOpened, + 20 => Self::Dispensing, + 21 => Self::FloatingDown, + 22 => Self::Disabled, + 23 => Self::EscrowStorageFull, + 24 => Self::IdleInEscrowSession, + 25 => Self::HostDisabledInEscrowSession, + 26 => Self::UnknownDocumentsDetected, + 27 => Self::PatternRecovering, + 28 => Self::DisabledAndJammed, + _ => Self::Unknown, + } + } +} + +impl From<&DeviceState> for DeviceStateFlags { + fn from(s: &DeviceState) -> Self { + Self::from(s.0) + } +} + +impl From<&mut DeviceState> for DeviceStateFlags { + fn from(s: &mut DeviceState) -> Self { + Self::from(&*s) + } +} + +impl From for DeviceStateFlags { + fn from(s: DeviceState) -> Self { + Self::from(&s) + } +} + +impl From<&DeviceStateFlags> for &'static str { + fn from(d: &DeviceStateFlags) -> Self { + match *d { + DeviceStateFlags::Disconnected => "Disconnected", + DeviceStateFlags::PowerUp => "Power up", + DeviceStateFlags::Initialize => "Initialize", + DeviceStateFlags::Download => "Download", + DeviceStateFlags::Idle => "Idle", + DeviceStateFlags::HostDisabled => "Host disabled", + DeviceStateFlags::BusyCalculation => "Busy calculation", + DeviceStateFlags::Escrowed => "Escrowed", + DeviceStateFlags::Accepting => "Accepting", + DeviceStateFlags::Stacking => "Stacking", + DeviceStateFlags::Returning => "Returning", + DeviceStateFlags::Cheated => "Cheated", + DeviceStateFlags::Jammed => "Jammed", + DeviceStateFlags::StackerFull => "Stacker full", + DeviceStateFlags::Paused => "Paused", + DeviceStateFlags::Calibration => "Calibration", + DeviceStateFlags::Failure => "Failure", + DeviceStateFlags::Stalled => "Stalled", + DeviceStateFlags::CashBoxRemoved => "CashBox removed", + DeviceStateFlags::TransportOpened => "Transport opened", + DeviceStateFlags::Dispensing => "Dispensing", + DeviceStateFlags::FloatingDown => "Floating down", + DeviceStateFlags::Disabled => "Disabled", + DeviceStateFlags::EscrowStorageFull => "Escrow storage full", + DeviceStateFlags::IdleInEscrowSession => "Idle in escrow session", + DeviceStateFlags::HostDisabledInEscrowSession => "Host disabled in escrow session", + DeviceStateFlags::UnknownDocumentsDetected => "Unknown documents detected", + DeviceStateFlags::PatternRecovering => "Pattern recovering", + DeviceStateFlags::DisabledAndJammed => "Disabled and jammed", + DeviceStateFlags::Unknown => "Unknown", + } + } +} + +impl From for &'static str { + fn from(d: DeviceStateFlags) -> Self { + (&d).into() + } +} + +impl fmt::Display for DeviceStateFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +bool_enum!(Idling, r"The device is idling. Not processing a document."); +bool_enum!(Accepting, r"The device is drawing in a document."); +bool_enum!(EscrowedState, r"There is a valid document in escrow."); +bool_enum!(Stacking, r"The device is stacking a document."); +bool_enum!(StackedEvent, r"The device has stacked a document."); +bool_enum!( + Returning, + r"The device is returning a document to the customer." +); +bool_enum!( + ReturnedEvent, + r"The device has returned a document to the customer." +); + +bitfield! { + /// DeviceStatus contains non-state related status of the device. + /// + /// DeviceStatus is a bitfield, representing the following: + /// + /// * [Cheated]: bit 0 + /// * [Rejected]: bit 1 + /// * [Jammed]: bit 2 + /// * [StackerFull]: bit 3 + /// * [CassetteAttached]: bit 4 + /// * [Paused]: bit 5 + /// * [Calibration]: bit 6 + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct DeviceStatus(u8); + u8; + /// The device has detected conditions consistent with an attempt to fraud the system. + pub cheated, set_cheated: 0; + /// The document presented to the device could not be validated and was returned to the customer. + pub rejected, set_rejected: 1; + /// The path is blocked and the device has been unable to resolve the issue. + /// Intervention is required. + pub jammed, set_jammed: 2; + /// The cash box is full of documents and no more may be accepted. + /// The device will be out of service until the issue is corrected. + pub stacker_full, set_stacker_full: 3; + /// If unset, the cash box has been removed. No documents may be accepted. + /// The device is out of service until the issue is corrected. + /// + /// If set, the cash box is attached to the device. + pub cassette_attached, set_cassette_attached: 4; + /// The customer is attempting to feed another note while the previous + /// note is still being processed. The customer must remove the note to + /// permit processing to continue. + pub paused, set_paused: 5; + /// It is possible to field calibrate devices. In general, due to advances in processes + /// used in manufacturing and continuous self-calibration, this is not needed. + /// Calibrating a device with an incorrect document will greatly reduce + /// performance. For more information on field calibration please refer to section 4.7. + /// + /// If unset, the device is in normal operation. + /// + /// If set, the device is in calibration mode. Intervention is required to feed a + /// special calibration document into the device. + pub calibration, set_calibration: 6; +} + +impl fmt::Display for DeviceStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"cheated\":{},\"rejected\":{},\"jammed\":{},\"stacker_full\":{},\"cassette_attached\":{},\"paused\":{},\"calibration\":{}{CLOSE_BRACE}", + self.cheated(), + self.rejected(), + self.jammed(), + self.stacker_full(), + self.cassette_attached(), + self.paused(), + self.calibration(), + ) + } +} + +impl DeviceStatus { + /// Creates a [DeviceStatus] with no set bits. + pub const fn none() -> Self { + Self(0) + } +} + +impl From for DeviceStatus { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From for u8 { + fn from(d: DeviceStatus) -> Self { + d.0 + } +} + +impl From<&DeviceStatus> for u8 { + fn from(d: &DeviceStatus) -> Self { + d.0 + } +} + +/// Values that represent the cash box status +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum CashBoxStatus { + Attached = 0, + Removed = 1, + Full = 2, + Unknown = 0xff, +} + +impl CashBoxStatus { + pub const fn default() -> Self { + Self::Attached + } +} + +impl From for CashBoxStatus { + fn from(b: bool) -> Self { + match b { + true => Self::Attached, + false => Self::Removed, + } + } +} + +impl From for CashBoxStatus { + fn from(b: u8) -> Self { + match b { + 0 => Self::Attached, + 1 => Self::Removed, + 2 => Self::Full, + _ => Self::Unknown, + } + } +} + +impl From<&CashBoxStatus> for u8 { + fn from(c: &CashBoxStatus) -> Self { + (*c) as u8 + } +} + +impl From for u8 { + fn from(c: CashBoxStatus) -> Self { + (&c).into() + } +} + +impl From for &'static str { + fn from(c: CashBoxStatus) -> Self { + match c { + CashBoxStatus::Attached => "attached", + CashBoxStatus::Removed => "removed", + CashBoxStatus::Full => "full", + CashBoxStatus::Unknown => "unknown", + } + } +} + +impl From<&CashBoxStatus> for &'static str { + fn from(c: &CashBoxStatus) -> Self { + (*c).into() + } +} + +impl fmt::Display for CashBoxStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +bool_enum!( + Cheated, + r"The device has detected conditions consistent with an attempt to fraud the system." +); +bool_enum!( + Rejected, + r"The document presented to the device could not be validated and was returned to the customer." +); +bool_enum!( + Jammed, + r" + The path is blocked and the device has been unable to resolve the issue. + + Intervention is required. +" +); +bool_enum!( + StackerFull, + r" + The cash box is full of documents and no more may be accepted. + + The device will be out of service until the issue is corrected. +" +); +bool_enum!( + CassetteAttached, + r" + Unset: the cash box has been removed. No documents may be accepted. + The device is out of service until the issue is corrected. + + Set: the cash box is attached to the device. +" +); +bool_enum!( + Paused, + r" + The customer is attempting to feed another note while the previous + note is still being processed. The customer must remove the note to + permit processing to continue. +" +); +bool_enum!( + Calibration, + r" + It is possible to field calibrate devices. In general, due to advances in processes + used in manufacturing and continuous self-calibration, this is not needed. + Calibrating a device with an incorrect document will greatly reduce + performance. For more information on field calibration please refer to section 4.7. + + Unset: the device is in normal operation. + + Set: the device is in calibration mode. Intervention is required to feed a + special calibration document into the device. +" +); + +bitfield! { + /// ExceptionStatus contains additional information on exceptional statuses as well as the reporting of the note value. (Non-extended mode only.) + /// + /// ExceptionStatus is a bitfield, representing the following: + /// + /// * [PowerUp]: bit 0 + /// * [InvalidCommand]: bit 1 + /// * [Failure]: bit 2 + /// * [NoteValue](crate::denomination::StandardDenomination): bit 3..5 + /// * [TranportOpen]: bit 6 + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct ExceptionStatus(u8); + u8; + /// Unset (0): The device is operating normally (Power up process is complete) + /// Set (1): The device has been powered up. It is performing its initialization + /// routine, and not yet ready to accept documents. + pub power_up, set_power_up: 0; + /// The device received an invalid command. + pub invalid_command, set_invalid_command: 1; + /// The device has encountered a problem and is out of service. + /// Intervention is required. + pub failure, set_failure: 2; + /// The non-extended note value field. This field is valid when the device is in non- + /// extended mode and either the escrow or stacked bits are set. + /// (See Omnibus Reply – Data Byte 0 for details of those events) + /// + /// 000 - Unknown/No credit + /// 001 - Denom1 + /// 010 - Denom2 + /// 011 - Denom3 + /// 100 - Denom4 + /// 101 - Denom5 + /// 110 - Denom6 + /// 111 - Denom7 + pub note_value, set_note_value: 5, 3; + /// Unset (0) - Note path access is closed + /// Set (1) - Note path access is opened (vault, door, or both). + /// + /// **WARNING**: This bit will also be reported one time upon power up if the note path + /// was opened while the unit was powered down. + pub transport_open, set_transport_open: 6; +} + +impl ExceptionStatus { + /// Create an [ExceptionStatus] with no bits set. + pub const fn none() -> Self { + Self(0) + } +} + +impl fmt::Display for ExceptionStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"power_up\":{},\"invalid_command\":{},\"note_value\":\"{}\",\"transport_open\":{}{CLOSE_BRACE}", + self.power_up(), + self.invalid_command(), + StandardDenomination::from_note_value(self.note_value()), + self.transport_open(), + ) + } +} + +impl From for ExceptionStatus { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From for u8 { + fn from(e: ExceptionStatus) -> Self { + e.0 + } +} + +impl From<&ExceptionStatus> for u8 { + fn from(e: &ExceptionStatus) -> Self { + e.0 + } +} + +bool_enum!( + PowerUpStatus, + r" + The power up status of the device. + + Unset: The device is operating normally (Power up process is complete) + + Set: The device has been powered up. It is performing its initialization routine, and not yet ready to accept documents. +" +); +bool_enum!( + InvalidCommand, + r"Whether the device received an invalid command." +); +bool_enum!( + Failure, + r" + The device has encountered a problem and is out of service. + + Intervention is required. +" +); +bool_enum!( + TransportOpen, + r" + Whether the device transport is open. + + Unset: Note path access is closed + + Set: Note path access is opened (vault, door, or both). + + **WARNING**: This bit will also be reported one time upon power up if the note path was opened while the unit was powered down. +" +); + +bitfield! { + /// MiscDeviceState contains miscellaneous device state fields. + /// + /// MiscDeviceState is a bitfield, representing the following: + /// + /// * [Stalled]: bit 0 + /// * [FlashDownload]: bit 1 + /// * [PreStack]: bit 2 + /// * [RawBarcode]: bit 3 + /// * [DeviceCapabilities]: bit 4 + /// * [Disabled]: bit 5 + /// * Reserved: bit 6 + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct MiscDeviceState(u8); + u8; + /// The device is stalled. + pub stalled, set_stalled: 0; + /// A flash download is ready to commence. The host may begin send + /// download records. See section 4.8 for details. + pub flash_download, set_flash_download: 1; + /// **Deprecated**: This bit indicates that the document has reached a + /// point in the stacking process where it can no longer be retrieved. + pub pre_stack, set_pre_stack: 2; + /// **Gaming only**: whether 24 character barcodes will be converted to 18 characters + pub raw_barcode, set_raw_barcode: 3; + /// Whether the Query Device Capabilities command is supported + pub device_capabilities, set_device_capabilities: 4; + /// Unset (0): SCR device enabled + /// Set (1): SCR device disabled + pub disabled, set_disabled: 5; + /// Reserved + pub reserved, _: 6; +} + +impl fmt::Display for MiscDeviceState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{OPEN_BRACE}\"stalled\":{},\"flash_download\":{},\"pre_stack\":{},\"raw_barcode\":{},\"device_capabilities\":{},\"disabled\":{}{CLOSE_BRACE}", + self.stalled(), + self.flash_download(), + self.pre_stack(), + self.raw_barcode(), + self.device_capabilities(), + self.disabled(), + ) + } +} + +impl From for MiscDeviceState { + fn from(b: u8) -> Self { + Self(b & 0b11_1111) + } +} + +impl From for u8 { + fn from(m: MiscDeviceState) -> Self { + m.0 + } +} + +impl From<&MiscDeviceState> for u8 { + fn from(m: &MiscDeviceState) -> Self { + m.0 + } +} + +bool_enum!(Stalled, r" The device is stalled."); +bool_enum!( + FlashDownload, + r" + A flash download is ready to commence. The host may begin send + download records. See section 4.8 for details. +" +); +bool_enum!( + PreStack, + r" + **Deprecated**: This bit indicates that the document has reached a + point in the stacking process where it can no longer be retrieved. +" +); +bool_enum!( + RawBarcode, + r" **Gaming only**: whether 24 character barcodes will be converted to 18 characters" +); +bool_enum!( + DeviceCapabilities, + r" Whether the Query Device Capabilities command is supported." +); +bool_enum!( + Disabled, + r" + Whether the SCR device is disabled. + + Unset: SCR device enabled + + Set: SCR device disabled +" +); + +bitfield! { + /// ModelNumber contains the model number identification of the device. The following tables show how the + /// device model can be obtained depending on the known device types. + /// + /// | Bit # | Name | Value | Description | + /// |-------|--------------|-------|------------------------------------------------| + /// | 0..6 | Model Number | nn | A value that represents the mode of the device | + /// + /// **S2K** + /// + /// | Hex | Decimal | ASCII | Product | + /// |:----:|:-------:|:-----:|:------------------------------------------------------------| + /// | 0x41 | 65 | **A** | AE2600 Gen2D, Australia | + /// | 0x42 | 66 | **B** | AE2800 Gen2D, Russia | + /// | 0x43 | 67 | **C** | AE2600 Gen2D, Canada | + /// | 0x44 | 68 | **D** | AE2800 Gen2D, Euro | + /// | 0x45 | 68 | **E** | Reserved (VN2300, US Economy) | + /// | 0x46 | 70 | **F** | Reserved (VN2600 Gen 2B, Gen 2D, China) | + /// | 0x47 | 71 | **G** | Reserved (AE2800 Gen2D, Argentina) | + /// | 0x48 | 72 | **H** | AE2400, US Economy | + /// | 0x49 | 73 | **I** | AE2600 Gen2D, Vietnam | + /// | 0x4A | 74 | **J** | VN2600 Gen2D, Colombian | + /// | 0x4B | 75 | **K** | VN2600 Gen2D, Ukraine | + /// | 0x4C | 76 | **L** | AE2400 Gen2C, US Low Cost | + /// | 0x4D | 77 | **M** | AE2800 Gen2D, Mexico | + /// | 0x4E | 78 | **N** | AE2400 Gen2D, US | + /// | 0x4F | 79 | **O** | VN2600 Gen2D, Philippines (Green revision) | + /// | 0x50 | 80 | **P** | AE2600 Gen2B, Gen2C, Gen2D, US Premium | + /// | 0x51 | 81 | **Q** | VN2600 Gen2D, Philippines (Red revision - **DISCONTINUED**) | + /// | 0x52 | 82 | **R** | VN2500, US VER1 Reference | + /// | 0x53 | 83 | **S** | AE2600 Gen2D, Saudi | + /// | 0x54 | 84 | **T** | RFU | + /// | 0x55 | 85 | **U** | VN2500 Gen2C, US | + /// | 0x56 | 86 | **V** | VN2500, US VER2 Reference | + /// | 0x57 | 87 | **W** | AE2800 Gen2D, Brazil | + /// | 0x58 | 88 | **X** | AE2800 Gen2D, US Expanded | + /// | 0x59 | 89 | **Y** | RFU | + /// | 0x5A | 90 | **Z** | AE2600 Gen2D, Indonesia | + /// | 0x68 | 104 | **h** | AE2600 Gen2D, Croatia | + /// | 0x69 | 105 | **i** | AE2600 Gen2D, Israel | + /// + /// **SC** **SC Adv** **SCR** + /// + /// | Hex | Decimal | ASCII | Product | + /// |:----:|:-------:|:-----:|:------------------------------------------------------------| + /// | 0x54 | 84 | **T** | Cashflow SC83
Cashflow SC85
SC Advance 83 (SCN83)
SC Advance 85 (SCN85)
SCR83 | + /// | 0x55 | 85 | **U** | Cashflow SC66
SC Advance 66 (SCN66) | + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct ModelNumber(u8); + u8; + pub model_number, set_model_number: 6, 0; +} + +#[cfg(feature = "s2k")] +impl fmt::Display for ModelNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let model_str: &'static str = S2kModelNumber::from(self).into(); + write!(f, "\"{}\"", model_str) + } +} + +#[cfg(feature = "sc")] +impl fmt::Display for ModelNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let model_str: &'static str = SCModelNumber::from(self).into(); + write!(f, "\"{}\"", model_str) + } +} + +#[cfg(not(any(feature = "s2k", feature = "sc")))] +impl fmt::Display for ModelNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{:x}", self.0) + } +} + +impl From for ModelNumber { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From for u8 { + fn from(m: ModelNumber) -> Self { + m.0 + } +} + +impl From<&ModelNumber> for u8 { + fn from(m: &ModelNumber) -> Self { + m.0 + } +} + +/// Model numbers for S2K machines +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum S2kModelNumber { + AE2600Australia = 0x41, + AE2800Russia = 0x42, + AE2600Canada = 0x43, + AE2800Euro = 0x44, + ReservedVN2300 = 0x45, + ReservedVN2600 = 0x46, + ReservedAE2800 = 0x47, + AE2400USEconomy = 0x48, + AE2600Vietnam = 0x49, + VN2600Colombia = 0x4a, + VN2600Ukraine = 0x4b, + AE2400USLowCost = 0x4c, + AE2800Mexico = 0x4d, + AE2400US = 0x4e, + VN2600PhilippinesGreen = 0x4f, + AE2600USPremium = 0x50, + VN2600PhilippinesRed = 0x51, + VN2500USReferenceV1 = 0x52, + AE2600Saudi = 0x53, + Reserved0 = 0x54, + VN2500US = 0x55, + VN2500USReferenceV2 = 0x56, + AE2800Brazil = 0x57, + AE2800USExpanded = 0x58, + Reserved1 = 0x59, + AE2600Indonesia = 0x5a, + AE2600Croatia = 0x68, + AE2600Israel = 0x69, + Reserved = 0xff, +} + +impl From for S2kModelNumber { + fn from(b: u8) -> Self { + match b { + 0x41 => Self::AE2600Australia, + 0x42 => Self::AE2800Russia, + 0x43 => Self::AE2600Canada, + 0x44 => Self::AE2800Euro, + 0x45 => Self::ReservedVN2300, + 0x46 => Self::ReservedVN2600, + 0x47 => Self::ReservedAE2800, + 0x48 => Self::AE2400USEconomy, + 0x49 => Self::AE2600Vietnam, + 0x4a => Self::VN2600Colombia, + 0x4b => Self::VN2600Ukraine, + 0x4c => Self::AE2400USLowCost, + 0x4d => Self::AE2800Mexico, + 0x4e => Self::AE2400US, + 0x4f => Self::VN2600PhilippinesGreen, + 0x50 => Self::AE2600USPremium, + 0x51 => Self::VN2600PhilippinesRed, + 0x52 => Self::VN2500USReferenceV1, + 0x53 => Self::AE2600Saudi, + 0x54 => Self::Reserved0, + 0x55 => Self::VN2500US, + 0x56 => Self::VN2500USReferenceV2, + 0x57 => Self::AE2800Brazil, + 0x58 => Self::AE2800USExpanded, + 0x59 => Self::Reserved1, + 0x5a => Self::AE2600Indonesia, + 0x68 => Self::AE2600Croatia, + 0x69 => Self::AE2600Israel, + _ => Self::Reserved, + } + } +} + +impl From for S2kModelNumber { + fn from(b: ModelNumber) -> Self { + b.0.into() + } +} + +impl From<&ModelNumber> for S2kModelNumber { + fn from(b: &ModelNumber) -> Self { + (*b).into() + } +} + +impl From for &'static str { + fn from(s: S2kModelNumber) -> Self { + match s { + S2kModelNumber::AE2600Australia => "AE2600 Gen2D, Australia", + S2kModelNumber::AE2800Russia => "AE2800 Gen2D, Russia", + S2kModelNumber::AE2600Canada => "AE2600 Gen2D, Canada", + S2kModelNumber::AE2800Euro => "AE2800 Gen2D, Euro", + S2kModelNumber::ReservedVN2300 => "Reserved (VN2300, US Economy)", + S2kModelNumber::ReservedVN2600 => "Reserved (VN2600 Gen2B, Gen2D, China)", + S2kModelNumber::ReservedAE2800 => "Reserved (AE2800 Gen2D, Argentina)", + S2kModelNumber::AE2400USEconomy => "AE2400, US Economy", + S2kModelNumber::AE2600Vietnam => "AE2600 Gen2D, Vietnam", + S2kModelNumber::VN2600Colombia => "VN2600 Gen2D, Colombian", + S2kModelNumber::VN2600Ukraine => "VN2600 Gen2D, Ukraine", + S2kModelNumber::AE2400USLowCost => "AE2400 Gen2C, US Low Cost", + S2kModelNumber::AE2800Mexico => "AE2800 Gen2D, Mexico", + S2kModelNumber::AE2400US => "AE2400 Gen2D, US", + S2kModelNumber::VN2600PhilippinesGreen => "VN2600 Gen2D, Philippines (Green revision)", + S2kModelNumber::AE2600USPremium => "AE2600 Gen2B, Gen2C, Gen2D, US Premium", + S2kModelNumber::VN2600PhilippinesRed => { + "VN2600 Gen 2D, Philippines (Red revision - DISCONTINUED)" + } + S2kModelNumber::VN2500USReferenceV1 => "VN2500, US VER1 Reference", + S2kModelNumber::AE2600Saudi => "AE2600 Gen2D, Saudi", + S2kModelNumber::Reserved0 => "RFU", + S2kModelNumber::VN2500US => "VN2500 Gen2C, US", + S2kModelNumber::VN2500USReferenceV2 => "VN2500, US VER2 Reference", + S2kModelNumber::AE2800Brazil => "AE2800 Gen2D, Brazil", + S2kModelNumber::AE2800USExpanded => "AE2800 Gen2D, US Expanded", + S2kModelNumber::Reserved1 => "RFU", + S2kModelNumber::AE2600Indonesia => "AE2600 Gen2D, Indonesia", + S2kModelNumber::AE2600Croatia => "AE2600 Gen2D, Croatia", + S2kModelNumber::AE2600Israel => "AE2600 Gen2D, Israel", + S2kModelNumber::Reserved => "Reserved", + } + } +} + +impl From<&S2kModelNumber> for &'static str { + fn from(s: &S2kModelNumber) -> &'static str { + (*s).into() + } +} + +/// Model numbers for SC, SC Advanced, and SCR machines +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SCModelNumber { + SC8385 = 0x54, + SC66 = 0x55, + Reserved = 0xff, +} + +impl From for &'static str { + fn from(s: SCModelNumber) -> Self { + match s { + SCModelNumber::SC8385 => "Cashflow SC83 / Cashflow SC85 / SC Advance 83 (SCN83) / SC Advance 85 (SCN85) / SCR83", + SCModelNumber::SC66 => "Cashflow SC66 / SC Advance 66 (SCN66)", + _ => "Reserved", + } + } +} + +impl From<&SCModelNumber> for &'static str { + fn from(s: &SCModelNumber) -> Self { + (*s).into() + } +} + +impl From for SCModelNumber { + fn from(b: u8) -> Self { + match b { + 0x54 => Self::SC8385, + 0x55 => Self::SC66, + _ => Self::Reserved, + } + } +} + +impl From for SCModelNumber { + fn from(b: ModelNumber) -> Self { + b.0.into() + } +} + +impl From<&ModelNumber> for SCModelNumber { + fn from(b: &ModelNumber) -> Self { + (*b).into() + } +} + +bitfield! { + /// CodeRevision contains the code revision identifier. However, the version number of the code is not + /// sufficient to identify that software. This is because different software parts use independent version + /// numbers. Version numbers are only useful for comparing firmware from the same software part. + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct CodeRevision(u8); + u8; + /// The version number of the firmware code in the device. This may be coded as: + /// + ///- CFSC , SC Adv , SCR : A seven bit binary value with an implied + /// divide by 10. (versions 0.0 through 12.7). Example (0x23 = 35 + /// decimal. 35/10 = version 3.50) + /// + ///- S2K : A 3 and 4 digit BCD value with an implied divide by 10. + /// (versions 0.0 through 7.9). Ignoring the most significant bit, + /// the next 3 bits make up major build (x111 = 7). Last 4 bits + /// make up the minor revision (1001 = 9) + pub code_revision, set_code_revision: 6, 0; +} + +impl From for CodeRevision { + fn from(b: u8) -> Self { + Self(b & 0b111_1111) + } +} + +impl From for u8 { + fn from(c: CodeRevision) -> Self { + c.0 + } +} + +impl From<&CodeRevision> for u8 { + fn from(c: &CodeRevision) -> Self { + c.0 + } +} + +#[cfg(feature = "s2k")] +impl fmt::Display for CodeRevision { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", S2kCodeRevision(*self)) + } +} + +#[cfg(feature = "sc")] +impl fmt::Display for CodeRevision { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", SCCodeRevision(*self)) + } +} + +/// Wrapper around [CodeRevision](CodeRevision) for S2K-variant devices +pub struct S2kCodeRevision(CodeRevision); + +impl fmt::Display for S2kCodeRevision { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.0 .0 & 0b111_0000 >> 4, self.0 .0 & 0b1111) + } +} + +/// Wrapper around [CodeRevision](CodeRevision) for SC-variant devices +pub struct SCCodeRevision(CodeRevision); + +impl fmt::Display for SCCodeRevision { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", (self.0 .0 & 0b111_1111) as f32 / 10f32) + } +} diff --git a/src/status/document_status.rs b/src/status/document_status.rs new file mode 100644 index 0000000..ec2bc9a --- /dev/null +++ b/src/status/document_status.rs @@ -0,0 +1,294 @@ +use crate::std::fmt; + +use crate::{ + banknote::{BanknoteOrientation, NoteTableItem}, + denomination::StandardDenomination, +}; + +/// An accepted [NoteTableItem]. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AcceptedNoteTableItem { + note_table_item: NoteTableItem, + banknote_orientation: BanknoteOrientation, +} + +impl AcceptedNoteTableItem { + /// Creates a new [AcceptedNoteTableItem]. + pub const fn new( + note_table_item: NoteTableItem, + banknote_orientation: BanknoteOrientation, + ) -> Self { + Self { + note_table_item, + banknote_orientation, + } + } + + /// Creates a default [AcceptedNoteTableItem]. + pub const fn default() -> Self { + Self { + note_table_item: NoteTableItem::default(), + banknote_orientation: BanknoteOrientation::default(), + } + } + + /// Gets the [NoteTableItem]. + pub fn note_table_item(&self) -> &NoteTableItem { + &self.note_table_item + } + + /// Sets the [NoteTableItem], consumes and returns the [NoteTableItem]. + pub fn with_note_table_item(mut self, note_table_item: NoteTableItem) -> Self { + self.note_table_item = note_table_item; + self + } + + /// Gets the [BanknoteOrientation]. + pub fn banknote_orientation(&self) -> &BanknoteOrientation { + &self.banknote_orientation + } + + /// Sets the [BanknoteOrientation], consumes and returns the [BanknoteOrientation]. + pub fn with_banknote_orientation(mut self, banknote_orientation: BanknoteOrientation) -> Self { + self.banknote_orientation = banknote_orientation; + self + } +} + +impl fmt::Display for AcceptedNoteTableItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (item, orientation) = (self.note_table_item(), self.banknote_orientation()); + write!( + f, + "Note table item: {item}, Banknote orientation: {orientation}" + ) + } +} + +/// Values that represent document events. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DocumentEvent { + DispensedEvent = 0, + EscrowedEvent = 1, + RejectedEvent = 2, + RetrievedEvent = 3, + ReturnedEvent = 4, + StackedEvent = 5, + MissingNoteReportReadyEvent = 6, + EscrowSessionSummaryReportReadyEvent = 7, + NoneEvent = 8, +} + +impl DocumentEvent { + /// Creates a default [DocumentEvent]. + pub const fn default() -> Self { + Self::NoneEvent + } +} + +impl From for &'static str { + fn from(d: DocumentEvent) -> Self { + match d { + DocumentEvent::DispensedEvent => "Dispensed event", + DocumentEvent::EscrowedEvent => "Escrowed event", + DocumentEvent::RejectedEvent => "Rejected event", + DocumentEvent::RetrievedEvent => "Retrieved event", + DocumentEvent::ReturnedEvent => "Returned event", + DocumentEvent::StackedEvent => "Stacked event", + DocumentEvent::MissingNoteReportReadyEvent => "Missing note report ready event", + DocumentEvent::EscrowSessionSummaryReportReadyEvent => { + "Escrow session summary report ready event" + } + DocumentEvent::NoneEvent => "None event", + } + } +} + +impl From<&DocumentEvent> for &'static str { + fn from(d: &DocumentEvent) -> Self { + (*d).into() + } +} + +impl fmt::Display for DocumentEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +/// Values that represent document routing directions. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DocumentRouting { + NoRoute = 0, + EscrowToRecycler = 1, + RecyclerToCashbox = 2, + RecyclerToRecycler = 3, + RecyclerToCustomer = 4, + EscrowToEscrowStorage = 8, + CustomerToCashbox = 129, + EscrowStorageToInventory = 131, +} + +impl DocumentRouting { + /// Creates a default [DocumentRouting]. + pub const fn default() -> Self { + Self::NoRoute + } +} + +impl From for &'static str { + fn from(d: DocumentRouting) -> Self { + match d { + DocumentRouting::NoRoute => "No route", + DocumentRouting::EscrowToRecycler => "Escrow to recycler", + DocumentRouting::RecyclerToCashbox => "Recycler to cashbox", + DocumentRouting::RecyclerToRecycler => "Recycler to recycler", + DocumentRouting::RecyclerToCustomer => "Recycler to customer", + DocumentRouting::EscrowToEscrowStorage => "Escrow to escrow storage", + DocumentRouting::CustomerToCashbox => "Customer to cashbox", + DocumentRouting::EscrowStorageToInventory => "Escrow storage to inventory", + } + } +} + +impl From<&DocumentRouting> for &'static str { + fn from(d: &DocumentRouting) -> Self { + (*d).into() + } +} + +impl fmt::Display for DocumentRouting { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", <&'static str>::from(self)) + } +} + +/// A document status. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DocumentStatus { + /// The [DocumentEvent]. + document_event: DocumentEvent, + /// The [DocumentRouting]. + document_routing: DocumentRouting, + /// The [AcceptedNoteTableItem]. + accepted_note_table_item: AcceptedNoteTableItem, + /// The [StandardDenomination]. + standard_denomination: StandardDenomination, +} + +impl fmt::Display for DocumentStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (event, routing, item, denom) = ( + &self.document_event, + &self.document_routing, + &self.accepted_note_table_item, + &self.standard_denomination, + ); + + write!(f, "Document event: {event}, Document routing: {routing}, Accepted note table item: {item}, Standard denomination: {denom}") + } +} + +impl DocumentStatus { + /// Creates a new [DocumentStatus]. + pub const fn new( + document_event: DocumentEvent, + document_routing: DocumentRouting, + accepted_note_table_item: AcceptedNoteTableItem, + standard_denomination: StandardDenomination, + ) -> Self { + Self { + document_event, + document_routing, + accepted_note_table_item, + standard_denomination, + } + } + + /// Creates a default [DocumentStatus]. + pub const fn default() -> Self { + Self { + document_event: DocumentEvent::default(), + document_routing: DocumentRouting::default(), + accepted_note_table_item: AcceptedNoteTableItem::default(), + standard_denomination: StandardDenomination::none(), + } + } + + /// Gets the [DocumentEvent]. + pub fn document_event(&self) -> &DocumentEvent { + &self.document_event + } + + /// Sets the [DocumentEvent]. + pub fn set_document_event(&mut self, document_event: DocumentEvent) { + self.document_event = document_event; + } + + /// Sets the [DocumentEvent], consumes and returns the [DocumentStatus]. + pub fn with_document_event(mut self, document_event: DocumentEvent) -> Self { + self.document_event = document_event; + self + } + + /// Gets the [DocumentRouting]. + pub fn document_routing(&self) -> &DocumentRouting { + &self.document_routing + } + + /// Sets the [DocumentRouting]. + pub fn set_document_routing(&mut self, document_routing: DocumentRouting) { + self.document_routing = document_routing; + } + + /// Sets the [DocumentRouting], consumes and returns the [DocumentStatus]. + pub fn with_document_routing(mut self, document_routing: DocumentRouting) -> Self { + self.document_routing = document_routing; + self + } + + /// Gets the [AcceptedNoteTableItem]. + pub fn accepted_note_table_item(&self) -> &AcceptedNoteTableItem { + &self.accepted_note_table_item + } + + /// Sets the [AcceptedNoteTableItem]. + pub fn set_accepted_note_table_item( + &mut self, + accepted_note_table_item: AcceptedNoteTableItem, + ) { + self.accepted_note_table_item = accepted_note_table_item; + } + + /// Sets the [AcceptedNoteTableItem], consumes and returns the [DocumentStatus]. + pub fn with_accepted_note_table_item( + mut self, + accepted_note_table_item: AcceptedNoteTableItem, + ) -> Self { + self.accepted_note_table_item = accepted_note_table_item; + self + } + + /// Gets the [StandardDenomination]. + pub fn standard_denomination(&self) -> StandardDenomination { + self.standard_denomination + } + + /// Sets the [StandardDenomination]. + pub fn set_standard_denomination(&mut self, standard_denomination: StandardDenomination) { + self.standard_denomination = standard_denomination; + } + + /// Sets the [StandardDenomination], consumes and returns the [DocumentStatus]. + pub fn with_standard_denomination( + mut self, + standard_denomination: StandardDenomination, + ) -> Self { + self.standard_denomination = standard_denomination; + self + } +} diff --git a/src/variant.rs b/src/variant.rs new file mode 100644 index 0000000..e330f42 --- /dev/null +++ b/src/variant.rs @@ -0,0 +1,940 @@ +use crate::std; +use std::fmt; + +use super::*; + +/// Message reply variants for message building. +#[derive(Debug)] +pub enum MessageVariant { + // Omnibus reply + OmnibusReply(OmnibusReply), + // Extended replies + AdvancedBookmarkModeReply(AdvancedBookmarkModeReply), + ClearAuditDataRequestAck(ClearAuditDataRequestAck), + ClearAuditDataRequestResults(ClearAuditDataRequestResults), + ExtendedNoteReply(ExtendedNoteReply), + ExtendedNoteInhibitsReplyAlt(ExtendedNoteInhibitsReplyAlt), + NoteRetrievedReply(NoteRetrievedReply), + NoteRetrievedEvent(NoteRetrievedEvent), + QueryValueTableReply(QueryValueTableReply), + SetEscrowTimeoutReply(SetEscrowTimeoutReply), + // Aux replies + QuerySoftwareCrcReply(QuerySoftwareCrcReply), + QueryBootPartNumberReply(QueryBootPartNumberReply), + QueryApplicationPartNumberReply(QueryApplicationPartNumberReply), + QueryVariantNameReply(QueryVariantNameReply), + QueryVariantPartNumberReply(QueryVariantPartNumberReply), + QueryDeviceCapabilitiesReply(QueryDeviceCapabilitiesReply), + QueryApplicationIdReply(QueryApplicationIdReply), + QueryVariantIdReply(QueryVariantIdReply), + // Flash download replies + BaudRateChangeReply(BaudRateChangeReply), + FlashDownloadReply7bit(FlashDownloadReply7bit), + FlashDownloadReply8bit(FlashDownloadReply8bit), + StartDownloadReply(StartDownloadReply), +} + +impl MessageVariant { + /// Validates the [MessageVariant] checksum. + pub fn validate_checksum(&self) -> Result<()> { + self.as_message().validate_checksum() + } + + /// Gets the [MessageVariant] as a generic [MessageOps] implementation. + pub fn as_message(&self) -> &dyn MessageOps { + match self { + Self::AdvancedBookmarkModeReply(msg) => msg, + Self::ClearAuditDataRequestAck(msg) => msg, + Self::ClearAuditDataRequestResults(msg) => msg, + Self::ExtendedNoteReply(msg) => msg, + Self::ExtendedNoteInhibitsReplyAlt(msg) => msg, + Self::NoteRetrievedReply(msg) => msg, + Self::NoteRetrievedEvent(msg) => msg, + Self::OmnibusReply(msg) => msg, + Self::QueryValueTableReply(msg) => msg, + Self::SetEscrowTimeoutReply(msg) => msg, + Self::QuerySoftwareCrcReply(msg) => msg, + Self::QueryBootPartNumberReply(msg) => msg, + Self::QueryApplicationPartNumberReply(msg) => msg, + Self::QueryVariantNameReply(msg) => msg, + Self::QueryVariantPartNumberReply(msg) => msg, + Self::QueryDeviceCapabilitiesReply(msg) => msg, + Self::QueryApplicationIdReply(msg) => msg, + Self::QueryVariantIdReply(msg) => msg, + Self::BaudRateChangeReply(msg) => msg, + Self::FlashDownloadReply7bit(msg) => msg, + Self::FlashDownloadReply8bit(msg) => msg, + Self::StartDownloadReply(msg) => msg, + } + } + + /// Gets the [MessageVariant] as a mutable generic [MessageOps] implementation. + pub fn as_message_mut(&mut self) -> &mut dyn MessageOps { + match self { + Self::AdvancedBookmarkModeReply(msg) => msg, + Self::ClearAuditDataRequestAck(msg) => msg, + Self::ClearAuditDataRequestResults(msg) => msg, + Self::ExtendedNoteReply(msg) => msg, + Self::ExtendedNoteInhibitsReplyAlt(msg) => msg, + Self::NoteRetrievedReply(msg) => msg, + Self::NoteRetrievedEvent(msg) => msg, + Self::OmnibusReply(msg) => msg, + Self::QueryValueTableReply(msg) => msg, + Self::SetEscrowTimeoutReply(msg) => msg, + Self::QuerySoftwareCrcReply(msg) => msg, + Self::QueryBootPartNumberReply(msg) => msg, + Self::QueryApplicationPartNumberReply(msg) => msg, + Self::QueryVariantNameReply(msg) => msg, + Self::QueryVariantPartNumberReply(msg) => msg, + Self::QueryDeviceCapabilitiesReply(msg) => msg, + Self::QueryApplicationIdReply(msg) => msg, + Self::QueryVariantIdReply(msg) => msg, + Self::BaudRateChangeReply(msg) => msg, + Self::FlashDownloadReply7bit(msg) => msg, + Self::FlashDownloadReply8bit(msg) => msg, + Self::StartDownloadReply(msg) => msg, + } + } + + /// Gets the [MessageVariant] as a generic [OmnibusReplyOps] implementation. + pub fn as_omnibus_reply(&self) -> &dyn OmnibusReplyOps { + match self { + Self::AdvancedBookmarkModeReply(msg) => msg, + Self::ClearAuditDataRequestAck(msg) => msg, + Self::ClearAuditDataRequestResults(msg) => msg, + Self::ExtendedNoteReply(msg) => msg, + Self::ExtendedNoteInhibitsReplyAlt(msg) => msg, + Self::NoteRetrievedReply(msg) => msg, + Self::NoteRetrievedEvent(msg) => msg, + Self::OmnibusReply(msg) => msg, + Self::QueryValueTableReply(msg) => msg, + Self::SetEscrowTimeoutReply(msg) => msg, + Self::QuerySoftwareCrcReply(msg) => msg, + Self::QueryBootPartNumberReply(msg) => msg, + Self::QueryApplicationPartNumberReply(msg) => msg, + Self::QueryVariantNameReply(msg) => msg, + Self::QueryVariantPartNumberReply(msg) => msg, + Self::QueryDeviceCapabilitiesReply(msg) => msg, + Self::QueryApplicationIdReply(msg) => msg, + Self::QueryVariantIdReply(msg) => msg, + Self::BaudRateChangeReply(msg) => msg, + Self::FlashDownloadReply7bit(msg) => msg, + Self::FlashDownloadReply8bit(msg) => msg, + Self::StartDownloadReply(msg) => msg, + } + } + + /// Gets the [MessageVariant] as a mutable generic [OmnibusReplyOps] implementation. + pub fn as_omnibus_reply_mut(&mut self) -> &mut dyn OmnibusReplyOps { + match self { + Self::AdvancedBookmarkModeReply(msg) => msg, + Self::ClearAuditDataRequestAck(msg) => msg, + Self::ClearAuditDataRequestResults(msg) => msg, + Self::ExtendedNoteReply(msg) => msg, + Self::ExtendedNoteInhibitsReplyAlt(msg) => msg, + Self::NoteRetrievedReply(msg) => msg, + Self::NoteRetrievedEvent(msg) => msg, + Self::OmnibusReply(msg) => msg, + Self::QueryValueTableReply(msg) => msg, + Self::SetEscrowTimeoutReply(msg) => msg, + Self::QuerySoftwareCrcReply(msg) => msg, + Self::QueryBootPartNumberReply(msg) => msg, + Self::QueryApplicationPartNumberReply(msg) => msg, + Self::QueryVariantNameReply(msg) => msg, + Self::QueryVariantPartNumberReply(msg) => msg, + Self::QueryDeviceCapabilitiesReply(msg) => msg, + Self::QueryApplicationIdReply(msg) => msg, + Self::QueryVariantIdReply(msg) => msg, + Self::BaudRateChangeReply(msg) => msg, + Self::FlashDownloadReply7bit(msg) => msg, + Self::FlashDownloadReply8bit(msg) => msg, + Self::StartDownloadReply(msg) => msg, + } + } + + pub fn into_omnibus_reply(self) -> OmnibusReply { + match self { + Self::AdvancedBookmarkModeReply(msg) => msg.into(), + Self::ClearAuditDataRequestAck(msg) => msg.into(), + Self::ClearAuditDataRequestResults(msg) => msg.into(), + Self::ExtendedNoteReply(msg) => msg.into(), + Self::ExtendedNoteInhibitsReplyAlt(msg) => msg.into(), + Self::NoteRetrievedReply(msg) => msg.into(), + Self::NoteRetrievedEvent(msg) => msg.into(), + Self::OmnibusReply(msg) => msg, + Self::QueryValueTableReply(msg) => msg.into(), + Self::SetEscrowTimeoutReply(msg) => msg.into(), + Self::QueryBootPartNumberReply(msg) => msg.into(), + Self::QueryApplicationPartNumberReply(msg) => msg.into(), + Self::QueryVariantNameReply(msg) => msg.into(), + Self::QueryVariantPartNumberReply(msg) => msg.into(), + Self::QueryDeviceCapabilitiesReply(msg) => msg.into(), + Self::QueryApplicationIdReply(msg) => msg.into(), + Self::QueryVariantIdReply(msg) => msg.into(), + _ => OmnibusReply::new(), + } + } + + /// Gets the [MessageVariant] as an [AdvancedBookmarkModeReply]. + pub fn as_advanced_bookmark_mode_reply(&self) -> Result<&AdvancedBookmarkModeReply> { + match self { + Self::AdvancedBookmarkModeReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected AdvancedBookmarkModeReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [AdvancedBookmarkModeReply]. + pub fn into_advanced_bookmark_mode_reply(self) -> Result { + match self { + Self::AdvancedBookmarkModeReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected AdvancedBookmarkModeReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [AdvancedBookmarkModeReply]. + pub fn is_advanced_bookmark_mode_reply(&self) -> bool { + matches!(self, Self::AdvancedBookmarkModeReply(_)) + } + + /// Gets the [MessageVariant] as an [QuerySoftwareCrcReply]. + pub fn as_query_software_crc_reply(&self) -> Result<&QuerySoftwareCrcReply> { + match self { + Self::QuerySoftwareCrcReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QuerySoftwareCrcReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QuerySoftwareCrcReply]. + pub fn into_query_software_crc_reply(self) -> Result { + match self { + Self::QuerySoftwareCrcReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QuerySoftwareCrcReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QuerySoftwareCrcReply]. + pub fn is_query_software_crc_reply(&self) -> bool { + matches!(self, Self::QuerySoftwareCrcReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryBootPartNumberReply]. + pub fn as_query_boot_part_number_reply(&self) -> Result<&QueryBootPartNumberReply> { + match self { + Self::QueryBootPartNumberReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryBootPartNumberReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryBootPartNumberReply]. + pub fn into_query_boot_part_number_reply(self) -> Result { + match self { + Self::QueryBootPartNumberReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryBootPartNumberReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryBootPartNumberReply]. + pub fn is_query_boot_part_number_reply(&self) -> bool { + matches!(self, Self::QueryBootPartNumberReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryApplicationPartNumberReply]. + pub fn as_query_application_part_number_reply( + &self, + ) -> Result<&QueryApplicationPartNumberReply> { + match self { + Self::QueryApplicationPartNumberReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryApplicationPartNumberReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryApplicationPartNumberReply]. + pub fn into_query_application_part_number_reply( + self, + ) -> Result { + match self { + Self::QueryApplicationPartNumberReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryApplicationPartNumberReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryApplicationPartNumberReply]. + pub fn is_query_application_part_number_reply(&self) -> bool { + matches!(self, Self::QueryApplicationPartNumberReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryVariantNameReply]. + pub fn as_query_variant_name_reply(&self) -> Result<&QueryVariantNameReply> { + match self { + Self::QueryVariantNameReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryVariantNameReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryVariantNameReply]. + pub fn into_query_variant_name_reply(self) -> Result { + match self { + Self::QueryVariantNameReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryVariantNameReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryVariantNameReply]. + pub fn is_query_variant_name_reply(&self) -> bool { + matches!(self, Self::QueryVariantNameReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryVariantPartNumberReply]. + pub fn as_query_variant_part_number_reply(&self) -> Result<&QueryVariantPartNumberReply> { + match self { + Self::QueryVariantPartNumberReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryVariantPartNumberReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryVariantPartNumberReply]. + pub fn into_query_variant_part_number_reply(self) -> Result { + match self { + Self::QueryVariantPartNumberReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryVariantPartNumberReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryVariantPartNumberReply]. + pub fn is_query_variant_part_number_reply(&self) -> bool { + matches!(self, Self::QueryVariantPartNumberReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryDeviceCapabilitiesReply]. + pub fn as_query_device_capabilities_reply(&self) -> Result<&QueryDeviceCapabilitiesReply> { + match self { + Self::QueryDeviceCapabilitiesReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryDeviceCapabilitiesReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryDeviceCapabilitiesReply]. + pub fn into_query_device_capabilities_reply(self) -> Result { + match self { + Self::QueryDeviceCapabilitiesReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryDeviceCapabilitiesReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryDeviceCapabilitiesReply]. + pub fn is_query_device_capabilities_reply(&self) -> bool { + matches!(self, Self::QueryDeviceCapabilitiesReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryApplicationIdReply]. + pub fn as_query_application_id_reply(&self) -> Result<&QueryApplicationIdReply> { + match self { + Self::QueryApplicationIdReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryApplicationIdReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryApplicationIdReply]. + pub fn into_query_application_id_reply(self) -> Result { + match self { + Self::QueryApplicationIdReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryApplicationIdReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryApplicationIdReply]. + pub fn is_query_application_id_reply(&self) -> bool { + matches!(self, Self::QueryApplicationIdReply(_)) + } + + /// Gets the [MessageVariant] as an [QueryVariantIdReply]. + pub fn as_query_variant_id_reply(&self) -> Result<&QueryVariantIdReply> { + match self { + Self::QueryVariantIdReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryVariantIdReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryVariantIdReply]. + pub fn into_query_variant_id_reply(self) -> Result { + match self { + Self::QueryVariantIdReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryVariantIdReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [QueryVariantIdReply]. + pub fn is_query_variant_id_reply(&self) -> bool { + matches!(self, Self::QueryVariantIdReply(_)) + } + + /// Gets the [MessageVariant] as an [NoteRetrievedReply]. + pub fn as_note_retrieved_reply(&self) -> Result<&NoteRetrievedReply> { + match self { + Self::NoteRetrievedReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected NoteRetrievedReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [NoteRetrievedReply]. + pub fn into_note_retrieved_reply(self) -> Result { + match self { + Self::NoteRetrievedReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected NoteRetrievedReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [NoteRetrievedReply]. + pub fn is_note_retrieved_reply(&self) -> bool { + matches!(self, Self::NoteRetrievedReply(_)) + } + + /// Gets the [MessageVariant] as an [NoteRetrievedEvent]. + pub fn as_note_retrieved_event(&self) -> Result<&NoteRetrievedEvent> { + match self { + Self::NoteRetrievedEvent(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected NoteRetrievedEvent, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [NoteRetrievedEvent]. + pub fn into_note_retrieved_event(self) -> Result { + match self { + Self::NoteRetrievedEvent(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected NoteRetrievedEvent, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [NoteRetrievedEvent]. + pub fn is_note_retrieved_event(&self) -> bool { + matches!(self, Self::NoteRetrievedEvent(_)) + } + + /// Gets the [MessageVariant] as an [ExtendedNoteReply]. + pub fn as_extended_note_reply(&self) -> Result<&ExtendedNoteReply> { + match self { + Self::ExtendedNoteReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ExtendedNoteReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [ExtendedNoteReply]. + pub fn into_extended_note_reply(self) -> Result { + match self { + Self::ExtendedNoteReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ExtendedNoteReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [ExtendedNoteReply]. + pub fn is_extended_note_reply(&self) -> bool { + matches!(self, Self::ExtendedNoteReply(_)) + } + + /// Gets the [MessageVariant] as a [ClearAuditDataRequestAck] message. + pub fn as_clear_audit_data_request_ack(&self) -> Result<&ClearAuditDataRequestAck> { + match self { + Self::ClearAuditDataRequestAck(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ClearAuditDataRequestAck, MessageVariant is {self}" + ))), + } + } + + /// Gets the [MessageVariant] as a [ClearAuditDataRequestResults] message. + pub fn as_clear_audit_data_request_results(&self) -> Result<&ClearAuditDataRequestResults> { + match self { + Self::ClearAuditDataRequestResults(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ClearAuditDataRequestResults, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into a [ClearAuditDataRequestResults] message. + pub fn into_clear_audit_data_request_results(self) -> Result { + match self { + Self::ClearAuditDataRequestResults(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ClearAuditDataRequestResults, MessageVariant is {self}" + ))), + } + } + + /// Gets the [MessageVariant] as an [QueryValueTableReply]. + pub fn as_query_value_table_reply(&self) -> Result<&QueryValueTableReply> { + match self { + Self::QueryValueTableReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryValueTableReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [QueryValueTableReply]. + pub fn into_query_value_table_reply(self) -> Result { + match self { + Self::QueryValueTableReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected QueryValueTableReply, MessageVariant is {self}" + ))), + } + } + + /// Gets the [MessageVariant] as an [SetEscrowTimeoutReply]. + pub fn as_set_escrow_timeout_reply(&self) -> Result<&SetEscrowTimeoutReply> { + match self { + Self::SetEscrowTimeoutReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected SetEscrowTimeoutReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [SetEscrowTimeoutReply]. + pub fn into_set_escrow_timeout_reply(self) -> Result { + match self { + Self::SetEscrowTimeoutReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected SetEscrowTimeoutReply, MessageVariant is {self}" + ))), + } + } + + /// Gets the [MessageVariant] as an [ExtendedNoteInhibitsReplyAlt]. + pub fn as_extended_note_inhibits_reply(&self) -> Result<&ExtendedNoteInhibitsReplyAlt> { + match self { + Self::ExtendedNoteInhibitsReplyAlt(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ExtendedNoteInhibitsReplyAlt, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [ExtendedNoteInhibitsReplyAlt]. + pub fn into_extended_note_inhibits_reply(self) -> Result { + match self { + Self::ExtendedNoteInhibitsReplyAlt(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected ExtendedNoteInhibitsReplyAlt, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [ExtendedNoteInhibitsReplyAlt]. + pub fn is_extended_note_inhibits_reply(&self) -> bool { + matches!(self, Self::ExtendedNoteInhibitsReplyAlt(_)) + } + + /// Gets the [MessageVariant] as an [BaudRateChangeReply]. + pub fn as_baud_rate_change_reply(&self) -> Result<&BaudRateChangeReply> { + match self { + Self::BaudRateChangeReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected BaudRateChangeReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [BaudRateChangeReply]. + pub fn into_baud_rate_change_reply(self) -> Result { + match self { + Self::BaudRateChangeReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected BaudRateChangeReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [BaudRateChangeReply]. + pub fn is_baud_rate_change_reply(&self) -> bool { + matches!(self, Self::BaudRateChangeReply(_)) + } + + /// Gets the [MessageVariant] as an [StartDownloadReply]. + pub fn as_start_download_reply(&self) -> Result<&StartDownloadReply> { + match self { + Self::StartDownloadReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected StartDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [StartDownloadReply]. + pub fn into_start_download_reply(self) -> Result { + match self { + Self::StartDownloadReply(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected StartDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [StartDownloadReply]. + pub fn is_start_download_reply(&self) -> bool { + matches!(self, Self::StartDownloadReply(_)) + } + + /// Gets the [MessageVariant] as an [FlashDownloadReply]. + pub fn as_flash_download_reply(&self) -> Result<&dyn FlashDownloadReply> { + match self { + Self::FlashDownloadReply7bit(msg) => Ok(msg), + Self::FlashDownloadReply8bit(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected FlashDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Gets the [MessageVariant] as an [FlashDownloadReply7bit]. + pub fn as_flash_download_reply_7bit(&self) -> Result<&FlashDownloadReply7bit> { + match self { + Self::FlashDownloadReply7bit(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected FlashDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Gets the [MessageVariant] as an [FlashDownloadReply7bit]. + pub fn as_flash_download_reply_8bit(&self) -> Result<&FlashDownloadReply8bit> { + match self { + Self::FlashDownloadReply8bit(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected FlashDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [FlashDownloadReply7bit]. + pub fn into_flash_download_reply_7bit(self) -> Result { + match self { + Self::FlashDownloadReply7bit(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected FlashDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Consumes and converts the [MessageVariant] into an [FlashDownloadReply8bit]. + pub fn into_flash_download_reply_8bit(self) -> Result { + match self { + Self::FlashDownloadReply8bit(msg) => Ok(msg), + _ => Err(Error::failure(format!( + "Expected FlashDownloadReply, MessageVariant is {self}" + ))), + } + } + + /// Gets whether the [MessageVariant] is an [FlashDownloadReply]. + pub fn is_flash_download_reply(&self) -> bool { + matches!( + self, + Self::FlashDownloadReply7bit(_) | Self::FlashDownloadReply8bit(_) + ) + } + + /// Gets whether the [MessageVariant] is an [FlashDownloadReply7bit]. + pub fn is_flash_download_reply_7bit(&self) -> bool { + matches!(self, Self::FlashDownloadReply7bit(_)) + } + + /// Gets whether the [MessageVariant] is an [FlashDownloadReply8bit]. + pub fn is_flash_download_reply_8bit(&self) -> bool { + matches!(self, Self::FlashDownloadReply8bit(_)) + } + + /// Converts a [MessageVariant] into a [Banknote](hal_common::banknote::Banknote). + pub fn into_banknote(&self) -> Result { + match self { + Self::ExtendedNoteReply(msg) => Ok(msg.into()), + Self::OmnibusReply(msg) => Ok(msg.into()), + _ => Err(Error::failure(format!("MessageVariant->Banknote conversion only implemented for ExtendedNoteReply, have: {self}"))), + } + } + + /// Converts the [MessageVariant] into a [DocumentStatus]. + pub fn document_status(&self) -> DocumentStatus { + if let Ok(extended_note) = self.as_extended_note_reply() { + extended_note.into() + } else { + self.as_omnibus_reply().into() + } + } + + /// Parses an Auxilliary command response from the provided buffer and command type. + /// + /// The command is provided to provide a better heuristic of what the response should be. + /// This is necessary because Aux commands do not include a subtype byte in the response like + /// Extended commands. Also, mutltiple response types share the same length, so the command + /// type from the sent message is the best guess for what the response should be. + pub fn from_aux_buf(buf: &[u8], command: AuxCommand) -> Result { + let msg_len = buf.len(); + + if !(len::MIN_MESSAGE..=len::MAX_MESSAGE).contains(&msg_len) { + return Err(Error::failure("invalid message length")); + } + + let msg_type: MessageType = Control::from(buf[index::CONTROL]).message_type().into(); + let exp_msg_type = MessageType::AuxCommand; + + if msg_type != exp_msg_type { + return Err(Error::failure( + "invalid message type: {msg_type}, expected: {exp_msg_type}", + )); + } + + match command { + AuxCommand::QuerySoftwareCrc => { + let mut msg = QuerySoftwareCrcReply::new(); + msg.from_buf(buf)?; + Ok(Self::QuerySoftwareCrcReply(msg)) + } + AuxCommand::QueryBootPartNumber => { + let mut msg = QueryBootPartNumberReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryBootPartNumberReply(msg)) + } + AuxCommand::QueryApplicationPartNumber => { + let mut msg = QueryApplicationPartNumberReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryApplicationPartNumberReply(msg)) + } + AuxCommand::QueryVariantName => { + let mut msg = QueryVariantNameReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryVariantNameReply(msg)) + } + AuxCommand::QueryVariantPartNumber => { + let mut msg = QueryVariantPartNumberReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryVariantPartNumberReply(msg)) + } + AuxCommand::QueryDeviceCapabilities => { + let mut msg = QueryDeviceCapabilitiesReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryDeviceCapabilitiesReply(msg)) + } + AuxCommand::QueryApplicationId => { + let mut msg = QueryApplicationIdReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryApplicationIdReply(msg)) + } + AuxCommand::QueryVariantId => { + let mut msg = QueryVariantIdReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryVariantIdReply(msg)) + } + _ => Err(Error::failure("invalid AuxCommand reply type")), + } + } + + /// Contructs a [MessageVariant] from a buffer + pub fn from_buf(buf: &[u8]) -> Result { + let msg_len = buf.len(); + + if !(len::MIN_MESSAGE..=len::MAX_MESSAGE).contains(&msg_len) { + return Err(Error::failure("invalid message length")); + } + + let control = Control::from(buf[index::CONTROL]); + let msg_type = MessageType::from(control.message_type()); + + match msg_type { + MessageType::OmnibusReply => { + let mut msg = OmnibusReply::new(); + msg.from_buf(buf)?; + Ok(Self::OmnibusReply(msg)) + } + MessageType::FirmwareDownload => match msg_len { + len::BAUD_CHANGE_REPLY => { + let mut msg = BaudRateChangeReply::new(); + msg.from_buf(buf)?; + Ok(Self::BaudRateChangeReply(msg)) + } + len::START_DOWNLOAD_REPLY => { + let mut msg = StartDownloadReply::new(); + msg.from_buf(buf)?; + Ok(Self::StartDownloadReply(msg)) + } + len::FLASH_DOWNLOAD_REPLY_7BIT => { + let mut msg = FlashDownloadReply7bit::new(); + msg.from_buf(buf)?; + Ok(Self::FlashDownloadReply7bit(msg)) + } + len::FLASH_DOWNLOAD_REPLY_8BIT => { + let mut msg = FlashDownloadReply8bit::new(); + msg.from_buf(buf)?; + Ok(Self::FlashDownloadReply8bit(msg)) + } + _ => Err(Error::failure(format!( + "unsupported FirmwareDownload reply message length: {msg_len}" + ))), + }, + MessageType::Extended => { + let raw_sub_type = buf[index::EXT_SUBTYPE]; + let sub_type = ExtendedCommand::from(raw_sub_type); + match sub_type { + ExtendedCommand::ClearAuditDataRequest => { + let cad_reply_diff = buf[10]; + if cad_reply_diff == 0x00 || cad_reply_diff == 0x01 { + // Acknowledgement will have a 0x00 or 0x01 value in the 10th index + let mut msg = ClearAuditDataRequestAck::new(); + msg.from_buf(buf)?; + Ok(Self::ClearAuditDataRequestAck(msg)) + } else if cad_reply_diff == 0x10 || cad_reply_diff == 0x11 { + // Results will have a 0x10 or 0x11 value in the 10th index + let mut msg = ClearAuditDataRequestResults::new(); + msg.from_buf(buf)?; + Ok(Self::ClearAuditDataRequestResults(msg)) + } else { + Err(Error::failure("invalid ClearAuditDataRequest reply type")) + } + } + ExtendedCommand::ExtendedNoteSpecification => { + let mut msg = ExtendedNoteReply::new(); + msg.from_buf(buf)?; + Ok(Self::ExtendedNoteReply(msg)) + } + ExtendedCommand::SetExtendedNoteInhibits => { + let mut msg = ExtendedNoteInhibitsReplyAlt::new(); + msg.from_buf(buf)?; + Ok(Self::ExtendedNoteInhibitsReplyAlt(msg)) + } + ExtendedCommand::QueryValueTable => { + let mut msg = QueryValueTableReply::new(); + msg.from_buf(buf)?; + Ok(Self::QueryValueTableReply(msg)) + } + ExtendedCommand::NoteRetrieved => { + use crate::note_retrieved::reply::index as nr_index; + + if buf.len() < len::NOTE_RETRIEVED_REPLY { + Err(Error::failure( + "invalid message length for a NoteRetrieved reply", + )) + } else { + let acknak_event = buf[nr_index::ACKNAK]; + match acknak_event { + 0x00 | 0x01 => { + let mut msg = NoteRetrievedReply::new(); + msg.from_buf(buf)?; + Ok(Self::NoteRetrievedReply(msg)) + } + 0x7f => { + let mut msg = NoteRetrievedEvent::new(); + msg.from_buf(buf)?; + Ok(Self::NoteRetrievedEvent(msg)) + } + _ => Err(Error::failure( + "invalid AckNak/Event value: 0x{acknak_event:x}", + )), + } + } + } + ExtendedCommand::AdvancedBookmark => { + let mut msg = AdvancedBookmarkModeReply::new(); + msg.from_buf(buf)?; + Ok(Self::AdvancedBookmarkModeReply(msg)) + } + _ => Err(Error::failure(format!( + "unsupported extended message type: {sub_type}, raw: 0x{raw_sub_type:x}" + ))), + } + } + // AuxCommands currently unsupported due to no reliable way to determine reply types + // without access to the command type. + _ => Err(Error::failure(format!( + "expected Omnibus or Extended reply types, received: {msg_type}" + ))), + } + } +} + +impl fmt::Display for MessageVariant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AdvancedBookmarkModeReply(msg) => write!(f, "AdvancedBookmarkModeReply({msg})"), + Self::ClearAuditDataRequestAck(msg) => write!(f, "ClearAuditDataRequestAck({msg})"), + Self::ClearAuditDataRequestResults(msg) => { + write!(f, "ClearAuditDataRequestResults({msg})") + } + Self::ExtendedNoteReply(msg) => write!(f, "ExtendedNoteReply({msg})"), + Self::ExtendedNoteInhibitsReplyAlt(msg) => { + write!(f, "ExtendedNoteInhibitsReplyAlt({msg})") + } + Self::NoteRetrievedReply(msg) => write!(f, "NoteRetrievedReply({msg})"), + Self::NoteRetrievedEvent(msg) => write!(f, "NoteRetrievedEvent({msg})"), + Self::OmnibusReply(msg) => write!(f, "OmnibusReply({msg})"), + Self::QueryValueTableReply(msg) => write!(f, "QueryValueTableReply({msg})"), + Self::SetEscrowTimeoutReply(msg) => write!(f, "SetEscrowTimeoutReply({msg})"), + Self::QuerySoftwareCrcReply(msg) => write!(f, "QuerySoftwareCrcReply({msg})"), + Self::QueryBootPartNumberReply(msg) => write!(f, "QueryBootPartNumberReply({msg})"), + Self::QueryApplicationPartNumberReply(msg) => { + write!(f, "QueryApplicationPartNumberReply({msg})") + } + Self::QueryVariantNameReply(msg) => write!(f, "QueryVariantNameReply({msg})"), + Self::QueryVariantPartNumberReply(msg) => { + write!(f, "QueryVariantPartNumberReply({msg})") + } + Self::QueryDeviceCapabilitiesReply(msg) => { + write!(f, "QueryDeviceCapabilitiesReply({msg})") + } + Self::QueryApplicationIdReply(msg) => write!(f, "QueryApplicationIdReply({msg})"), + Self::QueryVariantIdReply(msg) => write!(f, "QueryVariantIdReply({msg})"), + Self::BaudRateChangeReply(msg) => write!(f, "BaudRateChangeReply({msg})"), + Self::FlashDownloadReply7bit(msg) => write!(f, "FlashDownloadReply7bit({msg})"), + Self::FlashDownloadReply8bit(msg) => write!(f, "FlashDownloadReply8bit({msg})"), + Self::StartDownloadReply(msg) => write!(f, "StartDownloadReply({msg})"), + } + } +}