Porównaj commity

...

56 Commity

Autor SHA1 Wiadomość Data
Damien George 50ed36fbeb pyusb: Add MicroPython implementation of PyUSB library.
Signed-off-by: Damien George <damien@micropython.org>
2024-05-27 14:02:54 +10:00
Damien George 2c30a4e91b aioble/multitests: Use multitest.output_metric for perf results.
The perf multitests now "pass" when run.

Signed-off-by: Damien George <damien@micropython.org>
2024-05-25 20:51:51 +10:00
Damien George 1e792c39d3 aioble/multitests: Adjust expected output for write capture test.
Testing shows that the first two writes always go through and the rest are
dropped, so update the .exp file to match that.

Signed-off-by: Damien George <damien@micropython.org>
2024-05-25 20:51:51 +10:00
Damien George 63736c182a aioble/multitests: Add test for advertising and scanning services.
This tests both encoding and decoding multiple 16-bit and 32-bit services
within the one advertising field.

Signed-off-by: Damien George <damien@micropython.org>
2024-05-25 20:51:51 +10:00
Damien George 46e243c592 aioble/central.py: Fix ScanResult.services when decoding UUIDs.
Fixes are needed to support the cases of:
- There may be more than one UUID per advertising field.
- The UUID advertising field may be empty (no UUIDs).
- Constructing 32-bit `bluetooth.UUID()` entities, which must be done by
  passing in a 4-byte bytes object, not an integer.

Signed-off-by: Damien George <damien@micropython.org>
2024-05-25 18:27:37 +10:00
Trent Piepho e5389eb26a aioble/peripheral.py: Place multiple UUIDs in single advertisement LTV.
When multiple UUIDs of the same size are advertised, they should all be
listed in a single LTV.  Supplement to the Bluetooth Core Specification,
Part A, §1.1.1:  "A packet or data block shall not contain more than one
instance for each Service UUID data size."

When aioble construct the advertisement data, it is creating a new data
block for each UUID that contains only that single UUID.  Rather than,
e.g., a single 16-bit UUID block with a list of multiple UUIDs.

Not only is this against the specification, it wastes two bytes of limited
advertisement space per UUID beyond the first for the repeated data block
length and type fields.

Fix this by grouping each UUID size together.

Signed-off-by: Trent Piepho <tpiepho@gmail.com>
2024-05-24 21:42:33 +10:00
Rob Knegjens db7f9a18d4 aioble/device.py: Make default timeout None for disconnected() method.
The value for the `timeout_ms` optional argument to
`DeviceConnection.disconnected()` async method is changed from 60000 to
None.  This way users awaiting a device disconnection using `await
connection.disconnected()` won't be surprised by a 1 minute timeout.
2024-05-24 19:13:42 +10:00
Andrew Leech e7f605df33 aioble/device.py: Always create connection._event.
If the client disconnects immediately after connection, the irq can be run
before the initial connect handler has finished.
2024-05-24 19:03:49 +10:00
Rob Knegjens f0b683218e aioble/examples/temp_client.py: Check connection before reading temp.
Only read from the temp characteristic if the connection is still active.
Improves the example by avoiding a TypeError exception if/when the sensor
disconnects.
2024-05-24 18:48:10 +10:00
Brian Pugh da46c4b9f7 pathlib: Add __rtruediv__ magic method to pathlib.Path.
MicroPython now supports this behaviour of __rtruediv__.
2024-05-24 18:32:25 +10:00
Stephen More d4362d5cc3 aioble/examples/temp_sensor.py: Wait forever for client to disconnect.
This sets the disconnected timeout to None, so that the peripheral waits
forever for the client to disconnect.  Previously the peripheral would
abort the connection after 60 seconds (because that's the default timeout).

Signed-off-by: Stephen More <stephen.more@gmail.com>
2024-05-24 18:05:19 +10:00
Stephen More 191494ede7 aioble/examples/temp_sensor.py: Properly notify on update.
This ensures that the peripheral notifies subscribed clients when the
characteristic is written to.

Signed-off-by: Stephen More <stephen.more@gmail.com>
2024-05-24 18:03:41 +10:00
AuroraTea 2b0d7610ce aiohttp: Fix type of header's Sec-WebSocket-Key.
The function `binascii.b2a_base64()` returns a `bytes`, but here needs a
string.  Otherwise, the value of `Sec-WebSocket-Key` in the headers will be
`b'<BASE64-ENCODED_RANDOM_VALUE>'`.

Signed-off-by: AuroraTea <1352685369@qq.com>
2024-05-24 17:26:22 +10:00
Angus Gratton 00fc3fd37b all: Enable ruff E401 and E402 import lints.
Mostly small cleanups to put each top-level import on its own line.  But
explicitly disable the lint for examples/tests which insert the current
directory into the path before importing.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-05-17 17:06:33 +10:00
Angus Gratton 992eecfed4 all: Enable Ruff lint F541 'f-string without any placeholders'.
Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-05-15 18:11:46 +10:00
Angus Gratton 6c6fab1db1 all: Enable ruff F841 'Local variable is assigned to but never used'.
Most of these look like they were used for print debugging and then kept in
when the print statements were removed or commented.

Some look like missing or incomplete functionality, these have been marked
with comments where possible.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-05-15 18:05:21 +10:00
Jon Foster cb281a4177 ntptime: Fix Year 2036 bug.
Fix NTP client - it would report the wrong time after 7 Feb 2036.

Signed-off-by: Jon Foster <jon@jon-foster.co.uk>
2024-05-15 15:36:02 +10:00
Matt Trentini a2e4efa09a collections: Remove micropython-lib Python implementation of deque.
It's no longer necessary since the built-in C version of this type now
implements all the functionality here.

Signed-off-by: Matt Trentini <matt.trentini@gmail.com>
2024-05-15 14:12:14 +10:00
Damien George 7206da4645 mip: Bump minor version.
The previous commit added a new feature (ability to install from GitLab).

Signed-off-by: Damien George <damien@micropython.org>
2024-05-15 13:53:01 +10:00
Olivier Lenoir 57cbc34840 mip: Add support to mip install from GitLab.
Modify _rewrite_url() to allow mip install from `gitlab:` repository.

Signed-off-by: Olivier Lenoir <olivier.len02@gmail.com>
2024-05-15 13:43:06 +10:00
Angus Gratton 583bc0da70 usb: Add USB device support packages.
These packages build on top of machine.USBDevice() to provide high level
and flexible support for implementing USB devices in Python code.

Additional credits, as per included copyright notices:

- CDC support based on initial implementation by @hoihu with fixes by
  @linted.

- MIDI support based on initial implementation by @paulhamsh.

- HID keypad example based on work by @turmoni.

- Everyone who tested and provided feedback on early versions of these
  packages.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-04-30 15:57:50 +10:00
Damien George 45ead11f96 ssl: Use "from tls import *" to be compatible with axtls.
axtls doesn't define all the CERT_xxx constants, nor the MBEDTLS_VERSION
constant.

This change means that `tls.SSLContext` is imported into the module, but
that's subsequently overridden by the class definition in this module.

Signed-off-by: Damien George <damien@micropython.org>
2024-03-28 17:44:37 +11:00
iabdalkader 661efa48f0 senml: Use the updated cbor2 API.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2024-03-19 17:29:22 +11:00
iabdalkader 8ee876dcd6 cbor2: Deprecate decoder and encoder modules.
Deprecate decoder and encoder modules to maintain compatibility with the
CPython cbor2 module.

Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2024-03-19 17:28:35 +11:00
Jim Mussared 5c7e3fc0bc json: Move to unix-ffi.
It requires the unix pcre-based re module.

This work was funded through GitHub Sponsors.

Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
2024-03-19 17:23:07 +11:00
Damien George 23df50d0ea unix-ffi: Remove "unix_ffi" argument from require().
And describe how to use `add_library()` instead.

Signed-off-by: Damien George <damien@micropython.org>
2024-03-17 13:22:36 +11:00
Damien George ffb07dbce5 gzip: Fix recursion error in open() function.
And give the `mode` parameter a default, matching CPython.

Signed-off-by: Damien George <damien@micropython.org>
2024-02-29 14:54:24 +11:00
Angus Gratton 224246531e lora-sx126x: Clean up some struct formatting.
Changes are cosmetic - and maybe very minor code size - but not functional.
_reg_read() was calling struct.packinto() with an incorrect number of
arguments but it seems like MicroPython didn't mind, as result is correct
for both versions.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-02-20 16:58:38 +11:00
Angus Gratton 35bb7952ba lora-sx126x: Fix syncword setting.
Fixes issue #796.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-02-20 16:58:08 +11:00
Angus Gratton 546284817a lora-sx127x: Implement missing syncword support.
This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-02-20 16:58:06 +11:00
Angus Gratton ad6ab5a78c lora-sync: Fix race with fast or failed send().
If send completes before the first call to poll_send(), the driver could
get stuck in _sync_wait(). This had much less impact before rp2 port went
tickless, as _sync_wait(will_irq=True) calls machine.idle() which may not
wake very frequently on a tickless port.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-02-20 16:57:50 +11:00
Angus Gratton b712103519 lora-sx126x: Fix invalid default configuration after reset.
According to the docs, only freq_khz was needed for working output.
However:

- Without output_power setting, no output from SX1262 antenna (theory:
  output routed to the SX1261 antenna).

- SF,BW,etc. settings were different from the SX127x power on defaults, so
  modems with an identical configuration were unable to communicate.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-02-20 16:44:57 +11:00
Angus Gratton 4cc67065dd tools/ci.sh: Add unix-ffi library when testing unix-ffi subdirectory.
Signed-off-by: Angus Gratton <angus@redyak.com.au>
2024-02-19 23:21:04 +11:00
ubi de feo 8058b2935b tarfile-write: Fix permissions when adding to archive.
Signed-off-by: ubi de feo <me@ubidefeo.com>
2024-02-09 10:43:05 +11:00
Carlosgg 56f514f569 aiohttp: Fix binary data treatment.
- Fix binary data `Content-type` header and data `Content-Length`
  calculation.

- Fix query length when data is included.

- Fix `json` and `text` methods of `ClientResponse` to read
  `Content-Length` size

Signed-off-by: Carlos Gil <carlosgilglez@gmail.com>
2024-02-08 19:02:26 +11:00
Adam Knowles ddb1a27957 hmac: Fix passing in a string for digestmod argument.
The built-in `hashlib` module does not have a `.new` method (although the
Python version in this repository does).
2024-02-07 12:45:03 +11:00
Felix Dörre 35d41dbb0e ssl: Restructure micropython SSL interface to a new tls module.
MicroPython now supplies SSL/TLS functionality in a new built-in `tls`
module.  The `ssl` module is now implemented purely in Python, in this
repository.  Other libraries are updated to work with this scheme.

Signed-off-by: Felix Dörre <felix@dogcraft.de>
2024-02-07 12:12:13 +11:00
Felix Dörre 803452a1ac umqtt.simple: Simplify check for user being unused.
There don't seem to be any MQTT implementations that expect an empty
username (instead of the field missing), so the check for unused `user` can
be simplified.

Signed-off-by: Felix Dörre <felix@dogcraft.de>
2024-02-07 12:12:09 +11:00
Carlosgg 7cdf708815 aiohttp: Add new aiohttp package.
Implement `aiohttp` with `ClientSession`, websockets and `SSLContext`
support.

Only client is implemented and API is mostly compatible with CPython
`aiohttp`.

Signed-off-by: Carlos Gil <carlosgilglez@gmail.com>
2023-12-20 16:26:04 +11:00
Bhavesh Kakwani 57ce3ba95c aioble: Fix advertising variable name to use us not ms. 2023-12-20 15:22:21 +11:00
Mark Blakeney 9ceda53180 uaiohttpclient: Update example client code.
Signed-off-by: Mark Blakeney <mark.blakeney@bullet-systems.net>
2023-12-20 14:56:09 +11:00
Mark Blakeney 05efdd03a7 uaiohttpclient: Update "yield from" to "await".
Signed-off-by: Mark Blakeney <mark.blakeney@bullet-systems.net>
2023-12-20 14:56:04 +11:00
Mark Blakeney 9d09cdd4af uaiohttpclient: Make flake8 inspired improvements.
Signed-off-by: Mark Blakeney <mark.blakeney@bullet-systems.net>
2023-12-20 14:56:00 +11:00
Mark Blakeney 149226d3f7 uaiohttpclient: Fix hard coded port 80.
Signed-off-by: Mark Blakeney <mark.blakeney@bullet-systems.net>
2023-12-20 14:55:51 +11:00
scivision ae8ea8d113 os-path: Implement os.path.isfile().
Signed-off-by: Michael Hirsch <michael@scivision.dev>
2023-12-20 14:46:57 +11:00
Andrew Leech f672baa92b aiorepl: Add support for raw mode (ctrl-a).
Provides support for mpremote features like cp and mount.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
2023-12-20 12:35:40 +11:00
Andrew Leech 10c9281dad aiorepl: Add cursor left/right support.
Allows modifying current line, adding/deleting characters in the middle
etc.  Includes home/end keys to move to start/end of current line.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
2023-12-20 12:33:43 +11:00
Andrew Leech d41851ca72 aiorepl: Add support for paste mode (ctrl-e).
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
2023-12-20 12:33:19 +11:00
Andrew Leech e051a120bc aiorepl: Update import of asyncio.
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
2023-12-20 12:33:16 +11:00
Yu Ting 41aa257a31 base64: Implement custom maketrans and translate methods.
Re-implemented bytes.maketrans() and bytes.translate() as there are no such
functions in MicroPython.
2023-12-20 12:26:13 +11:00
Matt Trentini 340243e205 time: Add README to explain the purpose of the time extension library.
Signed-off-by: Matt Trentini <matt.trentini@gmail.com>
2023-12-20 12:05:35 +11:00
Jim Mussared 83f3991f41 lcd160cr: Remove support for options in manifest.
This is the last remaining use of the "options" feature. Nothing in the
main repo which `require()`'s this package sets it.

This work was funded through GitHub Sponsors.

Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
2023-12-20 12:01:22 +11:00
Jim Mussared cee0945f1c all: Replace "black" with "ruff format".
- Add config for [tool.ruff.format] to pyproject.toml.
- Update pre-commit to run both ruff and ruff-format.
- Update a small number of files that change with ruff's rules.
- Update CI.
- Simplify codeformat.py just forward directly to "ruff format"

This work was funded through GitHub Sponsors.

Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
2023-12-20 11:56:24 +11:00
Jim Mussared ad0a2590cc tools/verifygitlog.py: Add git commit message checking.
This adds verifygitlog.py from the main repo, adds it to GitHub workflows,
and also pre-commit.

This work was funded through GitHub Sponsors.

Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
2023-12-20 11:46:40 +11:00
Christian Marangi d8e163bb5f unix-ffi/re: Convert to PCRE2.
PCRE is marked as EOL and won't receive any new security update.

Convert the re module to PCRE2 API to enforce security.  Additional
dependency is now needed with uctypes due to changes in how PCRE2 return
the match_data in a pointer and require special handling.

The converted module is tested with the test_re.py with no regression.

Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
2023-11-01 10:14:36 +11:00
Jim Mussared 0620d02290 .github/workflows/ruff.yml: Pin to 0.1.0.
The `--format` flag was changed to `--output-format` in the recent update.

Pin to this version to prevent further updates from breaking (e.g. through new rules or other changes).

This work was funded through GitHub Sponsors.

Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
2023-10-26 11:48:26 +11:00
171 zmienionych plików z 4943 dodań i 514 usunięć

Wyświetl plik

@ -1,16 +0,0 @@
name: Check code formatting
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: Install packages
run: source tools/ci.sh && ci_code_formatting_setup
- name: Run code formatting
run: source tools/ci.sh && ci_code_formatting_run
- name: Check code formatting
run: git diff --exit-code

Wyświetl plik

@ -0,0 +1,18 @@
name: Check commit message formatting
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: '100'
- uses: actions/setup-python@v4
- name: Check commit message formatting
run: source tools/ci.sh && ci_commit_formatting_run

Wyświetl plik

@ -1,10 +1,11 @@
# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python code lint with ruff
name: Python code lint and formatting with ruff
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: pip install --user ruff
- run: ruff --format=github .
- uses: actions/checkout@v4
- run: pip install --user ruff==0.1.2
- run: ruff check --output-format=github .
- run: ruff format --diff .

Wyświetl plik

@ -1,11 +1,14 @@
repos:
- repo: local
hooks:
- id: codeformat
name: MicroPython codeformat.py for changed files
entry: tools/codeformat.py -v -f
- id: verifygitlog
name: MicroPython git commit message format checker
entry: tools/verifygitlog.py --check-file --ignore-rebase
language: python
verbose: true
stages: [commit-msg]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.280
rev: v0.1.2
hooks:
- id: ruff
id: ruff-format

Wyświetl plik

@ -21,7 +21,7 @@ To use this library, you need to import the library and then start the REPL task
For example, in main.py:
```py
import uasyncio as asyncio
import asyncio
import aiorepl
async def demo():

Wyświetl plik

@ -5,7 +5,7 @@ from micropython import const
import re
import sys
import time
import uasyncio as asyncio
import asyncio
# Import statement (needs to be global, and does not return).
_RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?")
@ -19,6 +19,13 @@ _RE_ASSIGN = re.compile("[^=]=[^=]")
_HISTORY_LIMIT = const(5 + 1)
CHAR_CTRL_A = const(1)
CHAR_CTRL_B = const(2)
CHAR_CTRL_C = const(3)
CHAR_CTRL_D = const(4)
CHAR_CTRL_E = const(5)
async def execute(code, g, s):
if not code.strip():
return
@ -39,13 +46,11 @@ async def __code():
{}
__exec_task = asyncio.create_task(__code())
""".format(
code
)
""".format(code)
async def kbd_intr_task(exec_task, s):
while True:
if ord(await s.read(1)) == 0x03:
if ord(await s.read(1)) == CHAR_CTRL_C:
exec_task.cancel()
return
@ -104,7 +109,9 @@ async def task(g=None, prompt="--> "):
while True:
hist_b = 0 # How far back in the history are we currently.
sys.stdout.write(prompt)
cmd = ""
cmd: str = ""
paste = False
curs = 0 # cursor offset from end of cmd buffer
while True:
b = await s.read(1)
pc = c # save previous character
@ -114,11 +121,19 @@ async def task(g=None, prompt="--> "):
if c < 0x20 or c > 0x7E:
if c == 0x0A:
# LF
if paste:
sys.stdout.write(b)
cmd += b
continue
# If the previous character was also LF, and was less
# than 20 ms ago, this was likely due to CRLF->LFLF
# conversion, so ignore this linefeed.
if pc == 0x0A and time.ticks_diff(t, pt) < 20:
continue
if curs:
# move cursor to end of the line
sys.stdout.write("\x1B[{}C".format(curs))
curs = 0
sys.stdout.write("\n")
if cmd:
# Push current command.
@ -135,31 +150,45 @@ async def task(g=None, prompt="--> "):
elif c == 0x08 or c == 0x7F:
# Backspace.
if cmd:
cmd = cmd[:-1]
sys.stdout.write("\x08 \x08")
elif c == 0x02:
# Ctrl-B
if curs:
cmd = "".join((cmd[: -curs - 1], cmd[-curs:]))
sys.stdout.write(
"\x08\x1B[K"
) # move cursor back, erase to end of line
sys.stdout.write(cmd[-curs:]) # redraw line
sys.stdout.write("\x1B[{}D".format(curs)) # reset cursor location
else:
cmd = cmd[:-1]
sys.stdout.write("\x08 \x08")
elif c == CHAR_CTRL_A:
await raw_repl(s, g)
break
elif c == CHAR_CTRL_B:
continue
elif c == 0x03:
# Ctrl-C
if pc == 0x03 and time.ticks_diff(t, pt) < 20:
# Two very quick Ctrl-C (faster than a human
# typing) likely means mpremote trying to
# escape.
asyncio.new_event_loop()
return
elif c == CHAR_CTRL_C:
if paste:
break
sys.stdout.write("\n")
break
elif c == 0x04:
# Ctrl-D
elif c == CHAR_CTRL_D:
if paste:
result = await execute(cmd, g, s)
if result is not None:
sys.stdout.write(repr(result))
sys.stdout.write("\n")
break
sys.stdout.write("\n")
# Shutdown asyncio.
asyncio.new_event_loop()
return
elif c == CHAR_CTRL_E:
sys.stdout.write("paste mode; Ctrl-C to cancel, Ctrl-D to finish\n===\n")
paste = True
elif c == 0x1B:
# Start of escape sequence.
key = await s.read(2)
if key in ("[A", "[B"):
if key in ("[A", "[B"): # up, down
# Stash the current command.
hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd
# Clear current command.
@ -175,12 +204,122 @@ async def task(g=None, prompt="--> "):
# Update current command.
cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT]
sys.stdout.write(cmd)
elif key == "[D": # left
if curs < len(cmd) - 1:
curs += 1
sys.stdout.write("\x1B")
sys.stdout.write(key)
elif key == "[C": # right
if curs:
curs -= 1
sys.stdout.write("\x1B")
sys.stdout.write(key)
elif key == "[H": # home
pcurs = curs
curs = len(cmd)
sys.stdout.write("\x1B[{}D".format(curs - pcurs)) # move cursor left
elif key == "[F": # end
pcurs = curs
curs = 0
sys.stdout.write("\x1B[{}C".format(pcurs)) # move cursor right
else:
# sys.stdout.write("\\x")
# sys.stdout.write(hex(c))
pass
else:
sys.stdout.write(b)
cmd += b
if curs:
# inserting into middle of line
cmd = "".join((cmd[:-curs], b, cmd[-curs:]))
sys.stdout.write(cmd[-curs - 1 :]) # redraw line to end
sys.stdout.write("\x1B[{}D".format(curs)) # reset cursor location
else:
sys.stdout.write(b)
cmd += b
finally:
micropython.kbd_intr(3)
async def raw_paste(s, g, window=512):
sys.stdout.write("R\x01") # supported
sys.stdout.write(bytearray([window & 0xFF, window >> 8, 0x01]).decode())
eof = False
idx = 0
buff = bytearray(window)
file = b""
while not eof:
for idx in range(window):
b = await s.read(1)
c = ord(b)
if c == CHAR_CTRL_C or c == CHAR_CTRL_D:
# end of file
sys.stdout.write(chr(CHAR_CTRL_D))
if c == CHAR_CTRL_C:
raise KeyboardInterrupt
file += buff[:idx]
eof = True
break
buff[idx] = c
if not eof:
file += buff
sys.stdout.write("\x01") # indicate window available to host
return file
async def raw_repl(s: asyncio.StreamReader, g: dict):
heading = "raw REPL; CTRL-B to exit\n"
line = ""
sys.stdout.write(heading)
while True:
line = ""
sys.stdout.write(">")
while True:
b = await s.read(1)
c = ord(b)
if c == CHAR_CTRL_A:
rline = line
line = ""
if len(rline) == 2 and ord(rline[0]) == CHAR_CTRL_E:
if rline[1] == "A":
line = await raw_paste(s, g)
break
else:
# reset raw REPL
sys.stdout.write(heading)
sys.stdout.write(">")
continue
elif c == CHAR_CTRL_B:
# exit raw REPL
sys.stdout.write("\n")
return 0
elif c == CHAR_CTRL_C:
# clear line
line = ""
elif c == CHAR_CTRL_D:
# entry finished
# indicate reception of command
sys.stdout.write("OK")
break
else:
# let through any other raw 8-bit value
line += b
if len(line) == 0:
# Normally used to trigger soft-reset but stay in raw mode.
# Fake it for aiorepl / mpremote.
sys.stdout.write("Ignored: soft reboot\n")
sys.stdout.write(heading)
try:
result = exec(line, g)
if result is not None:
sys.stdout.write(repr(result))
sys.stdout.write(chr(CHAR_CTRL_D))
except Exception as ex:
print(line)
sys.stdout.write(chr(CHAR_CTRL_D))
sys.print_exception(ex, sys.stdout)
sys.stdout.write(chr(CHAR_CTRL_D))

Wyświetl plik

@ -1,5 +1,5 @@
metadata(
version="0.1.1",
version="0.2.0",
description="Provides an asynchronous REPL that can run concurrently with an asyncio, also allowing await expressions.",
)

Wyświetl plik

@ -1,4 +1,4 @@
metadata(version="0.2.1")
metadata(version="0.2.2")
require("aioble-core")

Wyświetl plik

@ -1,4 +1,4 @@
metadata(version="0.2.0")
metadata(version="0.3.0")
package(
"aioble",

Wyświetl plik

@ -1,4 +1,4 @@
metadata(version="0.2.0")
metadata(version="0.2.1")
require("aioble-core")

Wyświetl plik

@ -108,7 +108,7 @@ _ENV_SENSE_UUID = bluetooth.UUID(0x181A)
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
_GENERIC_THERMOMETER = const(768)
_ADV_INTERVAL_MS = const(250000)
_ADV_INTERVAL_US = const(250000)
temp_service = aioble.Service(_ENV_SENSE_UUID)
temp_char = aioble.Characteristic(temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True)
@ -117,7 +117,7 @@ aioble.register_services(temp_service)
while True:
connection = await aioble.advertise(
_ADV_INTERVAL_MS,
_ADV_INTERVAL_US,
name="temp-sense",
services=[_ENV_SENSE_UUID],
appearance=_GENERIC_THERMOMETER,

Wyświetl plik

@ -195,12 +195,14 @@ class ScanResult:
# Generator that enumerates the service UUIDs that are advertised.
def services(self):
for u in self._decode_field(_ADV_TYPE_UUID16_INCOMPLETE, _ADV_TYPE_UUID16_COMPLETE):
yield bluetooth.UUID(struct.unpack("<H", u)[0])
for u in self._decode_field(_ADV_TYPE_UUID32_INCOMPLETE, _ADV_TYPE_UUID32_COMPLETE):
yield bluetooth.UUID(struct.unpack("<I", u)[0])
for u in self._decode_field(_ADV_TYPE_UUID128_INCOMPLETE, _ADV_TYPE_UUID128_COMPLETE):
yield bluetooth.UUID(u)
for uuid_len, codes in (
(2, (_ADV_TYPE_UUID16_INCOMPLETE, _ADV_TYPE_UUID16_COMPLETE)),
(4, (_ADV_TYPE_UUID32_INCOMPLETE, _ADV_TYPE_UUID32_COMPLETE)),
(16, (_ADV_TYPE_UUID128_INCOMPLETE, _ADV_TYPE_UUID128_COMPLETE)),
):
for u in self._decode_field(*codes):
for i in range(0, len(u), uuid_len):
yield bluetooth.UUID(u[i : i + uuid_len])
# Generator that returns (manufacturer_id, data) tuples.
def manufacturer(self, filter=None):

Wyświetl plik

@ -164,7 +164,7 @@ class DeviceConnection:
# This event is fired by the IRQ both for connection and disconnection
# and controls the device_task.
self._event = None
self._event = asyncio.ThreadSafeFlag()
# If we're waiting for a pending MTU exchange.
self._mtu_event = None
@ -207,15 +207,12 @@ class DeviceConnection:
t._task.cancel()
def _run_task(self):
# Event will be already created this if we initiated connection.
self._event = self._event or asyncio.ThreadSafeFlag()
self._task = asyncio.create_task(self.device_task())
async def disconnect(self, timeout_ms=2000):
await self.disconnected(timeout_ms, disconnect=True)
async def disconnected(self, timeout_ms=60000, disconnect=False):
async def disconnected(self, timeout_ms=None, disconnect=False):
if not self.is_connected():
return

Wyświetl plik

@ -129,14 +129,13 @@ async def advertise(
# Services are prioritised to go in the advertising data because iOS supports
# filtering scan results by service only, so services must come first.
if services:
for uuid in services:
b = bytes(uuid)
if len(b) == 2:
resp_data = _append(adv_data, resp_data, _ADV_TYPE_UUID16_COMPLETE, b)
elif len(b) == 4:
resp_data = _append(adv_data, resp_data, _ADV_TYPE_UUID32_COMPLETE, b)
elif len(b) == 16:
resp_data = _append(adv_data, resp_data, _ADV_TYPE_UUID128_COMPLETE, b)
for uuid_len, code in (
(2, _ADV_TYPE_UUID16_COMPLETE),
(4, _ADV_TYPE_UUID32_COMPLETE),
(16, _ADV_TYPE_UUID128_COMPLETE),
):
if uuids := [bytes(uuid) for uuid in services if len(bytes(uuid)) == uuid_len]:
resp_data = _append(adv_data, resp_data, code, b"".join(uuids))
if name:
resp_data = _append(adv_data, resp_data, _ADV_TYPE_NAME, name)

Wyświetl plik

@ -5,6 +5,7 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
@ -85,7 +86,7 @@ class FileClient:
async def download(self, path, dest):
size = await self.size(path)
send_seq = await self._command(_COMMAND_SEND, path.encode())
await self._command(_COMMAND_SEND, path.encode())
with open(dest, "wb") as f: # noqa: ASYNC101
total = 0
@ -97,7 +98,7 @@ class FileClient:
total += n
async def list(self, path):
send_seq = await self._command(_COMMAND_LIST, path.encode())
await self._command(_COMMAND_LIST, path.encode())
results = bytearray()
buf = bytearray(self._channel.our_mtu)
mv = memoryview(buf)

Wyświetl plik

@ -16,6 +16,7 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
@ -132,15 +133,12 @@ async def control_task(connection):
file = msg[2:].decode()
if command == _COMMAND_SEND:
op_seq = seq
send_file = file
l2cap_event.set()
elif command == _COMMAND_RECV:
op_seq = seq
recv_file = file
l2cap_event.set()
elif command == _COMMAND_LIST:
op_seq = seq
list_path = file
l2cap_event.set()
elif command == _COMMAND_SIZE:
@ -148,7 +146,7 @@ async def control_task(connection):
stat = os.stat(file)
size = stat[6]
status = 0
except OSError as e:
except OSError:
size = 0
status = _STATUS_NOT_FOUND
control_characteristic.notify(

Wyświetl plik

@ -1,5 +1,6 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
@ -54,7 +55,7 @@ async def main():
print("Timeout discovering services/characteristics")
return
while True:
while connection.is_connected():
temp_deg_c = _decode_temperature(await temp_characteristic.read())
print("Temperature: {:.2f}".format(temp_deg_c))
await asyncio.sleep_ms(1000)

Wyświetl plik

@ -1,5 +1,6 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
@ -39,7 +40,7 @@ def _encode_temperature(temp_deg_c):
async def sensor_task():
t = 24.5
while True:
temp_characteristic.write(_encode_temperature(t))
temp_characteristic.write(_encode_temperature(t), send_update=True)
t += random.uniform(-0.5, 0.5)
await asyncio.sleep_ms(1000)
@ -55,7 +56,7 @@ async def peripheral_task():
appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER,
) as connection:
print("Connection from", connection.device)
await connection.disconnected()
await connection.disconnected(timeout_ms=None)
# Run both tasks.

Wyświetl plik

@ -3,7 +3,7 @@
# code. This allows (for development purposes) all the files to live in the
# one directory.
metadata(version="0.4.1")
metadata(version="0.5.2")
# Default installation gives you everything. Install the individual
# components (or a combination of them) if you want a more minimal install.

Wyświetl plik

@ -0,0 +1,71 @@
# Test advertising multiple services, and scanning them.
import sys
# ruff: noqa: E402
sys.path.append("")
import asyncio
import aioble
import bluetooth
TIMEOUT_MS = 5000
_SERVICE_16_A = bluetooth.UUID(0x180F) # Battery Service
_SERVICE_16_B = bluetooth.UUID(0x181A) # Environmental Sensing Service
_SERVICE_32_A = bluetooth.UUID("AB12") # random
_SERVICE_32_B = bluetooth.UUID("CD34") # random
# Acting in peripheral role (advertising).
async def instance0_task():
multitest.globals(BDADDR=aioble.config("mac"))
multitest.next()
# Advertise, and wait for central to connect to us.
print("advertise")
async with await aioble.advertise(
20_000,
name="MPY",
services=[_SERVICE_16_A, _SERVICE_16_B, _SERVICE_32_A, _SERVICE_32_B],
timeout_ms=TIMEOUT_MS,
) as connection:
print("connected")
await connection.disconnected()
print("disconnected")
def instance0():
try:
asyncio.run(instance0_task())
finally:
aioble.stop()
# Acting in central role (scanning).
async def instance1_task():
multitest.next()
wanted_device = aioble.Device(*BDADDR)
# Scan for the wanted device/peripheral and print its advertised services.
async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
async for result in scanner:
if result.device == wanted_device:
services = list(result.services())
if services:
print(services)
break
# Connect to peripheral and then disconnect.
print("connect")
device = aioble.Device(*BDADDR)
async with await device.connect(timeout_ms=TIMEOUT_MS):
print("disconnect")
def instance1():
try:
asyncio.run(instance1_task())
finally:
aioble.stop()

Wyświetl plik

@ -0,0 +1,8 @@
--- instance0 ---
advertise
connected
disconnected
--- instance1 ---
[UUID(0x180f), UUID(0x181a), UUID(0x32314241), UUID(0x34334443)]
connect
disconnect

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble
@ -32,9 +34,7 @@ async def instance0_task():
char1_desc1.write("char1_desc1")
char1_desc2 = aioble.Descriptor(char1, CHAR1_DESC2_UUID, read=True, write=True)
char1_desc2.write("char1_desc2")
char2 = aioble.Characteristic(
service, CHAR2_UUID, read=True, write=True, notify=True, indicate=True
)
aioble.Characteristic(service, CHAR2_UUID, read=True, write=True, notify=True, indicate=True)
char3 = aioble.Characteristic(
service, CHAR3_UUID, read=True, write=True, notify=True, indicate=True
)

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble

Wyświetl plik

@ -2,7 +2,7 @@
advertise
connected
written b'central0'
written b'central2'
written b'central1'
written b'central0' True
written b'central1' True
written b'central2' True

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble

Wyświetl plik

@ -2,10 +2,12 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble
@ -45,6 +47,8 @@ async def instance0_task():
20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
)
print("connect")
client_characteristic = await discover_server(connection)
# Give the central enough time to discover chars.
@ -59,7 +63,7 @@ async def instance0_task():
ticks_end = time.ticks_ms()
ticks_total = time.ticks_diff(ticks_end, ticks_start)
print(
multitest.output_metric(
"Acknowledged {} notifications in {} ms. {} ms/notification.".format(
_NUM_NOTIFICATIONS, ticks_total, ticks_total // _NUM_NOTIFICATIONS
)
@ -85,6 +89,8 @@ async def instance1_task():
device = aioble.Device(*BDADDR)
connection = await device.connect(timeout_ms=TIMEOUT_MS)
print("connect")
client_characteristic = await discover_server(connection)
for i in range(_NUM_NOTIFICATIONS):

Wyświetl plik

@ -0,0 +1,4 @@
--- instance0 ---
connect
--- instance1 ---
connect

Wyświetl plik

@ -1,9 +1,11 @@
import sys
# ruff: noqa: E402
sys.path.append("")
from micropython import const
import time, machine
import machine
import time
import uasyncio as asyncio
import aioble
@ -30,6 +32,8 @@ async def instance0_task():
20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
)
print("connect")
channel = await connection.l2cap_accept(_L2CAP_PSM, _L2CAP_MTU, timeout_ms=TIMEOUT_MS)
random.seed(_RANDOM_SEED)
@ -64,6 +68,8 @@ async def instance1_task():
device = aioble.Device(*BDADDR)
connection = await device.connect(timeout_ms=TIMEOUT_MS)
print("connect")
await asyncio.sleep_ms(500)
channel = await connection.l2cap_connect(_L2CAP_PSM, _L2CAP_MTU, timeout_ms=TIMEOUT_MS)
@ -88,7 +94,7 @@ async def instance1_task():
ticks_end = time.ticks_ms()
total_ticks = time.ticks_diff(ticks_end, ticks_first_byte)
print(
multitest.output_metric(
"Received {}/{} bytes in {} ms. {} B/s".format(
recv_bytes, recv_correct, total_ticks, recv_bytes * 1000 // total_ticks
)

Wyświetl plik

@ -0,0 +1,4 @@
--- instance0 ---
connect
--- instance1 ---
connect

Wyświetl plik

@ -1,10 +1,11 @@
metadata(
version="0.1.0",
version="0.2.0",
description="Common networking packages for all network-capable deployments of MicroPython.",
)
require("mip")
require("ntptime")
require("ssl")
require("requests")
require("webrepl")

Wyświetl plik

@ -2,9 +2,10 @@
# MIT license; Copyright (c) 2017 Damien P. George
from micropython import const
import machine
from utime import sleep_ms
from ustruct import calcsize, pack_into
import uerrno, machine
import uerrno
# for set_orient
PORTRAIT = const(0)

Wyświetl plik

@ -1,7 +1,10 @@
# Driver test for official MicroPython LCD160CR display
# MIT license; Copyright (c) 2017 Damien P. George
import time, math, framebuf, lcd160cr
import framebuf
import lcd160cr
import math
import time
def get_lcd(lcd):

Wyświetl plik

@ -1,8 +1,3 @@
metadata(description="LCD160CR driver.", version="0.1.0")
options.defaults(test=False)
module("lcd160cr.py", opt=3)
if options.test:
module("lcd160cr_test.py", opt=3)

Wyświetl plik

@ -1,6 +1,8 @@
# Test for sdcard block protocol
# Peter hinch 30th Jan 2016
import os, sdcard, machine
import machine
import os
import sdcard
def sdtest():

Wyświetl plik

@ -235,7 +235,7 @@ class ESPFlash:
def flash_attach(self):
self._command(_CMD_SPI_ATTACH, struct.pack("<II", 0, 0))
print(f"Flash attached")
print("Flash attached")
def flash_config(self, flash_size=2 * 1024 * 1024):
self._command(
@ -258,7 +258,6 @@ class ESPFlash:
print(f"Flash write size: {size} total_blocks: {total_blocks} block size: {blksize}")
with open(path, "rb") as f:
seq = 0
subseq = 0
for i in range(total_blocks):
buf = f.read(blksize)
# Update digest
@ -289,7 +288,7 @@ class ESPFlash:
def flash_verify_file(self, path, digest=None, offset=0):
if digest is None:
if self.md5sum is None:
raise Exception(f"MD5 checksum missing.")
raise Exception("MD5 checksum missing.")
digest = binascii.hexlify(self.md5sum.digest())
size = os.stat(path)[6]
@ -301,7 +300,7 @@ class ESPFlash:
if digest == data[0:32]:
print("Firmware verified.")
else:
raise Exception(f"Firmware verification failed.")
raise Exception("Firmware verification failed.")
def reboot(self):
payload = struct.pack("<I", 0)

Wyświetl plik

@ -109,7 +109,7 @@ class AsyncModem:
await asyncio.sleep_ms(timeout_ms)
if _DEBUG:
print(f"wait complete")
print("wait complete")
def _callback(self):
# IRQ callback from BaseModem._radio_isr. May be in Hard IRQ context.

Wyświetl plik

@ -163,6 +163,9 @@ class _SX126x(BaseModem):
# 0x02 is 40us, default value appears undocumented but this is the SX1276 default
self._ramp_val = 0x02
# Configure the SX126x at least once after reset
self._configured = False
if reset:
# If the caller supplies a reset pin argument, reset the radio
reset.init(Pin.OUT, value=0)
@ -383,24 +386,24 @@ class _SX126x(BaseModem):
# see
# https://www.thethingsnetwork.org/forum/t/should-private-lorawan-networks-use-a-different-sync-word/34496/15
syncword = 0x0404 + ((syncword & 0x0F) << 4) + ((syncword & 0xF0) << 8)
self._cmd(">BBH", _CMD_WRITE_REGISTER, _REG_LSYNCRH, syncword)
self._cmd(">BHH", _CMD_WRITE_REGISTER, _REG_LSYNCRH, syncword)
if "output_power" in lora_cfg:
if not self._configured or any(
key in lora_cfg for key in ("output_power", "pa_ramp_us", "tx_ant")
):
pa_config_args, self._output_power = self._get_pa_tx_params(
lora_cfg["output_power"], lora_cfg.get("tx_ant", None)
lora_cfg.get("output_power", self._output_power), lora_cfg.get("tx_ant", None)
)
self._cmd("BBBBB", _CMD_SET_PA_CONFIG, *pa_config_args)
if "pa_ramp_us" in lora_cfg:
self._ramp_val = self._get_pa_ramp_val(
lora_cfg, [10, 20, 40, 80, 200, 800, 1700, 3400]
)
if "pa_ramp_us" in lora_cfg:
self._ramp_val = self._get_pa_ramp_val(
lora_cfg, [10, 20, 40, 80, 200, 800, 1700, 3400]
)
if "output_power" in lora_cfg or "pa_ramp_us" in lora_cfg:
# Only send the SetTxParams command if power level or PA ramp time have changed
self._cmd("BBB", _CMD_SET_TX_PARAMS, self._output_power, self._ramp_val)
if any(key in lora_cfg for key in ("sf", "bw", "coding_rate")):
if not self._configured or any(key in lora_cfg for key in ("sf", "bw", "coding_rate")):
if "sf" in lora_cfg:
self._sf = lora_cfg["sf"]
if self._sf < _CFG_SF_MIN or self._sf > _CFG_SF_MAX:
@ -441,6 +444,7 @@ class _SX126x(BaseModem):
self._reg_write(_REG_RX_GAIN, 0x96 if lora_cfg["rx_boost"] else 0x94)
self._check_error()
self._configured = True
def _invert_workaround(self, enable):
# Apply workaround for DS 15.4 Optimizing the Inverted IQ Operation
@ -465,7 +469,7 @@ class _SX126x(BaseModem):
# See DS 13.1.12 Calibrate Function
# calibParam 0xFE means to calibrate all blocks.
self._cmd("<BB", _CMD_CALIBRATE, 0xFE)
self._cmd("BB", _CMD_CALIBRATE, 0xFE)
time.sleep_us(_CALIBRATE_TYPICAL_TIME_US)
@ -541,7 +545,7 @@ class _SX126x(BaseModem):
else:
timeout = 0 # Single receive mode, no timeout
self._cmd(">BBH", _CMD_SET_RX, timeout >> 16, timeout)
self._cmd(">BBH", _CMD_SET_RX, timeout >> 16, timeout) # 24 bits
return self._dio1
@ -725,10 +729,10 @@ class _SX126x(BaseModem):
return res
def _reg_read(self, addr):
return self._cmd("BBBB", _CMD_READ_REGISTER, addr >> 8, addr & 0xFF, n_read=1)[0]
return self._cmd(">BHB", _CMD_READ_REGISTER, addr, 0, n_read=1)[0]
def _reg_write(self, addr, val):
return self._cmd("BBBB", _CMD_WRITE_REGISTER, addr >> 8, addr & 0xFF, val & 0xFF)
return self._cmd(">BHB", _CMD_WRITE_REGISTER, addr, val & 0xFF)
class _SX1262(_SX126x):

Wyświetl plik

@ -1,3 +1,3 @@
metadata(version="0.1.1")
metadata(version="0.1.2")
require("lora")
package("lora")

Wyświetl plik

@ -519,6 +519,9 @@ class _SX127x(BaseModem):
self._reg_update(_REG_MODEM_CONFIG3, update_mask, modem_config3)
if "syncword" in lora_cfg:
self._reg_write(_REG_SYNC_WORD, lora_cfg["syncword"])
def _reg_write(self, reg, value):
self._cs(0)
if isinstance(value, int):

Wyświetl plik

@ -1,3 +1,3 @@
metadata(version="0.1.0")
metadata(version="0.1.1")
require("lora")
package("lora")

Wyświetl plik

@ -42,8 +42,8 @@ class SyncModem:
tx = True
while tx is True:
tx = self.poll_send()
self._sync_wait(will_irq)
tx = self.poll_send()
return tx
def recv(self, timeout_ms=None, rx_length=0xFF, rx_packet=None):

Wyświetl plik

@ -1,3 +1,3 @@
metadata(version="0.1.0")
metadata(version="0.1.1")
require("lora")
package("lora")

Wyświetl plik

@ -37,10 +37,11 @@ class BaseModem:
self._ant_sw = ant_sw
self._irq_callback = None
# Common configuration settings that need to be tracked by all modem drivers
# (Note that subclasses may set these to other values in their constructors, to match
# the power-on-reset configuration of a particular modem.)
# Common configuration settings that need to be tracked by all modem drivers.
#
# Where modem hardware sets different values after reset, the driver should
# set them back to these defaults (if not provided by the user), so that
# behaviour remains consistent between different modems using the same driver.
self._rf_freq_hz = 0 # Needs to be set via configure()
self._sf = 7 # Spreading factor
self._bw_hz = 125000 # Reset value

Wyświetl plik

@ -1,4 +1,4 @@
metadata(version="0.2.0", description="On-device package installer for network-capable boards")
metadata(version="0.3.0", description="On-device package installer for network-capable boards")
require("requests")

Wyświetl plik

@ -73,6 +73,18 @@ def _rewrite_url(url, branch=None):
+ "/"
+ "/".join(url[2:])
)
elif url.startswith("gitlab:"):
url = url[7:].split("/")
url = (
"https://gitlab.com/"
+ url[0]
+ "/"
+ url[1]
+ "/-/raw/"
+ branch
+ "/"
+ "/".join(url[2:])
)
return url
@ -128,6 +140,7 @@ def _install_package(package, index, target, version, mpy):
package.startswith("http://")
or package.startswith("https://")
or package.startswith("github:")
or package.startswith("gitlab:")
):
if package.endswith(".py") or package.endswith(".mpy"):
print("Downloading {} to {}".format(package, target))

Wyświetl plik

@ -1,3 +1,3 @@
metadata(description="NTP client.", version="0.1.0")
metadata(description="NTP client.", version="0.1.1")
module("ntptime.py", opt=3)

Wyświetl plik

@ -22,12 +22,37 @@ def time():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.settimeout(timeout)
res = s.sendto(NTP_QUERY, addr)
s.sendto(NTP_QUERY, addr)
msg = s.recv(48)
finally:
s.close()
val = struct.unpack("!I", msg[40:44])[0]
# 2024-01-01 00:00:00 converted to an NTP timestamp
MIN_NTP_TIMESTAMP = 3913056000
# Y2036 fix
#
# The NTP timestamp has a 32-bit count of seconds, which will wrap back
# to zero on 7 Feb 2036 at 06:28:16.
#
# We know that this software was written during 2024 (or later).
# So we know that timestamps less than MIN_NTP_TIMESTAMP are impossible.
# So if the timestamp is less than MIN_NTP_TIMESTAMP, that probably means
# that the NTP time wrapped at 2^32 seconds. (Or someone set the wrong
# time on their NTP server, but we can't really do anything about that).
#
# So in that case, we need to add in those extra 2^32 seconds, to get the
# correct timestamp.
#
# This means that this code will work until the year 2160. More precisely,
# this code will not work after 7th Feb 2160 at 06:28:15.
#
if val < MIN_NTP_TIMESTAMP:
val += 0x100000000
# Convert timestamp from NTP format to our internal format
EPOCH_YEAR = utime.gmtime(0)[0]
if EPOCH_YEAR == 2000:
# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60

Wyświetl plik

@ -26,7 +26,7 @@ THE SOFTWARE.
from senml import *
import time
from cbor2 import decoder
import cbor2
pack = SenmlPack("device_name")
@ -38,5 +38,5 @@ while True:
cbor_val = pack.to_cbor()
print(cbor_val)
print(cbor_val.hex())
print(decoder.loads(cbor_val)) # convert to string again so we can print it.
print(cbor2.loads(cbor_val)) # convert to string again so we can print it.
time.sleep(1)

Wyświetl plik

@ -1,6 +1,6 @@
metadata(
description="SenML serialisation for MicroPython.",
version="0.1.0",
version="0.1.1",
pypi_publish="micropython-senml",
)

Wyświetl plik

@ -27,8 +27,7 @@ THE SOFTWARE.
from senml.senml_record import SenmlRecord
from senml.senml_base import SenmlBase
import json
from cbor2 import encoder
from cbor2 import decoder
import cbor2
class SenmlPackIterator:
@ -278,7 +277,7 @@ class SenmlPack(SenmlBase):
:param data: a byte array.
:return: None
"""
records = decoder.loads(data) # load the raw senml data
records = cbor2.loads(data) # load the raw senml data
naming_map = {
"bn": -2,
"bt": -3,
@ -320,7 +319,7 @@ class SenmlPack(SenmlBase):
}
converted = []
self._build_rec_dict(naming_map, converted)
return encoder.dumps(converted)
return cbor2.dumps(converted)
def add(self, item):
"""

Wyświetl plik

@ -1,31 +1,16 @@
#
# uaiohttpclient - fetch URL passed as command line argument.
#
import sys
import uasyncio as asyncio
import uaiohttpclient as aiohttp
def print_stream(resp):
print((yield from resp.read()))
return
while True:
line = yield from resp.readline()
if not line:
break
print(line.rstrip())
def run(url):
resp = yield from aiohttp.request("GET", url)
async def run(url):
resp = await aiohttp.request("GET", url)
print(resp)
yield from print_stream(resp)
print(await resp.read())
import sys
import logging
logging.basicConfig(level=logging.INFO)
url = sys.argv[1]
loop = asyncio.get_event_loop()
loop.run_until_complete(run(url))
loop.close()
asyncio.run(run(url))

Wyświetl plik

@ -1,4 +1,4 @@
metadata(description="HTTP client module for MicroPython uasyncio module", version="0.5.1")
metadata(description="HTTP client module for MicroPython uasyncio module", version="0.5.2")
# Originally written by Paul Sokolovsky.

Wyświetl plik

@ -5,8 +5,8 @@ class ClientResponse:
def __init__(self, reader):
self.content = reader
def read(self, sz=-1):
return (yield from self.content.read(sz))
async def read(self, sz=-1):
return await self.content.read(sz)
def __repr__(self):
return "<ClientResponse %d %s>" % (self.status, self.headers)
@ -17,22 +17,22 @@ class ChunkedClientResponse(ClientResponse):
self.content = reader
self.chunk_size = 0
def read(self, sz=4 * 1024 * 1024):
async def read(self, sz=4 * 1024 * 1024):
if self.chunk_size == 0:
l = yield from self.content.readline()
line = await self.content.readline()
# print("chunk line:", l)
l = l.split(b";", 1)[0]
self.chunk_size = int(l, 16)
line = line.split(b";", 1)[0]
self.chunk_size = int(line, 16)
# print("chunk size:", self.chunk_size)
if self.chunk_size == 0:
# End of message
sep = yield from self.content.read(2)
sep = await self.content.read(2)
assert sep == b"\r\n"
return b""
data = yield from self.content.read(min(sz, self.chunk_size))
data = await self.content.read(min(sz, self.chunk_size))
self.chunk_size -= len(data)
if self.chunk_size == 0:
sep = yield from self.content.read(2)
sep = await self.content.read(2)
assert sep == b"\r\n"
return data
@ -40,40 +40,46 @@ class ChunkedClientResponse(ClientResponse):
return "<ChunkedClientResponse %d %s>" % (self.status, self.headers)
def request_raw(method, url):
async def request_raw(method, url):
try:
proto, dummy, host, path = url.split("/", 3)
except ValueError:
proto, dummy, host = url.split("/", 2)
path = ""
if ":" in host:
host, port = host.split(":")
port = int(port)
else:
port = 80
if proto != "http:":
raise ValueError("Unsupported protocol: " + proto)
reader, writer = yield from asyncio.open_connection(host, 80)
# Use protocol 1.0, because 1.1 always allows to use chunked transfer-encoding
# But explicitly set Connection: close, even though this should be default for 1.0,
# because some servers misbehave w/o it.
reader, writer = await asyncio.open_connection(host, port)
# Use protocol 1.0, because 1.1 always allows to use chunked
# transfer-encoding But explicitly set Connection: close, even
# though this should be default for 1.0, because some servers
# misbehave w/o it.
query = "%s /%s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\nUser-Agent: compat\r\n\r\n" % (
method,
path,
host,
)
yield from writer.awrite(query.encode("latin-1"))
# yield from writer.aclose()
await writer.awrite(query.encode("latin-1"))
return reader
def request(method, url):
async def request(method, url):
redir_cnt = 0
redir_url = None
while redir_cnt < 2:
reader = yield from request_raw(method, url)
reader = await request_raw(method, url)
headers = []
sline = yield from reader.readline()
sline = await reader.readline()
sline = sline.split(None, 2)
status = int(sline[1])
chunked = False
while True:
line = yield from reader.readline()
line = await reader.readline()
if not line or line == b"\r\n":
break
headers.append(line)
@ -85,7 +91,7 @@ def request(method, url):
if 301 <= status <= 303:
redir_cnt += 1
yield from reader.aclose()
await reader.aclose()
continue
break

Wyświetl plik

@ -24,7 +24,7 @@ class ContextManagerTestCase(unittest.TestCase):
def test_context_manager_on_error(self):
exc = Exception()
try:
with self._manager(123) as x:
with self._manager(123):
raise exc
except Exception as e:
self.assertEqual(exc, e)

Wyświetl plik

@ -43,36 +43,28 @@ def parse_resp(buf, is_ipv6):
if is_ipv6:
typ = 28 # AAAA
id = buf.readbin(">H")
buf.readbin(">H") # id
flags = buf.readbin(">H")
assert flags & 0x8000
qcnt = buf.readbin(">H")
buf.readbin(">H") # qcnt
acnt = buf.readbin(">H")
nscnt = buf.readbin(">H")
addcnt = buf.readbin(">H")
# print(qcnt, acnt, nscnt, addcnt)
buf.readbin(">H") # nscnt
buf.readbin(">H") # addcnt
skip_fqdn(buf)
v = buf.readbin(">H")
# print(v)
v = buf.readbin(">H")
# print(v)
buf.readbin(">H")
buf.readbin(">H")
for i in range(acnt):
# print("Resp #%d" % i)
# v = read_fqdn(buf)
# print(v)
skip_fqdn(buf)
t = buf.readbin(">H")
# print("Type", t)
v = buf.readbin(">H")
# print("Class", v)
v = buf.readbin(">I")
# print("TTL", v)
t = buf.readbin(">H") # Type
buf.readbin(">H") # Class
buf.readbin(">I") # TTL
rlen = buf.readbin(">H")
# print("rlen", rlen)
rval = buf.read(rlen)
# print(rval)
if t == typ:
return rval

Wyświetl plik

@ -1,4 +1,4 @@
metadata(description="Lightweight MQTT client for MicroPython.", version="1.3.4")
metadata(description="Lightweight MQTT client for MicroPython.", version="1.4.0")
# Originally written by Paul Sokolovsky.

Wyświetl plik

@ -16,8 +16,7 @@ class MQTTClient:
user=None,
password=None,
keepalive=0,
ssl=False,
ssl_params={},
ssl=None,
):
if port == 0:
port = 8883 if ssl else 1883
@ -26,7 +25,6 @@ class MQTTClient:
self.server = server
self.port = port
self.ssl = ssl
self.ssl_params = ssl_params
self.pid = 0
self.cb = None
self.user = user
@ -67,15 +65,13 @@ class MQTTClient:
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
self.sock.connect(addr)
if self.ssl:
import ussl
self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
self.sock = self.ssl.wrap_socket(self.sock, server_hostname=self.server)
premsg = bytearray(b"\x10\0\0\0\0\0")
msg = bytearray(b"\x04MQTT\x04\x02\0\0")
sz = 10 + 2 + len(self.client_id)
msg[6] = clean_session << 1
if self.user is not None:
if self.user:
sz += 2 + len(self.user) + 2 + len(self.pswd)
msg[6] |= 0xC0
if self.keepalive:
@ -101,7 +97,7 @@ class MQTTClient:
if self.lw_topic:
self._send_str(self.lw_topic)
self._send_str(self.lw_msg)
if self.user is not None:
if self.user:
self._send_str(self.user)
self._send_str(self.pswd)
resp = self.sock.read(4)

Wyświetl plik

@ -1,4 +1,4 @@
metadata(version="0.6.0")
metadata(version="0.7.0")
# Originally written by Paul Sokolovsky.

Wyświetl plik

@ -12,7 +12,7 @@ def urlopen(url, data=None, method="GET"):
if proto == "http:":
port = 80
elif proto == "https:":
import ussl
import tls
port = 443
else:
@ -29,7 +29,9 @@ def urlopen(url, data=None, method="GET"):
try:
s.connect(ai[-1])
if proto == "https:":
s = ussl.wrap_socket(s, server_hostname=host)
context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
context.verify_mode = tls.CERT_NONE
s = context.wrap_socket(s, server_hostname=host)
s.write(method)
s.write(b" /")
@ -46,10 +48,10 @@ def urlopen(url, data=None, method="GET"):
if data:
s.write(data)
l = s.readline()
l = l.split(None, 2)
l = s.readline() # Status-Line
# l = l.split(None, 2)
# print(l)
status = int(l[1])
# status = int(l[1]) # FIXME: Status-Code element is not currently checked
while True:
l = s.readline()
if not l or l == b"\r\n":

Wyświetl plik

@ -0,0 +1,136 @@
# USB Drivers
These packages allow implementing USB functionality on a MicroPython system
using pure Python code.
Currently only USB device is implemented, not USB host.
## USB Device support
### Support
USB Device support depends on the low-level
[machine.USBDevice](https://docs.micropython.org/en/latest/library/machine.USBDevice.html)
class. This class is new and not supported on all ports, so please check the
documentation for your MicroPython version. It is possible to implement a USB
device using only the low-level USBDevice class. However, the packages here are
higher level and easier to use.
For more information about how to install packages, or "freeze" them into a
firmware image, consult the [MicroPython documentation on "Package
management"](https://docs.micropython.org/en/latest/reference/packages.html).
### Examples
The [examples/device](examples/device) directory in this repo has a range of
examples. After installing necessary packages, you can download an example and
run it with `mpremote run EXAMPLE_FILENAME.py` ([mpremote
docs](https://docs.micropython.org/en/latest/reference/mpremote.html#mpremote-command-run)).
#### Unexpected serial disconnects
If you normally connect to your MicroPython device over a USB serial port ("USB
CDC"), then running a USB example will disconnect mpremote when the new USB
device configuration activates and the serial port has to temporarily
disconnect. It is likely that mpremote will print an error. The example should
still start running, if necessary then you can reconnect with mpremote and type
Ctrl-B to restore the MicroPython REPL and/or Ctrl-C to stop the running
example.
If you use `mpremote run` again while a different USB device configuration is
already active, then the USB serial port may disconnect immediately before the
example runs. This is because mpremote has to soft-reset MicroPython, and when
the existing USB device is reset then the entire USB port needs to reset. If
this happens, run the same `mpremote run` command again.
We plan to add features to `mpremote` so that this limitation is less
disruptive. Other tools that communicate with MicroPython over the serial port
will encounter similar issues when runtime USB is in use.
### Initialising runtime USB
The overall pattern for enabling USB devices at runtime is:
1. Instantiate the Interface objects for your desired USB device.
2. Call `usb.device.get()` to get the singleton object for the high-level USB device.
3. Call `init(...)` to pass the desired interfaces as arguments, plus any custom
keyword arguments to configure the overall device.
An example, similar to [mouse_example.py](examples/device/mouse_example.py):
```py
m = usb.device.mouse.MouseInterface()
usb.device.get().init(m, builtin_driver=True)
```
Setting `builtin_driver=True` means that any built-in USB serial port will still
be available. Otherwise, you may permanently lose access to MicroPython until
the next time the device resets.
See [Unexpected serial disconnects](#Unexpected-serial-disconnects), above, for
an explanation of possible errors or disconnects when the runtime USB device
initialises.
Placing the call to `usb.device.get().init()` into the `boot.py` of the
MicroPython file system allows the runtime USB device to initialise immediately
on boot, before any built-in USB. This is a feature (not a bug) and allows you
full control over the USB device, for example to only enable USB HID and prevent
REPL access to the system.
However, note that calling this function on boot without `builtin_driver=True`
will make the MicroPython USB serial interface permanently inaccessible until
you "safe mode boot" (on supported boards) or completely erase the flash of your
device.
### Package usb-device
This base package contains the common implementation components for the other
packages, and can be used to implement new and different USB interface support.
All of the other `usb-device-<name>` packages depend on this package, and it
will be automatically installed as needed.
Specicially, this package provides the `usb.device.get()` function for accessing
the Device singleton object, and the `usb.device.core` module which contains the
low-level classes and utility functions for implementing new USB interface
drivers in Python. The best examples of how to use the core classes is the
source code of the other USB device packages.
### Package usb-device-keyboard
This package provides the `usb.device.keyboard` module. See
[keyboard_example.py](examples/device/keyboard_example.py) for an example
program.
### Package usb-device-mouse
This package provides the `usb.device.mouse` module. See
[mouse_example.py](examples/device/mouse_example.py) for an example program.
### Package usb-device-hid
This package provides the `usb.device.hid` module. USB HID (Human Interface
Device) class allows creating a wide variety of device types. The most common
are mouse and keyboard, which have their own packages in micropython-lib.
However, using the usb-device-hid package directly allows creation of any kind
of HID device.
See [hid_custom_keypad_example.py](examples/device/hid_custom_keypad_example.py)
for an example of a Keypad HID device with a custom HID descriptor.
### Package usb-device-cdc
This package provides the `usb.device.cdc` module. USB CDC (Communications
Device Class) is most commonly used for virtual serial port USB interfaces, and
that is what is supported here.
The example [cdc_repl_example.py](examples/device/cdc_repl_example.py)
demonstrates how to add a second USB serial interface and duplicate the
MicroPython REPL between the two.
### Package usb-device-midi
This package provides the `usb.device.midi` module. This allows implementing
USB MIDI devices in MicroPython.
The example [midi_example.py](examples/device/midi_example.py) demonstrates how
to create a simple MIDI device to send MIDI data to and from the USB host.

Wyświetl plik

@ -0,0 +1,47 @@
# MicroPython USB CDC REPL example
#
# Example demonstrating how to use os.dupterm() to provide the
# MicroPython REPL on a dynamic CDCInterface() serial port.
#
# To run this example:
#
# 1. Make sure `usb-device-cdc` is installed via: mpremote mip install usb-device-cdc
#
# 2. Run the example via: mpremote run cdc_repl_example.py
#
# 3. mpremote will exit with an error after the previous step, because when the
# example runs the existing USB device disconnects and then re-enumerates with
# the second serial port. If you check (for example by running mpremote connect
# list) then you should now see two USB serial devices.
#
# 4. Connect to one of the new ports: mpremote connect PORTNAME
#
# It may be necessary to type Ctrl-B to exit the raw REPL mode and resume the
# interactive REPL after mpremote connects.
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
import os
import time
import usb.device
from usb.device.cdc import CDCInterface
cdc = CDCInterface()
cdc.init(timeout=0) # zero timeout makes this non-blocking, suitable for os.dupterm()
# pass builtin_driver=True so that we get the built-in USB-CDC alongside,
# if it's available.
usb.device.get().init(cdc, builtin_driver=True)
print("Waiting for USB host to configure the interface...")
# wait for host enumerate as a CDC device...
while not cdc.is_open():
time.sleep_ms(100)
# Note: This example doesn't wait for the host to access the new CDC port,
# which could be done by polling cdc.dtr, as this will block the REPL
# from resuming while this code is still executing.
print("CDC port enumerated, duplicating REPL...")
old_term = os.dupterm(cdc)

Wyświetl plik

@ -0,0 +1,144 @@
# MicroPython USB HID custom Keypad example
#
# This example demonstrates creating a custom HID device with its own
# HID descriptor, in this case for a USB number keypad.
#
# For higher level examples that require less code to use, see mouse_example.py
# and keyboard_example.py
#
# To run this example:
#
# 1. Make sure `usb-device-hid` is installed via: mpremote mip install usb-device-hid
#
# 2. Run the example via: mpremote run hid_custom_keypad_example.py
#
# 3. mpremote will exit with an error after the previous step, because when the
# example runs the existing USB device disconnects and then re-enumerates with
# the custom HID interface present. At this point, the example is running.
#
# 4. To see output from the example, re-connect: mpremote connect PORTNAME
#
# MIT license; Copyright (c) 2023 Dave Wickham, 2023-2024 Angus Gratton
from micropython import const
import time
import usb.device
from usb.device.hid import HIDInterface
_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
def keypad_example():
k = KeypadInterface()
usb.device.get().init(k, builtin_driver=True)
while not k.is_open():
time.sleep_ms(100)
while True:
time.sleep(2)
print("Press NumLock...")
k.send_key("<NumLock>")
time.sleep_ms(100)
k.send_key()
time.sleep(1)
# continue
print("Press ...")
for _ in range(3):
time.sleep(0.1)
k.send_key(".")
time.sleep(0.1)
k.send_key()
print("Starting again...")
class KeypadInterface(HIDInterface):
# Very basic synchronous USB keypad HID interface
def __init__(self):
super().__init__(
_KEYPAD_REPORT_DESC,
set_report_buf=bytearray(1),
protocol=_INTERFACE_PROTOCOL_KEYBOARD,
interface_str="MicroPython Keypad",
)
self.numlock = False
def on_set_report(self, report_data, _report_id, _report_type):
report = report_data[0]
b = bool(report & 1)
if b != self.numlock:
print("Numlock: ", b)
self.numlock = b
def send_key(self, key=None):
if key is None:
self.send_report(b"\x00")
else:
self.send_report(_key_to_id(key).to_bytes(1, "big"))
# See HID Usages and Descriptions 1.4, section 10 Keyboard/Keypad Page (0x07)
#
# This keypad example has a contiguous series of keys (KEYPAD_KEY_IDS) starting
# from the NumLock/Clear keypad key (0x53), but you can send any Key IDs from
# the table in the HID Usages specification.
_KEYPAD_KEY_OFFS = const(0x53)
_KEYPAD_KEY_IDS = [
"<NumLock>",
"/",
"*",
"-",
"+",
"<Enter>",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
".",
]
def _key_to_id(key):
# This is a little slower than making a dict for lookup, but uses
# less memory and O(n) can be fast enough when n is small.
return _KEYPAD_KEY_IDS.index(key) + _KEYPAD_KEY_OFFS
# HID Report descriptor for a numeric keypad
#
# fmt: off
_KEYPAD_REPORT_DESC = (
b'\x05\x01' # Usage Page (Generic Desktop)
b'\x09\x07' # Usage (Keypad)
b'\xA1\x01' # Collection (Application)
b'\x05\x07' # Usage Page (Keypad)
b'\x19\x00' # Usage Minimum (0)
b'\x29\xFF' # Usage Maximum (ff)
b'\x15\x00' # Logical Minimum (0)
b'\x25\xFF' # Logical Maximum (ff)
b'\x95\x01' # Report Count (1),
b'\x75\x08' # Report Size (8),
b'\x81\x00' # Input (Data, Array, Absolute)
b'\x05\x08' # Usage page (LEDs)
b'\x19\x01' # Usage Minimum (1)
b'\x29\x01' # Usage Maximum (1),
b'\x95\x01' # Report Count (1),
b'\x75\x01' # Report Size (1),
b'\x91\x02' # Output (Data, Variable, Absolute)
b'\x95\x01' # Report Count (1),
b'\x75\x07' # Report Size (7),
b'\x91\x01' # Output (Constant) - padding bits
b'\xC0' # End Collection
)
# fmt: on
keypad_example()

Wyświetl plik

@ -0,0 +1,97 @@
# MicroPython USB Keyboard example
#
# To run this example:
#
# 1. Check the KEYS assignment below, and connect buttons or switches to the
# assigned GPIOs. You can change the entries as needed, look up the reference
# for your board to see what pins are available. Note that the example uses
# "active low" logic, so pressing a switch or button should switch the
# connected pin to Ground (0V).
#
# 2. Make sure `usb-device-keyboard` is installed via: mpremote mip install usb-device-keyboard
#
# 3. Run the example via: mpremote run keyboard_example.py
#
# 4. mpremote will exit with an error after the previous step, because when the
# example runs the existing USB device disconnects and then re-enumerates with
# the keyboard interface present. At this point, the example is running.
#
# 5. The example doesn't print anything to the serial port, but to stop it first
# re-connect: mpremote connect PORTNAME
#
# 6. Type Ctrl-C to interrupt the running example and stop it. You may have to
# also type Ctrl-B to restore the interactive REPL.
#
# To implement a keyboard with different USB HID characteristics, copy the
# usb-device-keyboard/usb/device/keyboard.py file into your own project and modify
# KeyboardInterface.
#
# MIT license; Copyright (c) 2024 Angus Gratton
import usb.device
from usb.device.keyboard import KeyboardInterface, KeyCode, LEDCode
from machine import Pin
import time
# Tuples mapping Pin inputs to the KeyCode each input generates
#
# (Big keyboards usually multiplex multiple keys per input with a scan matrix,
# but this is a simple example.)
KEYS = (
(Pin.cpu.GPIO10, KeyCode.CAPS_LOCK),
(Pin.cpu.GPIO11, KeyCode.LEFT_SHIFT),
(Pin.cpu.GPIO12, KeyCode.M),
(Pin.cpu.GPIO13, KeyCode.P),
# ... add more pin to KeyCode mappings here if needed
)
# Tuples mapping Pin outputs to the LEDCode that turns the output on
LEDS = (
(Pin.board.LED, LEDCode.CAPS_LOCK),
# ... add more pin to LEDCode mappings here if needed
)
class ExampleKeyboard(KeyboardInterface):
def on_led_update(self, led_mask):
# print(hex(led_mask))
for pin, code in LEDS:
# Set the pin high if 'code' bit is set in led_mask
pin(code & led_mask)
def keyboard_example():
# Initialise all the pins as active-low inputs with pullup resistors
for pin, _ in KEYS:
pin.init(Pin.IN, Pin.PULL_UP)
# Initialise all the LEDs as active-high outputs
for pin, _ in LEDS:
pin.init(Pin.OUT, value=0)
# Register the keyboard interface and re-enumerate
k = ExampleKeyboard()
usb.device.get().init(k, builtin_driver=True)
print("Entering keyboard loop...")
keys = [] # Keys held down, reuse the same list object
prev_keys = [None] # Previous keys, starts with a dummy value so first
# iteration will always send
while True:
if k.is_open():
keys.clear()
for pin, code in KEYS:
if not pin(): # active-low
keys.append(code)
if keys != prev_keys:
# print(keys)
k.send_keys(keys)
prev_keys.clear()
prev_keys.extend(keys)
# This simple example scans each input in an infinite loop, but a more
# complex implementation would probably use a timer or similar.
time.sleep_ms(1)
keyboard_example()

Wyświetl plik

@ -0,0 +1,78 @@
# MicroPython USB MIDI example
#
# This example demonstrates creating a custom MIDI device.
#
# To run this example:
#
# 1. Make sure `usb-device-midi` is installed via: mpremote mip install usb-device-midi
#
# 2. Run the example via: mpremote run midi_example.py
#
# 3. mpremote will exit with an error after the previous step, because when the
# example runs the existing USB device disconnects and then re-enumerates with
# the MIDI interface present. At this point, the example is running.
#
# 4. To see output from the example, re-connect: mpremote connect PORTNAME
#
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
import usb.device
from usb.device.midi import MIDIInterface
import time
class MIDIExample(MIDIInterface):
# Very simple example event handler functions, showing how to receive note
# and control change messages sent from the host to the device.
#
# If you need to send MIDI data to the host, then it's fine to instantiate
# MIDIInterface class directly.
def on_open(self):
super().on_open()
print("Device opened by host")
def on_note_on(self, channel, pitch, vel):
print(f"RX Note On channel {channel} pitch {pitch} velocity {vel}")
def on_note_off(self, channel, pitch, vel):
print(f"RX Note Off channel {channel} pitch {pitch} velocity {vel}")
def on_control_change(self, channel, controller, value):
print(f"RX Control channel {channel} controller {controller} value {value}")
m = MIDIExample()
# Remove builtin_driver=True if you don't want the MicroPython serial REPL available.
usb.device.get().init(m, builtin_driver=True)
print("Waiting for USB host to configure the interface...")
while not m.is_open():
time.sleep_ms(100)
print("Starting MIDI loop...")
# TX constants
CHANNEL = 0
PITCH = 60
CONTROLLER = 64
control_val = 0
while m.is_open():
time.sleep(1)
print(f"TX Note On channel {CHANNEL} pitch {PITCH}")
m.note_on(CHANNEL, PITCH) # Velocity is an optional third argument
time.sleep(0.5)
print(f"TX Note Off channel {CHANNEL} pitch {PITCH}")
m.note_off(CHANNEL, PITCH)
time.sleep(1)
print(f"TX Control channel {CHANNEL} controller {CONTROLLER} value {control_val}")
m.control_change(CHANNEL, CONTROLLER, control_val)
control_val += 1
if control_val == 0x7F:
control_val = 0
time.sleep(1)
print("USB host has reset device, example done.")

Wyświetl plik

@ -0,0 +1,52 @@
# MicroPython USB Mouse example
#
# To run this example:
#
# 1. Make sure `usb-device-mouse` is installed via: mpremote mip install usb-device-mouse
#
# 2. Run the example via: mpremote run mouse_example.py
#
# 3. mpremote will exit with an error after the previous step, because when the
# example runs the existing USB device disconnects and then re-enumerates with
# the mouse interface present. At this point, the example is running.
#
# 4. You should see the mouse move and right click. At this point, the example
# is finished executing.
#
# To implement a more complex mouse with more buttons or other custom interface
# features, copy the usb-device-mouse/usb/device/mouse.py file into your own
# project and modify MouseInterface.
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
import time
import usb.device
from usb.device.mouse import MouseInterface
def mouse_example():
m = MouseInterface()
# Note: builtin_driver=True means that if there's a USB-CDC REPL
# available then it will appear as well as the HID device.
usb.device.get().init(m, builtin_driver=True)
# wait for host to enumerate as a HID device...
while not m.is_open():
time.sleep_ms(100)
time.sleep_ms(2000)
print("Moving...")
m.move_by(-100, 0)
m.move_by(-100, 0)
time.sleep_ms(500)
print("Clicking...")
m.click_right(True)
time.sleep_ms(200)
m.click_right(False)
print("Done!")
mouse_example()

Wyświetl plik

@ -0,0 +1,3 @@
metadata(version="0.1.0")
require("usb-device")
package("usb")

Wyświetl plik

@ -0,0 +1,437 @@
# MicroPython USB CDC module
# MIT license; Copyright (c) 2022 Martin Fischer, 2023-2024 Angus Gratton
import io
import time
import errno
import machine
import struct
from micropython import const
from .core import Interface, Buffer, split_bmRequestType
_EP_IN_FLAG = const(1 << 7)
# Control transfer stages
_STAGE_IDLE = const(0)
_STAGE_SETUP = const(1)
_STAGE_DATA = const(2)
_STAGE_ACK = const(3)
# Request types
_REQ_TYPE_STANDARD = const(0x0)
_REQ_TYPE_CLASS = const(0x1)
_REQ_TYPE_VENDOR = const(0x2)
_REQ_TYPE_RESERVED = const(0x3)
_DEV_CLASS_MISC = const(0xEF)
_CS_DESC_TYPE = const(0x24) # CS Interface type communication descriptor
# CDC control interface definitions
_INTERFACE_CLASS_CDC = const(2)
_INTERFACE_SUBCLASS_CDC = const(2) # Abstract Control Mode
_PROTOCOL_NONE = const(0) # no protocol
# CDC descriptor subtype
# see also CDC120.pdf, table 13
_CDC_FUNC_DESC_HEADER = const(0)
_CDC_FUNC_DESC_CALL_MANAGEMENT = const(1)
_CDC_FUNC_DESC_ABSTRACT_CONTROL = const(2)
_CDC_FUNC_DESC_UNION = const(6)
# CDC class requests, table 13, PSTN subclass
_SET_LINE_CODING_REQ = const(0x20)
_GET_LINE_CODING_REQ = const(0x21)
_SET_CONTROL_LINE_STATE = const(0x22)
_SEND_BREAK_REQ = const(0x23)
_LINE_CODING_STOP_BIT_1 = const(0)
_LINE_CODING_STOP_BIT_1_5 = const(1)
_LINE_CODING_STOP_BIT_2 = const(2)
_LINE_CODING_PARITY_NONE = const(0)
_LINE_CODING_PARITY_ODD = const(1)
_LINE_CODING_PARITY_EVEN = const(2)
_LINE_CODING_PARITY_MARK = const(3)
_LINE_CODING_PARITY_SPACE = const(4)
_LINE_STATE_DTR = const(1)
_LINE_STATE_RTS = const(2)
_PARITY_BITS_REPR = "NOEMS"
_STOP_BITS_REPR = ("1", "1.5", "2")
# Other definitions
_CDC_VERSION = const(0x0120) # release number in binary-coded decimal
# Number of endpoints in each interface
_CDC_CONTROL_EP_NUM = const(1)
_CDC_DATA_EP_NUM = const(2)
# CDC data interface definitions
_CDC_ITF_DATA_CLASS = const(0xA)
_CDC_ITF_DATA_SUBCLASS = const(0)
_CDC_ITF_DATA_PROT = const(0) # no protocol
# Length of the bulk transfer endpoints. Maybe should be configurable?
_BULK_EP_LEN = const(64)
# MicroPython error constants (negated as IOBase.ioctl uses negative return values for error codes)
# these must match values in py/mperrno.h
_MP_EINVAL = const(-22)
_MP_ETIMEDOUT = const(-110)
# MicroPython stream ioctl requests, same as py/stream.h
_MP_STREAM_FLUSH = const(1)
_MP_STREAM_POLL = const(3)
# MicroPython ioctl poll values, same as py/stream.h
_MP_STREAM_POLL_WR = const(0x04)
_MP_STREAM_POLL_RD = const(0x01)
_MP_STREAM_POLL_HUP = const(0x10)
class CDCInterface(io.IOBase, Interface):
# USB CDC serial device class, designed to resemble machine.UART
# with some additional methods.
#
# Relies on multiple inheritance so it can be an io.IOBase for stream
# functions and also a Interface (actually an Interface Association
# Descriptor holding two interfaces.)
def __init__(self, **kwargs):
# io.IOBase has no __init__()
Interface.__init__(self)
# Callbacks for particular control changes initiated by the host
self.break_cb = None # Host sent a "break" condition
self.line_state_cb = None
self.line_coding_cb = None
self._line_state = 0 # DTR & RTS
# Set a default line coding of 115200/8N1
self._line_coding = bytearray(b"\x00\xc2\x01\x00\x00\x00\x08")
self._wb = () # Optional write Buffer (IN endpoint), set by CDC.init()
self._rb = () # Optional read Buffer (OUT endpoint), set by CDC.init()
self._timeout = 1000 # set from CDC.init() as well
# one control interface endpoint, two data interface endpoints
self.ep_c_in = self.ep_d_in = self.ep_d_out = None
self._c_itf = None # Number of control interface, data interface is one more
self.init(**kwargs)
def init(
self, baudrate=9600, bits=8, parity="N", stop=1, timeout=None, txbuf=256, rxbuf=256, flow=0
):
# Configure the CDC serial port. Note that many of these settings like
# baudrate, bits, parity, stop don't change the USB-CDC device behavior
# at all, only the "line coding" as communicated from/to the USB host.
# Store initial line coding parameters in the USB CDC binary format
# (there is nothing implemented to further change these from Python
# code, the USB host sets them.)
struct.pack_into(
"<LBBB",
self._line_coding,
0,
baudrate,
_STOP_BITS_REPR.index(str(stop)),
_PARITY_BITS_REPR.index(parity),
bits,
)
if flow != 0:
raise NotImplementedError # UART flow control currently not supported
if not (txbuf and rxbuf):
raise ValueError # Buffer sizes are required
self._timeout = timeout
self._wb = Buffer(txbuf)
self._rb = Buffer(rxbuf)
###
### Line State & Line Coding State property getters
###
@property
def rts(self):
return bool(self._line_state & _LINE_STATE_RTS)
@property
def dtr(self):
return bool(self._line_state & _LINE_STATE_DTR)
# Line Coding Representation
# Byte 0-3 Byte 4 Byte 5 Byte 6
# dwDTERate bCharFormat bParityType bDataBits
@property
def baudrate(self):
return struct.unpack("<LBBB", self._line_coding)[0]
@property
def stop_bits(self):
return _STOP_BITS_REPR[self._line_coding[4]]
@property
def parity(self):
return _PARITY_BITS_REPR[self._line_coding[5]]
@property
def data_bits(self):
return self._line_coding[6]
def __repr__(self):
return f"{self.baudrate}/{self.data_bits}{self.parity}{self.stop_bits} rts={self.rts} dtr={self.dtr}"
###
### Set callbacks for operations initiated by the host
###
def set_break_cb(self, cb):
self.break_cb = cb
def set_line_state_cb(self, cb):
self.line_state_cb = cb
def set_line_coding_cb(self, cb):
self.line_coding_cb = cb
###
### USB Interface Implementation
###
def desc_cfg(self, desc, itf_num, ep_num, strs):
# CDC needs a Interface Association Descriptor (IAD) wrapping two interfaces: Control & Data interfaces
desc.interface_assoc(itf_num, 2, _INTERFACE_CLASS_CDC, _INTERFACE_SUBCLASS_CDC)
# Now add the Control interface descriptor
self._c_itf = itf_num
desc.interface(itf_num, _CDC_CONTROL_EP_NUM, _INTERFACE_CLASS_CDC, _INTERFACE_SUBCLASS_CDC)
# Append the CDC class-specific interface descriptor
# see CDC120-track, p20
desc.pack(
"<BBBH",
5, # bFunctionLength
_CS_DESC_TYPE, # bDescriptorType
_CDC_FUNC_DESC_HEADER, # bDescriptorSubtype
_CDC_VERSION, # cdc version
)
# CDC-PSTN table3 "Call Management"
# set to No
desc.pack(
"<BBBBB",
5, # bFunctionLength
_CS_DESC_TYPE, # bDescriptorType
_CDC_FUNC_DESC_CALL_MANAGEMENT, # bDescriptorSubtype
0, # bmCapabilities - XXX no call managment so far
itf_num + 1, # bDataInterface - interface 1
)
# CDC-PSTN table4 "Abstract Control"
# set to support line_coding and send_break
desc.pack(
"<BBBB",
4, # bFunctionLength
_CS_DESC_TYPE, # bDescriptorType
_CDC_FUNC_DESC_ABSTRACT_CONTROL, # bDescriptorSubtype
0x6, # bmCapabilities D1, D2
)
# CDC-PSTN "Union"
# set control interface / data interface number
desc.pack(
"<BBBBB",
5, # bFunctionLength
_CS_DESC_TYPE, # bDescriptorType
_CDC_FUNC_DESC_UNION, # bDescriptorSubtype
itf_num, # bControlInterface
itf_num + 1, # bSubordinateInterface0 (data class itf number)
)
# Single control IN endpoint (currently unused in this implementation)
self.ep_c_in = ep_num | _EP_IN_FLAG
desc.endpoint(self.ep_c_in, "interrupt", 8, 16)
# Now add the data interface
desc.interface(
itf_num + 1,
_CDC_DATA_EP_NUM,
_CDC_ITF_DATA_CLASS,
_CDC_ITF_DATA_SUBCLASS,
_CDC_ITF_DATA_PROT,
)
# Two data endpoints, bulk OUT and IN
self.ep_d_out = ep_num + 1
self.ep_d_in = (ep_num + 1) | _EP_IN_FLAG
desc.endpoint(self.ep_d_out, "bulk", _BULK_EP_LEN, 0)
desc.endpoint(self.ep_d_in, "bulk", _BULK_EP_LEN, 0)
def num_itfs(self):
return 2
def num_eps(self):
return 2 # total after masking out _EP_IN_FLAG
def on_open(self):
super().on_open()
# kick off any transfers that may have queued while the device was not open
self._rd_xfer()
self._wr_xfer()
def on_interface_control_xfer(self, stage, request):
# Handle class-specific interface control transfers
bmRequestType, bRequest, wValue, wIndex, wLength = struct.unpack("BBHHH", request)
recipient, req_type, req_dir = split_bmRequestType(bmRequestType)
if wIndex != self._c_itf:
return False # Only for the control interface (may be redundant check?)
if req_type != _REQ_TYPE_CLASS:
return False # Unsupported request type
if stage == _STAGE_SETUP:
if bRequest in (_SET_LINE_CODING_REQ, _GET_LINE_CODING_REQ):
return self._line_coding # Buffer to read or write
# Continue on other supported requests, stall otherwise
return bRequest in (_SET_CONTROL_LINE_STATE, _SEND_BREAK_REQ)
if stage == _STAGE_ACK:
if bRequest == _SET_LINE_CODING_REQ:
if self.line_coding_cb:
self.line_coding_cb(self._line_coding)
elif bRequest == _SET_CONTROL_LINE_STATE:
self._line_state = wValue
if self.line_state_cb:
self.line_state_cb(wValue)
elif bRequest == _SEND_BREAK_REQ:
if self.break_cb:
self.break_cb(wValue)
return True # allow DATA/ACK stages to complete normally
def _wr_xfer(self):
# Submit a new data IN transfer from the _wb buffer, if needed
if self.is_open() and not self.xfer_pending(self.ep_d_in) and self._wb.readable():
self.submit_xfer(self.ep_d_in, self._wb.pend_read(), self._wr_cb)
def _wr_cb(self, ep, res, num_bytes):
# Whenever a data IN transfer ends
if res == 0:
self._wb.finish_read(num_bytes)
self._wr_xfer()
def _rd_xfer(self):
# Keep an active data OUT transfer to read data from the host,
# whenever the receive buffer has room for new data
if self.is_open() and not self.xfer_pending(self.ep_d_out) and self._rb.writable():
# Can only submit up to the endpoint length per transaction, otherwise we won't
# get any transfer callback until the full transaction completes.
self.submit_xfer(self.ep_d_out, self._rb.pend_write(_BULK_EP_LEN), self._rd_cb)
def _rd_cb(self, ep, res, num_bytes):
# Whenever a data OUT transfer ends
if res == 0:
self._rb.finish_write(num_bytes)
self._rd_xfer()
###
### io.IOBase stream implementation
###
def write(self, buf):
# use a memoryview to track how much of 'buf' we've written so far
# (unfortunately, this means a 1 block allocation for each write, but it's otherwise allocation free.)
start = time.ticks_ms()
mv = memoryview(buf)
while True:
# Keep pushing buf into _wb into it's all gone
nbytes = self._wb.write(mv)
self._wr_xfer() # make sure a transfer is running from _wb
if nbytes == len(mv):
return len(buf) # Success
mv = mv[nbytes:]
# check for timeout
if time.ticks_diff(time.ticks_ms(), start) >= self._timeout:
return len(buf) - len(mv)
machine.idle()
def read(self, size):
start = time.ticks_ms()
# Allocate a suitable buffer to read into
if size >= 0:
b = bytearray(size)
else:
# for size == -1, return however many bytes are ready
b = bytearray(self._rb.readable())
n = self._readinto(b, start)
if not n:
return None
if n < len(b):
return b[:n]
return b
def readinto(self, b):
return self._readinto(b, time.ticks_ms())
def _readinto(self, b, start):
if len(b) == 0:
return 0
n = 0
m = memoryview(b)
while n < len(b):
# copy out of the read buffer if there is anything available
if self._rb.readable():
n += self._rb.readinto(m if n == 0 else m[n:])
self._rd_xfer() # if _rd was previously full, no transfer will be running
if n == len(b):
break # Done, exit before we call machine.idle()
if time.ticks_diff(time.ticks_ms(), start) >= self._timeout:
break # Timed out
machine.idle()
return n or None
def ioctl(self, req, arg):
if req == _MP_STREAM_POLL:
return (
(_MP_STREAM_POLL_WR if (arg & _MP_STREAM_POLL_WR) and self._wb.writable() else 0)
| (_MP_STREAM_POLL_RD if (arg & _MP_STREAM_POLL_RD) and self._rb.readable() else 0)
|
# using the USB level "open" (i.e. connected to host) for !HUP, not !DTR (port is open)
(_MP_STREAM_POLL_HUP if (arg & _MP_STREAM_POLL_HUP) and not self.is_open() else 0)
)
elif req == _MP_STREAM_FLUSH:
start = time.ticks_ms()
# Wait until write buffer contains no bytes for the lower TinyUSB layer to "read"
while self._wb.readable():
if not self.is_open():
return _MP_EINVAL
if time.ticks_diff(time.ticks_ms(), start) > self._timeout:
return _MP_ETIMEDOUT
machine.idle()
return 0
return _MP_EINVAL
def flush(self):
# a C implementation of this exists in stream.c, but it's not in io.IOBase
# and can't immediately be called from here (AFAIK)
r = self.ioctl(_MP_STREAM_FLUSH, 0)
if r:
raise OSError(r)

Wyświetl plik

@ -0,0 +1,3 @@
metadata(version="0.1.0")
require("usb-device")
package("usb")

Wyświetl plik

@ -0,0 +1,232 @@
# MicroPython USB hid module
#
# This implements a base HIDInterface class that can be used directly,
# or subclassed into more specific HID interface types.
#
# MIT license; Copyright (c) 2023 Angus Gratton
from micropython import const
import machine
import struct
import time
from .core import Interface, Descriptor, split_bmRequestType
_EP_IN_FLAG = const(1 << 7)
# Control transfer stages
_STAGE_IDLE = const(0)
_STAGE_SETUP = const(1)
_STAGE_DATA = const(2)
_STAGE_ACK = const(3)
# Request types
_REQ_TYPE_STANDARD = const(0x0)
_REQ_TYPE_CLASS = const(0x1)
_REQ_TYPE_VENDOR = const(0x2)
_REQ_TYPE_RESERVED = const(0x3)
# Descriptor types
_DESC_HID_TYPE = const(0x21)
_DESC_REPORT_TYPE = const(0x22)
_DESC_PHYSICAL_TYPE = const(0x23)
# Interface and protocol identifiers
_INTERFACE_CLASS = const(0x03)
_INTERFACE_SUBCLASS_NONE = const(0x00)
_INTERFACE_SUBCLASS_BOOT = const(0x01)
_INTERFACE_PROTOCOL_NONE = const(0x00)
_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
_INTERFACE_PROTOCOL_MOUSE = const(0x02)
# bRequest values for HID control requests
_REQ_CONTROL_GET_REPORT = const(0x01)
_REQ_CONTROL_GET_IDLE = const(0x02)
_REQ_CONTROL_GET_PROTOCOL = const(0x03)
_REQ_CONTROL_GET_DESCRIPTOR = const(0x06)
_REQ_CONTROL_SET_REPORT = const(0x09)
_REQ_CONTROL_SET_IDLE = const(0x0A)
_REQ_CONTROL_SET_PROTOCOL = const(0x0B)
# Standard descriptor lengths
_STD_DESC_INTERFACE_LEN = const(9)
_STD_DESC_ENDPOINT_LEN = const(7)
class HIDInterface(Interface):
# Abstract base class to implement a USB device HID interface in Python.
def __init__(
self,
report_descriptor,
extra_descriptors=[],
set_report_buf=None,
protocol=_INTERFACE_PROTOCOL_NONE,
interface_str=None,
):
# Construct a new HID interface.
#
# - report_descriptor is the only mandatory argument, which is the binary
# data consisting of the HID Report Descriptor. See Device Class
# Definition for Human Interface Devices (HID) v1.11 section 6.2.2 Report
# Descriptor, p23.
#
# - extra_descriptors is an optional argument holding additional HID
# descriptors, to append after the mandatory report descriptor. Most
# HID devices do not use these.
#
# - set_report_buf is an optional writable buffer object (i.e.
# bytearray), where SET_REPORT requests from the host can be
# written. Only necessary if the report_descriptor contains Output
# entries. If set, the size must be at least the size of the largest
# Output entry.
#
# - protocol can be set to a specific value as per HID v1.11 section 4.3 Protocols, p9.
#
# - interface_str is an optional string descriptor to associate with the HID USB interface.
super().__init__()
self.report_descriptor = report_descriptor
self.extra_descriptors = extra_descriptors
self._set_report_buf = set_report_buf
self.protocol = protocol
self.interface_str = interface_str
self._int_ep = None # set during enumeration
def get_report(self):
return False
def on_set_report(self, report_data, report_id, report_type):
# Override this function in order to handle SET REPORT requests from the host,
# where it sends data to the HID device.
#
# This function will only be called if the Report descriptor contains at least one Output entry,
# and the set_report_buf argument is provided to the constructor.
#
# Return True to complete the control transfer normally, False to abort it.
return True
def busy(self):
# Returns True if the interrupt endpoint is busy (i.e. existing transfer is pending)
return self.is_open() and self.xfer_pending(self._int_ep)
def send_report(self, report_data, timeout_ms=100):
# Helper function to send a HID report in the typical USB interrupt
# endpoint associated with a HID interface.
#
# Returns True if successful, False if HID device is not active or timeout
# is reached without being able to queue the report for sending.
deadline = time.ticks_add(time.ticks_ms(), timeout_ms)
while self.busy():
if time.ticks_diff(deadline, time.ticks_ms()) <= 0:
return False
machine.idle()
if not self.is_open():
return False
self.submit_xfer(self._int_ep, report_data)
def desc_cfg(self, desc, itf_num, ep_num, strs):
# Add the standard interface descriptor
desc.interface(
itf_num,
1,
_INTERFACE_CLASS,
_INTERFACE_SUBCLASS_NONE,
self.protocol,
len(strs) if self.interface_str else 0,
)
if self.interface_str:
strs.append(self.interface_str)
# As per HID v1.11 section 7.1 Standard Requests, return the contents of
# the standard HID descriptor before the associated endpoint descriptor.
self.get_hid_descriptor(desc)
# Add the typical single USB interrupt endpoint descriptor associated
# with a HID interface.
self._int_ep = ep_num | _EP_IN_FLAG
desc.endpoint(self._int_ep, "interrupt", 8, 8)
self.idle_rate = 0
self.protocol = 0
def num_eps(self):
return 1
def get_hid_descriptor(self, desc=None):
# Append a full USB HID descriptor from the object's report descriptor
# and optional additional descriptors.
#
# See HID Specification Version 1.1, Section 6.2.1 HID Descriptor p22
l = 9 + 3 * len(self.extra_descriptors) # total length
if desc is None:
desc = Descriptor(bytearray(l))
desc.pack(
"<BBHBBBH",
l, # bLength
_DESC_HID_TYPE, # bDescriptorType
0x111, # bcdHID
0, # bCountryCode
len(self.extra_descriptors) + 1, # bNumDescriptors
0x22, # bDescriptorType, Report
len(self.report_descriptor), # wDescriptorLength, Report
)
# Fill in any additional descriptor type/length pairs
#
# TODO: unclear if this functionality is ever used, may be easier to not
# support in base class
for dt, dd in self.extra_descriptors:
desc.pack("<BH", dt, len(dd))
return desc.b
def on_interface_control_xfer(self, stage, request):
# Handle standard and class-specific interface control transfers for HID devices.
bmRequestType, bRequest, wValue, _, wLength = struct.unpack("BBHHH", request)
recipient, req_type, _ = split_bmRequestType(bmRequestType)
if stage == _STAGE_SETUP:
if req_type == _REQ_TYPE_STANDARD:
# HID Spec p48: 7.1 Standard Requests
if bRequest == _REQ_CONTROL_GET_DESCRIPTOR:
desc_type = wValue >> 8
if desc_type == _DESC_HID_TYPE:
return self.get_hid_descriptor()
if desc_type == _DESC_REPORT_TYPE:
return self.report_descriptor
elif req_type == _REQ_TYPE_CLASS:
# HID Spec p50: 7.2 Class-Specific Requests
if bRequest == _REQ_CONTROL_GET_REPORT:
print("GET_REPORT?")
return False # Unsupported for now
if bRequest == _REQ_CONTROL_GET_IDLE:
return bytes([self.idle_rate])
if bRequest == _REQ_CONTROL_GET_PROTOCOL:
return bytes([self.protocol])
if bRequest in (_REQ_CONTROL_SET_IDLE, _REQ_CONTROL_SET_PROTOCOL):
return True
if bRequest == _REQ_CONTROL_SET_REPORT:
return self._set_report_buf # If None, request will stall
return False # Unsupported request
if stage == _STAGE_ACK:
if req_type == _REQ_TYPE_CLASS:
if bRequest == _REQ_CONTROL_SET_IDLE:
self.idle_rate = wValue >> 8
elif bRequest == _REQ_CONTROL_SET_PROTOCOL:
self.protocol = wValue
elif bRequest == _REQ_CONTROL_SET_REPORT:
report_id = wValue & 0xFF
report_type = wValue >> 8
report_data = self._set_report_buf
if wLength < len(report_data):
# need to truncate the response in the callback if we got less bytes
# than allowed for in the buffer
report_data = memoryview(self._set_report_buf)[:wLength]
self.on_set_report(report_data, report_id, report_type)
return True # allow DATA/ACK stages to complete normally

Wyświetl plik

@ -0,0 +1,3 @@
metadata(version="0.1.0")
require("usb-device-hid")
package("usb")

Wyświetl plik

@ -0,0 +1,233 @@
# MIT license; Copyright (c) 2023-2024 Angus Gratton
from micropython import const
import time
import usb.device
from usb.device.hid import HIDInterface
_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
_KEY_ARRAY_LEN = const(6) # Size of HID key array, must match report descriptor
_KEY_REPORT_LEN = const(_KEY_ARRAY_LEN + 2) # Modifier Byte + Reserved Byte + Array entries
class KeyboardInterface(HIDInterface):
# Synchronous USB keyboard HID interface
def __init__(self):
super().__init__(
_KEYBOARD_REPORT_DESC,
set_report_buf=bytearray(1),
protocol=_INTERFACE_PROTOCOL_KEYBOARD,
interface_str="MicroPython Keyboard",
)
self._key_reports = [
bytearray(_KEY_REPORT_LEN),
bytearray(_KEY_REPORT_LEN),
] # Ping/pong report buffers
self.numlock = False
def on_set_report(self, report_data, _report_id, _report_type):
self.on_led_update(report_data[0])
def on_led_update(self, led_mask):
# Override to handle keyboard LED updates. led_mask is bitwise ORed
# together values as defined in LEDCode.
pass
def send_keys(self, down_keys, timeout_ms=100):
# Update the state of the keyboard by sending a report with down_keys
# set, where down_keys is an iterable (list or similar) of integer
# values such as the values defined in KeyCode.
#
# Will block for up to timeout_ms if a previous report is still
# pending to be sent to the host. Returns True on success.
r, s = self._key_reports # next report buffer to send, spare report buffer
r[0] = 0 # modifier byte
i = 2 # index for next key array item to write to
for k in down_keys:
if k < 0: # Modifier key
r[0] |= -k
elif i < _KEY_REPORT_LEN:
r[i] = k
i += 1
else: # Excess rollover! Can't report
r[0] = 0
for i in range(2, _KEY_REPORT_LEN):
r[i] = 0xFF
break
while i < _KEY_REPORT_LEN:
r[i] = 0
i += 1
if self.send_report(r, timeout_ms):
# Swap buffers if the previous one is newly queued to send, so
# any subsequent call can't modify that buffer mid-send
self._key_reports[0] = s
self._key_reports[1] = r
return True
return False
# HID keyboard report descriptor
#
# From p69 of http://www.usb.org/developers/devclass_docs/HID1_11.pdf
#
# fmt: off
_KEYBOARD_REPORT_DESC = (
b'\x05\x01' # Usage Page (Generic Desktop),
b'\x09\x06' # Usage (Keyboard),
b'\xA1\x01' # Collection (Application),
b'\x05\x07' # Usage Page (Key Codes);
b'\x19\xE0' # Usage Minimum (224),
b'\x29\xE7' # Usage Maximum (231),
b'\x15\x00' # Logical Minimum (0),
b'\x25\x01' # Logical Maximum (1),
b'\x75\x01' # Report Size (1),
b'\x95\x08' # Report Count (8),
b'\x81\x02' # Input (Data, Variable, Absolute), ;Modifier byte
b'\x95\x01' # Report Count (1),
b'\x75\x08' # Report Size (8),
b'\x81\x01' # Input (Constant), ;Reserved byte
b'\x95\x05' # Report Count (5),
b'\x75\x01' # Report Size (1),
b'\x05\x08' # Usage Page (Page# for LEDs),
b'\x19\x01' # Usage Minimum (1),
b'\x29\x05' # Usage Maximum (5),
b'\x91\x02' # Output (Data, Variable, Absolute), ;LED report
b'\x95\x01' # Report Count (1),
b'\x75\x03' # Report Size (3),
b'\x91\x01' # Output (Constant), ;LED report padding
b'\x95\x06' # Report Count (6),
b'\x75\x08' # Report Size (8),
b'\x15\x00' # Logical Minimum (0),
b'\x25\x65' # Logical Maximum(101),
b'\x05\x07' # Usage Page (Key Codes),
b'\x19\x00' # Usage Minimum (0),
b'\x29\x65' # Usage Maximum (101),
b'\x81\x00' # Input (Data, Array), ;Key arrays (6 bytes)
b'\xC0' # End Collection
)
# fmt: on
# Standard HID keycodes, as a pseudo-enum class for easy access
#
# Modifier keys are encoded as negative values
class KeyCode:
A = 4
B = 5
C = 6
D = 7
E = 8
F = 9
G = 10
H = 11
I = 12
J = 13
K = 14
L = 15
M = 16
N = 17
O = 18
P = 19
Q = 20
R = 21
S = 22
T = 23
U = 24
V = 25
W = 26
X = 27
Y = 28
Z = 29
N1 = 30 # Standard number row keys
N2 = 31
N3 = 32
N4 = 33
N5 = 34
N6 = 35
N7 = 36
N8 = 37
N9 = 38
N0 = 39
ENTER = 40
ESCAPE = 41
BACKSPACE = 42
TAB = 43
SPACE = 44
MINUS = 45 # - _
EQUAL = 46 # = +
OPEN_BRACKET = 47 # [ {
CLOSE_BRACKET = 48 # ] }
BACKSLASH = 49 # \ |
HASH = 50 # # ~
COLON = 51 # ; :
QUOTE = 52 # ' "
TILDE = 53 # ` ~
COMMA = 54 # , <
DOT = 55 # . >
SLASH = 56 # / ?
CAPS_LOCK = 57
F1 = 58
F2 = 59
F3 = 60
F4 = 61
F5 = 62
F6 = 63
F7 = 64
F8 = 65
F9 = 66
F10 = 67
F11 = 68
F12 = 69
PRINTSCREEN = 70
SCROLL_LOCK = 71
PAUSE = 72
INSERT = 73
HOME = 74
PAGEUP = 75
DELETE = 76
END = 77
PAGEDOWN = 78
RIGHT = 79 # Arrow keys
LEFT = 80
DOWN = 81
UP = 82
KP_NUM_LOCK = 83
KP_DIVIDE = 84
KP_AT = 85
KP_MULTIPLY = 85
KP_MINUS = 86
KP_PLUS = 87
KP_ENTER = 88
KP_1 = 89
KP_2 = 90
KP_3 = 91
KP_4 = 92
KP_5 = 93
KP_6 = 94
KP_7 = 95
KP_8 = 96
KP_9 = 97
KP_0 = 98
# HID modifier values (negated to allow them to be passed along with the normal keys)
LEFT_CTRL = -0x01
LEFT_SHIFT = -0x02
LEFT_ALT = -0x04
LEFT_UI = -0x08
RIGHT_CTRL = -0x10
RIGHT_SHIFT = -0x20
RIGHT_ALT = -0x40
RIGHT_UI = -0x80
# HID LED values
class LEDCode:
NUM_LOCK = 0x01
CAPS_LOCK = 0x02
SCROLL_LOCK = 0x04
COMPOSE = 0x08
KANA = 0x10

Wyświetl plik

@ -0,0 +1,3 @@
metadata(version="0.1.0")
require("usb-device")
package("usb")

Wyświetl plik

@ -0,0 +1,306 @@
# MicroPython USB MIDI module
# MIT license; Copyright (c) 2023 Paul Hamshere, 2023-2024 Angus Gratton
from micropython import const, schedule
import struct
from .core import Interface, Buffer
_EP_IN_FLAG = const(1 << 7)
_INTERFACE_CLASS_AUDIO = const(0x01)
_INTERFACE_SUBCLASS_AUDIO_CONTROL = const(0x01)
_INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING = const(0x03)
# Audio subclass extends the standard endpoint descriptor
# with two extra bytes
_STD_DESC_AUDIO_ENDPOINT_LEN = const(9)
_CLASS_DESC_ENDPOINT_LEN = const(5)
_STD_DESC_ENDPOINT_TYPE = const(0x5)
_JACK_TYPE_EMBEDDED = const(0x01)
_JACK_TYPE_EXTERNAL = const(0x02)
_JACK_IN_DESC_LEN = const(6)
_JACK_OUT_DESC_LEN = const(9)
# MIDI Status bytes. For Channel messages these are only the upper 4 bits, ORed with the channel number.
# As per https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message
_MIDI_NOTE_OFF = const(0x80)
_MIDI_NOTE_ON = const(0x90)
_MIDI_POLY_KEYPRESS = const(0xA0)
_MIDI_CONTROL_CHANGE = const(0xB0)
# USB-MIDI CINs (Code Index Numbers), as per USB MIDI Table 4-1
_CIN_SYS_COMMON_2BYTE = const(0x2)
_CIN_SYS_COMMON_3BYTE = const(0x3)
_CIN_SYSEX_START = const(0x4)
_CIN_SYSEX_END_1BYTE = const(0x5)
_CIN_SYSEX_END_2BYTE = const(0x6)
_CIN_SYSEX_END_3BYTE = const(0x7)
_CIN_NOTE_OFF = const(0x8)
_CIN_NOTE_ON = const(0x9)
_CIN_POLY_KEYPRESS = const(0xA)
_CIN_CONTROL_CHANGE = const(0xB)
_CIN_PROGRAM_CHANGE = const(0xC)
_CIN_CHANNEL_PRESSURE = const(0xD)
_CIN_PITCH_BEND = const(0xE)
_CIN_SINGLE_BYTE = const(0xF) # Not currently supported
# Jack IDs for a simple bidrectional MIDI device(!)
_EMB_IN_JACK_ID = const(1)
_EXT_IN_JACK_ID = const(2)
_EMB_OUT_JACK_ID = const(3)
_EXT_OUT_JACK_ID = const(4)
# Data flows, as modelled by USB-MIDI and this hypothetical interface, are as follows:
# Device RX = USB OUT EP => _EMB_IN_JACK => _EMB_OUT_JACK
# Device TX = _EXT_IN_JACK => _EMB_OUT_JACK => USB IN EP
class MIDIInterface(Interface):
# Base class to implement a USB MIDI device in Python.
#
# To be compliant this also regisers a dummy USB Audio interface, but that
# interface isn't otherwise used.
def __init__(self, rxlen=16, txlen=16):
# Arguments are size of transmit and receive buffers in bytes.
super().__init__()
self.ep_out = None # Set during enumeration. RX direction (host to device)
self.ep_in = None # TX direction (device to host)
self._rx = Buffer(rxlen)
self._tx = Buffer(txlen)
# Callbacks for handling received MIDI messages.
#
# Subclasses can choose between overriding on_midi_event
# and handling all MIDI events manually, or overriding the
# functions for note on/off and control change, only.
def on_midi_event(self, cin, midi0, midi1, midi2):
ch = midi0 & 0x0F
if cin == _CIN_NOTE_ON:
self.on_note_on(ch, midi1, midi2)
elif cin == _CIN_NOTE_OFF:
self.on_note_off(ch, midi1, midi2)
elif cin == _CIN_CONTROL_CHANGE:
self.on_control_change(ch, midi1, midi2)
def on_note_on(self, channel, pitch, vel):
pass # Override to handle Note On messages
def on_note_off(self, channel, pitch, vel):
pass # Override to handle Note On messages
def on_control_change(self, channel, controller, value):
pass # Override to handle Control Change messages
# Helper functions for sending common MIDI messages
def note_on(self, channel, pitch, vel=0x40):
self.send_event(_CIN_NOTE_ON, _MIDI_NOTE_ON | channel, pitch, vel)
def note_off(self, channel, pitch, vel=0x40):
self.send_event(_CIN_NOTE_OFF, _MIDI_NOTE_OFF | channel, pitch, vel)
def control_change(self, channel, controller, value):
self.send_event(_CIN_CONTROL_CHANGE, _MIDI_CONTROL_CHANGE | channel, controller, value)
def send_event(self, cin, midi0, midi1=0, midi2=0):
# Queue a MIDI Event Packet to send to the host.
#
# CIN = USB-MIDI Code Index Number, see USB MIDI 1.0 section 4 "USB-MIDI Event Packets"
#
# Remaining arguments are 0-3 MIDI data bytes.
#
# Note this function returns when the MIDI Event Packet has been queued,
# not when it's been received by the host.
#
# Returns False if the TX buffer is full and the MIDI Event could not be queued.
w = self._tx.pend_write()
if len(w) < 4:
return False # TX buffer is full. TODO: block here?
w[0] = cin # leave cable number as 0?
w[1] = midi0
w[2] = midi1
w[3] = midi2
self._tx.finish_write(4)
self._tx_xfer()
return True
def _tx_xfer(self):
# Keep an active IN transfer to send data to the host, whenever
# there is data to send.
if self.is_open() and not self.xfer_pending(self.ep_in) and self._tx.readable():
self.submit_xfer(self.ep_in, self._tx.pend_read(), self._tx_cb)
def _tx_cb(self, ep, res, num_bytes):
if res == 0:
self._tx.finish_read(num_bytes)
self._tx_xfer()
def _rx_xfer(self):
# Keep an active OUT transfer to receive MIDI events from the host
if self.is_open() and not self.xfer_pending(self.ep_out) and self._rx.writable():
self.submit_xfer(self.ep_out, self._rx.pend_write(), self._rx_cb)
def _rx_cb(self, ep, res, num_bytes):
if res == 0:
self._rx.finish_write(num_bytes)
schedule(self._on_rx, None)
self._rx_xfer()
def on_open(self):
super().on_open()
# kick off any transfers that may have queued while the device was not open
self._tx_xfer()
self._rx_xfer()
def _on_rx(self, _):
# Receive MIDI events. Called via micropython.schedule, outside of the USB callback function.
m = self._rx.pend_read()
i = 0
while i <= len(m) - 4:
cin = m[i] & 0x0F
self.on_midi_event(cin, m[i + 1], m[i + 2], m[i + 3])
i += 4
self._rx.finish_read(i)
def desc_cfg(self, desc, itf_num, ep_num, strs):
# Start by registering a USB Audio Control interface, that is required to point to the
# actual MIDI interface
desc.interface(itf_num, 0, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_CONTROL)
# Append the class-specific AudioControl interface descriptor
desc.pack(
"<BBBHHBB",
9, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
0x0009, # wTotalLength
0x01, # bInCollection,
itf_num + 1, # baInterfaceNr - points to the MIDI Streaming interface
)
# Next add the MIDI Streaming interface descriptor
desc.interface(
itf_num + 1, 2, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_MIDISTREAMING
)
# Append the class-specific interface descriptors
# Midi Streaming interface descriptor
desc.pack(
"<BBBHH",
7, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
# wTotalLength: of all class-specific descriptors
7
+ 2
* (
_JACK_IN_DESC_LEN
+ _JACK_OUT_DESC_LEN
+ _STD_DESC_AUDIO_ENDPOINT_LEN
+ _CLASS_DESC_ENDPOINT_LEN
),
)
# The USB MIDI standard 1.0 allows modelling a baffling range of MIDI
# devices with different permutations of Jack descriptors, with a lot of
# scope for indicating internal connections in the device (as
# "virtualised" by the USB MIDI standard). Much of the options don't
# really change the USB behaviour but provide metadata to the host.
#
# As observed elsewhere online, the standard ends up being pretty
# complex and unclear in parts, but there is a clear simple example in
# an Appendix. So nearly everyone implements the device from the
# Appendix as-is, even when it's not a good fit for their application,
# and ignores the rest of the standard.
#
# For now, this is what this class does as well.
_jack_in_desc(desc, _JACK_TYPE_EMBEDDED, _EMB_IN_JACK_ID)
_jack_in_desc(desc, _JACK_TYPE_EXTERNAL, _EXT_IN_JACK_ID)
_jack_out_desc(desc, _JACK_TYPE_EMBEDDED, _EMB_OUT_JACK_ID, _EXT_IN_JACK_ID, 1)
_jack_out_desc(desc, _JACK_TYPE_EXTERNAL, _EXT_OUT_JACK_ID, _EMB_IN_JACK_ID, 1)
# One MIDI endpoint in each direction, plus the
# associated CS descriptors
self.ep_out = ep_num
self.ep_in = ep_num | _EP_IN_FLAG
# rx side, USB "in" endpoint and embedded MIDI IN Jacks
_audio_endpoint(desc, self.ep_in, _EMB_OUT_JACK_ID)
# tx side, USB "out" endpoint and embedded MIDI OUT jacks
_audio_endpoint(desc, self.ep_out, _EMB_IN_JACK_ID)
def num_itfs(self):
return 2
def num_eps(self):
return 1
def _jack_in_desc(desc, bJackType, bJackID):
# Utility function appends a "JACK IN" descriptor with
# specified bJackType and bJackID
desc.pack(
"<BBBBBB",
_JACK_IN_DESC_LEN, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x02, # bDescriptorSubtype MIDI_IN_JACK
bJackType,
bJackID,
0x00, # iJack, no string descriptor support yet
)
def _jack_out_desc(desc, bJackType, bJackID, bSourceId, bSourcePin):
# Utility function appends a "JACK IN" descriptor with
# specified bJackType and bJackID
desc.pack(
"<BBBBBBBBB",
_JACK_OUT_DESC_LEN, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x03, # bDescriptorSubtype MIDI_OUT_JACK
bJackType,
bJackID,
0x01, # bNrInputPins
bSourceId, # baSourceID(1)
bSourcePin, # baSourcePin(1)
0x00, # iJack, no string descriptor support yet
)
def _audio_endpoint(desc, bEndpointAddress, emb_jack_id):
# Append a standard USB endpoint descriptor and the USB class endpoint descriptor
# for this endpoint.
#
# Audio Class devices extend the standard endpoint descriptor with two extra bytes,
# so we can't easily call desc.endpoint() for the first part.
desc.pack(
# Standard USB endpoint descriptor (plus audio tweaks)
"<BBBBHBBB"
# Class endpoint descriptor
"BBBBB",
_STD_DESC_AUDIO_ENDPOINT_LEN, # wLength
_STD_DESC_ENDPOINT_TYPE, # bDescriptorType
bEndpointAddress,
2, # bmAttributes, bulk
64, # wMaxPacketSize
0, # bInterval
0, # bRefresh (unused)
0, # bSynchInterval (unused)
_CLASS_DESC_ENDPOINT_LEN, # bLength
0x25, # bDescriptorType CS_ENDPOINT
0x01, # bDescriptorSubtype MS_GENERAL
1, # bNumEmbMIDIJack
emb_jack_id, # BaAssocJackID(1)
)

Wyświetl plik

@ -0,0 +1,3 @@
metadata(version="0.1.0")
require("usb-device-hid")
package("usb")

Wyświetl plik

@ -0,0 +1,100 @@
# MicroPython USB Mouse module
#
# MIT license; Copyright (c) 2023-2024 Angus Gratton
from micropython import const
import struct
import machine
from usb.device.hid import HIDInterface
_INTERFACE_PROTOCOL_MOUSE = const(0x02)
class MouseInterface(HIDInterface):
# A basic three button USB mouse HID interface
def __init__(self, interface_str="MicroPython Mouse"):
super().__init__(
_MOUSE_REPORT_DESC,
protocol=_INTERFACE_PROTOCOL_MOUSE,
interface_str=interface_str,
)
self._l = False # Left button
self._m = False # Middle button
self._r = False # Right button
self._buf = bytearray(3)
def send_report(self, dx=0, dy=0):
b = 0
if self._l:
b |= 1 << 0
if self._r:
b |= 1 << 1
if self._m:
b |= 1 << 2
# Wait for any pending report to be sent to the host
# before updating contents of _buf.
#
# This loop can be removed if you don't care about possibly missing a
# transient report, the final report buffer contents will always be the
# last one sent to the host (it just might lose one of the ones in the
# middle).
while self.busy():
machine.idle()
struct.pack_into("Bbb", self._buf, 0, b, dx, dy)
return super().send_report(self._buf)
def click_left(self, down=True):
self._l = down
return self.send_report()
def click_middle(self, down=True):
self._m = down
return self.send_report()
def click_right(self, down=True):
self._r = down
return self.send_report()
def move_by(self, dx, dy):
if not -127 <= dx <= 127:
raise ValueError("dx")
if not -127 <= dy <= 127:
raise ValueError("dy")
return self.send_report(dx, dy)
# Basic 3-button mouse HID Report Descriptor.
# This is based on Appendix E.10 of the HID v1.11 document.
# fmt: off
_MOUSE_REPORT_DESC = (
b'\x05\x01' # Usage Page (Generic Desktop)
b'\x09\x02' # Usage (Mouse)
b'\xA1\x01' # Collection (Application)
b'\x09\x01' # Usage (Pointer)
b'\xA1\x00' # Collection (Physical)
b'\x05\x09' # Usage Page (Buttons)
b'\x19\x01' # Usage Minimum (01),
b'\x29\x03' # Usage Maximun (03),
b'\x15\x00' # Logical Minimum (0),
b'\x25\x01' # Logical Maximum (1),
b'\x95\x03' # Report Count (3),
b'\x75\x01' # Report Size (1),
b'\x81\x02' # Input (Data, Variable, Absolute), ;3 button bits
b'\x95\x01' # Report Count (1),
b'\x75\x05' # Report Size (5),
b'\x81\x01' # Input (Constant), ;5 bit padding
b'\x05\x01' # Usage Page (Generic Desktop),
b'\x09\x30' # Usage (X),
b'\x09\x31' # Usage (Y),
b'\x15\x81' # Logical Minimum (-127),
b'\x25\x7F' # Logical Maximum (127),
b'\x75\x08' # Report Size (8),
b'\x95\x02' # Report Count (2),
b'\x81\x06' # Input (Data, Variable, Relative), ;2 position bytes (X & Y)
b'\xC0' # End Collection
b'\xC0' # End Collection
)
# fmt: on

Wyświetl plik

@ -1,3 +1,2 @@
metadata(version="0.1.0")
package("json")
package("usb")

Wyświetl plik

@ -0,0 +1,97 @@
# Tests for the Buffer class included in usb.device.core
#
# The easiest way to run this is using unix port. From the parent usb-device
# directory, run as:
#
# $ micropython -m tests.test_core_buffer
#
import micropython
from usb.device import core
if not hasattr(core.machine, "disable_irq"):
# Inject a fake machine module which allows testing on the unix port, and as
# a bonus have tests fail if the buffer allocates inside a critical section.
class FakeMachine:
def disable_irq(self):
return micropython.heap_lock()
def enable_irq(self, was_locked):
if not was_locked:
micropython.heap_unlock()
core.machine = FakeMachine()
b = core.Buffer(16)
# Check buffer is empty
assert b.readable() == 0
assert b.writable() == 16
# Single write then read
w = b.pend_write()
assert len(w) == 16
w[:8] = b"12345678"
b.finish_write(8)
# Empty again
assert b.readable() == 8
assert b.writable() == 8
r = b.pend_read()
assert len(r) == 8
assert r == b"12345678"
b.finish_read(8)
# Empty buffer again
assert b.readable() == 0
assert b.writable() == 16
# Single write then split reads
b.write(b"abcdefghijklmnop")
assert b.writable() == 0 # full buffer
r = b.pend_read()
assert r == b"abcdefghijklmnop"
b.finish_read(2)
r = b.pend_read()
assert r == b"cdefghijklmnop"
b.finish_read(3)
# write to end of buffer
b.write(b"AB")
r = b.pend_read()
assert r == b"fghijklmnopAB"
# write while a read is pending
b.write(b"XY")
# previous pend_read() memoryview should be the same
assert r == b"fghijklmnopAB"
b.finish_read(4)
r = b.pend_read()
assert r == b"jklmnopABXY" # four bytes consumed from head, one new byte at tail
# read while a write is pending
w = b.pend_write()
assert len(w) == 5
r = b.pend_read()
assert len(r) == 11
b.finish_read(3)
w[:2] = b"12"
b.finish_write(2)
# Expected final state of buffer
tmp = bytearray(b.readable())
assert b.readinto(tmp) == len(tmp)
assert tmp == b"mnopABXY12"
# Now buffer is empty again
assert b.readable() == 0
assert b.readinto(tmp) == 0
assert b.writable() == 16
print("All Buffer tests passed")

Wyświetl plik

@ -0,0 +1,2 @@
from . import core
from .core import get # Singleton _Device getter

Wyświetl plik

@ -0,0 +1,851 @@
# MicroPython Library runtime USB device implementation
#
# These contain the classes and utilities that are needed to
# implement a USB device, not any complete USB drivers.
#
# MIT license; Copyright (c) 2022-2024 Angus Gratton
from micropython import const
import machine
import struct
_EP_IN_FLAG = const(1 << 7)
# USB descriptor types
_STD_DESC_DEV_TYPE = const(0x1)
_STD_DESC_CONFIG_TYPE = const(0x2)
_STD_DESC_STRING_TYPE = const(0x3)
_STD_DESC_INTERFACE_TYPE = const(0x4)
_STD_DESC_ENDPOINT_TYPE = const(0x5)
_STD_DESC_INTERFACE_ASSOC = const(0xB)
_ITF_ASSOCIATION_DESC_TYPE = const(0xB) # Interface Association descriptor
# Standard USB descriptor lengths
_STD_DESC_CONFIG_LEN = const(9)
_STD_DESC_ENDPOINT_LEN = const(7)
_STD_DESC_INTERFACE_LEN = const(9)
_DESC_OFFSET_LEN = const(0)
_DESC_OFFSET_TYPE = const(1)
_DESC_OFFSET_INTERFACE_NUM = const(2) # for _STD_DESC_INTERFACE_TYPE
_DESC_OFFSET_ENDPOINT_NUM = const(2) # for _STD_DESC_ENDPOINT_TYPE
# Standard control request bmRequest fields, can extract by calling split_bmRequestType()
_REQ_RECIPIENT_DEVICE = const(0x0)
_REQ_RECIPIENT_INTERFACE = const(0x1)
_REQ_RECIPIENT_ENDPOINT = const(0x2)
_REQ_RECIPIENT_OTHER = const(0x3)
# Offsets into the standard configuration descriptor, to fixup
_OFFS_CONFIG_iConfiguration = const(6)
_INTERFACE_CLASS_VENDOR = const(0xFF)
_INTERFACE_SUBCLASS_NONE = const(0x00)
_PROTOCOL_NONE = const(0x00)
# These need to match the constants in tusb_config.h
_USB_STR_MANUF = const(0x01)
_USB_STR_PRODUCT = const(0x02)
_USB_STR_SERIAL = const(0x03)
# Error constant to match mperrno.h
_MP_EINVAL = const(22)
_dev = None # Singleton _Device instance
def get():
# Getter to access the singleton instance of the
# MicroPython _Device object
#
# (note this isn't the low-level machine.USBDevice object, the low-level object is
# get()._usbd.)
global _dev
if not _dev:
_dev = _Device()
return _dev
class _Device:
# Class that implements the Python parts of the MicroPython USBDevice.
#
# This class should only be instantiated by the singleton getter
# function usb.device.get(), never directly.
def __init__(self):
self._itfs = {} # Mapping from interface number to interface object, set by init()
self._eps = {} # Mapping from endpoint address to interface object, set by _open_cb()
self._ep_cbs = {} # Mapping from endpoint address to Optional[xfer callback]
self._usbd = machine.USBDevice() # low-level API
def init(self, *itfs, **kwargs):
# Helper function to configure the USB device and activate it in a single call
self.active(False)
self.config(*itfs, **kwargs)
self.active(True)
def config( # noqa: PLR0913
self,
*itfs,
builtin_driver=False,
manufacturer_str=None,
product_str=None,
serial_str=None,
configuration_str=None,
id_vendor=None,
id_product=None,
bcd_device=None,
device_class=0,
device_subclass=0,
device_protocol=0,
config_str=None,
max_power_ma=None,
):
# Configure the USB device with a set of interfaces, and optionally reconfiguring the
# device and configuration descriptor fields
_usbd = self._usbd
if self.active():
raise OSError(_MP_EINVAL) # Must set active(False) first
# Convenience: Allow builtin_driver to be True, False or one of
# the machine.USBDevice.BUILTIN_ constants
if isinstance(builtin_driver, bool):
builtin_driver = _usbd.BUILTIN_DEFAULT if builtin_driver else _usbd.BUILTIN_NONE
_usbd.builtin_driver = builtin_driver
# Putting None for any strings that should fall back to the "built-in" value
# Indexes in this list depends on _USB_STR_MANUF, _USB_STR_PRODUCT, _USB_STR_SERIAL
strs = [None, manufacturer_str, product_str, serial_str]
# Build the device descriptor
FMT = "<BBHBBBBHHHBBBB"
# read the static descriptor fields
f = struct.unpack(FMT, builtin_driver.desc_dev)
def maybe_set(value, idx):
# Override a numeric descriptor value or keep builtin value f[idx] if 'value' is None
if value is not None:
return value
return f[idx]
# Either copy each descriptor field directly from the builtin device descriptor, or 'maybe'
# set it to the custom value from the object
desc_dev = struct.pack(
FMT,
f[0], # bLength
f[1], # bDescriptorType
f[2], # bcdUSB
device_class, # bDeviceClass
device_subclass, # bDeviceSubClass
device_protocol, # bDeviceProtocol
f[6], # bMaxPacketSize0, TODO: allow overriding this value?
maybe_set(id_vendor, 7), # idVendor
maybe_set(id_product, 8), # idProduct
maybe_set(bcd_device, 9), # bcdDevice
_USB_STR_MANUF, # iManufacturer
_USB_STR_PRODUCT, # iProduct
_USB_STR_SERIAL, # iSerialNumber
1,
) # bNumConfigurations
# Iterate interfaces to build the configuration descriptor
# Keep track of the interface and endpoint indexes
itf_num = builtin_driver.itf_max
ep_num = max(builtin_driver.ep_max, 1) # Endpoint 0 always reserved for control
while len(strs) < builtin_driver.str_max:
strs.append(None) # Reserve other string indexes used by builtin drivers
initial_cfg = builtin_driver.desc_cfg or (b"\x00" * _STD_DESC_CONFIG_LEN)
self._itfs = {}
# Determine the total length of the configuration descriptor, by making dummy
# calls to build the config descriptor
desc = Descriptor(None)
desc.extend(initial_cfg)
for itf in itfs:
itf.desc_cfg(desc, 0, 0, [])
# Allocate the real Descriptor helper to write into it, starting
# after the standard configuration descriptor
desc = Descriptor(bytearray(desc.o))
desc.extend(initial_cfg)
for itf in itfs:
itf.desc_cfg(desc, itf_num, ep_num, strs)
for _ in range(itf.num_itfs()):
self._itfs[itf_num] = itf # Mapping from interface numbers to interfaces
itf_num += 1
ep_num += itf.num_eps()
# Go back and update the Standard Configuration Descriptor
# header at the start with values based on the complete
# descriptor.
#
# See USB 2.0 specification section 9.6.3 p264 for details.
bmAttributes = (
(1 << 7) # Reserved
| (0 if max_power_ma else (1 << 6)) # Self-Powered
# Remote Wakeup not currently supported
)
# Configuration string is optional but supported
iConfiguration = 0
if configuration_str:
iConfiguration = len(strs)
strs.append(configuration_str)
if max_power_ma is not None:
# Convert from mA to the units used in the descriptor
max_power_ma //= 2
else:
try:
# Default to whatever value the builtin driver reports
max_power_ma = _usbd.BUILTIN_DEFAULT.desc_cfg[8]
except IndexError:
# If no built-in driver, default to 250mA
max_power_ma = 125
desc.pack_into(
"<BBHBBBBB",
0,
_STD_DESC_CONFIG_LEN, # bLength
_STD_DESC_CONFIG_TYPE, # bDescriptorType
len(desc.b), # wTotalLength
itf_num,
1, # bConfigurationValue
iConfiguration,
bmAttributes,
max_power_ma,
)
_usbd.config(
desc_dev,
desc.b,
strs,
self._open_itf_cb,
self._reset_cb,
self._control_xfer_cb,
self._xfer_cb,
)
def active(self, *optional_value):
# Thin wrapper around the USBDevice active() function.
#
# Note: active only means the USB device is available, not that it has
# actually been connected to and configured by a USB host. Use the
# Interface.is_open() function to check if the host has configured an
# interface of the device.
return self._usbd.active(*optional_value)
def _open_itf_cb(self, desc):
# Singleton callback from TinyUSB custom class driver, when USB host does
# Set Configuration. Called once per interface or IAD.
# Note that even if the configuration descriptor contains an IAD, 'desc'
# starts from the first interface descriptor in the IAD and not the IAD
# descriptor.
itf_num = desc[_DESC_OFFSET_INTERFACE_NUM]
itf = self._itfs[itf_num]
# Scan the full descriptor:
# - Build _eps and _ep_addr from the endpoint descriptors
# - Find the highest numbered interface provided to the callback
# (which will be the first interface, unless we're scanning
# multiple interfaces inside an IAD.)
offs = 0
max_itf = itf_num
while offs < len(desc):
dl = desc[offs + _DESC_OFFSET_LEN]
dt = desc[offs + _DESC_OFFSET_TYPE]
if dt == _STD_DESC_ENDPOINT_TYPE:
ep_addr = desc[offs + _DESC_OFFSET_ENDPOINT_NUM]
self._eps[ep_addr] = itf
self._ep_cbs[ep_addr] = None
elif dt == _STD_DESC_INTERFACE_TYPE:
max_itf = max(max_itf, desc[offs + _DESC_OFFSET_INTERFACE_NUM])
offs += dl
# If 'desc' is not the inside of an Interface Association Descriptor but
# 'itf' object still represents multiple USB interfaces (i.e. MIDI),
# defer calling 'itf.on_open()' until this callback fires for the
# highest numbered USB interface.
#
# This means on_open() is only called once, and that it can
# safely submit transfers on any of the USB interfaces' endpoints.
if self._itfs.get(max_itf + 1, None) != itf:
itf.on_open()
def _reset_cb(self):
# Callback when the USB device is reset by the host
# Allow interfaces to respond to the reset
for itf in self._itfs.values():
itf.on_reset()
# Rebuilt when host re-enumerates
self._eps = {}
self._ep_cbs = {}
def _submit_xfer(self, ep_addr, data, done_cb=None):
# Singleton function to submit a USB transfer (of any type except control).
#
# Generally, drivers should call Interface.submit_xfer() instead. See
# that function for documentation about the possible parameter values.
if ep_addr not in self._eps:
raise ValueError("ep_addr")
if self._ep_cbs[ep_addr]:
raise RuntimeError("xfer_pending")
# USBDevice callback may be called immediately, before Python execution
# continues, so set it first.
#
# To allow xfer_pending checks to work, store True instead of None.
self._ep_cbs[ep_addr] = done_cb or True
return self._usbd.submit_xfer(ep_addr, data)
def _xfer_cb(self, ep_addr, result, xferred_bytes):
# Singleton callback from TinyUSB custom class driver when a transfer completes.
cb = self._ep_cbs.get(ep_addr, None)
self._ep_cbs[ep_addr] = None
if callable(cb):
cb(ep_addr, result, xferred_bytes)
def _control_xfer_cb(self, stage, request):
# Singleton callback from TinyUSB custom class driver when a control
# transfer is in progress.
#
# stage determines appropriate responses (possible values
# utils.STAGE_SETUP, utils.STAGE_DATA, utils.STAGE_ACK).
#
# The TinyUSB class driver framework only calls this function for
# particular types of control transfer, other standard control transfers
# are handled by TinyUSB itself.
wIndex = request[4] + (request[5] << 8)
recipient, _, _ = split_bmRequestType(request[0])
itf = None
result = None
if recipient == _REQ_RECIPIENT_DEVICE:
itf = self._itfs.get(wIndex & 0xFFFF, None)
if itf:
result = itf.on_device_control_xfer(stage, request)
elif recipient == _REQ_RECIPIENT_INTERFACE:
itf = self._itfs.get(wIndex & 0xFFFF, None)
if itf:
result = itf.on_interface_control_xfer(stage, request)
elif recipient == _REQ_RECIPIENT_ENDPOINT:
ep_num = wIndex & 0xFFFF
itf = self._eps.get(ep_num, None)
if itf:
result = itf.on_endpoint_control_xfer(stage, request)
if not itf:
# At time this code was written, only the control transfers shown
# above are passed to the class driver callback. See
# invoke_class_control() in tinyusb usbd.c
raise RuntimeError(f"Unexpected control request type {request[0]:#x}")
# Expecting any of the following possible replies from
# on_NNN_control_xfer():
#
# True - Continue transfer, no data
# False - STALL transfer
# Object with buffer interface - submit this data for the control transfer
return result
class Interface:
# Abstract base class to implement USB Interface (and associated endpoints),
# or a collection of USB Interfaces, in Python
#
# (Despite the name an object of type Interface can represent multiple
# associated interfaces, with or without an Interface Association Descriptor
# prepended to them. Override num_itfs() if assigning >1 USB interface.)
def __init__(self):
self._open = False
def desc_cfg(self, desc, itf_num, ep_num, strs):
# Function to build configuration descriptor contents for this interface
# or group of interfaces. This is called on each interface from
# USBDevice.init().
#
# This function should insert:
#
# - At least one standard Interface descriptor (can call
# - desc.interface()).
#
# Plus, optionally:
#
# - One or more endpoint descriptors (can call desc.endpoint()).
# - An Interface Association Descriptor, prepended before.
# - Other class-specific configuration descriptor data.
#
# This function is called twice per call to USBDevice.init(). The first
# time the values of all arguments are dummies that are used only to
# calculate the total length of the descriptor. Therefore, anything this
# function does should be idempotent and it should add the same
# descriptors each time. If saving interface numbers or endpoint numbers
# for later
#
# Parameters:
#
# - desc - Descriptor helper to write the configuration descriptor bytes into.
# The first time this function is called 'desc' is a dummy object
# with no backing buffer (exists to count the number of bytes needed).
#
# - itf_num - First bNumInterfaces value to assign. The descriptor
# should contain the same number of interfaces returned by num_itfs(),
# starting from this value.
#
# - ep_num - Address of the first available endpoint number to use for
# endpoint descriptor addresses. Subclasses should save the
# endpoint addresses selected, to look up later (although note the first
# time this function is called, the values will be dummies.)
#
# - strs - list of string descriptors for this USB device. This function
# can append to this list, and then insert the index of the new string
# in the list into the configuration descriptor.
raise NotImplementedError
def num_itfs(self):
# Return the number of actual USB Interfaces represented by this object
# (as set in desc_cfg().)
#
# Only needs to be overriden if implementing a Interface class that
# represents more than one USB Interface descriptor (i.e. MIDI), or an
# Interface Association Descriptor (i.e. USB-CDC).
return 1
def num_eps(self):
# Return the number of USB Endpoint numbers represented by this object
# (as set in desc_cfg().)
#
# Note for each count returned by this function, the interface may
# choose to have both an IN and OUT endpoint (i.e. IN flag is not
# considered a value here.)
#
# This value can be zero, if the USB Host only communicates with this
# interface using control transfers.
return 0
def on_open(self):
# Callback called when the USB host accepts the device configuration.
#
# Override this function to initiate any operations that the USB interface
# should do when the USB device is configured to the host.
self._open = True
def on_reset(self):
# Callback called on every registered interface when the USB device is
# reset by the host. This can happen when the USB device is unplugged,
# or if the host triggers a reset for some other reason.
#
# Override this function to cancel any pending operations specific to
# the interface (outstanding USB transfers are already cancelled).
#
# At this point, no USB functionality is available - on_open() will
# be called later if/when the USB host re-enumerates and configures the
# interface.
self._open = False
def is_open(self):
# Returns True if the interface has been configured by the host and is in
# active use.
return self._open
def on_device_control_xfer(self, stage, request):
# Control transfer callback. Override to handle a non-standard device
# control transfer where bmRequestType Recipient is Device, Type is
# utils.REQ_TYPE_CLASS, and the lower byte of wIndex indicates this interface.
#
# (See USB 2.0 specification 9.4 Standard Device Requests, p250).
#
# This particular request type seems pretty uncommon for a device class
# driver to need to handle, most hosts will not send this so most
# implementations won't need to override it.
#
# Parameters:
#
# - stage is one of utils.STAGE_SETUP, utils.STAGE_DATA, utils.STAGE_ACK.
#
# - request is a memoryview into a USB request packet, as per USB 2.0
# specification 9.3 USB Device Requests, p250. the memoryview is only
# valid while the callback is running.
#
# The function can call split_bmRequestType(request[0]) to split
# bmRequestType into (Recipient, Type, Direction).
#
# Result, any of:
#
# - True to continue the request, False to STALL the endpoint.
# - Buffer interface object to provide a buffer to the host as part of the
# transfer, if applicable.
return False
def on_interface_control_xfer(self, stage, request):
# Control transfer callback. Override to handle a device control
# transfer where bmRequestType Recipient is Interface, and the lower byte
# of wIndex indicates this interface.
#
# (See USB 2.0 specification 9.4 Standard Device Requests, p250).
#
# bmRequestType Type field may have different values. It's not necessary
# to handle the mandatory Standard requests (bmRequestType Type ==
# utils.REQ_TYPE_STANDARD), if the driver returns False in these cases then
# TinyUSB will provide the necessary responses.
#
# See on_device_control_xfer() for a description of the arguments and
# possible return values.
return False
def on_endpoint_control_xfer(self, stage, request):
# Control transfer callback. Override to handle a device
# control transfer where bmRequestType Recipient is Endpoint and
# the lower byte of wIndex indicates an endpoint address associated
# with this interface.
#
# bmRequestType Type will generally have any value except
# utils.REQ_TYPE_STANDARD, as Standard endpoint requests are handled by
# TinyUSB. The exception is the the Standard "Set Feature" request. This
# is handled by Tiny USB but also passed through to the driver in case it
# needs to change any internal state, but most drivers can ignore and
# return False in this case.
#
# (See USB 2.0 specification 9.4 Standard Device Requests, p250).
#
# See on_device_control_xfer() for a description of the parameters and
# possible return values.
return False
def xfer_pending(self, ep_addr):
# Return True if a transfer is already pending on ep_addr.
#
# Only one transfer can be submitted at a time.
return _dev and bool(_dev._ep_cbs[ep_addr])
def submit_xfer(self, ep_addr, data, done_cb=None):
# Submit a USB transfer (of any type except control)
#
# Parameters:
#
# - ep_addr. Address of the endpoint to submit the transfer on. Caller is
# responsible for ensuring that ep_addr is correct and belongs to this
# interface. Only one transfer can be active at a time on each endpoint.
#
# - data. Buffer containing data to send, or for data to be read into
# (depending on endpoint direction).
#
# - done_cb. Optional callback function for when the transfer
# completes. The callback is called with arguments (ep_addr, result,
# xferred_bytes) where result is one of xfer_result_t enum (see top of
# this file), and xferred_bytes is an integer.
#
# If the function returns, the transfer is queued.
#
# The function will raise RuntimeError under the following conditions:
#
# - The interface is not "open" (i.e. has not been enumerated and configured
# by the host yet.)
#
# - A transfer is already pending on this endpoint (use xfer_pending() to check
# before sending if needed.)
#
# - A DCD error occurred when queueing the transfer on the hardware.
#
#
# Will raise TypeError if 'data' isn't he correct type of buffer for the
# endpoint transfer direction.
#
# Note that done_cb may be called immediately, possibly before this
# function has returned to the caller.
if not self._open:
raise RuntimeError("Not open")
_dev._submit_xfer(ep_addr, data, done_cb)
def stall(self, ep_addr, *args):
# Set or get the endpoint STALL state.
#
# To get endpoint stall stage, call with a single argument.
# To set endpoint stall state, call with an additional boolean
# argument to set or clear.
#
# Generally endpoint STALL is handled automatically, but there are some
# device classes that need to explicitly stall or unstall an endpoint
# under certain conditions.
if not self._open or ep_addr not in self._eps:
raise RuntimeError
_dev._usbd.stall(ep_addr, *args)
class Descriptor:
# Wrapper class for writing a descriptor in-place into a provided buffer
#
# Doesn't resize the buffer.
#
# Can be initialised with b=None to perform a dummy pass that calculates the
# length needed for the buffer.
def __init__(self, b):
self.b = b
self.o = 0 # offset of data written to the buffer
def pack(self, fmt, *args):
# Utility function to pack new data into the descriptor
# buffer, starting at the current offset.
#
# Arguments are the same as struct.pack(), but it fills the
# pre-allocated descriptor buffer (growing if needed), instead of
# returning anything.
self.pack_into(fmt, self.o, *args)
def pack_into(self, fmt, offs, *args):
# Utility function to pack new data into the descriptor at offset 'offs'.
#
# If the data written is before 'offs' then self.o isn't incremented,
# otherwise it's incremented to point at the end of the written data.
end = offs + struct.calcsize(fmt)
if self.b:
struct.pack_into(fmt, self.b, offs, *args)
self.o = max(self.o, end)
def extend(self, a):
# Extend the descriptor with some bytes-like data
if self.b:
self.b[self.o : self.o + len(a)] = a
self.o += len(a)
# TODO: At the moment many of these arguments are named the same as the relevant field
# in the spec, as this is easier to understand. Can save some code size by collapsing them
# down.
def interface(
self,
bInterfaceNumber,
bNumEndpoints,
bInterfaceClass=_INTERFACE_CLASS_VENDOR,
bInterfaceSubClass=_INTERFACE_SUBCLASS_NONE,
bInterfaceProtocol=_PROTOCOL_NONE,
iInterface=0,
):
# Utility function to append a standard Interface descriptor, with
# the properties specified in the parameter list.
#
# Defaults for bInterfaceClass, SubClass and Protocol are a "vendor"
# device.
#
# Note that iInterface is a string index number. If set, it should be set
# by the caller Interface to the result of self._get_str_index(s),
# where 's' is a string found in self.strs.
self.pack(
"BBBBBBBBB",
_STD_DESC_INTERFACE_LEN, # bLength
_STD_DESC_INTERFACE_TYPE, # bDescriptorType
bInterfaceNumber,
0, # bAlternateSetting, not currently supported
bNumEndpoints,
bInterfaceClass,
bInterfaceSubClass,
bInterfaceProtocol,
iInterface,
)
def endpoint(self, bEndpointAddress, bmAttributes, wMaxPacketSize, bInterval=1):
# Utility function to append a standard Endpoint descriptor, with
# the properties specified in the parameter list.
#
# See USB 2.0 specification section 9.6.6 Endpoint p269
#
# As well as a numeric value, bmAttributes can be a string value to represent
# common endpoint types: "control", "bulk", "interrupt".
if bmAttributes == "control":
bmAttributes = 0
elif bmAttributes == "bulk":
bmAttributes = 2
elif bmAttributes == "interrupt":
bmAttributes = 3
self.pack(
"<BBBBHB",
_STD_DESC_ENDPOINT_LEN,
_STD_DESC_ENDPOINT_TYPE,
bEndpointAddress,
bmAttributes,
wMaxPacketSize,
bInterval,
)
def interface_assoc(
self,
bFirstInterface,
bInterfaceCount,
bFunctionClass,
bFunctionSubClass,
bFunctionProtocol=_PROTOCOL_NONE,
iFunction=0,
):
# Utility function to append an Interface Association descriptor,
# with the properties specified in the parameter list.
#
# See USB ECN: Interface Association Descriptor.
self.pack(
"<BBBBBBBB",
8,
_ITF_ASSOCIATION_DESC_TYPE,
bFirstInterface,
bInterfaceCount,
bFunctionClass,
bFunctionSubClass,
bFunctionProtocol,
iFunction,
)
def split_bmRequestType(bmRequestType):
# Utility function to split control transfer field bmRequestType into a tuple of 3 fields:
#
# Recipient
# Type
# Data transfer direction
#
# See USB 2.0 specification section 9.3 USB Device Requests and 9.3.1 bmRequestType, p248.
return (
bmRequestType & 0x1F,
(bmRequestType >> 5) & 0x03,
(bmRequestType >> 7) & 0x01,
)
class Buffer:
# An interrupt-safe producer/consumer buffer that wraps a bytearray object.
#
# Kind of like a ring buffer, but supports the idea of returning a
# memoryview for either read or write of multiple bytes (suitable for
# passing to a buffer function without needing to allocate another buffer to
# read into.)
#
# Consumer can call pend_read() to get a memoryview to read from, and then
# finish_read(n) when done to indicate it read 'n' bytes from the
# memoryview. There is also a readinto() convenience function.
#
# Producer must call pend_write() to get a memorybuffer to write into, and
# then finish_write(n) when done to indicate it wrote 'n' bytes into the
# memoryview. There is also a normal write() convenience function.
#
# - Only one producer and one consumer is supported.
#
# - Calling pend_read() and pend_write() is effectively idempotent, they can be
# called more than once without a corresponding finish_x() call if necessary
# (provided only one thread does this, as per the previous point.)
#
# - Calling finish_write() and finish_read() is hard interrupt safe (does
# not allocate). pend_read() and pend_write() each allocate 1 block for
# the memoryview that is returned.
#
# The buffer contents are always laid out as:
#
# - Slice [:_n] = bytes of valid data waiting to read
# - Slice [_n:_w] = unused space
# - Slice [_w:] = bytes of pending write buffer waiting to be written
#
# This buffer should be fast when most reads and writes are balanced and use
# the whole buffer. When this doesn't happen, performance degrades to
# approximate a Python-based single byte ringbuffer.
#
def __init__(self, length):
self._b = memoryview(bytearray(length))
# number of bytes in buffer read to read, starting at index 0. Updated
# by both producer & consumer.
self._n = 0
# start index of a pending write into the buffer, if any. equals
# len(self._b) if no write is pending. Updated by producer only.
self._w = length
def writable(self):
# Number of writable bytes in the buffer. Assumes no pending write is outstanding.
return len(self._b) - self._n
def readable(self):
# Number of readable bytes in the buffer. Assumes no pending read is outstanding.
return self._n
def pend_write(self, wmax=None):
# Returns a memoryview that the producer can write bytes into.
# start the write at self._n, the end of data waiting to read
#
# If wmax is set then the memoryview is pre-sliced to be at most
# this many bytes long.
#
# (No critical section needed as self._w is only updated by the producer.)
self._w = self._n
end = (self._w + wmax) if wmax else len(self._b)
return self._b[self._w : end]
def finish_write(self, nbytes):
# Called by the producer to indicate it wrote nbytes into the buffer.
ist = machine.disable_irq()
try:
assert nbytes <= len(self._b) - self._w # can't say we wrote more than was pended
if self._n == self._w:
# no data was read while the write was happening, so the buffer is already in place
# (this is the fast path)
self._n += nbytes
else:
# Slow path: data was read while the write was happening, so
# shuffle the newly written bytes back towards index 0 to avoid fragmentation
#
# As this updates self._n we have to do it in the critical
# section, so do it byte by byte to avoid allocating.
while nbytes > 0:
self._b[self._n] = self._b[self._w]
self._n += 1
self._w += 1
nbytes -= 1
self._w = len(self._b)
finally:
machine.enable_irq(ist)
def write(self, w):
# Helper method for the producer to write into the buffer in one call
pw = self.pend_write()
to_w = min(len(w), len(pw))
if to_w:
pw[:to_w] = w[:to_w]
self.finish_write(to_w)
return to_w
def pend_read(self):
# Return a memoryview slice that the consumer can read bytes from
return self._b[: self._n]
def finish_read(self, nbytes):
# Called by the consumer to indicate it read nbytes from the buffer.
if not nbytes:
return
ist = machine.disable_irq()
try:
assert nbytes <= self._n # can't say we read more than was available
i = 0
self._n -= nbytes
while i < self._n:
# consumer only read part of the buffer, so shuffle remaining
# read data back towards index 0 to avoid fragmentation
self._b[i] = self._b[i + nbytes]
i += 1
finally:
machine.enable_irq(ist)
def readinto(self, b):
# Helper method for the consumer to read out of the buffer in one call
pr = self.pend_read()
to_r = min(len(pr), len(b))
if to_r:
b[:to_r] = pr[:to_r]
self.finish_read(to_r)
return to_r

Wyświetl plik

@ -54,15 +54,13 @@ select = [
# "UP", # pyupgrade
]
ignore = [
"E401",
"E402",
"E722",
"E741",
"E741", # 'l' is currently widely used
"F401",
"F403",
"F405",
"F541",
"F841",
"E501", # line length, recommended to disable
"ISC001",
"ISC003", # micropython does not support implicit concatenation of f-strings
"PIE810", # micropython does not support passing tuples to .startswith or .endswith
"PLC1901",
@ -74,8 +72,9 @@ ignore = [
"PLW2901",
"RUF012",
"RUF100",
"W191", # tab-indent, redundant when using formatter
]
line-length = 260
line-length = 99
target-version = "py37"
[tool.ruff.mccabe]
@ -97,3 +96,5 @@ max-statements = 166
# ble multitests are evaluated with some names pre-defined
"micropython/bluetooth/aioble/multitests/*" = ["F821"]
[tool.ruff.format]

Wyświetl plik

@ -0,0 +1,32 @@
aiohttp is an HTTP client module for MicroPython asyncio module,
with API mostly compatible with CPython [aiohttp](https://github.com/aio-libs/aiohttp)
module.
> [!NOTE]
> Only client is implemented.
See `examples/client.py`
```py
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get('http://micropython.org') as response:
print("Status:", response.status)
print("Content-Type:", response.headers['Content-Type'])
html = await response.text()
print("Body:", html[:15], "...")
asyncio.run(main())
```
```
$ micropython examples/client.py
Status: 200
Content-Type: text/html; charset=utf-8
Body: <!DOCTYPE html> ...
```

Wyświetl plik

@ -0,0 +1,269 @@
# MicroPython aiohttp library
# MIT license; Copyright (c) 2023 Carlos Gil
import asyncio
import json as _json
from .aiohttp_ws import (
_WSRequestContextManager,
ClientWebSocketResponse,
WebSocketClient,
WSMsgType,
)
HttpVersion10 = "HTTP/1.0"
HttpVersion11 = "HTTP/1.1"
class ClientResponse:
def __init__(self, reader):
self.content = reader
def _decode(self, data):
c_encoding = self.headers.get("Content-Encoding")
if c_encoding in ("gzip", "deflate", "gzip,deflate"):
try:
import deflate
import io
if c_encoding == "deflate":
with deflate.DeflateIO(io.BytesIO(data), deflate.ZLIB) as d:
return d.read()
elif c_encoding == "gzip":
with deflate.DeflateIO(io.BytesIO(data), deflate.GZIP, 15) as d:
return d.read()
except ImportError:
print("WARNING: deflate module required")
return data
async def read(self, sz=-1):
return self._decode(await self.content.read(sz))
async def text(self, encoding="utf-8"):
return (await self.read(int(self.headers.get("Content-Length", -1)))).decode(encoding)
async def json(self):
return _json.loads(await self.read(int(self.headers.get("Content-Length", -1))))
def __repr__(self):
return "<ClientResponse %d %s>" % (self.status, self.headers)
class ChunkedClientResponse(ClientResponse):
def __init__(self, reader):
self.content = reader
self.chunk_size = 0
async def read(self, sz=4 * 1024 * 1024):
if self.chunk_size == 0:
l = await self.content.readline()
l = l.split(b";", 1)[0]
self.chunk_size = int(l, 16)
if self.chunk_size == 0:
# End of message
sep = await self.content.read(2)
assert sep == b"\r\n"
return b""
data = await self.content.read(min(sz, self.chunk_size))
self.chunk_size -= len(data)
if self.chunk_size == 0:
sep = await self.content.read(2)
assert sep == b"\r\n"
return self._decode(data)
def __repr__(self):
return "<ChunkedClientResponse %d %s>" % (self.status, self.headers)
class _RequestContextManager:
def __init__(self, client, request_co):
self.reqco = request_co
self.client = client
async def __aenter__(self):
return await self.reqco
async def __aexit__(self, *args):
await self.client._reader.aclose()
return await asyncio.sleep(0)
class ClientSession:
def __init__(self, base_url="", headers={}, version=HttpVersion10):
self._reader = None
self._base_url = base_url
self._base_headers = {"Connection": "close", "User-Agent": "compat"}
self._base_headers.update(**headers)
self._http_version = version
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return await asyncio.sleep(0)
# TODO: Implement timeouts
async def _request(self, method, url, data=None, json=None, ssl=None, params=None, headers={}):
redir_cnt = 0
while redir_cnt < 2:
reader = await self.request_raw(method, url, data, json, ssl, params, headers)
_headers = []
sline = await reader.readline()
sline = sline.split(None, 2)
status = int(sline[1])
chunked = False
while True:
line = await reader.readline()
if not line or line == b"\r\n":
break
_headers.append(line)
if line.startswith(b"Transfer-Encoding:"):
if b"chunked" in line:
chunked = True
elif line.startswith(b"Location:"):
url = line.rstrip().split(None, 1)[1].decode()
if 301 <= status <= 303:
redir_cnt += 1
await reader.aclose()
continue
break
if chunked:
resp = ChunkedClientResponse(reader)
else:
resp = ClientResponse(reader)
resp.status = status
resp.headers = _headers
resp.url = url
if params:
resp.url += "?" + "&".join(f"{k}={params[k]}" for k in sorted(params))
try:
resp.headers = {
val.split(":", 1)[0]: val.split(":", 1)[-1].strip()
for val in [hed.decode().strip() for hed in _headers]
}
except Exception:
pass
self._reader = reader
return resp
async def request_raw(
self,
method,
url,
data=None,
json=None,
ssl=None,
params=None,
headers={},
is_handshake=False,
version=None,
):
if json and isinstance(json, dict):
data = _json.dumps(json)
if data is not None and method == "GET":
method = "POST"
if params:
url += "?" + "&".join(f"{k}={params[k]}" for k in sorted(params))
try:
proto, dummy, host, path = url.split("/", 3)
except ValueError:
proto, dummy, host = url.split("/", 2)
path = ""
if proto == "http:":
port = 80
elif proto == "https:":
port = 443
if ssl is None:
ssl = True
else:
raise ValueError("Unsupported protocol: " + proto)
if ":" in host:
host, port = host.split(":", 1)
port = int(port)
reader, writer = await asyncio.open_connection(host, port, ssl=ssl)
# Use protocol 1.0, because 1.1 always allows to use chunked transfer-encoding
# But explicitly set Connection: close, even though this should be default for 1.0,
# because some servers misbehave w/o it.
if version is None:
version = self._http_version
if "Host" not in headers:
headers.update(Host=host)
if not data:
query = b"%s /%s %s\r\n%s\r\n" % (
method,
path,
version,
"\r\n".join(f"{k}: {v}" for k, v in headers.items()) + "\r\n" if headers else "",
)
else:
if json:
headers.update(**{"Content-Type": "application/json"})
if isinstance(data, bytes):
headers.update(**{"Content-Type": "application/octet-stream"})
else:
data = data.encode()
headers.update(**{"Content-Length": len(data)})
query = b"""%s /%s %s\r\n%s\r\n%s""" % (
method,
path,
version,
"\r\n".join(f"{k}: {v}" for k, v in headers.items()) + "\r\n",
data,
)
if not is_handshake:
await writer.awrite(query)
return reader
else:
await writer.awrite(query)
return reader, writer
def request(self, method, url, data=None, json=None, ssl=None, params=None, headers={}):
return _RequestContextManager(
self,
self._request(
method,
self._base_url + url,
data=data,
json=json,
ssl=ssl,
params=params,
headers=dict(**self._base_headers, **headers),
),
)
def get(self, url, **kwargs):
return self.request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.request("PUT", url, **kwargs)
def patch(self, url, **kwargs):
return self.request("PATCH", url, **kwargs)
def delete(self, url, **kwargs):
return self.request("DELETE", url, **kwargs)
def head(self, url, **kwargs):
return self.request("HEAD", url, **kwargs)
def options(self, url, **kwargs):
return self.request("OPTIONS", url, **kwargs)
def ws_connect(self, url, ssl=None):
return _WSRequestContextManager(self, self._ws_connect(url, ssl=ssl))
async def _ws_connect(self, url, ssl=None):
ws_client = WebSocketClient(None)
await ws_client.connect(url, ssl=ssl, handshake_request=self.request_raw)
self._reader = ws_client.reader
return ClientWebSocketResponse(ws_client)

Wyświetl plik

@ -0,0 +1,269 @@
# MicroPython aiohttp library
# MIT license; Copyright (c) 2023 Carlos Gil
# adapted from https://github.com/danni/uwebsockets
# and https://github.com/miguelgrinberg/microdot/blob/main/src/microdot_asyncio_websocket.py
import asyncio
import random
import json as _json
import binascii
import re
import struct
from collections import namedtuple
URL_RE = re.compile(r"(wss|ws)://([A-Za-z0-9-\.]+)(?:\:([0-9]+))?(/.+)?")
URI = namedtuple("URI", ("protocol", "hostname", "port", "path")) # noqa: PYI024
def urlparse(uri):
"""Parse ws:// URLs"""
match = URL_RE.match(uri)
if match:
protocol = match.group(1)
host = match.group(2)
port = match.group(3)
path = match.group(4)
if protocol == "wss":
if port is None:
port = 443
elif protocol == "ws":
if port is None:
port = 80
else:
raise ValueError("Scheme {} is invalid".format(protocol))
return URI(protocol, host, int(port), path)
class WebSocketMessage:
def __init__(self, opcode, data):
self.type = opcode
self.data = data
class WSMsgType:
TEXT = 1
BINARY = 2
ERROR = 258
class WebSocketClient:
CONT = 0
TEXT = 1
BINARY = 2
CLOSE = 8
PING = 9
PONG = 10
def __init__(self, params):
self.params = params
self.closed = False
self.reader = None
self.writer = None
async def connect(self, uri, ssl=None, handshake_request=None):
uri = urlparse(uri)
assert uri
if uri.protocol == "wss":
if not ssl:
ssl = True
await self.handshake(uri, ssl, handshake_request)
@classmethod
def _parse_frame_header(cls, header):
byte1, byte2 = struct.unpack("!BB", header)
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
fin = bool(byte1 & 0x80)
opcode = byte1 & 0x0F
# Byte 2: MASK(1) LENGTH(7)
mask = bool(byte2 & (1 << 7))
length = byte2 & 0x7F
return fin, opcode, mask, length
def _process_websocket_frame(self, opcode, payload):
if opcode == self.TEXT:
payload = str(payload, "utf-8")
elif opcode == self.BINARY:
pass
elif opcode == self.CLOSE:
# raise OSError(32, "Websocket connection closed")
return opcode, payload
elif opcode == self.PING:
return self.PONG, payload
elif opcode == self.PONG: # pragma: no branch
return None, None
return None, payload
@classmethod
def _encode_websocket_frame(cls, opcode, payload):
if opcode == cls.TEXT:
payload = payload.encode()
length = len(payload)
fin = mask = True
# Frame header
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
byte1 = 0x80 if fin else 0
byte1 |= opcode
# Byte 2: MASK(1) LENGTH(7)
byte2 = 0x80 if mask else 0
if length < 126: # 126 is magic value to use 2-byte length header
byte2 |= length
frame = struct.pack("!BB", byte1, byte2)
elif length < (1 << 16): # Length fits in 2-bytes
byte2 |= 126 # Magic code
frame = struct.pack("!BBH", byte1, byte2, length)
elif length < (1 << 64):
byte2 |= 127 # Magic code
frame = struct.pack("!BBQ", byte1, byte2, length)
else:
raise ValueError
# Mask is 4 bytes
mask_bits = struct.pack("!I", random.getrandbits(32))
frame += mask_bits
payload = bytes(b ^ mask_bits[i % 4] for i, b in enumerate(payload))
return frame + payload
async def handshake(self, uri, ssl, req):
headers = {}
_http_proto = "http" if uri.protocol != "wss" else "https"
url = f"{_http_proto}://{uri.hostname}:{uri.port}{uri.path or '/'}"
key = binascii.b2a_base64(bytes(random.getrandbits(8) for _ in range(16)))[:-1]
headers["Host"] = f"{uri.hostname}:{uri.port}"
headers["Connection"] = "Upgrade"
headers["Upgrade"] = "websocket"
headers["Sec-WebSocket-Key"] = str(key, "utf-8")
headers["Sec-WebSocket-Version"] = "13"
headers["Origin"] = f"{_http_proto}://{uri.hostname}:{uri.port}"
self.reader, self.writer = await req(
"GET",
url,
ssl=ssl,
headers=headers,
is_handshake=True,
version="HTTP/1.1",
)
header = await self.reader.readline()
header = header[:-2]
assert header.startswith(b"HTTP/1.1 101 "), header
while header:
header = await self.reader.readline()
header = header[:-2]
async def receive(self):
while True:
opcode, payload = await self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover
await self.send(data, send_opcode)
if opcode == self.CLOSE:
self.closed = True
return opcode, data
elif data: # pragma: no branch
return opcode, data
async def send(self, data, opcode=None):
frame = self._encode_websocket_frame(
opcode or (self.TEXT if isinstance(data, str) else self.BINARY), data
)
self.writer.write(frame)
await self.writer.drain()
async def close(self):
if not self.closed: # pragma: no cover
self.closed = True
await self.send(b"", self.CLOSE)
async def _read_frame(self):
header = await self.reader.read(2)
if len(header) != 2: # pragma: no cover
# raise OSError(32, "Websocket connection closed")
opcode = self.CLOSE
payload = b""
return opcode, payload
fin, opcode, has_mask, length = self._parse_frame_header(header)
if length == 126: # Magic number, length header is 2 bytes
(length,) = struct.unpack("!H", await self.reader.read(2))
elif length == 127: # Magic number, length header is 8 bytes
(length,) = struct.unpack("!Q", await self.reader.read(8))
if has_mask: # pragma: no cover
mask = await self.reader.read(4)
payload = await self.reader.read(length)
if has_mask: # pragma: no cover
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
return opcode, payload
class ClientWebSocketResponse:
def __init__(self, wsclient):
self.ws = wsclient
def __aiter__(self):
return self
async def __anext__(self):
msg = WebSocketMessage(*await self.ws.receive())
# print(msg.data, msg.type) # DEBUG
if (not msg.data and msg.type == self.ws.CLOSE) or self.ws.closed:
raise StopAsyncIteration
return msg
async def close(self):
await self.ws.close()
async def send_str(self, data):
if not isinstance(data, str):
raise TypeError("data argument must be str (%r)" % type(data))
await self.ws.send(data)
async def send_bytes(self, data):
if not isinstance(data, (bytes, bytearray, memoryview)):
raise TypeError("data argument must be byte-ish (%r)" % type(data))
await self.ws.send(data)
async def send_json(self, data):
await self.send_str(_json.dumps(data))
async def receive_str(self):
msg = WebSocketMessage(*await self.ws.receive())
if msg.type != self.ws.TEXT:
raise TypeError(f"Received message {msg.type}:{msg.data!r} is not str")
return msg.data
async def receive_bytes(self):
msg = WebSocketMessage(*await self.ws.receive())
if msg.type != self.ws.BINARY:
raise TypeError(f"Received message {msg.type}:{msg.data!r} is not bytes")
return msg.data
async def receive_json(self):
data = await self.receive_str()
return _json.loads(data)
class _WSRequestContextManager:
def __init__(self, client, request_co):
self.reqco = request_co
self.client = client
async def __aenter__(self):
return await self.reqco
async def __aexit__(self, *args):
await self.client._reader.aclose()
return await asyncio.sleep(0)

Wyświetl plik

@ -0,0 +1,19 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("http://micropython.org") as response:
print("Status:", response.status)
print("Content-Type:", response.headers["Content-Type"])
html = await response.text()
print("Body:", html[:15], "...")
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,21 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
headers = {"Accept-Encoding": "gzip,deflate"}
async def main():
async with aiohttp.ClientSession(headers=headers, version=aiohttp.HttpVersion11) as session:
async with session.get("http://micropython.org") as response:
print("Status:", response.status)
print("Content-Type:", response.headers["Content-Type"])
print(response.headers)
html = await response.text()
print(html)
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,30 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
URL = sys.argv.pop()
if not URL.startswith("http"):
URL = "http://micropython.org"
print(URL)
async def fetch(client):
async with client.get(URL) as resp:
assert resp.status == 200
return await resp.text()
async def main():
async with aiohttp.ClientSession() as client:
html = await fetch(client)
print(html)
if __name__ == "__main__":
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,19 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
headers = {"Authorization": "Basic bG9naW46cGFzcw=="}
async def main():
async with aiohttp.ClientSession(headers=headers) as session:
async with session.get("http://httpbin.org/headers") as r:
json_body = await r.json()
print(json_body)
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,26 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession("http://httpbin.org") as session:
async with session.get("/get") as resp:
assert resp.status == 200
rget = await resp.text()
print(f"GET: {rget}")
async with session.post("/post", json={"foo": "bar"}) as resp:
assert resp.status == 200
rpost = await resp.text()
print(f"POST: {rpost}")
async with session.put("/put", data=b"data") as resp:
assert resp.status == 200
rput = await resp.json()
print("PUT: ", rput)
if __name__ == "__main__":
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,21 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
params = {"key1": "value1", "key2": "value2"}
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("http://httpbin.org/get", params=params) as response:
expect = "http://httpbin.org/get?key1=value1&key2=value2"
assert str(response.url) == expect, f"{response.url} != {expect}"
html = await response.text()
print(html)
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,45 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
try:
URL = sys.argv[1] # expects a websocket echo server
except Exception:
URL = "ws://echo.websocket.events"
sslctx = False
if URL.startswith("wss:"):
try:
import ssl
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
sslctx.verify_mode = ssl.CERT_NONE
except Exception:
pass
async def ws_test_echo(session):
async with session.ws_connect(URL, ssl=sslctx) as ws:
await ws.send_str("hello world!\r\n")
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
print(msg.data)
if "close" in msg.data:
break
await ws.send_str("close\r\n")
await ws.close()
async def main():
async with aiohttp.ClientSession() as session:
await ws_test_echo(session)
if __name__ == "__main__":
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,54 @@
import sys
# ruff: noqa: E402
sys.path.insert(0, ".")
import aiohttp
import asyncio
try:
URL = sys.argv[1] # expects a websocket echo server
READ_BANNER = False
except Exception:
URL = "ws://echo.websocket.events"
READ_BANNER = True
sslctx = False
if URL.startswith("wss:"):
try:
import ssl
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
sslctx.verify_mode = ssl.CERT_NONE
except Exception:
pass
async def ws_test_echo(session):
async with session.ws_connect(URL, ssl=sslctx) as ws:
if READ_BANNER:
print(await ws.receive_str())
try:
while True:
await ws.send_str(f"{input('>>> ')}\r\n")
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
print(msg.data, end="")
break
except KeyboardInterrupt:
pass
finally:
await ws.close()
async def main():
async with aiohttp.ClientSession() as session:
await ws_test_echo(session)
if __name__ == "__main__":
asyncio.run(main())

Wyświetl plik

@ -0,0 +1,7 @@
metadata(
description="HTTP client module for MicroPython asyncio module",
version="0.0.3",
pypi="aiohttp",
)
package("aiohttp")

Wyświetl plik

@ -24,5 +24,10 @@ THE SOFTWARE.
"""
from . import decoder
from . import encoder
from ._decoder import CBORDecoder
from ._decoder import load
from ._decoder import loads
from ._encoder import CBOREncoder
from ._encoder import dump
from ._encoder import dumps

Wyświetl plik

@ -159,7 +159,7 @@ def decode_simple_value(decoder):
def decode_float16(decoder):
payload = decoder.read(2)
decoder.read(2)
raise NotImplementedError # no float16 unpack function
@ -210,8 +210,9 @@ class CBORDecoder(object):
data = self.fp.read(amount)
if len(data) < amount:
raise CBORDecodeError(
"premature end of stream (expected to read {} bytes, got {} "
"instead)".format(amount, len(data))
"premature end of stream (expected to read {} bytes, got {} instead)".format(
amount, len(data)
)
)
return data

Wyświetl plik

@ -24,16 +24,15 @@ THE SOFTWARE.
"""
from cbor2 import encoder
from cbor2 import decoder
import cbor2
input = [
{"bn": "urn:dev:ow:10e2073a01080063", "u": "Cel", "t": 1.276020076e09, "v": 23.5},
{"u": "Cel", "t": 1.276020091e09, "v": 23.6},
]
data = encoder.dumps(input)
data = cbor2.dumps(input)
print(data)
print(data.hex())
text = decoder.loads(data)
text = cbor2.loads(data)
print(text)

Some files were not shown because too many files have changed in this diff Show More