You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
680 lines
16 KiB
680 lines
16 KiB
|
1 month ago
|
# Migration 예시 — v1 → v2 → v3 변환
|
||
|
|
|
||
|
|
본 문서는 `device_config` / `protocol_config` 의 **schema 버전별 변환 예시** 와 양측 (Web Configurator, dpworldapp) 의 **변환 함수 spec** 을 제공합니다.
|
||
|
|
|
||
|
|
협의 spec 본문: `docs/config-spec/config-contract-proposal.md`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 변환 순서 + 호환성 정책
|
||
|
|
|
||
|
|
```
|
||
|
|
v1 (현재 — config_device.json default)
|
||
|
|
│ Phase 1 ~ 3 — Boolean/Integer/Enum 정규화
|
||
|
|
▼
|
||
|
|
v2 (옵션 B — flat 정규화, 권장)
|
||
|
|
│ Phase 5 — nested 재설계 (협의 후 결정)
|
||
|
|
▼
|
||
|
|
v3 (옵션 C — nested, future ideal)
|
||
|
|
```
|
||
|
|
|
||
|
|
**호환성 약속** (spec §5.7):
|
||
|
|
- 양측이 항상 **현재 + 직전** schema_version read 가능
|
||
|
|
- write 는 최신 schema 만
|
||
|
|
|
||
|
|
**Reader-level deserializer** 가 옛 형식 (`"on"`, `"534"`) 도 관용 수용. Writer 는 신규 형식만.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. v1 → v2 변환 (device)
|
||
|
|
|
||
|
|
### 2.1 변환 예시 — Boolean 필드
|
||
|
|
|
||
|
|
**v1 (현재)**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"wifi_static": "off",
|
||
|
|
"log_save": "on"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2 변환 후**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 2,
|
||
|
|
"wifi_static": false,
|
||
|
|
"log_save": true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**변환 규칙**:
|
||
|
|
| v1 값 | v2 값 |
|
||
|
|
|---|---|
|
||
|
|
| `"on"` | `true` |
|
||
|
|
| `"off"` | `false` |
|
||
|
|
| `true` | `true` (이미 v2) |
|
||
|
|
| `false` | `false` (이미 v2) |
|
||
|
|
| 그 외 | 에러 (validation reject 또는 default 사용) |
|
||
|
|
|
||
|
|
### 2.2 변환 예시 — Integer 필드
|
||
|
|
|
||
|
|
**v1**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"lte_port": "534",
|
||
|
|
"lte_server_port": "20111"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"lte_port": 534,
|
||
|
|
"lte_server_port": 20111
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**변환 규칙**:
|
||
|
|
| v1 값 | v2 값 |
|
||
|
|
|---|---|
|
||
|
|
| digit string (예: `"534"`) | `int(s)` |
|
||
|
|
| int (예: `534`) | 그대로 |
|
||
|
|
| 비-digit string | 에러 |
|
||
|
|
|
||
|
|
### 2.3 변환 예시 — Naming (WIFI_SSID → ssid_list)
|
||
|
|
|
||
|
|
**v1**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"WIFI_SSID": [
|
||
|
|
{"wifi_ssid": "mobidigm", "wifi_passwd": "mobidigm", "wifi_security": "wpa/wpa2"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"ssid_list": [
|
||
|
|
{"wifi_ssid": "mobidigm", "wifi_passwd": "mobidigm", "wifi_security": "wpa/wpa2"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**참고**: 옵션 B 에서는 array element 의 키 (`wifi_ssid`/`wifi_passwd`/`wifi_security`) 는 그대로. 옵션 C 에서는 `ssid`/`password`/`security` 로 추가 정규화.
|
||
|
|
|
||
|
|
### 2.4 device v1 → v2 전체 예시
|
||
|
|
|
||
|
|
**v1 (default `/var/www/html/config_device.json`)**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"wifi_static": "off",
|
||
|
|
"wifi_ip": "192.168.78.74",
|
||
|
|
"wifi_country_code": "KR",
|
||
|
|
"eth_ip": "192.168.55.55",
|
||
|
|
"lte_port": "534",
|
||
|
|
"lte_server_port": "20111",
|
||
|
|
"protocol_server_ip": "192.168.78.2",
|
||
|
|
"protocol_server_port": 8080,
|
||
|
|
"can_baudrate": 1000,
|
||
|
|
"rs485_databits": 8,
|
||
|
|
"rs485_stopbits": 0,
|
||
|
|
"imu_remap_x": "x",
|
||
|
|
"log_save": "on",
|
||
|
|
"log_max_size": 100,
|
||
|
|
"WIFI_SSID": [
|
||
|
|
{"wifi_ssid": "mobidigm", "wifi_passwd": "mobidigm", "wifi_security": "wpa/wpa2"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2 변환 후**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 2,
|
||
|
|
"wifi_static": false,
|
||
|
|
"wifi_ip": "192.168.78.74",
|
||
|
|
"wifi_country_code": "KR",
|
||
|
|
"eth_ip": "192.168.55.55",
|
||
|
|
"lte_port": 534,
|
||
|
|
"lte_server_port": 20111,
|
||
|
|
"protocol_server_ip": "192.168.78.2",
|
||
|
|
"protocol_server_port": 8080,
|
||
|
|
"can_baudrate": 1000,
|
||
|
|
"rs485_databits": 8,
|
||
|
|
"rs485_stopbits": 0,
|
||
|
|
"imu_remap_x": "x",
|
||
|
|
"log_save": true,
|
||
|
|
"log_max_size": 100,
|
||
|
|
"ssid_list": [
|
||
|
|
{"wifi_ssid": "mobidigm", "wifi_passwd": "mobidigm", "wifi_security": "wpa/wpa2"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. v1 → v2 변환 (protocol)
|
||
|
|
|
||
|
|
### 3.1 변환 예시 — Boolean + ai/di
|
||
|
|
|
||
|
|
**v1**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"dr_on": "on",
|
||
|
|
"odo_on": "on",
|
||
|
|
"ai0": "1",
|
||
|
|
"ai3": "0"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"dr_on": true,
|
||
|
|
"odo_on": true,
|
||
|
|
"ai0": true,
|
||
|
|
"ai3": false
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 변환 예시 — MEID 정규화
|
||
|
|
|
||
|
|
**v1 (default 는 int)**:
|
||
|
|
```json
|
||
|
|
{"MEID": 7000}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2**:
|
||
|
|
```json
|
||
|
|
{"meid": 7000}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 변환 예시 — odo_speed.shift Integer
|
||
|
|
|
||
|
|
**v1**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"odo_speed": {
|
||
|
|
"source": "CAN",
|
||
|
|
"id": "0x18FEFC28",
|
||
|
|
"shift": "8",
|
||
|
|
"mask": "0xffff",
|
||
|
|
"expr": "x*0.05+10.0"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"odo_speed": {
|
||
|
|
"source": "CAN",
|
||
|
|
"id": "0x18FEFC28",
|
||
|
|
"shift": 8,
|
||
|
|
"mask": "0xffff",
|
||
|
|
"expr": "x*0.05+10.0"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.4 protocol v1 → v2 전체 예시
|
||
|
|
|
||
|
|
**v1 일부**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"dev_type": "RTLS",
|
||
|
|
"version": "v1.0",
|
||
|
|
"dr_on": "on",
|
||
|
|
"odo_on": "on",
|
||
|
|
"MEID": 7000,
|
||
|
|
"protocol": "OPC-UA",
|
||
|
|
"ai0": "1",
|
||
|
|
"OPC_UA": [
|
||
|
|
{"field": "TMP1", "ns": "3", "addr": "1001", "odt": "integer", "dv": "-9", "expr": "x*10.0"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v2 변환**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 2,
|
||
|
|
"dev_type": "RTLS",
|
||
|
|
"version": "v1.0",
|
||
|
|
"dr_on": true,
|
||
|
|
"odo_on": true,
|
||
|
|
"meid": 7000,
|
||
|
|
"protocol": "OPC-UA",
|
||
|
|
"ai0": true,
|
||
|
|
"OPC_UA": [
|
||
|
|
{"field": "TMP1", "ns": 3, "addr": 1001, "odt": "integer", "dv": -9, "expr": "x*10.0"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. v2 → v3 변환 (옵션 C 시점)
|
||
|
|
|
||
|
|
### 4.1 device v2 → v3 — Nested grouping
|
||
|
|
|
||
|
|
**v2 일부**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 2,
|
||
|
|
"wifi_static": false,
|
||
|
|
"wifi_ip": "192.168.78.74",
|
||
|
|
"wifi_country_code": "KR",
|
||
|
|
"eth_ip": "192.168.55.55",
|
||
|
|
"lte_port": 534,
|
||
|
|
"lte_server_ip": "104.208.105.62",
|
||
|
|
"lte_server_port": 20111,
|
||
|
|
"protocol_server_ip": "192.168.78.2",
|
||
|
|
"protocol_server_port": 8080,
|
||
|
|
"ssid_list": [
|
||
|
|
{"wifi_ssid": "mobidigm", "wifi_passwd": "mobidigm", "wifi_security": "wpa/wpa2"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v3 변환**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 3,
|
||
|
|
"network": {
|
||
|
|
"wifi": {
|
||
|
|
"static": false,
|
||
|
|
"ip": "192.168.78.74",
|
||
|
|
"country_code": "KR",
|
||
|
|
"ssid_list": [
|
||
|
|
{"ssid": "mobidigm", "password": "mobidigm", "security": "wpa/wpa2"}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"eth": {
|
||
|
|
"ip": "192.168.55.55"
|
||
|
|
},
|
||
|
|
"lte": {
|
||
|
|
"port": 534,
|
||
|
|
"server": {"ip": "104.208.105.62", "port": 20111}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"servers": {
|
||
|
|
"protocol": {"ip": "192.168.78.2", "port": 8080}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 protocol v2 → v3
|
||
|
|
|
||
|
|
**v2 일부**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 2,
|
||
|
|
"dev_type": "RTLS",
|
||
|
|
"version": "v1.0",
|
||
|
|
"dr_on": true,
|
||
|
|
"odo_on": true,
|
||
|
|
"heading_on": true,
|
||
|
|
"ai0": true, "ai1": false, "ai2": false, "ai3": false,
|
||
|
|
"two_byte_order": "big",
|
||
|
|
"four_byte_order": "big"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**v3 변환**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"schema_version": 3,
|
||
|
|
"device": {
|
||
|
|
"type": "RTLS",
|
||
|
|
"version": "v1.0"
|
||
|
|
},
|
||
|
|
"features": {
|
||
|
|
"dr": true,
|
||
|
|
"odo": true,
|
||
|
|
"heading": true
|
||
|
|
},
|
||
|
|
"analog_input": {
|
||
|
|
"enabled": [true, false, false, false]
|
||
|
|
},
|
||
|
|
"byte_order": {
|
||
|
|
"two_byte": "big",
|
||
|
|
"four_byte": "big"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 양측 변환 함수 spec (pseudocode)
|
||
|
|
|
||
|
|
### 5.1 Web Configurator (Python)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# src/migrations.py — v1 → v2 (옵션 B 채택 가정)
|
||
|
|
|
||
|
|
# Boolean 필드 변환 정책
|
||
|
|
_BOOL_FIELDS_DEVICE = ("wifi_static", "log_save")
|
||
|
|
_BOOL_FIELDS_PROTOCOL = (
|
||
|
|
"dr_on", "odo_on", "heading_on", "heading_imu_on",
|
||
|
|
"fix_mode_on", "can_input",
|
||
|
|
"ai0", "ai1", "ai2", "ai3",
|
||
|
|
"di0", "di1", "di2", "di3",
|
||
|
|
)
|
||
|
|
_INT_FIELDS_DEVICE = ("lte_port", "lte_server_port")
|
||
|
|
_RENAME_DEVICE = {"WIFI_SSID": "ssid_list"}
|
||
|
|
_RENAME_PROTOCOL = {"MEID": "meid"}
|
||
|
|
|
||
|
|
|
||
|
|
def _normalize_bool(v):
|
||
|
|
"""v1 'on'/'off' 또는 v2 boolean 모두 v2 boolean 으로."""
|
||
|
|
if isinstance(v, bool):
|
||
|
|
return v
|
||
|
|
if isinstance(v, str):
|
||
|
|
s = v.strip().lower()
|
||
|
|
if s == "on": return True
|
||
|
|
if s == "off": return False
|
||
|
|
if s == "1": return True
|
||
|
|
if s == "0": return False
|
||
|
|
raise ValueError(f"unrecognized boolean value: {v!r}")
|
||
|
|
|
||
|
|
|
||
|
|
def _normalize_int(v):
|
||
|
|
"""digit string 또는 int 을 int 로."""
|
||
|
|
if isinstance(v, bool):
|
||
|
|
raise ValueError("bool not allowed for int field")
|
||
|
|
if isinstance(v, int):
|
||
|
|
return v
|
||
|
|
if isinstance(v, str) and v.strip().lstrip("-").isdigit():
|
||
|
|
return int(v)
|
||
|
|
raise ValueError(f"not a valid int: {v!r}")
|
||
|
|
|
||
|
|
|
||
|
|
def migrate_device_v1_to_v2(cfg: dict) -> dict:
|
||
|
|
"""device_config v1 → v2 변환. idempotent (v2 그대로 두면 no-op)."""
|
||
|
|
out = dict(cfg)
|
||
|
|
|
||
|
|
if out.get("schema_version") == 2:
|
||
|
|
return out # 이미 v2
|
||
|
|
|
||
|
|
# Boolean
|
||
|
|
for k in _BOOL_FIELDS_DEVICE:
|
||
|
|
if k in out:
|
||
|
|
out[k] = _normalize_bool(out[k])
|
||
|
|
|
||
|
|
# Integer
|
||
|
|
for k in _INT_FIELDS_DEVICE:
|
||
|
|
if k in out:
|
||
|
|
out[k] = _normalize_int(out[k])
|
||
|
|
|
||
|
|
# Rename
|
||
|
|
for old, new in _RENAME_DEVICE.items():
|
||
|
|
if old in out:
|
||
|
|
out[new] = out.pop(old)
|
||
|
|
|
||
|
|
# odo_speed.shift / odo_direction.shift 는 protocol_config 에 있으므로 device 에는 없음
|
||
|
|
# (참고용 — protocol 변환은 별도 함수)
|
||
|
|
|
||
|
|
out["schema_version"] = 2
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def migrate_protocol_v1_to_v2(cfg: dict) -> dict:
|
||
|
|
"""protocol_config v1 → v2 변환."""
|
||
|
|
out = dict(cfg)
|
||
|
|
|
||
|
|
if out.get("schema_version") == 2:
|
||
|
|
return out
|
||
|
|
|
||
|
|
# Boolean
|
||
|
|
for k in _BOOL_FIELDS_PROTOCOL:
|
||
|
|
if k in out:
|
||
|
|
out[k] = _normalize_bool(out[k])
|
||
|
|
|
||
|
|
# Rename
|
||
|
|
for old, new in _RENAME_PROTOCOL.items():
|
||
|
|
if old in out:
|
||
|
|
out[new] = out.pop(old)
|
||
|
|
|
||
|
|
# Nested shift Integer
|
||
|
|
for nk in ("odo_speed", "odo_direction"):
|
||
|
|
if isinstance(out.get(nk), dict) and "shift" in out[nk]:
|
||
|
|
out[nk]["shift"] = _normalize_int(out[nk]["shift"])
|
||
|
|
|
||
|
|
# Array element Integer
|
||
|
|
for arr_key in ("OPC_UA", "CAN", "MODBUS"):
|
||
|
|
if isinstance(out.get(arr_key), list):
|
||
|
|
for elem in out[arr_key]:
|
||
|
|
if not isinstance(elem, dict):
|
||
|
|
continue
|
||
|
|
for k in ("ns", "addr", "shift", "dv"):
|
||
|
|
if k in elem and not isinstance(elem[k], int):
|
||
|
|
elem[k] = _normalize_int(elem[k])
|
||
|
|
|
||
|
|
out["schema_version"] = 2
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
# v2 → v3 변환 (옵션 C 채택 시)
|
||
|
|
def migrate_device_v2_to_v3(cfg: dict) -> dict:
|
||
|
|
"""device_config v2 → v3 변환. nested 재설계."""
|
||
|
|
if cfg.get("schema_version") == 3:
|
||
|
|
return cfg
|
||
|
|
|
||
|
|
out = {"schema_version": 3}
|
||
|
|
network = {}
|
||
|
|
|
||
|
|
# wifi
|
||
|
|
wifi = {}
|
||
|
|
if "wifi_static" in cfg: wifi["static"] = cfg["wifi_static"]
|
||
|
|
if "wifi_ip" in cfg: wifi["ip"] = cfg["wifi_ip"]
|
||
|
|
if "wifi_netmask" in cfg: wifi["netmask"] = cfg["wifi_netmask"]
|
||
|
|
if "wifi_gateway" in cfg: wifi["gateway"] = cfg["wifi_gateway"]
|
||
|
|
if "wifi_dns1" in cfg: wifi["dns1"] = cfg["wifi_dns1"]
|
||
|
|
if "wifi_dns2" in cfg: wifi["dns2"] = cfg["wifi_dns2"]
|
||
|
|
if "wifi_country_code" in cfg: wifi["country_code"] = cfg["wifi_country_code"]
|
||
|
|
if "ssid_list" in cfg:
|
||
|
|
# ssid_list element 도 key rename
|
||
|
|
wifi["ssid_list"] = [
|
||
|
|
{
|
||
|
|
"ssid": e.get("wifi_ssid"),
|
||
|
|
"password": e.get("wifi_passwd"),
|
||
|
|
"security": e.get("wifi_security"),
|
||
|
|
}
|
||
|
|
for e in cfg["ssid_list"]
|
||
|
|
]
|
||
|
|
if wifi:
|
||
|
|
network["wifi"] = wifi
|
||
|
|
|
||
|
|
# eth
|
||
|
|
eth = {}
|
||
|
|
if "eth_ip" in cfg: eth["ip"] = cfg["eth_ip"]
|
||
|
|
if "eth_netmask" in cfg: eth["netmask"] = cfg["eth_netmask"]
|
||
|
|
if "eth_gateway" in cfg: eth["gateway"] = cfg["eth_gateway"]
|
||
|
|
if eth:
|
||
|
|
network["eth"] = eth
|
||
|
|
|
||
|
|
# lte
|
||
|
|
lte = {}
|
||
|
|
if "lte_ip" in cfg: lte["ip"] = cfg["lte_ip"]
|
||
|
|
if "lte_netmask" in cfg: lte["netmask"] = cfg["lte_netmask"]
|
||
|
|
if "lte_gateway" in cfg: lte["gateway"] = cfg["lte_gateway"]
|
||
|
|
if "lte_port" in cfg: lte["port"] = cfg["lte_port"]
|
||
|
|
if "lte_server_ip" in cfg or "lte_server_port" in cfg:
|
||
|
|
lte["server"] = {
|
||
|
|
"ip": cfg.get("lte_server_ip"),
|
||
|
|
"port": cfg.get("lte_server_port"),
|
||
|
|
}
|
||
|
|
if lte:
|
||
|
|
network["lte"] = lte
|
||
|
|
|
||
|
|
if network:
|
||
|
|
out["network"] = network
|
||
|
|
|
||
|
|
# servers
|
||
|
|
servers = {}
|
||
|
|
for prefix, key in [
|
||
|
|
("protocol", "protocol"),
|
||
|
|
("update", "update"),
|
||
|
|
("rtcm", "rtcm"),
|
||
|
|
("opc_ua", "opc_ua"),
|
||
|
|
("modbus", "modbus"),
|
||
|
|
]:
|
||
|
|
ip_k = f"{prefix}_server_ip"
|
||
|
|
port_k = f"{prefix}_server_port"
|
||
|
|
if ip_k in cfg or port_k in cfg:
|
||
|
|
servers[key] = {"ip": cfg.get(ip_k), "port": cfg.get(port_k)}
|
||
|
|
if servers:
|
||
|
|
out["servers"] = servers
|
||
|
|
|
||
|
|
# serial
|
||
|
|
serial = {}
|
||
|
|
can = {}
|
||
|
|
if "can_bus_type" in cfg: can["bus_type"] = cfg["can_bus_type"]
|
||
|
|
if "can_baudrate" in cfg: can["baudrate"] = cfg["can_baudrate"]
|
||
|
|
if can:
|
||
|
|
serial["can"] = can
|
||
|
|
|
||
|
|
rs485 = {}
|
||
|
|
for src, dst in [
|
||
|
|
("rs485_mode", "mode"),
|
||
|
|
("rs485_baudrate", "baudrate"),
|
||
|
|
("rs485_databits", "databits"),
|
||
|
|
("rs485_parity", "parity"),
|
||
|
|
("rs485_stopbits", "stopbits"),
|
||
|
|
]:
|
||
|
|
if src in cfg:
|
||
|
|
rs485[dst] = cfg[src]
|
||
|
|
if rs485:
|
||
|
|
serial["rs485"] = rs485
|
||
|
|
|
||
|
|
if serial:
|
||
|
|
out["serial"] = serial
|
||
|
|
|
||
|
|
# hardware.imu
|
||
|
|
imu_remap = {}
|
||
|
|
imu_sign = {}
|
||
|
|
for axis in ("x", "y", "z"):
|
||
|
|
if f"imu_remap_{axis}" in cfg:
|
||
|
|
imu_remap[axis] = cfg[f"imu_remap_{axis}"]
|
||
|
|
if f"imu_remap_{axis}_sign" in cfg:
|
||
|
|
imu_sign[axis] = cfg[f"imu_remap_{axis}_sign"]
|
||
|
|
if imu_remap or imu_sign:
|
||
|
|
out["hardware"] = {"imu": {}}
|
||
|
|
if imu_remap:
|
||
|
|
out["hardware"]["imu"]["remap"] = imu_remap
|
||
|
|
if imu_sign:
|
||
|
|
out["hardware"]["imu"]["sign"] = imu_sign
|
||
|
|
|
||
|
|
# log
|
||
|
|
log = {}
|
||
|
|
if "log_save" in cfg: log["save"] = cfg["log_save"]
|
||
|
|
if "log_max_size" in cfg: log["max_size"] = cfg["log_max_size"]
|
||
|
|
if "log_max_duration" in cfg: log["max_duration"] = cfg["log_max_duration"]
|
||
|
|
if log:
|
||
|
|
out["log"] = log
|
||
|
|
|
||
|
|
return out
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.2 dpworldapp (디바이스 측 config-reader 계약)
|
||
|
|
|
||
|
|
디바이스 측 동작은 **.56의 배포 dpworldapp 바이너리 대조로 검증**한 아래 contract를
|
||
|
|
충족하면 된다 (구현 언어/방식은 무관). 양측이 동일 의미를 보장하도록 동작만 명시한다:
|
||
|
|
|
||
|
|
- **`schema_version`**: 정수로 read. 없으면 v1(레거시)로 간주.
|
||
|
|
- **`wifi_static`** (boolean, v1 관용 수용): `true`/`false` 외에 문자열
|
||
|
|
`"on"`/`"true"`/`"1"` → `true`, `"off"`/`"false"`/`"0"` → `false` 로 해석.
|
||
|
|
그 외 값은 거부(invalid).
|
||
|
|
- **`lte_port`** (string-or-integer 관용 수용): 정수 또는 숫자 문자열(`"5000"`)을
|
||
|
|
모두 정수로 read. 숫자가 아닌 문자열은 거부(invalid).
|
||
|
|
|
||
|
|
> 위 관용 수용 규칙은 **reader 단계에서만** 적용되며, write 시점에는 정규(canonical)
|
||
|
|
> 형식(boolean·integer)으로 저장한다.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 운영 시나리오
|
||
|
|
|
||
|
|
### 6.1 양측 v1 → v2 동시 도입
|
||
|
|
|
||
|
|
1. 양측 v1 호환 deserializer + v2 writer 동시 배포
|
||
|
|
2. 디바이스 startup 시 양측이 v1 데이터 read 가능 → 메모리에서 v2 형식 유지
|
||
|
|
3. 다음 write 시점에 v2 형식으로 DB 저장 + `schema_version=2` 명시
|
||
|
|
4. 이후 read 시 schema_version=2 확인 → 변환 skip
|
||
|
|
|
||
|
|
### 6.2 옛 fleet 의 v1 → v2 자동 migration
|
||
|
|
|
||
|
|
- Web Configurator: `migrations.py` 의 `migrate_device_v1_to_v2` 자동 실행
|
||
|
|
- dpworldapp: startup 시 `migrate_*_config()` 자동 실행
|
||
|
|
- 양측 모두 idempotent — 여러 번 실행해도 안전
|
||
|
|
|
||
|
|
### 6.3 v2 → v3 단계적 전환 (옵션 C 채택 시)
|
||
|
|
|
||
|
|
- 양측이 v2 read + write 유지 (안정)
|
||
|
|
- v3 호환 deserializer + writer 추가 배포
|
||
|
|
- 운영자 결정 시점에 manual schema_version bump (또는 자동)
|
||
|
|
- v2 → v3 변환 함수 양측 동시 실행
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 검증 (양측 동일 spec)
|
||
|
|
|
||
|
|
### 7.1 JSON Schema validation
|
||
|
|
|
||
|
|
양측 모두 변환 후 `device_schema_v2.json` (또는 v3) 으로 validation:
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Web Configurator
|
||
|
|
import json
|
||
|
|
import jsonschema # 또는 자체 validator
|
||
|
|
schema = json.load(open("docs/dpworldapp_schema_handoff/device_schema_v2.json"))
|
||
|
|
jsonschema.validate(instance=device_config, schema=schema)
|
||
|
|
```
|
||
|
|
|
||
|
|
```java
|
||
|
|
// dpworldapp (json-schema-validator 라이브러리 사용 예)
|
||
|
|
JsonSchema schema = factory.getSchema(deviceSchemaV2Stream);
|
||
|
|
Set<ValidationMessage> errors = schema.validate(deviceConfigJsonNode);
|
||
|
|
if (!errors.isEmpty()) {
|
||
|
|
// reject
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7.2 검증 실패 시
|
||
|
|
|
||
|
|
spec §5.8 정책: hard reject + 명시 에러.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 잠재 함정
|
||
|
|
|
||
|
|
### 8.1 양측 동시 도입 안 됐을 때
|
||
|
|
|
||
|
|
- Web Configurator v2, dpworldapp v1: dpworldapp 이 v2 형식 못 읽음 → BC deserializer 필수
|
||
|
|
- Web Configurator v1, dpworldapp v2: Web Configurator 가 v2 read 못 함 → 위 마찬가지
|
||
|
|
|
||
|
|
→ **양측 BC deserializer 가 안전망**
|
||
|
|
|
||
|
|
### 8.2 array element 의 정규화 불일치
|
||
|
|
|
||
|
|
옵션 B: `ssid_list[].wifi_ssid` 유지 vs 옵션 C: `ssid_list[].ssid`
|
||
|
|
|
||
|
|
협의 시 array element 의 prefix 정규화도 같이 결정.
|
||
|
|
|
||
|
|
### 8.3 정규화 후 enum mismatch
|
||
|
|
|
||
|
|
예: dpworldapp 이 `wpa/wpa2` 만 알고 사용자가 `wpa3` 추가하면 reject.
|
||
|
|
|
||
|
|
→ 새 enum value 추가는 RFC-like 프로세스 (spec §5.6) 거쳐야 함.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 변경 이력
|
||
|
|
|
||
|
|
| 날짜 | 변경 | 출처 |
|
||
|
|
|---|---|---|
|
||
|
|
| 2026-06-10 | 초안 작성 | Web Configurator team |
|
||
|
|
| (협의 후) | 옵션 채택 + 함수 spec 정밀화 | dpworldapp 팀 협의 결과 |
|