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

Merge branch 'master' of github.com:undera/pylgbst

This commit is contained in:
Andrey Pohilko 2017-09-15 17:57:06 +03:00
commit 4e8708453a
5 changed files with 73 additions and 47 deletions

View File

@ -108,6 +108,7 @@ class MoveHub(object):
self.devices[port].finished() self.devices[port].finished()
elif status == STATUS_CONFLICT: elif status == STATUS_CONFLICT:
log.warning("Command conflict on port %s", PORTS[port]) log.warning("Command conflict on port %s", PORTS[port])
self.devices[port].finished()
else: else:
log.warning("Unhandled status value: 0x%x", status) log.warning("Unhandled status value: 0x%x", status)

View File

@ -1,13 +1,16 @@
""" """
This package holds communication aspects This package holds communication aspects
""" """
import binascii
import json import json
import logging import logging
import socket import socket
import sys import sys
import time import time
import traceback import traceback
from Queue import Queue
from abc import abstractmethod from abc import abstractmethod
from binascii import unhexlify
from gattlib import DiscoveryService, GATTRequester from gattlib import DiscoveryService, GATTRequester
from threading import Thread from threading import Thread
@ -15,29 +18,15 @@ from pylgbst.constants import LEGO_MOVE_HUB, MSG_DEVICE_SHUTDOWN
log = logging.getLogger('transport') log = logging.getLogger('transport')
if sys.version_info[0] == 2:
def str2hex(data):
return data.encode("hex")
def str2hex(data): # TODO: eliminate it
def hex2str(data):
return data.decode("hex")
def get_byte(seq, index):
return ord(seq[index])
else:
import binascii
def str2hex(data):
return binascii.hexlify(data).decode("utf8") return binascii.hexlify(data).decode("utf8")
def hex2str(data): if sys.version_info[0] == 2:
return binascii.unhexlify(data) def get_byte(seq, index):
return ord(seq[index])
else:
def get_byte(seq, index): def get_byte(seq, index):
return seq[index] return seq[index]
@ -53,14 +42,31 @@ class Requester(GATTRequester):
super(Requester, self).__init__(p_object, *args, **kwargs) super(Requester, self).__init__(p_object, *args, **kwargs)
self.notification_sink = None self.notification_sink = None
self._notify_queue = Queue()
self._notifier_thread = Thread(target=self._dispatch_notifications)
self._notifier_thread.setDaemon(True)
self._notifier_thread.setName("Notify queue dispatcher")
self._notifier_thread.start()
def on_notification(self, handle, data): def on_notification(self, handle, data):
# log.debug("requester notified, sink: %s", self.notification_sink) # log.debug("requester notified, sink: %s", self.notification_sink)
if self.notification_sink: self._notify_queue.put((handle, data))
self.notification_sink(handle, data)
def on_indication(self, handle, data): def on_indication(self, handle, data):
log.debug("Indication on handle %s: %s", handle, str2hex(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("Failed to dispatch notification: %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):
@abstractmethod @abstractmethod
@ -81,17 +87,17 @@ class BLEConnection(Connection):
Main transport class, uses real Bluetooth LE connection. Main transport class, uses real Bluetooth LE connection.
Loops with timeout of 1 seconds to find device named "Lego MOVE Hub" Loops with timeout of 1 seconds to find device named "Lego MOVE Hub"
:type requester: Requester :type _requester: Requester
""" """
def __init__(self): def __init__(self):
super(BLEConnection, self).__init__() super(BLEConnection, self).__init__()
self.requester = None self._requester = None
def connect(self, bt_iface_name='hci0'): def connect(self, bt_iface_name='hci0'):
service = DiscoveryService(bt_iface_name) service = DiscoveryService(bt_iface_name)
while not self.requester: while not self._requester:
log.info("Discovering devices using %s...", bt_iface_name) log.info("Discovering devices using %s...", bt_iface_name)
devices = service.discover(1) devices = service.discover(1)
log.debug("Devices: %s", devices) log.debug("Devices: %s", devices)
@ -99,25 +105,22 @@ class BLEConnection(Connection):
for address, name in devices.items(): for address, name in devices.items():
if name == LEGO_MOVE_HUB: if name == LEGO_MOVE_HUB:
logging.info("Found %s at %s", name, address) logging.info("Found %s at %s", name, address)
self._get_requester(address, bt_iface_name) self._requester = Requester(address, True, bt_iface_name)
break break
return self return self
def _get_requester(self, address, bt_iface_name):
self.requester = Requester(address, True, bt_iface_name)
def set_notify_handler(self, handler): def set_notify_handler(self, handler):
if self.requester: if self._requester:
log.debug("Setting notification handler: %s", handler) log.debug("Setting notification handler: %s", handler)
self.requester.notification_sink = handler self._requester.notification_sink = handler
else: else:
raise RuntimeError("No requester available") raise RuntimeError("No requester available")
def read(self, handle): def read(self, handle):
# FIXME: repeating reads hangs it... # FIXME: repeating reads hangs it...
log.debug("Reading from: %s", handle) log.debug("Reading from: %s", handle)
data = self.requester.read_by_handle(handle) data = self._requester.read_by_handle(handle)
log.debug("Result: %s", data) log.debug("Result: %s", data)
if isinstance(data, list): if isinstance(data, list):
data = data[0] data = data[0]
@ -125,7 +128,7 @@ class BLEConnection(Connection):
def write(self, handle, data): def write(self, handle, data):
log.debug("Writing to %s: %s", handle, str2hex(data)) log.debug("Writing to %s: %s", handle, str2hex(data))
return self.requester.write_by_handle(handle, data) return self._requester.write_by_handle(handle, data)
class DebugServer(object): class DebugServer(object):
@ -152,7 +155,7 @@ class DebugServer(object):
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.ble.requester.notification_sink = 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:
@ -160,14 +163,14 @@ 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.ble.requester.notification_sink = self._notify_dummy self.ble._requester.notification_sink = self._notify_dummy
conn.close() conn.close()
def __del__(self): def __del__(self):
self.sock.close() self.sock.close()
def _notify_dummy(self, handle, data): def _notify_dummy(self, handle, data):
log.debug("Notification from handle %s: %s", handle, hex2str(data)) log.debug("Notification from handle %s: %s", handle, hexlify(data.strip()))
self._check_shutdown(data) self._check_shutdown(data)
def _notify(self, conn, handle, data): def _notify(self, conn, handle, data):
@ -215,7 +218,7 @@ class DebugServer(object):
def _handle_cmd(self, cmd): def _handle_cmd(self, cmd):
if cmd['type'] == 'write': if cmd['type'] == 'write':
self.ble.write(cmd['handle'], hex2str(cmd['data'])) self.ble.write(cmd['handle'], unhexlify(cmd['data']))
elif cmd['type'] == 'read': elif cmd['type'] == 'read':
data = self.ble.read(cmd['handle']) data = self.ble.read(cmd['handle'])
payload = {"type": "response", "data": str2hex(data)} payload = {"type": "response", "data": str2hex(data)}
@ -239,6 +242,7 @@ class DebugServerConnection(Connection):
self.incoming = [] self.incoming = []
self.reader = Thread(target=self._recv) self.reader = Thread(target=self._recv)
self.reader.setName("Debug connection reader")
self.reader.setDaemon(True) self.reader.setDaemon(True)
self.reader.start() self.reader.start()
@ -264,7 +268,7 @@ class DebugServerConnection(Connection):
for item in self.incoming: for item in self.incoming:
if item['type'] == 'response': if item['type'] == 'response':
self.incoming.remove(item) self.incoming.remove(item)
return hex2str(item['data']) return unhexlify(item['data'])
time.sleep(0.1) time.sleep(0.1)
def _send(self, payload): def _send(self, payload):
@ -276,7 +280,7 @@ class DebugServerConnection(Connection):
data = self.sock.recv(1024) data = self.sock.recv(1024)
log.debug("Recv from debug server: %s", data.strip()) log.debug("Recv from debug server: %s", data.strip())
if not data: if not data:
break raise KeyboardInterrupt("Server has closed connection")
self.buf += data self.buf += data
@ -286,7 +290,7 @@ class DebugServerConnection(Connection):
if line: if line:
item = json.loads(line) item = json.loads(line)
if item['type'] == 'notification' and self.notify_handler: if item['type'] == 'notification' and self.notify_handler:
self.notify_handler(item['handle'], hex2str(item['data'])) self.notify_handler(item['handle'], unhexlify(item['data']))
elif item['type'] == 'response': elif item['type'] == 'response':
self.incoming.append(item) self.incoming.append(item)
else: else:
@ -294,3 +298,10 @@ class DebugServerConnection(Connection):
def set_notify_handler(self, handler): def set_notify_handler(self, handler):
self.notify_handler = handler self.notify_handler = handler
def start_debug_server(iface="hci0", port=9090):
ble = BLEConnection()
ble.connect(iface)
server = DebugServer(ble)
server.start(port)

View File

@ -126,7 +126,6 @@ class EncodedMotor(Peripheral):
command += self.TRAILER command += self.TRAILER
self._working = -1
self._write_to_hub(MSG_SET_PORT_VAL, command) self._write_to_hub(MSG_SET_PORT_VAL, command)
def timed(self, seconds, speed_primary=1, speed_secondary=None, async=False): def timed(self, seconds, speed_primary=1, speed_secondary=None, async=False):
@ -141,6 +140,7 @@ class EncodedMotor(Peripheral):
raise ValueError("Too large value for seconds: %s", seconds) raise ValueError("Too large value for seconds: %s", seconds)
command += pack('<H', msec) command += pack('<H', msec)
self._working = -1
self._wrap_and_write(command, speed_primary, speed_secondary) self._wrap_and_write(command, speed_primary, speed_secondary)
self.__wait_sync(async) self.__wait_sync(async)
@ -158,6 +158,7 @@ class EncodedMotor(Peripheral):
# angle # angle
command += pack('<I', angle) command += pack('<I', angle)
self._working = -1
self._wrap_and_write(command, speed_primary, speed_secondary) self._wrap_and_write(command, speed_primary, speed_secondary)
self.__wait_sync(async) self.__wait_sync(async)
@ -165,7 +166,9 @@ class EncodedMotor(Peripheral):
if not async: if not async:
log.debug("Waiting for sync command work to finish...") log.debug("Waiting for sync command work to finish...")
while self.is_working(): while self.is_working():
time.sleep(0.05) log.debug("Waiting")
time.sleep(0.1)
log.debug("Command has finished.")
# TODO: how to tell when motor has stopped? # TODO: how to tell when motor has stopped?

View File

@ -1,4 +1,5 @@
import unittest import unittest
from binascii import unhexlify
from pylgbst import * from pylgbst import *
@ -33,7 +34,7 @@ class ConnectionMock(Connection):
if self.notification_handler: if self.notification_handler:
while self.notifications: while self.notifications:
handle, data = self.notifications.pop(0) handle, data = self.notifications.pop(0)
self.notification_handler(handle, hex2str(data.replace(' ', ''))) self.notification_handler(handle, unhexlify(data.replace(' ', '')))
time.sleep(0.1) time.sleep(0.1)
self.finished = True self.finished = True
@ -69,7 +70,7 @@ class GeneralTest(unittest.TestCase):
hub = HubMock() hub = HubMock()
led = LED(hub, PORT_LED) led = LED(hub, PORT_LED)
led.set_color(COLOR_RED) led.set_color(COLOR_RED)
self.assertEqual("0801813211510009", hub.connection.writes[0][1]) self.assertEqual("0801813201510009", hub.connection.writes[0][1])
def test_tilt_sensor(self): def test_tilt_sensor(self):
hub = HubMock() hub = HubMock()
@ -103,10 +104,14 @@ class GeneralTest(unittest.TestCase):
conn = ConnectionMock() conn = ConnectionMock()
conn.notifications.append((14, '1b0e00 0900 04 39 0227003738')) conn.notifications.append((14, '1b0e00 0900 04 39 0227003738'))
hub = HubMock(conn) hub = HubMock(conn)
motor = EncodedMotor(hub, PORT_AB) time.sleep(0.1)
motor.timed(1.5)
conn.notifications.append((14, '1b0e00050082390a'))
hub.motor_AB.timed(1.5)
self.assertEqual("0d018139110adc056464647f03", conn.writes[0][1]) self.assertEqual("0d018139110adc056464647f03", conn.writes[0][1])
motor.angled(90)
conn.notifications.append((14, '1b0e00050082390a'))
hub.motor_AB.angled(90)
self.assertEqual("0f018139110c5a0000006464647f03", conn.writes[1][1]) self.assertEqual("0f018139110c5a0000006464647f03", conn.writes[1][1])
def test_capabilities(self): def test_capabilities(self):
@ -133,7 +138,7 @@ class GeneralTest(unittest.TestCase):
hub.connection.notifications.append((HANDLE, '1b0e000f0004010125000000001000000010')) hub.connection.notifications.append((HANDLE, '1b0e000f0004010125000000001000000010'))
time.sleep(1) time.sleep(1)
def callback(color, unk1, unk2): def callback(color, unk1, unk2=None):
name = COLORS[color] if color is not None else 'NONE' name = COLORS[color] if color is not None else 'NONE'
log.info("Color: %s %s %s", name, unk1, unk2) log.info("Color: %s %s %s", name, unk1, unk2)

View File

@ -22,6 +22,7 @@ class Vernie(MoveHub):
self._color_detected = COLOR_NONE self._color_detected = COLOR_NONE
self._sensor_distance = 10 self._sensor_distance = 10
self.color_distance_sensor.subscribe(self._color_distance_data) self.color_distance_sensor.subscribe(self._color_distance_data)
log.info("Vernie is ready.")
def _external_motor_data(self, data): def _external_motor_data(self, data):
log.debug("External motor position: %s", data) log.debug("External motor position: %s", data)
@ -43,6 +44,10 @@ class Vernie(MoveHub):
def program(self): def program(self):
time.sleep(1) time.sleep(1)
self.head_to(LEFT, angle=90)
self.head_to(RIGHT, angle=50)
time.sleep(1)
while True: while True:
self.head_to(LEFT) self.head_to(LEFT)
time.sleep(1) time.sleep(1)
@ -59,6 +64,7 @@ class Vernie(MoveHub):
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
comms.log.setLevel(logging.INFO)
try: try:
connection = DebugServerConnection() connection = DebugServerConnection()