diff --git a/app/env.d.ts b/app/env.d.ts index fe77695..8c7f1c8 100644 --- a/app/env.d.ts +++ b/app/env.d.ts @@ -1,8 +1,8 @@ -declare module "app-env" { - interface ENV { - VITE_USE_HOST_NAME: boolean; - } +declare module 'app-env' { + interface ENV { + VITE_USE_HOST_NAME: boolean + } - const appEnv: ENV; - export default appEnv; + const appEnv: ENV + export default appEnv } diff --git a/app/src/lib/components/Visualization.svelte b/app/src/lib/components/Visualization.svelte index 843c8c9..c9a6ca4 100644 --- a/app/src/lib/components/Visualization.svelte +++ b/app/src/lib/components/Visualization.svelte @@ -19,14 +19,21 @@ jointNames, currentKinematic, walkGait, - kinematicData, + kinematicData } from '$lib/stores' import { populateModelCache, getToeWorldPositions } from '$lib/utilities' import SceneBuilder from '$lib/sceneBuilder' import { lerp, degToRad } from 'three/src/math/MathUtils' import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import { type body_state_t } from '$lib/kinematic' - import { BezierState, CalibrationState, GaitState, IdleState, RestState, StandState } from '$lib/gait' + import { + BezierState, + CalibrationState, + GaitState, + IdleState, + RestState, + StandState + } from '$lib/gait' import { radToDeg } from 'three/src/math/MathUtils.js' import type { URDFRobot } from 'urdf-loader' import { get } from 'svelte/store' @@ -50,10 +57,12 @@ let sceneManager = $state(new SceneBuilder()) let canvas: HTMLCanvasElement + const NUM_ANGLES = 12 // TODO: This number should come from the robot - // TODO: This assumes that we have 12 angles (valid for the spot robot) but this should not be a static number defined in each individual data set - let currentModelAngles: AnglesData = AnglesData.create({ angles: new Array(12).fill(0) }) - let modelTargetAngles: AnglesData = AnglesData.create({ angles: new Array(12).fill(0) }) + let currentModelAngles: AnglesData = AnglesData.create({ + angles: new Array(NUM_ANGLES).fill(0) + }) + let modelTargetAngles: AnglesData = AnglesData.create({ angles: new Array(NUM_ANGLES).fill(0) }) let gui_panel: GUI const SMOOTH_AMOUNT = 0.2 @@ -63,8 +72,7 @@ let kinematic = get(currentKinematic) - // Incredibly ugly but cant be bothered to fix this or statement right now, we cant key on GaitState objects, only the class extensions themselves (which we dont use here) - const planners: Record = { + const planners: Record = { [ModesEnum.DEACTIVATED]: new IdleState(), [ModesEnum.IDLE]: new IdleState(), [ModesEnum.CALIBRATION]: new CalibrationState(), @@ -119,7 +127,9 @@ walkGait.subscribe(gait => { const walkPlanner = planners[ModesEnum.WALK] if (!(walkPlanner instanceof BezierState)) { - throw new Error(`Expected BezierState for WALK mode, got ${walkPlanner.constructor.name}`) + throw new Error( + `Expected BezierState for WALK mode, got ${walkPlanner.constructor.name}` + ) } walkPlanner.set_mode(gait.gait) }) @@ -163,14 +173,16 @@ } const updateKinematicPosition = () => { - kinematicData.set(KinematicData.create({ - omega: settings.omega, - phi: settings.phi, - psi: settings.psi, - xm: settings.xm, - ym: settings.ym, - zm: settings.zm - })) + kinematicData.set( + KinematicData.create({ + omega: settings.omega, + phi: settings.phi, + psi: settings.psi, + xm: settings.xm, + ym: settings.ym, + zm: settings.zm + }) + ) } const setSceneBackground = (c: string | null) => (sceneManager.scene.background = new Color(c!)) @@ -178,7 +190,9 @@ const updateAngles = (name: string, angle: number) => { modelTargetAngles.angles[$jointNames.indexOf(name)] = angle * (180 / Math.PI) servoAnglesOut.set( - AnglesData.create({ angles: modelTargetAngles.angles.map(num => Math.round(num)) }) + AnglesData.create({ + angles: modelTargetAngles.angles.map(num => Math.round(num)) + }) ) } @@ -282,7 +296,7 @@ const update_gait = () => { if (sceneManager.isDragging || !settings['Internal kinematic']) return - const controlData = get(outControllerData) + const controlData = get(input) let planner = planners[get(mode).mode] const delta = performance.now() - lastTick diff --git a/app/src/lib/components/statusbar/StopButton.svelte b/app/src/lib/components/statusbar/StopButton.svelte index 8f7cca2..04f7e94 100644 --- a/app/src/lib/components/statusbar/StopButton.svelte +++ b/app/src/lib/components/statusbar/StopButton.svelte @@ -1,6 +1,6 @@ - -

Hexadecimal Output

Hex output: {hex}

diff --git a/app/src/routes/system/status/SystemStatus.svelte b/app/src/routes/system/status/SystemStatus.svelte index 0ea1d76..8c5a9ab 100644 --- a/app/src/routes/system/status/SystemStatus.svelte +++ b/app/src/routes/system/status/SystemStatus.svelte @@ -51,9 +51,11 @@ const postSleep = async () => await api.post('api/sleep') - let unsub: (() => void) | undefined = undefined; - onMount(() => unsub = socket.on(AnalyticsData, handleSystemData)) - onDestroy(() => { if (unsub) unsub() }) + let unsub: (() => void) | undefined = undefined + onMount(() => (unsub = socket.on(AnalyticsData, handleSystemData))) + onDestroy(() => { + if (unsub) unsub() + }) const handleSystemData = (data: AnalyticsData) => { if (systemInformation) { @@ -179,7 +181,9 @@ icon={Speed} title="CPU Frequency" description={`${systemInformation.staticSystemInformation?.cpuFreqMhz} MHz ${ - systemInformation.staticSystemInformation?.cpuCores == 2 ? 'Dual Core' : 'Single Core' + systemInformation.staticSystemInformation?.cpuCores == 2 ? + 'Dual Core' + : 'Single Core' }`} /> @@ -199,11 +203,14 @@ icon={Sketch} title="Sketch (Used / Free)" description={`${( - (systemInformation.staticSystemInformation!.sketchSize / systemInformation.staticSystemInformation!.freeSketchSpace) * + (systemInformation.staticSystemInformation!.sketchSize / + systemInformation.staticSystemInformation!.freeSketchSpace) * 100 ).toFixed(1)} % of ${systemInformation.staticSystemInformation!.freeSketchSpace / 1000000} MB used (${ - (systemInformation.staticSystemInformation!.freeSketchSpace - systemInformation.staticSystemInformation!.sketchSize) / 1000000 + (systemInformation.staticSystemInformation!.freeSketchSpace - + systemInformation.staticSystemInformation!.sketchSize) / + 1000000 } MB free)`} /> @@ -219,10 +226,15 @@ icon={Folder} title="File System (Used / Total)" description={`${( - (systemInformation.analyticsData!.fsUsed / systemInformation.analyticsData!.fsTotal) * + (systemInformation.analyticsData!.fsUsed / + systemInformation.analyticsData!.fsTotal) * 100 - ).toFixed(1)} % of ${systemInformation.analyticsData!.fsTotal / 1000000} MB used (${ - (systemInformation.analyticsData!.fsTotal - systemInformation.analyticsData!.fsUsed) / 1000000 + ).toFixed( + 1 + )} % of ${systemInformation.analyticsData!.fsTotal / 1000000} MB used (${ + (systemInformation.analyticsData!.fsTotal - + systemInformation.analyticsData!.fsUsed) / + 1000000 } MB free)`} /> diff --git a/app/src/routes/wifi/sta/Wifi.svelte b/app/src/routes/wifi/sta/Wifi.svelte index e433542..a06ecb2 100644 --- a/app/src/routes/wifi/sta/Wifi.svelte +++ b/app/src/routes/wifi/sta/Wifi.svelte @@ -36,7 +36,7 @@ import { KnownNetworkItem } from '$lib/platform_shared/websocket_message' import { WifiSettings, type WifiStatus } from '$lib/platform_shared/rest_message' - let networkEditable: KnownNetworkItem = $state( KnownNetworkItem.create() ) + let networkEditable: KnownNetworkItem = $state(KnownNetworkItem.create()) let static_ip_config = $state(false) @@ -84,15 +84,17 @@ return wifiSettings } - let unsub_obj: (() => void) | undefined = undefined; + let unsub_obj: (() => void) | undefined = undefined onMount(() => { unsub_obj = socket.on(WifiSettings, data => { wifiSettings = data dndNetworkList = wifiSettings.wifiNetworks }) }) - - onDestroy(() => { if (unsub_obj) unsub_obj() } ) + + onDestroy(() => { + if (unsub_obj) unsub_obj() + }) async function postWiFiSettings(data: WifiSettings) { const result = await api.post('/api/wifi/sta/settings', data) if (result.isErr()) { diff --git a/app/tests/unit/socket.spec.ts b/app/tests/unit/socket.spec.ts index 96aa358..2656101 100644 --- a/app/tests/unit/socket.spec.ts +++ b/app/tests/unit/socket.spec.ts @@ -1,212 +1,216 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { WebSocketServer } from 'ws' import { decodeMessage, MESSAGE_KEY_TO_TAG, socket } from '../../src/lib/stores/socket' -import { IMUData, PingMsg, PongMsg, RSSIData, WebsocketMessage, protoMetadata as websocket_md } from '../../src/lib/platform_shared/websocket_message' +import { + IMUData, + PingMsg, + PongMsg, + WebsocketMessage +} from '../../src/lib/platform_shared/websocket_message' // Helper function to create encoded WebSocket messages -function createEncodedMessage(messageType: 'imu' | 'rssi' | 'mode', data: any): Uint8Array { - const message: any = {} - message[messageType] = data - const wsMessage = WebsocketMessage.create(message) - return WebsocketMessage.encode(wsMessage).finish() +function createEncodedMessage(messageType: 'imu' | 'rssi' | 'mode', data: unknown): Uint8Array { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message: any = {} + message[messageType] = data + const wsMessage = WebsocketMessage.create(message) + return WebsocketMessage.encode(wsMessage).finish() } describe.sequential('WebSocket Integration Tests', () => { - let wss: WebSocketServer - let TEST_PORT = 8765 + let wss: WebSocketServer + let TEST_PORT = 8765 - beforeEach(async () => { - // Use a different port for each test to avoid conflicts - TEST_PORT++ + beforeEach(async () => { + // Use a different port for each test to avoid conflicts + TEST_PORT++ - // Create real WebSocket server - wss = new WebSocketServer({ port: TEST_PORT }) + // Create real WebSocket server + wss = new WebSocketServer({ port: TEST_PORT }) - // Wait for server to start - await new Promise((resolve) => { - wss.on('listening', () => resolve()) - }) - }) + // Wait for server to start + await new Promise(resolve => { + wss.on('listening', () => resolve()) + }) + }) - afterEach(async () => { - // Close all connections and server - wss.clients.forEach((client) => client.close()) - await new Promise((resolve) => { - wss.close(() => resolve()) - }) - // Wait a bit for cleanup - await new Promise(resolve => setTimeout(resolve, 100)) - }) + afterEach(async () => { + // Close all connections and server + wss.clients.forEach(client => client.close()) + await new Promise(resolve => { + wss.close(() => resolve()) + }) + // Wait a bit for cleanup + await new Promise(resolve => setTimeout(resolve, 100)) + }) - it('should connect to WebSocket server', async () => { - socket.init(`ws://localhost:${TEST_PORT}`) + it('should connect to WebSocket server', async () => { + socket.init(`ws://localhost:${TEST_PORT}`) - // Wait for connection - await new Promise(resolve => setTimeout(resolve, 100)) + // Wait for connection + await new Promise(resolve => setTimeout(resolve, 100)) - let isConnected = false - socket.subscribe(value => { - isConnected = value - })() + let isConnected = false + socket.subscribe(value => { + isConnected = value + })() - expect(isConnected).toBe(true) - }) + expect(isConnected).toBe(true) + }) - it('should receive and decode IMU data from server', async () => { - let receivedIMUData: any = null + it('should receive and decode IMU data from server', async () => { + let receivedIMUData: any = null - // Subscribe to IMU messages before connecting - const unsubscribe = socket.on(IMUData, (data) => { - receivedIMUData = data - }) + // Subscribe to IMU messages before connecting + const unsubscribe = socket.on(IMUData, data => { + receivedIMUData = data + }) - // Connect socket - socket.init(`ws://localhost:${TEST_PORT}`) + // Connect socket + socket.init(`ws://localhost:${TEST_PORT}`) - // Wait for client to connect - await new Promise((resolve) => { - wss.on('connection', (ws) => { - // Server sends IMU data to client - const imuPayload = IMUData.create({ - x: 3.25, - y: 2.5, - z: 1.75, + // Wait for client to connect + await new Promise(resolve => { + wss.on('connection', ws => { + // Server sends IMU data to client + const imuPayload = IMUData.create({ + x: 3.25, + y: 2.5, + z: 1.75, heading: 10, altitude: 11, - bmpTemp: 22, + bmpTemp: 22, pressure: 23 - }) + }) - const encodedMessage = createEncodedMessage('imu', imuPayload) - ws.send(encodedMessage) + const encodedMessage = createEncodedMessage('imu', imuPayload) + ws.send(encodedMessage) - setTimeout(resolve, 50) - }) - }) + setTimeout(resolve, 50) + }) + }) - expect(receivedIMUData).toBeDefined() - expect(receivedIMUData?.imu).toBeDefined() + expect(receivedIMUData).toBeDefined() + expect(receivedIMUData?.imu).toBeDefined() expect(receivedIMUData?.imu.x).toBe(3.25) - expect(receivedIMUData?.imu.y).toBe(2.5) - expect(receivedIMUData?.imu.z).toBe(1.75) + expect(receivedIMUData?.imu.y).toBe(2.5) + expect(receivedIMUData?.imu.z).toBe(1.75) expect(receivedIMUData?.imu.heading).toBe(10) expect(receivedIMUData?.imu.altitude).toBe(11) - expect(receivedIMUData?.imu.bmpTemp).toBe(22) + expect(receivedIMUData?.imu.bmpTemp).toBe(22) expect(receivedIMUData?.imu.pressure).toBe(23) - unsubscribe() - }) + unsubscribe() + }) - it('should send IMU data from client to server using sendEvent', async () => { - let serverReceivedData: any = null + it('should send IMU data from client to server using sendEvent', async () => { + let serverReceivedData: any = null - // Connect socket - socket.init(`ws://localhost:${TEST_PORT}`) + // Connect socket + socket.init(`ws://localhost:${TEST_PORT}`) - // Wait for client to connect and send data - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Test timeout - server did not receive message')) - }, 3000) + // Wait for client to connect and send data + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Test timeout - server did not receive message')) + }, 3000) - wss.on('connection', (ws) => { - // console.log('Server: Client connected') + wss.on('connection', ws => { + // console.log('Server: Client connected') - // Server listens for messages from client - ws.on('message', (data: Buffer) => { - // console.log('Server: Received message, length:', data.length) + // Server listens for messages from client + ws.on('message', (data: Buffer) => { + // console.log('Server: Received message, length:', data.length) - // Skip empty messages (from ping, etc.) - if (data.length === 0) { - console.log('Server: Skipping empty message (Probably a ping') - return - } + // Skip empty messages (from ping, etc.) + if (data.length === 0) { + console.log('Server: Skipping empty message (Probably a ping') + return + } - try { - // Decode the protobuf message - const decoded = WebsocketMessage.decode(new Uint8Array(data)) - // console.log('Server: Decoded message:', JSON.stringify(decoded, null, 2)) + try { + // Decode the protobuf message + const decoded = WebsocketMessage.decode(new Uint8Array(data)) + // console.log('Server: Decoded message:', JSON.stringify(decoded, null, 2)) - // Only resolve if we got actual IMU data - if (decoded.imu) { - serverReceivedData = decoded - clearTimeout(timeout) - resolve() - } else { - // console.log('Server: Message decoded but no IMU data, waiting...') - } - } catch (error) { - console.error('Server: Failed to decode:', error) - clearTimeout(timeout) - reject(error) - } - }) - }) + // Only resolve if we got actual IMU data + if (decoded.imu) { + serverReceivedData = decoded + clearTimeout(timeout) + resolve() + } else { + // console.log('Server: Message decoded but no IMU data, waiting...') + } + } catch (error) { + console.error('Server: Failed to decode:', error) + clearTimeout(timeout) + reject(error) + } + }) + }) - // Wait for WebSocket to be fully connected - setTimeout(() => { - console.log('Client: Sending IMU data...') - // Client sends IMU data to server - const imuData = IMUData.create({ - x: 3.25, - y: 2.5, - z: 1.75, + // Wait for WebSocket to be fully connected + setTimeout(() => { + console.log('Client: Sending IMU data...') + // Client sends IMU data to server + const imuData = IMUData.create({ + x: 3.25, + y: 2.5, + z: 1.75, heading: 10, altitude: 11, - bmpTemp: 22, + bmpTemp: 22, pressure: 23 - }) - socket.sendEvent(IMUData, imuData) - console.log('Client: sendEvent called') - }, 150) - }) + }) + socket.sendEvent(IMUData, imuData) + console.log('Client: sendEvent called') + }, 150) + }) - // Verify server received the data - expect(serverReceivedData).toBeDefined() - expect(serverReceivedData?.imu).toBeDefined() + // Verify server received the data + expect(serverReceivedData).toBeDefined() + expect(serverReceivedData?.imu).toBeDefined() expect(serverReceivedData?.imu.x).toBe(3.25) - expect(serverReceivedData?.imu.y).toBe(2.5) - expect(serverReceivedData?.imu.z).toBe(1.75) + expect(serverReceivedData?.imu.y).toBe(2.5) + expect(serverReceivedData?.imu.z).toBe(1.75) expect(serverReceivedData?.imu.heading).toBe(10) expect(serverReceivedData?.imu.altitude).toBe(11) - expect(serverReceivedData?.imu.bmpTemp).toBe(22) + expect(serverReceivedData?.imu.bmpTemp).toBe(22) expect(serverReceivedData?.imu.pressure).toBe(23) - }) + }) + it('should fail to serialize data on sendEvent', async () => { + // Connect socket + socket.init(`ws://localhost:${TEST_PORT}`) - it('should fail to serialize data on sendEvent', async () => { - // Connect socket - socket.init(`ws://localhost:${TEST_PORT}`) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Test timeout')) + }, 1000) - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Test timeout')) - }, 1000) - - // Wait for WebSocket to be fully connected - setTimeout(() => { - console.log('Client: Sending invalid message type...') - // Send any invalid message type - const wsm = WebsocketMessage.create() - try { - socket.sendEvent(WebsocketMessage as any, wsm) - clearTimeout(timeout) - reject(new Error('Expected sendEvent to throw, but it did not')) - } catch (e) { - console.log('Client: sendEvent correctly threw error:', e) - clearTimeout(timeout) - resolve() - } - }, 150) - }) - }) + // Wait for WebSocket to be fully connected + setTimeout(() => { + console.log('Client: Sending invalid message type...') + // Send any invalid message type + const wsm = WebsocketMessage.create() + try { + socket.sendEvent(WebsocketMessage as any, wsm) + clearTimeout(timeout) + reject(new Error('Expected sendEvent to throw, but it did not')) + } catch (e) { + console.log('Client: sendEvent correctly threw error:', e) + clearTimeout(timeout) + resolve() + } + }, 150) + }) + }) }) describe('WebsocketMessage Protobuf Encoding/Decoding', () => { - it('should encode and decode IMU data correctly', () => { - + it('should encode and decode IMU data correctly', () => { const imuData = IMUData.create({ x: 3.25, y: 2.5, @@ -217,34 +221,35 @@ describe('WebsocketMessage Protobuf Encoding/Decoding', () => { pressure: 23 }) - const encoded = IMUData.encode(imuData).finish() - const decoded = IMUData.decode(encoded) + const encoded = IMUData.encode(imuData).finish() + const decoded = IMUData.decode(encoded) - expect(decoded.x).toBe(3.25) - expect(decoded.y).toBe(2.5) - expect(decoded.z).toBe(1.75) + expect(decoded.x).toBe(3.25) + expect(decoded.y).toBe(2.5) + expect(decoded.z).toBe(1.75) expect(decoded.heading).toBe(10) expect(decoded.altitude).toBe(11) - expect(decoded.bmpTemp).toBe(22) + expect(decoded.bmpTemp).toBe(22) expect(decoded.pressure).toBe(23) - }) + }) it('should encode and decode two empty types correctly', () => { - - const encoded_ping = WebsocketMessage.encode(WebsocketMessage.create({ pingmsg: PingMsg.create() })).finish() + const encoded_ping = WebsocketMessage.encode( + WebsocketMessage.create({ pingmsg: PingMsg.create() }) + ).finish() const decoded_ping = decodeMessage(encoded_ping.buffer) - expect(decoded_ping.tag).toBe(MESSAGE_KEY_TO_TAG.get("pingmsg")) + expect(decoded_ping.tag).toBe(MESSAGE_KEY_TO_TAG.get('pingmsg')) - const encoded_pong = WebsocketMessage.encode(WebsocketMessage.create({ pongmsg: PongMsg.create() })).finish() + const encoded_pong = WebsocketMessage.encode( + WebsocketMessage.create({ pongmsg: PongMsg.create() }) + ).finish() const decoded_pong = decodeMessage(encoded_pong.buffer) - expect(decoded_pong.tag).toBe(MESSAGE_KEY_TO_TAG.get("pongmsg")) - }) + expect(decoded_pong.tag).toBe(MESSAGE_KEY_TO_TAG.get('pongmsg')) + }) - - - it('should encode and decode complete WebsocketMessage', () => { - const original = WebsocketMessage.create({ - imu: IMUData.create({ + it('should encode and decode complete WebsocketMessage', () => { + const original = WebsocketMessage.create({ + imu: IMUData.create({ x: 3.25, y: 2.5, z: 1.75, @@ -253,19 +258,18 @@ describe('WebsocketMessage Protobuf Encoding/Decoding', () => { bmpTemp: 22, pressure: 23 }) - }) + }) - const encoded = WebsocketMessage.encode(original).finish() - const decoded = WebsocketMessage.decode(encoded) + const encoded = WebsocketMessage.encode(original).finish() + const decoded = WebsocketMessage.decode(encoded) - expect(decoded.imu).toBeDefined() - expect(decoded.imu?.x).toBe(3.25) - expect(decoded.imu?.y).toBe(2.5) - expect(decoded.imu?.z).toBe(1.75) + expect(decoded.imu).toBeDefined() + expect(decoded.imu?.x).toBe(3.25) + expect(decoded.imu?.y).toBe(2.5) + expect(decoded.imu?.z).toBe(1.75) expect(decoded.imu?.heading).toBe(10) expect(decoded.imu?.altitude).toBe(11) - expect(decoded.imu?.bmpTemp).toBe(22) + expect(decoded.imu?.bmpTemp).toBe(22) expect(decoded.imu?.pressure).toBe(23) - }) - + }) }) diff --git a/app/vitest.config.ts b/app/vitest.config.ts index fb718fb..bff7d27 100644 --- a/app/vitest.config.ts +++ b/app/vitest.config.ts @@ -3,15 +3,15 @@ import { svelte } from '@sveltejs/vite-plugin-svelte' import path from 'path' const config: UserConfigExport = { - plugins: [svelte()], - resolve: { - alias: { - $lib: path.resolve(__dirname, './src/lib') - } - }, - test: { - globals: true, - environment: 'jsdom' - } + plugins: [svelte()], + resolve: { + alias: { + $lib: path.resolve(__dirname, './src/lib') + } + }, + test: { + globals: true, + environment: 'jsdom' + } } export default defineConfig(config)