✨ Adds msgPack and update message protocol
This commit is contained in:
+139
-101
@@ -1,122 +1,160 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import msgpack from 'msgpack-lite';
|
||||
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
|
||||
type SocketEvent = (typeof socketEvents)[number];
|
||||
|
||||
type SocketMessage = [number, string?, unknown?];
|
||||
|
||||
let useBinary = false;
|
||||
|
||||
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
|
||||
useBinary = data instanceof ArrayBuffer;
|
||||
|
||||
try {
|
||||
if (useBinary) {
|
||||
return msgpack.decode(new Uint8Array(data as ArrayBuffer));
|
||||
}
|
||||
return JSON.parse(data as string);
|
||||
} catch (error) {
|
||||
console.error(`Could not decode data: ${data} - ${error}`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const encodeMessage = (data: unknown) => {
|
||||
try {
|
||||
return useBinary ? msgpack.encode(data) : JSON.stringify(data);
|
||||
} catch (error) {
|
||||
console.error(`Could not encode data: ${data} - ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
function createWebSocket() {
|
||||
let listeners = new Map<string, Set<(data?: unknown) => void>>();
|
||||
const { subscribe, set } = writable(false);
|
||||
const reconnectTimeoutTime = 5000;
|
||||
let unresponsiveTimeoutId: number;
|
||||
let reconnectTimeoutId: number;
|
||||
let ws: WebSocket;
|
||||
let socketUrl: string | URL;
|
||||
const listeners = new Map<string, Set<(data?: unknown) => void>>();
|
||||
const { subscribe, set } = writable(false);
|
||||
const reconnectTimeoutTime = 5000;
|
||||
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>;
|
||||
let reconnectTimeoutId: ReturnType<typeof setTimeout>;
|
||||
let ws: WebSocket;
|
||||
let socketUrl: string | URL;
|
||||
|
||||
function init(url: string | URL) {
|
||||
socketUrl = url;
|
||||
connect();
|
||||
}
|
||||
function init(url: string | URL) {
|
||||
socketUrl = url;
|
||||
connect();
|
||||
}
|
||||
|
||||
function disconnect(reason: SocketEvent, event?: Event) {
|
||||
ws.close();
|
||||
set(false);
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get(reason)?.forEach((listener) => listener(event));
|
||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
|
||||
}
|
||||
function disconnect(reason: SocketEvent, event?: Event) {
|
||||
ws.close();
|
||||
set(false);
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get(reason)?.forEach(listener => listener(event));
|
||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(socketUrl);
|
||||
ws.onopen = (ev) => {
|
||||
set(true);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get('open')?.forEach((listener) => listener(ev));
|
||||
for (const event of listeners.keys()) {
|
||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
};
|
||||
ws.onmessage = (message) => {
|
||||
resetUnresponsiveCheck();
|
||||
let data = message.data;
|
||||
if (data instanceof ArrayBuffer) {
|
||||
listeners.get('binary')?.forEach((listener) => listener(data));
|
||||
return;
|
||||
}
|
||||
data = data.substring(1);
|
||||
function connect() {
|
||||
ws = new WebSocket(socketUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = ev => {
|
||||
ping();
|
||||
useBinary = true;
|
||||
ping();
|
||||
set(true);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get('open')?.forEach(listener => listener(ev));
|
||||
for (const event of listeners.keys()) {
|
||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
};
|
||||
ws.onmessage = frame => {
|
||||
resetUnresponsiveCheck();
|
||||
const message = decodeMessage(frame.data);
|
||||
if (!message) return;
|
||||
const [_, event, payload = undefined] = message;
|
||||
if (event) listeners.get(event)?.forEach(listener => listener(payload));
|
||||
};
|
||||
ws.onerror = ev => disconnect('error', ev);
|
||||
ws.onclose = ev => disconnect('close', ev);
|
||||
}
|
||||
|
||||
if (!data) return;
|
||||
function unsubscribe(event: string, listener?: (data: unknown) => void) {
|
||||
const eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
|
||||
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
|
||||
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {}
|
||||
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
|
||||
};
|
||||
ws.onerror = (ev) => disconnect('error', ev);
|
||||
ws.onclose = (ev) => disconnect('close', ev);
|
||||
}
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function unsubscribe(event: string, listener?: (data: any) => void) {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([2, event, data]);
|
||||
}
|
||||
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
function unsubscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([1, event]);
|
||||
}
|
||||
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
||||
}
|
||||
function subscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([0, event]);
|
||||
}
|
||||
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(`2/${event}[${JSON.stringify(data)}]`);
|
||||
}
|
||||
function send(data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
const serialized = encodeMessage(data);
|
||||
if (!serialized) {
|
||||
console.error('Could not serialize data:', data);
|
||||
return;
|
||||
}
|
||||
ws.send(serialized);
|
||||
}
|
||||
|
||||
function unsubscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send('1/' + event);
|
||||
}
|
||||
function ping() {
|
||||
const serialized = encodeMessage([4]);
|
||||
if (!serialized) {
|
||||
console.error('Could not serialize message');
|
||||
return;
|
||||
}
|
||||
ws.send(serialized);
|
||||
}
|
||||
|
||||
function subscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send('0/' + event);
|
||||
}
|
||||
return {
|
||||
subscribe,
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event as SocketEvent)) {
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
eventListeners = new Set();
|
||||
listeners.set(event, eventListeners);
|
||||
}
|
||||
eventListeners.add(listener as (data: unknown) => void);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event as SocketEvent)) {
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
eventListeners = new Set();
|
||||
listeners.set(event, eventListeners);
|
||||
}
|
||||
eventListeners.add(listener as (data: any) => void);
|
||||
|
||||
return () => {
|
||||
unsubscribe(event, listener);
|
||||
};
|
||||
},
|
||||
off: (event: string, listener?: (data: any) => void) => {
|
||||
unsubscribe(event, listener);
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
unsubscribe(event, listener as (data: unknown) => void);
|
||||
};
|
||||
},
|
||||
off: <T>(event: string, listener?: (data: T) => void) => {
|
||||
unsubscribe(event, listener as (data: unknown) => void);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const socket = createWebSocket();
|
||||
export const socket = createWebSocket();
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { Modals, modals } from 'svelte-modals';
|
||||
import Toast from '$lib/components/toasts/Toast.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.css';
|
||||
import Menu from '../lib/components/menu/Menu.svelte';
|
||||
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
|
||||
import {
|
||||
telemetry,
|
||||
analytics,
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
outControllerData,
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location,
|
||||
useFeatureFlags
|
||||
} from '$lib/stores';
|
||||
import type { Analytics, DownloadOTA } from '$lib/types/models';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { Modals, modals } from 'svelte-modals';
|
||||
import Toast from '$lib/components/toasts/Toast.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.css';
|
||||
import Menu from '../lib/components/menu/Menu.svelte';
|
||||
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
|
||||
import {
|
||||
telemetry,
|
||||
analytics,
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
outControllerData,
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location,
|
||||
useFeatureFlags,
|
||||
} from '$lib/stores';
|
||||
import type { Analytics, DownloadOTA } from '$lib/types/models';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { children }: Props = $props();
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
onMount(async () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
onMount(async () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
|
||||
addEventListeners();
|
||||
addEventListeners();
|
||||
|
||||
outControllerData.subscribe(data => socket.sendEvent('input', { data }));
|
||||
mode.subscribe(data => socket.sendEvent('mode', { data }));
|
||||
servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data }));
|
||||
kinematicData.subscribe(data => socket.sendEvent('position', { data }));
|
||||
outControllerData.subscribe(data => socket.sendEvent('input', data));
|
||||
mode.subscribe(data => socket.sendEvent('mode', data));
|
||||
servoAnglesOut.subscribe(data => socket.sendEvent('angles', data));
|
||||
kinematicData.subscribe(data => socket.sendEvent('position', data));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListeners();
|
||||
});
|
||||
|
||||
const addEventListeners = () => {
|
||||
socket.on('open', handleOpen);
|
||||
socket.on('close', handleClose);
|
||||
socket.on('error', handleError);
|
||||
socket.on('rssi', handleNetworkStatus);
|
||||
socket.on('mode', (data: ModesEnum) => mode.set(data));
|
||||
socket.on('analytics', handleAnalytics);
|
||||
socket.on('angles', (angles: number[]) => {
|
||||
if (angles.length) servoAngles.set(angles);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListeners();
|
||||
features.subscribe(data => {
|
||||
if (data?.download_firmware) socket.on('otastatus', handleOAT);
|
||||
if (data?.sonar) socket.on('sonar', data => console.log(data));
|
||||
});
|
||||
};
|
||||
|
||||
const addEventListeners = () => {
|
||||
socket.on('open', handleOpen);
|
||||
socket.on('close', handleClose);
|
||||
socket.on('error', handleError);
|
||||
socket.on('rssi', handleNetworkStatus);
|
||||
socket.on('mode', (data: ModesEnum) => mode.set(data));
|
||||
socket.on('analytics', handleAnalytics);
|
||||
socket.on('angles', (angles: number[]) => {
|
||||
if (angles.length) servoAngles.set(angles);
|
||||
});
|
||||
features.subscribe(data => {
|
||||
if (data?.download_firmware) socket.on('otastatus', handleOAT);
|
||||
if (data?.sonar) socket.on('sonar', data => console.log(data));
|
||||
});
|
||||
};
|
||||
const removeEventListeners = () => {
|
||||
socket.off('analytics', handleAnalytics);
|
||||
socket.off('open', handleOpen);
|
||||
socket.off('close', handleClose);
|
||||
socket.off('rssi', handleNetworkStatus);
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
|
||||
const removeEventListeners = () => {
|
||||
socket.off('analytics', handleAnalytics);
|
||||
socket.off('open', handleOpen);
|
||||
socket.off('close', handleClose);
|
||||
socket.off('rssi', handleNetworkStatus);
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000);
|
||||
telemetry.setRSSI(0);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000);
|
||||
telemetry.setRSSI(0);
|
||||
};
|
||||
const handleError = (data: any) => console.error(data);
|
||||
|
||||
const handleError = (data: any) => console.error(data);
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data);
|
||||
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data);
|
||||
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
|
||||
|
||||
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
|
||||
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
|
||||
|
||||
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
|
||||
|
||||
let menuOpen = $state(false);
|
||||
let menuOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{page.data.title}</title>
|
||||
<title>{page.data.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
|
||||
<!-- Main page content here -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay"></label>
|
||||
<Menu menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
<!-- Main page content here -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay"></label>
|
||||
<Menu menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modals>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
{#snippet backdrop()}
|
||||
<div
|
||||
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
|
||||
transition:fade
|
||||
onclick={modals.closeAll}
|
||||
></div>
|
||||
{/snippet}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
{#snippet backdrop()}
|
||||
<div
|
||||
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
|
||||
transition:fade
|
||||
onclick={modals.closeAll}>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modals>
|
||||
|
||||
<Toast />
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket, useFeatureFlags } from '$lib/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket } from '$lib/stores';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
const update = async () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Connection</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Connection</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
</SettingsCard>
|
||||
|
||||
Reference in New Issue
Block a user