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:
@@ -35,6 +35,19 @@ export class DashboardUnity {
|
||||
'.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'
|
||||
);
|
||||
@@ -63,6 +76,10 @@ export class DashboardUnity {
|
||||
'.ntsh_dashboard-unity-parameters'
|
||||
);
|
||||
|
||||
sensorsTable: HTMLTableElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-sensors'
|
||||
);
|
||||
|
||||
errorContainer: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-error'
|
||||
);
|
||||
@@ -91,7 +108,7 @@ export class DashboardUnity {
|
||||
this.processStatus,
|
||||
{
|
||||
RUNNING: 'green',
|
||||
STOPPED: 'red',
|
||||
STOPPED: 'gray',
|
||||
STARTING: 'yellow',
|
||||
PROBLEM: 'red',
|
||||
}[state.state] as StatusType
|
||||
@@ -126,6 +143,33 @@ export class DashboardUnity {
|
||||
);
|
||||
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,
|
||||
@@ -146,11 +190,19 @@ export class DashboardUnity {
|
||||
: 'No';
|
||||
setProgressState(
|
||||
this.timelineProgress,
|
||||
state.parameters.timelineProgress
|
||||
Math.round(state.parameters.timelineProgress * 100),
|
||||
0,
|
||||
100,
|
||||
'%'
|
||||
);
|
||||
|
||||
// ----------- Parameters -----------
|
||||
this.renderParameterSliders(state.parameters.parameters);
|
||||
this.renderParameterSliders(
|
||||
state.state == 'CONNECTED' ? state.parameters.sliders : []
|
||||
);
|
||||
this.renderParameterSensors(
|
||||
state.state == 'CONNECTED' ? state.parameters.sensors : []
|
||||
);
|
||||
|
||||
// ----------- Error -----------
|
||||
if ((state?.error ?? '').trim().length > 0)
|
||||
@@ -159,78 +211,210 @@ export class DashboardUnity {
|
||||
this.updateError();
|
||||
}
|
||||
|
||||
private renderParameterSliders(parameters: UnityParameters['parameters']) {
|
||||
if (parameters.length === 0) return;
|
||||
|
||||
private renderParameterSliders(sliders: UnityParameters['sliders']) {
|
||||
const existingSliders = this.parametersTable.querySelectorAll(
|
||||
'.ntsh_dashboard-unity-parameter-row'
|
||||
);
|
||||
|
||||
if (existingSliders.length !== parameters.length) {
|
||||
if (existingSliders.length !== sliders.length) {
|
||||
this.parametersTable.innerHTML = '';
|
||||
|
||||
parameters.forEach((param) => {
|
||||
const row = ce('tr', 'ntsh_dashboard-unity-parameter-row');
|
||||
|
||||
const nameCell = ce('td');
|
||||
nameCell.appendChild(
|
||||
ce('div', 'mux_text', null, param.sliderName)
|
||||
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(nameCell);
|
||||
|
||||
const progressCell = ce('td', 'no-service');
|
||||
progressCell.appendChild(createProgress(param.outputValue));
|
||||
row.appendChild(progressCell);
|
||||
|
||||
const sliderCell = ce('td', 'only-service');
|
||||
const sliderProgress = createProgress(param.outputValue);
|
||||
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 lastPercentage: number = -1;
|
||||
resizer.on('resized', (size) => {
|
||||
const percentage =
|
||||
Math.round((size / sliderProgress.clientWidth) * 100) /
|
||||
100;
|
||||
if (percentage === lastPercentage) return;
|
||||
lastPercentage = percentage;
|
||||
|
||||
this._Main.socket.emit(
|
||||
'unityWebSocket',
|
||||
'parameterValue',
|
||||
param.sliderIndex,
|
||||
percentage
|
||||
);
|
||||
setProgressState(sliderProgress, percentage);
|
||||
});
|
||||
|
||||
row.appendChild(sliderCell);
|
||||
|
||||
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 value = parameters[index].outputValue;
|
||||
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);
|
||||
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);
|
||||
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,
|
||||
'%'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -264,9 +448,29 @@ export class DashboardUnity {
|
||||
'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) {
|
||||
private async executeCommand(
|
||||
command: string,
|
||||
message: string,
|
||||
type: 'unityRunner' | 'unityWebSocket' = 'unityRunner'
|
||||
) {
|
||||
const confirmed = await MorphFeature.Confirm({
|
||||
title: 'Are you sure?',
|
||||
message,
|
||||
@@ -275,10 +479,10 @@ export class DashboardUnity {
|
||||
|
||||
MorphFeature.Loader({
|
||||
active: true,
|
||||
message: `Requesting Unity Runner ${command}...`,
|
||||
message: `Dispatching command...`,
|
||||
});
|
||||
this._Main.socket.emit(
|
||||
'unityRunner',
|
||||
type,
|
||||
command,
|
||||
(response: { succeed: boolean; message?: string }) => {
|
||||
MorphFeature.Loader({ active: false });
|
||||
@@ -291,7 +495,7 @@ export class DashboardUnity {
|
||||
|
||||
MorphFeature.Notification({
|
||||
level: 'success',
|
||||
message: `Unity Runner is ${command}ing...`,
|
||||
message: `Dispatched command`,
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -322,7 +526,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;
|
||||
}
|
||||
|
||||
interface UnitySocketMessageBase {
|
||||
@@ -332,10 +545,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;
|
||||
@@ -349,5 +570,6 @@ interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
|
||||
streamInputPort: number;
|
||||
zedGrabError: number;
|
||||
};
|
||||
showOutOfService?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MorphComponent, MorphFeature } from 'morphux';
|
||||
import { ce, MorphComponent, MorphFeature } from 'morphux';
|
||||
import { Main } from './main';
|
||||
import { ComponentMenuBar } from 'morphux/dist/Components/MenuBar/Component.MenuBar';
|
||||
|
||||
@@ -9,11 +9,17 @@ export class MenuBar {
|
||||
|
||||
menubar: ComponentMenuBar;
|
||||
|
||||
supportNumber: string;
|
||||
|
||||
constructor(main: Main) {
|
||||
this._Main = main;
|
||||
|
||||
this.build();
|
||||
|
||||
this._Main.socket.on('supportNumber', (number: string) => {
|
||||
this.supportNumber = number;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (localStorage?.getItem('serviceMode') === 'true')
|
||||
this.toggleServiceMode(true, true);
|
||||
@@ -22,10 +28,47 @@ export class MenuBar {
|
||||
|
||||
build() {
|
||||
this.menubar = new ComponentMenuBar({
|
||||
mobile: {
|
||||
left: [
|
||||
{
|
||||
type: 'icon',
|
||||
text: 'Restart',
|
||||
materialIcon: 'restart_alt',
|
||||
uniqueIdentifier: 'restart_installation',
|
||||
|
||||
click: async () => {
|
||||
const mobileMenu: HTMLDivElement =
|
||||
document.querySelector('.mux_mobilemenu');
|
||||
mobileMenu?.click();
|
||||
|
||||
this.restartInstallation();
|
||||
},
|
||||
},
|
||||
],
|
||||
right: [
|
||||
{
|
||||
type: 'icon',
|
||||
text: 'Support',
|
||||
materialIcon: 'call_quality',
|
||||
uniqueIdentifier: 'call_support',
|
||||
click: () => this.showSupport(),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: [
|
||||
{
|
||||
type: 'image',
|
||||
url: '/img/morphix_logo_white.png',
|
||||
type: 'normal',
|
||||
text: 'Restart',
|
||||
materialIcon: 'restart_alt',
|
||||
uniqueIdentifier: 'restart_installation',
|
||||
|
||||
click: async () => {
|
||||
const mobileMenu: HTMLDivElement =
|
||||
document.querySelector('.mux_mobilemenu');
|
||||
mobileMenu?.click();
|
||||
|
||||
this.restartInstallation();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
@@ -65,7 +108,16 @@ export class MenuBar {
|
||||
|
||||
right: [
|
||||
{
|
||||
type: 'normal',
|
||||
type: 'icon',
|
||||
text: 'Support',
|
||||
materialIcon: 'call_quality',
|
||||
uniqueIdentifier: 'call_support',
|
||||
click: () => this.showSupport(),
|
||||
},
|
||||
{
|
||||
type: document.body.classList.contains('ntsh_service')
|
||||
? 'normal'
|
||||
: 'icon',
|
||||
text: document.body.classList.contains('ntsh_service')
|
||||
? 'Exit Service'
|
||||
: 'Service Mode',
|
||||
@@ -90,6 +142,51 @@ export class MenuBar {
|
||||
this.container.appendChild(this.menubar.container);
|
||||
}
|
||||
|
||||
async showSupport() {
|
||||
const dialog = new MorphComponent.Dialog({
|
||||
title: 'Contact Support',
|
||||
width: 'medium',
|
||||
height: 'auto',
|
||||
okButtonVisible: false,
|
||||
cancelButtonVisible: false,
|
||||
});
|
||||
|
||||
this.supportNumber.slice();
|
||||
const callAnchor = ce(
|
||||
'a',
|
||||
'ntsh_callanchor',
|
||||
{ href: `tel:${this.supportNumber}` },
|
||||
`+${this.supportNumber}`
|
||||
);
|
||||
dialog.content.appendChild(callAnchor);
|
||||
|
||||
setTimeout(() => callAnchor.click(), 100);
|
||||
}
|
||||
|
||||
async restartInstallation() {
|
||||
const confirmed = await MorphFeature.Confirm({
|
||||
title: 'Restart Installation',
|
||||
message: 'Are you sure you want to restart the installation?',
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
MorphFeature.Loader({
|
||||
active: true,
|
||||
message: 'Restarting installation...',
|
||||
});
|
||||
this._Main.socket.emit(
|
||||
'restartInstallation',
|
||||
(response: { succeed: boolean; message?: string }) => {
|
||||
MorphFeature.Loader({ active: false });
|
||||
if (!response.succeed)
|
||||
return MorphFeature.Alert({
|
||||
title: 'Error',
|
||||
message: response.message,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async toggleServiceMode(
|
||||
mode?: boolean,
|
||||
skipPin?: boolean
|
||||
|
||||
@@ -26,24 +26,34 @@ export type StatusType = 'green' | 'yellow' | 'red' | 'gray';
|
||||
|
||||
export const setProgressState = (
|
||||
progressElement: HTMLDivElement,
|
||||
percentage: number
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
unit: string
|
||||
) => {
|
||||
const value: HTMLDivElement = progressElement.querySelector(
|
||||
const percentage = (value - min) / (max - min);
|
||||
|
||||
const progressValue: HTMLDivElement = progressElement.querySelector(
|
||||
'.ntsh_progress-value'
|
||||
);
|
||||
value.style.width = `${percentage * 100}%`;
|
||||
progressValue.style.width = `${percentage * 100}%`;
|
||||
|
||||
const label: HTMLDivElement = progressElement.querySelector(
|
||||
'.ntsh_progress-label'
|
||||
);
|
||||
label.innerText = `${Math.round(percentage * 100)}%`;
|
||||
label.innerText = `${value}${unit}`;
|
||||
};
|
||||
|
||||
export const createProgress = (value: number) => {
|
||||
export const createProgress = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
unit: string
|
||||
) => {
|
||||
const progress = ce('div', 'ntsh_progress');
|
||||
progress.appendChild(ce('div', 'ntsh_progress-value'));
|
||||
progress.appendChild(ce('div', 'ntsh_progress-label'));
|
||||
setProgressState(progress, value);
|
||||
setProgressState(progress, value, min, max, unit);
|
||||
return progress;
|
||||
};
|
||||
export function capitalizeFirstLetter(string: string) {
|
||||
|
||||
Reference in New Issue
Block a user