Python control library for Uniden SDS100, SDS150, and SDS200 scanners
Project description
sds200-python
Python control and monitoring library for the Uniden SDS100, SDS150, and SDS200 scanners. All three models support USB serial control; the SDS200 also supports native Ethernet control.
The project provides a typed Python API and an sdsctl command-line tool for
scanner discovery, status monitoring, commands, connection profiles, diagnostics,
and live state updates.
[!IMPORTANT] This project is alpha software. The public API may change before version 1.0. It is not affiliated with or endorsed by Uniden.
Features
- USB serial control for SDS100, SDS150, and SDS200 scanners
- Native SDS200 Ethernet control over UDP
- Model detection, aliases, capability reporting, and model-specific limits
- Model-aware handheld battery reporting: optional SDS100 GSI telemetry and SDS150 GCS charge status
- Automatic USB and bounded LAN discovery
- Saved serial, network, and automatic fallback profiles
- Preferred transport ordering with live USB/Ethernet failover
- Typed commands and responses, including documented hold/next/previous navigation
- Structured
GSIand continuousPSIscanner information - Thread-safe synchronized radio state and change events
- Live terminal monitoring
- Exponential reconnect backoff with configurable retry limits
- Traffic tracing, replayable JSON Lines session capture, and deterministic replay
- Bounded health history and failover diagnostics
- JSON Lines events for connection, retry, failover, and state changes
- Discovery-based repair for stale USB paths and scanner IP addresses
- Separate public audio-stream architecture for future network audio
- UDP XML fragment validation, statistics, and bounded retries
- Bash and Zsh tab completion
- Strict MyPy typing, Ruff checks, and hardware-independent tests
Network audio streaming remains on the roadmap but is deferred while control-path reliability matures. Its control-independent API groundwork remains available.
Requirements
- Python 3.11 or newer
- A Uniden SDS100, SDS150, or SDS200
- For USB: scanner connected as a serial device
- For Linux desktop USB access, see the optional udev rule
- For Ethernet: scanner and computer on a trusted local network
Linux USB and Ethernet operation have been validated with an SDS200 running
firmware version 1.26.01. SDS100 USB control has also been validated on firmware
1.26.01. SDS150 support follows Uniden's shared SDS-series remote-command
specification and still needs physical-hardware validation. Explicit SDS200
network hosts work on any platform supported by Python's UDP sockets. Automatic
route detection and /dev/serial/by-id discovery are Linux-specific.
Installation
Install the published package from PyPI:
python -m pip install sds200
Install from source for development:
git clone https://github.com/stevenboyd78/sds200-python.git
cd sds200-python
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
For development:
python -m pip install -e ".[dev]"
Quick start
Find connected scanners
Search USB and directly connected IPv4 networks:
sdsctl discover
Search a specific network:
sdsctl discover --network 192.168.0.0/24 --network-only
Active LAN discovery sends the read-only MDL command to each usable host.
Only scan networks you own or are authorized to probe.
USB serial
Show scanner information using automatic model detection:
sdsctl info
Select a specific model when multiple USB scanners are connected:
sdsctl --model SDS100 info
sdsctl --model SDS150 info
Start the live monitor:
sdsctl monitor
Use an explicit port when automatic discovery is not appropriate:
sdsctl \
--port /dev/serial/by-id/usb-UNIDEN_AMERICA_CORP._SDS200_Serial_Port-if00 \
info
SDS200 Ethernet
sdsctl --host 192.168.0.251 info
sdsctl --host 192.168.0.251 scanner-info
sdsctl --host 192.168.0.251 monitor
The SDS200 virtual serial service uses UDP port 50536 by default.
Connection profiles and fallback
Create a profile directly from USB and LAN discovery:
sdsctl profile discover home \
--network 192.168.0.0/24 \
--prefer network
When both endpoints are found, the profile automatically falls back between Ethernet and USB. The saved preference can be overridden for one command:
sdsctl --profile home --prefer serial monitor
Manual profiles remain supported:
sdsctl profile add network-only --host 192.168.0.251
sdsctl profile add usb-only \
--port /dev/serial/by-id/usb-UNIDEN_AMERICA_CORP._SDS200_Serial_Port-if00 \
--model SDS200
sdsctl profile add handheld --port /dev/ttyACM0 --model SDS150
Profiles are stored in ${XDG_CONFIG_HOME:-~/.config}/sds200/profiles.toml.
Repair stale USB paths or a changed scanner IP address without losing the saved transport preference:
sdsctl profile repair home --network 192.168.0.0/24
sdsctl profile repair home --network 192.168.0.0/24 --dry-run
Reliability, health, and events
sdsctl --profile home health
sdsctl --profile home health --watch 5 --history
sdsctl --profile home health --watch 5 --history --json
sdsctl --profile home events --json
sdsctl --host 192.168.0.251 --trace scanner.trace monitor
Reconnects use capped exponential backoff. Retry forever by default, or set a finite recovery budget:
sdsctl --profile home \
--reconnect-attempts 8 \
--reconnect-initial-delay 1 \
--reconnect-multiplier 2 \
--reconnect-max-delay 30 \
monitor
events --json emits one JSON object per line for connection changes,
transport diagnostics, reconnect scheduling, failovers, and live state changes.
Capabilities, capture, and replay
Show the connected model's limits and validation status:
sdsctl --model SDS100 capabilities
Record a replayable session and then run the same operation without hardware:
sdsctl --model SDS100 --capture sds100-info.jsonl info
sdsctl --replay sds100-info.jsonl --model SDS100 info
Captures can contain local scanner data. Use repeated --redact TEXT options and
inspect files before sharing them. See Session capture and replay.
Typed navigation
The documented HLD, NXT, and PRV operations are available through
typed APIs and CLI commands:
sdsctl --model SDS100 hold SYS 100
sdsctl --model SDS100 next DEPT 200 100 --count 2
sdsctl --model SDS100 previous TGID 300
Targets and indexes are protocol values reported by GSI or GLT. The typed navigation layer is specification-backed and replay-tested; smoke-test it on the intended scanner before relying on it operationally. Prefer a replay fixture when developing navigation logic without a scanner attached.
Raw protocol commands
sdsctl command MDL
sdsctl command VER
sdsctl command GCS # SDS150 charge status
sdsctl command VOL
sdsctl command SQL
sdsctl command STS
Raw command access is intended for documented scanner commands and protocol development. Prefer the typed Python methods when they are available.
Shell completion
Activate Bash completion for the current shell:
eval "$(sdsctl completion bash)"
Enable it whenever Bash starts:
echo 'eval "$(sdsctl completion bash)"' >> ~/.bashrc
For Zsh:
eval "$(sdsctl completion zsh)"
Python API
USB
from sds200 import SDSScanner
with SDSScanner.auto(model="SDS150") as radio:
print(radio.get_model())
print(radio.get_firmware())
print(radio.get_volume())
print(radio.get_squelch())
SDS200 Ethernet
from sds200 import SDSScanner
with SDSScanner.network("192.168.0.251") as radio:
info = radio.get_scanner_info()
print(info.system)
print(info.department)
print(info.channel)
print(info.frequency)
Continuous state updates
from sds200 import SDSScanner
with SDSScanner.network("192.168.0.251") as radio:
radio.on_state_change(
lambda change: print(change.fields, change.current.channel)
)
with radio.scanner_info_push(interval_ms=500):
radio.wait()
Reconnect policy and health history
from sds200 import ReconnectPolicy, SDSScanner
policy = ReconnectPolicy(
initial_delay=1.0,
multiplier=2.0,
max_delay=30.0,
max_attempts=8,
)
with SDSScanner.network("192.168.0.251", reconnect_policy=policy) as radio:
print(radio.health_check().as_dict())
print(radio.health_summary().as_dict())
LAN discovery
from sds200 import discover_network_scanners
for scanner in discover_network_scanners(["192.168.0.0/24"]):
print(scanner.endpoint, scanner.model, scanner.latency_ms)
Project naming
The model-neutral executable is sdsctl. The distribution, Python import package,
configuration directory, and repository remain named sds200; Python applications
should use SDSScanner, while the historical SDS200 class name remains an alias.
Security
The SDS200 network-control protocol is unauthenticated and unencrypted. Keep it
on a trusted LAN or access it through a secured VPN. Do not expose UDP port
50536 directly to the public Internet.
This library is not a safety-critical or emergency-dispatch system. Do not rely on it as the sole means of receiving urgent communications.
See SECURITY.md for vulnerability reporting and docs/transports.md for transport limitations.
Documentation
- Supported scanner models
- Control transports
- LAN discovery and profiles
- Fallback profiles
- Reliability and observability
- Audio subsystem architecture
- Contributing
- Support
- Changelog
- Release process
Development
python -m pip install -e ".[dev]"
ruff check .
mypy src/sds200
pytest
python scripts/check_docs.py
python -m build
python -m twine check dist/*
Tests must not require physical scanner hardware. Hardware validation is documented separately in pull requests and release notes.
Project status
Version 0.8.0 adds model-aware SDS100 and SDS150 USB support while preserving
the validated SDS200 USB, Ethernet, fallback, monitoring, profile, and
reliability paths. SDS100 and SDS150 hardware validation is still in progress.
API compatibility is not guaranteed until version 1.0.
See CHANGELOG.md for development history and planned changes.
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sds200-0.9.0.tar.gz.
File metadata
- Download URL: sds200-0.9.0.tar.gz
- Upload date:
- Size: 91.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff70d72ab59efe75267e637989eb8e132fe222a9370c072622d4f12b62428721
|
|
| MD5 |
1d8de57d55edb6c41278c30db177b1c2
|
|
| BLAKE2b-256 |
b5f971af1a98ced0243961cb7c36de38d7881a8d3412a7da6a95817b26a37e0a
|
Provenance
The following attestation bundles were made for sds200-0.9.0.tar.gz:
Publisher:
release.yml on stevenboyd78/sds200-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sds200-0.9.0.tar.gz -
Subject digest:
ff70d72ab59efe75267e637989eb8e132fe222a9370c072622d4f12b62428721 - Sigstore transparency entry: 2239397051
- Sigstore integration time:
-
Permalink:
stevenboyd78/sds200-python@74e4e92618775cd59cc35b07b1d30be73d456dc4 -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/stevenboyd78
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74e4e92618775cd59cc35b07b1d30be73d456dc4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sds200-0.9.0-py3-none-any.whl.
File metadata
- Download URL: sds200-0.9.0-py3-none-any.whl
- Upload date:
- Size: 62.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7ffa92f106b6edab1113f296b70f7480db03c91b593a0a5f0af2bcd2e65555c
|
|
| MD5 |
57ba9908cc3d3c0d777ccf7c2c92f7e1
|
|
| BLAKE2b-256 |
1081fe80ba8c823bc5225379af246f857c7797e72804b46e625fe2782cf7c426
|
Provenance
The following attestation bundles were made for sds200-0.9.0-py3-none-any.whl:
Publisher:
release.yml on stevenboyd78/sds200-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sds200-0.9.0-py3-none-any.whl -
Subject digest:
f7ffa92f106b6edab1113f296b70f7480db03c91b593a0a5f0af2bcd2e65555c - Sigstore transparency entry: 2239397379
- Sigstore integration time:
-
Permalink:
stevenboyd78/sds200-python@74e4e92618775cd59cc35b07b1d30be73d456dc4 -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/stevenboyd78
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74e4e92618775cd59cc35b07b1d30be73d456dc4 -
Trigger Event:
push
-
Statement type: