async def getMode(self, address, scanDuration=3) -> CrownstoneOperationMode: """ This will wait until it has received an advertisement from the Crownstone with the specified address. Once it has received an advertisement, it knows the mode. We will return once we know. It can throw the following CrownstoneBleException - BleError.NO_SCANS_RECEIVED We have not received any scans from this Crownstone, and can't say anything about it's state. """ _LOGGER.debug(f"getMode address={address} scanDuration={scanDuration}") checker = ModeChecker(address, None) subscriptionId = BleEventBus.subscribe( BleTopics.rawAdvertisement, lambda scanData: checker.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) result = checker.getResult() if result is None: raise CrownstoneBleException( BleError.NO_SCANS_RECEIVED, f'During the {scanDuration} seconds of scanning, no advertisement was received from this address.' ) return result
async def _getNearest(self, setup, rssiAtLeast, scanDuration, returnFirstAcceptable, validated, addressesToExclude) -> ScanData or None: addressesToExcludeSet = set() if addressesToExclude is not None: for data in addressesToExclude: if hasattr(data, 'address'): addressesToExcludeSet.add(data.address.lower()) elif isinstance(data, dict): if "address" in data: addressesToExcludeSet.add(data["address"].lower()) else: raise CrownstoneException( CrownstoneError.INVALID_ADDRESS, "Addresses to Exclude is either an array of addresses (like 'f7:19:a4:ef:ea:f6') or an array of dicts with the field 'address'" ) else: addressesToExcludeSet.add(data.lower()) selector = NearestSelector(setup, rssiAtLeast, returnFirstAcceptable, addressesToExcludeSet) topic = BleTopics.advertisement if not validated: topic = BleTopics.rawAdvertisement subscriptionId = BleEventBus.subscribe( topic, lambda scanData: selector.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) return selector.getNearest()
async def waitForMode(self, address, requiredMode: CrownstoneOperationMode, scanDuration=5): """ This will wait until it has received an advertisement from the Crownstone with the specified address. Once it has received an advertisement, it knows the mode. We will scan for the scanDuration amount of seconds or until the Crownstone is in the required mode. It can throw the following CrownstoneBleException - BleError.NO_SCANS_RECEIVED We have not received any scans from this Crownstone, and can't say anything about it's state. - BleError.DIFFERENT_MODE_THAN_REQUIRED During the {scanDuration} seconds of scanning, the Crownstone was not in the required mode. """ _LOGGER.debug( f"waitForMode address={address} requiredMode={requiredMode} scanDuration={scanDuration}" ) checker = ModeChecker(address, requiredMode, True) subscriptionId = BleEventBus.subscribe( BleTopics.rawAdvertisement, lambda scanData: checker.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) result = checker.getResult() if result is None: raise CrownstoneBleException( BleError.NO_SCANS_RECEIVED, f'During the {scanDuration} seconds of scanning, no advertisement was received from this address.' ) if result != requiredMode: raise CrownstoneBleException( BleError.DIFFERENT_MODE_THAN_REQUIRED, f'During the {scanDuration} seconds of scanning, the Crownstone was not in the required mode..' )
async def isCrownstoneInSetupMode(self, address: str, scanDuration=3, waitUntilInSetupMode=False) -> bool: _LOGGER.warning( "isCrownstoneInSetupMode is deprecated. Will be removed in v3. Use either getMode or waitForMode instead." ) """ This will wait until it has received an advertisement from the Crownstone with the specified address. Once it has received an advertisement, it knows the mode. With default value for waitUntilInSetupMode (False), it will return True if the Crownstone is in setup mode, False if it isn't. You can use the boolean waitUntilInSetupMode to have it ignore advertisements from this Crownstone in other modes than setup mode. It can throw the following CrownstoneBleException - BleError.NO_SCANS_RECEIVED We have not received any scans from this Crownstone, and can't say anything about it's state. """ _LOGGER.debug( f"isCrownstoneInSetupMode address={address} scanDuration={scanDuration} waitUntilInSetupMode={waitUntilInSetupMode}" ) checker = ModeChecker(address, CrownstoneOperationMode.SETUP, waitUntilInSetupMode) subscriptionId = BleEventBus.subscribe(BleTopics.advertisement, checker.handleAdvertisement) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) result = checker.getResult() if result is None: raise CrownstoneBleException( BleError.NO_SCANS_RECEIVED, f'During the {scanDuration} seconds of scanning, no advertisement was received from this address.' ) return result
async def getRssiAverage(self, address, scanDuration=3): checker = RssiChecker(address) subscriptionId = BleEventBus.subscribe( BleTopics.rawAdvertisement, lambda scanData: checker.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) return checker.getResult()
async def getCrownstonesByScanning(self, scanDuration=3): gatherer = Gatherer() subscriptionIdAll = BleEventBus.subscribe( BleTopics.rawAdvertisement, lambda scanData: gatherer.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionIdAll) return gatherer.getCollection()
def handleAdvertisement(self, scanData: ScanData): if scanData.address != self.address: return self.result = scanData.operationMode if self.targetMode is not None and self.result != self.targetMode and self.waitUntilInTargetMode: pass elif self.targetMode is None and self.result == CrownstoneOperationMode.UNKNOWN: # if we're looking for a mode, we'll wait for the duration of the timeout in the hope it will be something other than unknown pass else: BleEventBus.emit(SystemBleTopics.abortScanning, True)
def parsePayload(self, address, rssi, nameText, serviceDataArray, serviceUUID): advertisement = Advertisement(address, rssi, nameText, serviceDataArray, serviceUUID) if advertisement.isCrownstoneFamily(): if advertisement.operationMode == CrownstoneOperationMode.SETUP: advertisement.parse() BleEventBus.emit(SystemBleTopics.rawAdvertisementClass, advertisement) else: try: advertisement.parse(self.settings.serviceDataKey) except: # fail silently. If we can't parse this, we just to propagate this message pass BleEventBus.emit(SystemBleTopics.rawAdvertisementClass, advertisement)
def __init__(self, settings: EncryptionSettings, bleAdapterAddress: str = None): # bleAdapterAddress is the MAC address of the adapter you want to use. self.settings = settings self.bleAdapterAddress = bleAdapterAddress # Connection self.activeClient: ActiveClient or None = None # Scanning self.scanner = BleakScanner(adapter=bleAdapterAddress) self.scanningActive = False self.scanAborted = False scanDelegate = BleakScanDelegate(self.settings) self.scanner.register_detection_callback(scanDelegate.handleDiscovery) # Event bus self.subscriptionIds = [] self.validator = Validator() self.subscriptionIds.append( BleEventBus.subscribe(SystemBleTopics.abortScanning, lambda x: self.abortScan())) # To be moved to active client or notification handler. self.notificationLoopActive = False
def handleAdvertisement(self, scanData: ScanData): if scanData.address in self.addressesToExcludeSet: return if self.setupModeOnly and not scanData.operationMode == CrownstoneOperationMode.SETUP: return # TODO: this is actually normalModeOnly, maybe change setupModeOnly to operationMode to filter for. if not self.setupModeOnly and scanData.operationMode == CrownstoneOperationMode.SETUP: return if scanData.rssi < self.rssiAtLeast: return self.deviceList.append(scanData) if self.returnFirstAcceptable: BleEventBus.emit(SystemBleTopics.abortScanning, True)
async def waitForPeripheralToDisconnect(self, timeout: int = 10): if self.activeClient is not None: if await self.activeClient.isConnected(): waiting = True def disconnectListener(data): nonlocal waiting waiting = False listenerId = BleEventBus.subscribe( SystemBleTopics.forcedDisconnect, disconnectListener) timer = 0 while waiting and timer < 10: await asyncio.sleep(0.1) timer += 0.1 BleEventBus.unsubscribe(listenerId) self.activeClient = None
def checkAdvertisement(self, advertisement): self.cleanupExpiredTrackers() if advertisement.address not in self.trackedCrownstones: self.trackedCrownstones[ advertisement.address] = StoneAdvertisementTracker( lambda: self.removeStone(advertisement.address)) self.trackedCrownstones[advertisement.address].update(advertisement) # forward all scans over this topic. It is located here instead of the delegates so it would be easier to convert the json to classes. data = fillScanDataFromAdvertisement( advertisement, self.trackedCrownstones[advertisement.address].verified) BleEventBus.emit(BleTopics.rawAdvertisement, data) if self.trackedCrownstones[advertisement.address].verified: BleEventBus.emit(BleTopics.advertisement, data) if not self.trackedCrownstones[advertisement.address].duplicate: BleEventBus.emit(BleTopics.newDataAvailable, data)
def __init__(self): BleEventBus.subscribe(SystemBleTopics.rawAdvertisementClass, self.checkAdvertisement) self.trackedCrownstones = {}
def forcedDisconnect(self, data): BleEventBus.emit(SystemBleTopics.forcedDisconnect, self.address) self.cleanupCallback()
async def shutDown(self): for subscriptionId in self.subscriptionIds: BleEventBus.unsubscribe(subscriptionId) await self.disconnect() await self.stopScanning()