Skip to content

Commit

Permalink
Merge pull request #164 from the-orange-alliance/fgc-2024/fcs-updates
Browse files Browse the repository at this point in the history
Update FCS and WLED
  • Loading branch information
kyle-flynn authored Sep 26, 2024
2 parents 730e1e1 + 29ff993 commit de3d748
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 98 deletions.
237 changes: 142 additions & 95 deletions back-end/realtime/src/util/WLEDWorker/WLEDController.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,158 @@
import { WledInitParameters, WledUpdateParameters } from "@toa-lib/models";
import logger from "../Logger.js";
import { buildWledInitializationPacket, buildWledSetColorPacket } from "../WLEDHelper.js";
import WebSocket from "ws";
import { WledInitParameters, WledUpdateParameters } from '@toa-lib/models';
import logger from '../Logger.js';
import {
buildWledInitializationPacket,
buildWledSetColorPacket
} from '../WLEDHelper.js';
import WebSocket from 'ws';

export class WledController {
private static heartbeatPeriodMs = 1000;
private static keepAliveTimeoutMs = 2000;
private static reconnectPeriodMs = 1000;

private socket: WebSocket | undefined;
private initPacket: WledInitParameters;
private keepAlive: NodeJS.Timeout | undefined;
private heartbeat: NodeJS.Timer | undefined;
private reinit: NodeJS.Timeout | undefined;
private static heartbeatPeriodMs = 500;
private static keepAlivePeriodMs = 500;
private static reconnectPeriodMs = 1250;
private static keepAliveTimeoutMs = 1250;

private socket: WebSocket | null;
private initPacket: WledInitParameters;
private keepAlive: NodeJS.Timer | null;
private heartbeat: NodeJS.Timer | null;

private latestState: WledUpdateParameters | undefined;
private connected = false;

private lastTimestamp: number | null;

constructor(initPacket: WledInitParameters) {
this.initPacket = initPacket;
this.keepAlive = null;
this.heartbeat = null;
this.socket = null;
this.lastTimestamp = null;
}

public initialize(initPacket?: WledInitParameters): void {
if (initPacket) {
this.initPacket = initPacket;
if (this.initPacket.address === this.socket?.url && this.connected)
return;
}

private latestState: WledUpdateParameters | undefined;
private connected = false;
if (this.initPacket.address === '') return;

constructor(initPacket: WledInitParameters) {
this.initPacket = initPacket;
try {
this.socket = new WebSocket(this.initPacket.address);
} catch (e) {
logger.error(`${this.getName()} failed to create websocket: ${e}`);
return;
}

public initialize(initPacket?: WledInitParameters): void {
if (initPacket) {
this.initPacket = initPacket;
if (this.initPacket.address === this.socket?.url && this.connected) return;
}
this.socket.onopen = (e: WebSocket.Event) => {
if (!this.socket) return;
if (this.socket.readyState === 0) return;
this.connected = true;
logger.info(`${this.getName()} === connected ===`);
try {
this.socket?.send(buildWledInitializationPacket(this.initPacket));
} catch (e) {
logger.error(`${this.getName()} failed to initialize: ${e}`);
}

this.startHeartbeat();
this.startKeepalive();

if (this.latestState) {
this.update(this.latestState);
}
};

this.socket.onclose = () => {
logger.error(`${this.getName()} disconnected`);
this.connected = false;
// If the keepalive loop is running, clear it
if (this.keepAlive) {
clearInterval(this.keepAlive);
this.keepAlive = null;
}
// Attempt to reconnect once the socket has closed
setTimeout(() => this.initialize(), WledController.reconnectPeriodMs);
}

this.socket.onerror = (e: WebSocket.ErrorEvent) => {
logger.error(`${this.getName()} failed to connect: ${e.error}`);
};

this.socket.onmessage = () => {
this.lastTimestamp = Date.now();
};
}

private startHeartbeat(): void {
logger.info(`${this.getName()} starting heartbeat`);
this.heartbeat = setInterval(() => {
// Send dummy message that the controller will respond to
try {
this.socket?.send('{}');
} catch {
logger.warn(`${this.getName()} failed to send heartbeat`);
}
if (!this.connected && this.heartbeat) {
logger.info(`${this.getName()} clearing heartbeat`);
clearInterval(this.heartbeat);
clearTimeout(this.reinit);
clearTimeout(this.keepAlive);

if (this.initPacket.address === '') return;

try {
this.socket = new WebSocket(this.initPacket.address);
} catch (e) {
logger.error(`Failed to create websocket for ${this.initPacket.address}: ${e}`);
return;
this.heartbeat = null;
}
}, WledController.heartbeatPeriodMs);
}

private startKeepalive(): void {
logger.info(`${this.getName()} starting keepalive`);
this.keepAlive = setInterval(() => {
if (!this.connected) return
if (!this.lastTimestamp) return;
if (
Date.now() - this.lastTimestamp >=
WledController.keepAliveTimeoutMs
) {
logger.error(`${this.getName()} keepalive timeout`);
this.connected = false;
this.socket?.terminate();

if (this.keepAlive) {
logger.info(`${this.getName()} clearing keepalive`);
clearInterval(this.keepAlive);
this.keepAlive = null;
}

this.socket.onopen = () => {
this.connected = true;
logger.info(`Connected to ${this.initPacket.address}`);
try {
this.socket?.send(buildWledInitializationPacket(this.initPacket));
} catch (e) {
logger.error(`Failed to initialize ${this.initPacket.address}: ${e}`);
}
this.startHeartbeat();

if (this.latestState) {
this.update(this.latestState);
}
};

this.socket.onerror = (e) => {
logger.error(`Failed to connect to ${this.initPacket.address}: ${e}`);

// Attempt to reconnect
this.reinit = setTimeout(() => {
this.initialize();
}, WledController.reconnectPeriodMs);
};

this.socket.onmessage = () => {
clearTimeout(this.keepAlive);
};
setTimeout(() => {
logger.info(`${this.getName()} attempting to reinitialize`);
this.initialize();
}, WledController.reconnectPeriodMs);
}
}, WledController.keepAlivePeriodMs);
}

public update(update: WledUpdateParameters): void {
try {
this.socket?.send(buildWledSetColorPacket(update));
} catch {
logger.error(`${this.getName()} failed to send pattern update`);
}

private startHeartbeat(): void {
this.heartbeat = setInterval(() => {
// Send dummy message that the controller will respond to
try {
this.socket?.send('{}');
} catch {
logger.error(`Failed to send heartbeat to ${this.initPacket.address}`);
}

// Start keepalive
this.keepAlive = setTimeout(() => {
this.connected = false;
logger.info(`Disconnected from ${this.initPacket.address}`);

// If the keepalive is not cleared in time attempt to reinitialize
this.initialize();
}, WledController.keepAliveTimeoutMs);
}, WledController.heartbeatPeriodMs);
if (!this.latestState) {
this.latestState = update;
}
for (const newPattern of update.patterns) {
this.latestState.patterns =
this.latestState.patterns.filter(
(oldPattern) => oldPattern.segment != newPattern.segment
) ?? [];

public update(update: WledUpdateParameters): void {
try {
this.socket?.send(buildWledSetColorPacket(update));
} catch {
logger.error(`Failed to send pattern to ${this.initPacket.address}`);
}

if (!this.latestState) {
this.latestState = update;
}
for (const newPattern of update.patterns) {
this.latestState.patterns =
this.latestState.patterns.filter(
(oldPattern) => oldPattern.segment != newPattern.segment
) ?? [];

this.latestState.patterns!.push(newPattern);
}
this.latestState.patterns!.push(newPattern);
}
}

private getName(): string {
const name = this.initPacket.address.replace(/(ws:\/\/)|(\/ws)/g, '');
const parts = name.split('-');
return name.replace(parts[0] + '-', '');
}
}
13 changes: 10 additions & 3 deletions lib/models/src/fcs/FeedingTheFutureFCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,12 @@ export class PacketManager {
private broadcastCallback: (update: FieldControlUpdatePacket) => void;
private matchEmitter: EventEmitter;
private actionQueue = new Map<string, Action>();
private matchState: 'prestart' | 'in progress' | 'ended' | 'aborted' =
'prestart';
private matchState:
| 'prestart'
| 'in progress'
| 'ended'
| 'aborted'
| 'all clear' = 'prestart';

private previousBalanced = true;

Expand Down Expand Up @@ -335,6 +339,7 @@ export class PacketManager {
};

public handleAllClear = (): void => {
this.matchState = 'all clear';
const result: FieldControlUpdatePacket = { hubs: {}, wleds: {} };
applyPatternToStrips(
this.fieldOptions.allClearColor,
Expand Down Expand Up @@ -639,7 +644,7 @@ export class PacketManager {
} satisfies ItemUpdate);
}

if (this.matchState !== 'prestart' && this.matchState !== 'aborted') {
if (this.matchState === 'in progress' || this.matchState === 'ended') {
const result: FieldControlUpdatePacket = { hubs: {}, wleds: {} };
applyPatternToStrips(
balanced
Expand All @@ -652,6 +657,8 @@ export class PacketManager {
}
}
});

this.previousBalanced = balanced;
};

runFoodProductionSequence = (
Expand Down

0 comments on commit de3d748

Please sign in to comment.