libkp 0.1.0__py3-none-any.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.
- libkp/__init__.py +348 -0
- libkp/_broadcast.py +48 -0
- libkp/_generated.py +638 -0
- libkp/_lane.py +111 -0
- libkp/_link.py +155 -0
- libkp/_routes.py +189 -0
- libkp/cbor.py +965 -0
- libkp/control.py +572 -0
- libkp/discovery.py +297 -0
- libkp/errors.py +286 -0
- libkp/midi3.py +122 -0
- libkp/model.py +1456 -0
- libkp/nav.py +152 -0
- libkp/nrpn.py +419 -0
- libkp/params.py +204 -0
- libkp/protocol.py +141 -0
- libkp/registry.py +125 -0
- libkp/session.py +420 -0
- libkp/state.py +1172 -0
- libkp/testing.py +460 -0
- libkp-0.1.0.dist-info/METADATA +440 -0
- libkp-0.1.0.dist-info/RECORD +23 -0
- libkp-0.1.0.dist-info/WHEEL +4 -0
libkp/__init__.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""libkp — the Kemper Profiler network protocol in pure Python.
|
|
2
|
+
|
|
3
|
+
Layers, lowest to highest:
|
|
4
|
+
|
|
5
|
+
- :mod:`libkp.protocol` — the TagStream wire encoding and the discovery poll.
|
|
6
|
+
- :mod:`libkp.discovery` — async UDP broadcast discovery.
|
|
7
|
+
- :mod:`libkp.session` — TCP connect plus the line-based protocol handshake.
|
|
8
|
+
- :mod:`libkp.midi3` — the 4-byte stream framing that carries MIDI over TCP.
|
|
9
|
+
- :mod:`libkp.nrpn` — Kemper NRPN-over-SysEx builders and parsers.
|
|
10
|
+
- :mod:`libkp.control` — the 7-bit CC/PC control vocabulary.
|
|
11
|
+
- :mod:`libkp.params` / :mod:`libkp.registry` — offline name and type lookups.
|
|
12
|
+
- :mod:`libkp.cbor` — the native CBOR control channel: codec and control link.
|
|
13
|
+
- :mod:`libkp.state` — the device-state tree and its pure decode routing.
|
|
14
|
+
- :mod:`libkp.nav` — the Navigator's pure state machine, the one way a rig is
|
|
15
|
+
loaded.
|
|
16
|
+
- :mod:`libkp.model` — :class:`~libkp.model.DeviceModel`, the async store over
|
|
17
|
+
the stream and the control link.
|
|
18
|
+
|
|
19
|
+
Beside the layers, :mod:`libkp.testing` holds :class:`~libkp.testing.FakeDevice`,
|
|
20
|
+
an in-process Profiler for driving all of the above without a device.
|
|
21
|
+
|
|
22
|
+
Constants and lookup tables come from :mod:`libkp._generated`, which is emitted
|
|
23
|
+
from the shared spec; the protocol logic is hand-written here and held to the
|
|
24
|
+
shared conformance vectors.
|
|
25
|
+
|
|
26
|
+
Quick start::
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
from libkp import DeviceModel, find_first
|
|
30
|
+
|
|
31
|
+
async def main():
|
|
32
|
+
reply = await find_first()
|
|
33
|
+
model = await DeviceModel.connect(reply.ip)
|
|
34
|
+
snapshots = model.subscribe()
|
|
35
|
+
state = await snapshots.get()
|
|
36
|
+
print(state.rig.name, state.status.loudness)
|
|
37
|
+
await model.close()
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from ._generated import SPEC_VERSION
|
|
45
|
+
from .cbor import (
|
|
46
|
+
CborSession,
|
|
47
|
+
StateSnapshot,
|
|
48
|
+
extract_snapshot,
|
|
49
|
+
fetch_state_snapshot,
|
|
50
|
+
numeric_values,
|
|
51
|
+
param_write,
|
|
52
|
+
state_dump_request,
|
|
53
|
+
)
|
|
54
|
+
from .control import (
|
|
55
|
+
Control,
|
|
56
|
+
ModuleSlot,
|
|
57
|
+
control_from_op,
|
|
58
|
+
program_change,
|
|
59
|
+
slot_enable_cc,
|
|
60
|
+
)
|
|
61
|
+
from .discovery import DiscoveryOptions, DiscoveryPort, Reply, discover, find_first
|
|
62
|
+
from .errors import (
|
|
63
|
+
ChannelDisconnectedError,
|
|
64
|
+
ChannelError,
|
|
65
|
+
ChannelOffError,
|
|
66
|
+
ChannelSessionError,
|
|
67
|
+
ChannelTooSoonError,
|
|
68
|
+
CommandError,
|
|
69
|
+
ConnectError,
|
|
70
|
+
ConnectionClosedError,
|
|
71
|
+
DisconnectedError,
|
|
72
|
+
DiscoverError,
|
|
73
|
+
FieldOverrunError,
|
|
74
|
+
LibKPError,
|
|
75
|
+
ParseError,
|
|
76
|
+
PortUnavailableError,
|
|
77
|
+
ProtocolRejectedError,
|
|
78
|
+
RequestDisconnectedError,
|
|
79
|
+
RequestError,
|
|
80
|
+
RequestTimeoutError,
|
|
81
|
+
RequestUnreadableError,
|
|
82
|
+
RigLoadRequiresNavigatorError,
|
|
83
|
+
SessionError,
|
|
84
|
+
TimeoutErrorLibKP,
|
|
85
|
+
TooShortError,
|
|
86
|
+
UnknownSlotError,
|
|
87
|
+
)
|
|
88
|
+
from .midi3 import Unframer, frame, is_kemper_sysex
|
|
89
|
+
from .model import (
|
|
90
|
+
Backoff,
|
|
91
|
+
ConnectOptions,
|
|
92
|
+
ControlPolicy,
|
|
93
|
+
DeviceModel,
|
|
94
|
+
ReconnectPolicy,
|
|
95
|
+
SyncStrategy,
|
|
96
|
+
)
|
|
97
|
+
from .nav import (
|
|
98
|
+
Dropped,
|
|
99
|
+
NavAction,
|
|
100
|
+
NavigatorState,
|
|
101
|
+
Send,
|
|
102
|
+
Settled,
|
|
103
|
+
StartSettle,
|
|
104
|
+
StartWindow,
|
|
105
|
+
)
|
|
106
|
+
from .nrpn import (
|
|
107
|
+
NrpnHeader,
|
|
108
|
+
beacon,
|
|
109
|
+
control_change,
|
|
110
|
+
ext_decode,
|
|
111
|
+
ext_encode,
|
|
112
|
+
multi_values,
|
|
113
|
+
parse_extended_string,
|
|
114
|
+
parse_rendered_string,
|
|
115
|
+
request_multi,
|
|
116
|
+
request_rendered_string,
|
|
117
|
+
request_single,
|
|
118
|
+
request_string,
|
|
119
|
+
set_single,
|
|
120
|
+
sysex,
|
|
121
|
+
u14,
|
|
122
|
+
u14_split,
|
|
123
|
+
)
|
|
124
|
+
from .params import (
|
|
125
|
+
EFFECT_SLOTS,
|
|
126
|
+
describe,
|
|
127
|
+
describe_numeric,
|
|
128
|
+
effect_category_name,
|
|
129
|
+
effect_slot_page,
|
|
130
|
+
effect_type_name,
|
|
131
|
+
page_name,
|
|
132
|
+
param_name,
|
|
133
|
+
string_tag_name,
|
|
134
|
+
)
|
|
135
|
+
from .protocol import PORT, TagStream, build_poll_request
|
|
136
|
+
from .registry import ParamDescriptor, ParamKind, descriptor, format_value
|
|
137
|
+
from .session import (
|
|
138
|
+
CONNECTION_COOLDOWN,
|
|
139
|
+
PROTOCOL_CBOR_CONTROL,
|
|
140
|
+
PROTOCOL_MIDI3_STREAM,
|
|
141
|
+
PROTOCOL_REQUEST_RESPONSE,
|
|
142
|
+
HandshakeOutcome,
|
|
143
|
+
Session,
|
|
144
|
+
)
|
|
145
|
+
from .state import (
|
|
146
|
+
Amp,
|
|
147
|
+
ApplyOutcome,
|
|
148
|
+
Bank,
|
|
149
|
+
BankPreview,
|
|
150
|
+
BankSlot,
|
|
151
|
+
BeatPulse,
|
|
152
|
+
Block,
|
|
153
|
+
Cabinet,
|
|
154
|
+
Channel,
|
|
155
|
+
ChannelChanged,
|
|
156
|
+
Channels,
|
|
157
|
+
ChannelState,
|
|
158
|
+
Connected,
|
|
159
|
+
Connection,
|
|
160
|
+
ConnectionChanged,
|
|
161
|
+
CurrentPosition,
|
|
162
|
+
Decoded,
|
|
163
|
+
DeviceEvent,
|
|
164
|
+
DeviceState,
|
|
165
|
+
Disconnected,
|
|
166
|
+
Effect,
|
|
167
|
+
EffectChanged,
|
|
168
|
+
MorphButton,
|
|
169
|
+
MorphChanged,
|
|
170
|
+
NavDrop,
|
|
171
|
+
Navigation,
|
|
172
|
+
NavigationDropped,
|
|
173
|
+
NavigationSettled,
|
|
174
|
+
Num,
|
|
175
|
+
Output,
|
|
176
|
+
ParamChanged,
|
|
177
|
+
Phase,
|
|
178
|
+
RealtimeStatus,
|
|
179
|
+
RenderedString,
|
|
180
|
+
RequestTimedOut,
|
|
181
|
+
Rig,
|
|
182
|
+
RigChanged,
|
|
183
|
+
Status,
|
|
184
|
+
StringTag,
|
|
185
|
+
SyncCompleted,
|
|
186
|
+
TempoBpm,
|
|
187
|
+
Text,
|
|
188
|
+
Tuner,
|
|
189
|
+
TunerDeviance,
|
|
190
|
+
TunerNote,
|
|
191
|
+
Update,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
__version__ = "0.1.0"
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
"SPEC_VERSION",
|
|
198
|
+
"__version__",
|
|
199
|
+
# protocol / transport
|
|
200
|
+
"PORT",
|
|
201
|
+
"TagStream",
|
|
202
|
+
"build_poll_request",
|
|
203
|
+
"DiscoveryOptions",
|
|
204
|
+
"Reply",
|
|
205
|
+
"discover",
|
|
206
|
+
"DiscoveryPort",
|
|
207
|
+
"find_first",
|
|
208
|
+
"Session",
|
|
209
|
+
"HandshakeOutcome",
|
|
210
|
+
"PROTOCOL_MIDI3_STREAM",
|
|
211
|
+
"PROTOCOL_REQUEST_RESPONSE",
|
|
212
|
+
"PROTOCOL_CBOR_CONTROL",
|
|
213
|
+
"CONNECTION_COOLDOWN",
|
|
214
|
+
"Unframer",
|
|
215
|
+
"frame",
|
|
216
|
+
"is_kemper_sysex",
|
|
217
|
+
# messages
|
|
218
|
+
"NrpnHeader",
|
|
219
|
+
"sysex",
|
|
220
|
+
"beacon",
|
|
221
|
+
"u14",
|
|
222
|
+
"u14_split",
|
|
223
|
+
"set_single",
|
|
224
|
+
"request_single",
|
|
225
|
+
"request_multi",
|
|
226
|
+
"request_string",
|
|
227
|
+
"request_rendered_string",
|
|
228
|
+
"control_change",
|
|
229
|
+
"program_change",
|
|
230
|
+
"multi_values",
|
|
231
|
+
"ext_decode",
|
|
232
|
+
"ext_encode",
|
|
233
|
+
"parse_extended_string",
|
|
234
|
+
"parse_rendered_string",
|
|
235
|
+
# control vocabulary
|
|
236
|
+
"Control",
|
|
237
|
+
"ModuleSlot",
|
|
238
|
+
"slot_enable_cc",
|
|
239
|
+
"control_from_op",
|
|
240
|
+
# lookups
|
|
241
|
+
"EFFECT_SLOTS",
|
|
242
|
+
"param_name",
|
|
243
|
+
"page_name",
|
|
244
|
+
"string_tag_name",
|
|
245
|
+
"effect_type_name",
|
|
246
|
+
"effect_category_name",
|
|
247
|
+
"effect_slot_page",
|
|
248
|
+
"describe",
|
|
249
|
+
"describe_numeric",
|
|
250
|
+
"ParamDescriptor",
|
|
251
|
+
"ParamKind",
|
|
252
|
+
"descriptor",
|
|
253
|
+
"format_value",
|
|
254
|
+
# state + model
|
|
255
|
+
"Connection",
|
|
256
|
+
"ChannelState",
|
|
257
|
+
"Channels",
|
|
258
|
+
"Navigation",
|
|
259
|
+
"NavDrop",
|
|
260
|
+
"DeviceState",
|
|
261
|
+
"RealtimeStatus",
|
|
262
|
+
"Rig",
|
|
263
|
+
"Amp",
|
|
264
|
+
"Cabinet",
|
|
265
|
+
"Effect",
|
|
266
|
+
"Tuner",
|
|
267
|
+
"Output",
|
|
268
|
+
"BankSlot",
|
|
269
|
+
"Bank",
|
|
270
|
+
"ApplyOutcome",
|
|
271
|
+
"DeviceEvent",
|
|
272
|
+
"RigChanged",
|
|
273
|
+
"StringTag",
|
|
274
|
+
"BankPreview",
|
|
275
|
+
"EffectChanged",
|
|
276
|
+
"ParamChanged",
|
|
277
|
+
"Status",
|
|
278
|
+
"BeatPulse",
|
|
279
|
+
"TempoBpm",
|
|
280
|
+
"MorphButton",
|
|
281
|
+
"MorphChanged",
|
|
282
|
+
"TunerDeviance",
|
|
283
|
+
"TunerNote",
|
|
284
|
+
"RenderedString",
|
|
285
|
+
"CurrentPosition",
|
|
286
|
+
"NavigationSettled",
|
|
287
|
+
"NavigationDropped",
|
|
288
|
+
"Connected",
|
|
289
|
+
"Disconnected",
|
|
290
|
+
"ConnectionChanged",
|
|
291
|
+
"ChannelChanged",
|
|
292
|
+
"SyncCompleted",
|
|
293
|
+
"RequestTimedOut",
|
|
294
|
+
"Update",
|
|
295
|
+
"Decoded",
|
|
296
|
+
"Num",
|
|
297
|
+
"Text",
|
|
298
|
+
"Block",
|
|
299
|
+
"Channel",
|
|
300
|
+
"Phase",
|
|
301
|
+
"DeviceModel",
|
|
302
|
+
"ConnectOptions",
|
|
303
|
+
"ControlPolicy",
|
|
304
|
+
"SyncStrategy",
|
|
305
|
+
"ReconnectPolicy",
|
|
306
|
+
"Backoff",
|
|
307
|
+
# the Navigator's state machine
|
|
308
|
+
"NavigatorState",
|
|
309
|
+
"NavAction",
|
|
310
|
+
"Send",
|
|
311
|
+
"StartSettle",
|
|
312
|
+
"StartWindow",
|
|
313
|
+
"Settled",
|
|
314
|
+
"Dropped",
|
|
315
|
+
# cbor state-dump snapshot
|
|
316
|
+
"CborSession",
|
|
317
|
+
"StateSnapshot",
|
|
318
|
+
"extract_snapshot",
|
|
319
|
+
"numeric_values",
|
|
320
|
+
"fetch_state_snapshot",
|
|
321
|
+
"param_write",
|
|
322
|
+
"state_dump_request",
|
|
323
|
+
# errors
|
|
324
|
+
"LibKPError",
|
|
325
|
+
"ParseError",
|
|
326
|
+
"PortUnavailableError",
|
|
327
|
+
"TooShortError",
|
|
328
|
+
"FieldOverrunError",
|
|
329
|
+
"DiscoverError",
|
|
330
|
+
"SessionError",
|
|
331
|
+
"ConnectError",
|
|
332
|
+
"ConnectionClosedError",
|
|
333
|
+
"ProtocolRejectedError",
|
|
334
|
+
"TimeoutErrorLibKP",
|
|
335
|
+
"CommandError",
|
|
336
|
+
"DisconnectedError",
|
|
337
|
+
"UnknownSlotError",
|
|
338
|
+
"RigLoadRequiresNavigatorError",
|
|
339
|
+
"RequestError",
|
|
340
|
+
"RequestDisconnectedError",
|
|
341
|
+
"RequestTimeoutError",
|
|
342
|
+
"RequestUnreadableError",
|
|
343
|
+
"ChannelError",
|
|
344
|
+
"ChannelOffError",
|
|
345
|
+
"ChannelTooSoonError",
|
|
346
|
+
"ChannelDisconnectedError",
|
|
347
|
+
"ChannelSessionError",
|
|
348
|
+
]
|
libkp/_broadcast.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Fan-out of values to per-subscriber queues, shared by the two transports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
|
|
8
|
+
#: Depth of each subscriber queue before the oldest item is dropped.
|
|
9
|
+
QUEUE_DEPTH: int = 256
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Broadcast:
|
|
13
|
+
"""Fan-out of values to per-subscriber queues, dropping the oldest on overflow.
|
|
14
|
+
|
|
15
|
+
A slow consumer never blocks the ingest task; for snapshots, dropping an
|
|
16
|
+
intermediate value loses nothing because the latest is always complete.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("_queues",)
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._queues: list[asyncio.Queue] = []
|
|
23
|
+
|
|
24
|
+
def subscribe(self, maxsize: int = QUEUE_DEPTH) -> asyncio.Queue:
|
|
25
|
+
"""A fresh queue fed by every future :meth:`send`. ``maxsize=0`` is
|
|
26
|
+
unbounded, for the one subscriber that must see every value -- the
|
|
27
|
+
state-dump replay in :meth:`libkp.cbor.CborSession.updates` -- rather
|
|
28
|
+
than only the latest."""
|
|
29
|
+
queue: asyncio.Queue = asyncio.Queue(maxsize=maxsize)
|
|
30
|
+
self._queues.append(queue)
|
|
31
|
+
return queue
|
|
32
|
+
|
|
33
|
+
def empty(self) -> bool:
|
|
34
|
+
"""True while nothing is subscribed, so a caller can buffer instead of
|
|
35
|
+
sending into a void."""
|
|
36
|
+
return not self._queues
|
|
37
|
+
|
|
38
|
+
def unsubscribe(self, queue: asyncio.Queue) -> None:
|
|
39
|
+
with contextlib.suppress(ValueError):
|
|
40
|
+
self._queues.remove(queue)
|
|
41
|
+
|
|
42
|
+
def send(self, value: object) -> None:
|
|
43
|
+
for queue in self._queues:
|
|
44
|
+
if queue.full():
|
|
45
|
+
with contextlib.suppress(asyncio.QueueEmpty):
|
|
46
|
+
queue.get_nowait()
|
|
47
|
+
with contextlib.suppress(asyncio.QueueFull):
|
|
48
|
+
queue.put_nowait(value)
|