#include "fgc/sensors/Sht41EnvSensor.h" #include "fgc/Logger.h" #include "fgc/sensors/I2cBus.h" #include "fgc/sensors/Sht41Protocol.h" #include #include #include #include #include namespace fgc { namespace { // The command bytes and the frame decode (CRC + raw->physical) live in // Sht41Protocol, which is pure and unit-tested; this file owns only the I/O: // bus access, poll timing, and the staleness rule. constexpr auto kConversionDelay = std::chrono::milliseconds(10); constexpr auto kSoftResetSettle = std::chrono::milliseconds(1); } // namespace struct Sht41EnvSensor::Impl { Impl(std::string dev, uint8_t a, std::chrono::milliseconds p) : bus(std::move(dev), a), period(p) {} using clock = std::chrono::steady_clock; I2cBus bus; std::chrono::milliseconds period; std::thread worker; std::atomic running{false}; std::mutex mutex; EnvSample latest; clock::time_point last_ok{}; // Consecutive failed reads. The transfers run every `period` forever, so the // failure is logged on the FIRST one only: a wrong bus would otherwise print // the same line every couple of seconds for the life of the process. Reset on // a good read, which also gives us a recovery message. long long fail_streak = 0; // A silent sensor is nearly always the wrong bus or address, and neither // open() nor the I2C_SLAVE ioctl can tell you that - the ioctl only records // the target address, it never probes. This is the first point where the // absence of a device is actually observable, so it is where it gets said. void reportFailure(const char* what) { if (fail_streak++ > 0) return; LOG_ERROR << "SHT41: " << what << " at address 0x" << std::hex << static_cast(bus.address()) << std::dec << " on " << bus.device() << " - check [Env] i2c_device/i2c_addr and the wiring" << " (probe with: i2cdetect -l, then i2ctransfer -y w1@0x" << std::hex << static_cast(bus.address()) << std::dec << " 0x89)"; LOG_ERROR << "SHT41: further read failures will not be logged until it responds"; } void readOnce() { if (!bus.write({kSht41CmdMeasureHighPrec})) { reportFailure("no response to the measure command"); return; } std::this_thread::sleep_for(kConversionDelay); std::vector rx(kSht41ResponseBytes); if (!bus.writeRead({}, rx)) { reportFailure("command acknowledged but no measurement came back"); return; } auto reading = decodeSht41Measurement(rx); if (!reading) { LOG_WARN << "SHT41: CRC mismatch, discarding sample"; return; } if (fail_streak > 0) { LOG_INFO << "SHT41: responding again after " << fail_streak << " failed read(s)"; fail_streak = 0; } std::lock_guard lock(mutex); latest.valid = true; latest.temp_c = reading->temp_c; latest.humidity_pct = reading->humidity_pct; latest.timestamp_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); last_ok = clock::now(); } void run() { while (running.load()) { readOnce(); std::this_thread::sleep_for(period); } } }; Sht41EnvSensor::Sht41EnvSensor(std::string i2c_device, uint8_t addr, std::chrono::milliseconds period) : impl_(std::make_unique(std::move(i2c_device), addr, period)) {} Sht41EnvSensor::~Sht41EnvSensor() { stop(); } void Sht41EnvSensor::start() { if (!impl_->bus.open()) { LOG_ERROR << "SHT41: failed to open I2C bus; sensor disabled"; return; } impl_->bus.write({kSht41CmdSoftReset}); std::this_thread::sleep_for(kSoftResetSettle); impl_->running = true; impl_->worker = std::thread([this] { impl_->run(); }); // Deliberately not "started OK": opening the node proves nothing about the // sensor being there. The first read either succeeds quietly or logs why not. LOG_INFO << "SHT41 env sensor polling 0x" << std::hex << static_cast(impl_->bus.address()) << std::dec << " on " << impl_->bus.device() << " every " << impl_->period.count() << " ms"; } void Sht41EnvSensor::stop() { if (!impl_) return; impl_->running = false; if (impl_->worker.joinable()) impl_->worker.join(); impl_->bus.close(); } bool Sht41EnvSensor::connected() const { std::lock_guard lock(impl_->mutex); return impl_->latest.valid && (Impl::clock::now() - impl_->last_ok) < 2 * impl_->period; } std::optional Sht41EnvSensor::sample() { std::lock_guard lock(impl_->mutex); if (!impl_->latest.valid || (Impl::clock::now() - impl_->last_ok) >= 2 * impl_->period) return std::nullopt; return impl_->latest; } } // namespace fgc