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 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)