mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
194 lines
5.1 KiB
JavaScript
194 lines
5.1 KiB
JavaScript
const socket = io();
|
|
|
|
const DRIVE_SPEED = 250;
|
|
const TURN_SPEED = 200;
|
|
const STATUS_FLAGS = [
|
|
{ bit: 0x01, label: 'wifi' },
|
|
{ bit: 0x02, label: 'oi-ready' },
|
|
{ bit: 0x04, label: 'sensors' },
|
|
];
|
|
|
|
const state = {
|
|
robots: [],
|
|
selectedRobotId: null,
|
|
telemetry: {},
|
|
activeKeys: new Set(),
|
|
};
|
|
|
|
const robotSelect = document.getElementById('robotSelect');
|
|
const telemetrySummary = document.getElementById('telemetrySummary');
|
|
const sensorList = document.getElementById('sensorList');
|
|
const safeModeBtn = document.getElementById('safeModeBtn');
|
|
const fullModeBtn = document.getElementById('fullModeBtn');
|
|
const enableOiBtn = document.getElementById('enableOiBtn');
|
|
const seekDockBtn = document.getElementById('seekDockBtn');
|
|
const playSongBtn = document.getElementById('playSongBtn');
|
|
const songSlotInput = document.getElementById('songSlot');
|
|
|
|
function flattenSensors(obj, prefix = '') {
|
|
const result = {};
|
|
Object.entries(obj || {}).forEach(([key, value]) => {
|
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
Object.assign(result, flattenSensors(value, path));
|
|
} else {
|
|
result[path] = value;
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function renderRobots() {
|
|
robotSelect.innerHTML = '';
|
|
state.robots.forEach((robot) => {
|
|
const option = document.createElement('option');
|
|
option.value = robot.id;
|
|
option.textContent = robot.id;
|
|
if (robot.id === state.selectedRobotId) {
|
|
option.selected = true;
|
|
}
|
|
robotSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function renderTelemetry() {
|
|
const telemetry = state.telemetry[state.selectedRobotId];
|
|
if (!telemetry) {
|
|
telemetrySummary.textContent = 'No telemetry';
|
|
sensorList.textContent = '';
|
|
return;
|
|
}
|
|
const { header, trailer, sensors } = telemetry;
|
|
const flags = STATUS_FLAGS
|
|
.filter((flag) => header.statusBits & flag.bit)
|
|
.map((flag) => flag.label)
|
|
.join(', ');
|
|
const summaryLines = [
|
|
`Seq: ${header.seq}`,
|
|
`Uptime: ${header.uptimeMs} ms`,
|
|
`Last Control Age: ${header.lastControlAgeMs} ms`,
|
|
`WiFi RSSI: ${header.wifiRssiDbm} dBm`,
|
|
`Status: ${flags || 'none'}`,
|
|
`Applied mm/s: L ${trailer.appliedLeftMmps} | R ${trailer.appliedRightMmps}`,
|
|
`Dropped control packets: ${trailer.droppedControlPackets}`,
|
|
];
|
|
telemetrySummary.textContent = summaryLines.join('\n');
|
|
|
|
if (sensors) {
|
|
const flat = flattenSensors(sensors);
|
|
sensorList.textContent = Object.entries(flat)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join('\n');
|
|
} else {
|
|
sensorList.textContent = 'Sensor block missing';
|
|
}
|
|
}
|
|
|
|
function broadcastDrive() {
|
|
if (!state.selectedRobotId) {
|
|
return;
|
|
}
|
|
const vectors = { w: 0, a: 0, s: 0, d: 0 };
|
|
state.activeKeys.forEach((key) => {
|
|
if (vectors[key] !== undefined) {
|
|
vectors[key] = 1;
|
|
}
|
|
});
|
|
|
|
let left = 0;
|
|
let right = 0;
|
|
if (vectors.w) {
|
|
left += DRIVE_SPEED;
|
|
right += DRIVE_SPEED;
|
|
}
|
|
if (vectors.s) {
|
|
left -= DRIVE_SPEED;
|
|
right -= DRIVE_SPEED;
|
|
}
|
|
if (vectors.a) {
|
|
left -= TURN_SPEED;
|
|
right += TURN_SPEED;
|
|
}
|
|
if (vectors.d) {
|
|
left += TURN_SPEED;
|
|
right -= TURN_SPEED;
|
|
}
|
|
|
|
socket.emit('drive', {
|
|
robotId: state.selectedRobotId,
|
|
left,
|
|
right,
|
|
});
|
|
}
|
|
|
|
function handleKey(event, isDown) {
|
|
const key = event.key.toLowerCase();
|
|
if (!['w', 'a', 's', 'd'].includes(key)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
if (isDown) {
|
|
state.activeKeys.add(key);
|
|
} else {
|
|
state.activeKeys.delete(key);
|
|
}
|
|
broadcastDrive();
|
|
}
|
|
|
|
document.addEventListener('keydown', (event) => handleKey(event, true));
|
|
document.addEventListener('keyup', (event) => handleKey(event, false));
|
|
|
|
robotSelect.addEventListener('change', (event) => {
|
|
state.selectedRobotId = event.target.value;
|
|
renderTelemetry();
|
|
});
|
|
|
|
safeModeBtn.addEventListener('click', () => {
|
|
if (!state.selectedRobotId) return;
|
|
socket.emit('mode', { robotId: state.selectedRobotId, mode: 'SAFE' });
|
|
});
|
|
|
|
fullModeBtn.addEventListener('click', () => {
|
|
if (!state.selectedRobotId) return;
|
|
socket.emit('mode', { robotId: state.selectedRobotId, mode: 'FULL' });
|
|
});
|
|
|
|
enableOiBtn.addEventListener('click', () => {
|
|
if (!state.selectedRobotId) return;
|
|
socket.emit('enableOi', { robotId: state.selectedRobotId });
|
|
});
|
|
|
|
seekDockBtn.addEventListener('click', () => {
|
|
if (!state.selectedRobotId) return;
|
|
socket.emit('seekDock', { robotId: state.selectedRobotId });
|
|
});
|
|
|
|
playSongBtn.addEventListener('click', () => {
|
|
if (!state.selectedRobotId) return;
|
|
const slot = Number(songSlotInput.value) || 0;
|
|
socket.emit('playSong', { robotId: state.selectedRobotId, slot });
|
|
});
|
|
|
|
socket.on('robots', (robots) => {
|
|
state.robots = robots;
|
|
if (!state.selectedRobotId && robots.length > 0) {
|
|
state.selectedRobotId = robots[0].id;
|
|
}
|
|
renderRobots();
|
|
renderTelemetry();
|
|
});
|
|
|
|
socket.on('telemetrySnapshot', (entries) => {
|
|
entries.forEach(({ robotId, telemetry }) => {
|
|
state.telemetry[robotId] = telemetry;
|
|
});
|
|
renderTelemetry();
|
|
});
|
|
|
|
socket.on('telemetry', ({ robotId, telemetry }) => {
|
|
state.telemetry[robotId] = telemetry;
|
|
if (robotId === state.selectedRobotId) {
|
|
renderTelemetry();
|
|
}
|
|
});
|