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
JavaScript
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