mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -04:00
slop
This commit is contained in:
@@ -11,9 +11,10 @@ const DEFAULT_CONFIG = Object.freeze({
|
|||||||
encoderNoiseFloorMmPerSec: 15,
|
encoderNoiseFloorMmPerSec: 15,
|
||||||
fullStallProgressRatio: 0.1,
|
fullStallProgressRatio: 0.1,
|
||||||
movingProgressRatio: 0.6,
|
movingProgressRatio: 0.6,
|
||||||
movingOrUnknownRatePerSec: 0.25,
|
movingOrUnknownRatePerSec: 0.2,
|
||||||
fullStallRatePerSec: 1,
|
fullStallRatePerSec: 0.6,
|
||||||
wheelRecoveryRatePerSec: 0.75,
|
wheelRecoveryDelaySec: 0.75,
|
||||||
|
wheelRecoveryRatePerSec: 0.4,
|
||||||
brushOvercurrentRatePerSec: 1,
|
brushOvercurrentRatePerSec: 1,
|
||||||
brushRecoveryRatePerSec: 0.75,
|
brushRecoveryRatePerSec: 0.75,
|
||||||
clearBeforeUnlockSec: 0.75,
|
clearBeforeUnlockSec: 0.75,
|
||||||
@@ -44,6 +45,7 @@ function createMotorState() {
|
|||||||
classification: 'unknown',
|
classification: 'unknown',
|
||||||
progressSamples: [],
|
progressSamples: [],
|
||||||
windowCommandSign: 0,
|
windowCommandSign: 0,
|
||||||
|
clearSec: 0,
|
||||||
stress: 0,
|
stress: 0,
|
||||||
cap: 1,
|
cap: 1,
|
||||||
};
|
};
|
||||||
@@ -325,6 +327,7 @@ function createOvercurrentProtectionService(options = {}) {
|
|||||||
motor.commandedSpeed = commandNumber;
|
motor.commandedSpeed = commandNumber;
|
||||||
motor.measuredSpeed = measuredValid ? measuredNumber : null;
|
motor.measuredSpeed = measuredValid ? measuredNumber : null;
|
||||||
motor.currentMa = Number.isFinite(Number(currentMa)) ? Number(currentMa) : null;
|
motor.currentMa = Number.isFinite(Number(currentMa)) ? Number(currentMa) : null;
|
||||||
|
motor.clearSec = motor.overcurrent ? 0 : motor.clearSec + deltaSec;
|
||||||
|
|
||||||
if (!motor.overcurrent) {
|
if (!motor.overcurrent) {
|
||||||
resetWheelProgressWindow(motor, commandSign);
|
resetWheelProgressWindow(motor, commandSign);
|
||||||
@@ -354,9 +357,14 @@ function createOvercurrentProtectionService(options = {}) {
|
|||||||
|
|
||||||
const riseRate = config.movingOrUnknownRatePerSec
|
const riseRate = config.movingOrUnknownRatePerSec
|
||||||
+ (config.fullStallRatePerSec - config.movingOrUnknownRatePerSec) * motor.stallFactor;
|
+ (config.fullStallRatePerSec - config.movingOrUnknownRatePerSec) * motor.stallFactor;
|
||||||
|
const recoveryAllowed = !motor.overcurrent && motor.clearSec >= config.wheelRecoveryDelaySec;
|
||||||
motor.stress = clampUnit(
|
motor.stress = clampUnit(
|
||||||
motor.stress
|
motor.stress
|
||||||
+ (motor.overcurrent ? riseRate * deltaSec : -config.wheelRecoveryRatePerSec * deltaSec),
|
+ (motor.overcurrent
|
||||||
|
? riseRate * deltaSec
|
||||||
|
: recoveryAllowed
|
||||||
|
? -config.wheelRecoveryRatePerSec * deltaSec
|
||||||
|
: 0),
|
||||||
);
|
);
|
||||||
motor.cap = calculateCap(motor.stress);
|
motor.cap = calculateCap(motor.stress);
|
||||||
}
|
}
|
||||||
@@ -524,7 +532,11 @@ function createOvercurrentProtectionService(options = {}) {
|
|||||||
// still inside the transient grace region. Reporting it separately keeps
|
// still inside the transient grace region. Reporting it separately keeps
|
||||||
// HUD visibility immediate without falsely claiming output is being scaled.
|
// HUD visibility immediate without falsely claiming output is being scaled.
|
||||||
if (anyOvercurrent) return 'overcurrent';
|
if (anyOvercurrent) return 'overcurrent';
|
||||||
if (anyLimited) return 'recovering';
|
// Stress below the command-limiting grace threshold still represents a
|
||||||
|
// recent hardware event. Keeping recovery visible until it reaches zero
|
||||||
|
// prevents the HUD from vanishing the instant the raw flag clears.
|
||||||
|
const anyStress = MOTOR_KEYS.some((key) => state.motors[key].stress > 0);
|
||||||
|
if (anyStress) return 'recovering';
|
||||||
return 'idle';
|
return 'idle';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ test('a short stalled-wheel spike remains inside the grace region', () => {
|
|||||||
wheelSpeedsMmPerSecond: { left: 0 },
|
wheelSpeedsMmPerSecond: { left: 0 },
|
||||||
}), start + 100);
|
}), start + 100);
|
||||||
|
|
||||||
assert.equal(snapshot.motors.leftWheel.stress, 0.025);
|
// Stress is accumulated with floating-point time arithmetic, so compare
|
||||||
|
// within a tiny tolerance instead of depending on an exact binary decimal.
|
||||||
|
assert.ok(Math.abs(snapshot.motors.leftWheel.stress - 0.02) < 1e-9);
|
||||||
assert.equal(snapshot.motors.leftWheel.cap, 1);
|
assert.equal(snapshot.motors.leftWheel.cap, 1);
|
||||||
// The hardware event is visible immediately even though the grace region
|
// The hardware event is visible immediately even though the grace region
|
||||||
// correctly leaves the command cap at full output.
|
// correctly leaves the command cap at full output.
|
||||||
@@ -62,7 +64,7 @@ test('persistent stalled-wheel overcurrent scales both wheels and then stops dri
|
|||||||
driveDirect: { left: 300, right: 200 },
|
driveDirect: { left: 300, right: 200 },
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let step = 0; step <= 12; step += 1) {
|
for (let step = 0; step <= 18; step += 1) {
|
||||||
service.processTelemetry('rover', makeSensors({
|
service.processTelemetry('rover', makeSensors({
|
||||||
wheelOvercurrents: { leftWheel: true },
|
wheelOvercurrents: { leftWheel: true },
|
||||||
wheelSpeedsMmPerSecond: { left: 0, right: 200 },
|
wheelSpeedsMmPerSecond: { left: 0, right: 200 },
|
||||||
@@ -70,15 +72,15 @@ test('persistent stalled-wheel overcurrent scales both wheels and then stops dri
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The first 400 ms establish that the wheel is making no net progress. Once
|
The first 400 ms establish that the wheel is making no net progress. The
|
||||||
classified, the faster stalled rate should approach—but not yet cross—the
|
tuned stalled rate should then approach—but not yet cross—the hard-stop
|
||||||
hard-stop boundary at 1.2 seconds.
|
boundary at 1.8 seconds.
|
||||||
*/
|
*/
|
||||||
assert.equal(service.getPublicState('rover').drive.blocked, false);
|
assert.equal(service.getPublicState('rover').drive.blocked, false);
|
||||||
service.processTelemetry('rover', makeSensors({
|
service.processTelemetry('rover', makeSensors({
|
||||||
wheelOvercurrents: { leftWheel: true },
|
wheelOvercurrents: { leftWheel: true },
|
||||||
wheelSpeedsMmPerSecond: { left: 0, right: 200 },
|
wheelSpeedsMmPerSecond: { left: 0, right: 200 },
|
||||||
}), start + 1300);
|
}), start + 1900);
|
||||||
|
|
||||||
const snapshot = service.getPublicState('rover');
|
const snapshot = service.getPublicState('rover');
|
||||||
assert.equal(snapshot.status, 'stopped');
|
assert.equal(snapshot.status, 'stopped');
|
||||||
@@ -140,7 +142,7 @@ test('encoder wobble around zero is classified as a full stall', () => {
|
|||||||
driveDirect: { left: 300, right: 300 },
|
driveDirect: { left: 300, right: 300 },
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let step = 0; step <= 13; step += 1) {
|
for (let step = 0; step <= 19; step += 1) {
|
||||||
const wobbleSpeed = step % 2 === 0 ? 10 : -9;
|
const wobbleSpeed = step % 2 === 0 ? 10 : -9;
|
||||||
service.processTelemetry('rover', makeSensors({
|
service.processTelemetry('rover', makeSensors({
|
||||||
wheelOvercurrents: { leftWheel: true },
|
wheelOvercurrents: { leftWheel: true },
|
||||||
@@ -222,7 +224,7 @@ test('wheel comparison follows scaled output and resets after reversal', () => {
|
|||||||
driveDirect: { left: 300, right: 300 },
|
driveDirect: { left: 300, right: 300 },
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let step = 0; step <= 6; step += 1) {
|
for (let step = 0; step <= 8; step += 1) {
|
||||||
service.processTelemetry('rover', makeSensors({
|
service.processTelemetry('rover', makeSensors({
|
||||||
wheelOvercurrents: { leftWheel: true },
|
wheelOvercurrents: { leftWheel: true },
|
||||||
wheelSpeedsMmPerSecond: { left: 0 },
|
wheelSpeedsMmPerSecond: { left: 0 },
|
||||||
@@ -237,7 +239,7 @@ test('wheel comparison follows scaled output and resets after reversal', () => {
|
|||||||
const reversed = service.processTelemetry('rover', makeSensors({
|
const reversed = service.processTelemetry('rover', makeSensors({
|
||||||
wheelOvercurrents: { leftWheel: true },
|
wheelOvercurrents: { leftWheel: true },
|
||||||
wheelSpeedsMmPerSecond: { left: -250 },
|
wheelSpeedsMmPerSecond: { left: -250 },
|
||||||
}), start + 700);
|
}), start + 900);
|
||||||
assert.equal(reversed.motors.leftWheel.classification, 'unknown');
|
assert.equal(reversed.motors.leftWheel.classification, 'unknown');
|
||||||
assert.equal(reversed.motors.leftWheel.progressRatio, null);
|
assert.equal(reversed.motors.leftWheel.progressRatio, null);
|
||||||
});
|
});
|
||||||
@@ -248,14 +250,14 @@ test('a stopped drive stays blocked until both clear time and neutral are observ
|
|||||||
service.protectCommand('rover', 'drive', {
|
service.protectCommand('rover', 'drive', {
|
||||||
driveDirect: { left: 300, right: 300 },
|
driveDirect: { left: 300, right: 300 },
|
||||||
});
|
});
|
||||||
for (let step = 0; step <= 13; step += 1) {
|
for (let step = 0; step <= 19; step += 1) {
|
||||||
service.processTelemetry('rover', makeSensors({
|
service.processTelemetry('rover', makeSensors({
|
||||||
wheelOvercurrents: { leftWheel: true },
|
wheelOvercurrents: { leftWheel: true },
|
||||||
wheelSpeedsMmPerSecond: { left: 0 },
|
wheelSpeedsMmPerSecond: { left: 0 },
|
||||||
}), start + step * 100);
|
}), start + step * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let step = 14; step <= 28; step += 1) {
|
for (let step = 20; step <= 52; step += 1) {
|
||||||
service.processTelemetry('rover', makeSensors(), start + step * 100);
|
service.processTelemetry('rover', makeSensors(), start + step * 100);
|
||||||
}
|
}
|
||||||
assert.equal(service.getPublicState('rover').drive.blocked, true);
|
assert.equal(service.getPublicState('rover').drive.blocked, true);
|
||||||
@@ -276,6 +278,34 @@ test('a stopped drive stays blocked until both clear time and neutral are observ
|
|||||||
assert.deepEqual(resumed.driveDirect, { left: 300, right: 300 });
|
assert.deepEqual(resumed.driveDirect, { left: 300, right: 300 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cleared wheel stress remains visible through the hold and drains to idle', () => {
|
||||||
|
const { service } = createHarness();
|
||||||
|
const start = Date.now();
|
||||||
|
service.protectCommand('rover', 'drive', {
|
||||||
|
driveDirect: { left: 300, right: 300 },
|
||||||
|
});
|
||||||
|
service.processTelemetry('rover', makeSensors({
|
||||||
|
wheelOvercurrents: { leftWheel: true },
|
||||||
|
wheelSpeedsMmPerSecond: { left: 0 },
|
||||||
|
}), start);
|
||||||
|
service.processTelemetry('rover', makeSensors({
|
||||||
|
wheelOvercurrents: { leftWheel: true },
|
||||||
|
wheelSpeedsMmPerSecond: { left: 0 },
|
||||||
|
}), start + 100);
|
||||||
|
|
||||||
|
const justCleared = service.processTelemetry('rover', makeSensors(), start + 200);
|
||||||
|
assert.equal(justCleared.status, 'recovering');
|
||||||
|
assert.ok(justCleared.motors.leftWheel.stress > 0);
|
||||||
|
|
||||||
|
const held = service.processTelemetry('rover', makeSensors(), start + 800);
|
||||||
|
assert.equal(held.status, 'recovering');
|
||||||
|
assert.ok(held.motors.leftWheel.stress > 0);
|
||||||
|
|
||||||
|
const recovered = service.processTelemetry('rover', makeSensors(), start + 1000);
|
||||||
|
assert.equal(recovered.status, 'idle');
|
||||||
|
assert.equal(recovered.motors.leftWheel.stress, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('brush stress limits only the brush that reports overcurrent', () => {
|
test('brush stress limits only the brush that reports overcurrent', () => {
|
||||||
const { service } = createHarness();
|
const { service } = createHarness();
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user