Using Keyboard Indicator LEDs to communicate in Morse Code with C

Your Keyboard as an Output Device?

Luke Zeisset Development Technologies, Programming, Tutorial Leave a Comment

Though we don’t really see as many of them as we used to, they are still with us. One helps indicate if a 10-key is in number or cursor mode. Another helps show when we use capital letters without holding the shift key. You might even have another to show if you accidentally hit the scroll lock key. I’m talking of course about keyboard state indicators.

Most people only think of a keyboard as an input device, but given that it has a changeable state, it most definitely can be used for output as well! Unfortunately, producing meaningful output using a keyboard state indicator (beyond their intended purpose) is rather tedious because they only have two states; the indicator light is either on or off. And to convolute things further, most keyboards these days don’t have many indicators in general. For example, the very keyboard I’m typing on only has a caps lock indicator! Thankfully, there is a well-established encoding that requires only one “bit” to be useful: Morse code!

In this article, I will show how I approached the development of a small utility to output Morse code on the caps lock LED. Even if it’s impractical, I wanted a challenge, and I had fun working through it. Though I’m certainly not an expert, I used the C programming language because I figured it would be the simplest approach.

I hope it is as clear to you, the reader, as it is to me, especially since I tried to make this as simple as possible. And don’t worry if you don’t have a caps lock indicator, either, as we will display the Morse code, too.

Prerequisites

Before getting started, I needed to have a C compiler. I decided to leverage the C compiler that comes with Zig because I already had it readily available. Feel free to use GCC, MSVC, Clang, or something else if you’d like. Be sure to know how to run your compiler against your code. In my case, I simply type zig cc -o morse.exe main.c.

I know this is slightly controversial, but I chose to not bother building this outside of my “normal” desktop experience. That’s right, I’m running Windows right now. Feel free to tweak the code as necessary to get it to work on your BSD, GNU, or Mac systems. This is just a fun exercise, after all!

Now, given that we have a target language and platform, I needed to make sure I knew how to trigger the keypress. Thankfully, there was already a short example program to reference. Additionally, for the purpose of making my keyboard light up in Morse code, I needed to have a reference to that as well. Wikipedia was perfect for this.

The Code

Keyboard Functions

Getting the caps lock LED to light up was as simple as copying the example code from the Microsoft site and using a different virtual key. I split it out into different helper functions, but there’s nothing particularly novel about it. This allows either specifically turning on or off the lock or simply toggling the state of a given key. Later on, we will call these with VK_CAPITAL, which is the virtual key for caps lock.

#include <windows.h>

void press( BYTE key ) {
	keybd_event(key, 0x45, KEYEVENTF_EXTENDEDKEY | 0, 0);
}

void release( BYTE key ) {
	keybd_event(key, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
}

void setLock( BYTE lock, BOOL bstate )
{
	BYTE keyState[256];

	GetKeyboardState((LPBYTE)&keyState);
	if ( (bstate && !(keyState[lock] & 1)) ||
			(!bstate && (keyState[lock] & 1)) )
	{
		press(lock);
		release(lock);
	}
}

void enableLock( BYTE lock ) {
	setLock(lock, TRUE);
}

void disableLock( BYTE lock ) {
	setLock(lock, FALSE);
}

void toggleLock( BYTE lock ) {
	press(lock);
	release(lock);
}

Morse Code… in C

One thing I find quite interesting about programming is seeing how people approach data representation. Sometimes it’s straightforward with little room for imagination, and sometimes it depends almost entirely on whatever craziness you have seen with your choice of tools or lack thereof. I encourage you as the reader to pause here for a moment before continuing. Think about how you would convert the dots and dashes of Morse into something a computer could use…

For my implementation, I’ve decided to provide arrays based on the length of time I expect the light to be on. The sequences of dots and dashes have different lengths, so I terminate my entries with a zero. I’ve seen this pattern for knowing when to end loops (rather than counting up to a number we determine beforehand). Though it may seem odd, it makes processing quite simple.

#define dit 1
#define dah 3
#define time_between_letters 3
#define time_between_words 7
#define ms_per_time_unit 200

const int m_A[] = {dit, dah, 0};
const int m_B[] = {dah, dit, dit, dit, 0};
const int m_C[] = {dah, dit, dah, dit, 0};
/* abridged */
const int m_X[] = {dah, dit, dit, dah, 0};
const int m_Y[] = {dah, dit, dah, dah, 0};
const int m_Z[] = {dah, dah, dit, dit, 0};

Based on information about Morse code timings, I simply had to describe the number of units for each dit, dah, and everything in between. The 200 milliseconds I defined for ms_per_time_unit is completely arbitrary, though. Feel free to adjust if you are faster at interpreting Morse code than me.

Now, to make use of this alphabet, I needed to organize them into something I could reference. An array of pointers will do the trick:

const int *MorseAlpha[] = {
	m_A, m_B, m_C, m_D, m_E,
	m_F, m_G, m_H, m_I, m_J,
	m_K, m_L, m_M, m_N, m_O,
	m_P, m_Q, m_R, m_S, m_T,
	m_U, m_V, m_W, m_X, m_Y,
	m_Z
};

Data, Meet Logic

We’re pretty close to being done! We just need to bridge the gap between the data we just defined and the existing logic that tells Windows when to toggle the caps lock key. I decided to also output the information onto the screen to confirm that we are generating the correct sequence.

char morse_display(int c) {
	switch (c) {
		case dit: return '.';
		case dah: return '-';
		default: return ' '; // shouldn't happen
	}
}

void blink(const int *sequence) {
	int dit_dah;
	for (int i = 0; (dit_dah = sequence[i]); ++i) {
		if (0 < i) {
			// Time between parts of the same letter
			Sleep(ms_per_time_unit);
		}
		printf("%c", morse_display(dit_dah));
		toggleLock( VK_CAPITAL );
		Sleep(dit_dah * ms_per_time_unit);
		toggleLock( VK_CAPITAL );
	}
}

We can use the Windows Sleep() function to wait a given number of milliseconds between virtual keypresses. Also, this blink() function only handles individual letters, so we need to call this while we are working on words, which our spell() function will take care of. This function does some simple checks to make sure we only try to blink when we are dealing with letters or numbers (yes, I implemented numbers, too!).

void spell(char *message) {
	char c;
	for (int i = 0; (c = message[i]); ++i) {
		// skip non-printing characters
		if (!isprint(c)) {
			continue;
		}
		if (' ' == c) {
			// Subtract 1 unit, since that will be reflected on next character
			Sleep((time_between_words - 1) * ms_per_time_unit);
			continue;
		}
		if (!isalnum(c)) {
			// Send to stdout, but don't blink
			printf("%c\n", c);
			continue;
		}
		
		Sleep(time_between_letters * ms_per_time_unit);
		
		int offset = 0;
		if (isalpha(c)) {
			offset = toupper(c)-'A';
			blink(MorseAlpha[offset]);
		} else {
			offset = c - '0';
			blink(MorseNumeric[offset]);
		}
		printf("\t %c\n", c);
	}
}

Wrapping It All Up

All that is left to do is to actually call into the spell() function with whatever message you want to have blinked out on your keyboard. I do this with a simple loop based on the command line arguments when the program is run. I’m sure there are edge cases or system behavior I’m not taking into consideration, but for a small project, this worked out quite well and comes in under 200 lines of code.

If any readers wish to take this a step further – see if you can take an Arduino device that presents itself as a keyboard and have it decode the keyboard status indicators back into text!

Happy coding! Helpful hint: the Keyhole Dev Blog is a great source for info and ideas.

The Complete Code

#include <windows.h>
#include <stdio.h>
#include <ctype.h>

#define dit 1
#define dah 3
#define time_between_letters 3
#define time_between_words 7
#define ms_per_time_unit 200

const int m_A[] = {dit, dah, 0};
const int m_B[] = {dah, dit, dit, dit, 0};
const int m_C[] = {dah, dit, dah, dit, 0};
const int m_D[] = {dah, dit, dit, 0};
const int m_E[] = {dit, 0};
const int m_F[] = {dit, dit, dah, 0};
const int m_G[] = {dah, dah, dit, 0};
const int m_H[] = {dit, dit, dit, dit, 0};
const int m_I[] = {dit, dit, 0};
const int m_J[] = {dit, dah, dah, dah, 0};
const int m_K[] = {dah, dit, dah, 0};
const int m_L[] = {dit, dah, dit, dit, 0};
const int m_M[] = {dah, dah, 0};
const int m_N[] = {dah, dit, 0};
const int m_O[] = {dah, dah, dah, 0};
const int m_P[] = {dit, dah, dah, dit, 0};
const int m_Q[] = {dah, dah, dit, dah, 0};
const int m_R[] = {dit, dah, dit, 0};
const int m_S[] = {dit, dit, dit, 0};
const int m_T[] = {dah, 0};
const int m_U[] = {dit, dit, dah, 0};
const int m_V[] = {dit, dit, dit, dah, 0};
const int m_W[] = {dit, dah, dah, 0};
const int m_X[] = {dah, dit, dit, dah, 0};
const int m_Y[] = {dah, dit, dah, dah, 0};
const int m_Z[] = {dah, dah, dit, dit, 0};
const int m_1[] = {dit, dah, dah, dah, dah, 0};
const int m_2[] = {dit, dit, dah, dah, dah, 0};
const int m_3[] = {dit, dit, dit, dah, dah, 0};
const int m_4[] = {dit, dit, dit, dit, dah, 0};
const int m_5[] = {dit, dit, dit, dit, dit, 0};
const int m_6[] = {dah, dit, dit, dit, dit, 0};
const int m_7[] = {dah, dah, dit, dit, dit, 0};
const int m_8[] = {dah, dah, dah, dit, dit, 0};
const int m_9[] = {dah, dah, dah, dah, dit, 0};
const int m_0[] = {dah, dah, dah, dah, dah, 0};

const int *MorseAlpha[] = {
	m_A, m_B, m_C, m_D, m_E,
	m_F, m_G, m_H, m_I, m_J,
	m_K, m_L, m_M, m_N, m_O,
	m_P, m_Q, m_R, m_S, m_T,
	m_U, m_V, m_W, m_X, m_Y,
	m_Z
};

const int *MorseNumeric[] = {
	m_0, m_1, m_2, m_3, m_4,
	m_5, m_6, m_7, m_8, m_9
};


void press( BYTE key ) {
	keybd_event(key, 0x45, KEYEVENTF_EXTENDEDKEY | 0, 0);
}

void release( BYTE key ) {
	keybd_event(key, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
}

void setLock( BYTE lock, BOOL bstate )
{
	BYTE keyState[256];

	GetKeyboardState((LPBYTE)&keyState);
	if ( (bstate && !(keyState[lock] & 1)) ||
			(!bstate && (keyState[lock] & 1)) ) {
		press(lock);
		release(lock);
	}
}

void enableLock( BYTE lock ) {
	setLock(lock, TRUE);
}

void disableLock( BYTE lock ) {
	setLock(lock, FALSE);
}

void toggleLock( BYTE lock ) {
	press(lock);
	release(lock);
}

char morse_display(int c) {
	switch (c) {
		case dit: return '.';
		case dah: return '-';
		default: return ' '; // shouldn't happen
	}
}

void blink(const int *sequence) {
	int dit_dah;
	for (int i = 0; (dit_dah = sequence[i]); ++i) {
		if (0 < i) {
			// Time between parts of the same letter
			Sleep(ms_per_time_unit);
		}
		printf("%c", morse_display(dit_dah));
		toggleLock( VK_CAPITAL );
		Sleep(dit_dah * ms_per_time_unit);
		toggleLock( VK_CAPITAL );
	}
}

void spell(char *message) {
	char c;
	for (int i = 0; (c = message[i]); ++i) {
		// skip non-printing characters
		if (!isprint(c)) {
			continue;
		}
		if (' ' == c) {
			// Subtract 1 unit, since that will be reflected on next character
			Sleep((time_between_words - 1) * ms_per_time_unit);
			continue;
		}
		if (!isalnum(c)) {
			// Send to stdout, but don't blink
			printf("%c\n", c);
			continue;
		}
		
		Sleep(time_between_letters * ms_per_time_unit);
		
		int offset = 0;
		if (isalpha(c)) {
			offset = toupper(c)-'A';
			blink(MorseAlpha[offset]);
		} else {
			offset = c - '0';
			blink(MorseNumeric[offset]);
		}
		printf("\t %c\n", c);
	}
}

int main(int argc, char** argv)
{
	if (argc == 1) {
		printf("Usage: %s TEXT", argv[0]);
		return -1;
	}
	
	// Clear caps lock before starting
	disableLock( VK_CAPITAL );
	
	char *message = 0;
	for (int i = 1; (message = argv[i]); ++i) {
		if (0 < i) {
			// Subtract 1 unit, since that will be reflected on next character
			Sleep((time_between_words - 1) * ms_per_time_unit);
		}
		spell(message);
		putc('\n', stdout);
	}
	
	return 0;
}
0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments