class RingBuffer {
public:
explicit RingBuffer(size_t cap) : cap_(cap) {}
void push(T v) {
std::lock_guard lk(mx_);
if (q_.size() == cap_) q_.pop_front();
q_.push_back(std::move(v));
cv_.notify_all();
}
std::vector snapshot() const {
std::lock_guard lk(mx_);
return std::vector(q_.begin(), q_.end());
}
std::optional wait_pop_for(Ms d) {
std::unique_lock lk(mx_);
if (!cv_.wait_for(lk, d, [&]{ return !q_.empty() || stop_; })) return {};
if (q_.empty()) return {};
T v = std::move(q_.front()); q_.pop_front(); return v;
}
void stop() {
{ std::lock_guard lk(mx_); stop_ = true; }
cv_.notify_all();
}
private:
size_t cap_;
mutable std::mutex mx_;
std::condition_variable cv_;
std::deque q_;
bool stop_{false};
};
struct Ema {
double alpha{0.2};
std::optional s;
double operator()(double x) {
if (!s) s = x;
else *s = alpha * x + (1.0 - alpha) * (*s);
return *s;
}
};
struct PeakHold {
double value{0.0};
double decay{0.95}; // 1.0 = no decay, 0.9 = faster fall
double update(double x) {
value = std::max(value * decay, x);
return value;
}
};
static inline std::string sparkline(const std::vector& xs, size_t width=40) {
if (xs.empty()) return std::string(width, ' ');
double lo = *std::min_element(xs.begin(), xs.end());
double hi = *std::max_element(xs.begin(), xs.end());
if (hi - lo < 1e-12) hi = lo + 1e-12;
std::string out; out.reserve(width);
for (size_t i=0;i(i * (xs.size()-1) / std::max(1,width-1));
double n = (xs[idx]-lo)/(hi-lo);
size_t b = std::min(bars.size()-1, static_cast(n*(bars.size()-1)));
out.push_back(bars[b]);
}
return out;
}
class Sensor {
public:
virtual ~Sensor() = default;
virtual std::string name() const = 0;
virtual double read() = 0;
protected:
std::mt19937 rng{std::random_device{}()};
};
class TempSensor : public Sensor {
public:
std::string name() const override { return "temp"; }
double read() override {
std::normal_distribution base(23.5, 0.15);
double drift = 0.6 * std::sin(now_ms()/1000.0 * 0.3);
return base(rng) + drift;
}
};
class LuxSensor : public Sensor {
public:
std::string name() const override { return "lux"; }
double read() override {
std::uniform_real_distribution u(450.0, 620.0);
double blink = (std::sin(now_ms()/1000.0 * 2.1) + 1.0) * 40.0;
return u(rng) + blink;
}
};
class ImuAx : public Sensor {
public:
std::string name() const override { return "ax"; }
double read() override {
std::normal_distribution n(0.0, 0.03);
return 0.2*std::sin(now_ms()/1000.0*1.7) + n(rng);
}
};
class ImuAy : public Sensor {
public:
std::string name() const override { return "ay"; }
double read() override {
std::normal_distribution n(0.0, 0.03);
return 0.2*std::cos(now_ms()/1000.0*1.1) + n(rng);
}
};
class ImuAz : public Sensor {
public:
std::string name() const override { return "az"; }
double read() override {
std::normal_distribution n(0.0, 0.02);
return 0.98 + 0.02*std::sin(now_ms()/1000.0*0.7) + n(rng);
}
};
class Acquisition {
public:
explicit Acquisition(int hz=25)
: period_(Ms(1000/hz)),
out_(std::make_shared>(1024)) {}
void start() {
if (running_) return;
running_ = true;
thr_ = std::thread(&Acquisition::loop, this);
}
void stop() {
running_ = false;
if (thr_.joinable()) thr_.join();
out_->stop();
}
std::shared_ptr> bus() const { return out_; }
private:
void loop() {
TempSensor ts; LuxSensor ls; ImuAx ax; ImuAy ay; ImuAz az;
Ema eTemp{0.15}, eLux{0.12}, eAx{0.2}, eAy{0.2}, eAz{0.12};
auto next = Clock::now();
while (running_) {
next += period_;
Sample s {
now_ms(),
eTemp(ts.read()),
eLux(ls.read()),
eAx(ax.read()), eAy(ay.read()), eAz(az.read())
};
out_->push(s);
std::this_thread::sleep_until(next);
}
}
Ms period_;
std::shared_ptr> out_;
std::atomic running_{false};
std::thread thr_;
};
class CsvLogger {
public:
explicit CsvLogger(const std::string& path) : f_(path, std::ios::out) {
if (f_) f_ << "t_ms,temp,lux,ax,ay,az\n";
}
void write(const Sample& s) {
if (!f_) return;
f_ << s.t << ',' << s.temp << ',' << s.lux << ','
<< s.ax << ',' << s.ay << ',' << s.az << '\n';
}
private:
std::ofstream f_;
};
class Dashboard {
public:
explicit Dashboard(std::shared_ptr> bus)
: bus_(std::move(bus)) {}
void run(int seconds=10) {
CsvLogger log("telemetry.csv");
PeakHold pkTemp{0.0, 0.97}, pkLux{0.0, 0.97}, pkAcc{0.0, 0.96};
std::vector hTemp, hLux, hAcc; hTemp.reserve(512); hLux.reserve(512); hAcc.reserve(512);
auto t0 = Clock::now();
while (std::chrono::duration_cast(Clock::now()-t0).count() < seconds*1000) {
auto opt = bus_->wait_pop_for(Ms(200));
if (!opt) continue;
auto s = *opt; log.write(s);
double accMag = std::sqrt(s.ax*s.ax + s.ay*s.ay + s.az*s.az);
hTemp.push_back(s.temp); if (hTemp.size()>240) hTemp.erase(hTemp.begin());
hLux.push_back(s.lux); if (hLux.size()>240) hLux.erase(hLux.begin());
hAcc.push_back(accMag); if (hAcc.size()>240) hAcc.erase(hAcc.begin());
pkTemp.update(s.temp);
pkLux.update(s.lux);
pkAcc.update(accMag);
render(hTemp, hLux, hAcc, s, pkTemp.value, pkLux.value, pkAcc.value);
}
}