- tools/make_deploy_bundles.py: PMS7003·Zero2W 디스플레이 노드 번들 생성 반영(폐쇄망 동봉) - tools/harden_pi.sh: 하드닝 스크립트 갱신 - docs/superpowers: 노드 센서 통합·스모크 파일럿·대시보드 다크 설계/계획 문서 - docs/claude-config: IoT 납품·검증 스킬/룰 참조본 - review/: 프로젝트 리뷰·UI v2 목업
12 KiB
⚠️ SUPERSEDED 2026-07-19: 화재/연기 감지는 PMS7003 미세먼지 metric 통합으로 대체됨. 개조/불꽃/CO 경로 폐기. 아래는 이력 보존용. 대체 설계:
2026-07-19-node-sensor-integration-design.md
연기(화재) 감지 노드 — 시범(Pilot) 설계
- 작성일: 2026-07-18
- 브랜치: feat/pi-c-sensor3-leak-node
- 범위: 기존 Pi4 누수+온습도 노드에 저가 광전식 연기 감지를 곁다리로 추가 (시범/개념검증)
- 상태: 설계 확정, 구현 계획 대기
1. 목표와 비목표
목표
- 저가 광전식 연기 감지기의 감지 이벤트 1건을 기존 PHP 서버까지 도달시켜 SMS 알림 발송.
- 기존 노드 구조(엣지검출 + confirm-read 디바운스 + HMAC 보고 + 센서별 SMS 그룹) 재사용.
- 최대한 저가. 시범(개념검증)이므로 인증·감시·로컬사이렌은 범위 밖.
비목표 (이번 시범에서 제외)
- 로컬 알람/사이렌 (필요 없음 — 감지 신호 도달만).
- 감시(FAULT)/단선검출 2선 루프, EOL 전원감시릴레이.
- KFI/UL 인증, 법정 자동화재탐지설비 대체.
- FAULT event_type, 정비 SMS 그룹.
인증 경계 (문서/대시보드 배너 필수): 이 시범 노드는 보조 통보용이며 법정 소방 화재경보설비를 대체하지 않는다. 개조된 저가 감지기는 인증품이 아니다.
2. 하드웨어 (개조 시범 경로 — 어댑터 불필요)
확정 근거: 사용자가 12~24V 어댑터 없이 가는 개조를 명시 선택. 단독형 배터리 알람이라 외부 전원 0. 대가는 납땜 + 멀티미터 프로빙(§ 트레이드오프).
BOM (1노드 ≈ ₩5,000, 외부 전원 없음)
| 부품 | 사양 | 가격 |
|---|---|---|
| 저가 광전식 단독형 연기알람 (AliExpress) | DC3V 배터리, 예비 포함 2개(1개 실습용). PGST 등 | ₩1,507~4,000/개 |
| PC817 옵토커플러 | 10팩 | ~₩1,000 |
| 저항 (1kΩ, 10kΩ) | 키트 | ~₩1,000 |
| 점퍼선 F-F + 미니 브레드보드 | 납땜 최소화용 | ~₩2,000 |
| (피에조 탭 시만) 1N4148 + 1~10µF | ~₩100 |
필요 공구(부품 아님, 개조 전제): 멀티미터(DC전압) + 인두기+납. 없으면 개조가 릴레이 경로보다 오히려 번거로움.
탭 지점 (감지 시 전압이 스위칭하는 노드) — 멀티미터 DC로 확정
평상시 vs 테스트버튼/연기 상태 비교:
- 알람 LED 애노드 (추천) — 0V → ~2V 안정 DC.
- 연동단자(연동형 모델만) — 0 → ~9VDC 안정.
- 피에조 구동선 — 0 → 9~12Vpp 교류 (정류 필요, 채터링 → 비추천).
Pi GPIO는 아날로그 저항을 못 읽는다(ADC 없음). 감지는 노드의 전압 레벨 변화를 디지털 HIGH/LOW로 읽는다.
배선 (PC817 절연, 감지=HIGH로 누수와 극성 통일)
[감지기측] LED노드(+) → 1kΩ → PC817 1핀(LED anode), PC817 2핀 → 감지기 GND
[Pi측] PC817 4핀(collector) → 3.3V, PC817 3핀(emitter) → GPIO27(BCM, 물리 pin13) + 10kΩ 풀다운→GND
동작: 연기 → LED 점등 → 옵토 도통 → GPIO27 = HIGH (감지, active-high)
- 옵토를 하이사이드(collector→3.3V, emitter→GPIO+풀다운)로 배선 → 감지=HIGH → 누수센서(ACTIVE_LOW=False)와 동일 극성 → 극성 코드 무변경.
- LED 노드 전압 V일 때 직렬저항 = (V − 1.2) / 2mA. ~2V면 470Ω, 애매하면 1kΩ.
- PC817 이유: 노드가 3.3V 초과/교류여도 안전 + 프로빙 실수로부터 Pi 보호 + 접지 공유 불필요.
GPIO 배정
| 신호 | Pi 핀 | 풀 | idle | 감지 |
|---|---|---|---|---|
| SMOKE | GPIO27 (물리 pin13) | 외부 10kΩ 풀다운→GND (또는 per-sensor PUD_DOWN) | LOW | HIGH(옵토 도통) |
- 기존: GPIO17=누수센서1, I2C(GPIO2/3)=SHT30, CSI=카메라. GPIO27은 config의 주석 처리된 여유 슬롯.
- 극성: 감지=HIGH →
ACTIVE_LOW=False(누수와 동일) 그대로. 풀다운만 필요(옵토 off 시 LOW 확정) → §8 미결 A/B. - 개조의 안전 한계: 배터리 죽으면 Pi가 모름(무음 실패). 단선도 무음. 시범이라 감수 — 실배포 승격 시 릴레이+NC로 교체.
fallback (어댑터 확보되면 권장 승격)
4선 광전 릴레이출력 감지기(₩4,250) + 12V 어댑터 → 무전압 접점 직결(납땜·옵토·멀티미터 불필요), NC=단선 페일세이프, 광전 확정, 부저 없음. 작업량은 개조보다 적으나 어댑터 필요.
3. 소프트웨어 — Pi 노드 (거의 신규 코드 없음)
접근: 기존 emitter 재사용
연기 채널을 SENSORS 목록의 한 항목으로 추가. 기존 엣지검출 + confirm-read 디바운스 + _post_event HMAC 파이프라인이 그대로 처리.
변경점
raspberry_pi4/config_local.py(배포 생성물, 소스는 make_deploy_bundles.py):SENSORS += [{"id": 4, "gpio": 27, "name": "연기(광전식)"}]sensor_id = 4(사용중 1/2/3와 겹치지 않음 → 누수 status 덮어쓰기 방지 + 독립 SMS 그룹).
- 극성: 옵토 하이사이드 배선이라 idle=LOW(옵토 off, 풀다운), 감지=HIGH(옵토 도통).
_read_is_leak관점에서 "감지=HIGH"이므로 전역ACTIVE_LOW=False(누수와 동일)를 그대로 쓴다 → 극성 코드 변경 불필요. 단 옵토 off 시 LOW를 확정하려면 GPIO27에 풀다운이 필요(전역은 PUD_OFF):- 옵션 A (코드변경 0): 외부 10kΩ 풀다운 저항을 GPIO27→GND에 물림. config 무변경.
- 옵션 B (권장, 부품 0):
setup_gpio를 per-sensorpull키 지원으로 소폭 승격해 GPIO27만 내부 PUD_DOWN 적용.ACTIVE_LOW는 건드리지 않음.
구현 계획에서 A/B 확정. 부품 안 늘리려면 B, 코드 안 건드리려면 A.
- 이벤트 방출: 감지 확정 시
smoke_alarm, 해제 시smoke_recoveryevent_type로 POST. 기존_post_event/post_to_server그대로(event_type 문자열만 추가). - 디바운스: 기존
CONFIRM_READS/CONFIRM_INTERVAL_SEC재사용. (시범 단계에선 별도 비대칭 디바운스 미도입.) - 카메라: 기본 OFF(저가·단순).
smoke_alarm에 캡처를 붙이려면 기존_handle_leak_with_photo재사용 가능(log_id 키만 맞으면 무관) — 옵션.
페이로드 (additive, 서명 바이트 순서 고정)
누수 body 재사용 + event_type만 smoke. 서버가 is_leak를 event_type로 강제하므로 is_leak는 그대로 두고 event_type로 구분.
{"device_id":"…","device_location":"…","sensor_id":4,"sensor_name":"연기(광전식)","event_type":"smoke_alarm","is_leak":0,"timestamp":<int>}
4. 소프트웨어 — 서버 (PHP, additive만)
필수:
sensor_data.php:208화이트리스트 +schema_sht30.sql:30ENUM이 모르는 event_type을 조용히periodic으로 강등(로그·SMS 없음). 아래 additive 변경 없으면 감지가 HTTP 200만 뜨고 사라진다.
php/api/sensor_data.php:208— event_type 화이트리스트에'smoke_alarm','smoke_recovery'추가.php/api/sensor_data.php:212-218— is_leak 강제 블록에 smoke 분기 추가(연기 상태 별도 플래그가 필요하면 additive 컬럼, 아니면 event_type만으로 로깅).is_leak·누수 로직 불변.sql/schema_sht30.sql:30—leak_sensor_log.event_typeENUM에'smoke_alarm','smoke_recovery'추가 + 기존 운영 DB용 ALTER 마이그레이션(migration_leak_smoke.sql, MySQL8 비멱등 주의).php/config.php:144-157—SMS_TAG_SMOKE_ALARM = '[화재감지]',SMS_TAG_SMOKE_RECOVERY = '[화재복구]'추가 +sms_summary_select_sql()(175-187)에 SUM(message LIKE …) 라인 추가.php/sms_send.php—send_smoke_alert_sms()/send_smoke_recovery_sms()(leak 함수 미러, 쿨다운 ~300s).sms_dispatch_to_recipients()가 sensor_id로 자동 라우팅 → id4 그룹.sensor_data.php의 leak 디스패치 분기(480-496) 근처에 smoke 분기 추가(즉시 발송; Pi가 디바운스하므로 서버 디바운스 미적용, SMS 재발신만 쿨다운).php/config.php—SENSOR_LABELS[4], (선택)SMOKE_SENSOR_IDS = [4](대시보드 카드용).config.local.php에SENSOR_RECIPIENTS[4] => [화재 수신자…].- 대시보드(선택):
status_summary.php/dashboard_data.php에 연기 카드 additive. 미적용 시 기존 per-sensor 행에 편승.
최소 대안 (서버 코드 변경 0)
sensor_id 4를 누수 센서로 취급(LEAK_SENSOR_IDS += 4) → 감지가 leak_alert 파이프라인으로 흐름. 단 SMS가 "[누수감지]"로 떠 화재를 오인 → display-vs-policy 위반. 채택 안 함(시범이라도 문구 구분 유지).
5. 상태 모델 (시범 = 2상태)
CLEAR ──(연기 확정: confirm-read 통과)──▶ ALARM → smoke_alarm POST
ALARM ──(연속 clear 확정)──────────────▶ CLEAR → smoke_recovery POST
- FAULT/감시 없음(시범). 실배포 승격 시 릴레이+NC 또는 2선 감시로 CLEAR/ALARM/FAULT 확장.
- 보고 주기: CLEAR heartbeat = 기존
REPORT_INTERVAL(600s), ALARM 재보고 =LEAK_REPORT_INTERVAL(10s)재사용, SMS 재발신은 서버 쿨다운.
6. 배포 (기존 절차 재사용)
- 소스 편집:
tools/make_deploy_bundles.pyPI_META의 Pi-C sensors에{"id":4,"gpio":27,"name":"연기(광전식)"}추가; 서버측SENSOR_LABELS[4], (선택)SMOKE_SENSOR_IDS. - 서버 PHP additive 변경(3~7항) +
config.local.php에SENSOR_RECIPIENTS[4]. python tools/make_deploy_bundles.py→ API_KEY prefix 불변 확인(.secrets 재사용).- 서버:
config.local.php업로드 + ENUM 마이그레이션(migration_leak_smoke.sql) 실행. - Pi: 번들 scp →
sudo systemctl restart leak-sensor(연기 채널은 기존 leak-sensor 서비스에 편입 — 별도 서비스 불필요).- harden_pi.sh로 SSH 잠긴 노드는 물리접근 또는 USB 마이그레이션 번들.
- 에어갭: ADC·신규 스택 없음 → 추가 벤더링 불필요(기존 rpi-lgpio/requests 그대로).
7. 테스트 (TDD 대상)
- 순수함수: smoke event_type body 빌더 바이트 동일성 + HMAC 서명 검증.
- 서버:
smoke_alarm수용(화이트리스트), ENUM insert 성공,[화재감지]태그 카운트, SMS 라우팅(id4 그룹). - 통합: 테스트버튼 눌러 실제 GPIO27 LOW → confirm-read 통과 →
smoke_alarmPOST → 서버 로그 + SMS 발송 엔드투엔드 확인(HTTP 200만으로 판정 금지 — SMS 실제 발송까지). - 회귀: 기존 누수/온습도 이벤트 불변 확인(
is_leak·leak_sensor_status 미오염).
8. 미해결/구현계획에서 결정
- GPIO27 풀다운 방식 A(외부 10kΩ 저항) vs B(per-sensor 내부 PUD_DOWN 키) 확정.
- 카메라 캡처를 smoke_alarm에 붙일지(옵션, 기본 OFF).
- Ali 단독형 알람 실물 확인: 광전식 여부, 알람 LED 노드 전압(멀티미터 DC, 평상시 vs 테스트버튼), 탭 지점 확정.
- 저배터리 무음 실패 완화책(주기 교체 일정 or 저배터리 칩 패턴 감지) 필요 여부.
9. 승격 경로 (시범 → 실배포)
| 항목 | 시범(현재) | 실배포 승격 |
|---|---|---|
| 감지기 | Ali 저가 단독형 개조(₩1,507~) | Ali 4선 릴레이출력(₩4,250) → 누멘스 SNC-300-SR(KFI/UL 인증) |
| 전원 | 배터리(어댑터 불필요) | 12~24V 어댑터/DIN 전원 |
| 배선 | LED 탭 + PC817 옵토 | 무전압 접점 직결(옵토 불필요), NC=페일세이프 |
| 감시 | 없음(배터리死·단선 무음) | NC=단선검출 → MY2N EOL 릴레이(정식 FAULT) |
| 소프트웨어 | 동일 (SENSORS +1, 서버 additive) | 동일 + (선택)FAULT event_type |
소프트웨어가 하드웨어와 무관하게 동일하므로, 시범→실배포는 하드웨어 교체(개조→릴레이→인증) + (선택)FAULT 추가로 끝난다. Pi/서버 코드 구조는 그대로.