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/_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