add a setpoint indicator
This commit is contained in:
+3
-3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user