From b1382f02b0bf90c455bfb311808eb848b2557c3f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 10 Feb 2020 15:08:15 -0600 Subject: [PATCH] move EV3 mailboxes to ev3messaging module - add separate maibox classes - drop bluetooth module since it is not considered public API --- doc/api/bluetooth.rst | 27 ---- doc/api/ev3_mailboxes.rst | 33 +--- doc/api/ev3messaging.rst | 43 +++++ doc/api/index.rst | 2 +- doc/common/conf.py | 1 + pybricks-projects | 2 +- pybricks/bluetooth.py | 322 -------------------------------------- pybricks/ev3messaging.py | 275 ++++++++++++++++++++++++++++++++ 8 files changed, 324 insertions(+), 381 deletions(-) delete mode 100644 doc/api/bluetooth.rst create mode 100644 doc/api/ev3messaging.rst delete mode 100644 pybricks/bluetooth.py create mode 100644 pybricks/ev3messaging.py diff --git a/doc/api/bluetooth.rst b/doc/api/bluetooth.rst deleted file mode 100644 index b2c2bce..0000000 --- a/doc/api/bluetooth.rst +++ /dev/null @@ -1,27 +0,0 @@ -:mod:`bluetooth ` -- Bluetooth -================================================== - -.. automodule:: pybricks.bluetooth - :no-members: - -.. currentmodule:: pybricks.bluetooth - - -EV3 Mailboxes -------------- - -Pybricks MicroPython provides a mailbox implementation that is compatible with -the standard LEGO firmware. This can be used to communicate between multiple -EV3 bricks running Pybrick MicroPython, the standard LEGO firmware or a -combination of the two. - -.. note:: See :doc:`ev3_mailboxes` for a more general overview. - -.. autoclass:: EV3MailboxClient - -.. autoclass:: EV3MailboxServer - -.. autoclass:: EV3MailboxMixIn - -.. autodata:: ALL_BRICKS - :annotation: diff --git a/doc/api/ev3_mailboxes.rst b/doc/api/ev3_mailboxes.rst index 58057cf..590950c 100644 --- a/doc/api/ev3_mailboxes.rst +++ b/doc/api/ev3_mailboxes.rst @@ -48,36 +48,9 @@ program is that client program has connect block. Server is always running. TODO: show equivalent blocks -- :meth:`pybricks.bluetooth.EV3MailboxMixIn.get_logic` -- :meth:`pybricks.bluetooth.EV3MailboxMixIn.get_numeric` -- :meth:`pybricks.bluetooth.EV3MailboxMixIn.get_text` -- :meth:`pybricks.bluetooth.EV3MailboxMixIn.send_logic` -- :meth:`pybricks.bluetooth.EV3MailboxMixIn.send_numeric` -- :meth:`pybricks.bluetooth.EV3MailboxMixIn.send_text` - - -.. rubric:: Sending objects as text - -Simple Python objects, like dictionaries, can be encoded as text using the -builtin ``repr`` function and turned in to objects again using the builtin -``eval`` function. - -Example:: - - # Server program - my_obj = { 'a': 1, 'b': 2 } - server.send_text(ALL_BRICKS, 'my_obj', repr(my_obj)) - -:: - - # Client program - client.wait_for_update('my_obj') - my_obj = eval(client.get_text('my_obj')) - - -.. warning:: In general, ``eval`` is considered a security risk because it - can execute arbitrary code! Never use ``eval`` with untrusted data, like - like data received from the Internet. +- :class:`pybricks.ev3messaging.LogicMailbox` +- :class:`pybricks.ev3messaging.NumericMailbox` +- :class:`pybricks.ev3messaging.TextMailbox` .. rubric:: More than two bricks diff --git a/doc/api/ev3messaging.rst b/doc/api/ev3messaging.rst new file mode 100644 index 0000000..d712a14 --- /dev/null +++ b/doc/api/ev3messaging.rst @@ -0,0 +1,43 @@ +:mod:`ev3messaging ` -- EV3 Messaging +============================================================ + +.. automodule:: pybricks.ev3messaging + :no-members: + +.. currentmodule:: pybricks.ev3messaging + + +Mailboxes +--------- + +Mailboxes are used to send data to and from other EV3s. + +.. autoclass:: LogicMailbox + :no-members: + + .. automethod:: read + .. automethod:: send + .. automethod:: wait + .. automethod:: wait_new + +.. autoclass:: NumericMailbox + :no-members: + + .. automethod:: read + .. automethod:: send + .. automethod:: wait + .. automethod:: wait_new + +.. autoclass:: TextMailbox + :no-members: + + .. automethod:: read + .. automethod:: send + .. automethod:: wait + .. automethod:: wait_new + +.. autoclass:: Mailbox + +.. autoclass:: BluetoothMailboxServer + +.. autoclass:: BluetoothMailboxClient diff --git a/doc/api/index.rst b/doc/api/index.rst index 7180523..9ab9815 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -29,7 +29,7 @@ Intro tools robotics media - bluetooth + ev3messaging .. toctree:: :maxdepth: 1 diff --git a/doc/common/conf.py b/doc/common/conf.py index 682aa8a..5a75d7b 100644 --- a/doc/common/conf.py +++ b/doc/common/conf.py @@ -112,6 +112,7 @@ nitpick_ignore = [ ('py:class', 'bool'), ('py:class', 'bytearray'), ('py:class', 'bytes'), + ('py:class', 'callable'), ('py:class', 'dict'), ('py:class', 'float'), ('py:class', 'int'), diff --git a/pybricks-projects b/pybricks-projects index fcc8af3..5ffedc9 160000 --- a/pybricks-projects +++ b/pybricks-projects @@ -1 +1 @@ -Subproject commit fcc8af3381a13625c829a5e043ac48761f1f67ed +Subproject commit 5ffedc9d51504041a932bcba8587b2fa2d3ee2ca diff --git a/pybricks/bluetooth.py b/pybricks/bluetooth.py deleted file mode 100644 index 6312e24..0000000 --- a/pybricks/bluetooth.py +++ /dev/null @@ -1,322 +0,0 @@ -# SPDX-License-Identifier: MIT -# Copyright (C) 2020 David Lechner - -"""This module provides classes for working with Bluetooth Classic. - -.. availability:: - :ev3dev-stretch: - -.. versionadded:: 2.0.0 -""" - -from socketserver import BaseServer, ThreadingMixIn, StreamRequestHandler - -try: - from socket import BDADDR_ANY -except ImportError: - BDADDR_ANY = '00:00:00:00:00:00' - - -__all__ = ['BDADDR_ANY', 'RFCOMMServer', 'ThreadingRFCOMMServer', - 'StreamRequestHandler', 'ALL_BRICKS', 'EV3MailboxServer', - 'EV3MailboxClient'] - - -class RFCOMMServer(BaseServer): - """Object that simplifies setting up an RFCOMM socket server. - - This is based on the ``socketserver.SocketServer`` class in the Python - standard library. - - .. availability:: - :ev3dev-stretch: - - .. versionadded:: 2.0.0 - """ - - -class ThreadingRFCOMMServer(ThreadingMixIn, RFCOMMServer): - """Version of :class:`RFCOMMServer` that handles connections in a new - thread. - - .. availability:: - :ev3dev-stretch: - - .. versionadded:: version - - .. versionadded:: 2.0.0 - """ - - -ALL_BRICKS = None -"""Can be used in ``send`` methods to broadcast to call connected bricks. - -.. availability:: - :ev3dev-stretch: - -.. versionadded:: 2.0.0 -""" - -EV3_RFCOMM_CHANNEL = 1 - - -class EV3MailboxMixIn: - """Methods shared by both :class:`EV3MailboxServer` and - :class:`EV3MailboxClient` - - .. availability:: - :ev3dev-stretch: - - .. versionadded:: 2.0.0 - """ - def get_raw_data(self, mbox): - """Gets the current raw data from a mailbox. - - Arguments: - mbox (str): - The name of the mailbox. - - Returns: - bytes: - The current mailbox raw data or ``None`` if nothing has ever - been delivered to the mailbox. - """ - return b'' - - def get_packed_data(self, mbox, fmt): - """Gets the current packed data from a mailbox. - - Arguments: - mbox (str): - The name of the mailbox. - fmt (str): - ``ustruct.unpack()`` format string used to decode the binary - data. - - Returns: - tuple: - The result of ``ustruct.unpack()`` on the mailbox data. - - Raises: - TypeError: - ``fmt`` is not a string. - RuntimeError: - ``mbox`` is empty. - """ - return () - - def get_logic(self, mbox): - """Gets the current value of the mailbox as a boolean value. - - This is compatible with the "logic" mailbox type in EV3-G. - - Arguments: - mbox (str): - The name of the mailbox. - - Returns: - bool: - The current value or ``None`` if the mailbox is empty. - """ - return False - - def get_numeric(self, mbox): - """Gets the current value of the mailbox as a floating point value. - - This is compatible with the "numeric" mailbox type in EV3-G. - - Arguments: - mbox (str): - The name of the mailbox. - - Returns: - float: - The current value or ``None`` if the mailbox is empty. - """ - return 0.0 - - def get_text(self, mbox): - """Gets the current value of the mailbox as a string value. - - This is compatible with the "text" mailbox type in EV3-G. - - Arguments: - mbox (str): - The name of the mailbox. - - Returns: - str: - The current value or ``None`` if the mailbox is empty. - """ - return "" - - def send_raw_data(self, brick, mbox, payload): - """Sends a mailbox value using raw bytes data. - - .. todo:: Currently the Bluetooth address must be used instead of the - the brick name. - - Arguments: - brick (str): - The name or Bluetooth address of the brick or - :data:`ALL_BRICKS` to broadcast to all connected devices. - mbox (str): - The name of the mailbox. - payload (bytes): - A bytes-like object that will be sent to the mailbox. - """ - - def send_packed_data(self, brick, mbox, fmt, *args): - """Sends a mailbox value using packed values. - - .. todo:: Currently the Bluetooth address must be used instead of the - the brick name. - - Arguments: - brick (str): - The name or Bluetooth address of the brick or - :data:`ALL_BRICKS` to broadcast to all connected devices. - mbox (str): - The name of the mailbox. - fmt (str): - Format string compatible with ``ustruct.pack()`` - *: - Arguments for ``ustruct.pack()`` - """ - - def send_logic(self, brick, mbox, value): - """Sends a boolean mailbox value. - - This is compatible with the "logic" mailbox type in EV3-G. - - .. todo:: Currently the Bluetooth address must be used instead of the - the brick name. - - Arguments: - brick (str): - The name or Bluetooth address of the brick or - :data:`ALL_BRICKS` to broadcast to all connected devices. - mbox (str): - The name of the mailbox. - value (bool): - The value that will be delivered to the mailbox. - """ - - def send_numeric(self, brick, mbox, value): - """Sends a float mailbox value. - - This is compatible with the "numeric" mailbox type in EV3-G. - - .. todo:: Currently the Bluetooth address must be used instead of the - the brick name. - - Arguments: - brick (str): - The name or Bluetooth address of the brick or - :data:`ALL_BRICKS` to broadcast to all connected devices. - mbox (str): - The name of the mailbox. - value (bool): - The value that will be delivered to the mailbox. - """ - - def send_text(self, brick, mbox, value): - """Sends a string mailbox value. - - This is compatible with the "text" mailbox type in EV3-G. - - .. todo:: Currently the Bluetooth address must be used instead of the - the brick name. - - Arguments: - brick (str): - The name or Bluetooth address of the brick or - :data:`ALL_BRICKS` to broadcast to all connected devices. - mbox (str): - The name of the mailbox. - value (bool): - The value that will be delivered to the mailbox. - """ - - def wait_for_update(self, mbox): - """Waits until ``mbox`` receives a value. - - Arguments: - mbox (str): - The name of the mailbox. - """ - - -class EV3MailboxServer(EV3MailboxMixIn, ThreadingRFCOMMServer): - """Object that represents an incoming Bluetooth connection from another - EV3. - - The remote EV3 can either be running MicroPython or the standard EV3 - firmare. - - See :class:`EV3MailboxMixIn` for additional methods. - - .. availability:: - :ev3dev-stretch: - - .. versionadded:: 2.0.0 - """ - def __init__(self): - pass - - def wait_for_connection(self, count=1): - """Waits for a :class:`EV3MailboxClient` on a remote device to connect. - - Arguments: - count (int): - The number of remote connections to wait for. - - Raises: - OSError: - There was a problem establishing the connection. - """ - - -class EV3MailboxClient(EV3MailboxMixIn, ThreadingMixIn): - """Object that represents an outgoing Bluetooth connection to another - EV3. - - The remote EV3 can either be running MicroPython or the standard EV3 - firmare. - - See :class:`EV3MailboxMixIn` for additional methods. - - .. availability:: - :ev3dev-stretch: - - .. versionadded:: 2.0.0 - """ - def __init__(self, brick): - """ - .. todo:: Currently the Bluetooth address must be used instead of the - the brick name. - - Arguments: - brick (str): - The name or Bluetooth address of the remote EV3 to connect to. - """ - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def connect(self): - """Connects to an :class:`EV3MailboxServer` on another device. - - The remote device must be paired and waiting for a connection. See - :meth:`EV3MailboxServer.wait_for_connection`. - - Raises: - OSError: - There was a problem establishing the connection. - """ - - def close(self): - """Closes the connection.""" diff --git a/pybricks/ev3messaging.py b/pybricks/ev3messaging.py new file mode 100644 index 0000000..aa9fa65 --- /dev/null +++ b/pybricks/ev3messaging.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: MIT +# Copyright (C) 2020 David Lechner + +""" +This module contains everything to do with EV3 bytecode messages. + +.. availability:: + :ev3dev-stretch: + +.. versionadded:: 2.0.0 +""" + + +class Mailbox: + def __init__(self, name, connection, encode=repr, decode=eval): + """Object that represents a mailbox that contains encoded data. + + See :class:`LogicMailbox`, :class:`NumericMailbox` and + :class:`TextMailbox` for mailboxes that are compatible with the + standard EV3 firmware and desktop programming software. + + ``encode`` is used in :meth:`send` and ``decode`` is used in + :meth:`read`. The defaults of ``repr`` and ``eval`` work with most + builtin types (like dict and list) but don't work for other objects + (like motors and sensors). + + .. warning:: In general, ``eval`` is considered a security risk because + it can execute arbitrary code! Never use ``eval`` with untrusted + data, like like data received from the Internet. + + Arguments: + name (str): + The name of this mailbox. + connection: + A connection object such as :class:`BluetoothMailboxClient`. + encode (callable): + Function that encodes a Python object to bytes. + decode (callable): + Function that creates a new Python object from bytes. + """ + + def read(self): + """Gets the current value of the mailbox. + + Returns: + The current value or ``None`` if the mailbox is empty. + """ + return '' + + def send(self, value, brick=None): + """Sends a value to this mailbox on connected devices. + + .. todo:: Currently the Bluetooth address must be used instead of the + the brick name. + + Arguments: + value: + The value that will be delivered to the mailbox. + brick (str): + The name or Bluetooth address of the brick or ``None`` to + to broadcast to all connected devices. + + Raises: + OSError: + There is a problem with the connection. + """ + + def wait(self): + """Waits for the mailbox to be updated by remote device.""" + + def wait_new(self): + """Waits for a new value to be delivered to the mailbox that is not + equal to the current value in the mailbox. + + + Returns: + The new value. + """ + return object() + + +class LogicMailbox(Mailbox): + def __init__(self, name, connection): + """ + Object that represents a mailbox that contains a logic (true/false) + value. + + This is compatible with the "logic" mailbox type in EV3-G. + + Arguments: + name (str): + The name of this mailbox. + connection: + A connection object such as :class:`BluetoothMailboxClient`. + """ + + def read(self): + """Gets the current value of the mailbox as a boolean value. + + Returns: + bool: + The current value or ``None`` if the mailbox is empty. + """ + return '' + + def send(self, value, brick=None): + """Sends a boolean value to this mailbox on connected devices. + + .. todo:: Currently the Bluetooth address must be used instead of the + the brick name. + + Arguments: + value (bool): + The value that will be delivered to the mailbox. + brick (str): + The name or Bluetooth address of the brick or ``None`` to + to broadcast to all connected devices. + + Raises: + OSError: + There is a problem with the connection. + """ + + +class NumericMailbox(Mailbox): + def __init__(self, name, connection): + """ + Object that represents a mailbox that contains a number. + + This is compatible with the "numeric" mailbox type in EV3-G. + + Arguments: + name (str): + The name of this mailbox. + connection: + A connection object such as :class:`BluetoothMailboxClient`. + """ + + def read(self): + """Gets the current value of the mailbox as a numeric value. + + Returns: + float: + The current value or ``None`` if the mailbox is empty. + """ + return '' + + def send(self, value, brick=None): + """Sends a numeric value to this mailbox on connected devices. + + .. todo:: Currently the Bluetooth address must be used instead of the + the brick name. + + Arguments: + value (float): + The value that will be delivered to the mailbox. + brick (str): + The name or Bluetooth address of the brick or ``None`` to + to broadcast to all connected devices. + + Raises: + TypeError: + ``value`` connot be converted to a floating point. + OSError: + There is a problem with the connection. + """ + + +class TextMailbox(Mailbox): + def __init__(self, name, connection): + """ + Object that represents a mailbox that contains text. + + This is compatible with the "text" mailbox type in EV3-G. + + Arguments: + name (str): + The name of this mailbox. + connection: + A connection object such as :class:`BluetoothMailboxClient`. + """ + + def read(self): + """Gets the current value of the mailbox as a string value. + + Returns: + str: + The current value or ``None`` if the mailbox is empty. + """ + return "" + + def send(self, value, brick=None): + """Sends a string value to this mailbox on connected devices. + + .. todo:: Currently the Bluetooth address must be used instead of the + the brick name. + + Arguments: + value (str): + The value that will be delivered to the mailbox. + brick (str): + The name or Bluetooth address of the brick or ``None`` to + to broadcast to all connected devices. + + Raises: + OSError: + There is a problem with the connection. + """ + + +class BluetoothMailboxServer: + """Object that represents an incoming Bluetooth connection from another + EV3. + + The remote EV3 can either be running MicroPython or the standard EV3 + firmare. + """ + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def wait_for_connection(self, count=1): + """Waits for a :class:`BluetoothMailboxClient` on a remote device to + connect. + + Arguments: + count (int): + The number of remote connections to wait for. + + Raises: + OSError: + There was a problem establishing the connection. + """ + + def close(self): + """Closes all connections.""" + + +class BluetoothMailboxClient: + """Object that represents an outgoing Bluetooth connection to another + EV3. + + The remote EV3 can either be running MicroPython or the standard EV3 + firmare. + """ + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def connect(self, brick): + """Connects to an :class:`BluetoothMailboxServer` on another device. + + The remote device must be paired and waiting for a connection. See + :meth:`BluetoothMailboxServer.wait_for_connection`. + + .. todo:: Currently the Bluetooth address must be used instead of the + the brick name. + + Arguments: + brick (str): + The name or Bluetooth address of the remote EV3 to connect to. + + Raises: + OSError: + There was a problem establishing the connection. + """ + + def close(self): + """Closes all connections."""