Skip to content

Commit c75e4f2

Browse files
authored
Fix TB1 LiDAR forward-axis convention (#13)
* Fix TB1 LiDAR forward-axis convention * Align robotless scans with TB1 sensor axis * Record TB1 scan-axis deployment evidence
1 parent 2b6b588 commit c75e4f2

14 files changed

Lines changed: 208 additions & 20 deletions

File tree

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ bringup, 상태 수집, 안전 제어, 웹 관제, 자율주행, 작업과 장
1515
> 19cm 로컬 clearance, 현재 pose 자유 셀
1616
> 감시를 배포했다. 15분 15초 정상 주행 로그 77건으로 Production 모델을 승격했고 정상 5분
1717
> replay 3개는 모두 NORMAL, transform timeout과 충돌·제어 지연 replay는 ANOMALY로 분리했다.
18-
> 지도 방향 표시는 원을 로봇 중심, 삼각형을 앞방향으로 명확히 했고, 마지막 현재 pose 오차는
19-
> LiDAR 일치율 13%에서 전역 정렬 91%로 복구해 적용 뒤 89%·지도 내부 100%를 재확인했다.
18+
> 지도 방향 표시는 원을 로봇 중심, 삼각형을 `base_link +X` 앞방향으로 명확히 했다. 이후
19+
> 물리 전방과 웹 전방이 반대라는 현장 관찰로 `base_link→base_scan`은 0°인데 LDS-02 원본
20+
> angle 0이 실제 전방과 180° 어긋난 센서 축 계약을 발견했다. Nav2용 정규화 스캔과 웹 raw-scan
21+
> overlay에 동일한 π rad 보정을 추가했으며, e-stop 아래 재정합과 짧은 전진을 다시 검증한다.
2022
> TB2와 자동 작업 할당은 완료 범위에 포함하지 않는다.
2123
2224
## 프로젝트 목표
@@ -56,7 +58,7 @@ bringup, 상태 수집, 안전 제어, 웹 관제, 자율주행, 작업과 장
5658
| Phase 4 | 단일 로봇 웹 관제 | 완료, `main` 병합 |
5759
| Phase 5 | TB1 SLAM·AMCL·Nav2와 웹 목적지 제어 | 완료, TB1 실차 수용 시험 및 제한사항 기록 |
5860
| Phase 6 | TB1 로그·장애 감지·단일 작업 수명주기 | 완료, TB1 terminal·프로세스 복구·재부팅 수용 시험 기록 |
59-
| Phase 7 | 정밀 지도 UX와 ROS 2 로그 이상탐지 MLOps | 완료, TB1 Production 승격·정상/오류 replay 수용 시험 기록 |
61+
| Phase 7 | 정밀 지도 UX와 ROS 2 로그 이상탐지 MLOps | 센서 전방축 보정 실차 재검증 중, MLOps Production·replay 수용 시험은 완료 |
6062

6163
완료 표시는 실제 검증한 범위에만 사용한다. Phase 1의 `/scan` 수신은 확인했지만 정확한 발행 주기는 아직 기록하지 못했다.
6264

@@ -126,6 +128,11 @@ Phase 5 실차 수용 시험에서는 다음을 확인했다.
126128
LiDAR endpoint로 확인했다. 자동 scan-map 정렬은 잘못된 후보의 일치도 8%를 92~93%로
127129
보정했고, 지도 내부 endpoint 비율은 99~100%였다. 정합률 35% 미만 pose는 초기 위치 서비스에
128130
전달하지 않는다.
131+
- 후속 물리 방향 시험에서 scan-map 점수는 높아도 웹 `base_link +X` 화살표와 실제 차체 전방이
132+
반대임을 확인했다. TF는 `base_link→base_scan` yaw 0°였지만 TB1 LDS-02 원본 angle 0이 실제
133+
차체 전방의 반대였으므로, `/scan_normalized`와 Gateway overlay에 같은 π rad 외부각 보정을
134+
적용했다. 이 발견으로 기존 180° AMCL 후보는 사용자 드래그 오류만이 아니라 센서 축 오류의
135+
결과로 재분류했다.
129136

130137
낮은 장애물이 LiDAR 평면 아래에 있어 검출되지 않은 현장 사례와, 큰 방향 전환을 반복한
131138
목표에서 recovery 후 stale 취소된 관찰은 운영 제한으로 남겼다. 상세 측정과 판정은

control/fleet_gateway/config/tb1.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ fleet_gateway:
2626
scan_sensor_y_m:
2727
- 0.0
2828
scan_sensor_yaw_rad:
29-
- 0.0
29+
# Keep the web raw-scan overlay in the same base_link +X convention.
30+
- 3.141592653589793

control/fleet_gateway/fleet_gateway/mock_robot.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
from std_srvs.srv import SetBool
2929

3030

31+
TB1_RAW_SCAN_YAW_OFFSET_RAD = math.pi
32+
33+
3134
def navigation_duration(target_x: float) -> float:
3235
"""Return deterministic mock duration for success and cancel exercises."""
3336
return 30.0 if target_x >= 1.5 else 2.0
@@ -47,13 +50,14 @@ def square_room_scan_ranges(
4750
pose: Tuple[float, float, float],
4851
sample_count: int = 360,
4952
wall_coordinate: float = 1.975,
53+
sensor_yaw_rad: float = TB1_RAW_SCAN_YAW_OFFSET_RAD,
5054
) -> List[float]:
51-
"""Ray-cast an ideal local scan against the mock map boundary."""
55+
"""Ray-cast raw TB1 scan angles against the mock map boundary."""
5256
x_value, y_value, yaw = pose
5357
ranges = []
5458
increment = 2.0 * math.pi / max(1, sample_count - 1)
5559
for index in range(sample_count):
56-
angle = yaw - math.pi + index * increment
60+
angle = yaw - math.pi + index * increment + sensor_yaw_rad
5761
direction_x = math.cos(angle)
5862
direction_y = math.sin(angle)
5963
candidates = []

control/fleet_gateway/test/test_mock_robot.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,12 @@ def test_square_room_scan_tracks_pose_and_yaw() -> None:
4242

4343
assert min(ranges) == pytest.approx(1.475, abs=0.001)
4444
assert all(0.12 <= distance <= 3.5 for distance in ranges)
45+
46+
47+
def test_square_room_raw_scan_uses_tb1_rear_facing_angle_zero() -> None:
48+
ranges = square_room_scan_ranges((0.5, 0.0, 0.0))
49+
50+
# Raw -pi points along physical base_link +X and is normalized to bin 0.
51+
assert ranges[0] == pytest.approx(1.475, abs=0.001)
52+
# Raw angle 0 points along physical -X before the pi-radian correction.
53+
assert ranges[180] == pytest.approx(2.475, abs=0.02)

docs/case-studies/slam-toolbox-variable-scan-normalization.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ scan_normalizer
5353
처리 규칙은 다음과 같다.
5454

5555
1. 원본 샘플의 실제 각도를 `angle_min + index * angle_increment`로 계산한다.
56-
2. 각도를 0~2π 범위로 감싼다.
57-
3. 가장 가까운 1도 bin에 측정값을 배치한다.
58-
4. 같은 bin에 둘 이상이 들어오면 더 가까운 거리를 선택한다.
59-
5. 관측되지 않은 bin과 유효 범위 밖 측정은 `+inf`로 둔다.
60-
6. 원본 `/scan`은 Robot Agent와 장애 분석을 위해 그대로 보존한다.
56+
2. TB1 LDS-02의 센서 전방축을 차체 `base_link +X`에 맞추는 π rad 외부각을 더한다.
57+
3. 각도를 0~2π 범위로 감싼다.
58+
4. 가장 가까운 1도 bin에 측정값을 배치한다.
59+
5. 같은 bin에 둘 이상이 들어오면 더 가까운 거리를 선택한다.
60+
6. 관측되지 않은 bin과 유효 범위 밖 측정은 `+inf`로 둔다.
61+
7. 원본 `/scan`은 Robot Agent와 장애 분석을 위해 그대로 보존한다.
6162

6263
`+inf`는 가짜 거리나 0을 넣는 것보다 안전하다. 0은 로봇 바로 앞 장애물로 해석될 수
6364
있고, 임의 보간은 센서가 보지 않은 장애물 정보를 만들어 낼 수 있다.
@@ -69,9 +70,24 @@ scan_normalizer
6970
오류는 벽을 휘거나 겹치게 만든다. 각도 기준 재투영은 배열 위치와 물리 방향의 계약을
7071
유지한다.
7172

73+
## 후속 현장 회귀: 고정 격자와 전방축은 서로 다른 문제다
74+
75+
고정 360-bin만 검증한 뒤 웹 삼각형과 실제 로봇 전방이 반대라는 물리 관찰이 나왔다.
76+
`tf2_echo base_link base_scan`은 yaw 0°였고 normalizer도 원본 각도를 offset 없이 bin에 넣고
77+
있었다. 즉 배열 형상은 안정됐지만 “angle 0이 어느 물리 방향인가”라는 외부각 계약이 틀렸다.
78+
scan-map matcher는 잘못된 센서 축을 180° 돌아간 AMCL pose로도 높은 점수를 만들 수 있어
79+
정합률만으로 차체 전방의 의미까지 검증할 수 없었다.
80+
81+
해결은 TB1 normalizer의 `angle_offset_rad`와 Gateway `scan_sensor_yaw_rad`를 모두 π로 고정하는
82+
것이다. Nav2·AMCL·SLAM은 `/scan_normalized`를 사용하고 웹은 보존된 raw `/scan`에 같은 외부각을
83+
적용한다. 보정값 0의 기존 동작, π의 180° bin 이동과 비유한값 거부를 단위 테스트로 고정했다.
84+
배포 뒤에는 e-stop 아래 초기 위치와 endpoint 정합을 새로 계산하고, 실제 차체 앞과 웹 삼각형이
85+
같은지 확인한 뒤에만 짧은 전진 시험을 한다.
86+
7287
## 검증 결과
7388

74-
- 새 단위 테스트 7개 통과
89+
- 스캔 정규화 단위·운영 설정 테스트 19개 통과
90+
- navigation agent 106개와 Gateway 79개, 총 185개 ROS 회귀 테스트 통과
7591
- TB1 `fleet_navigation` 15개 테스트 통과
7692
- 실제 `/scan_normalized` 20개 모두 길이 360
7793
- 고정 `angle_min=0`, `angle_increment=1도`, `angle_max=359도`

docs/design/phase-7-ros2-log-mlops.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ overlay를 분리한다. fit·zoom·pan 하나의 affine viewport를 그리기
125125
거리장을 만든 뒤 자유 셀과 yaw를 전역 coarse search하고 sub-cell·sub-degree로 두 번 보정한다.
126126
브라우저의 `LiDAR 자동 정렬`은 결과 pose와 다음 지표를 표시하지만 자동 적용하지 않는다.
127127

128+
TB1 센서 좌표 계약은 원본 LDS-02 angle 0에 π rad를 더해야 실제 `base_link +X`가 된다.
129+
Nav2·AMCL·SLAM이 소비하는 `/scan_normalized`는 normalizer의 `angle_offset_rad`로 보정하고,
130+
Gateway가 보존된 raw `/scan`을 base-local endpoint로 만들 때는 `scan_sensor_yaw_rad`에 같은
131+
값을 사용한다. 두 설정은 함께 변경해야 하며 운영 설정 테스트가 π 값을 고정한다. scan-map
132+
matcher는 잘못된 센서 축을 pose 180° 회전으로 보상할 수 있으므로 점수 gate만으로 이 계약을
133+
검증하지 않는다. 배포 뒤 e-stop 아래 재정합과 실제 차체 앞·웹 삼각형 비교를 별도 수용 gate로 둔다.
134+
128135
- `matched_ratio`: endpoint가 지도 벽 10cm 이내에 들어온 비율
129136
- `inside_ratio`: endpoint가 지도 범위 안에 들어온 비율
130137
- `score`: 벽 거리의 Gaussian 점수에서 지도 밖 endpoint를 감점한 값
@@ -161,6 +168,7 @@ authorization을 닫는다. LiDAR 중심 기준 0.05m는 footprint 안쪽이므
161168
- [x] 확대·이동·고해상도 overlay·셀 중앙 선택 지도 뷰포트
162169
- [x] 실시간 LiDAR overlay·scan-map 자동 정렬·정합 미달 초기 pose 거부
163170
- [x] 단위·API·지도 좌표·scan 정합 회귀 테스트
171+
- [ ] π rad 센서 전방축 보정 뒤 실제 차체 앞·웹 삼각형·짧은 전진 방향 일치 재검증
164172
- [x] TB1 `/rosout` 수신과 일별 raw artifact 실측(2026-07-19, 누적 10,334건)
165173
- [x] 정상 운전 기준 후보 학습·품질 검토·Production 승격
166174
- [x] 알려진 오류 로그 주입에서 ANOMALY와 원인 특징 확인
@@ -170,4 +178,5 @@ authorization을 닫는다. LiDAR 중심 기준 0.05m는 footprint 안쪽이므
170178
Production model `ros2-log-mad-2026-07-19T134802.259185+0000-6576983b`를 승격했다. 정상 5분
171179
replay 3개는 모두 NORMAL(score 25·14·20), transform timeout과 충돌·제어 지연 구간은
172180
ANOMALY(score 80)였다. false positive는 0/3이며 raw 30일, dataset·model·registry 자동 삭제
173-
없음을 운영 기준으로 확정했다. 이 증거로 Phase 7을 완료로 판정한다.
181+
없음을 운영 기준으로 확정했다. MLOps 수용 범위는 완료했지만, 후속 현장 회귀에서 발견한 센서
182+
전방축 보정의 물리 방향 재검증 전까지 Phase 7 전체 완료 판정은 다시 보류한다.

docs/learning-log/2026-07-19-phase-7-ros2-log-mlops-and-map-viewport.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,55 @@ inside 51.25%여서 화면 변환이 아니라 AMCL pose 자체가 틀린 상태
240240
`(-0.817,-0.122,-0.905rad)`를 받았고 재검증 match 88.75%, inside 100%였다. 웹에서도 붉은
241241
LiDAR endpoint가 지도 벽과 다시 겹쳤으며 active command 없음과 e-stop을 유지했다.
242242

243+
## 후속 물리 방향 시험: 센서 전방축 180° 불일치
244+
245+
초기 위치를 다시 적용한 뒤 5cm 앞 목표는 Nav2의 10cm 위치 허용오차 안이라 실제 이동 없이
246+
성공했다. 15cm 앞 목표에서는 로컬 감시가 odom 각속도 0.323650rad/s를 검출해 즉시 e-stop으로
247+
취소했고, 사용자가 실제 로봇의 전방과 웹 삼각형 전방이 서로 반대라고 확인했다. 목표는 남지
248+
않았고 e-stop 활성, motion unarmed와 0속도를 유지한 채 추가 이동을 중단했다.
249+
250+
정지 상태 진단에서 `base_link→base_scan` TF yaw는 0°였고 `/scan``/scan_normalized`
251+
frame도 `base_scan`이었다. 그러나 normalizer는 원본 angle에 외부각을 더하지 않았고 Gateway의
252+
`scan_sensor_yaw_rad`도 0이었다. 즉 UI 삼각형은 올바르게 `base_link +X`를 표시했지만 두 scan
253+
소비자는 LDS-02 원본 angle 0을 실제 차체 전방으로 잘못 가정했다. 이전의 180° 전역 정렬 결과는
254+
사용자 yaw 입력만의 문제가 아니라 matcher가 이 센서 축 오류를 pose 회전으로 보상한 결과로
255+
재분류했다.
256+
257+
TB1 `/scan_normalized`에 π rad `angle_offset_rad`를 추가하고 Gateway raw scan overlay에도 같은
258+
π rad를 적용했다. 원본 `/scan`은 진단을 위해 변경하지 않았다. 보정 0의 호환 동작, π에서
259+
angle 0이 180번 bin으로 이동하는 동작, 비유한 offset 거부를 추가했으며 navigation agent
260+
106개와 Gateway 79개, 총 185개 회귀 테스트가 통과했다. 배포 뒤에도 e-stop에서 기존 pose를
261+
폐기하고 scan-map 정합·웹 삼각형·실제 차체 앞을 다시 확인한 다음에만 짧은 전진을 허용한다.
262+
263+
첫 GitHub Actions 실행은 robotless navigation goal이 20초 지도 진척 감시에 걸려 실패했다.
264+
제품의 π 보정은 적용됐지만 fixture가 여전히 raw angle 0을 차체 전방으로 생성해, 가상 센서와
265+
차체 운동의 계약이 실제 TB1과 달랐기 때문이다. robotless fixture와 Dashboard mock도 raw
266+
angle 0이 물리 후방을 뜻하도록 π 센서 외부각을 반영하고, 비대칭 pose에서 raw -π가 전방 벽,
267+
raw 0이 후방 벽 거리를 내는 단위·운영 설정 테스트를 추가했다. 이는 안전 감시를 느슨하게 하지
268+
않고 테스트 장비가 제품의 물리 센서 계약을 그대로 재현하도록 고친 것이다.
269+
270+
두 번째 GitHub Actions run `29691887157`은 Humble build, 운영 파일 검증, navigation·Zenoh
271+
action·task/fault robotless smoke와 전체 workspace test를 모두 통과했다. 관제 PC에서도
272+
Gateway 80개와 navigation agent 107개, 총 187개 패키지 테스트와 동일한 navigation smoke를
273+
통과했고, 최대 선속도 0.05m/s·최대 각속도 0.16154rad/s·종료 0속도를 확인했다.
274+
275+
e-stop을 명시적으로 활성화한 뒤 TB1을 commit `6101ab9`로 배포했다. aarch64 TB1에서
276+
navigation agent 107개 테스트가 통과했고 실제 `/scan_normalized` node parameter는
277+
`angle_offset_rad=3.141592653589793`, 출력은 0~359° 360-bin이었다. Gateway에도 같은 π 설정을
278+
배포한 뒤 health와 TB1 online을 확인했다. 보정된 scan의 전역 정렬 후보는 score 0.88018,
279+
match 90.625%, inside 99.375%였고 pose `(-0.385,-0.270,0.131rad)`를 초기 위치로 적용했다.
280+
새 AMCL pose `(-0.405,-0.266,0.122rad)`를 다시 평가한 seed는 score 0.86184, match 90.625%,
281+
inside 100%였다. 이 시점에도 Nav2·localization ready, e-stop active, motion unarmed, active goal
282+
없음을 유지했다. 남은 물리 gate는 웹 삼각형과 실제 차체 앞의 일치 확인 및 짧은 전진 한 번이다.
283+
284+
사용자 부재 전 웹에서 보낸 후속 목표 `(0.315,-0.535,3.136rad)`는 31.60초 뒤 지도 진척
285+
20초 감시에 의해 `FAILED`가 됐다. 종료 pose는 `(0.145,-0.422,-0.514rad)`, 남은 거리
286+
0.2648m, recovery 0회였고 최종 odom 속도는 0이었다. 목표 종료 뒤 e-stop이 해제되고
287+
motion armed가 유지된 것을 자동 점검에서 발견해 로컬 `/safety_watchdog/set_estop`을 즉시
288+
활성화했다. 응답은 `Emergency stop activated`였고 이후 e-stop active, motion unarmed,
289+
active command 없음과 0속도를 확인했다. 실제 방향 확인 없이 이 실패를 단순 tolerance나 감시
290+
완화로 숨기지 않고, 다음 단계에서 최종 yaw UX와 controller 연속성·정지 원인을 로그로 분리한다.
291+
243292
## 배운 점
244293

245294
1. OccupancyGrid cell 수와 화면 canvas pixel 수는 같은 개념이 아니다.
@@ -258,3 +307,5 @@ LiDAR endpoint가 지도 벽과 다시 겹쳤으며 active command 없음과 e-s
258307
오류 탐지를 반복 검증하는 안전한 수용 방법이다.
259308
9. 지도 화살표를 그리는 수학이 맞아도 앞쪽 표식이 모호하면 작업자가 yaw를 반대로 입력할 수
260309
있으므로, 좌표 회귀 테스트와 명확한 방향 기호가 모두 필요하다.
310+
10. scan-map 정합 점수는 센서 외부각의 물리적 의미를 보장하지 않는다. frame 계약과 실제 차체
311+
전방을 별도로 시험해야 matcher가 180° pose로 센서 축 오류를 숨기는 일을 막을 수 있다.

docs/setup/tb1-navigation.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ ros2 launch navigation_agent tb1_mapping.launch.py
108108

109109
launch는 원본 `/scan`을 360개 각도 bin의 `/scan_normalized`로 재투영하고 SLAM Toolbox에는
110110
정규화 토픽만 연결한다. 실제 LDS-02에서 관찰한 207~219개 가변 배열이 다시 SLAM 입력을
111-
깨뜨리지 않는지 먼저 확인한다.
111+
깨뜨리지 않는지 먼저 확인한다. TB1의 원본 LDS-02 angle 0은 실제 `base_link +X` 전방과
112+
반대이므로 정규화 설정의 `angle_offset_rad=π`가 각 샘플을 물리 차체 기준으로 회전시킨다.
113+
Gateway도 원본 `/scan` overlay에 같은 π를 적용한다. 한쪽만 바꾸면 지도 정합은 높아도 웹
114+
화살표와 실제 이동이 다시 반대가 될 수 있으므로 두 설정은 하나의 센서 외부각 계약이다.
112115

113116
```bash
114117
ros2 topic hz /scan_normalized
@@ -166,7 +169,8 @@ ros2 topic echo /fleet/navigation_status
166169

167170
1. 지도에서 `초기 위치` 모드를 선택한다.
168171
2. 로봇의 대략 위치를 클릭하고 진행 방향으로 드래그한다. 붉은 LiDAR endpoint는 선택 후보를
169-
따라 움직이므로 지도 벽과의 오차를 바로 볼 수 있다.
172+
따라 움직이므로 지도 벽과의 오차를 바로 볼 수 있다. 원은 로봇 중심이고 드래그 끝의
173+
삼각형은 실제 차체의 앞, 즉 `base_link +X`여야 한다.
170174
3. `LiDAR 자동 정렬`을 누르고 5~6초 기다린다. `정렬 일치``지도 내부` 지표가 녹색으로
171175
표시되는지, 붉은 점이 지도 벽에 실제로 겹치는지 확인한다.
172176
4. `초기 위치 적용`을 누른다. 자동 정렬을 거치지 않았거나 match 35%·지도 내부 70% 기준을
@@ -179,6 +183,10 @@ ros2 topic echo /fleet/navigation_status
179183
0.05m를 더한 값이다. 현재 pose도 known free cell이어야 한다.
180184
8. `목적지 전송` 후 거리, 경과시간, 예상시간, recovery와 lease age를 관찰한다.
181185

186+
스캔 외부각 설정을 변경하거나 navigation agent를 재시작한 뒤에는 이전 AMCL pose를 재사용하지
187+
않는다. e-stop을 유지하고 `LiDAR 자동 정렬`과 초기 위치 적용을 다시 수행한 다음, 삼각형 방향과
188+
실제 차체 앞이 같은지 작업자가 확인해야 한다. 그 확인 전에는 목적지를 보내지 않는다.
189+
182190
로봇 상태 카드의 위치와 방향은 localization 이후 `navigation.current``map` 좌표를 표시한다.
183191
아직 map pose가 없을 때만 `odom` 좌표로 fallback하며 카드 라벨에 사용 frame을 함께 표시한다.
184192

0 commit comments

Comments
 (0)