rppal_pfd/
lib.rs

1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3//!
4//! ## Extended example
5//!
6//! This example is available in `${CARGO_MANIFEST_DIR}/examples/blink-fast-slow.rs`.
7//!
8//! ``` rust no_run
9#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/blink-fast-slow.rs"))]
10//! ```
11
12use std::{
13    cell::RefCell,
14    fmt::{self, Write},
15    rc::Rc,
16    result,
17    time::Duration,
18};
19
20#[cfg(not(any(test, feature = "mockspi")))]
21use log::warn;
22use log::{Level::Debug, debug, error, info, log_enabled};
23#[cfg(not(any(test, feature = "mockspi")))]
24use rppal::gpio::{self, Event as GpioEvent, Gpio, Trigger};
25#[cfg(not(feature = "mockspi"))]
26use rppal_mcp23s17::{IOCON, Mcp23s17, RegisterAddress};
27#[cfg(feature = "mockspi")]
28pub use rppal_mcp23s17::{IOCON, Mcp23s17, RegisterAddress};
29
30use thiserror::Error;
31
32/// Re-export of `rppal_mcp23s17` crate APIs which we use on this crate's APIs.
33pub use rppal_mcp23s17::{ChipSelect, InterruptMode, Level, OutputPin, SpiBus, SpiMode};
34
35//--------------------------------------------------------------------------------------
36/// The hardware address of the device - two bits.
37///
38/// The MCP23S17 supports three hardware address bits but the PiFace Digital only exposes
39/// `A0` and `A1` on `JP1` and `JP2` respectively.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
41pub struct HardwareAddress(u8);
42
43impl HardwareAddress {
44    /// Hardware address space is two bits wide so 0-3 are valid.
45    pub const MAX_HARDWARE_ADDRESS: u8 = 3;
46
47    /// Create a HardwareAddress bounds-checking that it is valid.
48    pub fn new(address: u8) -> Result<Self> {
49        if address <= Self::MAX_HARDWARE_ADDRESS {
50            Ok(Self(address))
51        } else {
52            Err(PiFaceDigitalError::HardwareAddressBoundsError(address))
53        }
54    }
55}
56
57impl TryFrom<u8> for HardwareAddress {
58    type Error = PiFaceDigitalError;
59
60    fn try_from(value: u8) -> Result<Self> {
61        HardwareAddress::new(value)
62    }
63}
64
65impl From<HardwareAddress> for rppal_mcp23s17::HardwareAddress {
66    fn from(addr: HardwareAddress) -> Self {
67        // A PiFace Digital address is smaller than an MCP23S17 address.
68        rppal_mcp23s17::HardwareAddress::new(addr.0).unwrap()
69    }
70}
71
72impl From<HardwareAddress> for u8 {
73    fn from(addr: HardwareAddress) -> Self {
74        addr.0
75    }
76}
77
78impl TryFrom<rppal_mcp23s17::HardwareAddress> for HardwareAddress {
79    type Error = PiFaceDigitalError;
80
81    fn try_from(value: rppal_mcp23s17::HardwareAddress) -> Result<Self> {
82        Self::try_from(u8::from(value))
83    }
84}
85
86impl fmt::Display for HardwareAddress {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        fmt::Display::fmt(&format!("{}", self.0), f)
89    }
90}
91
92//--------------------------------------------------------------------------------------
93
94/// Errors that operation of the PiFace Digital can raise.
95#[derive(Error, Debug)]
96pub enum PiFaceDigitalError {
97    /// Errors from the `rppal_mcp23s17::Mcp23s17`.
98    #[error("MCP23S17 error")]
99    Mcp23s17Error {
100        /// Underlying error source.
101        #[from]
102        source: rppal_mcp23s17::Mcp23s17Error,
103    },
104
105    /// Attempt to access a PiFace Digital beyond the hardware address range
106    /// (0 - [`HardwareAddress::MAX_HARDWARE_ADDRESS`]).
107    #[error("Hardware address out of range")]
108    HardwareAddressBoundsError(u8),
109
110    /// Failed to detect the presence of any physical PiFace Digital device connected to
111    /// the SPI bus.
112    #[error("No hardware connected to {spi_bus} at hardware address={hardware_address})")]
113    NoHardwareDetected {
114        /// SPI bus we tried to access the device over.
115        spi_bus: SpiBus,
116        /// Hardware address of the device we tried to access.
117        hardware_address: HardwareAddress,
118    },
119
120    /// Errors accessing the GPIO for the interrupt input.
121    #[error("GPIO error")]
122    GpioError {
123        /// Underlying error source.
124        #[from]
125        source: rppal::gpio::Error,
126    },
127}
128
129/// Convenient alias for [`Result<_>`] types can have [`PiFaceDigitalError`]s.
130pub type Result<T> = result::Result<T, PiFaceDigitalError>;
131
132/// An input pin.
133///
134/// The [`InputPin`] exposes the capabilities of the underlying `rppal_mcp23s17::InputPin`
135/// with the addition of interrupt handling.
136///
137/// # Example usage
138///
139/// ```no_run
140/// # use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, SpiBus, SpiMode};
141/// #
142/// # // Create an instance of the driver for the device with the hardware address
143/// # // (A1, A0) of 0b00 on SPI bus 0 clocked at 100kHz. The address bits are set using
144/// # // `JP1` and `JP2` on the PiFace Digital board.
145/// # let pfd = PiFaceDigital::new(
146/// #     HardwareAddress::new(0).expect("Invalid hardware address"),
147/// #     SpiBus::Spi0,
148/// #     ChipSelect::Cs0,
149/// #     100_000,
150/// #     SpiMode::Mode0,
151/// # )
152/// # .expect("Failed to create PiFace Digital");
153/// #
154/// // Given an instance of a PiFaceDigital, take ownership of the output pin on bit 4
155/// // of the device.
156/// let pin = pfd
157///     .get_output_pin(4)
158///     .expect("Failed to get Pin");
159///
160/// // Set the pin to logic-level low.
161/// pin.write(Level::Low).expect("Bad pin write");
162/// ```
163#[derive(Debug)]
164pub struct InputPin {
165    pin: rppal_mcp23s17::InputPin,
166    interrupts_enabled: bool,
167    #[allow(dead_code)] // in test config no functionality accesses pfd_state.
168    pfd_state: Rc<RefCell<PiFaceDigitalState>>,
169}
170
171/// Internal state of the PiFace Digital card.
172#[derive(Debug)]
173pub struct PiFaceDigitalState {
174    mcp23s17: Mcp23s17,
175    #[cfg(not(any(test, feature = "mockspi")))]
176    _gpio: Gpio,
177    #[cfg(not(any(test, feature = "mockspi")))]
178    interrupt_pin: gpio::InputPin,
179}
180
181/// Represents an instance of the PiFace Digital I/O expander for the Raspberry Pi.
182///
183/// This is the key entrypoint into the driver. This driver is a thin wrapper around
184/// the `rppal_mcp23s17` driver that is responsible for ensuring that the I/O
185/// expander chip is configured in a manner compatible with the capabilities of the
186/// PiFace Digital hardware.
187///
188/// The PiFace Digital has two GPIO ports:
189///
190/// - `GPIOA` configured as outputs to:
191///   - two relays (bits 0 and 1)
192///   - surface-mount LEDs on each output
193///   - an 8-way terminal block
194///
195/// - `GPIOB` configured as inputs connected to
196///   - four on-board push switches on bits 0-3
197///   - an 8-way terminal block
198///
199/// The user should instantiate a [`PiFaceDigital`] and then use
200/// [`PiFaceDigital::get_input_pin()`] and [`PiFaceDigital::get_output_pin()`] to acquire
201/// an [`InputPin`] or [`OutputPin`].
202///
203/// ```no_run
204/// use rppal_pfd::{ChipSelect, HardwareAddress, PiFaceDigital, SpiBus, SpiMode};
205///
206/// // Create an instance of the driver for the device with the hardware address
207/// // (A1, A0) of 0b00 on SPI bus 0 clocked at 100kHz. The address bits are set using
208/// // JP1 and JP2 on the PiFace Digital board.
209/// let pfd = PiFaceDigital::new(
210///     HardwareAddress::new(0).expect("Invalid hardware address"),
211///     SpiBus::Spi0,
212///     ChipSelect::Cs0,
213///     100_000,
214///     SpiMode::Mode0,
215/// )
216/// .expect("Failed to create PiFace Digital");
217///
218/// // Take ownership of the output pin on bit 4 of the device.
219/// let pin = pfd
220///     .get_output_pin(4)
221///     .expect("Failed to get OutputPin");
222/// ```
223#[derive(Debug)]
224pub struct PiFaceDigital {
225    pfd_state: Rc<RefCell<PiFaceDigitalState>>,
226}
227
228impl PiFaceDigital {
229    /// Create a PiFace Digital instance.
230    pub fn new(
231        address: HardwareAddress,
232        spi_bus: SpiBus,
233        chip_select: ChipSelect,
234        spi_clock: u32,
235        spi_mode: SpiMode,
236    ) -> Result<Self> {
237        let mcp23s17 = Mcp23s17::new(address.into(), spi_bus, chip_select, spi_clock, spi_mode)?;
238        #[cfg(any(test, feature = "mockspi"))]
239        let pfd_state = PiFaceDigitalState { mcp23s17 };
240        #[cfg(not(any(test, feature = "mockspi")))]
241        let pfd_state = {
242            let gpio = Gpio::new()?;
243            let interrupt_pin = gpio.get(25)?.into_input();
244            PiFaceDigitalState {
245                mcp23s17,
246                _gpio: gpio,
247                interrupt_pin,
248            }
249        };
250        Ok(PiFaceDigital {
251            pfd_state: Rc::new(RefCell::new(pfd_state)),
252        })
253    }
254
255    /// Initialise the PiFace Digital I/O board.
256    ///
257    /// Ensures that the registers in the MCP23S17 are configured appropriately for the
258    /// hardware setup. In normal use, this function should always be called immediately
259    /// after construction of the [`PiFaceDigital`] instance. Only makes sense _not_ to
260    /// initialise the device in situations where there are multiple instances and you
261    /// can guarantee another instance has initialised the device and you don't want
262    /// your instance to overwrite what may now be non-default config (_e.g._ because
263    /// pins have been constructed.)
264    ///
265    /// ## Default settings
266    ///
267    /// | Register  | Purpose              | GPIO-A side<br>(Output) | GPIO-B side<br>(Input) |
268    /// |-----------|----------------------|:-----------:|:-----------:|
269    /// |  IODIR    | I/O direction        | 0x00        | 0xFF        |
270    /// |  IPOL     | Input polarity       | 0x00        | 0x00        |
271    /// |  GPINTEN  | GPIO interrupt enable| 0x00        | 0x00        |
272    /// |  DEFVAL   | Default value        | 0x00        | 0x00        |
273    /// |  INTCON   | Interrupt control    | 0x00        | 0x00        |
274    /// |  IOCON    | I/O control          | 0x28        | (Note 1)    |
275    /// |  GPPU     | GPIO pull-up         | 0x00        | 0xFF<br>(Note 3) |
276    /// |  INTF     | Interrupt Flag       |             |             |
277    /// |  INTCAP   | Interrupt capture    |             |             |
278    /// |  GPIO     | Input/Output         | 0x00        |             |
279    /// |  OLAT     | Output latch         | (Note 2)    |(Note 2)     |
280    ///
281    /// **Notes:**
282    ///
283    /// 1) There is only one IOCON register (though it appears at two addresses).
284    ///    The value written into IOCON represents:
285    ///
286    ///     - `BANK` off
287    ///     - `MIRROR` off
288    ///     - `SEQOP` off
289    ///     - `DISSLW` enabled (this is an I<sup>2</sup>C function so effectively "don't care")
290    ///     - `HAEN` on
291    ///     - `ODR` off
292    ///     - `INTPOL` active low
293    ///
294    ///    The duplicate IOCON register is not written by this function.
295    ///
296    /// 2) OLATA is not explicitly written by this function but the value written to
297    ///    GPIOA is also written through to OLATA by the device itself.
298    ///
299    /// 3) May mean that active digital inputs see an inappropriate pull-up load on
300    ///    initialisation, but avoids having floating inputs picking up noise (and,
301    ///    anyway, this is what the four push-switches on `GPIOB-0` to `GPIOB-3` are
302    ///    expecting!)
303    ///
304    /// Once the MCP23S17 is in the desired state, the interrupt line on the Raspberry
305    /// Pi's GPIO gets enabled.
306    pub fn init(&mut self) -> Result<()> {
307        info!("Initialise PiFaceDigital registers to default values");
308
309        // First ensure IOCON is correct so that register addressing is set appropriately.
310        // It can't be done in the table below because the bits() function isn't const.
311        let iocon = (IOCON::BANK_OFF
312            | IOCON::MIRROR_OFF
313            | IOCON::SEQOP_OFF
314            | IOCON::DISSLW_SLEW_RATE_CONTROLLED
315            | IOCON::HAEN_ON
316            | IOCON::ODR_OFF
317            | IOCON::INTPOL_LOW)
318            .bits();
319        self.pfd_state
320            .borrow()
321            .mcp23s17
322            .write(RegisterAddress::IOCON, iocon)?;
323
324        // There are no acknowledgements in the SPI protocol so read-back the value to
325        // assess whether there's actually anything connected.
326        if self
327            .pfd_state
328            .borrow()
329            .mcp23s17
330            .read(RegisterAddress::IOCON)?
331            != iocon
332        {
333            return Err(PiFaceDigitalError::NoHardwareDetected {
334                spi_bus: self.pfd_state.borrow().mcp23s17.get_spi_bus(),
335                hardware_address: self
336                    .pfd_state
337                    .borrow()
338                    .mcp23s17
339                    .get_hardware_address()
340                    .try_into()
341                    .expect("MCP23S17 hardware address limited to PiFace Digital range"),
342            });
343        }
344
345        // Log debug info about the current register state.
346        debug!("Uninitialised MCP23S17 state");
347        self.debug_current_state("Uninitialised MCP23S17 state:")?;
348
349        const RESET_REGISTER_STATES: [(RegisterAddress, Option<u8>); RegisterAddress::LENGTH] = [
350            (RegisterAddress::IODIRA, Some(0x00)),
351            (RegisterAddress::IODIRB, Some(0xFF)),
352            (RegisterAddress::IPOLA, Some(0x00)),
353            (RegisterAddress::IPOLB, Some(0x00)),
354            (RegisterAddress::GPINTENA, Some(0x00)),
355            (RegisterAddress::GPINTENB, Some(0x00)),
356            (RegisterAddress::DEFVALA, Some(0x00)),
357            (RegisterAddress::DEFVALB, Some(0x00)),
358            (RegisterAddress::INTCONA, Some(0x00)),
359            (RegisterAddress::INTCONB, Some(0x00)),
360            (RegisterAddress::IOCON, None),
361            (RegisterAddress::IOCON2, None),
362            (RegisterAddress::GPPUA, Some(0x00)),
363            (RegisterAddress::GPPUB, Some(0xFF)),
364            (RegisterAddress::INTFA, None),
365            (RegisterAddress::INTFB, None),
366            (RegisterAddress::INTCAPA, None),
367            (RegisterAddress::INTCAPB, None),
368            (RegisterAddress::GPIOA, Some(0x00)),
369            (RegisterAddress::GPIOB, None),
370            (RegisterAddress::OLATA, None),
371            (RegisterAddress::OLATB, None),
372        ];
373
374        for (register_address, default_value) in RESET_REGISTER_STATES {
375            if let Some(data) = default_value {
376                self.pfd_state
377                    .borrow()
378                    .mcp23s17
379                    .write(register_address, data)?;
380                debug!("New {register_address:?} register state: 0x{data:02x}");
381            }
382        }
383
384        // Log debug info about the updated register state.
385        debug!("Initialised MCP23S17 state");
386        self.debug_current_state("Initialised MCP23S17 state:")?;
387
388        // Enable the GPIO interrupts. The MCP23S17 should be in a state where all
389        // interrupts are disabled so there shouldn't be an immediate trigger.
390        #[cfg(not(any(test, feature = "mockspi")))]
391        self.pfd_state
392            .borrow_mut()
393            .interrupt_pin
394            .set_interrupt(Trigger::FallingEdge, None)?;
395
396        Ok(())
397    }
398
399    /// Returns an [`InputPin`] for the specified pin number configured as a
400    /// high-impedance input.
401    ///
402    /// If the pin is already in use, or the pin number `pin` is greater than 7 then
403    /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
404    /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
405    ///
406    /// After the [`InputPin`] goes out of scope, it can be retrieved again through
407    /// another `get_input_pin()` call.
408    ///
409    /// When constructed, the pin has interrupts disabled.
410    pub fn get_input_pin(&self, pin: u8) -> Result<InputPin> {
411        // Get the unconfigured Pin (assuming it's available) and then convert it to
412        // an InputPin (i.e. high-impedance input).
413        Ok(InputPin {
414            pin: self
415                .pfd_state
416                .borrow()
417                .mcp23s17
418                .get(rppal_mcp23s17::Port::GpioB, pin)?
419                .into_input_pin()?,
420            interrupts_enabled: false,
421            pfd_state: self.pfd_state.clone(),
422        })
423    }
424
425    /// Returns an [`InputPin`] for the specified pin number configured with a pull-up
426    /// resistor.
427    ///
428    /// If the pin is already in use, or the pin number `pin` is greater than 7 then
429    /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
430    /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
431    ///
432    /// After the [`InputPin`] goes out of scope, it can be retrieved again through
433    /// another `get_input_pin()` call.
434    ///
435    /// When constructed, the pin has interrupts disabled.
436    pub fn get_pull_up_input_pin(&self, pin: u8) -> Result<InputPin> {
437        // Get the unconfigured Pin (assuming it's available) and then convert it to
438        // an InputPin (i.e. high-impedance input).
439        Ok(InputPin {
440            pin: self
441                .pfd_state
442                .borrow()
443                .mcp23s17
444                .get(rppal_mcp23s17::Port::GpioB, pin)?
445                .into_pullup_input_pin()?,
446            interrupts_enabled: false,
447            pfd_state: self.pfd_state.clone(),
448        })
449    }
450
451    /// Returns an [`OutputPin`] for the specified pin number.
452    ///
453    /// If the pin is already in use, or the pin number `pin` is greater than 7 then
454    /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
455    /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
456    ///
457    /// After the [`OutputPin`] goes out of scope, it can be retrieved again through
458    /// another `get_output_pin()` call.
459    pub fn get_output_pin(&self, pin: u8) -> Result<OutputPin> {
460        // Get the unconfigured Pin (assuming it's available) and then convert it to
461        // an InputPin (i.e. high-impedance input).
462        Ok(self
463            .pfd_state
464            .borrow()
465            .mcp23s17
466            .get(rppal_mcp23s17::Port::GpioA, pin)?
467            .into_output_pin()?)
468    }
469
470    /// Returns an [`OutputPin`] for the specified pin number already set high.
471    ///
472    /// If the pin is already in use, or the pin number `pin` is greater than 7 then
473    /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
474    /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
475    ///
476    /// After the [`OutputPin`] goes out of scope, it can be retrieved again through
477    /// another `get_output_pin()` call.
478    pub fn get_output_pin_high(&self, pin: u8) -> Result<OutputPin> {
479        // Get the unconfigured Pin (assuming it's available) and then convert it to
480        // an InputPin (i.e. high-impedance input).
481        Ok(self
482            .pfd_state
483            .borrow()
484            .mcp23s17
485            .get(rppal_mcp23s17::Port::GpioA, pin)?
486            .into_output_pin_high()?)
487    }
488
489    /// Returns an [`OutputPin`] for the specified pin number already set low.
490    ///
491    /// If the pin is already in use, or the pin number `pin` is greater than 7 then
492    /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
493    /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
494    ///
495    /// After the [`OutputPin`] goes out of scope, it can be retrieved again through
496    /// another `get_output_pin()` call.
497    pub fn get_output_pin_low(&self, pin: u8) -> Result<OutputPin> {
498        // Get the unconfigured Pin (assuming it's available) and then convert it to
499        // an InputPin (i.e. high-impedance input).
500        Ok(self
501            .pfd_state
502            .borrow()
503            .mcp23s17
504            .get(rppal_mcp23s17::Port::GpioA, pin)?
505            .into_output_pin_high()?)
506    }
507
508    // Waits for interrupts across a set of InputPins (actual docs are included because
509    // two flavours of this function exist)
510    #[doc = include_str!("sync-interrupts.md")]
511    #[cfg(not(any(test, feature = "mockspi")))]
512    pub fn poll_interrupts<'a>(
513        &self,
514        pins: &[&'a InputPin],
515        reset: bool,
516        timeout: Option<Duration>,
517    ) -> Result<Option<Vec<(&'a InputPin, Level)>>> {
518        // Including a pin that can't raise interrupts is considered a coding error.
519        for pin in pins {
520            assert!(
521                pin.interrupts_enabled(),
522                "InputPin({}) included in poll() does not have interrupts enabled!",
523                pin.get_pin_number()
524            );
525        }
526
527        let mut pfd_state = self.pfd_state.borrow_mut();
528
529        match pfd_state.interrupt_pin.poll_interrupt(reset, timeout)? {
530            Some(_level) => {
531                // There was an interrupt so work out what pin/pins registered it and
532                // get the input capture so we can report the levels on the pins.
533                let interrupt_flags = pfd_state.mcp23s17.read(RegisterAddress::INTFB)?;
534                let input_port = pfd_state.mcp23s17.read(RegisterAddress::INTCAPB)?;
535                let mut interrupting_pins = Vec::new();
536
537                for pin in pins {
538                    let pin_no = pin.get_pin_number();
539                    if (interrupt_flags & (0x01 << pin_no)) != 0 {
540                        let level: Level = (input_port & (0x01 << pin_no)).into();
541                        debug!("Active interrupt on pin {pin_no} level {level}");
542                        interrupting_pins.push((*pin, level));
543                    }
544                }
545
546                // Finding no active interrupts may be intentional but most likely
547                // indicates a misconfiguration, so log a warning.
548                if interrupting_pins.is_empty() {
549                    warn!(
550                        "No interrupts on any of pins {pins:?} - will poll again but interrupt will have been lost!"
551                    );
552                }
553                Ok(Some(interrupting_pins))
554            }
555
556            // Poll timed out.
557            None => Ok(None),
558        }
559    }
560
561    // Dummy ("mockspi") version of synchronous interrupt poll (actual docs are included
562    // because two flavours of this function exist).
563    #[doc = include_str!("sync-interrupts.md")]
564    #[cfg(any(test, feature = "mockspi"))]
565    pub fn poll_interrupts<'a>(
566        &self,
567        pins: &[&'a InputPin],
568        _reset: bool,
569        _timeout: Option<Duration>,
570    ) -> Result<Option<Vec<(&'a InputPin, Level)>>> {
571        for pin in pins {
572            assert!(
573                pin.interrupts_enabled(),
574                "InputPin({}) included in poll() does not have interrupts enabled!",
575                pin.get_pin_number()
576            );
577        }
578        let mut _pfd_state = self.pfd_state.borrow_mut();
579
580        Ok(None)
581    }
582
583    // Asynchronous interrupt poll (actual docs are included because two flavours of this
584    // function exist).
585    #[doc = include_str!("async-interrupts.md")]
586    #[cfg(not(any(test, feature = "mockspi")))]
587    pub fn subscribe_async_interrupts<C: FnMut(GpioEvent) + Send + 'static>(
588        &mut self,
589        callback: C,
590    ) -> Result<()> {
591        self.pfd_state
592            .borrow_mut()
593            .interrupt_pin
594            .set_async_interrupt(Trigger::FallingEdge, None, callback)?;
595        Ok(())
596    }
597
598    // Dummy ("mockspi") version of asynchronous interrupt poll (actual docs are included
599    // because two flavours of this function exist).
600    #[doc = include_str!("async-interrupts.md")]
601    #[cfg(any(test, feature = "mockspi"))]
602    pub fn subscribe_async_interrupts<C: FnMut(bool) + Send + 'static>(
603        &mut self,
604        _callback: C,
605    ) -> Result<()> {
606        let mut _pfd_state = self.pfd_state.borrow_mut();
607        Ok(())
608    }
609
610    /// Clear the registered callback for interrupt notifications.
611    #[cfg(not(any(test, feature = "mockspi")))]
612    pub fn clear_async_interrupts(&mut self) -> Result<()> {
613        self.pfd_state
614            .borrow_mut()
615            .interrupt_pin
616            .clear_async_interrupt()?;
617        Ok(())
618    }
619
620    /// Register a callback for interrupt notifications.
621    #[cfg(any(test, feature = "mockspi"))]
622    pub fn clear_async_interrupts(&mut self) -> Result<()> {
623        let _pfd_state = self.pfd_state.borrow_mut();
624        Ok(())
625    }
626
627    /// Access the Interrupt Capture register for the input port.
628    pub fn get_interrupt_capture(&self) -> Result<u8> {
629        let data = self
630            .pfd_state
631            .borrow()
632            .mcp23s17
633            .read(RegisterAddress::INTCAPB)?;
634        Ok(data)
635    }
636
637    /// Access the Interrupt Capture register for the input port.
638    pub fn get_interrupt_flags(&self) -> Result<u8> {
639        let data = self
640            .pfd_state
641            .borrow()
642            .mcp23s17
643            .read(RegisterAddress::INTFB)?;
644        Ok(data)
645    }
646    /// Generate a debug log containing the state of the MCP23S17.
647    ///
648    /// If logging at `Debug` level, log the values currently in the MCP23S17's
649    /// registers, otherwise does nothing.
650    pub fn debug_current_state(&self, context: &str) -> Result<()> {
651        if log_enabled!(Debug) {
652            let mut state = String::new();
653            for register in 0..RegisterAddress::LENGTH {
654                let register_address = RegisterAddress::try_from(register).unwrap();
655                let data = self.pfd_state.borrow().mcp23s17.read(register_address)?;
656                writeln!(state, "{:10} : 0x{:02x}", register_address, data).unwrap();
657            }
658            debug!("{context}\n{state}");
659        }
660        Ok(())
661    }
662
663    /// In testing environments provide an API to get access to the mock SPI that
664    /// allows unit tests to be run without a real Raspberry Pi.
665    ///
666    /// Returns a tuple containing:
667    ///
668    /// - The current data in the mock SPI register `register`.
669    /// - The number of read accesses made to the register.
670    /// - The number of write accesses made to the register.
671    ///
672    /// ```
673    /// use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, RegisterAddress, SpiBus, SpiMode};
674    ///
675    /// let mut pfd = PiFaceDigital::new(
676    ///     HardwareAddress::new(0).unwrap(),
677    ///     SpiBus::Spi0,
678    ///     ChipSelect::Cs0,
679    ///     100_000,
680    ///     SpiMode::Mode0,
681    /// ).expect("Failed to construct!");
682    /// pfd.init().expect("Failed to initialise!");
683    ///
684    /// // The IOCON register gets set once and then read back by the initialise to
685    /// // test that there's actually some hardware connected. The 0x28 represents the
686    /// // default configuration.
687    /// assert_eq!(pfd.get_mock_data(RegisterAddress::IOCON),
688    ///     (0x28, 1, 1));
689    /// ```
690
691    #[cfg(any(test, feature = "mockspi"))]
692    pub fn get_mock_data(&self, register: RegisterAddress) -> (u8, usize, usize) {
693        self.pfd_state.borrow().mcp23s17.get_mock_data(register)
694    }
695
696    /// In testing environments provide an API to get access to the mock SPI that
697    /// allows unit tests to be run without a real Raspberry Pi.
698    ///
699    /// ```
700    /// use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, RegisterAddress, SpiBus, SpiMode};
701    ///
702    /// let mut pfd = PiFaceDigital::new(
703    ///     HardwareAddress::new(0).unwrap(),
704    ///     SpiBus::Spi0,
705    ///     ChipSelect::Cs0,
706    ///     100_000,
707    ///     SpiMode::Mode0,
708    /// ).expect("Failed to construct!");
709    ///
710    /// pfd.set_mock_data(RegisterAddress::IOCON, 0x55);
711    /// assert_eq!(pfd.get_mock_data(RegisterAddress::IOCON),
712    ///     (0x55, 0, 0));
713    ///
714    /// pfd.init().expect("Failed to initialise!");
715    ///
716    /// // The IOCON register gets set once and then read back by the initialise to
717    /// // test that there's actually some hardware connected. The 0x28 represents the
718    /// // default configuration.
719    /// assert_eq!(pfd.get_mock_data(RegisterAddress::IOCON),
720    ///     (0x28, 1, 1));
721    /// ```
722    #[cfg(any(test, feature = "mockspi"))]
723    pub fn set_mock_data(&self, register: RegisterAddress, data: u8) {
724        self.pfd_state
725            .borrow()
726            .mcp23s17
727            .set_mock_data(register, data)
728    }
729}
730
731impl Default for PiFaceDigital {
732    /// Creates a default PiFaceDigital that:
733    ///
734    /// - Is at hardware address `0`
735    /// - On SPI bus `Spi0`
736    /// - Uses chip-select `Cs0`
737    /// - Is clocked at 100kHz
738    /// - Uses SPI mode 0
739    fn default() -> Self {
740        PiFaceDigital::new(
741            HardwareAddress::new(0).unwrap(),
742            SpiBus::Spi0,
743            ChipSelect::Cs0,
744            100_000,
745            SpiMode::Mode0,
746        )
747        .expect("Failed to create default PiFaceDigital")
748    }
749}
750
751impl InputPin {
752    /// Reads the pin's logic level.
753    #[inline]
754    pub fn read(&self) -> Result<Level> {
755        Ok(self.pin.read()?)
756    }
757
758    /// Reads the pin's logic level, and returns [`true`] if it is set to
759    /// [`Level::Low`].
760    #[inline]
761    pub fn is_low(&self) -> Result<bool> {
762        Ok(self.pin.read()? == Level::Low)
763    }
764
765    /// Reads the pin's logic level, and returns [`true`] if it is set to
766    /// [`Level::High`].
767    #[inline]
768    pub fn is_high(&self) -> Result<bool> {
769        Ok(self.pin.read()? == Level::High)
770    }
771
772    /// Enable synchronous interrupts.
773    ///
774    /// Synchronous interrupts can be polled once enabled by either:
775    ///
776    /// - Calling [`InputPin::poll_interrupt()`] in the case where just one `InputPin`
777    ///   is configured to raise interrupts.
778    /// - Calling [`PiFaceDigital::poll_interrupts()`] in the case where more than one
779    ///   InputPin is configured to raise interrupts.
780    ///
781    /// Interrupts can be disabled by a call to [`InputPin::clear_interrupt()`] and
782    /// will also be automatically disabled when the `InputPin` is dropped.
783    pub fn set_interrupt(&mut self, mode: InterruptMode) -> Result<()> {
784        self.interrupts_enabled = true;
785        self.pin.set_interrupt_mode(mode).map_err(|e| e.into())
786    }
787
788    /// Disable synchronous interrupts on the pin.
789    ///
790    /// Note that:
791    ///
792    /// - Multiple calls to `clear_interrupt()` are permitted (though don't do anything
793    ///   useful).
794    /// - If not explicitly disabled, the interrupts will be disabled when the pin
795    ///   is dropped.
796    pub fn clear_interrupt(&mut self) -> Result<()> {
797        self.interrupts_enabled = false;
798        self.pin
799            .set_interrupt_mode(InterruptMode::None)
800            .map_err(|e| e.into())
801    }
802
803    /// Wait for an interrupt (or timeout) on this pin.
804    ///
805    /// Must only be called if interrupts have been enabled - calling with interrupts
806    /// disabled is considered a coding error and will panic.
807    ///
808    /// If `reset` is `true` it will cause the GPIO interrupts to be flushed before
809    /// starting the poll.
810    ///
811    /// If no interrupts have happened after `timeout`, the function will exit returning
812    /// `Ok(None))`.
813    ///
814    /// Note that interrupts will have been re-enabled by the time that the poll returns
815    /// so there may be repeated interrupts.
816    ///
817    /// ## Example usage
818    ///
819    /// ```no_run
820    /// use rppal_pfd::{ChipSelect, HardwareAddress, InterruptMode, PiFaceDigital, SpiBus, SpiMode};
821    /// use std::time::Duration;
822    ///
823    ///  let mut pfd = PiFaceDigital::new(
824    ///     HardwareAddress::new(0).unwrap(),
825    ///     SpiBus::Spi0,
826    ///     ChipSelect::Cs0,
827    ///     100_000,
828    ///     SpiMode::Mode0,
829    ///     )
830    ///     .expect("Failed to create PFD");
831    /// pfd.init().expect("Failed to initialise PFD");
832    ///
833    /// let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
834    /// pin.set_interrupt(InterruptMode::BothEdges)
835    ///     .expect("Failed to enable interrupts");
836    ///
837    /// match pin.poll_interrupt(false, Some(Duration::from_secs(60))) {
838    ///     Ok(Some(level)) => {
839    ///         println!("Button pressed!");
840    ///         let pin_no = pin.get_pin_number();
841    ///         println!("Interrupt: pin({pin_no}) is {level}");
842    ///     }
843    ///
844    ///     Ok(None) => {
845    ///         println!("Poll timed out");
846    ///     }
847    ///
848    ///     Err(e) => {
849    ///         eprintln!("Poll failed with {e}");
850    ///     }     
851    /// }
852    ///
853    #[cfg(not(any(test, feature = "mockspi")))]
854    pub fn poll_interrupt(
855        &mut self,
856        reset: bool,
857        timeout: Option<Duration>,
858    ) -> Result<Option<Level>> {
859        use std::time::Instant;
860
861        assert!(
862            self.interrupts_enabled,
863            "InputPin({}): No interrupts enabled before trying to poll()",
864            self.get_pin_number()
865        );
866
867        let wait_until = timeout.map(|delay| Instant::now() + delay);
868
869        // The interrupt line may be asserted by any pin on this device or, potentially,
870        // other PiFace Digital devices on the same SPI bus.  Check the relevant INTFB
871        // bit to see if this pin caused the interrupt.
872        loop {
873            let timeout = wait_until.map(|end_time| end_time - Instant::now());
874            let mut pfd_state = self.pfd_state.borrow_mut();
875            match pfd_state.interrupt_pin.poll_interrupt(reset, timeout)? {
876                Some(_level) => {
877                    if pfd_state
878                        .mcp23s17
879                        .get_bit(RegisterAddress::INTFB, self.pin.get_pin_number())?
880                        .into()
881                    {
882                        // We did raise the interrupt condition.
883                        info!("Received interrupt on pin {}", self.pin.get_pin_number());
884                        return Ok(Some(self.read()?));
885                    } else {
886                        // Wasn't this pin. We have to read the port to clear the
887                        // interrupt but this probably wasn't what was intended so raise
888                        // a warning.
889                        warn!(
890                            "Interrupt was not on pin {} - will poll again but interrupt will have been lost!",
891                            self.pin.get_pin_number()
892                        );
893                        let _ = self.read()?;
894                    }
895                }
896                None => return Ok(None),
897            }
898        }
899    }
900
901    /// Dummy version of interrupt poll routine for use in testing environments.
902    ///
903    /// Immediately returns as if a timeout occurred.
904    #[cfg(any(test, feature = "mockspi"))]
905    pub fn poll_interrupt(
906        &mut self,
907        _reset: bool,
908        _timeout: Option<Duration>,
909    ) -> Result<Option<Level>> {
910        assert!(
911            self.interrupts_enabled,
912            "InputPin({}): No interrupts enabled before trying to poll()",
913            self.get_pin_number()
914        );
915        Ok(None)
916    }
917
918    /// Get the pin number (0-7) that this pin is connected to.
919    pub fn get_pin_number(&self) -> u8 {
920        self.pin.get_pin_number()
921    }
922
923    /// Get the interrupt state.
924    pub fn interrupts_enabled(&self) -> bool {
925        self.interrupts_enabled
926    }
927}
928
929impl Drop for InputPin {
930    fn drop(&mut self) {
931        if self.interrupts_enabled {
932            self.clear_interrupt()
933                .expect("InputPin failed to clear interrupts on Drop");
934        }
935    }
936}
937
938#[cfg(test)]
939mod test {
940    use super::*;
941
942    #[test]
943    fn pfd_input_pin_poll_interrupt() {
944        let mut pfd = PiFaceDigital::new(
945            HardwareAddress::new(0).unwrap(),
946            SpiBus::Spi0,
947            ChipSelect::Cs0,
948            100_000,
949            SpiMode::Mode0,
950        )
951        .expect("Failed to create PFD");
952        pfd.init().expect("Failed to initialise PFD");
953
954        let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
955        pin.set_interrupt(InterruptMode::BothEdges)
956            .expect("Failed to enable interrupts");
957
958        assert_eq!(pin.poll_interrupt(false, None).expect("Bad poll"), None);
959    }
960
961    #[test]
962    #[should_panic]
963    fn pfd_input_pin_poll_interrupt_bad_config() {
964        let mut pfd = PiFaceDigital::new(
965            HardwareAddress::new(0).unwrap(),
966            SpiBus::Spi0,
967            ChipSelect::Cs0,
968            100_000,
969            SpiMode::Mode0,
970        )
971        .expect("Failed to create PFD");
972        pfd.init().expect("Failed to initialise PFD");
973
974        let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
975
976        let _ = pin.poll_interrupt(false, None).expect("Bad poll");
977    }
978
979    #[test]
980    fn pfd_input_pins_poll_interrupts() {
981        let mut pfd = PiFaceDigital::new(
982            HardwareAddress::new(0).unwrap(),
983            SpiBus::Spi0,
984            ChipSelect::Cs0,
985            100_000,
986            SpiMode::Mode0,
987        )
988        .expect("Failed to create PFD");
989        pfd.init().expect("Failed to initialise PFD");
990
991        let mut pin1 = pfd.get_input_pin(0).expect("Failed to get pin");
992        pin1.set_interrupt(InterruptMode::BothEdges)
993            .expect("Failed to enable interrupts");
994        let mut pin2 = pfd.get_input_pin(1).expect("Failed to get pin");
995        pin2.set_interrupt(InterruptMode::BothEdges)
996            .expect("Failed to enable interrupts");
997
998        let interrupt_pins = [&pin1, &pin2];
999        if let Some(interrupting_pins) = pfd
1000            .poll_interrupts(&interrupt_pins, false, None)
1001            .expect("Bad poll")
1002        {
1003            panic!("Not expecting any interrupts! Got: {interrupting_pins:?}")
1004        }
1005    }
1006
1007    #[test]
1008    #[should_panic]
1009    fn pfd_input_pins_poll_interrupts_bad_config() {
1010        let mut pfd = PiFaceDigital::new(
1011            HardwareAddress::new(0).unwrap(),
1012            SpiBus::Spi0,
1013            ChipSelect::Cs0,
1014            100_000,
1015            SpiMode::Mode0,
1016        )
1017        .expect("Failed to create PFD");
1018        pfd.init().expect("Failed to initialise PFD");
1019
1020        let mut pin1 = pfd.get_input_pin(0).expect("Failed to get pin");
1021        pin1.set_interrupt(InterruptMode::BothEdges)
1022            .expect("Failed to enable interrupts");
1023        let pin2 = pfd.get_input_pin(1).expect("Failed to get pin");
1024
1025        let interrupt_pins = [&pin1, &pin2];
1026        let _ = pfd.poll_interrupts(&interrupt_pins, false, None);
1027    }
1028
1029    #[test]
1030    fn pfd_input_pin_enable_interrupts() {
1031        let mut pfd = PiFaceDigital::new(
1032            HardwareAddress::new(0).unwrap(),
1033            SpiBus::Spi0,
1034            ChipSelect::Cs0,
1035            100_000,
1036            SpiMode::Mode0,
1037        )
1038        .expect("Failed to create PFD");
1039        pfd.init().expect("Failed to initialise PFD");
1040        assert_eq!(
1041            pfd.get_mock_data(RegisterAddress::GPINTENB),
1042            (0b0000_0000, 0, 1)
1043        );
1044
1045        {
1046            let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
1047            pin.set_interrupt(InterruptMode::BothEdges)
1048                .expect("Failed to enable interrupts");
1049            assert_eq!(
1050                pfd.get_mock_data(RegisterAddress::GPINTENB),
1051                (0b0000_0001, 1, 2)
1052            );
1053        }
1054        assert_eq!(
1055            pfd.get_mock_data(RegisterAddress::GPINTENB),
1056            (0b0000_0000, 2, 3)
1057        );
1058    }
1059
1060    #[test]
1061    fn pfd_input_pin_read_levels() {
1062        let mut pfd = PiFaceDigital::new(
1063            HardwareAddress::new(0).unwrap(),
1064            SpiBus::Spi0,
1065            ChipSelect::Cs0,
1066            100_000,
1067            SpiMode::Mode0,
1068        )
1069        .expect("Failed to create PFD");
1070        pfd.init().expect("Failed to initialise PFD");
1071
1072        let pin = pfd.get_input_pin(0).expect("Failed to get pin");
1073        assert!(pin.is_low().expect("Bad pin access"));
1074
1075        pfd.set_mock_data(RegisterAddress::GPIOB, 0b0000_0001);
1076        assert!(pin.is_high().expect("Bad pin access"));
1077    }
1078
1079    #[test]
1080    fn pfd_init() {
1081        let mut pfd = PiFaceDigital::new(
1082            HardwareAddress::new(0).unwrap(),
1083            SpiBus::Spi0,
1084            ChipSelect::Cs0,
1085            100_000,
1086            SpiMode::Mode0,
1087        )
1088        .expect("Failed to create PFD");
1089        pfd.init().expect("Failed to initialise PFD");
1090
1091        // Sample a few of the registers for correct values.
1092        assert_eq!(
1093            pfd.get_mock_data(RegisterAddress::IODIRA),
1094            (0x00, 0, 1),
1095            "Bad IODIRA"
1096        );
1097        assert_eq!(
1098            pfd.get_mock_data(RegisterAddress::IODIRB),
1099            (0xFF, 0, 1),
1100            "Bad IODIRB"
1101        );
1102        assert_eq!(
1103            pfd.get_mock_data(RegisterAddress::IOCON),
1104            (0x28, 1, 1),
1105            "Bad IOCON"
1106        );
1107        assert_eq!(
1108            pfd.get_mock_data(RegisterAddress::GPPUB),
1109            (0xFF, 0, 1),
1110            "Bad GPPUB"
1111        );
1112    }
1113
1114    #[test]
1115    fn pfd_init_no_hardware() {
1116        let mut pfd = PiFaceDigital::new(
1117            HardwareAddress::new(0).unwrap(),
1118            SpiBus::Spi6, // Magic value that makes mock simulate no hardware.
1119            ChipSelect::Cs0,
1120            100_000,
1121            SpiMode::Mode0,
1122        )
1123        .expect("Failed to create PFD");
1124        let init_result = pfd.init();
1125
1126        // Check we get the expected error.
1127        println!("{init_result:?}");
1128        match init_result {
1129            Err(PiFaceDigitalError::NoHardwareDetected {
1130                spi_bus: bus,
1131                hardware_address: address,
1132            }) => {
1133                assert_eq!(bus, SpiBus::Spi6);
1134                assert_eq!(address, 0.try_into().unwrap())
1135            }
1136            _ => panic!("Unexpected return result: {init_result:?}"),
1137        }
1138    }
1139
1140    #[test]
1141    fn pfd_address_to_mcp23s17_addr() {
1142        let pfd_addr = HardwareAddress::new(3).expect("valid HardwareAddress");
1143        let mcp_addr: rppal_mcp23s17::HardwareAddress = pfd_addr.into();
1144        assert_eq!(
1145            mcp_addr,
1146            rppal_mcp23s17::HardwareAddress::new(3).expect("valid HardwareAddress")
1147        );
1148    }
1149    #[test]
1150    fn good_hardware_address() {
1151        let addr = HardwareAddress::new(2).expect("Bad address");
1152        assert_eq!(2u8, addr.into(), "Unexpected address value");
1153    }
1154
1155    #[test]
1156    fn bad_hardware_address() {
1157        let addr = HardwareAddress::new(4);
1158        match addr {
1159            Err(PiFaceDigitalError::HardwareAddressBoundsError(4)) => (),
1160            _ => panic!("Unexpected return value: {addr:?}"),
1161        }
1162    }
1163
1164    #[test]
1165    fn try_into_good_hardware_address() {
1166        let addr: HardwareAddress = 3u8.try_into().expect("Bad address");
1167        assert_eq!(3u8, addr.into(), "Unexpected address value");
1168    }
1169
1170    #[test]
1171    fn try_into_bad_hardware_address() {
1172        let addr: Result<HardwareAddress> = 8u8.try_into();
1173        match addr {
1174            Err(PiFaceDigitalError::HardwareAddressBoundsError(8)) => (),
1175            _ => panic!("Unexpected return value: {addr:?}"),
1176        }
1177    }
1178}