# NINA 控制 API 文档

## 1.4 中文深色界面与分级菜单

已按用户提供的参考图改为黑色背景和深灰分层界面。新增观测概览；主控台、设备和序列采用分级菜单，不再集中在一个长页面。中文正文 16px，375 / 390 / 430px 布局与底部导航经过截图复查。设计与信息主次见 docs/UI-DESIGN.md。

分体部署更新到 nina-server-1.4.zip 和 nina-host-1.4.zip，两端同时升级并保留 config.json 与运行数据。域名仍为 https://nina.finderskys.online，现有密钥不变。


## 1.3 主控台与图形化序列

新增“主控台 / Imaging”页面和多目标图形化序列生成器，继续分为服务器与工控机两个部署包，域名及现有密钥保留。详见 docs/IMAGING.md，其中按 NINA 源码列出已接入功能及 Advanced API 未开放的项目。
支持图像预览、历史与统计、解析、导星曲线、设备附加控制，及常用顺序序列的可视化编排、生成、保存、加载。生成与加载不会自动开始拍摄。
此版不是完整 NINA 桌面克隆，也不冒充支持插件全部节点；未接入项在功能对照中明确列出。


## 1.2 分体部署与实际拍摄入口

本次正式部署交付为两个包：`nina-server-1.2.zip` 和 `nina-host-1.2.zip`，服务器域名已配置为 `https://nina.finderskys.online`。两个包内 README 分别说明部署操作；详见随包交付的《部署与拍摄说明.md》。
服务器端仅运行转发和网页，工控机端运行 Agent，主动通过 HTTPS 连接服务器；NINA 与 PHD2 保持本机访问。两端保留当前项目的同一对密钥。
新增序列页拍摄监控，每 5 秒读取真实设备和序列状态；可按名称读取、加载 NINA 保存的序列，再单独开始。连续拍摄由 NINA 本地执行，原图保存工控机。
新增只读检查命令 `python -m nina_control.preflight --config config.json` 和 Windows 当前用户登录任务脚本 `install-agent-task.ps1`。不自动安装登录任务。
停止命令有独立执行名额，避免四个远程查询或操作等待时阻塞停止。停止序列、曝光、导星和赤道仪是独立命令。
HTTP 命令成功表示 NINA 接受或完成了相应 API 请求，不能用单帧曝光 API 的成功或 IsExposing=false 证明图像已落盘；首次拍摄需要核对 NINA 保存目录中的文件。
该版本未在你的远程服务器或真实设备上验收；Docker、HTTPS 证书签发和真实出片仍需部署后检查。序列 JSON 编辑沿用原生格式，未提供完整拖拽编辑器或原图云端上传下载。


Windows 启动脚本与服务器分离部署步骤见 docs/DEPLOY.md，运行服务后也可打开 /deploy。网页不再按缓存连接状态禁用控制按钮，实际请求交由后端返回结果。

## 1 连接结构与职责

网页或业务调用方 → HTTPS REST 平台服务器 → 主机主动轮询 → 本机 NINA Advanced API → 已配置的赤道仪驱动。

主机 Agent 每 0.2 秒空闲轮询一次，每 2 秒上报心跳和状态。只使用出站 HTTP 请求，不要求现场开放公网端口。相对长连接，这个首版更容易在代理和断网重连环境下调试；后续可将下行替换为 WebSocket/MQTT，保留 REST 和命令协议不变。方向点动仅在主机与本地 NINA 间使用 WebSocket。

服务器默认为 `http://127.0.0.1:18700`；联网 Agent 的主机本地 REST 默认为 `http://127.0.0.1:18701`；单独 host 模式默认 18700。两个 REST 服务提供相同业务接口。远程主机连接强制要求 HTTPS，环回调试允许 HTTP。所有功能只针对一套主机和赤道仪。

## 2 认证和请求规则

业务请求：`Authorization: Bearer <api_token>`。网页会记住已填写的密钥，保存在当前浏览器 localStorage；刷新及重新打开页面后自动使用。服务端密钥来自 config.json 或环境变量，不会在重启时自动轮换。

Agent 请求：`Authorization: Bearer <agent_token>`；注册后还要携带 `X-Agent-Session: <session_id>`。两种密钥必须不同且至少 32 字符。真实模式不提供无认证控制入口，静态网页和 API 文档可公开读取。

所有改变状态的命令必须携带 `Idempotency-Key`，长度 8–128。同键同内容返回原命令，同键不同内容返回 409。使用新键表示新的操作；网络超时后应查询旧命令状态，不要换新键重复移动。

POST 使用 JSON；单请求最大 8 MB。跨域请求被拒绝，不启用通配 CORS。默认只接受 localhost/127.0.0.1 Host；部署域名通过 `allowed_hosts` 配置。反向代理须保留 Host，浏览器与 API 保持同源。HTTP 监听器供本机代理使用，不是 TLS 终止器。

## 3 业务 REST 接口

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/health` | 无密钥存活检查，不代表设备在线 |
| POST | `/api/v1/session` | 校验已有 Bearer Token，不提供匿名会话或自动登录 |
| GET | `/api/v1/mount` | 缓存的现场状态；含 agent_online、stale、updated_at、mode、mount |
| GET | `/api/v1/mount/capabilities` | 向主机查询驱动能力及点动速率区间 |
| GET | `/api/v1/targets?q=M31&limit=20` | 向主机读取 NINA 星表，默认最多 20 条，上限 100 |
| POST | `/api/v1/commands` | 提交命令，202 返回命令记录及 Location |
| GET | `/api/v1/commands/{id}` | 查询命令状态、结果或错误 |
| GET | `/docs` | 中文离线文档 |
| GET | `/openapi.json` | OpenAPI 3.1 规范 |

查询结果示例：

```json
{
  "source": "nina_catalog",
  "epoch": "J2000",
  "targets": [{"id":"NGC224","aliases":["M31"],"name":"NGC224","ra_deg":10.6847,"dec_deg":41.2690,"epoch":"J2000","type":"GALXY","source":"nina_catalog"}]
}
```

示例坐标仅示意返回结构；实际使用主机星表数值。不会请求 NINA 不存在的 `/target/search` 接口。查询不会移动设备。

## 4 命令统一格式

```http
POST /api/v1/commands
Authorization: Bearer <api_token>
Idempotency-Key: goto-m31-20261029-001
Content-Type: application/json

{"action":"mount.goto","params":{"ra_deg":10.6847,"dec_deg":41.2690,"epoch":"J2000","tracking":"sidereal"}}
```

```json
{"id":"<UUID>","action":"mount.goto","params":{},"state":"queued","created":0,"expires":15,"updated":0,"result":null,"error":null}
```

上例时间字段结构为 Unix 秒，实际响应含完整原始 params。服务器创建的 expires 是 created+15 秒，表示开始执行期限，不表示运动必须 15 秒内完成。Agent 与服务器需进行系统时钟同步。

所有位置输入均为 **J2000 赤经角度 ra_deg [0,360)、赤纬 dec_deg [-90,90]**。赤经小时须先乘 15。当前不接受 JNOW 输入；NINA 在内部根据驱动完成坐标转换。返回 mount 的坐标可能属于驱动历元，必须同时查看 epoch/raw，不要直接与 J2000 目标做减法。

### 4.1 赤道仪命令

| action | params | NINA 对应接口 |
| --- | --- | --- |
| mount.info | `{}` | GET `/equipment/mount/info` |
| mount.capabilities | `{}` | 根据 mount/info 提取能力和速率 |
| mount.devices | `{}` | GET `/equipment/mount/list-devices` |
| mount.rescan | `{}` | GET `/equipment/mount/rescan` |
| mount.connect | 可选 `device_id` | GET `/equipment/mount/connect?to=...`；省略用 NINA 已选设备 |
| mount.disconnect | `{}` | GET `/equipment/mount/disconnect`；转向或跟踪中拒绝 |
| mount.goto | `ra_deg,dec_deg`；可选 epoch、tracking | GET `/equipment/mount/slew`；见下一节 |
| mount.stop | `{}` | GET `/equipment/mount/slew/stop`，再停止跟踪 |
| mount.tracking | `mode` | GET `/equipment/mount/tracking?mode=...` |
| mount.park | `{}` | GET `/equipment/mount/park` |
| mount.unpark | `{}` | GET `/equipment/mount/unpark` |
| mount.set_park | `{}` | GET `/equipment/mount/set-park-position` |
| mount.home | `{}` | GET `/equipment/mount/home` |
| mount.sync | `ra_deg,dec_deg`；可选 epoch | GET `/equipment/mount/sync`；同步坐标模型，不做转向 |
| mount.flip | `{}` | GET `/equipment/mount/flip`；只在 NINA 判定需要时翻转，不强制 |
| mount.jog | `direction,rate_deg_s`；可选 duration_ms | 本机 WS `/v2/mount`，发送 direction/rate |

`tracking`/`mode` 枚举：sidereal 恒星、lunar 月球、solar 太阳、king King、stopped 停止。对应上游数值 0/1/2/3/4；Goto 不接受 stopped，默认 sidereal。可用模式仍由驱动决定。设置跟踪模式不包含自动寻找相应天体。

Goto 流程：检查已连接、未停放、未转向和必要能力 → 开启指定跟踪 → NINA slew（ra/dec 为角度，center=false、rotate=false、waitForResult=true）→ 再次设置跟踪并读取状态确认。成功表示 NINA 转向返回成功，且读回非 Slewing、TrackingEnabled=true；不是拍照解析居中证明。上游转向请求超时为 180 秒，失败时尝试本地停止，不自动重试。停放设备必须显式 unpark，不会隐式解除停放。

停止不等待 Goto 操作锁。停止命令会取消仍在服务器队列的旧赤道仪控制；主机亦拒绝早于最近停止的旧命令。它是软件停止接口，不能代替硬件急停与现场限位。

park/home/flip 上游会启动异步动作，其命令成功结果标注 `completion: accepted_by_nina`，只表示 NINA 接受；使用 mount.info 的 AtPark/AtHome/Slewing 等确认物理完成。connect/unpark 等也应读回状态。不要把所有 succeeded 一律解释为设备已停止运动。

点动示例：

```json
{"action":"mount.jog","params":{"direction":"east","rate_deg_s":0.5,"duration_ms":500}}
```

direction 为 north/south/east/west；速度单位度/秒，非“恒星速倍数”。duration_ms 默认 500，范围 100–1500。请求速率必须在驱动上报区间内；服务上限为 10 度/秒。Agent 每约 0.5 秒续传，结束后发送 0 速，NINA 自身无续传约 2 秒停止。这里是有限时长点动，不能依赖浏览器 mouseup 或远程长按维持运动。

### 4.2 构图与目标命令

| action | params | 说明 |
| --- | --- | --- |
| target.search | `query`；可选 limit | 同目标查询 REST，读取 NINA.sqlite |
| framing.info | `{}` | 原样读取 NINA 构图助手信息 |
| framing.set | `ra_deg,dec_deg`；可选 epoch、rotation_deg、survey_image | 设置构图中心与旋转角、回传视场、可选真实底图 |
| framing.source | `source` | 切换 NINA 构图来源 |

source 可取 NASA、SKYSERVER、STSCI、ESO、HIPS2FITS、SKYATLAS、FILE、CACHE。NINA 必须至少打开过一次构图助手；set 不执行 slew，不触发相机曝光，不保证 NINA 自己已下载新底图。网页返回的底图由独立 CDS 查询取得，不冒充 NINA 界面截图。

```json
{"action":"framing.set","params":{"ra_deg":10.6847,"dec_deg":41.2690,"epoch":"J2000","rotation_deg":0,"survey_image":true}}
```

成功 result 包含：

- source：nina_framing。
- center：输入的 J2000 中心。
- rotation_deg：构图角度，0–360；仅更改构图助手，不转动旋转器。
- fov：width_deg、height_deg、width_px、height_px、pixel_um、focal_mm。
- nina：NINA 构图助手完整原始信息。
- image：真实底图 data_url、field_deg、TAN 投影、来源、归属信息和查询地址，或 null。
- image_error：底图不可用的原因，或 null；几何视场成功但底图失败不算整项失败。

视场公式：`width_deg = 2 * atan(width_px * pixel_um / 1000 / (2 * focal_mm)) * 180/pi`，高度同理。取 NINA 已有相机和焦距配置，不从浏览器参数臆造设备。参数缺失/非正数返回 FRAMING_NOT_READY。

底图采用固定 CDS HiPS2FITS DSS2 color 服务，800×800，J2000/ICRS 实用近似、TAN 投影，字段范围为相机较大视场的 1.45 倍。网页蓝框显示中心相机视场和构图旋转，是中心视场预览；未实现拼接面板、畸变模型和精密 WCS 拖拽。示意方向为北上东左。运行程序没有模拟模式或模拟回退；无法连接真实 NINA 时返回明确错误，不返回假视场。

## 5 命令状态与恢复

`queued → dispatched → running → succeeded / failed`。

queued 超过开始期限由队列认领时标记 expired。进程重启时 dispatched/running 标记 unknown，不自动重放硬件动作。若最终结果超过 240 秒仍未收到，查询时转 unknown，需检查现场。终态不被迟到回报覆盖。

主机日志使用 commandId 去重：网络重试只重传结果，不重新运动。服务器无心跳超过 10 秒判离线，拒绝新命令；状态超过 5 秒标记 stale。服务器重启不重新下发已 dispatch 的命令；Agent 断线不等于设备已经停止。普通 Goto 可能继续在 NINA 内执行，点动有主机时限和 NINA 续传超时。断网期间应在现场 NINA 或主机本地 REST 停止。当前不能保证操作系统崩溃与设备动作之间的严格 exactly-once。

数据库本身已完成 WAL 提交后才派发命令。命令记录没有自动清理策略，应按运营需要离线归档。此版无多租户权限、任务排程、外部天气限位或多机资源调度。

## 6 Agent 协议

| 方法 | 路径 | 请求/响应 |
| --- | --- | --- |
| POST | `/api/agent/heartbeat` | `{session_id,mode,mount}`；注册并更新在线状态 |
| POST | `/api/agent/next` | `{}`；返回 `{command:记录或null}`，单次领取、不会因 ACK 丢失重发运动 |
| POST | `/api/agent/commands/{id}` | `{state,result?,error?}`；上报 running/succeeded/failed/unknown |

session_id 每次 Agent 启动重新生成。已有在线会话时拒绝另一实例注册，避免同套设备同时运行两个 Agent。最后一次心跳超过 10 秒后允许新会话接管；旧会话不再可拉取命令。接管前仍需确认旧现场操作是否运行。

## 7 错误结构

```json
{"error":{"code":"MOUNT_PARKED","message":"Unpark explicitly before moving"}}
```

HTTP 常见状态：400 参数错误、401 密钥错误、403 来源/Host 错误、404 未知命令、409 状态冲突或上游拒绝、413 请求过大、501 驱动未声明能力、502 上游失败、503 主机/星表不可用、504 查询超时。

异步命令执行错误位于命令记录 error 中，含 code/message/status；查询命令本身仍返回 HTTP 200。NINA 可能 HTTP 200 但 JSON Success=false，本适配器会判为失败，不将其当成功。

## 8 快速调用

```powershell
$headers = @{ Authorization = "Bearer $env:NINA_API_TOKEN" }
Invoke-RestMethod 'http://127.0.0.1:18700/api/v1/targets?q=M31' -Headers $headers
$headers['Idempotency-Key'] = [guid]::NewGuid().ToString()
$body = @{ action='mount.goto'; params=@{ra_deg=10.6847; dec_deg=41.2690; epoch='J2000'; tracking='sidereal'} } | ConvertTo-Json
$cmd = Invoke-RestMethod 'http://127.0.0.1:18700/api/v1/commands' -Method Post -Headers $headers -ContentType 'application/json' -Body $body
Invoke-RestMethod "http://127.0.0.1:18700/api/v1/commands/$($cmd.id)" -Headers $headers
```

## 9 范围和上游依据

截图所示赤道仪常用功能已覆盖。驱动的物理能力以 capability 为准；未支持的能力不会伪装成功。Advanced API 本身未暴露的厂商私有控制、任意 RA/Dec 自定义跟踪率、脉冲导星、站点写入、驱动设置对话框尚未实现，详见 mount.capabilities 的 unsupported_via_advanced_api。主机不同时直连 ASCOM COM 创建第二个设备控制实例。

上游依据（核对日期 2026-10-29）：

- https://github.com/christian-photo/ninaAPI
- https://raw.githubusercontent.com/christian-photo/ninaAPI/dev/ninaAPI/api_spec.yaml
- https://raw.githubusercontent.com/christian-photo/ninaAPI/dev/ninaAPI/websocket_spec.yaml
- https://github.com/christian-photo/ninaAPI/blob/dev/ninaAPI/WebService/V2/Equipment/Mount.cs
- https://github.com/christian-photo/ninaAPI/blob/dev/ninaAPI/WebService/V2/Application/Framing.cs
- https://alasky.cds.unistra.fr/hips-image-services/hips2fits

本项目使用上游公开协议，未分发 NINA、驱动、其星表或其插件源码。上游 dev 分支会变化，真实联调应记录 /version 返回并核对 API 映射。交付包中的 tests/ 使用隔离的设备替身、伪 NINA 和本地反向连接；生产模块不含模拟设备；不构成 AVX 实机运动验收。


## 10 设备与序列扩展 1.1

新增功能不改变既有密钥、部署模式或赤道仪接口。仍使用 POST /api/v1/commands 下发 action，202 表示命令已入队；通过返回的 id 查询最终结果。

### 10.1 设备状态 REST

GET /api/v1/equipment：NINA 全部设备状态原文。

GET /api/v1/camera、/api/v1/filterwheel、/api/v1/focuser、/api/v1/guider：分别读取设备状态。GET /api/v1/guidecamera：通过 PHD2 读取导星相机状态、曝光时间及支持的曝光选项。所有接口均需要 api_token。

设备控制映射如下。参数的必填项以 OpenAPI 为准，带 * 的字段为必填。

| action | params | 上游接口 |
| --- | --- | --- |
| camera.info | {} | /equipment/camera/info |
| camera.devices | {} | /equipment/camera/list-devices |
| camera.rescan | {} | /equipment/camera/rescan |
| camera.connect | device_id | /equipment/camera/connect |
| camera.disconnect | {} | /equipment/camera/disconnect |
| filterwheel.info | {} | /equipment/filterwheel/info |
| filterwheel.devices | {} | /equipment/filterwheel/list-devices |
| filterwheel.rescan | {} | /equipment/filterwheel/rescan |
| filterwheel.connect | device_id | /equipment/filterwheel/connect |
| filterwheel.disconnect | {} | /equipment/filterwheel/disconnect |
| focuser.info | {} | /equipment/focuser/info |
| focuser.devices | {} | /equipment/focuser/list-devices |
| focuser.rescan | {} | /equipment/focuser/rescan |
| focuser.connect | device_id | /equipment/focuser/connect |
| focuser.disconnect | {} | /equipment/focuser/disconnect |
| guider.info | {} | /equipment/guider/info |
| guider.devices | {} | /equipment/guider/list-devices |
| guider.rescan | {} | /equipment/guider/rescan |
| guider.connect | device_id | /equipment/guider/connect |
| guider.disconnect | {} | /equipment/guider/disconnect |
| equipment.info | {} | /equipment/info |
| camera.capture | duration_s*, gain, image_type, save, target_name | /equipment/camera/capture |
| camera.abort | {} | /equipment/camera/abort-exposure |
| camera.cool | temperature_c*, minutes, cancel | /equipment/camera/cool |
| camera.warm | minutes, cancel | /equipment/camera/warm |
| camera.binning | binning* | /equipment/camera/set-binning |
| camera.readout | mode* | /equipment/camera/set-readout |
| camera.dew_heater | enabled* | /equipment/camera/dew-heater |
| camera.usb_limit | limit* | /equipment/camera/usb-limit |
| camera.statistics | {} | /equipment/camera/capture/statistics |
| filterwheel.change | filter_id* | /equipment/filterwheel/change-filter |
| filterwheel.filter_info | filter_id* | /equipment/filterwheel/filter-info |
| focuser.move | position* | /equipment/focuser/move |
| focuser.stop | {} | /equipment/focuser/stop-move |
| focuser.autofocus | cancel | /equipment/focuser/auto-focus |
| focuser.last_autofocus | {} | /equipment/focuser/last-af |
| guider.start | calibrate | /equipment/guider/start |
| guider.stop | {} | /equipment/guider/stop |
| guider.clear_calibration | {} | /equipment/guider/clear-calibration |
| guider.graph | {} | /equipment/guider/graph |
| guidecamera.info | {} | 本机 PHD2 JSON-RPC |
| guidecamera.connect | {} | 本机 PHD2 JSON-RPC |
| guidecamera.disconnect | {} | 本机 PHD2 JSON-RPC |
| guidecamera.loop | {} | 本机 PHD2 JSON-RPC |
| guidecamera.stop | {} | 本机 PHD2 JSON-RPC |
| guidecamera.exposure | exposure_ms* | 本机 PHD2 JSON-RPC |
| guidecamera.capture | exposure_ms | 本机 PHD2 JSON-RPC |
| guidecamera.star_image | size | 本机 PHD2 JSON-RPC |
| sequence.list | {} | 主机持久序列库 / NINA 原生 JSON 加载 |
| sequence.get | id* | 主机持久序列库 / NINA 原生 JSON 加载 |
| sequence.create | name*, document | 主机持久序列库 / NINA 原生 JSON 加载 |
| sequence.update | id*, revision*, name, document | 主机持久序列库 / NINA 原生 JSON 加载 |
| sequence.delete | id*, revision* | 主机持久序列库 / NINA 原生 JSON 加载 |
| sequence.load | id*, revision* | 主机持久序列库 / NINA 原生 JSON 加载 |
| sequence.current | {} | /sequence/json |
| sequence.state | {} | /sequence/state |
| sequence.available | {} | /sequence/list-available |
| sequence.start | {} | /sequence/start |
| sequence.stop | {} | /sequence/stop |
| sequence.reset | {} | /sequence/reset |
| sequence.skip | type* | /sequence/skip |
| sequence.load_file | name* | /sequence/load |
| sequence.edit_live | path*, value* | /sequence/edit |
| sequence.set_target | name*, ra_deg*, dec_deg*, rotation_deg*, index* | /sequence/set-target |

### 10.2 主相机

camera.capture 请求例子：

```json
{"action":"camera.capture","params":{"duration_s":60,"gain":100,"image_type":"LIGHT","save":true,"target_name":"M31"}}
```

曝光秒数、帧类型、增益由单次 capture 指定；binning 和 readout 通过独立命令设置。不把主相机与导星相机占用同一个硬件实例。原图保存目录由 NINA 配置，接口不返回原图二进制。曝光返回 accepted_by_nina 后用 camera.info 与 camera.statistics 判断真实完成情况；中止使用 camera.abort，停止请求不等待其他操作的控制锁。

制冷 temperature_c 单位摄氏度，minutes 单位分钟，-1 表示 NINA 默认时长；warm 不需要目标温度。驱动不支持某项能力时以真实 NINA 错误为准，不模拟成功。滤镜 ID 以 filterwheel.info 为准；focuser.move 是绝对步数，实际行程以驱动限制为准。自动对焦依赖 NINA 已配置好的相机、滤镜和对焦参数。停止调焦和取消自动对焦是两个独立操作。

### 10.3 导星相机与 PHD2

NINA guider.* 控制 NINA 已选的导星器：连接、启停、重新校准、读导星误差。guidecamera.* 则直接调用工控机 PHD2 的 JSON-RPC，用于导星相机曝光、循环取像、单帧取像和导星局部图像。

在工控机启动 PHD2，配置真实相机和赤道仪，然后启用“工具 → 启用服务器”。默认 TCP 4400；如第二实例使用其他端口，在 config.json 添加 phd2_port 整数，不需要改变任何已有密钥。所有 PHD2 连接限定 127.0.0.1，不向网页开放原始 TCP。

exposure_ms 单位毫秒，只接受 get_exposure_durations 返回的支持值。单帧取像会让 PHD2 检查当前导星/循环状态；需要时先停止取像。connect/disconnect 对应 PHD2 set_connected，影响其当前配置内全部设备，不是仅对相机做开关。star_image 返回导星星点的局部 16 位图像数据及宽高，不是全幅相机预览。gain、offset 等厂商私有导星相机参数未被 PHD2 JSON-RPC 公开，因此没有虚构接口。

### 10.4 原生序列库 CRUD

序列文件由工控机持久保存，默认在启动 --data 目录下的 sequences/library.sqlite；可用 config.json 的 sequence_directory 指定其他序列库目录。服务器和 Agent 分开部署时，库仍放在 Agent 工控机上，而不是只存到浏览器或服务器缓存。

| 方法 | 路径 | 请求体及行为 |
| --- | --- | --- |
| GET | /api/v1/sequences | 返回库内 id、name、revision、created、updated |
| POST | /api/v1/sequences | name 必填；document 可选；返回异步命令 |
| GET | /api/v1/sequences/{id} | 返回完整原生 document 和当前 revision |
| PUT 或 PATCH | /api/v1/sequences/{id} | revision 必填，name/document 至少一个；返回异步命令 |
| DELETE | /api/v1/sequences/{id} | JSON 请求体含 revision；返回异步命令 |
| POST | /api/v1/sequences/{id}/load | JSON 请求体含 revision；加载到 NINA，绝不自动开始 |

所有写请求需要 Idempotency-Key。也可通过统一命令接口调用同名 sequence.create/get/update/delete/list/load。id 为服务器生成的 32 位小写十六进制标识，不接受文件路径。revision 为乐观锁，更新后加一；版本不匹配返回 REVISION_CONFLICT，防止旧页面覆盖新编辑。

新建示例：

```json
{"action":"sequence.create","params":{"name":"M31 LRGB"}}
```

省略 document 时生成符合 NINA 容器结构的 Start / Imaging / End 空序列，不包含曝光或运动指令。实际拍摄可在 NINA 保存高级序列 JSON，然后在网页导入、修改原生 JSON 并保存为新序列。网页支持序列列表、读取、JSON 编辑、导入和导出，不提供完整的 NINA 拖拽编辑器。

NINA 原生保存 JSON 包含 $type，并以 SequenceRootContainer 为根。sequence.current 与 sequence.state 是状态快照，不是原生保存文件；服务会拒绝把这类状态结果当成原生序列导入。单序列上限 4 MB。新增自定义插件节点时，工控机 NINA 也必须安装相应插件。服务校验根结构与大小，不替代 NINA 对每个具体节点的验证。

加载指定序列：

```json
{"action":"sequence.load","params":{"id":"返回的32位id","revision":1}}
```

先在 NINA 打开高级序列器以完成初始化。load 使用 POST /sequence/load，发送未经额外包装的原生 JSON，结果标记 started=false。随后显式调用 sequence.start，保留 skipValidation=false，由 NINA 校验实际设备与序列条件。若 NINA 已在执行序列，加载冲突由 NINA 返回。

sequence.stop 停止 NINA 当前高级序列；sequence.reset 重置当前进度；sequence.skip 可选 CurrentItems/ToEnd/ToImaging。sequence.available 列出 NINA 自己序列目录内的文件；sequence.load_file 只接受这个列表里已有的相对名称，不允许任意路径读取。序列库与 NINA 默认文件目录分开管理：删除库条目不会删除用户原有 NINA 文件，也不会清空已加载的 NINA 副本。更改已保存条目不会直接修改正在执行的副本。

sequence.edit_live 使用 NINA sequence/state 中的路径，支持简单标量字段，是上游实验功能；格式为 Imaging-Items-0-ExposureTime 等。它只修改当前 NINA 内存对象，不自动写回序列库。建议先停止序列再操作，复杂对象/枚举应编辑库中的原生 document 后重新加载。

### 10.5 来源与验证

接口依据：NINA Advanced API v2 的 api_spec.yaml、Application/Sequence.cs，以及 PHD2 官方事件服务文档：

- https://github.com/christian-photo/ninaAPI/blob/dev/ninaAPI/api_spec.yaml
- https://github.com/christian-photo/ninaAPI/blob/dev/ninaAPI/WebService/V2/Application/Sequence.cs
- https://github.com/OpenPHDGuiding/phd2/wiki/EventMonitoring
- https://github.com/isbeorn/nina/blob/develop/NINA.Sequencer/Serialization/SequenceContainerCreationConverter.cs

扩展在隔离的 HTTP/TCP 假上游验证参数、状态和序列读写；没有自动曝光、调焦、切换滤镜或移动真实设备。目标设备与 NINA/PHD2 的实机兼容性仍需现场联调。


## 11 设备选择与操作反馈修正 1.1.1

网页进入设备页后自动读取设备列表和实时连接状态。相机、滤镜轮、电调焦、导星器和赤道仪均以设备名称显示在下拉框，界面不再要求手工输入 device_id；实际连接时把所选条目的 Id 传给原有 API。可点击刷新设备列表重新获取；没有列表或读取失败时明确显示原因，不会猜测设备 ID。

滤镜轮从 info 的 AvailableFilters 生成滤镜名称下拉框，用 SelectedFilter 显示当前滤镜。选择后自动读取 filter-info，切换前再次核对最新列表，发送实际 Id（支持不连续的编号），切换后读取 SelectedFilter 和 IsMoving 确认到位，不要求用户输入数字。

连接、断开、滤镜切换均显示卡片内反馈、处理中的按钮文案及成功/失败浮动通知。连接/断开由读回的 Connected 状态确认，连接时也核对驱动 DeviceId。12 秒内未确认时显示“请求已接受、状态未确认”，不会把命令接受直接当作连接成功。NINA/PHD2 错误在当前卡片和浮动通知显示。

PHD2 设备连接仍按 PHD2 当前配置执行，不通过 NINA 的 device_id 选择导星相机。该卡片明确显示 PHD2 中配置的相机名称与连接状态。既有 HTTP API、密钥和序列库兼容。


# NINA Control 1.3 · 主控台与图形化序列

## 源码依据与功能边界

已克隆官方仓库到 outputs/nina-upstream，开发版提交 717ea13；同时核对官方 Version-3.2 标签（2393eae581145ed5b8114bf07c48ca2580540fd5）的曝光、等待、制冷和滤镜序列化格式。
本次扩展的是网页和工控机 Agent，没有修改或替换已安装的 NINA 桌面程序。
主控台按 NINA Imaging 的信息面板和工具面板对照实现，连接 NINA Advanced API 2.2.15 系列。

| NINA 成像功能 | 网页对应实现 | 说明 |
| --- | --- | --- |
| 当前图像、历史图像 | 真实图像预览、历史点击查看 | 工控机从 NINA 二进制接口取图，经鉴权命令链路回传 |
| 缩放、适应、1:1、旋转、十字线 | 浏览器图像工具栏 | 1:1 为预览像素，最大 1280×960，不是传感器全分辨率 |
| 九宫格像差检查 | 预览图中心及边角九宫格 | 基于预览采样，不替代原图像差测量 |
| 图像拉伸、Debayer | 跟随 NINA 或手动 factor / blackClipping / unlinked / debayer | 只影响预览处理，不改原图 |
| 图像历史与 HFR 历史 | 历史列表和 HFR / Stars / Mean / Median / StDev / Temperature 曲线 | 图表使用 NINA 返回值；不编造缺失值 |
| 图像统计 | 原始 NINA 统计字段 | HFR、星点数等要求 NINA 自身完成分析 |
| 直方图 | 本地 8 位预览亮度直方图 | 明确标注为预览，不冒充原始 ADU 直方图 |
| 拍摄、保存开关、中止曝光 | 主控台相机面板 | 图像保存在 NINA 配置目录；接受请求不等于文件已保存 |
| 相机制冷、回温、Binning、读出模式、防露、USB 限速 | 相机控制与附加设置 | 实际支持范围由真实相机驱动决定 |
| 滤镜轮 | 自动读取名称和位置、切换并确认 | 不手输驱动 ID 或猜测滤镜槽位 |
| 电调焦 | 绝对位置、停止、自动对焦、取消、读取 AF 结果 | 对焦曲线原始结果可展开查看 |
| 旋转器 | 位置、机械位置、反转、机械范围、停止 | 设备按名称连接 |
| 赤道仪 | 连接、停放、解除、零位、跟踪、翻转、停止；目标页 Goto 与同步 API | 支持范围来自驱动与 Advanced API |
| 导星 | NINA / PHD2 控制、RA/Dec 曲线、RMS、像素/角秒 | 角秒换算需要 NINA 返回有效 PixelScale |
| 序列 | 状态、开始、停止、重置、原生文件与库管理 | 开始保留 skipValidation=false |
| 开关、天气、安全、平场板、圆顶 | 补充设备面板 | 名称选择连接；开关通道来自真实可写开关列表 |
| 图像解析 | 当前图、历史图、拍摄并解析 | 拍摄解析最长 120 秒曝光；不会自动同步或转向 |
| NINA 画面与日志 | 读取 NINA 截图、最近日志、切到成像页 | 截图不是相机图像 |

以下项目在 NINA 本体源码中存在，但 Advanced API 没有对应控制接口，本次没有宣称完成：原生相机 Live View、ROI 子框、自动 HFR 开关、逐星 HFR 标注、巴氏对焦分析器、自动选取对焦亮星、调焦温度补偿、取消解析、桌面窗口布局控制。也没有把定时刷新预览伪装成相机 Live View。
连续曝光通过 NINA 本地拍摄序列执行；没有引入断线后自动重发的浏览器循环曝光。
若要覆盖上述桌面专属功能，需要另行实现并安装 NINA 扩展插件，不能靠现有 REST 路由假装支持。

## 图形化生成序列

序列页顶部是图形编辑器。填写计划名称，添加目标，可直接按名称查询工控机 NINA 星表，也可使用目标页已选天体。
坐标输入为 J2000 赤经度数（0..360）与赤纬度数（-90..90），不是赤经小时。多个目标顺序执行。
开始、目标和结束区域均可添加步骤；步骤支持拖动、上移、下移、复制、删除，目标也可调整顺序。

支持的步骤：曝光组、目标转向、解析居中、跟踪设置、自动对焦、开始/停止导星、抖动、等待、制冷、回温、解除停放与停放。
曝光组可选真实滤镜、曝光时间、张数、Gain、Offset、Binning 和帧类型。滤镜 ID 只做接口定位，原生文件使用从 NINA 读回的真实滤镜 Position 和序列化字段。
曝光张数编译为 LoopCondition.Iterations；TakeExposure.ExposureCount 从 0 开始，不把它误用为总张数。
等待时长为秒；温控表单输入秒，生成原生 CoolCamera/WarmCamera.Duration 时转换成分钟。
负赤纬使用 NegativeDec 加有符号 DecDegrees，包含 -0.x° 情况。过中天翻转使用 NINA 自身触发器，需在本机配置翻转参数。

1. “生成 NINA 序列”只编译与校验，显示目标数、曝光张数、纯曝光时间与原生 JSON。
2. “保存到工控机序列库”只保存；“保存并加载”会保存并加载到 NINA，不自动开始。
3. 到主控台或序列监控单独点击“开始”，由 NINA 真实校验后执行。
4. 可导出原生 NINA JSON 或可编辑 .plan.json。草稿也保存在当前浏览器 localStorage。
5. 从序列库读取本工具生成的序列，点击“在图形生成器中编辑”可恢复图形计划，并沿用库条目 revision 更新。

普通 NINA 序列可能包含插件、自定义条件和触发器，不会强行反向转换成有限图形步骤而丢失内容。继续使用原生 JSON 编辑入口。
图形计划嵌入根对象 NinaControlPlan 字段供本工具恢复；NINA 执行的是 Items / Conditions / Triggers 原生结构。
图形器是常用顺序拍摄计划编辑器，不是 NINA 完整节点库：没有并行、嵌套循环编辑、任意插件节点、表达式或完整安全策略的可视化编排。

## API

仍使用 POST /api/v1/commands：action=sequence.generate，params={plan:{...}}，返回原生 document 和 summary。
它只读取选中滤镜信息，不发送曝光、转向、加载或开始命令。保存继续使用 sequence.create / sequence.update。
新增 image.history / image.preview / image.solve / image.solve_prepared / imaging.capture_solve / application.screenshot / application.logs / application.plugins / application.imaging。
新增 rotator / switch / weather / safetymonitor / flatdevice / dome 的控制路由；通过 GET /api/v1/console/schema 读取表单字段定义。
预览和截图的已完成命令结果仅保留最近 20 个；设备写命令去重记录继续持久保存。
图像以受限尺寸的 data_url 进入原有异步命令结果，不提供任意上游 URL，不开放磁盘路径下载。
所有写操作仍要求独立 API 密钥和 Idempotency-Key。工控机与服务器包必须同时更新。

## 验证范围

自动化测试覆盖生成文件的循环计数、坐标符号、RA 单位、温控单位、滤镜位置、引用完整性、无曝光/非法步骤拒绝，以及图像流与解析路由。
真实设备、NINA 内部完整反序列化与整夜拍摄仍需在你的工控机验证；当前机器没有 .NET SDK，未编译 NINA 桌面程序。
首次使用先加载生成序列，在 NINA 核对可识别的指令和校验结果，再拍一张短曝光检查保存文件。

源码与文档：
- https://github.com/isbeorn/nina/tree/717ea13
- https://github.com/isbeorn/nina/tree/Version-3.2
- https://github.com/christian-photo/ninaAPI/blob/dev/ninaAPI/api_spec.yaml
- https://nighttime-imaging.eu/docs/master/site/tabs/imaging/
