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 a new board.

You know what’s REALLY fun? Doing the same stupid mistake twice.

Take a minute and think about what might be wrong with this board…

If you guessed that I2C needs pull-UP resistors instead of pull-DOWNs like I have here, you are correct.

Nothing another quick and dirty fix can’t… fix…

Or so I thought…

Guess what’s EVEN FUNNIER than doing the same stupid mistake twice.

You’re damn right, I did it yet again.

Not only does anything related to I2C need pull-up resistors, but also the rotary encoder.

I have no idea, how or why this happened.

I mean, I had the whole thing built on a breadboard right in front of me the whole time I was designing the board, but I guess my brain is just completely fried at this point.

Anyways, time to desolder more pull-downs and see if I can solder three more quick and dirty fixes to the same port.

Deja_Vu.mp3 starts playing

This soldering job should really not exist and didn’t even fix the problem.

After a small break and some research, I ended up changing the pin definitions to use the internal pull-ups of the mainboard..

The board’s KiCAD file is linked down below, but I would only recommend using it for educational purposes.

Programming a GUI

To go a little further than a simple menu on the built in screen, I tried out building a proper GUI that runs on a computer and communicates with my board via a USB connection.

Since I actually enjoy working in the Arduino environment quite a bit, I decided to stick to Processing, which the Arduino IDE is based on.

Immediately upon starting the software, you’ll notice the similarities.

I began by following this tutorial by Hardik Rathod over on Hackster.io.

As a first order of business, I headed over to the contribution manager and installed ControlP5 by Andreas Schlegel, a library built to simplify creating GUIs.

I then imported said library and created a basic window with a dark blue background.

Next it was time to add a button to the window by creating a ControlP5 object, defining a new button, giving it a name, position and size.

One button doesn’t really cut it for a GUI, so let’s add some more.

Now function wise I kinda didn’t want to reinvent the wheel so I gave the buttons the same functions as the OLED menu.

One Button to show the graph, one for all the sensor values, one for calibration and one to return to the menu.

To do this I imported Processing’s serial communication library and set up the correct port for my controller.

After that I added functions with names corresponding to the buttons.

These will simply send a character via the serial port.

import controlP5.*;
import processing.serial.*;

Serial port;

ControlP5 cp5;

void setup() {
  size(500,650);

  port = new Serial(this, "COM17", 9600);

  cp5 = new ControlP5(this);

  cp5.addButton("graph")
  .setPosition(150,50)
  .setSize(200,100);

  cp5.addButton("values")
  .setPosition(150,200)
  .setSize(200,100);

  cp5.addButton("calibrate")
  .setPosition(150,350)
  .setSize(200,100);

  cp5.addButton("menu")
  .setPosition(150,500)
  .setSize(200,100);
}

void draw(){
  background(0,0,80);
}

void graph(){
  port.write('g');
}

void values(){
  port.write('v');
}

void calibrate(){
  port.write('c');
}

void menu(){
  port.write('m');
}

As always, you can find the file in the Download section at the bottom of this page.

After the GUI side was done I hopped back over to the Arduino IDE and started adjusting my menu code to accept and process the received characters.

Doing this was actually pretty straight forward, I just made sure that the GUI program and controller were talking at the same baud rate and created the following function, which I called in the loop section as well as inside the individual functions.

  void executeSerial(){
      char c = Serial.read();

      if(c == 'g') {
        menu = 1;
        updateMenu();
        executeAction();
      }

      if(c == 'c') {
        menu = 2;
        executeAction();
      }

      if(c == 'v') {
        menu = 3;
        executeAction();
      }

      if(c == 'm') {
        menu = 0;
        updateMenu();
      }
  }

The entire .ino file would be too large to paste here as plain text, so it is linked down below.

Stay hydrated.

Downloads

encoder_knob.f3d

encoder_oled.ino

Mainboard_Display.zip

GUI_communication.ino

simple_GUI.pde