# PynectDirect Python 交互式光谱例程使用说明

> 适用文件：`python_ctypes_example.py`  
> 运行环境：64 位 Windows、64 位 Python 3  
> 示例用途：读取设备信息并完成一次指定波长范围的光谱测量

## 1. 示例功能

`python_ctypes_example.py` 是一个可以直接运行的交互式 Python 示例。程序会按照提示完成以下操作：

1. 搜索并打开 PynectDirect 光谱仪。
2. 读取设备序列号。
3. 读取并显示设备完整波长范围。
4. 读取当前积分时间和平均次数。
5. 让用户输入本次测量使用的积分时间和平均次数。
6. 让用户输入准备读取的起始波长和结束波长。
7. 显示设备实际选择的波长范围。
8. 读取所选范围内的全部波长值和对应的强度值。
9. 说明实际读取的总点数，并等间隔打印最多 10 组代表数据。
10. 恢复运行前的积分时间和平均次数，并关闭设备。

本例程设置的是本次运行期间使用的临时测量参数，不会写入设备的默认固化配置。

## 2. 运行环境和文件布局

### 2.1 Python 要求

必须使用 64 位 Python。可以执行以下命令检查：

```powershell
python -c "import struct; print(struct.calcsize('P') * 8)"
```

正确输出为：

```text
64
```

本例程只使用 Python 标准库，不需要执行 `pip install`。

### 2.2 推荐的交付包布局

在完整 SDK 交付包中，文件布局如下：

```text
PynectDirect_SDK/
|-- bin/
|   |-- PynectDirect.dll
|   `-- ftd2xx.dll
`-- examples/
    |-- python_ctypes_example.py
    `-- python_ctypes_example_guide.md
```

`PynectDirect.dll` 和 `ftd2xx.dll` 必须放在同一个目录中。程序可以从交付包的 `bin` 目录自动找到它们。

### 2.3 单独复制例程时的布局

如果只复制 Python 例程，建议使用同级 `dll` 目录：

```text
my_spectrum_demo/
|-- python_ctypes_example.py
|-- python_ctypes_example_guide.md
`-- dll/
    |-- PynectDirect.dll
    `-- ftd2xx.dll
```

程序也支持把两个 DLL 直接放在 `python_ctypes_example.py` 同级目录。

## 3. 运行例程

打开 PowerShell，进入 SDK 交付包目录后执行：

```powershell
python .\examples\python_ctypes_example.py
```

如果已经进入 `examples` 目录，也可以执行：

```powershell
python .\python_ctypes_example.py
```

程序会扫描设备。只连接一台设备时自动使用设备索引 `0`；连接多台设备时，会先提示选择设备索引。

## 4. 逐项输入说明

程序会先读取设备当前状态，再提示输入本次测量参数。方括号中的数值是默认值，直接按 Enter 即采用该值。

| 提示项目 | 输入类型 | 单位或范围 | 直接按 Enter | 作用 |
| --- | --- | --- | --- | --- |
| 设备索引 | 整数 | 扫描结果中的 `0` 至最大索引 | 使用 `0` | 选择要打开的设备；仅多设备时出现 |
| 积分时间 | 正整数 | 微秒（us），不超过设备允许范围 | 保留当前积分时间 | 设置本次测量的临时积分时间 |
| 平均次数 | 整数 | `1-255` 次 | 保留当前平均次数 | 设置本次测量的临时平均次数 |
| 起始波长 | 数字 | nm，位于完整波长范围内 | 使用完整范围起点 | 选择本次读取范围的起点 |
| 结束波长 | 数字 | nm，位于完整波长范围内 | 使用完整范围终点 | 选择本次读取范围的终点 |

起始波长必须小于结束波长。输入文字、超出范围的数值或无效组合时，程序会说明问题并重新提示，不需要重新启动。

### 4.1 输入提示示例

假设设备当前积分时间为 `50000 us`，平均次数为 `1`，完整波长范围为 `350-850 nm`：

```text
Device serial number: PYN-TEST-001
Full wavelength range: 350.000-850.000 nm
Current integration time: 50000 us
Current average count: 1
Integration time in us [50000]: 100000
Average count [1]: 3
Start wavelength in nm [350.000]: 400
End wavelength in nm [850.000]: 600
```

以上输入表示：

- 本次测量使用 `100000 us` 的积分时间。
- 每个输出光谱点使用 `3` 次平均。
- 只读取请求范围 `400-600 nm` 内的光谱数据。

### 4.2 全部采用默认值

如果希望保留当前积分时间和平均次数，并读取完整波长范围，在四个参数提示处直接按 Enter 即可。

## 5. 输出内容说明

### 5.1 测量摘要

采集完成后，程序首先输出：

| 输出项目 | 含义 |
| --- | --- |
| `Device serial number` | 设备序列号 |
| `Full wavelength range` | 设备支持的完整波长范围 |
| `Integration time` | 本次测量实际使用的积分时间，单位 us |
| `Average count` | 本次测量实际使用的平均次数 |
| `Requested wavelength range` | 用户输入的起始波长和结束波长 |
| `Actual wavelength range` | 设备按实际像素位置匹配后的范围 |
| `Actual wavelength points read` | 所选范围内实际读取的数据点总数 |

请求范围和实际范围可能存在很小差异。这是因为设备只能选择实际存在的像素和标定波长点。处理数据时应以 `Actual wavelength range` 和实际返回数组为准。

### 5.2 波长值和强度值

例程会读取所选范围内的全部光谱点，但为了让控制台输出便于查看，只打印最多 10 组代表数据。实际点数不少于 10 时，程序按原始索引等间隔选取 10 组，并保留首点和尾点；实际点数少于 10 时，程序打印全部数据。

例如，某次测量实际读取 810 个点时，输出开头如下：

```text
Actual wavelength points read: 810
Showing 10 evenly spaced points:
Index   Wavelength (nm)   Intensity
0       800.090000        1200
90      811.197000        1380
...
809     899.911000        1760
```

三列含义如下：

- `Index`：所选范围内的数据序号，从 `0` 开始。
- `Wavelength (nm)`：该点的波长值，单位 nm。
- `Intensity`：相同索引位置对应的原始光谱强度值，范围为 `0-65535`。

波长值与强度值按索引一一对应。例如，索引 `1` 的 `500.000000 nm` 对应强度值 `2300`。不要对两个数组分别排序，否则会破坏这种对应关系。

控制台只省略显示，不会丢弃测量数据。`run_interactive_measurement()` 返回的 `result.wavelengths` 和 `result.intensities` 仍包含本次实际读取的全部数据；上例中两个列表都包含 810 个元素。

## 6. 在自己的程序中复用

### 6.1 读取完整光谱

原有 `read_full_spectrum(device_index=0)` 函数继续保留。它不进行交互输入，返回两个普通 Python 列表：

```python
from python_ctypes_example import read_full_spectrum

wavelengths, intensities = read_full_spectrum(device_index=0)
print("Point count:", len(wavelengths))
print("First pair:", wavelengths[0], intensities[0])
```

### 6.2 执行交互式选择波段测量

`run_interactive_measurement()` 会显示逐项提示，并返回 `SelectedSpectrumResult`：

```python
from python_ctypes_example import run_interactive_measurement

result = run_interactive_measurement()
for wavelength, intensity in zip(
    result.wavelengths, result.intensities
):
    print(wavelength, intensity)
```

结果对象的主要字段如下：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `serial_number` | `str` | 设备序列号 |
| `full_start_nm`、`full_end_nm` | `float` | 完整波长范围 |
| `integration_us` | `int` | 本次积分时间，单位 us |
| `average_count` | `int` | 本次平均次数 |
| `request_start_nm`、`request_end_nm` | `float` | 用户请求范围 |
| `actual_start_nm`、`actual_end_nm` | `float` | 设备实际匹配范围 |
| `start_pixel`、`end_pixel` | `int` | 实际起止像素 |
| `wavelengths` | `list` | 所选范围的波长列表 |
| `intensities` | `list` | 与波长列表对应的强度列表 |

正常返回时，`len(result.wavelengths)` 与 `len(result.intensities)` 始终相同。

## 7. 参数恢复和设备关闭

程序会在修改前保存原积分时间和平均次数。完成采集或遇到采集错误时，程序都会尝试：

1. 恢复原平均次数。
2. 恢复原积分时间。
3. 关闭设备。

如果恢复失败，程序会输出以 `WARNING:` 开头的提示。此时不要继续测量，应关闭其他可能使用设备的软件，重新打开设备并检查参数。

程序正常结束后，选择波长范围不会写入设备的长期配置。下次运行仍会重新读取完整范围并提示输入。

## 8. 常见错误

### 8.1 找不到 PynectDirect.dll

可能看到：

```text
ERROR: PynectDirect.dll not found
```

检查：

- 文件名是否严格为 `PynectDirect.dll`。
- `PynectDirect.dll` 和 `ftd2xx.dll` 是否位于同一个目录。
- DLL 是否位于脚本同级目录、同级 `dll` 目录或交付包 `bin` 目录。

### 8.2 Python 位数不正确

如果使用 32 位 Python，会看到需要 64 位 Python 的提示。安装并使用 64 位 Python 后重新运行。

### 8.3 没有发现设备

可能看到：

```text
ERROR: No PynectDirect device found
```

检查 USB 连接和设备供电，并关闭其他正在占用光谱仪的软件。

### 8.4 输入值无效

- 积分时间必须是正整数，单位为微秒。
- 平均次数必须在 `1-255` 之间。
- 起始波长和结束波长必须位于显示的完整范围内。
- 起始波长必须小于结束波长。

程序会重新提示，按要求输入即可。

### 8.5 API 返回负错误码

错误信息会包含操作名称和返回码，例如：

```text
ERROR: Get selected spectrum data failed, return code=-9
```

常见返回码包括：

| 返回码 | 含义 |
| ---: | --- |
| `-3` | 设备未打开 |
| `-4` | 设备通信或返回数据异常 |
| `-5` | 参数或数组容量无效 |
| `-7/-8` | 数据校验或数据包异常 |
| `-9` | 设备响应超时 |
| `-11` | 当前设备不支持该功能 |

完整错误码和 API 参数说明请查阅 `docs/PynectDirect_API_Guide.md`。

## 9. 安全说明

- 本例程只修改当前测量使用的临时积分时间和平均次数。
- 本例程不会调用 Flash、固化配置、氙灯、DAC 或设备复位功能。
- 不要在设备采集过程中拔出 USB 线。
- 较长积分时间和较大平均次数会增加等待时间，并不表示程序失去响应。
- 使用自己的程序调用时，也应保证异常路径最终关闭设备。
