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:
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 !
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.
importserialimportsysimporttimeLEDs=['b1','b2','b3','c1','c2','c3']DLY=0.1ser=serial.Serial('COM7',19200,timeout=0)if(ser.is_open):print("connected")else:print("Serial not connected")try:ser.close()except:passsys.exit()foriinrange(len(LEDs)):cmd=(LEDs[i]+"0")print(cmd)ser.write(cmd.encode())line=ser.readline()print(line)ser.flush()time.sleep(DLY)foriinrange(len(LEDs)):cmd=(LEDs[i]+"1")print(cmd)ser.write(cmd.encode())line=ser.readline()print(line)ser.flush()time.sleep(DLY)ser.close()
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
Now when I close the window or the terminal, everything closes correctly.
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, …
The video presents artifacts but they are due to encoding issues.
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.
But I didn’t know which button was pressed really easily by using the buttons object attributes.
1234567
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)
Finally, we can do that for all the buttons to detect which one was pressed.
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.
# #!/usr/bin/env python3fromkivy.appimportAppfromkivy.uix.buttonimportButtonfromkivy.uix.labelimportLabelfromkivy.uix.boxlayoutimportBoxLayoutfromkivy.graphicsimportColor,Rectangle,Linefromkivy.core.windowimportWindowfromkivy.clockimportClockfromkivy.uix.spinnerimportSpinner### serial USB connectionimportserialimportserial.tools.list_portsimporttimeCOM="COM7"#Default COM portLEDs=['b1','b2','b3','c1','c2','c3']#LEDs listLEDsStates=[0,0,0,0,0,0]# Current LED statesLEDDict={#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)defcmdLED(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 buffertime.sleep(DLY)defblinkLEDs(instance=None):"""Blink all LEDs in sequence"""foriinrange(len(LEDs)):cmdLED(LEDs[i],'1')foriinrange(len(LEDs)):cmdLED(LEDs[i],'0')defscanPorts():"""scan serial ports to find available ones"""ports=serial.tools.list_ports.comports()portList=[]forpinports:portList.append(str(p.device))returnportListconnectionLabel=NoneCOMSpinner=Noneser=NoneportList=scanPorts()### Connectiontry:ser=serial.Serial(COM,19200,timeout=0.1)if(ser.is_open):print("connected")else:print("Serial not connected")try:ser.close()exceptExceptionase:print(e)exceptExceptionase:print(e)ser=None# sys.exit()if(ser):#Initialization of LEDs: all offforiinrange(len(LEDs)):cmdLED(LEDs[i],'0')LEDsStates[i]=0blinkLEDs()#then blink themclassAppLayout(BoxLayout):"""main layout"""def__init__(self,**kwargs):super(AppLayout,self).__init__(**kwargs)self.orientation='vertical'### Connection InfoconnectLyt=connectLayout(size_hint=(1,0.2))self.add_widget(connectLyt)### blink buttonblinkBtn=BlinkButton(text="Blink LEDs",size_hint=(1,0.2))self.add_widget(blinkBtn)### LEDs and slaves layoutslaveLyt=slaveLayout()self.add_widget(slaveLyt)defcheckPortPresence(self):"""runs at 15Hz, scan ports and detects (dis)connections"""globalCOM,seravailablePorts=scanPorts()COMSpinner.values=availablePortsif(COMnotinavailablePorts):print("Device disconnected")connectionLabel.notConnected()ser=Noneelse:connectionLabel.connected()if(ser):try:if(ser.is_open):connectionLabel.connected()else:connectionLabel.notConnected()exceptExceptionase:print(e)connectionLabel.notConnected()else:connectionLabel.notConnected()classconnectLayout(BoxLayout):"""layout with COM spinner, connect button and connection label"""def__init__(self,**kwargs):super(connectLayout,self).__init__(**kwargs)globalportList,COMSpinner,connectionLabelCOMSpinner=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)classlabelConnection(Label):"""redefines label for connection with connected and not connected states, with corresponding colors"""defon_size(self,*args):self.canvas.before.clear()withself.canvas.before:Color(1,0,0,0.8)#background colorRectangle(pos=self.pos,size=self.size)##backgroundColor(0,0,0,1)#border colorLine(width=1.1,rectangle=(self.x,self.y,self.width,self.height))#adds a borderself.bold=Trueself.italic=Trueself.font_size='25sp'defconnected(self):withself.canvas.before:Color(0,0.8,0,1)#background colorRectangle(pos=self.pos,size=self.size)##backgroundColor(0,0,0,1)#border colorLine(width=1.1,rectangle=(self.x,self.y,self.width,self.height))#adds a borderself.text="SERIAL CONNECTED"defnotConnected(self):withself.canvas.before:Color(1,0,0,1)#background colorRectangle(pos=self.pos,size=self.size)##backgroundColor(0,0,0,1)#border colorLine(width=1.1,rectangle=(self.x,self.y,self.width,self.height))#adds a borderself.text="DISCONNECTED"deftryReconnect(instance):globalCOMSpinner,COM,serport=COMSpinner.textCOM=port#updates current COM port from the spinner choice### try to connecttry:ser=serial.Serial(port,19200,timeout=0.1)if(ser.is_open):print("connected")else:print("Serial not connected")try:ser.close()except:passexceptExceptionase:print(e)ser=NoneclassslaveLayout(BoxLayout):"""Layout that places slave layout side by side"""def__init__(self,**kwargs):super(slaveLayout,self).__init__(**kwargs)#Slave1slave1=LEDLayout()slave1.add_widget(labelSlave(text="Slave \"a\" LEDs",size_hint=(1,0.5)))slave1.addLEDsButtons("Slave 1 ","b")self.add_widget(slave1)#Slave2slave2=LEDLayout()slave2.add_widget(labelSlave(text="Slave \"b\" LEDs",size_hint=(1,0.5)))slave2.addLEDsButtons("Slave 2 ","c")self.add_widget(slave2)classlabelSlave(Label):defon_size(self,*args):self.canvas.before.clear()withself.canvas.before:Color(51/255,204/255,204/255,1)#background colorRectangle(pos=self.pos,size=self.size)##backgroundColor(0,0,0,1)#border colorLine(width=1.1,rectangle=(self.x,self.y,self.width,self.height))#adds a borderself.bold=Trueself.italic=Trueself.font_size='25sp'classLEDLayout(BoxLayout):"""Layout with LED buttons"""def__init__(self,**kwargs):super(LEDLayout,self).__init__(**kwargs)self.orientation='vertical'defaddLEDsButtons(self,slaveID,customSlaveName):btn1=LEDButtons(text=(slaveID+'LED 1'))btn1.custom=customSlaveName+"1"#used in callback to get slave adressbtn1.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)classLEDButtons(Button):"""redefines the buttons for LEDs with different colors"""defon_size(self,*args):self.background_color=(0.75,0.75,0.75,1)defon_press(self):self.background_color=(0.0,0.8,0.0,1.0)defon_release(self):self.background_color=(0.75,0.75,0.75,1)defledButtonCallback(instance):btnID=instance.textLEDID=instance.customprint('The button <%s> is being pressed'%btnID)newValue=(notLEDsStates[LEDDict[LEDID]])LEDsStates[LEDDict[LEDID]]=newValuecmdLED(LEDID,str(int(newValue)))classBlinkButton(Button):"""redefines blink button with different color and blink function binding on press"""defon_size(self,*args):self.background_color=(0.75,0.75,0.75,0.5)defon_press(self):self.background_color=(0.2,0.2,0.6,0.8)blinkLEDs()defon_release(self):self.background_color=(0.75,0.75,0.75,0.5)classLEDInterfaceApp(App):defbuild(self):app=AppLayoutClock.schedule_interval(app.checkPortPresence,1.0/15.0)#15Hz refresh of connected statereturnapp()defclose_application(self):# closing applicationApp.get_running_app().stop()# removing windowWindow.close()if(ser):ser.close()if__name__=='__main__':LEDInterfaceApp().run()