Skip to main content

pamoja_hal/
bus.rs

1//! One I2C bus, shared by a program and every driver on it.
2//!
3//! A driver takes its bus by value, which is the right shape on a microcontroller: the program
4//! owns the peripheral and hands it over once. A program on a host works differently. A gateway
5//! keeps several parts on `/dev/i2c-1`, gives the same bus to a driver for each, and looks at
6//! the bus itself between reads. A language binding needs the same, since it cannot give a
7//! driver a Rust type at all. [`I2cBus`] is a handle to one bus that clones cheaply: every
8//! clone reaches the same bus, and one transfer runs at a time.
9//!
10//! Behind the handle is one of three things:
11//!
12//! - The kernel's adapter, opened with [`I2cBus::open`]. Only Linux has one.
13//! - Simulated parts, [`I2cBus::simulated`], each answering at its own address from its
14//!   registers the way a [`sim::I2cPart`](crate::sim::I2cPart) does. A transfer to an address
15//!   no part holds is not acknowledged, as on a real bus.
16//! - A script, [`I2cBus::scripted`], that plays one conversation and refuses any other, the way
17//!   an [`I2cScript`] does.
18//!
19//! A driver also takes a delay. [`I2cBus::delay`] gives it one that suits the bus: the process
20//! sleeps when real parts are on the other end, and carries straight on when simulated or
21//! scripted ones are, since there is nothing to wait for. Either way the wait is counted, and
22//! [`I2cBus::waited_micros`] is the total, so a test checks that a driver waited as long as its
23//! datasheet asks without the test taking that long.
24//!
25//! # Examples
26//!
27//! ```
28//! use pamoja_hal::bus::I2cBus;
29//! use pamoja_hal::i2c::I2c;
30//! use pamoja_hal::sim::I2cPart;
31//!
32//! // Two parts on one bus, each holding the id its datasheet gives: a BME280, whose chip
33//! // id register reads 0x60, and a TMP117, whose 16-bit device id register reads 0x0117.
34//! const BME280: u8 = 0x76;
35//! const BME280_CHIP_ID_REGISTER: u8 = 0xD0;
36//! const TMP117: u8 = 0x48;
37//! const TMP117_DEVICE_ID_REGISTER: u8 = 0x0F;
38//! let bus = I2cBus::simulated([
39//!     I2cPart::new(BME280).holding(BME280_CHIP_ID_REGISTER, &[0x60]),
40//!     I2cPart::new(TMP117).holding(TMP117_DEVICE_ID_REGISTER, &0x0117u16.to_be_bytes()),
41//! ]);
42//!
43//! // A clone is what a driver is given; the program keeps the original.
44//! let mut driver = bus.clone();
45//! let mut id = [0u8; 1];
46//! driver.write_read(BME280, &[BME280_CHIP_ID_REGISTER], &mut id)?;
47//! assert_eq!(id, [0x60]);
48//!
49//! // Nothing answers at the BME280's other address.
50//! assert!(driver.write_read(BME280 + 1, &[BME280_CHIP_ID_REGISTER], &mut id).is_err());
51//! assert_eq!(bus.transfers(), 2);
52//! # Ok::<(), pamoja_hal::bus::BusError>(())
53//! ```
54
55use std::fmt;
56use std::path::{Path, PathBuf};
57use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
58use std::time::Duration;
59
60use embedded_hal::delay::DelayNs;
61use embedded_hal::i2c::{
62    ErrorKind, ErrorType, I2c, NoAcknowledgeSource, Operation, SevenBitAddress,
63};
64
65use crate::script::{I2cScript, ScriptError};
66use crate::sim::{FromPart, Part, PartError};
67
68/// What answers on an [`I2cBus`].
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum BusKind {
71    /// The kernel's adapter, with real parts on real wires.
72    Adapter,
73    /// Simulated parts, answering from their registers.
74    Simulated,
75    /// A script of the transfers a driver is expected to make.
76    Scripted,
77}
78
79/// Why an I2C adapter could not be opened.
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub enum OpenError {
82    /// This platform has no I2C character device, or this build left out the `linux` feature
83    /// that opens one.
84    Unsupported,
85    /// The adapter's device file could not be opened.
86    Adapter {
87        /// The device file.
88        path: PathBuf,
89        /// Why, in the kernel's words.
90        reason: String,
91    },
92}
93
94impl fmt::Display for OpenError {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            OpenError::Unsupported => f.write_str(
98                "an I2C adapter is opened through the kernel's i2c-dev interface, which only Linux has",
99            ),
100            OpenError::Adapter { path, reason } => write!(f, "{}: {reason}", path.display()),
101        }
102    }
103}
104
105impl std::error::Error for OpenError {}
106
107/// A part was put on a bus that is not simulated.
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub struct NotSimulated;
110
111impl fmt::Display for NotSimulated {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str("only a simulated bus takes parts")
114    }
115}
116
117impl std::error::Error for NotSimulated {}
118
119/// Why a transfer on an [`I2cBus`] failed.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum BusError {
122    /// No simulated part holds the address, so nothing acknowledged it.
123    NoAcknowledge {
124        /// The address the transfer went to.
125        address: u8,
126    },
127    /// A simulated part refused the transfer.
128    Part(PartError),
129    /// The script refused the transfer, or failed it on purpose.
130    Script(ScriptError),
131    /// The kernel's adapter failed the transfer.
132    Adapter {
133        /// The kind of failure, as `embedded-hal` names it.
134        kind: ErrorKind,
135        /// The device file and the kernel's reason.
136        reason: String,
137    },
138}
139
140impl fmt::Display for BusError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            BusError::NoAcknowledge { address } => write!(f, "nothing answered at {address:#04x}"),
144            BusError::Part(error) => write!(f, "{error}"),
145            BusError::Script(error) => write!(f, "{error}"),
146            BusError::Adapter { reason, .. } => f.write_str(reason),
147        }
148    }
149}
150
151impl std::error::Error for BusError {
152    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
153        match self {
154            BusError::Part(error) => Some(error),
155            BusError::Script(error) => Some(error),
156            BusError::NoAcknowledge { .. } | BusError::Adapter { .. } => None,
157        }
158    }
159}
160
161impl embedded_hal::i2c::Error for BusError {
162    fn kind(&self) -> ErrorKind {
163        match self {
164            BusError::NoAcknowledge { .. } => {
165                ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address)
166            }
167            BusError::Part(error) => error.kind(),
168            BusError::Script(error) => error.kind(),
169            BusError::Adapter { kind, .. } => *kind,
170        }
171    }
172}
173
174/// One I2C bus, reached by every clone of this handle.
175///
176/// # Examples
177///
178/// ```
179/// use pamoja_hal::bus::{BusKind, I2cBus};
180/// use pamoja_hal::i2c::I2c;
181/// use pamoja_hal::script::{I2cScript, I2cStep};
182///
183/// // A script of one register read, a BME280 answering its chip id, and a driver that
184/// // makes it.
185/// const BME280: u8 = 0x76;
186/// const CHIP_ID_REGISTER: u8 = 0xD0;
187/// const BME280_CHIP_ID: u8 = 0x60;
188/// let bus = I2cBus::scripted(I2cScript::new([I2cStep::write_read(
189///     BME280,
190///     [CHIP_ID_REGISTER],
191///     [BME280_CHIP_ID],
192/// )]));
193/// let mut driver = bus.clone();
194/// let mut id = [0u8; 1];
195/// driver.write_read(BME280, &[CHIP_ID_REGISTER], &mut id)?;
196///
197/// assert_eq!(bus.kind(), BusKind::Scripted);
198/// assert_eq!(bus.remaining(), Some(0), "the script has been played through");
199/// # Ok::<(), pamoja_hal::bus::BusError>(())
200/// ```
201#[derive(Clone)]
202pub struct I2cBus {
203    shared: Arc<Mutex<Shared>>,
204}
205
206struct Shared {
207    backend: Backend,
208    transfers: usize,
209    waited_ns: u64,
210}
211
212enum Backend {
213    #[cfg(all(feature = "linux", target_os = "linux"))]
214    Adapter {
215        path: PathBuf,
216        device: crate::linux::I2cdev,
217    },
218    Parts(Vec<Part>),
219    Script(I2cScript),
220}
221
222impl Backend {
223    fn kind(&self) -> BusKind {
224        match self {
225            #[cfg(all(feature = "linux", target_os = "linux"))]
226            Backend::Adapter { .. } => BusKind::Adapter,
227            Backend::Parts(_) => BusKind::Simulated,
228            Backend::Script(_) => BusKind::Scripted,
229        }
230    }
231}
232
233fn lock(shared: &Mutex<Shared>) -> MutexGuard<'_, Shared> {
234    shared.lock().unwrap_or_else(PoisonError::into_inner)
235}
236
237/// Puts a part in the list, in place of one at the same address.
238fn place(parts: &mut Vec<Part>, part: Part) -> Option<Part> {
239    match parts
240        .iter_mut()
241        .find(|held| held.address() == part.address())
242    {
243        Some(held) => Some(std::mem::replace(held, part)),
244        None => {
245            parts.push(part);
246            None
247        }
248    }
249}
250
251impl I2cBus {
252    fn with(backend: Backend) -> I2cBus {
253        I2cBus {
254            shared: Arc::new(Mutex::new(Shared {
255                backend,
256                transfers: 0,
257                waited_ns: 0,
258            })),
259        }
260    }
261
262    /// Opens the kernel's I2C adapter, such as `/dev/i2c-1` on a Raspberry Pi.
263    ///
264    /// # Arguments
265    ///
266    /// * `path` - the adapter's device file.
267    ///
268    /// # Returns
269    ///
270    /// The bus, with the real parts wired to it on the other end.
271    ///
272    /// # Errors
273    ///
274    /// [`OpenError::Unsupported`] anywhere but Linux, or in a build without the `linux`
275    /// feature, and [`OpenError::Adapter`] when the file cannot be opened as an adapter: the
276    /// interface is not turned on, or the process may not use it.
277    ///
278    /// # Examples
279    ///
280    /// ```no_run
281    /// use pamoja_hal::bus::I2cBus;
282    ///
283    /// let bus = I2cBus::open("/dev/i2c-1")?;
284    /// # Ok::<(), pamoja_hal::bus::OpenError>(())
285    /// ```
286    pub fn open(path: impl AsRef<Path>) -> Result<I2cBus, OpenError> {
287        #[cfg(all(feature = "linux", target_os = "linux"))]
288        {
289            let path = path.as_ref().to_path_buf();
290            let device = crate::linux::I2cdev::new(&path).map_err(|error| OpenError::Adapter {
291                path: path.clone(),
292                reason: error.to_string(),
293            })?;
294            Ok(I2cBus::with(Backend::Adapter { path, device }))
295        }
296        #[cfg(not(all(feature = "linux", target_os = "linux")))]
297        {
298            let _ = path;
299            Err(OpenError::Unsupported)
300        }
301    }
302
303    /// A bus of simulated parts, each answering at its own address.
304    ///
305    /// # Arguments
306    ///
307    /// * `parts` - the parts on the bus, of any of the three kinds, put on in order as
308    ///   [`I2cBus::attach`] puts them, so a part at an address an earlier one holds takes its
309    ///   place.
310    ///
311    /// # Returns
312    ///
313    /// The bus. A transfer to an address no part holds fails with
314    /// [`BusError::NoAcknowledge`].
315    pub fn simulated<P: Into<Part>>(parts: impl IntoIterator<Item = P>) -> I2cBus {
316        let mut placed: Vec<Part> = Vec::new();
317        for part in parts {
318            place(&mut placed, part.into());
319        }
320        I2cBus::with(Backend::Parts(placed))
321    }
322
323    /// Puts a simulated part on the bus, in place of any part already at its address.
324    ///
325    /// A driver keeps working across the change, which is how a test moves a reading on: a
326    /// part built to report different values takes the old one's place.
327    ///
328    /// # Arguments
329    ///
330    /// * `part` - the part, of any of the three kinds.
331    ///
332    /// # Returns
333    ///
334    /// The part it replaced, or `None` when the address was free.
335    ///
336    /// # Errors
337    ///
338    /// [`NotSimulated`] when the bus is the kernel's adapter or a script.
339    pub fn attach(&self, part: impl Into<Part>) -> Result<Option<Part>, NotSimulated> {
340        match &mut lock(&self.shared).backend {
341            Backend::Parts(parts) => Ok(place(parts, part.into())),
342            _ => Err(NotSimulated),
343        }
344    }
345
346    /// A bus that plays one conversation and refuses any other.
347    ///
348    /// # Arguments
349    ///
350    /// * `script` - the transfers a driver is expected to make, in order, and the replies.
351    ///
352    /// # Returns
353    ///
354    /// The bus. A transfer that does not match the next step fails with
355    /// [`BusError::Script`] and consumes nothing.
356    pub fn scripted(script: I2cScript) -> I2cBus {
357        I2cBus::with(Backend::Script(script))
358    }
359
360    /// What answers on the bus.
361    ///
362    /// # Returns
363    ///
364    /// The kind of bus.
365    #[must_use]
366    pub fn kind(&self) -> BusKind {
367        lock(&self.shared).backend.kind()
368    }
369
370    /// What a simulated part holds now, as the kind of part the caller names.
371    ///
372    /// # Arguments
373    ///
374    /// * `address` - the part's address.
375    ///
376    /// # Returns
377    ///
378    /// A copy of the part, with whatever drivers have written to it, or `None` when the bus
379    /// is not simulated, no part holds the address, or the part there is another kind. Ask
380    /// for a [`Part`] to take any kind.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use pamoja_hal::bus::I2cBus;
386    /// use pamoja_hal::i2c::I2c;
387    /// use pamoja_hal::sim::{I2cPart, WordPart};
388    ///
389    /// // A driver asks a BME280 for one forced measurement: ctrl_meas takes one temperature
390    /// // sample, one pressure sample, and forced mode, in its three fields.
391    /// const BME280: u8 = 0x76;
392    /// const CTRL_MEAS: u8 = 0xF4;
393    /// const FORCED_ONCE: u8 = 0b001_001_01;
394    /// let mut bus = I2cBus::simulated([I2cPart::new(BME280)]);
395    /// bus.write(BME280, &[CTRL_MEAS, FORCED_ONCE])?;
396    ///
397    /// // The program looks at what the driver left in the part.
398    /// let part: I2cPart = bus.part(BME280).expect("a byte-wide part");
399    /// assert_eq!(part.register(CTRL_MEAS), FORCED_ONCE);
400    /// assert!(bus.part::<WordPart>(BME280).is_none(), "it is not a word-wide one");
401    /// # Ok::<(), pamoja_hal::bus::BusError>(())
402    /// ```
403    #[must_use]
404    pub fn part<P: FromPart>(&self, address: u8) -> Option<P> {
405        match &lock(&self.shared).backend {
406            Backend::Parts(parts) => parts
407                .iter()
408                .find(|part| part.address() == address)
409                .cloned()
410                .and_then(P::from_part),
411            _ => None,
412        }
413    }
414
415    /// How many transfers have been made on the bus, by every clone, including any that
416    /// failed.
417    ///
418    /// # Returns
419    ///
420    /// The count, one per transaction however many operations it held.
421    #[must_use]
422    pub fn transfers(&self) -> usize {
423        lock(&self.shared).transfers
424    }
425
426    /// How many steps a script has left.
427    ///
428    /// # Returns
429    ///
430    /// The steps not yet reached, `Some(0)` once a driver has made every transfer the script
431    /// expected, or `None` when the bus is not scripted.
432    #[must_use]
433    pub fn remaining(&self) -> Option<usize> {
434        match &lock(&self.shared).backend {
435            Backend::Script(script) => Some(script.remaining()),
436            _ => None,
437        }
438    }
439
440    /// How long the drivers on the bus have asked to wait, through [`I2cBus::delay`].
441    ///
442    /// # Returns
443    ///
444    /// The total in microseconds, counted whether or not the process slept through it.
445    #[must_use]
446    pub fn waited_micros(&self) -> u64 {
447        lock(&self.shared).waited_ns / 1_000
448    }
449
450    /// The delay a driver on this bus waits with.
451    ///
452    /// # Returns
453    ///
454    /// A delay that sleeps the process when the bus is the kernel's adapter, returns at once
455    /// when it is simulated or scripted, and adds every wait to [`I2cBus::waited_micros`].
456    #[must_use]
457    pub fn delay(&self) -> BusDelay {
458        BusDelay {
459            shared: Arc::clone(&self.shared),
460        }
461    }
462}
463
464impl fmt::Debug for I2cBus {
465    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466        let shared = lock(&self.shared);
467        f.debug_struct("I2cBus")
468            .field("kind", &shared.backend.kind())
469            .field("transfers", &shared.transfers)
470            .finish_non_exhaustive()
471    }
472}
473
474impl ErrorType for I2cBus {
475    type Error = BusError;
476}
477
478impl I2c<SevenBitAddress> for I2cBus {
479    fn transaction(
480        &mut self,
481        address: SevenBitAddress,
482        operations: &mut [Operation<'_>],
483    ) -> Result<(), BusError> {
484        let mut shared = lock(&self.shared);
485        shared.transfers += 1;
486        match &mut shared.backend {
487            #[cfg(all(feature = "linux", target_os = "linux"))]
488            Backend::Adapter { path, device } => {
489                I2c::<SevenBitAddress>::transaction(device, address, operations)
490                    .map_err(|error| adapter_error(path, &error))
491            }
492            Backend::Parts(parts) => {
493                let Some(part) = parts.iter_mut().find(|part| part.address() == address) else {
494                    return Err(BusError::NoAcknowledge { address });
495                };
496                part.transaction(address, operations)
497                    .map_err(BusError::Part)
498            }
499            Backend::Script(script) => script
500                .transaction(address, operations)
501                .map_err(BusError::Script),
502        }
503    }
504}
505
506/// Names an adapter's failure. The Raspberry Pi's controllers, the BCM2835's and the RP1's
507/// DesignWare block, report a part that did not acknowledge as `EREMOTEIO`, which
508/// `linux-embedded-hal` leaves as [`ErrorKind::Other`]; it is a missing acknowledge here.
509#[cfg(all(feature = "linux", target_os = "linux"))]
510fn adapter_error(path: &Path, error: &crate::linux::I2CError) -> BusError {
511    use crate::linux::i2cdev::linux::LinuxI2CError;
512    use embedded_hal::i2c::Error as _;
513
514    let errno = match error.inner() {
515        LinuxI2CError::Errno(errno) => Some(*errno),
516        LinuxI2CError::Io(io) => io.raw_os_error(),
517    };
518    let kind = if errno == Some(nix::errno::Errno::EREMOTEIO as i32) {
519        ErrorKind::NoAcknowledge(NoAcknowledgeSource::Unknown)
520    } else {
521        error.kind()
522    };
523    BusError::Adapter {
524        kind,
525        reason: format!("{}: {error}", path.display()),
526    }
527}
528
529/// The delay a driver on an [`I2cBus`] waits with, from [`I2cBus::delay`].
530#[derive(Clone)]
531pub struct BusDelay {
532    shared: Arc<Mutex<Shared>>,
533}
534
535impl fmt::Debug for BusDelay {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        f.debug_struct("BusDelay").finish_non_exhaustive()
538    }
539}
540
541impl DelayNs for BusDelay {
542    fn delay_ns(&mut self, ns: u32) {
543        let sleeps = {
544            let mut shared = lock(&self.shared);
545            shared.waited_ns = shared.waited_ns.saturating_add(u64::from(ns));
546            shared.backend.kind() == BusKind::Adapter
547        };
548        if sleeps {
549            std::thread::sleep(Duration::from_nanos(u64::from(ns)));
550        }
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use crate::script::I2cStep;
558    use crate::sim::{CommandPart, I2cPart, WordPart};
559    use embedded_hal::i2c::Error as _;
560    use std::time::Instant;
561
562    #[test]
563    fn every_clone_reaches_the_same_parts() {
564        let bus = I2cBus::simulated([I2cPart::new(0x76), I2cPart::new(0x48)]);
565        let mut driver = bus.clone();
566
567        driver.write(0x76, &[0xf4, 0x25]).unwrap();
568        driver.write(0x48, &[0x01, 0x60, 0x20]).unwrap();
569
570        let part = bus.part::<I2cPart>(0x76).map(|part| part.register(0xf4));
571        assert_eq!(part, Some(0x25));
572        let part = bus.part::<I2cPart>(0x48).map(|part| part.register(0x02));
573        assert_eq!(part, Some(0x20));
574        assert_eq!(bus.transfers(), 2);
575        assert_eq!(bus.kind(), BusKind::Simulated);
576        assert_eq!(bus.remaining(), None);
577    }
578
579    #[test]
580    fn a_part_attached_at_a_held_address_takes_its_place() {
581        let bus = I2cBus::simulated([
582            I2cPart::new(0x76).holding(0xd0, &[0x58]),
583            I2cPart::new(0x76).holding(0xd0, &[0x60]),
584        ]);
585        let held = bus.part::<I2cPart>(0x76).map(|part| part.register(0xd0));
586        assert_eq!(held, Some(0x60));
587
588        let replaced = bus
589            .attach(I2cPart::new(0x76).holding(0xd0, &[0x61]))
590            .expect("a simulated bus takes parts");
591        let replaced = replaced
592            .and_then(I2cPart::from_part)
593            .map(|part| part.register(0xd0));
594        assert_eq!(replaced, Some(0x60));
595        let held = bus.part::<I2cPart>(0x76).map(|part| part.register(0xd0));
596        assert_eq!(held, Some(0x61));
597
598        assert!(bus
599            .attach(I2cPart::new(0x48))
600            .expect("a free address")
601            .is_none());
602        assert!(bus.part::<Part>(0x48).is_some());
603    }
604
605    #[test]
606    fn one_bus_holds_parts_of_every_kind() {
607        let mut bus = I2cBus::simulated([
608            Part::from(I2cPart::new(0x76).holding(0xd0, &[0x60])),
609            WordPart::new(0x48).holding(0x0f, 0x0117).into(),
610            CommandPart::new(0x44, 2)
611                .answering(&[0xf3, 0x2d], &[0x80, 0x10, 0xe1])
612                .into(),
613        ]);
614
615        let mut word = [0u8; 2];
616        bus.write_read(0x48, &[0x0f], &mut word).unwrap();
617        assert_eq!(u16::from_be_bytes(word), 0x0117);
618        bus.write(0x44, &[0xf3, 0x2d]).unwrap();
619        let mut status = [0u8; 3];
620        bus.read(0x44, &mut status).unwrap();
621        assert_eq!(status, [0x80, 0x10, 0xe1]);
622
623        assert!(bus.part::<WordPart>(0x48).is_some());
624        assert!(
625            bus.part::<I2cPart>(0x48).is_none(),
626            "a word-wide part is not a byte-wide one"
627        );
628        let commands: CommandPart = bus.part(0x44).expect("a part that takes commands");
629        assert_eq!(commands.received().len(), 1);
630        assert_eq!(
631            bus.read(0x44, &mut status).unwrap_err(),
632            BusError::Part(PartError::NoReply),
633            "the reply was taken"
634        );
635    }
636
637    #[test]
638    fn only_a_simulated_bus_takes_parts() {
639        let bus = I2cBus::scripted(I2cScript::new([]));
640        assert_eq!(bus.attach(I2cPart::new(0x76)).unwrap_err(), NotSimulated);
641        assert_eq!(NotSimulated.to_string(), "only a simulated bus takes parts");
642    }
643
644    #[test]
645    fn an_address_no_part_holds_is_not_acknowledged() {
646        let mut bus = I2cBus::simulated([I2cPart::new(0x76)]);
647
648        let error = bus.write(0x77, &[0xd0]).unwrap_err();
649        assert_eq!(error, BusError::NoAcknowledge { address: 0x77 });
650        assert_eq!(
651            error.kind(),
652            ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address)
653        );
654        assert_eq!(error.to_string(), "nothing answered at 0x77");
655        assert!(bus.part::<Part>(0x77).is_none());
656        assert_eq!(bus.transfers(), 1, "a refused transfer still happened");
657    }
658
659    #[test]
660    fn a_part_refusing_a_read_says_why() {
661        let mut bus = I2cBus::simulated([I2cPart::new(0x76)]);
662        let mut byte = [0u8; 1];
663
664        let error = bus.read(0x76, &mut byte).unwrap_err();
665        assert_eq!(error, BusError::Part(PartError::NoRegister));
666    }
667
668    #[test]
669    fn a_script_counts_what_is_left_and_refuses_the_unexpected() {
670        let mut bus = I2cBus::scripted(I2cScript::new([
671            I2cStep::write(0x76, [0xe0, 0xb6]),
672            I2cStep::write_read(0x76, [0xd0], [0x60]),
673        ]));
674        assert_eq!(bus.remaining(), Some(2));
675
676        bus.write(0x76, &[0xe0, 0xb6]).unwrap();
677        assert_eq!(bus.remaining(), Some(1));
678
679        let mut id = [0u8; 1];
680        let refused = bus.write_read(0x76, &[0xd1], &mut id).unwrap_err();
681        assert!(
682            matches!(
683                refused,
684                BusError::Script(ScriptError::Mismatch { step: 1, .. })
685            ),
686            "{refused:?}"
687        );
688        assert_eq!(
689            bus.remaining(),
690            Some(1),
691            "a refused transfer consumes nothing"
692        );
693
694        bus.write_read(0x76, &[0xd0], &mut id).unwrap();
695        assert_eq!(id, [0x60]);
696        assert_eq!(bus.remaining(), Some(0));
697        assert_eq!(bus.kind(), BusKind::Scripted);
698        assert!(bus.part::<Part>(0x76).is_none());
699    }
700
701    #[test]
702    fn a_simulated_bus_counts_a_wait_without_sleeping_through_it() {
703        let bus = I2cBus::simulated(Vec::<Part>::new());
704        let mut delay = bus.delay();
705
706        let started = Instant::now();
707        delay.delay_ms(500);
708        delay.delay_us(250);
709
710        assert!(started.elapsed() < Duration::from_millis(250));
711        assert_eq!(bus.waited_micros(), 500_250);
712    }
713
714    #[cfg(not(all(feature = "linux", target_os = "linux")))]
715    #[test]
716    fn opening_an_adapter_needs_linux() {
717        let error = I2cBus::open("/dev/i2c-1").unwrap_err();
718        assert_eq!(error, OpenError::Unsupported);
719        assert!(error.to_string().contains("only Linux"), "{error}");
720    }
721
722    #[cfg(all(feature = "linux", target_os = "linux"))]
723    #[test]
724    fn opening_a_missing_adapter_names_it() {
725        let error = I2cBus::open("/dev/i2c-pamoja-absent").unwrap_err();
726        assert!(
727            error.to_string().starts_with("/dev/i2c-pamoja-absent: "),
728            "{error}"
729        );
730    }
731}