UNPKG

ngx-kel-agent

Version:

This is a client library for Angular applications to integrate with [`kel-agent`](https://github.com/k0swe/kel-agent). It provides an Angular service that creates and manages the websocket connection, and exposes incoming messages as `Observable`s and out

493 lines (485 loc) 19.3 kB
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs'; import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; import { debounceTime, retryWhen, tap, delay } from 'rxjs/operators'; import { webSocket } from 'rxjs/webSocket'; class AgentMessageService { constructor() { this.rxMessage$ = new Subject(); this.txMessage$ = new Subject(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AgentMessageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AgentMessageService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AgentMessageService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [] }); class WsjtxService { constructor(messages) { this.messages = messages; /** Whether we're getting any messages from WSJT-X. */ this.connected$ = new BehaviorSubject(false); /** Subject for listening to WSJT-X "Heartbeat" messages. */ this.heartbeat$ = new ReplaySubject(1); /** Subject for listening to WSJT-X "Status" messages. */ this.status$ = new ReplaySubject(1); /** Subject for listening to WSJT-X "Decode" messages. */ this.decode$ = new Subject(); /** Subject for listening to WSJT-X "Clear" messages. */ this.clear$ = new Subject(); /** Subject for listening to WSJT-X "QsoLogged" messages. */ this.qsoLogged$ = new Subject(); /** Subject for listening to WSJT-X "Close" messages. */ this.close$ = new Subject(); /** Subject for listening to WSJT-X "WsprDecode" messages. */ this.wsprDecode$ = new Subject(); /** Subject for listening to WSJT-X "LoggedAdif" messages. */ this.loggedAdif$ = new Subject(); this.wsjtxId = 'WSJT-X'; this.setupBehaviors(); } setupBehaviors() { this.messages.rxMessage$.subscribe((msg) => this.handleMessage(msg)); // if we haven't heard from WSJT-X in 15 seconds, consider it "down" this.connected$ .pipe(debounceTime(15000)) .subscribe(() => this.connected$.next(false)); // When WSJT-X announces it's closing, set it to "down" immediately this.close$.subscribe(() => { this.connected$.next(false); }); // When WSJT-X goes down, clear its persistent message subjects this.connected$.subscribe((isUp) => { if (!isUp) { this.heartbeat$.next(null); this.status$.next(null); } }); } handleMessage(msg) { if (!msg.wsjtx || !msg.wsjtx.type) { return; } this.connected$.next(true); this.wsjtxId = msg.wsjtx.payload.id; switch (msg.wsjtx.type) { case 'HeartbeatMessage': this.heartbeat$.next(msg.wsjtx.payload); return; case 'StatusMessage': this.status$.next(msg.wsjtx.payload); return; case 'DecodeMessage': this.decode$.next(msg.wsjtx.payload); return; case 'ClearMessage': this.clear$.next(msg.wsjtx.payload); return; case 'QsoLoggedMessage': this.qsoLogged$.next(msg.wsjtx.payload); return; case 'CloseMessage': this.close$.next(msg.wsjtx.payload); return; case 'WSPRDecodeMessage': this.wsprDecode$.next(msg.wsjtx.payload); return; case 'LoggedAdifMessage': this.loggedAdif$.next(msg.wsjtx.payload); return; } } /** Send a command to WSJT-X to clear the Band Activity window. */ clearBandActivity() { const wsMsg = { wsjtx: { type: 'ClearMessage', payload: { id: this.wsjtxId, window: 0 }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to clear the Rx Frequency window. */ clearRxFreqWindow() { const wsMsg = { wsjtx: { type: 'ClearMessage', payload: { id: this.wsjtxId, window: 1 }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to clear the Band Activity and Rx Frequency windows. */ clearAll() { const wsMsg = { wsjtx: { type: 'ClearMessage', payload: { id: this.wsjtxId, window: 2 }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to replay messages. Useful for a fresh client that wants to hear * previous WSJT-X decodes. */ replay() { const wsMsg = { wsjtx: { type: 'ReplayMessage', payload: { id: this.wsjtxId }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to halt any transmissions immediately. */ haltTxNow() { const wsMsg = { wsjtx: { type: 'HaltTxMessage', payload: { id: this.wsjtxId, autoTxOnly: false }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to stop auto-transmitting after finishing the current round. */ haltTxAfterCurrent() { const wsMsg = { wsjtx: { type: 'HaltTxMessage', payload: { id: this.wsjtxId, autoTxOnly: true }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to reply to the given decode. The message must include CQ or QRZ. */ reply(decode) { const wsMsg = { wsjtx: { type: 'ReplyMessage', payload: { id: decode.id, time: decode.time, snr: decode.snr, deltaTime: decode.deltaTime, deltaFrequency: decode.deltaFrequency, mode: decode.mode, message: decode.message, lowConfidence: decode.lowConfidence, }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to reply to the given decode. The message must include CQ or QRZ. */ highlightCallsign(highlightMsg) { highlightMsg.id = this.wsjtxId; const wsMsg = { wsjtx: { type: 'HighlightCallsignMessage', payload: highlightMsg, }, }; this.messages.txMessage$.next(wsMsg); } /** * Send a command to WSJT-X to transmit the given free text. If the text is too long to be * encoded in a single message, it may be silently truncated. */ sendFreeText(freeText) { freeText.id = this.wsjtxId; const wsMsg = { wsjtx: { type: 'FreeTextMessage', payload: freeText, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to set the local station's Maidenhead grid. This is temporary, * lasting only as long as WSJT-X is running. */ setLocation(grid) { const wsMsg = { wsjtx: { type: 'LocationMessage', payload: { id: this.wsjtxId, location: grid, }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to switch to the named configuration. */ switchConfiguration(configName) { const wsMsg = { wsjtx: { type: 'SwitchConfigurationMessage', payload: { id: this.wsjtxId, configurationName: configName, }, }, }; this.messages.txMessage$.next(wsMsg); } /** Send a command to WSJT-X to set the given configuration parameters. */ configure(config) { config.id = this.wsjtxId; const wsMsg = { wsjtx: { type: 'ConfigureMessage', payload: config, }, }; this.messages.txMessage$.next(wsMsg); } /** Given a decode message, format a string the same way as displayed in the WSJT-X Band * Activity/Rx Frequency windows. */ static formatDecode(msg) { const timeStr = this.formatTime(msg.time); return `${timeStr} ${msg.snr.toString().padStart(3)} ${msg.deltaTime .toFixed(1) .padStart(4)} ${msg.deltaFrequency.toString().padStart(4)} ~ ${msg.message}`; } /** Given a time in milliseconds since midnight UTC, format as HHMMSS. */ static formatTime(time) { const secondsSinceMidnight = Math.floor(time / 1000); const hours = Math.floor(secondsSinceMidnight / 3600); const secondsSinceHour = secondsSinceMidnight - hours * 3600; const minutes = Math.floor(secondsSinceHour / 60); const seconds = secondsSinceHour - minutes * 60; return `${hours.toString().padStart(2, '0')}${minutes .toString() .padStart(2, '0')}${seconds.toString().padStart(2, '0')}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WsjtxService, deps: [{ token: AgentMessageService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WsjtxService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: WsjtxService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: AgentMessageService }] }); class HamlibService { constructor(messages) { this.messages = messages; /** Whether we're getting any messages from Hamlib. */ this.connected$ = new BehaviorSubject(false); /** Subject for listening to Hamlib "RigState" messages. */ this.rigState$ = new BehaviorSubject(null); this.setupBehaviors(); } setupBehaviors() { this.messages.rxMessage$.subscribe((msg) => this.handleMessage(msg)); // if we haven't heard from Hamlib in 15 seconds, consider it down this.connected$.pipe(debounceTime(15000)).subscribe(() => { this.connected$.next(false); }); // When Hamlib goes down, clear its persistent message subjects this.connected$.subscribe((isUp) => { if (!isUp) { this.rigState$.next(null); } }); } handleMessage(msg) { if (!msg.hamlib || !msg.hamlib.type) { return; } this.connected$.next(true); switch (msg.hamlib.type) { case 'RigState': this.rigState$.next(msg.hamlib.payload); return; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: HamlibService, deps: [{ token: AgentMessageService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: HamlibService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: HamlibService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: AgentMessageService }] }); class AgentService { constructor(messages, hamlibService, wsjtxService) { this.messages = messages; this.hamlibService = hamlibService; this.wsjtxService = wsjtxService; /** Whether we're connected to the agent. */ this.connectedState$ = new BehaviorSubject(false); this.defaultAgentHost = 'localhost'; this.defaultAgentPort = 8081; this.localStorageHostKey = 'agent-host'; this.localStoragePortKey = 'agent-port'; this.agentHost = this.defaultAgentHost; this.agentPort = this.defaultAgentPort; this.agentWebSocketSubject = null; this.agentWebsocketSubscription = null; this.hamlibState$ = this.hamlibService.connected$; this.hamlibRigState$ = this.hamlibService.rigState$; this.wsjtxState$ = this.wsjtxService.connected$; this.wsjtxHeartbeat$ = this.wsjtxService.heartbeat$; this.wsjtxStatus$ = this.wsjtxService.status$; this.wsjtxDecode$ = this.wsjtxService.decode$; this.wsjtxClear$ = this.wsjtxService.clear$; this.wsjtxQsoLogged$ = this.wsjtxService.qsoLogged$; this.wsjtxClose$ = this.wsjtxService.close$; this.wsjtxWsprDecode$ = this.wsjtxService.wsprDecode$; this.wsjtxLoggedAdif$ = this.wsjtxService.loggedAdif$; } init() { this.messages.txMessage$.subscribe((msg) => this.send(msg)); this.agentHost = this.getHost(); this.agentPort = this.getPort(); this.connect(); } /** Connect (or reconnect) the websocket to the kel-agent server. */ connect() { if (this.agentWebsocketSubscription) { this.agentWebsocketSubscription.unsubscribe(); } this.agentHost = this.getHost(); this.agentPort = this.getPort(); const protocol = this.agentHost === 'localhost' ? 'ws://' : 'wss://'; this.agentWebSocketSubject = webSocket({ url: protocol + this.agentHost + ':' + this.agentPort + '/websocket', }); this.connectedState$.next(true); this.agentWebsocketSubscription = this.agentWebSocketSubject .pipe(retryWhen((errors) => // retry the websocket connection after 10 seconds errors.pipe(tap(() => this.connectedState$.next(false)), delay(10000)))) .subscribe({ next: (msg) => { this.connectedState$.next(true); this.messages.rxMessage$.next(msg); }, error: () => this.connectedState$.next(false), complete: () => this.connectedState$.next(false), }); } /** Get the currently configured kel-agent host. */ getHost() { return (localStorage.getItem(this.localStorageHostKey) || this.defaultAgentHost); } /** Get the currently configured kel-agent port. */ getPort() { let portStr = localStorage.getItem(this.localStoragePortKey); if (portStr == null) { return this.defaultAgentPort; } let portNum = parseInt(portStr, 10); if (isNaN(portNum)) { return this.defaultAgentPort; } return portNum; } /** Set the kel-agent host. */ setHost(host) { localStorage.setItem(this.localStorageHostKey, host); this.connect(); } /** Set the kel-agent port. */ setPort(port) { localStorage.setItem(this.localStoragePortKey, String(port)); this.connect(); } send(wsMsg) { this.agentWebSocketSubject?.next(wsMsg); } /** * Send a command to WSJT-X to clear the Band Activity window. * * @deprecated Use {@link WsjtxService.clearBandActivity} instead. */ sendWsjtxClearBandActivity() { this.wsjtxService.clearBandActivity(); } /** * Send a command to WSJT-X to clear the Rx Frequency window. * * @deprecated Use {@link WsjtxService.clearRxFreqWindow} instead. */ sendWsjtxClearRxFreqWindow() { this.wsjtxService.clearRxFreqWindow(); } /** * Send a command to WSJT-X to clear the Band Activity and Rx Frequency windows. * * @deprecated Use {@link WsjtxService.clearAll} instead. */ sendWsjtxClearAll() { this.wsjtxService.clearAll(); } /** * Send a command to WSJT-X to replay messages. Useful for a fresh client that wants to hear * previous WSJT-X decodes. * * @deprecated Use {@link WsjtxService.replay} instead. */ sendWsjtxReplay() { this.wsjtxService.replay(); } /** * Send a command to WSJT-X to halt any transmissions immediately. * * @deprecated Use {@link WsjtxService.haltTxNow} instead. */ sendWsjtxHaltTxNow() { this.wsjtxService.haltTxNow(); } /** Send a command to WSJT-X to stop auto-transmitting after finishing the current round. * * @deprecated Use {@link WsjtxService.haltTxAfterCurrent} instead. */ sendWsjtxHaltTxAfterCurrent() { this.wsjtxService.haltTxAfterCurrent(); } /** * Send a command to WSJT-X to reply to the given decode. The message must include CQ or QRZ. * * @deprecated Use {@link WsjtxService.reply} instead. */ sendWsjtxReply(decode) { this.wsjtxService.reply(decode); } /** * Send a command to WSJT-X to reply to the given decode. The message must include CQ or QRZ. * * @deprecated Use {@link WsjtxService.highlightCallsign} instead. */ sendWsjtxHighlightCallsign(highlightMsg) { this.wsjtxService.highlightCallsign(highlightMsg); } /** * Given a decode message, format a string the same way as displayed in the WSJT-X Band * Activity/Rx Frequency windows. * * @deprecated Use {@link WsjtxService.formatDecode} instead. */ static formatDecode(msg) { return WsjtxService.formatDecode(msg); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AgentService, deps: [{ token: AgentMessageService }, { token: HamlibService }, { token: WsjtxService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AgentService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AgentService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: AgentMessageService }, { type: HamlibService }, { type: WsjtxService }] }); /* * Public API Surface of ngx-kel-agent */ // export * from './lib/ngx-kel-agent.component'; // export * from './lib/ngx-kel-agent.module'; /** * Generated bundle index. Do not edit. */ export { AgentMessageService, AgentService, HamlibService, WsjtxService }; //# sourceMappingURL=ngx-kel-agent.mjs.map