GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

bricknil.ble_queue   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 137
dl 0
loc 256
rs 9.6
c 0
b 0
f 0
wmc 35

7 Methods

Rating   Name   Duplication   Size   Complexity  
A BLEventQ.__init__() 0 17 2
A BLEventQ.send_message() 0 17 2
A BLEventQ.get_messages() 0 26 2
A BLEventQ.run() 0 14 4
B BLEventQ._check_devices_for() 0 30 6
B BLEventQ.connect() 0 50 6
D BLEventQ._ble_connect() 0 55 13
1
# Copyright 2019 Virantha N. Ekanayake 
2
# 
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
# 
7
# http://www.apache.org/licenses/LICENSE-2.0
8
# 
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
15
"""Singleton interface to the Adafruit Bluetooth library"""
16
import Adafruit_BluefruitLE
17
from curio import Queue, sleep, CancelledError
18
import sys, functools, uuid
19
20
from .sensor import Button # Hack! only to get the button sensor_id for the fake attach message
21
from .process import Process
22
from .message_dispatch import MessageDispatch
23
from .const import USE_BLEAK
24
25
# Need a class to represent the bluetooth adapter provided
26
# by adafruit that receives messages
27
class BLEventQ(Process):
28
    """All bluetooth comms go through this object
29
30
       Provides interfaces to connect to a device/hub, send_messages to,
31
       and receive_messages from.  Also abstracts away the underlying bluetooth library
32
       that depends on the OS (Adafruit_Bluefruit for Mac, and Bleak for Linux/Win10)
33
34
       All requests to send messages to the BLE device must be inserted into
35
       the :class:`bricknil.BLEventQ.q` Queue object.
36
37
    """
38
39
    def __init__(self, ble):
40
        super().__init__('BLE Event Q')
41
        self.ble = ble
42
        self.q = Queue()
43
        if USE_BLEAK:
44
            self.message('using bleak')
45
            self.adapter = None
46
            # User needs to make sure adapter is powered up and on
47
            #    sudo hciconfig hci0 up
48
        else:
49
            self.message('Clearing BLE cache data')
50
            self.ble.clear_cached_data()
51
            self.adapter = self.ble.get_default_adapter()
52
            self.message(f'Found adapter {self.adapter.name}')
53
            self.message(f'Powering up adapter {self.adapter.name}')
54
            self.adapter.power_on()
55
        self.hubs = {}
56
57
    async def run(self):
58
        try:
59
            while True:
60
                msg = await self.q.get()
61
                msg_type, hub, msg_val = msg
62
                await self.q.task_done()
63
                self.message_debug(f'Got msg: {msg_type} = {msg_val}')
64
                await self.send_message(hub.tx, msg_val)
65
        except CancelledError:
66
            self.message(f'Terminating and disconnecxting')
67
            if USE_BLEAK:
68
                await self.ble.in_queue.put( 'quit' )
69
            else:
70
                self.device.disconnect()
71
72
    async def send_message(self, characteristic, msg):
73
        """Prepends a byte with the length of the msg and writes it to
74
           the characteristic
75
76
           Arguments:
77
              characteristic : An object from bluefruit, or if using Bleak,
78
                  a tuple (device, uuid : str)
79
              msg (bytearray) : Message with header
80
        """
81
        # Message needs to have length prepended
82
        length = len(msg)+1
83
        values = bytearray([length]+msg)
84
        if USE_BLEAK:
85
            device, char_uuid = characteristic
86
            await self.ble.in_queue.put( ('tx', (device, char_uuid, values)) )
87
        else:
88
            characteristic.write_value(values)
89
90
    async def get_messages(self, hub):
91
        """Instance a Message object to parse incoming messages and setup
92
           the callback from the characteristic to call Message.parse on the
93
           incoming data bytes
94
        """
95
        # Message instance to parse and handle messages from this hub
96
        msg_parser = MessageDispatch(hub)
97
98
        # Create a fake attach message on port 255, so that we can attach any instantiated Button listeners if present
99
        msg_parser.parse(bytearray([15, 0x00, 0x04,255, 1, Button._sensor_id, 0x00, 0,0,0,0, 0,0,0,0]))
100
101
        def bleak_received(sender, data):
102
            self.message_debug(f'Bleak Raw data received: {data}')
103
            msg = msg_parser.parse(data)
104
            self.message_debug('{0} Received: {1}'.format(hub.name, msg))
105
        def received(data):
106
            self.message_debug(f'Adafruit_Bluefruit Raw data received: {data}')
107
            msg = msg_parser.parse(data)
108
            self.message_debug('{0} Received: {1}'.format(hub.name, msg))
109
110
        if USE_BLEAK:
111
            device, char_uuid = hub.tx
112
            await self.ble.in_queue.put( ('notify', (device, char_uuid, bleak_received) ))
113
        else:
114
            # Adafruit library does not callback with the sender, only the data
115
            hub.tx.start_notify(received)
116
117
118
    def _check_devices_for(self, devices, name, manufacturer_id, address):
119
        """Check if any of the devices match what we're looking for
120
           
121
           First, check to make sure the manufacturer_id matches.  If the
122
           manufacturer_id is not present in the BLE advertised data from the 
123
           device, then fall back to the name (although this is unreliable because
124
           the name on the device can be changed by the user through the LEGO apps).
125
126
           Then, if address is supplied, only return a device with a matching id/name
127
           if it's BLE MAC address also agrees
128
129
           Returns:
130
              device : Matching device (None if no matches)
131
        """
132
        for device in devices:
133
            self.message(f'checking manufacturer ID for device named {device.name} for {name}')
134
            # Get the device manufacturer id from the advertised data if present
135
            if device.manufacturer_id == manufacturer_id or device.name == name:
136
                if not address:
137
                    return device
138
                else:
139
                    ble_address = device.address
140
                    if address == ble_address:
141
                        return device
142
                    else:
143
                        self.message(f'Address {ble_address} is not a match')
144
            else:
145
                self.message(f'No match for device with advertised data {device.manufacturer_id}')
146
147
        return None
148
149
    async def _ble_connect(self, uart_uuid, ble_name, ble_manufacturer_id, ble_id=None, timeout=60):
150
        """Connect to the underlying BLE device with the needed UART UUID
151
        """
152
        # Set hub.ble_id to a specific hub id if you want it to connect to a
153
        # particular hardware hub instance
154
        if ble_id:
155
            self.message_info(f'Looking for specific hub id {ble_id}')
156
        else:
157
            self.message_info(f'Looking for first matching hub')
158
159
        # Start discovery
160
        if not USE_BLEAK:
161
            self.adapter.start_scan()
162
163
        try:
164
            found = False
165
            while not found and timeout > 0:
166
                if USE_BLEAK:
167
                    await self.ble.in_queue.put('discover')  # Tell bleak to start discovery
168
                    devices = await self.ble.out_queue.get() # Wait for discovered devices
169
                    await self.ble.out_queue.task_done()
170
                    # Filter out no-matching uuid
171
                    devices = [d for d in devices if str(uart_uuid) in d.uuids]
172
                    # NOw, extract the manufacturer_id
173
                    for device in devices:
174
                        assert len(device.manufacturer_data) == 1
175
                        data = next(iter(device.manufacturer_data.values())) # Get the one and only key
176
                        device.manufacturer_id = data[1]
177
                else:
178
                    devices = self.ble.find_devices(service_uuids=[uart_uuid])
179
                    for device in devices:
180
                        self.message_info(f'advertised: {device.advertised}')
181
                        if len(device.advertised) > 4:
182
                            device.manufacturer_id = device.advertised[4]
183
                        else:
184
                            device.manufacturer_id = None
185
                        # Remap device.id to device.address to be consistent with bleak
186
                        device.address = device.id
187
188
                device = self._check_devices_for(devices, ble_name, ble_manufacturer_id,  ble_id)
189
                if device:
190
                    self.device = device
191
                    found = True
192
                else:
193
                    self.message(f'Rescanning for {uart_uuid} ({timeout} tries left)')
194
                    timeout -= 1
195
                    self.device = None
196
                    await sleep(1)
197
            if self.device is None:
198
                raise RuntimeError('Failed to find UART device!')
199
        except:
200
            raise
201
        finally:
202
            if not USE_BLEAK:
203
                self.adapter.stop_scan()
204
205
206
    async def connect(self, hub):
207
        """
208
            We probably need a different ble_queue type per operating system,
209
            and try to abstract away some of these hacks.
210
211
            Todo:
212
                * This needs to be cleaned up to get rid of all the hacks for
213
                  different OS and libraries
214
215
        """
216
        # Connect the messaging queue for communication between self and the hub
217
        hub.message_queue = self.q
218
        self.message(f'Starting scan for UART {hub.uart_uuid}')
219
220
        # HACK
221
        try:
222
            ble_id = uuid.UUID(hub.ble_id) if hub.ble_id else None
223
        except ValueError:
224
            # In case the user passed in a 
225
            self.message_info(f"ble_id {hub.ble_id} is not a parseable UUID, so assuming it's a BLE network addresss")
226
            ble_id = hub.ble_id
227
228
        await self._ble_connect(hub.uart_uuid, hub.ble_name, hub.manufacturer_id, ble_id)
229
230
        self.message(f"found device {self.device.name}")
231
232
        if USE_BLEAK:
233
            await self.ble.in_queue.put( ('connect', self.device.address) )
234
            device = await self.ble.out_queue.get()
235
            await self.ble.out_queue.task_done()
236
            hub.ble_id = self.device.address
237
            self.message_info(f'Device advertised: {device.characteristics}')
238
            hub.tx = (device, hub.char_uuid)   # Need to store device because the char is not an object in Bleak, unlike Bluefruit library
239
            # Hack to fix device name on Windows
240
            if self.device.name == "Unknown" and hasattr(device._requester, 'Name'):
241
                self.device.name = device._requester.Name
242
        else:
243
            self.device.connect()
244
            hub.ble_id = self.device.id
245
            # discover services
246
            self.device.discover([hub.uart_uuid], [hub.char_uuid])
247
            uart = self.device.find_service(hub.uart_uuid)
248
            hub.tx = uart.find_characteristic(hub.char_uuid) # same for rx
249
            self.message_info(f'Device advertised {self.device.advertised}')
250
251
252
        self.message_info(f"Connected to device {self.device.name}:{hub.ble_id}")
253
        self.hubs[hub.ble_id] = hub
254
255
        await self.get_messages(hub)
256
257
258
259