Skip to main content

pamoja_ffi/
sensors_driver.rs

1//! The C ABI for the sensor drivers that run over an I2C bus.
2//!
3//! A driver runs the datasheet's whole conversation with a part over a [`PamojaI2cBus`]:
4//! reset, identify, read the calibration, configure, measure. It holds a share of the bus, so
5//! the caller may free its own bus handle straight after building the driver, or keep it to
6//! look at the bus between reads. A driver waits as its datasheet asks through the bus's
7//! delay, which sleeps only when real parts are on the other end.
8//!
9//! Every part has a simulated twin that answers a driver with nothing plugged in: a
10//! `_sim_part` that reads the datasheet's typical values and a `_sim_reporting` that reads
11//! whatever it is asked to. Either goes on a simulated bus with
12//! [`pamoja_i2c_bus_attach`](crate::hal::pamoja_i2c_bus_attach).
13//!
14//! A driver's failure is [`PamojaStatus::Io`] when the bus failed or the part never finished,
15//! and [`PamojaStatus::Codec`] when a part answered but is not the one the driver expected or
16//! its checksum failed, with the reason in the last error message either way.
17//!
18//! The DS18B20 is the exception: on a Linux board it is not driven over a bus but read through
19//! the files the kernel's 1-Wire driver serves, which [`pamoja_ds18b20_thermometer_read`] does.
20
21use std::fmt::{Debug, Display};
22use std::os::raw::c_char;
23use std::panic::{catch_unwind, AssertUnwindSafe};
24use std::path::Path;
25
26use pamoja_hal::bus::{BusDelay, I2cBus};
27use pamoja_sensors::driver::I2cRegisters;
28use pamoja_sensors::{
29    ads1115, bme280, bmp280, ds18b20, hdc1080, ina219, ina226, opt3001, scd4x, sht3x, tmp117,
30    DriverError,
31};
32
33use crate::hal::{PamojaI2cBus, PamojaI2cPart};
34use crate::sensors::{
35    conversion_time, humidity_resolution, repeatability_from_code, temperature_resolution,
36    PamojaAds1115Config, PamojaBme280Measurement, PamojaBmp280Coefficients, PamojaBmp280Reading,
37    PamojaDs18b20Reading, PamojaHdc1080Config, PamojaHdc1080Measurement, PamojaIna219Config,
38    PamojaIna226Config, PamojaIna226DieId, PamojaIna226MaskEnable, PamojaOpt3001Config,
39    PamojaScd4xMeasurement, PamojaSht3xMeasurement, PamojaSht3xStatus,
40    PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN, PAMOJA_BME280_CALIBRATION_TEMP_PRESS_LEN,
41    PAMOJA_BME280_MEASUREMENT_LEN, PAMOJA_BMP280_CALIBRATION_LEN, PAMOJA_BMP280_DATA_LEN,
42};
43use crate::{read_str, set_last_error, PamojaStatus, PamojaString};
44
45type Bme280Driver = bme280::Bme280<I2cRegisters<I2cBus>, BusDelay>;
46type Bmp280Driver = bmp280::Bmp280<I2cRegisters<I2cBus>, BusDelay>;
47type Tmp117Driver = tmp117::Tmp117<I2cBus, BusDelay>;
48type Opt3001Driver = opt3001::Opt3001<I2cBus, BusDelay>;
49type Hdc1080Driver = hdc1080::Hdc1080<I2cBus, BusDelay>;
50type Ina219Driver = ina219::Ina219<I2cBus, BusDelay>;
51type Ina226Driver = ina226::Ina226<I2cBus, BusDelay>;
52type Ads1115Driver = ads1115::Ads1115<I2cBus, BusDelay>;
53type Sht3xDriver = sht3x::Sht3x<I2cBus, BusDelay>;
54type Scd4xDriver = scd4x::Scd4x<I2cBus, BusDelay>;
55
56/// The status a simulated BME280 reports when it is neither measuring nor loading its
57/// calibration.
58pub const PAMOJA_BME280_SIM_STATUS_IDLE: u8 = 0x00;
59
60/// The status a simulated BMP280 reports when it is neither measuring nor loading its
61/// calibration.
62pub const PAMOJA_BMP280_SIM_STATUS_IDLE: u8 = 0x00;
63
64/// The temperature a simulated TMP117 reports unless it is asked for another.
65pub const PAMOJA_TMP117_SIM_CELSIUS: f32 = 21.25;
66
67/// The illuminance a simulated OPT3001 reports unless it is asked for another.
68pub const PAMOJA_OPT3001_SIM_LUX: f32 = 380.0;
69
70/// The temperature a simulated HDC1080 reports unless it is asked for another.
71pub const PAMOJA_HDC1080_SIM_CELSIUS: f32 = 22.5;
72
73/// The relative humidity a simulated HDC1080 reports unless it is asked for another.
74pub const PAMOJA_HDC1080_SIM_RELATIVE_HUMIDITY: f32 = 45.0;
75
76/// The shunt a simulated INA219 sits across, in milliohms: the common breakout's.
77pub const PAMOJA_INA219_SIM_SHUNT_MILLIOHMS: u32 = 100;
78
79/// The largest current a simulated INA219 is sized for, in microamps.
80pub const PAMOJA_INA219_SIM_MAX_MICROAMPS: u32 = 3_200_000;
81
82/// The bus voltage a simulated INA219 reports unless it is asked for another, in millivolts.
83pub const PAMOJA_INA219_SIM_BUS_MILLIVOLTS: u32 = 12_000;
84
85/// The current a simulated INA219 reports unless it is asked for another, in microamps.
86pub const PAMOJA_INA219_SIM_MICROAMPS: i32 = 500_000;
87
88/// The shunt a simulated INA226 sits across, in milliohms.
89pub const PAMOJA_INA226_SIM_SHUNT_MILLIOHMS: u32 = 100;
90
91/// The largest current a simulated INA226 is sized for, in microamps.
92pub const PAMOJA_INA226_SIM_MAX_MICROAMPS: u32 = 3_200_000;
93
94/// The bus voltage a simulated INA226 reports unless it is asked for another, in microvolts.
95pub const PAMOJA_INA226_SIM_BUS_MICROVOLTS: u32 = 12_000_000;
96
97/// The current a simulated INA226 reports unless it is asked for another, in microamps.
98pub const PAMOJA_INA226_SIM_MICROAMPS: i32 = 500_000;
99
100/// The voltage a simulated ADS1115 reports unless it is asked for another: half a 3.3 V
101/// supply.
102pub const PAMOJA_ADS1115_SIM_VOLTS: f32 = 1.65;
103
104/// The temperature a simulated SHT3x reports unless it is asked for another.
105pub const PAMOJA_SHT3X_SIM_CELSIUS: f32 = 22.5;
106
107/// The relative humidity a simulated SHT3x reports unless it is asked for another.
108pub const PAMOJA_SHT3X_SIM_RELATIVE_HUMIDITY: f32 = 45.0;
109
110/// The carbon dioxide a simulated SCD4x reports unless it is asked for another, in parts per
111/// million.
112pub const PAMOJA_SCD4X_SIM_CO2_PPM: u16 = 800;
113
114/// The temperature a simulated SCD4x reports unless it is asked for another.
115pub const PAMOJA_SCD4X_SIM_CELSIUS: f32 = 22.5;
116
117/// The relative humidity a simulated SCD4x reports unless it is asked for another.
118pub const PAMOJA_SCD4X_SIM_RELATIVE_HUMIDITY: f32 = 45.0;
119
120/// The serial number every simulated SCD4x reports.
121pub const PAMOJA_SCD4X_SIM_SERIAL: u64 = 0x0000_5A4D_0C1E_2B3F;
122
123const _: () = assert!(PAMOJA_BME280_SIM_STATUS_IDLE == bme280::sim::STATUS_IDLE);
124const _: () = assert!(PAMOJA_BMP280_SIM_STATUS_IDLE == bmp280::sim::STATUS_IDLE);
125const _: () = assert!(PAMOJA_TMP117_SIM_CELSIUS == tmp117::sim::CELSIUS);
126const _: () = assert!(PAMOJA_OPT3001_SIM_LUX == opt3001::sim::LUX);
127const _: () = assert!(PAMOJA_HDC1080_SIM_CELSIUS == hdc1080::sim::CELSIUS);
128const _: () = assert!(PAMOJA_HDC1080_SIM_RELATIVE_HUMIDITY == hdc1080::sim::RELATIVE_HUMIDITY);
129const _: () = assert!(PAMOJA_INA219_SIM_SHUNT_MILLIOHMS == ina219::sim::SHUNT_MILLIOHMS);
130const _: () = assert!(PAMOJA_INA219_SIM_MAX_MICROAMPS == ina219::sim::MAX_MICROAMPS);
131const _: () = assert!(PAMOJA_INA219_SIM_BUS_MILLIVOLTS == ina219::sim::BUS_MILLIVOLTS);
132const _: () = assert!(PAMOJA_INA219_SIM_MICROAMPS == ina219::sim::MICROAMPS);
133const _: () = assert!(PAMOJA_INA226_SIM_SHUNT_MILLIOHMS == ina226::sim::SHUNT_MILLIOHMS);
134const _: () = assert!(PAMOJA_INA226_SIM_MAX_MICROAMPS == ina226::sim::MAX_MICROAMPS);
135const _: () = assert!(PAMOJA_INA226_SIM_BUS_MICROVOLTS == ina226::sim::BUS_MICROVOLTS);
136const _: () = assert!(PAMOJA_INA226_SIM_MICROAMPS == ina226::sim::MICROAMPS);
137const _: () = assert!(PAMOJA_ADS1115_SIM_VOLTS == ads1115::sim::VOLTS);
138const _: () = assert!(PAMOJA_SHT3X_SIM_CELSIUS == sht3x::sim::CELSIUS);
139const _: () = assert!(PAMOJA_SHT3X_SIM_RELATIVE_HUMIDITY == sht3x::sim::RELATIVE_HUMIDITY);
140const _: () = assert!(PAMOJA_SCD4X_SIM_CO2_PPM == scd4x::sim::CO2_PPM);
141const _: () = assert!(PAMOJA_SCD4X_SIM_CELSIUS == scd4x::sim::CELSIUS);
142const _: () = assert!(PAMOJA_SCD4X_SIM_RELATIVE_HUMIDITY == scd4x::sim::RELATIVE_HUMIDITY);
143const _: () = assert!(PAMOJA_SCD4X_SIM_SERIAL == scd4x::sim::SERIAL);
144
145/// How a BME280 driver measures: the oversampling of each measurement and the IIR filter.
146#[repr(C)]
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct PamojaBme280Settings {
149    /// The temperature oversampling code, `0..=5`, where `0` skips the measurement.
150    pub temperature: u8,
151    /// The pressure oversampling code, `0..=5`, where `0` skips the measurement.
152    pub pressure: u8,
153    /// The humidity oversampling code, `0..=5`, where `0` skips the measurement.
154    pub humidity: u8,
155    /// The IIR filter code, `0..=4`, where `0` is off.
156    pub filter: u8,
157}
158
159/// How a BMP280 driver measures: the oversampling of each measurement and the IIR filter.
160#[repr(C)]
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub struct PamojaBmp280Settings {
163    /// The temperature oversampling code, `0..=5`, where `0` skips the measurement.
164    pub temperature: u8,
165    /// The pressure oversampling code, `0..=5`, where `0` skips the measurement.
166    pub pressure: u8,
167    /// The IIR filter's `filter[2:0]` code, `0` for the filter off, written as given.
168    pub filter: u8,
169}
170
171/// A TMP117 temperature result.
172#[repr(C)]
173#[derive(Clone, Copy, Debug, PartialEq)]
174pub struct PamojaTmp117Reading {
175    /// The temperature register, 7.8125 millidegrees Celsius per count.
176    pub raw: i16,
177    /// The temperature in micro-degrees Celsius, exact in integer arithmetic.
178    pub micro_celsius: i32,
179    /// The temperature in degrees Celsius.
180    pub celsius: f32,
181}
182
183/// The TMP117's alert flags: whether a result since they were last read crossed a limit.
184#[repr(C)]
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct PamojaTmp117Alerts {
187    /// `1` when a result was above the high limit.
188    pub high: u8,
189    /// `1` when a result was below the low limit.
190    pub low: u8,
191}
192
193/// How an OPT3001 driver measures.
194#[repr(C)]
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct PamojaOpt3001Settings {
197    /// `1` integrates each conversion for 800 ms, for resolution; `0` for 100 ms, for speed.
198    pub long_conversion: u8,
199    /// The full-scale range number, `0..=11`, or `12` to let the part choose.
200    pub range_number: u8,
201}
202
203/// An OPT3001 illuminance result.
204#[repr(C)]
205#[derive(Clone, Copy, Debug, PartialEq)]
206pub struct PamojaOpt3001Reading {
207    /// The result register: a four-bit exponent over a twelve-bit mantissa.
208    pub raw: u16,
209    /// The illuminance in millilux, exact in integer arithmetic.
210    pub milli_lux: u32,
211    /// The illuminance in lux.
212    pub lux: f32,
213}
214
215/// How an HDC1080 driver measures: the resolution of each channel.
216#[repr(C)]
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub struct PamojaHdc1080Settings {
219    /// The temperature resolution in bits: 14 or 11.
220    pub temperature_resolution_bits: u8,
221    /// The humidity resolution in bits: 14, 11, or 8.
222    pub humidity_resolution_bits: u8,
223}
224
225/// How an INA219 driver measures: the shunt, the current it is sized for, and the register
226/// settings.
227#[repr(C)]
228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
229pub struct PamojaIna219Settings {
230    /// The shunt resistance in milliohms.
231    pub shunt_milliohms: u32,
232    /// The largest current the shunt will carry, in microamps, which sets the finest current
233    /// step the calibration allows.
234    pub max_microamps: u32,
235    /// A current step in microamps per count to use instead, such as a round 100, or `0` to
236    /// take the finest step for `max_microamps`.
237    pub current_lsb_microamps: u32,
238    /// The range, gain, and converter settings; the mode is chosen per conversion.
239    pub config: PamojaIna219Config,
240}
241
242/// One INA219 conversion: the four result registers as read, and what they mean.
243#[repr(C)]
244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub struct PamojaIna219Reading {
246    /// The shunt-voltage register.
247    pub shunt: i16,
248    /// The bus-voltage register, flags included.
249    pub bus: u16,
250    /// The current register.
251    pub current: i16,
252    /// The power register.
253    pub power: u16,
254    /// The current step the calibration programmed, in microamps per count.
255    pub current_lsb_microamps: u32,
256    /// The shunt voltage in microvolts.
257    pub shunt_microvolts: i32,
258    /// The bus voltage in millivolts.
259    pub bus_millivolts: u32,
260    /// The current in microamps; negative flows the other way through the shunt.
261    pub current_microamps: i32,
262    /// The power in microwatts.
263    pub power_microwatts: u32,
264    /// `1` when the part's arithmetic overflowed and the current and power are meaningless.
265    pub math_overflow: u8,
266}
267
268/// How an INA226 driver measures: the shunt, the current it is sized for, and the register
269/// settings.
270#[repr(C)]
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct PamojaIna226Settings {
273    /// The shunt resistance in milliohms.
274    pub shunt_milliohms: u32,
275    /// The largest current the shunt will carry, in microamps, which sets the finest current
276    /// step the calibration allows.
277    pub max_microamps: u32,
278    /// A current step in microamps per count to use instead, or `0` to take the finest step
279    /// for `max_microamps`.
280    pub current_lsb_microamps: u32,
281    /// The averaging and conversion times; the mode is chosen per conversion.
282    pub config: PamojaIna226Config,
283}
284
285/// One INA226 conversion: the four result registers as read, and what they mean.
286#[repr(C)]
287#[derive(Clone, Copy, Debug, PartialEq)]
288pub struct PamojaIna226Reading {
289    /// The shunt-voltage register.
290    pub shunt: i16,
291    /// The bus-voltage register.
292    pub bus: u16,
293    /// The current register.
294    pub current: i16,
295    /// The power register.
296    pub power: u16,
297    /// The current step the calibration programmed, in microamps per count.
298    pub current_lsb_microamps: u32,
299    /// The shunt voltage in nanovolts.
300    pub shunt_nanovolts: i32,
301    /// The shunt voltage in millivolts.
302    pub shunt_millivolts: f32,
303    /// The bus voltage in microvolts.
304    pub bus_microvolts: u32,
305    /// The bus voltage in volts.
306    pub bus_volts: f32,
307    /// The current in microamps; negative flows the other way through the shunt.
308    pub current_microamps: i32,
309    /// The current in amps.
310    pub current_amps: f32,
311    /// The power in microwatts.
312    pub power_microwatts: u32,
313    /// The power in watts.
314    pub power_watts: f32,
315    /// `1` when the part's arithmetic overflowed and the current and power are meaningless.
316    pub math_overflow: u8,
317}
318
319/// How an ADS1115 driver converts: which input, at which range, how fast.
320#[repr(C)]
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct PamojaAds1115Settings {
323    /// The input multiplexer code, `0..=7`: `0..=3` a differential pair, `4..=7` one input
324    /// against ground.
325    pub mux: u8,
326    /// The gain code, `0..=7`, which sets the full-scale range.
327    pub pga: u8,
328    /// The data-rate code, `0..=7`, from 8 to 860 samples per second.
329    pub data_rate: u8,
330}
331
332/// One ADS1115 conversion.
333#[repr(C)]
334#[derive(Clone, Copy, Debug, PartialEq)]
335pub struct PamojaAds1115Sample {
336    /// The conversion register, two's complement.
337    pub raw: i16,
338    /// The gain code the conversion ran at.
339    pub pga: u8,
340    /// The voltage in nanovolts, exact in integer arithmetic.
341    pub nanovolts: i64,
342    /// The voltage in volts.
343    pub volts: f32,
344    /// `1` when the conversion sits at an end code, where the output clips for a signal past
345    /// the range, so the voltage is a bound rather than the reading.
346    pub clipped: u8,
347}
348
349/// A BME280 driven over an I2C bus. Opaque; release it with [`pamoja_bme280_free`].
350pub struct PamojaBme280 {
351    sensor: Bme280Driver,
352}
353
354/// A BMP280 driven over an I2C bus. Opaque; release it with [`pamoja_bmp280_free`].
355pub struct PamojaBmp280 {
356    sensor: Bmp280Driver,
357}
358
359/// A TMP117 driven over an I2C bus. Opaque; release it with [`pamoja_tmp117_free`].
360pub struct PamojaTmp117 {
361    sensor: Tmp117Driver,
362}
363
364/// An OPT3001 driven over an I2C bus. Opaque; release it with [`pamoja_opt3001_free`].
365pub struct PamojaOpt3001 {
366    sensor: Opt3001Driver,
367}
368
369/// An HDC1080 driven over an I2C bus. Opaque; release it with [`pamoja_hdc1080_free`].
370pub struct PamojaHdc1080 {
371    sensor: Hdc1080Driver,
372}
373
374/// An INA219 driven over an I2C bus. Opaque; release it with [`pamoja_ina219_free`].
375pub struct PamojaIna219 {
376    sensor: Ina219Driver,
377}
378
379/// An INA226 driven over an I2C bus. Opaque; release it with [`pamoja_ina226_free`].
380pub struct PamojaIna226 {
381    sensor: Ina226Driver,
382}
383
384/// An ADS1115 driven over an I2C bus. Opaque; release it with [`pamoja_ads1115_free`].
385pub struct PamojaAds1115 {
386    sensor: Ads1115Driver,
387}
388
389/// An SHT3x driven over an I2C bus. Opaque; release it with [`pamoja_sht3x_free`].
390pub struct PamojaSht3x {
391    sensor: Sht3xDriver,
392}
393
394/// An SCD40 or SCD41 driven over an I2C bus. Opaque; release it with [`pamoja_scd4x_free`].
395pub struct PamojaScd4x {
396    sensor: Scd4xDriver,
397}
398
399/// A DS18B20 the Linux kernel serves as a `w1_slave` file. Opaque; release it with
400/// [`pamoja_ds18b20_thermometer_free`].
401pub struct PamojaDs18b20Thermometer {
402    thermometer: ds18b20::linux::Thermometer,
403}
404
405/// The DS18B20s the kernel has found, in order of their directory names. Opaque; release it
406/// with [`pamoja_ds18b20_thermometers_free`].
407pub struct PamojaDs18b20Thermometers {
408    found: Vec<ds18b20::linux::Thermometer>,
409}
410
411/// Returns the settings a BME280 driver starts with: every measurement at oversampling x1 and
412/// the filter off.
413///
414/// # Returns
415///
416/// The settings.
417#[no_mangle]
418pub extern "C" fn pamoja_bme280_settings_default() -> PamojaBme280Settings {
419    PamojaBme280Settings {
420        temperature: bme280::Oversampling::X1.code(),
421        pressure: bme280::Oversampling::X1.code(),
422        humidity: bme280::Oversampling::X1.code(),
423        filter: bme280::Filter::Off.code(),
424    }
425}
426
427/// Creates a BME280 driver on a bus. Nothing is sent until [`pamoja_bme280_init`] or the first
428/// [`pamoja_bme280_measure`].
429///
430/// # Arguments
431///
432/// * `bus` - the bus the part is on; the driver holds its own share.
433/// * `address` - [`crate::sensors::PAMOJA_BME280_I2C_ADDRESS_PRIMARY`] with SDO low, or
434///   [`crate::sensors::PAMOJA_BME280_I2C_ADDRESS_SECONDARY`] with SDO high.
435/// * `settings` - the oversampling and the filter.
436/// * `out_sensor` - receives the driver.
437///
438/// # Returns
439///
440/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
441/// for a null argument.
442///
443/// # Safety
444///
445/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
446#[no_mangle]
447pub unsafe extern "C" fn pamoja_bme280_new(
448    bus: *const PamojaI2cBus,
449    address: u8,
450    settings: PamojaBme280Settings,
451    out_sensor: *mut *mut PamojaBme280,
452) -> PamojaStatus {
453    build(bus, out_sensor, |bus, delay| PamojaBme280 {
454        sensor: bme280::Bme280::i2c(bus, address, delay)
455            .with_oversampling(
456                bme280::Oversampling::from_code(settings.temperature),
457                bme280::Oversampling::from_code(settings.pressure),
458                bme280::Oversampling::from_code(settings.humidity),
459            )
460            .with_filter(bme280::Filter::from_code(settings.filter)),
461    })
462}
463
464/// Resets the part, checks it is a BME280, reads its calibration, and writes the settings, in
465/// the order the datasheet requires, leaving the part asleep.
466///
467/// # Arguments
468///
469/// * `sensor` - the driver.
470///
471/// # Returns
472///
473/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
474/// [`PamojaStatus::Io`] when the bus fails or the calibration never finishes loading; or
475/// [`PamojaStatus::Codec`] when the part at the address is not a BME280.
476///
477/// # Safety
478///
479/// `sensor` must be a live handle or null.
480#[no_mangle]
481pub unsafe extern "C" fn pamoja_bme280_init(sensor: *mut PamojaBme280) -> PamojaStatus {
482    run(sensor, |held| held.sensor.init())
483}
484
485/// Runs one forced measurement and compensates it, initializing the part first if
486/// [`pamoja_bme280_init`] has not run.
487///
488/// # Arguments
489///
490/// * `sensor` - the driver.
491/// * `out_measurement` - receives the reading.
492///
493/// # Returns
494///
495/// As [`pamoja_bme280_init`], with [`PamojaStatus::Io`] also when the part is still measuring
496/// after the datasheet's time.
497///
498/// # Safety
499///
500/// `sensor` must be a live handle or null, and `out_measurement` a writable pointer or null.
501#[no_mangle]
502pub unsafe extern "C" fn pamoja_bme280_measure(
503    sensor: *mut PamojaBme280,
504    out_measurement: *mut PamojaBme280Measurement,
505) -> PamojaStatus {
506    fill(
507        sensor,
508        out_measurement,
509        "out_measurement",
510        |held| held.sensor.measure(),
511        |reading| PamojaBme280Measurement {
512            celsius: reading.celsius(),
513            pascals: reading.pascals(),
514            hectopascals: reading.hectopascals(),
515            relative_humidity_percent: reading.relative_humidity_percent(),
516        },
517    )
518}
519
520/// Releases a driver and its share of the bus. A null pointer is ignored.
521///
522/// # Safety
523///
524/// `sensor` must be a handle from [`pamoja_bme280_new`] that has not been freed, or null.
525#[no_mangle]
526pub unsafe extern "C" fn pamoja_bme280_free(sensor: *mut PamojaBme280) {
527    release(sensor);
528}
529
530/// Creates a simulated BME280 holding a real part's calibration and one measurement it took,
531/// which compensate to 20.44 C, 848.05 hPa, and 44.65 %.
532///
533/// # Arguments
534///
535/// * `address` - the address it answers to.
536///
537/// # Returns
538///
539/// The part, which the caller puts on a simulated bus and releases with
540/// [`crate::hal::pamoja_i2c_part_free`].
541#[no_mangle]
542pub extern "C" fn pamoja_bme280_sim_part(address: u8) -> *mut PamojaI2cPart {
543    PamojaI2cPart::into_raw(bme280::sim::part(address))
544}
545
546/// Creates a simulated BME280 that reads what it is asked to.
547///
548/// # Arguments
549///
550/// * `address` - the address it answers to.
551/// * `celsius` - the temperature it reports.
552/// * `hectopascals` - the pressure it reports.
553/// * `relative_humidity` - the humidity it reports, as a percentage.
554///
555/// # Returns
556///
557/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its readings
558/// land within a hundredth of a degree, a hundredth of a hectopascal, and a thousandth of a
559/// percent of what was asked for, the nearest its converter can represent.
560#[no_mangle]
561pub extern "C" fn pamoja_bme280_sim_reporting(
562    address: u8,
563    celsius: f32,
564    hectopascals: f32,
565    relative_humidity: f32,
566) -> *mut PamojaI2cPart {
567    PamojaI2cPart::into_raw(bme280::sim::reporting(
568        address,
569        celsius,
570        hectopascals,
571        relative_humidity,
572    ))
573}
574
575/// Copies the calibration a simulated BME280 holds: the two blocks a driver reads at start-up.
576///
577/// # Arguments
578///
579/// * `out_temp_press` - receives the 26-byte temperature and pressure block.
580/// * `out_humidity` - receives the 7-byte humidity block.
581///
582/// # Returns
583///
584/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null pointer.
585///
586/// # Safety
587///
588/// `out_temp_press` must point to 26 writable bytes and `out_humidity` to 7.
589#[no_mangle]
590pub unsafe extern "C" fn pamoja_bme280_sim_calibration(
591    out_temp_press: *mut u8,
592    out_humidity: *mut u8,
593) -> PamojaStatus {
594    if out_temp_press.is_null() || out_humidity.is_null() {
595        set_last_error("out_temp_press and out_humidity must not be null".to_owned());
596        return PamojaStatus::InvalidArgument;
597    }
598    std::slice::from_raw_parts_mut(out_temp_press, PAMOJA_BME280_CALIBRATION_TEMP_PRESS_LEN)
599        .copy_from_slice(&bme280::sim::CALIBRATION);
600    std::slice::from_raw_parts_mut(out_humidity, PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN)
601        .copy_from_slice(&bme280::sim::CALIBRATION_HUMIDITY);
602    PamojaStatus::Ok
603}
604
605/// Copies the one measurement a simulated BME280 holds: the eight data registers a real part
606/// read, which compensate to 20.44 C, 848.05 hPa, and 44.65 % against its calibration.
607///
608/// # Arguments
609///
610/// * `out_burst` - receives the eight bytes a burst read returns.
611///
612/// # Returns
613///
614/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null pointer.
615///
616/// # Safety
617///
618/// `out_burst` must point to 8 writable bytes.
619#[no_mangle]
620pub unsafe extern "C" fn pamoja_bme280_sim_burst(out_burst: *mut u8) -> PamojaStatus {
621    copy_out(out_burst, "out_burst", &bme280::sim::BURST)
622}
623
624/// Builds the eight data registers a simulated BME280 holds when it reports a reading.
625///
626/// # Arguments
627///
628/// * `celsius` - the temperature.
629/// * `hectopascals` - the pressure.
630/// * `relative_humidity` - the humidity, as a percentage.
631/// * `out_burst` - receives the eight bytes a burst read returns.
632///
633/// # Returns
634///
635/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null pointer.
636///
637/// # Safety
638///
639/// `out_burst` must point to 8 writable bytes.
640#[no_mangle]
641pub unsafe extern "C" fn pamoja_bme280_sim_burst_for(
642    celsius: f32,
643    hectopascals: f32,
644    relative_humidity: f32,
645    out_burst: *mut u8,
646) -> PamojaStatus {
647    let burst: [u8; PAMOJA_BME280_MEASUREMENT_LEN] =
648        bme280::sim::burst_for(celsius, hectopascals, relative_humidity);
649    copy_out(out_burst, "out_burst", &burst)
650}
651
652/// Returns the settings a BMP280 driver starts with: both measurements at oversampling x1 and
653/// the filter off.
654///
655/// # Returns
656///
657/// The settings.
658#[no_mangle]
659pub extern "C" fn pamoja_bmp280_settings_default() -> PamojaBmp280Settings {
660    PamojaBmp280Settings {
661        temperature: bmp280::Oversampling::X1.code(),
662        pressure: bmp280::Oversampling::X1.code(),
663        filter: 0,
664    }
665}
666
667/// Creates a BMP280 driver on a bus. Nothing is sent until [`pamoja_bmp280_init`] or the first
668/// [`pamoja_bmp280_measure`].
669///
670/// # Arguments
671///
672/// * `bus` - the bus the part is on; the driver holds its own share.
673/// * `address` - [`crate::sensors::PAMOJA_BMP280_I2C_ADDRESS_PRIMARY`] with SDO low, or
674///   [`crate::sensors::PAMOJA_BMP280_I2C_ADDRESS_SECONDARY`] with SDO high.
675/// * `settings` - the oversampling and the filter.
676/// * `out_sensor` - receives the driver.
677///
678/// # Returns
679///
680/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
681/// for a null argument.
682///
683/// # Safety
684///
685/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
686#[no_mangle]
687pub unsafe extern "C" fn pamoja_bmp280_new(
688    bus: *const PamojaI2cBus,
689    address: u8,
690    settings: PamojaBmp280Settings,
691    out_sensor: *mut *mut PamojaBmp280,
692) -> PamojaStatus {
693    build(bus, out_sensor, |bus, delay| PamojaBmp280 {
694        sensor: bmp280::Bmp280::i2c(bus, address, delay)
695            .with_oversampling(
696                bmp280::Oversampling::from_code(settings.temperature),
697                bmp280::Oversampling::from_code(settings.pressure),
698            )
699            .with_filter(settings.filter),
700    })
701}
702
703/// Resets the part, checks it is a BMP280, reads its trimming coefficients, and writes the
704/// settings, leaving the part asleep.
705///
706/// # Arguments
707///
708/// * `sensor` - the driver.
709///
710/// # Returns
711///
712/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
713/// [`PamojaStatus::Io`] when the bus fails or the trimming never finishes loading; or
714/// [`PamojaStatus::Codec`] when the part at the address is not a BMP280.
715///
716/// # Safety
717///
718/// `sensor` must be a live handle or null.
719#[no_mangle]
720pub unsafe extern "C" fn pamoja_bmp280_init(sensor: *mut PamojaBmp280) -> PamojaStatus {
721    run(sensor, |held| held.sensor.init())
722}
723
724/// Runs one forced measurement and compensates it, initializing the part first if
725/// [`pamoja_bmp280_init`] has not run.
726///
727/// # Arguments
728///
729/// * `sensor` - the driver.
730/// * `out_reading` - receives the reading.
731///
732/// # Returns
733///
734/// As [`pamoja_bmp280_init`], with [`PamojaStatus::Io`] also when the part is still measuring
735/// after the datasheet's time.
736///
737/// # Safety
738///
739/// `sensor` must be a live handle or null, and `out_reading` a writable pointer or null.
740#[no_mangle]
741pub unsafe extern "C" fn pamoja_bmp280_measure(
742    sensor: *mut PamojaBmp280,
743    out_reading: *mut PamojaBmp280Reading,
744) -> PamojaStatus {
745    fill(
746        sensor,
747        out_reading,
748        "out_reading",
749        |held| held.sensor.measure(),
750        |reading| PamojaBmp280Reading {
751            celsius: reading.celsius(),
752            pascals: reading.pascals(),
753            hectopascals: reading.hectopascals(),
754        },
755    )
756}
757
758/// Copies the trimming coefficients a BMP280 driver read at initialization.
759///
760/// # Arguments
761///
762/// * `sensor` - the driver.
763/// * `out_coefficients` - receives the coefficients.
764///
765/// # Returns
766///
767/// `true` with the coefficients in `out_coefficients`; `false` before the part has been
768/// initialized, or for a null argument.
769///
770/// # Safety
771///
772/// `sensor` must be a live handle or null, and `out_coefficients` a writable pointer or null.
773#[no_mangle]
774pub unsafe extern "C" fn pamoja_bmp280_coefficients(
775    sensor: *const PamojaBmp280,
776    out_coefficients: *mut PamojaBmp280Coefficients,
777) -> bool {
778    let Some(held) = sensor.as_ref() else {
779        return false;
780    };
781    store(
782        out_coefficients,
783        held.sensor.calibration().map(|c| (*c).into()),
784    )
785}
786
787/// Releases a driver and its share of the bus. A null pointer is ignored.
788///
789/// # Safety
790///
791/// `sensor` must be a handle from [`pamoja_bmp280_new`] that has not been freed, or null.
792#[no_mangle]
793pub unsafe extern "C" fn pamoja_bmp280_free(sensor: *mut PamojaBmp280) {
794    release(sensor);
795}
796
797/// Creates a simulated BMP280 holding a real part's trimming and one measurement it took,
798/// which compensate to 20.44 C and 848.05 hPa.
799///
800/// # Arguments
801///
802/// * `address` - the address it answers to.
803///
804/// # Returns
805///
806/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
807#[no_mangle]
808pub extern "C" fn pamoja_bmp280_sim_part(address: u8) -> *mut PamojaI2cPart {
809    PamojaI2cPart::into_raw(bmp280::sim::part(address))
810}
811
812/// Creates a simulated BMP280 that reads what it is asked to.
813///
814/// # Arguments
815///
816/// * `address` - the address it answers to.
817/// * `celsius` - the temperature it reports.
818/// * `hectopascals` - the pressure it reports.
819///
820/// # Returns
821///
822/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its readings
823/// land within a hundredth of a degree and a hundredth of a hectopascal.
824#[no_mangle]
825pub extern "C" fn pamoja_bmp280_sim_reporting(
826    address: u8,
827    celsius: f32,
828    hectopascals: f32,
829) -> *mut PamojaI2cPart {
830    PamojaI2cPart::into_raw(bmp280::sim::reporting(address, celsius, hectopascals))
831}
832
833/// Copies the 24 trimming bytes a simulated BMP280 holds.
834///
835/// # Arguments
836///
837/// * `out_calibration` - receives the bytes.
838///
839/// # Returns
840///
841/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null pointer.
842///
843/// # Safety
844///
845/// `out_calibration` must point to 24 writable bytes.
846#[no_mangle]
847pub unsafe extern "C" fn pamoja_bmp280_sim_calibration(out_calibration: *mut u8) -> PamojaStatus {
848    let calibration: [u8; PAMOJA_BMP280_CALIBRATION_LEN] = bmp280::sim::CALIBRATION;
849    copy_out(out_calibration, "out_calibration", &calibration)
850}
851
852/// Copies the six data registers a simulated BMP280 holds: one measurement a real part took.
853///
854/// # Arguments
855///
856/// * `out_burst` - receives the six bytes a burst read returns.
857///
858/// # Returns
859///
860/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null pointer.
861///
862/// # Safety
863///
864/// `out_burst` must point to 6 writable bytes.
865#[no_mangle]
866pub unsafe extern "C" fn pamoja_bmp280_sim_burst(out_burst: *mut u8) -> PamojaStatus {
867    let burst: [u8; PAMOJA_BMP280_DATA_LEN] = bmp280::sim::BURST;
868    copy_out(out_burst, "out_burst", &burst)
869}
870
871/// Builds the six data registers a simulated BMP280 holds when it reports a reading.
872///
873/// # Arguments
874///
875/// * `celsius` - the temperature.
876/// * `hectopascals` - the pressure.
877/// * `out_burst` - receives the six bytes a burst read returns.
878///
879/// # Returns
880///
881/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null pointer.
882///
883/// # Safety
884///
885/// `out_burst` must point to 6 writable bytes.
886#[no_mangle]
887pub unsafe extern "C" fn pamoja_bmp280_sim_burst_for(
888    celsius: f32,
889    hectopascals: f32,
890    out_burst: *mut u8,
891) -> PamojaStatus {
892    copy_out(
893        out_burst,
894        "out_burst",
895        &bmp280::sim::burst_for(celsius, hectopascals),
896    )
897}
898
899/// Creates a TMP117 driver on a bus. Nothing is sent until [`pamoja_tmp117_init`] or the first
900/// [`pamoja_tmp117_measure`].
901///
902/// # Arguments
903///
904/// * `bus` - the bus the part is on; the driver holds its own share.
905/// * `address` - one of the `PAMOJA_TMP117_ADDRESS_` constants, by where ADD0 is tied.
906/// * `averaging` - the averaging code, `0..=3`, for 1, 8, 32, or 64 conversions per result;
907///   `1`, eight, is the factory setting.
908/// * `out_sensor` - receives the driver.
909///
910/// # Returns
911///
912/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
913/// for a null argument.
914///
915/// # Safety
916///
917/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
918#[no_mangle]
919pub unsafe extern "C" fn pamoja_tmp117_new(
920    bus: *const PamojaI2cBus,
921    address: u8,
922    averaging: u8,
923    out_sensor: *mut *mut PamojaTmp117,
924) -> PamojaStatus {
925    build(bus, out_sensor, |bus, delay| PamojaTmp117 {
926        sensor: tmp117::Tmp117::new(bus, address, delay)
927            .with_averaging(tmp117::Averaging::from_code(averaging)),
928    })
929}
930
931/// Checks the part is a TMP117, waits for its EEPROM to finish loading, and writes the
932/// settings with the part in shutdown.
933///
934/// # Arguments
935///
936/// * `sensor` - the driver.
937///
938/// # Returns
939///
940/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
941/// [`PamojaStatus::Io`] when the bus fails or the EEPROM never reports ready; or
942/// [`PamojaStatus::Codec`] when the device id is not a TMP117's.
943///
944/// # Safety
945///
946/// `sensor` must be a live handle or null.
947#[no_mangle]
948pub unsafe extern "C" fn pamoja_tmp117_init(sensor: *mut PamojaTmp117) -> PamojaStatus {
949    run(sensor, |held| held.sensor.init())
950}
951
952/// Runs one conversion and reads the temperature, initializing the part first if
953/// [`pamoja_tmp117_init`] has not run.
954///
955/// # Arguments
956///
957/// * `sensor` - the driver.
958/// * `out_reading` - receives the temperature.
959///
960/// # Returns
961///
962/// As [`pamoja_tmp117_init`], with [`PamojaStatus::Io`] also when the part never reports the
963/// conversion done.
964///
965/// # Safety
966///
967/// `sensor` must be a live handle or null, and `out_reading` a writable pointer or null.
968#[no_mangle]
969pub unsafe extern "C" fn pamoja_tmp117_measure(
970    sensor: *mut PamojaTmp117,
971    out_reading: *mut PamojaTmp117Reading,
972) -> PamojaStatus {
973    fill(
974        sensor,
975        out_reading,
976        "out_reading",
977        |held| held.sensor.measure(),
978        |reading| PamojaTmp117Reading {
979            raw: reading.raw(),
980            micro_celsius: reading.micro_celsius(),
981            celsius: reading.celsius(),
982        },
983    )
984}
985
986/// Writes the high and low limits the part compares each result against.
987///
988/// # Arguments
989///
990/// * `sensor` - the driver.
991/// * `high_celsius` - the high limit; the factory value is 192 C.
992/// * `low_celsius` - the low limit; the factory value is -256 C.
993///
994/// # Returns
995///
996/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null driver, or
997/// [`PamojaStatus::Io`] when the bus fails.
998///
999/// # Safety
1000///
1001/// `sensor` must be a live handle or null.
1002#[no_mangle]
1003pub unsafe extern "C" fn pamoja_tmp117_set_alert_limits(
1004    sensor: *mut PamojaTmp117,
1005    high_celsius: f32,
1006    low_celsius: f32,
1007) -> PamojaStatus {
1008    run(sensor, |held| {
1009        held.sensor.set_alert_limits(high_celsius, low_celsius)
1010    })
1011}
1012
1013/// Reads the alert flags: whether a result since the last call was above the high limit or
1014/// below the low limit, including results the driver's own reads saw.
1015///
1016/// # Arguments
1017///
1018/// * `sensor` - the driver.
1019/// * `out_alerts` - receives the flags.
1020///
1021/// # Returns
1022///
1023/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null argument, or
1024/// [`PamojaStatus::Io`] when the bus fails.
1025///
1026/// # Safety
1027///
1028/// `sensor` must be a live handle or null, and `out_alerts` a writable pointer or null.
1029#[no_mangle]
1030pub unsafe extern "C" fn pamoja_tmp117_alerts(
1031    sensor: *mut PamojaTmp117,
1032    out_alerts: *mut PamojaTmp117Alerts,
1033) -> PamojaStatus {
1034    fill(
1035        sensor,
1036        out_alerts,
1037        "out_alerts",
1038        |held| held.sensor.alerts(),
1039        |alerts| PamojaTmp117Alerts {
1040            high: u8::from(alerts.high),
1041            low: u8::from(alerts.low),
1042        },
1043    )
1044}
1045
1046/// Reports the silicon revision a TMP117 driver read at initialization.
1047///
1048/// # Arguments
1049///
1050/// * `sensor` - the driver.
1051/// * `out_revision` - receives the revision.
1052///
1053/// # Returns
1054///
1055/// `true` with the revision in `out_revision`; `false` before the part has been initialized,
1056/// or for a null argument.
1057///
1058/// # Safety
1059///
1060/// `sensor` must be a live handle or null, and `out_revision` a writable pointer or null.
1061#[no_mangle]
1062pub unsafe extern "C" fn pamoja_tmp117_silicon_revision(
1063    sensor: *const PamojaTmp117,
1064    out_revision: *mut u8,
1065) -> bool {
1066    let Some(held) = sensor.as_ref() else {
1067        return false;
1068    };
1069    store(out_revision, held.sensor.revision())
1070}
1071
1072/// Releases a driver and its share of the bus. A null pointer is ignored.
1073///
1074/// # Safety
1075///
1076/// `sensor` must be a handle from [`pamoja_tmp117_new`] that has not been freed, or null.
1077#[no_mangle]
1078pub unsafe extern "C" fn pamoja_tmp117_free(sensor: *mut PamojaTmp117) {
1079    release(sensor);
1080}
1081
1082/// Creates a simulated TMP117 reading [`PAMOJA_TMP117_SIM_CELSIUS`].
1083///
1084/// # Arguments
1085///
1086/// * `address` - the address it answers to.
1087///
1088/// # Returns
1089///
1090/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1091#[no_mangle]
1092pub extern "C" fn pamoja_tmp117_sim_part(address: u8) -> *mut PamojaI2cPart {
1093    PamojaI2cPart::into_raw(tmp117::sim::part(address))
1094}
1095
1096/// Creates a simulated TMP117 that reads what it is asked to. Its configuration register keeps
1097/// the flags the part sets for itself, with the data-ready flag set.
1098///
1099/// # Arguments
1100///
1101/// * `address` - the address it answers to.
1102/// * `celsius` - the temperature it reports, which lands on the nearest 7.8125 millidegrees.
1103///
1104/// # Returns
1105///
1106/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1107#[no_mangle]
1108pub extern "C" fn pamoja_tmp117_sim_reporting(address: u8, celsius: f32) -> *mut PamojaI2cPart {
1109    PamojaI2cPart::into_raw(tmp117::sim::reporting(address, celsius))
1110}
1111
1112/// Returns the settings an OPT3001 driver starts with, the part's own reset settings: the
1113/// 800 ms integration time and the range chosen automatically.
1114///
1115/// # Returns
1116///
1117/// The settings.
1118#[no_mangle]
1119pub extern "C" fn pamoja_opt3001_settings_default() -> PamojaOpt3001Settings {
1120    PamojaOpt3001Settings {
1121        long_conversion: 1,
1122        range_number: opt3001::RANGE_AUTOMATIC,
1123    }
1124}
1125
1126/// Creates an OPT3001 driver on a bus. Nothing is sent until [`pamoja_opt3001_init`] or the
1127/// first [`pamoja_opt3001_measure`].
1128///
1129/// # Arguments
1130///
1131/// * `bus` - the bus the part is on; the driver holds its own share.
1132/// * `address` - one of the `PAMOJA_OPT3001_I2C_ADDRESS_` constants, by where ADDR is tied.
1133/// * `settings` - the integration time and the range.
1134/// * `out_sensor` - receives the driver.
1135///
1136/// # Returns
1137///
1138/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
1139/// for a null argument.
1140///
1141/// # Safety
1142///
1143/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
1144#[no_mangle]
1145pub unsafe extern "C" fn pamoja_opt3001_new(
1146    bus: *const PamojaI2cBus,
1147    address: u8,
1148    settings: PamojaOpt3001Settings,
1149    out_sensor: *mut *mut PamojaOpt3001,
1150) -> PamojaStatus {
1151    build(bus, out_sensor, |bus, delay| PamojaOpt3001 {
1152        sensor: opt3001::Opt3001::new(bus, address, delay)
1153            .with_conversion_time(conversion_time(settings.long_conversion != 0))
1154            .with_range(settings.range_number),
1155    })
1156}
1157
1158/// Checks the part is an OPT3001 and writes the settings with the part in shutdown.
1159///
1160/// # Arguments
1161///
1162/// * `sensor` - the driver.
1163///
1164/// # Returns
1165///
1166/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
1167/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when either id register
1168/// is not an OPT3001's.
1169///
1170/// # Safety
1171///
1172/// `sensor` must be a live handle or null.
1173#[no_mangle]
1174pub unsafe extern "C" fn pamoja_opt3001_init(sensor: *mut PamojaOpt3001) -> PamojaStatus {
1175    run(sensor, |held| held.sensor.init())
1176}
1177
1178/// Runs one conversion and reads the illuminance, initializing the part first if
1179/// [`pamoja_opt3001_init`] has not run.
1180///
1181/// # Arguments
1182///
1183/// * `sensor` - the driver.
1184/// * `out_reading` - receives the illuminance.
1185///
1186/// # Returns
1187///
1188/// As [`pamoja_opt3001_init`], with [`PamojaStatus::Io`] also when the conversion never
1189/// finishes.
1190///
1191/// # Safety
1192///
1193/// `sensor` must be a live handle or null, and `out_reading` a writable pointer or null.
1194#[no_mangle]
1195pub unsafe extern "C" fn pamoja_opt3001_measure(
1196    sensor: *mut PamojaOpt3001,
1197    out_reading: *mut PamojaOpt3001Reading,
1198) -> PamojaStatus {
1199    fill(
1200        sensor,
1201        out_reading,
1202        "out_reading",
1203        |held| held.sensor.measure(),
1204        |reading| PamojaOpt3001Reading {
1205            raw: reading.raw(),
1206            milli_lux: reading.milli_lux(),
1207            lux: reading.lux(),
1208        },
1209    )
1210}
1211
1212/// Writes the low and high limits the part's interrupt pin compares each result against.
1213///
1214/// # Arguments
1215///
1216/// * `sensor` - the driver.
1217/// * `low_milli_lux` - the low limit, in millilux.
1218/// * `high_milli_lux` - the high limit, in millilux.
1219///
1220/// # Returns
1221///
1222/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null driver, or
1223/// [`PamojaStatus::Io`] when the bus fails.
1224///
1225/// # Safety
1226///
1227/// `sensor` must be a live handle or null.
1228#[no_mangle]
1229pub unsafe extern "C" fn pamoja_opt3001_set_limits(
1230    sensor: *mut PamojaOpt3001,
1231    low_milli_lux: u32,
1232    high_milli_lux: u32,
1233) -> PamojaStatus {
1234    run(sensor, |held| {
1235        held.sensor.set_limits(low_milli_lux, high_milli_lux)
1236    })
1237}
1238
1239/// Copies the configuration an OPT3001 driver writes, with the part in shutdown.
1240///
1241/// # Arguments
1242///
1243/// * `sensor` - the driver.
1244/// * `out_config` - receives the configuration.
1245///
1246/// # Returns
1247///
1248/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
1249///
1250/// # Safety
1251///
1252/// `sensor` must be a live handle or null, and `out_config` a writable pointer or null.
1253#[no_mangle]
1254pub unsafe extern "C" fn pamoja_opt3001_configuration(
1255    sensor: *const PamojaOpt3001,
1256    out_config: *mut PamojaOpt3001Config,
1257) -> PamojaStatus {
1258    look(sensor, out_config, "out_config", |held| {
1259        held.sensor.configuration().into()
1260    })
1261}
1262
1263/// Releases a driver and its share of the bus. A null pointer is ignored.
1264///
1265/// # Safety
1266///
1267/// `sensor` must be a handle from [`pamoja_opt3001_new`] that has not been freed, or null.
1268#[no_mangle]
1269pub unsafe extern "C" fn pamoja_opt3001_free(sensor: *mut PamojaOpt3001) {
1270    release(sensor);
1271}
1272
1273/// Creates a simulated OPT3001 reading [`PAMOJA_OPT3001_SIM_LUX`].
1274///
1275/// # Arguments
1276///
1277/// * `address` - the address it answers to.
1278///
1279/// # Returns
1280///
1281/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1282#[no_mangle]
1283pub extern "C" fn pamoja_opt3001_sim_part(address: u8) -> *mut PamojaI2cPart {
1284    PamojaI2cPart::into_raw(opt3001::sim::part(address))
1285}
1286
1287/// Creates a simulated OPT3001 that reads what it is asked to. Its configuration register
1288/// keeps the flags the part sets for itself, with the conversion-ready flag set.
1289///
1290/// # Arguments
1291///
1292/// * `address` - the address it answers to.
1293/// * `lux` - the illuminance it reports, which lands on the nearest step its exponent and
1294///   mantissa represent.
1295///
1296/// # Returns
1297///
1298/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1299#[no_mangle]
1300pub extern "C" fn pamoja_opt3001_sim_reporting(address: u8, lux: f32) -> *mut PamojaI2cPart {
1301    PamojaI2cPart::into_raw(opt3001::sim::reporting(address, lux))
1302}
1303
1304/// Returns the settings an HDC1080 driver starts with: both channels at 14 bits.
1305///
1306/// # Returns
1307///
1308/// The settings.
1309#[no_mangle]
1310pub extern "C" fn pamoja_hdc1080_settings_default() -> PamojaHdc1080Settings {
1311    PamojaHdc1080Settings {
1312        temperature_resolution_bits: 14,
1313        humidity_resolution_bits: 14,
1314    }
1315}
1316
1317/// Creates an HDC1080 driver on a bus; the part has one address. Nothing is sent until
1318/// [`pamoja_hdc1080_init`] or the first [`pamoja_hdc1080_measure`].
1319///
1320/// # Arguments
1321///
1322/// * `bus` - the bus the part is on; the driver holds its own share.
1323/// * `settings` - the resolution of each channel.
1324/// * `out_sensor` - receives the driver.
1325///
1326/// # Returns
1327///
1328/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
1329/// for a null argument or a resolution the part does not have.
1330///
1331/// # Safety
1332///
1333/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
1334#[no_mangle]
1335pub unsafe extern "C" fn pamoja_hdc1080_new(
1336    bus: *const PamojaI2cBus,
1337    settings: PamojaHdc1080Settings,
1338    out_sensor: *mut *mut PamojaHdc1080,
1339) -> PamojaStatus {
1340    let Some(temperature) = temperature_resolution(settings.temperature_resolution_bits) else {
1341        return clear_then(out_sensor, crate::sensors::bad_temperature_resolution());
1342    };
1343    let Some(humidity) = humidity_resolution(settings.humidity_resolution_bits) else {
1344        return clear_then(out_sensor, crate::sensors::bad_humidity_resolution());
1345    };
1346    build(bus, out_sensor, |bus, delay| PamojaHdc1080 {
1347        sensor: hdc1080::Hdc1080::new(bus, delay).with_resolutions(temperature, humidity),
1348    })
1349}
1350
1351/// Checks the part is an HDC1080 and writes the configuration.
1352///
1353/// # Arguments
1354///
1355/// * `sensor` - the driver.
1356///
1357/// # Returns
1358///
1359/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
1360/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when either id register
1361/// is not an HDC1080's.
1362///
1363/// # Safety
1364///
1365/// `sensor` must be a live handle or null.
1366#[no_mangle]
1367pub unsafe extern "C" fn pamoja_hdc1080_init(sensor: *mut PamojaHdc1080) -> PamojaStatus {
1368    run(sensor, |held| held.sensor.init())
1369}
1370
1371/// Triggers one acquisition of both channels and reads them, initializing the part first if
1372/// [`pamoja_hdc1080_init`] has not run.
1373///
1374/// # Arguments
1375///
1376/// * `sensor` - the driver.
1377/// * `out_measurement` - receives the temperature and humidity.
1378///
1379/// # Returns
1380///
1381/// As [`pamoja_hdc1080_init`]. A part that does not acknowledge the read before its results
1382/// are ready is [`PamojaStatus::Io`].
1383///
1384/// # Safety
1385///
1386/// `sensor` must be a live handle or null, and `out_measurement` a writable pointer or null.
1387#[no_mangle]
1388pub unsafe extern "C" fn pamoja_hdc1080_measure(
1389    sensor: *mut PamojaHdc1080,
1390    out_measurement: *mut PamojaHdc1080Measurement,
1391) -> PamojaStatus {
1392    fill(
1393        sensor,
1394        out_measurement,
1395        "out_measurement",
1396        |held| held.sensor.measure(),
1397        PamojaHdc1080Measurement::from,
1398    )
1399}
1400
1401/// Switches the on-die heater, which runs only during acquisitions, on or off.
1402///
1403/// # Arguments
1404///
1405/// * `sensor` - the driver.
1406/// * `on` - whether the heater runs.
1407///
1408/// # Returns
1409///
1410/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null driver, or
1411/// [`PamojaStatus::Io`] when the bus fails.
1412///
1413/// # Safety
1414///
1415/// `sensor` must be a live handle or null.
1416#[no_mangle]
1417pub unsafe extern "C" fn pamoja_hdc1080_heater(
1418    sensor: *mut PamojaHdc1080,
1419    on: bool,
1420) -> PamojaStatus {
1421    run(sensor, |held| held.sensor.heater(on))
1422}
1423
1424/// Copies the configuration an HDC1080 driver writes.
1425///
1426/// # Arguments
1427///
1428/// * `sensor` - the driver.
1429/// * `out_config` - receives the configuration.
1430///
1431/// # Returns
1432///
1433/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
1434///
1435/// # Safety
1436///
1437/// `sensor` must be a live handle or null, and `out_config` a writable pointer or null.
1438#[no_mangle]
1439pub unsafe extern "C" fn pamoja_hdc1080_configuration(
1440    sensor: *const PamojaHdc1080,
1441    out_config: *mut PamojaHdc1080Config,
1442) -> PamojaStatus {
1443    look(sensor, out_config, "out_config", |held| {
1444        held.sensor.configuration().into()
1445    })
1446}
1447
1448/// Releases a driver and its share of the bus. A null pointer is ignored.
1449///
1450/// # Safety
1451///
1452/// `sensor` must be a handle from [`pamoja_hdc1080_new`] that has not been freed, or null.
1453#[no_mangle]
1454pub unsafe extern "C" fn pamoja_hdc1080_free(sensor: *mut PamojaHdc1080) {
1455    release(sensor);
1456}
1457
1458/// Creates a simulated HDC1080 reading [`PAMOJA_HDC1080_SIM_CELSIUS`] and
1459/// [`PAMOJA_HDC1080_SIM_RELATIVE_HUMIDITY`].
1460///
1461/// # Returns
1462///
1463/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1464#[no_mangle]
1465pub extern "C" fn pamoja_hdc1080_sim_part() -> *mut PamojaI2cPart {
1466    PamojaI2cPart::into_raw(hdc1080::sim::part())
1467}
1468
1469/// Creates a simulated HDC1080 that reads what it is asked to.
1470///
1471/// # Arguments
1472///
1473/// * `celsius` - the temperature it reports.
1474/// * `relative_humidity` - the humidity it reports, as a percentage.
1475///
1476/// # Returns
1477///
1478/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its readings
1479/// land within three thousandths of a degree and two thousandths of a percent.
1480#[no_mangle]
1481pub extern "C" fn pamoja_hdc1080_sim_reporting(
1482    celsius: f32,
1483    relative_humidity: f32,
1484) -> *mut PamojaI2cPart {
1485    PamojaI2cPart::into_raw(hdc1080::sim::reporting(celsius, relative_humidity))
1486}
1487
1488/// Returns the settings an INA219 driver starts with: a 100 milliohm shunt sized for 3.2 A,
1489/// the common breakout, the finest current step for it, and the power-on register settings.
1490///
1491/// # Returns
1492///
1493/// The settings.
1494#[no_mangle]
1495pub extern "C" fn pamoja_ina219_settings_default() -> PamojaIna219Settings {
1496    PamojaIna219Settings {
1497        shunt_milliohms: 100,
1498        max_microamps: 3_200_000,
1499        current_lsb_microamps: 0,
1500        config: ina219::Configuration::default().into(),
1501    }
1502}
1503
1504/// Creates an INA219 driver on a bus. Nothing is sent until [`pamoja_ina219_init`] or the first
1505/// [`pamoja_ina219_measure`].
1506///
1507/// # Arguments
1508///
1509/// * `bus` - the bus the part is on; the driver holds its own share.
1510/// * `address` - [`crate::sensors::PAMOJA_INA219_BASE_ADDRESS`] plus what A1 and A0 add.
1511/// * `settings` - the shunt, the current it is sized for, and the register settings.
1512/// * `out_sensor` - receives the driver.
1513///
1514/// # Returns
1515///
1516/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
1517/// for a null argument.
1518///
1519/// # Safety
1520///
1521/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
1522#[no_mangle]
1523pub unsafe extern "C" fn pamoja_ina219_new(
1524    bus: *const PamojaI2cBus,
1525    address: u8,
1526    settings: PamojaIna219Settings,
1527    out_sensor: *mut *mut PamojaIna219,
1528) -> PamojaStatus {
1529    build(bus, out_sensor, |bus, delay| {
1530        let mut sensor = ina219::Ina219::new(bus, address, delay)
1531            .with_shunt(settings.shunt_milliohms, settings.max_microamps)
1532            .with_configuration(settings.config.into());
1533        if settings.current_lsb_microamps != 0 {
1534            sensor = sensor.with_current_lsb(settings.current_lsb_microamps);
1535        }
1536        PamojaIna219 { sensor }
1537    })
1538}
1539
1540/// Resets the part, writes the configuration and the calibration, and reads the calibration
1541/// back, which is the identity check a part with no id register allows.
1542///
1543/// # Arguments
1544///
1545/// * `sensor` - the driver.
1546///
1547/// # Returns
1548///
1549/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
1550/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when the calibration
1551/// register does not hold what was written.
1552///
1553/// # Safety
1554///
1555/// `sensor` must be a live handle or null.
1556#[no_mangle]
1557pub unsafe extern "C" fn pamoja_ina219_init(sensor: *mut PamojaIna219) -> PamojaStatus {
1558    run(sensor, |held| held.sensor.init())
1559}
1560
1561/// Triggers one shunt and bus conversion and reads every result, initializing the part first
1562/// if [`pamoja_ina219_init`] has not run.
1563///
1564/// # Arguments
1565///
1566/// * `sensor` - the driver.
1567/// * `out_reading` - receives the results.
1568///
1569/// # Returns
1570///
1571/// As [`pamoja_ina219_init`], with [`PamojaStatus::Io`] also when the conversion-ready flag
1572/// never sets.
1573///
1574/// # Safety
1575///
1576/// `sensor` must be a live handle or null, and `out_reading` a writable pointer or null.
1577#[no_mangle]
1578pub unsafe extern "C" fn pamoja_ina219_measure(
1579    sensor: *mut PamojaIna219,
1580    out_reading: *mut PamojaIna219Reading,
1581) -> PamojaStatus {
1582    fill(
1583        sensor,
1584        out_reading,
1585        "out_reading",
1586        |held| held.sensor.measure(),
1587        |reading| PamojaIna219Reading {
1588            shunt: reading.shunt,
1589            bus: reading.bus,
1590            current: reading.current,
1591            power: reading.power,
1592            current_lsb_microamps: reading.current_lsb_microamps,
1593            shunt_microvolts: reading.shunt_microvolts(),
1594            bus_millivolts: reading.bus_millivolts(),
1595            current_microamps: reading.current_microamps(),
1596            power_microwatts: reading.power_microwatts(),
1597            math_overflow: u8::from(reading.math_overflow()),
1598        },
1599    )
1600}
1601
1602/// Returns the current step an INA219 driver programs.
1603///
1604/// # Arguments
1605///
1606/// * `sensor` - the driver.
1607///
1608/// # Returns
1609///
1610/// Microamps per count of the current register, or 0 for a null driver.
1611///
1612/// # Safety
1613///
1614/// `sensor` must be a live handle or null.
1615#[no_mangle]
1616pub unsafe extern "C" fn pamoja_ina219_current_lsb(sensor: *const PamojaIna219) -> u32 {
1617    sensor
1618        .as_ref()
1619        .map_or(0, |held| held.sensor.current_lsb_microamps())
1620}
1621
1622/// Returns the calibration word an INA219 driver programs.
1623///
1624/// # Arguments
1625///
1626/// * `sensor` - the driver.
1627///
1628/// # Returns
1629///
1630/// The word, from the shunt and the current step by the datasheet's equation, or 0 for a
1631/// null driver.
1632///
1633/// # Safety
1634///
1635/// `sensor` must be a live handle or null.
1636#[no_mangle]
1637pub unsafe extern "C" fn pamoja_ina219_calibration_word(sensor: *const PamojaIna219) -> u16 {
1638    sensor.as_ref().map_or(0, |held| held.sensor.calibration())
1639}
1640
1641/// Releases a driver and its share of the bus. A null pointer is ignored.
1642///
1643/// # Safety
1644///
1645/// `sensor` must be a handle from [`pamoja_ina219_new`] that has not been freed, or null.
1646#[no_mangle]
1647pub unsafe extern "C" fn pamoja_ina219_free(sensor: *mut PamojaIna219) {
1648    release(sensor);
1649}
1650
1651/// Creates a simulated INA219 carrying [`PAMOJA_INA219_SIM_MICROAMPS`] at
1652/// [`PAMOJA_INA219_SIM_BUS_MILLIVOLTS`] through the shunt a driver starts with.
1653///
1654/// # Arguments
1655///
1656/// * `address` - the address it answers to.
1657///
1658/// # Returns
1659///
1660/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1661#[no_mangle]
1662pub extern "C" fn pamoja_ina219_sim_part(address: u8) -> *mut PamojaI2cPart {
1663    PamojaI2cPart::into_raw(ina219::sim::part(address))
1664}
1665
1666/// Creates a simulated INA219 that reads what it is asked to, calibrated for the same shunt and
1667/// largest current a driver is given.
1668///
1669/// # Arguments
1670///
1671/// * `address` - the address it answers to.
1672/// * `shunt_milliohms` - the shunt, as in the driver's settings.
1673/// * `max_microamps` - the largest current, as in the driver's settings.
1674/// * `bus_millivolts` - the bus voltage it reports.
1675/// * `microamps` - the current it reports; negative flows the other way through the shunt.
1676///
1677/// # Returns
1678///
1679/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its readings
1680/// land on the steps its registers count in: 4 mV of bus, 10 uV of shunt, and the
1681/// calibration's current step.
1682#[no_mangle]
1683pub extern "C" fn pamoja_ina219_sim_reporting(
1684    address: u8,
1685    shunt_milliohms: u32,
1686    max_microamps: u32,
1687    bus_millivolts: u32,
1688    microamps: i32,
1689) -> *mut PamojaI2cPart {
1690    PamojaI2cPart::into_raw(ina219::sim::reporting(
1691        address,
1692        shunt_milliohms,
1693        max_microamps,
1694        bus_millivolts,
1695        microamps,
1696    ))
1697}
1698
1699/// Returns the settings an INA226 driver starts with: a 100 milliohm shunt sized for 3.2 A,
1700/// the finest current step for it, and the power-on register settings.
1701///
1702/// # Returns
1703///
1704/// The settings.
1705#[no_mangle]
1706pub extern "C" fn pamoja_ina226_settings_default() -> PamojaIna226Settings {
1707    PamojaIna226Settings {
1708        shunt_milliohms: 100,
1709        max_microamps: 3_200_000,
1710        current_lsb_microamps: 0,
1711        config: ina226::Configuration::RESET.into(),
1712    }
1713}
1714
1715/// Creates an INA226 driver on a bus. Nothing is sent until [`pamoja_ina226_init`] or the first
1716/// [`pamoja_ina226_measure`].
1717///
1718/// # Arguments
1719///
1720/// * `bus` - the bus the part is on; the driver holds its own share.
1721/// * `address` - the address A1 and A0 select, which
1722///   [`crate::sensors::pamoja_ina226_address`] works out.
1723/// * `settings` - the shunt, the current it is sized for, and the register settings.
1724/// * `out_sensor` - receives the driver.
1725///
1726/// # Returns
1727///
1728/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
1729/// for a null argument.
1730///
1731/// # Safety
1732///
1733/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
1734#[no_mangle]
1735pub unsafe extern "C" fn pamoja_ina226_new(
1736    bus: *const PamojaI2cBus,
1737    address: u8,
1738    settings: PamojaIna226Settings,
1739    out_sensor: *mut *mut PamojaIna226,
1740) -> PamojaStatus {
1741    build(bus, out_sensor, |bus, delay| {
1742        let mut sensor = ina226::Ina226::new(bus, address, delay)
1743            .with_shunt(settings.shunt_milliohms, settings.max_microamps)
1744            .with_configuration(settings.config.into());
1745        if settings.current_lsb_microamps != 0 {
1746            sensor = sensor.with_current_lsb(settings.current_lsb_microamps);
1747        }
1748        PamojaIna226 { sensor }
1749    })
1750}
1751
1752/// Resets the part, checks it is an INA226, writes the configuration and the calibration, and
1753/// reads the calibration back.
1754///
1755/// # Arguments
1756///
1757/// * `sensor` - the driver.
1758///
1759/// # Returns
1760///
1761/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
1762/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when the id registers
1763/// are not an INA226's or the calibration does not read back.
1764///
1765/// # Safety
1766///
1767/// `sensor` must be a live handle or null.
1768#[no_mangle]
1769pub unsafe extern "C" fn pamoja_ina226_init(sensor: *mut PamojaIna226) -> PamojaStatus {
1770    run(sensor, |held| held.sensor.init())
1771}
1772
1773/// Triggers one shunt and bus conversion and reads every result, initializing the part first
1774/// if [`pamoja_ina226_init`] has not run.
1775///
1776/// # Arguments
1777///
1778/// * `sensor` - the driver.
1779/// * `out_reading` - receives the results.
1780///
1781/// # Returns
1782///
1783/// As [`pamoja_ina226_init`], with [`PamojaStatus::Io`] also when the conversion-ready flag
1784/// never sets.
1785///
1786/// # Safety
1787///
1788/// `sensor` must be a live handle or null, and `out_reading` a writable pointer or null.
1789#[no_mangle]
1790pub unsafe extern "C" fn pamoja_ina226_measure(
1791    sensor: *mut PamojaIna226,
1792    out_reading: *mut PamojaIna226Reading,
1793) -> PamojaStatus {
1794    fill(
1795        sensor,
1796        out_reading,
1797        "out_reading",
1798        |held| held.sensor.measure(),
1799        |reading| PamojaIna226Reading {
1800            shunt: reading.shunt,
1801            bus: reading.bus,
1802            current: reading.current,
1803            power: reading.power,
1804            current_lsb_microamps: reading.current_lsb_microamps,
1805            shunt_nanovolts: reading.shunt_nanovolts(),
1806            shunt_millivolts: reading.shunt_millivolts(),
1807            bus_microvolts: reading.bus_microvolts(),
1808            bus_volts: reading.bus_volts(),
1809            current_microamps: reading.current_microamps(),
1810            current_amps: reading.current_amps(),
1811            power_microwatts: reading.power_microwatts(),
1812            power_watts: reading.power_watts(),
1813            math_overflow: u8::from(reading.math_overflow),
1814        },
1815    )
1816}
1817
1818/// Programs the alert pin: which limit it watches, and the limit.
1819///
1820/// # Arguments
1821///
1822/// * `sensor` - the driver.
1823/// * `mask` - the mask/enable settings, one alert function at a time.
1824/// * `limit` - the alert-limit register, in the units of the register the function watches.
1825///
1826/// # Returns
1827///
1828/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null driver, or
1829/// [`PamojaStatus::Io`] when the bus fails.
1830///
1831/// # Safety
1832///
1833/// `sensor` must be a live handle or null.
1834#[no_mangle]
1835pub unsafe extern "C" fn pamoja_ina226_set_alert(
1836    sensor: *mut PamojaIna226,
1837    mask: PamojaIna226MaskEnable,
1838    limit: u16,
1839) -> PamojaStatus {
1840    run(sensor, |held| held.sensor.set_alert(mask.into(), limit))
1841}
1842
1843/// Returns the current step an INA226 driver programs.
1844///
1845/// # Arguments
1846///
1847/// * `sensor` - the driver.
1848///
1849/// # Returns
1850///
1851/// Microamps per count of the current register, or 0 for a null driver.
1852///
1853/// # Safety
1854///
1855/// `sensor` must be a live handle or null.
1856#[no_mangle]
1857pub unsafe extern "C" fn pamoja_ina226_current_lsb(sensor: *const PamojaIna226) -> u32 {
1858    sensor
1859        .as_ref()
1860        .map_or(0, |held| held.sensor.current_lsb_microamps())
1861}
1862
1863/// Returns the calibration word an INA226 driver programs.
1864///
1865/// # Arguments
1866///
1867/// * `sensor` - the driver.
1868///
1869/// # Returns
1870///
1871/// The word, or 0 for a null driver.
1872///
1873/// # Safety
1874///
1875/// `sensor` must be a live handle or null.
1876#[no_mangle]
1877pub unsafe extern "C" fn pamoja_ina226_calibration_word(sensor: *const PamojaIna226) -> u16 {
1878    sensor.as_ref().map_or(0, |held| held.sensor.calibration())
1879}
1880
1881/// Reports the die id an INA226 driver read at initialization.
1882///
1883/// # Arguments
1884///
1885/// * `sensor` - the driver.
1886/// * `out_die_id` - receives the device and revision.
1887///
1888/// # Returns
1889///
1890/// `true` with the id in `out_die_id`; `false` before the part has been initialized, or for a
1891/// null argument.
1892///
1893/// # Safety
1894///
1895/// `sensor` must be a live handle or null, and `out_die_id` a writable pointer or null.
1896#[no_mangle]
1897pub unsafe extern "C" fn pamoja_ina226_identity(
1898    sensor: *const PamojaIna226,
1899    out_die_id: *mut PamojaIna226DieId,
1900) -> bool {
1901    let Some(held) = sensor.as_ref() else {
1902        return false;
1903    };
1904    store(out_die_id, held.sensor.die_id().map(Into::into))
1905}
1906
1907/// Releases a driver and its share of the bus. A null pointer is ignored.
1908///
1909/// # Safety
1910///
1911/// `sensor` must be a handle from [`pamoja_ina226_new`] that has not been freed, or null.
1912#[no_mangle]
1913pub unsafe extern "C" fn pamoja_ina226_free(sensor: *mut PamojaIna226) {
1914    release(sensor);
1915}
1916
1917/// Creates a simulated INA226 carrying [`PAMOJA_INA226_SIM_MICROAMPS`] at
1918/// [`PAMOJA_INA226_SIM_BUS_MICROVOLTS`] through the shunt a driver starts with.
1919///
1920/// # Arguments
1921///
1922/// * `address` - the address it answers to.
1923///
1924/// # Returns
1925///
1926/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
1927#[no_mangle]
1928pub extern "C" fn pamoja_ina226_sim_part(address: u8) -> *mut PamojaI2cPart {
1929    PamojaI2cPart::into_raw(ina226::sim::part(address))
1930}
1931
1932/// Creates a simulated INA226 that reads what it is asked to, calibrated for the same shunt and
1933/// largest current a driver is given.
1934///
1935/// # Arguments
1936///
1937/// * `address` - the address it answers to.
1938/// * `shunt_milliohms` - the shunt, as in the driver's settings.
1939/// * `max_microamps` - the largest current, as in the driver's settings.
1940/// * `bus_microvolts` - the bus voltage it reports.
1941/// * `microamps` - the current it reports; negative flows the other way through the shunt.
1942///
1943/// # Returns
1944///
1945/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its readings
1946/// land on the steps its registers count in: 1.25 mV of bus, 2.5 uV of shunt, and the
1947/// calibration's current step.
1948#[no_mangle]
1949pub extern "C" fn pamoja_ina226_sim_reporting(
1950    address: u8,
1951    shunt_milliohms: u32,
1952    max_microamps: u32,
1953    bus_microvolts: u32,
1954    microamps: i32,
1955) -> *mut PamojaI2cPart {
1956    PamojaI2cPart::into_raw(ina226::sim::reporting(
1957        address,
1958        shunt_milliohms,
1959        max_microamps,
1960        bus_microvolts,
1961        microamps,
1962    ))
1963}
1964
1965/// Returns the settings an ADS1115 driver starts with, the part's own reset settings: AIN0
1966/// against AIN1, the 2.048 V range, and 128 samples per second.
1967///
1968/// # Returns
1969///
1970/// The settings.
1971#[no_mangle]
1972pub extern "C" fn pamoja_ads1115_settings_default() -> PamojaAds1115Settings {
1973    let config = ads1115::Config::default();
1974    PamojaAds1115Settings {
1975        mux: config.mux.code(),
1976        pga: config.pga.code(),
1977        data_rate: config.data_rate.code(),
1978    }
1979}
1980
1981/// Creates an ADS1115 driver on a bus. Nothing is sent until [`pamoja_ads1115_init`] or the
1982/// first [`pamoja_ads1115_sample`].
1983///
1984/// # Arguments
1985///
1986/// * `bus` - the bus the part is on; the driver holds its own share.
1987/// * `address` - one of the `PAMOJA_ADS1115_ADDRESS_` constants, by where ADDR is tied.
1988/// * `settings` - the input, the range, and the data rate.
1989/// * `out_sensor` - receives the driver.
1990///
1991/// # Returns
1992///
1993/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
1994/// for a null argument.
1995///
1996/// # Safety
1997///
1998/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
1999#[no_mangle]
2000pub unsafe extern "C" fn pamoja_ads1115_new(
2001    bus: *const PamojaI2cBus,
2002    address: u8,
2003    settings: PamojaAds1115Settings,
2004    out_sensor: *mut *mut PamojaAds1115,
2005) -> PamojaStatus {
2006    build(bus, out_sensor, |bus, delay| PamojaAds1115 {
2007        sensor: ads1115::Ads1115::new(bus, address, delay)
2008            .with_input(ads1115::Mux::from_code(settings.mux))
2009            .with_gain(ads1115::Pga::from_code(settings.pga))
2010            .with_data_rate(ads1115::DataRate::from_code(settings.data_rate)),
2011    })
2012}
2013
2014/// Writes the input, range, and data rate, and reads the configuration back, which is the
2015/// identity check a part with no id register allows.
2016///
2017/// # Arguments
2018///
2019/// * `sensor` - the driver.
2020///
2021/// # Returns
2022///
2023/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
2024/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when the configuration
2025/// reads back differently.
2026///
2027/// # Safety
2028///
2029/// `sensor` must be a live handle or null.
2030#[no_mangle]
2031pub unsafe extern "C" fn pamoja_ads1115_init(sensor: *mut PamojaAds1115) -> PamojaStatus {
2032    run(sensor, |held| held.sensor.init())
2033}
2034
2035/// Runs one conversion of the configured input, initializing the part first if
2036/// [`pamoja_ads1115_init`] has not run.
2037///
2038/// # Arguments
2039///
2040/// * `sensor` - the driver.
2041/// * `out_sample` - receives the conversion, with the range it ran at.
2042///
2043/// # Returns
2044///
2045/// As [`pamoja_ads1115_init`], with [`PamojaStatus::Io`] also when the part never reports the
2046/// conversion done.
2047///
2048/// # Safety
2049///
2050/// `sensor` must be a live handle or null, and `out_sample` a writable pointer or null.
2051#[no_mangle]
2052pub unsafe extern "C" fn pamoja_ads1115_sample(
2053    sensor: *mut PamojaAds1115,
2054    out_sample: *mut PamojaAds1115Sample,
2055) -> PamojaStatus {
2056    fill(
2057        sensor,
2058        out_sample,
2059        "out_sample",
2060        |held| held.sensor.sample(),
2061        sample_of,
2062    )
2063}
2064
2065/// Converts another input once, leaving the configured input as it was.
2066///
2067/// # Arguments
2068///
2069/// * `sensor` - the driver.
2070/// * `mux` - the input multiplexer code for this one conversion.
2071/// * `out_sample` - receives the conversion.
2072///
2073/// # Returns
2074///
2075/// As [`pamoja_ads1115_sample`].
2076///
2077/// # Safety
2078///
2079/// `sensor` must be a live handle or null, and `out_sample` a writable pointer or null.
2080#[no_mangle]
2081pub unsafe extern "C" fn pamoja_ads1115_sample_input(
2082    sensor: *mut PamojaAds1115,
2083    mux: u8,
2084    out_sample: *mut PamojaAds1115Sample,
2085) -> PamojaStatus {
2086    fill(
2087        sensor,
2088        out_sample,
2089        "out_sample",
2090        |held| held.sensor.sample_input(ads1115::Mux::from_code(mux)),
2091        sample_of,
2092    )
2093}
2094
2095/// Copies the configuration an ADS1115 driver writes.
2096///
2097/// # Arguments
2098///
2099/// * `sensor` - the driver.
2100/// * `out_config` - receives the configuration.
2101///
2102/// # Returns
2103///
2104/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
2105///
2106/// # Safety
2107///
2108/// `sensor` must be a live handle or null, and `out_config` a writable pointer or null.
2109#[no_mangle]
2110pub unsafe extern "C" fn pamoja_ads1115_config(
2111    sensor: *const PamojaAds1115,
2112    out_config: *mut PamojaAds1115Config,
2113) -> PamojaStatus {
2114    look(sensor, out_config, "out_config", |held| {
2115        held.sensor.config().into()
2116    })
2117}
2118
2119/// Releases a driver and its share of the bus. A null pointer is ignored.
2120///
2121/// # Safety
2122///
2123/// `sensor` must be a handle from [`pamoja_ads1115_new`] that has not been freed, or null.
2124#[no_mangle]
2125pub unsafe extern "C" fn pamoja_ads1115_free(sensor: *mut PamojaAds1115) {
2126    release(sensor);
2127}
2128
2129/// Creates a simulated ADS1115 reading [`PAMOJA_ADS1115_SIM_VOLTS`] at the range a driver
2130/// starts with.
2131///
2132/// # Arguments
2133///
2134/// * `address` - the address it answers to.
2135///
2136/// # Returns
2137///
2138/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
2139#[no_mangle]
2140pub extern "C" fn pamoja_ads1115_sim_part(address: u8) -> *mut PamojaI2cPart {
2141    PamojaI2cPart::into_raw(ads1115::sim::part(address))
2142}
2143
2144/// Creates a simulated ADS1115 that reads what it is asked to at the range a driver converts
2145/// at.
2146///
2147/// # Arguments
2148///
2149/// * `address` - the address it answers to.
2150/// * `pga` - the gain code the driver converts at.
2151/// * `volts` - the voltage it reports, held to the range.
2152///
2153/// # Returns
2154///
2155/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its reading
2156/// lands on the nearest of the 32768 steps either side of zero the range divides into.
2157#[no_mangle]
2158pub extern "C" fn pamoja_ads1115_sim_reporting(
2159    address: u8,
2160    pga: u8,
2161    volts: f32,
2162) -> *mut PamojaI2cPart {
2163    PamojaI2cPart::into_raw(ads1115::sim::reporting(
2164        address,
2165        ads1115::Pga::from_code(pga),
2166        volts,
2167    ))
2168}
2169
2170/// Creates an SHT3x driver on a bus. Nothing is sent until [`pamoja_sht3x_init`] or the first
2171/// [`pamoja_sht3x_measure`].
2172///
2173/// # Arguments
2174///
2175/// * `bus` - the bus the part is on; the driver holds its own share.
2176/// * `address` - `PAMOJA_SHT3X_I2C_ADDRESS_A` with ADDR low, `PAMOJA_SHT3X_I2C_ADDRESS_B` with
2177///   ADDR high.
2178/// * `repeatability` - `0` low, `1` medium, or `2` high, which trades noise against
2179///   measurement time.
2180/// * `out_sensor` - receives the driver.
2181///
2182/// # Returns
2183///
2184/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
2185/// for a null argument or a repeatability code the part does not have.
2186///
2187/// # Safety
2188///
2189/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
2190#[no_mangle]
2191pub unsafe extern "C" fn pamoja_sht3x_new(
2192    bus: *const PamojaI2cBus,
2193    address: u8,
2194    repeatability: u8,
2195    out_sensor: *mut *mut PamojaSht3x,
2196) -> PamojaStatus {
2197    let Some(repeatability) = repeatability_from_code(repeatability) else {
2198        return clear_then(out_sensor, crate::sensors::bad_repeatability());
2199    };
2200    build(bus, out_sensor, |bus, delay| PamojaSht3x {
2201        sensor: sht3x::Sht3x::new(bus, address, delay).with_repeatability(repeatability),
2202    })
2203}
2204
2205/// Soft-resets the part and reads its status; a status word whose CRC checks is what confirms
2206/// an SHT3x answers, as the part has no id register.
2207///
2208/// # Arguments
2209///
2210/// * `sensor` - the driver.
2211///
2212/// # Returns
2213///
2214/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
2215/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when the status word
2216/// fails its CRC.
2217///
2218/// # Safety
2219///
2220/// `sensor` must be a live handle or null.
2221#[no_mangle]
2222pub unsafe extern "C" fn pamoja_sht3x_init(sensor: *mut PamojaSht3x) -> PamojaStatus {
2223    run(sensor, |held| held.sensor.init())
2224}
2225
2226/// Runs one single-shot measurement, initializing the part first if [`pamoja_sht3x_init`] has
2227/// not run.
2228///
2229/// # Arguments
2230///
2231/// * `sensor` - the driver.
2232/// * `out_measurement` - receives the CRC-checked temperature and humidity.
2233///
2234/// # Returns
2235///
2236/// As [`pamoja_sht3x_init`], with [`PamojaStatus::Codec`] also when a data word fails its CRC.
2237///
2238/// # Safety
2239///
2240/// `sensor` must be a live handle or null, and `out_measurement` a writable pointer or null.
2241#[no_mangle]
2242pub unsafe extern "C" fn pamoja_sht3x_measure(
2243    sensor: *mut PamojaSht3x,
2244    out_measurement: *mut PamojaSht3xMeasurement,
2245) -> PamojaStatus {
2246    fill(
2247        sensor,
2248        out_measurement,
2249        "out_measurement",
2250        |held| held.sensor.measure(),
2251        PamojaSht3xMeasurement::from,
2252    )
2253}
2254
2255/// Reads the status register.
2256///
2257/// # Arguments
2258///
2259/// * `sensor` - the driver.
2260/// * `out_status` - receives the status.
2261///
2262/// # Returns
2263///
2264/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null argument,
2265/// [`PamojaStatus::Io`] when the bus fails, or [`PamojaStatus::Codec`] when the word fails its
2266/// CRC.
2267///
2268/// # Safety
2269///
2270/// `sensor` must be a live handle or null, and `out_status` a writable pointer or null.
2271#[no_mangle]
2272pub unsafe extern "C" fn pamoja_sht3x_read_status(
2273    sensor: *mut PamojaSht3x,
2274    out_status: *mut PamojaSht3xStatus,
2275) -> PamojaStatus {
2276    fill(
2277        sensor,
2278        out_status,
2279        "out_status",
2280        |held| held.sensor.read_status(),
2281        PamojaSht3xStatus::from,
2282    )
2283}
2284
2285/// Reports the status an SHT3x driver last read, without reading it again.
2286///
2287/// # Arguments
2288///
2289/// * `sensor` - the driver.
2290/// * `out_status` - receives the status.
2291///
2292/// # Returns
2293///
2294/// `true` with the status in `out_status`; `false` before the status has been read, or for a
2295/// null argument.
2296///
2297/// # Safety
2298///
2299/// `sensor` must be a live handle or null, and `out_status` a writable pointer or null.
2300#[no_mangle]
2301pub unsafe extern "C" fn pamoja_sht3x_last_status(
2302    sensor: *const PamojaSht3x,
2303    out_status: *mut PamojaSht3xStatus,
2304) -> bool {
2305    let Some(held) = sensor.as_ref() else {
2306        return false;
2307    };
2308    store(out_status, held.sensor.status().map(Into::into))
2309}
2310
2311/// Switches the plausibility-check heater on, initializing the part first if needed, since
2312/// the reset in initialization switches it off.
2313///
2314/// # Arguments
2315///
2316/// * `sensor` - the driver.
2317///
2318/// # Returns
2319///
2320/// As [`pamoja_sht3x_init`].
2321///
2322/// # Safety
2323///
2324/// `sensor` must be a live handle or null.
2325#[no_mangle]
2326pub unsafe extern "C" fn pamoja_sht3x_heater_on(sensor: *mut PamojaSht3x) -> PamojaStatus {
2327    run(sensor, |held| held.sensor.heater_on())
2328}
2329
2330/// Switches the heater off, which is its state after any reset.
2331///
2332/// # Arguments
2333///
2334/// * `sensor` - the driver.
2335///
2336/// # Returns
2337///
2338/// As [`pamoja_sht3x_init`].
2339///
2340/// # Safety
2341///
2342/// `sensor` must be a live handle or null.
2343#[no_mangle]
2344pub unsafe extern "C" fn pamoja_sht3x_heater_off(sensor: *mut PamojaSht3x) -> PamojaStatus {
2345    run(sensor, |held| held.sensor.heater_off())
2346}
2347
2348/// Releases a driver and its share of the bus. A null pointer is ignored.
2349///
2350/// # Safety
2351///
2352/// `sensor` must be a handle from [`pamoja_sht3x_new`] that has not been freed, or null.
2353#[no_mangle]
2354pub unsafe extern "C" fn pamoja_sht3x_free(sensor: *mut PamojaSht3x) {
2355    release(sensor);
2356}
2357
2358/// Creates a simulated SHT3x reading [`PAMOJA_SHT3X_SIM_CELSIUS`] and
2359/// [`PAMOJA_SHT3X_SIM_RELATIVE_HUMIDITY`].
2360///
2361/// # Arguments
2362///
2363/// * `address` - the address it answers to.
2364///
2365/// # Returns
2366///
2367/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
2368#[no_mangle]
2369pub extern "C" fn pamoja_sht3x_sim_part(address: u8) -> *mut PamojaI2cPart {
2370    PamojaI2cPart::into_raw(sht3x::sim::part(address))
2371}
2372
2373/// Creates a simulated SHT3x that reads what it is asked to. It answers every single-shot
2374/// command, a periodic fetch, and the status command with the replies a real part gives.
2375///
2376/// # Arguments
2377///
2378/// * `address` - the address it answers to.
2379/// * `celsius` - the temperature it reports.
2380/// * `relative_humidity` - the humidity it reports, as a percentage.
2381///
2382/// # Returns
2383///
2384/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`]. Its readings
2385/// land within three thousandths of a degree and two thousandths of a percent.
2386#[no_mangle]
2387pub extern "C" fn pamoja_sht3x_sim_reporting(
2388    address: u8,
2389    celsius: f32,
2390    relative_humidity: f32,
2391) -> *mut PamojaI2cPart {
2392    PamojaI2cPart::into_raw(sht3x::sim::reporting(address, celsius, relative_humidity))
2393}
2394
2395/// Creates an SCD40 or SCD41 driver on a bus; the part has one address. Nothing is sent until
2396/// [`pamoja_scd4x_init`] or the first [`pamoja_scd4x_measure`].
2397///
2398/// # Arguments
2399///
2400/// * `bus` - the bus the part is on; the driver holds its own share.
2401/// * `out_sensor` - receives the driver.
2402///
2403/// # Returns
2404///
2405/// [`PamojaStatus::Ok`] with the driver in `out_sensor`, or [`PamojaStatus::InvalidArgument`]
2406/// for a null argument.
2407///
2408/// # Safety
2409///
2410/// `bus` must be a live handle or null, and `out_sensor` a writable pointer or null.
2411#[no_mangle]
2412pub unsafe extern "C" fn pamoja_scd4x_new(
2413    bus: *const PamojaI2cBus,
2414    out_sensor: *mut *mut PamojaScd4x,
2415) -> PamojaStatus {
2416    build(bus, out_sensor, |bus, delay| PamojaScd4x {
2417        sensor: scd4x::Scd4x::new(bus, delay),
2418    })
2419}
2420
2421/// Stops any running measurement, reads the serial number, and starts periodic measurement,
2422/// after which the part has a new result every five seconds.
2423///
2424/// # Arguments
2425///
2426/// * `sensor` - the driver.
2427///
2428/// # Returns
2429///
2430/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null driver;
2431/// [`PamojaStatus::Io`] when the bus fails; or [`PamojaStatus::Codec`] when the serial number
2432/// fails its CRC.
2433///
2434/// # Safety
2435///
2436/// `sensor` must be a live handle or null.
2437#[no_mangle]
2438pub unsafe extern "C" fn pamoja_scd4x_init(sensor: *mut PamojaScd4x) -> PamojaStatus {
2439    run(sensor, |held| held.sensor.init())
2440}
2441
2442/// Waits for the next periodic result and reads it, initializing the part first if
2443/// [`pamoja_scd4x_init`] has not run.
2444///
2445/// # Arguments
2446///
2447/// * `sensor` - the driver.
2448/// * `out_measurement` - receives the carbon dioxide, temperature, and humidity.
2449///
2450/// # Returns
2451///
2452/// As [`pamoja_scd4x_init`], with [`PamojaStatus::Io`] also when no result becomes ready and
2453/// [`PamojaStatus::Codec`] when a word fails its CRC.
2454///
2455/// # Safety
2456///
2457/// `sensor` must be a live handle or null, and `out_measurement` a writable pointer or null.
2458#[no_mangle]
2459pub unsafe extern "C" fn pamoja_scd4x_measure(
2460    sensor: *mut PamojaScd4x,
2461    out_measurement: *mut PamojaScd4xMeasurement,
2462) -> PamojaStatus {
2463    fill(
2464        sensor,
2465        out_measurement,
2466        "out_measurement",
2467        |held| held.sensor.measure(),
2468        PamojaScd4xMeasurement::from,
2469    )
2470}
2471
2472/// Runs one on-demand measurement on an SCD41, which takes five seconds. The part must not be
2473/// measuring periodically: call [`pamoja_scd4x_stop`] first, or use this in place of
2474/// [`pamoja_scd4x_init`].
2475///
2476/// # Arguments
2477///
2478/// * `sensor` - the driver.
2479/// * `out_measurement` - receives the measurement.
2480///
2481/// # Returns
2482///
2483/// As [`pamoja_scd4x_measure`].
2484///
2485/// # Safety
2486///
2487/// `sensor` must be a live handle or null, and `out_measurement` a writable pointer or null.
2488#[no_mangle]
2489pub unsafe extern "C" fn pamoja_scd4x_measure_single_shot(
2490    sensor: *mut PamojaScd4x,
2491    out_measurement: *mut PamojaScd4xMeasurement,
2492) -> PamojaStatus {
2493    fill(
2494        sensor,
2495        out_measurement,
2496        "out_measurement",
2497        |held| held.sensor.measure_single_shot(),
2498        PamojaScd4xMeasurement::from,
2499    )
2500}
2501
2502/// Asks the part whether a periodic result is waiting.
2503///
2504/// # Arguments
2505///
2506/// * `sensor` - the driver.
2507/// * `out_ready` - receives `true` when [`pamoja_scd4x_measure`] would read without waiting.
2508///
2509/// # Returns
2510///
2511/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null argument,
2512/// [`PamojaStatus::Io`] when the bus fails, or [`PamojaStatus::Codec`] when the status word
2513/// fails its CRC.
2514///
2515/// # Safety
2516///
2517/// `sensor` must be a live handle or null, and `out_ready` a writable pointer or null.
2518#[no_mangle]
2519pub unsafe extern "C" fn pamoja_scd4x_poll_ready(
2520    sensor: *mut PamojaScd4x,
2521    out_ready: *mut bool,
2522) -> PamojaStatus {
2523    fill(
2524        sensor,
2525        out_ready,
2526        "out_ready",
2527        |held| held.sensor.data_ready(),
2528        |ready| ready,
2529    )
2530}
2531
2532/// Stops periodic measurement, after which the part takes its settings commands.
2533///
2534/// # Arguments
2535///
2536/// * `sensor` - the driver.
2537///
2538/// # Returns
2539///
2540/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null driver, or
2541/// [`PamojaStatus::Io`] when the bus fails.
2542///
2543/// # Safety
2544///
2545/// `sensor` must be a live handle or null.
2546#[no_mangle]
2547pub unsafe extern "C" fn pamoja_scd4x_stop(sensor: *mut PamojaScd4x) -> PamojaStatus {
2548    run(sensor, |held| held.sensor.stop())
2549}
2550
2551/// Starts periodic measurement.
2552///
2553/// # Arguments
2554///
2555/// * `sensor` - the driver.
2556///
2557/// # Returns
2558///
2559/// As [`pamoja_scd4x_stop`].
2560///
2561/// # Safety
2562///
2563/// `sensor` must be a live handle or null.
2564#[no_mangle]
2565pub unsafe extern "C" fn pamoja_scd4x_start(sensor: *mut PamojaScd4x) -> PamojaStatus {
2566    run(sensor, |held| held.sensor.start())
2567}
2568
2569/// Sets the temperature offset that compensates the part's own warmth, stopping periodic
2570/// measurement to write it and starting it again. The setting lasts until power is lost.
2571///
2572/// # Arguments
2573///
2574/// * `sensor` - the driver.
2575/// * `milli_celsius` - the offset to subtract, in millidegrees.
2576///
2577/// # Returns
2578///
2579/// As [`pamoja_scd4x_stop`].
2580///
2581/// # Safety
2582///
2583/// `sensor` must be a live handle or null.
2584#[no_mangle]
2585pub unsafe extern "C" fn pamoja_scd4x_set_temperature_offset(
2586    sensor: *mut PamojaScd4x,
2587    milli_celsius: u32,
2588) -> PamojaStatus {
2589    run(sensor, |held| {
2590        held.sensor.set_temperature_offset(milli_celsius)
2591    })
2592}
2593
2594/// Sets the altitude the part corrects its carbon dioxide reading for, stopping periodic
2595/// measurement to write it and starting it again.
2596///
2597/// # Arguments
2598///
2599/// * `sensor` - the driver.
2600/// * `meters` - the altitude above sea level.
2601///
2602/// # Returns
2603///
2604/// As [`pamoja_scd4x_stop`].
2605///
2606/// # Safety
2607///
2608/// `sensor` must be a live handle or null.
2609#[no_mangle]
2610pub unsafe extern "C" fn pamoja_scd4x_set_sensor_altitude(
2611    sensor: *mut PamojaScd4x,
2612    meters: u16,
2613) -> PamojaStatus {
2614    run(sensor, |held| held.sensor.set_sensor_altitude(meters))
2615}
2616
2617/// Reports the serial number an SCD4x driver read at initialization.
2618///
2619/// # Arguments
2620///
2621/// * `sensor` - the driver.
2622/// * `out_serial` - receives the 48-bit serial number.
2623///
2624/// # Returns
2625///
2626/// `true` with the serial in `out_serial`; `false` before the part has been initialized, or
2627/// for a null argument.
2628///
2629/// # Safety
2630///
2631/// `sensor` must be a live handle or null, and `out_serial` a writable pointer or null.
2632#[no_mangle]
2633pub unsafe extern "C" fn pamoja_scd4x_serial(
2634    sensor: *const PamojaScd4x,
2635    out_serial: *mut u64,
2636) -> bool {
2637    let Some(held) = sensor.as_ref() else {
2638        return false;
2639    };
2640    store(out_serial, held.sensor.serial())
2641}
2642
2643/// Releases a driver and its share of the bus. A null pointer is ignored.
2644///
2645/// # Safety
2646///
2647/// `sensor` must be a handle from [`pamoja_scd4x_new`] that has not been freed, or null.
2648#[no_mangle]
2649pub unsafe extern "C" fn pamoja_scd4x_free(sensor: *mut PamojaScd4x) {
2650    release(sensor);
2651}
2652
2653/// Creates a simulated SCD4x reading [`PAMOJA_SCD4X_SIM_CO2_PPM`],
2654/// [`PAMOJA_SCD4X_SIM_CELSIUS`], and [`PAMOJA_SCD4X_SIM_RELATIVE_HUMIDITY`].
2655///
2656/// # Returns
2657///
2658/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
2659#[no_mangle]
2660pub extern "C" fn pamoja_scd4x_sim_part() -> *mut PamojaI2cPart {
2661    PamojaI2cPart::into_raw(scd4x::sim::part())
2662}
2663
2664/// Creates a simulated SCD4x that reads what it is asked to. It answers the serial number,
2665/// data-ready, and measurement commands, and always has a result waiting.
2666///
2667/// # Arguments
2668///
2669/// * `co2_ppm` - the carbon dioxide it reports, in parts per million.
2670/// * `celsius` - the temperature it reports.
2671/// * `relative_humidity` - the humidity it reports, as a percentage.
2672///
2673/// # Returns
2674///
2675/// The part, which the caller releases with [`crate::hal::pamoja_i2c_part_free`].
2676#[no_mangle]
2677pub extern "C" fn pamoja_scd4x_sim_reporting(
2678    co2_ppm: u16,
2679    celsius: f32,
2680    relative_humidity: f32,
2681) -> *mut PamojaI2cPart {
2682    PamojaI2cPart::into_raw(scd4x::sim::reporting(co2_ppm, celsius, relative_humidity))
2683}
2684
2685/// Names a DS18B20 the kernel serves by the serial in its directory name, reading
2686/// `/sys/bus/w1/devices/28-<serial>/w1_slave`.
2687///
2688/// # Arguments
2689///
2690/// * `serial` - the twelve hex digits after `28-`.
2691/// * `out_thermometer` - receives the thermometer.
2692///
2693/// # Returns
2694///
2695/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null or non-UTF-8
2696/// argument. Nothing is read until [`pamoja_ds18b20_thermometer_read`].
2697///
2698/// # Safety
2699///
2700/// `serial` must be a null-terminated string or null, and `out_thermometer` a writable pointer
2701/// or null.
2702#[no_mangle]
2703pub unsafe extern "C" fn pamoja_ds18b20_thermometer_new(
2704    serial: *const c_char,
2705    out_thermometer: *mut *mut PamojaDs18b20Thermometer,
2706) -> PamojaStatus {
2707    thermometer_from(
2708        serial,
2709        "serial",
2710        out_thermometer,
2711        ds18b20::linux::Thermometer::new,
2712    )
2713}
2714
2715/// Names a DS18B20 by the path of its `w1_slave` file, for a system that mounts sysfs elsewhere
2716/// or a test that writes the file itself.
2717///
2718/// # Arguments
2719///
2720/// * `path` - the file to read.
2721/// * `out_thermometer` - receives the thermometer.
2722///
2723/// # Returns
2724///
2725/// As [`pamoja_ds18b20_thermometer_new`].
2726///
2727/// # Safety
2728///
2729/// `path` must be a null-terminated string or null, and `out_thermometer` a writable pointer or
2730/// null.
2731#[no_mangle]
2732pub unsafe extern "C" fn pamoja_ds18b20_thermometer_at(
2733    path: *const c_char,
2734    out_thermometer: *mut *mut PamojaDs18b20Thermometer,
2735) -> PamojaStatus {
2736    thermometer_from(path, "path", out_thermometer, |path| {
2737        ds18b20::linux::Thermometer::at(path)
2738    })
2739}
2740
2741/// Returns the path of the file a thermometer reads.
2742///
2743/// # Arguments
2744///
2745/// * `thermometer` - the thermometer.
2746///
2747/// # Returns
2748///
2749/// The path, which the caller releases with [`crate::pamoja_string_free`], or null for a null
2750/// thermometer.
2751///
2752/// # Safety
2753///
2754/// `thermometer` must be a live handle or null.
2755#[no_mangle]
2756pub unsafe extern "C" fn pamoja_ds18b20_thermometer_path(
2757    thermometer: *const PamojaDs18b20Thermometer,
2758) -> *mut PamojaString {
2759    match thermometer.as_ref() {
2760        Some(held) => {
2761            PamojaString::into_raw(held.thermometer.path().to_string_lossy().into_owned())
2762        }
2763        None => std::ptr::null_mut(),
2764    }
2765}
2766
2767/// Returns the serial the kernel named a thermometer's directory after.
2768///
2769/// # Arguments
2770///
2771/// * `thermometer` - the thermometer.
2772///
2773/// # Returns
2774///
2775/// The twelve hex digits after `28-`, which the caller releases with
2776/// [`crate::pamoja_string_free`], or null when the file does not sit in a DS18B20's
2777/// directory or the thermometer is null.
2778///
2779/// # Safety
2780///
2781/// `thermometer` must be a live handle or null.
2782#[no_mangle]
2783pub unsafe extern "C" fn pamoja_ds18b20_thermometer_serial(
2784    thermometer: *const PamojaDs18b20Thermometer,
2785) -> *mut PamojaString {
2786    match thermometer
2787        .as_ref()
2788        .and_then(|held| held.thermometer.serial())
2789    {
2790        Some(serial) => PamojaString::into_raw(serial.to_owned()),
2791        None => std::ptr::null_mut(),
2792    }
2793}
2794
2795/// Reads a thermometer's file, which makes the kernel run a conversion, and decodes it.
2796///
2797/// # Arguments
2798///
2799/// * `thermometer` - the thermometer.
2800/// * `out_reading` - receives the CRC-checked reading.
2801///
2802/// # Returns
2803///
2804/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument;
2805/// [`PamojaStatus::Io`] when the file cannot be read, because the 1-Wire overlay is off, the
2806/// probe is gone, or the process may not read it; or [`PamojaStatus::Codec`] when the kernel or
2807/// this decoder rejects the CRC.
2808///
2809/// # Safety
2810///
2811/// `thermometer` must be a live handle or null, and `out_reading` a writable pointer or null.
2812#[no_mangle]
2813pub unsafe extern "C" fn pamoja_ds18b20_thermometer_read(
2814    thermometer: *const PamojaDs18b20Thermometer,
2815    out_reading: *mut PamojaDs18b20Reading,
2816) -> PamojaStatus {
2817    if out_reading.is_null() {
2818        set_last_error("out_reading must not be null".to_owned());
2819        return PamojaStatus::InvalidArgument;
2820    }
2821    let Some(held) = thermometer.as_ref() else {
2822        set_last_error("thermometer must not be null".to_owned());
2823        return PamojaStatus::InvalidArgument;
2824    };
2825    match catch_unwind(AssertUnwindSafe(|| held.thermometer.read_scratchpad())) {
2826        Ok(Ok(scratchpad)) => {
2827            *out_reading = scratchpad.into();
2828            PamojaStatus::Ok
2829        }
2830        Ok(Err(error)) => {
2831            set_last_error(error.to_string());
2832            match error {
2833                ds18b20::linux::ThermometerError::Io(_) => PamojaStatus::Io,
2834                ds18b20::linux::ThermometerError::Sensor(_) => PamojaStatus::Codec,
2835            }
2836        }
2837        Err(_) => panicked(),
2838    }
2839}
2840
2841/// Releases a thermometer. A null pointer is ignored.
2842///
2843/// # Safety
2844///
2845/// `thermometer` must be a handle that has not been freed, or null.
2846#[no_mangle]
2847pub unsafe extern "C" fn pamoja_ds18b20_thermometer_free(
2848    thermometer: *mut PamojaDs18b20Thermometer,
2849) {
2850    release(thermometer);
2851}
2852
2853/// Lists every DS18B20 the kernel has found: one per `28-` directory.
2854///
2855/// # Arguments
2856///
2857/// * `devices` - the directory the kernel lists its 1-Wire devices in, or null for
2858///   `/sys/bus/w1/devices`.
2859/// * `out_thermometers` - receives the list, sorted by directory name.
2860///
2861/// # Returns
2862///
2863/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null `out_thermometers` or a
2864/// non-UTF-8 directory; or [`PamojaStatus::Io`] when the directory cannot be listed, which
2865/// usually means the 1-Wire overlay is off.
2866///
2867/// # Safety
2868///
2869/// `devices` must be a null-terminated string or null, and `out_thermometers` a writable
2870/// pointer or null.
2871#[no_mangle]
2872pub unsafe extern "C" fn pamoja_ds18b20_discover(
2873    devices: *const c_char,
2874    out_thermometers: *mut *mut PamojaDs18b20Thermometers,
2875) -> PamojaStatus {
2876    if out_thermometers.is_null() {
2877        set_last_error("out_thermometers must not be null".to_owned());
2878        return PamojaStatus::InvalidArgument;
2879    }
2880    *out_thermometers = std::ptr::null_mut();
2881    let devices = if devices.is_null() {
2882        ds18b20::linux::DEVICES
2883    } else {
2884        match read_str(devices, "devices") {
2885            Some(devices) => devices,
2886            None => return PamojaStatus::InvalidArgument,
2887        }
2888    };
2889    match ds18b20::linux::Thermometer::discover_in(Path::new(devices)) {
2890        Ok(found) => {
2891            *out_thermometers = Box::into_raw(Box::new(PamojaDs18b20Thermometers { found }));
2892            PamojaStatus::Ok
2893        }
2894        Err(error) => {
2895            set_last_error(format!("{devices}: {error}"));
2896            PamojaStatus::Io
2897        }
2898    }
2899}
2900
2901/// Returns how many thermometers a list holds.
2902///
2903/// # Arguments
2904///
2905/// * `thermometers` - the list.
2906///
2907/// # Returns
2908///
2909/// The count, or 0 for a null list.
2910///
2911/// # Safety
2912///
2913/// `thermometers` must be a live handle or null.
2914#[no_mangle]
2915pub unsafe extern "C" fn pamoja_ds18b20_thermometers_len(
2916    thermometers: *const PamojaDs18b20Thermometers,
2917) -> usize {
2918    thermometers.as_ref().map_or(0, |held| held.found.len())
2919}
2920
2921/// Copies one thermometer out of a list.
2922///
2923/// # Arguments
2924///
2925/// * `thermometers` - the list.
2926/// * `index` - which one, below [`pamoja_ds18b20_thermometers_len`].
2927///
2928/// # Returns
2929///
2930/// A new thermometer, which the caller releases with [`pamoja_ds18b20_thermometer_free`], or
2931/// null for a null list or an index past the end.
2932///
2933/// # Safety
2934///
2935/// `thermometers` must be a live handle or null.
2936#[no_mangle]
2937pub unsafe extern "C" fn pamoja_ds18b20_thermometers_get(
2938    thermometers: *const PamojaDs18b20Thermometers,
2939    index: usize,
2940) -> *mut PamojaDs18b20Thermometer {
2941    match thermometers.as_ref().and_then(|held| held.found.get(index)) {
2942        Some(thermometer) => Box::into_raw(Box::new(PamojaDs18b20Thermometer {
2943            thermometer: thermometer.clone(),
2944        })),
2945        None => std::ptr::null_mut(),
2946    }
2947}
2948
2949/// Releases a list of thermometers. A null pointer is ignored.
2950///
2951/// # Safety
2952///
2953/// `thermometers` must be a handle that has not been freed, or null.
2954#[no_mangle]
2955pub unsafe extern "C" fn pamoja_ds18b20_thermometers_free(
2956    thermometers: *mut PamojaDs18b20Thermometers,
2957) {
2958    release(thermometers);
2959}
2960
2961/// An ADS1115 sample as it crosses the boundary.
2962fn sample_of(sample: ads1115::Sample) -> PamojaAds1115Sample {
2963    PamojaAds1115Sample {
2964        raw: sample.raw,
2965        pga: sample.pga.code(),
2966        nanovolts: sample.nanovolts(),
2967        volts: sample.volts(),
2968        clipped: u8::from(sample.clipped()),
2969    }
2970}
2971
2972/// Builds a driver on the caller's bus and hands it back through `out`.
2973///
2974/// # Safety
2975///
2976/// `bus` must be a live handle or null, and `out` a writable pointer or null.
2977unsafe fn build<H>(
2978    bus: *const PamojaI2cBus,
2979    out: *mut *mut H,
2980    make: impl FnOnce(I2cBus, BusDelay) -> H,
2981) -> PamojaStatus {
2982    if out.is_null() {
2983        set_last_error("out_sensor must not be null".to_owned());
2984        return PamojaStatus::InvalidArgument;
2985    }
2986    *out = std::ptr::null_mut();
2987    let Some(bus) = bus.as_ref() else {
2988        set_last_error("bus must not be null".to_owned());
2989        return PamojaStatus::InvalidArgument;
2990    };
2991    let bus = bus.bus.clone();
2992    let delay = bus.delay();
2993    *out = Box::into_raw(Box::new(make(bus, delay)));
2994    PamojaStatus::Ok
2995}
2996
2997/// Clears a caller's out pointer before returning a refusal that built nothing.
2998///
2999/// # Safety
3000///
3001/// `out` must be a writable pointer or null.
3002unsafe fn clear_then<H>(out: *mut *mut H, status: PamojaStatus) -> PamojaStatus {
3003    if !out.is_null() {
3004        *out = std::ptr::null_mut();
3005    }
3006    status
3007}
3008
3009/// Names a DS18B20 from a string argument and hands it back through `out`.
3010///
3011/// # Safety
3012///
3013/// `text` must be a null-terminated string or null, and `out` a writable pointer or null.
3014unsafe fn thermometer_from(
3015    text: *const c_char,
3016    what: &str,
3017    out: *mut *mut PamojaDs18b20Thermometer,
3018    make: impl FnOnce(&str) -> ds18b20::linux::Thermometer,
3019) -> PamojaStatus {
3020    if out.is_null() {
3021        set_last_error("out_thermometer must not be null".to_owned());
3022        return PamojaStatus::InvalidArgument;
3023    }
3024    *out = std::ptr::null_mut();
3025    let Some(text) = read_str(text, what) else {
3026        return PamojaStatus::InvalidArgument;
3027    };
3028    *out = Box::into_raw(Box::new(PamojaDs18b20Thermometer {
3029        thermometer: make(text),
3030    }));
3031    PamojaStatus::Ok
3032}
3033
3034/// Runs a driver call that returns nothing, turning a failure or a panic into a status.
3035///
3036/// # Safety
3037///
3038/// `handle` must be a live handle or null.
3039unsafe fn run<H, E: Debug + Display>(
3040    handle: *mut H,
3041    call: impl FnOnce(&mut H) -> Result<(), DriverError<E>>,
3042) -> PamojaStatus {
3043    match on_driver(handle, call) {
3044        Ok(()) => PamojaStatus::Ok,
3045        Err(status) => status,
3046    }
3047}
3048
3049/// Runs a driver call and writes what it returns through `out`, converted for the boundary.
3050///
3051/// # Safety
3052///
3053/// `handle` must be a live handle or null, and `out` a writable pointer or null.
3054unsafe fn fill<H, T, R, E: Debug + Display>(
3055    handle: *mut H,
3056    out: *mut R,
3057    name: &str,
3058    call: impl FnOnce(&mut H) -> Result<T, DriverError<E>>,
3059    convert: impl FnOnce(T) -> R,
3060) -> PamojaStatus {
3061    if out.is_null() {
3062        set_last_error(format!("{name} must not be null"));
3063        return PamojaStatus::InvalidArgument;
3064    }
3065    match on_driver(handle, call) {
3066        Ok(value) => {
3067            *out = convert(value);
3068            PamojaStatus::Ok
3069        }
3070        Err(status) => status,
3071    }
3072}
3073
3074/// Writes something a driver holds, without touching the bus, through `out`.
3075///
3076/// # Safety
3077///
3078/// `handle` must be a live handle or null, and `out` a writable pointer or null.
3079unsafe fn look<H, R>(
3080    handle: *const H,
3081    out: *mut R,
3082    name: &str,
3083    read: impl FnOnce(&H) -> R,
3084) -> PamojaStatus {
3085    if out.is_null() {
3086        set_last_error(format!("{name} must not be null"));
3087        return PamojaStatus::InvalidArgument;
3088    }
3089    let Some(held) = handle.as_ref() else {
3090        set_last_error("sensor must not be null".to_owned());
3091        return PamojaStatus::InvalidArgument;
3092    };
3093    *out = read(held);
3094    PamojaStatus::Ok
3095}
3096
3097/// Writes a value a driver may not have yet through `out`, reporting whether it had one.
3098///
3099/// # Safety
3100///
3101/// `out` must be a writable pointer or null.
3102unsafe fn store<R>(out: *mut R, value: Option<R>) -> bool {
3103    match (out.is_null(), value) {
3104        (false, Some(value)) => {
3105            *out = value;
3106            true
3107        }
3108        _ => false,
3109    }
3110}
3111
3112/// Copies a block of bytes into a caller's buffer of exactly that size.
3113///
3114/// # Safety
3115///
3116/// `out` must point to `bytes.len()` writable bytes, or be null.
3117unsafe fn copy_out(out: *mut u8, name: &str, bytes: &[u8]) -> PamojaStatus {
3118    if out.is_null() {
3119        set_last_error(format!("{name} must not be null"));
3120        return PamojaStatus::InvalidArgument;
3121    }
3122    std::slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(bytes);
3123    PamojaStatus::Ok
3124}
3125
3126/// Releases a boxed handle; a null pointer is ignored.
3127///
3128/// # Safety
3129///
3130/// `handle` must be a boxed handle that has not been freed, or null.
3131unsafe fn release<H>(handle: *mut H) {
3132    if !handle.is_null() {
3133        drop(Box::from_raw(handle));
3134    }
3135}
3136
3137/// Runs a driver call, turning a failure or a panic into a status.
3138///
3139/// # Safety
3140///
3141/// `handle` must be a live handle or null.
3142unsafe fn on_driver<H, T, E: Debug + Display>(
3143    handle: *mut H,
3144    call: impl FnOnce(&mut H) -> Result<T, DriverError<E>>,
3145) -> Result<T, PamojaStatus> {
3146    let Some(held) = handle.as_mut() else {
3147        set_last_error("sensor must not be null".to_owned());
3148        return Err(PamojaStatus::InvalidArgument);
3149    };
3150    match catch_unwind(AssertUnwindSafe(|| call(held))) {
3151        Ok(Ok(value)) => Ok(value),
3152        Ok(Err(error)) => Err(driver_failed(&error)),
3153        Err(_) => Err(panicked()),
3154    }
3155}
3156
3157/// Records why a driver failed, in the bus's own words when the bus was the cause, and maps
3158/// it onto its status.
3159fn driver_failed<E: Debug + Display>(error: &DriverError<E>) -> PamojaStatus {
3160    match error {
3161        DriverError::Bus(error) => {
3162            set_last_error(error.to_string());
3163            PamojaStatus::Io
3164        }
3165        DriverError::Timeout => {
3166            set_last_error(error.to_string());
3167            PamojaStatus::Io
3168        }
3169        DriverError::Sensor(sensor) => {
3170            set_last_error(sensor.to_string());
3171            PamojaStatus::Codec
3172        }
3173    }
3174}
3175
3176/// Records a caught panic and reports it as [`PamojaStatus::Panic`].
3177fn panicked() -> PamojaStatus {
3178    set_last_error("panic at the FFI boundary".to_owned());
3179    PamojaStatus::Panic
3180}
3181
3182#[cfg(test)]
3183mod tests {
3184    use super::*;
3185    use crate::hal::{
3186        pamoja_i2c_bus_attach, pamoja_i2c_bus_free, pamoja_i2c_bus_part, pamoja_i2c_bus_simulated,
3187        pamoja_i2c_bus_waited_micros, pamoja_i2c_part_free, pamoja_i2c_part_load,
3188        pamoja_i2c_part_new, pamoja_i2c_part_received, pamoja_i2c_part_received_count,
3189        pamoja_i2c_part_register, pamoja_i2c_part_word,
3190    };
3191    use crate::sensors::{
3192        pamoja_bme280_ctrl_hum_from_bits, pamoja_bme280_ctrl_meas_from_bits,
3193        pamoja_bme280_max_measurement_micros, PamojaBme280CtrlMeas,
3194        PAMOJA_BME280_I2C_ADDRESS_PRIMARY, PAMOJA_BME280_REGISTER_CHIP_ID,
3195        PAMOJA_BME280_REGISTER_CTRL_HUM, PAMOJA_BME280_REGISTER_CTRL_MEAS,
3196    };
3197    use std::ffi::{CStr, CString};
3198    use std::ptr;
3199
3200    const PART: u8 = PAMOJA_BME280_I2C_ADDRESS_PRIMARY;
3201
3202    fn last_error() -> String {
3203        let message = crate::pamoja_last_error_message();
3204        if message.is_null() {
3205            return String::new();
3206        }
3207        unsafe { CStr::from_ptr(message) }
3208            .to_string_lossy()
3209            .into_owned()
3210    }
3211
3212    fn measurement() -> PamojaBme280Measurement {
3213        PamojaBme280Measurement {
3214            celsius: 0.0,
3215            pascals: 0,
3216            hectopascals: 0.0,
3217            relative_humidity_percent: 0.0,
3218        }
3219    }
3220
3221    /// A simulated bus holding one part, with the part's own handle already freed.
3222    unsafe fn bus_with(part: *mut PamojaI2cPart) -> *mut PamojaI2cBus {
3223        let bus = pamoja_i2c_bus_simulated();
3224        attach(bus, part);
3225        bus
3226    }
3227
3228    /// Puts a part on a simulated bus and frees the part's own handle.
3229    unsafe fn attach(bus: *mut PamojaI2cBus, part: *mut PamojaI2cPart) {
3230        assert_eq!(pamoja_i2c_bus_attach(bus, part), PamojaStatus::Ok);
3231        pamoja_i2c_part_free(part);
3232    }
3233
3234    #[test]
3235    fn a_driver_measures_a_simulated_part_and_leaves_it_configured() {
3236        unsafe {
3237            let bus = bus_with(pamoja_bme280_sim_part(PART));
3238            let mut sensor = ptr::null_mut();
3239            assert_eq!(
3240                pamoja_bme280_new(bus, PART, pamoja_bme280_settings_default(), &mut sensor),
3241                PamojaStatus::Ok
3242            );
3243            let mut reading = measurement();
3244            assert_eq!(
3245                pamoja_bme280_measure(sensor, &mut reading),
3246                PamojaStatus::Ok
3247            );
3248            assert_eq!(reading.celsius, 20.44);
3249            assert_eq!(reading.pascals, 84_805);
3250
3251            let held = pamoja_i2c_bus_part(bus, PART);
3252            let mut ctrl = PamojaBme280CtrlMeas {
3253                temperature: 0,
3254                pressure: 0,
3255                mode: 0,
3256            };
3257            assert_eq!(
3258                pamoja_bme280_ctrl_meas_from_bits(
3259                    pamoja_i2c_part_register(held, PAMOJA_BME280_REGISTER_CTRL_MEAS),
3260                    &mut ctrl
3261                ),
3262                PamojaStatus::Ok
3263            );
3264            assert_eq!(
3265                ctrl.mode,
3266                bme280::Mode::Forced.code(),
3267                "the last write forced it"
3268            );
3269            assert_eq!(
3270                pamoja_bme280_ctrl_hum_from_bits(pamoja_i2c_part_register(
3271                    held,
3272                    PAMOJA_BME280_REGISTER_CTRL_HUM
3273                )),
3274                bme280::Oversampling::X1.code()
3275            );
3276            pamoja_i2c_part_free(held);
3277
3278            assert_eq!(
3279                pamoja_i2c_bus_waited_micros(bus),
3280                u64::from(bme280::STARTUP_MICROS)
3281                    + u64::from(pamoja_bme280_max_measurement_micros(1, 1, 1)),
3282                "the reset's start-up time and one measurement's"
3283            );
3284            pamoja_bme280_free(sensor);
3285            pamoja_i2c_bus_free(bus);
3286        }
3287    }
3288
3289    #[test]
3290    fn a_part_asked_for_a_reading_reports_it() {
3291        unsafe {
3292            let bus = bus_with(pamoja_bme280_sim_reporting(PART, 4.0, 1013.25, 80.0));
3293            let mut sensor = ptr::null_mut();
3294            assert_eq!(
3295                pamoja_bme280_new(bus, PART, pamoja_bme280_settings_default(), &mut sensor),
3296                PamojaStatus::Ok
3297            );
3298            pamoja_i2c_bus_free(bus);
3299
3300            let mut reading = measurement();
3301            assert_eq!(
3302                pamoja_bme280_measure(sensor, &mut reading),
3303                PamojaStatus::Ok
3304            );
3305            assert_eq!(reading.celsius, 4.0);
3306            assert!((reading.hectopascals - 1013.25).abs() < 0.01);
3307            assert!((reading.relative_humidity_percent - 80.0).abs() < 0.01);
3308            pamoja_bme280_free(sensor);
3309        }
3310    }
3311
3312    #[test]
3313    fn a_missing_part_is_an_io_error_and_another_part_a_codec_error() {
3314        unsafe {
3315            let bus = pamoja_i2c_bus_simulated();
3316            let mut sensor = ptr::null_mut();
3317            assert_eq!(
3318                pamoja_bme280_new(bus, PART, pamoja_bme280_settings_default(), &mut sensor),
3319                PamojaStatus::Ok
3320            );
3321            assert_eq!(pamoja_bme280_init(sensor), PamojaStatus::Io);
3322            assert_eq!(last_error(), "nothing answered at 0x76");
3323
3324            let bmp280 = pamoja_i2c_part_new(PART);
3325            assert_eq!(
3326                pamoja_i2c_part_load(bmp280, PAMOJA_BME280_REGISTER_CHIP_ID, [0x58].as_ptr(), 1),
3327                PamojaStatus::Ok
3328            );
3329            assert_eq!(pamoja_i2c_bus_attach(bus, bmp280), PamojaStatus::Ok);
3330            pamoja_i2c_part_free(bmp280);
3331            assert_eq!(pamoja_bme280_init(sensor), PamojaStatus::Codec);
3332            assert_eq!(last_error(), "sensor identification mismatch");
3333
3334            pamoja_bme280_free(sensor);
3335            pamoja_i2c_bus_free(bus);
3336        }
3337    }
3338
3339    #[test]
3340    fn the_shipped_calibration_and_a_built_burst_compensate_to_the_reading() {
3341        unsafe {
3342            let mut temp_press = [0u8; PAMOJA_BME280_CALIBRATION_TEMP_PRESS_LEN];
3343            let mut humidity = [0u8; PAMOJA_BME280_CALIBRATION_HUMIDITY_LEN];
3344            assert_eq!(
3345                pamoja_bme280_sim_calibration(temp_press.as_mut_ptr(), humidity.as_mut_ptr()),
3346                PamojaStatus::Ok
3347            );
3348            let calibration = bme280::Calibration::from_registers(&temp_press, &humidity);
3349
3350            let mut shipped = [0u8; PAMOJA_BME280_MEASUREMENT_LEN];
3351            assert_eq!(
3352                pamoja_bme280_sim_burst(shipped.as_mut_ptr()),
3353                PamojaStatus::Ok
3354            );
3355            let reading = calibration.compensate(&bme280::RawMeasurement::from_registers(&shipped));
3356            assert_eq!(reading.celsius(), 20.44);
3357
3358            let mut burst = [0u8; PAMOJA_BME280_MEASUREMENT_LEN];
3359            assert_eq!(
3360                pamoja_bme280_sim_burst_for(-12.5, 990.0, 35.0, burst.as_mut_ptr()),
3361                PamojaStatus::Ok
3362            );
3363            let reading = calibration.compensate(&bme280::RawMeasurement::from_registers(&burst));
3364            assert_eq!(reading.celsius(), -12.5);
3365            assert!((reading.hectopascals() - 990.0).abs() < 0.01);
3366            assert!((reading.relative_humidity_percent() - 35.0).abs() < 0.01);
3367            assert_eq!(
3368                pamoja_bme280_sim_burst_for(0.0, 0.0, 0.0, ptr::null_mut()),
3369                PamojaStatus::InvalidArgument
3370            );
3371            assert_eq!(
3372                pamoja_bme280_sim_burst(ptr::null_mut()),
3373                PamojaStatus::InvalidArgument
3374            );
3375        }
3376    }
3377
3378    #[test]
3379    fn a_bmp280_reads_its_simulated_twin_and_keeps_the_trimming() {
3380        unsafe {
3381            let bus = bus_with(pamoja_bmp280_sim_reporting(0x77, -7.5, 1003.0));
3382            let mut sensor = ptr::null_mut();
3383            assert_eq!(
3384                pamoja_bmp280_new(bus, 0x77, pamoja_bmp280_settings_default(), &mut sensor),
3385                PamojaStatus::Ok
3386            );
3387            let mut coefficients = std::mem::zeroed::<PamojaBmp280Coefficients>();
3388            assert!(!pamoja_bmp280_coefficients(sensor, &mut coefficients));
3389            let mut reading = PamojaBmp280Reading {
3390                celsius: 0.0,
3391                pascals: 0,
3392                hectopascals: 0.0,
3393            };
3394            assert_eq!(
3395                pamoja_bmp280_measure(sensor, &mut reading),
3396                PamojaStatus::Ok
3397            );
3398            assert!((reading.celsius + 7.5).abs() < 0.01);
3399            assert!((reading.hectopascals - 1003.0).abs() < 0.01);
3400            assert!(pamoja_bmp280_coefficients(sensor, &mut coefficients));
3401            assert_eq!(
3402                coefficients,
3403                PamojaBmp280Coefficients::from(bmp280::sim::calibration())
3404            );
3405
3406            let mut burst = [0u8; PAMOJA_BMP280_DATA_LEN];
3407            let mut calibration = [0u8; PAMOJA_BMP280_CALIBRATION_LEN];
3408            assert_eq!(
3409                pamoja_bmp280_sim_burst(burst.as_mut_ptr()),
3410                PamojaStatus::Ok
3411            );
3412            assert_eq!(
3413                pamoja_bmp280_sim_calibration(calibration.as_mut_ptr()),
3414                PamojaStatus::Ok
3415            );
3416            let compensated = bmp280::Calibration::parse(&calibration)
3417                .compensate(&bmp280::Measurement::parse(&burst));
3418            assert_eq!(compensated.celsius(), 20.44);
3419            pamoja_bmp280_free(sensor);
3420            pamoja_i2c_bus_free(bus);
3421        }
3422    }
3423
3424    #[test]
3425    fn a_tmp117_reads_its_twin_and_holds_the_alerts_it_consumed() {
3426        unsafe {
3427            let bus = bus_with(pamoja_tmp117_sim_part(0x48));
3428            let mut sensor = ptr::null_mut();
3429            assert_eq!(
3430                pamoja_tmp117_new(bus, 0x48, 1, &mut sensor),
3431                PamojaStatus::Ok
3432            );
3433            let mut revision = 0;
3434            assert!(!pamoja_tmp117_silicon_revision(sensor, &mut revision));
3435            let mut reading = PamojaTmp117Reading {
3436                raw: 0,
3437                micro_celsius: 0,
3438                celsius: 0.0,
3439            };
3440            assert_eq!(
3441                pamoja_tmp117_measure(sensor, &mut reading),
3442                PamojaStatus::Ok
3443            );
3444            assert_eq!(reading.celsius, PAMOJA_TMP117_SIM_CELSIUS);
3445            assert_eq!(reading.micro_celsius, 21_250_000);
3446            assert!(pamoja_tmp117_silicon_revision(sensor, &mut revision));
3447            assert_eq!(
3448                pamoja_tmp117_set_alert_limits(sensor, 30.0, 10.0),
3449                PamojaStatus::Ok
3450            );
3451            let mut alerts = PamojaTmp117Alerts { high: 1, low: 1 };
3452            assert_eq!(pamoja_tmp117_alerts(sensor, &mut alerts), PamojaStatus::Ok);
3453            assert_eq!(alerts, PamojaTmp117Alerts { high: 0, low: 0 });
3454
3455            let held = pamoja_i2c_bus_part(bus, 0x48);
3456            assert_eq!(
3457                pamoja_i2c_part_word(held, tmp117::register::THIGH_LIMIT),
3458                tmp117::raw_from_celsius(30.0) as u16
3459            );
3460            pamoja_i2c_part_free(held);
3461            pamoja_tmp117_free(sensor);
3462            pamoja_i2c_bus_free(bus);
3463        }
3464    }
3465
3466    #[test]
3467    fn an_opt3001_and_an_hdc1080_read_their_twins() {
3468        unsafe {
3469            let bus = bus_with(pamoja_opt3001_sim_reporting(0x44, 1200.0));
3470            attach(bus, pamoja_hdc1080_sim_reporting(-20.0, 12.5));
3471
3472            let mut light = ptr::null_mut();
3473            let settings = PamojaOpt3001Settings {
3474                long_conversion: 0,
3475                range_number: opt3001::RANGE_AUTOMATIC,
3476            };
3477            assert_eq!(
3478                pamoja_opt3001_new(bus, 0x44, settings, &mut light),
3479                PamojaStatus::Ok
3480            );
3481            let mut lux = PamojaOpt3001Reading {
3482                raw: 0,
3483                milli_lux: 0,
3484                lux: 0.0,
3485            };
3486            assert_eq!(pamoja_opt3001_measure(light, &mut lux), PamojaStatus::Ok);
3487            assert_eq!(lux.lux, 1200.0);
3488            assert_eq!(
3489                pamoja_i2c_bus_waited_micros(bus),
3490                100_000,
3491                "one short conversion"
3492            );
3493            let mut config = std::mem::zeroed::<PamojaOpt3001Config>();
3494            assert_eq!(
3495                pamoja_opt3001_configuration(light, &mut config),
3496                PamojaStatus::Ok
3497            );
3498            assert_eq!(config.long_conversion, 0);
3499
3500            let mut climate = ptr::null_mut();
3501            let wrong = PamojaHdc1080Settings {
3502                temperature_resolution_bits: 12,
3503                humidity_resolution_bits: 14,
3504            };
3505            assert_eq!(
3506                pamoja_hdc1080_new(bus, wrong, &mut climate),
3507                PamojaStatus::InvalidArgument
3508            );
3509            assert!(climate.is_null());
3510            assert_eq!(
3511                pamoja_hdc1080_new(bus, pamoja_hdc1080_settings_default(), &mut climate),
3512                PamojaStatus::Ok
3513            );
3514            let mut reading = std::mem::zeroed::<PamojaHdc1080Measurement>();
3515            assert_eq!(
3516                pamoja_hdc1080_measure(climate, &mut reading),
3517                PamojaStatus::Ok
3518            );
3519            assert!((reading.celsius + 20.0).abs() < 0.003);
3520            assert!((reading.relative_humidity - 12.5).abs() < 0.002);
3521            assert_eq!(pamoja_hdc1080_heater(climate, true), PamojaStatus::Ok);
3522            let mut config = std::mem::zeroed::<PamojaHdc1080Config>();
3523            assert_eq!(
3524                pamoja_hdc1080_configuration(climate, &mut config),
3525                PamojaStatus::Ok
3526            );
3527            assert_eq!(config.heater, 1);
3528
3529            pamoja_opt3001_free(light);
3530            pamoja_hdc1080_free(climate);
3531            pamoja_i2c_bus_free(bus);
3532        }
3533    }
3534
3535    #[test]
3536    fn the_current_monitors_read_the_load_their_twins_carry() {
3537        unsafe {
3538            let bus = bus_with(pamoja_ina219_sim_part(0x40));
3539            attach(
3540                bus,
3541                pamoja_ina226_sim_reporting(0x45, 2, 20_000_000, 3_300_000, -10_000_000),
3542            );
3543
3544            let mut solar = ptr::null_mut();
3545            assert_eq!(
3546                pamoja_ina219_new(bus, 0x40, pamoja_ina219_settings_default(), &mut solar),
3547                PamojaStatus::Ok
3548            );
3549            let mut reading = std::mem::zeroed::<PamojaIna219Reading>();
3550            assert_eq!(pamoja_ina219_measure(solar, &mut reading), PamojaStatus::Ok);
3551            assert_eq!(reading.bus_millivolts, PAMOJA_INA219_SIM_BUS_MILLIVOLTS);
3552            assert!(
3553                (reading.current_microamps - PAMOJA_INA219_SIM_MICROAMPS).abs()
3554                    < reading.current_lsb_microamps as i32
3555            );
3556            assert_eq!(
3557                pamoja_ina219_current_lsb(solar),
3558                crate::sensors::pamoja_ina219_minimum_current_lsb_microamps(3_200_000)
3559            );
3560            assert_ne!(pamoja_ina219_calibration_word(solar), 0);
3561
3562            let mut battery = ptr::null_mut();
3563            let settings = PamojaIna226Settings {
3564                shunt_milliohms: 2,
3565                max_microamps: 20_000_000,
3566                ..pamoja_ina226_settings_default()
3567            };
3568            assert_eq!(
3569                pamoja_ina226_new(bus, 0x45, settings, &mut battery),
3570                PamojaStatus::Ok
3571            );
3572            let mut reading = std::mem::zeroed::<PamojaIna226Reading>();
3573            assert_eq!(
3574                pamoja_ina226_measure(battery, &mut reading),
3575                PamojaStatus::Ok
3576            );
3577            assert_eq!(reading.bus_microvolts, 3_300_000);
3578            assert!((reading.current_amps + 10.0).abs() < 0.001);
3579            let mut die = PamojaIna226DieId {
3580                device: 0,
3581                revision: 0,
3582            };
3583            assert!(pamoja_ina226_identity(battery, &mut die));
3584            assert_eq!(die.device, ina226::DEVICE_ID);
3585
3586            pamoja_ina219_free(solar);
3587            pamoja_ina226_free(battery);
3588            pamoja_i2c_bus_free(bus);
3589        }
3590    }
3591
3592    #[test]
3593    fn an_ads1115_converts_at_the_range_its_twin_was_built_for() {
3594        unsafe {
3595            let wide = ads1115::Pga::Fsr4_096.code();
3596            let bus = bus_with(pamoja_ads1115_sim_reporting(0x48, wide, 3.0));
3597            let mut adc = ptr::null_mut();
3598            let settings = PamojaAds1115Settings {
3599                pga: wide,
3600                ..pamoja_ads1115_settings_default()
3601            };
3602            assert_eq!(
3603                pamoja_ads1115_new(bus, 0x48, settings, &mut adc),
3604                PamojaStatus::Ok
3605            );
3606            let mut sample = std::mem::zeroed::<PamojaAds1115Sample>();
3607            assert_eq!(pamoja_ads1115_sample(adc, &mut sample), PamojaStatus::Ok);
3608            assert!((sample.volts - 3.0).abs() < 0.001);
3609            assert_eq!(sample.pga, wide);
3610            assert_eq!(
3611                pamoja_ads1115_sample_input(adc, ads1115::Mux::Ain3Gnd.code(), &mut sample),
3612                PamojaStatus::Ok
3613            );
3614            let mut config = std::mem::zeroed::<PamojaAds1115Config>();
3615            assert_eq!(pamoja_ads1115_config(adc, &mut config), PamojaStatus::Ok);
3616            assert_eq!(
3617                config.mux,
3618                ads1115::Mux::Ain0Ain1.code(),
3619                "one input once, then back"
3620            );
3621            pamoja_ads1115_free(adc);
3622            pamoja_i2c_bus_free(bus);
3623        }
3624    }
3625
3626    #[test]
3627    fn the_sensirion_parts_answer_their_commands() {
3628        unsafe {
3629            let bus = bus_with(pamoja_sht3x_sim_reporting(0x45, -12.0, 88.5));
3630            attach(bus, pamoja_scd4x_sim_part());
3631
3632            let mut sht = ptr::null_mut();
3633            assert_eq!(
3634                pamoja_sht3x_new(bus, 0x45, 7, &mut sht),
3635                PamojaStatus::InvalidArgument
3636            );
3637            assert_eq!(pamoja_sht3x_new(bus, 0x45, 2, &mut sht), PamojaStatus::Ok);
3638            let mut status = std::mem::zeroed::<PamojaSht3xStatus>();
3639            assert!(!pamoja_sht3x_last_status(sht, &mut status));
3640            let mut reading = std::mem::zeroed::<PamojaSht3xMeasurement>();
3641            assert_eq!(pamoja_sht3x_measure(sht, &mut reading), PamojaStatus::Ok);
3642            assert!((reading.celsius + 12.0).abs() < 0.003);
3643            assert!(pamoja_sht3x_last_status(sht, &mut status));
3644            assert_eq!(pamoja_sht3x_heater_on(sht), PamojaStatus::Ok);
3645
3646            let held = pamoja_i2c_bus_part(bus, 0x45);
3647            assert_eq!(pamoja_i2c_part_received_count(held), 4);
3648            let first = pamoja_i2c_part_received(held, 0);
3649            let written = std::slice::from_raw_parts(
3650                crate::pamoja_buffer_data(first),
3651                crate::pamoja_buffer_len(first),
3652            );
3653            assert_eq!(
3654                written,
3655                &sht3x::command::SOFT_RESET.to_be_bytes()[..],
3656                "the reset went first"
3657            );
3658            crate::pamoja_buffer_free(first);
3659            pamoja_i2c_part_free(held);
3660
3661            let mut co2 = ptr::null_mut();
3662            assert_eq!(pamoja_scd4x_new(bus, &mut co2), PamojaStatus::Ok);
3663            let mut serial = 0;
3664            assert!(!pamoja_scd4x_serial(co2, &mut serial));
3665            let mut air = std::mem::zeroed::<PamojaScd4xMeasurement>();
3666            assert_eq!(pamoja_scd4x_measure(co2, &mut air), PamojaStatus::Ok);
3667            assert_eq!(air.co2_ppm, PAMOJA_SCD4X_SIM_CO2_PPM);
3668            assert!(pamoja_scd4x_serial(co2, &mut serial));
3669            assert_eq!(serial, PAMOJA_SCD4X_SIM_SERIAL);
3670            let mut ready = false;
3671            assert_eq!(pamoja_scd4x_poll_ready(co2, &mut ready), PamojaStatus::Ok);
3672            assert!(ready);
3673            assert_eq!(
3674                pamoja_scd4x_set_sensor_altitude(co2, 1_600),
3675                PamojaStatus::Ok
3676            );
3677
3678            pamoja_sht3x_free(sht);
3679            pamoja_scd4x_free(co2);
3680            pamoja_i2c_bus_free(bus);
3681        }
3682    }
3683
3684    #[test]
3685    fn a_thermometer_reads_the_file_the_kernel_serves() {
3686        let raw = ds18b20::temperature_from_celsius(21.5, ds18b20::Resolution::Bits12);
3687        let bytes = ds18b20::Scratchpad::new(raw, ds18b20::Resolution::Bits12, 75, -10).to_bytes();
3688        let hex: Vec<String> = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
3689        let text = format!("{} : crc={:02x} YES\n", hex.join(" "), bytes[8]);
3690
3691        let dir = std::env::temp_dir().join(format!("pamoja-ffi-w1-{}", std::process::id()));
3692        let device = dir.join("28-000005e2fdc3");
3693        std::fs::create_dir_all(&device).unwrap();
3694        std::fs::write(device.join("w1_slave"), &text).unwrap();
3695
3696        unsafe {
3697            let devices = CString::new(dir.to_string_lossy().into_owned()).unwrap();
3698            let mut found = ptr::null_mut();
3699            assert_eq!(
3700                pamoja_ds18b20_discover(devices.as_ptr(), &mut found),
3701                PamojaStatus::Ok
3702            );
3703            assert_eq!(pamoja_ds18b20_thermometers_len(found), 1);
3704            let probe = pamoja_ds18b20_thermometers_get(found, 0);
3705            assert!(pamoja_ds18b20_thermometers_get(found, 1).is_null());
3706            pamoja_ds18b20_thermometers_free(found);
3707
3708            let mut reading = std::mem::zeroed::<PamojaDs18b20Reading>();
3709            assert_eq!(
3710                pamoja_ds18b20_thermometer_read(probe, &mut reading),
3711                PamojaStatus::Ok
3712            );
3713            assert_eq!(reading.micro_celsius, 21_500_000);
3714            let path = pamoja_ds18b20_thermometer_path(probe);
3715            let path_text = CStr::from_ptr(crate::pamoja_string_data(path))
3716                .to_string_lossy()
3717                .into_owned();
3718            assert!(path_text.ends_with("w1_slave"), "{path_text}");
3719            crate::pamoja_string_free(path);
3720            let serial = pamoja_ds18b20_thermometer_serial(probe);
3721            assert_eq!(
3722                CStr::from_ptr(crate::pamoja_string_data(serial)).to_str(),
3723                Ok("000005e2fdc3")
3724            );
3725            crate::pamoja_string_free(serial);
3726            assert!(pamoja_ds18b20_thermometer_serial(ptr::null()).is_null());
3727            pamoja_ds18b20_thermometer_free(probe);
3728
3729            let text = CString::new(text).unwrap();
3730            assert_eq!(
3731                crate::sensors::pamoja_ds18b20_parse_w1_slave(text.as_ptr(), &mut reading),
3732                PamojaStatus::Ok
3733            );
3734
3735            let serial = CString::new("0000deadbeef").unwrap();
3736            let mut missing = ptr::null_mut();
3737            assert_eq!(
3738                pamoja_ds18b20_thermometer_new(serial.as_ptr(), &mut missing),
3739                PamojaStatus::Ok
3740            );
3741            assert_eq!(
3742                pamoja_ds18b20_thermometer_read(missing, &mut reading),
3743                PamojaStatus::Io
3744            );
3745            assert!(
3746                last_error().starts_with("reading the w1_slave file"),
3747                "{}",
3748                last_error()
3749            );
3750            pamoja_ds18b20_thermometer_free(missing);
3751        }
3752        std::fs::remove_dir_all(&dir).unwrap();
3753    }
3754}