Skip to content

15. Interface and application programming

This week I made a simple application that can interact with microcontrollers. It will also be part of my final project.

Overview

  • write an application that interfaces a user with an input and/or output device that you made
  • group assignment: compare as many tool options as possible

View group assignment here

I have decided to complete the first part, which is writing an app that interface with input/output device that was made previously. In fact, to keep it simple, push buttons are input devices, and onboard LEDs are output devices. That got me thinking, of what application can I write that uses my 1614 board’s onboard i/o device?

After thinking for some time, I have decided to work on a simple version of a morse code output application. It is going to have the barebones functionality, and only sends the following in morse code:

  • “UP”
  • “DOWN”
  • “LEFT”
  • “RIGHT”

The reason for this is to also be in-line with my final project, which is an omniwheel robot. I will perhaps think about how to add a joystick to the applicaiton and think about how to make it feel more natural to use.

Mock-up

First, I tried to conceive a design that I could follow by drawing by hand how it would look like in the end.

Mockup

Basic components:

  • Joystick
  • D Pad
  • 4 virtual LEDs that indicate D Pad being pressed

I want to simplify it further, as the joystick is simply not used in this project. Perhaps just a layout that follows WASD/arrow keys and some virtual LEDs would suffice.

Mockup

Software to develop

I was aware of the following methods to write the application

  • Processing: Steven has extensively demo-ed to me and Jon on its usage
  • MIT App Inventor: Used to write native apps for android devices
  • Scratch: Drag-and-drop interface used to write programs
  • p5.js: A browser based application programming interface
  • Python tkinter: used to draw user interface for simple programs (interfaces with the Tcl/Tk GUI toolkit)

I wanted to try something new, so my choices boiled down to p5.js or tkinter. I wanted to try out writing a local app in python for communication with microcontrollers, hence I decided to try Tkinter instead.

How it was made

After that, I started to get to work. There are a total of 3 things I needed to learn/ decide:

  • How to use Tkinter
  • How to communicate via serial port from python scripts
  • What data structure/data format should be followed

How to use Tkinter

Initially, I tried to read the documentation directly, but found that it builds upon knowledge of Tckl/Tk libraries. I then took searched for online examples and API references that I can perhaps follow, which I then stumbled upon this link.

I first started by writing a simple Hello World program.

import tkinter as tk

if __name__ == "__main__":
    # starts a window
    window = tk.Tk()

    # adds a label to the screen
    greeting = tk.Label(text="Hello World")
    greeting.pack()

    # keeps the window on, until it is closed
    window.mainloop()

This program opens a window and shows a “Hello World” text. This is done by:

  1. Create a base window object first
  2. Create a Label object with “Hello World”
  3. Use pack to place the object in the window object. We can use place to specify where to put it on the window/
  4. use window.mainloop() to keep the application running

Hello World

Next, I identified that I need to learn how to use Buttons, which I also write a demo program for it. Replace the greeting label with the following

def buttonCallback():
    print("Button triggered")
...
# add a button to the screen
    button = ttk.Button(window, text="Click me to see what happens", command=buttonCallback)
    button.pack(
        ipadx=5,
        ipady=5,
        expand=True
    )

This code basically places a themed button (ttk) on the window. The button is linked to a callback, which is essentially a function to be run, when it is pressed.

Running the code in Terminal

Pressed button

I then expanded it to represent how should the final UI be:

Final UI

The tricky parts are as follows:

  • How do I represent virtual LEDs?
  • We need to think as low-level as possible. Basically, we need to change the original circle with a circle that has color in it for a brief moment, then change it back to how it originally was.
  • How do I run a function without anything being triggered? (After pressing button)
  • Two approaches:
    1. Making a on-press callback and an on-release callback
    2. Run after a timed delay
  • I decided to do the latter as a concept of LED triggered on click

And by modifying the code above, I get the following:

def offCallback(x1, x2, y1, y2):
    canvas.create_oval(x1, x2, y1, y2, fill=offColor)
    return None

def upCallback():
    print("up pressed")
    canvas.create_oval(12, 12, 76, 76, fill=upColor)
    window.after(300, lambda : offCallback(12, 12, 76, 76))
    return None
...
up = ttk.Button(
        window,
        image=upImage,
        command=upCallback
    )
...
up.place(x=150, y=0)
...
canvas = tk.Canvas(window)
canvas.create_oval(12, 12, 76, 76, fill=offColor)
...

There seems to be a lot going on, let’s break it down a little:

  1. Firstly, we defined a button and fill it with an image instead of text.
  2. Then, we place the button somewhere in the window
  3. We then create a canvas, which takes in a window as parameter, to be used to draw circles in the window.
  4. The buttons are tied to a callback. In this case, the up button is tied to upCallback
  5. The upCallback does 2 things:
  6. Draws a circle with different color on the screen
  7. attaches a callback to the windows object that runs 300ms after the callback is done running. The callback replaces the circle with the original color on the screen

And replicate the above code a few times, we are done!

How to communicate via serial port?

In previous weeks, we used the Arduino’s Serial Monitor to check some status sent from the arduino. Imagine the arduino Serial Monitor as an “application”, where it interacts with the port

Illustration of Serial Monitor to access COM port

What we need to do is to change the Serial Monitor into our own application!

The same way that we debug arduino programs with serial monitor, our application needs an arduino program to debug it. To do this, I wrote a simple arduino code that interacts with Serial port.

#define button 1 // PA5
#define led 10 // PA3

void setup() {
  // put your setup code here, to run once:
  pinMode(led, OUTPUT);
  pinMode(button, INPUT);
  Serial.begin(9600);
  while(!Serial.available()); // wait for serial
  String retrieve = Serial.readString();
  digitalWrite(led, HIGH);
}

void loop() {
  // put your main code here, to run repeatedly:
  int result = digitalRead(button);
  Serial.println(result);
  delay(100);
}

Basically, this code waits for something to be sent to the serial port. Once it has received it, it lights up an onboard LED. The code in the loop() is used to debug what values will be received on the computer side, and actually we just need to send it once for our test program to work.

Next, we write a simple code on the python side, which will replace the “Serial Monitor” segment on the diagram above.

import serial

def encode(input: str):
    return str.encode(input)

if __name__ == "__main__":
    com_port = "COM12"
    baud_rate = 9600
    port = serial.Serial(com_port, baud_rate)

    result = 1
    port.write(encode("Hello World"))
    while(result == 1):
        result = int(port.readline())
        if (result == 0):
            print("Button pushed, done")
            break
        print("Waiting for terminating")
    print("End")

Firstly, the line port.write(encode("Hello World")) is used to make the serial port available. The code will then run until it recieves a “0” from the serial port. “0” is sent directly from the readings of the button.

And with that, we are able to communicate from ATTiny1614 to our custom applications!

What data structure/data format should be followed?

Take the example above, we are using just 0s and 1s to identify which state the program should be in. In fact, the diagram below shows what we have essentially done:

Diagram of sending values between application and ATTiny1614

From the ATTiny1614 side, the logic LOW is represented as a 0 string, while the state on the application side is determined by some 0/1 integer value, and this value needs to be determined by the value received from the COM port.

Similarly, the buttons can be represented by integers as well:

Diagram of direction buttons to values sent form application

Final product

Now all we need to do is to put everything together. As an illustration, consider the code for the GUI:

  • When the upCallback is triggered, we need to serialize and send over the corresponding state from the application. (Add a port.write() in this callback)
  • The application then converts this result into bytes and sends it to the ATTiny1614
  • When ATTiny1614 receives the result, it needs to run the corresponding code block that actually blinks the LED according to the morse code representation of the alphabets

Morse code of alphabets
* -> short beep
_ -> long beep

UP -> * * _    * _ _ *
DOWN -> _ * *   _ _ _   * _ _   _ *
LEFT -> * _ * *   *   * * _ *   _
RIGHT -> * _ *   * *   _ _ *   * * * *   _

Next, a snippet of arduino code is written as shown

void short_delay() {
  delay(250);
}

void long_delay() {
  delay(750);
}

void short_beep() {
  digitalWrite(led, HIGH);
  short_delay();
  digitalWrite(led, LOW);
}

void long_beep() {
  digitalWrite(led, HIGH);
  long_delay();
  digitalWrite(led, LOW);
}
...
void up(){
  // U
  short_beep();
  short_delay();
  short_beep();
  short_delay();
  long_beep();
  long_delay();

  // P
  short_beep();
  short_delay();
  long_beep();
  short_delay();
  long_beep();
  short_delay();
  short_beep();
  long_delay();
}
...
void loop() {
  // put your main code here, to run repeatedly:
  if(Serial.available()) {
    int result = Serial.parseInt();
    switch(result) {
      case 0:
        up();
        break;
      case 1:
        down();
        break;
      case 2:
        left();
        break;
      case 3:
        right();
        break;
    }
    while(Serial.available()) {
      Serial.read(); // empty buffer
    }
  }

The code above heavily uses abstraction, and can definitely be optimized if need be. That being said, it is easier to explain it if it is written this way.

Reading from the loop, if there is data on serial port, it runs either up(), down(), left(), right(). Each of these correspond to their abstracted morse code logics (short beep, long beep, etc.).

The result is that the LED blinks accordingly!

Code files

This week, I didn’t need to make additional boards, but mostly wrote code to complete the assignment


Last update: November 30, 2022