feat(sht30): 벤더 예제(P4422-2) 기반 단발 측정 명령 0x2400 전환

라즈베리파이 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) 벤더링
- 서명/서버 프로토콜 무영향. 현장 하드웨어 실측 검증 필요(시뮬은 명령 무관)
This commit is contained in:
cw2007 2026-07-10 10:42:10 +09:00
parent 79af8ef530
commit 9f0e048e49
8 changed files with 254 additions and 38 deletions

View file

@ -0,0 +1,38 @@
# ArtronShop_SHT3x
Arduino library of SHT30, SHT31, SHT35 Digital humidity and temperature sensor
## Examples
```C++
#include <Arduino.h>
#include <Wire.h>
#include <ArtronShop_SHT3x.h>
ArtronShop_SHT3x sht3x(0x44, &Wire); // ADDR: 0 => 0x44, ADDR: 1 => 0x45
void setup() {
Serial.begin(115200);
Wire.begin();
while (!sht3x.begin()) {
Serial.println("SHT3x not found !");
delay(1000);
}
}
void loop() {
if (sht3x.measure()) {
Serial.print("Temperature: ");
Serial.print(sht3x.temperature(), 1);
Serial.print(" *C\tHumidity: ");
Serial.print(sht3x.humidity(), 1);
Serial.print(" %RH");
Serial.println();
} else {
Serial.println("SHT3x read error");
}
delay(1000);
}
```

View file

@ -0,0 +1,29 @@
#include <Arduino.h>
#include <Wire.h>
#include <ArtronShop_SHT3x.h>
ArtronShop_SHT3x sht3x(0x44, &Wire); // ADDR: 0 => 0x44, ADDR: 1 => 0x45
void setup() {
Serial.begin(115200);
Wire.begin();
while (!sht3x.begin()) {
Serial.println("SHT3x not found !");
delay(1000);
}
}
void loop() {
if (sht3x.measure()) {
Serial.print("Temperature: ");
Serial.print(sht3x.temperature(), 1);
Serial.print(" *C\tHumidity: ");
Serial.print(sht3x.humidity(), 1);
Serial.print(" %RH");
Serial.println();
} else {
Serial.println("SHT3x read error");
}
delay(1000);
}

View file

@ -0,0 +1,22 @@
#######################################
# Syntax Coloring Map For
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
ArtronShop_SHT3x KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
measure KEYWORD2
temperature KEYWORD2
humidity KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################

View file

@ -0,0 +1,9 @@
name=ArtronShop_SHT3x
version=1.0.0
author=ArtronShop Co.,Ltd.
maintainer=Sonthaya.NT <sonthaya.nt@artronshop.co.th>
sentence=Arduino library of SHT3x Digital humidity and temperature sensor
paragraph=Arduino library of SHT3x Digital humidity and temperature sensor
category=Sensors
url=https://github.com/ArtronShop/ArtronShop_SHT3x
architectures=*

View file

@ -0,0 +1,57 @@
#include "ArtronShop_SHT3x.h"
ArtronShop_SHT3x::ArtronShop_SHT3x(uint8_t addr, TwoWire *wire) : _addr(addr), _wire(wire) {
// -----
}
bool ArtronShop_SHT3x::begin() {
// periodic data acquisition mode
this->_wire->beginTransmission(this->_addr);
this->_wire->write(0x27); // mps => 10
this->_wire->write(0x37); // Repeatability => High
return this->_wire->endTransmission() == 0;
}
bool ArtronShop_SHT3x::measure() {
this->_wire->beginTransmission(this->_addr);
this->_wire->write(0xE0);
this->_wire->write(0x00);
if (this->_wire->endTransmission(false) != 0) {
this->_t = -99;
this->_h = -99;
return false;
}
if (this->_wire->requestFrom(this->_addr, (uint8_t)6) != 6) {
this->_t = -99;
this->_h = -99;
return false;
}
uint8_t rx_bytes[6];
memset(rx_bytes, 0, 6);
this->_wire->readBytes(rx_bytes, 6);
uint16_t t_ticks = ((uint16_t)rx_bytes[0] << 8) | rx_bytes[1];
// checksum_t = rx_bytes[2];
uint16_t rh_ticks = ((uint16_t)rx_bytes[3] << 8) | rx_bytes[4];
// checksum_rh = rx_bytes[5];
this->_t = -45.0f + (175.0f * (t_ticks / 65535.0f));
this->_h = 100 * (rh_ticks / 65535.0f);
if (this->_h > 100) {
this->_h = 100;
}
if (this->_h < 0) {
this->_h = 0;
}
return true;
}
float ArtronShop_SHT3x::temperature() {
return this->_t;
}
float ArtronShop_SHT3x::humidity() {
return this->_h;
}

View file

@ -0,0 +1,25 @@
#ifndef __ARTRONSHOP_SHT3X_H__
#define __ARTRONSHOP_SHT3X_H__
#include <Arduino.h>
#include <Wire.h>
#define SHT3X_ADDR0 (0x44)
#define SHT3X_ADDR1 (0x45)
class ArtronShop_SHT3x {
private:
uint8_t _addr;
TwoWire *_wire = NULL;
float _t, _h;
public:
ArtronShop_SHT3x(uint8_t addr = SHT3X_ADDR0, TwoWire *wire = &Wire) ;
bool begin() ;
bool measure() ;
float temperature() ;
float humidity() ;
};
#endif

View file

@ -0,0 +1,29 @@
#include <Arduino.h>
#include <Wire.h>
#include <ArtronShop_SHT3x.h>
ArtronShop_SHT3x sht3x(0x44, &Wire); // ADDR: 0 => 0x44, ADDR: 1 => 0x45
void setup() {
Serial.begin(115200);
Wire.begin();
while (!sht3x.begin()) {
Serial.println("SHT3x not found !");
delay(1000);
}
}
void loop() {
if (sht3x.measure()) {
Serial.print("Temperature: ");
Serial.print(sht3x.temperature(), 1);
Serial.print(" *C\tHumidity: ");
Serial.print(sht3x.humidity(), 1);
Serial.print(" %RH");
Serial.println();
} else {
Serial.println("SHT3x read error");
}
delay(1000);
}

View file

@ -1,33 +1,40 @@
# ============================================================================= # =============================================================================
# sht30_driver.py - SHT30 I2C 측정 드라이버 (Raspberry Pi 4, I2C 버스 1) # sht30_driver.py - SHT30 I2C measurement driver (Raspberry Pi 4, I2C bus 1)
# #
# [이식 출처] # [Ported from]
# firmware/common/sht30.c (0x2C06 명령 -> ~20ms 대기 -> 6바이트 read) # firmware/common/sht30.c (STM32=0x2C06; Pi uses 0x2400 -> ~20ms wait -> 6-byte read)
# firmware/common/sht30_convert.c (CRC-8 검증 + 물리값 변환) # * The Pi uses the clock-stretch-disabled command (0x2400) — Pi/Arduino I2C masters
# firmware/test/host/reference.py (sht30_crc8 / sht30_parse 파이썬 레퍼런스) # 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) 측정 명령 0x2C 0x06 전송 (고반복성, clock stretching 비활성) # 1) Send measurement command 0x24 0x00 (high-repeatability, clock stretching disabled)
# 2) ~20ms 대기 (고반복성 측정 최대 15ms + 마진) # 2) Wait ~20ms (high-repeatability measurement max 15ms + margin)
# 3) 6바이트 read: T_msb,T_lsb,T_crc, RH_msb,RH_lsb,RH_crc # 3) 6-byte read: T_msb,T_lsb,T_crc, RH_msb,RH_lsb,RH_crc
# 4) CRC-8(poly 0x31, init 0xFF) 검증 후 변환: # 4) CRC-8 (poly 0x31, init 0xFF) verification then conversion:
# temp_c = -45 + 175 * raw_t / 65535 # temp_c = -45 + 175 * raw_t / 65535
# rh = 100 * raw_rh / 65535 # rh = 100 * raw_rh / 65535
# #
# [I2C 접근] smbus2(순수 파이썬, arch 무관 wheel) 의 i2c_msg 로 write/read 를 # [I2C access] Uses smbus2's (pure Python, arch-agnostic wheel) i2c_msg to send write/read
# 명시적으로 분리 전송한다. read_i2c_block_data 는 레지스터 바이트를 먼저 # as explicitly separate transfers. read_i2c_block_data writes a register byte
# 쓰므로 SHT30 측정 프레임을 깨뜨린다 → i2c_msg.read 사용. # first, so it breaks the SHT30 measurement frame -> use i2c_msg.read.
# #
# [시뮬레이션] smbus2 미설치 또는 /dev/i2c-* 부재(개발 PC)면 SIMULATION=True 로 # [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 from __future__ import annotations
import time import time
# ── 상수 (firmware/common/sht30.c, app_config.h 와 동일) ───────────────────── # ── Constants ────────────────────────────────────────────────────────────────
SHT30_CMD = [0x2C, 0x06] # high-repeatability, clock stretching off # 0x2400 = single-shot, high-repeatability, clock stretching DISABLED (Sensirion SHT3x datasheet).
MEAS_DELAY_SEC = 0.02 # 측정 완료 대기 (15ms + 마진) # (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 FRAME_LEN = 6
try: try:
@ -37,7 +44,7 @@ except ImportError:
SMBUS_AVAILABLE = False SMBUS_AVAILABLE = False
# ── CRC-8 (sht30_convert.c::sht30_crc8 와 동일) ────────────────────────────── # ── CRC-8 (identical to sht30_convert.c::sht30_crc8) ─────────────────────────
def crc8(data: bytes) -> int: def crc8(data: bytes) -> int:
"""SHT3x CRC-8: poly 0x31, init 0xFF, no reflection, no final xor.""" """SHT3x CRC-8: poly 0x31, init 0xFF, no reflection, no final xor."""
crc = 0xFF crc = 0xFF
@ -48,18 +55,18 @@ def crc8(data: bytes) -> int:
return crc return crc
# ── 프레임 파싱 (sht30_convert.c::sht30_parse 와 동일) ─────────────────────── # ── Frame parsing (identical to sht30_convert.c::sht30_parse) ────────────────
def parse_frame(frame: bytes): def parse_frame(frame: bytes):
"""6바이트 측정 프레임 검증 + 변환 → (temp_c, rh). """Verify + convert the 6-byte measurement frame -> (temp_c, rh).
CRC 불일치 시 ValueError 를 던진다(호출측이 재시도/스킵). Raises ValueError on CRC mismatch (caller retries/skips).
""" """
if len(frame) != FRAME_LEN: if len(frame) != FRAME_LEN:
raise ValueError(f"프레임 길이 오류: {len(frame)} (기대 {FRAME_LEN})") raise ValueError(f"frame length error: {len(frame)} (expected {FRAME_LEN})")
if crc8(frame[0:2]) != frame[2]: if crc8(frame[0:2]) != frame[2]:
raise ValueError("온도 CRC 불일치") raise ValueError("temperature CRC mismatch")
if crc8(frame[3:5]) != frame[5]: if crc8(frame[3:5]) != frame[5]:
raise ValueError("습도 CRC 불일치") raise ValueError("humidity CRC mismatch")
raw_t = (frame[0] << 8) | frame[1] raw_t = (frame[0] << 8) | frame[1]
raw_rh = (frame[3] << 8) | frame[4] raw_rh = (frame[3] << 8) | frame[4]
temp_c = -45.0 + 175.0 * (raw_t / 65535.0) temp_c = -45.0 + 175.0 * (raw_t / 65535.0)
@ -68,11 +75,11 @@ def parse_frame(frame: bytes):
class SHT30: class SHT30:
"""SHT30 단발 측정 드라이버. """SHT30 single-shot measurement driver.
bus_num: I2C 버스 번호 (Pi 4 40핀 헤더 = 1) bus_num: I2C bus number (Pi 4 40-pin header = 1)
addr: 7-bit I2C 주소 (ADDR=GND 면 0x44, ADDR=VDD 면 0x45) addr: 7-bit I2C address (0x44 if ADDR=GND, 0x45 if ADDR=VDD)
simulate: True 면 하드웨어 없이 더미값 반환(강제 시뮬레이션) simulate: if True, returns dummy values without hardware (forced simulation)
""" """
def __init__(self, bus_num: int = 1, addr: int = 0x44, simulate: bool = False): def __init__(self, bus_num: int = 1, addr: int = 0x44, simulate: bool = False):
@ -85,13 +92,13 @@ class SHT30:
def open(self): def open(self):
if self.simulate: if self.simulate:
return return
# /dev/i2c-<bus_num> 가 없으면(예: i2c 미활성) 명확한 오류로 안내한다. # If /dev/i2c-<bus_num> is missing (e.g. I2C disabled), report a clear error.
try: try:
self._bus = SMBus(self.bus_num) self._bus = SMBus(self.bus_num)
except (FileNotFoundError, PermissionError, OSError) as e: except (FileNotFoundError, PermissionError, OSError) as e:
raise RuntimeError( raise RuntimeError(
f"I2C 버스 {self.bus_num} 열기 실패: {e}. " f"Failed to open I2C bus {self.bus_num}: {e}. "
f"`sudo raspi-config` 로 I2C 활성화 및 사용자 i2c 그룹 확인." f"Enable I2C via `sudo raspi-config` and verify the user's i2c group."
) from e ) from e
def close(self): def close(self):
@ -102,9 +109,9 @@ class SHT30:
self._bus = None self._bus = None
def measure(self): def measure(self):
"""단발 측정 → (temp_c, rh). 실패 시 예외(통신/CRC). """Single-shot measurement -> (temp_c, rh). Raises on failure (comms/CRC).
시뮬레이션 모드에서는 24~26℃, 48~54% 부근의 더미값을 반환한다. In simulation mode, returns dummy values around 24-26 C and 48-54%.
""" """
if self.simulate: if self.simulate:
self._sim_tick += 1 self._sim_tick += 1
@ -115,19 +122,19 @@ class SHT30:
if self._bus is None: if self._bus is None:
self.open() self.open()
# 1) 측정 명령 전송 (write) # 1) Send measurement command (write)
write = i2c_msg.write(self.addr, SHT30_CMD) write = i2c_msg.write(self.addr, SHT30_CMD)
self._bus.i2c_rdwr(write) self._bus.i2c_rdwr(write)
# 2) 측정 완료 대기 (clock stretching 비활성 → 호스트가 명시적 대기) # 2) Wait for measurement completion (clock stretching disabled -> host waits explicitly)
time.sleep(MEAS_DELAY_SEC) time.sleep(MEAS_DELAY_SEC)
# 3) 6바이트 read # 3) 6-byte read
read = i2c_msg.read(self.addr, FRAME_LEN) read = i2c_msg.read(self.addr, FRAME_LEN)
self._bus.i2c_rdwr(read) self._bus.i2c_rdwr(read)
frame = bytes(read) frame = bytes(read)
# 4) CRC 검증 + 변환 # 4) CRC verification + conversion
return parse_frame(frame) return parse_frame(frame)
def __enter__(self): def __enter__(self):