fetching + broadcasting dynamic data from RotClient-s

This commit is contained in:
2026-03-19 22:35:29 +01:00
parent b545d89a97
commit 47286248ef
5 changed files with 175 additions and 106 deletions
+86 -35
View File
@@ -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;
}
}
+19 -2
View File
@@ -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;
}
}
+15 -5
View File
@@ -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;
}
}
+8
View File
@@ -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())