interface and application programming¶
For this week I wanna make use of the Oled display on the controller I designed
I've seen a lot of small devices have GUI's using these displays like this. and I've always been interested in making one of these.
Credits: https://www.youtube.com/watch?v=HVHVkKt-ldc
I am going to use that video and try to make a library out of it so it is modular and easy to use for other people.
I first started out going to their description and downloading their program to upload to my esp. I quickly realized this code was outdated and broken. In the meantime a new library was released and he was using the older one. So I am trying to follow his video along with the newer library.
Creating the graphics¶
The guy in the video used photopea to create the graphics since you can easily export them as bitmaps and bitmaps can instantly be shown on the display using the screen's library
He first started out making the menu components each in different layers
For now I am going to use his graphics since I am porting the program to a new library.
Updating to the new library¶
I first opened an example of the new library (u8g2). In there I needed to select the screen I used.
If you don't know which screen you have it's best to give AI this list and describe your screen as best as you can and ask which one it thinks it is.
After I uploaded the code this popped up.
So it worked!
Now it's time to patch up the old program.
I first copied the display initializer and updated the library include.
Then I got a lot of compile errors at the drawbitmap function
I went into the reference of the library and searched for a similar function.
Luckily I found one and the only thing I needed to do was remove a
P
at each of these function calls and pray nothing in the parameters of the function changed.
After that it successfully uploaded and now it works!
Using the joysticks¶
I wanna use the joysticks to control the menu. Because at this time I don't have any way to control it. So I needed something that takes the joystick output and converts it to either a 0 or 1 for the menu input. So I created this function
struct hardJoystickValues
{
bool LXU; // Left joystick X axis up
bool LXD; // Left joystick X axis down
bool RXU; // Right joystick X axis up
bool RXD; // Right joystick X axis down
};
hardJoystickValues GUIParser(){
// Define joystick offsets (calibrated resting values)
const int offsetPWMCH1 = 1090; // Resting value for PWMCH1 (right joystick Y-axis)
const int offsetPWMCH2 = 1072; // Resting value for PWMCH2 (right joystick X-axis)
const int offsetPWMCH3 = 1043; // Resting value for PWMCH3 (left joystick X-axis)
const int offsetPWMCH4 = 1476; // Resting value for PWMCH4 (left joystick Y-axis)
// Define deadzone threshold
const int deadzone = 120;
// Adjust joystick values by subtracting offsets
int adjustedPWMCH1 = JoystickData.PWMCH1 - offsetPWMCH1;
int adjustedPWMCH2 = JoystickData.PWMCH2 - offsetPWMCH2;
int adjustedPWMCH3 = JoystickData.PWMCH3 - offsetPWMCH3;
int adjustedPWMCH4 = JoystickData.PWMCH4 - offsetPWMCH4;
// Apply deadzone
if (abs(adjustedPWMCH1) < deadzone) adjustedPWMCH1 = 0; //abs to avoid negatives
if (abs(adjustedPWMCH2) < deadzone) adjustedPWMCH2 = 0;
if (abs(adjustedPWMCH3) < deadzone) adjustedPWMCH3 = 0;
if (abs(adjustedPWMCH4) < deadzone) adjustedPWMCH4 = 0;
// Map joystick values to hard values
int LXU = 0; // Left joystick X axis up
int LXD = 0; // Left joystick X axis down
if (adjustedPWMCH1 > 0) {
LXU = 1; // Joystick is up
} else if (adjustedPWMCH1 < 0) {
LXD = 1; // Joystick is down
}
return {LXU, LXD, 0, 0}; // Return the values as a struct
}
Code
// simple project using Arduino UNO and 128x64 OLED Display to display a menu
// created by upir, 2022
// youtube channel: https://www.youtube.com/upir_upir
// YOUTUBE VIDEO: https://youtu.be/HVHVkKt-ldc
// WOKWI PROJECT: https://wokwi.com/projects/353101459130291201
// links from the video:
// Flipper Zero menu - https://docs.flipperzero.one/basics/control#M5BZO
// WOKWI start project progress bar - https://wokwi.com/projects/300867986768527882
// image2cpp - https://javl.github.io/image2cpp/
// 128x64 SSD1306 OLED Display: https://s.click.aliexpress.com/e/_DCKdvnh
// Transparent OLED display: https://s.click.aliexpress.com/e/_Dns6eLz
// Arduino UNO: https://s.click.aliexpress.com/e/_AXDw1h
// Arduino UNO MINI: https://store.arduino.cc/products/uno-mini-le
// Big OLED Display: https://s.click.aliexpress.com/e/_ADL0T9
// Arduino breadboard prototyping shield: https://s.click.aliexpress.com/e/_ApbCwx
// u8g fonts (fonts available for u8g library): https://nodemcu-build.com/u8g-fonts.php
// u8g documentation: https://github.com/olikraus/u8glib/wiki/userreference
// Photopea (online Photoshop-like tool): https://www.photopea.com/
// image2cpp (convert images into C code): https://javl.github.io/image2cpp/
// Push buttons - https://s.click.aliexpress.com/e/_DmXS8B9
// Related videos:
// Arduino Parking Sensor - https://youtu.be/sEWw087KOj0
// Turbo pressure gauge with Arduino and OLED display - https://youtu.be/JXmw1xOlBdk
// Arduino Car Cluster with OLED Display - https://youtu.be/El5SJelwV_0
// Knob over OLED Display - https://youtu.be/SmbcNx7tbX8
// Arduino + OLED = 3D ? - https://youtu.be/kBAcaA7NAlA
// Arduino OLED Gauge - https://youtu.be/xI6dXTA02UQ
// Smaller & Faster Arduino - https://youtu.be/4GfPQoIRqW8
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <Graphics.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
//=====================================================================================//
// Struct declarations
typedef struct struct_message
{
int PWMCH1;
int PWMCH2;
int PWMCH3;
int PWMCH4;
} struct_message;
struct_message JoystickData; // declare the struct as JoystickData
struct hardJoystickValues
{
int LXU; // Left joystick X axis up
int LXD; // Left joystick X axis down
int RXU; // Right joystick X axis up
int RXD; // Right joystick X axis down
};
hardJoystickValues JoystickDataHard; // declare the struct as JoystickDataHard
//=====================================================================================//
//declarations
void GUITestLoop();
void MUXSetup();
hardJoystickValues GUIParser();
int analogReadMultiPlexer(int addressA, int addressB, int addressC, int addressD, int pin);
int mapPot(int normalizedValue);
// Controller declarations
const int MAXPWMVALUE = 1000;
const int MINPWMVALUE = 2000;
// Screen declarations
const int NUM_ITEMS = 8; // number of items in the list and also the number of screenshots and screenshots with QR codes (other screens)
const int MAX_ITEM_LENGTH = 20; // maximum characters for the item name
char menu_items [NUM_ITEMS] [MAX_ITEM_LENGTH] = { // array with item names
{ "3D Cube" },
{ "Battery" },
{ "Dashboard" },
{ "Fireworks" },
{ "GPS Speed" },
{ "Big Knob" },
{ "Park Sensor" },
{ "Turbo Gauge" }
};
// note - when changing the order of items above, make sure the other arrays referencing bitmaps
// also have the same order, for example array "bitmap_icons" for icons, and other arrays for screenshots and QR codes
#define BUTTON_UP_PIN JoystickDataHard.LXU // pin for UP button
#define BUTTON_SELECT_PIN buttonRight// pin for SELECT button
#define BUTTON_DOWN_PIN JoystickDataHard.LXD // pin for DOWN button
#define DEMO_PIN D0 // pin for demo mode, use switch or wire to enable or disable demo mode, see more details below
int button_up_clicked = 0; // only perform action when button is clicked, and wait until another press
int button_select_clicked = 0; // same as above
int button_down_clicked = 0; // same as above
int item_selected = 0; // which item in the menu is selected
int item_sel_previous; // previous item - used in the menu screen to draw the item before the selected one
int item_sel_next; // next item - used in the menu screen to draw next item after the selected one
int current_screen = 0; // 0 = menu, 1 = screenshot, 2 = qr
int demo_mode = 0; // when demo mode is set to 1, it automatically goes over all the screens, 0 = control menu with buttons
int demo_mode_state = 0; // demo mode state = which screen and menu item to display
int demo_mode_delay = 0; // demo mode delay = used to slow down the screen switching
bool buttonRight;
bool buttonLeft;
void setup() {
u8g2.begin();
u8g2.setColorIndex(1); // set the color to white
MUXSetup(); // Setup the multiplexer
}
void loop() {
JoystickData.PWMCH1 = mapPot(analogReadMultiPlexer(0, 0, 0, 0, A0)); //Right joystick Y
JoystickData.PWMCH2 = mapPot(analogReadMultiPlexer(1, 0, 0, 0, A0)); // Right joystick X
JoystickData.PWMCH3 = mapPot(analogReadMultiPlexer(0, 0, 0, 1, A0)); // left joystick Y
JoystickData.PWMCH4 = mapPot(analogReadMultiPlexer(1, 0, 0, 1, A0)); // left joystick X
buttonRight = abs(analogReadMultiPlexer(0, 0, 1, 0, A0)/4095); // right button
buttonLeft = abs(analogReadMultiPlexer(1, 0, 1, 0, A0)/4095); // left button
JoystickDataHard = GUIParser();
GUITestLoop();
}
void GUITestLoop(){
// when pin 13 is LOW (DEMO_PIN), enable demo mode
// this could be done either by using a switch
// or simply by connecting the wire between pin 13 and GND
// (those pins are next to each other)
if (digitalRead(DEMO_PIN) == LOW) {
demo_mode = 0; // enable demo mode
}
else {
demo_mode = 0; // disable demo mode
}
if (demo_mode == 1) { // when demo mode is active, automatically switch between all the screens and menu items
demo_mode_delay++; // increase demo mode delay
if (demo_mode_delay > 15) { // after some time, switch to another screen - change this value to make it slower/faster
demo_mode_delay = 0;
demo_mode_state++; // increase counter
if (demo_mode_state >= NUM_ITEMS*3) {demo_mode_state=0;} // jump back to the first screen
}
if (demo_mode_state % 3 == 0) {current_screen = 0; item_selected = demo_mode_state/3; } // menu screen
else if (demo_mode_state % 3 == 1) {current_screen = 1; item_selected = demo_mode_state/3;} // screenshots screen
else if (demo_mode_state % 3 == 2) {current_screen = 2; item_selected = demo_mode_state/3;} // qr codes screen
} // end demo mode section
if (current_screen == 0) { // MENU SCREEN
// up and down buttons only work for the menu screen
if ((BUTTON_UP_PIN == LOW) && (button_up_clicked == 0)) { // up button clicked - jump to previous menu item
item_selected = item_selected - 1; // select previous item
button_up_clicked = 1; // set button to clicked to only perform the action once
if (item_selected < 0) { // if first item was selected, jump to last item
item_selected = NUM_ITEMS-1;
}
}
else if ((BUTTON_DOWN_PIN == LOW) && (button_down_clicked == 0)) { // down button clicked - jump to next menu item
item_selected = item_selected + 1; // select next item
button_down_clicked = 1; // set button to clicked to only perform the action once
if (item_selected >= NUM_ITEMS) { // last item was selected, jump to first menu item
item_selected = 0;
}
}
if ((BUTTON_UP_PIN == HIGH) && (button_up_clicked == 1)) { // unclick
button_up_clicked = 0;
}
if ((BUTTON_DOWN_PIN == HIGH) && (button_down_clicked == 1)) { // unclick
button_down_clicked = 0;
}
}
if ((BUTTON_SELECT_PIN == LOW) && (button_select_clicked == 0)) { // select button clicked, jump between screens
button_select_clicked = 1; // set button to clicked to only perform the action once
if (current_screen == 0) {current_screen = 1;} // menu items screen --> screenshots screen
else if (current_screen == 1) {current_screen = 2;} // screenshots screen --> qr codes screen
else {current_screen = 0;} // qr codes screen --> menu items screen
}
if ((BUTTON_SELECT_PIN == HIGH) && (button_select_clicked == 1)) { // unclick
button_select_clicked = 0;
}
// set correct values for the previous and next items
item_sel_previous = item_selected - 1;
if (item_sel_previous < 0) {item_sel_previous = NUM_ITEMS - 1;} // previous item would be below first = make it the last
item_sel_next = item_selected + 1;
if (item_sel_next >= NUM_ITEMS) {item_sel_next = 0;} // next item would be after last = make it the first
u8g2.firstPage(); // required for page drawing mode for u8g library
do {
if (current_screen == 0) { // MENU SCREEN
// selected item background
u8g2.drawBitmap(0, 22, 128/8, 21, bitmap_item_sel_outline);
// draw previous item as icon + label
u8g2.setFont(u8g_font_7x14);
u8g2.drawStr(25, 15, menu_items[item_sel_previous]);
u8g2.drawBitmap( 4, 2, 16/8, 16, bitmap_icons[item_sel_previous]);
// draw selected item as icon + label in bold font
u8g2.setFont(u8g_font_7x14B);
u8g2.drawStr(25, 15+20+2, menu_items[item_selected]);
u8g2.drawBitmap( 4, 24, 16/8, 16, bitmap_icons[item_selected]);
// draw next item as icon + label
u8g2.setFont(u8g_font_7x14);
u8g2.drawStr(25, 15+20+20+2+2, menu_items[item_sel_next]);
u8g2.drawBitmap( 4, 46, 16/8, 16, bitmap_icons[item_sel_next]);
// draw scrollbar background
u8g2.drawBitmap(128-8, 0, 8/8, 64, bitmap_scrollbar_background);
// draw scrollbar handle
u8g2.drawBox(125, 64/NUM_ITEMS * item_selected, 3, 64/NUM_ITEMS);
// draw upir logo
u8g2.drawBitmap(128-16-4, 64-4, 16/8, 4, upir_logo);
}
else if (current_screen == 1) { // SCREENSHOTS SCREEN
u8g2.drawBitmap( 0, 0, 128/8, 64, bitmap_screenshots[item_selected]); // draw screenshot
}
else if (current_screen == 2) { // QR SCREEN
u8g2.drawBitmap( 0, 0, 128/8, 64, bitmap_qr_codes[item_selected]); // draw qr code screenshot
}
} while ( u8g2.nextPage() ); // required for page drawing mode with u8g library
}
void MUXSetup()
{
pinMode(D3, OUTPUT); // MUX enable
pinMode(D6, OUTPUT); // MUX address A
pinMode(D7, OUTPUT); // MUX address B
pinMode(D9, OUTPUT); // MUX address C
pinMode(D8, OUTPUT); // MUX address D
pinMode(A0, INPUT); // MUX input
}
// Function to parse joystick data to hard values
//If joystick is Up then the value is 1 of var A
//If joystick is Down then the value is 1 of var B
//If joystick is in the middle A and B are 0
hardJoystickValues GUIParser(){
// Define joystick offsets (calibrated resting values)
const int offsetPWMCH1 = 1090; // Resting value for PWMCH1 (right joystick Y-axis)
const int offsetPWMCH2 = 1072; // Resting value for PWMCH2 (right joystick X-axis)
const int offsetPWMCH3 = 1043; // Resting value for PWMCH3 (left joystick X-axis)
const int offsetPWMCH4 = 1476; // Resting value for PWMCH4 (left joystick Y-axis)
// Define deadzone threshold
const int deadzone = 120;
// Adjust joystick values by subtracting offsets
int adjustedPWMCH1 = JoystickData.PWMCH1 - offsetPWMCH1;
int adjustedPWMCH2 = JoystickData.PWMCH2 - offsetPWMCH2;
int adjustedPWMCH3 = JoystickData.PWMCH3 - offsetPWMCH3;
int adjustedPWMCH4 = JoystickData.PWMCH4 - offsetPWMCH4;
// Apply deadzone
if (abs(adjustedPWMCH1) < deadzone) adjustedPWMCH1 = 0; //abs to avoid negatives
if (abs(adjustedPWMCH2) < deadzone) adjustedPWMCH2 = 0;
if (abs(adjustedPWMCH3) < deadzone) adjustedPWMCH3 = 0;
if (abs(adjustedPWMCH4) < deadzone) adjustedPWMCH4 = 0;
// Map joystick values to hard values
int LXU = 0; // Left joystick X axis up
int LXD = 0; // Left joystick X axis down
if (adjustedPWMCH1 > 0) {
LXU = 1; // Joystick is up
} else if (adjustedPWMCH1 < 0) {
LXD = 1; // Joystick is down
}
return {LXU, LXD, 0, 0}; // Return the values as a struct
}
int analogReadMultiPlexer(int addressA, int addressB, int addressC, int addressD, int pin)
{
digitalWrite(D3, LOW);
digitalWrite(D6, addressA);
digitalWrite(D7, addressB);
digitalWrite(D9, addressC);
digitalWrite(D8, addressD);
return analogRead(pin);
}
int mapPot(int normalizedValue)
{
return map(normalizedValue, 400, 2500, MINPWMVALUE, MAXPWMVALUE); // map the normalized value to the PWM range
}
This is the full code of the screen. The schematic for the pcb can be found in the final project page
Turning it into a library¶
So I asked Copilot (Claude 3.7 Sonnet) to turn this above into a library. Using this prompt "turn the screen thing into a library so i can easily create menus and stuff" and using the context from my files it came up with this broken and oversized mess.
Failure
#ifndef OLED_MENU_H
#define OLED_MENU_H
#include <Arduino.h>
#include <U8g2lib.h>
// Maximum number of menu items and item name length
#define MAX_MENU_ITEMS 20
#define MAX_ITEM_LENGTH 20
// Button states
#define BUTTON_RELEASED 0
#define BUTTON_PRESSED 1
class OLEDMenu {
public:
// Constructor
OLEDMenu(U8G2_SSD1306_128X64_NONAME_F_HW_I2C* display);
// Initialize the menu
void begin();
// Set button states
void setButtons(bool up, bool select, bool down);
// Add a menu item
bool addMenuItem(const char* item_name, const uint8_t* icon = nullptr);
// Set custom bitmaps for menu items
void setItemBitmap(int item_index, const uint8_t* bitmap);
// Set custom screen for a menu item
void setItemScreen(int item_index, const uint8_t* screen);
// Update and draw the menu
void update();
// Get currently selected item
int getSelectedItem();
// Set selected item
void setSelectedItem(int item);
// Change to specific screen
void setScreen(int screen_number);
// Get current screen
int getCurrentScreen();
// Set scrollbar visibility
void showScrollbar(bool show);
// Add custom screen drawing callback
typedef void (*DrawCallback)(U8G2_SSD1306_128X64_NONAME_F_HW_I2C* display, int item_index);
void setCustomScreenCallback(int item_index, DrawCallback callback);
private:
U8G2_SSD1306_128X64_NONAME_F_HW_I2C* _display;
// Menu items
char _menu_items[MAX_MENU_ITEMS][MAX_ITEM_LENGTH];
const uint8_t* _item_icons[MAX_MENU_ITEMS];
const uint8_t* _item_screens[MAX_MENU_ITEMS];
DrawCallback _custom_callbacks[MAX_MENU_ITEMS];
int _item_count;
int _selected_item;
int _prev_item;
int _next_item;
// Screen control
int _current_screen;
bool _show_scrollbar;
// Button states
bool _button_up;
bool _button_select;
bool _button_down;
bool _button_up_clicked;
bool _button_select_clicked;
bool _button_down_clicked;
// Internal methods
void drawMenuScreen();
void handleButtonEvents();
void updateNavigation();
};
#endif // OLED_MENU_H
Failure
#include "OLEDMenu.h"
// Constructor
OLEDMenu::OLEDMenu(U8G2_SSD1306_128X64_NONAME_F_HW_I2C* display) {
_display = display;
_item_count = 0;
_selected_item = 0;
_current_screen = 0;
_show_scrollbar = true;
// Initialize button states
_button_up = false;
_button_select = false;
_button_down = false;
_button_up_clicked = false;
_button_select_clicked = false;
_button_down_clicked = false;
// Initialize arrays
for (int i = 0; i < MAX_MENU_ITEMS; i++) {
_menu_items[i][0] = '\0';
_item_icons[i] = nullptr;
_item_screens[i] = nullptr;
_custom_callbacks[i] = nullptr;
}
}
void OLEDMenu::begin() {
updateNavigation();
}
void OLEDMenu::setButtons(bool up, bool select, bool down) {
_button_up = up;
_button_select = select;
_button_down = down;
}
bool OLEDMenu::addMenuItem(const char* item_name, const uint8_t* icon) {
// Check if we have space for another item
if (_item_count >= MAX_MENU_ITEMS) {
return false;
}
// Copy the item name
strncpy(_menu_items[_item_count], item_name, MAX_ITEM_LENGTH);
_menu_items[_item_count][MAX_ITEM_LENGTH - 1] = '\0'; // Ensure null termination
// Store the icon
_item_icons[_item_count] = icon;
// Increment the item count
_item_count++;
// Update navigation
updateNavigation();
return true;
}
void OLEDMenu::setItemBitmap(int item_index, const uint8_t* bitmap) {
if (item_index >= 0 && item_index < _item_count) {
_item_icons[item_index] = bitmap;
}
}
void OLEDMenu::setItemScreen(int item_index, const uint8_t* screen) {
if (item_index >= 0 && item_index < _item_count) {
_item_screens[item_index] = screen;
}
}
void OLEDMenu::setCustomScreenCallback(int item_index, DrawCallback callback) {
if (item_index >= 0 && item_index < _item_count) {
_custom_callbacks[item_index] = callback;
}
}
void OLEDMenu::update() {
// Handle button events
handleButtonEvents();
// Draw the appropriate screen
_display->firstPage();
do {
if (_current_screen == 0) {
// Draw the menu screen
drawMenuScreen();
} else if (_current_screen == 1 && _item_screens[_selected_item] != nullptr) {
// Draw item screen
_display->drawBitmap(0, 0, 128/8, 64, _item_screens[_selected_item]);
} else if (_current_screen == 2 && _custom_callbacks[_selected_item] != nullptr) {
// Draw custom screen using callback
_custom_callbacks[_selected_item](_display, _selected_item);
}
} while (_display->nextPage());
}
int OLEDMenu::getSelectedItem() {
return _selected_item;
}
void OLEDMenu::setSelectedItem(int item) {
if (item >= 0 && item < _item_count) {
_selected_item = item;
updateNavigation();
}
}
void OLEDMenu::setScreen(int screen_number) {
_current_screen = screen_number;
}
int OLEDMenu::getCurrentScreen() {
return _current_screen;
}
void OLEDMenu::showScrollbar(bool show) {
_show_scrollbar = show;
}
void OLEDMenu::drawMenuScreen() {
// Draw menu background elements
// You might want to add the bitmap_item_sel_outline here
// Draw previous item
_display->setFont(u8g_font_7x14);
_display->drawStr(25, 15, _menu_items[_prev_item]);
if (_item_icons[_prev_item] != nullptr) {
_display->drawBitmap(4, 2, 16/8, 16, _item_icons[_prev_item]);
}
// Draw selected item
_display->setFont(u8g_font_7x14B);
_display->drawStr(25, 15+20+2, _menu_items[_selected_item]);
if (_item_icons[_selected_item] != nullptr) {
_display->drawBitmap(4, 24, 16/8, 16, _item_icons[_selected_item]);
}
// Draw next item
_display->setFont(u8g_font_7x14);
_display->drawStr(25, 15+20+20+2+2, _menu_items[_next_item]);
if (_item_icons[_next_item] != nullptr) {
_display->drawBitmap(4, 46, 16/8, 16, _item_icons[_next_item]);
}
// Draw scrollbar if enabled
if (_show_scrollbar && _item_count > 1) {
// Draw scrollbar background
// You might want to add bitmap_scrollbar_background here
// Draw scrollbar handle
_display->drawBox(125, 64/_item_count * _selected_item, 3, 64/_item_count);
}
}
void OLEDMenu::handleButtonEvents() {
if (_current_screen == 0) {
// Handle up button for menu navigation
if (_button_up && !_button_up_clicked) {
_selected_item = _selected_item - 1;
_button_up_clicked = true;
if (_selected_item < 0) {
_selected_item = _item_count - 1;
}
updateNavigation();
} else if (!_button_up && _button_up_clicked) {
_button_up_clicked = false;
}
// Handle down button for menu navigation
if (_button_down && !_button_down_clicked) {
_selected_item = _selected_item + 1;
_button_down_clicked = true;
if (_selected_item >= _item_count) {
_selected_item = 0;
}
updateNavigation();
} else if (!_button_down && _button_down_clicked) {
_button_down_clicked = false;
}
}
// Handle select button for screen switching
if (_button_select && !_button_select_clicked) {
_button_select_clicked = true;
// Cycle through screens
_current_screen = (_current_screen + 1) % 3;
// Skip screens that don't have content
if (_current_screen == 1 && _item_screens[_selected_item] == nullptr) {
_current_screen = 2;
}
if (_current_screen == 2 && _custom_callbacks[_selected_item] == nullptr) {
_current_screen = 0;
}
} else if (!_button_select && _button_select_clicked) {
_button_select_clicked = false;
}
}
void OLEDMenu::updateNavigation() {
// Update previous and next item indices
_prev_item = _selected_item - 1;
if (_prev_item < 0) _prev_item = _item_count - 1;
_next_item = _selected_item + 1;
if (_next_item >= _item_count) _next_item = 0;
}
Then I asked it to rewrite it and make it simpler and to add in a way to call functions using these menu tabs and to use the original graphics.
Then it came up with this
Aigenerated
#include "MenuSystem.h"
#include "MenuGraphics.h"
MenuSystem::MenuSystem(U8G2_SSD1306_128X64_NONAME_F_HW_I2C* display) {
_display = display;
_itemCount = 0;
_selectedItem = 0;
_screen = 0;
_btnUp = false;
_btnSelect = false;
_btnDown = false;
_prevBtnUp = false;
_prevBtnSelect = false;
_prevBtnDown = false;
// Initialize arrays
for (int i = 0; i < MAX_ITEMS; i++) {
_itemNames[i][0] = '\0';
_itemIcons[i] = nullptr;
_itemScreens[i] = nullptr;
_itemCallbacks[i] = nullptr;
}
}
void MenuSystem::begin() {
// Ensure display is properly initialized
_display->begin();
_display->clearBuffer();
_display->sendBuffer();
delay(100); // Short delay to ensure display is ready
}
void MenuSystem::addMenuItem(const char* name, const uint8_t* icon) {
if (_itemCount < MAX_ITEMS) {
strncpy(_itemNames[_itemCount], name, MAX_NAME_LEN-1);
_itemNames[_itemCount][MAX_NAME_LEN-1] = '\0'; // Ensure null termination
_itemIcons[_itemCount] = icon;
_itemCount++;
}
}
void MenuSystem::setItemScreen(int index, const uint8_t* screen) {
if (index >= 0 && index < _itemCount) {
_itemScreens[index] = screen;
}
}
void MenuSystem::setItemCallback(int index, void (*callback)()) {
if (index >= 0 && index < _itemCount) {
_itemCallbacks[index] = callback;
}
}
void MenuSystem::setButtons(bool up, bool select, bool down) {
// Save previous button states
_prevBtnUp = _btnUp;
_prevBtnSelect = _btnSelect;
_prevBtnDown = _btnDown;
// Set new button states
_btnUp = up;
_btnSelect = select;
_btnDown = down;
}
void MenuSystem::update() {
// Process button inputs before drawing
handleButtons();
// Simple drawing approach (not using page buffer mode)
_display->clearBuffer();
if (_screen == 0) {
// Draw the main menu screen
drawMenu();
} else if (_screen == 1 && _itemScreens[_selectedItem] != nullptr) {
// Draw the full-screen bitmap for the selected item
_display->drawXBM(0, 0, 128, 64, _itemScreens[_selectedItem]);
}
_display->sendBuffer();
}
void MenuSystem::drawMenu() {
if (_itemCount == 0) return; // Nothing to draw
// Calculate which items to show (previous, current, next)
int prevItem = (_selectedItem > 0) ? _selectedItem - 1 : _itemCount - 1;
int nextItem = (_selectedItem < _itemCount - 1) ? _selectedItem + 1 : 0;
// Draw the background with the item selector outline (if enabled)
// _display->drawXBM(0, 0, 128, 64, bitmap_item_sel_outline);
// Instead, draw a simple selection highlight
_display->drawFrame(0, 20, 128, 24); // Highlight the middle selection
// Draw previous item (top position)
_display->setFont(u8g2_font_6x10_tf); // Use a different, simpler font
_display->drawStr(25, 15, _itemNames[prevItem]);
if (_itemIcons[prevItem] != nullptr) {
_display->drawXBM(4, 2, 16, 16, _itemIcons[prevItem]);
}
// Draw selected item (middle position)
_display->setFont(u8g2_font_6x10_tf); // Use same font for consistency
_display->drawStr(25, 35, _itemNames[_selectedItem]);
if (_itemIcons[_selectedItem] != nullptr) {
_display->drawXBM(4, 24, 16, 16, _itemIcons[_selectedItem]);
}
// Draw next item (bottom position)
_display->setFont(u8g2_font_6x10_tf); // Use same font for consistency
_display->drawStr(25, 55, _itemNames[nextItem]);
if (_itemIcons[nextItem] != nullptr) {
_display->drawXBM(4, 46, 16, 16, _itemIcons[nextItem]);
}
// Draw a simple scrollbar instead of using bitmap
if (_itemCount > 1) {
_display->drawVLine(125, 0, 64); // Scrollbar background
int barHeight = 64 / _itemCount;
int barY = (_selectedItem * 64) / _itemCount;
_display->drawBox(123, barY, 4, barHeight); // Scrollbar handle
}
}
void MenuSystem::handleButtons() {
// Only respond to button press events (not holds)
// Button rising edge detection for up
if (_btnUp && !_prevBtnUp) {
_selectedItem = (_selectedItem > 0) ? _selectedItem - 1 : _itemCount - 1;
Serial.print("Selected item: ");
Serial.println(_selectedItem);
}
// Button rising edge detection for down
if (_btnDown && !_prevBtnDown) {
_selectedItem = (_selectedItem < _itemCount - 1) ? _selectedItem + 1 : 0;
Serial.print("Selected item: ");
Serial.println(_selectedItem);
}
// Button rising edge detection for select
if (_btnSelect && !_prevBtnSelect) {
// Execute callback if it exists
if (_itemCallbacks[_selectedItem] != nullptr) {
_itemCallbacks[_selectedItem]();
}
// Toggle between menu and item screen if screen exists
if (_itemScreens[_selectedItem] != nullptr) {
_screen = (_screen == 0) ? 1 : 0;
Serial.print("Screen changed to: ");
Serial.println(_screen);
}
}
}
Aigenerated
#ifndef MENU_SYSTEM_H
#define MENU_SYSTEM_H
#include <Arduino.h>
#include <U8g2lib.h>
class MenuSystem {
public:
// Constructor
MenuSystem(U8G2_SSD1306_128X64_NONAME_F_HW_I2C* display);
// Initialize menu
void begin();
// Add menu item with icon
void addMenuItem(const char* name, const uint8_t* icon);
// Set callback function for item
void setItemCallback(int index, void (*callback)());
// Set button states
void setButtons(bool up, bool select, bool down);
// Update and draw menu
void update();
private:
U8G2_SSD1306_128X64_NONAME_F_HW_I2C* _display;
// Menu items
static const int MAX_ITEMS = 10;
static const int MAX_NAME_LEN = 20;
char _itemNames[MAX_ITEMS][MAX_NAME_LEN];
const uint8_t* _itemIcons[MAX_ITEMS];
const uint8_t* _itemScreens[MAX_ITEMS];
void (*_itemCallbacks[MAX_ITEMS])();
// Menu state
int _itemCount;
int _selectedItem;
int _screen; // 0 = menu, 1 = item screen
// Button handling
bool _btnUp;
bool _btnSelect;
bool _btnDown;
bool _prevBtnUp;
bool _prevBtnSelect;
bool _prevBtnDown;
// Draw the menu
void drawMenu();
// Update button states
void handleButtons();
};
#endif
Which worked really well with my code
Example
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "MenuSystem.h"
#include "Graphics.h"
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
MenuSystem menu(&u8g2);
//=====================================================================================//
// Struct declarations
typedef struct struct_message
{
int PWMCH1;
int PWMCH2;
int PWMCH3;
int PWMCH4;
} struct_message;
struct_message JoystickData; // declare the struct as JoystickData
struct hardJoystickValues
{
int LXU; // Left joystick X axis up
int LXD; // Left joystick X axis down
int RXU; // Right joystick X axis up
int RXD; // Right joystick X axis down
};
hardJoystickValues JoystickDataHard; // declare the struct as JoystickDataHard
//=====================================================================================//
//declarations
void MUXSetup();
hardJoystickValues GUIParser();
int analogReadMultiPlexer(int addressA, int addressB, int addressC, int addressD, int pin);
int mapPot(int normalizedValue);
// Controller declarations
const int MAXPWMVALUE = 1000;
const int MINPWMVALUE = 2000;
bool buttonRight;
bool buttonLeft;
// Actions for menu items
void action3DCube() {
Serial.println("3D Cube selected!");
// Your 3D cube function code here
}
void actionBattery() {
Serial.println("Battery selected!");
// Your battery function code here
}
void actionDashboard() {
Serial.println("Dashboard selected!");
// Your dashboard function code here
}
void actionFireworks() {
Serial.println("Fireworks selected!");
// Your fireworks function code here
}
void setup() {
Serial.begin(9600);
delay(100); // Small delay to stabilize
Wire.begin(); // Initialize I2C bus
Wire.setClock(400000); // Set to 400kHz (standard speed for most displays)
u8g2.begin();
u8g2.clearBuffer();
u8g2.sendBuffer();
MUXSetup(); // Setup the multiplexer
// Initialize the menu
menu.begin();
// Add menu items
menu.addMenuItem("3D Cube", bitmap_icons[0]);
menu.addMenuItem("Battery", bitmap_icons[1]);
menu.addMenuItem("Dashboard", bitmap_icons[2]);
menu.addMenuItem("Fireworks", bitmap_icons[3]);
menu.addMenuItem("WEEEEE", bitmap_icons[4]);
// Add action callbacks for menu items
menu.setItemCallback(0, action3DCube);
menu.setItemCallback(1, actionBattery);
menu.setItemCallback(2, actionDashboard);
menu.setItemCallback(3, actionFireworks);
// Clear the display once more
u8g2.clearBuffer();
u8g2.sendBuffer();
Serial.println("Menu system initialized");
}
void loop() {
// Read joystick and button inputs
JoystickData.PWMCH1 = mapPot(analogReadMultiPlexer(0, 0, 0, 0, A0)); //Right joystick Y
JoystickData.PWMCH2 = mapPot(analogReadMultiPlexer(1, 0, 0, 0, A0)); // Right joystick X
JoystickData.PWMCH3 = mapPot(analogReadMultiPlexer(0, 0, 0, 1, A0)); // left joystick Y
JoystickData.PWMCH4 = mapPot(analogReadMultiPlexer(1, 0, 0, 1, A0)); // left joystick X
buttonRight = analogReadMultiPlexer(0, 0, 1, 0, A0) > 2000; // right button
buttonLeft = analogReadMultiPlexer(1, 0, 1, 0, A0) > 2000; // left button
// Parse joystick data
JoystickDataHard = GUIParser();
// Update menu button states
menu.setButtons(JoystickDataHard.LXD, buttonRight, JoystickDataHard.LXU);
// Update and draw the menu
menu.update();
// Small delay to debounce buttons
delay(50);
}
void MUXSetup()
{
pinMode(D3, OUTPUT); // MUX enable
pinMode(D6, OUTPUT); // MUX address A
pinMode(D7, OUTPUT); // MUX address B
pinMode(D9, OUTPUT); // MUX address C
pinMode(D8, OUTPUT); // MUX address D
pinMode(A0, INPUT); // MUX input
}
// Function to parse joystick data to hard values
hardJoystickValues GUIParser(){
// Define joystick offsets (calibrated resting values)
const int offsetPWMCH1 = 1090; // Resting value for PWMCH1 (right joystick Y-axis)
const int offsetPWMCH2 = 1072; // Resting value for PWMCH2 (right joystick X-axis)
const int offsetPWMCH3 = 1043; // Resting value for PWMCH3 (left joystick X-axis)
const int offsetPWMCH4 = 1476; // Resting value for PWMCH4 (left joystick Y-axis)
// Define deadzone threshold
const int deadzone = 120;
// Adjust joystick values by subtracting offsets
int adjustedPWMCH1 = JoystickData.PWMCH1 - offsetPWMCH1;
int adjustedPWMCH2 = JoystickData.PWMCH2 - offsetPWMCH2;
int adjustedPWMCH3 = JoystickData.PWMCH3 - offsetPWMCH3;
int adjustedPWMCH4 = JoystickData.PWMCH4 - offsetPWMCH4;
// Apply deadzone
if (abs(adjustedPWMCH1) < deadzone) adjustedPWMCH1 = 0; //abs to avoid negatives
if (abs(adjustedPWMCH2) < deadzone) adjustedPWMCH2 = 0;
if (abs(adjustedPWMCH3) < deadzone) adjustedPWMCH3 = 0;
if (abs(adjustedPWMCH4) < deadzone) adjustedPWMCH4 = 0;
// Map joystick values to hard values
int LXU = 0; // Left joystick X axis up
int LXD = 0; // Left joystick X axis down
if (adjustedPWMCH1 > 0) {
LXU = 1; // Joystick is up
} else if (adjustedPWMCH1 < 0) {
LXD = 1; // Joystick is down
}
return {LXU, LXD, 0, 0}; // Return the values as a struct
}
int analogReadMultiPlexer(int addressA, int addressB, int addressC, int addressD, int pin)
{
digitalWrite(D3, LOW);
digitalWrite(D6, addressA);
digitalWrite(D7, addressB);
digitalWrite(D9, addressC);
digitalWrite(D8, addressD);
return analogRead(pin);
}
int mapPot(int normalizedValue)
{
return map(normalizedValue, 400, 2500, MINPWMVALUE, MAXPWMVALUE); // map the normalized value to the PWM range
}
Now I will turn this into a proper library and clean up what the AI made because there are still some things I want different. I first started off removing all of my code and reducing it down to just what's needed to run the screen with the library. Then I replaced all variables with generic ones instead of using my structs and bringing confusion
Instead ofI also removed all unused graphics from the file reducing it down to 65 lines instead of 1165 lines. Then I added some documentation to everything using DoxyGen.
Doxygen makes it so you can hover functions and see what they do and what all parameters are.