From 592315cd4588b018433383d038b13e8b075ede43 Mon Sep 17 00:00:00 2001 From: Alan Robertson Date: Sat, 15 Nov 2025 01:17:36 -0700 Subject: [PATCH 1/5] gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 67a1933..1a3a316 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ **/*.rs.bk **/.#* Cargo.lock +# IDE files +.idea/ +*.iml From 946b560a82f0e787cf41fb92322ae0a1831dcfac Mon Sep 17 00:00:00 2001 From: Alan Robertson Date: Sat, 15 Nov 2025 16:07:03 -0700 Subject: [PATCH 2/5] Made env var names and defaults consistent - and match my hardware setup --- examples/ice_bath_test.rs | 6 +++--- tests/100_ohm_test.rs | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/ice_bath_test.rs b/examples/ice_bath_test.rs index b1d5164..2ccb8a8 100644 --- a/examples/ice_bath_test.rs +++ b/examples/ice_bath_test.rs @@ -36,7 +36,7 @@ fn parse_filter_hz(s: &str) -> Result { fn main() -> Result<(), Box> { // Parse CLI args (simple manual scan for --key value; cargo passes after --) - let args: Vec = std::env::args().collect(); + let args: Vec = env::args().collect(); let mut cli_cs_pin: Option = None; let mut cli_leads: Option = None; let mut cli_filter: Option = None; @@ -75,7 +75,7 @@ fn main() -> Result<(), Box> { // Fallback to env vars, then defaults (CLI > env > default) let cs_pin_str = cli_cs_pin.map(|p| p.to_string()) .or_else(|| env::var("MAX31865_CS_PIN").ok()) - .unwrap_or_else(|| "8".to_string()); + .unwrap_or_else(|| "24".to_string()); let cs_pin: u8 = cs_pin_str.parse().map_err(|e| format!("Invalid CS_PIN '{}': {}", cs_pin_str, e))?; let leads_str = cli_leads.map(|l| match l { @@ -83,7 +83,7 @@ fn main() -> Result<(), Box> { RTDLeads::Three => "Three".to_string(), RTDLeads::Four => "Four".to_string(), }).or_else(|| env::var("MAX31865_LEADS").ok()) - .unwrap_or_else(|| "Three".to_string()); + .unwrap_or_else(|| "Four".to_string()); let leads = parse_rtd_leads(&leads_str).map_err(|e| format!("Invalid LEADS '{}': {}", leads_str, e))?; let filter_str = cli_filter.map(|f| match f { diff --git a/tests/100_ohm_test.rs b/tests/100_ohm_test.rs index ed23f36..8421900 100644 --- a/tests/100_ohm_test.rs +++ b/tests/100_ohm_test.rs @@ -1,3 +1,4 @@ +// Filename: tests/100_ohm_test.rs use std::env; #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] use simple_max31865::{RTDReader, RTDLeads, FilterHz}; @@ -13,7 +14,6 @@ use simple_max31865::{RTDReader, RTDLeads, FilterHz}; /// (100 ohm resistor) /// +---v\/\/\/\/----+ /// | | -/// | | /// +-----+ +-----+ /// | | | | /// + + + + @@ -50,24 +50,25 @@ fn get_pin_or_default(var_name: &str, default: u8) -> u8 { #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn setup_driver() -> RTDReader { - // Hardcode CS=24, 2-wire (TwoOrFourWire), 50Hz (as original; user can override pin externally if needed) - RTDReader::new(24, RTDLeads::Two, FilterHz::Fifty).unwrap() + // Hardcode CS=24, 4-wire (Four), 60Hz (defaults; user can override via env vars if needed) + let cs_pin = get_pin_or_default("MAX31865_CS_PIN", 24); + RTDReader::new(cs_pin, RTDLeads::Four, FilterHz::Sixty).unwrap() } #[test] fn test_env_pin_parsing() { // Simulate env vars (in real run, set them externally; here we mock via temp override for isolation) - std::env::set_var("NCS_PIN", "23"); + std::env::set_var("MAX31865_CS_PIN", "23"); - let ncs_pin = get_pin_or_default("NCS_PIN", 24); + let ncs_pin = get_pin_or_default("MAX31865_CS_PIN", 24); - assert_eq!(ncs_pin, 23, "Should parse NCS_PIN=23 from env"); + assert_eq!(ncs_pin, 23, "Should parse MAX31865_CS_PIN=23 from env"); // Reset for cleanliness (optional, but good practice) - std::env::remove_var("NCS_PIN"); + std::env::remove_var("MAX31865_CS_PIN"); // Fallback check - let default_ncs = get_pin_or_default("NCS_PIN", 24); + let default_ncs = get_pin_or_default("MAX31865_CS_PIN", 24); assert_eq!(default_ncs, 24, "Should fallback to 24 if unset"); } @@ -75,7 +76,7 @@ fn test_env_pin_parsing() { #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_configure_one_shot_fails() { // Placeholder: Verify new() succeeds for valid params (one-shot is always false in API) - let result = RTDReader::new(24, RTDLeads::Two, FilterHz::Fifty); + let result = RTDReader::new(24, RTDLeads::Four, FilterHz::Sixty); assert!(result.is_ok(), "new() should succeed for valid params (no one-shot)"); // Internal guard prevents one-shot; tested via code, not direct exposure } From 5a048ad5e8f57723b95018d1e87f8a94d6b0b9ed Mon Sep 17 00:00:00 2001 From: Alan Robertson Date: Sat, 15 Nov 2025 17:36:10 -0700 Subject: [PATCH 3/5] final updates for publishing --- Cargo.toml | 2 +- README.md | 129 ++++++++++++++++++++++++++++-------------- tests/100_ohm_test.rs | 56 +++++++++++------- 3 files changed, 122 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df826d9..01c3c38 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ rppal = { version = "0.17", features = ["embedded-hal"] } # Enables embedded-ha embedded-hal = "1.0" # Ensure v1 [dev-dependencies] -clap = { version = "4.0", features = ["derive"] } # For CLI args in examples +serial_test = "3.2.0" # For serial execution of hardware tests (avoids SPI/GPIO conflicts) [[example]] name = "ice_bath_test" diff --git a/README.md b/README.md index 4b5f106..d081f25 100755 --- a/README.md +++ b/README.md @@ -1,36 +1,35 @@ - # simple-max31865 -[![Crates.io](https://img.shields.io/crates/v/simple-max31865.svg)](https://crates.io/crates/simple-max31865) -[![Docs.rs](https://docs.rs/simple-max31865/badge.svg)](https://docs.rs/simple-max31865) -[![License](https://img.shields.io/badge/license-MIT%20or%20Apache-2.0-blue.svg)](LICENSE-APACHE) +[![Crates.io](https://img.shields.io/crates/v/simple-max31865.svg)↗](https://crates.io/crates/simple-max31865) [![Docs.rs](https://docs.rs/simple-max31865/badge.svg)↗](https://docs.rs/simple-max31865) [![License](https://img.shields.io/badge/license-MIT%20or%20Apache-2.0-blue.svg)](LICENSE-APACHE) + +An easy-to-use Raspberry Pi driver for the MAX31865 RTD to Digital converter -A Raspberry Pi driver for the MAX31865 RTD to Digital converter +## [Documentation↗](https://docs.rs/simple-max31865) -## [Documentation](https://docs.rs/simple-max31865) +## [Examples↗](https://github.com/Alan-R/simple-max31865-rs/tree/main/examples) -## [Examples](https://github.com/Alan-R/simple-max31865-rs/tree/main/examples) Examples are in the *examples* directory from the root of the tree. ## What works -- Reading the raw value and the converted temperature value either in f64 or scaled integer forms. -- Detecting and recovering from errors. -- Setting the ohmic calibration value. -- Interaction with the chip works well. -- Reading temperatures and resistances from PT-100 sensors. -- Fault detection and recovery. -- Support for Raspberry Pi (via `rppal` crate). -- Tested with Max31856 hardware from Playing With Fusion - and an Adafruit clone board, both with a PT100 sensor. +* Reading the raw value and the converted temperature value either in f64 or scaled integer forms. +* Detecting and recovering from errors. +* Setting the ohmic calibration value. +* Interaction with the chip works well. +* Reading temperatures and resistances from PT-100 sensors. +* Fault detection and recovery. +* Support for Raspberry Pi (via `rppal` crate). +* Tested with Max31856 hardware from Playing With Fusion and an Adafruit clone board, both with a PT100 sensor. ## TODO + See src/lib.rs for details. Future: Testing/Mocking -- Stub off hardware access by creating abstract implementations of Trait(s) and create minimal Mock unit tests (under a "mock" feature). -- Create mock implementation of SPI and Pin abstractions (controlled by a "mock" feature). -- Create "mock" hardware tests which exercise the APIs with mock feature. + +* Stub off hardware access by creating abstract implementations of Trait(s) and create minimal Mock unit tests (under a "mock" feature). +* Create mock implementation of SPI and Pin abstractions (controlled by a "mock" feature). +* Create "mock" hardware tests which exercise the APIs with mock feature. Many other thoughts listed in the `src/lib.rs` file. @@ -38,16 +37,19 @@ Many other thoughts listed in the `src/lib.rs` file. ### Raspberry Pi OS Configuration -- You need a Raspberry Pi with a working Max31865 HAT attached with a known chip select pin. -- enable GPIO/SPI via raspi-config or your OS settings before running. +* You need a Raspberry Pi with a working Max31865 HAT attached with a known chip select pin. +* Enable GPIO/SPI via `raspi-config` or your OS settings before running. ### Add to `Cargo.toml` - [dependencies] - simple-max31865 = "1.0" +```plaintext +[dependencies] +simple-max31865 = "1.0" +``` ### Basic Usage (Raspberry Pi) +```rust use simple_max31865::{decode_fault_status, RTDReader, RTDLeads, FilterHz}; fn main() -> Result<(), Box> { @@ -70,42 +72,83 @@ fn main() -> Result<(), Box> { } Ok(()) } +``` + +### Configuration Options + +The examples (e.g., `ice_bath_test`) support overriding defaults via CLI arguments or environment variables. +This is useful for customizing the chip select pin, lead configuration, or filter without modifying code. + +* **CLI Arguments** (passed after `--`): + + * `--cs-pin `: GPIO pin for chip select (default: 24). + * `--leads `: RTD lead configuration (default: "Three"). + * `--filter `: Noise filter (default: "Sixty"). + + Example: + + ``` + cargo run --example ice_bath_test -- --cs-pin 8 --leads Four --filter Fifty + ``` + +* **Environment Variables** (fallback if CLI not provided): + + * `MAX31865_CS_PIN`: GPIO pin for chip select (u8, e.g., "8"). + * `MAX31865_LEADS`: Lead configuration (e.g., "Four"). + * `MAX31865_FILTER`: Noise filter (e.g., "Fifty"). + + Example: + + ``` + export MAX31865_CS_PIN=8 + export MAX31865_LEADS=Four + export MAX31865_FILTER=Fifty + cargo run --example ice_bath_test + ``` + + +Precedence: CLI args > env vars > hardcoded defaults. + +### Running Hardware Tests + +The hardware validation tests (e.g., in `tests/100_ohm_test.rs`) access shared SPI/GPIO resources and use the `serial_test` crate to run serially by default. +No special flags are needed—just run them normally: + +```plaintext +cargo test test_read_resistance_hardware -- --ignored +``` -## Examples +For the full suite of ignored tests: -Full examples (e.g., basic reading, fault handling) will be added to the examples/ directory soon. -The Quick Start above demonstrates core usage. +```plaintext +cargo test -- --ignored +``` ## License Licensed under either of -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or - http://www.apache.org/licenses/LICENSE-2.0) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - at your option. +* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. # References -- [MAX31865 Datasheet](https://datasheets.maximintegrated.com/en/ds/MAX31865.pdf) -- [PT100 Sensor wiring diagrams](https://www.playingwithfusion.com/docs/1203) - a - great reference on wiring the various types of PT100 sensors. +* [MAX31865 Datasheet↗](https://datasheets.maximintegrated.com/en/ds/MAX31865.pdf) +* [PT100 Sensor wiring diagrams↗](https://www.playingwithfusion.com/docs/1203) - a great reference on wiring the various types of PT100 sensors. # Credits and License -The simple-max31865 crate is derived from version 1.0.0 of -the [rs-max31865](https://github.com/emeric-martineau/rs-max31865) +The simple-max31865 crate is derived from version 1.0.0 of the [rs-max31865↗](https://github.com/emeric-martineau/rs-max31865) crate by Rudi Horn and Emeric Martineau, with significant modifications: -- Greatly simplified, opaque API hiding hardware details. -- Added Raspberry Pi support via rppal. -- Floating-point temperature and resistance reads. +* Greatly simplified, opaque API hiding hardware details. +* Added Raspberry Pi support via rppal. +* Floating-point temperature and resistance reads. -Some original code snippets (e.g., register configs) and temperature conversion etc -are reused under the MIT/Apache-2.0 license. See the original repo for their contributions. +Some original code snippets (e.g., register configs) and temperature conversion etc are reused under the MIT/Apache-2.0 license. +See the original repo for their contributions. ### Contribution -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the -work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any -additional terms or conditions. \ No newline at end of file +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, +as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/tests/100_ohm_test.rs b/tests/100_ohm_test.rs index 8421900..d2318d4 100644 --- a/tests/100_ohm_test.rs +++ b/tests/100_ohm_test.rs @@ -2,6 +2,8 @@ use std::env; #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] use simple_max31865::{RTDReader, RTDLeads, FilterHz}; +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +use serial_test::serial; /// The 100 Ohm resistor test. /// /// Purpose of this test - to validate basic setup without having or relying @@ -20,15 +22,15 @@ use simple_max31865::{RTDReader, RTDLeads, FilterHz}; /// FRC+ RTD+ RTD- FRC- /// /// Wired this way it can be configured as either a 2 or 4 wire RTD probe. -/// Ideally, you'll use a 1% resistor or better. +/// Use a precision (1% or better) 100Ω resistor for accurate 0°C simulation. #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] -const RESISTOR_TOLERANCE: f64 = 0.20; //20% tolerance for generic 100Ω resistor (80–120Ω) +const RESISTOR_TOLERANCE: f64 = 0.05; // 5% tolerance for generic 100Ω resistor #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] const NOMINAL_OHMS: f64 = 100.0; #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] -const MIN_OHMS: f64 = NOMINAL_OHMS * (1.0 - RESISTOR_TOLERANCE); //80.0 +const MIN_OHMS: f64 = NOMINAL_OHMS * (1.0 - RESISTOR_TOLERANCE); #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] -const MAX_OHMS: f64 = NOMINAL_OHMS * (1.0 + RESISTOR_TOLERANCE); //120.0 +const MAX_OHMS: f64 = NOMINAL_OHMS * (1.0 + RESISTOR_TOLERANCE); #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] /// PT100 linear approximation: T = (R / 100 - 1) / 0.003851 (valid 0–300°C, per datasheet 5). @@ -37,9 +39,9 @@ const fn pt100_temperature_from_ohms(ohms: f64) -> f64 { } #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] -const MIN_TEMP_C: f64 = pt100_temperature_from_ohms(MIN_OHMS); // -52.5°C +const MIN_TEMP_C: f64 = pt100_temperature_from_ohms(MIN_OHMS); #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] -const MAX_TEMP_C: f64 = pt100_temperature_from_ohms(MAX_OHMS); // 51.9°C +const MAX_TEMP_C: f64 = pt100_temperature_from_ohms(MAX_OHMS); fn get_pin_or_default(var_name: &str, default: u8) -> u8 { env::var(var_name) @@ -58,14 +60,14 @@ fn setup_driver() -> RTDReader { #[test] fn test_env_pin_parsing() { // Simulate env vars (in real run, set them externally; here we mock via temp override for isolation) - std::env::set_var("MAX31865_CS_PIN", "23"); + env::set_var("MAX31865_CS_PIN", "23"); let ncs_pin = get_pin_or_default("MAX31865_CS_PIN", 24); assert_eq!(ncs_pin, 23, "Should parse MAX31865_CS_PIN=23 from env"); // Reset for cleanliness (optional, but good practice) - std::env::remove_var("MAX31865_CS_PIN"); + env::remove_var("MAX31865_CS_PIN"); // Fallback check let default_ncs = get_pin_or_default("MAX31865_CS_PIN", 24); @@ -82,6 +84,7 @@ fn test_configure_one_shot_fails() { } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_raw_hardware() { @@ -89,12 +92,13 @@ fn test_read_raw_hardware() { let raw = driver.get_raw_data().unwrap(); println!("Raw: {}", raw); - let expected_min = (MIN_OHMS / 200.0 * 32768.0) as u16; // ~13106 for 80Ω with 400Ω ref - let expected_max = (MAX_OHMS / 200.0 * 32768.0) as u16; // ~19659 for 120Ω - assert!(raw >= expected_min && raw <= expected_max, "Raw {} should be between {} and {} for 20% tolerance", raw, expected_min, expected_max); + let expected_min = (MIN_OHMS / 200.0 * 32768.0) as u16; // Computed bounds based on tolerance and reference resistor + let expected_max = (MAX_OHMS / 200.0 * 32768.0) as u16; + assert!(raw >= expected_min && raw <= expected_max, "Raw {} should be between {} and {} (±{:.1}% tolerance)", raw, expected_min, expected_max, RESISTOR_TOLERANCE * 100.0); } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_ohms_hardware() { @@ -102,23 +106,27 @@ fn test_read_ohms_hardware() { let ohms_raw = driver.get_ohms_100().unwrap(); println!("Ohms raw: {}", ohms_raw); - let expected_min = (MIN_OHMS * 100.0) as u32; // 8000 for 80Ω - let expected_max = (MAX_OHMS * 100.0) as u32; // 12000 for 120Ω - assert!(ohms_raw >= expected_min && ohms_raw <= expected_max, "Ohms raw {} should be between {} and {} for 20% tolerance", ohms_raw, expected_min, expected_max); + let expected_min = (MIN_OHMS * 100.0) as u32; // Computed bounds based on tolerance + let expected_max = (MAX_OHMS * 100.0) as u32; + assert!(ohms_raw >= expected_min && ohms_raw <= expected_max, "Ohms raw {} should be between {} and {} (±{:.1}% tolerance)", ohms_raw, expected_min, expected_max, RESISTOR_TOLERANCE * 100.0); } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_resistance_hardware() { let mut driver = setup_driver(); // Default pin let resistance = driver.get_resistance().unwrap(); + let observed_tolerance = ((resistance - NOMINAL_OHMS).abs() / NOMINAL_OHMS) * 100.0; println!("Resistance f64: {}", resistance); - assert!(resistance >= MIN_OHMS && resistance <= MAX_OHMS, "Resistance {} should be between {} and {} for 20% tolerance", resistance, MIN_OHMS, MAX_OHMS); + println!("Observed resistance: {:.2}Ω (tolerance: ±{:.2}%)", resistance, observed_tolerance); + assert!(resistance >= MIN_OHMS && resistance <= MAX_OHMS, "Resistance {} should be between {} and {} (±{:.1}% tolerance)", resistance, MIN_OHMS, MAX_OHMS, RESISTOR_TOLERANCE * 100.0); } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_default_conversion_hardware() { @@ -126,12 +134,13 @@ fn test_read_default_conversion_hardware() { let temp_raw = driver.read_temp_100().unwrap(); println!("Temp raw: {}", temp_raw); - let expected_min = (MIN_TEMP_C * 100.0) as i32; // ~ -5250 for -52.5°C *100 - let expected_max = (MAX_TEMP_C * 100.0) as i32; // ~ 5190 for 51.9°C *100 - assert!(temp_raw >= expected_min && temp_raw <= expected_max, "Temp raw {} should be between {} and {} for 20% tolerance", temp_raw, expected_min, expected_max); + let expected_min = (MIN_TEMP_C * 100.0) as i32; // Computed bounds based on tolerance + let expected_max = (MAX_TEMP_C * 100.0) as i32; + assert!(temp_raw >= expected_min && temp_raw <= expected_max, "Temp raw {} should be between {} and {} (±{:.1}% tolerance)", temp_raw, expected_min, expected_max, RESISTOR_TOLERANCE * 100.0); } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_temperature_hardware() { @@ -139,10 +148,11 @@ fn test_read_temperature_hardware() { let temperature = driver.get_temperature().unwrap(); println!("Temperature f64: {}", temperature); - assert!(temperature >= MIN_TEMP_C && temperature <= MAX_TEMP_C, "Temperature {} should be between {} and {} for 20% tolerance", temperature, MIN_TEMP_C, MAX_TEMP_C); + assert!(temperature >= MIN_TEMP_C && temperature <= MAX_TEMP_C, "Temperature {} should be between {} and {} (±{:.1}% tolerance)", temperature, MIN_TEMP_C, MAX_TEMP_C, RESISTOR_TOLERANCE * 100.0); } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_fault_status_hardware() { @@ -154,6 +164,7 @@ fn test_read_fault_status_hardware() { } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_clear_fault_hardware() { @@ -177,10 +188,11 @@ fn test_set_calibration() { let raw = 16382u16; // Mock raw for 100Ω let ohms_raw = ((raw as u32 >> 1) * 43000) >> 15; // u32 *100 with new calibration let ohms = ohms_raw as f64 / 100.0; - assert!((ohms - 100.0).abs() < 100.0 * RESISTOR_TOLERANCE, "Calibration 43000 should scale to ~100.0 ±20%"); + assert!((ohms - 100.0).abs() < NOMINAL_OHMS * RESISTOR_TOLERANCE, "Calibration 43000 should scale to ~100.0 ±{:.1}% tolerance", RESISTOR_TOLERANCE * 100.0); } #[test] +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_set_calibration_hardware() { @@ -188,6 +200,8 @@ fn test_set_calibration_hardware() { driver.set_calibration(43000); // 430Ω * 100 = 43000, for Adafruit match let resistance = driver.get_resistance().unwrap(); + let observed_tolerance = ((resistance - NOMINAL_OHMS).abs() / NOMINAL_OHMS) * 100.0; println!("Resistance with 43000 calibration: {}", resistance); - assert!(resistance >= MIN_OHMS && resistance <= MAX_OHMS, "Resistance {} should be between {} and {} for 20% tolerance", resistance, MIN_OHMS, MAX_OHMS); + println!("Observed resistance: {:.2}Ω (tolerance: ±{:.2}%)", resistance, observed_tolerance); + assert!(resistance >= MIN_OHMS && resistance <= MAX_OHMS, "Resistance {} should be between {} and {} (±{:.1}% tolerance)", resistance, MIN_OHMS, MAX_OHMS, RESISTOR_TOLERANCE * 100.0); } \ No newline at end of file From 7d49599207585f56fe8e468a77a5467ec266f27c Mon Sep 17 00:00:00 2001 From: Alan Robertson Date: Sat, 15 Nov 2025 19:13:24 -0700 Subject: [PATCH 4/5] Cargo fmt changes --- examples/ice_bath_test.rs | 199 +++++++++++----- src/lib.rs | 84 ++++--- src/temp_conversion.rs | 470 +++++++++++++++++++------------------- tests/100_ohm_test.rs | 109 +++++++-- 4 files changed, 516 insertions(+), 346 deletions(-) diff --git a/examples/ice_bath_test.rs b/examples/ice_bath_test.rs index 2ccb8a8..2121683 100644 --- a/examples/ice_bath_test.rs +++ b/examples/ice_bath_test.rs @@ -21,7 +21,10 @@ fn parse_rtd_leads(s: &str) -> Result { "two" | "2" => Ok(RTDLeads::Two), "three" | "3" => Ok(RTDLeads::Three), "four" | "4" => Ok(RTDLeads::Four), - _ => Err(format!("Invalid leads: {}. Must be Two/2, Three/3, or Four/4.", s)), + _ => Err(format!( + "Invalid leads: {}. Must be Two/2, Three/3, or Four/4.", + s + )), } } @@ -30,7 +33,10 @@ fn parse_filter_hz(s: &str) -> Result { match s.to_lowercase().as_str() { "fifty" | "50" => Ok(FilterHz::Fifty), "sixty" | "60" => Ok(FilterHz::Sixty), - _ => Err(format!("Invalid filter: {}. Must be Fifty/50 or Sixty/60.", s)), + _ => Err(format!( + "Invalid filter: {}. Must be Fifty/50 or Sixty/60.", + s + )), } } @@ -41,23 +47,33 @@ fn main() -> Result<(), Box> { let mut cli_leads: Option = None; let mut cli_filter: Option = None; - let mut i = 1; // Skip binary name (args[0]) + let mut i = 1; // Skip binary name (args[0]) while i < args.len() { if args[i].starts_with("--") { - let key = &args[i][2..]; // Strip "--" + let key = &args[i][2..]; // Strip "--" if i + 1 >= args.len() { return Err(format!("--{} requires a value", key).into()); } let value = &args[i + 1]; match key { "cs-pin" => { - cli_cs_pin = Some(value.parse().map_err(|e| format!("Invalid --cs-pin '{}': {}", value, e))?); + cli_cs_pin = Some( + value + .parse() + .map_err(|e| format!("Invalid --cs-pin '{}': {}", value, e))?, + ); } "leads" => { - cli_leads = Some(parse_rtd_leads(value).map_err(|e| format!("Invalid --leads '{}': {}", value, e))?); + cli_leads = Some( + parse_rtd_leads(value) + .map_err(|e| format!("Invalid --leads '{}': {}", value, e))?, + ); } "filter" => { - cli_filter = Some(parse_filter_hz(value).map_err(|e| format!("Invalid --filter '{}': {}", value, e))?); + cli_filter = Some( + parse_filter_hz(value) + .map_err(|e| format!("Invalid --filter '{}': {}", value, e))?, + ); } _ => { eprintln!("Unknown CLI arg: --{}", key); @@ -65,7 +81,7 @@ fn main() -> Result<(), Box> { std::process::exit(1); } } - i += 2; // Skip key + value + i += 2; // Skip key + value } else { eprintln!("Unexpected arg: {}", args[i]); i += 1; @@ -73,34 +89,57 @@ fn main() -> Result<(), Box> { } // Fallback to env vars, then defaults (CLI > env > default) - let cs_pin_str = cli_cs_pin.map(|p| p.to_string()) + let cs_pin_str = cli_cs_pin + .map(|p| p.to_string()) .or_else(|| env::var("MAX31865_CS_PIN").ok()) .unwrap_or_else(|| "24".to_string()); - let cs_pin: u8 = cs_pin_str.parse().map_err(|e| format!("Invalid CS_PIN '{}': {}", cs_pin_str, e))?; + let cs_pin: u8 = cs_pin_str + .parse() + .map_err(|e| format!("Invalid CS_PIN '{}': {}", cs_pin_str, e))?; - let leads_str = cli_leads.map(|l| match l { - RTDLeads::Two => "Two".to_string(), - RTDLeads::Three => "Three".to_string(), - RTDLeads::Four => "Four".to_string(), - }).or_else(|| env::var("MAX31865_LEADS").ok()) + let leads_str = cli_leads + .map(|l| match l { + RTDLeads::Two => "Two".to_string(), + RTDLeads::Three => "Three".to_string(), + RTDLeads::Four => "Four".to_string(), + }) + .or_else(|| env::var("MAX31865_LEADS").ok()) .unwrap_or_else(|| "Four".to_string()); - let leads = parse_rtd_leads(&leads_str).map_err(|e| format!("Invalid LEADS '{}': {}", leads_str, e))?; + let leads = + parse_rtd_leads(&leads_str).map_err(|e| format!("Invalid LEADS '{}': {}", leads_str, e))?; - let filter_str = cli_filter.map(|f| match f { - FilterHz::Fifty => "Fifty".to_string(), - FilterHz::Sixty => "Sixty".to_string(), - }).or_else(|| env::var("MAX31865_FILTER").ok()) + let filter_str = cli_filter + .map(|f| match f { + FilterHz::Fifty => "Fifty".to_string(), + FilterHz::Sixty => "Sixty".to_string(), + }) + .or_else(|| env::var("MAX31865_FILTER").ok()) .unwrap_or_else(|| "Sixty".to_string()); - let filter = parse_filter_hz(&filter_str).map_err(|e| format!("Invalid FILTER '{}': {}", filter_str, e))?; + let filter = parse_filter_hz(&filter_str) + .map_err(|e| format!("Invalid FILTER '{}': {}", filter_str, e))?; println!("=== MAX31865 Ice Bath Test ==="); println!("This test verifies your RTD sensor accuracy using an ice bath (~32°F/0°C)."); - println!("It checks initial room temp, monitors cooling in the bath, and recovery after removal."); + println!( + "It checks initial room temp, monitors cooling in the bath, and recovery after removal." + ); println!("Safety: Ensure probe is fully submerged (stir ice/water mix); keep water away from electronics."); - println!("Expected: Temps stabilize near 32°F in bath (±2°F ok), rise to 60°F+ after ~1-2 min."); + println!( + "Expected: Temps stabilize near 32°F in bath (±2°F ok), rise to 60°F+ after ~1-2 min." + ); println!("If temps don't change or faults occur, check wiring/probe."); - println!("Config: CS pin={}, {} leads, {} Hz filter", cs_pin, leads as u8, match filter { FilterHz::Fifty => 50, _ => 60 }); - println!("(From CLI/env vars: --cs-pin/--leads/--filter or MAX31865_*; defaults used if unset)"); + println!( + "Config: CS pin={}, {} leads, {} Hz filter", + cs_pin, + leads as u8, + match filter { + FilterHz::Fifty => 50, + _ => 60, + } + ); + println!( + "(From CLI/env vars: --cs-pin/--leads/--filter or MAX31865_*; defaults used if unset)" + ); println!(); let mut reader = RTDReader::new(cs_pin, leads, filter) @@ -113,7 +152,12 @@ fn main() -> Result<(), Box> { Ok(r) => r, Err(e) => { let status = reader.read_fault_status().unwrap_or(0); - eprintln!("Raw read failed: {:?}. Fault status: 0x{:02X} -> {:?}", e, status, decode_fault_status(status)); + eprintln!( + "Raw read failed: {:?}. Fault status: 0x{:02X} -> {:?}", + e, + status, + decode_fault_status(status) + ); return Err(format!("Raw read failed in initial phase: {:?}", e).into()); } }; @@ -142,16 +186,29 @@ fn main() -> Result<(), Box> { println!("Temperature: {:.1}°F ({:.1}°C)", temp_f, temp_c); if !(40.0..=110.0).contains(&temp_f) { - - eprintln!("Initial temp {:.1}°F out of range (expected 40-110°F).", temp_f); - eprintln!("Raw: 0x{:04X} (LSB fault: {}) | Resistance: {:.2} ohms | Temp: {:.1}°C", raw, raw & 1, resistance, temp_c); + eprintln!( + "Initial temp {:.1}°F out of range (expected 40-110°F).", + temp_f + ); + eprintln!( + "Raw: 0x{:04X} (LSB fault: {}) | Resistance: {:.2} ohms | Temp: {:.1}°C", + raw, + raw & 1, + resistance, + temp_c + ); eprintln!("Likely wiring/sensor issue: Check RTD connections (expected ~108 ohms at room temp, raw ~0x6A80)."); - eprintln!("If raw ~0x0000 or resistance ~0 ohms: Short circuit (check for solder bridges)."); + eprintln!( + "If raw ~0x0000 or resistance ~0 ohms: Short circuit (check for solder bridges)." + ); eprintln!("If raw ~0x7FFF or resistance very high: Open circuit (check wire continuity)."); return Err("Aborting test—fix hardware first.".into()); } - println!("Initial temp: {:.1}°F ({:.1}°C) - OK to proceed.", temp_f, temp_c); - let initial_temp_f = temp_f; // Save for summary + println!( + "Initial temp: {:.1}°F ({:.1}°C) - OK to proceed.", + temp_f, temp_c + ); + let initial_temp_f = temp_f; // Save for summary println!(); // Step 2: Ice Bath Phase @@ -162,24 +219,31 @@ fn main() -> Result<(), Box> { let bath_start = Instant::now(); let mut min_temp_f = f64::INFINITY; let mut consecutive_errors = 0; - let mut last_temp_f = initial_temp_f; // For stuck detection + let mut last_temp_f = initial_temp_f; // For stuck detection let mut stuck_warning = false; loop { if consecutive_errors >= 5 { - return Err("Too many consecutive read errors (5+). Aborting test—check hardware.".into()); + return Err( + "Too many consecutive read errors (5+). Aborting test—check hardware.".into(), + ); } match read_temp_with_faults(&mut reader, "Bath") { Ok(temp_f) => { - consecutive_errors = 0; // Reset on success + consecutive_errors = 0; // Reset on success min_temp_f = min_temp_f.min(temp_f); let elapsed = bath_start.elapsed().as_secs(); let temp_c = (temp_f - 32.0) * 5.0 / 9.0; - println!("Elapsed: {}s | Temp: {:.1}°F ({:.1}°C) | Min so far: {:.1}°F", elapsed, temp_f, temp_c, min_temp_f); + println!( + "Elapsed: {}s | Temp: {:.1}°F ({:.1}°C) | Min so far: {:.1}°F", + elapsed, temp_f, temp_c, min_temp_f + ); // Simple stuck check: Warn if no change for 10s (approx 10 reads) if (temp_f - last_temp_f).abs() < 0.1 && elapsed > 10 && !stuck_warning { - println!("Warning: Temperature not changing—stir bath or check for stuck reads."); + println!( + "Warning: Temperature not changing—stir bath or check for stuck reads." + ); stuck_warning = true; } last_temp_f = temp_f; @@ -200,8 +264,15 @@ fn main() -> Result<(), Box> { std::thread::sleep(Duration::from_secs(1)); } let bath_min_c = (min_temp_f - 32.0) * 5.0 / 9.0; - let reason = if bath_start.elapsed().as_secs() >= 300 { "timeout" } else { "threshold hit" }; - println!("\nBath phase done ({}). Min temp: {:.1}°F ({:.1}°C)", reason, min_temp_f, bath_min_c); + let reason = if bath_start.elapsed().as_secs() >= 300 { + "timeout" + } else { + "threshold hit" + }; + println!( + "\nBath phase done ({}). Min temp: {:.1}°F ({:.1}°C)", + reason, min_temp_f, bath_min_c + ); if min_temp_f > 34.0 { println!("Note: Expected ~32°F; yours was higher—ensure full immersion/stirring."); } @@ -228,7 +299,10 @@ fn main() -> Result<(), Box> { max_temp_f = max_temp_f.max(temp_f); let elapsed = recovery_start.elapsed().as_secs(); let temp_c = (temp_f - 32.0) * 5.0 / 9.0; - println!("Elapsed: {}s | Temp: {:.1}°F ({:.1}°C) | Max so far: {:.1}°F", elapsed, temp_f, temp_c, max_temp_f); + println!( + "Elapsed: {}s | Temp: {:.1}°F ({:.1}°C) | Max so far: {:.1}°F", + elapsed, temp_f, temp_c, max_temp_f + ); // Stuck check if (temp_f - last_temp_f).abs() < 0.1 && elapsed > 10 && !stuck_warning { @@ -243,7 +317,10 @@ fn main() -> Result<(), Box> { } Err(e) => { consecutive_errors += 1; - eprintln!("Read error (#{}) in recovery phase: {}", consecutive_errors, e); + eprintln!( + "Read error (#{}) in recovery phase: {}", + consecutive_errors, e + ); } } @@ -251,17 +328,29 @@ fn main() -> Result<(), Box> { } let final_temp_f = read_temp_with_faults(&mut reader, "Final")?; let final_temp_c = (final_temp_f - 32.0) * 5.0 / 9.0; - let reason = if recovery_start.elapsed().as_secs() >= 300 { "timeout" } else { "threshold hit" }; - println!("\nRecovery phase done ({}). Final temp: {:.1}°F ({:.1}°C)", reason, final_temp_f, final_temp_c); + let reason = if recovery_start.elapsed().as_secs() >= 300 { + "timeout" + } else { + "threshold hit" + }; + println!( + "\nRecovery phase done ({}). Final temp: {:.1}°F ({:.1}°C)", + reason, final_temp_f, final_temp_c + ); println!(); // Summary println!("=== Test Summary ==="); println!("Initial: {:.1}°F", initial_temp_f); println!("Bath min: {:.1}°F (expected ~32°F)", min_temp_f); - println!("Recovery final: {:.1}°F (expected near initial)", final_temp_f); - if !(30.0..=34.0).contains(&min_temp_f){ - println!("Potential issue: Bath temp off from 32°F. Recheck setup or compare to reference code."); + println!( + "Recovery final: {:.1}°F (expected near initial)", + final_temp_f + ); + if !(30.0..=34.0).contains(&min_temp_f) { + println!( + "Potential issue: Bath temp off from 32°F. Recheck setup or compare to reference code." + ); } else { println!("Test passed! Sensor behaving as expected."); } @@ -271,7 +360,10 @@ fn main() -> Result<(), Box> { } /// Read temp in °F, check/clear faults, log any. Returns Err on hard failure (after recovery attempt). -fn read_temp_with_faults(reader: &mut RTDReader, phase: &str) -> Result> { +fn read_temp_with_faults( + reader: &mut RTDReader, + phase: &str, +) -> Result> { // Read temp let temp_c = match reader.get_temperature() { Ok(t) => Ok(t), @@ -288,10 +380,15 @@ fn read_temp_with_faults(reader: &mut RTDReader, phase: &str) -> Result Ok(t), - Err(retry_e) => Err(format!("Retry failed after MAX fault: {:?}", retry_e)), + Err(retry_e) => { + Err(format!("Retry failed after MAX fault: {:?}", retry_e)) + } } } - _ => Err(format!("MAX fault during read (status unavailable): {:?}", e)), + _ => Err(format!( + "MAX fault during read (status unavailable): {:?}", + e + )), } } else { Err(format!("Hard read error in {} phase: {:?}", phase, e)) @@ -305,8 +402,8 @@ fn read_temp_with_faults(reader: &mut RTDReader, phase: &str) -> Result Result { - let gpio = Gpio::new().map_err(|e| RtdError::Init(format!("GPIO init failed: {}", e)))?; - let ncs = gpio.get(cs_pin).map_err(|e| RtdError::Init(format!("NCS pin {} invalid: {}", cs_pin, e)))?.into_output_high(); + let gpio = + Gpio::new().map_err(|e| RtdError::Init(format!("GPIO init failed: {}", e)))?; + let ncs = gpio + .get(cs_pin) + .map_err(|e| RtdError::Init(format!("NCS pin {} invalid: {}", cs_pin, e)))? + .into_output_high(); let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 1_000_000, SpiMode::Mode3) .map_err(|e| RtdError::Init(format!("SPI init failed: {}", e)))?; - let mut inner = Max31865::new(spi, ncs).map_err(|e| RtdError::Init(match e { - InternalError::GpioFault => "NCS pin setup failed".to_string(), - _ => "MAX31865 init failed".to_string(), - }))?; - inner.configure(leads, filter) + let mut inner = Max31865::new(spi, ncs).map_err(|e| { + RtdError::Init(match e { + InternalError::GpioFault => "NCS pin setup failed".to_string(), + _ => "MAX31865 init failed".to_string(), + }) + })?; + inner + .configure(leads, filter) .map_err(|e| RtdError::Init(format!("Configure failed: {:?}", e)))?; Ok(RTDReader { inner }) @@ -211,7 +218,9 @@ pub mod rtd_reader { /// Read temperature as scaled integer (degrees Celsius * 100). pub fn read_temp_100(&mut self) -> Result { - self.inner.read_default_conversion().map_err(map_internal_error) + self.inner + .read_default_conversion() + .map_err(map_internal_error) } /// Read resistance as scaled integer (ohms * 100). @@ -236,10 +245,12 @@ pub mod rtd_reader { /// Clear any latched faults (no-op if none). pub fn clear_fault(&mut self) -> Result<(), RtdError> { - self.inner.clear_fault().map_err(|e| RtdError::Read(match e { - InternalError::SpiErrorTransfer => "Clear fault SPI write failed".to_string(), - _ => "Clear fault failed".to_string(), - })) + self.inner.clear_fault().map_err(|e| { + RtdError::Read(match e { + InternalError::SpiErrorTransfer => "Clear fault SPI write failed".to_string(), + _ => "Clear fault failed".to_string(), + }) + }) } /// Set calibration (ohms * 100, e.g., 40000 for 400Ω). @@ -254,7 +265,7 @@ pub mod rtd_reader { InternalError::SpiErrorTransfer | InternalError::GpioFault => { RtdError::Read("SPI/GPIO transfer failed".to_string()) } - InternalError::MAXFault => RtdError::Fault(0), // Placeholder; call read_fault_status() for real status + InternalError::MAXFault => RtdError::Fault(0), // Placeholder; call read_fault_status() for real status } } } @@ -281,7 +292,7 @@ mod private { spi: SPI, ncs: NCS, calibration: u32, - base_config: u8, // Set in configure + base_config: u8, // Set in configure } impl Max31865 @@ -298,7 +309,7 @@ mod private { spi, ncs, calibration: default_calibration, - base_config: 0, // Set in configure + base_config: 0, // Set in configure }; Ok(max31865) @@ -319,25 +330,24 @@ mod private { /// Updates the devices configuration (internal use only). pub fn configure( &mut self, - sensor_type_enum: RTDLeads, // From public RTDLeads cast - filter_mode_enum: FilterHz, // From public FilterHz cast + sensor_type_enum: RTDLeads, // From public RTDLeads cast + filter_mode_enum: FilterHz, // From public FilterHz cast ) -> Result<(), Error> { - // Compute sensor type and filter mode bits directly let sensor_type = match sensor_type_enum { RTDLeads::Three => 1u8, - RTDLeads::Two | RTDLeads::Four => 0u8, // Two or Four = 0 + RTDLeads::Two | RTDLeads::Four => 0u8, // Two or Four = 0 }; let filter_mode = match filter_mode_enum { - FilterHz::Fifty => 1u8, // Fifty = 1 (low order bit) - FilterHz::Sixty => 0u8, // Sixty = 0 (no lower order bits) + FilterHz::Fifty => 1u8, // Fifty = 1 (low order bit) + FilterHz::Sixty => 0u8, // Sixty = 0 (no lower order bits) }; // One-shot config: V_BIAS=1, 1-SHOT=1, wires, filter (no AUTO= bit 3=0) self.base_config = (1u8 << 7) // V_BIAS=1 | (1u8 << 6) // 1-SHOT=1 (triggers on write) | (sensor_type << 4) // Wires bit 4 - | filter_mode; // Filter bit 0 - self.write(Register::CONFIG, self.base_config)?; // Initial write (starts first conversion) + | filter_mode; // Filter bit 0 + self.write(Register::CONFIG, self.base_config)?; // Initial write (starts first conversion) self.clear_fault()?; // Unlatch any boot faults (mimics Adafruit init) Ok(()) @@ -404,15 +414,16 @@ mod private { // Read RTD let buffer = self.read_two(Register::RTD_MSB)?; let raw = ((buffer[0] as u16) << 8) | (buffer[1] as u16); - if raw & 1 != 0 { // Fault: Clear + retry once - let _ = self.read_fault_status(); // Reads + clears faults - // Retry: Trigger again + if raw & 1 != 0 { + // Fault: Clear + retry once + let _ = self.read_fault_status(); // Reads + clears faults + // Retry: Trigger again self.write(Register::CONFIG, self.base_config)?; std::thread::sleep(std::time::Duration::from_millis(100)); let retry_buffer = self.read_two(Register::RTD_MSB)?; let retry_raw = ((retry_buffer[0] as u16) << 8) | (retry_buffer[1] as u16); if retry_raw & 1 != 0 { - return Err(Error::MAXFault); // Retry failed + return Err(Error::MAXFault); // Retry failed } return Ok(retry_raw); } @@ -464,7 +475,8 @@ mod private { #[allow(non_camel_case_types)] #[allow(dead_code)] #[derive(Clone, Copy)] - enum Register { // All the lovely Max31865 register offsets + enum Register { + // All the lovely Max31865 register offsets CONFIG = 0x00, RTD_MSB = 0x01, RTD_LSB = 0x02, @@ -487,4 +499,4 @@ mod private { *self as u8 | W } } -} \ No newline at end of file +} diff --git a/src/temp_conversion.rs b/src/temp_conversion.rs index 777f2b9..527eabc 100755 --- a/src/temp_conversion.rs +++ b/src/temp_conversion.rs @@ -1,235 +1,235 @@ -//! Temperature conversion vec and lookup function - -/// The lookup table maps temperature values to resistance values. The -/// temperature values are the range [min, min + step, ..., min + step * -/// (len(data) - 1)]. The resistance values are stored in the array data, -pub struct LookupTable<'a, D> { - min: i16, - step: i16, - data: &'a [D], -} - -fn interpolate(ohm_100: i32, first: (i32, i32), second: (i32, i32)) -> i32 { - let numerator = (second.0 - first.0) * (ohm_100 - first.1); - let denominator = second.1 - first.1; - - numerator / denominator + first.0 -} - -pub trait LookupToI32 { - fn lookup(&self, ind: usize) -> i32; - fn binary_search(&self, val: i32) -> Result; -} - -impl<'a> LookupToI32 for LookupTable<'a, u16> { - fn lookup(&self, ind: usize) -> i32 { - self.data[ind] as i32 - } - - fn binary_search(&self, val: i32) -> Result { - let val = val as u16; - self.data.binary_search(&val) - } -} - -impl<'a> LookupToI32 for LookupTable<'a, u32> { - fn lookup(&self, ind: usize) -> i32 { - self.data[ind] as i32 - } - - fn binary_search(&self, val: i32) -> Result { - let val = val as u32; - self.data.binary_search(&val) - } -} - -impl<'a, D> LookupTable<'a, D> -where - LookupTable<'a, D>: LookupToI32, -{ - fn reverse_index(&self, index: usize) -> i32 { - (self.min as i32 + (index * self.step as usize) as i32) * 100 - } - - /// The value from which lower bound interpolation should occur - fn ohm_lower_bound(&self) -> i32 { - self.lookup(1) - } - - /// The value from which upper bound interpolation should occur - fn ohm_upper_bound(&self) -> i32 { - self.lookup(self.data.len() - 2) - } - - fn interpolate_index(&self, ohm_100: i32, index: usize) -> i32 { - let first = (self.reverse_index(index), self.lookup(index)); - let second = (self.reverse_index(index + 1), self.lookup(index + 1)); - interpolate(ohm_100, first, second) - } - - /// Convert the specified resistance value into a temperature using the provided lookup table. - /// - /// # Arguments - /// - /// * `val` - A 16 bit unsigned integer specifying the resistance in Ohms - /// multiplied by 100, e.g. 13851 would indicate 138.51 Ohms and convert to 100 - /// degrees Celsius. - /// - /// # Remarks - /// - /// The output temperature will be in degrees Celsius multiplied by 100, e.g. - /// 10000 would signify 100.00 degrees Celsius. - /// - /// *Note*: This interpolates from the bottom or top values if the resistance - /// value is out of range. - pub fn lookup_temperature(&self, ohm_100: i32) -> i32 { - if ohm_100 < self.ohm_lower_bound() { - self.interpolate_index(ohm_100, 0) - } else if ohm_100 > self.ohm_upper_bound() { - self.interpolate_index(ohm_100, self.data.len() - 2) - } else { - let index = match self.binary_search(ohm_100) { - Ok(val) => val, - Err(val) => val - 1, - }; - self.interpolate_index(ohm_100, index) - } - } -} - -/// This lookup table contains the resistance values for a PT100 RTD ranging -/// from 0 C° up to 130 C° in steps of 10 C°, corresponding to a range from -/// 100.0 Ohms to 149.83 Ohms. -pub const LOOKUP_TABLE_PT100_SHORT: LookupTable<'static, u16> = LookupTable { - min: 0, - step: 10, - data: &[ - 10000, 10390, 10779, 11167, 11554, 11940, 12324, 12708, 13090, 13471, 13851, 14229, 14607, - 14983, - ], -}; - -/// This lookup table contains the resistance values for a PT100 RTD ranging -/// from -200 C° up to 800 C°, corresponding to a range from 18.52 Ohms to -/// 369.71 Ohms. Calculated using `fn make_lookup()` below. -pub const LOOKUP_VEC_PT100: LookupTable<'static, u32> = LookupTable { - min: -200, - step: 20, - data: &[ - 1852, 2710, 3554, 4388, 5211, 6026, 6833, 7633, 8427, 9216, 10000, 10779, 11554, 12324, - 13090, 13851, 14607, 15358, 16105, 16848, 17586, 18319, 19047, 19771, 20490, 21205, 21915, - 22621, 23321, 24018, 24709, 25396, 26078, 26756, 27429, 28098, 28762, 29421, 30075, 30725, - 31371, 32012, 32648, 33279, 33906, 34528, 35146, 35759, 36367, 36971, - ], -}; - -/// This lookup table contains the resistance values for a PT1000 RTD ranging -/// from -200 C° up to 800 C°, corresponding to a range from 185.20 Ohms to -/// 3697.12 Ohms. Calculated using `fn make_lookup()` below. -pub const LOOKUP_VEC_PT1000: LookupTable<'static, u32> = LookupTable { - min: -200, - step: 20, - data: &[ - 18520, 27096, 35543, 43876, 52110, 60256, 68325, 76328, 84271, 92160, 100000, 107794, - 115541, 123242, 130897, 138505, 146068, 153584, 161054, 168478, 175856, 183188, 190473, - 197712, 204905, 212052, 219152, 226206, 233214, 240176, 247092, 253961, 260785, 267562, - 274293, 280978, 287616, 294208, 300754, 307254, 313708, 320116, 326477, 332792, 339061, - 345284, 351460, 357590, 363674, 369712, - ], -}; - -/* -Don't understand how it works?!? -#[cfg(test)] -mod test { - use super::{ - index, lookup_temperature, lookup_temperature_pt1000, reverse_index, MAX, MIN, STEP, - }; - - const A: f64 = 3.9083e-3; - const B: f64 = -5.775e-7; - const C: f64 = -4.18301e-12; - - #[test] - fn make_lookup_pt100() { - make_lookup(100); - } - - #[test] - fn make_lookup_pt1000() { - make_lookup(1000); - } - - fn make_lookup(r0: u16) { - // use Callendar–Van Dusen equation - - /* - R(T) = R0(1 + aT + bT2 + c(T - 100)T3) - where: - T = temperature (NC) - R(T) = resistance at T - R0 = resistance at T = 0NC - IEC 751 specifies α = 0.00385055 and the following - Callendar-Van Dusen coefficient values: - a = 3.90830 x 10-3 - b = -5.77500 x 10-7 - c = -4.18301 - */ - - // according to wikipedia there are more accurate formula - let mut arr = [0u32; 50]; - - for t in (MIN..MAX).step_by(STEP) { - let c = if t < 0 { C } else { 0.0 }; - let t1 = t as f64; - let t2 = t1 * t1; - let t3 = t2 * t1; - //R_0*(1+a_*A4+b_*B4+D4*(A4-100)*C4) - let r = r0 as f64 * (1.0 + A * t1 + B * t2 + c * (t1 - 100.0) * t3); - - arr[index(t)] = (r * 100.0).round() as u32; - } - - if r0 == 100 { - // value taken from https://datasheets.maximintegrated.com/en/ds/MAX31865.pdf TABLE 9 - assert_eq!(arr[index(-200i16)], 1_852); - assert_eq!(arr[index(-100i16)], 6_026); - assert_eq!(arr[index(0i16)], 10_000); - assert_eq!(arr[index(100i16)], 13_851); - } else if r0 == 1000 { - assert_eq!(arr[index(0i16)], 100_000); - } - - //println!("{:?}", arr); - } - - #[test] - fn test_index() { - assert_eq!(index(-1), 9); - assert_eq!(index(0), 10); - assert_eq!(index(5), 10); - assert_eq!(index(20), 11); - } - - #[test] - fn test_reverse_index() { - assert_eq!(reverse_index(0), -20_000); // -200 C° - assert_eq!(reverse_index(1), -18_000); // -180 C° - assert_eq!(reverse_index(10), 0); - assert_eq!(reverse_index(20), 20_000); // 20 C° - } - - #[test] - fn test_lookup() { - assert!(lookup_temperature(0).is_none()); - - assert_eq!(lookup_temperature(10_000).unwrap(), 0); - assert_eq!(lookup_temperature(10_390).unwrap(), 1_001); - assert_eq!(lookup_temperature(20_000).unwrap(), 26_636); - assert_eq!(lookup_temperature(2_000).unwrap(), -19_656); - - assert_eq!(lookup_temperature_pt1000(100_000).unwrap(), 0); - assert_eq!(lookup_temperature_pt1000(103_900).unwrap(), 1_000); - } -} - */ \ No newline at end of file +//! Temperature conversion vec and lookup function + +/// The lookup table maps temperature values to resistance values. The +/// temperature values are the range [min, min + step, ..., min + step * +/// (len(data) - 1)]. The resistance values are stored in the array data, +pub struct LookupTable<'a, D> { + min: i16, + step: i16, + data: &'a [D], +} + +fn interpolate(ohm_100: i32, first: (i32, i32), second: (i32, i32)) -> i32 { + let numerator = (second.0 - first.0) * (ohm_100 - first.1); + let denominator = second.1 - first.1; + + numerator / denominator + first.0 +} + +pub trait LookupToI32 { + fn lookup(&self, ind: usize) -> i32; + fn binary_search(&self, val: i32) -> Result; +} + +impl<'a> LookupToI32 for LookupTable<'a, u16> { + fn lookup(&self, ind: usize) -> i32 { + self.data[ind] as i32 + } + + fn binary_search(&self, val: i32) -> Result { + let val = val as u16; + self.data.binary_search(&val) + } +} + +impl<'a> LookupToI32 for LookupTable<'a, u32> { + fn lookup(&self, ind: usize) -> i32 { + self.data[ind] as i32 + } + + fn binary_search(&self, val: i32) -> Result { + let val = val as u32; + self.data.binary_search(&val) + } +} + +impl<'a, D> LookupTable<'a, D> +where + LookupTable<'a, D>: LookupToI32, +{ + fn reverse_index(&self, index: usize) -> i32 { + (self.min as i32 + (index * self.step as usize) as i32) * 100 + } + + /// The value from which lower bound interpolation should occur + fn ohm_lower_bound(&self) -> i32 { + self.lookup(1) + } + + /// The value from which upper bound interpolation should occur + fn ohm_upper_bound(&self) -> i32 { + self.lookup(self.data.len() - 2) + } + + fn interpolate_index(&self, ohm_100: i32, index: usize) -> i32 { + let first = (self.reverse_index(index), self.lookup(index)); + let second = (self.reverse_index(index + 1), self.lookup(index + 1)); + interpolate(ohm_100, first, second) + } + + /// Convert the specified resistance value into a temperature using the provided lookup table. + /// + /// # Arguments + /// + /// * `val` - A 16 bit unsigned integer specifying the resistance in Ohms + /// multiplied by 100, e.g. 13851 would indicate 138.51 Ohms and convert to 100 + /// degrees Celsius. + /// + /// # Remarks + /// + /// The output temperature will be in degrees Celsius multiplied by 100, e.g. + /// 10000 would signify 100.00 degrees Celsius. + /// + /// *Note*: This interpolates from the bottom or top values if the resistance + /// value is out of range. + pub fn lookup_temperature(&self, ohm_100: i32) -> i32 { + if ohm_100 < self.ohm_lower_bound() { + self.interpolate_index(ohm_100, 0) + } else if ohm_100 > self.ohm_upper_bound() { + self.interpolate_index(ohm_100, self.data.len() - 2) + } else { + let index = match self.binary_search(ohm_100) { + Ok(val) => val, + Err(val) => val - 1, + }; + self.interpolate_index(ohm_100, index) + } + } +} + +/// This lookup table contains the resistance values for a PT100 RTD ranging +/// from 0 C° up to 130 C° in steps of 10 C°, corresponding to a range from +/// 100.0 Ohms to 149.83 Ohms. +pub const LOOKUP_TABLE_PT100_SHORT: LookupTable<'static, u16> = LookupTable { + min: 0, + step: 10, + data: &[ + 10000, 10390, 10779, 11167, 11554, 11940, 12324, 12708, 13090, 13471, 13851, 14229, 14607, + 14983, + ], +}; + +/// This lookup table contains the resistance values for a PT100 RTD ranging +/// from -200 C° up to 800 C°, corresponding to a range from 18.52 Ohms to +/// 369.71 Ohms. Calculated using `fn make_lookup()` below. +pub const LOOKUP_VEC_PT100: LookupTable<'static, u32> = LookupTable { + min: -200, + step: 20, + data: &[ + 1852, 2710, 3554, 4388, 5211, 6026, 6833, 7633, 8427, 9216, 10000, 10779, 11554, 12324, + 13090, 13851, 14607, 15358, 16105, 16848, 17586, 18319, 19047, 19771, 20490, 21205, 21915, + 22621, 23321, 24018, 24709, 25396, 26078, 26756, 27429, 28098, 28762, 29421, 30075, 30725, + 31371, 32012, 32648, 33279, 33906, 34528, 35146, 35759, 36367, 36971, + ], +}; + +/// This lookup table contains the resistance values for a PT1000 RTD ranging +/// from -200 C° up to 800 C°, corresponding to a range from 185.20 Ohms to +/// 3697.12 Ohms. Calculated using `fn make_lookup()` below. +pub const LOOKUP_VEC_PT1000: LookupTable<'static, u32> = LookupTable { + min: -200, + step: 20, + data: &[ + 18520, 27096, 35543, 43876, 52110, 60256, 68325, 76328, 84271, 92160, 100000, 107794, + 115541, 123242, 130897, 138505, 146068, 153584, 161054, 168478, 175856, 183188, 190473, + 197712, 204905, 212052, 219152, 226206, 233214, 240176, 247092, 253961, 260785, 267562, + 274293, 280978, 287616, 294208, 300754, 307254, 313708, 320116, 326477, 332792, 339061, + 345284, 351460, 357590, 363674, 369712, + ], +}; + +/* +Don't understand how it works?!? +#[cfg(test)] +mod test { + use super::{ + index, lookup_temperature, lookup_temperature_pt1000, reverse_index, MAX, MIN, STEP, + }; + + const A: f64 = 3.9083e-3; + const B: f64 = -5.775e-7; + const C: f64 = -4.18301e-12; + + #[test] + fn make_lookup_pt100() { + make_lookup(100); + } + + #[test] + fn make_lookup_pt1000() { + make_lookup(1000); + } + + fn make_lookup(r0: u16) { + // use Callendar–Van Dusen equation + + /* + R(T) = R0(1 + aT + bT2 + c(T - 100)T3) + where: + T = temperature (NC) + R(T) = resistance at T + R0 = resistance at T = 0NC + IEC 751 specifies α = 0.00385055 and the following + Callendar-Van Dusen coefficient values: + a = 3.90830 x 10-3 + b = -5.77500 x 10-7 + c = -4.18301 + */ + + // according to wikipedia there are more accurate formula + let mut arr = [0u32; 50]; + + for t in (MIN..MAX).step_by(STEP) { + let c = if t < 0 { C } else { 0.0 }; + let t1 = t as f64; + let t2 = t1 * t1; + let t3 = t2 * t1; + //R_0*(1+a_*A4+b_*B4+D4*(A4-100)*C4) + let r = r0 as f64 * (1.0 + A * t1 + B * t2 + c * (t1 - 100.0) * t3); + + arr[index(t)] = (r * 100.0).round() as u32; + } + + if r0 == 100 { + // value taken from https://datasheets.maximintegrated.com/en/ds/MAX31865.pdf TABLE 9 + assert_eq!(arr[index(-200i16)], 1_852); + assert_eq!(arr[index(-100i16)], 6_026); + assert_eq!(arr[index(0i16)], 10_000); + assert_eq!(arr[index(100i16)], 13_851); + } else if r0 == 1000 { + assert_eq!(arr[index(0i16)], 100_000); + } + + //println!("{:?}", arr); + } + + #[test] + fn test_index() { + assert_eq!(index(-1), 9); + assert_eq!(index(0), 10); + assert_eq!(index(5), 10); + assert_eq!(index(20), 11); + } + + #[test] + fn test_reverse_index() { + assert_eq!(reverse_index(0), -20_000); // -200 C° + assert_eq!(reverse_index(1), -18_000); // -180 C° + assert_eq!(reverse_index(10), 0); + assert_eq!(reverse_index(20), 20_000); // 20 C° + } + + #[test] + fn test_lookup() { + assert!(lookup_temperature(0).is_none()); + + assert_eq!(lookup_temperature(10_000).unwrap(), 0); + assert_eq!(lookup_temperature(10_390).unwrap(), 1_001); + assert_eq!(lookup_temperature(20_000).unwrap(), 26_636); + assert_eq!(lookup_temperature(2_000).unwrap(), -19_656); + + assert_eq!(lookup_temperature_pt1000(100_000).unwrap(), 0); + assert_eq!(lookup_temperature_pt1000(103_900).unwrap(), 1_000); + } +} + */ diff --git a/tests/100_ohm_test.rs b/tests/100_ohm_test.rs index d2318d4..6292ed6 100644 --- a/tests/100_ohm_test.rs +++ b/tests/100_ohm_test.rs @@ -1,9 +1,9 @@ // Filename: tests/100_ohm_test.rs -use std::env; -#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] -use simple_max31865::{RTDReader, RTDLeads, FilterHz}; #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] use serial_test::serial; +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +use simple_max31865::{FilterHz, RTDLeads, RTDReader}; +use std::env; /// The 100 Ohm resistor test. /// /// Purpose of this test - to validate basic setup without having or relying @@ -79,12 +79,15 @@ fn test_env_pin_parsing() { fn test_configure_one_shot_fails() { // Placeholder: Verify new() succeeds for valid params (one-shot is always false in API) let result = RTDReader::new(24, RTDLeads::Four, FilterHz::Sixty); - assert!(result.is_ok(), "new() should succeed for valid params (no one-shot)"); + assert!( + result.is_ok(), + "new() should succeed for valid params (no one-shot)" + ); // Internal guard prevents one-shot; tested via code, not direct exposure } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_raw_hardware() { @@ -94,11 +97,18 @@ fn test_read_raw_hardware() { println!("Raw: {}", raw); let expected_min = (MIN_OHMS / 200.0 * 32768.0) as u16; // Computed bounds based on tolerance and reference resistor let expected_max = (MAX_OHMS / 200.0 * 32768.0) as u16; - assert!(raw >= expected_min && raw <= expected_max, "Raw {} should be between {} and {} (±{:.1}% tolerance)", raw, expected_min, expected_max, RESISTOR_TOLERANCE * 100.0); + assert!( + raw >= expected_min && raw <= expected_max, + "Raw {} should be between {} and {} (±{:.1}% tolerance)", + raw, + expected_min, + expected_max, + RESISTOR_TOLERANCE * 100.0 + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_ohms_hardware() { @@ -108,11 +118,18 @@ fn test_read_ohms_hardware() { println!("Ohms raw: {}", ohms_raw); let expected_min = (MIN_OHMS * 100.0) as u32; // Computed bounds based on tolerance let expected_max = (MAX_OHMS * 100.0) as u32; - assert!(ohms_raw >= expected_min && ohms_raw <= expected_max, "Ohms raw {} should be between {} and {} (±{:.1}% tolerance)", ohms_raw, expected_min, expected_max, RESISTOR_TOLERANCE * 100.0); + assert!( + ohms_raw >= expected_min && ohms_raw <= expected_max, + "Ohms raw {} should be between {} and {} (±{:.1}% tolerance)", + ohms_raw, + expected_min, + expected_max, + RESISTOR_TOLERANCE * 100.0 + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_resistance_hardware() { @@ -121,12 +138,22 @@ fn test_read_resistance_hardware() { let resistance = driver.get_resistance().unwrap(); let observed_tolerance = ((resistance - NOMINAL_OHMS).abs() / NOMINAL_OHMS) * 100.0; println!("Resistance f64: {}", resistance); - println!("Observed resistance: {:.2}Ω (tolerance: ±{:.2}%)", resistance, observed_tolerance); - assert!(resistance >= MIN_OHMS && resistance <= MAX_OHMS, "Resistance {} should be between {} and {} (±{:.1}% tolerance)", resistance, MIN_OHMS, MAX_OHMS, RESISTOR_TOLERANCE * 100.0); + println!( + "Observed resistance: {:.2}Ω (tolerance: ±{:.2}%)", + resistance, observed_tolerance + ); + assert!( + resistance >= MIN_OHMS && resistance <= MAX_OHMS, + "Resistance {} should be between {} and {} (±{:.1}% tolerance)", + resistance, + MIN_OHMS, + MAX_OHMS, + RESISTOR_TOLERANCE * 100.0 + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_default_conversion_hardware() { @@ -136,11 +163,18 @@ fn test_read_default_conversion_hardware() { println!("Temp raw: {}", temp_raw); let expected_min = (MIN_TEMP_C * 100.0) as i32; // Computed bounds based on tolerance let expected_max = (MAX_TEMP_C * 100.0) as i32; - assert!(temp_raw >= expected_min && temp_raw <= expected_max, "Temp raw {} should be between {} and {} (±{:.1}% tolerance)", temp_raw, expected_min, expected_max, RESISTOR_TOLERANCE * 100.0); + assert!( + temp_raw >= expected_min && temp_raw <= expected_max, + "Temp raw {} should be between {} and {} (±{:.1}% tolerance)", + temp_raw, + expected_min, + expected_max, + RESISTOR_TOLERANCE * 100.0 + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_temperature_hardware() { @@ -148,11 +182,18 @@ fn test_read_temperature_hardware() { let temperature = driver.get_temperature().unwrap(); println!("Temperature f64: {}", temperature); - assert!(temperature >= MIN_TEMP_C && temperature <= MAX_TEMP_C, "Temperature {} should be between {} and {} (±{:.1}% tolerance)", temperature, MIN_TEMP_C, MAX_TEMP_C, RESISTOR_TOLERANCE * 100.0); + assert!( + temperature >= MIN_TEMP_C && temperature <= MAX_TEMP_C, + "Temperature {} should be between {} and {} (±{:.1}% tolerance)", + temperature, + MIN_TEMP_C, + MAX_TEMP_C, + RESISTOR_TOLERANCE * 100.0 + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_read_fault_status_hardware() { @@ -160,11 +201,14 @@ fn test_read_fault_status_hardware() { let status = driver.read_fault_status().unwrap(); println!("Fault status: 0x{:02X}", status); - assert_eq!(status, 0x00, "Fault status should be 0x00 (no fault with resistor)"); + assert_eq!( + status, 0x00, + "Fault status should be 0x00 (no fault with resistor)" + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_clear_fault_hardware() { @@ -172,7 +216,10 @@ fn test_clear_fault_hardware() { let status_before = driver.read_fault_status().unwrap(); println!("Status before clear: 0x{:02X}", status_before); - assert_eq!(status_before, 0x00, "Status before should be 0x00 (no fault)"); + assert_eq!( + status_before, 0x00, + "Status before should be 0x00 (no fault)" + ); driver.clear_fault().unwrap(); // No-op if no fault let status_after = driver.read_fault_status().unwrap(); @@ -188,11 +235,15 @@ fn test_set_calibration() { let raw = 16382u16; // Mock raw for 100Ω let ohms_raw = ((raw as u32 >> 1) * 43000) >> 15; // u32 *100 with new calibration let ohms = ohms_raw as f64 / 100.0; - assert!((ohms - 100.0).abs() < NOMINAL_OHMS * RESISTOR_TOLERANCE, "Calibration 43000 should scale to ~100.0 ±{:.1}% tolerance", RESISTOR_TOLERANCE * 100.0); + assert!( + (ohms - 100.0).abs() < NOMINAL_OHMS * RESISTOR_TOLERANCE, + "Calibration 43000 should scale to ~100.0 ±{:.1}% tolerance", + RESISTOR_TOLERANCE * 100.0 + ); } #[test] -#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts +#[serial] // Ensures serial execution to avoid SPI/GPIO conflicts #[ignore] // Manual hardware test: Wire 100Ω resistor across F+/F- (2/4-wire, no jumper) #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] fn test_set_calibration_hardware() { @@ -202,6 +253,16 @@ fn test_set_calibration_hardware() { let resistance = driver.get_resistance().unwrap(); let observed_tolerance = ((resistance - NOMINAL_OHMS).abs() / NOMINAL_OHMS) * 100.0; println!("Resistance with 43000 calibration: {}", resistance); - println!("Observed resistance: {:.2}Ω (tolerance: ±{:.2}%)", resistance, observed_tolerance); - assert!(resistance >= MIN_OHMS && resistance <= MAX_OHMS, "Resistance {} should be between {} and {} (±{:.1}% tolerance)", resistance, MIN_OHMS, MAX_OHMS, RESISTOR_TOLERANCE * 100.0); -} \ No newline at end of file + println!( + "Observed resistance: {:.2}Ω (tolerance: ±{:.2}%)", + resistance, observed_tolerance + ); + assert!( + resistance >= MIN_OHMS && resistance <= MAX_OHMS, + "Resistance {} should be between {} and {} (±{:.1}% tolerance)", + resistance, + MIN_OHMS, + MAX_OHMS, + RESISTOR_TOLERANCE * 100.0 + ); +} From cc1b906d88c77bd8230b8681032a1eb376cd3188 Mon Sep 17 00:00:00 2001 From: Alan Robertson Date: Sat, 15 Nov 2025 19:27:02 -0700 Subject: [PATCH 5/5] Added license files --- APACHE-LICENSE | 13 +++++++++++++ LICENSE-MIT | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 APACHE-LICENSE create mode 100644 LICENSE-MIT diff --git a/APACHE-LICENSE b/APACHE-LICENSE new file mode 100644 index 0000000..d8d155f --- /dev/null +++ b/APACHE-LICENSE @@ -0,0 +1,13 @@ +Copyright 2025 Alan Robertson + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..61c611d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,18 @@ +Copyright (c) 2025 Alan Robertson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file