Skip to content

Interface and Application Programming

This week I started programming a menu structure for the display used in my final project which is controlled via a rotary encoder.

I also checked in with Kerstin and Lars, the other students from HRW FabLab, to see what they have come up with and compare our work.

You can find our page right here.

Organizing my thoughts

The plan is pretty simple, I want to create a menu structure similar to the one usually found on 3D printers like the Creality Ender 3.

A basic UML diagram should help organize my thoughts.

If you are not familiar with UML (Unified Modeling Language) you can read more about it right here.

Take this as a little cheat sheet:

I visited Visual Paradigm and created this:

When powering up the system, I want it to show a splash screen with a logo or something I’ll figure out later.

After pressing the encoder’s button, the menu screen should appear and default to highlighting the first entry.

By turning the encoder clockwise or counterclockwise, I want to be able to scroll through the list and enter the submenus via another press of the button.

The graph menu is going to look something like this but with actual sensor values.

The calibration submenus should show the current value and have instructions for inserting rods of a known diameter to calibrate.

I also think it would be a smart move to clear / turn off the display after a set amount of time to prevent OLED burn-in.

Initial setup

My first setup was an absolute mess.

Since this was kind of a test to see if the rotary encoder even works with 3.3V (the datasheet is a vague as can be and only mentions 5V) I quickly soldered some wires to the encoder’s pins and used a breadboard to set up the connections and pull-up resistors.

I modified a piece of code I found here which let me confirm that my encoder indeed works.

#define pinA 10
#define pinB 8
#define pinSW 9

 int counter = 0; 
 int aState;
 int aLastState;  

 void setup() { 
   pinMode (pinA,INPUT);
   pinMode (pinB,INPUT);
   pinMode (pinSW,INPUT);

   Serial.begin (9600);
   aLastState = digitalRead(pinA);   
 } 

 void loop() { 
   aState = digitalRead(pinA);
   if (aState != aLastState){     
     if (digitalRead(pinB) != aState) { 
       counter ++;
     } else {
       counter --;
     }
     Serial.print("Position: ");
     Serial.println(counter);
   } 
   if(!digitalRead(pinSW)){
    Serial.println("button pressed");
    delay(50);
   }
   aLastState = aState;
 }

The way this works is by first reading the state of pin A.

At the start of each loop it is read again and if the state has changed, that means the encoder has been turned.

To determine in which direction it has been turned the state of pin A is compared to pin B’s state.

The detected rotation increases or decreases a counter that is then shown via the serial monitor.

To finish the test, I added another message to the serial monitor that shows up every time the encoders button is pressed.

Next I reintroduced my OLED display from week 10, which has definitely seen better days…

At least it still worked, so I started researching about how to set up menus.

Before I went back to coding though, I got tired of touching the encoder’s bare metal, so I quickly designed and printed a knob to press on.

Much better… you can find the Fusion file in the Download section at the bottom of this page.

After watching and following a Tutorial by MaxTechTV on YouTube, I adapted his code to the U8g2 library.

In doing so I realized a fatal flaw in the way I programmed the encoder logic.

It kept skipping values which doesn’t really work all that well for a tool to select menu points.

So instead of coding the logic myself, I installed the RotaryEncoder library by Matthias Hertel.

You can read more about his work right here.

With the library set up, I got to work on my menu. Here is what I came up with:

 void updateMenu() {
  switch (menu) {

    case 0:
      delay(200);
      menu = 1;
      encoder.setPosition(1);
      break;

    case 1:
      U8G2.clearDisplay();
      U8G2.setFont(u8g2_font_ncenB14_tr);
      U8G2.setCursor(0, 25);
      U8G2.print(">Graph");
      U8G2.setCursor(0, 55);
      U8G2.print("  Calibrate S1");
      U8G2.sendBuffer();
      break;

    case 2:
      U8G2.clearDisplay();
      U8G2.setFont(u8g2_font_ncenB14_tr);
      U8G2.setCursor(0, 25);
      U8G2.print("  Graph");
      U8G2.setCursor(0, 55);
      U8G2.print(">Calibrate S1");
      U8G2.sendBuffer();
      break;

    case 3:
      U8G2.clearDisplay();
      U8G2.setFont(u8g2_font_ncenB14_tr);
      U8G2.setCursor(0, 25);
      U8G2.print(">Calibrate S2");
      U8G2.setCursor(0, 55);
      U8G2.print("  Calibrate S3");
      U8G2.sendBuffer();
      break;

    case 4:
      U8G2.clearDisplay();
      U8G2.setFont(u8g2_font_ncenB14_tr);
      U8G2.setCursor(0, 25);
      U8G2.print("  Calibrate S2");
      U8G2.setCursor(0, 55);
      U8G2.print(">Calibrate S3");
      U8G2.sendBuffer();
      break;

    case 5:
      menu = 4;
      encoder.setPosition(4);
      break;
  }
}

void executeAction() {
  switch (menu) {

    case 1:
      U8G2.clearDisplay();
      delay(500);
      while(digitalRead(pinSW)) {
      graph();
      }
      break;

    case 2:
    U8G2.clearDisplay();
    delay(500);
    while(digitalRead(pinSW)){
      U8G2.setFont(u8g2_font_ncenB14_tr);
      U8G2.setCursor(0, 25);
      U8G2.print("Calibration");
      U8G2.setCursor(0, 55);
      U8G2.print("Sensor 1");
      U8G2.sendBuffer();
    }
      break;

    case 3:
    U8G2.clearDisplay();
    delay(500);
    while(digitalRead(pinSW)){
      U8G2.setFont(u8g2_font_ncenB14_tr);
      U8G2.setCursor(0, 25);
      U8G2.print("Calibration");
      U8G2.setCursor(0, 55);
      U8G2.print("Sensor 2");
      U8G2.sendBuffer();
    }
      break;

    case 4:
    U8G2.clearDisplay();
    delay(500);
    while(digitalRead(pinSW)){
      U8G2.setFont(u8g2_font_t0_11b_tr);
      U8G2.setCursor(0, 10);
      U8G2.print("Hollow Knight");
      U8G2.setCursor(0, 36);
      U8G2.print("Silksong");
      U8G2.setCursor(0, 62);
      U8G2.print("THIS YEAR!!!");
      U8G2.sendBuffer();
    }
      break;
  }
}

The updateMenu() function works by taking the current encoder position and based on that changes the OLED output.

For now there are only 4 points to my menu so I included cases for the encoder’s position being lower than 1 or higher than 4 that automatically adjust the position to be usable again.

executeAction() handles presses of the encoder’s button and displays new information until the button is pressed again.

Of course my graph function is also included.

 void graph(){
    U8G2.drawVLine(0, 0, 64);                               // draw Y-axis
    U8G2.drawHLine(0, 63, 128);                             // draw X-axis

    if(!digitalRead(pinSW)){
      menu = 0;
      updateMenu();
    }

    for (int j = 0 ; j < 120; j++){
      values[j] = buffer[j];                                // transfer values from buffer to output array
      U8G2.drawPixel(j+4, values[j]);                       // draw the sine wave with values from the array shifted 4 pixels to the right
    }
      U8G2.sendBuffer();
      U8G2.clearBuffer();
    for (int x = 0 ; x < 120; x++){                         // loop array via buffer
      if(x-1 < 0){
        buffer[x] = values[119];
      } else {
      buffer[x] = values[x-1];
      }
    }
  }

The result speaks for itself:

There is a little more to my code but it would be too much to just paste as plain text.

Take a look at the encoder_oled.ino file in the Download section at the bottom of this page.

Producing new boards

To tidy up the mess I created, I hopped into KiCAD and designed some new boards.

Downloads

encoder_knob.f3d

encoder_oled.ino