# 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 기준 `,,,,`. **③과 ④를 대조하면 "캐스터는 보냈는데 수신기가 안 쓴" 위성군이 그대로 드러난다** — §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 --pass --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 --secs 60 ``` `401` 이면 별도 가입 필요. 그동안의 대안으로 RTS1 `RTK-RTCM32` 정체도 확인: ```bash python3 tools/ntrip_probe.py sniff RTS1.ngii.go.kr 2101 RTK-RTCM32 \ --user hyuk6578 --pass --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 |