Skip to main content

pamoja_ffi/
hal.rs

1//! The C ABI for the bus layer: one I2C bus that a program and every driver on it share.
2//!
3//! A bus is the kernel's adapter on a Linux board, a set of simulated parts, or a script of
4//! the transfers a driver is expected to make, and the same calls work on each: a write, a
5//! read, and a write then a read in one transaction, which is how a register is read. A
6//! driver built on a bus, such as the BME280 in [`crate::sensors_driver`], holds a share of
7//! it, so the caller keeps its own handle, reads back what a simulated part holds after a
8//! driver wrote to it, and frees its handle whenever it likes; the bus closes when the last
9//! holder lets go.
10//!
11//! A part and a script are built here and copied onto a bus, so the handles that built them
12//! stay the caller's to free. Every failed transfer returns [`PamojaStatus::Io`] with the
13//! reason in the last error message: nothing answered at the address, the script expected
14//! something else, or the kernel's own words for what went wrong.
15//!
16//! A simulated part is one of three kinds, and one handle holds any of them:
17//! [`PAMOJA_I2C_PART_BYTES`], registers a byte wide, as Bosch's parts have;
18//! [`PAMOJA_I2C_PART_WORDS`], registers sixteen bits wide, as Texas Instruments' parts have;
19//! and [`PAMOJA_I2C_PART_COMMANDS`], commands that leave replies, as Sensirion's parts take.
20//! A call made on the wrong kind of part is refused with [`PamojaStatus::InvalidArgument`],
21//! or reads as zero where the call returns a value.
22
23use std::os::raw::c_char;
24use std::panic::{catch_unwind, AssertUnwindSafe};
25
26use pamoja_hal::bus::{BusKind, I2cBus, OpenError};
27use pamoja_hal::i2c::{ErrorKind, I2c, NoAcknowledgeSource};
28use pamoja_hal::script::{I2cScript, I2cStep};
29use pamoja_hal::sim::{CommandPart, I2cPart, Part, WordPart};
30
31use crate::{read_bytes, read_str, set_last_error, PamojaBuffer, PamojaStatus};
32
33/// A bus kind: the kernel's adapter, with real parts on real wires.
34pub const PAMOJA_I2C_BUS_ADAPTER: u8 = 0;
35
36/// A bus kind: simulated parts answering from their registers.
37pub const PAMOJA_I2C_BUS_SIMULATED: u8 = 1;
38
39/// A bus kind: a script of the transfers a driver is expected to make.
40pub const PAMOJA_I2C_BUS_SCRIPTED: u8 = 2;
41
42/// A scripted failure: nothing acknowledged the address.
43pub const PAMOJA_I2C_FAULT_NO_ACKNOWLEDGE_ADDRESS: u8 = 0;
44
45/// A scripted failure: the part did not acknowledge a data byte.
46pub const PAMOJA_I2C_FAULT_NO_ACKNOWLEDGE_DATA: u8 = 1;
47
48/// A scripted failure: a missing acknowledge, with no telling whether of the address or data.
49pub const PAMOJA_I2C_FAULT_NO_ACKNOWLEDGE: u8 = 2;
50
51/// A scripted failure: a bus error, such as a misplaced start or stop condition.
52pub const PAMOJA_I2C_FAULT_BUS: u8 = 3;
53
54/// A scripted failure: another controller won the bus.
55pub const PAMOJA_I2C_FAULT_ARBITRATION_LOSS: u8 = 4;
56
57/// A scripted failure: data arrived faster than it was taken.
58pub const PAMOJA_I2C_FAULT_OVERRUN: u8 = 5;
59
60/// A scripted failure of no more particular kind.
61pub const PAMOJA_I2C_FAULT_OTHER: u8 = 6;
62
63/// A part kind: 256 registers a byte wide.
64pub const PAMOJA_I2C_PART_BYTES: u8 = 0;
65
66/// A part kind: 256 registers sixteen bits wide, each traveling most significant byte first.
67pub const PAMOJA_I2C_PART_WORDS: u8 = 1;
68
69/// A part kind: commands, each leaving the reply it was given for a read to take.
70pub const PAMOJA_I2C_PART_COMMANDS: u8 = 2;
71
72/// A part that is not there, answering from its registers or its commands. Opaque; release it
73/// with [`pamoja_i2c_part_free`].
74pub struct PamojaI2cPart {
75    pub(crate) part: Part,
76}
77
78impl PamojaI2cPart {
79    /// Boxes any kind of simulated part for the caller to own.
80    ///
81    /// # Arguments
82    ///
83    /// * `part` - the part.
84    ///
85    /// # Returns
86    ///
87    /// A raw handle the caller releases with [`pamoja_i2c_part_free`].
88    pub(crate) fn into_raw(part: impl Into<Part>) -> *mut PamojaI2cPart {
89        Box::into_raw(Box::new(PamojaI2cPart { part: part.into() }))
90    }
91}
92
93/// The transfers a driver is expected to make, in order, and the replies. Opaque; release it
94/// with [`pamoja_i2c_script_free`].
95pub struct PamojaI2cScript {
96    steps: Vec<I2cStep>,
97}
98
99/// One I2C bus, shared with every driver built on it. Opaque; release it with
100/// [`pamoja_i2c_bus_free`].
101pub struct PamojaI2cBus {
102    pub(crate) bus: I2cBus,
103}
104
105/// Creates a part answering at one address from 256 registers a byte wide, every one reading
106/// zero.
107///
108/// # Arguments
109///
110/// * `address` - the 7-bit address it answers to.
111///
112/// # Returns
113///
114/// The part, which the caller releases with [`pamoja_i2c_part_free`].
115#[no_mangle]
116pub extern "C" fn pamoja_i2c_part_new(address: u8) -> *mut PamojaI2cPart {
117    PamojaI2cPart::into_raw(I2cPart::new(address))
118}
119
120/// Creates a part answering at one address from 256 registers sixteen bits wide, every one
121/// reading zero. A pointer byte names a register, a write of a word stores it and leaves the
122/// pointer where it was, and a read takes words from the pointer on.
123///
124/// # Arguments
125///
126/// * `address` - the 7-bit address it answers to.
127///
128/// # Returns
129///
130/// The part, which the caller releases with [`pamoja_i2c_part_free`].
131#[no_mangle]
132pub extern "C" fn pamoja_i2c_word_part_new(address: u8) -> *mut PamojaI2cPart {
133    PamojaI2cPart::into_raw(WordPart::new(address))
134}
135
136/// Creates a part answering at one address that takes commands and has been given no replies
137/// yet. A write sends a command and any arguments after it; a read takes the reply that
138/// command left, once, padded with `0xFF`; a read with no reply waiting is not acknowledged.
139///
140/// # Arguments
141///
142/// * `address` - the 7-bit address it answers to.
143/// * `width` - how many bytes a command takes: two for Sensirion's 16-bit commands.
144///
145/// # Returns
146///
147/// The part, which the caller releases with [`pamoja_i2c_part_free`].
148#[no_mangle]
149pub extern "C" fn pamoja_i2c_command_part_new(address: u8, width: usize) -> *mut PamojaI2cPart {
150    PamojaI2cPart::into_raw(CommandPart::new(address, width))
151}
152
153/// Returns which kind of part a handle holds.
154///
155/// # Arguments
156///
157/// * `part` - the part.
158///
159/// # Returns
160///
161/// [`PAMOJA_I2C_PART_BYTES`], [`PAMOJA_I2C_PART_WORDS`], or [`PAMOJA_I2C_PART_COMMANDS`]; the
162/// bytes code for a null part.
163///
164/// # Safety
165///
166/// `part` must be a live handle or null.
167#[no_mangle]
168pub unsafe extern "C" fn pamoja_i2c_part_kind(part: *const PamojaI2cPart) -> u8 {
169    if part.is_null() {
170        return PAMOJA_I2C_PART_BYTES;
171    }
172    match (*part).part {
173        Part::Bytes(_) => PAMOJA_I2C_PART_BYTES,
174        Part::Words(_) => PAMOJA_I2C_PART_WORDS,
175        Part::Commands(_) => PAMOJA_I2C_PART_COMMANDS,
176    }
177}
178
179/// Puts bytes in a part whose registers are a byte wide, from a register on. Past the last
180/// register they wrap to the first.
181///
182/// # Arguments
183///
184/// * `part` - the part.
185/// * `first` - the register the bytes start at.
186/// * `bytes` - what to put there.
187/// * `len` - how many bytes.
188///
189/// # Returns
190///
191/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null part, a null `bytes`
192/// with a nonzero length, or a part of another kind.
193///
194/// # Safety
195///
196/// `part` must be a live handle or null, and `bytes` must point to `len` readable bytes.
197#[no_mangle]
198pub unsafe extern "C" fn pamoja_i2c_part_load(
199    part: *mut PamojaI2cPart,
200    first: u8,
201    bytes: *const u8,
202    len: usize,
203) -> PamojaStatus {
204    let bytes = match read_bytes(bytes, len) {
205        Ok(bytes) => bytes,
206        Err(status) => return status,
207    };
208    match byte_part(part) {
209        Ok(part) => {
210            part.load(first, &bytes);
211            PamojaStatus::Ok
212        }
213        Err(status) => status,
214    }
215}
216
217/// Reads what one register of a part whose registers are a byte wide holds.
218///
219/// # Arguments
220///
221/// * `part` - the part.
222/// * `register` - which register.
223///
224/// # Returns
225///
226/// Its value, which is what a driver wrote if it wrote one, or 0 for a null part or a part of
227/// another kind.
228///
229/// # Safety
230///
231/// `part` must be a live handle or null.
232#[no_mangle]
233pub unsafe extern "C" fn pamoja_i2c_part_register(part: *const PamojaI2cPart, register: u8) -> u8 {
234    match part.as_ref().map(|part| &part.part) {
235        Some(Part::Bytes(part)) => part.register(register),
236        _ => 0,
237    }
238}
239
240/// Reads consecutive registers of a part whose registers are a byte wide, from one register
241/// on.
242///
243/// # Arguments
244///
245/// * `part` - the part.
246/// * `first` - the first register.
247/// * `out` - receives one byte per register. Past the last register it wraps to the first.
248/// * `len` - how many registers to read.
249///
250/// # Returns
251///
252/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or a part of
253/// another kind.
254///
255/// # Safety
256///
257/// `part` must be a live handle or null, and `out` must point to `len` writable bytes.
258#[no_mangle]
259pub unsafe extern "C" fn pamoja_i2c_part_read(
260    part: *const PamojaI2cPart,
261    first: u8,
262    out: *mut u8,
263    len: usize,
264) -> PamojaStatus {
265    let Some(out) = out_bytes(out, len) else {
266        return PamojaStatus::InvalidArgument;
267    };
268    let part = match byte_part(part.cast_mut()) {
269        Ok(part) => part,
270        Err(status) => return status,
271    };
272    let mut at = first;
273    for slot in out {
274        *slot = part.register(at);
275        at = at.wrapping_add(1);
276    }
277    PamojaStatus::Ok
278}
279
280/// Puts a value in one register of a part whose registers are sixteen bits wide, read-only
281/// bits included, the way the part itself would.
282///
283/// # Arguments
284///
285/// * `part` - the part.
286/// * `register` - the register.
287/// * `value` - what it holds.
288///
289/// # Returns
290///
291/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null part or a part of
292/// another kind.
293///
294/// # Safety
295///
296/// `part` must be a live handle or null.
297#[no_mangle]
298pub unsafe extern "C" fn pamoja_i2c_part_set_word(
299    part: *mut PamojaI2cPart,
300    register: u8,
301    value: u16,
302) -> PamojaStatus {
303    match word_part(part) {
304        Ok(part) => {
305            part.set(register, value);
306            PamojaStatus::Ok
307        }
308        Err(status) => status,
309    }
310}
311
312/// Marks bits of one register of a part whose registers are sixteen bits wide as the part's to
313/// set: a driver's write leaves them as the part holds them, as a conversion-ready flag is.
314///
315/// # Arguments
316///
317/// * `part` - the part.
318/// * `register` - the register.
319/// * `mask` - the bits that are the part's.
320///
321/// # Returns
322///
323/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null part or a part of
324/// another kind.
325///
326/// # Safety
327///
328/// `part` must be a live handle or null.
329#[no_mangle]
330pub unsafe extern "C" fn pamoja_i2c_part_read_only(
331    part: *mut PamojaI2cPart,
332    register: u8,
333    mask: u16,
334) -> PamojaStatus {
335    match word_part(part) {
336        Ok(held) => {
337            *held = held.clone().read_only(register, mask);
338            PamojaStatus::Ok
339        }
340        Err(status) => status,
341    }
342}
343
344/// Reads what one register of a part whose registers are sixteen bits wide holds.
345///
346/// # Arguments
347///
348/// * `part` - the part.
349/// * `register` - which register.
350///
351/// # Returns
352///
353/// Its value, which is what a driver wrote there apart from the read-only bits, or 0 for a
354/// null part or a part of another kind.
355///
356/// # Safety
357///
358/// `part` must be a live handle or null.
359#[no_mangle]
360pub unsafe extern "C" fn pamoja_i2c_part_word(part: *const PamojaI2cPart, register: u8) -> u16 {
361    match part.as_ref().map(|part| &part.part) {
362        Some(Part::Words(part)) => part.word(register),
363        _ => 0,
364    }
365}
366
367/// Gives a part that takes commands the reply one command leaves, in place of any reply given
368/// before.
369///
370/// # Arguments
371///
372/// * `part` - the part.
373/// * `command` - the command's bytes.
374/// * `command_len` - how many.
375/// * `reply` - what a read after the command returns.
376/// * `reply_len` - how many bytes.
377///
378/// # Returns
379///
380/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or a part of
381/// another kind.
382///
383/// # Safety
384///
385/// `part` must be a live handle or null, `command` must point to `command_len` readable bytes,
386/// and `reply` to `reply_len`.
387#[no_mangle]
388pub unsafe extern "C" fn pamoja_i2c_part_answer(
389    part: *mut PamojaI2cPart,
390    command: *const u8,
391    command_len: usize,
392    reply: *const u8,
393    reply_len: usize,
394) -> PamojaStatus {
395    let command = match read_bytes(command, command_len) {
396        Ok(command) => command,
397        Err(status) => return status,
398    };
399    let reply = match read_bytes(reply, reply_len) {
400        Ok(reply) => reply,
401        Err(status) => return status,
402    };
403    match command_part(part) {
404        Ok(part) => {
405            part.answer(&command, &reply);
406            PamojaStatus::Ok
407        }
408        Err(status) => status,
409    }
410}
411
412/// Returns how many writes a part that takes commands has received.
413///
414/// # Arguments
415///
416/// * `part` - the part.
417///
418/// # Returns
419///
420/// The count, or 0 for a null part or a part of another kind.
421///
422/// # Safety
423///
424/// `part` must be a live handle or null.
425#[no_mangle]
426pub unsafe extern "C" fn pamoja_i2c_part_received_count(part: *const PamojaI2cPart) -> usize {
427    match part.as_ref().map(|part| &part.part) {
428        Some(Part::Commands(part)) => part.received().len(),
429        _ => 0,
430    }
431}
432
433/// Copies one write a part that takes commands received: a command and any arguments after
434/// it.
435///
436/// # Arguments
437///
438/// * `part` - the part.
439/// * `index` - which write, oldest first, below [`pamoja_i2c_part_received_count`].
440///
441/// # Returns
442///
443/// The write's bytes, which the caller releases with [`crate::pamoja_buffer_free`], or null for
444/// a null part, a part of another kind, or an index past the last write.
445///
446/// # Safety
447///
448/// `part` must be a live handle or null.
449#[no_mangle]
450pub unsafe extern "C" fn pamoja_i2c_part_received(
451    part: *const PamojaI2cPart,
452    index: usize,
453) -> *mut PamojaBuffer {
454    match part.as_ref().map(|part| &part.part) {
455        Some(Part::Commands(part)) => match part.received().get(index) {
456            Some(write) => PamojaBuffer::into_raw(write.clone()),
457            None => std::ptr::null_mut(),
458        },
459        _ => std::ptr::null_mut(),
460    }
461}
462
463/// Returns the address a part answers to.
464///
465/// # Arguments
466///
467/// * `part` - the part.
468///
469/// # Returns
470///
471/// The 7-bit address, or 0 for a null part.
472///
473/// # Safety
474///
475/// `part` must be a live handle or null.
476#[no_mangle]
477pub unsafe extern "C" fn pamoja_i2c_part_address(part: *const PamojaI2cPart) -> u8 {
478    if part.is_null() {
479        return 0;
480    }
481    (*part).part.address()
482}
483
484/// Returns how many transfers a part has served.
485///
486/// # Arguments
487///
488/// * `part` - the part.
489///
490/// # Returns
491///
492/// The count, or 0 for a null part.
493///
494/// # Safety
495///
496/// `part` must be a live handle or null.
497#[no_mangle]
498pub unsafe extern "C" fn pamoja_i2c_part_transfers(part: *const PamojaI2cPart) -> usize {
499    if part.is_null() {
500        return 0;
501    }
502    (*part).part.transfers()
503}
504
505/// Releases a part. A null pointer is ignored.
506///
507/// # Safety
508///
509/// `part` must be a handle that has not been freed, or null.
510#[no_mangle]
511pub unsafe extern "C" fn pamoja_i2c_part_free(part: *mut PamojaI2cPart) {
512    if !part.is_null() {
513        drop(Box::from_raw(part));
514    }
515}
516
517/// Creates an empty script.
518///
519/// # Returns
520///
521/// The script, which the caller fills with the step functions and releases with
522/// [`pamoja_i2c_script_free`].
523#[no_mangle]
524pub extern "C" fn pamoja_i2c_script_new() -> *mut PamojaI2cScript {
525    Box::into_raw(Box::new(PamojaI2cScript { steps: Vec::new() }))
526}
527
528/// Adds a step: the driver writes exactly these bytes to the address.
529///
530/// # Arguments
531///
532/// * `script` - the script.
533/// * `address` - the 7-bit address the write must go to.
534/// * `bytes` - the bytes the driver must send.
535/// * `len` - how many.
536///
537/// # Returns
538///
539/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
540///
541/// # Safety
542///
543/// `script` must be a live handle or null, and `bytes` must point to `len` readable bytes.
544#[no_mangle]
545pub unsafe extern "C" fn pamoja_i2c_script_write(
546    script: *mut PamojaI2cScript,
547    address: u8,
548    bytes: *const u8,
549    len: usize,
550) -> PamojaStatus {
551    add_step(script, || {
552        Ok(I2cStep::write(address, read_bytes(bytes, len)?))
553    })
554}
555
556/// Adds a step: the driver reads from the address and receives the reply, whose length is
557/// the length it must ask for.
558///
559/// # Arguments
560///
561/// * `script` - the script.
562/// * `address` - the 7-bit address the read must come from.
563/// * `reply` - the bytes the part answers with.
564/// * `len` - how many.
565///
566/// # Returns
567///
568/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
569///
570/// # Safety
571///
572/// `script` must be a live handle or null, and `reply` must point to `len` readable bytes.
573#[no_mangle]
574pub unsafe extern "C" fn pamoja_i2c_script_read(
575    script: *mut PamojaI2cScript,
576    address: u8,
577    reply: *const u8,
578    len: usize,
579) -> PamojaStatus {
580    add_step(script, || {
581        Ok(I2cStep::read(address, read_bytes(reply, len)?))
582    })
583}
584
585/// Adds a step: the driver writes these bytes and then reads the reply in one transaction,
586/// the shape of a register read.
587///
588/// # Arguments
589///
590/// * `script` - the script.
591/// * `address` - the 7-bit address of the part.
592/// * `bytes` - the bytes the driver must send first, usually a register address.
593/// * `len` - how many.
594/// * `reply` - the bytes the part answers with.
595/// * `reply_len` - how many.
596///
597/// # Returns
598///
599/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
600///
601/// # Safety
602///
603/// `script` must be a live handle or null, `bytes` must point to `len` readable bytes, and
604/// `reply` to `reply_len`.
605#[no_mangle]
606pub unsafe extern "C" fn pamoja_i2c_script_write_read(
607    script: *mut PamojaI2cScript,
608    address: u8,
609    bytes: *const u8,
610    len: usize,
611    reply: *const u8,
612    reply_len: usize,
613) -> PamojaStatus {
614    add_step(script, || {
615        Ok(I2cStep::write_read(
616            address,
617            read_bytes(bytes, len)?,
618            read_bytes(reply, reply_len)?,
619        ))
620    })
621}
622
623/// Adds a step: the next transfer to the address fails, the way a missing or busy part does.
624///
625/// # Arguments
626///
627/// * `script` - the script.
628/// * `address` - the 7-bit address the failing transfer must go to.
629/// * `fault` - one of the `PAMOJA_I2C_FAULT_` codes.
630///
631/// # Returns
632///
633/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null script or a code
634/// that names no fault.
635///
636/// # Safety
637///
638/// `script` must be a live handle or null.
639#[no_mangle]
640pub unsafe extern "C" fn pamoja_i2c_script_fault(
641    script: *mut PamojaI2cScript,
642    address: u8,
643    fault: u8,
644) -> PamojaStatus {
645    add_step(script, || {
646        let kind = fault_kind(fault).ok_or_else(|| {
647            set_last_error(format!("{fault} is not an I2C fault code"));
648            PamojaStatus::InvalidArgument
649        })?;
650        Ok(I2cStep::fault(address, kind))
651    })
652}
653
654/// Returns how many steps a script holds.
655///
656/// # Arguments
657///
658/// * `script` - the script.
659///
660/// # Returns
661///
662/// The count, or 0 for a null script.
663///
664/// # Safety
665///
666/// `script` must be a live handle or null.
667#[no_mangle]
668pub unsafe extern "C" fn pamoja_i2c_script_len(script: *const PamojaI2cScript) -> usize {
669    if script.is_null() {
670        return 0;
671    }
672    (*script).steps.len()
673}
674
675/// Releases a script. A null pointer is ignored.
676///
677/// # Safety
678///
679/// `script` must be a handle that has not been freed, or null.
680#[no_mangle]
681pub unsafe extern "C" fn pamoja_i2c_script_free(script: *mut PamojaI2cScript) {
682    if !script.is_null() {
683        drop(Box::from_raw(script));
684    }
685}
686
687/// Opens the kernel's I2C adapter, such as `/dev/i2c-1` on a Raspberry Pi.
688///
689/// # Arguments
690///
691/// * `path` - the adapter's device file.
692/// * `out_bus` - receives the bus, or null when opening fails.
693///
694/// # Returns
695///
696/// [`PamojaStatus::Ok`] with the bus in `out_bus`; [`PamojaStatus::Unsupported`] on any
697/// platform but Linux; [`PamojaStatus::InvalidArgument`] for a null or non-UTF-8 argument; or
698/// [`PamojaStatus::Io`] when the file cannot be opened as an adapter, with a last error
699/// message that names it.
700///
701/// # Safety
702///
703/// `path` must be a null-terminated string or null, and `out_bus` a writable pointer or null.
704#[no_mangle]
705pub unsafe extern "C" fn pamoja_i2c_bus_open(
706    path: *const c_char,
707    out_bus: *mut *mut PamojaI2cBus,
708) -> PamojaStatus {
709    if out_bus.is_null() {
710        set_last_error("out_bus must not be null".to_owned());
711        return PamojaStatus::InvalidArgument;
712    }
713    *out_bus = std::ptr::null_mut();
714    let Some(path) = read_str(path, "path") else {
715        return PamojaStatus::InvalidArgument;
716    };
717    match catch_unwind(AssertUnwindSafe(|| I2cBus::open(path))) {
718        Ok(Ok(bus)) => {
719            *out_bus = Box::into_raw(Box::new(PamojaI2cBus { bus }));
720            PamojaStatus::Ok
721        }
722        Ok(Err(error)) => {
723            set_last_error(error.to_string());
724            match error {
725                OpenError::Unsupported => PamojaStatus::Unsupported,
726                OpenError::Adapter { .. } => PamojaStatus::Io,
727            }
728        }
729        Err(_) => panicked(),
730    }
731}
732
733/// Creates a simulated bus with no parts on it yet; [`pamoja_i2c_bus_attach`] puts them on.
734///
735/// # Returns
736///
737/// The bus, which the caller releases with [`pamoja_i2c_bus_free`]. Until a part is attached
738/// every transfer finds nothing at its address.
739#[no_mangle]
740pub extern "C" fn pamoja_i2c_bus_simulated() -> *mut PamojaI2cBus {
741    Box::into_raw(Box::new(PamojaI2cBus {
742        bus: I2cBus::simulated(Vec::<Part>::new()),
743    }))
744}
745
746/// Puts a copy of a part of any kind on a simulated bus, in place of any part already at its
747/// address.
748///
749/// # Arguments
750///
751/// * `bus` - the bus.
752/// * `part` - the part to copy onto it; the caller still owns the handle.
753///
754/// # Returns
755///
756/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or a bus
757/// that is not simulated.
758///
759/// # Safety
760///
761/// `bus` and `part` must be live handles or null.
762#[no_mangle]
763pub unsafe extern "C" fn pamoja_i2c_bus_attach(
764    bus: *const PamojaI2cBus,
765    part: *const PamojaI2cPart,
766) -> PamojaStatus {
767    if bus.is_null() || part.is_null() {
768        set_last_error("bus and part must not be null".to_owned());
769        return PamojaStatus::InvalidArgument;
770    }
771    match (*bus).bus.attach((*part).part.clone()) {
772        Ok(_) => PamojaStatus::Ok,
773        Err(error) => {
774            set_last_error(error.to_string());
775            PamojaStatus::InvalidArgument
776        }
777    }
778}
779
780/// Creates a bus that plays a script and refuses any transfer that is not its next step.
781///
782/// # Arguments
783///
784/// * `script` - the steps to copy; the caller still owns the handle.
785///
786/// # Returns
787///
788/// The bus, which the caller releases with [`pamoja_i2c_bus_free`], or null for a null
789/// script.
790///
791/// # Safety
792///
793/// `script` must be a live handle or null.
794#[no_mangle]
795pub unsafe extern "C" fn pamoja_i2c_bus_scripted(
796    script: *const PamojaI2cScript,
797) -> *mut PamojaI2cBus {
798    if script.is_null() {
799        set_last_error("script must not be null".to_owned());
800        return std::ptr::null_mut();
801    }
802    let steps = (*script).steps.clone();
803    Box::into_raw(Box::new(PamojaI2cBus {
804        bus: I2cBus::scripted(I2cScript::new(steps)),
805    }))
806}
807
808/// Returns what answers on a bus.
809///
810/// # Arguments
811///
812/// * `bus` - the bus.
813///
814/// # Returns
815///
816/// [`PAMOJA_I2C_BUS_ADAPTER`], [`PAMOJA_I2C_BUS_SIMULATED`], or [`PAMOJA_I2C_BUS_SCRIPTED`];
817/// the simulated code for a null bus.
818///
819/// # Safety
820///
821/// `bus` must be a live handle or null.
822#[no_mangle]
823pub unsafe extern "C" fn pamoja_i2c_bus_kind(bus: *const PamojaI2cBus) -> u8 {
824    if bus.is_null() {
825        return PAMOJA_I2C_BUS_SIMULATED;
826    }
827    match (*bus).bus.kind() {
828        BusKind::Adapter => PAMOJA_I2C_BUS_ADAPTER,
829        BusKind::Simulated => PAMOJA_I2C_BUS_SIMULATED,
830        BusKind::Scripted => PAMOJA_I2C_BUS_SCRIPTED,
831    }
832}
833
834/// Writes bytes to a part, in one transaction.
835///
836/// # Arguments
837///
838/// * `bus` - the bus.
839/// * `address` - the part's 7-bit address.
840/// * `bytes` - what to write, usually a register address and then its value.
841/// * `len` - how many bytes.
842///
843/// # Returns
844///
845/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument; or
846/// [`PamojaStatus::Io`] when the transfer fails, with the reason in the last error message.
847///
848/// # Safety
849///
850/// `bus` must be a live handle or null, and `bytes` must point to `len` readable bytes.
851#[no_mangle]
852pub unsafe extern "C" fn pamoja_i2c_bus_write(
853    bus: *const PamojaI2cBus,
854    address: u8,
855    bytes: *const u8,
856    len: usize,
857) -> PamojaStatus {
858    let bytes = match read_bytes(bytes, len) {
859        Ok(bytes) => bytes,
860        Err(status) => return status,
861    };
862    on_bus(bus, |bus| bus.write(address, &bytes))
863}
864
865/// Reads bytes from a part, in one transaction.
866///
867/// # Arguments
868///
869/// * `bus` - the bus.
870/// * `address` - the part's 7-bit address.
871/// * `out` - receives the bytes.
872/// * `len` - how many bytes to read.
873///
874/// # Returns
875///
876/// As [`pamoja_i2c_bus_write`].
877///
878/// # Safety
879///
880/// `bus` must be a live handle or null, and `out` must point to `len` writable bytes.
881#[no_mangle]
882pub unsafe extern "C" fn pamoja_i2c_bus_read(
883    bus: *const PamojaI2cBus,
884    address: u8,
885    out: *mut u8,
886    len: usize,
887) -> PamojaStatus {
888    let Some(out) = out_bytes(out, len) else {
889        return PamojaStatus::InvalidArgument;
890    };
891    on_bus(bus, |bus| bus.read(address, out))
892}
893
894/// Writes bytes to a part and reads its reply in one transaction, with a repeated start
895/// between them, which is how a register is read.
896///
897/// # Arguments
898///
899/// * `bus` - the bus.
900/// * `address` - the part's 7-bit address.
901/// * `bytes` - what to write first, usually the register address.
902/// * `len` - how many bytes to write.
903/// * `out` - receives the reply.
904/// * `out_len` - how many bytes to read.
905///
906/// # Returns
907///
908/// As [`pamoja_i2c_bus_write`].
909///
910/// # Safety
911///
912/// `bus` must be a live handle or null, `bytes` must point to `len` readable bytes, and `out`
913/// to `out_len` writable bytes.
914#[no_mangle]
915pub unsafe extern "C" fn pamoja_i2c_bus_write_read(
916    bus: *const PamojaI2cBus,
917    address: u8,
918    bytes: *const u8,
919    len: usize,
920    out: *mut u8,
921    out_len: usize,
922) -> PamojaStatus {
923    let bytes = match read_bytes(bytes, len) {
924        Ok(bytes) => bytes,
925        Err(status) => return status,
926    };
927    let Some(out) = out_bytes(out, out_len) else {
928        return PamojaStatus::InvalidArgument;
929    };
930    on_bus(bus, |bus| bus.write_read(address, &bytes, out))
931}
932
933/// Returns how many transfers have been made on a bus, by the caller and every driver on it,
934/// including any that failed.
935///
936/// # Arguments
937///
938/// * `bus` - the bus.
939///
940/// # Returns
941///
942/// The count, or 0 for a null bus.
943///
944/// # Safety
945///
946/// `bus` must be a live handle or null.
947#[no_mangle]
948pub unsafe extern "C" fn pamoja_i2c_bus_transfers(bus: *const PamojaI2cBus) -> usize {
949    if bus.is_null() {
950        return 0;
951    }
952    (*bus).bus.transfers()
953}
954
955/// Reports how many steps a scripted bus has left.
956///
957/// # Arguments
958///
959/// * `bus` - the bus.
960/// * `out_remaining` - receives the steps not yet reached.
961///
962/// # Returns
963///
964/// `true` with the count in `out_remaining` for a scripted bus; `false` for any other bus, a
965/// null bus, or a null `out_remaining`.
966///
967/// # Safety
968///
969/// `bus` must be a live handle or null, and `out_remaining` a writable pointer or null.
970#[no_mangle]
971pub unsafe extern "C" fn pamoja_i2c_bus_remaining(
972    bus: *const PamojaI2cBus,
973    out_remaining: *mut usize,
974) -> bool {
975    if bus.is_null() || out_remaining.is_null() {
976        return false;
977    }
978    match (*bus).bus.remaining() {
979        Some(remaining) => {
980            *out_remaining = remaining;
981            true
982        }
983        None => false,
984    }
985}
986
987/// Returns how long the drivers on a bus have asked to wait.
988///
989/// # Arguments
990///
991/// * `bus` - the bus.
992///
993/// # Returns
994///
995/// The total in microseconds, counted whether or not the process slept through it, or 0 for a
996/// null bus.
997///
998/// # Safety
999///
1000/// `bus` must be a live handle or null.
1001#[no_mangle]
1002pub unsafe extern "C" fn pamoja_i2c_bus_waited_micros(bus: *const PamojaI2cBus) -> u64 {
1003    if bus.is_null() {
1004        return 0;
1005    }
1006    (*bus).bus.waited_micros()
1007}
1008
1009/// Copies what a simulated part holds now, with whatever drivers have written to it.
1010///
1011/// # Arguments
1012///
1013/// * `bus` - the bus.
1014/// * `address` - the part's address.
1015///
1016/// # Returns
1017///
1018/// A new part of the kind that answers at the address, which [`pamoja_i2c_part_kind`] names
1019/// and the caller releases with [`pamoja_i2c_part_free`], or null when the bus is not
1020/// simulated, holds no part at the address, or is null.
1021///
1022/// # Safety
1023///
1024/// `bus` must be a live handle or null.
1025#[no_mangle]
1026pub unsafe extern "C" fn pamoja_i2c_bus_part(
1027    bus: *const PamojaI2cBus,
1028    address: u8,
1029) -> *mut PamojaI2cPart {
1030    if bus.is_null() {
1031        return std::ptr::null_mut();
1032    }
1033    match (*bus).bus.part::<Part>(address) {
1034        Some(part) => PamojaI2cPart::into_raw(part),
1035        None => std::ptr::null_mut(),
1036    }
1037}
1038
1039/// Releases the caller's share of a bus. The bus closes when no driver holds it either. A
1040/// null pointer is ignored.
1041///
1042/// # Safety
1043///
1044/// `bus` must be a handle that has not been freed, or null.
1045#[no_mangle]
1046pub unsafe extern "C" fn pamoja_i2c_bus_free(bus: *mut PamojaI2cBus) {
1047    if !bus.is_null() {
1048        drop(Box::from_raw(bus));
1049    }
1050}
1051
1052/// Adds a step built by `build` to a script, turning a null script into a status.
1053///
1054/// # Safety
1055///
1056/// `script` must be a live handle or null.
1057unsafe fn add_step(
1058    script: *mut PamojaI2cScript,
1059    build: impl FnOnce() -> Result<I2cStep, PamojaStatus>,
1060) -> PamojaStatus {
1061    if script.is_null() {
1062        set_last_error("script must not be null".to_owned());
1063        return PamojaStatus::InvalidArgument;
1064    }
1065    match build() {
1066        Ok(step) => {
1067            (*script).steps.push(step);
1068            PamojaStatus::Ok
1069        }
1070        Err(status) => status,
1071    }
1072}
1073
1074/// A part handle as a part whose registers are a byte wide, or the status that refuses it.
1075///
1076/// # Safety
1077///
1078/// `part` must be a live handle or null.
1079unsafe fn byte_part<'a>(part: *mut PamojaI2cPart) -> Result<&'a mut I2cPart, PamojaStatus> {
1080    match part.as_mut().map(|part| &mut part.part) {
1081        Some(Part::Bytes(part)) => Ok(part),
1082        other => Err(wrong_kind(other.is_some(), "registers a byte wide")),
1083    }
1084}
1085
1086/// A part handle as a part whose registers are sixteen bits wide, or the status that refuses
1087/// it.
1088///
1089/// # Safety
1090///
1091/// `part` must be a live handle or null.
1092unsafe fn word_part<'a>(part: *mut PamojaI2cPart) -> Result<&'a mut WordPart, PamojaStatus> {
1093    match part.as_mut().map(|part| &mut part.part) {
1094        Some(Part::Words(part)) => Ok(&mut **part),
1095        other => Err(wrong_kind(other.is_some(), "registers sixteen bits wide")),
1096    }
1097}
1098
1099/// A part handle as a part that takes commands, or the status that refuses it.
1100///
1101/// # Safety
1102///
1103/// `part` must be a live handle or null.
1104unsafe fn command_part<'a>(part: *mut PamojaI2cPart) -> Result<&'a mut CommandPart, PamojaStatus> {
1105    match part.as_mut().map(|part| &mut part.part) {
1106        Some(Part::Commands(part)) => Ok(part),
1107        other => Err(wrong_kind(other.is_some(), "commands")),
1108    }
1109}
1110
1111/// Records why a part handle was refused: it was null, or it held another kind of part.
1112fn wrong_kind(present: bool, wanted: &str) -> PamojaStatus {
1113    if present {
1114        set_last_error(format!("only a part with {wanted} takes that call"));
1115    } else {
1116        set_last_error("part must not be null".to_owned());
1117    }
1118    PamojaStatus::InvalidArgument
1119}
1120
1121/// The failure a fault code names.
1122fn fault_kind(code: u8) -> Option<ErrorKind> {
1123    Some(match code {
1124        PAMOJA_I2C_FAULT_NO_ACKNOWLEDGE_ADDRESS => {
1125            ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address)
1126        }
1127        PAMOJA_I2C_FAULT_NO_ACKNOWLEDGE_DATA => ErrorKind::NoAcknowledge(NoAcknowledgeSource::Data),
1128        PAMOJA_I2C_FAULT_NO_ACKNOWLEDGE => ErrorKind::NoAcknowledge(NoAcknowledgeSource::Unknown),
1129        PAMOJA_I2C_FAULT_BUS => ErrorKind::Bus,
1130        PAMOJA_I2C_FAULT_ARBITRATION_LOSS => ErrorKind::ArbitrationLoss,
1131        PAMOJA_I2C_FAULT_OVERRUN => ErrorKind::Overrun,
1132        PAMOJA_I2C_FAULT_OTHER => ErrorKind::Other,
1133        _ => return None,
1134    })
1135}
1136
1137/// A caller's buffer as a slice, or `None` after recording why it cannot be one.
1138///
1139/// # Safety
1140///
1141/// `out` must point to `len` writable bytes, or be null.
1142unsafe fn out_bytes<'a>(out: *mut u8, len: usize) -> Option<&'a mut [u8]> {
1143    if len == 0 {
1144        return Some(&mut []);
1145    }
1146    if out.is_null() {
1147        set_last_error("out must not be null".to_owned());
1148        return None;
1149    }
1150    Some(std::slice::from_raw_parts_mut(out, len))
1151}
1152
1153/// Runs a transfer on a bus, turning a refusal or a panic into a status.
1154///
1155/// # Safety
1156///
1157/// `bus` must be a live handle or null.
1158unsafe fn on_bus(
1159    bus: *const PamojaI2cBus,
1160    transfer: impl FnOnce(&mut I2cBus) -> Result<(), pamoja_hal::bus::BusError>,
1161) -> PamojaStatus {
1162    if bus.is_null() {
1163        set_last_error("bus must not be null".to_owned());
1164        return PamojaStatus::InvalidArgument;
1165    }
1166    let mut bus = (*bus).bus.clone();
1167    match catch_unwind(AssertUnwindSafe(|| transfer(&mut bus))) {
1168        Ok(Ok(())) => PamojaStatus::Ok,
1169        Ok(Err(error)) => {
1170            set_last_error(error.to_string());
1171            PamojaStatus::Io
1172        }
1173        Err(_) => panicked(),
1174    }
1175}
1176
1177/// Records a caught panic and reports it as [`PamojaStatus::Panic`].
1178fn panicked() -> PamojaStatus {
1179    set_last_error("panic at the FFI boundary".to_owned());
1180    PamojaStatus::Panic
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185    use super::*;
1186    use std::ffi::{CStr, CString};
1187    use std::ptr;
1188
1189    fn last_error() -> String {
1190        let message = crate::pamoja_last_error_message();
1191        if message.is_null() {
1192            return String::new();
1193        }
1194        unsafe { CStr::from_ptr(message) }
1195            .to_string_lossy()
1196            .into_owned()
1197    }
1198
1199    #[test]
1200    fn a_part_on_a_simulated_bus_answers_and_keeps_what_was_written() {
1201        unsafe {
1202            let part = pamoja_i2c_part_new(0x76);
1203            assert_eq!(
1204                pamoja_i2c_part_load(part, 0xd0, [0x60].as_ptr(), 1),
1205                PamojaStatus::Ok
1206            );
1207            let bus = pamoja_i2c_bus_simulated();
1208            assert_eq!(pamoja_i2c_bus_attach(bus, part), PamojaStatus::Ok);
1209            pamoja_i2c_part_free(part);
1210
1211            let mut id = [0u8; 1];
1212            assert_eq!(
1213                pamoja_i2c_bus_write_read(bus, 0x76, [0xd0].as_ptr(), 1, id.as_mut_ptr(), 1),
1214                PamojaStatus::Ok
1215            );
1216            assert_eq!(id, [0x60]);
1217            assert_eq!(
1218                pamoja_i2c_bus_write(bus, 0x76, [0xf4, 0x25].as_ptr(), 2),
1219                PamojaStatus::Ok
1220            );
1221
1222            let held = pamoja_i2c_bus_part(bus, 0x76);
1223            assert!(!held.is_null());
1224            assert_eq!(pamoja_i2c_part_register(held, 0xf4), 0x25);
1225            let mut block = [0u8; 2];
1226            assert_eq!(
1227                pamoja_i2c_part_read(held, 0xd0, block.as_mut_ptr(), 2),
1228                PamojaStatus::Ok
1229            );
1230            assert_eq!(block, [0x60, 0x00]);
1231            pamoja_i2c_part_free(held);
1232
1233            assert_eq!(pamoja_i2c_bus_kind(bus), PAMOJA_I2C_BUS_SIMULATED);
1234            assert_eq!(pamoja_i2c_bus_transfers(bus), 2);
1235            let mut remaining = 7;
1236            assert!(!pamoja_i2c_bus_remaining(bus, &mut remaining));
1237            pamoja_i2c_bus_free(bus);
1238        }
1239    }
1240
1241    #[test]
1242    fn nothing_at_an_address_is_an_io_error_that_says_so() {
1243        unsafe {
1244            let bus = pamoja_i2c_bus_simulated();
1245            let mut byte = [0u8; 1];
1246            assert_eq!(
1247                pamoja_i2c_bus_write_read(bus, 0x77, [0xd0].as_ptr(), 1, byte.as_mut_ptr(), 1),
1248                PamojaStatus::Io
1249            );
1250            assert_eq!(last_error(), "nothing answered at 0x77");
1251            assert!(pamoja_i2c_bus_part(bus, 0x77).is_null());
1252            pamoja_i2c_bus_free(bus);
1253        }
1254    }
1255
1256    #[test]
1257    fn a_script_counts_down_and_refuses_what_it_did_not_expect() {
1258        unsafe {
1259            let script = pamoja_i2c_script_new();
1260            assert_eq!(
1261                pamoja_i2c_script_write(script, 0x76, [0xe0, 0xb6].as_ptr(), 2),
1262                PamojaStatus::Ok
1263            );
1264            assert_eq!(
1265                pamoja_i2c_script_write_read(script, 0x76, [0xd0].as_ptr(), 1, [0x60].as_ptr(), 1),
1266                PamojaStatus::Ok
1267            );
1268            assert_eq!(
1269                pamoja_i2c_script_fault(script, 0x76, PAMOJA_I2C_FAULT_BUS),
1270                PamojaStatus::Ok
1271            );
1272            assert_eq!(
1273                pamoja_i2c_script_fault(script, 0x76, 42),
1274                PamojaStatus::InvalidArgument
1275            );
1276            assert_eq!(pamoja_i2c_script_len(script), 3);
1277
1278            let bus = pamoja_i2c_bus_scripted(script);
1279            pamoja_i2c_script_free(script);
1280            assert_eq!(pamoja_i2c_bus_kind(bus), PAMOJA_I2C_BUS_SCRIPTED);
1281
1282            let mut remaining = 0;
1283            assert!(pamoja_i2c_bus_remaining(bus, &mut remaining));
1284            assert_eq!(remaining, 3);
1285
1286            assert_eq!(
1287                pamoja_i2c_bus_write(bus, 0x76, [0xe0, 0xb7].as_ptr(), 2),
1288                PamojaStatus::Io
1289            );
1290            assert!(
1291                last_error().starts_with("i2c script step 0"),
1292                "{}",
1293                last_error()
1294            );
1295            assert_eq!(
1296                pamoja_i2c_bus_write(bus, 0x76, [0xe0, 0xb6].as_ptr(), 2),
1297                PamojaStatus::Ok
1298            );
1299            let mut id = [0u8; 1];
1300            assert_eq!(
1301                pamoja_i2c_bus_write_read(bus, 0x76, [0xd0].as_ptr(), 1, id.as_mut_ptr(), 1),
1302                PamojaStatus::Ok
1303            );
1304            assert_eq!(
1305                pamoja_i2c_bus_read(bus, 0x76, id.as_mut_ptr(), 1),
1306                PamojaStatus::Io
1307            );
1308            assert!(
1309                last_error().starts_with("i2c script fault"),
1310                "{}",
1311                last_error()
1312            );
1313            assert!(pamoja_i2c_bus_remaining(bus, &mut remaining));
1314            assert_eq!(remaining, 0);
1315            pamoja_i2c_bus_free(bus);
1316        }
1317    }
1318
1319    #[test]
1320    fn only_a_simulated_bus_takes_a_part() {
1321        unsafe {
1322            let script = pamoja_i2c_script_new();
1323            let bus = pamoja_i2c_bus_scripted(script);
1324            let part = pamoja_i2c_part_new(0x76);
1325            assert_eq!(
1326                pamoja_i2c_bus_attach(bus, part),
1327                PamojaStatus::InvalidArgument
1328            );
1329            assert_eq!(last_error(), "only a simulated bus takes parts");
1330            pamoja_i2c_part_free(part);
1331            pamoja_i2c_bus_free(bus);
1332            pamoja_i2c_script_free(script);
1333            assert!(pamoja_i2c_bus_scripted(ptr::null()).is_null());
1334        }
1335    }
1336
1337    #[test]
1338    fn word_and_command_parts_answer_in_their_own_shape() {
1339        unsafe {
1340            let words = pamoja_i2c_word_part_new(0x48);
1341            assert_eq!(pamoja_i2c_part_kind(words), PAMOJA_I2C_PART_WORDS);
1342            assert_eq!(
1343                pamoja_i2c_part_set_word(words, 0x01, 0x2000),
1344                PamojaStatus::Ok
1345            );
1346            assert_eq!(
1347                pamoja_i2c_part_read_only(words, 0x01, 0xF000),
1348                PamojaStatus::Ok
1349            );
1350            assert_eq!(
1351                pamoja_i2c_part_load(words, 0x00, [0x01].as_ptr(), 1),
1352                PamojaStatus::InvalidArgument
1353            );
1354            assert_eq!(
1355                last_error(),
1356                "only a part with registers a byte wide takes that call"
1357            );
1358
1359            let commands = pamoja_i2c_command_part_new(0x44, 2);
1360            assert_eq!(pamoja_i2c_part_kind(commands), PAMOJA_I2C_PART_COMMANDS);
1361            assert_eq!(
1362                pamoja_i2c_part_answer(
1363                    commands,
1364                    [0xF3, 0x2D].as_ptr(),
1365                    2,
1366                    [0x80, 0x10, 0xE1].as_ptr(),
1367                    3
1368                ),
1369                PamojaStatus::Ok
1370            );
1371            assert_eq!(
1372                pamoja_i2c_part_set_word(commands, 0x00, 0),
1373                PamojaStatus::InvalidArgument
1374            );
1375
1376            let bus = pamoja_i2c_bus_simulated();
1377            assert_eq!(pamoja_i2c_bus_attach(bus, words), PamojaStatus::Ok);
1378            assert_eq!(pamoja_i2c_bus_attach(bus, commands), PamojaStatus::Ok);
1379            pamoja_i2c_part_free(words);
1380            pamoja_i2c_part_free(commands);
1381
1382            assert_eq!(
1383                pamoja_i2c_bus_write(bus, 0x48, [0x01, 0x00, 0x20].as_ptr(), 3),
1384                PamojaStatus::Ok
1385            );
1386            let mut word = [0u8; 2];
1387            assert_eq!(
1388                pamoja_i2c_bus_write_read(bus, 0x48, [0x01].as_ptr(), 1, word.as_mut_ptr(), 2),
1389                PamojaStatus::Ok
1390            );
1391            assert_eq!(
1392                word,
1393                [0x20, 0x20],
1394                "the flag the part keeps, then the write"
1395            );
1396
1397            let mut status = [0u8; 3];
1398            assert_eq!(
1399                pamoja_i2c_bus_write(bus, 0x44, [0xF3, 0x2D].as_ptr(), 2),
1400                PamojaStatus::Ok
1401            );
1402            assert_eq!(
1403                pamoja_i2c_bus_read(bus, 0x44, status.as_mut_ptr(), 3),
1404                PamojaStatus::Ok
1405            );
1406            assert_eq!(status, [0x80, 0x10, 0xE1]);
1407            assert_eq!(
1408                pamoja_i2c_bus_read(bus, 0x44, status.as_mut_ptr(), 3),
1409                PamojaStatus::Io,
1410                "the reply was taken"
1411            );
1412
1413            let held = pamoja_i2c_bus_part(bus, 0x48);
1414            assert_eq!(pamoja_i2c_part_kind(held), PAMOJA_I2C_PART_WORDS);
1415            assert_eq!(pamoja_i2c_part_word(held, 0x01), 0x2020);
1416            assert_eq!(pamoja_i2c_part_register(held, 0x01), 0);
1417            pamoja_i2c_part_free(held);
1418
1419            let held = pamoja_i2c_bus_part(bus, 0x44);
1420            assert_eq!(pamoja_i2c_part_received_count(held), 1);
1421            let write = pamoja_i2c_part_received(held, 0);
1422            let received = std::slice::from_raw_parts(
1423                crate::pamoja_buffer_data(write),
1424                crate::pamoja_buffer_len(write),
1425            )
1426            .to_vec();
1427            assert_eq!(received, [0xF3, 0x2D]);
1428            crate::pamoja_buffer_free(write);
1429            assert!(pamoja_i2c_part_received(held, 1).is_null());
1430            pamoja_i2c_part_free(held);
1431            pamoja_i2c_bus_free(bus);
1432        }
1433    }
1434
1435    #[cfg(not(target_os = "linux"))]
1436    #[test]
1437    fn opening_an_adapter_off_linux_is_unsupported() {
1438        let path = CString::new("/dev/i2c-1").expect("no interior null");
1439        let mut bus = ptr::null_mut();
1440        let opened = unsafe { pamoja_i2c_bus_open(path.as_ptr(), &mut bus) };
1441        assert_eq!(opened, PamojaStatus::Unsupported);
1442        assert!(bus.is_null());
1443        assert!(last_error().contains("only Linux"), "{}", last_error());
1444    }
1445
1446    #[cfg(target_os = "linux")]
1447    #[test]
1448    fn opening_a_missing_adapter_is_an_io_error_that_names_it() {
1449        let path = CString::new("/dev/i2c-pamoja-absent").expect("no interior null");
1450        let mut bus = ptr::null_mut();
1451        let opened = unsafe { pamoja_i2c_bus_open(path.as_ptr(), &mut bus) };
1452        assert_eq!(opened, PamojaStatus::Io);
1453        assert!(bus.is_null());
1454        assert!(
1455            last_error().starts_with("/dev/i2c-pamoja-absent: "),
1456            "{}",
1457            last_error()
1458        );
1459    }
1460}