rover manager improvements and event bus

This commit is contained in:
legop3
2025-11-19 16:49:53 -05:00
parent 598c0404a2
commit d49ec34f0a
3 changed files with 112 additions and 5 deletions
+1
View File
@@ -8,6 +8,7 @@ require('./src/helpers/sensorDecoder');
require('./src/services/alertService');
require('./src/services/authService');
require('./src/services/eventBus');
require('./src/services/modeManager');
require('./src/services/lockdownGuard');
require('./src/services/roverManager');
+55
View File
@@ -0,0 +1,55 @@
const EventEmitter = require('events');
const logger = require('../globals/logger').child('eventBus');
const eventBus = new EventEmitter();
/**
* Publish a structured event onto the server-wide bus.
* @param {object} options
* @param {string} options.source - Origin module identifier.
* @param {string} options.type - Event type name.
* @param {any} [options.payload] - Optional event payload.
*/
function publishEvent({ source, type, payload = null }) {
if (!source) {
throw new Error('eventBus.publishEvent requires source');
}
if (!type) {
throw new Error('eventBus.publishEvent requires type');
}
const event = {
source,
type,
payload,
ts: Date.now(),
};
logger.debug('Publishing event', { source, type });
eventBus.emit(type, event);
eventBus.emit('*', event);
}
/**
* Subscribe to events of a given type.
* @param {string} type
* @param {(event: object) => void} handler
*/
function subscribe(type, handler) {
eventBus.on(type, handler);
return () => eventBus.off(type, handler);
}
/**
* Subscribe to all events on the bus.
* @param {(event: object) => void} handler
*/
function subscribeAll(handler) {
eventBus.on('*', handler);
return () => eventBus.off('*', handler);
}
module.exports = {
eventBus,
publishEvent,
subscribe,
subscribeAll,
};
+56 -5
View File
@@ -21,6 +21,8 @@ function ensureRecord(id) {
lastSensor: null,
drivers: new Set(),
locked: false,
lockReason: null,
batteryState: null,
room: `rover:${id}`,
lastSeen: Date.now(),
});
@@ -62,20 +64,32 @@ function removeRover(id) {
managerEvents.emit('rover', { roverId: id, action: 'removed' });
}
function lockRover(id, locked, actorSocket) {
function lockRover(id, locked, options = {}) {
const record = rovers.get(id);
if (!record) {
throw new Error('Unknown rover');
}
const reason = locked ? options.reason || 'manual' : null;
const silent = Boolean(options.silent);
if (locked) {
record.locked = true;
sendAlert({ color: COLORS.warn, title: 'Rover Locked', message: `${id} locked by admin.` });
record.lockReason = reason;
if (!silent) {
sendAlert({
color: COLORS.warn,
title: 'Rover Locked',
message: `${id} locked${record.lockReason ? ` (${record.lockReason})` : ''}.`,
});
}
} else {
record.locked = false;
sendAlert({ color: COLORS.success, title: 'Rover Unlocked', message: `${id} unlocked.` });
record.lockReason = null;
if (!silent) {
sendAlert({ color: COLORS.success, title: 'Rover Unlocked', message: `${id} unlocked.` });
}
}
broadcastRoster();
managerEvents.emit('lock', { roverId: id, locked: record.locked });
managerEvents.emit('lock', { roverId: id, locked: record.locked, reason: record.lockReason });
return record.locked;
}
@@ -84,10 +98,12 @@ function getRoster() {
id: record.id,
name: record.meta?.name || record.id,
battery: record.meta?.battery,
batteryState: record.batteryState,
maxWheelSpeed: record.meta?.maxWheelSpeed,
media: record.meta?.media,
cameraServo: record.meta?.cameraServo,
locked: record.locked,
lockReason: record.lockReason,
lastSeen: record.lastSeen,
}));
}
@@ -96,17 +112,52 @@ function broadcastRoster() {
io.emit('rovers', getRoster());
}
function computeBatteryState(record, sensors) {
if (!record) return null;
if (!sensors) return record.batteryState;
const config = record.meta?.battery || null;
const charge = sensors?.batteryChargeMah ?? null;
const capacity = sensors?.batteryCapacityMah ?? null;
const full = typeof config?.Full === 'number' ? config.Full : null;
const warn = typeof config?.Warn === 'number' ? config.Warn : null;
const urgent = typeof config?.Urgent === 'number' ? config.Urgent : null;
let percent = null;
if (charge != null && warn != null && full != null && full > warn) {
const span = full - warn;
percent = (charge - warn) / span;
} else if (charge != null && capacity != null && capacity > 0) {
percent = charge / capacity;
}
if (percent != null) {
percent = Math.max(0, Math.min(1, percent));
}
return {
charge,
capacity,
full,
warn,
urgent,
percent,
percentDisplay: percent == null ? null : Math.round(percent * 100),
warnActive: Boolean(warn != null && charge != null && charge <= warn),
urgentActive: Boolean(urgent != null && charge != null && charge <= urgent),
updatedAt: Date.now(),
};
}
function handleSensorFrame(roverId, frame) {
const record = rovers.get(roverId);
if (!record) return;
record.lastSeen = Date.now();
const decoded = parseSensorFrame(frame.data);
record.lastSensor = { raw: frame, decoded };
record.batteryState = computeBatteryState(record, decoded);
io.to(record.room).emit('sensorFrame', {
roverId,
frame,
sensors: decoded,
});
managerEvents.emit('sensor', { roverId, sensors: decoded, batteryState: record.batteryState });
}
function removeSocket(socket) {
@@ -274,7 +325,7 @@ io.on('connection', (socket) => {
return;
}
try {
lockRover(roverId, locked);
lockRover(roverId, locked, { reason: 'manual' });
logger.info('Lock state changed', roverId, locked);
cb({ success: true });
} catch (err) {