diff --git a/src/models/Pong.ts b/src/models/Pong.ts new file mode 100644 index 0000000..746762f --- /dev/null +++ b/src/models/Pong.ts @@ -0,0 +1,3 @@ +export interface PongMessage { + type: 'pong'; +} diff --git a/src/services/websocket.ts b/src/services/websocket.ts index 31ffec0..c0511fe 100644 --- a/src/services/websocket.ts +++ b/src/services/websocket.ts @@ -11,12 +11,37 @@ import { import { ref, reactive } from 'vue'; import type { Subs } from 'src/models/Subscribe'; import type { Sets } from 'src/models/Set'; +import type { PongMessage } from 'src/models/Pong'; const pendingResponses = new Map void>(); -export const lastKnownValues = reactive(new Map()); +const lastKnownValues: Record = reactive({}); export let socket: WebSocket | null = null; const isConnected = ref(false); +let lastPongTime = Date.now(); + +function pingLoop(interval: number = 5000) { + // Start sending ping every 5 seconds + setInterval(() => { + if (!socket || socket.readyState !== WebSocket.OPEN) return; + + // If no pong received in last 10 seconds, close + if (Date.now() - lastPongTime > interval + 10000) { + console.warn('No pong response, closing socket...'); + socket.close(); + return; + } + socket.send(JSON.stringify({ type: 'ping' })); + }, interval); +} + +function isPong(msg: PongMessage | undefined | null) { + if (msg?.type === 'pong') { + lastPongTime = Date.now(); + return true; + } + return false; +} export function initWebSocket(url: string, $q?: QVueGlobals) { const connect = () => { @@ -25,6 +50,8 @@ export function initWebSocket(url: string, $q?: QVueGlobals) { socket.onopen = () => { console.log('WebSocket connected'); isConnected.value = true; + // Start sending ping every 5 seconds + pingLoop(5000); }; socket.onclose = () => { isConnected.value = false; @@ -49,43 +76,42 @@ export function initWebSocket(url: string, $q?: QVueGlobals) { }); }; socket.onmessage = (event) => { - const message = JSON.parse(event.data); - const id = message.id; + if (typeof event.data === 'string') { + const message = JSON.parse(event.data); - if (id && pendingResponses.has(id)) { - pendingResponses.get(id)?.(message); // resolve the promise - pendingResponses.delete(id); - return; - } + // Handle pong + if (isPong(message)) return; - if (message.publish) { - let changed = false; + const id = message.id; + if (id && pendingResponses.has(id)) { + pendingResponses.get(id)?.(message); // resolve the promise + pendingResponses.delete(id); + return; + } + if (message.publish) { + (message.publish as Publish[]).forEach((pub) => { + const uuid = pub.uuid; + const value = pub.value ?? ''; - (message.publish as Publish[]).forEach((pub) => { - const uuid = pub.uuid; - const value = pub.value ?? ''; - - if (uuid === undefined) { - return; - } - - const oldValue = lastKnownValues.get(String(uuid)); - if (oldValue !== value) { - lastKnownValues.set(uuid, value); // this is now reactive - - const existing = getSubscriptionsByUuid(pub.uuid); - if (existing) { - existing.value = value; - } else { - getAllSubscriptions()?.push({ value, uuid: uuid }); + if (uuid === undefined) { + return; } - changed = true; - } - }); + const oldValue = lastKnownValues[uuid]; + if (oldValue !== value) { + lastKnownValues[uuid] = value; // this is now reactive + if (pub.uuid) { + const existing = getSubscriptionsByUuid(pub.uuid); - if (changed) { - dbmData.value = buildTree(getAllSubscriptions()); // rebuild reactive tree + if (existing.value) { + existing.value.value = value; + } + } else { + getAllSubscriptions().push({ value, uuid: uuid }); + } + dbmData.splice(0, dbmData.length, ...buildTree(getAllSubscriptions())); // rebuild reactive tree + } + }); } } }; diff --git a/src/utils/number-helpers.ts b/src/utils/number-helpers.ts new file mode 100644 index 0000000..605bcc0 --- /dev/null +++ b/src/utils/number-helpers.ts @@ -0,0 +1,50 @@ +import type { Ref } from 'vue'; + +export function separate16BitUint(value: number): { highByte: number; lowByte: number } { + // Ensure the value is treated as a 16-bit unsigned integer + // (optional, but good for clarity and safety if 'value' might be outside 0-65535) + const normalizedValue = value & 0xffff; // Mask to ensure it's within 16 bits + + // Extract the low byte (least significant 8 bits) + // This is simply the value modulo 256, or bitwise AND with 0xFF + const lowByte = normalizedValue & 0xff; + + // Extract the high byte (most significant 8 bits) + // Right shift by 8 bits to move the high byte into the low byte's position, + // then mask with 0xFF to get just those 8 bits. + const highByte = (normalizedValue >> 8) & 0xff; + + return { highByte, lowByte }; +} + +export function combineBytesTo16BitUint(highByte: number, lowByte: number): number { + // Ensure both bytes are within the 0-255 range for safety + const safeHighByte = highByte & 0xff; + const safeLowByte = lowByte & 0xff; + + // Shift the high byte 8 bits to the left to place it in the higher position. + // Example: if highByte is 0xA4 (10100100), after shifting it becomes 0xA400 (1010010000000000). + const shiftedHighByte = safeHighByte << 8; + + // Combine the shifted high byte with the low byte using a bitwise OR. + // Example: if shiftedHighByte is 0xA400 and lowByte is 0x78 (01111000), + // the result is 0xA478 (1010010001111000). + const combinedValue = shiftedHighByte | safeLowByte; + + // Optional: Mask the result to ensure it's strictly within the 16-bit unsigned range (0 to 65535). + // This is good practice as JavaScript numbers are 64-bit floats, and this ensures + // the value wraps correctly if intermediate operations somehow exceeded 16 bits. + return combinedValue & 0xffff; +} + +export function addOne(val: Ref, limit: number) { + if (val.value < limit) { + val.value++; + } +} + +export function substractOne(val: Ref, limit: number) { + if (val.value > limit) { + val.value--; + } +}