front-end
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import dgram from 'node:dgram';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
export default class N1mmServer {
|
||||
|
||||
constructor() {
|
||||
this.xmlParser = new XMLParser();
|
||||
this.turnHandlers = []; // TurnEventHandler
|
||||
}
|
||||
|
||||
start() {
|
||||
const server = dgram.createSocket('udp4');
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error(`server error:\n${err.stack}`);
|
||||
server.close();
|
||||
});
|
||||
server.on('listening', () => {
|
||||
const address = server.address();
|
||||
console.log(`server listening ${address.address}:${address.port}`);
|
||||
});
|
||||
server.on('message', this._onUdpMessage.bind(this));
|
||||
server.bind(12040);
|
||||
}
|
||||
|
||||
_onUdpMessage(msg, rinfo) {
|
||||
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
|
||||
this.processN1mmXml(msg);
|
||||
}
|
||||
|
||||
onTurnMessage(handler) {
|
||||
this.turnHandlers.push(handler);
|
||||
}
|
||||
|
||||
processN1mmXml(xml) {
|
||||
try {
|
||||
const msg = this.xmlParser.parse(xml);
|
||||
const band = parseInt(msg?.N1MMRotor?.freqband.split(',')[0] || 0, 10);
|
||||
const az = parseInt(msg?.N1MMRotor?.goazi.split(',')[0] || 0, 10);
|
||||
if (!band || !az) {
|
||||
console.error(`Missing band or azimuth`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.turnHandlers.forEach(h => h.handleTurnMessage({ band, az }));
|
||||
} catch (ex) {
|
||||
console.error(`Failed to parse: ${xml}`, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
constructor(label, clientConfig) {
|
||||
this.label = label;
|
||||
this.ip = clientConfig.ip;
|
||||
this.bands = clientConfig.bands;
|
||||
this.startOffset = null;
|
||||
|
||||
setInterval(async () => {
|
||||
console.log(`${this.label}: azi ${await this.readAzi()}, adc ${await this.readAdc()}, status ${await this.readStatus()}`);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
hasBand(band) {
|
||||
return this.bands.includes(band);
|
||||
}
|
||||
|
||||
turn(az) {
|
||||
fetch(`http://${this.ip}:88/`, {
|
||||
"body": "ROT=" + az,
|
||||
"method": "POST",
|
||||
}).catch(() => {});
|
||||
console.log(`Turning ${this.label} to ${az}°...`);
|
||||
}
|
||||
|
||||
async readAzi() {
|
||||
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);
|
||||
} catch(ex) {
|
||||
console.error(`${this.label}: Failed to read azimuth:`, 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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import RotClient from './RotClient.js';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export default class RotRouter { // implements TurnEventHandler
|
||||
constructor() {
|
||||
this.clients = [];
|
||||
this.loadConfig();
|
||||
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
findClientForBand(band) {
|
||||
return Object.values(this.clients).find(c => c.hasBand(band)) || null;
|
||||
}
|
||||
|
||||
handleTurnMessage(msg) { // msg : { band: 14, az: 321 }
|
||||
const client = this.findClientForBand(msg.band);
|
||||
if (!client) {
|
||||
console.error(`No RotClient found for the ${msg.band} band!`);
|
||||
return;
|
||||
}
|
||||
|
||||
client.turn(msg.az);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createNodeWebSocket } from '@hono/node-ws'
|
||||
import {serve} from "@hono/node-server";
|
||||
|
||||
export default class WebsocketManager {
|
||||
|
||||
constructor() {
|
||||
this.clients = [];
|
||||
this.lastPing = {};
|
||||
|
||||
setInterval(() => {
|
||||
const thr = Date.now() - 60000;
|
||||
this.clients.filter(client => this.lastPing[client] < thr).forEach((client) => this.leaveClient(client));
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
getHandler() {
|
||||
const _this = this;
|
||||
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;
|
||||
|
||||
const data = JSON.parse(event.data);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
if (data.ping) {
|
||||
_this.lastPing[ws] = Date.now();
|
||||
}
|
||||
|
||||
if (data.data)
|
||||
_this.handleMessage(ws, data.data);
|
||||
},
|
||||
onClose: (event, ws) => {
|
||||
this.leaveClient(ws);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(ws, data) {
|
||||
|
||||
}
|
||||
|
||||
broadcast(message) {
|
||||
this.clients.forEach(client => client.send(JSON.stringify({data: message})));
|
||||
}
|
||||
|
||||
joinClient(ws) {
|
||||
this.clients.push(ws);
|
||||
this.lastPing[ws] = Date.now();
|
||||
}
|
||||
|
||||
leaveClient(ws) {
|
||||
this.clients = this.clients.filter(s => s !== ws);
|
||||
delete this.lastPing[ws];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import RotRouter from './RotRouter.js';
|
||||
import N1mmServer from "./N1mmServer.js";
|
||||
import {Hono} from 'hono';
|
||||
import { serve } from '@hono/node-server'
|
||||
import { serveStatic } from '@hono/node-server/serve-static'
|
||||
import { createNodeWebSocket } from "@hono/node-ws";
|
||||
import WebsocketManager from "./WebsocketManager.js";
|
||||
|
||||
const router = new RotRouter();
|
||||
|
||||
const n1mm = new N1mmServer();
|
||||
n1mm.onTurnMessage(router);
|
||||
// n1mm.start();
|
||||
|
||||
const app = new Hono();
|
||||
// app.get('/', (c) => c.text('Hono!'));
|
||||
app.use('/*', serveStatic({ root: './public' }))
|
||||
|
||||
|
||||
const websocketManager = new WebsocketManager();
|
||||
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
|
||||
app.get(
|
||||
'/ws',
|
||||
upgradeWebSocket((c) => websocketManager.getHandler())
|
||||
);
|
||||
|
||||
const server = serve({
|
||||
fetch: app.fetch,
|
||||
port: 8787,
|
||||
}, (info) => {
|
||||
console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000
|
||||
});
|
||||
|
||||
injectWebSocket(server)
|
||||
Reference in New Issue
Block a user