kopia lustrzana https://github.com/micropython/micropython-lib
commit
06592f8246
|
@ -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
|
|
@ -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
|
|
@ -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 .
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# MIT license; Copyright (c) 2022 Jim Mussared
|
||||
|
||||
import micropython
|
||||
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 ([^ ]+))?")
|
||||
|
@ -18,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
|
||||
|
@ -38,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
|
||||
|
||||
|
@ -103,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
|
||||
|
@ -113,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.
|
||||
|
@ -134,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.
|
||||
|
@ -174,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))
|
||||
|
|
|
@ -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.",
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
metadata(version="0.3.0")
|
||||
metadata(version="0.4.1")
|
||||
|
||||
require("aioble-core")
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ Alternatively, install the `aioble` package, which will install everything.
|
|||
Usage
|
||||
-----
|
||||
|
||||
Passive scan for nearby devices for 5 seconds: (Observer)
|
||||
#### Passive scan for nearby devices for 5 seconds: (Observer)
|
||||
|
||||
```py
|
||||
async with aioble.scan(duration_ms=5000) as scanner:
|
||||
|
@ -87,7 +87,7 @@ async with aioble.scan(duration_ms=5000, interval_us=30000, window_us=30000, act
|
|||
print(result, result.name(), result.rssi, result.services())
|
||||
```
|
||||
|
||||
Connect to a peripheral device: (Central)
|
||||
#### Connect to a peripheral device: (Central)
|
||||
|
||||
```py
|
||||
# Either from scan result
|
||||
|
@ -101,14 +101,14 @@ except asyncio.TimeoutError:
|
|||
print('Timeout')
|
||||
```
|
||||
|
||||
Register services and wait for connection: (Peripheral, Server)
|
||||
#### Register services and wait for connection: (Peripheral, Server)
|
||||
|
||||
```py
|
||||
_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,
|
||||
|
@ -126,30 +126,95 @@ while True:
|
|||
print("Connection from", device)
|
||||
```
|
||||
|
||||
Update characteristic value: (Server)
|
||||
#### Update characteristic value: (Server)
|
||||
|
||||
```py
|
||||
# Write the local value.
|
||||
temp_char.write(b'data')
|
||||
|
||||
temp_char.notify(b'optional data')
|
||||
|
||||
await temp_char.indicate(timeout_ms=2000)
|
||||
```
|
||||
|
||||
Query the value of a characteristic: (Client)
|
||||
```py
|
||||
# Write the local value and notify/indicate subscribers.
|
||||
temp_char.write(b'data', send_update=True)
|
||||
```
|
||||
|
||||
#### Send notifications: (Server)
|
||||
|
||||
```py
|
||||
# Notify with the current value.
|
||||
temp_char.notify(connection)
|
||||
```
|
||||
|
||||
```py
|
||||
# Notify with a custom value.
|
||||
temp_char.notify(connection, b'optional data')
|
||||
```
|
||||
|
||||
#### Send indications: (Server)
|
||||
|
||||
```py
|
||||
# Indicate with current value.
|
||||
await temp_char.indicate(connection, timeout_ms=2000)
|
||||
```
|
||||
|
||||
```py
|
||||
# Indicate with custom value.
|
||||
await temp_char.indicate(connection, b'optional data', timeout_ms=2000)
|
||||
```
|
||||
|
||||
This will raise `GattError` if the indication is not acknowledged.
|
||||
|
||||
#### Wait for a write from the client: (Server)
|
||||
|
||||
```py
|
||||
# Normal characteristic, returns the connection that did the write.
|
||||
connection = await char.written(timeout_ms=2000)
|
||||
```
|
||||
|
||||
```py
|
||||
# Characteristic with capture enabled, also returns the value.
|
||||
char = Characteristic(..., capture=True)
|
||||
connection, data = await char.written(timeout_ms=2000)
|
||||
```
|
||||
|
||||
#### Query the value of a characteristic: (Client)
|
||||
|
||||
```py
|
||||
temp_service = await connection.service(_ENV_SENSE_UUID)
|
||||
temp_char = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)
|
||||
|
||||
data = await temp_char.read(timeout_ms=1000)
|
||||
```
|
||||
|
||||
#### Wait for a notification/indication: (Client)
|
||||
|
||||
```py
|
||||
# Notification
|
||||
data = await temp_char.notified(timeout_ms=1000)
|
||||
```
|
||||
|
||||
```py
|
||||
# Indication
|
||||
data = await temp_char.indicated(timeout_ms=1000)
|
||||
```
|
||||
|
||||
#### Subscribe to a characteristic: (Client)
|
||||
|
||||
```py
|
||||
# Subscribe for notification.
|
||||
await temp_char.subscribe(notify=True)
|
||||
while True:
|
||||
data = await temp_char.notified()
|
||||
```
|
||||
|
||||
Open L2CAP channels: (Listener)
|
||||
```py
|
||||
# Subscribe for indication.
|
||||
await temp_char.subscribe(indicate=True)
|
||||
while True:
|
||||
data = await temp_char.indicated()
|
||||
```
|
||||
|
||||
#### Open L2CAP channels: (Listener)
|
||||
|
||||
```py
|
||||
channel = await connection.l2cap_accept(_L2CAP_PSN, _L2CAP_MTU)
|
||||
|
@ -158,7 +223,7 @@ n = channel.recvinto(buf)
|
|||
channel.send(b'response')
|
||||
```
|
||||
|
||||
Open L2CAP channels: (Initiator)
|
||||
#### Open L2CAP channels: (Initiator)
|
||||
|
||||
```py
|
||||
channel = await connection.l2cap_connect(_L2CAP_PSN, _L2CAP_MTU)
|
||||
|
|
|
@ -257,7 +257,7 @@ class Characteristic(BaseCharacteristic):
|
|||
raise ValueError("Not supported")
|
||||
ble.gatts_notify(connection._conn_handle, self._value_handle, data)
|
||||
|
||||
async def indicate(self, connection, timeout_ms=1000):
|
||||
async def indicate(self, connection, data=None, timeout_ms=1000):
|
||||
if not (self.flags & _FLAG_INDICATE):
|
||||
raise ValueError("Not supported")
|
||||
if self._indicate_connection is not None:
|
||||
|
@ -270,7 +270,7 @@ class Characteristic(BaseCharacteristic):
|
|||
|
||||
try:
|
||||
with connection.timeout(timeout_ms):
|
||||
ble.gatts_indicate(connection._conn_handle, self._value_handle)
|
||||
ble.gatts_indicate(connection._conn_handle, self._value_handle, data)
|
||||
await self._indicate_event.wait()
|
||||
if self._indicate_status != 0:
|
||||
raise GattError(self._indicate_status)
|
||||
|
@ -290,8 +290,8 @@ class Characteristic(BaseCharacteristic):
|
|||
|
||||
|
||||
class BufferedCharacteristic(Characteristic):
|
||||
def __init__(self, service, uuid, max_len=20, append=False):
|
||||
super().__init__(service, uuid, read=True)
|
||||
def __init__(self, *args, max_len=20, append=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._max_len = max_len
|
||||
self._append = append
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# code. This allows (for development purposes) all the files to live in the
|
||||
# one directory.
|
||||
|
||||
metadata(version="0.3.1")
|
||||
metadata(version="0.4.1")
|
||||
|
||||
# Default installation gives you everything. Install the individual
|
||||
# components (or a combination of them) if you want a more minimal install.
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
# Test characteristic read/write/notify from both GATTS and GATTC.
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.append("")
|
||||
|
||||
from micropython import const
|
||||
import time, machine
|
||||
|
||||
import uasyncio as asyncio
|
||||
import aioble
|
||||
import bluetooth
|
||||
|
||||
TIMEOUT_MS = 5000
|
||||
|
||||
SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
|
||||
CHAR1_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
|
||||
CHAR2_UUID = bluetooth.UUID("00000000-1111-2222-3333-555555555555")
|
||||
CHAR3_UUID = bluetooth.UUID("00000000-1111-2222-3333-666666666666")
|
||||
|
||||
|
||||
# Acting in peripheral role.
|
||||
async def instance0_task():
|
||||
service = aioble.Service(SERVICE_UUID)
|
||||
characteristic1 = aioble.BufferedCharacteristic(service, CHAR1_UUID, write=True)
|
||||
characteristic2 = aioble.BufferedCharacteristic(service, CHAR2_UUID, write=True, max_len=40)
|
||||
characteristic3 = aioble.BufferedCharacteristic(
|
||||
service, CHAR3_UUID, write=True, max_len=80, append=True
|
||||
)
|
||||
aioble.register_services(service)
|
||||
|
||||
multitest.globals(BDADDR=aioble.config("mac"))
|
||||
multitest.next()
|
||||
|
||||
# Wait for central to connect to us.
|
||||
print("advertise")
|
||||
connection = await aioble.advertise(
|
||||
20_000, adv_data=b"\x02\x01\x06\x04\xffMPY", timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
print("connected")
|
||||
|
||||
# The first will just see the second write (truncated).
|
||||
await characteristic1.written(timeout_ms=TIMEOUT_MS)
|
||||
await characteristic1.written(timeout_ms=TIMEOUT_MS)
|
||||
print("written", characteristic1.read())
|
||||
|
||||
# The second will just see the second write (still truncated because MTU
|
||||
# exchange hasn't happened).
|
||||
await characteristic2.written(timeout_ms=TIMEOUT_MS)
|
||||
await characteristic2.written(timeout_ms=TIMEOUT_MS)
|
||||
print("written", characteristic2.read())
|
||||
|
||||
# MTU exchange should happen here.
|
||||
|
||||
# The second will now see the full second write.
|
||||
await characteristic2.written(timeout_ms=TIMEOUT_MS)
|
||||
await characteristic2.written(timeout_ms=TIMEOUT_MS)
|
||||
print("written", characteristic2.read())
|
||||
|
||||
# The third will see the two full writes concatenated.
|
||||
await characteristic3.written(timeout_ms=TIMEOUT_MS)
|
||||
await characteristic3.written(timeout_ms=TIMEOUT_MS)
|
||||
print("written", characteristic3.read())
|
||||
|
||||
# Wait for the central to disconnect.
|
||||
await connection.disconnected(timeout_ms=TIMEOUT_MS)
|
||||
print("disconnected")
|
||||
|
||||
|
||||
def instance0():
|
||||
try:
|
||||
asyncio.run(instance0_task())
|
||||
finally:
|
||||
aioble.stop()
|
||||
|
||||
|
||||
# Acting in central role.
|
||||
async def instance1_task():
|
||||
multitest.next()
|
||||
|
||||
# Connect to peripheral and then disconnect.
|
||||
print("connect")
|
||||
device = aioble.Device(*BDADDR)
|
||||
connection = await device.connect(timeout_ms=TIMEOUT_MS)
|
||||
|
||||
# Discover characteristics.
|
||||
service = await connection.service(SERVICE_UUID)
|
||||
print("service", service.uuid)
|
||||
characteristic1 = await service.characteristic(CHAR1_UUID)
|
||||
print("characteristic1", characteristic1.uuid)
|
||||
characteristic2 = await service.characteristic(CHAR2_UUID)
|
||||
print("characteristic2", characteristic2.uuid)
|
||||
characteristic3 = await service.characteristic(CHAR3_UUID)
|
||||
print("characteristic3", characteristic3.uuid)
|
||||
|
||||
# Write to each characteristic twice, with a long enough value to trigger
|
||||
# truncation.
|
||||
print("write1")
|
||||
await characteristic1.write(
|
||||
"central1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
await characteristic1.write(
|
||||
"central1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
print("write2a")
|
||||
await characteristic2.write(
|
||||
"central2a-aaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
await characteristic2.write(
|
||||
"central2a-bbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
print("exchange mtu")
|
||||
await connection.exchange_mtu(100)
|
||||
print("write2b")
|
||||
await characteristic2.write(
|
||||
"central2b-aaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
await characteristic2.write(
|
||||
"central2b-bbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
print("write3")
|
||||
await characteristic3.write(
|
||||
"central3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
await characteristic3.write(
|
||||
"central3-bbbbbbbbbbbbbbbbbbbbbbbbbbbbb", response=True, timeout_ms=TIMEOUT_MS
|
||||
)
|
||||
|
||||
# Disconnect from peripheral.
|
||||
print("disconnect")
|
||||
await connection.disconnect(timeout_ms=TIMEOUT_MS)
|
||||
print("disconnected")
|
||||
|
||||
|
||||
def instance1():
|
||||
try:
|
||||
asyncio.run(instance1_task())
|
||||
finally:
|
||||
aioble.stop()
|
|
@ -0,0 +1,21 @@
|
|||
--- instance0 ---
|
||||
advertise
|
||||
connected
|
||||
written b'central1-bbbbbbbbbbb'
|
||||
written b'central2a-bbbbbbbbbb'
|
||||
written b'central2b-bbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
written b'central3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaacentral3-bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
disconnected
|
||||
--- instance1 ---
|
||||
connect
|
||||
service UUID('a5a5a5a5-ffff-9999-1111-5a5a5a5a5a5a')
|
||||
characteristic1 UUID('00000000-1111-2222-3333-444444444444')
|
||||
characteristic2 UUID('00000000-1111-2222-3333-555555555555')
|
||||
characteristic3 UUID('00000000-1111-2222-3333-666666666666')
|
||||
write1
|
||||
write2a
|
||||
exchange mtu
|
||||
write2b
|
||||
write3
|
||||
disconnect
|
||||
disconnected
|
|
@ -1,3 +1,3 @@
|
|||
metadata(description="WM8960 codec.", version="0.1.0")
|
||||
metadata(description="WM8960 codec.", version="0.1.1")
|
||||
|
||||
module("wm8960.py", opt=3)
|
||||
|
|
|
@ -683,7 +683,7 @@ class WM8960:
|
|||
)
|
||||
self.regs[_ALC3] = (_ALC_MODE_MASK, mode << _ALC_MODE_SHIFT)
|
||||
try:
|
||||
rate = _alc_sample_rate_table[self.sample_rate]
|
||||
rate = self._alc_sample_rate_table[self.sample_rate]
|
||||
except:
|
||||
rate = 0
|
||||
self.regs[_ADDCTL3] = (_DACCTL3_ALCSR_MASK, rate)
|
||||
|
|
|
@ -5,7 +5,7 @@ import time, math, framebuf, lcd160cr
|
|||
|
||||
|
||||
def get_lcd(lcd):
|
||||
if type(lcd) is str:
|
||||
if isinstance(lcd, str):
|
||||
lcd = lcd160cr.LCD160CR(lcd)
|
||||
return lcd
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -598,7 +598,7 @@ class BMI270:
|
|||
def _write_burst(self, reg, data, chunk=16):
|
||||
self._write_reg(_INIT_ADDR_0, 0)
|
||||
self._write_reg(_INIT_ADDR_1, 0)
|
||||
for i in range(0, len(data) // chunk):
|
||||
for i in range(len(data) // chunk):
|
||||
offs = i * chunk
|
||||
self._write_reg(reg, data[offs : offs + chunk])
|
||||
init_addr = ((i + 1) * chunk) // 2
|
||||
|
@ -606,7 +606,7 @@ class BMI270:
|
|||
self._write_reg(_INIT_ADDR_1, (init_addr >> 4) & 0xFF)
|
||||
|
||||
def _poll_reg(self, reg, mask, retry=10, delay=100):
|
||||
for i in range(0, retry):
|
||||
for i in range(retry):
|
||||
if self._read_reg(reg) & mask:
|
||||
return True
|
||||
time.sleep_ms(delay)
|
||||
|
|
|
@ -165,11 +165,8 @@ class BMM150:
|
|||
z = (z5 / (z4 * 4)) / 16
|
||||
return z
|
||||
|
||||
def reset(self):
|
||||
self._write_reg(_CMD, 0xB6)
|
||||
|
||||
def magnet_raw(self):
|
||||
for i in range(0, 10):
|
||||
for i in range(10):
|
||||
self._read_reg_into(_DATA, self.scratch)
|
||||
if self.scratch[3] & 0x1:
|
||||
return (
|
||||
|
|
|
@ -46,6 +46,7 @@ while (True):
|
|||
|
||||
import array
|
||||
from micropython import const
|
||||
import time
|
||||
|
||||
_CTRL3_C = const(0x12)
|
||||
_CTRL1_XL = const(0x10)
|
||||
|
@ -196,7 +197,7 @@ class LSM6DSOX:
|
|||
|
||||
def reset(self):
|
||||
self._write_reg(_CTRL3_C, self._read_reg(_CTRL3_C) | 0x1)
|
||||
for i in range(0, 10):
|
||||
for i in range(10):
|
||||
if (self._read_reg(_CTRL3_C) & 0x01) == 0:
|
||||
return
|
||||
time.sleep_ms(10)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
metadata(description="ST LSM6DSOX imu driver.", version="1.0.0")
|
||||
metadata(description="ST LSM6DSOX imu driver.", version="1.0.1")
|
||||
module("lsm6dsox.py", opt=3)
|
||||
|
|
|
@ -44,6 +44,7 @@ while (True):
|
|||
time.sleep_ms(100)
|
||||
"""
|
||||
import array
|
||||
from micropython import const
|
||||
|
||||
|
||||
_WHO_AM_I = const(0xF)
|
||||
|
|
|
@ -38,6 +38,7 @@ while (True):
|
|||
time.sleep_ms(10)
|
||||
"""
|
||||
import machine
|
||||
from micropython import const
|
||||
|
||||
_LPS22_CTRL_REG1 = const(0x10)
|
||||
_LPS22_CTRL_REG2 = const(0x11)
|
||||
|
|
|
@ -104,7 +104,7 @@ class ESPFlash:
|
|||
raise Exception("Command ESP_WRITE_REG failed.")
|
||||
|
||||
def _poll_reg(self, addr, flag, retry=10, delay=0.050):
|
||||
for i in range(0, retry):
|
||||
for i in range(retry):
|
||||
reg = self._read_reg(addr)
|
||||
if (reg & flag) == 0:
|
||||
break
|
||||
|
|
|
@ -16,6 +16,7 @@ Currently these radio modem chipsets are supported:
|
|||
* SX1277
|
||||
* SX1278
|
||||
* SX1279
|
||||
* STM32WL55 "sub-GHz radio" peripheral
|
||||
|
||||
Most radio configuration features are supported, as well as transmitting or
|
||||
receiving packets.
|
||||
|
@ -37,6 +38,7 @@ modem model that matches your hardware:
|
|||
|
||||
- `lora-sx126x` for SX1261 & SX1262 support.
|
||||
- `lora-sx127x` for SX1276-SX1279 support.
|
||||
- `lora-stm32wl5` for STM32WL55 support.
|
||||
|
||||
It's recommended to install only the packages that you need, to save firmware
|
||||
size.
|
||||
|
@ -113,6 +115,24 @@ example: lower max frequency, lower maximum SF value) is responsibility of the
|
|||
calling code. When possible please use the correct class anyhow, as per-part
|
||||
code may be added in the future.
|
||||
|
||||
### Creating STM32WL55
|
||||
|
||||
```
|
||||
from lora import WL55SubGhzModem
|
||||
|
||||
def get_modem():
|
||||
# The LoRa configuration will depend on your board and location, see
|
||||
# below under "Modem Configuration" for some possible examples.
|
||||
lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE }
|
||||
return WL55SubGhzModem(lora_cfg)
|
||||
|
||||
modem = get_modem()
|
||||
```
|
||||
|
||||
Note: As this is an internal peripheral of the STM32WL55 microcontroller,
|
||||
support also depends on MicroPython being built for a board based on this
|
||||
microcontroller.
|
||||
|
||||
### Notes about initialisation
|
||||
|
||||
* See below for details about the `lora_cfg` structure that configures the modem's
|
||||
|
@ -157,6 +177,15 @@ Here is a full list of parameters that can be passed to both constructors:
|
|||
| `lora_cfg` | No | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on. | |
|
||||
| `ant`_sw | No | Optional antenna switch object instance, see below for description. | |
|
||||
|
||||
#### STM32WL55
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `lora_cfg` | No | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on. |
|
||||
| `tcxo_millivolts` | No | Defaults to 1700. The voltage supplied on pin PB0_VDDTCXO. See `dio3_tcxo_millivolts` above for details, this parameter has the same behaviour. |
|
||||
| ant_sw | No | Defaults to an instance of `lora.NucleoWL55RFConfig` class for the NUCLEO-WL55 development board. Set to `None` to disable any automatic antenna switching. See below for description. |
|
||||
|
||||
|
||||
## Modem Configuration
|
||||
|
||||
It is necessary to correctly configure the modem before use. At minimum, the
|
||||
|
@ -383,10 +412,11 @@ Type: `str`, not case sensitive
|
|||
|
||||
Default: RFO_HF or RFO_LF (low power)
|
||||
|
||||
SX127x modems have multiple antenna pins for different power levels and
|
||||
frequency ranges. The board/module that the LoRa modem chip is on may have
|
||||
particular antenna connections, or even an RF switch that needs to be set via a
|
||||
GPIO to connect an antenna pin to a particular output (see `ant_sw`, below).
|
||||
SX127x modems and STM32WL55 microcontrollers have multiple antenna pins for
|
||||
different power levels and frequency ranges. The board/module that the LoRa
|
||||
modem chip is on may have particular antenna connections, or even an RF switch
|
||||
that needs to be set via a GPIO to connect an antenna pin to a particular output
|
||||
(see `ant_sw`, below).
|
||||
|
||||
The driver must configure the modem to use the correct pin for a particular
|
||||
hardware antenna connection before transmitting. When receiving, the modem
|
||||
|
@ -396,7 +426,7 @@ A common symptom of incorrect `tx_ant` setting is an extremely weak RF signal.
|
|||
|
||||
Consult modem datasheet for more details.
|
||||
|
||||
SX127x values:
|
||||
##### SX127x tx_ant
|
||||
|
||||
| Value | RF Transmit Pin |
|
||||
|-----------------|----------------------------------|
|
||||
|
@ -407,23 +437,35 @@ Pin "RFO_HF" is automatically used for frequencies above 862MHz, and is not
|
|||
supported on SX1278. "RFO_LF" is used for frequencies below 862MHz. Consult
|
||||
datasheet Table 32 "Frequency Bands" for more details.
|
||||
|
||||
**Important**: If changing `tx_ant` value, configure `output_power` at the same
|
||||
time or again before transmitting.
|
||||
##### WL55SubGhzModem tx_ant
|
||||
|
||||
| Value | RF Transmit Pin |
|
||||
|-----------------|-------------------------|
|
||||
| `"PA_BOOST"` | RFO_HP pin (high power) |
|
||||
| Any other value | RFO_LP pin (low power) |
|
||||
|
||||
**NOTE**: Currently the `PA_BOOST` HP antenna output is lower than it should be
|
||||
on this board, due to an unknown driver bug.
|
||||
|
||||
If setting `tx_ant` value, also set `output_power` at the same time or again
|
||||
before transmitting.
|
||||
|
||||
#### `output_power` - Transmit output power level
|
||||
Type: `int`
|
||||
|
||||
Default: Depends on modem
|
||||
|
||||
Nominal TX output power in dBm. The possible range depends on the modem and (for
|
||||
SX127x only) the `tx_ant` configuration.
|
||||
Nominal TX output power in dBm. The possible range depends on the modem and for
|
||||
some modems the `tx_ant` configuration.
|
||||
|
||||
| Modem | `tx_ant` value | Range | "Optimal" |
|
||||
|--------|------------------|-------------------|------------------------|
|
||||
| SX1261 | N/A | -17 to +15 | +10, +14 or +15 [*][^] |
|
||||
| SX1262 | N/A | -9 to +22 | +14, +17, +20, +22 [*] |
|
||||
| SX127x | "PA_BOOST" | +2 to +17, or +20 | Any |
|
||||
| SX127x | RFO_HF or RFO_LF | -4 to +15 | Any |
|
||||
| Modem | `tx_ant` value | Range (dBm) | "Optimal" (dBm) | |
|
||||
|-----------------|----------------------------|-------------------|------------------------|---|
|
||||
| SX1261 | N/A | -17 to +15 | +10, +14 or +15 [*][^] | |
|
||||
| SX1262 | N/A | -9 to +22 | +14, +17, +20, +22 [*] | |
|
||||
| SX127x | "PA_BOOST" | +2 to +17, or +20 | Any | |
|
||||
| SX127x | RFO_HF or RFO_LF | -4 to +15 | Any | |
|
||||
| WL55SubGhzModem | "PA_BOOST" | -9 to +22 | +14, +17, +20, +22 [*] | |
|
||||
| WL55SubGhzModem | Any other value (not None) | -17 to +14 | +10, +14 or +15 [*][^] | |
|
||||
|
||||
Values which are out of range for the modem will be clamped at the
|
||||
minimum/maximum values shown above.
|
||||
|
@ -432,14 +474,14 @@ Actual radiated TX power for RF regulatory purposes depends on the RF hardware,
|
|||
antenna, and the rest of the modem configuration. It should be measured and
|
||||
tuned empirically not determined from this configuration information alone.
|
||||
|
||||
[*] For SX1261 and SX1262 the datasheet shows "Optimal" Power Amplifier
|
||||
[*] For some modems the datasheet shows "Optimal" Power Amplifier
|
||||
configuration values for these output power levels. If setting one of these
|
||||
levels, the optimal settings from the datasheet are applied automatically by the
|
||||
driver. Therefore it is recommended to use one of these power levels if
|
||||
possible.
|
||||
|
||||
[^] For SX1261 +15dBm is only possible with frequency above 400MHz, will be +14dBm
|
||||
otherwise.
|
||||
[^] In the marked configurations +15dBm is only possible with frequency above
|
||||
400MHz, will be +14dBm otherwise.
|
||||
|
||||
#### `implicit_header` - Implicit/Explicit Header Mode
|
||||
Type: `bool`
|
||||
|
@ -1028,12 +1070,12 @@ following different approaches:
|
|||
`poll_send()` now?" check function if there's no easy way to determine
|
||||
which interrupt has woken the board up.
|
||||
* Implement a custom interrupt callback function and call
|
||||
`modem.set_irq_callback()` to install it. The function will be called with a
|
||||
single argument, which is either the `Pin` that triggered a hardware interrupt
|
||||
or `None` for a soft interrupt. Refer to the documentation about [writing interrupt
|
||||
handlers](https://docs.micropython.org/en/latest/reference/isr_rules.html) for
|
||||
more information. The `lora-async` modem classes install their own callback here,
|
||||
so it's not possible to mix this approach with the provided asynchronous API.
|
||||
`modem.set_irq_callback()` to install it. The function will be called if a
|
||||
hardware interrupt occurs, possibly in hard interrupt context. Refer to the
|
||||
documentation about [writing interrupt handlers][isr_rules] for more
|
||||
information. It may also be called if the driver triggers a soft interrupt.
|
||||
The `lora-async` modem classes install their own callback here, so it's not
|
||||
possible to mix this approach with the provided asynchronous API.
|
||||
* Call `modem.poll_recv()` or `modem.poll_send()`. This takes more time
|
||||
and uses more power as it reads the modem IRQ status directly from the modem
|
||||
via SPI, but it also give the most definite result.
|
||||
|
@ -1137,9 +1179,21 @@ The meaning of `tx_arg` depends on the modem:
|
|||
above), and `False` otherwise.
|
||||
* For SX1262 it is `True` (indicating High Power mode).
|
||||
* For SX1261 it is `False` (indicating Low Power mode).
|
||||
* For WL55SubGhzModem it is `True` if the `PA_BOOST` `tx_ant` setting is in use (see above), and `False` otherwise.
|
||||
|
||||
This parameter can be ignored if it's already known what modem and antenna is being used.
|
||||
|
||||
### WL55SubGhzModem ant_sw
|
||||
|
||||
When instantiating the `WL55SubGhzModem` and `AsyncWL55SubGHzModem` classes, the
|
||||
default `ant_sw` parameter is not `None`. Instead, the default will instantiate
|
||||
an object of type `lora.NucleoWL55RFConfig`. This implements the antenna switch
|
||||
connections for the ST NUCLEO-WL55 development board (as connected to GPIO pins
|
||||
C4, C5 and C3). See ST document [UM2592][ST-UM2592-p27] (PDF) Figure 18 for details.
|
||||
|
||||
When using these modem classes (only), to disable any automatic antenna
|
||||
switching behaviour it's necessary to explicitly set `ant_sw=None`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Some common errors and their causes:
|
||||
|
@ -1150,7 +1204,10 @@ The SX1261/2 drivers will raise this exception if the modem's TCXO fails to
|
|||
provide the necessary clock signal when starting a transmit or receive
|
||||
operation, or moving into "standby" mode.
|
||||
|
||||
Usually, this means the constructor parameter `dio3_tcxo_millivolts` (see above)
|
||||
Sometimes, this means the constructor parameter `dio3_tcxo_millivolts` (see above)
|
||||
must be set as the SX126x chip DIO3 output pin is the power source for the TCXO
|
||||
connected to the modem. Often this parameter should be set to `3300` (3.3V) but
|
||||
it may be another value, consult the documentation for your LoRa modem module.
|
||||
|
||||
[isr_rules]: https://docs.micropython.org/en/latest/reference/isr_rules.html
|
||||
[ST-UM2592-p27]: https://www.st.com/resource/en/user_manual/dm00622917-stm32wl-nucleo64-board-mb1389-stmicroelectronics.pdf#page=27
|
||||
|
|
|
@ -111,9 +111,8 @@ class AsyncModem:
|
|||
if _DEBUG:
|
||||
print(f"wait complete")
|
||||
|
||||
def _callback(self, _):
|
||||
# IRQ callback from BaseModem._radio_isr. Hard IRQ context unless _DEBUG
|
||||
# is on.
|
||||
def _callback(self):
|
||||
# IRQ callback from BaseModem._radio_isr. May be in Hard IRQ context.
|
||||
#
|
||||
# Set both RX & TX flag. This isn't necessary for "real" interrupts, but may be necessary
|
||||
# to wake both for the case of a "soft" interrupt triggered by sleep() or standby(), where
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
metadata(version="0.1.0")
|
||||
metadata(version="0.1.1")
|
||||
require("lora")
|
||||
package("lora")
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
# MicroPython LoRa STM32WL55 embedded sub-ghz radio driver
|
||||
# MIT license; Copyright (c) 2022 Angus Gratton
|
||||
#
|
||||
# This driver is essentially an embedded SX1262 with a custom internal interface block.
|
||||
# Requires the stm module in MicroPython to be compiled with STM32WL5 subghz radio support.
|
||||
#
|
||||
# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
|
||||
from machine import Pin, SPI
|
||||
import stm
|
||||
from . import sx126x
|
||||
from micropython import const
|
||||
|
||||
_CMD_CLR_ERRORS = const(0x07)
|
||||
|
||||
_REG_OCP = const(0x8E7)
|
||||
|
||||
# Default antenna switch config is as per Nucleo WL-55 board. See UM2592 Fig 18.
|
||||
# Possible to work with other antenna switch board configurations by passing
|
||||
# different ant_sw_class arguments to the modem, any class that creates an object with rx/tx
|
||||
|
||||
|
||||
class NucleoWL55RFConfig:
|
||||
def __init__(self):
|
||||
self._FE_CTRL = (Pin(x, mode=Pin.OUT) for x in ("C4", "C5", "C3"))
|
||||
|
||||
def _set_fe_ctrl(self, values):
|
||||
for pin, val in zip(self._FE_CTRL, values):
|
||||
pin(val)
|
||||
|
||||
def rx(self):
|
||||
self._set_fe_ctrl((1, 0, 1))
|
||||
|
||||
def tx(self, hp):
|
||||
self._set_fe_ctrl((0 if hp else 1, 1, 1))
|
||||
|
||||
def idle(self):
|
||||
pass
|
||||
|
||||
|
||||
class DIO1:
|
||||
# Dummy DIO1 "Pin" wrapper class to pass to the _SX126x class
|
||||
def irq(self, handler, _):
|
||||
stm.subghz_irq(handler)
|
||||
|
||||
|
||||
class _WL55SubGhzModem(sx126x._SX126x):
|
||||
# Don't construct this directly, construct lora.WL55SubGhzModem or lora.AsyncWL55SubGHzModem
|
||||
def __init__(
|
||||
self,
|
||||
lora_cfg=None,
|
||||
tcxo_millivolts=1700,
|
||||
ant_sw=NucleoWL55RFConfig,
|
||||
):
|
||||
self._hp = False
|
||||
|
||||
if ant_sw == NucleoWL55RFConfig:
|
||||
# To avoid the default argument being an object instance
|
||||
ant_sw = NucleoWL55RFConfig()
|
||||
|
||||
super().__init__(
|
||||
# RM0453 7.2.13 says max 16MHz, but this seems more stable
|
||||
SPI("SUBGHZ", baudrate=8_000_000),
|
||||
stm.subghz_cs,
|
||||
stm.subghz_is_busy,
|
||||
DIO1(),
|
||||
False, # dio2_rf_sw
|
||||
tcxo_millivolts, # dio3_tcxo_millivolts
|
||||
10_000, # dio3_tcxo_start_time_us, first time after POR is quite long
|
||||
None, # reset
|
||||
lora_cfg,
|
||||
ant_sw,
|
||||
)
|
||||
|
||||
def _clear_errors(self):
|
||||
# A weird difference between STM32WL55 and SX1262, WL55 only takes one
|
||||
# parameter byte for the Clr_Error() command compared to two on SX1262.
|
||||
# The bytes are always zero in both cases.
|
||||
#
|
||||
# (Not clear if sending two bytes will also work always/sometimes, but
|
||||
# sending one byte to SX1262 definitely does not work!
|
||||
self._cmd("BB", _CMD_CLR_ERRORS, 0x00)
|
||||
|
||||
def _clear_irq(self, clear_bits=0xFFFF):
|
||||
super()._clear_irq(clear_bits)
|
||||
# SUBGHZ Radio IRQ requires manual re-enabling after interrupt
|
||||
stm.subghz_irq(self._radio_isr)
|
||||
|
||||
def _tx_hp(self):
|
||||
# STM32WL5 supports both High and Low Power antenna pins depending on tx_ant setting
|
||||
return self._hp
|
||||
|
||||
def _get_pa_tx_params(self, output_power, tx_ant):
|
||||
# Given an output power level in dBm and the tx_ant setting (if any),
|
||||
# return settings for SetPaConfig and SetTxParams.
|
||||
#
|
||||
# ST document RM0453 Set_PaConfig() reference and accompanying Table 35
|
||||
# show values that are an exact superset of the SX1261 and SX1262
|
||||
# available values, depending on which antenna pin is to be
|
||||
# used. Therefore, call either modem's existing _get_pa_tx_params()
|
||||
# function depending on the current tx_ant setting (default is low
|
||||
# power).
|
||||
|
||||
if tx_ant is not None:
|
||||
# Note: currently HP antenna power output is less than it should be,
|
||||
# due to some (unknown) bug.
|
||||
self._hp = tx_ant == "PA_BOOST"
|
||||
|
||||
# Update the OCP register to match the maximum power level
|
||||
self._reg_write(_REG_OCP, 0x38 if self._hp else 0x18)
|
||||
|
||||
if self._hp:
|
||||
return sx126x._SX1262._get_pa_tx_params(self, output_power, tx_ant)
|
||||
else:
|
||||
return sx126x._SX1261._get_pa_tx_params(self, output_power, tx_ant)
|
||||
|
||||
|
||||
# Define the actual modem classes that use the SyncModem & AsyncModem "mixin-like" classes
|
||||
# to create sync and async variants.
|
||||
|
||||
try:
|
||||
from .sync_modem import SyncModem
|
||||
|
||||
class WL55SubGhzModem(_WL55SubGhzModem, SyncModem):
|
||||
pass
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from .async_modem import AsyncModem
|
||||
|
||||
class AsyncWL55SubGhzModem(_WL55SubGhzModem, AsyncModem):
|
||||
pass
|
||||
|
||||
except ImportError:
|
||||
pass
|
|
@ -0,0 +1,3 @@
|
|||
metadata(version="0.1")
|
||||
require("lora-sx126x")
|
||||
package("lora")
|
|
@ -99,7 +99,7 @@ _IRQ_DRIVER_RX_MASK = const(_IRQ_RX_DONE | _IRQ_TIMEOUT | _IRQ_CRC_ERR | _IRQ_HE
|
|||
# In any case, timeouts here are to catch broken/bad hardware or massive driver
|
||||
# bugs rather than commonplace issues.
|
||||
#
|
||||
_CMD_BUSY_TIMEOUT_BASE_US = const(200)
|
||||
_CMD_BUSY_TIMEOUT_BASE_US = const(7000)
|
||||
|
||||
# Datasheet says 3.5ms needed to run a full Calibrate command (all blocks),
|
||||
# however testing shows it can be as much as as 18ms.
|
||||
|
@ -141,12 +141,16 @@ class _SX126x(BaseModem):
|
|||
self._sleep = True # assume the radio is in sleep mode to start, will wake on _cmd
|
||||
self._dio1 = dio1
|
||||
|
||||
busy.init(Pin.IN)
|
||||
cs.init(Pin.OUT, value=1)
|
||||
if dio1:
|
||||
if hasattr(busy, "init"):
|
||||
busy.init(Pin.IN)
|
||||
if hasattr(cs, "init"):
|
||||
cs.init(Pin.OUT, value=1)
|
||||
if hasattr(dio1, "init"):
|
||||
dio1.init(Pin.IN)
|
||||
|
||||
self._busy_timeout = _CMD_BUSY_TIMEOUT_BASE_US
|
||||
self._busy_timeout = _CMD_BUSY_TIMEOUT_BASE_US + (
|
||||
dio3_tcxo_start_time_us if dio3_tcxo_millivolts else 0
|
||||
)
|
||||
|
||||
self._buf = bytearray(9) # shared buffer for commands
|
||||
|
||||
|
@ -166,7 +170,8 @@ class _SX126x(BaseModem):
|
|||
reset(1)
|
||||
time.sleep_ms(5)
|
||||
else:
|
||||
self.standby() # Otherwise, at least put the radio to a known state
|
||||
# Otherwise, at least put the radio to a known state
|
||||
self._cmd("BB", _CMD_SET_STANDBY, 0) # STDBY_RC mode, not ready for TCXO yet
|
||||
|
||||
status = self._get_status()
|
||||
if (status[0] != _STATUS_MODE_STANDBY_RC and status[0] != _STATUS_MODE_STANDBY_HSE32) or (
|
||||
|
@ -185,7 +190,6 @@ class _SX126x(BaseModem):
|
|||
#
|
||||
# timeout register is set in units of 15.625us each, use integer math
|
||||
# to calculate and round up:
|
||||
self._busy_timeout = (_CMD_BUSY_TIMEOUT_BASE_US + dio3_tcxo_start_time_us) * 2
|
||||
timeout = (dio3_tcxo_start_time_us * 1000 + 15624) // 15625
|
||||
if timeout < 0 or timeout > 1 << 24:
|
||||
raise ValueError("{} out of range".format("dio3_tcxo_start_time_us"))
|
||||
|
@ -231,7 +235,7 @@ class _SX126x(BaseModem):
|
|||
0x0, # DIO2Mask, not used
|
||||
0x0, # DIO3Mask, not used
|
||||
)
|
||||
dio1.irq(self._radio_isr, trigger=Pin.IRQ_RISING)
|
||||
dio1.irq(self._radio_isr, Pin.IRQ_RISING)
|
||||
|
||||
self._clear_irq()
|
||||
|
||||
|
@ -382,7 +386,9 @@ class _SX126x(BaseModem):
|
|||
self._cmd(">BBH", _CMD_WRITE_REGISTER, _REG_LSYNCRH, syncword)
|
||||
|
||||
if "output_power" in lora_cfg:
|
||||
pa_config_args, self._output_power = self._get_pa_tx_params(lora_cfg["output_power"])
|
||||
pa_config_args, self._output_power = self._get_pa_tx_params(
|
||||
lora_cfg["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:
|
||||
|
@ -664,7 +670,7 @@ class _SX126x(BaseModem):
|
|||
while self._busy():
|
||||
ticks_diff = time.ticks_diff(time.ticks_us(), start)
|
||||
if ticks_diff > timeout_us:
|
||||
raise RuntimeError("BUSY timeout")
|
||||
raise RuntimeError("BUSY timeout", timeout_us)
|
||||
time.sleep_us(1)
|
||||
if _DEBUG and ticks_diff > 105:
|
||||
# By default, debug log any busy time that takes longer than the
|
||||
|
@ -760,7 +766,7 @@ class _SX1262(_SX126x):
|
|||
# SX1262 has High Power only (deviceSel==0)
|
||||
return True
|
||||
|
||||
def _get_pa_tx_params(self, output_power):
|
||||
def _get_pa_tx_params(self, output_power, tx_ant):
|
||||
# Given an output power level in dB, return a 2-tuple:
|
||||
# - First item is the 3 arguments for SetPaConfig command
|
||||
# - Second item is the power level argument value for SetTxParams command.
|
||||
|
@ -831,7 +837,7 @@ class _SX1261(_SX126x):
|
|||
# SX1261 has Low Power only (deviceSel==1)
|
||||
return False
|
||||
|
||||
def _get_pa_tx_params(self, output_power):
|
||||
def _get_pa_tx_params(self, output_power, tx_ant):
|
||||
# Given an output power level in dB, return a 2-tuple:
|
||||
# - First item is the 3 arguments for SetPaConfig command
|
||||
# - Second item is the power level argument value for SetTxParams command.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
metadata(version="0.1.0")
|
||||
metadata(version="0.1.1")
|
||||
require("lora")
|
||||
package("lora")
|
||||
|
|
|
@ -23,7 +23,18 @@ except ImportError as e:
|
|||
if "no module named 'lora." not in str(e):
|
||||
raise
|
||||
|
||||
try:
|
||||
from .stm32wl5 import * # noqa: F401
|
||||
|
||||
ok = True
|
||||
except ImportError as e:
|
||||
if "no module named 'lora." not in str(e):
|
||||
raise
|
||||
|
||||
|
||||
if not ok:
|
||||
raise ImportError(
|
||||
"Incomplete lora installation. Need at least one of lora-sync, lora-async and one of lora-sx126x, lora-sx127x"
|
||||
)
|
||||
|
||||
del ok
|
||||
|
|
|
@ -233,25 +233,16 @@ class BaseModem:
|
|||
#
|
||||
# ISR implementation is relatively simple, just exists to signal an optional
|
||||
# callback, record a timestamp, and wake up the hardware if
|
||||
# needed. ppplication code is expected to call poll_send() or
|
||||
# needed. Application code is expected to call poll_send() or
|
||||
# poll_recv() as applicable in order to confirm the modem state.
|
||||
#
|
||||
# This is a MP hard irq in some configurations, meaning no memory allocation is possible.
|
||||
#
|
||||
# 'pin' may also be None if this is a "soft" IRQ triggered after a receive
|
||||
# timed out during a send (meaning no receive IRQ will fire, but the
|
||||
# receiver should wake up and move on anyhow.)
|
||||
def _radio_isr(self, pin):
|
||||
# This is a MP hard irq in some configurations.
|
||||
def _radio_isr(self, _):
|
||||
self._last_irq = time.ticks_ms()
|
||||
if self._irq_callback:
|
||||
self._irq_callback(pin)
|
||||
self._irq_callback()
|
||||
if _DEBUG:
|
||||
# Note: this may cause a MemoryError and fail if _DEBUG is enabled in this base class
|
||||
# but disabled in the subclass, meaning this is a hard irq handler
|
||||
try:
|
||||
print("_radio_isr pin={}".format(pin))
|
||||
except MemoryError:
|
||||
pass
|
||||
print("_radio_isr")
|
||||
|
||||
def irq_triggered(self):
|
||||
# Returns True if the ISR has executed since the last time a send or a receive
|
||||
|
@ -264,8 +255,7 @@ class BaseModem:
|
|||
# This is used by the AsyncModem implementation, but can be called in
|
||||
# other circumstances to implement custom ISR logic.
|
||||
#
|
||||
# Note that callback may be called in hard ISR context, meaning no
|
||||
# memory allocation is possible.
|
||||
# Note that callback may be called in hard ISR context.
|
||||
self._irq_callback = callback
|
||||
|
||||
def _get_last_irq(self):
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
metadata(version="0.1.0")
|
||||
metadata(version="0.2.0")
|
||||
package("lora")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# MicroPython package installer
|
||||
# MIT license; Copyright (c) 2022 Jim Mussared
|
||||
|
||||
from micropython import const
|
||||
import requests
|
||||
import sys
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# This module should be imported from REPL, not run from command line.
|
||||
import binascii
|
||||
import hashlib
|
||||
from micropython import const
|
||||
import network
|
||||
import os
|
||||
import socket
|
||||
|
|
|
@ -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 reader.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))
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -61,9 +61,10 @@ ignore = [
|
|||
"F401",
|
||||
"F403",
|
||||
"F405",
|
||||
"E501",
|
||||
"F541",
|
||||
"F821",
|
||||
"F841",
|
||||
"ISC001",
|
||||
"ISC003", # micropython does not support implicit concatenation of f-strings
|
||||
"PIE810", # micropython does not support passing tuples to .startswith or .endswith
|
||||
"PLC1901",
|
||||
|
@ -75,8 +76,9 @@ ignore = [
|
|||
"PLW2901",
|
||||
"RUF012",
|
||||
"RUF100",
|
||||
"W191",
|
||||
]
|
||||
line-length = 260
|
||||
line-length = 99
|
||||
target-version = "py37"
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
|
@ -91,3 +93,12 @@ max-statements = 166
|
|||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"micropython/aiorepl/aiorepl.py" = ["PGH001"]
|
||||
|
||||
# manifest.py files are evaluated with some global names pre-defined
|
||||
"**/manifest.py" = ["F821"]
|
||||
"ports/**/boards/manifest*.py" = ["F821"]
|
||||
|
||||
# ble multitests are evaluated with some names pre-defined
|
||||
"micropython/bluetooth/aioble/multitests/*" = ["F821"]
|
||||
|
||||
[tool.ruff.format]
|
||||
|
|
|
@ -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> ...
|
||||
|
||||
```
|
|
@ -0,0 +1,264 @@
|
|||
# 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, 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(sz=-1)).decode(encoding)
|
||||
|
||||
async def json(self):
|
||||
return _json.loads(await self.read())
|
||||
|
||||
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
|
||||
redir_url = None
|
||||
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("latin-1")
|
||||
|
||||
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 = "%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:
|
||||
headers.update(**{"Content-Length": len(str(data))})
|
||||
if json:
|
||||
headers.update(**{"Content-Type": "application/json"})
|
||||
query = """%s /%s %s\r\n%s\r\n%s\r\n\r\n""" % (
|
||||
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.encode("latin-1"))
|
||||
return reader
|
||||
else:
|
||||
await writer.awrite(query.encode())
|
||||
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)
|
|
@ -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 = payload.decode()
|
||||
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"] = key
|
||||
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)
|
|
@ -0,0 +1,18 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,20 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,29 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,18 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,25 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,20 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,44 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,53 @@
|
|||
import sys
|
||||
|
||||
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())
|
|
@ -0,0 +1,7 @@
|
|||
metadata(
|
||||
description="HTTP client module for MicroPython asyncio module",
|
||||
version="0.0.1",
|
||||
pypi="aiohttp",
|
||||
)
|
||||
|
||||
package("aiohttp")
|
|
@ -160,7 +160,7 @@ def decode_simple_value(decoder):
|
|||
|
||||
def decode_float16(decoder):
|
||||
payload = decoder.read(2)
|
||||
return unpack_float16(payload)
|
||||
raise NotImplementedError # no float16 unpack function
|
||||
|
||||
|
||||
def decode_float32(decoder):
|
||||
|
@ -185,7 +185,7 @@ special_decoders = {
|
|||
20: lambda self: False,
|
||||
21: lambda self: True,
|
||||
22: lambda self: None,
|
||||
23: lambda self: undefined,
|
||||
# 23 is undefined
|
||||
24: decode_simple_value,
|
||||
25: decode_float16,
|
||||
26: decode_float32,
|
||||
|
@ -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
|
||||
|
|
|
@ -380,9 +380,11 @@ def client(host, udp=False, reverse=False, bandwidth=10 * 1024 * 1024):
|
|||
ticks_us_end = param["time"] * 1000000
|
||||
poll = select.poll()
|
||||
poll.register(s_ctrl, select.POLLIN)
|
||||
buf = None
|
||||
s_data = None
|
||||
start = None
|
||||
udp_packet_id = 0
|
||||
udp_last_send = None
|
||||
while True:
|
||||
for pollable in poll.poll(stats.max_dt_ms()):
|
||||
if pollable_is_sock(pollable, s_data):
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
metadata(version="0.8.0", pypi="requests")
|
||||
metadata(version="0.8.1", pypi="requests")
|
||||
|
||||
package("requests")
|
||||
|
|
|
@ -45,7 +45,7 @@ def request(
|
|||
parse_headers=True,
|
||||
):
|
||||
redirect = None # redirection url, None means no redirection
|
||||
chunked_data = data and getattr(data, "__iter__", None) and not getattr(data, "__len__", None)
|
||||
chunked_data = data and getattr(data, "__next__", None) and not getattr(data, "__len__", None)
|
||||
|
||||
if auth is not None:
|
||||
import ubinascii
|
||||
|
|
|
@ -5,3 +5,4 @@ absolute_import = True
|
|||
with_statement = True
|
||||
print_function = True
|
||||
unicode_literals = True
|
||||
annotations = True
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
metadata(version="0.0.3")
|
||||
metadata(version="0.1.0")
|
||||
|
||||
module("__future__.py")
|
||||
|
|
|
@ -52,6 +52,25 @@ def _bytes_from_decode_data(s):
|
|||
raise TypeError("argument should be bytes or ASCII string, not %s" % s.__class__.__name__)
|
||||
|
||||
|
||||
def _maketrans(f, t):
|
||||
"""Re-implement bytes.maketrans() as there is no such function in micropython"""
|
||||
if len(f) != len(t):
|
||||
raise ValueError("maketrans arguments must have same length")
|
||||
translation_table = dict(zip(f, t))
|
||||
return translation_table
|
||||
|
||||
|
||||
def _translate(input_bytes, trans_table):
|
||||
"""Re-implement bytes.translate() as there is no such function in micropython"""
|
||||
result = bytearray()
|
||||
|
||||
for byte in input_bytes:
|
||||
translated_byte = trans_table.get(byte, byte)
|
||||
result.append(translated_byte)
|
||||
|
||||
return bytes(result)
|
||||
|
||||
|
||||
# Base64 encoding/decoding uses binascii
|
||||
|
||||
|
||||
|
@ -73,7 +92,7 @@ def b64encode(s, altchars=None):
|
|||
if not isinstance(altchars, bytes_types):
|
||||
raise TypeError("expected bytes, not %s" % altchars.__class__.__name__)
|
||||
assert len(altchars) == 2, repr(altchars)
|
||||
return encoded.translate(bytes.maketrans(b"+/", altchars))
|
||||
encoded = _translate(encoded, _maketrans(b"+/", altchars))
|
||||
return encoded
|
||||
|
||||
|
||||
|
@ -95,7 +114,7 @@ def b64decode(s, altchars=None, validate=False):
|
|||
if altchars is not None:
|
||||
altchars = _bytes_from_decode_data(altchars)
|
||||
assert len(altchars) == 2, repr(altchars)
|
||||
s = s.translate(bytes.maketrans(altchars, b"+/"))
|
||||
s = _translate(s, _maketrans(altchars, b"+/"))
|
||||
if validate and not re.match(b"^[A-Za-z0-9+/]*=*$", s):
|
||||
raise binascii.Error("Non-base64 digit found")
|
||||
return binascii.a2b_base64(s)
|
||||
|
@ -120,8 +139,8 @@ def standard_b64decode(s):
|
|||
return b64decode(s)
|
||||
|
||||
|
||||
# _urlsafe_encode_translation = bytes.maketrans(b'+/', b'-_')
|
||||
# _urlsafe_decode_translation = bytes.maketrans(b'-_', b'+/')
|
||||
# _urlsafe_encode_translation = _maketrans(b'+/', b'-_')
|
||||
# _urlsafe_decode_translation = _maketrans(b'-_', b'+/')
|
||||
|
||||
|
||||
def urlsafe_b64encode(s):
|
||||
|
@ -132,7 +151,7 @@ def urlsafe_b64encode(s):
|
|||
'/'.
|
||||
"""
|
||||
# return b64encode(s).translate(_urlsafe_encode_translation)
|
||||
raise NotImplementedError()
|
||||
return b64encode(s, b"-_").rstrip(b"\n")
|
||||
|
||||
|
||||
def urlsafe_b64decode(s):
|
||||
|
@ -266,7 +285,7 @@ def b32decode(s, casefold=False, map01=None):
|
|||
if map01 is not None:
|
||||
map01 = _bytes_from_decode_data(map01)
|
||||
assert len(map01) == 1, repr(map01)
|
||||
s = s.translate(bytes.maketrans(b"01", b"O" + map01))
|
||||
s = _translate(s, _maketrans(b"01", b"O" + map01))
|
||||
if casefold:
|
||||
s = s.upper()
|
||||
# Strip off pad characters from the right. We need to count the pad
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
metadata(version="3.3.4")
|
||||
metadata(version="3.3.5")
|
||||
|
||||
require("binascii")
|
||||
require("struct")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
metadata(version="0.1.4")
|
||||
metadata(version="0.2.0")
|
||||
|
||||
# Originally written by Paul Sokolovsky.
|
||||
|
||||
|
|
|
@ -66,6 +66,13 @@ def isdir(path):
|
|||
return False
|
||||
|
||||
|
||||
def isfile(path):
|
||||
try:
|
||||
return bool(os.stat(path)[0] & 0x8000)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def expanduser(s):
|
||||
if s == "~" or s.startswith("~/"):
|
||||
h = os.getenv("HOME")
|
||||
|
|
|
@ -20,3 +20,7 @@ assert not exists(dir + "/test_path.py--")
|
|||
assert isdir(dir + "/os")
|
||||
assert not isdir(dir + "/os--")
|
||||
assert not isdir(dir + "/test_path.py")
|
||||
|
||||
assert not isfile(dir + "/os")
|
||||
assert isfile(dir + "/test_path.py")
|
||||
assert not isfile(dir + "/test_path.py--")
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# time
|
||||
|
||||
This library _extends_ the built-in [MicroPython `time`
|
||||
module](https://docs.micropython.org/en/latest/library/time.html#module-time) to
|
||||
include
|
||||
[`time.strftime()`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior).
|
||||
|
||||
`strftime()` is omitted from the built-in `time` module to conserve space.
|
||||
|
||||
## Installation
|
||||
|
||||
Use `mip` via `mpremote`:
|
||||
|
||||
```bash
|
||||
> mpremote mip install time
|
||||
```
|
||||
|
||||
See [Package management](https://docs.micropython.org/en/latest/reference/packages.html) for more details on using `mip` and `mpremote`.
|
||||
|
||||
## Common uses
|
||||
|
||||
`strftime()` is used when using a loggging [Formatter
|
||||
Object](https://docs.python.org/3/library/logging.html#formatter-objects) that
|
||||
employs
|
||||
[`asctime`](https://docs.python.org/3/library/logging.html#formatter-objects).
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
logging.Formatter('%(asctime)s | %(name)s | %(levelname)s - %(message)s')
|
||||
```
|
||||
|
||||
The expected output might look like:
|
||||
|
||||
```text
|
||||
Tue Feb 17 09:42:58 2009 | MAIN | INFO - test
|
||||
```
|
||||
|
||||
But if this `time` extension library isn't installed, `asctime` will always be
|
||||
`None`:
|
||||
|
||||
|
||||
```text
|
||||
None | MAIN | INFO - test
|
||||
```
|
19
tools/ci.sh
19
tools/ci.sh
|
@ -1,18 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
########################################################################################
|
||||
# code formatting
|
||||
# commit formatting
|
||||
|
||||
function ci_code_formatting_setup {
|
||||
sudo apt-add-repository --yes --update ppa:pybricks/ppa
|
||||
sudo apt-get install uncrustify
|
||||
pip3 install black
|
||||
uncrustify --version
|
||||
black --version
|
||||
}
|
||||
|
||||
function ci_code_formatting_run {
|
||||
tools/codeformat.py -v
|
||||
function ci_commit_formatting_run {
|
||||
git remote add upstream https://github.com/micropython/micropython-lib.git
|
||||
git fetch --depth=100 upstream master
|
||||
# If the common ancestor commit hasn't been found, fetch more.
|
||||
git merge-base upstream/master HEAD || git fetch --unshallow upstream master
|
||||
# For a PR, upstream/master..HEAD ends with a merge commit into master, exclude that one.
|
||||
tools/verifygitlog.py -v upstream/master..HEAD --no-merges
|
||||
}
|
||||
|
||||
########################################################################################
|
||||
|
|
|
@ -25,87 +25,19 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# This is based on tools/codeformat.py from the main micropython/micropython
|
||||
# repository but without support for .c/.h files.
|
||||
# This is just a wrapper around running ruff format, so that code formatting can be
|
||||
# invoked in the same way as in the main repo.
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
# Relative to top-level repo dir.
|
||||
PATHS = [
|
||||
"**/*.py",
|
||||
]
|
||||
|
||||
EXCLUSIONS = []
|
||||
|
||||
# Path to repo top-level dir.
|
||||
TOP = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
PY_EXTS = (".py",)
|
||||
|
||||
|
||||
def list_files(paths, exclusions=None, prefix=""):
|
||||
files = set()
|
||||
for pattern in paths:
|
||||
files.update(glob.glob(os.path.join(prefix, pattern), recursive=True))
|
||||
for pattern in exclusions or []:
|
||||
files.difference_update(glob.fnmatch.filter(files, os.path.join(prefix, pattern)))
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def main():
|
||||
cmd_parser = argparse.ArgumentParser(description="Auto-format Python files.")
|
||||
cmd_parser.add_argument("-v", action="store_true", help="Enable verbose output")
|
||||
cmd_parser.add_argument(
|
||||
"-f",
|
||||
action="store_true",
|
||||
help="Filter files provided on the command line against the default list of files to check.",
|
||||
)
|
||||
cmd_parser.add_argument("files", nargs="*", help="Run on specific globs")
|
||||
args = cmd_parser.parse_args()
|
||||
|
||||
# Expand the globs passed on the command line, or use the default globs above.
|
||||
files = []
|
||||
if args.files:
|
||||
files = list_files(args.files)
|
||||
if args.f:
|
||||
# Filter against the default list of files. This is a little fiddly
|
||||
# because we need to apply both the inclusion globs given in PATHS
|
||||
# as well as the EXCLUSIONS, and use absolute paths
|
||||
files = {os.path.abspath(f) for f in files}
|
||||
all_files = set(list_files(PATHS, EXCLUSIONS, TOP))
|
||||
if args.v: # In verbose mode, log any files we're skipping
|
||||
for f in files - all_files:
|
||||
print("Not checking: {}".format(f))
|
||||
files = list(files & all_files)
|
||||
else:
|
||||
files = list_files(PATHS, EXCLUSIONS, TOP)
|
||||
|
||||
# Extract files matching a specific language.
|
||||
def lang_files(exts):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1].lower() in exts:
|
||||
yield file
|
||||
|
||||
# Run tool on N files at a time (to avoid making the command line too long).
|
||||
def batch(cmd, files, N=200):
|
||||
while True:
|
||||
file_args = list(itertools.islice(files, N))
|
||||
if not file_args:
|
||||
break
|
||||
subprocess.check_call(cmd + file_args)
|
||||
|
||||
# Format Python files with black.
|
||||
command = ["black", "--fast", "--line-length=99"]
|
||||
if args.v:
|
||||
command.append("-v")
|
||||
else:
|
||||
command.append("-q")
|
||||
batch(command, lang_files(PY_EXTS))
|
||||
command = ["ruff", "format", "."]
|
||||
subprocess.check_call(command, cwd=TOP)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -185,9 +185,7 @@ urls = {{ Homepage = "https://github.com/micropython/micropython-lib" }}
|
|||
"""
|
||||
[tool.hatch.build]
|
||||
packages = ["{}"]
|
||||
""".format(
|
||||
top_level_package
|
||||
),
|
||||
""".format(top_level_package),
|
||||
file=toml_file,
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# This is an exact duplicate of verifygitlog.py from the main repo.
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
verbosity = 0 # Show what's going on, 0 1 or 2.
|
||||
suggestions = 1 # Set to 0 to not include lengthy suggestions in error messages.
|
||||
|
||||
ignore_prefixes = []
|
||||
|
||||
|
||||
def verbose(*args):
|
||||
if verbosity:
|
||||
print(*args)
|
||||
|
||||
|
||||
def very_verbose(*args):
|
||||
if verbosity > 1:
|
||||
print(*args)
|
||||
|
||||
|
||||
class ErrorCollection:
|
||||
# Track errors and warnings as the program runs
|
||||
def __init__(self):
|
||||
self.has_errors = False
|
||||
self.has_warnings = False
|
||||
self.prefix = ""
|
||||
|
||||
def error(self, text):
|
||||
print("error: {}{}".format(self.prefix, text))
|
||||
self.has_errors = True
|
||||
|
||||
def warning(self, text):
|
||||
print("warning: {}{}".format(self.prefix, text))
|
||||
self.has_warnings = True
|
||||
|
||||
|
||||
def git_log(pretty_format, *args):
|
||||
# Delete pretty argument from user args so it doesn't interfere with what we do.
|
||||
args = ["git", "log"] + [arg for arg in args if "--pretty" not in args]
|
||||
args.append("--pretty=format:" + pretty_format)
|
||||
very_verbose("git_log", *args)
|
||||
# Generator yielding each output line.
|
||||
for line in subprocess.Popen(args, stdout=subprocess.PIPE).stdout:
|
||||
yield line.decode().rstrip("\r\n")
|
||||
|
||||
|
||||
def diagnose_subject_line(subject_line, subject_line_format, err):
|
||||
err.error("Subject line: " + subject_line)
|
||||
if not subject_line.endswith("."):
|
||||
err.error('* must end with "."')
|
||||
if not re.match(r"^[^!]+: ", subject_line):
|
||||
err.error('* must start with "path: "')
|
||||
if re.match(r"^[^!]+: *$", subject_line):
|
||||
err.error("* must contain a subject after the path.")
|
||||
m = re.match(r"^[^!]+: ([a-z][^ ]*)", subject_line)
|
||||
if m:
|
||||
err.error('* first word of subject ("{}") must be capitalised.'.format(m.group(1)))
|
||||
if re.match(r"^[^!]+: [^ ]+$", subject_line):
|
||||
err.error("* subject must contain more than one word.")
|
||||
err.error("* must match: " + repr(subject_line_format))
|
||||
err.error('* Example: "py/runtime: Add support for foo to bar."')
|
||||
|
||||
|
||||
def verify(sha, err):
|
||||
verbose("verify", sha)
|
||||
err.prefix = "commit " + sha + ": "
|
||||
|
||||
# Author and committer email.
|
||||
for line in git_log("%ae%n%ce", sha, "-n1"):
|
||||
very_verbose("email", line)
|
||||
if "noreply" in line:
|
||||
err.error("Unwanted email address: " + line)
|
||||
|
||||
# Message body.
|
||||
raw_body = list(git_log("%B", sha, "-n1"))
|
||||
verify_message_body(raw_body, err)
|
||||
|
||||
|
||||
def verify_message_body(raw_body, err):
|
||||
if not raw_body:
|
||||
err.error("Message is empty")
|
||||
return
|
||||
|
||||
# Subject line.
|
||||
subject_line = raw_body[0]
|
||||
for prefix in ignore_prefixes:
|
||||
if subject_line.startswith(prefix):
|
||||
verbose("Skipping ignored commit message")
|
||||
return
|
||||
very_verbose("subject_line", subject_line)
|
||||
subject_line_format = r"^[^!]+: [A-Z]+.+ .+\.$"
|
||||
if not re.match(subject_line_format, subject_line):
|
||||
diagnose_subject_line(subject_line, subject_line_format, err)
|
||||
if len(subject_line) >= 73:
|
||||
err.error("Subject line must be 72 or fewer characters: " + subject_line)
|
||||
|
||||
# Second one divides subject and body.
|
||||
if len(raw_body) > 1 and raw_body[1]:
|
||||
err.error("Second message line must be empty: " + raw_body[1])
|
||||
|
||||
# Message body lines.
|
||||
for line in raw_body[2:]:
|
||||
# Long lines with URLs are exempt from the line length rule.
|
||||
if len(line) >= 76 and "://" not in line:
|
||||
err.error("Message lines should be 75 or less characters: " + line)
|
||||
|
||||
if not raw_body[-1].startswith("Signed-off-by: ") or "@" not in raw_body[-1]:
|
||||
err.error('Message must be signed-off. Use "git commit -s".')
|
||||
|
||||
|
||||
def run(args):
|
||||
verbose("run", *args)
|
||||
|
||||
err = ErrorCollection()
|
||||
|
||||
if "--check-file" in args:
|
||||
filename = args[-1]
|
||||
verbose("checking commit message from", filename)
|
||||
with open(args[-1]) as f:
|
||||
# Remove comment lines as well as any empty lines at the end.
|
||||
lines = [line.rstrip("\r\n") for line in f if not line.startswith("#")]
|
||||
while not lines[-1]:
|
||||
lines.pop()
|
||||
verify_message_body(lines, err)
|
||||
else: # Normal operation, pass arguments to git log
|
||||
for sha in git_log("%h", *args):
|
||||
verify(sha, err)
|
||||
|
||||
if err.has_errors or err.has_warnings:
|
||||
if suggestions:
|
||||
print("See https://github.com/micropython/micropython/blob/master/CODECONVENTIONS.md")
|
||||
else:
|
||||
print("ok")
|
||||
if err.has_errors:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def show_help():
|
||||
print("usage: verifygitlog.py [-v -n -h --check-file] ...")
|
||||
print("-v : increase verbosity, can be specified multiple times")
|
||||
print("-n : do not print multi-line suggestions")
|
||||
print("-h : print this help message and exit")
|
||||
print(
|
||||
"--check-file : Pass a single argument which is a file containing a candidate commit message"
|
||||
)
|
||||
print(
|
||||
"--ignore-rebase : Skip checking commits with git rebase autosquash prefixes or WIP as a prefix"
|
||||
)
|
||||
print("... : arguments passed to git log to retrieve commits to verify")
|
||||
print(" see https://www.git-scm.com/docs/git-log")
|
||||
print(" passing no arguments at all will verify all commits")
|
||||
print("examples:")
|
||||
print("verifygitlog.py -n10 # Check last 10 commits")
|
||||
print("verifygitlog.py -v master..HEAD # Check commits since master")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
verbosity = args.count("-v")
|
||||
suggestions = args.count("-n") == 0
|
||||
if "--ignore-rebase" in args:
|
||||
args.remove("--ignore-rebase")
|
||||
ignore_prefixes = ["squash!", "fixup!", "amend!", "WIP"]
|
||||
|
||||
if "-h" in args:
|
||||
show_help()
|
||||
else:
|
||||
args = [arg for arg in args if arg not in ["-v", "-n", "-h"]]
|
||||
run(args)
|
|
@ -1,36 +1,55 @@
|
|||
import sys
|
||||
import ffilib
|
||||
import array
|
||||
import uctypes
|
||||
|
||||
pcre2 = ffilib.open("libpcre2-8")
|
||||
|
||||
# pcre2_code *pcre2_compile(PCRE2_SPTR pattern, PCRE2_SIZE length,
|
||||
# uint32_t options, int *errorcode, PCRE2_SIZE *erroroffset,
|
||||
# pcre2_compile_context *ccontext);
|
||||
pcre2_compile = pcre2.func("p", "pcre2_compile_8", "siippp")
|
||||
|
||||
# int pcre2_match(const pcre2_code *code, PCRE2_SPTR subject,
|
||||
# PCRE2_SIZE length, PCRE2_SIZE startoffset, uint32_t options,
|
||||
# pcre2_match_data *match_data, pcre2_match_context *mcontext);
|
||||
pcre2_match = pcre2.func("i", "pcre2_match_8", "Psiiipp")
|
||||
|
||||
# int pcre2_pattern_info(const pcre2_code *code, uint32_t what,
|
||||
# void *where);
|
||||
pcre2_pattern_info = pcre2.func("i", "pcre2_pattern_info_8", "Pip")
|
||||
|
||||
# PCRE2_SIZE *pcre2_get_ovector_pointer(pcre2_match_data *match_data);
|
||||
pcre2_get_ovector_pointer = pcre2.func("p", "pcre2_get_ovector_pointer_8", "p")
|
||||
|
||||
# pcre2_match_data *pcre2_match_data_create_from_pattern(const pcre2_code *code,
|
||||
# pcre2_general_context *gcontext);
|
||||
pcre2_match_data_create_from_pattern = pcre2.func(
|
||||
"p", "pcre2_match_data_create_from_pattern_8", "Pp"
|
||||
)
|
||||
|
||||
# PCRE2_SIZE that is of type size_t.
|
||||
# Use ULONG as type to support both 32bit and 64bit.
|
||||
PCRE2_SIZE_SIZE = uctypes.sizeof({"field": 0 | uctypes.ULONG})
|
||||
PCRE2_SIZE_TYPE = "L"
|
||||
|
||||
# Real value in pcre2.h is 0xFFFFFFFF for 32bit and
|
||||
# 0x0xFFFFFFFFFFFFFFFF for 64bit that is equivalent
|
||||
# to -1
|
||||
PCRE2_ZERO_TERMINATED = -1
|
||||
|
||||
|
||||
pcre = ffilib.open("libpcre")
|
||||
|
||||
# pcre *pcre_compile(const char *pattern, int options,
|
||||
# const char **errptr, int *erroffset,
|
||||
# const unsigned char *tableptr);
|
||||
pcre_compile = pcre.func("p", "pcre_compile", "sipps")
|
||||
|
||||
# int pcre_exec(const pcre *code, const pcre_extra *extra,
|
||||
# const char *subject, int length, int startoffset,
|
||||
# int options, int *ovector, int ovecsize);
|
||||
pcre_exec = pcre.func("i", "pcre_exec", "PPsiiipi")
|
||||
|
||||
# int pcre_fullinfo(const pcre *code, const pcre_extra *extra,
|
||||
# int what, void *where);
|
||||
pcre_fullinfo = pcre.func("i", "pcre_fullinfo", "PPip")
|
||||
|
||||
|
||||
IGNORECASE = I = 1
|
||||
MULTILINE = M = 2
|
||||
DOTALL = S = 4
|
||||
VERBOSE = X = 8
|
||||
PCRE_ANCHORED = 0x10
|
||||
IGNORECASE = I = 0x8
|
||||
MULTILINE = M = 0x400
|
||||
DOTALL = S = 0x20
|
||||
VERBOSE = X = 0x80
|
||||
PCRE2_ANCHORED = 0x80000000
|
||||
|
||||
# TODO. Note that Python3 has unicode by default
|
||||
ASCII = A = 0
|
||||
UNICODE = U = 0
|
||||
|
||||
PCRE_INFO_CAPTURECOUNT = 2
|
||||
PCRE2_INFO_CAPTURECOUNT = 0x4
|
||||
|
||||
|
||||
class PCREMatch:
|
||||
|
@ -67,19 +86,23 @@ class PCREPattern:
|
|||
def search(self, s, pos=0, endpos=-1, _flags=0):
|
||||
assert endpos == -1, "pos: %d, endpos: %d" % (pos, endpos)
|
||||
buf = array.array("i", [0])
|
||||
pcre_fullinfo(self.obj, None, PCRE_INFO_CAPTURECOUNT, buf)
|
||||
pcre2_pattern_info(self.obj, PCRE2_INFO_CAPTURECOUNT, buf)
|
||||
cap_count = buf[0]
|
||||
ov = array.array("i", [0, 0, 0] * (cap_count + 1))
|
||||
num = pcre_exec(self.obj, None, s, len(s), pos, _flags, ov, len(ov))
|
||||
match_data = pcre2_match_data_create_from_pattern(self.obj, None)
|
||||
num = pcre2_match(self.obj, s, len(s), pos, _flags, match_data, None)
|
||||
if num == -1:
|
||||
# No match
|
||||
return None
|
||||
ov_ptr = pcre2_get_ovector_pointer(match_data)
|
||||
# pcre2_get_ovector_pointer return PCRE2_SIZE
|
||||
ov_buf = uctypes.bytearray_at(ov_ptr, PCRE2_SIZE_SIZE * (cap_count + 1) * 2)
|
||||
ov = array.array(PCRE2_SIZE_TYPE, ov_buf)
|
||||
# We don't care how many matching subexpressions we got, we
|
||||
# care only about total # of capturing ones (including empty)
|
||||
return PCREMatch(s, cap_count + 1, ov)
|
||||
|
||||
def match(self, s, pos=0, endpos=-1):
|
||||
return self.search(s, pos, endpos, PCRE_ANCHORED)
|
||||
return self.search(s, pos, endpos, PCRE2_ANCHORED)
|
||||
|
||||
def sub(self, repl, s, count=0):
|
||||
if not callable(repl):
|
||||
|
@ -141,9 +164,9 @@ class PCREPattern:
|
|||
|
||||
|
||||
def compile(pattern, flags=0):
|
||||
errptr = bytes(4)
|
||||
errcode = bytes(4)
|
||||
erroffset = bytes(4)
|
||||
regex = pcre_compile(pattern, flags, errptr, erroffset, None)
|
||||
regex = pcre2_compile(pattern, PCRE2_ZERO_TERMINATED, flags, errcode, erroffset, None)
|
||||
assert regex
|
||||
return PCREPattern(regex)
|
||||
|
||||
|
@ -154,7 +177,7 @@ def search(pattern, string, flags=0):
|
|||
|
||||
|
||||
def match(pattern, string, flags=0):
|
||||
r = compile(pattern, flags | PCRE_ANCHORED)
|
||||
r = compile(pattern, flags | PCRE2_ANCHORED)
|
||||
return r.search(string)
|
||||
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue