Skip to content

14. Interface and Application Programming

Objectives

  • Group Assignment: Compare as many tools options as possible.
  • Write an application that interfaces a user with an input and/or output device that you made.

Group assignment

Jason and I compared several possibilities when it comes to languages and interfaces on the group page.

Previous design

Last week, I developed my network featuring three boards (one master, two slaves) that can communicate so I can switch on and off the LEDs on the boards using the Serial USB port with the master. Initially, I only had one slave and one master and I finally added a second slave:

logo text
The 2 slaves and the master together

Making the LEDs subsequently blink


Commanding the indivual LEDs through the network

This week’s objective

This week, I intend to develop a Python interface using Kivy to command the LED on each board with this interface. Kivy is an open-source Python framework to develop mainly mobile apps and multitouch application software. I won’t really be using these capabilities but I will learn the basics and if I ever want to develop a mobile application, that will be useful.

Moreover, initially, I wanted to use PyQt but I already know some of it as I used it for other projects so I wanted to try something else !

logo text
A previous project made with PyQt

1. Talking with the board

First, I needed to have a working Python script that allows me to communicate with my board. For the moment, I type the commands (like b21 to switch on the second led on the slave on address b) directly in the terminal with a Serial connection through Putty or the Arduino IDE.

I installed PySerial (pip install pyserial), opened a new serial connection on the right port, with the right baud rate, and then, using serial.write() I can automate the sending of the commands and light up the LEDs. The code itself is pretty easy but works well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import serial
import sys
import time

LEDs = ['b1','b2','b3', 'c1','c2','c3']
DLY = 0.1

ser = serial.Serial('COM7', 19200, timeout = 0)

if(ser.is_open):
    print("connected")
else:
    print("Serial not connected")
    try:
        ser.close()
    except:
        pass
    sys.exit()


for i in range(len(LEDs)):
    cmd = (LEDs[i]+"0")
    print(cmd)
    ser.write(cmd.encode())
    line = ser.readline()
    print(line)
    ser.flush()
    time.sleep(DLY)

for i in range(len(LEDs)):    
    cmd = (LEDs[i]+"1")
    print(cmd)
    ser.write(cmd.encode())
    line = ser.readline()
    print(line)
    ser.flush()
    time.sleep(DLY)

ser.close()

2. Installing Kivy and playing with it

To install Kivy, simply use pip install kivy. This is a lightweight version and if you need audio and video, you can use pip install kivy[base.media].

When designing a Kivy app, the idea is to:

  • sub-classing the App class
  • Implement the build() method and returns the widget instance
  • instantiate the App class and call its run() method

I started by launching some examples from the documentation and I ran into an annoying issue with my Python IDE (Spyder 4.0): Whenever I want to close the window, it freezes and I need to shut down the kernel to make it quit and be able to launch a new instance of my app.

I quickly realized that I was having issues with the built-in Spyder console so I changed the settings in the run - Configuration per file options to make this particular script run in an external window. I then added some shebang lines to make sure my terminal understands it’s a Python3 script. #!/usr/bin/env python3

logo text
Configuration per file
logo text
External console

Now when I close the window or the terminal, everything closes correctly.

3. Kivy basics and Pong tutorial

To learn Kivy basics, I follow the Pong tutorial in the documentation. I learned how to create the application, build it, add widgets, play with properties, …

logo text
Early pong design

The video presents artifacts but they are due to encoding issues.

4. Making my app

To design my app, I played with many more Kivy possibilities and added iteratively new components. First, I added some buttons and labels in a simple Grid layout. I changed their colors and behaviors on presses and I could detect the presses on the buttons really easily.

1
2
3
def on_press(self):
    self.background_color = (0.0, 0.8, 0.0, 1.0)
    print("pressed")

But I didn’t know which button was pressed really easily by using the buttons object attributes.

1
2
3
4
5
6
7
def callback(instance):
    print('The button <%s> is being pressed' % instance.text)

### in the layout:
btn1 = LEDButtons(text='LED 1')
btn1.bind(on_press=callback)
self.add_widget(btn1)

logo text

Finally, we can do that for all the buttons to detect which one was pressed.

logo text

Then I added some Serial detection of connection and disconnections and the button to initialize a new connection. The serial connection is refreshed and detected every 66ms. I also started playing around with multiple layouts to make my app more modular.

Finally, I mixed my PySerial script with the buttons, using dictionaries and arrays to store all the values, added some more layouts and buttons as well as a “spinner” to select the COM port and I was finally happy with my interface. I can now make my LEDs blink in sequence or light every single one on demand. It also detects connections and disconnections.

logo text
Final interface

The final code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# #!/usr/bin/env python3
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Color, Rectangle, Line
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.uix.spinner import Spinner

### serial USB connection
import serial
import serial.tools.list_ports
import time

COM = "COM7" #Default COM port
LEDs = ['b1','b2','b3', 'c1','c2','c3'] #LEDs list
LEDsStates = [0,0,0,0,0,0]# Current LED states
LEDDict = { #dictionnary to go from LED name to index
    "b1": 0,
    "b2": 1,
    "b3": 2,
    "c1": 3,
    "c2": 4,
    "c3": 5
    }
DLY = 0.1 #Delay between transmissions (s)

def cmdLED(led,param):
    """transmit data to switch on/off an LED"""
    #led = LED name
    #param = 1 (on) or 0 (off)
    cmd = (led+param)
    print(cmd)
    ser.write(cmd.encode())
    line = ser.readline()
    print(line)
    ser.flush() #empty the outgoing buffer
    time.sleep(DLY)

def blinkLEDs(instance=None):
    """Blink all LEDs in sequence"""
    for i in range(len(LEDs)):    
        cmdLED(LEDs[i],'1')
    for i in range(len(LEDs)):
        cmdLED(LEDs[i],'0')

def scanPorts():
    """scan serial ports to find available ones"""
    ports = serial.tools.list_ports.comports()
    portList = []
    for p in ports:
        portList.append(str(p.device))
    return portList

connectionLabel = None
COMSpinner = None
ser = None
portList = scanPorts()
### Connection
try:
    ser = serial.Serial(COM, 19200, timeout = 0.1)
    if(ser.is_open):
        print("connected")
    else:
        print("Serial not connected")
        try:
            ser.close()
        except Exception as e:
            print(e)
except Exception as e:
    print(e)
    ser = None
    # sys.exit()


if(ser):
    #Initialization of LEDs: all off
    for i in range(len(LEDs)):
        cmdLED(LEDs[i],'0')
        LEDsStates[i] = 0
    blinkLEDs() #then blink them


class AppLayout(BoxLayout):
    """main layout"""
    def __init__(self, **kwargs):
        super(AppLayout, self).__init__(**kwargs)

        self.orientation = 'vertical'
        ### Connection Info
        connectLyt = connectLayout(size_hint=(1,0.2))
        self.add_widget(connectLyt)

        ### blink button
        blinkBtn = BlinkButton(text="Blink LEDs",size_hint=(1,0.2))
        self.add_widget(blinkBtn)

        ### LEDs and slaves layout
        slaveLyt = slaveLayout()
        self.add_widget(slaveLyt)


    def checkPortPresence(self):
        """runs at 15Hz, scan ports and detects (dis)connections"""
        global COM, ser
        availablePorts = scanPorts()
        COMSpinner.values = availablePorts
        if(COM not in availablePorts):
            print("Device disconnected")
            connectionLabel.notConnected()
            ser = None
        else:
            connectionLabel.connected()
        if(ser):
            try:
                if(ser.is_open):
                    connectionLabel.connected()
                else:
                    connectionLabel.notConnected()
            except Exception as e:
                print(e)
                connectionLabel.notConnected()
        else:
            connectionLabel.notConnected()


class connectLayout(BoxLayout):
    """layout with COM spinner, connect button and connection label"""
    def __init__(self, **kwargs):
        super(connectLayout, self).__init__(**kwargs)        
        global portList, COMSpinner, connectionLabel
        COMSpinner = Spinner(text="COM ports", values = portList, text_autoupdate = True)
        self.add_widget(COMSpinner)
        reconnectBtn = Button(text="RECONNECT")
        reconnectBtn.bind(on_press=tryReconnect)
        self.add_widget(reconnectBtn)
        connectionLabel = labelConnection(text = "SERIAL CONNECTION")
        self.add_widget(connectionLabel)

class labelConnection(Label):
    """redefines label for connection with connected and not connected states, with corresponding colors"""
    def on_size(self, *args):
        self.canvas.before.clear()
        with self.canvas.before:
            Color(1, 0, 0, 0.8) #background color
            Rectangle(pos=self.pos, size=self.size) ##background
            Color(0,0,0,1) #border color
            Line(width= 1.1, rectangle = (self.x, self.y, self.width, self.height)) #adds a border
        self.bold = True
        self.italic = True
        self.font_size='25sp'

    def connected(self):
        with self.canvas.before:
            Color(0, 0.8, 0, 1) #background color
            Rectangle(pos=self.pos, size=self.size) ##background
            Color(0,0,0,1) #border color
            Line(width= 1.1, rectangle = (self.x, self.y, self.width, self.height)) #adds a border
        self.text = "SERIAL CONNECTED"

    def notConnected(self):
        with self.canvas.before:
            Color(1, 0, 0, 1) #background color
            Rectangle(pos=self.pos, size=self.size) ##background
            Color(0,0,0,1) #border color
            Line(width= 1.1, rectangle = (self.x, self.y, self.width, self.height)) #adds a border
        self.text = "DISCONNECTED"

def tryReconnect(instance):
    global COMSpinner, COM, ser
    port = COMSpinner.text
    COM = port #updates current COM port from the spinner choice
    ### try to connect
    try:
        ser = serial.Serial(port, 19200, timeout = 0.1)
        if(ser.is_open):
            print("connected")
        else:
            print("Serial not connected")
            try:
                ser.close()
            except:
                pass
    except Exception as e:
        print(e)
        ser = None

class slaveLayout(BoxLayout):
    """Layout that places slave layout side by side"""
    def __init__(self, **kwargs):
        super(slaveLayout, self).__init__(**kwargs)

        #Slave1
        slave1 = LEDLayout()
        slave1.add_widget(labelSlave(text = "Slave \"a\" LEDs", size_hint = (1,0.5)))
        slave1.addLEDsButtons("Slave 1 ", "b")
        self.add_widget(slave1)

        #Slave2
        slave2 = LEDLayout()
        slave2.add_widget(labelSlave(text = "Slave \"b\" LEDs", size_hint = (1,0.5)))
        slave2.addLEDsButtons("Slave 2 ", "c")
        self.add_widget(slave2)

class labelSlave(Label):
    def on_size(self, *args):
        self.canvas.before.clear()
        with self.canvas.before:
            Color(51/255, 204/255, 204/255, 1) #background color
            Rectangle(pos=self.pos, size=self.size) ##background
            Color(0,0,0,1) #border color
            Line(width= 1.1, rectangle = (self.x, self.y, self.width, self.height)) #adds a border
        self.bold = True
        self.italic = True
        self.font_size='25sp'       

class LEDLayout(BoxLayout):
    """Layout with LED buttons"""
    def __init__(self, **kwargs):
        super(LEDLayout, self).__init__(**kwargs)
        self.orientation = 'vertical'

    def addLEDsButtons(self, slaveID, customSlaveName):
        btn1 = LEDButtons(text=(slaveID + 'LED 1'))
        btn1.custom = customSlaveName+"1" #used in callback to get slave adress
        btn1.bind(on_press=ledButtonCallback)
        btn2 = LEDButtons(text=(slaveID + 'LED 2'))
        btn2.custom = customSlaveName+"2"
        btn2.bind(on_press=ledButtonCallback)
        btn3 = LEDButtons(text=(slaveID + 'LED 3'))
        btn3.custom = customSlaveName+"3"
        btn3.bind(on_press=ledButtonCallback)
        self.add_widget(btn1)
        self.add_widget(btn2)
        self.add_widget(btn3)

class LEDButtons(Button):
    """redefines the buttons for LEDs with different colors"""
    def on_size(self, *args):
        self.background_color = (0.75,0.75,0.75,1)
    def on_press(self):
        self.background_color = (0.0, 0.8, 0.0, 1.0)
    def on_release(self):
        self.background_color = (0.75,0.75,0.75,1)

def ledButtonCallback(instance):
    btnID = instance.text
    LEDID = instance.custom
    print('The button <%s> is being pressed' % btnID)
    newValue = (not LEDsStates[LEDDict[LEDID]])
    LEDsStates[LEDDict[LEDID]] = newValue
    cmdLED(LEDID, str(int(newValue)))

class BlinkButton(Button):
    """redefines blink button with different color and blink function binding on press"""
    def on_size(self, *args):
        self.background_color = (0.75,0.75,0.75,0.5)
    def on_press(self):
        self.background_color = (0.2, 0.2, 0.6, 0.8)
        blinkLEDs()
    def on_release(self):
        self.background_color = (0.75,0.75,0.75,0.5)



class LEDInterfaceApp(App):
    def build(self):
        app = AppLayout
        Clock.schedule_interval(app.checkPortPresence, 1.0 / 15.0) #15Hz refresh of connected state
        return app()

    def close_application(self):
        # closing application
        App.get_running_app().stop()
        # removing window
        Window.close()
        if(ser):
            ser.close()

if __name__ == '__main__':
    LEDInterfaceApp().run()

My design files

My Design files


Last update: June 19, 2021 13:50:28