├── .gitignore ├── LICENSE ├── README.md ├── examples └── dots.py ├── nuimo ├── __init__.py └── nuimo.py ├── nuimoctl.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Senic GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuimo Python SDK 2 | [Nuimo](https://senic.com) is a universal smart device controller made by [Senic](https://senic.com). 3 | 4 | The Nuimo Python SDK for Linux allows you to integrate your Nuimo(s) into any type of Linux application or script that can execute Python code. 5 | 6 | ## Prerequisites 7 | The Nuimo SDK requires [Python 3.4+](https://www.python.org) and a recent installation of [BlueZ](http://www.bluez.org/). It is tested to work fine with BlueZ 5.44, slightly older versions should however work, too. 8 | 9 | ## Installation 10 | These instructions assume a Debian-based Linux. 11 | 12 | On Linux the [BlueZ](http://www.bluez.org/) library is necessary to access your built-in Bluetooth controller or Bluetooth USB dongle. Some Linux distributions provide a more up-to-date BlueZ package, some other distributions only install older versions that don't implement all Bluetooth features needed for this SDK. In those cases you want to either update BlueZ or build it from sources. 13 | 14 | ### Updating/installing BlueZ via apt-get 15 | 16 | 1. `bluetoothd --version` Obtains the version of the pre-installed BlueZ. `bluetoothd` daemon must run at startup to expose the Bluetooth API via D-Bus. 17 | 2. `sudo apt-get install --no-install-recommends bluetooth` Installs BlueZ 18 | 3. If the installed version is too old, proceed with next step: [Installing BlueZ from sources](#installing-bluez-from-sources) 19 | 20 | ### Installing BlueZ from sources 21 | 22 | The `bluetoothd` daemon provides BlueZ's D-Bus interfaces that is accessed by the Nuimo SDK to communicate with Nuimo Bluetooth controllers. The following commands download BlueZ 5.44 sources, built them and replace any pre-installed `bluetoothd` daemon. It's not suggested to remove any pre-installed BlueZ package as its deinstallation might remove necessary Bluetooth drivers as well. 23 | 24 | 1. `sudo systemctl stop bluetooth` 25 | 2. `sudo apt-get update` 26 | 3. `sudo apt-get install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev libdbus-glib-1-dev unzip` 27 | 4. `cd` 28 | 5. `mkdir bluez` 29 | 6. `cd bluez` 30 | 7. `wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.44.tar.xz` 31 | 8. `tar xf bluez-5.44.tar.xz` 32 | 9. `cd bluez-5.44` 33 | 10. `./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-library` 34 | 11. `make` 35 | 12. `sudo make install` 36 | 13. `sudo ln -svf /usr/libexec/bluetooth/bluetoothd /usr/sbin/` 37 | 14. `sudo install -v -dm755 /etc/bluetooth` 38 | 15. `sudo install -v -m644 src/main.conf /etc/bluetooth/main.conf` 39 | 16. `sudo systemctl daemon-reload` 40 | 17. `sudo systemctl start bluetooth` 41 | 18. `bluetoothd --version` # should now print 5.44 42 | 43 | Please note that some distributions might use a different directory for system deamons, apply step 13 only as needed. 44 | 45 | ### Enabling your Bluetooth adapter 46 | 47 | 1. `echo "power on" | sudo bluetoothctl` Enables your built-in Bluetooth adapter or external Bluetooth USB dongle 48 | 49 | ### Using BlueZ commandline tools 50 | BlueZ also provides an interactive commandline tool to interact with Bluetooth devices. You know that your BlueZ installation is working fine if it discovers any Bluetooth devices nearby. 51 | 52 | `sudo bluetoothctl` Starts an interactive mode to talk to BlueZ 53 | * `power on` Enables the Bluetooth adapter 54 | * `scan on` Start Bluetooth device scanning and lists all found devices with MAC addresses 55 | * `connect AA:BB:CC:DD:EE:FF` Connects to a Nuimo controller with specified MAC address 56 | * `exit` Quits the interactive mode 57 | 58 | ### Installing Nuimo Python SDK 59 | 60 | To install Nuimo module and the Python3 D-Bus dependency globally, run: 61 | 62 | ``` 63 | sudo pip3 install nuimo 64 | sudo apt-get install python3-dbus 65 | ``` 66 | 67 | #### Running the Nuimo control script 68 | 69 | To test if your setup is working, run the following command. Note that it must be run as root because on Linux, Bluetooth discovery is a restricted operation. 70 | 71 | ``` 72 | sudo nuimoctl --discover 73 | sudo nuimoctl --connect AA:BB:CC:DD:EE:FF # Replace the MAC address with your Nuimo's MAC address 74 | sudo nuimoctl --help # To list all available commands 75 | ``` 76 | 77 | ## SDK Usage 78 | 79 | ### Discovering nearby Nuimo controllers 80 | 81 | The SDK entry point is the `ControllerManager` class. Check the following example to dicover any Nuimo controller nearby. 82 | 83 | Please note that communication with your Bluetooth adapter happens over BlueZ's D-Bus API, hence an event loop needs to be run in order to receive all Bluetooth related events. You can start and stop the event loop via `run()` and `stop()` calls to your `ControllerManager` instance. 84 | 85 | 86 | ```python 87 | import nuimo 88 | 89 | class ControllerManagerPrintListener(nuimo.ControllerManagerListener): 90 | def controller_discovered(self, controller): 91 | print("Discovered Nuimo controller", controller.mac_address) 92 | 93 | manager = nuimo.ControllerManager(adapter_name='hci0') 94 | manager.listener = ControllerManagerPrintListener() 95 | manager.start_discovery() 96 | manager.run() 97 | ``` 98 | 99 | ### Connecting to a Nuimo controller and receiving user input events 100 | 101 | Once `ControllerManager` has discovered a Nuimo controller you can use the `controller` object that you retrieved from `ControllerManagerListener.controller_discovered()` to connect to it. Alternatively you can create a new instance of `Controller` using the name of your Bluetooth adapter (typically `hci0`) and Nuimo's MAC address. 102 | 103 | Make sure to assign a `ControllerListener` object to the `listener` attribute of your controller instance. It will notify you about all Nuimo controller related events such connection, disconnection and user input events. 104 | 105 | The following example connects to a Nuimo controller and uses the predefined `ControllerPrintListener` class to print all controller events: 106 | 107 | ```python 108 | import nuimo 109 | 110 | manager = nuimo.ControllerManager(adapter_name='hci0') 111 | 112 | controller = nuimo.Controller(mac_address='AA:BB:CC:DD:EE:FF', manager=manager) 113 | controller.listener = nuimo.ControllerListener() # Use an instance of your own nuimo.ControllerListener subclass 114 | controller.connect() 115 | 116 | manager.run() 117 | ``` 118 | 119 | As with Nuimo controller discovery, remember to start the Bluetooth event loop with `ControllerManager.run()`. 120 | 121 | ### Write to Nuimo's LED matrix 122 | 123 | Once a Nuimo controller is connected you can send an LED matrix to its display. Therefor create an `LedMatrix` object by initializing it with a string. That string should contain 81 characters: each character, starting from top left corner, tells whether the corresponding LED should be on or off. `' '` and `'0'` signal LED off all other characters power the corresponding LED. The following example shows a cross: 124 | 125 | ```python 126 | matrix = nuimo.LedMatrix( 127 | "* *" 128 | " * * " 129 | " * * " 130 | " * * " 131 | " * " 132 | " * * " 133 | " * * " 134 | " * * " 135 | "* *" 136 | ) 137 | controller.display_matrix(matrix) 138 | ``` 139 | 140 | You can pass additional parameters to `display_matrix()` to control the following options: 141 | * `interval: float` # Display interval in seconds, default: `2.0` seconds 142 | * `brightness: float` # LED matrix brightness, default: `1.0` (100%) 143 | * `fading: bool` # Whether to fade the previous matrix into the next one, aka "onion skinning effect", default: `False` 144 | * `ignore_duplicates: bool` # Whether or not send an LED matrix to a Nuimo controller if it's already being displayed, default: `False` 145 | 146 | ## Support 147 | 148 | Please open an issue or drop us an email to [developers@senic.com](mailto:developers@senic.com). 149 | 150 | ## Contributing 151 | 152 | Contributions are welcome via pull requests. Please open an issue first in case you want to discus your possible improvements to this SDK. 153 | 154 | ## License 155 | 156 | The Nuimo Python SDK is available under the MIT License. 157 | -------------------------------------------------------------------------------- /examples/dots.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | from threading import Thread 3 | from time import sleep 4 | 5 | from nuimo import Controller, ControllerManager, ControllerListener, LedMatrix 6 | 7 | 8 | class NuimoListener(ControllerListener): 9 | def __init__(self, controller): 10 | self.controller = controller 11 | 12 | self.stopping = False 13 | self.thread = Thread(target=self.show_dots) 14 | 15 | def connect_succeeded(self): 16 | self.thread.start() 17 | 18 | def show_dots(self): 19 | num_dots = 1 20 | 21 | while not self.stopping: 22 | sleep(0.5) 23 | 24 | s = "{:<81}".format("*" * num_dots) 25 | self.controller.display_matrix(LedMatrix(s), interval=3.0, brightness=1.0, fading=True) 26 | 27 | num_dots += 1 28 | if num_dots > 81: 29 | num_dots = 1 30 | 31 | def stop(self): 32 | self.controller.disconnect() 33 | self.stopping = True 34 | 35 | 36 | def main(mac_address): 37 | manager = ControllerManager(adapter_name="hci0") 38 | controller = Controller(mac_address=mac_address, manager=manager) 39 | listener = NuimoListener(controller) 40 | controller.listener = listener 41 | controller.connect() 42 | 43 | try: 44 | manager.run() 45 | except KeyboardInterrupt: 46 | print("Stopping...") 47 | listener.stop() 48 | manager.stop() 49 | 50 | 51 | if __name__ == "__main__": 52 | if len(argv) > 1: 53 | main(argv[-1]) 54 | else: 55 | print("Usage: {} ".format(argv[0])) 56 | -------------------------------------------------------------------------------- /nuimo/__init__.py: -------------------------------------------------------------------------------- 1 | from .nuimo import ControllerManager, ControllerManagerListener, Controller, ControllerListener, GestureEvent, Gesture, LedMatrix 2 | -------------------------------------------------------------------------------- /nuimo/nuimo.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import gatt 3 | from datetime import datetime 4 | from enum import Enum 5 | import binascii 6 | 7 | 8 | class ControllerManager(gatt.DeviceManager): 9 | """ 10 | Entry point for managing and discovering Nuimo ``Controller``s. 11 | """ 12 | 13 | def __init__(self, adapter_name='hci0'): 14 | """ 15 | Instantiates a ``ControllerManager`` 16 | 17 | :param adapter_name: name of Bluetooth adapter used by this controller manager 18 | """ 19 | super().__init__(adapter_name) 20 | self.listener = None 21 | self.discovered_controllers = {} 22 | 23 | def controllers(self): 24 | """ 25 | Returns all known Nuimo controllers. 26 | """ 27 | return self.devices() 28 | 29 | def start_discovery(self): 30 | """ 31 | Starts a Bluetooth discovery for Nuimo controllers. 32 | 33 | Assign a `ControllerManagerListener` to the `listener` attribute to collect discovered Nuimos. 34 | """ 35 | self._discovered_controllers = {} 36 | super().start_discovery(service_uuids=Controller.SERVICE_UUIDS) 37 | 38 | def make_device(self, mac_address): 39 | device = gatt.Device(mac_address=mac_address, manager=self, managed=False) 40 | if device.alias() != 'Nuimo': 41 | return None 42 | return Controller(mac_address=mac_address, manager=self) 43 | 44 | def device_discovered(self, device): 45 | super().device_discovered(device) 46 | if device.mac_address in self.discovered_controllers: 47 | return 48 | self.discovered_controllers[device.mac_address] = device 49 | if self.listener is not None: 50 | self.listener.controller_discovered(device) 51 | 52 | 53 | class ControllerManagerListener: 54 | """ 55 | Base class for receiving discovery events from ``ControllerManager``. 56 | 57 | Assign an instance of your subclass to the ``listener`` attribute of your 58 | ``ControllerManager`` to receive discovery events. 59 | """ 60 | 61 | def controller_discovered(self, controller): 62 | """ 63 | This method gets called once for each Nuimo controller discovered nearby. 64 | 65 | :param controller: the Nuimo controller that was discovered 66 | """ 67 | pass 68 | 69 | 70 | class Controller(gatt.Device): 71 | """ 72 | This class represents a Nuimo controller. 73 | 74 | Obtain instances of this class by using the discovery mechanism of 75 | ``ControllerManager`` or by manually creating an instance. 76 | 77 | Assign an instance of ``ControllerListener`` to the ``listener`` attribute to 78 | receive all Nuimo related events such as connection, disconnection and user input. 79 | 80 | :ivar adapter_name: name of Bluetooth adapter that can connect to this controller 81 | :ivar mac_address: MAC address of this Nuimo controller 82 | :ivar listener: instance of ``ControllerListener`` that will be notified with all events 83 | """ 84 | 85 | NUIMO_SERVICE_UUID = 'f29b1525-cb19-40f3-be5c-7241ecb82fd2' 86 | BUTTON_CHARACTERISTIC_UUID = 'f29b1529-cb19-40f3-be5c-7241ecb82fd2' 87 | TOUCH_CHARACTERISTIC_UUID = 'f29b1527-cb19-40f3-be5c-7241ecb82fd2' 88 | ROTATION_CHARACTERISTIC_UUID = 'f29b1528-cb19-40f3-be5c-7241ecb82fd2' 89 | FLY_CHARACTERISTIC_UUID = 'f29b1526-cb19-40f3-be5c-7241ecb82fd2' 90 | LED_MATRIX_CHARACTERISTIC_UUID = 'f29b152d-cb19-40f3-be5c-7241ecb82fd2' 91 | BATTERY_CHARACTERISTIC_UUID = '00002a19-0000-1000-8000-00805f9b34fb' 92 | 93 | LEGACY_LED_MATRIX_SERVICE = 'f29b1523-cb19-40f3-be5c-7241ecb82fd1' 94 | LEGACY_LED_MATRIX_CHARACTERISTIC_UUID = 'f29b1524-cb19-40f3-be5c-7241ecb82fd1' 95 | 96 | # TODO: Give services their actual names 97 | UNNAMED1_SERVICE_UUID = '00001801-0000-1000-8000-00805f9b34fb' 98 | UNNAMED2_SERVICE_UUID = '0000180a-0000-1000-8000-00805f9b34fb' 99 | BATTERY_SERVICE_UUID = '0000180f-0000-1000-8000-00805f9b34fb' 100 | 101 | SERVICE_UUIDS = [ 102 | NUIMO_SERVICE_UUID, 103 | LEGACY_LED_MATRIX_SERVICE, 104 | UNNAMED1_SERVICE_UUID, 105 | UNNAMED2_SERVICE_UUID, 106 | BATTERY_SERVICE_UUID] 107 | 108 | def __init__(self, mac_address, manager): 109 | """ 110 | Create an instance with given Bluetooth adapter name and MAC address. 111 | 112 | :param mac_address: MAC address of Nuimo controller with format: ``AA:BB:CC:DD:EE:FF`` 113 | :param manager: reference to the `ControllerManager` that manages this controller 114 | """ 115 | super().__init__(mac_address=mac_address, manager=manager) 116 | 117 | self.listener = None 118 | self._matrix_writer = _LedMatrixWriter(controller=self) 119 | self.battery_level = None 120 | 121 | def connect(self): 122 | """ 123 | Tries to connect to this Nuimo controller and blocks until it has connected 124 | or failed to connect. 125 | 126 | Notifies ``listener`` as soon has the connection has succeeded or failed. 127 | """ 128 | if self.listener: 129 | self.listener.started_connecting() 130 | super().connect() 131 | 132 | def connect_failed(self, error): 133 | super().connect_failed(error) 134 | if self.listener: 135 | self.listener.connect_failed(error) 136 | 137 | def disconnect(self): 138 | """ 139 | Disconnects this Nuimo controller if connected. 140 | 141 | Notifies ``listener`` as soon as Nuimo was disconnected. 142 | """ 143 | if self.listener: 144 | self.listener.started_disconnecting() 145 | super().disconnect() 146 | 147 | def disconnect_succeeded(self): 148 | super().disconnect_succeeded() 149 | if self.listener: 150 | self.listener.disconnect_succeeded() 151 | 152 | def services_resolved(self): 153 | super().services_resolved() 154 | 155 | nuimo_service = next((service for service in self.services if service.uuid == self.NUIMO_SERVICE_UUID), None) 156 | if nuimo_service is None: 157 | if self.listener: 158 | # TODO: Use proper exception subclass 159 | self.listener.connect_failed(Exception("Nuimo GATT service missing")) 160 | return 161 | 162 | self._matrix_writer.led_matrix_characteristic = next(( 163 | characteristic for characteristic in nuimo_service.characteristics 164 | if characteristic.uuid == self.LED_MATRIX_CHARACTERISTIC_UUID), None) 165 | # TODO: Fallback to legacy led matrix service 166 | # This is needed for older Nuimo firmware were the LED characteristic was a separate service) 167 | 168 | notification_characteristic_uuids = [ 169 | self.BUTTON_CHARACTERISTIC_UUID, 170 | self.TOUCH_CHARACTERISTIC_UUID, 171 | self.ROTATION_CHARACTERISTIC_UUID, 172 | self.FLY_CHARACTERISTIC_UUID 173 | ] 174 | 175 | for characteristic_uuid in notification_characteristic_uuids: 176 | characteristic = next(( 177 | characteristic for characteristic in nuimo_service.characteristics 178 | if characteristic.uuid == characteristic_uuid), None) 179 | if characteristic is None: 180 | # TODO: Use proper exception subclass 181 | self.listener.connect_failed(Exception("Nuimo GATT characteristic " + characteristic_uuid + " missing")) 182 | return 183 | characteristic.enable_notifications() 184 | 185 | battery_service = next((service for service in self.services if service.uuid == self.BATTERY_SERVICE_UUID), None) 186 | if battery_service is None: 187 | if self.listener: 188 | # TODO: Use proper exception subclass 189 | self.listener.connect_failed(Exception("Nuimo GATT service missing")) 190 | return 191 | 192 | battery_characteristic = next(( 193 | characteristic for characteristic in battery_service.characteristics 194 | if characteristic.uuid == self.BATTERY_CHARACTERISTIC_UUID), None) 195 | if battery_characteristic is None: 196 | # TODO: Use proper exception subclass 197 | self.listener.connect_failed(Exception("Nuimo GATT characteristic " + self.BATTERY_CHARACTERISTIC_UUID + " missing")) 198 | return 199 | battery_characteristic.read_value() 200 | battery_characteristic.enable_notifications() 201 | # TODO: Only fire connected event when we read the firmware version or battery value as in other SDKs 202 | if self.listener: 203 | self.listener.connect_succeeded() 204 | 205 | def read_battery_level(self): 206 | return self.battery_level 207 | 208 | def display_matrix(self, matrix, interval=2.0, brightness=1.0, fading=False, ignore_duplicates=False): 209 | """ 210 | Displays an LED matrix on Nuimo's LED matrix display. 211 | 212 | :param matrix: the matrix to display 213 | :param interval: interval in seconds until the matrix disappears again 214 | :param brightness: led brightness between 0..1 215 | :param fading: if True, the previous matrix fades into the new matrix 216 | :param ignore_duplicates: if True, the matrix is not sent again if already being displayed 217 | """ 218 | self._matrix_writer.write( 219 | matrix=matrix, 220 | interval=interval, 221 | brightness=brightness, 222 | fading=fading, 223 | ignore_duplicates=ignore_duplicates 224 | ) 225 | 226 | def characteristic_value_updated(self, characteristic, value): 227 | { 228 | self.BUTTON_CHARACTERISTIC_UUID: self._notify_button_event, 229 | self.TOUCH_CHARACTERISTIC_UUID: self._notify_touch_event, 230 | self.ROTATION_CHARACTERISTIC_UUID: self._notify_rotation_event, 231 | self.FLY_CHARACTERISTIC_UUID: self._notify_fly_event, 232 | self.BATTERY_CHARACTERISTIC_UUID: self._update_battery_level 233 | }[characteristic.uuid](value) 234 | 235 | def characteristic_write_value_succeeded(self, characteristic): 236 | if characteristic.uuid == self.LED_MATRIX_CHARACTERISTIC_UUID: 237 | self._matrix_writer.write_succeeded() 238 | 239 | def characteristic_write_value_failed(self, characteristic, error): 240 | if characteristic.uuid == self.LED_MATRIX_CHARACTERISTIC_UUID: 241 | self._matrix_writer.write_failed(error) 242 | 243 | def _notify_button_event(self, value): 244 | self._notify_gesture_event(gesture=Gesture.BUTTON_RELEASE if value[0] == 0 else Gesture.BUTTON_PRESS) 245 | 246 | def _notify_touch_event(self, value): 247 | gesture = { 248 | 0: Gesture.SWIPE_LEFT, 249 | 1: Gesture.SWIPE_RIGHT, 250 | 2: Gesture.SWIPE_UP, 251 | 3: Gesture.SWIPE_DOWN, 252 | 4: Gesture.TOUCH_LEFT, 253 | 5: Gesture.TOUCH_RIGHT, 254 | 6: Gesture.TOUCH_TOP, 255 | 7: Gesture.TOUCH_BOTTOM, 256 | 8: Gesture.LONGTOUCH_LEFT, 257 | 9: Gesture.LONGTOUCH_RIGHT, 258 | 10: Gesture.LONGTOUCH_TOP, 259 | 11: Gesture.LONGTOUCH_BOTTOM 260 | }.get(value[0]) 261 | if gesture is not None: 262 | self._notify_gesture_event(gesture=gesture) 263 | 264 | def _notify_rotation_event(self, value): 265 | rotation_value = value[0] + (value[1] << 8) 266 | if (value[1] >> 7) > 0: 267 | rotation_value -= 1 << 16 268 | self._notify_gesture_event(gesture=Gesture.ROTATION, value=rotation_value) 269 | 270 | def _notify_fly_event(self, value): 271 | if value[0] == 0: 272 | self._notify_gesture_event(gesture=Gesture.FLY_LEFT) 273 | elif value[0] == 1: 274 | self._notify_gesture_event(gesture=Gesture.FLY_RIGHT) 275 | elif value[0] == 4: 276 | self._notify_gesture_event(gesture=Gesture.FLY_UPDOWN, value=value[1]) 277 | 278 | def _notify_gesture_event(self, gesture, value=None): 279 | if self.listener: 280 | self.listener.received_gesture_event(GestureEvent(gesture=gesture, value=value)) 281 | 282 | def _update_battery_level(self, value): 283 | self.battery_level = int(binascii.hexlify(value), 16) 284 | self._notify_gesture_event(gesture=Gesture.BATTERY_LEVEL, value=self.battery_level) 285 | 286 | 287 | class _LedMatrixWriter(): 288 | def __init__(self, controller): 289 | self.controller = controller 290 | self.led_matrix_characteristic = None 291 | 292 | self.last_written_matrix = None 293 | self.last_written_matrix_interval = 0 294 | self.last_written_matrix_date = datetime.utcfromtimestamp(0) 295 | self.matrix = None 296 | self.interval = 0 297 | self.brightness = 0 298 | self.fading = False 299 | self.is_waiting_for_response = False 300 | self.write_on_response = False 301 | 302 | def write(self, matrix, interval, brightness, fading, ignore_duplicates): 303 | if (ignore_duplicates and 304 | (self.last_written_matrix is not None) and 305 | (self.last_written_matrix == matrix) and 306 | ((self.last_written_matrix_interval <= 0) or 307 | (datetime.now() - self.last_written_matrix_date).total_seconds() < self.last_written_matrix_interval)): 308 | return 309 | 310 | self.matrix = matrix 311 | self.interval = interval 312 | self.brightness = brightness 313 | self.fading = fading 314 | 315 | if (self.is_waiting_for_response and 316 | (datetime.now() - self.last_written_matrix_date).total_seconds() < 1.0): 317 | self.write_on_response = True 318 | else: 319 | self.write_now() 320 | 321 | def write_now(self): 322 | if not self.controller.is_connected() or self.led_matrix_characteristic is None: 323 | return 324 | 325 | matrix_bytes = list( 326 | map(lambda leds: functools.reduce( 327 | lambda acc, led: acc + (1 << led if leds[led] else 0), range(0, len(leds)), 0), 328 | [self.matrix.leds[i:i + 8] for i in range(0, 81, 8)])) 329 | 330 | matrix_bytes += [ 331 | max(0, min(255, int(self.brightness * 255.0))), 332 | max(0, min(255, int(self.interval * 10.0)))] 333 | 334 | if self.fading: 335 | matrix_bytes[10] ^= 1 << 4 336 | 337 | # TODO: Support write requests without response 338 | # bluetoothd probably doesn't support selecting the request mode 339 | self.is_waiting_for_response = True 340 | self.led_matrix_characteristic.write_value(matrix_bytes) 341 | 342 | self.last_written_matrix = self.matrix 343 | self.last_written_matrix_date = datetime.now() 344 | self.last_written_matrix_interval = self.interval 345 | 346 | def write_succeeded(self): 347 | self.is_waiting_for_response = False 348 | if self.write_on_response: 349 | self.write_on_response = False 350 | self.write_now() 351 | 352 | def write_failed(self, error): 353 | self.is_waiting_for_response = False 354 | 355 | 356 | class ControllerListener: 357 | """ 358 | Base class of listeners for a ``NuimoController`` with empty handler implementations. 359 | """ 360 | def received_gesture_event(self, event): 361 | pass 362 | 363 | def started_connecting(self): 364 | pass 365 | 366 | def connect_succeeded(self): 367 | pass 368 | 369 | def connect_failed(self, error): 370 | pass 371 | 372 | def started_disconnecting(self): 373 | pass 374 | 375 | def disconnect_succeeded(self): 376 | pass 377 | 378 | 379 | class GestureEvent: 380 | """ 381 | A gesture event as it can be received from a Nuimo controller. 382 | 383 | :ivar gesture: gesture that was performed 384 | :ivar value: value associated with the gesture, i.e. number of rotation steps 385 | """ 386 | def __init__(self, gesture, value=None): 387 | self.gesture = gesture 388 | self.value = value 389 | 390 | def __repr__(self): 391 | return str(self.gesture) + (("," + str(self.value)) if self.value is not None else "") 392 | 393 | 394 | class Gesture(Enum): 395 | """ 396 | A gesture that can be performed by the user on a Nuimo controller. 397 | """ 398 | BUTTON_PRESS = 1 399 | BUTTON_RELEASE = 2 400 | SWIPE_LEFT = 3 401 | SWIPE_RIGHT = 4 402 | SWIPE_UP = 5 403 | SWIPE_DOWN = 6 404 | TOUCH_LEFT = 8, 405 | TOUCH_RIGHT = 9, 406 | TOUCH_TOP = 10, 407 | TOUCH_BOTTOM = 11, 408 | LONGTOUCH_LEFT = 12 409 | LONGTOUCH_RIGHT = 13 410 | LONGTOUCH_TOP = 14, 411 | LONGTOUCH_BOTTOM = 15, 412 | ROTATION = 16, 413 | FLY_LEFT = 17, 414 | FLY_RIGHT = 18, 415 | FLY_UPDOWN = 19, 416 | BATTERY_LEVEL = 20 417 | 418 | 419 | class LedMatrix: 420 | """ 421 | Represents an LED matrix to be displayed on a Nuimo controller. 422 | 423 | :ivar leds: Boolean array with 81 values each representing the LEDs being on or off. 424 | """ 425 | def __init__(self, string): 426 | """ 427 | Initializes an LED matrix with a string where each character represents one LED. 428 | 429 | :param string: 81 character string: ' ' and '0' represent LED off, all other characters represent LED on. 430 | """ 431 | string = '{:<81}'.format(string[:81]) 432 | self.leds = [c not in [' ', '0'] for c in string] 433 | 434 | def __eq__(self, other): 435 | return (other is not None) and (self.leds == other.leds) 436 | 437 | def __ne__(self, other): 438 | return not (self == other) 439 | -------------------------------------------------------------------------------- /nuimoctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from argparse import ArgumentParser 5 | import nuimo 6 | 7 | controller_manager = None 8 | 9 | 10 | class ControllerPrintListener(nuimo.ControllerListener): 11 | """ 12 | An implementation of ``ControllerListener`` that prints each event. 13 | """ 14 | def __init__(self, controller): 15 | self.controller = controller 16 | 17 | def started_connecting(self): 18 | self.print("connecting...") 19 | 20 | def connect_succeeded(self): 21 | self.print("connected") 22 | 23 | def connect_failed(self, error): 24 | self.print("connect failed: " + str(error)) 25 | 26 | def started_disconnecting(self): 27 | self.print("disconnecting...") 28 | 29 | def disconnect_succeeded(self): 30 | self.print("disconnected") 31 | 32 | def received_gesture_event(self, event): 33 | self.print("did send gesture event " + str(event)) 34 | 35 | def print(self, string): 36 | print("Nuimo controller " + self.controller.mac_address + " " + string) 37 | 38 | 39 | class ControllerTestListener(ControllerPrintListener): 40 | def __init__(self, controller, auto_reconnect=False): 41 | super().__init__(controller) 42 | self.auto_reconnect = auto_reconnect 43 | 44 | def connect_failed(self, error): 45 | super().connect_failed(error) 46 | controller_manager.stop() 47 | sys.exit(0) 48 | 49 | def disconnect_succeeded(self): 50 | super().disconnect_succeeded() 51 | 52 | if self.auto_reconnect: 53 | # Reconnect as soon as Nuimo was disconnected 54 | print("Disconnected, reconnecting...") 55 | self.controller.connect() 56 | else: 57 | controller_manager.stop() 58 | sys.exit(0) 59 | 60 | def received_gesture_event(self, event): 61 | super().received_gesture_event(event) 62 | self.controller.display_matrix(nuimo.LedMatrix( 63 | "* " 64 | " * " 65 | " * " 66 | " * " 67 | " * " 68 | " * " 69 | " * " 70 | " * " 71 | " *")) 72 | 73 | 74 | class ControllerManagerPrintListener(nuimo.ControllerManagerListener): 75 | def controller_discovered(self, controller): 76 | print("Discovered Nuimo controller", controller.mac_address) 77 | 78 | 79 | def main(): 80 | arg_parser = ArgumentParser(description="Nuimo Controller Demo") 81 | arg_parser.add_argument( 82 | '--adapter', 83 | default='hci0', 84 | help="Name of Bluetooth adapter, defaults to 'hci0'") 85 | arg_commands_group = arg_parser.add_mutually_exclusive_group(required=True) 86 | arg_commands_group.add_argument( 87 | '--discover', 88 | action='store_true', 89 | help="Lists all nearby Nuimo controllers") 90 | arg_commands_group.add_argument( 91 | '--known', 92 | action='store_true', 93 | help="Lists all known Nuimo controllers") 94 | arg_commands_group.add_argument( 95 | '--connect', 96 | metavar='address', 97 | type=str, 98 | help="Connect to a Nuimo controller with a given MAC address") 99 | arg_commands_group.add_argument( 100 | '--auto', 101 | metavar='address', 102 | type=str, 103 | help="Connect and automatically reconnect to a Nuimo controller with a given MAC address") 104 | arg_commands_group.add_argument( 105 | '--disconnect', 106 | metavar='address', 107 | type=str, 108 | help="Disconnect a Nuimo controller with a given MAC address") 109 | args = arg_parser.parse_args() 110 | 111 | global controller_manager 112 | controller_manager = nuimo.ControllerManager(adapter_name=args.adapter) 113 | 114 | if args.discover: 115 | controller_manager.listener = ControllerManagerPrintListener() 116 | controller_manager.start_discovery() 117 | if args.known: 118 | for controller in controller_manager.controllers(): 119 | print("[%s] %s" % (controller.mac_address, controller.alias())) 120 | return 121 | elif args.connect: 122 | controller = nuimo.Controller(mac_address=args.connect, manager=controller_manager) 123 | controller.listener = ControllerTestListener(controller=controller) 124 | controller.connect() 125 | elif args.auto: 126 | controller = nuimo.Controller(mac_address=args.auto, manager=controller_manager) 127 | controller.listener = ControllerTestListener(controller=controller, auto_reconnect=True) 128 | controller.connect() 129 | elif args.disconnect: 130 | controller = nuimo.Controller(mac_address=args.disconnect, manager=controller_manager) 131 | controller.disconnect() 132 | return 133 | 134 | print("Terminate with Ctrl+C") 135 | try: 136 | controller_manager.run() 137 | except KeyboardInterrupt: 138 | pass 139 | 140 | if __name__ == '__main__': 141 | main() 142 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='nuimo', 5 | packages=['nuimo'], 6 | install_requires=['gatt>=0.2.4'], 7 | version='0.3.6', 8 | description='Nuimo SDK for Python on Linux', 9 | keywords='nuimo', 10 | url='https://github.com/getsenic/nuimo-linux-python', 11 | download_url='https://github.com/getsenic/nuimo-linux-python/archive/0.3.6.tar.gz', 12 | author='Senic GmbH', 13 | author_email='developers@senic.com', 14 | license='MIT', 15 | py_modules=['nuimoctl'], 16 | entry_points={ 17 | 'console_scripts': ['nuimoctl = nuimoctl:main'] 18 | } 19 | ) 20 | --------------------------------------------------------------------------------