fwt_software/tests/test_sht41.cpp

101 lines
3.8 KiB
C++

#include <doctest/doctest.h>
#include "fgc/mock/MockEnvSensor.h"
#include "fgc/sensors/Sht41Protocol.h"
using namespace fgc;
namespace {
// Build a measurement response with correct CRCs for both words.
std::vector<uint8_t> frame(uint8_t t_msb, uint8_t t_lsb, uint8_t h_msb, uint8_t h_lsb) {
std::vector<uint8_t> rx = {t_msb, t_lsb, 0, h_msb, h_lsb, 0};
rx[2] = sht41Crc8(rx.data(), 2);
rx[5] = sht41Crc8(rx.data() + 3, 2);
return rx;
}
} // namespace
TEST_CASE("sht41Crc8 matches the datasheet test vector") {
// Sensirion's published check value — pins polynomial (0x31), init (0xFF)
// and the no-reflection/no-final-XOR choices all at once.
const uint8_t v[] = {0xBE, 0xEF};
CHECK(sht41Crc8(v, 2) == 0x92);
// CRCs computed independently for the frames used below; a byte-order or
// polynomial regression breaks these even if the datasheet vector survives.
const uint8_t a[] = {0x66, 0x66};
CHECK(sht41Crc8(a, 2) == 0x93);
const uint8_t zero[] = {0x00, 0x00};
CHECK(sht41Crc8(zero, 2) == 0x81);
const uint8_t full[] = {0xFF, 0xFF};
CHECK(sht41Crc8(full, 2) == 0xAC);
}
TEST_CASE("decodeSht41Measurement converts raw words to physical units") {
// 0x6666 is exactly 25 °C under T = -45 + 175 * raw/65535.
auto r = decodeSht41Measurement(frame(0x66, 0x66, 0x80, 0x00));
REQUIRE(r.has_value());
CHECK(r->temp_c == doctest::Approx(25.0).epsilon(0.001));
CHECK(r->humidity_pct == doctest::Approx(56.501).epsilon(0.001));
}
TEST_CASE("decodeSht41Measurement clamps humidity to 0..100") {
// The datasheet RH formula overshoots both ends by design: raw 0 yields
// -6 %RH and raw 65535 yields 119 %RH. Temperature is NOT clamped — its
// full -45..130 °C range is real.
auto lo = decodeSht41Measurement(frame(0x00, 0x00, 0x00, 0x00));
REQUIRE(lo.has_value());
CHECK(lo->temp_c == doctest::Approx(-45.0).epsilon(0.001));
CHECK(lo->humidity_pct == doctest::Approx(0.0));
auto hi = decodeSht41Measurement(frame(0xFF, 0xFF, 0xFF, 0xFF));
REQUIRE(hi.has_value());
CHECK(hi->temp_c == doctest::Approx(130.0).epsilon(0.001));
CHECK(hi->humidity_pct == doctest::Approx(100.0));
}
TEST_CASE("decodeSht41Measurement rejects corrupt frames instead of reporting them") {
// A garbled word converts to a perfectly plausible temperature, so the CRC
// is the only thing standing between line noise and a believed reading.
std::vector<uint8_t> bad_temp_crc = frame(0x66, 0x66, 0x80, 0x00);
bad_temp_crc[2] ^= 0xFF;
CHECK_FALSE(decodeSht41Measurement(bad_temp_crc).has_value());
std::vector<uint8_t> bad_rh_crc = frame(0x66, 0x66, 0x80, 0x00);
bad_rh_crc[5] ^= 0xFF;
CHECK_FALSE(decodeSht41Measurement(bad_rh_crc).has_value());
// A flipped data bit invalidates the word its CRC covers.
std::vector<uint8_t> flipped = frame(0x66, 0x66, 0x80, 0x00);
flipped[1] ^= 0x01;
CHECK_FALSE(decodeSht41Measurement(flipped).has_value());
// Short/absent reads (a bus hiccup) must not be indexed into.
CHECK_FALSE(decodeSht41Measurement({}).has_value());
CHECK_FALSE(decodeSht41Measurement({0x66, 0x66, 0x93}).has_value());
std::vector<uint8_t> too_long = frame(0x66, 0x66, 0x80, 0x00);
too_long.push_back(0x00);
CHECK_FALSE(decodeSht41Measurement(too_long).has_value());
}
TEST_CASE("MockEnvSensor reports plausible, varying ambient readings") {
MockEnvSensor env;
env.start();
CHECK(env.connected());
CHECK(env.name().find("SHT41") != std::string::npos);
auto s = env.sample();
REQUIRE(s.has_value());
CHECK(s->valid);
CHECK(s->timestamp_ms > 0);
// Indoor-plausible, and within the ranges the real decode can produce.
CHECK(s->temp_c > -45.0f);
CHECK(s->temp_c < 130.0f);
CHECK(s->humidity_pct >= 0.0f);
CHECK(s->humidity_pct <= 100.0f);
env.stop();
}