129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
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<InitData> {
|
|
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<Record<string, string | number | null>> {
|
|
const data: Record<string, string | number | null> = {};
|
|
for (const { key, url, numType } of endpoints) {
|
|
data[key] = await this.readKey(url, numType);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
private async fetchDynamicData(): Promise<void> {
|
|
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<keyof DynamicData>).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<string | number | null> {
|
|
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;
|
|
}
|
|
}
|