Files
NTSH-Control/src/Unity/UnityWebSocket.ts

456 lines
10 KiB
TypeScript

import { RawData, WebSocket } from 'ws';
import { Main } from '../Main';
import { delay, ServiceState } from '../Utils';
import { State, StatusType } from '../Status';
const PREFIX = '[Unity]';
export class UnityWebSocket {
private _Main: Main;
state: ServiceState = 'DISCONNECTED';
message?: string = 'Waiting for process...';
error?: string;
errorTriggerStartupDelay = 1000;
parameters: UnityParameters = {
timelineWatching: false,
timelineStanding: false,
timelineProgress: 0,
zedPath: '',
zedReady: false,
zedFPS: '-',
outOfService: null,
sliders: [],
advancedSliders: [],
sensors: [],
};
socket: WebSocket;
constructor(Main: Main) {
this._Main = Main;
this.updateStatus();
}
handle(command: string, ...args: any[]) {
switch (command) {
case 'parameterValue':
const sliderIndex: number = args[0];
const percentage: number = args[1];
this.setSliderValue(sliderIndex, percentage);
break;
case 'advancedParameterValue':
const advSliderIndex: number = args[0];
const advPercentage: number = args[1];
this.setAdvancedSliderValue(advSliderIndex, advPercentage);
break;
case 'enableOutOfService':
const enableCallback: Function = args[0];
if (typeof enableCallback !== 'function') return;
this.setOutOfService(true);
enableCallback({ succeed: true });
break;
case 'disableOutOfService':
const disableCallback: Function = args[0];
if (typeof disableCallback !== 'function') return;
this.setOutOfService(false);
disableCallback({ succeed: true });
break;
}
}
quitApplication() {
if (this.socket == null || this.socket.readyState !== WebSocket.OPEN)
return;
this.socket.send(
JSON.stringify({
type: 'quit_application',
}),
);
}
setSliderValue(sliderIndex: number, sliderValue: number) {
if (this.socket == null || this.socket.readyState !== WebSocket.OPEN)
return;
this.socket.send(
JSON.stringify({
type: 'set_slider_value',
sliderIndex,
sliderValue,
}),
);
if (this.parameters.sliders[sliderIndex] == undefined) return;
this.parameters.sliders[sliderIndex].outputValue = sliderValue;
this.broadcastState();
}
setAdvancedSliderValue(sliderIndex: number, sliderValue: number) {
if (this.socket == null || this.socket.readyState !== WebSocket.OPEN)
return;
this.socket.send(
JSON.stringify({
type: 'set_advanced_slider_value',
sliderIndex,
sliderValue,
}),
);
if (this.parameters.advancedSliders[sliderIndex] == undefined) return;
this.parameters.advancedSliders[sliderIndex].outputValue = sliderValue;
this.broadcastState();
}
setOutOfService(state: boolean) {
if (this.socket == null || this.socket.readyState !== WebSocket.OPEN)
return;
this.socket.send(
JSON.stringify({
type: 'set_out_of_service',
showOutOfService: state,
}),
);
this.parameters.outOfService = true;
this.broadcastState();
}
broadcastState() {
this._Main.WebServer.socket.emit(
'unityWebSocketState',
this.getState(),
);
}
updateStatus() {
if (this.state != 'CONNECTED') {
this._Main.Status.update(
StatusType.CameraUnityStream,
CameraUnityStateColors[this.state],
this.message,
{
reboot: this.state === 'FAILED',
},
);
} else {
const status = !this.parameters.zedReady
? 'Waiting for ZED stream'
: this.message;
this._Main.Status.update(
StatusType.CameraUnityStream,
this.parameters.zedReady ? State.Green : State.Yellow,
status,
{
reboot: this.parameters.zedReady,
},
);
}
}
setInfo(message: string, error: string, state: ServiceState = 'FAILED') {
this.message = message;
this.error = error;
this.state = state;
this.updateStatus();
this.broadcastState();
if (error != null) this._Main.Twilio.sendError('UnityWebSocket', error);
if (state == 'FAILED' || state == 'DISCONNECTED')
console.warn(PREFIX, message ?? error);
else console.log(PREFIX, message ?? error);
}
stopFetchClocks() {
clearInterval(this.heartbeatClock);
clearInterval(this.calibrationImageClock);
}
handleSocketMessage(data: RawData) {
let message: UnitySocketMessage;
try {
message = JSON.parse(data.toString());
} catch (error) {
return;
}
switch (message.type) {
case 'heartbeat_data':
this.parameters.timelineWatching =
message.heartbeat.dataTimeline.isWatching;
this.parameters.timelineStanding =
message.heartbeat.dataTimeline.isStanding;
this.parameters.timelineProgress =
message.heartbeat.dataTimeline.timelineProgress;
this.parameters.zedPath = `${message.heartbeat.zedCamera.streamInputIP}:${message.heartbeat.zedCamera.streamInputPort}`;
this.parameters.zedReady =
message.heartbeat.zedCamera.isZedReady;
this.parameters.zedFPS = message.heartbeat.zedCamera.cameraFPS;
this.parameters.outOfService =
message.heartbeat.showOutOfService ?? false;
this.parameters.sensors = message.heartbeat.dataSensors;
this.parameters.sliders = message.heartbeat.dataSliders.map(
(slider) => {
return {
...slider,
decimalPlaces:
(slider.min == 0 || slider.min == -1) &&
slider.max == 1
? 2
: null,
};
},
);
this.parameters.advancedSliders =
message.heartbeat.dataAdvancedSliders.map((slider) => {
return {
...slider,
decimalPlaces:
(slider.min == 0 || slider.min == -1) &&
slider.max == 1
? 2
: null,
};
});
this.updateStatus();
this.broadcastState();
break;
case 'response_camera_frame':
this._Main.WebServer.Calibration.writeCalibrationImage(
message.imageBase64,
);
break;
}
}
disconnected: boolean = false;
async disconnect() {
this.restartRequested = true;
this.disconnected = true;
if (this.socket != null) {
this.socket.close();
this.socket = null;
}
this.stopFetchClocks();
this.setInfo('Waiting for process...', null, 'DISCONNECTED');
}
private restartRequested = false;
async reconnect() {
if (this.restartRequested) return;
if (this.disconnected) return;
this.restartRequested = true;
this.stopFetchClocks();
if (this.socket != null) {
this.socket.close();
this.socket = null;
}
await delay(2000);
if (this.disconnected) return;
this.message = `Reconnecting in 10 seconds...`;
this.broadcastState();
await delay(10000);
if (this.disconnected) return;
await this.connect();
}
async connect() {
this.restartRequested = false;
this.disconnected = false;
this.stopFetchClocks();
this.setInfo('Connecting...', null, 'CONNECTING');
await delay(1000);
this.socket = new WebSocket(
`ws://${this._Main.Config.unity.webSocket.ip}:${this._Main.Config.unity.webSocket.port}`,
);
this.socket.on('error', (error) => {
if (this.restartRequested) return;
this.setInfo(
'Connection error',
`Could not connect: ${error.message}`,
'FAILED',
);
this.reconnect();
});
this.socket.on('open', () => {
this.startFetchClocks();
this._Main.Twilio.resetError('UnityWebSocket');
this.setInfo('Connected', null, 'CONNECTED');
});
this.socket.on('close', () => {
if (this._Main.UnityRunner.restartTriggered) return;
if (this.restartRequested) return;
this.setInfo(
'Disconnected',
'Unity was disconnected unexpectedly',
'FAILED',
);
this.reconnect();
});
this.socket.on('message', (data) => this.handleSocketMessage(data));
}
private heartbeatClock: NodeJS.Timeout;
private calibrationImageClock: NodeJS.Timeout;
startFetchClocks() {
this.socket.send(
JSON.stringify({
type: 'set_heartbeat_auto_send',
autoSend: false,
}),
);
this.heartbeatClock = setInterval(() => {
if (
this.socket == null ||
this.socket.readyState !== WebSocket.OPEN
)
return;
this.socket.send(JSON.stringify({ type: 'request_heartbeat' }));
}, this._Main.Config.unity.heartbeatInterval);
this.calibrationImageClock = setInterval(() => {
if (
this.socket == null ||
this.socket.readyState !== WebSocket.OPEN
)
return;
this.socket.send(JSON.stringify({ type: 'request_camera_frame' }));
}, this._Main.Config.unity.calibrationImageInterval);
}
getState(): UnityWebSocketStatus {
return {
state: this.state,
message: this.message,
error: this.error,
parameters: this.parameters,
};
}
}
interface UnityWebSocketStatus {
state: ServiceState;
message?: string;
error?: string;
parameters: UnityParameters;
}
interface UnityParameters {
timelineWatching: boolean;
timelineStanding: boolean;
timelineProgress: number;
zedPath: string;
zedReady: boolean;
zedFPS: string;
outOfService: boolean;
sliders: UnityParameterSlider[];
advancedSliders: UnityParameterSlider[];
sensors: UnitySocketMessageHeartbeat['heartbeat']['dataSensors'];
}
type UnityHeartbeatSlider =
UnitySocketMessageHeartbeat['heartbeat']['dataSliders'][number];
interface UnityParameterSlider extends UnityHeartbeatSlider {
visualMultiplier?: number;
decimalPlaces?: number;
}
type UnitySocketMessage =
| UnitySocketMessageHeartbeat
| UnitySocketMessageCameraFrame;
interface UnitySocketMessageBase {
type: string;
timestamp: number;
}
interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
type: 'heartbeat_data';
heartbeat: {
dataSensors: {
sensorIndex: number;
deviceName: string;
outputValue: number;
}[];
dataSliders: {
sliderIndex: number;
sliderName: string;
outputValue: number;
min: number;
max: number;
unit: string;
}[];
dataAdvancedSliders: {
sliderIndex: number;
sliderName: string;
outputValue: number;
min: number;
max: number;
unit: string;
}[];
dataTimeline: {
isStanding: boolean;
isWatching: boolean;
timelineProgress: number;
};
zedCamera: {
cameraFPS: string;
isZedReady: boolean;
streamInputIP: string;
streamInputPort: number;
zedGrabError: number;
};
showOutOfService?: boolean;
};
}
export const CameraUnityStateColors: Record<ServiceState, State> = {
CONNECTED: State.Green,
DISCONNECTED: State.Gray,
CONNECTING: State.Yellow,
FAILED: State.Red,
};
interface UnitySocketMessageCameraFrame extends UnitySocketMessageBase {
type: 'response_camera_frame';
imageBase64: string;
}