Skip to main content

Python control library for Uniden SDS100, SDS150, and SDS200 scanners

Project description

sds200-python

CI Python 3.11–3.14 Development status: alpha License: MIT

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 and opt-in recovery
  • Typed commands and responses, including documented hold/next/previous navigation
  • Structured GSI and continuous PSI scanner 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 plus failover and preferred-recovery diagnostics
  • JSON Lines events for connection, retry, failover, and state changes
  • Discovery-based repair for stale USB paths and scanner IP addresses
  • Hardware-validated SDS200 network audio over RTSP/RTP
  • Native G.711 mu-law decoding and streaming PCM WAV recording
  • UDP XML fragment validation, statistics, and bounded retries
  • Bash and Zsh tab completion
  • Strict MyPy typing, Ruff checks, and hardware-independent tests

Network audio remains independent from scanner control, so recording does not open or affect the USB serial or UDP control transport.

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, Ethernet control, and RTSP/RTP audio recording 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 TCP and 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.

SDS200 network audio recording

Record the scanner's RTSP/RTP audio directly to an 8 kHz mono signed 16-bit PCM WAV file without requiring FFmpeg:

sdsctl --host 192.168.0.251 audio \
  --output scanner-audio.wav \
  --duration 30

Omit --duration to record until Ctrl+C. Existing files are protected unless overwrite is explicitly enabled:

sdsctl --host 192.168.0.251 audio \
  --output scanner-audio.wav \
  --force

The scanner requires a nonstandard single RTP client port during RTSP SETUP. The built-in transport handles that negotiation, receives payload type 0 PCMU, decodes it natively, and finalizes the WAV header during orderly shutdown.

Each recording summary reports estimated packet loss, duplicates, late and malformed packets, and RTP timestamp discontinuities. A five-minute SDS200 hardware soak received 7,500 packets and 2,400,000 audio samples with every reliability counter remaining at zero.

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

Preferred recovery is disabled by default. Enable validated return to the saved preferred endpoint when creating a fallback profile:

sdsctl profile discover home \
  --network 192.168.0.0/24 \
  --prefer network \
  --recover-preferred \
  --recovery-probe-interval 30 \
  --recovery-stability-window 5 \
  --recovery-cooldown 30

Override the saved recovery policy for one process:

sdsctl --profile home \
  --recover-preferred \
  --recovery-probe-interval 15 \
  monitor

Recovery opens the inactive preferred endpoint, sends MDL, verifies that an SDS200 responds, waits through the stability window, probes again, and promotes only while no request/response command is pending. Continuous PSI updates are restarted after promotion.

Manual profiles remain supported, including explicit fallback profiles:

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
sdsctl profile add home \
  --port /dev/serial/by-id/usb-UNIDEN_AMERICA_CORP._SDS200_Serial_Port-if00 \
  --host 192.168.0.251 \
  --prefer network \
  --recover-preferred

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, preferred recoveries, 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

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.11.0 adds hardware-validated SDS200 RTSP/RTP network audio, native PCM WAV recording, and RTP reliability telemetry while preserving the existing USB, Ethernet-control, fallback, monitoring, profile, capture, and replay paths. SDS100 USB support is hardware-validated; 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

sds200-0.12.0.tar.gz (132.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sds200-0.12.0-py3-none-any.whl (88.5 kB view details)

Uploaded Python 3

File details

Details for the file sds200-0.12.0.tar.gz.

File metadata

  • Download URL: sds200-0.12.0.tar.gz
  • Upload date:
  • Size: 132.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for sds200-0.12.0.tar.gz
Algorithm Hash digest
SHA256 34086eee876c83b0ad61d0af50a07689cbc6a6a2f1cb305dad0cfe1c73e9c597
MD5 dcac7cf37a1a558d8b583fb002426e7b
BLAKE2b-256 db27c6e69eddc93dca830f4639910aa95954c902091d3ffc0aa1274a5c4cef54

See more details on using hashes here.

Provenance

The following attestation bundles were made for sds200-0.12.0.tar.gz:

Publisher: release.yml on stevenboyd78/sds200-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sds200-0.12.0-py3-none-any.whl.

File metadata

  • Download URL: sds200-0.12.0-py3-none-any.whl
  • Upload date:
  • Size: 88.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for sds200-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eaa624d999f3aad035b68916940a3cb1fae963aac223cb470cfd539a44f31e86
MD5 7d31050b2bbaded882028d6fe7e9a38a
BLAKE2b-256 320d32593370b42ff42b15a0856b81cd0c028d47d821ccf4de96f800d2cd2e38

See more details on using hashes here.

Provenance

The following attestation bundles were made for sds200-0.12.0-py3-none-any.whl:

Publisher: release.yml on stevenboyd78/sds200-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page