Lots of changes

-    Feedback van dataSensor array
-    dataSliders min/max and unit
-    Camera before Unity
-    Restart knop buiten service mode
-    Operator phonenumber button
-    Gracefull shutdown
-    Out of service control
This commit is contained in:
2025-10-23 17:45:35 +02:00
parent f07ba57168
commit cd33670361
36 changed files with 1444 additions and 277 deletions

View File

@@ -39,7 +39,7 @@ export class CameraRunner {
}
sendCommand(
command: string,
command: 'reboot' | 'restart',
callback: (response: { succeed: boolean; message?: string }) => void
) {
if (this.socket == null || !this.socket.connected)

View File

@@ -102,6 +102,7 @@ export interface Config {
webServer: ConfigWebServer;
unity: ConfigUnity;
cameraRunner: ConfigCameraRunner;
support: ConfigSupport;
}
export interface ConfigWebServer {
@@ -132,3 +133,7 @@ export interface ConfigCameraRunner {
pollInterval: number;
}
export interface ConfigSupport {
telephone: string;
}

View File

@@ -27,4 +27,7 @@ export const DefaultConfiguration: Config = {
pollInterval: 5000,
},
support: {
telephone: '+31613392837',
},
};

View File

@@ -31,4 +31,35 @@ export class Main {
this.UnityRunner.start();
}, this.Config.unity.executable.startUpDelay ?? 0);
}
async restart() {
console.log('Stopping UnityRunner...');
await this.UnityRunner.stop();
const doReboot = !process.argv.includes('--no-reboot');
console.log(`${doReboot ? 'Rebooting' : 'Restarting'} CameraRunner...`);
const succeed = await new Promise((resolve) => {
this.CameraRunner.sendCommand(
doReboot ? 'reboot' : 'restart',
(response: { succeed: boolean; message?: string }) => {
if (!response.succeed) {
console.error(
'Failed to reboot CameraRunner:',
response.message
);
resolve(false);
} else {
console.log('CameraRunner rebooted successfully.');
resolve(true);
}
}
);
});
if (!succeed) return;
console.log('Starting UnityRunner...');
await this.UnityRunner.start();
console.log('Restart complete.');
}
}

View File

@@ -82,42 +82,77 @@ export class UnityRunner {
else console.log(PREFIX, message ?? error);
}
private restartTriggered: boolean = false;
async restart(instant: boolean = false) {
if (this.restartTriggered) return;
clearInterval(this.statusClock);
async stop() {
if (this.process == null) return;
this._Main.WebServer.Calibration.hasCalibrationImage = false;
this.restartTriggered = true;
if (this._Main.UnityWebSocket.state === 'CONNECTED') {
this._Main.UnityWebSocket.quitApplication();
this.setInfo(
'Requested quit through WebSocket...',
null,
'STARTING'
);
} else {
this.process.kill('SIGTERM');
this.setInfo('Requested quit through SIGTERM...', null, 'STARTING');
}
const quitSucceeded = await new Promise((resolve) => {
const timeout = setTimeout(() => {
clearInterval(c);
resolve(false);
}, 5000);
const c = setInterval(() => {
if (
this.process != null &&
!this.process.killed &&
this.process.exitCode == null
)
return;
clearTimeout(timeout);
clearInterval(c);
resolve(true);
});
});
if (!quitSucceeded) this.process.kill('SIGKILL');
this.setInfo('Stopped', null, 'STOPPED');
this._Main.UnityWebSocket.disconnect();
this._Main.WebServer.Calibration.hasCalibrationImage = false;
this.startTime = -1;
}
restartTriggered: boolean = false;
async restart(instant: boolean = false) {
if (this.restartTriggered) return;
this.restartTriggered = true;
clearInterval(this.statusClock);
this.startTime = -1;
this.broadcastState();
await delay(2000);
if (!instant) await delay(2000);
if (this.output.current.length > 0) {
this.output.last = [...this.output.current];
this.output.current = [];
}
if (instant)
await this.stop();
if (instant) {
await delay(1000);
this.setInfo('Process will restart shortly...', null, 'STOPPED');
if (this.process != null) {
this.process.kill('SIGTERM');
await delay(3000);
if (!this.process.killed && this.process.exitCode === null) {
this.process.kill('SIGKILL');
console.log(PREFIX, 'Sent SIGKILL to process.');
}
}
this.startTime = -1;
if (!instant) {
await delay(2000);
} else {
this.message = 'Reconnecting in 10 seconds...';
this.broadcastState();
@@ -199,7 +234,7 @@ export class UnityRunner {
this.setInfo(
'Process exited',
`Process exited with code ${code} and signal ${signal}`,
'STOPPED'
'PROBLEM'
);
this.restart();
});

View File

@@ -17,7 +17,9 @@ export class UnityWebSocket {
zedPath: '',
zedReady: false,
zedFPS: '-',
parameters: [],
outOfService: null,
sliders: [],
sensors: [],
};
socket: WebSocket;
@@ -32,30 +34,72 @@ export class UnityWebSocket {
const sliderIndex: number = args[0];
const percentage: number = args[1];
if (this.parameters.parameters[sliderIndex] == undefined)
return;
this.parameters.parameters[sliderIndex].outputValue =
percentage;
this.setSliderValue(sliderIndex, percentage);
break;
if (
this.socket == null ||
this.socket.readyState !== WebSocket.OPEN
)
return;
case 'enableOutOfService':
const enableCallback: Function = args[0];
if (typeof enableCallback !== 'function') return;
this.socket.send(
JSON.stringify({
type: 'set_slider_value',
sliderIndex: sliderIndex,
sliderValue: percentage,
})
);
this.setOutOfService(true);
this.broadcastState();
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();
}
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',
@@ -99,7 +143,28 @@ export class UnityWebSocket {
this.parameters.zedReady =
message.heartbeat.zedCamera.isZedReady;
this.parameters.zedFPS = message.heartbeat.zedCamera.cameraFPS;
this.parameters.parameters = message.heartbeat.dataSliders;
this.parameters.outOfService =
message.heartbeat.showOutOfService ?? false;
this.parameters.sensors = message.heartbeat.dataSensors;
this.parameters.sliders = message.heartbeat.dataSliders.map(
(slider) => {
return {
...slider,
min: slider.min ?? 0,
max: slider.max ?? 1,
unit: slider.unit ?? '%',
visualMultiplier:
(slider.min ?? 0) == 0 && (slider.max ?? 1) == 1
? 100
: null,
decimalPlaces:
(slider.min ?? 0) == 0 && (slider.max ?? 1) == 1
? 0
: 2,
} as UnityParameterSlider;
}
);
this.broadcastState();
break;
@@ -182,6 +247,7 @@ export class UnityWebSocket {
});
this.socket.on('close', () => {
if (this._Main.UnityRunner.restartTriggered) return;
if (this.restartRequested) return;
this.setInfo(
'Disconnected',
@@ -247,7 +313,16 @@ interface UnityParameters {
zedPath: string;
zedReady: boolean;
zedFPS: string;
parameters: UnitySocketMessageHeartbeat['heartbeat']['dataSliders'];
outOfService: boolean;
sliders: UnityParameterSlider[];
sensors: UnitySocketMessageHeartbeat['heartbeat']['dataSensors'];
}
type UnityHeartbeatSlider =
UnitySocketMessageHeartbeat['heartbeat']['dataSliders'][number];
interface UnityParameterSlider extends UnityHeartbeatSlider {
visualMultiplier?: number;
decimalPlaces?: number;
}
type UnitySocketMessage =
@@ -261,10 +336,18 @@ interface UnitySocketMessageBase {
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;
}[];
dataTimeline: {
isStanding: boolean;
@@ -278,6 +361,7 @@ interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
streamInputPort: number;
zedGrabError: number;
};
showOutOfService?: boolean;
};
}
interface UnitySocketMessageCameraFrame extends UnitySocketMessageBase {

View File

@@ -50,7 +50,30 @@ export class WebServer {
'unityWebSocketState',
this._Main.UnityWebSocket.getState()
);
socket.emit(
'supportNumber',
this._Main.Config.support.telephone.replace('+', '')
);
socket.on(
'restartInstallation',
(
callback: (response: {
succeed: boolean;
message?: string;
}) => void
) => {
if (this._Main.CameraRunner.state !== 'CONNECTED')
return callback({
succeed: false,
message:
'Cannot reboot camera runner because it is not connected.',
});
this._Main.restart();
callback({ succeed: true });
}
);
socket.on('cameraRunner', (command: string, ...args: any[]) =>
this._Main.CameraRunner.handle(command, ...args)
);