sdr-rtltcp-discovery 0.1.0

mDNS/DNS-SD discovery for rtl_tcp servers — announce and browse the _rtl_tcp._tcp.local. service. Pure-Rust (mdns-sd), no Avahi/Bonjour, no async runtime.
Documentation

sdr-rtltcp-discovery

CI crates.io docs.rs License: MIT OR Apache-2.0

mDNS/DNS-SD discovery for rtl_tcp-compatible servers. A server announces itself on the local network; a client browses for one without the user hand-typing host:port. Service type: _rtl_tcp._tcp.local. — the same string ShinySDR, rtl_tcp_client, and other SDR tools use by convention, so this interops with them where they implement discovery.

Pure Rust: built on mdns-sd, so there's no Avahi / Bonjour system dependency and no async runtime. The mDNS daemon runs on its own thread internally; the browser spawns one more thread that translates daemon events into the typed events below. libc is the only other dependency (one gethostname(3) call on Unix; a "localhost" stub elsewhere).

Browse for servers

use sdr_rtltcp_discovery::{Browser, DiscoveryEvent};

# fn main() -> Result<(), Box<dyn std::error::Error>> {
let _browser = Browser::start(|event| match event {
    DiscoveryEvent::ServerAnnounced(server) => {
        let addr = server.addresses.first().copied();
        println!(
            "found {:?} @ {:?}:{}  (tuner={}, {} gain steps)",
            server.txt.nickname, addr, server.port,
            server.txt.tuner, server.txt.gains,
        );
    }
    DiscoveryEvent::ServerWithdrawn { instance_name } => {
        println!("{instance_name} went away");
    }
})?;

// The `Browser` runs until dropped. Park, poll a channel, integrate
// into your event loop — whatever suits. Here we just wait a bit.
std::thread::sleep(std::time::Duration::from_secs(10));
# Ok(())
# }

Announce a server

use sdr_rtltcp_discovery::{AdvertiseOptions, Advertiser, TxtRecord, local_hostname};

# fn main() -> Result<(), Box<dyn std::error::Error>> {
let host = local_hostname();           // e.g. "shack-pi"
let txt = TxtRecord {
    tuner: "R820T".to_string(),
    version: env!("CARGO_PKG_VERSION").to_string(),
    gains: 29,
    nickname: host.clone(),
    txbuf: None,
    codecs: None,
    auth_required: None,
};

let _ad = Advertiser::announce(AdvertiseOptions {
    port: 1234,
    instance_name: format!("{host} rtl-sdr"),
    hostname: String::new(),           // empty -> auto-derive from system hostname
    txt,
})?;

// The advertisement stays live until the `Advertiser` is dropped (or
// `.stop()` is called), which unregisters the service cleanly.
# Ok(())
# }

TXT-record schema

Each advertisement carries a small TXT record so a browsing client can show useful info before it ever connects. Keys are kept short (under ~10 chars) to keep the whole registration inside one UDP packet:

Key Type Meaning
tuner string Tuner family the dongle reports (R820T, E4000, …).
version string Advertiser version — clients render "running X" vs. "unknown rtl_tcp source".
gains u32 Number of discrete gain steps; the gain table is not on the wire.
nickname string User-editable label; defaults to the host's hostname.
txbuf usize, optional Server buffer depth in bytes — a latency hint. Omitted when unset.
codecs u8, optional Bitmask of stream codecs the server will negotiate (server-specific). Omitted when unset.
auth_required bool, optional true if the server wants a pre-shared key before connect. Only Some(true) is emitted; absence ⇒ "no auth".

TxtRecord::to_properties enforces the per-entry 255-byte limit and a 400-byte total budget; from_properties parses an incoming record back, treating missing optional keys as None.

Minimum supported Rust version

1.95. Bumping the MSRV is a minor-version change.

License

Dual-licensed under either of:

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.