Initial commit

This commit is contained in:
2025-10-22 22:06:16 +02:00
commit d8ca4e154f
141 changed files with 32231 additions and 0 deletions

286
src/Unity/UnityWebSocket.ts Normal file
View File

@@ -0,0 +1,286 @@
import { RawData, WebSocket } from 'ws';
import { Main } from '../Main';
import { delay, ServiceState } from '../Utils';
const PREFIX = '[Unity]';
export class UnityWebSocket {
private _Main: Main;
state: ServiceState = 'DISCONNECTED';
message?: string = 'Waiting for process...';
error?: string;
parameters: UnityParameters = {
timelineWatching: false,
timelineStanding: false,
timelineProgress: 0,
zedPath: '',
zedReady: false,
zedFPS: '-',
parameters: [],
};
socket: WebSocket;
constructor(Main: Main) {
this._Main = Main;
}
handle(command: string, ...args: any[]) {
switch (command) {
case 'parameterValue':
const sliderIndex: number = args[0];
const percentage: number = args[1];
if (this.parameters.parameters[sliderIndex] == undefined)
return;
this.parameters.parameters[sliderIndex].outputValue =
percentage;
if (
this.socket == null ||
this.socket.readyState !== WebSocket.OPEN
)
return;
this.socket.send(
JSON.stringify({
type: 'set_slider_value',
sliderIndex: sliderIndex,
sliderValue: percentage,
})
);
this.broadcastState();
break;
}
}
broadcastState() {
this._Main.WebServer.socket.emit(
'unityWebSocketState',
this.getState()
);
}
setInfo(message: string, error: string, state: ServiceState = 'FAILED') {
this.message = message;
this.error = error;
this.state = state;
this.broadcastState();
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.parameters = message.heartbeat.dataSliders;
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.setInfo('Connected', null, 'CONNECTED');
});
this.socket.on('close', () => {
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;
parameters: UnitySocketMessageHeartbeat['heartbeat']['dataSliders'];
}
type UnitySocketMessage =
| UnitySocketMessageHeartbeat
| UnitySocketMessageCameraFrame;
interface UnitySocketMessageBase {
type: string;
timestamp: number;
}
interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
type: 'heartbeat_data';
heartbeat: {
dataSliders: {
sliderIndex: number;
sliderName: string;
outputValue: number;
}[];
dataTimeline: {
isStanding: boolean;
isWatching: boolean;
timelineProgress: number;
};
zedCamera: {
cameraFPS: string;
isZedReady: boolean;
streamInputIP: string;
streamInputPort: number;
zedGrabError: number;
};
};
}
interface UnitySocketMessageCameraFrame extends UnitySocketMessageBase {
type: 'response_camera_frame';
imageBase64: string;
}