From b545d89a978ef04adec0fdaf91cc8ea2c3c5fa81 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 19 Mar 2026 18:55:00 +0100 Subject: [PATCH] front-end --- RotRouter.js | 45 --- config.json | 2 +- index.js | 26 -- package-lock.json | 63 +++- package.json | 9 +- public/index.html | 504 +++++++++++++++++++++++++++++++ src/N1mmServer.js | 50 +++ RotClient.js => src/RotClient.js | 1 + src/RotRouter.js | 33 ++ src/WebsocketManager.js | 63 ++++ src/index.js | 34 +++ 11 files changed, 754 insertions(+), 76 deletions(-) delete mode 100644 RotRouter.js delete mode 100644 index.js create mode 100644 public/index.html create mode 100644 src/N1mmServer.js rename RotClient.js => src/RotClient.js (99%) create mode 100644 src/RotRouter.js create mode 100644 src/WebsocketManager.js create mode 100644 src/index.js diff --git a/RotRouter.js b/RotRouter.js deleted file mode 100644 index a513a48..0000000 --- a/RotRouter.js +++ /dev/null @@ -1,45 +0,0 @@ -import RotClient from './RotClient.js'; -import { XMLParser } from 'fast-xml-parser'; -import fs from 'node:fs'; - -export default class RotRouter { - constructor() { - this.clients = []; - this.loadConfig(); - - this.xmlParser = new XMLParser(); - } - - 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; - } - const client = this.findClientForBand(band); - if (!client) { - console.error(`No RotClient found for the ${band} band!`); - return; - } - - client.turn(az); - } catch (ex) { - console.error(`Failed to parse: ${xml}`, ex); - } - } - - loadConfig() { - const raw = fs.readFileSync("config.json", 'utf-8'); - 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; - } -} \ No newline at end of file diff --git a/config.json b/config.json index 2221f44..4602303 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "rot14": { - "ip": "192.168.0.82", + "ip": "192.168.33.82", "bands": [ 14 ] diff --git a/index.js b/index.js deleted file mode 100644 index 96527e9..0000000 --- a/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import dgram from 'node:dgram'; -import RotRouter from './RotRouter.js'; - -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}`); -}); - - -const router = new RotRouter(); - -server.on('message', (msg, rinfo) => { - console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); - router.processN1mmXml(msg); - -}); - - - -server.bind(12040); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 001c18b..e25b52c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,38 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "fast-xml-parser": "^5.3.5" + "@hono/node-server": "^1.19.11", + "@hono/node-ws": "^1.3.0", + "fast-xml-parser": "^5.3.5", + "hono": "^4.12.8" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@hono/node-ws": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@hono/node-ws/-/node-ws-1.3.0.tgz", + "integrity": "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q==", + "license": "MIT", + "dependencies": { + "ws": "^8.17.0" + }, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "@hono/node-server": "^1.19.2", + "hono": "^4.6.0" } }, "node_modules/fast-xml-parser": { @@ -30,6 +61,15 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/hono": { + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/strnum": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", @@ -41,6 +81,27 @@ } ], "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 6240286..9fa1c72 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,15 @@ "license": "ISC", "author": "ericek111", "type": "module", - "main": "index.js", + "main": "src/index.js", "scripts": { - "start": "node index.js", + "start": "node src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "fast-xml-parser": "^5.3.5" + "@hono/node-server": "^1.19.11", + "@hono/node-ws": "^1.3.0", + "fast-xml-parser": "^5.3.5", + "hono": "^4.12.8" } } diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..2f77935 --- /dev/null +++ b/public/index.html @@ -0,0 +1,504 @@ + + + + + + IP rotator + + + + + +
+ + +
+

+ + Loading... | POE + 0 V | + raw 0° | + SETUP + +
+ + +

+
+
+ + + + \ No newline at end of file diff --git a/src/N1mmServer.js b/src/N1mmServer.js new file mode 100644 index 0000000..dcee795 --- /dev/null +++ b/src/N1mmServer.js @@ -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); + } + } +} \ No newline at end of file diff --git a/RotClient.js b/src/RotClient.js similarity index 99% rename from RotClient.js rename to src/RotClient.js index 0514980..979b4e1 100644 --- a/RotClient.js +++ b/src/RotClient.js @@ -2,6 +2,7 @@ * 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; diff --git a/src/RotRouter.js b/src/RotRouter.js new file mode 100644 index 0000000..da26413 --- /dev/null +++ b/src/RotRouter.js @@ -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); + } + +} \ No newline at end of file diff --git a/src/WebsocketManager.js b/src/WebsocketManager.js new file mode 100644 index 0000000..8813306 --- /dev/null +++ b/src/WebsocketManager.js @@ -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]; + } + +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..88abc42 --- /dev/null +++ b/src/index.js @@ -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) \ No newline at end of file