class BTWATTCH2: def __init__(self, address): self.client = BleakClient(address) self.loop = asyncio.get_event_loop() self.services = self.loop.run_until_complete(self.setup()) self.Tx = self.services.get_characteristic(UART_TX_UUID) self.Rx = self.services.get_characteristic(UART_RX_UUID) self.char_device_name = self.services.get_characteristic( DEVICE_NAME_UUID) self.loop.create_task(self.enable_notify()) self.callback = print_measurement @property def address(self): return self.client.address @property def model_number(self): read_device_name = self.client.read_gatt_char(self.char_device_name) return self.loop.run_until_complete(read_device_name).decode() async def setup(self): await self.client.connect() return await self.client.get_services() async def enable_notify(self): await self.client.start_notify(self.Rx, self._cache_message()) async def disable_notify(self): await self.client.stop_notify(self.Rx) def pack_command(self, payload: bytearray): pld_length = len(payload).to_bytes(2, 'big') return CMD_HEADER + pld_length + payload + crc8(payload).to_bytes( 1, 'big') def _write(self, payload: bytearray): async def _write_(payload): command = self.pack_command(payload) await self.client.write_gatt_char(self.Tx, command, True) if self.loop.is_running(): return self.loop.create_task(_write_(payload)) else: return self.loop.run_until_complete(_write_(payload)) def set_timer(self): time.sleep(1 - datetime.datetime.now().microsecond / 1e6) d = datetime.datetime.now().timetuple() payload = (ID_TIMER[0], d.tm_sec, d.tm_min, d.tm_hour, d.tm_mday, d.tm_mon - 1, d.tm_year - 1900, d.tm_wday) self._write(bytearray(payload)) def on(self): self._write(ID_TURN_ON) def off(self): self._write(ID_TURN_OFF) def measure(self): self._write(ID_ENERGY_USAGE) interval = 1.05 - datetime.datetime.now().microsecond / 1e6 self.loop.run_until_complete(asyncio.sleep(interval)) def _cache_message(self): buffer = bytearray() def _cache_message_(sender: int, value: bytearray): nonlocal buffer buffer = buffer + value if buffer[0] == CMD_HEADER[0]: payload_length = int.from_bytes(buffer[1:3], 'big') if len(buffer[3:-1]) < payload_length: return elif len(buffer[3:-1]) == payload_length: if crc8(buffer[3:]) == 0: self._classify_response(buffer) buffer.clear() return _cache_message_ def _classify_response(self, data): if data[3] == ID_ENERGY_USAGE[0]: measurement = self.decode_measurement(data) self.callback(**measurement) else: pass # to be implemented def decode_measurement(self, data: bytearray): return { "voltage": int.from_bytes(data[5:11], 'little') / (16**6), "current": int.from_bytes(data[11:17], 'little') / (32**6) * 1000, "wattage": int.from_bytes(data[17:23], 'little') / (16**6), "timestamp": datetime.datetime(1900 + data[28], data[27] + 1, *data[26:22:-1]), }
class BleakLink(BaseLink): def __init__(self, device="hci0", loop=None, *args, **kwargs): self.device = device self.timeout = 5 self.loop = loop or asyncio.get_event_loop() self._rx_fifo = Fifo() self._client = None self._th = None def __enter__(self): self.start() return self def start(self): if self._th: return self._th = Thread(target=run_worker, args=(self.loop, )) self._th.daemon = True self._th.start() def __exit__(self, exc_type, exc_value, traceback): if self._client: self.close() def close(self): asyncio.run_coroutine_threadsafe(self._client.disconnect(), self.loop).result(10) def scan(self, timeout=1): devices = asyncio.run_coroutine_threadsafe( discover(timeout=timeout, device=self.device), self.loop).result(timeout * 3) # We need to keep scanning going for connect() to properly work asyncio.run_coroutine_threadsafe( discover(timeout=timeout, device=self.device), self.loop) return [ (dev.name, dev.address) for dev in devices if dev.metadata.get('manufacturer_data', {}).get(_manuf_id, []) in [_manuf_data_xiaomi, _manuf_data_xiaomi_pro, _manuf_data_ninebot] ] def open(self, port): fut = asyncio.run_coroutine_threadsafe(self._connect(port), self.loop) fut.result(10) async def _connect(self, port): if isinstance(port, tuple): port = port[1] self._client = BleakClient(port, device=self.device) await self._client.connect() print("connected") await self._client.start_notify(_tx_char_uuid, self._data_received) print("services:", list(await self._client.get_services())) def _data_received(self, sender, data): self._rx_fifo.write(data) def write(self, data): fut = asyncio.run_coroutine_threadsafe( self._client.write_gatt_char(_rx_char_uuid, bytearray(data), True), self.loop, ) return fut.result(3) def read(self, size): try: data = self._rx_fifo.read(size, timeout=self.timeout) except queue.Empty: raise LinkTimeoutException return data def fetch_keys(self): return asyncio.run_coroutine_threadsafe( self._client.read_gatt_char(_keys_char_uuid), self.loop).result(5)
class BleAioTransport: """ This class encapsulates management of the BLE transport using asyncio and communicating with the Telemetrix4Esp32BLE server resident on an ESP32 board. """ def __init__(self, ble_mac_address=None, loop=None, receive_callback=None): """ :param ble_mac_address: User specified mac address. If not specified mac address discovery will be attempted. :param loop: asyncio loop :param receive_callback: method to be called when data is received from the BLE connected server. """ # make sure the user specified a handler for incoming data if not receive_callback: raise RuntimeError( 'ble_aio_transport: A receive callback must be specified') else: self.receive_callback = receive_callback # mac address of device. # If set to None, then an attempt at autodiscovery will take place. self.ble_mac_address = ble_mac_address # loop management self.loop = loop if not self.loop: self.loop = asyncio.get_event_loop() # characteristics for the BLE UART service # Nordic NUS characteristic for transmit. self.UART_TX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" # Nordic NUS characteristic for receive. self.UART_RX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" # the client is what we call the BLE transport self.client = None # a variable to keep track if the transport is currently connected self.connected = False async def notification_handler(self, sender, data): """ Process incoming BLE data Call the incoming data processing method :param sender: sender ID - not used :param data: data received """ await self.receive_callback(sender, data) # noinspection PyTypeChecker async def connect(self): """ This method will attempt a connection with the ble device if not already connected. If a ble MAC address was provided it will use that address, and if not, it will attempt to auto discover the device before connection """ if self.connected: raise RuntimeError( 'ble_aio_transport: connect - Already connected') # user did not specify a mac address, so we try to do auto-discovery of # the server's mac address. if not self.ble_mac_address: print('Retrieving BLE Mac Address of Ble Device. Please wait...') devices = await discover() for d in devices: if d.name == 'Telemetrix4ESP32BLE': self.ble_mac_address = d.address # now attempt to connect print(f'Connecting to {self.ble_mac_address}. Please wait....') self.client = BleakClient(self.ble_mac_address) try: await self.client.connect() except bleak.exc.BleakDBusError: raise KeyboardInterrupt self.connected = True print('Connection successful') # associate the notification handler with incoming data await self.client.start_notify(self.UART_RX_UUID, self.notification_handler) # self.loop.create_task(self.ble_read()) async def disconnect(self): try: await self.client.disconnect() except AttributeError: pass def ble_read(self): try: self.loop.run_until_complete( self.client.read_gatt_char(self.UART_RX_UUID)) except: pass async def write(self, data): """ This method writes sends data to the BLE device :param data: data is in the form of a bytearray """ # noinspection PyBroadException try: await self.client.write_gatt_char(self.UART_TX_UUID, data) except Exception: pass if sys.platform.startswith('win32'): await asyncio.sleep(.2)