sds200 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sds200/__init__.py ADDED
@@ -0,0 +1,145 @@
1
+ from .audio import AudioChunk, AudioStream, AudioTransport
2
+ from .commands import (
3
+ GetChargeStatus,
4
+ GetFirmware,
5
+ GetModel,
6
+ GetScannerInfo,
7
+ GetSquelch,
8
+ GetStatus,
9
+ GetVolume,
10
+ SetSquelch,
11
+ SetVolume,
12
+ StartScannerInfoPush,
13
+ )
14
+ from .device import (
15
+ DEFAULT_SDS200_PATTERN,
16
+ DEFAULT_SDS_PATTERN,
17
+ ScannerDevice,
18
+ discover_scanners,
19
+ )
20
+ from .discovery import (
21
+ DEFAULT_DISCOVERY_WORKERS,
22
+ NetworkScanner,
23
+ discover_network_scanners,
24
+ local_ipv4_networks,
25
+ )
26
+ from .exceptions import (
27
+ CommandRejectedError,
28
+ SDS200Error,
29
+ SDSScannerError,
30
+ UnsupportedScannerFeatureError,
31
+ UnsupportedScannerModelError,
32
+ )
33
+ from .fallback import FallbackTransport, TransportCandidate
34
+ from .models import (
35
+ ChargeStatus,
36
+ FirmwareResponse,
37
+ HealthSummary,
38
+ ModelResponse,
39
+ Packet,
40
+ RadioEvent,
41
+ RadioHealth,
42
+ ScannerInfo,
43
+ ScannerNode,
44
+ StatusResponse,
45
+ ValueResponse,
46
+ )
47
+ from .network import DEFAULT_UDP_PORT, UdpDatagramDecoder, UdpTransport
48
+ from .profiles import (
49
+ TRANSPORT_PREFERENCES,
50
+ ConnectionProfile,
51
+ ProfileKind,
52
+ ProfileRepairResult,
53
+ ProfileStore,
54
+ TransportPreference,
55
+ profile_from_discovery,
56
+ repair_profile,
57
+ )
58
+ from .radio import SDS200, SDSScanner
59
+ from .reliability import HealthHistory, HealthThresholds, ReconnectPolicy
60
+ from .scanner import (
61
+ SUPPORTED_SCANNER_MODELS,
62
+ ScannerCapabilities,
63
+ ScannerModel,
64
+ capabilities_for_model,
65
+ infer_model_from_device_name,
66
+ normalize_model_name,
67
+ )
68
+ from .state import RadioState, RadioStateSnapshot, StateChange
69
+ from .transport import (
70
+ ControlTransport,
71
+ SerialTransport,
72
+ TransportDiagnostic,
73
+ )
74
+
75
+ __all__ = [
76
+ "AudioChunk",
77
+ "AudioStream",
78
+ "AudioTransport",
79
+ "ChargeStatus",
80
+ "CommandRejectedError",
81
+ "ConnectionProfile",
82
+ "ControlTransport",
83
+ "DEFAULT_DISCOVERY_WORKERS",
84
+ "DEFAULT_SDS_PATTERN",
85
+ "DEFAULT_SDS200_PATTERN",
86
+ "DEFAULT_UDP_PORT",
87
+ "FallbackTransport",
88
+ "FirmwareResponse",
89
+ "GetChargeStatus",
90
+ "GetFirmware",
91
+ "GetModel",
92
+ "GetScannerInfo",
93
+ "GetSquelch",
94
+ "GetStatus",
95
+ "GetVolume",
96
+ "HealthHistory",
97
+ "HealthSummary",
98
+ "HealthThresholds",
99
+ "ModelResponse",
100
+ "NetworkScanner",
101
+ "Packet",
102
+ "ProfileKind",
103
+ "ProfileRepairResult",
104
+ "ProfileStore",
105
+ "RadioEvent",
106
+ "RadioHealth",
107
+ "RadioState",
108
+ "RadioStateSnapshot",
109
+ "ReconnectPolicy",
110
+ "SDS200",
111
+ "SDS200Error",
112
+ "SDSScanner",
113
+ "SDSScannerError",
114
+ "SUPPORTED_SCANNER_MODELS",
115
+ "ScannerCapabilities",
116
+ "ScannerDevice",
117
+ "ScannerInfo",
118
+ "ScannerModel",
119
+ "ScannerNode",
120
+ "SerialTransport",
121
+ "SetSquelch",
122
+ "SetVolume",
123
+ "StartScannerInfoPush",
124
+ "StateChange",
125
+ "StatusResponse",
126
+ "TRANSPORT_PREFERENCES",
127
+ "TransportCandidate",
128
+ "TransportPreference",
129
+ "TransportDiagnostic",
130
+ "UdpDatagramDecoder",
131
+ "UdpTransport",
132
+ "UnsupportedScannerFeatureError",
133
+ "UnsupportedScannerModelError",
134
+ "ValueResponse",
135
+ "capabilities_for_model",
136
+ "discover_network_scanners",
137
+ "discover_scanners",
138
+ "infer_model_from_device_name",
139
+ "local_ipv4_networks",
140
+ "normalize_model_name",
141
+ "profile_from_discovery",
142
+ "repair_profile",
143
+ ]
144
+
145
+ __version__ = "0.8.2"
sds200/audio.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from datetime import UTC, datetime
6
+ from typing import Protocol, Self, runtime_checkable
7
+
8
+ from .events import EventBus
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class AudioChunk:
13
+ """Opaque audio bytes received from an audio transport.
14
+
15
+ Codec and packet framing intentionally remain transport-specific until
16
+ a scanner audio protocol is implemented and validated against hardware.
17
+ """
18
+
19
+ data: bytes
20
+ received_at: datetime = field(default_factory=lambda: datetime.now(UTC))
21
+
22
+
23
+ AudioChunkHandler = Callable[[AudioChunk], None]
24
+
25
+
26
+ @runtime_checkable
27
+ class AudioTransport(Protocol):
28
+ """Lifecycle contract for future USB, RTP, or RTSP audio transports."""
29
+
30
+ @property
31
+ def endpoint(self) -> str: ...
32
+
33
+ @property
34
+ def running(self) -> bool: ...
35
+
36
+ def start(self, handler: AudioChunkHandler) -> None: ...
37
+
38
+ def stop(self) -> None: ...
39
+
40
+
41
+ class AudioStream:
42
+ """Transport-independent audio event stream.
43
+
44
+ Audio is deliberately separate from :class:`sds200.SDSScanner`, so control
45
+ failover and protocol parsing cannot be destabilized by audio work.
46
+ """
47
+
48
+ def __init__(self, transport: AudioTransport) -> None:
49
+ self.transport = transport
50
+ self.events = EventBus()
51
+
52
+ @property
53
+ def endpoint(self) -> str:
54
+ return self.transport.endpoint
55
+
56
+ @property
57
+ def running(self) -> bool:
58
+ return self.transport.running
59
+
60
+ def on_chunk(self, callback: AudioChunkHandler) -> Callable[[], None]:
61
+ return self.events.subscribe("chunk", callback)
62
+
63
+ def start(self) -> None:
64
+ self.transport.start(self._receive_chunk)
65
+
66
+ def stop(self) -> None:
67
+ self.transport.stop()
68
+
69
+ def __enter__(self) -> Self:
70
+ self.start()
71
+ return self
72
+
73
+ def __exit__(self, *_: object) -> None:
74
+ self.stop()
75
+
76
+ def _receive_chunk(self, chunk: AudioChunk) -> None:
77
+ self.events.emit("chunk", chunk)