@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.
Files changed (2) hide show
  1. package/dist/index.mjs +131 -42
  2. 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.15`);
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-r2",
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 >= tuning.rapidLongitudinalPeakThreshold) {
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 >= tuning.longitudinalThreshold) return {
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) return {
1691
- value: this.filteredGravity,
1692
- sampledAtMs: sample.sampledAtMs,
1693
- source: "live"
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 = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", longitudinalRetentionMs);
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.retainedNormal) this.longitudinalClearSinceMs = null;
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 = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
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 = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
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
- 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);
2087
2128
  const rapidMean = trimmedMean(rapidValues);
2088
2129
  const rapidDirection = Math.sign(rapidMean);
2089
- const rapidCoherence = signCoherence(rapidValues, rapidDirection, tuning.longitudinalExitThreshold);
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(rapidWindow.map((sample) => sample.vertical));
2093
- const reversalWindow = recentMetrics(window, tuning.longitudinalReversalWindowMs, "linearAccelerationSampledAtMs");
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(rapidWindow, "linearAccelerationSampledAtMs"),
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
- 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) {
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) 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";
2110
2156
  }
2111
2157
  }
2112
2158
  let normalCandidate = null;
2113
- let retainedNormal = false;
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 values = normalWindow.map((sample) => sample.longitudinal);
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 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);
2120
2171
  normalStatistics = {
2121
2172
  sampleCount: normalWindow.length,
2122
- durationMs: metricWindowDuration(normalWindow, "linearAccelerationSampledAtMs"),
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
- retainedNormal = Math.abs(trimmedMean(retentionValues)) >= tuning.longitudinalExitThreshold;
2129
- 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";
2130
2186
  }
2131
2187
  return {
2132
2188
  rapidCandidate,
2133
2189
  normalCandidate,
2134
- retainedNormal,
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 >= 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";
2194
2253
  if (Math.abs(metrics.yaw) >= tuning.turnYawThreshold) return metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
2195
- 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";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziztechnology/dial-library",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "private": false,
5
5
  "description": "Tooony 表盘工具函数、方法",
6
6
  "files": [