Metadata-Version: 2.4
Name: mtdaq
Version: 1.1.1
Summary: MangoTree DAQ Device SDK
Author-email: MangoTree <support@mangotree.cn>
License-Expression: MIT
Project-URL: Homepage, https://www.mangotree.cn/
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# MTDAQ Demo 设计思路与注意事项

## 环境

| 依赖 | 版本 |
|------|------|
| Python | 3.14.0 |
| numpy | 2.4.4 |
| PyQt5 | 5.15.11 |
| pyqtgraph | 0.14.0 |
| mtdaq | 1.1.0 |

## 1. 概述

本文档描述MangoTree设备数据采集面板的设计思路，旨在与以下材料配合使用，让生成式 AI 能自主生成不同 MangoTree 设备、不同工作模式的实时采集监控面板：

| 输入材料 | 作用 |
|----------|------|
| 本文档 | 设计思路、架构决策、适配方法、常见坑点 |
| X612/AIO.py | 已注释的参考实现 (MT-X612 / General AIO 模式) |
| X612/DI_Trigger_AI.py | 已注释的参考实现 (MT-X612 / DI-Trigger-AI 模式) |
| D814/AIO.py | 已注释的参考实现 (MT-D814 / General AIO 模式) |
| D814/AIO_Sync.py | 已注释的参考实现 (MT-D814 / AOSyncAI 同步模式) |
| D814/DIO.py | 已注释的参考实现 (MT-D814 / General DIO 模式) |
| X610/AIO.py | 已注释的参考实现 (MT-X610 / General AIO 模式) |
| mtdaq 库 | `Device` API、`Mode` 枚举、`config.txt` 配置模板 |
| 设备信息 | 来自 [MangoTree 官网](https://www.mangotree.cn/) Datasheet/网页的设备规格 (通道数/采样率/类型等) |

**技术栈**: Python 3, PyQt5, pyqtgraph, numpy, threading.

**现有 Demo 清单**:

| 目录 | 文件 | 设备 | 模式 | 功能 |
|------|------|------|------|------|
| X612/ | AIO.py | MT-X612 | General (0) | 6 AI + 2 AO + 采样率枚举下拉 |
| X612/ | DI_Trigger_AI.py | MT-X612 | DI-Trigger-AI (5) | 6 AI (触发) + 2 AO |
| D814/ | AIO.py | MT-D814 | General (0) | 16 AI + 4 AO + 独立输入采样率 |
| D814/ | AIO_Sync.py | MT-D814 | AOSyncAI (1) | 16 AI + 4 AO + 同步采样率 (125kHz max) |
| D814/ | DIO.py | MT-D814 | General (0) | 16 DI (指示灯) + 16 DO (翘板开关) |
| D814/ | AIO_Test.py | MT-D814 | General (0) | 16 AI + 4 AO 通道检测 + 分析报告 |
| X610/ | AIO.py | MT-X610 | General (0) | 2 AI + 2 AO + 48k/96k/102.4k/192k/204.8k 采样率 |

> **文件命名规范**：每个 demo 按其功能以大写驼峰命名——`AIO.py`（模拟输入/输出）、`DIO.py`（数字输入/输出）、`DI_Trigger_AI.py`（DI 触发 AI 采集）。历史聊天记录保留在 `生成MT-X612 AIO General模式Demo.md` 中供参考。

---

### 1.1 获取设备信息

生成新设备 demo 前，需要从官方渠道获取目标设备的完整规格参数。

#### 官网产品页

| 产品类别 | 官网链接 |
|----------|----------|
| MT-PXIe 模块 (主页面) | https://www.mangotree.cn/p/19659/ |
| MT-PXIe 模块 (列表页) | https://www.mangotree.cn/p/225128/ |
| APP-PXIe 模块产品 | https://www.mangotree.cn/p/1062267/ |
| APP-PXIe 控制器产品 | https://www.mangotree.cn/p/1062266/ |
| APP-PXIe 机箱产品 | https://www.mangotree.cn/p/1062265/ |
| PXI 机箱、控制器 | https://www.mangotree.cn/app-video/hot.html |
| PCB-RIO | https://www.mangotree.cn/product/659354/ |

#### Datasheet PDF 目录结构

所有设备 Datasheet PDF 存放在 `server.mangotree.cn:9900`，按设备类别分层组织：

```
数据手册/
├── AtomDAQ/              # 一体机 (D814, D815...)
├── AtomRIO/              # Atom RIO 系列
├── E系列采集卡/           # E 系列采集卡
├── PCB-RIO/              # PCB RIO 系列
├── PCIe DAQ模块/          # PCIe DAQ 模块
├── PXIeAll/
│   ├── PXIe-Chassis/     # PXIe 机箱
│   ├── PXIe-Controller/  # PXIe 控制器
│   └── PXIe-Module/
│       ├── DAQ/          # DAQ 模块 (X300~X776, 100+ 型号)
│       ├── RIO/          # FPGA 板卡 (X900~X971)
│       └── Switch/       # 开关模块
├── RobustRIO/            # Robust RIO 系列
├── 产品手册/              # 整体产品手册
└── 雷电模块/              # 雷电接口模块
```

**PDF URL 模式**:
```
# PXIe DAQ 模块
https://server.mangotree.cn:9900/WebFile/Downloads/数据手册/PXIeAll/PXIe-Module/DAQ/{型号}/MT-{型号}%20DataSheet.pdf

# PXIe RIO (FPGA) 模块
https://server.mangotree.cn:9900/WebFile/Downloads/数据手册/PXIeAll/PXIe-Module/RIO/{型号}/MT-{型号}%20DataSheet.pdf

# AtomDAQ 一体机
https://server.mangotree.cn:9900/WebFile/Downloads/数据手册/AtomDAQ/{型号}/MT-{型号}%20DataSheet.pdf
```

**已知 Datasheet 链接**:

| 型号 | 类别 | 路径 |
|------|------|------|
| MT-D814 | AtomDAQ 一体机 | `AtomDAQ/D814/MT-D814%20DataSheet.pdf` |
| MT-D815 | AtomDAQ 一体机 | `AtomDAQ/D815/MT-D815%20DataSheet.pdf` |
| MT-X590 | 32bit 高精度 8AI | `PXIeAll/PXIe-Module/DAQ/X590/` |
| MT-X591 | 32bit 高精度 6AI+2AO | `PXIeAll/PXIe-Module/DAQ/X591/` |
| MT-X596 | 32bit 高精度 34AI+4AO | `PXIeAll/PXIe-Module/DAQ/X596/` |
| MT-X606 | 热电偶采集 | `PXIeAll/PXIe-Module/DAQ/X606/` |
| MT-X610 | 声音振动 2AI+2AO | `PXIeAll/PXIe-Module/DAQ/X610/` |
| MT-X612 | 声音振动 6AI+2AO | `PXIeAll/PXIe-Module/DAQ/X612/` |
| MT-X613 | 声音振动 4AI+IEPE | `PXIeAll/PXIe-Module/DAQ/X613/` |
| MT-X614 | 声音振动 | `PXIeAll/PXIe-Module/DAQ/X614/` |
| MT-X900 | FPGA 纯数字 DIO 128ch | `PXIeAll/PXIe-Module/RIO/X900/` |
| MT-X920 | FPGA DIO+AI/AO 混合 | `PXIeAll/PXIe-Module/RIO/X920/` |

> **提示**: `server.mangotree.cn:9900` 的目录页可直接浏览, 点击进入子目录即可查看所有型号列表。

#### PDF 文本提取方法

Datasheet PDF 可通过 `pdftotext` 或 Python (`PyPDF2`/`pdfplumber`) 提取文本：

```bash
# 方法1: pdftotext (poppler-utils)
pdftotext -layout MT-X610_DataSheet.pdf output.txt

# 方法2: Python pdfplumber
python -c "import pdfplumber; pdf=pdfplumber.open('MT-X610_DataSheet.pdf'); [print(p.extract_text()) for p in pdf.pages]"
```

提取后重点查看: 首页标题行 (通道数/分辨率/采样率)、AI/AO 章节的 "Support Sample rate" 行、Idle Channel Noise 表。

#### 第三方信息源

当官网 PDF 无法直接获取时，以下渠道可获取设备规格：

| 来源 | 说明 |
|------|------|
| [elecfans.com](https://www.elecfans.com) | 电子发烧友，有 MangoTree 32bit 精度实测系列文章 |
| [eepw.com.cn](https://www.eepw.com.cn) | 电子产品世界，有 MangoTree 新品发布和技术文章 |
| [red-hb.com](http://www.red-hb.com) | MangoTree 代理商/合作伙伴，部分产品页有详细规格表 |
| Web Search | 搜索 `MT-X{型号} PXIe 规格 采样率` 可找到分散在多个平台的产品信息 |

#### 适配 demo 需要提取的关键规格

从 Datasheet/产品页中提取以下信息用于 `SAMPLE_RATES`、`DEFAULT_CONFIG`、`info_group` 和 `setup_ui` 的填写：

| 参数 | 用途 |
|------|------|
| AI 通道数 | `DEFAULT_CONFIG["ai_channels"]`、动态曲线数量 |
| AO 通道数 | `DEFAULT_CONFIG["ao_channels"]`、AO 控件数量 |
| 支持的采样率 | `SAMPLE_RATES` 字典 |
| 电压范围 | `plot_widget.setYRange()`、`QDoubleSpinBox.setRange()` |
| 分辨率 (bit) | `info_group` 设备信息文本 |
| 耦合方式 | `info_group` 设备信息文本 |
| 总线类型 (PXIe/USB) | `info_group` 设备信息文本 |
| 特殊功能 (IEPE/触发等) | `info_group` 设备信息文本、Config 附加段 |

---

## 2. 架构设计

### 2.1 三层结构

```
DAQManager (设备管理层)
  ├── _ai_loop 线程: 循环 dev.analogRead() → 共享缓冲区
  ├── _ao_loop 线程: 循环生成波形 → dev.analogWrite()
  └── _data_version: 单调递增, 标记新数据

MainWindow (UI 层)
  ├── QTimer(33ms) → update_ui(): 版本号比对, 拷贝新数据
  ├── _render_latest(): 更新 pyqtgraph 曲线
  └── 暂停: 快照 pausing_data, 切换可见性只操作快照

UI 布局 (QSplitter 左右分栏)
  ├── 左侧: 设备信息/配置/启停/暂停/AO控制
  └── 右侧: pyqtgraph 波形图 + 右上角半透明图例浮层
```

### 2.2 数据流 (版本号驱动刷新)

```
_ai_loop 线程:
  analogRead(samples) 阻塞等待
    → reshape 为 (samples, channels)
    → lock(_data_lock):
        _latest_data = arr.copy()
        _data_version += 1

UI 定时器 (33ms 轮询):
  if paused: return
  if _data_version == _last_data_version: return  // 无新数据, 跳过
  _last_data_version = version
  lock(_data_lock): pending_data = _latest_data.copy()
  _render_latest()
```

**为什么不用 Queue**: `analogRead` 在 General 模式下每次阻塞约 1 秒才返回数据，Queue 会让定时器每 33ms 空转检查、暂停时积压数据。版本号方案只在有新数据时才做一次拷贝+渲染。

### 2.3 暂停/继续机制

```
暂停: _paused_data = pending_data.copy()   // 快照
      update_ui 中 if paused: return        // 不接触新数据
      _render_latest 中 从 _paused_data 读取 // 复选框操作快照

继续: _last_data_version = -1              // 强制下次刷新
      _paused_data = empty
      _render_latest()                      // 立即渲染最新数据
```

### 2.4 DIO 模式的架构差异

DIO demo 不含 AI/AO，架构更简洁：

```
DAQManager
  ├── _di_loop 线程: digitalRead() → uint64 写入共享缓冲区 + 版本号
  └── digitalWrite(dodata): 由 UI 线程通过信号直接调用

MainWindow (UI 层)
  ├── QTimer(33ms) → update_ui(): 版本号比对, 逐 bit 更新 DI 指示灯颜色
  ├── DO 翘板开关: QPushButton(checkable) → on_do_toggled() → digitalWrite()
  └── DI 指示灯: QFrame 圆形, 绿色=1, 灰色=0
```

**关键 API 差异**:
- `digitalRead()` 返回 `ctypes.c_uint64`，通过 `.value` 获取 int，每 bit 对应一个通道
- `digitalWrite(dodata)` 写入 int，每 bit 对应一个通道，低位先
- DI 轮询间隔：`stop_event.wait(0.03)` 控制 ~33Hz 读取频率
- 无需 Queue 或版本号：DI 数据只有 64bit，直接状态比对

---

## 3. Config 配置系统

### 3.1 配置模板来源

配置模板定义在 mtdaq 库源码目录下的 `config.txt` 文件中 (与 `mtdaq.py` 同级)。

所有 MTDAQ 设备通过一个 XML-like 字符串进行配置。关键段：

```xml
<Device>        <!-- 设备标识: 型号/IP/ID/Slot (ID和Slot可用*) -->
<Advance>       <!-- Reset-Device=Auto/Force; Clock-From=Onboard/MT-Chassis -->
<DAQMode>       <!-- 工作模式: 0~7 (见 Mode 枚举), Path 仅 Mode=7 有效 -->
<AI>            <!-- AI-Channel 通道列表; AI-SampleRate -->
<AO>            <!-- AO-Channel 通道列表; AO-SampleRate -->
<DITrigger>     <!-- 仅 Mode=4/5/6: DITrigger-Channel/SampleClock/Direction/AIOSamplePerTrigger -->
<TC>            <!-- 热电偶: TC-Channel/SampleRate/Type/CJC-Type -->
<RTD>           <!-- 热电阻: RTD-Channel/4wire或3wire -->
<DI>/<DO>       <!-- 低速数字IO -->
<DigitalWaveformInput>  <!-- 高速数字波形输入 -->
<DigitalWaveformOutput> <!-- 高速数字波形输出 -->
<Counter>       <!-- 计数器 -->
<PWM>           <!-- 脉宽调制 -->
<Encoder>       <!-- 编码器 -->
```

### 3.2 工作模式 (Mode 枚举)

| DAQMode |     枚举值       | 含义                | 需要的额外配置段 |
|---------|------------------|---------------------|------------------|
|    0    | General          | AI/AO 独立运行      | —                |
|    1    | AOSyncAI         | AO 同步 AI          | —                |
|    2    | AISyncEncoder    | AI 同步 Encoder     | `<Encoder>`      |
|    3    | EncoderSyncAI    | Encoder 同步 AI     | `<Encoder>`      |
|    4    | DITrigerAISyncAO | DI 触发, AI/AO 同步 | `<DITrigger>`    |
|    5    | DITrigerAI       | DI 触发 AI, AO 独立 | `<DITrigger>`    |
|    6    | DITrigerAO       | DI 触发 AO          | `<DITrigger>`    |

### 3.3 Config 字符串生成模式

各 demo 中 `build_config()` 用 Python f-string 模板生成 Config。生成新 demo 时：

- **修改 `Device-Model`** 为目标设备型号 (如 X614, X550, X920 等)
- **修改 `DAQMode`** 为目标工作模式
- **根据模式添加/删除配置段**: 如触发模式需添加 `<DITrigger>` 段
- **通道号和采样率** 用参数字典占位符 `{cfg["key"]}` 动态替换

### 3.4 通道范围语法

Config 中通道字段支持 **范围写法**，用尖括号 `<m-n>` 表示从 m 到 n（含两端）的所有连续通道号：

| 写法 | 等价展开 |
|------|---------|
| `<0-5>` | `0,1,2,3,4,5` |
| `<0-2>,4` | `0,1,2,4` |
| `<0-7>,<12-15>` | `0,1,2,3,4,5,6,7,12,13,14,15` |

范围与单独通道可混用，仍以逗号 `,` 分隔。Demo 中通道数较多时（如 D814 的 16 通道），应优先使用范围写法以提高可读性：

```python
"di_channels": "<0-15>"   # ✅ 清晰
"di_channels": "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15"  # ❌ 冗长
```

---

## 4. DAQManager — 设备管理层的设计

### 4.1 职责边界

DAQManager 只负责设备和数据，**不持有任何 UI 引用**。它通过以下接口与 UI 层交互：

| 方向 | 接口 |
|------|------|
| UI → Manager | `start()` / `stop()` / `update_ao_param()` / `set_chunk_samples()` |
| Manager → UI | `running` / `error` / `num_channels` / `num_ao_channels` / `_data_version` / `_latest_data` |

### 4.2 AI 采集线程

```python
def _ai_loop(self):
    while not self.stop_event.is_set():
        data_raw = self.dev.analogRead(samples=samples)
        arr = np.ctypeslib.as_array(data_raw)      # ctypes → numpy
        arr = arr.reshape(samples, self.num_channels)
        with self._data_lock:
            self._latest_data = arr.copy()
            self._data_version += 1
```

**关键点**：
- `analogRead` 返回 ctypes float 数组，数据交错存储: `[ch0_s0, ch1_s0, ch0_s1, ch1_s1, ...]`
- `reshape(samples, channels)` 将其转为便于按列索引的二维数组
- `.copy()` 必须调用，因为下一次 `analogRead` 可能覆写 ctypes 缓冲区
- `_data_version` 递增告知 UI 有新数据

### 4.3 AO 输出线程

```python
def _ao_loop(self):
    t_base = 0.0  # 全局时间基准, 保证相位连续
    while not self.stop_event.is_set():
        t = np.linspace(t_base, t_base + CHUNK_SECONDS, samples, endpoint=False)
        t_base += CHUNK_SECONDS
        for each channel:
            wave = amp * sin(2π * freq * t)
            full_ao_data[ch_idx::num_channels] = wave  # 交错写入
        self.dev.analogWrite(full_ao_data.tolist())
```

**关键点**：
- 输出格式与输入相同：交错存储 `[ch0_s0, ch1_s0, ...]`
- `t_base` 必须跨循环累积，否则每次输出开头会有相位跳变
- 通过 `ao_lock` 保护 `ao_params`，UI 线程可安全修改参数

### 4.4 通道数查询

```python
self.num_channels = self.dev._get_cached_channel_num("AI")
```

支持查询的通道类型: `"AI"`, `"AO"`, `"HSDI"`, `"HSDO"`, `"CI"`, `"CO"`, `"Encoder"`, `"TC"`。

### 4.5 低速数字 I/O (LDI/LDO)

```python
# 读取 DI: 返回 ct.c_uint64, 每 bit 对应一个通道 (低位先)
ldi = self.dev.digitalRead()
di_value = ldi.value  # int

# 写入 DO: 传入 int, 每 bit 对应一个通道 (低位先)
self.dev.digitalWrite(do_value)
```

**DI 轮询线程**：使用 `stop_event.wait(0.03)` 替代 `time.sleep`，可以被 `stop_event.set()` 立即中断退出。

```python
def _di_loop(self):
    while not self.stop_event.is_set():
        raw = self.dev.digitalRead()
        with self._di_lock:
            self._latest_di = raw.value
            self._di_version += 1
        self.stop_event.wait(0.03)  # ~33Hz 轮询
```

**DO 写入**：直接由 UI 线程调用，无需后台线程。通过 `QPushButton(checkable)` 的 `clicked` 信号触发，更新内部 `_do_state` 位掩码后调用 `digitalWrite`。

---

## 5. 生成不同设备/模式 Demo 的适配清单

### 5.1 必须修改的位置

| 位置 | 修改内容 | 示例 |
|------|---------|------|
| `SAMPLE_RATES` | 设备支持的采样率 | X550: `{"48kHz":48000,"96kHz":96000,"200kHz":200000}` |
| `DEFAULT_CONFIG` | 默认 IP/通道/采样率 | 改为目标设备的默认值 |
| `build_config()` | 设备型号和 DAQMode | `Device-Model=X550;` + `DAQMode=5;` |
| `setup_ui()` 中 `info_group` | 设备信息文本 | 型号/模式说明/通道规格 |
| `__init__` 中的 `setWindowTitle` | 窗口标题 | `"MT-X550 DI-Trigger-AI"` |

### 5.2 按需修改的位置

| 场景 | 修改内容 |
|------|---------|
| 触发模式 (Mode=4/5/6) | `build_config()` 添加 `<DITrigger>` 段；`_ai_loop` 中 `analogRead` 行为变为阻塞等待触发 |
| 无 AO 的设备 | DAQManager 中 `num_ao_channels` 为 0 时自动隐藏 AO 控件，无需额外处理 |
| 通道数不同的设备 | 无需修改代码，`setup_curves()` 根据实际通道数动态创建 |
| 其他 I/O 类型 (计数器/编码器/DIO) | 1) `build_config()` 添加对应段 2) DAQManager 新增采集线程 3) UI 新增控制面板 |
| DIO 专用 (DI+DO) | 1) `build_config()` 不需要 `<AI>/<AO>` 2) 参考 `D814/DIO.py` 的 DI 轮询 + DO 翘板开关模式 3) UI 用 QGridLayout 排列指示灯和开关 |
| 不同电压范围 | 修改 `plot_widget.setYRange()` 和 `QDoubleSpinBox.setRange()` |
| 采样率输入方式 | X61 系列 (声音振动): 采样率为固定枚举值, 用 **QComboBox 下拉框**; 其余设备: 采样率可自由输入, 用 **QLineEdit 输入框**. 不要使用预设按钮 |

### 5.3 触发模式注意事项

- Mode=5 (DITrigerAI): `analogRead` 阻塞等待 DI 触发，触发后一次性返回指定采样点
- `<DITrigger>` 段字段名严格遵循 config.txt:
  - `DITrigger-Channel=0;`
  - `DITrigger-SampleClock=500000Hz;` (触发检测时钟，最大 1MHz)
  - `DITrigger-Direction=0;` (0=RisingEdge, 1=FallingEdge, 2=EitherEdge)
  - `DITrigger-AIOSamplePerTrigger=-1;` (-1=连续采样, N=每次触发采N点)
- 触发模式下 `_data_version` 更新频率取决于触发频率，定时器需保持轮询

---

## 6. PyQt5 UI 设计要点

### 6.1 布局结构

- `QSplitter(Qt.Horizontal)` 左右分栏，stretch 比例 1:4
- 左侧控制面板用 `QVBoxLayout` 垂直排列各控件组
- 右侧图表用 `QVBoxLayout` 填满 pyqtgraph PlotWidget

### 6.2 图例浮层

图例面板是 `QFrame`，parent 设为 plot_container 而非 plot_widget。原因：PlotWidget 内部的 ViewBox 有坐标变换，子控件的绝对定位会受影响。使用外层容器配合 `move(x, y)` 精确定位。

```python
self.legend_panel = QFrame(plot_container)  # 非 plot_widget
self.legend_panel.move(x, y)                # 绝对定位到右上角
```

编辑框宽度自适应：通过 `QFontMetrics.horizontalAdvance()` 计算所有编辑框中最长文本的像素宽度，统一设置；文字变化时通过 `textChanged` 信号自动重新同步。

### 6.3 启动/停止时的 UI 状态管理

| 状态 | 配置控件 | 启动按钮 | 停止按钮 | 暂停按钮 | 定时器 | 曲线 |
|------|---------|---------|---------|---------|-------|------|
| 初始 | enabled | enabled | disabled | disabled | 停止 | 无 |
| 运行中 | disabled | disabled | enabled | enabled | 运行 | 动态创建 |
| 已停止 | enabled | enabled | disabled | disabled | 停止 | 已清除 |

**暂停按钮**仅在运行中可用。文本在"暂停"/"继续"之间切换。

### 6.4 AO 控件闭包

```python
def make_update(idx, a=amp_spin, f=freq_spin):
    def update():
        if self.manager:
            self.manager.update_ao_param(idx, a.value(), f.value())
    return update

update_fn = make_update(i)
amp_spin.editingFinished.connect(update_fn)
```

**为什么用闭包而不是 lambda**: Python 的 lambda 在循环中会产生延迟求值问题——所有 lambda 会捕获同一个变量 `i`，最终都是循环终止时的值。闭包通过默认参数 `a=amp_spin, f=freq_spin` 在定义时绑定当前值。

### 6.5 DIO 控件设计

**DI 指示灯**: `QFrame` 固定尺寸 (14×14px)，圆角样式 (`border-radius: 6px`)。绿色 (#00ff00) 表示高电平，深灰 (#333333) 表示低电平。

**DO 翘板开关**: `QPushButton(checkable=True)` 配合样式表模拟翘板开关。`checked` 伪状态切换背景色：未选中深灰 (#444)，选中绿色 (#228B22)。

```python
btn = QPushButton()
btn.setCheckable(True)
btn.setFixedSize(48, 24)
btn.setStyleSheet("""
    QPushButton { background-color: #444; border: 1px solid #777; border-radius: 10px; }
    QPushButton:checked { background-color: #228B22; border: 1px solid #00ff00; }
""")
# 闭包捕获通道索引, 避免循环变量延迟求值
btn.clicked.connect(lambda checked, c=ch: self.on_do_toggled(c))
```

**位掩码管理**: DO 状态用一个 `_do_state` int 表示，`on_do_toggled(ch)` 通过 `bit = 1 << ch` 翻转对应位后调用 `digitalWrite(_do_state)`。

**布局**: DI/DO 各 16 通道用 `QGridLayout` 排列，每行 8 列（每个通道占 2 列：控件+标签）。

---

## 7. 关键约束与注意事项

### 7.1 线程安全

| 资源 | 保护方式 |
|------|---------|
| `_latest_data` + `_data_version` | `threading.Lock()` (_data_lock) |
| `ao_params` | `threading.Lock()` (ao_lock) |
| Qt 控件 | 只能主线程访问 (通过信号/槽和 QTimer) |

**禁止在后台线程直接操作 Qt 控件**。后台线程只更新 DAQManager 的数据字段，UI 线程通过 QTimer 回调读取。

### 7.2 设备生命周期

- `mtdaq.Device(config_str)` — 创建但不启动
- `dev.start()` — 调用 DLL `MTDAQ_Start`，获取设备引用
- `dev.close()` — 调用 DLL `MTDAQ_Close`，释放设备
- 必须在 `start()` 后调用 `_get_cached_channel_num()` 才能获取通道数
- `close()` 在 `stop()` 中调用，`stop()` 需等待线程退出后再 close

### 7.3 资源清理

- `closeEvent` 中必须调用 `stop_device()`
- `stop_device()` 的清理顺序: `manager.stop()` → `timer.stop()` → `clear_curves()`
- `clear_curves()` 中: 移除 pyqtgraph items → clear 列表 → 递归清空布局 → 调用 `deleteLater()`
- `_clear_layout` 是递归的: 子布局也需要递归清空

### 7.4 numpy 内存

- ctypes 数组通过 `np.ctypeslib.as_array()` 创建的是视图 (view)，需 `.copy()` 确保独立所有权
- `pending_data` 和 `_latest_data` 是独立的 numpy 数组，不会互扰

### 7.5 采样率与采样点数的联动

- 切换采样率时 (QComboBox currentTextChanged) → 同步更新 `chunk_spin` 的值
- 修改采样点数时 (QSpinBox valueChanged) → 运行时通过 `set_chunk_samples()` 同步到 DAQManager
- `blockSignals(True/False)` 防止修改 spinbox 值时再次触发 valueChanged

### 7.6 设备 IP 和 ID/Slot 通配符

- `Device-ID=*` 和 `Device-Slot=*` 使用通配符匹配任意设备
- 只有在多设备环境需要区分时才需指定具体 ID/Slot

### 7.7 DIO 注意事项

- `digitalRead()` 返回 `ctypes.c_uint64`，**不是 int**，需通过 `.value` 获取 Python int
- DI 轮询用 `stop_event.wait(0.03)` 而非 `time.sleep`，可立即响应停止信号
- DO 的 `digitalWrite()` 无需后台线程，UI 线程直接调用是安全的
- 位掩码翻转: `_do_state ^= (1 << ch)` 或 `if _do_state & bit: clear else: set`

---

## 8. 文件结构速查

### 8.1 X612/AIO.py (General AIO)

```
行 1-37     文件头注释 (架构概述 / Config 结构 / 适配方法)
行 39-48    import
行 50-60    可配置常量 (SAMPLE_RATES, DEFAULT_CONFIG)
行 70-101   build_config() — Config 字符串生成
行 110-260  class DAQManager — 设备管理层
行 270-890  class MainWindow — UI 层
            行 304-326   配置相关
            行 328-342   UI 控件回调
            行 344-411   启停/暂停
            行 413-427   UI 辅助
            行 429-588   曲线与图例管理
            行 590-768   UI 构建
            行 770-810   图例面板辅助
            行 812-882   数据刷新与渲染
行 890-897  入口
```

### 8.2 D814/DIO.py (General DIO)

```
行 1-37     文件头注释 (架构概述 / 设备规格)
行 39-47    import
行 50-58    可配置常量 (DI_COUNT, DO_COUNT, DEFAULT_CONFIG)
行 60-85    build_config() — 仅 <DI> + <DO> 段
行 88-135   class DAQManager — DI 轮询 + DO 直写
行 138-310  class MainWindow — UI 层
            行 138-160   __init__ / get_config / set_config_fields_enabled
            行 162-185   启停 (start_device, stop_device)
            行 187-194   on_do_toggled (位掩码翻转)
            行 196-215   update_ui + _render_di (指示灯刷新)
            行 217-240   控件工厂 (_make_di_indicator, _make_do_toggle)
            行 242-305   setup_ui (QGridLayout DI+DO)
            行 307-310   closeEvent
行 313-318  入口
```

### 8.3 D814/AIO.py (General AIO, 自由采样率)

```
行 1-53     文件头注释 (架构概述 / 设备规格 / Config 结构 / 适配方法)
行 55-62    import
行 64-70    可配置常量 (DEFAULT_AI_SR, DEFAULT_AO_SR, DEFAULT_CONFIG)
行 72-95    build_config() + _parse_sr()
行 97-215   class DAQManager — AI 循环 + AO 循环, 版本号驱动
行 217-713  class MainWindow — UI 层
            行 220-240   __init__ / get_config / set_config_fields_enabled
            行 242-260   启停/暂停 (start_device, stop_device, toggle_pause)
            行 262-275   曲线清理 (clear_curves, _clear_layout)
            行 277-328   setup_curves — 动态曲线 + 图例浮层
            行 330-370   setup_ao_controls — AO 幅值/频率控件
            行 372-660   setup_ui — 完整 UI 构建 (左右分栏)
            行 662-680   图例自适应 (_sync_edit_widths, _update_legend_pos)
            行 682-710   数据刷新与渲染 (update_ui, _render_latest)
```
### 8.4 D814/AIO_Sync.py (AOSyncAI, 同步采样率)

```
行 1-28     文件头注释 (设备规格 / 同步模式说明 / 架构概述)
行 30-38    import
行 42-49    可配置常量 (DEFAULT_SYNC_SR, DEFAULT_CONFIG)
行 53-76    build_config() — DAQMode=1, AI/AO 共用 sync_sample_rate
行 78-84    _parse_sr() — 采样率字符串解析
行 88-185   class DAQManager — _sync_sr 统一驱动 AI 和 AO
            行 96-98    __init__ 接收 sync_sr 而非 ai_sr, ao_sr
            行 141-157  _ai_loop: analogRead 采集
            行 159-179  _ao_loop: 使用 _sync_sr 计算 chunk 时长
行 189-440  class MainWindow — UI 层
            行 209-219   get_config / set_config_fields_enabled
            行 223-233   on_sync_sr_changed — 同步采样点数默认值
            行 237-265   启停/暂停
            行 278-326   setup_curves — 动态曲线 + 图例浮层
            行 328-368   setup_ao_controls — AO 幅值/频率控件
            行 372-530   setup_ui — QLineEdit 输入同步采样率 (无预设按钮)
            行 547-566   数据刷新与渲染 — 使用 _sync_sr 计算 X 轴时间
```

**与 D814/AIO.py (General) 的关键差异**:
- `DAQMode=1` 替代 `DAQMode=0`
- 单个 `sync_sample_rate` (QLineEdit) 替代独立的 `ai_sample_rate` / `ao_sample_rate`
- `DAQManager.__init__` 接收 `sync_sr` 而非 `ai_sr, ao_sr`
- `_ao_loop` 使用 `_sync_sr` 计算 chunk 时长
- `_render_latest` 使用 `_sync_sr` 计算 X 轴时间
- 硬件层面 AO 生成主时钟, AI 相位锁定跟随; Python API 调用方式不变

### 8.5 X612/DI_Trigger_AI.py (DI-Trigger-AI)

```
行 1-47     CONFIG 静态配置 (DITrigger 段含触发通道/时钟/方向/每次采样数)
行 49-51    常量 (SAMPLE_RATE, TRIGGER_SAMPLES, PLOT_SECONDS)
行 54-70    class DAQManager — Queue 驱动模式, trigger_count 计数器
            行 62-70    _ai_loop: analogRead 阻塞等待 DI 上升沿触发
行 73-405   class MainWindow — UI 层
            行 73-100   __init__ / 数据初始化
            行 102-180  setup_ui — 设备信息/配置/启停/参数控件
            行 182-395  setup_curves_dynamic — 启动后动态创建图表+图例+AO
            行 397-405  _render_checked / closeEvent
```

---

## 9. 配置模板速查 (config.txt 节选)

以下字段名必须严格匹配，大小写敏感：

```
<DITrigger>
DITrigger-Channel=0;
DITrigger-SampleClock=500000Hz;    // 最大 1MHz
DITrigger-Direction=0;             // 0:Rising 1:Falling 2:Either
DITrigger-AIOSamplePerTrigger=-1;  // -1:连续 N:每次触发采N点
</DITrigger>

<Counter>
Counter-Channel=0;
Counter-Direction=0;        // 0:RisingEdge 1:FallingEdge
Counter-SampleRate=1000Hz;
Counter-PusleSamples=10;
</Counter>

<PWM>
PWM-Channel=0;
PWM-Frequency=1000Hz;
PWM-DutyCycle=50%;
</PWM>

<Encoder>
Encoder-Channel=0;
Encoder-SampleRate=1000Hz;
Encoder-Resolution=2000P/R;  // 每圈脉冲数
</Encoder>

<TC>
TC-Channel=0;
TC-SampleRate=20Hz;
TC-Type=K;             // B/E/J/K/N/R/S/T
TC-CJC-Type=0;         // 0:Internal 1:External
</TC>
```
