# PynectDirect SDK Python 用户手册

> 适用对象：第一次使用 PynectDirect 光谱仪的学生、教师和应用开发人员  
> 运行环境：64 位 Windows、64 位 Python 3  
> 文档范围：设备使用、Python 调用、数据采集、API 查询和故障排查

## 1. 阅读本手册

### 1.1 学习目标

完成本手册后，你应当能够：

1. 正确放置运行文件并完成只读设备检查。
2. 使用 Python 扫描、打开和关闭光谱仪。
3. 读取设备信息、波长数组和光谱强度数组。
4. 设置积分时间、平均次数和采集波段。
5. 将测量结果保存为 CSV，并绘制光谱曲线。
6. 根据错误码定位常见问题。
7. 在需要时查询全部 55 个公开 API。

### 1.2 支持的设备

PynectDirect 使用同一套 Python 调用方式支持两类设备：

- UV/VIS：紫外可见光谱仪。
- NIR：近红外光谱仪。

打开设备后会自动识别类型。两类设备的基础采谱流程相同，但氙灯、外触发、DAC 和固化配置等功能可能只在特定设备上可用。调用进阶功能前，应先读取设备能力。

### 1.3 常用术语

| 术语 | 含义 |
| --- | --- |
| DLL | Python 实际加载的 PynectDirect 功能库 |
| API | Python 可以调用的设备功能 |
| 设备索引 | 扫描结果中的编号，从 `0` 开始 |
| 芯片序列号 | USB 芯片的唯一编号，适合区分多台设备 |
| 设备序列号 | 光谱仪自身的业务编号 |
| 波长点数 | 一次光谱中包含的数据点数量 |
| nm | 纳米，波长单位 |
| 积分时间 | 探测器累计信号的时间，单位为微秒 |
| 平均次数 | 多次采集后求平均的次数 |

### 1.4 风险等级

本手册对接口使用以下风险标记：

| 等级 | 含义 |
| --- | --- |
| 只读 | 不改变设备参数，适合初次测试 |
| 临时设置 | 改变当前运行参数，关闭或恢复后不应长期保留 |
| 需谨慎 | 会影响外部电路、光源或触发行为，只能在实验方案明确时使用 |
| 高风险 | 可能修改持久数据、复位设备或影响标定，不用于课堂自由实验 |

## 2. 五分钟快速开始

### 2.1 准备文件

推荐将文件整理为以下形式：

```text
PynectDirect_User/
|-- minimal_test.py
|-- python_ctypes_example.py
`-- dll/
    |-- PynectDirect.dll
    `-- ftd2xx.dll
```

`dll` 文件夹与 `minimal_test.py` 位于同一级。两个 DLL 必须放在同一个 `dll` 文件夹中。

脚本也支持把 `PynectDirect.dll` 直接放在 `minimal_test.py` 旁边，但为了保持目录整洁，建议使用上面的布局。

### 2.2 检查 Python 位数

在文件夹空白处打开 PowerShell，然后运行：

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

正确输出为：

```text
64
```

如果输出 `32`，需要安装 64 位 Python。32 位 Python 无法加载本 SDK 的 64 位 DLL。

### 2.3 连接设备

1. 将光谱仪连接到电脑 USB 接口。
2. 等待 Windows 完成设备识别。
3. 关闭其他可能正在使用光谱仪的软件。
4. 不要同时启动两个采集程序访问同一台设备。

### 2.4 运行只读测试

在 `minimal_test.py` 所在目录运行：

```powershell
python .\minimal_test.py --read-only
```

典型输出如下。设备型号、序列号、波长范围和强度值会因设备而不同。

```text
[PASS] DLL load: ...\dll\PynectDirect.dll
[PASS] API exports: 55 functions
[PASS] Device scan: found 1 device(s)
[PASS] Open device: 0
[PASS] Device information
[PASS] Wavelength count: 2048
[PASS] Full wavelength data: points=2048 ...
[PASS] Full spectrum: points=2048 ...
[PASS] Close device: closed

Summary: PASS=17 SKIP=2 FAIL=0
```

满足以下条件即可认为基础环境可用：

- `DLL load` 为 `PASS`，且路径指向预期的 DLL。
- `API exports` 显示 `55 functions`。
- `Device scan` 至少发现一台设备。
- `Full spectrum` 为 `PASS`。
- 最终汇总中的 `FAIL=0`。

`SKIP` 不等于失败。只读模式会主动跳过参数写入和指定波段设置，因此通常会出现 `SKIP`。

## 3. 使用最小测试工具

### 3.1 输出状态

| 状态 | 含义 | 处理方式 |
| --- | --- | --- |
| `PASS` | 该检查成功 | 无需处理 |
| `SKIP` | 当前模式或设备能力不适用 | 查看同一行原因 |
| `FAIL` | 加载、参数、通信或数据检查失败 | 按第 14 章排查 |

### 3.2 测试项目说明

| 项目 | 检查内容 |
| --- | --- |
| `DLL load` | 是否找到并加载 `PynectDirect.dll` 及其依赖 |
| `API exports` | DLL 是否包含全部 55 个公开函数 |
| `Device scan` | 当前可访问的设备数量 |
| `FTDI chip serial` | USB 芯片序列号 |
| `Open device` | 是否成功打开指定设备 |
| `Capabilities` | 当前设备支持的功能集合 |
| `Device identity` | 协议、型号、设备序列号和硬件版本 |
| `Wavelength count` | 光谱数据点数量 |
| `Full wavelength data` | 完整波长数组是否可读 |
| `Full spectrum` | 是否成功采集强度数组 |
| `Close device` | 是否可靠释放设备 |

### 3.3 命令参数

```powershell
python .\minimal_test.py --help
```

| 参数 | 默认值 | 用途 |
| --- | ---: | --- |
| `--dll PATH` | 自动查找 | 指定 `PynectDirect.dll` 的完整路径 |
| `--device-index N` | `0` | 测试扫描结果中的第 N 台设备 |
| `--read-only` | 关闭 | 只读测试，不改变当前参数 |
| `--integration-us N` | `50000` | 普通测试使用的临时积分时间，单位微秒 |
| `--average N` | `1` | 普通测试使用的临时平均次数，范围 `1-255` |

常用命令：

```powershell
# 第一次连接时推荐
python .\minimal_test.py --read-only

# 测试第二台设备
python .\minimal_test.py --read-only --device-index 1

# 临时使用 100 ms 积分时间和 3 次平均
python .\minimal_test.py --integration-us 100000 --average 3

# 明确指定 DLL
python .\minimal_test.py --dll .\dll\PynectDirect.dll --read-only
```

不带 `--read-only` 时，工具会先保存当前积分时间、平均次数和触发设置，再使用测试值，结束时恢复原值。发生异常时也会尝试恢复并关闭设备。该工具不会默认执行 Flash 写入、设备复位、固化参数、DAC 输出或氙灯控制。

## 4. 第一次连接设备

### 4.1 Python 文件布局

在 `minimal_test.py` 旁边新建 `device_info.py`：

```text
PynectDirect_User/
|-- minimal_test.py
|-- device_info.py
`-- dll/
    |-- PynectDirect.dll
    `-- ftd2xx.dll
```

`minimal_test.py` 已包含全部 API 的 Python 类型绑定。自己的程序可以直接复用这些经过验证的加载和绑定功能。

### 4.2 读取设备信息

将以下内容写入 `device_info.py`：

```python
import ctypes

from minimal_test import (
    PROTOCOL_AUTO,
    PynectDirectDeviceInfo,
    bind_api,
    decode_char_array,
    find_dll,
    load_library,
)


SUCCESS = 0


def require_success(code, operation):
    if code != SUCCESS:
        raise RuntimeError(f"{operation} failed, return code={code}")


sdk = load_library(find_dll())
bind_api(sdk)
opened = False

try:
    device_count = sdk.pynect_direct_scan_devices()
    if device_count <= 0:
        raise RuntimeError("没有发现可用设备")

    require_success(
        sdk.pynect_direct_set_protocol_preference(PROTOCOL_AUTO),
        "设置自动识别",
    )
    require_success(sdk.pynect_direct_open_device(0), "打开设备")
    opened = True

    info = PynectDirectDeviceInfo()
    require_success(
        sdk.pynect_direct_get_device_info(ctypes.byref(info)),
        "读取设备信息",
    )

    print("协议:", decode_char_array(info.protocol_name))
    print("型号:", decode_char_array(info.model))
    print("序列号:", decode_char_array(info.serial_number))
    print("硬件版本:", decode_char_array(info.hardware_version))
    print(f"能力值: 0x{info.capabilities:08X}")
finally:
    if opened:
        sdk.pynect_direct_close_device()
```

运行：

```powershell
python .\device_info.py
```

预期会显示设备协议、型号、序列号、硬件版本和能力值。无论读取是否成功，`finally` 都会执行关闭操作。

### 4.3 基本调用顺序

所有常规程序都应遵循以下顺序：

1. 加载 DLL 并绑定 API。
2. 扫描设备。
3. 选择设备索引或芯片序列号。
4. 打开设备。
5. 读取能力和设备信息。
6. 设置临时参数并采集数据。
7. 在 `finally` 中关闭设备。

## 5. 第一次采集完整光谱

### 5.1 完整采集示例

SDK 已附带可直接运行的只读例程：

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

例程中的 `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(len(wavelengths), len(intensities))
```

该函数不会修改积分时间、平均次数或其他设备参数。下面给出相同流程的展开版本，便于学习每一步的调用方式。

在同一目录新建 `capture_spectrum.py`：

```python
import ctypes

from minimal_test import bind_api, find_dll, load_library


SUCCESS = 0


def require_success(code, operation):
    if code != SUCCESS:
        raise RuntimeError(f"{operation} failed, return code={code}")


sdk = load_library(find_dll())
bind_api(sdk)
opened = False

try:
    device_count = sdk.pynect_direct_scan_devices()
    if device_count <= 0:
        raise RuntimeError("没有发现可用设备")

    require_success(sdk.pynect_direct_open_device(0), "打开设备")
    opened = True

    point_count = ctypes.c_uint16()
    require_success(
        sdk.pynect_direct_get_wavelength_count(ctypes.byref(point_count)),
        "读取波长点数",
    )
    if point_count.value == 0:
        raise RuntimeError("设备返回的波长点数为 0")

    wavelengths = (ctypes.c_double * point_count.value)()
    intensities = (ctypes.c_uint16 * point_count.value)()

    require_success(
        sdk.pynect_direct_get_full_wavelength_data(
            wavelengths, point_count.value
        ),
        "读取波长数组",
    )
    require_success(
        sdk.pynect_direct_get_full_spectrum_data(
            intensities, point_count.value
        ),
        "采集光谱强度",
    )

    print("点数:", point_count.value)
    print(f"波长范围: {wavelengths[0]:.3f} - {wavelengths[-1]:.3f} nm")
    print("强度范围:", min(intensities), "-", max(intensities))
    print("前 5 个数据点:")
    for index in range(min(5, point_count.value)):
        print(f"  {wavelengths[index]:.3f} nm, {intensities[index]}")
finally:
    if opened:
        sdk.pynect_direct_close_device()
```

运行：

```powershell
python .\capture_spectrum.py
```

典型结果：

```text
点数: 2048
波长范围: 780.294 - 1002.186 nm
强度范围: 90 - 319
前 5 个数据点:
  780.294 nm, 116
  ...
```

具体数字取决于设备、光源、样品、积分时间和环境。

### 5.2 为什么先读取点数

不同型号可能返回不同的数据点数量。必须先调用 `pynect_direct_get_wavelength_count`，再按该数量分配波长和强度数组。数组过小会返回参数错误，写死 `2048` 会导致程序无法兼容其他型号。

### 5.3 一次采集得到什么

- `wavelengths[index]`：第 `index` 个数据点对应的波长，单位 nm。
- `intensities[index]`：同一位置的原始强度值。
- 两个数组长度必须相同。
- `pynect_direct_get_full_spectrum_data` 会取得一次新的光谱数据。

## 6. 理解光谱数据

### 6.1 波长和强度必须按索引配对

第 N 个波长只对应第 N 个强度：

```text
wavelengths[0]  <-> intensities[0]
wavelengths[1]  <-> intensities[1]
...
wavelengths[N]  <-> intensities[N]
```

不要分别排序两个数组，也不要删除其中一个数组的单独数据点。

### 6.2 强度值的含义

光谱强度为无符号 16 位原始数值，理论存储范围为 `0-65535`。实际有效范围、饱和位置和暗信号水平由设备及测量条件决定。

原始强度不是透过率、吸光度或反射率。得到这些物理量通常还需要暗背景、参考光谱和相应计算流程。

### 6.3 数据质量检查

每次采集后至少检查：

- 点数是否大于 0。
- 波长是否从小到大排列。
- 结束波长是否大于起始波长。
- 强度是否全部接近 0。
- 是否出现大量相同的最大值，可能表示饱和。
- 多次测量的峰位置是否稳定。

## 7. 设置测量参数和波长范围

### 7.1 积分时间

积分时间单位为微秒。`50000` 表示 50 ms，`100000` 表示 100 ms。

```python
import ctypes

old_integration = ctypes.c_uint()
require_success(
    sdk.pynect_direct_get_integration_time(ctypes.byref(old_integration)),
    "读取积分时间",
)
require_success(
    sdk.pynect_direct_set_integration_time(100000),
    "设置积分时间",
)
print("原积分时间:", old_integration.value, "us")
```

一般规律：

- 信号过弱时，可逐步增加积分时间。
- 信号饱和时，应减小积分时间。
- 积分时间越长，单次测量等待时间通常越长。
- 可用范围以具体设备说明为准；参数必须为正数。

该设置属于临时设置。实验结束前建议恢复原值。

### 7.2 平均次数

平均次数通常使用 `1-255`：

```python
import ctypes

old_average = ctypes.c_uint8()
require_success(
    sdk.pynect_direct_get_average_number(ctypes.byref(old_average)),
    "读取平均次数",
)
require_success(
    sdk.pynect_direct_set_average_number(3),
    "设置平均次数",
)
print("原平均次数:", old_average.value)
```

增加平均次数通常可以降低随机波动，但会增加采集时间。课堂实验可依次比较 `1`、`3`、`5` 次平均的差异。

### 7.3 恢复临时参数

修改参数前先读取旧值，并在 `finally` 中恢复：

```python
old_integration = ctypes.c_uint()
old_average = ctypes.c_uint8()

require_success(
    sdk.pynect_direct_get_integration_time(ctypes.byref(old_integration)),
    "读取积分时间",
)
require_success(
    sdk.pynect_direct_get_average_number(ctypes.byref(old_average)),
    "读取平均次数",
)

try:
    require_success(
        sdk.pynect_direct_set_integration_time(100000),
        "设置积分时间",
    )
    require_success(
        sdk.pynect_direct_set_average_number(3),
        "设置平均次数",
    )
    # 在这里完成测量
finally:
    sdk.pynect_direct_set_average_number(old_average.value)
    sdk.pynect_direct_set_integration_time(old_integration.value)
```

### 7.4 指定波长范围

指定波段只改变本次程序返回的数据窗口，不改变设备的物理波长范围。

```python
import ctypes

from minimal_test import PynectDirectSelectedWavelengthRange


request_start_nm = 900.0
request_end_nm = 950.0

full_start = ctypes.c_double()
full_end = ctypes.c_double()
require_success(
    sdk.pynect_direct_get_full_wavelength_range(
        ctypes.byref(full_start), ctypes.byref(full_end)
    ),
    "读取完整波长范围",
)

if not (
    full_start.value <= request_start_nm
    < request_end_nm <= full_end.value
):
    raise ValueError(
        f"请求范围必须位于 {full_start.value:.3f} - "
        f"{full_end.value:.3f} nm 内"
    )

require_success(
    sdk.pynect_direct_set_selected_wavelength_range(
        request_start_nm, request_end_nm
    ),
    "设置波长范围",
)

selected = PynectDirectSelectedWavelengthRange()
require_success(
    sdk.pynect_direct_get_selected_wavelength_range(
        ctypes.byref(selected)
    ),
    "读取实际波长范围",
)

wavelengths = (ctypes.c_double * selected.count)()
intensities = (ctypes.c_uint16 * selected.count)()
require_success(
    sdk.pynect_direct_get_selected_wavelength_data(
        wavelengths, selected.count
    ),
    "读取波段波长",
)
require_success(
    sdk.pynect_direct_get_selected_spectrum_data(
        intensities, selected.count
    ),
    "读取波段光谱",
)

print(
    f"请求范围: {selected.request_start_nm:.3f} - "
    f"{selected.request_end_nm:.3f} nm"
)
print(
    f"实际范围: {selected.actual_start_nm:.3f} - "
    f"{selected.actual_end_nm:.3f} nm"
)
print("点数:", selected.count)
```

实际范围可能与请求值略有差异，因为设备只能选择已有像素对应的波长点。后续数组长度必须使用 `selected.count`。

## 8. 保存 CSV 数据

采集完成后，可使用 Python 自带的 `csv` 模块保存数据：

```python
import csv
from datetime import datetime
from pathlib import Path


output_path = Path(__file__).resolve().parent / "spectrum.csv"
measured_at = datetime.now().isoformat(timespec="seconds")

with output_path.open("w", newline="", encoding="utf-8-sig") as file:
    writer = csv.writer(file)
    writer.writerow(["measured_at", measured_at])
    writer.writerow(["wavelength_nm", "intensity"])
    writer.writerows(zip(wavelengths, intensities))

print("数据已保存:", output_path)
```

生成的文件结构如下：

```text
measured_at,2026-07-31T17:10:00
wavelength_nm,intensity
780.294,116
780.402,121
...
```

建议同时记录：

- 设备型号和序列号。
- 积分时间。
- 平均次数。
- 样品名称。
- 暗背景或参考光谱信息。
- 测量时间。

`utf-8-sig` 便于 Windows 表格软件正确识别中文。保存完成后再关闭设备，不要在采集过程中移动或断开 USB。

## 9. 绘制光谱曲线

### 9.1 安装绘图库

首次使用时运行：

```powershell
python -m pip install matplotlib
```

### 9.2 从 CSV 绘图

在 `spectrum.csv` 旁边新建 `plot_spectrum.py`：

```python
import csv
from pathlib import Path

import matplotlib.pyplot as plt


csv_path = Path(__file__).resolve().parent / "spectrum.csv"
wavelengths = []
intensities = []

with csv_path.open("r", encoding="utf-8-sig", newline="") as file:
    reader = csv.reader(file)
    next(reader)  # 测量时间
    next(reader)  # 列标题
    for wavelength_nm, intensity in reader:
        wavelengths.append(float(wavelength_nm))
        intensities.append(int(intensity))

plt.figure(figsize=(9, 5))
plt.plot(wavelengths, intensities, color="#1261A0", linewidth=1.2)
plt.xlabel("Wavelength (nm)")
plt.ylabel("Intensity")
plt.title("PynectDirect Spectrum")
plt.grid(True, alpha=0.25)
plt.tight_layout()
plt.show()
```

运行：

```powershell
python .\plot_spectrum.py
```

如果图形为空，先检查 CSV 是否包含数据、两列长度是否相同，以及波长列能否转换为数字。

## 10. 多设备使用

### 10.1 索引与芯片序列号

设备索引由当前扫描顺序决定，重新插拔后可能改变。长期绑定某台设备时，应记录芯片序列号，并按序列号打开。

```python
import ctypes


device_count = sdk.pynect_direct_scan_devices()
if device_count <= 0:
    raise RuntimeError("没有发现设备")

chip_serials = []
for index in range(device_count):
    serial_buffer = ctypes.create_string_buffer(64)
    require_success(
        sdk.pynect_direct_get_chip_serial_by_index(
            index, serial_buffer, len(serial_buffer)
        ),
        f"读取设备 {index} 的芯片序列号",
    )
    chip_serial = serial_buffer.value.decode(
        "utf-8", errors="replace"
    )
    chip_serials.append(chip_serial)
    print(index, chip_serial)

target_serial = chip_serials[0]
require_success(
    sdk.pynect_direct_open_device_by_chip_serial(
        target_serial.encode("utf-8")
    ),
    "按芯片序列号打开设备",
)
```

### 10.2 多台设备的推荐顺序

同一个 Python 进程一次只操作一台当前设备：

1. 扫描并保存全部芯片序列号。
2. 按第一个序列号打开、采集、关闭。
3. 按下一个序列号打开、采集、关闭。
4. 每台设备的数据文件中记录对应芯片序列号和设备序列号。

不要在多个线程中同时调用同一个 DLL 实例。确需并行采集时，应先与设备供应方确认支持方式。

## 11. 识别设备能力

### 11.1 为什么要检查能力

不同设备支持的功能不同。基础采谱通常可用，但 UV/VIS 专用功能在 NIR 设备上可能返回 `-11`。正确做法是打开设备后读取能力值，再决定是否调用某项功能。

### 11.2 能力表

| 能力名称 | 数值 | 说明 |
| --- | ---: | --- |
| `PYNECT_DIRECT_CAP_INTEGRATION_TIME` | `0x00000001` | 积分时间 |
| `PYNECT_DIRECT_CAP_AVERAGE_NUMBER` | `0x00000002` | 平均次数 |
| `PYNECT_DIRECT_CAP_LEVEL_OUTPUT` | `0x00000004` | 电平输出 |
| `PYNECT_DIRECT_CAP_FLASH` | `0x00000008` | Flash/FLS |
| `PYNECT_DIRECT_CAP_WAVELENGTH_DATA` | `0x00000010` | 波长数据 |
| `PYNECT_DIRECT_CAP_SPECTRUM_DATA` | `0x00000020` | 光谱数据 |
| `PYNECT_DIRECT_CAP_TEMPERATURE` | `0x00000040` | 温度 |
| `PYNECT_DIRECT_CAP_HARDWARE_VERSION` | `0x00000080` | 硬件版本 |
| `PYNECT_DIRECT_CAP_CALIBRATION` | `0x00000100` | 标定系数 |
| `PYNECT_DIRECT_CAP_XENON` | `0x00000200` | 氙灯控制 |
| `PYNECT_DIRECT_CAP_USB_STATUS` | `0x00000400` | USB 状态和速率参数 |
| `PYNECT_DIRECT_CAP_EXTERNAL_TRIGGER` | `0x00000800` | 外触发 |
| `PYNECT_DIRECT_CAP_DAC` | `0x00001000` | DAC 输出 |
| `PYNECT_DIRECT_CAP_STORED_CONFIG` | `0x00002000` | 固化配置 |
| `PYNECT_DIRECT_CAP_SLIT_WIDTH` | `0x00004000` | 狭缝宽度 |

### 11.3 Python 检查方式

```python
import ctypes


CAP_EXTERNAL_TRIGGER = 0x00000800
capabilities = ctypes.c_uint32()
require_success(
    sdk.pynect_direct_get_capabilities(ctypes.byref(capabilities)),
    "读取设备能力",
)

if capabilities.value & CAP_EXTERNAL_TRIGGER:
    print("设备支持外触发")
else:
    print("设备不支持外触发")
```

## 12. 高级功能与安全

### 12.1 功能风险速查

| 功能 | 代表 API | 风险 | 使用要求 |
| --- | --- | --- | --- |
| 设备信息和光谱读取 | `pynect_direct_get_*` | 只读 | 打开设备后使用 |
| 积分时间和平均次数 | `pynect_direct_set_integration_time` 等 | 临时设置 | 先保存旧值，结束时恢复 |
| GPIO 电平 | `pynect_direct_set_level_output` | 需谨慎 | 明确外部接线和位定义 |
| 氙灯 | `pynect_direct_set_xenon_*` | 需谨慎 | 确认光源、电源和散热条件 |
| 外触发 | `pynect_direct_set_external_trigger_config` | 需谨慎 | 确认触发电平和时序 |
| DAC | `pynect_direct_set_dac_voltage` | 需谨慎 | 确认输出连接和允许码值 |
| Flash 写入 | `pynect_direct_flash_write` | 高风险 | 备份数据并获得教师或供应方授权 |
| 固化配置 | `pynect_direct_set_stored_*` | 高风险 | 记录原值，确认设备型号和参数范围 |
| 设备复位 | `pynect_direct_reset_device` | 高风险 | 停止采集，准备重新初始化 |

### 12.2 氙灯控制

氙灯功能通常只在支持 `PYNECT_DIRECT_CAP_XENON` 的 UV/VIS 设备上可用。周期和高电平宽度的单位都是 10 ns。设置前必须确认：

- 高电平宽度不大于周期。
- 光源和设备允许连续或单次模式。
- 实验现场具备必要的防护措施。
- 异常退出时能够关闭光源。

不要把氙灯写操作放入自动启动程序。

### 12.3 外触发

推荐流程：

1. 读取并记录原触发配置。
2. 根据设备说明设置触发开关、类型和帧数。
3. 由外部信号触发采集。
4. 查询采集状态。
5. 读取指定帧数据。
6. 恢复原触发配置。

触发电平、接线和时序错误可能造成无数据或持续等待。课堂基础实验应保持触发关闭，使用普通光谱读取接口。

### 12.4 Flash 和固化配置

Flash 和固化参数会影响设备长期状态，不属于基础采谱操作。使用前必须：

- 确认设备型号和目标扇区。
- 备份原始数据并校验备份可读。
- 明确写入数据长度和格式。
- 获得教师、实验室管理员或设备供应方授权。
- 写入后重新读取并核对结果。

学生练习不得执行 Flash 写入、固化配置或设备复位。

## 13. 错误处理与安全关闭

### 13.1 错误码

多数函数成功时返回 `0`，失败时返回负数：

| 返回值 | 名称 | 用户含义 |
| ---: | --- | --- |
| `0` | `PYNECT_DIRECT_SUCCESS` | 操作成功 |
| `-1` | `PYNECT_DIRECT_ERR_NOT_INITIALIZED` | 所需状态尚未建立，例如未设置指定波段 |
| `-2` | `PYNECT_DIRECT_ERR_DEVICE_NOT_FOUND` | 未找到目标设备 |
| `-3` | `PYNECT_DIRECT_ERR_DEVICE_NOT_OPEN` | 尚未打开设备 |
| `-4` | `PYNECT_DIRECT_ERR_COMM_FAIL` | 设备通信或返回数据异常 |
| `-5` | `PYNECT_DIRECT_ERR_INVALID_PARAM` | 参数、范围或缓冲区长度错误 |
| `-6` | `PYNECT_DIRECT_ERR_OPEN_FAILED` | 设备打开失败 |
| `-7` | `PYNECT_DIRECT_ERR_CRC` | 数据校验失败 |
| `-8` | `PYNECT_DIRECT_ERR_PREAMBLE` | 接收数据格式异常 |
| `-9` | `PYNECT_DIRECT_ERR_TIMEOUT` | 等待设备响应超时 |
| `-10` | `PYNECT_DIRECT_ERR_WPROT` | 写入条件或写保护状态不满足 |
| `-11` | `PYNECT_DIRECT_ERR_UNSUPPORTED` | 当前设备不支持该功能 |

注意以下例外：

- `pynect_direct_scan_devices` 成功时返回设备数量，`0` 表示未发现设备。
- `pynect_direct_is_open` 返回 `1` 或 `0` 表示状态。
- `pynect_direct_flash_read` 成功时返回读取的字节数。
- `pynect_direct_external_spectrum_control` 成功时返回数据字节数。
- `pynect_direct_close_device` 没有返回值。

### 13.2 统一检查返回值

```python
ERROR_NAMES = {
    -1: "状态未初始化",
    -2: "未找到设备",
    -3: "设备未打开",
    -4: "通信失败",
    -5: "参数错误",
    -6: "打开失败",
    -7: "数据校验失败",
    -8: "数据格式异常",
    -9: "设备响应超时",
    -10: "写保护状态不满足",
    -11: "设备不支持该功能",
}


def require_success(code, operation):
    if code != 0:
        reason = ERROR_NAMES.get(code, "未知错误")
        raise RuntimeError(
            f"{operation}失败: {reason}, return code={code}"
        )
```

### 13.3 始终关闭设备

设备成功打开后，后续代码必须使用 `try/finally`：

```python
opened = False
try:
    require_success(sdk.pynect_direct_open_device(0), "打开设备")
    opened = True
    # 读取或采集
finally:
    if opened:
        sdk.pynect_direct_close_device()
```

不要依赖程序自然退出释放设备。异常退出后，如果其他软件无法打开设备，先关闭残留的 Python 进程，再重新插拔 USB。

## 14. 故障排查

### 14.1 提示无法加载 DLL

依次检查：

1. Python 位数检查结果是否为 `64`。
2. 文件名是否严格为 `PynectDirect.dll`。
3. `ftd2xx.dll` 是否与 `PynectDirect.dll` 在同一文件夹。
4. `dll` 文件夹是否与 `minimal_test.py` 同级。
5. 是否误把 DLL 放入多一层同名文件夹。
6. 使用 `--dll` 指定路径后是否仍然失败。

测试命令：

```powershell
python .\minimal_test.py --dll .\dll\PynectDirect.dll --read-only
```

### 14.2 `Device scan` 显示 0

- 确认 USB 线支持数据传输，而不是仅供电。
- 更换电脑 USB 接口，避免无供电的扩展接口。
- 在 Windows 设备管理器中确认设备已识别。
- 关闭其他光谱软件和残留的 Python 进程。
- 重新插拔设备后再运行只读测试。

### 14.3 返回 `-6`，设备打开失败

最常见原因是设备已被另一个程序占用。关闭其他采集程序后重试。如果多台设备同时连接，先运行只读测试确认索引，再使用正确的 `--device-index`。

### 14.4 返回 `-9`，设备响应超时

- 检查 USB 连接是否松动。
- 降低程序连续调用频率。
- 确认外触发没有处于等待状态。
- 关闭设备并重新打开；仍无效时重新插拔 USB。

### 14.5 返回 `-11`，设备不支持该功能

这通常不是故障。读取能力值，只调用当前设备明确支持的功能。NIR 设备不一定支持 UV/VIS 的氙灯、外触发、DAC 或固化配置。

### 14.6 返回 `-5`，参数错误

重点检查：

- 平均次数是否在 `1-255`。
- 积分时间是否为正数并符合设备范围。
- 起始波长是否小于结束波长。
- 请求波段是否位于完整波长范围内。
- 数组长度是否至少等于设备返回的点数。
- 字符串缓冲区和数据缓冲区是否足够大。

### 14.7 指定波段返回 `-1`

必须先调用 `pynect_direct_set_selected_wavelength_range`，成功后再读取指定波段信息和数据。设备复位或重新打开后，应重新设置波段。

### 14.8 光谱全为低值、噪声大或饱和

- 确认光源、光纤和样品位置。
- 信号过低时逐步增加积分时间。
- 噪声较大时适当增加平均次数。
- 大量数据接近上限时减小积分时间。
- 保持测量条件稳定，多采几次比较。

### 14.9 向技术支持提供什么

请准备：

- `minimal_test.py --read-only` 的完整输出。
- Python 版本和 64 位检查结果。
- DLL 实际加载路径。
- 设备型号、芯片序列号和设备序列号。
- 出错 API 名称和返回值。
- 能否被其他设备软件正常识别。

## 15. 完整 API 参考

本章所有 Python 示例均假定：已经使用 `load_library()` 加载 DLL，已经使用 `bind_api(sdk)` 设置函数类型，并已定义第 13.2 节中的 `require_success()`。需要设备连接的接口还假定设备已经成功打开。示例中的 `ctypes` 来自 Python 标准库。

参数表中的“输入”表示 Python 向 DLL 提供值；“输出”表示 DLL 将结果写入 Python 创建的变量或数组；“输入/输出”表示调用前需要提供容量，调用后从同一对象读取结果。

### 15.1 设备发现、打开和识别

#### `pynect_direct_scan_devices`

**功能说明**

刷新当前 USB 设备列表，在控制台打印每台设备的索引、描述和芯片序列号，并返回本次发现的设备数量。该函数不打开设备。

**适用范围**

UV/VIS、NIR；只读；不要求任何 capability 位。

**调用前提**

设备已连接并被 Windows 识别。其他程序不应独占目标设备。扫描可以在设备未打开时调用。

**输入参数**

无。

**输出参数**

无输出容器；设备列表直接打印到控制台。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `1-64` | 本次发现的设备数量；每台设备可用索引为 `0` 到 `count - 1` |
| `0` | 未发现设备，或底层 USB 枚举失败；该函数不返回负错误码 |

**Python 示例**

```python
device_count = sdk.pynect_direct_scan_devices()
if device_count == 0:
    print("No PynectDirect device found")
else:
    print(f"Found {device_count} device(s)")
```

**常见错误**

把 `0` 当作成功状态码。此函数与多数 API 不同，`0` 表示没有可用设备。关联 API：`pynect_direct_get_chip_serial_by_index`、`pynect_direct_open_device`。

#### `pynect_direct_get_chip_serial_by_index`

**功能说明**

根据扫描索引读取 USB 芯片序列号。该序列号用于稳定区分多台设备，与光谱仪业务序列号不是同一个字段。

**适用范围**

UV/VIS、NIR；只读；不要求设备已经打开。

**调用前提**

建议先调用 `pynect_direct_scan_devices`，并确保 `index` 小于扫描返回数量。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `index` | `int` | 输入 | `0` 到 `device_count - 1` | 扫描列表索引 |
| `chip_serial` | `ctypes.create_string_buffer` | 输入/输出 | 建议 64 字节 | 接收以 `\0` 结尾的芯片序列号 |
| `max_len` | `int` | 输入 | 至少 `2`；通常传 `len(buffer)` | 缓冲区总字节数，包含结尾字符空间 |

**输出参数**

成功后从 `chip_serial.value` 取得字节串，再按 UTF-8 解码。若缓冲区较小，字符串会被截断，因此建议分配 64 字节。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 读取成功 |
| `-2` | 未发现设备或索引超出当前设备数量 |
| `-5` | 索引为负、缓冲区无效或 `max_len <= 1` |

**Python 示例**

```python
import ctypes

device_count = sdk.pynect_direct_scan_devices()
if device_count > 0:
    serial_buffer = ctypes.create_string_buffer(64)
    code = sdk.pynect_direct_get_chip_serial_by_index(
        0, serial_buffer, len(serial_buffer)
    )
    require_success(code, "Get chip serial")
    chip_serial = serial_buffer.value.decode("utf-8", errors="replace")
    print(chip_serial)
```

**常见错误**

传入业务序列号接口的缓冲区结果作为芯片序列号，或在重新插拔设备后继续使用旧索引。关联 API：`pynect_direct_open_device_by_chip_serial`、`pynect_direct_get_serial_number`。

#### `pynect_direct_open_device`

**功能说明**

按当前扫描索引打开一台设备，并识别设备协议和 capability。若当前已有设备打开，函数会先关闭旧设备，再尝试打开新索引。

**适用范围**

UV/VIS、NIR；连接操作；成功后建立 DLL 的当前设备状态。

**调用前提**

先扫描设备并选择有效索引。目标设备不能被其他进程独占。需要强制协议时，应在打开前调用 `pynect_direct_set_protocol_preference`。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `index` | `int` | 输入 | `0` 到 `device_count - 1` | 当前 USB 枚举列表中的设备索引 |

**输出参数**

无。成功后可通过 `pynect_direct_is_open`、`pynect_direct_get_device_protocol` 和 `pynect_direct_get_capabilities` 查询连接状态。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 打开成功 |
| `-6` | 索引无效、设备被占用或 USB 打开失败 |

**Python 示例**

```python
device_count = sdk.pynect_direct_scan_devices()
if device_count <= 0:
    raise RuntimeError("No device found")
require_success(sdk.pynect_direct_open_device(0), "Open device")
print("Device opened")
```

**常见错误**

直接写死索引而不先扫描；多台设备重新插拔后索引可能变化。打开新设备会关闭旧设备，因此不要假设两个索引可以在同一 DLL 实例中同时保持打开。关联 API：`pynect_direct_scan_devices`、`pynect_direct_close_device`。

#### `pynect_direct_open_device_by_chip_serial`

**功能说明**

遍历当前 USB 设备，找到芯片序列号完全匹配的设备并打开。适合设备索引可能变化的多设备系统。

**适用范围**

UV/VIS、NIR；连接操作。

**调用前提**

已通过 `pynect_direct_get_chip_serial_by_index` 取得非空芯片序列号。目标设备没有被其他程序占用。

**输入参数**

| 参数 | Python 类型 | 方向 | 格式 | 说明 |
| --- | --- | --- | --- | --- |
| `chip_serial` | `bytes` / `ctypes.c_char_p` | 输入 | 非空、以 `\0` 终止 | USB 芯片序列号；Python 字符串需先编码为字节串 |

**输出参数**

无。成功后建立当前设备连接。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 找到匹配设备并打开成功 |
| `-2` | 没有设备或没有匹配序列号 |
| `-5` | `chip_serial` 为空 |
| `-6` | 找到设备但打开失败或设备被占用 |

**Python 示例**

```python
chip_serial = "B002P00S"
code = sdk.pynect_direct_open_device_by_chip_serial(
    chip_serial.encode("utf-8")
)
require_success(code, "Open device by chip serial")
```

**常见错误**

传入光谱仪业务序列号而不是 USB 芯片序列号，或直接传 Python `str` 而未编码。关联 API：`pynect_direct_get_chip_serial_by_index`、`pynect_direct_get_serial_number`。

#### `pynect_direct_close_device`

**功能说明**

关闭当前设备句柄，清除协议、能力和指定波段状态。重复调用是允许的。

**适用范围**

UV/VIS、NIR；资源清理操作。

**调用前提**

无。建议只要设备曾成功打开，就在 `finally` 中调用。

**输入参数**

无。

**输出参数**

无。

**返回值**

没有返回值，Python 中结果为 `None`。无法通过返回值判断底层关闭状态，可在关闭后调用 `pynect_direct_is_open` 验证。

**Python 示例**

```python
opened = False
try:
    require_success(sdk.pynect_direct_open_device(0), "Open device")
    opened = True
    # Perform read-only operations here.
finally:
    if opened:
        sdk.pynect_direct_close_device()
```

**常见错误**

只在正常路径关闭设备，异常时遗漏清理；这会导致下一次程序无法打开设备。关联 API：`pynect_direct_open_device`、`pynect_direct_is_open`。

#### `pynect_direct_is_open`

**功能说明**

读取 DLL 当前设备状态，不与设备通信。

**适用范围**

UV/VIS、NIR；只读状态查询。

**调用前提**

无。

**输入参数**

无。

**输出参数**

无输出容器；状态直接通过函数返回值给出。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `1` | DLL 当前记录为设备已打开 |
| `0` | 当前没有打开的设备 |

**Python 示例**

```python
if sdk.pynect_direct_is_open() == 1:
    print("Device is open")
else:
    print("Device is closed")
```

**常见错误**

把返回值当作标准错误码，认为 `0` 表示成功。此函数的 `0` 表示“未打开”。关联 API：`pynect_direct_open_device`、`pynect_direct_close_device`。

#### `pynect_direct_set_protocol_preference`

**功能说明**

设置下一次打开设备时使用的协议偏好。`AUTO` 会自动识别设备；强制模式用于已确认型号但自动识别异常的现场。

**适用范围**

UV/VIS、NIR；临时连接设置；不写入设备。

**调用前提**

当前必须没有打开的设备。正常使用推荐自动模式。

**输入参数**

| 参数 | Python 类型 | 方向 | 允许值 | 说明 |
| --- | --- | --- | --- | --- |
| `protocol` | `int` / `ctypes.c_int` | 输入 | `0`、`1`、`2` | `0=AUTO`，`1=UVVIS_CM2`，`2=NIR_LEGACY` |

**输出参数**

无。设置只影响后续打开操作。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 偏好设置成功 |
| `-3` | 当前已有设备打开；应先关闭设备 |
| `-5` | `protocol` 不是 `0`、`1` 或 `2` |

**Python 示例**

```python
PROTOCOL_AUTO = 0
if sdk.pynect_direct_is_open():
    sdk.pynect_direct_close_device()
require_success(
    sdk.pynect_direct_set_protocol_preference(PROTOCOL_AUTO),
    "Set protocol preference",
)
```

**常见错误**

在设备已打开后设置偏好，或无依据地强制错误协议。关联 API：`pynect_direct_open_device`、`pynect_direct_get_device_protocol`。

#### `pynect_direct_get_device_protocol`

**功能说明**

读取当前已打开设备实际采用的协议类型，而不是打开前设置的偏好值。

**适用范围**

UV/VIS、NIR；只读。

**调用前提**

设备已成功打开。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 输出值 | 说明 |
| --- | --- | --- | --- | --- |
| `protocol` | `ctypes.c_int()` + `ctypes.byref()` | 输出 | `1`、`2`、可能的 `255` | `1=UVVIS_CM2`，`2=NIR_LEGACY`，`255=UNKNOWN` |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 输出变量已写入协议值 |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效 |

**Python 示例**

```python
import ctypes

protocol = ctypes.c_int()
require_success(
    sdk.pynect_direct_get_device_protocol(ctypes.byref(protocol)),
    "Get device protocol",
)
print("Protocol value:", protocol.value)
```

**常见错误**

忘记使用 `ctypes.byref(protocol)`，或在打开设备前查询。关联 API：`pynect_direct_set_protocol_preference`、`pynect_direct_get_device_info`。

#### `pynect_direct_get_capabilities`

**功能说明**

读取当前设备的 32 位 capability 位掩码。每一位表示一类功能是否可用。

**适用范围**

UV/VIS、NIR；只读；调用任何设备专用功能前都应执行。

**调用前提**

设备已成功打开并完成协议识别。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 宽度 | 说明 |
| --- | --- | --- | --- | --- |
| `capabilities` | `ctypes.c_uint32()` + `ctypes.byref()` | 输出 | 32 位无符号整数 | 按第 11.2 节列出的位值进行按位与检查 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | capability 值读取成功 |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效 |

**Python 示例**

```python
import ctypes

CAP_SPECTRUM_DATA = 0x00000020
capabilities = ctypes.c_uint32()
require_success(
    sdk.pynect_direct_get_capabilities(ctypes.byref(capabilities)),
    "Get capabilities",
)
if capabilities.value & CAP_SPECTRUM_DATA:
    print("Spectrum acquisition is supported")
```

**常见错误**

直接比较整个值是否等于某个 capability，而不是使用按位与；一台设备通常同时设置多个能力位。关联 API：`pynect_direct_get_device_info` 以及所有返回 `-11` 的设备专用接口。

#### `pynect_direct_get_device_info`

**功能说明**

一次读取协议、capability、设备型号、业务序列号和硬件版本。个别字符串读取失败时，对应字段可能为空，但函数仍可返回成功，因此应用应检查必要字段。

**适用范围**

UV/VIS、NIR；只读。

**调用前提**

设备已成功打开。Python 结构定义必须与 `minimal_test.py` 中的 `PynectDirectDeviceInfo` 一致。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 字段 | 说明 |
| --- | --- | --- | --- | --- |
| `info` | `PynectDirectDeviceInfo()` + `ctypes.byref()` | 输出 | `protocol` | 协议整数值 |
| `info.capabilities` | `ctypes.c_uint32` 字段 | 输出 | capability 位掩码 | 功能支持集合 |
| `info.protocol_name` | 32 字节字符数组 | 输出 | 协议名称 | 使用 `decode_char_array` 解码 |
| `info.model` | 32 字节字符数组 | 输出 | 设备型号 | 读取失败时可能为空 |
| `info.serial_number` | 32 字节字符数组 | 输出 | 业务序列号 | NIR 可能返回通用标识 |
| `info.hardware_version` | 64 字节字符数组 | 输出 | 硬件版本 | 读取失败时可能为空 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 基本信息对象已写入；仍应检查字符串字段是否为空 |
| `-3` | 设备未打开 |
| `-5` | `info` 输出对象无效 |

**Python 示例**

```python
import ctypes
from minimal_test import PynectDirectDeviceInfo, decode_char_array

info = PynectDirectDeviceInfo()
require_success(
    sdk.pynect_direct_get_device_info(ctypes.byref(info)),
    "Get device info",
)
print("Protocol:", decode_char_array(info.protocol_name))
print("Model:", decode_char_array(info.model))
print("Serial:", decode_char_array(info.serial_number))
print(f"Capabilities: 0x{info.capabilities:08X}")
```

**常见错误**

结构字段顺序或字符数组长度定义错误会导致错误数据；不要自行缩短结构定义。关联 API：`pynect_direct_get_device_protocol`、`pynect_direct_get_capabilities`、各单项身份接口。

### 15.2 临时测量参数

#### `pynect_direct_set_integration_time`

**功能说明**

设置当前会话的探测器积分时间。该值直接影响信号强度和单次采集耗时，不写入设备的默认固化配置。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_INTEGRATION_TIME`；临时设置。

**调用前提**

设备已打开。先读取并记录原积分时间，确认目标值符合具体设备允许范围。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `us` | `int` / `ctypes.c_uint` | 输入 | 微秒；建议大于 `0`，上限以设备资料为准 | `50000` 表示 50 ms；过大可能导致饱和和长时间等待 |

**输出参数**

无。成功后后续采谱使用新的积分时间。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 设置成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 通信、校验、数据格式或超时错误 |

**Python 示例**

```python
integration_us = 100_000
require_success(
    sdk.pynect_direct_set_integration_time(integration_us),
    "Set integration time",
)
```

**常见错误**

把毫秒值直接当微秒传入，例如希望 100 ms 却传 `100`；正确值是 `100000`。修改前不保存旧值也会影响后续实验。关联 API：`pynect_direct_get_integration_time`、`pynect_direct_set_stored_integration_time`。

#### `pynect_direct_get_integration_time`

**功能说明**

读取当前会话实际使用的积分时间。

**适用范围**

UV/VIS、NIR；要求积分时间能力；只读。

**调用前提**

设备已打开。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位 | 说明 |
| --- | --- | --- | --- | --- |
| `us` | `ctypes.c_uint()` + `ctypes.byref()` | 输出 | 微秒 | 32 位无符号积分时间值 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 输出变量已写入当前积分时间 |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效 |
| `-4/-7/-8/-9` | 设备返回异常或通信超时 |

**Python 示例**

```python
import ctypes

integration_us = ctypes.c_uint()
require_success(
    sdk.pynect_direct_get_integration_time(
        ctypes.byref(integration_us)
    ),
    "Get integration time",
)
print(f"Integration: {integration_us.value} us")
```

**常见错误**

传入整数而不是 `ctypes` 输出变量，或忘记 `ctypes.byref()`。该值是当前运行参数，不是固化默认值。关联 API：`pynect_direct_set_integration_time`、`pynect_direct_get_stored_integration_time`。

#### `pynect_direct_set_average_number`

**功能说明**

设置当前会话一次结果所使用的光谱平均次数。增加平均次数通常降低随机波动，同时增加采集时间。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_AVERAGE_NUMBER`；临时设置。

**调用前提**

设备已打开。先读取原值，并评估积分时间与平均次数共同造成的总等待时间。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `count` | `int` / `ctypes.c_uint8` | 输入 | 推荐 `1-255` | `1` 表示不做多次平均；不要使用 `0`，不同设备对 `0` 的处理可能不同 |

**输出参数**

无。成功后后续采谱使用新的平均次数。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 设置成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 通信或响应错误 |

**Python 示例**

```python
average_count = 3
require_success(
    sdk.pynect_direct_set_average_number(average_count),
    "Set average number",
)
```

**常见错误**

将平均次数设置为 `0`，或在长积分时间下使用很大的平均次数导致程序看似无响应。关联 API：`pynect_direct_get_average_number`、`pynect_direct_set_stored_average_number`。

#### `pynect_direct_get_average_number`

**功能说明**

读取当前会话的光谱平均次数。

**适用范围**

UV/VIS、NIR；要求平均次数能力；只读。

**调用前提**

设备已打开。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `count` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0-255` | 当前设备报告的平均次数；正常应用通常应为 `1-255` |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 输出变量已写入平均次数 |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效 |
| `-4/-7/-8/-9` | 通信或响应错误 |

**Python 示例**

```python
import ctypes

average_count = ctypes.c_uint8()
require_success(
    sdk.pynect_direct_get_average_number(
        ctypes.byref(average_count)
    ),
    "Get average number",
)
print("Average count:", average_count.value)
```

**常见错误**

使用 `ctypes.c_uint` 接收 8 位结果，或把运行时平均次数与固化默认值混淆。关联 API：`pynect_direct_set_average_number`、`pynect_direct_get_stored_average_number`。

#### `pynect_direct_set_level_output`

**功能说明**

设置设备数字电平输出位掩码。每一位对应的物理输出由具体设备定义。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_LEVEL_OUTPUT`；需谨慎，可能驱动外部电路。

**调用前提**

设备已打开，已确认接线、电平标准和每一位的用途。先读取原值以便恢复。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `level` | `int` / `ctypes.c_uint8` | 输入 | `0x00-0xFF` | 8 位输出掩码；每一位的含义以设备资料为准 |

**输出参数**

无。成功后物理输出可能立即变化。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 输出位设置成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 通信或设备响应错误 |

**Python 示例**

```python
ALLOW_LEVEL_OUTPUT = False
target_level = 0x01

if ALLOW_LEVEL_OUTPUT:
    require_success(
        sdk.pynect_direct_set_level_output(target_level),
        "Set level output",
    )
else:
    print("Level output skipped; enable only after checking wiring")
```

**常见错误**

把 `level` 当作单个逻辑值而忽略它是位掩码，或未确认外部负载就执行示例。关联 API：`pynect_direct_get_level_output`、设备接线说明。

#### `pynect_direct_get_level_output`

**功能说明**

读取当前数字电平输出位掩码。

**适用范围**

UV/VIS、NIR；要求电平输出能力；只读。

**调用前提**

设备已打开。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `level` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0x00-0xFF` | 当前 8 位输出掩码 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 输出变量已写入当前位掩码 |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效 |
| `-4/-7/-8/-9` | 通信或响应错误 |

**Python 示例**

```python
import ctypes

level = ctypes.c_uint8()
require_success(
    sdk.pynect_direct_get_level_output(ctypes.byref(level)),
    "Get level output",
)
print(f"Level mask: 0x{level.value:02X}")
```

**常见错误**

将返回的十进制数直接理解成某一路电平而不做按位检查。关联 API：`pynect_direct_set_level_output`。

### 15.3 波长与光谱

#### `pynect_direct_get_wavelength_count`

**功能说明**

读取当前设备完整光谱包含的数据点数量。该点数决定波长数组和强度数组的最小容量。

**适用范围**

UV/VIS、NIR；要求波长数据能力；只读。

**调用前提**

设备已打开。每次更换设备后都应重新读取，不能写死为某个型号的固定点数。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `count` | `ctypes.c_uint16()` + `ctypes.byref()` | 输出 | 数据点；`0-65535` | 成功后保存完整波长和完整强度的点数 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 点数读取成功；仍应检查 `count.value > 0` |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效或内存分配失败 |
| `-4/-7/-8/-9` | 数据长度异常、通信或超时错误 |

**Python 示例**

```python
import ctypes

point_count = ctypes.c_uint16()
require_success(
    sdk.pynect_direct_get_wavelength_count(ctypes.byref(point_count)),
    "Get wavelength count",
)
if point_count.value == 0:
    raise RuntimeError("Device returned zero wavelength points")
print("Points:", point_count.value)
```

**常见错误**

把函数返回值 `0` 当作点数；真正点数位于 `count.value`。关联 API：`pynect_direct_get_full_wavelength_data`、`pynect_direct_get_full_spectrum_data`。

#### `pynect_direct_get_full_wavelength_range`

**功能说明**

读取设备完整数据集的起始波长和结束波长，结果按从小到大返回。

**适用范围**

UV/VIS、NIR；要求波长数据能力；只读。

**调用前提**

设备已打开。结果用于验证用户请求波段是否合法。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位 | 说明 |
| --- | --- | --- | --- | --- |
| `start_nm` | `ctypes.c_double()` + `ctypes.byref()` | 输出 | nm | 完整范围的最小波长 |
| `end_nm` | `ctypes.c_double()` + `ctypes.byref()` | 输出 | nm | 完整范围的最大波长，应大于 `start_nm` |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 两个输出变量已写入 |
| `-3` | 设备未打开 |
| `-5` | 任一输出变量无效或内部数据分配失败 |
| `-4/-7/-8/-9` | 波长数据或通信异常 |

**Python 示例**

```python
import ctypes

start_nm = ctypes.c_double()
end_nm = ctypes.c_double()
require_success(
    sdk.pynect_direct_get_full_wavelength_range(
        ctypes.byref(start_nm), ctypes.byref(end_nm)
    ),
    "Get full wavelength range",
)
print(f"Range: {start_nm.value:.3f}-{end_nm.value:.3f} nm")
```

**常见错误**

将输出变量顺序写反，或把 nm 当作像素索引。关联 API：`pynect_direct_set_selected_wavelength_range`、`pynect_direct_get_full_wavelength_data`。

#### `pynect_direct_get_full_wavelength_data`

**功能说明**

读取每个探测器数据点对应的完整波长数组。该接口只读取波长坐标，不采集强度。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_WAVELENGTH_DATA`；只读。

**调用前提**

设备已打开；先调用 `pynect_direct_get_wavelength_count`，按返回点数创建 `ctypes.c_double` 数组。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `buffer` | `(ctypes.c_double * count)()` | 输入/输出 | `count` 个 double 元素 | DLL 将波长依次写入该数组 |
| `max_len` | `int` | 输入 | 数据点，不是字节数 | 必须至少等于完整波长点数 |

**输出参数**

成功后 `buffer[0:max_len]` 中的有效部分为波长值，单位 nm。实际有效元素数等于 `pynect_direct_get_wavelength_count` 返回值。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 波长数组读取成功 |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效、`max_len <= 0` 或容量小于实际点数 |
| `-4/-7/-8/-9` | 返回数据长度异常、通信或超时错误 |

**Python 示例**

```python
import ctypes

count = ctypes.c_uint16()
require_success(
    sdk.pynect_direct_get_wavelength_count(ctypes.byref(count)),
    "Get wavelength count",
)
wavelengths = (ctypes.c_double * count.value)()
require_success(
    sdk.pynect_direct_get_full_wavelength_data(
        wavelengths, count.value
    ),
    "Get full wavelength data",
)
print(wavelengths[0], wavelengths[count.value - 1])
```

**常见错误**

把 `max_len` 传成数组字节数，或使用 `ctypes.c_float` 数组。关联 API：`pynect_direct_get_wavelength_count`、`pynect_direct_get_full_spectrum_data`。

#### `pynect_direct_get_full_spectrum_data`

**功能说明**

触发或读取一次完整光谱采集，将每个波长点对应的原始强度写入数组。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_SPECTRUM_DATA`；只读采集。

**调用前提**

设备已打开；已按完整点数创建 `ctypes.c_uint16` 数组。普通软件采集时应确保外触发没有处于等待状态。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `buffer` | `(ctypes.c_uint16 * count)()` | 输入/输出 | `count` 个 16 位元素 | 接收原始光谱强度 |
| `max_len` | `int` | 输入 | 数据点，不是字节数 | 必须至少等于完整光谱点数 |

**输出参数**

成功后 `buffer[index]` 与波长数组的 `wavelengths[index]` 一一对应。每个强度值存储范围为 `0-65535`。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 完整强度数组采集成功 |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效、容量不足或内存分配失败 |
| `-4/-7/-8/-9` | 光谱格式、通信、校验或等待超时错误 |

**Python 示例**

```python
import ctypes

count = ctypes.c_uint16()
require_success(
    sdk.pynect_direct_get_wavelength_count(ctypes.byref(count)),
    "Get wavelength count",
)
intensities = (ctypes.c_uint16 * count.value)()
require_success(
    sdk.pynect_direct_get_full_spectrum_data(
        intensities, count.value
    ),
    "Get full spectrum data",
)
print("Intensity range:", min(intensities), max(intensities))
```

**常见错误**

使用字节数组接收 16 位强度，或在外触发等待状态下调用普通采谱接口。关联 API：`pynect_direct_get_full_wavelength_data`、积分时间和平均次数接口。

#### `pynect_direct_set_selected_wavelength_range`

**功能说明**

根据用户请求的波长边界选择一段连续像素窗口，供后续指定波段接口使用。该设置只保存在当前 DLL 会话中。

**适用范围**

UV/VIS、NIR；临时设置；不改变设备标定或物理波长范围。

**调用前提**

设备已打开；先读取完整波长范围。`start_nm < end_nm`，并且两个边界都位于完整范围内。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `start_nm` | `float` / `ctypes.c_double` | 输入 | nm；不小于完整起始波长 | 请求波段下限 |
| `end_nm` | `float` / `ctypes.c_double` | 输入 | nm；不大于完整结束波长 | 请求波段上限，必须大于 `start_nm` |

**输出参数**

无直接输出。实际匹配边界、像素索引和点数通过 `pynect_direct_get_selected_wavelength_range` 读取。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 已找到至少一个有效数据点并保存波段状态 |
| `-3` | 设备未打开 |
| `-5` | 边界顺序错误、超出完整范围、没有匹配点或内存分配失败 |
| `-4/-7/-8/-9` | 读取完整波长数据时发生通信错误 |

**Python 示例**

```python
request_start_nm = 900.0
request_end_nm = 950.0
require_success(
    sdk.pynect_direct_set_selected_wavelength_range(
        request_start_nm, request_end_nm
    ),
    "Set selected wavelength range",
)
```

**常见错误**

使用设备范围以外的边界，或认为设置后会永久改变硬件。设备关闭、重新打开或复位后需要重新设置。关联 API：`pynect_direct_get_full_wavelength_range`、`pynect_direct_get_selected_wavelength_range`。

#### `pynect_direct_get_selected_wavelength_range`

**功能说明**

读取最近一次指定波段设置的请求边界、实际像素边界、像素索引和数据点数。

**适用范围**

UV/VIS、NIR；只读。

**调用前提**

设备已打开，并已成功调用 `pynect_direct_set_selected_wavelength_range`。

**输入参数**

无普通输入值。

**输出参数**

| 字段 | Python 类型 | 方向 | 单位 | 说明 |
| --- | --- | --- | --- | --- |
| `out_range.request_start_nm` | `float` 字段 | 输出 | nm | 用户请求的起始边界 |
| `out_range.request_end_nm` | `float` 字段 | 输出 | nm | 用户请求的结束边界 |
| `out_range.actual_start_nm` | `float` 字段 | 输出 | nm | 第一个匹配像素的实际波长 |
| `out_range.actual_end_nm` | `float` 字段 | 输出 | nm | 最后一个匹配像素的实际波长 |
| `start_pixel` / `end_pixel` | 16 位整数字段 | 输出 | 像素索引 | 完整数组中的闭区间索引 |
| `count` | 16 位整数字段 | 输出 | 数据点 | 后续指定波段数组的最小容量 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 输出结构已写入 |
| `-1` | 当前会话尚未设置有效波段 |
| `-3` | 设备未打开 |
| `-5` | 输出对象无效 |

**Python 示例**

```python
import ctypes
from minimal_test import PynectDirectSelectedWavelengthRange

selected = PynectDirectSelectedWavelengthRange()
require_success(
    sdk.pynect_direct_get_selected_wavelength_range(
        ctypes.byref(selected)
    ),
    "Get selected wavelength range",
)
print(selected.actual_start_nm, selected.actual_end_nm, selected.count)
```

**常见错误**

未先设置波段，或使用请求边界计算数组长度；正确容量是 `selected.count`。关联 API：`pynect_direct_set_selected_wavelength_range`、两个指定波段数据接口。

#### `pynect_direct_get_selected_wavelength_data`

**功能说明**

读取当前指定波段内的波长数组。

**适用范围**

UV/VIS、NIR；只读。

**调用前提**

设备已打开，波段已设置，并已读取 `selected.count`。

**输入参数**

| 参数 | Python 类型 | 方向 | 容量 | 说明 |
| --- | --- | --- | --- | --- |
| `buffer` | `(ctypes.c_double * selected.count)()` | 输入/输出 | 至少 `selected.count` 个元素 | 接收波段内的 nm 值 |
| `max_len` | `int` | 输入 | 数据点 | 必须大于或等于 `selected.count` |

**输出参数**

成功后数组包含从 `actual_start_nm` 到 `actual_end_nm` 的连续波长点。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 波段波长数组读取成功 |
| `-1` | 未设置有效波段 |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效或容量不足 |
| `-4/-7/-8/-9` | 完整波长读取或通信错误 |

**Python 示例**

```python
selected_wavelengths = (ctypes.c_double * selected.count)()
require_success(
    sdk.pynect_direct_get_selected_wavelength_data(
        selected_wavelengths, selected.count
    ),
    "Get selected wavelength data",
)
print(selected_wavelengths[0], selected_wavelengths[selected.count - 1])
```

**常见错误**

用完整光谱点数代替 `selected.count` 虽不会必然失败，但会造成含义不清；容量过小会返回 `-5`。关联 API：`pynect_direct_get_selected_wavelength_range`。

#### `pynect_direct_get_selected_spectrum_data`

**功能说明**

采集一次完整光谱，并把当前指定波段对应的强度区间复制到输出数组。

**适用范围**

UV/VIS、NIR；只读采集。

**调用前提**

设备已打开，波段已设置，并已按 `selected.count` 创建 16 位强度数组。

**输入参数**

| 参数 | Python 类型 | 方向 | 容量 | 说明 |
| --- | --- | --- | --- | --- |
| `buffer` | `(ctypes.c_uint16 * selected.count)()` | 输入/输出 | 至少 `selected.count` 个元素 | 接收波段强度 |
| `max_len` | `int` | 输入 | 数据点 | 必须大于或等于 `selected.count` |

**输出参数**

成功后 `buffer[index]` 与 `pynect_direct_get_selected_wavelength_data` 返回数组的相同索引一一对应。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 波段强度采集成功 |
| `-1` | 未设置有效波段 |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效、容量不足或内存分配失败 |
| `-4/-7/-8/-9` | 完整光谱采集或通信错误 |

**Python 示例**

```python
selected_intensities = (ctypes.c_uint16 * selected.count)()
require_success(
    sdk.pynect_direct_get_selected_spectrum_data(
        selected_intensities, selected.count
    ),
    "Get selected spectrum data",
)
print(min(selected_intensities), max(selected_intensities))
```

**常见错误**

波长数组与强度数组使用了不同的波段状态，或修改波段后仍沿用旧 `count`。关联 API：`pynect_direct_set_selected_wavelength_range`、`pynect_direct_get_selected_wavelength_data`。

### 15.4 设备状态与身份信息

#### `pynect_direct_get_calibration_coefficients`

**功能说明**

读取设备保存的四个波长标定系数，通常按从低阶到高阶的顺序存入数组。该接口只读取系数，不修改标定。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_CALIBRATION`；只读。

**调用前提**

设备已打开。应用必须分配恰当的双精度数组；系数的物理解释应遵循设备标定资料。

**输入参数**

| 参数 | Python 类型 | 方向 | 容量 | 说明 |
| --- | --- | --- | --- | --- |
| `coeffs` | `(ctypes.c_double * 4)()` | 输入/输出 | 至少 4 个 double | 接收四个波长标定系数 |

**输出参数**

成功后可读取 `coeffs[0]` 到 `coeffs[3]`。本接口不返回系数数量，调用方必须固定分配 4 个元素。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 四个系数读取成功 |
| `-3` | 设备未打开 |
| `-5` | 输出数组无效 |
| `-4/-7/-8/-9` | 数据长度、校验、通信或超时错误 |

**Python 示例**

```python
import ctypes

coefficients = (ctypes.c_double * 4)()
require_success(
    sdk.pynect_direct_get_calibration_coefficients(coefficients),
    "Get calibration coefficients",
)
print(list(coefficients))
```

**常见错误**

只分配一个 `ctypes.c_double`，或自行用系数替换 SDK 已返回的波长数组。关联 API：`pynect_direct_get_full_wavelength_data`。

#### `pynect_direct_get_temperature`

**功能说明**

读取设备内部温度传感器值并转换为摄氏度浮点数。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_TEMPERATURE`；只读。

**调用前提**

设备已打开且能力位表明支持温度读取。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位 | 说明 |
| --- | --- | --- | --- | --- |
| `temp_c` | `ctypes.c_float()` + `ctypes.byref()` | 输出 | 摄氏度 | 当前设备报告的内部温度，可为小数或负数 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 温度读取成功 |
| `-3` | 设备未打开 |
| `-5` | 输出变量无效 |
| `-4/-7/-8/-9` | 返回长度、通信、校验或超时错误 |

**Python 示例**

```python
import ctypes

temperature_c = ctypes.c_float()
require_success(
    sdk.pynect_direct_get_temperature(ctypes.byref(temperature_c)),
    "Get temperature",
)
print(f"Temperature: {temperature_c.value:.2f} C")
```

**常见错误**

使用 `ctypes.c_double` 接收单精度输出，或将某些设备报告的固定值直接认定为环境温度。关联 API：`pynect_direct_get_capabilities`。

#### `pynect_direct_get_hardware_version`

**功能说明**

读取设备硬件版本并写入以 `\0` 结尾的字符串缓冲区。

**适用范围**

UV/VIS、NIR；要求硬件版本能力；只读。

**调用前提**

设备已打开。准备至少 2 字节的可写字符缓冲区，推荐 64 字节以避免截断。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `version_str` | `ctypes.create_string_buffer` | 输入/输出 | 推荐 64 字节 | 接收硬件版本字符串 |
| `max_len` | `int` | 输入 | 至少 `2` | 缓冲区总字节数，包含结尾 `\0` 空间 |

**输出参数**

成功后读取 `version_str.value` 并解码。UV/VIS 常见格式类似 `03.4D.43.63`；NIR 可能返回设备定义的版本文本。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 版本字符串读取成功 |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效或 `max_len <= 1` |
| `-4/-7/-8/-9` | 返回数据或通信异常 |

**Python 示例**

```python
import ctypes

version_buffer = ctypes.create_string_buffer(64)
require_success(
    sdk.pynect_direct_get_hardware_version(
        version_buffer, len(version_buffer)
    ),
    "Get hardware version",
)
print(version_buffer.value.decode("utf-8", errors="replace"))
```

**常见错误**

传入不可写的 Python 字符串，或 `max_len` 大于实际缓冲区容量。关联 API：`pynect_direct_get_device_info`。

#### `pynect_direct_get_serial_number`

**功能说明**

读取光谱仪业务序列号。该编号由设备保存，与 USB 芯片序列号用途不同。

**适用范围**

UV/VIS、NIR；只读。NIR 当前可能返回通用字符串 `NIR`，不保证是唯一编号。

**调用前提**

设备已打开。需要唯一绑定时，应同时记录芯片序列号。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `sn` | `ctypes.create_string_buffer` | 输入/输出 | 推荐 64 字节 | 接收设备业务序列号 |
| `max_len` | `int` | 输入 | 至少 `2` | 缓冲区总字节数，包含结尾空间 |

**输出参数**

成功后从 `sn.value` 读取字节串并解码。UV/VIS 最多读取设备保存的 12 字节内容；缓冲区较小时会截断。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 序列号或 NIR 通用标识读取成功 |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效或 `max_len <= 1` |
| `-4/-7/-8/-9` | UV/VIS 数据长度或通信异常 |

**Python 示例**

```python
import ctypes

serial_buffer = ctypes.create_string_buffer(64)
require_success(
    sdk.pynect_direct_get_serial_number(
        serial_buffer, len(serial_buffer)
    ),
    "Get device serial number",
)
print(serial_buffer.value.decode("utf-8", errors="replace"))
```

**常见错误**

把返回值用于多设备唯一绑定，而忽略 NIR 可能只返回 `NIR`。关联 API：`pynect_direct_get_chip_serial_by_index`。

#### `pynect_direct_get_detector_serial_number`

**功能说明**

读取 UV/VIS 设备中的探测器序列号，最多复制设备返回的 8 字节标识。

**适用范围**

UV/VIS；只读。NIR 返回 `-11`。

**调用前提**

设备已打开。必须提供有效可写缓冲区；推荐 16 或 64 字节。由于低层接口要求严格，禁止传空缓冲区或小于 2 的长度。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `sn` | `ctypes.create_string_buffer` | 输入/输出 | 推荐至少 16 字节 | 接收最多 8 字节探测器序列号及结尾字符 |
| `max_len` | `int` | 输入 | 至少 `2`，不得超过实际容量 | 缓冲区总字节数 |

**输出参数**

成功后从 `sn.value` 取得探测器标识并解码。内容是否可打印由设备数据决定。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 探测器序列号读取成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回长度或通信异常 |
| `-11` | 当前设备不是支持该功能的 UV/VIS 设备 |

**Python 示例**

```python
import ctypes

detector_serial = ctypes.create_string_buffer(16)
code = sdk.pynect_direct_get_detector_serial_number(
    detector_serial, len(detector_serial)
)
if code == -11:
    print("Detector serial is not supported")
else:
    require_success(code, "Get detector serial")
    print(detector_serial.value.decode("utf-8", errors="replace"))
```

**常见错误**

在 NIR 设备上把 `-11` 当作硬件故障，或传入长度为 0 的缓冲区。关联 API：`pynect_direct_get_capabilities`、`pynect_direct_get_serial_number`。

#### `pynect_direct_get_device_model`

**功能说明**

读取设备型号字符串。UV/VIS 返回设备保存的型号；NIR 根据可用硬件信息生成型号，信息不足时返回 `NIR`。

**适用范围**

UV/VIS、NIR；只读。

**调用前提**

设备已打开，并准备有效字符缓冲区。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/容量 | 说明 |
| --- | --- | --- | --- | --- |
| `model` | `ctypes.create_string_buffer` | 输入/输出 | 推荐 64 字节 | 接收型号字符串 |
| `max_len` | `int` | 输入 | 至少 `2` | 包含结尾字符空间的缓冲区容量 |

**输出参数**

成功后从 `model.value` 取得字节串。UV/VIS 最多使用设备返回的 16 字节型号内容。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 型号读取成功；NIR 无详细信息时也可能返回通用 `NIR` |
| `-3` | 设备未打开 |
| `-5` | 缓冲区无效或长度不足 |
| `-4/-7/-8/-9` | UV/VIS 返回数据或通信异常 |

**Python 示例**

```python
import ctypes

model_buffer = ctypes.create_string_buffer(64)
require_success(
    sdk.pynect_direct_get_device_model(
        model_buffer, len(model_buffer)
    ),
    "Get device model",
)
print(model_buffer.value.decode("utf-8", errors="replace"))
```

**常见错误**

把通用 `NIR` 文本误认为唯一型号，或忽略字符串可能被截断。关联 API：`pynect_direct_get_device_info`。

#### `pynect_direct_get_usb_status`

**功能说明**

读取 UV/VIS 设备内部报告的 USB 连接状态。它是设备状态字段，不替代 Windows 设备枚举结果。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_USB_STATUS`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持 USB 状态查询。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 输出值 | 说明 |
| --- | --- | --- | --- | --- |
| `connected` | `ctypes.c_int()` + `ctypes.byref()` | 输出 | `1` 或 `0` | `1` 表示设备报告已连接，`0` 表示未连接 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 状态变量已写入；连接状态应读取 `connected.value` |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据或通信异常 |
| `-11` | 当前设备不支持该查询 |

**Python 示例**

```python
import ctypes

connected = ctypes.c_int()
code = sdk.pynect_direct_get_usb_status(ctypes.byref(connected))
if code == -11:
    print("USB status query is not supported")
else:
    require_success(code, "Get USB status")
    print("Connected:", bool(connected.value))
```

**常见错误**

把函数返回的 `0` 理解为“未连接”；连接状态位于输出变量中。关联 API：`pynect_direct_scan_devices`、`pynect_direct_get_capabilities`。

### 15.5 氙灯、USB 参数与 DAC

#### `pynect_direct_set_xenon_pulse`

**功能说明**

设置氙灯脉冲的完整周期和一个周期内的高电平宽度。该函数会改变当前光源时序，不负责打开或关闭氙灯，也不会自动检查目标时序是否适合具体灯源。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_XENON`。NIR 返回 `-11`。这是光源控制接口，须由熟悉设备和光源安全要求的人员使用。

**调用前提**

设备已打开，capability 已确认。建议先用 `pynect_direct_get_xenon_mode` 确认氙灯处于关闭状态，再读取并记录原脉冲参数。目标值必须来自设备资料或实验室批准的配置。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `period_10ns` | Python `int`，按 `ctypes.c_uint` 传入 | 输入 | 10 ns；`0-4294967295` | 一个完整脉冲周期的计数值 |
| `high_10ns` | Python `int`，按 `ctypes.c_uint` 传入 | 输入 | 10 ns；`0-4294967295` | 一个周期内高电平持续时间；通常不得大于 `period_10ns` |

Python 传入前应自行检查设备允许范围。超出无符号 32 位范围的整数可能被截断，不能依赖 DLL 自动拒绝。

**输出参数**

无。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 脉冲参数设置成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备拒绝、返回校验异常或通信超时 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持氙灯控制 |

**Python 示例**

```python
ALLOW_XENON_CONFIGURATION = False

# 以下数值仅演示 10 ns 计数的写法，必须替换为设备批准值。
target_period_10ns = 10_000_000
target_high_10ns = 1_000_000

if ALLOW_XENON_CONFIGURATION:
    if not (0 < target_high_10ns <= target_period_10ns <= 0xFFFFFFFF):
        raise ValueError("Invalid xenon pulse timing")
    require_success(
        sdk.pynect_direct_set_xenon_pulse(
            target_period_10ns, target_high_10ns
        ),
        "Set xenon pulse",
    )
else:
    print("Skipped: xenon configuration is disabled")
```

**常见错误**

把参数单位误写成微秒或毫秒；高电平宽度大于周期；氙灯已点亮时直接改变时序；未记录原值。关联 API：`pynect_direct_get_xenon_pulse`、`pynect_direct_get_xenon_mode`、`pynect_direct_set_xenon_mode`。

#### `pynect_direct_get_xenon_pulse`

**功能说明**

读取当前氙灯脉冲周期和高电平宽度。函数只查询参数，不改变氙灯模式或输出状态。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_XENON`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持氙灯控制。准备两个独立的无符号 32 位输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `period` | `ctypes.c_uint()` + `ctypes.byref()` | 输出 | 10 ns；无符号 32 位 | 当前完整周期计数 |
| `high` | `ctypes.c_uint()` + `ctypes.byref()` | 输出 | 10 ns；无符号 32 位 | 当前高电平宽度计数 |

实际时间可按 `计数值 * 10 ns` 换算；换算为秒时乘以 `1e-8`。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 两个输出变量均已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持氙灯控制 |

**Python 示例**

```python
import ctypes

period = ctypes.c_uint()
high = ctypes.c_uint()
code = sdk.pynect_direct_get_xenon_pulse(
    ctypes.byref(period), ctypes.byref(high)
)
if code == -11:
    print("Xenon pulse query is not supported")
else:
    require_success(code, "Get xenon pulse")
    print("Period:", period.value, "x 10 ns")
    print("High level:", high.value, "x 10 ns")
    print("Period (s):", period.value * 1e-8)
```

**常见错误**

使用 `ctypes.c_uint16` 导致数据宽度不足；忘记 `ctypes.byref()`；把函数返回码当成周期。关联 API：`pynect_direct_set_xenon_pulse`。

#### `pynect_direct_set_xenon_mode`

**功能说明**

设置氙灯工作模式，可关闭氙灯、进入连续模式或请求单次模式。该函数可能直接改变光源输出状态。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_XENON`。NIR 返回 `-11`。除关闭模式外均属于需要现场确认的光源操作。

**调用前提**

设备已打开，氙灯脉冲参数已确认，光路和防护装置已就绪。应用应在 `finally` 清理流程或退出流程中关闭氙灯。

**输入参数**

| 参数 | Python 类型 | 方向 | 允许值 | 说明 |
| --- | --- | --- | --- | --- |
| `mode` | Python `int`，按 `ctypes.c_uint8` 传入 | 输入 | `0x00` | `XENON_OFF`，关闭 |
| `mode` | 同上 | 输入 | `0x01` | `XENON_CONTINUOUS`，连续模式 |
| `mode` | 同上 | 输入 | `0x81` | `XENON_SINGLE`，单次模式 |

不要传入表中未定义的模式值。

**输出参数**

无。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 模式命令执行成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备返回、校验或通信异常 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持氙灯控制 |

**Python 示例**

```python
XENON_OFF = 0x00
XENON_CONTINUOUS = 0x01
XENON_SINGLE = 0x81
ALLOW_XENON_OUTPUT = False

target_mode = XENON_OFF
if target_mode not in (XENON_OFF, XENON_CONTINUOUS, XENON_SINGLE):
    raise ValueError("Unknown xenon mode")

if target_mode == XENON_OFF or ALLOW_XENON_OUTPUT:
    require_success(
        sdk.pynect_direct_set_xenon_mode(target_mode),
        "Set xenon mode",
    )
else:
    print("Skipped: xenon output is disabled")
```

**常见错误**

把十进制 `81` 当作十六进制 `0x81`；程序异常退出后没有关闭氙灯；未先确认脉冲时序。关联 API：`pynect_direct_get_xenon_mode`、`pynect_direct_set_xenon_pulse`。

#### `pynect_direct_get_xenon_mode`

**功能说明**

读取当前氙灯模式。函数只查询状态，不会关闭或触发氙灯。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_XENON`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持氙灯控制。准备一个 8 位无符号输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 输出值 | 说明 |
| --- | --- | --- | --- | --- |
| `mode` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0x00` | 关闭 |
| `mode` | 同上 | 输出 | `0x01` | 连续模式 |
| `mode` | 同上 | 输出 | `0x81` | 单次模式 |

设备若返回其他值，应用应显示原始十六进制值，而不是自行猜测含义。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `mode.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持氙灯控制 |

**Python 示例**

```python
import ctypes

mode_names = {
    0x00: "off",
    0x01: "continuous",
    0x81: "single",
}
mode = ctypes.c_uint8()
code = sdk.pynect_direct_get_xenon_mode(ctypes.byref(mode))
if code == -11:
    print("Xenon mode query is not supported")
else:
    require_success(code, "Get xenon mode")
    print(mode_names.get(mode.value, f"unknown 0x{mode.value:02X}"))
```

**常见错误**

用 `ctypes.c_int` 替代 8 位输出容器，或把未知值默认当成关闭。关联 API：`pynect_direct_set_xenon_mode`。

#### `pynect_direct_set_usb_baud_rate`

**功能说明**

写入 UV/VIS 设备的 USB 通信速率参数。该参数是设备使用的无符号 16 位配置码，不保证与常见串口波特率数值一一对应。

**适用范围**

UV/VIS；设备专用维护接口。NIR 返回 `-11`。错误设置可能造成后续通信失败，不建议普通测量程序调用。

**调用前提**

设备已打开，已读取并保存原参数，并从设备供应方资料获得准确的目标配置码和恢复方法。仅凭常见波特率数值不得调用。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `rate` | Python `int`，按 `ctypes.c_uint16` 传入 | 输入 | `0-65535` | 设备 USB 速率原始参数，不直接表示 bit/s |

**输出参数**

无。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 参数写入命令成功；仍需验证后续通信 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备返回、校验或通信异常 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持该设置 |

**Python 示例**

```python
ALLOW_USB_PARAMETER_WRITE = False
approved_rate_parameter = None

if ALLOW_USB_PARAMETER_WRITE:
    if approved_rate_parameter is None:
        raise ValueError("Set the supplier-approved USB parameter first")
    if not 0 <= approved_rate_parameter <= 0xFFFF:
        raise ValueError("USB parameter must fit uint16")
    require_success(
        sdk.pynect_direct_set_usb_baud_rate(approved_rate_parameter),
        "Set USB rate parameter",
    )
else:
    print("Skipped: USB parameter write is disabled")
```

**常见错误**

直接传入 `115200` 等常见串口波特率并认为设备会自动换算；未保存原值；写入后没有立即验证设备通信。关联 API：`pynect_direct_get_usb_baud_rate`、`pynect_direct_get_usb_status`。

#### `pynect_direct_get_usb_baud_rate`

**功能说明**

读取 UV/VIS 设备保存的 USB 通信速率原始参数。该函数不会修改通信设置。

**适用范围**

UV/VIS；只读。NIR 返回 `-11`。

**调用前提**

设备已打开。准备一个无符号 16 位输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围 | 说明 |
| --- | --- | --- | --- | --- |
| `rate` | `ctypes.c_uint16()` + `ctypes.byref()` | 输出 | `0-65535` | 设备 USB 速率原始参数 |

除非设备资料明确给出换算关系，否则只记录 `rate.value`，不要在界面中加上 bit/s、baud 等单位。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `rate.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持该查询 |

**Python 示例**

```python
import ctypes

rate_parameter = ctypes.c_uint16()
code = sdk.pynect_direct_get_usb_baud_rate(
    ctypes.byref(rate_parameter)
)
if code == -11:
    print("USB rate parameter is not supported")
else:
    require_success(code, "Get USB rate parameter")
    print("Raw USB rate parameter:", rate_parameter.value)
```

**常见错误**

给原始值附加未经确认的波特率单位；把返回码 `0` 当成查询结果；输出变量类型错误。关联 API：`pynect_direct_set_usb_baud_rate`。

#### `pynect_direct_set_dac_voltage`

**功能说明**

设置设备 DAC 的 12 位原始输出码。函数名中的 `voltage` 不表示参数单位是伏特；实际电压取决于设备的基准电压、输出电路和标定关系。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_DAC`。NIR 返回 `-11`。该接口会改变硬件输出，调用前必须确认外部负载和允许电压范围。

**调用前提**

设备已打开，DAC capability 已确认，外部连接安全，并已根据设备资料把目标电压换算成批准的原始码值。建议先读取并记录当前码值。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/单位 | 说明 |
| --- | --- | --- | --- | --- |
| `dac_value` | Python `int`，按 `ctypes.c_uint16` 传入 | 输入 | 原始码 `0-4095` | 12 位 DAC 码值，不是 mV 或 V |

尽管 ABI 容器可表示到 `65535`，应用层必须限制到 12 位范围。

**输出参数**

无。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | DAC 码值设置成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备返回、校验或通信异常 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持 DAC |

**Python 示例**

```python
ALLOW_DAC_OUTPUT = False
approved_dac_code = 0

if ALLOW_DAC_OUTPUT:
    if not 0 <= approved_dac_code <= 4095:
        raise ValueError("DAC code must be in the 12-bit range")
    require_success(
        sdk.pynect_direct_set_dac_voltage(approved_dac_code),
        "Set DAC code",
    )
else:
    print("Skipped: DAC output is disabled")
```

**常见错误**

把 `3.3`、`3300` 等电压值直接传入；只按 `uint16` 范围检查而允许大于 `4095`；未确认负载。关联 API：`pynect_direct_get_dac_voltage`、`pynect_direct_get_capabilities`。

#### `pynect_direct_get_dac_voltage`

**功能说明**

读取当前 DAC 的 12 位原始码值。函数只返回设备码值，不执行电压换算。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_DAC`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持 DAC。准备一个无符号 16 位输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围/单位 | 说明 |
| --- | --- | --- | --- | --- |
| `dac_value` | `ctypes.c_uint16()` + `ctypes.byref()` | 输出 | 原始码 `0-4095` | 当前 12 位 DAC 码值 |

若需要显示电压，必须使用设备资料规定的换算和标定参数，不能只按 `码值 / 4095` 猜测。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `dac_value.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持 DAC |

**Python 示例**

```python
import ctypes

dac_code = ctypes.c_uint16()
code = sdk.pynect_direct_get_dac_voltage(ctypes.byref(dac_code))
if code == -11:
    print("DAC query is not supported")
else:
    require_success(code, "Get DAC code")
    print("Raw 12-bit DAC code:", dac_code.value)
```

**常见错误**

把码值直接显示为伏特；使用有符号 16 位容器；把函数返回码当作 DAC 数据。关联 API：`pynect_direct_set_dac_voltage`。

### 15.6 外触发采集

#### `pynect_direct_set_external_trigger_config`

**功能说明**

设置外触发开关、触发类型和计划采集帧数。该函数改变当前设备的采集触发方式；配置错误时，普通采谱可能一直等待外部信号或超时。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_EXTERNAL_TRIGGER`。NIR 返回 `-11`。这是临时采集配置，修改前应保存原配置并在实验结束后恢复。

**调用前提**

设备已打开，外部触发 capability 已确认，触发线、电平和时序符合设备资料。课堂基础实验应保持外触发关闭。

**输入参数**

| 参数 | Python 类型 | 方向 | 常用值/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `enable` | Python `int`，按 `ctypes.c_uint8` 传入 | 输入 | `0x00`、`0xAA` | `0x00` 为 `TRIGGER_OFF`；`0xAA` 为 SDK 定义的 `TRIGGER_ON_RISE` |
| `trig_type` | Python `int`，按 `ctypes.c_uint8` 传入 | 输入 | 常用 `0xBB` | `0xBB` 为 `TRIGGER_ON_LEVEL`；其他取值必须以设备资料为准 |
| `scan_num` | Python `int`，按 `ctypes.c_uint8` 传入 | 输入 | `1-255` 帧 | 计划采集帧数；`0` 的含义依设备而定，不建议使用 |

三个参数在 ABI 中都是无符号 8 位值。SDK 不替应用验证组合是否适合具体固件。

**输出参数**

无。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 外触发配置命令成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备返回、校验或通信异常 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持外触发 |

**Python 示例**

```python
import ctypes

TRIGGER_OFF = 0x00
TRIGGER_ON_LEVEL = 0xBB
ALLOW_TRIGGER_CONFIGURATION = False

old_enable = ctypes.c_uint8()
old_type = ctypes.c_uint8()
old_scan_num = ctypes.c_uint8()
require_success(
    sdk.pynect_direct_get_external_trigger_config(
        ctypes.byref(old_enable),
        ctypes.byref(old_type),
        ctypes.byref(old_scan_num),
    ),
    "Read trigger configuration before changing it",
)

if ALLOW_TRIGGER_CONFIGURATION:
    require_success(
        sdk.pynect_direct_set_external_trigger_config(
            TRIGGER_OFF, TRIGGER_ON_LEVEL, 1
        ),
        "Disable external trigger",
    )
else:
    print("Skipped: trigger configuration is disabled")
```

**常见错误**

修改前未保存三个原值；把 `scan_num` 当成字节数；不了解接线就启用外触发；实验结束后未恢复配置。关联 API：`pynect_direct_get_external_trigger_config`、`pynect_direct_get_spectrum_capture_status`。

#### `pynect_direct_get_external_trigger_config`

**功能说明**

读取当前外触发开关值、触发类型值和计划采集帧数。函数只查询配置，不会启动采集。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_EXTERNAL_TRIGGER`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持外触发。准备三个独立的无符号 8 位输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 输出范围 | 说明 |
| --- | --- | --- | --- | --- |
| `enable` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0-255` | 外触发开关原始值；常见 `0x00` 或 `0xAA` |
| `trig_type` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0-255` | 触发类型原始值；常见电平类型为 `0xBB` |
| `scan_num` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0-255` | 设备报告的计划帧数 |

未知原始值应以十六进制记录并查询对应设备资料。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 三个输出变量均已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持外触发 |

**Python 示例**

```python
import ctypes

enable = ctypes.c_uint8()
trigger_type = ctypes.c_uint8()
scan_num = ctypes.c_uint8()
code = sdk.pynect_direct_get_external_trigger_config(
    ctypes.byref(enable),
    ctypes.byref(trigger_type),
    ctypes.byref(scan_num),
)
if code == -11:
    print("External trigger is not supported")
else:
    require_success(code, "Get external trigger configuration")
    print(f"Enable: 0x{enable.value:02X}")
    print(f"Type: 0x{trigger_type.value:02X}")
    print("Scan frames:", scan_num.value)
```

**常见错误**

只读取一个输出变量；忘记 `ctypes.byref()`；看到未知值后自行套用其他型号设备的含义。关联 API：`pynect_direct_set_external_trigger_config`。

#### `pynect_direct_get_spectrum_capture_status`

**功能说明**

读取外触发采集的 64 位原始状态字，用于判断设备当前采集状态或已完成情况。SDK 返回完整状态值，但不替应用解释各状态位。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_EXTERNAL_TRIGGER`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持外触发。若要判断具体位含义，必须准备对应型号和固件版本的设备资料。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围/格式 | 说明 |
| --- | --- | --- | --- | --- |
| `status` | `ctypes.c_uint64()` + `ctypes.byref()` | 输出 | `0-0xFFFFFFFFFFFFFFFF` | 64 位原始采集状态字 |

在没有位定义资料时，可记录十六进制值用于诊断，不应仅凭某次观察给各位命名。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `status.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持外触发状态查询 |

**Python 示例**

```python
import ctypes

capture_status = ctypes.c_uint64()
code = sdk.pynect_direct_get_spectrum_capture_status(
    ctypes.byref(capture_status)
)
if code == -11:
    print("Capture status is not supported")
else:
    require_success(code, "Get capture status")
    print(f"Capture status: 0x{capture_status.value:016X}")
```

**常见错误**

使用 `ctypes.c_uint32` 丢失高 32 位；把函数返回码当成状态值；没有设备位定义就把非零值解释为“已完成”。关联 API：`pynect_direct_get_external_trigger_config`、`pynect_direct_external_spectrum_control`。

#### `pynect_direct_external_spectrum_control`

**功能说明**

请求并读取指定外触发帧的原始光谱强度。缓冲区中每个数据点是无符号 16 位整数，但公开参数按字节指针传入；函数成功后返回写入的总字节数，而不是标准成功码 `0`。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_EXTERNAL_TRIGGER` 和光谱数据能力；只读采集。NIR 返回 `-11`。

**调用前提**

设备已打开；外触发已按设备资料配置；目标帧已经采集完成；`frame_index` 的起始编号已按设备资料确认。先查询完整光谱点数，并按该点数分配 16 位数组。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `frame_index` | Python `int`，按 `ctypes.c_uint16` 传入 | 输入 | `0-65535`，帧编号 | 要读取的外触发帧；是否从 0 开始以设备资料为准 |
| `data_out` | `(ctypes.c_uint16 * point_capacity)()` 后转换为 `POINTER(c_uint8)` | 输入/输出 | `point_capacity` 个 16 位元素 | 接收原始强度；虽然参数是字节指针，实际按 16 位光谱点写入 |
| `max_len` | Python `int` | 输入 | 数据点 | 缓冲区最多容纳的光谱点数，不是字节数 |

缓冲区实际字节容量必须至少为 `max_len * 2`。不要把 `max_len` 传成字节容量。

**输出参数**

成功时，前 `返回值 / 2` 个 `ctypes.c_uint16` 元素有效。每个元素范围为 `0-65535`，并应与设备的波长数组按索引对应。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `>= 0` | 成功读取的字节数；有效点数为 `返回值 // 2`，正常结果应为偶数 |
| `-3` | 设备未打开 |
| `-4` | 设备报告帧读取失败、状态异常或返回数据异常 |
| `-5` | 内存或缓冲区相关参数无效 |
| `-7/-8` | 数据校验或包头异常 |
| `-9` | 等待指定帧准备完成时超时 |
| `-11` | 当前设备不支持外触发帧读取 |

**Python 示例**

```python
import ctypes

frame_index = 0
point_count = ctypes.c_uint16()
require_success(
    sdk.pynect_direct_get_wavelength_count(ctypes.byref(point_count)),
    "Get wavelength count",
)

intensities = (ctypes.c_uint16 * point_count.value)()
byte_pointer = ctypes.cast(
    intensities, ctypes.POINTER(ctypes.c_uint8)
)
byte_count = sdk.pynect_direct_external_spectrum_control(
    frame_index, byte_pointer, point_count.value
)
if byte_count < 0:
    raise RuntimeError(f"Read external frame failed: {byte_count}")
if byte_count % 2 != 0:
    raise RuntimeError("Device returned an odd spectrum byte count")

valid_points = byte_count // 2
values = list(intensities[:valid_points])
print("External frame points:", valid_points)
print("First intensity values:", values[:5])
```

**常见错误**

成功时仍要求返回 `0`；按字节数分配 `c_uint8` 数组后直接当 16 位强度使用；把 `max_len` 传成数组字节数；目标帧未完成就读取。关联 API：`pynect_direct_get_wavelength_count`、`pynect_direct_get_spectrum_capture_status`、完整波长数据接口。

### 15.7 Flash、写保护和设备复位

#### `pynect_direct_set_flash_write_protect`

**功能说明**

向设备发送 Flash/FLS 写保护流程使用的设备定义状态命令。该函数没有布尔参数，不是通用的“打开保护”或“关闭保护”开关；具体作用必须结合设备型号的维护资料理解。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_FLASH`；高风险维护接口。普通测量、课堂实验和自动启动程序不得调用。

**调用前提**

设备已打开，已取得设备供应方或实验室管理员的明确维护授权，并已确认该型号对本命令的定义、当前状态和恢复流程。

**输入参数**

无。

**输出参数**

无。命令执行后的状态应另用 `pynect_direct_get_flash_write_protect` 查询。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 设备已接受状态命令；不等于任何 Flash 数据已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备拒绝、返回校验异常或通信超时 |
| `-10` | 设备报告写保护相关拒绝 |

**Python 示例**

```python
import ctypes

ALLOW_FLASH_MAINTENANCE = False

if ALLOW_FLASH_MAINTENANCE:
    before = ctypes.c_int()
    require_success(
        sdk.pynect_direct_get_flash_write_protect(ctypes.byref(before)),
        "Read Flash state before maintenance command",
    )
    print("State before command:", before.value)

    require_success(
        sdk.pynect_direct_set_flash_write_protect(),
        "Set device-defined Flash protection state",
    )
else:
    print("Skipped: Flash maintenance is disabled")
```

**常见错误**

根据英文函数名猜测它一定会“锁定”或“解锁”设备；未查询原状态；把返回 `0` 当成 Flash 写入成功。关联 API：`pynect_direct_get_flash_write_protect`、`pynect_direct_flash_write`。

#### `pynect_direct_get_flash_write_protect`

**功能说明**

读取设备报告的 Flash/FLS 写保护流程状态，并转换成 `1` 或 `0`。该布尔值只表示设备状态标记是否处于已启用值，不应脱离设备维护资料解释为“当前一定可写”或“当前一定不可写”。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_FLASH`；只读。

**调用前提**

设备已打开。准备一个 `ctypes.c_int` 输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 输出值 | 说明 |
| --- | --- | --- | --- | --- |
| `enabled` | `ctypes.c_int()` + `ctypes.byref()` | 输出 | `1` | 设备报告保护流程状态标记已启用 |
| `enabled` | 同上 | 输出 | `0` | 设备报告该状态标记未启用 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `enabled.value` 已写入；状态值不在函数返回码中 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-5` | 输出指针无效 |

**Python 示例**

```python
import ctypes

flash_state = ctypes.c_int()
require_success(
    sdk.pynect_direct_get_flash_write_protect(
        ctypes.byref(flash_state)
    ),
    "Get Flash protection state",
)
print("Device state flag enabled:", bool(flash_state.value))
```

**常见错误**

把函数返回的 `0` 当成“保护关闭”；看到输出 `1` 就在没有授权的情况下执行写入；使用 `c_uint8` 代替 `c_int`。关联 API：`pynect_direct_set_flash_write_protect`。

#### `pynect_direct_reset_device`

**功能说明**

向当前设备发送复位命令。复位可能中断正在进行的采集，并使临时参数或当前设备状态发生变化；DLL 中已选择的波长范围会失效，不能直接继续调用指定波段读取接口。

**适用范围**

UV/VIS、NIR；高风险设备控制接口。仅用于明确的故障恢复或维护流程。

**调用前提**

设备已打开；采集已停止；未保存的测量结果已处理；应用已准备在复位后重新确认设备是否在线、重新查询 capability，并恢复所需临时参数和波长范围。

**输入参数**

无。

**输出参数**

无。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 复位命令发送成功；仍需执行复位后检查 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备返回、校验或通信异常 |

**Python 示例**

```python
ALLOW_DEVICE_RESET = False

if ALLOW_DEVICE_RESET:
    require_success(
        sdk.pynect_direct_reset_device(),
        "Reset device",
    )
    print("Reset sent; recheck connection, settings, and wavelength range")
else:
    print("Skipped: device reset is disabled")
```

**常见错误**

采集过程中复位；复位成功后立即读取指定波段而没有重新设置范围；把命令发送成功等同于设备已经完全恢复。关联 API：`pynect_direct_is_open`、`pynect_direct_get_capabilities`、`pynect_direct_set_selected_wavelength_range`。

#### `pynect_direct_flash_read`

**功能说明**

读取指定 Flash/FLS 扇区的原始字节。函数不会解析标定、身份或配置字段；成功时直接返回实际读取字节数，而不是标准成功码 `0`。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_FLASH`；只读维护接口。读取到的数据可能包含重要设备配置，必须按设备资料解释。

**调用前提**

设备已打开，扇区编号已由设备资料确认，调用者已按字节容量分配输出数组。UV/VIS 单次有效读取上限约为 `1024` 字节；更大的内容应按设备规定分段读取。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `sector` | Python `int`，按 `ctypes.c_uint16` 传入 | 输入 | `0-65535`，扇区编号 | 只能使用设备资料明确允许读取的扇区 |
| `data_out` | `(ctypes.c_uint8 * max_len)()` | 输入/输出 | `max_len` 字节 | 接收原始 Flash/FLS 内容 |
| `max_len` | Python `int` | 输入 | 字节，必须大于 `0` | 输出缓冲区容量；不是 16 位数据点数量 |

**输出参数**

成功后 `data_out[0:返回值]` 有效。未读取到的数组尾部仍是初始化值，不属于设备返回内容。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `>= 0` | 实际读取字节数；只处理该长度内的数据 |
| `-3` | 设备未打开 |
| `-4` | 设备报告读取失败或返回数据异常 |
| `-5` | 缓冲区无效、长度不大于 0 或内存分配失败 |
| `-7/-8` | 数据校验或包头异常 |
| `-9` | 等待 Flash 读取完成时超时 |

**Python 示例**

```python
import ctypes

# 扇区编号必须替换为设备资料明确允许读取的值。
approved_sector = 0
buffer_capacity = 1024
buffer = (ctypes.c_uint8 * buffer_capacity)()

bytes_read = sdk.pynect_direct_flash_read(
    approved_sector, buffer, buffer_capacity
)
if bytes_read < 0:
    raise RuntimeError(f"Flash read failed: {bytes_read}")

payload = bytes(buffer[:bytes_read])
print("Flash bytes read:", bytes_read)
print("First bytes:", payload[:16].hex(" "))
```

**常见错误**

成功时要求返回 `0`；把 `max_len` 当成数据点数；解析整个缓冲区而不是前 `bytes_read` 字节；未经资料确认就猜测扇区含义。关联 API：`pynect_direct_get_flash_write_protect`、`pynect_direct_get_capabilities`。

#### `pynect_direct_flash_write`

**功能说明**

把原始字节写入指定 Flash/FLS 扇区。错误的扇区、长度或数据可能破坏设备标定、身份信息或启动配置，导致设备测量错误或无法使用。

**适用范围**

UV/VIS、NIR；要求 `PYNECT_DIRECT_CAP_FLASH`；最高风险维护接口。学生实验禁止调用，普通应用不应提供此功能入口。

**调用前提**

设备已打开；已获得书面维护授权；目标扇区和精确数据格式已由设备供应方确认；原扇区已有可验证备份；电源和 USB 连接稳定；写后校验及失败恢复方案已准备完成。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `sector` | Python `int`，按 `ctypes.c_uint16` 传入 | 输入 | `0-65535`，扇区编号 | 必须是维护资料明确授权的目标扇区 |
| `data` | `(ctypes.c_uint8 * len)(...)` | 输入 | `len` 字节 | 待写入的完整原始字节，不得使用试验数据 |
| `len` | Python `int` | 输入 | 字节，必须大于 `0` | 必须等于数组中有效数据的准确长度，并符合设备单次写入限制 |

**输出参数**

无。写入成功后应使用设备规定的只读方式重新读取并核对内容；不要仅依赖返回码判断长期保存结果。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 写入流程完成 |
| `-3` | 设备未打开 |
| `-4` | 设备拒绝写入、完成状态异常或数据返回异常 |
| `-5` | 数据为空、长度不大于 0 或内存分配失败 |
| `-7/-8` | 数据校验或包头异常 |
| `-9` | 等待写入完成时超时 |
| `-10` | 设备报告写保护，写入未获允许 |

**Python 示例**

```python
import ctypes

ALLOW_FLASH_WRITE = False
approved_sector = None
approved_payload = None

if ALLOW_FLASH_WRITE:
    if approved_sector is None or approved_payload is None:
        raise ValueError("Approved sector and payload are required")
    if not 0 <= approved_sector <= 0xFFFF:
        raise ValueError("Flash sector must fit uint16")

    payload_bytes = bytes(approved_payload)
    if not payload_bytes:
        raise ValueError("Flash payload must not be empty")
    data = (ctypes.c_uint8 * len(payload_bytes)).from_buffer_copy(
        payload_bytes
    )
    require_success(
        sdk.pynect_direct_flash_write(
            approved_sector, data, len(payload_bytes)
        ),
        "Write approved Flash payload",
    )
else:
    print("Skipped: Flash write is disabled")
```

**常见错误**

用全零或随机数据测试写接口；扇区编号偏移一位；`len` 与有效数组长度不一致；没有备份和写后校验；出现超时后立即重复写入。关联 API：`pynect_direct_flash_read`、`pynect_direct_get_flash_write_protect`。

### 15.8 固化配置与狭缝信息

#### `pynect_direct_set_stored_integration_time`

**功能说明**

把默认积分时间写入设备的固化配置。它不同于只影响当前会话的 `pynect_direct_set_integration_time`，可能改变设备以后上电或初始化时采用的默认值。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_STORED_CONFIG`；高风险持久配置接口。NIR 返回 `-11`。

**调用前提**

设备已打开；设备型号和允许的积分时间范围已确认；已读取并记录原固化值；已获得教师、实验室管理员或设备维护人员授权。

**输入参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `us` | Python `int`，按 `ctypes.c_uint` 传入 | 输入 | 微秒；ABI 范围 `0-4294967295` | 要保存的默认积分时间；应用应限制为设备允许的正整数范围 |

ABI 可表示范围不等于设备有效范围。超出设备资料范围的值不得通过试错方式写入。

**输出参数**

无。写入后应调用对应读取函数核对。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 固化积分时间写入命令成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备拒绝、返回校验异常或通信超时 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持固化配置 |

**Python 示例**

```python
import ctypes

ALLOW_STORED_CONFIG_WRITE = False
approved_integration_us = None

if ALLOW_STORED_CONFIG_WRITE:
    if approved_integration_us is None:
        raise ValueError("An approved integration time is required")
    if not 0 < approved_integration_us <= 0xFFFFFFFF:
        raise ValueError("Stored integration time must fit uint32")

    old_value = ctypes.c_uint()
    require_success(
        sdk.pynect_direct_get_stored_integration_time(
            ctypes.byref(old_value)
        ),
        "Read stored integration time",
    )
    print("Original stored value:", old_value.value, "us")
    require_success(
        sdk.pynect_direct_set_stored_integration_time(
            approved_integration_us
        ),
        "Set stored integration time",
    )
else:
    print("Skipped: stored configuration write is disabled")
```

**常见错误**

误以为该设置只对当前采集有效；单位使用毫秒；未记录原值；用 ABI 最大范围代替设备有效范围。关联 API：`pynect_direct_get_stored_integration_time`、`pynect_direct_set_integration_time`。

#### `pynect_direct_set_stored_average_number`

**功能说明**

把默认光谱平均次数写入设备的固化配置。较大的默认平均次数会增加以后采集的总耗时，且可能影响依赖响应时间的实验流程。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_STORED_CONFIG`；高风险持久配置接口。NIR 返回 `-11`。

**调用前提**

设备已打开；已确认设备允许范围并记录原固化值；已评估目标平均次数对采集时间的影响；已获得维护授权。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/单位 | 说明 |
| --- | --- | --- | --- | --- |
| `count` | Python `int`，按 `ctypes.c_uint8` 传入 | 输入 | 通常 `1-255` 次 | 要保存的默认平均次数；准确有效范围以设备资料为准 |

不建议写入 `0`，除非具体设备资料明确规定其含义。

**输出参数**

无。写入后应调用对应读取函数核对。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | 固化平均次数写入命令成功 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 设备拒绝、返回校验异常或通信超时 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前设备不支持固化配置 |

**Python 示例**

```python
import ctypes

ALLOW_STORED_CONFIG_WRITE = False
approved_average_count = None

if ALLOW_STORED_CONFIG_WRITE:
    if approved_average_count is None:
        raise ValueError("An approved average count is required")
    if not 1 <= approved_average_count <= 255:
        raise ValueError("Stored average count must be 1-255")

    old_value = ctypes.c_uint8()
    require_success(
        sdk.pynect_direct_get_stored_average_number(
            ctypes.byref(old_value)
        ),
        "Read stored average count",
    )
    print("Original stored average:", old_value.value)
    require_success(
        sdk.pynect_direct_set_stored_average_number(
            approved_average_count
        ),
        "Set stored average count",
    )
else:
    print("Skipped: stored configuration write is disabled")
```

**常见错误**

把平均次数当作毫秒；写入 `0` 而未确认设备含义；忽略平均次数会成倍增加采集时间；把固化值与当前会话值混淆。关联 API：`pynect_direct_get_stored_average_number`、`pynect_direct_set_average_number`。

#### `pynect_direct_set_stored_smoothing_width`

**功能说明**

把默认平滑宽度写入设备的固化配置。平滑会影响光谱细节和峰形，错误参数可能降低后续测量的光谱分辨表现。

**适用范围**

仅应在报告 `PYNECT_DIRECT_CAP_STORED_CONFIG` 的 UV/VIS 设备上使用；高风险持久配置接口。当前库对 NIR 调用可能返回 `0` 但不实际应用设置，因此 NIR 程序必须依靠 capability 检查跳过此函数，不能仅根据返回码判断是否生效。

**调用前提**

设备已打开；capability 已确认；平滑宽度的允许范围和定义已从设备资料取得；已记录原固化值并获得维护授权。

**输入参数**

| 参数 | Python 类型 | 方向 | 范围/单位 | 说明 |
| --- | --- | --- | --- | --- |
| `width` | Python `int`，按 `ctypes.c_uint8` 传入 | 输入 | ABI 范围 `0-255`，设备定义的宽度值 | 准确有效范围、是否允许 `0` 以及窗口含义均以设备资料为准 |

**输出参数**

无。UV/VIS 写入后应调用对应读取函数核对；NIR 的成功返回不得视为已应用。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | UV/VIS 写入命令成功；NIR 当前也可能返回 `0` 但不执行设置 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | UV/VIS 设备拒绝、返回校验异常或通信超时 |
| `-10` | 设备报告当前写操作受保护 |
| `-11` | 当前协议不是受支持的 UV/VIS 或兼容 NIR 路径 |

**Python 示例**

```python
import ctypes

CAP_STORED_CONFIG = 0x00002000
ALLOW_STORED_CONFIG_WRITE = False
approved_smoothing_width = None

capabilities = ctypes.c_uint32()
require_success(
    sdk.pynect_direct_get_capabilities(ctypes.byref(capabilities)),
    "Get capabilities",
)

if not capabilities.value & CAP_STORED_CONFIG:
    print("Skipped: stored smoothing is not supported")
elif ALLOW_STORED_CONFIG_WRITE:
    if approved_smoothing_width is None:
        raise ValueError("An approved smoothing width is required")
    if not 0 <= approved_smoothing_width <= 255:
        raise ValueError("Smoothing width must fit uint8")
    require_success(
        sdk.pynect_direct_set_stored_smoothing_width(
            approved_smoothing_width
        ),
        "Set stored smoothing width",
    )
else:
    print("Skipped: stored configuration write is disabled")
```

**常见错误**

在 NIR 上看到返回 `0` 就认为设置已保存；未检查 capability；用试错方式寻找宽度范围；忽略平滑对峰形的影响。关联 API：`pynect_direct_get_stored_smoothing_width`、`pynect_direct_get_capabilities`。

#### `pynect_direct_get_stored_integration_time`

**功能说明**

读取设备固化配置中的默认积分时间。该值不一定等于当前会话正在使用的积分时间。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_STORED_CONFIG`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持固化配置。准备一个与无符号 32 位参数匹配的 `ctypes.c_uint` 输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `us` | `ctypes.c_uint()` + `ctypes.byref()` | 输出 | 微秒；无符号 32 位 | 设备保存的默认积分时间 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `us.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持固化配置读取 |

**Python 示例**

```python
import ctypes

stored_us = ctypes.c_uint()
code = sdk.pynect_direct_get_stored_integration_time(
    ctypes.byref(stored_us)
)
if code == -11:
    print("Stored integration time is not supported")
else:
    require_success(code, "Get stored integration time")
    print("Stored integration time:", stored_us.value, "us")
```

**常见错误**

使用 `c_uint8` 或 `c_uint16` 截断数值；把固化默认值当成当前实际值；误把单位当成毫秒。关联 API：`pynect_direct_get_integration_time`、`pynect_direct_set_stored_integration_time`。

#### `pynect_direct_get_stored_average_number`

**功能说明**

读取设备固化配置中的默认平均次数。该值用于了解设备保存的启动配置，不一定等于当前会话的平均次数。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_STORED_CONFIG`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持固化配置。准备一个无符号 8 位输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围/单位 | 说明 |
| --- | --- | --- | --- | --- |
| `count` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0-255` 次 | 设备保存的默认平均次数；有效含义以设备资料为准 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `count.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持固化配置读取 |

**Python 示例**

```python
import ctypes

stored_average = ctypes.c_uint8()
code = sdk.pynect_direct_get_stored_average_number(
    ctypes.byref(stored_average)
)
if code == -11:
    print("Stored average count is not supported")
else:
    require_success(code, "Get stored average count")
    print("Stored average count:", stored_average.value)
```

**常见错误**

把函数返回码当成平均次数；把固化默认值与当前平均次数混淆；看到 `0` 后擅自解释其设备含义。关联 API：`pynect_direct_get_average_number`、`pynect_direct_set_stored_average_number`。

#### `pynect_direct_get_stored_smoothing_width`

**功能说明**

读取设备固化配置中的默认平滑宽度。返回的是设备定义的原始宽度值，SDK 不解释窗口算法或平滑阶数。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_STORED_CONFIG`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持固化配置。若要解释该值对光谱处理的影响，需要对应设备的平滑参数资料。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 范围/单位 | 说明 |
| --- | --- | --- | --- | --- |
| `width` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | `0-255`，设备定义宽度值 | 设备保存的默认平滑宽度 |

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `width.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持该固化配置查询 |

**Python 示例**

```python
import ctypes

stored_width = ctypes.c_uint8()
code = sdk.pynect_direct_get_stored_smoothing_width(
    ctypes.byref(stored_width)
)
if code == -11:
    print("Stored smoothing width is not supported")
else:
    require_success(code, "Get stored smoothing width")
    print("Stored smoothing width:", stored_width.value)
```

**常见错误**

把宽度值直接解释成 nm 或像素数；在 NIR 上调用后忽略 `-11`；把读取成功误认为当前光谱一定已应用该平滑。关联 API：`pynect_direct_set_stored_smoothing_width`。

#### `pynect_direct_get_slit_width`

**功能说明**

读取设备保存的入射狭缝宽度信息。该值是设备身份和光学配置参数，不会改变狭缝，也不能单独代表光谱分辨率。

**适用范围**

UV/VIS；要求 `PYNECT_DIRECT_CAP_SLIT_WIDTH`；只读。NIR 返回 `-11`。

**调用前提**

设备已打开并支持狭缝信息查询。准备一个无符号 8 位输出变量。

**输入参数**

无普通输入值。

**输出参数**

| 参数 | Python 类型 | 方向 | 单位/范围 | 说明 |
| --- | --- | --- | --- | --- |
| `um` | `ctypes.c_uint8()` + `ctypes.byref()` | 输出 | 微米（um）；`0-255` | 设备报告的狭缝宽度 |

文档使用 ASCII `um` 与函数参数名保持一致，其物理单位为微米。

**返回值**

| 返回值 | 含义 |
| ---: | --- |
| `0` | `um.value` 已写入 |
| `-3` | 设备未打开 |
| `-4/-7/-8/-9` | 返回数据长度、校验或通信异常 |
| `-11` | 当前设备不支持狭缝宽度查询 |

**Python 示例**

```python
import ctypes

slit_width_um = ctypes.c_uint8()
code = sdk.pynect_direct_get_slit_width(
    ctypes.byref(slit_width_um)
)
if code == -11:
    print("Slit width query is not supported")
else:
    require_success(code, "Get slit width")
    print("Slit width:", slit_width_um.value, "um")
```

**常见错误**

使用 `c_uint16` 后忽略 ABI 定义；把数值单位当成 nm；仅凭狭缝宽度推断整机分辨率。关联 API：`pynect_direct_get_device_model`、`pynect_direct_get_hardware_version`。

## 16. 学生实验

以下实验默认禁止调用第 12 章列出的高风险写接口。

### 16.1 实验一：环境和设备检查

目标：完成只读测试并识别设备。

步骤：

1. 检查 Python 为 64 位。
2. 检查 `dll` 文件夹内包含两个 DLL。
3. 运行 `minimal_test.py --read-only`。
4. 记录 DLL 路径、芯片序列号、设备型号、设备序列号和波长点数。

完成标准：最终 `FAIL=0`，并能解释 `PASS` 和 `SKIP` 的区别。

### 16.2 实验二：采集并保存完整光谱

目标：理解波长和强度的一一对应关系。

步骤：

1. 运行完整光谱采集程序。
2. 检查点数和波长范围。
3. 保存 `spectrum.csv`。
4. 随机检查 5 行数据是否能在 Python 输出中找到对应值。

完成标准：CSV 中数据行数与波长点数一致，两列数据无错位。

### 16.3 实验三：绘制光谱曲线

目标：将 CSV 转换为可检查的光谱图。

步骤：

1. 使用第 9 章脚本绘图。
2. 标注样品名称、积分时间和平均次数。
3. 检查横轴波长范围与设备返回范围一致。
4. 描述主要峰或变化区域，不要求进行物质定性结论。

完成标准：图形包含标题、横轴名称、纵轴名称，数据没有明显错位。

### 16.4 实验四：积分时间比较

目标：观察积分时间对强度和采集时间的影响。

建议设置：使用设备允许范围内的三个积分时间，每个条件保持样品、光路和平均次数不变。

记录：

- 每个积分时间的最大强度。
- 是否出现饱和。
- 单次采集大致耗时。
- 实验结束后是否恢复原积分时间。

### 16.5 实验五：平均次数比较

目标：观察平均次数对随机波动和采集时间的影响。

建议依次使用 `1`、`3`、`5` 次平均，保持积分时间和样品不变。比较曲线平滑程度和采集时间，并在结束后恢复原值。

### 16.6 实验六：指定波段采集

目标：理解请求范围、实际范围和像素点之间的关系。

步骤：

1. 读取设备完整波长范围。
2. 在完整范围内选择一个较小波段。
3. 设置波段并读取实际范围。
4. 保存指定波段的波长和强度。
5. 比较请求边界与实际边界的差异。

### 16.7 提交检查清单

提交前确认：

- [ ] 记录 Python 位数和测试命令。
- [ ] 记录设备型号、芯片序列号和设备序列号。
- [ ] 记录积分时间、平均次数和测量时间。
- [ ] CSV 数据行数与点数一致。
- [ ] 波长和强度按索引配对。
- [ ] 图形坐标轴和单位完整。
- [ ] 程序使用 `try/finally` 关闭设备。
- [ ] 临时参数已恢复。
- [ ] 没有执行 Flash 写入、固化配置、设备复位或未知输出操作。

## 17. 日常使用检查表

测量前：

- 检查 USB、光源、光纤和样品位置。
- 确认没有其他程序占用设备。
- 确认加载的 DLL 路径正确。
- 记录原积分时间和平均次数。

测量中：

- 检查光谱点数和波长范围。
- 检查强度是否过低或饱和。
- 保持样品和光路稳定。
- 为每次数据记录设备和参数信息。

测量后：

- 恢复临时参数。
- 关闭设备。
- 保存 CSV、图形和实验记录。
- 确认文件可以重新打开和读取。
