sdrnative 0.1.0__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.
- sdrnative/__init__.py +146 -0
- sdrnative/__main__.py +33 -0
- sdrnative/_native.py +1514 -0
- sdrnative/py.typed +0 -0
- sdrnative/sdrnative.dll +0 -0
- sdrnative/sdrnative.lib +0 -0
- sdrnative-0.1.0.dist-info/DELVEWHEEL +2 -0
- sdrnative-0.1.0.dist-info/METADATA +99 -0
- sdrnative-0.1.0.dist-info/RECORD +15 -0
- sdrnative-0.1.0.dist-info/WHEEL +5 -0
- sdrnative-0.1.0.dist-info/licenses/LICENSE +373 -0
- sdrnative-0.1.0.dist-info/licenses/NOTICE +13 -0
- sdrnative-0.1.0.dist-info/licenses/licenses/libusb-LGPL-2.1.txt +501 -0
- sdrnative.libs/.load-order-sdrnative-0.1.0 +1 -0
- sdrnative.libs/libusb-1.0.dll +0 -0
sdrnative/_native.py
ADDED
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
# SDRNative
|
|
2
|
+
# Python native binding
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2026 Ruben Roy
|
|
5
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ctypes as C
|
|
9
|
+
import atexit
|
|
10
|
+
from contextlib import ExitStack
|
|
11
|
+
from importlib.resources import as_file, files
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
from typing import Iterable, Iterator
|
|
17
|
+
from enum import IntEnum, IntFlag
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorDomain(IntEnum):
|
|
21
|
+
NONE = 0
|
|
22
|
+
ARGUMENT = 1
|
|
23
|
+
STATE = 2
|
|
24
|
+
USB = 3
|
|
25
|
+
TUNER = 4
|
|
26
|
+
DEMOD = 5
|
|
27
|
+
STREAM = 6
|
|
28
|
+
INTERNAL = 7
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ErrorFlags(IntFlag):
|
|
32
|
+
NONE = 0
|
|
33
|
+
TERMINAL_STREAM = 1 << 0
|
|
34
|
+
DEVICE_GONE = 1 << 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TunerFamily(IntEnum):
|
|
38
|
+
UNKNOWN = 0
|
|
39
|
+
R820T_FAMILY = 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DemodFamily(IntEnum):
|
|
43
|
+
UNKNOWN = 0
|
|
44
|
+
RTL2832 = 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncOverflowPolicy(IntEnum):
|
|
48
|
+
DROP_OLDEST = 0
|
|
49
|
+
DROP_NEWEST = 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
ASYNC_MIN_TRANSFER_SIZE = 4 * 1024
|
|
53
|
+
ASYNC_MAX_TRANSFER_SIZE = 1024 * 1024
|
|
54
|
+
ASYNC_MIN_TRANSFER_COUNT = 2
|
|
55
|
+
ASYNC_MAX_TRANSFER_COUNT = 32
|
|
56
|
+
ASYNC_MIN_QUEUE_DEPTH = 2
|
|
57
|
+
ASYNC_MAX_QUEUE_DEPTH = 128
|
|
58
|
+
ASYNC_MAX_QUEUE_BYTES = 64 * 1024 * 1024
|
|
59
|
+
ASYNC_MAX_TRANSFER_POOL_BYTES = 32 * 1024 * 1024
|
|
60
|
+
ASYNC_DEFAULT_TRANSFER_SIZE = 64 * 1024
|
|
61
|
+
ASYNC_DEFAULT_TRANSFER_COUNT = 8
|
|
62
|
+
ASYNC_DEFAULT_QUEUE_DEPTH = 16
|
|
63
|
+
ASYNC_WAIT_INFINITE = (1 << 32) - 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class ErrorInfo:
|
|
68
|
+
status: int
|
|
69
|
+
domain: ErrorDomain
|
|
70
|
+
native_code: int
|
|
71
|
+
flags: ErrorFlags
|
|
72
|
+
message: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SDRNativeError(RuntimeError):
|
|
76
|
+
def __init__(self, status: int, message: str, info: ErrorInfo | None = None):
|
|
77
|
+
super().__init__(message)
|
|
78
|
+
self.status = status
|
|
79
|
+
self.info = info
|
|
80
|
+
self.domain = info.domain if info else ErrorDomain.NONE
|
|
81
|
+
self.native_code = info.native_code if info else 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SDRNativeArgumentError(SDRNativeError):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SDRNativeStateError(SDRNativeError):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SDRNativeUSBError(SDRNativeError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SDRNativeTunerError(SDRNativeError):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SDRNativeDemodError(SDRNativeError):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SDRNativeStreamError(SDRNativeError):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SDRNativeInternalError(SDRNativeError):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _DeviceInfoC(C.Structure):
|
|
113
|
+
_fields_ = [
|
|
114
|
+
("vid", C.c_uint16),
|
|
115
|
+
("pid", C.c_uint16),
|
|
116
|
+
("bus", C.c_uint8),
|
|
117
|
+
("address", C.c_uint8),
|
|
118
|
+
("manufacturer", C.c_char * 128),
|
|
119
|
+
("product", C.c_char * 128),
|
|
120
|
+
("serial", C.c_char * 128),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _CapabilitiesC(C.Structure):
|
|
125
|
+
_fields_ = [
|
|
126
|
+
("flags", C.c_uint32),
|
|
127
|
+
("dvbt_bandwidth_mask", C.c_uint32),
|
|
128
|
+
("max_pid_filters", C.c_uint32),
|
|
129
|
+
("tuner_xtal_hz", C.c_uint32),
|
|
130
|
+
("demod_xtal_hz", C.c_uint32),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _FrequencyRangeC(C.Structure):
|
|
135
|
+
_fields_ = [("min_hz", C.c_uint64), ("max_hz", C.c_uint64)]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _RateRangeC(C.Structure):
|
|
139
|
+
_fields_ = [("min_hz", C.c_uint32), ("max_hz", C.c_uint32)]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _DeviceProfileC(C.Structure):
|
|
143
|
+
_fields_ = [
|
|
144
|
+
("profile_version", C.c_uint32),
|
|
145
|
+
("feature_flags", C.c_uint32),
|
|
146
|
+
("tuner_family", C.c_int32),
|
|
147
|
+
("demod_family", C.c_int32),
|
|
148
|
+
("max_pid_filters", C.c_uint32),
|
|
149
|
+
("tuner_xtal_hz", C.c_uint32),
|
|
150
|
+
("demod_xtal_hz", C.c_uint32),
|
|
151
|
+
("rf_range_count", C.c_uint32),
|
|
152
|
+
("rf_ranges", _FrequencyRangeC * 4),
|
|
153
|
+
("iq_sample_rate_range_count", C.c_uint32),
|
|
154
|
+
("iq_sample_rate_ranges", _RateRangeC * 4),
|
|
155
|
+
("iq_bandwidth_range_count", C.c_uint32),
|
|
156
|
+
("iq_bandwidth_ranges", _RateRangeC * 4),
|
|
157
|
+
("dvbt_bandwidth_count", C.c_uint32),
|
|
158
|
+
("dvbt_bandwidth_hz", C.c_uint32 * 8),
|
|
159
|
+
("reserved", C.c_uint32 * 8),
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _TuneInfoC(C.Structure):
|
|
164
|
+
_fields_ = [
|
|
165
|
+
("rf_hz", C.c_uint64),
|
|
166
|
+
("bandwidth_hz", C.c_uint32),
|
|
167
|
+
("if_hz", C.c_uint32),
|
|
168
|
+
("lo_hz", C.c_uint64),
|
|
169
|
+
("tuner_pll_locked", C.c_uint8),
|
|
170
|
+
("filter_cal_code", C.c_uint8),
|
|
171
|
+
("reserved", C.c_uint8 * 6),
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _IQTuneInfoC(C.Structure):
|
|
176
|
+
_fields_ = [
|
|
177
|
+
("rf_hz", C.c_uint64),
|
|
178
|
+
("requested_sample_rate_hz", C.c_uint32),
|
|
179
|
+
("actual_sample_rate_hz", C.c_uint32),
|
|
180
|
+
("bandwidth_hz", C.c_uint32),
|
|
181
|
+
("if_hz", C.c_uint32),
|
|
182
|
+
("lo_hz", C.c_uint64),
|
|
183
|
+
("tuner_pll_locked", C.c_uint8),
|
|
184
|
+
("reserved", C.c_uint8 * 7),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class _StreamInfoC(C.Structure):
|
|
189
|
+
_fields_ = [
|
|
190
|
+
("format", C.c_int),
|
|
191
|
+
("sample_rate_hz", C.c_uint32),
|
|
192
|
+
("packet_size_bytes", C.c_uint32),
|
|
193
|
+
("bytes_per_complex_sample", C.c_uint32),
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class _SignalQualityC(C.Structure):
|
|
198
|
+
_fields_ = [
|
|
199
|
+
("fsm_stage", C.c_uint8),
|
|
200
|
+
("snr_valid", C.c_uint8),
|
|
201
|
+
("ber_valid", C.c_uint8),
|
|
202
|
+
("reserved0", C.c_uint8),
|
|
203
|
+
("strength_percent", C.c_int32),
|
|
204
|
+
("snr_db", C.c_double),
|
|
205
|
+
("ber", C.c_uint32),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _USBStatsC(C.Structure):
|
|
210
|
+
_fields_ = [
|
|
211
|
+
("control_transfers", C.c_uint64),
|
|
212
|
+
("control_errors", C.c_uint64),
|
|
213
|
+
("control_pipe_events", C.c_uint64),
|
|
214
|
+
("session_recovery_attempts", C.c_uint64),
|
|
215
|
+
("session_recovery_successes", C.c_uint64),
|
|
216
|
+
("session_recovery_failures", C.c_uint64),
|
|
217
|
+
("bulk_reads", C.c_uint64),
|
|
218
|
+
("bulk_timeouts", C.c_uint64),
|
|
219
|
+
("bulk_errors", C.c_uint64),
|
|
220
|
+
("bulk_bytes", C.c_uint64),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class _USBRecoveryStatsC(C.Structure):
|
|
225
|
+
_fields_ = [
|
|
226
|
+
("struct_size", C.c_uint32),
|
|
227
|
+
("stats_version", C.c_uint32),
|
|
228
|
+
("control_pipe_events", C.c_uint64),
|
|
229
|
+
("session_reopen_attempts", C.c_uint64),
|
|
230
|
+
("session_reopen_successes", C.c_uint64),
|
|
231
|
+
("session_reopen_failures", C.c_uint64),
|
|
232
|
+
("transaction_retry_attempts", C.c_uint64),
|
|
233
|
+
("transaction_retry_successes", C.c_uint64),
|
|
234
|
+
("transaction_retry_failures", C.c_uint64),
|
|
235
|
+
("reserved", C.c_uint64 * 4),
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _AsyncConfigC(C.Structure):
|
|
240
|
+
_fields_ = [
|
|
241
|
+
("struct_size", C.c_uint32),
|
|
242
|
+
("config_version", C.c_uint32),
|
|
243
|
+
("transfer_size_bytes", C.c_uint32),
|
|
244
|
+
("transfer_count", C.c_uint32),
|
|
245
|
+
("queue_depth", C.c_uint32),
|
|
246
|
+
("transfer_timeout_ms", C.c_uint32),
|
|
247
|
+
("overflow_policy", C.c_int32),
|
|
248
|
+
("reserved", C.c_uint32 * 9),
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class _AsyncStatsC(C.Structure):
|
|
253
|
+
_fields_ = [
|
|
254
|
+
("struct_size", C.c_uint32),
|
|
255
|
+
("stats_version", C.c_uint32),
|
|
256
|
+
("running", C.c_uint32),
|
|
257
|
+
("queue_current", C.c_uint32),
|
|
258
|
+
("queue_high_water", C.c_uint32),
|
|
259
|
+
("reserved0", C.c_uint32),
|
|
260
|
+
("transfer_callbacks", C.c_uint64),
|
|
261
|
+
("transfers_completed", C.c_uint64),
|
|
262
|
+
("transfer_bytes", C.c_uint64),
|
|
263
|
+
("transfer_timeouts", C.c_uint64),
|
|
264
|
+
("transfer_errors", C.c_uint64),
|
|
265
|
+
("buffers_enqueued", C.c_uint64),
|
|
266
|
+
("buffers_dequeued", C.c_uint64),
|
|
267
|
+
("buffers_dropped_oldest", C.c_uint64),
|
|
268
|
+
("buffers_dropped_newest", C.c_uint64),
|
|
269
|
+
("consumer_reads", C.c_uint64),
|
|
270
|
+
("consumer_bytes", C.c_uint64),
|
|
271
|
+
("buffers_discarded_on_stop", C.c_uint64),
|
|
272
|
+
("terminal_status", C.c_int32),
|
|
273
|
+
("terminal_native_code", C.c_int32),
|
|
274
|
+
("reserved", C.c_uint64 * 4),
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class _ErrorInfoC(C.Structure):
|
|
279
|
+
_fields_ = [
|
|
280
|
+
("status", C.c_int32),
|
|
281
|
+
("domain", C.c_int32),
|
|
282
|
+
("native_code", C.c_int32),
|
|
283
|
+
("flags", C.c_uint32),
|
|
284
|
+
("reserved", C.c_uint32 * 4),
|
|
285
|
+
("message", C.c_char * 256),
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class _ABIInfoC(C.Structure):
|
|
290
|
+
_fields_ = [
|
|
291
|
+
("struct_size", C.c_uint32),
|
|
292
|
+
("info_version", C.c_uint32),
|
|
293
|
+
("abi_version", C.c_uint32),
|
|
294
|
+
("flags", C.c_uint32),
|
|
295
|
+
("pointer_size", C.c_uint32),
|
|
296
|
+
("status_size", C.c_uint32),
|
|
297
|
+
("log_level_size", C.c_uint32),
|
|
298
|
+
("mode_size", C.c_uint32),
|
|
299
|
+
("state_size", C.c_uint32),
|
|
300
|
+
("error_domain_size", C.c_uint32),
|
|
301
|
+
("stream_format_size", C.c_uint32),
|
|
302
|
+
("tuner_family_size", C.c_uint32),
|
|
303
|
+
("demod_family_size", C.c_uint32),
|
|
304
|
+
("device_info_size", C.c_uint32),
|
|
305
|
+
("device_info_align", C.c_uint32),
|
|
306
|
+
("capabilities_size", C.c_uint32),
|
|
307
|
+
("capabilities_align", C.c_uint32),
|
|
308
|
+
("error_info_size", C.c_uint32),
|
|
309
|
+
("error_info_align", C.c_uint32),
|
|
310
|
+
("frequency_range_size", C.c_uint32),
|
|
311
|
+
("frequency_range_align", C.c_uint32),
|
|
312
|
+
("rate_range_size", C.c_uint32),
|
|
313
|
+
("rate_range_align", C.c_uint32),
|
|
314
|
+
("device_profile_size", C.c_uint32),
|
|
315
|
+
("device_profile_align", C.c_uint32),
|
|
316
|
+
("dvbt_tune_info_size", C.c_uint32),
|
|
317
|
+
("dvbt_tune_info_align", C.c_uint32),
|
|
318
|
+
("iq_tune_info_size", C.c_uint32),
|
|
319
|
+
("iq_tune_info_align", C.c_uint32),
|
|
320
|
+
("stream_info_size", C.c_uint32),
|
|
321
|
+
("stream_info_align", C.c_uint32),
|
|
322
|
+
("signal_quality_size", C.c_uint32),
|
|
323
|
+
("signal_quality_align", C.c_uint32),
|
|
324
|
+
("usb_stats_size", C.c_uint32),
|
|
325
|
+
("usb_stats_align", C.c_uint32),
|
|
326
|
+
("reserved", C.c_uint32 * 8),
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _text(raw: bytes) -> str:
|
|
331
|
+
return raw.split(b"\0", 1)[0].decode("utf-8", "replace")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass(frozen=True)
|
|
335
|
+
class ABIInfo:
|
|
336
|
+
abi_version: int
|
|
337
|
+
info_version: int
|
|
338
|
+
flags: int
|
|
339
|
+
pointer_size: int
|
|
340
|
+
device_profile_size: int
|
|
341
|
+
error_info_size: int
|
|
342
|
+
usb_stats_size: int
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass(frozen=True)
|
|
346
|
+
class DeviceInfo:
|
|
347
|
+
vid: int
|
|
348
|
+
pid: int
|
|
349
|
+
bus: int
|
|
350
|
+
address: int
|
|
351
|
+
manufacturer: str
|
|
352
|
+
product: str
|
|
353
|
+
serial: str
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass(frozen=True)
|
|
357
|
+
class Capabilities:
|
|
358
|
+
flags: int
|
|
359
|
+
dvbt_bandwidths_mhz: tuple[int, ...]
|
|
360
|
+
max_pid_filters: int
|
|
361
|
+
tuner_xtal_hz: int
|
|
362
|
+
demod_xtal_hz: int
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@dataclass(frozen=True)
|
|
366
|
+
class FrequencyRange:
|
|
367
|
+
min_hz: int
|
|
368
|
+
max_hz: int
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@dataclass(frozen=True)
|
|
372
|
+
class RateRange:
|
|
373
|
+
min_hz: int
|
|
374
|
+
max_hz: int
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@dataclass(frozen=True)
|
|
378
|
+
class DeviceProfile:
|
|
379
|
+
profile_version: int
|
|
380
|
+
feature_flags: int
|
|
381
|
+
tuner_family: TunerFamily
|
|
382
|
+
demod_family: DemodFamily
|
|
383
|
+
max_pid_filters: int
|
|
384
|
+
tuner_xtal_hz: int
|
|
385
|
+
demod_xtal_hz: int
|
|
386
|
+
rf_ranges: tuple[FrequencyRange, ...]
|
|
387
|
+
iq_sample_rate_ranges: tuple[RateRange, ...]
|
|
388
|
+
iq_bandwidth_ranges: tuple[RateRange, ...]
|
|
389
|
+
dvbt_bandwidths_hz: tuple[int, ...]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@dataclass(frozen=True)
|
|
393
|
+
class DVBTTuneInfo:
|
|
394
|
+
rf_hz: int
|
|
395
|
+
bandwidth_hz: int
|
|
396
|
+
if_hz: int
|
|
397
|
+
lo_hz: int
|
|
398
|
+
tuner_pll_locked: bool
|
|
399
|
+
filter_cal_code: int
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@dataclass(frozen=True)
|
|
403
|
+
class IQTuneInfo:
|
|
404
|
+
rf_hz: int
|
|
405
|
+
requested_sample_rate_hz: int
|
|
406
|
+
actual_sample_rate_hz: int
|
|
407
|
+
bandwidth_hz: int
|
|
408
|
+
if_hz: int
|
|
409
|
+
lo_hz: int
|
|
410
|
+
tuner_pll_locked: bool
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@dataclass(frozen=True)
|
|
414
|
+
class StreamInfo:
|
|
415
|
+
format: int
|
|
416
|
+
sample_rate_hz: int
|
|
417
|
+
packet_size_bytes: int
|
|
418
|
+
bytes_per_complex_sample: int
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@dataclass(frozen=True)
|
|
422
|
+
class SignalQuality:
|
|
423
|
+
fsm_stage: int
|
|
424
|
+
strength_percent: int
|
|
425
|
+
snr_db: float | None
|
|
426
|
+
ber: int | None
|
|
427
|
+
ber_raw: int
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass(frozen=True)
|
|
431
|
+
class USBStats:
|
|
432
|
+
control_transfers: int
|
|
433
|
+
control_errors: int
|
|
434
|
+
control_pipe_events: int
|
|
435
|
+
session_recovery_attempts: int
|
|
436
|
+
session_recovery_successes: int
|
|
437
|
+
session_recovery_failures: int
|
|
438
|
+
bulk_reads: int
|
|
439
|
+
bulk_timeouts: int
|
|
440
|
+
bulk_errors: int
|
|
441
|
+
bulk_bytes: int
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass(frozen=True)
|
|
445
|
+
class USBRecoveryStats:
|
|
446
|
+
stats_version: int
|
|
447
|
+
control_pipe_events: int
|
|
448
|
+
session_reopen_attempts: int
|
|
449
|
+
session_reopen_successes: int
|
|
450
|
+
session_reopen_failures: int
|
|
451
|
+
transaction_retry_attempts: int
|
|
452
|
+
transaction_retry_successes: int
|
|
453
|
+
transaction_retry_failures: int
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@dataclass(frozen=True)
|
|
457
|
+
class AsyncConfig:
|
|
458
|
+
transfer_size_bytes: int = ASYNC_DEFAULT_TRANSFER_SIZE
|
|
459
|
+
transfer_count: int = ASYNC_DEFAULT_TRANSFER_COUNT
|
|
460
|
+
queue_depth: int = ASYNC_DEFAULT_QUEUE_DEPTH
|
|
461
|
+
transfer_timeout_ms: int = 0
|
|
462
|
+
overflow_policy: AsyncOverflowPolicy = AsyncOverflowPolicy.DROP_OLDEST
|
|
463
|
+
|
|
464
|
+
def __post_init__(self) -> None:
|
|
465
|
+
try:
|
|
466
|
+
policy = AsyncOverflowPolicy(int(self.overflow_policy))
|
|
467
|
+
except (TypeError, ValueError) as exc:
|
|
468
|
+
raise ValueError("overflow_policy must be AsyncOverflowPolicy.DROP_OLDEST or DROP_NEWEST") from exc
|
|
469
|
+
object.__setattr__(self, "overflow_policy", policy)
|
|
470
|
+
|
|
471
|
+
if not ASYNC_MIN_TRANSFER_SIZE <= int(self.transfer_size_bytes) <= ASYNC_MAX_TRANSFER_SIZE:
|
|
472
|
+
raise ValueError(
|
|
473
|
+
f"transfer_size_bytes must be {ASYNC_MIN_TRANSFER_SIZE}..{ASYNC_MAX_TRANSFER_SIZE}"
|
|
474
|
+
)
|
|
475
|
+
if not ASYNC_MIN_TRANSFER_COUNT <= int(self.transfer_count) <= ASYNC_MAX_TRANSFER_COUNT:
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"transfer_count must be {ASYNC_MIN_TRANSFER_COUNT}..{ASYNC_MAX_TRANSFER_COUNT}"
|
|
478
|
+
)
|
|
479
|
+
if not ASYNC_MIN_QUEUE_DEPTH <= int(self.queue_depth) <= ASYNC_MAX_QUEUE_DEPTH:
|
|
480
|
+
raise ValueError(
|
|
481
|
+
f"queue_depth must be {ASYNC_MIN_QUEUE_DEPTH}..{ASYNC_MAX_QUEUE_DEPTH}"
|
|
482
|
+
)
|
|
483
|
+
if not 0 <= int(self.transfer_timeout_ms) <= ASYNC_WAIT_INFINITE:
|
|
484
|
+
raise ValueError("transfer_timeout_ms must fit uint32")
|
|
485
|
+
if int(self.transfer_size_bytes) * int(self.queue_depth) > ASYNC_MAX_QUEUE_BYTES:
|
|
486
|
+
raise ValueError(f"async queue exceeds {ASYNC_MAX_QUEUE_BYTES} bytes")
|
|
487
|
+
if int(self.transfer_size_bytes) * int(self.transfer_count) > ASYNC_MAX_TRANSFER_POOL_BYTES:
|
|
488
|
+
raise ValueError(
|
|
489
|
+
f"async transfer pool payload exceeds {ASYNC_MAX_TRANSFER_POOL_BYTES} bytes"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@dataclass(frozen=True)
|
|
494
|
+
class AsyncBlock:
|
|
495
|
+
data: bytes
|
|
496
|
+
sequence: int
|
|
497
|
+
|
|
498
|
+
def __len__(self) -> int:
|
|
499
|
+
return len(self.data)
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def nbytes(self) -> int:
|
|
503
|
+
# n of payload bytes in this producer block/read fragment
|
|
504
|
+
return len(self.data)
|
|
505
|
+
|
|
506
|
+
def __bytes__(self) -> bytes:
|
|
507
|
+
return self.data
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@dataclass(frozen=True)
|
|
511
|
+
class AsyncStats:
|
|
512
|
+
stats_version: int
|
|
513
|
+
running: bool
|
|
514
|
+
queue_current: int
|
|
515
|
+
queue_high_water: int
|
|
516
|
+
transfer_callbacks: int
|
|
517
|
+
transfers_completed: int
|
|
518
|
+
transfer_bytes: int
|
|
519
|
+
transfer_timeouts: int
|
|
520
|
+
transfer_errors: int
|
|
521
|
+
buffers_enqueued: int
|
|
522
|
+
buffers_dequeued: int
|
|
523
|
+
buffers_dropped_oldest: int
|
|
524
|
+
buffers_dropped_newest: int
|
|
525
|
+
consumer_reads: int
|
|
526
|
+
consumer_bytes: int
|
|
527
|
+
buffers_discarded_on_stop: int
|
|
528
|
+
terminal_status: int
|
|
529
|
+
terminal_native_code: int
|
|
530
|
+
|
|
531
|
+
@property
|
|
532
|
+
def dropped_buffers(self) -> int:
|
|
533
|
+
# total queue overflow drops across both configured policies
|
|
534
|
+
return self.buffers_dropped_oldest + self.buffers_dropped_newest
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def terminal(self) -> bool:
|
|
538
|
+
# whether the native async engine recorded a terminal status
|
|
539
|
+
return self.terminal_status != 0
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
_LOG_CALLBACK = C.CFUNCTYPE(None, C.c_void_p, C.c_int, C.c_char_p)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
_resource_stack = ExitStack()
|
|
546
|
+
atexit.register(_resource_stack.close)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _candidate_library_paths() -> list[Path]:
|
|
550
|
+
explicit = os.environ.get("SDRNATIVE_LIBRARY")
|
|
551
|
+
if explicit:
|
|
552
|
+
return [Path(explicit).expanduser()]
|
|
553
|
+
|
|
554
|
+
here = Path(__file__).resolve()
|
|
555
|
+
repo = here.parents[3]
|
|
556
|
+
system = platform.system()
|
|
557
|
+
if system == "Darwin":
|
|
558
|
+
names = ["libsdrnative.dylib", "libsdrnative.0.dylib"]
|
|
559
|
+
dirs = [repo / "build", repo / "build" / "lib", repo / "dist" / "lib"]
|
|
560
|
+
elif system == "Windows":
|
|
561
|
+
names = ["sdrnative.dll", "libsdrnative.dll"]
|
|
562
|
+
dirs = [
|
|
563
|
+
repo / "build-win" / "Release",
|
|
564
|
+
repo / "build-win" / "Debug",
|
|
565
|
+
repo / "build",
|
|
566
|
+
repo / "build" / "Release",
|
|
567
|
+
repo / "build" / "Debug",
|
|
568
|
+
repo / "dist" / "bin",
|
|
569
|
+
]
|
|
570
|
+
else:
|
|
571
|
+
names = ["libsdrnative.so", "libsdrnative.so.0"]
|
|
572
|
+
dirs = [repo / "build", repo / "build" / "lib", repo / "dist" / "lib"]
|
|
573
|
+
|
|
574
|
+
candidates: list[Path] = []
|
|
575
|
+
for name in names:
|
|
576
|
+
resource = files("sdrnative").joinpath(name)
|
|
577
|
+
if resource.is_file():
|
|
578
|
+
candidates.append(Path(_resource_stack.enter_context(as_file(resource))))
|
|
579
|
+
for d in dirs:
|
|
580
|
+
for name in names:
|
|
581
|
+
candidates.append(d / name)
|
|
582
|
+
return candidates
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _load_library() -> C.CDLL:
|
|
586
|
+
attempts: list[str] = []
|
|
587
|
+
for path in _candidate_library_paths():
|
|
588
|
+
attempts.append(str(path))
|
|
589
|
+
if path.exists():
|
|
590
|
+
return C.CDLL(str(path))
|
|
591
|
+
|
|
592
|
+
# last fallback asks dynamic linker by soname
|
|
593
|
+
sonames = {
|
|
594
|
+
"Darwin": ["libsdrnative.dylib"],
|
|
595
|
+
"Windows": ["sdrnative.dll"],
|
|
596
|
+
}.get(platform.system(), ["libsdrnative.so"])
|
|
597
|
+
for name in sonames:
|
|
598
|
+
attempts.append(name)
|
|
599
|
+
try:
|
|
600
|
+
return C.CDLL(name)
|
|
601
|
+
except OSError:
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
raise OSError(
|
|
605
|
+
"SDRNative library not found; build it or set "
|
|
606
|
+
"SDRNATIVE_LIBRARY. Searched:\n " + "\n ".join(attempts)
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
_lib = _load_library()
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def native_library_path() -> str:
|
|
614
|
+
# return the path/name used to load the canonical SDRNative C library
|
|
615
|
+
return str(_lib._name)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# native signatures
|
|
619
|
+
_lib.sdrn_version_string.restype = C.c_char_p
|
|
620
|
+
try:
|
|
621
|
+
_lib.sdrn_abi_version.restype = C.c_uint32
|
|
622
|
+
_lib.sdrn_get_abi_info.argtypes = [C.POINTER(_ABIInfoC)]
|
|
623
|
+
_lib.sdrn_get_abi_info.restype = C.c_int
|
|
624
|
+
except AttributeError as exc:
|
|
625
|
+
raise OSError(
|
|
626
|
+
"loaded SDRNative library lacks ABI v1 metadata; "
|
|
627
|
+
"check the installed library version"
|
|
628
|
+
) from exc
|
|
629
|
+
_lib.sdrn_status_string.argtypes = [C.c_int]
|
|
630
|
+
_lib.sdrn_status_string.restype = C.c_char_p
|
|
631
|
+
_lib.sdrn_last_global_error.restype = C.c_char_p
|
|
632
|
+
_lib.sdrn_get_last_global_error_info.argtypes = [C.POINTER(_ErrorInfoC)]
|
|
633
|
+
_lib.sdrn_get_last_global_error_info.restype = C.c_int
|
|
634
|
+
_lib.sdrn_list_devices.argtypes = [C.POINTER(_DeviceInfoC), C.c_size_t, C.POINTER(C.c_size_t)]
|
|
635
|
+
_lib.sdrn_list_devices.restype = C.c_int
|
|
636
|
+
_lib.sdrn_open.argtypes = [C.c_size_t, C.POINTER(C.c_void_p)]
|
|
637
|
+
_lib.sdrn_open.restype = C.c_int
|
|
638
|
+
_lib.sdrn_close.argtypes = [C.c_void_p]
|
|
639
|
+
_lib.sdrn_last_error.argtypes = [C.c_void_p]
|
|
640
|
+
_lib.sdrn_last_error.restype = C.c_char_p
|
|
641
|
+
_lib.sdrn_get_last_error_info.argtypes = [C.c_void_p, C.POINTER(_ErrorInfoC)]
|
|
642
|
+
_lib.sdrn_get_last_error_info.restype = C.c_int
|
|
643
|
+
_lib.sdrn_set_log_callback.argtypes = [C.c_void_p, _LOG_CALLBACK, C.c_void_p, C.c_int]
|
|
644
|
+
_lib.sdrn_get_device_info.argtypes = [C.c_void_p, C.POINTER(_DeviceInfoC)]
|
|
645
|
+
_lib.sdrn_get_device_info.restype = C.c_int
|
|
646
|
+
_lib.sdrn_get_capabilities.argtypes = [C.c_void_p, C.POINTER(_CapabilitiesC)]
|
|
647
|
+
_lib.sdrn_get_capabilities.restype = C.c_int
|
|
648
|
+
_lib.sdrn_get_device_profile.argtypes = [C.c_void_p, C.POINTER(_DeviceProfileC)]
|
|
649
|
+
_lib.sdrn_get_device_profile.restype = C.c_int
|
|
650
|
+
_lib.sdrn_tuner_family_string.argtypes = [C.c_int]
|
|
651
|
+
_lib.sdrn_tuner_family_string.restype = C.c_char_p
|
|
652
|
+
_lib.sdrn_demod_family_string.argtypes = [C.c_int]
|
|
653
|
+
_lib.sdrn_demod_family_string.restype = C.c_char_p
|
|
654
|
+
_lib.sdrn_get_mode.argtypes = [C.c_void_p]
|
|
655
|
+
_lib.sdrn_get_mode.restype = C.c_int
|
|
656
|
+
_lib.sdrn_get_state.argtypes = [C.c_void_p]
|
|
657
|
+
_lib.sdrn_get_state.restype = C.c_int
|
|
658
|
+
_lib.sdrn_get_usb_stats.argtypes = [C.c_void_p, C.POINTER(_USBStatsC)]
|
|
659
|
+
_lib.sdrn_get_usb_stats.restype = C.c_int
|
|
660
|
+
_lib.sdrn_get_usb_recovery_stats.argtypes = [C.c_void_p, C.POINTER(_USBRecoveryStatsC)]
|
|
661
|
+
_lib.sdrn_get_usb_recovery_stats.restype = C.c_int
|
|
662
|
+
_lib.sdrn_reset_usb_stats.argtypes = [C.c_void_p]
|
|
663
|
+
_lib.sdrn_reset_usb_stats.restype = C.c_int
|
|
664
|
+
_lib.sdrn_stop.argtypes = [C.c_void_p]
|
|
665
|
+
_lib.sdrn_stop.restype = C.c_int
|
|
666
|
+
_lib.sdrn_get_stream_info.argtypes = [C.c_void_p, C.POINTER(_StreamInfoC)]
|
|
667
|
+
_lib.sdrn_get_stream_info.restype = C.c_int
|
|
668
|
+
_lib.sdrn_async_config_init.argtypes = [C.POINTER(_AsyncConfigC)]
|
|
669
|
+
_lib.sdrn_async_config_init.restype = None
|
|
670
|
+
_lib.sdrn_async_start.argtypes = [C.c_void_p, C.POINTER(_AsyncConfigC)]
|
|
671
|
+
_lib.sdrn_async_start.restype = C.c_int
|
|
672
|
+
_lib.sdrn_async_read.argtypes = [
|
|
673
|
+
C.c_void_p, C.c_void_p, C.c_size_t, C.POINTER(C.c_size_t), C.POINTER(C.c_uint64), C.c_uint32
|
|
674
|
+
]
|
|
675
|
+
_lib.sdrn_async_read.restype = C.c_int
|
|
676
|
+
_lib.sdrn_async_stop.argtypes = [C.c_void_p]
|
|
677
|
+
_lib.sdrn_async_stop.restype = C.c_int
|
|
678
|
+
_lib.sdrn_async_is_running.argtypes = [C.c_void_p]
|
|
679
|
+
_lib.sdrn_async_is_running.restype = C.c_int
|
|
680
|
+
_lib.sdrn_async_get_stats.argtypes = [C.c_void_p, C.POINTER(_AsyncStatsC)]
|
|
681
|
+
_lib.sdrn_async_get_stats.restype = C.c_int
|
|
682
|
+
_lib.sdrn_async_reset_stats.argtypes = [C.c_void_p]
|
|
683
|
+
_lib.sdrn_async_reset_stats.restype = C.c_int
|
|
684
|
+
_lib.sdrn_iq_start.argtypes = [C.c_void_p, C.c_uint64, C.c_uint32, C.c_uint32, C.POINTER(_IQTuneInfoC)]
|
|
685
|
+
_lib.sdrn_iq_start.restype = C.c_int
|
|
686
|
+
_lib.sdrn_iq_tune.argtypes = [C.c_void_p, C.c_uint64, C.POINTER(_IQTuneInfoC)]
|
|
687
|
+
_lib.sdrn_iq_tune.restype = C.c_int
|
|
688
|
+
_lib.sdrn_iq_set_sample_rate.argtypes = [C.c_void_p, C.c_uint32, C.POINTER(C.c_uint32)]
|
|
689
|
+
_lib.sdrn_iq_set_sample_rate.restype = C.c_int
|
|
690
|
+
_lib.sdrn_iq_set_bandwidth.argtypes = [C.c_void_p, C.c_uint32, C.POINTER(_IQTuneInfoC)]
|
|
691
|
+
_lib.sdrn_iq_set_bandwidth.restype = C.c_int
|
|
692
|
+
_lib.sdrn_dvbt_start.argtypes = [C.c_void_p, C.c_uint64, C.c_uint32, C.POINTER(_TuneInfoC)]
|
|
693
|
+
_lib.sdrn_dvbt_start.restype = C.c_int
|
|
694
|
+
_lib.sdrn_dvbt_tune.argtypes = [C.c_void_p, C.c_uint64, C.c_uint32, C.POINTER(_TuneInfoC)]
|
|
695
|
+
_lib.sdrn_dvbt_tune.restype = C.c_int
|
|
696
|
+
_lib.sdrn_dvbt_wait_lock.argtypes = [C.c_void_p, C.c_uint32, C.POINTER(C.c_uint8)]
|
|
697
|
+
_lib.sdrn_dvbt_wait_lock.restype = C.c_int
|
|
698
|
+
_lib.sdrn_dvbt_get_signal_quality.argtypes = [C.c_void_p, C.POINTER(_SignalQualityC)]
|
|
699
|
+
_lib.sdrn_dvbt_get_signal_quality.restype = C.c_int
|
|
700
|
+
_lib.sdrn_stream_read.argtypes = [C.c_void_p, C.c_void_p, C.c_size_t, C.POINTER(C.c_size_t), C.c_uint32]
|
|
701
|
+
_lib.sdrn_stream_read.restype = C.c_int
|
|
702
|
+
_lib.sdrn_dvbt_set_pid_filter.argtypes = [C.c_void_p, C.POINTER(C.c_uint16), C.c_size_t]
|
|
703
|
+
_lib.sdrn_dvbt_set_pid_filter.restype = C.c_int
|
|
704
|
+
_lib.sdrn_dvbt_disable_pid_filter.argtypes = [C.c_void_p]
|
|
705
|
+
_lib.sdrn_dvbt_disable_pid_filter.restype = C.c_int
|
|
706
|
+
_lib.sdrn_control_read.argtypes = [C.c_void_p, C.c_uint16, C.c_uint16, C.c_void_p, C.c_size_t]
|
|
707
|
+
_lib.sdrn_control_read.restype = C.c_int
|
|
708
|
+
_lib.sdrn_control_write.argtypes = [C.c_void_p, C.c_uint16, C.c_uint16, C.c_void_p, C.c_size_t]
|
|
709
|
+
_lib.sdrn_control_write.restype = C.c_int
|
|
710
|
+
_lib.sdrn_reg_read.argtypes = [C.c_void_p, C.c_uint16, C.c_void_p, C.c_size_t]
|
|
711
|
+
_lib.sdrn_reg_read.restype = C.c_int
|
|
712
|
+
_lib.sdrn_reg_write.argtypes = [C.c_void_p, C.c_uint16, C.c_void_p, C.c_size_t]
|
|
713
|
+
_lib.sdrn_reg_write.restype = C.c_int
|
|
714
|
+
_lib.sdrn_demod_read.argtypes = [C.c_void_p, C.c_uint8, C.c_uint8, C.c_void_p, C.c_size_t]
|
|
715
|
+
_lib.sdrn_demod_read.restype = C.c_int
|
|
716
|
+
_lib.sdrn_demod_write.argtypes = [C.c_void_p, C.c_uint8, C.c_uint8, C.c_void_p, C.c_size_t]
|
|
717
|
+
_lib.sdrn_demod_write.restype = C.c_int
|
|
718
|
+
_lib.sdrn_i2c_read_reg.argtypes = [C.c_void_p, C.c_uint8, C.c_uint8, C.c_void_p, C.c_size_t]
|
|
719
|
+
_lib.sdrn_i2c_read_reg.restype = C.c_int
|
|
720
|
+
_lib.sdrn_i2c_write_reg.argtypes = [C.c_void_p, C.c_uint8, C.c_uint8, C.c_void_p, C.c_size_t]
|
|
721
|
+
_lib.sdrn_i2c_write_reg.restype = C.c_int
|
|
722
|
+
_lib.sdrn_i2c_read_direct.argtypes = [C.c_void_p, C.c_uint8, C.c_void_p, C.c_size_t]
|
|
723
|
+
_lib.sdrn_i2c_read_direct.restype = C.c_int
|
|
724
|
+
_lib.sdrn_i2c_write_direct.argtypes = [C.c_void_p, C.c_uint8, C.c_void_p, C.c_size_t]
|
|
725
|
+
_lib.sdrn_i2c_write_direct.restype = C.c_int
|
|
726
|
+
_lib.sdrn_i2c_gate_open.argtypes = [C.c_void_p]
|
|
727
|
+
_lib.sdrn_i2c_gate_open.restype = C.c_int
|
|
728
|
+
_lib.sdrn_i2c_gate_close.argtypes = [C.c_void_p]
|
|
729
|
+
_lib.sdrn_i2c_gate_close.restype = C.c_int
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
_ABI_EXPECTED_VERSION = 1
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _read_abi_info_c() -> _ABIInfoC:
|
|
736
|
+
info = _ABIInfoC()
|
|
737
|
+
rc = int(_lib.sdrn_get_abi_info(C.byref(info)))
|
|
738
|
+
if rc != 0:
|
|
739
|
+
raise OSError(f"sdrn_get_abi_info failed with status {rc}")
|
|
740
|
+
return info
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _validate_abi() -> None:
|
|
744
|
+
info = _read_abi_info_c()
|
|
745
|
+
failures: list[str] = []
|
|
746
|
+
|
|
747
|
+
native_abi = int(_lib.sdrn_abi_version())
|
|
748
|
+
if native_abi != _ABI_EXPECTED_VERSION:
|
|
749
|
+
failures.append(f"ABI generation {native_abi} != expected {_ABI_EXPECTED_VERSION}")
|
|
750
|
+
if int(info.abi_version) != _ABI_EXPECTED_VERSION:
|
|
751
|
+
failures.append(
|
|
752
|
+
f"ABI info generation {int(info.abi_version)} != expected {_ABI_EXPECTED_VERSION}"
|
|
753
|
+
)
|
|
754
|
+
if int(info.struct_size) != C.sizeof(_ABIInfoC):
|
|
755
|
+
failures.append(
|
|
756
|
+
f"sdrn_abi_info size {int(info.struct_size)} != ctypes {C.sizeof(_ABIInfoC)}"
|
|
757
|
+
)
|
|
758
|
+
if int(info.pointer_size) != C.sizeof(C.c_void_p):
|
|
759
|
+
failures.append(
|
|
760
|
+
f"pointer size {int(info.pointer_size)} != ctypes {C.sizeof(C.c_void_p)}"
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
enum_fields = (
|
|
764
|
+
"status_size", "log_level_size", "mode_size", "state_size",
|
|
765
|
+
"error_domain_size", "stream_format_size", "tuner_family_size",
|
|
766
|
+
"demod_family_size",
|
|
767
|
+
)
|
|
768
|
+
enum_size = C.sizeof(C.c_int)
|
|
769
|
+
for field in enum_fields:
|
|
770
|
+
native_size = int(getattr(info, field))
|
|
771
|
+
if native_size != enum_size:
|
|
772
|
+
failures.append(f"{field} {native_size} != ctypes int {enum_size}")
|
|
773
|
+
|
|
774
|
+
layouts = (
|
|
775
|
+
("device_info", _DeviceInfoC),
|
|
776
|
+
("capabilities", _CapabilitiesC),
|
|
777
|
+
("error_info", _ErrorInfoC),
|
|
778
|
+
("frequency_range", _FrequencyRangeC),
|
|
779
|
+
("rate_range", _RateRangeC),
|
|
780
|
+
("device_profile", _DeviceProfileC),
|
|
781
|
+
("dvbt_tune_info", _TuneInfoC),
|
|
782
|
+
("iq_tune_info", _IQTuneInfoC),
|
|
783
|
+
("stream_info", _StreamInfoC),
|
|
784
|
+
("signal_quality", _SignalQualityC),
|
|
785
|
+
("usb_stats", _USBStatsC),
|
|
786
|
+
)
|
|
787
|
+
for field, ctype in layouts:
|
|
788
|
+
native_size = int(getattr(info, f"{field}_size"))
|
|
789
|
+
native_align = int(getattr(info, f"{field}_align"))
|
|
790
|
+
py_size = C.sizeof(ctype)
|
|
791
|
+
py_align = C.alignment(ctype)
|
|
792
|
+
if native_size != py_size or native_align != py_align:
|
|
793
|
+
failures.append(
|
|
794
|
+
f"{field} layout native={native_size}/{native_align} "
|
|
795
|
+
f"ctypes={py_size}/{py_align}"
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
if failures:
|
|
799
|
+
raise OSError("C/Python ABI mismatch: " + "; ".join(failures))
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
_validate_abi()
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _validate_async_contract() -> None:
|
|
806
|
+
config = _AsyncConfigC()
|
|
807
|
+
_lib.sdrn_async_config_init(C.byref(config))
|
|
808
|
+
failures: list[str] = []
|
|
809
|
+
if int(config.struct_size) != C.sizeof(_AsyncConfigC):
|
|
810
|
+
failures.append(
|
|
811
|
+
f"sdrn_async_config size {int(config.struct_size)} != ctypes {C.sizeof(_AsyncConfigC)}"
|
|
812
|
+
)
|
|
813
|
+
if int(config.config_version) != 1:
|
|
814
|
+
failures.append(f"async config version {int(config.config_version)} != expected 1")
|
|
815
|
+
if int(config.transfer_size_bytes) != ASYNC_DEFAULT_TRANSFER_SIZE:
|
|
816
|
+
failures.append("async default transfer size does not match the public async contract")
|
|
817
|
+
if int(config.transfer_count) != ASYNC_DEFAULT_TRANSFER_COUNT:
|
|
818
|
+
failures.append("async default transfer count does not match the public async contract")
|
|
819
|
+
if int(config.queue_depth) != ASYNC_DEFAULT_QUEUE_DEPTH:
|
|
820
|
+
failures.append("async default queue depth does not match the public async contract")
|
|
821
|
+
if int(config.overflow_policy) != int(AsyncOverflowPolicy.DROP_OLDEST):
|
|
822
|
+
failures.append("async default overflow policy does not match the public async contract")
|
|
823
|
+
if failures:
|
|
824
|
+
raise OSError("C/Python async ABI mismatch: " + "; ".join(failures))
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
_validate_async_contract()
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def version() -> str:
|
|
831
|
+
return _lib.sdrn_version_string().decode()
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def abi_version() -> int:
|
|
835
|
+
return int(_lib.sdrn_abi_version())
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def abi_info() -> ABIInfo:
|
|
839
|
+
info = _read_abi_info_c()
|
|
840
|
+
return ABIInfo(
|
|
841
|
+
abi_version=int(info.abi_version),
|
|
842
|
+
info_version=int(info.info_version),
|
|
843
|
+
flags=int(info.flags),
|
|
844
|
+
pointer_size=int(info.pointer_size),
|
|
845
|
+
device_profile_size=int(info.device_profile_size),
|
|
846
|
+
error_info_size=int(info.error_info_size),
|
|
847
|
+
usb_stats_size=int(info.usb_stats_size),
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _status_name(status: int) -> str:
|
|
852
|
+
return _lib.sdrn_status_string(status).decode()
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _domain(value: int) -> ErrorDomain:
|
|
856
|
+
try:
|
|
857
|
+
return ErrorDomain(value)
|
|
858
|
+
except ValueError:
|
|
859
|
+
return ErrorDomain.NONE
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _fallback_domain(status: int) -> ErrorDomain:
|
|
863
|
+
if status == -1:
|
|
864
|
+
return ErrorDomain.ARGUMENT
|
|
865
|
+
if status == -9:
|
|
866
|
+
return ErrorDomain.STATE
|
|
867
|
+
if status in (-3, -4, -5, -6):
|
|
868
|
+
return ErrorDomain.USB
|
|
869
|
+
if status == -10:
|
|
870
|
+
return ErrorDomain.TUNER
|
|
871
|
+
if status in (-11, -12):
|
|
872
|
+
return ErrorDomain.DEMOD
|
|
873
|
+
if status in (-2, -13):
|
|
874
|
+
return ErrorDomain.INTERNAL
|
|
875
|
+
return ErrorDomain.NONE
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _read_error_info(handle: C.c_void_p | None, status: int) -> ErrorInfo:
|
|
879
|
+
c = _ErrorInfoC()
|
|
880
|
+
rc = -1
|
|
881
|
+
if handle and handle.value:
|
|
882
|
+
rc = _lib.sdrn_get_last_error_info(handle, C.byref(c))
|
|
883
|
+
else:
|
|
884
|
+
rc = _lib.sdrn_get_last_global_error_info(C.byref(c))
|
|
885
|
+
|
|
886
|
+
if rc == 0:
|
|
887
|
+
domain = _domain(int(c.domain))
|
|
888
|
+
return ErrorInfo(
|
|
889
|
+
status=int(c.status) if int(c.status) != 0 else int(status),
|
|
890
|
+
domain=domain if domain != ErrorDomain.NONE else _fallback_domain(status),
|
|
891
|
+
native_code=int(c.native_code),
|
|
892
|
+
flags=ErrorFlags(int(c.flags)),
|
|
893
|
+
message=_text(bytes(c.message)),
|
|
894
|
+
)
|
|
895
|
+
return ErrorInfo(int(status), _fallback_domain(status), 0, ErrorFlags.NONE, "")
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
_ERROR_TYPES = {
|
|
899
|
+
ErrorDomain.ARGUMENT: SDRNativeArgumentError,
|
|
900
|
+
ErrorDomain.STATE: SDRNativeStateError,
|
|
901
|
+
ErrorDomain.USB: SDRNativeUSBError,
|
|
902
|
+
ErrorDomain.TUNER: SDRNativeTunerError,
|
|
903
|
+
ErrorDomain.DEMOD: SDRNativeDemodError,
|
|
904
|
+
ErrorDomain.STREAM: SDRNativeStreamError,
|
|
905
|
+
ErrorDomain.INTERNAL: SDRNativeInternalError,
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _check(status: int, handle: C.c_void_p | None = None) -> None:
|
|
910
|
+
if status == 0:
|
|
911
|
+
return
|
|
912
|
+
info = _read_error_info(handle, status)
|
|
913
|
+
message = _status_name(status)
|
|
914
|
+
if info.message:
|
|
915
|
+
message += f": {info.message}"
|
|
916
|
+
if info.native_code:
|
|
917
|
+
message += f" [native={info.native_code}]"
|
|
918
|
+
exc_type = _ERROR_TYPES.get(info.domain, SDRNativeError)
|
|
919
|
+
raise exc_type(status, message, info)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def last_global_error_info() -> ErrorInfo:
|
|
923
|
+
return _read_error_info(None, 0)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _info(c: _DeviceInfoC) -> DeviceInfo:
|
|
927
|
+
return DeviceInfo(
|
|
928
|
+
vid=c.vid,
|
|
929
|
+
pid=c.pid,
|
|
930
|
+
bus=c.bus,
|
|
931
|
+
address=c.address,
|
|
932
|
+
manufacturer=_text(bytes(c.manufacturer)),
|
|
933
|
+
product=_text(bytes(c.product)),
|
|
934
|
+
serial=_text(bytes(c.serial)),
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def list_devices() -> list[DeviceInfo]:
|
|
939
|
+
count = C.c_size_t()
|
|
940
|
+
_check(_lib.sdrn_list_devices(None, 0, C.byref(count)))
|
|
941
|
+
if not count.value:
|
|
942
|
+
return []
|
|
943
|
+
array = (_DeviceInfoC * count.value)()
|
|
944
|
+
_check(_lib.sdrn_list_devices(array, count.value, C.byref(count)))
|
|
945
|
+
return [_info(array[i]) for i in range(count.value)]
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def tuner_family_name(family: TunerFamily | int) -> str:
|
|
949
|
+
return _lib.sdrn_tuner_family_string(int(family)).decode()
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def demod_family_name(family: DemodFamily | int) -> str:
|
|
953
|
+
return _lib.sdrn_demod_family_string(int(family)).decode()
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _enum_or_unknown(enum_type, value):
|
|
957
|
+
try:
|
|
958
|
+
return enum_type(value)
|
|
959
|
+
except ValueError:
|
|
960
|
+
return enum_type.UNKNOWN
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _async_config_c(config: AsyncConfig) -> _AsyncConfigC:
|
|
964
|
+
c = _AsyncConfigC()
|
|
965
|
+
_lib.sdrn_async_config_init(C.byref(c))
|
|
966
|
+
c.transfer_size_bytes = int(config.transfer_size_bytes)
|
|
967
|
+
c.transfer_count = int(config.transfer_count)
|
|
968
|
+
c.queue_depth = int(config.queue_depth)
|
|
969
|
+
c.transfer_timeout_ms = int(config.transfer_timeout_ms)
|
|
970
|
+
c.overflow_policy = int(config.overflow_policy)
|
|
971
|
+
return c
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _async_stats(c: _AsyncStatsC) -> AsyncStats:
|
|
975
|
+
if int(c.struct_size) != C.sizeof(_AsyncStatsC) or int(c.stats_version) != 1:
|
|
976
|
+
raise OSError(
|
|
977
|
+
"unsupported async stats layout: "
|
|
978
|
+
f"version={int(c.stats_version)} size={int(c.struct_size)}"
|
|
979
|
+
)
|
|
980
|
+
return AsyncStats(
|
|
981
|
+
stats_version=int(c.stats_version),
|
|
982
|
+
running=bool(c.running),
|
|
983
|
+
queue_current=int(c.queue_current),
|
|
984
|
+
queue_high_water=int(c.queue_high_water),
|
|
985
|
+
transfer_callbacks=int(c.transfer_callbacks),
|
|
986
|
+
transfers_completed=int(c.transfers_completed),
|
|
987
|
+
transfer_bytes=int(c.transfer_bytes),
|
|
988
|
+
transfer_timeouts=int(c.transfer_timeouts),
|
|
989
|
+
transfer_errors=int(c.transfer_errors),
|
|
990
|
+
buffers_enqueued=int(c.buffers_enqueued),
|
|
991
|
+
buffers_dequeued=int(c.buffers_dequeued),
|
|
992
|
+
buffers_dropped_oldest=int(c.buffers_dropped_oldest),
|
|
993
|
+
buffers_dropped_newest=int(c.buffers_dropped_newest),
|
|
994
|
+
consumer_reads=int(c.consumer_reads),
|
|
995
|
+
consumer_bytes=int(c.consumer_bytes),
|
|
996
|
+
buffers_discarded_on_stop=int(c.buffers_discarded_on_stop),
|
|
997
|
+
terminal_status=int(c.terminal_status),
|
|
998
|
+
terminal_native_code=int(c.terminal_native_code),
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _async_timeout(timeout_ms: int | None) -> int:
|
|
1003
|
+
if timeout_ms is None:
|
|
1004
|
+
return ASYNC_WAIT_INFINITE
|
|
1005
|
+
timeout = int(timeout_ms)
|
|
1006
|
+
if not 0 <= timeout <= ASYNC_WAIT_INFINITE:
|
|
1007
|
+
raise ValueError("timeout_ms must be None or fit uint32")
|
|
1008
|
+
return timeout
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
class Device:
|
|
1012
|
+
def __init__(self, handle: C.c_void_p):
|
|
1013
|
+
self._handle = handle
|
|
1014
|
+
self._log_callback = None
|
|
1015
|
+
|
|
1016
|
+
@classmethod
|
|
1017
|
+
def open(cls, index: int = 0) -> "Device":
|
|
1018
|
+
handle = C.c_void_p()
|
|
1019
|
+
status = _lib.sdrn_open(index, C.byref(handle))
|
|
1020
|
+
_check(status)
|
|
1021
|
+
return cls(handle)
|
|
1022
|
+
|
|
1023
|
+
@property
|
|
1024
|
+
def closed(self) -> bool:
|
|
1025
|
+
return not bool(self._handle and self._handle.value)
|
|
1026
|
+
|
|
1027
|
+
def _require(self) -> C.c_void_p:
|
|
1028
|
+
if self.closed:
|
|
1029
|
+
raise RuntimeError("device is closed")
|
|
1030
|
+
return self._handle
|
|
1031
|
+
|
|
1032
|
+
def close(self) -> None:
|
|
1033
|
+
if not self.closed:
|
|
1034
|
+
_lib.sdrn_close(self._handle)
|
|
1035
|
+
self._handle = C.c_void_p()
|
|
1036
|
+
self._log_callback = None
|
|
1037
|
+
|
|
1038
|
+
def __enter__(self) -> "Device":
|
|
1039
|
+
return self
|
|
1040
|
+
|
|
1041
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
1042
|
+
self.close()
|
|
1043
|
+
|
|
1044
|
+
def set_logger(self, callback=None, *, level: int = 3) -> None:
|
|
1045
|
+
handle = self._require()
|
|
1046
|
+
if callback is None:
|
|
1047
|
+
self._log_callback = None
|
|
1048
|
+
# ctypes CFUNCTYPE slot cannot receive None
|
|
1049
|
+
# use no op callback instead
|
|
1050
|
+
callback = lambda lvl, msg: None
|
|
1051
|
+
|
|
1052
|
+
def bridge(_user, lvl, message):
|
|
1053
|
+
callback(lvl, message.decode("utf-8", "replace"))
|
|
1054
|
+
|
|
1055
|
+
c_cb = _LOG_CALLBACK(bridge)
|
|
1056
|
+
self._log_callback = c_cb
|
|
1057
|
+
_lib.sdrn_set_log_callback(handle, c_cb, None, int(level))
|
|
1058
|
+
|
|
1059
|
+
@property
|
|
1060
|
+
def info(self) -> DeviceInfo:
|
|
1061
|
+
handle = self._require()
|
|
1062
|
+
c = _DeviceInfoC()
|
|
1063
|
+
_check(_lib.sdrn_get_device_info(handle, C.byref(c)), handle)
|
|
1064
|
+
return _info(c)
|
|
1065
|
+
|
|
1066
|
+
@property
|
|
1067
|
+
def capabilities(self) -> Capabilities:
|
|
1068
|
+
handle = self._require()
|
|
1069
|
+
c = _CapabilitiesC()
|
|
1070
|
+
_check(_lib.sdrn_get_capabilities(handle, C.byref(c)), handle)
|
|
1071
|
+
bands = tuple(mhz for mhz in (6, 7, 8) if c.dvbt_bandwidth_mask & (1 << mhz))
|
|
1072
|
+
return Capabilities(c.flags, bands, c.max_pid_filters, c.tuner_xtal_hz, c.demod_xtal_hz)
|
|
1073
|
+
|
|
1074
|
+
@property
|
|
1075
|
+
def profile(self) -> DeviceProfile:
|
|
1076
|
+
handle = self._require()
|
|
1077
|
+
c = _DeviceProfileC()
|
|
1078
|
+
_check(_lib.sdrn_get_device_profile(handle, C.byref(c)), handle)
|
|
1079
|
+
rf_count = min(int(c.rf_range_count), 4)
|
|
1080
|
+
sample_count = min(int(c.iq_sample_rate_range_count), 4)
|
|
1081
|
+
bandwidth_count = min(int(c.iq_bandwidth_range_count), 4)
|
|
1082
|
+
dvbt_count = min(int(c.dvbt_bandwidth_count), 8)
|
|
1083
|
+
return DeviceProfile(
|
|
1084
|
+
profile_version=int(c.profile_version),
|
|
1085
|
+
feature_flags=int(c.feature_flags),
|
|
1086
|
+
tuner_family=_enum_or_unknown(TunerFamily, int(c.tuner_family)),
|
|
1087
|
+
demod_family=_enum_or_unknown(DemodFamily, int(c.demod_family)),
|
|
1088
|
+
max_pid_filters=int(c.max_pid_filters),
|
|
1089
|
+
tuner_xtal_hz=int(c.tuner_xtal_hz),
|
|
1090
|
+
demod_xtal_hz=int(c.demod_xtal_hz),
|
|
1091
|
+
rf_ranges=tuple(
|
|
1092
|
+
FrequencyRange(int(c.rf_ranges[i].min_hz), int(c.rf_ranges[i].max_hz))
|
|
1093
|
+
for i in range(rf_count)
|
|
1094
|
+
),
|
|
1095
|
+
iq_sample_rate_ranges=tuple(
|
|
1096
|
+
RateRange(
|
|
1097
|
+
int(c.iq_sample_rate_ranges[i].min_hz),
|
|
1098
|
+
int(c.iq_sample_rate_ranges[i].max_hz),
|
|
1099
|
+
)
|
|
1100
|
+
for i in range(sample_count)
|
|
1101
|
+
),
|
|
1102
|
+
iq_bandwidth_ranges=tuple(
|
|
1103
|
+
RateRange(
|
|
1104
|
+
int(c.iq_bandwidth_ranges[i].min_hz),
|
|
1105
|
+
int(c.iq_bandwidth_ranges[i].max_hz),
|
|
1106
|
+
)
|
|
1107
|
+
for i in range(bandwidth_count)
|
|
1108
|
+
),
|
|
1109
|
+
dvbt_bandwidths_hz=tuple(int(c.dvbt_bandwidth_hz[i]) for i in range(dvbt_count)),
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
@property
|
|
1113
|
+
def mode(self) -> int:
|
|
1114
|
+
return int(_lib.sdrn_get_mode(self._require()))
|
|
1115
|
+
|
|
1116
|
+
@property
|
|
1117
|
+
def state(self) -> int:
|
|
1118
|
+
return int(_lib.sdrn_get_state(self._require()))
|
|
1119
|
+
|
|
1120
|
+
@property
|
|
1121
|
+
def last_error_info(self) -> ErrorInfo:
|
|
1122
|
+
return _read_error_info(self._require(), 0)
|
|
1123
|
+
|
|
1124
|
+
@property
|
|
1125
|
+
def usb_stats(self) -> USBStats:
|
|
1126
|
+
h = self._require()
|
|
1127
|
+
c = _USBStatsC()
|
|
1128
|
+
_check(_lib.sdrn_get_usb_stats(h, C.byref(c)), h)
|
|
1129
|
+
return USBStats(
|
|
1130
|
+
control_transfers=c.control_transfers,
|
|
1131
|
+
control_errors=c.control_errors,
|
|
1132
|
+
control_pipe_events=c.control_pipe_events,
|
|
1133
|
+
session_recovery_attempts=c.session_recovery_attempts,
|
|
1134
|
+
session_recovery_successes=c.session_recovery_successes,
|
|
1135
|
+
session_recovery_failures=c.session_recovery_failures,
|
|
1136
|
+
bulk_reads=c.bulk_reads,
|
|
1137
|
+
bulk_timeouts=c.bulk_timeouts,
|
|
1138
|
+
bulk_errors=c.bulk_errors,
|
|
1139
|
+
bulk_bytes=c.bulk_bytes,
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
@property
|
|
1143
|
+
def usb_recovery_stats(self) -> USBRecoveryStats:
|
|
1144
|
+
h = self._require()
|
|
1145
|
+
c = _USBRecoveryStatsC()
|
|
1146
|
+
_check(_lib.sdrn_get_usb_recovery_stats(h, C.byref(c)), h)
|
|
1147
|
+
if int(c.stats_version) != 1 or int(c.struct_size) != C.sizeof(_USBRecoveryStatsC):
|
|
1148
|
+
raise OSError(
|
|
1149
|
+
"unsupported USB recovery stats layout: "
|
|
1150
|
+
f"version={int(c.stats_version)} size={int(c.struct_size)}"
|
|
1151
|
+
)
|
|
1152
|
+
return USBRecoveryStats(
|
|
1153
|
+
stats_version=int(c.stats_version),
|
|
1154
|
+
control_pipe_events=int(c.control_pipe_events),
|
|
1155
|
+
session_reopen_attempts=int(c.session_reopen_attempts),
|
|
1156
|
+
session_reopen_successes=int(c.session_reopen_successes),
|
|
1157
|
+
session_reopen_failures=int(c.session_reopen_failures),
|
|
1158
|
+
transaction_retry_attempts=int(c.transaction_retry_attempts),
|
|
1159
|
+
transaction_retry_successes=int(c.transaction_retry_successes),
|
|
1160
|
+
transaction_retry_failures=int(c.transaction_retry_failures),
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
def reset_usb_stats(self) -> None:
|
|
1164
|
+
h = self._require()
|
|
1165
|
+
_check(_lib.sdrn_reset_usb_stats(h), h)
|
|
1166
|
+
|
|
1167
|
+
def stop(self) -> None:
|
|
1168
|
+
h = self._require()
|
|
1169
|
+
_check(_lib.sdrn_stop(h), h)
|
|
1170
|
+
|
|
1171
|
+
@property
|
|
1172
|
+
def stream_info(self) -> StreamInfo:
|
|
1173
|
+
h = self._require()
|
|
1174
|
+
c = _StreamInfoC()
|
|
1175
|
+
_check(_lib.sdrn_get_stream_info(h, C.byref(c)), h)
|
|
1176
|
+
return StreamInfo(c.format, c.sample_rate_hz, c.packet_size_bytes, c.bytes_per_complex_sample)
|
|
1177
|
+
|
|
1178
|
+
def async_start(self, config: AsyncConfig | None = None) -> None:
|
|
1179
|
+
h = self._require()
|
|
1180
|
+
if config is None:
|
|
1181
|
+
_check(_lib.sdrn_async_start(h, None), h)
|
|
1182
|
+
return
|
|
1183
|
+
if not isinstance(config, AsyncConfig):
|
|
1184
|
+
raise TypeError("config must be AsyncConfig or None")
|
|
1185
|
+
c = _async_config_c(config)
|
|
1186
|
+
_check(_lib.sdrn_async_start(h, C.byref(c)), h)
|
|
1187
|
+
|
|
1188
|
+
def async_read(
|
|
1189
|
+
self,
|
|
1190
|
+
size: int = ASYNC_DEFAULT_TRANSFER_SIZE,
|
|
1191
|
+
timeout_ms: int | None = 1000,
|
|
1192
|
+
) -> AsyncBlock | None:
|
|
1193
|
+
if size <= 0:
|
|
1194
|
+
raise ValueError("size must be positive")
|
|
1195
|
+
h = self._require()
|
|
1196
|
+
buf = (C.c_uint8 * int(size))()
|
|
1197
|
+
got = C.c_size_t()
|
|
1198
|
+
sequence = C.c_uint64()
|
|
1199
|
+
status = int(
|
|
1200
|
+
_lib.sdrn_async_read(
|
|
1201
|
+
h, buf, int(size), C.byref(got), C.byref(sequence), _async_timeout(timeout_ms)
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
# consumer timeout is a normal empty read
|
|
1205
|
+
# do not route it through terminal error state
|
|
1206
|
+
if status == -6:
|
|
1207
|
+
return None
|
|
1208
|
+
_check(status, h)
|
|
1209
|
+
return AsyncBlock(bytes(buf[: got.value]), int(sequence.value))
|
|
1210
|
+
|
|
1211
|
+
def async_stop(self) -> None:
|
|
1212
|
+
h = self._require()
|
|
1213
|
+
_check(_lib.sdrn_async_stop(h), h)
|
|
1214
|
+
|
|
1215
|
+
@property
|
|
1216
|
+
def async_running(self) -> bool:
|
|
1217
|
+
return bool(_lib.sdrn_async_is_running(self._require()))
|
|
1218
|
+
|
|
1219
|
+
@property
|
|
1220
|
+
def async_stats(self) -> AsyncStats:
|
|
1221
|
+
h = self._require()
|
|
1222
|
+
c = _AsyncStatsC()
|
|
1223
|
+
_check(_lib.sdrn_async_get_stats(h, C.byref(c)), h)
|
|
1224
|
+
return _async_stats(c)
|
|
1225
|
+
|
|
1226
|
+
def reset_async_stats(self) -> None:
|
|
1227
|
+
h = self._require()
|
|
1228
|
+
_check(_lib.sdrn_async_reset_stats(h), h)
|
|
1229
|
+
|
|
1230
|
+
def async_stream(
|
|
1231
|
+
self,
|
|
1232
|
+
config: AsyncConfig | None = None,
|
|
1233
|
+
*,
|
|
1234
|
+
read_size: int | None = None,
|
|
1235
|
+
timeout_ms: int | None = 1000,
|
|
1236
|
+
) -> "AsyncStream":
|
|
1237
|
+
if config is not None and not isinstance(config, AsyncConfig):
|
|
1238
|
+
raise TypeError("config must be AsyncConfig or None")
|
|
1239
|
+
if read_size is None:
|
|
1240
|
+
read_size = config.transfer_size_bytes if config is not None else ASYNC_DEFAULT_TRANSFER_SIZE
|
|
1241
|
+
return AsyncStream(self, config=config, read_size=read_size, timeout_ms=timeout_ms)
|
|
1242
|
+
|
|
1243
|
+
@staticmethod
|
|
1244
|
+
def _iq_tune(c: _IQTuneInfoC) -> IQTuneInfo:
|
|
1245
|
+
return IQTuneInfo(
|
|
1246
|
+
rf_hz=c.rf_hz,
|
|
1247
|
+
requested_sample_rate_hz=c.requested_sample_rate_hz,
|
|
1248
|
+
actual_sample_rate_hz=c.actual_sample_rate_hz,
|
|
1249
|
+
bandwidth_hz=c.bandwidth_hz,
|
|
1250
|
+
if_hz=c.if_hz,
|
|
1251
|
+
lo_hz=c.lo_hz,
|
|
1252
|
+
tuner_pll_locked=bool(c.tuner_pll_locked),
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
def iq_start(
|
|
1256
|
+
self,
|
|
1257
|
+
frequency_hz: int,
|
|
1258
|
+
sample_rate_hz: int = 2_400_000,
|
|
1259
|
+
bandwidth_hz: int = 0,
|
|
1260
|
+
) -> IQTuneInfo:
|
|
1261
|
+
h = self._require()
|
|
1262
|
+
c = _IQTuneInfoC()
|
|
1263
|
+
_check(
|
|
1264
|
+
_lib.sdrn_iq_start(
|
|
1265
|
+
h, int(frequency_hz), int(sample_rate_hz), int(bandwidth_hz), C.byref(c)
|
|
1266
|
+
),
|
|
1267
|
+
h,
|
|
1268
|
+
)
|
|
1269
|
+
return self._iq_tune(c)
|
|
1270
|
+
|
|
1271
|
+
def iq_tune(self, frequency_hz: int) -> IQTuneInfo:
|
|
1272
|
+
h = self._require()
|
|
1273
|
+
c = _IQTuneInfoC()
|
|
1274
|
+
_check(_lib.sdrn_iq_tune(h, int(frequency_hz), C.byref(c)), h)
|
|
1275
|
+
return self._iq_tune(c)
|
|
1276
|
+
|
|
1277
|
+
def iq_set_sample_rate(self, sample_rate_hz: int) -> int:
|
|
1278
|
+
h = self._require()
|
|
1279
|
+
actual = C.c_uint32()
|
|
1280
|
+
_check(_lib.sdrn_iq_set_sample_rate(h, int(sample_rate_hz), C.byref(actual)), h)
|
|
1281
|
+
return int(actual.value)
|
|
1282
|
+
|
|
1283
|
+
def iq_set_bandwidth(self, bandwidth_hz: int) -> IQTuneInfo:
|
|
1284
|
+
h = self._require()
|
|
1285
|
+
c = _IQTuneInfoC()
|
|
1286
|
+
_check(_lib.sdrn_iq_set_bandwidth(h, int(bandwidth_hz), C.byref(c)), h)
|
|
1287
|
+
return self._iq_tune(c)
|
|
1288
|
+
|
|
1289
|
+
@staticmethod
|
|
1290
|
+
def _tune(c: _TuneInfoC) -> DVBTTuneInfo:
|
|
1291
|
+
return DVBTTuneInfo(
|
|
1292
|
+
rf_hz=c.rf_hz,
|
|
1293
|
+
bandwidth_hz=c.bandwidth_hz,
|
|
1294
|
+
if_hz=c.if_hz,
|
|
1295
|
+
lo_hz=c.lo_hz,
|
|
1296
|
+
tuner_pll_locked=bool(c.tuner_pll_locked),
|
|
1297
|
+
filter_cal_code=c.filter_cal_code,
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
def dvbt_start(self, frequency_hz: int, bandwidth_hz: int) -> DVBTTuneInfo:
|
|
1301
|
+
h = self._require()
|
|
1302
|
+
c = _TuneInfoC()
|
|
1303
|
+
_check(_lib.sdrn_dvbt_start(h, int(frequency_hz), int(bandwidth_hz), C.byref(c)), h)
|
|
1304
|
+
return self._tune(c)
|
|
1305
|
+
|
|
1306
|
+
def dvbt_tune(self, frequency_hz: int, bandwidth_hz: int) -> DVBTTuneInfo:
|
|
1307
|
+
h = self._require()
|
|
1308
|
+
c = _TuneInfoC()
|
|
1309
|
+
_check(_lib.sdrn_dvbt_tune(h, int(frequency_hz), int(bandwidth_hz), C.byref(c)), h)
|
|
1310
|
+
return self._tune(c)
|
|
1311
|
+
|
|
1312
|
+
def wait_lock(self, timeout_ms: int = 4000) -> int:
|
|
1313
|
+
h = self._require()
|
|
1314
|
+
stage = C.c_uint8()
|
|
1315
|
+
_check(_lib.sdrn_dvbt_wait_lock(h, timeout_ms, C.byref(stage)), h)
|
|
1316
|
+
return int(stage.value)
|
|
1317
|
+
|
|
1318
|
+
def signal_quality(self) -> SignalQuality:
|
|
1319
|
+
h = self._require()
|
|
1320
|
+
c = _SignalQualityC()
|
|
1321
|
+
_check(_lib.sdrn_dvbt_get_signal_quality(h, C.byref(c)), h)
|
|
1322
|
+
return SignalQuality(
|
|
1323
|
+
fsm_stage=c.fsm_stage,
|
|
1324
|
+
strength_percent=c.strength_percent,
|
|
1325
|
+
snr_db=c.snr_db if c.snr_valid else None,
|
|
1326
|
+
ber=c.ber if c.ber_valid else None,
|
|
1327
|
+
ber_raw=c.ber,
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
def read(self, size: int = 256 * 1024, timeout_ms: int = 1000) -> bytes:
|
|
1331
|
+
if size <= 0:
|
|
1332
|
+
raise ValueError("size must be positive")
|
|
1333
|
+
h = self._require()
|
|
1334
|
+
buf = (C.c_uint8 * size)()
|
|
1335
|
+
got = C.c_size_t()
|
|
1336
|
+
_check(_lib.sdrn_stream_read(h, buf, size, C.byref(got), timeout_ms), h)
|
|
1337
|
+
return bytes(buf[: got.value])
|
|
1338
|
+
|
|
1339
|
+
def blocks(self, size: int = 256 * 1024, timeout_ms: int = 1000) -> Iterator[bytes]:
|
|
1340
|
+
while not self.closed:
|
|
1341
|
+
block = self.read(size=size, timeout_ms=timeout_ms)
|
|
1342
|
+
if block:
|
|
1343
|
+
yield block
|
|
1344
|
+
|
|
1345
|
+
def set_pid_filter(self, pids: Iterable[int]) -> None:
|
|
1346
|
+
values = [int(p) for p in pids]
|
|
1347
|
+
if not values:
|
|
1348
|
+
raise ValueError("at least one PID is required")
|
|
1349
|
+
array = (C.c_uint16 * len(values))(*values)
|
|
1350
|
+
h = self._require()
|
|
1351
|
+
_check(_lib.sdrn_dvbt_set_pid_filter(h, array, len(values)), h)
|
|
1352
|
+
|
|
1353
|
+
def disable_pid_filter(self) -> None:
|
|
1354
|
+
h = self._require()
|
|
1355
|
+
_check(_lib.sdrn_dvbt_disable_pid_filter(h), h)
|
|
1356
|
+
|
|
1357
|
+
# low level hardware access only
|
|
1358
|
+
def control_read(self, value: int, index: int, length: int) -> bytes:
|
|
1359
|
+
if length <= 0:
|
|
1360
|
+
raise ValueError("length must be positive")
|
|
1361
|
+
h = self._require()
|
|
1362
|
+
buf = (C.c_uint8 * length)()
|
|
1363
|
+
_check(_lib.sdrn_control_read(h, value, index, buf, length), h)
|
|
1364
|
+
return bytes(buf)
|
|
1365
|
+
|
|
1366
|
+
def control_write(self, value: int, index: int, data: bytes) -> None:
|
|
1367
|
+
raw = bytes(data)
|
|
1368
|
+
if not raw:
|
|
1369
|
+
raise ValueError("data must not be empty")
|
|
1370
|
+
h = self._require()
|
|
1371
|
+
buf = (C.c_uint8 * len(raw)).from_buffer_copy(raw)
|
|
1372
|
+
_check(_lib.sdrn_control_write(h, value, index, buf, len(raw)), h)
|
|
1373
|
+
|
|
1374
|
+
def reg_read(self, reg: int, length: int = 1) -> bytes:
|
|
1375
|
+
h = self._require()
|
|
1376
|
+
buf = (C.c_uint8 * length)()
|
|
1377
|
+
_check(_lib.sdrn_reg_read(h, reg, buf, length), h)
|
|
1378
|
+
return bytes(buf)
|
|
1379
|
+
|
|
1380
|
+
def reg_write(self, reg: int, data: bytes) -> None:
|
|
1381
|
+
h = self._require()
|
|
1382
|
+
raw = bytes(data)
|
|
1383
|
+
buf = (C.c_uint8 * len(raw)).from_buffer_copy(raw)
|
|
1384
|
+
_check(_lib.sdrn_reg_write(h, reg, buf, len(raw)), h)
|
|
1385
|
+
|
|
1386
|
+
def demod_read(self, page: int, reg: int, length: int = 1) -> bytes:
|
|
1387
|
+
h = self._require()
|
|
1388
|
+
buf = (C.c_uint8 * length)()
|
|
1389
|
+
_check(_lib.sdrn_demod_read(h, page, reg, buf, length), h)
|
|
1390
|
+
return bytes(buf)
|
|
1391
|
+
|
|
1392
|
+
def demod_write(self, page: int, reg: int, data: bytes) -> None:
|
|
1393
|
+
h = self._require()
|
|
1394
|
+
raw = bytes(data)
|
|
1395
|
+
buf = (C.c_uint8 * len(raw)).from_buffer_copy(raw)
|
|
1396
|
+
_check(_lib.sdrn_demod_write(h, page, reg, buf, len(raw)), h)
|
|
1397
|
+
|
|
1398
|
+
def i2c_read_reg(self, address: int, reg: int, length: int = 1) -> bytes:
|
|
1399
|
+
h = self._require()
|
|
1400
|
+
buf = (C.c_uint8 * length)()
|
|
1401
|
+
_check(_lib.sdrn_i2c_read_reg(h, address, reg, buf, length), h)
|
|
1402
|
+
return bytes(buf)
|
|
1403
|
+
|
|
1404
|
+
def i2c_write_reg(self, address: int, reg: int, data: bytes) -> None:
|
|
1405
|
+
h = self._require()
|
|
1406
|
+
raw = bytes(data)
|
|
1407
|
+
buf = (C.c_uint8 * len(raw)).from_buffer_copy(raw)
|
|
1408
|
+
_check(_lib.sdrn_i2c_write_reg(h, address, reg, buf, len(raw)), h)
|
|
1409
|
+
|
|
1410
|
+
def i2c_read_direct(self, address: int, length: int = 1) -> bytes:
|
|
1411
|
+
if length <= 0:
|
|
1412
|
+
raise ValueError("length must be positive")
|
|
1413
|
+
h = self._require()
|
|
1414
|
+
buf = (C.c_uint8 * length)()
|
|
1415
|
+
_check(_lib.sdrn_i2c_read_direct(h, address, buf, length), h)
|
|
1416
|
+
return bytes(buf)
|
|
1417
|
+
|
|
1418
|
+
def i2c_write_direct(self, address: int, data: bytes) -> None:
|
|
1419
|
+
raw = bytes(data)
|
|
1420
|
+
if not raw:
|
|
1421
|
+
raise ValueError("data must not be empty")
|
|
1422
|
+
h = self._require()
|
|
1423
|
+
buf = (C.c_uint8 * len(raw)).from_buffer_copy(raw)
|
|
1424
|
+
_check(_lib.sdrn_i2c_write_direct(h, address, buf, len(raw)), h)
|
|
1425
|
+
|
|
1426
|
+
def i2c_gate(self, open_: bool) -> None:
|
|
1427
|
+
h = self._require()
|
|
1428
|
+
fn = _lib.sdrn_i2c_gate_open if open_ else _lib.sdrn_i2c_gate_close
|
|
1429
|
+
_check(fn(h), h)
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
class AsyncStream:
|
|
1433
|
+
# single consumer iterator over native async queue
|
|
1434
|
+
|
|
1435
|
+
def __init__(
|
|
1436
|
+
self,
|
|
1437
|
+
device: Device,
|
|
1438
|
+
*,
|
|
1439
|
+
config: AsyncConfig | None = None,
|
|
1440
|
+
read_size: int = ASYNC_DEFAULT_TRANSFER_SIZE,
|
|
1441
|
+
timeout_ms: int | None = 1000,
|
|
1442
|
+
):
|
|
1443
|
+
if read_size <= 0:
|
|
1444
|
+
raise ValueError("read_size must be positive")
|
|
1445
|
+
_async_timeout(timeout_ms)
|
|
1446
|
+
self.device = device
|
|
1447
|
+
self.config = config
|
|
1448
|
+
self.read_size = int(read_size)
|
|
1449
|
+
self.timeout_ms = timeout_ms
|
|
1450
|
+
|
|
1451
|
+
@property
|
|
1452
|
+
def running(self) -> bool:
|
|
1453
|
+
return False if self.device.closed else self.device.async_running
|
|
1454
|
+
|
|
1455
|
+
@property
|
|
1456
|
+
def stats(self) -> AsyncStats:
|
|
1457
|
+
return self.device.async_stats
|
|
1458
|
+
|
|
1459
|
+
def start(self) -> "AsyncStream":
|
|
1460
|
+
# native state machine owns active stream validation
|
|
1461
|
+
self.device.async_start(self.config)
|
|
1462
|
+
return self
|
|
1463
|
+
|
|
1464
|
+
def stop(self) -> None:
|
|
1465
|
+
if not self.device.closed:
|
|
1466
|
+
# terminal producer error stops running but still owns queue.
|
|
1467
|
+
self.device.async_stop()
|
|
1468
|
+
|
|
1469
|
+
def read(self, *, size: int | None = None, timeout_ms: int | None = None) -> AsyncBlock | None:
|
|
1470
|
+
if size is None:
|
|
1471
|
+
size = self.read_size
|
|
1472
|
+
if timeout_ms is None:
|
|
1473
|
+
timeout_ms = self.timeout_ms
|
|
1474
|
+
return self.device.async_read(size=size, timeout_ms=timeout_ms)
|
|
1475
|
+
|
|
1476
|
+
def read_many(self, count: int) -> list[AsyncBlock]:
|
|
1477
|
+
# read count non timeout blocks using stream defaults
|
|
1478
|
+
if count < 0:
|
|
1479
|
+
raise ValueError("count must be non-negative")
|
|
1480
|
+
return [next(self) for _ in range(count)]
|
|
1481
|
+
|
|
1482
|
+
def blocks(self) -> Iterator[AsyncBlock]:
|
|
1483
|
+
# return this stream's single consumer iterator
|
|
1484
|
+
return iter(self)
|
|
1485
|
+
|
|
1486
|
+
def __iter__(self) -> "AsyncStream":
|
|
1487
|
+
return self
|
|
1488
|
+
|
|
1489
|
+
def __next__(self) -> AsyncBlock:
|
|
1490
|
+
# stopped producer may still have queued data or a terminal error
|
|
1491
|
+
while not self.device.closed:
|
|
1492
|
+
try:
|
|
1493
|
+
block = self.device.async_read(size=self.read_size, timeout_ms=self.timeout_ms)
|
|
1494
|
+
except SDRNativeStateError:
|
|
1495
|
+
# stop from another thread wakes blocked consumer
|
|
1496
|
+
if self.device.closed or not self.device.async_running:
|
|
1497
|
+
raise StopIteration from None
|
|
1498
|
+
raise
|
|
1499
|
+
if block is not None:
|
|
1500
|
+
return block
|
|
1501
|
+
raise StopIteration
|
|
1502
|
+
|
|
1503
|
+
def __enter__(self) -> "AsyncStream":
|
|
1504
|
+
return self.start()
|
|
1505
|
+
|
|
1506
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
1507
|
+
if exc_type is None:
|
|
1508
|
+
self.stop()
|
|
1509
|
+
return
|
|
1510
|
+
# keep original exception when teardown also fails
|
|
1511
|
+
try:
|
|
1512
|
+
self.stop()
|
|
1513
|
+
except SDRNativeError:
|
|
1514
|
+
pass
|