라즈베리파이 SHT30 드라이버 측정 명령을 0x2C06(clock stretching ENABLED, 주석은 "비활성"이라 모순)에서 0x2400(clock stretching DISABLED)으로 교정. 드라이버의 write→대기→read 접근 패턴 및 Pi BCM I2C 클럭스트레치 버그와 정합하며, 벤더 예제 ArtronShop_SHT3x(P4422-2)의 무-스트레치 방식과 일치. CRC-8 검증은 유지(벤더 예제는 생략). 변환식·주소(0x44)·프레임 동일. STM32 펌웨어는 0x2C06 유지(HAL 스트레치 지원). - P4422-2/: 벤더 SHT3x 아두이노 예제(ArtronShop_SHT3x) 벤더링 - 서명/서버 프로토콜 무영향. 현장 하드웨어 실측 검증 필요(시뮬은 명령 무관)
146 lines
5.9 KiB
Python
146 lines
5.9 KiB
Python
# =============================================================================
|
|
# sht30_driver.py - SHT30 I2C measurement driver (Raspberry Pi 4, I2C bus 1)
|
|
#
|
|
# [Ported from]
|
|
# firmware/common/sht30.c (STM32=0x2C06; Pi uses 0x2400 -> ~20ms wait -> 6-byte read)
|
|
# * The Pi uses the clock-stretch-disabled command (0x2400) — Pi/Arduino I2C masters
|
|
# handle clock stretching poorly (Pi BCM I2C stretch bug). The vendor example (P4422-2
|
|
# ArtronShop_SHT3x) also avoids stretching for the same reason (periodic + fetch). STM32
|
|
# (HAL supports stretching) keeps 0x2C06. Conversion/CRC are identical.
|
|
# firmware/common/sht30_convert.c (CRC-8 verification + physical-value conversion)
|
|
# firmware/test/host/reference.py (sht30_crc8 / sht30_parse Python reference)
|
|
#
|
|
# [Operation]
|
|
# 1) Send measurement command 0x24 0x00 (high-repeatability, clock stretching disabled)
|
|
# 2) Wait ~20ms (high-repeatability measurement max 15ms + margin)
|
|
# 3) 6-byte read: T_msb,T_lsb,T_crc, RH_msb,RH_lsb,RH_crc
|
|
# 4) CRC-8 (poly 0x31, init 0xFF) verification then conversion:
|
|
# temp_c = -45 + 175 * raw_t / 65535
|
|
# rh = 100 * raw_rh / 65535
|
|
#
|
|
# [I2C access] Uses smbus2's (pure Python, arch-agnostic wheel) i2c_msg to send write/read
|
|
# as explicitly separate transfers. read_i2c_block_data writes a register byte
|
|
# first, so it breaks the SHT30 measurement frame -> use i2c_msg.read.
|
|
#
|
|
# [Simulation] If smbus2 is not installed or /dev/i2c-* is absent (dev PC), it switches to
|
|
# SIMULATION=True and returns plausible dummy measurements (for server integration/format checks).
|
|
# =============================================================================
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
# ── Constants ────────────────────────────────────────────────────────────────
|
|
# 0x2400 = single-shot, high-repeatability, clock stretching DISABLED (Sensirion SHT3x datasheet).
|
|
# (0x2C06 = stretch ENABLED — for STM32 firmware. The Pi uses a write->wait->read pattern, so
|
|
# the stretch-disabled command is accurate and safe. The vendor example ArtronShop_SHT3x also avoids stretching.)
|
|
SHT30_CMD = [0x24, 0x00] # high-repeatability, clock stretching disabled (Pi-safe)
|
|
MEAS_DELAY_SEC = 0.02 # wait for measurement completion (15ms + margin)
|
|
FRAME_LEN = 6
|
|
|
|
try:
|
|
from smbus2 import SMBus, i2c_msg
|
|
SMBUS_AVAILABLE = True
|
|
except ImportError:
|
|
SMBUS_AVAILABLE = False
|
|
|
|
|
|
# ── CRC-8 (identical to 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
|
|
|
|
|
|
# ── Frame parsing (identical to sht30_convert.c::sht30_parse) ────────────────
|
|
def parse_frame(frame: bytes):
|
|
"""Verify + convert the 6-byte measurement frame -> (temp_c, rh).
|
|
|
|
Raises ValueError on CRC mismatch (caller retries/skips).
|
|
"""
|
|
if len(frame) != FRAME_LEN:
|
|
raise ValueError(f"frame length error: {len(frame)} (expected {FRAME_LEN})")
|
|
if crc8(frame[0:2]) != frame[2]:
|
|
raise ValueError("temperature CRC mismatch")
|
|
if crc8(frame[3:5]) != frame[5]:
|
|
raise ValueError("humidity CRC mismatch")
|
|
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 single-shot measurement driver.
|
|
|
|
bus_num: I2C bus number (Pi 4 40-pin header = 1)
|
|
addr: 7-bit I2C address (0x44 if ADDR=GND, 0x45 if ADDR=VDD)
|
|
simulate: if True, returns dummy values without hardware (forced simulation)
|
|
"""
|
|
|
|
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
|
|
# If /dev/i2c-<bus_num> is missing (e.g. I2C disabled), report a clear error.
|
|
try:
|
|
self._bus = SMBus(self.bus_num)
|
|
except (FileNotFoundError, PermissionError, OSError) as e:
|
|
raise RuntimeError(
|
|
f"Failed to open I2C bus {self.bus_num}: {e}. "
|
|
f"Enable I2C via `sudo raspi-config` and verify the user's i2c group."
|
|
) from e
|
|
|
|
def close(self):
|
|
if self._bus is not None:
|
|
try:
|
|
self._bus.close()
|
|
finally:
|
|
self._bus = None
|
|
|
|
def measure(self):
|
|
"""Single-shot measurement -> (temp_c, rh). Raises on failure (comms/CRC).
|
|
|
|
In simulation mode, returns dummy values around 24-26 C and 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) Send measurement command (write)
|
|
write = i2c_msg.write(self.addr, SHT30_CMD)
|
|
self._bus.i2c_rdwr(write)
|
|
|
|
# 2) Wait for measurement completion (clock stretching disabled -> host waits explicitly)
|
|
time.sleep(MEAS_DELAY_SEC)
|
|
|
|
# 3) 6-byte read
|
|
read = i2c_msg.read(self.addr, FRAME_LEN)
|
|
self._bus.i2c_rdwr(read)
|
|
frame = bytes(read)
|
|
|
|
# 4) CRC verification + conversion
|
|
return parse_frame(frame)
|
|
|
|
def __enter__(self):
|
|
self.open()
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self.close()
|
|
return False
|