# VRS(가상 기지국) → 단일기준국(실제 상시관측소) 전환

> 배경: GNSS 운용 담당자로부터 "가상 기지국(VRS) 말고 주변의 진짜 기지국을 우선으로 잡아라"는
> 조언을 받았고, 실제로 **RTK Fixed 도달 자체가 안 되는** 상황이라 VRS 사용을 중단하기로 결정.
> 조사·구현일 2026-08-22. 캐스터는 직접 조회해 확인했고, 코드는 로컬에서 컴파일·단위검증까지
> 마쳤다. 하드웨어 검증은 §7 절차로 로봇에서 해야 한다.

---

## 1. 결론 먼저

**UM982 모듈 설정 문제가 아니다.** 수신기에는 "어느 기지국을 잡을지" 고르는 명령이 없다
(Unicore N4 매뉴얼 §4.6 `CONFIG RTK` 확인). 기지국 선택은 전적으로 **NTRIP 마운트포인트**가
결정한다.

그리고 조사 중에 **Fixed 가 안 되는 유력한 원인**을 따로 찾았다 — §3. 마운트포인트를 바꿔야
하는 이유가 "기준 프레임 일관성" 뿐만이 아니게 됐다.

---

## 2. Fixed 실패의 유력 원인 — VRS 스트림과 UM982 의 위성군 불일치

### 2.1 두 자료를 대조하면 나온다

**(A) VRS 스트림에 실제로 들어있는 것** — [진단기록 §2.5](UM982-RTK-Fixed-진단기록_2026-07-30.md)
에서 파이썬으로 직접 접속해 파싱한 결과:

```
1005/1007          기준국 좌표/안테나
1030~1033          네트워크 RTK 보조
1075 / 1085 / 1095 / 1115    GPS / GLONASS / Galileo / QZSS  MSM5
※ 북두(BeiDou) 보정 메시지는 이 마운트포인트에 없음
```

**(B) UM982 가 지원한다고 매뉴얼에 적힌 것** — Appendix 2 "Supported RTCM V3 messages":

| 위성군 | 매뉴얼 기재 | VRS 가 보내는 것 | 판정 |
|---|---|---|---|
| GPS | 1001~1004, **1074/1075** | 1075 | ✅ 쓸 수 있음 |
| GLONASS | 1009~1012, **1084/1085** | 1085 | ✅ 쓸 수 있음 |
| BeiDou | 1104, **1123~1127** | **없음** | ❌ 지원하는데 안 옴 |
| Galileo | **1091~1097 없음** (1094 만 base 출력 예시에 등장) | 1095 | ❌ 올 수 있는데 못 씀 |
| QZSS | **1111~1117 아예 없음** | 1115 | ❌ 올 수 있는데 못 씀 |

### 2.2 무슨 뜻인가

VRS 는 4개 위성군 보정을 보내지만, 그중 UM982 가 RTK 에 실제로 쓸 수 있는 건 **GPS 와 GLONASS
둘뿐일 가능성이 높다**. 동시에 UM982 가 잘 지원하는 **BeiDou 는 이 VRS 가 아예 안 보낸다** —
한국에서 북두는 가시위성이 많은데 통째로 놓치는 셈이다.

진단기록에서 "위성 31개 잡히는데 Fixed 가 안 된다"고 했던 것과 앞뒤가 맞는다. **수신기가 추적하는
위성 수(nSV)와, 기지국 보정이 있어서 RTK 모호성 해석에 실제로 기여하는 위성 수는 다르다.**
`CONFIG SIGNALGROUP 4 5` 로 5개 위성군을 추적해도, 보정이 안 오거나 못 읽는 위성군은 RTK 에
기여하지 못한다.

> ⚠️ 이건 **가설**이다. 매뉴얼 Appendix 2 가 불완전할 가능성도 있다(1094 가 다른 절에는 나오는
> 걸 보면 목록 관리가 엉성하다). §7.1 로 확정할 것.

### 2.3 그래서 기준국 선택 기준이 바뀐다
소스테이블의 위성군 문자열(`GPS+GLO+GAL+BDS+QZS`)이 아니라 **실제로 오는 메시지 ID** 로 판단해야
한다. **BeiDou MSM(1124/1125)을 보내는 관측소가 진짜 이득**이고, Galileo/QZSS 는 있으나 마나다.

---

## 3. VRS 를 그만 써야 하는 나머지 이유

### 3.1 기준 프레임이 움직인다
VRS 는 GGA 로 보고한 위치 근처에 가상 기지국을 만들고, 로버가 임계거리를 넘으면 **가상 기지국을
새 위치로 재생성**한다. 그때마다 기준점이 바뀌어 해에 불연속이 생기고 RTK 재초기화가 일어날 수
있다. FAST-LIVO2-RTK 백엔드는 GNSS 해를 GTSAM 팩터로 넣으므로, 궤적 도중의 기준 프레임 변경은
그대로 제약조건 불일치가 된다. 한 스캔 세션 동안 기준점은 고정돼야 한다.

### 3.2 사후처리(PPK)가 불가능하다
VRS 스트림은 서버가 그 순간 합성한 것이라 재현이 안 된다. 실제 관측소를 쓰면
[gnssdata.or.kr](https://gnssdata.or.kr) 에서 해당 시간대 RINEX 를 받아 재처리할 수 있다.

### 3.3 대가 — 거리 의존성
단일기준국은 베이스라인이 멀수록 나빠진다(대략 1km당 1mm + 전리층 비상관). 실무상 10~20km 이내가
좋고, 30km 넘어가면 Fixed 가 눈에 띄게 어려워진다. → 드라이버가 베이스라인을 로깅하고 임계 초과
시 경고하도록 했다(§5).

---

## 4. 캐스터 실측 조사

`tools/ntrip_probe.py table` 로 소스테이블을 직접 받아 확인(조회는 인증 불필요).

| 캐스터 | 서버 | 결과 |
|---|---|---|
| RTS1.ngii.go.kr:2101 | Trimble Pivot 5.2 | STR 5개 **전부 `solution=1`(네트워크)**. VRS-RTCM23/31/34, VRS-CMRx, RTK-RTCM32 |
| RTS2.ngii.go.kr:2101 | Geo++ GNSMART | FKP-RTCM31, VRS-RTCM31/32, SSR-SSRG — **전부 네트워크** |
| **gnssdata.or.kr:2101** | NTRIP Caster 1.1 | **STR 562개 전부 `solution=0`(단일기준국), `nmea=0`, 고유 관측소 176곳** ✅ |
| gnss.eseoul.go.kr:2101 | Trimble Pivot 4.3 | VRS 외에 서울권 관측소별 `solution=0` 스트림 제공 (대안) |

즉 **RTS1/RTS2 에는 단일기준국이 아예 없다.** 진짜 기준국은 GNSS 데이터 통합센터에 있다.
마운트포인트 명명 = `<4자리 관측소코드>-<포맷>` (예: `DBON-RTCM32`). 포맷은 `-RTCM32` 를 쓴다.

RTS1 의 `RTK-RTCM32` 는 이름상 단일기준국 서비스일 수 있으나 소스테이블은 네트워크로 신고한다.
표기는 못 믿는다 — VRS-RTCM34 도 `1004,1014/1015/1016` 을 신고하지만 실제로는 `1030~1033, MSM5`
가 온다. 계정을 그대로 쓸 수 있어 제일 간단한 선택지이므로 §7.2 로 정체를 확인할 가치가 있다.

⚠️ **인증**: gnssdata.or.kr 은 무인증 시 `401`. RTS1 계정(`hyuk6578`)이 여기서도 되는지 **미확인**
이고, 이게 최우선 확인 항목이다. 안 되면 별도 가입 필요(위치기준과 031-210-2655).

---

## 5. 구현 완료 내역

### 5.1 설정 — [`config/um982.yaml`](../src/um982_driver/config/um982.yaml)
```yaml
ntrip_host: "gnssdata.or.kr"
ntrip_mountpoint: "AUTO"        # 최근접 단일기준국 자동 선택 (또는 "DBON-RTCM32" 처럼 고정)
ntrip_auto_format: "RTCM 3.2"
max_baseline_km: 30.0
gga_send_period: 0.0            # 단일기준국은 GGA 업링크 불필요
```

> 수정 후 **반드시 리빌드**. `install/` 은 복사본이라 리빌드 없이는 반영 안 된다
> (진단기록 §2.3 에서 이것 때문에 NTRIP 이 빈 계정으로 붙던 전례).
> `colcon build --packages-select um982_driver` 또는 `--symlink-install`.

### 5.2 드라이버 — [`um982_driver_node.cpp`](../src/um982_driver/src/um982_driver_node.cpp)

**① AUTO 최근접 기준국 선택**
소스테이블을 받아 `solution=0` 인 STR 만 후보로 두고 현재 위치에서 거리순 정렬, 가장 가까운
곳에 붙는다. 접속 실패 시 다음 후보로 자동 전환(관측소 점검/장애 대비, 최대 5곳).
"주변에 있는 진짜 기지국을 우선으로 잡는다"를 그대로 구현한 것.

**② GGA 대기 제거**
기존에는 첫 유효 GGA 가 나올 때까지 NTRIP 접속을 막았다. VRS 는 그래야 하지만 단일기준국은
`nmea=0` 이라 불필요하고, 이 대기 때문에 **수신기가 자력으로 fix 를 얻기 전까지 RTCM 이 한
바이트도 안 들어갔다**. 조건이 나쁜 곳에서 콜드스타트할 때 수렴이 늦어지는 원인.
이제 `gga_send_period <= 0` 이면 즉시 접속한다(AUTO 는 위치가 필요해 첫 GGA 까지만 기다림).

**③ RTCM 스니퍼 — 캐스터가 보낸 것**
주입 경로에서 RTCM3 프레임을 뜯어 30초마다 요약한다:
- 메시지 종류별 카운트
- 보정이 오는 위성군
- 네트워크 RTK 메시지(1014/1015/1016/1030~1035) 발견 시 **경고** → 단일기준국이 아님
- **매뉴얼 미기재 메시지 발견 시 경고** (§2 의 Galileo/QZSS 문제를 런타임에 바로 드러냄)
- 1005/1006 에서 기지국 ID·좌표·**베이스라인 km** 를 로깅, 좌표가 움직이면 VRS 로 판정해 경고

**④ RTCMSTATUS — 수신기가 받아들인 것**
기동 시 `RTCMSTATUSA ONCHANGED` 를 RAM 에만 설정(SAVECONFIG 안 함)하고, 수신기가 뱉는
`#RTCMSTATUSA` 를 파싱해 로깅한다. 매뉴얼 §7.3.56 기준 `<MsgID>,<MsgNum>,<BaseID>,<SatsNum>,<L1~L6>`.
**③과 ④를 대조하면 "캐스터는 보냈는데 수신기가 안 쓴" 위성군이 그대로 드러난다** — §2 가설의
결정적 검증 수단.

**⑤ 접속 거부 사유 로깅**
401(계정)과 404(마운트포인트 오타/미개방)를 구분해 찍는다.

### 5.3 발견해 고친 버그 — RTCM 파서 스톨
프레임 추출에서 `0xD3` 만 보고 동기를 잡으면, 관측치 페이로드 안의 우연한 `0xD3` 을 프레임
시작으로 착각한다. 그 가짜 길이(최대 1023B)만큼 데이터가 더 쌓일 때까지 **파서가 멈춘다**.
RTCM3 는 길이 상위 바이트의 reserved 6bit 가 항상 0 이므로 이걸로 걸러내도록 수정
(`(buf[j+1] & 0xFC) != 0` 이면 오동기). 파이썬 도구에도 동일 수정 적용.

### 5.4 로컬 검증
ROS 없는 맥에서 스텁 헤더로 `-Wall -Wextra` 컴파일 통과. RTCM 헬퍼는 단위테스트로 검증:
1005/1006 파싱·ECEF→위경도 왕복(1e-7도), 안테나고, 베이스라인 거리(파이썬 참조구현과 일치),
프레임 추출(연접/1바이트 분할/선행 쓰레기/가짜 0xD3/CRC 깨짐 복구), 메시지 분류 함수 — **전부 통과**.
※ 이건 로컬 검증이고, **실제 하드웨어·실제 스트림 검증은 안 됐다**(§7).

---

## 6. 조사 도구 — [`tools/ntrip_probe.py`](../tools/ntrip_probe.py)

표준 라이브러리만 쓰는 단독 실행 스크립트. 드라이버를 안 건드리고 캐스터를 조사할 때.

```bash
python3 ntrip_probe.py table gnssdata.or.kr 2101 --filter DBON
python3 ntrip_probe.py near  gnssdata.or.kr 2101 <위도> <경도> --format RTCM3.2
python3 ntrip_probe.py sniff gnssdata.or.kr 2101 DBON-RTCM32 --user <ID> --pass <PW> --secs 60
```

`sniff` 판정 기준:

| 관찰 | 의미 |
|---|---|
| 1014/1015/1016/1030~1035 존재 | ❌ 네트워크 해 (MAC/FKP/VRS) |
| 1005/1006 좌표가 관측 중 이동 | ❌ VRS |
| 네트워크 메시지 없음 + 1005/1006 고정 | ✅ 단일기준국 |
| 1075/1085/1095/1115/1125 구성 | 어느 위성군 보정이 오는지 — **1124/1125(BDS) 유무가 핵심** |

---

## 7. 로봇에서 할 검증 (순서대로)

### 7.1 §2 가설 확정 — 먼저 이것부터
**아직 VRS 인 상태로** 드라이버를 새로 빌드해 띄우고 로그를 본다.
```
[um982_driver]: RTCM 누적 ... | 메시지: 1005x.. 1030x.. 1075x.. 1085x.. 1095x.. 1115x..
[um982_driver]:   ⚠️ 매뉴얼 미기재 메시지: 1095(Galileo) 1115(QZSS) — 이 위성군은 RTK 에 안 쓰일 수 있다
[um982_driver]: 수신기 RTCM 수용: 1075,1234,0,9,...
```
**`수신기 RTCM 수용:` 줄에 1095/1115 가 안 나오면 §2 가설이 맞다.** 나오면 가설은 틀렸고
원인을 다시 찾아야 한다. 어느 쪽이든 이 로그 한 번이면 결판난다.

### 7.2 계정 확인
```bash
python3 tools/ntrip_probe.py sniff gnssdata.or.kr 2101 DBON-RTCM32 --user hyuk6578 --pass <PW> --secs 60
```
`401` 이면 별도 가입 필요. 그동안의 대안으로 RTS1 `RTK-RTCM32` 정체도 확인:
```bash
python3 tools/ntrip_probe.py sniff RTS1.ngii.go.kr 2101 RTK-RTCM32 \
    --user hyuk6578 --pass <PW> --gga-from-latlon <위도> <경도> --secs 60
```

### 7.3 관측소 선정
`near` 로 작업 위치 기준 후보를 뽑고, `sniff` 로 **BDS(1124/1125)를 보내는 곳**을 고른다.
소스테이블 좌표는 대부분 소수점 2자리(약 1km)이고 4개 그룹은 여러 관측소가 같은 좌표로 잘못
적혀 있으니(GGEO/JJHG/SGWI/YNGU 가 모두 38.29,128.14), 실제 거리는 `sniff` 의 1005/1006 으로 확인.

### 7.4 전환 후 A/B
난간 위치(진단기록에서 Fixed 나왔던 조건)에서 VRS vs 단일기준국으로 Fixed 수렴시간·h_acc 비교.
안테나 위치가 여전히 지배적 변수이므로(진단기록 §3) **같은 위치에서** 비교해야 의미가 있다.

---

## 8. 미확정 사항

- [ ] §2 가설 — 수신기가 Galileo/QZSS MSM 을 실제로 버리는가 (7.1 로 확정)
- [ ] RTS1 계정이 gnssdata.or.kr 에서 통하는지
- [ ] RTS1 `RTK-RTCM32` 가 단일기준국인지
- [ ] 작업 위치 기준 최근접 관측소 확정
- [ ] AUTO 선택 로직의 실스트림 동작 (로컬 컴파일·단위테스트만 함)

---

## 9. 관련 파일

| 파일 | 내용 |
|---|---|
| [`tools/ntrip_probe.py`](../tools/ntrip_probe.py) | 소스테이블 조회 / 최근접 기준국 / 스트림 VRS 판정 |
| [`src/um982_driver/config/um982.yaml`](../src/um982_driver/config/um982.yaml) | 캐스터·마운트포인트·GGA 설정 |
| [`src/um982_driver/src/um982_driver_node.cpp`](../src/um982_driver/src/um982_driver_node.cpp) | AUTO 선택, RTCM 스니퍼, RTCMSTATUS 로깅 |
| [`docs/UM982-RTK-Fixed-진단기록_2026-07-30.md`](UM982-RTK-Fixed-진단기록_2026-07-30.md) | VRS 스트림 실제 구성(§2.5), 안테나 위치가 지배적(§3), 리빌드 함정(§2.3) |
| `docs/Unicore Reference Commands Manual ... R1.4.pdf` | Appendix 2 지원 RTCM 목록, §4.6 CONFIG RTK, §7.3.56 RTCMSTATUS |
