DEV Community

Cover image for Digital Clock using Rust (toy project)
Nirmalya Sengupta
Nirmalya Sengupta

Posted on • Updated on

Digital Clock using Rust (toy project)

Attribution: Cover image by Image by rawpixel.com on Freepik

I am a Rust newbie. Like many other Rust (#rustlang) learners, I am also cutting my teeth on the language by paging through the Rust book (Link) and the one from Oreilly (Link), solving small puzzles, writing code snippets, and rummaging through a host of helpful blog-posts, as well as questions/answers on Stackoverflow and Rust users' group (Link).

On one such evening, while wandering through Youtube, I had come across this particular video (Link) by (Andy Thomason). In the video, Andy codes online, and takes us through a small application which brings up a digital clock on the screen. I found that quite interesting, especially the way he modeled the arms of the digits (code is here). The code was wonderfully concise and posed no problem for a newbie like me to follow.

I thought I will take this idea further, primarily to strengthen my Rust skills. My intention has been to emulate a few things while writing this toy application:

  • To model a 7-Led BCD decoder for each digit
  • To use special ASCII characters to represent each Led and adopt ANSI Escape Sequences for cursor positioning and drawing on a text - xterm, in my case - terminal (note: Andy did the same, I just followed his footsteps)
  • To behave as if the clock's ticks are coming in as external signals, as opposed to sleeping between two successive calls to get current time
  • To trap a CTRL-C command (that stops the application) and ensure that the cursor is back to its regular blinking behaviour

Note: Let me repeat that all the above are emulations, an attempt to highlight the core behaviour; no hardware is interfaced with the toy application I have written.


BCD for each digit

For a realistic clock ( HH:MM:SS ) to appear on the screen, 6 digits are needed, namely H, H, M, M, S, S. Each of these digits is supposed to be a 7-segment display unit.

Typical 7-segment display electronic unit
(Source: https://en.wikipedia.org/wiki/Seven-segment_display)

For a digit to be visible, several of these segments - also called LEDs (or LCDs) - should glow and other should remain unlit. The LEDs are numbered - lettered, to be more accurate - thus:
Enumerated 7 LEDs

Source: https://en.wikipedia.org/wiki/Seven-segment_display

Each digit can be one of the 10 values, namely '0' to '9'. For example, for displaying digits '6', '0' and '2', the LEDs should light up as below:

Lit and unlit LEDs

Source: https://www.electricaltechnology.org/2018/05/bcd-to-7-segment-display-decoder.html

The following table shows, the input signal combinations vs the corresponding LED-borne digits:
Truth Table for 3 digits

The point is that using a 4-bit input set - referred to as Binary Coded Decimals (BCD) - all the 10 digits can be displayed. (the truth-table is in the README).

I have modeled:

  • every LED as a struct :
// Datatype that captures a Led
pub struct Led {
    name: String,
    show_character: String,
    hide_character: String ,
    light_status: bool,
    on_receiving_next_signal: fn(&u8) -> bool // Closure that evaluates a BCD-signal, before deciding what the light_status should become
}
Enter fullscreen mode Exit fullscreen mode
  • A BCD signal as an u8 (unsigned 8-bit):
pub struct Nibbles(pub u8);
Enter fullscreen mode Exit fullscreen mode

The 4 most-significant-bits (MSB) of this u8 are ignored always and the 4 least-significant-bits (LSB) represent the BCD for each of 10 digits. Moreover, 4 LSB combinations which represent values '10' to '15' are ignored as well (the truth-table is in the README).

Logic to determine if the LED should be lit

Using Karnaugh Map, the minimal boolean signal combination is arrived at, for every LED. For example, based on the table above, the boolean logic that leads to a lit or unlit LED 'a' is this:

const LED_A_GATE_LOGIC: fn(&u8) -> bool = | input: &u8 | {
    // 8-bits and BCD (MSBs start from leftmost)
    // 0       0       0       0       A       B      C      D
    // Using karnaugh Map and don't care conditions: A + C + B.D + ~B.~D

    (input & 0b00001000 == 0x08)              // *nibble_a == 1u8
        || (input & 0b00000010 == 0x02)       // *nibble_c == 1u8
        || (input & 0b00000101 == 0x05)       // (*nibble_b == 1u8 && *nibble_d == 1u8)
        || (input & 0b00000101 ==  0x00)      // (*nibble_b == 0u8 && *nibble_d == 0u8)
};
Enter fullscreen mode Exit fullscreen mode

While constructing any specific LED, the logic is passed on as a closure. By defining them as const, I find it easier to read, test and if necessary, modify them:

pub fn new(name: &str, displayChar: &str, hide_character: &str, evaluator: fn(&u8) -> bool) -> Led {
        Led {
            name: name.to_string(),
            show_character: displayChar.to_string(),
            hide_character: hide_character.to_string(),
            light_status: false,
            on_receiving_next_signal: evaluator
        }
    }
Enter fullscreen mode Exit fullscreen mode

Every LED has a show_character and a hide_character to represent its display in the lit and unlit mode. I have used special ASCII characters to help show a LED on the terminal. For example, for LED 'a', it is "━━━━" and for LED 'b', it is " ┃": the space prefixed is for easier positioning on the screen. The section below exemplifies this.


At the call-site of calling the constructor of LED 'a':

Led::new("a", "━━━━", "    ", LED_A_GATE_LOGIC /* closure as a const */);
Enter fullscreen mode Exit fullscreen mode

Data and Code structure

This is the easier part. Using regular OO technique:

  • A DisplayUnit is composed of 7 LEDs of its own and is responsible for constructing those
  • A ScreenClock is composed of 6 DisplayUnits and is responsible for constructing those

A DisplayUnit is a struct:

pub struct DigitDisplayUnit {
    // TODO: These leds should be in a map, identifiable  by the letter associated
    led_a: Led,
    led_b: Led,
    led_c: Led,
    led_d: Led,
    led_e: Led,
    led_f: Led,
    led_g: Led,
}

impl DigitDisplayUnit {
    pub fn new() -> DigitDisplayUnit {
        let leda = Led::new("a", "━━━━", "    ", LED_A_GATE_LOGIC);
        let ledb = Led::new("b", " ┃", "  ",     LED_B_GATE_LOGIC);
       // ...

        DigitDisplayUnit {
            led_a: leda,
            led_b: ledb,
            // ..
        }
    }
Enter fullscreen mode Exit fullscreen mode

So, to create a ScreenClock, we do this:

pub struct ScreenClock {
    top_left_row: u8,
    top_left_col: u8,
    display_units: [DigitDisplayUnit;6],
}

impl ScreenClock {
    pub fn new( start_at_row: u8, start_at_col: u8 ) -> ScreenClock {

        // From left to right, on the display panel!
        let digital_display_unit0 = DigitDisplayUnit::new(); // h_ of hh
        let digital_display_unit1 = DigitDisplayUnit::new(); // _h of hh
        let digital_display_unit2 = DigitDisplayUnit::new(); // m_ of mm
        let digital_display_unit3 = DigitDisplayUnit::new(); // _m of mm
        let digital_display_unit4 = DigitDisplayUnit::new(); // s_ of ss
        let digital_display_unit5 = DigitDisplayUnit::new(); // _s of ss


        ScreenClock {
            top_left_row: start_at_row,
            top_left_col: start_at_col,
            display_units: [
                digital_display_unit0,
                digital_display_unit1,
                digital_display_unit2,
                digital_display_unit3,
                digital_display_unit4,
                digital_display_unit5,
            ]
        }
    }
// ... rest of it
Enter fullscreen mode Exit fullscreen mode

Once the skeleton is in place, solution becomes apparent to one:

  • Decide the position of the clock on the screen (row / column)
  • Create a ScreenClock ( refer to the constructor new)
  • Every second on the system's own date/time -- Pass the current hour/minute/second readings to ScreenClock -- Refresh the ScreenClock (i.e. produce the LEDs on the screen)

On the left, this digital clock and on the right, Unix date command's output, on my Ubuntu 22.10 laptop, with terminal type xterm-256color :

Screen play


Respond to a tick

The simplest way to get the current system time every second is to use Local::now() from the chrono:: crate, after a second's duration and sleeping in between. However, my intention has been to implement a behaviour of reaction: let a nudge arrive at a second's frequency indicating that a second has elapsed and then, the current time be read again. This way there is no enforced sleeping.

It turns out that crossbeam:: crate has exactly the facility that I need, a function named tick:

crossbeam_channel::channel
pub fn tick(duration: Duration)
Enter fullscreen mode Exit fullscreen mode

This is how I set it up:

let notification_on_next_second = tick(Duration::from_secs(1));
Enter fullscreen mode Exit fullscreen mode

Whenever a tick arrives, the application reads the current time and uses that to drive the digital boolean logic, which lights up the LEDs as needed.

Trap CTRL-C to restore the cursor

At the beginning, the application hides the cursor by issuing a ANSI Escape sequence, thus:

print!("\x1b[?25l");
Enter fullscreen mode Exit fullscreen mode

When the user terminates the program by pressing CTRL-C, I need to restore the blinking cursor; a well-behaved DigitalClock must do that. Some kind of signal handler is required, so that just before it exits, the application issues the complementary ANSI escape sequence that restores the cursor. This time, the standard library provides the handler facility, aptly named: ctrlc:

fn notify_me_when_user_exits() -> Result<Receiver<u8>, ctrlc::Error> {
    let (sender, receiver) =  bounded(8);
    ctrlc::set_handler(move || {
        print!("\x1b[?25h");  // Restore the hidden cursor!
        let _ = sender.send(0xFF);
    })?;

    Ok(receiver)
}
Enter fullscreen mode Exit fullscreen mode

That little closure, named ctrlc::set_handler does the job. It makes use of a facility that crossbeam_channel::bounded(n) provides. Upon trapping the CTRL-C, the closure issues an appropriate ANSI Escape sequence, and sends a 0xFF (an arbitrary value, not used) in the channel.

Handling two non-deterministic asynchronous inputs

The main thread sets itself up for two inputs:

  • A periodic tick: when it arrives, current time is picked up and the DigitalClock displays the digits
  • An intimation by the user to exit: when it arrives, the handler gets into action, keeps the house as it were and leaves.

Thus, a structure is required to respond to either of these two inputs which can arrive in an unforeseen order; as if, the application is passively waiting for either of two interrupts and then reacting to the one that arrives first.

While strolling through rust-cli (handbook), I have come across a nifty arrangement of select! macro. This macro sits perfectly at the receiving end of a (bi-ended) channel and executes logic associated with the receipt of data through that channel. Structurally, it caters to multiple such receiving ends but responds to any one of them at a given point in time:, much like its homographic elder cousin named select() Unix system call:

select! {
            recv(notification_on_next_second) -> _ => {
                let time = read_clock_now()      // Current HH:MM:SS as a string
                .chars()                         // An iterator of characters it contains
                .filter( | c  | c != &':')       // Scrape the ':' character from the middle
                .map(| c | c as u8 - '0' as u8)  // Get the numeric digit from ascii digit
                .collect();

                screen_clock
                .on_next_second(
                    hr_and_min_and_sec[0],
                    hr_and_min_and_sec[1],
                    hr_and_min_and_sec[2],
                    hr_and_min_and_sec[3],
                    hr_and_min_and_sec[4],
                    hr_and_min_and_sec[5]
                )
                .refresh();

                print!("\x1b[7A");
            }

            recv(notification_on_user_exiting) -> _ => {
                println!("Goodbye!");
                break;
            }
        }
Enter fullscreen mode Exit fullscreen mode

The complete code is here.

There it is, a simple application that makes use of some Rust facilities, idioms and techniques that I have managed to learn so far. It is not optimized for memory and speed and certainly, the code can be improved at few places, I know. Yet, I would love to hear from Rustaceans if and where do they think,the design can be cleaner and Rust's features can be adopted better. Educate me.


I am a software programmer with ~3 decades of experience, having learnt through many successes I have influenced or caused, and many failures and mistakes I have made. I work as a platform technologist with a consortium of consultants: Swanspeed Consulting.

Top comments (1)

Collapse
 
lakincoder profile image
Lakin Mohapatra

Very nice