Files
NTSH-Control/frontend/views/dashboard/ts/dashboard.unity.ts

754 lines
19 KiB
TypeScript

import { ce, MorphComponent, MorphFeature } from 'morphux';
import { Main } from './main';
import {
createProgress,
formatUptime,
ServiceState,
setProgressState,
setStatusState,
StatusType,
} from './utils';
export class DashboardUnity {
private _Main: Main;
container: HTMLDivElement = document.querySelector('.ntsh_dashboard-unity');
processStatus: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-processstatus'
);
processInfo: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-processinfo'
);
restartButton: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-restart'
);
startButton: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-start'
);
stopButton: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-stop'
);
uptimeInfo: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-uptime'
);
webSocketStatus: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-websocketstatus'
);
webSocketInfo: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-websocketinfo'
);
outOfServiceStatus: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-outofservicestatus'
);
outOfServiceInfo: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-outofserviceinfo'
);
enableOutOfServiceButton: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-enableoutofservice'
);
disableOutOfServiceButton: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-disableoutofservice'
);
zedStreamStatus: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-zedstreamstatus'
);
zedStreamInfo: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-zedstreaminfo'
);
zedStreamFps: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-zedstreamfps'
);
zedStreamPath: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-zedstreampath'
);
timelineWatching: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-timeline-watching'
);
timelineStanding: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-timeline-standing'
);
timelineProgress: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-timeline-progress'
);
parametersTable: HTMLTableElement = document.querySelector(
'.ntsh_dashboard-unity-parameters'
);
advancedParametersTable: HTMLTableElement = document.querySelector(
'.ntsh_dashboard-unity-advancedparameters'
);
sensorsTable: HTMLTableElement = document.querySelector(
'.ntsh_dashboard-unity-sensors'
);
errorContainer: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-error'
);
errorText: HTMLDivElement = document.querySelector(
'.ntsh_dashboard-unity-errortext'
);
constructor(Main: Main) {
this._Main = Main;
this.registerListeners();
}
private runnerError: string;
updateRunnerState(state: UnityRunnerStatus) {
// ----------- Process -----------
if (state.state != 'RUNNING') {
state.startTime = -1;
this.restartButton.style.display = 'none';
this.stopButton.style.display = 'none';
} else {
this.restartButton.style.display = 'flex';
this.stopButton.style.display = 'flex';
}
this.startButton.style.display =
state.state == 'STOPPED' ? 'flex' : 'none';
setStatusState(
this.processStatus,
{
RUNNING: 'green',
STOPPED: 'gray',
STARTING: 'yellow',
PROBLEM: 'red',
}[state.state] as StatusType
);
this.processInfo.innerText = state.message ?? '';
// ----------- Uptime -----------
const uptimeSeconds =
state.startTime == -1 ? -1 : (Date.now() - state.startTime) / 1000;
this.uptimeInfo.innerText = formatUptime(uptimeSeconds);
// ----------- Error -----------
if ((state?.error ?? '').trim().length > 0)
this.runnerError = state.error;
else this.runnerError = null;
this.updateError();
this._Main.Logs.setUnityLogs(state.output.current);
}
private webSocketError: string;
updateWebSocketState(state: UnityWebSocketStatus) {
// ----------- WebSocket -----------
setStatusState(
this.webSocketStatus,
{
CONNECTING: 'yellow',
CONNECTED: 'green',
DISCONNECTED: 'gray',
FAILED: 'red',
}[state.state] as StatusType
);
this.webSocketInfo.innerText = state.message ?? '';
// ----------- Out of Service -----------
setStatusState(
this.outOfServiceStatus,
state.parameters.outOfService == null
? 'gray'
: state.parameters.outOfService
? 'red'
: 'green'
);
this.outOfServiceInfo.innerText =
state.parameters.outOfService == null
? ''
: state.parameters.outOfService
? 'Out of Service'
: 'Operational';
this.enableOutOfServiceButton.style.display =
state.parameters.outOfService == null ||
state.parameters.outOfService
? 'none'
: 'flex';
this.disableOutOfServiceButton.style.display =
state.parameters.outOfService &&
state.parameters.outOfService != null
? 'flex'
: 'none';
// ----------- ZED Stream -----------
setStatusState(
this.zedStreamStatus,
state.parameters.zedReady ? 'green' : 'red'
);
this.zedStreamInfo.innerText = state.parameters.zedReady
? `Connected to ${state.parameters.zedPath}`
: 'Not ready';
this.zedStreamFps.innerText =
state.parameters.zedFPS == '-' ? '' : state.parameters.zedFPS;
// ----------- Timeline -----------
this.timelineWatching.innerText = state.parameters.timelineWatching
? 'Yes'
: 'No';
this.timelineStanding.innerText = state.parameters.timelineStanding
? 'Yes'
: 'No';
setProgressState(
this.timelineProgress,
Math.round(state.parameters.timelineProgress * 100),
0,
100,
'%'
);
// ----------- Parameters -----------
this.renderParameterSliders(
state.state == 'CONNECTED' ? state.parameters.sliders : []
);
this.renderAdvancedParameterSliders(
state.state == 'CONNECTED' ? state.parameters.advancedSliders : []
);
this.renderParameterSensors(
state.state == 'CONNECTED' ? state.parameters.sensors : []
);
// ----------- Error -----------
if ((state?.error ?? '').trim().length > 0)
this.webSocketError = state.error;
else this.webSocketError = null;
this.updateError();
}
private renderParameterSliders(sliders: UnityParameters['sliders']) {
const existingSliders = this.parametersTable.querySelectorAll(
'.ntsh_dashboard-unity-parameter-row'
);
if (existingSliders.length !== sliders.length) {
this.parametersTable.innerHTML = '';
if (sliders.length === 0) {
const row = ce('tr');
const cell = ce('td');
cell.appendChild(
ce(
'div',
['mux_text', 'ntsh_dashboard-unity-parameters-loading'],
null,
'Waiting for Unity...'
)
);
row.appendChild(cell);
this.parametersTable.appendChild(row);
} else
sliders.forEach((slider) => {
const multiplierFactor = slider.visualMultiplier ?? 1;
const decimalPlacesFactor =
10 ** (slider.decimalPlaces ?? 0);
const value =
Math.round(
slider.outputValue *
multiplierFactor *
decimalPlacesFactor
) / decimalPlacesFactor;
const row = ce('tr', 'ntsh_dashboard-unity-parameter-row');
const nameCell = ce('td');
nameCell.appendChild(
ce('div', 'mux_text', null, slider.sliderName)
);
row.appendChild(nameCell);
const progressCell = ce('td', 'no-service');
progressCell.appendChild(
createProgress(
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
)
);
row.appendChild(progressCell);
const sliderCell = ce('td', 'only-service');
const sliderProgress = createProgress(
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
const sliderValue: HTMLDivElement =
sliderProgress.querySelector('.ntsh_progress-value');
sliderValue.classList.add('mux_resizer');
sliderCell.appendChild(sliderProgress);
const resizer = new MorphComponent.Resizer({
existingContainer: sliderValue,
direction: 'right',
relative: true,
min: 0,
max: () => sliderProgress.clientWidth,
});
let lastValue: number = -1;
resizer.on('resized', (size) => {
const percentage =
Math.round(
(size / sliderProgress.clientWidth) * 100
) / 100;
var actualValue =
slider.min + percentage * (slider.max - slider.min);
if (actualValue === lastValue) return;
lastValue = actualValue;
this._Main.socket.emit(
'unityWebSocket',
'parameterValue',
slider.sliderIndex,
actualValue
);
setProgressState(
sliderProgress,
Math.round(
actualValue *
multiplierFactor *
decimalPlacesFactor
) / decimalPlacesFactor,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
});
row.appendChild(sliderCell);
this.parametersTable.appendChild(row);
});
} else {
existingSliders.forEach((row, index) => {
const slider = sliders[index];
const multiplierFactor = slider.visualMultiplier ?? 1;
const decimalPlacesFactor = 10 ** (slider.decimalPlaces ?? 0);
const value =
Math.round(
slider.outputValue *
multiplierFactor *
decimalPlacesFactor
) / decimalPlacesFactor;
const progressElement: HTMLDivElement = row.querySelector(
'.no-service .ntsh_progress'
);
setProgressState(
progressElement,
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
const sliderElement: HTMLDivElement = row.querySelector(
'.only-service .ntsh_progress'
);
if (sliderElement.querySelector('.mux_resizer-moving') == null)
setProgressState(
sliderElement,
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
});
}
}
private renderAdvancedParameterSliders(
sliders: UnityParameters['sliders']
) {
const existingSliders = this.advancedParametersTable.querySelectorAll(
'.ntsh_dashboard-unity-parameter-row'
);
if (existingSliders.length !== sliders.length) {
this.advancedParametersTable.innerHTML = '';
if (sliders.length === 0) {
const row = ce('tr');
const cell = ce('td');
cell.appendChild(
ce(
'div',
['mux_text', 'ntsh_dashboard-unity-parameters-loading'],
null,
'Waiting for Unity...'
)
);
row.appendChild(cell);
this.advancedParametersTable.appendChild(row);
} else
sliders.forEach((slider) => {
const multiplierFactor = slider.visualMultiplier ?? 1;
const decimalPlacesFactor =
10 ** (slider.decimalPlaces ?? 0);
const value =
Math.round(
slider.outputValue *
multiplierFactor *
decimalPlacesFactor
) / decimalPlacesFactor;
const row = ce('tr', 'ntsh_dashboard-unity-parameter-row');
const nameCell = ce('td');
nameCell.appendChild(
ce('div', 'mux_text', null, slider.sliderName)
);
row.appendChild(nameCell);
const progressCell = ce('td', 'no-service');
progressCell.appendChild(
createProgress(
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
)
);
row.appendChild(progressCell);
const sliderCell = ce('td', 'only-service');
const sliderProgress = createProgress(
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
const sliderValue: HTMLDivElement =
sliderProgress.querySelector('.ntsh_progress-value');
sliderValue.classList.add('mux_resizer');
sliderCell.appendChild(sliderProgress);
const resizer = new MorphComponent.Resizer({
existingContainer: sliderValue,
direction: 'right',
relative: true,
min: 0,
max: () => sliderProgress.clientWidth,
});
let lastValue: number = -1;
resizer.on('resized', (size) => {
const percentage =
Math.round(
(size / sliderProgress.clientWidth) * 100
) / 100;
var actualValue =
slider.min + percentage * (slider.max - slider.min);
if (actualValue === lastValue) return;
lastValue = actualValue;
this._Main.socket.emit(
'unityWebSocket',
'advancedParameterValue',
slider.sliderIndex,
actualValue
);
setProgressState(
sliderProgress,
Math.round(
actualValue *
multiplierFactor *
decimalPlacesFactor
) / decimalPlacesFactor,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
});
row.appendChild(sliderCell);
this.advancedParametersTable.appendChild(row);
});
} else {
existingSliders.forEach((row, index) => {
const slider = sliders[index];
const multiplierFactor = slider.visualMultiplier ?? 1;
const decimalPlacesFactor = 10 ** (slider.decimalPlaces ?? 0);
const value =
Math.round(
slider.outputValue *
multiplierFactor *
decimalPlacesFactor
) / decimalPlacesFactor;
const progressElement: HTMLDivElement = row.querySelector(
'.no-service .ntsh_progress'
);
setProgressState(
progressElement,
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
const sliderElement: HTMLDivElement = row.querySelector(
'.only-service .ntsh_progress'
);
if (sliderElement.querySelector('.mux_resizer-moving') == null)
setProgressState(
sliderElement,
value,
slider.min * multiplierFactor,
slider.max * multiplierFactor,
slider.unit
);
});
}
}
private renderParameterSensors(sensors: UnityParameters['sensors']) {
const existingSensors = this.sensorsTable.querySelectorAll(
'.ntsh_dashboard-unity-sensor-row'
);
if (existingSensors.length !== sensors.length) {
this.sensorsTable.innerHTML = '';
if (sensors.length === 0) {
const row = ce('tr');
const cell = ce('td');
cell.appendChild(
ce(
'div',
['mux_text', 'ntsh_dashboard-unity-sensors-loading'],
null,
'Waiting for Unity...'
)
);
row.appendChild(cell);
this.sensorsTable.appendChild(row);
} else
sensors.forEach((sensor) => {
const row = ce('tr', 'ntsh_dashboard-unity-sensor-row');
const nameCell = ce('td');
nameCell.appendChild(
ce('div', 'mux_text', null, sensor.deviceName)
);
row.appendChild(nameCell);
const progressCell = ce('td');
progressCell.appendChild(
createProgress(
Math.round(sensor.outputValue * 100),
0,
100,
'%'
)
);
row.appendChild(progressCell);
this.sensorsTable.appendChild(row);
});
} else {
existingSensors.forEach((row, index) => {
const value = sensors[index].outputValue;
const progressElement: HTMLDivElement =
row.querySelector('.ntsh_progress');
setProgressState(
progressElement,
Math.round(value * 100),
0,
100,
'%'
);
});
}
}
private updateError() {
const errors: string[] = [];
if (this.runnerError != null) errors.push(this.runnerError);
if (this.webSocketError != null) errors.push(this.webSocketError);
if (errors.length > 0) {
this.errorText.innerText = errors.join('\n');
this.errorContainer.style.display = 'block';
} else {
this.errorContainer.style.display = 'none';
this.errorText.innerText = '';
}
}
registerListeners() {
this._Main.socket.on('unityRunnerState', (state: UnityRunnerStatus) =>
this.updateRunnerState(state)
);
this._Main.socket.on(
'unityWebSocketState',
(state: UnityWebSocketStatus) => this.updateWebSocketState(state)
);
this.restartButton.addEventListener('click', async () => {
this.executeCommand(
'restart',
'Are you sure you want to restart the Unity Runner process?'
);
});
this.startButton.addEventListener('click', async () => {
this.executeCommand(
'start',
'Are you sure you want to start the Unity Runner process?'
);
});
this.stopButton.addEventListener('click', async () => {
this.executeCommand(
'stop',
'Are you sure you want to stop the Unity Runner process?'
);
});
this.enableOutOfServiceButton.addEventListener('click', async () => {
this.executeCommand(
'enableOutOfService',
'Are you sure you want to set the installation to "Out of Service"?',
'unityWebSocket'
);
});
this.disableOutOfServiceButton.addEventListener('click', async () => {
this.executeCommand(
'disableOutOfService',
'Are you sure you want to set the installation to "Operational"?',
'unityWebSocket'
);
});
}
private async executeCommand(
command: string,
message: string,
type: 'unityRunner' | 'unityWebSocket' = 'unityRunner'
) {
const confirmed = await MorphFeature.Confirm({
title: 'Are you sure?',
message,
});
if (!confirmed) return;
MorphFeature.Loader({
active: true,
message: `Dispatching command...`,
});
this._Main.socket.emit(
type,
command,
(response: { succeed: boolean; message?: string }) => {
MorphFeature.Loader({ active: false });
if (!response.succeed)
return MorphFeature.Alert({
title: 'Error',
message: response.message,
});
MorphFeature.Notification({
level: 'success',
message: `Dispatched command`,
});
}
);
}
}
interface UnityRunnerStatus {
state: 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';
message?: string;
error?: string;
startTime: number;
output: { current: string[]; last: string[] };
}
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;
}
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;
}[];
dataTimeline: {
isStanding: boolean;
isWatching: boolean;
timelineProgress: number;
};
zedCamera: {
cameraFPS: string;
isZedReady: boolean;
streamInputIP: string;
streamInputPort: number;
zedGrabError: number;
};
showOutOfService?: boolean;
};
}