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