Files
ParkingRobot/detour-information-checklist.md
T

389 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Detour 定位接口信息确认(源码答复)
用途:答复控制程序(`MultiWheelC` / `StateEstimation`)使用定位结果所必需的接口语义。
依据:本仓库 `DetourCore` 源码(`Location.cs``TightCoupler.cs``Frame.cs``LidarOdometry.cs``LidarMap.cs``WebAPI.cs``G.cs`)。
仓库内**没有**名为 `getCartLocation()` 的函数;控制侧该调用对应 Detour 的两类对外位姿出口。下文按字段对齐后统一说明。
---
## 对外位姿出口(先对齐接口)
控制程序看到的 `x / y / theta / tick / l_step`,来自下面之一(或对它们的封装)。字段名在源码里是 `th`,不是 `theta`
| 出口 | 入口 | 字段 | `tick` 的真实来源 |
|---|---|---|---|
| HTTP `GET /getPos` | `CartLocation.FormatPosition()` | JSON`x, y, th, l_step, tick`,异常时多 `error` | `tick = CartLocation.st_time`(观测时刻) |
| 共享对象 `DetourPos{shareObjectTag}` | `TightCoupler.CommitLocation` 每次提交后 `Post` | 二进制:`x, y, th, tick, l_step, error` | `tick = DateTime.Now.Ticks`(提交/推送时刻) |
默认 HTTP 端口:`Configuration.conf.guru.DetourPort`,默认 `4321`
`shareObjectTag` 必须与控制侧 Clumsy 的 soTag 一致。
两种出口的 `x/y` 都已经过:
```
x_out = 内部x / guru.inputScale + guru.biasX
y_out = 内部y / guru.inputScale + guru.biasY
```
默认 `inputScale = 1``biasX = biasY = 0`,因此默认单位就是内部单位(毫米)。
`th` / `l_step` 不做缩放。
---
## 1. `getCartLocation()` 字段定义
### 1.1 各字段含义
| 字段 | 源码名 | 含义 |
|---|---|---|
| `x` | `CartLocation.x` | 车体原点在**当前地图坐标系**中的 X。已从传感器坐标系经安装外参反变换到车体中心(`TightCoupler.CommitLocation``ReverseTransform(传感器位姿, 组件安装位姿)`)。 |
| `y` | `CartLocation.y` | 同上,Y。 |
| `theta` | `CartLocation.th` | 车体航向。`0°` 朝向地图 `+X`,逆时针为正(内部用 `cos(th)` / `sin(th)` 画朝向)。 |
| `tick` | 见上表 | **不是**统一的一种时间。HTTP 是观测时间 `st_time`DObject 是提交瞬间的 `DateTime.Now.Ticks`。控制侧必须先确认自己走的是哪条通道。 |
| `l_step` | `Frame.l_step` | **到已标注(labeled)关键帧的图上步数**,用来表达“离可信锚点有多远”,**不是**优化迭代次数,也不是匹配分数。注释原文:`steps to labeled keyframe`。 |
### 1.2 单位、正方向、坐标系
- **内部单位**`x``y` 为毫米。UI 直接按 `mm` 显示。
- **对外单位**:默认仍是毫米;只有改了 `guru.inputScale`(例如设为 `1000` 输出米)才会变。
- **角度单位**:度。
- **坐标系**:当前加载的 SLAM 地图坐标系。原点由建图/标注关键帧决定,不是 GNSS 或车体启动点。
- **车体姿态**:输出的是**车体原点**,不是雷达原点。雷达/相机安装位姿在 `layout.components``x,y,th` 里。
- **右手平面**`+X` 为航向 0`+Y` 为航向 +90°。
### 1.3 `theta` 取值范围
**不是严格的 `[-180°, 180°]`。**
`LessMath.normalizeTh` 只在绝对值 ≥ 360 时按 360° 回绕,`(-360, 360)` 内原样返回。因此对外可能看到 `200``-270` 这类值。控制程序若假设 `[-180, 180]``[0, 360)`,应自己归一化。内部比较角度用 `LessMath.thDiff`,会按最短弧处理。
### 1.4 一次调用的字段是否同一帧
**是。** 一次读取对应同一个 `CartLocation` 对象上的 `x/y/th/l_step/st_time`
`getPos` 先用当前 `latest` 判断 Timeout/Unstable,再序列化;若这两步之间恰好发生新提交,状态字和位姿可能差一帧,但单次 JSON 里的数值字段仍来自同一个对象。
---
## 2. `tick` 的准确含义(优先)
### 2.1 它是什么时刻
分通道:
**HTTP `/getPos` 的 `tick` = `st_time`**
- `st_time``Frame` 上的注释是:传感器数据**到达 Detour 时**由 `G.watch` 打的时间,不是激光头硬件曝光时刻,也不是 HTTP 返回时刻。
- 激光采集线程会按扫描周期做轻微平滑,并加上 `time_bias_ms`
- 里程计把 `frame.st_time` 原样交给 `TightCoupler.CommitLocation`,再写入 `CartLocation.st_time`
- 因此:`tick`**该位姿所对应的那帧观测到达时刻**。SLAM 计算发生在这之后,接口返回更晚。
**DObject `DetourPos` 的 `tick` = `DateTime.Now.Ticks`**
- 这是 **TightCoupler 提交并推送的本地时刻**,100 纳秒为单位,从公元 1 年 1 月 1 日起算(.NET `DateTime.Ticks`)。
- 它**不是**传感器采集时刻,也**不是** Unix epoch。
- 受本机本地时钟、时区、校时影响,必要时可能回跳。
### 2.2 单位、频率、单调、回绕
| 项目 | HTTP `tick``st_time` | DObject `tick``DateTime.Now.Ticks` |
|---|---|---|
| 单位 | 毫秒 | 100 ns1 ms = 10000 |
| 时钟 | 进程启动时锚定一次 UTC,之后用 `Stopwatch` 单调累加:`ElapsedTicks*1000/Frequency + stMillis` | 本机本地 `DateTime.Now` |
| 更新频率 | 随传感器/融合提交,通常接近雷达帧率(常见 10–20 Hz,取决于设备 `timeBudget` | 每次 `CommitLocation` 成功推一次 |
| 单调 | 进程内单调递增(不受事后改系统时间影响) | 一般递增;改系统时间或 DST 可能回跳 |
| 回绕 | `long` 毫秒,实际不会回绕 | `long` ticks,实际不会回绕 |
| 进程重启 | 重新锚定当前 UTC,**不会从 0 开始**,但与重启前不保证连续 | 继续跟本机时钟,与进程无关 |
`G.watch.TimeStampMillis` **不是** Unix 毫秒(1970),而是从公元 1 年起算的 UTC 毫秒,再加上启动后的单调流逝。
### 2.3 多车能否用 `tick` 对齐
**不能当作已同步的多车时钟。**
- 各车各自启动 `G.watch`,没有 PTP/NTP 协议,也没有跨车时间服务。
- HTTP `tick`:若各车系统 UTC 在启动时大致同步,数值会接近“同一套绝对时间”,但仍有启动锚定误差和各车处理延迟,**不能直接当多车位姿对齐的主时钟**。
- DObject `tick`:本地时间 ticks,跨时区会直接错开,更不适合多车对齐。
- 进程重启、暂停(`G.paused`)、提交失败都会造成时间空洞。
控制侧建议:单车内部用 `tick` 做延迟估计和短时预测;多车对齐应使用外部统一时钟,或只把 Detour `tick` 当相对时间。
---
## 3. 定位延迟与更新方式
### 3.1 输出位姿比真实运动滞后多少
源码**没有**标定“官方延迟 xx ms”。能确定的是延迟结构:
```
真实运动 → 雷达扫描/到达(st_time) → 里程计配准 → TightCoupler 融合 → 写入 CartLocation.latest → 接口读到缓存
```
经验上由源码阈值约束:
- 激光一圈通常几十毫秒(`Lidar2DStat.timeBudget`)。
- TightCoupler 常用窗口 `TCtimeWndSz = 150 ms`,上限 `TCtimeWndLimit = 700 ms`
- `LidarOdometry``reg_ms > 200` 记为 `bad perf`
- `/getPos``now - st_time > 500 ms``Timeout`
因此正常运行时,**从观测到可读位姿大约是 1 帧雷达周期 + 配准/融合,常见几十到两百毫秒;超过 500 ms 会被 HTTP 接口判超时。** 这是实现上的门槛,不是出厂标定值。
### 3.2 `getCartLocation()` 是否返回最近一次缓存
**是。**
- `CartLocation.latest` 是全局缓存。
- 只有 `TightCoupler.CommitLocation` 成功后才会替换(取 `history``st_time` 最新的一帧)。
- `/getPos` 只读缓存,不触发新的 SLAM 计算。
- 配准失败、`CommitLocation` 返回 `null``bad variance` / `bad trace`)、或 `G.paused` 时,缓存停在上一帧。
### 3.3 原地自转时频率/延迟是否明显变化
**轮询频率不变,有效更新和延迟会变差。**
- 接口仍按调用方频率读同一缓存。
- 原地旋转时二维扫描重叠变差,帧间分、相位锁分容易掉,`allowCommit` 更常失败,缓存更容易停住。
- 配准变慢时单帧 `reg_ms` 上升;失败后 `l_step` 被加大(见第 4 节),看起来像“转的时候定位变钝”。
- 源码没有“自转专用降频”。
控制侧:自转短时预测应假设**有效位姿更新可能变稀、变老**,不要假设 `tick` 仍按雷达周期前进。
---
## 4. `l_step` 的含义(优先)
### 4.1 它是什么
**定位质量的图距离,不是优化迭代次数,也不是匹配状态枚举。**
定义:当前位姿/关键帧沿约束图走到**已标注关键帧**还要几步。
- `0`:本身就是标注帧(`labeledXY` / `labeledTh`),或刚被标成锚点。
- `1`:刚和地图/标注帧配准成功(`LidarMap` 成功后会把 `compared.l_step = 1`)。
- 正常递推:新关键帧 `l_step = 旧关键帧.l_step + lstepInc`
- TightCoupler 提交时:有参考关键帧则 `reference.l_step + 1`;没有则 `latest.l_step + 3`
- `9999`:未定位、手动设位、或与标注图断开。这是 `Frame` / 初始 `CartLocation` 的默认值。
**越小越好、越可信。**
`AIImplementationNote.md` 里“越小越不确定”与源码相反,不要采用。
内部用途包括:
- TightCoupler 边权:`l_step > 10 → 1``> 5 → 2``> 2 → 3`,否则 `4`;标注帧权 1000。
- 图优化用 `1/(l_step+0.01)` 当权。
- `l_step` 大时 `LidarMap` 更容易走全局配准(`forceGRegStep``GregThresK * l_step`)。
- `l_step > 1000` 且孤立的关键帧可被删掉。
源码 TODO 也写过:希望将来“去掉 `l_step`,改用误差圆”。当前对外接口仍是这个整数。
### 4.2 为什么原地自转会从 2~4 升到几十、上百
自转本身不会改公式,但会让 **`lstepInc` 连续被惩罚**,再在切关键帧时一次性加到 `l_step` 上。
`LidarOdometry` 里常见加项:
| 条件 | `lstepInc` 增量 |
|---|---|
| 帧间隔过大 | `+3``+20` |
| 时间落后 > 1 s | 可到 `+1000` 量级并重启局部图 |
| 有效点太少 | `+3` |
| 掩膜后点过少 | `+10` |
| 帧间序贯配准失败 | `+5` |
| 局部里程计分过低 | `+15` |
| 历史分偏低 | `+1``+15` |
| 局部图重启 / 相位锁差 | `+2` |
| 上一帧能提交、这一帧不能 | `+100` |
| 离开 hard 区域 | 默认再 `+20` |
原地自转时典型连锁是:旋转导致重叠变差 → 分数掉 → `lstepInc` 累加 → 因“新点变多 / 超时 / restart”切关键帧 → `新 l_step = 旧值 + lstepInc`
若此时地图匹配也失败,TightCoupler 按 `latest.l_step + 3` 继续推高。
所以 2~4 很快可以变成几十,失败恢复前看到上百是符合实现的。
匹配一旦重新成功,关键帧会被改回 `l_step = 1`,输出也会掉下来。
### 4.3 有没有官方阈值
**没有写给控制程序的官方阈值表。** 下面是源码内部实际在用的分档,可作控制侧初值,现场仍要按地图和雷达标定。
| `l_step` | 源码含义 | 给控制程序的建议 |
|---|---|---|
| `0` | 标注锚点 | 最可信 |
| `1``2` | 刚贴上地图 / 离锚点很近 | **正常,接受** |
| `3``5` | TightCoupler 已降权 | 可用,开始警惕跳变 |
| `6``10` | 权更低;地图更倾向全局配准 | **退化,降低信任 / 收紧预测** |
| `11``98` | 最低权;曾从不稳定区回到 `2` 时会打工作区快照 | **明显退化,不宜做精细控制** |
| `≥ 99` | TightCoupler 允许关键帧被推得更狠 | 按不可靠处理 |
| `≥ 1000` | 孤立帧可删 | 基本断开 |
| `9999` | 未定位 / 手动设位 / 断开 | **不可用,应安全停止或等待重定位** |
Detour 自己判断“车是否静止可做全局重定位”时用:`l_step > 2` 且 TightCoupler 速度估计很小。这是内部策略,不是对外状态机。
---
## 5. 坐标跳变与重定位行为(优先)
### 5.1 重定位、回环、失败恢复后,`x/y/theta` 会不会永久跳变
**会。跳变是设计行为,不是毛刺。**
会改当前输出的情况:
1. **地图配准成功**`LidarMap` 把当前关键帧改到匹配位姿,并 `l_step = 1`,再交给 TightCoupler。下一帧 `CartLocation` 跟着新参考走,表现为一次台阶。
2. **全局重定位**`/relocalize` 或 UI Relocalize):定位器进入 `relocalizing`,按全图关键帧搜索(`source = 9`)。第一次更好的匹配会 `TightCoupler.Reset`,位姿被拉到地图上,通常是大幅度永久跳变。
3. **回环 + `GraphOptimizer`**:关键帧 `x/y/th` 被就地改写。当前参考帧若被挪动,后续融合位姿跟着变。优化有动量平滑,但仍可能出现肉眼可见的台阶。
4. **手动 `/setLocation`**:直接改 `CartLocation.latest` 并重置融合窗口。
5. **匹配长期失败后突然恢复**:从里程计漂过的位置一下子贴回地图,跳变幅度等于累计漂移。
没有“只在内部跳、对外插值抹平”的保证。控制程序必须自己做跳变连续化或拒绝。
### 5.2 跳变后还在原来的地图坐标系吗
**同一张已加载地图内:是。**
跳的是车在这张图里的估计,不是换了一套轴。
会换坐标系的只有:换图(`/loadMap`)、`Remapper`(GNSS/外参映射)、或手动把车标到另一个锚点。这些不是普通重定位。
### 5.3 有没有“正在重定位 / 定位丢失 / 地图坐标调整”标志
**定位结果包里没有这些标志。**
内部有、但**不随 `getPos` / `DetourPos` 下发**
| 内部量 | 作用 | 是否对外 |
|---|---|---|
| `Locator.relocalizing` / `relocalized` | 地图层正在/已经全局搜 | 否 |
| `G.IsSettingPosition` | 正在手动设位 | 否(`/getStat``globalStat` 里能看到) |
| `G.paused` | 定位暂停 | 否(同上,或调 `/pause` `/resume` |
| `CartLocation.unstable` | 本帧掩膜过狠、场景不稳定 | **仅 HTTP**`error = "Unstable"`。DObject 当前推送的 `error` 被写成空串 |
| `l_step` 升到很大 / `9999` | 实际的“丢了” | 是,但这是间接指标 |
| 图优化改关键帧 | “地图坐标调整” | 无单独标志 |
HTTP `/getPos` 仅有的显式错误:
- `"Timeout"``st_time` 已超过 500 ms
- `"Unstable"``latest.unstable == true`
没有 `"Relocalizing"``"Lost"``"MapAdjusted"`
`/getStat` 可拉到各模块 `StatusMember`(雷达间隔、里程计状态、TC 状态等),但不是每帧位姿附属字段,也不适合当硬实时互锁。
控制侧应自己构造状态:
- **疑似丢失**`error` 为 Timeout/Unstable,或 `l_step ≥ 99`,或 `tick` 长期不涨。
- **疑似重定位/回环跳变**:相邻两帧 `x/y/th` 突变,同时 `l_step` 突然掉回 12。
- **地图在拧**:跳变较缓、持续多帧,且 `l_step` 并不爆掉。
---
## 6. 可用的定位质量接口(优先)
### 6.1 除 `l_step` 外还能拿到什么
**位姿包几乎只有 `l_step` + HTTP 的 `error`。**
| 信息 | 有没有 | 说明 |
|---|---|---|
| 匹配得分 | 对控制程序:无 | 在 `LidarOdometry` / `LidarMap` 内部(`score``phaselocker_score`),不下发 |
| 协方差 | 对控制程序:无 | `Frame.errXX/errXY/errYY` 已预留,**未填进 `/getPos``DetourLocation`** |
| 置信度 | 间接 | 就是 `l_step`;源码 TODO 想换成误差圆,尚未做 |
| 定位状态 | 很弱 | HTTP`Timeout` / `Unstable`DObject`error` 目前恒为空 |
| 错误码 | 无枚举 | 只有上述字符串 |
| 系统状态 | 有,但是慢接口 | `GET /getStat` → 布局/里程计/定位器/TC/GO/`G.paused`/`G.IsSettingPosition` |
因此控制程序**不能**指望每帧拿到匹配分或协方差。
### 6.2 Detour 实际用什么条件接受一帧(可当作官方内部规则)
源码里“接受并提交”的条件是分层的,没有单独的对外规范文档:
1. **里程计层**`LidarOdometry.updateLocation`
- 分数过低会把 `strength` 压下去,甚至不把 `reference` 挂上。
- 掩膜过狠 → `unstable = true`
- 点太少、序贯/局部配准失败 → 不切健康关键帧,并加大 `lstepInc`
2. **融合层**`TightCoupler.CommitLocation`
- 传感器处于 `bad variance` / `bad trace` → 直接丢弃,返回 `null`
- 丢弃后该源会被禁一段时间(约 1~2 s)。
3. **地图层**`LidarMap`
- `result.score < ScoreThres` 丢弃。
- 相对已有位姿的 `xy/th` 偏差超过按 `l_step` 放大的门限则丢弃(防止乱跳)。
- 落到无效区域丢弃。
4. **HTTP 出口**
- 缓存超过 500 ms → `Timeout`
- `unstable``Unstable`
### 6.3 建议控制程序如何接受/拒绝一帧
源码没有写给 `MultiWheelC` 的官方判据。按上面的内部规则,建议:
**接受(正常闭环)**
- HTTP:无 `error`DObject:至少 `tick` 在前进)。
- `l_step ≤ 5`
- `tick` 新鲜(HTTP`now - tick < 300 ms` 较稳妥;DObject:换算成 ms 后同样看提交间隔)。
- 相对上一接受帧:位移/转角不超过本底盘短周期能达到的上限。
**降级(短时预测、降低增益、禁止精细对位)**
- `6 ≤ l_step ≤ 10`,或偶发 Timeout 后立刻恢复。
- 原地自转期间 `l_step` 爬升但 `tick` 仍在更新。
**拒绝并安全停止 / 等待**
- `error == "Timeout"` 持续,或 `error == "Unstable"`
- `l_step ≥ 99`(含 `9999`)。
- 单帧出现与运动学不符的永久台阶(尤其伴随 `l_step` 从很大突然回到 1):先当重定位跳变,做连续化或刹停,不要直接当编码器。
**不要做的事**
- 不要用 `l_step` 当 ICP 迭代次数或“正在计算中”。
- 不要假设 `theta ∈ [-180, 180]`
- 不要用 `tick` 做多车时间同步主时钟。
- 不要假设 DObject 的 `error` 会带 Timeout/Unstable(当前实现是空的)。
---
## 对 `StateEstimation` 的直接含义
清单里最优先的 2 / 4 / 5 / 6,对应控制侧应这样定:
1. **时间同步**
- 先确认 `getCartLocation` 走 HTTP 还是 `DetourPos`。两条通道的 `tick` **单位和语义都不同**
- 单车:用 `tick` 估延迟、做自转短时预测。
- 多车:另选同步时钟。
2. **自转短时预测**
- 自转时有效更新可能变稀,`l_step` 会从个位数爬到几十上百。
- 这是质量变差,不是接口卡死。预测窗口应随 `tick` 变老、`l_step` 变大而缩短。
3. **跳变连续化**
- 重定位、回环、失败恢复都会造成**同地图下的永久台阶**。
- 没有“正在改图”标志;用位姿差分 + `l_step` 回落来识别。
4. **安全停止**
- 硬条件:`Timeout` / `Unstable` / `l_step ≥ 99` / `tick` 停更。
- `l_step` 建议按 `≤5` 正常、`610` 退化、`>10` 不可用于精细控制。
---
## 源码锚点
| 主题 | 位置 |
|---|---|
| 对外 JSON / Timeout / Unstable | `DetourCore/Location.cs``ConstructRet``FormatPosition` |
| HTTP 路由 | `DetourCore/WebAPI.cs``/getPos``/setLocation``/relocalize``/getStat` |
| DObject 推送与 `tick=DateTime.Now.Ticks` | `DetourCore/Algorithms/TightCoupler.cs``DetourLocation``CommitLocation` |
| `l_step` 定义 | `DetourCore/Types/Frame.cs` |
| 车体中心变换、提交缓存 | `TightCoupler.CommitLocation` |
| 时钟 | `DetourCore/G.cs``DetourWatch.TimeStampMillis` |
| 角度回绕 | `DetourCore/LessMath.cs``normalizeTh` |
| 自转时 `l_step` 被加大 | `DetourCore/Algorithms/LidarOdometry.cs``lstepInc` |
| 匹配成功后的位姿跳变 | `DetourCore/LocatorTypes/LidarMap.cs`loop / relocalize |
| 回环拧图 | `DetourCore/Algorithms/GraphOptimizer.cs` |
| 全局重定位入口 | `DetourCore/DetourLib.cs``Relocalize()` |
| 单位缩放 | `DetourCore/Configuration.cs``GuruOptions.inputScale / biasX / biasY` |
---
## 修订记录
- 2026-08-24:按当前 Detour 源码逐条答复原《Detour 信息确认清单》。