# ============================================================================= # 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- 가 없으면(예: 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