Skip to content

Commit f9d646b

Browse files
committed
Refine motor chatter burst detection
1 parent 9426db9 commit f9d646b

2 files changed

Lines changed: 107 additions & 12 deletions

File tree

src/domain/blackbox/events/detectEvents.js

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,41 @@ function axisPeak(values) {
1616
return Math.max(...numbers.map((value) => Math.abs(value)));
1717
}
1818

19+
function percentile(values, ratio) {
20+
if (!values.length) {
21+
return null;
22+
}
23+
const sorted = [...values].sort((left, right) => left - right);
24+
const index = Math.min(
25+
sorted.length - 1,
26+
Math.max(0, Math.floor((sorted.length - 1) * ratio))
27+
);
28+
return sorted[index];
29+
}
30+
31+
function buildMotorChatterThresholds(samples) {
32+
const activeWindows = samples.filter(
33+
(sample) =>
34+
sample.motorChatter.activePairCount >= 4 &&
35+
(sample.motorChatter.avgThrottle ?? 0) >= 18 &&
36+
(sample.motorChatter.avgRpm ?? 0) >= 900
37+
);
38+
const scores = activeWindows
39+
.map((sample) => sample.motorChatter.oscillationScore)
40+
.filter((value) => Number.isFinite(value));
41+
const normalizedDeltas = activeWindows
42+
.map((sample) => sample.motorChatter.avgNormalizedDelta)
43+
.filter((value) => Number.isFinite(value));
44+
45+
return {
46+
scoreThreshold: Math.max(0.055, (percentile(scores, 0.97) ?? 0) * 0.92),
47+
normalizedDeltaThreshold: Math.max(
48+
0.05,
49+
(percentile(normalizedDeltas, 0.95) ?? 0) * 0.9
50+
),
51+
};
52+
}
53+
1954
function summarizeSegment(
2055
type,
2156
samples,
@@ -317,6 +352,13 @@ export function detectAnalysisEvents(windowSlice, locale = "en", options = {}) {
317352
sample.rc.throttle !== undefined
318353
? previous.rc.throttle - sample.rc.throttle
319354
: null,
355+
throttleRise:
356+
previous?.rc.throttle !== null &&
357+
previous?.rc.throttle !== undefined &&
358+
sample.rc.throttle !== null &&
359+
sample.rc.throttle !== undefined
360+
? sample.rc.throttle - previous.rc.throttle
361+
: null,
320362
status,
321363
errorMagnitude: getErrorMagnitude(sample.error),
322364
turnInput: rcTurnInput,
@@ -335,15 +377,16 @@ export function detectAnalysisEvents(windowSlice, locale = "en", options = {}) {
335377
};
336378
});
337379
const derivedWithMotorChatter = derived.map((sample, index) => {
338-
const localStart = Math.max(0, index - 2);
339-
const localEnd = Math.min(derived.length, index + 3);
380+
const localStart = Math.max(0, index - 3);
381+
const localEnd = Math.min(derived.length, index + 4);
340382
const motorChatter = getMotorChatterReviewSummary(derived.slice(localStart, localEnd));
341383

342384
return {
343385
...sample,
344386
motorChatter,
345387
};
346388
});
389+
const motorChatterThresholds = buildMotorChatterThresholds(derivedWithMotorChatter);
347390

348391
const events = [
349392
...segmentByPredicate(derivedWithMotorChatter, EVENT_TYPES.HIGH_THROTTLE_STRAIGHT, (sample) => {
@@ -419,27 +462,45 @@ export function detectAnalysisEvents(windowSlice, locale = "en", options = {}) {
419462
derivedWithMotorChatter,
420463
EVENT_TYPES.MOTOR_CHATTER,
421464
(sample) => {
465+
const turnDemand = Math.max(
466+
sample.turnInput ?? 0,
467+
(sample.setpointTurnInput ?? 0) * 0.75
468+
);
469+
const throttlePunch =
470+
(sample.throttleRise ?? 0) >= 10 ||
471+
((sample.previousThrottle ?? 0) < 55 && (sample.rc.throttle ?? 0) >= 62);
472+
const loadedDemand =
473+
turnDemand >= 135 ||
474+
throttlePunch ||
475+
((sample.rc.throttle ?? 0) >= 72 && (sample.motorChatter.flipRate ?? 0) >= 0.35);
476+
422477
if (
423478
sample.mode?.armed === false ||
424479
sample.motorChatter.activePairCount < 4 ||
425-
(sample.motorChatter.avgThrottle ?? 0) < 18 ||
480+
(sample.motorChatter.avgThrottle ?? 0) < 22 ||
426481
(sample.motorChatter.avgRpm ?? 0) < 900 ||
427-
sample.motorChatter.affectedMotorCount < 2
482+
!loadedDemand
428483
) {
429484
return false;
430485
}
431486

432487
if (
433-
(sample.motorChatter.oscillationScore ?? 0) < 0.04 ||
434-
(sample.motorChatter.avgNormalizedDelta ?? 0) < 0.045
488+
(sample.motorChatter.oscillationScore ?? 0) <
489+
motorChatterThresholds.scoreThreshold ||
490+
(sample.motorChatter.avgNormalizedDelta ?? 0) <
491+
motorChatterThresholds.normalizedDeltaThreshold ||
492+
(sample.motorChatter.affectedMotorCount < 2 &&
493+
(sample.motorChatter.peakMotorSpreadRatio ?? 0) < 0.24)
435494
) {
436495
return false;
437496
}
438497

439498
return {
440499
score:
441500
(sample.motorChatter.oscillationScore ?? 0) * 1000 +
442-
(sample.motorChatter.flipRate ?? 0) * 100,
501+
(sample.motorChatter.flipRate ?? 0) * 100 +
502+
turnDemand * 0.2 +
503+
((sample.throttleRise ?? 0) * 4),
443504
};
444505
},
445506
locale

src/domain/blackbox/events/detectEvents.test.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,17 @@ describe("detectAnalysisEvents", () => {
100100

101101
const events = detectAnalysisEvents({ samples });
102102

103-
expect(events).toHaveLength(1);
104-
expect(events[0].type).toBe(EVENT_TYPES.CHOP_TURN);
105-
expect(events[0].lowThrottleContext).toEqual(
103+
const chopTurn = events.find((event) => event.type === EVENT_TYPES.CHOP_TURN);
104+
105+
expect(chopTurn).toBeTruthy();
106+
expect(chopTurn.lowThrottleContext).toEqual(
106107
expect.objectContaining({
107108
hasRpmData: true,
108109
lowThrottleSamples: 3,
109110
rpmFloor: 840,
110111
})
111112
);
112-
expect(events[0].detail).toContain("RPM floor");
113+
expect(chopTurn.detail).toContain("RPM floor");
113114
});
114115

115116
it("detects battery warning and critical bands from embedded thresholds", () => {
@@ -143,7 +144,8 @@ describe("detectAnalysisEvents", () => {
143144
it("detects motor chatter during active rpm oscillation", () => {
144145
const samples = Array.from({ length: 8 }, (_, index) =>
145146
buildSample(index * 50000, {
146-
rc: { throttle: 58 },
147+
rc: { throttle: index < 2 ? 42 : 68, roll: 190 },
148+
setpoint: { roll: 240, pitch: 30, yaw: 0 },
147149
rpm:
148150
index % 2 === 0
149151
? [1520, 1120, 1510, 1130]
@@ -178,4 +180,36 @@ describe("detectAnalysisEvents", () => {
178180

179181
expect(events.map((event) => event.type)).not.toContain(EVENT_TYPES.MOTOR_CHATTER);
180182
});
183+
184+
it("treats broad background oscillation as baseline and highlights burstier turn windows", () => {
185+
const samples = Array.from({ length: 22 }, (_, index) => {
186+
const burst = index >= 8 && index <= 16;
187+
return buildSample(index * 50000, {
188+
rc: {
189+
throttle: burst ? (index < 11 ? 46 : 76) : 56,
190+
roll: burst ? 210 : 22,
191+
},
192+
setpoint: {
193+
roll: burst ? 260 : 30,
194+
pitch: burst ? 70 : 10,
195+
yaw: 0,
196+
},
197+
rpm:
198+
index % 2 === 0
199+
? burst
200+
? [1570, 1070, 1540, 1050]
201+
: [1470, 1410, 1460, 1405]
202+
: burst
203+
? [1040, 1580, 1030, 1590]
204+
: [1400, 1475, 1405, 1468],
205+
});
206+
});
207+
208+
const events = detectAnalysisEvents({ samples });
209+
const chatterEvents = events.filter((event) => event.type === EVENT_TYPES.MOTOR_CHATTER);
210+
211+
expect(chatterEvents).toHaveLength(1);
212+
expect(chatterEvents[0].startUs).toBeGreaterThanOrEqual(8 * 50000);
213+
expect(chatterEvents[0].endUs).toBeLessThanOrEqual(16 * 50000);
214+
});
181215
});

0 commit comments

Comments
 (0)