@ziztechnology/dial-library 0.0.14 → 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 +136 -42
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Gunzip } from "fflate";
|
|
2
|
-
const resolveLogIntervalMs = (intervalMs = void 0) => typeof intervalMs === "number" && Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs :
|
|
2
|
+
const resolveLogIntervalMs = (intervalMs = void 0) => typeof intervalMs === "number" && Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 200;
|
|
3
3
|
//#endregion
|
|
4
4
|
//#region src/helper/debugger.ts
|
|
5
5
|
const logIntervalMs = resolveLogIntervalMs();
|
|
6
6
|
const lastLoggedAtByType = /* @__PURE__ */ new Map();
|
|
7
|
+
let hasLoggedSdkVersion = false;
|
|
7
8
|
const createLogTypeKey = (moduleName, scope, phase) => JSON.stringify([
|
|
8
9
|
moduleName,
|
|
9
10
|
scope,
|
|
@@ -38,6 +39,10 @@ const dialSdkDebugWarn = (moduleName, scope, phase, data) => {
|
|
|
38
39
|
if (!shouldLog(moduleName, scope, phase)) return;
|
|
39
40
|
const message = `[DIAL_SDK_DEBUG - ${moduleName}] ${scope}: ${phase}`;
|
|
40
41
|
try {
|
|
42
|
+
if (!hasLoggedSdkVersion) {
|
|
43
|
+
console.log(`[DIAL_SDK_DEBUG] SDK version: 0.0.16`);
|
|
44
|
+
hasLoggedSdkVersion = true;
|
|
45
|
+
}
|
|
41
46
|
if (data === void 0) console.log(message);
|
|
42
47
|
else console.log(message, stringifyDebugData(data));
|
|
43
48
|
} catch {}
|
|
@@ -143,6 +148,25 @@ const isRecord = (value, debug) => {
|
|
|
143
148
|
return result;
|
|
144
149
|
};
|
|
145
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
|
|
146
170
|
//#region src/driving_expression/check_status.ts
|
|
147
171
|
const DEBUG_MODULE$4 = "check_status.ts";
|
|
148
172
|
const DEVICE_RIGHT = {
|
|
@@ -156,7 +180,7 @@ const ZERO_VECTOR$1 = {
|
|
|
156
180
|
z: 0
|
|
157
181
|
};
|
|
158
182
|
const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
|
|
159
|
-
version: "driving-status-calibrated-2026-07-17-
|
|
183
|
+
version: "driving-status-calibrated-2026-07-17-r3",
|
|
160
184
|
maxSampleAgeMs: 1e3,
|
|
161
185
|
gravityFilterTimeConstantMs: 1e3,
|
|
162
186
|
biasFilterTimeConstantMs: 1e4,
|
|
@@ -377,8 +401,9 @@ const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TU
|
|
|
377
401
|
return result;
|
|
378
402
|
};
|
|
379
403
|
const classifyInstantaneousEvidence = (metrics, tuning) => {
|
|
404
|
+
const directionalTuning = resolveDirectionalLongitudinalTuning(tuning);
|
|
380
405
|
const longitudinalMagnitude = Math.abs(metrics.longitudinal);
|
|
381
|
-
if (longitudinalMagnitude >=
|
|
406
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.suddenBrakingPeakThreshold : directionalTuning.rapidAccelerationPeakThreshold)) {
|
|
382
407
|
if (Math.abs(metrics.vertical) > longitudinalMagnitude * tuning.verticalBumpRatio) return {
|
|
383
408
|
candidateStatus: null,
|
|
384
409
|
rejectionReason: "vertical_bump"
|
|
@@ -392,7 +417,7 @@ const classifyInstantaneousEvidence = (metrics, tuning) => {
|
|
|
392
417
|
candidateStatus: metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN",
|
|
393
418
|
rejectionReason: null
|
|
394
419
|
};
|
|
395
|
-
if (longitudinalMagnitude >=
|
|
420
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.brakingThreshold : directionalTuning.accelerationThreshold)) return {
|
|
396
421
|
candidateStatus: metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING",
|
|
397
422
|
rejectionReason: null
|
|
398
423
|
};
|
|
@@ -1402,6 +1427,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1402
1427
|
this.unavailableSinceMs = null;
|
|
1403
1428
|
this.fallbackApplied = false;
|
|
1404
1429
|
this.filteredGravity = null;
|
|
1430
|
+
this.rawGravity = null;
|
|
1405
1431
|
this.lastGravitySampledAtMs = null;
|
|
1406
1432
|
this.linearAccelerationBias = { ...ZERO_VECTOR };
|
|
1407
1433
|
this.gyroscopeBias = { ...ZERO_VECTOR };
|
|
@@ -1471,6 +1497,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1471
1497
|
this.pollIntervalMs = requirePositiveFinite("pollIntervalMs", options.pollIntervalMs ?? 200, REQUIRE_POSITIVE_FINITE_DEBUG);
|
|
1472
1498
|
this.unavailableFallbackMs = requirePositiveFinite("unavailableFallbackMs", options.unavailableFallbackMs ?? 3e3, REQUIRE_POSITIVE_FINITE_DEBUG);
|
|
1473
1499
|
this.tuning = resolveDrivingStatusTuning(options.tuning);
|
|
1500
|
+
this.directionalLongitudinalTuning = resolveDirectionalLongitudinalTuning(this.tuning);
|
|
1474
1501
|
this.clock = options.clock ?? systemClock;
|
|
1475
1502
|
this.onDiagnostic = options.onDiagnostic;
|
|
1476
1503
|
this.managePageLifecycle = options.managePageLifecycle ?? true;
|
|
@@ -1483,6 +1510,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1483
1510
|
this.status = initialStatus;
|
|
1484
1511
|
this.baseStatus = isBaseStatus(initialStatus) ? initialStatus : "STOPPED";
|
|
1485
1512
|
this.statusCommittedAtMs = this.clock.now();
|
|
1513
|
+
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "resolved directional longitudinal tuning", this.directionalLongitudinalTuning);
|
|
1486
1514
|
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "created", this.getSnapshot());
|
|
1487
1515
|
}
|
|
1488
1516
|
start() {
|
|
@@ -1674,38 +1702,46 @@ var DefaultDrivingStatusController = class {
|
|
|
1674
1702
|
resolveGravityFrame(info) {
|
|
1675
1703
|
const sample = extractFreshVector(info.gravity, info.capturedAtMs, this.tuning.maxSampleAgeMs);
|
|
1676
1704
|
if (sample === null) {
|
|
1677
|
-
if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) return null;
|
|
1705
|
+
if (this.filteredGravity === null || this.rawGravity === null || this.lastGravitySampledAtMs === null) return null;
|
|
1678
1706
|
const ageMs = info.capturedAtMs - this.lastGravitySampledAtMs;
|
|
1679
1707
|
return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs ? {
|
|
1680
1708
|
value: this.filteredGravity,
|
|
1681
1709
|
sampledAtMs: this.lastGravitySampledAtMs,
|
|
1682
|
-
source: "cached"
|
|
1710
|
+
source: "cached",
|
|
1711
|
+
rawValue: this.rawGravity
|
|
1683
1712
|
} : null;
|
|
1684
1713
|
}
|
|
1685
|
-
if (sample.sampledAtMs === this.lastGravitySampledAtMs && this.filteredGravity !== null)
|
|
1686
|
-
value
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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
|
+
}
|
|
1690
1723
|
if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) this.filteredGravity = { ...sample.value };
|
|
1691
1724
|
else {
|
|
1692
1725
|
if (sample.sampledAtMs < this.lastGravitySampledAtMs) {
|
|
1693
1726
|
const ageMs = info.capturedAtMs - this.lastGravitySampledAtMs;
|
|
1694
|
-
return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs ? {
|
|
1727
|
+
return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs && this.rawGravity !== null ? {
|
|
1695
1728
|
value: this.filteredGravity,
|
|
1696
1729
|
sampledAtMs: this.lastGravitySampledAtMs,
|
|
1697
|
-
source: "cached"
|
|
1730
|
+
source: "cached",
|
|
1731
|
+
rawValue: this.rawGravity
|
|
1698
1732
|
} : null;
|
|
1699
1733
|
}
|
|
1700
1734
|
const elapsedMs = Math.max(0, sample.sampledAtMs - this.lastGravitySampledAtMs);
|
|
1701
1735
|
const alpha = 1 - Math.exp(-elapsedMs / this.tuning.gravityFilterTimeConstantMs);
|
|
1702
1736
|
this.filteredGravity = lerpVector(this.filteredGravity, sample.value, alpha);
|
|
1703
1737
|
}
|
|
1738
|
+
this.rawGravity = { ...sample.value };
|
|
1704
1739
|
this.lastGravitySampledAtMs = sample.sampledAtMs;
|
|
1705
1740
|
return {
|
|
1706
1741
|
value: this.filteredGravity,
|
|
1707
1742
|
sampledAtMs: sample.sampledAtMs,
|
|
1708
|
-
source: "live"
|
|
1743
|
+
source: "live",
|
|
1744
|
+
rawValue: this.rawGravity
|
|
1709
1745
|
};
|
|
1710
1746
|
}
|
|
1711
1747
|
processAvailable(metrics, accelerometerSample, gravity) {
|
|
@@ -1726,7 +1762,10 @@ var DefaultDrivingStatusController = class {
|
|
|
1726
1762
|
tuningVersion: this.tuning.version
|
|
1727
1763
|
});
|
|
1728
1764
|
const longitudinalRetentionMs = Math.max(recognitionRetentionMs(this.tuning.longitudinalWindowMs, this.tuning.longitudinalMinimumSamples), recognitionRetentionMs(this.tuning.rapidLongitudinalWindowMs, this.tuning.rapidLongitudinalMinimumSamples));
|
|
1729
|
-
const addedLongitudinal =
|
|
1765
|
+
const addedLongitudinal = pushUniqueLongitudinalObservation(this.longitudinalWindow, {
|
|
1766
|
+
metrics,
|
|
1767
|
+
rawGravity: gravity.rawValue
|
|
1768
|
+
}, longitudinalRetentionMs);
|
|
1730
1769
|
const yawRetentionMs = Math.max(this.tuning.turnWindowMs, this.tuning.turnClosureWindowMs, recognitionRetentionMs(this.tuning.turnConfirmationMs, this.tuning.turnMinimumSamples));
|
|
1731
1770
|
const addedYaw = pushUniqueMetric(this.yawWindow, metrics, "gyroscopeSampledAtMs", yawRetentionMs);
|
|
1732
1771
|
const addedMotion = pushUniqueVector(this.motionWindow, accelerometerSample, recognitionRetentionMs(this.tuning.motionWindowMs, this.tuning.motionMinimumSamples));
|
|
@@ -1742,7 +1781,11 @@ var DefaultDrivingStatusController = class {
|
|
|
1742
1781
|
const previousLongitudinalStatus = this.normalLongitudinalStatus;
|
|
1743
1782
|
const previousTurnStatus = this.turnStatus;
|
|
1744
1783
|
const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion, motionCoverageWindow?.[0]?.sampledAtMs ?? accelerometerSample.sampledAtMs);
|
|
1745
|
-
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
|
+
});
|
|
1746
1789
|
if (addedLongitudinal) this.updateLongitudinalDetector(longitudinalEvidence, metrics.linearAccelerationSampledAtMs);
|
|
1747
1790
|
const turnEvidence = detectTurnEvidence(this.yawWindow, this.tuning);
|
|
1748
1791
|
if (addedYaw) this.updateTurnDetector(turnEvidence);
|
|
@@ -1773,7 +1816,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1773
1816
|
this.emitDetectorConfirmation("rapid_longitudinal", previousUrgentStatus, this.urgentStatus, statistics, now);
|
|
1774
1817
|
this.emitDetectorConfirmation("longitudinal", previousLongitudinalStatus, this.normalLongitudinalStatus, statistics, now);
|
|
1775
1818
|
this.emitDetectorConfirmation("turn", previousTurnStatus, this.turnStatus, statistics, now);
|
|
1776
|
-
const pendingCandidate = instantaneousCandidate(metrics, this.tuning);
|
|
1819
|
+
const pendingCandidate = instantaneousCandidate(metrics, this.tuning, this.directionalLongitudinalTuning);
|
|
1777
1820
|
this.setCandidate(pendingCandidate === this.status ? null : pendingCandidate, observationTime(metrics));
|
|
1778
1821
|
this.emitDiagnostic({
|
|
1779
1822
|
type: "detector_evidence",
|
|
@@ -1840,7 +1883,7 @@ var DefaultDrivingStatusController = class {
|
|
|
1840
1883
|
if (evidence.normalCandidate !== null) {
|
|
1841
1884
|
this.normalLongitudinalStatus = evidence.normalCandidate;
|
|
1842
1885
|
this.longitudinalClearSinceMs = null;
|
|
1843
|
-
} else if (this.normalLongitudinalStatus !== null) if (evidence.
|
|
1886
|
+
} else if (this.normalLongitudinalStatus !== null) if (this.normalLongitudinalStatus === "BRAKING" ? evidence.retainedBraking : evidence.retainedAcceleration) this.longitudinalClearSinceMs = null;
|
|
1844
1887
|
else {
|
|
1845
1888
|
if (this.longitudinalClearSinceMs === null) this.longitudinalClearSinceMs = sampledAtMs;
|
|
1846
1889
|
if (sampledAtMs - this.longitudinalClearSinceMs >= this.tuning.longitudinalReleaseMs) {
|
|
@@ -2046,6 +2089,7 @@ var DefaultDrivingStatusController = class {
|
|
|
2046
2089
|
this.unavailableSinceMs = null;
|
|
2047
2090
|
this.fallbackApplied = false;
|
|
2048
2091
|
this.filteredGravity = null;
|
|
2092
|
+
this.rawGravity = null;
|
|
2049
2093
|
this.lastGravitySampledAtMs = null;
|
|
2050
2094
|
}
|
|
2051
2095
|
emitDiagnostic(event) {
|
|
@@ -2069,27 +2113,31 @@ var DefaultDrivingStatusController = class {
|
|
|
2069
2113
|
this.unsubscribePageLifecycle = null;
|
|
2070
2114
|
}
|
|
2071
2115
|
};
|
|
2072
|
-
const detectLongitudinalEvidence = (window, tuning) => {
|
|
2073
|
-
const recentRapidWindow =
|
|
2074
|
-
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);
|
|
2075
2119
|
const rapidWindow = rapidCoverageWindow ?? recentRapidWindow;
|
|
2076
|
-
const recentNormalWindow =
|
|
2077
|
-
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);
|
|
2078
2122
|
const normalWindow = normalCoverageWindow ?? recentNormalWindow;
|
|
2079
2123
|
let rapidCandidate = null;
|
|
2080
2124
|
let rejectionReason = null;
|
|
2081
|
-
|
|
2125
|
+
let suddenBrakingQualityRejection = null;
|
|
2126
|
+
const rapidMetrics = rapidWindow.map((sample) => sample.metrics);
|
|
2127
|
+
const rapidValues = rapidMetrics.map((sample) => sample.longitudinal);
|
|
2082
2128
|
const rapidMean = trimmedMean(rapidValues);
|
|
2083
2129
|
const rapidDirection = Math.sign(rapidMean);
|
|
2084
|
-
const
|
|
2130
|
+
const rapidDeadband = rapidDirection < 0 ? directionalTuning.brakingExitThreshold : directionalTuning.accelerationExitThreshold;
|
|
2131
|
+
const rapidCoherence = signCoherence(rapidValues, rapidDirection, rapidDeadband);
|
|
2085
2132
|
const rapidPeak = maximumAbsolute(rapidValues);
|
|
2086
2133
|
const longitudinalRms = rootMeanSquare(rapidValues);
|
|
2087
|
-
const verticalRms = rootMeanSquare(
|
|
2088
|
-
const reversalWindow =
|
|
2089
|
-
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);
|
|
2090
2138
|
const rapidStatistics = rapidWindow.length === 0 ? null : {
|
|
2091
2139
|
sampleCount: rapidWindow.length,
|
|
2092
|
-
durationMs: metricWindowDuration(
|
|
2140
|
+
durationMs: metricWindowDuration(rapidMetrics, "linearAccelerationSampledAtMs"),
|
|
2093
2141
|
mean: rapidMean,
|
|
2094
2142
|
peak: rapidPeak,
|
|
2095
2143
|
coherence: rapidCoherence,
|
|
@@ -2098,36 +2146,52 @@ const detectLongitudinalEvidence = (window, tuning) => {
|
|
|
2098
2146
|
hasReversal
|
|
2099
2147
|
};
|
|
2100
2148
|
if (rapidCoverageWindow !== null) {
|
|
2101
|
-
|
|
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) {
|
|
2102
2152
|
if (verticalRms > longitudinalRms * tuning.verticalBumpRatio) rejectionReason = "vertical_bump";
|
|
2103
2153
|
else if (hasReversal) rejectionReason = "longitudinal_reversal";
|
|
2104
|
-
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";
|
|
2105
2156
|
}
|
|
2106
2157
|
}
|
|
2107
2158
|
let normalCandidate = null;
|
|
2108
|
-
let
|
|
2159
|
+
let retainedAcceleration = false;
|
|
2160
|
+
let retainedBraking = false;
|
|
2161
|
+
let brakingQualityRejectionResult = null;
|
|
2109
2162
|
let normalStatistics = null;
|
|
2110
2163
|
if (normalWindow.length > 0) {
|
|
2111
|
-
const
|
|
2164
|
+
const normalMetrics = normalWindow.map((sample) => sample.metrics);
|
|
2165
|
+
const values = normalMetrics.map((sample) => sample.longitudinal);
|
|
2112
2166
|
const mean = trimmedMean(values);
|
|
2113
2167
|
const direction = Math.sign(mean);
|
|
2114
|
-
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);
|
|
2115
2171
|
normalStatistics = {
|
|
2116
2172
|
sampleCount: normalWindow.length,
|
|
2117
|
-
durationMs: metricWindowDuration(
|
|
2173
|
+
durationMs: metricWindowDuration(normalMetrics, "linearAccelerationSampledAtMs"),
|
|
2118
2174
|
mean,
|
|
2119
2175
|
peak: maximumAbsolute(values),
|
|
2120
2176
|
coherence
|
|
2121
2177
|
};
|
|
2122
|
-
const retentionValues = recentNormalWindow.map((sample) => sample.longitudinal);
|
|
2123
|
-
|
|
2124
|
-
|
|
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";
|
|
2125
2186
|
}
|
|
2126
2187
|
return {
|
|
2127
2188
|
rapidCandidate,
|
|
2128
2189
|
normalCandidate,
|
|
2129
|
-
|
|
2190
|
+
retainedAcceleration,
|
|
2191
|
+
retainedBraking,
|
|
2130
2192
|
rejectionReason,
|
|
2193
|
+
brakingQualityRejection: brakingQualityRejectionResult,
|
|
2194
|
+
suddenBrakingQualityRejection,
|
|
2131
2195
|
normalStatistics,
|
|
2132
2196
|
rapidStatistics
|
|
2133
2197
|
};
|
|
@@ -2183,13 +2247,17 @@ const detectTurnEvidence = (window, tuning) => {
|
|
|
2183
2247
|
}
|
|
2184
2248
|
};
|
|
2185
2249
|
};
|
|
2186
|
-
const instantaneousCandidate = (metrics, tuning) => {
|
|
2250
|
+
const instantaneousCandidate = (metrics, tuning, directionalTuning) => {
|
|
2187
2251
|
const longitudinalMagnitude = Math.abs(metrics.longitudinal);
|
|
2188
|
-
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";
|
|
2189
2253
|
if (Math.abs(metrics.yaw) >= tuning.turnYawThreshold) return metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
2190
|
-
if (longitudinalMagnitude >=
|
|
2254
|
+
if (longitudinalMagnitude >= (metrics.longitudinal < 0 ? directionalTuning.brakingThreshold : directionalTuning.accelerationThreshold)) return metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING";
|
|
2191
2255
|
return null;
|
|
2192
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
|
+
};
|
|
2193
2261
|
const recentMetrics = (values, windowMs, timestampKey) => {
|
|
2194
2262
|
const latestAtMs = values[values.length - 1]?.[timestampKey] ?? 0;
|
|
2195
2263
|
return values.filter((value) => value[timestampKey] >= latestAtMs - windowMs);
|
|
@@ -2240,6 +2308,23 @@ const signCoherence = (values, direction, deadband) => {
|
|
|
2240
2308
|
};
|
|
2241
2309
|
const containsBothSigns = (values, threshold) => values.some((value) => value >= threshold) && values.some((value) => value <= -threshold);
|
|
2242
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
|
+
};
|
|
2243
2328
|
const maximumAbsolute = (values) => values.length === 0 ? 0 : Math.max(...values.map(Math.abs));
|
|
2244
2329
|
const metricWindowDuration = (values, timestampKey) => {
|
|
2245
2330
|
const first = values[0];
|
|
@@ -2275,6 +2360,15 @@ const pushUniqueMetric = (window, metrics, timestampKey, windowMs) => {
|
|
|
2275
2360
|
while (window[0] && window[0][timestampKey] < cutoffMs) window.shift();
|
|
2276
2361
|
return true;
|
|
2277
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
|
+
};
|
|
2278
2372
|
const pushUniqueVector = (window, sample, windowMs) => {
|
|
2279
2373
|
const latest = window[window.length - 1];
|
|
2280
2374
|
if (latest && sample.sampledAtMs <= latest.sampledAtMs) return false;
|