diff --git a/include/v2/events.h b/include/v2/events.h index 99376ff..13ab053 100644 --- a/include/v2/events.h +++ b/include/v2/events.h @@ -67,7 +67,7 @@ namespace pocketpd { /** * @brief Latest sensor reading from INA226. */ - struct SensorSnapshot { + struct LoadReading { uint32_t timestamp_ms = 0; uint32_t vbus_mv = 0; uint32_t current_ma = 0; @@ -77,7 +77,7 @@ namespace pocketpd { * @brief Published by SensorTask. Carries one bus reading. */ struct SensorEvent { - SensorSnapshot snapshot; + LoadReading load; }; /** diff --git a/include/v2/stages/energy/energy_view.h b/include/v2/stages/energy/energy_view.h index 7d34db8..7d57f60 100644 --- a/include/v2/stages/energy/energy_view.h +++ b/include/v2/stages/energy/energy_view.h @@ -21,7 +21,7 @@ namespace pocketpd { * @brief Frozen snapshot of everything EnergyView needs to draw one frame. */ struct EnergyViewModel { - SensorSnapshot snapshot{}; + LoadReading load_reading{}; double accumulated_wh = 0.0; double accumulated_ah = 0.0; uint32_t total_seconds = 0; @@ -72,10 +72,10 @@ namespace pocketpd { display.set_font(tempo::Font::BASE); // Mid — Voltage Current - format_milli(buf, vm.snapshot.vbus_mv, 'V'); + format_milli(buf, vm.load_reading.vbus_mv, 'V'); display.draw_text(V_X, ROW2_Y - 10, buf.data()); - format_milli(buf, vm.snapshot.current_ma, 'A'); + format_milli(buf, vm.load_reading.current_ma, 'A'); display.draw_text(A_X, ROW2_Y + 5, buf.data()); // Bottom row — Wh Time Ah @@ -90,7 +90,8 @@ namespace pocketpd { // The big W label in the middle display.set_font(tempo::Font::XL); - const double watts = (vm.snapshot.vbus_mv * vm.snapshot.current_ma) / 1'000'000.0; + const double watts = + (vm.load_reading.vbus_mv * vm.load_reading.current_ma) / 1'000'000.0; format_auto(buf, watts); draw_value(display, COL1_X, ROW2_Y, buf.data(), "W"); diff --git a/include/v2/stages/energy_stage.h b/include/v2/stages/energy_stage.h index 31cea2c..3dd8591 100644 --- a/include/v2/stages/energy_stage.h +++ b/include/v2/stages/energy_stage.h @@ -23,6 +23,7 @@ #include "v2/images.h" #include "v2/stages/energy/energy_view.h" #include "v2/pocketpd.h" +#include "v2/util/filter.h" namespace pocketpd { @@ -32,8 +33,8 @@ namespace pocketpd { Display& m_display; OutputGate& m_output_gate; - SensorSnapshot m_snapshot{}; - bool m_snapshot_seeded = false; + LoadReading m_load_reading{}; + bool m_load_init = false; EnergyEvent m_energy{}; int8_t m_active_pdo_index = -1; tempo::IntervalTimer m_render_interval{40}; @@ -96,7 +97,12 @@ namespace pocketpd { conductor.request(m_active_pdo_index); } }, - [&](const SensorEvent& evt) { ema_filter(evt.snapshot); }, + [&](const SensorEvent& evt) { + m_load_reading = m_load_init + ? Filter::ema(m_load_reading, evt.load, SENSOR_EMA_DEN) + : evt.load; + m_load_init = true; + }, [&](const EnergyEvent& evt) { m_energy = evt; }, [](const auto&) {}, }; @@ -105,25 +111,9 @@ namespace pocketpd { } private: - /** - * @brief Apply EMA smoothing to displayed mV / mA. Same shape as - * NormalStage so the readout is consistent across screens. - */ - void ema_filter(const SensorSnapshot& s) { - if (!m_snapshot_seeded) { - m_snapshot = s; - m_snapshot_seeded = true; - return; - } - const uint32_t a = SENSOR_EMA_DEN - 1; - m_snapshot.vbus_mv = (m_snapshot.vbus_mv * a + s.vbus_mv) / SENSOR_EMA_DEN; - m_snapshot.current_ma = (m_snapshot.current_ma * a + s.current_ma) / SENSOR_EMA_DEN; - m_snapshot.timestamp_ms = s.timestamp_ms; - } - EnergyViewModel build_view_model() const { return EnergyViewModel{ - .snapshot = m_snapshot, + .load_reading = m_load_reading, .accumulated_wh = m_energy.accumulated_wh, .accumulated_ah = m_energy.accumulated_ah, .total_seconds = m_energy.total_seconds, diff --git a/include/v2/stages/normal/normal_view.h b/include/v2/stages/normal/normal_view.h index 3446809..fdc2303 100644 --- a/include/v2/stages/normal/normal_view.h +++ b/include/v2/stages/normal/normal_view.h @@ -28,7 +28,7 @@ namespace pocketpd { bool readout_visible = true; bool locked = false; uint8_t arrow_frame = 0; - SensorSnapshot snapshot{}; + LoadReading load_reading{}; // —— PPS branch (valid when has_profile && is_pps) @@ -79,8 +79,12 @@ namespace pocketpd { std::array buf{}; d.set_font(tempo::Font::XL); - draw_measured(d, "V", V_MEASURED_Y, vm.snapshot.vbus_mv, buf, vm.readout_visible); - draw_measured(d, "A", A_MEASURED_Y, vm.snapshot.current_ma, buf, vm.readout_visible); + draw_measured( + d, "V", V_MEASURED_Y, vm.load_reading.vbus_mv, buf, vm.readout_visible + ); + draw_measured( + d, "A", A_MEASURED_Y, vm.load_reading.current_ma, buf, vm.readout_visible + ); d.set_font(tempo::Font::BASE); std::snprintf(buf.data(), buf.size(), "[%u]", vm.active_pdo_index); diff --git a/include/v2/stages/normal_stage.h b/include/v2/stages/normal_stage.h index 9f9d269..74910a2 100644 --- a/include/v2/stages/normal_stage.h +++ b/include/v2/stages/normal_stage.h @@ -18,6 +18,7 @@ #include "v2/stages/normal/fixed_mode.h" #include "v2/stages/normal/normal_view.h" #include "v2/stages/normal/pps_mode.h" +#include "v2/util/filter.h" namespace pocketpd { @@ -32,8 +33,8 @@ namespace pocketpd { PdSinkController& m_pd_sink; OutputGate& m_output_gate; - SensorSnapshot m_snapshot{}; - bool m_snapshot_init = false; + LoadReading m_load_reading{}; + bool m_load_init = false; int8_t m_active_pdo_index = -1; int8_t m_last_active_index = -1; @@ -181,7 +182,12 @@ namespace pocketpd { log.error(msg, m_active_pdo_index, pps->target_mv, pps->target_ma); } }, - [&](const SensorEvent& evt) { ema_filter(evt.snapshot); }, + [&](const SensorEvent& evt) { + m_load_reading = m_load_init + ? Filter::ema(m_load_reading, evt.load, SENSOR_EMA_DEN) + : evt.load; + m_load_init = true; + }, [](const auto&) {}, }; @@ -189,24 +195,6 @@ namespace pocketpd { } private: - /** - * @brief Apply EMA smoothing to displayed mV / mA. - */ - void ema_filter(const SensorSnapshot& s) { - if (!m_snapshot_init) { - m_snapshot = s; - m_snapshot_init = true; - return; - } - - // For SENSOR_EMA_DEN = 4: new = 0.75 * new + 0.25 * old - - const uint32_t a = SENSOR_EMA_DEN - 1; - m_snapshot.vbus_mv = (m_snapshot.vbus_mv * a + s.vbus_mv) / SENSOR_EMA_DEN; - m_snapshot.current_ma = (m_snapshot.current_ma * a + s.current_ma) / SENSOR_EMA_DEN; - m_snapshot.timestamp_ms = s.timestamp_ms; - } - /** * @brief Enter (or re-enter) a PPS profile. Construct fresh state on profile change or * mode switch; otherwise preserve the user's prior target edits and reissue. @@ -252,7 +240,7 @@ namespace pocketpd { .readout_visible = m_blink_visible, .locked = m_locked, .arrow_frame = m_arrow_frame, - .snapshot = m_snapshot, + .load_reading = m_load_reading, }; if (!vm.has_profile) { diff --git a/include/v2/tasks/energy_task.h b/include/v2/tasks/energy_task.h index 6432a3e..91fff6a 100644 --- a/include/v2/tasks/energy_task.h +++ b/include/v2/tasks/energy_task.h @@ -52,7 +52,7 @@ namespace pocketpd { void on_event(const Event& event, uint32_t) override { if (const auto sensor = std::get_if(&event)) { - integrate(sensor->snapshot); + integrate(sensor->load); } } @@ -67,7 +67,7 @@ namespace pocketpd { } private: - void integrate(const SensorSnapshot& s) { + void integrate(const LoadReading& s) { if (!m_output_gate.is_enabled()) { m_session_active = false; return; diff --git a/include/v2/tasks/sensor_task.h b/include/v2/tasks/sensor_task.h index 232d268..bac42b1 100644 --- a/include/v2/tasks/sensor_task.h +++ b/include/v2/tasks/sensor_task.h @@ -33,7 +33,7 @@ namespace pocketpd { if (!reading.valid) { return; } - publish(SensorEvent{SensorSnapshot{now_ms, reading.mv, reading.ma}}); + publish(SensorEvent{LoadReading{now_ms, reading.mv, reading.ma}}); } }; diff --git a/include/v2/util/filter.h b/include/v2/util/filter.h new file mode 100644 index 0000000..294d7e9 --- /dev/null +++ b/include/v2/util/filter.h @@ -0,0 +1,33 @@ +/** + * @file filter.h + * @brief Stateless EMA scalar + snapshot overloads. + */ +#pragma once + +#include + +#include "v2/events.h" + +namespace pocketpd { + + class Filter { + public: + /** + * @brief One EMA step. `new = ((den-1)*prev + sample) / den`. + * For `den = 4`: weights 0.75 prev / 0.25 sample. + */ + static uint32_t ema(uint32_t prev, uint32_t sample, uint32_t den) { + return (prev * (den - 1) + sample) / den; + } + + /** @brief Per-field EMA across a `LoadReading`. `timestamp_ms` tracks the latest. */ + static LoadReading ema(const LoadReading& prev, const LoadReading& sample, uint32_t den) { + return LoadReading{ + .timestamp_ms = sample.timestamp_ms, + .vbus_mv = ema(prev.vbus_mv, sample.vbus_mv, den), + .current_ma = ema(prev.current_ma, sample.current_ma, den), + }; + } + }; + +} // namespace pocketpd diff --git a/test/test_v2_energy/test.cpp b/test/test_v2_energy/test.cpp index cd5813f..145cde6 100644 --- a/test/test_v2_energy/test.cpp +++ b/test/test_v2_energy/test.cpp @@ -36,7 +36,7 @@ using TestConductor = App::Conductor; namespace { SensorEvent make_sensor(uint32_t ts_ms, uint32_t mv, uint32_t ma) { - return SensorEvent{SensorSnapshot{ts_ms, mv, ma}}; + return SensorEvent{LoadReading{ts_ms, mv, ma}}; } const EnergyEvent* drain_last_energy(TestQueue& q) { diff --git a/test/test_v2_normal/test.cpp b/test/test_v2_normal/test.cpp index 36d1a4a..e6a6813 100644 --- a/test/test_v2_normal/test.cpp +++ b/test/test_v2_normal/test.cpp @@ -355,7 +355,7 @@ TEST(NormalStage, OnEnterPdoBranchRendersVAReadoutAndPdoIndex) { TestConductor conductor; conductor.register_stage(normal); normal.prepare(2); - normal.on_event(conductor, SensorEvent{SensorSnapshot{0, 5000, 1234}}, 0); + normal.on_event(conductor, SensorEvent{LoadReading{0, 5000, 1234}}, 0); conductor.start(); } @@ -420,7 +420,7 @@ TEST(NormalView, ReadoutHiddenKeepsLabelsHidesValues) { vm.output_enabled = false; vm.readout_visible = false; vm.active_pdo_index = 0; - vm.snapshot = SensorSnapshot{0, 5000, 1234}; + vm.load_reading = LoadReading{0, 5000, 1234}; NormalView::render(display, vm); } diff --git a/test/test_v2_sensor/test.cpp b/test/test_v2_sensor/test.cpp index bd077f9..0f27774 100644 --- a/test/test_v2_sensor/test.cpp +++ b/test/test_v2_sensor/test.cpp @@ -47,9 +47,9 @@ TEST(SensorTask, OnTickPublishesSensorEventWithTimestampAndValues) { const auto* evt = pop_sensor(q); ASSERT_NE(evt, nullptr); - EXPECT_EQ(evt->snapshot.timestamp_ms, 42u); - EXPECT_EQ(evt->snapshot.vbus_mv, 5000u); - EXPECT_EQ(evt->snapshot.current_ma, 1234u); + EXPECT_EQ(evt->load.timestamp_ms, 42u); + EXPECT_EQ(evt->load.vbus_mv, 5000u); + EXPECT_EQ(evt->load.current_ma, 1234u); } TEST(SensorTask, InvalidReadingDoesNotPublish) {