BeaconAir – Track your Pi
iBeacon Software
The iBeacons are problematic. They aren't very accurate, and lots of different environmental factors affect the RSSI (received power). If your BeaconAir sensitivities set high, you can sit in one place and watch the lights grow brighter and dimmer as the received signals vary: It's kind of a visual map of the electromagnetic spectrum. Setting your brightness sensitivities lower and the light range higher clears up this problem.
Two functions used by BeaconAir determine position (Listing 5). First is the calculation of distance from RSSI with a smoothing function on the received values to reduce jitter (lines 1-8). Distance is already scaled in meters. The second key piece is the position calculation using trilateration (lines 10-39), a method of determining the position of a point given the distance to three control points.
Listing 5
iBeacon Positions
01 def calculateDistanceWithRSSI(rssi,beaconnumber): 02 03 beacon = conf.BeaconList[beaconnumber]; 04 txPower = beacon[7] 05 ratio_db = txPower - rssi; 06 ratio_linear = pow(10, ratio_db / 10); 07 r = pow(ratio_linear, .5); 08 return r 09 10 def getXYFrom3Beacons(beaconnumbera, beaconnumberb, beaconnumberc, rollingRSSIArray): 11 12 beacona = conf.BeaconList[beaconnumbera]; 13 beaconb = conf.BeaconList[beaconnumberb]; 14 beaconc = conf.BeaconList[beaconnumberc]; 15 xa = float(beacona[2]) 16 ya = float(beacona[3]) 17 xb = float(beaconb[2]) 18 yb = float(beaconb[3]) 19 xc = float(beaconc[2]) 20 yc = float(beaconc[3]) 21 22 ra = float(calculateDistanceWithRSSI(rollingRSSIArray[beaconnumbera], beaconnumbera )) 23 rb = float(calculateDistanceWithRSSI(rollingRSSIArray[beaconnumberb], beaconnumberb )) 24 rc = float(calculateDistanceWithRSSI(rollingRSSIArray[beaconnumberc], beaconnumberc )) 25 26 S = (pow(xc, 2.) - pow(xb, 2.) + pow(yc, 2.) - pow(yb, 2.) + pow(rb, 2.) - pow(rc, 2.)) / 2.0 27 T = (pow(xa, 2.) - pow(xb, 2.) + pow(ya, 2.) - pow(yb, 2.) + pow(rb, 2.) - pow(ra, 2.)) / 2.0 28 29 30 try: 31 y = ((T * (xb - xc)) - (S * (xb - xa))) / (((ya - yb) * (xb - xc)) - ((yc - yb) * (xb - xa))) 32 x = ((y * (ya - yb)) - T) / (xb - xa) 33 34 except ZeroDivisionError as detail: 35 print 'Handling run-time error:', detail 36 return [-1,-1] 37 38 point = [x, y] 39 return point
BeaconAir Control Panel
The BeaconAir control panel is built with the RasPiConnect app [12], which lets you build elaborate control panels on iPhones and iPads with almost no coding and, in particular, no coding on the mobile device itself. The response is good, especially on a local network, and I get to use lots of fun and colorful buttons and controls (see Figure 9).
The right side of the control panel has the HTML map and controls for the lights. The Remote Webview HTML control has already been discussed. The Green logging box is a Bubble Talk control that can be set up to read periodically from the server and write out logging information to the control panel. The code is contained in bubblelog.py
in the BeaconAir
directory.
In the example, you can see the close beacons drifting slightly, changing the ranking of which beacon is closest. Finally, two lights are turned on, as shown in the house map. Below the logging box is a graph showing how many beacons are read. The example graph shows the Rasp Pi going in and out of range. The controls on the left side are used to set the distance in meters at which to turn the lights on, and the second control sets the brightness of the light.
For example, if you set the brightness sensitivity to 1 meter, it will start getting bright at 1 meter and grow brighter as you get closer. It would be easy to modify the software to change the colors of the Philips Hue bulbs according to distance or time of day. The graph on the bottom left is a Dynamic SparkLine control (event driven) set to advance every time the jitter value changes. You could also set it to a timed event, which means it advances all the time and just adds new values as they come in from BeaconAir.
The code for all buttons is quite similar. Pushing a button on the iPad sends an HTML XML packet to the Rasp Pi software, which writes to a command file; BeaconAir then picks it up and executes the requested functions. For the All Lights On button (FB-2
, a Feedback Button), the Local.py
code (in RasPiConnectServer
) is shown in Listing 6.
Listing 6
Local.py
01 # FB-2 - turns lights on and off 02 if (objectServerID == "FB-2"): 03 #check for validate request 04 # validate allows RasPiConnect to verify this object is here 05 if (validate == "YES"): 06 outgoingXMLData += Validate.buildValidateResponse("YES") 07 outgoingXMLData += BuildResponse.buildFooter() 08 return outgoingXMLData 09 10 # not validate request, so execute 11 12 responseData = "XXX" 13 14 15 if (objectName is None): 16 objectName = "XXX" 17 18 lowername = objectName.lower() 19 20 21 if (lowername == "all lights on"): 22 23 status = sendCommandToBeaconAirAndWait("ALLLIGHTSON") 24 responseData = "all lights off" 25 responseData = responseData.title() 26 27 28 elif (lowername == "all lights off"): 29 30 status = sendCommandToBeaconAirAndWait("ALLLIGHTSOFF") 31 responseData = "all lights on" 32 responseData = responseData.title() 33 34 35 # defaults to on 36 else: 37 status = sendCommandToBeaconAirAndWait("ALLLIGHTSON") 38 lowername = "all lights off" 39 responseData = lowername.title() 40 41 42 outgoingXMLData += BuildResponse.buildResponse(responseData) 43 outgoingXMLData += BuildResponse.buildFooter() 44 return outgoingXMLData
When the button value ("all lights off") comes, the code validates it, sends a command to BeaconAir, toggles the value ("all lights on"), and sends it back to the RasPiConnect App, setting up the next button push (which will next turn all the lights on). The rest of the code is boilerplate, building the RasPiConnect XML request and handling an error condition that sometimes happens (e.g., the button goes blank).
The BeaconAir code to handle the command file request is simple:
if (command == "ALLLIGHTSON"): lights.allLights(True, currentLightState ) completeCommand() return True ** if (command == "ALLLIGHTSOFF"): lights.allLights(False, currentLightState) completeCommand() return True
All of the controls follow the same design pattern, although the graphing controls are a bit more complicated. To follow any command through the system, you need to figure out the control ID you are looking for (FB-2
in this example), track it through Local.py
, and then see what the command does in BeaconAir.py
.
In some cases, such as the graphs, BeaconAir is building the graph data and writing it to a file; then, RasPiConnectServer reads it in Local.py
. In the Remote Webview (the house map), W-10
is the object ID, which is one of the controls that reads in a file generated by BeaconAir.py
(see the software in webmap.py
[6]). The RasPiConnect XML configuration file for BeaconAir is on GitHub [13].
The last thing to note is how to build the cool BeaconAir background on the control panel screen. The trick is to build a JPEG or PNG file on any graphical app (such as, Grafio or GIMP) and add it to the iPad photo library.
Now you just have to select the control panel background picture for the BeaconAir page in RasPiConnect to get an instant, professional-looking background.
Buy this article as PDF
(incl. VAT)