diff --git a/bricknil/bleak.py b/bricknil/bleak.py deleted file mode 100644 index bb7229b..0000000 --- a/bricknil/bleak.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2019 Virantha N. Ekanayake -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Interface to the BLEAK library in Linux for BLE calls - -""" -import curio, asyncio, threading, logging - -import bleak -from bleak import BleakClient - -class Bleak: - """Interface class between curio loop and asyncio loop running bleak - - This class is basically just a queue interface. It has two queue, - one for incoming messages `in_queue` and one for outgoing messages `out_queue`. - - A loop running in asyncio's event_loop waits for messages on the `in_queue`. - - The `out_queue` is used to respond to "discover" and "connect" messages with the - list of discovered devices and a connected device respectively. All messages - incoming from a device are relayed directly to a call back function, and does - not go through either of these queues. - - - """ - - def __init__(self): - # Need to start an event loop - self.in_queue = curio.UniversalQueue() # Incoming message queue - self.out_queue = curio.UniversalQueue() # Outgoing message queue - - self.devices = [] - #self.loop = threading.Thread(target=self.run, daemon=True) - #self.loop.start() - - def run(self): - #self.loop = asyncio.new_event_loop() - #asyncio.set_event_loop(self.loop) - self.loop = asyncio.get_event_loop() - self.loop.run_until_complete(self.asyncio_loop()) - - async def asyncio_loop(self): - - # Wait for messages on in_queue - done = False - while not done: - msg = await self.in_queue.get() - if isinstance(msg, tuple): - msg, val = msg - await self.in_queue.task_done() - if msg == 'discover': - print('Awaiting on bleak discover') - devices = await bleak.discover(timeout=1, loop=self.loop) - print('Done Awaiting on bleak discover') - await self.out_queue.put(devices) - elif msg == 'connect': - device = BleakClient(address=val, loop=self.loop) - self.devices.append(device) - await device.connect() - await self.out_queue.put(device) - elif msg == 'tx': - device, char_uuid, msg_bytes = val - await device.write_gatt_char(char_uuid, msg_bytes) - elif msg == 'notify': - device, char_uuid, msg_handler = val - await device.start_notify(char_uuid, msg_handler) - elif msg =='quit': - logging.info('quitting') - for device in self.devices: - await device.disconnect() - done = True - else: - print(f'Unknown message to Bleak: {msg}') - - - - diff --git a/bricknil/bricknil.py b/bricknil/bricknil.py index 7ea0d4f..459c536 100644 --- a/bricknil/bricknil.py +++ b/bricknil/bricknil.py @@ -34,8 +34,8 @@ from .const import USE_BLEAK from .sockets import bricknil_socket_server -if USE_BLEAK: - from .bleak import Bleak +#if USE_BLEAK: + #from .bleak_interface import Bleak import threading @@ -188,9 +188,9 @@ def _curio_event_run(ble, system): system : Coroutine that the user provided to instantate their system """ - run(_run_all(ble, system), with_monitor=True) + run(_run_all(ble, system), with_monitor=False) -def start(user_system_setup_func): +def start(user_system_setup_func): #pragma: no cover """ Main entry point into running everything. @@ -204,6 +204,7 @@ def start(user_system_setup_func): """ if USE_BLEAK: + from .bleak_interface import Bleak ble = Bleak() # Run curio in a thread curry_curio_event_run = partial(_curio_event_run, ble=ble, system=user_system_setup_func) diff --git a/bricknil/sockets.py b/bricknil/sockets.py index 46c46d1..79ad982 100644 --- a/bricknil/sockets.py +++ b/bricknil/sockets.py @@ -30,7 +30,7 @@ #wc = WebClient(client, addr, web_out_queue) #await spawn(wc.run, daemon=True) -async def bricknil_socket_server(web_out_queue, address): +async def bricknil_socket_server(web_out_queue, address): #pragma: no cover """Listen for client connections on port 25000 and spawn `WebClient` instance. This fuction is spawned as a task during system instantiation @@ -44,7 +44,7 @@ async def web_client_connected(client, addr): task = await spawn(tcp_server, '', 25000, web_client_connected, daemon=True) -class WebClient: +class WebClient: #pragma: no cover """ Represents a client that has connected to BrickNil's server Each client has a connection to the global BrickNil `curio.Queue` diff --git a/test/test_hub.py b/test/test_hub.py index a5b50ee..6098336 100644 --- a/test/test_hub.py +++ b/test/test_hub.py @@ -1,8 +1,10 @@ import pytest -import os, struct, copy -import logging +import os, struct, copy, sys +from functools import partial +import logging, threading from asyncio import coroutine from curio import kernel, sleep, spawn, Event +import time from mock import Mock from mock import patch, call, create_autospec @@ -17,7 +19,7 @@ from bricknil.sensor import * from bricknil.const import DEVICES from bricknil import attach, start -from bricknil.hub import PoweredUpHub, Hub, BoostHub, DuploTrainHub +from bricknil.hub import PoweredUpHub, Hub, BoostHub, DuploTrainHub, PoweredUpRemote import bricknil import bricknil.const @@ -39,7 +41,7 @@ def setup(self): DuploVisionSensor, VoltageSensor, ] - self.hub_list = [ PoweredUpHub, BoostHub, DuploTrainHub] + self.hub_list = [ PoweredUpHub, BoostHub, DuploTrainHub, PoweredUpRemote] def _with_header(self, msg:bytearray): l = len(msg)+2 @@ -52,6 +54,7 @@ def _draw_capabilities(self, data, sensor): # or some combination of those in the allowed_combo list capabilities = data.draw( st.one_of( + st.lists(st.sampled_from([cap.name for cap in list(sensor.capability)]), min_size=1, max_size=1), st.lists(st.sampled_from(sensor.capability), min_size=1, max_size=1), st.lists(st.sampled_from(sensor.allowed_combo), min_size=1, unique=True) ) @@ -162,6 +165,93 @@ async def dummy(): await hub_stop_evt.set() await system.join() + @given(data = st.data()) + def test_run_hub_with_bleak(self, data): + + Hub.hubs = [] + sensor_name = 'sensor' + sensor = data.draw(st.sampled_from(self.sensor_list)) + capabilities = self._draw_capabilities(data, sensor) + + hub_type = data.draw(st.sampled_from(self.hub_list)) + TestHub, stop_evt = self._get_hub_class(hub_type, sensor, sensor_name, capabilities) + hub = TestHub('test_hub') + + async def dummy(): + pass + # Start the hub + #MockBleak = MagicMock() + sys.modules['bleak'] = MockBleak(hub) + with patch('bricknil.bricknil.USE_BLEAK', True), \ + patch('bricknil.ble_queue.USE_BLEAK', True) as use_bleak: + sensor_obj = getattr(hub, sensor_name) + sensor_obj.send_message = Mock(side_effect=coroutine(lambda x,y: "the awaitable should return this")) + from bricknil.bleak_interface import Bleak + ble = Bleak() + # Run curio in a thread + async def dummy(): pass + + async def start_curio(): + system = await spawn(bricknil.bricknil._run_all(ble, dummy)) + while len(ble.devices) < 1 or not ble.devices[0].notify: + await sleep(0.01) + await stop_evt.set() + print("sending quit") + await ble.in_queue.put( ('quit', '')) + #await system.join() + print('system joined') + + def start_thread(): + kernel.run(start_curio) + + t = threading.Thread(target=start_thread) + t.start() + print('started thread for curio') + ble.run() + t.join() + + + + +class MockBleak(MagicMock): + def __init__(self, hub): + MockBleak.hub = hub + pass + @classmethod + async def discover(cls, timeout, loop): + # Need to return devices here, which is a list of device tuples + hub = MockBleak.hub + devices = [MockBleakDevice(hub.uart_uuid, hub.manufacturer_id)] + return devices + + @classmethod + def BleakClient(cls, address, loop): + print("starting BleakClient") + hub = MockBleak.hub + device = MockBleakDevice(hub.uart_uuid, hub.manufacturer_id) + return device + +class MockBleakDevice: + def __init__(self, uuid, manufacturer_id): + self.uuids = [str(uuid)] + self.manufacturer_data = {'values': [0, manufacturer_id] } + self.name = "" + self.address = "XX:XX:XX:XX:XX" + self.notify = False + + async def connect(self): + self.characteristics = MockBleak.hub.char_uuid + pass + async def write_gatt_char(self, char_uuid, msg_bytes): + print(f'Got msg on {char_uuid}: {msg_bytes}') + + async def start_notify(self, char_uuid, handler): + print("started notify") + self.notify = True + + async def disconnect(self): + print("device disconnected") + class MockBLE: def __init__(self, hub): self.hub = hub diff --git a/tox.ini b/tox.ini index 7d3ae48..8fd7009 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py37 +envlist=py37,py36 [testenv] changedir=test