diff --git a/.editorconfig b/.editorconfig index 13304fe..144a9b7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,5 +20,5 @@ insert_final_newline = false [makefile] indent_style = tab -[*.{yml, yaml}] +[*.{yml,yaml}] indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 08d793b..416c8e4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -* Bonfo configuration management version: +* Bonfo version: * Python version: * Operating System: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3d02218 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "monthly" + assignees: + - "destos" diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1e566f2..25faab4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -6,9 +6,9 @@ name: dev workflow on: # Triggers the workflow on push or pull request events but only for the dev branch push: - branches: [ dev ] + branches: [dev] pull_request: - branches: [ dev ] + branches: [dev] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -20,9 +20,21 @@ jobs: # The type of runner that the job will run on strategy: matrix: - python-versions: ["3.9", "3.10", "3.11.0-alpha - 3.11.0"] + python-version: ["3.9", "3.10"] os: [ubuntu-latest, macos-latest, windows-latest] + experimental: [false] + # include: + # - python-version: 3.11.0-alpha - 3.11.0 + # os: ubuntu-latest + # experimental: true + # - python-version: 3.11.0-alpha - 3.11.0 + # os: macos-latest + # experimental: true + # - python-version: 3.11.0-alpha - 3.11.0 + # os: windows-latest + # experimental: true runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -30,7 +42,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python-versions }} + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | @@ -38,8 +50,7 @@ jobs: pip install poetry tox tox-gh-actions - name: test with tox - run: - tox + run: tox - name: list files run: ls -l . diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index d7b4fe7..0692edb 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -6,7 +6,7 @@ name: stage & preview workflow on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main ] + branches: [main] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -18,13 +18,13 @@ jobs: strategy: matrix: - python-versions: [ "3.10" ] + python-version: ["3.10"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python-versions }} + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | @@ -32,8 +32,7 @@ jobs: pip install poetry tox tox-gh-actions - name: test with tox - run: - tox + run: tox - name: Build wheels and source tarball run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0156d41..9f5ac8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: # Triggers the workflow on push events but only for new version tags push: tags: - - 'v*' + - "v*" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -21,7 +21,7 @@ jobs: strategy: matrix: - python-versions: ["3.10"] + python-version: ["3.10"] # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -43,7 +43,7 @@ jobs: - uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python-versions }} + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | diff --git a/.gitignore b/.gitignore index 90f0ab1..ad58e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,6 @@ ENV/ # mkdocs build dir site/ + +# examples are not commited for now +examples/ diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..7e78d9a --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.10.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf158a5..51af7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ -# Changelog +## 0.1.1 (2022-04-XX) -## [0.1.0] - 2022-03-29 +* Switched to asyncio +* Added profile management to Board class +* Added tests for Board and Profile classes +* Switched to dataclasses for message struct generation + +## 0.1.0 (2022-03-29) * First release on PyPI. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e4e36c..8424423 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,3 @@ -# Contributing - Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. @@ -29,8 +27,8 @@ and "help wanted" is open to whoever wants to implement it. ### Write Documentation -Bonfo configuration management could always use more documentation, whether as part of the -official Bonfo configuration management docs, in docstrings, or even on the web in blog posts, +Bonfo could always use more documentation, whether as part of the +official Bonfo docs, in docstrings, or even on the web in blog posts, articles, and such. ### Submit Feedback diff --git a/README.md b/README.md index cd9b428..4106ea6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# Bonfo - Betaflight configuration management +# Bonfo - drone configuration management [![pypi](https://img.shields.io/pypi/v/bonfo.svg)](https://pypi.org/project/bonfo/) [![python](https://img.shields.io/pypi/pyversions/bonfo.svg)](https://pypi.org/project/bonfo/) -[![Build Status](https://github.com/destos/bonfo/actions/workflows/dev.yml/badge.svg)](https://github.com/destos/bonfo/actions/workflows/dev.yml) +[![Build Status](https://github.com/destos/bonfo/actions/workflows/release.yml/badge.svg)](https://github.com/destos/bonfo/actions/workflows/release.yml) +[![Dev Build Status](https://github.com/destos/bonfo/actions/workflows/dev.yml/badge.svg?branch=dev)](https://github.com/destos/bonfo/actions/workflows/dev.yml) [![codecov](https://codecov.io/gh/destos/bonfo/branch/main/graphs/badge.svg)](https://codecov.io/github/destos/bonfo) - +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) * Documentation: * GitHub: @@ -19,3 +20,5 @@ ## Credits This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [waynerv/cookiecutter-pypackage](https://github.com/waynerv/cookiecutter-pypackage) project template. + +Inspired by some of the existing multi-wii protocol packages like [YAMSPy](https://pypi.org/project/yamspy/) diff --git a/bonfo/__init__.py b/bonfo/__init__.py index 0a11618..ace7a7d 100644 --- a/bonfo/__init__.py +++ b/bonfo/__init__.py @@ -1,5 +1,8 @@ -"""Top-level package for Bonfo configuration management.""" +"""Top-level package for Bonfo.""" __author__ = """Patrick Forringer""" -__email__ = 'patrick@forringer.com' -__version__ = '0.1.0' +__email__ = "patrick@forringer.com" +__version__ = "0.1.0" + +from .board import Board # noqa +from .profile import Profile # noqa diff --git a/bonfo/board.py b/bonfo/board.py new file mode 100644 index 0000000..39bc37a --- /dev/null +++ b/bonfo/board.py @@ -0,0 +1,261 @@ +"""Board package for Bonfo.""" +from __future__ import annotations + +import asyncio +import logging +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +from typing import AsyncIterator, Coroutine, Iterable, Optional + +from construct import ConstError, StreamError +from semver import VersionInfo +from serial_asyncio import open_serial_connection, serial + +from bonfo.exceptions import BonfoOperatorException + +from .msp.codes import MSP +from .msp.fields.base import Direction +from .msp.fields.pids import MSPFields +from .msp.fields.statuses import ApiVersion, BoardInfo, BuildInfo, CombinedBoardInfo, FcVariant, FcVersion, Name, Uid +from .msp.message import Data, Preamble +from .msp.utils import msg_data, out_message_builder +from .profile import Profile + +logger = logging.getLogger(__name__) + + +__all__ = ["Board"] + + +@dataclass +class Board: + """Board is an interface for , configuration retrieval and saving.""" + + device: str + baudrate: int = 115200 + initial_data: bool = True + loop: Optional[asyncio.AbstractEventLoop] = None + profile: Optional[Profile] = None + + _ready_tasks: Iterable[Coroutine] = field(default_factory=lambda: list(), init=False, repr=False) + + def __post_init__(self) -> None: + # board events + self.connected = asyncio.Event() + self.ready = asyncio.Event() + + self.read_lock = asyncio.Lock() + self.write_lock = asyncio.Lock() + self.message_lock = asyncio.Lock() + + if self.loop is None: + self.loop = asyncio.get_running_loop() + + # TODO: every message sent by the board attaches the current msp version to the context + # for conditional struct building. The initial board message can't send it as + self.info = CombinedBoardInfo(None, None, None, None, None, None, None) + + # rate and pid profile manager + # TODO: handle assigning board to custom profiles + if self.profile is None: + self.profile = Profile(board=self) + self._ready_task(self.profile._check_connection()) + + # TODO: register configs for saving/applying? + # self.rx_conf = RxConfig() + # self.rc_tuning = RcTuning() + + self._ready_task(self.open_serial()) + if self.initial_data: + self._ready_task(self.get_board_info()) + self.loop.create_task(self._run_ready_tasks()) + + def _ready_task(self, coro: Coroutine) -> None: + self._ready_tasks.append(coro) # type:ignore + + async def _run_ready_tasks(self) -> None: + await asyncio.gather(*self._ready_tasks) + self.ready.set() + + async def get_board_info(self) -> CombinedBoardInfo: + await self.connected.wait() + name = await (self > Name) + api = await (self > ApiVersion) + version = await (self > FcVersion) + build_info = await (self > BuildInfo) + board_info = await (self > BoardInfo) + variant = await (self > FcVariant) + uid = await (self > Uid) + self.info = CombinedBoardInfo( + name, + api, + version, + build_info, + board_info, + variant, + uid, + ) + return self.info + + @property + def msp_version(self) -> Optional[VersionInfo]: + try: + return self.info.api.semver # type:ignore + except AttributeError: + return None + + async def open_serial(self, **kwargs) -> None: + try: + self.reader, self.writer = await open_serial_connection( + url=self.device, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=0.1, + xonxoff=False, + rtscts=False, + dsrdtr=False, + # writeTimeout=0, + **kwargs, + ) + self.connected.set() + except serial.SerialException as exc: + logger.exception("Unable to connect to the serial device %s: Will retry", self.device, exc_info=exc) + logger.info("connected to serial device %s", self.device) + + @asynccontextmanager + async def connect(self) -> AsyncIterator["Board"]: + try: + await self.ready.wait() + yield self + except Exception as e: + logger.exception("Error during connection to board", exc_info=e) + finally: + self.disconnect() + + def disconnect(self): + self.connected.clear() + # self.loop.close() + + # TODO: update fields arg requirement here + async def send_msg(self, code: MSP, fields=None, blocking=True, timeout=-1): + """Generates and sends a message with the passed code and data. + + Args: + code (MSP): MSP code enum or code integer + fields (Any supported message struct, optional): structured data to send, + converted to binary by struct. Defaults to None. + + Returns: + int: Total bytes sent + """ + buff = out_message_builder(code, fields=fields, msp=self.msp_version) + + async with self.write_lock: + try: + self.writer.write(buff) + except serial.SerialException as e: + logger.exception("Error writing to serial port", exc_info=e) + finally: + logger.debug("sent: %s %s", code, buff) + + async def receive_msg(self): + """Read the current line from the serial port and parse the MSP message. + + Parse the message and return a construct Container. + + Returns: + Container | None: Containter holding the message data, or None on no data. + """ + # "All this because reader.readline wasn't working... :/" + async with self.read_lock: + preamble_bytes = await self.reader.read(5) + + try: + preamble = Preamble.parse(preamble_bytes) + except (StreamError, ConstError) as e: + logger.exception("Error with preamble bytes", exc_info=e) + return None, None + logger.debug( + "received: preamble code %s (%s): %s", MSP(preamble.frame_id), preamble.data_length, preamble_bytes + ) + if preamble.data_length > 0: + total_length = preamble.data_length + 1 + data_bytes = await self.reader.read(total_length) + all_bytes = preamble_bytes + data_bytes + data_bytes = preamble_bytes[3:5] + data_bytes + logger.debug("all bytes: %s", all_bytes) + msp = self.msp_version + msg = Data.parse(data_bytes, msp=msp) + data = msg_data(msg) + logger.debug("msp: %s fields: %s", msp, data) + return preamble, data + else: + # read last crc byte + # TODO: fix message struct as to actually perform crc when receiving messages + await self.reader.read(1) + + return preamble, None + + async def send_receive(self, code: MSP, fields): + # TODO: Use an asyncio Queue to make sure the send/receive happens consecutively? + async with self.message_lock: + await self.send_msg(code, fields=fields) + return await self.receive_msg() + + async def get(self, fields): + """Get data from the board with optional fields values. + + Args: + fields (Fields): The un-initialized or MSPFields instance with values. + + Returns: + DataclassStruct: The data class instance related to the get request + """ + assert fields.get_direction() in [Direction.OUT, Direction.BOTH] + assert fields.get_code is not None + + async with self.message_lock: + await self.send_msg(fields.get_code) + pre, data = await self.receive_msg() + if pre is None: + return None + # assert code received is the same get_code + assert fields.get_code == pre.frame_id + # TODO: raise error if preamble received is an error + return data + + async def set(self, fields): + """Sends a set message to the board with the values of the given fields. + + Args: + fields (Fields): The un-initialized or MSPFields instance with values. + + Returns: + DataclassStruct: The data class instance related to the set request + """ + assert fields.get_direction() in [Direction.IN, Direction.BOTH] + assert fields.set_code is not None + + async with self.message_lock: + await self.send_msg(fields.set_code, fields=fields) + pre, data = await self.receive_msg() + if pre is None: + return None + # assert code received is the same set_code + assert fields.set_code == pre.frame_id + # TODO: raise error if preamble received is an error + return data + + async def __gt__(self, other): + """Get data from the board with the > operator.""" + if isinstance(other, MSPFields) or issubclass(other, MSPFields): + return await self.get(other) + raise BonfoOperatorException("Not a compatible > type") + + async def __lt__(self, other): + """Set data on the board with the < operator.""" + if isinstance(other, MSPFields) or issubclass(other, MSPFields): + return await self.set(other) + raise BonfoOperatorException("Not a compatible < type") diff --git a/bonfo/cli.py b/bonfo/cli.py index dbd58bd..3dd7e5a 100644 --- a/bonfo/cli.py +++ b/bonfo/cli.py @@ -1,23 +1,26 @@ """Console script for bonfo.""" +import asyncio import atexit +import functools import logging import os import shelve import sys from dataclasses import dataclass -from time import sleep from typing import Optional, Sequence import rich_click as click -from construct import ChecksumError, ConstError +from click import Abort +from construct import StreamError from loca import Loca from rich import print from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo -from bonfo.msp.board import Board +from bonfo.board import Board from bonfo.msp.codes import MSP +from bonfo.msp.fields.boxes import BoxIds click.rich_click.USE_MARKDOWN = True click.rich_click.SHOW_ARGUMENTS = True @@ -30,6 +33,7 @@ ) # config things +# TODO: use datawizard instead, and init the board/port via property. try: loca = Loca() path = loca.user.state.config() @@ -42,7 +46,18 @@ pass finally: logger.debug("State file: %s", state_file) - state_store = shelve.open(state_file, flag="c") + try: + state_store = shelve.open(state_file, flag="c") + except Exception as e: + logger.exception("Error loading state", exc_info=e) + + +def async_cmd(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return asyncio.run(func(*args, **kwargs)) + + return wrapper @dataclass @@ -73,17 +88,17 @@ def board(self) -> Optional[Board]: @click.group("bonfo") @click.pass_context def cli(ctx): - """Bonfo is configuration management for flight controllers running **MSP v1** compatible flight controllers. + """Bonfo is configuration management for flight controllers running **MSP v1**. > Supported flight controller software: - > - BetaFlight + > - BetaFlight(>=3.4) """ ctx.obj = BonfoContext(port=state_store.get("port", None)) @cli.command() @bonfo_context -def check_context(ctx): +def check_context(ctx: BonfoContext): """Output current context values.""" print(ctx.board) print(ctx.port) @@ -95,29 +110,144 @@ def connect(ctx: BonfoContext): """Connect to the FC board.""" if ctx.board is None: return click.echo("No port selected") - with ctx.board as board: + with ctx.board.connect() as board: click.echo(board.send_msg(MSP.API_VERSION)) @cli.command() @bonfo_context -def update(ctx: BonfoContext): +@async_cmd +async def test(ctx: BonfoContext): + """Just me, testing things.""" + if ctx.board is None: + return click.echo("No port selected") + async with ctx.board.connect() as board: + pass + # print(board.info) + # print(str(board.profile)) + # status = await board.get(StatusEx) + # print(status) + # print(Name(name="bobby").build()) + # name = await (board < Name(name="robby")) + # print(name) + # name = await (board > Name) + # sensor = await (board > SensorAlignment) + # att = await (board > BoxIds) + # pid = await (board > BoxNames) + fc = await (board > BoxIds) + print(fc) + # fc.features &= Features.ESC_SENSOR + # breakpoint() + # await (board < fc) + # fcsecond = await (board > FeatureConfig) + # print(name) + # print(att) + # print(sensor) + # print(fcres) + # print(fcsecond) + # while True: + # att = await (board > Attitude) + # print(att) + # await asyncio.sleep(0.01) + # print(pid) + + +@cli.command() +@bonfo_context +@async_cmd +async def msp_cli(ctx: BonfoContext): + """Drop into the MSP CLI.""" if ctx.board is None: return click.echo("No port selected") - with ctx.board as board: - while True: - sleep(1) - click.echo(board.send_msg(MSP.STATUS_EX)) - try: - click.echo(board.receive_msg()) - except (ChecksumError, ConstError) as e: - logger.exception("error", exc_info=e) + async with ctx.board.connect() as board: + board.writer.write(b"#") + + try: + while True: + cmd = click.prompt("$") + command = b"# " + cmd.encode("utf-8") + logger.debug(command) + board.writer.write(command) + line = await board.reader.readuntil() + print(line) + except (StreamError, Exception) as e: + logger.exception("Error in cli", exc_info=e) + logger.info("Exiting due to exception") + except (KeyboardInterrupt, Abort) as e: + logger.exception("Interrupted", exc_info=e) + logger.info("Exiting CLI") + finally: + board.writer.write(b"exit\r") + + +@cli.group("profiles") +@bonfo_context +def profiles(ctx: BonfoContext): + pass + + +@profiles.command() +@bonfo_context +@async_cmd +async def get(ctx: BonfoContext): + """Get the PID and rate profile of the flight controller.""" + if ctx.board is None: + return click.echo("No port selected") + async with ctx.board.connect() as board: + click.echo(f"{board.profile}") + + +@profiles.command() +@bonfo_context +@click.option("-p", "--pid", type=int) +@click.option("-r", "--rate", type=int) +@async_cmd +async def set(ctx: BonfoContext, pid, rate): + """Set the PID or rate profile of the flight controller.""" + if ctx.board is None: + return click.echo("No port selected") + async with ctx.board.connect() as board: + click.echo(f"Before: {board.profile}") + if pid is not None: + board.profile.pid = pid + if rate is not None: + board.profile.rate = rate + await board.profile.apply_changes() + click.echo(f"After: {board.profile}") + + +@cli.group("config") +@bonfo_context +def config(ctx: BonfoContext): + """Manage and apply configuration files.""" + pass + + +@config.command() +@bonfo_context +@click.option("-c", "--check", type=bool) +@click.argument("file", type=click.File("r")) +def apply(ctx: BonfoContext, check, file): + """Apply passed configuration.""" + pass + # click.echo(BoardConf.from_yaml(file.read())) + + +@config.command() +@bonfo_context +@click.argument("file", type=click.Path(dir_okay=False)) +def create(ctx: BonfoContext, file): + pass + # BoardConf( + # pid_profiles={1: PidTranslator(test_one=123, test2=123), 2: PidTranslator(test_one=3, test2=4)}, + # # rates=[RateTranslator(profile=1, yaw=123)] + # ).to_yaml_file(file) @cli.command() @bonfo_context -@click.option('-s', '--include-links', is_flag=True, help='include entries that are symlinks to real devices') -def set_port(cxt, include_links, err=True): +@click.option("-s", "--include-links", is_flag=True, help="include entries that are symlinks to real devices") +def set_port(cxt: BonfoContext, include_links, err=True): """Set the default port to use during this session.""" # TODO: let user know they are changing the port from the context if it changes # or show current as well diff --git a/bonfo/msp/structs/__init__.py b/bonfo/config/__init__.py similarity index 100% rename from bonfo/msp/structs/__init__.py rename to bonfo/config/__init__.py diff --git a/bonfo/config/root.py b/bonfo/config/root.py new file mode 100644 index 0000000..f97148c --- /dev/null +++ b/bonfo/config/root.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass, field + +from dataclass_wizard import YAMLWizard +from dataclass_wizard.enums import LetterCase + +from bonfo import __version__ as current_bonfo_version + + +@dataclass +class BoardConf(YAMLWizard, key_transform=LetterCase.SNAKE): # type: ignore + version: int = 1 + bonfo_version: str = field(default=current_bonfo_version) + # pid_profiles: dict[int, PidTranslator] | None = None + # rate_profiles: dict[int, RateTranslator] | None = None + # pids: list[PidTranslator] | None = None + # rates: list[RateTranslator] | None = None + + # TODO: version validation diff --git a/bonfo/exceptions.py b/bonfo/exceptions.py new file mode 100644 index 0000000..1795b97 --- /dev/null +++ b/bonfo/exceptions.py @@ -0,0 +1,10 @@ +class BoardException(Exception): + pass + + +class ConnectionException(BoardException): + pass + + +class BonfoOperatorException(Exception): + pass diff --git a/bonfo/msp/__init__.py b/bonfo/msp/__init__.py index e69de29..705ca5b 100644 --- a/bonfo/msp/__init__.py +++ b/bonfo/msp/__init__.py @@ -0,0 +1,4 @@ +from .codes import MSP +from .fields import * # noqa +from .message import Message +from .utils import in_message_builder, out_message_builder diff --git a/bonfo/msp/adapters.py b/bonfo/msp/adapters.py new file mode 100644 index 0000000..03e9801 --- /dev/null +++ b/bonfo/msp/adapters.py @@ -0,0 +1,77 @@ +from math import floor + +import arrow +from construct import Adapter, Array, Byte, ExprAdapter, Int8ub, Int16ub, PaddedString, Validator, obj_ + +from bonfo.msp.codes import MSP + + +class RcAdapter(Adapter): + def _decode(self, obj, context, path): + # Maybe turn into a dict that keeps the original value + return round(obj / 100.0, 2) + + def _encode(self, obj, context, path): + return floor(obj * 100.0) + + +class MessageTypeAdapter(Adapter): + def _decode(self, obj, context, path): + return MSP(obj) + + def _encode(self, obj, context, path): + return obj.value + + +MessageType = MessageTypeAdapter(Byte) + +RcFloat = RcAdapter(Int8ub) + +RawSingle = Array(3, Int16ub) + +Int8ubPlusOne = ExprAdapter(Int8ub, obj_ + 1, obj_ - 1) + + +class PIDProfileValidator(Validator): + def _validate(self, obj, context, path): + return int(obj) in list(range(1, 4)) + + +class RateProfileValidator(Validator): + def _validate(self, obj, context, path): + return int(obj) in list(range(1, 7)) + + +SelectPIDProfile = PIDProfileValidator(Int8ubPlusOne) + + +RATEPROFILE_MASK = 0x80 # 1 << 7 +SelectRateProfile = RateProfileValidator( + ExprAdapter( + Int8ub, + # decoder + (obj_ + 1) ^ RATEPROFILE_MASK, + # encoder + (obj_ ^ RATEPROFILE_MASK) - 1, + ) +) + + +DATE_TIME_LENGTH = 11 + 8 +GIT_HASH_LENGTH = 7 + + +class TimestampAdapter(Adapter): + def _decode(self, obj, context, path) -> arrow.Arrow: + try: + return arrow.get(obj, "MMM D YYYYHH:mm:ss") + except arrow.ParserError: + return arrow.get(obj, "MMM D YYYYHH:mm:ss") + + def _encode(self, obj, context, path) -> str: + return arrow.get(obj).format("MMM D YYYYHH:mm:ss") + + +BTFLTimestamp = TimestampAdapter(PaddedString(DATE_TIME_LENGTH, "utf8")) + +GitHash = PaddedString(GIT_HASH_LENGTH, "utf8") diff --git a/bonfo/msp/board.py b/bonfo/msp/board.py deleted file mode 100644 index 6535b0a..0000000 --- a/bonfo/msp/board.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Board package for Bonfo configuration management.""" -import logging -import time -from threading import BoundedSemaphore - -import serial - -from bonfo.msp.codes import MSP -from bonfo.msp.message import Message -from bonfo.msp.utils import out_message_builder - -from .state import Config, RcTuning, RxConfig - -logger = logging.getLogger(__name__) - - -class Board: - """Board is an interface for the serial connection, configuration retrieval and saving of data.""" - - def __init__(self, port: str, baudrate: int = 115200, trials=100) -> None: - self.conf = Config() - - # TODO: register configs for saving/applying? - self.rx_conf = RxConfig() - self.rc_tuning = RcTuning() - - self.serial_trials = trials - self.serial_write_lock = BoundedSemaphore(value=1) - self.serial_read_lock = BoundedSemaphore(value=1) - - self.init_serial(port, baudrate=baudrate) - - def init_serial(self, port: str, baudrate: int = 115200): - self.serial = serial.Serial( - port, - baudrate=baudrate, - bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - timeout=0.1, - xonxoff=False, - rtscts=False, - dsrdtr=False, - writeTimeout=0, - ) - logger.debug(self.serial) - - def __enter__(self): - self.is_serial_open = self.connect(trials=self.serial_trials) - - if self.is_serial_open: - return self - else: - logger.warning("Serial port ({}) not ready/available".format(self.serial.port)) - return False - - def __exit__(self, exc_type, exc_value, traceback): - if self.serial.is_open: - self.serial.readlines - self.serial.close() - - """ - - Returns: - """ - - def connect(self, trials=100, delay=0.5): - """Opens the serial connection with the board. - - Args: - trials (int, optional): How many times to try and connect. Defaults to 100. - delay (float, optional): Wait between connection trials. Defaults to 0.5. - - Returns: - bool: True on a successful connection - """ - for _ in range(trials): - try: - if self.serial.is_open: - self.serial.close() - self.serial.open() - # self.basic_info() - return True - - except serial.SerialException as err: - logger.warning("Error opening the serial port ({0}): {1}".format(self.serial.port, err), exc_info=err) - - except FileNotFoundError as err: - logger.warning("Port ({0}) not found: {1}".format(self.serial.port, err), exc_info=err) - time.sleep(delay) - - return False - - def send_msg(self, code: MSP, data=None, blocking=True, timeout=-1): - """Generates and sends a message with the passed code and data. - - Args: - code (MSP): MSP code enum or code integer - data (Any supported message struct, optional): structured data to send, - converted to binary by struct. Defaults to None. - blocking (bool, optional): Send message with blocking of other messages. Defaults to True. - timeout (int, optional): Blocking timeout. Defaults to -1. - - Returns: - int: Total bytes sent - """ - buff = out_message_builder(code, fields=data) - - if self.serial_write_lock.acquire(blocking, timeout): - try: - sent_bytes = self.serial.write(buff) - except Exception as e: - logger.exception("Error writing to serial port", exc_info=e) - finally: - self.serial_write_lock.release() - if sent_bytes > 0: - logger.debug("RAW message sent: {}".format(buff)) - - return sent_bytes - - def receive_msg(self): - - with self.serial_read_lock: - # TODO: do this mo-better - buff = self.serial.readline() - logger.debug("RAW message received: {}".format(buff)) - if len(buff) > 0: - return Message.parse(buff) - return None - - -__all__ = ["Board"] diff --git a/bonfo/msp/codes.py b/bonfo/msp/codes.py index b3f52d3..216845e 100644 --- a/bonfo/msp/codes.py +++ b/bonfo/msp/codes.py @@ -166,62 +166,5 @@ class MSP(IntEnum): DEBUGMSG = 253 # deprecated DEBUG = 254 - # INAV/v2 specific codes - SETTING = 0x1003 - SET_SETTING = 0x1004 - COMMON_MOTOR_MIXER = 0x1005 - COMMON_SET_MOTOR_MIXER = 0x1006 - - COMMON_SETTING_INFO = 0x1007 - COMMON_PG_LIST = 0x1008 - - INAV_CF_SERIAL_CONFIG = 0x1009 - INAV_SET_CF_SERIAL_CONFIG = 0x100A - - INAV_STATUS = 0x2000 - INAV_OPTICAL_FLOW = 0x2001 - INAV_ANALOG = 0x2002 - INAV_MISC = 0x2003 - INAV_SET_MISC = 0x2004 - INAV_BATTERY_CONFIG = 0x2005 - INAV_SET_BATTERY_CONFIG = 0x2006 - INAV_RATE_PROFILE = 0x2007 - INAV_SET_RATE_PROFILE = 0x2008 - INAV_AIR_SPEED = 0x2009 - INAV_OUTPUT_MAPPING = 0x200A - - INAV_MIXER = 0x2010 - INAV_SET_MIXER = 0x2011 - - INAV_OSD_LAYOUTS = 0x2012 - INAV_OSD_SET_LAYOUT_ITEM = 0x2013 - INAV_OSD_ALARMS = 0x2014 - INAV_OSD_SET_ALARMS = 0x2015 - INAV_OSD_PREFERENCES = 0x2016 - INAV_OSD_SET_PREFERENCES = 0x2017 - - INAV_MC_BRAKING = 0x200B - INAV_SET_MC_BRAKING = 0x200C - - INAV_SELECT_BATTERY_PROFILE = 0x2018 - - INAV_DEBUG = 0x2019 - - INAV_BLACKBOX_CONFIG = 0x201A - INAV_SET_BLACKBOX_CONFIG = 0x201B - - INAV_SENSOR_CONFIG = 0x201C - INAV_SET_SENSOR_CONFIG = 0x201D - INAV_TEMPERATURES = 0x201E - - INAV_SERVO_MIXER = 0x2020 - INAV_SET_SERVO_MIXER = 0x2021 - INAV_LOGIC_CONDITIONS = 0x2022 - INAV_SET_LOGIC_CONDITIONS = 0x2023 - INAV_LOGIC_CONDITIONS_STATUS = 0x2026 - - INAV_PID = 0x2030 - INAV_SET_PID = 0x2031 - - INAV_OPFLOW_CALIBRATION = 0x203 +frame_map = {v.real: v for v in MSP.__members__.values()} diff --git a/bonfo/msp/expr.py b/bonfo/msp/expr.py new file mode 100644 index 0000000..6668d1b --- /dev/null +++ b/bonfo/msp/expr.py @@ -0,0 +1,15 @@ +from construct import FuncPath + +from bonfo.msp.fields.base import MSPFields + + +def zero_none_len(data): + # TODO: please fix this, the rebuild shouldn't be passing the data class here. + if isinstance(data, MSPFields): + return len(data.get_struct().build(data)) + if data is None: + return 0 + return len(data) + + +zero_none_len_ = FuncPath(zero_none_len) # type: ignore diff --git a/bonfo/msp/fields/__init__.py b/bonfo/msp/fields/__init__.py new file mode 100644 index 0000000..ade8a9e --- /dev/null +++ b/bonfo/msp/fields/__init__.py @@ -0,0 +1,5 @@ +from .boxes import * # noqa +from .config import * # noqa +from .pids import * # noqa +from .sensors import * # noqa +from .statuses import * # noqa diff --git a/bonfo/msp/fields/base.py b/bonfo/msp/fields/base.py new file mode 100644 index 0000000..9e5bf4c --- /dev/null +++ b/bonfo/msp/fields/base.py @@ -0,0 +1,69 @@ +import functools +import logging +from enum import Enum +from typing import Any, Dict, Optional + +from construct_typed import DataclassMixin, DataclassStruct + +from bonfo.msp.codes import MSP + +logger = logging.getLogger(__name__) + + +class Direction(Enum): + """Direction of message from board.""" + + IN = 1 # To board + OUT = 2 # From board + BOTH = 3 # Bidirectional + + +class MSPFields(DataclassMixin): + get_code: Optional[MSP] = None + set_code: Optional[MSP] = None + + @classmethod + def __init_subclass__(cls, get_code: MSP = None, set_code: MSP = None) -> None: + cls.get_code = get_code + cls.set_code = set_code + + @classmethod + @functools.cache + def get_struct(cls) -> Optional[DataclassStruct]: + """Returns the generated DataClassStruct based on the configuration of this MSPFields subclass. + + Returns: + Optional[DataclassStruct]: The Struct used for parsing or building MSP message data + """ + try: + struct = DataclassStruct(cls) + except TypeError as e: + logger.exception("Error building struct from dataclass", exc_info=e) + return None + return struct + + def build(self): + return self.get_struct().build(self) + + @classmethod + @functools.cache + def get_direction(cls) -> Optional[Direction]: + if cls.get_code is not None and cls.set_code is not None: + return Direction.BOTH + if cls.get_code is None: + return Direction.IN + if cls.set_code is None: + return Direction.OUT + return None + + +@functools.cache +def build_fields_mapping() -> Dict[MSP, Optional[DataclassStruct[Any]]]: + fields_mappings = dict() + for fields in MSPFields.__subclasses__(): + struct = fields.get_struct() + if fields.get_code is not None: + fields_mappings[fields.get_code] = struct + if fields.set_code is not None: + fields_mappings[fields.set_code] = struct + return fields_mappings diff --git a/bonfo/msp/fields/boxes.py b/bonfo/msp/fields/boxes.py new file mode 100644 index 0000000..1f8f154 --- /dev/null +++ b/bonfo/msp/fields/boxes.py @@ -0,0 +1,80 @@ +"""Boxes are betaflight feature checkboxes.""" + +from dataclasses import dataclass + +from construct import Int8ub, Int64ub +from construct_typed import FlagsEnumBase, TFlagsEnum, csfield + +from ..codes import MSP +from .base import MSPFields +from .utils import BIT + +__all__ = ["BoxNames", "BoxIds", "Boxes"] + + +class Boxes(FlagsEnumBase): + ARM = BIT(0) # "ARM", + ANGLE = BIT(1) # "ANGLE", + HORIZON = BIT(2) # "HORIZON", + BARO = BIT(3) # (deprecated) "BARO", + ANTIGRAVITY = BIT(4) # "ANTI GRAVITY", + MAG = BIT(5) # "MAG", + HEADFREE = BIT(6) # "HEADFREE", + HEADADJ = BIT(7) # "HEADADJ", + CAMSTAB = BIT(8) # "CAMSTAB", + CAMTRIG = BIT(9) # (deprecated) "CAMTRIG", + GPSHOME = BIT(10) # (deprecated) "GPS HOME", + GPSHOLD = BIT(11) # (deprecated) "GPS HOLD", + PASSTHRU = BIT(12) # "PASSTHRU", + BEEPERON = BIT(13) # "BEEPER", + LEDMAX = BIT(14) # (deprecated) "LEDMAX", + LEDLOW = BIT(15) # "LEDLOW", + LLIGHTS = BIT(16) # (deprecated) "LLIGHTS", + CALIB = BIT(17) # "CALIB", + GOV = BIT(18) # (deprecated) "GOVERNOR", + OSD = BIT(19) # "OSD DISABLE", + TELEMETRY = BIT(20) # "TELEMETRY", + GTUNE = BIT(21) # (deprecated) "GTUNE", + RANGEFINDER = BIT(22) # (deprecated) "RANGEFINDER", + SERVO1 = BIT(23) # "SERVO1", + SERVO2 = BIT(24) # "SERVO2", + SERVO3 = BIT(25) # "SERVO3", + BLACKBOX = BIT(26) # "BLACKBOX", + FAILSAFE = BIT(27) # "FAILSAFE", + AIRMODE = BIT(28) # "AIR MODE", + THREED = BIT(29) # "3D DISABLE / SWITCH", + FPVANGLEMIX = BIT(30) # "FPV ANGLE MIX", + BLACKBOXERASE = BIT(31) # "BLACKBOX ERASE (>30s)", + CAMERA1 = BIT(32) # "CAMERA CONTROL 1", + CAMERA2 = BIT(33) # "CAMERA CONTROL 2", + CAMERA3 = BIT(34) # "CAMERA CONTROL 3", + FLIPOVERAFTERCRASH = BIT(35) # "FLIP OVER AFTER CRASH", + PREARM = BIT(36) # "PREARM", + BEEPGPSCOUNT = BIT(37) # "GPS BEEP SATELLITE COUNT", + THREEDONASWITCH = BIT(38) # (deprecated) "3D ON A SWITCH", + VTXPITMODE = BIT(39) # "VTX PIT MODE", + USER1 = BIT(40) # "USER1", + USER2 = BIT(41) # "USER2", + USER3 = BIT(42) # "USER3", + USER4 = BIT(43) # "USER4", + PIDAUDIO = BIT(44) # "PID AUDIO", + PARALYZE = BIT(45) # "PARALYZE", + GPSRESCUE = BIT(46) # "GPS RESCUE", + ACROTRAINER = BIT(47) # "ACRO TRAINER", + VTXCONTROLDISABLE = BIT(48) # "VTX CONTROL DISABLE", + LAUNCHCONTROL = BIT(49) # "LAUNCH CONTROL", + MSPOVERRIDE = BIT(50) # "MSP OVERRIDE", + STICKCOMMANDDISABLE = BIT(51) # "STICK COMMANDS DISABLE", + BEEPERMUTE = BIT(52) # "BEEPER MUTE", + + +@dataclass +class BoxNames(MSPFields, get_code=MSP.BOXNAMES): + """May not be needed, Boxes enum provides names.""" + + temp: int = csfield(Int8ub) + + +@dataclass +class BoxIds(MSPFields, get_code=MSP.BOXIDS): + boxes: Boxes = csfield(TFlagsEnum(Int64ub, Boxes)) diff --git a/bonfo/msp/fields/config.py b/bonfo/msp/fields/config.py new file mode 100644 index 0000000..70b8cc0 --- /dev/null +++ b/bonfo/msp/fields/config.py @@ -0,0 +1,141 @@ +"""Message structs that deal with configuring the board.""" + +from dataclasses import dataclass +from typing import Any + +from construct import Container, Int8ub, Int16ub, Int32ub, Union +from construct_typed import FlagsEnumBase, TFlagsEnum, csfield + +from bonfo.msp.fields.utils import BIT + +from ..adapters import RcFloat, SelectPIDProfile, SelectRateProfile +from ..codes import MSP +from ..fields.base import MSPFields +from ..structs import MSPCutoff +from ..versions import MSPVersions + + +@dataclass +class SelectSetting(MSPFields, set_code=MSP.SELECT_SETTING): + """Select between rate and PID profiles in one message. + + The first bit is a flag to allow switching between updating the PID profile + or rate profile. They are mutually exclusive. + + PID profiles are limited to 1-3, and rate profiles 1-6. + Any values over or under those will select pid/rate profile 1. + + This message will not be successful if the board is armed. + """ + + profile: Container[Any] = csfield(Union(None, "pid_profile" / SelectPIDProfile, "rate_profile" / SelectRateProfile)) + + +def SelectPID(pid): + """SelectPID is a wrapper around a configured SelectSetting instance.""" + return SelectSetting(profile=dict(pid_profile=pid)) + + +def SelectRate(rate): + """SelectRate is a wrapper around a configured SelectSetting instance.""" + return SelectSetting(profile=dict(rate_profile=rate)) + + +@dataclass +class CopyProfile(MSPFields, set_code=MSP.COPY_PROFILE): + """Tell one profile to copy to another.""" + + +@dataclass +class EepromWrite(MSPFields, set_code=MSP.EEPROM_WRITE): + """Save all settings to board EEPROM.""" + + +@dataclass +class RxConfig(MSPFields, get_code=MSP.RX_CONFIG, set_code=MSP.SET_RX_CONFIG): + """RxConfig.""" + + +@dataclass +class RcTuning(MSPFields, get_code=MSP.RC_TUNING, set_code=MSP.SET_RC_TUNING): + roll_rate_wat: float = csfield(RcFloat) + roll_expo: float = csfield(RcFloat) + # grouped R,P,Yrates + roll_rate: float = csfield(RcFloat) + pitch_rate: float = csfield(RcFloat) + yaw_rate: float = csfield(RcFloat) + tpa_rate: float = csfield(RcFloat) + throttle_mid: float = csfield(RcFloat) + throttle_expo: float = csfield(RcFloat) + tpa_breakpoint: float = csfield(Int16ub) + rc_yaw_expo: float = csfield(RcFloat) + rc_yaw_rate: float = csfield(RcFloat) + rc_pitch_rate: float = csfield(RcFloat) + rc_pitch_expo: float = csfield(RcFloat) + # added in 1.41 + throttle_limit_type: float = csfield(MSPCutoff(RcFloat, MSPVersions.V1_41)) + throttle_limit_percent: float = csfield(MSPCutoff(RcFloat, MSPVersions.V1_41)) + # added in 1.42 + roll_rate_limit: int = csfield(MSPCutoff(Int16ub, MSPVersions.V1_42)) + pitch_rate_limit: int = csfield(MSPCutoff(Int16ub, MSPVersions.V1_42)) + yaw_rate_limit: int = csfield(MSPCutoff(Int16ub, MSPVersions.V1_42)) + # added in 1.43 + rates_type: int = csfield(MSPCutoff(Int8ub, MSPVersions.V1_43)) + + +class Features(FlagsEnumBase): + RX_PPM = BIT(0) + INFLIGHT_ACC_CAL = BIT(2) + RX_SERIAL = BIT(3) + MOTOR_STOP = BIT(4) + SERVO_TILT = BIT(5) + SOFTSERIAL = BIT(6) + GPS = BIT(7) + RANGEFINDER = BIT(9) + TELEMETRY = BIT(10) + THREED = BIT(12) + RX_PARALLEL_PWM = BIT(13) + RX_MSP = BIT(14) + RSSI_ADC = BIT(15) + LED_STRIP = BIT(16) + DASHBOARD = BIT(17) + OSD = BIT(18) + CHANNEL_FORWARDING = BIT(20) + TRANSPONDER = BIT(21) + AIRMODE = BIT(22) + RX_SPI = BIT(25) + SOFTSPI = BIT(26) # (removed) + ESC_SENSOR = BIT(27) + ANTI_GRAVITY = BIT(28) + DYNAMIC_FILTER = BIT(29) # (removed) + + # def __add__(self, other: int) -> int: + # if not isinstance(other, (self.__class__, int)): + # return NotImplemented + # return self.__class__(self.__class__(other)._value_ & ~self) + + # def __add__(self, other: int) -> int: + # return None + # # return self.__class__(other & (~self)) + # return other & (~self) + + # def __add__(self, other): + # if not isinstance(other, (self.__class__, int)): + # return NotImplemented + # return self.__class__(~self._value_ & self.__class__(other)._value_) + + +@dataclass +class FeatureConfig(MSPFields, get_code=MSP.FEATURE_CONFIG, set_code=MSP.SET_FEATURE_CONFIG): + features: Features = csfield(TFlagsEnum(Int32ub, Features)) + + +__all__ = [ + "RxConfig", + "RcTuning", + "SelectSetting", + "EepromWrite", + "CopyProfile", + "Features", + "FeatureConfig", +] diff --git a/bonfo/msp/fields/pids.py b/bonfo/msp/fields/pids.py new file mode 100644 index 0000000..e1f1228 --- /dev/null +++ b/bonfo/msp/fields/pids.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass +from typing import Any, Iterable, Optional + +from construct import Array, Default, Int8ub, Int16ub, Int32ub +from construct import Optional as CSOptional +from construct import Struct +from construct_typed import csfield + +from bonfo.msp.codes import MSP +from bonfo.msp.fields.base import MSPFields +from bonfo.msp.structs import MSPCutoff +from bonfo.msp.versions import MSPVersions + +__all__ = ["PidAdvanced", "PidCoefficients"] + +PID_COUNT = 5 + + +@dataclass +class PidCoefficients(MSPFields, get_code=MSP.PID): + pids: Iterable[Any] = csfield( + Array( + PID_COUNT, + Struct( + "p" / Int8ub, + "i" / Int8ub, + "d" / Int8ub, + ), + ) + ) + + +@dataclass +class PidAdvanced(MSPFields, get_code=MSP.PID_ADVANCED, set_code=MSP.SET_PID_ADVANCED): + _unused1: Optional[int] = csfield(CSOptional(Default(Int32ub, 0)), "unknown reserved") + _unused3: Optional[int] = csfield(CSOptional(Default(Int16ub, 0)), "was pidProfile.yaw_p_limit") + _unused4: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "reserved") + _unused5: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "was vbatPidCompensation") + feedforward_transition: Optional[int] = csfield( + MSPCutoff(Int8ub, MSPVersions.V1_44, Default(Int8ub, 0)), "USE_FEEDFORWARD" + ) + _unused6: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "was low byte of dtermSetpointWeight") + _unused7: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "reserved") + _unused8: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "reserved") + _unused9: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "reserved") + rate_accel_limit: Optional[int] = csfield(Int16ub) + yaw_rate_accel_limit: Optional[int] = csfield(Int16ub) + level_angle_limit: Optional[int] = csfield(Int8ub) + _unused10: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "was pidProfile.levelSensitivity") + iterm_throttle_threshold: Optional[int] = csfield(Int16ub) + iterm_accelerator_gain: Optional[int] = csfield(Int16ub) + _unused11: Optional[int] = csfield(CSOptional(Default(Int16ub, 0)), "was dtermSetpointWeight") + iterm_rotation: Optional[int] = csfield(Int8ub) + _unused12: Optional[int] = csfield(CSOptional(Default(Int8ub, 0)), "was smart_feedforward") + iterm_relax: Optional[int] = csfield(Int8ub, "USE_ITERM_RELAX") + iterm_relax_type: Optional[int] = csfield(Int8ub, "USE_ITERM_RELAX") + abs_control_gain: Optional[int] = csfield(Int8ub, "USE_ABSOLUTE_CONTROL") + throttle_boost: Optional[int] = csfield(Int8ub, "USE_THROTTLE_BOOST") + acro_trainer_angle_limit: Optional[int] = csfield(Int8ub, "USE_ACRO_TRAINER") + pid_roll_f: Optional[int] = csfield(Int16ub, "pid roll feed forward term") + pid_pitch_f: Optional[int] = csfield(Int16ub, "pid pitch feed forward term") + pid_yaw_f: Optional[int] = csfield(Int16ub, "pid yaw feed forward term") + anti_gravity_mode: Optional[int] = csfield(Int8ub, "Flag?") + d_min_roll: Optional[int] = csfield(Int8ub, "USE_D_MIN") + d_min_pitch: Optional[int] = csfield(Int8ub, "USE_D_MIN") + d_min_yaw: Optional[int] = csfield(Int8ub, "USE_D_MIN") + d_min_gain: Optional[int] = csfield(Int8ub, "USE_D_MIN") + d_min_advance: Optional[int] = csfield(Int8ub, "USE_D_MIN") + use_integrated_yaw: Optional[int] = csfield(Int8ub, "USE_INTEGRATED_YAW_CONTROL") + integrated_yaw_relax: Optional[int] = csfield(Int8ub, "USE_INTEGRATED_YAW_CONTROL") + iterm_relax_cutoff: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_42), "USE_ITERM_RELAX") + motor_output_limit: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_43)) + auto_profile_cell_count: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_43)) + dyn_idle_min_rpm: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_43), "USE_DYN_IDLE") + feedforward_averaging: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_FEEDFORWARD") + feedforward_smooth_factor: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_FEEDFORWARD") + feedforward_boost: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_FEEDFORWARD") + feedforward_max_rate_limit: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_FEEDFORWARD") + feedforward_jitter_factor: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_FEEDFORWARD") + vbat_sag_compensation: Optional[int] = csfield( + MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_BATTERY_VOLTAGE_SAG_COMPENSATION" + ) + thrust_linearization: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_44), "USE_THRUST_LINEARIZATION") diff --git a/bonfo/msp/fields/sensors.py b/bonfo/msp/fields/sensors.py new file mode 100644 index 0000000..9dec56c --- /dev/null +++ b/bonfo/msp/fields/sensors.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + +from construct import Int16ub +from construct_typed import csfield + +from ..codes import MSP +from .base import MSPFields + +__all__ = ["Attitude"] + + +@dataclass +class Attitude(MSPFields, get_code=MSP.ATTITUDE): + roll: int = csfield(Int16ub) + pitch: int = csfield(Int16ub) + yaw: int = csfield(Int16ub) diff --git a/bonfo/msp/fields/statuses.py b/bonfo/msp/fields/statuses.py new file mode 100644 index 0000000..9c49b1e --- /dev/null +++ b/bonfo/msp/fields/statuses.py @@ -0,0 +1,284 @@ +"""Message structs that deal with the status of the board.""" + + +from dataclasses import dataclass +from functools import cached_property +from typing import Optional, Union + +from arrow import Arrow +from construct import ( + Array, + FixedSized, + GreedyBytes, + Int8ub, + Int16ub, + Int16ul, + Int32ub, + ListContainer, + PaddedString, + this, +) +from construct_typed import EnumBase, FlagsEnumBase, TEnum, TFlagsEnum, csfield +from semver import VersionInfo + +from ..adapters import BTFLTimestamp, GitHash, Int8ubPlusOne, RawSingle +from ..codes import MSP +from ..structs import MSPCutoff +from ..versions import MSPMaxSupported, MSPVersions +from .base import MSPFields +from .utils import BIT + +__all__ = [ + "ApiVersion", + "FcVariant", + "FcVersion", + "BuildInfo", + "TargetCapabilitiesFlags", + "ConfigurationProblemsFlags", + "BoardInfo", + "Uid", + "AccTrim", + "Name", + "Status", + "ActiveSensorsFlags", + "ArmingDisableFlags", + "ConfigStateFlags", + "StatusEx", + "GyroDetectionFlags", + "SensorAlignment", + "RawIMU", +] + + +@dataclass +class ApiVersion(MSPFields, get_code=MSP.API_VERSION): + # Should return a semver for comparisons? + # First byte is version id 0=1, 1=2? + # Could validate and alert if 2 is returned + msp_protocol: int = csfield(Int8ub) + api_major: int = csfield(Int8ub) + api_minor: int = csfield(Int8ub) + + @cached_property + def semver(self): + return VersionInfo(self.msp_protocol, self.api_major, self.api_minor) + + @cached_property + def supported(self): + return self.semver <= MSPMaxSupported + + +@dataclass +class FcVariant(MSPFields, get_code=MSP.FC_VARIANT): + variant: str = csfield(PaddedString(4, "utf8")) + + +@dataclass +class FcVersion(MSPFields, get_code=MSP.FC_VERSION): + major: int = csfield(Int8ub) + minor: int = csfield(Int8ub) + patch: int = csfield(Int8ub) + + +@dataclass +class BuildInfo(MSPFields, get_code=MSP.BUILD_INFO): + date_time: Union[str, Arrow] = csfield(BTFLTimestamp) + git_hash: str = csfield(GitHash) + + +class TargetCapabilitiesFlags(FlagsEnumBase): + HAS_VCP = BIT(0) + HAS_SOFTSERIAL = BIT(1) + IS_UNIFIED = BIT(2) + HAS_FLASH_BOOTLOADER = BIT(3) + SUPPORTS_CUSTOM_DEFAULTS = BIT(4) + HAS_CUSTOM_DEFAULTS = BIT(5) + SUPPORTS_RX_BIND = BIT(6) + + +class ConfigurationProblemsFlags(FlagsEnumBase): + ACC_NEEDS_CALIBRATION = BIT(0) + MOTOR_PROTOCOL_DISABLED = BIT(1) + + +@dataclass +class BoardInfo(MSPFields, get_code=MSP.BOARD_INFO): + short_name: str = csfield(PaddedString(4, "utf8")) + hardware_revision: int = csfield(Int16ub) + uses_max7456: int = csfield(Int8ub, "if 2, uses a MAX7456") + target_capabilities: TargetCapabilitiesFlags = csfield(TFlagsEnum(Int8ub, TargetCapabilitiesFlags)) + _target_name_length: int = csfield(Int8ub) + target_name: str = csfield(PaddedString(this._target_name_length, "utf8")) + _board_name_length: int = csfield(Int8ub) + board_name: str = csfield(PaddedString(this._board_name_length, "utf8")) + _manufacturer_id_length: int = csfield(Int8ub) + manufacturer_id: str = csfield(PaddedString(this._manufacturer_id_length, "utf8")) + signature: str = csfield(PaddedString(32, "utf8")) + mcu_type: int = csfield(Int8ub) + configuration_state: Optional[int] = csfield(MSPCutoff(Int8ub, MSPVersions.V1_42)) + sample_rate: Optional[int] = csfield(MSPCutoff(Int16ub, MSPVersions.V1_43), "gyro sample rate") + configuration_problems: ConfigurationProblemsFlags = csfield( + MSPCutoff(TFlagsEnum(Int32ub, ConfigurationProblemsFlags), MSPVersions.V1_43), + ) + + +@dataclass +class SetBoardInfo(MSPFields, set_code=MSP.SET_BOARD_INFO): + _board_name_length: int = csfield(Int8ub) + board_name: str = csfield(PaddedString(this._board_name_length, "utf8")) + + +@dataclass +class Uid(MSPFields, get_code=MSP.UID): + """Board UID.""" + + uid: ListContainer[int] = csfield(Array(3, Int32ub)) + + +@dataclass +class Name(MSPFields, get_code=MSP.NAME, set_code=MSP.SET_NAME): + name: str = csfield(PaddedString(16, "utf8")) + + +@dataclass +class CombinedBoardInfo: + name: Optional[Name] + api: Optional[ApiVersion] + version: Optional[FcVersion] + build: Optional[BuildInfo] + board: Optional[BoardInfo] + variant: Optional[FcVariant] + uid: Optional[Uid] + + +@dataclass +class AccTrim(MSPFields, get_code=MSP.ACC_TRIM): + pass + + +@dataclass +class Status(MSPFields, get_code=MSP.STATUS): + """Status is not Used!""" + + cycle_time: int = csfield(Int16ub) + i2c_error: int = csfield(Int16ub) + # sensor flags + active_sensors: int = csfield(Int16ub) + # mode flags + mode: int = csfield(Int32ub) + # selected profile + profile: int = csfield(Int8ub) + + +class ActiveSensorsFlags(FlagsEnumBase): + # bit enum position + flag shift + ACC = 1 << 1 + BARO = 1 << 2 << 1 + MAG = 1 << 3 << 2 + GPS = 1 << 5 << 3 + RANGEFINDER = 1 << 4 << 4 + GYRO = 1 << 0 << 5 + # v2 sensors + # SONAR = 1 << 4, + # GPSMAG = 1 << 6, + + +class ArmingDisableFlags(FlagsEnumBase): + NO_GYRO = BIT(0) + FAILSAFE = BIT(1) + RX_FAILSAFE = BIT(2) + BAD_RX_RECOVERY = BIT(3) + BOXFAILSAFE = BIT(4) + RUNAWAY_TAKEOFF = BIT(5) + CRASH_DETECTED = BIT(6) + THROTTLE = BIT(7) + ANGLE = BIT(8) + BOOT_GRACE_TIME = BIT(9) + NOPREARM = BIT(10) + LOAD = BIT(11) + CALIBRATING = BIT(12) + CLI = BIT(13) + CMS_MENU = BIT(14) + BST = BIT(15) + MSP = BIT(16) + PARALYZE = BIT(17) + GPS = BIT(18) + RESC = BIT(19) + RPMFILTER = BIT(20) + REBOOT_REQUIRED = BIT(21) + DSHOT_BITBANG = BIT(22) + ACC_CALIBRATION = BIT(23) + MOTOR_PROTOCOL = BIT(24) + ARM_SWITCH = BIT(25) + + +class ConfigStateFlags(FlagsEnumBase): + REBOOT = BIT(8) + + +@dataclass +class StatusEx(MSPFields, get_code=MSP.STATUS_EX): + cycle_time: int = csfield(Int16ub, "cycle time in us") + i2c_error: int = csfield(Int16ub, "i2x error counter") + active_sensors: ActiveSensorsFlags = csfield(TFlagsEnum(Int16ul, ActiveSensorsFlags), "sensor flags") + mode: int = csfield(Int32ub) + pid_profile: int = csfield(Int8ubPlusOne, "Selected pid profile") + cpuload: int = csfield(Int16ub, "Load percent") + profile_count: int = csfield(Int8ub, "Total number of profiles") + rate_profile: int = csfield(Int8ubPlusOne, "Rate profile index") + additional_mode_bytes: int = csfield(Int8ub, "length of additional flag bytes") + additional_mode: bytes = csfield( + FixedSized(this.additional_mode_bytes, GreedyBytes), "a continuation of the above mode flags" + ) + arming_disable_flags: ArmingDisableFlags = csfield(TFlagsEnum(Int32ub, ArmingDisableFlags)) + config_state: ConfigStateFlags = csfield(TFlagsEnum(Int8ub, ConfigStateFlags)) + + +@dataclass +class RawIMU(MSPFields, get_code=MSP.RAW_IMU): + accelerometer: ListContainer[int] = csfield(RawSingle) + gyroscope: ListContainer[int] = csfield(RawSingle) + magnetometer: ListContainer[int] = csfield(RawSingle) + + +class GyroDetectionFlags(FlagsEnumBase): + GYRO_NONE_MASK = 0 + GYRO_1_MASK = BIT(0) + GYRO_2_MASK = BIT(1) + GYRO_ALL_MASK = GYRO_1_MASK | GYRO_2_MASK + GYRO_IDENTICAL_MASK = BIT(7) # All gyros are of the same hardware type + + +class SensorAlignEnum(EnumBase): + """SensorAlignEnum mirror enum defintion from betaflight sensor_alignment.h.""" + + ALIGN_DEFAULT = 0 # driver-provided alignment + # the order of these 8 values also correlate to corresponding code in ALIGNMENT_TO_BITMASK. + # R, P, Y + CW0_DEG = 1 # 00,00,00 + CW90_DEG = 2 # 00,00,01 + CW180_DEG = 3 # 00,00,10 + CW270_DEG = 4 # 00,00,11 + CW0_DEG_FLIP = 5 # 00,10,00 # _FLIP = 2x90 degree PITCH rotations + CW90_DEG_FLIP = 6 # 00,10,01 + CW180_DEG_FLIP = 7 # 00,10,10 + CW270_DEG_FLIP = 8 # 00,10,11 + ALIGN_CUSTOM = 9 # arbitrary sensor angles, e.g. for external sensors + + +# Alignment mask results as part of masking? +# Maybe the fallback struct resolver that resolve the mask first, if fails returns the enum? + + +@dataclass +class SensorAlignment(MSPFields, get_code=MSP.SENSOR_ALIGNMENT, set_code=MSP.SET_SENSOR_ALIGNMENT): + # First byte may be ignored on set?, check msp.c + align_gyro: SensorAlignEnum = csfield(TEnum(Int8ub, SensorAlignEnum)) + align_acc: SensorAlignEnum = csfield(TEnum(Int8ub, SensorAlignEnum)) + align_mag: SensorAlignEnum = csfield(TEnum(Int8ub, SensorAlignEnum)) + gyro_detection_flags: GyroDetectionFlags = csfield( + MSPCutoff(TFlagsEnum(Int8ub, GyroDetectionFlags), MSPVersions.V1_41) + ) + gyro_to_use: int = csfield(MSPCutoff(Int8ubPlusOne, MSPVersions.V1_41)) + gyro_1_align: SensorAlignEnum = csfield(MSPCutoff(TEnum(Int8ub, SensorAlignEnum), MSPVersions.V1_41)) + gyro_2_align: SensorAlignEnum = csfield(MSPCutoff(TEnum(Int8ub, SensorAlignEnum), MSPVersions.V1_41)) diff --git a/bonfo/msp/fields/utils.py b/bonfo/msp/fields/utils.py new file mode 100644 index 0000000..6a390f7 --- /dev/null +++ b/bonfo/msp/fields/utils.py @@ -0,0 +1,2 @@ +def BIT(num): + return 1 << num diff --git a/bonfo/msp/message.py b/bonfo/msp/message.py index 538a5ee..417deca 100644 --- a/bonfo/msp/message.py +++ b/bonfo/msp/message.py @@ -1,71 +1,30 @@ -import logging from functools import reduce from operator import xor from construct import ( Byte, Checksum, - ChecksumError, Const, Default, Enum, FixedSized, - FuncPath, Hex, + Int8ub, Mapping, - Optional, RawCopy, Rebuild, Struct, - Switch, this, ) -from bonfo.msp.codes import MSP -from bonfo.msp.structs.adapters import MessageType +from bonfo.msp.structs import FrameStruct -from .structs import config as structs - -logger = logging.getLogger(__name__) - -# Do this map differently + automate or just register the structs? -function_map = { - MSP.API_VERSION: structs.ApiVersion, - MSP.FC_VARIANT: structs.FcVariant, - MSP.FC_VERSION: structs.FcVersion, - MSP.BUILD_INFO: structs.BuildInfo, - MSP.BOARD_INFO: structs.BoardInfo, - MSP.UID: structs.Uid, - MSP.ACC_TRIM: structs.AccTrim, - MSP.NAME: structs.Name, - MSP.STATUS: structs.Status, - MSP.STATUS_EX: structs.StatusEx, - MSP.RX_CONFIG: structs.RxConfig, - MSP.RC_TUNING: structs.RcTuning, - MSP.RAW_IMU: structs.RawIMU, -} - - -message_id_mapping = {m.value: m for m in MSP} - - -def zero_none_len(data): - if data is None: - return 0 - return len(data) - - -zero_none_len_ = FuncPath(zero_none_len) # type: ignore - - -class LenientChecksum(Checksum): - def _parse(self, stream, context, path): - try: - return super()._parse(stream, context, path) - except ChecksumError as e: - logger.error("CRC failed {}", exc_info=e) - return self.checksumfield._parsereport(stream, context, path) +from .adapters import MessageType +from .codes import frame_map +from .expr import zero_none_len_ +# INFO: Make sure all fields are loaded before processing messages +from .fields import * # noqa # fmt: off # MSP v1 message struct @@ -87,8 +46,8 @@ def _parse(self, stream, context, path): # "_is_out" / Computed(this.message_type == "OUT"), "packet" / RawCopy(Struct( "data_length" / Rebuild(Byte, zero_none_len_(this.fields)), - "frame_id" / Mapping(MessageType, message_id_mapping), - "fields" / FixedSized(this.data_length, Optional(Switch(this.frame_id, function_map))), # type: ignore + "frame_id" / Mapping(MessageType, frame_map), + "fields" / FixedSized(this.data_length, FrameStruct(this.frame_id)), # type:ignore )), "crc" / Hex(Checksum( Byte, @@ -97,3 +56,44 @@ def _parse(self, stream, context, path): )) ) # fmt: on + +# The idea for this, if we can't get readline to work +# split the message into the preamble, and data segment. +# We know how long the preamble is, grab until the data length byte +# Use the length to grab the rest + crc value. +# concat the data length byte to newly received bytes and parse the data if available +# Would love to fix StreamReader.readline so I didn't have to do this + +MessageTypeEnum = Enum( + Byte, + IN=ord(">"), + OUT=ord("<"), + ERR=ord("!"), +) + +Preamble = Struct( + "signature" / Const(b"$"), + "version" / Const(b"M"), + # Don't need to support v2 at this moment + # "version" / Default( + # Enum(Byte, + # V1 = ord("M"), + # V2 = ord("X"), + # ), "V1"), + "message_type" / Default(MessageTypeEnum, "IN"), + "data_length" / Int8ub, + "frame_id" / Mapping(MessageType, frame_map), +) + +Data = Struct( + "packet" + / RawCopy( + Struct( + "data_length" / Int8ub, + "frame_id" / Mapping(MessageType, frame_map), + "fields" / FixedSized(this.data_length, FrameStruct(this.frame_id)), # type:ignore + ) + ), + # "crc" / Byte + "crc" / Hex(Checksum(Byte, lambda data: reduce(xor, data, 0), this.packet.data)), +) diff --git a/bonfo/msp/state.py b/bonfo/msp/state.py deleted file mode 100644 index 012f553..0000000 --- a/bonfo/msp/state.py +++ /dev/null @@ -1,545 +0,0 @@ -"""Data classes that store parsed information from a MSP message.""" - -from dataclasses import dataclass, field -from typing import Optional - -from dataclass_wizard import YAMLWizard - - -@dataclass -class Config: - api_version: str = "0.0.0" - flight_controller_identifier: str = '' - flight_controller_version: str = '' - version: int = 0 - build_info: str = '' - multi_type: int = 0 - msp_version: int = 0 - capability: int = 0 - cycle_time: int = 0 - i2c_error: int = 0 - active_sensors: int = 0 - mode: int = 0 - profile: int = 0 - uid: list = field(default_factory=lambda: [0, 0, 0]) - accelerometer_trims: list = field(default_factory=lambda: [0, 0]) - name: str = '' - display_name: str = 'pilot' - num_profiles: int = 3 - rate_profile: int = 0 - board_type: int = 0 - arming_disable_count: int = 0 - arming_disable_flags: int = 0 - arming_disabled: bool = False - runaway_takeoff_prevention_disabled: bool = False - board_identifier: str = "" - board_version: int = 0 - comm_capabilities: int = 0 - target_name: str = "" - board_name: str = "" - manufacturer_id: str = "" - signature: list = field(default_factory=lambda: []) - mcu_type_id: int = 255 - - @property - def is_inav(self): - return "INAV" in self.flight_controller_identifier - - -# State and others potentially can be an enum? -# Revamp how this is used in the FC msp client code and state + msp version switching -@dataclass -class DataHandler: - msp_version: int = 1 - state: float = 0 - flags: int = 0 - message_direction: int = -1 - code: int = 0 - data_view: int = 0 - message_length_expected: int = 0 - message_length_received: int = 0 - message_buffer: list = field(default_factory=lambda: []) - message_buffer_uint8_view: list = field(default_factory=lambda: []) - message_checksum: int = 0 - messageIsJumboFrame: bool = False - crcError: bool = False - callbacks: list = field(default_factory=lambda: []) - packet_error: int = 0 - unsupported: int = 0 - last_received_timestamp: Optional[float] = None - listeners: list = field(default_factory=lambda: []) - # TODO: state property with data class wizard that tracks field changes and logs them? - - -# @dataclass -# class SensorData: -# gyroscope: int = [0, 0, 0] -# accelerometer: int = [0, 0, 0] -# magnetometer: int = [0, 0, 0] -# altitude: int = 0 -# sonar: int = 0 -# kinematics: int = [0.0, 0.0, 0.0] -# debug: int = [0, 0, 0, 0, 0, 0, 0, 0] # 8 values for special situations like MSP2_INAV_DEBUG - - -# # @dataclass -# # class MotorData: -# # # defaults -# # # roll, pitch, yaw@dataclass, throttle, aux 1 ... aux n - - -# @dataclass -# class Rc: -# active_channels: int = 0 -# channels: int = ([0] * 32,) - - -# @dataclass -# class GpsData: -# fix: int = 0 -# numSat: int = 0 -# lat: int = 0 -# lon: int = 0 -# alt: int = 0 -# speed: int = 0 -# ground_course: int = 0 -# distanceToHome: int = 0 -# ditectionToHome: int = 0 -# update: int = 0 -# chn: int = [] -# svid: int = [] -# quality: int = [] -# cno: int = [] - - -# # @dataclass -# # class Analog: -# # @dataclass -# # class Voltage_Meters: -# # @dataclass -# # class Current_Meters: -# # @dataclass -# # class Battery_State: - - -# @dataclass -# class Sensor_Alignment: -# align_gyro: int = 0 -# align_acc: int = 0 -# align_mag: int = 0 -# gyro_detection_flags: int = 0 -# gyro_to_use: int = 0 -# gyro_1_align: int = 0 -# gyro_2_align: int = 0 - - -# @dataclass -# class Board_Alignment_Config: -# roll: int = 0 -# pitch: int = 0 -# yaw: int = 0 - - -# @dataclass -# class Arming_Config: -# auto_disarm_delay: int = 0 -# disarm_kill_switch: int = 0 -# small_angle: int = 0 - - -# # @dataclass -# # class Feature_Config: -# # featuremask: int = 0 - -# # features: int = { -# # 0: { 'group: int = 'rxMode', 'name: int = 'RX_PPM', 'enabled: int = False -# # 2: { 'group: int = 'other', 'name: int = 'INFLIGHT_ACC_CAL', 'enabled: int = False -# # 3: { 'group: int = 'rxMode', 'name: int = 'RX_SERIAL', 'enabled: int = False -# # 4: { 'group: int = 'esc', 'name: int = 'MOTOR_STOP', 'enabled: int = False -# # 5: { 'group: int = 'other', 'name: int = 'SERVO_TILT', 'enabled: int = False -# # 6: { 'group: int = 'other', 'name: int = 'SOFTSERIAL', 'enabled: int = False -# # 7: { 'group: int = 'gps', 'name: int = 'GPS', 'enabled: int = False -# # 9: { 'group: int = 'other', 'name: int = 'SONAR', 'enabled: int = False -# # 10: { 'group: int = 'other', 'name: int = 'TELEMETRY', 'enabled: int = False -# # 12: { 'group: int = '3D', 'name: int = '3D', 'enabled: int = False -# # 13: { 'group: int = 'rxMode', 'name: int = 'RX_PARALLEL_PWM', 'enabled: int = False -# # 14: { 'group: int = 'rxMode', 'name: int = 'RX_MSP', 'enabled: int = False -# # 15: { 'group: int = 'rssi', 'name: int = 'RSSI_ADC', 'enabled: int = False -# # 16: { 'group: int = 'other', 'name: int = 'LED_STRIP', 'enabled: int = False -# # 17: { 'group: int = 'other', 'name: int = 'DISPLAY', 'enabled: int = False -# # 19: { 'group: int = 'other', 'name: int = 'BLACKBOX', 'enabled: int = False -# # 20: { 'group: int = 'other', 'name: int = 'CHANNEL_FORWARDING', 'enabled: int = False -# # 21: { 'group: int = 'other', 'name: int = 'TRANSPONDER', 'enabled: int = False -# # 22: { 'group: int = 'other', 'name: int = 'AIRMODE', 'enabled: int = False -# # 18: { 'group: int = 'other', 'name: int = 'OSD', 'enabled: int = False -# # 25: { 'group: int = 'rxMode', 'name: int = 'RX_SPI', 'enabled: int = False -# # 27: { 'group: int = 'other', 'name: int = 'ESC_SENSOR', 'enabled: int = False -# # 28: { 'group: int = 'other', 'name: int = 'ANTI_GRAVITY', 'enabled: int = False -# # 29: { 'group: int = 'other', 'name: int = 'DYNAMIC_FILTER', 'enabled: int = False - - -@dataclass -class RxConfig: - serialrx_provider: int = 0 - stick_max: int = 0 - stick_center: int = 0 - stick_min: int = 0 - spektrum_sat_bind: int = 0 - rx_min_usec: int = 0 - rx_max_usec: int = 0 - rc_interpolation: int = 0 - rc_interpolation_interval: int = 0 - rc_interpolation_channels: int = 0 - air_mode_activate_threshold: int = 0 - rx_spi_protocol: int = 0 - rx_spi_id: int = 0 - rx_spi_rf_channel_count: int = 0 - fpv_cam_angle_degrees: int = 0 - rc_smoothing_type: int = 0 - rc_smoothing_input_cutoff: int = 0 - rc_smoothing_derivative_cutoff: int = 0 - rc_smoothing_input_type: int = 0 - rc_smoothing_derivative_type: int = 0 - - -# @dataclass -# class Profile(YAMLWizard): -# rc_tuning: RcTuning = [] - - -@dataclass -class RcTuning(YAMLWizard): - # May allow for saving the raw byte value to get more percission than the float value conversion? - # TODO: decimals in the future? store actual byte values and convert as needed? - rc_rate: float = 0 - rc_expo: float = 0 - roll_pitch_rate: float = 0 # pre 1.7 api only - roll_rate: float = 0 - pitch_rate: float = 0 - yaw_rate: float = 0 - dynamic_thr_pid: float = 0 - throttle_mid: float = 0 - throttle_expo: float = 0 - dynamic_thr_breakpoint: float = 0 - rc_yaw_expo: float = 0 - rcyawrate: float = 0 - rcpitchrate: float = 0 - rc_pitch_expo: float = 0 - roll_rate_limit: int = 1998 - pitch_rate_limit: int = 1998 - yaw_rate_limit: int = 1998 - - def apply_struct(self, data): - self.struct.parse(data) - breakpoint() - - -# # @dataclass -# # class Rc_Map: - -# # @dataclass -# # class Aux_Config_Ids: - -# # @dataclass -# # class Mode_Ranges: - -# # @dataclass -# # class Mode_Ranges_Extra: - -# # @dataclass -# # class Adjustment_Ranges: - -# # @dataclass -# # class Rxfail_Config: - - -# @dataclass -# class Failsafe_Config: -# failsafe_delay: int = 0 -# failsafe_off_delay: int = 0 -# failsafe_throttle: int = 0 -# failsafe_switch_mode: int = 0 -# failsafe_throttle_low_delay: int = 0 -# failsafe_procedure: int = 0 - - -# # @dataclass -# # class ServoData: - -# # @dataclass -# # class Voltage_Meter_Configs: - -# # @dataclass -# # class Current_Meter_Configs: - -# @dataclass -# class Battery_Config: -# vbatmincellvoltage: int = 0 -# vbatmaxcellvoltage: int = 0 -# vbatwarningcellvoltage: int = 0 -# capacity: int = 0 -# voltageMeterSource: int = 0 -# currentMeterSource: int = 0 - - -# # @dataclass -# # class Pids: - -# # @dataclass -# # class Pid: -# # controller: int = 0 - - -# @dataclass -# class Fc_Config: -# loopTime: int = 0 - - -# @dataclass -# class Motor_Config: -# minthrottle: int = 0 -# maxthrottle: int = 0 -# mincommand: int = 0 - - -# @dataclass -# class Misc: -# # DEPRECATED = only used to store values that are written back to the fc as-is, do NOT use for any other purpose -# failsafe_throttle: int = 0 -# gps_baudrate: int = 0 -# multiwiicurrentoutput: int = 0 -# placeholder2: int = 0 -# vbatscale: int = 0 -# vbatmincellvoltage: int = 0 -# vbatmaxcellvoltage: int = 0 -# vbatwarningcellvoltage: int = 0 -# batterymetertype: int = 1 # 1=ADC, 2=ESC - - -# @dataclass -# class Gps_Config: -# provider: int = 0 -# ublox_sbas: int = 0 -# auto_config: int = 0 -# auto_baud: int = 0 - - -# @dataclass -# class Rssi_Config: -# channel: int = 0 - - -# @dataclass -# class Compass_Config: -# mag_declination: int = 0 - - -# @dataclass -# class Gps_Rescue: -# angle: int = 0 -# initialAltitudeM: int = 0 -# descentDistanceM: int = 0 -# rescueGroundspeed: int = 0 -# throttleMin: int = 0 -# throttleMax: int = 0 -# throttleHover: int = 0 -# sanityChecks: int = 0 -# minSats: int = 0 - - -# @dataclass -# class Motor_3d_Config: -# deadband3d_low: int = 0 -# deadband3d_high: int = 0 -# neutral: int = 0 - - -# # @dataclass -# # class Aux_Config: -# # @dataclass -# # class Pidnames: -# # @dataclass -# # class Servo_Config: - - -# @dataclass -# class Rc_Deadband_Config: -# deadband: int = 0 -# yaw_deadband: int = 0 -# alt_hold_deadband: int = 0 -# deadband3d_throttle: int = 0 - - -# @dataclass -# class Beeper_Config: -# beepers: int = 0 -# dshotBeaconTone: int = 0 -# dshotBeaconConditions: int = 0 - - -# @dataclass -# class Mixer_Config: -# mixer: int = 0 -# reverseMotorDir: int = 0 - - -# @dataclass -# class Reboot_Types: -# FIRMWARE: int = 0 -# BOOTLOADER: int = 1 -# MSC: int = (2,) -# MSC_UTC: int = 3 - - -# # 0 based index, must be identical to 'baudRates' in 'src/main/io/serial.c' in betaflight -# # @dataclass -# # class Baud_Rates: -# # 230400', '250000', '400000', '460800', '500000', '921600', '1000000', -# # 1500000', '2000000', '2470000'] - -# # needs to be identical to 'serialPortFunction_e' in 'src/main/io/serial.h' in betaflight -# @dataclass -# class Serial_Port_Functions: -# MSP: int = 0 -# GPS: int = 1 -# TELEMETRY_FRSKY: int = 2 -# TELEMETRY_HOTT: int = 3 -# TELEMETRY_MSP: int = 4 -# TELEMETRY_LTM: int = 4 # LTM replaced MSP -# TELEMETRY_SMARTPORT: int = 5 -# RX_SERIAL: int = 6 -# BLACKBOX: int = 7 -# TELEMETRY_MAVLINK: int = 9 -# ESC_SENSOR: int = 1 -# TBS_SMARTAUDIO: int = 11 -# TELEMETRY_IBUS: int = 12 -# IRC_TRAMP: int = 13 -# RUNCAM_DEVICE_CONTROL: int = 14 # support communicate with RunCam Device -# LIDAR_TF: int = 15 - - -# @dataclass -# class Serial_Config: -# ports: int = [] -# # pre 1.6 settings -# mspBaudRate: int = 0 -# gpsBaudRate: int = 0 -# gpsPassthroughBaudRate: int = 0 -# cliBaudRate: int = 0 - - -# @dataclass -# class Pid_Advanced_Config: -# gyro_sync_denom: int = 0 -# pid_process_denom: int = 0 -# use_unsyncedPwm: int = 0 -# fast_pwm_protocol: int = 0 -# motor_pwm_rate: int = 0 -# digitalIdlePercent: int = 0 -# gyroUse32kHz: int = 0 - - -# @dataclass -# class Filter_Config: -# gyro_hardware_lpf: int = 0 -# gyro_32khz_hardware_lpf: int = 0 -# gyro_lowpass_hz: int = 0 -# gyro_lowpass_dyn_min_hz: int = 0 -# gyro_lowpass_dyn_max_hz: int = 0 -# gyro_lowpass_type: int = 0 -# gyro_lowpass2_hz: int = 0 -# gyro_lowpass2_type: int = 0 -# gyro_notch_hz: int = 0 -# gyro_notch_cutoff: int = 0 -# gyro_notch2_hz: int = 0 -# gyro_notch2_cutoff: int = 0 -# dterm_lowpass_hz: int = 0 -# dterm_lowpass_dyn_min_hz: int = 0 -# dterm_lowpass_dyn_max_hz: int = 0 -# dterm_lowpass_type: int = 0 -# dterm_lowpass2_hz: int = 0 -# dterm_lowpass2_type: int = 0 -# dterm_notch_hz: int = 0 -# dterm_notch_cutoff: int = 0 -# yaw_lowpass_hz: int = 0 - - -# @dataclass -# class Advanced_Tuning: -# rollPitchItermIgnoreRate: int = 0 -# yawItermIgnoreRate: int = 0 -# yaw_p_limit: int = 0 -# deltaMethod: int = 0 -# vbatPidCompensation: int = 0 -# dtermSetpointTransition: int = 0 -# dtermSetpointWeight: int = 0 -# toleranceBand: int = 0 -# toleranceBandReduction: int = 0 -# itermThrottleGain: int = 0 -# pidMaxVelocity: int = 0 -# pidMaxVelocityYaw: int = 0 -# levelAngleLimit: int = 0 -# levelSensitivity: int = 0 -# itermThrottleThreshold: int = 0 -# itermAcceleratorGain: int = 0 -# itermRotation: int = 0 -# smartFeedforward: int = 0 -# itermRelax: int = 0 -# itermRelaxType: int = 0 -# absoluteControlGain: int = 0 -# throttleBoost: int = 0 -# acroTrainerAngleLimit: int = 0 -# feedforwardRoll: int = 0 -# feedforwardPitch: int = 0 -# feedforwardYaw: int = 0 -# feedforwardTransition: int = 0 -# antiGravityMode: int = 0 -# dMinRoll: int = 0 -# dMinPitch: int = 0 -# dMinYaw: int = 0 -# dMinGain: int = 0 -# dMinAdvance: int = 0 -# useIntegratedYaw: int = 0 -# integratedYawRelax: int = 0 - - -# @dataclass -# class Sensor_Config: -# acc_hardware: int = 0 -# baro_hardware: int = 0 -# mag_hardware: int = 0 - - -# @dataclass -# class Dataflash: -# ready: bool = False -# supported: bool = False -# sectors: int = 0 -# totalSize: int = 0 -# usedSize: int = 0 - - -# @dataclass -# class Sdcard: -# supported: bool = False -# state: int = 0 -# filesystemLastError: int = 0 -# freeSizeKB: int = 0 -# totalSizeKB: int = 0 - - -# @dataclass -# class Blackbox: -# supported: bool = False -# blackboxDevice: int = 0 -# blackboxRateNum: int = 1 -# blackboxRateDenom: int = 1 -# blackboxPDenom: int = 0 - - -# @dataclass -# class Transponder: -# supported: bool = False -# data: int = [] -# provider: int = 0 -# providers: int = [] diff --git a/bonfo/msp/structs.py b/bonfo/msp/structs.py new file mode 100644 index 0000000..5b4ed24 --- /dev/null +++ b/bonfo/msp/structs.py @@ -0,0 +1,50 @@ +import logging + +from construct import Checksum, ChecksumError, IfThenElse, Optional, Switch + +from bonfo.msp.codes import MSP +from bonfo.msp.versions import MSPVersions + +logger = logging.getLogger(__name__) + + +class MSPCutoff(IfThenElse): # type:ignore + """MSPCutoff returns an optional struct if the context msp version is less than specified.""" + + def __init__(self, thensubcon, version_added: MSPVersions, elsesubcon=None) -> None: + self.version_added = version_added + if elsesubcon is None: + elsesubcon = Optional(thensubcon) + super().__init__(self.version_checker, thensubcon, elsesubcon) + # do we need some way to allow a field to be optional and required in the dataclass init? + # self.flagbuildnone = elsesubcon.flagbuildnone + + def version_checker(self, context): + # Get msp version from params context + msp: MSPVersions = context._params.get("msp", None) + if msp is not None: + return msp >= self.version_added.value + return True + + +class LenientChecksum(Checksum): + """LenientChecksum doesn't raise an error on checksum failure.""" + + def _parse(self, stream, context, path): + try: + return super()._parse(stream, context, path) + except ChecksumError as e: + logger.error("CRC failed {}", exc_info=e) + return self.checksumfield._parsereport(stream, context, path) + + +def FrameStruct(frame_id: MSP): + """FrameStruct wraps a optional switch so as to not cause errors when no data is passed.""" + # load all fields to make sure sub classes are populated + from .fields.base import build_fields_mapping + + return Optional(Switch(frame_id, build_fields_mapping())) # type:ignore + + +# def SpecifiedString +# Make a string struct that combines the first byte with a padded string that specifies the total string length diff --git a/bonfo/msp/structs/adapters.py b/bonfo/msp/structs/adapters.py deleted file mode 100644 index fe28754..0000000 --- a/bonfo/msp/structs/adapters.py +++ /dev/null @@ -1,27 +0,0 @@ -from math import floor - -from construct import Adapter, Array, Byte, Int8ub, Int16ub - -from bonfo.msp.codes import MSP - - -class RcAdapter(Adapter): - def _decode(self, obj, context, path): - # Maybe turn into a dict that keeps the original value - return round(obj / 100.0, 2) - - def _encode(self, obj, context, path): - return floor(obj * 100.0) - - -class MessageTypeAdapter(Adapter): - def _decode(self, obj, context, path): - return MSP(obj) - - def _encode(self, obj, context, path): - return obj.value - - -MessageType = MessageTypeAdapter(Byte) -RcFloat = RcAdapter(Int8ub) -RawSingle = Array(3, Int16ub) diff --git a/bonfo/msp/structs/config.py b/bonfo/msp/structs/config.py deleted file mode 100644 index ad64e7f..0000000 --- a/bonfo/msp/structs/config.py +++ /dev/null @@ -1,176 +0,0 @@ -from construct import ( - Array, - FixedSized, - FlagsEnum, - GreedyBytes, - Int8ub, - Int16ub, - Int16ul, - Int32ub, - PaddedString, - Struct, - this, -) - -from .adapters import RawSingle, RcFloat - -ApiVersion = Struct( - "msp_protocol" / Int8ub, - "api_major" / Int8ub, - "api_minor" / Int8ub, -) - -FcVariant = Struct("name" / PaddedString(4, "utf8")) - -FcVersion = Struct( - "major" / Int8ub, - "minor" / Int8ub, - "patch" / Int8ub, -) - -BuildInfo = Struct() -BoardInfo = Struct() -Uid = Struct("uid" / Array(3, Int32ub)) -AccTrim = Struct() -Name = Struct() - -Status = Struct( - "cycle_time" / Int16ub, - "i2c_error" / Int16ub, - # sensor flags - "active_sensors" / Int16ub, - # mode flags - "mode" / Int32ub, - # selected profile - "profile" / Int8ub, -) - -StatusEx = Struct( - # cycle time in us - "cycle_time" / Int16ub, - # i2x error counter - "i2c_error" / Int16ub, - # sensor flags - "active_sensors" - / FlagsEnum( - Int16ul, - # bit enum position + flag shift - ACC=1 << 1, - BARO=1 << 2 << 1, - MAG=1 << 3 << 2, - GPS=1 << 5 << 3, - RANGEFINDER=1 << 4 << 4, - GYRO=1 << 0 << 5, - # v2 sensors - # SONAR = 1 << 4, - # GPSMAG = 1 << 6, - ), - # first 32bits of mode flags - "mode" / Int32ub, - # selected profile - "profile" / Int8ub, - # load percent - "cpuload" / Int16ub, - # Total number of profiles - "profile_count" / Int8ub, - # Rate profile index - "profile_index" / Int8ub, - # length of additional flag bytes - "additional_mode_bytes" / Int8ub, - # a continuation of the above mode flags - "additional_mode" / FixedSized(this.additional_mode_bytes, GreedyBytes), - # Flags indicating why arming is currently disabled - "arming_disable_flags" - / FlagsEnum( - Int32ub, - NO_GYRO=1 << 0, - FAILSAFE=1 << 1, - RX_FAILSAFE=1 << 2, - BAD_RX_RECOVERY=1 << 3, - BOXFAILSAFE=1 << 4, - RUNAWAY_TAKEOFF=1 << 5, - CRASH_DETECTED=1 << 6, - THROTTLE=1 << 7, - ANGLE=1 << 8, - BOOT_GRACE_TIME=1 << 9, - NOPREARM=1 << 10, - LOAD=1 << 11, - CALIBRATING=1 << 12, - CLI=1 << 13, - CMS_MENU=1 << 14, - BST=1 << 15, - MSP=1 << 16, - PARALYZE=1 << 17, - GPS=1 << 18, - RESC=1 << 19, - RPMFILTER=1 << 20, - REBOOT_REQUIRED=1 << 21, - DSHOT_BITBANG=1 << 22, - ACC_CALIBRATION=1 << 23, - MOTOR_PROTOCOL=1 << 24, - ARM_SWITCH=1 << 25, - ), - # configuration state ( reboot required ) - "config_state" / FlagsEnum(Int8ub, REBOOT=1 << 8), -) -RxConfig = Struct() -RcTuning = Struct( - "roll_rate_wat" / RcFloat, - "roll_expo" / RcFloat, - "roll_rate" / RcFloat, - "pitch_rate" / RcFloat, - "yaw_rate" / RcFloat, - "tpa_rate" / RcFloat, - "throttle_mid" / RcFloat, - "throttle_expo" / RcFloat, - "tpa_breakpoint" / Int16ub, - "yaw_expo" / RcFloat, - "yaw_rate" / RcFloat, - "pitch_rate" / RcFloat, - "pitch_expo" / RcFloat, - # added in 1.41 - "throttle_limit_type" / RcFloat, - "throttle_limit_percent" / RcFloat, - # added in 1.42 - "roll_rate_limit" / Int16ub, - "pitch_rate_limit" / Int16ub, - "yaw_rate_limit" / Int16ub, - # added in 1.43 - "rates_type" / Int8ub, -) - -SensorAlignment = Struct( - "align_gyro" / Int8ub, - "align_acc" / Int8ub, - "align_mag" / Int8ub, - # if self.conf.is_inav: - # "align_opflow" / Int8ub, - # else: - "gyro_detection_flags" / Int8ub, - "gyro_to_use" / Int8ub, - "gyro_1_align" / Int8ub, - "gyro_2_align" / Int8ub, -) - -RawIMU = Struct( - "accelerometer" / RawSingle, - "gyroscope" / RawSingle, - "magnetometer" / RawSingle, -) - -__all__ = [ - "ApiVersion", - "FcVariant", - "FcVersion", - "BuildInfo", - "BoardInfo", - "Uid", - "AccTrim", - "Name", - "Status", - "StatusEx", - "RxConfig", - "RcTuning", - "SensorAlignment", - "RawIMU", -] diff --git a/bonfo/msp/utils.py b/bonfo/msp/utils.py index 2c8cd8c..af8e350 100644 --- a/bonfo/msp/utils.py +++ b/bonfo/msp/utils.py @@ -8,16 +8,28 @@ logger = logging.getLogger(__name__) -def message_builder(message_type: str, code: MSP, fields=None, debug=False): +def message_builder(message_type: str, code: MSP, fields=None, debug=False, **context): Msg = Debugger(Message) if debug else Message return Msg.build( # type: ignore - dict(message_type=message_type, packet=dict(value=dict(frame_id=code, fields=fields))) + dict(message_type=message_type, packet=dict(value=dict(frame_id=code, fields=fields)), **context) ) -def out_message_builder(code: MSP, fields=None, debug=False): - return message_builder("OUT", code, fields, debug=debug) +def out_message_builder(code: MSP, fields=None, debug=False, **context): + return message_builder("OUT", code, fields, debug=debug, **context) -def in_message_builder(code: MSP, fields=None, debug=False): - return message_builder("IN", code, fields, debug=debug) +def in_message_builder(code: MSP, fields=None, debug=False, **context): + return message_builder("IN", code, fields, debug=debug, **context) + + +def msg_packet(msg): + return msg.packet.value + + +def msg_data(msg): + try: + return msg.packet.value.fields + except Exception as e: + logger.exception("problem in msg_data", exc_info=e) + return None diff --git a/bonfo/msp/versions.py b/bonfo/msp/versions.py new file mode 100644 index 0000000..132f7a7 --- /dev/null +++ b/bonfo/msp/versions.py @@ -0,0 +1,16 @@ +from enum import Enum + +from semver import VersionInfo + +parse = VersionInfo.parse + + +class MSPVersions(Enum): + V1_40 = parse("0.1.40") + V1_41 = parse("0.1.41") + V1_42 = parse("0.1.42") + V1_43 = parse("0.1.43") + V1_44 = parse("0.1.44") + + +MSPMaxSupported = MSPVersions.V1_44 diff --git a/bonfo/profile.py b/bonfo/profile.py new file mode 100644 index 0000000..615d02e --- /dev/null +++ b/bonfo/profile.py @@ -0,0 +1,159 @@ +"""Profile package for Bonfo.""" +from __future__ import annotations + +import logging +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, AsyncIterator, Optional, Tuple + +from .msp.fields.config import SelectPID, SelectRate +from .msp.fields.statuses import StatusEx + +if TYPE_CHECKING: + from .board import Board + +logger = logging.getLogger(__name__) + + +__all__ = ["Profile"] + + +@dataclass +class Profile: + """Profile context manager. + + pid and rate properties should always reflect what is currently selected on the board. + + When dealing with internal profile tuples, the order will always be (pid, rate). + """ + + board: "Board" + # pid: int = 1 + # rate: int = 1 + # pid: Annotated[int, field(default=1)] + # rate: Annotated[int, field(default=1)] + + # pid: int + # rate: int + # TODO: move to property classes that have individual synced states and setters + _pid: int = field(default=1, init=False, repr=False) + _rate: int = field(default=1, init=False, repr=False) + + # Track profile changes before they are applied + _profile_tracker: Tuple[int, int] = field(default_factory=lambda: (1, 1), repr=False) + + # hold previous profiles for reversion purposes + _revert_to_profiles: Tuple[int, int] = field(default_factory=lambda: (1, 1), repr=False) + + class SyncedState(Enum): + """Local synced/saved status for selected profiles.""" + + UNFETCHED = 1 + FETCHING = 2 + CLEAN = 3 + AWAITING_APPLY = 4 + + _state = SyncedState.UNFETCHED + + def __str__(self) -> str: + return f"pid: {self.pid} rate: {self.rate} {self._state}" + + @property + def pid(self) -> int: + if self._state == self.SyncedState.UNFETCHED: + logger.warning("PID profile not yet fetched from board") + return self._pid + + @pid.setter + def pid(self, pid: Optional[int]) -> None: + if pid is None: + return + assert pid in range(1, 4), "PID out of range" + self._profile_tracker = (pid, self._profile_tracker[1]) + self._state = self.SyncedState.AWAITING_APPLY + + @property + def rate(self) -> int: + if self._state == self.SyncedState.UNFETCHED: + logger.warning("Rate profile not yet fetched from board") + return self._rate + + @rate.setter + def rate(self, rate: Optional[int]) -> None: + if rate is None: + return + assert rate in range(1, 7), "Rate out of range" + self._profile_tracker = (self._profile_tracker[0], rate) + self._state = self.SyncedState.AWAITING_APPLY + + async def _check_connection(self): + """Wait for a connection, then get current profiles from board.""" + await self.board.connected.wait() + await self._set_profiles_from_board() + + @asynccontextmanager + async def __call__( + self, pid: Optional[int] = None, rate: Optional[int] = None, revert_on_exit=False + ) -> AsyncIterator["Profile"]: + self._state = self.SyncedState.FETCHING + + # wait here till the board is ready + await self.board.ready.wait() + + logger.debug("Asked to set profiles to: %s", (pid, rate)) + logger.debug("Got current profiles from Board: %s", self._profile_tracker) + + self._revert_to_profiles = (self.pid, self.rate) + logger.debug("Setting _revert_to_profiles to: %s", self._revert_to_profiles) + + (self.pid, self.rate) = (pid, rate) # type:ignore + + await self.apply_changes() + + logger.debug("returning profile context") + + yield self + if revert_on_exit: + logger.debug("reverting profiles") + (self.pid, self.rate) = self._revert_to_profiles + await self.apply_changes() + + async def _set_profiles_from_board(self) -> Tuple[int, int]: + status = await (self.board > StatusEx) + if status is None: + return self._profile_tracker + self._state = self.SyncedState.CLEAN + self._profile_tracker = (self._pid, self._rate) = (status.pid_profile, status.rate_profile) + return self._profile_tracker + + async def _send_pid_to_board(self, pid) -> bool: + logger.debug("PID profile to: %s", pid) + await (self.board < SelectPID(pid)) + return True + + async def _send_rate_to_board(self, rate) -> bool: + logger.debug("Rate profile to: %s", rate) + await (self.board < SelectRate(rate)) + return True + + async def apply_changes(self) -> bool: + """Apply any locally changed profiles to the board. + + Returns False if it failed to apply. + """ + if not self._state == self.SyncedState.AWAITING_APPLY: + return False + (asked_pid, asked_rate) = self._profile_tracker + if asked_pid != self.pid: + logger.debug("PID profile differs, updating board") + await self._send_pid_to_board(asked_pid) + if asked_rate != self.rate: + logger.debug("Rate profile differs, updating board") + await self._send_rate_to_board(asked_rate) + + (found_pid, found_rate) = await self._set_profiles_from_board() + # Sanity check + if asked_pid != found_pid or asked_rate != found_rate: + return False + return True diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..d2d9bf8 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/docs/ideas.md b/docs/ideas.md index f05eef2..2bcfb20 100644 --- a/docs/ideas.md +++ b/docs/ideas.md @@ -1,6 +1,4 @@ -# Ideas - -* Support Betaflight, and other firmwares that support MSP comms +* Support Betaflight, and other firmwares that support MSP v1 * Pull down and parse a FCs configuration and save it locally * Provide a yaml configuration that can be loaded and applied to a flight controller * Settings parser and validator for different flight controller firmwares @@ -9,3 +7,6 @@ * Backup, save and apply configuration groupings * Diff between configurations * Mix and match saved configurations +* Automated tuning steps +* Message result caching and invalidation timeout +* Passthrough to betaflight CLI diff --git a/docs/installation.md b/docs/installation.md index 8c723c0..202238a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,43 +1,77 @@ -# Installation +# Stable release +## As a CLI utility -## Stable release +If you're interested in utilizing bonfo's CLI interface you can install it a couple ways to make it available to your user. -To install Bonfo configuration management, run this command in your -terminal: +Using **pipx** or **pip**: +=== "pipx" + ``` bash + $ pipx install bonfo + $ bonfo + ``` -``` console -$ pip install bonfo -``` +=== "pip" + ``` bash + $ pip install --user bonfo + $ bonfo + ``` + +## As a python module +Run one of these commands in your terminal: + +=== "poetry" + + ``` bash + $ poetry install bonfo + ``` + +=== "pip" + + ``` bash + $ pip install bonfo + ``` -This is the preferred method to install Bonfo configuration management, as it will always install the most recent stable release. +This is the preferred method to install Bonfo, as it will always install the most recent stable release. If you don't have [pip][] installed, this [Python installation guide][] can guide you through the process. -## From source +For [Poetry][], follow their [installation guide][]. -The source for Bonfo configuration management can be downloaded from +# From source + +The source for Bonfo can be downloaded from the [Github repo][]. You can either clone the public repository: -``` console +``` bash $ git clone git://github.com/destos/bonfo ``` Or download the [tarball][]: -``` console -$ curl -OJL https://github.com/destos/bonfo/tarball/master +``` bash +$ curl -OJL https://github.com/destos/bonfo/tarball/main ``` -Once you have a copy of the source, you can install it with: +Once you have a copy of the source, you can install it with pip or poetry while inside the Bonfo source directory: -``` console -$ pip install . -``` +=== "poetry" + + ``` bash + $ poetry install + ``` + +=== "pip" + + ``` bash + $ pip install . + ``` - [pip]: https://pip.pypa.io - [Python installation guide]: http://docs.python-guide.org/en/latest/starting/installation/ - [Github repo]: https://github.com/%7B%7B%20cookiecutter.github_username%20%7D%7D/%7B%7B%20cookiecutter.project_slug%20%7D%7D - [tarball]: https://github.com/%7B%7B%20cookiecutter.github_username%20%7D%7D/%7B%7B%20cookiecutter.project_slug%20%7D%7D/tarball/master +[pip]: https://pip.pypa.io +[Python installation guide]: http://docs.python-guide.org/en/latest/starting/installation/ +[Github repo]: https://github.com/destos/bonfo +[tarball]: https://github.com/destos/bonfo/tarball/main +[Poetry]: https://python-poetry.org/ +[installation guide]: https://python-poetry.org/docs/#installation diff --git a/docs/api.md b/docs/modules.md similarity index 100% rename from docs/api.md rename to docs/modules.md diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..894f62f --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,7 @@ +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #ca1643; + --md-primary-fg-color--light: #231d1e; + --md-primary-fg-color--dark: #181415; + --md-default-bg-color: #181415; + --md-hue: 345; +} diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index a20009a..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,7 +0,0 @@ -# Usage - -To use Bonfo configuration management in a project - -``` -import bonfo -``` diff --git a/docs/usage/board.md b/docs/usage/board.md new file mode 100644 index 0000000..b00cf3e --- /dev/null +++ b/docs/usage/board.md @@ -0,0 +1,96 @@ +The `Board` class is the main entrypoint to communicate with your connected flight controllers. + +# Scripting + +## Connecting manually + +One option for communicating with the board is to wait for the ready event and then perform any desired board operations. + +=== "script" + ```python + import asyncio + from rich import print + + from bonfo import Board + from bonfo.msp import StatusEx + + async def main(): + board = Board("/dev/tty.usbmodem0x80000001") + await board.ready.wait() + print(board.info) + print(str(board.profile)) + _, data = await board.get(StatusEx) + print(data) + board.disconnect() + + asyncio.run(main()) + ``` + +=== "output" + ```python + CombinedBoardInfo( + name=None, + api=ApiVersion(msp_protocol=0, api_major=1, api_minor=43), + version=FcVersion(major=4, minor=2, patch=11), + build=BuildInfo(date_time=, git_hash='948ba63'), + board=BoardInfo( + short_name='S405', + hardware_revision=0, + uses_max7456=2, + target_capabilities=, + _target_name_length=9, + target_name='STM32F405', + _board_name_length=10, + board_name='CLRACINGF4', + _manufacturer_id_length=4, + manufacturer_id='CLRA', + signature='', + mcu_type=3, + configuration_state=2, + sample_rate=16415, + configuration_problems= + ), + variant=FcVariant(variant='BTFL'), + uid=Uid(uid=ListContainer([520102400, 139540020, 926365744])) + ) + pid: 1 rate: 5 SyncedState.CLEAN + StatusEx( + cycle_time=32000, + i2c_error=0, + active_sensors=, + mode=0, + pid_profile=1, + cpuload=1280, + profile_count=3, + rate_profile=5, + additional_mode_bytes=0, + additional_mode=b'', + arming_disable_flags=, + config_state= + ) + ``` + +## Using the async context manager + +Using the much shorter async manager `connect` is preferred. + +``` python +import asyncio +from rich import print + +from bonfo import Board +from bonfo.msp import StatusEx, SensorAlignment + +async def main(): + async with Board("/dev/tty.usbmodem0x80000001").connect() as board: + print(board.info) + print(str(board.profile)) + _, data = await board.get(StatusEx) + _, sensor = await board.get(SensorAlignment) + print(data) + print(sensor) + +asyncio.run(main()) +``` + +You should see the same output as the script above. diff --git a/docs/usage/cli.md b/docs/usage/cli.md new file mode 100644 index 0000000..447d546 --- /dev/null +++ b/docs/usage/cli.md @@ -0,0 +1,5 @@ +This page provides documentation for the bonfo command line tool. + +::: mkdocs-click + :module: bonfo.cli + :command: cli diff --git a/docs/usage/index.md b/docs/usage/index.md new file mode 100644 index 0000000..476cab8 --- /dev/null +++ b/docs/usage/index.md @@ -0,0 +1,37 @@ +--- +title: Bonfo usage +--- + +Bonfo can be used a few different ways to automate configuration and communication with your flight controllers. + +There also is a [command line utility](./cli.md) provided that speeds up common user interactions and as an interface for applying previously saved configurations. + +# As a module + +``` python +# TODO: this doesn't work currently +import asyncio +from rich import print + +from bonfo import Board +from bonfo.msp.fields import Name, Features, FeatureConfig, EepromWrite + +async def main(): + async with Board("/dev/tty.usbmodem0x80000001").connect() as board: + await (board < Name("slim shady")) + name = await (board > Name) + print(f"My name is what? {name}") + # Get current enabled features... + features_conf = await (board > FeatureConfig) + print(features_conf) + # Make sure we have serial RX enabled + features_conf.features += Features.RX_SERIAL + await (board < features_conf) + # Save our changes + await (board < EepromWrite()) + # Check that they made it + features_conf = await (board > FeatureConfig) + print(features_conf) + +asyncio.run(main()) +``` diff --git a/docs/usage/messages.md b/docs/usage/messages.md new file mode 100644 index 0000000..0b46b5c --- /dev/null +++ b/docs/usage/messages.md @@ -0,0 +1,6 @@ +One of the core pieces of functionality provided by **Bonfo** is the ability send and receive MSP messages from your flight controller in an abstract manner. + +Bonfo provides these in the form of [dataclass fields] that extend the [MSPFields] class. + +[dataclass fields]: ../modules.md#bonfo.msp.fields +[mspfields]: ../modules.md#bonfo.msp.fields.base.MSPFields diff --git a/docs/usage/profiles.md b/docs/usage/profiles.md new file mode 100644 index 0000000..d47740b --- /dev/null +++ b/docs/usage/profiles.md @@ -0,0 +1,63 @@ +Betaflight gives you access to a few customization profiles when configuring your flight controller. This allows you to set specific PIDs and rate groups for different scenarios. For example, you could set a pid and rate profile aside for testing purposes, or separate your racing and freestyle rates/PIDs into different profiles. + +Currently Betaflight allows for **3 PID profiles** and **6 rate profiles**. + +# Scripting + +If you're interested in interacting with your profiles programmatically, here are a few code examples that can help. + +## Profile selection: + +To manually select the rate or PID profile update the board's profile `pid` or `rate` attributes and run the profile's `apply_changes()` method asynchronously. + +```python hl_lines="5 6 10" +async def manual_change_profile_coro(): + async with Board("/dev/tty0000").connect() as board: + print(f"Before: {board.profile}") + # > Before pid: 1 rate: 1 + board.profile.pid = 3 + board.profile.rate = 3 + print(f"After setting: {board.profile}") + # After setting: pid: 1 rate: 1 + # note: profiles not applied to the board yet + await board.profile.apply_changes() + print(f"After: {board.profile}") + # > After: pid: 2 rate: 2 +``` + +To manage the profile with a context manager call the `profile` instance. + +```python hl_lines="5" +async def change_profile_coro(): + async with Board("/dev/tty0000").connect() as board: + print(f"Before: {board.profile}") + # > Before: pid: 1 rate: 1 + async with board.profile(pid=2, rate=2) as profile: + print(f"During: {board.profile}") + # > During: pid: 2 rate: 2 + # Use board here with profile modifying or retrieval commands. + # Using profile relevant commands outside the context manager scope should + # interact with the profiles set in our board.profile() call. + print(f"After: {board.profile}") + # > After: pid: 2 rate: 2 +``` + +## Revert on exit + +To set the board's pid and rate profile to their initial values after exiting the context manager, use the `revert_on_exit` kwarg set to `True`. + +```python hl_lines="5" +async def change_profile_coro(): + async with Board("/dev/tty0000").connect() as board: + print(f"Before: {board.profile}") + # > pid: 1 rate: 1 + async with board.profile(pid=2, rate=2, revert_on_exit=True) as profile: + print(f"During: {board.profile}") + # > pid: 2 rate: 2 + # Use board here with profile modifying or retrieval commands. + # Using profile relevant commands outside the context manager scope should + # interact with the profiles before our board.profile() call. + print(f"After: {board.profile}") + # > pid: 1 rate: 1 + # Profiles were reverted on context manager exit. +``` diff --git a/makefile b/makefile index ee2e1c4..9a85795 100644 --- a/makefile +++ b/makefile @@ -7,10 +7,11 @@ test: format lint unittest format: ${prun} isort $(sources) tests ${prun} black $(sources) tests + # ${prun} autoflake8 -r -i --ignore-init-module-imports $(sources) tests lint: ${prun} flake8 $(sources) tests - ${prun} mypy $(sources) tests + ${prun} mypy $(sources) unittest: ${prun} pytest @@ -29,3 +30,9 @@ clean: docs: ${prun} mkdocs build + +docs-serve: + ${prun} mkdocs serve + +install: + poetry install -E doc -E test -E dev diff --git a/mkdocs.yml b/mkdocs.yml index e4e5a95..6a4f707 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,28 +1,57 @@ -site_name: Bonfo configuration management +site_name: Bonfo - drone configuration management site_url: https://destos.github.io/bonfo repo_url: https://github.com/destos/bonfo repo_name: destos/bonfo +edit_uri: edit/main/docs/ #strict: true nav: - Home: index.md - - Ideas: ideas.md - Installation: installation.md - - Usage: usage.md - - Modules: api.md + - Ideas: ideas.md + - Usage: + - usage/index.md + - Board: usage/board.md + - Profiles: usage/profiles.md + - Sending & Receiving Messages: usage/messages.md + - CLI: usage/cli.md + - Modules: modules.md - Contributing: contributing.md - Changelog: changelog.md theme: + logo: assets/logo.svg + palette: + scheme: slate + # primary: purple + # accent: orange + # - scheme: subsiv-light + # toggle: + # icon: material/toggle-switch-off-outline + # name: Switch to dark mode + # - scheme: subsiv-dark + # toggle: + # icon: material/toggle-switch + # name: Switch to light mode + # - media: "(prefers-color-scheme: light)" + # scheme: subsiv-light + # toggle: + # icon: material/toggle-switch-off-outline + # name: Switch to dark mode + # - media: "(prefers-color-scheme: dark)" + # scheme: subsiv-dark + # toggle: + # icon: material/toggle-switch + # name: Switch to light mode name: material language: en - #logo: assets/logo.png - palette: - scheme: preference - primary: indigo - accent: indigo features: + - header.autohide - navigation.indexes + - navigation.expanded - navigation.instant + - navigation.tabs - navigation.tabs.sticky + - navigation.sections + - toc.integrate markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji @@ -33,11 +62,14 @@ markdown_extensions: - pymdownx.tilde - pymdownx.tabbed - attr_list - - pymdownx.arithmatex: - generic: true + - md_in_html + # - pymdownx.arithmatex: + # generic: true - pymdownx.highlight: linenums: false - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true - pymdownx.inlinehilite - pymdownx.details - admonition @@ -46,8 +78,11 @@ markdown_extensions: permalink: true slugify: !!python/name:pymdownx.slugs.uslugify - meta + - mkdocs-click plugins: - include-markdown + - git-revision-date: + enabled_if_env: CI - search: lang: en - mkdocstrings: @@ -55,22 +90,26 @@ plugins: - bonfo extra: social: - - icon: fontawesome/brands/twitter - # replace with your own tweet link below - link: https://github.com/waynerv/cookiecutter-pypackage - name: Tweet - - icon: fontawesome/brands/facebook - # replace with your own facebook link below - link: https://github.com/waynerv/cookiecutter-pypackage - name: Facebook + # - icon: fontawesome/brands/twitter + # # replace with your own tweet link below + # link: https://github.com/waynerv/cookiecutter-pypackage + # name: Tweet + # - icon: fontawesome/brands/facebook + # # replace with your own facebook link below + # link: https://github.com/waynerv/cookiecutter-pypackage + # name: Facebook - icon: fontawesome/brands/github link: https://github.com/destos/bonfo name: Github - icon: material/email - link: "mailto:patrick@forringer.com" + link: "mailto:patrick@forringer.com?subject=Bonfo" # to enable disqus, uncomment the following and put your disqus id below # disqus: disqus_id +extra_css: + - stylesheets/extra.css # uncomment the following and put your google tracking id below to enable GA #google_analytics: #- UA-xxx #- auto + +# TODO: add tags to pages and tags index diff --git a/poetry.lock b/poetry.lock index 150cbd8..c05e369 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,13 +1,13 @@ [[package]] -name = "astunparse" -version = "1.6.3" -description = "An AST unparser for Python" +name = "arrow" +version = "1.2.2" +description = "Better dates & times for Python" category = "main" -optional = true -python-versions = "*" +optional = false +python-versions = ">=3.6" [package.dependencies] -six = ">=1.6.1,<2.0" +python-dateutil = ">=2.7.0" [[package]] name = "atomicwrites" @@ -26,10 +26,10 @@ optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] [[package]] name = "autoflake8" @@ -44,43 +44,41 @@ pyflakes = ">=2.3.0" [[package]] name = "black" -version = "21.12b0" +version = "22.3.0" description = "The uncompromising code formatter." category = "main" optional = true python-versions = ">=3.6.2" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "4.1.0" +version = "5.0.0" description = "An easy safelist-based HTML-sanitizing tool." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -packaging = "*" six = ">=1.9.0" webencodings = "*" +[package.extras] +dev = ["black (==22.3.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.942)", "pip-tools (==6.5.1)", "pytest (==7.1.1)", "sphinx (==4.3.2)", "tox (==3.24.5)", "twine (==4.0.0)", "wheel (==0.37.1)"] + [[package]] name = "bump2version" version = "1.0.1" @@ -91,11 +89,11 @@ python-versions = ">=3.5" [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.5.18.1" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = true -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "cffi" @@ -129,11 +127,11 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.1" +version = "8.1.3" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -173,7 +171,7 @@ name = "construct-typing" version = "0.5.2" description = "Extension for the python package 'construct' that adds typing features" category = "main" -optional = true +optional = false python-versions = ">=3.7" [package.dependencies] @@ -181,18 +179,21 @@ construct = "2.10.67" [[package]] name = "coverage" -version = "6.3.2" +version = "6.3.3" description = "Code coverage measurement for Python" category = "main" optional = true python-versions = ">=3.7" +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + [package.extras] toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.2" +version = "37.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = true @@ -202,16 +203,16 @@ python-versions = ">=3.6" cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "dataclass-wizard" -version = "0.22.0" +version = "0.22.1" description = "Marshal dataclasses to/from JSON. Use field properties with initial values. Construct a dataclass schema with JSON input." category = "main" optional = false @@ -242,7 +243,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "filelock" -version = "3.6.0" +version = "3.7.0" description = "A platform independent file lock." category = "main" optional = true @@ -279,7 +280,7 @@ pydocstyle = ">=2.1" [[package]] name = "ghp-import" -version = "2.0.2" +version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." category = "main" optional = true @@ -289,11 +290,33 @@ python-versions = "*" python-dateutil = ">=2.8.1" [package.extras] -dev = ["twine", "markdown", "flake8", "wheel"] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "gitdb" +version = "4.0.9" +description = "Git Object Database" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.27" +description = "GitPython is a python library used to interact with Git repositories" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" [[package]] name = "identify" -version = "2.4.12" +version = "2.5.1" description = "File identification library for Python" category = "main" optional = true @@ -322,9 +345,9 @@ python-versions = ">=3.7" zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -343,26 +366,26 @@ optional = true python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "jeepney" -version = "0.7.1" +version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] -trio = ["trio", "async-generator"] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] [[package]] name = "jinja2" -version = "3.1.1" +version = "3.1.2" description = "A very fast and expressive template engine." category = "main" optional = true @@ -389,8 +412,8 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] [[package]] name = "loca" @@ -402,7 +425,7 @@ python-versions = ">=3.7" [[package]] name = "markdown" -version = "3.3.6" +version = "3.3.7" description = "Python implementation of Markdown." category = "main" optional = true @@ -473,33 +496,59 @@ python-versions = ">=3.7" Markdown = ">=3.3" mkdocs = ">=1.1" +[[package]] +name = "mkdocs-click" +version = "0.7.0" +description = "An MkDocs extension to generate documentation for Click command line applications" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.1,<9.0" +markdown = ">=3.0.0,<4.0.0" + +[[package]] +name = "mkdocs-git-revision-date-plugin" +version = "0.3.2" +description = "MkDocs plugin for setting revision date from git per markdown file." +category = "main" +optional = true +python-versions = ">=3.4" + +[package.dependencies] +GitPython = "*" +jinja2 = "*" +mkdocs = ">=0.17" + [[package]] name = "mkdocs-include-markdown-plugin" -version = "3.3.0" +version = "3.4.1" description = "Mkdocs Markdown includer plugin." category = "main" optional = true python-versions = ">=3.6" [package.extras] -dev = ["bump2version (==1.0.1)", "flake8 (==3.9.2)", "flake8-implicit-str-concat (==0.2.0)", "flake8-print (==4.0.0)", "isort (==5.9.1)", "mdpo (==0.3.61)", "pre-commit (==2.13.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "pyupgrade (==2.19.4)", "yamllint (==1.26.1)"] +dev = ["bump2version (==1.0.1)", "flake8 (==3.9.2)", "flake8-implicit-str-concat (==0.2.0)", "flake8-print (==4.0.0)", "isort (==5.9.1)", "mdpo (==0.3.61)", "mkdocs (==1.2.3)", "pre-commit (==2.13.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "pyupgrade (==2.19.4)", "yamllint (==1.26.1)"] test = ["pytest (==6.2.5)", "pytest-cov (==3.0.0)"] [[package]] name = "mkdocs-material" -version = "8.2.8" -description = "A Material Design theme for MkDocs" +version = "8.5.4" +description = "Documentation that simply works" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -jinja2 = ">=2.11.1" +jinja2 = ">=3.0.2" markdown = ">=3.2" mkdocs = ">=1.3.0" mkdocs-material-extensions = ">=1.0.3" -pygments = ">=2.10" -pymdown-extensions = ">=9.0" +pygments = ">=2.12" +pymdown-extensions = ">=9.4" +requests = ">=2.26" [[package]] name = "mkdocs-material-extensions" @@ -545,7 +594,7 @@ pytkdocs = ">=0.14" [[package]] name = "mypy" -version = "0.940" +version = "0.950" description = "Optional static typing for Python" category = "main" optional = true @@ -553,7 +602,7 @@ python-versions = ">=3.6" [package.dependencies] mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=3.10" [package.extras] @@ -596,6 +645,14 @@ category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +[[package]] +name = "pip" +version = "20.3.4" +description = "The PyPA recommended tool for installing Python packages." +category = "main" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + [[package]] name = "pkginfo" version = "1.8.2" @@ -609,14 +666,14 @@ testing = ["coverage", "nose"] [[package]] name = "platformdirs" -version = "2.5.1" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" optional = true python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] @@ -633,11 +690,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.19.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "main" optional = true -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" @@ -695,30 +752,30 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.11.2" +version = "2.12.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "pymdown-extensions" -version = "9.3" +version = "9.4" description = "Extension pack for Python Markdown." category = "main" optional = true python-versions = ">=3.7" [package.dependencies] -Markdown = ">=3.2" +markdown = ">=3.2" [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] @@ -734,13 +791,24 @@ python-versions = "*" [package.extras] cp2110 = ["hidapi"] +[[package]] +name = "pyserial-asyncio" +version = "0.6" +description = "Python Serial Port Extension - Asynchronous I/O support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pyserial = "*" + [[package]] name = "pytest" -version = "6.2.5" +version = "7.1.2" description = "pytest: simple powerful testing with Python" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -750,26 +818,53 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.18.3" +description = "Pytest support for asyncio" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" -version = "2.12.1" +version = "3.0.0" description = "Pytest plugin for measuring coverage." category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -coverage = ">=5.2.1" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" -toml = "*" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.7.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-watcher" @@ -787,7 +882,7 @@ name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" -optional = true +optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] @@ -801,9 +896,6 @@ category = "main" optional = true python-versions = ">=3.7" -[package.dependencies] -astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} - [package.extras] numpy-style = ["docstring_parser (>=0.7)"] @@ -836,11 +928,11 @@ pyyaml = "*" [[package]] name = "readme-renderer" -version = "34.0" +version = "35.0" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] bleach = ">=2.1.0" @@ -892,11 +984,11 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "12.0.1" +version = "12.4.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">=3.6.3,<4.0.0" [package.dependencies] commonmark = ">=0.9.0,<0.10.0" @@ -907,7 +999,7 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "rich-click" -version = "1.3.0" +version = "1.4" description = "Format click help output nicely with rich" category = "main" optional = false @@ -915,7 +1007,7 @@ python-versions = ">=3.7" [package.dependencies] click = ">=7" -rich = ">=10" +rich = ">=10.7.0" [package.extras] dev = ["pre-commit"] @@ -923,7 +1015,7 @@ typer = ["typer (>=0.4)"] [[package]] name = "secretstorage" -version = "3.3.1" +version = "3.3.2" description = "Python bindings to FreeDesktop.org Secret Service API" category = "main" optional = true @@ -933,14 +1025,30 @@ python-versions = ">=3.6" cryptography = ">=2.0" jeepney = ">=0.6" +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "main" +optional = true +python-versions = ">=3.6" + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -949,6 +1057,17 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "textual" +version = "0.1.18" +description = "Text User Interface using Rich" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +rich = ">=12.3.0,<13.0.0" + [[package]] name = "toml" version = "0.10.2" @@ -959,15 +1078,15 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "tox" -version = "3.24.5" +version = "3.25.0" description = "tox is a generic virtualenv management and test command line tool" category = "main" optional = true @@ -985,34 +1104,17 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] - -[[package]] -name = "tqdm" -version = "4.63.1" -description = "Fast, Extensible Progress Meter" -category = "main" -optional = true -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -notebook = ["ipywidgets (>=6)"] -telegram = ["requests"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "twine" -version = "3.8.0" +version = "4.0.0" description = "Collection of utilities for publishing packages on PyPI" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -colorama = ">=0.4.3" importlib-metadata = ">=3.6" keyring = ">=15.1" pkginfo = ">=1.8.1" @@ -1020,16 +1122,24 @@ readme-renderer = ">=21.0" requests = ">=2.20" requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" rfc3986 = ">=1.4.0" -tqdm = ">=4.14" +rich = ">=12.0.0" urllib3 = ">=1.26.0" +[[package]] +name = "types-mock" +version = "4.0.13" +description = "Typing stubs for mock" +category = "main" +optional = true +python-versions = "*" + [[package]] name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" @@ -1040,13 +1150,13 @@ optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.14.0" +version = "20.14.1" description = "Virtual Python Environment builder" category = "main" optional = true @@ -1060,11 +1170,11 @@ six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "watchdog" -version = "2.1.7" +version = "2.1.8" description = "Filesystem events monitoring" category = "main" optional = true @@ -1083,30 +1193,30 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.7.0" +version = "3.8.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = true python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] -dev = ["tox", "pre-commit", "virtualenv", "pip", "twine", "toml", "bump2version", "autoflake8", "construct-typing", "pytest-watcher"] -doc = ["mkdocs", "mkdocs-include-markdown-plugin", "mkdocs-material", "mkdocstrings", "mkdocs-autorefs"] -test = ["pytest", "black", "isort", "mypy", "flake8", "flake8-docstrings", "pytest-cov"] +dev = ["tox", "pre-commit", "virtualenv", "pip", "twine", "toml", "black", "isort", "mypy", "bump2version", "autoflake8", "pytest-watcher", "flake8", "flake8-docstrings"] +doc = ["mkdocs", "mkdocs-click", "mkdocs-material", "mkdocs-autorefs", "mkdocs-include-markdown-plugin", "mkdocs-git-revision-date-plugin", "mkdocstrings"] +test = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "types-mock"] [metadata] lock-version = "1.1" -python-versions = ">=3.8,<4.0" -content-hash = "ebc9888b3e788355182be64d643bef57e81ecc9445eee417dc7d9cb181a09ae2" +python-versions = ">=3.9,<4.0" +content-hash = "dcb38d342e99e05498994c7ca308cc3458d7448eecac87c7077385a0b8b7aee2" [metadata.files] -astunparse = [ - {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, - {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +arrow = [ + {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, + {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1121,20 +1231,41 @@ autoflake8 = [ {file = "autoflake8-0.3.2.tar.gz", hash = "sha256:4465a10186e29830eb5df5ada7eed29fdc0918b7d8eafbb06a681236639ca695"}, ] black = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] bleach = [ - {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, - {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, + {file = "bleach-5.0.0-py3-none-any.whl", hash = "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1"}, + {file = "bleach-5.0.0.tar.gz", hash = "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565"}, ] bump2version = [ {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, ] certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, + {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, ] cffi = [ {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, @@ -1197,8 +1328,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1216,73 +1347,75 @@ construct-typing = [ {file = "construct_typing-0.5.2-py3-none-any.whl", hash = "sha256:553ecf36b44ae8d87e2f9c28a6fc8ffd75d050bd0b66ac72e433046c45b52116"}, ] coverage = [ - {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, - {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, - {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, - {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, - {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, - {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, - {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, - {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, - {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, - {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, - {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, - {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, + {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"}, + {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"}, + {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"}, + {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"}, + {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"}, + {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"}, + {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"}, + {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"}, + {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"}, + {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"}, + {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"}, + {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, + {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, ] cryptography = [ - {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"}, - {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf"}, - {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86"}, - {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2"}, - {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb"}, - {file = "cryptography-36.0.2-cp36-abi3-win32.whl", hash = "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6"}, - {file = "cryptography-36.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29"}, - {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7"}, - {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e"}, - {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51"}, - {file = "cryptography-36.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3"}, - {file = "cryptography-36.0.2.tar.gz", hash = "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9"}, + {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, + {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, + {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, + {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, + {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, + {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, + {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, + {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, + {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, ] dataclass-wizard = [ - {file = "dataclass-wizard-0.22.0.tar.gz", hash = "sha256:1a870882a8ff19e7ab9ede7b672b2f7c1ce8d69bbd2fc6d9629da749227268fd"}, - {file = "dataclass_wizard-0.22.0-py2.py3-none-any.whl", hash = "sha256:8d6dab6b406d6c6c1b2d6ec5bfc12f3adab952d3f02573233b5a603db1e515e4"}, + {file = "dataclass-wizard-0.22.1.tar.gz", hash = "sha256:77a6584a758788ca0d32cf9d09ebbd3cb12e5eb5dcded4d846e358257f5fc3a0"}, + {file = "dataclass_wizard-0.22.1-py2.py3-none-any.whl", hash = "sha256:e2cef176cbfac9f5adc812a8957d67aafc0c9c85b8c72ac7ca0ae0e7e692db38"}, ] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, @@ -1293,8 +1426,8 @@ docutils = [ {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, ] filelock = [ - {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, - {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, + {file = "filelock-3.7.0-py3-none-any.whl", hash = "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6"}, + {file = "filelock-3.7.0.tar.gz", hash = "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1305,12 +1438,20 @@ flake8-docstrings = [ {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, ] ghp-import = [ - {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, - {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] +gitdb = [ + {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, + {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, +] +gitpython = [ + {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, + {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, ] identify = [ - {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"}, - {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"}, + {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, + {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1329,12 +1470,12 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jeepney = [ - {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, - {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, ] jinja2 = [ - {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"}, - {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"}, + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] keyring = [ {file = "keyring-23.5.0-py3-none-any.whl", hash = "sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"}, @@ -1345,8 +1486,8 @@ loca = [ {file = "loca-2.0.1.tar.gz", hash = "sha256:13dec9098d57b546bd9bb8e21ade0d043cb6e6dc184f6b409b2496fdd0f7c064"}, ] markdown = [ - {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, - {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, + {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, + {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, @@ -1406,13 +1547,20 @@ mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, ] +mkdocs-click = [ + {file = "mkdocs_click-0.7.0-py3-none-any.whl", hash = "sha256:a9e92631f82a28a038ec8482e687eaaa0e3d506ca8aaa97ef57f0f785029de6e"}, + {file = "mkdocs_click-0.7.0.tar.gz", hash = "sha256:b869715b9461eeef5ce0e88bfb8d711f75eda3c5e75b63e7bdacc8566806c1eb"}, +] +mkdocs-git-revision-date-plugin = [ + {file = "mkdocs_git_revision_date_plugin-0.3.2-py3-none-any.whl", hash = "sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef"}, +] mkdocs-include-markdown-plugin = [ - {file = "mkdocs_include_markdown_plugin-3.3.0-py3-none-any.whl", hash = "sha256:d820523a28c893b92f4c5c383332d36379874a3741f4fe04f11bf41afca18a2b"}, - {file = "mkdocs_include_markdown_plugin-3.3.0.tar.gz", hash = "sha256:1462d343f150b59e78f0aebf9ff9ef9d591fbd9630193e0c9d661abf6df92d6a"}, + {file = "mkdocs_include_markdown_plugin-3.4.1-py3-none-any.whl", hash = "sha256:d1cacce5f2edac32c066fe452379ff06763eec20c0850f8b431e5eac785ce5b8"}, + {file = "mkdocs_include_markdown_plugin-3.4.1.tar.gz", hash = "sha256:d9f20fc1cdb693d54562e9ca64fd0a750603190625854d95d782009621d96fcb"}, ] mkdocs-material = [ - {file = "mkdocs-material-8.2.8.tar.gz", hash = "sha256:f0696538929b3778b064f650589eb2027e4a2c0b3ab37afa8900de7924100901"}, - {file = "mkdocs_material-8.2.8-py2.py3-none-any.whl", hash = "sha256:16ccd382431d5148a4bdbc37c949d0ea5f80e3e9bcce3bd0d774716e5aad9dff"}, + {file = "mkdocs_material-8.5.4-py3-none-any.whl", hash = "sha256:aec2f0f2143109f8388aadf76e6fff749a2b74ebe730d0f674c65b53da89d19d"}, + {file = "mkdocs_material-8.5.4.tar.gz", hash = "sha256:70dc47820d4765b77968b9119f2957d09b4d8d328d950bee4544ff224d5c7b36"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, @@ -1427,29 +1575,29 @@ mkdocstrings-python-legacy = [ {file = "mkdocstrings_python_legacy-0.2.2-py3-none-any.whl", hash = "sha256:379107a3a5b8db9b462efc4493c122efe21e825e3702425dbd404621302a563a"}, ] mypy = [ - {file = "mypy-0.940-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0fdc9191a49c77ab5fa0439915d405e80a1118b163ab03cd2a530f346b12566a"}, - {file = "mypy-0.940-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1903c92ff8642d521b4627e51a67e49f5be5aedb1fb03465b3aae4c3338ec491"}, - {file = "mypy-0.940-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471af97c35a32061883b0f8a3305ac17947fd42ce962ca9e2b0639eb9141492f"}, - {file = "mypy-0.940-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:13677cb8b050f03b5bb2e8bf7b2668cd918b001d56c2435082bbfc9d5f730f42"}, - {file = "mypy-0.940-cp310-cp310-win_amd64.whl", hash = "sha256:2efd76893fb8327eca7e942e21b373e6f3c5c083ff860fb1e82ddd0462d662bd"}, - {file = "mypy-0.940-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8fe1bfab792e4300f80013edaf9949b34e4c056a7b2531b5ef3a0fb9d598ae2"}, - {file = "mypy-0.940-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2dba92f58610d116f68ec1221fb2de2a346d081d17b24a784624389b17a4b3f9"}, - {file = "mypy-0.940-cp36-cp36m-win_amd64.whl", hash = "sha256:712affcc456de637e774448c73e21c84dfa5a70bcda34e9b0be4fb898a9e8e07"}, - {file = "mypy-0.940-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8aaf18d0f8bc3ffba56d32a85971dfbd371a5be5036da41ac16aefec440eff17"}, - {file = "mypy-0.940-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:51be997c1922e2b7be514a5215d1e1799a40832c0a0dee325ba8794f2c48818f"}, - {file = "mypy-0.940-cp37-cp37m-win_amd64.whl", hash = "sha256:628f5513268ebbc563750af672ccba5eef7f92d2d90154233edd498dfb98ca4e"}, - {file = "mypy-0.940-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:68038d514ae59d5b2f326be502a359160158d886bd153fc2489dbf7a03c44c96"}, - {file = "mypy-0.940-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2fa5f2d597478ccfe1f274f8da2f50ea1e63da5a7ae2342c5b3b2f3e57ec340"}, - {file = "mypy-0.940-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b1a116c451b41e35afc09618f454b5c2704ba7a4e36f9ff65014fef26bb6075b"}, - {file = "mypy-0.940-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f66f2309cdbb07e95e60e83fb4a8272095bd4ea6ee58bf9a70d5fb304ec3e3f"}, - {file = "mypy-0.940-cp38-cp38-win_amd64.whl", hash = "sha256:3ac14949677ae9cb1adc498c423b194ad4d25b13322f6fe889fb72b664c79121"}, - {file = "mypy-0.940-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6eab2bcc2b9489b7df87d7c20743b66d13254ad4d6430e1dfe1a655d51f0933d"}, - {file = "mypy-0.940-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0b52778a018559a256c819ee31b2e21e10b31ddca8705624317253d6d08dbc35"}, - {file = "mypy-0.940-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9d7647505bf427bc7931e8baf6cacf9be97e78a397724511f20ddec2a850752"}, - {file = "mypy-0.940-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a0e5657ccaedeb5fdfda59918cc98fc6d8a8e83041bc0cec347a2ab6915f9998"}, - {file = "mypy-0.940-cp39-cp39-win_amd64.whl", hash = "sha256:83f66190e3c32603217105913fbfe0a3ef154ab6bbc7ef2c989f5b2957b55840"}, - {file = "mypy-0.940-py3-none-any.whl", hash = "sha256:a168da06eccf51875fdff5f305a47f021f23f300e2b89768abdac24538b1f8ec"}, - {file = "mypy-0.940.tar.gz", hash = "sha256:71bec3d2782d0b1fecef7b1c436253544d81c1c0e9ca58190aed9befd8f081c5"}, + {file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"}, + {file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"}, + {file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"}, + {file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"}, + {file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"}, + {file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"}, + {file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"}, + {file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"}, + {file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"}, + {file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"}, + {file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"}, + {file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"}, + {file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"}, + {file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"}, + {file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"}, + {file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"}, + {file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"}, + {file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"}, + {file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"}, + {file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"}, + {file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"}, + {file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"}, + {file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1467,21 +1615,25 @@ pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] +pip = [ + {file = "pip-20.3.4-py2.py3-none-any.whl", hash = "sha256:217ae5161a0e08c0fb873858806e3478c9775caffce5168b50ec885e358c199d"}, + {file = "pip-20.3.4.tar.gz", hash = "sha256:6773934e5f5fc3eaa8c5a44949b5b924fc122daa0a8aa9f80c835b4ca2a543fc"}, +] pkginfo = [ {file = "pkginfo-1.8.2-py2.py3-none-any.whl", hash = "sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"}, {file = "pkginfo-1.8.2.tar.gz", hash = "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, - {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, + {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, + {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1504,28 +1656,41 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, - {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, + {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, + {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-9.3.tar.gz", hash = "sha256:a80553b243d3ed2d6c27723bcd64ca9887e560e6f4808baa96f36e93061eaf90"}, - {file = "pymdown_extensions-9.3-py3-none-any.whl", hash = "sha256:b37461a181c1c8103cfe1660081726a0361a8294cbfda88e5b02cefe976f0546"}, + {file = "pymdown_extensions-9.4-py3-none-any.whl", hash = "sha256:5b7432456bf555ce2b0ab3c2439401084cda8110f24f6b3ecef952b8313dfa1b"}, + {file = "pymdown_extensions-9.4.tar.gz", hash = "sha256:1baa22a60550f731630474cad28feb0405c8101f1a7ddc3ec0ed86ee510bcc43"}, ] pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pyserial = [ {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, ] +pyserial-asyncio = [ + {file = "pyserial-asyncio-0.6.tar.gz", hash = "sha256:b6032923e05e9d75ec17a5af9a98429c46d2839adfaf80604d52e0faacd7a32f"}, + {file = "pyserial_asyncio-0.6-py3-none-any.whl", hash = "sha256:de9337922619421b62b9b1a84048634b3ac520e1d690a674ed246a2af7ce1fc5"}, +] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] pytest-cov = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytest-mock = [ + {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, + {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, ] pytest-watcher = [ {file = "pytest-watcher-0.2.3.tar.gz", hash = "sha256:1937dd97e72caafd371d8cea7b3d70c88ff4fe35e6cdecb29c41bbdcbf1dcc2b"}, @@ -1551,6 +1716,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -1583,8 +1755,8 @@ pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] readme-renderer = [ - {file = "readme_renderer-34.0-py3-none-any.whl", hash = "sha256:262510fe6aae81ed4e94d8b169077f325614c0b1a45916a80442c6576264a9c2"}, - {file = "readme_renderer-34.0.tar.gz", hash = "sha256:dfb4d17f21706d145f7473e0b61ca245ba58e810cf9b2209a48239677f82e5b0"}, + {file = "readme_renderer-35.0-py3-none-any.whl", hash = "sha256:73b84905d091c31f36e50b4ae05ae2acead661f6a09a9abb4df7d2ddcdb6a698"}, + {file = "readme_renderer-35.0.tar.gz", hash = "sha256:a727999acfc222fc21d82a12ed48c957c4989785e5865807c65a487d21677497"}, ] requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, @@ -1599,88 +1771,101 @@ rfc3986 = [ {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, ] rich = [ - {file = "rich-12.0.1-py3-none-any.whl", hash = "sha256:ce5c714e984a2d185399e4e1dd1f8b2feacb7cecfc576f1522425643a36a57ea"}, - {file = "rich-12.0.1.tar.gz", hash = "sha256:3fba9dd15ebe048e2795a02ac19baee79dc12cc50b074ef70f2958cd651b59a9"}, + {file = "rich-12.4.1-py3-none-any.whl", hash = "sha256:d13c6c90c42e24eb7ce660db397e8c398edd58acb7f92a2a88a95572b838aaa4"}, + {file = "rich-12.4.1.tar.gz", hash = "sha256:d239001c0fb7de985e21ec9a4bb542b5150350330bbc1849f835b9cbc8923b91"}, ] rich-click = [ - {file = "rich-click-1.3.0.tar.gz", hash = "sha256:13985c97e723fd156f53b9d3f66418d4dd588161b55661fb60ee52e568341747"}, - {file = "rich_click-1.3.0-py3-none-any.whl", hash = "sha256:dbe52db129f5295ce7680ebc646c9da5ef5fb1a116a6d6e01ea6cb97334fe102"}, + {file = "rich-click-1.4.tar.gz", hash = "sha256:bcb4662aaf0ac15edf8863e95c72ef3608a1ef346a8cc1a505115ba5eeeb0260"}, + {file = "rich_click-1.4-py3-none-any.whl", hash = "sha256:35b42aa8933894b17de0f2a6c5ca4aeed429e660b39d34fc29567684d58d3c6b"}, ] secretstorage = [ - {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, - {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, + {file = "SecretStorage-3.3.2-py3-none-any.whl", hash = "sha256:755dc845b6ad76dcbcbc07ea3da75ae54bb1ea529eb72d15f83d26499a5df319"}, + {file = "SecretStorage-3.3.2.tar.gz", hash = "sha256:0a8eb9645b320881c222e827c26f4cfcf55363e8b374a021981ef886657a912f"}, +] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +smmap = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +textual = [ + {file = "textual-0.1.18-py3-none-any.whl", hash = "sha256:59110935418c597c1c50876edfd6799ad5b59d51a91ca6744fc45e36eb89638e"}, + {file = "textual-0.1.18.tar.gz", hash = "sha256:b2883f8ed291de58b9aa73de6d24bbaae0174687487458a4eb2a7c188a2acf23"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tox = [ - {file = "tox-3.24.5-py2.py3-none-any.whl", hash = "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c"}, - {file = "tox-3.24.5.tar.gz", hash = "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993"}, -] -tqdm = [ - {file = "tqdm-4.63.1-py2.py3-none-any.whl", hash = "sha256:6461b009d6792008d0000e1b0c7ca50195ec78c0e808a3a6b668a56a3236c3a5"}, - {file = "tqdm-4.63.1.tar.gz", hash = "sha256:4230a49119a416c88cc47d0d2d32d5d90f1a282d5e497d49801950704e49863d"}, + {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, + {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, ] twine = [ - {file = "twine-3.8.0-py3-none-any.whl", hash = "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8"}, - {file = "twine-3.8.0.tar.gz", hash = "sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19"}, + {file = "twine-4.0.0-py3-none-any.whl", hash = "sha256:6f7496cf14a3a8903474552d5271c79c71916519edb42554f23f42a8563498a9"}, + {file = "twine-4.0.0.tar.gz", hash = "sha256:817aa0c0bdc02a5ebe32051e168e23c71a0608334e624c793011f120dbbc05b7"}, +] +types-mock = [ + {file = "types-mock-4.0.13.tar.gz", hash = "sha256:82c819a5ba384c47a65c148df062138f38790b61d7e6b40eaeb8f2179f28f842"}, + {file = "types_mock-4.0.13-py3-none-any.whl", hash = "sha256:7c1db9f937109d96da14e5ed95c89920c3e4913eeff44a4adfea38eb335d0c99"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] urllib3 = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] virtualenv = [ - {file = "virtualenv-20.14.0-py2.py3-none-any.whl", hash = "sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66"}, - {file = "virtualenv-20.14.0.tar.gz", hash = "sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"}, + {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, + {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, ] watchdog = [ - {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"}, - {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"}, - {file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"}, - {file = "watchdog-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa"}, - {file = "watchdog-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab"}, - {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a"}, - {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8"}, - {file = "watchdog-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2"}, - {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99"}, - {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b"}, - {file = "watchdog-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd"}, - {file = "watchdog-2.1.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420"}, - {file = "watchdog-2.1.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_armv7l.whl", hash = "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_i686.whl", hash = "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64.whl", hash = "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"}, - {file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"}, - {file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"}, - {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, - {file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"}, - {file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"}, + {file = "watchdog-2.1.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:676263bee67b165f16b05abc52acc7a94feac5b5ab2449b491f1a97638a79277"}, + {file = "watchdog-2.1.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aa68d2d9a89d686fae99d28a6edf3b18595e78f5adf4f5c18fbfda549ac0f20c"}, + {file = "watchdog-2.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e2e51c53666850c3ecffe9d265fc5d7351db644de17b15e9c685dd3cdcd6f97"}, + {file = "watchdog-2.1.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7721ac736170b191c50806f43357407138c6748e4eb3e69b071397f7f7aaeedd"}, + {file = "watchdog-2.1.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ce7376aed3da5fd777483fe5ebc8475a440c6d18f23998024f832134b2938e7b"}, + {file = "watchdog-2.1.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f9ee4c6bf3a1b2ed6be90a2d78f3f4bbd8105b6390c04a86eb48ed67bbfa0b0b"}, + {file = "watchdog-2.1.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:68dbe75e0fa1ba4d73ab3f8e67b21770fbed0651d32ce515cd38919a26873266"}, + {file = "watchdog-2.1.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0c520009b8cce79099237d810aaa19bc920941c268578436b62013b2f0102320"}, + {file = "watchdog-2.1.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efcc8cbc1b43902571b3dce7ef53003f5b97fe4f275fe0489565fc6e2ebe3314"}, + {file = "watchdog-2.1.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:746e4c197ec1083581bb1f64d07d1136accf03437badb5ff8fcb862565c193b2"}, + {file = "watchdog-2.1.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ae17b6be788fb8e4d8753d8d599de948f0275a232416e16436363c682c6f850"}, + {file = "watchdog-2.1.8-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ddde157dc1447d8130cb5b8df102fad845916fe4335e3d3c3f44c16565becbb7"}, + {file = "watchdog-2.1.8-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4978db33fc0934c92013ee163a9db158ec216099b69fce5aec790aba704da412"}, + {file = "watchdog-2.1.8-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b962de4d7d92ff78fb2dbc6a0cb292a679dea879a0eb5568911484d56545b153"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1e5d0fdfaa265c29dc12621913a76ae99656cf7587d03950dfeb3595e5a26102"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_armv7l.whl", hash = "sha256:036ed15f7cd656351bf4e17244447be0a09a61aaa92014332d50719fc5973bc0"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_i686.whl", hash = "sha256:2962628a8777650703e8f6f2593065884c602df7bae95759b2df267bd89b2ef5"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_ppc64.whl", hash = "sha256:156ec3a94695ea68cfb83454b98754af6e276031ba1ae7ae724dc6bf8973b92a"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:47598fe6713fc1fee86b1ca85c9cbe77e9b72d002d6adeab9c3b608f8a5ead10"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_s390x.whl", hash = "sha256:fed4de6e45a4f16e4046ea00917b4fe1700b97244e5d114f594b4a1b9de6bed8"}, + {file = "watchdog-2.1.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:24dedcc3ce75e150f2a1d704661f6879764461a481ba15a57dc80543de46021c"}, + {file = "watchdog-2.1.8-py3-none-win32.whl", hash = "sha256:6ddf67bc9f413791072e3afb466e46cc72c6799ba73dea18439b412e8f2e3257"}, + {file = "watchdog-2.1.8-py3-none-win_amd64.whl", hash = "sha256:88ef3e8640ef0a64b7ad7394b0f23384f58ac19dd759da7eaa9bc04b2898943f"}, + {file = "watchdog-2.1.8-py3-none-win_ia64.whl", hash = "sha256:0fb60c7d31474b21acba54079ce9ff0136411183e9a591369417cddb1d7d00d7"}, + {file = "watchdog-2.1.8.tar.gz", hash = "sha256:6d03149126864abd32715d4e9267d2754cede25a69052901399356ad3bc5ecff"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, ] diff --git a/pyproject.toml b/pyproject.toml index 5af2b25..4191e27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,8 @@ name = "bonfo" version = "0.1.0" homepage = "https://github.com/destos/bonfo" +repository = "https://github.com/destos/bonfo" +documentation = "https://destos.github.io/bonfo" description = "Multiwii flight controller configuration management" authors = ["Patrick Forringer "] readme = "README.md" @@ -15,9 +17,9 @@ classifiers=[ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] packages = [ { include = "bonfo" }, @@ -25,48 +27,56 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8,<4.0" -click = "8.0.1" +python = ">=3.9,<4.0" pyserial = "^3.5" -dataclass-wizard = "^0.22.0" -construct = "^2.10.67" -rich = "^12.0.1" -rich-click = "^1.2.1" -loca = "^2.0.1" +pyserial-asyncio = "^0.6" +dataclass-wizard = "^0.22" +construct = "^2.10" +construct-typing = "^0" +arrow = "^1.2.2" +semver = "^2.13.0" +click = { version = "^8.0", extras = ["cli"] } +rich = { version = "^12.0", extras = ["cli"] } +rich-click = { version = "^1.2", extras = ["cli"] } +loca = { version = "^2.0", extras = ["cli"] } -black = { version = "^21.5b2", optional = true} -isort = { version = "^5.8.0", optional = true} -flake8 = { version = "^3.9.2", optional = true} -flake8-docstrings = { version = "^1.6.0", optional = true } -mypy = {version = "^0.940", optional = true} -pytest = { version = "^6.2.4", optional = true} -pytest-cov = { version = "^2.12.0", optional = true} -pytest-watcher = {version = "^0.2.3", optional = true} -tox = { version = "^3.20.1", optional = true} -virtualenv = { version = "^20.2.2", optional = true} -pip = { version = "^20.3.1", optional = true} -mkdocs = { version = "^1.3.0", optional = true} -mkdocs-include-markdown-plugin = { version = "^3.3.0", optional = true} -mkdocs-material = { version = "^8.2.8", optional = true} -mkdocstrings = { version = "^0.18.1", optional = true} -mkdocs-material-extensions = { version = "^1.0.3", optional = true} -twine = { version = "^3.8.0", optional = true} -mkdocs-autorefs = {version = "^0.4.1", optional = true} -pre-commit = {version = "^2.12.0", optional = true} -toml = {version = "^0.10.2", optional = true} -bump2version = {version = "^1.0.1", optional = true} -autoflake8 = {version = "^0.3.1", optional = true} -construct-typing = {version = "^0.5.2", optional = true} +black = { version = "^22.0", optional = true } +isort = { version = "^5", optional = true } +flake8 = { version = "^3", optional = true } +flake8-docstrings = { version = "^1", optional = true } +mypy = { version = ">=0.940,<0.951", optional = true } +pytest = { version = "^7", optional = true } +pytest-asyncio = { version = "^0.18.3", optional = true } +pytest-cov = { version = "^3", optional = true } +pytest-watcher = { version = "^0", optional = true } +pytest-mock = { version = "^3", optional = true } +types-mock = { version = "^4", optional = true } +# types-docutils = { version } +tox = { version = "^3", optional = true } +virtualenv = { version = "^20", optional = true } +pip = { version = "^20", optional = true } +mkdocs = { version = "^1", optional = true } +mkdocstrings = { version = "^0", optional = true } +mkdocs-click = { version = "^0", optional = true } +mkdocs-git-revision-date-plugin = { version = "^0", optional = true } +mkdocs-include-markdown-plugin = { version = "^3", optional = true } +mkdocs-material = { version = "^8", optional = true } +mkdocs-material-extensions = { version = "^1", optional = true } +twine = { version = "^4", optional = true } +mkdocs-autorefs = { version = "^0", optional = true } +pre-commit = { version = "^2", optional = true } +toml = { version = "^0", optional = true } +bump2version = { version = "^1", optional = true } +autoflake8 = { version = "^0", optional = true } +textual = "^0.1.18" [tool.poetry.extras] test = [ "pytest", - "black", - "isort", - "mypy", - "flake8", - "flake8-docstrings", + "pytest-asyncio", "pytest-cov", + "pytest-mock", + "types-mock", ] dev = [ @@ -76,19 +86,25 @@ dev = [ "pip", "twine", "toml", + "black", + "isort", + "mypy", "bump2version", "autoflake8", - "construct-typing", "pytest-watcher", + "flake8", + "flake8-docstrings", ] doc = [ "mkdocs", - "mkdocs-include-markdown-plugin", + "mkdocs-click", "mkdocs-material", - "mkdocstrings", "mkdocs-material-extension", - "mkdocs-autorefs" + "mkdocs-autorefs", + "mkdocs-include-markdown-plugin", + "mkdocs-git-revision-date-plugin", + "mkdocstrings", ] [tool.poetry.scripts] @@ -122,16 +138,30 @@ use_parentheses = true ensure_newline_before_comments = true line_length = 120 skip_gitignore = true -# remove_all_unused_imports = true # you can skip files as below #skip_glob = docs/conf.py +[tool.autoflake8] +in-place = true +# remove-unused-variables = true +remove-all-unused-imports = true +ignore-init-module-imports = true +recursive = true +verbose = true + [tool.pytest.ini_options] log_cli = true log_cli_level = "DEBUG" log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" log_cli_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" + +[tool.mypy] +ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "bonfo.cli" +ignore_errors = true [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/setup.cfg b/setup.cfg index fe84b85..b3ed6a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ max-line-length = 120 max-complexity = 18 # TODO: re-enable docstrings checks in the future + add docstrings -ignore = E203, E266, W503, E128, E251, D100, D101, D102, D103, D104, D105, D107 +ignore = E203, E266, W503, E128, E251, E501, D100, D101, D102, D103, D104, D105, D107, D403 docstring-convention = google per-file-ignores = __init__.py:F401 exclude = .git, @@ -19,7 +19,7 @@ exclude = .git, .vscode, .github, # By default test codes will be linted. - # tests + tests [mypy] ignore_missing_imports = True @@ -46,8 +46,8 @@ envlist = py39, py310, py311, format, lint, build [gh-actions] python = 3.11: py311 - 3.10: py310, format, lint, build - 3.9: py39 + 3.10: py310 + 3.9: py39, format, lint, build [testenv] allowlist_externals = pytest @@ -64,11 +64,14 @@ commands = allowlist_externals = isort black + autoflake8 extras = test + dev commands = - isort --check-only bonfo + isort --check-only bonfo tests black --check bonfo tests + # autoflake8 -r --check --ignore-init-module-imports bonfo tests [testenv:lint] allowlist_externals = @@ -76,9 +79,10 @@ allowlist_externals = mypy extras = test + dev commands = flake8 bonfo - mypy bonfo tests + mypy bonfo [testenv:build] allowlist_externals = diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..76638d3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,64 @@ +import asyncio +from functools import wraps + +import pytest +from pytest_mock import MockerFixture, MockFixture + + +@pytest.fixture(scope="function") +def init_board_mocks(module_mocker): + init_serial = module_mocker.patch("bonfo.board.Board._init_serial").stub() + init_serial.is_open = True + module_mocker.patch("bonfo.board.Board.connect") + + +# @pytest.fixture(scope="function") +# def init_msg_mocks(module_mocker): +# module_mocker.patch("bonfo.board.Board.send_msg").stub() +# module_mocker.patch("bonfo.board.Board.receive_msg").stub() + + +@pytest.fixture(scope="function") +def mock_open_serial_connection(module_mocker, mocker): + read = mocker.AsyncMock() + reader = mocker.Mock(read=read) + write = mocker.AsyncMock() + writer = mocker.Mock(write=write) + open_serial = module_mocker.patch("bonfo.board.open_serial_connection") + open_serial.side_effect = [(reader, writer)] + return open_serial + + +@pytest.fixture(scope="function") +def mock_board_get(module_mocker): + return module_mocker.patch("bonfo.board.Board.get") + + +@pytest.fixture(scope="function") +def mock_board_set(module_mocker): + return module_mocker.patch("bonfo.board.Board.set") + + +@pytest.fixture(scope="function") +def mock_profile(module_mocker, mocker: MockerFixture): + profile = module_mocker.patch("bonfo.profile.Profile") + profile.pid = 2 + profile.rate = 2 + profile._check_connection = mocker.AsyncMock() + return profile + + +@pytest.fixture(scope="function") +def mock_board(mocker: MockerFixture) -> MockFixture: + mocker.AsyncMock + board = mocker.Mock().stub() + board.ready = asyncio.Event() + # Always ready + board.ready.set() + board.send_receive = mocker.AsyncMock() + board.get = mocker.AsyncMock() + board.set = mocker.AsyncMock() + + board.__gt__ = board.get + board.__lt__ = board.set + return board diff --git a/tests/test_structs.py b/tests/fields/__init__.py similarity index 100% rename from tests/test_structs.py rename to tests/fields/__init__.py diff --git a/tests/fields/test_boxes.py b/tests/fields/test_boxes.py new file mode 100644 index 0000000..93b2fdd --- /dev/null +++ b/tests/fields/test_boxes.py @@ -0,0 +1,13 @@ +from construct_typed import DataclassStruct + +from bonfo.msp.fields.base import Direction +from bonfo.msp.fields.boxes import Boxes, BoxIds + + +def test_boxids(): + assert isinstance(BoxIds.get_struct(), DataclassStruct) + assert BoxIds.get_direction() == Direction.OUT + standard = BoxIds(boxes=Boxes.ARM) + built = BoxIds.get_struct().build(standard) + assert built == b"\x00\x00\x00\x00\x00\x00\x00\x01" + assert BoxIds.get_struct().parse(built) == standard diff --git a/tests/fields/test_config.py b/tests/fields/test_config.py new file mode 100644 index 0000000..480f4c4 --- /dev/null +++ b/tests/fields/test_config.py @@ -0,0 +1,104 @@ +import pytest +from construct import Debugger, ValidationError + +from bonfo.msp.fields.config import FeatureConfig, Features, SelectPID, SelectRate, SelectSetting + +from .. import messages + + +def test_select_profile_out_of_range(): + with pytest.raises(ValidationError): + data = SelectPID(10) + SelectSetting.get_struct().build(data) + + with pytest.raises(ValidationError): + data = SelectRate(10) + SelectSetting.get_struct().build(data) + + +def test_select_setting_rate_profile_build(): + data = SelectSetting(profile=dict(rate_profile=3)) + assert SelectSetting.get_struct().build(data) == b"\x82" + + +def test_select_rate_build(): + data = SelectRate(3) + assert SelectSetting.get_struct().build(data) == b"\x82" + + +def test_select_setting_pid_profile_build(): + data = SelectSetting(profile=dict(pid_profile=2)) + assert SelectSetting.get_struct().build(data) == b"\x01" + + +def test_select_pid_build(): + data = SelectPID(3) + assert SelectSetting.get_struct().build(data) == b"\x02" + + +def test_feature_config(): + messages.feature_config_response + # struct = Debugger(FeatureConfig.get_struct()) + struct = FeatureConfig.get_struct() + struct.parse(messages.feature_config_response) + + +def test_feature_config_operations(): + pass + # Test flag manipulation features? + # | + # & + # ^ + rx_gps = sum([Features.RX_SERIAL, Features.GPS]) + rx_gps = Features.RX_SERIAL - Features.GPS + + print(rx_gps) + + +def xtest_rc_tuning_parse(): + # RcTuning( + # rc_rate=1.0, + # rc_expo=0.0, + # roll_pitch_rate=0, + # roll_rate=0.7, + # pitch_rate=0.7, + # yaw_rate=0.7, + # dynamic_thr_pid=0.65, + # throttle_mid=0.5, + # throttle_expo=0.0, + # dynamic_thr_breakpofloat=0, + # rc_yaw_expo=0.0, + # rcyawrate=1.0, + # rcpitchrate=1.0, + # rc_pitch_expo=0.0, + # roll_rate_limit=1998, + # pitch_rate_limit=1998, + # yaw_rate_limit=1998, + # ) + pass + + +def xtest_rx_config(): + # RxConfig( + # serialrx_provider=2, + # stick_max=1900, + # stick_center=1500, + # stick_min=1050, + # spektrum_sat_bind=0, + # rx_min_usec=885, + # rx_max_usec=2115, + # rc_interpolation=2, + # rc_interpolation_interval=19, + # rc_interpolation_channels=2, + # air_mode_activate_threshold=1250, + # rx_spi_protocol=0, + # rx_spi_id=0, + # rx_spi_rf_channel_count=0, + # fpv_cam_angle_degrees=40, + # rc_smoothing_type=1, + # rc_smoothing_input_cutoff=0, + # rc_smoothing_derivative_cutoff=0, + # rc_smoothing_input_type=1, + # rc_smoothing_derivative_type=3, + # ) + pass diff --git a/tests/fields/test_pids.py b/tests/fields/test_pids.py new file mode 100644 index 0000000..019323e --- /dev/null +++ b/tests/fields/test_pids.py @@ -0,0 +1,151 @@ +import pytest +from construct import Container, ListContainer +from construct_typed import DataclassStruct + +from bonfo.msp.codes import MSP +from bonfo.msp.fields.base import Direction +from bonfo.msp.fields.pids import PidAdvanced, PidCoefficients +from bonfo.msp.versions import MSPVersions +from tests import messages +from tests.utils import minus_preamble + + +def test_pid_coefficients(): + assert PidCoefficients.get_direction() == Direction.OUT + assert PidCoefficients.get_code == MSP.PID + assert PidCoefficients.set_code is None + assert isinstance(PidCoefficients.get_struct(), DataclassStruct) + + +def test_pid_coefficients_parse(): + data_bytes = minus_preamble(messages.pid) + data = PidCoefficients.get_struct().parse(data_bytes) + assert isinstance(data, PidCoefficients) + assert data == PidCoefficients( + pids=ListContainer( + [ + Container(p=22, i=68, d=31), + Container(p=26, i=68, d=31), + Container(p=29, i=76, d=4), + Container(p=53, i=55, d=75), + Container(p=40, i=0, d=0), + ] + ) + ) + + +def test_pid_parse_and_build_non_destructive(): + """Pid advanced generated struct should be non-destructive to bytestring.""" + data_bytes = minus_preamble(messages.pid) + struct = PidCoefficients.get_struct() + data = struct.parse(data_bytes) + assert isinstance(data, PidCoefficients) + output_data_bytes = struct.build(data) + assert data_bytes == output_data_bytes + + +def test_pid_advanced(): + assert PidAdvanced.get_direction() == Direction.BOTH + assert PidAdvanced.get_code == MSP.PID_ADVANCED + assert PidAdvanced.set_code == MSP.SET_PID_ADVANCED + assert isinstance(PidAdvanced.get_struct(), DataclassStruct) + + +def xtest_pid_advanced_parse(): + data_bytes = minus_preamble(messages.pid_advanced) + data = PidAdvanced.get_struct().parse(data_bytes, msp=MSPVersions.V1_43) + assert isinstance(data, PidAdvanced) + # TODO: don't require unused parameters fields + assert data == PidAdvanced( + feedforward_transition=1, + rate_accel_limit=1, + yaw_rate_accel_limit=1, + level_angle_limit=1, + iterm_throttle_threshold=1, + iterm_accelerator_gain=1, + iterm_rotation=1, + iterm_relax=1, + iterm_relax_type=1, + abs_control_gain=1, + throttle_boost=1, + acro_trainer_angle_limit=1, + pid_roll_f=1, + pid_pitch_f=1, + pid_yaw_f=1, + anti_gravity_mode=1, + d_min_roll=1, + d_min_pitch=1, + d_min_yaw=1, + d_min_gain=1, + d_min_advance=1, + use_integrated_yaw=1, + integrated_yaw_relax=1, + iterm_relax_cutoff=1, + motor_output_limit=1, + auto_profile_cell_count=1, + dyn_idle_min_rpm=1, + feedforward_averaging=1, + feedforward_smooth_factor=1, + feedforward_boost=1, + feedforward_max_rate_limit=1, + feedforward_jitter_factor=1, + vbat_sag_compensation=1, + thrust_linearization=1, + ) + + +def test_pid_advanced_parse_and_build_non_destructive(): + """Pi aAdvanced generated struct should be non-destructive to bytescring.""" + data_bytes = minus_preamble(messages.pid_advanced) + struct = PidAdvanced.get_struct() + data = struct.parse(data_bytes, msp=MSPVersions.V1_43.value) + assert isinstance(data, PidAdvanced) + output_data_bytes = struct.build(data, msp=MSPVersions.V1_43.value) + assert data_bytes == output_data_bytes + + +def test_pid_advanced_dataclass_no_args_errors(): + """No args throws a type error with missing count.""" + with pytest.raises(TypeError) as exec_info: + PidAdvanced() + assert "34" in exec_info.exconly() + + +def test_pid_advanced_dataclass_init_good_data(): + """Pid advanced shouldn't error when initialized.""" + PidAdvanced( + feedforward_transition=1, + rate_accel_limit=1, + yaw_rate_accel_limit=1, + level_angle_limit=1, + iterm_throttle_threshold=1, + iterm_accelerator_gain=1, + iterm_rotation=1, + iterm_relax=1, + iterm_relax_type=1, + abs_control_gain=1, + throttle_boost=1, + acro_trainer_angle_limit=1, + pid_roll_f=1, + pid_pitch_f=1, + pid_yaw_f=1, + anti_gravity_mode=1, + d_min_roll=1, + d_min_pitch=1, + d_min_yaw=1, + d_min_gain=1, + d_min_advance=1, + use_integrated_yaw=1, + integrated_yaw_relax=1, + iterm_relax_cutoff=1, + motor_output_limit=1, + auto_profile_cell_count=1, + dyn_idle_min_rpm=1, + feedforward_averaging=1, + feedforward_smooth_factor=1, + feedforward_boost=1, + feedforward_max_rate_limit=1, + feedforward_jitter_factor=1, + vbat_sag_compensation=1, + thrust_linearization=1, + ) diff --git a/tests/fields/test_sensors.py b/tests/fields/test_sensors.py new file mode 100644 index 0000000..f53eee1 --- /dev/null +++ b/tests/fields/test_sensors.py @@ -0,0 +1,27 @@ +from construct_typed import DataclassStruct + +from bonfo.msp.fields.base import Direction +from bonfo.msp.fields.sensors import Attitude +from tests import messages +from tests.utils import minus_preamble + + +def test_attitude(): + assert isinstance(Attitude.get_struct(), DataclassStruct) + assert Attitude.get_direction() == Direction.OUT + standard = Attitude(roll=10, pitch=10, yaw=12) + built = Attitude.get_struct().build(standard) + assert built == b"\x00\n\x00\n\x00\x0c" + assert Attitude.get_struct().parse(built) == standard + + +def test_status_response(): + target_bytes = minus_preamble(messages.attitude_response) + struct = Attitude.get_struct() + target_data = struct.parse(target_bytes) + build_data = Attitude(roll=24578, pitch=43775, yaw=3584) + # parsed target and manually created dataclass are the same + assert target_data == build_data + # Building the manual definition is the same as the parsed target data + built_bytes = struct.build(build_data) + assert target_bytes == built_bytes diff --git a/tests/fields/test_statuses.py b/tests/fields/test_statuses.py new file mode 100644 index 0000000..f25c06f --- /dev/null +++ b/tests/fields/test_statuses.py @@ -0,0 +1,108 @@ +import arrow +from construct_typed import DataclassStruct + +from bonfo.msp.fields.base import Direction +from bonfo.msp.fields.statuses import ( + ApiVersion, + BoardInfo, + BuildInfo, + ConfigurationProblemsFlags, + FcVariant, + FcVersion, + SensorAlignment, + Status, + StatusEx, + TargetCapabilitiesFlags, +) +from tests import messages +from tests.utils import minus_preamble + + +def test_api_version(): + assert isinstance(ApiVersion.get_struct(), DataclassStruct) + assert ApiVersion.get_direction() == Direction.OUT + standard = ApiVersion(msp_protocol=0, api_major=1, api_minor=43) + built = ApiVersion.get_struct().build(standard) + assert built == b"\x00\x01+" + assert ApiVersion.get_struct().parse(built) == standard + + +def test_fc_variant(): + assert isinstance(FcVariant.get_struct(), DataclassStruct) + assert FcVariant.get_direction() == Direction.OUT + standard = FcVariant(variant="BTFL") + built = FcVariant.get_struct().build(standard) + assert built == b"BTFL" + assert FcVariant.get_struct().parse(built) == standard + + +def test_fc_version(): + assert isinstance(FcVersion.get_struct(), DataclassStruct) + assert FcVersion.get_direction() == Direction.OUT + standard = FcVersion(major=1, minor=12, patch=21) + built = FcVersion.get_struct().build(standard) + assert built == b"\x01\x0c\x15" + assert FcVersion.get_struct().parse(built) == standard + + +def test_build_info(): + assert isinstance(BuildInfo.get_struct(), DataclassStruct) + assert BuildInfo.get_direction() == Direction.OUT + standard = BuildInfo(date_time=arrow.get(2013, 5, 5, 10, 20, 30), git_hash="85c6fdf") + built = BuildInfo.get_struct().build(standard) + assert built == b"May 5 201310:20:30\x0085c6fdf" + assert BuildInfo.get_struct().parse(built) == standard + + +def xtest_status_response(): + """Status not used.""" + target_bytes = minus_preamble(messages.status_response) + built_bytes = Status.get_struct().build(Status(cycle_time=10, i2c_error=10, active_sensors=12, mode=1, profile=2)) + assert target_bytes == built_bytes + + +def xtest_status_ex_response(): + target_bytes = minus_preamble(messages.ssstatus_ex_response) + parsed = StatusEx.get_struct().parse(target_bytes) + assert parsed == dict() + # built_bytes = StatusEx.get_struct().build( + # StatusEx(cycle_time=10, i2c_error=10, active_sensors=dict(), mode=1, pid_profile=2) + # ) + # assert target_bytes == built_bytes + + +def test_board_info(): + assert isinstance(BoardInfo.get_struct(), DataclassStruct) + assert BoardInfo.get_direction() == Direction.OUT + target_bytes = minus_preamble(messages.board_info) + struct = BoardInfo.get_struct() + parsed = struct.parse(target_bytes) + target = BoardInfo( + short_name="S405", + hardware_revision=0, + uses_max7456=2, + target_capabilities=TargetCapabilitiesFlags(55), + target_name="STM32F405", + board_name="CLRACINGF4", + manufacturer_id="CLRA", + signature="", + mcu_type=3, + configuration_state=2, + sample_rate=16415, + configuration_problems=ConfigurationProblemsFlags(0), + _target_name_length=9, + _board_name_length=10, + _manufacturer_id_length=4, + ) + assert parsed == target + assert BoardInfo.build(parsed) == target_bytes + + +def test_sensor_alignment(): + assert isinstance(SensorAlignment.get_struct(), DataclassStruct) + assert SensorAlignment.get_direction() == Direction.BOTH + target_bytes = minus_preamble(messages.sensor_alignment) + struct = SensorAlignment.get_struct() + parsed = struct.parse(target_bytes) + print(parsed) + # target = SensorAlignment() diff --git a/tests/fields/test_structs.py b/tests/fields/test_structs.py new file mode 100644 index 0000000..c59b4ee --- /dev/null +++ b/tests/fields/test_structs.py @@ -0,0 +1,38 @@ +import pytest +from construct import Default, Int16ub, StreamError + +from bonfo.msp.structs import MSPCutoff +from bonfo.msp.versions import MSPVersions + + +def test_mspversion_struct(): + """When only specifying the msp version, missing data errors, older versions are optional.""" + struct = MSPCutoff(Int16ub, MSPVersions.V1_44) + found = struct.parse(b"\xff\x12") + assert found == 65298 + + with pytest.raises(StreamError): + struct.parse(b"") + + found = struct.parse(b"", msp=MSPVersions.V1_43.value) + assert found is None + + found = struct.build(None, msp=MSPVersions.V1_43.value) + assert found == b"" + + +def test_mspversion_struct_else_subcon(): + """When providing a non-optional else subcon, it acts differently.""" + struct = MSPCutoff(Int16ub, MSPVersions.V1_44, Default(Int16ub, 0)) + found = struct.parse(b"\xff\x12") + assert found == 65298 + + with pytest.raises(StreamError): + struct.parse(b"") + + with pytest.raises(StreamError): + # We error now if nothing is passed for parsing + struct.parse(b"", msp=MSPVersions.V1_43.value) + + found = struct.build(None, msp=MSPVersions.V1_43.value) + assert found == b"\x00\x00" diff --git a/tests/messages.py b/tests/messages.py new file mode 100644 index 0000000..7819103 --- /dev/null +++ b/tests/messages.py @@ -0,0 +1,48 @@ +# V1.43 messages + +# PID advanced +# does not include feedforward data or vbat sag comp or thrust linearization +# pid_advanced = b"$M>2^\x00\x00\x00\x00x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x007\x00\xfa\x00\xd8\x0e\x00\x00\x00\x00\x01\x01\x00\n\x14H\x00H\x00H\x00\x00\x15\x1a\x00(\x14\x00\xc8\x0fd\x04\x00\xb1" +pid_advanced = b'$M>2^\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x007\x00\xfa\x00\xd8\x0e\x00\x00\x00\x00\x01\x01\x00\n\x14H\x00H\x00H\x00\x00\x15\x1a\x00(\x14\x00\xc8\x0fd\x04\x00\xb1' + +# PID coefficient +pid = b"$M>\x0fp\x16D\x1f\x1aD\x1f\x1dL\x0457K(\x00\x00G" + +fc_version = b"$M>\x03\x03\x01\x0c\x15\x18" +fc_version_2 = b"$M>\x03\x01\x00\x01\x15\x16" +api_version = b"$M>\x03\x01\x00\x01\x15\x16" + + +status_response = b"$M>\x16e}\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x1a\x04\x01\x01\x00\x004" +status_ex_response = ( + b"$M>\x16\x96}\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x05\x00\x03\x00\x00\x1a\x04\x01\x01\x00\x00\xc4" +) +sensor_alignment = b"$M>\x07~\x01\x01\x00\x01\x00\x01\x01x" + +# status_ex_response = ( +# b'$M>\x16\x96}\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x05\x00\x03\x04\x00\x1a\x04\x00\x00\x00\x00\xc0' +# ) + +rc_tuning = b"$M>\x17od\x00FFFA2\x00F\x05\x00dd\x00\x00d\xce\x07\xce\x07\xce\x07\x00\xc7" +rx_tuning2 = ( + b"$M>\x02l\x07\xdc\x05\x1a\x04\x00u\x03C\x08\x02\x13\xe2\x04\x00\x00\x00\x00\x00\x00(\x02\x01\x00\x00\x01\x03\x00" +) + +board_info = b"$M>J\x04S405\x00\x00\x027\tSTM32F405\nCLRACINGF4\x04CLRA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x02@\x1f\x00\x00\x00\x00\r" + +# MSP.ATTITUDE +attitude_response = b"$M>\x06l`\x02\xaa\xff\x0e\x00S" + +# MSP.BOXNAMES +box_names_response = ( + b"$M>\xfft\x0b\x01ARM;ANGLE;HORIZON;HEADFREE;FAILSAFE;HEADADJ;BEEPER;" + b"OSD DISABLE SW;BLACKBOX;FPV ANGLE MIX;BLACKBOX ERASE (>30s);CAMERA CONTROL 1;" + b"CAMERA CONTROL 2;CAMERA CONTROL 3;FLIP OVER AFTER CRASH;PREARM;VTX PIT MODE;" + b"PARALYZE;USER1;ACRO TRAINER;DISABLE VTX CONTROL;LA" +) + +# MSP.BOXIDS +box_names_response = b'$M>\x16w\x00\x01\x02\x06\x1b\x07\r\x13\x1a\x1e\x1f !"#$\'-(/01U' + +# MSP.FEATURE_CONFIG +feature_config_response = b'$M>\x04$\x00 D0t' diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..8b367a9 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,43 @@ +import arrow +import pytest +from construct import ValidationError + +from bonfo.msp.adapters import BTFLTimestamp, GitHash, SelectPIDProfile, SelectRateProfile + + +def test_select_pid_profile_selector_byte(): + assert SelectPIDProfile.build(1) == b"\x00" + assert SelectPIDProfile.build(2) == b"\x01" + assert SelectPIDProfile.build(3) == b"\x02" + with pytest.raises(ValidationError): + SelectPIDProfile.build(4) + with pytest.raises(ValidationError): + SelectPIDProfile.build(-1) + + +def test_select_rate_profile_selector_byte(): + assert SelectRateProfile.build(1) == b"\x80" + assert SelectRateProfile.build(2) == b"\x81" + assert SelectRateProfile.build(3) == b"\x82" + assert SelectRateProfile.build(4) == b"\x83" + assert SelectRateProfile.build(5) == b"\x84" + assert SelectRateProfile.build(6) == b"\x85" + with pytest.raises(ValidationError): + SelectRateProfile.build(7) + with pytest.raises(ValidationError): + SelectRateProfile.build(-1) + + +def test_betaflight_timestamp_adapter(): + first_variant = BTFLTimestamp.parse(b"Jan 9 202212:13:14") + second_variant = BTFLTimestamp.parse(b"Jan 12 202212:13:14") + assert first_variant == arrow.Arrow(2022, 1, 9, 12, 13, 14) + assert second_variant == arrow.Arrow(2022, 1, 12, 12, 13, 14) + + +def test_githash_adapter(): + assert GitHash.parse(b"bad01235678") == "bad0123" + + +def test_select_rate_profile_selector_byte_parse(): + assert SelectRateProfile.parse(b"\x85") == 6 diff --git a/tests/test_board.py b/tests/test_board.py new file mode 100644 index 0000000..3477cbd --- /dev/null +++ b/tests/test_board.py @@ -0,0 +1,91 @@ +import asyncio +import logging + +from pytest_mock import MockerFixture +from serial_asyncio import serial + +from bonfo.board import Board +from bonfo.msp.fields.statuses import ( + ApiVersion, + BoardInfo, + BuildInfo, + CombinedBoardInfo, + FcVariant, + FcVersion, + Name, + Uid, +) + +logger = logging.getLogger(__name__) + + +async def test_board_init_values(mock_open_serial_connection, mock_profile, mock_board_get) -> None: + board = Board("/dev/tty-mock", initial_data=False, profile=mock_profile) + await board.ready.wait() + mock_board_get.side_effect = [None] + mock_open_serial_connection.assert_awaited_once_with( + url="/dev/tty-mock", + baudrate=115200, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=0.1, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) + assert board.device == "/dev/tty-mock" + assert board.baudrate == 115200 + assert isinstance(board.info, CombinedBoardInfo) + assert board.profile == mock_profile + assert isinstance(board.write_lock, asyncio.Lock) + assert isinstance(board.read_lock, asyncio.Lock) + assert isinstance(board.message_lock, asyncio.Lock) + mock_profile._check_connection.assert_awaited_once() + + +async def test_board_connect_manager(mock_open_serial_connection, mock_profile, mock_board_get, mock_board_set): + mock_board_get.side_effect = [None] + async with Board("/dev/tty", initial_data=False).connect() as board: + assert board.connected.is_set() is True + assert board.ready.is_set() is True + assert board.connected.is_set() is False + + +async def test_board_initial_data_true( + mock_open_serial_connection, mock_profile, mock_board_get, mocker: MockerFixture +): + gbi = mocker.patch("bonfo.board.Board.get_board_info") + async with Board("/dev/tty", initial_data=True, profile=mock_profile).connect() as board: + gbi.assert_awaited_with() + + +async def test_board_get_board_info(mock_open_serial_connection, mock_profile, mock_board_get, mocker: MockerFixture): + cbi = mocker.patch("bonfo.board.CombinedBoardInfo") + mock_board_get.side_effect = [ + "name", + "api", + "version", + "build_info", + "board_info", + "variant", + "uid", + ] + board = Board("/dev/tty", initial_data=False, profile=mock_profile) + # does not run on init + mock_board_get.assert_not_awaited() + board.connected.set() + # TODO: test for returned mocked info + await board.get_board_info() + mock_board_get.assert_has_awaits( + [ + mocker.call(Name), + mocker.call(ApiVersion), + mocker.call(FcVersion), + mocker.call(BuildInfo), + mocker.call(BoardInfo), + mocker.call(FcVariant), + mocker.call(Uid), + ] + ) + cbi.assert_called_with("name", "api", "version", "build_info", "board_info", "variant", "uid") diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..319b9f3 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,25 @@ +# import time +# from unittest import mock + +# import pytest +# from click.testing import CliRunner + +# from bonfo.cli import cli + + +# @pytest.mark.parametrize("use_mock, min_time, max_time", ((True, 2.5, 3.5), (False, 1.0, 2.0))) +# def xtest_async_cli(use_mock, min_time, max_time): +# def test_hook(delay): +# return AsyncContext(delay + 0.5) + +# runner = CliRunner() +# start = time.time() +# if use_mock: +# with mock.patch("test_code.TestAsyncContext", test_hook): +# result = runner.invoke(cli) +# else: +# result = runner.invoke(cli) +# stop = time.time() +# assert result.exit_code == 0 +# assert result.stdout == "hello\n" +# assert min_time < stop - start < max_time diff --git a/tests/test_message.py b/tests/test_message.py index 11a57a7..375bee4 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,117 +1,23 @@ -import logging - from bonfo.msp.codes import MSP +from bonfo.msp.fields.config import SelectPID, SelectRate from bonfo.msp.message import Message -from bonfo.msp.utils import in_message_builder, out_message_builder - -logger = logging.getLogger(__name__) - - -def test_build_fc_version_msg(): - buff = in_message_builder(MSP.FC_VERSION, fields=dict(major=1, minor=12, patch=21)) - assert buff == b"$M>\x03\x03\x01\x0c\x15\x18" - - -def xtest_fc_version_msg(): - msg = Message.parse(b"$M<\x01\x03\x01\x0c\x15\x1a") - packet = msg.packet.value - assert packet.frame_id == MSP.FC_VERSION - assert packet.fields == dict(major=1, minor=12, patch=21) - - -def test_request_fc_version_msg(): - buff = out_message_builder(MSP.FC_VERSION) - assert buff == b"$M<\x00\x03\x03" - - -def test_parse_api_version_msg(): - fc_version_response = b"$M>\x03\x01\x00\x01\x15\x16" - msg = Message.parse(fc_version_response) - packet = msg.packet.value - assert msg.crc == 22 - assert msg.message_type == "IN" - assert packet.frame_id == MSP.API_VERSION - assert packet.fields == dict(msp_protocol=0, api_major=1, api_minor=21) - - -def test_fc_variant_request(): - proper_request = b'$M<\x00\x02\x02' - msg = out_message_builder(MSP.FC_VARIANT, fields=None) - assert msg == proper_request - - -def test_fc_variant_response(): - fc_response = b"$M>\x04\x02BTFL\x1a" - msg = Message.parse(fc_response) - assert msg.packet.value.fields.name == "BTFL" - - -def xtest_build_fc_variant(): - buff = in_message_builder(MSP.FC_VARIANT, fields=dict(name="DAMN")) - assert buff == b"$M<\x04\x02DAMN\x01" - - -def xtest_status_response(): - fc_response = b"$M>\x16e}\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x1a\x04\x01\x01\x00\x004" - - msg = Message.parse(fc_response) - values = msg.packet.value.fields - assert values is None - +from bonfo.msp.structs import FrameStruct +from bonfo.msp.utils import msg_packet, out_message_builder -def test_status_ex_response(): - fc_response = b"$M>\x16\x96}\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x05\x00\x03\x00\x00\x1a\x04\x01\x01\x00\x00\xc4" - msg = Message.parse(fc_response) - # TODO: better tests - assert msg is not None +def test_select_setting_ack(): + msg = Message.parse(b"$M>\x00\xd2\xd2") + msg = msg_packet(msg) + assert msg.frame_id == MSP.SELECT_SETTING + assert msg.data_length == 0 + assert msg.fields is None -def xtest_rc_tuning(): - # b"$M>\x02l\x07\xdc\x05\x1a\x04\x00u\x03C\x08\x02\x13\xe2\x04\x00\x00\x00\x00\x00\x00(\x02\x01\x00\x00\x01\x03\x00" - # RcTuning( - # rc_rate=1.0, - # rc_expo=0.0, - # roll_pitch_rate=0, - # roll_rate=0.7, - # pitch_rate=0.7, - # yaw_rate=0.7, - # dynamic_thr_pid=0.65, - # throttle_mid=0.5, - # throttle_expo=0.0, - # dynamic_thr_breakpofloat=0, - # rc_yaw_expo=0.0, - # rcyawrate=1.0, - # rcpitchrate=1.0, - # rc_pitch_expo=0.0, - # roll_rate_limit=1998, - # pitch_rate_limit=1998, - # yaw_rate_limit=1998, - # ) - pass +def test_out_message_builder(): + out_msg = out_message_builder(MSP.SELECT_SETTING, fields=(SelectPID(2))) + assert out_msg == b'$M<\x01\xd2\x01\xd2' -def xtest_rx_config(): - # RxConfig( - # serialrx_provider=2, - # stick_max=1900, - # stick_center=1500, - # stick_min=1050, - # spektrum_sat_bind=0, - # rx_min_usec=885, - # rx_max_usec=2115, - # rc_interpolation=2, - # rc_interpolation_interval=19, - # rc_interpolation_channels=2, - # air_mode_activate_threshold=1250, - # rx_spi_protocol=0, - # rx_spi_id=0, - # rx_spi_rf_channel_count=0, - # fpv_cam_angle_degrees=40, - # rc_smoothing_type=1, - # rc_smoothing_input_cutoff=0, - # rc_smoothing_derivative_cutoff=0, - # rc_smoothing_input_type=1, - # rc_smoothing_derivative_type=3, - # ) - pass +def test_frame_struct(): + result = FrameStruct(MSP.SELECT_SETTING).build(SelectRate(2)) + assert result == b"\x81" diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 0000000..edb9530 --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,239 @@ +from construct import Container +from pytest_mock import MockerFixture + +from bonfo.board import Board +from bonfo.msp.fields.config import SelectPID, SelectRate +from bonfo.msp.fields.statuses import StatusEx +from bonfo.profile import Profile + + +def test_profile_str(mock_board) -> None: + """String cast of profile object.""" + profile = Profile(board=mock_board) + profile._pid = 2 + profile._rate = 3 + assert str(profile) == "pid: 2 rate: 3 SyncedState.UNFETCHED" + + +def test_profile_pid_attr(mock_board) -> None: + """Side effects of changing the pid profile via its attribute.""" + profile = Profile(board=mock_board) + assert profile.pid == 1 + assert profile._state == Profile.SyncedState.UNFETCHED + assert profile._profile_tracker == (1, 1) + profile.pid = 2 + assert profile.pid == 1 + assert profile._state == Profile.SyncedState.AWAITING_APPLY + assert profile._profile_tracker == (2, 1) + + +def test_profile_rate_attr(mock_board) -> None: + """Side effects of changing the rate profile via its attribute.""" + profile = Profile(board=mock_board) + assert profile.rate == 1 + assert profile._state == Profile.SyncedState.UNFETCHED + assert profile._profile_tracker == (1, 1) + profile.rate = 2 + assert profile.rate == 1 + assert profile._state == Profile.SyncedState.AWAITING_APPLY + assert profile._profile_tracker == (1, 2) + + +async def xtest_board_ready_wait(mock_board): + """Board ready event halts profile context manager.""" + # Make sure we halt on a future await wait() + mock_board.ready.clear() + profile = Profile(board=mock_board) + async with profile() as pro: + assert pro.board.ready.is_set() is False + + +async def test_profile_manager_with_no_args(mock_board, mocker: MockerFixture) -> None: + """If a profile context manager is made with no args do nothing.""" + apply_changes_spy = mocker.spy(Profile, "apply_changes") + profile = Profile(board=mock_board) + profile.board.send_receive.side_effect = [] + assert profile.pid == 1 + assert profile.rate == 1 + async with profile() as pro: + assert pro._revert_to_profiles == (1, 1) + assert pro.pid == 1 + assert pro.rate == 1 + assert isinstance(pro, Profile) + apply_changes_spy.assert_called_once_with(profile) + # Called once in total after exit + apply_changes_spy.assert_has_calls([mocker.call(profile)]) + assert profile._revert_to_profiles == (1, 1) + assert profile.pid == 1 + assert profile.rate == 1 + profile.board.send_receive.assert_not_awaited() + + +async def test_profile_manager_with_selections(mock_board: Board, mocker: MockerFixture) -> None: + """Side effects of providing a different pid and rate in the async profile manager.""" + apply_changes_spy = mocker.spy(Profile, "apply_changes") + profile = Profile(board=mock_board) + profile.board.set.side_effect = [ + # return from _send_pid_to_board call, not used currently + None, + # return from _send_rate_to_board call, not used currently + None, + ] + profile.board.get.side_effect = [ + # return from _set_profiles_from_board call, from the "board", partial of StatusEx + Container(pid_profile=2, rate_profile=4), + ] + assert profile.pid == 1 + assert profile.rate == 1 + async with profile(pid=2, rate=4) as pro: + assert pro._revert_to_profiles == (1, 1) + assert pro.pid == 2 + assert pro.rate == 4 + assert isinstance(pro, Profile) + apply_changes_spy.assert_called_once_with(profile) + # Called once in total after exit + apply_changes_spy.assert_called_once_with(profile) + assert profile._revert_to_profiles == (1, 1) + # pid and rate not reverted + assert profile.pid == 2 + assert profile.rate == 4 + profile.board.set.assert_any_await(SelectRate(4)) + profile.board.set.assert_any_await(SelectPID(2)) + profile.board.get.assert_any_await(StatusEx) + + +async def test_profile_manager_with_revert_on_exit(mock_board, mocker: MockerFixture) -> None: + """revert_on_exit flag reverts to previous profiles on exit of manager.""" + apply_changes_spy = mocker.spy(Profile, "apply_changes") + profile = Profile(board=mock_board) + profile.board.set.side_effect = [ + # return from _set_pid_to_board call, not used currently + None, + # return from _set_rate_to_board call, not used currently + None, + # second return from _set_pid_to_board call, not used currently + None, + # second return from _set_rate_to_board call, not used currently + None, + ] + profile.board.get.side_effect = [ + # return from _set_profiles_from_board call, inside apply_changes + Container(pid_profile=2, rate_profile=4), + # return from _set_profiles_from_board second call, inside apply_changes + Container(pid_profile=1, rate_profile=1), + ] + assert profile.pid == 1 + assert profile.rate == 1 + async with profile(pid=2, rate=4, revert_on_exit=True) as pro: + assert pro._revert_to_profiles == (1, 1) + assert pro.pid == 2 + assert pro.rate == 4 + assert isinstance(pro, Profile) + apply_changes_spy.assert_called_once_with(profile) + # Called once in total after exit + apply_changes_spy.assert_has_calls([mocker.call(profile), mocker.call(profile)]) + assert profile._revert_to_profiles == (1, 1) + # pid and rate not reverted + assert profile.pid == 1 + assert profile.rate == 1 + profile.board.set.assert_any_await(SelectPID(2)) + profile.board.set.assert_any_await(SelectRate(4)) + profile.board.get.assert_any_await(StatusEx) + profile.board.set.assert_any_await(SelectPID(1)) + profile.board.set.assert_any_await(SelectRate(1)) + profile.board.get.assert_any_await(StatusEx) + + +async def test_set_profiles_from_board(mock_board): + """Sync down the current board profiles and set to local attributes.""" + profile = Profile(board=mock_board) + mock_board.get.side_effect = [ + # emulate a partial status_ex message container + Container(pid_profile=3, rate_profile=6), + ] + assert profile.pid == 1 + assert profile.rate == 1 + result = await profile._set_profiles_from_board() + mock_board.get.assert_any_await(StatusEx) + assert profile._profile_tracker == (3, 6) == result + assert profile._state == Profile.SyncedState.CLEAN + assert profile.pid == 3 + assert profile.rate == 6 + + +async def test_send_pid_to_board(mock_board): + """Send the selected PID to the board, should not change local values.""" + profile = Profile(board=mock_board) + profile.board.set.side_effect = [ + None, + ] + assert profile.pid == 1 + assert await profile._send_pid_to_board(3) is True + profile.board.set.assert_any_await(SelectPID(3)) + assert profile._profile_tracker == (1, 1) + assert profile._state == Profile.SyncedState.UNFETCHED + assert profile.pid == 1 + + +async def test_send_rate_to_board(mock_board): + """Send the selected rate to the board, should not change local values.""" + profile = Profile(board=mock_board) + profile.board.set.side_effect = [ + None, + ] + assert profile.rate == 1 + await profile._send_rate_to_board(3) + profile.board.set.assert_any_await(SelectRate(3)) + assert profile._profile_tracker == (1, 1) + assert profile._state == Profile.SyncedState.UNFETCHED + assert profile.rate == 1 + + +async def test_apply_changes_applied(mock_board, mocker: MockerFixture): + """Full apply test.""" + mock_send_pid = mocker.patch("bonfo.profile.Profile._send_pid_to_board") + mock_send_rate = mocker.patch("bonfo.profile.Profile._send_rate_to_board") + mock_set_profiles = mocker.patch.object(Profile, "_set_profiles_from_board", return_value=(2, 2)) + profile = Profile(board=mock_board) + profile._state = Profile.SyncedState.AWAITING_APPLY + profile._profile_tracker = (2, 2) + result = await profile.apply_changes() + assert profile._profile_tracker == (2, 2) + assert result is True + mock_send_pid.assert_awaited_once_with(2) + mock_send_rate.assert_awaited_once_with(2) + mock_set_profiles.assert_awaited_once_with() + + +async def test_apply_changes_bad_state(mock_board, mocker: MockerFixture): + """Profile is in bad state to apply.""" + mock_send_pid = mocker.patch("bonfo.profile.Profile._send_pid_to_board") + mock_send_rate = mocker.patch("bonfo.profile.Profile._send_rate_to_board") + mock_set_profiles = mocker.patch.object(Profile, "_set_profiles_from_board", return_value=(2, 2)) + profile = Profile(board=mock_board) + profile._state = Profile.SyncedState.CLEAN + profile._profile_tracker = (2, 2) + result = await profile.apply_changes() + assert profile._profile_tracker == (2, 2) + # no need to apply, so returns False + assert result is False + mock_send_pid.assert_not_awaited() + mock_send_rate.assert_not_awaited() + mock_set_profiles.assert_not_awaited() + + +async def test_apply_changes_different_board_result(mock_board, mocker: MockerFixture): + """Profiles from board differ.""" + mock_send_pid = mocker.patch("bonfo.profile.Profile._send_pid_to_board") + mock_send_rate = mocker.patch("bonfo.profile.Profile._send_rate_to_board") + mock_set_profiles = mocker.patch.object(Profile, "_set_profiles_from_board", return_value=(2, 4)) + profile = Profile(board=mock_board) + profile._state = Profile.SyncedState.AWAITING_APPLY + profile._profile_tracker = (2, 2) + result = await profile.apply_changes() + assert profile._profile_tracker == (2, 2) + # no need to apply, so returns False + assert result is False + mock_send_pid.assert_awaited_once_with(2) + mock_send_rate.assert_awaited_once_with(2) + mock_set_profiles.assert_awaited_once_with() diff --git a/tests/test_state.py b/tests/test_state.py index b745fb5..a28f2bd 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -5,4 +5,3 @@ def xtest_config_serialization(): For profiles, maybe we know which profile we're operating under and what those fields are MSP_COPY_PROFILE could be used to duplicate a profile via a command and then fill it out and save the yaml? """ - pass diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..3573d51 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,8 @@ +from bonfo.msp.message import Preamble + +pre_size = Preamble.sizeof() + + +# Trim off preamble and CRC byte at end +def minus_preamble(msg): + return msg[pre_size:-1]