mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
virtual wall private rover safety
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -78,7 +78,7 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-DHQ5vi8_.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BnsUBjx0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DFtJPk7c.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -26,6 +26,13 @@ const DEFAULT_PRIVATE_SAFETY = Object.freeze({
|
||||
cliffEnabled: false,
|
||||
cliffBackoffSpeed: 250,
|
||||
cliffBackoffMs: 500,
|
||||
// Private rovers live in the sensitive area, so the virtual wall guard is
|
||||
// default-on even though the older safety features remain opt-in. A missing
|
||||
// value from an older rover config should therefore behave like "enabled",
|
||||
// not like a deliberate disabled setting.
|
||||
virtualWallEnabled: true,
|
||||
virtualWallBackoffSpeed: 250,
|
||||
virtualWallBackoffMs: 500,
|
||||
triggerCooldownMs: 800,
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,26 @@ function normalizePrivateSafety(raw = {}) {
|
||||
5000,
|
||||
DEFAULT_PRIVATE_SAFETY.cliffBackoffMs,
|
||||
),
|
||||
// Unlike most private safety toggles, virtual wall support is meant to be
|
||||
// enabled by default for private rovers. The nullish check preserves that
|
||||
// default for older rover configs while still allowing lockdown admins to
|
||||
// explicitly turn the guard off.
|
||||
virtualWallEnabled:
|
||||
source.virtualWallEnabled == null
|
||||
? DEFAULT_PRIVATE_SAFETY.virtualWallEnabled
|
||||
: Boolean(source.virtualWallEnabled),
|
||||
virtualWallBackoffSpeed: clampInt(
|
||||
source.virtualWallBackoffSpeed,
|
||||
1,
|
||||
500,
|
||||
DEFAULT_PRIVATE_SAFETY.virtualWallBackoffSpeed,
|
||||
),
|
||||
virtualWallBackoffMs: clampInt(
|
||||
source.virtualWallBackoffMs,
|
||||
100,
|
||||
5000,
|
||||
DEFAULT_PRIVATE_SAFETY.virtualWallBackoffMs,
|
||||
),
|
||||
triggerCooldownMs: clampInt(
|
||||
source.triggerCooldownMs,
|
||||
100,
|
||||
|
||||
@@ -45,6 +45,8 @@ function createSensorPipeline(deps) {
|
||||
lastOvercurrent: false,
|
||||
lastBump: false,
|
||||
lastCliff: false,
|
||||
lastVirtualWall: false,
|
||||
lastDriveDirection: null,
|
||||
});
|
||||
}
|
||||
return privateSafetyStates.get(roverId);
|
||||
@@ -63,6 +65,9 @@ function createSensorPipeline(deps) {
|
||||
const cooldownMs = clampInt(options.cooldownMs, 100, 10000, DEFAULT_PRIVATE_SAFETY.triggerCooldownMs);
|
||||
const backoffMs = clampInt(options.backoffMs, 50, 5000, 0);
|
||||
const backoffSpeed = clampInt(options.backoffSpeed, 0, 500, 0);
|
||||
const explicitBackoffDrive = options.backoffDrive && typeof options.backoffDrive === 'object'
|
||||
? options.backoffDrive
|
||||
: null;
|
||||
try {
|
||||
issueCommand(roverId, { type: 'drive', driveDirect: { left: 0, right: 0 } });
|
||||
issueCommand(roverId, { type: 'motors', motorPwm: { main: 0, side: 0, vacuum: 0 } });
|
||||
@@ -70,10 +75,14 @@ function createSensorPipeline(deps) {
|
||||
logger.warn('Private safety stop failed', { roverId, mode, error: err.message });
|
||||
}
|
||||
stopSafetyBackoffTimer(roverId);
|
||||
if (backoffMs > 0 && backoffSpeed > 0) {
|
||||
if (backoffMs > 0 && (backoffSpeed > 0 || explicitBackoffDrive)) {
|
||||
// Bump and cliff safety only need a simple straight reverse. Virtual wall
|
||||
// safety can be hit while turning or arcing, so it may pass an explicit
|
||||
// per-wheel escape command that reverses the last commanded wheel signs.
|
||||
const speed = Math.max(SAFETY_BACKOFF_MIN, Math.min(SAFETY_BACKOFF_MAX, -Math.abs(backoffSpeed)));
|
||||
const backoffDrive = explicitBackoffDrive || { left: speed, right: speed };
|
||||
try {
|
||||
issueCommand(roverId, { type: 'drive', driveDirect: { left: speed, right: speed } });
|
||||
issueCommand(roverId, { type: 'drive', driveDirect: backoffDrive });
|
||||
} catch (err) {
|
||||
logger.warn('Private safety backoff failed', { roverId, mode, error: err.message });
|
||||
}
|
||||
@@ -100,10 +109,54 @@ function createSensorPipeline(deps) {
|
||||
publishEvent({
|
||||
source: 'roverManager',
|
||||
type: 'rover.privateSafetyTriggered',
|
||||
payload: { roverId, mode, cooldownMs, backoffMs, backoffSpeed },
|
||||
payload: { roverId, mode, cooldownMs, backoffMs, backoffSpeed, backoffDrive: explicitBackoffDrive },
|
||||
});
|
||||
}
|
||||
|
||||
function rememberPrivateDriveDirection(roverId, driveDirect = null) {
|
||||
if (!roverId || !driveDirect || typeof driveDirect !== 'object') return;
|
||||
const left = clampInt(driveDirect.left, -500, 500, 0);
|
||||
const right = clampInt(driveDirect.right, -500, 500, 0);
|
||||
if (left === 0 && right === 0) return;
|
||||
const state = getPrivateSafetyState(String(roverId));
|
||||
// Store signs instead of raw speeds because safety escape speed is its own
|
||||
// configured value. The thing we need from the driver command is direction,
|
||||
// not magnitude, so later speed-limit changes cannot make the remembered
|
||||
// state stale or unsafe.
|
||||
state.lastDriveDirection = {
|
||||
left: Math.sign(left),
|
||||
right: Math.sign(right),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function getOppositePrivateDrive(record, speed) {
|
||||
const state = getPrivateSafetyState(record.id);
|
||||
const direction = state.lastDriveDirection || null;
|
||||
const safeSpeed = clampInt(speed, 1, 500, DEFAULT_PRIVATE_SAFETY.virtualWallBackoffSpeed);
|
||||
if (!direction) {
|
||||
// If the rover has not received a remembered drive command yet, straight
|
||||
// reverse is the least surprising escape because virtual walls are meant
|
||||
// to block forward travel into a restricted area.
|
||||
return { left: -safeSpeed, right: -safeSpeed };
|
||||
}
|
||||
|
||||
let leftSign = Number(direction.left) || 0;
|
||||
let rightSign = Number(direction.right) || 0;
|
||||
if (leftSign === 0 && rightSign === 0) {
|
||||
leftSign = 1;
|
||||
rightSign = 1;
|
||||
} else if (leftSign === 0) {
|
||||
leftSign = rightSign;
|
||||
} else if (rightSign === 0) {
|
||||
rightSign = leftSign;
|
||||
}
|
||||
return {
|
||||
left: -leftSign * safeSpeed,
|
||||
right: -rightSign * safeSpeed,
|
||||
};
|
||||
}
|
||||
|
||||
function evaluatePrivateSafety(record, sensors) {
|
||||
if (!record || !sensors) return;
|
||||
const roverId = record.id;
|
||||
@@ -118,14 +171,17 @@ function createSensorPipeline(deps) {
|
||||
const cliff = Boolean(
|
||||
sensors?.cliffLeft || sensors?.cliffFrontLeft || sensors?.cliffFrontRight || sensors?.cliffRight,
|
||||
);
|
||||
const virtualWall = Boolean(sensors?.virtualWall);
|
||||
const currentOver = overcurrent;
|
||||
const currentBump = bump;
|
||||
const currentCliff = cliff;
|
||||
const currentVirtualWall = virtualWall;
|
||||
if (!shouldApplyPrivateSensorSafety(record)) {
|
||||
state.blockedUntil = 0;
|
||||
state.lastOvercurrent = currentOver;
|
||||
state.lastBump = currentBump;
|
||||
state.lastCliff = currentCliff;
|
||||
state.lastVirtualWall = currentVirtualWall;
|
||||
return;
|
||||
}
|
||||
const safety = getPrivateSafety(record);
|
||||
@@ -134,6 +190,7 @@ function createSensorPipeline(deps) {
|
||||
state.lastOvercurrent = currentOver;
|
||||
state.lastBump = currentBump;
|
||||
state.lastCliff = currentCliff;
|
||||
state.lastVirtualWall = currentVirtualWall;
|
||||
return;
|
||||
}
|
||||
let triggered = false;
|
||||
@@ -158,11 +215,20 @@ function createSensorPipeline(deps) {
|
||||
backoffSpeed: safety.cliffBackoffSpeed,
|
||||
});
|
||||
triggered = true;
|
||||
} else if (safety.virtualWallEnabled && currentVirtualWall && !state.lastVirtualWall) {
|
||||
triggerSafetyAction(record, 'virtualWall', {
|
||||
cooldownMs: safety.triggerCooldownMs,
|
||||
backoffMs: safety.virtualWallBackoffMs,
|
||||
backoffSpeed: safety.virtualWallBackoffSpeed,
|
||||
backoffDrive: getOppositePrivateDrive(record, safety.virtualWallBackoffSpeed),
|
||||
});
|
||||
triggered = true;
|
||||
}
|
||||
if (!triggered) state.blockedUntil = 0;
|
||||
state.lastOvercurrent = currentOver;
|
||||
state.lastBump = currentBump;
|
||||
state.lastCliff = currentCliff;
|
||||
state.lastVirtualWall = currentVirtualWall;
|
||||
}
|
||||
|
||||
function applyPrivateDriveSafety(roverId, socket, driveDirect = null) {
|
||||
@@ -170,11 +236,20 @@ function createSensorPipeline(deps) {
|
||||
if (!record || !driveDirect || typeof driveDirect !== 'object') return driveDirect;
|
||||
if (!shouldApplyPrivateSafety(record, socket)) return driveDirect;
|
||||
const safety = getPrivateSafety(record);
|
||||
if (!safety.speedLimitEnabled) return driveDirect;
|
||||
if (!safety.speedLimitEnabled) {
|
||||
rememberPrivateDriveDirection(roverId, driveDirect);
|
||||
return driveDirect;
|
||||
}
|
||||
const limit = clampInt(safety.speedLimitMaxWheelSpeed, 1, 500, DEFAULT_PRIVATE_SAFETY.speedLimitMaxWheelSpeed);
|
||||
const left = clampInt(driveDirect.left, -500, 500, 0);
|
||||
const right = clampInt(driveDirect.right, -500, 500, 0);
|
||||
return { ...driveDirect, left: Math.max(-limit, Math.min(limit, left)), right: Math.max(-limit, Math.min(limit, right)) };
|
||||
const safeDrive = {
|
||||
...driveDirect,
|
||||
left: Math.max(-limit, Math.min(limit, left)),
|
||||
right: Math.max(-limit, Math.min(limit, right)),
|
||||
};
|
||||
rememberPrivateDriveDirection(roverId, safeDrive);
|
||||
return safeDrive;
|
||||
}
|
||||
|
||||
function handlePrivateButtonHold(record, sensors) {
|
||||
|
||||
Reference in New Issue
Block a user