diff --git a/snippets/ev3/bluetooth_pc/pybricks/bluetooth.py b/snippets/ev3/bluetooth_pc/pybricks/bluetooth.py new file mode 100644 index 0000000..a1c6cc2 --- /dev/null +++ b/snippets/ev3/bluetooth_pc/pybricks/bluetooth.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: MIT +# Copyright (C) 2020 The Pybricks Authors + +""" +:class:`RFCOMMServer` can be used to communicate with other Bluetooth RFCOMM +devices that don't support the EV3 mailbox protocol. + +It is based on the standard library ``socketserver`` module and attempts to +remain a strict subset of that implementation when it comes to low-level +implementation details. +""" + +from _thread import start_new_thread +from uctypes import addressof, sizeof, struct, ARRAY, UINT8, UINT16 +from usocket import socket, SOCK_STREAM + +from bluetooth_c import resolve + +# stuff from bluetooth/bluetooth.h + +AF_BLUETOOTH = 31 +BTPROTO_RFCOMM = 3 +BDADDR_ANY = '00:00:00:00:00:00' + +sa_family_t = UINT16 + +bd_addr_t = { + 'b': (ARRAY | 0, UINT8 | 6) +} + +sockaddr_rc = { + 'rc_family': sa_family_t | 0, + 'rc_bdaddr': (2, bd_addr_t), + 'rc_channel': UINT8 | 8, +} + + +def str2ba(string, ba): + """Convert string to Bluetooth address""" + for i, v in enumerate(string.split(':')): + ba.b[5-i] = int(v, 16) + + +def ba2str(ba): + """Convert Bluetooth address to string""" + string = [] + for b in ba.b: + string.append('{:02X}'.format(b)) + string.reverse() + return ':'.join(string).upper() + + +class RFCOMMServer: + """Object that simplifies setting up an RFCOMM socket server. + + This is based on the ``socketserver.SocketServer`` class in the Python + standard library. + """ + request_queue_size = 1 + + def __init__(self, server_address, RequestHandlerClass): + self.server_address = server_address + self.RequestHandlerClass = RequestHandlerClass + + self.socket = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM) + + try: + addr_data = bytearray(sizeof(sockaddr_rc)) + addr = struct(addressof(addr_data), sockaddr_rc) + addr.rc_family = AF_BLUETOOTH + str2ba(server_address[0], addr.rc_bdaddr) + addr.rc_channel = server_address[1] + self.socket.bind(addr_data) + # self.server_address = self.socket.getsockname() + self.socket.listen(self.request_queue_size) + except: + self.server_close() + raise + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.server_close() + + def handle_request(self): + try: + request, addr_data = self.socket.accept() + except OSError: + return + + try: + addr = struct(addressof(addr_data), sockaddr_rc) + client_address = (ba2str(addr.rc_bdaddr), addr.rc_channel) + self.process_request(request, client_address) + except: + request.close() + raise + + def process_request(self, request, client_address): + self.finish_request(request, client_address) + request.close() + + def finish_request(self, request, client_address): + self.RequestHandlerClass(request, client_address, self) + + def server_close(self): + self.socket.close() + + +class ThreadingMixIn: + def process_request_thread(self, request, client_address): + try: + self.finish_request(request, client_address) + finally: + request.close() + + def process_request(self, request, client_address): + start_new_thread(self.process_request_thread, (request, client_address)) + + +class ThreadingRFCOMMServer(ThreadingMixIn, RFCOMMServer): + """Version of :class:`RFCOMMServer` that handles connections in a new + thread. + """ + pass + + +class StreamRequestHandler: + """Class that handles incoming requests. + + This is based on ``socketserver.StreamRequestHandler`` from the Python + standard library. + """ + def __init__(self, request, client_address, server): + self.request = request + self.client_address = client_address + self.server = server + self.setup() + try: + self.handle() + finally: + self.finish() + + def setup(self): + self.wfile = self.request + self.rfile = self.request + + def handle(self): + pass + + def finish(self): + pass + + +class RFCOMMClient: + def __init__(self, client_address, RequestHandlerClass): + self.client_address = client_address + self.RequestHandlerClass = RequestHandlerClass + self.socket = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM) + + def handle_request(self): + addr_data = bytearray(sizeof(sockaddr_rc)) + addr = struct(addressof(addr_data), sockaddr_rc) + addr.rc_family = AF_BLUETOOTH + str2ba(self.client_address[0], addr.rc_bdaddr) + addr.rc_channel = self.client_address[1] + self.socket.connect(addr_data) + try: + self.process_request(self.socket, self.client_address) + except: + self.socket.close() + raise + + def process_request(self, request, client_address): + self.finish_request(request, client_address) + request.close() + + def finish_request(self, request, client_address): + self.RequestHandlerClass(request, client_address, self) + + def client_close(self): + self.socket.close() + + +class ThreadingRFCOMMClient(ThreadingMixIn, RFCOMMClient): + pass diff --git a/snippets/ev3/bluetooth_pc/pybricks/messaging.py b/snippets/ev3/bluetooth_pc/pybricks/messaging.py new file mode 100644 index 0000000..093ba1e --- /dev/null +++ b/snippets/ev3/bluetooth_pc/pybricks/messaging.py @@ -0,0 +1,327 @@ +# SPDX-License-Identifier: MIT +# Copyright (C) 2020 The Pybricks Authors + +from _thread import allocate_lock +from uerrno import ECONNRESET +from ustruct import pack, unpack + +from pybricks.bluetooth import (resolve, BDADDR_ANY, ThreadingRFCOMMServer, + ThreadingRFCOMMClient, StreamRequestHandler) + + +class Mailbox: + def __init__(self, name, connection, encode=None, decode=None): + """Object that represents a mailbox for sending an receiving messages + from other connected devices. + + Arguments: + name (str): + The name of this mailbox. + connection: + A connection object that implements the mailbox connection + interface. + encode: + A function that encodes an object into a bytes-like object. + decode: + A function that decodes an object from a bytes-like object. + """ + self.name = name + self._connection = connection + + if encode: + self.encode = encode + + if decode: + self.decode = decode + + def encode(self, value): + return value + + def decode(self, payload): + return payload + + def read(self): + """Reads the current value of the mailbox. + + Returns: + The decoded value or ``None`` if the mailbox has never received + a value. + """ + data = self._connection.read_from_mailbox(self.name) + if data is None: + return None + return self.decode(data) + + def send(self, value, destination=None): + """Sends a value to remote mailboxes with the same name as this + mailbox. + + Arguments: + value: The value to send. + destination: The name or address of a specific device or ``None`` + to broadcast to all connected devices. + """ + data = self.encode(value) + self._connection.send_to_mailbox(destination, self.name, data) + + def wait(self): + """Waits for the mailbox to receive a message.""" + self._connection.wait_for_mailbox_update(self.name) + + def wait_new(self): + """Waits for the mailbox to receive a message that is different from + the current contents of the mailbox. + + Returns: + The new value. (Same as return value of :meth:`read`.) + """ + old = self.read() + while True: + self.wait() + new = self.read() + if new != old: + return new + + +class LogicMailbox(Mailbox): + """:class:`Mailbox` that holds a logic or boolean value. + + This is compatible with the "logic" message blocks in the standard + EV3 firmware. + """ + + def encode(self, value): + return b'\x01' if value else b'\x00' + + def decode(self, payload): + return bool(payload[0]) + + +class NumericMailbox(Mailbox): + """:class:`Mailbox` that holds a numeric or floating point value. + + This is compatible with the "numeric" message blocks in the standard + EV3 firmware. + """ + + def encode(self, value): + return pack('