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())