POSA_LEAKSMS/docs/superpowers/specs/2026-07-18-smoke-node-pilot-design.md
cw2007 90b93f818c chore(deploy+docs): 노드 번들 생성기·하드닝 갱신 + 설계문서
- tools/make_deploy_bundles.py: PMS7003·Zero2W 디스플레이 노드 번들 생성 반영(폐쇄망 동봉)
- tools/harden_pi.sh: 하드닝 스크립트 갱신
- docs/superpowers: 노드 센서 통합·스모크 파일럿·대시보드 다크 설계/계획 문서
- docs/claude-config: IoT 납품·검증 스킬/룰 참조본
- review/: 프로젝트 리뷰·UI v2 목업
2026-07-19 10:48:09 +09:00

12 KiB
Raw Blame History

⚠️ 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 테스트버튼/연기 상태 비교:

  1. 알람 LED 애노드 (추천) — 0V → ~2V 안정 DC.
  2. 연동단자(연동형 모델만) — 0 → ~9VDC 안정.
  3. 피에조 구동선 — 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 파이프라인이 그대로 처리.

변경점

  1. raspberry_pi4/config_local.py (배포 생성물, 소스는 make_deploy_bundles.py):
    SENSORS += [{"id": 4, "gpio": 27, "name": "연기(광전식)"}]
    
    • sensor_id = 4 (사용중 1/2/3와 겹치지 않음 → 누수 status 덮어쓰기 방지 + 독립 SMS 그룹).
  2. 극성: 옵토 하이사이드 배선이라 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-sensor pull 키 지원으로 소폭 승격해 GPIO27만 내부 PUD_DOWN 적용. ACTIVE_LOW는 건드리지 않음.

    구현 계획에서 A/B 확정. 부품 안 늘리려면 B, 코드 안 건드리려면 A.

  3. 이벤트 방출: 감지 확정 시 smoke_alarm, 해제 시 smoke_recovery event_type로 POST. 기존 _post_event/post_to_server 그대로(event_type 문자열만 추가).
  4. 디바운스: 기존 CONFIRM_READS/CONFIRM_INTERVAL_SEC 재사용. (시범 단계에선 별도 비대칭 디바운스 미도입.)
  5. 카메라: 기본 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:30 ENUM이 모르는 event_type을 조용히 periodic으로 강등(로그·SMS 없음). 아래 additive 변경 없으면 감지가 HTTP 200만 뜨고 사라진다.

  1. php/api/sensor_data.php:208 — event_type 화이트리스트에 'smoke_alarm', 'smoke_recovery' 추가.
  2. php/api/sensor_data.php:212-218 — is_leak 강제 블록에 smoke 분기 추가(연기 상태 별도 플래그가 필요하면 additive 컬럼, 아니면 event_type만으로 로깅). is_leak·누수 로직 불변.
  3. sql/schema_sht30.sql:30 — leak_sensor_log.event_type ENUM에 'smoke_alarm','smoke_recovery' 추가 + 기존 운영 DB용 ALTER 마이그레이션(migration_leak_smoke.sql, MySQL8 비멱등 주의).
  4. php/config.php:144-157 — SMS_TAG_SMOKE_ALARM = '[화재감지]', SMS_TAG_SMOKE_RECOVERY = '[화재복구]' 추가 + sms_summary_select_sql()(175-187)에 SUM(message LIKE …) 라인 추가.
  5. 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 재발신만 쿨다운).
  6. php/config.php — SENSOR_LABELS[4], (선택) SMOKE_SENSOR_IDS = [4](대시보드 카드용). config.local.php에 SENSOR_RECIPIENTS[4] => [화재 수신자…].
  7. 대시보드(선택): 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. 배포 (기존 절차 재사용)

  1. 소스 편집: tools/make_deploy_bundles.py PI_META의 Pi-C sensors에 {"id":4,"gpio":27,"name":"연기(광전식)"} 추가; 서버측 SENSOR_LABELS[4], (선택) SMOKE_SENSOR_IDS.
  2. 서버 PHP additive 변경(3~7항) + config.local.php에 SENSOR_RECIPIENTS[4].
  3. python tools/make_deploy_bundles.py → API_KEY prefix 불변 확인(.secrets 재사용).
  4. 서버: config.local.php 업로드 + ENUM 마이그레이션(migration_leak_smoke.sql) 실행.
  5. Pi: 번들 scp → sudo systemctl restart leak-sensor (연기 채널은 기존 leak-sensor 서비스에 편입 — 별도 서비스 불필요).
    • harden_pi.sh로 SSH 잠긴 노드는 물리접근 또는 USB 마이그레이션 번들.
  6. 에어갭: ADC·신규 스택 없음 → 추가 벤더링 불필요(기존 rpi-lgpio/requests 그대로).

7. 테스트 (TDD 대상)

  • 순수함수: smoke event_type body 빌더 바이트 동일성 + HMAC 서명 검증.
  • 서버: smoke_alarm 수용(화이트리스트), ENUM insert 성공, [화재감지] 태그 카운트, SMS 라우팅(id4 그룹).
  • 통합: 테스트버튼 눌러 실제 GPIO27 LOW → confirm-read 통과 → smoke_alarm POST → 서버 로그 + 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/서버 코드 구조는 그대로.