# pb-calibration

**当前版本：0.1.1**

基于 **vehicle_config.proto** 的 PB 车辆配置/标定文件（`.pb.txt`）反序列化校验、解析为 YAML、从 YAML 按顺序组装为 `.pb.txt`。  
输出位置与组合位置一致，且可指定。自 0.0.5 起仅使用 `vehicle_config.proto`，不再使用 `calibration.proto`。

## 环境与安装

推荐使用 **Conda** 环境 `py310`，并在该环境中安装本包及依赖：

```bash
conda activate py310
pip install -e .   # 开发安装
# 或
pip install .      # 普通安装
```

若仅安装不开发，也可：

```bash
pip install pb-calibration
```

## 功能

1. **反序列化检查**：检查 `pb.txt` 是否存在格式问题（能否正常解析）。
2. **解析为 YAML**：将 `pb.txt` 解析为每个单独的 yaml 文件，输出到**指定目录**；该目录与后续「组合」时使用的目录一致。
3. **从 YAML 组装**：按顺序将指定目录中的单独 yaml 文件组装成 `pb.txt`（输入目录与解析输出目录一致，可指定）。
4. **YamlConfigManager**（0.0.8）：类，存储默认 YAML 路径；**不缓存 YAML 内容**，每次从默认路径读取，保证多程序共同维护时数据为最新。支持 `read_yaml` / `update_yaml`、`read_order_manifest` / `update_order_manifest`、`rescan` / `get_cached` / `get`，供其他程序获取最新数据。
5. **函数式 API**：`read_yaml`、`update_yaml`、`read_order_manifest`、`update_order_manifest`（显式传路径，无状态）。

## 架构

本包以 **vehicle_config.proto** 为唯一规范：与 `.pb.txt` 的读写均通过生成的 `VehicleConfig` Message 做标准序列化/反序列化，不再使用手写正则或手拼字符串。中间仍保留 **YAML 目录** 作为可编辑、可版本管理的中间表示。

### 数据流

```mermaid
flowchart LR
  subgraph pb [PB 文本]
    PBTxt[".pb.txt"]
  end
  subgraph proto [Proto 层]
    Msg[VehicleConfig]
  end
  subgraph yaml [YAML 中间层]
    Dict[dict]
    YAML["order_manifest + 分组 YAML"]
  end

  PBTxt -->|"check: 仅验证"| Msg
  PBTxt -->|"parse: 反序列化"| Msg
  Msg -->|"Message → dict"| Dict
  Dict -->|"写文件"| YAML
  YAML -->|"读文件"| Dict
  Dict -->|"dict → Message"| Msg
  Msg -->|"build: 序列化"| PBTxt
```

- **check**：`.pb.txt` → 读入文本 → `text_format.Parse(content, VehicleConfig())`，成功即通过，不写 YAML。
- **parse**：`.pb.txt` → 标准反序列化为 `VehicleConfig` → 转为 dict → 按现有目录结构写出 YAML（`order_manifest.yaml`、`vehicle_info/vehicle_param`、`extrinsics/*`、`intrinsics/*`）。
- **build**：从 YAML 目录读取 → 组装为 dict → 填充 `VehicleConfig` → `text_format.MessageToString` 写出 `.pb.txt`。

### 实现要点

| 环节 | 说明 |
|------|------|
| **PB → Message** | 统一使用 `vehicle_config.proto` 生成的 `VehicleConfig` + `google.protobuf.text_format.Parse`，check 与 parse 共用。 |
| **Message → dict** | 转换层（`_proto_io`）将 Message 转为与现有 YAML 一致的 dict，支持 proto 中的 oneof（pinhole / fisheye）及 VehicleParam 嵌套 LatencyParam。 |
| **dict → Message** | build 时从 YAML 得到 dict 后，填充 `VehicleConfig` 再序列化，保证输出符合 proto。 |
| **YAML 目录** | 结构不变：`order_manifest.yaml`、`vehicle_info.yaml`、`vehicle_param.yaml`、`extrinsics/`、`intrinsics/`；仅「pb ↔ 内存」一段改为经 proto。 |

## 命令行

```bash
# 1. 检查 pb.txt 格式
pb-calibration check -i /path/to/vehicle_config.pb.txt

# 2. 解析 pb.txt 为 yaml，输出到指定目录（该目录即后续 build 的输入）
pb-calibration parse -i /path/to/vehicle_config.pb.txt -o /path/to/yaml_dir

# 3. 从指定目录按顺序组装为 pb.txt（目录与 parse 的 -o 一致）
pb-calibration build -i /path/to/yaml_dir -o /path/to/vehicle_config_out.pb.txt
```

可选参数：`--templates /path/to/proto_templates` 指定模板目录，不指定则使用包内模板。

## Python API

### 接口说明

| 接口 | 签名 | 说明 |
|------|------|------|
| **check** | `check(pb_path, proto_templates_dir=None) -> (bool, str \| None)` | 反序列化校验 `.pb.txt` 格式；返回 `(是否通过, 错误信息)`，通过时错误信息为 `None`。 |
| **parse** | `parse(pb_path, yaml_dir, proto_templates_dir=None) -> (dict, dict)` | 将 `.pb.txt` 解析为多份 YAML 并写入 `yaml_dir`；返回 `(解析后的结构化数据, 缺项/漏项报告)`；失败会抛异常。 |
| **build** | `build(yaml_dir, output_pb_path, proto_templates_dir=None) -> None` | 按顺序将 `yaml_dir` 中的 YAML 组装成 `.pb.txt`；无返回值，失败抛异常。 |
| **YamlConfigManager** | `YamlConfigManager(default_yaml_dir)` | 类。`read_yaml(relative_path)`、`update_yaml(relative_path, updates, output_relative_path=None)`、`read_order_manifest()`、`update_order_manifest(updates, output_dir=None)`、`rescan() -> dict`、`get_cached() -> dict`、`get(key) -> dict \| None`。无缓存，每次从磁盘读取。 |
| **read_yaml** | `read_yaml(yaml_path) -> dict` | 读取任意 YAML 文件，返回字典。 |
| **update_yaml** | `update_yaml(yaml_path, updates, output_path=None) -> None` | 对 YAML 做深合并后写回；`output_path` 为 None 时写回原路径。 |
| **read_order_manifest** | `read_order_manifest(yaml_dir) -> dict` | 读取 `yaml_dir/order_manifest.yaml`。 |
| **update_order_manifest** | `update_order_manifest(yaml_dir, updates, output_dir=None) -> None` | 读取 order_manifest、深合并 updates 后写回。 |

**参数说明：**

- **check**  
  - `pb_path` (str, 必填)：`.pb.txt` 文件路径。  
  - `proto_templates_dir` (str \| None, 可选)：proto 模板目录，不传则使用包内模板。

- **parse**  
  - `pb_path` (str, 必填)：`.pb.txt` 文件路径。  
  - `yaml_dir` (str, 必填)：YAML 输出目录，**应与后续 `build` 的输入目录一致**。  
  - `proto_templates_dir` (str \| None, 可选)：proto 模板目录。

- **build**  
  - `yaml_dir` (str, 必填)：包含 `order_manifest.yaml` 及分组 YAML 的目录（即 `parse` 的输出目录）。  
  - `output_pb_path` (str, 必填)：输出的 `.pb.txt` 路径。  
  - `proto_templates_dir` (str \| None, 可选)：proto 模板目录。

### 使用方法

```python
from pb_calibration import check, parse, build

# 1. 反序列化检查
ok, err = check("vehicle_config.pb.txt")
if not ok:
    print("格式错误:", err)

# 2. 解析为 yaml，输出到指定目录（与 build 输入一致）
yaml_dir = "./output/yaml"  # 可任意指定
data, missing_report = parse("vehicle_config.pb.txt", yaml_dir)
# data: 解析后的结构化数据；missing_report: 与模板对比的缺项报告

# 3. 从同一目录按顺序组装为 pb.txt
build(yaml_dir, "vehicle_config_rebuilt.pb.txt")

# 4. 使用 YamlConfigManager（0.0.8）：默认路径、无缓存、每次从磁盘读取
from pb_calibration import YamlConfigManager

mgr = YamlConfigManager(yaml_dir)
om = mgr.read_order_manifest()
vi = mgr.get("vehicle_info")
all_data = mgr.rescan()  # 或 get_cached()，供其他程序获取最新
mgr.update_yaml("vehicle_info.yaml", {"plate": "京A00001"})
build(yaml_dir, "vehicle_config_rebuilt.pb.txt")
```

**典型流程：**

1. **只校验**：`check(pb_path)` → 根据 `ok` 和 `err` 判断格式是否正常。  
2. **PB → 可编辑 YAML**：`parse(pb_path, yaml_dir)` → 在 `yaml_dir` 中修改 YAML。  
3. **YAML → 新 PB**：`build(yaml_dir, output_pb_path)`。  
4. **用 Manager 统一读写**（0.0.8）：`parse(...)` 后创建 `YamlConfigManager(yaml_dir)` → `read_*` / `rescan()` / `get(key)` 从磁盘取数 → `update_*` 修改 → 其他程序可同时维护同一目录，每次 `get`/`rescan` 拿到最新 → `build(...)`。

- **输出位置与组合位置一致**：`parse(..., yaml_dir)` 与 `build(yaml_dir, ...)` 使用同一个 `yaml_dir`，可任意指定路径。

## YAML 目录结构

解析后目录内包含：

- `order_manifest.yaml`：顺序清单
- `vehicle_info.yaml`、`vehicle_param.yaml`
- `extrinsics/000.yaml`、`001.yaml`、…
- `intrinsics/<frame_id>.yaml`、…

组装时从同一目录按 `order_manifest.yaml` 顺序读取并生成 `.pb.txt`。

## 测试

共 **20 个测试**：往返（不变/修改 YAML/长小数容差）、check/parse/build API 及边界、YamlConfigManager 与 YAML 读写（无缓存、从磁盘读取）。使用 `test/vehicle_config.pb.txt` 做 parse → temp YAML → build 往返与语义一致性校验。在项目根目录（V0.0.8）下执行：

```bash
conda activate py310
pip install -e .
pytest test/ -v
```

或单独运行：`pytest test/test_roundtrip.py -v`、`pytest test/test_api.py -v`。

## 依赖

- Python >= 3.10（推荐在 conda 环境 `py310` 下使用）
- PyYAML >= 6.0
- protobuf >= 4.21.0

## 版本与更新

- 版本历史与更新说明见 [update_readme.md](update_readme.md)。

## License

MIT
