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.
(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:
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:
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:
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
}
- A BCD signal as an
u8
(unsigned 8-bit):
pub struct Nibbles(pub u8);
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)
};
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
}
}
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 */);
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,
// ..
}
}
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
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 constructornew
) - Every second on the system's own date/time
-- Pass the current hour/minute/second readings to
ScreenClock
-- Refresh theScreenClock
(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 :
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 sleep
ing 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 sleep
ing.
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)
This is how I set it up:
let notification_on_next_second = tick(Duration::from_secs(1));
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");
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)
}
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;
}
}
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)
Very nice