|
- #!/usr/bin/env python3
- """正点原子逻辑分析仪脉冲频率轨迹查看器。"""
-
- from __future__ import annotations
-
- import bisect
- import math
- import queue
- import threading
- import tkinter as tk
- from pathlib import Path
- from tkinter import filedialog, messagebox, ttk
-
- from logic_waveform import (
- RATE_UNITS,
- FrequencyCurve,
- WaveformChannel,
- WaveformData,
- build_frequency_curves,
- load_waveform,
- parse_marker_times_ms,
- )
- from parse_logic_csv import CsvParseError
-
-
- CHANNEL_COLORS = (
- "#2563eb",
- "#dc2626",
- "#16a34a",
- "#ea580c",
- "#7c3aed",
- "#0891b2",
- "#c026d3",
- "#4d7c0f",
- )
- EDGE_MODES = {
- "自动": "auto",
- "上升沿": "rising",
- "下降沿": "falling",
- }
-
-
- class LogicWaveformApp(tk.Tk):
- """读取逻辑分析仪文件并显示频率随时间的变化。"""
-
- def __init__(self) -> None:
- super().__init__()
- self.title("正点原子逻辑分析仪脉冲频率轨迹查看器")
- self.geometry("1180x760")
- self.minsize(880, 580)
-
- self.input_var = tk.StringVar()
- self.sample_rate_var = tk.StringVar(value="20")
- self.sample_rate_unit_var = tk.StringVar(value="MHz")
- self.bin_channel_count_var = tk.StringVar(value="16")
- self.channel_var = tk.StringVar(value="自动")
- self.edge_var = tk.StringVar(value="自动")
- self.marker_var = tk.StringVar(value="100")
- self.status_var = tk.StringVar(value="请选择正点原子导出的 CSV 或 BIN 文件")
- self.file_info_var = tk.StringVar(value="尚未读取频率轨迹")
- self.cursor_var = tk.StringVar(value="光标:-")
- self.marker_info_var = tk.StringVar(value="关键节点:-")
-
- self.result_queue: queue.Queue[tuple[str, object]] = queue.Queue()
- self.waveform_data: WaveformData | None = None
- self.visible_channels: list[WaveformChannel] = []
- self.frequency_curves: list[FrequencyCurve] = []
- self.marker_times: list[float] = [0.1]
- self.frequency_origin = 0.0
- self.view_start = 0.0
- self.view_end = 1.0
- self.drag_state: tuple[int, float, float] | None = None
- self.redraw_job: str | None = None
-
- self.load_button: ttk.Button
- self.canvas: tk.Canvas
- self._build_widgets()
-
- def _build_widgets(self) -> None:
- """创建文件参数区和频率曲线显示区。"""
- self.columnconfigure(0, weight=1)
- self.rowconfigure(3, weight=1)
-
- header = ttk.Frame(self, padding=(16, 14, 16, 8))
- header.grid(row=0, column=0, sticky="ew")
- header.columnconfigure(0, weight=1)
- ttk.Label(
- header,
- text="脉冲频率轨迹查看器",
- font=("Microsoft YaHei UI", 15, "bold"),
- ).grid(row=0, column=0, sticky="w")
- ttk.Label(header, textvariable=self.status_var).grid(
- row=0, column=1, sticky="e"
- )
-
- controls = ttk.Frame(self, padding=(16, 0, 16, 8))
- controls.grid(row=1, column=0, sticky="ew")
- controls.columnconfigure(1, weight=1)
- ttk.Label(controls, text="波形文件").grid(row=0, column=0, sticky="w")
- ttk.Entry(controls, textvariable=self.input_var).grid(
- row=0, column=1, sticky="ew", padx=(8, 8)
- )
- ttk.Button(controls, text="选择文件", command=self._choose_input).grid(
- row=0, column=2, padx=(0, 8)
- )
- self.load_button = ttk.Button(
- controls, text="读取并显示", command=self._start_load
- )
- self.load_button.grid(row=0, column=3)
-
- options = ttk.Frame(self, padding=(16, 0, 16, 10))
- options.grid(row=2, column=0, sticky="ew")
- ttk.Label(options, text="BIN采样率").grid(row=0, column=0, sticky="w")
- ttk.Entry(options, textvariable=self.sample_rate_var, width=9).grid(
- row=0, column=1, padx=(8, 4)
- )
- ttk.Combobox(
- options,
- textvariable=self.sample_rate_unit_var,
- values=("Hz", "kHz", "MHz", "GHz"),
- state="readonly",
- width=6,
- ).grid(row=0, column=2, padx=(0, 18))
-
- ttk.Label(options, text="BIN通道数").grid(row=0, column=3, sticky="w")
- ttk.Combobox(
- options,
- textvariable=self.bin_channel_count_var,
- values=("8", "16"),
- state="readonly",
- width=5,
- ).grid(row=0, column=4, padx=(8, 18))
-
- ttk.Label(options, text="显示通道").grid(row=0, column=5, sticky="w")
- ttk.Entry(options, textvariable=self.channel_var, width=14).grid(
- row=0, column=6, padx=(8, 8)
- )
-
- ttk.Label(options, text="测量边沿").grid(row=1, column=0, sticky="w", pady=(8, 0))
- edge_box = ttk.Combobox(
- options,
- textvariable=self.edge_var,
- values=tuple(EDGE_MODES),
- state="readonly",
- width=8,
- )
- edge_box.grid(row=1, column=1, sticky="w", padx=(8, 18), pady=(8, 0))
- edge_box.bind("<<ComboboxSelected>>", lambda _event: self._apply_plot_settings())
-
- ttk.Label(options, text="关键时间(ms,逗号分隔,最多10个)").grid(
- row=1, column=3, sticky="w", pady=(8, 0)
- )
- marker_entry = ttk.Entry(options, textvariable=self.marker_var, width=28)
- marker_entry.grid(row=1, column=4, columnspan=2, sticky="w", padx=(8, 8), pady=(8, 0))
- marker_entry.bind("<Return>", lambda _event: self._apply_plot_settings())
- ttk.Button(options, text="更新曲线", command=self._apply_plot_settings).grid(
- row=1, column=6, sticky="w", pady=(8, 0)
- )
-
- viewer = ttk.Frame(self, padding=(16, 0, 16, 10))
- viewer.grid(row=3, column=0, sticky="nsew")
- viewer.columnconfigure(0, weight=1)
- viewer.rowconfigure(2, weight=1)
-
- info_bar = ttk.Frame(viewer)
- info_bar.grid(row=0, column=0, sticky="ew", pady=(0, 6))
- info_bar.columnconfigure(0, weight=1)
- ttk.Label(info_bar, textvariable=self.file_info_var).grid(
- row=0, column=0, sticky="w"
- )
- ttk.Label(info_bar, textvariable=self.cursor_var).grid(
- row=0, column=1, sticky="e"
- )
-
- toolbar = ttk.Frame(viewer)
- toolbar.grid(row=1, column=0, sticky="ew", pady=(0, 6))
- toolbar.columnconfigure(3, weight=1)
- ttk.Button(toolbar, text="放大", command=lambda: self._zoom_view(0.6)).grid(
- row=0, column=0
- )
- ttk.Button(toolbar, text="缩小", command=lambda: self._zoom_view(1.6)).grid(
- row=0, column=1, padx=(6, 0)
- )
- ttk.Button(toolbar, text="适应窗口", command=self._fit_view).grid(
- row=0, column=2, padx=(6, 0)
- )
- ttk.Label(
- toolbar,
- textvariable=self.marker_info_var,
- wraplength=800,
- ).grid(
- row=1, column=0, columnspan=4, sticky="w", pady=(6, 0)
- )
-
- self.canvas = tk.Canvas(
- viewer,
- background="#ffffff",
- highlightthickness=1,
- highlightbackground="#aeb4bc",
- cursor="crosshair",
- )
- self.canvas.grid(row=2, column=0, sticky="nsew")
- self.canvas.bind("<Configure>", lambda _event: self._schedule_redraw())
- self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
- self.canvas.bind("<ButtonPress-1>", self._start_drag)
- self.canvas.bind("<B1-Motion>", self._drag_view)
- self.canvas.bind("<ButtonRelease-1>", self._end_drag)
- self.canvas.bind("<Motion>", self._show_cursor)
- self.canvas.bind("<Leave>", self._hide_cursor)
-
- def _choose_input(self) -> None:
- """选择 CSV 或 BIN 文件并立即读取。"""
- path = filedialog.askopenfilename(
- title="选择正点原子逻辑分析仪波形文件",
- filetypes=(
- ("逻辑分析仪文件", "*.csv *.bin"),
- ("CSV 文件", "*.csv"),
- ("BIN 文件", "*.bin"),
- ("所有文件", "*.*"),
- ),
- )
- if not path:
- return
- self.input_var.set(path)
- self._start_load()
-
- def _get_bin_sample_rate(self) -> float:
- """读取界面中的 BIN 采样率。"""
- try:
- value = float(self.sample_rate_var.get().strip())
- except ValueError as exc:
- raise CsvParseError("BIN 采样率必须是数字") from exc
- if value <= 0:
- raise CsvParseError("BIN 采样率必须大于 0")
- return value * RATE_UNITS[self.sample_rate_unit_var.get().lower()]
-
- def _start_load(self) -> None:
- """校验参数并在后台读取波形文件。"""
- input_name = self.input_var.get().strip()
- if not input_name:
- messagebox.showwarning("未选择文件", "请先选择 CSV 或 BIN 文件。")
- return
- path = Path(input_name)
- if not path.is_file():
- messagebox.showerror("文件不存在", f"找不到文件:\n{path}")
- return
- try:
- sample_rate = self._get_bin_sample_rate()
- channel_count = int(self.bin_channel_count_var.get())
- parse_marker_times_ms(self.marker_var.get())
- except (ValueError, CsvParseError) as exc:
- messagebox.showerror("参数错误", str(exc))
- return
-
- self.load_button.configure(state="disabled")
- self.status_var.set("正在读取并计算频率……")
- self.file_info_var.set(f"正在读取:{path.name}")
- worker = threading.Thread(
- target=self._load_worker,
- args=(path, sample_rate, channel_count, self.channel_var.get()),
- daemon=True,
- )
- worker.start()
- self.after(100, self._check_load_result)
-
- def _load_worker(
- self,
- path: Path,
- sample_rate: float,
- channel_count: int,
- channel_text: str,
- ) -> None:
- """在线程中解析较大的 BIN 文件。"""
- try:
- result = load_waveform(path, sample_rate, channel_count, channel_text)
- self.result_queue.put(("ok", result))
- except (OSError, CsvParseError) as exc:
- self.result_queue.put(("error", str(exc)))
-
- def _check_load_result(self) -> None:
- """接收后台解析结果并生成频率曲线。"""
- try:
- result_type, result = self.result_queue.get_nowait()
- except queue.Empty:
- self.after(100, self._check_load_result)
- return
-
- self.load_button.configure(state="normal")
- if result_type == "error":
- self.status_var.set("读取失败")
- self.file_info_var.set("波形文件读取失败")
- messagebox.showerror("读取失败", str(result))
- return
-
- data, channels = result # type: ignore[misc]
- self.waveform_data = data
- self.visible_channels = channels
- try:
- self._rebuild_frequency_curves()
- except CsvParseError as exc:
- self.status_var.set("频率计算失败")
- messagebox.showerror("频率计算失败", str(exc))
- return
- self.file_info_var.set(self._build_file_info(data))
- self.status_var.set(f"已生成 {len(self.frequency_curves)} 条频率曲线")
-
- def _apply_plot_settings(self) -> None:
- """应用测量边沿和关键时间设置。"""
- if self.waveform_data is None:
- return
- try:
- self._rebuild_frequency_curves()
- except CsvParseError as exc:
- messagebox.showerror("曲线设置错误", str(exc))
-
- def _rebuild_frequency_curves(self) -> None:
- """按当前边沿设置重新计算频率轨迹。"""
- edge_mode = EDGE_MODES[self.edge_var.get()]
- self.marker_times = parse_marker_times_ms(self.marker_var.get())
- self.frequency_curves = build_frequency_curves(
- self.visible_channels, edge_mode
- )
- self.frequency_origin = min(curve.start_time for curve in self.frequency_curves)
- self.view_start = self.frequency_origin
- self.view_end = max(curve.end_time for curve in self.frequency_curves)
- if self.view_end <= self.view_start:
- self.view_end = self.view_start + 1e-6
- self._update_marker_summary()
- self.cursor_var.set("光标:-")
- self._schedule_redraw()
-
- @staticmethod
- def _build_file_info(data: WaveformData) -> str:
- """生成文件采样信息。"""
- parts = [data.source_type]
- if data.sample_rate:
- parts.append(f"采样率 {format_frequency(data.sample_rate)}")
- if data.sample_count is not None:
- parts.append(f"采样点 {data.sample_count:,}")
- if data.source_type == "CSV":
- parts.append(f"记录行 {data.record_count:,}")
- return " | ".join(parts)
-
- def _update_marker_summary(self) -> None:
- """在工具栏显示所有关键节点的测量结果。"""
- if not self.marker_times or not self.frequency_curves:
- self.marker_info_var.set("关键节点:-")
- return
- summaries = []
- for marker in self.marker_times:
- target_time = self.frequency_origin + marker
- values = []
- for curve in self.frequency_curves:
- if target_time > curve.end_time:
- values.append(f"{curve.channel_name} 超出范围")
- continue
- measured_time, frequency = curve.frequency_at(target_time)
- measured_ms = (measured_time - self.frequency_origin) * 1_000.0
- values.append(
- f"{curve.channel_name} {format_frequency(frequency)}"
- f"@{measured_ms:.3f}ms"
- )
- summaries.append(f"{marker * 1000:g}ms:" + " | ".join(values))
- self.marker_info_var.set(" ".join(summaries))
-
- def _plot_bounds(self) -> tuple[float, float, float, float]:
- """返回频率曲线绘图区边界。"""
- width = max(1, self.canvas.winfo_width())
- height = max(1, self.canvas.winfo_height())
- return 92.0, 35.0, max(93.0, width - 24.0), max(36.0, height - 58.0)
-
- def _schedule_redraw(self) -> None:
- """合并连续的重绘请求。"""
- if self.redraw_job is None:
- self.redraw_job = self.after_idle(self._draw_frequency_chart)
-
- def _draw_frequency_chart(self) -> None:
- """绘制频率随时间变化的曲线。"""
- self.redraw_job = None
- self.canvas.delete("all")
- if not self.frequency_curves:
- self.canvas.create_text(
- self.canvas.winfo_width() / 2,
- self.canvas.winfo_height() / 2,
- text="请选择包含脉冲的 CSV 或 BIN 文件",
- fill="#6b7280",
- font=("Microsoft YaHei UI", 12),
- )
- return
-
- left, top, right, bottom = self._plot_bounds()
- plot_width = right - left
- plot_height = bottom - top
- time_span = max(self.view_end - self.view_start, 1e-15)
- maximum_frequency = nice_frequency_max(self._visible_max_frequency())
-
- # 绘制横向频率网格和纵轴刻度。
- for tick in range(9):
- ratio = tick / 8.0
- y = bottom - ratio * plot_height
- frequency = ratio * maximum_frequency
- self.canvas.create_line(left, y, right, y, fill="#e2e5e9", width=1)
- self.canvas.create_text(
- left - 9,
- y,
- anchor="e",
- text=format_hz_tick(frequency),
- fill="#4b5563",
- font=("Consolas", 9),
- )
-
- # 绘制时间网格,横轴从第一个有效脉冲边沿开始计时。
- time_scale, time_unit = choose_time_unit(time_span)
- for tick in range(11):
- ratio = tick / 10.0
- x = left + ratio * plot_width
- time_value = self.view_start + ratio * time_span
- relative_time = (time_value - self.frequency_origin) * time_scale
- self.canvas.create_line(x, top, x, bottom, fill="#edf0f3", width=1)
- self.canvas.create_text(
- x,
- bottom + 18,
- text=format_tick(relative_time),
- fill="#4b5563",
- font=("Consolas", 9),
- )
-
- self.canvas.create_text(
- 20,
- (top + bottom) / 2,
- text="脉冲频率 (Hz)",
- angle=90,
- fill="#111827",
- font=("Microsoft YaHei UI", 10),
- )
- self.canvas.create_text(
- (left + right) / 2,
- bottom + 42,
- text=f"脉冲开始后的时间 ({time_unit})",
- fill="#111827",
- font=("Microsoft YaHei UI", 10),
- )
-
- for curve_number, curve in enumerate(self.frequency_curves):
- color = CHANNEL_COLORS[curve.channel_index % len(CHANNEL_COLORS)]
- self._draw_curve(
- curve, left, right, top, bottom, maximum_frequency, color
- )
- self.canvas.create_line(
- left + curve_number * 190,
- 16,
- left + 20 + curve_number * 190,
- 16,
- fill=color,
- width=3,
- )
- self.canvas.create_text(
- left + 26 + curve_number * 190,
- 16,
- anchor="w",
- text=f"{curve.channel_name}({curve.edge_name})",
- fill="#1f2937",
- font=("Microsoft YaHei UI", 9, "bold"),
- )
-
- self._draw_markers(left, right, top, bottom, maximum_frequency)
- self.canvas.create_line(left, top, left, bottom, fill="#4b5563", width=1)
- self.canvas.create_line(left, bottom, right, bottom, fill="#4b5563", width=1)
-
- def _visible_max_frequency(self) -> float:
- """读取当前时间窗口中的最大频率。"""
- maximum = 0.0
- for curve in self.frequency_curves:
- start = max(0, bisect.bisect_left(curve.times, self.view_start) - 1)
- end = min(len(curve.times), bisect.bisect_right(curve.times, self.view_end) + 1)
- if start < end:
- maximum = max(maximum, max(curve.frequencies[start:end]))
- return max(maximum, 1.0)
-
- def _draw_curve(
- self,
- curve: FrequencyCurve,
- left: float,
- right: float,
- top: float,
- bottom: float,
- maximum_frequency: float,
- color: str,
- ) -> None:
- """绘制一条频率轨迹。"""
- start = max(0, bisect.bisect_left(curve.times, self.view_start) - 1)
- end = min(len(curve.times), bisect.bisect_right(curve.times, self.view_end) + 1)
- if end - start < 2:
- return
- time_span = self.view_end - self.view_start
- plot_width = right - left
- plot_height = bottom - top
- indexes = list(range(start, end))
- maximum_points = max(100, int(plot_width * 3))
- if len(indexes) > maximum_points:
- indexes = decimate_curve_indexes(
- curve.frequencies, start, end, maximum_points
- )
-
- coordinates: list[float] = []
- for index in indexes:
- x = left + (curve.times[index] - self.view_start) / time_span * plot_width
- y = bottom - curve.frequencies[index] / maximum_frequency * plot_height
- coordinates.extend((x, y))
- self.canvas.create_line(*coordinates, fill=color, width=2, smooth=False)
-
- def _draw_markers(
- self,
- left: float,
- right: float,
- top: float,
- bottom: float,
- maximum_frequency: float,
- ) -> None:
- """标出用户设置的关键时间和对应频率。"""
- time_span = self.view_end - self.view_start
- plot_height = bottom - top
- for marker in self.marker_times:
- target_time = self.frequency_origin + marker
- if not self.view_start <= target_time <= self.view_end:
- continue
- x = left + (target_time - self.view_start) / time_span * (right - left)
- self.canvas.create_line(
- x, top, x, bottom, fill="#d97706", width=1, dash=(5, 4)
- )
- self.canvas.create_text(
- x + 5,
- top + 4,
- anchor="nw",
- text=f"{marker * 1000:g} ms",
- fill="#92400e",
- font=("Microsoft YaHei UI", 9, "bold"),
- )
- for curve_number, curve in enumerate(self.frequency_curves):
- if target_time > curve.end_time:
- continue
- measured_time, frequency = curve.frequency_at(target_time)
- point_x = left + (measured_time - self.view_start) / time_span * (right - left)
- point_y = bottom - frequency / maximum_frequency * plot_height
- color = CHANNEL_COLORS[curve.channel_index % len(CHANNEL_COLORS)]
- self.canvas.create_oval(
- point_x - 4,
- point_y - 4,
- point_x + 4,
- point_y + 4,
- fill="#ffffff",
- outline=color,
- width=2,
- )
- self.canvas.create_text(
- point_x + 7,
- point_y - 7 - curve_number * 17,
- anchor="sw",
- text=format_frequency(frequency),
- fill=color,
- font=("Microsoft YaHei UI", 9, "bold"),
- )
-
- def _full_time_range(self) -> tuple[float, float]:
- """返回所有频率曲线的完整时间范围。"""
- start = self.frequency_origin
- end = max(curve.end_time for curve in self.frequency_curves)
- return start, max(end, start + 1e-15)
-
- def _fit_view(self) -> None:
- """恢复频率曲线的完整时间范围。"""
- if not self.frequency_curves:
- return
- self.view_start, self.view_end = self._full_time_range()
- self._schedule_redraw()
-
- def _zoom_view(self, factor: float, anchor_x: float | None = None) -> None:
- """以鼠标位置或窗口中心为基准缩放时间轴。"""
- if not self.frequency_curves:
- return
- left, _top, right, _bottom = self._plot_bounds()
- ratio = 0.5
- if anchor_x is not None and right > left:
- ratio = min(1.0, max(0.0, (anchor_x - left) / (right - left)))
- old_span = self.view_end - self.view_start
- full_start, full_end = self._full_time_range()
- full_span = full_end - full_start
- minimum_span = max(full_span / 1_000_000_000.0, 1e-12)
- new_span = min(full_span, max(minimum_span, old_span * factor))
- anchor_time = self.view_start + old_span * ratio
- new_start = anchor_time - new_span * ratio
- self._set_view_range(new_start, new_start + new_span)
-
- def _set_view_range(self, start: float, end: float) -> None:
- """限制时间窗口不能移出频率曲线范围。"""
- if not self.frequency_curves:
- return
- full_start, full_end = self._full_time_range()
- span = min(end - start, full_end - full_start)
- if start < full_start:
- start = full_start
- if start + span > full_end:
- start = full_end - span
- self.view_start = start
- self.view_end = start + span
- self._schedule_redraw()
-
- def _on_mouse_wheel(self, event: tk.Event) -> None:
- """使用鼠标滚轮缩放时间轴。"""
- factor = 0.75 if event.delta > 0 else 1.35
- self._zoom_view(factor, float(event.x))
-
- def _start_drag(self, event: tk.Event) -> None:
- """记录拖动开始位置。"""
- if self.frequency_curves:
- self.drag_state = (event.x, self.view_start, self.view_end)
-
- def _drag_view(self, event: tk.Event) -> None:
- """按鼠标水平位移平移时间轴。"""
- if self.drag_state is None:
- return
- start_x, original_start, original_end = self.drag_state
- left, _top, right, _bottom = self._plot_bounds()
- if right <= left:
- return
- time_shift = -(event.x - start_x) / (right - left) * (
- original_end - original_start
- )
- self._set_view_range(original_start + time_shift, original_end + time_shift)
-
- def _end_drag(self, _event: tk.Event) -> None:
- """结束鼠标拖动。"""
- self.drag_state = None
-
- def _show_cursor(self, event: tk.Event) -> None:
- """显示鼠标时间位置和各通道的实测频率。"""
- if not self.frequency_curves:
- return
- left, top, right, bottom = self._plot_bounds()
- if not left <= event.x <= right or not top <= event.y <= bottom:
- self._hide_cursor(event)
- return
- ratio = (event.x - left) / (right - left)
- time_value = self.view_start + ratio * (self.view_end - self.view_start)
- relative_time = time_value - self.frequency_origin
- values = []
- for curve in self.frequency_curves:
- _measured_time, frequency = curve.frequency_at(time_value)
- values.append(f"{curve.channel_name} {format_frequency(frequency)}")
- self.canvas.delete("cursor")
- self.canvas.create_line(
- event.x,
- top,
- event.x,
- bottom,
- fill="#374151",
- dash=(3, 3),
- tags="cursor",
- )
- self.cursor_var.set(
- f"光标 {format_duration(relative_time)}:" + " | ".join(values)
- )
-
- def _hide_cursor(self, _event: tk.Event | None = None) -> None:
- """清除鼠标光标线。"""
- self.canvas.delete("cursor")
- self.cursor_var.set("光标:-")
-
-
- def nice_frequency_max(value: float) -> float:
- """把纵轴最大频率向上取为 1、2、5、10 的整倍数。"""
- if value <= 0:
- return 1.0
- exponent = 10 ** math.floor(math.log10(value))
- fraction = value / exponent
- for candidate in (1.0, 2.0, 5.0, 10.0):
- if fraction <= candidate:
- return candidate * exponent
- return 10.0 * exponent
-
-
- def decimate_curve_indexes(
- frequencies: list[float], start: int, end: int, maximum_points: int
- ) -> list[int]:
- """抽取曲线点,同时保留每个时间桶内的最高值和最低值。"""
- point_count = end - start
- if point_count <= maximum_points:
- return list(range(start, end))
- bucket_count = max(1, maximum_points // 2)
- indexes = [start]
- for bucket in range(bucket_count):
- bucket_start = start + int(bucket * point_count / bucket_count)
- bucket_end = start + int((bucket + 1) * point_count / bucket_count)
- bucket_end = min(end, max(bucket_start + 1, bucket_end))
- minimum_index = min(
- range(bucket_start, bucket_end), key=frequencies.__getitem__
- )
- maximum_index = max(
- range(bucket_start, bucket_end), key=frequencies.__getitem__
- )
- indexes.extend(sorted((minimum_index, maximum_index)))
- indexes.append(end - 1)
- return sorted(set(indexes))
-
-
- def choose_time_unit(span: float) -> tuple[float, str]:
- """根据当前时间范围选择合适的横轴单位。"""
- if span >= 1.0:
- return 1.0, "s"
- if span >= 0.001:
- return 1_000.0, "ms"
- if span >= 0.000001:
- return 1_000_000.0, "us"
- return 1_000_000_000.0, "ns"
-
-
- def format_tick(value: float) -> str:
- """格式化时间轴刻度。"""
- absolute = abs(value)
- if absolute >= 1000:
- return f"{value:.0f}"
- if absolute >= 10:
- return f"{value:.2f}"
- return f"{value:.3f}"
-
-
- def format_hz_tick(frequency: float) -> str:
- """以 Hz 为单位格式化纵轴刻度。"""
- if frequency >= 100:
- return f"{frequency:.0f}"
- if frequency >= 10:
- return f"{frequency:.1f}"
- return f"{frequency:.2f}"
-
-
- def format_frequency(frequency: float) -> str:
- """使用 Hz、kHz、MHz 或 GHz 显示测量结果。"""
- if frequency >= 1_000_000_000:
- return f"{frequency / 1_000_000_000:.4g} GHz"
- if frequency >= 1_000_000:
- return f"{frequency / 1_000_000:.4g} MHz"
- if frequency >= 1_000:
- return f"{frequency / 1_000:.4g} kHz"
- return f"{frequency:.4g} Hz"
-
-
- def format_duration(duration: float) -> str:
- """使用合适单位显示时间长度。"""
- if duration >= 1.0:
- return f"{duration:.6g} s"
- if duration >= 0.001:
- return f"{duration * 1_000:.6g} ms"
- if duration >= 0.000001:
- return f"{duration * 1_000_000:.6g} us"
- return f"{duration * 1_000_000_000:.6g} ns"
-
-
- def main() -> None:
- """启动脉冲频率轨迹查看器。"""
- app = LogicWaveformApp()
- app.mainloop()
-
-
- if __name__ == "__main__":
- main()
|