commit a4c27387dbf4758c53731f275133cc066cb251ac Author: DELL4 Date: Thu Feb 12 20:19:46 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/RotClient.js b/RotClient.js new file mode 100644 index 0000000..0514980 --- /dev/null +++ b/RotClient.js @@ -0,0 +1,73 @@ +/** + * 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; + } + } + + +} \ No newline at end of file diff --git a/RotRouter.js b/RotRouter.js new file mode 100644 index 0000000..a513a48 --- /dev/null +++ b/RotRouter.js @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..2221f44 --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "rot14": { + "ip": "192.168.0.82", + "bands": [ + 14 + ] + } +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..96527e9 --- /dev/null +++ b/index.js @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..001c18b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "rotrouter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rotrouter", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "fast-xml-parser": "^5.3.5" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.5.tgz", + "integrity": "sha512-JeaA2Vm9ffQKp9VjvfzObuMCjUYAp5WDYhRYL5LrBPY/jUDlUtOvDfot0vKSkB9tuX885BDHjtw4fZadD95wnA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6240286 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "rotrouter", + "version": "1.0.0", + "description": "Route rotator requests to the proper rotator controller", + "license": "ISC", + "author": "ericek111", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "fast-xml-parser": "^5.3.5" + } +}