1
0
mirror of https://github.com/undera/pylgbst.git synced 2020-11-18 19:37:26 -08:00

Compare commits

..

No commits in common. "master" and "0.5" have entirely different histories.
master ... 0.5

57 changed files with 1535 additions and 5952 deletions

6
.gitignore vendored
View File

@ -1,7 +1,3 @@
.idea .idea
*.iml *.iml
*.pyc *.pyc
build
*.avi
test_real.py
.vscode/settings.json

View File

@ -1,37 +1,23 @@
sudo: false sudo: false
language: python language: python
python:
- 3.6 matrix:
- 3.8 include:
- os: linux
python: 2.7
- os: linux
python: 3.5
addons: addons:
apt: apt:
packages: packages:
- libboost-python-dev - libboost-python-dev
- libboost-thread-dev - libboost-thread-dev
- libbluetooth-dev - libbluetooth-dev
- libglib2.0-dev
- libglib2.0-dev
- libdbus-1-dev
- libdbus-glib-1-dev
- libgirepository-1.0-1
- libgirepository1.0-dev
- bluez
install: install:
- wget https://github.com/labapart/gattlib/releases/download/dev/gattlib_dbus_0.2-dev_x86_64.deb - pip install codecov gattlib
- sudo dpkg -i gattlib_dbus_0.2-dev_x86_64.deb script: coverage run --source=. `which nosetests` . --nocapture
- pip install codecov codacy-coverage pytest pygatt gatt pexpect bluepy bleak packaging dbus-python pygobject
- pip install --upgrade attrs
env:
- READTHEDOCS=True
script:
- coverage run --omit="examples/*" --source=. -m pytest -v --ignore=examples --log-level=INFO tests
after_success: after_success:
- coverage report -m - coverage report -m
- coverage xml - codecov
- codecov
- python-codacy-coverage -r coverage.xml

314
README.md
View File

@ -1,122 +1,304 @@
# Python library to interact with Move Hub / PoweredUp Hubs # Python library to interact with Move Hub
_Move Hub is central controller block of [LEGO® Boost Robotics Set](https://www.lego.com/themes/boost)._ _Move Hub is central controller block of [LEGO® Boost Robotics Set](https://www.lego.com/en-us/boost)._
In fact, Move Hub is just a Bluetooth hardware piece, and all manipulations with it are made by commands passed through Bluetooth Low Energy (BLE) wireless protocol. One of the ways to issue these commands is to write Python program using this library. In fact, Move Hub is just Bluetooth hardware, all manipulations are done with commands passed through Bluetooth Low Energy (BLE) wireless protocol. One of ways to issue these commands is to write Python program using this library.
The best way to start is to look into [`demo.py`](examples/demo.py) file, and run it (assuming you have installed library). Best way to start is to look into [`demo.py`](examples/demo.py) file, and run it (assuming you have installed library).
If you have Vernie assembled, you might run scripts from [`examples/vernie`](examples/vernie) directory. If you have Vernie assembled, you might run scripts from [`vernie`](examples/vernie/) directory.
## Demonstrational Videos Demonstrational videos:
[![Vernie Programmed](http://img.youtube.com/vi/oqsmgZlVE8I/0.jpg)](http://www.youtube.com/watch?v=oqsmgZlVE8I) [![Demonstrational video](http://img.youtube.com/vi/oqsmgZlVE8I/0.jpg)](http://www.youtube.com/watch?v=oqsmgZlVE8I)
[![Laser Engraver](http://img.youtube.com/vi/ZbKmqVBBMhM/0.jpg)](https://youtu.be/ZbKmqVBBMhM) [![Demonstrational video](http://img.youtube.com/vi/ZbKmqVBBMhM/0.jpg)](https://youtu.be/ZbKmqVBBMhM)
[![Color Sorter](http://img.youtube.com/vi/829RKT8v8M0/0.jpg)](https://youtu.be/829RKT8v8M0)
[![Face Tracker](http://img.youtube.com/vi/WUOa3j-6XfI/0.jpg)](https://youtu.be/WUOa3j-6XfI)
[![Color Pin Bot](http://img.youtube.com/vi/QY6nRYXQw_U/0.jpg)](https://youtu.be/QY6nRYXQw_U)
[![BB-8 Joystick](http://img.youtube.com/vi/55kE9I4IQSU/0.jpg)](https://youtu.be/55kE9I4IQSU)
## Features ## Features
- auto-detect and connect to [Move Hub](docs/MoveHub.md) device - auto-detect and connect to Move Hub device
- auto-detects [peripheral devices](docs/Peripherals.md) connected to Hub - auto-detects peripheral devices connected to Hub
- constant, angled and timed movement for [motors](docs/Motor.md), rotation sensor subscription - constant, angled and timed movement for motors, rotation sensor subscription
- [vision sensor](docs/VisionSensor.md): several modes to measure distance, color and luminosity - color & distance sensor: several modes to measure distance, color and luminosity
- [tilt sensor](docs/TiltSensor.md) subscription: 2 axis, 3 axis, bump detect modes - tilt sensor subscription: 2 axis, 3 axis, bump detect modes
- [RGB LED](docs/LED.md) color change - LED color change
- [push button](docs/MoveHub.md#push-button) status subscription - push button status subscription
- [battery voltage and current](docs/VoltageCurrent.md) subscription available - battery voltage subscription available
- permanent Bluetooth connection server for faster debugging - permanent Bluetooth connection server for faster debugging
## Usage ## Usage
_Please note that this library requires one of Bluetooth backend libraries to be installed, please read section [here](#bluetooth-backend-prerequisites) for details._
Install library like this: Install library like this:
```bash ```bash
pip install -U pylgbst pip install https://github.com/undera/pylgbst/archive/0.5.tar.gz
``` ```
Then instantiate MoveHub object and start invoking its methods. Following is example to just print peripherals detected on Hub: Then instantiate MoveHub object and start invoking its methods. Following is example to just print peripherals detected on Hub:
```python ```python
from pylgbst.hub import MoveHub from pylgbst import MoveHub
hub = MoveHub() hub = MoveHub()
for device in hub.peripherals: for device in hub.devices:
print(device) print(device)
``` ```
Each peripheral kind has own methods to do actions and/or get sensor data. See [features](#features) list for individual doc pages. ### Controlling Motors
## Bluetooth Backend Prerequisites MoveHub provides motors via following fields:
- `motor_A` - port A
- `motor_B` - port B
- `motor_AB` - motor group of A+B manipulated together
- `motor_external` - external motor attached to port C or D
You have following options to install as Bluetooth backend (some of them might require `sudo` on Linux): Methods to activate motors are:
- `constant(speed_primary, speed_secondary)` - enables motor with specified speed forever
- `timed(time, speed_primary, speed_secondary)` - enables motor with specified speed for `time` seconds, float values accepted
- `angled(angle, speed_primary, speed_secondary)` - makes motor to rotate to specified angle, `angle` value is integer degrees, can be negative and can be more than 360 for several rounds
- `stop()` - stops motor at once, it is equivalent for `constant(0)`
- `pip install pygatt` - [pygatt](https://github.com/peplin/pygatt) lib, works on both Windows and Linux Parameter `speed_secondary` is used when it is motor group of `motor_AB` running together. By default, `speed_secondary` equals `speed_primary`. Speed values range is `-1.0` to `1.0`, float values. _Note: In group angled mode, total rotation angle is distributed across 2 motors according to motor speeds ratio._
- `pip install gatt` - [gatt](https://github.com/getsenic/gatt-python) lib, supports Linux, does not work on Windows
- `pip install gattlib` - [gattlib](https://bitbucket.org/OscarAcena/pygattlib) - supports Linux, does not work on Windows, requires `sudo`
- `pip install bluepy` - [bluepy](https://github.com/IanHarvey/bluepy) lib, supports Linux, including Raspbian, which allows connection to the hub from the Raspberry PI
- `pip install bleak` - [bleak](https://github.com/hbldh/bleak) lib, supports Linux/Windows/MacOS
Running on Windows requires [Bluegiga BLED112 Bluetooth Smart Dongle](https://www.silabs.com/products/wireless/bluetooth/bluetooth-low-energy-modules/bled112-bluetooth-smart-dongle) hardware piece, because no other hardware currently works on Windows with Python+BLE. All these methods are synchronous by default, means method does not return untill it gets confirmation from Hub that command has completed. You can pass `async=True` parameter to any of methods to switch into asynchronous, which means command will return immediately, without waiting for rotation to complete. Be careful with asynchronous calls, as they make Hub to stop reporting synchronizing statuses.
_Please let author know if you have discovered any compatibility/preprequisite details, so we will update this section to help future users_ An example:
Depending on backend type, you might need Linux `sudo` to be used when running Python.
### Bluetooth Connection Options
There is an optional parameter for `MoveHub` class constructor, accepting instance of `Connection` object. By default, it will try to use whatever `get_connection_auto()` returns. You have several options to manually control that:
- use `get_connection_auto()` to attempt backend auto-detection
- use `get_connection_bluegiga()` - if you use BlueGiga Adapter (`pygatt` library prerequisite)
- use `get_connection_gatt()` - if you use Gatt Backend on Linux (`gatt` library prerequisite)
- use `get_connection_gattool()` - if you use GattTool Backend on Linux (`pygatt` library prerequisite)
- use `get_connection_gattlib()` - if you use GattLib Backend on Linux (`gattlib` library prerequisite)
- use `get_connection_bluepy()` - if you use Bluepy backend on Linux/Raspbian (`bluepy` library prerequisite)
- use `get_connection_bleak()` - if you use Bleak backend (`bleak` library prerequisite)
- pass instance of `DebugServerConnection` if you are using [Debug Server](#debug-server) (more details below).
All the functions above have optional arguments to specify adapter name and Hub name (or mac address). Please take a look at functions source code for details.
If you want to specify name for Bluetooth interface to use on local computer, you can pass that to class or function of getting a connection. Then pass connection object to `MoveHub` constructor. Like this:
```python ```python
from pylgbst.hub import MoveHub from pylgbst import MoveHub
from pylgbst import get_connection_gatt import time
hub = MoveHub()
hub.motor_A.timed(0.5, 0.8)
hub.motor_A.timed(0.5, -0.8)
hub.motor_B.angled(90, 0.8)
hub.motor_B.angled(-90, 0.8)
hub.motor_AB.timed(1.5, 0.8, -0.8)
hub.motor_AB.angled(90, 0.8, -0.8)
hub.motor_external.constant(0.2)
time.sleep(2)
hub.motor_external.stop()
```
### Motor Rotation Sensors
Any motor allows to subscribe to its rotation sensor. Two sensor modes are available: rotation angle (`EncodedMotor.SENSOR_ANGLE`) and rotation speed (`EncodedMotor.SENSOR_SPEED`). Example:
```python
from pylgbst import MoveHub, EncodedMotor
import time
def callback(angle):
print("Angle: %s" % angle)
hub = MoveHub()
hub.motor_A.subscribe(callback, mode=EncodedMotor.SENSOR_ANGLE)
time.sleep(60) # rotate motor A
hub.motor_A.unsubscribe(callback)
```
### Tilt Sensor
MoveHub's internal tilt sensor is available through `tilt_sensor` field. There are several modes to subscribe to sensor, providing 2-axis, 3-axis and bump detect data.
An example:
```python
from pylgbst import MoveHub, TiltSensor
import time
def callback(pitch, roll, yaw):
print("Pitch: %s / Roll: %s / Yaw: %s" % (pitch, roll, yaw))
hub = MoveHub()
hub.tilt_sensor.subscribe(callback, mode=TiltSensor.MODE_3AXIS_FULL)
time.sleep(60) # turn MoveHub block in different ways
hub.tilt_sensor.unsubscribe(callback)
```
`TiltSensor` sensor mode constants:
- `MODE_2AXIS_SIMPLE` - use `callback(state)` for 2-axis simple state detect
- `MODE_2AXIS_FULL` - use `callback(roll, pitch)` for 2-axis roll&pitch degree values
- `MODE_3AXIS_SIMPLE` - use `callback(state)` for 3-axis simple state detect
- `MODE_3AXIS_FULL` - use `callback(roll, pitch)` for 2-axis roll&pitch degree values
- `MODE_BUMP_COUNT` - use `callback(count)` to detect bumps
There are tilt sensor constants for "simple" states, for 2-axis mode their names are also available through `TiltSensor.DUO_STATES`:
- `DUO_HORIZ` - "HORIZONTAL"
- `DUO_DOWN` - "DOWN"
- `DUO_LEFT` - "LEFT"
- `DUO_RIGHT` - "RIGHT"
- `DUO_UP` - "UP"
For 3-axis simple mode map name is `TiltSensor.TRI_STATES` with values:
- `TRI_BACK` - "BACK"
- `TRI_UP` - "UP"
- `TRI_DOWN` - "DOWN"
- `TRI_LEFT` - "LEFT"
- `TRI_RIGHT` - "RIGHT"
- `TRI_FRONT` - "FRONT"
### Color & Distance Sensor
Field named `color_distance_sensor` holds instance of `ColorDistanceSensor`, if one is attached to MoveHub. Sensor has number of different modes to subscribe.
Colors that are detected are part of `COLORS` map (see [LED](#led) section). Only several colors are possible to detect: `BLACK`, `BLUE`, `CYAN`, `YELLOW`, `RED`, `WHITE`. Sensor does its best to detect best color, but only works when sample is very close to sensor.
Distance works in range of 0-10 inches, with ability to measure last inch in higher detail.
Simple example of subscribing to sensor:
```python
from pylgbst import MoveHub, ColorDistanceSensor
import time
def callback(clr, distance):
print("Color: %s / Distance: %s" % (clr, distance))
hub = MoveHub()
hub.color_distance_sensor.subscribe(callback, mode=ColorDistanceSensor.COLOR_DISTANCE_FLOAT)
time.sleep(60) # play with sensor while it waits
hub.color_distance_sensor.unsubscribe(callback)
```
Subscription mode constants in class `ColorDistanceSensor` are:
- `COLOR_DISTANCE_FLOAT` - default mode, use `callback(color, distance)` where `distance` is float value in inches
- `COLOR_ONLY` - use `callback(color)`
- `DISTANCE_INCHES` - use `callback(color)` measures distance in integer inches count
- `COUNT_2INCH` - use `callback(count)` - it counts crossing distance ~2 inches in front of sensor
- `DISTANCE_HOW_CLOSE` - use `callback(value)` - value of 0 to 255 for 30 inches, larger with closer distance
- `DISTANCE_SUBINCH_HOW_CLOSE` - use `callback(value)` - value of 0 to 255 for 1 inch, larger with closer distance
- `LUMINOSITY` - use `callback(luminosity)` where `luminosity` is float value from 0 to 1
- `OFF1` and `OFF2` - seems to turn sensor LED and notifications off
- `STREAM_3_VALUES` - use `callback(val1, val2, val3)`, sends some values correlating to distance, not well understood at the moment
Tip: laser pointer pointing to sensor makes it to trigger distance sensor
### LED
`MoveHub` class has field `led` to access color LED near push button. To change its color, use `set_color(color)` method.
You can obtain colors are present as constants `COLOR_*` and also a map of available color-to-name as `COLORS`. There are 12 color values, including `COLOR_BLACK` and `COLOR_NONE` which turn LED off.
Additionally, you can subscribe to LED color change events, using callback function as shown in example below.
```python
from pylgbst import MoveHub, COLORS, COLOR_NONE, COLOR_RED
import time
def callback(clr):
print("Color has changed: %s" % clr)
hub = MoveHub()
hub.led.subscribe(callback)
hub.led.set_color(COLOR_RED)
for color in COLORS:
hub.led.set_color(color)
time.sleep(0.5)
hub.led.set_color(COLOR_NONE)
hub.led.unsubscribe(callback)
```
Tip: blinking orange color of LED means battery is low.
### Push Button
`MoveHub` class has field `button` to subscribe to button press and release events.
Note that `Button` class is not real `Peripheral`, as it has no port and not listed in `devices` field of Hub. Still, subscribing to button is done usual way:
```python
from pylgbst import MoveHub
def callback(is_pressed):
print("Btn pressed: %s" % is_pressed)
hub = MoveHub()
hub.button.subscribe(callback)
```
### Power Voltage & Battery
`MoveHub` class has field `voltage` to subscribe to battery voltage status. Callback accepts single parameter with current value. The range of values is float between `0` and `1.0`. Every time data is received, value is also written into `last_value` field of `Voltage` object. Values less than `0.2` are known as lowest values, when unit turns off.
```python
from pylgbst import MoveHub
import time
def callback(value):
print("Voltage: %s" % value)
hub = MoveHub()
hub.voltage.subscribe(callback)
time.sleep(1)
print ("Value: " % hub.voltage.last_value)
```
### General Notes
#### Bluetooth Connection
There is optional parameter for `MoveHub` class constructor, accepting instance of `Connection` object. By default, it uses instance of `BLEConnection` to connect directly to Move Hub. You can specify instance of `DebugServerConnection` if you are using Debug Server (more details below).
If you want to specify name for Bluetooth interface to use on local computer, create instance of `BLEConnection` and call `connect(if_name)` method of connection. Then pass it to `MoveHub` constructor. Like this:
```python
from pylgbst import BLEConnection, MoveHub
conn = BLEConnection()
conn.connect("hci1")
conn = get_connection_gatt(hub_mac='AA:BB:CC:DD:EE:FF')
hub = MoveHub(conn) hub = MoveHub(conn)
``` ```
#### Devices Detecting
As part of instantiating process, `MoveHub` waits up to 1 minute for all builtin devices to appear, such as motors on ports A and B, tilt sensor, button and battery. This not guarantees that external motor and/or color sensor will be present right after `MoveHub` instantiated. Usually, sleeping for couple of seconds gives it enough time to detect everything.
#### Subscribing to Sensors
Each sensor usually has several different "subscription modes", differing with callback parameters and value semantics.
There is optional `granularity` parameter for each subscription call, by default it is `1`. This parameter tells Hub when to issue sensor data notification. Value of notification has to change greater or equals to `granularity` to issue notification. This means that specifying `0` will cause it to constantly send notifications, and specifying `5` will cause less frequent notifications, only when values change for more than `5` (inclusive).
It is possible to subscribe with multiple times for the same sensor. Only one, very last subscribe mode is in effect, with many subscriber callbacks allowed to receive notifications.
Good practice for any program is to unsubscribe from all sensor subscriptions before ending, especially when used with `DebugServer`.
## Debug Server ## Debug Server
Running debug server opens permanent BLE connection to Hub and listening on TCP port for communications. This avoids the need to re-start Hub all the time. Running debug server opens permanent BLE connection to Hub and listening on TCP port for communications. This avoids the need to re-start Hub all the time.
There is `DebugServerConnection` class that you can use with it, instead of `BLEConnection`. There is `DebugServerConnection` class that you can use with it, instead of `BLEConnection`.
Starting debug server is done like this (you may need to run it with `sudo`, depending on your BLE backend): Starting debug server is done like this:
```bash ```bash
python -c "import logging; logging.basicConfig(level=logging.DEBUG); \ sudo python -c "from pylgbst.comms import *; \
import pylgbst; pylgbst.start_debug_server()" import logging; logging.basicConfig(level=logging.DEBUG); \
DebugServer(BLEConnection().connect()).start()"
``` ```
Then push green button on MoveHub, so permanent BLE connection will be established. Then push green button on MoveHub, so permanent BLE connection will be established.
## Roadmap & TODO ## Roadmap & TODO
- validate operations with other Hub types (train, PUP etc)
- make connections to detect hub by UUID instead of name
- document all API methods - document all API methods
- make sure unit tests cover all important code
- make debug server to re-establish BLE connection on loss - make debug server to re-establish BLE connection on loss
## Links ## Links
- https://github.com/LEGO/lego-ble-wireless-protocol-docs - true docs for LEGO BLE protocol - https://github.com/JorgePe/BOOSTreveng - source of protocol knowledge
- https://github.com/JorgePe/BOOSTreveng - initial source of protocol knowledge
- https://github.com/nathankellenicki/node-poweredup - JavaScript version of library
- https://github.com/spezifisch/sphero-python/blob/master/BB8joyDrive.py - example with another approach to bluetooth libs - https://github.com/spezifisch/sphero-python/blob/master/BB8joyDrive.py - example with another approach to bluetooth libs
- https://github.com/virantha/bricknil - for the lovers of async Python, alternative implementation of library to control PoweredUp Hubs
Some things around visual programming:
- https://github.com/RealTimeWeb/blockpy
- https://ru.wikipedia.org/wiki/App_Inventor
- https://en.wikipedia.org/wiki/Blockly

View File

@ -1,26 +0,0 @@
# Generic Powered Up Hub
## Connecting to Hub via Bluetooth
## Accessing Peripherals
## Sending and Receiving Low-Level Messages
`Hub.send(msg)`
add_message_handler
## Use Disconnect in `finally`
It is recommended to make sure `disconnect()` method is called on connection object after you have finished your program. This ensures Bluetooth subsystem is cleared and avoids problems for subsequent re-connects of MoveHub. The best way to do that in Python is to use `try ... finally` clause:
```python
from pylgbst import get_connection_auto
from pylgbst.hub import Hub
conn = get_connection_auto() # ! don't put this into `try` block
try:
hub = Hub(conn)
finally:
conn.disconnect()
```
Additionally, hub has `Hub.disconnect()` and `Hub.switch_off()` methods to call corresponding commands.

View File

@ -1,31 +0,0 @@
### LED
`MoveHub` class has field `led` to access color LED near push button. To change its color, use `set_color(color)` method.
You can obtain colors are present as constants `COLOR_*` and also a map of available color-to-name as `COLORS`. There are 12 color values, including `COLOR_BLACK` and `COLOR_NONE` which turn LED off.
Additionally, you can subscribe to LED color change events, using callback function as shown in example below.
```python
from pylgbst.hub import MoveHub, COLORS, COLOR_NONE, COLOR_RED
import time
def callback(clr):
print("Color has changed: %s" % clr)
hub = MoveHub()
hub.led.subscribe(callback)
hub.led.set_color(COLOR_RED)
for color in COLORS:
hub.led.set_color(color)
time.sleep(0.5)
hub.led.set_color(COLOR_NONE)
hub.led.unsubscribe(callback)
```
Tip: blinking orange color of LED means battery is low.
Note that Vision Sensor can also be used to set its LED color into indexed colors.

View File

@ -1,55 +0,0 @@
# Motors
![](https://img.bricklink.com/ItemImage/PL/6181852.png)
## Controlling Motors
Methods to activate motors are:
- `start_speed(speed_primary, speed_secondary)` - enables motor with specified speed forever
- `timed(time, speed_primary, speed_secondary)` - enables motor with specified speed for `time` seconds, float values accepted
- `angled(angle, speed_primary, speed_secondary)` - makes motor to rotate to specified angle, `angle` value is integer degrees, can be negative and can be more than 360 for several rounds
- `stop()` - stops motor
Parameter `speed_secondary` is used when it is motor group of `motor_AB` running together. By default, `speed_secondary` equals `speed_primary`.
Speed values range is `-1.0` to `1.0`, float values. _Note: In group angled mode, total rotation angle is distributed across 2 motors according to motor speeds ratio, see official doc [here](https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#tacho-math)._
An example:
```python
from pylgbst.hub import MoveHub
import time
hub = MoveHub()
hub.motor_A.timed(0.5, 0.8)
hub.motor_A.timed(0.5, -0.8)
hub.motor_B.angled(90, 0.8)
hub.motor_B.angled(-90, 0.8)
hub.motor_AB.timed(1.5, 0.8, -0.8)
hub.motor_AB.angled(90, 0.8, -0.8)
hub.motor_external.start_speed(0.2)
time.sleep(2)
hub.motor_external.stop()
```
## Motor Rotation Sensors
Any motor allows to subscribe to its rotation sensor. Two sensor modes are available: rotation angle (`EncodedMotor.SENSOR_ANGLE`) and rotation speed (`EncodedMotor.SENSOR_SPEED`). Example:
```python
from pylgbst.hub import MoveHub, EncodedMotor
import time
def callback(angle):
print("Angle: %s" % angle)
hub = MoveHub()
hub.motor_A.subscribe(callback, mode=EncodedMotor.SENSOR_ANGLE)
time.sleep(60) # rotate motor A
hub.motor_A.unsubscribe(callback)
```

View File

@ -1,43 +0,0 @@
# Move Hub
![](http://bricker.info/images/parts/26910c01.png)
`MoveHub` is extension of generic [Powered Up Hub](GenericHub.md) class. `MoveHub` class delivers specifics of MoveHub brick, such as internal motor port names. Apart from specifics listed below, all operations on Hub are done [as usual](GenericHub.md).
## Devices Detecting
As part of instantiating process, `MoveHub` waits up to 1 minute for builtin devices to appear, such as motors on ports A and B, [tilt sensor](TiltSensor.md), [LED](LED.md) and [battery](VoltageCurrent.md). This not guarantees that external motor and/or color sensor will be present right after `MoveHub` instantiated. Usually, `time.sleep(1.0)` for couple of seconds gives it enough time to detect everything.
MoveHub provides motors via following fields:
- `motor_A` - port A motor
- `motor_B` - port B motor
- `motor_AB` - combined motors A+B manipulated together
- `motor_external` - external motor attached to port C or D
MoveHub's internal [tilt sensor](TiltSensor.md) is available through `tilt_sensor` field.
Field named `vision_sensor` holds instance of [`VisionSensor`](VisionSensor.md), if one is attached to MoveHub.
Fields named `current` and `voltage` present [corresponding sensors](VoltageCurrent.md) from Hub.
## Push Button
`MoveHub` class has field `button` to subscribe to button press and release events.
Note that `Button` class is not real `Peripheral`, as it has no port and not listed in `peripherals` field of Hub. For convenience, subscribing to button is still done usual way:
```python
from pylgbst.hub import MoveHub
def callback(is_pressed):
print("Btn pressed: %s" % is_pressed)
hub = MoveHub()
hub.button.subscribe(callback)
```
The state for button has 3 possible values:
- `0` - not released
- `1` - pressed
- `2` - pressed
It is for now unknown why Hub always issues notification with `1` and immediately with `2`, after button is pressed.

View File

@ -1,24 +0,0 @@
# Peripheral Types
Here is the list of peripheral devices that have dedicated classes in library:
- [Motors](Motor.md)
- [RGB LED](LED.md)
- [Tilt Sensor](TiltSensor.md)
- [Vision Sensor](VisionSensor.md) (color and/or distance)
- [Voltage and Current Sensors](VoltageCurrent.md)
In case device you attached to Hub is of an unknown type, it will get generic `Peripheral` class, allowing direct low-level interactions.
## Subscribing to Sensors
Each sensor usually has several different "subscription modes", differing with callback parameters and value semantics.
There is optional `granularity` parameter for each subscription call, by default it is `1`. This parameter tells Hub when to issue sensor data notification. Value of notification has to change greater or equals to `granularity` to issue notification. This means that specifying `0` will cause it to constantly send notifications, and specifying `5` will cause less frequent notifications, only when values change for more than `5` (inclusive).
It is possible to subscribe with multiple times for the same sensor. Only one, very last subscribe mode is in effect, with many subscriber callbacks allowed to receive notifications.
Good practice for any program is to unsubscribe from all sensor subscriptions before exiting, especially when used with `DebugServer`.
## Generic Perihpheral
In case you have used a peripheral that is not recognized by the library, it will be detected as generic `Peripheral` class. You still can use subscription and sensor info getting commands for it.

View File

@ -1,42 +0,0 @@
### Tilt Sensor
There are several modes to subscribe to sensor, providing 2-axis, 3-axis and bump detect data.
An example:
```python
from pylgbst.hub import MoveHub, TiltSensor
import time
def callback(pitch, roll, yaw):
print("Pitch: %s / Roll: %s / Yaw: %s" % (pitch, roll, yaw))
hub = MoveHub()
hub.tilt_sensor.subscribe(callback, mode=TiltSensor.MODE_3AXIS_SIMPLE)
time.sleep(60) # turn MoveHub block in different ways
hub.tilt_sensor.unsubscribe(callback)
```
`TiltSensor` sensor mode constants:
- `MODE_2AXIS_SIMPLE` - use `callback(state)` for 2-axis simple state detect
- `MODE_2AXIS_FULL` - use `callback(roll, pitch)` for 2-axis roll&pitch degree values
- `MODE_3AXIS_SIMPLE` - use `callback(state)` for 3-axis simple state detect
- `MODE_3AXIS_FULL` - use `callback(roll, pitch)` for 2-axis roll&pitch degree values
- `MODE_BUMP_COUNT` - use `callback(count)` to detect bumps
There are tilt sensor constants for "simple" states, for 2-axis mode their names are also available through `TiltSensor.DUO_STATES`:
- `DUO_HORIZ` - "HORIZONTAL"
- `DUO_DOWN` - "DOWN"
- `DUO_LEFT` - "LEFT"
- `DUO_RIGHT` - "RIGHT"
- `DUO_UP` - "UP"
For 3-axis simple mode map name is `TiltSensor.TRI_STATES` with values:
- `TRI_BACK` - "BACK"
- `TRI_UP` - "UP"
- `TRI_DOWN` - "DOWN"
- `TRI_LEFT` - "LEFT"
- `TRI_RIGHT` - "RIGHT"
- `TRI_FRONT` - "FRONT"

View File

@ -1,36 +0,0 @@
### Color & Distance Sensor
![](https://img.bricklink.com/ItemImage/PL/6182145.png)
Sensor has number of different modes to subscribe.
Colors that are detected are part of `COLORS` map (see [LED](#led) section). Only several colors are possible to detect: `BLACK`, `BLUE`, `CYAN`, `YELLOW`, `RED`, `WHITE`. Sensor does its best to detect best color, but only works when sample is very close to sensor.
Distance works in range of 0-10 inches, with ability to measure last inch in higher detail.
Simple example of subscribing to sensor:
```python
from pylgbst.hub import MoveHub, VisionSensor
import time
def callback(clr, distance):
print("Color: %s / Distance: %s" % (clr, distance))
hub = MoveHub()
hub.vision_sensor.subscribe(callback, mode=VisionSensor.COLOR_DISTANCE_FLOAT)
time.sleep(60) # play with sensor while it waits
hub.vision_sensor.unsubscribe(callback)
```
Subscription mode constants in class `ColorDistanceSensor` are:
- `COLOR_DISTANCE_FLOAT` - default mode, use `callback(color, distance)` where `distance` is float value in inches
- `COLOR_ONLY` - use `callback(color)`
- `DISTANCE_INCHES` - use `callback(color)` measures distance in integer inches count
- `COUNT_2INCH` - use `callback(count)` - it counts crossing distance ~2 inches in front of sensor
- `DISTANCE_HOW_CLOSE` - use `callback(value)` - value of 0 to 255 for 30 inches, larger with closer distance
- `DISTANCE_SUBINCH_HOW_CLOSE` - use `callback(value)` - value of 0 to 255 for 1 inch, larger with closer distance
- `LUMINOSITY` - use `callback(luminosity)` where `luminosity` is float value from 0 to 1
- `OFF1` and `OFF2` - seems to turn sensor LED and notifications off
- `STREAM_3_VALUES` - use `callback(val1, val2, val3)`, sends some values correlating to distance, not well understood at the moment

View File

@ -1,14 +0,0 @@
### Power Voltage & Battery
`MoveHub` class has field `voltage` to subscribe to battery voltage status. Callback accepts single parameter with current value. The range of values is float between `0` and `1.0`. Every time data is received, value is also written into `last_value` field of `Voltage` object. Values less than `0.2` are known as lowest values, when unit turns off.
```python
from pylgbst.hub import MoveHub, Voltage
def callback(value):
print("Voltage: %s" % value)
hub = MoveHub()
print ("Value L: " % hub.voltage.get_sensor_data(Voltage.VOLTAGE_L))
print ("Value S: " % hub.voltage.get_sensor_data(Voltage.VOLTAGE_S))
```

View File

@ -1,40 +0,0 @@
### Advanced button
This example shows how you can add additional functionallity to the move hub button.
It adds three new actions which you can use instead of the standard subscription to a button press:
- Click - a single quick up/down press
- Double click - a double up/down press, second click must occur within .5 secs of first one
- Long press - a press and hold on the button for > .7 secs
```python
from pylgbst.hub import MoveHub
from advancedbutton import AdvancedButton
import time
hub = MoveHub()
b = AdvancedButton(hub)
def clicked():
print("button clicked")
def pressed():
print("button pressed")
def doubleclicked():
print("button double clicked")
b.click.subscribe(clicked)
b.double_click.subscribe(doubleclicked)
b.long_press.subscribe(pressed)
time.sleep(120)
```
You can alter the timings using the two constants `DOUBLE_CLICK_TIME` and `LONG_PRESS_TIME`

View File

@ -1,73 +0,0 @@
import time
import threading
DOUBLE_CLICK_TIME = 0.5
LONG_PRESS_TIME = 0.7
class AdvancedButton:
def __init__(self, hub):
self.state = 0
self.last_pressed = 0
self.press_time = None
self.hub = hub
self.hub.button.subscribe(self.pressed)
self.click = ButtonAction()
self.double_click = ButtonAction()
self.long_press = ButtonAction()
def pressed(self, state):
if state == 2:
return
press_time = time.time()
if state == 1:
self.state = 1
self.press_time = press_time
return
if state == 0 and self.state == 1:
self.state = 0
press_duration = press_time - self.press_time
else:
return
if press_duration > LONG_PRESS_TIME:
# long press
self.long_press.notify()
return
if (press_time - self.last_pressed) < DOUBLE_CLICK_TIME:
# double click
self.last_pressed = 0
self.double_click.notify()
return
# could be first of a double click, could be single click
self.last_pressed = press_time
def timeout():
time.sleep(DOUBLE_CLICK_TIME)
if self.last_pressed == press_time:
# not clicked while sleeping
# single click
self.click.notify()
threading.Thread(target=timeout).start()
class ButtonAction:
def __init__(self):
self.subscribers = set()
def subscribe(self, callback):
self.subscribers.add(callback)
def unsubscribe(self, callback=None):
if callback in self.subscribers:
self.subscribers.remove(callback)
def notify(self):
for subscriber in self.subscribers.copy():
subscriber()

View File

@ -1,79 +0,0 @@
import logging
import time
from collections import Counter
from pylgbst.hub import MoveHub, COLOR_NONE, COLOR_BLACK, COLORS, COLOR_CYAN, COLOR_BLUE, COLOR_RED, COLOR_YELLOW, \
COLOR_WHITE
from pylgbst.peripherals import EncodedMotor
class Automata(object):
BASE_SPEED = 1.0
def __init__(self):
super(Automata, self).__init__()
self.__hub = MoveHub()
self.__hub.vision_sensor.subscribe(self.__on_sensor)
self._sensor = []
def __on_sensor(self, color, distance=-1):
logging.debug("Sensor data: %s/%s", COLORS[color], distance)
if distance <= 4:
if color not in (COLOR_NONE, COLOR_BLACK):
self._sensor.append((color, int(distance)))
logging.debug("Sensor data: %s", COLORS[color])
def feed_tape(self):
self.__hub.motor_external.angled(60, 0.5)
time.sleep(0.1)
self.__hub.motor_external.angled(60, 0.5)
time.sleep(0.1)
def get_color(self):
res = self._sensor
self._sensor = []
logging.debug("Sensor data: %s", res)
cnts = Counter([x[0] for x in res])
clr = cnts.most_common(1)[0][0] if cnts else COLOR_NONE
if clr == COLOR_CYAN:
clr = COLOR_BLUE
self.__hub.led.set_color(clr)
return clr
def left(self):
self.__hub.motor_AB.angled(290, self.BASE_SPEED, -self.BASE_SPEED, end_state=EncodedMotor.END_STATE_FLOAT)
time.sleep(0.1)
self.__hub.motor_AB.stop()
def right(self):
self.__hub.motor_AB.angled(270, -self.BASE_SPEED, self.BASE_SPEED, end_state=EncodedMotor.END_STATE_FLOAT)
time.sleep(0.1)
self.__hub.motor_AB.stop()
def forward(self):
self.__hub.motor_AB.angled(500, self.BASE_SPEED)
def backward(self):
self.__hub.motor_AB.angled(500, -self.BASE_SPEED)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
bot = Automata()
color = None
cmds = []
while color != COLOR_NONE:
bot.feed_tape()
color = bot.get_color()
logging.warning(COLORS[color])
if color == COLOR_BLUE:
bot.forward()
elif color == COLOR_RED:
bot.backward()
elif color == COLOR_YELLOW:
bot.left()
elif color == COLOR_WHITE:
bot.right()

View File

@ -1,34 +0,0 @@
from examples.automata import Automata
from pylgbst.peripherals import *
# this program is written by Sofia in early 2019
def action_by_color(color):
if color == COLOR_BLUE:
bot.forward()
if color == COLOR_RED:
bot.backward()
if color == COLOR_WHITE:
bot.right()
if color == COLOR_YELLOW:
bot.left()
def read_color():
bot.feed_tape()
color = bot.get_color()
print(COLORS[color])
return color
bot = Automata()
number = 0
color = None
while color != COLOR_NONE:
color = read_color()
number = number + 1
action_by_color(color)
print(number)

View File

@ -1,57 +0,0 @@
import logging
import sys
import time
from examples.bb8joystick import joystick
from examples.bb8joystick.bb8 import BB8
from examples.bb8joystick.joystick import Joystick
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG if 'pydevd' in sys.modules else logging.WARNING)
bb8 = BB8()
joystick = Joystick()
def set_bb_color(flag):
if flag:
bb8.color(255, 255, 255)
else:
bb8.color(0, 0, 0)
def set_heading(angle):
a = int(angle) % 360
print("Angle", a)
bb8.heading(a)
def roll(speed, direction):
print("Roll", speed, direction)
if speed < 3:
speed = 0
bb8.roll(speed, direction)
def stop(state):
if state:
print("Stop")
bb8.roll(0, 0)
else:
print("Stabilize")
bb8.color(255, 0, 255)
bb8.stabilize()
bb8.color(0, 0, 0)
try:
joystick.on_button(set_bb_color)
joystick.on_button(stop)
joystick.on_rotation(set_heading)
joystick.on_joystick(roll)
print("All set up")
time.sleep(300)
finally:
bb8.disconnect()
joystick.disconnect()

View File

@ -1,113 +0,0 @@
import asyncio
import time
import spheropy
# noinspection PyProtectedMember
from spheropy.spheropy import _ClientCommandPacket, _DEVICE_ID_CORE, _DEVICE_ID_SPHERO
class BLEInterfaceGattool(spheropy.BleInterface):
def _find_adapter(self):
adapter = spheropy.pygatt.GATTToolBackend()
adapter.start()
adapter_type = spheropy.BleInterface.BleAdapterType.PYGATT
self._adapter = adapter
self._adapter_type = adapter_type
return True
class _SpheroImproved(spheropy.Sphero):
async def connect(self, search_name=None, address=None, port=None, bluetooth_interface=None, use_ble=False,
num_retry_attempts=1):
gattool = BLEInterfaceGattool(search_name)
return await
super().connect(search_name, address, port, gattool, use_ble, num_retry_attempts)
async def sleep(self, sleeptime, reset_inactivity_timeout=True, response_timeout_in_seconds=None):
# port from https://github.com/jchadwhite/SpheroBB8-python/blob/master/BB8_driver.py#L394
command = _ClientCommandPacket(device_id=_DEVICE_ID_CORE,
command_id=0x22,
sequence_number=self._get_and_increment_command_sequence_number(),
data=[(sleeptime >> 8), (sleeptime & 0xff), 0],
wait_for_response=False,
reset_inactivity_timeout=reset_inactivity_timeout)
return await
self._send_command(command, response_timeout_in_seconds)
async def set_rotation_rate(self, rate, reset_inactivity_timeout=True, response_timeout_in_seconds=None):
# port from https://github.com/jchadwhite/SpheroBB8-python/blob/master/BB8_driver.py
command = _ClientCommandPacket(device_id=_DEVICE_ID_SPHERO,
command_id=0x03,
sequence_number=self._get_and_increment_command_sequence_number(),
data=[rate],
wait_for_response=False,
reset_inactivity_timeout=reset_inactivity_timeout)
return await
self._send_command(command, response_timeout_in_seconds)
class BB8(object):
def __init__(self, name="BB-CC13"):
self._heading = 0
# marry sync with async https://www.aeracode.org/2018/02/19/python-async-simplified/
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
print("Started to wake up BB-8...")
self._sphero = _SpheroImproved()
self._loop.run_until_complete(self._sphero.connect(num_retry_attempts=3, use_ble=True, search_name=name))
self._loop.run_until_complete(self._sphero.set_stabilization(True))
self._loop.run_until_complete(self._sphero.set_rotation_rate(1))
self.color(0, 0xFF, 0)
self.stabilize()
print("BB-8 is ready for commands")
def disconnect(self):
print("BB8 enters sleep")
self._loop.run_until_complete(self._sphero.sleep(0))
self._sphero.disconnect()
def color(self, red, green, blue):
self._wait_loop()
self._loop.run_until_complete(self._sphero.set_rgb_led(red, green, blue, wait_for_response=False))
def heading(self, heading):
self._wait_loop()
heading = 359 - heading
self._heading = heading
# self._loop.run_until_complete(self._sphero.set_heading(359 - heading))
self._loop.run_until_complete(self._sphero.roll(1, heading, spheropy.RollMode.IN_PLACE_ROTATE))
def roll(self, speed=10, direction=0):
self._wait_loop()
direction += self._heading
direction %= 360
speed = int(255 * speed / 10)
speed *= 0.75 # throttle down a bit
self._loop.run_until_complete(self._sphero.roll(int(speed), direction))
def stop(self):
self._wait_loop()
self._loop.run_until_complete(self._sphero.roll(0, 0))
def stabilize(self):
self._wait_loop()
self._loop.run_until_complete(self._sphero.self_level())
def _wait_loop(self):
while self._loop.is_running():
time.sleep(0.001)
if __name__ == '__main__':
bb8 = BB8()
bb8.color(255, 0, 0)
time.sleep(1)
bb8.color(0, 255, 0)
time.sleep(1)
bb8.color(0, 0, 255)
time.sleep(1)

View File

@ -1,145 +0,0 @@
import logging
import math
import sys
import time
from pylgbst.hub import MoveHub
def _clamp(minvalue, value, maxvalue):
return max(minvalue, min(value, maxvalue))
class Joystick(object):
RANGE_A = 40
RANGE_C = 30
def __init__(self):
super(Joystick, self).__init__()
self._on_joystick = set()
self.button_pressed = False
self._angle_A = 0
self.angle_B = 0
self._angle_C = 0
print("Starting search for Joystick...")
self._hub = MoveHub()
self._reset_sensors()
self._hub.button.subscribe(self._on_btn)
self._on_motor_a(self._on_a)
self.on_rotation(self._on_b)
self._on_motor_c(self._on_c)
print("Joystick is ready")
def disconnect(self):
print("Joystick disconnects")
self._hub.disconnect()
def _reset_sensors(self):
logging.info("Resetting motor encoders")
self._hub.motor_A.preset_encoder()
self._hub.motor_B.preset_encoder()
self._hub.motor_external.preset_encoder()
def on_button(self, callback):
"""
Notifies about button state change. ``callback(state)`` gets single bool parameter
"""
def wrapper(state):
if state in (0, 1):
callback(bool(state))
self._hub.button.subscribe(wrapper)
def _on_motor_a(self, callback):
def wrapper(angle):
logging.debug("Raw angle: %s", angle)
angle = _clamp(-self.RANGE_A, angle, self.RANGE_A)
callback(angle)
self._hub.motor_A.subscribe(wrapper)
def on_rotation(self, callback):
"""
Notifies about B motor rotation. ``callback(state)`` gets single int parameter from 0 to 359
"""
def wrapper(angle):
logging.debug("Raw angle: %s", angle)
val = angle % 360
val = val if val >= 0 else 360 - val - 1
val = 359 - val
callback(val)
self._hub.motor_B.subscribe(wrapper)
def _on_motor_c(self, callback):
def wrapper(angle):
logging.debug("Raw angle: %s", angle)
angle = _clamp(-self.RANGE_C, angle, self.RANGE_C)
callback(angle)
self._hub.motor_external.subscribe(wrapper)
def _on_btn(self, state):
self.button_pressed = bool(state)
def _on_a(self, angle):
logging.debug("A rotated: %s", angle)
self._angle_A = angle
self._calc_joystick()
def _on_b(self, angle):
logging.debug("B rotated: %s", angle)
self.angle_B = angle
def _on_c(self, angle):
logging.debug("C rotated: %s", angle)
self._angle_C = angle
self._calc_joystick()
def on_joystick(self, callback):
"""
Notifies about joystick change. ``callback(speed, direction)`` gets parameters:
- ``speed`` - int value from 0 to 10
- ``direction`` - int value from 0 to 359
"""
self._on_joystick.add(callback)
def _calc_joystick(self):
norm_a = self._angle_A / self.RANGE_A
norm_b = self._angle_C / self.RANGE_C
logging.debug("%s / %s", self._angle_A, self._angle_C)
logging.debug("%s / %s", norm_a, norm_b)
speed = math.sqrt(norm_a ** 2 + norm_b ** 2) # / math.sqrt(2)
speed = _clamp(-1.0, speed, 1.0)
maxsize = sys.maxsize if norm_a >= 0 else -sys.maxsize
direction = math.atan(norm_a / norm_b if norm_b else maxsize)
direction *= 180 / math.pi
if norm_a >= 0 and norm_b >= 0:
direction = 90 - direction
elif norm_a < 0 and norm_b >= 0:
direction = 90 - direction
elif norm_a < 0 and norm_b < 0:
direction = 270 - direction
else:
direction = 270 - direction
for callback in self._on_joystick:
callback(int(round(10 * speed)), int(direction))
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
stick = Joystick()
stick.on_button(lambda x: logging.info("Button: %s" % x))
stick.on_rotation(lambda x: logging.info("Rotation: %s" % x))
stick.on_joystick(lambda speed, head: logging.info("Speed: %s, Direction: %s" % (speed, head)))
time.sleep(100)

View File

@ -1,18 +0,0 @@
import time
from examples.bb8joystick import BB8, Joystick
bb8 = BB8()
def button(p):
print(p)
if p == True:
bb8.color(255, 255, 255)
else:
bb8.color(0, 0, 0)
deck = Joystick()
deck.on_button(button)
time.sleep(60)

View File

@ -1,10 +1,9 @@
# coding=utf-8 # coding=utf-8
import time
from time import sleep from time import sleep
from pylgbst import * from pylgbst import *
from pylgbst.hub import MoveHub from pylgbst.comms import DebugServerConnection
from pylgbst.peripherals import EncodedMotor, TiltSensor, Current, Voltage, COLORS, COLOR_BLACK from pylgbst.movehub import MoveHub
log = logging.getLogger("demo") log = logging.getLogger("demo")
@ -12,13 +11,8 @@ log = logging.getLogger("demo")
def demo_led_colors(movehub): def demo_led_colors(movehub):
# LED colors demo # LED colors demo
log.info("LED colors demo") log.info("LED colors demo")
movehub.color_distance_sensor.subscribe(lambda x, y: None)
# We get a response with payload and port, not x and y here... for color in COLORS.keys()[1:] + [COLOR_BLACK]:
def colour_callback(named):
log.info("LED Color callback: %s", named)
movehub.led.subscribe(colour_callback)
for color in list(COLORS.keys())[1:] + [COLOR_BLACK]:
log.info("Setting LED color to: %s", COLORS[color]) log.info("Setting LED color to: %s", COLORS[color])
movehub.led.set_color(color) movehub.led.set_color(color)
sleep(1) sleep(1)
@ -26,7 +20,7 @@ def demo_led_colors(movehub):
def demo_motors_timed(movehub): def demo_motors_timed(movehub):
log.info("Motors movement demo: timed") log.info("Motors movement demo: timed")
for level in range(0, 101, 10): for level in range(0, 101, 5):
level /= 100.0 level /= 100.0
log.info("Speed level: %s%%", level * 100) log.info("Speed level: %s%%", level * 100)
movehub.motor_A.timed(0.2, level) movehub.motor_A.timed(0.2, level)
@ -99,7 +93,7 @@ def demo_tilt_sensor_precise(movehub):
demo_tilt_sensor_simple.cnt += 1 demo_tilt_sensor_simple.cnt += 1
log.info("Tilt #%s of %s: roll:%s pitch:%s yaw:%s", demo_tilt_sensor_simple.cnt, limit, pitch, roll, yaw) log.info("Tilt #%s of %s: roll:%s pitch:%s yaw:%s", demo_tilt_sensor_simple.cnt, limit, pitch, roll, yaw)
movehub.tilt_sensor.subscribe(callback, mode=TiltSensor.MODE_3AXIS_ACCEL) movehub.tilt_sensor.subscribe(callback, mode=TiltSensor.MODE_3AXIS_FULL)
while demo_tilt_sensor_simple.cnt < limit: while demo_tilt_sensor_simple.cnt < limit:
time.sleep(1) time.sleep(1)
@ -115,16 +109,16 @@ def demo_color_sensor(movehub):
demo_color_sensor.cnt += 1 demo_color_sensor.cnt += 1
log.info("#%s/%s: Color %s, distance %s", demo_color_sensor.cnt, limit, COLORS[color], distance) log.info("#%s/%s: Color %s, distance %s", demo_color_sensor.cnt, limit, COLORS[color], distance)
movehub.vision_sensor.subscribe(callback) movehub.color_distance_sensor.subscribe(callback)
while demo_color_sensor.cnt < limit: while demo_color_sensor.cnt < limit:
time.sleep(1) time.sleep(1)
movehub.vision_sensor.unsubscribe(callback) movehub.color_distance_sensor.unsubscribe(callback)
def demo_motor_sensors(movehub): def demo_motor_sensors(movehub):
log.info("Motor rotation sensors test. Rotate all available motors once") log.info("Motor rotation sensors test. Rotate all available motors once")
demo_motor_sensors.states = {movehub.motor_A: 0, movehub.motor_B: 0, movehub.motor_external: 0} demo_motor_sensors.states = {movehub.motor_A: None, movehub.motor_B: None}
def callback_a(param1): def callback_a(param1):
demo_motor_sensors.states[movehub.motor_A] = param1 demo_motor_sensors.states[movehub.motor_A] = param1
@ -145,13 +139,14 @@ def demo_motor_sensors(movehub):
demo_motor_sensors.states[movehub.motor_external] = None demo_motor_sensors.states[movehub.motor_external] = None
movehub.motor_external.subscribe(callback_e) movehub.motor_external.subscribe(callback_e)
while not all([x is not None and abs(x) > 30 for x in demo_motor_sensors.states.values()]): while None in demo_motor_sensors.states.values():
time.sleep(1) time.sleep(1)
movehub.motor_A.unsubscribe(callback_a) movehub.motor_A.unsubscribe(callback_a)
movehub.motor_B.unsubscribe(callback_b) movehub.motor_B.unsubscribe(callback_b)
if movehub.motor_external is not None: if movehub.motor_external is not None:
demo_motor_sensors.states[movehub.motor_external] = None
movehub.motor_external.unsubscribe(callback_e) movehub.motor_external.unsubscribe(callback_e)
@ -162,103 +157,37 @@ def demo_voltage(movehub):
def callback2(value): def callback2(value):
log.info("Voltage: %s", value) log.info("Voltage: %s", value)
movehub.current.subscribe(callback1, mode=Current.CURRENT_L, granularity=0) movehub.amperage.subscribe(callback1, mode=Amperage.MODE1, granularity=0)
movehub.current.subscribe(callback1, mode=Current.CURRENT_L, granularity=1) movehub.amperage.subscribe(callback1, mode=Amperage.MODE1, granularity=1)
movehub.voltage.subscribe(callback2, mode=Voltage.VOLTAGE_L, granularity=0) movehub.voltage.subscribe(callback2, mode=Voltage.MODE1, granularity=0)
movehub.voltage.subscribe(callback2, mode=Voltage.VOLTAGE_L, granularity=1) movehub.voltage.subscribe(callback2, mode=Voltage.MODE1, granularity=1)
time.sleep(5) time.sleep(5)
movehub.current.unsubscribe(callback1) movehub.amperage.unsubscribe(callback1)
movehub.voltage.unsubscribe(callback2) movehub.voltage.unsubscribe(callback2)
def demo_all(movehub): def demo_all(movehub):
demo_voltage(movehub)
demo_led_colors(movehub)
demo_motors_timed(movehub) demo_motors_timed(movehub)
demo_motors_angled(movehub) demo_motors_angled(movehub)
demo_port_cd_motor(movehub) demo_port_cd_motor(movehub)
demo_led_colors(movehub)
demo_tilt_sensor_simple(movehub) demo_tilt_sensor_simple(movehub)
demo_tilt_sensor_precise(movehub) demo_tilt_sensor_precise(movehub)
demo_color_sensor(movehub) demo_color_sensor(movehub)
demo_motor_sensors(movehub) demo_motor_sensors(movehub)
demo_voltage(movehub)
DEMO_CHOICES = {
'all': demo_all,
'voltage': demo_voltage,
'led_colors': demo_led_colors,
'motors_timed': demo_motors_timed,
'motors_angled': demo_motors_angled,
'port_cd_motor': demo_port_cd_motor,
'tilt_sensor': demo_tilt_sensor_simple,
'tilt_sensor_precise': demo_tilt_sensor_precise,
'color_sensor': demo_color_sensor,
'motor_sensors': demo_motor_sensors,
}
def get_options():
import argparse
arg_parser = argparse.ArgumentParser(
description='Demonstrate move-hub communications',
)
arg_parser.add_argument(
'-c', '--connection',
default='auto://',
help='''Specify connection URL to use, `protocol://mac?param=X` with protocol in:
"gatt","pygatt","gattlib","gattool", "bluepy","bluegiga"'''
)
arg_parser.add_argument(
'-d', '--demo',
default='all',
choices=sorted(DEMO_CHOICES.keys()),
help="Run a particular demo, default all"
)
return arg_parser
def connection_from_url(url):
import pylgbst
if url == 'auto://':
return None
try:
from urllib.parse import urlparse, parse_qs
except ImportError:
from urlparse import urlparse, parse_qs
parsed = urlparse(url)
name = 'get_connection_%s' % parsed.scheme
factory = getattr(pylgbst, name, None)
if not factory:
msg = "Unrecognised URL scheme/protocol, expect a get_connection_<protocol> in pylgbst: %s"
raise ValueError(msg % parsed.protocol)
params = {}
if parsed.netloc.strip():
params['hub_mac'] = parsed.netloc
for key, value in parse_qs(parsed.query).items():
if len(value) == 1:
params[key] = value[0]
else:
params[key] = value
return factory(
**params
)
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(relativeCreated)d\t%(levelname)s\t%(name)s\t%(message)s') logging.basicConfig(level=logging.INFO)
parser = get_options()
options = parser.parse_args()
parameters = {}
try:
connection = connection_from_url(options.connection) # get_connection_bleak(hub_name=MoveHub.DEFAULT_NAME)
parameters['connection'] = connection
except ValueError as err:
parser.error(err.args[0])
hub = MoveHub(**parameters)
try: try:
demo = DEMO_CHOICES[options.demo] connection = DebugServerConnection()
demo(hub) except BaseException:
finally: logging.warning("Failed to use debug server: %s", traceback.format_exc())
hub.disconnect() connection = BLEConnection().connect()
hub = MoveHub(connection)
sleep(10000)
#demo_all(hub)

View File

@ -1,18 +0,0 @@
import logging
import traceback
from pylgbst import get_connection_auto
from pylgbst.comms import DebugServerConnection
from pylgbst.hub import MoveHub
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
hub = MoveHub()
try:
hub.motor_AB.start_power(0.45, 0.45)
hub.motor_external.angled(12590, 0.1)
# time.sleep(180)
finally:
hub.motor_AB.stop()
if hub.motor_external:
hub.motor_external.stop()

View File

@ -2,7 +2,7 @@ import logging
import math import math
import time import time
from pylgbst.peripherals import VisionSensor, COLOR_RED, COLOR_CYAN, COLORS from pylgbst import ColorDistanceSensor, COLORS, COLOR_RED, COLOR_CYAN
class Plotter(object): class Plotter(object):
@ -12,7 +12,7 @@ class Plotter(object):
def __init__(self, hub, base_speed=1.0): def __init__(self, hub, base_speed=1.0):
""" """
:type hub: pylgbst.hub.MoveHub :type hub: pylgbst.movehub.MoveHub
""" """
self._hub = hub self._hub = hub
self.caret = self._hub.motor_A self.caret = self._hub.motor_A
@ -35,27 +35,27 @@ class Plotter(object):
self.is_tool_down = False self.is_tool_down = False
def _reset_caret(self): def _reset_caret(self):
if not self._hub.vision_sensor: if not self._hub.color_distance_sensor:
logging.warning("No color/distance sensor, cannot center caret") logging.warning("No color/distance sensor, cannot center caret")
return return
self._hub.vision_sensor.subscribe(self._on_distance, mode=VisionSensor.COLOR_DISTANCE_FLOAT) self._hub.color_distance_sensor.subscribe(self._on_distance, mode=ColorDistanceSensor.COLOR_DISTANCE_FLOAT)
self.caret.timed(0.5, 1) self.caret.timed(0.5, 1)
try: try:
self.caret.start_power(-1) self.caret.constant(-1)
count = 0 count = 0
max_tries = 50 max_tries = 50
while self._marker_color not in (COLOR_RED, COLOR_CYAN) and count < max_tries: while self._marker_color not in (COLOR_RED, COLOR_CYAN) and count < max_tries:
time.sleep(30.0 / max_tries) time.sleep(30.0 / max_tries)
count += 1 count += 1
self._hub.vision_sensor.unsubscribe(self._on_distance) self._hub.color_distance_sensor.unsubscribe(self._on_distance)
clr = COLORS[self._marker_color] if self._marker_color else None clr = COLORS[self._marker_color] if self._marker_color else None
logging.info("Centering tries: %s, color #%s", count, clr) logging.info("Centering tries: %s, color #%s", count, clr)
if count >= max_tries: if count >= max_tries:
raise RuntimeError("Failed to center caret") raise RuntimeError("Failed to center caret")
finally: finally:
self.caret.stop() self.caret.stop()
self._hub.vision_sensor.unsubscribe(self._on_distance) self._hub.color_distance_sensor.unsubscribe(self._on_distance)
if self._marker_color == COLOR_CYAN: if self._marker_color == COLOR_CYAN:
self.move(-0.1, 0) self.move(-0.1, 0)
@ -83,7 +83,7 @@ class Plotter(object):
def finalize(self): def finalize(self):
if self.is_tool_down: if self.is_tool_down:
self._tool_up() self._tool_up()
self.both.stop() self.both.stop(async=True)
def _tool_down(self): def _tool_down(self):
self.is_tool_down = True self.is_tool_down = True
@ -191,7 +191,7 @@ class Plotter(object):
spa = speed_a * self.base_speed spa = speed_a * self.base_speed
spb = -speed_b * self.base_speed * self.MOTOR_RATIO spb = -speed_b * self.base_speed * self.MOTOR_RATIO
logging.info("Motor speeds: %.3f / %.3f", spa, spb) logging.info("Motor speeds: %.3f / %.3f", spa, spb)
self.both.start_power(spa, spb) self.both.constant(spa, spb)
time.sleep(dur) time.sleep(dur)
def spiral(self, rounds, growth): def spiral(self, rounds, growth):
@ -215,7 +215,7 @@ class Plotter(object):
for speed_a, speed_b, dur in speeds: for speed_a, speed_b, dur in speeds:
spa = speed_a * self.base_speed spa = speed_a * self.base_speed
spb = -speed_b * self.base_speed * self.MOTOR_RATIO spb = -speed_b * self.base_speed * self.MOTOR_RATIO
self.both.start_power(spa, spb) self.both.constant(spa, spb)
logging.info("Motor speeds: %.3f / %.3f", spa, spb) logging.info("Motor speeds: %.3f / %.3f", spa, spb)
time.sleep(dur) time.sleep(dur)

View File

@ -0,0 +1,203 @@
import json
import logging
import matplotlib.pyplot as plt
import time
from threading import Thread
import numpy
from PIL import Image
class Tracer(object):
def __init__(self, fname):
super(Tracer, self).__init__()
self.threshold = 64
self.orig_fname = fname
self.orig = Image.open(fname)
self.conv1 = self.remove_transparency(self.orig)
self.conv1 = self.conv1.convert("L")
self.src = numpy.asarray(self.conv1)
self.dst = numpy.copy(self.src)
self.dst.fill(False)
self.mark = numpy.copy(self.dst)
# start in center
self.height, self.width = self.dst.shape[0:2]
self.posy = self.height / 2
self.posx = self.width / 2
self.lines = []
def remove_transparency(self, im, bg_colour=(255, 255, 255)):
# from https://stackoverflow.com/questions/35859140/remove-transparency-alpha-from-any-image-using-pil
# Only process if image has transparency (http://stackoverflow.com/a/1963146)
if im.mode in ('RGBA', 'LA') or (im.mode == 'P' and 'transparency' in im.info):
# Need to convert to RGBA if LA format due to a bug in PIL (http://stackoverflow.com/a/1963146)
alpha = im.convert('RGBA').split()[-1]
# Create a new background image of our matt color.
# Must be RGBA because paste requires both images have the same format
# (http://stackoverflow.com/a/8720632 and http://stackoverflow.com/a/9459208)
bg = Image.new("RGBA", im.size, bg_colour + (255,))
bg.paste(im, mask=alpha)
return bg
else:
return im
def trace(self):
while self._has_unchecked_pixels():
# go circles to find a pixel in src
if not self._spiral_till_pixel():
break
# move until we find new pixels
self._move_while_you_can()
logging.info("Done")
with open(self.orig_fname + ".json", "wt") as fhd:
fhd.write(json.dumps(self.lines))
def _has_unchecked_pixels(self):
ix, iy = numpy.where(self.mark == False) # FIXME: highly inefficient
return len(ix) or len(iy)
def is_src(self, posx, posy):
return self.src[posy][posx] < self.threshold
def _spiral_till_pixel(self): # TODO: optimize it, maybe use different algo (not spiral, just walkthrough?)
radius = 1
direction = 0
offset = 0
while self._has_unchecked_pixels():
in_lower = self.posy < self.height and self.posx < self.width
in_upper = self.posy >= 0 and self.posx >= 0
if in_lower and in_upper and not self.mark[self.posy][self.posx]:
if self.is_src(self.posx, self.posy):
return True
self.mark[self.posy][self.posx] = True
if direction == 0:
self.posx += 1
self.posy += 0
elif direction == 1:
self.posx += 0
self.posy += 1
elif direction == 2:
self.posx += -1
self.posy += 0
elif direction == 3:
self.posx += 0
self.posy += -1
else:
raise ValueError()
offset += 1
if offset >= radius:
# time.sleep(0.01)
offset = 0
direction += 1
if direction > 3:
direction = 0
if direction in (0, 2):
radius += 1
return False
def _move_while_you_can(self):
# time.sleep(0.1)
logging.debug("%s:%s=%s", self.posy, self.posx, self.src[self.posy][self.posx])
dirs = self._check_directions() # TODO: use stack of this knowledge to speed-up walktrough
dx, dy, length = self._get_best_direction(dirs)
self.dst[self.posy][self.posx] = True
self.mark[self.posy][self.posx] = True
line = {
"x1": self.posx, "y1": self.posy,
"x2": self.posx + dx * length, "y2": self.posy + dy * length,
"len": length
}
self.lines.append(line)
logging.info("%s", line)
for n in range(0, length):
self.posy += dy
self.posx += dx
self.dst[self.posy][self.posx] = True
self.mark[self.posy][self.posx] = True
def _check_directions(self):
dirs = {
-1: {-1: 0, 0: 0, 1: 0},
0: {-1: 0, 0: 0, 1: 0},
1: {-1: 0, 0: 0, 1: 0},
}
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
if dy == 0 and dx == 0:
continue
length = 1
while True:
cx = self.posx + length * dx
cy = self.posy + length * dy
if not (0 <= cx < self.width) or not (0 <= cy < self.height):
break
if not self.is_src(cx, cy) or self.mark[cy][cx]:
break
dirs[dy][dx] = length
length += 1
return dirs
def _get_best_direction(self, dirs):
bestlen = 0
bestx = 0
besty = 0
for y in dirs:
for x in dirs[y]:
if dirs[y][x] > bestlen:
bestlen = dirs[y][x]
bestx = x
besty = y
return bestx, besty, bestlen
class TracerVisualizer(object):
def __init__(self, tracer):
"""
:type tracer: Tracer
"""
self.tracer = tracer
def run(self):
tracer = self.tracer
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2)
ax1.imshow(tracer.orig)
ax2.imshow(tracer.src, cmap='binary')
plt.show(block=False)
thr = Thread(target=tracer.trace)
thr.setDaemon(True)
thr.start()
while plt.get_fignums(): # weird trick to react on close
ax3.set_title("%s:%s" % (tracer.posx, tracer.posy))
ax3.imshow(tracer.mark, cmap='gray')
ax4.imshow(tracer.dst, cmap='gray')
plt.pause(1)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
trc = Tracer("test3.png")
TracerVisualizer(trc).run()
time.sleep(5)

View File

@ -1,9 +1,11 @@
# coding=utf-8
import logging import logging
import time import time
import traceback
from examples.plotter import Plotter from examples.plotter import Plotter
from pylgbst.hub import EncodedMotor, MoveHub from examples.plotter.lego import lego
from pylgbst import EncodedMotor, PORT_AB, PORT_C, PORT_A, PORT_B, MoveHub
from pylgbst.comms import DebugServerConnection, BLEConnection
from tests import HubMock from tests import HubMock
@ -86,11 +88,11 @@ def try_speeds():
speeds = [x * 1.0 / 10.0 for x in range(1, 11)] speeds = [x * 1.0 / 10.0 for x in range(1, 11)]
for s in speeds: for s in speeds:
logging.info("%s", s) logging.info("%s", s)
plotter.both.start_power(s, -s) plotter.both.constant(s, -s)
time.sleep(1) time.sleep(1)
for s in reversed(speeds): for s in reversed(speeds):
logging.info("%s", s) logging.info("%s", s)
plotter.both.start_power(-s, s) plotter.both.constant(-s, s)
time.sleep(1) time.sleep(1)
@ -177,63 +179,38 @@ def angles_experiment():
class MotorMock(EncodedMotor): class MotorMock(EncodedMotor):
pass def _wait_sync(self, async):
super(MotorMock, self)._wait_sync(True)
def get_hub_mock(): def get_hub_mock():
hub = HubMock() hub = HubMock()
hub.motor_A = MotorMock(hub, MoveHub.PORT_A) hub.motor_A = MotorMock(hub, PORT_A)
hub.motor_B = MotorMock(hub, MoveHub.PORT_B) hub.motor_B = MotorMock(hub, PORT_B)
hub.motor_AB = MotorMock(hub, MoveHub.PORT_AB) hub.motor_AB = MotorMock(hub, PORT_AB)
hub.motor_external = MotorMock(hub, MoveHub.PORT_C) hub.motor_external = MotorMock(hub, PORT_C)
return hub return hub
def interpret_command(cmd, plotter):
scale = 0.075
for c in cmd.lower():
if c == u'л':
plotter._transfer_to(-scale, 0)
elif c == u'п':
plotter._transfer_to(scale, 0)
elif c == u'н':
plotter._transfer_to(0, -scale)
elif c == u'в':
plotter._transfer_to(0, scale)
elif c == u'1':
plotter._tool_down()
elif c == u'0':
plotter._tool_up()
elif c == u' ':
pass
else:
logging.warning(u"Неизвестная команда: %s", c)
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logging.getLogger('').setLevel(logging.DEBUG) logging.getLogger('').setLevel(logging.INFO)
hub = MoveHub() if 1 else get_hub_mock() try:
conn = DebugServerConnection()
except BaseException:
logging.warning("Failed to use debug server: %s", traceback.format_exc())
conn = BLEConnection().connect()
hub = MoveHub(conn) if 1 else get_hub_mock()
plotter = Plotter(hub, 0.75) plotter = Plotter(hub, 0.75)
FIELD_WIDTH = 0.9 FIELD_WIDTH = 0.9
try: try:
"""
while True:
cmd = six.moves.input("программа> ").decode('utf8')
if not cmd.strip():
continue
plotter.initialize()
interpret_command(cmd, plotter)
plotter.finalize()
"""
time.sleep(1)
plotter.initialize() plotter.initialize()
# snowflake(0.75) snowflake(0.75)
# christmas_tree() # christmas_tree()
# square_spiral() # square_spiral()
# lego(plotter, FIELD_WIDTH / 7.0) # lego(plotter, FIELD_WIDTH / 7.0)

View File

@ -1,90 +0,0 @@
import logging
from pylgbst.hub import MoveHub
from pylgbst.peripherals import COLOR_YELLOW, COLOR_BLUE, COLOR_CYAN, COLOR_RED, COLOR_BLACK, COLORS
class ColorSorter(MoveHub):
positions = [COLOR_YELLOW, COLOR_BLUE, COLOR_CYAN, COLOR_RED]
def __init__(self, connection=None):
super(ColorSorter, self).__init__(connection)
self.position = len(self.positions)
self.color = 0
self.distance = 10
self._last_wheel_dir = 1
self.vision_sensor.subscribe(self.on_color)
self.queue = [None for _ in range(0, 1)]
def on_color(self, colr, dist):
if colr not in (COLOR_BLACK,) or dist < 2.5:
logging.debug("%s %s", COLORS[colr], dist)
self.color = colr
self.distance = dist
def feed(self):
self.motor_A.angled(1080) # 1080 was true
def move_to_bucket(self, color):
logging.info("Bucket: %s", COLORS[color])
if color in self.positions:
newpos = self.positions.index(color)
else:
newpos = len(self.positions)
if newpos == self.position:
return
offset = newpos - self.position
wheel_dir = offset / abs(offset)
# if wheel_dir != self._last_wheel_dir:
# self.motor_B.angled(30 * wheel_dir)
self.motor_B.angled(offset * 600, 0.8)
self.position = newpos
self._last_wheel_dir = wheel_dir
def clear(self):
self.vision_sensor.unsubscribe(self.on_color)
self.move_to_bucket(COLOR_BLACK)
self.motor_AB.stop()
def tick(self):
res = False
item = (self.color, self.distance) # read once
if item[1] <= 5.0:
logging.info("Detected: %s", COLORS[item[0]])
self.queue.append(item)
res = True
else:
self.queue.append(None)
logging.debug("%s", [COLORS[x[0]] if x else None for x in self.queue])
last = self.queue.pop(0)
if last:
self.move_to_bucket(last[0])
res = True
self.feed()
return res
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
sorter = ColorSorter()
empty = 0
try:
while True:
empty += 1
if sorter.tick():
empty = 0
elif empty > 20:
break
finally:
sorter.clear()

View File

@ -1,218 +0,0 @@
import json
import logging
import os
import time
import traceback
from threading import Thread
import cv2
import imutils as imutils
from matplotlib import pyplot
from pylgbst.hub import MoveHub
from pylgbst.peripherals import COLOR_RED, COLOR_BLUE, COLOR_YELLOW
cascades_dir = '/usr/share/opencv/haarcascades'
face_cascade = cv2.CascadeClassifier(cascades_dir + '/haarcascade_frontalface_default.xml')
smile_cascade = cv2.CascadeClassifier(cascades_dir + '/haarcascade_smile.xml')
LINE_THICKNESS = 2
class FaceTracker(MoveHub):
def __init__(self, connection=None):
super(FaceTracker, self).__init__(connection)
self._is_smile_on = False
self.cur_img = None
self.cur_face = None
self.cur_smile = None
self.smile_counter = 0
def capture(self):
logging.info("Starting cam...")
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920 / 2)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080 / 2)
# cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1)
# cap.set(cv2.CAP_PROP_XI_AUTO_WB, 1)
# cap.set(cv2.CAP_PROP_XI_AEAG, 1)
size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
fourcc = cv2.VideoWriter_fourcc(*'XVID')
fps = cap.get(cv2.CAP_PROP_FPS)
video = cv2.VideoWriter('output_%d.avi' % int(time.time()), fourcc, fps, size)
try:
while True:
flag, img = cap.read()
if self.cur_face is not None:
(x, y, w, h) = self.cur_face
if LINE_THICKNESS:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255,), LINE_THICKNESS)
video.write(img)
self.cur_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
logging.debug("Got frame")
finally:
logging.info("Releasing cam...")
cap.release()
video.release()
def _find_face(self):
bodies, rejects, weights = face_cascade.detectMultiScale3(self.cur_img, 1.5, 5, outputRejectLevels=True)
if len(bodies):
logging.debug("Bodies: %s / Weights: %s", bodies, weights)
else:
return None
items = []
for n in range(0, len(bodies)):
items.append((bodies[n], weights[n]))
self.cur_face = None
return self._reduce(bodies)
def _reduce(self, values):
res = None
for x, y, w, h in values:
if res is None:
res = (x, y, w, h)
else:
new_xy = (min(x, res[0]), min(y, res[1]))
res = new_xy + (max(x + w, res[0] + res[2]) - new_xy[0], max(y + h, res[1] + res[3]) - new_xy[1])
return res
def _find_smile(self, cur_face):
roi_color = None
if cur_face is not None:
(x, y, w, h) = cur_face
roi_color = self.cur_img[y:y + h, x:x + w]
smile = self._reduce(smile_cascade.detectMultiScale(roi_color, 1.5, 20))
else:
smile = None
if not smile:
self.cur_smile = None
self.smile_counter -= 1
else:
(ex, ey, ew, eh) = smile
self.cur_smile = (ex, ey, ew, eh)
if LINE_THICKNESS:
cv2.rectangle(roi_color, (ex, ey), (ex + ew, ey + eh), (0, 255, 0), LINE_THICKNESS)
self.smile_counter += 1
logging.info("Smile counter: %s", self.smile_counter)
if self.smile_counter > 2:
self.smile_counter = 2
self._smile(True)
if self.smile_counter < 0:
self.smile_counter = 0
self._smile(False)
def _smile(self, on=True):
if on and not self._is_smile_on:
self._is_smile_on = True
self.motor_B.angled(-90, 0.5)
self.led.set_color(COLOR_RED)
if not on and self._is_smile_on:
self._is_smile_on = False
self.motor_B.angled(90, 0.5)
def _find_color(self):
# from https://www.pyimagesearch.com/2015/09/14/ball-tracking-with-opencv/
# and https://thecodacus.com/opencv-object-tracking-colour-detection-python/#.W2DHFd_IQsM
blurred = cv2.GaussianBlur(self.cur_img, (11, 11), 0)
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
try:
# having color values in file allows finding values easier
with open(os.path.join(os.path.dirname(__file__), "color.json")) as fhd:
data = json.loads(fhd.read())
lower = tuple(data[0])
upper = tuple(data[1])
except BaseException:
logging.debug("%s", traceback.format_exc())
lower = (100, 100, 100,)
upper = (130, 255, 255,)
mask = cv2.inRange(hsv, lower, upper)
mask = cv2.erode(mask, None, iterations=5)
mask = cv2.dilate(mask, None, iterations=5)
# if not (int(time.time()) % 2):
# self.cur_img = mask
ret, thresh = cv2.threshold(mask, 20, 255, 0)
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
return self._reduce([cv2.boundingRect(c) for c in cnts])
def _auto_pan(self, cur_face):
(x, y, w, h) = cur_face
height, width, channels = self.cur_img.shape
cam_center = (width / 2, height / 2)
face_center = (x + w / 2, y + h / 2)
horz = 1.5 * (face_center[0] - cam_center[0]) / float(width)
vert = 0.7 * (face_center[1] - cam_center[1]) / float(height)
logging.info("Horiz %.3f, vert %3f", horz, vert)
if abs(horz) < 0.1:
horz = 0
if abs(vert) < 0.15:
vert = 0
self.motor_external.start_power(horz)
self.motor_A.start_power(-vert)
def main(self):
thr = Thread(target=self.capture)
thr.setDaemon(True)
thr.start()
while self.cur_img is None:
pass
plt = pyplot.imshow(self.cur_img)
pyplot.tight_layout()
pyplot.ion()
pyplot.show()
try:
while thr.isAlive() and self.connection.is_alive():
self._process_picture(plt)
finally:
self._smile(False)
def _process_picture(self, plt):
self.cur_face = self._find_face()
# self.cur_face = self._find_color()
if self.cur_face is None:
self.motor_external.stop()
self.motor_AB.stop()
if not self._is_smile_on:
self.led.set_color(COLOR_BLUE)
else:
if not self._is_smile_on:
self.led.set_color(COLOR_YELLOW)
self._auto_pan(self.cur_face)
self._find_smile(self.cur_face)
plt.set_array(self.cur_img)
logging.debug("Updated frame")
pyplot.pause(0.02)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
try:
hub = FaceTracker()
hub.main()
finally:
pass
# conn.disconnect()

View File

@ -1,12 +0,0 @@
[
[
125,
64,
64
],
[
145,
255,
250
]
]

View File

@ -3,10 +3,9 @@ import hashlib
import os import os
import re import re
import subprocess import subprocess
import time
from pylgbst import * from pylgbst import *
from pylgbst.hub import MoveHub from pylgbst.comms import DebugServerConnection
try: try:
import gtts import gtts
@ -42,8 +41,7 @@ SPEECH_LANG_MAP = {
"commands help": "Available commands are: " "commands help": "Available commands are: "
"forward, backward, turn left, turn right, " "forward, backward, turn left, turn right, "
"head left, head right, head straight, shot and say", "head left, head right, head straight, shot and say",
"finished": "Thank you! Robot is now turning off", "finished": "Thank you! Robot is now turning off"
"text is empty": "Please, enter not empty text to say!"
}, },
"ru": { "ru": {
"ready": "Робот Веернии готов к работе", "ready": "Робот Веернии готов к работе",
@ -51,10 +49,9 @@ SPEECH_LANG_MAP = {
"ok": "хорошо", "ok": "хорошо",
"commands help": "Доступные команды это: вперёд, назад, влево, вправо, " "commands help": "Доступные команды это: вперёд, назад, влево, вправо, "
"голову влево, голову вправо, голову прямо, выстрел, скажи", "голову влево, голову вправо, голову прямо, выстрел, скажи",
"finished": "Робот завершает работу. Спасибо!", "Finished": "Робот завершает работу. Спасибо!",
"commands from file": "Исполняю команды из файла", "commands from file": "Исполняю команды из файла",
"fire": "Выстрел!", "fire": "Выстрел!",
"text is empty": "Пожалуйста, наберите не пустой текст!"
} }
} }
@ -64,11 +61,17 @@ VERNIE_SINGLE_MOVE = 430
class Vernie(MoveHub): class Vernie(MoveHub):
def __init__(self, language='en'): def __init__(self, language='en'):
super(Vernie, self).__init__() try:
conn = DebugServerConnection()
except BaseException:
logging.debug("Failed to use debug server: %s", traceback.format_exc())
conn = BLEConnection().connect()
super(Vernie, self).__init__(conn)
self.language = language self.language = language
while True: while True:
required_devices = (self.vision_sensor, self.motor_external) required_devices = (self.color_distance_sensor, self.motor_external)
if None not in required_devices: if None not in required_devices:
break break
log.debug("Waiting for required devices to appear: %s", required_devices) log.debug("Waiting for required devices to appear: %s", required_devices)
@ -129,9 +132,6 @@ class Vernie(MoveHub):
confirm(cmd) confirm(cmd)
self.head(STRAIGHT) self.head(STRAIGHT)
elif cmd[0] in ("say", "скажи", "сказать"): elif cmd[0] in ("say", "скажи", "сказать"):
if not cmd[1:]:
self.say("text is empty")
return
say(' '.join(cmd[1:])) say(' '.join(cmd[1:]))
elif cmd[0] in ("fire", "shot", "огонь", "выстрел"): elif cmd[0] in ("fire", "shot", "огонь", "выстрел"):
say("fire") say("fire")

View File

@ -2,15 +2,15 @@
To use it, install this android app: To use it, install this android app:
https://play.google.com/store/apps/details?id=com.mscino.sensornode https://play.google.com/store/apps/details?id=com.mscino.sensornode
Then open app on phone and choose "Stream" => "Stream live data (XML)". Then open app on phone and choose "Stream" => "Stream live data (XML)".
Check the "Accelerometer" option and put your IP address into corresponding field. Check the "Accelerometer" option and put your IP address into corresponding filed.
Specify port there as 8999, and enable streaming. Then run this script on computer. Specify port there as 8999, and enable streaming. Then run this script on computer.
""" """
import logging import logging
import socket import socket
import time import time
from examples.vernie import Vernie from . import Vernie
from pylgbst.peripherals import VisionSensor from pylgbst.peripherals import ColorDistanceSensor
host = '' host = ''
port = 8999 port = 8999
@ -54,7 +54,7 @@ robot = Vernie()
robot.button.subscribe(on_btn) robot.button.subscribe(on_btn)
robot.motor_AB.stop() robot.motor_AB.stop()
robot.vision_sensor.subscribe(on_distance, VisionSensor.DISTANCE_INCHES) robot.color_distance_sensor.subscribe(on_distance, ColorDistanceSensor.DISTANCE_INCHES)
try: try:
udp_sock.bind((host, port)) udp_sock.bind((host, port))
time.sleep(1) time.sleep(1)
@ -85,7 +85,7 @@ try:
sa = round(c + b / divider, 1) sa = round(c + b / divider, 1)
sb = round(c - b / divider, 1) sb = round(c - b / divider, 1)
logging.info("SpeedA=%s, SpeedB=%s", sa, sb) logging.info("SpeedA=%s, SpeedB=%s", sa, sb)
robot.motor_AB.start_speed(sa, sb) robot.motor_AB.constant(sa, sb)
# time.sleep(0.5) # time.sleep(0.5)
finally: finally:
robot.motor_AB.stop() robot.motor_AB.stop()

View File

@ -1,7 +1,4 @@
import logging from . import *
from examples.vernie import Vernie
from pylgbst.peripherals import VisionSensor
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -12,8 +9,7 @@ criterion = min
cur_luminosity = 0 cur_luminosity = 0
def on_change_lum(lumn, unknown): def on_change_lum(lumn):
del unknown
global cur_luminosity global cur_luminosity
cur_luminosity = lumn cur_luminosity = lumn
@ -32,7 +28,7 @@ def on_turn(angl):
robot.button.subscribe(on_btn) robot.button.subscribe(on_btn)
robot.vision_sensor.subscribe(on_change_lum, VisionSensor.DEBUG, granularity=1) robot.color_distance_sensor.subscribe(on_change_lum, ColorDistanceSensor.LUMINOSITY, granularity=1)
robot.motor_A.subscribe(on_turn, granularity=30) robot.motor_A.subscribe(on_turn, granularity=30)
# TODO: add bump detect to go back? # TODO: add bump detect to go back?
@ -65,5 +61,5 @@ while running:
logging.info("Luminosity is %.3f, moving towards it", cur_luminosity) logging.info("Luminosity is %.3f, moving towards it", cur_luminosity)
robot.move(FORWARD, 1) robot.move(FORWARD, 1)
robot.vision_sensor.unsubscribe(on_change_lum) robot.color_distance_sensor.unsubscribe(on_change_lum)
robot.button.unsubscribe(on_btn) robot.button.unsubscribe(on_btn)

View File

@ -1,5 +1,4 @@
from pylgbst.peripherals import COLOR_GREEN, COLOR_NONE from . import *
from vernie import *
robot = Vernie() robot = Vernie()
running = True running = True
@ -11,7 +10,7 @@ def callback(color, distance):
secs = (10 - distance + 1) / 10.0 secs = (10 - distance + 1) / 10.0
print("Distance is %.1f inches, I'm running back with %s%% speed!" % (distance, int(speed * 100))) print("Distance is %.1f inches, I'm running back with %s%% speed!" % (distance, int(speed * 100)))
if speed <= 1: if speed <= 1:
robot.motor_AB.timed(secs / 1, -speed) robot.motor_AB.timed(secs / 1, -speed, async=True)
robot.say("Ouch") robot.say("Ouch")
@ -23,11 +22,14 @@ def on_btn(pressed):
robot.led.set_color(COLOR_GREEN) robot.led.set_color(COLOR_GREEN)
robot.button.subscribe(on_btn) robot.button.subscribe(on_btn)
robot.vision_sensor.subscribe(callback) robot.color_distance_sensor.subscribe(callback)
robot.say("Place your hand in front of sensor") robot.say("Place your hand in front of sensor")
while running: while running:
time.sleep(1) time.sleep(1)
robot.vision_sensor.unsubscribe(callback) robot.color_distance_sensor.unsubscribe(callback)
robot.button.unsubscribe(on_btn) robot.button.unsubscribe(on_btn)
robot.led.set_color(COLOR_NONE)
while robot.led.in_progress():
time.sleep(1)

View File

@ -1,5 +1,3 @@
import sys
from . import * from . import *
robot = Vernie() robot = Vernie()
@ -11,7 +9,7 @@ def confirmation(command):
robot.say(command[0]) robot.say(command[0])
with open(os.path.join(os.path.dirname(__file__), "vernie.commands")) as fhd: with open("vernie.commands") as fhd:
for cmd in fhd.readlines(): for cmd in fhd.readlines():
sys.stdout.write("%s" % cmd) sys.stdout.write("%s" % cmd)
robot.interpret_command(cmd, confirmation) robot.interpret_command(cmd, confirmation)

View File

@ -1,86 +1,2 @@
import logging from pylgbst.movehub import *
import traceback from pylgbst.peripherals import *
from pylgbst.comms import DebugServer
log = logging.getLogger('pylgbst')
def get_connection_bluegiga(controller=None, hub_mac=None, hub_name=None):
del controller # to prevent code analysis warning
from pylgbst.comms.cpygatt import BlueGigaConnection
return BlueGigaConnection().connect(hub_mac, hub_name)
def get_connection_gattool(controller='hci0', hub_mac=None, hub_name=None):
from pylgbst.comms.cpygatt import GattoolConnection
return GattoolConnection(controller).connect(hub_mac, hub_name)
def get_connection_gatt(controller='hci0', hub_mac=None, hub_name=None):
from pylgbst.comms.cgatt import GattConnection
return GattConnection(controller).connect(hub_mac, hub_name)
def get_connection_gattlib(controller='hci0', hub_mac=None, hub_name=None):
from pylgbst.comms.cgattlib import GattLibConnection
return GattLibConnection(controller).connect(hub_mac, hub_name)
def get_connection_bluepy(controller='hci0', hub_mac=None, hub_name=None):
from pylgbst.comms.cbluepy import BluepyConnection
return BluepyConnection(controller).connect(hub_mac, hub_name)
def get_connection_bleak(controller='hci0', hub_mac=None, hub_name=None):
"""
Return connection based with Bleak API as an endpoint.
:param controller: Not used, kept for compatibility with others.
:param hub_mac: Optional Lego HUB MAC to connect to.
:return: Driver object.
"""
del controller # to prevent code analysis warning
from pylgbst.comms.cbleak import BleakDriver
return BleakDriver(hub_mac, hub_name)
def get_connection_auto(controller='hci0', hub_mac=None, hub_name=None):
fns = [
get_connection_bluepy,
get_connection_bluegiga,
get_connection_gatt,
get_connection_bleak,
get_connection_gattool,
get_connection_gattlib,
]
conn = None
for fn in fns:
try:
logging.info("Trying %s", fn.__name__)
return fn(controller, hub_mac, hub_name)
except KeyboardInterrupt:
raise
except BaseException:
logging.debug("Failed: %s", traceback.format_exc())
if conn is None:
raise Exception("Failed to autodetect connection, make sure you have installed prerequisites")
logging.info("Succeeded with %s", conn.__class__.__name__)
return conn
def start_debug_server(iface="hci0", port=9090):
server = DebugServer(get_connection_auto(iface))
try:
server.start(port)
finally:
server.connection.disconnect()

View File

@ -8,32 +8,54 @@ import socket
import traceback import traceback
from abc import abstractmethod from abc import abstractmethod
from binascii import unhexlify from binascii import unhexlify
from gattlib import DiscoveryService, GATTRequester
from threading import Thread from threading import Thread
from pylgbst.messages import MsgHubAction from pylgbst.constants import MSG_DEVICE_SHUTDOWN, queue, str2hex
from pylgbst.utilities import str2hex
log = logging.getLogger('comms') log = logging.getLogger('comms')
MOVE_HUB_HW_UUID_SERV = '00001623-1212-efde-1623-785feabcd123' LEGO_MOVE_HUB = "LEGO Move Hub"
MOVE_HUB_HW_UUID_CHAR = '00001624-1212-efde-1623-785feabcd123'
ENABLE_NOTIFICATIONS_HANDLE = 0x000f
ENABLE_NOTIFICATIONS_VALUE = b'\x01\x00'
MOVE_HUB_HARDWARE_HANDLE = 0x0E
# noinspection PyMethodOverriding
class Requester(GATTRequester):
"""
Wrapper to access `on_notification` capability of GATT
Set "notification_sink" field to a callable that will handle incoming data
"""
def __init__(self, p_object, *args, **kwargs):
super(Requester, self).__init__(p_object, *args, **kwargs)
self.notification_sink = None
self._notify_queue = queue.Queue() # this queue is to minimize time spent in gattlib C code
thr = Thread(target=self._dispatch_notifications)
thr.setDaemon(True)
thr.setName("Notify queue dispatcher")
thr.start()
def on_notification(self, handle, data):
# log.debug("requester notified, sink: %s", self.notification_sink)
self._notify_queue.put((handle, data))
def on_indication(self, handle, data):
log.debug("Indication on handle %s: %s", handle, str2hex(data))
def _dispatch_notifications(self):
while True:
handle, data = self._notify_queue.get()
if self.notification_sink:
try:
self.notification_sink(handle, data)
except BaseException:
log.warning("Data was: %s", str2hex(data))
log.warning("Failed to dispatch notification: %s", traceback.format_exc())
else:
log.warning("Dropped notification %s: %s", handle, str2hex(data))
class Connection(object): class Connection(object):
def connect(self, hub_mac=None):
pass
@abstractmethod
def is_alive(self):
pass
def disconnect(self):
pass
@abstractmethod @abstractmethod
def write(self, handle, data): def write(self, handle, data):
pass pass
@ -42,24 +64,45 @@ class Connection(object):
def set_notify_handler(self, handler): def set_notify_handler(self, handler):
pass pass
def enable_notifications(self):
self.write(ENABLE_NOTIFICATIONS_HANDLE, ENABLE_NOTIFICATIONS_VALUE)
def _is_device_matched(self, address, dev_name, hub_mac, find_name): class BLEConnection(Connection):
assert hub_mac or find_name, 'You have to provide either hub_mac or hub_name in connection options' """
log.debug("Checking device: %s, MAC: %s", dev_name, address) Main transport class, uses real Bluetooth LE connection.
matched = False Loops with timeout of 1 seconds to find device named "Lego MOVE Hub"
if address != "00:00:00:00:00:00":
if hub_mac:
if hub_mac.lower() == address.lower():
matched = True
elif dev_name == find_name:
matched = True
if matched: :type requester: Requester
log.info("Found %s at %s", dev_name, address) """
return matched def __init__(self):
super(BLEConnection, self).__init__()
self.requester = None
def connect(self, bt_iface_name='hci0', hub_mac=None):
service = DiscoveryService(bt_iface_name)
while not self.requester:
log.info("Discovering devices using %s...", bt_iface_name)
devices = service.discover(1)
log.debug("Devices: %s", devices)
for address, name in devices.items():
if name == LEGO_MOVE_HUB or hub_mac == address:
logging.info("Found %s at %s", name, address)
self.requester = Requester(address, True, bt_iface_name)
break
return self
def set_notify_handler(self, handler):
if self.requester:
log.debug("Setting notification handler: %s", handler)
self.requester.notification_sink = handler
else:
raise RuntimeError("No requester available")
def write(self, handle, data):
log.debug("Writing to %s: %s", handle, str2hex(data))
return self.requester.write_by_handle(handle, data)
class DebugServer(object): class DebugServer(object):
@ -68,13 +111,13 @@ class DebugServer(object):
It holds BLE connection to Move Hub, so no need to re-start it every time It holds BLE connection to Move Hub, so no need to re-start it every time
Usage: DebugServer(BLEConnection().connect()).start() Usage: DebugServer(BLEConnection().connect()).start()
:type connection: BLEConnection :type ble: BLEConnection
""" """
def __init__(self, connection): def __init__(self, ble_trans):
self._running = False self._running = False
self.sock = socket.socket() self.sock = socket.socket()
self.connection = connection self.ble = ble_trans
def start(self, port=9090): def start(self, port=9090):
self.sock.bind(('', port)) self.sock.bind(('', port))
@ -82,11 +125,11 @@ class DebugServer(object):
self._running = True self._running = True
while self._running: while self._running:
log.info("Accepting MoveHub debug connections at %s", port) log.info("Accepting connections at %s", port)
conn, addr = self.sock.accept() conn, addr = self.sock.accept()
if not self._running: if not self._running:
raise KeyboardInterrupt("Shutdown") raise KeyboardInterrupt("Shutdown")
self.connection.set_notify_handler(lambda x, y: self._notify(conn, x, y)) self.ble.requester.notification_sink = lambda x, y: self._notify(conn, x, y)
try: try:
self._handle_conn(conn) self._handle_conn(conn)
except KeyboardInterrupt: except KeyboardInterrupt:
@ -94,7 +137,7 @@ class DebugServer(object):
except BaseException: except BaseException:
log.error("Problem handling incoming connection: %s", traceback.format_exc()) log.error("Problem handling incoming connection: %s", traceback.format_exc())
finally: finally:
self.connection.set_notify_handler(self._notify_dummy) self.ble.requester.notification_sink = self._notify_dummy
conn.close() conn.close()
def __del__(self): def __del__(self):
@ -117,7 +160,7 @@ class DebugServer(object):
self._check_shutdown(data) self._check_shutdown(data)
def _check_shutdown(self, data): def _check_shutdown(self, data):
if data[5] == MsgHubAction.TYPE: if data[5] == MSG_DEVICE_SHUTDOWN:
log.warning("Device shutdown") log.warning("Device shutdown")
self._running = False self._running = False
@ -139,7 +182,7 @@ class DebugServer(object):
buf = buf[buf.index("\n") + 1:] buf = buf[buf.index("\n") + 1:]
if line: if line:
log.debug("Cmd line: %s", line) log.info("Cmd line: %s", line)
try: try:
self._handle_cmd(json.loads(line)) self._handle_cmd(json.loads(line))
except KeyboardInterrupt: except KeyboardInterrupt:
@ -149,7 +192,7 @@ class DebugServer(object):
def _handle_cmd(self, cmd): def _handle_cmd(self, cmd):
if cmd['type'] == 'write': if cmd['type'] == 'write':
self.connection.write(cmd['handle'], unhexlify(cmd['data'])) self.ble.write(cmd['handle'], unhexlify(cmd['data']))
else: else:
raise ValueError("Unhandled cmd: %s", cmd) raise ValueError("Unhandled cmd: %s", cmd)
@ -214,5 +257,9 @@ class DebugServerConnection(Connection):
def set_notify_handler(self, handler): def set_notify_handler(self, handler):
self.notify_handler = handler self.notify_handler = handler
def is_alive(self):
return self.reader.isAlive() def start_debug_server(iface="hci0", port=9090):
ble = BLEConnection()
ble.connect(iface)
server = DebugServer(ble)
server.start(port)

View File

@ -1,216 +0,0 @@
import asyncio
import logging
import queue
import threading
import time
from bleak import BleakClient, discover
from pylgbst.comms import Connection, MOVE_HUB_HW_UUID_CHAR
log = logging.getLogger('comms-bleak')
# Queues to handle request / responses. Acts as a buffer between API and async BLE driver
resp_queue = queue.Queue()
req_queue = queue.Queue()
class BleakDriver(object):
"""Driver that provides interface between API and Bleak."""
def __init__(self, hub_mac=None, hub_name=None):
"""
Initialize new object of Bleak Driver class.
:param hub_mac: Optional Lego HUB MAC to connect to.
"""
self.hub_mac = hub_mac
self.hub_name = hub_name
self._handler = None
self._abort = False
self._connection_thread = None
self._processing_thread = None
def set_notify_handler(self, handler):
"""
Set handler function used to communicate with an API.
:param handler: Handler function called by driver when received data
:return: None
"""
self._handler = handler
def enable_notifications(self):
"""
Enable notifications, in our cases starts communication threads.
We cannot do this earlier, because API need to fist set notification handler.
:return: None
"""
self._connection_thread = threading.Thread(target=lambda: asyncio.run(self._bleak_thread()))
self._connection_thread.daemon = True
self._connection_thread.start()
self._processing_thread = threading.Thread(target=self._processing)
self._processing_thread.daemon = True
self._processing_thread.start()
async def _bleak_thread(self):
bleak = BleakConnection()
await bleak.connect(self.hub_mac, self.hub_name)
await bleak.set_notify_handler(self._safe_handler)
# After connecting, need to send any data or hub will drop the connection,
# below command is Advertising name request update
await bleak.write_char(MOVE_HUB_HW_UUID_CHAR, bytearray([0x05, 0x00, 0x01, 0x01, 0x05]))
while not self._abort:
await asyncio.sleep(0.1)
if req_queue.qsize() != 0:
data = req_queue.get()
await bleak.write(data[0], data[1])
logging.info("Communications thread has exited")
@staticmethod
def _safe_handler(handler, data):
resp_queue.put((handler, data))
def _processing(self):
while not self._abort:
if resp_queue.qsize() != 0:
msg = resp_queue.get()
self._handler(msg[0], bytes(msg[1]))
time.sleep(0.01)
logging.info("Processing thread has exited")
def write(self, handle, data):
"""
Send data to given handle number.
:param handle: Handle number that will be translated into characteristic uuid
:param data: data to send
:raises ConnectionError" When internal threads are not working
:return: None
"""
if not self._connection_thread.is_alive() or not self._processing_thread.is_alive():
raise ConnectionError('Something went wrong, communication threads not functioning.')
req_queue.put((handle, data))
def disconnect(self):
"""
Disconnect and stops communication threads.
:return: None
"""
self._abort = True
def is_alive(self):
"""
Indicate whether driver is functioning or not.
:return: True if driver is functioning; False otherwise.
"""
if self._connection_thread is not None and self._processing_thread is not None:
return self._connection_thread.is_alive() and self._processing_thread.is_alive()
else:
return False
class BleakConnection(Connection):
"""Bleak driver for communicating with BLE device."""
def __init__(self):
"""Initialize new instance of BleakConnection class."""
Connection.__init__(self)
self.loop = asyncio.get_event_loop()
self._device = None
self._client = None
logging.getLogger('bleak.backends.dotnet.client').setLevel(logging.WARNING)
logging.getLogger('bleak.backends.bluezdbus.client').setLevel(logging.WARNING)
async def connect(self, hub_mac=None, hub_name=None):
"""
Connect to device.
:param hub_mac: Optional Lego HUB MAC to connect to.
:raises ConnectionError: When cannot connect to given MAC or name matching fails.
:return: None
"""
log.info("Discovering devices... Press green button on Hub")
for i in range(0, 30):
devices = await discover(timeout=1)
log.debug("Devices: %s", devices)
for dev in devices:
log.debug(dev)
address = dev.address
name = dev.name
if self._is_device_matched(address, name, hub_mac, hub_name):
log.info('Device matched: %r', dev)
self._device = dev
break
else:
continue
break
else:
raise ConnectionError('Device not found.')
self._client = BleakClient(self._device.address, self.loop)
status = await self._client.connect()
log.debug('Connection status: {status}'.format(status=status))
async def write(self, handle, data):
"""
Send data to given handle number.
If handle cannot be found in service description, hardcoded LEGO uuid will be used.
:param handle: Handle number that will be translated into characteristic uuid
:param data: data to send
:return: None
"""
log.debug('Request: {handle} {payload}'.format(handle=handle, payload=[hex(x) for x in data]))
desc = self._client.services.get_descriptor(handle)
if not isinstance(data, bytearray):
data = bytearray(data)
if desc is None:
# dedicated handle not found, try to send by using LEGO Move Hub default characteristic
await self._client.write_gatt_char(MOVE_HUB_HW_UUID_CHAR, data)
else:
await self._client.write_gatt_char(desc.characteristic_uuid, data)
async def write_char(self, characteristic_uuid, data):
"""
Send data to given handle number.
:param characteristic_uuid: Characteristic uuid used to send data
:param data: data to send
:return: None
"""
await self._client.write_gatt_char(characteristic_uuid, data)
async def set_notify_handler(self, handler):
"""
Set notification handler.
:param handler: Handle function to be called when receive any data.
:return: None
"""
def c(handle, data):
log.debug('Response: {handle} {payload}'.format(handle=handle, payload=[hex(x) for x in data]))
handler(handle, data)
await self._client.start_notify(MOVE_HUB_HW_UUID_CHAR, c)
def is_alive(self):
"""
To keep compatibility with the driver interface.
This method does nothing.
:return: None.
"""
pass

View File

@ -1,121 +0,0 @@
import logging
import re
from threading import Thread, Event
from bluepy import btle
from pylgbst.comms import Connection
from pylgbst.utilities import str2hex, queue
log = logging.getLogger('comms-bluepy')
COMPLETE_LOCAL_NAME_ADTYPE = 9
PROPAGATE_DISPATCHER_EXCEPTION = False
def _get_iface_number(controller):
"""bluepy uses iface numbers instead of full names."""
if not controller:
return None
m = re.search(r'hci(\d+)$', controller)
if not m:
raise ValueError('Cannot find iface number in {}.'.format(controller))
return int(m.group(1))
class BluepyDelegate(btle.DefaultDelegate):
def __init__(self, handler):
btle.DefaultDelegate.__init__(self)
self._handler = handler
def handleNotification(self, cHandle, data):
log.debug('Incoming notification')
self._handler(cHandle, data)
# We need a separate thread to wait for notifications,
# but calling peripheral's methods from different threads creates issues,
# so we will wrap all the calls into a thread
class BluepyThreadedPeripheral(object):
def __init__(self, addr, addrType, controller):
self._call_queue = queue.Queue()
self._addr = addr
self._addrType = addrType
self._iface_number = _get_iface_number(controller)
self._disconnect_event = Event()
self._dispatcher_thread = Thread(target=self._dispatch_calls)
self._dispatcher_thread.setDaemon(True)
self._dispatcher_thread.setName("Bluepy call dispatcher")
self._dispatcher_thread.start()
def _dispatch_calls(self):
self._peripheral = btle.Peripheral(self._addr, self._addrType, self._iface_number)
try:
while not self._disconnect_event.is_set():
try:
try:
method = self._call_queue.get(False)
method()
except queue.Empty:
pass
self._peripheral.waitForNotifications(1.)
except Exception as ex:
log.exception('Exception in call dispatcher thread', exc_info=ex)
if PROPAGATE_DISPATCHER_EXCEPTION:
log.error("Terminating dispatcher thread.")
raise
finally:
self._peripheral.disconnect()
def write(self, handle, data):
self._call_queue.put(lambda: self._peripheral.writeCharacteristic(handle, data))
def set_notify_handler(self, handler):
delegate = BluepyDelegate(handler)
self._call_queue.put(lambda: self._peripheral.withDelegate(delegate))
def disconnect(self):
self._disconnect_event.set()
class BluepyConnection(Connection):
def __init__(self, controller='hci0'):
Connection.__init__(self)
self._peripheral = None # :type BluepyThreadedPeripheral
self._controller = controller
def connect(self, hub_mac=None, hub_name=None):
log.debug("Trying to connect client to MoveHub with MAC: %s", hub_mac)
scanner = btle.Scanner()
while not self._peripheral:
log.info("Discovering devices...")
scanner.scan(1)
devices = scanner.getDevices()
for dev in devices:
address = dev.addr
address_type = dev.addrType
name = dev.getValueText(COMPLETE_LOCAL_NAME_ADTYPE)
if self._is_device_matched(address, name, hub_mac, hub_name):
self._peripheral = BluepyThreadedPeripheral(address, address_type, self._controller)
break
return self
def disconnect(self):
self._peripheral.disconnect()
def write(self, handle, data):
log.debug("Writing to handle %s: %s", handle, str2hex(data))
self._peripheral.write(handle, data)
def set_notify_handler(self, handler):
self._peripheral.set_notify_handler(handler)
def is_alive(self):
return True

View File

@ -1,127 +0,0 @@
import logging
import re
import threading
from time import sleep
import gatt
from pylgbst.comms import Connection, MOVE_HUB_HW_UUID_SERV, MOVE_HUB_HW_UUID_CHAR, \
MOVE_HUB_HARDWARE_HANDLE
from pylgbst.utilities import str2hex
log = logging.getLogger('comms-gatt')
class CustomDevice(gatt.Device, object):
def __init__(self, mac_address, manager):
gatt.Device.__init__(self, mac_address=mac_address, manager=manager)
self._notify_callback = lambda hnd, val: None
self._handle = None
def connect(self):
gatt.Device.connect(self)
log.info("Waiting for device connection...")
while self._handle is None:
log.debug("Sleeping...")
sleep(1)
if isinstance(self._handle, BaseException):
exc = self._handle
self._handle = None
raise exc
def write(self, data):
log.debug("Writing to handle: %s", str2hex(data))
return self._handle.write_value(data)
def enable_notifications(self):
log.debug('Enable Notifications...')
self._handle.enable_notifications()
def set_notific_handler(self, func_hnd):
self._notify_callback = func_hnd
def services_resolved(self):
log.debug('Getting MoveHub services and characteristics...')
gatt.Device.services_resolved(self)
log.debug("[%s] Resolved services", self.mac_address)
for service in self.services:
log.debug("[%s] Service [%s]", self.mac_address, service.uuid)
for characteristic in service.characteristics:
log.debug("[%s] Characteristic [%s]", self.mac_address, characteristic.uuid)
if service.uuid == MOVE_HUB_HW_UUID_SERV and characteristic.uuid == MOVE_HUB_HW_UUID_CHAR:
log.debug('MoveHub characteristic found')
self._handle = characteristic
if self._handle is None:
self.manager.stop()
self._handle = RuntimeError("Failed to obtain MoveHub handle")
def characteristic_value_updated(self, characteristic, value):
value = self._fix_weird_bug(value)
log.debug('Notification in GattDevice: %s', str2hex(value))
self._notify_callback(MOVE_HUB_HARDWARE_HANDLE, value)
def _fix_weird_bug(self, value):
if isinstance(value, str) and "dbus.Array" in value: # weird bug from gatt on my Ubuntu 16.04!
log.debug("Fixing broken notify string: %s", value)
return ''.join([chr(int(x.group(1))) for x in re.finditer(r"dbus.Byte\((\d+)\)", value)])
return value
class GattConnection(Connection):
"""
:type _device: CustomDevice
"""
def __init__(self, bt_iface_name='hci0'):
super(GattConnection, self).__init__()
self._device = None
self._iface = bt_iface_name
try:
self._manager = gatt.DeviceManager(adapter_name=self._iface)
except TypeError:
raise NotImplementedError("Gatt is not implemented for this platform")
self._manager_thread = threading.Thread(target=self._manager.run)
self._manager_thread.setDaemon(True)
log.debug('Starting DeviceManager...')
def connect(self, hub_mac=None, hub_name=None):
self._manager_thread.start()
self._manager.start_discovery()
while not self._device:
log.info("Discovering devices...")
devices = self._manager.devices()
log.debug("Devices: %s", devices)
for dev in devices:
address = dev.mac_address
name = dev.alias()
if self._is_device_matched(address, name, hub_mac, hub_name):
self._device = CustomDevice(address, self._manager)
break
if not self._device:
sleep(1)
self._device.connect()
return self
def disconnect(self):
self._manager.stop()
self._device.disconnect()
def write(self, handle, data):
self._device.write(data)
def set_notify_handler(self, handler):
self._device.set_notific_handler(handler)
def enable_notifications(self):
self._device.enable_notifications()
def is_alive(self):
return self._manager_thread.isAlive()

View File

@ -1,94 +0,0 @@
# noinspection PyMethodOverriding
import logging
import traceback
from threading import Thread
from gattlib import DiscoveryService, GATTRequester
from pylgbst.comms import Connection
from pylgbst.utilities import queue, str2hex
log = logging.getLogger('comms-gattlib')
class Requester(GATTRequester):
"""
Wrapper to access `on_notification` capability of GATT
Set "notification_sink" field to a callable that will handle incoming data
"""
def __init__(self, p_object, *args, **kwargs):
super(Requester, self).__init__(p_object, *args, **kwargs)
self.notification_sink = None
self._notify_queue = queue.Queue() # this queue is to minimize time spent in gattlib C code
self.notify_thread = Thread(target=self._dispatch_notifications)
self.notify_thread.setDaemon(True)
self.notify_thread.setName("Notify queue dispatcher")
self.notify_thread.start()
def on_notification(self, handle, data):
# log.debug("requester notified, sink: %s", self.notification_sink)
self._notify_queue.put((handle, data))
def on_indication(self, handle, data):
log.debug("Indication on handle %s: %s", handle, str2hex(data))
def _dispatch_notifications(self):
while True:
handle, data = self._notify_queue.get()
data = data[3:] # for some reason, there are extra bytes
if self.notification_sink:
try:
self.notification_sink(handle, data)
except BaseException:
log.warning("Data was: %s", str2hex(data))
log.warning("Failed to dispatch notification: %s", traceback.format_exc())
else:
log.warning("Dropped notification %s: %s", handle, str2hex(data))
class GattLibConnection(Connection):
"""
Main transport class, uses real Bluetooth LE connection.
Loops with timeout of 1 seconds to find device named "Lego MOVE Hub"
:type requester: Requester
"""
def __init__(self, bt_iface_name='hci0'):
super(GattLibConnection, self).__init__()
self.requester = None
self._iface = bt_iface_name
def connect(self, hub_mac=None, hub_name=None):
service = DiscoveryService(self._iface)
while not self.requester:
log.info("Discovering devices using %s...", self._iface)
devices = service.discover(1)
log.debug("Devices: %s", devices)
for address, name in devices.items():
if self._is_device_matched(address, name, hub_mac, hub_name):
self.requester = Requester(address, True, self._iface)
break
if self.requester:
break
return self
def set_notify_handler(self, handler):
if self.requester:
log.debug("Setting notification handler: %s", handler)
self.requester.notification_sink = handler
else:
raise RuntimeError("No requester available")
def write(self, handle, data):
log.debug("Writing to %s: %s", handle, str2hex(data))
return self.requester.write_by_handle(handle, data)
def is_alive(self):
return self.requester.notify_thread.isAlive()

View File

@ -1,63 +0,0 @@
import logging
import pygatt
from pylgbst.comms import Connection, MOVE_HUB_HW_UUID_CHAR
from pylgbst.utilities import str2hex
log = logging.getLogger('comms-pygatt')
class GattoolConnection(Connection):
"""
Used for connecting to
:type _conn_hnd: pygatt.backends.bgapi.device.BGAPIBLEDevice
"""
def __init__(self, controller='hci0'):
Connection.__init__(self)
self.backend = lambda: pygatt.GATTToolBackend(hci_device=controller)
self._conn_hnd = None
def connect(self, hub_mac=None, hub_name=None):
log.debug("Trying to connect client to MoveHub with MAC: %s", hub_mac)
adapter = self.backend()
adapter.start() # enable or disable restart? What's the best?
while not self._conn_hnd:
log.info("Discovering devices...")
devices = adapter.scan(1)
log.debug("Devices: %s", devices)
# Pass each device found to _is_device_matched( ) to see if it the device we want
for dev in devices:
address = dev['address']
name = dev['name']
if self._is_device_matched(address, name, hub_mac, hub_name):
self._conn_hnd = adapter.connect(address)
break
if self._conn_hnd:
break
return self
def disconnect(self):
self._conn_hnd.disconnect()
def write(self, handle, data):
log.debug("Writing to handle %s: %s", handle, str2hex(data))
return self._conn_hnd.char_write_handle(handle, bytearray(data))
def set_notify_handler(self, handler):
self._conn_hnd.subscribe(MOVE_HUB_HW_UUID_CHAR, handler)
def is_alive(self):
return True
class BlueGigaConnection(GattoolConnection):
def __init__(self):
super(BlueGigaConnection, self).__init__()
self.backend = lambda: pygatt.BGAPIBackend()

138
pylgbst/constants.py Normal file
View File

@ -0,0 +1,138 @@
import binascii
import struct
import sys
if sys.version_info[0] == 2:
import Queue as queue
else:
import queue as queue
queue = queue # just to use it
def str2hex(data):
return binascii.hexlify(data).decode("utf8")
def usbyte(seq, index):
return struct.unpack("<B", seq[index:index + 1])[0]
def ushort(seq, index):
return struct.unpack("<H", seq[index:index + 2])[0]
# GENERAL
MOVE_HUB_HARDWARE_HANDLE = 0x0E
MOVE_HUB_HARDWARE_UUID = '00001624-1212-efde-1623-785feabcd123'
PACKET_VER = 0x01
# PORTS
PORT_C = 0x01
PORT_D = 0x02
PORT_LED = 0x32
PORT_A = 0x37
PORT_B = 0x38
PORT_AB = 0x39
PORT_TILT_SENSOR = 0x3A
PORT_AMPERAGE = 0x3B
PORT_VOLTAGE = 0x3C
PORTS = {
PORT_A: "A",
PORT_B: "B",
PORT_AB: "AB",
PORT_C: "C",
PORT_D: "D",
PORT_LED: "LED",
PORT_TILT_SENSOR: "TILT_SENSOR",
PORT_AMPERAGE: "AMPERAGE",
PORT_VOLTAGE: "VOLTAGE",
}
# PACKET TYPES
MSG_DEVICE_INFO = 0x01
# 0501010305 gives 090001030600000010
MSG_DEVICE_SHUTDOWN = 0x02 # sent when hub shuts down by button hold
MSG_PING_RESPONSE = 0x03
MSG_PORT_INFO = 0x04
MSG_PORT_CMD_ERROR = 0x05
MSG_SET_PORT_VAL = 0x81
MSG_PORT_STATUS = 0x82
MSG_SENSOR_SUBSCRIBE = 0x41
MSG_SENSOR_SOMETHING1 = 0x42 # it is seen close to sensor subscribe commands. Subscription options? Initial value?
MSG_SENSOR_DATA = 0x45
MSG_SENSOR_SUBSCRIBE_ACK = 0x47
# DEVICE TYPES
DEV_VOLTAGE = 0x14
DEV_AMPERAGE = 0x15
DEV_LED = 0x17
DEV_DCS = 0x25
DEV_IMOTOR = 0x26
DEV_MOTOR = 0x27
DEV_TILT_SENSOR = 0x28
DEVICE_TYPES = {
DEV_DCS: "DISTANCE_COLOR_SENSOR",
DEV_IMOTOR: "IMOTOR",
DEV_MOTOR: "MOTOR",
DEV_TILT_SENSOR: "TILT_SENSOR",
DEV_LED: "LED",
DEV_AMPERAGE: "AMPERAGE",
DEV_VOLTAGE: "VOLTAGE",
}
# NOTIFICATIONS
STATUS_STARTED = 0x01
STATUS_CONFLICT = 0x05
STATUS_FINISHED = 0x0a
STATUS_INPROGRESS = 0x0c # FIXME: not sure about description
STATUS_INTERRUPTED = 0x0e # FIXME: not sure about description
# COLORS
COLOR_BLACK = 0x00
COLOR_PINK = 0x01
COLOR_PURPLE = 0x02
COLOR_BLUE = 0x03
COLOR_LIGHTBLUE = 0x04
COLOR_CYAN = 0x05
COLOR_GREEN = 0x06
COLOR_YELLOW = 0x07
COLOR_ORANGE = 0x09
COLOR_RED = 0x09
COLOR_WHITE = 0x0a
COLOR_NONE = 0xFF
COLORS = {
COLOR_BLACK: "BLACK",
COLOR_PINK: "PINK",
COLOR_PURPLE: "PURPLE",
COLOR_BLUE: "BLUE",
COLOR_LIGHTBLUE: "LIGHTBLUE",
COLOR_CYAN: "CYAN",
COLOR_GREEN: "GREEN",
COLOR_YELLOW: "YELLOW",
COLOR_ORANGE: "ORANGE",
COLOR_RED: "RED",
COLOR_WHITE: "WHITE",
COLOR_NONE: "NONE"
}
# DEVICE INFO
INFO_DEVICE_NAME = 0x01
INFO_BUTTON_STATE = 0x02
INFO_FIRMWARE_VERSION = 0x03
INFO_SOME4 = 0x04
INFO_SOME5_JITTERING = 0x05
INFO_SOME6 = 0x06
INFO_SOME7 = 0x07
INFO_MANUFACTURER = 0x08
INFO_HW_VERSION = 0x09
INFO_SOME10 = 0x0a
INFO_SOME11 = 0x0b
INFO_SOME12 = 0x0c
INFO_ACTION_SUBSCRIBE = 0x02
INFO_ACTION_UNSUBSCRIBE = 0x03
INFO_ACTION_GET = 0x05

View File

@ -1,292 +0,0 @@
import threading
import time
from pylgbst import get_connection_auto
from pylgbst.messages import *
from pylgbst.peripherals import *
from pylgbst.utilities import queue
from pylgbst.utilities import str2hex, usbyte, ushort
log = logging.getLogger('hub')
PERIPHERAL_TYPES = {
MsgHubAttachedIO.DEV_MOTOR: Motor,
MsgHubAttachedIO.DEV_MOTOR_EXTERNAL_TACHO: EncodedMotor,
MsgHubAttachedIO.DEV_MOTOR_INTERNAL_TACHO: EncodedMotor,
MsgHubAttachedIO.DEV_VISION_SENSOR: VisionSensor,
MsgHubAttachedIO.DEV_RGB_LIGHT: LEDRGB,
MsgHubAttachedIO.DEV_TILT_EXTERNAL: TiltSensor,
MsgHubAttachedIO.DEV_TILT_INTERNAL: TiltSensor,
MsgHubAttachedIO.DEV_CURRENT: Current,
MsgHubAttachedIO.DEV_VOLTAGE: Voltage,
}
class Hub(object):
"""
:type connection: pylgbst.comms.Connection
:type peripherals: dict[int,Peripheral]
"""
HUB_HARDWARE_HANDLE = 0x0E
def __init__(self, connection=None):
self._msg_handlers = []
self.peripherals = {}
self._sync_request = None
self._sync_replies = queue.Queue(1)
self._sync_lock = threading.Lock()
self.add_message_handler(MsgHubAttachedIO, self._handle_device_change)
self.add_message_handler(MsgPortValueSingle, self._handle_sensor_data)
self.add_message_handler(MsgPortValueCombined, self._handle_sensor_data)
self.add_message_handler(MsgGenericError, self._handle_error)
self.add_message_handler(MsgHubAction, self._handle_action)
if not connection:
connection = get_connection_auto() # TODO: how to identify the hub?
self.connection = connection
self.connection.set_notify_handler(self._notify)
self.connection.enable_notifications()
def __del__(self):
if self.connection and self.connection.is_alive():
self.connection.disconnect()
def add_message_handler(self, classname, callback):
self._msg_handlers.append((classname, callback))
def send(self, msg):
"""
:type msg: pylgbst.messages.DownstreamMsg
:rtype: pylgbst.messages.UpstreamMsg
"""
log.debug("Send message: %r", msg)
msgbytes = msg.bytes()
if msg.needs_reply:
with self._sync_lock:
assert not self._sync_request, "Pending request %r while trying to put %r" % (self._sync_request, msg)
self._sync_request = msg
log.debug("Waiting for sync reply to %r...", msg)
self.connection.write(self.HUB_HARDWARE_HANDLE, msgbytes)
resp = self._sync_replies.get()
log.debug("Fetched sync reply: %r", resp)
if isinstance(resp, MsgGenericError):
raise RuntimeError(resp.message())
return resp
else:
self.connection.write(self.HUB_HARDWARE_HANDLE, msgbytes)
return None
def _notify(self, handle, data):
log.debug("Notification on %s: %s", handle, str2hex(data))
msg = self._get_upstream_msg(data)
with self._sync_lock:
if self._sync_request:
if self._sync_request.is_reply(msg):
log.debug("Found matching upstream msg: %r", msg)
self._sync_replies.put(msg)
self._sync_request = None
for msg_class, handler in self._msg_handlers:
if isinstance(msg, msg_class):
log.debug("Handling msg with %s: %r", handler, msg)
handler(msg)
def _get_upstream_msg(self, data):
msg_type = usbyte(data, 2)
msg = None
for msg_kind in UPSTREAM_MSGS:
if msg_type == msg_kind.TYPE:
msg = msg_kind.decode(data)
log.debug("Decoded message: %r", msg)
break
assert msg
return msg
def _handle_error(self, msg):
log.warning("Command error: %s", msg.message())
with self._sync_lock:
if self._sync_request:
self._sync_request = None
self._sync_replies.put(msg)
def _handle_action(self, msg):
"""
:type msg: MsgHubAction
"""
if msg.action == MsgHubAction.UPSTREAM_DISCONNECT:
log.warning("Hub disconnects")
self.connection.disconnect()
elif msg.action == MsgHubAction.UPSTREAM_SHUTDOWN:
log.warning("Hub switches off")
self.connection.disconnect()
def _handle_device_change(self, msg):
if msg.event == MsgHubAttachedIO.EVENT_DETACHED:
log.debug("Detaching peripheral: %s", self.peripherals[msg.port])
self.peripherals.pop(msg.port)
return
assert msg.event in (msg.EVENT_ATTACHED, msg.EVENT_ATTACHED_VIRTUAL)
port = msg.port
dev_type = ushort(msg.payload, 0)
if dev_type in PERIPHERAL_TYPES:
self.peripherals[port] = PERIPHERAL_TYPES[dev_type](self, port)
else:
log.warning("Have not dedicated class for peripheral type %x on port %x", dev_type, port)
self.peripherals[port] = Peripheral(self, port)
log.info("Attached peripheral: %s", self.peripherals[msg.port])
if msg.event == msg.EVENT_ATTACHED:
hw_revision = reversed([usbyte(msg.payload, x) for x in range(2, 6)])
sw_revision = reversed([usbyte(msg.payload, x) for x in range(6, 10)])
# what to do with this info? it's useless, I guess
del hw_revision, sw_revision
elif msg.event == msg.EVENT_ATTACHED_VIRTUAL:
self.peripherals[port].virtual_ports = (usbyte(msg.payload, 2), usbyte(msg.payload, 3))
def _handle_sensor_data(self, msg):
assert isinstance(msg, (MsgPortValueSingle, MsgPortValueCombined))
if msg.port not in self.peripherals:
log.warning("Notification on port with no device: %s", msg.port)
return
device = self.peripherals[msg.port]
device.queue_port_data(msg)
def disconnect(self):
self.send(MsgHubAction(MsgHubAction.DISCONNECT))
def switch_off(self):
self.send(MsgHubAction(MsgHubAction.SWITCH_OFF))
class MoveHub(Hub):
"""
Class implementing Lego Boost's MoveHub specifics
:type led: LEDRGB
:type tilt_sensor: TiltSensor
:type button: Button
:type current: Current
:type voltage: Voltage
:type vision_sensor: pylgbst.peripherals.VisionSensor
:type port_C: Peripheral
:type port_D: Peripheral
:type motor_A: EncodedMotor
:type motor_B: EncodedMotor
:type motor_AB: EncodedMotor
:type motor_external: EncodedMotor
"""
DEFAULT_NAME = "LEGO Move Hub"
# PORTS
PORT_A = 0x00
PORT_B = 0x01
PORT_C = 0x02
PORT_D = 0x03
PORT_AB = 0x10
PORT_LED = 0x32
PORT_TILT_SENSOR = 0x3A
PORT_CURRENT = 0x3B
PORT_VOLTAGE = 0x3C
# noinspection PyTypeChecker
def __init__(self, connection=None):
self._comm_lock = threading.RLock()
if connection is None:
connection = get_connection_auto(hub_name=self.DEFAULT_NAME)
super(MoveHub, self).__init__(connection)
self.info = {}
# shorthand fields
self.button = Button(self)
self.led = None
self.current = None
self.voltage = None
self.motor_A = None
self.motor_B = None
self.motor_AB = None
self.vision_sensor = None
self.tilt_sensor = None
self.motor_external = None
self.port_C = None
self.port_D = None
self._wait_for_devices()
self._report_status()
def _wait_for_devices(self, get_dev_set=None):
if not get_dev_set:
get_dev_set = lambda: (self.motor_A, self.motor_B, self.motor_AB, self.led, self.tilt_sensor,
self.current, self.voltage)
for num in range(0, 100):
devices = get_dev_set()
if all(devices):
log.debug("All devices are present: %s", devices)
return
log.debug("Waiting for builtin devices to appear: %s", devices)
time.sleep(0.1)
log.warning("Got only these devices: %s", get_dev_set())
def _report_status(self):
# maybe add firmware version
name = self.send(MsgHubProperties(MsgHubProperties.ADVERTISE_NAME, MsgHubProperties.UPD_REQUEST))
mac = self.send(MsgHubProperties(MsgHubProperties.PRIMARY_MAC, MsgHubProperties.UPD_REQUEST))
log.info("%s on %s", name.payload, str2hex(mac.payload))
voltage = self.send(MsgHubProperties(MsgHubProperties.VOLTAGE_PERC, MsgHubProperties.UPD_REQUEST))
assert isinstance(voltage, MsgHubProperties)
log.info("Voltage: %s%%", usbyte(voltage.parameters, 0))
voltage = self.send(MsgHubAlert(MsgHubAlert.LOW_VOLTAGE, MsgHubAlert.UPD_REQUEST))
assert isinstance(voltage, MsgHubAlert)
if not voltage.is_ok():
log.warning("Low voltage, check power source (maybe replace battery)")
# noinspection PyTypeChecker
def _handle_device_change(self, msg):
with self._comm_lock:
super(MoveHub, self)._handle_device_change(msg)
if isinstance(msg, MsgHubAttachedIO) and msg.event != MsgHubAttachedIO.EVENT_DETACHED:
port = msg.port
if port == self.PORT_A:
self.motor_A = self.peripherals[port]
elif port == self.PORT_B:
self.motor_B = self.peripherals[port]
elif port == self.PORT_AB:
self.motor_AB = self.peripherals[port]
elif port == self.PORT_C:
self.port_C = self.peripherals[port]
elif port == self.PORT_D:
self.port_D = self.peripherals[port]
elif port == self.PORT_LED:
self.led = self.peripherals[port]
elif port == self.PORT_TILT_SENSOR:
self.tilt_sensor = self.peripherals[port]
elif port == self.PORT_CURRENT:
self.current = self.peripherals[port]
elif port == self.PORT_VOLTAGE:
self.voltage = self.peripherals[port]
if type(self.peripherals[port]) == VisionSensor:
self.vision_sensor = self.peripherals[port]
elif type(self.peripherals[port]) == EncodedMotor \
and port not in (self.PORT_A, self.PORT_B, self.PORT_AB):
self.motor_external = self.peripherals[port]
class TrainHub(Hub):
DEFAULT_NAME = 'TrainHub'
def __init__(self, connection=None):
if connection is None:
connection = get_connection_auto(hub_name=self.DEFAULT_NAME)
super(TrainHub, self).__init__(connection)

View File

@ -1,745 +0,0 @@
import logging
from struct import pack, unpack
from pylgbst.utilities import str2hex
log = logging.getLogger('hub')
class Message(object):
TYPE = None
def __init__(self):
self.hub_id = 0x00 # not used according to official doc
self.payload = b""
def bytes(self):
"""
see https://lego.github.io/lego-ble-wireless-protocol-docs/#common-message-header
"""
msglen = len(self.payload) + 3
assert msglen < 127, "TODO: handle longer messages with 2-byte len"
return pack("<B", msglen) + pack("<B", self.hub_id) + pack("<B", self.TYPE) + self.payload
def __repr__(self):
# assert self.bytes() # to trigger any field changes
data = self.__dict__
data = {x: (str2hex(y) if isinstance(y, bytes) else y)
for x, y in data.items()
if x not in ('hub_id',)}
return self.__class__.__name__ + "(%s)" % data
class DownstreamMsg(Message):
def __init__(self):
super(DownstreamMsg, self).__init__()
self.needs_reply = False
def is_reply(self, msg):
del msg
return False
class UpstreamMsg(Message):
def __init__(self):
super(UpstreamMsg, self).__init__()
@classmethod
def decode(cls, data):
"""
see https://lego.github.io/lego-ble-wireless-protocol-docs/#common-message-header
"""
msg = cls()
msg.payload = data
msglen = msg._byte()
assert msglen < 127, "TODO: handle longer messages with 2-byte len"
hub_id = msg._byte()
assert hub_id == 0
msg_type = msg._byte()
assert cls.TYPE == msg_type, "Message type does not match: %x!=%x" % (cls.TYPE, msg_type)
assert isinstance(msg.payload, (bytes, bytearray))
return msg
def __shift(self, vtype, vlen):
val = self.payload[0:vlen]
self.payload = self.payload[vlen:]
return unpack("<" + vtype, val)[0]
def _byte(self):
return self.__shift("B", 1)
def _short(self):
return self.__shift("H", 2)
def _long(self):
return self.__shift("I", 4)
def _float(self):
return self.__shift("f", 4)
def _bits_list(self, val):
res = []
x = 1
for i in range(16 + 1):
if val & x:
res.append(i)
x <<= 1
return res
class MsgHubProperties(DownstreamMsg, UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#hub-properties
"""
TYPE = 0x01
ADVERTISE_NAME = 0x01
BUTTON = 0x02
FW_VERSION = 0x03
HW_VERSION = 0x04
RSSI = 0x05
VOLTAGE_PERC = 0x06
BATTERY_TYPE = 0x07
MANUFACTURER = 0x08
RADIO_FW_VERSION = 0x09
WIRELESS_PROTO_VERSION = 0x0A
SYSTEM_TYPE_ID = 0x0B
HW_NETW_ID = 0x0C
PRIMARY_MAC = 0x0D
SECONDARY_MAC = 0x0E
HARDWARE_NETWORK_FAMILY = 0x0F
SET = 0x01
UPD_ENABLE = 0x02
UPD_DISABLE = 0x03
RESET = 0x04
UPD_REQUEST = 0x05
UPSTREAM_UPDATE = 0x06
def __init__(self, prop=None, operation=None, parameters=b""):
super(MsgHubProperties, self).__init__()
self.property = prop
self.operation = operation
self.parameters = parameters
def bytes(self):
if self.operation in (self.UPD_REQUEST, self.UPD_ENABLE):
self.needs_reply = True
self.payload = pack("<B", self.property) + pack("<B", self.operation) + self.parameters
return super(MsgHubProperties, self).bytes()
@classmethod
def decode(cls, data):
msg = super(MsgHubProperties, cls).decode(data)
assert isinstance(msg, MsgHubProperties)
msg.property = msg._byte()
msg.operation = msg._byte()
msg.parameters = msg.payload
return msg
def is_reply(self, msg):
return isinstance(msg, MsgHubProperties) \
and msg.operation == self.UPSTREAM_UPDATE and msg.property == self.property
class MsgHubAction(DownstreamMsg, UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#hub-actions
"""
TYPE = 0x02
SWITCH_OFF = 0x01
DISCONNECT = 0x02
VCC_PORT_CONTROL_ON = 0x03
VCC_PORT_CONTROL_OFF = 0x04
BUSY_INDICATION_ON = 0x05
BUSY_INDICATION_OFF = 0x06
SWITCH_OFF_IMMEDIATELY = 0x2F
UPSTREAM_SHUTDOWN = 0x30
UPSTREAM_DISCONNECT = 0x31
UPSTREAM_BOOT_MODE = 0x32
def __init__(self, action=None):
super(MsgHubAction, self).__init__()
self.action = action
def bytes(self):
self.payload = pack("<B", self.action)
self.needs_reply = self.action in (self.DISCONNECT, self.SWITCH_OFF)
return super(MsgHubAction, self).bytes()
def is_reply(self, msg):
if not isinstance(msg, MsgHubAction):
raise TypeError("Unexpected message type: %s" % (msg.__class__,))
if self.action == self.DISCONNECT and msg.action == self.UPSTREAM_DISCONNECT:
return True
if self.action == self.SWITCH_OFF and msg.action == self.UPSTREAM_SHUTDOWN:
return True
@classmethod
def decode(cls, data):
msg = super(MsgHubAction, cls).decode(data)
assert isinstance(msg, MsgHubAction)
msg.action = msg._byte()
return msg
class MsgHubAlert(DownstreamMsg, UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#hub-alerts
"""
TYPE = 0x03
LOW_VOLTAGE = 0x01
HIGH_CURRENT = 0x02
LOW_SIGNAL = 0x03
OVER_POWER = 0x04
DESCR = {
LOW_VOLTAGE: "low voltage",
HIGH_CURRENT: "high current",
LOW_SIGNAL: "low signal",
OVER_POWER: "over power"
}
UPD_ENABLE = 0x01
UPD_DISABLE = 0x02
UPD_REQUEST = 0x03
UPSTREAM_UPDATE = 0x04
def __init__(self, atype=None, operation=None):
super(MsgHubAlert, self).__init__()
self.atype = atype
self.operation = operation
self.status = None
def bytes(self):
self.payload = pack("<B", self.atype) + pack("<B", self.operation)
if self.operation == self.UPD_REQUEST:
self.needs_reply = True
return super(MsgHubAlert, self).bytes()
@classmethod
def decode(cls, data):
msg = super(MsgHubAlert, cls).decode(data)
assert isinstance(msg, MsgHubAlert)
msg.atype = msg._byte()
msg.operation = msg._byte()
msg.status = msg._byte()
assert msg.operation == cls.UPSTREAM_UPDATE
return msg
def is_ok(self):
return not self.status
def is_reply(self, msg):
return isinstance(msg, MsgHubAlert) \
and msg.operation == self.UPSTREAM_UPDATE and msg.atype == self.atype
class MsgHubAttachedIO(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#hub-attached-i-o
"""
TYPE = 0x04
EVENT_DETACHED = 0x00
EVENT_ATTACHED = 0x01
EVENT_ATTACHED_VIRTUAL = 0x02
# DEVICE TYPES
DEV_MOTOR = 0x0001
DEV_SYSTEM_TRAIN_MOTOR = 0x0002
DEV_BUTTON = 0x0005
DEV_LED_LIGHT = 0x0008
DEV_VOLTAGE = 0x0014
DEV_CURRENT = 0x0015
DEV_PIEZO_SOUND = 0x0016
DEV_RGB_LIGHT = 0x0017
DEV_TILT_EXTERNAL = 0x0022
DEV_MOTION_SENSOR = 0x0023
DEV_VISION_SENSOR = 0x0025
DEV_MOTOR_EXTERNAL_TACHO = 0x0026
DEV_MOTOR_INTERNAL_TACHO = 0x0027
DEV_TILT_INTERNAL = 0x0028
def __init__(self):
super(MsgHubAttachedIO, self).__init__()
self.port = None
self.event = None
@classmethod
def decode(cls, data):
msg = super(MsgHubAttachedIO, cls).decode(data)
assert isinstance(msg, MsgHubAttachedIO)
msg.port = msg._byte()
msg.event = msg._byte()
return msg
class MsgGenericError(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#generic-error-messages
"""
TYPE = 0x05
ERR_ACK = 0x01 # ACK
ERR_MACK = 0x02 # MACK
ERR_BUFFER_OVERFLOW = 0x03 # Buffer Overflow
ERR_TIMEOUT = 0x04 # Timeout
ERR_WRONG_COMMAND = 0x05 # Command NOT recognized
ERR_WRONG_PARAMS = 0x06 # Invalid use (e.g. parameter error(s)
ERR_OVERCURRENT = 0x07
ERR_INTERNAL = 0x08
DESCR = {
ERR_ACK: "ACK",
ERR_MACK: "MACK",
ERR_BUFFER_OVERFLOW: "Buffer Overflow",
ERR_TIMEOUT: "Timeout",
ERR_WRONG_COMMAND: "Command NOT recognized",
ERR_WRONG_PARAMS: "Invalid use (e.g. parameter error(s)",
ERR_OVERCURRENT: "Overcurrent",
ERR_INTERNAL: "Internal ERROR",
}
def __init__(self):
super(MsgGenericError, self).__init__()
self.cmd = None
self.err = None
@classmethod
def decode(cls, data):
msg = super(MsgGenericError, cls).decode(data)
assert isinstance(msg, MsgGenericError)
msg.cmd = msg._byte()
msg.err = msg._byte()
return msg
def message(self):
return "Command 0x%x caused error 0x%x: %s" % (self.cmd, self.err, self.DESCR[self.err])
class MsgPortInfoRequest(DownstreamMsg):
"""
This is sync request for value on port
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-information-request
"""
TYPE = 0x21
INFO_PORT_VALUE = 0x00
INFO_MODE_INFO = 0x01
INFO_MODE_COMBINATIONS = 0x02
def __init__(self, port, info_type):
super(MsgPortInfoRequest, self).__init__()
self.port = port
self.info_type = info_type
self.needs_reply = True
def bytes(self):
self.payload = pack("<B", self.port) + pack("<B", self.info_type)
return super(MsgPortInfoRequest, self).bytes()
def is_reply(self, msg):
if msg.port != self.port:
return False
if self.info_type == self.INFO_PORT_VALUE:
return isinstance(msg, (MsgPortValueSingle, MsgPortValueCombined))
else:
return isinstance(msg, (MsgPortInfo,))
class MsgPortModeInfoRequest(DownstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-mode-information-request
"""
TYPE = 0x22
INFO_NAME = 0x00
INFO_RAW_RANGE = 0x01
INFO_PCT_RANGE = 0x02
INFO_SI_RANGE = 0x03 # no idea what 'SI' stands for
INFO_UNITS = 0x04
INFO_MAPPING = 0x05
# INFO_INTERNAL = 0x06
INFO_MOTOR_BIAS = 0x07
INFO_CAPABILITY_BITS = 0x08
INFO_VALUE_FORMAT = 0x80
INFO_TYPES = {
INFO_NAME: "Name",
INFO_RAW_RANGE: "Raw range",
INFO_PCT_RANGE: "Percent range",
INFO_SI_RANGE: "SI value range",
INFO_UNITS: "Units",
INFO_MAPPING: "Mapping",
INFO_MOTOR_BIAS: "Motor bias",
INFO_CAPABILITY_BITS: "Capabilities",
INFO_VALUE_FORMAT: "Value encoding",
}
def __init__(self, port, mode, info_type):
super(MsgPortModeInfoRequest, self).__init__()
self.port = port
self.mode = mode
self.info_type = info_type
self.payload = pack("<B", port) + pack("<B", mode) + pack("<B", info_type)
self.needs_reply = True
def is_reply(self, msg):
if not isinstance(msg, MsgPortModeInfo):
return False
if msg.port != self.port or msg.mode != self.mode or msg.info_type != self.info_type:
return False
return True
class MsgPortInputFmtSetupSingle(DownstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-input-format-setup-single
"""
TYPE = 0x41
def __init__(self, port, mode, delta=1, update_enable=0):
super(MsgPortInputFmtSetupSingle, self).__init__()
self.port = port
self.mode = mode
self.updates_enabled = update_enable
self.update_delta = delta
self.payload = pack("<B", port) + pack("<B", mode) + pack("<I", delta) + pack("<B", update_enable)
self.needs_reply = True
def is_reply(self, msg):
if isinstance(msg, MsgPortInputFmtSingle) and msg.port == self.port:
return True
class MsgPortInputFmtSetupCombined(DownstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-input-format-setup-combinedmode
"""
TYPE = 0x42
def __init__(self, port, mode, delta=1, update_enable=0):
super(MsgPortInputFmtSetupCombined, self).__init__()
self.port = port
self.payload = pack("<B", port) + pack("<B", mode) + pack("<I", delta) + pack("<B", update_enable)
self.needs_reply = True
def is_reply(self, msg):
if isinstance(msg, MsgPortInputFmtCombined) and msg.port == self.port:
return True
class MsgPortInfo(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-information
"""
TYPE = 0x43
CAP_OUTPUT = 0b00000001
CAP_INPUT = 0b00000010
CAP_COMBINABLE = 0b00000100
CAP_SYNCHRONIZABLE = 0b00001000
def __init__(self):
super(MsgPortInfo, self).__init__()
self.port = None
self.info_type = None
self.capabilities = None
self.total_modes = None
self.input_modes = None
self.output_modes = None
self.possible_mode_combinations = []
@classmethod
def decode(cls, data):
msg = super(MsgPortInfo, cls).decode(data)
assert isinstance(msg, MsgPortInfo)
msg.port = msg._byte()
msg.info_type = msg._byte()
if msg.info_type == MsgPortInfoRequest.INFO_MODE_INFO:
msg.capabilities = msg._byte()
msg.total_modes = msg._byte()
msg.input_modes = msg._bits_list(msg._short())
msg.output_modes = msg._bits_list(msg._short())
else:
while msg.payload:
# https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#pos-m
val = msg._short()
msg.possible_mode_combinations.append(msg._bits_list(val))
if not val:
break
return msg
def is_output(self):
assert self.info_type == MsgPortInfoRequest.INFO_MODE_INFO
return bool(self.capabilities & self.CAP_OUTPUT)
def is_input(self):
assert self.info_type == MsgPortInfoRequest.INFO_MODE_INFO
return bool(self.capabilities & self.CAP_INPUT)
def is_combinable(self):
assert self.info_type == MsgPortInfoRequest.INFO_MODE_INFO
return bool(self.capabilities & self.CAP_COMBINABLE)
def is_synchronizable(self):
assert self.info_type == MsgPortInfoRequest.INFO_MODE_INFO
return bool(self.capabilities & self.CAP_SYNCHRONIZABLE)
class MsgPortModeInfo(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-mode-information
"""
TYPE = 0x44
MAPPING_FLAGS = {
7: "Supports NULL value",
6: "Supports Functional Mapping 2.0+",
5: "N/A",
4: "Absolute [min..max]",
3: "Relative [-1..1]",
2: "Discrete [0, 1, 2, 3]",
1: "N/A",
0: "N/A",
}
DATASET_TYPES = {
0b00: "8 bit",
0b01: "16 bit",
0b10: "32 bit",
0b11: "FLOAT",
}
def __init__(self):
super(MsgPortModeInfo, self).__init__()
self.port = None
self.mode = None
self.info_type = None # @see MsgPortModeInfoRequest
self.value = None
@classmethod
def decode(cls, data):
msg = super(MsgPortModeInfo, cls).decode(data)
assert isinstance(msg, MsgPortModeInfo)
msg.port = msg._byte()
msg.mode = msg._byte()
msg.info_type = msg._byte()
msg.value = msg._value()
return msg
def _value(self):
info = MsgPortModeInfoRequest
if self.info_type == info.INFO_NAME:
return self.payload[:self.payload.index(b"\00")].decode('ascii')
elif self.info_type in (info.INFO_RAW_RANGE, info.INFO_PCT_RANGE, info.INFO_SI_RANGE):
return [self._float(), self._float()]
elif self.info_type == info.INFO_UNITS:
return self.payload[:self.payload.index(b"\00")].decode('ascii')
elif self.info_type == info.INFO_MAPPING:
inp = self._bits_list(self._byte())
outp = self._bits_list(self._byte())
return {
"input": [self.MAPPING_FLAGS[x] for x in inp],
"output": [self.MAPPING_FLAGS[x] for x in outp],
}
elif self.info_type == info.INFO_MOTOR_BIAS:
return self._byte()
elif self.info_type == info.INFO_VALUE_FORMAT:
return {
"datasets": self._byte(),
"type": self.DATASET_TYPES[self._byte()],
"total_figures": self._byte(),
"decimals": self._byte(),
}
else:
return self.payload # FIXME: will probably fail here
class MsgPortValueSingle(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-value-single
"""
TYPE = 0x45
def __init__(self):
super(MsgPortValueSingle, self).__init__()
self.port = None
@classmethod
def decode(cls, data):
msg = super(MsgPortValueSingle, cls).decode(data)
assert isinstance(msg, MsgPortValueSingle)
msg.port = msg._byte()
return msg
class MsgPortValueCombined(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-value-combinedmode
"""
TYPE = 0x46
def __init__(self):
super(MsgPortValueCombined, self).__init__()
self.port = None
@classmethod
def decode(cls, data):
msg = super(MsgPortValueCombined, cls).decode(data)
assert isinstance(msg, MsgPortValueCombined)
msg.port = msg._byte()
return msg
class MsgPortInputFmtSingle(UpstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-input-format-single
"""
TYPE = 0x47
def __init__(self, port=None, mode=None, upd_enabled=None, upd_delta=None):
super(MsgPortInputFmtSingle, self).__init__()
self.port = port
self.mode = mode
self.upd_delta = upd_delta
self.upd_enabled = upd_enabled
@classmethod
def decode(cls, data):
msg = super(MsgPortInputFmtSingle, cls).decode(data)
assert isinstance(msg, MsgPortInputFmtSingle)
msg.port = msg._byte()
msg.mode = msg._byte()
msg.upd_delta = msg._long()
if len(msg.payload):
msg.upd_enabled = msg._byte()
return msg
class MsgPortInputFmtCombined(UpstreamMsg): # TODO
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-input-format-combinedmode
"""
TYPE = 0x48
def __init__(self):
super(MsgPortInputFmtCombined, self).__init__()
self.port = None
self.combined_control = None
@classmethod
def decode(cls, data):
msg = super(MsgPortInputFmtCombined, cls).decode(data)
assert isinstance(msg, MsgPortInputFmtSingle)
msg.port = msg._byte()
return msg
class MsgVirtualPortSetup(DownstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#virtual-port-setup
"""
TYPE = 0x61
CMD_DISCONNECT = 0x00
CMD_CONNECT = 0x01
def __init__(self, cmd, port):
super(MsgVirtualPortSetup, self).__init__()
self.payload = pack("<B", cmd)
if cmd == self.CMD_DISCONNECT:
assert isinstance(port, int)
self.payload += pack("<B", port)
else:
assert isinstance(port, (list, tuple))
self.payload += pack("<B", port[0]) + pack("<B", port[1])
class MsgPortOutput(DownstreamMsg):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#port-output-command
"""
TYPE = 0x81
SC_NO_BUFFER = 0b00000001
SC_FEEDBACK = 0b00010000
WRITE_DIRECT = 0x50
WRITE_DIRECT_MODE_DATA = 0x51
def __init__(self, port, subcommand, params):
super(MsgPortOutput, self).__init__()
self.port = port
self.is_buffered = False
self.do_feedback = True
self.subcommand = subcommand
self.params = params
def bytes(self):
startup_completion_flags = 0
if not self.is_buffered:
startup_completion_flags |= self.SC_NO_BUFFER
if self.do_feedback:
startup_completion_flags |= self.SC_FEEDBACK
self.needs_reply = True
self.payload = pack("<B", self.port) + pack("<B", startup_completion_flags) \
+ pack("<B", self.subcommand) + self.params
return super(MsgPortOutput, self).bytes()
def is_reply(self, msg):
return isinstance(msg, MsgPortOutputFeedback) and msg.port == self.port \
and (msg.is_completed() or self.is_buffered)
class MsgPortOutputFeedback(UpstreamMsg):
TYPE = 0x82
def __init__(self):
super(MsgPortOutputFeedback, self).__init__()
self.port = None
self.status = None
@classmethod
def decode(cls, data):
msg = super(MsgPortOutputFeedback, cls).decode(data)
assert isinstance(msg, MsgPortOutputFeedback)
assert len(msg.payload) == 2, "TODO: implement multi-port feedback message"
msg.port = msg._byte()
msg.status = msg._byte()
return msg
def is_in_progress(self):
return self.status & 0b0001
def is_completed(self):
return self.status & 0b0010
def is_discarded(self):
return self.status & 0b0100
def is_idle(self):
return self.status & 0b1000
UPSTREAM_MSGS = (
MsgHubProperties, MsgHubAction, MsgHubAlert, MsgHubAttachedIO, MsgGenericError,
MsgPortInfo, MsgPortModeInfo,
MsgPortValueSingle, MsgPortValueCombined, MsgPortInputFmtSingle, MsgPortInputFmtCombined,
MsgPortOutputFeedback
)

245
pylgbst/movehub.py Normal file
View File

@ -0,0 +1,245 @@
import logging
import time
from struct import pack
from pylgbst.comms import BLEConnection
from pylgbst.constants import *
from pylgbst.peripherals import Button, EncodedMotor, ColorDistanceSensor, LED, TiltSensor, Voltage, Peripheral, \
Amperage
log = logging.getLogger('movehub')
ENABLE_NOTIFICATIONS_HANDLE = 0x000f
ENABLE_NOTIFICATIONS_VALUE = b'\x01\x00'
class MoveHub(object):
"""
:type connection: pylgbst.comms.Connection
:type devices: dict[int,Peripheral]
:type led: LED
:type tilt_sensor: TiltSensor
:type button: Button
:type amperage: Amperage
:type voltage: Voltage
:type color_distance_sensor: pylgbst.peripherals.ColorDistanceSensor
:type port_C: Peripheral
:type port_D: Peripheral
:type motor_A: EncodedMotor
:type motor_B: EncodedMotor
:type motor_AB: EncodedMotor
:type motor_external: EncodedMotor
"""
DEV_STATUS_DETACHED = 0x00
DEV_STATUS_DEVICE = 0x01
DEV_STATUS_GROUP = 0x02
def __init__(self, connection=None):
if not connection:
connection = BLEConnection()
self.connection = connection
self.info = {}
self.devices = {}
# shorthand fields
self.button = Button(self)
self.led = None
self.amperage = None
self.voltage = None
self.motor_A = None
self.motor_B = None
self.motor_AB = None
self.color_distance_sensor = None
self.tilt_sensor = None
self.motor_external = None
self.port_C = None
self.port_D = None
self.connection.set_notify_handler(self._notify)
self._wait_for_devices()
self._report_status()
def send(self, msg_type, payload):
cmd = pack("<B", PACKET_VER) + pack("<B", msg_type) + payload
self.connection.write(MOVE_HUB_HARDWARE_HANDLE, pack("<B", len(cmd) + 1) + cmd)
def _wait_for_devices(self):
self.connection.write(ENABLE_NOTIFICATIONS_HANDLE, ENABLE_NOTIFICATIONS_VALUE)
builtin_devices = ()
for num in range(0, 60):
builtin_devices = (self.led, self.motor_A, self.motor_B,
self.motor_AB, self.tilt_sensor, self.button, self.amperage, self.voltage)
if None not in builtin_devices:
return
log.debug("Waiting for builtin devices to appear: %s", builtin_devices)
time.sleep(0.05)
log.warning("Got only these devices: %s", builtin_devices)
raise RuntimeError("Failed to obtain all builtin devices")
def _notify(self, handle, data):
orig = data
if handle != MOVE_HUB_HARDWARE_HANDLE:
log.warning("Unsupported notification handle: 0x%s", handle)
return
data = data[3:]
log.debug("Notification on %s: %s", handle, str2hex(orig))
msg_type = usbyte(data, 2)
if msg_type == MSG_PORT_INFO:
self._handle_port_info(data)
elif msg_type == MSG_PORT_STATUS:
self._handle_port_status(data)
elif msg_type == MSG_SENSOR_DATA:
self._handle_sensor_data(data)
elif msg_type == MSG_SENSOR_SUBSCRIBE_ACK:
port = usbyte(data, 3)
log.debug("Sensor subscribe ack on port %s", PORTS[port])
self.devices[port].finished()
elif msg_type == MSG_PORT_CMD_ERROR:
log.warning("Command error: %s", str2hex(data[3:]))
port = usbyte(data, 3)
self.devices[port].finished()
elif msg_type == MSG_DEVICE_SHUTDOWN:
log.warning("Device reported shutdown: %s", str2hex(data))
raise KeyboardInterrupt("Device shutdown")
elif msg_type == MSG_DEVICE_INFO:
self._handle_device_info(data)
else:
log.warning("Unhandled msg type 0x%x: %s", msg_type, str2hex(orig))
def _handle_device_info(self, data):
kind = usbyte(data, 3)
if kind == 2:
self.button.handle_port_data(data)
if usbyte(data, 4) == 0x06:
self.info[kind] = data[5:]
else:
log.warning("Unhandled device info: %s", str2hex(data))
def _handle_sensor_data(self, data):
port = usbyte(data, 3)
if port not in self.devices:
log.warning("Notification on port with no device: %s", PORTS[port])
return
device = self.devices[port]
device.queue_port_data(data)
def _handle_port_status(self, data):
port = usbyte(data, 3)
status = usbyte(data, 4)
if status == STATUS_STARTED:
self.devices[port].started()
elif status == STATUS_FINISHED:
self.devices[port].finished()
elif status == STATUS_CONFLICT:
log.warning("Command conflict on port %s", PORTS[port])
self.devices[port].finished()
elif status == STATUS_INPROGRESS:
log.warning("Another command is in progress on port %s", PORTS[port])
self.devices[port].finished()
elif status == STATUS_INTERRUPTED:
log.warning("Command interrupted on port %s", PORTS[port])
self.devices[port].finished()
else:
log.warning("Unhandled status value: 0x%x on port %s", status, PORTS[port])
def _handle_port_info(self, data):
port = usbyte(data, 3)
status = usbyte(data, 4)
if status == self.DEV_STATUS_DETACHED:
log.info("Detached %s", self.devices[port])
self.devices[port] = None
elif status == self.DEV_STATUS_DEVICE or status == self.DEV_STATUS_GROUP:
dev_type = usbyte(data, 5)
self._attach_device(dev_type, port)
else:
raise ValueError("Unhandled device status: %s", status)
self._update_field(port)
if self.devices[port] is None:
del self.devices[port]
def _attach_device(self, dev_type, port):
if port in PORTS and dev_type in DEVICE_TYPES:
log.info("Attached %s on port %s", DEVICE_TYPES[dev_type], PORTS[port])
else:
log.warning("Attached device 0x%x on port 0x%x", dev_type, port)
if dev_type == DEV_MOTOR:
self.devices[port] = EncodedMotor(self, port)
elif dev_type == DEV_IMOTOR:
self.motor_external = EncodedMotor(self, port)
self.devices[port] = self.motor_external
elif dev_type == DEV_DCS:
self.color_distance_sensor = ColorDistanceSensor(self, port)
self.devices[port] = self.color_distance_sensor
elif dev_type == DEV_LED:
self.devices[port] = LED(self, port)
elif dev_type == DEV_TILT_SENSOR:
self.devices[port] = TiltSensor(self, port)
elif dev_type == DEV_AMPERAGE:
self.devices[port] = Amperage(self, port)
elif dev_type == DEV_VOLTAGE:
self.devices[port] = Voltage(self, port)
else:
log.warning("Unhandled peripheral type 0x%x on port 0x%x", dev_type, port)
self.devices[port] = Peripheral(self, port)
def _update_field(self, port):
if port == PORT_A:
self.motor_A = self.devices[port]
elif port == PORT_B:
self.motor_B = self.devices[port]
elif port == PORT_AB:
self.motor_AB = self.devices[port]
elif port == PORT_C:
self.port_C = self.devices[port]
elif port == PORT_D:
self.port_D = self.devices[port]
elif port == PORT_LED:
self.led = self.devices[port]
elif port == PORT_TILT_SENSOR:
self.tilt_sensor = self.devices[port]
elif port == PORT_AMPERAGE:
self.amperage = self.devices[port]
elif port == PORT_VOLTAGE:
self.voltage = self.devices[port]
else:
log.warning("Unhandled port: %s", PORTS[port])
def shutdown(self):
self.send(MSG_DEVICE_SHUTDOWN, b'')
def _report_status(self):
# TODO: add firmware version
log.info("%s by %s", self.info_get(INFO_DEVICE_NAME), self.info_get(INFO_MANUFACTURER))
self.__voltage = 0
def on_voltage(value):
self.__voltage = value
self.voltage.subscribe(on_voltage, granularity=0)
while not self.__voltage:
time.sleep(0.05)
self.voltage.unsubscribe(on_voltage)
log.info("Voltage: %d%%", self.__voltage * 100)
def info_get(self, info_type):
self.info[info_type] = None
self.send(MSG_DEVICE_INFO, pack("<B", info_type) + pack("<B", INFO_ACTION_GET))
while self.info[info_type] is None: # FIXME: will hang forever on error
time.sleep(0.05)
return self.info[info_type]

View File

@ -1,118 +1,70 @@
import logging import logging
import math import math
import time
import traceback import traceback
from struct import pack, unpack from struct import pack, unpack
from threading import Thread from threading import Thread
from pylgbst.messages import MsgHubProperties, MsgPortOutput, MsgPortInputFmtSetupSingle, MsgPortInfoRequest, \ from pylgbst.constants import *
MsgPortModeInfoRequest, MsgPortInfo, MsgPortModeInfo, MsgPortInputFmtSingle
from pylgbst.utilities import queue, str2hex, usbyte, ushort, usint
log = logging.getLogger('peripherals') log = logging.getLogger('peripherals')
# COLORS
COLOR_BLACK = 0x00
COLOR_PINK = 0x01
COLOR_PURPLE = 0x02
COLOR_BLUE = 0x03
COLOR_LIGHTBLUE = 0x04
COLOR_CYAN = 0x05
COLOR_GREEN = 0x06
COLOR_YELLOW = 0x07
COLOR_ORANGE = 0x08
COLOR_RED = 0x09
COLOR_WHITE = 0x0a
COLOR_NONE = 0xFF
COLORS = {
COLOR_BLACK: "BLACK",
COLOR_PINK: "PINK",
COLOR_PURPLE: "PURPLE",
COLOR_BLUE: "BLUE",
COLOR_LIGHTBLUE: "LIGHTBLUE",
COLOR_CYAN: "CYAN",
COLOR_GREEN: "GREEN",
COLOR_YELLOW: "YELLOW",
COLOR_ORANGE: "ORANGE",
COLOR_RED: "RED",
COLOR_WHITE: "WHITE",
COLOR_NONE: "NONE"
}
# TODO: support more types of peripherals from
# https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#io-type-id
class Peripheral(object): class Peripheral(object):
""" """
:type parent: pylgbst.hub.Hub :type parent: MoveHub
:type _incoming_port_data: queue.Queue :type _incoming_port_data: queue.Queue
:type _port_mode: MsgPortInputFmtSingle
""" """
def __init__(self, parent, port): def __init__(self, parent, port):
""" """
:type parent: pylgbst.hub.Hub :type parent: pylgbst.movehub.MoveHub
:type port: int :type port: int
""" """
super(Peripheral, self).__init__() super(Peripheral, self).__init__()
self.virtual_ports = () self.parent = parent
self.hub = parent
self.port = port self.port = port
self._working = False
self.is_buffered = False
self._subscribers = set() self._subscribers = set()
self._port_mode = MsgPortInputFmtSingle(self.port, None, False, 1) self._port_subscription_mode = None
self._incoming_port_data = queue.Queue()
self._incoming_port_data = queue.Queue(1) # limit 1 means we drop data if we can't handle it fast enough
thr = Thread(target=self._queue_reader) thr = Thread(target=self._queue_reader)
thr.setDaemon(True) thr.setDaemon(True)
thr.setName("Port data queue: %s" % self) thr.setName("Port data queue: %s" % self)
thr.start() thr.start()
def __repr__(self): def __repr__(self):
msg = "%s on port 0x%x" % (self.__class__.__name__, self.port) return "%s on port %s" % (self.__class__.__name__, PORTS[self.port] if self.port in PORTS else self.port)
if self.virtual_ports:
msg += " (ports 0x%x and 0x%x combined)" % (self.virtual_ports[0], self.virtual_ports[1])
return msg
def set_port_mode(self, mode, send_updates=None, update_delta=None): def _write_to_hub(self, msg_type, params):
assert not self.virtual_ports, "TODO: support combined mode for sensors" cmd = pack("<B", self.port) + params
self.parent.send(msg_type, cmd)
if send_updates is None: def _port_subscribe(self, mode, granularity, enable):
send_updates = self._port_mode.upd_enabled params = pack("<B", mode)
log.debug("Implied update is enabled=%s", send_updates) params += pack("<H", granularity)
params += b'\x00\x00' # maybe also bytes of granularity
params += pack("<?", bool(enable))
self._write_to_hub(MSG_SENSOR_SUBSCRIBE, params)
if update_delta is None: def started(self):
update_delta = self._port_mode.upd_delta log.debug("Started: %s", self)
log.debug("Implied update delta=%s", update_delta) self._working = True
if self._port_mode.mode == mode \ def finished(self):
and self._port_mode.upd_enabled == send_updates \ log.debug("Finished: %s", self)
and self._port_mode.upd_delta == update_delta: self._working = False
log.debug("Already in target mode, no need to switch")
return
else:
msg = MsgPortInputFmtSetupSingle(self.port, mode, update_delta, send_updates)
resp = self.hub.send(msg)
assert isinstance(resp, MsgPortInputFmtSingle)
self._port_mode = resp
def _send_output(self, msg): def in_progress(self):
assert isinstance(msg, MsgPortOutput) return bool(self._working)
msg.is_buffered = self.is_buffered # TODO: support buffering
self.hub.send(msg)
def get_sensor_data(self, mode): def subscribe(self, callback, mode, granularity=1, async=False):
self.set_port_mode(mode) self._port_subscription_mode = mode
msg = MsgPortInfoRequest(self.port, MsgPortInfoRequest.INFO_PORT_VALUE) self.started()
resp = self.hub.send(msg) self._port_subscribe(self._port_subscription_mode, granularity, True)
return self._decode_port_data(resp)
self._wait_sync(async) # having async=True leads to stuck notifications
def subscribe(self, callback, mode=0x00, granularity=1):
if self._port_mode.mode != mode and self._subscribers:
raise ValueError("Port is in active mode %r, unsubscribe all subscribers first" % self._port_mode)
self.set_port_mode(mode, True, granularity)
if callback: if callback:
self._subscribers.add(callback) self._subscribers.add(callback)
@ -120,145 +72,93 @@ class Peripheral(object):
if callback in self._subscribers: if callback in self._subscribers:
self._subscribers.remove(callback) self._subscribers.remove(callback)
if not self._port_mode.upd_enabled: if self._port_subscription_mode is None:
log.warning("Attempt to unsubscribe while port value updates are off: %s", self) log.warning("Attempt to unsubscribe while never subscribed: %s", self)
elif not self._subscribers: elif not self._subscribers:
self.set_port_mode(self._port_mode.mode, False) self._port_subscribe(self._port_subscription_mode, 0, False)
self._port_subscription_mode = None
def _notify_subscribers(self, *args, **kwargs): def _notify_subscribers(self, *args, **kwargs):
for subscriber in self._subscribers.copy(): for subscriber in self._subscribers:
subscriber(*args, **kwargs) subscriber(*args, **kwargs)
return args
def queue_port_data(self, msg): def queue_port_data(self, data):
try: self._incoming_port_data.put(data)
self._incoming_port_data.put_nowait(msg)
except queue.Full:
log.debug("Dropped port data: %r", msg)
def _decode_port_data(self, msg): def handle_port_data(self, data):
""" log.warning("Unhandled device notification for %s: %s", self, str2hex(data[4:]))
:rtype: tuple self._notify_subscribers(data[4:])
"""
log.warning("Unhandled port data: %r", msg)
return ()
def _handle_port_data(self, msg):
"""
:type msg: pylgbst.messages.MsgPortValueSingle
"""
decoded = self._decode_port_data(msg)
assert isinstance(decoded, (tuple, list)), "Unexpected data type: %s" % type(decoded)
self._notify_subscribers(*decoded)
def _queue_reader(self): def _queue_reader(self):
while True: while True:
msg = self._incoming_port_data.get() data = self._incoming_port_data.get()
try: try:
self._handle_port_data(msg) self.handle_port_data(data)
except BaseException: except BaseException:
log.warning("%s", traceback.format_exc()) log.warning("%s", traceback.format_exc())
log.warning("Failed to handle port data by %s: %r", self, msg) log.warning("Failed to handle port data by %s: %s", self, str2hex(data))
def describe_possible_modes(self): def _wait_sync(self, async):
mode_info = self.hub.send(MsgPortInfoRequest(self.port, MsgPortInfoRequest.INFO_MODE_INFO)) if not async:
assert isinstance(mode_info, MsgPortInfo) log.debug("Waiting for sync command work to finish...")
info = { while self.in_progress():
"mode_count": mode_info.total_modes, time.sleep(0.001)
"input_modes": [], log.debug("Command has finished.")
"output_modes": [],
"capabilities": {
"logically_combinable": mode_info.is_combinable(),
"synchronizable": mode_info.is_synchronizable(),
"can_output": mode_info.is_output(),
"can_input": mode_info.is_input(),
}
}
if mode_info.is_combinable():
mode_combinations = self.hub.send(MsgPortInfoRequest(self.port, MsgPortInfoRequest.INFO_MODE_COMBINATIONS))
assert isinstance(mode_combinations, MsgPortInfo)
info['possible_mode_combinations'] = mode_combinations.possible_mode_combinations
info['modes'] = []
for mode in range(256):
info['modes'].append(self._describe_mode(mode))
for mode in mode_info.output_modes:
info['output_modes'].append(self._describe_mode(mode))
for mode in mode_info.input_modes:
info['input_modes'].append(self._describe_mode(mode))
log.debug("Port info for 0x%x: %s", self.port, info)
return info
def _describe_mode(self, mode):
descr = {"Mode": mode}
for info in MsgPortModeInfoRequest.INFO_TYPES:
try:
resp = self.hub.send(MsgPortModeInfoRequest(self.port, mode, info))
assert isinstance(resp, MsgPortModeInfo)
descr[MsgPortModeInfoRequest.INFO_TYPES[info]] = resp.value
except RuntimeError:
log.debug("Got error while requesting info 0x%x: %s", info, traceback.format_exc())
if info == MsgPortModeInfoRequest.INFO_NAME:
break
return descr
class LEDRGB(Peripheral): class LED(Peripheral):
MODE_INDEX = 0x00 SOMETHING = b'\x51\x00'
MODE_RGB = 0x01
def __init__(self, parent, port): def __init__(self, parent, port):
super(LEDRGB, self).__init__(parent, port) super(LED, self).__init__(parent, port)
self._last_color_set = COLOR_NONE
def set_color(self, color): def set_color(self, color, do_notify=True):
if isinstance(color, (list, tuple)): if color == COLOR_NONE:
assert len(color) == 3, "RGB color has to have 3 values" color = COLOR_BLACK
self.set_port_mode(self.MODE_RGB)
payload = pack("<B", self.MODE_RGB) + pack("<B", color[0]) + pack("<B", color[1]) + pack("<B", color[2])
else:
if color == COLOR_NONE:
color = COLOR_BLACK
if color not in COLORS: if color not in COLORS:
raise ValueError("Color %s is not in list of available colors" % color) raise ValueError("Color %s is not in list of available colors" % color)
self.set_port_mode(self.MODE_INDEX) self._last_color_set = color
payload = pack("<B", self.MODE_INDEX) + pack("<B", color) cmd = pack("<B", do_notify) + self.SOMETHING + pack("<B", color)
self.started()
self._write_to_hub(MSG_SET_PORT_VAL, cmd)
msg = MsgPortOutput(self.port, MsgPortOutput.WRITE_DIRECT_MODE_DATA, payload) def finished(self):
self._send_output(msg) super(LED, self).finished()
log.debug("LED has changed color to %s", COLORS[self._last_color_set])
self._notify_subscribers(self._last_color_set)
def _decode_port_data(self, msg): def subscribe(self, callback, mode=None, granularity=None, async=False):
if len(msg.payload) == 3: self._subscribers.add(callback)
return usbyte(msg.payload, 0), usbyte(msg.payload, 1), usbyte(msg.payload, 2),
else: def unsubscribe(self, callback=None):
return usbyte(msg.payload, 0), if callback in self._subscribers:
self._subscribers.remove(callback)
class Motor(Peripheral): class EncodedMotor(Peripheral):
SUBCMD_START_POWER = 0x01 TRAILER = b'\x64\x7f\x03' # NOTE: \x64 is 100, might mean something; also trailer might be a sequence terminator
SUBCMD_START_POWER_GROUPED = 0x02 # TODO: investigate sequence behavior, seen with zero values passed to angled mode
SUBCMD_SET_ACC_TIME = 0x05 # trailer is not required for all movement types
SUBCMD_SET_DEC_TIME = 0x06 MOVEMENT_TYPE = 0x11
SUBCMD_START_SPEED = 0x07
# SUBCMD_START_SPEED = 0x08
SUBCMD_START_SPEED_FOR_TIME = 0x09
# SUBCMD_START_SPEED_FOR_TIME = 0x0A
END_STATE_BRAKE = 127 CONSTANT_SINGLE = 0x01
END_STATE_HOLD = 126 CONSTANT_GROUP = 0x02
END_STATE_FLOAT = 0 SOME_SINGLE = 0x07
SOME_GROUP = 0x08
TIMED_SINGLE = 0x09
TIMED_GROUP = 0x0A
ANGLED_SINGLE = 0x0B
ANGLED_GROUP = 0x0C
# MOTORS
SENSOR_SOMETHING1 = 0x00 # TODO: understand it
SENSOR_SPEED = 0x01
SENSOR_ANGLE = 0x02
def _speed_abs(self, relative): def _speed_abs(self, relative):
if relative == Motor.END_STATE_BRAKE or relative == Motor.END_STATE_HOLD:
# special value for BRAKE
# https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-startpower-power
return relative
if relative < -1: if relative < -1:
log.warning("Speed cannot be less than -1") log.warning("Speed cannot be less than -1")
relative = -1 relative = -1
@ -270,198 +170,97 @@ class Motor(Peripheral):
absolute = math.ceil(relative * 100) # scale of 100 is proven by experiments absolute = math.ceil(relative * 100) # scale of 100 is proven by experiments
return int(absolute) return int(absolute)
def _write_direct_mode(self, subcmd, params): def _wrap_and_write(self, mtype, params, speed_primary, speed_secondary):
params = pack("<B", subcmd) + params if self.port == PORT_AB:
msg = MsgPortOutput(self.port, MsgPortOutput.WRITE_DIRECT_MODE_DATA, params) mtype += 1 # de-facto rule
self._send_output(msg)
def _send_cmd(self, subcmd, params): abs_primary = self._speed_abs(speed_primary)
if self.virtual_ports: abs_secondary = self._speed_abs(speed_secondary)
subcmd += 1 # de-facto rule if abs_primary == -97 and abs_secondary == 3:
logging.info("P/S: %s/%s", abs_primary, abs_secondary)
msg = MsgPortOutput(self.port, subcmd, params) if mtype == self.ANGLED_GROUP and (not abs_secondary or not abs_primary):
self._send_output(msg) raise ValueError("Cannot have zero speed in double angled mode") # otherwise it gets nuts
def start_power(self, power_primary=1.0, power_secondary=None): params = pack("<B", self.MOVEMENT_TYPE) + pack("<B", mtype) + params
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-startpower-power
"""
if power_secondary is None:
power_secondary = power_primary
if self.virtual_ports: params += pack("<b", abs_primary)
cmd = self.SUBCMD_START_POWER_GROUPED - 1 # because _send_cmd will do +1 if self.port == PORT_AB:
else: params += pack("<b", abs_secondary)
cmd = self.SUBCMD_START_POWER
params = b"" params += self.TRAILER
params += pack("<b", self._speed_abs(power_primary))
if self.virtual_ports:
params += pack("<b", self._speed_abs(power_secondary))
self._send_cmd(cmd, params) self._write_to_hub(MSG_SET_PORT_VAL, params)
def stop(self): def timed(self, seconds, speed_primary=1, speed_secondary=None, async=False):
self.timed(0)
def set_acc_profile(self, seconds, profile_no=0x00):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-setacctime-time-profileno-0x05
"""
params = b""
params += pack("<H", int(seconds * 1000))
params += pack("<B", profile_no)
self._send_cmd(self.SUBCMD_SET_ACC_TIME, params)
def set_dec_profile(self, seconds, profile_no=0x00):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-setdectime-time-profileno-0x06
"""
params = b""
params += pack("<H", int(seconds * 1000))
params += pack("<B", profile_no)
self._send_cmd(self.SUBCMD_SET_DEC_TIME, params)
def start_speed(self, speed_primary=1.0, speed_secondary=None, max_power=1.0, use_profile=0b11):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-startspeed-speed-maxpower-useprofile-0x07
"""
if speed_secondary is None: if speed_secondary is None:
speed_secondary = speed_primary speed_secondary = speed_primary
params = b"" params = pack('<H', int(seconds * 1000))
params += pack("<b", self._speed_abs(speed_primary))
if self.virtual_ports:
params += pack("<b", self._speed_abs(speed_secondary))
params += pack("<B", int(100 * max_power)) self.started()
params += pack("<B", use_profile) self._wrap_and_write(self.TIMED_SINGLE, params, speed_primary, speed_secondary)
self._wait_sync(async)
self._send_cmd(self.SUBCMD_START_SPEED, params) def angled(self, angle, speed_primary=1, speed_secondary=None, async=False):
def timed(self, seconds, speed_primary=1.0, speed_secondary=None, max_power=1.0, end_state=END_STATE_BRAKE,
use_profile=0b11):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-startspeedfortime-time-speed-maxpower-endstate-useprofile-0x09
"""
if speed_secondary is None: if speed_secondary is None:
speed_secondary = speed_primary speed_secondary = speed_primary
params = b"" angle = int(round(angle))
params += pack("<H", int(seconds * 1000)) if angle < 0:
params += pack("<b", self._speed_abs(speed_primary)) angle = -angle
if self.virtual_ports:
params += pack("<b", self._speed_abs(speed_secondary))
params += pack("<B", int(100 * max_power))
params += pack("<B", end_state)
params += pack("<B", use_profile)
self._send_cmd(self.SUBCMD_START_SPEED_FOR_TIME, params)
class EncodedMotor(Motor):
SUBCMD_START_SPEED_FOR_DEGREES = 0x0B
# SUBCMD_START_SPEED_FOR_DEGREES = 0x0C
SUBCMD_GOTO_ABSOLUTE_POSITION = 0x0D
# SUBCMD_GOTO_ABSOLUTE_POSITIONC = 0x0E
SUBCMD_PRESET_ENCODER = 0x14
SENSOR_POWER = 0x00 # it's not input mode, hovewer returns some numbers
SENSOR_SPEED = 0x01
SENSOR_ANGLE = 0x02
SENSOR_TEST = 0x03 # exists, but neither input nor output mode
def angled(self, degrees, speed_primary=1.0, speed_secondary=None, max_power=1.0, end_state=Motor.END_STATE_BRAKE,
use_profile=0b11):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-startspeedfordegrees-degrees-speed-maxpower-endstate-useprofile-0x0b
:type degrees: int
:type speed_primary: float
"""
if speed_secondary is None:
speed_secondary = speed_primary
degrees = int(round(degrees))
if degrees < 0:
degrees = -degrees
speed_primary = -speed_primary speed_primary = -speed_primary
speed_secondary = -speed_secondary speed_secondary = -speed_secondary
params = b"" params = pack('<I', angle)
params += pack("<I", degrees)
params += pack("<b", self._speed_abs(speed_primary))
if self.virtual_ports:
params += pack("<b", self._speed_abs(speed_secondary))
params += pack("<B", int(100 * max_power)) self.started()
params += pack("<B", end_state) self._wrap_and_write(self.ANGLED_SINGLE, params, speed_primary, speed_secondary)
params += pack("<B", use_profile) self._wait_sync(async)
self._send_cmd(self.SUBCMD_START_SPEED_FOR_DEGREES, params) def constant(self, speed_primary=1, speed_secondary=None, async=False):
if speed_secondary is None:
speed_secondary = speed_primary
def goto_position(self, degrees_primary, degrees_secondary=None, speed=1.0, max_power=1.0, self.started()
end_state=Motor.END_STATE_BRAKE, use_profile=0b11): self._wrap_and_write(self.CONSTANT_SINGLE, b"", speed_primary, speed_secondary)
""" self._wait_sync(async)
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-gotoabsoluteposition-abspos-speed-maxpower-endstate-useprofile-0x0d
"""
if degrees_secondary is None:
degrees_secondary = degrees_primary
params = b"" def __some(self, speed_primary=1, speed_secondary=None, async=False):
params += pack("<i", degrees_primary) if speed_secondary is None:
if self.virtual_ports: speed_secondary = speed_primary
params += pack("<i", degrees_secondary)
params += pack("<b", self._speed_abs(speed)) self.started()
self._wrap_and_write(self.SOME_SINGLE, b"", speed_primary, speed_secondary)
self._wait_sync(async)
params += pack("<B", int(100 * max_power)) def stop(self, async=False):
params += pack("<B", end_state) self.constant(0, async=async)
params += pack("<B", use_profile)
self._send_cmd(self.SUBCMD_GOTO_ABSOLUTE_POSITION, params) def handle_port_data(self, data):
if self._port_subscription_mode == self.SENSOR_ANGLE:
def _decode_port_data(self, msg): rotation = unpack("<l", data[4:8])[0]
data = msg.payload self._notify_subscribers(rotation)
if self._port_mode.mode == self.SENSOR_ANGLE: elif self._port_subscription_mode == self.SENSOR_SOMETHING1:
angle = unpack("<l", data[0:4])[0] # TODO: understand what it means
return (angle,) rotation = usbyte(data, 4)
elif self._port_mode.mode == self.SENSOR_SPEED: self._notify_subscribers(rotation)
speed = unpack("<b", data[0:1])[0] elif self._port_subscription_mode == self.SENSOR_SPEED:
return (speed,) rotation = unpack("<b", data[4])[0]
self._notify_subscribers(rotation)
else: else:
log.debug("Got motor sensor data while in unexpected mode: %r", self._port_mode) log.debug("Got motor sensor data while in unexpected mode: %s", self._port_subscription_mode)
return ()
def subscribe(self, callback, mode=SENSOR_ANGLE, granularity=1): def subscribe(self, callback, mode=SENSOR_ANGLE, granularity=1, async=False):
super(EncodedMotor, self).subscribe(callback, mode, granularity) super(EncodedMotor, self).subscribe(callback, mode, granularity)
def preset_encoder(self, degrees=0, degrees_secondary=None, only_combined=False):
"""
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-presetencoder-position-n-a
https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-presetencoder-leftposition-rightposition-0x14
"""
if degrees_secondary is None:
degrees_secondary = degrees
if self.virtual_ports and not only_combined:
self._send_cmd(self.SUBCMD_PRESET_ENCODER, pack("<i", degrees) + pack("<i", degrees_secondary))
else:
params = pack("<i", degrees)
self._write_direct_mode(self.SENSOR_ANGLE, params)
class TiltSensor(Peripheral): class TiltSensor(Peripheral):
MODE_2AXIS_ANGLE = 0x00 MODE_2AXIS_FULL = 0x00
MODE_2AXIS_SIMPLE = 0x01 MODE_2AXIS_SIMPLE = 0x01
MODE_3AXIS_SIMPLE = 0x02 MODE_3AXIS_SIMPLE = 0x02
MODE_IMPACT_COUNT = 0x03 MODE_BUMP_COUNT = 0x03
MODE_3AXIS_ACCEL = 0x04 MODE_3AXIS_FULL = 0x04
MODE_ORIENT_CF = 0x05
MODE_IMPACT_CF = 0x06
MODE_CALIBRATION = 0x07
TRI_BACK = 0x00 TRI_BACK = 0x00
TRI_UP = 0x01 TRI_UP = 0x01
@ -493,152 +292,134 @@ class TiltSensor(Peripheral):
TRI_FRONT: "FRONT", TRI_FRONT: "FRONT",
} }
def subscribe(self, callback, mode=MODE_3AXIS_SIMPLE, granularity=1): def subscribe(self, callback, mode=MODE_3AXIS_SIMPLE, granularity=1, async=False):
super(TiltSensor, self).subscribe(callback, mode, granularity) super(TiltSensor, self).subscribe(callback, mode, granularity)
def _decode_port_data(self, msg): def handle_port_data(self, data):
data = msg.payload if self._port_subscription_mode == self.MODE_3AXIS_SIMPLE:
if self._port_mode.mode == self.MODE_2AXIS_ANGLE: state = usbyte(data, 4)
roll = unpack('<b', data[0:1])[0] self._notify_subscribers(state)
pitch = unpack('<b', data[1:2])[0] elif self._port_subscription_mode == self.MODE_2AXIS_SIMPLE:
return (roll, pitch) state = usbyte(data, 4)
elif self._port_mode.mode == self.MODE_3AXIS_SIMPLE: self._notify_subscribers(state)
state = usbyte(data, 0) elif self._port_subscription_mode == self.MODE_BUMP_COUNT:
return (state,) bump_count = ushort(data, 4)
elif self._port_mode.mode == self.MODE_2AXIS_SIMPLE: self._notify_subscribers(bump_count)
state = usbyte(data, 0) elif self._port_subscription_mode == self.MODE_2AXIS_FULL:
return (state,) roll = self._byte2deg(usbyte(data, 4))
elif self._port_mode.mode == self.MODE_IMPACT_COUNT: pitch = self._byte2deg(usbyte(data, 5))
bump_count = usint(data, 0) self._notify_subscribers(roll, pitch)
return (bump_count,) elif self._port_subscription_mode == self.MODE_3AXIS_FULL:
elif self._port_mode.mode == self.MODE_3AXIS_ACCEL: roll = self._byte2deg(usbyte(data, 4))
roll = unpack('<b', data[0:1])[0] pitch = self._byte2deg(usbyte(data, 5))
pitch = unpack('<b', data[1:2])[0] yaw = self._byte2deg(usbyte(data, 6)) # did I get the order right?
yaw = unpack('<b', data[2:3])[0] # did I get the order right? self._notify_subscribers(roll, pitch, yaw)
return (roll, pitch, yaw)
elif self._port_mode.mode == self.MODE_ORIENT_CF:
state = usbyte(data, 0)
return (state,)
elif self._port_mode.mode == self.MODE_IMPACT_CF:
state = usbyte(data, 0)
return (state,)
elif self._port_mode.mode == self.MODE_CALIBRATION:
return (usbyte(data, 0), usbyte(data, 1), usbyte(data, 2))
else: else:
log.debug("Got tilt sensor data while in unexpected mode: %r", self._port_mode) log.debug("Got tilt sensor data while in unexpected mode: %s", self._port_subscription_mode)
return ()
# TODO: add some methods from official doc, like def _byte2deg(self, val):
# https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#output-sub-command-tiltconfigimpact-impactthreshold-bumpholdoff-n-a if val > 90:
return val - 256
else:
return val
class VisionSensor(Peripheral): class ColorDistanceSensor(Peripheral):
COLOR_INDEX = 0x00 COLOR_ONLY = 0x00
DISTANCE_INCHES = 0x01 DISTANCE_INCHES = 0x01
COUNT_2INCH = 0x02 COUNT_2INCH = 0x02
DISTANCE_REFLECTED = 0x03 DISTANCE_HOW_CLOSE = 0x03
AMBIENT_LIGHT = 0x04 DISTANCE_SUBINCH_HOW_CLOSE = 0x04
SET_COLOR = 0x05 OFF1 = 0x05
COLOR_RGB = 0x06 STREAM_3_VALUES = 0x06
SET_IR_TX = 0x07 OFF2 = 0x07
COLOR_DISTANCE_FLOAT = 0x08 # it's not declared by dev's mode info COLOR_DISTANCE_FLOAT = 0x08
LUMINOSITY = 0x09
DEBUG = 0x09 # first val is by fact ambient light, second is zero SOME_20BYTES = 0x0a # TODO: understand it
CALIBRATE = 0x0a # gives constant values
def __init__(self, parent, port): def __init__(self, parent, port):
super(VisionSensor, self).__init__(parent, port) super(ColorDistanceSensor, self).__init__(parent, port)
def subscribe(self, callback, mode=COLOR_DISTANCE_FLOAT, granularity=1): def subscribe(self, callback, mode=COLOR_DISTANCE_FLOAT, granularity=1, async=False):
super(VisionSensor, self).subscribe(callback, mode, granularity) super(ColorDistanceSensor, self).subscribe(callback, mode, granularity)
def _decode_port_data(self, msg): def handle_port_data(self, data):
data = msg.payload if self._port_subscription_mode == self.COLOR_DISTANCE_FLOAT:
if self._port_mode.mode == self.COLOR_INDEX: color = usbyte(data, 4)
color = usbyte(data, 0) distance = usbyte(data, 5)
return (color,) partial = usbyte(data, 7)
elif self._port_mode.mode == self.COLOR_DISTANCE_FLOAT:
color = usbyte(data, 0)
val = usbyte(data, 1)
partial = usbyte(data, 3)
if partial: if partial:
val += 1.0 / partial distance += 1.0 / partial
return (color, float(val)) self._notify_subscribers(color, float(distance))
elif self._port_mode.mode == self.DISTANCE_INCHES: elif self._port_subscription_mode == self.COLOR_ONLY:
val = usbyte(data, 0) color = usbyte(data, 4)
return (val,) self._notify_subscribers(color)
elif self._port_mode.mode == self.DISTANCE_REFLECTED: elif self._port_subscription_mode == self.DISTANCE_INCHES:
val = usbyte(data, 0) / 100.0 distance = usbyte(data, 4)
return (val,) self._notify_subscribers(distance)
elif self._port_mode.mode == self.AMBIENT_LIGHT: elif self._port_subscription_mode == self.DISTANCE_HOW_CLOSE:
val = usbyte(data, 0) / 100.0 distance = usbyte(data, 4)
return (val,) self._notify_subscribers(distance)
elif self._port_mode.mode == self.COUNT_2INCH: elif self._port_subscription_mode == self.DISTANCE_SUBINCH_HOW_CLOSE:
count = usint(data, 0) distance = usbyte(data, 4)
return (count,) self._notify_subscribers(distance)
elif self._port_mode.mode == self.COLOR_RGB: elif self._port_subscription_mode == self.OFF1 or self._port_subscription_mode == self.OFF2:
val1 = int(255 * ushort(data, 0) / 1023.0) log.info("Turned off led on %s", self)
val2 = int(255 * ushort(data, 2) / 1023.0) elif self._port_subscription_mode == self.COUNT_2INCH:
val3 = int(255 * ushort(data, 4) / 1023.0) count = unpack("<L", data[4:8])[0] # is it all 4 bytes or just 2?
return (val1, val2, val3) self._notify_subscribers(count)
elif self._port_mode.mode == self.DEBUG: elif self._port_subscription_mode == self.STREAM_3_VALUES:
val1 = 10 * ushort(data, 0) / 1023.0 # TODO: understand better meaning of these 3 values
val2 = 10 * ushort(data, 2) / 1023.0 val1 = ushort(data, 4)
return (val1, val2) val2 = ushort(data, 6)
elif self._port_mode.mode == self.CALIBRATE: val3 = ushort(data, 8)
return [ushort(data, x * 2) for x in range(8)] self._notify_subscribers(val1, val2, val3)
else: elif self._port_subscription_mode == self.LUMINOSITY:
log.debug("Unhandled VisionSensor data in mode %s: %s", self._port_mode.mode, str2hex(data)) luminosity = ushort(data, 4) / 1023.0
return () self._notify_subscribers(luminosity)
else: # TODO: support whatever we forgot
def set_color(self, color): log.debug("Unhandled data in mode %s: %s", self._port_subscription_mode, str2hex(data))
if color == COLOR_NONE:
color = COLOR_BLACK
if color not in COLORS:
raise ValueError("Color %s is not in list of available colors" % color)
self.set_port_mode(self.SET_COLOR)
payload = pack("<B", self.SET_COLOR) + pack("<B", color)
msg = MsgPortOutput(self.port, MsgPortOutput.WRITE_DIRECT_MODE_DATA, payload)
self._send_output(msg)
def set_ir_tx(self, level=1.0):
assert 0 <= level <= 1.0
self.set_port_mode(self.SET_IR_TX)
payload = pack("<B", self.SET_IR_TX) + pack("<H", int(level * 65535))
msg = MsgPortOutput(self.port, MsgPortOutput.WRITE_DIRECT_MODE_DATA, payload)
self._send_output(msg)
class Voltage(Peripheral): class Voltage(Peripheral):
# sensor says there are "L" and "S" values, but what are they? MODE1 = 0x00 # give less frequent notifications
VOLTAGE_L = 0x00 MODE2 = 0x01 # give more frequent notifications, maybe different value (cpu vs board?)
VOLTAGE_S = 0x01
def __init__(self, parent, port): def __init__(self, parent, port):
super(Voltage, self).__init__(parent, port) super(Voltage, self).__init__(parent, port)
self.last_value = None
def _decode_port_data(self, msg): def subscribe(self, callback, mode=MODE1, granularity=1, async=False):
data = msg.payload super(Voltage, self).subscribe(callback, mode, granularity)
val = ushort(data, 0)
volts = 9600.0 * val / 3893.0 / 1000.0 # we know only voltage subscription from it. is it really battery or just onboard voltage?
return (volts,) # device has turned off on 1b0e00060045 3c 0803 / 1b0e000600453c 0703
# moderate 9v ~= 3840
# good 7.5v ~= 3892
# liion 5v ~= 2100
def handle_port_data(self, data):
val = ushort(data, 4)
self.last_value = val / 4096.0
if self.last_value < 0.2:
logging.warning("Battery low! %s%%", int(100 * self.last_value))
self._notify_subscribers(self.last_value)
class Current(Peripheral): class Amperage(Peripheral):
CURRENT_L = 0x00 MODE1 = 0x00 # give less frequent notifications
CURRENT_S = 0x01 MODE2 = 0x01 # give more frequent notifications, maybe different value (cpu vs board?)
def __init__(self, parent, port): def __init__(self, parent, port):
super(Current, self).__init__(parent, port) super(Amperage, self).__init__(parent, port)
self.last_value = None
def _decode_port_data(self, msg): def subscribe(self, callback, mode=MODE1, granularity=1, async=False):
val = ushort(msg.payload, 0) super(Amperage, self).subscribe(callback, mode, granularity)
milliampers = 2444 * val / 4095.0
return (milliampers,) def handle_port_data(self, data):
val = ushort(data, 4)
self.last_value = val / 4096.0
self._notify_subscribers(self.last_value)
class Button(Peripheral): class Button(Peripheral):
@ -648,10 +429,11 @@ class Button(Peripheral):
def __init__(self, parent): def __init__(self, parent):
super(Button, self).__init__(parent, 0) # fake port 0 super(Button, self).__init__(parent, 0) # fake port 0
self.hub.add_message_handler(MsgHubProperties, self._props_msg)
def subscribe(self, callback, mode=None, granularity=1): def subscribe(self, callback, mode=None, granularity=1, async=False):
self.hub.send(MsgHubProperties(MsgHubProperties.BUTTON, MsgHubProperties.UPD_ENABLE)) self.started()
self.parent.send(MSG_DEVICE_INFO, pack('<B', INFO_BUTTON_STATE) + pack('<B', INFO_ACTION_SUBSCRIBE))
self._wait_sync(async)
if callback: if callback:
self._subscribers.add(callback) self._subscribers.add(callback)
@ -661,11 +443,10 @@ class Button(Peripheral):
self._subscribers.remove(callback) self._subscribers.remove(callback)
if not self._subscribers: if not self._subscribers:
self.hub.send(MsgHubProperties(MsgHubProperties.BUTTON, MsgHubProperties.UPD_DISABLE)) self.parent.send(MSG_DEVICE_INFO, pack('<B', INFO_BUTTON_STATE) + pack('<B', INFO_ACTION_UNSUBSCRIBE))
def _props_msg(self, msg): def handle_port_data(self, data):
""" param = usbyte(data, 5)
:type msg: MsgHubProperties if self.in_progress():
""" self.finished()
if msg.property == MsgHubProperties.BUTTON and msg.operation == MsgHubProperties.UPSTREAM_UPDATE: self._notify_subscribers(bool(param))
self._notify_subscribers(usbyte(msg.parameters, 0))

View File

@ -1,45 +0,0 @@
"""
This module offers some utilities, in a way they are work in both Python 2 and 3
"""
import binascii
import logging
import sys
from struct import unpack
log = logging.getLogger(__name__)
if sys.version_info[0] == 2:
import Queue as queue
else:
import queue as queue
queue = queue # just to use it
def check_unpack(seq, index, pattern, size):
"""Check that we got size bytes, if so, unpack using pattern"""
data = seq[index: index + size]
assert len(data) == size, "Unexpected data len %d, expected %d" % (len(data), size)
return unpack(pattern, data)[0]
def usbyte(seq, index):
return check_unpack(seq, index, "<B", 1)
def ushort(seq, index):
return check_unpack(seq, index, "<H", 2)
def usint(seq, index):
return check_unpack(seq, index, "<I", 4)
def str2hex(data): # we need it for python 2+3 compatibility
# if sys.version_info[0] == 3:
# data = bytes(data, 'ascii')
if not isinstance(data, (bytes, bytearray)):
data = bytes(data, "ascii")
hexed = binascii.hexlify(data)
return hexed

View File

@ -1,2 +0,0 @@
[metadata]
description-file = README.md

View File

@ -1,24 +1,10 @@
from setuptools import setup from distutils.core import setup
setup( setup(name='pylgbst',
name="pylgbst", description='Python library to interact with LEGO Move Hub (from Lego BOOST set)',
version="1.2.0", version='0.4',
author='Andrey Pokhilko',
author="Andrey Pokhilko", author_email='apc4@ya.ru',
author_email="apc4@ya.ru", packages=['pylgbst'],
license="MIT", requires=['gattlib']
description="Python library to interact with LEGO PoweredUp devices (Lego BOOST etc.)", )
url='https://github.com/undera/pylgbst',
keywords=['LEGO', 'ROBOTICS', 'BLUETOOTH'],
packages=["pylgbst", "pylgbst.comms"],
requires=[],
extras_require={
# Note that dbus and gi are normally system packages
"gatt": ["gatt", "dbus", "gi"],
"gattlib": ["gattlib"],
"pygatt": ["pygatt", "pexpect"],
"bluepy": ["bluepy"],
"bleak": ["bleak"],
},
)

235
tests.py Normal file
View File

@ -0,0 +1,235 @@
import unittest
from binascii import unhexlify
from examples.plotter import Plotter
from pylgbst import *
from pylgbst.comms import Connection
from pylgbst.movehub import MoveHub
HANDLE = MOVE_HUB_HARDWARE_HANDLE
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('test')
class ConnectionMock(Connection):
"""
For unit testing purposes
"""
def __init__(self):
super(ConnectionMock, self).__init__()
self.writes = []
self.notifications = []
self.notification_handler = None
self.running = True
self.finished = False
def set_notify_handler(self, handler):
self.notification_handler = handler
thr = Thread(target=self.notifier)
thr.setDaemon(True)
thr.start()
def notifier(self):
while self.running:
if self.notification_handler:
while self.notifications:
handle, data = self.notifications.pop(0)
self.notification_handler(handle, unhexlify(data.replace(' ', '')))
time.sleep(0.1)
self.finished = True
def write(self, handle, data):
log.debug("Writing to %s: %s", handle, str2hex(data))
self.writes.append((handle, str2hex(data)))
class HubMock(MoveHub):
# noinspection PyUnresolvedReferences
def __init__(self, connection=None):
"""
:type connection: ConnectionMock
"""
super(HubMock, self).__init__(connection if connection else ConnectionMock())
self.notify_mock = self.connection.notifications
self.writes = self.connection.writes
def _wait_for_devices(self):
pass
def _report_status(self):
pass
class GeneralTest(unittest.TestCase):
def _wait_notifications_handled(self, hub):
hub.connection.running = False
for _ in range(1, 180):
time.sleep(1)
log.debug("Waiting for notifications to process...")
if hub.connection.finished:
log.debug("Done waiting")
break
def test_led(self):
hub = HubMock()
led = LED(hub, PORT_LED)
led.set_color(COLOR_RED)
self.assertEqual("0801813201510009", hub.writes[0][1])
def test_tilt_sensor(self):
hub = HubMock()
hub.notify_mock.append((HANDLE, '1b0e00 0f00 04 3a 0128000000000100000001'))
time.sleep(1)
def callback(param1, param2=None, param3=None):
if param2 is None:
log.debug("Tilt: %s", TiltSensor.DUO_STATES[param1])
else:
log.debug("Tilt: %s %s %s", param1, param2, param3)
self._inject_notification(hub, '1b0e000a00 47 3a 090100000001', 1)
hub.tilt_sensor.subscribe(callback)
hub.notify_mock.append((HANDLE, "1b0e000500453a05"))
hub.notify_mock.append((HANDLE, "1b0e000a00473a010100000001"))
time.sleep(1)
self._inject_notification(hub, '1b0e000a00 47 3a 090100000001', 1)
hub.tilt_sensor.subscribe(callback, TiltSensor.MODE_2AXIS_SIMPLE)
hub.notify_mock.append((HANDLE, "1b0e000500453a09"))
time.sleep(1)
self._inject_notification(hub, '1b0e000a00 47 3a 090100000001', 1)
hub.tilt_sensor.subscribe(callback, TiltSensor.MODE_2AXIS_FULL)
hub.notify_mock.append((HANDLE, "1b0e000600453a04fe"))
time.sleep(1)
self._wait_notifications_handled(hub)
hub.tilt_sensor.unsubscribe(callback)
# TODO: assert
def test_motor(self):
conn = ConnectionMock()
conn.notifications.append((14, '1b0e00 0900 04 39 0227003738'))
hub = HubMock(conn)
time.sleep(0.1)
conn.notifications.append((14, '1b0e00050082390a'))
hub.motor_AB.timed(1.5)
self.assertEqual("0d018139110adc056464647f03", conn.writes[0][1])
conn.notifications.append((14, '1b0e00050082390a'))
hub.motor_AB.angled(90)
self.assertEqual("0f018139110c5a0000006464647f03", conn.writes[1][1])
def test_capabilities(self):
conn = ConnectionMock()
conn.notifications.append((14, '1b0e00 0f00 04 01 0125000000001000000010'))
conn.notifications.append((14, '1b0e00 0f00 04 02 0126000000001000000010'))
conn.notifications.append((14, '1b0e00 0f00 04 37 0127000100000001000000'))
conn.notifications.append((14, '1b0e00 0f00 04 38 0127000100000001000000'))
conn.notifications.append((14, '1b0e00 0900 04 39 0227003738'))
conn.notifications.append((14, '1b0e00 0f00 04 32 0117000100000001000000'))
conn.notifications.append((14, '1b0e00 0f00 04 3a 0128000000000100000001'))
conn.notifications.append((14, '1b0e00 0f00 04 3b 0115000200000002000000'))
conn.notifications.append((14, '1b0e00 0f00 04 3c 0114000200000002000000'))
conn.notifications.append((14, '1b0e00 0f00 8202 01'))
conn.notifications.append((14, '1b0e00 0f00 8202 0a'))
self._inject_notification(conn, '1b0e00 1200 0101 06 4c45474f204d6f766520487562', 1)
self._inject_notification(conn, '1b0e00 1200 0108 06 4c45474f204d6f766520487562', 2)
self._inject_notification(conn, '1b0e00 0900 47 3c 0227003738', 3)
self._inject_notification(conn, '1b0e00 0600 45 3c 020d', 4)
hub = MoveHub(conn)
# demo_all(hub)
self._wait_notifications_handled(hub)
def test_color_sensor(self):
#
hub = HubMock()
hub.notify_mock.append((HANDLE, '1b0e000f00 04010125000000001000000010'))
time.sleep(1)
def callback(color, unk1, unk2=None):
name = COLORS[color] if color is not None else 'NONE'
log.info("Color: %s %s %s", name, unk1, unk2)
self._inject_notification(hub, '1b0e000a00 4701090100000001', 1)
hub.color_distance_sensor.subscribe(callback)
hub.notify_mock.append((HANDLE, "1b0e0008004501ff0aff00"))
time.sleep(1)
# TODO: assert
self._wait_notifications_handled(hub)
hub.color_distance_sensor.unsubscribe(callback)
def test_button(self):
hub = HubMock()
time.sleep(1)
def callback(pressed):
log.info("Pressed: %s", pressed)
hub.notify_mock.append((HANDLE, "1b0e00060001020600"))
hub.button.subscribe(callback)
hub.notify_mock.append((HANDLE, "1b0e00060001020601"))
hub.notify_mock.append((HANDLE, "1b0e00060001020600"))
time.sleep(1)
# TODO: assert
self._wait_notifications_handled(hub)
hub.button.unsubscribe(callback)
def _inject_notification(self, hub, notify, pause):
def inject():
time.sleep(pause)
if isinstance(hub, ConnectionMock):
hub.notifications.append((HANDLE, notify))
else:
hub.notify_mock.append((HANDLE, notify))
Thread(target=inject).start()
class TestPlotter(unittest.TestCase):
def test_calc1(self):
self.assertEqual((100, 1, 0.5), Plotter._calc_motor_timed(100, 50))
def test_calc2(self):
self.assertEqual((200, 0.5, 1), Plotter._calc_motor_timed(100, 200))
def test_calc_xoverflow(self):
self.assertEqual((800, 0.0125, 1), Plotter._calc_motor_timed(10, 800))
def test_calc3(self):
self.assertEqual((100, 1, 0), Plotter._calc_motor_timed(100, 0))
def test_calc4(self):
self.assertEqual((200, 0, 1), Plotter._calc_motor_timed(0, 200))
def test_calc5(self):
parts = 2
for x in range(0, parts + 1):
res = Plotter._calc_motor_angled(1.0, x * 1.0 / parts)
logging.debug("%s", res)
for x in range(0, parts + 1):
res = Plotter._calc_motor_angled(x * 1.0 / parts, 1.0)
logging.debug("%s", res)
def test_zeroes(self):
res = Plotter._calc_motor_angled(1.0, 0.0)
self.assertNotEqual(0, res[1])
res = Plotter._calc_motor_angled(0.0, 1.0)
self.assertNotEqual(0, res[2])
def test_calc6(self):
res = Plotter._calc_motor_angled(1.0, 0.2)
logging.debug("%s", res)
res = Plotter._calc_motor_angled(1.0, 0.5)
logging.debug("%s", res)
res = Plotter._calc_motor_angled(1.0, 0.8)
logging.debug("%s", res)

View File

@ -1,87 +0,0 @@
import sys
import time
from binascii import unhexlify
from pylgbst.comms import Connection
from pylgbst.hub import MoveHub, Hub
from pylgbst.peripherals import *
logging.basicConfig(level=logging.DEBUG if 'pydevd' in sys.modules else logging.INFO)
log = logging.getLogger('test')
class HubMock(Hub):
"""
:type connection: ConnectionMock
"""
# noinspection PyUnresolvedReferences
def __init__(self, conn=None):
super(HubMock, self).__init__(conn if conn else ConnectionMock())
self.connection = self.connection
self.notify_mock = self.connection.notifications
self.writes = self.connection.writes
class ConnectionMock(Connection):
"""
For unit testing purposes
"""
def __init__(self):
super(ConnectionMock, self).__init__()
self.writes = []
self.notifications = []
self.notification_handler = None
self.running = True
self.finished = False
self.thr = Thread(target=self.notifier)
self.thr.setDaemon(True)
def set_notify_handler(self, handler):
self.notification_handler = handler
self.thr.start()
def notifier(self):
while self.running or self.notifications:
if self.notification_handler:
while self.notifications:
data = self.notifications.pop(0)
s = unhexlify(data.replace(' ', ''))
self.notification_handler(MoveHub.HUB_HARDWARE_HANDLE, bytes(s))
time.sleep(0.01)
self.finished = True
def write(self, handle, data):
log.debug("Writing to %s: %s", handle, str2hex(data))
self.writes.append((handle, str2hex(data)))
def connect(self, hub_mac=None):
"""
:rtype: ConnectionMock
"""
super(ConnectionMock, self).connect(hub_mac)
log.debug("Mock connected")
return self
def is_alive(self):
return not self.finished and self.thr.is_alive()
def notification_delayed(self, payload, pause=0.001):
def inject():
time.sleep(pause)
self.notifications.append(payload)
Thread(target=inject).start()
def wait_notifications_handled(self):
self.running = False
for _ in range(1, 180):
time.sleep(0.01)
log.debug("Waiting for notifications to process...")
if self.finished:
log.debug("Done waiting for notifications to process")
break

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +0,0 @@
import sys
import time
import unittest
import bleak
from packaging import version
import pylgbst
import pylgbst.comms.cbleak as cbleak
bleak.BleakClient = object()
bleak.discover = object()
last_response = None
lt37 = version.parse(sys.version.split(' ')[0]) < version.parse("3.7")
class BleakDriverTest(unittest.TestCase):
def test_driver_creation(self):
connection = pylgbst.get_connection_bleak()
self.assertIsInstance(connection, cbleak.BleakDriver)
self.assertFalse(connection.is_alive(), 'Checking that factory returns not started driver')
@unittest.skipIf(lt37, "Python version is too low")
def test_communication(self):
driver = cbleak.BleakDriver()
async def fake_thread():
print('Fake thread initialized')
while not driver._abort:
time.sleep(0.1)
if cbleak.req_queue.qsize() != 0:
print('Received data, sending back')
data = cbleak.req_queue.get()
cbleak.resp_queue.put(data)
driver._bleak_thread = fake_thread
driver.set_notify_handler(BleakDriverTest.validation_handler)
driver.enable_notifications()
time.sleep(0.5) # time for driver initialization
self.assertTrue(driver.is_alive(), 'Checking that driver starts')
handle = 0x32
data = [0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF]
driver.write(handle, data)
time.sleep(0.5) # processing time
self.assertEqual(handle, last_response[0], 'Verifying response handle')
self.assertEqual(bytes(data), last_response[1], 'Verifying response data')
driver.disconnect()
time.sleep(0.5) # processing time
self.assertFalse(driver.is_alive())
@staticmethod
def validation_handler(handle, data):
global last_response
last_response = (handle, data)
if __name__ == '__main__':
unittest.main()

View File

@ -1,61 +0,0 @@
import unittest
import pylgbst.comms.cbluepy as bp_backend
class PeripheralMock(object):
def __init__(self, addr, addrType, ifaceNumber):
pass
def waitForNotifications(self, timeout):
pass
def writeCharacteristic(self, handle, data):
pass
def withDelegate(self, delegate):
pass
def disconnect(self):
pass
bp_backend.PROPAGATE_DISPATCHER_EXCEPTION = True
bp_backend.btle.Peripheral = lambda *args, **kwargs: PeripheralMock(*args, **kwargs)
class BluepyTestCase(unittest.TestCase):
def test_get_iface_number(self):
self.assertEqual(bp_backend._get_iface_number('hci0'), 0)
self.assertEqual(bp_backend._get_iface_number('hci10'), 10)
try:
bp_backend._get_iface_number('ads12')
self.fail('Missing exception for incorrect value')
except ValueError:
pass
def test_delegate(self):
def _handler(handle, data):
_handler.called = True
delegate = bp_backend.BluepyDelegate(_handler)
delegate.handleNotification(123, 'qwe')
self.assertEqual(_handler.called, True)
def test_threaded_peripheral(self):
tp = bp_backend.BluepyThreadedPeripheral('address', 'addrType', 'hci0')
self.assertEqual(tp._addr, 'address')
self.assertEqual(tp._addrType, 'addrType')
self.assertEqual(tp._iface_number, 0)
self.assertNotEqual(tp._dispatcher_thread, None)
# Schedule some methods to async queue and give them some time to resolve
tp.set_notify_handler(lambda: '')
tp.write(123, 'qwe')
tp._dispatcher_thread.join(1)
self.assertEqual(tp._dispatcher_thread.is_alive(), True)
tp.disconnect()
tp._dispatcher_thread.join(2)
self.assertEqual(tp._dispatcher_thread.is_alive(), False)

View File

@ -1,40 +0,0 @@
import unittest
from pylgbst.comms import *
class ConnectionTestCase(unittest.TestCase):
def test_is_device_matched(self):
conn = Connection()
hub_address = '1a:2A:3A:4A:5A:6A'
other_address = 'A1:a2:a3:a4:a5:a6'
zero_address = '00:00:00:00:00:00'
hub_name = 'LEGO Move Hub'
other_name = 'HRM'
test_matrix = [
# address, name, hub_mac, expected
(hub_address, hub_name, hub_address, None, True),
(hub_address, hub_name, None, hub_name, True),
(hub_address, None, hub_address, None, True),
(hub_address, None, None, hub_name, False),
(hub_address, other_name, hub_address, None, True),
(hub_address, other_name, None, hub_name, False),
(other_address, hub_name, hub_address, None, False),
(other_address, hub_name, None, hub_name, True),
(other_address, None, hub_address, None, False),
(other_address, None, None, hub_name, False),
(other_address, other_name, hub_address, None, False),
(other_address, other_name, None, hub_name, False),
(zero_address, hub_name, hub_address, None, False),
(zero_address, hub_name, None, hub_name, False),
(zero_address, None, hub_address, None, False),
(zero_address, None, None, hub_name, False),
(zero_address, other_name, hub_address, None, False),
(zero_address, other_name, None, hub_name, False),
]
for address, name, hub_mac, fname, expected in test_matrix:
matched = conn._is_device_matched(address=address, dev_name=name, hub_mac=hub_mac, find_name=fname)
self.assertEqual(matched, expected)

View File

@ -1,50 +0,0 @@
import dbus
import sys
import unittest
from gatt import DeviceManager
from pylgbst.comms.cgatt import CustomDevice, GattConnection
from tests import log, str2hex
class MockBus(object):
def __init__(self, *args, **kwargs):
# super(MockBus, self).__init__(*args, **kwargs)
pass
def get_object(self, bus_name, object_path, introspect=True, follow_name_owner_changes=False, **kwargs):
return None
dbus.SystemBus = lambda: MockBus()
class DeviceManagerMock(DeviceManager, object):
def update_devices(self):
pass
class TestGatt(unittest.TestCase):
def test_one(self):
log.debug("")
manager = DeviceManagerMock("hci0")
obj = CustomDevice("AA", manager)
def callback(handle, value):
log.debug("%s: %s", type(value), str2hex(value))
if sys.version_info[0] == 2:
self.assertEquals("0f0004020126000000001000000010", str2hex(value))
obj.set_notific_handler(callback)
arr = "dbus.Array([dbus.Byte(15), dbus.Byte(0), dbus.Byte(4), dbus.Byte(2), dbus.Byte(1), dbus.Byte(38), " \
"dbus.Byte(0), dbus.Byte(0), dbus.Byte(0), dbus.Byte(0), dbus.Byte(16), dbus.Byte(0), dbus.Byte(0), " \
"dbus.Byte(0), dbus.Byte(16)], signature=dbus.Signature('y'), variant_level=1)"
obj.characteristic_value_updated(None, arr if sys.version_info[0] == 2 else bytes(arr, 'ascii'))
def test_conn(self):
try:
obj = GattConnection()
obj.connect()
except AttributeError:
pass

View File

@ -1,145 +0,0 @@
import time
import unittest
from pylgbst.hub import Hub, MoveHub
from pylgbst.messages import MsgHubAction, MsgHubAlert, MsgHubProperties
from pylgbst.peripherals import VisionSensor
from pylgbst.utilities import usbyte
from tests import ConnectionMock
class HubTest(unittest.TestCase):
def test_hub_properties(self):
conn = ConnectionMock().connect()
hub = Hub(conn)
conn.notification_delayed('060001060600', 0.1)
msg = MsgHubProperties(MsgHubProperties.VOLTAGE_PERC, MsgHubProperties.UPD_REQUEST)
resp = hub.send(msg)
assert isinstance(resp, MsgHubProperties)
self.assertEqual(1, len(resp.parameters))
self.assertEqual(0, usbyte(resp.parameters, 0))
conn.notification_delayed('12000101064c45474f204d6f766520487562', 0.1)
msg = MsgHubProperties(MsgHubProperties.ADVERTISE_NAME, MsgHubProperties.UPD_REQUEST)
resp = hub.send(msg)
assert isinstance(resp, MsgHubProperties)
self.assertEqual(b"LEGO Move Hub", resp.parameters)
conn.wait_notifications_handled()
def test_device_attached(self):
conn = ConnectionMock().connect()
hub = Hub(conn)
# regular startup attaches
conn.notifications.append('0f0004010126000000001000000010')
conn.notifications.append('0f0004020125000000001000000010')
# detach and reattach
conn.notifications.append('0500040100')
conn.notifications.append('0500040200')
conn.notifications.append('0f0004010126000000001000000010')
conn.notifications.append('0f0004020125000000001000000010')
del hub
conn.wait_notifications_handled()
def test_hub_actions(self):
conn = ConnectionMock().connect()
hub = Hub(conn)
conn.notification_delayed('04000230', 0.1)
hub.send(MsgHubAction(MsgHubAction.UPSTREAM_SHUTDOWN))
conn.notification_delayed('04000231', 0.1)
hub.send(MsgHubAction(MsgHubAction.UPSTREAM_DISCONNECT))
conn.notification_delayed('04000232', 0.1)
hub.send(MsgHubAction(MsgHubAction.UPSTREAM_BOOT_MODE))
time.sleep(0.1)
conn.wait_notifications_handled()
def test_hub_alerts(self):
conn = ConnectionMock().connect()
hub = Hub(conn)
conn.notification_delayed('0600 03 0104ff', 0.1)
hub.send(MsgHubAlert(MsgHubAlert.LOW_VOLTAGE, MsgHubAlert.UPD_REQUEST))
conn.notification_delayed('0600 03 030400', 0.1)
hub.send(MsgHubAlert(MsgHubAlert.LOW_SIGNAL, MsgHubAlert.UPD_REQUEST))
conn.wait_notifications_handled()
def test_error(self):
conn = ConnectionMock().connect()
hub = Hub(conn)
conn.notification_delayed("0500056105", 0.1)
time.sleep(0.2)
conn.wait_notifications_handled()
def test_disconnect_off(self):
conn = ConnectionMock().connect()
hub = Hub(conn)
conn.notification_delayed("04000231", 0.1)
hub.disconnect()
self.assertEqual(b"04000202", conn.writes[1][1])
conn = ConnectionMock().connect()
hub = Hub(conn)
conn.notification_delayed("04000230", 0.1)
hub.switch_off()
self.assertEqual(b"04000201", conn.writes[1][1])
def test_sensor(self):
conn = ConnectionMock().connect()
conn.notifications.append("0f0004020125000000001000000010") # add dev
hub = Hub(conn)
time.sleep(0.1)
dev = hub.peripherals[0x02]
assert isinstance(dev, VisionSensor)
conn.notification_delayed("0a004702080000000000", 0.1)
conn.notification_delayed("08004502ff0aff00", 0.2) # value for sensor
self.assertEqual((255, 10.0), dev.get_sensor_data(VisionSensor.COLOR_DISTANCE_FLOAT))
self.assertEqual(b"0a004102080100000000", conn.writes.pop(1)[1])
self.assertEqual(b"0500210200", conn.writes.pop(1)[1])
vals = []
cb = lambda x, y=None: vals.append((x, y))
conn.notification_delayed("0a004702080100000001", 0.1) # subscribe ack
dev.subscribe(cb, granularity=1)
self.assertEqual(b"0a004102080100000001", conn.writes.pop(1)[1])
conn.notification_delayed("08004502ff0aff00", 0.1) # value for sensor
time.sleep(0.3)
conn.notification_delayed("0a004702080000000000", 0.3) # unsubscribe ack
dev.unsubscribe(cb)
self.assertEqual(b"0a004102080100000000", conn.writes.pop(1)[1])
self.assertEqual([(255, 10.0)], vals)
class MoveHubTest(unittest.TestCase):
def test_capabilities(self):
conn = ConnectionMock()
conn.notifications.append('0f00 04 02 0125000000001000000010')
conn.notifications.append('0f00 04 03 0126000000001000000010')
conn.notifications.append('0f00 04 00 0127000100000001000000')
conn.notifications.append('0f00 04 01 0127000100000001000000')
conn.notifications.append('0900 04 10 0227003738')
conn.notifications.append('0f00 04 32 0117000100000001000000')
conn.notifications.append('0f00 04 3a 0128000000000100000001')
conn.notifications.append('0f00 04 3b 0115000200000002000000')
conn.notifications.append('0f00 04 3c 0114000200000002000000')
conn.notification_delayed('12000101064c45474f204d6f766520487562', 1.1)
conn.notification_delayed('0b00010d06001653a0d1d4', 1.3)
conn.notification_delayed('060001060600', 1.5)
conn.notification_delayed('0600030104ff', 1.7)
MoveHub(conn.connect())
time.sleep(1)
conn.wait_notifications_handled()
self.assertEqual(b"0500010105", conn.writes[1][1])
self.assertEqual(b"0500010d05", conn.writes[2][1])
self.assertEqual(b"0500010605", conn.writes[3][1])
self.assertEqual(b"0500030103", conn.writes[4][1])

View File

@ -1,242 +0,0 @@
import logging
import time
import unittest
from pylgbst.hub import MoveHub
from pylgbst.peripherals import LEDRGB, TiltSensor, COLOR_RED, Button, Current, Voltage, VisionSensor, \
EncodedMotor
from tests import HubMock
class PeripheralsTest(unittest.TestCase):
def test_button(self):
hub = HubMock()
time.sleep(0.1)
button = Button(hub)
hub.peripherals[0x00] = button
vals = []
def callback(pressed):
vals.append(pressed)
hub.connection.notification_delayed("060001020600", 0.0)
button.subscribe(callback)
time.sleep(0.1)
hub.connection.notification_delayed("060001020600", 0.1)
hub.connection.notification_delayed("060001020601", 0.2)
hub.connection.notification_delayed("060001020600", 0.3)
time.sleep(0.4)
button.unsubscribe(callback)
time.sleep(0.1)
hub.connection.wait_notifications_handled()
self.assertEqual([0, 1, 0], vals)
self.assertEqual(b"0500010202", hub.writes[1][1])
self.assertEqual(b"0500010203", hub.writes[2][1])
def test_led(self):
hub = HubMock()
hub.led = LEDRGB(hub, MoveHub.PORT_LED)
hub.peripherals[MoveHub.PORT_LED] = hub.led
hub.connection.notification_delayed("0a004732000100000000", 0.1)
hub.connection.notification_delayed("050082320a", 0.2)
hub.led.set_color(COLOR_RED)
self.assertEqual(b"0a004132000100000000", hub.writes.pop(1)[1])
self.assertEqual(b"0800813211510009", hub.writes.pop(1)[1])
hub.connection.notification_delayed("0a004732010100000000", 0.1)
hub.connection.notification_delayed("050082320a", 0.2)
hub.led.set_color((32, 64, 96))
self.assertEqual(b"0a004132010100000000", hub.writes.pop(1)[1])
self.assertEqual(b"0a008132115101204060", hub.writes.pop(1)[1])
def test_current(self):
hub = HubMock()
time.sleep(0.1)
current = Current(hub, MoveHub.PORT_CURRENT)
hub.peripherals[MoveHub.PORT_CURRENT] = current
vals = []
def callback(val):
vals.append(val)
hub.connection.notification_delayed("0a00473b000100000001", 0.1)
current.subscribe(callback)
hub.connection.notification_delayed("0600453ba400", 0.1)
time.sleep(0.2)
hub.connection.notification_delayed("0a00473b000000000000", 0.1)
current.unsubscribe(callback)
hub.connection.wait_notifications_handled()
self.assertEqual([97.87936507936507], vals)
self.assertEqual(b"0a00413b000100000001", hub.writes[1][1])
self.assertEqual(b"0a00413b000100000000", hub.writes[2][1])
def test_voltage(self):
hub = HubMock()
time.sleep(0.1)
voltage = Voltage(hub, MoveHub.PORT_VOLTAGE)
hub.peripherals[MoveHub.PORT_VOLTAGE] = voltage
vals = []
def callback(val):
vals.append(val)
hub.connection.notification_delayed("0a00473c000100000001", 0.1)
voltage.subscribe(callback)
hub.connection.notification_delayed("0600453c9907", 0.1)
time.sleep(0.2)
hub.connection.notification_delayed("0a00473c000000000000", 0.1)
voltage.unsubscribe(callback)
hub.connection.wait_notifications_handled()
self.assertEqual([4.79630105317236], vals)
self.assertEqual(b"0a00413c000100000001", hub.writes[1][1])
self.assertEqual(b"0a00413c000100000000", hub.writes[2][1])
def test_tilt_sensor(self):
hub = HubMock()
sensor = TiltSensor(hub, MoveHub.PORT_TILT_SENSOR)
hub.peripherals[MoveHub.PORT_TILT_SENSOR] = sensor
hub.connection.notification_delayed('0a00473a000100000000', 0.05)
hub.connection.notification_delayed('0600453a0201', 0.1)
self.assertEqual((2, 1), sensor.get_sensor_data(TiltSensor.MODE_2AXIS_ANGLE))
hub.connection.notification_delayed('0a00473a010100000000', 0.05)
hub.connection.notification_delayed('0500453a00', 0.1)
self.assertEqual((0,), sensor.get_sensor_data(TiltSensor.MODE_2AXIS_SIMPLE))
hub.connection.notification_delayed('0a00473a020100000000', 0.05)
hub.connection.notification_delayed('0600453a0201', 0.1)
self.assertEqual((2,), sensor.get_sensor_data(TiltSensor.MODE_3AXIS_SIMPLE))
hub.connection.notification_delayed('0a00473a030100000000', 0.05)
hub.connection.notification_delayed('0800453a00000000', 0.1)
self.assertEqual((0,), sensor.get_sensor_data(TiltSensor.MODE_IMPACT_COUNT))
hub.connection.notification_delayed('0a00473a040100000000', 0.05)
hub.connection.notification_delayed('0700453afd0140', 0.1)
self.assertEqual((-3, 1, 64), sensor.get_sensor_data(TiltSensor.MODE_3AXIS_ACCEL))
hub.connection.notification_delayed('0a00473a050100000000', 0.05)
hub.connection.notification_delayed('0500453a00', 0.1)
self.assertEqual((0,), sensor.get_sensor_data(TiltSensor.MODE_ORIENT_CF))
hub.connection.notification_delayed('0a00473a060100000000', 0.05)
hub.connection.notification_delayed('0600453a7f14', 0.1)
self.assertEqual((127,), sensor.get_sensor_data(TiltSensor.MODE_IMPACT_CF))
hub.connection.notification_delayed('0a00473a070100000000', 0.05)
hub.connection.notification_delayed('0700453a00feff', 0.1)
self.assertEqual((0, 254, 255), sensor.get_sensor_data(TiltSensor.MODE_CALIBRATION))
hub.connection.wait_notifications_handled()
def test_motor(self):
hub = HubMock()
motor = EncodedMotor(hub, MoveHub.PORT_D)
hub.peripherals[MoveHub.PORT_D] = motor
hub.connection.notification_delayed('050082030a', 0.1)
motor.start_power(1.0)
self.assertEqual(b"07008103110164", hub.writes.pop(1)[1])
hub.connection.notification_delayed('050082030a', 0.1)
motor.stop()
self.assertEqual(b"0c0081031109000064647f03", hub.writes.pop(1)[1])
hub.connection.notification_delayed('050082030a', 0.1)
motor.set_acc_profile(1.0)
self.assertEqual(b"090081031105e80300", hub.writes.pop(1)[1])
hub.connection.notification_delayed('050082030a', 0.1)
motor.set_dec_profile(1.0)
self.assertEqual(b"090081031106e80300", hub.writes.pop(1)[1])
hub.connection.notification_delayed('050082030a', 0.1)
motor.start_speed(1.0)
self.assertEqual(b"090081031107646403", hub.writes.pop(1)[1])
hub.connection.notification_delayed('050082030a', 0.1)
motor.stop()
self.assertEqual(b"0c0081031109000064647f03", hub.writes.pop(1)[1])
logging.debug("\n\n")
hub.connection.notification_delayed('0500820301', 0.1)
hub.connection.notification_delayed('050082030a', 0.2)
motor.timed(1.0)
self.assertEqual(b"0c0081031109e80364647f03", hub.writes.pop(1)[1])
hub.connection.notification_delayed('0500820301', 0.1)
hub.connection.notification_delayed('050082030a', 0.2)
motor.angled(180)
self.assertEqual(b"0e008103110bb400000064647f03", hub.writes.pop(1)[1])
hub.connection.notification_delayed('050082030a', 0.2)
motor.preset_encoder(-180)
self.assertEqual(b"0b0081031151024cffffff", hub.writes.pop(1)[1])
hub.connection.notification_delayed('0500820301', 0.1)
hub.connection.notification_delayed('050082030a', 0.2)
motor.goto_position(0)
self.assertEqual(b"0e008103110d0000000064647f03", hub.writes.pop(1)[1])
hub.connection.wait_notifications_handled()
def test_motor_sensor(self):
hub = HubMock()
motor = EncodedMotor(hub, MoveHub.PORT_C)
hub.peripherals[MoveHub.PORT_C] = motor
vals = []
def callback(*args):
vals.append(args)
hub.connection.notification_delayed('0a004702020100000001', 0.1)
motor.subscribe(callback)
hub.connection.notification_delayed("0800450200000000", 0.1)
hub.connection.notification_delayed("08004502ffffffff", 0.2)
hub.connection.notification_delayed("08004502feffffff", 0.3)
time.sleep(0.4)
hub.connection.notification_delayed('0a004702020000000000', 0.1)
motor.unsubscribe(callback)
hub.connection.wait_notifications_handled()
self.assertEqual([(0,), (-1,), (-2,)], vals)
def test_color_sensor(self):
hub = HubMock()
cds = VisionSensor(hub, MoveHub.PORT_C)
hub.peripherals[MoveHub.PORT_C] = cds
vals = []
def callback(*args):
vals.append(args)
hub.connection.notification_delayed('0a00 4702080100000001', 0.1)
cds.subscribe(callback)
hub.connection.notification_delayed("08004502ff0aff00", 0.1)
time.sleep(0.2)
hub.connection.notification_delayed('0a00 4702090100000001', 0.1)
cds.unsubscribe(callback)
hub.connection.wait_notifications_handled()
self.assertEqual([(255, 10.0)], vals)

View File

@ -1,69 +0,0 @@
import unittest
import pygatt
import serial
from pygatt import BLEAddressType
from pygatt.backends.bgapi.bgapi import MAX_CONNECTION_ATTEMPTS
from pygatt.backends.bgapi.device import BGAPIBLEDevice
from pygatt.backends.bgapi.util import USBSerialDeviceInfo
from pylgbst.comms.cpygatt import GattoolConnection
from tests import log
class SerialMock(serial.Serial):
def write(self, data):
self.is_open = True
log.debug("Write data to serial: %s", data)
return len(data)
def flush(self, *args, **kwargs):
pass
def close(self):
pass
def read(self, size=1):
return bytes()
class BGAPIBLEDeviceMock(BGAPIBLEDevice):
def subscribe(self, uuid, callback=None, indication=False):
log.debug("Mock subscribing")
def char_write_handle(self, char_handle, value, wait_for_response=False):
log.debug("Mock write: %s", value)
class BlueGigaBackendMock(pygatt.BGAPIBackend):
def _open_serial_port(self, max_connection_attempts=MAX_CONNECTION_ATTEMPTS):
log.debug("Mock open serial port")
self._ser = SerialMock()
def expect(self, expected, *args, **kargs):
log.debug("Mock expect")
data = {
"packet_type": 0x04,
"sender": "abcdef".encode('ascii'),
"data": [1, 2, 3],
"rssi": 1
}
self._ble_evt_gap_scan_response(data)
def connect(self, address, timeout=5, address_type=BLEAddressType.public, interval_min=60, interval_max=76,
supervision_timeout=100, latency=0):
log.debug("Mock connect")
device = BGAPIBLEDeviceMock("address", 0, self)
return device
def _detect_device_port(self):
return USBSerialDeviceInfo().port_name
class BlueGigaTests(unittest.TestCase):
def test_1(self):
obj = GattoolConnection()
obj.backend = BlueGigaBackendMock
obj.connect(u'66:65:64:63:62:61')
obj.write(0, "test".encode('ascii'))
obj.set_notify_handler(lambda x: None)