async def _writeControlAndGetResult( self, controlPacket, acceptedResultValues=[ ResultValue.SUCCESS, ResultValue.SUCCESS_NO_CHANGE ] ) -> ResultPacket: """ Writes the control packet, checks the result value, and returns the result packet. @param controlPacket: Serialized control packet to write. @param acceptedResultValues: List of result values that are ok. @return: The result packet. """ if self.core.ble.hasCharacteristic(SetupCharacteristics.Result): result = await self.core.ble.setupSingleNotification( CSServices.SetupService, SetupCharacteristics.Result, lambda: self._writeControlPacket(controlPacket)) else: result = await self.core.ble.setupSingleNotification( CSServices.CrownstoneService, CrownstoneCharacteristics.Result, lambda: self._writeControlPacket(controlPacket)) resultPacket = ResultPacket(result) if not resultPacket.valid: raise CrownstoneException( CrownstoneError.INCORRECT_RESPONSE_LENGTH, "Result is invalid") if resultPacket.resultCode not in acceptedResultValues: raise CrownstoneException( CrownstoneError.RESULT_NOT_SUCCESS, f"Result code is {resultPacket.resultCode}") return resultPacket
def parse(self, decryptionKey=None): if decryptionKey is not None: self.decrypt(decryptionKey) reader = BufferReader(self._data) reader.skip(2) if self.opCode == 7: try: self.payload = parseOpcode7(reader.getRemainingBytes()) except CrownstoneException: self.payload = AdvUnknownData() self.payload.data = self._data raise CrownstoneException( CrownstoneError.INVALID_SERVICE_DATA, "Protocol not supported. Unknown data type. Could be because decryption failed." ) elif self.opCode == 6: self.payload = parseOpCode6(reader.getRemainingBytes()) else: self.payload = AdvUnknownData() self.payload.data = self._data raise CrownstoneException( CrownstoneError.INVALID_SERVICE_DATA, "Protocol not supported. Unknown opcode.")
async def _command_via_mesh_broadcast(self, packet: bytearray): # this is only for time and noop # broadcast to all: # value: 1 corePacket = MeshBroadcastPacket(packet).getPacket() controlPacket = ControlPacket( ControlType.MESH_COMMAND).loadByteArray(corePacket).getPacket() uartMessage = UartMessagePacket(UartTxType.CONTROL, controlPacket).getPacket() uartPacket = UartWrapperPacket(UartMessageType.UART_MESSAGE, uartMessage).getPacket() resultCollector = Collector(timeout=2, topic=SystemTopics.resultPacket) # send the message to the Crownstone UartEventBus.emit(SystemTopics.uartWriteData, uartPacket) # wait for the collectors to fill commandResultData = await resultCollector.receive() if commandResultData is not None: if commandResultData.resultCode is ResultValue.BUSY: await asyncio.sleep(0.2) return await self._command_via_mesh_broadcast(packet) elif commandResultData.resultCode is not ResultValue.SUCCESS: raise CrownstoneException(commandResultData.resultCode, "Command has failed.") await asyncio.sleep(0.1)
def get_master_crc_from_filter_crcs(input_data: [[int, int]]) -> int: """ Get the master CRC from filter CRCs. :param input_data: A list of [filterId, filterCRC]. :returns: The master CRC. """ def sort_method(val): return val[0] input_data.sort(key=sort_method) # Check for duplicate filter IDs. for i in range(0, len(input_data) - 1): if input_data[i][0] == input_data[i + 1][0]: raise CrownstoneException( CrownstoneError.INVALID_INPUT, "Cannot have 2 filters with the same ID.") writer = BufferWriter() for id_and_filter_crc in input_data: writer.putUInt8(id_and_filter_crc[0]) writer.putUInt32(id_and_filter_crc[1]) return crc32(writer.getBuffer())
async def _writeControlAndGetResult(self, controlPacket): """ Writes the control packet, and returns the result packet. """ result = await self.core.ble.setupSingleNotification(CSServices.CrownstoneService, CrownstoneCharacteristics.Result, lambda: self._writeControlPacket(controlPacket)) resultPacket = ResultPacket(result) if not resultPacket.valid: raise CrownstoneException(CrownstoneError.INCORRECT_RESPONSE_LENGTH, "Result is invalid") return resultPacket
async def getUptime(self): """ Get the uptime of the crownstone in seconds. """ controlPacket = ControlPacket(ControlType.GET_UPTIME).getPacket() result = await self._writeControlAndGetResult(controlPacket) if result.resultCode != ResultValue.SUCCESS: raise CrownstoneException(CrownstoneError.RESULT_NOT_SUCCESS, "Result: " + str(result.resultCode)) return Conversion.uint8_array_to_uint32(result.payload)
async def getPowerSamplesAtIndex(self, samplesType, index): """ Get power samples of given type at given index. Returns a PowerSamplesPacket. """ result = await self._getPowerSamples(samplesType, index) if result.resultCode != ResultValue.SUCCESS: raise CrownstoneException(CrownstoneError.RESULT_NOT_SUCCESS, "Result: " + str(result.resultCode)) return PowerSamplesPacket(result.payload)
async def getAdcChannelSwaps(self): """ Get number of ADC channel swaps since boot. Returns an AdcChannelSwapsPacket. """ controlPacket = ControlPacket(ControlType.GET_ADC_CHANNEL_SWAPS).getPacket() result = await self._writeControlAndGetResult(controlPacket) if result.resultCode != ResultValue.SUCCESS: raise CrownstoneException(CrownstoneError.RESULT_NOT_SUCCESS, "Result: " + str(result.resultCode)) return AdcChannelSwapsPacket(result.payload)
async def getSwitchHistory(self): """ Get the switch history. Returns a SwitchHistoryListPacket. """ controlPacket = ControlPacket(ControlType.GET_SWITCH_HISTORY).getPacket() result = await self._writeControlAndGetResult(controlPacket) if result.resultCode != ResultValue.SUCCESS: raise CrownstoneException(CrownstoneError.RESULT_NOT_SUCCESS, "Result: " + str(result.resultCode)) return SwitchHistoryListPacket(result.payload)
def filterByNameWithWildcards(self, name: str, complete: bool = True): """ Assets are filtered by their name (case sensitive). :param name: Name, with wildcards, to be added filter. '?' matches any single character '*' matches zero or more characters, only at the end of a name. Example: "??_dev*" will match: "my_dev", and "01_device" won't match: "your_device", or "_dev" :param complete: Whether to look for the complete or shortened name. """ self._resetCache() if len(name) > 31: raise CrownstoneException(CrownstoneError.INVALID_SIZE, f"Name is too long: {name}") bitmask = 0 asset_name = "" for i in range(0, len(name)): if name[i] != '?': bitmask = set_bit(bitmask, i) asset_name += name[i] if name[-1] == '*': bitmask = set_bit(bitmask, len(name) - 1, False) asset_name = asset_name[:-1] else: # Set all remaining bits. If there is more data, it will be used as input. for i in range(len(name), 32): bitmask = set_bit(bitmask, i) _LOGGER.info( f"name={name} bitmask={bitmask:032b} asset_name={asset_name}") adType = 0x09 if complete else 0x08 self._input = InputDescriptionMaskedAdData(adType, bitmask) self._assets = [Conversion.string_to_uint8_array(asset_name)] return self
async def _checkRecoveryProcess(self): result = self.core.ble.readCharacteristicWithoutEncryption( CSServices.CrownstoneService, CrownstoneCharacteristics.FactoryReset) if result[0] == 1: return True elif result[0] == 2: raise CrownstoneException( BleError.RECOVERY_MODE_DISABLED, "The recovery mechanism has been disabled by the Crownstone owner." ) else: raise CrownstoneException( BleError.NOT_IN_RECOVERY_MODE, "The recovery mechanism has expired. It is only available briefly after the Crownstone is powered on." )
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()
def _checkResult(self, success_codes): if success_codes == [] or self.result.resultCode in success_codes: self.__del__() return self.result else: raise CrownstoneException( "WRITE_EXCEPTION", "Incorrect result type. Got " + str(self.result.resultCode) + ", expected one of " + str(success_codes), 400 )
def _request(self, size): if self.position + size <= self.length: start = self.position self.position += size return self.data[start:self.position] else: raise CrownstoneException( CrownstoneError.INVALID_SIZE, f"bufferSize={self.length} " f"position={self.position} " f"requestedSize={size} " f"buffer={self.data}")
def parseOpCode6(data): reader = BufferReader(data) dataType = reader.getUInt8() if dataType == 0: return parseSetupState(reader) elif dataType == 5: return parseHubData(reader) else: raise CrownstoneException(CrownstoneError.UNKNOWN_SERVICE_DATA, "Could not parse this dataType")
async def enableMicroapp(self, index): packet = MicroappHeaderPacket(index) controlPacket = ControlPacket( ControlType.MICROAPP_ENABLE).loadByteArray( packet.toBuffer()).getPacket() result = await self.core.ble.setupSingleNotification( CSServices.CrownstoneService, CrownstoneCharacteristics.Result, lambda: self._writeControlPacket(controlPacket)) resultPacket = ResultPacket(result) if resultPacket.resultCode != ResultValue.SUCCESS: raise CrownstoneException(CrownstoneError.RESULT_NOT_SUCCESS, f"result={resultPacket.resultCode}")
async def getPowerSamples(self, samplesType): """ Get all power samples of the given type. Returns a list of PowerSamplesPacket. """ allSamples = [] index = 0 while True: result = await self._getPowerSamples(samplesType, index) if result.resultCode == ResultValue.WRONG_PARAMETER: return allSamples elif result.resultCode == ResultValue.SUCCESS: samples = PowerSamplesPacket(result.payload) allSamples.append(samples) index += 1 else: raise CrownstoneException(CrownstoneError.RESULT_NOT_SUCCESS, "Result: " + str(result.resultCode))
def decrypt(self, keyHexString): if len(keyHexString) >= 16 and len(self._data) == 18: if self.operationMode == CrownstoneOperationMode.NORMAL: reader = BufferReader(self._data) encryptedData = reader.skip(2).getRemainingBytes() result = EncryptionHandler.decryptECB(encryptedData, keyHexString) for i in range(0, len(encryptedData)): # the first 2 bytes are opcode and device type self._data[i + 2] = result[i] self.decrypted = True else: raise CrownstoneException( CrownstoneError.COULD_NOT_DECRYPT, "ServiceData decryption failed. Invalid key or invalid data.")
async def getFilterSummaries(self) -> FilterSummariesPacket: """ Get a summary of the filters that are on the Crownstones. This can be used to determine: - Which filters should be changed. - What the next master version should be. - How much space there is left for new filters. - The new master CRC. :return: The filter summaries packet. """ _LOGGER.info(f"getFilterSummaries") result = await self._write(ControlPacketsGenerator.getGetFilterSummariesPacket()) if result is None: raise CrownstoneException(CrownstoneError.DATA_MISSING, "No summaries received") summaries = FilterSummariesPacket(result) return summaries
def add(self, item: list): """ Add an item to the filter. :param item: Byte array representation of the item. """ if item in self.items: # Avoid duplicates return if self.itemCount: if self.itemSize != len(item): raise CrownstoneException( CrownstoneError.INVALID_SIZE, f"Must be same size as other items ({self.itemSize})") else: self.itemSize = len(item) self.items.append(item) self.itemCount = len(self.items)
def parseOpcode7(data): reader = BufferReader(data) dataType = reader.getUInt8() if dataType == 0: return parseStatePacket(reader) elif dataType == 1: return parseErrorPacket(reader) elif dataType == 2: return parseExternalStatePacket(reader) elif dataType == 3: return parseExternalErrorPacket(reader) elif dataType == 4: return parseAlternativeState(reader) elif dataType == 5: return parseHubData(reader) elif dataType == 6: return parseMicroappServiceData(reader) else: raise CrownstoneException(CrownstoneError.UNKNOWN_SERVICE_DATA, "Could not parse this dataType")
async def setPowerZero(self, mW: int): controlPacket = ControlStateSetPacket( StateType.POWER_ZERO).loadInt32(mW).serialize() uartMessage = UartMessagePacket(UartTxType.CONTROL, controlPacket).serialize() uartPacket = UartWrapperPacket(UartMessageType.UART_MESSAGE, uartMessage).serialize() resultCollector = Collector(timeout=1, topic=SystemTopics.resultPacket) # send the message to the Crownstone UartEventBus.emit(SystemTopics.uartWriteData, uartPacket) # wait for the collectors to fill commandResultData = await resultCollector.receive() if commandResultData is not None: if commandResultData.resultCode is ResultValue.BUSY: await asyncio.sleep(0.2) return await self.setPowerZero(mW) elif commandResultData.resultCode is not ResultValue.SUCCESS: raise CrownstoneException(commandResultData.resultCode, "Command has failed.")
async def _write(self, controlPacket: [int], successCodes = [ResultValue.SUCCESS, ResultValue.SUCCESS_NO_CHANGE, ResultValue.WAIT_FOR_SUCCESS]) -> [int] or None: """ Returns the result payload. TODO: return result packet. TODO: use a ControlPacket as param, instead of int array. """ _LOGGER.debug(f"Write control packet {controlPacket}") uartMessage = UartMessagePacket(UartTxType.CONTROL, controlPacket).serialize() uartPacket = UartWrapperPacket(UartMessageType.UART_MESSAGE, uartMessage).serialize() resultCollector = Collector(timeout=1, topic=SystemTopics.resultPacket) # send the message to the Crownstone UartEventBus.emit(SystemTopics.uartWriteData, uartPacket) # wait for the collectors to fill commandResultData = await resultCollector.receive() if commandResultData is not None: if commandResultData.resultCode not in successCodes: raise CrownstoneException(commandResultData.resultCode, f"Command has failed: result code is {commandResultData.resultCode}") return commandResultData.payload return None
async def _command_via_mesh_broadcast_acked( self, crownstone_uid_array: List[int], packet: bytearray) -> MeshResult: # this is only for the set_iBeacon_config_id # broadcast to all, but retry until ID's in list have acked or timeout # value: 3 corePacket = MeshBroadcastAckedPacket(crownstone_uid_array, packet).getPacket() controlPacket = ControlPacket( ControlType.MESH_COMMAND).loadByteArray(corePacket).getPacket() uartMessage = UartMessagePacket(UartTxType.CONTROL, controlPacket).getPacket() uartPacket = UartWrapperPacket(UartMessageType.UART_MESSAGE, uartMessage).getPacket() resultCollector = Collector(timeout=2, topic=SystemTopics.resultPacket) individualCollector = BatchCollector( timeout=15, topic=SystemTopics.meshResultPacket) finalCollector = Collector(timeout=15, topic=SystemTopics.meshResultFinalPacket) # send the message to the Crownstone UartEventBus.emit(SystemTopics.uartWriteData, uartPacket) # wait for the collectors to fill commandResultData = await resultCollector.receive() if commandResultData is not None: if commandResultData.resultCode is ResultValue.BUSY: await asyncio.sleep(0.2) return await self._command_via_mesh_broadcast_acked( crownstone_uid_array, packet) elif commandResultData.resultCode is not ResultValue.SUCCESS: raise CrownstoneException(commandResultData.resultCode, "Command has failed.") return await self._handleCollectors(crownstone_uid_array, individualCollector, finalCollector)
async def _set_state_via_mesh_acked(self, crownstone_id: int, packet: bytearray) -> MeshResult: # 1:1 message to N crownstones with acks (only N = 1 supported for now) # flag value: 2 corePacket = MeshSetStatePacket(crownstone_id, packet).getPacket() controlPacket = ControlPacket( ControlType.MESH_COMMAND).loadByteArray(corePacket).getPacket() uartMessage = UartMessagePacket(UartTxType.CONTROL, controlPacket).getPacket() uartPacket = UartWrapperPacket(UartMessageType.UART_MESSAGE, uartMessage).getPacket() resultCollector = Collector(timeout=2, topic=SystemTopics.resultPacket) individualCollector = BatchCollector( timeout=15, topic=SystemTopics.meshResultPacket) finalCollector = Collector(timeout=15, topic=SystemTopics.meshResultFinalPacket) # send the message to the Crownstone UartEventBus.emit(SystemTopics.uartWriteData, uartPacket) # wait for the collectors to fill commandResultData = await resultCollector.receive() if commandResultData is not None: if commandResultData.resultCode is ResultValue.BUSY: await asyncio.sleep(0.2) return await self._set_state_via_mesh_acked( crownstone_id, packet) elif commandResultData.resultCode is not ResultValue.SUCCESS: raise CrownstoneException(commandResultData.resultCode, "Command has failed.") return await self._handleCollectors([crownstone_id], individualCollector, finalCollector)
def build(self) -> AssetFilterPacket: # Check variables if self._filterId is None: raise CrownstoneException(CrownstoneError.DATA_MISSING, f"No filter ID set.") if self._input is None: raise CrownstoneException(CrownstoneError.DATA_MISSING, f"No filter input set.") if (self._outputType is None) and (not self._exclude): raise CrownstoneException(CrownstoneError.DATA_MISSING, f"No filter output set.") # Determine output description outputType = FilterOutputDescriptionType.MAC_ADDRESS if self._outputType is not None: outputType = self._outputType inFormat = None if self._assetIdSourceBuilder is not None: inFormat = self._assetIdSourceBuilder.build() output = FilterOutputDescription(outputType, inFormat) # Remove duplicates. uniqueAssets = [] for asset in self._assets: if asset not in uniqueAssets: uniqueAssets.append(asset) self._assets = uniqueAssets # Determine filter type to use if it hasn't been set. if self._filterType is None: equalSize = True assetSize = len(self._assets[0]) totalSize = 0 for asset in self._assets: if len(asset) != assetSize: equalSize = False totalSize += len(asset) _LOGGER.debug(f"equalSize={equalSize} totalSize={totalSize}") filterOverhead = 100 # TODO: this is a very rough estimate. if totalSize + filterOverhead < self._maxFilterSize and equalSize: self._filterType = FilterType.EXACT_MATCH else: self._filterType = FilterType.CUCKOO # Build the meta data. metaData = FilterMetaData(self._filterType, self._input, output, self._profileId, FilterFlags(exclude=self._exclude)) # Construct and fill the filter. if self._filterType == FilterType.EXACT_MATCH: filterData = ExactMatchFilter() for asset in self._assets: filterData.add(asset) elif self._filterType == FilterType.CUCKOO: # TODO: move this to cuckoo filter implementation. initialNestsPerBucket = 4 requiredBucketCount = len( self._assets) / 0.95 / initialNestsPerBucket bucketCountLog2 = max(0, math.ceil(math.log2(requiredBucketCount))) bucketCount = math.pow(2, bucketCountLog2) nestsPerBucket = math.ceil(len(self._assets) / bucketCount) cuckooFilter = CuckooFilter(bucketCountLog2, nestsPerBucket) for asset in self._assets: if not cuckooFilter.add(asset): raise CrownstoneException( CrownstoneError.INVALID_SIZE, "Failed to add asset to cuckoo filter.") filterData = cuckooFilter.getData() else: raise CrownstoneException( CrownstoneError.UNKNOWN_TYPE, f"Unknown filter type: {self._filterType}") self._packet = AssetFilterPacket(metaData, filterData) self._crc = crc32(self.serialize()) return self._packet
def _wrapUpFailedResult(self, wait_until_result): self.__del__() # TODO: Invalid error type. raise CrownstoneException("WRITE_EXCEPTION", "No result received after writing to UART. Waited for " + str( wait_until_result) + " seconds", 404)
def _wrapUpFailedWrite(self): # we should never arrive here. If the write went wrong, an error should have been thrown by the write method before we get here. self.__del__() # TODO: Invalid error type. raise CrownstoneException("WRITE_EXCEPTION", "Write not completed, but no error was thrown.", 500)