vehicle_condition_fusion 0.3.3
vehicle_condition_fusion: ^0.3.3 copied to clipboard
Safety-calibrated fusion of in-vehicle signals (road friction, TCS/ABS, wiper/rain, ambient temperature) into a driving-condition hazard verdict, with an honest-degradation rail. Pure Dart, no Flutter [...]
Changelog #
0.3.3 #
Safety fix — a broken friction sensor no longer reports PERFECT GRIP.
fromVss divided the VSS road-friction percent by 100 and then clamped the
result into 0.0..1.0. A finite value outside the spec — most commonly the
classic 255 "no reading" sentinel a broken ESC or CAN bridge emits — therefore
became:
(255 / 100).clamp(0.0, 1.0) == 1.0
…and roadFriction: 1.0 does not mean "no reading". It means a measured,
perfect-grip road. Every caller reading VehicleConditionSignals.roadFriction
— to display it, to log it, to feed its own logic — was handed a confident
measurement manufactured out of a dead sensor.
A finite reading outside 0..100 is now absent (null). The producer is
broken, and the honest answer is that we do not know the road's grip. It is never
clamped: clamping an out-of-range value to the maximum asserts perfect grip on a
road nobody measured, which is precisely the failure this seam exists to prevent.
This is the same rule kuksa_dart_sdk's RoadFriction already enforces on the
producer side, where an out-of-spec value is RoadGrip.unknown and never coerced.
Honest scope — what this does NOT change #
On the 0.3.x line WeatherCondition.iceRisk is a plain bool, so it could not
say "unknown" either before or after this patch: a 255 reading yielded
iceRisk: false before (via the fabricated 1.0) and yields iceRisk: false
now (via absence). The ice verdict on this line is unchanged. What is fixed is
the fabricated measurement itself — the public roadFriction field. Modelling
"we did not measure the road" as a first-class value requires the tri-state
bool? iceRisk, which is a breaking change and lands on the next minor line.
The test that certified the bug #
test/vss_adapter_test.dart contained test('friction clamp: 150% → 1.0, -5% → 0.0') — a test that asserted the fabrication was correct, sitting fifteen
lines above another test refusing the identical harm for NaN ("would assert MAX
grip"). The suite was green because the defect had a test defending it. That test
is now inverted, and covers 255.0 / 150.0 / 100.5 / -5.0 / -0.1.
Compatibility #
No API change. Source- and binary-compatible with 0.3.2; a pure narrowing of
behaviour (values that were previously fabricated into a measurement now correctly
read as absent). ^0.3.x consumers receive it on the next pub get.
0.3.2 #
Safety fix — an absent ambient temperature no longer hides an ice hazard.
Up to and including 0.3.1, a vehicle that published no ambient temperature
(Vehicle.Exterior.AirTemperature absent, airTempC == null) was treated as if
the temperature were +5 °C — above freezing. Because the cold-slip ice rule
required a temperature at or below +2 °C, a car that was actively losing
traction (TCS, ABS, or ESC engaged) on a road whose temperature it did not
publish was classified as iceRisk: false — reported as aquaplaning, not
ice.
What you may have been shown: on such a vehicle, a real traction-loss event with
no temperature signal produced a non-icy road-surface verdict (e.g. wet
instead of blackIce) and no black-ice advisory, while the car was skidding.
- An absent temperature is now treated as unknown, not warm. A present traction-loss event (TCS/ABS/ESC engaged) keeps the ice concern when the temperature is unknown — absence never downgrades a hazard (caution-add-only). When a real temperature is published, behaviour is unchanged: a genuine warm-road traction-loss (e.g. +12 °C) is still not classified as ice.
- The friction path is unchanged (it was always temperature-independent): a low-friction reading still asserts ice with or without a temperature.
kAssumedAboveFreezingCelsiusis now@Deprecated. It is still exported and still5.0(nothing removed, no signature changed), but it no longer decides ice risk. It is retained only to fill the non-nullable temperature field and disambiguate rain-vs-snow when no ice hazard is at stake.
Known residual (needs a breaking type change, tracked for the next x.y line):
the downstream WeatherCondition.temperatureCelsius field is non-nullable, so an
absent temperature is still back-filled for the rain-vs-snow split. A
freezing-rain road with a dead temperature sensor and no friction/traction
signal can still be classified wet. Modelling temperature-absence as a
first-class value resolves it but changes the type contract.
Migration: none required — this release is source-compatible. If your code reads
kAssumedAboveFreezingCelsius, expect a deprecation notice; the value is
unchanged.
0.3.1 #
- Runnable KUKSA-databroker bridge example —
example/kuksa_databroker.dart. Shows the whole adapter as a four-line map chain:client.subscribe(VehicleConditionSignals.recognizedVssPaths)→ decode eachDatapointto its scalar.value→fromVssper frame →VehicleConditionFusion.fromPartialFrames(carry-forward rail).- It imports the real
kuksa_dart_sdk(an example-only dev-dependency — the published package stays completely SDK-free), so the liveclient.subscribe(...)bridge is compile-checked against the actual SDK types.--live host:portconstructs a realKuksaClient. - The default
dart runuses an in-process fake source shaped exactly like the decodedsubscribeyield, so it runs end-to-end with no databroker. It demonstrates: an escalating black-ice verdict; partial-frame carry-forward (a speed-only frame still reports black ice); garbage-frame honesty (a non-finite friction degrades tonull, never a fabricated max grip, and cannot erase a once-seen ice hazard); and honest degradation (a mid-drive disconnect yields an explicitunavailable, never the stale verdict). - Behavioural tests in
example/test/kuksa_databroker_test.dartassert each of those properties.
- It imports the real
- Docs-only / additive: the package
liband its public API are unchanged.
0.3.0 #
- Zero-glue KUKSA on-ramp —
VehicleConditionSignals.fromVss(...). A new factory that maps standard COVESA VSS (v6.0) leaf paths straight to the typed fields, so a KUKSA databroker user can hand the decoded{path: value}map (what aget/subscribeyields) in directly with no hand-written per-field glue.- The exact paths read are exposed as
VehicleConditionSignals.recognizedVssPaths(and individually asvssRoadFriction,vssTcsEngaged, … constants) for building asubscriberequest and for tests. - The wiper leaf is the Front-instance path
Vehicle.Body.Windshield.Front.Wiping.Intensity—Windshieldis an instanced branch (["Front", "Rear"]) in VSS, so this is the deployed databroker key. It is an actuator setpoint (uint8, no fixed max), a coarse precipitation cue; the measured precipitation channel is the separateRaindetection.Intensitysensor. ESC.RoadFriction.MostProbableis a PERCENT in VSS, so it is divided by 100 and clamped to0.0..1.0;Raindetection.Intensityis rounded/clamped to0..100;Windshield.Front.Wiping.Intensity(no fixed VSS max) is clamped>= 0only.- Honest by construction: an absent /
null/ non-coercible leaf leaves that fieldnull(never a fabricated default), and a garbage frame degrades tonullrather than throwing — a malformed leaf cannot crash the pipeline. A non-finite numeric (NaN/Infinity) is treated as garbage →null(notably, aNaNfriction can no longer fabricate1.0max grip). - The VSS-typed boolean leaves (
TCS/ABS/ESC.IsEngaged) accept a Dartbool, or an int1/0(faithful decoding of abooleanleaf from a CAN bridge / non-SDK source, so a traction-loss ice signal is not silently dropped); numeric leaves never accept abool. - For KUKSA
subscribe(partial frames), build afromVssper frame and feedVehicleConditionFusion.fromPartialFrames(carry-forward rail).
- The exact paths read are exposed as
- New
escEngagedfield + ESC now contributes to cold-slip ice risk. Added an additive (non-breaking)bool? escEngagedtoVehicleConditionSignals(mapped fromVehicle.ADAS.ESC.IsEngaged).Vehicle.ADAS.ESC.IsEngagedis as strong an ice indicator as TCS/ABS, so — like them — an engaged ESC at/belowkColdSlipCelsiusnow also contributes to ice risk invehicleSignalsToWeatherCondition. - Additive and non-breaking: the existing fields, the classifier semantics for prior signals, and every existing public API are unchanged; still pure Dart, no Flutter, no databroker SDK, no protobuf, no new dependency.
0.2.0 #
- Lower the floor — try it with just a laptop. Added an opt-in
scenarios.dartlibrary (a SEPARATE import, intentionally NOT in the main barrel, so the safety-calibrated fusion is untouched) with:replayWinterDrive(frames, {step})— replays a list ofVehicleConditionSignalsas a stream (partial-frame style, to pair withVehicleConditionFusion.fromPartialFrames);step: Duration.zero(default) is instant for tests, a non-zero step paces a live demo.- named ILLUSTRATIVE / SYNTHETIC traces —
akitaWhiteoutDrive(dry → compacted snow → black-ice whiteout → slush → wet),blackIcePatch(cold-slip TCS ice path), andclearRoad(benign). These are hand-authored plausible values, not recorded sensor data.
- Added a runnable
example/main.dartthat replaysakitaWhiteoutDrivethrough the published fusion and prints the evolving assessment (surface state, grip, visibility cue, advisory) per step — so a developer SEES the hazard escalate and clear with no databroker and no vehicle. - Added
test/scenarios_test.dartpinning that the replay emits its frames and that each synthetic trace actually yields the hazard progression it claims (including that carry-forward HOLDS the whiteout across a speed-only frame). - README: a "Try it in 2 minutes" on-ramp.
- Additive and non-breaking: the core API and the safety-calibrated fusion are unchanged; still pure Dart, no Flutter, no vehicle-SDK dependency.
0.1.0 #
- Initial release. Deterministic, safety-calibrated fusion of vehicle signals
(road friction, TCS/ABS engagement, wiper/rain intensity, ambient temperature)
into a
DrivingConditionAssessment. Ice-risk is asserted only from a direct road measurement, a missing temperature never fabricates ice, and the visibility value is a documented precipitation proxy (a vehicle has no meteorological visibility sensor). - Honest degradation: a dead or absent source surfaces
unavailableand never a fabricated condition; no emission while no signal is decodable. - Two transport rails so the safe behavior is the default per source type: the
default constructor for complete snapshots, and
VehicleConditionFusion.fromPartialFramesfor a partial-frame transport (e.g. KUKSAsubscribe) that carries the last-known value forward, so a once-seen hazard is not under-warned on a later partial frame. - Pure Dart — no Flutter and no vehicle-SDK dependency. The input seam is a plain
VehicleConditionSignals, so it mocks with a single constructor (no protobuf, no databroker).