mirror of
https://github.com/kawasaki/pyscrlink.git
synced 2025-09-06 17:50:20 +02:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2d489321d0 | ||
|
48e460b9c6 | ||
|
3cf61a145b | ||
|
3ffbde35b1 | ||
|
18e0818e98 | ||
|
266bcc98bd | ||
|
80b99f84a1 | ||
|
f8ce9e3089 | ||
|
e6d63a5c97 | ||
|
6f7da4f720 | ||
|
32da72492f | ||
|
1af4baa7d6 | ||
|
39587b497b | ||
|
f506bbcaab |
15
README.md
15
README.md
@@ -42,7 +42,7 @@ Linux distros:
|
|||||||
* elementary OS 5.1 Hera
|
* elementary OS 5.1 Hera
|
||||||
|
|
||||||
Browsers:
|
Browsers:
|
||||||
* FireFox
|
* Firefox
|
||||||
* Chromium
|
* Chromium
|
||||||
|
|
||||||
It was reported that pyscrlink (former bluepy-scratch-link) working with
|
It was reported that pyscrlink (former bluepy-scratch-link) working with
|
||||||
@@ -51,7 +51,7 @@ following devices and Linux distros.
|
|||||||
Devices:
|
Devices:
|
||||||
* LEGO Mindstorm EV3 by @chrisglencross
|
* LEGO Mindstorm EV3 by @chrisglencross
|
||||||
* LEGO WeDo by @zhaowe, @KingBBQ
|
* LEGO WeDo by @zhaowe, @KingBBQ
|
||||||
* LEGO Boost by @laurentchar, @miguev, @jacquesdt
|
* LEGO Boost and compatible devices by @laurentchar, @miguev, @jacquesdt, @n3storm
|
||||||
* Intelino Smart Train by @ErrorJan
|
* Intelino Smart Train by @ErrorJan
|
||||||
* toio by @shimodash
|
* toio by @shimodash
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ Linux distros:
|
|||||||
* Raspbian by @chrisglencross
|
* Raspbian by @chrisglencross
|
||||||
* Ubuntu 16.04 @jacquesdt
|
* Ubuntu 16.04 @jacquesdt
|
||||||
* Ubuntu Studio 20.04 @miguev
|
* Ubuntu Studio 20.04 @miguev
|
||||||
|
* Debian 11 @n3storm
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
@@ -87,7 +88,8 @@ Installation
|
|||||||
$ pip3 install pyscrlink
|
$ pip3 install pyscrlink
|
||||||
```
|
```
|
||||||
|
|
||||||
4. For Bluetooth Low Energy (BLE) devices, set bluepy-helper capability.
|
4. Set bluepy-helper capability.
|
||||||
|
This step is required for most of devices except LEGO Mindstorm EV3.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ bluepy_helper_cap
|
$ bluepy_helper_cap
|
||||||
@@ -159,6 +161,8 @@ Usage
|
|||||||
```sh
|
```sh
|
||||||
$ scratch_link
|
$ scratch_link
|
||||||
```
|
```
|
||||||
|
If your device is toio, add "-s 1" option to the scratch_link command. It
|
||||||
|
allows the toio Do Visual Programming to connect to toio automatically.
|
||||||
|
|
||||||
3. Connect scratch to the target device such as micro:bit or LEGO Mindstorms:
|
3. Connect scratch to the target device such as micro:bit or LEGO Mindstorms:
|
||||||
* Open FireFox or Chrome. (Make sure to run as the same user for scratch-link python script.)
|
* Open FireFox or Chrome. (Make sure to run as the same user for scratch-link python script.)
|
||||||
@@ -202,6 +206,11 @@ Please file issues to [GitHub issue tracker](https://github.com/kawasaki/pyscrli
|
|||||||
Releases
|
Releases
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
Release 0.2.4
|
||||||
|
|
||||||
|
* Added -s option to specify BLE scan duration
|
||||||
|
* Improved README.md
|
||||||
|
|
||||||
Release 0.2.3
|
Release 0.2.3
|
||||||
|
|
||||||
* Fixed eternal loop caused by hostname resolve failure
|
* Fixed eternal loop caused by hostname resolve failure
|
||||||
|
@@ -15,6 +15,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
import traceback
|
import traceback
|
||||||
|
import argparse
|
||||||
|
|
||||||
# for Bluetooth (e.g. Lego EV3)
|
# for Bluetooth (e.g. Lego EV3)
|
||||||
import bluetooth
|
import bluetooth
|
||||||
@@ -44,6 +45,9 @@ logger.addHandler(handler)
|
|||||||
logger.propagate = False
|
logger.propagate = False
|
||||||
|
|
||||||
HOSTNAME="device-manager.scratch.mit.edu"
|
HOSTNAME="device-manager.scratch.mit.edu"
|
||||||
|
scan_seconds=10.0
|
||||||
|
|
||||||
|
CCCD_UUID = 0x2902
|
||||||
|
|
||||||
class Session():
|
class Session():
|
||||||
"""Base class for BTSession and BLESession"""
|
"""Base class for BTSession and BLESession"""
|
||||||
@@ -168,6 +172,9 @@ class BTSession(Session):
|
|||||||
logger.debug(f"Found device {name} addr={address} class={device_class} rssi={rssi}")
|
logger.debug(f"Found device {name} addr={address} class={device_class} rssi={rssi}")
|
||||||
major_class = (device_class & 0x1F00) >> 8
|
major_class = (device_class & 0x1F00) >> 8
|
||||||
minor_class = (device_class & 0xFF) >> 2
|
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:
|
if major_class == self.major_class and minor_class == self.minor_class:
|
||||||
self.found_devices[address] = (name, device_class, rssi)
|
self.found_devices[address] = (name, device_class, rssi)
|
||||||
|
|
||||||
@@ -183,7 +190,13 @@ class BTSession(Session):
|
|||||||
self.ping_time = None
|
self.ping_time = None
|
||||||
|
|
||||||
def discover(self):
|
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)
|
discoverer.find_devices(lookup_names=True)
|
||||||
while self.session.status == self.session.DISCOVERY and not discoverer.done and not self.cancel_discovery:
|
while self.session.status == self.session.DISCOVERY and not discoverer.done and not self.cancel_discovery:
|
||||||
readable = select.select([discoverer], [], [], 0.5)[0]
|
readable = select.select([discoverer], [], [], 0.5)[0]
|
||||||
@@ -201,7 +214,7 @@ class BTSession(Session):
|
|||||||
def run(self):
|
def run(self):
|
||||||
while self.session.status != self.session.DONE:
|
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()))
|
current_time = int(round(time.time()))
|
||||||
|
|
||||||
if self.session.status == self.session.DISCOVERY and not self.cancel_discovery:
|
if self.session.status == self.session.DISCOVERY and not self.cancel_discovery:
|
||||||
@@ -351,6 +364,14 @@ class BLESession(Session):
|
|||||||
scan_lock = threading.RLock()
|
scan_lock = threading.RLock()
|
||||||
scan_started = False
|
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):
|
class BLEThread(threading.Thread):
|
||||||
"""
|
"""
|
||||||
Separated thread to control notifications to Scratch.
|
Separated thread to control notifications to Scratch.
|
||||||
@@ -370,7 +391,7 @@ class BLESession(Session):
|
|||||||
for d in devices:
|
for d in devices:
|
||||||
params = { 'rssi': d.rssi }
|
params = { 'rssi': d.rssi }
|
||||||
params['peripheralId'] = devices.index(d)
|
params['peripheralId'] = devices.index(d)
|
||||||
params['name'] = d.getValueText(0x9) or d.getValueText(0x8)
|
params['name'] = BLESession._getDevName(d)
|
||||||
self.session.notify('didDiscoverPeripheral', params)
|
self.session.notify('didDiscoverPeripheral', params)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
elif self.session.status == self.session.CONNECTED:
|
elif self.session.status == self.session.CONNECTED:
|
||||||
@@ -387,7 +408,8 @@ class BLESession(Session):
|
|||||||
logger.debug("after waitForNotification")
|
logger.debug("after waitForNotification")
|
||||||
logger.debug("released lock for waitForNotification")
|
logger.debug("released lock for waitForNotification")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(f"Exception in waitForNotifications: "
|
||||||
|
f"{type(e).__name__}: {e}")
|
||||||
self.session.close()
|
self.session.close()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -411,7 +433,8 @@ class BLESession(Session):
|
|||||||
self.restart_notification_event.set()
|
self.restart_notification_event.set()
|
||||||
|
|
||||||
def add_handle(self, serviceId, charId, handle):
|
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(),
|
params = { 'serviceId': UUID(serviceId).getCommonName(),
|
||||||
'characteristicId': charId,
|
'characteristicId': charId,
|
||||||
'encoding': 'base64' }
|
'encoding': 'base64' }
|
||||||
@@ -419,6 +442,14 @@ class BLESession(Session):
|
|||||||
|
|
||||||
def handleNotification(self, handle, data):
|
def handleNotification(self, handle, data):
|
||||||
logger.debug(f"BLE notification: {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 = self.handles[handle].copy()
|
||||||
params['message'] = base64.standard_b64encode(data).decode('ascii')
|
params['message'] = base64.standard_b64encode(data).decode('ascii')
|
||||||
self.session.notify('characteristicDidChange', params)
|
self.session.notify('characteristicDidChange', params)
|
||||||
@@ -451,12 +482,14 @@ class BLESession(Session):
|
|||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _get_dev_uuid(self, dev):
|
def _get_dev_uuids(self, dev):
|
||||||
for adtype in self.SERVICE_CLASS_UUID_ADTYPES:
|
for adtype in self.SERVICE_CLASS_UUID_ADTYPES:
|
||||||
service_class_uuid = dev.getValueText(adtype)
|
service_class_uuids = dev.getValue(adtype)
|
||||||
if service_class_uuid:
|
if service_class_uuids:
|
||||||
logger.debug(self.SERVICE_CLASS_UUID_ADTYPES[adtype])
|
for u in service_class_uuids:
|
||||||
return UUID(service_class_uuid)
|
a = self.SERVICE_CLASS_UUID_ADTYPES[adtype]
|
||||||
|
logger.debug(f"service class uuid for {a}/{adtype}: {u}")
|
||||||
|
return service_class_uuids
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def matches(self, dev, filters):
|
def matches(self, dev, filters):
|
||||||
@@ -469,18 +502,19 @@ class BLESession(Session):
|
|||||||
for s in f['services']:
|
for s in f['services']:
|
||||||
logger.debug(f"service to check: {s}")
|
logger.debug(f"service to check: {s}")
|
||||||
given_uuid = s
|
given_uuid = s
|
||||||
logger.debug(f"given: {given_uuid}")
|
logger.debug(f"given UUID: {given_uuid} hash={UUID(given_uuid).__hash__()}")
|
||||||
dev_uuid = self._get_dev_uuid(dev)
|
dev_uuids = self._get_dev_uuids(dev)
|
||||||
if not dev_uuid:
|
if not dev_uuids:
|
||||||
continue
|
continue
|
||||||
logger.debug(f"dev: {dev_uuid}")
|
for u in dev_uuids:
|
||||||
logger.debug(given_uuid == dev_uuid)
|
logger.debug(f"dev UUID: {u} hash={u.__hash__()}")
|
||||||
if given_uuid == dev_uuid:
|
logger.debug(given_uuid == u)
|
||||||
logger.debug("match...")
|
if given_uuid == u:
|
||||||
return True
|
logger.debug("match...")
|
||||||
|
return True
|
||||||
if 'namePrefix' in f:
|
if 'namePrefix' in f:
|
||||||
# 0x08: Shortened Local Name
|
# 0x08: Shortened Local Name
|
||||||
deviceName = dev.getValueText(0x08)
|
deviceName = self._getDevName(dev)
|
||||||
if not deviceName:
|
if not deviceName:
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Name of \"{deviceName}\" begins with: \"{f['namePrefix']}\"?")
|
logger.debug(f"Name of \"{deviceName}\" begins with: \"{f['namePrefix']}\"?")
|
||||||
@@ -495,6 +529,7 @@ class BLESession(Session):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _scan_devices(self, params):
|
def _scan_devices(self, params):
|
||||||
|
global scan_seconds
|
||||||
if BLESession.nr_connected > 0:
|
if BLESession.nr_connected > 0:
|
||||||
return len(BLESession.found_devices) > 0
|
return len(BLESession.found_devices) > 0
|
||||||
found = False
|
found = False
|
||||||
@@ -505,7 +540,8 @@ class BLESession(Session):
|
|||||||
for i in range(self.MAX_SCANNER_IF):
|
for i in range(self.MAX_SCANNER_IF):
|
||||||
scanner = Scanner(iface=i)
|
scanner = Scanner(iface=i)
|
||||||
try:
|
try:
|
||||||
devices = scanner.scan(10.0)
|
logger.debug(f"start BLE scan: {scan_seconds} seconds")
|
||||||
|
devices = scanner.scan(scan_seconds)
|
||||||
for dev in devices:
|
for dev in devices:
|
||||||
if self.matches(dev, params['filters']):
|
if self.matches(dev, params['filters']):
|
||||||
BLESession.found_devices.append(dev)
|
BLESession.found_devices.append(dev)
|
||||||
@@ -594,7 +630,7 @@ class BLESession(Session):
|
|||||||
elif self.status == self.DISCOVERY and method == 'connect':
|
elif self.status == self.DISCOVERY and method == 'connect':
|
||||||
logger.debug("connecting to the BLE device")
|
logger.debug("connecting to the BLE device")
|
||||||
self.device = BLESession.found_devices[params['peripheralId']]
|
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:
|
try:
|
||||||
self.perip = Peripheral(self.device)
|
self.perip = Peripheral(self.device)
|
||||||
logger.info(f"connected to the BLE peripheral: {self.deviceName}")
|
logger.info(f"connected to the BLE peripheral: {self.deviceName}")
|
||||||
@@ -671,11 +707,19 @@ class BLESession(Session):
|
|||||||
service = self._get_service(service_id)
|
service = self._get_service(service_id)
|
||||||
c = self._get_characteristic(chara_id)
|
c = self._get_characteristic(chara_id)
|
||||||
handle = c.getHandle()
|
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
|
# prepare notification handler
|
||||||
self.delegate.add_handle(service_id, chara_id, handle)
|
self.delegate.add_handle(service_id, chara_id, handle)
|
||||||
# request notification to the BLE device
|
# request notification to the BLE device
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.perip.writeCharacteristic(handle + 1, value, True)
|
self.perip.writeCharacteristic(cccd.handle, value, True)
|
||||||
|
|
||||||
def startNotifications(self, service_id, chara_id):
|
def startNotifications(self, service_id, chara_id):
|
||||||
logger.debug(f"start notification for {chara_id}")
|
logger.debug(f"start notification for {chara_id}")
|
||||||
@@ -718,18 +762,20 @@ def stack_trace():
|
|||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
opts = [opt for opt in sys.argv[1:] if opt.startswith("-")]
|
global scan_seconds
|
||||||
if "-h" in opts:
|
parser = argparse.ArgumentParser(description='start Scratch-link')
|
||||||
print((f"Usage: {sys.argv[0]} [OPTS]\n"
|
parser.add_argument('-d', '--debug', action='store_true',
|
||||||
"OPTS:\t-h Show this help.\n"
|
help='print debug messages')
|
||||||
"\t-d Print debug messages."
|
parser.add_argument('-s', '--scan_seconds', type=float, default=10.0,
|
||||||
))
|
help='specifiy duration to scan BLE devices in seconds')
|
||||||
sys.exit(1)
|
args = parser.parse_args()
|
||||||
elif "-d" in opts:
|
if args.debug:
|
||||||
print("Print debug messages")
|
print("Print debug messages")
|
||||||
logLevel = logging.DEBUG
|
logLevel = logging.DEBUG
|
||||||
handler.setLevel(logLevel)
|
handler.setLevel(logLevel)
|
||||||
logger.setLevel(logLevel)
|
logger.setLevel(logLevel)
|
||||||
|
scan_seconds = args.scan_seconds
|
||||||
|
logger.debug(f"set scan_seconds: {scan_seconds}")
|
||||||
|
|
||||||
# Prepare certificate of the WSS server
|
# Prepare certificate of the WSS server
|
||||||
gencert.prep_cert()
|
gencert.prep_cert()
|
||||||
|
2
setup.py
2
setup.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="pyscrlink",
|
name="pyscrlink",
|
||||||
version="0.2.3",
|
version="0.2.4",
|
||||||
author="Shin'ichiro Kawasaki",
|
author="Shin'ichiro Kawasaki",
|
||||||
author_email='kawasaki@juno.dti.ne.jp',
|
author_email='kawasaki@juno.dti.ne.jp',
|
||||||
description='Scratch-link for Linux with Python',
|
description='Scratch-link for Linux with Python',
|
||||||
|
Reference in New Issue
Block a user