732 lines
18 KiB
TypeScript
732 lines
18 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'
|
|
);
|
|
|
|
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';
|
|
} else {
|
|
this.restartButton.style.display = 'flex';
|
|
}
|
|
|
|
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.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;
|
|
};
|
|
}
|