Files

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();
}
});