From dda2c6f5e2316bf67d8a1dea7712ec6c8136a93b Mon Sep 17 00:00:00 2001 From: Neil Enns Date: Wed, 15 May 2024 10:21:08 -0700 Subject: [PATCH 1/2] Make sure code comments are good Fixes #28 --- DEVELOPMENT.md | 12 +++++++++ src/actionManager.ts | 32 ++++++++++++++++-------- src/actions/station-status.ts | 9 +++++++ src/actions/trackAudio-status.ts | 9 ++++++- src/helpers/helpers.ts | 8 ++++++ src/plugin.ts | 2 +- src/stationStatusAction.ts | 25 ++++++++++++++----- src/trackAudioManager.ts | 20 ++++++++++----- src/trackAudioStatusAction.ts | 11 +++++++- src/types/messages.ts | 43 ++++++++++++++++++++++++++++++++ 10 files changed, 146 insertions(+), 25 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d700432..7ef7b13 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -17,3 +17,15 @@ Run `npm run package`. Make sure `npm run build` was done first. VSCode can debug using the `Attach to Plugin` profile. Select the appropriate node.js instance. Yes, there should be a better, more automatic, way to do this. I'll figure it out someday. + +## About the code + +This code my first attempt at writing a StreamDeck plugin using the (as of this writing) [beta node.js SDK](https://github.com/elgatosf/streamdeck) +in conjunction with websockets to read and display data from another app. Here are some bits and pieces that might be interesting: + +* `src/actionManager.ts` is a singleton class that keeps track of the plugin's actions as they are added to a StreamDeck profile. It exposes methods that are called to set the state, image, or display text in response to websocket messages. +* `src/trackAudioManager.ts` is a singleton class that manages the websocket connection with TrackAudio. It listens to various messages from TrackAudio then fires its own events that are handled by the plugin to update the buttons. The connection to TrackAudio is only opened if the profile has at least one button from this plugin in it, and it disconnects from TrackAudio if all plugin buttons are removed. +* `src/plugin.ts` has all the event handlers for events fired by the `TrackAudioManager`. It processes those events and then calls the appropriate methods on the `ActionManager` to update the buttons. +* `eslint` is used with strict TypeScript rules to validate the code +* `markdownlint` is used to validate the markdown files +* Automated CI/CD builds are handled with GitHub workflows in the `.github/workflows` folder. This includes automatically setting the plugin version to the GitHub release version and attaching the built plugin package to the pull request and release page. diff --git a/src/actionManager.ts b/src/actionManager.ts index 8a89ee7..d67317b 100644 --- a/src/actionManager.ts +++ b/src/actionManager.ts @@ -10,8 +10,14 @@ import { isTrackAudioStatusAction, } from "./trackAudioStatusAction"; +/** + * Type union for all possible actions supported by this plugin + */ export type StatusAction = StationStatusAction | TrackAudioStatusAction; +/** + * Singleton class that manages StreamDeck actions + */ export default class ActionManager extends EventEmitter { private static instance: ActionManager | null = null; private actions: StatusAction[] = []; @@ -20,6 +26,10 @@ export default class ActionManager extends EventEmitter { super(); } + /** + * Provides access to the ActionManager instance. + * @returns The instance of ActionManager + */ public static getInstance(): ActionManager { if (!ActionManager.instance) { ActionManager.instance = new ActionManager(); @@ -28,17 +38,19 @@ export default class ActionManager extends EventEmitter { } /** - * Adds a VectorAudio status action to the action list. - * @param action The action to add. + * Adds a TrackAudio status action to the action list. Emits a trackAudioStatusAdded event + * after the action is added. + * @param action The action to add */ - public addVectorAudio(action: Action) { + public addTrackAudio(action: Action) { this.actions.push(new TrackAudioStatusAction(action)); - this.emit("vectorAudioStatusAdded", this.actions.length); + this.emit("trackAudioStatusAdded", this.actions.length); } /** - * Adds a station status action to the list with the associated callsign. + * Adds a station status action to the list with the associated callsign. Emits a stationStatusAdded + * event after the action is added. * @param callsign The callsign associated with the action * @param action The action */ @@ -52,7 +64,7 @@ export default class ActionManager extends EventEmitter { } /** - * Updates the settings associated with a station status action + * Updates the settings associated with a station status action. * @param action The action to update * @param settings The new settings to use */ @@ -69,7 +81,7 @@ export default class ActionManager extends EventEmitter { } /** - * Updates the frequency on the first station status action that matches the callsign + * Updates the frequency on the first station status action that matches the callsign. * @param callsign The callsign of the station to update the frequency on * @param frequency The frequency to update to */ @@ -257,7 +269,7 @@ export default class ActionManager extends EventEmitter { } /** - * Retrieves the list of all tracked StationStatusActions + * Retrieves the list of all tracked StationStatusActions. * @returns An array of StationStatusActions */ public getStationStatusActions(): StationStatusAction[] { @@ -267,8 +279,8 @@ export default class ActionManager extends EventEmitter { } /** - * Retrieves the list of all tracked StationStatusActions - * @returns An array of StationStatusActions + * Retrieves the list of all tracked TrackAudioStatusActions. + * @returns An array of TrackAudioStatusActions */ public getTrackAudioStatusActions(): TrackAudioStatusAction[] { return this.actions.filter((action) => diff --git a/src/actions/station-status.ts b/src/actions/station-status.ts index 23c8657..18f44cc 100644 --- a/src/actions/station-status.ts +++ b/src/actions/station-status.ts @@ -10,7 +10,13 @@ import { getDisplayTitle } from "../helpers/helpers"; import { ListenTo } from "../stationStatusAction"; @action({ UUID: "com.neil-enns.trackaudio.stationstatus" }) +/** + * Represents the status of a TrackAudio station + */ export class StationStatus extends SingletonAction { + // When the action is added to a profile it gets saved in the ActionManager + // instance for use elsewhere in the code. The default title is also set + // to something useful. onWillAppear(ev: WillAppearEvent): void | Promise { ActionManager.getInstance().addStation(ev.action, { callsign: ev.payload.settings.callsign, @@ -34,12 +40,15 @@ export class StationStatus extends SingletonAction { }); } + // When the action is removed from a profile it also gets removed from the ActionManager. onWillDisappear( ev: WillDisappearEvent ): void | Promise { ActionManager.getInstance().remove(ev.action); } + // When settings are received the ActionManager is called to update the existing + // settings on the saved action. onDidReceiveSettings( ev: DidReceiveSettingsEvent ): void | Promise { diff --git a/src/actions/trackAudio-status.ts b/src/actions/trackAudio-status.ts index fc3ac40..dda43e7 100644 --- a/src/actions/trackAudio-status.ts +++ b/src/actions/trackAudio-status.ts @@ -7,14 +7,20 @@ import { import ActionManager from "../actionManager"; @action({ UUID: "com.neil-enns.trackaudio.trackaudiostatus" }) +/** + * Represents the status of the websocket connection to TrackAudio + */ export class TrackAudioStatus extends SingletonAction { + // When the action is added to a profile it gets saved in the ActionManager + // instance for use elsewhere in the code. onWillAppear( ev: WillAppearEvent ): void | Promise { console.log("Hello"); - ActionManager.getInstance().addVectorAudio(ev.action); + ActionManager.getInstance().addTrackAudio(ev.action); } + // When the action is removed from a profile it also gets removed from the ActionManager. onWillDisappear( ev: WillDisappearEvent ): void | Promise { @@ -22,4 +28,5 @@ export class TrackAudioStatus extends SingletonAction } } +// Currently no settings are needed for this action type TrackAudioStatusSettings = Record; diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index 5eafb8e..2e56499 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -1,5 +1,13 @@ import { ListenTo } from "../stationStatusAction"; +/** + * Takes a callsign and listenTo and converts it to a display title. If the callsign + * isn't provided then a default of "Not set" is used. If the ListenTo property is provided + * then it is appended to the callsign on a new line. + * @param callsign The callsign + * @param listenTo The ListenTo setting + * @returns The display title + */ export function getDisplayTitle( callsign: string | null, listenTo: ListenTo | "" | null diff --git a/src/plugin.ts b/src/plugin.ts index a2ef41b..2b48dae 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -132,7 +132,7 @@ actionManager.on("stationStatusAdded", (count: number) => { updateStationStatusButtons(); }); -actionManager.on("vectorAudioStatusAdded", (count: number) => { +actionManager.on("trackAudioStatusAdded", (count: number) => { if (count === 1) { trackAudio.connect(); } diff --git a/src/stationStatusAction.ts b/src/stationStatusAction.ts index 842190b..33ac3b5 100644 --- a/src/stationStatusAction.ts +++ b/src/stationStatusAction.ts @@ -1,8 +1,16 @@ import { Action } from "@elgato/streamdeck"; import { StatusAction } from "./actionManager"; +// Valid values for the ListenTo property. This must match +// the list of array property names that come from TrackAudio +// in the kFrequenciesUpdate message. export type ListenTo = "rx" | "tx" | "xc"; +/** + * Settings for the StationStatusAction. This tracks the values that + * were provided from the Property Inspector so they are available for + * use outside of StreamDeck events. + */ export class StationStatusActionSettings { callsign = ""; listenTo: ListenTo = "rx"; @@ -13,6 +21,10 @@ export class StationStatusActionSettings { activeCommsIconPath: string | undefined; } +/** + * A StationStatus action, for use with ActionManager. Tracks the settings, + * state and StreamDeck action for an individual action in a profile. + */ export class StationStatusAction { type = "StationStatusAction"; action: Action; @@ -24,13 +36,9 @@ export class StationStatusAction { settings: StationStatusActionSettings = new StationStatusActionSettings(); /** - * + * Creates a new StationStatusAction object. * @param callsign The callsign for the action - * @param listenTo The type of listening requested, either rx, tx, or xc - * @param notListeningIconPath The path to the icon file for the not listening state, or undefined to use the default - * @param listeningIconPath The path to the icon file for the listening state, or undefined to use the default - * @param activeCommsIconPath The path to the icon file for the active comms state, or undefined to use the default - * @param action The StreamDeck action object + * @param options: The options for the action */ constructor(action: Action, options: StationStatusActionSettings) { this.action = action; @@ -42,6 +50,11 @@ export class StationStatusAction { } } +/** + * Typeguard for StationStatusAction. + * @param action The action + * @returns True if the action is a StationStatusAction + */ export function isStationStatusAction( action: StatusAction ): action is StationStatusAction { diff --git a/src/trackAudioManager.ts b/src/trackAudioManager.ts index 250a0a5..410435e 100644 --- a/src/trackAudioManager.ts +++ b/src/trackAudioManager.ts @@ -9,6 +9,9 @@ import { isTxEnd, } from "./types/messages"; +/** + * Manages the websocket connection to TrackAudio. + */ export default class TrackAudioManager extends EventEmitter { private static instance: TrackAudioManager | null; private socket: WebSocket | null = null; @@ -21,7 +24,7 @@ export default class TrackAudioManager extends EventEmitter { } /** - * Provides access to the TrackAudio websocket connection + * Provides access to the TrackAudio websocket connection. * @returns The websocket instance */ public static getInstance(): TrackAudioManager { @@ -32,7 +35,7 @@ export default class TrackAudioManager extends EventEmitter { } /** - * Sets the connection URL for TrackAudio + * Sets the connection URL for TrackAudio. * @param url The URL for the TrackAudio instance */ public setUrl(url: string) { @@ -40,7 +43,7 @@ export default class TrackAudioManager extends EventEmitter { } /** - * Provides the current state of the connection to TrackAudio + * Provides the current state of the connection to TrackAudio. * @returns True if there is an open connection to TrackAudio, false otherwise. */ public isConnected(): boolean { @@ -48,7 +51,7 @@ export default class TrackAudioManager extends EventEmitter { } /** - * Connects to a TrackAudio instance + * Connects to a TrackAudio instance and registers event handlers for various socket events. * @param url The URL of the TrackAudio instance to connect to, typically ws://localhost:49080/ws */ public connect(): void { @@ -93,6 +96,11 @@ export default class TrackAudioManager extends EventEmitter { }); } + /** + * Takes an incoming websocket message from TrackAudio, determines the type, and then + * fires the appropriate event. + * @param message The message to process + */ private processMessage(message: string): void { console.log("received: %s", message); @@ -114,7 +122,7 @@ export default class TrackAudioManager extends EventEmitter { } /** - * Sets up a timer to attempt to reconnect to the websocket + * Sets up a timer to attempt to reconnect to the websocket. */ private reconnect(): void { // Check to see if a reconnect attempt is already in progress. If so @@ -130,7 +138,7 @@ export default class TrackAudioManager extends EventEmitter { } /** - * Disconnects from a TrackAudio instance + * Disconnects from a TrackAudio instance. */ public disconnect(): void { if (this.socket) { diff --git a/src/trackAudioStatusAction.ts b/src/trackAudioStatusAction.ts index fbc121e..60cf0b6 100644 --- a/src/trackAudioStatusAction.ts +++ b/src/trackAudioStatusAction.ts @@ -1,13 +1,17 @@ import { Action } from "@elgato/streamdeck"; import { StatusAction } from "./actionManager"; +/** + * A TrackAudioStatusAction action, for use with ActionManager. Tracks the + * state and StreamDeck action for an individual action in a profile. + */ export class TrackAudioStatusAction { type = "trackAudioStatusAction"; action: Action; isConnected = false; /** - * Creates a new TrackAudioStatusAction + * Creates a new TrackAudioStatusAction. * @param action The StreamDeck action object */ constructor(action: Action) { @@ -15,6 +19,11 @@ export class TrackAudioStatusAction { } } +/** + * Typeguard for TrackAudioStatusAction. + * @param action The action + * @returns True if the action is a TrackAudioStatusAction + */ export function isTrackAudioStatusAction( action: StatusAction ): action is TrackAudioStatusAction { diff --git a/src/types/messages.ts b/src/types/messages.ts index ea6f645..5c79d51 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -1,3 +1,6 @@ +/** + * Represents the kFrequenciesUpdate message from TrackAudio. + */ export interface FrequenciesUpdate { type: "kFrequenciesUpdate"; value: { @@ -16,6 +19,9 @@ export interface FrequenciesUpdate { }; } +/** + * Represents the kTxBegin message from TrackAudio. + */ export interface TxBegin { type: "kTxBegin"; value: { @@ -24,6 +30,9 @@ export interface TxBegin { }; } +/** + * Represents the kTxEnd message from TrackAudio. + */ export interface TxEnd { type: "kTxEnd"; value: { @@ -32,6 +41,9 @@ export interface TxEnd { }; } +/** + * Represents the kRxBegin message from TrackAudio. + */ export interface RxBegin { type: "kRxBegin"; value: { @@ -40,6 +52,9 @@ export interface RxBegin { }; } +/** + * Represents the kRxEnd message from TrackAudio. + */ export interface RxEnd { type: "kRxEnd"; value: { @@ -48,26 +63,54 @@ export interface RxEnd { }; } +/** + * Type union for all possible websocket messages from TrackAudio + */ export type Message = FrequenciesUpdate | RxBegin | RxEnd | TxBegin | TxEnd; +/** + * Typeguard for FrequencyStatusUpdate. + * @param message The message + * @returns True if the message is a FrequencyStatusUpdate + */ export function isFrequencyStateUpdate( message: Message ): message is FrequenciesUpdate { return message.type === "kFrequenciesUpdate"; } +/** + * Typeguard for RxBegin. + * @param message The message + * @returns True if the message is a RxBegin + */ export function isRxBegin(message: Message): message is RxBegin { return message.type === "kRxBegin"; } +/** + * Typeguard for RxEnd. + * @param message The message + * @returns True if the message is a RxEnd + */ export function isRxEnd(message: Message): message is RxEnd { return message.type === "kRxEnd"; } +/** + * Typeguard for TxBegin. + * @param message The message + * @returns True if the message is a TxBegin + */ export function isTxBegin(message: Message): message is TxBegin { return message.type === "kTxBegin"; } +/** + * Typeguard for TxEnd. + * @param message The message + * @returns True if the message is a TxEnd + */ export function isTxEnd(message: Message): message is TxEnd { return message.type === "kTxEnd"; } From 4a24aa52e29dd7ab85a499b6ae01531521186c05 Mon Sep 17 00:00:00 2001 From: Neil Enns Date: Wed, 15 May 2024 10:22:38 -0700 Subject: [PATCH 2/2] Code cleanup --- src/trackAudioManager.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/trackAudioManager.ts b/src/trackAudioManager.ts index 410435e..366ad9d 100644 --- a/src/trackAudioManager.ts +++ b/src/trackAudioManager.ts @@ -70,13 +70,13 @@ export default class TrackAudioManager extends EventEmitter { this.socket.on("open", () => { console.log("WebSocket connection established."); - TrackAudioManager.instance?.emit("connected"); + this.emit("connected"); }); this.socket.on("close", () => { console.log("WebSocket connection closed"); - TrackAudioManager.instance?.emit("disconnected"); + this.emit("disconnected"); this.reconnect(); }); @@ -109,15 +109,15 @@ export default class TrackAudioManager extends EventEmitter { // Check if the received message is of the desired event type if (isFrequencyStateUpdate(data)) { - TrackAudioManager.instance?.emit("frequencyUpdate", data); + this.emit("frequencyUpdate", data); } else if (isRxBegin(data)) { - TrackAudioManager.instance?.emit("rxBegin", data); + this.emit("rxBegin", data); } else if (isRxEnd(data)) { - TrackAudioManager.instance?.emit("rxEnd", data); + this.emit("rxEnd", data); } else if (isTxBegin(data)) { - TrackAudioManager.instance?.emit("txBegin", data); + this.emit("txBegin", data); } else if (isTxEnd(data)) { - TrackAudioManager.instance?.emit("txEnd", data); + this.emit("txEnd", data); } }