uclog 4__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uclog/__init__.py +35 -0
- uclog/aio.py +102 -0
- uclog/bin/uclog-hub.exe +0 -0
- uclog/cbor.py +160 -0
- uclog/connection.py +677 -0
- uclog/errors.py +53 -0
- uclog/hub.py +490 -0
- uclog/logdata.py +381 -0
- uclog/proto.py +180 -0
- uclog/py.typed +0 -0
- uclog/resolver.py +161 -0
- uclog/semihost.py +296 -0
- uclog-4.dist-info/METADATA +215 -0
- uclog-4.dist-info/RECORD +16 -0
- uclog-4.dist-info/WHEEL +5 -0
- uclog-4.dist-info/top_level.txt +1 -0
uclog/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# © 2026 Unit Circle Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""ucLog for scripts: connect to a device through the ucLog hub, send and receive on any port,
|
|
5
|
+
and decode logs.
|
|
6
|
+
|
|
7
|
+
import uclog
|
|
8
|
+
|
|
9
|
+
with uclog.connect(logdata="build/zephyr/zephyr.elf") as dev:
|
|
10
|
+
print(dev.device_info)
|
|
11
|
+
dev.send(61, b"version s\\r")
|
|
12
|
+
print(dev.recv(61, timeout=1.0))
|
|
13
|
+
for rec in dev.logs(timeout=5.0):
|
|
14
|
+
print(rec)
|
|
15
|
+
|
|
16
|
+
All device I/O goes through the native hub (`uclog-hub`); a hub is started when none is running
|
|
17
|
+
and stopped when the script exits.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import aio, cbor, proto
|
|
21
|
+
from .connection import Connection, connect
|
|
22
|
+
from .errors import (ApprovalTimeoutError, ConnectionClosedError, DeviceNotRespondingError,
|
|
23
|
+
HubBusyError, HubStartError, MultipleHubsError, NoHubError, UclogError)
|
|
24
|
+
from .hub import DeviceInfo, HubInfo, list_devices, list_hubs, start_hub, stop_hub
|
|
25
|
+
from .logdata import DecodeError, LogData, LogDataError, LogRecord
|
|
26
|
+
from .resolver import Resolver
|
|
27
|
+
|
|
28
|
+
__version__ = '4'
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'connect', 'Connection', 'list_hubs', 'list_devices', 'start_hub', 'stop_hub', 'HubInfo',
|
|
32
|
+
'DeviceInfo', 'LogData', 'LogRecord', 'Resolver', 'DecodeError', 'LogDataError', 'UclogError',
|
|
33
|
+
'NoHubError', 'MultipleHubsError', 'HubBusyError', 'ApprovalTimeoutError',
|
|
34
|
+
'DeviceNotRespondingError', 'HubStartError', 'ConnectionClosedError', 'aio', 'cbor', 'proto',
|
|
35
|
+
]
|
uclog/aio.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# © 2026 Unit Circle Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""asyncio API: the same calls as the sync one, awaited.
|
|
5
|
+
|
|
6
|
+
async with await uclog.aio.connect(logdata="build/zephyr/zephyr.elf") as dev:
|
|
7
|
+
await dev.send(61, b"version s\\r")
|
|
8
|
+
print(await dev.recv(61, timeout=1.0))
|
|
9
|
+
async for rec in dev.logs(timeout=5.0):
|
|
10
|
+
print(rec)
|
|
11
|
+
|
|
12
|
+
The connection's I/O runs on its own threads (see `uclog.Connection`); waits run in the
|
|
13
|
+
default executor so the event loop is never blocked.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import functools
|
|
18
|
+
from typing import AsyncIterator, Optional
|
|
19
|
+
|
|
20
|
+
from . import connection
|
|
21
|
+
from .logdata import LogRecord
|
|
22
|
+
|
|
23
|
+
__all__ = ['connect', 'Connection']
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def _run(fn, *args, **kw):
|
|
27
|
+
loop = asyncio.get_running_loop()
|
|
28
|
+
return await loop.run_in_executor(None, functools.partial(fn, *args, **kw))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Connection:
|
|
32
|
+
"""Async wrapper of `uclog.Connection`. `sync` is the wrapped connection."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, sync: connection.Connection):
|
|
35
|
+
self.sync = sync
|
|
36
|
+
|
|
37
|
+
async def __aenter__(self):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
async def __aexit__(self, *exc):
|
|
41
|
+
await self.close()
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def alive(self) -> bool:
|
|
46
|
+
return self.sync.alive
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def closed(self) -> bool:
|
|
50
|
+
return self.sync.closed
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def dropped(self):
|
|
54
|
+
return self.sync.dropped
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def notes(self):
|
|
58
|
+
return self.sync.notes
|
|
59
|
+
|
|
60
|
+
async def device_info(self, timeout: Optional[float] = None) -> Optional[dict]:
|
|
61
|
+
"""The port-62 map. Without `timeout`, waits as long as the sync `device_info` property."""
|
|
62
|
+
return await _run(self.sync.wait_device_info,
|
|
63
|
+
self.sync._info_timeout() if timeout is None else timeout)
|
|
64
|
+
|
|
65
|
+
async def send(self, port: int, data) -> None:
|
|
66
|
+
self.sync.send(port, data)
|
|
67
|
+
|
|
68
|
+
async def send_frame(self, payload: bytes) -> None:
|
|
69
|
+
self.sync.send_frame(payload)
|
|
70
|
+
|
|
71
|
+
async def recv(self, port: int, timeout: Optional[float] = None) -> Optional[bytes]:
|
|
72
|
+
return await _run(self.sync.recv, port, timeout)
|
|
73
|
+
|
|
74
|
+
async def console(self, timeout: Optional[float] = None):
|
|
75
|
+
return await _run(self.sync.console, timeout)
|
|
76
|
+
|
|
77
|
+
async def command(self, obj: dict, timeout: float = 2.0):
|
|
78
|
+
return await _run(self.sync.command, obj, timeout)
|
|
79
|
+
|
|
80
|
+
async def logs(self, timeout: Optional[float] = None, duration: Optional[float] = None) -> AsyncIterator[LogRecord]:
|
|
81
|
+
it = self.sync.logs(timeout, duration)
|
|
82
|
+
while True:
|
|
83
|
+
r = await _run(next, it, None)
|
|
84
|
+
if r is None:
|
|
85
|
+
return
|
|
86
|
+
yield r
|
|
87
|
+
|
|
88
|
+
async def frames(self, timeout: Optional[float] = None, duration: Optional[float] = None) -> AsyncIterator[bytes]:
|
|
89
|
+
it = self.sync.frames(timeout, duration)
|
|
90
|
+
while True:
|
|
91
|
+
r = await _run(next, it, None)
|
|
92
|
+
if r is None:
|
|
93
|
+
return
|
|
94
|
+
yield r
|
|
95
|
+
|
|
96
|
+
async def close(self) -> None:
|
|
97
|
+
await _run(self.sync.close)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def connect(*args, **kw) -> Connection:
|
|
101
|
+
"""Same arguments as `uclog.connect()`."""
|
|
102
|
+
return Connection(await _run(connection.connect, *args, **kw))
|
uclog/bin/uclog-hub.exe
ADDED
|
Binary file
|
uclog/cbor.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# © 2026 Unit Circle Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Minimal CBOR (RFC 8949): enough for every payload in the ucLog protocol.
|
|
5
|
+
|
|
6
|
+
From spec/reference/ucref.py. Standard library only, so the package has no dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
__all__ = ['dumps', 'loads', 'CborError']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CborError(ValueError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _head(major, n):
|
|
19
|
+
if n < 24:
|
|
20
|
+
return bytes(((major << 5) | n,))
|
|
21
|
+
for ai, fmt in ((24, '>B'), (25, '>H'), (26, '>I'), (27, '>Q')):
|
|
22
|
+
if n < (1 << (8 * struct.calcsize(fmt))):
|
|
23
|
+
return bytes(((major << 5) | ai,)) + struct.pack(fmt, n)
|
|
24
|
+
raise ValueError('integer too large for CBOR')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def dumps(v) -> bytes:
|
|
28
|
+
"""Encode: shortest heads, map keys in insertion order, floats as float64."""
|
|
29
|
+
if v is None:
|
|
30
|
+
return b'\xf6'
|
|
31
|
+
if v is True:
|
|
32
|
+
return b'\xf5'
|
|
33
|
+
if v is False:
|
|
34
|
+
return b'\xf4'
|
|
35
|
+
if isinstance(v, int):
|
|
36
|
+
return _head(0, v) if v >= 0 else _head(1, -1 - v)
|
|
37
|
+
if isinstance(v, float):
|
|
38
|
+
return b'\xfb' + struct.pack('>d', v)
|
|
39
|
+
if isinstance(v, (bytes, bytearray, memoryview)):
|
|
40
|
+
return _head(2, len(v)) + bytes(v)
|
|
41
|
+
if isinstance(v, str):
|
|
42
|
+
b = v.encode('utf-8')
|
|
43
|
+
return _head(3, len(b)) + b
|
|
44
|
+
if isinstance(v, (list, tuple)):
|
|
45
|
+
return _head(4, len(v)) + b''.join(dumps(x) for x in v)
|
|
46
|
+
if isinstance(v, dict):
|
|
47
|
+
return _head(5, len(v)) + b''.join(dumps(k) + dumps(x) for k, x in v.items())
|
|
48
|
+
raise TypeError(f'cannot encode {type(v).__name__} as CBOR')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
MAX_DEPTH = 64 # nesting limit for arrays, maps and tags
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def loads(data: bytes):
|
|
55
|
+
"""Decode one item. Array map keys become tuples; tags are ignored. Any malformed input
|
|
56
|
+
raises CborError (and nothing else)."""
|
|
57
|
+
try:
|
|
58
|
+
d = memoryview(bytes(data))
|
|
59
|
+
except TypeError as e:
|
|
60
|
+
raise CborError(f'not bytes: {e}') from None
|
|
61
|
+
v, i = _item(d, 0, 0)
|
|
62
|
+
if i != len(d):
|
|
63
|
+
raise CborError(f'{len(d) - i} trailing bytes')
|
|
64
|
+
return v
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _freeze(k):
|
|
68
|
+
if isinstance(k, list):
|
|
69
|
+
return tuple(_freeze(x) for x in k)
|
|
70
|
+
return k
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _key(k):
|
|
74
|
+
k = _freeze(k)
|
|
75
|
+
try:
|
|
76
|
+
hash(k)
|
|
77
|
+
except TypeError:
|
|
78
|
+
raise CborError(f'unsupported map key type {type(k).__name__}') from None
|
|
79
|
+
return k
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _byte(d, i):
|
|
83
|
+
if i >= len(d):
|
|
84
|
+
raise CborError('truncated')
|
|
85
|
+
return d[i]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _item(d, i, depth):
|
|
89
|
+
if depth > MAX_DEPTH:
|
|
90
|
+
raise CborError(f'nested more than {MAX_DEPTH} deep')
|
|
91
|
+
ib = _byte(d, i)
|
|
92
|
+
major, ai = ib >> 5, ib & 31
|
|
93
|
+
i += 1
|
|
94
|
+
if major == 7:
|
|
95
|
+
if ai == 20:
|
|
96
|
+
return False, i
|
|
97
|
+
if ai == 21:
|
|
98
|
+
return True, i
|
|
99
|
+
if ai in (22, 23):
|
|
100
|
+
return None, i
|
|
101
|
+
size = {25: 2, 26: 4, 27: 8}.get(ai)
|
|
102
|
+
if size is None:
|
|
103
|
+
raise CborError(f'unsupported simple value {ai}')
|
|
104
|
+
if i + size > len(d):
|
|
105
|
+
raise CborError('truncated')
|
|
106
|
+
return struct.unpack({2: '>e', 4: '>f', 8: '>d'}[size], d[i:i + size])[0], i + size
|
|
107
|
+
if ai < 24:
|
|
108
|
+
n = ai
|
|
109
|
+
elif ai in (24, 25, 26, 27):
|
|
110
|
+
size = 1 << (ai - 24)
|
|
111
|
+
if i + size > len(d):
|
|
112
|
+
raise CborError('truncated')
|
|
113
|
+
n = int.from_bytes(d[i:i + size], 'big')
|
|
114
|
+
i += size
|
|
115
|
+
elif ai == 31 and major in (2, 3, 4, 5):
|
|
116
|
+
n = None # indefinite length
|
|
117
|
+
else:
|
|
118
|
+
raise CborError(f'bad additional info {ai}')
|
|
119
|
+
if major == 0:
|
|
120
|
+
return n, i
|
|
121
|
+
if major == 1:
|
|
122
|
+
return -1 - n, i
|
|
123
|
+
if major in (2, 3):
|
|
124
|
+
if n is None:
|
|
125
|
+
# Chunks are definite-length strings of the same major type (RFC 8949 §3.2.3).
|
|
126
|
+
parts = []
|
|
127
|
+
while _byte(d, i) != 0xFF:
|
|
128
|
+
if d[i] >> 5 != major or d[i] & 31 == 31:
|
|
129
|
+
raise CborError('bad chunk in an indefinite-length string')
|
|
130
|
+
p, i = _item(d, i, depth + 1)
|
|
131
|
+
parts.append(p)
|
|
132
|
+
i += 1
|
|
133
|
+
return (b''.join(parts) if major == 2 else ''.join(parts)), i
|
|
134
|
+
if i + n > len(d):
|
|
135
|
+
raise CborError('truncated')
|
|
136
|
+
raw = bytes(d[i:i + n])
|
|
137
|
+
try:
|
|
138
|
+
return (raw if major == 2 else raw.decode('utf-8')), i + n
|
|
139
|
+
except UnicodeDecodeError as e:
|
|
140
|
+
raise CborError(f'invalid UTF-8 in text: {e}') from None
|
|
141
|
+
if major == 4:
|
|
142
|
+
out = []
|
|
143
|
+
while (n is None and _byte(d, i) != 0xFF) or (n is not None and len(out) < n):
|
|
144
|
+
x, i = _item(d, i, depth + 1)
|
|
145
|
+
out.append(x)
|
|
146
|
+
if n is None:
|
|
147
|
+
i += 1
|
|
148
|
+
return out, i
|
|
149
|
+
if major == 5:
|
|
150
|
+
out = {}
|
|
151
|
+
count = 0
|
|
152
|
+
while (n is None and _byte(d, i) != 0xFF) or (n is not None and count < n):
|
|
153
|
+
k, i = _item(d, i, depth + 1)
|
|
154
|
+
x, i = _item(d, i, depth + 1)
|
|
155
|
+
out[_key(k)] = x
|
|
156
|
+
count += 1
|
|
157
|
+
if n is None:
|
|
158
|
+
i += 1
|
|
159
|
+
return out, i
|
|
160
|
+
return _item(d, i, depth + 1) # major 6: tag, ignored
|