Morse Translator and Morse Lamp in vanilla JS - Part II
Hello Everyone! I hope you are doing well. In the last blog of this series I made a Morse translator using funtranslations API. In this blog we will make a Morse Lamp like the one here.
Disclaimer: I am a web dev beginner and my methods may not be the best.
I will not be covering HTML and CSS part as that will make this blog too long but here are the query selectors so you have an idea about the main parts of lamp which are the bulb itself and a transmit button which when clicked makes the bulb flicker in message sequence.
var bulb = document.querySelector(".bulb");
var bulb_button = document.querySelector(".bulb-button");
setTimeout function
The standard way of introducing a delay in JavaScript programs is to use its setTimeout method. For example:
console.log("Next line will be logged after 5 seconds");
setTimeout(() => { console.log("5 seconds are over"); }, 5000);
// syntax: setTimeout(callback, milliseconds)
How this works is it logs that first line immediately in console and then 5000 milliseconds later another line gets logged. In many cases, this is enough: do something, wait, then do something else. However the setTimeout function is actually asynchronous in nature.
Try this code:
console.log("First");
setTimeout(() => {console.log("Second");}, 5000);
console.log("Third");
One would think that after logging "First" it will wait 5 seconds and then log "Second" and then "Third". But what actually ends up getting logged is:
First
Third
Second
We see that 'First' and 'Third' get logged and 'Second' comes up 5 seconds later because setTimeout
does not block the thread of execution. setTimeout
is an API provided by the browser (webapi) so it is passed from the call stack to the browser for completing the timer. The webAPIs cannot push callbacks to call stack instead they push to task queue. The event loop checks if the call stack is empty and if it is, pushes the task onto task queue. Don't worry if you don't understand everything here; it is not necessary for this project.
It would be extremely inconvenient to use this function as it is because if we use setTimeout
directly with a loop to change the state of bulb (glow/dark) the timers will start together and will glow the bulb at the same time. For example, if a dot makes bulb glow for one unit time, and if we have an inter-symbol period of one unit time and a dash constitutes of 3 units of time, the bulb will glow for an unnoticeable amount of time and stop because both timers finish almost together and then 2 units of time later it will glow again for dash. What we want is to somehow wait till one timer is complete and only then proceed to next timer.
The solution
We can combine the techniques learned earlier from last blog to make a better sleep function:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
This returns a promise that resolves only when setTimeout is completed.
We can now use .then()
to perform an action, let's say, start another timer when this is completed and we can chain these sleep calls with .then()
.
Units of time
Going by the International Morse code recommendation, the dot duration is the basic unit of time measurement in Morse code transmission. The duration of a dash is three times the duration of a dot. Each dot or dash within an encoded character is followed by a period of signal absence, called a space (inter-symbol duration), equal to the dot duration. The letters of a word are separated by a space of duration equal to three dots, and words are separated by a space equal to seven dots.
dot : 1 unit : 200ms
dash : 3 units : 600ms
space : 1 unit : 200ms
inter-letter : 3 units : 600ms
inter-word : 7 units : 1400ms
Signals
After translation, we will make an array which will have all the signal durations according to the Morse code. For example, [200,200,600]
means lamp glows for 200ms, stops glowing for 200ms, glows for 600ms which can be decoded to dot-space-dash. Also remember that at last bulb should always stop glowing to indicate transmission of last symbol is completed. An SOS call's array will look like: [200, 200, 200, 200, 200, 600, 600, 200, 600, 200, 600, 600, 200, 200, 200, 200, 200, 600]
function afterTranslation() {
bulb_button.removeAttribute("disabled"); // Can only be used after text has been translated.
let x = 0;
time_periods = []; // time periods / signal duration
m = morse.value;
m = m.replaceAll(" ", "w"); // w denotes new word.
for (let i = 0; i < m.length; i++) {
if (m[i] === ".") {
x = 200; // dot time = 1 unit time.
time_periods.push(x);
time_periods.push(200); // Inter symbol time = 1 unit time.
} else if (m[i] === "-") {
x = 600; // dash time = 3 units.
time_periods.push(x);
time_periods.push(200); // Inter symbol time = 1 unit time.
} else if (m[i] === " ") {
time_periods[time_periods.length - 1] = 600; // Replace last wait time with Inter letter time = 3 units.
} else if (m[i] === "w") {
time_periods[time_periods.length - 1] = 1400; // Replace the last wait time with 1400ms (= 7 units) in case of a new word.
}
}
}
Above is the code for generating the array of time periods or signal durations. This array needs to be declared outside this function so it has global scope and we can use it in another function. The Morse code we received from API contains five whitespaces at the places where there is a word change or an actual whitespace in the text we sent, and a single whitespace between two symbols indicates a letter change or the inter-letter gap. So in order to distinguish whether it is a word gap or letter gap, we replace the word gaps with a letter 'w'.
Now we loop through this new string and if the character is a dot we push 200ms to the array, if character is a dash, we push 600ms to the array. Also notice that we push 200ms after both of them; this is because the lamp should be dark for that duration. If we encounter a whitespace, we replace the last time period (dark for 200ms) by 600ms which is the inter-letter gap. Similarly if we encounter a 'w', we change that last time period by 1400ms.
function translateHandler() {
.
.
fetch(translationURL(text))
.then((response) => response.json())
.then((json) => {
...
})
.then(afterTranslation) // This will only be called after translation is complete.
.catch(errorHandler);
The Glow Function
This is the function that is responsible for changing state of the bulb. By "changing the state", I mean removing and adding a CSS class that has glow color for lamp.
let ind = 0;
let sl = 0;
// Switching the state
function glow(ind) {
if (bulb.classList.contains("glow")) {
bulb.classList.remove("glow");
} else {
bulb.classList.add("glow");
}
sl = time_periods[ind]; // The time period
sleep(sl).then(() => {
glow(ind + 1);
}); // Complete the time period then proceed
}
This is a recursive method and ind
here is an index variable for the time period array.
Now you may be thinking why not just use a loop to iterate over the array? Well if we use loop here, the next iteration of loop will occur before the sleep
in this iteration is completed successfully.
There is one issue. When our array ends, the lamp will begin flickering as our index has crossed the length
function glow(ind) {
.
.
// If we are not at the last time period
if (ind + 1 < time_periods.length) {
sleep(sl).then(() => {
glow(ind + 1);
}); // Complete the time period then proceed
}
}
There is still an issue. A user might just click that transmit button again by mistake and if that happens, our transmission again gets messed up.
function glow(ind) {
// After transmit is clicked, disable the transmit button for the duration of transmission.
if (ind === 0) {
bulb_button.disabled = true;
} else if (ind + 1 === time_periods.length) {
bulb_button.removeAttribute("disabled");
}
.
.
}
Now add an event listener or make a DOM event to trigger this glow function when the transmit button is clicked .
Congratulations! There you have it. The Morse lamp is now complete. If you need any help (like the CSS), this is the github repository.
Thanks for reading. Peace! ✌️