diff --git a/server/src/services/overcurrentProtectionService/index.js b/server/src/services/overcurrentProtectionService/index.js index 38fb81e3..cd28faa2 100644 --- a/server/src/services/overcurrentProtectionService/index.js +++ b/server/src/services/overcurrentProtectionService/index.js @@ -6,10 +6,15 @@ const logger = require('../../globals/logger').child('overcurrentProtectionServi const DEFAULT_CONFIG = Object.freeze({ minimumUsefulWheelIntent: 75, - stressGrace: 0.2, - baseWheelOvercurrentRatePerSec: 0.75, - stalledWheelAdditionalRatePerSec: 1.25, - wheelRecoveryRatePerSec: 1, + // A fully stalled wheel now accumulates 0.25 stress per second, producing a + // roughly four-second hard-stop window. The smaller base rate also prevents + // a wheel that is still making progress from being treated like a hard jam. + stressGrace: 0.25, + baseWheelOvercurrentRatePerSec: 0.08, + stalledWheelAdditionalRatePerSec: 0.17, + // Clear telemetry removes stress faster than even a complete stall adds it, + // so short threshold climbs and direction-change spikes do not linger. + wheelRecoveryRatePerSec: 0.5, brushOvercurrentRatePerSec: 1, brushRecoveryRatePerSec: 0.75, clearBeforeUnlockSec: 0.75, diff --git a/server/src/services/overcurrentProtectionService/index.test.js b/server/src/services/overcurrentProtectionService/index.test.js index 8287225a..970baef5 100644 --- a/server/src/services/overcurrentProtectionService/index.test.js +++ b/server/src/services/overcurrentProtectionService/index.test.js @@ -48,7 +48,7 @@ test('a short stalled-wheel spike remains inside the grace region', () => { wheelSpeedsMmPerSecond: { left: 0 }, }), start + 100); - assert.equal(snapshot.motors.leftWheel.stress, 0.2); + assert.equal(snapshot.motors.leftWheel.stress, 0.025); assert.equal(snapshot.motors.leftWheel.cap, 1); assert.equal(snapshot.status, 'idle'); }); @@ -60,13 +60,24 @@ test('persistent stalled-wheel overcurrent scales both wheels and then stops dri driveDirect: { left: 300, right: 200 }, }); - for (let step = 0; step <= 5; step += 1) { + for (let step = 0; step <= 39; step += 1) { service.processTelemetry('rover', makeSensors({ wheelOvercurrents: { leftWheel: true }, wheelSpeedsMmPerSecond: { left: 0, right: 200 }, }), start + step * 100); } + /* + Thirty-nine accumulated 100 ms intervals represent 3.9 seconds at the + maximum 0.25/s rate. The drive must still be available immediately before + the intended four-second hard-stop boundary. + */ + assert.equal(service.getPublicState('rover').drive.blocked, false); + service.processTelemetry('rover', makeSensors({ + wheelOvercurrents: { leftWheel: true }, + wheelSpeedsMmPerSecond: { left: 0, right: 200 }, + }), start + 4000); + const snapshot = service.getPublicState('rover'); assert.equal(snapshot.status, 'stopped'); assert.equal(snapshot.drive.blocked, true); @@ -85,10 +96,15 @@ test('persistent stalled-wheel overcurrent scales both wheels and then stops dri const scaledDrive = issued.find((entry) => entry.payload.type === 'drive' && entry.payload.driveDirect.left > 0); assert.ok(scaledDrive); - assert.equal( - scaledDrive.payload.driveDirect.left / 300, - scaledDrive.payload.driveDirect.right / 200, - ); + const leftScale = scaledDrive.payload.driveDirect.left / 300; + const rightScale = scaledDrive.payload.driveDirect.right / 200; + /* + Motor commands are integers, so applying one shared fractional cap can + round the two differently sized wheel commands by different sub-unit + amounts. A one-percent tolerance verifies the shared curve without + pretending integer transport preserves an exact floating-point ratio. + */ + assert.ok(Math.abs(leftScale - rightScale) < 0.01); }); test('administrator commands and telemetry bypass all enforcement', () => { @@ -121,14 +137,14 @@ test('a stopped drive stays blocked until both clear time and neutral are observ service.protectCommand('rover', 'drive', { driveDirect: { left: 300, right: 300 }, }); - for (let step = 0; step <= 5; step += 1) { + for (let step = 0; step <= 40; step += 1) { service.processTelemetry('rover', makeSensors({ wheelOvercurrents: { leftWheel: true }, wheelSpeedsMmPerSecond: { left: 0 }, }), start + step * 100); } - for (let step = 6; step <= 14; step += 1) { + for (let step = 41; step <= 61; step += 1) { service.processTelemetry('rover', makeSensors(), start + step * 100); } assert.equal(service.getPublicState('rover').drive.blocked, true);