1. 프로젝트가 하는 일
이 프로젝트는 엣지 디바이스(ESP32)에서 온·습도 센서(AHT20/21) 값을 읽고, 작은 예측 모델(MLP, Rolling Window 4단계·12-64-32-2 ReLU)로 “다음 온·습도”를 추정한다. 예측이 괜찮으면 전송하지 않고, 예측 오차가 임계값을 넘을 때만 433MHz LoRa로 한 번 보낸다. 그렇게 해서 통신 횟수·전력을 줄이면서도, 게이트웨이와 엣지 모두에서 실제값을 받을 때마다 온라인 학습으로 같은 모델을 유지·동기화한다. 수집된 데이터는 MQTT를 한 번 발행한 뒤, 구독자가 MySQL·CSV에 각각 저장하고, Flask 대시보드·API·Prometheus 메트릭·Grafana로 모니터링한다. 실험 시간대는 라스베이거스(UTC-8) 기준으로, 엣지·게이트웨이 모두 “하루 중 시간 비율(time_n, 0~1)”을 입력으로 쓴다. 한 줄로 말하면, “필요할 때만 보내고, 엣지와 게이트웨이가 같은 모델로 함께 학습하는 IoT + AoII 실험 시스템”이다.
2. AoII와 시스템 구성
AoII(Age of Incorrect Information)란?
“잘못된 정보가 얼마나 오래 유지되는가”를 재는 지표다. IoT·센서 네트워크에서는 데이터를 매번 보내지 않고도 원격지가 현재 상태를 잘 추정하게 만드는 연구와 연결된다. 이 프로젝트에서는 온도 오차 β_temp(0.5°C) 또는 습도 오차 β_hum(3%) 이상일 때, 또는 10분 주기 하트비트, 시간 미동기화일 때만 전송한다. 그 외에는 전송을 생략(SKIP)해 전력을 아끼고, 전송 횟수·오차·수신 데이터를 DB·CSV·메트릭으로 쌓아 AoII·전송 효율·모델 성능을 분석할 수 있게 했다.
시스템 구성 상세

엣지: Heltec WiFi LoRa 32 v2, AHT20/21 온습도, 433MHz LoRa, OLED. 1분 주기로 light sleep 후 측정·예측·전송 조건 판단을 한다. 전송 조건을 만족하면 LoRa로 “온도,습도” 문자열을 보내고, 게이트웨이에서 시리얼로 넘겨 주는 Unix 시간을 받아 시간 동기화 후 엣지 MLP를 한 번 온라인 학습한다. 시간만 요청할 때는 Ping(0.0, 0.0) 같은 특수 패킷을 쓰고, 응답으로 시간만 받을 수 있다. USB 시리얼로 매 주기마다 SKIP/SEND/HEARTBEAT 등 한 줄 로그를 출력해, 별도 로거로 수집해 DB에 남길 수 있다.
게이트웨이(하드웨어): ESP32 + LoRa. 엣지 패킷을 받으면 수신 내용을 그대로 USB 시리얼로 전달한다. ML 연산은 하지 않고, 게이트웨이 소프트웨어가 시리얼을 읽어 처리한다.
게이트웨이(소프트웨어): 시리얼 포트는 설정 파일(.env)에서 지정한다(맥북 예: /dev/cu.usbserial-3, 라즈베리파이 예: /dev/ttyUSB0). 시리얼에서 “Received: 온도,습도” 형태의 문자열을 파싱해 실제 온·습도를 얻고, 엣지와 동일한 12-64-32-2 MLP로 현재 상태에서 예측한 뒤 실제값으로 온라인 학습(역전파)을 수행한다. 그다음 MQTT 브로커(설정에서 주소·포트 지정, 라즈베리파이에서 실행 시 보통 localhost, 맥북에서 실행 시 라즈베리파이 IP)로 특정 토픽(예: aoii/readings)에 JSON을 발행한다. 이벤트 종류는 두 가지다.
RX: 엣지에서 데이터를 수신했을 때(실제 온·습도, 예측값, 오차, 누적 전송 횟수 등 포함)
EST: 60초마다 게이트웨이만의 예측값을 보낼 때. RX가 발생하면 엣지에게 시리얼로 Unix 시간을 보내 ACK 역할을 한다.
메시지 브로커: Mosquitto를 라즈베리파이에서 실행한다(기본 포트 1883). 외부에서 접속하려면 listener를 0.0.0.0으로 두고, 필요 시 allow_anonymous 등 설정을 한다. 발행·구독은 하나의 토픽으로 중개된다.
저장: 구독자 두 개를 둘 수 있다.
(1) DB 저장: event=RX인 메시지만 MySQL의 readings 테이블에 insert한다. 맥북에서 MySQL을 쓰고 이 구독자를 실행하면, created_at은 로컬 시간으로 저장해 대시보드·CSV와 맞춘다.
(2) CSV 저장: RX·EST 모두 프로젝트 루트의 experiment_log_online.csv에 한 줄씩 append한다. 라즈베리파이에서 이 구독자만 실행하면 Pi에 CSV가 쌓여 실험 로그·재현용으로 쓴다.
(3) 선택적으로 엣지 USB 시리얼을 읽는 로거를 두어, SKIP/SEND/HEARTBEAT 구분으로 edge_log 테이블에 남길 수 있다.
모니터링: Flask 서버는 DB에서 통계(get_stats)·최근 데이터(get_recent)를 조회해 대시보드(차트·통계)와 API(/api/stats, /api/recent)를 제공한다. 동시에 Prometheus 형식의 /metrics 엔드포인트를 노출해, aoii_readings_total(총 수신 횟수), aoii_mae_temp(온도 평균 절대 오차) 등 메트릭을 준다. Prometheus는 설정(prometheus.yml)에 따라 이 /metrics를 주기적으로 스크래핑하고, Grafana는 Prometheus를 데이터 소스로 연결해 대시보드·알람을 만든다.
3. 데이터 흐름과 ML 모델
데이터 흐름 (단계별)

1단계 — 엣지: AHT로 온도·습도를 측정한다. 입력은 과거 4시점의 (온도, 습도, time_n)을 펼친 12차원이다. 12-64-32-2 MLP로 “다음 예측 온도·습도”를 낸 뒤, 실제 측정값과 비교해 err_t ≥ β_temp(0.5°C) 또는 err_h ≥ β_hum(3%)이면, 또는 10분 하트비트 구간이면, 또는 시간이 아직 동기화되지 않았으면 LoRa로 “온도,습도”를 전송한다. 전송한 경우에는 게이트웨이 ESP32가 시리얼로 넘겨 주는 Unix 시간을 LoRa로 받아 시간을 맞추고, 방금 보낸 실제값으로 엣지 MLP를 한 번 역전파(온라인 학습)한다.
2단계 — 게이트웨이 ESP32: LoRa로 엣지 패킷을 받으면 수신 문자열(예: “Received: 25.3,60.2”)을 USB 시리얼로 그대로 PC 또는 라즈베리파이에 전달한다. Ping 0.0,0.0이면 시간 요청/응답만 처리할 수 있다.
3단계 — 게이트웨이 소프트웨어: 시리얼에서 “Received: ...”를 파싱해 실제 온·습도를 얻는다. 동일한 12-64-32-2 MLP로 현재 상태에서 예측한 뒤, 이 실제값으로 온라인 학습(online_update)을 호출해 가중치를 갱신한다. MQTT로 aoii/readings 토픽에 JSON을 발행한다. event=RX(엣지 수신 시) 또는 60초마다 event=EST(게이트웨이만의 예측). 페이로드에는 timestamp, time_n, actual_t, actual_h, pred_t, pred_h, error_t, error_h, total_tx 등이 들어간다. RX일 때는 엣지에게 시리얼로 Unix 시간을 보내 ACK·시간 동기화를 한다.
4단계 — MQTT 구독자: DB 저장 구독자는 event=RX인 메시지만 MySQL readings 테이블에 insert한다(맥북에서 실행). CSV 저장 구독자는 RX·EST 모두 experiment_log_online.csv에 한 줄씩 append한다(라즈베리파이에서 실행 시 Pi에 파일 생성).
5단계 — 모니터링: Flask는 DB에서 get_stats·get_recent를 조회해 대시보드·API를 제공하고, /metrics에서 Prometheus 메트릭을 노출한다. Prometheus가 이를 스크래핑하고, Grafana에서 시각화·알람을 구성한다.
ML 모델 (12-64-32-2 MLP, Rolling Window)
구조: 입력 12(과거 4시점의 온도·습도·time_n을 펼친 벡터) → 은닉층 64(ReLU) → 은닉층 32(ReLU) → 출력 2(예측 온도, 예측 습도). 엣지와 게이트웨이에 동일한 구조·가중치를 둔다.
사전 학습: 사전에 수집한 데이터셋(타임스탬프, 온도, 습도)으로 Rolling Window(크기 4)를 적용해 12차원 입력·2차원 출력 데이터를 만들고, sklearn MLPRegressor 12-64-32-2(ReLU)를 학습한 뒤, 가중치와 스케일러(정규화 파라미터)를 추출해 엣지에는 C 배열로, 게이트웨이에는 Python으로 동일하게 반영한다.
온라인 학습: 전송이 일어날 때마다 엣지·게이트웨이 모두 실제값(방금 수신한 온·습도)으로 역전파를 한 번 수행한다. 학습률은 0.05 등으로 고정해 두고, 같은 데이터로 갱신하므로 양쪽 모델이 계속 동기화된다.
비교 실험: “온라인 학습 없이 고정 모델만 쓰는 경우”(오프라인 TinyML 비교군)는 게이트웨이에서 online_update 호출만 주석 처리하면 된다. CSV·DB에 event, total_tx, error_t, error_h가 쌓이므로, β 변경·학습 여부에 따른 전송 횟수·MAE 비교가 가능하다.
4. 내 역할과 맡은 부분
이 프로젝트에서 나는 데이터 파이프라인 설계·구현, 설정·저장·모니터링 정리, 그리고 장애 지점 파악·보완 과제 정리를 맡았다. 엣지 펌웨어나 LoRa 통신 프로토콜 자체보다는, 게이트웨이가 받은 패킷을 어디로 보낼지(한 번만 MQTT 발행), 누가 DB에 넣고 누가 CSV에 넣을지(구독자 분리), 시리얼 포트·MQTT 브로커 주소·DB 비밀번호는 어디에 두는지(.env 단일 소스), DB·CSV·대시보드의 시간을 어떻게 맞출지(로컬 시간 저장), 파이프라인 어디가 끊기면 어떤 영향인지(장애 지점 표), 장애가 생기면 무엇을 더 해야 하는지(QoS·DLQ·재시도·백업·런북 등)를 문서와 코드로 정리하는 쪽에 기여했다.
5. 지금까지 진행한 작업
이벤트 기반 파이프라인으로 전환

원래는 게이트웨이가 수신할 때 DB와 CSV에 직접 쓰는 구조였다. DB 연결 실패나 CSV 쓰기 실패가 나면 한 경로 장애가 전체 저장 실패로 이어질 수 있어서, 게이트웨이는 수신 시 MQTT로 한 번만 발행하도록 바꿨다. DB 저장은 “RX만 MySQL readings 테이블에 insert하는 구독자”, CSV 저장은 “RX·EST 모두 experiment_log_online.csv에 append하는 구독자”가 각각 구독해 처리한다. 발행부와 저장부를 분리해 장애 격리가 되었고, 나중에 저장 채널을 추가하거나(예: 다른 DB, 다른 파일) 변경할 때도 구독자만 추가·수정하면 된다.
분산 환경 역할 분리·문서화
라즈베리파이에는 MQTT 브로커(Mosquitto)와 CSV 저장 구독자만 실행하고 MySQL은 설치하지 않는다. 맥북에서는 게이트웨이 프로세스(시리얼 수신), MySQL, DB 저장 구독자를 실행한다. “어디서 무엇을 실행할지”, 실행 순서(예: 브로커 → 게이트웨이 → 구독자), 포트·토픽 이름(aoii/readings 등)을 MQTT·모니터링 문서에 정리해 두었다.
설정·비밀정보 단일 소스
여러 스크립트가 각자 설정 파일을 갖지 않고, 프로젝트 루트의 .env 한 곳만 읽도록 통일했다. SERIAL_PORT, MQTT_BROKER, MQTT_PORT, MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD 등 필요한 키를 .env에 두고, .env는 git에서 제외한다. 팀원에게는 “필요한 키 목록 + 예시값”만 공유하고, 실제 비밀번호는 직접 채우도록 했다. mysql_example.env 같은 중복 예시 파일은 제거했다.
데이터 정합성(시간)
DB에만 UTC로 저장하고 CSV·모니터링은 로컬 시간을 쓰다 보니 8~9시간 차이가 났다. DB 저장 시 insert_reading·insert_edge_log에서 created_at을 로컬 시간으로 기록하도록 수정해, 대시보드·CSV와 맞춰 두었다.
모니터링·문서 정리
Flask의 /metrics 노출 → Prometheus가 prometheus.yml 설정대로 스크래핑 → Grafana에서 Prometheus를 데이터 소스로 연결하는 절차와, aoii_readings_total, aoii_mae_temp 등 메트릭 목록을 MONITORING.md에 정리했다. MQTT.md, MONITORING.md를 “실행 순서·역할·설정” 중심으로 재구성해, 새로 합류한 사람이 어디서 무엇을 켜야 하는지 따라 할 수 있게 했다.
코드·중복 정리
루트에 있던 중복 게이트웨이 스크립트를 삭제하고, RX 이벤트가 두 번 로그에 찍히던 부분을 제거했다. 시리얼 포트·MQTT 브로커 주소 등은 모두 .env에서 읽도록 환경 변수화해, 설정 변경은 .env 한 곳에서만 하면 되도록 했다. Prometheus가 생성하는 data/ 폴더 용도도 문서에 적어 두었다.
6. 장애가 날 수 있는 지점
파이프라인은 “엣지 → LoRa → 게이트웨이 ESP32 → USB 시리얼 → 게이트웨이 소프트웨어 → MQTT 브로커(라즈베리파이) → mqtt_to_csv / mqtt_to_mysql” 순이다. 구간별로 장애 가능 지점·영향·비고를 정리해 두었다.
- 시리얼: USB 케이블 분리, 잘못된 포트 지정, 다른 프로세스가 포트 점유(예: 아두이노 IDE 시리얼 모니터)하면 게이트웨이 소프트웨어가 데이터를 받지 못하고 전체 파이프라인 중단이 된다. 맥북에서 게이트웨이를 돌릴 때 시리얼 포트 충돌이 자주 원인이 된다.
- 게이트웨이 프로세스: 크래시, 맥북 슬립/종료 시 수신·MQTT 발행이 끊긴다. 이후 구간(브로커·구독자)에는 데이터가 들어오지 않는다. 단일 프로세스라 재시작 전까지 복구되지 않는다.
- 맥북–라즈베리파이 네트워크: Wi‑Fi 끊김, 라즈베리파이 전원 나감·재부팅 시 게이트웨이가 브로커에 접속하지 못하고, 맥북에서 돌리는 DB 저장 구독자도 브로커를 구독하지 못한다. 브로커가 라즈베리파이에 있으므로 네트워크 의존도가 크다.
- MQTT 브로커(Mosquitto): 프로세스 종료, Pi 디스크 풀, Pi 전원 장애 시 모든 구독자에게 메시지 전달이 불가하다. 단일 브로커라 SPOF(단일 장애점)이다.
- CSV 저장 구독자: 프로세스 종료, Pi 디스크 풀, 파일 권한 오류 시 CSV만 갱신되지 않는다. DB·MQTT 자체는 동작하므로 영향은 CSV 구간으로 한정된다.
- DB 저장 구독자·MySQL: 구독자 프로세스 종료나 MySQL 다운·연결 한도 초과 시 DB에 insert가 되지 않는다. 현재 MQTT는 QoS 0이라 브로커가 메시지를 보관하지 않는다. 따라서 구독자가 다운된 시점에 발행된 메시지는 유실되고, 재시작 후에도 “그 사이” 메시지는 복구할 수 없다. MySQL 쪽에서 insert 실패 시 재시도가 없으면 해당 메시지 역시 유실된다.
요약하면, 전체 중단은 시리얼 단절·게이트웨이 중단·브로커 중단·맥북–Pi 네트워크 단절일 때이고, 부분 유실은 DB 저장 구독자·MySQL 장애 시 해당 구간 데이터 유실, 그리고 QoS 0으로 인한 “구독자 다운 시점 메시지 유실”이다.
7. 앞으로 발전시킬 것들
한 번 들어온 데이터는 유실 없이 처리·보관하고, 장애 시 빠르게 알아내고 복구할 수 있게 하려면 아래 같은 보완이 필요하다. 우선순위와 리소스에 맞춰 단계적으로 적용할 수 있다.
파이프라인·가용성
- 메시지 유실 방지: 현재 QoS 0은 브로커가 메시지를 저장하지 않아 구독자 다운 시 유실된다. QoS 1/2 또는 persistent session을 도입하면 구독자가 잠시 꺼져 있어도 브로커가 메시지를 보관·재전달할 수 있다. 채널 암호화·신뢰성 요구에 맞추려면 보통 보완이 필요하다.
- Dead Letter Queue(DLQ): DB insert 실패 등 처리 실패 메시지를 별도 토픽·큐로 보내 두고, 원인(DB 다운, 스키마 오류 등)을 조치한 뒤 재처리(재생)할 수 있게 한다. 한 번은 반드시 처리하거나 보관하는 구조로 가져가는 것이 좋다.
- 브로커 이중화: 단일 Mosquitto는 SPOF이므로, 클러스터 또는 대기 브로커를 두어 장애 시 전환되도록 구성하면 가용성이 올라간다.
- 게이트웨이 자동 재기동: systemd·supervisor 등으로 게이트웨이 프로세스를 감시하고, 죽으면 자동 재시작하도록 한다. 가능하면 동일 역할을 하는 인스턴스를 여러 개 두는 것도 검토할 수 있다.
장애 대응·모니터링
- 헬스체크·알람: 이미 “수신 횟수·MAE” 등 메트릭이 있으므로, Grafana에서 “N분 이상 수신 없음”일 때 알람 규칙을 강화한다. 게이트웨이·MQTT·MySQL 프로세스가 살아 있는지 주기적으로 확인하는 헬스체크를 추가하면, 어느 구간에서 끊겼는지 빠르게 파악할 수 있다.
- 장애 시나리오 런북: “시리얼이 안 뜬다 → 포트 목록 확인, 다른 프로세스 점유 여부, 재연결” / “MQTT 연결 실패 → 브로커 주소·포트·네트워크 확인” / “MySQL 연결 실패 → 서비스 상태·연결 한도·비밀번호 확인” / “CSV가 안 쌓인다 → 구독자 프로세스·디스크·권한 확인”처럼 증상별 원인·확인 절차·조치를 문서(MD)로 정리해 두면 운영 시 유용하다.
- 메트릭 보강: “마지막 MQTT publish 성공 시각”, “구독자별 마지막 처리 시각”, “insert 실패 횟수” 등 파이프라인 구간별 메트릭을 추가하면, 어디서 지연·실패가 나는지 시각화·알람으로 잡기 쉬워진다.
- 로그 표준화: 에러·경고 시 시간·구간·에러 코드를 포함한 구조화된 로그를 남기고, 필요 시 중앙 수집(파일·에이전트)으로 추적하기 쉽게 한다.
데이터·운영 안정성
- 재시도·백오프: MySQL insert 실패 시 제한된 횟수·지수 백오프로 재시도하고, 그래도 실패하면 DLQ로 보낸다. 무한 재시도는 부하를 키우므로 막는 것이 좋다.
- 멱등성: 같은 메시지가 중복 수신돼도 중복 insert가 나지 않도록, 메시지 ID나 (timestamp, device) 같은 유니크 키로 제약을 두거나 “이미 있으면 skip” 로직을 넣는다.
- 백업·복구: DB·CSV 정기 백업과 보관 주기·삭제 정책을 정하고, 복구 절차(RPO/RTO)를 정의해 두면 장애 시 데이터 복구가 수월하다.
- 리텐션·용량: CSV·DB 보관 기간과 디스크 사용량을 추정하고, 오래된 데이터는 아카이빙·삭제하는 정책을 두어 디스크 풀을 방지한다.
보안·감사
- MQTT 브로커·클라이언트 간 TLS 적용(채널 암호화 요구 대응), MySQL 연결 TLS·필요 시 민감 컬럼 암호화, 설정(.env)·중요 스크립트 배포 시 “누가/언제/무엇을” 변경했는지 이력 관리 절차를 검토한다.
설계·문서
- 엣지–게이트웨이–MQTT–구독자–DB/CSV 구간과 장애 지점을 한 그림에 표시한 아키텍처 다이어그램을 두면, 신규 합류자·운영자가 구조를 이해하기 쉽다. Part 2의 장애 지점 표를 확장해 “장애 유형 → 영향 구간 → 복구 절차” 매트릭스로 정리해 두는 것도 좋다. 용어(Gateway, 브로커, 구독자, readings 등)를 문서·코드 주석에서 통일하면 유지보수에 도움이 된다.
8. 마치며
이 프로젝트는 엣지 IoT에서 AoII를 고려한 “필요할 때만 전송”을 구현하고, 엣지와 게이트웨이에 같은 12-64-32-2 MLP를 두어 전송 시마다 온라인 학습으로 동기화하며, MQTT로 발행·구독을 나눠 수집·저장·모니터링을 분리한 구조다. 설정은 .env 단일 소스로 두고, 실행 순서와 역할은 MQTT·모니터링 문서를 참고하면 된다.
나는 그중에서 파이프라인 설계·이벤트 기반 전환(게이트웨이 → MQTT 한 번 발행, 구독자 분리), 분산 환경(라즈베리파이·맥북 역할 분리)·문서화, .env 단일 소스·비밀정보 관리, DB 시간 정합성(로컬 시간 저장), 모니터링 절차·메트릭 목록 정리, 코드·중복 제거, 그리고 “어디가 깨질 수 있는지”를 문서로 정리하는 역할을 했다.
앞으로는 메시지 유실 방지(QoS 1/2·persistent session·DLQ), 재시도·백오프·멱등성·백업, 헬스체크·알람·장애 시나리오 런북, 메트릭 보강, 필요 시 브로커 이중화·TLS·감사 절차를 단계적으로 적용하면, 실험용을 넘어 운영·서비스 관점에서도 더 견고한 시스템으로 발전시킬 수 있다.
'UNLV' 카테고리의 다른 글
| [UNLV] 🇺🇸 Las Vegas에서 한 달 여행기 - 상편 (0) | 2026.03.10 |
|---|---|
| [UNLV] - 온라인 머신러닝과 이상 탐지: 이론과 진동 센서 실습 (13일차) (0) | 2026.02.24 |
| [UNLV] - 이상 탐지: 이론과 진동 센서 실습(12일차) (0) | 2026.02.21 |
| [UNLV] - 오프라인 음성 인식으로 Tello 드론 제어(11일차) (0) | 2026.02.20 |
| [UNLV] - OpenCV와 YOLO: 컴퓨터 비전 실습(10일차) (2) | 2026.02.19 |