@ziztechnology/dial-library 0.0.15 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +131 -42
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const dialSdkDebugWarn = (moduleName, scope, phase, data) => {
|
|
|
40
40
|
const message = `[DIAL_SDK_DEBUG - ${moduleName}] ${scope}: ${phase}`;
|
|
41
41
|
try {
|
|
42
42
|
if (!hasLoggedSdkVersion) {
|
|
43
|
-
console.log(`[DIAL_SDK_DEBUG] SDK version: 0.0.
|
|
43
|
+
console.log(`[DIAL_SDK_DEBUG] SDK version: 0.0.16`);
|
|
44
44
|
hasLoggedSdkVersion = true;
|
|
45
45
|
}
|
|
46
46
|
if (data === void 0) console.log(message);
|
|
@@ -148,6 +148,25 @@ const isRecord = (value, debug) => {
|
|
|
148
148
|
return result;
|
|
149
149
|
};
|
|
150
150
|
//#endregion
|
|
151
|
+
//#region src/driving_expression/directional_longitudinal_tuning.ts
|
|
152
|
+
const BRAKING_ENTRY_OFFSET = .3;
|
|
153
|
+
const BRAKING_EXIT_OFFSET = .15;
|
|
154
|
+
const SUDDEN_BRAKING_PEAK_OFFSET = .4;
|
|
155
|
+
const SUDDEN_BRAKING_MEAN_OFFSET = .3;
|
|
156
|
+
const resolveDirectionalLongitudinalTuning = (tuning) => ({
|
|
157
|
+
accelerationThreshold: tuning.longitudinalThreshold,
|
|
158
|
+
accelerationExitThreshold: tuning.longitudinalExitThreshold,
|
|
159
|
+
brakingThreshold: tuning.longitudinalThreshold + BRAKING_ENTRY_OFFSET,
|
|
160
|
+
brakingExitThreshold: tuning.longitudinalExitThreshold + BRAKING_EXIT_OFFSET,
|
|
161
|
+
rapidAccelerationPeakThreshold: tuning.rapidLongitudinalPeakThreshold,
|
|
162
|
+
rapidAccelerationMeanThreshold: tuning.rapidLongitudinalMeanThreshold,
|
|
163
|
+
suddenBrakingPeakThreshold: tuning.rapidLongitudinalPeakThreshold + SUDDEN_BRAKING_PEAK_OFFSET,
|
|
164
|
+
suddenBrakingMeanThreshold: tuning.rapidLongitudinalMeanThreshold + SUDDEN_BRAKING_MEAN_OFFSET,
|
|
165
|
+
normalBrakingMinimumDominance: .8,
|
|
166
|
+
suddenBrakingMinimumDominance: 1.25,
|
|
167
|
+
maximumBrakingGravityDirectionChangeRad: Math.PI / 60
|
|
168
|
+
});
|
|
169
|
+
//#endregion
|
|
151
170
|
//#region src/driving_expression/check_status.ts
|
|
152
171
|
const DEBUG_MODULE$4 = "check_status.ts";
|
|
153
172
|
const DEVICE_RIGHT = {
|
|
@@ -161,7 +180,7 @@ const ZERO_VECTOR$1 = {
|
|
|
161
180
|
z: 0
|
|
162
181
|
};
|
|
163
182
|
const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
|
|
164
|
-
version: "driving-status-calibrated-2026-07-17-
|
|
183
|
+
version: "driving-status-calibrated-2026-07-17-r3",
|
|
165
184
|
maxSampleAgeMs: 1e3,
|
|
166
185
|
gravityFilterTimeConstantMs: 1e3,
|
|
167
186
|
biasFilterTimeConstantMs: 1e4,
|
|
@@ -382,8 +401,9 @@ const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TU
|
|
|
382
401
|
return result;
|
|
383
402
|
};
|
|
384
403
|
const classifyInstantaneousEvidence = (metrics, tuning) => {
|
|
404
|
+
const directionalTuning = resolveDirectionalLongitudinalTuning(tuning);
|
|
385
405
|
const longitudinalMagnitude = Math.abs(metrics.longitudinal);
|
|
386
|
-
if (longitudinalMagnitude >=
|
|
406
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.suddenBrakingPeakThreshold : directionalTuning.rapidAccelerationPeakThreshold)) {
|
|
387
407
|
if (Math.abs(metrics.vertical) > longitudinalMagnitude * tuning.verticalBumpRatio) return {
|
|
388
408
|
candidateStatus: null,
|
|
389
409
|
rejectionReason: "vertical_bump"
|
|
@@ -397,7 +417,7 @@ const classifyInstantaneousEvidence = (metrics, tuning) => {
|
|
|
397
417
|
candidateStatus: metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN",
|
|
398
418
|
rejectionReason: null
|
|
399
419
|
};
|
|
400
|
-
if (longitudinalMagnitude >=
|
|
420
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.brakingThreshold : directionalTuning.accelerationThreshold)) return {
|
|
401
421
|
candidateStatus: metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING",
|
|
402
422
|
rejectionReason: null
|
|
403
423
|
};
|
|
@@ -1407,6 +1427,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1407
1427
|
this.unavailableSinceMs = null;
|
|
1408
1428
|
this.fallbackApplied = false;
|
|
1409
1429
|
this.filteredGravity = null;
|
|
1430
|
+
this.rawGravity = null;
|
|
1410
1431
|
this.lastGravitySampledAtMs = null;
|
|
1411
1432
|
this.linearAccelerationBias = { ...ZERO_VECTOR };
|
|
1412
1433
|
this.gyroscopeBias = { ...ZERO_VECTOR };
|
|
@@ -1476,6 +1497,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1476
1497
|
this.pollIntervalMs = requirePositiveFinite("pollIntervalMs", options.pollIntervalMs ?? 200, REQUIRE_POSITIVE_FINITE_DEBUG);
|
|
1477
1498
|
this.unavailableFallbackMs = requirePositiveFinite("unavailableFallbackMs", options.unavailableFallbackMs ?? 3e3, REQUIRE_POSITIVE_FINITE_DEBUG);
|
|
1478
1499
|
this.tuning = resolveDrivingStatusTuning(options.tuning);
|
|
1500
|
+
this.directionalLongitudinalTuning = resolveDirectionalLongitudinalTuning(this.tuning);
|
|
1479
1501
|
this.clock = options.clock ?? systemClock;
|
|
1480
1502
|
this.onDiagnostic = options.onDiagnostic;
|
|
1481
1503
|
this.managePageLifecycle = options.managePageLifecycle ?? true;
|
|
@@ -1488,6 +1510,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1488
1510
|
this.status = initialStatus;
|
|
1489
1511
|
this.baseStatus = isBaseStatus(initialStatus) ? initialStatus : "STOPPED";
|
|
1490
1512
|
this.statusCommittedAtMs = this.clock.now();
|
|
1513
|
+
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "resolved directional longitudinal tuning", this.directionalLongitudinalTuning);
|
|
1491
1514
|
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "created", this.getSnapshot());
|
|
1492
1515
|
}
|
|
1493
1516
|
start() {
|
|
@@ -1679,38 +1702,46 @@ var DefaultDrivingStatusController = class {
|
|
|
1679
1702
|
resolveGravityFrame(info) {
|
|
1680
1703
|
const sample = extractFreshVector(info.gravity, info.capturedAtMs, this.tuning.maxSampleAgeMs);
|
|
1681
1704
|
if (sample === null) {
|
|
1682
|
-
if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) return null;
|
|
1705
|
+
if (this.filteredGravity === null || this.rawGravity === null || this.lastGravitySampledAtMs === null) return null;
|
|
1683
1706
|
const ageMs = info.capturedAtMs - this.lastGravitySampledAtMs;
|
|
1684
1707
|
return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs ? {
|
|
1685
1708
|
value: this.filteredGravity,
|
|
1686
1709
|
sampledAtMs: this.lastGravitySampledAtMs,
|
|
1687
|
-
source: "cached"
|
|
1710
|
+
source: "cached",
|
|
1711
|
+
rawValue: this.rawGravity
|
|
1688
1712
|
} : null;
|
|
1689
1713
|
}
|
|
1690
|
-
if (sample.sampledAtMs === this.lastGravitySampledAtMs && this.filteredGravity !== null)
|
|
1691
|
-
value
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1714
|
+
if (sample.sampledAtMs === this.lastGravitySampledAtMs && this.filteredGravity !== null) {
|
|
1715
|
+
this.rawGravity = { ...sample.value };
|
|
1716
|
+
return {
|
|
1717
|
+
value: this.filteredGravity,
|
|
1718
|
+
sampledAtMs: sample.sampledAtMs,
|
|
1719
|
+
source: "live",
|
|
1720
|
+
rawValue: this.rawGravity
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1695
1723
|
if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) this.filteredGravity = { ...sample.value };
|
|
1696
1724
|
else {
|
|
1697
1725
|
if (sample.sampledAtMs < this.lastGravitySampledAtMs) {
|
|
1698
1726
|
const ageMs = info.capturedAtMs - this.lastGravitySampledAtMs;
|
|
1699
|
-
return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs ? {
|
|
1727
|
+
return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs && this.rawGravity !== null ? {
|
|
1700
1728
|
value: this.filteredGravity,
|
|
1701
1729
|
sampledAtMs: this.lastGravitySampledAtMs,
|
|
1702
|
-
source: "cached"
|
|
1730
|
+
source: "cached",
|
|
1731
|
+
rawValue: this.rawGravity
|
|
1703
1732
|
} : null;
|
|
1704
1733
|
}
|
|
1705
1734
|
const elapsedMs = Math.max(0, sample.sampledAtMs - this.lastGravitySampledAtMs);
|
|
1706
1735
|
const alpha = 1 - Math.exp(-elapsedMs / this.tuning.gravityFilterTimeConstantMs);
|
|
1707
1736
|
this.filteredGravity = lerpVector(this.filteredGravity, sample.value, alpha);
|
|
1708
1737
|
}
|
|
1738
|
+
this.rawGravity = { ...sample.value };
|
|
1709
1739
|
this.lastGravitySampledAtMs = sample.sampledAtMs;
|
|
1710
1740
|
return {
|
|
1711
1741
|
value: this.filteredGravity,
|
|
1712
1742
|
sampledAtMs: sample.sampledAtMs,
|
|
1713
|
-
source: "live"
|
|
1743
|
+
source: "live",
|
|
1744
|
+
rawValue: this.rawGravity
|
|
1714
1745
|
};
|
|
1715
1746
|
}
|
|
1716
1747
|
processAvailable(metrics, accelerometerSample, gravity) {
|
|
@@ -1731,7 +1762,10 @@ var DefaultDrivingStatusController = class {
|
|
|
1731
1762
|
tuningVersion: this.tuning.version
|
|
1732
1763
|
});
|
|
1733
1764
|
const longitudinalRetentionMs = Math.max(recognitionRetentionMs(this.tuning.longitudinalWindowMs, this.tuning.longitudinalMinimumSamples), recognitionRetentionMs(this.tuning.rapidLongitudinalWindowMs, this.tuning.rapidLongitudinalMinimumSamples));
|
|
1734
|
-
const addedLongitudinal =
|
|
1765
|
+
const addedLongitudinal = pushUniqueLongitudinalObservation(this.longitudinalWindow, {
|
|
1766
|
+
metrics,
|
|
1767
|
+
rawGravity: gravity.rawValue
|
|
1768
|
+
}, longitudinalRetentionMs);
|
|
1735
1769
|
const yawRetentionMs = Math.max(this.tuning.turnWindowMs, this.tuning.turnClosureWindowMs, recognitionRetentionMs(this.tuning.turnConfirmationMs, this.tuning.turnMinimumSamples));
|
|
1736
1770
|
const addedYaw = pushUniqueMetric(this.yawWindow, metrics, "gyroscopeSampledAtMs", yawRetentionMs);
|
|
1737
1771
|
const addedMotion = pushUniqueVector(this.motionWindow, accelerometerSample, recognitionRetentionMs(this.tuning.motionWindowMs, this.tuning.motionMinimumSamples));
|
|
@@ -1747,7 +1781,11 @@ var DefaultDrivingStatusController = class {
|
|
|
1747
1781
|
const previousLongitudinalStatus = this.normalLongitudinalStatus;
|
|
1748
1782
|
const previousTurnStatus = this.turnStatus;
|
|
1749
1783
|
const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion, motionCoverageWindow?.[0]?.sampledAtMs ?? accelerometerSample.sampledAtMs);
|
|
1750
|
-
const longitudinalEvidence = detectLongitudinalEvidence(this.longitudinalWindow, this.tuning);
|
|
1784
|
+
const longitudinalEvidence = detectLongitudinalEvidence(this.longitudinalWindow, this.tuning, this.directionalLongitudinalTuning);
|
|
1785
|
+
if (longitudinalEvidence.brakingQualityRejection !== null || longitudinalEvidence.suddenBrakingQualityRejection !== null) dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "rejected braking quality", {
|
|
1786
|
+
braking: longitudinalEvidence.brakingQualityRejection,
|
|
1787
|
+
suddenBraking: longitudinalEvidence.suddenBrakingQualityRejection
|
|
1788
|
+
});
|
|
1751
1789
|
if (addedLongitudinal) this.updateLongitudinalDetector(longitudinalEvidence, metrics.linearAccelerationSampledAtMs);
|
|
1752
1790
|
const turnEvidence = detectTurnEvidence(this.yawWindow, this.tuning);
|
|
1753
1791
|
if (addedYaw) this.updateTurnDetector(turnEvidence);
|
|
@@ -1778,7 +1816,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1778
1816
|
this.emitDetectorConfirmation("rapid_longitudinal", previousUrgentStatus, this.urgentStatus, statistics, now);
|
|
1779
1817
|
this.emitDetectorConfirmation("longitudinal", previousLongitudinalStatus, this.normalLongitudinalStatus, statistics, now);
|
|
1780
1818
|
this.emitDetectorConfirmation("turn", previousTurnStatus, this.turnStatus, statistics, now);
|
|
1781
|
-
const pendingCandidate = instantaneousCandidate(metrics, this.tuning);
|
|
1819
|
+
const pendingCandidate = instantaneousCandidate(metrics, this.tuning, this.directionalLongitudinalTuning);
|
|
1782
1820
|
this.setCandidate(pendingCandidate === this.status ? null : pendingCandidate, observationTime(metrics));
|
|
1783
1821
|
this.emitDiagnostic({
|
|
1784
1822
|
type: "detector_evidence",
|
|
@@ -1845,7 +1883,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1845
1883
|
if (evidence.normalCandidate !== null) {
|
|
1846
1884
|
this.normalLongitudinalStatus = evidence.normalCandidate;
|
|
1847
1885
|
this.longitudinalClearSinceMs = null;
|
|
1848
|
-
} else if (this.normalLongitudinalStatus !== null) if (evidence.
|
|
1886
|
+
} else if (this.normalLongitudinalStatus !== null) if (this.normalLongitudinalStatus === "BRAKING" ? evidence.retainedBraking : evidence.retainedAcceleration) this.longitudinalClearSinceMs = null;
|
|
1849
1887
|
else {
|
|
1850
1888
|
if (this.longitudinalClearSinceMs === null) this.longitudinalClearSinceMs = sampledAtMs;
|
|
1851
1889
|
if (sampledAtMs - this.longitudinalClearSinceMs >= this.tuning.longitudinalReleaseMs) {
|
|
@@ -2051,6 +2089,7 @@ var DefaultDrivingStatusController = class {
|
|
|
2051
2089
|
this.unavailableSinceMs = null;
|
|
2052
2090
|
this.fallbackApplied = false;
|
|
2053
2091
|
this.filteredGravity = null;
|
|
2092
|
+
this.rawGravity = null;
|
|
2054
2093
|
this.lastGravitySampledAtMs = null;
|
|
2055
2094
|
}
|
|
2056
2095
|
emitDiagnostic(event) {
|
|
@@ -2074,27 +2113,31 @@ var DefaultDrivingStatusController = class {
|
|
|
2074
2113
|
this.unsubscribePageLifecycle = null;
|
|
2075
2114
|
}
|
|
2076
2115
|
};
|
|
2077
|
-
const detectLongitudinalEvidence = (window, tuning) => {
|
|
2078
|
-
const recentRapidWindow =
|
|
2079
|
-
const rapidCoverageWindow = selectCoverageWindow(window, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, (sample) => sample.linearAccelerationSampledAtMs);
|
|
2116
|
+
const detectLongitudinalEvidence = (window, tuning, directionalTuning) => {
|
|
2117
|
+
const recentRapidWindow = recentLongitudinalObservations(window, tuning.rapidLongitudinalWindowMs);
|
|
2118
|
+
const rapidCoverageWindow = selectCoverageWindow(window, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, (sample) => sample.metrics.linearAccelerationSampledAtMs);
|
|
2080
2119
|
const rapidWindow = rapidCoverageWindow ?? recentRapidWindow;
|
|
2081
|
-
const recentNormalWindow =
|
|
2082
|
-
const normalCoverageWindow = selectCoverageWindow(window, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, (sample) => sample.linearAccelerationSampledAtMs);
|
|
2120
|
+
const recentNormalWindow = recentLongitudinalObservations(window, tuning.longitudinalWindowMs);
|
|
2121
|
+
const normalCoverageWindow = selectCoverageWindow(window, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, (sample) => sample.metrics.linearAccelerationSampledAtMs);
|
|
2083
2122
|
const normalWindow = normalCoverageWindow ?? recentNormalWindow;
|
|
2084
2123
|
let rapidCandidate = null;
|
|
2085
2124
|
let rejectionReason = null;
|
|
2086
|
-
|
|
2125
|
+
let suddenBrakingQualityRejection = null;
|
|
2126
|
+
const rapidMetrics = rapidWindow.map((sample) => sample.metrics);
|
|
2127
|
+
const rapidValues = rapidMetrics.map((sample) => sample.longitudinal);
|
|
2087
2128
|
const rapidMean = trimmedMean(rapidValues);
|
|
2088
2129
|
const rapidDirection = Math.sign(rapidMean);
|
|
2089
|
-
const
|
|
2130
|
+
const rapidDeadband = rapidDirection < 0 ? directionalTuning.brakingExitThreshold : directionalTuning.accelerationExitThreshold;
|
|
2131
|
+
const rapidCoherence = signCoherence(rapidValues, rapidDirection, rapidDeadband);
|
|
2090
2132
|
const rapidPeak = maximumAbsolute(rapidValues);
|
|
2091
2133
|
const longitudinalRms = rootMeanSquare(rapidValues);
|
|
2092
|
-
const verticalRms = rootMeanSquare(
|
|
2093
|
-
const reversalWindow =
|
|
2094
|
-
const hasReversal = containsBothSigns(reversalWindow.map((sample) => sample.longitudinal), tuning.longitudinalThreshold);
|
|
2134
|
+
const verticalRms = rootMeanSquare(rapidMetrics.map((sample) => sample.vertical));
|
|
2135
|
+
const reversalWindow = recentLongitudinalObservations(window, tuning.longitudinalReversalWindowMs);
|
|
2136
|
+
const hasReversal = containsBothSigns(reversalWindow.map((sample) => sample.metrics.longitudinal), tuning.longitudinalThreshold);
|
|
2137
|
+
const rapidBrakingQuality = brakingQualityRejection(rapidWindow, directionalTuning.suddenBrakingMinimumDominance, directionalTuning.maximumBrakingGravityDirectionChangeRad);
|
|
2095
2138
|
const rapidStatistics = rapidWindow.length === 0 ? null : {
|
|
2096
2139
|
sampleCount: rapidWindow.length,
|
|
2097
|
-
durationMs: metricWindowDuration(
|
|
2140
|
+
durationMs: metricWindowDuration(rapidMetrics, "linearAccelerationSampledAtMs"),
|
|
2098
2141
|
mean: rapidMean,
|
|
2099
2142
|
peak: rapidPeak,
|
|
2100
2143
|
coherence: rapidCoherence,
|
|
@@ -2103,36 +2146,52 @@ const detectLongitudinalEvidence = (window, tuning) => {
|
|
|
2103
2146
|
hasReversal
|
|
2104
2147
|
};
|
|
2105
2148
|
if (rapidCoverageWindow !== null) {
|
|
2106
|
-
|
|
2149
|
+
const rapidPeakThreshold = rapidDirection < 0 ? directionalTuning.suddenBrakingPeakThreshold : directionalTuning.rapidAccelerationPeakThreshold;
|
|
2150
|
+
const rapidMeanThreshold = rapidDirection < 0 ? directionalTuning.suddenBrakingMeanThreshold : directionalTuning.rapidAccelerationMeanThreshold;
|
|
2151
|
+
if (rapidPeak >= rapidPeakThreshold && Math.abs(rapidMean) >= rapidMeanThreshold) {
|
|
2107
2152
|
if (verticalRms > longitudinalRms * tuning.verticalBumpRatio) rejectionReason = "vertical_bump";
|
|
2108
2153
|
else if (hasReversal) rejectionReason = "longitudinal_reversal";
|
|
2109
|
-
else if (rapidDirection !== 0 && rapidCoherence >= tuning.rapidLongitudinalSignCoherence)
|
|
2154
|
+
else if (rapidDirection !== 0 && rapidCoherence >= tuning.rapidLongitudinalSignCoherence) if (rapidDirection < 0 && rapidBrakingQuality !== null) suddenBrakingQualityRejection = rapidBrakingQuality;
|
|
2155
|
+
else rapidCandidate = rapidDirection > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
|
|
2110
2156
|
}
|
|
2111
2157
|
}
|
|
2112
2158
|
let normalCandidate = null;
|
|
2113
|
-
let
|
|
2159
|
+
let retainedAcceleration = false;
|
|
2160
|
+
let retainedBraking = false;
|
|
2161
|
+
let brakingQualityRejectionResult = null;
|
|
2114
2162
|
let normalStatistics = null;
|
|
2115
2163
|
if (normalWindow.length > 0) {
|
|
2116
|
-
const
|
|
2164
|
+
const normalMetrics = normalWindow.map((sample) => sample.metrics);
|
|
2165
|
+
const values = normalMetrics.map((sample) => sample.longitudinal);
|
|
2117
2166
|
const mean = trimmedMean(values);
|
|
2118
2167
|
const direction = Math.sign(mean);
|
|
2119
|
-
const
|
|
2168
|
+
const deadband = direction < 0 ? directionalTuning.brakingExitThreshold : directionalTuning.accelerationExitThreshold;
|
|
2169
|
+
const coherence = signCoherence(values, direction, deadband);
|
|
2170
|
+
const normalBrakingQuality = brakingQualityRejection(normalWindow, directionalTuning.normalBrakingMinimumDominance, directionalTuning.maximumBrakingGravityDirectionChangeRad);
|
|
2120
2171
|
normalStatistics = {
|
|
2121
2172
|
sampleCount: normalWindow.length,
|
|
2122
|
-
durationMs: metricWindowDuration(
|
|
2173
|
+
durationMs: metricWindowDuration(normalMetrics, "linearAccelerationSampledAtMs"),
|
|
2123
2174
|
mean,
|
|
2124
2175
|
peak: maximumAbsolute(values),
|
|
2125
2176
|
coherence
|
|
2126
2177
|
};
|
|
2127
|
-
const retentionValues = recentNormalWindow.map((sample) => sample.longitudinal);
|
|
2128
|
-
|
|
2129
|
-
|
|
2178
|
+
const retentionValues = recentNormalWindow.map((sample) => sample.metrics.longitudinal);
|
|
2179
|
+
const retentionMean = trimmedMean(retentionValues);
|
|
2180
|
+
const retentionBrakingQuality = brakingQualityRejection(recentNormalWindow, directionalTuning.normalBrakingMinimumDominance, directionalTuning.maximumBrakingGravityDirectionChangeRad);
|
|
2181
|
+
retainedAcceleration = retentionMean >= directionalTuning.accelerationExitThreshold;
|
|
2182
|
+
retainedBraking = retentionMean <= -directionalTuning.brakingExitThreshold && retentionBrakingQuality === null;
|
|
2183
|
+
const threshold = direction < 0 ? directionalTuning.brakingThreshold : directionalTuning.accelerationThreshold;
|
|
2184
|
+
if (normalCoverageWindow !== null && Math.abs(mean) >= threshold && coherence >= tuning.longitudinalSignCoherence) if (direction < 0 && normalBrakingQuality !== null) brakingQualityRejectionResult = normalBrakingQuality;
|
|
2185
|
+
else normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
|
|
2130
2186
|
}
|
|
2131
2187
|
return {
|
|
2132
2188
|
rapidCandidate,
|
|
2133
2189
|
normalCandidate,
|
|
2134
|
-
|
|
2190
|
+
retainedAcceleration,
|
|
2191
|
+
retainedBraking,
|
|
2135
2192
|
rejectionReason,
|
|
2193
|
+
brakingQualityRejection: brakingQualityRejectionResult,
|
|
2194
|
+
suddenBrakingQualityRejection,
|
|
2136
2195
|
normalStatistics,
|
|
2137
2196
|
rapidStatistics
|
|
2138
2197
|
};
|
|
@@ -2188,13 +2247,17 @@ const detectTurnEvidence = (window, tuning) => {
|
|
|
2188
2247
|
}
|
|
2189
2248
|
};
|
|
2190
2249
|
};
|
|
2191
|
-
const instantaneousCandidate = (metrics, tuning) => {
|
|
2250
|
+
const instantaneousCandidate = (metrics, tuning, directionalTuning) => {
|
|
2192
2251
|
const longitudinalMagnitude = Math.abs(metrics.longitudinal);
|
|
2193
|
-
if (longitudinalMagnitude >=
|
|
2252
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.suddenBrakingPeakThreshold : directionalTuning.rapidAccelerationPeakThreshold) && Math.abs(metrics.vertical) <= longitudinalMagnitude * tuning.verticalBumpRatio) return metrics.longitudinal > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
|
|
2194
2253
|
if (Math.abs(metrics.yaw) >= tuning.turnYawThreshold) return metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
2195
|
-
if (longitudinalMagnitude >=
|
|
2254
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.brakingThreshold : directionalTuning.accelerationThreshold)) return metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING";
|
|
2196
2255
|
return null;
|
|
2197
2256
|
};
|
|
2257
|
+
const recentLongitudinalObservations = (values, windowMs) => {
|
|
2258
|
+
const latestAtMs = values[values.length - 1]?.metrics.linearAccelerationSampledAtMs ?? 0;
|
|
2259
|
+
return values.filter((value) => value.metrics.linearAccelerationSampledAtMs >= latestAtMs - windowMs);
|
|
2260
|
+
};
|
|
2198
2261
|
const recentMetrics = (values, windowMs, timestampKey) => {
|
|
2199
2262
|
const latestAtMs = values[values.length - 1]?.[timestampKey] ?? 0;
|
|
2200
2263
|
return values.filter((value) => value[timestampKey] >= latestAtMs - windowMs);
|
|
@@ -2245,6 +2308,23 @@ const signCoherence = (values, direction, deadband) => {
|
|
|
2245
2308
|
};
|
|
2246
2309
|
const containsBothSigns = (values, threshold) => values.some((value) => value >= threshold) && values.some((value) => value <= -threshold);
|
|
2247
2310
|
const rootMeanSquare = (values) => values.length === 0 ? 0 : Math.sqrt(values.reduce((sum, value) => sum + value ** 2, 0) / values.length);
|
|
2311
|
+
const brakingQualityRejection = (observations, minimumDominance, maximumGravityDirectionChangeRad) => {
|
|
2312
|
+
if (observations.length === 0) return null;
|
|
2313
|
+
if (rootMeanSquare(observations.map((sample) => sample.metrics.longitudinal)) < rootMeanSquare(observations.map((sample) => Math.hypot(sample.metrics.lateral, sample.metrics.vertical))) * minimumDominance) return "cross_axis_motion";
|
|
2314
|
+
return maximumVectorAngle(observations.map((sample) => sample.rawGravity)) > maximumGravityDirectionChangeRad ? "gravity_direction_change" : null;
|
|
2315
|
+
};
|
|
2316
|
+
const maximumVectorAngle = (values) => {
|
|
2317
|
+
let maximumAngle = 0;
|
|
2318
|
+
for (let leftIndex = 0; leftIndex < values.length; leftIndex += 1) for (let rightIndex = leftIndex + 1; rightIndex < values.length; rightIndex += 1) {
|
|
2319
|
+
const left = values[leftIndex];
|
|
2320
|
+
const right = values[rightIndex];
|
|
2321
|
+
const denominator = Math.hypot(left.x, left.y, left.z) * Math.hypot(right.x, right.y, right.z);
|
|
2322
|
+
if (denominator <= Number.EPSILON) return Number.POSITIVE_INFINITY;
|
|
2323
|
+
const cosine = (left.x * right.x + left.y * right.y + left.z * right.z) / denominator;
|
|
2324
|
+
maximumAngle = Math.max(maximumAngle, Math.acos(Math.max(-1, Math.min(1, cosine))));
|
|
2325
|
+
}
|
|
2326
|
+
return maximumAngle;
|
|
2327
|
+
};
|
|
2248
2328
|
const maximumAbsolute = (values) => values.length === 0 ? 0 : Math.max(...values.map(Math.abs));
|
|
2249
2329
|
const metricWindowDuration = (values, timestampKey) => {
|
|
2250
2330
|
const first = values[0];
|
|
@@ -2280,6 +2360,15 @@ const pushUniqueMetric = (window, metrics, timestampKey, windowMs) => {
|
|
|
2280
2360
|
while (window[0] && window[0][timestampKey] < cutoffMs) window.shift();
|
|
2281
2361
|
return true;
|
|
2282
2362
|
};
|
|
2363
|
+
const pushUniqueLongitudinalObservation = (window, observation, windowMs) => {
|
|
2364
|
+
const latest = window[window.length - 1];
|
|
2365
|
+
const sampledAtMs = observation.metrics.linearAccelerationSampledAtMs;
|
|
2366
|
+
if (latest && sampledAtMs <= latest.metrics.linearAccelerationSampledAtMs) return false;
|
|
2367
|
+
window.push(observation);
|
|
2368
|
+
const cutoffMs = sampledAtMs - windowMs;
|
|
2369
|
+
while (window[0] && window[0].metrics.linearAccelerationSampledAtMs < cutoffMs) window.shift();
|
|
2370
|
+
return true;
|
|
2371
|
+
};
|
|
2283
2372
|
const pushUniqueVector = (window, sample, windowMs) => {
|
|
2284
2373
|
const latest = window[window.length - 1];
|
|
2285
2374
|
if (latest && sample.sampledAtMs <= latest.sampledAtMs) return false;
|