uclog 4__py3-none-win_amd64.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 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))
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