initial commit

This commit is contained in:
2026-02-12 20:19:46 +01:00
commit a4c27387db
7 changed files with 215 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

73
RotClient.js Normal file
View File

@@ -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;
}
}
}

45
RotRouter.js Normal file
View File

@@ -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;
}
}

8
config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"rot14": {
"ip": "192.168.0.82",
"bands": [
14
]
}
}

26
index.js Normal file
View File

@@ -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);

46
package-lock.json generated Normal file
View File

@@ -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"
}
}
}

16
package.json Normal file
View File

@@ -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"
}
}