From 8a06c83b2de05dea4fc3602a7b75c9b772b18a01 Mon Sep 17 00:00:00 2001 From: hunterhubble Date: Fri, 5 Jun 2026 20:49:34 +0000 Subject: [PATCH 1/2] feat: expose RS corrections per packet - Includes pdu_n_corr and header_n_corr as items of the packet, so we can see the RS corrections per packet Signed-off-by: hunterhubble --- src/stream_web/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stream_web/app.py b/src/stream_web/app.py index 136bd99..76e014f 100644 --- a/src/stream_web/app.py +++ b/src/stream_web/app.py @@ -405,7 +405,7 @@ def api_packets(): """Poll-and-drain: return all decodes since last call as JSONL, then clear. Each line is a JSON object with: device_id, seq_num, device_type, - timestamp, rssi_dB, channel_num, freq_offset_hz. + timestamp, rssi_dB, channel_num, freq_offset_hz, pdu_n_corr, header_n_corr. """ with state.lock: entries = list(state.packet_feed) @@ -430,6 +430,8 @@ def api_packets(): "channel_num": e.get("channel_num"), "freq_offset_hz": e.get("freq_delta_hz"), "payload_b64": payload_b64, + "pdu_n_corr": e.get("pdu_n_corr"), + "header_n_corr": e.get("header_n_corr"), })) payload = "\n".join(lines) + ("\n" if lines else "") return Response(payload, mimetype="application/x-ndjson") From 0db32298cf67236bb0dd7795eac44e7a04592b5c Mon Sep 17 00:00:00 2001 From: hunterhubble Date: Fri, 5 Jun 2026 23:39:49 +0000 Subject: [PATCH 2/2] feat: Expose timing averages to td_info Signed-off-by: hunterhubble --- src/stream_web/processor.py | 3 ++- src/stream_web/spectrogram.py | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/stream_web/processor.py b/src/stream_web/processor.py index 5131abd..c257a97 100644 --- a/src/stream_web/processor.py +++ b/src/stream_web/processor.py @@ -236,7 +236,7 @@ def _extract_last(n): if not decode_info.get("energy_dB"): decode_info["energy_dB"] = td_hit.get("total_energy_dB") try: - td_img = render_td_plot(td_seg, decode_info=decode_info) + td_img, td_stats = render_td_plot(td_seg, decode_info=decode_info) status = f"t={td_hit['time_s']:.3f}s" if decode_info["decoded"]: seq = decode_info.get("seq_num") @@ -248,6 +248,7 @@ def _extract_last(n): k: v for k, v in decode_info.items() if isinstance(v, (str, int, float, bool, list, type(None))) } + td_decode_info_out.update(td_stats) td_iq_seg_out = td_seg.copy() except Exception as e: print(f"[TD] Plot error: {e}") diff --git a/src/stream_web/spectrogram.py b/src/stream_web/spectrogram.py index 31fa30c..49844ae 100644 --- a/src/stream_web/spectrogram.py +++ b/src/stream_web/spectrogram.py @@ -175,8 +175,16 @@ def _draw_decoder_overlay(ax, decode_info: dict): fontfamily="monospace", va="bottom", ha="left", alpha=1.0) -def render_td_plot(iq_segment: np.ndarray, decode_info: dict | None = None) -> bytes: - """Render a time-domain magnitude plot + spectrogram with annotations.""" +def render_td_plot( + iq_segment: np.ndarray, decode_info: dict | None = None +) -> tuple[bytes, dict]: + """Render a time-domain magnitude plot + spectrogram with annotations. + + Returns ``(image_bytes, stats)`` where *stats* contains symbol and gap + duration statistics derived from the envelope-based edge detection: + ``sym_count``, ``sym_mean_ms``, ``sym_std_ms``, + ``gap_count``, ``gap_mean_ms``, ``gap_std_ms``. + """ n = len(iq_segment) t_ms = np.arange(n) / config.SAMPLE_RATE * 1e3 mag = np.abs(iq_segment) @@ -397,4 +405,13 @@ def render_td_plot(iq_segment: np.ndarray, decode_info: dict | None = None) -> b buf = io.BytesIO() canvas.print_png(buf) buf.seek(0) - return buf.read() + + stats: dict = { + "sym_count": int(len(sym_dur_ms)), + "sym_mean_ms": float(round(np.mean(sym_dur_ms), 4)) if len(sym_dur_ms) else None, + "sym_std_ms": float(round(np.std(sym_dur_ms), 4)) if len(sym_dur_ms) else None, + "gap_count": int(len(gap_dur_ms)), + "gap_mean_ms": float(round(np.mean(gap_dur_ms), 4)) if len(gap_dur_ms) else None, + "gap_std_ms": float(round(np.std(gap_dur_ms), 4)) if len(gap_dur_ms) else None, + } + return buf.read(), stats