claude: move to typescript
This commit is contained in:
130
src/RotClient.ts
Normal file
130
src/RotClient.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { ClientConfig, DynamicData, InitData } from './types.js';
|
||||
|
||||
// Numeric parsing strategy for rotator HTTP endpoints
|
||||
const NumType = {
|
||||
String: 0,
|
||||
Int: 1,
|
||||
Float: 2,
|
||||
} as const;
|
||||
type NumType = (typeof NumType)[keyof typeof NumType];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user