Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/stream_web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion src/stream_web/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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}")
Expand Down
23 changes: 20 additions & 3 deletions src/stream_web/spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Loading