Skip to content

Commit

Permalink
Merge pull request #30 from neilenns/neilenns/issue28
Browse files Browse the repository at this point in the history
Add consistent code comments everywhere
  • Loading branch information
neilenns authored May 15, 2024
2 parents 5ca910b + 4a24aa5 commit f0e7f0e
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 32 deletions.
12 changes: 12 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
32 changes: 22 additions & 10 deletions src/actionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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();
Expand All @@ -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
*/
Expand All @@ -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
*/
Expand All @@ -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
*/
Expand Down Expand Up @@ -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[] {
Expand All @@ -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) =>
Expand Down
9 changes: 9 additions & 0 deletions src/actions/station-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StationSettings> {
// 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<StationSettings>): void | Promise<void> {
ActionManager.getInstance().addStation(ev.action, {
callsign: ev.payload.settings.callsign,
Expand All @@ -34,12 +40,15 @@ export class StationStatus extends SingletonAction<StationSettings> {
});
}

// When the action is removed from a profile it also gets removed from the ActionManager.
onWillDisappear(
ev: WillDisappearEvent<StationSettings>
): void | Promise<void> {
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<StationSettings>
): void | Promise<void> {
Expand Down
9 changes: 8 additions & 1 deletion src/actions/trackAudio-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ 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<TrackAudioStatusSettings> {
// When the action is added to a profile it gets saved in the ActionManager
// instance for use elsewhere in the code.
onWillAppear(
ev: WillAppearEvent<TrackAudioStatusSettings>
): void | Promise<void> {
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<TrackAudioStatusSettings>
): void | Promise<void> {
ActionManager.getInstance().remove(ev.action);
}
}

// Currently no settings are needed for this action
type TrackAudioStatusSettings = Record<string, never>;
8 changes: 8 additions & 0 deletions src/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
25 changes: 19 additions & 6 deletions src/stationStatusAction.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
34 changes: 21 additions & 13 deletions src/trackAudioManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -32,23 +35,23 @@ 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) {
this.url = url;
}

/**
* 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 {
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
}

/**
* 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 {
Expand All @@ -67,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();
});

Expand All @@ -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);

Expand All @@ -101,20 +109,20 @@ 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);
}
}

/**
* 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
Expand All @@ -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) {
Expand Down
11 changes: 10 additions & 1 deletion src/trackAudioStatusAction.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
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) {
this.action = action;
}
}

/**
* Typeguard for TrackAudioStatusAction.
* @param action The action
* @returns True if the action is a TrackAudioStatusAction
*/
export function isTrackAudioStatusAction(
action: StatusAction
): action is TrackAudioStatusAction {
Expand Down
Loading

0 comments on commit f0e7f0e

Please sign in to comment.