initial EV3 bluetooth mailbox docs

This commit is contained in:
David Lechner
2020-02-12 09:55:28 +01:00
committed by Laurens Valk
parent f16876cca9
commit 7acbcbd874
7 changed files with 487 additions and 2 deletions
+1 -1
View File
@@ -14,6 +14,6 @@ install:
script:
- pipenv run python setup.py build
- make -C doc html SOURCEDIR=api
- pipenv run make -C doc html SOURCEDIR=api
- pipenv run flake8
- pipenv run doc8
+27
View File
@@ -0,0 +1,27 @@
:mod:`bluetooth <pybricks.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:
+106
View File
@@ -0,0 +1,106 @@
EV3 Mailboxes
=============
TODO: introduction... messages are sent immediately, received messages are held
in mailbox for getting later.
.. rubric:: Pairing
Before two EV3s can communicate with each other via Bluetooth, they must be
paired.
TODO: screenshots of pairing with brickman, don't press the connect button!
.. rubric:: Client and server
Programs can be written using either an EV3 mailbox *client* object or an EV3
mailbox *server* object.
The only difference between the *client* and the *server* is which one
initiates the connection at the beginning of the program. After that, sending
and receiving messages is bidirectional and works the same from either point of
view.
The *server* waits for an incoming connection while the *client* initiates the
connection. Therefore, the server program must always be started first. If not,
both programs will wait forever for a connection.
Here is a basic example where two EV3s are connected and send greetings to
each other.
.. rubric:: Client program
.. literalinclude::
../../pybricks-projects/snippets/ev3/bluetooth_client/client.py
.. rubric:: Server program
.. literalinclude::
../../pybricks-projects/snippets/ev3/bluetooth_server/server.py
.. rubric:: EV3-G compatibility
TODO: screenshots of EV3-G programs, difference between client and server
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.
.. rubric:: More than two bricks
A single client EV3 can connect to multiple server EV3s or a single server EV3
can accept connections from multiple clients.
TODO: the actual implementation needs to be updated to match this example.
Example::
client = EV3MailboxClient()
# connect to 4 different servers
client.connect(SERVER1)
client.connect(SERVER2)
client.connect(SERVER3)
client.connect(SERVER4)
Example::
server = EV3MailboxServer()
# wait for 4 clients to connect
server.wait_for_connection(4)
+2
View File
@@ -29,6 +29,7 @@ Intro
tools
robotics
media
bluetooth
.. toctree::
:maxdepth: 1
@@ -37,4 +38,5 @@ Intro
signaltypes
motors
ev3_mailboxes
.. frames
+28
View File
@@ -19,6 +19,10 @@
#
import os
import sys
from docutils import nodes
from docutils.parsers.rst.directives import flag
from docutils.parsers.rst import Directive
from sphinx.application import Sphinx
from sphinx.domains.python import PyClassmember, PythonDomain
sys.path.insert(0, os.path.abspath('../..'))
@@ -116,6 +120,7 @@ nitpick_ignore = [
('py:class', 'object'),
('py:class', 'str'),
('py:class', 'tuple'),
('py:exc', 'OSError'),
('py:exc', 'RuntimeError'),
('py:exc', 'TypeError'),
('py:exc', 'ValueError'),
@@ -268,6 +273,29 @@ texinfo_documents = [
]
# -- .. availability:: directive
class AvailabilityDirective(Directive):
has_content = True
option_spec = {
'movehub': flag,
'cityhub': flag,
'cplushub': flag,
'ev3dev-stretch': flag,
}
def run(self):
if not self.options:
raise self.error('Must specify at least one platform.')
# TODO: make links to platform pages
return [nodes.emphasis(text='Availability: '),
nodes.Text(', '.join(self.options))]
def setup(app: Sphinx):
app.add_directive('availability', AvailabilityDirective)
# -- Python domain hacks ---------------------------------------------------
real_get_signature_prefix = PyClassmember.get_signature_prefix
+322
View File
@@ -0,0 +1,322 @@
# 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."""
+1 -1
View File
@@ -16,4 +16,4 @@ exclude = .venv/,versioneer.py
max-line-length = 88
[doc8]
ignore-path = .venv/
ignore-path = .venv/,doc/api/build/