Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt (2026.9.4.1)
Das Fahrtfenster stand seit dem 01.09. in der Gerätezeit, der Verlauf war nach Ankunftszeit sortiert. Zwei Uhren, und das Gerät puffert: was verspätet ankam, fiel aus einem Fenster heraus, in dem es der Sache nach lag. Am 03.09. hat das eine Fahrt gekostet - 26 von 48 GPS-Punkten und 3,0 statt 7 km. verlauf_lesen() stempelt jeden Punkt jetzt mit der Gerätezeit seines Datensatzes. Eine Stelle, an der alle Verbraucher vorbeikommen. Dazu zwei Riegel aus derselben Untersuchung: ein Rückschritt von 43 Metern ist keine Zählerrücksetzung mehr (aus 87 Metern wurden vorher 13,7 km), und das Fahrtende wird auf das letzte Lebenszeichen des Geräts geklemmt - der Nachlauf ist eine Rechnung, kein Messwert. Am echten Recorder nachgerechnet: Tacho 3,0 -> 7,0 km, GNSS 2,952 -> 6,877 km, Route 26 -> 47 Punkte, Ende 23:21:57 -> 23:17:48. Beim ersten Lauf haben zwei Fahrten eine Strecke bekommen, die vorher gar keine hatten. Nicht das Gerät war schuld: es hat lückenlos aufgezeichnet. Die zehn Minuten Funkstille waren eine offene, aber tote TCP-Sitzung (flespi-Log: 703 s, 16 Nachrichten) - kein Funkloch. Begründung und die Empfehlung fürs Gerät stehen in AGENTS.md, Abschnitt CB. Dazu die neun Paritätsbefunde, alle Richtung Panel gelöst - darunter ein unmaskierter Punkt in beiden Codebasen, der aus "vor 3 Tage" ein "vor 3 Tag" machte. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-09-03** (Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
|
||||
**Last updated: 2026-09-04** (Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun
|
||||
Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
|
||||
schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
|
||||
zurueck, `2026.9.3.8`, Abschnitt BY. Davor: „Fahrt beenden“ auf Knopfdruck mit vorlaeufigem Ende, Schieberegler in
|
||||
5-Minuten-Schritten und im Tagmodus sichtbar, `2026.9.3.7`, Abschnitt BX.
|
||||
@@ -9310,3 +9312,153 @@ performance.getEntriesByType("resource")
|
||||
gespeicherte Stand stammt von vor dem Filter aus `.11`, und `wunsch_uebernehmen()`
|
||||
liest zu Recht nicht neu, solange das Ziel schon unterwegs ist. Ein Druck auf
|
||||
„Jetzt lesen" räumt es weg.
|
||||
|
||||
## CB. Device time vs. arrival time — the root cause behind a 3-km trip (2026.9.4.1)
|
||||
|
||||
Owner report: the last trip is 3 km long, "and in reality considerably longer". It became the
|
||||
longest single investigation of this project, and almost every hypothesis along the way was wrong.
|
||||
The findings are recorded in that order on purpose — the wrong turns are the useful part.
|
||||
|
||||
### What the device actually did
|
||||
|
||||
Nothing wrong. Measured against the recorder, the FMM003 recorded the drive **completely**: 48 GPS
|
||||
records in the trip window, first at 23:09:40 in Schottenau/Eichstaett, last at 23:17:48 at Am Anger
|
||||
in Adelschlag, largest gap between two records 30 s, and three independent distance measures that
|
||||
agree — track 6,758 km, GNSS counter 6,877 km, CAN odometer 7,0 km.
|
||||
|
||||
What ended at 23:17:48 was the *supply*: the owner pulled the plug about 40 s after the car came to
|
||||
rest at home, with the engine still running (`run_time_since_engine_start` counted 144 to 632 s
|
||||
without a break). From then on there was no CAN, hence no RPM, hence — `ignition_detection.source`
|
||||
is 8, RPM only — an "ignition off" report 180 s later, which the device could only send at 23:24:27.
|
||||
|
||||
**Discarded along the way, each after measuring:** a loose OBD contact (the supply oscillation on
|
||||
03.09 was the *vehicle's* own charging behaviour — its `control_module_voltage` swung just as much);
|
||||
a vehicle-side power cut (a gateway blocks CAN, not 12 V, and the supply came back *while parked*,
|
||||
the opposite of any known behaviour); overcurrent (an FMM003 draws about 0,1 A against a 7,5–15 A
|
||||
fuse, and a blown fuse reads 0,00 V, not the observed 1,1 V); a firmware update (the GNSS counter
|
||||
ran through the event without a reset, 13,725 to 13,753 to 14,015); deep sleep during the drive.
|
||||
|
||||
**The 1,1 V is not the internal battery** — that is a separate AVL element (67, range 3,0–4,2 V)
|
||||
which this device does not send at all. It is the residual on a floating input after the plug comes
|
||||
out.
|
||||
|
||||
### Why nothing arrived for ten minutes — and it was not a radio hole
|
||||
|
||||
The device sends no network elements (signal strength, RSRP, cell) in its records, so "radio hole"
|
||||
was an assumption, not a finding. flespi's own connection log settled it: the session opened at
|
||||
23:12:57 stayed **open for 703 seconds and carried 16 messages**, then closed with `close_code 12` at
|
||||
23:24:37 — the same second in which the device opened a new one to a different flespi address, over
|
||||
which 35 messages went out in 119 s. The proof that the old session was dead in both directions:
|
||||
flespi pushed our own `sleep_mode` setting over it at 23:19:15 and got nothing for 5 1/2 minutes;
|
||||
over the fresh connection the same setting was confirmed in 17 seconds. The device kept writing into
|
||||
a socket that no longer carried anything, and recovered only because losing external power forced it
|
||||
to reinitialise. Pulling the plug is what delivered the data.
|
||||
|
||||
`/gw/devices/{id}/logs` is readable with the existing "device configuration" token (telemetry and
|
||||
messages are not — 403). **It is the fastest way to tell a coverage problem from a dead session** —
|
||||
use it before theorising.
|
||||
|
||||
**Confirmed from the device's own `config_24.cfg`** (gzip'd plaintext, `gunzip` reads it), which was
|
||||
the configuration running that evening — 1000 Open Link Timeout 60 s, 1001 Response Timeout 30 s,
|
||||
1002 Sort by = 1 (Oldest), **1003 Network Ping Timeout = 0, disabled**, and **1004 Acknowledgment
|
||||
Type = 0, TCP/IP**. With ACK over TCP/IP the device trusts its own stack instead of an
|
||||
application-level answer, so the 30-second Response Timeout never gets anything to act on; with
|
||||
Network Ping at 0 there is no independent liveness check either. There was no mechanism that *could*
|
||||
have noticed. Recommended to the owner: **ACK Type to AVL** and **Network Ping to 60 s**; 1000 and
|
||||
1001 unchanged.
|
||||
|
||||
The same file confirms **`400 = 180`** — `ZUENDUNG_NACHLAUF_S = 180` is right, and the earlier doubt
|
||||
("maybe 60, from flespi's `ign.stop`") is settled. Independently measured at three clean
|
||||
ignition-off events: 183 / 184 / 184 s. Also `11800 = 0`: the trip scenario is switched off in the
|
||||
device, so the 900-second `NACHLAUF_S` currently does not exist at all.
|
||||
|
||||
### The defect, and it is ours
|
||||
|
||||
**The trip window is in device time; the recorder is indexed by arrival time.** Since 2026-09-01
|
||||
(section AY) `ts_start`/`ts_end` come from `MELDEZEIT_SENSOR`, while `verlauf._rohverlauf()` stamped
|
||||
every point with `zustand.last_updated`. Two clocks — and the device buffers.
|
||||
|
||||
Measured lag *during drives*: 02.09. median 4 s (11 % over 60 s); 03.09. evening median 83 s
|
||||
(60 % over 60 s, worst 641 s). Over the whole recorder the median is 78 s. This is normal operation
|
||||
of a buffering device, not an exception.
|
||||
|
||||
Consequences, all from that one cause: 26 of 48 route points stored; 3,0 instead of 7 km; and the
|
||||
backlog arriving out of order (the current record first, then the buffer oldest-first), which
|
||||
`strecke_aus_zaehler()` correctly read as a counter reset and turned into 9,8 km.
|
||||
|
||||
**The fix sits at the one place every consumer passes through.** `verlauf_lesen()` takes an optional
|
||||
`meldezeit_id`; with it, every point carries the **device time** of the record it belongs to, and
|
||||
`wert_bei()`, `wert_ab()`, `zaehlerstrecke()`, `route_aus_verlauf()`, `hoechstwert_im_fenster()` and
|
||||
`naechster_wert()` all work in the same clock as the window. New: `NACHREICHFENSTER` (6 h — the read
|
||||
window must extend past `ende`, or the late records are never read at all), `ZUORDNUNG_MAX_S` (1 s;
|
||||
a point with no mark in reach keeps its arrival time, which also protects entities of *other*
|
||||
integrations), and the pure, testable `geraetezeit_zuordnen()`.
|
||||
|
||||
**Second riegel, from the same investigation:** `RUECKSETZ_ANTEIL = 0.5` in `strecke_aus_zaehler()`.
|
||||
A backward step only counts as a counter reset if the new value falls far below the old one. The
|
||||
real reset went 0,415 to 0; the ordering artefact went 6,891 to 6,848. Reading the latter as a reset
|
||||
booked the whole counter as distance — 87 metres became 13,7 km.
|
||||
|
||||
**Third: `ts_end` is clamped to the last sign of life.** `_letzter_datensatz()` in
|
||||
`fahrterkennung.py`. The nachlauf subtraction is a calculation, not a measurement, and on 03.09 it
|
||||
placed the end four minutes after the last record that exists.
|
||||
|
||||
**Proven against the real recording**, read-only, with the production functions: CAN odometer 3,0 to
|
||||
**7,0 km** (21332 to 21339), GNSS counter 2,952 to **6,877 km**, route 26 to **47 points** with the
|
||||
last one in Adelschlag, trip end 23:21:57 (736 s) to **23:17:48** (487 s).
|
||||
|
||||
12 regression tests in `tests/verlauf/test_geraetezeit.py`; all 60 backend tests green. Deployed to
|
||||
`audi_ha_test`, clean restart, and it immediately gave two trips a distance that had **none** before
|
||||
(`t-f5837cd8160b` 6,958 km and `t-98425e4f140e` 6,857 km, both previously rejected as a backward
|
||||
jump).
|
||||
|
||||
**What it does not do:** repair `t-66c4209276e7`. The screening only ever fills empty fields, so the
|
||||
stored 3,0 km and the wrong end stay until someone clears them — the owner's data and their call.
|
||||
|
||||
### Tooling note that cost real time
|
||||
|
||||
`.replace(/.$/, "")` — an unescaped dot, which strips *any* last character — was in both codebases
|
||||
("Gelesen vor 3 Tag."). Fixing it through a Node script written by a quoted heredoc failed silently:
|
||||
a doubled backslash in the heredoc arrives single in the JS source and vanishes in the string, so
|
||||
search and replacement were identical and the script reported success. **Build a backslash as
|
||||
`String.fromCharCode(92)`, or pass the strings through a JSON file, and always verify afterwards
|
||||
with `grep -c -F`.**
|
||||
|
||||
### Parity findings of 2026-09-03/04, all resolved
|
||||
|
||||
Source-level comparison of routes, services, visible strings, map code and tile order. Clean: 24
|
||||
routes identical, all 28 services wired on the correct side (`entitaeten_schreiben` is panel-only by
|
||||
the parity rule's own carve-out; `screening_jetzt` is called by neither frontend).
|
||||
|
||||
1. **`replace(/.$/, "")`** — real bug, both sides (see above).
|
||||
2. **Dongle tile wording** — the app said "15 ist unterwegs zum Geraet" and "wir rechnen mit 15", the
|
||||
panel "-> 15, unterwegs" and "(wir: 15)". Aligned to the panel; `Wertzeile` gained a `warnung`
|
||||
prop so a deviating value is coloured `var(--warn)` as it is in the panel.
|
||||
3. **"Zugaenge" tile** sat tenth in the app, second in the panel. Moved.
|
||||
4. **Einzelbeleg map** — the app drew the generic black start dot with a "Start" tooltip. Now the CI
|
||||
`poi` pin with its silhouette (verbatim from the panel), a popup with station name and address,
|
||||
and the centre button; new `.dm-kartenbox`, `.dm-kartensteuerung--klein`, `.dm-nadel`.
|
||||
5. **Einzelfahrt map** — `bindTooltip("Start")` became `bindPopup("Start · " + address)` as in the
|
||||
panel. A tooltip is practically invisible on a phone, and the address is the point.
|
||||
6. **Einzelfahrt row order** — Verbrauch moved behind Hoechstgeschwindigkeit.
|
||||
7. **PDF rejection** — "Bitte den Beleg als PDF auswaehlen." to "Nur PDF-Belege werden unterstuetzt".
|
||||
8. **Token dialog** — "Token fuer Gitea loeschen?" to "Token loeschen?"; footnote aligned.
|
||||
9. **"Fahrt beenden"** in the panel had no loading state, unlike its own dongle button. Now
|
||||
`FAHRTENDE_LAEUFT` with "Wird beendet ...", the same mechanism.
|
||||
|
||||
Still open, unchanged: `.dm-fussnote` across 35 sites; `Versicherung.tsx`'s sub-screens never
|
||||
individually re-verified; the unreproduced map-layer icon report; the unreproduced "Batteriespannung
|
||||
row not clickable on desktop".
|
||||
|
||||
### Battery history is contaminated by bench measurements
|
||||
|
||||
Found while clarifying that the 12,2 V after the drive was the owner's desk power supply.
|
||||
`batterie.py` only checks 10–15,5 V, and a power supply at 12,2 V passes. Checked every stored daily
|
||||
minimum against whether the vehicle was reporting over CAN within plus/minus 5 minutes: **3 of 16
|
||||
days pass, 13 do not** — including 2026-09-03 (12,149 V at 23:56, the desk). These values feed the
|
||||
resting voltage, the SOC, the SOH trend and the fallback shown in "Mein Audi / Zustand".
|
||||
|
||||
**Not built, proposed:** require a fresh CAN value near the voltage reading before recording it —
|
||||
the same mechanism as `wert_in_naehe()` with its 300 s window for the outside temperature. Without a
|
||||
vehicle, nothing gets recorded. The 13 existing entries are the owner's data; removal is via the
|
||||
swipe gesture in the measurement list.
|
||||
|
||||
Reference in New Issue
Block a user