Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

516 linhas
18 KiB

  1. #!/usr/bin/env python3
  2. """读取正点原子逻辑分析仪导出的 CSV 和 BIN 波形数据。"""
  3. from __future__ import annotations
  4. import array
  5. import bisect
  6. import csv
  7. import io
  8. import re
  9. import sys
  10. from dataclasses import dataclass
  11. from pathlib import Path
  12. from typing import Iterable
  13. from parse_logic_csv import CsvParseError, detect_delimiter, read_text, remove_comment_lines
  14. RATE_UNITS = {
  15. "hz": 1.0,
  16. "khz": 1_000.0,
  17. "mhz": 1_000_000.0,
  18. "ghz": 1_000_000_000.0,
  19. }
  20. MAX_BIN_TRANSITIONS = 2_000_000
  21. MAX_MARKER_COUNT = 10
  22. @dataclass
  23. class WaveformChannel:
  24. """保存一个数字通道的初始电平和跳变点。"""
  25. index: int
  26. name: str
  27. times: list[float]
  28. levels: list[int]
  29. @property
  30. def transition_count(self) -> int:
  31. """返回真实跳变次数,不包含初始点。"""
  32. return max(0, len(self.times) - 1)
  33. def average_frequency(self) -> float | None:
  34. """使用相邻上升沿估算平均频率。"""
  35. rising_times = [
  36. time_value
  37. for time_value, level in zip(self.times[1:], self.levels[1:])
  38. if level == 1
  39. ]
  40. if len(rising_times) < 2:
  41. return None
  42. duration = rising_times[-1] - rising_times[0]
  43. if duration <= 0:
  44. return None
  45. return (len(rising_times) - 1) / duration
  46. @dataclass
  47. class WaveformData:
  48. """保存一个波形文件的公共信息。"""
  49. path: Path
  50. source_type: str
  51. sample_rate: float | None
  52. sample_count: int | None
  53. record_count: int
  54. start_time: float
  55. end_time: float
  56. channels: list[WaveformChannel]
  57. @property
  58. def duration(self) -> float:
  59. """返回当前文件的可见时间长度。"""
  60. return max(0.0, self.end_time - self.start_time)
  61. @dataclass
  62. class FrequencyCurve:
  63. """保存由同方向脉冲边沿计算出的频率曲线。"""
  64. channel_index: int
  65. channel_name: str
  66. edge_name: str
  67. times: list[float]
  68. frequencies: list[float]
  69. @property
  70. def start_time(self) -> float:
  71. """返回曲线第一个有效脉冲边沿时间。"""
  72. return self.times[0]
  73. @property
  74. def end_time(self) -> float:
  75. """返回曲线最后一个有效脉冲边沿时间。"""
  76. return self.times[-1]
  77. def frequency_at(self, time_value: float) -> tuple[float, float]:
  78. """返回指定时间之后最近一个完整周期的时间和频率。"""
  79. index = bisect.bisect_left(self.times, time_value)
  80. if index >= len(self.times):
  81. index = len(self.times) - 1
  82. return self.times[index], self.frequencies[index]
  83. def build_frequency_curve(
  84. channel: WaveformChannel, edge_mode: str = "auto"
  85. ) -> FrequencyCurve:
  86. """使用相邻同方向边沿的周期计算瞬时脉冲频率。"""
  87. if edge_mode not in ("auto", "rising", "falling"):
  88. raise CsvParseError(f"不支持的测量边沿:{edge_mode}")
  89. if len(channel.times) < 3:
  90. raise CsvParseError(f"{channel.name} 的脉冲边沿不足,无法计算频率")
  91. # times[0] 只是文件中的初始电平状态,不能当成真实边沿。
  92. transitions = list(zip(channel.times[1:], channel.levels[1:]))
  93. if edge_mode == "auto":
  94. selected_level = transitions[0][1]
  95. else:
  96. selected_level = 1 if edge_mode == "rising" else 0
  97. edge_name = "上升沿" if selected_level == 1 else "下降沿"
  98. edge_times = [time_value for time_value, level in transitions if level == selected_level]
  99. # 自动边沿不足时再尝试另一种边沿,避免文件从脉冲中间开始造成误判。
  100. if len(edge_times) < 2 and edge_mode == "auto":
  101. selected_level = 1 - selected_level
  102. edge_name = "上升沿" if selected_level == 1 else "下降沿"
  103. edge_times = [
  104. time_value for time_value, level in transitions if level == selected_level
  105. ]
  106. if len(edge_times) < 2:
  107. raise CsvParseError(f"{channel.name} 的{edge_name}不足,无法计算频率")
  108. frequencies: list[float] = []
  109. valid_edge_times: list[float] = [edge_times[0]]
  110. for index in range(1, len(edge_times)):
  111. period = edge_times[index] - edge_times[index - 1]
  112. if period <= 0:
  113. continue
  114. frequencies.append(1.0 / period)
  115. valid_edge_times.append(edge_times[index])
  116. if not frequencies:
  117. raise CsvParseError(f"{channel.name} 没有有效脉冲周期")
  118. # 起点还没有完整周期,使用第一个完整周期的频率补齐曲线起点。
  119. curve_frequencies = [frequencies[0], *frequencies]
  120. return FrequencyCurve(
  121. channel_index=channel.index,
  122. channel_name=channel.name,
  123. edge_name=edge_name,
  124. times=valid_edge_times,
  125. frequencies=curve_frequencies,
  126. )
  127. def build_frequency_curves(
  128. channels: Iterable[WaveformChannel], edge_mode: str = "auto"
  129. ) -> list[FrequencyCurve]:
  130. """为所有具有足够脉冲边沿的通道生成频率曲线。"""
  131. curves: list[FrequencyCurve] = []
  132. for channel in channels:
  133. try:
  134. curves.append(build_frequency_curve(channel, edge_mode))
  135. except CsvParseError:
  136. continue
  137. if not curves:
  138. raise CsvParseError("所选通道没有足够的脉冲边沿,无法生成频率曲线")
  139. return curves
  140. def parse_marker_times_ms(text: str) -> list[float]:
  141. """解析最多十个毫秒关键时间,并转换为秒。"""
  142. if not text.strip():
  143. return []
  144. markers: set[float] = set()
  145. for part in re.split(r"[,,\s]+", text.strip()):
  146. if not part:
  147. continue
  148. try:
  149. milliseconds = float(part)
  150. except ValueError as exc:
  151. raise CsvParseError(f"无法识别关键时间:{part!r}") from exc
  152. if milliseconds < 0:
  153. raise CsvParseError("关键时间不能小于 0 ms")
  154. markers.add(milliseconds / 1_000.0)
  155. if len(markers) > MAX_MARKER_COUNT:
  156. raise CsvParseError(f"关键时间最多设置 {MAX_MARKER_COUNT} 个")
  157. return sorted(markers)
  158. def parse_engineering_number(text: str) -> float:
  159. """解析 20 MHz、500 kHz 等带单位的数值。"""
  160. match = re.fullmatch(
  161. r"\s*([0-9]+(?:\.[0-9]+)?)\s*([kKmMgG]?[hH][zZ])?\s*", text
  162. )
  163. if not match:
  164. raise CsvParseError(f"无法识别采样率:{text!r}")
  165. number = float(match.group(1))
  166. unit = (match.group(2) or "Hz").lower()
  167. return number * RATE_UNITS[unit]
  168. def parse_scaled_count(text: str) -> int | None:
  169. """解析 125.351087 M 形式的采样点数量。"""
  170. match = re.fullmatch(r"\s*([0-9]+(?:\.[0-9]+)?)\s*([kKmMgG])?\s*", text)
  171. if not match:
  172. return None
  173. scales = {"": 1.0, "k": 1_000.0, "m": 1_000_000.0, "g": 1_000_000_000.0}
  174. return int(float(match.group(1)) * scales[(match.group(2) or "").lower()])
  175. def parse_metadata(text: str) -> tuple[float | None, int | None]:
  176. """读取 CSV 注释中的采样率和采样数量。"""
  177. sample_rate: float | None = None
  178. sample_count: int | None = None
  179. for line in text.splitlines():
  180. stripped = line.strip()
  181. if not stripped.startswith(";"):
  182. continue
  183. content = stripped[1:].strip()
  184. key, separator, value = content.partition(":")
  185. if not separator:
  186. continue
  187. key = key.strip().lower()
  188. value = value.strip()
  189. if key == "sample rate":
  190. sample_rate = parse_engineering_number(value)
  191. elif key == "sample count":
  192. sample_count = parse_scaled_count(value)
  193. return sample_rate, sample_count
  194. def find_numeric_time_column(header: list[str], rows: list[list[str]]) -> int:
  195. """优先选择正点原子 CSV 中的 Time(s) 数字时间列。"""
  196. lowered = [name.strip().lower() for name in header]
  197. preferred_names = ("time(s)", "time (s)", "time", "timestamp", "t", "时间")
  198. for name in preferred_names:
  199. if name in lowered:
  200. return lowered.index(name)
  201. # 没有标准名称时,选择前几行都能转换成浮点数的列。
  202. for column in range(len(header)):
  203. valid = True
  204. for row in rows[:20]:
  205. if column >= len(row):
  206. valid = False
  207. break
  208. try:
  209. float(row[column].strip().lstrip("'"))
  210. except ValueError:
  211. valid = False
  212. break
  213. if valid:
  214. return column
  215. raise CsvParseError("找不到可用的数字时间列")
  216. def find_channel_columns(
  217. header: list[str], rows: list[list[str]], time_index: int
  218. ) -> list[tuple[int, int, str]]:
  219. """识别 Channel 0、CH1、D2 等数字通道列。"""
  220. channels: list[tuple[int, int, str]] = []
  221. used_indexes: set[int] = set()
  222. patterns = (
  223. re.compile(r"^channel\s*[_-]?\s*(\d+)$", re.IGNORECASE),
  224. re.compile(r"^(?:ch|d)\s*[_-]?\s*(\d+)$", re.IGNORECASE),
  225. )
  226. for column, name in enumerate(header):
  227. if column == time_index:
  228. continue
  229. channel_index: int | None = None
  230. for pattern in patterns:
  231. match = pattern.fullmatch(name.strip())
  232. if match:
  233. channel_index = int(match.group(1))
  234. break
  235. if channel_index is None or channel_index in used_indexes:
  236. continue
  237. channels.append((column, channel_index, name.strip()))
  238. used_indexes.add(channel_index)
  239. if channels:
  240. return sorted(channels, key=lambda item: item[1])
  241. # 兼容只有“时间,电平”两列的简单文件。
  242. for column, name in enumerate(header):
  243. if column == time_index:
  244. continue
  245. valid = True
  246. for row in rows[:100]:
  247. if column >= len(row) or row[column].strip() not in ("0", "1"):
  248. valid = False
  249. break
  250. if valid:
  251. channels.append((column, len(channels), name.strip() or f"Channel {len(channels)}"))
  252. if not channels:
  253. raise CsvParseError("CSV 中没有找到高低电平通道")
  254. return channels
  255. def load_csv_waveform(path: Path, encoding: str = "auto") -> WaveformData:
  256. """读取 CSV 中的全部数字通道并保存跳变点。"""
  257. text = read_text(path, encoding)
  258. sample_rate, sample_count = parse_metadata(text)
  259. clean_text = remove_comment_lines(text)
  260. if not clean_text:
  261. raise CsvParseError("CSV 中没有有效数据")
  262. delimiter = detect_delimiter(clean_text)
  263. rows = list(csv.reader(io.StringIO(clean_text), delimiter=delimiter))
  264. if len(rows) < 2:
  265. raise CsvParseError("CSV 缺少表头或采样数据")
  266. header = [name.strip() for name in rows[0]]
  267. data_rows = [row for row in rows[1:] if row]
  268. time_index = find_numeric_time_column(header, data_rows)
  269. channel_columns = find_channel_columns(header, data_rows, time_index)
  270. channel_map = {
  271. column: WaveformChannel(index, name, [], [])
  272. for column, index, name in channel_columns
  273. }
  274. first_time: float | None = None
  275. last_time: float | None = None
  276. previous_levels: dict[int, int] = {}
  277. valid_rows = 0
  278. for line_number, row in enumerate(data_rows, start=2):
  279. needed_column = max([time_index, *channel_map.keys()])
  280. if len(row) <= needed_column:
  281. raise CsvParseError(f"CSV 第 {line_number} 行列数不足")
  282. try:
  283. time_value = float(row[time_index].strip().lstrip("'"))
  284. except ValueError as exc:
  285. raise CsvParseError(f"CSV 第 {line_number} 行时间不是数字") from exc
  286. if last_time is not None and time_value < last_time:
  287. raise CsvParseError(f"CSV 第 {line_number} 行时间顺序错误")
  288. if first_time is None:
  289. first_time = time_value
  290. last_time = time_value
  291. valid_rows += 1
  292. for column, channel in channel_map.items():
  293. level_text = row[column].strip()
  294. if level_text not in ("0", "1"):
  295. raise CsvParseError(
  296. f"CSV 第 {line_number} 行的 {channel.name} 不是 0 或 1"
  297. )
  298. level = int(level_text)
  299. if column not in previous_levels or previous_levels[column] != level:
  300. channel.times.append(time_value)
  301. channel.levels.append(level)
  302. previous_levels[column] = level
  303. if first_time is None or last_time is None:
  304. raise CsvParseError("CSV 中没有有效采样点")
  305. return WaveformData(
  306. path=path,
  307. source_type="CSV",
  308. sample_rate=sample_rate,
  309. sample_count=sample_count,
  310. record_count=valid_rows,
  311. start_time=first_time,
  312. end_time=last_time,
  313. channels=list(channel_map.values()),
  314. )
  315. def parse_channel_selection(text: str, channel_count: int) -> list[int] | None:
  316. """解析自动、4、0,1,4、0-7 等通道选择写法。"""
  317. stripped = text.strip().lower()
  318. if not stripped or stripped in ("自动", "auto", "all", "全部"):
  319. return None
  320. selected: set[int] = set()
  321. for part in re.split(r"[,,\s]+", stripped):
  322. if not part:
  323. continue
  324. if "-" in part:
  325. start_text, end_text = part.split("-", 1)
  326. if not start_text.isdigit() or not end_text.isdigit():
  327. raise CsvParseError(f"无法识别通道范围:{part}")
  328. start = int(start_text)
  329. end = int(end_text)
  330. if start > end:
  331. start, end = end, start
  332. selected.update(range(start, end + 1))
  333. elif part.isdigit():
  334. selected.add(int(part))
  335. else:
  336. match = re.fullmatch(r"(?:channel|ch|d)\s*(\d+)", part)
  337. if not match:
  338. raise CsvParseError(f"无法识别通道:{part}")
  339. selected.add(int(match.group(1)))
  340. invalid = [index for index in sorted(selected) if not 0 <= index < channel_count]
  341. if invalid:
  342. raise CsvParseError(f"通道超出 0~{channel_count - 1} 范围:{invalid}")
  343. return sorted(selected)
  344. def _sample_values(data: bytes, sample_width: int) -> Iterable[int]:
  345. """把原始字节按每个采样点 1 或 2 字节解释。"""
  346. if sample_width == 1:
  347. return data
  348. values = array.array("H")
  349. values.frombytes(data)
  350. if sys.byteorder != "little":
  351. values.byteswap()
  352. return values
  353. def load_bin_waveform(
  354. path: Path,
  355. sample_rate: float,
  356. channel_count: int,
  357. selected_indexes: list[int] | None = None,
  358. ) -> WaveformData:
  359. """按无文件头的原始采样格式读取 BIN。"""
  360. if sample_rate <= 0:
  361. raise CsvParseError("BIN 采样率必须大于 0")
  362. if not 1 <= channel_count <= 16:
  363. raise CsvParseError("BIN 通道数只支持 1~16")
  364. sample_width = 1 if channel_count <= 8 else 2
  365. data = path.read_bytes()
  366. if not data:
  367. raise CsvParseError("BIN 文件为空")
  368. if len(data) % sample_width != 0:
  369. raise CsvParseError(f"BIN 文件长度不能按 {sample_width} 字节采样点整除")
  370. values = _sample_values(data, sample_width)
  371. del data
  372. sample_count = len(values) # type: ignore[arg-type]
  373. selected = selected_indexes or list(range(channel_count))
  374. selected_mask = sum(1 << index for index in selected)
  375. first_value = values[0] # type: ignore[index]
  376. channel_map = {
  377. index: WaveformChannel(
  378. index=index,
  379. name=f"Channel {index}",
  380. times=[0.0],
  381. levels=[(first_value >> index) & 1],
  382. )
  383. for index in selected
  384. }
  385. previous_value = first_value
  386. transition_total = 0
  387. for sample_index in range(1, sample_count):
  388. value = values[sample_index] # type: ignore[index]
  389. changed = (value ^ previous_value) & selected_mask
  390. while changed:
  391. lowest_bit = changed & -changed
  392. channel_index = lowest_bit.bit_length() - 1
  393. channel = channel_map[channel_index]
  394. channel.times.append(sample_index / sample_rate)
  395. channel.levels.append((value >> channel_index) & 1)
  396. transition_total += 1
  397. if transition_total > MAX_BIN_TRANSITIONS:
  398. raise CsvParseError(
  399. "BIN 跳变点过多,请在“显示通道”中只填写需要查看的通道"
  400. )
  401. changed ^= lowest_bit
  402. previous_value = value
  403. end_time = 0.0 if sample_count <= 1 else (sample_count - 1) / sample_rate
  404. return WaveformData(
  405. path=path,
  406. source_type="BIN",
  407. sample_rate=sample_rate,
  408. sample_count=sample_count,
  409. record_count=sample_count,
  410. start_time=0.0,
  411. end_time=end_time,
  412. channels=list(channel_map.values()),
  413. )
  414. def load_waveform(
  415. path: Path,
  416. bin_sample_rate: float,
  417. bin_channel_count: int,
  418. channel_text: str,
  419. ) -> tuple[WaveformData, list[WaveformChannel]]:
  420. """根据扩展名读取文件,并返回需要显示的通道。"""
  421. suffix = path.suffix.lower()
  422. if suffix == ".csv":
  423. data = load_csv_waveform(path)
  424. selected_indexes = parse_channel_selection(channel_text, len(data.channels))
  425. if selected_indexes is None:
  426. visible = [channel for channel in data.channels if channel.transition_count > 0]
  427. if not visible:
  428. visible = data.channels[:8]
  429. else:
  430. selected_set = set(selected_indexes)
  431. visible = [channel for channel in data.channels if channel.index in selected_set]
  432. elif suffix == ".bin":
  433. selected_indexes = parse_channel_selection(channel_text, bin_channel_count)
  434. data = load_bin_waveform(
  435. path,
  436. sample_rate=bin_sample_rate,
  437. channel_count=bin_channel_count,
  438. selected_indexes=selected_indexes,
  439. )
  440. if selected_indexes is None:
  441. visible = [channel for channel in data.channels if channel.transition_count > 0]
  442. if not visible:
  443. visible = data.channels[:8]
  444. else:
  445. visible = data.channels
  446. else:
  447. raise CsvParseError("只支持正点原子导出的 CSV 或 BIN 文件")
  448. if not visible:
  449. raise CsvParseError("所选通道没有可显示的数据")
  450. return data, visible