101 lines
3.8 KiB
C++
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();
|
|
}
|