import type { ClientConfig, DynamicData, InitData } from './types.js'; const enum NumType { String, Int, Float, } interface EndpointDef { key: string; url: string; numType: NumType; } /** * Client for the simple_rotator_interface_v protocol. * @see https://remoteqth.com/w/doku.php?id=simple_rotator_interface_v */ export default class RotClient { readonly label: string; private readonly ip: string; private readonly bands: number[]; private cachedData: InitData | null = null; private dynamicData: DynamicData | null = null; private readonly dynamicHandlers: Array<(data: DynamicData) => void> = []; constructor(label: string, { ip, bands }: ClientConfig) { this.label = label; this.ip = ip; this.bands = bands; setInterval(() => void this.fetchDynamicData(), 1000); } async readInitData(): Promise { if (this.cachedData) return this.cachedData; const endpoints: EndpointDef[] = [ { key: 'azShift', url: 'readStart', numType: NumType.Int }, { key: 'azRange', url: 'readMax', numType: NumType.Int }, { key: 'antRadiationAngle', url: 'readAnt', numType: NumType.Int }, { key: 'antName', url: 'readAntName', numType: NumType.String }, { key: 'mapUrl', url: 'readMapUrl', numType: NumType.String }, { key: 'mac', url: 'readMAC', numType: NumType.String }, { key: 'elevation', url: 'readElevation', numType: NumType.Int }, ]; this.cachedData = await this.readEndpoints(endpoints) as unknown as InitData; console.log(`${this.label} Set the initial offset to ${this.cachedData.azShift}.`); return this.cachedData; } private async readEndpoints(endpoints: EndpointDef[]): Promise> { const data: Record = {}; for (const { key, url, numType } of endpoints) { data[key] = await this.readKey(url, numType); } return data; } private async fetchDynamicData(): Promise { const endpoints: EndpointDef[] = [ { key: 'adc', url: 'readADC', numType: NumType.Float }, { key: 'azimuth', url: 'readAZ', numType: NumType.Int }, { key: 'status', url: 'readStat', numType: NumType.Int }, ]; const data = await this.readEndpoints(endpoints) as unknown as DynamicData; if (this.dynamicData !== null) { const oldData = this.dynamicData; const hasChanged = (Object.keys(oldData) as Array).some( key => oldData[key] !== data[key] ); if (hasChanged) { this.dynamicHandlers.forEach(handler => handler(data)); } } this.dynamicData = data; console.log(`${this.label}: `, this.dynamicData); } onDynamicDataUpdate(handler: (data: DynamicData) => void): void { this.dynamicHandlers.push(handler); } hasBand(band: number): boolean { return this.bands.includes(band); } turn(az: number): void { fetch(`http://${this.ip}:88/`, { body: `ROT=${az}`, method: 'POST', }).catch(() => {}); console.log(`Turning ${this.label} to ${az}°...`); } private async readKey(endpoint: string, numType: NumType): Promise { try { const resp = await fetch(`http://${this.ip}:88/${endpoint}`); const text = await resp.text(); if (numType === NumType.String) return text; if (numType === NumType.Int) return parseInt(text, 10); return parseFloat(text); } catch (ex) { console.error(`${this.label}: Failed to read ${endpoint}:`, ex); return null; } } getDynamicData(): DynamicData | null { return this.dynamicData; } getAzimuth(): number | null { return this.dynamicData?.azimuth ?? null; } getAdc(): number | null { return this.dynamicData?.adc ?? null; } getStatus(): number | null { return this.dynamicData?.status ?? null; } }