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