From 869d5baebce3698a9b4b7a6df272a799bb556a0f Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Mon, 9 Feb 2026 17:02:16 +0300 Subject: [PATCH] feat: new data format: 's0 0181 +000019' where s0 stands for channel 0. Chan number is shown near to sweep --- RFG_ADC_dataplotter.py | 92 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/RFG_ADC_dataplotter.py b/RFG_ADC_dataplotter.py index d10acc9..4efe717 100755 --- a/RFG_ADC_dataplotter.py +++ b/RFG_ADC_dataplotter.py @@ -4,7 +4,7 @@ Формат строк: - "Sweep_start" — начало нового свипа (предыдущий считается завершённым) - - "s X Y" — точка (индекс X, значение Y), все целые со знаком + - "s CH X Y" — точка (номер канала, индекс X, значение Y), все целые со знаком Отрисовываются два графика: - Левый: последний полученный свип (Y vs X) @@ -287,9 +287,11 @@ class SweepReader(threading.Thread): self._last_sweep_ts: Optional[float] = None self._n_valid_hist = deque() - def _finalize_current(self, xs, ys): + def _finalize_current(self, xs, ys, channels: Optional[set[int]]): if not xs: return + ch_list = sorted(channels) if channels else [0] + ch_primary = ch_list[0] if ch_list else 0 max_x = max(xs) width = max_x + 1 self._max_width = max(self._max_width, width) @@ -340,6 +342,10 @@ class SweepReader(threading.Thread): # Метрики для статусной строки (вид словаря: переменная -> значение) self._sweep_idx += 1 + if len(ch_list) > 1: + sys.stderr.write( + f"[warn] Sweep {self._sweep_idx}: изменялся номер канала: {ch_list}\n" + ) now = time.time() if self._last_sweep_ts is None: dt_ms = float("nan") @@ -363,6 +369,8 @@ class SweepReader(threading.Thread): vmin = vmax = mean = std = float("nan") info: SweepInfo = { "sweep": self._sweep_idx, + "ch": ch_primary, + "chs": ch_list, "n_valid": n_valid, "min": vmin, "max": vmax, @@ -388,6 +396,8 @@ class SweepReader(threading.Thread): # Состояние текущего свипа xs: list[int] = [] ys: list[int] = [] + cur_channel: Optional[int] = None + cur_channels: set[int] = set() try: self._src = SerialLineSource(self._port_path, self._baud, timeout=1.0) @@ -422,20 +432,37 @@ class SweepReader(threading.Thread): continue if line.startswith(b"Sweep_start"): - self._finalize_current(xs, ys) + self._finalize_current(xs, ys, cur_channels) xs.clear() ys.clear() + cur_channel = None + cur_channels.clear() continue - # s X Y (оба целые со знаком). Разделяем по любым пробелам/табам. + # sCH X Y или s CH X Y (все целые со знаком). Разделяем по любым пробелам/табам. if len(line) >= 3: parts = line.split() - if len(parts) >= 3 and parts[0].lower() == b"s": + if len(parts) >= 3 and (parts[0].lower() == b"s" or parts[0].lower().startswith(b"s")): try: - x = int(parts[1], 10) - y = int(parts[2], 10) # поддержка знака: "+…" и "-…" + if parts[0].lower() == b"s": + if len(parts) >= 4: + ch = int(parts[1], 10) + x = int(parts[2], 10) + y = int(parts[3], 10) # поддержка знака: "+…" и "-…" + else: + ch = 0 + x = int(parts[1], 10) + y = int(parts[2], 10) # поддержка знака: "+…" и "-…" + else: + # формат вида "s0" + ch = int(parts[0][1:], 10) + x = int(parts[1], 10) + y = int(parts[2], 10) # поддержка знака: "+…" и "-…" except Exception: continue + if cur_channel is None: + cur_channel = ch + cur_channels.add(ch) xs.append(x) ys.append(y) @@ -445,7 +472,7 @@ class SweepReader(threading.Thread): finally: try: # Завершаем оставшийся свип - self._finalize_current(xs, ys) + self._finalize_current(xs, ys, cur_channels) except Exception: pass try: @@ -579,6 +606,16 @@ def main(): ax_line.set_title("Сырые данные", pad=1) ax_line.set_xlabel("F") ax_line.set_ylabel("") + channel_text = ax_line.text( + 0.98, + 0.98, + "", + transform=ax_line.transAxes, + ha="right", + va="top", + fontsize=9, + family="monospace", + ) # Линейный график спектра текущего свипа fft_line_obj, = ax_fft.plot([], [], lw=1) @@ -910,9 +947,24 @@ def main(): if changed and current_info: status_text.set_text(_format_status_kv(current_info)) + chs = current_info.get("chs") if isinstance(current_info, dict) else None + if chs is None: + chs = current_info.get("ch") if isinstance(current_info, dict) else None + if chs is None: + channel_text.set_text("") + else: + try: + if isinstance(chs, (list, tuple, set)): + ch_list = sorted(int(v) for v in chs) + ch_text_val = ", ".join(str(v) for v in ch_list) + else: + ch_text_val = str(int(chs)) + channel_text.set_text(f"chs {ch_text_val}") + except Exception: + channel_text.set_text(f"chs {chs}") # Возвращаем обновлённые артисты - return (line_obj, img_obj, fft_line_obj, img_fft_obj, status_text) + return (line_obj, img_obj, fft_line_obj, img_fft_obj, status_text, channel_text) ani = FuncAnimation(fig, update, interval=interval_ms, blit=False) @@ -960,6 +1012,9 @@ def run_pyqtgraph(args): curve = p_line.plot(pen=pg.mkPen((80, 120, 255), width=1)) p_line.setLabel("bottom", "X") p_line.setLabel("left", "Y") + ch_text = pg.TextItem("", anchor=(1, 1)) + ch_text.setZValue(10) + p_line.addItem(ch_text) # Водопад (справа-сверху) p_img = win.addPlot(row=0, col=1, title="Сырые данные водопад") @@ -1182,6 +1237,25 @@ def run_pyqtgraph(args): status.setText(_format_status_kv(current_info)) except Exception: pass + try: + chs = current_info.get("chs") if isinstance(current_info, dict) else None + if chs is None: + chs = current_info.get("ch") if isinstance(current_info, dict) else None + if chs is None: + ch_text.setText("") + else: + if isinstance(chs, (list, tuple, set)): + ch_list = sorted(int(v) for v in chs) + ch_text_val = ", ".join(str(v) for v in ch_list) + else: + ch_text_val = str(int(chs)) + ch_text.setText(f"chs {ch_text_val}") + (x0, x1), (y0, y1) = p_line.viewRange() + dx = 0.01 * max(1.0, float(x1 - x0)) + dy = 0.01 * max(1.0, float(y1 - y0)) + ch_text.setPos(float(x1 - dx), float(y1 - dy)) + except Exception: + pass if changed and ring_fft is not None: disp_fft = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0)