Intern Project: Keystroke Per Minute Measurement System

Greetings, everyone! In today’s blog post, we will delve into the realm of designing a keystroke per minute measurement system. The system will be designed with the Shabakah board at the center of the action.

Details About The Project

This system comes with two levels of difficulty: easy and hard. The degree of challenge is determined by the water level detected by the water sensor.
Easy Level:
The easy level is chosen when the water level detected by the water sensor is low. The LCD screen prompts you to press the button on the Shabakah board. As you do so, a fascinating display unravels – a sequence of random characters drawn from the pool of [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, *, #]. Your task? It’s to swiftly replicate this sequence using the keypad, as if you’re in a WPM race game.
Once you’ve completed this challenge: the system measures your Keystrokes Per Minute (KPM) and presents it proudly on the screen. But the adventure doesn’t stop there. With the press of a button, you can dive back into the excitement, honing your skills and pushing your limits again and again.
Hard Level:
Shifting our attention to the more challenging tier – a tier that becomes accessible when the water sensor detects an elevated water level. Upon the LCD’s cue, you’ll press the button on the Shabakah board. In an instant, an array of characters will be shown on the screen, selected at random from the pool [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, *, #].
Your mission is to recreate this sequence, character by character, almost like partaking in a WPM race. Yet, a captivating twist awaits. With each character inputted correctly, the LCD environment undergoes a transformation. Characters sway and shuffle, much like a dynamic puzzle. However, your progress is
safeguarded – the characters you’ve entered correctly remain unaltered. This showcases your advancement amidst the ever-changing backdrop.
Imagine, for instance, the LCD initially displaying: 125324689*#0#65. When you input 1, the entire character composition alters, yet the 1 stands unwavering. Thus, the screen transforms to something akin to 186949398345##*, as you masterfully progress through the challenge.
Upon mastering this formidable feat, the system unveils your Keystrokes Per Minute (KPM) achievement on the LCD screen. Nevertheless, this journey doesn’t culminate here. With a simple press of a button, you’re poised to relive the thrill. This perpetual cycle empowers you to refine your skills, consistently surpassing prior milestones.

Here's a rundown of what you're going to need:

1) 4X3 Keypad
This will be used by the user to input the characters
2) 16X2 LCD with I2C interface
This will be used to display the instructions and characters to the user
3) Water Sensor
This will be used to sense the level of water, which determines the hardness of the level
4) Shabakah Board
Note: Other board can also be used for this system, but the connections will be different

The Connection

The connections for this system are pretty simple. It uses 8 DIO pins and 1 I2C interface pins.
First of all the 4X3 keypad has 7 pins total. 4 of the pins are going to be set as input which are used to specify the row and 3 pins are going to be set as output which are used to specify the column.
For this system, we will use pins [5, 6, 7, 10] for rows and pins [3, 4, 9] for columns
The water sensor connection is pretty straight forward. The components have 3 pins, one will be connected to a 3.3V pin, one will be connected to grd and one will be connected to an ADC pin.
The ADC pin used for this project is pin 1
The LCD screen uses an I2C connection, which requires an SDA pin for data transmission and an SCL pin for synchronization.
In the case of Shabakah, the SDA pin is connected to pin 2, and the SCL pin is connected to pin 8.
Now that we have explored the connections and components in our project, it’s time to get a complete view of the entire system. Let’s take a look at the picture showcasing the fully assembled setup.

The Code

The code for this project is pretty straightforward. It’s written in Arduino. I will add it all below, then explain it in sections. 

				
					/**
 * @file main.cpp
 * @brief Key Typing Game with Keypad and LCD
 * @author Zain Kamal, Remal IoT
 * @date 12 July 2023
 * 
 * This program implements a key typing game using a keypad and LCD display.
 * The user is required to enter a sequence of characters displayed on the LCD,
 * and the program measures the user's typing speed in keys per minute (KPM).
 * The game has two modes: one where random characters are displayed and the user
 * has to type them in order, and another where the user has to type a pre-defined
 * sequence of characters.
 */

#include <Wire.h>
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>
#include "Freenove_WS2812_Lib_for_ESP32.h"

// Define the pins
#define COL1 4
#define COL2 3
#define COL3 TX
#define ROW1 6
#define ROW2 10
#define ROW3 7
#define ROW4 5
#define SIGNAL_PIN 1
#define NUM_LEDS 1                                           // Number of LED
#define CHANNEL 0                                            // RMT channel

/**************************************************Initialize Keypad*****************************************************/

const byte ROWS = 4;                                         // Four rows
const byte COLS = 3;                                         // Three columns
char keys[ROWS][COLS] = {                                    // Keypad character mapping
  { '1', '2', '3' },
  { '4', '5', '6' },
  { '7', '8', '9' },
  { '*', '0', '#' }
};

byte rowPins[ROWS] = { ROW1, ROW2, ROW3, ROW4 };             // Connect to the row pinouts of the keypad
byte colPins[COLS] = { COL1, COL2, COL3 };                   // Connect to the column pinouts of the keypad

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
LiquidCrystal_I2C lcd(0x27, 16, 2);

/**************************************************Initialize Keypad*****************************************************/

char array[12] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '*', '#' };  // Initialize the characters array
char str[17] = "                ";                           // Initialize the random characters printed by LCD
String typedSTR = "";                                        // Initialize the characters typed by the user
char key;                                                    // The character typed by the user at a specific time
float kpm = 0.0;                                             // The value of key per minute
unsigned long start_time = 0.0;                              // The starting time used to measure the duration the user took to type all the characters
unsigned long finish_time = 0.0;                             // The finishing time used to measure the duration the user took to type all the characters
int water_level = 0;
bool exitLoop = false;                                       // Flag to control loop exit
int currentCharIndex = 0;                                    // Current index of the character to match
int previousCharIndex = -1;                                  // Index of the previously matched character
int16_t LED_Brightness = 30;                                 // Controls LED brightness

Freenove_ESP32_WS2812 Shabakah_RGBLED = Freenove_ESP32_WS2812(NUM_LEDS, LED_BUILTIN, CHANNEL, TYPE_GRB);  // Create RGBLED object

/**
 * @brief Initialize the program and setup necessary components.
 */
void setup() {
  pinMode(SHBK_BTN, INPUT);                                   // Setting shabakah button as input
  Serial.begin(9600);                                         // Initializing the serial monitor
  pinMode(ROW1, INPUT);                                       // Setting row1 as input to not interfere with built-in LED

  lcd.init();                                                 // Initializing the LCD      
  lcd.backlight();                                            // Turn on the backlight of the LCD
  lcd.setCursor(0, 0);                                        // Set the cursor position to the first row, first column
  lcd.print("Press button to");                               // Print the message "Press button to" on the LCD
  lcd.setCursor(0, 1);                                        // Set the cursor position to the second row, first column
  lcd.print("Start");                                         // Print the message "Start" on the LCD

  /* Initialize RGB LED */
  Shabakah_RGBLED.begin();                                    // Begin the RGB LED operation
  Shabakah_RGBLED.setBrightness(LED_Brightness);              // Set the brightness of the RGB LED
}

/**
 * @brief The main program loop.
 */
void loop() {
  // Check if the shabakah button is pressed
  if (digitalRead(SHBK_BTN)) { 
    
    water_level = analogRead(SIGNAL_PIN);                     // Read the water level from the analog pin
    Serial.println(water_level);                              // Print the water level to the serial monitor

    typedSTR = "";                                            // Reset the typed string and the characters array
    for (int i = 0; i < 16; i++)
      str[i] = ' ';
    
    lcd.clear();                                              // Clear the LCD display

    if (water_level < 1000) {                                 // Check the water level to determine the game mode
      
      Shabakah_RGBLED.setLedColorData(0, 0, 255, 0);          // Set the RGB LED color to green
      Shabakah_RGBLED.show();                                 // Update the RGB LED

      // Generate random characters for the LCD display
      for (int i = 0; i < 16; i++) {
        str[i] = array[random(12)];
      }
      
      lcd.setCursor(0, 0);                                    // Set the cursor position to the first row, first column
      lcd.print(str);                                         // Print the random characters on the LCD
      lcd.setCursor(0, 1);                                    // Set the cursor position to the second row, first column
      
      start_time = millis();                                  // Record the starting time

      // Read the user's key presses and update the LCD display
      for (int j = 0; j < 16; j++) {
        key = keypad.getKey();                                // Get the pressed key
        if (key) {
          lcd.setCursor(j, 1);                                // Move cursor to the current position in the second row
          lcd.print(key);                                     // Display the pressed key
          typedSTR += key;                                    // Append the key to the typed string
        }
      }
    } else {
      
      Shabakah_RGBLED.setLedColorData(0, 255, 0, 0);          // Set the RGB LED color to red
      Shabakah_RGBLED.show();                                 // Update the RGB LED
      exitLoop = false;                                       // Reset the exit loop flag
      start_time = millis();                                  // Record the starting time

      // Main game loop for typing predefined characters
      while (!exitLoop) {
        
        lcd.setCursor(0, 0);                                  // Set the cursor position to the first row, first column
        // Update characters after the current index
        for (int i = currentCharIndex; i < 16; i++) {
          key = keypad.getKey();                              // Get the pressed key
          if (key) {
            lcd.setCursor(currentCharIndex, 1);               // Set cursor to the position in the second row
            lcd.print(key);                                   // Display the pressed key
            typedSTR += key;                                  // Append the key to the typed string
            if (typedSTR[currentCharIndex] == str[currentCharIndex]) {
              previousCharIndex = currentCharIndex;           // Update the previous character index
              currentCharIndex++;                             // Move to the next character
            }
            if (currentCharIndex == 16) {                     // All characters matched
              exitLoop = true;
            }
          }
        }
      }

      // Reset variables and prepare for the next game
      currentCharIndex = 0;
      previousCharIndex = -1;
    }

    // Check if the typed string matches the characters array
    if (typedSTR == str) {
      
      finish_time = millis();                                   // Record the finishing time
      
      unsigned long duration = finish_time - start_time;        // Calculate the duration in seconds
      float seconds = (duration / 1000.0);
      
      kpm = (16 / seconds) * 60;                                // Calculate the typing speed in KPM
      
      lcd.clear();                                              // Clear the LCD display
      lcd.print("KPM : ");                                      // Print the label "KPM :" on the LCD
      lcd.setCursor(0, 1);                                      // Set the cursor position to the second row, first column
      lcd.print(kpm);                                           // Print the typing speed on the LCD

    } else {
      
      lcd.clear();                                              // Clear the LCD display
      lcd.setCursor(0, 0);                                      // Set the cursor position to the first row, first column
      lcd.print("Press button to");                             // Print the message "Press button to" on the LCD
      lcd.setCursor(0, 1);                                      // Set the cursor position to the second row, first column
      lcd.print("Start");                                       // Print the message "Start" on the LCD
    }
  }
}

				
			

The Code - Explained

We start with the headers, these are libraries needed to get the project working:

				
					#include <Wire.h>
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>
#include "Freenove_WS2812_Lib_for_ESP32.h"
				
			

The lines below contain defines used throughout the project. It is good practice to always capitalize your defines:

				
					// Define the pins
#define COL1 4
#define COL2 3
#define COL3 TX
#define ROW1 6
#define ROW2 10
#define ROW3 7
#define ROW4 5
#define SIGNAL_PIN 1
#define NUM_LEDS 1                                           // Number of LED
#define CHANNEL 0                                            // RMT channel
				
			

The code below is required to initialize the keypad, aka define the pins used for row and column.

				
					/**************************************************Initialize Keypad*****************************************************/

const byte ROWS = 4;                                         // Four rows
const byte COLS = 3;                                         // Three columns
char keys[ROWS][COLS] = {                                    // Keypad character mapping
  { '1', '2', '3' },
  { '4', '5', '6' },
  { '7', '8', '9' },
  { '*', '0', '#' }
};

byte rowPins[ROWS] = { ROW1, ROW2, ROW3, ROW4 };             // Connect to the row pinouts of the keypad
byte colPins[COLS] = { COL1, COL2, COL3 };                   // Connect to the column pinouts of the keypad

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
LiquidCrystal_I2C lcd(0x27, 16, 2);

/**************************************************Initialize Keypad*****************************************************/
				
			

The global variables used in this project:

				
					char array[12] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '*', '#'};  // Initialize the characters array
char str[17] = "                ";                           // Initialize the random characters printed by LCD
String typedSTR = "";                                        // Initialize the characters typed by the user
char key;                                                    // The character typed by the user at a specific time
float kpm = 0.0;                                             // The value of key per minute
unsigned long start_time = 0.0;                              // The starting time used to measure the duration the user took to type all the characters
unsigned long finish_time = 0.0;                             // The finishing time used to measure the duration the user took to type all the characters
int water_level = 0;
bool exitLoop = false;                                       // Flag to control loop exit
int currentCharIndex = 0;                                    // Current index of the character to match
int previousCharIndex = -1;                                  // Index of the previously matched character
int16_t LED_Brightness = 30;                                 // Controls LED brightness
				
			

The code below is used to initalise the LED inside the Shabakah board.

				
					Freenove_ESP32_WS2812 Shabakah_RGBLED = Freenove_ESP32_WS2812(NUM_LEDS, LED_BUILTIN, CHANNEL, TYPE_GRB);  // Create RGBLED object
				
			

In this section, we’ll be setting up our project. The first step is to configure the pins on Shabakah. We set the pin connected to the
built-in button as an input so that we can use the button functionality. Similarly, we set the pin connected to the LED as an output to avoid any issues.

After setting up the pins, we initialize the LCD module. We make sure that the backlight is turned on to ensure good visibility. Finally, we display some text on the LCD screen. This initialization step prepares our project for further interactions and operations.

				
					/**
 * @brief Initialize the program and setup necessary components.
 */
void setup() {
  pinMode(SHBK_BTN, INPUT);                                   // Setting shabakah button as input
  Serial.begin(9600);                                         // Initializing the serial monitor
  pinMode(ROW1, INPUT);                                       // Setting row1 as input to not interfere with built-in LED

  lcd.init();                                                 // Initializing the LCD      
  lcd.backlight();                                            // Turn on the backlight of the LCD
  lcd.setCursor(0, 0);                                        // Set the cursor position to the first row, first column
  lcd.print("Press button to");                               // Print the message "Press button to" on the LCD
  lcd.setCursor(0, 1);                                        // Set the cursor position to the second row, first column
  lcd.print("Start");                                         // Print the message "Start" on the LCD

  /* Initialize RGB LED */
  Shabakah_RGBLED.begin();                                    // Begin the RGB LED operation
  Shabakah_RGBLED.setBrightness(LED_Brightness);              // Set the brightness of the RGB LED
}
				
			

In the loop function, the initial code segment checks whether the user has pressed the Shabakah in-built button. The system will only proceed once the user has pressed the button. After that, the water level is read using the water sensor, and the variable that stores the user’s typed string is reset.

				
					void loop() {
  // Check if the shabakah button is pressed
  if (digitalRead(SHBK_BTN)) { 
    
    water_level = analogRead(SIGNAL_PIN);                     // Read the water level from the analog pin
    Serial.println(water_level);                              // Print the water level to the serial monitor

    typedSTR = "";                                            // Reset the typed string and the characters array
    for (int i = 0; i < 16; i++)
      str[i] = ' ';
    
    lcd.clear();          
				
			

The system operates based on the readings obtained from a water sensor. When the output value from the water sensor is below 1000, the system switches to easy mode, and the starting time is recorded. In this mode, the LCD screen generates a random character sequence once. Following that, the system continuously monitors the user’s input through a keypad. Once the user enters a 16-character sequence, the KPM calculation section will be executed.

				
					if (water_level < 1000) {                                 // Check the water level to determine the game mode
      
      Shabakah_RGBLED.setLedColorData(0, 0, 255, 0);          // Set the RGB LED color to green
      Shabakah_RGBLED.show();                                 // Update the RGB LED

      // Generate random characters for the LCD display
      for (int i = 0; i < 16; i++) {
        str[i] = array[random(12)];
      }
      
      lcd.setCursor(0, 0);                                    // Set the cursor position to the first row, first column
      lcd.print(str);                                         // Print the random characters on the LCD
      lcd.setCursor(0, 1);                                    // Set the cursor position to the second row, first column
      
      start_time = millis();                                  // Record the starting time

      // Read the user's key presses and update the LCD display
      for (int j = 0; j < 16; j++) {
        key = keypad.getKey();
        while (!key) {
          key = keypad.getKey();
        }
        
        typedSTR += key;                                      // Append the key to the typed string
        
        lcd.setCursor(0, 1);                                  // Set the cursor position to the second row, first column
        lcd.print(typedSTR);                                  // Print the typed string on the LCD
      }
    }
				
			

When the output of the water sensor reaches or exceeds 1000, the system selects the hard level, initiates a loop, and the starting time is recorded. The loop continues until the user enters 16 characters, at which point the exit condition is triggered. It’s important to note that a character will only count if it matches one of the dynamically changing characters displayed on the LCD. Once the loop is exited, all variables are reset to prepare for the next game.

				
					else if (1000 <= water_level) {
      
      Shabakah_RGBLED.setLedColorData(0, 255, 0, 0);          // Set the RGB LED color to red
      Shabakah_RGBLED.show();                                 // Update the RGB LED
      exitLoop = false;                                       // Reset the exit loop flag
      start_time = millis();                                  // Record the starting time

      // Main game loop for typing predefined characters
      while (!exitLoop) {
        
        lcd.setCursor(0, 0);                                  // Set the cursor position to the first row, first column
        // Update characters after the current index
        for (int i = currentCharIndex; i = 16) {
              exitLoop = true;
            }
          }
        }
      }

      // Reset variables and prepare for the next game
      currentCharIndex = 0;
      previousCharIndex = -1;
    }
				
			

Finally, once the user has finished typing all the letters, the program will record the finishing time and will compare the entered text with the text displayed on the LCD. If they are similar, the program proceeds to calculate the Keystrokes Per Minute (KPM) using the formula: KPM = (16 / seconds) * 60.

				
					if (typedSTR == str) {
      
      finish_time = millis();                                   // Record the finishing time
      
      unsigned long duration = finish_time - start_time;        // Calculate the duration in seconds
      float seconds = (duration / 1000);
      
      kpm = (16 / seconds) * 60;                                // Calculate the typing speed in KPM
      
      lcd.clear();                                              // Clear the LCD display
      lcd.print("KPM : ");                                      // Print the label "KPM :" on the LCD
      lcd.setCursor(0, 1);                                      // Set the cursor position to the second row, first column
      lcd.print(kpm);                                           // Print the typing speed on the LCD

    } else {
      
      lcd.clear();                                              // Clear the LCD display
      lcd.setCursor(0, 0);                                      // Set the cursor position to the first row, first column
      lcd.print("Press button to");                             // Print the message "Press button to" on the LCD
      lcd.setCursor(0, 1);                                      // Set the cursor position to the second row, first column
      lcd.print("Start");                                       // Print the message "Start" on the LCD
    }
  }
				
			

Leave a Reply

Your email address will not be published. Required fields are marked *