Skip to content

Instantly share code, notes, and snippets.

@tito
Last active July 21, 2021 14:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tito/c45b6ce51db79f9eba8a to your computer and use it in GitHub Desktop.
Save tito/c45b6ce51db79f9eba8a to your computer and use it in GitHub Desktop.
iBeacon iOS 8 Scanner (python / pyobjus / kivy), using ranging API
# coding=utf-8
"""
iBeacon Scanner
===============
This scanner works exclusively on iOS real devices, simulator don't support
iBeacon ranging API at all.
The usage is quite simple:
0. Add CoreLocation framework to your app (should be done by default),
and a key `NSLocationAlwaysUsageDescription` to a string value `My app
want to access to your location`
1. Create a scanner with `scanner = IBeaconScanner()`
2. Register your iBeacon using the ibeacon uuid like:
scanner.register_beacon("E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
3. Monitor the event you want
scanner.bind(on_beacon_update=do_something)
4. Start the scanner
scanner.start_monitoring()
Output example captured from a test run:
(('on_beacon_entered', <__main__.IBeaconScanner object at 0x1704bd238>), {'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -57, 'proximity': 'near', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -54, 'proximity': 'near', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -52, 'proximity': 'immediate', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -52, 'proximity': 'immediate', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -53, 'proximity': 'immediate', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -65, 'proximity': 'immediate', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -74, 'proximity': 'near', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -76, 'proximity': 'near', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -77, 'proximity': 'near', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_update', <__main__.IBeaconScanner object at 0x1704bd238>), {'rssi': -77, 'proximity': 'near', 'major': 4250, 'minor': 9865, 'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
(('on_beacon_leaved', <__main__.IBeaconScanner object at 0x1704bd238>), {'uuid': 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'})
"""
from kivy.event import EventDispatcher
from pyobjus import autoclass, protocol
CLLocationManager = autoclass("CLLocationManager")
CLBeaconRegion = autoclass("CLBeaconRegion")
NSUUID = autoclass("NSUUID")
class IBeaconScanner(EventDispatcher):
"""
iBeacon Scanner class that works exclusively on iOS real device.
"""
PROXIMITY = ["unknown", "immediate", "near", "far"]
__events__ = ("on_beacon_entered", "on_beacon_update", "on_beacon_leaved",
"on_error")
def __init__(self):
super(IBeaconScanner, self).__init__()
self._regions = {}
self._regions_nsuuid = {}
self._regions_seen = []
self._region_activated = False
def start_monitoring(self):
"""Start the scanner monitoring"""
self._clm = CLLocationManager.alloc().init()
self._clm.delegate = self
self._clm.requestAlwaysAuthorization()
def stop_monitoring(self):
"""Stop the scanner monitoring"""
self._deactivate_ibeacons()
self._regions_seen = []
self._clm.delegate = None
del self._clm
def register_beacon(self, uuid, name=None):
"""Register a beacon to be tracked, using the ibeacon `uuid`"""
assert(len(uuid) == 36)
uuid = uuid.upper()
nsuuid = NSUUID.alloc().initWithUUIDString_(uuid)
self._regions[uuid] = CLBeaconRegion.alloc(
).initWithProximityUUID_identifier_(nsuuid, name or uuid)
self._regions_nsuuid[uuid] = nsuuid
def unregister_beacon(self, uuid):
"""Unregister a beacon to be tracked."""
if uuid not in self._regions:
return
self._regions_nsuuid.pop(uuid)
region = self._regions.pop(uuid)
if self._region_activated:
self._clm.stopRangingBeaconsInRegion_(region)
if uuid in self._regions_seen:
self._regions_seen.remove(uuid)
self.dispatch("on_beacon_leaved", uuid=uuid)
def on_beacon_entered(self, uuid):
"""Event fired when a beacon just entered in the sight of the device"""
pass
def on_beacon_leaved(self, uuid):
"""Event fired when a beacon is not in the sight of the device"""
pass
def on_beacon_update(self, uuid, major, minor, proximity, rssi):
"""Event fired when we got information about a beacon"""
pass
def on_error(self, uuid, msg):
"""Event fired when a beacon have an issue / monitoring issues"""
pass
# (implementation internal)
@protocol("CLLocationManagerDelegate")
def locationManager_didChangeAuthorizationStatus_(self, manager, status):
if status == 3: # kCLAuthorizationStatusAuthorized
self._activate_ibeacons()
elif status == 2: # kCLAuthorizationStatusDenied
pass
elif status == 1: # kCLAuthorizationStatusRestricted
pass
else: # kCLAuthorizationStatusNotDetermined
pass
@protocol("CLLocationManagerDelegate")
def locationManager_didRangeBeacons_inRegion_(self, manager, beacons,
region):
uuid = region.proximityUUID.UUIDString().UTF8String()
if uuid not in self._regions:
return
beacon = None
count = beacons.count()
if count:
beacon = beacons.objectAtIndex_(0)
if beacon.rssi == 0:
beacon = None
if beacon:
if uuid not in self._regions_seen:
self._regions_seen.append(uuid)
self.dispatch("on_beacon_entered", uuid=uuid)
self.dispatch("on_beacon_update",
uuid=uuid,
major=beacon.major.integerValue(),
minor=beacon.minor.integerValue(),
proximity=self.PROXIMITY[beacon.proximity],
rssi=beacon.rssi)
else:
if uuid in self._regions_seen:
self._regions_seen.remove(uuid)
self.dispatch("on_beacon_leaved", uuid=uuid)
@protocol("CLLocationManagerDelegate")
def locationManager_rangingBeaconsDidFailForRegion_withError_(
self, manager, region, error
):
uuid = region.proximityUUID.UUIDString().UTF8String()
msg = error.localizedDescription.UTF8String()
self.dispatch("on_error", uuid=uuid, msg=msg)
def _activate_ibeacons(self):
for region in self._regions.values():
self._clm.startRangingBeaconsInRegion_(region)
self._region_activated = True
def _deactivate_ibeacons(self):
for region in self._regions.values():
self._clm.stopRangingBeaconsInRegion_(region)
self._region_activated = False
if __name__ == "__main__":
from kivy.app import App
from kivy.uix.button import Button
def dprint(*args, **kwargs):
print(args, kwargs)
class IbeaconScanner(App):
def build(self):
self._scanner = IBeaconScanner()
self._scanner.register_beacon(
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
from functools import partial
self._scanner.bind(
on_beacon_entered=partial(dprint, "on_beacon_entered"),
on_beacon_update=partial(dprint, "on_beacon_update"),
on_beacon_leaved=partial(dprint, "on_beacon_leaved"),
on_error=partial(dprint, "on_error"))
return Button(text="Start Scanner", on_release=self.start_scanner)
def start_scanner(self, *args):
self._scanner.start_monitoring()
def on_pause(self):
return True
IbeaconScanner().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment