144 lines
5.1 KiB
C++
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
|