Added basic control panel
This commit is contained in:
41
frontend/views/control/ts/calibration.ts
Normal file
41
frontend/views/control/ts/calibration.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
146
frontend/views/control/ts/checklist.ts
Normal file
146
frontend/views/control/ts/checklist.ts
Normal 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 };
|
||||
}
|
||||
180
frontend/views/control/ts/main.ts
Normal file
180
frontend/views/control/ts/main.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
42
frontend/views/control/ts/menu.ts
Normal file
42
frontend/views/control/ts/menu.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
29
frontend/views/control/ts/outOfServiceMode.ts
Normal file
29
frontend/views/control/ts/outOfServiceMode.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
36
frontend/views/control/ts/timer.ts
Normal file
36
frontend/views/control/ts/timer.ts
Normal 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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user