xr2280x_hid/
lib.rs

1//! # XR2280x HID Driver
2//!
3//! This crate provides a high-performance Rust driver for the Exar XR2280x family of USB HID to I2C/GPIO bridge chips.
4//! These chips provide a convenient way to add I2C, GPIO, and PWM capabilities to any system with USB support.
5//!
6//! ## Features
7//!
8//! - **Fast I2C master controller** with support for 7-bit and 10-bit addressing
9//!   - Optimized I2C device scanning (112 addresses in ~1 second)
10//!   - Configurable bus speeds up to 400kHz
11//! - **Flexible GPIO control** with interrupt support
12//!   - Individual pin control with direction, pull-up/pull-down, and open-drain modes
13//!   - Bulk operations for efficient multi-pin control
14//! - **PWM output generation** on any GPIO pin
15//!   - Two independent PWM channels with nanosecond precision
16//!   - Multiple operating modes (idle, one-shot, free-run)
17//! - **Cross-platform support** via hidapi (Linux, Windows, macOS)
18//! - **Zero-copy operations** where possible for maximum performance
19//!
20//! ## Device Support
21//!
22//! | Model   | GPIO Pins | I2C | PWM | Interrupts |
23//! |---------|-----------|-----|-----|------------|
24//! | XR22800 | 8         | ✓   | ✓   | ✓          |
25//! | XR22801 | 8         | ✓   | ✓   | ✓          |
26//! | XR22802 | 32        | ✓   | ✓   | ✓          |
27//! | XR22804 | 32        | ✓   | ✓   | ✓          |
28//!
29//! All devices operate at USB 2.0 Full Speed (12 Mbps) and support 3.3V logic levels.
30//!
31//! ## Quick Start
32//!
33//! ```no_run
34//! use xr2280x_hid::{Xr2280x, device_find_first};
35//! use hidapi::HidApi;
36//!
37//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! // Initialize HID API and find the first XR2280x device
39//! let hid_api = HidApi::new()?;
40//! let device_info = device_find_first(&hid_api)?;
41//! let device = Xr2280x::device_open(&hid_api, &device_info)?;
42//!
43//! // Scan I2C bus for connected devices
44//! device.i2c_set_speed_khz(100)?;
45//! let devices = device.i2c_scan_default()?;
46//! println!("Found {} I2C devices: {:02X?}", devices.len(), devices);
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! ## Multi-Device Selection
52//!
53//! When multiple XR2280x devices are connected, you can select specific devices using various methods:
54//!
55//! ### Enumerate All Hardware Devices
56//!
57//! ```no_run
58//! use xr2280x_hid::Xr2280x;
59//! use hidapi::HidApi;
60//!
61//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
62//! let hid_api = HidApi::new()?;
63//!
64//! // Get list of all XR2280x devices
65//! let devices = Xr2280x::device_enumerate(&hid_api)?;
66//! println!("Found {} XR2280x devices:", devices.len());
67//!
68//! for (i, info) in devices.iter().enumerate() {
69//!     println!("  [{}] Serial: {}, Product: {}",
70//!         i,
71//!         info.serial_number.as_deref().unwrap_or("N/A"),
72//!         info.product_string.as_deref().unwrap_or("Unknown")
73//!     );
74//! }
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! ### Open by Serial Number
80//!
81//! ```no_run
82//! use xr2280x_hid::Xr2280x;
83//! use hidapi::HidApi;
84//!
85//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
86//! let hid_api = HidApi::new()?;
87//!
88//! // Open specific device by serial number
89//! let device = Xr2280x::open_by_serial(&hid_api, "ABC123456")?;
90//! println!("Opened device with serial ABC123456");
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! ### Open by Index
96//!
97//! ```no_run
98//! use xr2280x_hid::Xr2280x;
99//! use hidapi::HidApi;
100//!
101//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
102//! let hid_api = HidApi::new()?;
103//!
104//! // Open the first device (index 0)
105//! let device = Xr2280x::open_by_index(&hid_api, 0)?;
106//! # Ok(())
107//! # }
108//! ```
109//!
110//! ## Performance Architecture and Best Practices
111//!
112//! **⚠️ CRITICAL**: Understanding the communication architecture is essential for high-performance applications.
113//! The XR2280x devices communicate via USB HID Feature Reports, and each operation has significant overhead.
114//!
115//! ### Communication Architecture
116//!
117//! Every GPIO and I2C operation translates to one or more HID Feature Report transactions:
118//!
119//! - **USB Protocol Overhead**: Control transfer setup and response handling
120//! - **HID Report Processing**: Structured data formatting and parsing
121//! - **Device Firmware Execution**: Register updates and hardware control
122//! - **Typical Transaction Latency**: 5-10ms per HID Feature Report
123//!
124//! This means traditional "one operation per function call" patterns can be extremely inefficient.
125//!
126//! ### GPIO Performance Impact Analysis
127//!
128//! | Operation Pattern | HID Transactions | Typical Latency | Improvement |
129//! |-------------------|------------------|-----------------|-------------|
130//! | Single pin (individual calls) | 8 transactions | ~40-80ms | Baseline |
131//! | Single pin (efficient API) | 5 transactions | ~25-50ms | **1.6x faster** |
132//! | 4 pins (individual calls) | 32 transactions | ~160-320ms | Baseline |
133//! | 4 pins (bulk API) | 6 transactions | ~30-60ms | **5.3x faster** |
134//! | 8 pins (individual calls) | 64 transactions | ~320-640ms | Baseline |
135//! | 8 pins (bulk API) | 6 transactions | ~30-60ms | **10.7x faster** |
136//!
137//! ### Root Cause: Read-Modify-Write Cycles
138//!
139//! Traditional GPIO operations perform inefficient read-modify-write cycles:
140//!
141//! ```text
142//! gpio_set_direction(): [READ register] → [MODIFY bit] → [WRITE register] = 2 HID transactions
143//! gpio_set_pull():      [READ pull-up] → [READ pull-down] → [WRITE pull-up] → [WRITE pull-down] = 4 HID transactions
144//! gpio_write():         [WRITE to SET/CLEAR register] = 1 HID transaction
145//!
146//! Total per pin: 7-8 HID transactions
147//! ```
148//!
149//! ### Architectural Solutions
150//!
151//! The high-performance APIs eliminate redundant operations through:
152//!
153//! 1. **Hardware-Aware Register Grouping**: Pins 0-15 (Group 0) and 16-31 (Group 1) use separate registers
154//! 2. **Transaction Batching**: Combined operations reduce individual read-modify-write cycles
155//! 3. **Bulk Processing**: O(1) complexity for multiple pins instead of O(N)
156//! 4. **Optimized Register Access**: Uses dedicated SET/CLEAR registers where available
157//!
158//! ### High-Performance GPIO APIs
159//!
160//! **✅ RECOMMENDED: Efficient Single Pin Setup**
161//! ```no_run
162//! use xr2280x_hid::gpio::{GpioPin, GpioLevel, GpioPull};
163//! # use xr2280x_hid::Xr2280x;
164//! # fn example(device: &Xr2280x) -> xr2280x_hid::Result<()> {
165//!
166//! // ✅ Efficient output setup (5 HID transactions vs 8)
167//! let pin = GpioPin::new(0)?;
168//! device.gpio_setup_output(pin, GpioLevel::Low, GpioPull::None)?;
169//!
170//! // ✅ Efficient input setup (4 HID transactions vs 6)
171//! device.gpio_setup_input(pin, GpioPull::Up)?;
172//! # Ok(())
173//! # }
174//! ```
175//!
176//! **✅ HIGHLY RECOMMENDED: Bulk Operations**
177//! ```no_run
178//! use xr2280x_hid::gpio::{GpioPin, GpioLevel, GpioPull};
179//! # use xr2280x_hid::Xr2280x;
180//! # fn example(device: &Xr2280x) -> xr2280x_hid::Result<()> {
181//!
182//! // ✅ Bulk output setup (6 HID transactions total, regardless of pin count)
183//! let pin_configs = vec![
184//!     (GpioPin::new(0)?, GpioLevel::High),
185//!     (GpioPin::new(1)?, GpioLevel::Low),
186//!     (GpioPin::new(2)?, GpioLevel::High),
187//!     (GpioPin::new(3)?, GpioLevel::Low),
188//! ];
189//! device.gpio_setup_outputs(&pin_configs, GpioPull::None)?;
190//!
191//! // ✅ Bulk input setup (6 HID transactions total)
192//! let input_pins = vec![GpioPin::new(8)?, GpioPin::new(9)?, GpioPin::new(10)?];
193//! device.gpio_setup_inputs(&input_pins, GpioPull::Up)?;
194//! # Ok(())
195//! # }
196//! ```
197//!
198//! **❌ ANTI-PATTERN: Individual Operations in Loops**
199//! ```no_run
200//! use xr2280x_hid::gpio::{GpioPin, GpioDirection, GpioLevel, GpioPull};
201//! # use xr2280x_hid::Xr2280x;
202//! # fn example(device: &Xr2280x) -> xr2280x_hid::Result<()> {
203//!
204//! // ❌ EXTREMELY INEFFICIENT: 8 HID transactions × 4 pins = 32 transactions
205//! for i in 0..4 {
206//!     let pin = GpioPin::new(i)?;
207//!     device.gpio_set_direction(pin, GpioDirection::Output)?;  // 2 transactions
208//!     device.gpio_set_pull(pin, GpioPull::None)?;             // 4 transactions
209//!     device.gpio_write(pin, GpioLevel::Low)?;                // 1 transaction
210//!     // This loop creates 32 HID transactions vs 6 with bulk APIs!
211//! }
212//! # Ok(())
213//! # }
214//! ```
215//!
216//! ### HID Transaction Cost Reference
217//!
218//! | Operation | HID Transactions | Notes |
219//! |-----------|------------------|-------|
220//! | `gpio_write()` | 1 | Most efficient (uses SET/CLEAR registers) |
221//! | `gpio_read()` | 1 | Single register read |
222//! | `gpio_set_direction()` | 2 | Read-modify-write cycle |
223//! | `gpio_set_pull()` | 4 | **Most expensive** (both pull registers) |
224//! | `gpio_set_open_drain()` | 2 | Read-modify-write cycle |
225//! | `gpio_setup_output()` | 5 | **Optimized combination** |
226//! | `gpio_setup_input()` | 4 | **Optimized combination** |
227//! | `gpio_setup_outputs()` | 6 | **Bulk operation (O(1) complexity)** |
228//! | `gpio_setup_inputs()` | 6 | **Bulk operation (O(1) complexity)** |
229//!
230//! ### Migration Strategies
231//!
232//! **Immediate Wins - Simple Replacements:**
233//! ```no_run
234//! # use xr2280x_hid::{Xr2280x, gpio::*};
235//! # fn example(device: &Xr2280x) -> xr2280x_hid::Result<()> {
236//! // ❌ OLD (8 HID transactions)
237//! // device.gpio_set_direction(pin, GpioDirection::Output)?;
238//! // device.gpio_set_pull(pin, GpioPull::None)?;
239//! // device.gpio_write(pin, GpioLevel::Low)?;
240//!
241//! // ✅ NEW (5 HID transactions - 37% improvement)
242//! let pin = GpioPin::new(0)?;
243//! device.gpio_setup_output(pin, GpioLevel::Low, GpioPull::None)?;
244//! # Ok(())
245//! # }
246//! ```
247//!
248//! **Major Wins - Bulk Migration:**
249//! ```no_run
250//! # use xr2280x_hid::{Xr2280x, gpio::*};
251//! # fn example(device: &Xr2280x) -> xr2280x_hid::Result<()> {
252//! // ❌ OLD (N × 8 HID transactions)
253//! // for (pin, level) in &pin_configs {
254//! //     device.gpio_set_direction(*pin, GpioDirection::Output)?;
255//! //     device.gpio_set_pull(*pin, GpioPull::None)?;
256//! //     device.gpio_write(*pin, *level)?;
257//! // }
258//!
259//! // ✅ NEW (6 HID transactions total - up to 10.7x improvement)
260//! let pin_configs = vec![(GpioPin::new(0)?, GpioLevel::Low)];
261//! device.gpio_setup_outputs(&pin_configs, GpioPull::None)?;
262//! # Ok(())
263//! # }
264//! ```
265//!
266//! ### Performance Optimization Guidelines
267//!
268//! 1. **Initialize Once**: Use `gpio_setup_*()` functions during device initialization
269//! 2. **Runtime Efficiency**: Use `gpio_write()` and `gpio_write_masked()` for state changes
270//! 3. **Bulk Operations**: Always prefer bulk APIs when configuring multiple pins
271//! 4. **Register Grouping**: Group operations by hardware boundaries (pins 0-15 vs 16-31)
272//! 5. **State Caching**: Track GPIO states in application logic to avoid redundant reads
273//! 6. **Minimize Reconfiguration**: Avoid repeatedly changing pin configurations
274//!
275//! ### I2C Performance Considerations
276//!
277//! - **Use `i2c_write_read()`** instead of separate `i2c_write()` + `i2c_read()` calls
278//! - **Batch device scanning** with appropriate timeouts (see `i2c::timeouts` module)
279//! - **Cache device responses** when possible to reduce bus traffic
280//!
281//! See the `gpio_efficient_config.rs` example for comprehensive performance demonstrations and measurements.
282//!
283//! ## Advanced Error Handling
284//!
285//! The XR2280x-HID crate provides comprehensive, context-aware error handling designed to make debugging
286//! hardware issues and application problems much easier.
287//!
288//! ### Specific Error Types by Domain
289//!
290//! Instead of generic errors, the crate provides domain-specific error variants:
291//!
292//! **I2C Communication Errors:**
293//! - [`Error::I2cNack`] - Device not found (normal during scanning)
294//! - [`Error::I2cTimeout`] - Hardware issues (stuck bus, power problems)
295//! - [`Error::I2cArbitrationLost`] - Bus contention or interference
296//! - [`Error::I2cRequestError`] - Invalid parameters or data length
297//! - [`Error::I2cUnknownError`] - Firmware-level issues
298//!
299//! **GPIO Hardware Errors:**
300//! - [`Error::GpioRegisterReadError`] - Pin-specific register read failures
301//! - [`Error::GpioRegisterWriteError`] - Pin-specific register write failures
302//! - [`Error::GpioConfigurationError`] - Invalid pin configuration combinations
303//! - [`Error::GpioHardwareError`] - Hardware-level GPIO issues
304//! - [`Error::PinArgumentOutOfRange`] - Invalid pin numbers
305//!
306//! **PWM Configuration Errors:**
307//! - [`Error::PwmConfigurationError`] - Channel configuration issues
308//! - [`Error::PwmParameterError`] - Invalid timing or duty cycle parameters
309//! - [`Error::PwmHardwareError`] - PWM hardware communication failures
310//!
311//! ### Enhanced Error Context
312//!
313//! Each error includes specific context to help with debugging:
314//!
315//! ```no_run
316//! # use xr2280x_hid::*;
317//! # use hidapi::HidApi;
318//! # fn example() -> Result<()> {
319//! # let hid_api = HidApi::new()?;
320//! # let device = Xr2280x::device_open_first(&hid_api)?;
321//! use xr2280x_hid::gpio::*;
322//!
323//! // GPIO errors include pin number and register details
324//! match device.gpio_set_direction(GpioPin::new(5)?, GpioDirection::Output) {
325//!     Err(Error::GpioRegisterWriteError { pin, register, message }) => {
326//!         eprintln!("Failed to configure pin {} register 0x{:04X}: {}", pin, register, message);
327//!         // Can implement pin-specific recovery logic
328//!     }
329//!     Err(Error::PinArgumentOutOfRange { pin, message }) => {
330//!         eprintln!("Invalid pin {}: {}", pin, message);
331//!         // Handle invalid pin number
332//!     }
333//!     Ok(_) => println!("Pin configured successfully"),
334//!     Err(e) => return Err(e),
335//! }
336//! # Ok(())
337//! # }
338//! ```
339//!
340//! ### Error Recovery Strategies
341//!
342//! The specific error types enable targeted recovery strategies:
343//!
344//! ```no_run
345//! # use xr2280x_hid::*;
346//! # use hidapi::HidApi;
347//! # fn example() -> Result<()> {
348//! # let hid_api = HidApi::new()?;
349//! # let device = Xr2280x::device_open_first(&hid_api)?;
350//! // I2C error handling with specific recovery actions
351//! match device.i2c_scan_default() {
352//!     Ok(devices) => println!("Found devices: {:02X?}", devices),
353//!     Err(Error::I2cTimeout { address }) => {
354//!         eprintln!("Hardware issue detected at address {}", address);
355//!         eprintln!("Recovery steps:");
356//!         eprintln!("  1. Check device power supply");
357//!         eprintln!("  2. Verify I2C pull-up resistors");
358//!         eprintln!("  3. Test with fewer devices connected");
359//!         // Could implement automatic retry logic here
360//!     }
361//!     Err(Error::I2cArbitrationLost { address }) => {
362//!         eprintln!("Bus contention at {}, retrying...", address);
363//!         // Implement retry with exponential backoff
364//!     }
365//!     Err(e) => return Err(e),
366//! }
367//! # Ok(())
368//! # }
369//! ```
370//!
371//! ### Benefits for Applications
372//!
373//! 1. **Precise Diagnostics**: Know exactly which pin, register, or device caused issues
374//! 2. **Targeted Recovery**: Different error types enable different recovery strategies
375//! 3. **Better User Experience**: Provide specific troubleshooting guidance to end users
376//! 4. **Robust Applications**: Handle transient vs permanent failures appropriately
377//! 5. **Development Efficiency**: Faster debugging with detailed error context
378//!
379//! ### Open by Path
380//!
381//! ```no_run
382//! use xr2280x_hid::Xr2280x;
383//! use hidapi::HidApi;
384//! use std::ffi::CString;
385//!
386//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
387//! let hid_api = HidApi::new()?;
388//!
389//! // Open device by platform-specific path
390//! let path = CString::new("/dev/hidraw0")?;
391//! let device = Xr2280x::open_by_path(&hid_api, &path)?;
392//! println!("Opened device at path /dev/hidraw0");
393//! # Ok(())
394//! # }
395//! ```
396//!
397//! ## Example Usage
398//!
399//! ### I2C Communication
400//!
401//! ```no_run
402//! use xr2280x_hid::{Xr2280x, device_find_first};
403//! use hidapi::HidApi;
404//!
405//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
406//! let hid_api = HidApi::new()?;
407//! let device = Xr2280x::device_open_first(&hid_api)?;
408//!
409//! // Configure I2C bus speed (supports 100kHz, 400kHz, etc.)
410//! device.i2c_set_speed_khz(400)?;
411//!
412//! // Write to EEPROM at address 0x50
413//! let write_data = [0x00, 0x10, 0x48, 0x65, 0x6C, 0x6C, 0x6F]; // Address + "Hello"
414//! device.i2c_write_7bit(0x50, &write_data)?;
415//!
416//! // Read back from EEPROM
417//! device.i2c_write_7bit(0x50, &[0x00, 0x10])?; // Set read address
418//! let mut read_buffer = [0u8; 5];
419//! device.i2c_read_7bit(0x50, &mut read_buffer)?;
420//! println!("Read: {:?}", std::str::from_utf8(&read_buffer));
421//!
422//! // Combined write-read operation
423//! let mut buffer = [0u8; 4];
424//! device.i2c_write_read_7bit(0x50, &[0x00, 0x00], &mut buffer)?;
425//! # Ok(())
426//! # }
427//! ```
428//!
429//! ### Fast I2C Device Discovery
430//!
431//! ```no_run
432//! use xr2280x_hid::{Xr2280x, device_find_first};
433//! use hidapi::HidApi;
434//!
435//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
436//! let hid_api = HidApi::new()?;
437//! let device = Xr2280x::device_open_first(&hid_api)?;
438//!
439//! device.i2c_set_speed_khz(100)?;
440//!
441//! // Fast scan with progress reporting
442//! let devices = device.i2c_scan_with_progress(0x08, 0x77, |addr, found, current, total| {
443//!     if found {
444//!         println!("Found device at 0x{:02X}", addr);
445//!     }
446//!     if current % 16 == 0 {
447//!         println!("Progress: {}/{}", current, total);
448//!     }
449//! })?;
450//!
451//! println!("Scan complete! Found {} devices in total", devices.len());
452//! # Ok(())
453//! # }
454//! ```
455//!
456//! ### GPIO Control
457//!
458//! ```no_run
459//! use xr2280x_hid::{Xr2280x, GpioPin, GpioDirection, GpioLevel, GpioPull, device_find_first};
460//! use hidapi::HidApi;
461//! use std::thread::sleep;
462//! use std::time::Duration;
463//!
464//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
465//! let hid_api = HidApi::new()?;
466//! let device = Xr2280x::device_open_first(&hid_api)?;
467//!
468//! // Configure GPIO pin 0 as output (LED)
469//! let led_pin = GpioPin::new(0)?;
470//! device.gpio_assign_to_edge(led_pin)?;
471//! device.gpio_set_direction(led_pin, GpioDirection::Output)?;
472//!
473//! // Configure GPIO pin 1 as input with pull-up (button)
474//! let button_pin = GpioPin::new(1)?;
475//! device.gpio_assign_to_edge(button_pin)?;
476//! device.gpio_set_direction(button_pin, GpioDirection::Input)?;
477//! device.gpio_set_pull(button_pin, GpioPull::Up)?;
478//!
479//! // Blink LED and read button
480//! for _ in 0..10 {
481//!     device.gpio_write(led_pin, GpioLevel::High)?;
482//!     let button_state = device.gpio_read(button_pin)?;
483//!     println!("LED ON, Button: {:?}", button_state);
484//!
485//!     sleep(Duration::from_millis(500));
486//!
487//!     device.gpio_write(led_pin, GpioLevel::Low)?;
488//!     sleep(Duration::from_millis(500));
489//! }
490//! # Ok(())
491//! # }
492//! ```
493//!
494//! ### Bulk GPIO Operations
495//!
496//! ```no_run
497//! use xr2280x_hid::{Xr2280x, GpioGroup, GpioDirection, GpioPin, device_find_first};
498//! use hidapi::HidApi;
499//!
500//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
501//! let hid_api = HidApi::new()?;
502//! let device = Xr2280x::device_open_first(&hid_api)?;
503//!
504//! // Configure pins 0-7 as outputs (LED array)
505//! let led_mask = 0x00FF; // Pins 0-7
506//!
507//! // Assign pins to EDGE controller individually
508//! for pin_num in 0..8 {
509//!     let pin = GpioPin::new(pin_num)?;
510//!     device.gpio_assign_to_edge(pin)?;
511//! }
512//!
513//! // Set direction for all pins at once using mask
514//! device.gpio_set_direction_masked(GpioGroup::Group0, led_mask, GpioDirection::Output)?;
515//!
516//! // Create a running light effect
517//! for i in 0..8 {
518//!     let pattern = 1u16 << i;
519//!     device.gpio_write_masked(GpioGroup::Group0, led_mask, pattern)?;
520//!     std::thread::sleep(std::time::Duration::from_millis(100));
521//! }
522//! # Ok(())
523//! # }
524//! ```
525//!
526//! ### GPIO Interrupt Handling
527//!
528//! The XR2280x devices support GPIO interrupts with configurable edge detection.
529//! The crate provides a **consistent Pin API** that eliminates the need for manual
530//! pin number conversions and provides type safety throughout.
531//!
532//! #### Consistent Pin API Benefits
533//!
534//! - **🛡️ Type Safety**: All pin numbers validated through `GpioPin::new()`
535//! - **🎯 Ergonomics**: No manual `u8` to `GpioPin` conversions required
536//! - **⚡ Error Handling**: Invalid pin numbers caught at API boundary
537//! - **🔄 Consistency**: Uniform use of `GpioPin` across all GPIO functions
538//!
539//! #### GPIO Edge Detection
540//!
541//! ```no_run
542//! use xr2280x_hid::{Xr2280x, GpioEdge, GpioPin, GpioPull, device_find_first};
543//! use hidapi::HidApi;
544//!
545//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
546//! let hid_api = HidApi::new()?;
547//! let device = Xr2280x::device_open_first(&hid_api)?;
548//!
549//! // Configure GPIO pins for interrupt monitoring
550//! let interrupt_pins = [0, 1, 2, 3];
551//!
552//! for &pin_num in &interrupt_pins {
553//!     let pin = GpioPin::new(pin_num)?;
554//!
555//!     // Assign pin to EDGE interface
556//!     device.gpio_assign_to_edge(pin)?;
557//!
558//!     // Configure as input with pull-up
559//!     device.gpio_setup_input(pin, GpioPull::Up)?;
560//!
561//!     // Enable interrupts on both edges
562//!     device.gpio_configure_interrupt(pin, true, true, true)?;
563//! }
564//!
565//! println!("GPIO interrupts configured. Monitoring for events...");
566//! # Ok(())
567//! # }
568//! ```
569//!
570//! #### Modern Interrupt Parsing API
571//!
572//! The new `parse_gpio_interrupt_pins()` function provides individual pin/edge
573//! combinations with full type safety:
574//!
575//! ```no_run
576//! use xr2280x_hid::{Xr2280x, GpioEdge, GpioLevel, device_find_first};
577//! use hidapi::HidApi;
578//!
579//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
580//! # let hid_api = HidApi::new()?;
581//! # let device = Xr2280x::device_open_first(&hid_api)?;
582//! // Read interrupt report with timeout
583//! let report = device.read_gpio_interrupt_report(Some(1000))?;
584//!
585//! // NEW: Get individual pin/edge combinations with type safety
586//! let pin_events = device.parse_gpio_interrupt_pins(&report)?;
587//!
588//! for (pin, edge) in pin_events {
589//!     println!("📌 Pin {} triggered on {:?} edge", pin.number(), edge);
590//!
591//!     // Direct use with other GPIO functions - no conversion needed!
592//!     let current_level = device.gpio_read(pin)?;
593//!     let direction = device.gpio_get_direction(pin)?;
594//!
595//!     // Validate edge detection
596//!     let edge_matches = matches!(
597//!         (edge, current_level),
598//!         (GpioEdge::Rising, GpioLevel::High) |
599//!         (GpioEdge::Falling, GpioLevel::Low) |
600//!         (GpioEdge::Both, _)
601//!     );
602//!
603//!     if edge_matches {
604//!         println!("✅ Edge detection consistent with current level");
605//!     }
606//! }
607//! # Ok(())
608//! # }
609//! ```
610//!
611//! #### API Migration Guide
612//!
613//! **Old Approach (Manual Parsing):**
614//! ```no_run
615//! # use xr2280x_hid::{Xr2280x, GpioPin};
616//! # fn old_example(device: &Xr2280x, report: &xr2280x_hid::GpioInterruptReport) -> xr2280x_hid::Result<()> {
617//! // ❌ OLD: Manual group mask parsing
618//! let parsed = unsafe { device.parse_gpio_interrupt_report(report)? };
619//!
620//! // User had to manually parse group masks and convert u8 to GpioPin
621//! for bit_pos in 0..16 {
622//!     if parsed.trigger_mask_group0 & (1 << bit_pos) != 0 {
623//!         let pin_num = bit_pos as u8;
624//!         let pin = GpioPin::new(pin_num)?; // Manual conversion required
625//!         // Use pin with other GPIO functions...
626//!     }
627//! }
628//! # Ok(())
629//! # }
630//! ```
631//!
632//! **New Approach (Consistent Pin API):**
633//! ```no_run
634//! # use xr2280x_hid::{Xr2280x, GpioPin};
635//! # fn new_example(device: &Xr2280x, report: &xr2280x_hid::GpioInterruptReport) -> xr2280x_hid::Result<()> {
636//! // ✅ NEW: Clean, type-safe API
637//! let pin_events = device.parse_gpio_interrupt_pins(report)?;
638//!
639//! for (pin, edge) in pin_events {
640//!     // Pin is already validated and typed - no conversion needed!
641//!     println!("Pin {} triggered on {:?} edge", pin.number(), edge);
642//!
643//!     // Direct use with GPIO functions
644//!     let level = device.gpio_read(pin)?;
645//! }
646//! # Ok(())
647//! # }
648//! ```
649//!
650//! #### Complete Interrupt Monitoring Example
651//!
652//! ```no_run
653//! use xr2280x_hid::{Xr2280x, GpioEdge, GpioLevel, GpioPin, GpioPull, device_find_first};
654//! use hidapi::HidApi;
655//! use std::collections::HashMap;
656//! use std::time::Duration;
657//!
658//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
659//! let hid_api = HidApi::new()?;
660//! let device = Xr2280x::device_open_first(&hid_api)?;
661//!
662//! // Setup interrupt monitoring
663//! let monitor_pins = [0, 1, 2, 3];
664//! let mut pin_event_counts: HashMap<u8, usize> = HashMap::new();
665//!
666//! for &pin_num in &monitor_pins {
667//!     let pin = GpioPin::new(pin_num)?;
668//!     device.gpio_assign_to_edge(pin)?;
669//!     device.gpio_setup_input(pin, GpioPull::Up)?;
670//!     device.gpio_configure_interrupt(pin, true, true, true)?;
671//!     pin_event_counts.insert(pin_num, 0);
672//! }
673//!
674//! println!("Monitoring GPIO interrupts. Connect/disconnect pins to generate events...");
675//!
676//! // Monitor for 10 seconds
677//! for _ in 0..100 {
678//!     match device.read_gpio_interrupt_report(Some(100)) {
679//!         Ok(report) => {
680//!             // Process interrupt events with type-safe API
681//!             let pin_events = device.parse_gpio_interrupt_pins(&report)?;
682//!
683//!             for (pin, edge) in pin_events {
684//!                 let count = pin_event_counts.entry(pin.number()).or_insert(0);
685//!                 *count += 1;
686//!
687//!                 println!("🎉 Pin {} {:?} edge (count: {})",
688//!                     pin.number(), edge, count);
689//!
690//!                 // Validate current state
691//!                 let current_level = device.gpio_read(pin)?;
692//!                 println!("   Current level: {:?}", current_level);
693//!             }
694//!         }
695//!         Err(xr2280x_hid::Error::Timeout) => {
696//!             // Normal timeout, continue monitoring
697//!             continue;
698//!         }
699//!         Err(e) => return Err(e.into()),
700//!     }
701//! }
702//!
703//! // Display summary
704//! println!("\nInterrupt Summary:");
705//! for (pin, count) in pin_event_counts {
706//!     println!("  Pin {}: {} events", pin, count);
707//! }
708//! # Ok(())
709//! # }
710//! ```
711//!
712//! #### Key Improvements
713//!
714//! 1. **Type Safety**: All pin numbers validated through `GpioPin::new()`
715//! 2. **API Consistency**: Entire GPIO API uses `GpioPin` throughout
716//! 3. **Error Handling**: Invalid pin numbers caught at API boundary
717//! 4. **Ergonomics**: No manual `u8` to `GpioPin` conversions required
718//! 5. **Edge Detection**: Typed `GpioEdge` enum for clear edge identification
719//!
720//! See the `consistent_pin_api.rs` and `gpio_interrupt_safe_usage.rs` examples
721//! for comprehensive demonstrations of interrupt handling patterns.
722//!
723//! ### PWM Generation
724//!
725//! ```no_run
726//! use xr2280x_hid::{Xr2280x, PwmChannel, PwmCommand, GpioPin, device_find_first};
727//! use hidapi::HidApi;
728//! use std::thread::sleep;
729//! use std::time::Duration;
730//!
731//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
732//! let hid_api = HidApi::new()?;
733//! let device = Xr2280x::device_open_first(&hid_api)?;
734//!
735//! // Configure PWM0 on GPIO pin 2 (servo control)
736//! let servo_pin = GpioPin::new(2)?;
737//! device.pwm_set_pin(PwmChannel::Pwm0, servo_pin)?;
738//!
739//! // Set servo PWM: 20ms period, variable pulse width
740//! let period_ns = 20_000_000; // 20ms = 50Hz
741//!
742//! // Servo positions: 1ms = 0°, 1.5ms = 90°, 2ms = 180°
743//! let positions = [1_000_000, 1_500_000, 2_000_000]; // pulse widths in ns
744//!
745//! device.pwm_control(PwmChannel::Pwm0, true, PwmCommand::FreeRun)?;
746//!
747//! // Move servo through positions
748//! for &pulse_width in &positions {
749//!     let low_time = period_ns - pulse_width;
750//!     device.pwm_set_periods_ns(PwmChannel::Pwm0, pulse_width, low_time)?;
751//!     sleep(Duration::from_millis(1000));
752//! }
753//!
754//! // Stop PWM
755//! device.pwm_control(PwmChannel::Pwm0, false, PwmCommand::Idle)?;
756//! # Ok(())
757//! # }
758//! ```
759//!
760//! ### LED Brightness Control
761//!
762//! ```no_run
763//! use xr2280x_hid::{Xr2280x, PwmChannel, PwmCommand, GpioPin, device_find_first};
764//! use hidapi::HidApi;
765//!
766//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
767//! let hid_api = HidApi::new()?;
768//! let device = Xr2280x::device_open_first(&hid_api)?;
769//!
770//! // Configure PWM1 on GPIO pin 5 for LED brightness
771//! let led_pin = GpioPin::new(5)?;
772//! device.pwm_set_pin(PwmChannel::Pwm1, led_pin)?;
773//!
774//! // High-frequency PWM for smooth dimming (1kHz)
775//! let frequency_hz = 1000;
776//! let period_ns = 1_000_000_000 / frequency_hz;
777//!
778//! device.pwm_control(PwmChannel::Pwm1, true, PwmCommand::FreeRun)?;
779//!
780//! // Fade from 0% to 100% brightness
781//! for brightness in 0..=100 {
782//!     let duty_cycle = brightness as f32 / 100.0;
783//!     let high_time = (period_ns as f32 * duty_cycle) as u64;
784//!     let low_time = period_ns - high_time;
785//!
786//!     device.pwm_set_periods_ns(PwmChannel::Pwm1, high_time, low_time)?;
787//!     std::thread::sleep(std::time::Duration::from_millis(50));
788//! }
789//! # Ok(())
790//! # }
791//! ```
792//!
793//! ## Code Quality and Maintainability
794//!
795//! This crate emphasizes high code quality through systematic elimination of common
796//! anti-patterns and the use of modern Rust best practices.
797//!
798//! ### Magic Number Elimination
799//!
800//! All hardcoded integer offsets in HID report parsing have been replaced with
801//! descriptive named constants, significantly improving code readability and
802//! maintainability.
803//!
804//! #### Before: Magic Numbers (Anti-pattern)
805//! ```ignore
806//! // ❌ Unclear what data is at each offset
807//! let status_flags = in_buf[1];
808//! let read_length = in_buf[3] as usize;
809//! read_buf.copy_from_slice(&in_buf[5..5 + actual_read_len]);
810//!
811//! // ❌ No context for interrupt report structure
812//! let group0_state = u16::from_le_bytes([report.raw_data[1], report.raw_data[2]]);
813//! let group1_state = u16::from_le_bytes([report.raw_data[3], report.raw_data[4]]);
814//! ```
815//!
816//! #### After: Named Constants (Best Practice)
817//! ```ignore
818//! // ✅ Self-documenting code with clear intent
819//! let status_flags = in_buf[response_offsets::STATUS_FLAGS];
820//! let read_length = in_buf[response_offsets::READ_LENGTH] as usize;
821//! read_buf.copy_from_slice(
822//!     &in_buf[response_offsets::READ_DATA_START
823//!         ..response_offsets::READ_DATA_START + actual_read_len]
824//! );
825//!
826//! // ✅ Clear GPIO interrupt report structure
827//! let group0_state = u16::from_le_bytes([
828//!     report.raw_data[report_offsets::GROUP0_STATE_LOW],
829//!     report.raw_data[report_offsets::GROUP0_STATE_HIGH],
830//! ]);
831//! let group1_state = u16::from_le_bytes([
832//!     report.raw_data[report_offsets::GROUP1_STATE_LOW],
833//!     report.raw_data[report_offsets::GROUP1_STATE_HIGH],
834//! ]);
835//! ```
836//!
837//! #### Benefits Achieved
838//!
839//! 1. **Improved Readability**: Code is self-documenting through descriptive constant names
840//! 2. **Enhanced Maintainability**: Single point of change if HID report structure changes
841//! 3. **Better Error Prevention**: Type system helps prevent using wrong constants in wrong contexts
842//! 4. **Documentation Value**: Constants serve as inline documentation of report structure
843//! 5. **Future-Proofing**: Easy to extend with new report types or fields
844//!
845//! The improvement affects **22 magic number locations** across **3 core files**,
846//! replacing them with **26 descriptive named constants** organized into **6 logical modules**.
847//!
848//! ## Performance
849//!
850//! This driver includes several optimizations for maximum performance:
851//!
852//! - **Fast I2C scanning**: Complete 112-address scan in ~1 second (500x faster than naive implementations)
853//! - **Bulk GPIO operations**: Update multiple pins in a single USB transaction
854//! - **Optimized timeouts**: Minimal delays while maintaining reliability
855//! - **Zero-copy reads**: Direct buffer access where possible
856//!
857//! ## Platform Support
858//!
859//! - **Linux**: Requires udev rules for non-root access
860//! - **Windows**: Works with built-in HID drivers
861//! - **macOS**: Supported via hidapi
862//!
863//! ### Linux Setup
864//!
865//! Create `/etc/udev/rules.d/99-xr2280x.rules`:
866//! ```text
867//! # XR2280x I2C Interface
868//! SUBSYSTEM=="hidraw", ATTRS{idVendor}=="04e2", ATTRS{idProduct}=="1100", MODE="0666"
869//! # XR2280x EDGE Interface
870//! SUBSYSTEM=="hidraw", ATTRS{idVendor}=="04e2", ATTRS{idProduct}=="1200", MODE="0666"
871//! ```
872//!
873//! Then reload udev rules: `sudo udevadm control --reload-rules`
874//!
875//! ## Architecture
876//!
877//! The XR2280x chips expose multiple USB HID interfaces as separate logical devices:
878//! - **I2C Interface** (PID 0x1100): I2C master controller with configurable speeds
879//! - **EDGE Interface** (PID 0x1200): GPIO, PWM, and interrupt controller
880//!
881//! This driver groups these logical interfaces by hardware device (using serial number)
882//! and automatically opens both interfaces to present a unified API for complete device access.
883//! The device approach eliminates the need to manage separate logical device connections.
884//!
885//! ## Error Handling
886//!
887//! All operations return `Result<T, Error>` with detailed error information:
888//!
889//! ```no_run
890//! use xr2280x_hid::{Xr2280x, Error, device_find_first};
891//! use hidapi::HidApi;
892//!
893//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
894//! let hid_api = HidApi::new()?;
895//! let device = Xr2280x::device_open_first(&hid_api)?;
896//!
897//! match device.i2c_write_7bit(0x50, &[0x00, 0x01]) {
898//!     Ok(()) => println!("Write successful"),
899//!     Err(Error::I2cNack { address }) => {
900//!         println!("Device at {:?} did not acknowledge", address);
901//!     },
902//!     Err(Error::DeviceNotFound) => {
903//!         println!("XR2280x device not connected");
904//!     },
905//!     Err(e) => println!("Other error: {}", e),
906//! }
907//! # Ok(())
908//! # }
909//! ```
910//!
911//! ### Hardware Device Selection Errors
912//!
913//! The hardware device selection methods provide specific error types for better error handling:
914//!
915//! ```no_run
916//! use xr2280x_hid::{Xr2280x, Error};
917//! use hidapi::HidApi;
918//!
919//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
920//! let hid_api = HidApi::new()?;
921//!
922//! // Handle specific hardware device selection errors
923//! match Xr2280x::open_by_serial(&hid_api, "NONEXISTENT") {
924//!     Ok(device) => println!("Hardware device opened successfully"),
925//!     Err(Error::DeviceNotFoundBySerial { serial, message }) => {
926//!         println!("No hardware device found with serial '{}': {}", serial, message);
927//!     },
928//!     Err(Error::DeviceNotFoundByIndex { index, message }) => {
929//!         println!("No hardware device found at index {}: {}", index, message);
930//!     },
931//!     Err(Error::MultipleDevicesFound { count, message }) => {
932//!         println!("Found {} devices when expecting one: {}", count, message);
933//!     },
934//!     Err(e) => println!("Other error: {}", e),
935//! }
936//! # Ok(())
937//! # }
938//! ```
939//!
940//! ## Safety and Limitations
941//!
942//! - GPIO pins operate at 3.3V logic levels
943//! - Maximum I2C speed is device-dependent (typically 400kHz)
944//! - PWM resolution depends on frequency (higher frequency = lower resolution)
945//! - No electrical isolation - use appropriate level shifters for 5V systems
946//!
947//! ## Troubleshooting
948//!
949//! **Device not found**: Check USB connection and permissions (udev rules on Linux)
950//!
951//! **I2C timeouts**: Verify I2C device connections and pull-up resistors
952//!
953//! **GPIO not working**: Ensure pins are assigned to EDGE interface before use
954//!
955//! **PWM frequency limits**: PWM resolution decreases at higher frequencies due to hardware constraints
956//! This driver supports both interfaces through a unified API.
957//!
958//! ## Thread Safety
959//!
960//! The `Xr2280x` handle is not thread-safe (`!Send`, `!Sync`) due to the underlying hidapi
961//! device handle. For concurrent access, use external synchronization or create separate
962//! handles for each thread.
963//!
964//! ## Error Handling
965//!
966//! All operations return a `Result<T, Error>` where `Error` provides detailed information
967//! about the failure, including:
968//! - HID communication errors
969//! - I2C bus errors (NACK, arbitration lost, timeout)
970//! - Invalid arguments or unsupported operations
971//! - Device-specific limitations
972//!
973//! ## Logging
974//!
975//! This crate uses the `log` crate for debugging output. Enable logging in your application
976//! to see detailed communication traces:
977//!
978//! ```no_run
979//! env_logger::init();
980//! ```
981//!
982//! ## Platform Support
983//!
984//! Supported on Windows, Linux, and macOS through the hidapi library.
985//! Requires appropriate permissions for USB device access on Linux.
986//!
987//! ## References
988//!
989//! - [XR2280x Datasheet](https://www.maxlinear.com/product/interface/uarts/usb-uarts/xr22804)
990//! - [Application Note AN365](https://www.maxlinear.com/appnote/AN365.pdf)
991
992// Re-export hidapi for convenience
993pub use hidapi;
994
995// Internal modules
996mod consts;
997mod error;
998
999// Public modules
1000pub mod device;
1001pub mod gpio;
1002pub mod i2c;
1003pub mod interrupt;
1004pub mod pwm;
1005
1006// Re-export main types and functions
1007pub use device::{
1008    Capabilities, Xr2280x, XrDeviceDetails, XrDeviceInfo, device_find, device_find_all,
1009    device_find_first,
1010};
1011pub use error::{Error, Result};
1012pub use gpio::{GpioDirection, GpioEdge, GpioGroup, GpioLevel, GpioPin, GpioPull, GpioTransaction};
1013pub use i2c::{I2cAddress, timeouts};
1014pub use interrupt::{GpioInterruptReport, ParsedGpioInterruptReport};
1015pub use pwm::{PwmChannel, PwmCommand};
1016
1017// Re-export essential hidapi types for multi-device selection
1018pub use hidapi::{DeviceInfo, HidApi};
1019
1020// Re-export only essential public constants
1021pub use consts::{EXAR_VID, XR2280X_EDGE_PID, XR2280X_I2C_PID};
1022
1023// --- Re-export necessary constants for public API use ---
1024/// Publicly accessible flags for controlling device features.
1025pub mod flags {
1026    /// Flags for use with [`crate::Xr2280x::i2c_transfer_raw`].
1027    pub mod i2c {
1028        // Re-export flags needed for i2c_transfer_raw
1029        pub use crate::consts::i2c::out_flags::{ACK_LAST_READ, START_BIT, STOP_BIT};
1030    }
1031    // Add other flags here if needed (e.g., for interrupts if a parsing API is added)
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036    use super::*;
1037
1038    #[test]
1039    fn test_gpio_pin_creation() {
1040        assert!(GpioPin::new(0).is_ok());
1041        assert!(GpioPin::new(31).is_ok());
1042        assert!(GpioPin::new(32).is_err());
1043    }
1044
1045    #[test]
1046    fn test_gpio_pin_helpers() {
1047        let pin = GpioPin::new(17).unwrap();
1048        assert_eq!(pin.number(), 17);
1049        assert_eq!(pin.group_index(), 1);
1050        assert_eq!(pin.bit_index(), 1);
1051        assert_eq!(pin.mask(), 0x0002);
1052    }
1053
1054    #[test]
1055    fn test_i2c_address_creation() {
1056        assert!(I2cAddress::new_7bit(0x50).is_ok());
1057        assert!(I2cAddress::new_7bit(0x7F).is_ok());
1058        assert!(I2cAddress::new_7bit(0x80).is_err());
1059
1060        assert!(I2cAddress::new_10bit(0x000).is_ok());
1061        assert!(I2cAddress::new_10bit(0x3FF).is_ok());
1062        assert!(I2cAddress::new_10bit(0x400).is_err());
1063    }
1064
1065    #[test]
1066    fn test_i2c_address_wire_format() {
1067        // Test that 7-bit addresses are correctly converted to 8-bit wire format
1068        // In I2C, a 7-bit address 0x50 becomes 0xA0 on the wire (shifted left)
1069
1070        // Common I2C device addresses and their expected wire format
1071        let test_cases = [
1072            (0x50, 0xA0), // EEPROM
1073            (0x68, 0xD0), // RTC
1074            (0x77, 0xEE), // Barometric sensor
1075            (0x3C, 0x78), // OLED display
1076            (0x48, 0x90), // Temperature sensor
1077        ];
1078
1079        for (addr_7bit, expected_wire) in test_cases {
1080            let addr = I2cAddress::new_7bit(addr_7bit).unwrap();
1081            if let I2cAddress::Bit7(a) = addr {
1082                let wire_format = a << 1;
1083                assert_eq!(
1084                    wire_format, expected_wire,
1085                    "7-bit address 0x{addr_7bit:02X} should become 0x{expected_wire:02X} on wire, got 0x{wire_format:02X}"
1086                );
1087            }
1088        }
1089
1090        // Verify the range is still correct after shifting
1091        assert_eq!(0x00 << 1, 0x00); // Minimum
1092        assert_eq!(0x7F << 1, 0xFE); // Maximum (0xFF would include R/W bit)
1093    }
1094
1095    #[test]
1096    fn test_pwm_unit_conversion() {
1097        // Test conversion constants
1098        let unit_ns = consts::edge::PWM_UNIT_TIME_NS;
1099
1100        // Helper to convert ns to pwm units (matching the implementation)
1101        let ns_to_units = |nanoseconds: u64| -> Result<u16> {
1102            if nanoseconds == 0 {
1103                return Err(Error::ArgumentOutOfRange(
1104                    "PWM time must be greater than 0 ns".to_string(),
1105                ));
1106            }
1107            let units = (nanoseconds as f64 / unit_ns).round() as u64;
1108            if units < consts::edge::PWM_MIN_UNITS as u64 {
1109                Err(Error::ArgumentOutOfRange("too small".to_string()))
1110            } else if units > consts::edge::PWM_MAX_UNITS as u64 {
1111                Err(Error::ArgumentOutOfRange("too large".to_string()))
1112            } else {
1113                Ok(units as u16)
1114            }
1115        };
1116
1117        // Helper to convert pwm units to ns
1118        let units_to_ns = |units: u16| -> u64 { (units as f64 * unit_ns).round() as u64 };
1119
1120        // Test basic conversions
1121        let units = ns_to_units(1000).unwrap();
1122        assert_eq!(units, 4); // 1000ns / 266.667ns ≈ 3.75, rounds to 4
1123
1124        let ns = units_to_ns(4);
1125        assert_eq!(ns, 1067); // 4 * 266.667ns ≈ 1066.67, rounds to 1067
1126
1127        // Test edge cases
1128        assert!(ns_to_units(0).is_err());
1129        assert!(ns_to_units(134).is_ok()); // Minimum that rounds to 1 unit
1130        assert!(ns_to_units(133).is_err()); // Just below minimum
1131        assert!(ns_to_units(1_100_000).is_err()); // Too large
1132
1133        // Test round-trip conversion accuracy
1134        for units in [1, 10, 100, 1000, 4095] {
1135            let ns = units_to_ns(units);
1136            let units_back = ns_to_units(ns).unwrap();
1137            // Should be exact or within 1 unit due to rounding
1138            assert!(
1139                units_back == units || units_back == units - 1 || units_back == units + 1,
1140                "Round-trip failed for {units} units: got {units_back} back"
1141            );
1142        }
1143    }
1144}