issi_is31fl3731/
lib.rs

1// Copyright Open Logistics Foundation
2//
3// Licensed under the Open Logistics Foundation License 1.3.
4// For details on the licensing terms, see the LICENSE file.
5// SPDX-License-Identifier: OLFL-1.3
6
7#![cfg_attr(not(test), no_std)]
8
9//! This crate implements the audio modulated matrix LED driver IS31FL3731 by Integrated Silicon
10//! Solution Inc. (ISSI) ([Datasheet](https://www.issi.com/WW/pdf/31FL3731.pdf)).
11//!
12//! To reduce CPU usage, up to 8 frames can be stored with individual time delays between frames
13//! to play small animations automatically. LED frames can be modulated with audio signal, but is not implemented in this crate yet.
14//! The matrix driver can handle up to 144 separated LEDs which are aligned in two blocks.
15//! Each LED can be dimmed individually with 8-bit, resulting in 256 steps of dimming.
16//!
17//! # License
18//!
19//! Open Logistics Foundation License\
20//! Version 1.3, January 2023
21//!
22//! See the LICENSE file in the top-level directory.
23//!
24//! # Contact
25//! Fraunhofer IML Embedded Rust Group - <embedded-rust@iml.fraunhofer.de>
26
27use embedded_hal::blocking::i2c;
28
29/// Maximal possible dimension of a matrix
30///
31/// According to the maximal amount of LEDs (144 = 16 * 9)
32const MAX_WIDTH: usize = 16;
33const MAX_HEIGHT: usize = 9;
34
35/// Driver-specific quantized delay type
36///
37/// Several features in the LED matrix driver can be timed. In general, the timing is controlled by
38/// setting the value `A` as a few bits in a control register. To calculate the resulting delay,
39/// `A` is either multiplied with `TAU` which is the smallest quantization unit (e.g. for playing
40/// animations or blinking) or by calculating `TAU*2^A` (e.g. for the breath control feature).
41///
42/// The desired [`core::time::Duration`] is saturated, i.e. forced on the possible value range. If
43/// the requested delay can not be represented exactly, the value is rounded down, so the resulting
44/// delay will be shorter than the requested delay. The minimum and maximum delays are:
45/// * For the `A*TAU` case: min. `TAU`, max. `A_MAX*TAU`
46/// * For the `TAU*2^A` case: min. `TAU`, max. `TAU*2^A_MAX`
47///
48/// For the "frame delay time" for the autoplay feature, the special case `A=0 => FDT=64*TAU` is
49/// _ignored_! So the maximum "frame delay time" is 693ms instead of 704ms. This decision has been
50/// made because that exception is not documented for the "blink period time" for the blink
51/// feature.
52pub struct Delay<const TAU_US: u32, const A_MAX: u8, const EXPONENTIAL: bool> {
53    value: u8,
54    is_exact: bool,
55}
56
57// Custom log2 function
58// TODO Replace by https://doc.rust-lang.org/std/primitive.u128.html#method.log2
59// as soon as stabilized
60fn log2(mut value: u128) -> u8 {
61    let mut exponent = 0;
62    loop {
63        if value <= 1 {
64            return exponent;
65        }
66        value /= 2;
67        exponent += 1;
68    }
69}
70
71impl<const TAU_US: u32, const A_MAX: u8, const EXPONENTIAL: bool>
72    Delay<TAU_US, A_MAX, EXPONENTIAL>
73{
74    pub fn new(delay: core::time::Duration) -> Self {
75        let desired_micros = delay.as_micros();
76
77        let desired_value = if !EXPONENTIAL {
78            desired_micros / TAU_US as u128
79        } else {
80            log2(desired_micros / TAU_US as u128) as u128
81        };
82
83        if desired_value < 1 && !EXPONENTIAL {
84            Self {
85                value: 1,
86                is_exact: false,
87            }
88        } else if desired_value > A_MAX as u128 {
89            Self {
90                value: A_MAX,
91                is_exact: false,
92            }
93        } else {
94            let is_exact = if !EXPONENTIAL {
95                TAU_US as u128 * desired_value == desired_micros
96            } else {
97                TAU_US as u128 * (1 << desired_value) == desired_micros
98            };
99            Self {
100                value: desired_value as u8,
101                is_exact,
102            }
103        }
104    }
105
106    /// Returns the rounded microseconds
107    pub fn micros(&self) -> u32 {
108        if !EXPONENTIAL {
109            TAU_US * self.value as u32
110        } else {
111            TAU_US * (1 << self.value as u32)
112        }
113    }
114
115    /// Returns `A`
116    pub fn value(&self) -> u8 {
117        self.value
118    }
119
120    /// Returns if this quantized delay matches the desired delay exactly or if it was rounded or
121    /// saturated
122    pub fn is_exact(&self) -> bool {
123        self.is_exact
124    }
125}
126
127/// Command register address
128///
129/// The Command Register should be configured first after writing in the slave address to
130/// choose the available register (Frame Registers and Function Registers). Afterwards the chosen register can be written.
131const CMD_REG: u8 = 0xFD;
132
133/// Control Register stores on or off state for each LED (1 bit per LED => 18 Byte)
134const FRAME_CONTROL_REG: u8 = 0x00;
135
136/// Blink Register controls the blink function of each LED (1 bit per LED => 18 Byte)
137const FRAME_BLINK_REG: u8 = 0x12;
138
139/// PWM Register sets the duty cycle of each individual LED (8 bit per LED => 144 Byte)
140const FRAME_PWM_REG: u8 = 0x24;
141
142//const MIN_WAIT_BETWEEN_COMMAND_MS: u32 = 1;
143
144/// Public struct for holding a frame array with the given size
145pub struct Frame<const WIDTH: usize, const HEIGHT: usize> {
146    pub pixels: [[u8; WIDTH]; HEIGHT],
147}
148
149/// Power Pin of the Sensors
150///
151/// Allows the drivers user (!) to enable/disable the sensor while it is not in use for power saving.
152#[derive(PartialEq, Eq)]
153pub enum PowerActive<PwrPin: embedded_hal::digital::v2::OutputPin> {
154    High(PwrPin),
155    Low(PwrPin),
156    AlwaysOn,
157}
158
159/// Slave I2C addresses is determined by the AD pin of the driver
160#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
161pub enum Addr {
162    GND = 0b1110100, // AD pin connected to GND
163    VCC = 0b1110111, // AD pin connected to VCC
164    SCL = 0b1110101, // AD pin connected to SCL
165    SDA = 0b1110110, // AD pin connected to SDA
166}
167
168/// Register Definitions for the eight frames
169#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
170pub enum FrameId {
171    One = 0x00,
172    Two = 0x01,
173    Three = 0x02,
174    Four = 0x03,
175    Five = 0x04,
176    Six = 0x05,
177    Seven = 0x06,
178    Eight = 0x07,
179}
180
181/// Number of loops playing selection. Taken from table 10 in the datasheet.
182pub enum AnimationLoops {
183    Endless = 0x00,
184    One = 0x01,
185    Two = 0x02,
186    Three = 0x03,
187    Four = 0x04,
188    Five = 0x05,
189    Six = 0x06,
190    Seven = 0x07,
191}
192
193/// Register Definitions of page nine (Function Register). Taken from table 3 in the datasheet.
194#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
195pub enum Functions {
196    Config = 0x00,
197    PictureDisplay = 0x01,
198    AutoPlayCtrl1 = 0x02,
199    AutoPlayCtrl2 = 0x03,
200    DisplayOption = 0x05,
201    AutoSynch = 0x06,
202    FrameState = 0x07,
203    BreathCtrl1 = 0x08,
204    BreathCtrl2 = 0x09,
205    Shutdown = 0x0A,
206    AGCCtrl = 0x0B,
207    AudioADCRate = 0x0C,
208}
209
210/// Register Definitions of the Configuration. Taken from Table 8 in the datasheet.
211#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
212pub enum Mode {
213    Picture = 0x00,
214    AutoFramePlay = 0x01,
215    AudioFramePlay = 0x02,
216}
217
218#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
219pub enum Page {
220    Frame(FrameId),
221    Function,
222}
223
224impl Page {
225    fn id(&self) -> u8 {
226        match self {
227            Page::Frame(id) => *id as u8,
228            Page::Function => 0x0B, // Definition of the Function Register.
229        }
230    }
231}
232
233/// Register Definitions of the Shutdown. Taken from table 17 in the datasheet.
234#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
235enum ShutdownControl {
236    ShutdownMode = 0x00,
237    NormalOperation = 0x01,
238}
239
240/// Custom type for enabling or disabling the Blink functionality.
241#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
242pub enum Blink {
243    Enable = 0x01,
244    Disable = 0x00,
245}
246
247/// Custom type for setting the IntensityControl of the Blink functionality.
248///
249/// - `LikeFirstFrame` sets the intensity of each frame like the one of the first frame.
250/// - `Individual` takes the intensity of each frame independently.
251#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
252pub enum IntensityControl {
253    LikeFirstFrame = 0x01,
254    Individual = 0x00,
255}
256
257/// Custom type for enabling or disabling the Breath functionality.
258#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
259pub enum Breath {
260    Enable = 0x01,
261    Disable = 0x00,
262}
263
264/// Custom type for handling the matrix orientation.
265#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
266pub enum Orientation {
267    SensorUp,
268    SensorDown,
269}
270
271/// LED driver type
272///
273/// Stores the I2C address and the device peripherals.
274pub struct LedMatrix<PwrPin, ShutdownPin, I2C, const WIDTH: usize, const HEIGHT: usize>
275where
276    PwrPin: embedded_hal::digital::v2::OutputPin,
277    ShutdownPin: embedded_hal::digital::v2::OutputPin,
278    I2C: i2c::WriteRead + i2c::Write,
279{
280    addr: Addr,
281    power_active: PowerActive<PwrPin>,
282    shutdown_pin: ShutdownPin,
283    i2c: I2C,
284}
285
286#[derive(Debug)]
287pub enum Error<PwrPinErr, ShutdownPinErr, I2cErr> {
288    PwrPin(PwrPinErr),
289    ShutdownPin(ShutdownPinErr),
290    I2c(I2cErr),
291}
292
293// Helper macro so we do not have to type out the whole generic error type for each method
294macro_rules! GenericError {
295    () => {
296        Error<PwrPin::Error, ShutdownPin::Error, I2cErr>
297    };
298}
299
300impl<PwrPin, ShutdownPin, I2C, I2cErr, const WIDTH: usize, const HEIGHT: usize>
301    LedMatrix<PwrPin, ShutdownPin, I2C, WIDTH, HEIGHT>
302where
303    PwrPin: embedded_hal::digital::v2::OutputPin,
304    ShutdownPin: embedded_hal::digital::v2::OutputPin,
305    I2C: i2c::WriteRead<Error = I2cErr> + i2c::Write<Error = I2cErr>,
306{
307    /// Create a new instance of the LED driver
308    pub fn new(
309        addr: Addr,
310        power_active: PowerActive<PwrPin>,
311        shutdown_pin: ShutdownPin,
312        i2c: I2C,
313    ) -> Self {
314        Self {
315            addr,
316            power_active,
317            shutdown_pin,
318            i2c,
319        }
320    }
321
322    /// Select the Function Page and write a `value` to the register `reg`
323    fn write_function_register(
324        &mut self,
325        reg: Functions,
326        value: u8,
327    ) -> Result<(), GenericError!()> {
328        self.select_page(Page::Function)?;
329        self.i2c
330            .write(self.addr as u8, &[reg as u8, value])
331            .map_err(Error::I2c)
332    }
333
334    /// Writes the given `page` into the Command register, i.e. selects the given `page`
335    fn select_page(&mut self, page: Page) -> Result<(), GenericError!()> {
336        self.i2c
337            .write(self.addr as u8, &[CMD_REG, page.id()])
338            .map_err(Error::I2c)
339    }
340
341    /// Writes the Shutdown register
342    fn set_shutdown(&mut self, shutdown: ShutdownControl) -> Result<(), GenericError!()> {
343        self.write_function_register(Functions::Shutdown, shutdown as u8)
344    }
345
346    /// Writes to the Configuration register to set the `mode` and the start frame for the
347    /// AutoPlayAnimation (only needed for AutoPlay functionality)
348    fn set_configuration(
349        &mut self,
350        mode: Mode,
351        start_frame_autoplay: FrameId,
352    ) -> Result<(), GenericError!()> {
353        let value = (mode as u8) << 3 | start_frame_autoplay as u8;
354        self.write_function_register(Functions::Config, value)
355    }
356
357    /// Selects the PictureDisplay Mode and writes the `picture` frame
358    fn set_picture_display(&mut self, picture: FrameId) -> Result<(), GenericError!()> {
359        self.write_function_register(Functions::PictureDisplay, picture as u8)
360    }
361
362    // Reads the FrameStateRegister with Interrupt Bit of the Animation and current FrameId
363    fn write_read_register(&mut self, reg: Functions) -> Result<u8, GenericError!()> {
364        let mut buf = [0_u8; 1];
365        // self.i2c.write(self.addr as u8, &[reg as u8]).map(|_|());
366        // self.delay.delay_ms(10);
367        // self.i2c.read(self.addr as u8, &mut [buff]).map(|_|());
368        // self.delay.delay_ms(10);
369        self.i2c
370            .write_read(self.addr as u8, &[reg as u8], &mut buf)
371            .map_err(Error::I2c)?;
372
373        Ok(buf[0])
374    }
375
376    /// Power the driver on (true) or off (false)
377    ///
378    /// This method sets the power pin according to the bool which is given, but stays in software
379    /// "ShutdownMode". To enable the LEDs, the chip needs to be in "NormalOperation", which should
380    /// be set with the [`show_frame()`](LedMatrix::show_frame) method after writing to the desired
381    /// frame.
382    pub fn power_on(&mut self, on: bool) -> Result<(), GenericError!()> {
383        if !on {
384            self.set_shutdown(ShutdownControl::ShutdownMode)?;
385            self.shutdown_pin.set_low().map_err(Error::ShutdownPin)?;
386        }
387        match (&mut self.power_active, on) {
388            (PowerActive::High(pp), true) => pp.set_high(),
389            (PowerActive::Low(pp), true) => pp.set_low(),
390            (PowerActive::High(pp), false) => pp.set_low(),
391            (PowerActive::Low(pp), false) => pp.set_high(),
392            (_, _) => Ok(()),
393        }
394        .map_err(Error::PwrPin)?;
395        if on {
396            self.shutdown_pin.set_high().map_err(Error::ShutdownPin)?;
397            self.set_shutdown(ShutdownControl::ShutdownMode)?;
398        }
399        Ok(())
400    }
401
402    /// Writes an array of PWM values to the desired frame
403    ///
404    /// The `frame` will be selected through the `frame_id`. The driver will set each control and
405    /// PWM pin according to the data in the `frame` array. For used LEDs, the control pin will be
406    /// set to one and the PWM values will be set to the data in the `frame` array.
407    pub fn write_frame(
408        &mut self,
409        frame_id: FrameId,
410        frame: Frame<WIDTH, HEIGHT>,
411    ) -> Result<(), GenericError!()> {
412        let mut led_control = [0_u8; 1 + (MAX_WIDTH * MAX_HEIGHT) / 8];
413        let mut led_blink = [0_u8; 1 + (MAX_WIDTH * MAX_HEIGHT) / 8];
414        let mut led_pwm = [0_u8; 1 + (MAX_WIDTH * MAX_HEIGHT)];
415
416        // First byte of each register is the address
417        led_control[0] = FRAME_CONTROL_REG;
418        led_blink[0] = FRAME_BLINK_REG;
419        led_pwm[0] = FRAME_PWM_REG;
420
421        let mut counter = 0;
422        let mut control = 0;
423
424        for y in 0..MAX_HEIGHT {
425            for x in 0..MAX_WIDTH {
426                if x < WIDTH && y < HEIGHT {
427                    led_pwm[1 + y * MAX_WIDTH + x] = frame.pixels[y][x];
428                    control <<= 1;
429                    control |= 1;
430                }
431
432                counter += 1;
433
434                if counter % 8 == 0 {
435                    led_control[counter / 8] = control;
436                    control = 0;
437                }
438            }
439        }
440
441        self.select_page(Page::Frame(frame_id))?;
442        self.i2c
443            .write(self.addr as u8, &led_control)
444            .map_err(Error::I2c)?;
445        self.i2c
446            .write(self.addr as u8, &led_blink)
447            .map_err(Error::I2c)?;
448        self.i2c
449            .write(self.addr as u8, &led_pwm)
450            .map_err(Error::I2c)?;
451        Ok(())
452    }
453
454    /// Sets the driver to PictureMode and displays the given frame with `frame_id` on the LED matrix
455    pub fn show_frame(&mut self, frame_id: FrameId) -> Result<(), GenericError!()> {
456        self.set_configuration(Mode::Picture, FrameId::One)?; // ['FrameId'] irrelevant for PictureMode
457        self.set_picture_display(frame_id)?;
458        self.set_shutdown(ShutdownControl::NormalOperation)
459    }
460
461    /// Clears a frame, i.e. displays a zero array to the given `frame_id`
462    pub fn clear_frame(&mut self, frame_id: FrameId) -> Result<(), GenericError!()> {
463        let frame_zero = [[0_u8; WIDTH]; HEIGHT];
464        self.write_frame(frame_id, Frame { pixels: frame_zero })?;
465        self.show_frame(frame_id)
466    }
467
468    /// This method will read the FrameStateRegister of the LED driver. The Interrupt Bit (D4)
469    /// states if an Animation has finished or not. The Current Frame Display (D2:D0) stores the
470    /// currently selected frame.
471    pub fn read_frame_state(&mut self) -> Result<u8, GenericError!()> {
472        self.write_read_register(Functions::FrameState)
473    }
474
475    /// This method starts the AutoPlayAnimation feature
476    ///
477    /// The AutoPlayAnimation cycles from the `first_frame` to the `last_frame` with the
478    /// `frame_delay_time` between each frame.  The `frame_delay_time will be rounded to a
479    /// multiple of 11ms with a maximum delay of 693ms.  The amount of animation loops
480    /// is set by `repeats`. The type `AnimationLoops` occupies a range from `One` to
481    /// `Eight` and `Endless`.
482    pub fn play_animation(
483        &mut self,
484        first_frame: FrameId,
485        last_frame: FrameId,
486        repeats: AnimationLoops,
487        inter_frame_delay: Delay<11000, 63, false>,
488    ) -> Result<(), GenericError!()> {
489        self.set_configuration(Mode::AutoFramePlay, first_frame)?;
490
491        let mut num_of_frames_playing = last_frame as u8 - first_frame as u8 + 1;
492        // FNS 3 bit register: 0 => all frames, 1..7 => 1..7 frames playing
493        if num_of_frames_playing > 7 {
494            num_of_frames_playing = 0;
495        }
496
497        let auto_play_ctrl1 = (repeats as u8) << 4 | num_of_frames_playing;
498        self.write_function_register(Functions::AutoPlayCtrl1, auto_play_ctrl1)?;
499
500        let auto_play_ctrl2 = inter_frame_delay.value();
501        self.write_function_register(Functions::AutoPlayCtrl2, auto_play_ctrl2)?;
502        self.set_shutdown(ShutdownControl::NormalOperation)
503    }
504
505    /// Enables/Disables the Blink feature of the LED driver
506    ///
507    /// If `blink_enable` is set to `Enable` the led driver will blink the selected frame
508    /// `frame_id` with a duty cycle of 50% and a delay of `blink_period`. The `blink_period`
509    /// will be rounded to a multiple of 270ms with a maximum period of 1890ms.  The
510    /// `intensity_control` defines if the intensity equals Frame::One (LikeFirstFrame) or is
511    /// set for each frame individually (Individual).
512    pub fn blink_frame(
513        &mut self,
514        frame_id: FrameId,
515        blink_enable: Blink,
516        intensity_control: IntensityControl,
517        blink_period_time: Delay<270000, 7, false>,
518    ) -> Result<(), GenericError!()> {
519        let display_option =
520            (blink_enable as u8) << 3 | (intensity_control as u8) << 5 | blink_period_time.value();
521        self.select_page(Page::Frame(frame_id))?;
522        self.write_function_register(Functions::DisplayOption, display_option)?;
523
524        if blink_enable == Blink::Enable {
525            self.show_frame(frame_id)?;
526        }
527        Ok(())
528    }
529
530    /// Enables/Disables the Breath feature of the LED driver
531    ///
532    /// If `breath_enable` is set to `Enable` all LEDs will be fade in and out with the given
533    /// fade and extinguish times.  This feature is available in PictureMode as well as
534    /// AutoPlayAnimation. `fade_in_time` and `fade_out_time` will be rounded to a multiple of
535    /// 26ms each with a maximum duration of 182ms. `extinguish_time` will be rounded to a
536    /// multiple of 3.5ms with a maximum duration of 24.5ms.
537    pub fn breath_control(
538        &mut self,
539        breath_enable: Breath,
540        fade_in_time: Delay<26000, 7, true>,
541        fade_out_time: Delay<26000, 7, true>,
542        extinguish_time: Delay<3500, 7, true>,
543    ) -> Result<(), GenericError!()> {
544        let breath_ctrl1 = fade_out_time.value() << 4 | fade_in_time.value();
545        let breath_ctrl2 = (breath_enable as u8) << 4 | extinguish_time.value();
546        self.write_function_register(Functions::BreathCtrl1, breath_ctrl1)?;
547        self.write_function_register(Functions::BreathCtrl2, breath_ctrl2)
548    }
549}
550
551#[cfg(test)]
552mod test {
553    use super::*;
554    use core::time::Duration;
555
556    #[test]
557    fn test_log2() {
558        assert_eq!(log2(0), 0);
559        assert_eq!(log2(1), 0);
560        assert_eq!(log2(2), 1);
561        assert_eq!(log2(3), 1);
562        assert_eq!(log2(4), 2);
563        assert_eq!(log2(5), 2);
564        assert_eq!(log2(7), 2);
565        assert_eq!(log2(8), 3);
566        assert_eq!(log2(128), 7);
567        assert_eq!(log2(255), 7);
568        assert_eq!(log2(256), 8);
569        assert_eq!(log2(0xffffffff_ffffffff_ffffffff_ffffffff), 127);
570    }
571
572    #[test]
573    fn test_quantized_delay() {
574        let d = Delay::<1000, 10, false>::new(Duration::from_millis(1));
575        assert_eq!(d.value(), 1);
576        assert_eq!(d.is_exact(), true);
577        let d = Delay::<400, 10, false>::new(Duration::from_millis(1));
578        assert_eq!(d.value(), 2);
579        assert_eq!(d.is_exact(), false); // rounded
580        let d = Delay::<1000, 10, false>::new(Duration::from_millis(0));
581        assert_eq!(d.value(), 1);
582        assert_eq!(d.is_exact(), false); // desired too low
583        let d = Delay::<1000, 10, false>::new(Duration::from_millis(100));
584        assert_eq!(d.value(), 10);
585        assert_eq!(d.is_exact(), false); // desired too high
586        let d = Delay::<1000, 10, true>::new(Duration::from_millis(1));
587        assert_eq!(d.value(), 0);
588        assert_eq!(d.is_exact(), true);
589        let d = Delay::<1000, 10, true>::new(Duration::from_millis(16));
590        assert_eq!(d.value(), 4);
591        assert_eq!(d.is_exact(), true);
592        let d = Delay::<1000, 20, true>::new(Duration::from_millis(17));
593        assert_eq!(d.value(), 4);
594        assert_eq!(d.is_exact(), false); // rounded
595        let d = Delay::<1000, 20, false>::new(Duration::from_millis(17));
596        assert_eq!(d.value(), 17);
597        assert_eq!(d.is_exact(), true); // with linear it is possible
598        let d = Delay::<1000, 10, true>::new(Duration::from_micros(500));
599        assert_eq!(d.value(), 0);
600        assert_eq!(d.is_exact(), false); // too low
601        let d = Delay::<1000, 10, true>::new(Duration::from_millis(1025));
602        assert_eq!(d.value(), 10);
603        assert_eq!(d.is_exact(), false); // rounded
604    }
605}