@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.
Files changed (2) hide show
  1. package/dist/index.mjs +136 -42
  2. 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 : 500;
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-r2",
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 >= tuning.rapidLongitudinalPeakThreshold) {
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 >= tuning.longitudinalThreshold) return {
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) return {
1686
- value: this.filteredGravity,
1687
- sampledAtMs: sample.sampledAtMs,
1688
- source: "live"
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 = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", longitudinalRetentionMs);
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.retainedNormal) this.longitudinalClearSinceMs = null;
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 = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
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 = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
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
- const rapidValues = rapidWindow.map((sample) => sample.longitudinal);
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 rapidCoherence = signCoherence(rapidValues, rapidDirection, tuning.longitudinalExitThreshold);
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(rapidWindow.map((sample) => sample.vertical));
2088
- const reversalWindow = recentMetrics(window, tuning.longitudinalReversalWindowMs, "linearAccelerationSampledAtMs");
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(rapidWindow, "linearAccelerationSampledAtMs"),
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
- if (rapidPeak >= tuning.rapidLongitudinalPeakThreshold && Math.abs(rapidMean) >= tuning.rapidLongitudinalMeanThreshold) {
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) rapidCandidate = rapidDirection > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
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 retainedNormal = false;
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 values = normalWindow.map((sample) => sample.longitudinal);
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 coherence = signCoherence(values, direction, tuning.longitudinalExitThreshold);
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(normalWindow, "linearAccelerationSampledAtMs"),
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
- retainedNormal = Math.abs(trimmedMean(retentionValues)) >= tuning.longitudinalExitThreshold;
2124
- if (normalCoverageWindow !== null && Math.abs(mean) >= tuning.longitudinalThreshold && coherence >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
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
- retainedNormal,
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 >= tuning.rapidLongitudinalPeakThreshold && Math.abs(metrics.vertical) <= longitudinalMagnitude * tuning.verticalBumpRatio) return metrics.longitudinal > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
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 >= tuning.longitudinalThreshold) return metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziztechnology/dial-library",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "private": false,
5
5
  "description": "Tooony 表盘工具函数、方法",
6
6
  "files": [