집에 고등어무늬 고양이 두 마리가 있다. aki와 kuro다. 이전 글에서 거실의 Tapo C210을 Frigate에 연동해서 녹화 다시보기, 고양이 다이어리, 일일 하이라이트 영상을 만들었다. 그런데 C210은 벽에 걸려서 거실 전경을 찍는 터라, 급식 구역이 딱 사각지대에 있었다.
카메라와 자동 급식 기능이 동시에 있는 장치를 찾아서 이 시야를 보완하고 싶었다. 간단해 보였다: 반려동물 급식기 카메라 한 대, 영상 스트림을 Frigate에 연결할 수 있으면 되니까.
결과적으로 10개 브랜드를 조사하고 나서야 한 가지 사실을 깨달았다: 이 제품군에서 영상 스트리밍을 개방하는 제품은 단 하나도 없다. 일부가 를 지원하지 않는 게 아니라, 제품군 전체가 구조적으로 지원하지 않는다.
모두 클라우드에 묶여 있다, 우연이 아니다#
가장 먼저 본 건 미지아 스마트 반려동물 급식기 2 비전 에디션이다. 2026년 3월 크라우드펀딩 출시, 500만 화소, 150° 초광각, 940nm 적외선 야간 투시, 5L 사료통에 무게 측정 기능까지. 게다가 소스 코드에서 xiaomi 소스의 장치 필터 조건에 .feeder.가 명시적으로 포함되어 있었다——완벽해 보였다.
하지만 go2rtc의 xiaomi 모듈 소스를 다 읽고 나서 문제를 발견했다: 영상 전송 경로가 두 가지뿐인데, 샤오미 자체 클라우드 릴레이(CS2) 아니면 ThroughTek Kalay(TUTK) P2P, 둘 다 클라우드를 거쳐야 한다. "키 교환만 인터넷, 영상은 로컬 네트워크"가 아니라, 영상 자체가 클라우드를 통과한다. 외부 인터넷을 차단하면 스트림이 바로 끊긴다.
비교 대상으로 이미 쓰고 있는 C210을 보면: Tapo의 RTSP는 순수 로컬 네트워크다. 라우터에서 외부 인터넷을 끊어도 녹화와 감지가 전부 정상 동작한다. 샤오미 카메라는 구조적으로 전혀 다른 물건이다.
IMPORTANT
나중에 go2rtc 커뮤니티에서 중요한 관찰을 발견했다: go2rtc의 xiaomi 소스 지원 목록에 카메라 보고가 50대 넘게 있는데, 급식기는 0대다. 개발자 본인도 이렇게 말한 적 있다: 「If a camera isn't on the list, don't ask if it works. No one knows until you try it.」
대만 공식 채널에서는 카메라 없는 일반 모델만 판매한다. 비전 에디션은 중국 내수(인민폐 449 ≈ NT$2,000)로, 타오바오나 구매대행이 필요하고 보증도 없다. 여기에 샤오미의 보안 이력——2020년에 누군가 Google Nest Hub에서 남의 집 샤오미 카메라 영상을 본 사건, 2021년 ThroughTek Kalay의 CVSS 9.6 취약점 공개(약 8,300만 대 영향)——을 더하면, 사지 않기로 했다.
PETKIT + LocalKit: 클라우드 제거의 대가#
다음으로 찾은 건 YumShare Dual Gen2(P592). 이 제품 자체는 로컬 스트리밍을 지원하지 않지만, GitHub에 LocalKit이라는 서드파티 프로젝트가 있어서 PETKIT 장치를 완전히 로컬화할 수 있다고 했다. 카메라 스트리밍 포함.
배포 문서를 읽고 포기했다. 필요한 것들:
- DNS 리라이트——PETKIT 클라우드 도메인을 로컬 서버로 돌려서, 장치가 정품 서버에 연결됐다고 착각하게 만들기
- 인증서 신뢰 수정——장치에 cert pinning이 있어서, 인증서 신뢰 체인을 수정해야 함
- 로컬 MQTT broker——PETKIT 클라우드 broker를 모방하는 컨테이너 실행
- 장치에서 go2rtc 실행——카메라가 있는 각 장치마다 go2rtc를 돌려야 함
- 펌웨어 버전 895 고정——설치 스크립트가 특정 펌웨어 버전만 인식하며, PETKIT 자동 업데이트를 막아야 함
게다가 1인 유지보수 프로젝트다. Tapo의 RTSP는 제조사가 공식 지원하는 기능이고, PETKIT의 로컬 스트리밍은 제조사의 허점 위에 서 있는 임시 발판이다——OTA 한 번이면 전부 날아갈 수 있다.
P592 대만 가격은 약 NT$5,000. 펌웨어를 고정하고 DNS 리라이트를 해야 하는 급식기에 이 돈을 쓰는 건, 방향이 틀린 느낌이었다.
구조적 이유: 왜 어떤 제조사도 개방하지 않는가#
여러 제품을 조사한 후, 이 제품군 전체가 왜 영상 스트리밍을 개방하지 않는지 이해하기 시작했다.
카메라 브랜드(Tapo, Reolink, EZVIZ, Imou)는 하드웨어로 수익을 내고, RTSP 개방은 판매 포인트가 된다. 급식기 브랜드는 앱 종속성과 클라우드 구독으로 수익을 낸다——클라우드 저장소, AI 반려동물 인식, 원격 시청, 전부 자사 앱에 묶여 있다. 스트리밍을 개방하면 사용자를 서드파티 NVR로 밀어내는 셈이라, 구독을 아무도 사지 않게 된다.
NOTE
RTSP 전통이 있는 카메라 제조사조차, 급식기 제품군으로 넘어오면 닫아버렸다. Imou(다화 소비자 라인)의 PF1 급식기 카메라는 세 군데 소스에서 RTSP 증거를 찾지 못했다: iSpy의 Imou 장치 목록에 PF1 없음, HA의 imou_life 통합에 관련 issue 0건, Frigate 커뮤니티 보고 0건. 게다가 대만에 유통 경로도 없다.
사기 전에 알 수 있는 방법: APK를 풀어 영상 SDK 확인하기#
매장 페이지에 "로컬 RTSP 지원"이라고 써놓지 않는 이상, 구매 전에 급식기의 영상을 뽑아낼 수 있는지 어떻게 판단할까?
방법은 의외로 간단하다: 앱 설치 파일을 다운로드해서, 압축을 풀고 어떤 영상 SDK를 쓰는지 확인한다.
는 본질적으로 ZIP 파일이고, lib/ 폴더의 네이티브 라이브러리(.so 파일)가 어떤 영상 전송 SDK를 사용하는지 드러낸다. 디컴파일할 필요 없고, 설치할 필요도 없다. 파일 이름만 보면 된다:
| 이런 라이브러리가 보이면 | 사용 SDK | Frigate에 연결 가능? |
|---|---|---|
libtuya_*.so | Tuya | go2rtc에 tuya:// 있지만 시그널링이 클라우드 |
libmiot*.so, libmiss*.so | 미지아 | go2rtc에 xiaomi:// 있지만 영상이 클라우드 강제 |
libIOTCAPIs.so, libAVAPIs.so | ThroughTek Kalay(TUTK) | go2rtc에 tutk 없음, 직접 리버싱 필요 |
libagora-*.so | Agora | 상용 클라우드 RTC, 로컬 경로 전무 |
"TUTK 라이브러리가 보인다"는 것만으로는 부족하다. 핵심은 앱이 SDK의 로컬 네트워크 직접 연결 기능을 호출하는지 여부다——일부 앱은 TUTK SDK를 포함하면서도 클라우드 릴레이만 사용하고 로컬 네트워크 시도 자체를 하지 않는다. 이건 한 단계 더 들어가 dex 바이트코드의 호출을 봐야 하지만, "가능성 제로" vs "가능성 있음"을 판단하기에는 충분하다.
비용 제로의 구매 전 스크리닝#
이 방법으로 대만에서 구매 가능한(또는 구매대행 가능한) 10개 브랜드를 스캔했다:
展開
| 브랜드 | 앱 패키지명 | 영상 SDK | 앱에서 로컬 직접 연결 호출 | 판단 |
|---|---|---|---|---|
| FurSpark | com.ddpai.cpp_overseas | TUTK Kalay | 있음(12곳) | ✅ 가능성 있음 |
| Dogness | — | TUTK Kalay | 있음(17곳) | ✅ 가능성 있음 |
| CATLINK | — | TUTK Kalay | 있음(15곳) | 대만에서 카메라 없는 모델만 판매 |
| 샤오미 미지아 | — | TUTK + CS2 | 있음 | 영상 클라우드 강제 |
| PETLIBRO | — | TUTK | 없음 | 클라우드 릴레이만 사용 |
| Petwant | — | TUTK | 없음 | 클라우드 릴레이만 사용 |
| PETKIT | — | Agora RTC | — | 앱 측 로컬 경로 없음 |
| Petoneer | — | Agora RTC | — | 위와 동일 |
| Hama Home(독일) | — | Tuya | — | 앱 내부 100% Tuya |
| maoxga | — | Agora RTC | — | 카메라 연결 불가 확정 |
의외의 발견이 몇 가지 있었다:
- PETKIT은 TUTK가 아니라 Agora를 사용한다. 처음에는 PETLIBRO처럼 Kalay일 거라고 생각했는데, 전혀 아니었다——LocalKit이 반드시 기기에 접근해야 하는 이유도 이걸로 설명된다: 앱 측에 로컬 경로 자체가 없다.
- 독일 Hama의 자체 앱은 내부가 100% Tuya다. 겉으로는 알 수 없지만, 풀어보면 한눈에 알 수 있다. "Tuya인지 알 수 없다"는 건 맞지만, "알아낼 수 없다"는 건 아니다.
- maoxga 앱에서 예상치 못한 Matter SDK를 발견했다. 급식 쪽은 Matter를 지원할 수도 있다는 뜻이다. 하지만 카메라 부분은 Agora이므로 로컬 경로가 없는 건 확정.
WARNING
앱에서 로컬 직접 연결 함수를 호출한다고 해서, 펌웨어 쪽도 지원한다는 뜻은 아니다. 앱의 호출은 "개발자가 이 코드를 작성했다"는 것만 증명할 뿐, 장치 측에 대응하는 처리가 있는지는 기기를 직접 가져봐야 안다. 하지만 반대로, 앱에 로컬 직접 연결 호출이 전혀 없다면, 바로 제외할 수 있다.
최종적으로 FurSpark를 선택했다#
스크리닝 후 가능성이 남은 건 두 대: Dogness F10과 FurSpark Big Eye.
| FurSpark Big Eye | Dogness F10 | PETKIT P592 | |
|---|---|---|---|
| 해상도 | 2K | 1080P | 1080P |
| 화각 | 광각(공식 수치 비공개) | 165° | 140° |
| 사료통 | 3.7L | 3.6L | 듀얼 사료통 |
| 저장 | 내장 64GB, 48h 재생 | — | — |
| 특수 기능 | 기계식 눈꺼풀 물리 렌즈 가림 | 건전지 백업 | 고양이 얼굴 인식 |
| 영상 SDK | TUTK(로컬 직접 연결 있음) | TUTK(로컬 직접 연결 있음) | Agora(로컬 경로 없음) |
| Frigate 연동 | 리버싱 필요 | 리버싱 필요 | LocalKit 개조 필요 |
| 대만 최저가 | NT$3,990 | NT$4,200(momo 프로모션) | ~NT$5,000 |
FurSpark의 하드웨어가 가장 좋고, 가격이 가장 낮다. 게다가 앱 패키지명 com.ddpai.cpp_overseas가 흥미로운 사실을 드러낸다: **개발사가 선전 DDPai(盯盯拍)**라는 블랙박스 회사다. "미국 Furspark"는 마케팅 포장이다.
오히려 좋은 소식이다——DDPai의 블랙박스는 공개 분해기가 있고, 로컬 HTTP 인터페이스의 전례가 있다. TUTK P2P 외에 두 번째 경로가 있을 수도 있다는 뜻이다.
IMPORTANT
이 제품이 반드시 Frigate에 연결될 거라고 보장할 수 없다. 모든 정보가 "가능성 있음"을 가리키지만, 아무도 해본 적 없다. 결국 구매를 결정한 이유는 간단하다: 연동에 실패해도 정시 급식이 되고 앱으로 볼 수 있는 급식기로서는 문제없다. 펌웨어를 건드리지 않고 공장 기능을 전부 유지하니까, 최악의 경우는 평범한 급식기 하나가 더 생기는 것뿐이다.
영상 연동: 독자적인 TCP 포트#
2026년 9월 13일, 급식기 도착. 설치하고, 고양이 밥 주고, Wi-Fi 연결하고, 영상 찾기 시작.
RTSP는 없지만 daemon은 살아 있다#
nmap으로 장치의 열린 포트를 스캔하니, 554번에 Hisilicon RTSP Streaming Media Server/1.0.0이 돌아가고 있었다. 하지만 열 몇 가지 경로를 시도해도 전부 400이나 500을 반환——미디어 소스가 마운트되지 않았다. RTSP daemon은 살아 있지만, 아무도 콘텐츠를 공급하지 않는 상태.
NOTE
첫 탐색에서 초보적인 실수를 했다: RTSP 요청에 절대 URI(rtsp://host:554/path) 대신 상대 URI를 사용해서 400을 받았다. 올바른 형식으로 다시 보내자, OPTIONS는 200을 반환하고 CSeq도 정확히 응답했다. daemon이 파싱은 하고 있었다. 하지만 DESCRIBE는 일부 경로에서 500(경로는 인식하지만 미디어를 가져오지 못함), 나머지는 400(경로 자체를 인식하지 못함). 어떤 경로도 SDP를 자체적으로 내보내지 않았다.
ONVIF도 없다. 장치가 보고하는 모델명은 DDPaiQ3, SoC는 Rockchip RV1126——hostname이 공장 기본값 RV1126_RV1109 그대로 남아 있었다.
독자적 TCP 6200#
다른 포트로 눈을 돌렸다. TCP 6200에 무언가가 있었다: 연결하자마자 바이너리 데이터를 쏟아내기 시작했다. 각 패킷 앞에 8바이트 헤더가 있고, 16진수로 보면:
0a 0b 0c 00 [4-byte 빅엔디안 길이] → 키프레임(I-frame)
0a 0b 0c 01 [4-byte 빅엔디안 길이] → 일반 프레임(P-frame)
0a 0b 0c는 고정 매직 바이트, 네 번째 바이트가 프레임 유형을 구분하고, 이어지는 4바이트가 이 NAL 유닛의 길이다. 헤더를 벗기고 나머지를 ffmpeg에 넘기면, 디코딩 에러 0건.
ffprobe로 측정한 사양:
| 항목 | 값 |
|---|---|
| 해상도 | 1664 × 2224(세로형) |
| 프레임레이트 | 25 fps |
| 코덱 | H.264 High profile, level 5.0 |
| 비트레이트 | ~2530 kbps(≈ 15 GB/일) |
| 오디오 | 없음(이 경로에는 오디오 트랙 없음) |
| 키프레임 간격 | 4초 |
세로형 영상. 공식 페이지에 "expanding the upper and lower visual range from the previous 16:9 aspect ratio"라는 문구가 있다——이전 세대는 16:9였고, 신모델은 이미지 센서를 90° 회전 장착해서 상하 시야를 확장했다. 블랙박스 부품을 재활용한 것이다.
WARNING
이 스트림의 사양은 바뀔 수 있다. 히스토리 녹화의 ffprobe로 확인했다: 첫날은 576×768 / level 3.1이었는데, 다음 날 1664×2224 / level 5.0으로 뛰었다. 앱의 "영상 품질" 설정이 변경된 것으로 추정된다(현재 "초고화질"로 설정). Frigate의 detect 해상도도 이에 맞춰야 하고, 그렇지 않으면 다운샘플링이나 오버샘플링이 되어 감지 품질에 영향을 준다.
아키텍처: 장치 → go2rtc → Frigate#
go2rtc는 exec:ffmpeg 소스로 장치의 TCP 6200에 연결하고, ffmpeg가 원시 H.264 바이트 스트림을 받아 RTSP로 변환한다. Frigate가 go2rtc에서 RTSP를 가져와 감지와 녹화를 동시에 처리한다. 브라우저는 WebRTC를 통해 go2rtc에서 직접 영상을 받는다.
몇 가지 함정이 있다:
- 연결은 하나만 열 수 있다. TCP 6200에 동시에 두 번째 연결을 열면 camera_fps가 100 이상으로 치솟고 NAL 에러가 발생하며 전체 스트림이 깨진다. 스마트폰 앱이 연결할 때도 같은 경쟁이 발생할 수 있다.
- go2rtc는 전체 인터페이스(
:8554)에 바인딩해야 한다. ffmpeg producer 내부에서 미디어를rtsp://127.0.0.1:${port}로 publish하는데, loopback이 리스닝하지 않으면 self-loop가 실패한다. 방화벽 규칙으로 보호해야 한다——Frigate 컨테이너의 네트워크 대역만 RTSP를 가져갈 수 있게 허용하고 나머지는 거부한다. - 워터마크는 마스킹할 필요 없다. 화면 왼쪽 하단에 "Forspark Big Eye"와 초 단위 타임스탬프가 있지만, Frigate의 모션 감지 해상도로 축소하면 약 1픽셀 면적밖에 안 되어
contour_area임곗값보다 훨씬 작아 자동으로 걸러진다.
화면이 뜨려면 몇 초 걸린다#
장치의 키프레임 간격이 4초로 고정되어 있어서, 새 연결은 다음 키프레임을 기다려야 디코딩을 시작할 수 있다. 평균 대기 2초, 최악 4초. C210은 약 0.1~1.7초. 앱에 키프레임 간격 조정 옵션은 없다.
오디오 쪽의 ffmpeg 입력 포맷 탐색도 화면 표시를 늦춘다(go2rtc가 producer를 순차적으로 시작하므로). -f aac -probesize 32 -analyzeduration 0을 추가하면 ~3.2초에서 0.2초로 줄어든다. 둘을 합치면 화면 표시까지 약 3초.
급식 제어: 클라우드 MQTT를 Home Assistant로 브리지#
영상은 로컬, 급식 제어는 클라우드. 두 경로는 완전히 분리되어 있다.
장치의 실시간 상태와 제어 명령은 제조사 자체 broker를 통해 전달된다. 앱에서 연결 방식과 명령 형식을 리버싱해서, Python daemon(furspark_ha.py)을 작성했다. 한쪽에서는 제조사의 MQTT에 연결해 상태를 읽고, 다른 쪽에서는 MQTT discovery로 로컬 Home Assistant에 푸시한다.
IMPORTANT
급식 제어의 구체적인 명령 형식과 클라우드 엔드포인트는 이 장치의 독자 프로토콜이며, 다른 사용자의 장치 보안과 관련되므로 여기서 공개하지 않는다. 아래에서는 "무엇을 만들었는지"만 설명하고, "어떻게 명령을 보내는지"는 다루지 않는다.
22개 entity, 전부 하나의 장치 아래에 등록#
| 유형 | 항목 |
|---|---|
| sensor | 사료 잔량 등급, 급식 스케줄, 오늘 스케줄 배출 횟수, 다음 급식 시간, 건조제 잔여 일수, 저장 공간 잔여량 및 사용률, 영상 품질, 펌웨어 버전 |
| binary_sensor | 온라인 상태, 고장 |
| switch | 카메라 스위치, 표시등, 차일드 락, 마이크, 급식 알림, 급식 음성 |
| number | 스피커 볼륨, 버튼당 급식 횟수, 급식량 |
| button | 즉시 급식, 1회분 급식 |
| text + button | 스케줄 편집기(형식 07:30x40, 18:00x2), 스케줄 적용, 스케줄 활성화 |
헷갈리기 쉬운 점 몇 가지:
- 사료 잔량은 퍼센트가 아니라 등급이다. 장치가 반환하는 값은 0(충분), 1(부족), 2(소진)이지, 0–100이 아니다. 처음에 퍼센트로 착각해서 "가득 채웠는데 0% 표시"가 됐다.
- 1회분 ≈ 1그램. 실측 결과이며, 설명서에는 "사료 밀도에 따라 변동, 조절 불가"라고 되어 있다. 고양이 두 마리가 아침에 40그램을 먹으니 40회분으로 설정한다. 회분 상한은 실측으로 99까지 가능했다.
- 상주 모드(
--daemon)에서만 제어 항목이 등록된다. 일회성 모드에서는 sensor만 푸시한다——스위치와 버튼은 명령을 모니터링하는 누군가가 있어야 의미가 있으므로.
daemon은 systemd 서비스로 설치해 부팅 시 자동 시작. 10분마다 상태를 폴링한다. HA에서 스위치를 토글하면 즉시 반영——표시등과 카메라 스위치를 실측했는데, 명령 전송부터 장치 상태 변경까지 1초 미만. 급식 버튼은 실측하지 않았다(실제로 사료가 나오므로). 수동으로 누르도록 남겨뒀다.
실시간 오디오와 양방향 통화#
TCP 6200 스트림에는 오디오 트랙이 없다. 하지만 장치의 녹화 파일에는 AAC 오디오가 있어서, 마이크는 정상이다——소리가 6200 경로에 없을 뿐.
TUTK P2P 브리지#
오디오는 ThroughTek Kalay(TUTK)의 P2P 프로토콜을 사용한다. go2rtc의 41가지 소스 프로토콜에 tutk는 없으므로 직접 브리지를 만들어야 한다.
방법은 앱의 APK에서 arm64 버전의 TUTK SDK 라이브러리(libIOTCAPIs.so, libAVAPIs.so 등)를 추출하고, 으로 x86 호스트에서 실행한 뒤, Python의 ctypes로 SDK 함수를 호출해 P2P 세션을 열고 오디오를 수신하는 것이다.
전체 프로젝트에서 가장 어려운 부분이다. TUTK SDK는 Android(bionic libc)용으로 컴파일되어 있어서, Linux(glibc)에서 실행하려면 ABI 비호환 문제가 여러 개 있고 shim으로 가로채야 한다:
展開
| 문제 | 증상 | 해결법 |
|---|---|---|
pthread_mutex_t 크기 차이 | bionic 4바이트, glibc arm64 48바이트. TUTK FIFO가 bionic 크기의 mutex를 내장 → segfault | shim이 pthread_mutex_*를 가로채서, 객체 주소를 key로 실제 glibc mutex를 별도 유지 |
struct addrinfo 필드 순서 차이 | bionic의 ai_canonname과 ai_addr이 뒤바뀜 → 와일드 포인터 | shim이 getaddrinfo를 가로채서 bionic 레이아웃으로 변환 |
| init_array에 null 항목 | bionic linker는 건너뛰지만 glibc는 주소 0을 직접 호출 → segfault | 각 라이브러리를 스캔해서, 전부 null인 init_array의 DT_INIT_ARRAYSZ를 0으로 설정 |
__errno 심볼명 차이 | bionic은 __errno, glibc는 __errno_location | shim으로 포워딩 |
_ctype_가 포인터가 아닌 배열 | bionic의 _ctype_는 const char*인데, 배열을 주면 와일드 포인터로 처리 | shim이 올바른 포인터 버전 제공 |
브리지의 아키텍처는 온디맨드 TCP 서버다. go2rtc가 연결해 오면(누군가 화면을 보고 있으면) 그때 TUTK에 연결하고 오디오 수신을 시작해 AAC-ADTS를 출력한다. 아무도 보지 않을 때는 세션을 해제해서 장치의 P2P 연결 수를 점유하지 않는다.
안정 상태 리소스 소비: CPU ~5%(코어 하나), RAM 67MB, 오디오 트래픽 ~65kbps. 이전에 "qemu 브리지를 24/7 돌리면 리소스를 많이 먹겠다"고 걱정했는데 틀렸다——빌드 시 순간 피크이고, 안정 상태는 극히 가볍다.
겪었던 함정들#
인증이 만료된다. 연결에 필요한 비밀번호는 클라우드 API에서 계산되며, 며칠 안에 만료된다. 만료되면 DTLS 핸드셰이크가 조용히 실패하고, 에러 코드는 "시간 초과"지 "비밀번호 오류"가 아니다——인증 문제라는 걸 전혀 알 수 없다. 현재는 crontab으로 6시간마다 재계산한다.
오디오가 끊기면 영상까지 끌어내린다. go2rtc의 다중 소스 트랙 합성은 "전부 아니면 전무"라서, 오디오 소스가 연결되지 않으면 전체 스트림이 시작되지 않고 영상까지 같이 검게 된다. 해결법은 브리지가 "클라이언트가 있는 한 항상 오디오 트랙을 출력"하게 만드는 것——실제 오디오가 600ms 이상 끊기면 무음 프레임을 채우고, TUTK 연결 실패는 지수 백오프로 재시도한다.
WebRTC에는 Opus가 필요하다. 오디오를 AAC만 제공하면 WebRTC가 가져가지 못하고(AAC 미지원), 브라우저가 MSE로 폴백하면서 외부 네트워크에서 버벅인다. Opus 트랜스코딩을 추가한 후 WebRTC가 오디오를 전달할 수 있게 되어 외부 네트워크에서도 원활해졌다.
양방향 통화#
송화 프로토콜도 리버싱했다. 장치는 AAC-LC 16kHz mono ADTS 형식의 오디오를 수신한다(수신 쪽과 동일). 올바른 코덱을 식별하는 방법은 무식했다: 네 가지 후보 형식에 각각 다른 비프음 횟수의 음성 파일(1/2/3/4회)을 만들어 한 번에 전부 전송하고, 급식기 옆에서 사람이 비프음을 세는 것——세 번째 그룹의 비프음만 들렸다면 그 형식이 맞는 거다.
NOTE
송화 도중 6일간 숨어 있던 버그를 발견했다: Python의 struct.pack이 부호 있는 int32로 밀리초 타임스탬프를 넣는데, 값이 2^31을 초과하면 바로 struct.error를 던진다——송화가 첫 프레임을 보내기도 전에 중단됐고, 한 프레임도 전송되지 않았다. 앞서 네 라운드에 걸쳐 다른 코덱 형식을 맹목적으로 스캔했는데, 실행조차 되지 않는 경로를 테스트한 셈이었다. 교훈: "실제로 전송됐는지" 확인하는 것이 "전송 형식이 맞는지"보다 중요하다.
실시간 통화(브라우저 마이크로 직접 말하기)는 성공하지 못했다. go2rtc의 exec: 소스에 backchannel 지원이 있어서 이론적으로는 가능하지만, 브라우저가 통화 모드로 전환할 때 go2rtc가 producer를 시작하지 않았다. 다양한 오디오 형식과 아키텍처를 시도했지만 결과는 같았고, 근본 원인을 찾지 못했다. 현재 대안은 HA에 버튼을 만들어서, 누르면 미리 녹음된 알림음을 재생하는 것——고양이를 밥 먹으러 부르는 데는 충분하다.
급식기 카메라의 개별 고양이 식별#
급식기 카메라는 기기 상단에 장착되어 있고, 스테인리스 스틸 그릇은 화면 최하단에 있다. 고양이가 밥을 먹을 때 얼굴이 카메라를 정면으로 향하고, 거리가 약 40cm, 화면에서 얼굴이 차지하는 비율이 약 37%——3미터 밖에서 찍는 C210의 6%보다 훨씬 크다. 이론적으로 aki와 kuro를 구분하기가 더 쉬워야 한다.
그릇 앞 영역#
Frigate에서 feeder_bowl 영역을 정의했다. 화면 하단 5%를 커버한다. 이 영역에 10초 이상 머문 고양이만 "방문"으로 집계해, 그릇 옆을 지나가는 경우를 걸러낸다.
히스토리 이벤트로 이 설정을 검증했다: 바운딩 박스 하단 좌표 ≥ 0.95인 이벤트에서 8장을 샘플링했는데, 8장 전부 먹고 있었다. 0.88에서 0.95 사이는 8장 중 2장만 먹고 있었다. 10초 임곗값에 영역 제한을 더하면 오판율이 매우 낮다.
zone을 추가하기 전에는 daemon이 "고양이가 거실 전체에서 ≥10초 체류"로 계산했다——카메라가 거실 전체를 찍기 때문에, 하루에 30회 이상이 찍히고, 대부분 식탁이나 선반 위의 고양이지 밥 먹는 게 아니었다. 추가 후 하루 약 7.5회로 줄어서 합리적이 됐다.
식사 병합#
daemon이 Frigate의 이벤트 API에서 "오늘 feeder_bowl 영역에서 감지된 고양이"를 가져와서, 같은 고양이의 두 이벤트가 2분 이내 간격이면 같은 식사로 병합한다(Frigate는 한 끼를 여러 이벤트로 나누는 경우가 많다). HA에는 sensor가 두 개 있다: aki와 kuro의 오늘 방문 횟수.
NOTE
이름은 "방문"이지 "식사"가 아니다. 카메라는 고양이가 그릇 앞에 얼마나 있었는지만 확인할 수 있고, 실제로 먹고 있었는지는 알 수 없다——앉아만 있거나 그루밍 중일 수도 있다. 처음에는 "오늘 식사 횟수"로 이름 붙였다가, 추론을 사실처럼 쓴다는 지적을 받았다.
전체 시스템의 현재 모습#
| 항목 | 사양 |
|---|---|
| 하드웨어 비용 | NT$3,990(FurSpark Big Eye, quoview 최저가) |
| 월 비용 | NT$0 |
| 영상 | 1664×2224 세로형, 25fps, 로컬 실시간 스트리밍(TCP 6200 → go2rtc → Frigate) |
| 녹화 | 연속 녹화 3일(~15 GB/일, C210의 ~10 GB/일과 같은 디스크) |
| 급식 제어 | HA에서 스위치 토글, 양 조절, 스케줄 편집, 원클릭 급식 |
| 오디오 | 실시간 수음(TUTK 브리지 → Opus/AAC → WebRTC), HA 버튼으로 알림음 재생 |
| 개별 식별 | 그릇 앞 영역 + 10초 임곗값, 일일 방문 횟수 × 두 마리 |
| 감지 | Frigate 객체 감지(cat / person), GPU 사용률 6% |
| 상주 서비스 | furspark-ha.service(급식 제어), tutk-audio-bridge(오디오), go2rtc(스트리밍) |
Furbo와 비교:
| Furbo 360° + 펫 시터 구독 | 이 시스템 | |
|---|---|---|
| 하드웨어 | NT$5,399(약정 없는 단독 구매) | NT$3,990 |
| 연 비용 | NT$1,840부터 | NT$0 |
| 녹화 | AI 클립, 3–7일 보관 | 연속 녹화, 보관 일수 자유 설정 |
| 다시보기 | 구독 한정 | 언제든 |
| 양방향 통화 | 있음 | 미리 녹음된 알림음 재생 |
| 간식 던지기 | 있음 | 정시 급식(간식 던지기 없음) |
| 펫 다이어리 | 구독 기능 | 이전 글에서 만든 것 |
| 개체 식별 | 구독 기능(다중 반려동물 인식) | Frigate 자체 학습 분류(그릇 앞 근접 촬영 명명률 ~89%) |
| 데이터 위치 | Furbo 클라우드 | 로컬 하드 디스크 |
마지막 한 가지. 이 급식기의 RTSP daemon은 살아 있고, 미디어 소스가 마운트되지 않았을 뿐이다. 펌웨어에 "RTSP 활성화"로 보이는 명령이 두 개 있는데(이름은 커뮤니티에 공개된 블랙박스 펌웨어 문자열 테이블에서 찾았다), 시도하면 오디오 트랙이 포함된 RTSP 스트림을 바로 얻을 수도 있다. 하지만 장치 상태가 변경되어 현재 안정적으로 동작하는 6200 라이브 스트림이 중단될 위험이 있어서 아직 시도하지 않았다. 언젠가 펌웨어 업데이트가 뭔가를 망가뜨려서 어쩔 수 없이 재설정해야 할 때 해볼 생각이다.