diff --git a/config.json b/config.json index 4602303..208c2d8 100644 --- a/config.json +++ b/config.json @@ -4,5 +4,17 @@ "bands": [ 14 ] + }, + "rot7": { + "ip": "192.168.33.83", + "bands": [ + 7, 28 + ] + }, + "rot21": { + "ip": "192.168.33.84", + "bands": [ + 21, 144 + ] } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 15f3756..265f225 100644 --- a/public/index.html +++ b/public/index.html @@ -3,11 +3,11 @@ - IP rotator + RotRouter -
- - -
-

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

-
-
+
- \ No newline at end of file + diff --git a/src/N1mmServer.ts b/src/N1mmServer.ts index cfbed78..aeaa2a7 100644 --- a/src/N1mmServer.ts +++ b/src/N1mmServer.ts @@ -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); } diff --git a/src/RotClient.ts b/src/RotClient.ts index 4f872bc..7120925 100644 --- a/src/RotClient.ts +++ b/src/RotClient.ts @@ -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}°...`); } diff --git a/src/RotRouter.ts b/src/RotRouter.ts index ae71c5e..2bff637 100644 --- a/src/RotRouter.ts +++ b/src/RotRouter.ts @@ -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 = {}; 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 { 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; } diff --git a/src/index.ts b/src/index.ts index 6136ebb..aefb600 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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( diff --git a/src/types.ts b/src/types.ts index f621f63..df377ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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;