fwt_software/src/sensors/Sht41EnvSensor.cpp

144 lines
5.1 KiB
C++

#include "fgc/sensors/Sht41EnvSensor.h"
#include "fgc/Logger.h"
#include "fgc/sensors/I2cBus.h"
#include "fgc/sensors/Sht41Protocol.h"
#include <atomic>
#include <ios>
#include <chrono>
#include <mutex>
#include <thread>
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<bool> 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<int>(bus.address()) << std::dec << " on " << bus.device()
<< " - check [Env] i2c_device/i2c_addr and the wiring"
<< " (probe with: i2cdetect -l, then i2ctransfer -y <bus> w1@0x"
<< std::hex << static_cast<int>(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<uint8_t> 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<std::mutex> 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::milliseconds>(
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<Impl>(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<int>(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<std::mutex> lock(impl_->mutex);
return impl_->latest.valid &&
(Impl::clock::now() - impl_->last_ok) < 2 * impl_->period;
}
std::optional<EnvSample> Sht41EnvSensor::sample() {
std::lock_guard<std::mutex> lock(impl_->mutex);
if (!impl_->latest.valid || (Impl::clock::now() - impl_->last_ok) >= 2 * impl_->period)
return std::nullopt;
return impl_->latest;
}
} // namespace fgc