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.

CS_Platform_RSL10_HB cyan LED

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).

ON Sweet home GUI - Think ON Design Challenge

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

RSL10-SENSE-DB-GEVK Enclosure

Enclosure: Enclosure with wall mount for the RSL10-Sense-Gevk

RSL10-SENSE-GEVK Enclosure (Front Cap)

Enclosure: Enclosure Top Cap (RSL10-Sense-Gevk)

RSL10-SENSE-DB-GEVK Enclosure (Top Cap)

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

  1. ON Sweet Home - Introduction
  2. ON Sweet Home - Getting to know your RSL10
  3. ON Sweet Home - Collecting the Beacon data
  4. ON Sweet Home - 3D printed parts
  5. ON Sweet Home - GUI is alive and the project is complete!