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

@@ -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;
};
}

View File

@@ -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

View File

@@ -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) {