> **⚠️ SUPERSEDED 2026-07-19:** 화재/연기 감지는 PMS7003 미세먼지 metric 통합으로 대체됨. > 개조/불꽃/CO 경로 폐기. 아래는 이력 보존용. > 대체 설계: [`2026-07-19-node-sensor-integration-design.md`](./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):** ```python 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로 구분. ```json {"device_id":"…","device_location":"…","sensor_id":4,"sensor_name":"연기(광전식)","event_type":"smoke_alarm","is_leak":0,"timestamp":} ``` --- ## 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/서버 코드 구조는 그대로.