Skip to content
reflexive-labsPublic

About

A software defined radio for MacOS. Native app, CLI, and MCP.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1,314 Commits

Folders and files

Repository files navigation

Leyline SDR

CI Licence: Apache-2.0 and GPL-3.0-or-later

Leyline SDR lets you listen to, scan, record and decode radio on a Mac. It works with an RTL-SDR or a HackRF, plugged in or served over the network with rtl_tcp. It only receives; it never transmits.

Status: pre-release. There are no binaries yet; you build it from source.

Requirements: macOS 26 with Xcode 26 (the Swift 6.2 toolchain), Homebrew, and Go 1.27 or later. A radio is optional: an IQ recording plays through the same pipeline.

How it works

A background daemon, leylined (Swift), owns the radio and does all the signal processing. The ley command (Go) drives it over a Unix socket using one gRPC contract, leyline.v1. The Mac app, a script or an agent speaks the same contract. Each is a peer of ley; none has a second path to the hardware.

What works today

  • Radios. An RTL-SDR or HackRF on USB, an RTL-SDR another machine serves with rtl_tcp (ley devices attach rtltcp pi.local:1234, remembered from then on), or an IQ recording.
  • Listening. NFM, WFM, AM, USB, LSB and CW demodulation, squelch, CTCSS and DCS tone detection, and audio out through the Mac's speakers. Two channels can play from one radio, and a second terminal can adjust the channel the first is playing.
  • Seeing the band. Live spectrum, waterfall and persistence views in the terminal, band-level meters, and a few seconds of a channel's waveform.
  • Scanning. A band scan with an energy detector that reports what it found and never guesses a modulation, and ley monitor, which logs each transmission on a band.
  • Recording. Audio or IQ to files through a job the daemon runs, and playback of what was recorded.
  • Decoding. APRS, SAME weather alerts, marine AIS and ADS-B aircraft, with ley watch to notify on matching records.
  • Bands and bookmarks. Channel plans for common bands, bookmarks, and CHIRP CSV import.
  • Scripts and agents. --json on every verb, and ley mcp, which serves the same verbs as MCP tools.
  • The Mac app, built from source with make app-run. It shows a spectrum and waterfall, plays and tunes a channel, records, lists bands and bookmarks, and has an inspector for signal, tuning error, deviation and recent transmissions.

"Where things stand" below lists what is partial or not built yet.

Receive only

Leyline has no transmit path. Listening is legal in most places, but some countries and states restrict decoding, recording or sharing certain transmissions. Check the rules where you are.

Install (from source)

brew install go
brew install librtlsdr                 # for a local RTL-SDR
brew install hackrf                    # for a local HackRF; install either or both drivers
git clone https://github.com/reflexive-labs/leysdr.git && cd leysdr
make go swift-release fixtures       # go/bin/ley + leyfix, engine/.build/release/leylined, IQ fixtures
make install-decoders                # the APRS, SAME, AIS and ADS-B decoders
export PATH=$PWD/go/bin:$PATH
ley daemon start --bin $PWD/engine/.build/release/leylined

scripts/bootstrap-mac.sh runs the same steps; pass --rtl-only or --hackrf-only to install just one native driver. The daemon builds and runs if either or both drivers are absent. It logs each unavailable backend once and carries on with the others, rtl_tcp and file radios. Restart the daemon after installing a missing library. ley daemon install --bin … instead of start writes a LaunchAgent so the daemon starts at login; after that ley daemon start|stop|status|logs go through launchd. Starting at login, a radio on another machine over rtl_tcp, where the daemon keeps its files and uninstalling are in docs/guide/install.md; when something fails, docs/guide/troubleshooting.md. Building for development (the gate, make reload, Linux) is docs/dev/setup.md.

Quickstart

Five commands take you from nothing to a station playing. The output below was recorded against the contract's fake daemon (go/internal/fakedaemon), so your ids, model and levels will differ.

$ ley devices                           # 1. is my radio visible?
MODEL                     STATE      RANGE                    RATES                GAIN
Generic RTL2832U (R820T)  AVAILABLE  24.000 MHz to 1.766 GHz  0.25..3.2 MSPS (11)  TUNER 0–49.6 dB auto

$ ley tune 146.52                       # 2. listen: a bare number is MHz; mode and squelch are chosen for you
using NFM: 2 m amateur band default
Listening to 146.520 MHz (NFM, 2 m amateur)
Radio Generic RTL2832U (R820T), gain auto
Squelch auto → -80 dBFS (10 dB above the band's noise floor, -90 dBFS).
Ctrl-C stops.
From another terminal: ley set squelch -50 · ley set gain 30 · ley spectrum
146.520 MHz NFM  signal -39 dBFS  audio
transmission  0.3 s  peak snr 51 dB  peak -39 dBFS
146.520 MHz NFM  signal -69 dBFS  muted, waiting for a signal

Leave that running and open a second terminal:

$ ley set squelch -45                   # 3. adjust it while it plays
squelch -80 dBFS → -45 dBFS on 146.520 MHz NFM (channel 1)

$ ley set gain 30                       # 4. the radio's gain snaps to what the tuner can do
gain auto → 29.7 dB on the radio (TUNER)

$ ley spectrum                          # 5. see the band the radio is tuned to
146.520 MHz  span 2.400 MHz  floor -100 dBFS  145.320 MHz to 147.720 MHz
1024 bins of 2.344 kHz
 -35 dBFS|                                   :
         |                                  .|
         |                                  ||
         |                                  ||
 -97     |..................................--..................................
         |
-105     ------|-------------|--------------|--------------|-------------|------
          145.500 MHz   146.000 MHz    146.500 MHz    147.000 MHz   147.500 MHz
peak    146.521 MHz  -40 dBFS  60 dB above the floor
tune with: ley tune 146.521

Back in the first terminal the session reports what the other one did (another terminal set the squelch to -45 dBFS) and keeps playing. ley scan 144M..148M sweeps a band and lists what it found with frequency, width, SNR and how often it was seen; ley waterfall and ley phosphor show what comes and goes; ley bands 146.52 shows which band a frequency is in and what tune will do with it. Bare ley shows the current state and what to type next; ley help glossary explains the words (capture, channel, dBFS, FFT, squelch); ley help presets lists names like noaa and calling that tune accepts in place of a frequency. The task-by-task walkthrough, including --json and exit codes for scripts and what to do when something fails, is docs/guide/using-ley.md.

Without a radio

make fixtures generates IQ recordings of known signals (an NFM tone, AM, SSB, CW, a calibrated noise floor, a band with four carriers); ley play fixtures/nfm_tone.cf32 --loop plays one through the same pipeline as a radio, and you should hear a 1 kHz tone. The whole test suite runs this way.

Layout

path what
proto/leyline/v1 the contract: control, telemetry and bulk planes; jobs and resources
engine/ SwiftPM package: EngineCore (devices, capture, DSP, sinks) and LeylineDaemon (leylined: services, session store, jobs). GPL-3.0-or-later
app/ SwiftPM package: the Mac app and its client façade (docs/dev/app.md)
swift/LeylineProto SwiftPM package: the generated Swift contract (make proto), shared by the engine and the app
go/ Go module: the pkg/leyline client library, ley, the decoders' binaries, leyfix (fixture generator), leyeval (agent evals), the fake daemon the ley tests run against, and the end-to-end tests
decoders/ decoder manifests (APRS, SAME, AIS, ADS-B, and the iqstat test decoder); make install-decoders installs them
evals/ scenarios that grade an agent's use of ley mcp (docs/dev/evals.md)
scripts/ setup, code generation, licence checks, bundling the app
third_party/ licence texts of the dependencies (third_party/licenses/MANIFEST.txt)
fixtures/ generated IQ signals with expected demod outputs (make fixtures; gitignored)
docs/ docs/README.md is the map: guide/ for using Leyline, reference/ for ley and the contract, design/ for why it is built this way, dev/ for contributors, decisions/, plans/

Where things stand

"Works" means built, tested in CI, and used with a real radio or a recording. The milestone detail is docs/plans/build-order.md.

feature status notes
RTL-SDR on USB works tested on real RF with a Nooelec RTL-SDR
HackRF on USB works HackRF One and HackRF Pro, through libhackrf
RTL-SDR over rtl_tcp works no authentication or encryption on the link
IQ file playback works .cf32 with a JSON sidecar; files from other software are not read yet
Demodulation: NFM, WFM, AM, USB, LSB, CW works with squelch and audio out on macOS
CTCSS and DCS detection works
Terminal views works spectrum, waterfall, phosphor, scope, levels, waveform
Band scan and ley monitor works energy detector; no modulation guess
Two channels on one radio, several clients works
Recording and playback works audio (WAV) or IQ; a squelch-gated recording writes one file per transmission
Bands, channel plans, bookmarks, CHIRP import works in ley and the app
Decoders: APRS, SAME, AIS, ADS-B works more are planned (docs/plans/decoders.md)
ley watch notifications works webhook, shell command or macOS notification
Speech-to-text works the speech decoder sends each over to OpenAI or an OpenAI-compatible server on your key, for gated recordings (ley transcribe, the Library) and live channels; no on-device engine yet
The assistant works in the app, on your Anthropic, OpenAI or OpenAI-compatible key; runs the radio with ley mcp's tools, and asks first before paid work or taking the radio from another listener
Provider keys and usage works ley ai and Settings > AI; the key stays in the login keychain
MCP adapter (ley mcp) partial signal identification and transcript tools are not implemented (docs/plans/mcp.md)
Mac app partial starts its own daemon as a login item; the layer 2 parameter inspector is incomplete
Signed app bundle and installer partial make release builds a notarized DMG that carries the drivers and updates through Sparkle; released to a small group of testers since 0.1.0, not yet public (docs/plans/distribution.md)
Terminal dashboard planned
Durable watch jobs partial kept decode jobs survive a daemon restart; other jobs do not
Channel occupancy and burst capture planned docs/plans/band-watching.md
Audio spectrogram (ley sonogram) planned
Transmit not planned Leyline is receive only

Writing a client

Every verb's --json is the standard proto3 JSON mapping of the contract, and everything a person reads goes to stderr, so stdout is always parseable (ley help scripting). If you write your own client, read docs/reference/clients.md first: the daemon's HTTP/2 stack drops connections that ping on every data frame, which grpc-go and grpc-python do by default, and the fix is one dial option.

Contributing, security, licence

CONTRIBUTING.md says how to build, test and propose a change. SECURITY.md describes what the daemon trusts (a local socket, your user, no authentication) and how to report a problem.

Everything here is open source. The engine under engine/ (leylined) is GPL-3.0-or-later because it links librtlsdr; everything else, the leyline.v1 contract, the generated code, the client library and ley included, is Apache-2.0, so a program that talks to the daemon can be licensed however you like. NOTICE lists what the binaries carry, docs/decisions/D2-licensing.md explains the split, and TRADEMARK.md covers the name. Problems and questions: open an issue on the repository.

About

A software defined radio for MacOS. Native app, CLI, and MCP.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages