mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 10:00:46 -04:00
rover manager improvements and event bus
This commit is contained in:
@@ -8,6 +8,7 @@ require('./src/helpers/sensorDecoder');
|
|||||||
|
|
||||||
require('./src/services/alertService');
|
require('./src/services/alertService');
|
||||||
require('./src/services/authService');
|
require('./src/services/authService');
|
||||||
|
require('./src/services/eventBus');
|
||||||
require('./src/services/modeManager');
|
require('./src/services/modeManager');
|
||||||
require('./src/services/lockdownGuard');
|
require('./src/services/lockdownGuard');
|
||||||
require('./src/services/roverManager');
|
require('./src/services/roverManager');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -21,6 +21,8 @@ function ensureRecord(id) {
|
|||||||
lastSensor: null,
|
lastSensor: null,
|
||||||
drivers: new Set(),
|
drivers: new Set(),
|
||||||
locked: false,
|
locked: false,
|
||||||
|
lockReason: null,
|
||||||
|
batteryState: null,
|
||||||
room: `rover:${id}`,
|
room: `rover:${id}`,
|
||||||
lastSeen: Date.now(),
|
lastSeen: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -62,20 +64,32 @@ function removeRover(id) {
|
|||||||
managerEvents.emit('rover', { roverId: id, action: 'removed' });
|
managerEvents.emit('rover', { roverId: id, action: 'removed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockRover(id, locked, actorSocket) {
|
function lockRover(id, locked, options = {}) {
|
||||||
const record = rovers.get(id);
|
const record = rovers.get(id);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw new Error('Unknown rover');
|
throw new Error('Unknown rover');
|
||||||
}
|
}
|
||||||
|
const reason = locked ? options.reason || 'manual' : null;
|
||||||
|
const silent = Boolean(options.silent);
|
||||||
if (locked) {
|
if (locked) {
|
||||||
record.locked = true;
|
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 {
|
} else {
|
||||||
record.locked = false;
|
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();
|
broadcastRoster();
|
||||||
managerEvents.emit('lock', { roverId: id, locked: record.locked });
|
managerEvents.emit('lock', { roverId: id, locked: record.locked, reason: record.lockReason });
|
||||||
return record.locked;
|
return record.locked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,10 +98,12 @@ function getRoster() {
|
|||||||
id: record.id,
|
id: record.id,
|
||||||
name: record.meta?.name || record.id,
|
name: record.meta?.name || record.id,
|
||||||
battery: record.meta?.battery,
|
battery: record.meta?.battery,
|
||||||
|
batteryState: record.batteryState,
|
||||||
maxWheelSpeed: record.meta?.maxWheelSpeed,
|
maxWheelSpeed: record.meta?.maxWheelSpeed,
|
||||||
media: record.meta?.media,
|
media: record.meta?.media,
|
||||||
cameraServo: record.meta?.cameraServo,
|
cameraServo: record.meta?.cameraServo,
|
||||||
locked: record.locked,
|
locked: record.locked,
|
||||||
|
lockReason: record.lockReason,
|
||||||
lastSeen: record.lastSeen,
|
lastSeen: record.lastSeen,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -96,17 +112,52 @@ function broadcastRoster() {
|
|||||||
io.emit('rovers', getRoster());
|
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) {
|
function handleSensorFrame(roverId, frame) {
|
||||||
const record = rovers.get(roverId);
|
const record = rovers.get(roverId);
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
record.lastSeen = Date.now();
|
record.lastSeen = Date.now();
|
||||||
const decoded = parseSensorFrame(frame.data);
|
const decoded = parseSensorFrame(frame.data);
|
||||||
record.lastSensor = { raw: frame, decoded };
|
record.lastSensor = { raw: frame, decoded };
|
||||||
|
record.batteryState = computeBatteryState(record, decoded);
|
||||||
io.to(record.room).emit('sensorFrame', {
|
io.to(record.room).emit('sensorFrame', {
|
||||||
roverId,
|
roverId,
|
||||||
frame,
|
frame,
|
||||||
sensors: decoded,
|
sensors: decoded,
|
||||||
});
|
});
|
||||||
|
managerEvents.emit('sensor', { roverId, sensors: decoded, batteryState: record.batteryState });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSocket(socket) {
|
function removeSocket(socket) {
|
||||||
@@ -274,7 +325,7 @@ io.on('connection', (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
lockRover(roverId, locked);
|
lockRover(roverId, locked, { reason: 'manual' });
|
||||||
logger.info('Lock state changed', roverId, locked);
|
logger.info('Lock state changed', roverId, locked);
|
||||||
cb({ success: true });
|
cb({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user