mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 10:42:45 -05:00
117 lines
5.7 KiB
JavaScript
117 lines
5.7 KiB
JavaScript
// Complete card snapshots; live diagnostics opt into 10 Hz, ordinary cards use 1 Hz.
|
|
const io = require('../../globals/io');
|
|
const roverManager = require('../roverManager');
|
|
const { isVerified } = require('../verificationService');
|
|
const { listModels } = require('./models');
|
|
const { compatible } = require('./capabilities');
|
|
|
|
function createGateway({ root, driving, getSystemState, activity }) {
|
|
const bindings = new Map();
|
|
const viewers = new Map();
|
|
let models = [];
|
|
let modelError = null;
|
|
let lastCatalog = 0;
|
|
let closed = false;
|
|
let refreshing = false;
|
|
let lastOrdinary = 0;
|
|
|
|
function stateFor(socket, roverId) {
|
|
if (!isVerified(socket)) return { available: false };
|
|
const visible = roverId && roverManager.canSeeRover(roverId, socket);
|
|
const id = visible ? roverId : null;
|
|
const session = id ? driving.getState(id) : null;
|
|
const canControl = Boolean(id && driving.canControl(socket, id));
|
|
const system = getSystemState();
|
|
return {
|
|
available: true, roverId: id, updatedAt: Date.now(),
|
|
control: { canStart: canControl && !session, canStop: Boolean(session && (canControl || session.socketId === socket.id)),
|
|
reason: !id ? 'Select a rover first' : !canControl ? 'You do not currently have control of this rover' : null },
|
|
session,
|
|
models: models.filter((model) => model.trainedRovers?.some((rover) => roverManager.canSeeRover(rover, socket)))
|
|
.map((model) => ({ id: model.id, name: model.name, createdAt: model.createdAt,
|
|
trainingSteps: model.trainingSteps, examplesProcessed: model.examplesProcessed,
|
|
weightBytes: model.weightBytes, parameters: model.parameters, trainingLoss: model.trainingLoss,
|
|
evaluation: model.evaluation, validation: model.validation, distinctMinutes: model.distinctMinutes, controls: model.specification?.commands?.length || 0,
|
|
compatible: Boolean(id && compatible(model, roverManager.rovers.get(id))) })),
|
|
catalogError: modelError,
|
|
recording: { ...system.recording, sessions: system.recording.sessions.filter((s) => roverManager.canSeeRover(s.roverId, socket)) },
|
|
training: { ...system.training, sessions: undefined },
|
|
limits: system.limits,
|
|
activity: system.activity.filter((entry) => !entry.roverId || roverManager.canSeeRover(entry.roverId, socket)),
|
|
};
|
|
}
|
|
|
|
async function refresh() {
|
|
if (closed || refreshing) return;
|
|
refreshing = true;
|
|
try {
|
|
if (Date.now() - lastCatalog >= 15000) {
|
|
lastCatalog = Date.now();
|
|
try { models = await listModels(root); modelError = null; }
|
|
catch (error) { modelError = error.message; }
|
|
}
|
|
if (closed) return;
|
|
const ordinary = Date.now() - lastOrdinary >= 1000;
|
|
if (ordinary) lastOrdinary = Date.now();
|
|
for (const [socket, view] of viewers) {
|
|
if (ordinary || view.live) socket.volatile.emit('roverLearning:state', stateFor(socket, view.roverId));
|
|
}
|
|
} finally { refreshing = false; }
|
|
}
|
|
|
|
function connect(socket) {
|
|
const subscribe = (payload = {}, reply) => {
|
|
const respond = typeof reply === 'function' ? reply : () => {};
|
|
if (!isVerified(socket)) { respond({ error: 'Verification required' }); return; }
|
|
const roverId = typeof payload?.roverId === 'string' ? payload.roverId : null;
|
|
viewers.set(socket, { roverId, live: payload.live === true });
|
|
respond({ state: stateFor(socket, roverId) });
|
|
refresh().catch((error) => activity('Card update failed', { error: error.message }));
|
|
};
|
|
const action = async (payload = {}, reply) => {
|
|
const respond = typeof reply === 'function' ? reply : () => {};
|
|
try {
|
|
if (!isVerified(socket)) throw new Error('Verification required');
|
|
const roverId = String(payload.roverId || '');
|
|
if (payload.action === 'start') await driving.start(socket, roverId, String(payload.modelId || ''));
|
|
else if (payload.action === 'stop') {
|
|
const session = driving.getState(roverId);
|
|
if (!session || !(driving.canControl(socket, roverId) || session.socketId === socket.id)) throw new Error('You cannot stop this controller');
|
|
await driving.stop(roverId);
|
|
} else throw new Error('Unknown action');
|
|
viewers.set(socket, { roverId, live: viewers.get(socket)?.live || false });
|
|
respond({ state: stateFor(socket, roverId) });
|
|
} catch (error) { respond({ error: error.message }); }
|
|
refresh().catch((error) => activity('Card update failed', { error: error.message }));
|
|
};
|
|
const unsubscribe = () => viewers.delete(socket);
|
|
const disconnect = () => { unsubscribe(); detach(socket); };
|
|
const handlers = { 'roverLearning:subscribe': subscribe, 'roverLearning:action': action,
|
|
'roverLearning:unsubscribe': unsubscribe, disconnect };
|
|
for (const [name, handler] of Object.entries(handlers)) socket.on(name, handler);
|
|
bindings.set(socket, handlers);
|
|
}
|
|
|
|
function detach(socket) {
|
|
for (const [name, handler] of Object.entries(bindings.get(socket) || {})) socket.off(name, handler);
|
|
bindings.delete(socket);
|
|
}
|
|
|
|
io.on('connection', connect);
|
|
for (const socket of io.sockets.sockets.values()) connect(socket);
|
|
const timer = setInterval(() => { refresh().catch((error) => activity('Card update failed', { error: error.message })); }, 100);
|
|
refresh().catch((error) => activity('Catalog unavailable', { error: error.message }));
|
|
return {
|
|
close() {
|
|
closed = true;
|
|
clearInterval(timer);
|
|
io.off('connection', connect);
|
|
for (const socket of viewers.keys()) socket.emit('roverLearning:state', { available: false });
|
|
viewers.clear();
|
|
for (const socket of bindings.keys()) detach(socket);
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { createGateway };
|