Last active
July 21, 2021 14:31
-
-
Save tito/c45b6ce51db79f9eba8a to your computer and use it in GitHub Desktop.
iBeacon iOS 8 Scanner (python / pyobjus / kivy), using ranging API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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