Added basic control panel

This commit is contained in:
2026-03-11 16:46:06 +01:00
parent 7df210aaf2
commit c4eedfff1e
105 changed files with 21923 additions and 958 deletions

View File

@@ -0,0 +1,41 @@
import { Main } from './main';
export class Calibration {
private _Main: Main;
observer: IntersectionObserver;
visible: boolean = false;
container: HTMLDivElement = document.querySelector('.ntsh-calibration');
image: HTMLImageElement = this.container.querySelector('img');
constructor(Main: Main) {
this._Main = Main;
this.registerListeners();
this.startClock();
}
private startClock() {
setInterval(() => {
if (this.visible && this.image)
this.image.src = `/calibrationImage?t=${Date.now()}`;
}, 1000);
}
private registerListeners() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.visible = true;
console.log('Calibration visible');
} else {
this.visible = false;
console.log('Calibration not visible');
}
});
});
this.observer.observe(this.container);
}
}

View File

@@ -0,0 +1,146 @@
import { Main } from './main';
import { MorphFeature } from 'morphux';
export class Checklist {
private _Main: Main;
Rows = {
CAMERAPC: document.querySelector(
`.ntsh-checklist-row[status="CAMERAPC"]`,
) as HTMLDivElement,
CAMERAPROCESS: document.querySelector(
`.ntsh-checklist-row[status="CAMERAPROCESS"]`,
) as HTMLDivElement,
CAMERAUNITYSTREAM: document.querySelector(
`.ntsh-checklist-row[status="CAMERAUNITYSTREAM"]`,
) as HTMLDivElement,
UNITYBUILD: document.querySelector(
`.ntsh-checklist-row[status="UNITYBUILD"]`,
) as HTMLDivElement,
REPLAYFUNCTION: document.querySelector(
`.ntsh-checklist-row[status="REPLAYFUNCTION"]`,
) as HTMLDivElement,
};
FullReboot: HTMLDivElement = document.querySelector(
'.ntsh-fullreboot-button',
);
constructor(Main: Main) {
this._Main = Main;
this.registerListeners();
}
update(status: Status) {
this.updateRow(this.Rows.CAMERAPC, status.CAMERAPC);
this.updateRow(this.Rows.CAMERAPROCESS, status.CAMERAPROCESS);
this.updateRow(this.Rows.CAMERAUNITYSTREAM, status.CAMERAUNITYSTREAM);
this.updateRow(this.Rows.UNITYBUILD, status.UNITYBUILD);
this.updateRow(this.Rows.REPLAYFUNCTION, status.REPLAYFUNCTION);
console.log('Updated checklist:', status);
}
updateRow(row: HTMLDivElement, state: StateEntry) {
const status: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-status',
);
const message: HTMLDivElement = row.querySelector('p');
const startButton: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-button.start',
);
const stopButton: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-button.stop',
);
const rebootButton: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-button.reboot',
);
status.classList.remove('RED', 'GREEN', 'YELLOW', 'GRAY');
status.classList.add(state.state);
message.innerText = state.message;
startButton.style.display = state.buttons?.start ? 'block' : 'none';
stopButton.style.display = state.buttons?.stop ? 'block' : 'none';
rebootButton.style.display = state.buttons?.reboot ? 'block' : 'none';
}
private registerListeners() {
this.FullReboot.onclick = () => {
MorphFeature.Confirm(
{
title: 'Full Reboot',
message: 'Are you sure you want to perform a full reboot?',
},
(state) => {
if (!state) return;
this._Main.socket.emit('status', 'fullreboot');
},
);
};
for (const key in this.Rows) {
const row = this.Rows[key];
const startButton: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-button.start',
);
startButton.onclick = () =>
MorphFeature.Confirm(
{
title: 'Start',
message: 'Are you sure you want to start?',
},
(state) => {
if (!state) return;
this._Main.socket.emit('status', 'start', key);
},
);
const stopButton: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-button.stop',
);
stopButton.onclick = () =>
MorphFeature.Confirm(
{
title: 'Stop',
message: 'Are you sure you want to stop?',
},
(state) => {
if (!state) return;
this._Main.socket.emit('status', 'stop', key);
},
);
const rebootButton: HTMLDivElement = row.querySelector(
'.ntsh-checklist-row-button.reboot',
);
rebootButton.onclick = () =>
MorphFeature.Confirm(
{
title: 'Reboot',
message: 'Are you sure you want to reboot?',
},
(state) => {
if (!state) return;
this._Main.socket.emit('status', 'reboot', key);
},
);
}
}
}
interface Status {
CAMERAPC: StateEntry;
CAMERAPROCESS: StateEntry;
CAMERAUNITYSTREAM: StateEntry;
UNITYBUILD: StateEntry;
REPLAYFUNCTION: StateEntry;
}
interface StateEntry {
state: 'GREEN' | 'RED' | 'YELLOW' | 'GRAY';
message: string;
buttons?: { reboot?: boolean; start?: boolean; stop?: boolean };
}

View File

@@ -0,0 +1,180 @@
import { io } from 'socket.io-client';
import { Menu } from './menu';
import { Checklist } from './checklist';
import { Calibration } from './calibration';
import { ce, MorphComponent, MorphFeature } from 'morphux';
import { OutOfServiceMode } from './outOfServiceMode';
import { Timer } from './timer';
const socket = io('/');
export class Main {
Menu = new Menu();
CheckList = new Checklist(this);
Calibration = new Calibration(this);
OutOfServiceMode = new OutOfServiceMode(this);
Timer = new Timer(this);
socket = socket;
supportButton: HTMLDivElement = document.querySelector('.ntsh-support img');
supportNumber: string = '';
constructor() {
this.registerListeners();
}
private registerListeners() {
this.supportButton.onclick = () => this.showSupport();
socket.on('status', (data) => {
this.CheckList.update(data);
});
socket.on('supportNumber', (number: string) => {
this.supportNumber = number;
});
socket.on('unityWebSocketState', (state: UnityWebSocketStatus) => {
this.OutOfServiceMode.input.checked =
state?.parameters?.outOfService ?? false;
this.OutOfServiceMode.state =
state?.parameters?.outOfService ?? false;
});
socket.on('timer', (data) => {
this.Timer.update(data);
});
}
async executeCommand(
command: string,
message: string,
type: 'unityRunner' | 'unityWebSocket' = 'unityRunner',
): Promise<boolean> {
return new Promise<boolean>(async (resolve) => {
const confirmed = await MorphFeature.Confirm({
title: 'Are you sure?',
message,
});
if (!confirmed) return resolve(false);
MorphFeature.Loader({
active: true,
message: `Dispatching command...`,
});
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`,
});
},
);
resolve(true);
});
}
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);
}
}
const _Main = new Main();
export type ServiceState =
| 'CONNECTING'
| 'CONNECTED'
| 'DISCONNECTED'
| 'FAILED';
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;
};
}

View File

@@ -0,0 +1,42 @@
export class Menu {
menuContainer: HTMLDivElement = document.querySelector('.ntsh_menubar');
tabContainer: HTMLDivElement = document.querySelector('.ntsh_tabs');
constructor() {
this.registerListeners();
if (window.location.search.includes('advanced'))
this.selectTab('advanced');
}
selectTab(tabId: string) {
this.menuContainer
.querySelectorAll('.ntsh_menubar-item')
.forEach((item) => {
if (item.getAttribute('tabid') === tabId) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
this.tabContainer.querySelectorAll('.ntsh_tab').forEach((tab) => {
if (tab.getAttribute('tabid') === tabId) {
tab.classList.add('visible');
} else {
tab.classList.remove('visible');
}
});
}
private registerListeners() {
this.menuContainer
.querySelectorAll('.ntsh_menubar-item')
.forEach((item) => {
item.addEventListener('click', () => {
const itemId = item.getAttribute('tabid');
this.selectTab(itemId);
});
});
}
}

View File

@@ -0,0 +1,29 @@
import { MorphComponent } from 'morphux';
import { Main } from './main';
export class OutOfServiceMode {
private _Main: Main;
state: boolean = false;
input: HTMLInputElement = document.querySelector(
'.ntsh-outofservicemode-input',
);
constructor(Main: Main) {
this._Main = Main;
this.registerListeners();
}
private registerListeners() {
this.input.addEventListener('change', async () => {
const valid = await this._Main.executeCommand(
this.state ? 'disableOutOfService' : 'enableOutOfService',
`Are you sure you want to set the installation to "${this.state ? 'Out of Service' : 'Operational'}"?`,
'unityWebSocket',
);
if (!valid) this.input.checked = this.state;
this.state = this.input.checked;
});
}
}

View File

@@ -0,0 +1,36 @@
import { Main } from './main';
export class Timer {
private _Main: Main;
startup: HTMLInputElement = document.querySelector('.ntsh-timer-startup');
shutdown: HTMLInputElement = document.querySelector('.ntsh-timer-shutdown');
constructor(Main: Main) {
this._Main = Main;
this.registerListeners();
}
update(data: {
start: { hour: number; minute: number };
end: { hour: number; minute: number };
}) {
const start = `${data.start.hour.toString().padStart(2, '0')}:${data.start.minute.toString().padStart(2, '0')}`;
const end = `${data.end.hour.toString().padStart(2, '0')}:${data.end.minute.toString().padStart(2, '0')}`;
this.startup.value = start;
this.shutdown.value = end;
}
registerListeners() {
this.startup.onchange = () => {
const [hour, minute] = this.startup.value.split(':').map(Number);
this._Main.socket.emit('setTimerStart', { hour, minute });
};
this.shutdown.onchange = () => {
const [hour, minute] = this.shutdown.value.split(':').map(Number);
this._Main.socket.emit('setTimerEnd', { hour, minute });
};
}
}