add a setpoint indicator

This commit is contained in:
2026-04-02 19:13:35 +02:00
parent 80b66be4fc
commit 22d750e3ab
7 changed files with 250 additions and 148 deletions
+3 -3
View File
@@ -28,7 +28,7 @@ export default class N1mmServer {
server.on('message', (msg, rinfo) => {
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
this.processN1mmXml(msg.toString());
this.processN1mmXml(msg.toString(), rinfo.address);
});
server.bind(12040);
@@ -38,7 +38,7 @@ export default class N1mmServer {
this.turnHandlers.push(handler);
}
private processN1mmXml(xml: string): void {
private processN1mmXml(xml: string, sourceIp: string): void {
try {
const parsed = this.xmlParser.parse(xml) as N1MMRotorXml;
const band = parseInt(parsed?.N1MMRotor?.freqband?.split(',')[0] ?? '0', 10);
@@ -49,7 +49,7 @@ export default class N1mmServer {
return;
}
this.turnHandlers.forEach(h => h.handleTurnMessage({ band, az }));
this.turnHandlers.forEach(h => h.handleTurnMessage({ band, az, sourceIp }));
} catch (ex) {
console.error(`Failed to parse: ${xml}`, ex);
}
+7
View File
@@ -89,10 +89,17 @@ export default class RotClient {
return this.bands.includes(band);
}
getBands(): number[] {
return this.bands;
}
turn(az: number): void {
fetch(`http://${this.ip}:88/`, {
body: `ROT=${az}`,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
}).catch(() => {});
console.log(`Turning ${this.label} to ${az}°...`);
}
+26 -1
View File
@@ -2,9 +2,16 @@ import { readFileSync } from 'node:fs';
import RotClient from './RotClient.js';
import type { Config, DynamicData, InitialState, TurnMessage, TurnMessageHandler } from './types.js';
export interface TurnEvent {
label: string;
az: number;
sourceIp: string;
}
export default class RotRouter implements TurnMessageHandler {
private readonly clients: Record<string, RotClient> = {};
private readonly routerDataHandlers: Array<(rotator: RotClient, data: DynamicData) => void> = [];
private readonly turnEventHandlers: Array<(event: TurnEvent) => void> = [];
constructor() {
this.loadConfig();
@@ -31,6 +38,24 @@ export default class RotRouter implements TurnMessageHandler {
return;
}
client.turn(msg.az);
this.fireTurnEvent(client.label, msg.az, msg.sourceIp);
}
turn(label: string, az: number, sourceIp: string): boolean {
const client = this.clients[label];
if (!client) return false;
client.turn(az);
this.fireTurnEvent(label, az, sourceIp);
return true;
}
private fireTurnEvent(label: string, az: number, sourceIp: string): void {
const event: TurnEvent = { label, az, sourceIp };
this.turnEventHandlers.forEach(h => h(event));
}
onTurnEvent(handler: (event: TurnEvent) => void): void {
this.turnEventHandlers.push(handler);
}
onRotatorData(handler: (rotator: RotClient, data: DynamicData) => void): void {
@@ -40,7 +65,7 @@ export default class RotRouter implements TurnMessageHandler {
async readInitialState(): Promise<InitialState> {
const state: InitialState = {};
for (const client of Object.values(this.clients)) {
state[client.label] = { initData: await client.readInitData() };
state[client.label] = { initData: await client.readInitData(), bands: client.getBands() };
}
return state;
}
+19 -1
View File
@@ -18,12 +18,30 @@ app.use('/*', serveStatic({ root: './public' }));
const websocketManager = new WebsocketManager();
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
websocketManager.setInitData(await router.readInitialState());
const initialState = await router.readInitialState();
websocketManager.setInitData(initialState);
router.onRotatorData((rotator, data) => {
websocketManager.broadcast({ [rotator.label]: { dynamic: data } });
});
router.onTurnEvent((event) => {
const setpoint = { targetAz: event.az, sourceIp: event.sourceIp };
const client = initialState[event.label];
if (client) client.setpoint = setpoint;
websocketManager.broadcast({ [event.label]: { turnedBy: event.sourceIp, targetAz: event.az } });
});
app.post('/turn/:label', async (c) => {
const label = c.req.param('label');
const body = await c.req.parseBody();
const az = Number(body['ROT']);
if (isNaN(az)) return c.text('Invalid azimuth', 400);
const sourceIp = c.req.header('x-forwarded-for') ?? c.req.header('x-real-ip') ?? 'unknown';
const ok = router.turn(label, az, sourceIp);
return ok ? c.text(`Turning ${label} to ${az}`) : c.text('Unknown rotator', 404);
});
app.get('/ws', upgradeWebSocket(() => websocketManager.getHandler()));
const server = serve(
+8
View File
@@ -1,6 +1,7 @@
export interface TurnMessage {
band: number;
az: number;
sourceIp: string;
}
export interface TurnMessageHandler {
@@ -30,8 +31,15 @@ export interface InitData {
elevation: number | null;
}
export interface Setpoint {
targetAz: number;
sourceIp: string;
}
export interface ClientState {
initData: InitData;
bands: number[];
setpoint?: Setpoint;
}
export type InitialState = Record<string, ClientState>;