Compare commits

...

19 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod
1af61e8af3 Update getting_started.md 2022-06-04 23:03:31 -07:00
Gilles Boccon-Gibod
e11119c565 Update README.md 2022-06-04 22:57:14 -07:00
Gilles Boccon-Gibod
b1a31564ef Merge pull request #3 from google/gbg/usb-serial-number
gbg/usb serial number
2022-06-04 20:47:18 -07:00
Gilles Boccon-Gibod
01492d510c close device inside for loop 2022-06-03 19:53:26 -07:00
Gilles Boccon-Gibod
302c495178 fix mkdocstrings python dependency 2022-06-02 15:56:01 -07:00
Gilles Boccon-Gibod
fc7923f83b add missing paths config for mkdocstrings 2022-06-02 15:47:27 -07:00
Gilles Boccon-Gibod
a9bd77e6ee add build workflow 2022-06-02 15:24:45 -07:00
Gilles Boccon-Gibod
ce0cf5fd27 refactor doc 2022-06-02 15:07:50 -07:00
Gilles Boccon-Gibod
86ded3fece support selecting usb device by serial number 2022-05-29 16:30:05 -07:00
Gilles Boccon-Gibod
d6b426eeec Merge pull request #2 from stefanst3000/fix-missing-dep
setup.cfg: add protobuf dependency and update package_dir
2022-05-19 12:55:07 -07:00
Stefan Stanacar
884315ae00 setup.cfg: add protobuf dependenc
Add protobuf to required dependencies, since is needed when using the
android-emulator transport.

Signed-off-by: Stefan Stanacar <stefanst@google.com>
2022-05-19 18:26:02 +00:00
Gilles Boccon-Gibod
7e8b201999 Update python-build-test.yml 2022-05-17 09:16:17 -07:00
Gilles Boccon-Gibod
e99d291cb7 Create python-build-test.yml 2022-05-17 09:14:49 -07:00
Gilles Boccon-Gibod
27c0551279 Create codeql-analysis.yml
Enable code analysis workflow
2022-05-16 20:24:05 -07:00
Gilles Boccon-Gibod
db2c833276 update mkdocs options 2022-05-16 20:16:02 -07:00
Gilles Boccon-Gibod
3dc2b9b2a8 Merge branch 'main' of https://github.com/google/bumble 2022-05-16 20:05:24 -07:00
Gilles Boccon-Gibod
210a509385 point to pre-built doc on GitHub Pages host 2022-05-16 20:05:10 -07:00
Gilles Boccon-Gibod
7b7c0ffa42 Merge pull request #1 from google/dependabot/pip/docs/mkdocs/mkdocs-1.2.3
Bump mkdocs from 1.2 to 1.2.3 in /docs/mkdocs
2022-05-16 19:47:25 -07:00
dependabot[bot]
ba0e123d96 Bump mkdocs from 1.2 to 1.2.3 in /docs/mkdocs
Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.2 to 1.2.3.
- [Release notes](https://github.com/mkdocs/mkdocs/releases)
- [Commits](https://github.com/mkdocs/mkdocs/compare/1.2...1.2.3)

---
updated-dependencies:
- dependency-name: mkdocs
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-17 02:46:22 +00:00
14 changed files with 293 additions and 85 deletions

72
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '39 21 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

34
.github/workflows/python-build-test.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
# Build and test the python package
name: Python build and test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[test,development,documentation]"
- name: Test with pytest
run: |
pytest
- name: Build
run: |
inv build
inv mkdocs

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.eggs/
build/
dist/
*.egg-info/
*~
bumble/__pycache__
docs/mkdocs/site
tests/__pycache__
test-results.xml
bumble/transport/__pycache__

View File

@@ -11,9 +11,12 @@ Bluetooth Stack for Apps, Emulation, Test and Experimentation
<img src="docs/mkdocs/src/images/logo_framed.png" alt="drawing" width="200" height="200"/>
Bumble is a full-featured Bluetooth stack written entirely in Python. It supports most of the common Bluetooth Low Energy (BLE) and Bluetooth Classic (BR/EDR) protocols and profiles, including GAP, L2CAP, ATT, GATT, SMP, SDP, RFCOMM, HFP, HID and A2DP. The stack can be used with physical radios via HCI over USB, UART, or the Linux VHCI, as well as virtual radios, including the virtual Bluetooth support of the Android emulator.
## Documentation
See the documentation under `docs/mkdocs/src`, or build the static HTML site from the markdown text with:
Browse the pre-built [Online Documentation](https://google.github.io/bumble/),
or see the documentation source under `docs/mkdocs/src`, or build the static HTML site from the markdown text with:
```
mkdocs build -f docs/mkdocs/mkdocs.yml
```

View File

@@ -20,7 +20,7 @@ import click
from colors import color
from bumble import hci
from bumble.transport import PacketReader
from bumble.transport.common import PacketReader
from bumble.helpers import PacketTracer

View File

@@ -243,7 +243,7 @@ class L2CAP_Control_Frame:
# -----------------------------------------------------------------------------
@L2CAP_Control_Frame.subclass([
('reason', {'size': 2, 'mapper': lambda x: L2CAP_Command_Reject.map_reason(x)}),
('reason', {'size': 2, 'mapper': lambda x: L2CAP_Command_Reject.reason_name(x)}),
('data', '*')
])
class L2CAP_Command_Reject(L2CAP_Control_Frame):
@@ -262,7 +262,7 @@ class L2CAP_Command_Reject(L2CAP_Control_Frame):
}
@staticmethod
def map_reason(reason):
def reason_name(reason):
return name_or_number(L2CAP_Command_Reject.REASON_NAMES, reason)
@@ -330,7 +330,7 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame):
@L2CAP_Control_Frame.subclass([
('source_cid', 2),
('flags', 2),
('result', {'size': 2, 'mapper': lambda x: L2CAP_Configure_Response.map_result(x)}),
('result', {'size': 2, 'mapper': lambda x: L2CAP_Configure_Response.result_name(x)}),
('options', '*')
])
class L2CAP_Configure_Response(L2CAP_Control_Frame):
@@ -355,7 +355,7 @@ class L2CAP_Configure_Response(L2CAP_Control_Frame):
}
@staticmethod
def map_result(result):
def result_name(result):
return name_or_number(L2CAP_Configure_Response.RESULT_NAMES, result)
@@ -403,31 +403,49 @@ class L2CAP_Echo_Response(L2CAP_Control_Frame):
# -----------------------------------------------------------------------------
@L2CAP_Control_Frame.subclass([
('info_type', 2)
('info_type', {'size': 2, 'mapper': lambda x: L2CAP_Information_Request.info_type_name(x)})
])
class L2CAP_Information_Request(L2CAP_Control_Frame):
'''
See Bluetooth spec @ Vol 3, Part A - 4.10 INFORMATION REQUEST
'''
SUCCESS = 0x00
NOT_SUPPORTED = 0x01
CONNECTIONLESS_MTU = 0x0001
EXTENDED_FEATURES_SUPPORTED = 0x0002
FIXED_CHANNELS_SUPPORTED = 0x0003
INFO_TYPE_NAMES = {
CONNECTIONLESS_MTU: 'CONNECTIONLESS_MTU',
EXTENDED_FEATURES_SUPPORTED: 'EXTENDED_FEATURES_SUPPORTED',
FIXED_CHANNELS_SUPPORTED: 'FIXED_CHANNELS_SUPPORTED'
}
@staticmethod
def info_type_name(info_type):
return name_or_number(L2CAP_Information_Request.INFO_TYPE_NAMES, info_type)
# -----------------------------------------------------------------------------
@L2CAP_Control_Frame.subclass([
('info_type', 2),
('result', 2),
('info_type', {'size': 2, 'mapper': L2CAP_Information_Request.info_type_name}),
('result', {'size': 2, 'mapper': lambda x: L2CAP_Information_Response.result_name(x)}),
('data', '*')
])
class L2CAP_Information_Response(L2CAP_Control_Frame):
'''
See Bluetooth spec @ Vol 3, Part A - 4.11 INFORMATION RESPONSE
'''
SUCCESS = 0x00
NOT_SUPPORTED = 0x01
RESULT_NAMES = {
SUCCESS: 'SUCCESS',
NOT_SUPPORTED: 'NOT_SUPPORTED'
}
@staticmethod
def result_name(result):
return name_or_number(L2CAP_Information_Response.RESULT_NAMES, result)
# -----------------------------------------------------------------------------
@@ -473,7 +491,7 @@ class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame):
('mtu', 2),
('mps', 2),
('initial_credits', 2),
('result', {'size': 2, 'mapper': lambda x: L2CAP_LE_Credit_Based_Connection_Response.map_result(x)})
('result', {'size': 2, 'mapper': lambda x: L2CAP_LE_Credit_Based_Connection_Response.result_name(x)})
])
class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
'''
@@ -505,7 +523,7 @@ class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
}
@staticmethod
def map_result(result):
def result_name(result):
return name_or_number(L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_RESULT_NAMES, result)
@@ -980,13 +998,13 @@ class ChannelManager:
def on_l2cap_information_request(self, connection, cid, request):
if request.info_type == L2CAP_Information_Request.CONNECTIONLESS_MTU:
result = L2CAP_Information_Request.SUCCESS
result = L2CAP_Information_Response.SUCCESS
data = struct.pack('<H', 1024) # TODO: don't use a fixed value
elif request.info_type == L2CAP_Information_Request.EXTENDED_FEATURES_SUPPORTED:
result = L2CAP_Information_Request.SUCCESS
result = L2CAP_Information_Response.SUCCESS
data = bytes.fromhex('00000000') # TODO: don't use a fixed value
elif request.info_type == L2CAP_Information_Request.FIXED_CHANNELS_SUPPORTED:
result = L2CAP_Information_Request.SUCCESS
result = L2CAP_Information_Response.SUCCESS
data = bytes.fromhex('FFFFFFFFFFFFFFFF') # TODO: don't use a fixed value
else:
result = L2CAP_Information_Request.NO_SUPPORTED

View File

@@ -37,14 +37,17 @@ async def open_usb_transport(spec):
'''
Open a USB transport.
The parameter string has this syntax:
either <index> or <vendor>:<product>
either <index> or <vendor>:<product>[/<serial-number>]
With <index> as the 0-based index to select amongst all the devices that appear
to be supporting Bluetooth HCI (0 being the first one), or
Where <vendor> and <product> are the vendor ID and product ID in hexadecimal.
Where <vendor> and <product> are the vendor ID and product ID in hexadecimal. The
/<serial-number> suffix max be specified when more than one device with the same
vendor and product identifiers are present.
Examples:
0 --> the first BT USB dongle
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
04b4:f901/00E04C239987 --> the BT USB dongle with vendor=04b4 and product=f901 and serial number 00E04C239987
'''
USB_RECIPIENT_DEVICE = 0x00
@@ -268,22 +271,32 @@ async def open_usb_transport(spec):
found = None
if ':' in spec:
vendor_id, product_id = spec.split(':')
found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True)
if '/' in product_id:
product_id, serial_number = product_id.split('/')
for device in context.getDeviceIterator(skip_on_error=True):
if (
device.getVendorID() == int(vendor_id, 16) and
device.getProductID() == int(product_id, 16) and
device.getSerialNumber() == serial_number
):
found = device
break
device.close()
else:
found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True)
else:
device_index = int(spec)
device_iterator = context.getDeviceIterator(skip_on_error=True)
try:
for device in device_iterator:
if device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and \
device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and \
device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER:
if device_index == 0:
found = device
break
device_index -= 1
device.close()
finally:
device_iterator.close()
for device in context.getDeviceIterator(skip_on_error=True):
if (
device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and
device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and
device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER
):
if device_index == 0:
found = device
break
device_index -= 1
device.close()
if found is None:
context.close()

View File

@@ -1,10 +1,12 @@
site_name: Bumble
use_directory_urls: true
use_directory_urls: false
nav:
- Introduction: index.md
- Getting Started: getting_started.md
- Development:
- Python Environments: development/python_environments.md
- Use Cases:
- Overview: use_cases/index.md
- Use Case 1: use_cases/use_case_1.md
@@ -63,15 +65,18 @@ theme:
custom_dir: 'theme'
plugins:
- mkdocstrings
- mkdocstrings:
handlers:
python:
paths: [../..]
docs_dir: 'src'
edit_uri: ''
# Repo info
#repo_name: TBD
#repo_url: https://github.com/TBD
repo_name: 'GitHub'
repo_url: https://github.com/google/bumble
# Extensions
markdown_extensions:

View File

@@ -1,5 +1,5 @@
# This requirements file is for python3
mkdocs == 1.2
mkdocs == 1.2.3
mkdocs-material == 7.1.7
mkdocs-material-extensions == 1.0.1
pymdown-extensions == 8.2

View File

@@ -7,6 +7,6 @@ These include:
* [Console](console.md) - an interactive text-based console
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
* [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool"
* [`Show`](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
* [`Link Relay`](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other.
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
* [Link Relay](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other.

View File

@@ -0,0 +1,45 @@
PYTHON ENVIRONMENTS
===================
When you don't want to install Bumble in your main/default python environment,
using a virtual environment, where the package and its dependencies can be
installed, isolated from the rest, may be useful.
There are many flavors of python environments and dependency managers.
This page describes a few of the most common ones.
## venv
`venv` is a standard module that is included with python.
Visit the [`venv` documentation](https://docs.python.org/3/library/venv.html) page for details.
## Pyenv
`pyenv` lets you easily switch between multiple versions of Python. It's simple, unobtrusive, and follows the UNIX tradition of single-purpose tools that do one thing well.
Visit the [`pyenv` site](https://github.com/pyenv/pyenv) for instructions on how to install
and use `pyenv`
## Conda
Conda is a convenient package manager and virtual environment.
The file `environment.yml` is a Conda environment file that you can use to create
a new Conda environment. Once created, you can simply activate this environment when
working with Bumble.
Visit the [Conda site](https://docs.conda.io/en/latest/) for instructions on how to install
and use Conda.
A few useful commands:
### Create a new `bumble` Conda environment
```
$ conda env create -f environment.yml
```
This will create a new environment, named `bumble`, which you can then activate with:
```
$ conda activate bumble
```
### Update an existing `bumble` environment
```
$ conda env update -f environment.yml
```

View File

@@ -20,7 +20,7 @@ You may be simply using Bumble as a module for your own application or as a depe
module, or you may be working on modifying or contributing to the Bumble module or example code
itself.
# Working With Bumble As A Module
# Using Bumble As A Python Module
## Installing
@@ -29,52 +29,40 @@ manager, or from source.
!!! tip "Python Virtual Environments"
When you install Bumble, you have the option to install it as part of your default
python environment, or in a virtual environment, such as a `venv`, `pyenv` or `conda` environment
### venv
`venv` is a standard module that is included with python.
Visit the [`venv` documentation](https://docs.python.org/3/library/venv.html) page for details.
### Pyenv
`pyenv` lets you easily switch between multiple versions of Python. It's simple, unobtrusive, and follows the UNIX tradition of single-purpose tools that do one thing well.
Visit the [`pyenv` site](https://github.com/pyenv/pyenv) for instructions on how to install
and use `pyenv`
### Conda
Conda is a convenient package manager and virtual environment.
The file `environment.yml` is a Conda environment file that you can use to create
a new Conda environment. Once created, you can simply activate this environment when
working with Bumble.
Visit the [Conda side](https://docs.conda.io/en/latest/) for instructions on how to install
and use Conda.
A few useful commands:
#### Create a new `bumble` Conda environment
```
$ conda env create -f environment.yml
```
This will create a new environment, named `bumble`, which you can then activate with:
```
$ conda activate bumble
```
#### Update an existing `bumble` environment
```
$ conda env update -f environment.yml
```
python environment, or in a virtual environment, such as a `venv`, `pyenv` or `conda` environment.
See the [Python Environments page](development/python_environments.md) page for details.
### Install From Source
The instructions for working with virtual Python environments above also apply in this case.
Install with `pip`
Install with `pip`. Run in a command shell in the directory where you downloaded the source
distribution
```
$ python -m pip install -e .
```
### Install from GitHub
You can install directly from GitHub without first downloading the repo.
Install the latest commit from the main branch with `pip`:
```
$ python -m pip install git+https://github.com/google/bumble.git
```
You can specify a specific tag.
Install tag `v0.0.1` with `pip`:
```
$ python -m pip install git+https://github.com/google/bumble.git@v0.0.1
```
You can also specify a specific commit.
Install commit `27c0551` with `pip`:
```
$ python -m pip install git+https://github.com/google/bumble.git@27c0551
```
# Working On The Bumble Code
When you work on the Bumble code itself, and run some of the tests or example apps, or import the
module in your own code, you typically either install the package from source in "development mode" as described above, or you may choose to skip the install phase.
@@ -106,3 +94,13 @@ Setting `PYTHONPATH` locally with each command would look something like:
```
$ PYTHONPATH=. python examples/run_advertiser.py examples/device1.json serial:/dev/tty.usbmodem0006839912171
```
# Where To Go Next
Once you've installed or downloaded Bumble, you can either start using some of the
[Bundled apps and tools](apps_and_tools/index.md), or look at the [examples](examples/index.md)
to get a feel for how to use the APIs, and start writing your own applications.
Depending on the use case you're interested in exploring, you may need to use a physical Bluetooth
controller, like a USB dongle or a board with a Bluetooth radio. Visit the [Hardware page](hardware/index.md)
for more information on using a physical radio, and/or the [Transports page](transports/index.md) for more
details on interfacing with either hardware modules or virtual controllers over various transports.

View File

@@ -46,7 +46,7 @@ through it with Android applications as well as system-managed profiles.
To connect a Bumble host stack to a Root Canal virtual controller instance, use
the bumble `android-emulator` transport in `host` mode (the default).
!!! example "Running the example GATT server connected to the emulator"
!!! example "Run the example GATT server connected to the emulator"
``` shell
$ python run_gatt_server.py device1.json android-emulator
```
@@ -57,7 +57,7 @@ This is an advanced use case, which may not be officially supported, but should
versions of the emulator.
You will likely need to start the emulator from the command line, in order to specify the `-forward-vhci` option (unless the emulator offers a way to control that feature from a user/ui menu).
!!! example "Launching the emulator with VHCI forwarding"
!!! example "Launch the emulator with VHCI forwarding"
In this example, we launch an emulator AVD named "Tiramisu"
```shell
$ emulator -forward-vhci -avd Tiramisu
@@ -70,10 +70,20 @@ You will likely need to start the emulator from the command line, in order to sp
To connect a virtual controller to the Android Bluetooth stack, use the bumble `android-emulator` transport in `controller` mode. For example, using the default gRPC port, the transport name would be: `android-emulator:mode=controller`.
!!! example "connecting the Android emulator to the first USB Bluetooth dongle, using the `hci_bridge` application"
!!! example "Connect the Android emulator to the first USB Bluetooth dongle, using the `hci_bridge` application"
```shell
$ bumble-hci-bridge android-emulator:mode=controller usb:0
```
## Other Tools
The `show` application that's included with Bumble can be used to parse and pretty-print the HCI packets
from an Android HCI "snoop log" (see [this page](https://source.android.com/devices/bluetooth/verifying_debugging)
for details on how to obtain HCI snoop logs from an Android device).
Use the `--format snoop` option to specify that the file is in that specific format.
!!! example "Analyze an Android HCI snoop log file"
```shell
$ bumble-show --format snoop btsnoop_hci.log
```

View File

@@ -26,7 +26,6 @@ packages = bumble, bumble.transport, bumble.apps, bumble.apps.link_relay
package_dir =
bumble = bumble
bumble.apps = apps
bumble.apps.link_relay = apps/link_relay
install_requires =
aioconsole >= 0.4.1
ansicolors >= 1.1
@@ -37,6 +36,7 @@ install_requires =
grpcio >= 1.46; platform_system!='Emscripten'
libusb1 >= 2.0.1; platform_system!='Emscripten'
prompt_toolkit >= 3.0.16; platform_system!='Emscripten'
protobuf >= 3.12.4
pyee >= 8.2.2
pyserial-asyncio >= 0.5; platform_system!='Emscripten'
pyserial >= 3.5; platform_system!='Emscripten'
@@ -65,4 +65,4 @@ development =
documentation =
mkdocs >= 1.2.3
mkdocs-material >= 8.1.9
mkdocstrings >= 0.17.0
mkdocstrings[python] >= 0.19.0