2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증. 1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp. 서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약. DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마. 보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS. 문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
# =============================================================================
|
|
# sht30_driver.py - SHT30 I2C 측정 드라이버 (Raspberry Pi 4, I2C 버스 1)
|
|
#
|
|
# [이식 출처]
|
|
# firmware/common/sht30.c (0x2C06 명령 -> ~20ms 대기 -> 6바이트 read)
|
|
# firmware/common/sht30_convert.c (CRC-8 검증 + 물리값 변환)
|
|
# firmware/test/host/reference.py (sht30_crc8 / sht30_parse 파이썬 레퍼런스)
|
|
#
|
|
# [동작]
|
|
# 1) 측정 명령 0x2C 0x06 전송 (고반복성, clock stretching 비활성)
|
|
# 2) ~20ms 대기 (고반복성 측정 최대 15ms + 마진)
|
|
# 3) 6바이트 read: T_msb,T_lsb,T_crc, RH_msb,RH_lsb,RH_crc
|
|
# 4) CRC-8(poly 0x31, init 0xFF) 검증 후 변환:
|
|
# temp_c = -45 + 175 * raw_t / 65535
|
|
# rh = 100 * raw_rh / 65535
|
|
#
|
|
# [I2C 접근] smbus2(순수 파이썬, arch 무관 wheel) 의 i2c_msg 로 write/read 를
|
|
# 명시적으로 분리 전송한다. read_i2c_block_data 는 레지스터 바이트를 먼저
|
|
# 쓰므로 SHT30 측정 프레임을 깨뜨린다 → i2c_msg.read 사용.
|
|
#
|
|
# [시뮬레이션] smbus2 미설치 또는 /dev/i2c-* 부재(개발 PC)면 SIMULATION=True 로
|
|
# 전환되어 그럴듯한 더미 측정값을 돌려준다(서버 연동/포맷 점검용).
|
|
# =============================================================================
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
# ── 상수 (firmware/common/sht30.c, app_config.h 와 동일) ─────────────────────
|
|
SHT30_CMD = [0x2C, 0x06] # high-repeatability, clock stretching off
|
|
MEAS_DELAY_SEC = 0.02 # 측정 완료 대기 (15ms + 마진)
|
|
FRAME_LEN = 6
|
|
|
|
try:
|
|
from smbus2 import SMBus, i2c_msg
|
|
SMBUS_AVAILABLE = True
|
|
except ImportError:
|
|
SMBUS_AVAILABLE = False
|
|
|
|
|
|
# ── CRC-8 (sht30_convert.c::sht30_crc8 와 동일) ──────────────────────────────
|
|
def crc8(data: bytes) -> int:
|
|
"""SHT3x CRC-8: poly 0x31, init 0xFF, no reflection, no final xor."""
|
|
crc = 0xFF
|
|
for b in data:
|
|
crc ^= b
|
|
for _ in range(8):
|
|
crc = ((crc << 1) ^ 0x31) & 0xFF if (crc & 0x80) else (crc << 1) & 0xFF
|
|
return crc
|
|
|
|
|
|
# ── 프레임 파싱 (sht30_convert.c::sht30_parse 와 동일) ───────────────────────
|
|
def parse_frame(frame: bytes):
|
|
"""6바이트 측정 프레임 검증 + 변환 → (temp_c, rh).
|
|
|
|
CRC 불일치 시 ValueError 를 던진다(호출측이 재시도/스킵).
|
|
"""
|
|
if len(frame) != FRAME_LEN:
|
|
raise ValueError(f"프레임 길이 오류: {len(frame)} (기대 {FRAME_LEN})")
|
|
if crc8(frame[0:2]) != frame[2]:
|
|
raise ValueError("온도 CRC 불일치")
|
|
if crc8(frame[3:5]) != frame[5]:
|
|
raise ValueError("습도 CRC 불일치")
|
|
raw_t = (frame[0] << 8) | frame[1]
|
|
raw_rh = (frame[3] << 8) | frame[4]
|
|
temp_c = -45.0 + 175.0 * (raw_t / 65535.0)
|
|
rh = 100.0 * (raw_rh / 65535.0)
|
|
return temp_c, rh
|
|
|
|
|
|
class SHT30:
|
|
"""SHT30 단발 측정 드라이버.
|
|
|
|
bus_num: I2C 버스 번호 (Pi 4 40핀 헤더 = 1)
|
|
addr: 7-bit I2C 주소 (ADDR=GND 면 0x44, ADDR=VDD 면 0x45)
|
|
simulate: True 면 하드웨어 없이 더미값 반환(강제 시뮬레이션)
|
|
"""
|
|
|
|
def __init__(self, bus_num: int = 1, addr: int = 0x44, simulate: bool = False):
|
|
self.bus_num = bus_num
|
|
self.addr = addr
|
|
self.simulate = simulate or not SMBUS_AVAILABLE
|
|
self._bus = None
|
|
self._sim_tick = 0
|
|
|
|
def open(self):
|
|
if self.simulate:
|
|
return
|
|
# /dev/i2c-<bus_num> 가 없으면(예: i2c 미활성) 명확한 오류로 안내한다.
|
|
try:
|
|
self._bus = SMBus(self.bus_num)
|
|
except (FileNotFoundError, PermissionError, OSError) as e:
|
|
raise RuntimeError(
|
|
f"I2C 버스 {self.bus_num} 열기 실패: {e}. "
|
|
f"`sudo raspi-config` 로 I2C 활성화 및 사용자 i2c 그룹 확인."
|
|
) from e
|
|
|
|
def close(self):
|
|
if self._bus is not None:
|
|
try:
|
|
self._bus.close()
|
|
finally:
|
|
self._bus = None
|
|
|
|
def measure(self):
|
|
"""단발 측정 → (temp_c, rh). 실패 시 예외(통신/CRC).
|
|
|
|
시뮬레이션 모드에서는 24~26℃, 48~54% 부근의 더미값을 반환한다.
|
|
"""
|
|
if self.simulate:
|
|
self._sim_tick += 1
|
|
temp = 24.0 + (self._sim_tick % 5) * 0.5 # 24.0~26.0
|
|
rh = 48.0 + (self._sim_tick % 7) * 1.0 # 48.0~54.0
|
|
return temp, rh
|
|
|
|
if self._bus is None:
|
|
self.open()
|
|
|
|
# 1) 측정 명령 전송 (write)
|
|
write = i2c_msg.write(self.addr, SHT30_CMD)
|
|
self._bus.i2c_rdwr(write)
|
|
|
|
# 2) 측정 완료 대기 (clock stretching 비활성 → 호스트가 명시적 대기)
|
|
time.sleep(MEAS_DELAY_SEC)
|
|
|
|
# 3) 6바이트 read
|
|
read = i2c_msg.read(self.addr, FRAME_LEN)
|
|
self._bus.i2c_rdwr(read)
|
|
frame = bytes(read)
|
|
|
|
# 4) CRC 검증 + 변환
|
|
return parse_frame(frame)
|
|
|
|
def __enter__(self):
|
|
self.open()
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self.close()
|
|
return False
|