Skip to content

IQ Capture and Record Analyze#28

Open
hunterhubble wants to merge 2 commits into
HubbleNetwork:mainfrom
hunterhubble:iq-capture
Open

IQ Capture and Record Analyze#28
hunterhubble wants to merge 2 commits into
HubbleNetwork:mainfrom
hunterhubble:iq-capture

Conversation

@hunterhubble

@hunterhubble hunterhubble commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Added GET /api/iq_capture to return a /npy file of IQ samples for a given duration (default 10s, max 30s)

Added GET /api/record_analyze to record IQ samples and print statistics to logs, including the frequency, timing, amplitude, and snr of each symbol. To note, this does not use the existing GET /api/td_info, because we will probably want to analyze packets that don't decode in a future iteration. The logs are printed in plaintext. (default 10s, max 30s)

I also refactored some code that I repeat in analyze.py into their own functions

=== Hubble sat-record diagnostic ===
capture: 10 s @ 781250 Hz, center 2482.4404 MHz, 7812500 samples
decoded packets: 3

=== representative packet ===
  t=5.8349s  channel 14  chipset nordic

PER-SYMBOL TIMING (duration, gap; drift measured vs an ideal 8000 us symbol / 800 us gap grid)
  Symbol  0: duration=8026.88 us, gap=771.84 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  1: duration=8028.16 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  2: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  3: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  4: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  5: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  6: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  7: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  8: duration=8026.88 us, gap=774.40 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  9: duration=8025.60 us, gap=773.12 us, drift=+1.28 us, rate=+1.28 us/sym
  Symbol 10: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=-1.28 us/sym
  Symbol 11: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 12: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 13: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 14: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 15: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 16: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 17: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 18: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 19: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 20: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 21: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 22: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 23: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 24: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 25: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 26: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 27: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 28: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 29: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 30: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 31: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 32: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 33: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 34: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 35: duration=8026.88 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol 36: duration=8026.88 us, drift=+0.00 us, rate=+0.00 us/sym
  Overall drift: +0.00 us (+0.00 us/symbol over 37 symbols)

PER-SYMBOL FREQUENCY / CHANNEL HOPPING
  hop sequence idx 0, start channel 14, expected channels [14, 5, 6]
  calibration: preamble sym63=138625.0 Hz sym0=107875.0 Hz, FSK step=488.1 Hz, channel width=31238.1 Hz
  channel spacing: 25847.2 Hz (calibrated from PDU hops)
  Symbol  0: ch 14, freq=  138625.0 Hz  [ok]
  Symbol  1: ch 14, freq=  107875.0 Hz  [ok]
  Symbol  2: ch 14, freq=  138625.0 Hz  [ok]
  Symbol  3: ch 14, freq=  107875.0 Hz  [ok]
  Symbol  4: ch 14, freq=  138625.0 Hz  [ok]
  Symbol  5: ch 14, freq=  107875.0 Hz  [ok]
  Symbol  6: ch 14, freq=  138625.0 Hz  [ok]
  Symbol  7: ch 14, freq=  138625.0 Hz  [ok]
  Symbol  8: ch 14, freq=  109875.0 Hz  [ok]
  Symbol  9: ch 14, freq=  114750.0 Hz  [ok]
  Symbol 10: ch 14, freq=  128375.0 Hz  [ok]
  Symbol 11: ch 14, freq=  110875.0 Hz  [ok]
  Symbol 12: ch 14, freq=  134750.0 Hz  [ok]
  Symbol 13: ch 14, freq=  112250.0 Hz  [ok]
  Symbol 14: ch 14, freq=  134750.0 Hz  [ok]
  Symbol 15: ch 14, freq=  133250.0 Hz  [ok]
  Symbol 16: ch  5, freq= -116750.0 Hz  [ok]
  Symbol 17: ch  5, freq=  -97625.0 Hz  [ok]
  Symbol 18: ch  5, freq= -121625.0 Hz  [ok]
  Symbol 19: ch  5, freq=  -99625.0 Hz  [ok]
  Symbol 20: ch  5, freq=  -99125.0 Hz  [ok]
  Symbol 21: ch  5, freq= -123500.0 Hz  [ok]
  Symbol 22: ch  5, freq= -106000.0 Hz  [ok]
  Symbol 23: ch  5, freq=  -95250.0 Hz  [ok]
  Symbol 24: ch  5, freq= -123500.0 Hz  [ok]
  Symbol 25: ch  5, freq=  -98125.0 Hz  [ok]
  Symbol 26: ch  5, freq= -107875.0 Hz  [ok]
  Symbol 27: ch  5, freq=  -96750.0 Hz  [ok]
  Symbol 28: ch  5, freq= -123500.0 Hz  [ok]
  Symbol 29: ch  5, freq= -112375.0 Hz  [ok]
  Symbol 30: ch  5, freq= -108375.0 Hz  [ok]
  Symbol 31: ch  5, freq= -116250.0 Hz  [ok]
  Symbol 32: ch  6, freq=  -71750.0 Hz  [ok]
  Symbol 33: ch  6, freq=  -96750.0 Hz  [ok]
  Symbol 34: ch  6, freq=  -91750.0 Hz  [ok]
  Symbol 35: ch  6, freq=  -94750.0 Hz  [ok]
  Symbol 36: ch  6, freq=  -95750.0 Hz  [ok]
  channel validation: PASS (37/37 symbols in window)

PER-SYMBOL AMPLITUDE (RMS dBFS; SNR above the inter-symbol noise floor)
  Symbol  0: amp=-28.83 dBFS, snr=30.17 dB
  Symbol  1: amp=-28.84 dBFS, snr=30.16 dB
  Symbol  2: amp=-28.87 dBFS, snr=30.13 dB
  Symbol  3: amp=-28.87 dBFS, snr=30.12 dB
  Symbol  4: amp=-28.90 dBFS, snr=30.10 dB
  Symbol  5: amp=-28.89 dBFS, snr=30.10 dB
  Symbol  6: amp=-28.92 dBFS, snr=30.08 dB
  Symbol  7: amp=-28.92 dBFS, snr=30.07 dB
  Symbol  8: amp=-28.92 dBFS, snr=30.07 dB
  Symbol  9: amp=-28.93 dBFS, snr=30.06 dB
  Symbol 10: amp=-28.94 dBFS, snr=30.05 dB
  Symbol 11: amp=-28.95 dBFS, snr=30.05 dB
  Symbol 12: amp=-28.97 dBFS, snr=30.03 dB
  Symbol 13: amp=-28.97 dBFS, snr=30.03 dB
  Symbol 14: amp=-28.98 dBFS, snr=30.01 dB
  Symbol 15: amp=-28.99 dBFS, snr=30.01 dB
  Symbol 16: amp=-28.98 dBFS, snr=30.01 dB
  Symbol 17: amp=-28.98 dBFS, snr=30.01 dB
  Symbol 18: amp=-28.99 dBFS, snr=30.00 dB
  Symbol 19: amp=-29.00 dBFS, snr=30.00 dB
  Symbol 20: amp=-29.00 dBFS, snr=29.99 dB
  Symbol 21: amp=-29.02 dBFS, snr=29.98 dB
  Symbol 22: amp=-29.01 dBFS, snr=29.98 dB
  Symbol 23: amp=-29.02 dBFS, snr=29.98 dB
  Symbol 24: amp=-29.03 dBFS, snr=29.97 dB
  Symbol 25: amp=-29.02 dBFS, snr=29.97 dB
  Symbol 26: amp=-29.03 dBFS, snr=29.97 dB
  Symbol 27: amp=-29.03 dBFS, snr=29.96 dB
  Symbol 28: amp=-29.04 dBFS, snr=29.95 dB
  Symbol 29: amp=-29.04 dBFS, snr=29.96 dB
  Symbol 30: amp=-29.04 dBFS, snr=29.95 dB
  Symbol 31: amp=-29.05 dBFS, snr=29.95 dB
  Symbol 32: amp=-29.04 dBFS, snr=29.95 dB
  Symbol 33: amp=-29.05 dBFS, snr=29.95 dB
  Symbol 34: amp=-29.05 dBFS, snr=29.95 dB
  Symbol 35: amp=-29.05 dBFS, snr=29.94 dB
  Symbol 36: amp=-29.05 dBFS, snr=29.94 dB
  summary: mean -28.98 dBFS, dropoff (p-p) 0.22 dB, noise floor -59.0 dBFS, SNR 30.02 dB

FREQUENCY / CHIPSET
  offset from center: -5493.4 Hz
  chipset (decoder): nordic
  synth-res: measured 488.13 Hz (chipset nominal 488.28125 Hz)

RS corrections: header_mean=0.0 pdu_mean=0.0 pdu_max=0

PER-PACKET (all packets)
  t=5.8349s ch14 nordic: sym=8.0269ms gap=0.7731ms drift=0.0us amp=-28.98dBFS snr=30.02dB
  t=7.1651s ch9 nordic: sym=8.0269ms gap=0.7731ms drift=1.28us amp=-29.1dBFS snr=28.88dB
  t=8.4958s ch8 nordic: sym=8.0268ms gap=0.7733ms drift=1.28us amp=-29.12dBFS snr=28.56dB```

Helpful for:
- Extra signal processing through numpy or matplotlib
- Recording samples with pyhubblenetwork
- Testing Satellite hardware with IQ samples from real devices
- Determining the frequency of a continuous tone

Signed-off-by: hunterhubble <hunter@hubble.com>
- Created /record_analyze API endpoint to record IQ samples,
attempt to decode from them, then provides a log of statistics
to match our satellite protocol in a plain text format
- Statistics include frequency, amplitude, and timing per symbol.
From this, we can tell if it matches frequency hopping, see any
amplitude drop-offs, and observe timing violations

- Also refactored some code to turn into functions I can call in
analyze.py

Signed-off-by: hunterhubble <hunter@hubble.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds capture-and-diagnostics capabilities to the SDR web service by introducing endpoints to export raw IQ and generate a plain-text analysis report, plus refactors shared symbol/timing utilities to support the new analysis flow.

Changes:

  • Added GET /api/iq_capture to download a .npy recording of recent IQ samples.
  • Added GET /api/record_analyze and new stream_web/analysis.py to decode and produce per-symbol timing/frequency/amplitude diagnostics.
  • Refactored/shared symbol-grid and per-symbol FFT/amplitude helpers across processor/spectrogram/analysis.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/stream_web/timing.py Adds reusable helpers for dominant per-symbol frequency and RMS dBFS amplitude/noise floor.
src/stream_web/spectrogram.py Uses shared symbol-grid helper and refactors per-symbol frequency computation to the new helper.
src/stream_web/processor.py Uses shared packet symbol-grid helper when computing corrected symbol edges.
src/stream_web/config.py Adds packet_symbol_grid() and rotated_hop_sequence() utilities shared across modules.
src/stream_web/app.py Adds /api/iq_capture and /api/record_analyze, including ring-buffer capture logic and concurrency lock.
src/stream_web/analysis.py New offline diagnostic pipeline: decode, per-symbol analyses, and report formatting.
CLAUDE.md Documents the new RX endpoints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +186 to +189
sr, sps, hop = config.SAMPLE_RATE, config.samples_per_symbol, config.NUM_SYM_PER_HOP
ch = rotated[0]
freqs = [dominant_symbol_freq(iq[s:s + sps], sr) for s, _ in edges]

Comment thread src/stream_web/app.py
Comment on lines +574 to +579
buf = io.BytesIO()
np.save(buf, segment)
buf.seek(0)
fname = f"iq_{int(time.time())}_{seconds}s.npy"
resp = send_file(buf, mimetype="application/octet-stream",
as_attachment=True, download_name=fname)
Comment thread src/stream_web/app.py
Comment on lines +559 to +563
@app.route("/api/iq_capture", methods=["GET"])
def api_iq_capture():
seconds, err = _parse_seconds()
if err:
return err
Comment thread CLAUDE.md
Comment on lines +46 to +47
- `GET /api/iq_capture?seconds=N` — record N s (1–60) of raw IQ forward from now; returns a `.npy` (complex64) download with `X-Sample-Rate-Hz` / `X-Center-Freq-Hz` / `X-N-Samples` headers
- `GET /api/record_analyze?seconds=N` — record N s (1–30) and return a plain-text diagnostic report file: per-symbol timing (drift/rate/duration/gap) and per-symbol frequency vs expected channel window for the representative packet, plus synth-res/chipset, amplitude/SNR, RS corrections, regime + diagnosis; noise/mis-hits filtered by preamble strength; backs `hubblenetwork sat record`

@HongNguyen635 HongNguyen635 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please address copilot's comment above?
Also, in the pr summary:

PER-SYMBOL TIMING (duration, gap; drift measured vs an ideal 8000 us symbol / 800 us gap grid)
  Symbol  0: duration=8026.88 us, gap=771.84 us, drift=+0.00 us, rate=+0.00 us/sym
  Symbol  1: duration=8028.16 us, gap=773.12 us, drift=+0.00 us, rate=+0.00 us/sym

this feels a bit off, 8026.88 + 771.84 = 8798.72. So why is the drift = 0.00 us?
is this gap supposed to be for symbol 1 then?

Comment on lines +29 to +30
_WINDOW_OVERLAP_S = 0.6 # decoder-window overlap; > the longest packet (~0.53 s)
_DEDUP_WINDOW_S = 0.5 # duplicates (same seq/auth/payload) collapse within this span

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these constants the same as defined in config.py?

Comment thread src/stream_web/config.py
Comment on lines +127 to +136
def packet_symbol_grid(decode_info: dict) -> tuple[int, int, int]:
"""Return ``(n_sym, slot, sym_len)`` for a decoded-packet / decode_info dict:
total symbols (preamble + header + PDU), samples per symbol slot, and symbol length."""
ver = decode_info.get("phy_ver", 1)
slot = slot_samples.get(ver, slot_samples[1])["slot"]
n_sym = PREAMBLE_LEN + NUM_HEADER_SYMS + (decode_info.get("num_pdu_symbols") or 0)
return n_sym, slot, samples_per_symbol


def rotated_hop_sequence(channel_num, hop_seq_idx) -> list[int] | None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think adding these 2 functions here makes sense?

I assumed config.py is supposed to be for constants.

@HongNguyen635 HongNguyen635 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another comment: maybe it's worth it to think back about why we have the sepration between hubble_satnet_decoder and sdr-docker. I forgot to pay attention to it until now, but isn't these timing analysis that is protocol specific supposed to be in the satnet decoder? and the docker is just there to capture RF -> call satnet decoder -> serves the data?

from .timing import correct_symbol_edges, dominant_symbol_freq, symbol_amplitudes_dbfs

log = logging.getLogger(__name__)
_SYM_GRID_MS, _GAP_GRID_MS = 8.0, 0.8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to pull this put into config.py as well? or is it something you can import from hubble_satnet_decoder constants.py?

from my understanding, hubble_satnet_decoder defined the protocol, so these things are not supposed to be re-defined here. When protocol changes, you're going to have to change these.

Since we're talking about protocol change, maybe it's relevant to think about (not now, something to think about as you go on). When we're adding more and more things that are specific to a certain protocol instead of protocol agnostic, can we reuse this file / debug when there's a new protocol?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point, I'll spend some more time thinking about how to organize all this, I think the protocol specific things can/should be in hubble-satnet-decoder.

@hunterhubble

Copy link
Copy Markdown
Contributor Author

@HongNguyen635 That's a good point, the current design has the drift based off of the timing of the first symbol, then the rest of the symbols are measured off of that. It's also taken from the midpoint of the symbol relative to the midpoint of the symbols of a 'perfect' template, so while it does look strange that a symbol is too long

With that being said, I am re-thinking this design, because the first symbol could be too long, like with this instance, and that should be reported. Good catch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants