Compare commits

...
Author SHA1 Message Date
EmpirePhoenix 4002a886a5 add hardware reference notes and INA219 bit-banged I2C implementation 2026-08-13 22:23:27 +02:00
EmpirePhoenix c64d145a1a add USB CDC support, and implement self-check diagnostic 2026-08-13 22:23:15 +02:00
judge 58b3a97d73 lib-bms-protocol expose timestamp error and enable thiserror 2026-04-18 22:34:46 +02:00
judge 2d6cfdb031 lib-bms-protocol expose U26 error and use thiserror 2026-04-18 14:39:35 +02:00
judge 2744589110 Merge branch 'feature/base-type-error-correcting' 2026-04-12 22:30:48 +02:00
judge 71bfcd4cfd Merge branch 'feature/timestamp-store-transit-u52' into feature/base-type-error-correcting 2026-04-12 22:29:59 +02:00
judge 1dba2e85f2 Add Store/Transit_DateTimeUtc wrapper type for 52-bit timestamp 2026-04-12 22:28:59 +02:00
judge b3df9fedad basetypes i26 und u26 enable clone and disable warnings 2026-04-12 22:17:57 +02:00
judge 1c0ad62b79 add chrono dependency 2026-04-12 22:15:08 +02:00
judge 739065409c Merge branch 'feature/protocol-use-transit-u26' into feature/base-type-error-correcting 2026-03-26 22:29:46 +01:00
judge bf2ad52e0f Fix Transit_I26 implementation and complete protocol conversion 2026-03-26 22:26:59 +01:00
judge 2332b507b2 Replace u32::from with .into() for idiomatic Rust 2026-03-26 22:26:57 +01:00
judge 83f54d5953 Add specific ChecksumError variant to BmsProtocolError 2026-03-26 22:26:55 +01:00
judge 0437d30d04 Convert communication protocol to use Transit_U26 with built-in CRC calculation 2026-03-26 22:26:50 +01:00
11 changed files with 1155 additions and 118 deletions
Generated
+71 -53
View File
@@ -46,8 +46,8 @@ dependencies = [
"ch32-metapac 0.0.6", "ch32-metapac 0.0.6",
"embassy-executor", "embassy-executor",
"embassy-futures", "embassy-futures",
"embassy-sync 0.7.2", "embassy-sync",
"embassy-time 0.4.0", "embassy-time",
"embassy-usb", "embassy-usb",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"heapless", "heapless",
@@ -57,6 +57,7 @@ dependencies = [
"panic-halt", "panic-halt",
"qingke", "qingke",
"qingke-rt", "qingke-rt",
"static_cell",
"thiserror", "thiserror",
] ]
@@ -75,23 +76,24 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "ch32-hal" name = "ch32-hal"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ju6ge/ch32-hal?branch=feature%2Fi2c-slave-api#9b1df89f5e8de21c7daad0f22fa7186dcfcbecdf" source = "git+https://github.com/ju6ge/ch32-hal?branch=feature%2Fi2c-slave-api#dd285961d7c4604caf1f9cf3c306d4b0929e36c6"
dependencies = [ dependencies = [
"bitmaps", "bitmaps",
"ch32-metapac 0.1.0", "ch32-metapac 0.1.0",
"critical-section", "critical-section",
"embassy-futures", "embassy-futures",
"embassy-hal-internal", "embassy-hal-internal",
"embassy-sync 0.7.2", "embassy-sync",
"embassy-time 0.5.0", "embassy-time",
"embassy-time-driver", "embassy-time-driver",
"embassy-time-queue-utils", "embassy-time-queue-utils",
"embassy-usb-driver 0.2.0", "embassy-usb-driver",
"embedded-can", "embedded-can",
"embedded-hal 0.2.7", "embedded-hal 0.2.7",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-hal-async", "embedded-hal-async",
"embedded-hal-nb", "embedded-hal-nb",
"embedded-storage",
"futures", "futures",
"nb 1.1.0", "nb 1.1.0",
"proc-macro2", "proc-macro2",
@@ -115,12 +117,21 @@ dependencies = [
[[package]] [[package]]
name = "ch32-metapac" name = "ch32-metapac"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ch32-rs/ch32-metapac?rev=b1cbc7a98e43af3fd3170821654784e2c01cb26b#b1cbc7a98e43af3fd3170821654784e2c01cb26b" source = "git+https://github.com/ch32-rs/ch32-metapac?rev=7cd6fb70d07de3a987c63d0d8f5547bb8261e403#7cd6fb70d07de3a987c63d0d8f5547bb8261e403"
dependencies = [ dependencies = [
"riscv 0.11.1", "riscv 0.12.1",
"vcell", "vcell",
] ]
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "critical-section" name = "critical-section"
version = "1.2.0" version = "1.2.0"
@@ -229,21 +240,7 @@ checksum = "b7b2739fbcf6cd206ae08779c7d709087b16577d255f2ea4a45bc4bbbf305b3f"
dependencies = [ dependencies = [
"embassy-futures", "embassy-futures",
"embassy-net-driver", "embassy-net-driver",
"embassy-sync 0.7.2", "embassy-sync",
]
[[package]]
name = "embassy-sync"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049"
dependencies = [
"cfg-if",
"critical-section",
"embedded-io-async",
"futures-sink",
"futures-util",
"heapless",
] ]
[[package]] [[package]]
@@ -260,22 +257,6 @@ dependencies = [
"heapless", "heapless",
] ]
[[package]]
name = "embassy-time"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f820157f198ada183ad62e0a66f554c610cdcd1a9f27d4b316358103ced7a1f8"
dependencies = [
"cfg-if",
"critical-section",
"document-features",
"embassy-time-driver",
"embedded-hal 0.2.7",
"embedded-hal 1.0.0",
"embedded-hal-async",
"futures-util",
]
[[package]] [[package]]
name = "embassy-time" name = "embassy-time"
version = "0.5.0" version = "0.5.0"
@@ -286,6 +267,7 @@ dependencies = [
"critical-section", "critical-section",
"document-features", "document-features",
"embassy-time-driver", "embassy-time-driver",
"embassy-time-queue-utils",
"embedded-hal 0.2.7", "embedded-hal 0.2.7",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-hal-async", "embedded-hal-async",
@@ -313,28 +295,20 @@ dependencies = [
[[package]] [[package]]
name = "embassy-usb" name = "embassy-usb"
version = "0.3.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d0b882133fa684b9d4652351cd7aac5afe8a2c2bf4a7da59f442ff61087cda2" checksum = "dc4462e48b19a4f401a11901bdd981aab80c6a826608016a0bdc73cbbab31954"
dependencies = [ dependencies = [
"embassy-futures", "embassy-futures",
"embassy-net-driver-channel", "embassy-net-driver-channel",
"embassy-sync 0.6.2", "embassy-sync",
"embassy-usb-driver 0.1.1", "embassy-usb-driver",
"embedded-io-async",
"heapless", "heapless",
"ssmarshal", "ssmarshal",
"usbd-hid", "usbd-hid",
] ]
[[package]]
name = "embassy-usb-driver"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "340c5ce591ef58c6449e43f51d2c53efe1bf0bb6a40cbf80afa0d259c7d52c76"
dependencies = [
"embedded-io-async",
]
[[package]] [[package]]
name = "embassy-usb-driver" name = "embassy-usb-driver"
version = "0.2.0" version = "0.2.0"
@@ -403,6 +377,12 @@ dependencies = [
"embedded-io", "embedded-io",
] ]
[[package]]
name = "embedded-storage"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032"
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "0.3.6" version = "0.3.6"
@@ -513,6 +493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [ dependencies = [
"hash32", "hash32",
"portable-atomic",
"stable_deref_trait", "stable_deref_trait",
] ]
@@ -536,6 +517,7 @@ dependencies = [
name = "lib-bms-protocol" name = "lib-bms-protocol"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"thiserror", "thiserror",
] ]
@@ -620,6 +602,9 @@ name = "portable-atomic"
version = "1.13.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
dependencies = [
"critical-section",
]
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
@@ -713,6 +698,19 @@ dependencies = [
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
] ]
[[package]]
name = "riscv"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea8ff73d3720bdd0a97925f0bf79ad2744b6da8ff36be3840c48ac81191d7a7"
dependencies = [
"critical-section",
"embedded-hal 1.0.0",
"paste",
"riscv-macros 0.1.0",
"riscv-pac",
]
[[package]] [[package]]
name = "riscv" name = "riscv"
version = "0.15.0" version = "0.15.0"
@@ -722,10 +720,21 @@ dependencies = [
"critical-section", "critical-section",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"paste", "paste",
"riscv-macros", "riscv-macros 0.3.0",
"riscv-pac", "riscv-pac",
] ]
[[package]]
name = "riscv-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f265be5d634272320a7de94cea15c22a3bfdd4eb42eb43edc528415f066a1f25"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]] [[package]]
name = "riscv-macros" name = "riscv-macros"
version = "0.3.0" version = "0.3.0"
@@ -794,6 +803,15 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_cell"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
+3 -1
View File
@@ -10,5 +10,7 @@ rustflags = [
# runner = "gdb -q -x openocd.gdb" # runner = "gdb -q -x openocd.gdb"
# runner = "wlink -v flash" # runner = "wlink -v flash"
runner = "wlink -v flash --enable-sdi-print --watch-serial --erase"
runner = "wchisp flash"
#runner = "wlink -v flash --enable-sdi-print --watch-serial --erase"
# runner = "wlink -v flash" # runner = "wlink -v flash"
+10 -4
View File
@@ -14,14 +14,20 @@ ch32-hal = { git = "https://github.com/ju6ge/ch32-hal", branch = "feature/i2c-sl
], default-features = false } ], default-features = false }
embassy-executor = { version = "0.7.0", features = [ embassy-executor = { version = "0.7.0", features = [
"arch-riscv32", # arch-riscv32's WFI-based sleep races with the USB interrupt wake signal,
# so tasks awaiting USB (e.g. wait_connection/write_packet) can hang
# forever even though the device enumerates fine. arch-spin busy-polls
# instead of sleeping, sidestepping the race. See CAN_Sensor/Cargo.toml
# for the same fix on the same ch32-hal/embassy-executor combo.
"arch-spin",
"executor-thread", "executor-thread",
] } ] }
embassy-time = { version = "0.4.0" } embassy-time = { version = "0.5.0", features = ["generic-queue-8"] }
embassy-usb = { version = "0.3.0" } embassy-usb = { version = "0.5.1" }
embassy-futures = { version = "0.1.0" } embassy-futures = { version = "0.1.0" }
embassy-sync = { version = "0.7.2" } embassy-sync = { version = "0.7.2" }
static_cell = "2.1.1"
# This is okay because we should automatically use whatever ch32-hal uses # This is okay because we should automatically use whatever ch32-hal uses
qingke-rt = "*" qingke-rt = "*"
@@ -30,7 +36,7 @@ qingke = "*"
panic-halt = "1.0" panic-halt = "1.0"
embedded-hal = "1.0.0" embedded-hal = "1.0.0"
heapless = "0.8.0" heapless = { version = "0.8.0", features = ["portable-atomic-critical-section"] }
micromath = { version = "2.1.0", features = ["num-traits"] } micromath = { version = "2.1.0", features = ["num-traits"] }
ch32-metapac = { version = "0.0.6", features = ["ch32v203c8t6"] } ch32-metapac = { version = "0.0.6", features = ["ch32v203c8t6"] }
ina219 = "0.2.0" ina219 = "0.2.0"
+151
View File
@@ -0,0 +1,151 @@
# Hardware implementor's notes
Reference for anyone touching `firmware/src/*.rs`: what's wired to what, which
peripherals/pins are used and why, and the non-obvious dependency-version
constraints this firmware relies on. Written against the real, currently
maintained schematic at `/home/empire/workspace/PlantCtrl/Hardware/open-bms/bms/bms.kicad_sch`.
**The KiCad project checked into *this* repo (`board/bms/`) is a stale, older
board revision.** Its component references and pin assignments do not match
the hardware this firmware actually targets — do not use it as a source of
truth. Re-derive facts from the real schematic (e.g. via
`kicad-cli sch export netlist`) if anything below is in doubt.
Target chip: **CH32V203C8T6** (RISC-V, `riscv32imc-unknown-none-elf`), no
external crystal populated — clocked from HSI only (see RCC note below).
## Pin map
| Pin | Function | Notes |
|---|---|---|
| PA8 | D7 LED | Active-high, 2.2kΩ (R13) to GND on the cathode side |
| PB12 | D3 LED | Active-high, 2.2kΩ (R14) to GND |
| PB13 | D4 LED | Active-high, 2.2kΩ (R15) to GND |
| PB14 | D5 LED | Active-high, 2.2kΩ (R16) to GND |
| PB15 | D6 LED | Active-high, 2.2kΩ (R17) to GND |
| PA11 | USB D | Fixed-function USB pin on this chip, not remappable |
| PA12 | USB D+ | Fixed-function USB pin on this chip, not remappable |
| PA4 | SPI1 NSS (flash CS) | Driven manually as a GPIO `Output``ch32-hal`'s `Spi` has no CS management. Idle-high, matches the board's 10kΩ pull-up (R7) |
| PA5 | SPI1 SCK | Default/non-remapped SPI1 pin |
| PA6 | SPI1 MISO | Default/non-remapped SPI1 pin |
| PA7 | SPI1 MOSI | Default/non-remapped SPI1 pin |
| PB9 | INA219 SDA | **Bit-banged**, not hardware I2C — see gotcha below |
| PB10 | INA219 SCL | **Bit-banged**, not hardware I2C — see gotcha below |
| PB2 | Spare GPIO input | Pulled low via 10kΩ (R8), nothing else attached. Only real "input" available on this board |
| PB6 | I2C1 SDA (`S_SDA`) | Battery-pack-facing bus (XT30 connector pins 3/4, BAV99 protection diodes). Currently unused — see I2C1 note below |
| PB7 | I2C1 SCL (`S_SCL`) | Battery-pack-facing bus. Currently unused — see I2C1 note below |
Not wired to anything: PA0-3, PA9-10, PA15, PB0-1, PB3-5, PB8, PB11, PC13-15.
`Boot1`/`Reset1` are the BOOT0 strap and NRST reset buttons respectively —
not runtime-readable GPIOs, don't try to poll them as inputs.
## Peripherals in use
- **USBD** — USB CDC-ACM virtual serial console. `hal::usbd::Driver::new(p.USBD, Irqs, p.PA12 /*dp*/, p.PA11 /*dm*/)`. Interrupt vector is shared with CAN1: `bind_interrupts!` needs `USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>`. Max packet size is 64 bytes — `write_packet()` on a single call **fails** (`EndpointError::BufferOverflow`, does not panic) for anything longer; chunk before sending (see `usb_writer` in `main.rs`).
- **SPI1** — blocking, talks to the W25Q128JVE NOR flash (U5). `hal::spi::Spi::new_blocking::<0>(p.SPI1, sck, mosi, miso, config)` — note the argument order is `(sck, mosi, miso)`, not alphabetical. No hardware CS support in this driver; drive it yourself.
- **I2C1** — hardware peripheral, PB6/PB7 (remap 0). Intended for the battery-pack-facing slave bus (device acts as an I2C *slave*, address `0x55` in the existing but currently-commented-out code). **Not currently enabled**`I2c::listen_blocking()` is a genuine busy-spin with no `.await`, which starves every other embassy task (USB included) on this single-threaded executor the instant it's called. Don't re-enable it without either moving it off the main task's critical path or replacing it with a non-blocking variant.
- **I2C2** — hardware peripheral, fixed to PB10 (SCL) / PB11 (SDA), no remap. **Do not use for the INA219.** PB11 is completely unconnected on this board. See the bit-bang gotcha below.
- **TIM2** — claimed by `ch32-hal`'s `time-driver-tim2` feature as the embassy time driver's tick source. Don't reuse TIM2 directly for anything else.
## Hardware gotcha: the INA219 bus needs software (bit-banged) I2C
The schematic labels a bus "SCL"/"SDA" going to the INA219 (U3), wired to
PB10 and PB9. This looks like it should be a normal hardware I2C bus, but
**it isn't reachable by any single hardware I2C peripheral on this chip**:
- I2C2 is fixed to PB10=SCL, **PB11**=SDA (no remap available). PB11 is
unconnected on this board.
- I2C1 remap 1 is **PB8**=SCL, PB9=SDA. PB8 is unconnected on this board.
PB10 only pairs (in hardware) with PB11; PB9 only pairs with PB8. Neither
partner pin is wired to the sensor. This is a board wiring quirk, not
something fixable via `ch32-hal` configuration. `firmware/src/selfcheck.rs`
therefore implements a minimal bit-banged I2C driver (`BitbangI2c`) over
plain open-drain GPIOs (`hal::gpio::Flex`) on PB9/PB10, implementing
`embedded_hal::i2c::I2c` via its single required `transaction()` method (the
`read`/`write`/`write_read` provided methods come for free from that). It
does **not** support clock stretching — fine for the INA219, which doesn't
stretch, but keep that in mind if another device ever goes on this bus.
INA219 address: **0x48**`A0`→GND, `A1`→SDA on this board, i.e.
`ina219::address::Address::from_pins(Pin::Gnd, Pin::Sda)`. Not the more
common default addresses seen in INA219 examples/datasheet defaults.
Current-sense shunt: R4 ‖ R5, 100mΩ each → **50mΩ** combined. Current is
derived in software from `shunt_voltage_uv() / 50`, not from the INA219's
own calibration registers (`IntCalibration`) — this board's real max current
draw isn't documented anywhere, so guessing a `current_lsb` for hardware
calibration seemed worse than a simple, honest Ohm's-law calculation using
the known shunt value.
## RCC / clock configuration
`hal::init` is called with `rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI`,
**not** the HAL's default config. This is required for USB to get a valid
48MHz derived clock, and it's HSI-based because there's no external crystal
on this board (`OSC_IN`/`OSC_OUT` are unconnected). If you ever add code
that assumes a different clock tree, check this first.
## Dependency versions that matter (not arbitrary pins in `Cargo.toml`)
These aren't just "whatever version worked" — each one fixes a real,
previously-hit build or runtime failure. Don't casually bump/change them
without re-verifying:
- **`embassy-executor` features = `["arch-spin", "executor-thread"]`** — NOT
`arch-riscv32`. `arch-riscv32`'s WFI-based sleep races with the USB
interrupt's wake signal: if the interrupt fires right as the core is about
to sleep, the wake can be missed and anything `.await`ing on it (e.g.
`wait_connection()`/`write_packet()`) hangs forever, even though the
device still enumerates fine. `arch-spin` busy-polls instead of sleeping,
sidestepping the race. (Symptom if this regresses: USB enumerates, but no
console output ever appears.) Confirmed the same fix is needed in the
sibling `PlantCtrl/Software/CAN_Sensor` project on the same HAL fork.
- **`embassy-time` features = `["generic-queue-8"]`, version `"0.5.0"`** —
needs to match the `embassy-time` version `ch32-hal` itself pulls in
transitively (currently 0.5.x); a direct-dependency version mismatch
causes a Cargo `links` conflict (`embassy-time-queue-utils` can't resolve
two different versions simultaneously). The `generic-queue-8` feature
makes `embassy-time` use its own self-contained timer queue instead of
requiring `embassy-executor` to implement an "integrated timer queue"
symbol that our pinned `embassy-executor 0.7.0` doesn't provide (that
support landed in later `embassy-executor` versions).
- **`embassy-usb = "0.5.1"`** — NOT 0.3.0/0.4.0. Those depend on
`embassy-usb-driver ^0.1.0`; `ch32-hal`'s `hal::usbd::Driver` implements
the `embassy-usb-driver 0.2.0` traits. `embassy-usb` versions ≥0.5.0 are
the first to depend on `embassy-usb-driver ^0.2.0`. Using an older
`embassy-usb` here is a hard type-check failure, not a subtle bug.
- **`heapless` feature = `"portable-atomic-critical-section"`** — needed for
`static_cell` (used for the `'static` USB buffers/class/device via the
`mk_static!` macro in `main.rs`) to build at all. `riscv32imc` has no
native atomic compare-and-swap instruction; without this feature,
`static_cell`'s internal `AtomicBool::compare_exchange` fails to compile
for this target. This feature makes `portable-atomic` (a shared transitive
dependency) emulate CAS via critical sections instead.
- **`ch32-hal`** — git dependency on `ju6ge/ch32-hal`, branch
`feature/i2c-slave-api` (upstream `ch32-rs/ch32-hal` does **not** have the
I2C slave-mode API this firmware's (currently disabled) I2C1 code depends
on — checked directly, no `SlaveConfig`/`listen_blocking` there). This
branch has been force-pushed/rebased before (the previously-locked commit
disappeared from GitHub entirely) — if `cargo build` ever fails to fetch
the pinned commit in `Cargo.lock`, that's almost certainly why; re-run
`cargo update -p ch32-hal` to pick up the branch's current tip.
- Entry point is `qingke_rt::entry` (via
`#[embassy_executor::main(entry = "qingke_rt::entry")]`), matching
`ch32-hal`'s own examples for this chip family.
## Logging
`println!` in `main.rs` is a **local macro**, not `ch32_hal::println!` — the
latter writes over WCH's SDI single-wire debug protocol (needs a WCH-Link
probe attached, see `hal`'s `debug.rs`). The local one formats into a
`heapless::String<128>` and pushes it onto a channel (`LOG_CH`) that a
spawned `usb_writer` task drains and writes out over the USB CDC-ACM
console. It's `#[macro_export]`ed with `$crate::LOG_CH` internally so other
modules (e.g. `selfcheck.rs`) can call `crate::println!(...)` and have it
land in the same place. Sends are non-blocking (`try_send`) and silently
drop the line if the channel (capacity 8) is full — there's no backpressure,
so a burst of many `println!` calls in a tight loop with no `.await` in
between can lose messages if nothing's draining the channel yet (e.g. no
terminal connected). USB CDC devices also re-enumerate on every reflash;
most terminal programs need to be manually reconnected afterward.
+159 -29
View File
@@ -4,24 +4,53 @@
#![feature(impl_trait_in_assoc_type)] #![feature(impl_trait_in_assoc_type)]
mod measurement; mod measurement;
mod selfcheck;
use ch32_hal::i2c::mode::OperatingMode; use ch32_hal::i2c::mode::OperatingMode;
use ch32_hal::mode::Mode; use ch32_hal::mode::Mode;
use ch32_hal::println; use ch32_hal::peripherals::USBD;
use ch32_hal::time::Hertz; use core::fmt::Write as _;
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}; use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
use embassy_sync::channel::Channel;
use embassy_sync::lazy_lock::LazyLock; use embassy_sync::lazy_lock::LazyLock;
use embassy_sync::mutex::Mutex; use embassy_sync::mutex::Mutex;
use embassy_sync::rwlock::RwLock; use embassy_sync::rwlock::RwLock;
//use embassy_time::{Duration, Timer}; use embassy_time::Timer;
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
use embassy_usb::{Builder, UsbDevice};
use hal::bind_interrupts; use hal::bind_interrupts;
use hal::i2c::{Config, I2c, SlaveAddress, SlaveConfig}; use hal::gpio::{Flex, Input, Level, Output, Pull};
use hal::i2c::I2c;
use hal::spi::Spi;
use hal::usbd::Driver;
use {ch32_hal as hal, panic_halt as _}; use {ch32_hal as hal, panic_halt as _};
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
pub(crate) static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
/// Formats like `std::println!`, but sends the line over USB CDC instead of stdout.
#[macro_export]
macro_rules! println {
($($arg:tt)*) => {{
let mut s: heapless::String<128> = heapless::String::new();
let _ = core::write!(&mut s, $($arg)*);
let _ = $crate::LOG_CH.try_send(s);
}};
}
bind_interrupts!(struct Irqs { bind_interrupts!(struct Irqs {
I2C1_EV => ch32_hal::i2c::EventInterruptHandler<ch32_hal::peripherals::I2C1>; I2C1_EV => ch32_hal::i2c::EventInterruptHandler<ch32_hal::peripherals::I2C1>;
I2C1_ER => ch32_hal::i2c::ErrorInterruptHandler<ch32_hal::peripherals::I2C1>; I2C1_ER => ch32_hal::i2c::ErrorInterruptHandler<ch32_hal::peripherals::I2C1>;
USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>;
}); });
const MEASUREMENT_SAMPLES_CAPACITY: usize = 1024; const MEASUREMENT_SAMPLES_CAPACITY: usize = 1024;
@@ -38,36 +67,137 @@ async fn measurement_task<'m, M: Mode, O: OperatingMode>(
} }
#[embassy_executor::main(entry = "qingke_rt::entry")] #[embassy_executor::main(entry = "qingke_rt::entry")]
async fn main(_spawner: Spawner) -> ! { async fn main(spawner: Spawner) {
let p = hal::init(Default::default()); let p = hal::init(hal::Config {
rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI,
..Default::default()
});
let config = Config::default(); // D7 startup blink so we can visually confirm the board booted before it
let mut i2c_slave = I2c::new_blocking(p.I2C1, p.PB6, p.PB7, Hertz::khz(100), config) // goes quiet waiting on the I2C bus.
.into_slave(SlaveConfig { let mut led = Output::new(p.PA8, Level::Low, Default::default());
address: SlaveAddress::SevenBit(0x55), for _ in 0..5 {
general_call: false, led.toggle();
}); Timer::after_millis(200).await;
}
led.set_low();
// USB CDC-ACM console: lets us `println!` self-test output over the USB
// cable instead of needing a WCH-Link SWD probe attached.
let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11);
let mut usb_config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
usb_config.manufacturer = Some("ch32-bms");
usb_config.product = Some("BMS debug console");
usb_config.serial_number = Some("12345678");
usb_config.max_power = 100;
usb_config.max_packet_size_0 = 64;
// Windows compatibility requires these; CDC-ACM
usb_config.device_class = 0x02;
usb_config.device_sub_class = 0x02;
usb_config.device_protocol = 0x00;
usb_config.composite_with_iads = false;
let mut builder = Builder::new(
driver,
usb_config,
mk_static!([u8; 256], [0; 256]),
mk_static!([u8; 256], [0; 256]),
&mut [], // no msos descriptors
mk_static!([u8; 64], [0; 64]),
);
let class = mk_static!(
CdcAcmClass<'static, Driver<'static, USBD>>,
CdcAcmClass::new(&mut builder, mk_static!(State, State::new()), 64)
);
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build());
spawner.spawn(usb_task(usb)).unwrap();
spawner.spawn(usb_writer(class)).unwrap();
println!("BMS booted, D7 blink done, USB console up");
// Power-on self-check: exercises the other LEDs, the spare input pin,
// the INA219 current sensor, and the SPI flash, reporting over USB.
let mut led_d3 = Output::new(p.PB12, Level::Low, Default::default());
let mut led_d4 = Output::new(p.PB13, Level::Low, Default::default());
let mut led_d5 = Output::new(p.PB14, Level::Low, Default::default());
let mut led_d6 = Output::new(p.PB15, Level::Low, Default::default());
let input_pb2 = Input::new(p.PB2, Pull::None);
let mut spi = Spi::new_blocking::<0>(p.SPI1, p.PA5, p.PA7, p.PA6, Default::default());
let mut flash_cs = Output::new(p.PA4, Level::High, Default::default());
let mut i2c_scl = Flex::new(p.PB10);
let mut i2c_sda = Flex::new(p.PB9);
loop { loop {
match i2c_slave.listen_blocking() { selfcheck::run(
Ok(command) => { &mut led,
match command { &mut led_d3,
ch32_hal::i2c::SlaveCommand::GeneralCall => { /* will not be triggered because we disabled it */ &mut led_d4,
} &mut led_d5,
ch32_hal::i2c::SlaveCommand::ReadCommand => { &mut led_d6,
// send empty response &input_pb2,
let _ = i2c_slave.blocking_write_timeout(&[0x05, 0x01]); &mut spi,
} &mut flash_cs,
ch32_hal::i2c::SlaveCommand::WriteCommand => { &mut i2c_scl,
let mut buf: [u8; 3] = [0x0, 0x0, 0x0]; &mut i2c_sda,
let _ = i2c_slave.blocking_read_timeout(&mut buf); )
println!("received byte 0x{buf:x?}"); .await;
} Timer::after_secs(5).await;
}
// I2C slave listen loop disabled for now: it busy-waits with no `.await`,
// which would starve the USB tasks above on this single-threaded executor.
// let config = hal::i2c::Config::default();
// let mut i2c_slave = I2c::new_blocking(p.I2C1, p.PB6, p.PB7, hal::time::Hertz::khz(100), config)
// .into_slave(hal::i2c::SlaveConfig {
// address: hal::i2c::SlaveAddress::SevenBit(0x55),
// general_call: false,
// });
//
// loop {
// match i2c_slave.listen_blocking() {
// Ok(command) => {
// match command {
// ch32_hal::i2c::SlaveCommand::GeneralCall => { /* will not be triggered because we disabled it */
// }
// ch32_hal::i2c::SlaveCommand::ReadCommand => {
// // send empty response
// let _ = i2c_slave.blocking_write_timeout(&[0x05, 0x01]);
// }
// ch32_hal::i2c::SlaveCommand::WriteCommand => {
// let mut buf: [u8; 3] = [0x0, 0x0, 0x0];
// let _ = i2c_slave.blocking_read_timeout(&mut buf);
// println!("received byte 0x{buf:x?}");
// }
// }
// }
// Err(err) => {
// println!("{:?}", err)
// }
// }
// }
}
#[embassy_executor::task]
async fn usb_task(usb: &'static mut UsbDevice<'static, Driver<'static, USBD>>) {
usb.run().await;
}
#[embassy_executor::task]
async fn usb_writer(class: &'static mut CdcAcmClass<'static, Driver<'static, USBD>>) {
loop {
class.wait_connection().await;
'connected: loop {
let msg = LOG_CH.receive().await;
// write_packet() fails outright for any single write over the
// endpoint's max packet size (64 bytes here), so chunk it.
for chunk in msg.as_bytes().chunks(64) {
if class.write_packet(chunk).await.is_err() {
// Disconnected or endpoint disabled; wait for reconnection.
break 'connected;
} }
} }
Err(err) => {
println!("{:?}", err)
}
} }
} }
} }
-1
View File
@@ -1,5 +1,4 @@
use embassy_usb::msos::ConfigurationSubsetHeader;
use lib_bms_protocol::BatteryState; use lib_bms_protocol::BatteryState;
use ina219::{address::Address, calibration::{IntCalibration, MicroAmpere, MicroWatt, UnCalibrated}, configuration::Configuration, errors::{ConfigurationReadError, InitializationError}, measurements::{CurrentRegister, Measurements, PowerRegister}, SyncIna219}; use ina219::{address::Address, calibration::{IntCalibration, MicroAmpere, MicroWatt, UnCalibrated}, configuration::Configuration, errors::{ConfigurationReadError, InitializationError}, measurements::{CurrentRegister, Measurements, PowerRegister}, SyncIna219};
+219
View File
@@ -0,0 +1,219 @@
//! Power-on self-check: exercises every external component on the board and
//! reports pass/fail plus readings over the USB console.
use core::fmt::Write as _;
use embassy_time::Timer;
use embedded_hal::i2c::{ErrorKind, ErrorType, I2c, NoAcknowledgeSource, Operation};
use hal::gpio::{Flex, Input, Output, Speed};
use hal::mode::Blocking;
use hal::spi::Spi;
use ina219::address::{Address, Pin as Ina219Pin};
use ina219::SyncIna219;
use {ch32_hal as hal, ch32_hal::peripherals::SPI1};
/// Bit-banged I2C on two plain GPIOs.
///
/// The INA219's I2C bus is wired to PB9 (SDA) and PB10 (SCL) on this board,
/// but those two pins belong to *different* hardware I2C peripherals (PB10 is
/// I2C2's SCL, whose SDA partner PB11 is unconnected here; PB9 is only valid
/// as I2C1's SDA under pin-remap, whose SCL partner PB8 is also unconnected).
/// No single hardware I2C instance can reach both pins as wired, so this bus
/// has to be driven as plain open-drain GPIOs instead. No clock stretching
/// support, same as most minimal bit-bang I2C implementations.
pub struct BitbangI2c<'a, 'd> {
scl: &'a mut Flex<'d>,
sda: &'a mut Flex<'d>,
}
#[derive(Debug)]
pub struct BitbangI2cError;
impl embedded_hal::i2c::Error for BitbangI2cError {
fn kind(&self) -> ErrorKind {
ErrorKind::NoAcknowledge(NoAcknowledgeSource::Unknown)
}
}
impl<'a, 'd> BitbangI2c<'a, 'd> {
pub fn new(scl: &'a mut Flex<'d>, sda: &'a mut Flex<'d>) -> Self {
scl.set_as_output_open_drain(Speed::Low);
sda.set_as_output_open_drain(Speed::Low);
scl.set_high();
sda.set_high();
let this = Self { scl, sda };
Self::half_delay();
this
}
fn half_delay() {
// ~10us at 144MHz -> ~50kHz bit-bang clock, well within I2C standard mode.
qingke::riscv::asm::delay(1500);
}
fn start(&mut self) {
self.sda.set_high();
self.scl.set_high();
Self::half_delay();
self.sda.set_low();
Self::half_delay();
self.scl.set_low();
Self::half_delay();
}
fn stop(&mut self) {
self.sda.set_low();
Self::half_delay();
self.scl.set_high();
Self::half_delay();
self.sda.set_high();
Self::half_delay();
}
fn write_bit(&mut self, bit: bool) {
if bit {
self.sda.set_high();
} else {
self.sda.set_low();
}
Self::half_delay();
self.scl.set_high();
Self::half_delay();
self.scl.set_low();
Self::half_delay();
}
fn read_bit(&mut self) -> bool {
self.sda.set_high(); // release so the slave can drive it
Self::half_delay();
self.scl.set_high();
Self::half_delay();
let bit = self.sda.is_high();
self.scl.set_low();
Self::half_delay();
bit
}
fn write_byte(&mut self, byte: u8) -> Result<(), BitbangI2cError> {
for i in (0..8).rev() {
self.write_bit((byte >> i) & 1 != 0);
}
if self.read_bit() {
// NACK
Err(BitbangI2cError)
} else {
Ok(())
}
}
fn read_byte(&mut self, ack: bool) -> u8 {
let mut byte = 0u8;
for _ in 0..8 {
byte = (byte << 1) | u8::from(self.read_bit());
}
self.write_bit(!ack);
byte
}
}
impl<'a, 'd> ErrorType for BitbangI2c<'a, 'd> {
type Error = BitbangI2cError;
}
impl<'a, 'd> I2c for BitbangI2c<'a, 'd> {
fn transaction(&mut self, address: u8, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
self.start();
let mut last_read: Option<bool> = None;
for op in operations.iter_mut() {
let is_read = matches!(op, Operation::Read(_));
if last_read != Some(is_read) {
if last_read.is_some() {
self.start(); // repeated start
}
self.write_byte((address << 1) | u8::from(is_read))?;
}
match op {
Operation::Write(data) => {
for &b in data.iter() {
self.write_byte(b)?;
}
}
Operation::Read(buf) => {
let len = buf.len();
for (i, b) in buf.iter_mut().enumerate() {
*b = self.read_byte(i + 1 != len);
}
}
}
last_read = Some(is_read);
}
self.stop();
Ok(())
}
}
/// Current-sense shunt on this board: R4 (100mOhm) parallel with R5 (100mOhm).
const SHUNT_MILLIOHM: i32 = 50;
#[allow(clippy::too_many_arguments)]
pub async fn run<'d>(
d7: &mut Output<'d>,
d3: &mut Output<'d>,
d4: &mut Output<'d>,
d5: &mut Output<'d>,
d6: &mut Output<'d>,
input_pb2: &Input<'d>,
spi: &mut Spi<'d, SPI1, Blocking>,
flash_cs: &mut Output<'d>,
i2c_scl: &mut Flex<'d>,
i2c_sda: &mut Flex<'d>,
) {
crate::println!("=== self-check start ===");
crate::println!("-- LED outputs --");
let leds: [(&str, &mut Output<'d>); 5] = [("D3", d3), ("D4", d4), ("D5", d5), ("D6", d6), ("D7", d7)];
for (name, led) in leds {
crate::println!(" {name}: on");
led.set_high();
Timer::after_millis(150).await;
led.set_low();
}
crate::println!("-- input --");
crate::println!(" PB2: {}", if input_pb2.is_high() { "high" } else { "low" });
crate::println!("-- current sensor (INA219 via bit-banged I2C on PB9/PB10) --");
let i2c = BitbangI2c::new(i2c_scl, i2c_sda);
match SyncIna219::new(i2c, Address::from_pins(Ina219Pin::Gnd, Ina219Pin::Sda)) {
Ok(mut dev) => match (dev.bus_voltage(), dev.shunt_voltage()) {
(Ok(bus), Ok(shunt)) => {
let bus_mv = bus.voltage_mv();
let shunt_uv = shunt.shunt_voltage_uv();
let current_ma = shunt_uv / SHUNT_MILLIOHM;
crate::println!(
" PASS: bus={bus_mv}mV shunt={shunt_uv}uV current={current_ma}mA (shunt={SHUNT_MILLIOHM}mOhm)"
);
}
_ => crate::println!(" FAIL: INA219 responded but a measurement read failed"),
},
Err(_) => crate::println!(" FAIL: INA219 not responding (addr 0x48)"),
}
crate::println!("-- SPI flash (W25Q128, JEDEC ID) --");
flash_cs.set_low();
let mut buf: [u8; 4] = [0x9F, 0, 0, 0];
let result = spi.blocking_transfer_in_place(&mut buf);
flash_cs.set_high();
match result {
Ok(()) => {
let [_, mfg, mem_type, capacity] = buf;
if mfg == 0xEF {
crate::println!(" PASS: JEDEC ID {mfg:02x} {mem_type:02x} {capacity:02x}");
} else {
crate::println!(" FAIL: unexpected JEDEC ID {mfg:02x} {mem_type:02x} {capacity:02x}");
}
}
Err(_) => crate::println!(" FAIL: SPI transfer error"),
}
crate::println!("=== self-check done ===");
}
+1
View File
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
chrono = { version = "0.4.44", default-features = false }
embedded-hal = "1.0.0" embedded-hal = "1.0.0"
thiserror = { version = "2.0.17", default-features = false } thiserror = { version = "2.0.17", default-features = false }
+40 -19
View File
@@ -3,7 +3,13 @@
#[cfg(test)] #[cfg(test)]
extern crate std; extern crate std;
extern crate chrono;
mod types; mod types;
mod timestamp;
pub use types::{Stored_U26, Transit_U26, Transit_I26, U26Error};
pub use timestamp::{Store_DateTimeUtc, Transit_DateTimeUtc, DateTimeUtcError};
use embedded_hal::i2c::{I2c, Operation, SevenBitAddress}; use embedded_hal::i2c::{I2c, Operation, SevenBitAddress};
use thiserror::Error; use thiserror::Error;
@@ -27,6 +33,8 @@ pub trait BmsWriteable {
pub enum BmsProtocolError { pub enum BmsProtocolError {
#[error("i2c communication failed")] #[error("i2c communication failed")]
I2cCommunicationError, I2cCommunicationError,
#[error("checksum error in received data")]
ChecksumError,
} }
impl<E> From<E> for BmsProtocolError impl<E> From<E> for BmsProtocolError
@@ -38,6 +46,15 @@ where
} }
} }
impl From<crate::types::U26Error> for BmsProtocolError {
fn from(error: crate::types::U26Error) -> Self {
match error {
crate::types::U26Error::ChecksumError => Self::ChecksumError,
_ => Self::I2cCommunicationError,
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct BmsSoftwareReset; pub struct BmsSoftwareReset;
@@ -73,7 +90,8 @@ impl BmsReadable for ProtocolVersion {
Operation::Read(&mut version), Operation::Read(&mut version),
], ],
)?; )?;
Ok(ProtocolVersion(u32::from_be_bytes(version))) let transit = Transit_U26::from_be_bytes(version)?;
Ok(ProtocolVersion(transit.into()))
} }
} }
@@ -96,7 +114,8 @@ impl BmsReadable for FirmwareVersion {
Operation::Read(&mut version), Operation::Read(&mut version),
], ],
)?; )?;
Ok(FirmwareVersion(u32::from_be_bytes(version))) let transit = Transit_U26::from_be_bytes(version)?;
Ok(FirmwareVersion(transit.into()))
} }
} }
@@ -127,9 +146,9 @@ impl BmsReadable for Config {
], ],
)?; )?;
Ok(Config { Ok(Config {
capacity_mah: u32::from_be_bytes(capacity_mah), capacity_mah: Transit_U26::from_be_bytes(capacity_mah)?.into(),
v_full_mv: u32::from_be_bytes(v_full_mv), v_full_mv: Transit_U26::from_be_bytes(v_full_mv)?.into(),
v_empty_mv: u32::from_be_bytes(v_empty_mv), v_empty_mv: Transit_U26::from_be_bytes(v_empty_mv)?.into(),
}) })
} }
} }
@@ -167,11 +186,12 @@ impl BmsReadable for BatteryState {
], ],
)?; )?;
Ok(BatteryState { Ok(BatteryState {
lifetime_capacity_mah: u32::from_be_bytes(lifetime_capacity_mah), lifetime_capacity_mah: Transit_U26::from_be_bytes(lifetime_capacity_mah)?.into(),
remaining_capacity_mah: u32::from_be_bytes(remaining_capacity_mah), remaining_capacity_mah: Transit_U26::from_be_bytes(remaining_capacity_mah)?.into(),
current_mv: u32::from_be_bytes(current_mv), current_mv: Transit_U26::from_be_bytes(current_mv)?.into(),
temperature_celcius: i32::from_be_bytes(temperature_celcius), temperature_celcius: crate::types::Transit_I26::from_be_bytes(temperature_celcius)?
health_percent: u32::from_be_bytes(health_percent), .into(),
health_percent: Transit_U26::from_be_bytes(health_percent)?.into(),
}) })
} }
} }
@@ -194,9 +214,9 @@ impl BmsReadable for ChargeInfoWindowSec {
Operation::Read(&mut charge_info_window_sec), Operation::Read(&mut charge_info_window_sec),
], ],
)?; )?;
Ok(ChargeInfoWindowSec(u32::from_be_bytes( Ok(ChargeInfoWindowSec(
charge_info_window_sec, Transit_U26::from_be_bytes(charge_info_window_sec)?.into(),
))) ))
} }
} }
@@ -206,11 +226,12 @@ impl BmsWriteable for ChargeInfoWindowSec {
I: I2c<SevenBitAddress>, I: I2c<SevenBitAddress>,
{ {
let cmd = BmsRegisterMap::ChargoInfoWindowTotalSecs; let cmd = BmsRegisterMap::ChargoInfoWindowTotalSecs;
let transit = Transit_U26::try_from(self.0)?;
i2c_dev.transaction( i2c_dev.transaction(
BMS_I2C_ADDRESS, BMS_I2C_ADDRESS,
&mut [ &mut [
Operation::Write(&(cmd as u32).to_be_bytes()), Operation::Write(&(cmd as u32).to_be_bytes()),
Operation::Write(&self.0.to_be_bytes()), Operation::Write(&transit.to_be_bytes()),
], ],
)?; )?;
Ok(()) Ok(())
@@ -250,11 +271,11 @@ impl BmsReadable for ChargeInfo {
], ],
)?; )?;
Ok(ChargeInfo { Ok(ChargeInfo {
total_charge_ma: u32::from_be_bytes(total_charge_ma), total_charge_ma: Transit_U26::from_be_bytes(total_charge_ma)?.into(),
total_discharge_ma: u32::from_be_bytes(total_discharge_ma), total_discharge_ma: Transit_U26::from_be_bytes(total_discharge_ma)?.into(),
max_charge_mw: u32::from_be_bytes(max_charge_mw), max_charge_mw: Transit_U26::from_be_bytes(max_charge_mw)?.into(),
max_discharge_mw: u32::from_be_bytes(max_discharge_mw), max_discharge_mw: Transit_U26::from_be_bytes(max_discharge_mw)?.into(),
avg_voltage_mv: u32::from_be_bytes(avg_voltage_mv), avg_voltage_mv: Transit_U26::from_be_bytes(avg_voltage_mv)?.into(),
}) })
} }
} }
+377
View File
@@ -0,0 +1,377 @@
use chrono::{DateTime, Utc};
use thiserror::Error;
use crate::types::{Stored_U26, Transit_U26, U26Error};
#[derive(Debug, PartialEq, Error)]
/// Error type for DateTimeUtc wrapper types
pub enum DateTimeUtcError {
#[error("timestamp overflow")]
Overflow,
#[error("timestamp invalid")]
InvalidTimestamp,
#[error("timestamp conversion error")]
ConversionError,
}
impl From<U26Error> for DateTimeUtcError {
fn from(error: U26Error) -> Self {
match error {
U26Error::Overflow => DateTimeUtcError::Overflow,
U26Error::InvalidCodeWord => DateTimeUtcError::InvalidTimestamp,
U26Error::IncorrectibleError => DateTimeUtcError::InvalidTimestamp,
U26Error::ChecksumError => DateTimeUtcError::InvalidTimestamp,
}
}
}
#[derive(Debug, PartialEq)]
#[allow(non_camel_case_types)]
/// Wrapper type for storing DateTime<Utc> with error correction
/// Uses two Stored_U26 values internally to store 52 bits of milliseconds since Unix epoch
pub struct Store_DateTimeUtc(Stored_U26, Stored_U26);
impl Store_DateTimeUtc {
/// Maximum timestamp value that can be stored (in milliseconds)
const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1;
/// Create a new Store_DateTimeUtc from a DateTime<Utc>
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
let timestamp_ms = dt.timestamp_millis();
if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 {
return Err(DateTimeUtcError::Overflow);
}
let timestamp_u64 = timestamp_ms as u64;
let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32;
let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32;
let stored_low = Stored_U26::try_from(low_26)?;
let stored_high = Stored_U26::try_from(high_26)?;
Ok(Self(stored_low, stored_high))
}
/// Get the DateTime<Utc> value
pub fn into_datetime(self) -> Result<DateTime<Utc>, DateTimeUtcError> {
let low_26: u32 = self.0.into();
let high_26: u32 = self.1.into();
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
DateTime::from_timestamp_millis(timestamp_u64 as i64)
.ok_or(DateTimeUtcError::ConversionError)
}
/// Get the DateTime<Utc> value (borrowed)
pub fn to_datetime(&self) -> Result<DateTime<Utc>, DateTimeUtcError> {
let low_26: u32 = self.0.clone().into();
let high_26: u32 = self.1.clone().into();
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
DateTime::from_timestamp_millis(timestamp_u64 as i64)
.ok_or(DateTimeUtcError::ConversionError)
}
/// Serialize to big-endian bytes with CRC values
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc]
pub fn to_be_bytes(&self) -> [u8; 10] {
let mut bytes = [0u8; 10];
let high_bytes = self.1.to_be_bytes();
let high_crc = self.1.calc_crc6();
let low_bytes = self.0.to_be_bytes();
let low_crc = self.0.calc_crc6();
bytes[0] = high_bytes[0];
bytes[1] = high_bytes[1];
bytes[2] = high_bytes[2];
bytes[3] = high_bytes[3];
bytes[4] = high_crc;
bytes[5] = low_bytes[0];
bytes[6] = low_bytes[1];
bytes[7] = low_bytes[2];
bytes[8] = low_bytes[3];
bytes[9] = low_crc;
bytes
}
/// Deserialize from big-endian bytes with CRC check
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc]
pub fn from_be_bytes(bytes: [u8; 10]) -> Result<Self, DateTimeUtcError> {
let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]];
let high_crc = bytes[4];
let low_bytes = [bytes[5], bytes[6], bytes[7], bytes[8]];
let low_crc = bytes[9];
let stored_high = Stored_U26::from_be_bytes(high_bytes, high_crc)?;
let stored_low = Stored_U26::from_be_bytes(low_bytes, low_crc)?;
Ok(Self(stored_low, stored_high))
}
}
impl From<Store_DateTimeUtc> for DateTime<Utc> {
fn from(value: Store_DateTimeUtc) -> Self {
value.into_datetime().unwrap()
}
}
impl TryFrom<DateTime<Utc>> for Store_DateTimeUtc {
type Error = DateTimeUtcError;
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<Store_DateTimeUtc> for u64 {
fn from(value: Store_DateTimeUtc) -> Self {
let low_26: u32 = value.0.into();
let high_26: u32 = value.1.into();
((high_26 as u64) << 26) | (low_26 as u64)
}
}
#[derive(Debug, PartialEq)]
#[allow(non_camel_case_types)]
/// Wrapper type for transmitting DateTime<Utc> with CRC error detection
/// Uses two Transit_U26 values internally to store 52 bits of milliseconds since Unix epoch
pub struct Transit_DateTimeUtc(Transit_U26, Transit_U26);
impl Transit_DateTimeUtc {
/// Maximum timestamp value that can be transmitted (in milliseconds)
const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1;
/// Create a new Transit_DateTimeUtc from a DateTime<Utc>
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
let timestamp_ms = dt.timestamp_millis();
if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 {
return Err(DateTimeUtcError::Overflow);
}
let timestamp_u64 = timestamp_ms as u64;
let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32;
let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32;
let transit_low = Transit_U26::try_from(low_26)?;
let transit_high = Transit_U26::try_from(high_26)?;
Ok(Self(transit_low, transit_high))
}
/// Get the DateTime<Utc> value
pub fn into_datetime(self) -> Result<DateTime<Utc>, DateTimeUtcError> {
let low_26: u32 = self.0.into();
let high_26: u32 = self.1.into();
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
DateTime::from_timestamp_millis(timestamp_u64 as i64)
.ok_or(DateTimeUtcError::ConversionError)
}
/// Get the DateTime<Utc> value (borrowed)
pub fn to_datetime(&self) -> Result<DateTime<Utc>, DateTimeUtcError> {
let low_26: u32 = self.0.clone().into();
let high_26: u32 = self.1.clone().into();
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
DateTime::from_timestamp_millis(timestamp_u64 as i64)
.ok_or(DateTimeUtcError::ConversionError)
}
/// Serialize to big-endian bytes
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]]
pub fn to_be_bytes(&self) -> [u8; 8] {
let mut bytes = [0u8; 8];
let high_bytes = self.1.to_be_bytes();
let low_bytes = self.0.to_be_bytes();
bytes[0] = high_bytes[0];
bytes[1] = high_bytes[1];
bytes[2] = high_bytes[2];
bytes[3] = high_bytes[3];
bytes[4] = low_bytes[0];
bytes[5] = low_bytes[1];
bytes[6] = low_bytes[2];
bytes[7] = low_bytes[3];
bytes
}
/// Deserialize from big-endian bytes with CRC check
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]]
pub fn from_be_bytes(bytes: [u8; 8]) -> Result<Self, DateTimeUtcError> {
let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]];
let low_bytes = [bytes[4], bytes[5], bytes[6], bytes[7]];
let transit_high = Transit_U26::from_be_bytes(high_bytes)?;
let transit_low = Transit_U26::from_be_bytes(low_bytes)?;
Ok(Self(transit_low, transit_high))
}
}
impl From<Transit_DateTimeUtc> for DateTime<Utc> {
fn from(value: Transit_DateTimeUtc) -> Self {
value.into_datetime().unwrap()
}
}
impl TryFrom<DateTime<Utc>> for Transit_DateTimeUtc {
type Error = DateTimeUtcError;
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<Transit_DateTimeUtc> for u64 {
fn from(value: Transit_DateTimeUtc) -> Self {
let low_26: u32 = value.0.into();
let high_26: u32 = value.1.into();
((high_26 as u64) << 26) | (low_26 as u64)
}
}
#[cfg(test)]
mod timestamp_test {
use super::*;
use chrono::{DateTime, TimeZone, Utc};
#[test]
fn store_datetimeutc_basic_operations() {
// Test with Unix epoch
let epoch = Utc.timestamp_millis_opt(0).unwrap();
let stored = Store_DateTimeUtc::new(epoch).unwrap();
let retrieved: DateTime<Utc> = stored.into();
assert_eq!(retrieved, epoch);
// Test with a specific date (within 52-bit range)
let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap();
let stored = Store_DateTimeUtc::new(dt).unwrap();
let retrieved: DateTime<Utc> = stored.into();
assert_eq!(retrieved, dt);
}
#[test]
fn store_datetimeutc_serialization() {
let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap();
let stored = Store_DateTimeUtc::new(dt).unwrap();
let bytes = stored.to_be_bytes();
// Deserialize and verify
let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap();
let retrieved: DateTime<Utc> = deserialized.into();
assert_eq!(retrieved, dt);
}
#[test]
fn store_datetimeutc_error_correction() {
let dt = Utc.with_ymd_and_hms(2020, 3, 10, 14, 25, 10).unwrap();
let stored = Store_DateTimeUtc::new(dt).unwrap();
let mut bytes = stored.to_be_bytes();
// Introduce a single bit error in low bytes that can be corrected
bytes[7] ^= 0x01;
let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap();
let retrieved: DateTime<Utc> = deserialized.into();
assert_eq!(retrieved, dt);
}
#[test]
fn store_datetimeutc_overflow() {
// Test with timestamp beyond max value
let max_timestamp = Store_DateTimeUtc::MAX_TIMESTAMP_MS;
let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap();
let result = Store_DateTimeUtc::new(dt);
assert!(matches!(result, Err(DateTimeUtcError::Overflow)));
}
#[test]
fn store_datetimeutc_borrowed_method() {
let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap();
let stored = Store_DateTimeUtc::new(dt).unwrap();
// Test the borrowed to_datetime method
let retrieved = stored.to_datetime().unwrap();
assert_eq!(retrieved, dt);
}
#[test]
fn store_datetimeutc_try_from() {
let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap();
let stored: Store_DateTimeUtc = dt.try_into().unwrap();
let retrieved: DateTime<Utc> = stored.into();
assert_eq!(retrieved, dt);
}
#[test]
fn store_datetimeutc_crc_error_detection() {
let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap();
let stored = Store_DateTimeUtc::new(dt).unwrap();
let mut bytes = stored.to_be_bytes();
// Introduce multiple bit errors in low bytes that can't be corrected
bytes[5] ^= 0x01;
bytes[6] ^= 0x01;
let result = Store_DateTimeUtc::from_be_bytes(bytes);
assert!(result.is_err());
}
#[test]
fn transit_datetimeutc_basic_operations() {
// Test with Unix epoch
let epoch = Utc.timestamp_millis_opt(0).unwrap();
let transit = Transit_DateTimeUtc::new(epoch).unwrap();
let retrieved: DateTime<Utc> = transit.into();
assert_eq!(retrieved, epoch);
// Test with a specific date (within 52-bit range)
let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap();
let transit = Transit_DateTimeUtc::new(dt).unwrap();
let retrieved: DateTime<Utc> = transit.into();
assert_eq!(retrieved, dt);
}
#[test]
fn transit_datetimeutc_serialization() {
let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap();
let transit = Transit_DateTimeUtc::new(dt).unwrap();
let bytes = transit.to_be_bytes();
// Deserialize and verify
let deserialized = Transit_DateTimeUtc::from_be_bytes(bytes).unwrap();
let retrieved: DateTime<Utc> = deserialized.into();
assert_eq!(retrieved, dt);
}
#[test]
fn transit_datetimeutc_crc_error_detection() {
let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap();
let transit = Transit_DateTimeUtc::new(dt).unwrap();
let mut bytes = transit.to_be_bytes();
// Introduce bit errors in low bytes that will cause CRC mismatch
bytes[4] ^= 0x01;
bytes[5] ^= 0x01;
let result = Transit_DateTimeUtc::from_be_bytes(bytes);
assert!(result.is_err());
}
#[test]
fn transit_datetimeutc_overflow() {
// Test with timestamp beyond max value
let max_timestamp = Transit_DateTimeUtc::MAX_TIMESTAMP_MS;
let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap();
let result = Transit_DateTimeUtc::new(dt);
assert!(matches!(result, Err(DateTimeUtcError::Overflow)));
}
#[test]
fn transit_datetimeutc_borrowed_method() {
let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap();
let transit = Transit_DateTimeUtc::new(dt).unwrap();
// Test the borrowed to_datetime method
let retrieved = transit.to_datetime().unwrap();
assert_eq!(retrieved, dt);
}
#[test]
fn transit_datetimeutc_try_from() {
let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap();
let transit: Transit_DateTimeUtc = dt.try_into().unwrap();
let retrieved: DateTime<Utc> = transit.into();
assert_eq!(retrieved, dt);
}
}
+122 -9
View File
@@ -44,19 +44,26 @@
//! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code. //! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code.
//! Meaning the chance of errors going undetected is <2%. //! Meaning the chance of errors going undetected is <2%.
use thiserror::Error;
const U26_VALUE_MASK: u32 = 0xFFFFFFc0; const U26_VALUE_MASK: u32 = 0xFFFFFFc0;
const U26_MAX_VALUE: u32 = 0x03FFFFFF; const U26_MAX_VALUE: u32 = 0x03FFFFFF;
const U26_HEMMING_MASK: u32 = 0x0000003e; const U26_HEMMING_MASK: u32 = 0x0000003e;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Error)]
pub enum U26Error { pub enum U26Error {
#[error("data read from bytes has unexpected format")]
InvalidCodeWord, InvalidCodeWord,
#[error("detected an uncorrectible error, data is invalid")]
IncorrectibleError, IncorrectibleError,
#[error("data does not fit in u26")]
Overflow, Overflow,
#[error("checksum error")]
ChecksumError, ChecksumError,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
#[allow(non_camel_case_types)]
/// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption /// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption
/// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in /// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in
/// hemming code for one mit error correction. /// hemming code for one mit error correction.
@@ -116,7 +123,7 @@ impl Stored_U26 {
impl From<Stored_U26> for u32 { impl From<Stored_U26> for u32 {
fn from(value: Stored_U26) -> Self { fn from(value: Stored_U26) -> Self {
value.0 & U26_VALUE_MASK >> 6 (value.0 & U26_VALUE_MASK) >> 6
} }
} }
@@ -132,7 +139,8 @@ impl TryFrom<u32> for Stored_U26 {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
#[allow(non_camel_case_types)]
/// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur /// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur
/// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used /// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used
/// ///
@@ -165,7 +173,7 @@ impl Transit_U26 {
impl From<Transit_U26> for u32 { impl From<Transit_U26> for u32 {
fn from(value: Transit_U26) -> Self { fn from(value: Transit_U26) -> Self {
value.0 & U26_VALUE_MASK >> 6 (value.0 & U26_VALUE_MASK) >> 6
} }
} }
@@ -181,6 +189,59 @@ impl TryFrom<u32> for Transit_U26 {
} }
} }
#[derive(Debug, PartialEq, Clone)]
#[allow(non_camel_case_types)]
/// this type is used for transmitting signed 26-bit integer data
/// It wraps the Transit_U26 type to provide i32 support
/// The i32 value is converted to u32 by casting, preserving the bit pattern
/// This ensures proper sign extension when converting between signed and unsigned
/// Valid range: -33554432 to 33554431 (26-bit signed)
pub struct Transit_I26(Transit_U26);
impl Transit_I26 {
pub fn new(value: i32) -> Result<Self, U26Error> {
// Check if value is within 26-bit signed range
if value < -0x02000000 || value > 0x01FFFFFF {
return Err(U26Error::Overflow);
}
// Extract only the lower 26 bits
let u26_val = (value as u32) & 0x03FFFFFF;
let transit_u26 = Transit_U26::try_from(u26_val)?;
Ok(Self(transit_u26))
}
pub fn from_be_bytes(bytes: [u8; 4]) -> Result<Self, U26Error> {
let transit_u26 = Transit_U26::from_be_bytes(bytes)?;
Ok(Self(transit_u26))
}
pub fn to_be_bytes(&self) -> [u8; 4] {
self.0.to_be_bytes()
}
}
impl From<Transit_I26> for i32 {
fn from(value: Transit_I26) -> Self {
// Convert u32 back to i32 by casting, preserving the exact bit pattern
let u32_val: u32 = value.0.into();
// Interpret as signed 26-bit integer
if u32_val & 0x02000000 != 0 {
// Sign-extend to 32 bits
(u32_val as i32) | 0xFC000000u32 as i32
} else {
u32_val as i32
}
}
}
impl TryFrom<i32> for Transit_I26 {
type Error = U26Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
Self::new(value)
}
}
// only calc hemming code for the first 24 data bits including the 5 hemming bits // only calc hemming code for the first 24 data bits including the 5 hemming bits
// pos | | p1 | p2 | p3 | p4 | p5 | // pos | | p1 | p2 | p3 | p4 | p5 |
// 00001 | p1 | o | | | | | // 00001 | p1 | o | | | | |
@@ -360,7 +421,7 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu
pub fn calc_crc6(data: u32) -> u8 { pub fn calc_crc6(data: u32) -> u8 {
let mut crc: u8 = 0x00; let mut crc: u8 = 0x00;
for pos in 0..32 { for pos in 0..32 {
if ((data >> (31-pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 { if ((data >> (31 - pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 {
crc = ((crc << 1) ^ 0x03) & 0x3F; crc = ((crc << 1) ^ 0x03) & 0x3F;
} else { } else {
crc = (crc << 1) & 0x3F; crc = (crc << 1) & 0x3F;
@@ -376,7 +437,7 @@ pub fn calc_crc6(data: u32) -> u8 {
pub fn check_crc6(data: u32, crc: u8) -> bool { pub fn check_crc6(data: u32, crc: u8) -> bool {
let mut check_crc: u8 = 0x00; let mut check_crc: u8 = 0x00;
for pos in 0..32 { for pos in 0..32 {
if ((data >> (31-pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 { if ((data >> (31 - pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 {
check_crc = ((check_crc << 1) ^ 0x03) & 0x3F; check_crc = ((check_crc << 1) ^ 0x03) & 0x3F;
} else { } else {
check_crc = (check_crc << 1) & 0x3F; check_crc = (check_crc << 1) & 0x3F;
@@ -388,7 +449,8 @@ pub fn check_crc6(data: u32, crc: u8) -> bool {
#[cfg(test)] #[cfg(test)]
mod base_type_tests { mod base_type_tests {
use super::{ use super::{
Stored_U26, Transit_U26, U26_MAX_VALUE, calc_crc6, calc_hemming, check_crc6, check_hemming, calc_crc6, calc_hemming, check_crc6, check_hemming, Stored_U26, Transit_I26, Transit_U26,
U26_MAX_VALUE,
}; };
use crate::types::U26Error; use crate::types::U26Error;
@@ -469,7 +531,10 @@ mod base_type_tests {
assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data); assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data);
// introduce second bit error // introduce second bit error
bytes[0] = bytes[0] ^ 0x01; bytes[0] = bytes[0] ^ 0x01;
assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap_err(), U26Error::IncorrectibleError); assert_eq!(
Stored_U26::from_be_bytes(bytes, crc).unwrap_err(),
U26Error::IncorrectibleError
);
} }
#[test] #[test]
@@ -515,4 +580,52 @@ mod base_type_tests {
assert!(!check_crc6(corrupted_data, original_crc)); assert!(!check_crc6(corrupted_data, original_crc));
} }
} }
#[test]
fn transit_i26_positive_values() {
let data = Transit_I26::new(12345).unwrap();
let bytes = data.to_be_bytes();
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
assert_eq!(i32::from(decoded), 12345);
}
#[test]
fn transit_i26_negative_values() {
let data = Transit_I26::new(-12345).unwrap();
let bytes = data.to_be_bytes();
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
assert_eq!(i32::from(decoded), -12345);
}
#[test]
fn transit_i26_zero() {
let data = Transit_I26::new(0).unwrap();
let bytes = data.to_be_bytes();
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
assert_eq!(i32::from(decoded), 0);
}
#[test]
fn transit_i26_max_positive() {
let data = Transit_I26::new(0x01FFFFFF).unwrap();
let bytes = data.to_be_bytes();
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
assert_eq!(i32::from(decoded), 0x01FFFFFF as i32);
}
#[test]
fn transit_i26_max_negative() {
let data = Transit_I26::new(-0x01FFFFFF).unwrap();
let bytes = data.to_be_bytes();
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
assert_eq!(i32::from(decoded), -0x01FFFFFF as i32);
}
#[test]
fn transit_i26_checksum_error() {
let data = Transit_I26::new(12345).unwrap();
let mut bytes = data.to_be_bytes();
bytes[1] ^= 0x01; // Corrupt one byte
assert!(Transit_I26::from_be_bytes(bytes).is_err());
}
} }