The pin crosses libavr's phase-6 close and the guideline sweep behind it; the image is byte-identical in both modes at 8206 bytes. The port's own sweep, against the same rules. Every mutable `static inline` takes `m_` - uptime's counter, the sampler's window, the controller's five, the statistics histogram and the terminal's line state (rule 46; a private `static constexpr` is a constant rather than state and keeps its bare name). The command table is `std::to_array` and the serial config breaks one member per line (rules 36, 40). And three numbers get the name they already had somewhere: duty goes through `percent_t::of()` rather than a hand-built basis-point count, the ADC's top count is `thermistor::adc_full_scale` instead of 1023 in four places, and the two `0xffffffff` are `open_circuit` - which was already declared five lines away - and `never_written`, which replaces a comment explaining the literal (rules 5, 6, 41). Measured, not assumed: rendering `adc_full_scale` into the `show` line instead of leaving it in the message string cost 6 bytes, so the display text stays text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
2.4 KiB
C++
119 lines
2.4 KiB
C++
#pragma once
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
|
|
#include <libavr/libavr.hpp>
|
|
|
|
#include "board.hpp"
|
|
|
|
// Temperature histogram: one uint32 bucket per C 0..99, sampled once a
|
|
// second, written back to EEPROM every 30 minutes (update() only touches
|
|
// changed bytes).
|
|
namespace app {
|
|
|
|
class statistics {
|
|
static constexpr std::uint8_t range = 100;
|
|
static constexpr std::uint32_t sample_delay_ms = 1'000;
|
|
static constexpr std::uint32_t writeback_delay_ms = 1'800'000;
|
|
|
|
// What an erased cell reads back as, so a bucket nobody has written yet
|
|
// counts as no samples rather than four billion.
|
|
static constexpr std::uint32_t never_written = ~std::uint32_t{0};
|
|
|
|
using stored = avr::eeprom::var<std::array<std::uint32_t, range>, 0>;
|
|
|
|
static inline std::array<std::uint32_t, range> m_histogram{};
|
|
static inline std::uint64_t m_last_sample = 0;
|
|
static inline std::uint64_t m_last_writeback = 0;
|
|
|
|
static constexpr std::uint8_t clamp(std::int8_t t)
|
|
{
|
|
return t < 0 ? 0 : (t >= range ? range - 1 : static_cast<std::uint8_t>(t));
|
|
}
|
|
|
|
public:
|
|
static constexpr auto claims = stored::claims;
|
|
|
|
static void init()
|
|
{
|
|
m_histogram = stored::read();
|
|
for (auto &bucket : m_histogram) {
|
|
if (bucket == never_written) {
|
|
bucket = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void record(std::int8_t celsius)
|
|
{
|
|
auto now = uptime::millis();
|
|
if (now >= m_last_sample + sample_delay_ms) {
|
|
++m_histogram[clamp(celsius)];
|
|
m_last_sample = now;
|
|
}
|
|
if (now >= m_last_writeback + writeback_delay_ms) {
|
|
save();
|
|
m_last_writeback = now;
|
|
}
|
|
}
|
|
|
|
static void save()
|
|
{
|
|
stored::update(m_histogram);
|
|
}
|
|
|
|
static void reset()
|
|
{
|
|
m_histogram = {};
|
|
stored::update(m_histogram);
|
|
}
|
|
|
|
static std::uint8_t min_temperature()
|
|
{
|
|
for (std::uint8_t i = 0; i < range; ++i) {
|
|
if (m_histogram[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
return range;
|
|
}
|
|
|
|
static std::uint8_t max_temperature()
|
|
{
|
|
for (std::uint8_t i = range; i > 0; --i) {
|
|
if (m_histogram[i - 1]) {
|
|
return i - 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static std::uint64_t total_samples()
|
|
{
|
|
std::uint64_t total = 0;
|
|
for (auto bucket : m_histogram) {
|
|
total += bucket;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
static std::uint32_t highest_bucket()
|
|
{
|
|
std::uint32_t highest = 0;
|
|
for (auto bucket : m_histogram) {
|
|
if (bucket > highest) {
|
|
highest = bucket;
|
|
}
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
static std::uint32_t bucket(std::uint8_t celsius)
|
|
{
|
|
return m_histogram[clamp(static_cast<std::int8_t>(celsius))];
|
|
}
|
|
};
|
|
|
|
} // namespace app
|