#include #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 frame(uint8_t t_msb, uint8_t t_lsb, uint8_t h_msb, uint8_t h_lsb) { std::vector 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 bad_temp_crc = frame(0x66, 0x66, 0x80, 0x00); bad_temp_crc[2] ^= 0xFF; CHECK_FALSE(decodeSht41Measurement(bad_temp_crc).has_value()); std::vector 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 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 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(); }