Skip to content

Add opt-in history-anchored post-processing reconciliation for premature dial transitions (#4112)#4137

Open
Apfelstruhdl wants to merge 3 commits into
jomjol:mainfrom
Apfelstruhdl:feat/history-anchored-postprocessing
Open

Add opt-in history-anchored post-processing reconciliation for premature dial transitions (#4112)#4137
Apfelstruhdl wants to merge 3 commits into
jomjol:mainfrom
Apfelstruhdl:feat/history-anchored-postprocessing

Conversation

@Apfelstruhdl

Copy link
Copy Markdown

Summary

Adds an opt-in history-anchored reconciliation layer to post-processing that fixes a class of wrong readings on meters with "early transition" analog dials, addressing #4112.

On such meters a pointer (or digit) flips ahead of the real rollover, so a single frame can be recognised as a value that is off by one dial step (e.g. ±100 L). The existing single-frame logic and the MaxRateValue / AllowNegativeRates guards can't distinguish such a premature — and often stable — misread from real flow, so it gets accepted and poisons PreValue; a tightened rate cap then rejects the legitimate catch-up and pins the reading. Config alone can't resolve this (the flip magnitude overlaps real flow; Analog_error is a compile-time constant).

Default-off, so existing installs are byte-for-byte unaffected.

How it works

A new pure, dependency-free module (MeterReconcile, code/components/jomjol_flowcontroll/MeterReconcile.{h,cpp}) reconciles each frame's recognised value against the last trusted value using the meter's physical properties:

  • Carry gate. A value crossing a most-significant-analog-dial step (a carry) is accepted only with evidence that the dial below actually wrapped 9→0, as a single forward step. This is enforced on both "clean" and ambiguous frames, because a pointer misread part of a step off its true position need not land near an integer — so a premature carry can otherwise masquerade as a trustworthy reading. Wrap evidence comes from three independent channels:
    1. the dial-below transition observed directly since the previous frame;
    2. a short "sticky" window, since the wrap and the valid carry can land on different frames (the wrap frame may itself be a misread);
    3. inference from the value — a genuine carry passes the sub-position (the part below one dial step) through zero, while a premature flip leaves it unchanged. This alone recovers a stalled reading with no observed wrap and no clean frame.
  • Within-step motion is trusted on a clean frame; on an ambiguous frame forward motion is allowed while only sub-noise backward jitter is tolerated.
  • Stuck-recovery. After the value has been held for a sustained, stable stretch, a clean frame re-anchors across a carry (heals a wrong anchor / absorbs catch-up).
  • Gross-misread ceiling. A physically impossible single-frame jump is never accepted.

All thresholds (msbStep, noiseTol, maxJump) are derived per-frame from the meter's decimal scaling, so it works for digit+analog and analog-only layouts, extended resolution or not. It complements CheckDigitIncreaseConsistency (which handles the digit chain) by adding the equivalent protection on the analog dial chain, and replaces the fixed rate guards only while enabled.

Configuration

Two new per-<NUMBER> parameters (docs under param-docs/):

  • HistoryReconcile (default false) — enable it.
  • HistoryMaxJump (default: auto-derived from the dial scale) — optional gross-misread ceiling override.

No effect on sequences without analog ROIs.

Testing

  • Host unit tests (code/test/host/, 53 checks) cover the carry gate (all three evidence channels), premature-flip rejection on clean and ambiguous frames, backward/multi-step carry rejection, gross-misread rejection, scale invariance, floating-point boundary safety, and stability-bounded recovery. A CI job compiles and runs them on every push (can be dropped if you'd prefer not to touch the workflow).
  • Firmware build + packaging pass in CI.
  • Validated on a real early-transition meter for ~1 week, including the exact failures from Premature digit transition accepted on early-transition meters; no history-based logic to detect impossible digit/analog combinations #4112:
    • premature ±100 L dial flips — held, no longer poison PreValue;
    • the ±900 L 0/9-seam flicker (ana1 read as 9.x while just past 0) — rejected every frame while genuine flow between them is still tracked;
    • a full multi-dial cascade rollover (ana1 9→0 + thousands digit + dig5=N) — crossed cleanly in real time, zero hold frames (the case that previously stalled for ~50 min);
    • zero accepted poison over 3 continuous days; running total stays exact and HA long-term statistics stay clean.

Notes

  • Opt-in + default-off ⇒ no regression risk for existing users.
  • Scoped to sequences with analog ROIs by design: the digit chain already has its
    history-anchored guard (CheckDigitIncreaseConsistency), and the reconciler's evidence signals
    (boundary-band cleanliness, wrap observation, sub-position inference) are properties of
    continuously rotating pointers — a digit wheel parks at integers, so the ambiguity model does
    not transfer. Extending it to digit-only meters with continuous digit models would be possible
    future work, but needs its own ambiguity model and validation.
  • Operating assumption (documented): typical consumption between two cycles stays well below one
    most-significant-dial step. With very long capture intervals, heavy flow can exceed that
    per-cycle step; the value then holds during the burst and catches up shortly after flow pauses
    (nothing lost, but readings lag). A time-aware accept path for such multi-step catch-up would be
    possible future work.
  • Recognition scatter is the root trigger; reducing it further (e.g. enabling AntiAliasing for the alignment rotation) lowers how often the guard has to engage — measured ~50 % less analog scatter on the test meter — but that's independent of this change.

Pure, dependency-free C++ module that reconciles the per-frame recognition
candidate against the last trusted value using the meter's physical priors
(monotonic, slow vs. frame rate, absolute encoder, over-determined dial chain).

A most-significant analog-dial carry is accepted only as a single forward step
with wrap evidence from the dial below - its observed transition (kept valid
for a short window, since the wrap and the valid carry can land on different
frames) or inference from the value itself (a genuine carry passes the
sub-position through zero; a premature dial flip leaves it unchanged). The
gate applies on unambiguous frames too, since a misread dial need not land
near an integer. Within-step motion is trusted on unambiguous frames; a stuck
value recovers on a clean frame with a stable recognition after sustained
holds; physically impossible single-frame jumps are never accepted.

Includes a host unit test (g++, 53 checks) covering the carry gate on all
evidence channels, premature-flip rejection on clean and ambiguous frames,
backward/multi-step carry rejection, gross-misread rejection, scale
invariance, floating-point boundary safety, and stability-bounded recovery.
…l#4112)

Opt-in per-<NUMBER> parameters HistoryReconcile (default false) and
HistoryMaxJump (default auto-derived from the dial scale). When enabled for a
sequence with analog ROIs, doFlow() runs the reconciliation core in place of
the fixed AllowNegativeRates/MaxRateValue guards: it derives the clean/wrap
signals from the analog dial floats (plus the previous frame) and the
msbStep / noiseTol / maxJump thresholds from the meter's decimal scaling
(correct for digit+analog and analog-only layouts, extended resolution or
not; a single-dial sequence disables the carry gate for lack of an evidence
channel). The digit-chain CheckDigitIncreaseConsistency path is unchanged and
behaviour is byte-identical while the feature is off.

Adds NumberPost state, config parsing/handlers, parameter documentation, and
a Changelog entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant