🎨 Format and simplify controls

This commit is contained in:
Rune Harlyk
2026-01-02 22:35:04 +01:00
committed by nikguin04
parent a6b5b0881a
commit 0ef55bcc7e
25 changed files with 6424 additions and 5981 deletions
+6 -6
View File
@@ -1,8 +1,8 @@
declare module "app-env" { declare module 'app-env' {
interface ENV { interface ENV {
VITE_USE_HOST_NAME: boolean; VITE_USE_HOST_NAME: boolean
} }
const appEnv: ENV; const appEnv: ENV
export default appEnv; export default appEnv
} }
+32 -18
View File
@@ -19,14 +19,21 @@
jointNames, jointNames,
currentKinematic, currentKinematic,
walkGait, walkGait,
kinematicData, kinematicData
} from '$lib/stores' } from '$lib/stores'
import { populateModelCache, getToeWorldPositions } from '$lib/utilities' import { populateModelCache, getToeWorldPositions } from '$lib/utilities'
import SceneBuilder from '$lib/sceneBuilder' import SceneBuilder from '$lib/sceneBuilder'
import { lerp, degToRad } from 'three/src/math/MathUtils' import { lerp, degToRad } from 'three/src/math/MathUtils'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import { type body_state_t } from '$lib/kinematic' 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 { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader' import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store' import { get } from 'svelte/store'
@@ -50,10 +57,12 @@
let sceneManager = $state(new SceneBuilder()) let sceneManager = $state(new SceneBuilder())
let canvas: HTMLCanvasElement 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({
let currentModelAngles: AnglesData = AnglesData.create({ angles: new Array(12).fill(0) }) angles: new Array(NUM_ANGLES).fill(0)
let modelTargetAngles: AnglesData = AnglesData.create({ angles: new Array(12).fill(0) }) })
let modelTargetAngles: AnglesData = AnglesData.create({ angles: new Array(NUM_ANGLES).fill(0) })
let gui_panel: GUI let gui_panel: GUI
const SMOOTH_AMOUNT = 0.2 const SMOOTH_AMOUNT = 0.2
@@ -63,8 +72,7 @@
let kinematic = get(currentKinematic) 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<ModesEnum, GaitState> = {
const planners: Record<ModesEnum, IdleState | CalibrationState | RestState | StandState | BezierState> = {
[ModesEnum.DEACTIVATED]: new IdleState(), [ModesEnum.DEACTIVATED]: new IdleState(),
[ModesEnum.IDLE]: new IdleState(), [ModesEnum.IDLE]: new IdleState(),
[ModesEnum.CALIBRATION]: new CalibrationState(), [ModesEnum.CALIBRATION]: new CalibrationState(),
@@ -119,7 +127,9 @@
walkGait.subscribe(gait => { walkGait.subscribe(gait => {
const walkPlanner = planners[ModesEnum.WALK] const walkPlanner = planners[ModesEnum.WALK]
if (!(walkPlanner instanceof BezierState)) { 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) walkPlanner.set_mode(gait.gait)
}) })
@@ -163,14 +173,16 @@
} }
const updateKinematicPosition = () => { const updateKinematicPosition = () => {
kinematicData.set(KinematicData.create({ kinematicData.set(
omega: settings.omega, KinematicData.create({
phi: settings.phi, omega: settings.omega,
psi: settings.psi, phi: settings.phi,
xm: settings.xm, psi: settings.psi,
ym: settings.ym, xm: settings.xm,
zm: settings.zm ym: settings.ym,
})) zm: settings.zm
})
)
} }
const setSceneBackground = (c: string | null) => (sceneManager.scene.background = new Color(c!)) const setSceneBackground = (c: string | null) => (sceneManager.scene.background = new Color(c!))
@@ -178,7 +190,9 @@
const updateAngles = (name: string, angle: number) => { const updateAngles = (name: string, angle: number) => {
modelTargetAngles.angles[$jointNames.indexOf(name)] = angle * (180 / Math.PI) modelTargetAngles.angles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
servoAnglesOut.set( 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 = () => { const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData) const controlData = get(input)
let planner = planners[get(mode).mode] let planner = planners[get(mode).mode]
const delta = performance.now() - lastTick const delta = performance.now() - lastTick
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { ModeData, ModesEnum } from '$lib/platform_shared/websocket_message' import { ModeData, ModesEnum } from '$lib/platform_shared/websocket_message'
import { mode, modes } from '$lib/stores' import { mode } from '$lib/stores'
const deactivate = async () => { const deactivate = async () => {
mode.set(ModeData.create({ mode: ModesEnum.DEACTIVATED })) mode.set(ModeData.create({ mode: ModesEnum.DEACTIVATED }))
-2
View File
@@ -169,8 +169,6 @@ export class BezierState extends GaitState {
} }
set_mode(mode: WalkGaits, duty?: number, order?: [number, number, number, number]) { set_mode(mode: WalkGaits, duty?: number, order?: [number, number, number, number]) {
console.log('BezierState set_mode', mode)
this.mode = mode this.mode = mode
if (mode === WalkGaits.CRAWL) { if (mode === WalkGaits.CRAWL) {
this.speed_factor = 0.5 this.speed_factor = 0.5
+138 -134
View File
@@ -5,163 +5,167 @@
// source: platform_shared/imu_report.proto // source: platform_shared/imu_report.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire'
export const protobufPackage = ""; export const protobufPackage = ''
export interface IMUReport { export interface IMUReport {
x: number; x: number
y: number; y: number
z: number; z: number
temp: number; temp: number
success: boolean; success: boolean
} }
function createBaseIMUReport(): IMUReport { function createBaseIMUReport(): IMUReport {
return { x: 0, y: 0, z: 0, temp: 0, success: false }; return { x: 0, y: 0, z: 0, temp: 0, success: false }
} }
export const IMUReport: MessageFns<IMUReport> = { export const IMUReport: MessageFns<IMUReport> = {
encode(message: IMUReport, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: IMUReport, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.x !== 0) { if (message.x !== 0) {
writer.uint32(13).float(message.x); writer.uint32(13).float(message.x)
}
if (message.y !== 0) {
writer.uint32(21).float(message.y);
}
if (message.z !== 0) {
writer.uint32(29).float(message.z);
}
if (message.temp !== 0) {
writer.uint32(37).float(message.temp);
}
if (message.success !== false) {
writer.uint32(40).bool(message.success);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): IMUReport {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseIMUReport();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 13) {
break;
}
message.x = reader.float();
continue;
} }
case 2: { if (message.y !== 0) {
if (tag !== 21) { writer.uint32(21).float(message.y)
break;
}
message.y = reader.float();
continue;
} }
case 3: { if (message.z !== 0) {
if (tag !== 29) { writer.uint32(29).float(message.z)
break;
}
message.z = reader.float();
continue;
} }
case 4: { if (message.temp !== 0) {
if (tag !== 37) { writer.uint32(37).float(message.temp)
break;
}
message.temp = reader.float();
continue;
} }
case 5: { if (message.success !== false) {
if (tag !== 40) { writer.uint32(40).bool(message.success)
break;
}
message.success = reader.bool();
continue;
} }
} return writer
if ((tag & 7) === 4 || tag === 0) { },
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): IMUReport { decode(input: BinaryReader | Uint8Array, length?: number): IMUReport {
return { const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
x: isSet(object.x) ? globalThis.Number(object.x) : 0, const end = length === undefined ? reader.len : reader.pos + length
y: isSet(object.y) ? globalThis.Number(object.y) : 0, const message = createBaseIMUReport()
z: isSet(object.z) ? globalThis.Number(object.z) : 0, while (reader.pos < end) {
temp: isSet(object.temp) ? globalThis.Number(object.temp) : 0, const tag = reader.uint32()
success: isSet(object.success) ? globalThis.Boolean(object.success) : false, switch (tag >>> 3) {
}; case 1: {
}, if (tag !== 13) {
break
}
toJSON(message: IMUReport): unknown { message.x = reader.float()
const obj: any = {}; continue
if (message.x !== 0) { }
obj.x = message.x; case 2: {
} if (tag !== 21) {
if (message.y !== 0) { break
obj.y = message.y; }
}
if (message.z !== 0) {
obj.z = message.z;
}
if (message.temp !== 0) {
obj.temp = message.temp;
}
if (message.success !== false) {
obj.success = message.success;
}
return obj;
},
create<I extends Exact<DeepPartial<IMUReport>, I>>(base?: I): IMUReport { message.y = reader.float()
return IMUReport.fromPartial(base ?? ({} as any)); continue
}, }
fromPartial<I extends Exact<DeepPartial<IMUReport>, I>>(object: I): IMUReport { case 3: {
const message = createBaseIMUReport(); if (tag !== 29) {
message.x = object.x ?? 0; break
message.y = object.y ?? 0; }
message.z = object.z ?? 0;
message.temp = object.temp ?? 0;
message.success = object.success ?? false;
return message;
},
};
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; message.z = reader.float()
continue
}
case 4: {
if (tag !== 37) {
break
}
export type DeepPartial<T> = T extends Builtin ? T message.temp = reader.float()
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>> continue
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> }
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } case 5: {
: Partial<T>; if (tag !== 40) {
break
}
type KeysOfUnion<T> = T extends T ? keyof T : never; message.success = reader.bool()
export type Exact<P, I extends P> = P extends Builtin ? P continue
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }; }
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): IMUReport {
return {
x: isSet(object.x) ? globalThis.Number(object.x) : 0,
y: isSet(object.y) ? globalThis.Number(object.y) : 0,
z: isSet(object.z) ? globalThis.Number(object.z) : 0,
temp: isSet(object.temp) ? globalThis.Number(object.temp) : 0,
success: isSet(object.success) ? globalThis.Boolean(object.success) : false
}
},
toJSON(message: IMUReport): unknown {
const obj: any = {}
if (message.x !== 0) {
obj.x = message.x
}
if (message.y !== 0) {
obj.y = message.y
}
if (message.z !== 0) {
obj.z = message.z
}
if (message.temp !== 0) {
obj.temp = message.temp
}
if (message.success !== false) {
obj.success = message.success
}
return obj
},
create<I extends Exact<DeepPartial<IMUReport>, I>>(base?: I): IMUReport {
return IMUReport.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<IMUReport>, I>>(object: I): IMUReport {
const message = createBaseIMUReport()
message.x = object.x ?? 0
message.y = object.y ?? 0
message.z = object.z ?? 0
message.temp = object.temp ?? 0
message.success = object.success ?? false
return message
}
}
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> =
T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> =
P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never
}
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined
} }
export interface MessageFns<T> { export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter; encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T; decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T; fromJSON(object: any): T
toJSON(message: T): unknown; toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T; create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T; fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,8 +1,7 @@
import { AnalyticsData } from '$lib/platform_shared/websocket_message' import { AnalyticsData } from '$lib/platform_shared/websocket_message'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
const analytics_data: AnalyticsData[] = []; const analytics_data: AnalyticsData[] = []
const maxAnalyticsData = 100 const maxAnalyticsData = 100
+1 -2
View File
@@ -1,10 +1,9 @@
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { IMUData } from '$lib/platform_shared/websocket_message' import { IMUData } from '$lib/platform_shared/websocket_message'
const imu_data: IMUData[] = []; const imu_data: IMUData[] = []
const maxIMUData = 100 const maxIMUData = 100
export const imu = (() => { export const imu = (() => {
const { subscribe, update } = writable(imu_data) const { subscribe, update } = writable(imu_data)
+38 -27
View File
@@ -1,4 +1,12 @@
import { HumanInputData, KinematicData, ModeData, ModesEnum, WalkGaitData, WalkGaits } from '$lib/platform_shared/websocket_message' import Kinematic from '$lib/kinematic'
import {
HumanInputData,
KinematicData,
ModeData,
ModesEnum,
WalkGaitData,
WalkGaits
} from '$lib/platform_shared/websocket_message'
import { persistentStore } from '$lib/utilities/svelte-utilities' import { persistentStore } from '$lib/utilities/svelte-utilities'
import { writable, type Writable } from 'svelte/store' import { writable, type Writable } from 'svelte/store'
@@ -10,36 +18,39 @@ export const model = writable()
export const mode: Writable<ModeData> = writable(ModeData.create({ mode: ModesEnum.DEACTIVATED })) export const mode: Writable<ModeData> = writable(ModeData.create({ mode: ModesEnum.DEACTIVATED }))
export const walkGait: Writable<WalkGaitData> = writable( WalkGaitData.create({gait: WalkGaits.TROT }) ) export const walkGait: Writable<WalkGaitData> = writable(
WalkGaitData.create({ gait: WalkGaits.TROT })
export const outControllerData = writable( HumanInputData.create( {left: {x:0,y:0}, right: {x:0,y:0}, height:0, s1:0, speed:0} ) ) )
export const kinematicData = writable(KinematicData.create()) export const kinematicData = writable(KinematicData.create())
export const input: Writable<HumanInputData> = writable( HumanInputData.create( {left: {x:0,y:0}, right: {x:0,y:0}, height:0, s1:0, speed:0} ) ) export const input: Writable<HumanInputData> = writable(
HumanInputData.create({
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 0.7,
s1: 0.5,
speed: 0.5
})
)
function enumToValuesAndLabels<T extends number>(enumObj: Record<string, T | string>) {
const entries = Object.entries(enumObj).filter(
([key, v]) => typeof v === 'number' && key !== 'UNRECOGNIZED'
) as [string, T][]
// Following code is generated from CLAUDE CODE return {
// Auto-generate modes array from ModesEnum (excluding UNRECOGNIZED) values: entries.map(([, v]) => v),
export const modes = Object.values(ModesEnum) labels: Object.fromEntries(
.filter((v): v is ModesEnum => typeof v === 'number' && v !== ModesEnum.UNRECOGNIZED) entries.map(([k, v]) => [v, k.charAt(0) + k.slice(1).toLowerCase()])
) as Record<T, string>
}
}
// Auto-generate mode labels from enum keys const modesData = enumToValuesAndLabels<ModesEnum>(ModesEnum)
export const modeLabels: Record<ModesEnum, string> = Object.entries(ModesEnum) export const modes = modesData.values
.filter(([_, v]) => typeof v === 'number' && v !== ModesEnum.UNRECOGNIZED) export const modeLabels = modesData.labels
.reduce((acc, [key, value]) => {
acc[value as ModesEnum] = key.charAt(0) + key.slice(1).toLowerCase()
return acc
}, {} as Record<ModesEnum, string>)
// Auto-generate walk gaits array from WalkGaits enum (excluding UNRECOGNIZED) const walkGaitsData = enumToValuesAndLabels<WalkGaits>(WalkGaits)
export const walkGaits = Object.values(WalkGaits) export const walkGaits = walkGaitsData.values
.filter((v): v is WalkGaits => typeof v === 'number' && v !== WalkGaits.UNRECOGNIZED) export const walkGaitLabels = walkGaitsData.labels
// Auto-generate walk gait labels from enum keys
export const walkGaitLabels: Record<WalkGaits, string> = Object.entries(WalkGaits)
.filter(([_, v]) => typeof v === 'number' && v !== WalkGaits.UNRECOGNIZED)
.reduce((acc, [key, value]) => {
acc[value as WalkGaits] = key.charAt(0) + key.slice(1).toLowerCase()
return acc
}, {} as Record<WalkGaits, string>)
+6 -2
View File
@@ -1,8 +1,12 @@
import { AnglesData } from '$lib/platform_shared/websocket_message' import { AnglesData } from '$lib/platform_shared/websocket_message'
import { writable, type Writable } from 'svelte/store' import { writable, type Writable } from 'svelte/store'
export const servoAnglesOut: Writable<AnglesData> = writable(AnglesData.create({angles: [0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90]})) export const servoAnglesOut: Writable<AnglesData> = writable(
export const servoAngles: Writable<AnglesData> = writable(AnglesData.create({angles: [0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90]})) AnglesData.create({ angles: [0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90] })
)
export const servoAngles: Writable<AnglesData> = writable(
AnglesData.create({ angles: [0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90] })
)
export const logs = writable([] as string[]) export const logs = writable([] as string[])
export const mpu = writable({ heading: 0 }) export const mpu = writable({ heading: 0 })
+44 -43
View File
@@ -1,10 +1,13 @@
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { encode, decode } from '@msgpack/msgpack' import { encode, decode } from '@msgpack/msgpack'
import { WebsocketMessage, type MessageFns, protoMetadata as websocket_md } from '$lib/platform_shared/websocket_message' import {
WebsocketMessage,
type MessageFns,
protoMetadata as websocket_md
} from '$lib/platform_shared/websocket_message'
import * as WebsocketMessages from '$lib/platform_shared/websocket_message' import * as WebsocketMessages from '$lib/platform_shared/websocket_message'
import type { BinaryWriter } from '@bufbuild/protobuf/wire' import type { BinaryWriter } from '@bufbuild/protobuf/wire'
// -------- START PARSING PROTO DATA -------- // -------- START PARSING PROTO DATA --------
// Auto-build reverse mapping from MessageFns to event key and tag // Auto-build reverse mapping from MessageFns to event key and tag
export const MESSAGE_TYPE_TO_KEY = new Map<MessageFns<any>, string>() export const MESSAGE_TYPE_TO_KEY = new Map<MessageFns<any>, string>()
@@ -13,7 +16,7 @@ export const MESSAGE_KEY_TO_TAG = new Map<string, number>()
// Build the mapping using references from metadata // Build the mapping using references from metadata
const websocketMessageType = websocket_md.fileDescriptor.messageType?.find( const websocketMessageType = websocket_md.fileDescriptor.messageType?.find(
( msg: { name: string } ) => msg.name === 'WebsocketMessage' (msg: { name: string }) => msg.name === 'WebsocketMessage'
) )
if (websocketMessageType?.field) { if (websocketMessageType?.field) {
@@ -33,7 +36,9 @@ if (websocketMessageType?.field) {
function get_name_from_messagetype(event_type: MessageFns<any>): string { function get_name_from_messagetype(event_type: MessageFns<any>): string {
const event = MESSAGE_TYPE_TO_KEY.get(event_type) const event = MESSAGE_TYPE_TO_KEY.get(event_type)
if (!event) { if (!event) {
throw new Error("Event type not found in 'WebsocketMessage'. The MessageFns you passed doesn't correspond to any WebsocketMessage field."); throw new Error(
"Event type not found in 'WebsocketMessage'. The MessageFns you passed doesn't correspond to any WebsocketMessage field."
)
} }
return event return event
} }
@@ -42,7 +47,9 @@ function get_name_from_messagetype(event_type: MessageFns<any>): string {
function get_tag_from_messagetype(event_type: MessageFns<any>): number { function get_tag_from_messagetype(event_type: MessageFns<any>): number {
const fieldNumber = MESSAGE_TYPE_TO_TAG.get(event_type) const fieldNumber = MESSAGE_TYPE_TO_TAG.get(event_type)
if (fieldNumber === undefined) { if (fieldNumber === undefined) {
throw new Error("Tag not found in 'WebsocketMessage'. The MessageFns you passed doesn't correspond to any WebsocketMessage field."); throw new Error(
"Tag not found in 'WebsocketMessage'. The MessageFns you passed doesn't correspond to any WebsocketMessage field."
)
} }
return fieldNumber return fieldNumber
} }
@@ -52,29 +59,26 @@ function get_tag_from_messagetype(event_type: MessageFns<any>): number {
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number] type SocketEvent = (typeof socketEvents)[number]
type TaggedSocketMessage = {"tag": number, "msg": WebsocketMessage} type TaggedSocketMessage = { tag: number; msg: WebsocketMessage }
// Only exported for socket test // Only exported for socket test
export const decodeMessage = (data: ArrayBuffer): TaggedSocketMessage => { export const decodeMessage = (data: ArrayBuffer): TaggedSocketMessage => {
const decoded = WebsocketMessage.decode(new Uint8Array(data))
const decoded = WebsocketMessage.decode(new Uint8Array(data));
const values = Object.entries(decoded).filter(([, value]) => value !== undefined) // Filter all values which are not undefined const values = Object.entries(decoded).filter(([, value]) => value !== undefined) // Filter all values which are not undefined
if (values.length != 1) { if (values.length != 1) {
throw new Error("Message included either 0 or more than 1 data point") throw new Error('Message included either 0 or more than 1 data point')
} }
const fieldName = values[0][0] const fieldName = values[0][0]
const tag = MESSAGE_KEY_TO_TAG.get(fieldName) const tag = MESSAGE_KEY_TO_TAG.get(fieldName)
if (tag === undefined) { if (tag === undefined) {
throw new Error(`Tag not found for field: ${fieldName}`) throw new Error(`Tag not found for field: ${fieldName}`)
} }
return {"tag": tag, "msg": decoded} return { tag: tag, msg: decoded }
} }
export const encodeMessage = (data: WebsocketMessage): Uint8Array<ArrayBuffer> => { export const encodeMessage = (data: WebsocketMessage): Uint8Array<ArrayBuffer> => {
const encoded = WebsocketMessage.encode(data).finish(); const encoded = WebsocketMessage.encode(data).finish()
return encoded; return encoded
} }
function createWebSocket() { function createWebSocket() {
@@ -92,22 +96,21 @@ function createWebSocket() {
connect() connect()
} }
function getMsgListeners<MT>(event_type: MessageFns<MT>): Set<(data?: unknown) => void> { function getMsgListeners<MT>(event_type: MessageFns<MT>): Set<(data?: unknown) => void> {
const type_tag = get_tag_from_messagetype(event_type) const type_tag = get_tag_from_messagetype(event_type)
const type_listeners = message_listeners.get(type_tag); const type_listeners = message_listeners.get(type_tag)
if (type_listeners == undefined) { if (type_listeners == undefined) {
return new Set() return new Set()
} }
return type_listeners; return type_listeners
} }
function getListeners<MT>(event: string): Set<(data?: unknown) => void> { function getListeners<MT>(event: string): Set<(data?: unknown) => void> {
const event_listeners_forevent = event_listeners.get(event)
const event_listeners_forevent = event_listeners.get(event);
if (event_listeners_forevent == undefined) { if (event_listeners_forevent == undefined) {
return new Set() return new Set()
} }
return event_listeners_forevent; return event_listeners_forevent
} }
function disconnect(reason: SocketEvent, event?: Event) { function disconnect(reason: SocketEvent, event?: Event) {
@@ -135,7 +138,7 @@ function createWebSocket() {
} }
ws.onmessage = frame => { ws.onmessage = frame => {
resetUnresponsiveCheck() resetUnresponsiveCheck()
const {tag, msg} = decodeMessage(frame.data) const { tag, msg } = decodeMessage(frame.data)
if (tag) message_listeners.get(tag)?.forEach(listener => listener(msg)) if (tag) message_listeners.get(tag)?.forEach(listener => listener(msg))
} }
ws.onerror = ev => disconnect('error', ev) ws.onerror = ev => disconnect('error', ev)
@@ -149,14 +152,13 @@ function createWebSocket() {
// TODO: This looks like it deletes an individual listener, but unsubscribe unsubscribes for everyone. Not sure what it is supposed to do right now // TODO: This looks like it deletes an individual listener, but unsubscribe unsubscribes for everyone. Not sure what it is supposed to do right now
message_listeners_totag?.delete(listener as (data?: unknown) => void) message_listeners_totag?.delete(listener as (data?: unknown) => void)
if (message_listeners_totag.size == 0) { // No more listeners, so we can unsubscribe if (message_listeners_totag.size == 0) {
// No more listeners, so we can unsubscribe
unsubscribeToMessageFromServer(event_type) unsubscribeToMessageFromServer(event_type)
} }
} }
function unsubscribe_event(event_type: SocketEvent, listener: (data: unknown) => void) { function unsubscribe_event(event_type: SocketEvent, listener: (data: unknown) => void) {
const message_listeners_totag = event_listeners.get(event_type) const message_listeners_totag = event_listeners.get(event_type)
if (!message_listeners_totag) return if (!message_listeners_totag) return
@@ -171,38 +173,38 @@ function createWebSocket() {
// T must extend a type of WebsocketMessages // T must extend a type of WebsocketMessages
function sendEvent<T>(event: MessageFns<T>, data: T) { function sendEvent<T>(event: MessageFns<T>, data: T) {
if (!ws || ws.readyState !== WebSocket.OPEN) return if (!ws || ws.readyState !== WebSocket.OPEN) return
const type = get_name_from_messagetype(event); const type = get_name_from_messagetype(event)
const wsm = WebsocketMessage.create(); const wsm = WebsocketMessage.create()
(wsm as any)[type] = data ;(wsm as any)[type] = data
send(wsm) send(wsm)
} }
function unsubscribeToMessageFromServer<T>(event_type: MessageFns<T>) { function unsubscribeToMessageFromServer<T>(event_type: MessageFns<T>) {
if (!ws || ws.readyState !== WebSocket.OPEN) return if (!ws || ws.readyState !== WebSocket.OPEN) return
const event = get_name_from_messagetype(event_type); const event = get_name_from_messagetype(event_type)
const unsub_msg = WebsocketMessages.UnsubscribeNotification.create( const unsub_msg = WebsocketMessages.UnsubscribeNotification.create({
{tag: get_tag_from_messagetype(event_type)} tag: get_tag_from_messagetype(event_type)
); })
send(WebsocketMessage.create({unsubNotif: unsub_msg})); send(WebsocketMessage.create({ unsubNotif: unsub_msg }))
} }
function subscribeToEvent<T>(event_type: MessageFns<T>) { function subscribeToEvent<T>(event_type: MessageFns<T>) {
if (!ws || ws.readyState !== WebSocket.OPEN) return if (!ws || ws.readyState !== WebSocket.OPEN) return
const event = get_name_from_messagetype(event_type); const event = get_name_from_messagetype(event_type)
const sub_msg = WebsocketMessages.SubscribeNotification.create( const sub_msg = WebsocketMessages.SubscribeNotification.create({
{tag: get_tag_from_messagetype(event_type)} tag: get_tag_from_messagetype(event_type)
); })
send(WebsocketMessage.create({subNotif: sub_msg})); send(WebsocketMessage.create({ subNotif: sub_msg }))
} }
function send(data: WebsocketMessage) { function send(data: WebsocketMessage) {
if (!ws || ws.readyState !== WebSocket.OPEN) return if (!ws || ws.readyState !== WebSocket.OPEN) return
const encoded = encodeMessage(data); const encoded = encodeMessage(data)
ws.send(encoded); ws.send(encoded)
} }
function ping() { function ping() {
send(WebsocketMessage.create({pingmsg: {}})) send(WebsocketMessage.create({ pingmsg: {} }))
} }
return { return {
@@ -210,7 +212,7 @@ function createWebSocket() {
sendEvent, sendEvent,
init, init,
on: <MT>(event_type: MessageFns<MT>, listener: (data: MT) => void): (() => void) => { on: <MT>(event_type: MessageFns<MT>, listener: (data: MT) => void): (() => void) => {
const tag = get_tag_from_messagetype(event_type); const tag = get_tag_from_messagetype(event_type)
let message_listeners_totag = message_listeners.get(tag) let message_listeners_totag = message_listeners.get(tag)
if (!message_listeners_totag) { if (!message_listeners_totag) {
@@ -225,7 +227,6 @@ function createWebSocket() {
} }
}, },
onEvent: (event_type: SocketEvent, listener: (data: unknown) => void): (() => void) => { onEvent: (event_type: SocketEvent, listener: (data: unknown) => void): (() => void) => {
return () => { return () => {
unsubscribe_event(event_type, listener) unsubscribe_event(event_type, listener)
} }
+14 -5
View File
@@ -2,10 +2,13 @@ import { DownloadOTAData, RSSIData } from '$lib/platform_shared/websocket_messag
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
type telemetry_data_type = { type telemetry_data_type = {
rssi: RSSIData; rssi: RSSIData
download_ota: DownloadOTAData; download_ota: DownloadOTAData
} }
const telemetry_data: telemetry_data_type = { rssi: RSSIData.create(), download_ota: DownloadOTAData.create() }; // Note: perhaps init these as null instead of an undefined create() const telemetry_data: telemetry_data_type = {
rssi: RSSIData.create(),
download_ota: DownloadOTAData.create()
} // Note: perhaps init these as null instead of an undefined create()
function createTelemetry() { function createTelemetry() {
const { subscribe, update } = writable(telemetry_data) const { subscribe, update } = writable(telemetry_data)
@@ -13,10 +16,16 @@ function createTelemetry() {
return { return {
subscribe, subscribe,
setRSSI: (data: RSSIData) => { setRSSI: (data: RSSIData) => {
update(telemetry_data => { telemetry_data.rssi = data; return telemetry_data }) update(telemetry_data => {
telemetry_data.rssi = data
return telemetry_data
})
}, },
setDownloadOTA: (data: DownloadOTAData) => { setDownloadOTA: (data: DownloadOTAData) => {
update(telemetry_data => { telemetry_data.download_ota = data; return telemetry_data }) update(telemetry_data => {
telemetry_data.download_ota = data
return telemetry_data
})
} }
} }
} }
-9
View File
@@ -1,8 +1,5 @@
import type { AnalyticsData } from "$lib/platform_shared/websocket_message";
export type vector = { x: number; y: number } export type vector = { x: number; y: number }
export type GithubRelease = { export type GithubRelease = {
message: string message: string
tag_name: string tag_name: string
@@ -12,13 +9,10 @@ export type GithubRelease = {
}> }>
} }
export type NetworkList = { export type NetworkList = {
networks: NetworkItem[] networks: NetworkItem[]
} }
export type NetworkItem = { export type NetworkItem = {
rssi: number rssi: number
ssid: string ssid: string
@@ -46,14 +40,11 @@ export type ApSettings = {
subnet_mask: string subnet_mask: string
} }
export type Rssi = { export type Rssi = {
rssi: number rssi: number
ssid: string ssid: string
} }
export type CameraSettings = { export type CameraSettings = {
framesize: number framesize: number
quality: number quality: number
+40 -17
View File
@@ -21,7 +21,17 @@
useFeatureFlags, useFeatureFlags,
walkGait walkGait
} from '$lib/stores' } from '$lib/stores'
import { AnalyticsData, AnglesData, DownloadOTAData, HumanInputData, KinematicData, ModeData, RSSIData, SonarData, WalkGaitData } from '$lib/platform_shared/websocket_message' import {
AnalyticsData,
AnglesData,
DownloadOTAData,
HumanInputData,
KinematicData,
ModeData,
RSSIData,
SonarData,
WalkGaitData
} from '$lib/platform_shared/websocket_message'
import { Throttler } from '$lib/utilities' import { Throttler } from '$lib/utilities'
interface Props { interface Props {
@@ -39,10 +49,14 @@
addEventListeners() addEventListeners()
outControllerData.subscribe(data => socket.sendEvent(HumanInputData, data)) input.subscribe(data =>
throttler.throttle(() => socket.sendEvent(HumanInputData, data), 100)
)
mode.subscribe(data => socket.sendEvent(ModeData, data)) mode.subscribe(data => socket.sendEvent(ModeData, data))
walkGait.subscribe(data => socket.sendEvent(WalkGaitData, data)) walkGait.subscribe(data => socket.sendEvent(WalkGaitData, data))
servoAnglesOut.subscribe(data => socket.sendEvent(AnglesData, data)) servoAnglesOut.subscribe(data =>
throttler.throttle(() => socket.sendEvent(AnglesData, data), 100)
)
kinematicData.subscribe(data => socket.sendEvent(KinematicData, data)) kinematicData.subscribe(data => socket.sendEvent(KinematicData, data))
}) })
@@ -50,26 +64,35 @@
removeEventListeners() removeEventListeners()
}) })
const eventListeners: (() => void)[] = []; const eventListeners: (() => void)[] = []
const addEventListeners = () => { const addEventListeners = () => {
eventListeners.push(...[ eventListeners.push(
socket.onEvent('open', handleOpen), ...[
socket.onEvent('close', handleClose), socket.onEvent('open', handleOpen),
socket.onEvent('error', handleError), socket.onEvent('close', handleClose),
socket.on(RSSIData, (data) => telemetry.setRSSI(data)), socket.onEvent('error', handleError),
socket.on(ModeData, (data) => mode.set(data)), socket.on(RSSIData, data => telemetry.setRSSI(data)),
socket.on(AnalyticsData, (data) => {analytics.addData(data)}), socket.on(ModeData, data => mode.set(data)),
socket.on(AnglesData, (data) => {servoAngles.set(data)}) socket.on(AnalyticsData, data => {
]) analytics.addData(data)
}),
socket.on(AnglesData, data => {
servoAngles.set(data)
})
]
)
features.subscribe(data => { features.subscribe(data => {
if (data?.download_firmware) eventListeners.push( socket.on(DownloadOTAData, (data) => telemetry.setDownloadOTA(data)) ) if (data?.download_firmware)
if (data?.sonar) eventListeners.push( socket.on(SonarData, (data) => console.log(data)) ) eventListeners.push(
socket.on(DownloadOTAData, data => telemetry.setDownloadOTA(data))
)
if (data?.sonar) eventListeners.push(socket.on(SonarData, data => console.log(data)))
}) })
} }
const removeEventListeners = () => { const removeEventListeners = () => {
for (let offFunction of eventListeners) { for (let offFunction of eventListeners) {
offFunction(); offFunction()
} }
} }
@@ -79,7 +102,7 @@
const handleClose = () => { const handleClose = () => {
notifications.error('Connection to device lost', 5000) notifications.error('Connection to device lost', 5000)
telemetry.setRSSI( RSSIData.create({rssi: 0}) ) telemetry.setRSSI(RSSIData.create({ rssi: 0 }))
} }
const handleError = (data: unknown) => console.error(data) const handleError = (data: unknown) => console.error(data)
@@ -15,7 +15,6 @@
import { gamepadAxes, gamepadButtonsEdges, hasGamepad } from '$lib/stores/gamepad' import { gamepadAxes, gamepadButtonsEdges, hasGamepad } from '$lib/stores/gamepad'
import { notifications } from '$lib/components/toasts/notifications' import { notifications } from '$lib/components/toasts/notifications'
import { import {
HumanInputData,
ModeData, ModeData,
ModesEnum, ModesEnum,
WalkGaitData, WalkGaitData,
@@ -25,14 +24,6 @@
let left: nipplejs.JoystickManager let left: nipplejs.JoystickManager
let right: nipplejs.JoystickManager let right: nipplejs.JoystickManager
let data: HumanInputData = HumanInputData.create({
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 0,
s1: 0,
speed: 0
})
$effect(() => { $effect(() => {
if ($hasGamepad) { if ($hasGamepad) {
notifications.success('🎮 Gamepad connected', 3000) notifications.success('🎮 Gamepad connected', 3000)
+6 -2
View File
@@ -4,7 +4,11 @@
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
import { Connection } from '$lib/components/icons' import { Connection } from '$lib/components/icons'
import I2CSetting from './i2cSetting.svelte' import I2CSetting from './i2cSetting.svelte'
import { I2CDevice, I2CScanData, I2CScanDataRequest } from '$lib/platform_shared/websocket_message' import {
I2CDevice,
I2CScanData,
I2CScanDataRequest
} from '$lib/platform_shared/websocket_message'
// TODO: Delete this completely, this should be done on esp side, as it decides what addresses are actually valid, as for example ICM20948 and MPU6050 can have same address // TODO: Delete this completely, this should be done on esp side, as it decides what addresses are actually valid, as for example ICM20948 and MPU6050 can have same address
// const i2cDevices = [ // const i2cDevices = [
@@ -66,7 +70,7 @@
<div>No I2C devices found</div> <div>No I2C devices found</div>
{:else} {:else}
{#each active_devices as device (device.address)} {#each active_devices as device (device.address)}
<div>[{device.address.toString(16)}] {device.part_number} - {device.name}</div> <div>[{device.address.toString(16)}] {device.partNumber} - {device.name}</div>
{/each} {/each}
{/if} {/if}
</div> </div>
@@ -4,7 +4,10 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { modals } from 'svelte-modals' import { modals } from 'svelte-modals'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte' import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import { PeripheralSettingsData, PeripheralSettingsDataRequest } from '$lib/platform_shared/websocket_message' import {
PeripheralSettingsData,
PeripheralSettingsDataRequest
} from '$lib/platform_shared/websocket_message'
let settings: PeripheralSettingsData | null = $state(null) let settings: PeripheralSettingsData | null = $state(null)
let isEditing = $state(false) let isEditing = $state(false)
+29 -19
View File
@@ -9,9 +9,11 @@
import { useFeatureFlags } from '$lib/stores/featureFlags' import { useFeatureFlags } from '$lib/stores/featureFlags'
import { Rotate3d } from '$lib/components/icons' import { Rotate3d } from '$lib/components/icons'
import { IMUReport } from '$lib/platform_shared/imu_report'; import {
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; IMUCalibrateData,
import { IMUCalibrateData, IMUCalibrateExecute, IMUData } from '$lib/platform_shared/websocket_message' IMUCalibrateExecute,
IMUData
} from '$lib/platform_shared/websocket_message'
Chart.register(...registerables) Chart.register(...registerables)
@@ -189,9 +191,9 @@
const updateData = () => { const updateData = () => {
if ($features.imu) { if ($features.imu) {
const x = $imu.map(datapoint => datapoint.x) const x = $imu.map(datapoint => datapoint.x)
const y= $imu.map(datapoint => datapoint.y) const y = $imu.map(datapoint => datapoint.y)
const z = $imu.map(datapoint => datapoint.z) const z = $imu.map(datapoint => datapoint.z)
angleChart.data.labels = Array.from({ length: $imu.length }, (_, i) => i + 1) angleChart.data.labels = Array.from({ length: $imu.length }, (_, i) => i + 1)
angleChart.data.datasets[0].data = x angleChart.data.datasets[0].data = x
angleChart.data.datasets[1].data = y angleChart.data.datasets[1].data = y
@@ -204,23 +206,31 @@
} }
if ($features.bmp) { if ($features.bmp) {
updateChartData(tempChart, $imu.map(datapoint => datapoint.bmpTemp)) updateChartData(
updateChartData(altitudeChart, $imu.map(datapoint => datapoint.altitude)) tempChart,
$imu.map(datapoint => datapoint.bmpTemp)
)
updateChartData(
altitudeChart,
$imu.map(datapoint => datapoint.altitude)
)
} }
} }
const eventListeners: (() => void)[] = []; const eventListeners: (() => void)[] = []
onMount(() => { onMount(() => {
eventListeners.push(...[ eventListeners.push(
socket.on(IMUData, (data) => { ...[
console.log(data) socket.on(IMUData, data => {
imu.addData(data) console.log(data)
}), imu.addData(data)
}),
socket.on(IMUCalibrateData, (data) => { socket.on(IMUCalibrateData, data => {
isCalibrating = false isCalibrating = false
calibrationResult = data calibrationResult = data
}) })
]) ]
)
initializeCharts() initializeCharts()
intervalId = setInterval(updateData, 200) intervalId = setInterval(updateData, 200)
@@ -228,7 +238,7 @@
onDestroy(() => { onDestroy(() => {
for (let offFunction of eventListeners) { for (let offFunction of eventListeners) {
offFunction(); offFunction()
} }
clearInterval(intervalId) clearInterval(intervalId)
}) })
@@ -12,16 +12,16 @@
const throttler = new Throttler() const throttler = new Throttler()
const activateServo = () => { const activateServo = () => {
socket.sendEvent(ServoStateData, ServoStateData.create({active: true})) socket.sendEvent(ServoStateData, ServoStateData.create({ active: true }))
} }
const deactivateServo = () => { const deactivateServo = () => {
socket.sendEvent(ServoStateData, ServoStateData.create({active: false})) socket.sendEvent(ServoStateData, ServoStateData.create({ active: false }))
} }
const updatePWM = () => { const updatePWM = () => {
throttler.throttle(() => { throttler.throttle(() => {
socket.sendEvent(ServoPWMData, ServoPWMData.create({ servoId: servoId, servoPwm: pwm })) socket.sendEvent(ServoPWMData, ServoPWMData.create({ servoId: servoId, servoPwm: pwm }))
}, 10) }, 10)
} }
+15 -21
View File
@@ -1,31 +1,29 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
//import { IMUReport, IMUType } from '$lib/platform_shared/example';
//import { IMUReport, IMUType } from '$lib/platform_shared/example';
import { AnglesData, WebsocketMessage, IMUData } from '$lib/platform_shared/websocket_message' import { AnglesData, WebsocketMessage, IMUData } from '$lib/platform_shared/websocket_message'
// const imu_report: IMUReport = {type: IMUType.IMU_ACCEL, xVal: 4} // const imu_report: IMUReport = {type: IMUType.IMU_ACCEL, xVal: 4}
// const writer = IMUReport.encode(imu_report); // const writer = IMUReport.encode(imu_report);
// const bytes = writer.finish(); // const bytes = writer.finish();
// // Convert bytes to hex // // Convert bytes to hex
// const hex = Array.from(bytes) // const hex = Array.from(bytes)
// .map((b) => b.toString(16).padStart(2, '0')) // .map((b) => b.toString(16).padStart(2, '0'))
// .join(' '); // .join(' ');
// const wmd: WebsocketMessage = { imu: {temp: 0, x: 0, y: 0, z: 1}, angles: {angles: [2]}} // const wmd: WebsocketMessage = { imu: {temp: 0, x: 0, y: 0, z: 1}, angles: {angles: [2]}}
// const wmd: WebsocketMessage = { imu: {temp: 0, x: 0, y: 0, z: 0} } // const wmd: WebsocketMessage = { imu: {temp: 0, x: 0, y: 0, z: 0} }
const wmd: WebsocketMessage = { rssi: {rssi: 16} } const wmd: WebsocketMessage = { rssi: { rssi: 16 } }
// const wmd: WebsocketMessage = { imu: {temp: 1, x: 2, y: 4, z: 5} } // const wmd: WebsocketMessage = { imu: {temp: 1, x: 2, y: 4, z: 5} }
// const wmd: WebsocketMessage = { angles: {angles: [1,2,3,4]} } // const wmd: WebsocketMessage = { angles: {angles: [1,2,3,4]} }
const writer = WebsocketMessage.encode(wmd); const writer = WebsocketMessage.encode(wmd)
const bytes = writer.finish(); const bytes = writer.finish()
// Convert bytes to hex // Convert bytes to hex
const hex = Array.from(bytes) const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0')) .map(b => b.toString(16).padStart(2, '0'))
.join(' '); .join(' ')
// const decodedmsg: WebsocketMessage = WebsocketMessage.decode(bytes); // const decodedmsg: WebsocketMessage = WebsocketMessage.decode(bytes);
// const objects = Object.entries(decodedmsg) // const objects = Object.entries(decodedmsg)
@@ -46,17 +44,13 @@
// } // }
const handleData = (data: IMUData) => { const handleData = (data: IMUData) => {
console.log(data)
console.log(data);
} }
onMount(() => { onMount(() => {
return socket.on(IMUData, handleData) return socket.on(IMUData, handleData)
}) })
</script> </script>
<h1>Hexadecimal Output</h1> <h1>Hexadecimal Output</h1>
<p><strong>Hex output:</strong> {hex}</p> <p><strong>Hex output:</strong> {hex}</p>
@@ -51,9 +51,11 @@
const postSleep = async () => await api.post('api/sleep') const postSleep = async () => await api.post('api/sleep')
let unsub: (() => void) | undefined = undefined; let unsub: (() => void) | undefined = undefined
onMount(() => unsub = socket.on(AnalyticsData, handleSystemData)) onMount(() => (unsub = socket.on(AnalyticsData, handleSystemData)))
onDestroy(() => { if (unsub) unsub() }) onDestroy(() => {
if (unsub) unsub()
})
const handleSystemData = (data: AnalyticsData) => { const handleSystemData = (data: AnalyticsData) => {
if (systemInformation) { if (systemInformation) {
@@ -179,7 +181,9 @@
icon={Speed} icon={Speed}
title="CPU Frequency" title="CPU Frequency"
description={`${systemInformation.staticSystemInformation?.cpuFreqMhz} MHz ${ 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} icon={Sketch}
title="Sketch (Used / Free)" title="Sketch (Used / Free)"
description={`${( description={`${(
(systemInformation.staticSystemInformation!.sketchSize / systemInformation.staticSystemInformation!.freeSketchSpace) * (systemInformation.staticSystemInformation!.sketchSize /
systemInformation.staticSystemInformation!.freeSketchSpace) *
100 100
).toFixed(1)} % of ).toFixed(1)} % of
${systemInformation.staticSystemInformation!.freeSketchSpace / 1000000} MB used (${ ${systemInformation.staticSystemInformation!.freeSketchSpace / 1000000} MB used (${
(systemInformation.staticSystemInformation!.freeSketchSpace - systemInformation.staticSystemInformation!.sketchSize) / 1000000 (systemInformation.staticSystemInformation!.freeSketchSpace -
systemInformation.staticSystemInformation!.sketchSize) /
1000000
} MB free)`} } MB free)`}
/> />
@@ -219,10 +226,15 @@
icon={Folder} icon={Folder}
title="File System (Used / Total)" title="File System (Used / Total)"
description={`${( description={`${(
(systemInformation.analyticsData!.fsUsed / systemInformation.analyticsData!.fsTotal) * (systemInformation.analyticsData!.fsUsed /
systemInformation.analyticsData!.fsTotal) *
100 100
).toFixed(1)} % of ${systemInformation.analyticsData!.fsTotal / 1000000} MB used (${ ).toFixed(
(systemInformation.analyticsData!.fsTotal - systemInformation.analyticsData!.fsUsed) / 1000000 1
)} % of ${systemInformation.analyticsData!.fsTotal / 1000000} MB used (${
(systemInformation.analyticsData!.fsTotal -
systemInformation.analyticsData!.fsUsed) /
1000000
} }
MB free)`} MB free)`}
/> />
+6 -4
View File
@@ -36,7 +36,7 @@
import { KnownNetworkItem } from '$lib/platform_shared/websocket_message' import { KnownNetworkItem } from '$lib/platform_shared/websocket_message'
import { WifiSettings, type WifiStatus } from '$lib/platform_shared/rest_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) let static_ip_config = $state(false)
@@ -84,15 +84,17 @@
return wifiSettings return wifiSettings
} }
let unsub_obj: (() => void) | undefined = undefined; let unsub_obj: (() => void) | undefined = undefined
onMount(() => { onMount(() => {
unsub_obj = socket.on<WifiSettings>(WifiSettings, data => { unsub_obj = socket.on<WifiSettings>(WifiSettings, data => {
wifiSettings = data wifiSettings = data
dndNetworkList = wifiSettings.wifiNetworks dndNetworkList = wifiSettings.wifiNetworks
}) })
}) })
onDestroy(() => { if (unsub_obj) unsub_obj() } ) onDestroy(() => {
if (unsub_obj) unsub_obj()
})
async function postWiFiSettings(data: WifiSettings) { async function postWiFiSettings(data: WifiSettings) {
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data) const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data)
if (result.isErr()) { if (result.isErr()) {
+185 -181
View File
@@ -1,212 +1,216 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import { decodeMessage, MESSAGE_KEY_TO_TAG, socket } from '../../src/lib/stores/socket' 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 // Helper function to create encoded WebSocket messages
function createEncodedMessage(messageType: 'imu' | 'rssi' | 'mode', data: any): Uint8Array { function createEncodedMessage(messageType: 'imu' | 'rssi' | 'mode', data: unknown): Uint8Array {
const message: any = {} // eslint-disable-next-line @typescript-eslint/no-explicit-any
message[messageType] = data const message: any = {}
const wsMessage = WebsocketMessage.create(message) message[messageType] = data
return WebsocketMessage.encode(wsMessage).finish() const wsMessage = WebsocketMessage.create(message)
return WebsocketMessage.encode(wsMessage).finish()
} }
describe.sequential('WebSocket Integration Tests', () => { describe.sequential('WebSocket Integration Tests', () => {
let wss: WebSocketServer let wss: WebSocketServer
let TEST_PORT = 8765 let TEST_PORT = 8765
beforeEach(async () => { beforeEach(async () => {
// Use a different port for each test to avoid conflicts // Use a different port for each test to avoid conflicts
TEST_PORT++ TEST_PORT++
// Create real WebSocket server // Create real WebSocket server
wss = new WebSocketServer({ port: TEST_PORT }) wss = new WebSocketServer({ port: TEST_PORT })
// Wait for server to start // Wait for server to start
await new Promise<void>((resolve) => { await new Promise<void>(resolve => {
wss.on('listening', () => resolve()) wss.on('listening', () => resolve())
}) })
}) })
afterEach(async () => { afterEach(async () => {
// Close all connections and server // Close all connections and server
wss.clients.forEach((client) => client.close()) wss.clients.forEach(client => client.close())
await new Promise<void>((resolve) => { await new Promise<void>(resolve => {
wss.close(() => resolve()) wss.close(() => resolve())
}) })
// Wait a bit for cleanup // Wait a bit for cleanup
await new Promise(resolve => setTimeout(resolve, 100)) await new Promise(resolve => setTimeout(resolve, 100))
}) })
it('should connect to WebSocket server', async () => { it('should connect to WebSocket server', async () => {
socket.init(`ws://localhost:${TEST_PORT}`) socket.init(`ws://localhost:${TEST_PORT}`)
// Wait for connection // Wait for connection
await new Promise(resolve => setTimeout(resolve, 100)) await new Promise(resolve => setTimeout(resolve, 100))
let isConnected = false let isConnected = false
socket.subscribe(value => { socket.subscribe(value => {
isConnected = value isConnected = value
})() })()
expect(isConnected).toBe(true) expect(isConnected).toBe(true)
}) })
it('should receive and decode IMU data from server', async () => { it('should receive and decode IMU data from server', async () => {
let receivedIMUData: any = null let receivedIMUData: any = null
// Subscribe to IMU messages before connecting // Subscribe to IMU messages before connecting
const unsubscribe = socket.on(IMUData, (data) => { const unsubscribe = socket.on(IMUData, data => {
receivedIMUData = data receivedIMUData = data
}) })
// Connect socket // Connect socket
socket.init(`ws://localhost:${TEST_PORT}`) socket.init(`ws://localhost:${TEST_PORT}`)
// Wait for client to connect // Wait for client to connect
await new Promise<void>((resolve) => { await new Promise<void>(resolve => {
wss.on('connection', (ws) => { wss.on('connection', ws => {
// Server sends IMU data to client // Server sends IMU data to client
const imuPayload = IMUData.create({ const imuPayload = IMUData.create({
x: 3.25, x: 3.25,
y: 2.5, y: 2.5,
z: 1.75, z: 1.75,
heading: 10, heading: 10,
altitude: 11, altitude: 11,
bmpTemp: 22, bmpTemp: 22,
pressure: 23 pressure: 23
}) })
const encodedMessage = createEncodedMessage('imu', imuPayload) const encodedMessage = createEncodedMessage('imu', imuPayload)
ws.send(encodedMessage) ws.send(encodedMessage)
setTimeout(resolve, 50) setTimeout(resolve, 50)
}) })
}) })
expect(receivedIMUData).toBeDefined() expect(receivedIMUData).toBeDefined()
expect(receivedIMUData?.imu).toBeDefined() expect(receivedIMUData?.imu).toBeDefined()
expect(receivedIMUData?.imu.x).toBe(3.25) expect(receivedIMUData?.imu.x).toBe(3.25)
expect(receivedIMUData?.imu.y).toBe(2.5) expect(receivedIMUData?.imu.y).toBe(2.5)
expect(receivedIMUData?.imu.z).toBe(1.75) expect(receivedIMUData?.imu.z).toBe(1.75)
expect(receivedIMUData?.imu.heading).toBe(10) expect(receivedIMUData?.imu.heading).toBe(10)
expect(receivedIMUData?.imu.altitude).toBe(11) expect(receivedIMUData?.imu.altitude).toBe(11)
expect(receivedIMUData?.imu.bmpTemp).toBe(22) expect(receivedIMUData?.imu.bmpTemp).toBe(22)
expect(receivedIMUData?.imu.pressure).toBe(23) expect(receivedIMUData?.imu.pressure).toBe(23)
unsubscribe() unsubscribe()
}) })
it('should send IMU data from client to server using sendEvent', async () => { it('should send IMU data from client to server using sendEvent', async () => {
let serverReceivedData: any = null let serverReceivedData: any = null
// Connect socket // Connect socket
socket.init(`ws://localhost:${TEST_PORT}`) socket.init(`ws://localhost:${TEST_PORT}`)
// Wait for client to connect and send data // Wait for client to connect and send data
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
reject(new Error('Test timeout - server did not receive message')) reject(new Error('Test timeout - server did not receive message'))
}, 3000) }, 3000)
wss.on('connection', (ws) => { wss.on('connection', ws => {
// console.log('Server: Client connected') // console.log('Server: Client connected')
// Server listens for messages from client // Server listens for messages from client
ws.on('message', (data: Buffer) => { ws.on('message', (data: Buffer) => {
// console.log('Server: Received message, length:', data.length) // console.log('Server: Received message, length:', data.length)
// Skip empty messages (from ping, etc.) // Skip empty messages (from ping, etc.)
if (data.length === 0) { if (data.length === 0) {
console.log('Server: Skipping empty message (Probably a ping') console.log('Server: Skipping empty message (Probably a ping')
return return
} }
try { try {
// Decode the protobuf message // Decode the protobuf message
const decoded = WebsocketMessage.decode(new Uint8Array(data)) const decoded = WebsocketMessage.decode(new Uint8Array(data))
// console.log('Server: Decoded message:', JSON.stringify(decoded, null, 2)) // console.log('Server: Decoded message:', JSON.stringify(decoded, null, 2))
// Only resolve if we got actual IMU data // Only resolve if we got actual IMU data
if (decoded.imu) { if (decoded.imu) {
serverReceivedData = decoded serverReceivedData = decoded
clearTimeout(timeout) clearTimeout(timeout)
resolve() resolve()
} else { } else {
// console.log('Server: Message decoded but no IMU data, waiting...') // console.log('Server: Message decoded but no IMU data, waiting...')
} }
} catch (error) { } catch (error) {
console.error('Server: Failed to decode:', error) console.error('Server: Failed to decode:', error)
clearTimeout(timeout) clearTimeout(timeout)
reject(error) reject(error)
} }
}) })
}) })
// Wait for WebSocket to be fully connected // Wait for WebSocket to be fully connected
setTimeout(() => { setTimeout(() => {
console.log('Client: Sending IMU data...') console.log('Client: Sending IMU data...')
// Client sends IMU data to server // Client sends IMU data to server
const imuData = IMUData.create({ const imuData = IMUData.create({
x: 3.25, x: 3.25,
y: 2.5, y: 2.5,
z: 1.75, z: 1.75,
heading: 10, heading: 10,
altitude: 11, altitude: 11,
bmpTemp: 22, bmpTemp: 22,
pressure: 23 pressure: 23
}) })
socket.sendEvent(IMUData, imuData) socket.sendEvent(IMUData, imuData)
console.log('Client: sendEvent called') console.log('Client: sendEvent called')
}, 150) }, 150)
}) })
// Verify server received the data // Verify server received the data
expect(serverReceivedData).toBeDefined() expect(serverReceivedData).toBeDefined()
expect(serverReceivedData?.imu).toBeDefined() expect(serverReceivedData?.imu).toBeDefined()
expect(serverReceivedData?.imu.x).toBe(3.25) expect(serverReceivedData?.imu.x).toBe(3.25)
expect(serverReceivedData?.imu.y).toBe(2.5) expect(serverReceivedData?.imu.y).toBe(2.5)
expect(serverReceivedData?.imu.z).toBe(1.75) expect(serverReceivedData?.imu.z).toBe(1.75)
expect(serverReceivedData?.imu.heading).toBe(10) expect(serverReceivedData?.imu.heading).toBe(10)
expect(serverReceivedData?.imu.altitude).toBe(11) expect(serverReceivedData?.imu.altitude).toBe(11)
expect(serverReceivedData?.imu.bmpTemp).toBe(22) expect(serverReceivedData?.imu.bmpTemp).toBe(22)
expect(serverReceivedData?.imu.pressure).toBe(23) 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 () => { await new Promise<void>((resolve, reject) => {
// Connect socket const timeout = setTimeout(() => {
socket.init(`ws://localhost:${TEST_PORT}`) reject(new Error('Test timeout'))
}, 1000)
await new Promise<void>((resolve, reject) => { // Wait for WebSocket to be fully connected
const timeout = setTimeout(() => { setTimeout(() => {
reject(new Error('Test timeout')) console.log('Client: Sending invalid message type...')
}, 1000) // Send any invalid message type
const wsm = WebsocketMessage.create()
// Wait for WebSocket to be fully connected try {
setTimeout(() => { socket.sendEvent(WebsocketMessage as any, wsm)
console.log('Client: Sending invalid message type...') clearTimeout(timeout)
// Send any invalid message type reject(new Error('Expected sendEvent to throw, but it did not'))
const wsm = WebsocketMessage.create() } catch (e) {
try { console.log('Client: sendEvent correctly threw error:', e)
socket.sendEvent(WebsocketMessage as any, wsm) clearTimeout(timeout)
clearTimeout(timeout) resolve()
reject(new Error('Expected sendEvent to throw, but it did not')) }
} catch (e) { }, 150)
console.log('Client: sendEvent correctly threw error:', e) })
clearTimeout(timeout) })
resolve()
}
}, 150)
})
})
}) })
describe('WebsocketMessage Protobuf Encoding/Decoding', () => { 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({ const imuData = IMUData.create({
x: 3.25, x: 3.25,
y: 2.5, y: 2.5,
@@ -217,34 +221,35 @@ describe('WebsocketMessage Protobuf Encoding/Decoding', () => {
pressure: 23 pressure: 23
}) })
const encoded = IMUData.encode(imuData).finish() const encoded = IMUData.encode(imuData).finish()
const decoded = IMUData.decode(encoded) const decoded = IMUData.decode(encoded)
expect(decoded.x).toBe(3.25) expect(decoded.x).toBe(3.25)
expect(decoded.y).toBe(2.5) expect(decoded.y).toBe(2.5)
expect(decoded.z).toBe(1.75) expect(decoded.z).toBe(1.75)
expect(decoded.heading).toBe(10) expect(decoded.heading).toBe(10)
expect(decoded.altitude).toBe(11) expect(decoded.altitude).toBe(11)
expect(decoded.bmpTemp).toBe(22) expect(decoded.bmpTemp).toBe(22)
expect(decoded.pressure).toBe(23) expect(decoded.pressure).toBe(23)
}) })
it('should encode and decode two empty types correctly', () => { it('should encode and decode two empty types correctly', () => {
const encoded_ping = WebsocketMessage.encode(
const encoded_ping = WebsocketMessage.encode(WebsocketMessage.create({ pingmsg: PingMsg.create() })).finish() WebsocketMessage.create({ pingmsg: PingMsg.create() })
).finish()
const decoded_ping = decodeMessage(encoded_ping.buffer) 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) 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({
it('should encode and decode complete WebsocketMessage', () => { imu: IMUData.create({
const original = WebsocketMessage.create({
imu: IMUData.create({
x: 3.25, x: 3.25,
y: 2.5, y: 2.5,
z: 1.75, z: 1.75,
@@ -253,19 +258,18 @@ describe('WebsocketMessage Protobuf Encoding/Decoding', () => {
bmpTemp: 22, bmpTemp: 22,
pressure: 23 pressure: 23
}) })
}) })
const encoded = WebsocketMessage.encode(original).finish() const encoded = WebsocketMessage.encode(original).finish()
const decoded = WebsocketMessage.decode(encoded) const decoded = WebsocketMessage.decode(encoded)
expect(decoded.imu).toBeDefined() expect(decoded.imu).toBeDefined()
expect(decoded.imu?.x).toBe(3.25) expect(decoded.imu?.x).toBe(3.25)
expect(decoded.imu?.y).toBe(2.5) expect(decoded.imu?.y).toBe(2.5)
expect(decoded.imu?.z).toBe(1.75) expect(decoded.imu?.z).toBe(1.75)
expect(decoded.imu?.heading).toBe(10) expect(decoded.imu?.heading).toBe(10)
expect(decoded.imu?.altitude).toBe(11) expect(decoded.imu?.altitude).toBe(11)
expect(decoded.imu?.bmpTemp).toBe(22) expect(decoded.imu?.bmpTemp).toBe(22)
expect(decoded.imu?.pressure).toBe(23) expect(decoded.imu?.pressure).toBe(23)
}) })
}) })
+10 -10
View File
@@ -3,15 +3,15 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
import path from 'path' import path from 'path'
const config: UserConfigExport = { const config: UserConfigExport = {
plugins: [svelte()], plugins: [svelte()],
resolve: { resolve: {
alias: { alias: {
$lib: path.resolve(__dirname, './src/lib') $lib: path.resolve(__dirname, './src/lib')
} }
}, },
test: { test: {
globals: true, globals: true,
environment: 'jsdom' environment: 'jsdom'
} }
} }
export default defineConfig(config) export default defineConfig(config)