mirror of
https://github.com/kawasaki/pyscrlink.git
synced 2025-09-06 17:50:20 +02:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e6d63a5c97 | ||
|
6f7da4f720 | ||
|
32da72492f | ||
|
1af4baa7d6 | ||
|
39587b497b | ||
|
f506bbcaab | ||
|
9265086b12 | ||
|
58a60c94db | ||
|
ea1109cee2 | ||
|
f4af270fbc | ||
|
1b92af0f0a | ||
|
c206f7a5c5 |
37
README.md
37
README.md
@@ -13,7 +13,7 @@ and its python interfaces [pybluez](https://github.com/pybluez/pybluez) to
|
||||
handle Bluetooth, and [bluepy](https://github.com/IanHarvey/bluepy) to handle
|
||||
Bluetooth Low Energy (BLE) connections. It has been reported that pyscrlink
|
||||
connects Scratch 3.0 with micro:bit, LEGO Mindstorms EV3, LEGO WeDo, LEGO
|
||||
Boost and Intelino Smart Train.
|
||||
Boost, Intelino Smart Train and toio.
|
||||
|
||||
To use websockets, pyscrlink requires python version 3.6 or later. If your
|
||||
system has python older than version 3.6, install newer version. If your
|
||||
@@ -42,7 +42,7 @@ Linux distros:
|
||||
* elementary OS 5.1 Hera
|
||||
|
||||
Browsers:
|
||||
* FireFox
|
||||
* Firefox
|
||||
* Chromium
|
||||
|
||||
It was reported that pyscrlink (former bluepy-scratch-link) working with
|
||||
@@ -51,13 +51,15 @@ following devices and Linux distros.
|
||||
Devices:
|
||||
* LEGO Mindstorm EV3 by @chrisglencross
|
||||
* 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
|
||||
* toio by @shimodash
|
||||
|
||||
Linux distros:
|
||||
* Raspbian by @chrisglencross
|
||||
* Ubuntu 16.04 @jacquesdt
|
||||
* Ubuntu Studio 20.04 @miguev
|
||||
* Debian 11 @n3storm
|
||||
|
||||
Installation
|
||||
------------
|
||||
@@ -86,7 +88,8 @@ Installation
|
||||
$ 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
|
||||
@@ -99,7 +102,7 @@ Installation
|
||||
5. For micro:bit, install Scratch-link hex on your device.
|
||||
|
||||
* Download and unzip the [micro:bit Scratch Hex file](https://downloads.scratch.mit.edu/microbit/scratch-microbit-1.1.0.hex.zip).
|
||||
* Flash the micro:bit over USB with the Scratch .Hex File, you will see the
|
||||
* Flash the micro:bit over USB with the Scratch Hex File, you will see the
|
||||
five character name of the micro:bit scroll across the screen such as
|
||||
'zo9ev'.
|
||||
|
||||
@@ -158,8 +161,10 @@ Usage
|
||||
```sh
|
||||
$ 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 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.)
|
||||
* Access [Scratch 3.0](https://scratch.mit.edu/) and create your project.
|
||||
* Select the "Add Extension" button.
|
||||
@@ -187,6 +192,12 @@ In Case You Fail to Connect
|
||||
3. If scratch_link.py says "failed to connect to BT device: [Errno 13] Permission denied",
|
||||
make sure to pair the bluetooth device to your PC before connecting to Scratch.
|
||||
|
||||
4. To connect to multiple devices at the same time, make all the target devices
|
||||
ready for scan at the first device scan. This is important for toio. The toio
|
||||
allows a single project to connect to two toio devices.
|
||||
* When the second device was prepared after the first device was connected, device scan can not find the second device.
|
||||
* To scan and find the second device, disconnect connections for the first device beforehand.
|
||||
|
||||
Issus Reporting
|
||||
---------------
|
||||
|
||||
@@ -195,6 +206,20 @@ Please file issues to [GitHub issue tracker](https://github.com/kawasaki/pyscrli
|
||||
Releases
|
||||
--------
|
||||
|
||||
Release 0.2.4
|
||||
|
||||
* Added -s option to specify BLE scan duration
|
||||
* Improved README.md
|
||||
|
||||
Release 0.2.3
|
||||
|
||||
* Fixed eternal loop caused by hostname resolve failure
|
||||
|
||||
Release 0.2.2
|
||||
|
||||
* Supported multiple device connections for toio
|
||||
* Improved session closure handling
|
||||
|
||||
Release 0.2.1
|
||||
|
||||
* Added libglib to required package list in README.md
|
||||
|
@@ -8,12 +8,14 @@ import asyncio
|
||||
import pathlib
|
||||
import ssl
|
||||
import websockets
|
||||
import socket
|
||||
import json
|
||||
import base64
|
||||
import logging
|
||||
import sys
|
||||
import signal
|
||||
import traceback
|
||||
import argparse
|
||||
|
||||
# for Bluetooth (e.g. Lego EV3)
|
||||
import bluetooth
|
||||
@@ -42,6 +44,9 @@ logger.setLevel(logLevel)
|
||||
logger.addHandler(handler)
|
||||
logger.propagate = False
|
||||
|
||||
HOSTNAME="device-manager.scratch.mit.edu"
|
||||
scan_seconds=10.0
|
||||
|
||||
class Session():
|
||||
"""Base class for BTSession and BLESession"""
|
||||
def __init__(self, websocket, loop):
|
||||
@@ -343,6 +348,11 @@ class BLESession(Session):
|
||||
|
||||
MAX_SCANNER_IF = 3
|
||||
|
||||
found_devices = []
|
||||
nr_connected = 0
|
||||
scan_lock = threading.RLock()
|
||||
scan_started = False
|
||||
|
||||
class BLEThread(threading.Thread):
|
||||
"""
|
||||
Separated thread to control notifications to Scratch.
|
||||
@@ -358,7 +368,7 @@ class BLESession(Session):
|
||||
logger.debug("loop in BLE thread")
|
||||
if self.session.status == self.session.DISCOVERY:
|
||||
logger.debug("send out found devices")
|
||||
devices = self.session.found_devices
|
||||
devices = BLESession.found_devices
|
||||
for d in devices:
|
||||
params = { 'rssi': d.rssi }
|
||||
params['peripheralId'] = devices.index(d)
|
||||
@@ -418,7 +428,6 @@ class BLESession(Session):
|
||||
def __init__(self, websocket, loop):
|
||||
super().__init__(websocket, loop)
|
||||
self.status = self.INITIAL
|
||||
self.found_devices = []
|
||||
self.device = None
|
||||
self.deviceName = None
|
||||
self.perip = None
|
||||
@@ -426,6 +435,13 @@ class BLESession(Session):
|
||||
self.characteristics_cache = []
|
||||
|
||||
def close(self):
|
||||
if self.status == self.CONNECTED:
|
||||
BLESession.nr_connected -= 1
|
||||
logger.info(f"BLE session disconnected")
|
||||
logger.debug(f"BLE session connected={BLESession.nr_connected}")
|
||||
if BLESession.nr_connected == 0:
|
||||
logger.info("all BLE sessions disconnected")
|
||||
BLESession.scan_started = False
|
||||
self.status = self.DONE
|
||||
if self.perip:
|
||||
logger.info("disconnect from the BLE peripheral: "
|
||||
@@ -480,6 +496,31 @@ class BLESession(Session):
|
||||
# ref: https://github.com/LLK/scratch-link/blob/develop/Documentation/BluetoothLE.md
|
||||
return False
|
||||
|
||||
def _scan_devices(self, params):
|
||||
global scan_seconds
|
||||
if BLESession.nr_connected > 0:
|
||||
return len(BLESession.found_devices) > 0
|
||||
found = False
|
||||
with BLESession.scan_lock:
|
||||
if not BLESession.scan_started:
|
||||
BLESession.scan_started = True
|
||||
BLESession.found_devices.clear()
|
||||
for i in range(self.MAX_SCANNER_IF):
|
||||
scanner = Scanner(iface=i)
|
||||
try:
|
||||
logger.debug(f"start BLE scan: {scan_seconds} seconds")
|
||||
devices = scanner.scan(scan_seconds)
|
||||
for dev in devices:
|
||||
if self.matches(dev, params['filters']):
|
||||
BLESession.found_devices.append(dev)
|
||||
found = True
|
||||
logger.debug(f"BLE device found with iface #{i}");
|
||||
except BTLEManagementError as e:
|
||||
logger.debug(f"BLE iface #{i}: {e}");
|
||||
else:
|
||||
found = len(BLESession.found_devices) > 0
|
||||
return found
|
||||
|
||||
def _get_service(self, service_id):
|
||||
with self.lock:
|
||||
service = self.perip.getServiceByUUID(UUID(service_id))
|
||||
@@ -532,26 +573,17 @@ class BLESession(Session):
|
||||
logger.error("e.g. $ bluepy_helper_cap")
|
||||
logger.error("e.g. $ sudo bluepy_helper_cap.py")
|
||||
sys.exit(1)
|
||||
found_ifaces = 0
|
||||
for i in range(self.MAX_SCANNER_IF):
|
||||
scanner = Scanner(iface=i)
|
||||
try:
|
||||
devices = scanner.scan(10.0)
|
||||
for dev in devices:
|
||||
if self.matches(dev, params['filters']):
|
||||
self.found_devices.append(dev)
|
||||
found_ifaces += 1
|
||||
logger.debug(f"BLE device found with iface #{i}");
|
||||
except BTLEManagementError as e:
|
||||
logger.debug(f"BLE iface #{i}: {e}");
|
||||
|
||||
if found_ifaces == 0:
|
||||
found = self._scan_devices(params)
|
||||
if not found:
|
||||
if BLESession.nr_connected > 0:
|
||||
err_msg = "Can not scan BLE devices. Disconnect other sessions."
|
||||
elif len(BLESession.found_devices) == 0:
|
||||
err_msg = "Can not scan BLE devices. Check BLE controller."
|
||||
logger.error(err_msg);
|
||||
res["error"] = { "message": err_msg }
|
||||
self.status = self.DONE
|
||||
|
||||
if len(self.found_devices) == 0 and not err_msg:
|
||||
if len(BLESession.found_devices) == 0 and not err_msg:
|
||||
err_msg = (f"No BLE device found: {params['filters']}. "
|
||||
"Check BLE device.")
|
||||
res["error"] = { "message": err_msg }
|
||||
@@ -565,11 +597,12 @@ class BLESession(Session):
|
||||
|
||||
elif self.status == self.DISCOVERY and method == 'connect':
|
||||
logger.debug("connecting to the BLE device")
|
||||
self.device = self.found_devices[params['peripheralId']]
|
||||
self.device = BLESession.found_devices[params['peripheralId']]
|
||||
self.deviceName = self.device.getValueText(0x9) or self.device.getValueText(0x8)
|
||||
try:
|
||||
self.perip = Peripheral(self.device)
|
||||
logger.info(f"connected to the BLE peripheral: {self.deviceName}")
|
||||
BLESession.found_devices.remove(self.device)
|
||||
except BTLEDisconnectError as e:
|
||||
logger.error(f"failed to connect to the BLE device \"{self.deviceName}\": {e}")
|
||||
self.status = self.DONE
|
||||
@@ -577,6 +610,8 @@ class BLESession(Session):
|
||||
if self.perip:
|
||||
res["result"] = None
|
||||
self.status = self.CONNECTED
|
||||
BLESession.nr_connected += 1
|
||||
logger.debug(f"BLE session connected={BLESession.nr_connected}")
|
||||
self.delegate = self.BLEDelegate(self)
|
||||
self.perip.withDelegate(self.delegate)
|
||||
self._cache_characteristics()
|
||||
@@ -669,7 +704,8 @@ async def ws_handler(websocket, path):
|
||||
await session.handle()
|
||||
except Exception as e:
|
||||
logger.error(f"Failure in session for web socket path: {path}")
|
||||
logger.error(e)
|
||||
logger.error(f"{type(e).__name__}: {e}")
|
||||
session.close()
|
||||
|
||||
def stack_trace():
|
||||
print("in stack_trace")
|
||||
@@ -686,18 +722,20 @@ def stack_trace():
|
||||
print(line)
|
||||
|
||||
def main():
|
||||
opts = [opt for opt in sys.argv[1:] if opt.startswith("-")]
|
||||
if "-h" in opts:
|
||||
print((f"Usage: {sys.argv[0]} [OPTS]\n"
|
||||
"OPTS:\t-h Show this help.\n"
|
||||
"\t-d Print debug messages."
|
||||
))
|
||||
sys.exit(1)
|
||||
elif "-d" in opts:
|
||||
global scan_seconds
|
||||
parser = argparse.ArgumentParser(description='start Scratch-link')
|
||||
parser.add_argument('-d', '--debug', action='store_true',
|
||||
help='print debug messages')
|
||||
parser.add_argument('-s', '--scan_seconds', type=float, default=10.0,
|
||||
help='specifiy duration to scan BLE devices in seconds')
|
||||
args = parser.parse_args()
|
||||
if args.debug:
|
||||
print("Print debug messages")
|
||||
logLevel = logging.DEBUG
|
||||
handler.setLevel(logLevel)
|
||||
logger.setLevel(logLevel)
|
||||
scan_seconds = args.scan_seconds
|
||||
logger.debug(f"set scan_seconds: {scan_seconds}")
|
||||
|
||||
# Prepare certificate of the WSS server
|
||||
gencert.prep_cert()
|
||||
@@ -709,7 +747,7 @@ def main():
|
||||
ssl_context.load_cert_chain(localhost_cer, localhost_key)
|
||||
|
||||
start_server = websockets.serve(
|
||||
ws_handler, "device-manager.scratch.mit.edu", 20110, ssl=ssl_context
|
||||
ws_handler, HOSTNAME, 20110, ssl=ssl_context
|
||||
)
|
||||
|
||||
while True:
|
||||
@@ -720,7 +758,13 @@ def main():
|
||||
except KeyboardInterrupt as e:
|
||||
stack_trace()
|
||||
break
|
||||
except socket.gaierror as e:
|
||||
logger.error(f"{type(e).__name__}: {e}")
|
||||
logger.info(f"Check internet connection to {HOSTNAME}. If not "
|
||||
f"available, add '127.0.0.1 {HOSTNAME}' to /etc/hosts.")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"{type(e).__name__}: {e}")
|
||||
logger.info("Restarting scratch-link...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
Reference in New Issue
Block a user