diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..bffb90e Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6d0a4b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + env: + TERM: xterm-256color + FORCE_COLOR: 1 + + # Skip CI if [ci skip] in the commit message + if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[tests,docs] + - name: Lint with ruff + run: | + make lint + - name: Build the doc + run: | + make doc + - name: Check codestyle + run: | + make check-codestyle + - name: Type check + run: | + make type + - name: Test with pytest + run: | + make pytest diff --git a/.gitignore b/.gitignore index 7053d6f..d8c170e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,13 @@ __pycache__/ .cache/ *.x *.pyc +_build/ +.pytest_cache/ # Setuptools distribution folder. dist/ # Python egg metadata, regenerated from source files by setuptools. *.egg-info +dist/ +build/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ace29b0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python -os: - - linux -python: - - "2.7" - - "3.4" - - "3.5" - - "3.6" -# command to install dependencies -install: - - pip install -r requirements.txt -# command to run tests -script: - - pytest # or py.test for Python versions 3.5 and below diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4f23e37 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +SHELL=/bin/bash +LINT_PATHS=robust_serial/ tests/ docs/conf.py setup.py examples/ + +pytest: + python3 -m pytest --cov-config .coveragerc --cov-report html --cov-report term --cov=. -v --color=yes + +pytype: + pytype -j auto + +mypy: + mypy ${LINT_PATHS} + +#type: pytype mypy +type: mypy + + +lint: + # stop the build if there are Python syntax errors or undefined names + # see https://www.flake8rules.com/ + ruff ${LINT_PATHS} --select=E9,F63,F7,F82 --show-source + # exit-zero treats all errors as warnings. + ruff ${LINT_PATHS} --exit-zero + +format: + # Sort imports + isort ${LINT_PATHS} + # Reformat using black + black ${LINT_PATHS} + +check-codestyle: + # Sort imports + isort --check ${LINT_PATHS} + # Reformat using black + black --check ${LINT_PATHS} + +commit-checks: format type lint + +doc: + cd docs && make html + +spelling: + cd docs && make spelling + +clean: + cd docs && make clean + +# PyPi package release +release: + python setup.py sdist + python setup.py bdist_wheel + twine upload dist/* + +# Test PyPi package release +test-release: + python setup.py sdist + python setup.py bdist_wheel + twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +.PHONY: clean spelling doc lint format check-codestyle commit-checks diff --git a/README.md b/README.md index d29b30d..1e2549d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # Robust Arduino Serial Protocol in Python -[![Build Status](https://travis-ci.org/araffin/python-arduino-serial.svg?branch=master)](https://travis-ci.org/araffin/python-arduino-serial) +![CI](https://github.com/araffin/python-arduino-serial/workflows/CI/badge.svg) [![Documentation Status](https://readthedocs.org/projects/python-arduino-serial/badge/?version=latest)](https://python-arduino-serial.readthedocs.io/en/latest/?badge=latest) **Robust Arduino Serial** is a simple and robust serial communication protocol. It was designed to make two arduinos communicate, but can also be useful when you want a computer (e.g. a Raspberry Pi) to communicate with an Arduino. -It supports both Python 2 and 3. +It supports Python 3.8+. This repository is part of the Robust Arduino Serial project, main repository: [https://github.com/araffin/arduino-robust-serial](https://github.com/araffin/arduino-robust-serial) **Please read the [Medium Article](https://medium.com/@araffin/simple-and-robust-computer-arduino-serial-communication-f91b95596788) to have an overview of this protocol.** +Documentation: [https://python-arduino-serial.readthedocs.io](https://python-arduino-serial.readthedocs.io) + Implementations are available in various programming languages: - [Arduino](https://github.com/araffin/arduino-robust-serial) @@ -19,6 +21,12 @@ Implementations are available in various programming languages: ## Installation +Using pip: +``` +pip install robust_serial +``` + +From Source: ``` git clone https://github.com/araffin/python-arduino-serial.git pip install -e . @@ -27,7 +35,7 @@ pip install -e . ## Tests Run the tests (require pytest): ``` -pytest +make pytest ``` ## Examples @@ -42,9 +50,23 @@ Serial communication with an Arduino: [Arduino Source Code](https://github.com/a python -m examples.arduino_serial ``` -### Bluetooth Example +### Example: Communication with Sockets + +It can be useful when you want two programs to communicate over a network (e.g. using wifi) or even locally on the same computer (e.g. when you want a python2 script that communicates with a python3 script). + +1. Start the server: +``` +python -m examples.socket_example --server +``` + +2. Run the client: +``` +python -m examples.socket_example --client +``` + +### Bluetooth Example with Two Computers -Dependency: +Dependencies: ``` sudo apt-get install libbluetooth-dev bluez pip install pybluez diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..a7780c8 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = RobustArduinoSerialProtocol +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/_static/css/custom_theme.css b/docs/_static/css/custom_theme.css new file mode 100644 index 0000000..77fc220 --- /dev/null +++ b/docs/_static/css/custom_theme.css @@ -0,0 +1,10 @@ +/* Header fonts y */ +h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend, p.caption { + font-family: "Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; +} + + +/* Make code blocks have a background */ +.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight'] { + background: #f8f8f8;; +} diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..6e860db --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,8 @@ +.. _api: + + +Available Functions +=================== + +.. automodule:: robust_serial.robust_serial + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..c5e39df --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,178 @@ +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +from typing import Dict + +sys.path.insert(0, os.path.abspath("..")) + +import robust_serial # noqa: E402 + +# -- Project information ----------------------------------------------------- + +project = "Robust Arduino Serial Protocol" +copyright = "2018-2023, Antonin Raffin" +author = "Antonin Raffin" + +# The short X.Y version +version = "" +# The full version, including alpha/beta/rc tags +release = robust_serial.__version__ + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.viewcode", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# Fix for read the docs +on_rtd = os.environ.get("READTHEDOCS") == "True" +if on_rtd: + html_theme = "default" +else: + html_theme = "sphinx_rtd_theme" + + +def setup(app): + app.add_css_file("css/custom_theme.css") + + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "RobustArduinoSerialProtocoldoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements: Dict[str, str] = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "RobustArduinoSerialProtocol.tex", + "Robust Arduino Serial Protocol Documentation", + "Antonin Raffin", + "manual", + ), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "robustarduinoserialprotocol", "Robust Arduino Serial Protocol Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "RobustArduinoSerialProtocol", + "Robust Arduino Serial Protocol Documentation", + author, + "RobustArduinoSerialProtocol", + "One line description of project.", + "Miscellaneous", + ), +] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..9668ffb --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,94 @@ +.. _examples: + +Examples +======== + +Examples provided here are also in the ``examples/`` folder of the repo. + +Arduino Serial Communication +---------------------------- + +Serial communication with an Arduino: `Arduino Source Code`_ + +.. _Arduino Source Code: https://github.com/araffin/arduino-robust-serial/tree/master/arduino-board/ + + +.. code-block:: python + + from __future__ import print_function, division, absolute_import + + import time + + from robust_serial import write_order, Order, write_i8, write_i16, read_i8, read_order + from robust_serial.utils import open_serial_port + + + try: + serial_file = open_serial_port(baudrate=115200, timeout=None) + except Exception as e: + raise e + + is_connected = False + # Initialize communication with Arduino + while not is_connected: + print("Waiting for arduino...") + write_order(serial_file, Order.HELLO) + bytes_array = bytearray(serial_file.read(1)) + if not bytes_array: + time.sleep(2) + continue + byte = bytes_array[0] + if byte in [Order.HELLO.value, Order.ALREADY_CONNECTED.value]: + is_connected = True + + print("Connected to Arduino") + + motor_speed = -56 + + # Equivalent to write_i8(serial_file, Order.MOTOR.value) + write_order(serial_file, Order.MOTOR) + write_i8(serial_file, motor_speed) + + write_order(serial_file, Order.SERVO) + write_i16(serial_file, 120) + + for _ in range(10): + order = read_order(serial_file) + print("Ordered received: {:?}", order) + + + +Reading / Writing in a file +--------------------------- + +Read write in a file (WARNING: the file will be deleted when the script exits) + + +.. code-block:: python + + from __future__ import print_function, division, absolute_import + import os + + from robust_serial import Order, write_order, write_i8, write_i16, write_i32, read_i8, read_i16, read_i32, read_order + + test_file = "test.txt" + + with open(test_file, 'wb') as f: + write_order(f, Order.HELLO) + + write_i8(f, Order.MOTOR.value) + write_i16(f, -56) + write_i32(f, 131072) + + with open(test_file, 'rb') as f: + # Equivalent to Order(read_i8(f)) + order = read_order(f) + print(order) + + motor_order = read_order(f) + print(motor_order) + print(read_i16(f)) + print(read_i32(f)) + + # Delete file + os.remove(test_file) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..6c41c77 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,60 @@ +.. Robust Arduino Serial Protocol documentation master file, created by + sphinx-quickstart on Sun Sep 2 15:21:19 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Robust Arduino Serial Protocol's documentation! +========================================================== + +**Robust Arduino Serial** is a simple and robust serial communication +protocol. It was designed to make two arduinos communicate, but can also +be useful when you want a computer (e.g. a Raspberry Pi) to communicate +with an Arduino. + +It supports both Python 3.8+. + +This repository is part of the Robust Arduino Serial project, main +repository: `https://github.com/araffin/arduino-robust-serial`_ + +.. warning:: + + Please read the `Medium Article`_ to have an overview of this + protocol. + +Implementations are available in various programming languages: + +- `Arduino`_ +- `Python`_ +- `C++`_ +- `Rust`_ + +.. _`https://github.com/araffin/arduino-robust-serial`: https://github.com/araffin/arduino-robust-serial +.. _Medium Article: https://medium.com/@araffin/simple-and-robust-computer-arduino-serial-communication-f91b95596788 +.. _Arduino: https://github.com/araffin/arduino-robust-serial +.. _Python: https://github.com/araffin/python-arduino-serial +.. _C++: https://github.com/araffin/cpp-arduino-serial +.. _Rust: https://github.com/araffin/rust-arduino-serial + +.. toctree:: + :maxdepth: 1 + :caption: User Guide + + install + + +.. toctree:: + :maxdepth: 2 + :caption: Reference + + api + utils + examples + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..584bba0 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,15 @@ +Installation +============ + +Using pip: + +.. code-block:: bash + + pip install robust_serial + +From Source: + +.. code-block:: bash + + git clone https://github.com/araffin/python-arduino-serial.git + pip install -e . diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..93a7cb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=RobustArduinoSerialProtocol + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/utils.rst b/docs/utils.rst new file mode 100644 index 0000000..9a7cc77 --- /dev/null +++ b/docs/utils.rst @@ -0,0 +1,14 @@ +.. _utils: + +Utils +===== + +.. automodule:: robust_serial.utils + :members: + + +Threads +======= + +.. automodule:: robust_serial.threads + :members: diff --git a/examples/arduino_serial.py b/examples/arduino_serial.py index 3829478..6390e0a 100644 --- a/examples/arduino_serial.py +++ b/examples/arduino_serial.py @@ -1,12 +1,9 @@ -from __future__ import print_function, division, absolute_import - import time -from robust_serial import write_order, Order, write_i8, write_i16, read_i8, read_order +from robust_serial import Order, read_order, write_i8, write_i16, write_order from robust_serial.utils import open_serial_port - -if __name__ == '__main__': +if __name__ == "__main__": try: serial_file = open_serial_port(baudrate=115200, timeout=None) except Exception as e: diff --git a/examples/arduino_threads.py b/examples/arduino_threads.py index 6e7d869..fe10eb1 100644 --- a/examples/arduino_threads.py +++ b/examples/arduino_threads.py @@ -1,12 +1,9 @@ -from __future__ import print_function, division, absolute_import - -import time import threading +import time -from robust_serial import write_order, Order +from robust_serial import Order, write_order from robust_serial.threads import CommandThread, ListenerThread - -from robust_serial.utils import open_serial_port, CustomQueue +from robust_serial.utils import CustomQueue, open_serial_port def reset_command_queue(): @@ -16,7 +13,7 @@ def reset_command_queue(): command_queue.clear() -if __name__ == '__main__': +if __name__ == "__main__": try: serial_file = open_serial_port(baudrate=115200) except Exception as e: @@ -50,8 +47,10 @@ def reset_command_queue(): print("Starting Communication Threads") # Threads for arduino communication - threads = [CommandThread(serial_file, command_queue, exit_event, n_received_semaphore, serial_lock), - ListenerThread(serial_file, exit_event, n_received_semaphore, serial_lock)] + threads = [ + CommandThread(serial_file, command_queue, exit_event, n_received_semaphore, serial_lock), + ListenerThread(serial_file, exit_event, n_received_semaphore, serial_lock), + ] for t in threads: t.start() diff --git a/examples/bluetooth_example.py b/examples/bluetooth_example.py index 90ccb9b..d0f43f1 100644 --- a/examples/bluetooth_example.py +++ b/examples/bluetooth_example.py @@ -1,12 +1,10 @@ -from __future__ import print_function, division - import argparse import bluetooth -from robust_serial import write_i8, write_i32, read_i8, read_i32 +from robust_serial import read_i8, read_i32, write_i8, write_i32 -PORT = 4885 +CHANNEL = 1 # show mac address: hciconfig SERVER_ADDR = "B8:27:EB:F1:E4:5F" @@ -16,21 +14,21 @@ def receive_messages(): Receive messages (server side) """ server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM) - server_sock.bind(("", PORT)) + server_sock.bind(("", CHANNEL)) print("Waiting for client...") # Wait for client server_sock.listen(1) client_sock, client_address = server_sock.accept() - print("Accepted connection from {}".format(client_address)) + print(f"Accepted connection from {client_address}") # Rename function to work with the lib client_sock.read = client_sock.recv - for i in range(10): - print("Received (i8): {}".format(read_i8(client_sock))) + for _ in range(10): + print(f"Received (i8): {read_i8(client_sock)}") big_number = read_i32(client_sock) - print("Received (i32): {}".format(big_number)) + print(f"Received (i32): {big_number}") client_sock.close() server_sock.close() @@ -42,9 +40,9 @@ def send_messages(mac_address): :param mac_address: (str) """ socket = bluetooth.BluetoothSocket(bluetooth.RFCOMM) - socket.connect((mac_address, PORT)) + socket.connect((mac_address, CHANNEL)) - print("Connected to {}".format(mac_address)) + print(f"Connected to {mac_address}") # Rename function to work with the lib socket.write = socket.send for i in range(10): @@ -59,16 +57,14 @@ def discover_devices(): """ nearby_devices = bluetooth.discover_devices() for bdaddr in nearby_devices: - print("{} + [{}]".format(bluetooth.lookup_name(bdaddr), bdaddr)) + print(f"{bluetooth.lookup_name(bdaddr)} + [{bdaddr}]") -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser(description="Test bluetooth server/client connection") arg_group = parser.add_mutually_exclusive_group(required=True) - arg_group.add_argument("-s", "--server", dest="server", - action='store_true', default=False, help="Create a server") - arg_group.add_argument("-c", "--client", dest="client", - action='store_true', default=False, help="Create a client") + arg_group.add_argument("-s", "--server", dest="server", action="store_true", default=False, help="Create a server") + arg_group.add_argument("-c", "--client", dest="client", action="store_true", default=False, help="Create a client") args = parser.parse_args() if args.server: receive_messages() diff --git a/examples/file_read_write.py b/examples/file_read_write.py index 18f9bf8..e862ed7 100644 --- a/examples/file_read_write.py +++ b/examples/file_read_write.py @@ -1,24 +1,21 @@ -from __future__ import print_function, division, absolute_import import argparse import os -from robust_serial import Order, write_order, write_i8, write_i16, write_i32, read_i8, read_i16, read_i32, read_order +from robust_serial import Order, read_i16, read_i32, read_order, write_i8, write_i16, write_i32, write_order - -if __name__ == '__main__': - - parser = argparse.ArgumentParser(description='Reading / Writing a file') - parser.add_argument('-f', '--test_file', help='Test file name', default="test.txt", type=str) +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Reading / Writing a file") + parser.add_argument("-f", "--test_file", help="Test file name", default="test.txt", type=str) args = parser.parse_args() - with open(args.test_file, 'wb') as f: + with open(args.test_file, "wb") as f: write_order(f, Order.HELLO) write_i8(f, Order.MOTOR.value) write_i16(f, -56) write_i32(f, 131072) - with open(args.test_file, 'rb') as f: + with open(args.test_file, "rb") as f: # Equivalent to Order(read_i8(f)) order = read_order(f) print(order) diff --git a/examples/socket_example.py b/examples/socket_example.py new file mode 100644 index 0000000..1e6c1cf --- /dev/null +++ b/examples/socket_example.py @@ -0,0 +1,84 @@ +import argparse +import socket + +from robust_serial import read_i8, read_i32, write_i8, write_i32 + +PORT = 4444 +SERVER_ADDR = "localhost" + + +class SocketAdapter: + """ + Wrapper around socket object to use the robust_serial lib + It just renames recv() to read() and send() to write() + """ + + def __init__(self, client_socket): + super().__init__() + self.client_socket = client_socket + + def read(self, num_bytes): + return self.client_socket.recv(num_bytes) + + def write(self, num_bytes): + return self.client_socket.send(num_bytes) + + def close(self): + return self.client_socket.close() + + +def receive_messages(): + """ + Receive messages (server side) + """ + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind(("", PORT)) + print("Waiting for client...") + # Wait for client + server_socket.listen(1) + + client_sock, client_address = server_socket.accept() + print(f"Accepted connection from {client_address}") + # Wrap socket to work with the lib + client_sock = SocketAdapter(client_sock) + + for _ in range(10): + print(f"Received (i8): {read_i8(client_sock)}") + big_number = read_i32(client_sock) + + print(f"Received (i32): {big_number}") + + print("Server exiting...") + client_sock.close() + server_socket.close() + + +def send_messages(server_address): + """ + Send messages (client side) + :param server_address: (str) + """ + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect((server_address, PORT)) + + # Wrap socket to work with the lib + client_socket = SocketAdapter(client_socket) + + print(f"Connected to {server_address}") + for i in range(10): + write_i8(client_socket, i) + write_i32(client_socket, 32768) + print("Client exiting...") + client_socket.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Test socket server/client connection") + arg_group = parser.add_mutually_exclusive_group(required=True) + arg_group.add_argument("-s", "--server", dest="server", action="store_true", default=False, help="Create a server") + arg_group.add_argument("-c", "--client", dest="client", action="store_true", default=False, help="Create a client") + args = parser.parse_args() + if args.server: + receive_messages() + else: + send_messages(SERVER_ADDR) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..849fbc9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[tool.ruff] +# Same as Black. +line-length = 127 +# Assume Python 3.8 +target-version = "py38" +# See https://beta.ruff.rs/docs/rules/ +select = ["E", "F", "B", "UP", "C90", "RUF"] +# Ignore explicit stacklevel` +ignore = ["B028"] + + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 15 + +[tool.black] +line-length = 127 + +[tool.isort] +profile = "black" +line_length = 127 +src_paths = ["robust_serial"] + +[tool.pytype] +inputs = ["robust_serial"] +disable = ["pyi-error"] + +[tool.mypy] +ignore_missing_imports = true +follow_imports = "silent" +show_error_codes = true +exclude = """(?x)( + stable_baselines3/a2c/a2c.py$ + )""" + +[tool.pytest.ini_options] +# Deterministic ordering for tests; useful for pytest-xdist. +env = [ + "PYTHONHASHSEED=0" +] + +filterwarnings = [ +] + + +[tool.coverage.run] +disable_warnings = ["couldnt-parse"] +branch = false +omit = [ + "tests/*", + "setup.py", + # Require device + "examples/*", +] + +[tool.coverage.report] +exclude_lines = [ "pragma: no cover", "raise NotImplementedError()", "if typing.TYPE_CHECKING:"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 13832b9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyserial==3.4 -enum34==1.1.6 diff --git a/robust_serial/__init__.py b/robust_serial/__init__.py index 6a23d8e..a8d6dca 100644 --- a/robust_serial/__init__.py +++ b/robust_serial/__init__.py @@ -1,2 +1,27 @@ -from .robust_serial import * -__version__ = "0.1" +from robust_serial.robust_serial import ( + Order, + decode_order, + read_i8, + read_i16, + read_i32, + read_order, + write_i8, + write_i16, + write_i32, + write_order, +) + +__version__ = "0.2" + +__all__ = [ + "Order", + "read_order", + "read_i8", + "read_i16", + "read_i32", + "write_i8", + "write_order", + "write_i16", + "write_i32", + "decode_order", +] diff --git a/robust_serial/robust_serial.py b/robust_serial/robust_serial.py index f0a0d50..ad98a7b 100644 --- a/robust_serial/robust_serial.py +++ b/robust_serial/robust_serial.py @@ -1,11 +1,13 @@ -from __future__ import print_function, division, unicode_literals, absolute_import - import struct - from enum import Enum +from typing import BinaryIO class Order(Enum): + """ + Pre-defined orders + """ + HELLO = 0 SERVO = 1 MOTOR = 2 @@ -14,27 +16,29 @@ class Order(Enum): RECEIVED = 5 STOP = 6 -def read_order(f): + +def read_order(f: BinaryIO) -> Order: """ :param f: file handler or serial file :return: (Order Enum Object) """ return Order(read_i8(f)) -def read_i8(f): + +def read_i8(f: BinaryIO) -> Order: """ :param f: file handler or serial file :return: (int8_t) """ - return struct.unpack(' Order: """ :param f: file handler or serial file :return: (int16_t) """ - return struct.unpack(' None: """ :param f: file handler or serial file :param value: (int8_t) """ if -128 <= value <= 127: - f.write(struct.pack(' None: """ :param f: file handler or serial file :param order: (Order Enum Object) @@ -64,23 +68,23 @@ def write_order(f, order): write_i8(f, order.value) -def write_i16(f, value): +def write_i16(f: BinaryIO, value: int) -> None: """ :param f: file handler or serial file :param value: (int16_t) """ - f.write(struct.pack(' None: """ :param f: file handler or serial file :param value: (int32_t) """ - f.write(struct.pack(' None: """ :param f: file handler or serial file :param byte: (int8_t) @@ -94,15 +98,15 @@ def decode_order(f, byte, debug=False): angle = read_i16(f) # Bit representation # print('{0:016b}'.format(angle)) - msg = "SERVO {}".format(angle) + msg = f"SERVO {angle}" elif order == Order.MOTOR: speed = read_i8(f) - msg = "motor {}".format(speed) + msg = f"motor {speed}" elif order == Order.ALREADY_CONNECTED: msg = "ALREADY_CONNECTED" elif order == Order.ERROR: error_code = read_i16(f) - msg = "Error {}".format(error_code) + msg = f"Error {error_code}" elif order == Order.RECEIVED: msg = "RECEIVED" elif order == Order.STOP: @@ -114,5 +118,5 @@ def decode_order(f, byte, debug=False): if debug: print(msg) except Exception as e: - print("Error decoding order {}: {}".format(byte, e)) - print('byte={0:08b}'.format(byte)) + print(f"Error decoding order {byte}: {e}") + print(f"byte={byte:08b}") diff --git a/robust_serial/threads.py b/robust_serial/threads.py index d52c1a4..4db8906 100644 --- a/robust_serial/threads.py +++ b/robust_serial/threads.py @@ -1,11 +1,9 @@ -from __future__ import print_function, division, absolute_import - import threading import time import serial -from .robust_serial import write_order, Order, write_i8, write_i16, decode_order +from .robust_serial import Order, decode_order, write_i8, write_i16, write_order from .utils import queue rate = 1 / 2000 # 2000 Hz (limit the rate of communication with the arduino) @@ -14,7 +12,8 @@ class CommandThread(threading.Thread): """ Thread that send orders to the arduino - it blocks if there no more send_token left (here it is the n_received_semaphore) + it blocks if there no more send_token left (here it is the n_received_semaphore). + :param serial_file: (Serial object) :param command_queue: (Queue) :param exit_event: (Threading.Event object) @@ -58,6 +57,7 @@ class ListenerThread(threading.Thread): """ Thread that listen to the Arduino It is used to add send_tokens to the n_received_semaphore + :param serial_file: (Serial object) :param exit_event: (threading.Event object) :param n_received_semaphore: (threading.Semaphore) diff --git a/robust_serial/utils.py b/robust_serial/utils.py index ee3d185..67a02dd 100644 --- a/robust_serial/utils.py +++ b/robust_serial/utils.py @@ -1,12 +1,7 @@ -from __future__ import print_function, division, absolute_import - -import sys import glob - -try: - import queue -except ImportError: - import Queue as queue +import queue +import sys +from typing import List, Optional import serial @@ -17,7 +12,7 @@ class CustomQueue(queue.Queue): A custom queue subclass that provides a :meth:`clear` method. """ - def clear(self): + def clear(self) -> None: """ Clears all items from the queue. """ @@ -26,7 +21,7 @@ def clear(self): unfinished = self.unfinished_tasks - len(self.queue) if unfinished <= 0: if unfinished < 0: - raise ValueError('task_done() called too many times') + raise ValueError("task_done() called too many times") self.all_tasks_done.notify_all() self.unfinished_tasks = unfinished self.queue.clear() @@ -34,20 +29,22 @@ def clear(self): # From https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python -def get_serial_ports(): +def get_serial_ports() -> List[str]: """ - Lists serial ports - :return: [str] A list of available serial ports + Lists serial ports. + + + :return: A list of available serial ports """ - if sys.platform.startswith('win'): - ports = ['COM%s' % (i + 1) for i in range(256)] - elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): + if sys.platform.startswith("win"): + ports = ["COM%s" % (i + 1) for i in range(256)] + elif sys.platform.startswith("linux") or sys.platform.startswith("cygwin"): # this excludes your current terminal "/dev/tty" - ports = glob.glob('/dev/tty[A-Za-z]*') - elif sys.platform.startswith('darwin'): - ports = glob.glob('/dev/tty.*') + ports = glob.glob("/dev/tty[A-Za-z]*") + elif sys.platform.startswith("darwin"): + ports = glob.glob("/dev/tty.*") else: - raise EnvironmentError('Unsupported platform') + raise OSError("Unsupported platform") results = [] for port in ports: @@ -60,14 +57,20 @@ def get_serial_ports(): return results -def open_serial_port(serial_port=None, baudrate=115200, timeout=0, write_timeout=0): +def open_serial_port( + serial_port: Optional[str] = None, + baudrate: int = 115200, + timeout: Optional[int] = 0, + write_timeout: int = 0, +) -> serial.Serial: """ Try to open serial port with Arduino If not port is specified, it will be automatically detected - :param serial_port: (str) - :param baudrate: (int) - :param timeout: (int) None -> blocking mode - :param write_timeout: (int) + + :param serial_port: + :param baudrate: + :param timeout: None -> blocking mode + :param write_timeout: :return: (Serial Object) """ # Open serial port (for communication with Arduino) diff --git a/setup.py b/setup.py index c1eafd0..5345abe 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,36 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup +long_description = """ +Robust Arduino Serial is a simple and robust serial communication protocol. +It was designed to make two arduinos communicate, +but can also be useful when you want a computer (e.g. a Raspberry Pi) to communicate with an Arduino. +https://medium.com/@araffin/simple-and-robust-computer-arduino-serial-communication-f91b95596788 +""" -setup(name="robust_serial", - packages=[package for package in find_packages() - if package.startswith('robust_serial')], - install_requires=[ - 'pyserial', - 'enum34' - ], - tests_require=['pytest'], - author="Antonin RAFFIN", - author_email="antonin.raffin@ensta.org", - url="https://github.com/araffin/", - description="Simple and Robust Serial Communication Protocol", - keywords="serial hardware arduino RS232 communication protocol raspberry", - license="MIT", - version="0.1", - zip_safe=False) +setup( + name="robust_serial", + packages=[package for package in find_packages() if package.startswith("robust_serial")], + install_requires=[ + "pyserial", + ], + extras_require={ + "tests": ["pytest", "pytest-cov", "mypy", "ruff", "black", "isort"], + "docs": ["sphinx", "sphinx_rtd_theme", "sphinx-autodoc-typehints"], + }, + author="Antonin RAFFIN", + author_email="antonin.raffin@ensta.org", + url="https://github.com/araffin/arduino-robust-serial", + description="Simple and Robust Serial Communication Protocol", + long_description=long_description, + keywords="serial hardware arduino RS232 communication protocol raspberry", + license="MIT", + version="0.2", + python_requires=">=3.8", + classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + zip_safe=False, +) diff --git a/tests/test_read_write.py b/tests/test_read_write.py index b55a94a..0d2ea2c 100644 --- a/tests/test_read_write.py +++ b/tests/test_read_write.py @@ -1,11 +1,10 @@ -from __future__ import print_function, division, absolute_import from tempfile import TemporaryFile -from robust_serial import Order, write_order, read_order, write_i8, write_i16, write_i32, read_i8, read_i16, read_i32 +from robust_serial import Order, read_i8, read_i16, read_i32, read_order, write_i8, write_i16, write_i32, write_order def assert_eq(left, right): - assert left == right, "{} != {}".format(left, right) + assert left == right, f"{left} != {right}" def test_read_write_orders():