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 [...]
vehicle_condition_fusion #
Safety-calibrated fusion of a vehicle's own signals into a driving-condition hazard verdict, with an honest-degradation rail. Pure Dart — no Flutter, no databroker SDK, no protobuf, no gRPC.
When the network and GPS are gone (the compound-failure worst case), a car still
knows things about the road from its own sensors: the ESC's road-friction
estimate, whether TCS/ABS are engaging, wiper / rain-sensor intensity, and
ambient air temperature. This package fuses those signals — deterministically
and typed — into the same DrivingConditionAssessment (driving_conditions)
that drives road-surface classification, grip, and visibility, so an edge
developer can turn raw vehicle telemetry into a glanceable, safety-calibrated
hazard picture and unit-test it with a one-line mock.
Try it in 2 minutes (no databroker, no vehicle) #
You can drive the fusion through a full Akita heavy-snow / low-visibility
drive with just a laptop — the package ships an opt-in scenarios.dart
library of hand-authored synthetic winter traces and a one-line replay
helper. Two ways to run it:
A. From your own project (pub add) #
$ dart pub add vehicle_condition_fusion
pub add installs the package as a dependency — it does not copy this
repo's example/ into your project. So drop this self-contained snippet into
bin/demo.dart and run it with dart run bin/demo.dart:
import 'package:vehicle_condition_fusion/scenarios.dart';
import 'package:vehicle_condition_fusion/vehicle_condition_fusion.dart';
Future<void> main() async {
// Replay the bundled SYNTHETIC Akita whiteout through the published fusion.
// Swap replayWinterDrive() for your own Stream<VehicleConditionSignals>
// (a KUKSA adapter, a CAN reader, a test) — same fusion.
final fusion = VehicleConditionFusion.fromPartialFrames(
partialFrames: replayWinterDrive(akitaWhiteoutDrive),
);
await for (final update in fusion.conditions) {
if (update.isAvailable) {
final a = update.assessment!;
print('${a.surfaceState.name.padRight(13)} ${a.advisoryMessage}');
} else {
print('— drive ended (${update.unavailableReason}) —');
break;
}
}
await fusion.dispose();
}
It replays the bundled synthetic drive, printing the surface verdict and advisory as the hazard escalates and clears:
dry Conditions normal
dry Conditions normal
dry Conditions normal
compactedSnow Compacted snow — use winter tyres, reduce speed
compactedSnow Compacted snow — use winter tyres, reduce speed
blackIce Black ice risk — reduce speed significantly
blackIce Black ice risk — reduce speed significantly
blackIce Black ice risk — reduce speed significantly
slush Slushy conditions — maintain safe following distance
slush Slushy conditions — maintain safe following distance
wet Wet road — increased stopping distance
wet Wet road — increased stopping distance
— drive ended (vehicle signal stream ended) —
B. Run the shipped example (from a repo clone) #
The richer example — per-step inputs, the debounced surface verdict, grip, a
visibility cue, and a (held) marker on debounce-held steps — runs from a
clone of this repository (it is also what appears on the pub.dev Example
tab). pub add does not install it, so:
$ git clone https://github.com/aki1770-del/SNGNav
$ cd SNGNav/packages/vehicle_condition_fusion
$ dart pub get
$ dart run example/main.dart
You will see the hazard escalate and clear, step by step (the whole
assessment is debounced, so fresh inputs sit beside a (held) verdict while
the surface state has not yet persisted):
step 1 fric= 0.95 temp= -1°C tcs=off wiper=0 → dry grip=1.00
vis: clear (~10 km) Conditions normal
step 2 fric= 0.95 temp= -1°C tcs=off wiper=0 → dry grip=1.00
vis: clear (~10 km) Conditions normal
step 3 fric= 0.45 temp= -3°C tcs=off wiper=3 → dry grip=1.00 (held)
vis: clear (~10 km) Conditions normal
step 4 fric= 0.45 temp= -3°C tcs=off wiper=4 → compactedSnow grip=0.30
vis: reduced (~800 m) Compacted snow — use winter tyres, reduce speed
step 5 fric= 0.18 temp= -6°C tcs=ON wiper=5 → compactedSnow grip=0.30 (held)
vis: reduced (~800 m) Compacted snow — use winter tyres, reduce speed
step 6 fric= 0.18 temp= -6°C tcs=ON wiper=5 → blackIce grip=0.15
vis: LOW (~300 m) Black ice risk — reduce speed significantly
step 7 fric= 0.18 temp= -6°C tcs=ON wiper=5 → blackIce grip=0.15
vis: LOW (~300 m) Black ice risk — reduce speed significantly
step 8 fric= 0.55 temp= -2°C tcs=off wiper=2 → blackIce grip=0.15 (held)
vis: LOW (~300 m) Black ice risk — reduce speed significantly
step 9 fric= 0.55 temp= -1°C tcs=off wiper=1 → slush grip=0.50
vis: clear (~5 km) Slushy conditions — maintain safe following distance
step 10 fric= 0.85 temp= 3°C tcs=off wiper=2 → slush grip=0.50 (held)
vis: clear (~5 km) Slushy conditions — maintain safe following distance
step 11 fric= 0.85 temp= 4°C tcs=off wiper=2 → wet grip=0.70
vis: clear (~5 km) Wet road — increased stopping distance
step 12 fric= 0.85 temp= 4°C tcs=off wiper=2 → wet grip=0.70
vis: clear (~5 km) Wet road — increased stopping distance
— drive ended — fusion now reports: vehicle signal stream ended
(no live signals → honest "unavailable", never a fabricated road)
Then swap in your own signal source. The fusion's input seam is a plain
Stream<VehicleConditionSignals>, so replace replayWinterDrive(...) with your
KUKSA databroker adapter, your CAN reader, or any source that produces
VehicleConditionSignals:
final fusion = VehicleConditionFusion.fromPartialFrames(
partialFrames: myKuksaAdapter.signals, // your real source — same fusion
);
⚠️ The bundled traces are ILLUSTRATIVE / SYNTHETIC — hand-authored plausible values chosen to exercise the hazard rules, not recordings of any real vehicle, drive, or storm. They are a teaching fixture to lower the floor; they are not evidence about real roads. The
scenarios.dartlibrary is a separate, opt-in import and is intentionally not part of the main barrel, so the safety-calibrated fusion is untouched — the traces only feed it.
What it does #
VehicleConditionSignals— a typed, all-nullable snapshot of the snow-safety signals (roadFriction,tcsEngaged,absEngaged,escEngaged,wiperIntensity,rainIntensity,airTempC,speedKmh). A signal the vehicle has not published isnull, never a fabricated default.vehicleSignalsToWeatherCondition(...)— a pure, total, deterministic mapping to aWeatherCondition(no LLM, no prose, no ad-hoc heuristic), with named calibration thresholds:kIcyFrictionThreshold(0.3),kColdSlipCelsius(2.0),kAssumedAboveFreezingCelsius(5.0).VehicleConditionFusion— a stream processor:Stream<VehicleConditionSignals>→Stream<VehicleConditionUpdate>. It fuses viaDrivingConditionAssessment.fromCondition, debounces road-surface flicker with the existingHysteresisFilter, and degrades honestly. It has two source rails, differing only in how anullfield on a later frame is read: the default constructor treats each snapshot as a complete picture (anullmeans "genuinely unknown now" — for a CAN reader / sensor fusion / tests), whileVehicleConditionFusion.fromPartialFramesis for a partial-frame transport (KUKSAsubscribe) and carries forward the last-known value per signal so a once-seen ice signal is not dropped (see the ⚠️ safety note below).
import 'dart:async';
import 'package:vehicle_condition_fusion/vehicle_condition_fusion.dart';
final source = StreamController<VehicleConditionSignals>();
final fusion = VehicleConditionFusion(signals: source.stream);
fusion.conditions.listen((u) {
if (u.isAvailable) {
print(u.assessment!.surfaceState); // e.g. RoadSurfaceState.blackIce
} else {
print('no live vehicle signals: ${u.unavailableReason}');
}
});
source.add(const VehicleConditionSignals(roadFriction: 0.2, airTempC: -5));
Calibration rules (preserved verbatim from the source pipeline) #
- temperature = ambient air temp, or
kAssumedAboveFreezingCelsius(5 °C) when absent — a missing temperature never fabricates ice. - precipitation present iff wiper/rain-sensor say so; type is
snowwhen temperature ≤ 0 °C elserain(temperature disambiguates what the wiper cannot). - ice risk iff a direct road measurement says so — friction below
kIcyFrictionThreshold, or TCS/ABS/ESC engaged at/belowkColdSlipCelsius.
Honest bounds — read this #
- The visibility value is a DOCUMENTED PROXY, not a measurement. A vehicle
has no meteorological visibility sensor; the visibility metres here are
derived from precipitation intensity (wiper / rain-sensor) as an explicit,
typed cue. Treat it as a glanceable hint, not a sensor reading. Because it is
a precipitation-intensity proxy only — there is no wind signal in the set,
so
windSpeedKmhis hard-set to0.0— it cannot represent a wind-driven whiteout (地吹雪 / ground blizzard), where blowing snow collapses visibility with little or no falling precipitation. The bundled severe phase is therefore a heavy-snow, low-visibility drive (~300 m proxy), not a true whiteout. - Reads only — never commands. This package only interprets signals; it never writes to or commands the vehicle.
- Offline / vehicle-local. No network, no GPS, no cloud weather — just the signals you feed it.
- No fabrication. A snapshot with no real signal is not emitted; a source
error or end-of-stream surfaces a
VehicleConditionUpdate.unavailablemarker so the caller can keep its offline default and stop claiming "live".
The input seam — bring any source #
The input is VehicleConditionSignals, decoupled from any transport. Produce it
from:
- a KUKSA databroker adapter — your adapter decodes each raw frame to a
VehicleConditionSignals(keeping the SDK / protobuf in your adapter, this package SDK-free) and drivesVehicleConditionFusion.fromPartialFrames, which does the partial-frame carry-forward for you (see the ⚠️ safety note below), - your own CAN reader (complete snapshots → the default constructor), or
- a test —
const VehicleConditionSignals(roadFriction: 0.2, airTempC: -5).
That is the whole point: you can unit-test the full hazard fusion with a plain
StreamController<VehicleConditionSignals> and direct construction — no
kuksa_dart_sdk import, no protobuf, no running databroker.
From a KUKSA databroker (VSS paths) #
If your source speaks standard COVESA VSS (Vehicle Signal Specification,
v6.0) — as a KUKSA databroker does — you do not even write the per-field
mapping. A KUKSA get / subscribe yields a map of {VSS-leaf-path: value};
hand it straight to VehicleConditionSignals.fromVss(...):
final signals = VehicleConditionSignals.fromVss({
'Vehicle.ADAS.ESC.RoadFriction.MostProbable': 18.0, // percent → 0.18
'Vehicle.ADAS.TCS.IsEngaged': true,
'Vehicle.ADAS.ABS.IsEngaged': false,
'Vehicle.ADAS.ESC.IsEngaged': true,
'Vehicle.Exterior.AirTemperature': -6.0, // °C
'Vehicle.Speed': 35.0, // km/h
'Vehicle.Body.Windshield.Front.Wiping.Intensity': 4, // setpoint, no fixed max
'Vehicle.Body.Raindetection.Intensity': 70, // percent
});
The map values are the decoded scalars your KUKSA client yields — a live
client returns typed Datapoints, so you pass datapoint.value (a bool /
int / double), not the wrapper.
- The leaves it reads are exactly
VehicleConditionSignals.recognizedVssPaths(use it to build yoursubscriberequest); unknown / extra keys are ignored. Windshieldis an instanced branch (["Front", "Rear"]) in VSS, so the deployed databroker key is the Front-instance path (Vehicle.Body.Windshield.Front.Wiping.Intensity).ESC.RoadFriction.MostProbableis a percent in VSS, so it is divided by 100 and clamped to0.0..1.0;Raindetection.Intensity(a SENSOR, percent) is clamped0..100;Windshield.Front.Wiping.Intensityis an actuator setpoint with no fixed VSS max, so it is clamped>= 0only (a coarse precipitation cue, not a measured value).- 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. - No fabrication: a path that is absent,
null, or holds a non-coercible value (including a non-finiteNaN/Infinity) leaves that fieldnull— a missing or garbage leaf is never a default, and a malformed frame degrades tonullinstead of throwing. Vehicle.ADAS.ESC.IsEngagedis mapped toescEngagedand, like TCS/ABS, contributes to cold-slip ice risk.- Intentionally not mapped: the
Vehicle.Exterior.RoadSurfaceConditionfamily (VSSmaster/ unreleased v7, not in any released spec).
For a KUKSA subscribe (a partial-frame transport — only changed leaves are
re-sent), build a fromVss snapshot per frame and feed
VehicleConditionFusion.fromPartialFrames, which carries the last-known value
forward so a once-seen ice signal is not under-warned (see the ⚠️ safety note
below):
final fusion = VehicleConditionFusion.fromPartialFrames(
partialFrames: kuksaFrames.map(VehicleConditionSignals.fromVss),
);
Runnable end-to-end bridge: example/kuksa_databroker.dart
wires a real KuksaClient.subscribe(...) into this fusion and ships an
in-process fake source so it runs on a laptop with no databroker —
dart run example/kuksa_databroker.dart prints an escalating black-ice verdict,
shows partial-frame carry-forward and garbage-frame honesty, then an honest
unavailable on disconnect. Go live with
dart run example/kuksa_databroker.dart --live localhost:55555.
⚠️ SAFETY — partial-frame transports MUST carry forward; use the
fromPartialFramesrail. On the documented-primary KUKSA path,subscribere-sends only the signals that changed after the first cycle. If those partial frames are fed to the default constructor (which treats every snapshot as complete),roadFriction,tcsEngaged/absEngaged/escEngaged(and the other ice-risk signals) drop tonullthe moment they rotate out of a frame — even while the real ice hazard on the road persists unchanged. Because this processor honestly refuses to fabricate from a missing signal, that would fail toward NO ice warning while a real ice hazard persists — intermittently as signals flicker, or sustainedly if an ice signal stops re-firing. That is the driver-dangerous direction: the snapshot looks safe exactly when the road is not.The rail that closes this gap is
VehicleConditionFusion.fromPartialFrames: it maintains a running merged snapshot, carrying forward the last-known value per signal (viaVehicleConditionSignals.carriedForwardOnto) before running the identical fusion pipeline — so a once-seen ice signal is held across later partial frames.// KUKSA / partial-frame transport: the rail carries forward for you. final fusion = VehicleConditionFusion.fromPartialFrames( partialFrames: adapter.signals, // each event may re-send only what changed );Pick the rail by your transport's framing semantics: use
fromPartialFramesfor KUKSAsubscribe(anullmeans "unchanged / not re-sent"); use the default constructor for complete snapshots — a CAN reader or sensor fusion where anullgenuinely means "no longer valid" — which does not carry forward and so never over-warns on retracted data (see What it does NOT do).
Attribution / no affiliation. VSS (Vehicle Signal Specification) is a COVESA standard and KUKSA is an Eclipse Foundation project. This package is an independent, SDK-free consumer of those open standards — it is not affiliated with, nor endorsed by, COVESA, the Eclipse Foundation, or the KUKSA project.
What it does NOT do #
- It is not a transport. It does not connect to or subscribe from a
databroker — that is an adapter's job (and keeps this package SDK-free). Your
adapter decodes raw frames to
VehicleConditionSignals; the partial-frame carry-forward that KUKSAsubscriberequires is then provided for you by theVehicleConditionFusion.fromPartialFramesrail (it is a safety obligation, not a stylistic split — see the ⚠️ note above). The carry-forward is rail-selected rather than always-on for a reason: the default complete-snapshot rail treats every snapshot as a complete picture, so a non-KUKSA source can null a field to mean "no longer valid"; carrying that forward would make such a source over-warn on stale data — the opposite divergence. The carry-forward is correct only when the source's framing semantics are known, which is why you pick the rail. - It is not a substitute for real sensors or real weather. It interprets the signals you give it; garbage in, garbage out. It asserts ice only from a direct friction/traction measurement, never from a missing signal.
- It does not classify road surfaces itself — it reuses
driving_conditions' existing classifier rather than duplicating it.
License / safety #
Display and advisory only (ASIL-QM) — no vehicle control.