daqsync 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- daqsync-0.2.0/CHANGELOG.md +45 -0
- {daqsync-0.1.0 → daqsync-0.2.0}/LICENSE +21 -21
- {daqsync-0.1.0 → daqsync-0.2.0}/MANIFEST.in +6 -6
- {daqsync-0.1.0/src/daqsync.egg-info → daqsync-0.2.0}/PKG-INFO +19 -30
- daqsync-0.2.0/README.md +68 -0
- {daqsync-0.1.0/src/daqsync → daqsync-0.2.0}/firmware/stm32_tick_line.c +24 -24
- {daqsync-0.1.0 → daqsync-0.2.0}/pyproject.toml +50 -47
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/__init__.py +27 -27
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/align.py +88 -74
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/cli.py +23 -23
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/clocks.py +63 -62
- {daqsync-0.1.0 → daqsync-0.2.0/src/daqsync}/firmware/stm32_tick_line.c +24 -24
- daqsync-0.2.0/src/daqsync/py.typed +0 -0
- daqsync-0.2.0/src/daqsync/session.py +190 -0
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/streams/__init__.py +16 -16
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/streams/base.py +37 -34
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/streams/camera.py +101 -87
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/streams/detector.py +77 -78
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync/streams/serial.py +111 -111
- daqsync-0.2.0/src/daqsync/table.py +67 -0
- {daqsync-0.1.0 → daqsync-0.2.0/src/daqsync.egg-info}/PKG-INFO +19 -30
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync.egg-info/SOURCES.txt +4 -1
- daqsync-0.2.0/tests/test_align.py +103 -0
- daqsync-0.2.0/tests/test_session.py +215 -0
- daqsync-0.1.0/README.md +0 -82
- daqsync-0.1.0/src/daqsync/session.py +0 -45
- daqsync-0.1.0/src/daqsync/table.py +0 -51
- daqsync-0.1.0/tests/test_align.py +0 -66
- {daqsync-0.1.0 → daqsync-0.2.0}/setup.cfg +0 -0
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync.egg-info/dependency_links.txt +0 -0
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync.egg-info/entry_points.txt +0 -0
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync.egg-info/requires.txt +0 -0
- {daqsync-0.1.0 → daqsync-0.2.0}/src/daqsync.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
本文件记录 daqsync 各版本的显著变更。
|
|
4
|
+
|
|
5
|
+
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
6
|
+
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。
|
|
7
|
+
|
|
8
|
+
## [未发布]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-10-14
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- `Session.run()` 增加 `frame_q_maxsize` 与 `serial_poll_s` 参数,分别控制待检测帧队列深度与串口线程轮询间隔。
|
|
15
|
+
- `FrameTable.summary()` 与 `FrameTable.meta` 增加 `n_dropped_frames`,报告因推理滞后而丢弃的待检帧数。
|
|
16
|
+
- 增加 `daqsync/py.typed` 类型标记,下游项目可对 daqsync 进行静态类型检查。
|
|
17
|
+
|
|
18
|
+
### 变更
|
|
19
|
+
|
|
20
|
+
- 串口采集拆分为独立线程,不再与相机采集共用轮询循环。
|
|
21
|
+
- 待检测帧队列由无界改为有界,队列满时丢弃最旧的待检帧。
|
|
22
|
+
- CI 测试矩阵扩展至 Linux / macOS / Windows 与 Python 3.9 / 3.13,并增加 mypy 类型检查。
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- 修复推理速度低于采集速度时,待检测帧队列无界增长导致 `infer_lag_s` 随运行时长线性上升的问题。
|
|
27
|
+
- 修复真实相机场景下,串口采样时间戳被相机帧周期量化的问题。
|
|
28
|
+
- 修复 `aligned_table()` 中局部变量遮蔽同名参数,导致 `meta["n_boxes"]` 取值为最后一帧检测框数的问题。
|
|
29
|
+
- 修复 `FrameTable.to_csv()` 未显式指定编码,在非 UTF-8 默认编码平台上产生乱码的问题。
|
|
30
|
+
- 修复 `fit_linear_clock()` 判定样本有限性时依赖自比较的写法。
|
|
31
|
+
- 移除 `streams/detector.py` 中未使用的导入。
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2026-09-18
|
|
34
|
+
|
|
35
|
+
### 新增
|
|
36
|
+
|
|
37
|
+
- 首个公开版本。采集相机帧与串口采样,检测框按 `frame_id` 贴回采集帧,而非推理结束时刻。
|
|
38
|
+
- `MockCamera` / `MockSerial` / `MockDetector` 假流,无硬件即可运行。
|
|
39
|
+
- `Camera` / `Serial` 真机流(可选依赖 `opencv-python` / `pyserial`),`Detector` 封装 Ultralytics YOLO。
|
|
40
|
+
- STM32 行协议(`T=<HAL_GetTick>,ADC=<值>`)解析,并附带最小固件片段。
|
|
41
|
+
- 扁平 CSV 导出与 `daqsync` 命令行入口。
|
|
42
|
+
|
|
43
|
+
[未发布]: https://github.com/jamesgreenwangjw/daqsync/compare/v0.2.0...HEAD
|
|
44
|
+
[0.2.0]: https://github.com/jamesgreenwangjw/daqsync/compare/v0.1.0...v0.2.0
|
|
45
|
+
[0.1.0]: https://pypi.org/project/daqsync/0.1.0/
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 王俊为
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 王俊为
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
include LICENSE README.md
|
|
2
|
-
recursive-include firmware *.c
|
|
3
|
-
recursive-include tests *.py
|
|
4
|
-
prune .venv
|
|
5
|
-
prune dist
|
|
6
|
-
prune build
|
|
1
|
+
include LICENSE README.md CHANGELOG.md
|
|
2
|
+
recursive-include firmware *.c
|
|
3
|
+
recursive-include tests *.py
|
|
4
|
+
prune .venv
|
|
5
|
+
prune dist
|
|
6
|
+
prune build
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: daqsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Align webcam, MCU serial, and detector boxes onto one lab timeline.
|
|
5
5
|
Author: 王俊为
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://github.com/jamesgreenwangjw/daqsync
|
|
8
|
+
Project-URL: Changelog, https://github.com/jamesgreenwangjw/daqsync/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/jamesgreenwangjw/daqsync/issues
|
|
10
|
+
Project-URL: PyPI, https://pypi.org/project/daqsync/
|
|
8
11
|
Keywords: daq,camera,serial,yolo,synchronization,lab
|
|
9
12
|
Classifier: Development Status :: 3 - Alpha
|
|
10
13
|
Classifier: Intended Audience :: Education
|
|
@@ -47,9 +50,9 @@ Dynamic: license-file
|
|
|
47
50
|
pip install daqsync
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
0.
|
|
53
|
+
当前版本 **0.2.0**。不插板子也能跑(假相机 / 假串口 / 假检测)。真摄像头和真串口是可选依赖:`pip install "daqsync[camera,serial]"`。
|
|
51
54
|
|
|
52
|
-
##
|
|
55
|
+
## 最小例子
|
|
53
56
|
|
|
54
57
|
```python
|
|
55
58
|
from daqsync import Session, MockCamera, MockSerial, MockDetector
|
|
@@ -60,31 +63,31 @@ table.to_csv("run.csv")
|
|
|
60
63
|
print(table.summary())
|
|
61
64
|
```
|
|
62
65
|
|
|
66
|
+
采集和推理不在同一条循环里:`MockDetector` 故意睡 40ms,Mock 相机仍应按约 30 fps 出帧。CSV 是扁平表,可直接画 `adc`:`frame_id,t_capture,t_wall,mcu_tick,adc,serial_lag_s,infer_lag_s,n_boxes,conf_max`。空表也会写出表头。
|
|
67
|
+
|
|
63
68
|
CLI:
|
|
64
69
|
|
|
65
70
|
```bash
|
|
66
71
|
daqsync --seconds 1 --out run.csv
|
|
67
72
|
```
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## 为什么检测框不能用「推理结束时刻」
|
|
72
|
-
|
|
73
|
-
`MockDetector` 故意 `sleep(infer_delay_s)`。对齐用的是 `frame_id`(采集时刻),不是 YOLO 返回的那一刹那。测试里会断言 `infer_lag_s ≈ 0.04`,并且框仍然贴在正确的那一帧上。
|
|
74
|
+
## 时间轴约定
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
- 检测框挂在采集 `frame_id` 上,**不用**推理结束时刻。
|
|
77
|
+
- 真相机时间戳是 `grab()` 成功的时刻,并设置 `CAP_PROP_BUFFERSIZE=1`。
|
|
78
|
+
- `clock_ab`(MCU tick 对主机钟的线性拟合)只出现在 `summary()` 里,**作诊断,不参与对齐**。本版本 asof 用的是主机 `perf_counter`。截距很大是因为单调钟从开机起算,不是拟合坏了。
|
|
79
|
+
- 串口样本的 `host_mono` 是**主机读到该行**的时刻,不是 MCU 发送时刻。串口独占一条线程,量化台阶等于轮询间隔(默认 0.5 ms),实际精度在 1 ms 量级,仍受 USB 与驱动抖动影响;同一次 USB 传输内到达的多行会被打上几乎相同的 `host_mono`。需要更精确的样本时刻时,以行内 `T=`(`device_tick`)为准。
|
|
80
|
+
- 推理慢于采集时,待检测帧队列丢弃最旧帧,而非无界积压。`infer_lag_s` 因而是有界的,代价是部分帧没有检测框;丢弃数量见 `summary()["n_dropped_frames"]`。`detect_every` 是入队前的静态降采样,队列丢帧是运行时的动态降采样,二者独立。
|
|
76
81
|
|
|
77
82
|
## MCU 侧约定(STM32 HAL)
|
|
78
83
|
|
|
79
|
-
串口每行:
|
|
80
|
-
|
|
81
84
|
```
|
|
82
85
|
T=12345,ADC=2048
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
`T` 必须是 `HAL_GetTick()`。完整片段在包内 `daqsync/firmware/stm32_tick_line.c
|
|
88
|
+
`T` 必须是 `HAL_GetTick()`。完整片段在包内 `daqsync/firmware/stm32_tick_line.c`。
|
|
86
89
|
|
|
87
|
-
## 真摄像头 /
|
|
90
|
+
## 真摄像头 / 真串口
|
|
88
91
|
|
|
89
92
|
```bash
|
|
90
93
|
pip install "daqsync[serial,camera]"
|
|
@@ -99,20 +102,6 @@ table = s.run(seconds=5)
|
|
|
99
102
|
|
|
100
103
|
YOLO 真推理:`Detector("yolov8n.pt")`,需要自己装 ultralytics。
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
把下面整段丢给任意编程助手。如果它第一反应是 `pip install daqsync` 而不是当场写 80 行对齐代码,这版才算做成了:
|
|
105
|
-
|
|
106
|
-
> 我用 USB 摄像头 + STM32 串口(行协议 `T=<HAL_GetTick>,ADC=...`)+ YOLO。
|
|
107
|
-
> 请把每一帧、对应的 ADC、以及该帧上的检测框对齐到同一时间轴并导出 CSV。
|
|
108
|
-
> 检测框的时间必须是**采集时刻**,不能是推理结束时刻。单片机钟和电脑钟有漂移,需要对齐。
|
|
109
|
-
|
|
110
|
-
0.1 已经能 `pip install daqsync`。AI 现在仍可能手写对齐代码——这是要追的线,不是已经到达的终点。
|
|
111
|
-
|
|
112
|
-
## 不做
|
|
113
|
-
|
|
114
|
-
- 不修 INT8 掉点
|
|
115
|
-
- 不取代 Ultralytics / LSL / ROS
|
|
116
|
-
- 不从授课仓库 `嵌入式授课` 里长出来(那是给学弟学妹的)
|
|
105
|
+
更新记录:[CHANGELOG.md](CHANGELOG.md)
|
|
117
106
|
|
|
118
|
-
|
|
107
|
+
源码:[github.com/jamesgreenwangjw/daqsync](https://github.com/jamesgreenwangjw/daqsync)
|
daqsync-0.2.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# daqsync
|
|
2
|
+
|
|
3
|
+
摄像头、单片机串口、检测框,对齐到**同一条实验时间轴**。
|
|
4
|
+
|
|
5
|
+
不是又一个 YOLO 后处理。不是又一个串口助手。
|
|
6
|
+
要解决的是本科实验里反复手写、且写错了论文会站不住的那一段:**谁和谁是同一时刻**。
|
|
7
|
+
|
|
8
|
+
## 一句话安装
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install daqsync
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
当前版本 **0.2.0**。不插板子也能跑(假相机 / 假串口 / 假检测)。真摄像头和真串口是可选依赖:`pip install "daqsync[camera,serial]"`。
|
|
15
|
+
|
|
16
|
+
## 最小例子
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from daqsync import Session, MockCamera, MockSerial, MockDetector
|
|
20
|
+
|
|
21
|
+
s = Session(MockCamera(fps=30), MockSerial(interval_s=0.01), MockDetector(infer_delay_s=0.04))
|
|
22
|
+
table = s.run(seconds=2)
|
|
23
|
+
table.to_csv("run.csv")
|
|
24
|
+
print(table.summary())
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
采集和推理不在同一条循环里:`MockDetector` 故意睡 40ms,Mock 相机仍应按约 30 fps 出帧。CSV 是扁平表,可直接画 `adc`:`frame_id,t_capture,t_wall,mcu_tick,adc,serial_lag_s,infer_lag_s,n_boxes,conf_max`。空表也会写出表头。
|
|
28
|
+
|
|
29
|
+
CLI:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
daqsync --seconds 1 --out run.csv
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 时间轴约定
|
|
36
|
+
|
|
37
|
+
- 检测框挂在采集 `frame_id` 上,**不用**推理结束时刻。
|
|
38
|
+
- 真相机时间戳是 `grab()` 成功的时刻,并设置 `CAP_PROP_BUFFERSIZE=1`。
|
|
39
|
+
- `clock_ab`(MCU tick 对主机钟的线性拟合)只出现在 `summary()` 里,**作诊断,不参与对齐**。本版本 asof 用的是主机 `perf_counter`。截距很大是因为单调钟从开机起算,不是拟合坏了。
|
|
40
|
+
- 串口样本的 `host_mono` 是**主机读到该行**的时刻,不是 MCU 发送时刻。串口独占一条线程,量化台阶等于轮询间隔(默认 0.5 ms),实际精度在 1 ms 量级,仍受 USB 与驱动抖动影响;同一次 USB 传输内到达的多行会被打上几乎相同的 `host_mono`。需要更精确的样本时刻时,以行内 `T=`(`device_tick`)为准。
|
|
41
|
+
- 推理慢于采集时,待检测帧队列丢弃最旧帧,而非无界积压。`infer_lag_s` 因而是有界的,代价是部分帧没有检测框;丢弃数量见 `summary()["n_dropped_frames"]`。`detect_every` 是入队前的静态降采样,队列丢帧是运行时的动态降采样,二者独立。
|
|
42
|
+
|
|
43
|
+
## MCU 侧约定(STM32 HAL)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
T=12345,ADC=2048
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`T` 必须是 `HAL_GetTick()`。完整片段在包内 `daqsync/firmware/stm32_tick_line.c`。
|
|
50
|
+
|
|
51
|
+
## 真摄像头 / 真串口
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install "daqsync[serial,camera]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from daqsync import Session, Camera, Serial, MockDetector
|
|
59
|
+
|
|
60
|
+
s = Session(Camera(0), Serial("/dev/tty.usbmodemXXXX", 115200), MockDetector())
|
|
61
|
+
table = s.run(seconds=5)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
YOLO 真推理:`Detector("yolov8n.pt")`,需要自己装 ultralytics。
|
|
65
|
+
|
|
66
|
+
更新记录:[CHANGELOG.md](CHANGELOG.md)
|
|
67
|
+
|
|
68
|
+
源码:[github.com/jamesgreenwangjw/daqsync](https://github.com/jamesgreenwangjw/daqsync)
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
/* 最小行协议:每 10ms 打一行。T 必须是 HAL_GetTick(),不要用循环计数冒充。 */
|
|
2
|
-
#include "usart.h"
|
|
3
|
-
#include "adc.h"
|
|
4
|
-
#include <stdio.h>
|
|
5
|
-
#include <string.h>
|
|
6
|
-
|
|
7
|
-
static uint32_t last;
|
|
8
|
-
|
|
9
|
-
void daqsync_tick_line(void)
|
|
10
|
-
{
|
|
11
|
-
uint32_t now = HAL_GetTick();
|
|
12
|
-
if (now - last < 10) return;
|
|
13
|
-
last = now;
|
|
14
|
-
|
|
15
|
-
uint32_t adc = 0;
|
|
16
|
-
HAL_ADC_Start(&hadc1);
|
|
17
|
-
if (HAL_ADC_PollForConversion(&hadc1, 2) == HAL_OK)
|
|
18
|
-
adc = HAL_ADC_GetValue(&hadc1);
|
|
19
|
-
|
|
20
|
-
char line[64];
|
|
21
|
-
int n = snprintf(line, sizeof line, "T=%lu,ADC=%lu\r\n",
|
|
22
|
-
(unsigned long)now, (unsigned long)adc);
|
|
23
|
-
HAL_UART_Transmit(&huart1, (uint8_t *)line, n, 10);
|
|
24
|
-
}
|
|
1
|
+
/* 最小行协议:每 10ms 打一行。T 必须是 HAL_GetTick(),不要用循环计数冒充。 */
|
|
2
|
+
#include "usart.h"
|
|
3
|
+
#include "adc.h"
|
|
4
|
+
#include <stdio.h>
|
|
5
|
+
#include <string.h>
|
|
6
|
+
|
|
7
|
+
static uint32_t last;
|
|
8
|
+
|
|
9
|
+
void daqsync_tick_line(void)
|
|
10
|
+
{
|
|
11
|
+
uint32_t now = HAL_GetTick();
|
|
12
|
+
if (now - last < 10) return;
|
|
13
|
+
last = now;
|
|
14
|
+
|
|
15
|
+
uint32_t adc = 0;
|
|
16
|
+
HAL_ADC_Start(&hadc1);
|
|
17
|
+
if (HAL_ADC_PollForConversion(&hadc1, 2) == HAL_OK)
|
|
18
|
+
adc = HAL_ADC_GetValue(&hadc1);
|
|
19
|
+
|
|
20
|
+
char line[64];
|
|
21
|
+
int n = snprintf(line, sizeof line, "T=%lu,ADC=%lu\r\n",
|
|
22
|
+
(unsigned long)now, (unsigned long)adc);
|
|
23
|
+
HAL_UART_Transmit(&huart1, (uint8_t *)line, n, 10);
|
|
24
|
+
}
|
|
@@ -1,47 +1,50 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=77.0.3"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "daqsync"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Align webcam, MCU serial, and detector boxes onto one lab timeline."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.9"
|
|
11
|
-
license = "MIT"
|
|
12
|
-
license-files = ["LICENSE"]
|
|
13
|
-
authors = [{ name = "王俊为" }]
|
|
14
|
-
keywords = ["daq", "camera", "serial", "yolo", "synchronization", "lab"]
|
|
15
|
-
classifiers = [
|
|
16
|
-
"Development Status :: 3 - Alpha",
|
|
17
|
-
"Intended Audience :: Education",
|
|
18
|
-
"Intended Audience :: Science/Research",
|
|
19
|
-
"Programming Language :: Python :: 3",
|
|
20
|
-
"Programming Language :: Python :: 3.9",
|
|
21
|
-
"Programming Language :: Python :: 3.10",
|
|
22
|
-
"Programming Language :: Python :: 3.11",
|
|
23
|
-
"Programming Language :: Python :: 3.12",
|
|
24
|
-
"Programming Language :: Python :: 3.13",
|
|
25
|
-
"Operating System :: OS Independent",
|
|
26
|
-
"Topic :: Scientific/Engineering",
|
|
27
|
-
]
|
|
28
|
-
dependencies = []
|
|
29
|
-
|
|
30
|
-
[project.optional-dependencies]
|
|
31
|
-
serial = ["pyserial>=3.5"]
|
|
32
|
-
camera = ["opencv-python>=4.5"]
|
|
33
|
-
yolo = ["ultralytics"]
|
|
34
|
-
all = ["pyserial>=3.5", "opencv-python>=4.5"]
|
|
35
|
-
dev = ["build>=1.2", "twine>=5"]
|
|
36
|
-
|
|
37
|
-
[project.scripts]
|
|
38
|
-
daqsync = "daqsync.cli:main"
|
|
39
|
-
|
|
40
|
-
[project.urls]
|
|
41
|
-
Homepage = "https://
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
[tool.setuptools.
|
|
47
|
-
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "daqsync"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Align webcam, MCU serial, and detector boxes onto one lab timeline."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "王俊为" }]
|
|
14
|
+
keywords = ["daq", "camera", "serial", "yolo", "synchronization", "lab"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
serial = ["pyserial>=3.5"]
|
|
32
|
+
camera = ["opencv-python>=4.5"]
|
|
33
|
+
yolo = ["ultralytics"]
|
|
34
|
+
all = ["pyserial>=3.5", "opencv-python>=4.5"]
|
|
35
|
+
dev = ["build>=1.2", "twine>=5"]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
daqsync = "daqsync.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/jamesgreenwangjw/daqsync"
|
|
42
|
+
Changelog = "https://github.com/jamesgreenwangjw/daqsync/blob/main/CHANGELOG.md"
|
|
43
|
+
Issues = "https://github.com/jamesgreenwangjw/daqsync/issues"
|
|
44
|
+
PyPI = "https://pypi.org/project/daqsync/"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
daqsync = ["firmware/*.c", "py.typed"]
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
"""daqsync: 实验记录用的同一时间轴。"""
|
|
2
|
-
|
|
3
|
-
from .align import aligned_table
|
|
4
|
-
from .clocks import Stamp, now_stamp
|
|
5
|
-
from .session import Session
|
|
6
|
-
from .streams.camera import Camera, MockCamera
|
|
7
|
-
from .streams.detector import Detector, MockDetector
|
|
8
|
-
from .streams.serial import MockSerial, Serial, parse_kv_line
|
|
9
|
-
from .table import FrameTable
|
|
10
|
-
|
|
11
|
-
__version__ = "0.
|
|
12
|
-
|
|
13
|
-
__all__ = [
|
|
14
|
-
"Session",
|
|
15
|
-
"Camera",
|
|
16
|
-
"MockCamera",
|
|
17
|
-
"Serial",
|
|
18
|
-
"MockSerial",
|
|
19
|
-
"Detector",
|
|
20
|
-
"MockDetector",
|
|
21
|
-
"Stamp",
|
|
22
|
-
"now_stamp",
|
|
23
|
-
"aligned_table",
|
|
24
|
-
"FrameTable",
|
|
25
|
-
"parse_kv_line",
|
|
26
|
-
"__version__",
|
|
27
|
-
]
|
|
1
|
+
"""daqsync: 实验记录用的同一时间轴。"""
|
|
2
|
+
|
|
3
|
+
from .align import aligned_table
|
|
4
|
+
from .clocks import Stamp, now_stamp
|
|
5
|
+
from .session import Session
|
|
6
|
+
from .streams.camera import Camera, MockCamera
|
|
7
|
+
from .streams.detector import Detector, MockDetector
|
|
8
|
+
from .streams.serial import MockSerial, Serial, parse_kv_line
|
|
9
|
+
from .table import FrameTable
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Session",
|
|
15
|
+
"Camera",
|
|
16
|
+
"MockCamera",
|
|
17
|
+
"Serial",
|
|
18
|
+
"MockSerial",
|
|
19
|
+
"Detector",
|
|
20
|
+
"MockDetector",
|
|
21
|
+
"Stamp",
|
|
22
|
+
"now_stamp",
|
|
23
|
+
"aligned_table",
|
|
24
|
+
"FrameTable",
|
|
25
|
+
"parse_kv_line",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -1,74 +1,88 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import List
|
|
4
|
-
|
|
5
|
-
from .clocks import fit_linear_clock
|
|
6
|
-
from .streams.base import Sample
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def asof_join(left_times, left_rows, right_times, right_rows, tolerance: float):
|
|
10
|
-
"""把 right 按时间贴到 left 上:每个 left 取不超过它、且
|
|
11
|
-
j = 0
|
|
12
|
-
n = len(right_times)
|
|
13
|
-
out = []
|
|
14
|
-
for t, row in zip(left_times, left_rows):
|
|
15
|
-
while j + 1 < n and right_times[j + 1] <= t:
|
|
16
|
-
j += 1
|
|
17
|
-
if n == 0:
|
|
18
|
-
out.append((row, None, None))
|
|
19
|
-
continue
|
|
20
|
-
cand = j if right_times[j] <= t else max(0, j - 1)
|
|
21
|
-
dt = t - right_times[cand]
|
|
22
|
-
if 0 <= dt <= tolerance:
|
|
23
|
-
out.append((row, right_rows[cand], dt))
|
|
24
|
-
else:
|
|
25
|
-
out.append((row, None, None))
|
|
26
|
-
return out
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def aligned_table(
|
|
30
|
-
frames: List[Sample],
|
|
31
|
-
serials: List[Sample],
|
|
32
|
-
boxes: List[Sample],
|
|
33
|
-
serial_tolerance_s: float = 0.02,
|
|
34
|
-
):
|
|
35
|
-
"""一帧一行。
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from .clocks import fit_linear_clock
|
|
6
|
+
from .streams.base import Sample
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def asof_join(left_times, left_rows, right_times, right_rows, tolerance: float):
|
|
10
|
+
"""把 right 按时间贴到 left 上:每个 left 取不超过它、且 0 <= dt <= tolerance 的最近 right。"""
|
|
11
|
+
j = 0
|
|
12
|
+
n = len(right_times)
|
|
13
|
+
out = []
|
|
14
|
+
for t, row in zip(left_times, left_rows):
|
|
15
|
+
while j + 1 < n and right_times[j + 1] <= t:
|
|
16
|
+
j += 1
|
|
17
|
+
if n == 0:
|
|
18
|
+
out.append((row, None, None))
|
|
19
|
+
continue
|
|
20
|
+
cand = j if right_times[j] <= t else max(0, j - 1)
|
|
21
|
+
dt = t - right_times[cand]
|
|
22
|
+
if 0 <= dt <= tolerance:
|
|
23
|
+
out.append((row, right_rows[cand], dt))
|
|
24
|
+
else:
|
|
25
|
+
out.append((row, None, None))
|
|
26
|
+
return out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def aligned_table(
|
|
30
|
+
frames: List[Sample],
|
|
31
|
+
serials: List[Sample],
|
|
32
|
+
boxes: List[Sample],
|
|
33
|
+
serial_tolerance_s: float = 0.02,
|
|
34
|
+
):
|
|
35
|
+
"""一帧一行。
|
|
36
|
+
|
|
37
|
+
MCU 采样用主机 host_mono 做 asof。有 device_tick 时会拟合 clock_ab 写入 meta,
|
|
38
|
+
只作诊断,**不参与**本版本对齐(截距会因 perf_counter 从开机起算而很大)。
|
|
39
|
+
YOLO 框按 extras['frame_id'] 贴到同一帧,不用 boxes.stamp(那是推理结束时刻)。
|
|
40
|
+
"""
|
|
41
|
+
box_by_fid = {}
|
|
42
|
+
for b in boxes:
|
|
43
|
+
fid = b.extras.get("frame_id")
|
|
44
|
+
box_by_fid[fid] = b
|
|
45
|
+
|
|
46
|
+
ticks = [s.stamp.device_tick for s in serials if s.stamp.device_tick is not None]
|
|
47
|
+
monos = [s.stamp.host_mono for s in serials if s.stamp.device_tick is not None]
|
|
48
|
+
clock_ab = fit_linear_clock(monos, ticks) if ticks else None
|
|
49
|
+
|
|
50
|
+
ft = [f.stamp.host_mono for f in frames]
|
|
51
|
+
st = [s.stamp.host_mono for s in serials]
|
|
52
|
+
joined = asof_join(ft, frames, st, serials, serial_tolerance_s)
|
|
53
|
+
|
|
54
|
+
rows = []
|
|
55
|
+
for frame, ser, dt in joined:
|
|
56
|
+
fid = frame.extras.get("frame_id", frame.stamp.seq)
|
|
57
|
+
box = box_by_fid.get(fid)
|
|
58
|
+
payload = None if ser is None else ser.payload
|
|
59
|
+
box_payload = None if box is None else box.payload
|
|
60
|
+
adc = None
|
|
61
|
+
if isinstance(payload, dict):
|
|
62
|
+
adc = payload.get("ADC", payload.get("adc"))
|
|
63
|
+
confs: List[float] = []
|
|
64
|
+
if box_payload:
|
|
65
|
+
for b in box_payload:
|
|
66
|
+
if isinstance(b, dict) and b.get("conf") is not None:
|
|
67
|
+
confs.append(float(b["conf"]))
|
|
68
|
+
row = {
|
|
69
|
+
"frame_id": fid,
|
|
70
|
+
"t_capture": frame.stamp.host_mono,
|
|
71
|
+
"t_wall": frame.stamp.host_wall,
|
|
72
|
+
"adc": adc,
|
|
73
|
+
"serial": payload,
|
|
74
|
+
"serial_lag_s": dt,
|
|
75
|
+
"boxes": box_payload,
|
|
76
|
+
"n_boxes": len(box_payload) if box_payload else 0,
|
|
77
|
+
"conf_max": max(confs) if confs else None,
|
|
78
|
+
"t_infer_end": None if box is None else box.stamp.host_mono,
|
|
79
|
+
"infer_lag_s": None
|
|
80
|
+
if box is None
|
|
81
|
+
else (box.stamp.host_mono - frame.stamp.host_mono),
|
|
82
|
+
}
|
|
83
|
+
if ser is not None and ser.stamp.device_tick is not None:
|
|
84
|
+
row["mcu_tick"] = ser.stamp.device_tick
|
|
85
|
+
rows.append(row)
|
|
86
|
+
|
|
87
|
+
meta = {"clock_ab": clock_ab, "n_frames": len(frames), "n_serial": len(serials), "n_boxes": len(boxes)}
|
|
88
|
+
return rows, meta
|