Skip to main content

pamoja_ffi/
sensors.rs

1//! The C ABI for the sensor drivers.
2//!
3//! These functions wrap [`pamoja_sensors`] for callers that reach the SDK through
4//! the flat C boundary: the decode half of eleven common parts, turning the
5//! register bytes a bus driver read into the physical reading the datasheet says
6//! they mean, and the inverse builders that produce those bytes again.
7//!
8//! A reading is a handful of scalars, so it crosses by value as a `#[repr(C)]`
9//! struct rather than as a handle. The exceptions are the BME280 and BMP280
10//! calibrations, which are read once at start-up and then reused for every
11//! measurement, so each is a handle the caller keeps.
12//!
13//! Enumerated settings cross as the code the datasheet prints, because that is
14//! what a caller holding a register value already has in front of them, and
15//! booleans cross as a `uint8_t` that is `1` for the state its name describes.
16//!
17//! A part that carries its own checksum, an undefined register code, or an
18//! identification register reports a mismatch as [`PamojaStatus::Codec`], so a
19//! caller re-reads rather than trusting the value.
20
21use pamoja_sensors::{
22    ads1115, bme280, bmp280, ds18b20, hdc1080, ina219, ina226, opt3001, scd4x, sht3x, tmp117,
23    SensorError,
24};
25
26use crate::{read_bytes, set_last_error, PamojaStatus};
27
28/// The number of calibration bytes a BME280 reports for temperature and pressure.
29pub const PAMOJA_BME280_CALIBRATION_TEMP_PRESS_LEN: usize = 26;
30
31/// The number of calibration bytes a BME280 reports for humidity.
32pub const PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN: usize = 7;
33
34/// The number of measurement bytes a BME280 burst read returns.
35pub const PAMOJA_BME280_MEASUREMENT_LEN: usize = 8;
36
37/// The number of bytes in a DS18B20 scratchpad, the ninth being its CRC.
38pub const PAMOJA_DS18B20_SCRATCHPAD_LEN: usize = 9;
39
40/// An opaque handle to a BME280's factory calibration.
41///
42/// Read the calibration registers once at start-up, build one of these, and reuse
43/// it for every measurement. Release it with
44/// [`pamoja_bme280_calibration_free`].
45pub struct PamojaBme280Calibration {
46    calibration: bme280::Calibration,
47}
48
49/// A compensated BME280 reading.
50#[repr(C)]
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct PamojaBme280Measurement {
53    /// The temperature in degrees Celsius.
54    pub celsius: f32,
55    /// The pressure in pascals.
56    pub pascals: u32,
57    /// The pressure in hectopascals, the unit a barometer is usually quoted in.
58    pub hectopascals: f32,
59    /// The relative humidity as a percentage.
60    pub relative_humidity_percent: f32,
61}
62
63/// A decoded DS18B20 scratchpad.
64#[repr(C)]
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub struct PamojaDs18b20Reading {
67    /// The raw temperature register, 1/16 degree Celsius per count.
68    pub raw_temperature: i16,
69    /// The temperature in micro-degrees Celsius, exact in integer arithmetic.
70    pub micro_celsius: i32,
71    /// The high alarm threshold in whole degrees Celsius.
72    pub alarm_high: i8,
73    /// The low alarm threshold in whole degrees Celsius.
74    pub alarm_low: i8,
75    /// The configured resolution, as a number of bits: 9, 10, 11, or 12.
76    pub resolution_bits: u8,
77}
78
79/// An ADS1115 configuration register, field by field.
80///
81/// The multi-way settings carry the code the datasheet prints; the single-bit
82/// settings are named for the state that bit selects, so there is no code to look
83/// up for a flag.
84#[repr(C)]
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub struct PamojaAds1115Config {
87    /// `1` starts a single conversion when written.
88    pub start_conversion: u8,
89    /// The input multiplexer code, `0..=7`.
90    pub mux: u8,
91    /// The gain code, `0..=7`, which sets the full-scale range.
92    pub pga: u8,
93    /// `1` converts once per request and powers down, `0` converts continuously.
94    pub single_shot: u8,
95    /// The data rate code, `0..=7`.
96    pub data_rate: u8,
97    /// `1` selects the window comparator, `0` the traditional one.
98    pub window_comparator: u8,
99    /// `1` makes the ALERT/RDY pin active high.
100    pub comparator_active_high: u8,
101    /// `1` latches the comparator until the conversion is read.
102    pub comparator_latching: u8,
103    /// The comparator queue code, `0..=3`, where `3` disables the comparator.
104    pub comparator_queue: u8,
105}
106
107/// Builds a BME280 calibration from the bytes read out of its registers.
108///
109/// # Returns
110///
111/// [`PamojaStatus::Ok`] on success, with `*out_calibration` set to a new handle
112/// the caller must release with [`pamoja_bme280_calibration_free`], or
113/// [`PamojaStatus::InvalidArgument`] if either buffer is the wrong length.
114///
115/// # Safety
116///
117/// `temp_press` must point to at least `temp_press_len` readable bytes and
118/// `humidity` to at least `humidity_len`, and `out_calibration` must point to a
119/// writable `*mut PamojaBme280Calibration`.
120#[no_mangle]
121pub unsafe extern "C" fn pamoja_bme280_calibration_new(
122    temp_press: *const u8,
123    temp_press_len: usize,
124    humidity: *const u8,
125    humidity_len: usize,
126    out_calibration: *mut *mut PamojaBme280Calibration,
127) -> PamojaStatus {
128    if out_calibration.is_null() {
129        set_last_error("out_calibration must not be null".to_owned());
130        return PamojaStatus::InvalidArgument;
131    }
132    let slot = &mut *out_calibration;
133    *slot = std::ptr::null_mut();
134
135    let temp_press = match read_bytes(temp_press, temp_press_len) {
136        Ok(bytes) => bytes,
137        Err(status) => return status,
138    };
139    let humidity = match read_bytes(humidity, humidity_len) {
140        Ok(bytes) => bytes,
141        Err(status) => return status,
142    };
143
144    let Ok(temp_press) =
145        <[u8; PAMOJA_BME280_CALIBRATION_TEMP_PRESS_LEN]>::try_from(&temp_press[..])
146    else {
147        return wrong_length("temperature and pressure calibration", 26);
148    };
149    let Ok(humidity) = <[u8; PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN]>::try_from(&humidity[..])
150    else {
151        return wrong_length("humidity calibration", 7);
152    };
153
154    *slot = Box::into_raw(Box::new(PamojaBme280Calibration {
155        calibration: bme280::Calibration::from_registers(&temp_press, &humidity),
156    }));
157    PamojaStatus::Ok
158}
159
160/// Turns a BME280 burst read into a compensated reading.
161///
162/// # Returns
163///
164/// [`PamojaStatus::Ok`] on success, with `*out_measurement` filled in, or
165/// [`PamojaStatus::InvalidArgument`] if the calibration is null or the
166/// measurement is not eight bytes.
167///
168/// # Safety
169///
170/// `calibration` must be a live handle from [`pamoja_bme280_calibration_new`],
171/// `measurement` must point to at least `measurement_len` readable bytes, and
172/// `out_measurement` must point to a writable `PamojaBme280Measurement`.
173#[no_mangle]
174pub unsafe extern "C" fn pamoja_bme280_compensate(
175    calibration: *const PamojaBme280Calibration,
176    measurement: *const u8,
177    measurement_len: usize,
178    out_measurement: *mut PamojaBme280Measurement,
179) -> PamojaStatus {
180    if calibration.is_null() || out_measurement.is_null() {
181        set_last_error("calibration and out_measurement must not be null".to_owned());
182        return PamojaStatus::InvalidArgument;
183    }
184    let measurement = match read_bytes(measurement, measurement_len) {
185        Ok(bytes) => bytes,
186        Err(status) => return status,
187    };
188    let Ok(registers) = <[u8; PAMOJA_BME280_MEASUREMENT_LEN]>::try_from(&measurement[..]) else {
189        return wrong_length("measurement", 8);
190    };
191
192    let reading = (*calibration)
193        .calibration
194        .compensate(&bme280::RawMeasurement::from_registers(&registers));
195    *out_measurement = PamojaBme280Measurement {
196        celsius: reading.celsius(),
197        pascals: reading.pascals(),
198        hectopascals: reading.hectopascals(),
199        relative_humidity_percent: reading.relative_humidity_percent(),
200    };
201    PamojaStatus::Ok
202}
203
204/// Releases a BME280 calibration handle.
205///
206/// Passing null is a no-op.
207///
208/// # Safety
209///
210/// `calibration` must be a handle from [`pamoja_bme280_calibration_new`] that has
211/// not already been freed, or null. After this call it must not be used again.
212#[no_mangle]
213pub unsafe extern "C" fn pamoja_bme280_calibration_free(calibration: *mut PamojaBme280Calibration) {
214    if !calibration.is_null() {
215        drop(Box::from_raw(calibration));
216    }
217}
218
219/// Parses and CRC-checks a nine-byte DS18B20 scratchpad.
220///
221/// # Returns
222///
223/// [`PamojaStatus::Ok`] on success, with `*out_reading` filled in, or
224/// [`PamojaStatus::Codec`] if the CRC does not match, which means the read was
225/// corrupted on the bus and should be repeated.
226///
227/// # Safety
228///
229/// `bytes` must point to at least `bytes_len` readable bytes, and `out_reading`
230/// must point to a writable `PamojaDs18b20Reading`.
231#[no_mangle]
232pub unsafe extern "C" fn pamoja_ds18b20_parse_scratchpad(
233    bytes: *const u8,
234    bytes_len: usize,
235    out_reading: *mut PamojaDs18b20Reading,
236) -> PamojaStatus {
237    if out_reading.is_null() {
238        set_last_error("out_reading must not be null".to_owned());
239        return PamojaStatus::InvalidArgument;
240    }
241    let bytes = match read_bytes(bytes, bytes_len) {
242        Ok(bytes) => bytes,
243        Err(status) => return status,
244    };
245    let Ok(scratchpad) = <[u8; PAMOJA_DS18B20_SCRATCHPAD_LEN]>::try_from(&bytes[..]) else {
246        return wrong_length("scratchpad", 9);
247    };
248
249    match ds18b20::Scratchpad::parse(&scratchpad) {
250        Ok(reading) => {
251            *out_reading = PamojaDs18b20Reading {
252                raw_temperature: reading.raw_temperature(),
253                micro_celsius: reading.temperature_micro_celsius(),
254                alarm_high: reading.alarm_high(),
255                alarm_low: reading.alarm_low(),
256                resolution_bits: reading.resolution().bits(),
257            };
258            PamojaStatus::Ok
259        }
260        Err(error) => failed(error),
261    }
262}
263
264/// Builds the nine bytes a DS18B20 in the given state puts on the bus, CRC last.
265///
266/// This is the inverse of [`pamoja_ds18b20_parse_scratchpad`], so a node can be
267/// written and tested against what a thermometer sends without one attached.
268///
269/// # Returns
270///
271/// [`PamojaStatus::Ok`] on success, with the nine bytes written to `out_bytes`, or
272/// [`PamojaStatus::InvalidArgument`] if `bits` is not 9, 10, 11, or 12.
273///
274/// # Safety
275///
276/// `out_bytes` must point to at least nine writable bytes.
277#[no_mangle]
278pub unsafe extern "C" fn pamoja_ds18b20_build_scratchpad(
279    celsius: f32,
280    bits: u8,
281    alarm_high: i8,
282    alarm_low: i8,
283    out_bytes: *mut u8,
284) -> PamojaStatus {
285    if out_bytes.is_null() {
286        set_last_error("out_bytes must not be null".to_owned());
287        return PamojaStatus::InvalidArgument;
288    }
289    let Some(resolution) = resolution(bits) else {
290        return bad_resolution();
291    };
292    let raw = ds18b20::temperature_from_celsius(celsius, resolution);
293    let scratchpad = ds18b20::Scratchpad::new(raw, resolution, alarm_high, alarm_low);
294    core::ptr::copy_nonoverlapping(scratchpad.to_bytes().as_ptr(), out_bytes, 9);
295    PamojaStatus::Ok
296}
297
298/// Computes the Maxim CRC-8 a 1-Wire device checks its own bytes with.
299///
300/// # Returns
301///
302/// The checksum over `data`.
303///
304/// # Safety
305///
306/// `data` must point to at least `data_len` readable bytes, or be null when
307/// `data_len` is 0.
308#[no_mangle]
309pub unsafe extern "C" fn pamoja_ds18b20_crc8(data: *const u8, data_len: usize) -> u8 {
310    match read_bytes(data, data_len) {
311        Ok(data) => ds18b20::crc8(&data),
312        Err(_) => 0,
313    }
314}
315
316/// Converts a raw DS18B20 temperature register to micro-degrees Celsius.
317///
318/// # Returns
319///
320/// The temperature, exact in integer arithmetic.
321#[no_mangle]
322pub extern "C" fn pamoja_ds18b20_micro_celsius(raw: i16) -> i32 {
323    ds18b20::temperature_to_micro_celsius(raw)
324}
325
326/// Converts a raw DS18B20 temperature register to degrees Celsius.
327///
328/// # Returns
329///
330/// The temperature.
331#[no_mangle]
332pub extern "C" fn pamoja_ds18b20_celsius(raw: i16) -> f32 {
333    ds18b20::temperature_to_celsius(raw)
334}
335
336/// Returns the configuration byte that selects a DS18B20 resolution.
337///
338/// # Returns
339///
340/// [`PamojaStatus::Ok`] on success, with `*out_byte` set, or
341/// [`PamojaStatus::InvalidArgument`] if `bits` is not 9, 10, 11, or 12.
342///
343/// # Safety
344///
345/// `out_byte` must point to a writable `uint8_t`.
346#[no_mangle]
347pub unsafe extern "C" fn pamoja_ds18b20_config_byte(bits: u8, out_byte: *mut u8) -> PamojaStatus {
348    if out_byte.is_null() {
349        set_last_error("out_byte must not be null".to_owned());
350        return PamojaStatus::InvalidArgument;
351    }
352    match resolution(bits) {
353        Some(resolution) => {
354            *out_byte = resolution.config_byte();
355            PamojaStatus::Ok
356        }
357        None => bad_resolution(),
358    }
359}
360
361/// Returns the resolution a DS18B20 configuration byte selects.
362///
363/// # Returns
364///
365/// The number of bits: 9, 10, 11, or 12. Every byte names a resolution, so this
366/// never fails.
367#[no_mangle]
368pub extern "C" fn pamoja_ds18b20_resolution_bits(config_byte: u8) -> u8 {
369    ds18b20::Resolution::from_config_byte(config_byte).bits()
370}
371
372/// Returns the temperature step a DS18B20 resolution resolves.
373///
374/// # Returns
375///
376/// [`PamojaStatus::Ok`] on success, with `*out_micro_celsius` set, or
377/// [`PamojaStatus::InvalidArgument`] if `bits` is not 9, 10, 11, or 12.
378///
379/// # Safety
380///
381/// `out_micro_celsius` must point to a writable `uint32_t`.
382#[no_mangle]
383pub unsafe extern "C" fn pamoja_ds18b20_step_micro_celsius(
384    bits: u8,
385    out_micro_celsius: *mut u32,
386) -> PamojaStatus {
387    if out_micro_celsius.is_null() {
388        set_last_error("out_micro_celsius must not be null".to_owned());
389        return PamojaStatus::InvalidArgument;
390    }
391    match resolution(bits) {
392        Some(resolution) => {
393            *out_micro_celsius = resolution.step_micro_celsius();
394            PamojaStatus::Ok
395        }
396        None => bad_resolution(),
397    }
398}
399
400/// Returns how long a DS18B20 conversion may take at a resolution.
401///
402/// # Returns
403///
404/// [`PamojaStatus::Ok`] on success, with `*out_micros` set to the datasheet's
405/// worst case, or [`PamojaStatus::InvalidArgument`] if `bits` is not 9, 10, 11,
406/// or 12.
407///
408/// # Safety
409///
410/// `out_micros` must point to a writable `uint32_t`.
411#[no_mangle]
412pub unsafe extern "C" fn pamoja_ds18b20_max_conversion_micros(
413    bits: u8,
414    out_micros: *mut u32,
415) -> PamojaStatus {
416    if out_micros.is_null() {
417        set_last_error("out_micros must not be null".to_owned());
418        return PamojaStatus::InvalidArgument;
419    }
420    match resolution(bits) {
421        Some(resolution) => {
422            *out_micros = resolution.max_conversion_micros();
423            PamojaStatus::Ok
424        }
425        None => bad_resolution(),
426    }
427}
428
429/// Computes the INA219 calibration register for a shunt and current resolution.
430///
431/// # Returns
432///
433/// The register value to write.
434#[no_mangle]
435pub extern "C" fn pamoja_ina219_calibration(
436    current_lsb_microamps: u32,
437    shunt_milliohms: u32,
438) -> u16 {
439    ina219::calibration(current_lsb_microamps, shunt_milliohms)
440}
441
442/// Returns the smallest current resolution that still covers an expected maximum.
443///
444/// # Returns
445///
446/// The current LSB in microamps.
447#[no_mangle]
448pub extern "C" fn pamoja_ina219_minimum_current_lsb_microamps(max_expected_microamps: u32) -> u32 {
449    ina219::minimum_current_lsb_microamps(max_expected_microamps)
450}
451
452/// Builds the INA219 shunt-voltage register a monitor reports for a shunt voltage.
453///
454/// # Returns
455///
456/// The signed shunt-voltage register, at 10 uV per count.
457#[no_mangle]
458pub extern "C" fn pamoja_ina219_shunt_register(microvolts: i32) -> i16 {
459    ina219::shunt_register(microvolts)
460}
461
462/// Builds the INA219 bus-voltage register a monitor reports for a bus voltage.
463///
464/// # Returns
465///
466/// The bus-voltage register, with the conversion-ready flag set.
467#[no_mangle]
468pub extern "C" fn pamoja_ina219_bus_register(millivolts: u32) -> u16 {
469    ina219::bus_register(millivolts)
470}
471
472/// Builds the INA219 current register a monitor reports for a current.
473///
474/// # Returns
475///
476/// The signed current register, or zero if `current_lsb_microamps` is zero.
477#[no_mangle]
478pub extern "C" fn pamoja_ina219_current_register(
479    microamps: i32,
480    current_lsb_microamps: u32,
481) -> i16 {
482    ina219::current_register(microamps, current_lsb_microamps)
483}
484
485/// Builds the INA219 power register a monitor reports for a power.
486///
487/// # Returns
488///
489/// The power register, or zero if `current_lsb_microamps` is zero.
490#[no_mangle]
491pub extern "C" fn pamoja_ina219_power_register(microwatts: u32, current_lsb_microamps: u32) -> u16 {
492    ina219::power_register(microwatts, current_lsb_microamps)
493}
494
495/// Converts a raw INA219 shunt-voltage register to microvolts.
496///
497/// # Returns
498///
499/// The shunt voltage.
500#[no_mangle]
501pub extern "C" fn pamoja_ina219_shunt_microvolts(raw: i16) -> i32 {
502    ina219::shunt_microvolts(raw)
503}
504
505/// Converts a raw INA219 bus-voltage register to millivolts.
506///
507/// # Returns
508///
509/// The bus voltage.
510#[no_mangle]
511pub extern "C" fn pamoja_ina219_bus_millivolts(raw: u16) -> u32 {
512    ina219::bus_millivolts(raw)
513}
514
515/// Reports whether an INA219 bus-voltage register says a conversion is ready.
516///
517/// # Returns
518///
519/// `true` when the conversion-ready flag is set.
520#[no_mangle]
521pub extern "C" fn pamoja_ina219_conversion_ready(raw: u16) -> bool {
522    ina219::conversion_ready(raw)
523}
524
525/// Reports whether an INA219 bus-voltage register flags a math overflow.
526///
527/// # Returns
528///
529/// `true` when the current or power reading is meaningless and the calibration
530/// needs revisiting.
531#[no_mangle]
532pub extern "C" fn pamoja_ina219_math_overflow(raw: u16) -> bool {
533    ina219::math_overflow(raw)
534}
535
536/// Converts a raw INA219 current register to microamps.
537///
538/// # Returns
539///
540/// The current, at the resolution the calibration selected.
541#[no_mangle]
542pub extern "C" fn pamoja_ina219_current_microamps(raw: i16, current_lsb_microamps: u32) -> i32 {
543    ina219::current_microamps(raw, current_lsb_microamps)
544}
545
546/// Converts a raw INA219 power register to microwatts.
547///
548/// # Returns
549///
550/// The power, at the resolution the calibration selected.
551#[no_mangle]
552pub extern "C" fn pamoja_ina219_power_microwatts(raw: u16, current_lsb_microamps: u32) -> u32 {
553    ina219::power_microwatts(raw, current_lsb_microamps)
554}
555
556/// Assembles the 16-bit ADS1115 configuration register value.
557///
558/// # Returns
559///
560/// The register value to write, most significant bit first.
561#[no_mangle]
562pub extern "C" fn pamoja_ads1115_config_bits(config: PamojaAds1115Config) -> u16 {
563    ads1115::Config::from(config).bits()
564}
565
566/// Parses a 16-bit ADS1115 configuration register value.
567///
568/// # Returns
569///
570/// [`PamojaStatus::Ok`], with `*out_config` filled in. Every register value
571/// decodes, so this fails only on a null pointer.
572///
573/// # Safety
574///
575/// `out_config` must point to a writable `PamojaAds1115Config`.
576#[no_mangle]
577pub unsafe extern "C" fn pamoja_ads1115_config_from_bits(
578    bits: u16,
579    out_config: *mut PamojaAds1115Config,
580) -> PamojaStatus {
581    if out_config.is_null() {
582        set_last_error("out_config must not be null".to_owned());
583        return PamojaStatus::InvalidArgument;
584    }
585    *out_config = ads1115::Config::from_bits(bits).into();
586    PamojaStatus::Ok
587}
588
589/// Returns the full-scale range an ADS1115 gain code selects.
590///
591/// # Returns
592///
593/// The full scale in microvolts.
594#[no_mangle]
595pub extern "C" fn pamoja_ads1115_full_scale_microvolts(pga: u8) -> u32 {
596    ads1115::Pga::from_code(pga).full_scale_microvolts()
597}
598
599/// Returns the sample rate an ADS1115 data-rate code selects.
600///
601/// # Returns
602///
603/// The rate in samples per second.
604#[no_mangle]
605pub extern "C" fn pamoja_ads1115_samples_per_second(data_rate: u8) -> u16 {
606    ads1115::DataRate::from_code(data_rate).samples_per_second()
607}
608
609/// Converts a raw ADS1115 conversion result to nanovolts.
610///
611/// # Returns
612///
613/// The measured voltage, exact in integer arithmetic at every gain setting.
614#[no_mangle]
615pub extern "C" fn pamoja_ads1115_to_nanovolts(pga: u8, raw: i16) -> i64 {
616    ads1115::to_nanovolts(ads1115::Pga::from_code(pga), raw)
617}
618
619/// Converts a raw ADS1115 conversion result to volts.
620///
621/// # Returns
622///
623/// The measured voltage.
624#[no_mangle]
625pub extern "C" fn pamoja_ads1115_to_volts(pga: u8, raw: i16) -> f32 {
626    ads1115::to_volts(ads1115::Pga::from_code(pga), raw)
627}
628
629/// The number of calibration bytes a BMP280 reports.
630pub const PAMOJA_BMP280_CALIBRATION_LEN: usize = 24;
631
632/// The number of measurement bytes a BMP280 burst read returns.
633pub const PAMOJA_BMP280_DATA_LEN: usize = 6;
634
635/// The address a BMP280 answers on with its SDO pin low.
636pub const PAMOJA_BMP280_I2C_ADDRESS_PRIMARY: u8 = 0x76;
637
638/// The address it answers on with SDO high.
639pub const PAMOJA_BMP280_I2C_ADDRESS_SECONDARY: u8 = 0x77;
640
641/// The value a BMP280's chip-ID register reads, which confirms the part.
642pub const PAMOJA_BMP280_CHIP_ID: u8 = 0x58;
643
644/// The byte written to the reset register to restart a BMP280.
645pub const PAMOJA_BMP280_RESET_WORD: u8 = 0xB6;
646
647/// The raw code a BMP280 reports when oversampling is off and nothing was measured.
648pub const PAMOJA_BMP280_SKIPPED_OUTPUT: u32 = 0x80000;
649
650/// The first of the 24 BMP280 calibration registers.
651pub const PAMOJA_BMP280_REGISTER_CALIBRATION: u8 = 0x88;
652
653/// The BMP280 chip-ID register.
654pub const PAMOJA_BMP280_REGISTER_CHIP_ID: u8 = 0xD0;
655
656/// The BMP280 reset register.
657pub const PAMOJA_BMP280_REGISTER_RESET: u8 = 0xE0;
658
659/// The BMP280 status register.
660pub const PAMOJA_BMP280_REGISTER_STATUS: u8 = 0xF3;
661
662/// The BMP280 `ctrl_meas` register.
663pub const PAMOJA_BMP280_REGISTER_CTRL_MEAS: u8 = 0xF4;
664
665/// The BMP280 `config` register.
666pub const PAMOJA_BMP280_REGISTER_CONFIG: u8 = 0xF5;
667
668/// The first of the six BMP280 data registers.
669pub const PAMOJA_BMP280_REGISTER_DATA: u8 = 0xF7;
670
671// The header generator does not read the crates this one depends on, so these
672// carry their value rather than the name of the constant that defines it.
673const _: () = assert!(PAMOJA_BMP280_CALIBRATION_LEN == bmp280::CALIBRATION_LEN);
674const _: () = assert!(PAMOJA_BMP280_DATA_LEN == bmp280::DATA_LEN);
675const _: () = assert!(PAMOJA_BMP280_I2C_ADDRESS_PRIMARY == bmp280::I2C_ADDRESS_PRIMARY);
676const _: () = assert!(PAMOJA_BMP280_I2C_ADDRESS_SECONDARY == bmp280::I2C_ADDRESS_SECONDARY);
677const _: () = assert!(PAMOJA_BMP280_CHIP_ID == bmp280::CHIP_ID);
678const _: () = assert!(PAMOJA_BMP280_RESET_WORD == bmp280::RESET_WORD);
679const _: () = assert!(PAMOJA_BMP280_SKIPPED_OUTPUT == bmp280::SKIPPED_OUTPUT);
680const _: () = assert!(PAMOJA_BMP280_REGISTER_CALIBRATION == bmp280::register::CALIBRATION);
681const _: () = assert!(PAMOJA_BMP280_REGISTER_CHIP_ID == bmp280::register::CHIP_ID);
682const _: () = assert!(PAMOJA_BMP280_REGISTER_RESET == bmp280::register::RESET);
683const _: () = assert!(PAMOJA_BMP280_REGISTER_STATUS == bmp280::register::STATUS);
684const _: () = assert!(PAMOJA_BMP280_REGISTER_CTRL_MEAS == bmp280::register::CTRL_MEAS);
685const _: () = assert!(PAMOJA_BMP280_REGISTER_CONFIG == bmp280::register::CONFIG);
686const _: () = assert!(PAMOJA_BMP280_REGISTER_DATA == bmp280::register::DATA);
687
688/// An opaque handle to a BMP280's factory calibration.
689///
690/// Read the calibration registers once at start-up, build one of these, and reuse
691/// it for every measurement. Release it with
692/// [`pamoja_bmp280_calibration_free`].
693pub struct PamojaBmp280Calibration {
694    calibration: bmp280::Calibration,
695}
696
697/// A BMP280's per-chip trimming coefficients, as they sit in its registers.
698#[repr(C)]
699#[derive(Clone, Copy, Debug, PartialEq, Eq)]
700pub struct PamojaBmp280Coefficients {
701    /// The `dig_T1` coefficient.
702    pub dig_t1: u16,
703    /// The `dig_T2` coefficient.
704    pub dig_t2: i16,
705    /// The `dig_T3` coefficient.
706    pub dig_t3: i16,
707    /// The `dig_P1` coefficient.
708    pub dig_p1: u16,
709    /// The `dig_P2` coefficient.
710    pub dig_p2: i16,
711    /// The `dig_P3` coefficient.
712    pub dig_p3: i16,
713    /// The `dig_P4` coefficient.
714    pub dig_p4: i16,
715    /// The `dig_P5` coefficient.
716    pub dig_p5: i16,
717    /// The `dig_P6` coefficient.
718    pub dig_p6: i16,
719    /// The `dig_P7` coefficient.
720    pub dig_p7: i16,
721    /// The `dig_P8` coefficient.
722    pub dig_p8: i16,
723    /// The `dig_P9` coefficient.
724    pub dig_p9: i16,
725}
726
727/// A compensated BMP280 reading.
728#[repr(C)]
729#[derive(Clone, Copy, Debug, PartialEq)]
730pub struct PamojaBmp280Reading {
731    /// The temperature in degrees Celsius.
732    pub celsius: f32,
733    /// The pressure in pascals.
734    pub pascals: u32,
735    /// The pressure in hectopascals, the unit a barometer is usually quoted in.
736    pub hectopascals: f32,
737}
738
739/// The uncompensated codes a BMP280 burst read carries.
740#[repr(C)]
741#[derive(Clone, Copy, Debug, PartialEq, Eq)]
742pub struct PamojaBmp280Measurement {
743    /// The 20-bit pressure code.
744    pub pressure: u32,
745    /// The 20-bit temperature code.
746    pub temperature: u32,
747    /// `1` when pressure oversampling was off, so the code carries no reading.
748    pub pressure_skipped: u8,
749    /// `1` when temperature oversampling was off, so the code carries no reading.
750    pub temperature_skipped: u8,
751}
752
753/// A BMP280 `ctrl_meas` register, field by field.
754#[repr(C)]
755#[derive(Clone, Copy, Debug, PartialEq, Eq)]
756pub struct PamojaBmp280CtrlMeas {
757    /// The temperature oversampling code, `0..=5`, where `0` skips the measurement.
758    pub temperature: u8,
759    /// The pressure oversampling code, `0..=5`, where `0` skips the measurement.
760    pub pressure: u8,
761    /// The power mode code: `0` sleep, `1` forced, `3` normal.
762    pub mode: u8,
763}
764
765/// A BMP280 `config` register, field by field.
766#[repr(C)]
767#[derive(Clone, Copy, Debug, PartialEq, Eq)]
768pub struct PamojaBmp280Config {
769    /// The normal-mode standby code, `0..=7`.
770    pub standby: u8,
771    /// The IIR filter code, `0..=7`.
772    pub filter: u8,
773    /// `1` enables the 3-wire SPI interface.
774    pub spi_3wire: u8,
775}
776
777/// Builds a BMP280 calibration from the 24 bytes read out of its registers.
778///
779/// # Returns
780///
781/// [`PamojaStatus::Ok`] on success, with `*out_calibration` set to a new handle
782/// the caller must release with [`pamoja_bmp280_calibration_free`], or
783/// [`PamojaStatus::InvalidArgument`] if the buffer is not 24 bytes.
784///
785/// # Safety
786///
787/// `bytes` must point to at least `bytes_len` readable bytes, and
788/// `out_calibration` must point to a writable `*mut PamojaBmp280Calibration`.
789#[no_mangle]
790pub unsafe extern "C" fn pamoja_bmp280_calibration_new(
791    bytes: *const u8,
792    bytes_len: usize,
793    out_calibration: *mut *mut PamojaBmp280Calibration,
794) -> PamojaStatus {
795    if out_calibration.is_null() {
796        set_last_error("out_calibration must not be null".to_owned());
797        return PamojaStatus::InvalidArgument;
798    }
799    let slot = &mut *out_calibration;
800    *slot = std::ptr::null_mut();
801
802    let bytes = match read_bytes(bytes, bytes_len) {
803        Ok(bytes) => bytes,
804        Err(status) => return status,
805    };
806    let Ok(registers) = <[u8; PAMOJA_BMP280_CALIBRATION_LEN]>::try_from(&bytes[..]) else {
807        return wrong_length("calibration", PAMOJA_BMP280_CALIBRATION_LEN);
808    };
809
810    *slot = Box::into_raw(Box::new(PamojaBmp280Calibration {
811        calibration: bmp280::Calibration::parse(&registers),
812    }));
813    PamojaStatus::Ok
814}
815
816/// Turns a BMP280 burst read into a compensated reading.
817///
818/// # Returns
819///
820/// [`PamojaStatus::Ok`] on success, with `*out_reading` filled in, or
821/// [`PamojaStatus::InvalidArgument`] if the calibration is null or the
822/// measurement is not six bytes.
823///
824/// # Safety
825///
826/// `calibration` must be a live handle from [`pamoja_bmp280_calibration_new`],
827/// `measurement` must point to at least `measurement_len` readable bytes, and
828/// `out_reading` must point to a writable `PamojaBmp280Reading`.
829#[no_mangle]
830pub unsafe extern "C" fn pamoja_bmp280_compensate(
831    calibration: *const PamojaBmp280Calibration,
832    measurement: *const u8,
833    measurement_len: usize,
834    out_reading: *mut PamojaBmp280Reading,
835) -> PamojaStatus {
836    if calibration.is_null() || out_reading.is_null() {
837        set_last_error("calibration and out_reading must not be null".to_owned());
838        return PamojaStatus::InvalidArgument;
839    }
840    let measurement = match read_bytes(measurement, measurement_len) {
841        Ok(bytes) => bytes,
842        Err(status) => return status,
843    };
844    let Ok(registers) = <[u8; PAMOJA_BMP280_DATA_LEN]>::try_from(&measurement[..]) else {
845        return wrong_length("measurement", PAMOJA_BMP280_DATA_LEN);
846    };
847
848    let reading = (*calibration)
849        .calibration
850        .compensate(&bmp280::Measurement::parse(&registers));
851    *out_reading = PamojaBmp280Reading {
852        celsius: reading.celsius(),
853        pascals: reading.pascals(),
854        hectopascals: reading.hectopascals(),
855    };
856    PamojaStatus::Ok
857}
858
859/// Rebuilds the 24 calibration bytes a device holding these coefficients returns.
860///
861/// # Returns
862///
863/// [`PamojaStatus::Ok`] on success, with the 24 bytes written to `out_bytes`, or
864/// [`PamojaStatus::InvalidArgument`] if either pointer is null.
865///
866/// # Safety
867///
868/// `calibration` must be a live handle from [`pamoja_bmp280_calibration_new`],
869/// and `out_bytes` must point to at least 24 writable bytes.
870#[no_mangle]
871pub unsafe extern "C" fn pamoja_bmp280_calibration_to_bytes(
872    calibration: *const PamojaBmp280Calibration,
873    out_bytes: *mut u8,
874) -> PamojaStatus {
875    if calibration.is_null() || out_bytes.is_null() {
876        set_last_error("calibration and out_bytes must not be null".to_owned());
877        return PamojaStatus::InvalidArgument;
878    }
879    let bytes = (*calibration).calibration.to_bytes();
880    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_BMP280_CALIBRATION_LEN);
881    PamojaStatus::Ok
882}
883
884/// Reads out the trimming coefficients a BMP280 calibration carries.
885///
886/// # Returns
887///
888/// [`PamojaStatus::Ok`] on success, with `*out_coefficients` filled in, or
889/// [`PamojaStatus::InvalidArgument`] if either pointer is null.
890///
891/// # Safety
892///
893/// `calibration` must be a live handle from [`pamoja_bmp280_calibration_new`],
894/// and `out_coefficients` must point to a writable `PamojaBmp280Coefficients`.
895#[no_mangle]
896pub unsafe extern "C" fn pamoja_bmp280_calibration_coefficients(
897    calibration: *const PamojaBmp280Calibration,
898    out_coefficients: *mut PamojaBmp280Coefficients,
899) -> PamojaStatus {
900    if calibration.is_null() || out_coefficients.is_null() {
901        set_last_error("calibration and out_coefficients must not be null".to_owned());
902        return PamojaStatus::InvalidArgument;
903    }
904    *out_coefficients = (*calibration).calibration.into();
905    PamojaStatus::Ok
906}
907
908/// Releases a BMP280 calibration handle.
909///
910/// Passing null is a no-op.
911///
912/// # Safety
913///
914/// `calibration` must be a handle from [`pamoja_bmp280_calibration_new`] that has
915/// not already been freed, or null. After this call it must not be used again.
916#[no_mangle]
917pub unsafe extern "C" fn pamoja_bmp280_calibration_free(calibration: *mut PamojaBmp280Calibration) {
918    if !calibration.is_null() {
919        drop(Box::from_raw(calibration));
920    }
921}
922
923/// Unpacks the six data bytes a BMP280 burst read returns.
924///
925/// # Returns
926///
927/// [`PamojaStatus::Ok`] on success, with `*out_measurement` filled in, or
928/// [`PamojaStatus::InvalidArgument`] if the buffer is not six bytes.
929///
930/// # Safety
931///
932/// `data` must point to at least `data_len` readable bytes, and `out_measurement`
933/// must point to a writable `PamojaBmp280Measurement`.
934#[no_mangle]
935pub unsafe extern "C" fn pamoja_bmp280_parse_measurement(
936    data: *const u8,
937    data_len: usize,
938    out_measurement: *mut PamojaBmp280Measurement,
939) -> PamojaStatus {
940    if out_measurement.is_null() {
941        set_last_error("out_measurement must not be null".to_owned());
942        return PamojaStatus::InvalidArgument;
943    }
944    let data = match read_bytes(data, data_len) {
945        Ok(bytes) => bytes,
946        Err(status) => return status,
947    };
948    let Ok(registers) = <[u8; PAMOJA_BMP280_DATA_LEN]>::try_from(&data[..]) else {
949        return wrong_length("measurement", PAMOJA_BMP280_DATA_LEN);
950    };
951    *out_measurement = bmp280::Measurement::parse(&registers).into();
952    PamojaStatus::Ok
953}
954
955/// Builds the six data bytes a BMP280 holding these codes would return.
956///
957/// # Returns
958///
959/// [`PamojaStatus::Ok`] on success, with the six bytes written to `out_bytes`, or
960/// [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
961///
962/// # Safety
963///
964/// `out_bytes` must point to at least six writable bytes.
965#[no_mangle]
966pub unsafe extern "C" fn pamoja_bmp280_measurement_bytes(
967    pressure: u32,
968    temperature: u32,
969    out_bytes: *mut u8,
970) -> PamojaStatus {
971    if out_bytes.is_null() {
972        set_last_error("out_bytes must not be null".to_owned());
973        return PamojaStatus::InvalidArgument;
974    }
975    let measurement = bmp280::Measurement {
976        pressure,
977        temperature,
978    };
979    let bytes = measurement.to_bytes();
980    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_BMP280_DATA_LEN);
981    PamojaStatus::Ok
982}
983
984/// Reports whether a BMP280 status byte says a conversion is running.
985///
986/// # Returns
987///
988/// `true` while the part is measuring.
989#[no_mangle]
990pub extern "C" fn pamoja_bmp280_measuring(status: u8) -> bool {
991    bmp280::measuring(status)
992}
993
994/// Reports whether a BMP280 status byte says the calibration image is loading.
995///
996/// # Returns
997///
998/// `true` while the coefficients are being copied out of non-volatile memory.
999#[no_mangle]
1000pub extern "C" fn pamoja_bmp280_image_updating(status: u8) -> bool {
1001    bmp280::image_updating(status)
1002}
1003
1004/// Assembles a BMP280 `ctrl_meas` register value.
1005///
1006/// # Returns
1007///
1008/// The register value to write.
1009#[no_mangle]
1010pub extern "C" fn pamoja_bmp280_ctrl_meas_bits(config: PamojaBmp280CtrlMeas) -> u8 {
1011    bmp280::CtrlMeas::from(config).bits()
1012}
1013
1014/// Parses a BMP280 `ctrl_meas` register value.
1015///
1016/// # Returns
1017///
1018/// [`PamojaStatus::Ok`], with `*out_config` filled in. Every register value
1019/// decodes, so this fails only on a null pointer.
1020///
1021/// # Safety
1022///
1023/// `out_config` must point to a writable `PamojaBmp280CtrlMeas`.
1024#[no_mangle]
1025pub unsafe extern "C" fn pamoja_bmp280_ctrl_meas_from_bits(
1026    bits: u8,
1027    out_config: *mut PamojaBmp280CtrlMeas,
1028) -> PamojaStatus {
1029    if out_config.is_null() {
1030        set_last_error("out_config must not be null".to_owned());
1031        return PamojaStatus::InvalidArgument;
1032    }
1033    *out_config = bmp280::CtrlMeas::from_bits(bits).into();
1034    PamojaStatus::Ok
1035}
1036
1037/// Assembles a BMP280 `config` register value.
1038///
1039/// # Returns
1040///
1041/// The register value to write.
1042#[no_mangle]
1043pub extern "C" fn pamoja_bmp280_config_bits(config: PamojaBmp280Config) -> u8 {
1044    bmp280::Config::from(config).bits()
1045}
1046
1047/// Parses a BMP280 `config` register value.
1048///
1049/// # Returns
1050///
1051/// [`PamojaStatus::Ok`], with `*out_config` filled in. Every register value
1052/// decodes, so this fails only on a null pointer.
1053///
1054/// # Safety
1055///
1056/// `out_config` must point to a writable `PamojaBmp280Config`.
1057#[no_mangle]
1058pub unsafe extern "C" fn pamoja_bmp280_config_from_bits(
1059    bits: u8,
1060    out_config: *mut PamojaBmp280Config,
1061) -> PamojaStatus {
1062    if out_config.is_null() {
1063        set_last_error("out_config must not be null".to_owned());
1064        return PamojaStatus::InvalidArgument;
1065    }
1066    *out_config = bmp280::Config::from_bits(bits).into();
1067    PamojaStatus::Ok
1068}
1069
1070/// Returns how many samples a BMP280 oversampling code averages.
1071///
1072/// # Returns
1073///
1074/// The oversampling factor, `1` to `16`.
1075#[no_mangle]
1076pub extern "C" fn pamoja_bmp280_oversampling_factor(code: u8) -> u8 {
1077    bmp280::Oversampling::from_code(code).factor()
1078}
1079
1080/// Returns the normal-mode standby period a BMP280 code selects.
1081///
1082/// # Returns
1083///
1084/// The period in microseconds.
1085#[no_mangle]
1086pub extern "C" fn pamoja_bmp280_standby_micros(code: u8) -> u32 {
1087    bmp280::Standby::from_code(code).microseconds()
1088}
1089
1090/// The number of bytes in an SHT3x measurement frame, each word followed by its CRC.
1091pub const PAMOJA_SHT3X_MEASUREMENT_LEN: usize = 6;
1092
1093/// The number of bytes in an SHT3x word frame: the word then its CRC.
1094pub const PAMOJA_SHT3X_WORD_LEN: usize = 3;
1095
1096/// The address an SHT3x answers on with its ADDR pin low.
1097pub const PAMOJA_SHT3X_I2C_ADDRESS_A: u8 = 0x44;
1098
1099/// The address it answers on with ADDR high.
1100pub const PAMOJA_SHT3X_I2C_ADDRESS_B: u8 = 0x45;
1101
1102/// The gap an SHT3x needs between two commands, in microseconds.
1103pub const PAMOJA_SHT3X_MIN_COMMAND_GAP_MICROS: u32 = 1_000;
1104
1105/// The status word an SHT3x reads after a reset.
1106pub const PAMOJA_SHT3X_STATUS_DEFAULT: u16 = 0x8010;
1107
1108/// One high-repeatability measurement, holding the bus until it is ready.
1109pub const PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_HIGH_STRETCH: u16 = 0x2C06;
1110
1111/// One medium-repeatability measurement, holding the bus.
1112pub const PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_MEDIUM_STRETCH: u16 = 0x2C0D;
1113
1114/// One low-repeatability measurement, holding the bus.
1115pub const PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_LOW_STRETCH: u16 = 0x2C10;
1116
1117/// One high-repeatability measurement, released and fetched later.
1118pub const PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_HIGH: u16 = 0x2400;
1119
1120/// One medium-repeatability measurement, released and fetched later.
1121pub const PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_MEDIUM: u16 = 0x240B;
1122
1123/// One low-repeatability measurement, released and fetched later.
1124pub const PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_LOW: u16 = 0x2416;
1125
1126/// A measurement every two seconds at high repeatability.
1127pub const PAMOJA_SHT3X_COMMAND_PERIODIC_HALF_MPS_HIGH: u16 = 0x2032;
1128
1129/// A measurement every two seconds at medium repeatability.
1130pub const PAMOJA_SHT3X_COMMAND_PERIODIC_HALF_MPS_MEDIUM: u16 = 0x2024;
1131
1132/// A measurement every two seconds at low repeatability.
1133pub const PAMOJA_SHT3X_COMMAND_PERIODIC_HALF_MPS_LOW: u16 = 0x202F;
1134
1135/// One measurement a second at high repeatability.
1136pub const PAMOJA_SHT3X_COMMAND_PERIODIC_ONE_MPS_HIGH: u16 = 0x2130;
1137
1138/// One measurement a second at medium repeatability.
1139pub const PAMOJA_SHT3X_COMMAND_PERIODIC_ONE_MPS_MEDIUM: u16 = 0x2126;
1140
1141/// One measurement a second at low repeatability.
1142pub const PAMOJA_SHT3X_COMMAND_PERIODIC_ONE_MPS_LOW: u16 = 0x212D;
1143
1144/// Two measurements a second at high repeatability.
1145pub const PAMOJA_SHT3X_COMMAND_PERIODIC_TWO_MPS_HIGH: u16 = 0x2236;
1146
1147/// Two measurements a second at medium repeatability.
1148pub const PAMOJA_SHT3X_COMMAND_PERIODIC_TWO_MPS_MEDIUM: u16 = 0x2220;
1149
1150/// Two measurements a second at low repeatability.
1151pub const PAMOJA_SHT3X_COMMAND_PERIODIC_TWO_MPS_LOW: u16 = 0x222B;
1152
1153/// Four measurements a second at high repeatability.
1154pub const PAMOJA_SHT3X_COMMAND_PERIODIC_FOUR_MPS_HIGH: u16 = 0x2334;
1155
1156/// Four measurements a second at medium repeatability.
1157pub const PAMOJA_SHT3X_COMMAND_PERIODIC_FOUR_MPS_MEDIUM: u16 = 0x2322;
1158
1159/// Four measurements a second at low repeatability.
1160pub const PAMOJA_SHT3X_COMMAND_PERIODIC_FOUR_MPS_LOW: u16 = 0x2329;
1161
1162/// Ten measurements a second at high repeatability.
1163pub const PAMOJA_SHT3X_COMMAND_PERIODIC_TEN_MPS_HIGH: u16 = 0x2737;
1164
1165/// Ten measurements a second at medium repeatability.
1166pub const PAMOJA_SHT3X_COMMAND_PERIODIC_TEN_MPS_MEDIUM: u16 = 0x2721;
1167
1168/// Ten measurements a second at low repeatability.
1169pub const PAMOJA_SHT3X_COMMAND_PERIODIC_TEN_MPS_LOW: u16 = 0x272A;
1170
1171/// Accelerated response time: four measurements a second with a faster filter.
1172pub const PAMOJA_SHT3X_COMMAND_PERIODIC_ART: u16 = 0x2B32;
1173
1174/// Fetches the last result of a periodic measurement.
1175pub const PAMOJA_SHT3X_COMMAND_FETCH_DATA: u16 = 0xE000;
1176
1177/// Leaves periodic mode so another command can be accepted.
1178pub const PAMOJA_SHT3X_COMMAND_BREAK: u16 = 0x3093;
1179
1180/// Restarts the part as if it had been power-cycled.
1181pub const PAMOJA_SHT3X_COMMAND_SOFT_RESET: u16 = 0x30A2;
1182
1183/// The general-call reset, addressed to 0x00.
1184pub const PAMOJA_SHT3X_COMMAND_GENERAL_CALL_RESET: u16 = 0x0006;
1185
1186/// Turns the on-die heater on.
1187pub const PAMOJA_SHT3X_COMMAND_HEATER_ENABLE: u16 = 0x306D;
1188
1189/// Turns the on-die heater off.
1190pub const PAMOJA_SHT3X_COMMAND_HEATER_DISABLE: u16 = 0x3066;
1191
1192/// Reads the status register.
1193pub const PAMOJA_SHT3X_COMMAND_READ_STATUS: u16 = 0xF32D;
1194
1195/// Clears the latched flags in the status register.
1196pub const PAMOJA_SHT3X_COMMAND_CLEAR_STATUS: u16 = 0x3041;
1197
1198// The header generator does not read the crates this one depends on, so these
1199// carry their value rather than the name of the constant that defines it.
1200const _: () = assert!(PAMOJA_SHT3X_I2C_ADDRESS_A == sht3x::I2C_ADDRESS_A);
1201const _: () = assert!(PAMOJA_SHT3X_I2C_ADDRESS_B == sht3x::I2C_ADDRESS_B);
1202const _: () = assert!(PAMOJA_SHT3X_MIN_COMMAND_GAP_MICROS == sht3x::MIN_COMMAND_GAP_MICROS);
1203const _: () = assert!(PAMOJA_SHT3X_STATUS_DEFAULT == sht3x::Status::DEFAULT);
1204const _: () = assert!(
1205    PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_HIGH_STRETCH == sht3x::command::SINGLE_SHOT_HIGH_STRETCH
1206);
1207const _: () = assert!(
1208    PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_MEDIUM_STRETCH == sht3x::command::SINGLE_SHOT_MEDIUM_STRETCH
1209);
1210const _: () = assert!(
1211    PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_LOW_STRETCH == sht3x::command::SINGLE_SHOT_LOW_STRETCH
1212);
1213const _: () = assert!(PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_HIGH == sht3x::command::SINGLE_SHOT_HIGH);
1214const _: () =
1215    assert!(PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_MEDIUM == sht3x::command::SINGLE_SHOT_MEDIUM);
1216const _: () = assert!(PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_LOW == sht3x::command::SINGLE_SHOT_LOW);
1217const _: () =
1218    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_HALF_MPS_HIGH == sht3x::command::PERIODIC_0_5_MPS_HIGH);
1219const _: () = assert!(
1220    PAMOJA_SHT3X_COMMAND_PERIODIC_HALF_MPS_MEDIUM == sht3x::command::PERIODIC_0_5_MPS_MEDIUM
1221);
1222const _: () =
1223    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_HALF_MPS_LOW == sht3x::command::PERIODIC_0_5_MPS_LOW);
1224const _: () =
1225    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_ONE_MPS_HIGH == sht3x::command::PERIODIC_1_MPS_HIGH);
1226const _: () =
1227    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_ONE_MPS_MEDIUM == sht3x::command::PERIODIC_1_MPS_MEDIUM);
1228const _: () =
1229    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_ONE_MPS_LOW == sht3x::command::PERIODIC_1_MPS_LOW);
1230const _: () =
1231    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_TWO_MPS_HIGH == sht3x::command::PERIODIC_2_MPS_HIGH);
1232const _: () =
1233    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_TWO_MPS_MEDIUM == sht3x::command::PERIODIC_2_MPS_MEDIUM);
1234const _: () =
1235    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_TWO_MPS_LOW == sht3x::command::PERIODIC_2_MPS_LOW);
1236const _: () =
1237    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_FOUR_MPS_HIGH == sht3x::command::PERIODIC_4_MPS_HIGH);
1238const _: () =
1239    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_FOUR_MPS_MEDIUM == sht3x::command::PERIODIC_4_MPS_MEDIUM);
1240const _: () =
1241    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_FOUR_MPS_LOW == sht3x::command::PERIODIC_4_MPS_LOW);
1242const _: () =
1243    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_TEN_MPS_HIGH == sht3x::command::PERIODIC_10_MPS_HIGH);
1244const _: () =
1245    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_TEN_MPS_MEDIUM == sht3x::command::PERIODIC_10_MPS_MEDIUM);
1246const _: () =
1247    assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_TEN_MPS_LOW == sht3x::command::PERIODIC_10_MPS_LOW);
1248const _: () = assert!(PAMOJA_SHT3X_COMMAND_PERIODIC_ART == sht3x::command::PERIODIC_ART);
1249const _: () = assert!(PAMOJA_SHT3X_COMMAND_FETCH_DATA == sht3x::command::FETCH_DATA);
1250const _: () = assert!(PAMOJA_SHT3X_COMMAND_BREAK == sht3x::command::BREAK);
1251const _: () = assert!(PAMOJA_SHT3X_COMMAND_SOFT_RESET == sht3x::command::SOFT_RESET);
1252const _: () =
1253    assert!(PAMOJA_SHT3X_COMMAND_GENERAL_CALL_RESET == sht3x::command::GENERAL_CALL_RESET);
1254const _: () = assert!(PAMOJA_SHT3X_COMMAND_HEATER_ENABLE == sht3x::command::HEATER_ENABLE);
1255const _: () = assert!(PAMOJA_SHT3X_COMMAND_HEATER_DISABLE == sht3x::command::HEATER_DISABLE);
1256const _: () = assert!(PAMOJA_SHT3X_COMMAND_READ_STATUS == sht3x::command::READ_STATUS);
1257const _: () = assert!(PAMOJA_SHT3X_COMMAND_CLEAR_STATUS == sht3x::command::CLEAR_STATUS);
1258
1259/// A decoded SHT3x temperature and humidity pair.
1260#[repr(C)]
1261#[derive(Clone, Copy, Debug, PartialEq)]
1262pub struct PamojaSht3xMeasurement {
1263    /// The raw temperature word.
1264    pub temperature_raw: u16,
1265    /// The raw humidity word.
1266    pub humidity_raw: u16,
1267    /// The temperature in milli-degrees Celsius, exact in integer arithmetic.
1268    pub milli_celsius: i32,
1269    /// The temperature in degrees Celsius.
1270    pub celsius: f32,
1271    /// The temperature in milli-degrees Fahrenheit.
1272    pub milli_fahrenheit: i32,
1273    /// The temperature in degrees Fahrenheit.
1274    pub fahrenheit: f32,
1275    /// The relative humidity in milli-percent.
1276    pub milli_percent: u32,
1277    /// The relative humidity as a percentage.
1278    pub relative_humidity: f32,
1279}
1280
1281/// A decoded SHT3x status register.
1282#[repr(C)]
1283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1284pub struct PamojaSht3xStatus {
1285    /// The 16-bit status word the flags were read from.
1286    pub bits: u16,
1287    /// `1` when at least one alert condition is pending.
1288    pub alert_pending: u8,
1289    /// `1` while the on-die heater is running.
1290    pub heater_on: u8,
1291    /// `1` when a humidity tracking alert is set.
1292    pub humidity_tracking_alert: u8,
1293    /// `1` when a temperature tracking alert is set.
1294    pub temperature_tracking_alert: u8,
1295    /// `1` when the part has reset since the flag was last cleared.
1296    pub reset_detected: u8,
1297    /// `1` when the last command could not be processed.
1298    pub command_failed: u8,
1299    /// `1` when the last write failed its checksum.
1300    pub write_checksum_failed: u8,
1301}
1302
1303/// Computes the CRC-8 an SHT3x appends to every data word.
1304///
1305/// # Returns
1306///
1307/// The checksum over `data`, or 0 if the pointer is null with a non-zero length.
1308///
1309/// # Safety
1310///
1311/// `data` must point to at least `data_len` readable bytes, or be null when
1312/// `data_len` is 0.
1313#[no_mangle]
1314pub unsafe extern "C" fn pamoja_sht3x_crc(data: *const u8, data_len: usize) -> u8 {
1315    match read_bytes(data, data_len) {
1316        Ok(data) => sht3x::crc(&data),
1317        Err(_) => 0,
1318    }
1319}
1320
1321/// Reads a CRC-checked three-byte SHT3x word frame.
1322///
1323/// # Returns
1324///
1325/// [`PamojaStatus::Ok`] on success, with `*out_word` set, or
1326/// [`PamojaStatus::Codec`] if the CRC does not match.
1327///
1328/// # Safety
1329///
1330/// `frame` must point to at least `frame_len` readable bytes, and `out_word` must
1331/// point to a writable `uint16_t`.
1332#[no_mangle]
1333pub unsafe extern "C" fn pamoja_sht3x_word(
1334    frame: *const u8,
1335    frame_len: usize,
1336    out_word: *mut u16,
1337) -> PamojaStatus {
1338    if out_word.is_null() {
1339        set_last_error("out_word must not be null".to_owned());
1340        return PamojaStatus::InvalidArgument;
1341    }
1342    let frame = match read_bytes(frame, frame_len) {
1343        Ok(bytes) => bytes,
1344        Err(status) => return status,
1345    };
1346    let Ok(frame) = <[u8; PAMOJA_SHT3X_WORD_LEN]>::try_from(&frame[..]) else {
1347        return wrong_length("word frame", PAMOJA_SHT3X_WORD_LEN);
1348    };
1349    match sht3x::word(&frame) {
1350        Ok(word) => {
1351            *out_word = word;
1352            PamojaStatus::Ok
1353        }
1354        Err(error) => failed(error),
1355    }
1356}
1357
1358/// Builds the three bytes an SHT3x sends for a word: the word then its CRC.
1359///
1360/// # Returns
1361///
1362/// [`PamojaStatus::Ok`] on success, with the three bytes written to `out_bytes`,
1363/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
1364///
1365/// # Safety
1366///
1367/// `out_bytes` must point to at least three writable bytes.
1368#[no_mangle]
1369pub unsafe extern "C" fn pamoja_sht3x_word_bytes(value: u16, out_bytes: *mut u8) -> PamojaStatus {
1370    if out_bytes.is_null() {
1371        set_last_error("out_bytes must not be null".to_owned());
1372        return PamojaStatus::InvalidArgument;
1373    }
1374    let bytes = sht3x::word_bytes(value);
1375    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SHT3X_WORD_LEN);
1376    PamojaStatus::Ok
1377}
1378
1379/// Parses and CRC-checks a six-byte SHT3x measurement frame.
1380///
1381/// # Returns
1382///
1383/// [`PamojaStatus::Ok`] on success, with `*out_measurement` filled in, or
1384/// [`PamojaStatus::Codec`] if either word fails its checksum, which means the read
1385/// was corrupted on the bus and should be repeated.
1386///
1387/// # Safety
1388///
1389/// `frame` must point to at least `frame_len` readable bytes, and
1390/// `out_measurement` must point to a writable `PamojaSht3xMeasurement`.
1391#[no_mangle]
1392pub unsafe extern "C" fn pamoja_sht3x_parse_measurement(
1393    frame: *const u8,
1394    frame_len: usize,
1395    out_measurement: *mut PamojaSht3xMeasurement,
1396) -> PamojaStatus {
1397    if out_measurement.is_null() {
1398        set_last_error("out_measurement must not be null".to_owned());
1399        return PamojaStatus::InvalidArgument;
1400    }
1401    let frame = match read_bytes(frame, frame_len) {
1402        Ok(bytes) => bytes,
1403        Err(status) => return status,
1404    };
1405    let Ok(frame) = <[u8; PAMOJA_SHT3X_MEASUREMENT_LEN]>::try_from(&frame[..]) else {
1406        return wrong_length("measurement frame", PAMOJA_SHT3X_MEASUREMENT_LEN);
1407    };
1408    match sht3x::Measurement::parse(&frame) {
1409        Ok(measurement) => {
1410            *out_measurement = measurement.into();
1411            PamojaStatus::Ok
1412        }
1413        Err(error) => failed(error),
1414    }
1415}
1416
1417/// Builds the six bytes an SHT3x sends for a pair of raw words.
1418///
1419/// # Returns
1420///
1421/// [`PamojaStatus::Ok`] on success, with the six bytes written to `out_bytes`, or
1422/// [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
1423///
1424/// # Safety
1425///
1426/// `out_bytes` must point to at least six writable bytes.
1427#[no_mangle]
1428pub unsafe extern "C" fn pamoja_sht3x_measurement_bytes(
1429    temperature_raw: u16,
1430    humidity_raw: u16,
1431    out_bytes: *mut u8,
1432) -> PamojaStatus {
1433    if out_bytes.is_null() {
1434        set_last_error("out_bytes must not be null".to_owned());
1435        return PamojaStatus::InvalidArgument;
1436    }
1437    let measurement = sht3x::Measurement {
1438        temperature_raw,
1439        humidity_raw,
1440    };
1441    let bytes = measurement.to_bytes();
1442    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SHT3X_MEASUREMENT_LEN);
1443    PamojaStatus::Ok
1444}
1445
1446/// Converts a raw SHT3x temperature word to milli-degrees Celsius.
1447///
1448/// # Returns
1449///
1450/// The temperature, exact in integer arithmetic.
1451#[no_mangle]
1452pub extern "C" fn pamoja_sht3x_milli_celsius(raw: u16) -> i32 {
1453    sht3x::milli_celsius(raw)
1454}
1455
1456/// Converts a raw SHT3x temperature word to degrees Celsius.
1457///
1458/// # Returns
1459///
1460/// The temperature.
1461#[no_mangle]
1462pub extern "C" fn pamoja_sht3x_celsius(raw: u16) -> f32 {
1463    sht3x::celsius(raw)
1464}
1465
1466/// Converts a raw SHT3x temperature word to milli-degrees Fahrenheit.
1467///
1468/// # Returns
1469///
1470/// The temperature, exact in integer arithmetic.
1471#[no_mangle]
1472pub extern "C" fn pamoja_sht3x_milli_fahrenheit(raw: u16) -> i32 {
1473    sht3x::milli_fahrenheit(raw)
1474}
1475
1476/// Converts a raw SHT3x temperature word to degrees Fahrenheit.
1477///
1478/// # Returns
1479///
1480/// The temperature.
1481#[no_mangle]
1482pub extern "C" fn pamoja_sht3x_fahrenheit(raw: u16) -> f32 {
1483    sht3x::fahrenheit(raw)
1484}
1485
1486/// Converts a raw SHT3x humidity word to milli-percent.
1487///
1488/// # Returns
1489///
1490/// The relative humidity, exact in integer arithmetic.
1491#[no_mangle]
1492pub extern "C" fn pamoja_sht3x_milli_percent(raw: u16) -> u32 {
1493    sht3x::milli_percent(raw)
1494}
1495
1496/// Converts a raw SHT3x humidity word to a relative humidity percentage.
1497///
1498/// # Returns
1499///
1500/// The relative humidity.
1501#[no_mangle]
1502pub extern "C" fn pamoja_sht3x_relative_humidity(raw: u16) -> f32 {
1503    sht3x::relative_humidity(raw)
1504}
1505
1506/// Builds the SHT3x temperature word that decodes to a temperature.
1507///
1508/// # Returns
1509///
1510/// The raw word, saturating at the ends of the part's range.
1511#[no_mangle]
1512pub extern "C" fn pamoja_sht3x_temperature_raw_from_milli_celsius(milli_celsius: i32) -> u16 {
1513    sht3x::temperature_raw_from_milli_celsius(milli_celsius)
1514}
1515
1516/// Builds the SHT3x temperature word that decodes to a temperature in Celsius.
1517///
1518/// # Returns
1519///
1520/// The raw word, saturating at the ends of the part's range.
1521#[no_mangle]
1522pub extern "C" fn pamoja_sht3x_temperature_raw_from_celsius(celsius: f32) -> u16 {
1523    sht3x::temperature_raw_from_celsius(celsius)
1524}
1525
1526/// Builds the SHT3x temperature word that decodes to a temperature in Fahrenheit.
1527///
1528/// # Returns
1529///
1530/// The raw word, saturating at the ends of the part's range.
1531#[no_mangle]
1532pub extern "C" fn pamoja_sht3x_temperature_raw_from_milli_fahrenheit(milli_fahrenheit: i32) -> u16 {
1533    sht3x::temperature_raw_from_milli_fahrenheit(milli_fahrenheit)
1534}
1535
1536/// Builds the SHT3x humidity word that decodes to a relative humidity.
1537///
1538/// # Returns
1539///
1540/// The raw word, saturating at full scale.
1541#[no_mangle]
1542pub extern "C" fn pamoja_sht3x_humidity_raw_from_milli_percent(milli_percent: u32) -> u16 {
1543    sht3x::humidity_raw_from_milli_percent(milli_percent)
1544}
1545
1546/// Builds the SHT3x humidity word that decodes to a relative humidity percentage.
1547///
1548/// # Returns
1549///
1550/// The raw word, saturating at full scale.
1551#[no_mangle]
1552pub extern "C" fn pamoja_sht3x_humidity_raw_from_relative_humidity(percent: f32) -> u16 {
1553    sht3x::humidity_raw_from_relative_humidity(percent)
1554}
1555
1556/// Parses and CRC-checks a three-byte SHT3x status frame.
1557///
1558/// # Returns
1559///
1560/// [`PamojaStatus::Ok`] on success, with `*out_status` filled in, or
1561/// [`PamojaStatus::Codec`] if the CRC does not match.
1562///
1563/// # Safety
1564///
1565/// `frame` must point to at least `frame_len` readable bytes, and `out_status`
1566/// must point to a writable `PamojaSht3xStatus`.
1567#[no_mangle]
1568pub unsafe extern "C" fn pamoja_sht3x_parse_status(
1569    frame: *const u8,
1570    frame_len: usize,
1571    out_status: *mut PamojaSht3xStatus,
1572) -> PamojaStatus {
1573    if out_status.is_null() {
1574        set_last_error("out_status must not be null".to_owned());
1575        return PamojaStatus::InvalidArgument;
1576    }
1577    let frame = match read_bytes(frame, frame_len) {
1578        Ok(bytes) => bytes,
1579        Err(status) => return status,
1580    };
1581    let Ok(frame) = <[u8; PAMOJA_SHT3X_WORD_LEN]>::try_from(&frame[..]) else {
1582        return wrong_length("status frame", PAMOJA_SHT3X_WORD_LEN);
1583    };
1584    match sht3x::Status::parse(&frame) {
1585        Ok(status) => {
1586            *out_status = status.into();
1587            PamojaStatus::Ok
1588        }
1589        Err(error) => failed(error),
1590    }
1591}
1592
1593/// Splits an SHT3x status word into its flags.
1594///
1595/// # Returns
1596///
1597/// [`PamojaStatus::Ok`], with `*out_status` filled in. Every word decodes, so
1598/// this fails only on a null pointer.
1599///
1600/// # Safety
1601///
1602/// `out_status` must point to a writable `PamojaSht3xStatus`.
1603#[no_mangle]
1604pub unsafe extern "C" fn pamoja_sht3x_status_from_bits(
1605    bits: u16,
1606    out_status: *mut PamojaSht3xStatus,
1607) -> PamojaStatus {
1608    if out_status.is_null() {
1609        set_last_error("out_status must not be null".to_owned());
1610        return PamojaStatus::InvalidArgument;
1611    }
1612    *out_status = sht3x::Status::from_bits(bits).into();
1613    PamojaStatus::Ok
1614}
1615
1616/// Builds the three bytes an SHT3x sends for a status word, CRC last.
1617///
1618/// # Returns
1619///
1620/// [`PamojaStatus::Ok`] on success, with the three bytes written to `out_bytes`,
1621/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
1622///
1623/// # Safety
1624///
1625/// `out_bytes` must point to at least three writable bytes.
1626#[no_mangle]
1627pub unsafe extern "C" fn pamoja_sht3x_status_bytes(bits: u16, out_bytes: *mut u8) -> PamojaStatus {
1628    if out_bytes.is_null() {
1629        set_last_error("out_bytes must not be null".to_owned());
1630        return PamojaStatus::InvalidArgument;
1631    }
1632    let bytes = sht3x::Status::from_bits(bits).to_bytes();
1633    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SHT3X_WORD_LEN);
1634    PamojaStatus::Ok
1635}
1636
1637/// Returns the SHT3x single-shot command for a repeatability and clock mode.
1638///
1639/// # Returns
1640///
1641/// [`PamojaStatus::Ok`] on success, with `*out_command` set, or
1642/// [`PamojaStatus::InvalidArgument`] if `repeatability` is not 0, 1, or 2.
1643///
1644/// # Safety
1645///
1646/// `out_command` must point to a writable `uint16_t`.
1647#[no_mangle]
1648pub unsafe extern "C" fn pamoja_sht3x_single_shot(
1649    repeatability: u8,
1650    clock_stretching: bool,
1651    out_command: *mut u16,
1652) -> PamojaStatus {
1653    if out_command.is_null() {
1654        set_last_error("out_command must not be null".to_owned());
1655        return PamojaStatus::InvalidArgument;
1656    }
1657    let Some(repeatability) = repeatability_from_code(repeatability) else {
1658        return bad_repeatability();
1659    };
1660    *out_command = sht3x::single_shot(repeatability, clock_stretching);
1661    PamojaStatus::Ok
1662}
1663
1664/// Returns the SHT3x periodic-mode command for a repeatability and rate.
1665///
1666/// # Returns
1667///
1668/// [`PamojaStatus::Ok`] on success, with `*out_command` set, or
1669/// [`PamojaStatus::InvalidArgument`] if either code is outside its range.
1670///
1671/// # Safety
1672///
1673/// `out_command` must point to a writable `uint16_t`.
1674#[no_mangle]
1675pub unsafe extern "C" fn pamoja_sht3x_periodic(
1676    repeatability: u8,
1677    rate: u8,
1678    out_command: *mut u16,
1679) -> PamojaStatus {
1680    if out_command.is_null() {
1681        set_last_error("out_command must not be null".to_owned());
1682        return PamojaStatus::InvalidArgument;
1683    }
1684    let Some(repeatability) = repeatability_from_code(repeatability) else {
1685        return bad_repeatability();
1686    };
1687    let Some(rate) = rate_from_code(rate) else {
1688        return bad_rate();
1689    };
1690    *out_command = sht3x::periodic(repeatability, rate);
1691    PamojaStatus::Ok
1692}
1693
1694/// Returns how long an SHT3x measurement may take at a repeatability.
1695///
1696/// # Returns
1697///
1698/// [`PamojaStatus::Ok`] on success, with `*out_micros` set to the datasheet's
1699/// worst case, or [`PamojaStatus::InvalidArgument`] if the code is out of range.
1700///
1701/// # Safety
1702///
1703/// `out_micros` must point to a writable `uint32_t`.
1704#[no_mangle]
1705pub unsafe extern "C" fn pamoja_sht3x_max_measurement_micros(
1706    repeatability: u8,
1707    out_micros: *mut u32,
1708) -> PamojaStatus {
1709    if out_micros.is_null() {
1710        set_last_error("out_micros must not be null".to_owned());
1711        return PamojaStatus::InvalidArgument;
1712    }
1713    match repeatability_from_code(repeatability) {
1714        Some(repeatability) => {
1715            *out_micros = repeatability.max_measurement_micros();
1716            PamojaStatus::Ok
1717        }
1718        None => bad_repeatability(),
1719    }
1720}
1721
1722/// Returns how long an SHT3x measurement typically takes at a repeatability.
1723///
1724/// # Returns
1725///
1726/// [`PamojaStatus::Ok`] on success, with `*out_micros` set, or
1727/// [`PamojaStatus::InvalidArgument`] if the code is out of range.
1728///
1729/// # Safety
1730///
1731/// `out_micros` must point to a writable `uint32_t`.
1732#[no_mangle]
1733pub unsafe extern "C" fn pamoja_sht3x_typical_measurement_micros(
1734    repeatability: u8,
1735    out_micros: *mut u32,
1736) -> PamojaStatus {
1737    if out_micros.is_null() {
1738        set_last_error("out_micros must not be null".to_owned());
1739        return PamojaStatus::InvalidArgument;
1740    }
1741    match repeatability_from_code(repeatability) {
1742        Some(repeatability) => {
1743            *out_micros = repeatability.typical_measurement_micros();
1744            PamojaStatus::Ok
1745        }
1746        None => bad_repeatability(),
1747    }
1748}
1749
1750/// Returns the gap between SHT3x periodic measurements at a rate.
1751///
1752/// # Returns
1753///
1754/// [`PamojaStatus::Ok`] on success, with `*out_micros` set, or
1755/// [`PamojaStatus::InvalidArgument`] if the code is out of range.
1756///
1757/// # Safety
1758///
1759/// `out_micros` must point to a writable `uint32_t`.
1760#[no_mangle]
1761pub unsafe extern "C" fn pamoja_sht3x_interval_micros(
1762    rate: u8,
1763    out_micros: *mut u32,
1764) -> PamojaStatus {
1765    if out_micros.is_null() {
1766        set_last_error("out_micros must not be null".to_owned());
1767        return PamojaStatus::InvalidArgument;
1768    }
1769    match rate_from_code(rate) {
1770        Some(rate) => {
1771            *out_micros = rate.interval_micros();
1772            PamojaStatus::Ok
1773        }
1774        None => bad_rate(),
1775    }
1776}
1777
1778/// The number of bytes in an SCD4x measurement frame, three words with their CRCs.
1779pub const PAMOJA_SCD4X_MEASUREMENT_LEN: usize = 9;
1780
1781/// The number of bytes in an SCD4x word frame: the word then its CRC.
1782pub const PAMOJA_SCD4X_WORD_LEN: usize = 3;
1783
1784/// The number of bytes in an SCD4x command frame.
1785pub const PAMOJA_SCD4X_COMMAND_LEN: usize = 2;
1786
1787/// The number of bytes in an SCD4x write frame: a command, a word, and its CRC.
1788pub const PAMOJA_SCD4X_WRITE_LEN: usize = 5;
1789
1790/// The single address an SCD4x answers on.
1791pub const PAMOJA_SCD4X_I2C_ADDRESS: u8 = 0x62;
1792
1793/// The highest carbon dioxide concentration an SCD4x reports, in parts per million.
1794pub const PAMOJA_SCD4X_CO2_MAX_PPM: u16 = 40_000;
1795
1796/// The temperature offset an SCD4x holds after a factory reset.
1797pub const PAMOJA_SCD4X_DEFAULT_TEMPERATURE_OFFSET_MILLI_CELSIUS: u32 = 4_000;
1798
1799/// How often an SCD4x in periodic mode produces a result, in milliseconds.
1800pub const PAMOJA_SCD4X_PERIODIC_MEASUREMENT_INTERVAL_MS: u32 = 5_000;
1801
1802/// How often it produces a result in low-power periodic mode, in milliseconds.
1803pub const PAMOJA_SCD4X_LOW_POWER_PERIODIC_MEASUREMENT_INTERVAL_MS: u32 = 30_000;
1804
1805/// How long an SCD4x takes to become responsive after power-up, in milliseconds.
1806pub const PAMOJA_SCD4X_POWER_UP_TIME_MS: u32 = 1_000;
1807
1808/// The word a forced recalibration returns when it did not take.
1809pub const PAMOJA_SCD4X_FORCED_RECALIBRATION_FAILED: u16 = 0xffff;
1810
1811/// Starts periodic measurements at one result every five seconds.
1812pub const PAMOJA_SCD4X_COMMAND_START_PERIODIC_MEASUREMENT: u16 = 0x21b1;
1813
1814/// Reads the latest carbon dioxide, temperature, and humidity words.
1815pub const PAMOJA_SCD4X_COMMAND_READ_MEASUREMENT: u16 = 0xec05;
1816
1817/// Stops periodic measurements so other commands are accepted again.
1818pub const PAMOJA_SCD4X_COMMAND_STOP_PERIODIC_MEASUREMENT: u16 = 0x3f86;
1819
1820/// Writes the temperature offset the part subtracts from its own reading.
1821pub const PAMOJA_SCD4X_COMMAND_SET_TEMPERATURE_OFFSET: u16 = 0x241d;
1822
1823/// Reads the temperature offset back.
1824pub const PAMOJA_SCD4X_COMMAND_GET_TEMPERATURE_OFFSET: u16 = 0x2318;
1825
1826/// Writes the altitude the part compensates its pressure for, in metres.
1827pub const PAMOJA_SCD4X_COMMAND_SET_SENSOR_ALTITUDE: u16 = 0x2427;
1828
1829/// Reads the configured altitude back.
1830pub const PAMOJA_SCD4X_COMMAND_GET_SENSOR_ALTITUDE: u16 = 0x2322;
1831
1832/// Writes the ambient pressure, which may be sent during a measurement.
1833pub const PAMOJA_SCD4X_COMMAND_SET_AMBIENT_PRESSURE: u16 = 0xe000;
1834
1835/// Recalibrates against a known concentration and returns the correction applied.
1836pub const PAMOJA_SCD4X_COMMAND_PERFORM_FORCED_RECALIBRATION: u16 = 0x362f;
1837
1838/// Turns automatic self-calibration on or off.
1839pub const PAMOJA_SCD4X_COMMAND_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED: u16 = 0x2416;
1840
1841/// Reads whether automatic self-calibration is on.
1842pub const PAMOJA_SCD4X_COMMAND_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED: u16 = 0x2313;
1843
1844/// Starts low-power periodic measurements, one result every thirty seconds.
1845pub const PAMOJA_SCD4X_COMMAND_START_LOW_POWER_PERIODIC_MEASUREMENT: u16 = 0x21ac;
1846
1847/// Reads whether a fresh result is waiting.
1848pub const PAMOJA_SCD4X_COMMAND_GET_DATA_READY_STATUS: u16 = 0xe4b8;
1849
1850/// Stores the current settings in non-volatile memory.
1851pub const PAMOJA_SCD4X_COMMAND_PERSIST_SETTINGS: u16 = 0x3615;
1852
1853/// Reads the 48-bit serial number, three words with their CRCs.
1854pub const PAMOJA_SCD4X_COMMAND_GET_SERIAL_NUMBER: u16 = 0x3682;
1855
1856/// Runs the on-board self test, which takes ten seconds.
1857pub const PAMOJA_SCD4X_COMMAND_PERFORM_SELF_TEST: u16 = 0x3639;
1858
1859/// Restores the factory settings, discarding the stored calibration.
1860pub const PAMOJA_SCD4X_COMMAND_PERFORM_FACTORY_RESET: u16 = 0x3632;
1861
1862/// Reloads the stored settings without a power cycle.
1863pub const PAMOJA_SCD4X_COMMAND_REINIT: u16 = 0x3646;
1864
1865/// Takes one measurement on demand, an SCD41 command.
1866pub const PAMOJA_SCD4X_COMMAND_MEASURE_SINGLE_SHOT: u16 = 0x219d;
1867
1868/// Takes one humidity and temperature measurement without the photoacoustic cell.
1869pub const PAMOJA_SCD4X_COMMAND_MEASURE_SINGLE_SHOT_RHT_ONLY: u16 = 0x2196;
1870
1871/// Puts an SCD41 into its lowest-power state.
1872pub const PAMOJA_SCD4X_COMMAND_POWER_DOWN: u16 = 0x36e0;
1873
1874/// Brings an SCD41 back out of power-down.
1875pub const PAMOJA_SCD4X_COMMAND_WAKE_UP: u16 = 0x36f6;
1876
1877// The header generator does not read the crates this one depends on, so these
1878// carry their value rather than the name of the constant that defines it.
1879const _: () = assert!(PAMOJA_SCD4X_I2C_ADDRESS == scd4x::I2C_ADDRESS);
1880const _: () = assert!(PAMOJA_SCD4X_CO2_MAX_PPM == scd4x::CO2_MAX_PPM);
1881const _: () = assert!(
1882    PAMOJA_SCD4X_DEFAULT_TEMPERATURE_OFFSET_MILLI_CELSIUS
1883        == scd4x::DEFAULT_TEMPERATURE_OFFSET_MILLI_CELSIUS
1884);
1885const _: () = assert!(
1886    PAMOJA_SCD4X_PERIODIC_MEASUREMENT_INTERVAL_MS == scd4x::PERIODIC_MEASUREMENT_INTERVAL_MS
1887);
1888const _: () = assert!(
1889    PAMOJA_SCD4X_LOW_POWER_PERIODIC_MEASUREMENT_INTERVAL_MS
1890        == scd4x::LOW_POWER_PERIODIC_MEASUREMENT_INTERVAL_MS
1891);
1892const _: () = assert!(PAMOJA_SCD4X_POWER_UP_TIME_MS == scd4x::POWER_UP_TIME_MS);
1893const _: () =
1894    assert!(PAMOJA_SCD4X_FORCED_RECALIBRATION_FAILED == scd4x::FORCED_RECALIBRATION_FAILED);
1895const _: () = assert!(
1896    PAMOJA_SCD4X_COMMAND_START_PERIODIC_MEASUREMENT == scd4x::command::START_PERIODIC_MEASUREMENT
1897);
1898const _: () = assert!(PAMOJA_SCD4X_COMMAND_READ_MEASUREMENT == scd4x::command::READ_MEASUREMENT);
1899const _: () = assert!(
1900    PAMOJA_SCD4X_COMMAND_STOP_PERIODIC_MEASUREMENT == scd4x::command::STOP_PERIODIC_MEASUREMENT
1901);
1902const _: () =
1903    assert!(PAMOJA_SCD4X_COMMAND_SET_TEMPERATURE_OFFSET == scd4x::command::SET_TEMPERATURE_OFFSET);
1904const _: () =
1905    assert!(PAMOJA_SCD4X_COMMAND_GET_TEMPERATURE_OFFSET == scd4x::command::GET_TEMPERATURE_OFFSET);
1906const _: () =
1907    assert!(PAMOJA_SCD4X_COMMAND_SET_SENSOR_ALTITUDE == scd4x::command::SET_SENSOR_ALTITUDE);
1908const _: () =
1909    assert!(PAMOJA_SCD4X_COMMAND_GET_SENSOR_ALTITUDE == scd4x::command::GET_SENSOR_ALTITUDE);
1910const _: () =
1911    assert!(PAMOJA_SCD4X_COMMAND_SET_AMBIENT_PRESSURE == scd4x::command::SET_AMBIENT_PRESSURE);
1912const _: () = assert!(
1913    PAMOJA_SCD4X_COMMAND_PERFORM_FORCED_RECALIBRATION
1914        == scd4x::command::PERFORM_FORCED_RECALIBRATION
1915);
1916const _: () = assert!(
1917    PAMOJA_SCD4X_COMMAND_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED
1918        == scd4x::command::SET_AUTOMATIC_SELF_CALIBRATION_ENABLED
1919);
1920const _: () = assert!(
1921    PAMOJA_SCD4X_COMMAND_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED
1922        == scd4x::command::GET_AUTOMATIC_SELF_CALIBRATION_ENABLED
1923);
1924const _: () = assert!(
1925    PAMOJA_SCD4X_COMMAND_START_LOW_POWER_PERIODIC_MEASUREMENT
1926        == scd4x::command::START_LOW_POWER_PERIODIC_MEASUREMENT
1927);
1928const _: () =
1929    assert!(PAMOJA_SCD4X_COMMAND_GET_DATA_READY_STATUS == scd4x::command::GET_DATA_READY_STATUS);
1930const _: () = assert!(PAMOJA_SCD4X_COMMAND_PERSIST_SETTINGS == scd4x::command::PERSIST_SETTINGS);
1931const _: () = assert!(PAMOJA_SCD4X_COMMAND_GET_SERIAL_NUMBER == scd4x::command::GET_SERIAL_NUMBER);
1932const _: () = assert!(PAMOJA_SCD4X_COMMAND_PERFORM_SELF_TEST == scd4x::command::PERFORM_SELF_TEST);
1933const _: () =
1934    assert!(PAMOJA_SCD4X_COMMAND_PERFORM_FACTORY_RESET == scd4x::command::PERFORM_FACTORY_RESET);
1935const _: () = assert!(PAMOJA_SCD4X_COMMAND_REINIT == scd4x::command::REINIT);
1936const _: () =
1937    assert!(PAMOJA_SCD4X_COMMAND_MEASURE_SINGLE_SHOT == scd4x::command::MEASURE_SINGLE_SHOT);
1938const _: () = assert!(
1939    PAMOJA_SCD4X_COMMAND_MEASURE_SINGLE_SHOT_RHT_ONLY
1940        == scd4x::command::MEASURE_SINGLE_SHOT_RHT_ONLY
1941);
1942const _: () = assert!(PAMOJA_SCD4X_COMMAND_POWER_DOWN == scd4x::command::POWER_DOWN);
1943const _: () = assert!(PAMOJA_SCD4X_COMMAND_WAKE_UP == scd4x::command::WAKE_UP);
1944
1945/// A decoded SCD4x measurement frame.
1946#[repr(C)]
1947#[derive(Clone, Copy, Debug, PartialEq)]
1948pub struct PamojaScd4xMeasurement {
1949    /// The carbon dioxide concentration in parts per million.
1950    pub co2_ppm: u16,
1951    /// The raw temperature word.
1952    pub temperature_raw: u16,
1953    /// The raw humidity word.
1954    pub humidity_raw: u16,
1955    /// The temperature in milli-degrees Celsius, exact in integer arithmetic.
1956    pub milli_celsius: i32,
1957    /// The temperature in degrees Celsius.
1958    pub celsius: f32,
1959    /// The relative humidity in milli-percent.
1960    pub humidity_milli_percent: u32,
1961    /// The relative humidity as a percentage.
1962    pub relative_humidity_percent: f32,
1963}
1964
1965/// Computes the CRC-8 an SCD4x appends to every data word.
1966///
1967/// # Returns
1968///
1969/// The checksum over `data`, or 0 if the pointer is null with a non-zero length.
1970///
1971/// # Safety
1972///
1973/// `data` must point to at least `data_len` readable bytes, or be null when
1974/// `data_len` is 0.
1975#[no_mangle]
1976pub unsafe extern "C" fn pamoja_scd4x_crc(data: *const u8, data_len: usize) -> u8 {
1977    match read_bytes(data, data_len) {
1978        Ok(data) => scd4x::crc(&data),
1979        Err(_) => 0,
1980    }
1981}
1982
1983/// Reads a CRC-checked three-byte SCD4x word frame.
1984///
1985/// # Returns
1986///
1987/// [`PamojaStatus::Ok`] on success, with `*out_word` set, or
1988/// [`PamojaStatus::Codec`] if the CRC does not match.
1989///
1990/// # Safety
1991///
1992/// `frame` must point to at least `frame_len` readable bytes, and `out_word` must
1993/// point to a writable `uint16_t`.
1994#[no_mangle]
1995pub unsafe extern "C" fn pamoja_scd4x_word(
1996    frame: *const u8,
1997    frame_len: usize,
1998    out_word: *mut u16,
1999) -> PamojaStatus {
2000    if out_word.is_null() {
2001        set_last_error("out_word must not be null".to_owned());
2002        return PamojaStatus::InvalidArgument;
2003    }
2004    let frame = match read_bytes(frame, frame_len) {
2005        Ok(bytes) => bytes,
2006        Err(status) => return status,
2007    };
2008    let Ok(frame) = <[u8; PAMOJA_SCD4X_WORD_LEN]>::try_from(&frame[..]) else {
2009        return wrong_length("word frame", PAMOJA_SCD4X_WORD_LEN);
2010    };
2011    match scd4x::word(&frame) {
2012        Ok(word) => {
2013            *out_word = word;
2014            PamojaStatus::Ok
2015        }
2016        Err(error) => failed(error),
2017    }
2018}
2019
2020/// Builds the three bytes an SCD4x sends for a word: the word then its CRC.
2021///
2022/// # Returns
2023///
2024/// [`PamojaStatus::Ok`] on success, with the three bytes written to `out_bytes`,
2025/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
2026///
2027/// # Safety
2028///
2029/// `out_bytes` must point to at least three writable bytes.
2030#[no_mangle]
2031pub unsafe extern "C" fn pamoja_scd4x_word_frame(value: u16, out_bytes: *mut u8) -> PamojaStatus {
2032    if out_bytes.is_null() {
2033        set_last_error("out_bytes must not be null".to_owned());
2034        return PamojaStatus::InvalidArgument;
2035    }
2036    let bytes = scd4x::word_frame(value);
2037    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SCD4X_WORD_LEN);
2038    PamojaStatus::Ok
2039}
2040
2041/// Builds the two bytes that address an SCD4x command, most significant first.
2042///
2043/// # Returns
2044///
2045/// [`PamojaStatus::Ok`] on success, with the two bytes written to `out_bytes`, or
2046/// [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
2047///
2048/// # Safety
2049///
2050/// `out_bytes` must point to at least two writable bytes.
2051#[no_mangle]
2052pub unsafe extern "C" fn pamoja_scd4x_command_frame(
2053    command: u16,
2054    out_bytes: *mut u8,
2055) -> PamojaStatus {
2056    if out_bytes.is_null() {
2057        set_last_error("out_bytes must not be null".to_owned());
2058        return PamojaStatus::InvalidArgument;
2059    }
2060    let bytes = scd4x::command_frame(command);
2061    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SCD4X_COMMAND_LEN);
2062    PamojaStatus::Ok
2063}
2064
2065/// Builds the five bytes that write a word to an SCD4x: command, word, CRC.
2066///
2067/// # Returns
2068///
2069/// [`PamojaStatus::Ok`] on success, with the five bytes written to `out_bytes`,
2070/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
2071///
2072/// # Safety
2073///
2074/// `out_bytes` must point to at least five writable bytes.
2075#[no_mangle]
2076pub unsafe extern "C" fn pamoja_scd4x_write_frame(
2077    command: u16,
2078    value: u16,
2079    out_bytes: *mut u8,
2080) -> PamojaStatus {
2081    if out_bytes.is_null() {
2082        set_last_error("out_bytes must not be null".to_owned());
2083        return PamojaStatus::InvalidArgument;
2084    }
2085    let bytes = scd4x::write_frame(command, value);
2086    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SCD4X_WRITE_LEN);
2087    PamojaStatus::Ok
2088}
2089
2090/// Returns how long an SCD4x command may take before its result can be read.
2091///
2092/// # Returns
2093///
2094/// `true` when the command has a documented execution time, with `*out_millis`
2095/// set to it; `false` when it completes as soon as it is acknowledged.
2096///
2097/// # Safety
2098///
2099/// `out_millis` must point to a writable `uint16_t`.
2100#[no_mangle]
2101pub unsafe extern "C" fn pamoja_scd4x_max_duration_ms(command: u16, out_millis: *mut u16) -> bool {
2102    match scd4x::max_duration_ms(command) {
2103        Some(millis) if !out_millis.is_null() => {
2104            *out_millis = millis;
2105            true
2106        }
2107        _ => false,
2108    }
2109}
2110
2111/// Reports whether an SCD4x accepts a command while it is measuring.
2112///
2113/// # Returns
2114///
2115/// `true` when the command may be sent without stopping periodic measurements.
2116#[no_mangle]
2117pub extern "C" fn pamoja_scd4x_allowed_during_measurement(command: u16) -> bool {
2118    scd4x::allowed_during_measurement(command)
2119}
2120
2121/// Parses and CRC-checks a nine-byte SCD4x measurement frame.
2122///
2123/// # Returns
2124///
2125/// [`PamojaStatus::Ok`] on success, with `*out_measurement` filled in, or
2126/// [`PamojaStatus::Codec`] if any word fails its checksum, which means the read
2127/// was corrupted on the bus and should be repeated.
2128///
2129/// # Safety
2130///
2131/// `frame` must point to at least `frame_len` readable bytes, and
2132/// `out_measurement` must point to a writable `PamojaScd4xMeasurement`.
2133#[no_mangle]
2134pub unsafe extern "C" fn pamoja_scd4x_parse_measurement(
2135    frame: *const u8,
2136    frame_len: usize,
2137    out_measurement: *mut PamojaScd4xMeasurement,
2138) -> PamojaStatus {
2139    if out_measurement.is_null() {
2140        set_last_error("out_measurement must not be null".to_owned());
2141        return PamojaStatus::InvalidArgument;
2142    }
2143    let frame = match read_bytes(frame, frame_len) {
2144        Ok(bytes) => bytes,
2145        Err(status) => return status,
2146    };
2147    let Ok(frame) = <[u8; PAMOJA_SCD4X_MEASUREMENT_LEN]>::try_from(&frame[..]) else {
2148        return wrong_length("measurement frame", PAMOJA_SCD4X_MEASUREMENT_LEN);
2149    };
2150    match scd4x::Measurement::parse(&frame) {
2151        Ok(measurement) => {
2152            *out_measurement = measurement.into();
2153            PamojaStatus::Ok
2154        }
2155        Err(error) => failed(error),
2156    }
2157}
2158
2159/// Builds the SCD4x measurement a sensor reporting these physical values would send.
2160///
2161/// # Returns
2162///
2163/// [`PamojaStatus::Ok`], with `*out_measurement` filled in, or
2164/// [`PamojaStatus::InvalidArgument`] if the pointer is null.
2165///
2166/// # Safety
2167///
2168/// `out_measurement` must point to a writable `PamojaScd4xMeasurement`.
2169#[no_mangle]
2170pub unsafe extern "C" fn pamoja_scd4x_measurement_from_physical(
2171    co2_ppm: u16,
2172    milli_celsius: i32,
2173    humidity_milli_percent: u32,
2174    out_measurement: *mut PamojaScd4xMeasurement,
2175) -> PamojaStatus {
2176    if out_measurement.is_null() {
2177        set_last_error("out_measurement must not be null".to_owned());
2178        return PamojaStatus::InvalidArgument;
2179    }
2180    let measurement =
2181        scd4x::Measurement::from_physical(co2_ppm, milli_celsius, humidity_milli_percent);
2182    *out_measurement = measurement.into();
2183    PamojaStatus::Ok
2184}
2185
2186/// Builds the nine bytes an SCD4x sends for a set of raw words, each CRC included.
2187///
2188/// # Returns
2189///
2190/// [`PamojaStatus::Ok`] on success, with the nine bytes written to `out_bytes`,
2191/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
2192///
2193/// # Safety
2194///
2195/// `out_bytes` must point to at least nine writable bytes.
2196#[no_mangle]
2197pub unsafe extern "C" fn pamoja_scd4x_measurement_bytes(
2198    co2_ppm: u16,
2199    temperature_raw: u16,
2200    humidity_raw: u16,
2201    out_bytes: *mut u8,
2202) -> PamojaStatus {
2203    if out_bytes.is_null() {
2204        set_last_error("out_bytes must not be null".to_owned());
2205        return PamojaStatus::InvalidArgument;
2206    }
2207    let measurement = scd4x::Measurement {
2208        co2_ppm,
2209        temperature_raw,
2210        humidity_raw,
2211    };
2212    let bytes = measurement.to_bytes();
2213    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SCD4X_MEASUREMENT_LEN);
2214    PamojaStatus::Ok
2215}
2216
2217/// Converts a raw SCD4x temperature word to milli-degrees Celsius.
2218///
2219/// # Returns
2220///
2221/// The temperature, exact in integer arithmetic.
2222#[no_mangle]
2223pub extern "C" fn pamoja_scd4x_milli_celsius(raw: u16) -> i32 {
2224    scd4x::milli_celsius(raw)
2225}
2226
2227/// Converts a raw SCD4x temperature word to degrees Celsius.
2228///
2229/// # Returns
2230///
2231/// The temperature.
2232#[no_mangle]
2233pub extern "C" fn pamoja_scd4x_celsius(raw: u16) -> f32 {
2234    scd4x::celsius(raw)
2235}
2236
2237/// Builds the SCD4x temperature word that decodes to a temperature.
2238///
2239/// # Returns
2240///
2241/// The raw word, saturating at the ends of the part's range.
2242#[no_mangle]
2243pub extern "C" fn pamoja_scd4x_temperature_raw(milli_celsius: i32) -> u16 {
2244    scd4x::temperature_raw(milli_celsius)
2245}
2246
2247/// Converts a raw SCD4x humidity word to milli-percent.
2248///
2249/// # Returns
2250///
2251/// The relative humidity, exact in integer arithmetic.
2252#[no_mangle]
2253pub extern "C" fn pamoja_scd4x_humidity_milli_percent(raw: u16) -> u32 {
2254    scd4x::humidity_milli_percent(raw)
2255}
2256
2257/// Converts a raw SCD4x humidity word to a relative humidity percentage.
2258///
2259/// # Returns
2260///
2261/// The relative humidity.
2262#[no_mangle]
2263pub extern "C" fn pamoja_scd4x_relative_humidity_percent(raw: u16) -> f32 {
2264    scd4x::relative_humidity_percent(raw)
2265}
2266
2267/// Builds the SCD4x humidity word that decodes to a relative humidity.
2268///
2269/// # Returns
2270///
2271/// The raw word, saturating at full scale.
2272#[no_mangle]
2273pub extern "C" fn pamoja_scd4x_humidity_raw(milli_percent: u32) -> u16 {
2274    scd4x::humidity_raw(milli_percent)
2275}
2276
2277/// Reports whether an SCD4x data-ready word says a fresh result is waiting.
2278///
2279/// # Returns
2280///
2281/// `true` when any of the low eleven bits is set.
2282#[no_mangle]
2283pub extern "C" fn pamoja_scd4x_data_ready(word: u16) -> bool {
2284    scd4x::data_ready(word)
2285}
2286
2287/// Builds the SCD4x temperature-offset word for an offset.
2288///
2289/// # Returns
2290///
2291/// The word to write, which scales by 2^16 rather than by the 2^16 - 1 the
2292/// measurement words use.
2293#[no_mangle]
2294pub extern "C" fn pamoja_scd4x_temperature_offset_word(milli_celsius: u32) -> u16 {
2295    scd4x::temperature_offset_word(milli_celsius)
2296}
2297
2298/// Reads an SCD4x temperature-offset word back as an offset.
2299///
2300/// # Returns
2301///
2302/// The offset in milli-degrees Celsius.
2303#[no_mangle]
2304pub extern "C" fn pamoja_scd4x_temperature_offset_milli_celsius(word: u16) -> u32 {
2305    scd4x::temperature_offset_milli_celsius(word)
2306}
2307
2308/// Builds the SCD4x ambient-pressure word for a pressure.
2309///
2310/// # Returns
2311///
2312/// The word to write, at 100 pascals per count.
2313#[no_mangle]
2314pub extern "C" fn pamoja_scd4x_ambient_pressure_word(pascals: u32) -> u16 {
2315    scd4x::ambient_pressure_word(pascals)
2316}
2317
2318/// Reads an SCD4x ambient-pressure word back as a pressure.
2319///
2320/// # Returns
2321///
2322/// The pressure in pascals.
2323#[no_mangle]
2324pub extern "C" fn pamoja_scd4x_ambient_pressure_pascals(word: u16) -> u32 {
2325    scd4x::ambient_pressure_pascals(word)
2326}
2327
2328/// Reads the correction a forced recalibration applied.
2329///
2330/// # Returns
2331///
2332/// `true` when the recalibration took, with `*out_ppm` set to the correction in
2333/// parts per million; `false` when the part reported that it failed.
2334///
2335/// # Safety
2336///
2337/// `out_ppm` must point to a writable `int32_t`.
2338#[no_mangle]
2339pub unsafe extern "C" fn pamoja_scd4x_forced_recalibration_correction_ppm(
2340    word: u16,
2341    out_ppm: *mut i32,
2342) -> bool {
2343    match scd4x::forced_recalibration_correction_ppm(word) {
2344        Some(ppm) if !out_ppm.is_null() => {
2345            *out_ppm = ppm;
2346            true
2347        }
2348        _ => false,
2349    }
2350}
2351
2352/// Builds the word an SCD4x returns for a forced-recalibration outcome.
2353///
2354/// # Returns
2355///
2356/// The word, which is the failure sentinel when `succeeded` is `false`.
2357#[no_mangle]
2358pub extern "C" fn pamoja_scd4x_forced_recalibration_word(
2359    succeeded: bool,
2360    correction_ppm: i32,
2361) -> u16 {
2362    scd4x::forced_recalibration_word(succeeded.then_some(correction_ppm))
2363}
2364
2365/// Reports whether an SCD4x word says automatic self-calibration is on.
2366///
2367/// # Returns
2368///
2369/// `true` when the part recalibrates itself against clean air.
2370#[no_mangle]
2371pub extern "C" fn pamoja_scd4x_automatic_self_calibration_enabled(word: u16) -> bool {
2372    scd4x::automatic_self_calibration_enabled(word)
2373}
2374
2375/// Builds the SCD4x word that turns automatic self-calibration on or off.
2376///
2377/// # Returns
2378///
2379/// The word to write.
2380#[no_mangle]
2381pub extern "C" fn pamoja_scd4x_automatic_self_calibration_word(enabled: bool) -> u16 {
2382    scd4x::automatic_self_calibration_word(enabled)
2383}
2384
2385/// Reports whether an SCD4x self-test word says the part is healthy.
2386///
2387/// # Returns
2388///
2389/// `true` when the self test found no malfunction.
2390#[no_mangle]
2391pub extern "C" fn pamoja_scd4x_self_test_passed(word: u16) -> bool {
2392    scd4x::self_test_passed(word)
2393}
2394
2395/// Reads the 48-bit serial number out of a nine-byte SCD4x frame.
2396///
2397/// # Returns
2398///
2399/// [`PamojaStatus::Ok`] on success, with `*out_serial` set, or
2400/// [`PamojaStatus::Codec`] if any word fails its checksum.
2401///
2402/// # Safety
2403///
2404/// `frame` must point to at least `frame_len` readable bytes, and `out_serial`
2405/// must point to a writable `uint64_t`.
2406#[no_mangle]
2407pub unsafe extern "C" fn pamoja_scd4x_serial_number(
2408    frame: *const u8,
2409    frame_len: usize,
2410    out_serial: *mut u64,
2411) -> PamojaStatus {
2412    if out_serial.is_null() {
2413        set_last_error("out_serial must not be null".to_owned());
2414        return PamojaStatus::InvalidArgument;
2415    }
2416    let frame = match read_bytes(frame, frame_len) {
2417        Ok(bytes) => bytes,
2418        Err(status) => return status,
2419    };
2420    let Ok(frame) = <[u8; PAMOJA_SCD4X_MEASUREMENT_LEN]>::try_from(&frame[..]) else {
2421        return wrong_length("serial number frame", PAMOJA_SCD4X_MEASUREMENT_LEN);
2422    };
2423    match scd4x::serial_number(&frame) {
2424        Ok(serial) => {
2425            *out_serial = serial;
2426            PamojaStatus::Ok
2427        }
2428        Err(error) => failed(error),
2429    }
2430}
2431
2432/// Builds the nine bytes an SCD4x sends for a serial number, each CRC included.
2433///
2434/// # Returns
2435///
2436/// [`PamojaStatus::Ok`] on success, with the nine bytes written to `out_bytes`,
2437/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
2438///
2439/// # Safety
2440///
2441/// `out_bytes` must point to at least nine writable bytes.
2442#[no_mangle]
2443pub unsafe extern "C" fn pamoja_scd4x_serial_number_frame(
2444    serial: u64,
2445    out_bytes: *mut u8,
2446) -> PamojaStatus {
2447    if out_bytes.is_null() {
2448        set_last_error("out_bytes must not be null".to_owned());
2449        return PamojaStatus::InvalidArgument;
2450    }
2451    let bytes = scd4x::serial_number_frame(serial);
2452    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_SCD4X_MEASUREMENT_LEN);
2453    PamojaStatus::Ok
2454}
2455
2456/// The number of bytes in a TMP117 register read.
2457pub const PAMOJA_TMP117_REGISTER_LEN: usize = 2;
2458
2459/// The value a TMP117's device-ID register reads, which confirms the part.
2460pub const PAMOJA_TMP117_DEVICE_ID: u16 = 0x0117;
2461
2462/// The value its configuration register reads after a reset.
2463pub const PAMOJA_TMP117_CONFIG_RESET: u16 = 0x0220;
2464
2465/// The value its high-limit register reads after a reset.
2466pub const PAMOJA_TMP117_HIGH_LIMIT_RESET: u16 = 0x6000;
2467
2468/// The value its low-limit register reads after a reset.
2469pub const PAMOJA_TMP117_LOW_LIMIT_RESET: u16 = 0x8000;
2470
2471/// The value its result register reads before the first conversion completes.
2472pub const PAMOJA_TMP117_TEMP_RESULT_RESET: u16 = 0x8000;
2473
2474/// The byte a general-call reset sends to address 0x00.
2475pub const PAMOJA_TMP117_GENERAL_CALL_RESET: u8 = 0x06;
2476
2477/// The word written to the EEPROM unlock register to allow a write.
2478pub const PAMOJA_TMP117_EEPROM_UNLOCK: u16 = 0x8000;
2479
2480/// The address a TMP117 answers on with ADD0 tied to GND.
2481pub const PAMOJA_TMP117_ADDRESS_ADD0_GND: u8 = 0x48;
2482
2483/// The address it answers on with ADD0 tied to V+.
2484pub const PAMOJA_TMP117_ADDRESS_ADD0_VPLUS: u8 = 0x49;
2485
2486/// The address it answers on with ADD0 tied to SDA.
2487pub const PAMOJA_TMP117_ADDRESS_ADD0_SDA: u8 = 0x4A;
2488
2489/// The address it answers on with ADD0 tied to SCL.
2490pub const PAMOJA_TMP117_ADDRESS_ADD0_SCL: u8 = 0x4B;
2491
2492/// The TMP117 temperature result register.
2493pub const PAMOJA_TMP117_REGISTER_TEMP_RESULT: u8 = 0x00;
2494
2495/// The TMP117 configuration register.
2496pub const PAMOJA_TMP117_REGISTER_CONFIGURATION: u8 = 0x01;
2497
2498/// The TMP117 high-limit register.
2499pub const PAMOJA_TMP117_REGISTER_THIGH_LIMIT: u8 = 0x02;
2500
2501/// The TMP117 low-limit register.
2502pub const PAMOJA_TMP117_REGISTER_TLOW_LIMIT: u8 = 0x03;
2503
2504/// The TMP117 EEPROM unlock register.
2505pub const PAMOJA_TMP117_REGISTER_EEPROM_UL: u8 = 0x04;
2506
2507/// The first TMP117 general-purpose EEPROM register.
2508pub const PAMOJA_TMP117_REGISTER_EEPROM1: u8 = 0x05;
2509
2510/// The second TMP117 general-purpose EEPROM register.
2511pub const PAMOJA_TMP117_REGISTER_EEPROM2: u8 = 0x06;
2512
2513/// The TMP117 temperature offset register.
2514pub const PAMOJA_TMP117_REGISTER_TEMP_OFFSET: u8 = 0x07;
2515
2516/// The third TMP117 general-purpose EEPROM register.
2517pub const PAMOJA_TMP117_REGISTER_EEPROM3: u8 = 0x08;
2518
2519/// The TMP117 device-ID register.
2520pub const PAMOJA_TMP117_REGISTER_DEVICE_ID: u8 = 0x0F;
2521
2522// The header generator does not read the crates this one depends on, so these
2523// carry their value rather than the name of the constant that defines it.
2524const _: () = assert!(PAMOJA_TMP117_DEVICE_ID == tmp117::DEVICE_ID);
2525const _: () = assert!(PAMOJA_TMP117_CONFIG_RESET == tmp117::CONFIG_RESET);
2526const _: () = assert!(PAMOJA_TMP117_HIGH_LIMIT_RESET == tmp117::HIGH_LIMIT_RESET);
2527const _: () = assert!(PAMOJA_TMP117_LOW_LIMIT_RESET == tmp117::LOW_LIMIT_RESET);
2528const _: () = assert!(PAMOJA_TMP117_TEMP_RESULT_RESET == tmp117::TEMP_RESULT_RESET);
2529const _: () = assert!(PAMOJA_TMP117_GENERAL_CALL_RESET == tmp117::GENERAL_CALL_RESET);
2530const _: () = assert!(PAMOJA_TMP117_EEPROM_UNLOCK == tmp117::EEPROM_UNLOCK);
2531const _: () = assert!(PAMOJA_TMP117_ADDRESS_ADD0_GND == tmp117::address::ADD0_GND);
2532const _: () = assert!(PAMOJA_TMP117_ADDRESS_ADD0_VPLUS == tmp117::address::ADD0_VPLUS);
2533const _: () = assert!(PAMOJA_TMP117_ADDRESS_ADD0_SDA == tmp117::address::ADD0_SDA);
2534const _: () = assert!(PAMOJA_TMP117_ADDRESS_ADD0_SCL == tmp117::address::ADD0_SCL);
2535const _: () = assert!(PAMOJA_TMP117_REGISTER_TEMP_RESULT == tmp117::register::TEMP_RESULT);
2536const _: () = assert!(PAMOJA_TMP117_REGISTER_CONFIGURATION == tmp117::register::CONFIGURATION);
2537const _: () = assert!(PAMOJA_TMP117_REGISTER_THIGH_LIMIT == tmp117::register::THIGH_LIMIT);
2538const _: () = assert!(PAMOJA_TMP117_REGISTER_TLOW_LIMIT == tmp117::register::TLOW_LIMIT);
2539const _: () = assert!(PAMOJA_TMP117_REGISTER_EEPROM_UL == tmp117::register::EEPROM_UL);
2540const _: () = assert!(PAMOJA_TMP117_REGISTER_EEPROM1 == tmp117::register::EEPROM1);
2541const _: () = assert!(PAMOJA_TMP117_REGISTER_EEPROM2 == tmp117::register::EEPROM2);
2542const _: () = assert!(PAMOJA_TMP117_REGISTER_TEMP_OFFSET == tmp117::register::TEMP_OFFSET);
2543const _: () = assert!(PAMOJA_TMP117_REGISTER_EEPROM3 == tmp117::register::EEPROM3);
2544const _: () = assert!(PAMOJA_TMP117_REGISTER_DEVICE_ID == tmp117::register::DEVICE_ID);
2545
2546/// A TMP117 configuration register, field by field.
2547#[repr(C)]
2548#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2549pub struct PamojaTmp117Config {
2550    /// `1` when a result went above the high limit.
2551    pub high_alert: u8,
2552    /// `1` when a result went below the low limit.
2553    pub low_alert: u8,
2554    /// `1` when a conversion has completed since the register was last read.
2555    pub data_ready: u8,
2556    /// `1` while an EEPROM write is still in progress.
2557    pub eeprom_busy: u8,
2558    /// The conversion-mode code: `0` continuous, `1` shutdown, `3` one-shot.
2559    pub mode: u8,
2560    /// The conversion-cycle code, `0..=7`.
2561    pub cycle: u8,
2562    /// The averaging code, `0..=3`.
2563    pub averaging: u8,
2564    /// `1` makes the limits a therm hysteresis band rather than alerts.
2565    pub therm_mode: u8,
2566    /// `1` makes the ALERT pin active high.
2567    pub alert_active_high: u8,
2568    /// `1` makes the ALERT pin reflect data ready rather than the alert flags.
2569    pub alert_pin_data_ready: u8,
2570    /// `1` triggers a software reset when this register is written.
2571    pub soft_reset: u8,
2572}
2573
2574/// Converts a raw TMP117 temperature register to nano-degrees Celsius.
2575///
2576/// # Returns
2577///
2578/// The temperature, exact in integer arithmetic at the part's 7.8125 m°C step.
2579#[no_mangle]
2580pub extern "C" fn pamoja_tmp117_nano_celsius(raw: i16) -> i64 {
2581    tmp117::nano_celsius(raw)
2582}
2583
2584/// Converts a raw TMP117 temperature register to micro-degrees Celsius.
2585///
2586/// # Returns
2587///
2588/// The temperature, truncated toward zero at the last digit.
2589#[no_mangle]
2590pub extern "C" fn pamoja_tmp117_micro_celsius(raw: i16) -> i32 {
2591    tmp117::micro_celsius(raw)
2592}
2593
2594/// Converts a raw TMP117 temperature register to degrees Celsius.
2595///
2596/// # Returns
2597///
2598/// The temperature.
2599#[no_mangle]
2600pub extern "C" fn pamoja_tmp117_celsius(raw: i16) -> f32 {
2601    tmp117::celsius(raw)
2602}
2603
2604/// Builds the TMP117 temperature register that decodes to a temperature.
2605///
2606/// # Returns
2607///
2608/// The nearest register value, saturating at the ends of the part's range.
2609#[no_mangle]
2610pub extern "C" fn pamoja_tmp117_raw_from_micro_celsius(micro_celsius: i32) -> i16 {
2611    tmp117::raw_from_micro_celsius(micro_celsius)
2612}
2613
2614/// Builds the TMP117 temperature register that decodes to a temperature in Celsius.
2615///
2616/// # Returns
2617///
2618/// The nearest register value, saturating at the ends of the part's range.
2619#[no_mangle]
2620pub extern "C" fn pamoja_tmp117_raw_from_celsius(celsius: f32) -> i16 {
2621    tmp117::raw_from_celsius(celsius)
2622}
2623
2624/// Builds the two bytes a TMP117 sends for a temperature register.
2625///
2626/// # Returns
2627///
2628/// [`PamojaStatus::Ok`] on success, with the two bytes written to `out_bytes`, or
2629/// [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
2630///
2631/// # Safety
2632///
2633/// `out_bytes` must point to at least two writable bytes.
2634#[no_mangle]
2635pub unsafe extern "C" fn pamoja_tmp117_temperature_bytes(
2636    raw: i16,
2637    out_bytes: *mut u8,
2638) -> PamojaStatus {
2639    if out_bytes.is_null() {
2640        set_last_error("out_bytes must not be null".to_owned());
2641        return PamojaStatus::InvalidArgument;
2642    }
2643    let bytes = tmp117::temperature_bytes(raw);
2644    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_TMP117_REGISTER_LEN);
2645    PamojaStatus::Ok
2646}
2647
2648/// Reads the two bytes a TMP117 sends for a temperature register.
2649///
2650/// # Returns
2651///
2652/// [`PamojaStatus::Ok`] on success, with `*out_raw` set, or
2653/// [`PamojaStatus::InvalidArgument`] if the buffer is not two bytes.
2654///
2655/// # Safety
2656///
2657/// `bytes` must point to at least `bytes_len` readable bytes, and `out_raw` must
2658/// point to a writable `int16_t`.
2659#[no_mangle]
2660pub unsafe extern "C" fn pamoja_tmp117_temperature_from_bytes(
2661    bytes: *const u8,
2662    bytes_len: usize,
2663    out_raw: *mut i16,
2664) -> PamojaStatus {
2665    if out_raw.is_null() {
2666        set_last_error("out_raw must not be null".to_owned());
2667        return PamojaStatus::InvalidArgument;
2668    }
2669    let bytes = match read_bytes(bytes, bytes_len) {
2670        Ok(bytes) => bytes,
2671        Err(status) => return status,
2672    };
2673    let Ok(bytes) = <[u8; PAMOJA_TMP117_REGISTER_LEN]>::try_from(&bytes[..]) else {
2674        return wrong_length("temperature register", PAMOJA_TMP117_REGISTER_LEN);
2675    };
2676    *out_raw = tmp117::temperature_from_bytes(bytes);
2677    PamojaStatus::Ok
2678}
2679
2680/// Reads the device identifier out of a TMP117 device-ID register.
2681///
2682/// # Returns
2683///
2684/// The low twelve bits, which are 0x117 for a TMP117.
2685#[no_mangle]
2686pub extern "C" fn pamoja_tmp117_device_id(raw: u16) -> u16 {
2687    tmp117::device_id(raw)
2688}
2689
2690/// Reads the die revision out of a TMP117 device-ID register.
2691///
2692/// # Returns
2693///
2694/// The high four bits.
2695#[no_mangle]
2696pub extern "C" fn pamoja_tmp117_revision(raw: u16) -> u8 {
2697    tmp117::revision(raw)
2698}
2699
2700/// Reports whether a TMP117 configuration register flags a high alert.
2701///
2702/// # Returns
2703///
2704/// `true` when a result went above the high limit.
2705#[no_mangle]
2706pub extern "C" fn pamoja_tmp117_high_alert(config: u16) -> bool {
2707    tmp117::high_alert(config)
2708}
2709
2710/// Reports whether a TMP117 configuration register flags a low alert.
2711///
2712/// # Returns
2713///
2714/// `true` when a result went below the low limit.
2715#[no_mangle]
2716pub extern "C" fn pamoja_tmp117_low_alert(config: u16) -> bool {
2717    tmp117::low_alert(config)
2718}
2719
2720/// Reports whether a TMP117 configuration register says a result is ready.
2721///
2722/// # Returns
2723///
2724/// `true` when a conversion completed since the register was last read.
2725#[no_mangle]
2726pub extern "C" fn pamoja_tmp117_data_ready(config: u16) -> bool {
2727    tmp117::data_ready(config)
2728}
2729
2730/// Reports whether a TMP117 configuration register says an EEPROM write is running.
2731///
2732/// # Returns
2733///
2734/// `true` while the write is in progress.
2735#[no_mangle]
2736pub extern "C" fn pamoja_tmp117_eeprom_busy(config: u16) -> bool {
2737    tmp117::eeprom_busy(config)
2738}
2739
2740/// Reports whether a TMP117 EEPROM unlock register says a write is running.
2741///
2742/// # Returns
2743///
2744/// `true` while the write is in progress.
2745#[no_mangle]
2746pub extern "C" fn pamoja_tmp117_eeprom_unlock_busy(unlock: u16) -> bool {
2747    tmp117::eeprom_unlock_busy(unlock)
2748}
2749
2750/// Assembles the 16-bit TMP117 configuration register value.
2751///
2752/// # Returns
2753///
2754/// The register value to write.
2755#[no_mangle]
2756pub extern "C" fn pamoja_tmp117_config_bits(config: PamojaTmp117Config) -> u16 {
2757    tmp117::Configuration::from(config).bits()
2758}
2759
2760/// Parses a 16-bit TMP117 configuration register value.
2761///
2762/// # Returns
2763///
2764/// [`PamojaStatus::Ok`], with `*out_config` filled in. Every register value
2765/// decodes, so this fails only on a null pointer.
2766///
2767/// # Safety
2768///
2769/// `out_config` must point to a writable `PamojaTmp117Config`.
2770#[no_mangle]
2771pub unsafe extern "C" fn pamoja_tmp117_config_from_bits(
2772    bits: u16,
2773    out_config: *mut PamojaTmp117Config,
2774) -> PamojaStatus {
2775    if out_config.is_null() {
2776        set_last_error("out_config must not be null".to_owned());
2777        return PamojaStatus::InvalidArgument;
2778    }
2779    *out_config = tmp117::Configuration::from_bits(bits).into();
2780    PamojaStatus::Ok
2781}
2782
2783/// Returns how many conversions a TMP117 averaging code folds into one result.
2784///
2785/// # Returns
2786///
2787/// The conversion count: 1, 8, 32, or 64.
2788#[no_mangle]
2789pub extern "C" fn pamoja_tmp117_averaging_conversions(code: u8) -> u8 {
2790    tmp117::Averaging::from_code(code).conversions()
2791}
2792
2793/// Returns how long a TMP117 averaging code takes to convert.
2794///
2795/// # Returns
2796///
2797/// The conversion time in microseconds.
2798#[no_mangle]
2799pub extern "C" fn pamoja_tmp117_averaging_micros(code: u8) -> u32 {
2800    tmp117::Averaging::from_code(code).conversion_micros()
2801}
2802
2803/// Returns the nominal cycle a TMP117 conversion-cycle code selects.
2804///
2805/// # Returns
2806///
2807/// The cycle in microseconds, before the averaging setting extends it.
2808#[no_mangle]
2809pub extern "C" fn pamoja_tmp117_cycle_nominal_micros(code: u8) -> u32 {
2810    tmp117::ConversionCycle::from_code(code).nominal_micros()
2811}
2812
2813/// Returns how often a TMP117 updates its result for a cycle and averaging code.
2814///
2815/// # Returns
2816///
2817/// The longer of the nominal cycle and the time the averaging takes.
2818#[no_mangle]
2819pub extern "C" fn pamoja_tmp117_cycle_micros(cycle: u8, averaging: u8) -> u32 {
2820    let averaging = tmp117::Averaging::from_code(averaging);
2821    tmp117::ConversionCycle::from_code(cycle).cycle_micros(averaging)
2822}
2823
2824/// The number of bytes an HDC1080 sequential read returns.
2825pub const PAMOJA_HDC1080_MEASUREMENT_LEN: usize = 4;
2826
2827/// The number of serial-ID registers an HDC1080 carries.
2828pub const PAMOJA_HDC1080_SERIAL_ID_REGISTERS: usize = 3;
2829
2830/// The single address an HDC1080 answers on.
2831pub const PAMOJA_HDC1080_I2C_ADDRESS: u8 = 0x40;
2832
2833/// The value its manufacturer-ID register reads: TI.
2834pub const PAMOJA_HDC1080_MANUFACTURER_ID: u16 = 0x5449;
2835
2836/// The value its device-ID register reads, which confirms the part.
2837pub const PAMOJA_HDC1080_DEVICE_ID: u16 = 0x1050;
2838
2839/// The value its configuration register reads after a reset.
2840pub const PAMOJA_HDC1080_CONFIGURATION_RESET: u16 = 0x1000;
2841
2842/// The HDC1080 temperature register.
2843pub const PAMOJA_HDC1080_REGISTER_TEMPERATURE: u8 = 0x00;
2844
2845/// The HDC1080 humidity register.
2846pub const PAMOJA_HDC1080_REGISTER_HUMIDITY: u8 = 0x01;
2847
2848/// The HDC1080 configuration register.
2849pub const PAMOJA_HDC1080_REGISTER_CONFIGURATION: u8 = 0x02;
2850
2851/// The high word of the HDC1080 serial ID.
2852pub const PAMOJA_HDC1080_REGISTER_SERIAL_ID_HIGH: u8 = 0xFB;
2853
2854/// The middle word of the HDC1080 serial ID.
2855pub const PAMOJA_HDC1080_REGISTER_SERIAL_ID_MID: u8 = 0xFC;
2856
2857/// The low word of the HDC1080 serial ID.
2858pub const PAMOJA_HDC1080_REGISTER_SERIAL_ID_LOW: u8 = 0xFD;
2859
2860/// The HDC1080 manufacturer-ID register.
2861pub const PAMOJA_HDC1080_REGISTER_MANUFACTURER_ID: u8 = 0xFE;
2862
2863/// The HDC1080 device-ID register.
2864pub const PAMOJA_HDC1080_REGISTER_DEVICE_ID: u8 = 0xFF;
2865
2866// The header generator does not read the crates this one depends on, so these
2867// carry their value rather than the name of the constant that defines it.
2868const _: () = assert!(PAMOJA_HDC1080_I2C_ADDRESS == hdc1080::I2C_ADDRESS);
2869const _: () = assert!(PAMOJA_HDC1080_MANUFACTURER_ID == hdc1080::MANUFACTURER_ID);
2870const _: () = assert!(PAMOJA_HDC1080_DEVICE_ID == hdc1080::DEVICE_ID);
2871const _: () = assert!(PAMOJA_HDC1080_CONFIGURATION_RESET == hdc1080::CONFIGURATION_RESET);
2872const _: () = assert!(PAMOJA_HDC1080_REGISTER_TEMPERATURE == hdc1080::register::TEMPERATURE);
2873const _: () = assert!(PAMOJA_HDC1080_REGISTER_HUMIDITY == hdc1080::register::HUMIDITY);
2874const _: () = assert!(PAMOJA_HDC1080_REGISTER_CONFIGURATION == hdc1080::register::CONFIGURATION);
2875const _: () = assert!(PAMOJA_HDC1080_REGISTER_SERIAL_ID_HIGH == hdc1080::register::SERIAL_ID_HIGH);
2876const _: () = assert!(PAMOJA_HDC1080_REGISTER_SERIAL_ID_MID == hdc1080::register::SERIAL_ID_MID);
2877const _: () = assert!(PAMOJA_HDC1080_REGISTER_SERIAL_ID_LOW == hdc1080::register::SERIAL_ID_LOW);
2878const _: () =
2879    assert!(PAMOJA_HDC1080_REGISTER_MANUFACTURER_ID == hdc1080::register::MANUFACTURER_ID);
2880const _: () = assert!(PAMOJA_HDC1080_REGISTER_DEVICE_ID == hdc1080::register::DEVICE_ID);
2881
2882/// A decoded HDC1080 temperature and humidity pair.
2883#[repr(C)]
2884#[derive(Clone, Copy, Debug, PartialEq)]
2885pub struct PamojaHdc1080Measurement {
2886    /// The raw temperature register.
2887    pub temperature_raw: u16,
2888    /// The raw humidity register.
2889    pub humidity_raw: u16,
2890    /// The temperature in milli-degrees Celsius, exact in integer arithmetic.
2891    pub milli_celsius: i32,
2892    /// The temperature in degrees Celsius.
2893    pub celsius: f32,
2894    /// The relative humidity in milli-percent.
2895    pub milli_percent: u32,
2896    /// The relative humidity as a percentage.
2897    pub relative_humidity: f32,
2898}
2899
2900/// An HDC1080 configuration register, field by field.
2901#[repr(C)]
2902#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2903pub struct PamojaHdc1080Config {
2904    /// `1` resets the part when this register is written.
2905    pub software_reset: u8,
2906    /// `1` runs the on-die heater during measurements.
2907    pub heater: u8,
2908    /// `1` acquires temperature and humidity from one trigger.
2909    pub sequential: u8,
2910    /// `1` when the supply has dropped below 2.8 V, which the part reports back.
2911    pub battery_low: u8,
2912    /// The temperature resolution in bits: 14 or 11.
2913    pub temperature_resolution_bits: u8,
2914    /// The humidity resolution in bits: 14, 11, or 8.
2915    pub humidity_resolution_bits: u8,
2916}
2917
2918/// Converts a raw HDC1080 temperature register to milli-degrees Celsius.
2919///
2920/// # Returns
2921///
2922/// The temperature, exact in integer arithmetic.
2923#[no_mangle]
2924pub extern "C" fn pamoja_hdc1080_milli_celsius(raw: u16) -> i32 {
2925    hdc1080::milli_celsius(raw)
2926}
2927
2928/// Converts a raw HDC1080 temperature register to degrees Celsius.
2929///
2930/// # Returns
2931///
2932/// The temperature.
2933#[no_mangle]
2934pub extern "C" fn pamoja_hdc1080_celsius(raw: u16) -> f32 {
2935    hdc1080::celsius(raw)
2936}
2937
2938/// Converts a raw HDC1080 humidity register to milli-percent.
2939///
2940/// # Returns
2941///
2942/// The relative humidity, exact in integer arithmetic.
2943#[no_mangle]
2944pub extern "C" fn pamoja_hdc1080_milli_percent(raw: u16) -> u32 {
2945    hdc1080::milli_percent(raw)
2946}
2947
2948/// Converts a raw HDC1080 humidity register to a relative humidity percentage.
2949///
2950/// # Returns
2951///
2952/// The relative humidity.
2953#[no_mangle]
2954pub extern "C" fn pamoja_hdc1080_relative_humidity(raw: u16) -> f32 {
2955    hdc1080::relative_humidity(raw)
2956}
2957
2958/// Builds the HDC1080 temperature register that decodes to a temperature.
2959///
2960/// # Returns
2961///
2962/// The 14-bit code in bits 15:2, clamped to the part's range.
2963#[no_mangle]
2964pub extern "C" fn pamoja_hdc1080_temperature_register(milli_celsius: i32) -> u16 {
2965    hdc1080::temperature_register(milli_celsius)
2966}
2967
2968/// Builds the HDC1080 humidity register that decodes to a relative humidity.
2969///
2970/// # Returns
2971///
2972/// The 14-bit code in bits 15:2, clamped to full scale.
2973#[no_mangle]
2974pub extern "C" fn pamoja_hdc1080_humidity_register(milli_percent: u32) -> u16 {
2975    hdc1080::humidity_register(milli_percent)
2976}
2977
2978/// Joins the three HDC1080 serial-ID registers into the 40-bit serial number.
2979///
2980/// # Returns
2981///
2982/// The serial number.
2983#[no_mangle]
2984pub extern "C" fn pamoja_hdc1080_serial_id(high: u16, mid: u16, low: u16) -> u64 {
2985    hdc1080::serial_id(high, mid, low)
2986}
2987
2988/// Splits a serial number back into the three HDC1080 serial-ID registers.
2989///
2990/// # Returns
2991///
2992/// [`PamojaStatus::Ok`] on success, with the three registers written to
2993/// `out_registers` high word first, or [`PamojaStatus::InvalidArgument`] if the
2994/// pointer is null.
2995///
2996/// # Safety
2997///
2998/// `out_registers` must point to at least three writable `uint16_t` values.
2999#[no_mangle]
3000pub unsafe extern "C" fn pamoja_hdc1080_serial_id_registers(
3001    serial: u64,
3002    out_registers: *mut u16,
3003) -> PamojaStatus {
3004    if out_registers.is_null() {
3005        set_last_error("out_registers must not be null".to_owned());
3006        return PamojaStatus::InvalidArgument;
3007    }
3008    let registers = hdc1080::serial_id_registers(serial);
3009    core::ptr::copy_nonoverlapping(
3010        registers.as_ptr(),
3011        out_registers,
3012        PAMOJA_HDC1080_SERIAL_ID_REGISTERS,
3013    );
3014    PamojaStatus::Ok
3015}
3016
3017/// Parses the four bytes an HDC1080 sequential read returns.
3018///
3019/// # Returns
3020///
3021/// [`PamojaStatus::Ok`] on success, with `*out_measurement` filled in, or
3022/// [`PamojaStatus::InvalidArgument`] if the buffer is not four bytes. The part
3023/// sends no checksum, so a well-sized read always decodes.
3024///
3025/// # Safety
3026///
3027/// `bytes` must point to at least `bytes_len` readable bytes, and
3028/// `out_measurement` must point to a writable `PamojaHdc1080Measurement`.
3029#[no_mangle]
3030pub unsafe extern "C" fn pamoja_hdc1080_parse_measurement(
3031    bytes: *const u8,
3032    bytes_len: usize,
3033    out_measurement: *mut PamojaHdc1080Measurement,
3034) -> PamojaStatus {
3035    if out_measurement.is_null() {
3036        set_last_error("out_measurement must not be null".to_owned());
3037        return PamojaStatus::InvalidArgument;
3038    }
3039    let bytes = match read_bytes(bytes, bytes_len) {
3040        Ok(bytes) => bytes,
3041        Err(status) => return status,
3042    };
3043    let Ok(bytes) = <[u8; PAMOJA_HDC1080_MEASUREMENT_LEN]>::try_from(&bytes[..]) else {
3044        return wrong_length("measurement", PAMOJA_HDC1080_MEASUREMENT_LEN);
3045    };
3046    *out_measurement = hdc1080::Measurement::parse(&bytes).into();
3047    PamojaStatus::Ok
3048}
3049
3050/// Builds the HDC1080 measurement a sensor reporting these physical values would send.
3051///
3052/// # Returns
3053///
3054/// [`PamojaStatus::Ok`], with `*out_measurement` filled in, or
3055/// [`PamojaStatus::InvalidArgument`] if the pointer is null.
3056///
3057/// # Safety
3058///
3059/// `out_measurement` must point to a writable `PamojaHdc1080Measurement`.
3060#[no_mangle]
3061pub unsafe extern "C" fn pamoja_hdc1080_measurement_from_physical(
3062    milli_celsius: i32,
3063    milli_percent: u32,
3064    out_measurement: *mut PamojaHdc1080Measurement,
3065) -> PamojaStatus {
3066    if out_measurement.is_null() {
3067        set_last_error("out_measurement must not be null".to_owned());
3068        return PamojaStatus::InvalidArgument;
3069    }
3070    let measurement = hdc1080::Measurement::from_physical(milli_celsius, milli_percent);
3071    *out_measurement = measurement.into();
3072    PamojaStatus::Ok
3073}
3074
3075/// Builds the four bytes an HDC1080 sends for a pair of raw registers.
3076///
3077/// # Returns
3078///
3079/// [`PamojaStatus::Ok`] on success, with the four bytes written to `out_bytes`,
3080/// or [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
3081///
3082/// # Safety
3083///
3084/// `out_bytes` must point to at least four writable bytes.
3085#[no_mangle]
3086pub unsafe extern "C" fn pamoja_hdc1080_measurement_bytes(
3087    temperature_raw: u16,
3088    humidity_raw: u16,
3089    out_bytes: *mut u8,
3090) -> PamojaStatus {
3091    if out_bytes.is_null() {
3092        set_last_error("out_bytes must not be null".to_owned());
3093        return PamojaStatus::InvalidArgument;
3094    }
3095    let measurement = hdc1080::Measurement {
3096        temperature: temperature_raw,
3097        humidity: humidity_raw,
3098    };
3099    let bytes = measurement.to_bytes();
3100    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_HDC1080_MEASUREMENT_LEN);
3101    PamojaStatus::Ok
3102}
3103
3104/// Parses an HDC1080 configuration register value.
3105///
3106/// # Returns
3107///
3108/// [`PamojaStatus::Ok`] on success, with `*out_config` filled in, or
3109/// [`PamojaStatus::Codec`] if the humidity-resolution field carries the code the
3110/// datasheet leaves undefined, which means the value did not come from a working
3111/// part.
3112///
3113/// # Safety
3114///
3115/// `out_config` must point to a writable `PamojaHdc1080Config`.
3116#[no_mangle]
3117pub unsafe extern "C" fn pamoja_hdc1080_config_from_register(
3118    raw: u16,
3119    out_config: *mut PamojaHdc1080Config,
3120) -> PamojaStatus {
3121    if out_config.is_null() {
3122        set_last_error("out_config must not be null".to_owned());
3123        return PamojaStatus::InvalidArgument;
3124    }
3125    match hdc1080::Configuration::from_register(raw) {
3126        Ok(config) => {
3127            *out_config = config.into();
3128            PamojaStatus::Ok
3129        }
3130        Err(error) => failed(error),
3131    }
3132}
3133
3134/// Assembles an HDC1080 configuration register value.
3135///
3136/// # Returns
3137///
3138/// [`PamojaStatus::Ok`] on success, with `*out_register` set, or
3139/// [`PamojaStatus::InvalidArgument`] if either resolution is not one the part
3140/// offers.
3141///
3142/// # Safety
3143///
3144/// `out_register` must point to a writable `uint16_t`.
3145#[no_mangle]
3146pub unsafe extern "C" fn pamoja_hdc1080_config_to_register(
3147    config: PamojaHdc1080Config,
3148    out_register: *mut u16,
3149) -> PamojaStatus {
3150    if out_register.is_null() {
3151        set_last_error("out_register must not be null".to_owned());
3152        return PamojaStatus::InvalidArgument;
3153    }
3154    match hdc1080::Configuration::try_from(config) {
3155        Ok(config) => {
3156            *out_register = config.to_register();
3157            PamojaStatus::Ok
3158        }
3159        Err(status) => status,
3160    }
3161}
3162
3163/// Returns how long to wait after triggering an HDC1080 in a configuration.
3164///
3165/// # Returns
3166///
3167/// [`PamojaStatus::Ok`] on success, with `*out_micros` set, or
3168/// [`PamojaStatus::InvalidArgument`] if either resolution is not one the part
3169/// offers.
3170///
3171/// # Safety
3172///
3173/// `out_micros` must point to a writable `uint32_t`.
3174#[no_mangle]
3175pub unsafe extern "C" fn pamoja_hdc1080_conversion_time_micros(
3176    config: PamojaHdc1080Config,
3177    out_micros: *mut u32,
3178) -> PamojaStatus {
3179    if out_micros.is_null() {
3180        set_last_error("out_micros must not be null".to_owned());
3181        return PamojaStatus::InvalidArgument;
3182    }
3183    match hdc1080::Configuration::try_from(config) {
3184        Ok(config) => {
3185            *out_micros = config.conversion_time_micros();
3186            PamojaStatus::Ok
3187        }
3188        Err(status) => status,
3189    }
3190}
3191
3192/// Returns how long an HDC1080 temperature conversion takes at a resolution.
3193///
3194/// # Returns
3195///
3196/// [`PamojaStatus::Ok`] on success, with `*out_micros` set, or
3197/// [`PamojaStatus::InvalidArgument`] if `bits` is not 14 or 11.
3198///
3199/// # Safety
3200///
3201/// `out_micros` must point to a writable `uint32_t`.
3202#[no_mangle]
3203pub unsafe extern "C" fn pamoja_hdc1080_temperature_conversion_micros(
3204    bits: u8,
3205    out_micros: *mut u32,
3206) -> PamojaStatus {
3207    if out_micros.is_null() {
3208        set_last_error("out_micros must not be null".to_owned());
3209        return PamojaStatus::InvalidArgument;
3210    }
3211    match temperature_resolution(bits) {
3212        Some(resolution) => {
3213            *out_micros = resolution.conversion_time_micros();
3214            PamojaStatus::Ok
3215        }
3216        None => bad_temperature_resolution(),
3217    }
3218}
3219
3220/// Returns how long an HDC1080 humidity conversion takes at a resolution.
3221///
3222/// # Returns
3223///
3224/// [`PamojaStatus::Ok`] on success, with `*out_micros` set, or
3225/// [`PamojaStatus::InvalidArgument`] if `bits` is not 14, 11, or 8.
3226///
3227/// # Safety
3228///
3229/// `out_micros` must point to a writable `uint32_t`.
3230#[no_mangle]
3231pub unsafe extern "C" fn pamoja_hdc1080_humidity_conversion_micros(
3232    bits: u8,
3233    out_micros: *mut u32,
3234) -> PamojaStatus {
3235    if out_micros.is_null() {
3236        set_last_error("out_micros must not be null".to_owned());
3237        return PamojaStatus::InvalidArgument;
3238    }
3239    match humidity_resolution(bits) {
3240        Some(resolution) => {
3241            *out_micros = resolution.conversion_time_micros();
3242            PamojaStatus::Ok
3243        }
3244        None => bad_humidity_resolution(),
3245    }
3246}
3247
3248/// The number of bytes in an OPT3001 register read.
3249pub const PAMOJA_OPT3001_REGISTER_LEN: usize = 2;
3250
3251/// The address an OPT3001 answers on with its ADDR pin tied to GND.
3252pub const PAMOJA_OPT3001_I2C_ADDRESS_GND: u8 = 0x44;
3253
3254/// The address it answers on with ADDR tied to VDD.
3255pub const PAMOJA_OPT3001_I2C_ADDRESS_VDD: u8 = 0x45;
3256
3257/// The address it answers on with ADDR tied to SDA.
3258pub const PAMOJA_OPT3001_I2C_ADDRESS_SDA: u8 = 0x46;
3259
3260/// The address it answers on with ADDR tied to SCL.
3261pub const PAMOJA_OPT3001_I2C_ADDRESS_SCL: u8 = 0x47;
3262
3263/// The value its manufacturer-ID register reads: TI.
3264pub const PAMOJA_OPT3001_MANUFACTURER_ID: u16 = 0x5449;
3265
3266/// The value its device-ID register reads, which confirms the part.
3267pub const PAMOJA_OPT3001_DEVICE_ID: u16 = 0x3001;
3268
3269/// The value its configuration register reads after a reset.
3270pub const PAMOJA_OPT3001_CONFIGURATION_RESET: u16 = 0xC810;
3271
3272/// The value its low-limit register reads after a reset.
3273pub const PAMOJA_OPT3001_LOW_LIMIT_RESET: u16 = 0x0000;
3274
3275/// The value its high-limit register reads after a reset.
3276pub const PAMOJA_OPT3001_HIGH_LIMIT_RESET: u16 = 0xBFFF;
3277
3278/// The low-limit value that turns the INT pin into an end-of-conversion signal.
3279pub const PAMOJA_OPT3001_LOW_LIMIT_END_OF_CONVERSION: u16 = 0xC000;
3280
3281/// The range number that lets the part choose its own full scale.
3282pub const PAMOJA_OPT3001_RANGE_AUTOMATIC: u8 = 0b1100;
3283
3284/// The highest fixed range number the part defines.
3285pub const PAMOJA_OPT3001_RANGE_MAX: u8 = 11;
3286
3287/// The OPT3001 result register.
3288pub const PAMOJA_OPT3001_REGISTER_RESULT: u8 = 0x00;
3289
3290/// The OPT3001 configuration register.
3291pub const PAMOJA_OPT3001_REGISTER_CONFIGURATION: u8 = 0x01;
3292
3293/// The OPT3001 low-limit register.
3294pub const PAMOJA_OPT3001_REGISTER_LOW_LIMIT: u8 = 0x02;
3295
3296/// The OPT3001 high-limit register.
3297pub const PAMOJA_OPT3001_REGISTER_HIGH_LIMIT: u8 = 0x03;
3298
3299/// The OPT3001 manufacturer-ID register.
3300pub const PAMOJA_OPT3001_REGISTER_MANUFACTURER_ID: u8 = 0x7E;
3301
3302/// The OPT3001 device-ID register.
3303pub const PAMOJA_OPT3001_REGISTER_DEVICE_ID: u8 = 0x7F;
3304
3305// The header generator does not read the crates this one depends on, so these
3306// carry their value rather than the name of the constant that defines it.
3307const _: () = assert!(PAMOJA_OPT3001_I2C_ADDRESS_GND == opt3001::I2C_ADDRESS_GND);
3308const _: () = assert!(PAMOJA_OPT3001_I2C_ADDRESS_VDD == opt3001::I2C_ADDRESS_VDD);
3309const _: () = assert!(PAMOJA_OPT3001_I2C_ADDRESS_SDA == opt3001::I2C_ADDRESS_SDA);
3310const _: () = assert!(PAMOJA_OPT3001_I2C_ADDRESS_SCL == opt3001::I2C_ADDRESS_SCL);
3311const _: () = assert!(PAMOJA_OPT3001_MANUFACTURER_ID == opt3001::MANUFACTURER_ID);
3312const _: () = assert!(PAMOJA_OPT3001_DEVICE_ID == opt3001::DEVICE_ID);
3313const _: () = assert!(PAMOJA_OPT3001_CONFIGURATION_RESET == opt3001::CONFIGURATION_RESET);
3314const _: () = assert!(PAMOJA_OPT3001_LOW_LIMIT_RESET == opt3001::LOW_LIMIT_RESET);
3315const _: () = assert!(PAMOJA_OPT3001_HIGH_LIMIT_RESET == opt3001::HIGH_LIMIT_RESET);
3316const _: () =
3317    assert!(PAMOJA_OPT3001_LOW_LIMIT_END_OF_CONVERSION == opt3001::LOW_LIMIT_END_OF_CONVERSION);
3318const _: () = assert!(PAMOJA_OPT3001_RANGE_AUTOMATIC == opt3001::RANGE_AUTOMATIC);
3319const _: () = assert!(PAMOJA_OPT3001_RANGE_MAX == opt3001::RANGE_MAX);
3320const _: () = assert!(PAMOJA_OPT3001_REGISTER_RESULT == opt3001::register::RESULT);
3321const _: () = assert!(PAMOJA_OPT3001_REGISTER_CONFIGURATION == opt3001::register::CONFIGURATION);
3322const _: () = assert!(PAMOJA_OPT3001_REGISTER_LOW_LIMIT == opt3001::register::LOW_LIMIT);
3323const _: () = assert!(PAMOJA_OPT3001_REGISTER_HIGH_LIMIT == opt3001::register::HIGH_LIMIT);
3324const _: () =
3325    assert!(PAMOJA_OPT3001_REGISTER_MANUFACTURER_ID == opt3001::register::MANUFACTURER_ID);
3326const _: () = assert!(PAMOJA_OPT3001_REGISTER_DEVICE_ID == opt3001::register::DEVICE_ID);
3327
3328/// An OPT3001 configuration register, field by field.
3329#[repr(C)]
3330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3331pub struct PamojaOpt3001Config {
3332    /// The full-scale range number, `0..=11`, or `12` to set the range automatically.
3333    pub range_number: u8,
3334    /// `1` makes a conversion take 800 ms rather than 100 ms.
3335    pub long_conversion: u8,
3336    /// The mode code: `0` shutdown, `1` single shot, `2` continuous.
3337    pub mode: u8,
3338    /// `1` when the last result overflowed its range.
3339    pub overflow: u8,
3340    /// `1` when a conversion has completed since the register was last read.
3341    pub conversion_ready: u8,
3342    /// `1` when the result went above the high limit.
3343    pub flag_high: u8,
3344    /// `1` when the result went below the low limit.
3345    pub flag_low: u8,
3346    /// `1` latches the INT pin until the configuration register is read.
3347    pub latched_window: u8,
3348    /// `1` makes the INT pin active high.
3349    pub active_high: u8,
3350    /// `1` makes the limit registers carry a mantissa alone, without an exponent.
3351    pub mask_exponent: u8,
3352    /// The fault-count code, `0..=3`, for one, two, four, or eight faults.
3353    pub fault_count: u8,
3354}
3355
3356/// Returns the illuminance one count carries at an OPT3001 exponent.
3357///
3358/// # Returns
3359///
3360/// `true` when the exponent is one the part defines, with `*out_milli_lux` set to
3361/// the step; `false` for a reserved exponent.
3362///
3363/// # Safety
3364///
3365/// `out_milli_lux` must point to a writable `uint32_t`.
3366#[no_mangle]
3367pub unsafe extern "C" fn pamoja_opt3001_lsb_milli_lux(
3368    exponent: u8,
3369    out_milli_lux: *mut u32,
3370) -> bool {
3371    match opt3001::lsb_milli_lux(exponent) {
3372        Some(step) if !out_milli_lux.is_null() => {
3373            *out_milli_lux = step;
3374            true
3375        }
3376        _ => false,
3377    }
3378}
3379
3380/// Returns the full scale an OPT3001 range number covers.
3381///
3382/// # Returns
3383///
3384/// `true` when the range is one the part defines, with `*out_milli_lux` set to
3385/// the full scale; `false` for a reserved range number, which has none.
3386///
3387/// # Safety
3388///
3389/// `out_milli_lux` must point to a writable `uint32_t`.
3390#[no_mangle]
3391pub unsafe extern "C" fn pamoja_opt3001_full_scale_milli_lux(
3392    range_number: u8,
3393    out_milli_lux: *mut u32,
3394) -> bool {
3395    match opt3001::full_scale_milli_lux(range_number) {
3396        Some(full_scale) if !out_milli_lux.is_null() => {
3397            *out_milli_lux = full_scale;
3398            true
3399        }
3400        _ => false,
3401    }
3402}
3403
3404/// Converts a raw OPT3001 result register to milli-lux.
3405///
3406/// # Returns
3407///
3408/// The illuminance, exact in integer arithmetic.
3409#[no_mangle]
3410pub extern "C" fn pamoja_opt3001_milli_lux(raw: u16) -> u32 {
3411    opt3001::milli_lux(raw)
3412}
3413
3414/// Converts a raw OPT3001 result register to lux.
3415///
3416/// # Returns
3417///
3418/// The illuminance.
3419#[no_mangle]
3420pub extern "C" fn pamoja_opt3001_lux(raw: u16) -> f32 {
3421    opt3001::lux(raw)
3422}
3423
3424/// Builds the OPT3001 result register that decodes to an illuminance.
3425///
3426/// # Returns
3427///
3428/// The register value, using the smallest exponent that fits and saturating at
3429/// full scale.
3430#[no_mangle]
3431pub extern "C" fn pamoja_opt3001_raw_from_milli_lux(milli_lux: u32) -> u16 {
3432    opt3001::raw_from_milli_lux(milli_lux)
3433}
3434
3435/// Reads the two bytes an OPT3001 sends for a register, most significant first.
3436///
3437/// # Returns
3438///
3439/// [`PamojaStatus::Ok`] on success, with `*out_word` set, or
3440/// [`PamojaStatus::InvalidArgument`] if the buffer is not two bytes.
3441///
3442/// # Safety
3443///
3444/// `bytes` must point to at least `bytes_len` readable bytes, and `out_word` must
3445/// point to a writable `uint16_t`.
3446#[no_mangle]
3447pub unsafe extern "C" fn pamoja_opt3001_word_from_bytes(
3448    bytes: *const u8,
3449    bytes_len: usize,
3450    out_word: *mut u16,
3451) -> PamojaStatus {
3452    if out_word.is_null() {
3453        set_last_error("out_word must not be null".to_owned());
3454        return PamojaStatus::InvalidArgument;
3455    }
3456    let bytes = match read_bytes(bytes, bytes_len) {
3457        Ok(bytes) => bytes,
3458        Err(status) => return status,
3459    };
3460    let Ok(bytes) = <[u8; PAMOJA_OPT3001_REGISTER_LEN]>::try_from(&bytes[..]) else {
3461        return wrong_length("register", PAMOJA_OPT3001_REGISTER_LEN);
3462    };
3463    *out_word = opt3001::word_from_bytes(bytes);
3464    PamojaStatus::Ok
3465}
3466
3467/// Builds the two bytes an OPT3001 sends for a register, most significant first.
3468///
3469/// # Returns
3470///
3471/// [`PamojaStatus::Ok`] on success, with the two bytes written to `out_bytes`, or
3472/// [`PamojaStatus::InvalidArgument`] if `out_bytes` is null.
3473///
3474/// # Safety
3475///
3476/// `out_bytes` must point to at least two writable bytes.
3477#[no_mangle]
3478pub unsafe extern "C" fn pamoja_opt3001_word_to_bytes(
3479    word: u16,
3480    out_bytes: *mut u8,
3481) -> PamojaStatus {
3482    if out_bytes.is_null() {
3483        set_last_error("out_bytes must not be null".to_owned());
3484        return PamojaStatus::InvalidArgument;
3485    }
3486    let bytes = opt3001::word_to_bytes(word);
3487    core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_bytes, PAMOJA_OPT3001_REGISTER_LEN);
3488    PamojaStatus::Ok
3489}
3490
3491/// Assembles the 16-bit OPT3001 configuration register value.
3492///
3493/// # Returns
3494///
3495/// The register value to write, with the read-only status bits written as zero.
3496#[no_mangle]
3497pub extern "C" fn pamoja_opt3001_config_bits(config: PamojaOpt3001Config) -> u16 {
3498    opt3001::Configuration::from(config).bits()
3499}
3500
3501/// Parses a 16-bit OPT3001 configuration register value.
3502///
3503/// # Returns
3504///
3505/// [`PamojaStatus::Ok`], with `*out_config` filled in. Every register value
3506/// decodes, so this fails only on a null pointer.
3507///
3508/// # Safety
3509///
3510/// `out_config` must point to a writable `PamojaOpt3001Config`.
3511#[no_mangle]
3512pub unsafe extern "C" fn pamoja_opt3001_config_from_bits(
3513    bits: u16,
3514    out_config: *mut PamojaOpt3001Config,
3515) -> PamojaStatus {
3516    if out_config.is_null() {
3517        set_last_error("out_config must not be null".to_owned());
3518        return PamojaStatus::InvalidArgument;
3519    }
3520    *out_config = opt3001::Configuration::from_bits(bits).into();
3521    PamojaStatus::Ok
3522}
3523
3524/// Returns the conversion time an OPT3001 setting selects.
3525///
3526/// # Returns
3527///
3528/// The time in milliseconds: 800 for the long conversion, 100 otherwise.
3529#[no_mangle]
3530pub extern "C" fn pamoja_opt3001_conversion_millis(long_conversion: bool) -> u16 {
3531    conversion_time(long_conversion).millis()
3532}
3533
3534/// Returns how many consecutive faults an OPT3001 fault-count code requires.
3535///
3536/// # Returns
3537///
3538/// The fault count: 1, 2, 4, or 8.
3539#[no_mangle]
3540pub extern "C" fn pamoja_opt3001_fault_count(code: u8) -> u8 {
3541    opt3001::FaultCount::from_code(code).count()
3542}
3543
3544/// Reports whether an OPT3001 range number sets the full scale automatically.
3545///
3546/// # Returns
3547///
3548/// `true` for the automatic range number, which has no fixed full scale.
3549#[no_mangle]
3550pub extern "C" fn pamoja_opt3001_is_automatic_range(range_number: u8) -> bool {
3551    range_number == opt3001::RANGE_AUTOMATIC
3552}
3553
3554/// The address an INA226 answers on with both address pins tied to GND.
3555pub const PAMOJA_INA226_BASE_ADDRESS: u8 = 0x40;
3556
3557/// The value its manufacturer-ID register reads: TI.
3558pub const PAMOJA_INA226_MANUFACTURER_ID: u16 = 0x5449;
3559
3560/// The device identifier its die-ID register carries.
3561pub const PAMOJA_INA226_DEVICE_ID: u16 = 0x226;
3562
3563/// The value its configuration register reads after a reset.
3564pub const PAMOJA_INA226_CONFIG_RESET: u16 = 0x4127;
3565
3566/// The shunt-voltage resolution, in nanovolts per count.
3567pub const PAMOJA_INA226_SHUNT_LSB_NANOVOLTS: i32 = 2_500;
3568
3569/// The bus-voltage resolution, in microvolts per count.
3570pub const PAMOJA_INA226_BUS_LSB_MICROVOLTS: u32 = 1_250;
3571
3572/// How many times the power resolution is the current resolution.
3573pub const PAMOJA_INA226_POWER_LSB_RATIO: u32 = 25;
3574
3575/// The INA226 configuration register.
3576pub const PAMOJA_INA226_REGISTER_CONFIGURATION: u8 = 0x00;
3577
3578/// The INA226 shunt-voltage register.
3579pub const PAMOJA_INA226_REGISTER_SHUNT_VOLTAGE: u8 = 0x01;
3580
3581/// The INA226 bus-voltage register.
3582pub const PAMOJA_INA226_REGISTER_BUS_VOLTAGE: u8 = 0x02;
3583
3584/// The INA226 power register.
3585pub const PAMOJA_INA226_REGISTER_POWER: u8 = 0x03;
3586
3587/// The INA226 current register.
3588pub const PAMOJA_INA226_REGISTER_CURRENT: u8 = 0x04;
3589
3590/// The INA226 calibration register.
3591pub const PAMOJA_INA226_REGISTER_CALIBRATION: u8 = 0x05;
3592
3593/// The INA226 Mask/Enable register.
3594pub const PAMOJA_INA226_REGISTER_MASK_ENABLE: u8 = 0x06;
3595
3596/// The INA226 alert-limit register.
3597pub const PAMOJA_INA226_REGISTER_ALERT_LIMIT: u8 = 0x07;
3598
3599/// The INA226 manufacturer-ID register.
3600pub const PAMOJA_INA226_REGISTER_MANUFACTURER_ID: u8 = 0xFE;
3601
3602/// The INA226 die-ID register.
3603pub const PAMOJA_INA226_REGISTER_DIE_ID: u8 = 0xFF;
3604
3605// The header generator does not read the crates this one depends on, so these
3606// carry their value rather than the name of the constant that defines it.
3607const _: () = assert!(PAMOJA_INA226_BASE_ADDRESS == ina226::BASE_ADDRESS);
3608const _: () = assert!(PAMOJA_INA226_MANUFACTURER_ID == ina226::MANUFACTURER_ID);
3609const _: () = assert!(PAMOJA_INA226_DEVICE_ID == ina226::DEVICE_ID);
3610const _: () = assert!(PAMOJA_INA226_CONFIG_RESET == ina226::CONFIG_RESET);
3611const _: () = assert!(PAMOJA_INA226_SHUNT_LSB_NANOVOLTS == ina226::SHUNT_LSB_NANOVOLTS);
3612const _: () = assert!(PAMOJA_INA226_BUS_LSB_MICROVOLTS == ina226::BUS_LSB_MICROVOLTS);
3613const _: () = assert!(PAMOJA_INA226_POWER_LSB_RATIO == ina226::POWER_LSB_RATIO);
3614const _: () = assert!(PAMOJA_INA226_REGISTER_CONFIGURATION == ina226::register::CONFIGURATION);
3615const _: () = assert!(PAMOJA_INA226_REGISTER_SHUNT_VOLTAGE == ina226::register::SHUNT_VOLTAGE);
3616const _: () = assert!(PAMOJA_INA226_REGISTER_BUS_VOLTAGE == ina226::register::BUS_VOLTAGE);
3617const _: () = assert!(PAMOJA_INA226_REGISTER_POWER == ina226::register::POWER);
3618const _: () = assert!(PAMOJA_INA226_REGISTER_CURRENT == ina226::register::CURRENT);
3619const _: () = assert!(PAMOJA_INA226_REGISTER_CALIBRATION == ina226::register::CALIBRATION);
3620const _: () = assert!(PAMOJA_INA226_REGISTER_MASK_ENABLE == ina226::register::MASK_ENABLE);
3621const _: () = assert!(PAMOJA_INA226_REGISTER_ALERT_LIMIT == ina226::register::ALERT_LIMIT);
3622const _: () = assert!(PAMOJA_INA226_REGISTER_MANUFACTURER_ID == ina226::register::MANUFACTURER_ID);
3623const _: () = assert!(PAMOJA_INA226_REGISTER_DIE_ID == ina226::register::DIE_ID);
3624
3625/// The limit comparison an INA226 alert pin responds to.
3626#[repr(C)]
3627#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3628pub enum PamojaIna226AlertFunction {
3629    /// Shunt voltage above the alert limit.
3630    ShuntOverLimit = 0,
3631    /// Shunt voltage below the alert limit.
3632    ShuntUnderLimit = 1,
3633    /// Bus voltage above the alert limit.
3634    BusOverLimit = 2,
3635    /// Bus voltage below the alert limit.
3636    BusUnderLimit = 3,
3637    /// Power above the alert limit.
3638    PowerOverLimit = 4,
3639}
3640
3641/// An INA226 configuration register, field by field.
3642#[repr(C)]
3643#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3644pub struct PamojaIna226Config {
3645    /// `1` resets the part when this register is written.
3646    pub reset: u8,
3647    /// The averaging code, `0..=7`, from 1 to 1024 samples.
3648    pub averaging: u8,
3649    /// The bus-voltage conversion-time code, `0..=7`.
3650    pub bus_conversion_time: u8,
3651    /// The shunt-voltage conversion-time code, `0..=7`.
3652    pub shunt_conversion_time: u8,
3653    /// The operating-mode code, `0..=7`.
3654    pub mode: u8,
3655}
3656
3657/// An INA226 Mask/Enable register, field by field.
3658#[repr(C)]
3659#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3660pub struct PamojaIna226MaskEnable {
3661    /// `1` alerts when the shunt voltage exceeds the limit.
3662    pub shunt_over_limit: u8,
3663    /// `1` alerts when the shunt voltage drops below the limit.
3664    pub shunt_under_limit: u8,
3665    /// `1` alerts when the bus voltage exceeds the limit.
3666    pub bus_over_limit: u8,
3667    /// `1` alerts when the bus voltage drops below the limit.
3668    pub bus_under_limit: u8,
3669    /// `1` alerts when the power exceeds the limit.
3670    pub power_over_limit: u8,
3671    /// `1` also alerts when a conversion completes.
3672    pub conversion_ready: u8,
3673    /// `1` when the selected limit function caused the last alert.
3674    pub alert_function_flag: u8,
3675    /// `1` when every conversion and multiplication has completed.
3676    pub conversion_ready_flag: u8,
3677    /// `1` when an arithmetic overflow left current and power invalid.
3678    pub math_overflow: u8,
3679    /// `1` makes the alert pin active high.
3680    pub alert_active_high: u8,
3681    /// `1` latches the alert pin until this register is read.
3682    pub alert_latch: u8,
3683}
3684
3685/// A decoded INA226 die-ID register.
3686#[repr(C)]
3687#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3688pub struct PamojaIna226DieId {
3689    /// The 12-bit device identifier.
3690    pub device: u16,
3691    /// The 4-bit die revision.
3692    pub revision: u8,
3693}
3694
3695/// Returns the I2C address an INA226's A1 and A0 pin codes select.
3696///
3697/// # Returns
3698///
3699/// [`PamojaStatus::Ok`] on success, with `*out_address` set to a 7-bit address in
3700/// `0x40..=0x4F`, or [`PamojaStatus::InvalidArgument`] if either code is above 3.
3701///
3702/// # Safety
3703///
3704/// `out_address` must point to a writable `uint8_t`.
3705#[no_mangle]
3706pub unsafe extern "C" fn pamoja_ina226_address(
3707    a1: u8,
3708    a0: u8,
3709    out_address: *mut u8,
3710) -> PamojaStatus {
3711    if out_address.is_null() {
3712        set_last_error("out_address must not be null".to_owned());
3713        return PamojaStatus::InvalidArgument;
3714    }
3715    let (Some(a1), Some(a0)) = (address_pin(a1), address_pin(a0)) else {
3716        set_last_error(
3717            "an INA226 address pin must be tied to GND, VS, SDA, or SCL: code 0 to 3".to_owned(),
3718        );
3719        return PamojaStatus::InvalidArgument;
3720    };
3721    *out_address = ina226::address(a1, a0);
3722    PamojaStatus::Ok
3723}
3724
3725/// Returns how many samples an INA226 averaging code folds into one result.
3726///
3727/// # Returns
3728///
3729/// The sample count, from 1 to 1024.
3730#[no_mangle]
3731pub extern "C" fn pamoja_ina226_averaging_samples(code: u8) -> u16 {
3732    averaging(code).samples()
3733}
3734
3735/// Returns the conversion time an INA226 code selects.
3736///
3737/// # Returns
3738///
3739/// The time in microseconds.
3740#[no_mangle]
3741pub extern "C" fn pamoja_ina226_conversion_micros(code: u8) -> u32 {
3742    conversion_time_setting(code).microseconds()
3743}
3744
3745/// Reports whether an INA226 mode code converts the shunt voltage.
3746///
3747/// # Returns
3748///
3749/// `true` when the shunt is measured in that mode.
3750#[no_mangle]
3751pub extern "C" fn pamoja_ina226_measures_shunt(code: u8) -> bool {
3752    mode(code).measures_shunt()
3753}
3754
3755/// Reports whether an INA226 mode code converts the bus voltage.
3756///
3757/// # Returns
3758///
3759/// `true` when the bus is measured in that mode.
3760#[no_mangle]
3761pub extern "C" fn pamoja_ina226_measures_bus(code: u8) -> bool {
3762    mode(code).measures_bus()
3763}
3764
3765/// Reports whether an INA226 mode code keeps converting after the first result.
3766///
3767/// # Returns
3768///
3769/// `true` for the continuous modes.
3770#[no_mangle]
3771pub extern "C" fn pamoja_ina226_is_continuous(code: u8) -> bool {
3772    mode(code).is_continuous()
3773}
3774
3775/// Parses an INA226 configuration register value.
3776///
3777/// # Returns
3778///
3779/// [`PamojaStatus::Ok`], with `*out_config` filled in. Every register value
3780/// decodes, so this fails only on a null pointer.
3781///
3782/// # Safety
3783///
3784/// `out_config` must point to a writable `PamojaIna226Config`.
3785#[no_mangle]
3786pub unsafe extern "C" fn pamoja_ina226_config_from_register(
3787    raw: u16,
3788    out_config: *mut PamojaIna226Config,
3789) -> PamojaStatus {
3790    if out_config.is_null() {
3791        set_last_error("out_config must not be null".to_owned());
3792        return PamojaStatus::InvalidArgument;
3793    }
3794    *out_config = ina226::Configuration::from_register(raw).into();
3795    PamojaStatus::Ok
3796}
3797
3798/// Assembles an INA226 configuration register value.
3799///
3800/// # Returns
3801///
3802/// The register value to write.
3803#[no_mangle]
3804pub extern "C" fn pamoja_ina226_config_to_register(config: PamojaIna226Config) -> u16 {
3805    ina226::Configuration::from(config).to_register()
3806}
3807
3808/// Returns how often an INA226 in a configuration updates its results.
3809///
3810/// # Returns
3811///
3812/// The interval in microseconds: every conversion the mode takes, averaged.
3813#[no_mangle]
3814pub extern "C" fn pamoja_ina226_update_micros(config: PamojaIna226Config) -> u32 {
3815    ina226::Configuration::from(config).update_microseconds()
3816}
3817
3818/// Parses an INA226 Mask/Enable register value.
3819///
3820/// # Returns
3821///
3822/// [`PamojaStatus::Ok`], with `*out_mask` filled in. Every register value
3823/// decodes, so this fails only on a null pointer.
3824///
3825/// # Safety
3826///
3827/// `out_mask` must point to a writable `PamojaIna226MaskEnable`.
3828#[no_mangle]
3829pub unsafe extern "C" fn pamoja_ina226_mask_enable_from_register(
3830    raw: u16,
3831    out_mask: *mut PamojaIna226MaskEnable,
3832) -> PamojaStatus {
3833    if out_mask.is_null() {
3834        set_last_error("out_mask must not be null".to_owned());
3835        return PamojaStatus::InvalidArgument;
3836    }
3837    *out_mask = ina226::MaskEnable::from_register(raw).into();
3838    PamojaStatus::Ok
3839}
3840
3841/// Assembles an INA226 Mask/Enable register value.
3842///
3843/// # Returns
3844///
3845/// The register value to write, with the read-only flags written as zero.
3846#[no_mangle]
3847pub extern "C" fn pamoja_ina226_mask_enable_to_register(mask: PamojaIna226MaskEnable) -> u16 {
3848    ina226::MaskEnable::from(mask).to_register()
3849}
3850
3851/// Returns the alert function an INA226 pin actually responds to.
3852///
3853/// # Returns
3854///
3855/// `true` when one limit function is selected, with `*out_function` set to it;
3856/// `false` when none is, so the pin only ever signals a completed conversion.
3857///
3858/// # Safety
3859///
3860/// `out_function` must point to a writable `PamojaIna226AlertFunction`.
3861#[no_mangle]
3862pub unsafe extern "C" fn pamoja_ina226_active_alert_function(
3863    mask: PamojaIna226MaskEnable,
3864    out_function: *mut PamojaIna226AlertFunction,
3865) -> bool {
3866    match ina226::MaskEnable::from(mask).active_alert_function() {
3867        Some(function) if !out_function.is_null() => {
3868            *out_function = function.into();
3869            true
3870        }
3871        _ => false,
3872    }
3873}
3874
3875/// Splits an INA226 die-ID register into its device and revision fields.
3876///
3877/// # Returns
3878///
3879/// [`PamojaStatus::Ok`], with `*out_die_id` filled in. Every register value
3880/// decodes, so this fails only on a null pointer.
3881///
3882/// # Safety
3883///
3884/// `out_die_id` must point to a writable `PamojaIna226DieId`.
3885#[no_mangle]
3886pub unsafe extern "C" fn pamoja_ina226_die_id(
3887    raw: u16,
3888    out_die_id: *mut PamojaIna226DieId,
3889) -> PamojaStatus {
3890    if out_die_id.is_null() {
3891        set_last_error("out_die_id must not be null".to_owned());
3892        return PamojaStatus::InvalidArgument;
3893    }
3894    *out_die_id = ina226::DieId::from_register(raw).into();
3895    PamojaStatus::Ok
3896}
3897
3898/// Checks that a pair of identification registers belongs to an INA226.
3899///
3900/// # Returns
3901///
3902/// [`PamojaStatus::Ok`] on success, with `*out_die_id` filled in, or
3903/// [`PamojaStatus::Codec`] if either register carries something other than the
3904/// values the datasheet fixes, which means a different part, or nothing at all,
3905/// answered at that address.
3906///
3907/// # Safety
3908///
3909/// `out_die_id` must point to a writable `PamojaIna226DieId`.
3910#[no_mangle]
3911pub unsafe extern "C" fn pamoja_ina226_identify(
3912    manufacturer_id: u16,
3913    die_id: u16,
3914    out_die_id: *mut PamojaIna226DieId,
3915) -> PamojaStatus {
3916    if out_die_id.is_null() {
3917        set_last_error("out_die_id must not be null".to_owned());
3918        return PamojaStatus::InvalidArgument;
3919    }
3920    match ina226::identify(manufacturer_id, die_id) {
3921        Ok(identified) => {
3922            *out_die_id = identified.into();
3923            PamojaStatus::Ok
3924        }
3925        Err(error) => failed(error),
3926    }
3927}
3928
3929/// Computes the INA226 calibration register for a shunt and current resolution.
3930///
3931/// # Returns
3932///
3933/// The register value to write.
3934#[no_mangle]
3935pub extern "C" fn pamoja_ina226_calibration(
3936    current_lsb_microamps: u32,
3937    shunt_milliohms: u32,
3938) -> u16 {
3939    ina226::calibration(current_lsb_microamps, shunt_milliohms)
3940}
3941
3942/// Returns the smallest current resolution that still covers an expected maximum.
3943///
3944/// # Returns
3945///
3946/// The current LSB in microamps.
3947#[no_mangle]
3948pub extern "C" fn pamoja_ina226_minimum_current_lsb_microamps(max_expected_microamps: u32) -> u32 {
3949    ina226::minimum_current_lsb_microamps(max_expected_microamps)
3950}
3951
3952/// Converts a raw INA226 shunt-voltage register to nanovolts.
3953///
3954/// # Returns
3955///
3956/// The shunt voltage, exact in integer arithmetic at 2.5 uV per count.
3957#[no_mangle]
3958pub extern "C" fn pamoja_ina226_shunt_nanovolts(raw: i16) -> i32 {
3959    ina226::shunt_nanovolts(raw)
3960}
3961
3962/// Converts a raw INA226 shunt-voltage register to millivolts.
3963///
3964/// # Returns
3965///
3966/// The shunt voltage.
3967#[no_mangle]
3968pub extern "C" fn pamoja_ina226_shunt_millivolts(raw: i16) -> f32 {
3969    ina226::shunt_millivolts_f32(raw)
3970}
3971
3972/// Converts a raw INA226 bus-voltage register to microvolts.
3973///
3974/// # Returns
3975///
3976/// The bus voltage, exact in integer arithmetic at 1.25 mV per count.
3977#[no_mangle]
3978pub extern "C" fn pamoja_ina226_bus_microvolts(raw: u16) -> u32 {
3979    ina226::bus_microvolts(raw)
3980}
3981
3982/// Converts a raw INA226 bus-voltage register to volts.
3983///
3984/// # Returns
3985///
3986/// The bus voltage.
3987#[no_mangle]
3988pub extern "C" fn pamoja_ina226_bus_volts(raw: u16) -> f32 {
3989    ina226::bus_volts_f32(raw)
3990}
3991
3992/// Converts a raw INA226 current register to microamps.
3993///
3994/// # Returns
3995///
3996/// The current, at the resolution the calibration selected.
3997#[no_mangle]
3998pub extern "C" fn pamoja_ina226_current_microamps(raw: i16, current_lsb_microamps: u32) -> i32 {
3999    ina226::current_microamps(raw, current_lsb_microamps)
4000}
4001
4002/// Converts a raw INA226 current register to amps.
4003///
4004/// # Returns
4005///
4006/// The current.
4007#[no_mangle]
4008pub extern "C" fn pamoja_ina226_current_amps(raw: i16, current_lsb_microamps: u32) -> f32 {
4009    ina226::current_amps_f32(raw, current_lsb_microamps)
4010}
4011
4012/// Converts a raw INA226 power register to microwatts.
4013///
4014/// # Returns
4015///
4016/// The power. The power LSB is fixed at twenty-five times the current LSB.
4017#[no_mangle]
4018pub extern "C" fn pamoja_ina226_power_microwatts(raw: u16, current_lsb_microamps: u32) -> u32 {
4019    ina226::power_microwatts(raw, current_lsb_microamps)
4020}
4021
4022/// Converts a raw INA226 power register to watts.
4023///
4024/// # Returns
4025///
4026/// The power.
4027#[no_mangle]
4028pub extern "C" fn pamoja_ina226_power_watts(raw: u16, current_lsb_microamps: u32) -> f32 {
4029    ina226::power_watts_f32(raw, current_lsb_microamps)
4030}
4031
4032/// Builds the INA226 shunt-voltage register a monitor reports for a shunt voltage.
4033///
4034/// # Returns
4035///
4036/// The signed register value, at 2.5 uV per count.
4037#[no_mangle]
4038pub extern "C" fn pamoja_ina226_shunt_register(nanovolts: i32) -> i16 {
4039    ina226::shunt_register(nanovolts)
4040}
4041
4042/// Builds the INA226 bus-voltage register a monitor reports for a bus voltage.
4043///
4044/// # Returns
4045///
4046/// The register value, at 1.25 mV per count.
4047#[no_mangle]
4048pub extern "C" fn pamoja_ina226_bus_register(microvolts: u32) -> u16 {
4049    ina226::bus_register(microvolts)
4050}
4051
4052/// Builds the INA226 current register a monitor reports for a current.
4053///
4054/// # Returns
4055///
4056/// The signed register value, or zero if `current_lsb_microamps` is zero.
4057#[no_mangle]
4058pub extern "C" fn pamoja_ina226_current_register(
4059    microamps: i32,
4060    current_lsb_microamps: u32,
4061) -> i16 {
4062    ina226::current_register(microamps, current_lsb_microamps)
4063}
4064
4065/// Builds the INA226 power register a monitor reports for a power.
4066///
4067/// # Returns
4068///
4069/// The register value, or zero if `current_lsb_microamps` is zero.
4070#[no_mangle]
4071pub extern "C" fn pamoja_ina226_power_register(microwatts: u32, current_lsb_microamps: u32) -> u16 {
4072    ina226::power_register(microwatts, current_lsb_microamps)
4073}
4074
4075/// Computes the INA226 current register the chip derives from a shunt reading.
4076///
4077/// # Returns
4078///
4079/// The signed current register the part's own arithmetic produces.
4080#[no_mangle]
4081pub extern "C" fn pamoja_ina226_current_register_from_shunt(shunt: i16, calibration: u16) -> i16 {
4082    ina226::current_register_from_shunt(shunt, calibration)
4083}
4084
4085/// Computes the INA226 power register the chip derives from a current reading.
4086///
4087/// # Returns
4088///
4089/// The power register the part's own arithmetic produces.
4090#[no_mangle]
4091pub extern "C" fn pamoja_ina226_power_register_from_current(current: i16, bus: u16) -> u16 {
4092    ina226::power_register_from_current(current, bus)
4093}
4094
4095impl From<PamojaAds1115Config> for ads1115::Config {
4096    fn from(value: PamojaAds1115Config) -> Self {
4097        ads1115::Config {
4098            start_conversion: value.start_conversion != 0,
4099            mux: ads1115::Mux::from_code(value.mux),
4100            pga: ads1115::Pga::from_code(value.pga),
4101            mode: if value.single_shot != 0 {
4102                ads1115::Mode::SingleShot
4103            } else {
4104                ads1115::Mode::Continuous
4105            },
4106            data_rate: ads1115::DataRate::from_code(value.data_rate),
4107            comparator_mode: if value.window_comparator != 0 {
4108                ads1115::ComparatorMode::Window
4109            } else {
4110                ads1115::ComparatorMode::Traditional
4111            },
4112            comparator_polarity: if value.comparator_active_high != 0 {
4113                ads1115::ComparatorPolarity::ActiveHigh
4114            } else {
4115                ads1115::ComparatorPolarity::ActiveLow
4116            },
4117            comparator_latch: if value.comparator_latching != 0 {
4118                ads1115::ComparatorLatch::Latching
4119            } else {
4120                ads1115::ComparatorLatch::NonLatching
4121            },
4122            comparator_queue: ads1115::ComparatorQueue::from_code(value.comparator_queue),
4123        }
4124    }
4125}
4126
4127impl From<ads1115::Config> for PamojaAds1115Config {
4128    fn from(value: ads1115::Config) -> Self {
4129        PamojaAds1115Config {
4130            start_conversion: u8::from(value.start_conversion),
4131            mux: value.mux.code(),
4132            pga: value.pga.code(),
4133            single_shot: u8::from(matches!(value.mode, ads1115::Mode::SingleShot)),
4134            data_rate: value.data_rate.code(),
4135            window_comparator: u8::from(matches!(
4136                value.comparator_mode,
4137                ads1115::ComparatorMode::Window
4138            )),
4139            comparator_active_high: u8::from(matches!(
4140                value.comparator_polarity,
4141                ads1115::ComparatorPolarity::ActiveHigh
4142            )),
4143            comparator_latching: u8::from(matches!(
4144                value.comparator_latch,
4145                ads1115::ComparatorLatch::Latching
4146            )),
4147            comparator_queue: value.comparator_queue.code(),
4148        }
4149    }
4150}
4151
4152impl From<bmp280::Calibration> for PamojaBmp280Coefficients {
4153    fn from(value: bmp280::Calibration) -> Self {
4154        PamojaBmp280Coefficients {
4155            dig_t1: value.dig_t1,
4156            dig_t2: value.dig_t2,
4157            dig_t3: value.dig_t3,
4158            dig_p1: value.dig_p1,
4159            dig_p2: value.dig_p2,
4160            dig_p3: value.dig_p3,
4161            dig_p4: value.dig_p4,
4162            dig_p5: value.dig_p5,
4163            dig_p6: value.dig_p6,
4164            dig_p7: value.dig_p7,
4165            dig_p8: value.dig_p8,
4166            dig_p9: value.dig_p9,
4167        }
4168    }
4169}
4170
4171impl From<bmp280::Measurement> for PamojaBmp280Measurement {
4172    fn from(value: bmp280::Measurement) -> Self {
4173        PamojaBmp280Measurement {
4174            pressure: value.pressure,
4175            temperature: value.temperature,
4176            pressure_skipped: u8::from(value.pressure_skipped()),
4177            temperature_skipped: u8::from(value.temperature_skipped()),
4178        }
4179    }
4180}
4181
4182impl From<PamojaBmp280CtrlMeas> for bmp280::CtrlMeas {
4183    fn from(value: PamojaBmp280CtrlMeas) -> Self {
4184        bmp280::CtrlMeas {
4185            temperature: bmp280::Oversampling::from_code(value.temperature),
4186            pressure: bmp280::Oversampling::from_code(value.pressure),
4187            mode: bmp280::Mode::from_code(value.mode),
4188        }
4189    }
4190}
4191
4192impl From<bmp280::CtrlMeas> for PamojaBmp280CtrlMeas {
4193    fn from(value: bmp280::CtrlMeas) -> Self {
4194        PamojaBmp280CtrlMeas {
4195            temperature: value.temperature.code(),
4196            pressure: value.pressure.code(),
4197            mode: value.mode.code(),
4198        }
4199    }
4200}
4201
4202impl From<PamojaBmp280Config> for bmp280::Config {
4203    fn from(value: PamojaBmp280Config) -> Self {
4204        bmp280::Config {
4205            standby: bmp280::Standby::from_code(value.standby),
4206            filter: value.filter,
4207            spi_3wire: value.spi_3wire != 0,
4208        }
4209    }
4210}
4211
4212impl From<bmp280::Config> for PamojaBmp280Config {
4213    fn from(value: bmp280::Config) -> Self {
4214        PamojaBmp280Config {
4215            standby: value.standby.code(),
4216            filter: value.filter,
4217            spi_3wire: u8::from(value.spi_3wire),
4218        }
4219    }
4220}
4221
4222impl From<sht3x::Measurement> for PamojaSht3xMeasurement {
4223    fn from(value: sht3x::Measurement) -> Self {
4224        PamojaSht3xMeasurement {
4225            temperature_raw: value.temperature_raw,
4226            humidity_raw: value.humidity_raw,
4227            milli_celsius: value.temperature_milli_celsius(),
4228            celsius: value.temperature_celsius(),
4229            milli_fahrenheit: value.temperature_milli_fahrenheit(),
4230            fahrenheit: value.temperature_fahrenheit(),
4231            milli_percent: value.humidity_milli_percent(),
4232            relative_humidity: value.relative_humidity(),
4233        }
4234    }
4235}
4236
4237impl From<sht3x::Status> for PamojaSht3xStatus {
4238    fn from(value: sht3x::Status) -> Self {
4239        PamojaSht3xStatus {
4240            bits: value.bits(),
4241            alert_pending: u8::from(value.alert_pending()),
4242            heater_on: u8::from(value.heater_on()),
4243            humidity_tracking_alert: u8::from(value.humidity_tracking_alert()),
4244            temperature_tracking_alert: u8::from(value.temperature_tracking_alert()),
4245            reset_detected: u8::from(value.reset_detected()),
4246            command_failed: u8::from(value.command_failed()),
4247            write_checksum_failed: u8::from(value.write_checksum_failed()),
4248        }
4249    }
4250}
4251
4252impl From<scd4x::Measurement> for PamojaScd4xMeasurement {
4253    fn from(value: scd4x::Measurement) -> Self {
4254        PamojaScd4xMeasurement {
4255            co2_ppm: value.co2_ppm,
4256            temperature_raw: value.temperature_raw,
4257            humidity_raw: value.humidity_raw,
4258            milli_celsius: value.milli_celsius(),
4259            celsius: value.celsius(),
4260            humidity_milli_percent: value.humidity_milli_percent(),
4261            relative_humidity_percent: value.relative_humidity_percent(),
4262        }
4263    }
4264}
4265
4266impl From<PamojaTmp117Config> for tmp117::Configuration {
4267    fn from(value: PamojaTmp117Config) -> Self {
4268        tmp117::Configuration {
4269            high_alert: value.high_alert != 0,
4270            low_alert: value.low_alert != 0,
4271            data_ready: value.data_ready != 0,
4272            eeprom_busy: value.eeprom_busy != 0,
4273            mode: tmp117::ConversionMode::from_code(value.mode),
4274            cycle: tmp117::ConversionCycle::from_code(value.cycle),
4275            averaging: tmp117::Averaging::from_code(value.averaging),
4276            alert_mode: if value.therm_mode != 0 {
4277                tmp117::AlertMode::Therm
4278            } else {
4279                tmp117::AlertMode::Alert
4280            },
4281            alert_polarity: if value.alert_active_high != 0 {
4282                tmp117::AlertPolarity::ActiveHigh
4283            } else {
4284                tmp117::AlertPolarity::ActiveLow
4285            },
4286            alert_pin: if value.alert_pin_data_ready != 0 {
4287                tmp117::AlertPin::DataReady
4288            } else {
4289                tmp117::AlertPin::AlertFlags
4290            },
4291            soft_reset: value.soft_reset != 0,
4292        }
4293    }
4294}
4295
4296impl From<tmp117::Configuration> for PamojaTmp117Config {
4297    fn from(value: tmp117::Configuration) -> Self {
4298        PamojaTmp117Config {
4299            high_alert: u8::from(value.high_alert),
4300            low_alert: u8::from(value.low_alert),
4301            data_ready: u8::from(value.data_ready),
4302            eeprom_busy: u8::from(value.eeprom_busy),
4303            mode: value.mode.code(),
4304            cycle: value.cycle.code(),
4305            averaging: value.averaging.code(),
4306            therm_mode: u8::from(matches!(value.alert_mode, tmp117::AlertMode::Therm)),
4307            alert_active_high: u8::from(matches!(
4308                value.alert_polarity,
4309                tmp117::AlertPolarity::ActiveHigh
4310            )),
4311            alert_pin_data_ready: u8::from(matches!(value.alert_pin, tmp117::AlertPin::DataReady)),
4312            soft_reset: u8::from(value.soft_reset),
4313        }
4314    }
4315}
4316
4317impl From<hdc1080::Measurement> for PamojaHdc1080Measurement {
4318    fn from(value: hdc1080::Measurement) -> Self {
4319        PamojaHdc1080Measurement {
4320            temperature_raw: value.temperature,
4321            humidity_raw: value.humidity,
4322            milli_celsius: value.milli_celsius(),
4323            celsius: value.celsius(),
4324            milli_percent: value.milli_percent(),
4325            relative_humidity: value.relative_humidity(),
4326        }
4327    }
4328}
4329
4330impl From<hdc1080::Configuration> for PamojaHdc1080Config {
4331    fn from(value: hdc1080::Configuration) -> Self {
4332        PamojaHdc1080Config {
4333            software_reset: u8::from(value.software_reset),
4334            heater: u8::from(value.heater),
4335            sequential: u8::from(matches!(
4336                value.mode,
4337                hdc1080::AcquisitionMode::TemperatureThenHumidity
4338            )),
4339            battery_low: u8::from(value.battery_low),
4340            temperature_resolution_bits: match value.temperature_resolution {
4341                hdc1080::TemperatureResolution::Bits14 => 14,
4342                hdc1080::TemperatureResolution::Bits11 => 11,
4343            },
4344            humidity_resolution_bits: match value.humidity_resolution {
4345                hdc1080::HumidityResolution::Bits14 => 14,
4346                hdc1080::HumidityResolution::Bits11 => 11,
4347                hdc1080::HumidityResolution::Bits8 => 8,
4348            },
4349        }
4350    }
4351}
4352
4353impl TryFrom<PamojaHdc1080Config> for hdc1080::Configuration {
4354    type Error = PamojaStatus;
4355
4356    fn try_from(value: PamojaHdc1080Config) -> Result<Self, PamojaStatus> {
4357        let Some(temperature_resolution) =
4358            temperature_resolution(value.temperature_resolution_bits)
4359        else {
4360            return Err(bad_temperature_resolution());
4361        };
4362        let Some(humidity_resolution) = humidity_resolution(value.humidity_resolution_bits) else {
4363            return Err(bad_humidity_resolution());
4364        };
4365        Ok(hdc1080::Configuration {
4366            software_reset: value.software_reset != 0,
4367            heater: value.heater != 0,
4368            mode: if value.sequential != 0 {
4369                hdc1080::AcquisitionMode::TemperatureThenHumidity
4370            } else {
4371                hdc1080::AcquisitionMode::Single
4372            },
4373            battery_low: value.battery_low != 0,
4374            temperature_resolution,
4375            humidity_resolution,
4376        })
4377    }
4378}
4379
4380impl From<PamojaOpt3001Config> for opt3001::Configuration {
4381    fn from(value: PamojaOpt3001Config) -> Self {
4382        opt3001::Configuration {
4383            range_number: value.range_number,
4384            conversion_time: conversion_time(value.long_conversion != 0),
4385            mode: opt3001::Mode::from_code(value.mode),
4386            overflow: value.overflow != 0,
4387            conversion_ready: value.conversion_ready != 0,
4388            flag_high: value.flag_high != 0,
4389            flag_low: value.flag_low != 0,
4390            latch: if value.latched_window != 0 {
4391                opt3001::Latch::LatchedWindow
4392            } else {
4393                opt3001::Latch::TransparentHysteresis
4394            },
4395            polarity: if value.active_high != 0 {
4396                opt3001::Polarity::ActiveHigh
4397            } else {
4398                opt3001::Polarity::ActiveLow
4399            },
4400            mask_exponent: value.mask_exponent != 0,
4401            fault_count: opt3001::FaultCount::from_code(value.fault_count),
4402        }
4403    }
4404}
4405
4406impl From<opt3001::Configuration> for PamojaOpt3001Config {
4407    fn from(value: opt3001::Configuration) -> Self {
4408        PamojaOpt3001Config {
4409            range_number: value.range_number,
4410            long_conversion: u8::from(matches!(
4411                value.conversion_time,
4412                opt3001::ConversionTime::Ms800
4413            )),
4414            mode: value.mode.code(),
4415            overflow: u8::from(value.overflow),
4416            conversion_ready: u8::from(value.conversion_ready),
4417            flag_high: u8::from(value.flag_high),
4418            flag_low: u8::from(value.flag_low),
4419            latched_window: u8::from(matches!(value.latch, opt3001::Latch::LatchedWindow)),
4420            active_high: u8::from(matches!(value.polarity, opt3001::Polarity::ActiveHigh)),
4421            mask_exponent: u8::from(value.mask_exponent),
4422            fault_count: value.fault_count.code(),
4423        }
4424    }
4425}
4426
4427impl From<PamojaIna226Config> for ina226::Configuration {
4428    fn from(value: PamojaIna226Config) -> Self {
4429        ina226::Configuration {
4430            reset: value.reset != 0,
4431            averaging: averaging(value.averaging),
4432            bus_conversion_time: conversion_time_setting(value.bus_conversion_time),
4433            shunt_conversion_time: conversion_time_setting(value.shunt_conversion_time),
4434            mode: mode(value.mode),
4435        }
4436    }
4437}
4438
4439impl From<ina226::Configuration> for PamojaIna226Config {
4440    fn from(value: ina226::Configuration) -> Self {
4441        PamojaIna226Config {
4442            reset: u8::from(value.reset),
4443            averaging: value.averaging as u8,
4444            bus_conversion_time: value.bus_conversion_time as u8,
4445            shunt_conversion_time: value.shunt_conversion_time as u8,
4446            mode: value.mode as u8,
4447        }
4448    }
4449}
4450
4451impl From<PamojaIna226MaskEnable> for ina226::MaskEnable {
4452    fn from(value: PamojaIna226MaskEnable) -> Self {
4453        ina226::MaskEnable {
4454            shunt_over_limit: value.shunt_over_limit != 0,
4455            shunt_under_limit: value.shunt_under_limit != 0,
4456            bus_over_limit: value.bus_over_limit != 0,
4457            bus_under_limit: value.bus_under_limit != 0,
4458            power_over_limit: value.power_over_limit != 0,
4459            conversion_ready: value.conversion_ready != 0,
4460            alert_function_flag: value.alert_function_flag != 0,
4461            conversion_ready_flag: value.conversion_ready_flag != 0,
4462            math_overflow: value.math_overflow != 0,
4463            alert_active_high: value.alert_active_high != 0,
4464            alert_latch: value.alert_latch != 0,
4465        }
4466    }
4467}
4468
4469impl From<ina226::MaskEnable> for PamojaIna226MaskEnable {
4470    fn from(value: ina226::MaskEnable) -> Self {
4471        PamojaIna226MaskEnable {
4472            shunt_over_limit: u8::from(value.shunt_over_limit),
4473            shunt_under_limit: u8::from(value.shunt_under_limit),
4474            bus_over_limit: u8::from(value.bus_over_limit),
4475            bus_under_limit: u8::from(value.bus_under_limit),
4476            power_over_limit: u8::from(value.power_over_limit),
4477            conversion_ready: u8::from(value.conversion_ready),
4478            alert_function_flag: u8::from(value.alert_function_flag),
4479            conversion_ready_flag: u8::from(value.conversion_ready_flag),
4480            math_overflow: u8::from(value.math_overflow),
4481            alert_active_high: u8::from(value.alert_active_high),
4482            alert_latch: u8::from(value.alert_latch),
4483        }
4484    }
4485}
4486
4487impl From<ina226::AlertFunction> for PamojaIna226AlertFunction {
4488    fn from(value: ina226::AlertFunction) -> Self {
4489        match value {
4490            ina226::AlertFunction::ShuntOverLimit => Self::ShuntOverLimit,
4491            ina226::AlertFunction::ShuntUnderLimit => Self::ShuntUnderLimit,
4492            ina226::AlertFunction::BusOverLimit => Self::BusOverLimit,
4493            ina226::AlertFunction::BusUnderLimit => Self::BusUnderLimit,
4494            ina226::AlertFunction::PowerOverLimit => Self::PowerOverLimit,
4495        }
4496    }
4497}
4498
4499impl From<ina226::DieId> for PamojaIna226DieId {
4500    fn from(value: ina226::DieId) -> Self {
4501        PamojaIna226DieId {
4502            device: value.device,
4503            revision: value.revision,
4504        }
4505    }
4506}
4507
4508/// Maps a code onto the SHT3x repeatability it names.
4509fn repeatability_from_code(code: u8) -> Option<sht3x::Repeatability> {
4510    match code {
4511        0 => Some(sht3x::Repeatability::Low),
4512        1 => Some(sht3x::Repeatability::Medium),
4513        2 => Some(sht3x::Repeatability::High),
4514        _ => None,
4515    }
4516}
4517
4518/// Records a rejected repeatability and reports it as an invalid argument.
4519fn bad_repeatability() -> PamojaStatus {
4520    set_last_error("SHT3x repeatability must be 0 low, 1 medium, or 2 high".to_owned());
4521    PamojaStatus::InvalidArgument
4522}
4523
4524/// Maps a code onto the SHT3x periodic-mode rate it names.
4525fn rate_from_code(code: u8) -> Option<sht3x::Rate> {
4526    match code {
4527        0 => Some(sht3x::Rate::HalfMps),
4528        1 => Some(sht3x::Rate::OneMps),
4529        2 => Some(sht3x::Rate::TwoMps),
4530        3 => Some(sht3x::Rate::FourMps),
4531        4 => Some(sht3x::Rate::TenMps),
4532        _ => None,
4533    }
4534}
4535
4536/// Records a rejected rate and reports it as an invalid argument.
4537fn bad_rate() -> PamojaStatus {
4538    set_last_error("SHT3x rate must be 0 for 0.5, 1, 2, 3, or 4 for 10 per second".to_owned());
4539    PamojaStatus::InvalidArgument
4540}
4541
4542/// Maps a bit count onto the HDC1080 temperature resolution it names.
4543fn temperature_resolution(bits: u8) -> Option<hdc1080::TemperatureResolution> {
4544    match bits {
4545        14 => Some(hdc1080::TemperatureResolution::Bits14),
4546        11 => Some(hdc1080::TemperatureResolution::Bits11),
4547        _ => None,
4548    }
4549}
4550
4551/// Records a rejected temperature resolution and reports it as an invalid argument.
4552fn bad_temperature_resolution() -> PamojaStatus {
4553    set_last_error("HDC1080 temperature resolution must be 14 or 11 bits".to_owned());
4554    PamojaStatus::InvalidArgument
4555}
4556
4557/// Maps a bit count onto the HDC1080 humidity resolution it names.
4558fn humidity_resolution(bits: u8) -> Option<hdc1080::HumidityResolution> {
4559    match bits {
4560        14 => Some(hdc1080::HumidityResolution::Bits14),
4561        11 => Some(hdc1080::HumidityResolution::Bits11),
4562        8 => Some(hdc1080::HumidityResolution::Bits8),
4563        _ => None,
4564    }
4565}
4566
4567/// Records a rejected humidity resolution and reports it as an invalid argument.
4568fn bad_humidity_resolution() -> PamojaStatus {
4569    set_last_error("HDC1080 humidity resolution must be 14, 11, or 8 bits".to_owned());
4570    PamojaStatus::InvalidArgument
4571}
4572
4573/// Maps the long-conversion flag onto the OPT3001 conversion time it names.
4574fn conversion_time(long_conversion: bool) -> opt3001::ConversionTime {
4575    if long_conversion {
4576        opt3001::ConversionTime::Ms800
4577    } else {
4578        opt3001::ConversionTime::Ms100
4579    }
4580}
4581
4582/// Maps an averaging code onto the INA226 setting it names.
4583fn averaging(code: u8) -> ina226::Averaging {
4584    ina226::Configuration::from_register(u16::from(code & 0x07) << 9).averaging
4585}
4586
4587/// Maps a conversion-time code onto the INA226 setting it names.
4588fn conversion_time_setting(code: u8) -> ina226::ConversionTime {
4589    ina226::Configuration::from_register(u16::from(code & 0x07) << 6).bus_conversion_time
4590}
4591
4592/// Maps a mode code onto the INA226 setting it names.
4593fn mode(code: u8) -> ina226::Mode {
4594    ina226::Configuration::from_register(u16::from(code & 0x07)).mode
4595}
4596
4597/// Maps a pin code onto the level an INA226 address pin is tied to.
4598fn address_pin(code: u8) -> Option<ina226::AddressPin> {
4599    match code {
4600        0 => Some(ina226::AddressPin::Ground),
4601        1 => Some(ina226::AddressPin::Supply),
4602        2 => Some(ina226::AddressPin::Sda),
4603        3 => Some(ina226::AddressPin::Scl),
4604        _ => None,
4605    }
4606}
4607
4608/// Maps a bit count onto the resolution it names.
4609fn resolution(bits: u8) -> Option<ds18b20::Resolution> {
4610    match bits {
4611        9 => Some(ds18b20::Resolution::Bits9),
4612        10 => Some(ds18b20::Resolution::Bits10),
4613        11 => Some(ds18b20::Resolution::Bits11),
4614        12 => Some(ds18b20::Resolution::Bits12),
4615        _ => None,
4616    }
4617}
4618
4619/// Records a rejected resolution and reports it as an invalid argument.
4620fn bad_resolution() -> PamojaStatus {
4621    set_last_error("DS18B20 resolution must be 9, 10, 11, or 12 bits".to_owned());
4622    PamojaStatus::InvalidArgument
4623}
4624
4625/// Records a buffer of the wrong size and reports it as an invalid argument.
4626fn wrong_length(what: &str, expected: usize) -> PamojaStatus {
4627    set_last_error(format!("{what} must be exactly {expected} bytes"));
4628    PamojaStatus::InvalidArgument
4629}
4630
4631/// Records a sensor error and maps it onto its status.
4632fn failed(error: SensorError) -> PamojaStatus {
4633    set_last_error(error.to_string());
4634    PamojaStatus::Codec
4635}
4636
4637#[cfg(test)]
4638mod tests {
4639    use super::*;
4640    use std::ptr;
4641
4642    #[test]
4643    fn a_bme280_burst_read_compensates_to_a_reading() {
4644        // The calibration and measurement bytes from the crate's own datasheet case.
4645        let temp_press = [0u8; PAMOJA_BME280_CALIBRATION_TEMP_PRESS_LEN];
4646        let humidity = [0u8; PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN];
4647        let measurement = [0u8; PAMOJA_BME280_MEASUREMENT_LEN];
4648        let mut calibration = ptr::null_mut();
4649        let mut reading = PamojaBme280Measurement {
4650            celsius: 0.0,
4651            pascals: 0,
4652            hectopascals: 0.0,
4653            relative_humidity_percent: 0.0,
4654        };
4655
4656        // Safety: the inputs are valid slices and the out-pointers are writable.
4657        unsafe {
4658            assert_eq!(
4659                pamoja_bme280_calibration_new(
4660                    temp_press.as_ptr(),
4661                    temp_press.len(),
4662                    humidity.as_ptr(),
4663                    humidity.len(),
4664                    &mut calibration
4665                ),
4666                PamojaStatus::Ok
4667            );
4668            assert_eq!(
4669                pamoja_bme280_compensate(
4670                    calibration,
4671                    measurement.as_ptr(),
4672                    measurement.len(),
4673                    &mut reading
4674                ),
4675                PamojaStatus::Ok
4676            );
4677            pamoja_bme280_calibration_free(calibration);
4678        }
4679        assert!(reading.hectopascals.is_finite());
4680    }
4681
4682    #[test]
4683    fn a_calibration_of_the_wrong_length_is_refused() {
4684        let short = [0u8; 4];
4685        let humidity = [0u8; PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN];
4686        let mut calibration = ptr::null_mut();
4687        // Safety: the inputs are valid slices and the out-pointer is writable.
4688        let status = unsafe {
4689            pamoja_bme280_calibration_new(
4690                short.as_ptr(),
4691                short.len(),
4692                humidity.as_ptr(),
4693                humidity.len(),
4694                &mut calibration,
4695            )
4696        };
4697        assert_eq!(status, PamojaStatus::InvalidArgument);
4698        assert!(calibration.is_null());
4699    }
4700
4701    #[test]
4702    fn a_scratchpad_decodes_and_its_crc_is_checked() {
4703        // 25.0625 C at 12-bit resolution, with the CRC the device would send.
4704        let mut bytes = [0x91u8, 0x01, 0x4B, 0x46, 0x7F, 0xFF, 0x0C, 0x10, 0x00];
4705        // Safety: the input is a valid slice.
4706        bytes[8] = unsafe { pamoja_ds18b20_crc8(bytes.as_ptr(), 8) };
4707
4708        let mut reading = PamojaDs18b20Reading {
4709            raw_temperature: 0,
4710            micro_celsius: 0,
4711            alarm_high: 0,
4712            alarm_low: 0,
4713            resolution_bits: 0,
4714        };
4715        // Safety: the input is a valid slice and the out-pointer is writable.
4716        unsafe {
4717            assert_eq!(
4718                pamoja_ds18b20_parse_scratchpad(bytes.as_ptr(), bytes.len(), &mut reading),
4719                PamojaStatus::Ok
4720            );
4721        }
4722        assert_eq!(reading.raw_temperature, 0x0191);
4723        assert_eq!(reading.micro_celsius, 25_062_500);
4724        assert_eq!(reading.resolution_bits, 12);
4725
4726        bytes[0] ^= 0xFF;
4727        // Safety: the input is a valid slice and the out-pointer is writable.
4728        let status =
4729            unsafe { pamoja_ds18b20_parse_scratchpad(bytes.as_ptr(), bytes.len(), &mut reading) };
4730        assert_eq!(
4731            status,
4732            PamojaStatus::Codec,
4733            "a read corrupted on the bus must not be trusted"
4734        );
4735    }
4736
4737    #[test]
4738    fn a_resolution_outside_the_datasheet_is_refused() {
4739        let mut byte = 0u8;
4740        // Safety: the out-pointer is writable.
4741        let status = unsafe { pamoja_ds18b20_config_byte(8, &mut byte) };
4742        assert_eq!(status, PamojaStatus::InvalidArgument);
4743    }
4744
4745    #[test]
4746    fn the_resolution_round_trips_through_its_config_byte() {
4747        for bits in [9u8, 10, 11, 12] {
4748            let mut byte = 0u8;
4749            // Safety: the out-pointer is writable.
4750            unsafe {
4751                assert_eq!(
4752                    pamoja_ds18b20_config_byte(bits, &mut byte),
4753                    PamojaStatus::Ok
4754                );
4755            }
4756            assert_eq!(pamoja_ds18b20_resolution_bits(byte), bits);
4757        }
4758    }
4759
4760    #[test]
4761    fn an_ina219_reading_converts_at_its_calibrated_resolution() {
4762        // The datasheet's worked design example: 15 A across a 2 milliohm shunt at
4763        // 1 mA per count.
4764        const CURRENT_LSB: u32 = 1_000;
4765        assert_eq!(pamoja_ina219_calibration(CURRENT_LSB, 2), 0x5000);
4766        assert_eq!(
4767            pamoja_ina219_minimum_current_lsb_microamps(15_000_000),
4768            458,
4769            "15 A over a 15-bit register, rounded up to the next whole microamp"
4770        );
4771        assert_eq!(
4772            pamoja_ina219_current_microamps(1_000, CURRENT_LSB),
4773            1_000_000
4774        );
4775        // The power LSB is fixed at twenty times the current LSB.
4776        assert_eq!(pamoja_ina219_power_microwatts(100, CURRENT_LSB), 2_000_000);
4777        assert!(pamoja_ina219_conversion_ready(0x0002));
4778        assert!(pamoja_ina219_math_overflow(0x0001));
4779    }
4780
4781    #[test]
4782    fn an_ads1115_config_round_trips_through_its_register() {
4783        let config = PamojaAds1115Config {
4784            start_conversion: 1,
4785            mux: 4,
4786            pga: 1,
4787            single_shot: 1,
4788            data_rate: 4,
4789            window_comparator: 0,
4790            comparator_active_high: 0,
4791            comparator_latching: 0,
4792            comparator_queue: 3,
4793        };
4794        let bits = pamoja_ads1115_config_bits(config);
4795        let mut back = config;
4796        // Safety: the out-pointer is writable.
4797        unsafe {
4798            assert_eq!(
4799                pamoja_ads1115_config_from_bits(bits, &mut back),
4800                PamojaStatus::Ok
4801            );
4802        }
4803        assert_eq!(back, config);
4804    }
4805
4806    #[test]
4807    fn an_ads1115_conversion_scales_to_its_full_range() {
4808        // Gain code 1 is the plus or minus 4.096 V range.
4809        assert_eq!(pamoja_ads1115_full_scale_microvolts(1), 4_096_000);
4810        assert_eq!(pamoja_ads1115_to_nanovolts(1, 32_767), 4_095_875_000);
4811        assert!((pamoja_ads1115_to_volts(1, 0)).abs() < f32::EPSILON);
4812    }
4813
4814    #[test]
4815    fn a_bmp280_burst_read_compensates_and_its_coefficients_round_trip() {
4816        // The calibration block and burst read from the crate's own datasheet case.
4817        let calibration_bytes = [
4818            0x70, 0x6B, 0x43, 0x67, 0x18, 0xFC, 0x7D, 0x8E, 0x43, 0xD6, 0xD0, 0x0B, 0x27, 0x0B,
4819            0x8C, 0x00, 0xF9, 0xFF, 0x8C, 0x3C, 0xF8, 0xC6, 0x70, 0x17,
4820        ];
4821        let measurement = [0x65u8, 0x5A, 0xC0, 0x7E, 0xED, 0x00];
4822        let mut calibration = ptr::null_mut();
4823        let mut reading = PamojaBmp280Reading {
4824            celsius: 0.0,
4825            pascals: 0,
4826            hectopascals: 0.0,
4827        };
4828        let mut coefficients = [0u8; PAMOJA_BMP280_CALIBRATION_LEN];
4829
4830        // Safety: the inputs are valid slices and the out-pointers are writable.
4831        unsafe {
4832            assert_eq!(
4833                pamoja_bmp280_calibration_new(
4834                    calibration_bytes.as_ptr(),
4835                    calibration_bytes.len(),
4836                    &mut calibration
4837                ),
4838                PamojaStatus::Ok
4839            );
4840            assert_eq!(
4841                pamoja_bmp280_compensate(
4842                    calibration,
4843                    measurement.as_ptr(),
4844                    measurement.len(),
4845                    &mut reading
4846                ),
4847                PamojaStatus::Ok
4848            );
4849            assert_eq!(
4850                pamoja_bmp280_calibration_to_bytes(calibration, coefficients.as_mut_ptr()),
4851                PamojaStatus::Ok
4852            );
4853            pamoja_bmp280_calibration_free(calibration);
4854        }
4855        assert_eq!(reading.pascals, 100_653);
4856        assert!((reading.celsius - 25.08).abs() < 1e-2);
4857        assert_eq!(
4858            coefficients, calibration_bytes,
4859            "the coefficients round-trip through their registers"
4860        );
4861    }
4862
4863    #[test]
4864    fn a_bmp280_control_register_round_trips() {
4865        let ctrl = PamojaBmp280CtrlMeas {
4866            temperature: 2,
4867            pressure: 5,
4868            mode: 3,
4869        };
4870        let bits = pamoja_bmp280_ctrl_meas_bits(ctrl);
4871        let mut back = ctrl;
4872        // Safety: the out-pointer is writable.
4873        unsafe {
4874            assert_eq!(
4875                pamoja_bmp280_ctrl_meas_from_bits(bits, &mut back),
4876                PamojaStatus::Ok
4877            );
4878        }
4879        assert_eq!(back, ctrl);
4880        assert_eq!(pamoja_bmp280_oversampling_factor(5), 16);
4881        assert!(pamoja_bmp280_measuring(0x08));
4882    }
4883
4884    #[test]
4885    fn an_sht3x_frame_decodes_and_its_crc_is_checked() {
4886        // Two fifths of full scale is 25 C, three fifths is 60 percent.
4887        let frame = [0x66u8, 0x66, 0x93, 0x99, 0x99, 0xBE];
4888        let mut measurement = PamojaSht3xMeasurement {
4889            temperature_raw: 0,
4890            humidity_raw: 0,
4891            milli_celsius: 0,
4892            celsius: 0.0,
4893            milli_fahrenheit: 0,
4894            fahrenheit: 0.0,
4895            milli_percent: 0,
4896            relative_humidity: 0.0,
4897        };
4898        // Safety: the input is a valid slice and the out-pointer is writable.
4899        unsafe {
4900            assert_eq!(
4901                pamoja_sht3x_parse_measurement(frame.as_ptr(), frame.len(), &mut measurement),
4902                PamojaStatus::Ok
4903            );
4904        }
4905        assert_eq!(measurement.milli_celsius, 25_000);
4906        assert_eq!(measurement.milli_fahrenheit, 77_000);
4907        assert_eq!(measurement.milli_percent, 60_000);
4908
4909        let check = [0xBEu8, 0xEF];
4910        // Safety: the input is a valid slice.
4911        assert_eq!(
4912            unsafe { pamoja_sht3x_crc(check.as_ptr(), check.len()) },
4913            0x92,
4914            "Sensirion's published check value"
4915        );
4916
4917        let corrupt = [0x66u8, 0x67, 0x93, 0x99, 0x99, 0xBE];
4918        // Safety: the input is a valid slice and the out-pointer is writable.
4919        let status = unsafe {
4920            pamoja_sht3x_parse_measurement(corrupt.as_ptr(), corrupt.len(), &mut measurement)
4921        };
4922        assert_eq!(
4923            status,
4924            PamojaStatus::Codec,
4925            "a read corrupted on the bus must not be trusted"
4926        );
4927    }
4928
4929    #[test]
4930    fn an_sht3x_status_and_command_follow_the_tables() {
4931        let frame = [0x80u8, 0x10, 0xE1];
4932        let mut status = PamojaSht3xStatus {
4933            bits: 0,
4934            alert_pending: 0,
4935            heater_on: 0,
4936            humidity_tracking_alert: 0,
4937            temperature_tracking_alert: 0,
4938            reset_detected: 0,
4939            command_failed: 0,
4940            write_checksum_failed: 0,
4941        };
4942        let mut command = 0u16;
4943        // Safety: the input is a valid slice and the out-pointers are writable.
4944        unsafe {
4945            assert_eq!(
4946                pamoja_sht3x_parse_status(frame.as_ptr(), frame.len(), &mut status),
4947                PamojaStatus::Ok
4948            );
4949            assert_eq!(
4950                pamoja_sht3x_single_shot(2, true, &mut command),
4951                PamojaStatus::Ok
4952            );
4953        }
4954        assert_eq!(status.bits, PAMOJA_SHT3X_STATUS_DEFAULT);
4955        assert_eq!(status.alert_pending, 1);
4956        assert_eq!(status.reset_detected, 1);
4957        assert_eq!(status.heater_on, 0);
4958        assert_eq!(command, PAMOJA_SHT3X_COMMAND_SINGLE_SHOT_HIGH_STRETCH);
4959
4960        // Safety: the out-pointer is writable.
4961        let status = unsafe { pamoja_sht3x_single_shot(3, true, &mut command) };
4962        assert_eq!(status, PamojaStatus::InvalidArgument);
4963    }
4964
4965    #[test]
4966    fn an_scd4x_frame_decodes_and_its_crc_is_checked() {
4967        let frame = [0x01u8, 0xF4, 0x33, 0x66, 0x67, 0xA2, 0x5E, 0xB9, 0x3C];
4968        let mut measurement = PamojaScd4xMeasurement {
4969            co2_ppm: 0,
4970            temperature_raw: 0,
4971            humidity_raw: 0,
4972            milli_celsius: 0,
4973            celsius: 0.0,
4974            humidity_milli_percent: 0,
4975            relative_humidity_percent: 0.0,
4976        };
4977        // Safety: the input is a valid slice and the out-pointer is writable.
4978        unsafe {
4979            assert_eq!(
4980                pamoja_scd4x_parse_measurement(frame.as_ptr(), frame.len(), &mut measurement),
4981                PamojaStatus::Ok
4982            );
4983        }
4984        assert_eq!(measurement.co2_ppm, 500);
4985        assert_eq!(measurement.milli_celsius, 25_003);
4986        assert_eq!(measurement.humidity_milli_percent, 37_002);
4987
4988        let corrupt = [0x01u8, 0xF4, 0xCC, 0x66, 0x67, 0xA2, 0x5E, 0xB9, 0x3C];
4989        // Safety: the input is a valid slice and the out-pointer is writable.
4990        let status = unsafe {
4991            pamoja_scd4x_parse_measurement(corrupt.as_ptr(), corrupt.len(), &mut measurement)
4992        };
4993        assert_eq!(status, PamojaStatus::Codec);
4994
4995        // The offset scales by 2^16, not by the 2^16 - 1 the measurement words use.
4996        assert_eq!(pamoja_scd4x_temperature_offset_word(5_400), 0x07E6);
4997
4998        let serial_frame = [0xF8u8, 0x96, 0x31, 0x9F, 0x07, 0xC2, 0x3B, 0xBE, 0x89];
4999        let mut serial = 0u64;
5000        // Safety: the input is a valid slice and the out-pointer is writable.
5001        unsafe {
5002            assert_eq!(
5003                pamoja_scd4x_serial_number(serial_frame.as_ptr(), serial_frame.len(), &mut serial),
5004                PamojaStatus::Ok
5005            );
5006        }
5007        assert_eq!(serial, 273_325_796_834_238);
5008    }
5009
5010    #[test]
5011    fn a_tmp117_register_decodes_to_its_datasheet_row() {
5012        assert_eq!(pamoja_tmp117_micro_celsius(0x0C80), 25_000_000);
5013        assert_eq!(pamoja_tmp117_nano_celsius(-1), -7_812_500);
5014        assert_eq!(pamoja_tmp117_raw_from_micro_celsius(-25_000_000), -3_200);
5015        assert_eq!(pamoja_tmp117_device_id(0x1117), 0x117);
5016        assert_eq!(pamoja_tmp117_revision(0x1117), 1);
5017        assert!(pamoja_tmp117_high_alert(0xA220));
5018        assert!(!pamoja_tmp117_low_alert(0xA220));
5019        assert!(pamoja_tmp117_data_ready(0xA220));
5020
5021        let mut config = PamojaTmp117Config {
5022            high_alert: 0,
5023            low_alert: 0,
5024            data_ready: 0,
5025            eeprom_busy: 0,
5026            mode: 0,
5027            cycle: 0,
5028            averaging: 0,
5029            therm_mode: 0,
5030            alert_active_high: 0,
5031            alert_pin_data_ready: 0,
5032            soft_reset: 0,
5033        };
5034        // Safety: the out-pointer is writable.
5035        unsafe {
5036            assert_eq!(
5037                pamoja_tmp117_config_from_bits(PAMOJA_TMP117_CONFIG_RESET, &mut config),
5038                PamojaStatus::Ok
5039            );
5040        }
5041        assert_eq!(
5042            pamoja_tmp117_config_bits(config),
5043            PAMOJA_TMP117_CONFIG_RESET
5044        );
5045        assert_eq!(pamoja_tmp117_cycle_micros(5, 0), 4_000_000);
5046    }
5047
5048    #[test]
5049    fn an_hdc1080_measurement_decodes_and_an_undefined_code_is_refused() {
5050        let bytes = [0x60u8, 0x00, 0x40, 0x00];
5051        let mut measurement = PamojaHdc1080Measurement {
5052            temperature_raw: 0,
5053            humidity_raw: 0,
5054            milli_celsius: 0,
5055            celsius: 0.0,
5056            milli_percent: 0,
5057            relative_humidity: 0.0,
5058        };
5059        let mut config = PamojaHdc1080Config {
5060            software_reset: 0,
5061            heater: 0,
5062            sequential: 0,
5063            battery_low: 0,
5064            temperature_resolution_bits: 0,
5065            humidity_resolution_bits: 0,
5066        };
5067        let mut register = 0u16;
5068        // Safety: the input is a valid slice and the out-pointers are writable.
5069        unsafe {
5070            assert_eq!(
5071                pamoja_hdc1080_parse_measurement(bytes.as_ptr(), bytes.len(), &mut measurement),
5072                PamojaStatus::Ok
5073            );
5074            assert_eq!(
5075                pamoja_hdc1080_config_from_register(
5076                    PAMOJA_HDC1080_CONFIGURATION_RESET,
5077                    &mut config
5078                ),
5079                PamojaStatus::Ok
5080            );
5081            assert_eq!(
5082                pamoja_hdc1080_config_to_register(config, &mut register),
5083                PamojaStatus::Ok
5084            );
5085        }
5086        assert_eq!(measurement.milli_celsius, 21_875);
5087        assert_eq!(measurement.milli_percent, 25_000);
5088        assert_eq!(register, PAMOJA_HDC1080_CONFIGURATION_RESET);
5089
5090        // Safety: the out-pointer is writable.
5091        let status = unsafe { pamoja_hdc1080_config_from_register(0x1300, &mut config) };
5092        assert_eq!(
5093            status,
5094            PamojaStatus::Codec,
5095            "the humidity-resolution code the datasheet leaves undefined is refused"
5096        );
5097    }
5098
5099    #[test]
5100    fn an_opt3001_result_decodes_and_a_reserved_range_has_no_full_scale() {
5101        assert_eq!(pamoja_opt3001_milli_lux(0xBFFF), 83_865_600);
5102        assert_eq!(pamoja_opt3001_raw_from_milli_lux(88_800), 0x28AC);
5103
5104        let mut full_scale = 0u32;
5105        // Safety: the out-pointer is writable.
5106        unsafe {
5107            assert!(pamoja_opt3001_full_scale_milli_lux(0, &mut full_scale));
5108            assert_eq!(full_scale, 40_950);
5109            assert!(
5110                !pamoja_opt3001_full_scale_milli_lux(
5111                    PAMOJA_OPT3001_RANGE_AUTOMATIC,
5112                    &mut full_scale
5113                ),
5114                "a reserved range number has no full scale"
5115            );
5116        }
5117
5118        let mut config = PamojaOpt3001Config {
5119            range_number: 0,
5120            long_conversion: 0,
5121            mode: 0,
5122            overflow: 0,
5123            conversion_ready: 0,
5124            flag_high: 0,
5125            flag_low: 0,
5126            latched_window: 0,
5127            active_high: 0,
5128            mask_exponent: 0,
5129            fault_count: 0,
5130        };
5131        // Safety: the out-pointer is writable.
5132        unsafe {
5133            assert_eq!(
5134                pamoja_opt3001_config_from_bits(PAMOJA_OPT3001_CONFIGURATION_RESET, &mut config),
5135                PamojaStatus::Ok
5136            );
5137        }
5138        assert_eq!(
5139            pamoja_opt3001_config_bits(config),
5140            PAMOJA_OPT3001_CONFIGURATION_RESET
5141        );
5142    }
5143
5144    #[test]
5145    fn an_ina226_converts_at_its_calibrated_resolution_and_checks_its_die() {
5146        // The datasheet's worked design example: 15 A across a 2 milliohm shunt at
5147        // 1 mA per count.
5148        const CURRENT_LSB: u32 = 1_000;
5149        assert_eq!(pamoja_ina226_calibration(CURRENT_LSB, 2), 2_560);
5150        assert_eq!(pamoja_ina226_minimum_current_lsb_microamps(15_000_000), 458);
5151        assert_eq!(pamoja_ina226_shunt_nanovolts(8_000), 20_000_000);
5152        assert_eq!(pamoja_ina226_bus_microvolts(9_584), 11_980_000);
5153        assert_eq!(
5154            pamoja_ina226_current_microamps(10_000, CURRENT_LSB),
5155            10_000_000
5156        );
5157        // The power LSB is fixed at twenty-five times the current LSB.
5158        assert_eq!(
5159            pamoja_ina226_power_microwatts(4_792, CURRENT_LSB),
5160            119_800_000
5161        );
5162
5163        let mut die = PamojaIna226DieId {
5164            device: 0,
5165            revision: 0,
5166        };
5167        // Safety: the out-pointer is writable.
5168        unsafe {
5169            assert_eq!(
5170                pamoja_ina226_identify(PAMOJA_INA226_MANUFACTURER_ID, 0x2260, &mut die),
5171                PamojaStatus::Ok
5172            );
5173            assert_eq!(die.device, PAMOJA_INA226_DEVICE_ID);
5174            assert_eq!(
5175                pamoja_ina226_identify(PAMOJA_INA226_MANUFACTURER_ID, 0x2270, &mut die),
5176                PamojaStatus::Codec,
5177                "a die that is not an INA226 is refused"
5178            );
5179        }
5180    }
5181}