Files
MultiRoombaRover/server/public/src/services/roverUI.js
T
2025-11-14 02:29:11 -05:00

186 lines
6.0 KiB
JavaScript

registerModule('services/roverUI', (require, exports) => {
const { socket } = require('globals/socket');
const { formatHex } = require('helpers/formatters');
const videoPlayer = require('services/videoPlayer');
const state = require('services/state');
const statusEl = document.getElementById('status');
const roverSelect = document.getElementById('roverSelect');
const sensorOutput = document.getElementById('sensorOutput');
const lockBtn = document.getElementById('lockToggle');
const forceCheckbox = document.getElementById('forceControl');
const adminControls = document.getElementById('adminControls');
const modeControls = document.getElementById('modeControls');
const modeSelect = document.getElementById('modeSelect');
const activeDriverEl = document.getElementById('activeDriver');
const currentRoverEl = document.getElementById('currentRover');
const videoContainer = document.getElementById('videoContainer');
const driverVideo = document.getElementById('driverVideo');
const videoStatus = document.getElementById('videoStatus');
const videoReconnect = document.getElementById('videoReconnect');
let roster = [];
let lastSelection = null;
let currentVideoRover = null;
state.onRoleChange((role) => {
const isAdmin = role === 'admin' || role === 'lockdown';
if (lockBtn) lockBtn.disabled = !isAdmin;
if (roverSelect) roverSelect.disabled = !isAdmin;
if (adminControls) {
adminControls.style.display = isAdmin ? 'flex' : 'none';
}
if (modeControls) {
modeControls.style.display = isAdmin ? 'block' : 'none';
if (modeSelect) modeSelect.disabled = !isAdmin;
}
});
socket.on('rovers', (list) => {
roster = list;
roverSelect.innerHTML = '';
list.forEach((rover) => {
const option = document.createElement('option');
option.value = rover.id;
option.textContent = `${rover.name}${rover.locked ? ' (locked)' : ''}`;
roverSelect.appendChild(option);
});
const selected = state.getSelected();
if ((!selected || !list.find((r) => r.id === selected)) && list.length) {
state.setSelected(list[0].id);
} else if (!list.length) {
state.setSelected(null);
}
renderSelection(true);
});
roverSelect.addEventListener('change', () => {
const roverId = roverSelect.value;
const role = state.getRole();
state.setSelected(roverId);
renderSelection(true);
if (role === 'admin' || role === 'lockdown') {
socket.emit('requestControl', { roverId, force: !!forceCheckbox?.checked });
}
});
socket.on('controlGranted', ({ roverId }) => {
state.setSelected(roverId);
renderSelection(true);
statusEl.textContent = `Driving ${roverId}`;
maybeConnectVideo(true);
});
socket.on('sensorFrame', ({ roverId, frame, sensors }) => {
if (roverId !== state.getSelected()) return;
const lines = [];
if (sensors) {
Object.entries(sensors).forEach(([key, value]) => {
lines.push(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
});
}
lines.push('', 'raw:', formatHex(frame?.data));
sensorOutput.textContent = lines.join('\n');
});
socket.on('activeDriver', ({ roverId, socketId }) => {
if (roverId !== state.getSelected()) return;
if (activeDriverEl) {
activeDriverEl.textContent = `Active driver: ${socketId || 'none'}`;
}
});
lockBtn?.addEventListener('click', () => {
const roverId = state.getSelected();
const rover = roster.find((r) => r.id === roverId);
if (!rover) return;
socket.emit('lockRover', { roverId, locked: !rover.locked });
});
modeSelect?.addEventListener('change', () => {
socket.emit('setMode', { mode: modeSelect.value });
});
socket.on('mode', ({ mode }) => {
if (modeSelect) {
modeSelect.value = mode;
}
});
videoReconnect?.addEventListener('click', () => maybeConnectVideo(true));
socket.on('connect', () => {
statusEl.textContent = 'Connected';
});
socket.on('disconnect', () => {
statusEl.textContent = 'Disconnected';
});
function renderSelection(clearOutput) {
const roverId = state.getSelected();
if (roverSelect && roverId) {
roverSelect.value = roverId;
} else if (roverSelect && !roverId) {
roverSelect.value = '';
}
if (currentRoverEl) {
currentRoverEl.textContent = `Active rover: ${roverId || '--'}`;
}
if (sensorOutput && (clearOutput || roverId !== lastSelection)) {
sensorOutput.textContent = roverId ? 'Waiting for sensor data...' : '';
}
lastSelection = roverId;
maybeConnectVideo(clearOutput);
}
function maybeConnectVideo(force = false) {
const roverId = state.getSelected();
if (!roverId) {
currentVideoRover = null;
videoPlayer.stopStream('driver');
setVideoStatus('No rover selected');
return;
}
if (!force && roverId === currentVideoRover) {
return;
}
currentVideoRover = roverId;
setVideoStatus('Connecting video…');
socket.emit('video:request', { roverId }, (resp = {}) => {
if (resp.error) {
setVideoStatus(`Video error: ${resp.error}`);
videoPlayer.stopStream('driver');
return;
}
const { url, token } = resp;
videoPlayer
.startStream('driver', {
url,
token,
mount: videoContainer,
videoEl: driverVideo,
onStatus: (state, detail) => {
if (state === 'playing') {
setVideoStatus('Video live');
} else if (state === 'connecting') {
setVideoStatus('Connecting video…');
} else if (state === 'error') {
setVideoStatus(`Video error: ${detail}`);
} else if (state === 'stopped') {
setVideoStatus('Video stopped');
}
},
})
.catch((err) => {
setVideoStatus(`Video error: ${err.message}`);
});
});
}
function setVideoStatus(message) {
if (videoStatus) {
videoStatus.textContent = message;
}
}
});