Repository navigation
JETSON_SENSOR_ACCESS
Jetson Orin에 붙은 LiDAR·카메라를 원격에서 보거나 데이터를 가져올 때 필요한 것. 접속 경로 자체는 JETSON_SSH_ACCESS를 먼저 본다. 이 문서는 접속이 된다는 전제에서 시작한다.
이 문서의 거의 모든 내용이 이 숫자에서 나온다.
10 MB 전송에 33.2초 → 약 0.30 MB/s (2.4 Mbps)
Jetson은 ZER01NE_WIFI(2.4 GHz)에 붙어 있고, 외부에서는 Tailscale로만 닿는다. 신호 자체는 멀쩡하다:
freq 2457 (2.4GHz) signal −36 dBm tx bitrate 52 Mbit/s
신호 −36 dBm은 아주 좋은 값인데 실효 처리량은 PHY의 5%다. 전파 문제가 아니라 2.4 GHz 혼잡 또는 사무실 업링크 병목이다. 5 GHz 대역이나 유선으로 옮기지 않는 한 이 제약은 그대로다.
센서가 만들어내는 양과 비교하면:
| 토픽 | 대역폭 | 링크 대비 |
|---|---|---|
/camera/color/image_raw |
13.84 MB/s | 46배 초과 |
/livox/lidar |
5.20 MB/s | 17배 초과 |
/camera/color/image_raw/compressed |
1.49 MB/s | 5배 초과 |
즉 어떤 센서 토픽도 원격에서 온전한 레이트로는 못 받는다. 실시간으로 보려면 줄여야 하고, 전체 데이터가 필요하면 녹화 후 옮겨야 한다.
Jetson에서 상시 도는 서비스가 발행한다 (livox-driver, realsense, foxglove-bridge, mosquitto).
| 토픽 | 타입 | 레이트 | 비고 |
|---|---|---|---|
/livox/lidar |
PointCloud2 |
10 Hz | frame livox_frame, 19968 pts/msg |
/livox/imu |
Imu |
— | |
/camera/color/image_raw |
Image |
14.9 Hz |
848×480 rgb8, fx 614.31 |
/camera/color/image_raw/compressed |
CompressedImage |
14.9 Hz | 848×480 JPEG q95 |
/camera/color/camera_info |
CameraInfo |
14.9 Hz | plumb_bob |
/camera/aligned_depth_to_color/image_raw |
Image |
13.8 Hz |
848×480 16UC1 — 깊이는 이걸 써라
|
/camera/aligned_depth_to_color/camera_info |
CameraInfo |
13.8 Hz | color 와 동일 intrinsics |
/camera/depth/image_rect_raw |
Image |
14.1 Hz | 640×480 16UC1, fx 383.14 — 정렬 안 됨, 쓰지 마라 |
/tf_static |
map·livox_frame·camera_* 전부 한 트리 (§1.1) |
/livox/lidar의 point field는 x, y, z, intensity, tag, line, timestamp.
이전 판 경고. 2026-09-05 오전에 배포한 dvlc 단독 값은 병진이 198 mm 틀렸다. 같은 날 오후 검증에서 드러나 수정했다. 그 이전 값으로 LiDAR–이미지 융합을 돌린 결과가 있으면 다시 봐야 한다 (오차: 1 m에서 90 px, 2 m에서 45 px).
정본 수치와 근거는 Jetson의 ~/spot_extrinsic.yaml. 백업은 ~/spot_extrinsic.yaml.bak-20260905.
body = x 전방, y 좌, z 상. 원점은 엉덩이 중심이고 기립 시 지면 위 0.5087 m다 (body_T_gpe 실측).
body -> livox_frame
xyz 0.304668878 -0.000000116 0.230026415
quat 0.0 0.104528463 0.0 0.994521895 # pitch down 12.0°
body -> camera_link # == camera_depth_optical_frame
xyz 0.379725244 0.017510534 0.096776966
quat 0.009310769 0.026752321 0.002583650 0.999595392
body -> camera_color_optical_frame
xyz 0.379381950 0.032434884 0.097408095
quat -0.508819740 0.516735369 -0.475408153 0.498070395
# 광학규약 제거 시: roll +1.776 pitch +2.971 yaw +0.891 deg
CAD는 카메라 회전을 전부 0으로 놓았는데 틀렸다. roll·pitch가 실제로 +1.78 / +2.97°다. 병진은 CAD가 맞다.
| 항목 | 상태 | 근거 |
|---|---|---|
body_T_livox roll·pitch·tz |
✅ CAD 맞음 (0.25~0.36°, 25 mm) | 지면평면 3회 |
body_T_livox 병진 |
✅ CAD 맞음 | 줄자 |
body_T_livox yaw |
✅ CAD 0° 맞음 | LiDAR→이미지 투영 육안검사 |
body_T_camera roll·pitch |
✅ CAD 아님. +1.78 / +2.97° | 지면평면 + dvlc 교차검증 |
body_T_camera 병진 |
✅ CAD 맞음 | 줄자 + 피듀셜 잔차 (x 18 mm, z 8 mm) |
body_T_camera yaw |
✅ +0.891 ± 0.3° | 피듀셜 3배치 |
spot-extrinsic.service가 두 개를 발행한다. enabled 상태라 재부팅해도 유지된다.
body -> livox_frame
xyz 0.304668878 -0.000000116 0.230026415
quat 0.0 0.104528463 0.0 0.994521895
livox_frame -> camera_link
xyz 0.101120323 0.017510650 -0.114732533
quat 0.008989699 -0.077880401 0.003542737 0.996915883
body -- livox_frame -- camera_link --+-- camera_color_frame -- camera_color_optical_frame
+-- camera_depth_frame -- camera_depth_optical_frame
map -> livox_frame 자리표시자는 2026-09-05 제거했다 (~/svc_livox.sh). 전부 0이라 의미가 없었고, body를 넣으려면 livox_frame의 부모가 둘이 되면 안 됐다. map을 참조하는 노드·코드는 없다(전수 확인).
body는 지금 정적 루트일 뿐이다. 로봇이 움직일 때의 body 자세(odom -> body)를 발행하는 브리지가 아직 없다. 그래서 걷는 로봇의 bag에서는 body가 고정으로 보인다. 브리지가 생기면odom -- body -- livox_frame -- ...로 자연스럽게 확장된다.
확인:
ros2 run tf2_ros tf2_echo body camera_color_optical_frame -p 6
# xyz 0.379382 0.032435 0.097408
ros2 run tf2_ros tf2_echo livox_frame camera_color_optical_frame -p 6
# xyz 0.100653 0.032435 -0.114187 / rpy -80.969 1.568 -88.849 deg
⚠️ livox_frame -> camera_color_optical_frame을 직접 발행하지 않는다. RealSense 드라이버가 이미camera_color_frame -> camera_color_optical_frame을 발행하고 있어서, 저것을 또 쏘면 optical frame의 부모가 둘이 되고 TF 트리가 갈라진다.
배포 TF를 병진·회전 둘 다 교체했다. 이제 dvlc 값을 직접 쓰지 않고, body 기준 실측 두 개를 합성해 유도한다.
병진 old 0.205696150 -0.137685857 -0.032145274
new 0.101120323 0.017510650 -0.114732533 (|Δ| = 198 mm)
회전 old qxyzw 0.005522886 -0.077601178 0.029733257 0.996525709
new qxyzw 0.008989699 -0.077880401 0.003542737 0.996915883 (3.028°, 전부 yaw)
dvlc 판정 — tilt만 쓸 만하다:
| 성분 | 판정 | 어떻게 알았나 |
|---|---|---|
| roll·pitch (tilt) | ✅ 양호, 0.68° | 지면평면 교차검증 |
| yaw | ❌ 3.028° 오류 | LiDAR→이미지 투영 육안검사 |
| 병진 | ❌ 198 mm 오류 | 줄자 + CAD |
깊이 다양성이 부족하면 병진과 회전이 서로 상쇄되며 수렴하는 전형적 실패다. 재투영 잔차만 보면 이게 안 드러난다.
이전 문서에 적혀 있던 ±10 cm / ±2°는 tilt에만 해당했다. yaw와 병진은 그 범위를 넘었다.
고치기 전 LiDAR→이미지 투영 오차: 병진분이 1 m에서 90 px / 2 m 45 px / 5 m 18 px, 거기에 yaw 오류가 거리 무관 상수 32 px를 더했다.
지면평면 — roll·pitch·tz 3 DOF
평평한 바닥(반경 3 m 이상)에 세우고 12초 녹화. Spot이 주는 body_T_gpe(발바닥 접촉 기반 지면추정)의 법선·높이와 각 센서에서 피팅한 바닥 평면을 비교한다.
- yaw·tx·ty는 원리상 안 나온다 — 평면은 자기 위로 미끄러지는 것과 법선축 회전에 불변이다.
- 카메라는 정면 5 m 이상 열린 바닥이 필요하다. 벽을 보면 벽 밑동이 바닥으로 오분류돼 3~6°씩 틀린 답이 나온다 (2026-09-05에 실제로 겪었다).
- 기립 흔들림은 무시 가능: 12초 p-p roll 0.011°, pitch 0.013°, z 0.0 mm.
피듀셜 — yaw
146 mm AprilTag 36h11을 세워서 전방 1.5~2.5 m, 지상 0.9 m 부근. Spot이 body_T_tag를, RealSense가 같은 태그를 본다.
- 깊이를 쓰지 않는다. 태그 중앙이 검은색이라 D435 IR 깊이가 편향된다. 카메라 광선 방향과 Spot이 준 태그 방향만 맞춰 yaw 1 DOF를 푼다.
- 태그 중심은 코너 평균이 아니라 대각선 교점을 쓴다 (사영 불변).
- 노이즈: 카메라 방위 std 0.016° / Spot 방위 std 0.244°. Spot이 지배적이니 배치마다 20~25회 폴링해 평균낸다.
- 배치의 방위를 넓게 흩어야 yaw와 ty가 분리된다.
- 바닥에 누운 태그는 못 쓴다. 가까이 가면 앙각이 FOV를 벗어나고, 멀어지면 납작해져 디코딩이 안 된다. 성립 구간이 없다.
투영 육안검사 — LiDAR yaw
두 가설(dvlc 회전 / 피듀셜 유래 회전)로 LiDAR 점구름을 컬러 이미지에 각각 투영해 비교한다. 회전 오차는 거리에 무관한 상수 화각 오차라 3° = 32 px로 항상 보인다. 박스·케이스의 수직 에지에서 어느 쪽이 물체 경계와 맞는지 보면 된다.
- 병진을 먼저 고쳐야 유효하다. 아니면 병진 오차에 묻혀 판별이 안 된다.
- 스크립트
scratchpad/proj_test.py, 결과 이미지~/Desktop/spot-calib/proj_check/
Jetson에 foxglove-bridge가 0.0.0.0:8765로 상시 떠 있다. 다만 브릿지는 평문 ws://라 https://app.foxglove.dev에서 직접 연결하면 브라우저가 mixed content로 차단한다.
SSH 터널로 우회한다. 브라우저는 localhost를 secure로 취급하므로 차단되지 않는다.
# 터미널 1 — 터널 (작업 끝나면 Ctrl-C)
ssh -N -L 8765:127.0.0.1:8765 jetson-orin-spot브라우저에서:
https://app.foxglove.dev/?ds=foxglove-websocket&ds.url=ws://localhost:8765
| 토픽 | 결과 |
|---|---|
/camera/color/image_raw |
❌ 사실상 정지. 프레임이 0.92 MB라 3초에 한 장, 브릿지 송신 큐에 수 MB 적체 → 지연 폭증 |
/camera/color/image_raw/compressed |
✅ 보인다. 15 Hz는 못 채우고 ~3 fps로 떨어지지만 화면은 나온다. 단 링크를 포화시킨다 |
/livox/lidar |
✅ 보인다. 마찬가지로 대량 드랍 중이지만 포인트클라우드는 일부만 와도 그림이 그려져 티가 덜 난다 |
3D 패널 fixed frame은 map으로 둔다 (static TF가 그것뿐).
Foxglove가 켜져 있으면 업링크를 통째로 먹는다. 실측 사례 — bag 전송 중에 Foxglove가 raw 이미지를 구독하고 있었더니:
Foxglove 연결 중: rsync 0.073 MB/s
Foxglove 끊은 후: rsync 0.30 MB/s (4배)
데이터 반출·rsync·bag 전송 중에는 Foxglove를 끄는 게 맞다. 850 MB 받을 때 10분 vs 40분 차이가 난다.
토픽을 축소해서 새로 발행하는 작은 노드를 띄우는 방법이 있다. Jetson 안에서 resize + JPEG 하므로 링크에는 작은 것만 나간다.
/camera/color/image_raw (로컬이라 공짜)
→ cv2 resize 424×240, imencode q50, 5 Hz로 간축
→ 12 KB/frame = 0.06 MB/s (compressed 대비 25배 작음)
기존 서비스를 건드리지 않고 토픽만 추가하는 방식이라 안전하다. 상시 필요하지는 않아서 지금은 안 띄워놨다. 필요하면 만들어 쓰면 된다.
실시간이 안 되면 녹화해서 옮긴다. 이건 시간만 쓰면 되므로 확실하다.
전송은 rsync -az를 쓴다. rgb8 raw 이미지는 압축이 잘 먹어서 체감 차이가 크다 — 188 MB bag이 약 2분에 들어왔다 (비압축 추정 12분).
rsync -az --info=progress2 \
jetson-orin-spot:/home/zer01ne-spot/calib_bags/<이름> ~/원하는경로/direct_visual_lidar_calibration (koide3, MIT) 을 쓰기 위한 수집 절차다.
한 번 돌려서 결과를 이미 배포했다 (§1.1). 여기부터는 값을 다시 뽑아야 할 때만 필요하다 — 센서를 다시 마운트했거나, 더 정확한 값이 필요할 때. 그냥 쓰려는 것뿐이면
/tf_static에 이미 나오고 있으니 이 절은 건너뛴다.
⚠️ 이 절차의 병진 출력은 믿지 마라. 2026-09-05 검증에서 dvlc 병진이 198 mm 틀린 것으로 드러났다(§1.1). 회전은 0.68° 안에서 정확했다. 깊이 다양성이 부족하면 병진과 회전이 서로 상쇄되며 수렴하는데, 재투영 오차만 보면 이게 안 보인다.다시 돌린다면 반드시:
- 병진은 CAD·줄자로 독립 교차검증한다 (두 센서 하우징 중심 사이 거리를 직접 잰다)
- 수집 자세의 깊이 다양성을 늘린다 — 근·중·원거리를 고루 섞는다
- 회전은 지면평면 교차검증으로 확인한다 (§1.1 "어떻게 쟀나")
레퍼런스 데이터셋은 이미지를 2 Hz로 기록한다 (15.65초 bag에 points 157개, image 30개). 15 Hz로 뜨면 용량만 7배가 되고 이득이 없다. topic_tools가 Jetson에 설치되어 있지 않아 스로틀 노드를 따로 띄운다.
Jetson에 이미 올려둔 것:
~/calib_throttle.py /camera/color/image_raw 15Hz → /calib/image 2Hz
+ /calib/camera_info 를 이미지와 같은 스탬프로 동기 발행
~/record_calib.sh 스로틀 켜고 → 녹화 → 끄고 → 결과 출력
ssh jetson-orin-spot '~/record_calib.sh <지점명> 30'
rsync -az jetson-orin-spot:/home/zer01ne-spot/calib_bags/<지점명> ~/원하는경로/한 지점 30초 기준 약 190 MB, 전송 약 2분이다.
기록되는 토픽은 셋뿐이다. preprocess -a(자동 토픽 선택)가 bag마다 image/camera_info/points 각 1개를 요구하기 때문이다.
/calib/image sensor_msgs/Image ~2 Hz
/calib/camera_info sensor_msgs/CameraInfo ~2 Hz
/livox/lidar PointCloud2 10 Hz
-
Spot 정지 상태로 5~8지점. MID-360은 non-repetitive scan이라 정지 누적이 정답이다 (
preprocess -d불필요) - 지점마다 yaw 30~40°씩 변화를 준다. 회전 관측성이 없으면 해가 국소 최적에 빠진다
- 유리·모니터·광택 바닥을 피한다. 텍스처 있는 벽과 직교 엣지(문틀, 기둥, 박스)가 있는 곳
- 최소 1지점만으로도 계산은 되지만(single-shot 지원), 정확도를 위해 여러 지점을 권장한다
direct_visual_lidar_calibration의 preprocess는 IMU를 읽지 않는다. /camera/imu는 롤링셔터 분석용으로 목적이 다르니 별도 bag에 담는다.
자동 초기값 추정은 SuperGlue를 쓰는데 상용 이용이 금지되어 있다. initial_guess_manual을 써야 하고, 이건 GUI(Iridescence)가 필요하므로 headless인 Jetson이 아니라 작업자 PC에서 돌린다. 대응점을 3점 이상 클릭하는 방식이다.
로컬 설치는 GTSAM·Ceres·Iridescence 빌드가 필요해서 오래 걸린다. 공식 도커 이미지(koide3/direct_visual_lidar_calibration)를 쓰는 편이 빠르다.
증상 — /camera/color/image_raw/compressed가 빈 메시지(data: [])만 발행하고, 구독자가 붙을 때마다 realsense 로그에 에러가 쏟아졌다.
[CompressedPublisher]: OpenCV(4.8.0) alloc.cpp:73:
(-4:Insufficient memory) Failed to allocate 323193268964880 bytes
원인 — 한 프로세스에 OpenCV 두 버전이 동시에 로드되고 있었다.
librealsense2_camera.so (ros2_ws 로컬 빌드) → libopencv_core.so.408 (JetPack 4.8.0)
cv_bridge, compressed_image_transport (apt) → libopencv_core.so.4.5d (Ubuntu 4.5.4)
realsense가 먼저 로드되므로 cv::imencode 심볼이 4.8.0으로 해석되는데, 플러그인은 4.5.4 헤더 기준으로 컴파일되어 있다. cv::Mat 레이아웃이 달라 쓰레기 크기를 읽고 할당에 실패한다.
조치 — cv_bridge와 compressed_image_transport를 설치된 apt 버전과 동일한 태그로 소스를 받아 OpenCV 4.8.0으로 재빌드하고 오버레이에 올렸다. /opt/ros 원본은 건드리지 않았다.
cd ~/ros2_ws/src
git clone --branch humble https://github.com/ros-perception/vision_opencv.git
git clone --branch humble https://github.com/ros-perception/image_transport_plugins.git
cd vision_opencv && git checkout 3.2.1 # apt cv_bridge 3.2.1 과 일치
cd ../image_transport_plugins && git checkout 2.5.5 # apt 2.5.5 와 일치
cd ~/ros2_ws
colcon build --packages-select cv_bridge compressed_image_transport \
--cmake-args -DCMAKE_BUILD_TYPE=Release -DOpenCV_DIR=/usr/lib/cmake/opencv4
sudo systemctl restart realsensesvc_realsense.sh가 이미 ~/ros2_ws/install/setup.bash를 source하므로 재시작만으로 적용된다.
원복이 필요하면 ~/ros2_ws/install/{cv_bridge,compressed_image_transport}를 지우고 realsense를 재시작한다.
ros2_ws에서 다른 패키지를 새로 빌드할 때 같은 함정에 빠질 수 있다. apt로 깔린 ROS 바이너리는 Ubuntu OpenCV 4.5.4 기준이고, 이 장비의 dev 헤더는 JetPack 4.8.0이다. OpenCV를 쓰는 노드를 로컬 빌드하면 둘이 섞인다.
camera.color.image_raw.jpeg_quality를 바꿔도 출력 크기가 그대로다.
param 95 → 99465 B
param 40 → 97989 B
param 30 → 100386 B (구독을 끊었다 다시 붙여도 동일)
같은 프레임을 Jetson에서 cv2로 직접 인코딩하면 q95 95.6 KB / q50 22.6 KB / q10 9.6 KB가 나온다. 즉 플러그인이 항상 q95로 인코딩하고 파라미터는 선언만 되어 있다. compressed_image_transport 2.5.5 자체 문제로 보인다.
원격 대역폭을 줄이려면 이 파라미터에 기대지 말고 §2의 프리뷰 노드 방식을 쓴다.
LiDAR와 카메라는 Jetson에 직결되어 있어서 SPOT 로봇 본체가 꺼져 있어도 정상 동작한다. 실제로 로봇이 18시간 넘게 응답 없는 상태에서도 두 센서는 정상 발행 중이었다.
로봇 본체 연결이 끊기면 이렇게 보인다:
spot-state-ro 로그: ProxyConnectionError: The proxy on the robot could not be reached.
ip neigh dev eno1: 192.168.77.72 INCOMPLETE ← ARP 응답 없음
INCOMPLETE는 로봇이 L2에서 아예 안 보인다는 뜻이다. 소프트웨어가 아니라 전원 또는 이더넷 연결을 확인해야 한다. 서브넷은 192.168.77.x이고 Jetson이 eno1에서 DHCP 서버(spot-dhcp) 역할을 한다.
- Google ADK 검토 (프레임워크·모델 비교)
- 페르소나 + 상황 카드 모델 (2차 시나리오 구동 방식)
- Mac Studio 로컬 LLM + Claude Opus 하이브리드 라우팅 전략
- 개발 일정·할 일 점검표 (단계별 진행 점검)
- 로봇 Adapter 개발 온보딩 (HA·MQTT 계정 등록, 토픽 적용, 테스트 송수신, 상태 확인)
- LG ThinQ 가전 연동 (HA 통합 · REST 데이터 취득 · 제어)
- 장소 도착 자동화 (태스크/이벤트/장소 규칙 → SPOT·로봇팔·반려로봇·Hub 자동 명령)
- 엔딩 화면 데이터 출처 점검 (차량 3필드 · 세션 종료 엔딩 5화면)