8 Commits
v0.2.7 ... dev

Author SHA1 Message Date
Shin'ichiro Kawasaki
2d489321d0 BLESession.setNotifications: Lookup CCCD to start or stop notifications
Per Bluetooth Core Specification Vol.3, Part G, Section 3.3.1.1, if
Notify property is set for a Characteristic, the Client Characteristic
Configuration Descriptor, or CCCD, shall exist. As noted in section
3.3.3.3 of the specification, notification of the Characteristic is
enabled or disabled by setting bit 0 of the CCCD.

This CCCD exists within the characteristic definition following the
Characteristic Value declaration, and section 3.3.3 of the specification
warns that the order of characteristic declaration shall not be assumed.
However, current pyscrlink code assumes that the CCCD comes just after
the Characteristic Value and CCCD handle is obtained just adding 1 to
the handle of the Characteristic Value. This works many of the BLE
devices but does not work all of them.

To get correct CCCD handle, look up descriptors of the Characteristic to
find out the CCCD.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2022-01-03 12:27:39 +09:00
Shin'ichiro Kawasaki
48e460b9c6 BLESession: Add _getDevName() helper function
GitHub issue #31 reported that Intelino Smart Train is not connected
with pyscrlink. It also reported that getValueText(0x9) call for the
device instead of getValueText(0x8) avoids the failure. This implies
that the Intelino Smart Train does not report AdType 0x8 "Shortened
local name" but it reports AdType 0x9 "Complete local name".

To get local name of the BLE devices regardless AdType support status
of those devices, introduce a helper function _getDevName(). It gets the
AdType 0x9 "Complete local name" first. If it is not available, get
AdType 0x8 "Shortened local name". Use this helper function at all
places to get the device name.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-12-31 14:49:51 +09:00
Shin'ichiro Kawasaki
3cf61a145b BTSession: Additional change to pretend LEGO EV3 with LEGO Hub
The previous commit tweaked the class IDs returned from the LEGO Hub as
that of LEGO EV3. On top of that, it is required to tweak the class ID
to search and discover for bluetooth.DeviceDiscoverer API.

Also, add some more debug prints to track BT device discovery.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-11-21 14:07:15 +09:00
Shin'ichiro Kawasaki
3ffbde35b1 BLESession: handle device UUIDs as list
In the debug for the issue #30, it turned out that the bluepy API
ScanEntry.getValueText() for adtypes may return multiple UUIDs
concatenated with comma. Current code assumes that the API returns
single UUID, then multiple UUIDs cause the "Error: Non-hexadecimal digit
found" for the comma.

To fix it, replace ScanEntry.getValueText() call with getValue(), and
handle the result as a list of UUIDs. Also rename the helper function
_get_dev_uuid() to _get_dev_uuids() accordingly.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-11-14 20:46:53 +09:00
Shin'ichiro Kawasaki
18e0818e98 BTSession: Pretend LEGO EV3 with LEGO Hub
To try out LEGO Hub connection as LEGO EV3, tweak BTSession device
discovery code. When the device name has "LEGO Hub", prented its device
class (major/minor=8/4) as LEGO EV3's device class (major/minor=8/1).
This is a trial code for the GitHub issue #21.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-11-14 14:46:04 +09:00
Shin'ichiro Kawasaki
266bcc98bd BLESession: Enrich logs for device UUID check
To debug the GitHub issue #30, add more debug logs to functions to check
UUID.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-11-14 13:41:54 +09:00
Shin'ichiro Kawasaki
80b99f84a1 BLESession: Override unknown handle with known handle
Unknown handle "13" was recorded in debug log for the Git Hub issue #26.
As a debug trial, replace the unknown handle with the known handle.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-11-14 13:13:19 +09:00
Shin'ichiro Kawasaki
f8ce9e3089 BLESession: Enrich logs for notification handling
To debug the GitHub issue #26, add more debug and error logs to
functions to handle notifications.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2021-11-14 13:13:19 +09:00

View File

@@ -47,6 +47,8 @@ logger.propagate = False
HOSTNAME="device-manager.scratch.mit.edu"
scan_seconds=10.0
CCCD_UUID = 0x2902
class Session():
"""Base class for BTSession and BLESession"""
def __init__(self, websocket, loop):
@@ -170,6 +172,9 @@ class BTSession(Session):
logger.debug(f"Found device {name} addr={address} class={device_class} rssi={rssi}")
major_class = (device_class & 0x1F00) >> 8
minor_class = (device_class & 0xFF) >> 2
if "LEGO Hub" in name:
minor_class = 1
logger.info(f"Pretend to be LEGO EV3 with LEGO Hub: class={major_class/minor_class}")
if major_class == self.major_class and minor_class == self.minor_class:
self.found_devices[address] = (name, device_class, rssi)
@@ -185,7 +190,13 @@ class BTSession(Session):
self.ping_time = None
def discover(self):
discoverer = self.BTDiscoverer(self.major_device_class, self.minor_device_class)
major = self.major_device_class
minor = self.minor_device_class
if major == 8 and minor == 1:
logger.info(f"Search LEGO Hub instead of LEGO EV3")
minor = 4
logger.debug(f"BT discover: class={major}/{minor}")
discoverer = self.BTDiscoverer(major, minor)
discoverer.find_devices(lookup_names=True)
while self.session.status == self.session.DISCOVERY and not discoverer.done and not self.cancel_discovery:
readable = select.select([discoverer], [], [], 0.5)[0]
@@ -203,7 +214,7 @@ class BTSession(Session):
def run(self):
while self.session.status != self.session.DONE:
logger.debug("loop in BT thread")
logger.debug(f"loop in BT thread: session status={self.session.status}")
current_time = int(round(time.time()))
if self.session.status == self.session.DISCOVERY and not self.cancel_discovery:
@@ -353,6 +364,14 @@ class BLESession(Session):
scan_lock = threading.RLock()
scan_started = False
@staticmethod
def _getDevName(dev):
"""
Get AdType 0x09 (Completed local name). If it is not available,
get AdType 0x08 (Shortened local name).
"""
return dev.getValueText(0x9) or dev.getValueText(0x8)
class BLEThread(threading.Thread):
"""
Separated thread to control notifications to Scratch.
@@ -372,7 +391,7 @@ class BLESession(Session):
for d in devices:
params = { 'rssi': d.rssi }
params['peripheralId'] = devices.index(d)
params['name'] = d.getValueText(0x9) or d.getValueText(0x8)
params['name'] = BLESession._getDevName(d)
self.session.notify('didDiscoverPeripheral', params)
time.sleep(1)
elif self.session.status == self.session.CONNECTED:
@@ -389,7 +408,8 @@ class BLESession(Session):
logger.debug("after waitForNotification")
logger.debug("released lock for waitForNotification")
except Exception as e:
logger.error(e)
logger.error(f"Exception in waitForNotifications: "
f"{type(e).__name__}: {e}")
self.session.close()
break
else:
@@ -413,7 +433,8 @@ class BLESession(Session):
self.restart_notification_event.set()
def add_handle(self, serviceId, charId, handle):
logger.debug(f"add handle for notification: {handle}")
logger.debug(f"add handle for notification: "
f"{serviceId} {charId} {handle}")
params = { 'serviceId': UUID(serviceId).getCommonName(),
'characteristicId': charId,
'encoding': 'base64' }
@@ -421,6 +442,14 @@ class BLESession(Session):
def handleNotification(self, handle, data):
logger.debug(f"BLE notification: {handle} {data}")
if handle not in self.handles:
logger.error(f"Notification with unknown handle: {handle}")
keys = list(self.handles.keys())
if keys and len(keys) == 1:
logger.debug(f"Debug: override {handle} with {keys[0]}")
handle = keys[0]
else:
return
params = self.handles[handle].copy()
params['message'] = base64.standard_b64encode(data).decode('ascii')
self.session.notify('characteristicDidChange', params)
@@ -453,12 +482,14 @@ class BLESession(Session):
def __del__(self):
self.close()
def _get_dev_uuid(self, dev):
def _get_dev_uuids(self, dev):
for adtype in self.SERVICE_CLASS_UUID_ADTYPES:
service_class_uuid = dev.getValueText(adtype)
if service_class_uuid:
logger.debug(self.SERVICE_CLASS_UUID_ADTYPES[adtype])
return UUID(service_class_uuid)
service_class_uuids = dev.getValue(adtype)
if service_class_uuids:
for u in service_class_uuids:
a = self.SERVICE_CLASS_UUID_ADTYPES[adtype]
logger.debug(f"service class uuid for {a}/{adtype}: {u}")
return service_class_uuids
return None
def matches(self, dev, filters):
@@ -471,18 +502,19 @@ class BLESession(Session):
for s in f['services']:
logger.debug(f"service to check: {s}")
given_uuid = s
logger.debug(f"given: {given_uuid}")
dev_uuid = self._get_dev_uuid(dev)
if not dev_uuid:
logger.debug(f"given UUID: {given_uuid} hash={UUID(given_uuid).__hash__()}")
dev_uuids = self._get_dev_uuids(dev)
if not dev_uuids:
continue
logger.debug(f"dev: {dev_uuid}")
logger.debug(given_uuid == dev_uuid)
if given_uuid == dev_uuid:
logger.debug("match...")
return True
for u in dev_uuids:
logger.debug(f"dev UUID: {u} hash={u.__hash__()}")
logger.debug(given_uuid == u)
if given_uuid == u:
logger.debug("match...")
return True
if 'namePrefix' in f:
# 0x08: Shortened Local Name
deviceName = dev.getValueText(0x08)
deviceName = self._getDevName(dev)
if not deviceName:
continue
logger.debug(f"Name of \"{deviceName}\" begins with: \"{f['namePrefix']}\"?")
@@ -598,7 +630,7 @@ class BLESession(Session):
elif self.status == self.DISCOVERY and method == 'connect':
logger.debug("connecting to the BLE device")
self.device = BLESession.found_devices[params['peripheralId']]
self.deviceName = self.device.getValueText(0x9) or self.device.getValueText(0x8)
self.deviceName = self._getDevName(self.device)
try:
self.perip = Peripheral(self.device)
logger.info(f"connected to the BLE peripheral: {self.deviceName}")
@@ -675,11 +707,19 @@ class BLESession(Session):
service = self._get_service(service_id)
c = self._get_characteristic(chara_id)
handle = c.getHandle()
# get CCCD or Client Characterstic Configuration Descriptor
cccd = None
for d in c.getDescriptors():
if d.uuid == UUID(CCCD_UUID):
cccd = d
if not cccd:
logger.error("Characteristic {char_id} does not have CCCD")
return
# prepare notification handler
self.delegate.add_handle(service_id, chara_id, handle)
# request notification to the BLE device
with self.lock:
self.perip.writeCharacteristic(handle + 1, value, True)
self.perip.writeCharacteristic(cccd.handle, value, True)
def startNotifications(self, service_id, chara_id):
logger.debug(f"start notification for {chara_id}")