Skip to main content

four_letter_phat_rs/
lib.rs

1//! # [four-letter-phat-rs](https://gitlab.com/tti0/four-letter-phat-rs)
2//!
3//! four-letter-phat-rs is a Rust driver for I2C alphanumeric displays consisting of 4 14-segment
4//! displays driven by a HT16K33 LED controller. Examples of these are the Pimoroni
5//! [Four Letter pHAT](https://shop.pimoroni.com/products/four-letter-phat) and the Adafruit
6//! [Quad Alphanumeric Display Backpack](https://www.adafruit.com/product/2160).
7//!
8//! It is agnostic to the underlying I2C device used to interface with the display, and works in
9//! `no_std` environments without an allocator.
10
11#![no_std]
12
13use embedded_hal::{delay::DelayNs, i2c::I2c};
14use embedded_hal_compat::{Reverse, ReverseCompat};
15use ht16k33::{HT16K33, LedLocation};
16use phf::phf_map;
17
18pub use ht16k33::{Dimming, Display};
19
20mod error;
21pub use error::PhatError;
22
23// Implementation note
24//
25// There are four "digits" on Four Letter pHAT displays. Each digit corresponds to two "rows" in the
26// HT16K33 multiplex, and segments within each digit correspond to "commons" in the multiplex. We
27// represent the segments required for each character as a tuple of 2 arrays, where tuple.0 is the
28// commons required for the digit's first row, and tuple.1 is the commons required for the digit's
29// second row.
30
31const ALLOWED_POSITIONS: [u8; 4] = [0, 1, 2, 3];
32
33const ALL_LEDS: ([u8; 8], [u8; 6]) = ([0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5]);
34
35static CHARACTER_TO_COMMONS: phf::Map<char, (&'static [u8], &'static [u8])> = phf_map! {
36    ' ' => (&[], &[]),
37    '!' => (&[1, 2], &[]),
38    '"' => (&[5], &[1]),
39    '#' => (&[1, 2, 3, 6, 7], &[1, 4]),
40    '$' => (&[0, 2, 3, 5, 6, 7], &[1, 4]),
41    '£' => (&[3, 7], &[2, 3]),
42    '%' => (&[2, 5], &[2, 3]),
43    '&' => (&[0, 2, 3, 4, 6], &[0, 1, 5]),
44    '\'' => (&[], &[2]),
45    '(' => (&[], &[2, 5]),
46    ')' => (&[], &[0, 3]),
47    '*' => (&[6, 7], &[0, 1, 2, 3, 4, 5]),
48    '+' => (&[6, 7], &[1, 4]),
49    ',' => (&[], &[3]),
50    '-' => (&[6, 7], &[]),
51    '.' => (&[], &[]),
52    '/' => (&[], &[2, 3]),
53    '0' => (&[0, 1, 2, 3, 4, 5], &[2, 3]),
54    '1' => (&[1, 2], &[]),
55    '2' => (&[0, 1, 3, 4, 6, 7], &[]),
56    '3' => (&[0, 1, 2, 3, 7], &[]),
57    '4' => (&[1, 2, 5, 6, 7], &[]),
58    '5' => (&[0, 3, 5, 6], &[5]),
59    '6' => (&[0, 2, 3, 4, 5, 6, 7], &[]),
60    '7' => (&[0, 1, 2], &[]),
61    '8' => (&[0, 1, 2, 3, 4, 5, 6, 7], &[]),
62    '9' => (&[0, 1, 2, 3, 5, 6, 7], &[]),
63    ':' => (&[], &[1, 4]),
64    ';' => (&[], &[1, 3]),
65    '<' => (&[], &[2, 5]),
66    '=' => (&[3, 6, 7], &[]),
67    '>' => (&[], &[0, 3]),
68    '?' => (&[0, 1, 7], &[4]),
69    '@' => (&[0, 1, 3, 4, 5, 7], &[1]),
70    'A' => (&[0, 1, 2, 4, 5, 6, 7], &[]),
71    'B' => (&[0, 1, 2, 3, 7], &[1, 4]),
72    'C' => (&[0, 3, 4, 5], &[]),
73    'D' => (&[0, 1, 2, 3], &[1, 4]),
74    'E' => (&[0, 3, 4, 5, 6, 7], &[]),
75    'F' => (&[0, 4, 5, 6], &[]),
76    'G' => (&[0, 2, 3, 4, 5, 7], &[]),
77    'H' => (&[1, 2, 4, 5, 6, 7], &[]),
78    'I' => (&[], &[1, 4]),
79    'J' => (&[1, 2, 3, 4], &[]),
80    'K' => (&[4, 5, 6], &[2, 5]),
81    'L' => (&[3, 4, 5], &[]),
82    'M' => (&[1, 2, 4, 5], &[0, 2]),
83    'N' => (&[1, 2, 4, 5], &[0, 5]),
84    'O' => (&[0, 1, 2, 3, 4, 5], &[]),
85    'P' => (&[0, 1, 4, 5, 6, 7], &[]),
86    'Q' => (&[0, 1, 2, 3, 4, 5], &[5]),
87    'R' => (&[0, 1, 4, 5, 6, 7], &[5]),
88    'S' => (&[0, 2, 3, 5, 6, 7], &[]),
89    'T' => (&[0], &[1, 4]),
90    'U' => (&[1, 2, 3, 4, 5], &[]),
91    'V' => (&[4, 5], &[2, 3]),
92    'W' => (&[1, 2, 4, 5], &[3, 5]),
93    'X' => (&[], &[0, 2, 3, 5]),
94    'Y' => (&[], &[0, 2, 4]),
95    'Z' => (&[0, 3], &[2, 3]),
96    '[' => (&[0, 3, 4, 5], &[]),
97    '\\' => (&[], &[0, 5]),
98    ']' => (&[0, 1, 2, 3], &[]),
99    '^' => (&[0, 1], &[2, 3]),
100    '_' => (&[3], &[]),
101    '`' => (&[], &[0]),
102    'a' => (&[3, 4, 6], &[4]),
103    'b' => (&[3, 4, 5, 6], &[5]),
104    'c' => (&[3, 4, 6, 7], &[]),
105    'd' => (&[1, 2, 3, 7], &[3]),
106    'e' => (&[3, 4, 6], &[3]),
107    'f' => (&[0, 4, 5, 6], &[]),
108    'g' => (&[1, 2, 3, 7], &[2]),
109    'h' => (&[4, 5, 6], &[4]),
110    'i' => (&[], &[4]),
111    'j' => (&[1, 2, 3], &[]),
112    'k' => (&[], &[1, 2, 4, 5]),
113    'l' => (&[4, 5], &[]),
114    'm' => (&[2, 4, 6, 7], &[4]),
115    'n' => (&[4, 6], &[4]),
116    'o' => (&[2, 3, 4, 6, 7], &[]),
117    'p' => (&[4, 5, 6], &[0]),
118    'q' => (&[1, 2, 7], &[2]),
119    'r' => (&[4, 6], &[]),
120    's' => (&[3, 7], &[5]),
121    't' => (&[3, 4, 5, 6], &[]),
122    'u' => (&[2, 3, 4], &[]),
123    'v' => (&[2], &[5]),
124    'w' => (&[2, 4], &[3, 5]),
125    'x' => (&[6, 7], &[3, 5]),
126    'y' => (&[2, 3], &[5]),
127    'z' => (&[3, 6], &[3]),
128    '{' => (&[0, 3, 6], &[0, 3]),
129    '|' => (&[], &[1, 4]),
130    '}' => (&[0, 3, 7], &[2, 5]),
131    '~' => (&[5], &[0, 2]),
132};
133
134/// A board with 4 14-segment displays and a HT16K33 driver
135///
136/// Pimoroni Four Letter pHATs or Adafruit Quad Alphanumeric Display Backpacks can be driven with
137/// this struct, but we refer to all these boards as "Four Letter pHATs" in this library.
138pub struct FourLetterPhat<I2C> {
139    driver: HT16K33<Reverse<I2C>>,
140}
141
142impl<I2C: I2c<Error = I2CE>, I2CE: core::fmt::Debug> FourLetterPhat<I2C> {
143    fn position_to_rows(position: u8) -> Result<(u8, u8), PhatError<I2CE>> {
144        if !ALLOWED_POSITIONS.contains(&position) {
145            return Err(PhatError::InvalidCharacter);
146        }
147
148        Ok((2 * position, 2 * position + 1))
149    }
150
151    /// Initialise a Four Letter pHAT connected to a specified I2C device
152    ///
153    /// # Arguments
154    ///
155    /// * `i2c` - the I2C device to which the Four Letter pHAT is connected
156    pub fn new(i2c: I2C, address: u8) -> Result<FourLetterPhat<I2C>, PhatError<I2CE>> {
157        let i2c_hal_v0 = i2c.reverse();
158
159        let mut driver = HT16K33::new(i2c_hal_v0, address);
160        driver.initialize()?;
161        driver.set_display(Display::ON)?;
162        driver.set_dimming(Dimming::BRIGHTNESS_MAX)?;
163        Ok(FourLetterPhat { driver })
164    }
165
166    /// Turn off all the segments of all positions (including decimal points) on the Four Letter pHAT
167    pub fn clear_display(&mut self) -> Result<(), PhatError<I2CE>> {
168        self.driver.clear_display_buffer();
169        self.driver.write_display_buffer()?;
170
171        Ok(())
172    }
173
174    /// Turn off all the segments of the specified digit on the Four Letter pHAT
175    ///
176    /// # Arguments
177    ///
178    /// * `position` - the digit on the pHAT which should be cleared (one of [0, 1, 2, 3], numbered
179    ///   from left to right)
180    pub fn clear_digit(&mut self, position: u8) -> Result<(), PhatError<I2CE>> {
181        let rows = Self::position_to_rows(position)?;
182
183        for common in ALL_LEDS.0.iter() {
184            self.driver.set_led(
185                LedLocation::new(rows.0, *common).map_err(PhatError::HT16K33Validation)?,
186                false,
187            )?;
188        }
189
190        for common in ALL_LEDS.1.iter() {
191            self.driver.set_led(
192                LedLocation::new(rows.1, *common).map_err(PhatError::HT16K33Validation)?,
193                false,
194            )?;
195        }
196
197        Ok(())
198    }
199
200    /// Display a character on the specified digit of the Four Letter pHAT
201    ///
202    /// # Arguments
203    ///
204    /// * `position` - the digit on the pHAT where the character should be displayed (one of
205    ///   [0, 1, 2, 3], numbered from left to right)
206    /// * `character` - a character to be displayed
207    pub fn set_digit(&mut self, position: u8, character: char) -> Result<(), PhatError<I2CE>> {
208        let rows = Self::position_to_rows(position)?;
209
210        self.clear_digit(position)?;
211
212        if let Some(commons) = CHARACTER_TO_COMMONS.get(&character) {
213            for common in commons.0.iter() {
214                self.driver.update_display_buffer(
215                    LedLocation::new(rows.0, *common).map_err(PhatError::HT16K33Validation)?,
216                    true,
217                );
218            }
219
220            for common in commons.1.iter() {
221                self.driver.update_display_buffer(
222                    LedLocation::new(rows.1, *common).map_err(PhatError::HT16K33Validation)?,
223                    true,
224                );
225            }
226
227            self.driver.write_display_buffer()?;
228
229            Ok(())
230        } else {
231            Err(PhatError::InvalidCharacter)
232        }
233    }
234
235    /// Turn off the specified decimal point on the Four Letter pHAT
236    ///
237    /// # Arguments
238    ///
239    /// * `position` - the decimal point on the pHAT which should be turned off (one of
240    ///   [0, 1, 2, 3], numbered from left to right)
241    pub fn clear_decimal(&mut self, position: u8) -> Result<(), PhatError<I2CE>> {
242        let row = Self::position_to_rows(position)?.1;
243
244        self.driver.set_led(
245            LedLocation::new(row, 6).map_err(PhatError::HT16K33Validation)?,
246            false,
247        )?;
248
249        Ok(())
250    }
251
252    /// Turn on the specified decimal point on the Four Letter pHAT
253    ///
254    /// # Arguments
255    ///
256    /// * `position` - the decimal point on the pHAT which should be turned on (one of [0, 1, 2, 3],
257    ///   numbered from left to right)
258    pub fn set_decimal(&mut self, position: u8) -> Result<(), PhatError<I2CE>> {
259        let row = Self::position_to_rows(position)?.1;
260
261        self.driver.set_led(
262            LedLocation::new(row, 6).map_err(PhatError::HT16K33Validation)?,
263            true,
264        )?;
265
266        Ok(())
267    }
268
269    /// Display a left-aligned string of no more than 4 characters on the Four Letter pHAT
270    ///
271    /// # Arguments
272    ///
273    /// * `message` - the string to display (at most 4 characters)
274    pub fn print_str(&mut self, message: &str) -> Result<(), PhatError<I2CE>> {
275        if message.len() > 4 {
276            return Err(PhatError::StringLength);
277        }
278
279        self.clear_display()?;
280
281        for (position, char) in message.chars().enumerate() {
282            self.set_digit(position as u8, char)?;
283        }
284
285        Ok(())
286    }
287
288    /// Scroll a string (of any length) of characters across the Four Letter pHAT, from right to
289    /// left
290    ///
291    /// # Arguments
292    ///
293    /// * `message` - the string of characters to scroll
294    /// * `delay` - a struct which implements the blocking DelayMs trait from embedded-hal, used to
295    ///   pause between displaying frames of the message without relying on std::thread::sleep
296    /// * `tempo_ms` - how often the scrolling message should be advanced, in milliseconds
297    pub fn scroll_print_str(
298        &mut self,
299        message: &str,
300        mut delay: impl DelayNs,
301        tempo_ms: u32,
302    ) -> Result<(), PhatError<I2CE>> {
303        let frames_count = message.len().saturating_sub(3);
304
305        let chars = message.chars();
306
307        let mut iterators = [
308            #[allow(clippy::iter_skip_zero)]
309            chars.clone().skip(0),
310            chars.clone().skip(1),
311            chars.clone().skip(2),
312            chars.skip(3),
313        ];
314
315        for _ in 0..frames_count {
316            for (pos, iterator) in iterators.iter_mut().enumerate() {
317                self.set_digit(pos as u8, iterator.next().unwrap_or(' '))?;
318            }
319            delay.delay_ms(tempo_ms);
320        }
321
322        self.clear_display()?;
323
324        Ok(())
325    }
326
327    /// Configure the display mode of all segments of the Four Letter pHAT, by configuring the
328    /// underlying HT16K33 driver
329    ///
330    /// This can be used to turn the display on or off, or to cause it to blink at a specified frequency
331    ///
332    /// # Arguments
333    ///
334    /// * `display` - the new display mode of the display
335    pub fn set_display(&mut self, display: Display) -> Result<(), PhatError<I2CE>> {
336        self.driver.set_display(display)?;
337
338        Ok(())
339    }
340
341    /// Set the brightness of all segments on the Four Letter pHAT, by configuring the underlying
342    /// HT16K33 driver
343    ///
344    /// # Arguments
345    ///
346    /// * `dimming` - the new brightness level of the display
347    pub fn set_dimming(&mut self, dimming: Dimming) -> Result<(), PhatError<I2CE>> {
348        self.driver.set_dimming(dimming)?;
349
350        Ok(())
351    }
352}