I'm really excited to show the completed project to my fellow readers. For the wrap-up post I made a GUI with Python and Tkinter to collect and display all the beacons data. In the video, two beacons are connected providing Temperature, Humidity and Battery Level feedback regularly.
The Python program that reads all the sensor's data uses the functionality which I developed and explained in a previous blog post.
To capture the events in the video, I modified the RSL10 firmware (file CS_Platform_RSL10_HB.c) in order to turn ON the built-in RGB LED while the communication is in progress (Request and Response) which can be appreciated in few parts of the video.
static void CS_PlatformReadHandler(struct BLE_ICS_RxIndData *ind) { cs_request = ind; LED_On(0); // Blue ON LED_On(1); // Green ON //LED_On(2); // Red ON CS_Loop(0); LED_Off(0); // Blue OFF LED_Off(1); // Green OFF //LED_Off(2); // Red OFF cs_request = NULL; }
GUI Features
The GUI is designed to display the current date and time which are constantly being updated. It has a working power button which closes the application and room for 3 widgets (one beacon each) with some level of customization.
For each widget, the Beacon name or is displayed at the top, temperature, humidity and Battery Level are displayed and refreshed regularly. The Battery Level is displayed as a progress bar which changes color from green (good), yellow (medium) and red (low battery).
When tapping the Temperature Scale ºF or ºC it will automatically convert the temperature between Fahrenheit and Celsius (seen also in the video).
Source Code
The GUI can be launched remotely and will provide text feedback on the events that are happening in real time. To launch the GUI on the Pi's main screen, run the following before running the Python script remotely:
export DISPLAY=:0.0
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Luis Ortiz - luislab.com # March 8, 2020 from bluepy import btle import binascii import subprocess import re import sys from textcolor import textColor import time from datetime import datetime import tkinter as tk import tkinter.font import random class console_log: def __init__(self, debug = False, ansi = False): self.debug = debug self.ansi = ansi def Sys_Info(self, text:str): if self.debug: if not self.ansi: text = re.sub('(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]', '', text); print (text) def elapsed_time(elapsed): s, ms = divmod(elapsed, 1000) m, s = divmod(s, 60) h, m = divmod(m, 60) time_str = "" if (h > 0): time_str = "%dh " % h if (m > 0): time_str += "%dm " % m if (s > 0): time_str += "%ds " % s if (ms > 0): time_str += "%dms " % ms return time_str.strip() class beacon: def __init__(self, macAddr: str): self.mac = macAddr self.minOrd = ord('0') self.maxOrd = ord('~') self.token_limit = (self.maxOrd - self.minOrd) + 1 # CS.c if (request.token[0] < '0' || request.token[0] > '~') self.rToken = 0 # Request token self.color = textColor() self.cl = console_log(True, True) color = self.color cl = self.cl cl.Sys_Info ("\nConnecting...") self.p = btle.Peripheral(macAddr, btle.ADDR_TYPE_PUBLIC) cl.Sys_Info ("\nConnected [" + color.green(self.p.addr) + "]") for gs in self.p.getServices(): for gc in gs.getCharacteristics(): if gc.supportsRead() and gc.uuid.getCommonName() == str(gc.uuid) and 'NOTIFY' in gc.propertiesToString(): self.rxC = gc cl.Sys_Info('__init__ rxC %s' % self.rxC) elif gc.uuid.getCommonName() == str(gc.uuid) and 'WRITE' in gc.propertiesToString(): self.txC = gc cl.Sys_Info('__init__ txC %s' % self.txC) elif gc.uuid == btle.AssignedNumbers.batteryLevel and 'NOTIFY' in gc.propertiesToString(): self.battC = gc cl.Sys_Info('__init__ battC %s' % self.battC) def disconnect(self): color = self.color cl = self.cl self.p.disconnect() cl.Sys_Info ("\nBeacon [" + color.green(self.p.addr) + "] disconnected.") def getBatteryLevel(self): color = self.color cl = self.cl try: bValue = self.battC.read() sValue = binascii.b2a_hex(bValue).decode('utf-8') cl.Sys_Info(("Battery Level: " + color.cyan("0x%s") + \ " (" + color.yellow("%d%%") + ")") \ % (sValue, int(sValue, 16))) return sValue except btle.BTLEException as exc: cl.Sys_Info(exc) return None def readNode(self, node:str, prop:str): color = self.color cl = self.cl try: self.rToken = (self.rToken + 1) % self.token_limit cl.Sys_Info("\nrToken %d" % self.rToken) sReq = chr(self.rToken + self.minOrd) + '/' + node + '/' + prop bReq = sReq.encode('utf-8') cl.Sys_Info("Requested %s" % color.cyan('\'' + sReq + '\'')) startTime = time.time() self.txC.write(bReq, withResponse = True) success = self.p.waitForNotifications(2) endTime = time.time() if not success: cl.Sys_Info("Request timeout.") else: cl.Sys_Info("Notification received in %.2f seconds" % (endTime - startTime)) bReq = self.rxC.read() sReq = bReq.decode('utf-8') cl.Sys_Info ("Read sReq %s" % color.pink(sReq)) result = re.match('(.{1}\/)(.?)(\/)([0-9\.]+$)', sReq) if result: vType = result.group(2) value = result.group(4) return vType, value else: cl.Sys_Info ("No \"re\" match") return None, None except ValueError: return None, None except btle.BTLEException as exc: cl.Sys_Info (exc) return None, None def getNodeValue(self, node:str, prop:str=''): # node (sensor), property color = self.color cl = self.cl if node == 'BL': # Battery Level batteryLevel = int(self.getBatteryLevel(), 16) return batteryLevel elif node == 'EV': # Environmental sensor (Temp, Humidity, Raw Pressure, Indoor Air Quality) if prop in ('T', 'TF', 'H', 'PP', 'P', 'A'): # Temp C valueType, nodeStrValue = self.readNode(node, prop) if nodeStrValue.replace('.', '1').isdigit(): nodeValue = float(nodeStrValue) if prop in ('T'): tempC = nodeValue cl.Sys_Info (("valueType: %s, temp: " + color.yellow("%2.1f°C")) \ % (color.purple("\'" + valueType + "\'"), tempC)) return tempC elif prop in ('TF'): tempF = nodeValue cl.Sys_Info (("valueType: %s, temp: " + color.yellow("%2.1f°F")) \ % (color.purple("\'" + valueType + "\'"), tempF)) return tempF elif prop in ('H'): humidity = nodeValue cl.Sys_Info (("valueType: %s, humidity: " + color.yellow("%2.f%%")) \ % (color.purple("\'" + valueType + "\'"), humidity)) return humidity else: cl.Sys_Info (("valueType: %s, value: " + color.yellow("%2.2f") + ", property: %s") \ % (color.purple("\'" + valueType + "\'"), nodeValue, color.cyan(prop))) return nodeValue else: cl.Sys_Info ("valueType: %s, nodeValue %s" \ % (color.purple("\'" + valueType + "\'"), color.purple("\'" + nodeStrValue + "\'"))) return nodeStrValue elif node == 'AL': # Light Sensor if prop in ('L'): # Lux valueType, nodeStrValue = self.readNode(node, prop) if nodeStrValue.replace('.', '1').isdigit(): lux = float(nodeStrValue) cl.Sys_Info (("valueType: %s, illuminance: " + color.yellow("%2.2f lux")) % (color.purple("\'" + valueType + "\'"), lux)) return lux else: cl.Sys_Info ("valueType: %s, nodeValue %s" % (color.purple("\'" + valueType + "\'"), color.purple("\'" + nodeStrValue + "\'"))) return nodeStrValue class beaconCSV: def __init__(self, fileName): self.fileName = fileName try: with open(self.fileName, mode='xt', encoding='utf-8') as f: f.write("\"Date\",\"TempC\",\"TempF\",\"Humidity\"\n") except FileExistsError: pass def write(self, tempC, tempF, humidity): dt_string = datetime.now().strftime('%Y%m%d %H:%M:%S') fileStr = "\"" + dt_string + "\",%2.1f" % tempC + ",%2.1f" % tempF + ",%2.f" % humidity cl.Sys_Info(fileStr) with open(self.fileName, mode='at', encoding='utf-8') as f: f.write(fileStr + "\n") class Application(tk.Tk): def refreshDateTime(self): self.date_str.set(datetime.now().strftime('%a, %b %-d')) self.time_str.set(datetime.now().strftime('%-I:%M %p')) def closeApp(self): self.destroy() for b in self.beacons: if b[0]: b[0].disconnect() def __init__(self, width=800, height=480): tk.Tk.__init__(self) self.title('On Sweet Home') self.gui_fg='white' # Foreground color self.gui_bg='black' # Background color self.gui_width=width self.gui_height=height self.geometry(str(self.gui_width) + "x" + str(self.gui_height)) self.resizable(0, 0) self.configure(background=self.gui_bg, cursor="none") self.title("On Sweet Home") self.overrideredirect(True) self.backgroundImage = tk.PhotoImage(file = './images/hub.png') self.backgroundLabel = tk.Label(self, image=self.backgroundImage).place(x=0, y=0, relwidth=1, relheight=1) self.beacons = [] self.beaconsLen = 0 self.refreshLoop = 0 self.createWidgets() def createWidgets(self): self.widg_bg='#333333' # gray20 self.iconExit = tk.PhotoImage(file = './icons/power.png') self.buttonExit = tk.Button(self, image=self.iconExit, fg=self.gui_bg, bg=self.gui_bg, highlightthickness=0, bd=0, \ activebackground=self.gui_bg, command=self.closeApp) self.buttonExit.place(x=(self.gui_width - 40), y=8) self.date_str = tk.StringVar() self.time_str = tk.StringVar() self.refreshDateTime() self.dateLabel = tk.Label(self, textvariable=self.date_str, font=("Quicksand", 22), fg="gray60", bg=self.gui_bg).place(x=600, y=95, anchor="n") self.timeLabel = tk.Label(self, textvariable=self.time_str, font="Quicksand 52 bold", fg=self.gui_fg, bg=self.gui_bg).place(x=600, y=140, anchor="n") self.acTemp = tk.Label(self, text="72", font="Quicksand 90 normal", padx=0, pady=0, fg=self.gui_fg, bg=self.widg_bg, bd=0).place(x=275, y=140, anchor="ne") self.acTempUn = tk.Label(self, text="°F", font="Quicksand 30 normal", fg=self.gui_fg, bg=self.widg_bg, borderwidth=0).place(x=275, y=165, anchor="nw") self.acMode1 = tk.Label(self, text="COOL", font="Arial 10 normal", fg="gray10", bg=self.widg_bg, borderwidth=0).place(x=85, y=205, anchor="w") self.acMode2 = tk.Label(self, text="HEAT", font="Arial 10 normal", fg=self.gui_fg, bg=self.widg_bg, borderwidth=0).place(x=85, y=225, anchor="w") self.acMode3 = tk.Label(self, text="OFF", font="Arial 10 normal", fg="gray10", bg=self.widg_bg, borderwidth=0).place(x=85, y=245, anchor="w") self.iconBattery = tk.PhotoImage(file = './icons/battery.png') def getBeaconData(self, i): if self.beacons[i][0]: # If there is a Beacon available, read the sensor's data print (("\nRequesting Beacon_%d data...") % (i)) self.beacons[i][1] = int(self.beacons[i][0].getNodeValue('EV', 'TF')) self.beacons[i][2] = 'F' self.beacons[i][5] = int(self.beacons[i][0].getNodeValue('EV', 'H')) self.beacons[i][7] = self.beacons[i][0].getNodeValue('BL') / 100 self.refreshWidget(i) def refreshWidget(self, i): temp = self.beacons[i][1] # Temp is always stored in F tempScale = self.beacons[i][2] humidity = self.beacons[i][5] battLevel = self.beacons[i][7] posX = self.beacons[i][9][0] posY = self.beacons[i][9][1] canvW = battLevel * 20 # Canvas width represents Battery Level if battLevel <= 0.3: # Canvas color according to the battery level canvBG="#cc0000" elif battLevel <= 0.65: canvBG="#aaaa00" else: canvBG="#009900" if tempScale == 'C': tempC = int((self.beacons[i][1] - 32) / 1.8) self.beacons[i][3].set(str(tempC)) # Temperature C else: self.beacons[i][3].set(str(self.beacons[i][1])) # Temperature F self.beacons[i][4].set("°" + tempScale) # Temperature Scale (C, F) self.beacons[i][6].set(str(humidity) + "%") # Humidity self.beacons[i][8].place(x=posX+240-35+(20-canvW), y=posY+13) # Battery level bar self.beacons[i][8].config(bg=canvBG, width=canvW) def changeTempScale(self, i): if self.beacons[i][2] == 'F': self.beacons[i][2] = 'C' else: self.beacons[i][2] = 'F' print("Changed temperature scale to °%s, Widget[%d]" % (self.beacons[i][2], i)) self.refreshWidget(i) def addWidget(self, mac, name, color, posX, posY): canvW=20 widgName = tk.Label(self, text=name, font=("Arial 12"), fg="gray60", bg=self.widg_bg) widgName.place(x=posX+105, y=posY+15, anchor="center") batteryIcon = tk.Label(self, image=self.iconBattery, fg=self.widg_bg, bg=self.widg_bg) batteryIcon.place(x=posX+240-40, y=posY+10) canvBatt = tk.Canvas(self, width=canvW, height=10, bg=self.gui_bg, highlightthickness=0) tempStr = tk.StringVar() # Temperature tempScl = tk.StringVar() # Temperature Scale humdStr = tk.StringVar() # Humidity # Initialization Values self.beacons.append([None, \ 0, \ "F", \ tempStr, \ tempScl, \ 35, \ humdStr, \ 0, \ canvBatt, \ [posX, posY]]) self.beaconsLen += 1 i = self.beaconsLen - 1 if mac: # Add BTLE beacon (RSL10-SENSE-GEVK) self.beacons[i][0] = beacon(mac) self.getBeaconData(i) else: # If no beacon available, make random data available self.beacons[i][1] = random.randrange(68, 74) self.beacons[i][5] = random.randrange(10, 25) self.beacons[i][7] = random.random() widgHumidity = tk.Button(self, textvariable=self.beacons[i][6], font=("Quicksand", 24), fg=color, bg=self.widg_bg, \ activebackground=self.widg_bg, highlightthickness=0, bd=0, command = None) widgHumidity.place(x=posX+150, y=posY+130, anchor="sw") widgTemp = tk.Button(self, textvariable=self.beacons[i][3], font=("Quicksand", 62), fg=color, bg=self.widg_bg, \ activebackground=self.widg_bg, highlightthickness=0, bd=0, command = None) widgTemp.place(x=posX+123, y=posY+30, anchor="ne") widgTempScale = tk.Button(self, textvariable=self.beacons[i][4], font=("Quicksand", 21), fg=color, bg=self.widg_bg, \ activeforeground=color, activebackground=self.widg_bg, highlightthickness=0, bd=0, height=1, width=1, command=lambda: self.changeTempScale(i)) widgTempScale.place(x=posX+110, y=posY+47, anchor="nw") def refresh(self): self.refreshDateTime() if self.refreshLoop >= 2: self.getBeaconData(0) self.getBeaconData(1) self.getBeaconData(2) self.refreshLoop = 0 else: self.refreshLoop += 1 self.refreshWidget(2) self.after(5000, self.refresh) cl = console_log(True, True) app = Application(800, 480) app.addWidget(mac='60:C0:BF:29:EF:50', name='Living room', color="#66ccff", posX=20, posY=310) app.addWidget(mac=None, name='Bedroom', color="#ff9900", posX=280, posY=310) app.addWidget(mac=None, name='Bedroom', color="#cc66ff", posX=540, posY=310) app.after(0, app.refresh) app.mainloop() print("Bye.")
{gallery:width=648,height=432,autoplay=false} ON Sweet Home |
---|
Enclosure: Enclosure with wall mount for the RSL10-Sense-Gevk |
Enclosure: Enclosure Top Cap (RSL10-Sense-Gevk) |
Enclosure: Enclosure Top Cap - Rear (RSL10-Sense-Gevk) |
If you made it this far, thanks for reading. I really hope all the resources of this project are useful in any way to you. A big thanks to Element14 for sponsoring my project and all the community members who showed up in any way.
Luis
Blogs in this series
- ON Sweet Home - Introduction
- ON Sweet Home - Getting to know your RSL10
- ON Sweet Home - Collecting the Beacon data
- ON Sweet Home - 3D printed parts
- ON Sweet Home - GUI is alive and the project is complete!
Top Comments