Programming Interface and Application¶
Okay, so if last week we already learn how to interacting an input device (Rotary Encoder) to our OLED I2C screen. This week, we’re going to learn how to design a case that interfaces a user with these two.
Since I will be using a Rotary Encoder and an OLED I2C Screen for my final project, I will utilize this week to make an interface application for the user to select menu options for measurement in OLED on my devices.
Menu Selection Interface using Rotary Encoder¶
Design Inspiration¶
So, I want to make 4 menu options to display my sensors data, and I want the rotary encoder to be able to navigate / switching menu options and if clicked, the page will change to display the value of the sensors selected.
At first, I want to follow this tutorial with amazing UI design for the OLED. But I think for the first spiral, I will go with the simple one first. For that, I refer to this menu selection with OLED tutorial and alsomenu selection with Nokia lCD.
Hardware Setup & Initialization¶
Components Used¶
- Rotary Encoder HW-040
- OLED I2C SH1106 1.3” (White)
- SeeedStucio XIAO RP2040 (on my custom devboard for FP)
- Jumper cables
Circuit Connection¶
- (OLED) VCC –> 3.3V (XIAO)
- (OLED) GND –> GND (XIAO)
- (OLED) SCL –> D5 (XIAO)
-
(OLED) SDA –> D4 (XIAO)
-
(Rotary Encoder) CLK –> D10 (XIAO)
- (Rotary Encoder) DT –> D9 (XIAO)
- (Rotary Encoder) SW –> D8 (XIAO)
- (Rotary Encoder) VCC –> 3.3V (XIAO)
- (Rotary Encoder) GND –> GND (XIAO)
The reason I’m assigning the CLK, DT, and SW pins of Rotary Encoder to D8, D9, D10 in the XIAO is because I’m reserving all the analog pins for the sensors that I will use for my final project.
The wiring connection looks like this on my final project board
Initialization¶
If you have never connected/ setup communication between OLED and MCU before, you might want to first setup the communication protocol between your OLED and MCU by folllowing the step bt step process mentioned in Week 9: Output Devices and Week 13: Embedded Networking and Communication .
Programming Process: Menu Selection (2x2 Grid)¶
To program menu selection options that’s controlled by Rotary Encoder, I referred to these tutorials that uses OLED and Nokia LCD referred by Elaine. Elaine also showed me her example code that is adapted to her project need. For this assignment, I adapted the programs specifically for an OLED I2C SH1106 and incorporated them with a Rotary Encoder, utilizing Eka’s debouncing program that I also tried to implemented in W13 embededded networking and communication assignment. At this stage, I’m not using any additional design software (yet), instead I’m focusing on controlling the interface directly through code adjustments.
-
Libraries and Definitions¶
- Include necessary libraries for SPI, I2C communication (Wire), and for driving the OLED display (Adafruit_GFX and Adafruit_SH110X)
- Define pins for the rotary encoder (DT, CLK) and the selection button (SW).
- Define OLED paramaters
#include "rotary_encoder.h" #include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SH110X.h> // Define pins for rotary encoder #define DT D9 #define CLK D10 #define SW D8 // Define OLED display parameters #define i2c_Address 0x3C // Initialize with the I2C addr 0x3C #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define OLED_RESET -1 // QT-PY / XIAO
-
Objects Initialization¶
- Initialize a rotary encoder object with pins DT (D9) and CLK (D10).
- Initialize an OLED display object with specified screen dimensions and reset pin configuration.
-
Constants and Variables¶
// Menu items const char* menuItems[] = {"EC", "TDS", "pH", "Cal"}; const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]); // Menu navigation variables int menuIndex = 0; bool buttonPressed = false;
Menu Items:
- Define menu options EC, TDS, pH, Cal into
menuItems
array - Calculate number of items in
menuItems
throughmenuLength
Menu Navigation Variables:
- menuIndex keeps track of the currently selected menu item index
- buttonPressed flags when the selection button (SW) is pressed.
- Define menu options EC, TDS, pH, Cal into
-
Setup Function¶
- Initializes serial communication for debugging purposes.
- Initializes the OLED display, clears it after 2 seconds, and sets up the button pin (SW) as input with a pull-up resistor.
- Calls
updateDisplay()
to initially display the menu on the OLED.
void setup() { // Initialize serial communication Serial.begin(9600); // Initialize the OLED display display.begin(i2c_Address, true); // Address 0x3C default display.display(); delay(2000); display.clearDisplay(); // Initialize button pin pinMode(SW, INPUT_PULLUP); // Display initial menu updateDisplay(); }
-
Main Loop function¶
-
Custom Functions: ROtary Encoder Input Handling¶
Callback function: countUp()
To increment menuIndex to move to the next menu item, wrapping around to the first item after the last.
// Callback function for clockwise rotation void countUp() { menuIndex++; if (menuIndex >= menuLength) { menuIndex = 0; } updateDisplay(); //refresh the display with the updated selection }
Callback function: countDown()
To decrements menuIndex to move to the previous menu item, wrapping around to the last item after the first.
// Callback function for counter-clockwise rotation void countDown() { menuIndex--; if (menuIndex < 0) { menuIndex = menuLength - 1; } updateDisplay();//refresh the display with the updated selection }
checkButtonPress() function
checkButtonPress()
to monitor changes in the button state and setsbuttonPressed
flag accordingly when the button is pressed.
-
Custom Functions: OLED Output Management¶
UpdateDisplay() function
The role of updateDisplay() function here is to manage the OLED display to present menu items in a grid layout. It uses geometric calculations to position and draw rectangles around each menu item, highlighting the selected item for clear user feedback. This method ensures that the user interface remains organized and visually appealing, enhancing user interaction with the device.
Overview of the code steps:
- Clear the display and updates it with the current menu items arranged in a 2x2 grid layout.
- Each menu item is displayed with a “>” symbol as navigation next to the selected item.
- Then, draws rectangles around each menu item and prints them on the OLED display.
// Function to update the OLED display with the current menu item void updateDisplay() { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SH110X_WHITE); // Setting box dimensions for menu item int boxWidth = SCREEN_WIDTH / 2 - 4; // Width of each box int boxHeight = SCREEN_HEIGHT / 2 - 4; // Height of each box for (int i = 0; i < menuLength; i++) { int x = (i % 2) * (SCREEN_WIDTH / 2); // Calculate x position based on column int y = (i / 2) * (SCREEN_HEIGHT / 2); // Calculate y position based on row // Draw rectangle around the menu item display.drawRect(x + 2, y + 2, boxWidth, boxHeight, SH110X_WHITE); display.setCursor(x + 6, y + 8); if (i == menuIndex) { display.print(">"); } else { display.print(" "); } display.print(menuItems[i]); } display.display(); }
How do I make the design?
Basically, I planned since the beginning that I want to make the menu side by side in 2x2 grid-like arrangement. To do this, basically I’m using the OLED screen’s width and height as a reference as to how wide / tall the boxes that I want to make and also as the reference point for the cursor coordination. You can also customize these items below:
- Box Dimensions for Menu Items: Calculate the dimensions for each menu item box based on the OLED screen width (SCREEN_WIDTH) and height (SCREEN_WIDTH). Each box is half the width and height of the screen minus a margin (-4), ensuring proper spacing between items.
-
Drawing Menu Items:
- Loop through Menu Items: iterate through each item in the
menuItems
array (menuLength is the total number of items). - Calculate Position (x, y): Calculates the position (x, y) for each menu item based on its index (i);
- x is determined by (i % 2) * (SCREEN_WIDTH / 2), which places items in two columns across the width of the screen.
-
y is determined by (i / 2) * (SCREEN_HEIGHT / 2), organizing items into rows within half the height of the screen.
-
Draw Rectangle: Uses
display.drawRect()
to draw a rectangle around each menu item at position (x + 2, y + 2) with dimensions boxWidth by boxHeight, using SH110X_WHITE as the color. -
Highlight Selected Item: Checks if i equals menuIndex (the index of the currently selected item). If true, prints a “>” symbol before the menu item text to visually highlight it.
- Display Menu Item Text: Prints the text of each menu item (menuItems[i]) starting at position (x + 6, y + 8) on the OLED display.
- Loop through Menu Items: iterate through each item in the
selectMenuItem() function
Function to handle menu item selection. Works by clearing the display, displays the selected menu item, waits for 2 seconds using delay(2000), and then returns to displaying the menu using updateDisplay().
// Function to handle menu item selection void selectMenuItem() { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SH110X_WHITE); display.setCursor(0, 0); display.print("Selected:"); display.setCursor(0, 20); display.setTextSize(2); display.print(menuItems[menuIndex]); display.display(); delay(2000); // Display the selected menu item for 2 seconds
Complete code: Menu Selection Interface (2x2 Grid)¶
#include "rotary_encoder.h"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
// Define pins for rotary encoder
#define DT D9
#define CLK D10
#define SW D8
// Initialize the rotary encoder object
RotaryEncoder rotary(DT, CLK);
// Define OLED display parameters
#define i2c_Address 0x3C // Initialize with the I2C addr 0x3C
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // QT-PY / XIAO
// Initialize the OLED display object
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Menu items
const char* menuItems[] = {"EC", "TDS", "pH", "Cal"};
const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]);
// Menu navigation variables
int menuIndex = 0;
bool buttonPressed = false;
// Function prototypes for rotary encoder callbacks
void countUp();
void countDown();
void checkButtonPress();
void setup() {
// Initialize serial communication
Serial.begin(9600);
// Initialize the OLED display
display.begin(i2c_Address, true); // Address 0x3C default
display.display();
delay(2000);
display.clearDisplay();
// Initialize button pin
pinMode(SW, INPUT_PULLUP);
// Display initial menu
updateDisplay();
}
void loop() {
// Decode rotary encoder input
rotary.decode(&countUp, &countDown);
// Check if the button was pressed
checkButtonPress();
// If the button was pressed, handle the selection
if (buttonPressed) {
buttonPressed = false;
selectMenuItem();
}
}
// Callback function for clockwise rotation
void countUp() {
menuIndex++;
if (menuIndex >= menuLength) {
menuIndex = 0;
}
updateDisplay();
}
// Callback function for counter-clockwise rotation
void countDown() {
menuIndex--;
if (menuIndex < 0) {
menuIndex = menuLength - 1;
}
updateDisplay();
}
// Function to check button press
void checkButtonPress() {
static bool lastButtonState = HIGH;
bool currentButtonState = digitalRead(SW);
if (lastButtonState == HIGH && currentButtonState == LOW) {
buttonPressed = true;
}
lastButtonState = currentButtonState;
}
// Function to update the OLED display with the current menu item
void updateDisplay() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
int boxWidth = SCREEN_WIDTH / 2 - 4; // Width of each box
int boxHeight = 26; // Height of each box
for (int i = 0; i < menuLength; i++) {
int x = (i % 2) * (SCREEN_WIDTH / 2); // 0 or SCREEN_WIDTH / 2
int y = (i / 2) * (SCREEN_HEIGHT / 2); // 0 or SCREEN_HEIGHT / 2
// Draw rectangle around the menu item
display.drawRect(x + 2, y + 2, boxWidth, boxHeight, SH110X_WHITE);
display.setCursor(x + 6, y + 8);
if (i == menuIndex) {
display.print(">");
} else {
display.print(" ");
}
display.print(menuItems[i]);
}
display.display();
}
// Function to handle menu item selection
void selectMenuItem() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Selected:");
display.setCursor(0, 20);
display.setTextSize(2);
display.print(menuItems[menuIndex]);
display.display();
delay(2000); // Display the selected menu item for 2 seconds
// Return to the menu
updateDisplay();
}
Result | Uploaded Code¶
Next step is to just upload the code to our board.
Other Tests and Trials & Errors¶
I played with different combinations and variations before setlling into the final design. I had to play and adjust the sizing, settings, orders, etc to get here.. Here you can witness the evolvement of my interface designing programming process
Initial Menu Selection v1¶
#include "rotary_encoder.h"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
// Define pins for rotary encoder
#define DT D9
#define CLK D10
#define SW D8
// Initialize the rotary encoder object
RotaryEncoder rotary(DT, CLK);
// Define OLED display parameters
#define i2c_Address 0x3C // Initialize with the I2C addr 0x3C
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // QT-PY / XIAO
// Initialize the OLED display object
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Menu items
const char* menuItems[] = {"Option 1", "Option 2", "Option 3", "Option 4"};
const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]);
// Menu navigation variables
int menuIndex = 0;
bool buttonPressed = false;
// Function prototypes for rotary encoder callbacks
void countUp();
void countDown();
void checkButtonPress();
void setup() {
// Initialize serial communication
Serial.begin(9600);
// Initialize the OLED display
display.begin(i2c_Address, true); // Address 0x3C default
display.display();
delay(2000);
display.clearDisplay();
// Initialize button pin
pinMode(SW, INPUT_PULLUP);
// Display initial menu
updateDisplay();
}
void loop() {
// Decode rotary encoder input
rotary.decode(&countUp, &countDown);
// Check if the button was pressed
checkButtonPress();
// If the button was pressed, handle the selection
if (buttonPressed) {
buttonPressed = false;
selectMenuItem();
}
}
// Callback function for clockwise rotation
void countUp() {
menuIndex++;
if (menuIndex >= menuLength) {
menuIndex = 0;
}
updateDisplay();
}
// Callback function for counter-clockwise rotation
void countDown() {
menuIndex--;
if (menuIndex < 0) {
menuIndex = menuLength - 1;
}
updateDisplay();
}
// Function to check button press
void checkButtonPress() {
static bool lastButtonState = HIGH;
bool currentButtonState = digitalRead(SW);
if (lastButtonState == HIGH && currentButtonState == LOW) {
buttonPressed = true;
}
lastButtonState = currentButtonState;
}
// Function to update the OLED display with the current menu item
void updateDisplay() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Menu:");
display.setTextSize(1);
for (int i = 0; i < menuLength; i++) {
display.setCursor(0, 16 + i * 10);
if (i == menuIndex) {
display.print("> ");
} else {
display.print(" ");
}
display.print(menuItems[i]);
}
display.display();
}
// Function to handle menu item selection
void selectMenuItem() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(0, 0);
display.print("Selected:");
display.setCursor(0, 20);
display.setTextSize(2);
display.print(menuItems[menuIndex]);
display.display();
delay(2000); // Display the selected menu item for 2 seconds
// Return to the menu
updateDisplay();
}