공부/인공지능

문패를 e-paper로 바꿨더니 배터리가 13배 오래 갔다 — ESP32 문패 구축기

TechToast 2026. 10. 14. 13:09
반응형

무엇을 만들었나

연구실 문 앞에 붙이는 '자리 비움' 표시는 종이 문패를 쓰면 상태를 바꿀 때마다 다시 써야 하고, 재실/부재/강의/회의/퇴근 상태를 그때그때 알리고 싶으면 사람이 직접 바꿔야 합니다. 그래서 온라인으로 상태를 바꾸면 몇 초 안에 문패 화면이 갱신되는 e-paper 문패를 만들었습니다. 만들면서 배터리를 13배 아끼는 과정과, 펌웨어에 숨어 있던 정수 오버플로 버그까지 함께 다뤄봅니다.

 

 

하드웨어는 ESP32(T-Display 클론)와 4.2인치 흑/백/적 3색 e-paper(SSD1683)입니다. 3색이라 흑 평면과 적 평면 두 장을 겹쳐서 400×300 픽셀에 그립니다. 서버는 랩 내부 서버의 Docker 컨테이너가 담당하는데, FastAPI와 Pillow가 3색 비트맵(약 30KB)을 렌더링하고 한글 폰트 처리도 서버 쪽에서 합니다. 웹 UI는 브라우저에서 상태 카드나 그림을 클릭하면 문패로 전송해 주고, 순서 드래그, 이미지 업로드, 맞춤/채움·디더링 전환까지 지원합니다.

 

동작 방식을 비유하면 이렇습니다. 문패는 한참 자리에 앉아 "혹시 그림 바뀌었어요?"라고 서버에 물어보기를 반복하고, 서버는 "그림이 바뀌었으니 새로 받아 가세요"라고 답해주는 방식입니다. 이렇게 주기적으로 물어보는 걸 롱폴링이라고 합니다. 그런데 24시간 내내 자리 지키는 문패에게 이 대화 주기는 곧 배터리와 직결되어서, 여기부터 삽질이 시작됩니다.

 

 

배터리: 3시간이 41시간으로

가장 큰 삽질은 배터리였습니다. 처음 Arduino 단독 빌드는 라이트슬립 기능을 못 써서 CPU 240MHz로 계속 깨어 있었습니다. 라이트슬립은 컴퓨터로 치면 화면만 꺼두는 절전 모드 같은 것이라, 이걸 못 쓰면 문패가 학습 때 GPU처럼 계속 100% 근무하는 셈입니다. 그 결과 약 219mV/h씩 떨어져 3시간 만에 방전됐습니다. 그냥 못 씁니다.

 

tdisplay_lp 빌드(ESP-IDF 기반 PlatformIO)에서는 CONFIG_PM_ENABLE과 tickless idle을 켜서 자동 라이트슬립을 넣고, CPU를 80/40MHz로 낮추며, WiFi는 MAX_MODEM listen_interval 10으로 설정하고, 롱폴링 주기를 240초로 잡았습니다. 이렇게 하니 약 12~18mV/h로 떨어져서 대략 13배 개선됐습니다.

 

측정 결과는 기존 셀(라벨 560mAh)로 완충 후 41.0시간을 버텼습니다. 참고로 충전 시 USB 입력이 약 1W였던 것으로 보아 셀 실제 용량은 약 200mAh로 추정되는데, 라벨이랑 실제가 3배 가까이 달랐습니다. 평균 전류는 약 5mA입니다.

 

그런데 배터리를 재는 일 자체가 또 다른 관문이었습니다. 멀티미터로 3.90V를 확인하는데 ADC는 3944mV로 읽혔습니다. 약 1% 높게 읽히는 셈이라 게인 0.9888로 보정했습니다. 그리고 이 보드의 충전 회로는 4.2V가 아니라 4.098V에서 충전을 끝냅니다. 처음에 '만충 = 4.18V'로 가정했다가, 완충 직후 93%로 표시되는 어이없는 버그가 생겼습니다. USB를 꽂으면 충전 중인 VBUS(약 4.9V)가 ADC에 읽혀 잔량을 실측할 수 없어서, 서버는 시작 잔량과 경과 시간으로 추정합니다. CSV에는 원시 ADC만 저장하고 보정은 표시할 때만 적용해서, 나중에 보정식을 바꾸면 과거 기록도 다시 계산되게 했습니다.

 

10/12에 3500mAh 셀로 교체했습니다. 새 셀 완충 1회 지속 시간이 다음 기준값이 됩니다.

자동 출퇴근 표시

문패를 손으로 바꾸는 것도 귀찮아서, autostatus 데몬을 붙였습니다. 판단 근거는 구글 캘린더 '수업' 일정과 라즈베리파이 CCTV의 사람 감지입니다. 1분마다 in | away | lecture | meeting | out 상태를 서버로 POST합니다. 웹에서 수동으로 바꾸면 30분 동안 자동 전환을 보류합니다.

 

상태는 글자 카드 대신 그림 하나씩으로 연결했습니다. 재실은 '방에 있어요', 부재중은 '외부에 있어요', 강의 중은 '수업갔어요', 회의 중은 '회의중', 퇴근은 '퇴근', 운동 중은 '운동갔어요'처럼요. 자동화 쪽은 상태 이름만 보내고, 서버가 연결된 그림으로 바꿔 표시하는 구조라서 상태 표를 관리하기 편해졌습니다.

 

 

펌웨어에 숨은 버그

롱폴링을 240초로 잡았다고 생각했는데, 로그를 보니 실제로는 약 63초마다 요청을 포기하고 다시 보내고 있었습니다. 원인은 HTTPClient::setTimeout의 인자가 uint16_t(ms)이기 때문입니다. 260000ms를 넣으면 63392ms로 잘린 겁니다. 마치 10키로짜리 상자에 260키로그램짜리 짐을 실으려다가 무게 제한에 걸려 잘려서 들어온 것과 같습니다. 그 결과 서버에는 버려진 요청이 계속 쌓였습니다. 41시간이라는 수명도 사실 이 약 1분 주기 조건에서 잰 값이었습니다.

 

수정은 WAIT_SEC=55, 타임아웃 65초로 잡았습니다. 여기에 자가 복구도 넣었습니다. 연속 3회 실패하면 WiFi를 재연결하고, 20분간 무응답이면 재부팅합니다. 부팅 원인(rr), 누적 실패(nf), WiFi 끊김 사유(ds)를 서버로 보고해서 지금 이 문패가 왜 이 상태인지 알 수 있게 했습니다.

정직한 한계, 그리고 다음 계획

새 셀 완충 지속 시간은 아직 확보하지 못했습니다. 기존 셀의 실제 용량도 몰라서 예상은 11~30일로 넓습니다. 다음 완충 주기에서 기준값을 잡으려 합니다.

 

철문이 WiFi를 약하게 만드는 것도 숙제입니다. 문 밖에서는 -75~-85dBm까지 떨어지는데, 철판이 PCB 안테나를 가리는 영향으로 추정됩니다. 공유기를 문 가까이 옮기거나 안테나 쪽을 위로 띄우는 걸 시도해볼 계획입니다. 3500mAh로 커진 무게 때문에 케이스가 벌어지는 문제는, 배터리를 뒷판(자석 쪽)에 고정하고 모서리를 글루건이나 M2 태핑 나사로 보강하려 합니다.

 

3~4주를 못 버티면 DFRobot FireBeetle 2 ESP32-C6로 교체하고 TWT(이 공유기는 이미 켜져 있습니다)를 검토합니다. 그리고 ntfy 휴대폰 알림(잔량 15%·5%)과 data 폴더 백업을 선택지로 남겨뒀습니다.

오픈소스

전체 소스는 GitHub에 공개했습니다. github.com/yangzepa/roomdisplay입니다. server/(Docker·FastAPI·웹 UI), firmware/(PlatformIO), hardware/(부품·결선·케이스 STL), docs/(전력·배터리 노트, 원시 배터리 로그, 곰 일러스트 원본)로 구성되어 있습니다. 와이파이 비밀번호, 내부 IP, 구글 토큰은 제외했습니다.

반응형