From 47286248ef3b8607febcc680363273071e2494d4 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 19 Mar 2026 22:35:29 +0100 Subject: [PATCH] fetching + broadcasting dynamic data from RotClient-s --- public/index.html | 111 ++++++++++++++++-------------------- src/RotClient.js | 121 ++++++++++++++++++++++++++++------------ src/RotRouter.js | 21 ++++++- src/WebsocketManager.js | 20 +++++-- src/index.js | 8 +++ 5 files changed, 175 insertions(+), 106 deletions(-) diff --git a/public/index.html b/public/index.html index 2f77935..15f3756 100644 --- a/public/index.html +++ b/public/index.html @@ -395,6 +395,7 @@ this.state = new RotatorState(); this.ui = new RotatorUI(this.state); this.renderer = new RotatorRenderer('MainCanvas', this.state); + this.websocket = null; this.bindEvents(); } @@ -415,22 +416,10 @@ } async initialize() { - // Sequential fetch to protect embedded web servers from connection exhaustion - const endpoints = [ - { key: 'azShift', url: 'readStart', isNum: true }, - { key: 'azRange', url: 'readMax', isNum: true }, - { key: 'antRadiationAngle', url: 'readAnt', isNum: true }, - { key: 'antName', url: 'readAntName', isNum: false }, - { key: 'mapUrl', url: 'readMapUrl', isNum: false }, - { key: 'mac', url: 'readMAC', isNum: false }, - { key: 'elevation', url: 'readElevation', isNum: true } - ]; + const initData = await this.startWs(); - for (let conf of endpoints) { - const val = await RotatorAPI.fetchText(conf.url); - if (val !== null) { - this.state[conf.key] = conf.isNum ? Number(val) : val; - } + for (const [key, value] of Object.entries(initData)) { + this.state[key] = value; } this.ui.updateStatic(); @@ -438,59 +427,53 @@ // Start timers // this.startLoops(); - this.startWs(); + } - startWs() { - const websocket = new WebSocket(document.location.href.replace(/\/?$/, '/') + "ws"); - websocket.addEventListener("message", (e) => { - if (!e || !e.data) - return; - const data = JSON.parse(e.data); - if (!data) - return; + async startWs() { + return new Promise((resolve, reject) => { + this.websocket = new WebSocket(document.location.href.replace(/\/?$/, '/') + "ws"); + this.websocket.addEventListener("message", (e) => { + if (!e || !e.data) + return; - console.log(`RECEIVED: `, data); + const parsed = JSON.parse(e.data); + if (!parsed) + return; + + const data = parsed.data; + if (!data) + return; + + const rotData = data['rot14']; + if (rotData.initData) { + resolve(rotData.initData); // super ugly + } + + if (rotData.dynamic) { + const numAz = rotData.dynamic.azimuth; + const numStat = rotData.dynamic.status; + + let needsRender = false; + if (this.state.azimuth !== numAz || this.state.status !== numStat) { + needsRender = true; + } + + this.state.adc = rotData.dynamic.adc; + this.state.azimuth = numAz; + this.state.status = numStat; + this.state.lastSeen = Date.now(); + + if (needsRender) this.renderer.render(); + } + + console.log(`RECEIVED: `, data); + }); + + setInterval(() => { + this.websocket.send(JSON.stringify({ ping: 1 })); + }, 2000); }); - setInterval(() => { - websocket.send(JSON.stringify({ ping: 1 })); - }, 2000); - } - - async pollData() { - const adc = await RotatorAPI.fetchText('readADC'); - const az = await RotatorAPI.fetchText('readAZ'); - const stat = await RotatorAPI.fetchText('readStat'); - - if (adc !== null && az !== null && stat !== null) { - const numAz = Number(az); - const numStat = Number(stat); - - let needsRender = false; - if (this.state.azimuth !== numAz || this.state.status !== numStat) { - needsRender = true; - } - - this.state.adc = Number(adc); - this.state.azimuth = numAz; - this.state.status = numStat; - this.state.lastSeen = Date.now(); - - if (needsRender) this.renderer.render(); - } - this.ui.updateDynamic(); - } - - startLoops() { - // Data polling - setInterval(() => this.pollData(), 500); - // Force UI status check (offline state handling) - setInterval(() => this.ui.updateDynamic(), 2000); - // Map refresh - setInterval(() => this.renderer.updateMap(this.state.mapUrl), 600000); - - // Trigger first poll instantly - this.pollData(); } } diff --git a/src/RotClient.js b/src/RotClient.js index 979b4e1..703f168 100644 --- a/src/RotClient.js +++ b/src/RotClient.js @@ -1,4 +1,6 @@ -/** +import {handle} from "@hono/node-server/vercel"; + +/** * Implementation of a simple client for simple_rotator_interface_v -- https://remoteqth.com/w/doku.php?id=simple_rotator_interface_v */ export default class RotClient { @@ -8,12 +10,78 @@ export default class RotClient { this.ip = clientConfig.ip; this.bands = clientConfig.bands; this.startOffset = null; + this.cachedData = null + this.dynamicData = null; + this.dynamicHandlers = []; setInterval(async () => { - console.log(`${this.label}: azi ${await this.readAzi()}, adc ${await this.readAdc()}, status ${await this.readStatus()}`); + await this._readDynamicData(); + console.log(`${this.label}: `, this.dynamicData); }, 1000); } + async readInitData() { + if (this.cachedData) + return this.cachedData; + + const endpoints = [ + { key: 'azShift', url: 'readStart', numType: 1 }, + { key: 'azRange', url: 'readMax', numType: 1 }, + { key: 'antRadiationAngle', url: 'readAnt', numType: 1 }, + { key: 'antName', url: 'readAntName', numType: 0 }, + { key: 'mapUrl', url: 'readMapUrl', numType: 0 }, + { key: 'mac', url: 'readMAC', numType: 0 }, + { key: 'elevation', url: 'readElevation', numType: 1 }, + ]; + + this.cachedData = await this._readEndpoints(endpoints); + + console.log(`${this.label} Set the initial offset to ${this.cachedData.azShift}.`); + + return this.cachedData; + } + + async _readEndpoints(endpoints) { + const data = {}; + for (const row of endpoints) { + data[row.key] = await this.readKey(row.url, row.numType); + } + return data; + } + + async _readDynamicData() { + const dynamicEndpoints = [ + { key: 'adc', url: 'readADC', numType: 2 }, + { key: 'azimuth', url: 'readAZ', numType: 1 }, + { key: 'status', url: 'readStat', numType: 1 }, + ]; + + const data = await this._readEndpoints(dynamicEndpoints); + + if (this.dynamicData !== null) { + const oldData = this.dynamicData; + // const changed = {}; // send everything for now + let hasChanged = false; + for (const key in oldData) { + if (oldData[key] !== data[key]) { + // changed[key] = data[key]; + hasChanged = true; + } + } + + if (hasChanged) + this.dynamicHandlers.forEach((handler) => handler(data)); + } + + this.dynamicData = data; + + return data; + } + + onDynamicDataUpdate(handler) { + this.dynamicHandlers.push(handler); + } + hasBand(band) { return this.bands.includes(band); } @@ -26,49 +94,32 @@ export default class RotClient { console.log(`Turning ${this.label} to ${az}°...`); } - async readAzi() { + async readKey(endpoint, numType) { try { - if (this.startOffset === null) { - this.startOffset = await this.readOffset(); - console.log(`${this.label} Set the initial offset to ${this.startOffset}.`); - } - const resp = await fetch(`http://${this.ip}:88/readAZ`); - return this.startOffset + parseInt(await resp.text(), 10); + const resp = await fetch(`http://${this.ip}:88/${endpoint}`); + const text = await resp.text(); + // TODO: End my suffering. + return numType ? (numType === 1 ? parseInt(text, 10) : parseFloat(text)) : text; } catch(ex) { - console.error(`${this.label}: Failed to read azimuth:`, ex); + console.error(`${this.label}: Failed to read ${endpoint}:`, ex); return null; } } - async readAdc() { - try { - const resp = await fetch(`http://${this.ip}:88/readADC`); - return parseFloat(await resp.text()); - } catch(ex) { - console.error(`${this.label}: Failed to read ADC:`, ex); - return null; - } + getDynamicData() { + return this.dynamicData; } - async readStatus() { - try { - const resp = await fetch(`http://${this.ip}:88/readStat`); - return parseInt(await resp.text(), 10); - } catch(ex) { - console.error(`${this.label}: Failed to read status:`, ex); - return null; - } + getAzi() { + return this.dynamicData.azimuth; } - async readOffset() { - try { - const resp = await fetch(`http://${this.ip}:88/readStart`); - return parseInt(await resp.text(), 10); - } catch(ex) { - console.error(`${this.label}: Failed to read the initial offset:`, ex); - return null; - } + getAdc() { + return this.dynamicData.adc; + } + + getStatus() { + return this.dynamicData.status; } - } \ No newline at end of file diff --git a/src/RotRouter.js b/src/RotRouter.js index da26413..0bb61c2 100644 --- a/src/RotRouter.js +++ b/src/RotRouter.js @@ -3,8 +3,9 @@ import fs from 'node:fs'; export default class RotRouter { // implements TurnEventHandler constructor() { - this.clients = []; + this.clients = {}; this.loadConfig(); + this.routerDataHandlers = []; } @@ -12,7 +13,9 @@ export default class RotRouter { // implements TurnEventHandler const raw = fs.readFileSync("config.json", 'utf-8'); // TODO: Is the path correct? const parsed = JSON.parse(raw); for (const [label, clientConfig] of Object.entries(parsed)) { - this.clients[label] = new RotClient(label, clientConfig); + const client = new RotClient(label, clientConfig); + client.onDynamicDataUpdate(data => this.routerDataHandlers.forEach(handler => handler(client, data))); + this.clients[label] = client; } } @@ -30,4 +33,18 @@ export default class RotRouter { // implements TurnEventHandler client.turn(msg.az); } + onRotatorData(handler) { // (RotClient, data) + this.routerDataHandlers.push(handler); + } + + async readInitialState() { + const ret = {}; + for (const client of Object.values(this.clients)) { + ret[client.label] = { + initData: await client.readInitData() + }; + } + return ret; + } + } \ No newline at end of file diff --git a/src/WebsocketManager.js b/src/WebsocketManager.js index 8813306..feec76c 100644 --- a/src/WebsocketManager.js +++ b/src/WebsocketManager.js @@ -6,6 +6,7 @@ export default class WebsocketManager { constructor() { this.clients = []; this.lastPing = {}; + this.initData = null; setInterval(() => { const thr = Date.now() - 60000; @@ -18,10 +19,8 @@ export default class WebsocketManager { return { onOpen: (event, ws) => { this.joinClient(ws); - ws.send(JSON.stringify("cau")); }, onMessage(event, ws) { - console.log(`Message from client: ${event.data}`) if (!event.data) return; @@ -33,8 +32,10 @@ export default class WebsocketManager { _this.lastPing[ws] = Date.now(); } - if (data.data) + if (data.data) { _this.handleMessage(ws, data.data); + console.log(`Data from client: ${event.data}`); + } }, onClose: (event, ws) => { this.leaveClient(ws); @@ -46,13 +47,18 @@ export default class WebsocketManager { } - broadcast(message) { - this.clients.forEach(client => client.send(JSON.stringify({data: message}))); + send(ws, data) { + ws.send(JSON.stringify({data: data})); + } + + broadcast(data) { + this.clients.forEach(client => this.send(client, data)); } joinClient(ws) { this.clients.push(ws); this.lastPing[ws] = Date.now(); + this.send(ws, this.initData) } leaveClient(ws) { @@ -60,4 +66,8 @@ export default class WebsocketManager { delete this.lastPing[ws]; } + setInitData(initData) { + this.initData = initData; + } + } diff --git a/src/index.js b/src/index.js index 88abc42..15b5ecb 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,14 @@ app.use('/*', serveStatic({ root: './public' })) const websocketManager = new WebsocketManager(); const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }) +websocketManager.setInitData(await router.readInitialState()); + +router.onRotatorData((rotator, data) => { + const wsUpdate = {}; + wsUpdate[rotator.label] = { dynamic: data }; + websocketManager.broadcast(wsUpdate); +}); + app.get( '/ws', upgradeWebSocket((c) => websocketManager.getHandler())