Skip to content

Doppler CLI

The doppler compose CLI is a pipeline orchestrator for Doppler signal processing chains. It lets you wire sources, DSP blocks, and sinks together with a single command — or a declarative compose file — and manages the lifetime of every process.

Install:

pip install "doppler-dsp[cli]"

Quick start

# Scaffold a named chain and start it
doppler compose init tone specan --name my-chain
doppler compose up my-chain

# Or use a hex ID (auto-generated without --name)
doppler compose init tone fir specan
doppler compose up          # defaults to most recently created

# Check what's running
doppler ps

# Tear it down
doppler stop my-chain

Architecture

Each block in a pipeline runs as an independent OS process. Blocks exchange IQ samples over the NATS JetStream PUSH/PULL work-queue tier. The compose runner assigns ports, spawns processes, and tracks their state in ~/.doppler/chains/.

flowchart TB
    subgraph Runner ["doppler compose up"]
        CFG["compose file\n~/.doppler/chains/a3f7c2.yml"]
        STATE["chain state\n~/.doppler/chains/a3f7c2.json"]
    end

    subgraph Pipeline ["Signal Chain"]
        direction TB
        SRC["Source\n(e.g. tone)"]
        DSP["DSP Block\n(e.g. fir)"]
        SINK["Sink\n(e.g. specan)"]

        SRC -- "PUSH  nats://127.0.0.1:4222/dp-chain-5600" --> DSP
        DSP -- "PUSH  nats://127.0.0.1:4222/dp-chain-5601" --> SINK
    end

    Runner -- "spawns + tracks" --> Pipeline

Subject convention: every block publishes its output and subscribes to its input on a NATS subject. Sources publish only. Sinks subscribe only. Each inter-block link gets a unique subject (dp-chain-<token>) on the local broker — <token> is the same integer doppler compose has always called a "port" (see doppler.cli.ports), now namespacing a NATS subject instead of binding an OS TCP port.

source   →  binds   PUSH  dp-chain-5600
fir      →  connects PULL dp-chain-5600,  binds PUSH dp-chain-5601
specan   →  connects PULL dp-chain-5601

Compose file

doppler compose init <BLOCKS...> scaffolds a fully-resolved compose file with all defaults and auto-assigned ports filled in.

id: a3f7c2

source:
  type: tone
  sample_rate: 2048000.0
  center_freq: 0.0
  tone_freq: 100000.0
  tone_power: -20.0
  noise_floor: -90.0
  port: 5600          # output port this block binds

chain:
  - fir:
      taps: []
      port: 5601      # output port this block binds

sink:
  type: specan
  mode: web        # terminal mode requires a foreground TTY; use web in pipelines
  center: 0.0
  span: null
  rbw: null
  level: null
  web_port: 8080
  web_host: 127.0.0.1

Ports default to auto-assigned from the range 5600–5700. Specify them explicitly to pin a chain to fixed addresses.


Commands

Chain lifecycle

Command Description
doppler ps List all running chains with status and uptime
doppler stop <ID> Graceful shutdown (SIGTERM all block processes)
doppler kill <ID> Immediate shutdown (SIGKILL all block processes)
doppler inspect <ID> Print resolved config, PIDs, and port assignments
doppler logs <ID> [--block NAME] Stream stdout/stderr from a chain or block

Compose

Command Description
doppler compose init <BLOCKS...> Scaffold a compose file with defaults
doppler compose init <BLOCKS...> --name NAME Give the chain a human-readable name
doppler compose init <BLOCKS...> --out FILE Write to a specific path
doppler compose up [FILE\|NAME] Spawn all blocks described in FILE (defaults to latest)
doppler compose down <ID\|NAME> Stop a running chain (alias for stop)

Block catalog

These are the built-in blocks shipped with the doppler-dsp[cli] extra. Custom blocks can be added via a Dopplerfile — they appear alongside these in doppler compose init.

tone — synthetic source

Generates a calibrated complex tone plus AWGN. Good for validating filter frequency response before connecting a real IQ source.

Field Default Description
sample_rate 2048000.0 Output sample rate (Hz)
center_freq 0.0 Nominal center frequency (Hz, metadata)
tone_freq 100000.0 Tone offset from DC (Hz)
tone_power -20.0 Tone power (dBm)
noise_floor -90.0 AWGN floor (dBm)

fir — FIR filter

Applies a real FIR filter to the IQ stream. Design taps with any standard tool (e.g. scipy.signal.firwin).

Field Default Description
taps [] Filter coefficients (passthrough if empty)

Example — design a 101-tap lowpass and use it in a chain:

from scipy.signal import firwin
taps = firwin(101, cutoff=0.1).tolist()

Then set taps in the compose file, or patch it:

doppler compose init tone fir specan --out chain.yml
# edit chain.yml: set fir.taps: [...]
doppler compose up chain.yml

specan — spectrum analyzer sink

Displays the spectrum of the incoming IQ stream. Connects to the doppler-specan terminal or web UI.

Field Default Description
mode "web" "terminal" or "web"
center 0.0 Center frequency (Hz)
span null Display span (Hz); defaults to full bandwidth
rbw null Resolution bandwidth (Hz)
level null Reference level, top of display (dBm)
web_port 8080 HTTP port for web mode
web_host "127.0.0.1" Address the web UI binds in web mode

Typical workflows

Measure a filter's frequency response

doppler compose init tone fir specan --out filter_test.yml
# Edit filter_test.yml:
#   fir.taps: [<your taps>]
#   specan.span: 500000
doppler compose up filter_test.yml
flowchart TB
    T["tone\n−20 dBm @ 100 kHz"]
    F["fir\nlowpass taps"]
    S["specan\n500 kHz span"]
    T --> F --> S

Connect a real IQ source

Replace tone with any NATS publisher emitting doppler-framed IQ:

source:
  type: socket
  address: nats://192.168.1.10:4222/iq

A nats:// endpoint needs a running NATS broker; one command starts one.

Note

A socket source block is planned for a future release. In the meantime, run doppler-specan --source socket --address <addr> directly to attach the spectrum analyzer to an existing publisher.


State files

Running chain state is persisted in ~/.doppler/chains/:

~/.doppler/chains/
  a3f7c2.yml    # compose file (copy written by init)
  a3f7c2.json   # live state: PIDs, ports, start time

doppler stop and doppler kill remove the .json file on completion. Orphaned .json files from crashed chains can be removed manually or with doppler stop <ID> (gracefully handles dead PIDs).


Creating a new block

No Python required

The fastest way to add a custom block is a dopplerfile — a small YAML file that registers any script as a pipeline block with zero Python. See the Dopplerfile guide.

The following shows how to add a built-in block in Python. Use this path when you need full control over config validation, complex arg building, or want to ship the block as part of the doppler CLI itself.

Here is a minimal example — a noise source that emits pure AWGN:

1. Config schema — declare fields with defaults using pydantic:

# my_blocks/noise.py  (your own package; the block API is doppler.cli.blocks)
from doppler.cli.blocks import Block, BlockConfig, register


class NoiseConfig(BlockConfig):
    sample_rate: float = 2.048e6
    noise_floor: float = -60.0


@register
class NoiseBlock(Block):
    name = "noise"
    Config = NoiseConfig
    role = "source"  # "source" | "chain" | "sink"

    def command(self, config, input_addr, output_addr):
        assert output_addr is not None
        return [
            "doppler-noise",
            "--bind", output_addr,
            "--fs", str(config.sample_rate),
            "--noise-floor", str(config.noise_floor),
        ]

2. Register it — import the module in __main__.py:

import my_blocks.noise  # noqa: F401

3. Entry point — add a doppler-noise script in pyproject.toml:

[project.scripts]
doppler-noise = "my_blocks.noise_source:main"

4. Startup log — every block entry point must print a health line on startup so doppler logs confirms what's running:

from argparse import Namespace
from datetime import datetime, timezone

def _log(msg):
    ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    print(f"[{ts}] {msg}", flush=True)

# In main(), before the processing loop (args is the parsed argparse
# Namespace — shown here with representative values):
args = Namespace(bind="nats://127.0.0.1:4222/dp-chain-5600", fs=2.048e6)
_log(f"doppler-noise started — bind={args.bind} fs={args.fs:.0f}")

5. Use it:

doppler compose init noise specan --name noise-test
doppler compose up noise-test
doppler logs noise-test
# [2026-04-01T10:00:00Z] doppler-noise started — bind=nats://127.0.0.1:4222/dp-chain-5600 fs=2048000
# [2026-04-01T10:00:00Z] doppler-specan started — mode=web source=pull address=nats://127.0.0.1:4222/dp-chain-5600

Port allocation

Ports are auto-assigned from the range 5600–5700 by scanning existing state files for in-use ports.

To pin ports explicitly, set port: on the source and each chain block in the compose file. Pinned ports are used as-is; no allocation is performed.