├── .github ├── dependabot.yml └── workflows │ └── test_and_publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst ├── installation.rst ├── make.bat ├── modules.rst ├── modules │ └── hdlcontroller.rst ├── overview.rst └── usage.rst ├── hdlcontroller ├── __init__.py ├── __main__.py ├── cli.py └── hdlcontroller.py ├── pyproject.toml ├── tests ├── __init__.py └── unit │ ├── __init__.py │ └── test_controller.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: pip 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | time: "09:00" 9 | timezone: Europe/Dublin 10 | open-pull-requests-limit: 10 11 | target-branch: develop 12 | 13 | - package-ecosystem: github-actions 14 | directory: "/" 15 | schedule: 16 | interval: weekly 17 | time: "09:00" 18 | timezone: Europe/Dublin 19 | open-pull-requests-limit: 10 20 | target-branch: develop 21 | -------------------------------------------------------------------------------- /.github/workflows/test_and_publish.yml: -------------------------------------------------------------------------------- 1 | name: Test and Publish 2 | 3 | on: 4 | push: 5 | 6 | pull_request: 7 | branches: [ 'develop' ] 8 | 9 | jobs: 10 | is-duplicate: 11 | name: Is Duplicate 12 | runs-on: ubuntu-latest 13 | outputs: 14 | should_skip: ${{ steps.skip-check.outputs.should_skip }} 15 | permissions: 16 | actions: write 17 | contents: read 18 | 19 | steps: 20 | - id: skip-check 21 | name: Skip Check 22 | uses: fkirc/skip-duplicate-actions@master 23 | with: 24 | paths_ignore: '["**.rst", "**.md", "**.txt"]' 25 | 26 | test-code: 27 | name: Test code 28 | runs-on: ${{ matrix.os }} 29 | needs: is-duplicate 30 | if: needs.is-duplicate.outputs.should_skip != 'true' 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest, macos-latest, windows-latest] 34 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 35 | 36 | steps: 37 | - name: Check out code 38 | uses: actions/checkout@v4 39 | 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v5 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | 45 | - name: Install dependencies 46 | run: | 47 | python -m pip install --upgrade pip 48 | pip install tox 49 | 50 | - name: Test with Tox 51 | run: tox 52 | 53 | test-docs: 54 | name: Test documentation 55 | runs-on: ubuntu-latest 56 | needs: is-duplicate 57 | if: needs.is-duplicate.outputs.should_skip != 'true' 58 | env: 59 | PYTHON_VERSION: '3.x' 60 | steps: 61 | - name: Check out code 62 | uses: actions/checkout@v4 63 | 64 | - name: Set up Python ${{ env.PYTHON_VERSION }} 65 | uses: actions/setup-python@v5 66 | with: 67 | python-version: ${{ env.PYTHON_VERSION }} 68 | 69 | - name: Install dependencies 70 | run: | 71 | python -m pip install --upgrade pip 72 | pip install .[docs] 73 | 74 | - name: Build documentation 75 | working-directory: docs 76 | run: make html 77 | 78 | publish-to-test-pypi: 79 | name: Publish to TestPyPI 80 | runs-on: ubuntu-latest 81 | environment: 82 | name: staging 83 | url: https://test.pypi.org/project/hdlcontroller/ 84 | permissions: 85 | # Required for trusted publishing on PyPI. 86 | id-token: write 87 | needs: [test-code, test-docs] 88 | if: | 89 | !failure() && 90 | github.event_name == 'push' && 91 | startsWith(github.ref, 'refs/tags/v') 92 | 93 | steps: 94 | - name: Check out code 95 | uses: actions/checkout@v4 96 | 97 | - name: Set up Python 98 | uses: actions/setup-python@v5 99 | with: 100 | python-version: '3.x' 101 | 102 | - name: Install dependencies 103 | run: | 104 | python -m pip install --upgrade pip 105 | pip install --upgrade build setuptools wheel 106 | 107 | - name: Build Python package 108 | run: python -m build 109 | 110 | - name: Publish to TestPyPI 111 | uses: pypa/gh-action-pypi-publish@v1.8.14 112 | with: 113 | repository-url: https://test.pypi.org/legacy/ 114 | print-hash: true 115 | 116 | publish-to-pypi: 117 | name: Publish to PyPI 118 | runs-on: ubuntu-latest 119 | environment: 120 | name: production 121 | url: https://pypi.org/project/hdlcontroller/ 122 | permissions: 123 | # Required for trusted publishing on PyPI. 124 | id-token: write 125 | needs: [publish-to-test-pypi] 126 | if: | 127 | !failure() && 128 | github.event_name == 'push' && 129 | startsWith(github.ref, 'refs/tags/v') 130 | 131 | steps: 132 | - name: Check out code 133 | uses: actions/checkout@v4 134 | 135 | - name: Set up Python 136 | uses: actions/setup-python@v5 137 | with: 138 | python-version: '3.x' 139 | 140 | - name: Install dependencies 141 | run: | 142 | python -m pip install --upgrade pip 143 | pip install --upgrade build setuptools wheel 144 | 145 | - name: Build Python package 146 | run: python -m build 147 | 148 | - name: Publish to PyPI 149 | uses: pypa/gh-action-pypi-publish@v1.8.14 150 | with: 151 | print-hash: true 152 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # Others 62 | *.swp 63 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: f71fa2c1f9cf5cb705f73dffe4b21f7c61470ba9 # frozen: v4.4.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-yaml 7 | - id: trailing-whitespace 8 | 9 | - repo: https://github.com/charliermarsh/ruff-pre-commit 10 | rev: e812d61e6e9d269f44ecda63904ed670a1948fe8 # frozen: v0.0.257 11 | hooks: 12 | - id: ruff 13 | 14 | - repo: https://github.com/pycqa/isort 15 | rev: dbf82f2dd09ae41d9355bcd7ab69187a19e6bf2f # frozen: 5.12.0 16 | hooks: 17 | - id: isort 18 | 19 | - repo: https://github.com/psf/black 20 | rev: b0d1fba7ac3be53c71fb0d3211d911e629f8aecb # frozen: 23.1.0 21 | hooks: 22 | - id: black 23 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | python: 2 | version: 3 3 | pip_install: true 4 | extra_requirements: 5 | - docs 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 Paul-Emmanuel Raoul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Python HDLC Controller 3 | ====================== 4 | 5 | |PyPI Package| |PyPI Downloads| |PyPI Python Versions| |Build Status| 6 | |Documentation Status| 7 | 8 | HDLC_ controller written in Python and based on the `python4yahdlc 9 | `__ Python module to encode and 10 | decode the HDLC frames. 11 | 12 | Installation 13 | ============ 14 | 15 | From PyPI (recommended) 16 | ----------------------- 17 | 18 | :: 19 | 20 | pip3 install --upgrade hdlcontroller 21 | 22 | From sources 23 | ------------ 24 | 25 | :: 26 | 27 | git clone https://github.com/SkypLabs/python-hdlc-controller.git 28 | cd python-hdlc-controller 29 | pip3 install --upgrade . 30 | 31 | Documentation 32 | ============= 33 | 34 | The full documentation is available `here 35 | `__. 36 | 37 | License 38 | ======= 39 | 40 | `MIT `__ 41 | 42 | .. _HDLC: https://en.wikipedia.org/wiki/High-Level_Data_Link_Control 43 | 44 | .. |Build Status| image:: https://github.com/SkypLabs/python-hdlc-controller/actions/workflows/test_and_publish.yml/badge.svg?branch=develop 45 | :target: https://github.com/SkypLabs/python-hdlc-controller/actions/workflows/test_and_publish.yml?branch=develop 46 | :alt: Build Status Develop Branch 47 | 48 | .. |Documentation Status| image:: https://readthedocs.org/projects/python-hdlc-controller/badge/?version=latest 49 | :target: https://python-hdlc-controller.readthedocs.io/en/latest/?badge=latest 50 | :alt: Documentation Status 51 | 52 | .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/hdlcontroller.svg?style=flat 53 | :target: https://pypi.org/project/hdlcontroller/ 54 | :alt: PyPI Package Downloads Per Month 55 | 56 | .. |PyPI Package| image:: https://badge.fury.io/py/hdlcontroller.svg 57 | :target: https://pypi.org/project/hdlcontroller/ 58 | :alt: PyPI Package Latest Release 59 | 60 | .. |PyPI Python Versions| image:: https://img.shields.io/pypi/pyversions/hdlcontroller.svg?logo=python&style=flat 61 | :target: https://pypi.org/project/hdlcontroller/ 62 | :alt: PyPI Package Python Versions 63 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = HDLController 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath("..")) 18 | 19 | from hdlcontroller import __version__ as VERSION 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "HDLController" 24 | copyright = "2015, Paul-Emmanuel Raoul" 25 | author = "Paul-Emmanuel Raoul" 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = VERSION 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.graphviz", 43 | "sphinx.ext.mathjax", 44 | "sphinx.ext.todo", 45 | "sphinx.ext.viewcode", 46 | "sphinxarg.ext", 47 | "sphinxcontrib.seqdiag", 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = ".rst" 58 | 59 | # The master toctree document. 60 | master_doc = "index" 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = "en" 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This pattern also affects html_static_path and html_extra_path . 72 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = "sphinx" 76 | 77 | 78 | # -- Options for HTML output ------------------------------------------------- 79 | 80 | # The theme to use for HTML and HTML Help pages. See the documentation for 81 | # a list of builtin themes. 82 | # 83 | html_theme = "sphinx_rtd_theme" 84 | 85 | # Theme options are theme-specific and customize the look and feel of a theme 86 | # further. For a list of options available for each theme, see the 87 | # documentation. 88 | # 89 | # html_theme_options = {} 90 | 91 | # Add any paths that contain custom static files (such as style sheets) here, 92 | # relative to this directory. They are copied after the builtin static files, 93 | # so a file named "default.css" will overwrite the builtin "default.css". 94 | html_static_path = [] 95 | 96 | # Custom sidebar templates, must be a dictionary that maps document names 97 | # to template names. 98 | # 99 | # The default sidebars (for documents that don't match any pattern) are 100 | # defined by theme itself. Builtin themes are using these templates by 101 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 102 | # 'searchbox.html']``. 103 | # 104 | # html_sidebars = {} 105 | 106 | 107 | # -- Options for HTMLHelp output --------------------------------------------- 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = "HDLControllerdoc" 111 | 112 | 113 | # -- Options for LaTeX output ------------------------------------------------ 114 | 115 | latex_elements = { 116 | # The paper size ('letterpaper' or 'a4paper'). 117 | # 118 | # 'papersize': 'letterpaper', 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | # Additional stuff for the LaTeX preamble. 123 | # 124 | # 'preamble': '', 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | ( 135 | master_doc, 136 | "HDLController.tex", 137 | "HDLController Documentation", 138 | "Paul-Emmanuel Raoul", 139 | "manual", 140 | ), 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [(master_doc, "hdlcontroller", "HDLController Documentation", [author], 1)] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | ( 158 | master_doc, 159 | "HDLController", 160 | "HDLController Documentation", 161 | author, 162 | "HDLController", 163 | "One line description of project.", 164 | "Miscellaneous", 165 | ), 166 | ] 167 | 168 | 169 | # -- Options for Epub output ------------------------------------------------- 170 | 171 | # Bibliographic Dublin Core info. 172 | epub_title = project 173 | epub_author = author 174 | epub_publisher = author 175 | epub_copyright = copyright 176 | 177 | # The unique identifier of the text. This can be a ISBN number 178 | # or the project homepage. 179 | # 180 | # epub_identifier = '' 181 | 182 | # A unique identification for the text. 183 | # 184 | # epub_uid = '' 185 | 186 | # A list of files that should not be packed into the epub file. 187 | epub_exclude_files = ["search.html"] 188 | 189 | 190 | # -- Extension configuration ------------------------------------------------- 191 | 192 | # -- Options for todo extension ---------------------------------------------- 193 | 194 | # If true, `todo` and `todoList` produce output, else they produce nothing. 195 | todo_include_todos = True 196 | 197 | # -- Options for sphinxcontrib-seqdiag extension ----------------------------- 198 | 199 | # Fontpath for seqdiag (truetype font). 200 | seqdiag_fontpath = "/usr/share/fonts/truetype/ipafont/ipagp.ttf" 201 | 202 | # -- Options for GitHub integration ------------------------------------------ 203 | 204 | html_context = { 205 | "display_github": True, # Integrate GitHub 206 | "github_user": "SkypLabs", # Username 207 | "github_repo": "python-hdlc-controller", # Repo name 208 | "github_version": "main", # Version 209 | "conf_py_path": "/docs/", # Path in the checkout to the docs root 210 | } 211 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. HDLController documentation master file, created by 2 | sphinx-quickstart on Thu Jun 7 16:41:51 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to HDLController's documentation! 7 | ========================================= 8 | 9 | HDLController is an HDLC controller written in Python and based on the 10 | `python4yahdlc `__ Python module to 11 | encode and decode the HDLC frames. 12 | 13 | .. toctree:: 14 | :caption: Table of Contents 15 | 16 | overview 17 | installation 18 | usage 19 | modules 20 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | From PyPI (recommanded) 6 | ----------------------- 7 | 8 | :: 9 | 10 | pip3 install --upgrade hdlcontroller 11 | 12 | From sources 13 | ------------ 14 | 15 | HDLController is packaged with Setuptools_. 16 | 17 | The default Git branch is ``develop``. To install the latest stable version, 18 | you need to clone the ``main`` branch. 19 | 20 | :: 21 | 22 | git clone https://github.com/SkypLabs/python-hdlc-controller.git 23 | cd python-hdlc-controller 24 | pip3 install --upgrade . 25 | 26 | .. _Setuptools: https://setuptools.pypa.io/ 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=HDLController 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Modules 3 | ======= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | :glob: 8 | 9 | modules/* 10 | -------------------------------------------------------------------------------- /docs/modules/hdlcontroller.rst: -------------------------------------------------------------------------------- 1 | HDLController 2 | ------------- 3 | 4 | .. automodule:: hdlcontroller.hdlcontroller 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | The HDLC controller supports the following frames: 6 | 7 | - DATA (I-frame_ with Poll bit) 8 | - ACK (`S-frame Receive Ready`_ with Final bit) 9 | - NACK (`S-frame Reject`_ with Final bit) 10 | 11 | Each DATA frame must be positively or negatively acknowledged using 12 | respectively an ACK or NACK frame. The highest sequence number is 7. As a 13 | result, when sending a DATA frame, the expected acknowledgment sequence number 14 | is ``seq_no + 1 % MAX_SEQ_NO`` with ``MAX_SEQ_NO = 8``. 15 | 16 | .. seqdiag:: 17 | 18 | seqdiag { 19 | activation = None; 20 | default_note_color = lightblue; 21 | 22 | "A" ->> "B" [label = "DATA [Seq No = 1]", note = "The expected ACK frame's sequence number 23 | is 1 + 1 % MAX_SEQ_NO = 2"] 24 | "A" ->> "B" [label = "DATA [Seq No = 2]"] 25 | "A" <<- "B" [label = "ACK [Seq No = 2]", note = "ACK of the DATA frame's sequence number 1"] 26 | "A" <<- "B" [label = "ACK [Seq No = 3]", note = "ACK of the DATA frame's sequence number 2"] 27 | } 28 | 29 | The number of DATA frames that can be sent before receiving the first 30 | acknowledgment is determined by the ``window`` parameter of 31 | :py:class:`HDLController `. Its 32 | default value is 3. 33 | 34 | If the FCS_ field of a received frame is not valid, an NACK will be sent back 35 | with the same sequence number as the one of the corrupted frame to notify the 36 | sender about it: 37 | 38 | .. seqdiag:: 39 | 40 | seqdiag { 41 | activation = None; 42 | 43 | "A" ->> "B" [label = "DATA [Seq No = 1]"] 44 | "A" <<- "B" [label = "NACK [Seq No = 1]"] 45 | "A" ->> "B" [label = "DATA [Seq No = 1]"] 46 | } 47 | 48 | For each DATA frame sent, a timer is started. If the timer ends before 49 | receiving any corresponding ACK and NACK frame, the DATA frame will be sent 50 | again: 51 | 52 | .. seqdiag:: 53 | 54 | seqdiag { 55 | activation = None; 56 | default_note_color = lightblue; 57 | 58 | "A" ->> "B" [label = "DATA [Seq No = 1]"] 59 | "A" <<- "B" [failed, note = "No ACK/NACK received before the end of 60 | the timer"] 61 | "A" ->> "B" [label = "DATA [Seq No = 1]"] 62 | } 63 | 64 | The default timer value is 2 seconds and can be changed using the 65 | ``sending_timeout`` parameter of :py:class:`HDLController 66 | `. 67 | 68 | .. _FCS: https://en.wikipedia.org/wiki/Frame_check_sequence 69 | .. _I-frame: https://en.wikipedia.org/wiki/High-Level_Data_Link_Control#I-Frames_(user_data) 70 | .. _S-frame Receive Ready: https://en.wikipedia.org/wiki/High-Level_Data_Link_Control#Receive_Ready_(RR) 71 | .. _S-frame Reject: https://en.wikipedia.org/wiki/High-Level_Data_Link_Control#Reject_(REJ) 72 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To create a new HDLC controller instance, you need to call the 6 | :py:class:`HDLController ` class 7 | with two parameters: 8 | 9 | .. code-block:: python 10 | 11 | hdlc_c = HDLController(read_func, write_func) 12 | 13 | The first parameter is a function used to read from the serial bus while the 14 | second parameter is a function used to write on it. For example, using the 15 | pyserial_ module: 16 | 17 | .. code-block:: python 18 | 19 | ser = serial.Serial('/dev/ttyACM0') 20 | 21 | def read_serial(): 22 | return ser.read(ser.in_waiting) 23 | 24 | hdlc_c = HDLController(read_serial, ser.write) 25 | 26 | To start the reception thread: 27 | 28 | .. code-block:: python 29 | 30 | hdlc_c.start() 31 | 32 | To send a new data frame: 33 | 34 | .. code-block:: python 35 | 36 | hdlc_c.send('Hello world!') 37 | 38 | And to get the next received data frame available in the 39 | :py:class:`HDLController ` internal 40 | queue: 41 | 42 | .. code-block:: python 43 | 44 | data = hdlc_c.get_data() 45 | 46 | The :py:meth:`get_data() ` 47 | method will block until a new data frame is available. 48 | 49 | Finally, to stop all the :py:class:`HDLController 50 | ` threads: 51 | 52 | .. code-block:: python 53 | 54 | hdlc_c.stop() 55 | 56 | .. _pyserial: https://pythonhosted.org/pyserial/ 57 | -------------------------------------------------------------------------------- /hdlcontroller/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | HDLC Controller package. 3 | """ 4 | 5 | from pkg_resources import get_distribution 6 | 7 | __version__ = get_distribution("hdlcontroller").version 8 | -------------------------------------------------------------------------------- /hdlcontroller/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Executes the command-line tool when run as a script or with 'python -m'. 3 | """ 4 | 5 | from .cli import main 6 | 7 | main() 8 | -------------------------------------------------------------------------------- /hdlcontroller/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI module. 3 | """ 4 | 5 | from argparse import ArgumentParser 6 | from sys import exit as sys_exit 7 | from sys import stderr, stdout 8 | from time import sleep 9 | 10 | import serial 11 | 12 | from hdlcontroller.hdlcontroller import HDLController 13 | 14 | 15 | def get_arg_parser(): 16 | """ 17 | Returns the argument parser. 18 | """ 19 | 20 | arg_parser = ArgumentParser( 21 | description="HDLC controller example", 22 | epilog=""" 23 | Example: hdlc-tester -d /dev/ttyUSB0 -b 115200 -m 'Hello world!' 24 | """, 25 | ) 26 | 27 | arg_parser.add_argument( 28 | "-b", 29 | "--baudrate", 30 | type=int, 31 | default="9600", 32 | help="serial baudrate value in bauds per second (default: 9600)", 33 | ) 34 | 35 | arg_parser.add_argument( 36 | "-d", 37 | "--device", 38 | default="/dev/ttyACM0", 39 | help="serial device to use (default: /dev/ttyACM0)", 40 | ) 41 | 42 | arg_parser.add_argument( 43 | "-i", 44 | "--interval", 45 | type=float, 46 | default="1.0", 47 | help=""" 48 | sending interval between two data frames in seconds (default: 1.0) 49 | """, 50 | ) 51 | 52 | arg_parser.add_argument( 53 | "-m", 54 | "--message", 55 | default="test", 56 | help="test message to send (default: test)", 57 | ) 58 | 59 | arg_parser.add_argument( 60 | "-N", 61 | "--no-fcs-nack", 62 | action="store_true", 63 | help=""" 64 | do not send back an NACK when a corrupted frame is received 65 | (default: false) 66 | """, 67 | ) 68 | 69 | arg_parser.add_argument( 70 | "-q", 71 | "--quiet", 72 | action="store_true", 73 | help=""" 74 | do not send anything, just display what is received (default: false) 75 | """, 76 | ) 77 | 78 | arg_parser.add_argument( 79 | "-Q", 80 | "--queue-size", 81 | type=int, 82 | default="0", 83 | help="queue size for data frames received (default: 0)", 84 | ) 85 | 86 | arg_parser.add_argument( 87 | "-t", 88 | "--serial-timeout", 89 | type=int, 90 | default="0", 91 | help="serial read timeout value in seconds (default: 0)", 92 | ) 93 | 94 | arg_parser.add_argument( 95 | "-T", 96 | "--sending-timeout", 97 | type=float, 98 | default="2.0", 99 | help="HDLC sending timeout value in seconds (default: 2.0)", 100 | ) 101 | 102 | arg_parser.add_argument( 103 | "-w", 104 | "--window", 105 | type=int, 106 | default="3", 107 | help="sending window (default: 3)", 108 | ) 109 | 110 | arg_parser.set_defaults( 111 | quiet=False, 112 | no_fcs_nack=False, 113 | ) 114 | 115 | return arg_parser 116 | 117 | 118 | def main(): 119 | """ 120 | Entry point of the command-line tool. 121 | """ 122 | 123 | args = vars(get_arg_parser().parse_args()) 124 | 125 | # Serial port configuration 126 | ser = serial.Serial() 127 | ser.port = args["device"] 128 | ser.baudrate = args["baudrate"] 129 | ser.timeout = args["serial_timeout"] 130 | 131 | stdout.write("[*] Connection...\n") 132 | 133 | try: 134 | ser.open() 135 | except serial.SerialException as err: 136 | stderr.write("[x] Serial connection problem: {0}\n".format(err)) 137 | sys_exit(1) 138 | 139 | def read_uart(): 140 | return ser.read(ser.in_waiting) 141 | 142 | def send_callback(data): 143 | print("> {0}".format(data)) 144 | 145 | def receive_callback(data): 146 | print("< {0}".format(data)) 147 | 148 | try: 149 | hdlc_c = HDLController( 150 | read_uart, 151 | ser.write, 152 | window=args["window"], 153 | sending_timeout=args["sending_timeout"], 154 | frames_queue_size=args["queue_size"], 155 | fcs_nack=not (args["no_fcs_nack"]), 156 | ) 157 | hdlc_c.set_send_callback(send_callback) 158 | hdlc_c.set_receive_callback(receive_callback) 159 | hdlc_c.start() 160 | 161 | while True: 162 | if not args["quiet"]: 163 | hdlc_c.send(args["message"]) 164 | 165 | sleep(args["interval"]) 166 | except KeyboardInterrupt: 167 | stdout.write("[*] Bye!\n") 168 | finally: 169 | if "hdlc_c" in locals(): 170 | hdlc_c.stop() # type: ignore 171 | 172 | ser.close() 173 | -------------------------------------------------------------------------------- /hdlcontroller/hdlcontroller.py: -------------------------------------------------------------------------------- 1 | from queue import Full, Queue 2 | from threading import Event, Lock, Thread 3 | from time import sleep, time 4 | from typing import Callable, Dict, NewType, Union 5 | 6 | from yahdlc import ( 7 | FRAME_ACK, 8 | FRAME_DATA, 9 | FRAME_NACK, 10 | FCSError, 11 | MessageError, 12 | frame_data, 13 | get_data, 14 | ) 15 | 16 | SequenceNumber = NewType("SequenceNumber", int) 17 | Timeout = NewType("Timeout", float) 18 | 19 | ReadFunction = Callable[[], bytes] 20 | WriteFunction = Callable[[bytes], Union[int, None]] 21 | 22 | Callback = Callable[[bytes], None] 23 | 24 | 25 | class HDLController: 26 | """ 27 | An HDLC controller based on python4yahdlc. 28 | """ 29 | 30 | MAX_SEQ_NO = 8 31 | MIN_SENDING_TIMEOUT = 0.5 32 | 33 | def __init__( 34 | self, 35 | read_func: ReadFunction, 36 | write_func: WriteFunction, 37 | sending_timeout: Timeout = Timeout(2.0), 38 | window: int = 3, 39 | frames_queue_size: int = 0, 40 | fcs_nack: bool = True, 41 | ): 42 | if not callable(read_func): 43 | raise TypeError("'read_func' is not callable") 44 | 45 | if not callable(write_func): 46 | raise TypeError("'write_func' is not callable") 47 | 48 | self.read: ReadFunction = read_func 49 | self.write: WriteFunction = write_func 50 | 51 | self.window: int = window 52 | self.fcs_nack: bool = fcs_nack 53 | self.senders: Dict[SequenceNumber, HDLController.Sender] = {} 54 | self.send_lock: Lock = Lock() 55 | self.new_seq_no: SequenceNumber = SequenceNumber(0) 56 | 57 | self.send_callback: Union[Callback, None] = None 58 | self.receive_callback: Union[Callback, None] = None 59 | 60 | self.set_sending_timeout(sending_timeout) 61 | 62 | self.receiver: Union[HDLController.Receiver, None] = None 63 | self.frames_received: Queue = Queue(maxsize=frames_queue_size) 64 | 65 | def start(self) -> None: 66 | """ 67 | Starts HDLC controller's threads. 68 | """ 69 | 70 | self.receiver = self.Receiver( 71 | self.read, 72 | self.write, 73 | self.send_lock, 74 | self.senders, 75 | self.frames_received, 76 | callback=self.receive_callback, 77 | fcs_nack=self.fcs_nack, 78 | ) 79 | 80 | self.receiver.start() 81 | 82 | def stop(self) -> None: 83 | """ 84 | Stops HDLC controller's threads. 85 | """ 86 | 87 | if self.receiver is not None: 88 | self.receiver.join() 89 | 90 | for sender in self.senders.values(): 91 | sender.join() 92 | 93 | def set_send_callback(self, callback: Callback) -> None: 94 | """ 95 | Sets the send callback function. 96 | 97 | If the HDLC controller has already been started, the new callback 98 | function will be taken into account for the next data frames to be 99 | sent. 100 | """ 101 | 102 | if not callable(callback): 103 | raise TypeError("'callback' is not callable") 104 | 105 | self.send_callback = callback 106 | 107 | def set_receive_callback(self, callback: Callback) -> None: 108 | """ 109 | Sets the receive callback function. 110 | 111 | This method has to be called before starting the HDLC controller. 112 | """ 113 | 114 | if not callable(callback): 115 | raise TypeError("'callback' is not callable") 116 | 117 | self.receive_callback = callback 118 | 119 | def set_sending_timeout(self, sending_timeout: Timeout) -> None: 120 | """ 121 | Sets the sending timeout. 122 | """ 123 | 124 | if sending_timeout >= HDLController.MIN_SENDING_TIMEOUT: 125 | self.sending_timeout = sending_timeout 126 | 127 | def get_senders_number(self) -> int: 128 | """ 129 | Returns the number of active senders. 130 | """ 131 | 132 | return len(self.senders) 133 | 134 | def send(self, data: bytes) -> None: 135 | """ 136 | Sends a new data frame. 137 | 138 | This method will block until a new room is available for a new sender. 139 | This limit is determined by the size of the window. 140 | """ 141 | 142 | while len(self.senders) >= self.window: 143 | pass 144 | 145 | self.senders[self.new_seq_no] = self.Sender( 146 | self.write, 147 | self.send_lock, 148 | data, 149 | self.new_seq_no, 150 | timeout=self.sending_timeout, 151 | callback=self.send_callback, 152 | ) 153 | 154 | self.senders[self.new_seq_no].start() 155 | self.new_seq_no = SequenceNumber( 156 | (self.new_seq_no + 1) % HDLController.MAX_SEQ_NO 157 | ) 158 | 159 | def get_data(self) -> bytes: 160 | """ 161 | Gets the next frame received. 162 | 163 | This method will block until a new data frame is available. 164 | """ 165 | 166 | return self.frames_received.get() 167 | 168 | class Sender(Thread): 169 | """ 170 | Thread used to send HDLC frames. 171 | """ 172 | 173 | def __init__( 174 | self, 175 | write_func: WriteFunction, 176 | send_lock: Lock, 177 | data: bytes, 178 | seq_no: SequenceNumber, 179 | timeout: Timeout = Timeout(2.0), 180 | callback: Union[Callback, None] = None, 181 | ): 182 | super().__init__() 183 | self.write: WriteFunction = write_func 184 | self.send_lock: Lock = send_lock 185 | self.data: bytes = data 186 | self.seq_no: SequenceNumber = seq_no 187 | self.timeout: Timeout = timeout 188 | self.callback: Union[Callback, None] = callback 189 | 190 | self.stop_sender: Event = Event() 191 | self.stop_timeout: Event = Event() 192 | self.next_timeout: Timeout = Timeout(0.0) 193 | 194 | def run(self) -> None: 195 | while not self.stop_sender.is_set(): 196 | self.stop_timeout.wait(max(0, self.next_timeout - time())) 197 | self.stop_timeout.clear() 198 | 199 | if not self.stop_sender.is_set(): 200 | self.next_timeout = Timeout(time() + self.timeout) 201 | 202 | with self.send_lock: 203 | self.__send_data() 204 | 205 | def join(self, timeout: Union[Timeout, None] = None) -> None: 206 | """ 207 | Stops the current thread. 208 | """ 209 | 210 | self.stop_sender.set() 211 | self.stop_timeout.set() 212 | super().join(timeout) 213 | 214 | def ack_received(self) -> None: 215 | """ 216 | Informs the sender that the related ACK frame has been received. 217 | As a consequence, the current thread is being stopped. 218 | """ 219 | 220 | self.join() 221 | 222 | def nack_received(self) -> None: 223 | """ 224 | Informs the sender that an NACK frame has been received. As a 225 | consequence, the data frame is being resent. 226 | """ 227 | 228 | self.stop_timeout.set() 229 | 230 | def __send_data(self) -> None: 231 | """ 232 | Sends a new data frame. 233 | """ 234 | 235 | if self.callback is not None: 236 | self.callback(self.data) 237 | 238 | self.write(frame_data(self.data, FRAME_DATA, self.seq_no)) 239 | 240 | class Receiver(Thread): 241 | """ 242 | Thread used to receive HDLC frames. 243 | """ 244 | 245 | def __init__( 246 | self, 247 | read_func: ReadFunction, 248 | write_func: WriteFunction, 249 | send_lock: Lock, 250 | senders_list: Dict[SequenceNumber, "HDLController.Sender"], 251 | frames_received: Queue, 252 | callback: Union[Callback, None] = None, 253 | fcs_nack: bool = True, 254 | ): 255 | super().__init__() 256 | self.read: ReadFunction = read_func 257 | self.write: WriteFunction = write_func 258 | self.send_lock: Lock = send_lock 259 | self.senders: Dict[SequenceNumber, "HDLController.Sender"] = senders_list 260 | self.frames_received: Queue = frames_received 261 | self.callback: Union[Callback, None] = callback 262 | self.fcs_nack: bool = fcs_nack 263 | 264 | self.stop_receiver: Event = Event() 265 | 266 | def run(self): 267 | while not self.stop_receiver.is_set(): 268 | try: 269 | data, ftype, seq_no = get_data(self.read()) 270 | 271 | if ftype == FRAME_DATA: 272 | with self.send_lock: 273 | if self.callback is not None: 274 | self.callback(data) 275 | 276 | self.frames_received.put_nowait(data) 277 | self.__send_ack((seq_no + 1) % HDLController.MAX_SEQ_NO) 278 | elif ftype == FRAME_ACK: 279 | seq_no_sent = (seq_no - 1) % HDLController.MAX_SEQ_NO 280 | self.senders[seq_no_sent].ack_received() 281 | del self.senders[seq_no_sent] 282 | elif ftype == FRAME_NACK: 283 | self.senders[seq_no].nack_received() 284 | else: 285 | raise TypeError("Bad frame type received") 286 | except MessageError: 287 | # No HDLC frame detected. 288 | pass 289 | except KeyError: 290 | # Drops bad (N)ACKs. 291 | pass 292 | except Full: 293 | # Drops new data frames when the receive queue is full. 294 | pass 295 | except FCSError as err: 296 | # Sends back an NACK if a corrupted frame is received and 297 | # if the FCS NACK option is enabled. 298 | if self.fcs_nack: 299 | with self.send_lock: 300 | self.__send_nack(err.args[0]) 301 | except TypeError: 302 | # Generally, raised when an HDLC frame with a bad frame 303 | # type is received. 304 | pass 305 | finally: 306 | # 200 µs. 307 | sleep(200 / 1000000.0) 308 | 309 | def join(self, timeout: Union[Timeout, None] = None): 310 | """ 311 | Stops the current thread. 312 | """ 313 | 314 | self.stop_receiver.set() 315 | super().join(timeout) 316 | 317 | def __send_ack(self, seq_no: SequenceNumber): 318 | """ 319 | Sends a new ACK frame. 320 | """ 321 | 322 | self.write(frame_data("", FRAME_ACK, seq_no)) 323 | 324 | def __send_nack(self, seq_no: SequenceNumber): 325 | """ 326 | Sends a new NACK frame. 327 | """ 328 | 329 | self.write(frame_data("", FRAME_NACK, seq_no)) 330 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "hdlcontroller" 3 | version = "0.5.2" 4 | description = "HDLC controller" 5 | readme = "README.rst" 6 | requires-python = ">= 3.7, <4" 7 | license = {file = "LICENSE"} 8 | keywords = ["hdlc", "controller"] 9 | authors = [ 10 | {name = "Paul-Emmanuel Raoul", email = "skyper@skyplabs.net"}, 11 | ] 12 | 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "Intended Audience :: Developers", 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | "Programming Language :: Python :: 3 :: Only", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "License :: OSI Approved :: MIT License", 24 | ] 25 | 26 | dependencies = [ 27 | "python4yahdlc >= 1.3.5", 28 | "pyserial >= 3.0", 29 | ] 30 | 31 | [project.optional-dependencies] 32 | dev = [ 33 | "black", 34 | "isort", 35 | "ruff", 36 | ] 37 | 38 | docs = [ 39 | "sphinx >= 1.4.0", 40 | "sphinxcontrib-seqdiag >= 0.8.5", 41 | "sphinx-argparse >= 0.2.2", 42 | "sphinx_rtd_theme", 43 | ] 44 | 45 | tests = [ 46 | "tox" 47 | ] 48 | 49 | [project.urls] 50 | documentation = "https://python-hdlc-controller.readthedocs.io/en/latest" 51 | repository = "https://github.com/SkypLabs/python-hdlc-controller" 52 | 53 | [project.scripts] 54 | hdlc-tester = "hdlcontroller.cli:main" 55 | 56 | [tool.isort] 57 | profile = "black" 58 | 59 | [build-system] 60 | requires = [ 61 | "setuptools >= 42", 62 | "wheel", 63 | ] 64 | build-backend = "setuptools.build_meta" 65 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkypLabs/pyhdlcontroller/498ef44da7767e456bcfe3d267ee0fad7170f4eb/tests/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkypLabs/pyhdlcontroller/498ef44da7767e456bcfe3d267ee0fad7170f4eb/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_controller.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the HDLC controller. 3 | """ 4 | 5 | import unittest 6 | from time import sleep 7 | 8 | from yahdlc import FRAME_ACK, FRAME_DATA, FRAME_NACK, frame_data 9 | 10 | from hdlcontroller.hdlcontroller import HDLController, Timeout 11 | 12 | 13 | class TestHDLCController(unittest.TestCase): 14 | """ 15 | Tests the HDLC Controller. 16 | """ 17 | 18 | def test_without_parameters(self): 19 | """ 20 | Instantiates a new HDLC controller without parameters. 21 | """ 22 | 23 | with self.assertRaises(TypeError): 24 | _ = HDLController() # type: ignore 25 | 26 | def test_with_only_one_parameter(self): 27 | """ 28 | Instantiates a new HDLC controller without write function. 29 | """ 30 | 31 | def read_func() -> bytes: 32 | return b"test" 33 | 34 | with self.assertRaises(TypeError): 35 | _ = HDLController(read_func) # type: ignore 36 | 37 | def test_bad_read_function(self): 38 | """ 39 | Instantiates a new HDLC controller with an invalid read function. 40 | """ 41 | 42 | read_func = "not a function" 43 | 44 | def write_func(_: bytes) -> None: 45 | pass 46 | 47 | with self.assertRaises(TypeError): 48 | _ = HDLController(read_func, write_func) # type: ignore 49 | 50 | def test_bad_write_function(self): 51 | """ 52 | Instantiates a new HDLC controller with an invalid write function. 53 | """ 54 | 55 | write_func = "not a function" 56 | 57 | def read_func() -> bytes: 58 | return b"test" 59 | 60 | with self.assertRaises(TypeError): 61 | _ = HDLController(read_func, write_func) # type: ignore 62 | 63 | def test_stop_before_start(self): 64 | """ 65 | Stops the HDLC controller before it even started. 66 | """ 67 | 68 | def read_func() -> bytes: 69 | return b"test" 70 | 71 | def write_func(_: bytes) -> None: 72 | pass 73 | 74 | hdlc_c = HDLController(read_func, write_func) 75 | hdlc_c.stop() 76 | 77 | def test_send_one_frame(self): 78 | """ 79 | Tests the HDLC controller by sending one frame. 80 | """ 81 | 82 | def read_func() -> bytes: 83 | return b"test" 84 | 85 | def write_func(data: bytes) -> None: 86 | write_func.data = data 87 | 88 | write_func.data = None 89 | 90 | hdlc_c = HDLController(read_func, write_func) 91 | 92 | hdlc_c.send(b"test") 93 | while write_func.data is None: 94 | pass 95 | self.assertEqual(write_func.data, frame_data("test", FRAME_DATA, 0)) 96 | self.assertEqual(hdlc_c.get_senders_number(), 1) 97 | 98 | hdlc_c.stop() 99 | 100 | def test_send_three_frames(self): 101 | """ 102 | Tests the HDLC controller by sending three frame. 103 | """ 104 | 105 | def read_func() -> bytes: 106 | return b"test" 107 | 108 | def write_func(data: bytes) -> None: 109 | write_func.data = data 110 | 111 | hdlc_c = HDLController(read_func, write_func) 112 | 113 | write_func.data = None 114 | hdlc_c.send(b"test_1") 115 | while write_func.data is None: 116 | pass 117 | self.assertEqual(write_func.data, frame_data("test_1", FRAME_DATA, 0)) 118 | self.assertEqual(hdlc_c.get_senders_number(), 1) 119 | 120 | write_func.data = None 121 | hdlc_c.send(b"test2") 122 | while write_func.data is None: 123 | pass 124 | self.assertEqual(write_func.data, frame_data("test2", FRAME_DATA, 1)) 125 | self.assertEqual(hdlc_c.get_senders_number(), 2) 126 | 127 | write_func.data = None 128 | hdlc_c.send(b"test3") 129 | while write_func.data is None: 130 | pass 131 | self.assertEqual(write_func.data, frame_data("test3", FRAME_DATA, 2)) 132 | self.assertEqual(hdlc_c.get_senders_number(), 3) 133 | 134 | hdlc_c.stop() 135 | 136 | def test_send_one_frame_and_wait_timeout(self): 137 | """ 138 | Tests the timeout while sending one frame. 139 | """ 140 | 141 | def read_func() -> bytes: 142 | return b"test" 143 | 144 | def write_func(data: bytes) -> None: 145 | write_func.data = data 146 | 147 | hdlc_c = HDLController(read_func, write_func) 148 | 149 | write_func.data = None 150 | hdlc_c.send(b"test") 151 | while write_func.data is None: 152 | pass 153 | self.assertEqual(write_func.data, frame_data("test", FRAME_DATA, 0)) 154 | self.assertEqual(hdlc_c.get_senders_number(), 1) 155 | 156 | write_func.data = None 157 | while write_func.data is None: 158 | pass 159 | self.assertEqual(write_func.data, frame_data("test", FRAME_DATA, 0)) 160 | self.assertEqual(hdlc_c.get_senders_number(), 1) 161 | 162 | hdlc_c.stop() 163 | 164 | def test_send_three_frames_and_wait_timeout(self): 165 | """ 166 | Tests the timeout while sending three frames. 167 | """ 168 | 169 | def read_func() -> bytes: 170 | return b"test" 171 | 172 | def write_func(data: bytes) -> None: 173 | write_func.data = data 174 | 175 | hdlc_c = HDLController(read_func, write_func, sending_timeout=Timeout(5.0)) 176 | 177 | write_func.data = None 178 | hdlc_c.send(b"test_1") 179 | while write_func.data is None: 180 | pass 181 | self.assertEqual(write_func.data, frame_data("test_1", FRAME_DATA, 0)) 182 | self.assertEqual(hdlc_c.get_senders_number(), 1) 183 | 184 | sleep(1) 185 | 186 | write_func.data = None 187 | hdlc_c.send(b"test_2") 188 | while write_func.data is None: 189 | pass 190 | self.assertEqual(write_func.data, frame_data("test_2", FRAME_DATA, 1)) 191 | self.assertEqual(hdlc_c.get_senders_number(), 2) 192 | 193 | sleep(1) 194 | 195 | write_func.data = None 196 | hdlc_c.send(b"test_3") 197 | while write_func.data is None: 198 | pass 199 | self.assertEqual(write_func.data, frame_data("test_3", FRAME_DATA, 2)) 200 | self.assertEqual(hdlc_c.get_senders_number(), 3) 201 | 202 | write_func.data = None 203 | while write_func.data is None: 204 | pass 205 | self.assertEqual(write_func.data, frame_data("test_1", FRAME_DATA, 0)) 206 | self.assertEqual(hdlc_c.get_senders_number(), 3) 207 | 208 | write_func.data = None 209 | while write_func.data is None: 210 | pass 211 | self.assertEqual(write_func.data, frame_data("test_2", FRAME_DATA, 1)) 212 | self.assertEqual(hdlc_c.get_senders_number(), 3) 213 | 214 | write_func.data = None 215 | while write_func.data is None: 216 | pass 217 | self.assertEqual(write_func.data, frame_data("test_3", FRAME_DATA, 2)) 218 | self.assertEqual(hdlc_c.get_senders_number(), 3) 219 | 220 | hdlc_c.stop() 221 | 222 | def test_send_frame_and_receive_ack(self): 223 | """ 224 | Tests the reception of an ACK frame after having sent a DATA one. 225 | """ 226 | 227 | def read_func() -> bytes: 228 | return frame_data("", FRAME_ACK, 1) 229 | 230 | def write_func(data: bytes) -> None: 231 | write_func.data = data 232 | 233 | hdlc_c = HDLController(read_func, write_func) 234 | 235 | write_func.data = None 236 | hdlc_c.send(b"test") 237 | while write_func.data is None: 238 | pass 239 | self.assertEqual(write_func.data, frame_data("test", FRAME_DATA, 0)) 240 | self.assertEqual(hdlc_c.get_senders_number(), 1) 241 | 242 | hdlc_c.start() 243 | sleep(1) 244 | self.assertEqual(hdlc_c.get_senders_number(), 0) 245 | 246 | hdlc_c.stop() 247 | 248 | def test_send_frame_and_receive_bad_ack(self): 249 | """ 250 | Tests the reception of an invalid ACK frame after having sent a DATA 251 | one. 252 | """ 253 | 254 | def read_func() -> bytes: 255 | return frame_data("", FRAME_ACK, 4) 256 | 257 | def write_func(data: bytes) -> None: 258 | write_func.data = data 259 | 260 | hdlc_c = HDLController(read_func, write_func) 261 | 262 | write_func.data = None 263 | hdlc_c.send(b"test") 264 | while write_func.data is None: 265 | pass 266 | self.assertEqual(write_func.data, frame_data("test", FRAME_DATA, 0)) 267 | self.assertEqual(hdlc_c.get_senders_number(), 1) 268 | 269 | hdlc_c.start() 270 | sleep(1) 271 | self.assertEqual(hdlc_c.get_senders_number(), 1) 272 | 273 | hdlc_c.stop() 274 | 275 | def test_send_frame_and_receive_nack(self): 276 | """ 277 | Tests the reception of an NACK frame after having sent a DATA one. 278 | """ 279 | 280 | def read_func() -> bytes: 281 | return frame_data("", FRAME_NACK, 0) 282 | 283 | def write_func(data: bytes) -> None: 284 | write_func.data = data 285 | 286 | hdlc_c = HDLController(read_func, write_func) 287 | 288 | write_func.data = None 289 | hdlc_c.send(b"test") 290 | while write_func.data is None: 291 | pass 292 | self.assertEqual(write_func.data, frame_data("test", FRAME_DATA, 0)) 293 | self.assertEqual(hdlc_c.get_senders_number(), 1) 294 | 295 | hdlc_c.start() 296 | sleep(1) 297 | self.assertEqual(hdlc_c.get_senders_number(), 1) 298 | 299 | hdlc_c.stop() 300 | 301 | def test_receive_one_frame(self): 302 | """ 303 | Tests the reception of one DATA frame. 304 | """ 305 | 306 | def read_func() -> bytes: 307 | return frame_data("test", FRAME_DATA, 0) 308 | 309 | def write_func(data: bytes) -> None: 310 | write_func.data = data 311 | 312 | hdlc_c = HDLController(read_func, write_func) 313 | 314 | write_func.data = None 315 | hdlc_c.start() 316 | self.assertEqual(hdlc_c.get_data(), b"test") 317 | self.assertEqual(write_func.data, frame_data("", FRAME_ACK, 1)) 318 | 319 | hdlc_c.stop() 320 | 321 | def test_receive_three_frames(self): 322 | """ 323 | Tests the reception of three DATA frames. 324 | """ 325 | 326 | def read_func() -> bytes: 327 | data = frame_data("test_" + str(read_func.i), FRAME_DATA, read_func.i) 328 | read_func.i += 1 329 | return data 330 | 331 | def write_func(_: bytes) -> None: 332 | pass 333 | 334 | hdlc_c = HDLController(read_func, write_func) 335 | 336 | read_func.i = 1 337 | hdlc_c.start() 338 | self.assertEqual(hdlc_c.get_data(), b"test_1") 339 | self.assertEqual(hdlc_c.get_data(), b"test_2") 340 | self.assertEqual(hdlc_c.get_data(), b"test_3") 341 | 342 | hdlc_c.stop() 343 | 344 | def test_receive_one_corrupted_frame_and_send_back_nack(self): 345 | """ 346 | Tests the reception of a corrupted DATA frame and the emission of an 347 | NACK as expected. 348 | """ 349 | 350 | def read_func() -> bytes: 351 | data = bytearray(frame_data("test", FRAME_DATA, 0)) 352 | data[7] ^= 0x01 353 | return bytes(data) 354 | 355 | def write_func(data: bytes) -> None: 356 | write_func.data = data 357 | 358 | hdlc_c = HDLController(read_func, write_func) 359 | 360 | write_func.data = None 361 | hdlc_c.start() 362 | while write_func.data is None: 363 | pass 364 | self.assertEqual(write_func.data, frame_data("", FRAME_NACK, 0)) 365 | 366 | hdlc_c.stop() 367 | 368 | def test_receive_one_corrupted_frame_and_do_not_send_back_nack(self): 369 | """ 370 | Tests the reception of a corrupted DATA frame and that the controller 371 | does not send back an NACK as the option has been turned off. 372 | """ 373 | 374 | def read_func() -> bytes: 375 | data = bytearray(frame_data("test", FRAME_DATA, 0)) 376 | data[7] ^= 0x01 377 | return bytes(data) 378 | 379 | def write_func(data: bytes) -> None: 380 | write_func.data = data 381 | 382 | hdlc_c = HDLController(read_func, write_func, fcs_nack=False) 383 | 384 | write_func.data = None 385 | hdlc_c.start() 386 | sleep(1) 387 | self.assertEqual(write_func.data, None) 388 | 389 | hdlc_c.stop() 390 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | minversion = 4.0 8 | isolated_build = true 9 | skip_missing_interpreters = true 10 | 11 | envlist = 12 | py37 13 | py38 14 | py39 15 | py310 16 | py311 17 | lint 18 | 19 | [testenv] 20 | description = "Run all tests" 21 | commands = 22 | {envpython} -m unittest discover 23 | 24 | [testenv:lint] 25 | description = "Run linters" 26 | skip_install = true 27 | deps = 28 | black 29 | ruff 30 | commands = 31 | black hdlcontroller tests 32 | ruff hdlcontroller tests 33 | --------------------------------------------------------------------------------