From 42b7495f53e7806156c7a2ffd127ac281408acad Mon Sep 17 00:00:00 2001 From: Robson Oliveira dos Santos Date: Thu, 5 Dec 2024 21:09:39 +0930 Subject: [PATCH] feat(spie-ui): restructure app to add support to plotter view --- apps/spie-ui/eslint.config.js | 4 + apps/spie-ui/project.json | 6 +- apps/spie-ui/src/app/app.component.ts | 16 ++ apps/spie-ui/src/app/app.routes.ts | 10 +- .../connection-advanced-modal.component.html | 0 .../connection-advanced-modal.component.scss | 0 .../connection-advanced-modal.component.ts | 38 +-- .../connection/connection.component.html | 81 ++++++ .../connection/connection.component.scss | 0 .../connection/connection.component.ts | 85 +++--- .../components/plotter/plotter.component.html | 72 +++++ .../plotter/plotter.component.scss} | 0 .../components/plotter/plotter.component.ts | 247 ++++++++++++++++++ .../send-advanced-modal.component.html | 0 .../send-advanced-modal.component.scss | 0 .../send-advanced-modal.component.ts | 14 +- .../app/components/send/send.component.html | 45 ++++ .../send/send.component.scss | 0 .../send/send.component.ts | 26 +- .../terminal-advanced-modal.component.html | 0 .../terminal-advanced-modal.component.scss | 0 .../terminal-advanced-modal.component.ts | 37 +-- .../terminal/terminal.component.html | 73 ++++++ .../terminal/terminal.component.scss | 0 .../components/terminal/terminal.component.ts | 146 +++++++++++ .../update-modal/update-modal.component.html | 0 .../update-modal/update-modal.component.scss | 0 .../update-modal/update-modal.component.ts | 9 +- .../home/connection/connection.component.html | 73 ------ .../src/app/pages/home/home.component.html | 16 -- .../src/app/pages/home/home.component.ts | 30 --- .../spie-ui/src/app/pages/home/home.page.html | 17 ++ .../spie-ui/src/app/pages/home/home.page.scss | 0 apps/spie-ui/src/app/pages/home/home.page.ts | 28 ++ .../spie-ui/src/app/pages/home/home.routes.ts | 37 +++ .../app/pages/home/plotter/plotter.page.html | 9 + .../app/pages/home/plotter/plotter.page.scss | 0 .../app/pages/home/plotter/plotter.page.ts | 18 ++ .../app/pages/home/send/send.component.html | 41 --- .../home/terminal/terminal.component.html | 40 --- .../pages/home/terminal/terminal.component.ts | 73 ------ .../pages/home/terminal/terminal.page.html | 10 + .../pages/home/terminal/terminal.page.scss | 0 .../app/pages/home/terminal/terminal.page.ts | 26 ++ .../src/app/services/electron.service.ts | 12 +- .../src/app/services/serial-port.service.ts | 99 +++---- apps/spie/src/app/api/main.preload.ts | 2 + .../spie/src/app/events/serial-port.events.ts | 55 ++-- apps/spie/src/app/events/update.events.ts | 203 +++++++------- libs/types/src/lib/electron.d.ts | 12 +- package-lock.json | 93 +++++++ package.json | 2 + 52 files changed, 1245 insertions(+), 560 deletions(-) rename apps/spie-ui/src/app/{pages/home/connection => components}/connection-advanced-modal/connection-advanced-modal.component.html (100%) rename apps/spie-ui/src/app/{pages/home/connection => components}/connection-advanced-modal/connection-advanced-modal.component.scss (100%) rename apps/spie-ui/src/app/{pages/home/connection => components}/connection-advanced-modal/connection-advanced-modal.component.ts (75%) create mode 100644 apps/spie-ui/src/app/components/connection/connection.component.html rename apps/spie-ui/src/app/{pages/home => components}/connection/connection.component.scss (100%) rename apps/spie-ui/src/app/{pages/home => components}/connection/connection.component.ts (66%) create mode 100644 apps/spie-ui/src/app/components/plotter/plotter.component.html rename apps/spie-ui/src/app/{pages/home/home.component.scss => components/plotter/plotter.component.scss} (100%) create mode 100644 apps/spie-ui/src/app/components/plotter/plotter.component.ts rename apps/spie-ui/src/app/{pages/home/send => components}/send-advanced-modal/send-advanced-modal.component.html (100%) rename apps/spie-ui/src/app/{pages/home/send => components}/send-advanced-modal/send-advanced-modal.component.scss (100%) rename apps/spie-ui/src/app/{pages/home/send => components}/send-advanced-modal/send-advanced-modal.component.ts (76%) create mode 100644 apps/spie-ui/src/app/components/send/send.component.html rename apps/spie-ui/src/app/{pages/home => components}/send/send.component.scss (100%) rename apps/spie-ui/src/app/{pages/home => components}/send/send.component.ts (83%) rename apps/spie-ui/src/app/{pages/home/terminal => components}/terminal-advanced-modal/terminal-advanced-modal.component.html (100%) rename apps/spie-ui/src/app/{pages/home/terminal => components}/terminal-advanced-modal/terminal-advanced-modal.component.scss (100%) rename apps/spie-ui/src/app/{pages/home/terminal => components}/terminal-advanced-modal/terminal-advanced-modal.component.ts (65%) create mode 100644 apps/spie-ui/src/app/components/terminal/terminal.component.html rename apps/spie-ui/src/app/{pages/home => components}/terminal/terminal.component.scss (100%) create mode 100644 apps/spie-ui/src/app/components/terminal/terminal.component.ts rename apps/spie-ui/src/app/{pages/home => components}/update-modal/update-modal.component.html (100%) rename apps/spie-ui/src/app/{pages/home => components}/update-modal/update-modal.component.scss (100%) rename apps/spie-ui/src/app/{pages/home => components}/update-modal/update-modal.component.ts (94%) delete mode 100644 apps/spie-ui/src/app/pages/home/connection/connection.component.html delete mode 100644 apps/spie-ui/src/app/pages/home/home.component.html delete mode 100644 apps/spie-ui/src/app/pages/home/home.component.ts create mode 100644 apps/spie-ui/src/app/pages/home/home.page.html create mode 100644 apps/spie-ui/src/app/pages/home/home.page.scss create mode 100644 apps/spie-ui/src/app/pages/home/home.page.ts create mode 100644 apps/spie-ui/src/app/pages/home/home.routes.ts create mode 100644 apps/spie-ui/src/app/pages/home/plotter/plotter.page.html create mode 100644 apps/spie-ui/src/app/pages/home/plotter/plotter.page.scss create mode 100644 apps/spie-ui/src/app/pages/home/plotter/plotter.page.ts delete mode 100644 apps/spie-ui/src/app/pages/home/send/send.component.html delete mode 100644 apps/spie-ui/src/app/pages/home/terminal/terminal.component.html delete mode 100644 apps/spie-ui/src/app/pages/home/terminal/terminal.component.ts create mode 100644 apps/spie-ui/src/app/pages/home/terminal/terminal.page.html create mode 100644 apps/spie-ui/src/app/pages/home/terminal/terminal.page.scss create mode 100644 apps/spie-ui/src/app/pages/home/terminal/terminal.page.ts diff --git a/apps/spie-ui/eslint.config.js b/apps/spie-ui/eslint.config.js index e8fffd9..4afc7f5 100644 --- a/apps/spie-ui/eslint.config.js +++ b/apps/spie-ui/eslint.config.js @@ -25,6 +25,10 @@ module.exports = [ style: 'kebab-case', }, ], + '@angular-eslint/component-class-suffix': [ + 'error', + { suffixes: ['Page', 'Component'] }, + ], }, }, { diff --git a/apps/spie-ui/project.json b/apps/spie-ui/project.json index 9bfe4bf..8779f1b 100644 --- a/apps/spie-ui/project.json +++ b/apps/spie-ui/project.json @@ -27,15 +27,15 @@ "apps/spie-ui/src/global.scss", "apps/spie-ui/src/theme/variables.scss" ], - "scripts": [] + "scripts": ["node_modules/apexcharts/dist/apexcharts.min.js"] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "2mb", + "maximumError": "5mb" }, { "type": "anyComponentStyle", diff --git a/apps/spie-ui/src/app/app.component.ts b/apps/spie-ui/src/app/app.component.ts index 14a6860..eb568ac 100644 --- a/apps/spie-ui/src/app/app.component.ts +++ b/apps/spie-ui/src/app/app.component.ts @@ -4,10 +4,18 @@ import { addIcons } from 'ionicons'; import { cloudUploadOutline, documentOutline, + logInOutline, + logOutOutline, + pauseOutline, + playOutline, + pulseOutline, + sendOutline, settingsOutline, speedometerOutline, statsChartOutline, + terminalOutline, timeOutline, + trashOutline, } from 'ionicons/icons'; @Component({ @@ -21,9 +29,17 @@ export class AppComponent { constructor() { addIcons({ cloudUploadOutline }); addIcons({ documentOutline }); + addIcons({ logInOutline }); + addIcons({ logOutOutline }); + addIcons({ pauseOutline }); + addIcons({ playOutline }); + addIcons({ pulseOutline }); + addIcons({ sendOutline }); addIcons({ settingsOutline }); addIcons({ speedometerOutline }); addIcons({ statsChartOutline }); + addIcons({ terminalOutline }); addIcons({ timeOutline }); + addIcons({ trashOutline }); } } diff --git a/apps/spie-ui/src/app/app.routes.ts b/apps/spie-ui/src/app/app.routes.ts index a40ff91..fb9e7fb 100644 --- a/apps/spie-ui/src/app/app.routes.ts +++ b/apps/spie-ui/src/app/app.routes.ts @@ -1,15 +1,9 @@ import { type Routes } from '@angular/router'; -import { HomeComponent } from './pages/home/home.component'; - export const routes: Routes = [ { path: '', - component: HomeComponent, - }, - { - path: '**', - redirectTo: '', - pathMatch: 'full', + loadChildren: () => + import('./pages/home/home.routes').then((m) => m.routes), }, ]; diff --git a/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.html b/apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.html similarity index 100% rename from apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.html rename to apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.html diff --git a/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.scss b/apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.scss rename to apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.scss diff --git a/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts b/apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.ts similarity index 75% rename from apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts rename to apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.ts index 7ec5e93..2df966b 100644 --- a/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts +++ b/apps/spie-ui/src/app/components/connection-advanced-modal/connection-advanced-modal.component.ts @@ -18,11 +18,11 @@ import { type Subject } from 'rxjs'; import { type CheckboxCustomEvent, type SelectCustomEvent, -} from '../../../../interfaces/ionic.interface'; -import { SerialPortService } from '../../../../services/serial-port.service'; +} from '../../interfaces/ionic.interface'; +import { SerialPortService } from '../../services/serial-port.service'; @Component({ - selector: 'app-connection-advanced-modal', + selector: 'app-connection-advanced-modal-component', templateUrl: 'connection-advanced-modal.component.html', styleUrls: ['./connection-advanced-modal.component.scss'], standalone: true, @@ -53,8 +53,8 @@ export class ConnectionAdvancedComponent { onChangeDataBits(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, dataBits: parseInt(selectedOption, 10) as 5 | 6 | 7 | 8, })); @@ -63,8 +63,8 @@ export class ConnectionAdvancedComponent { onChangeStopBits(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, stopBits: parseFloat(selectedOption) as 1 | 1.5 | 2, })); @@ -73,8 +73,8 @@ export class ConnectionAdvancedComponent { onChangeParity(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, parity: selectedOption, })); @@ -83,8 +83,8 @@ export class ConnectionAdvancedComponent { onChangeRtscts(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, rtscts: selectedOption, })); @@ -93,8 +93,8 @@ export class ConnectionAdvancedComponent { onChangeXon(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, xon: selectedOption, })); @@ -103,8 +103,8 @@ export class ConnectionAdvancedComponent { onChangeXoff(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, xoff: selectedOption, })); @@ -113,8 +113,8 @@ export class ConnectionAdvancedComponent { onChangeXany(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, xany: selectedOption, })); @@ -123,8 +123,8 @@ export class ConnectionAdvancedComponent { onChangeHupcl(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, hupcl: selectedOption, })); diff --git a/apps/spie-ui/src/app/components/connection/connection.component.html b/apps/spie-ui/src/app/components/connection/connection.component.html new file mode 100644 index 0000000..4cbb4ae --- /dev/null +++ b/apps/spie-ui/src/app/components/connection/connection.component.html @@ -0,0 +1,81 @@ + + + + Connection + + + + + + + + + + @for (serialPort of serialPorts(); track $index) { + + {{ serialPort.path }} - {{ serialPort.manufacturer }} + + } + + + + + + + + @for (baudRate of baudRates; track $index) { + {{ + baudRate + }} + } + + + + + + + + + + {{ isOpen() ? 'Disconnect' : 'Connect' }} + + + + + + + + + + + + + + + diff --git a/apps/spie-ui/src/app/pages/home/connection/connection.component.scss b/apps/spie-ui/src/app/components/connection/connection.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/connection/connection.component.scss rename to apps/spie-ui/src/app/components/connection/connection.component.scss diff --git a/apps/spie-ui/src/app/pages/home/connection/connection.component.ts b/apps/spie-ui/src/app/components/connection/connection.component.ts similarity index 66% rename from apps/spie-ui/src/app/pages/home/connection/connection.component.ts rename to apps/spie-ui/src/app/components/connection/connection.component.ts index f1ee258..13d312f 100644 --- a/apps/spie-ui/src/app/pages/home/connection/connection.component.ts +++ b/apps/spie-ui/src/app/components/connection/connection.component.ts @@ -1,45 +1,49 @@ import { Component, inject, signal, viewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { + IonAccordion, + IonAccordionGroup, IonButton, IonCard, - IonCardHeader, + IonCardContent, IonCol, IonGrid, IonIcon, IonItem, + IonLabel, IonRow, IonSelect, IonSelectOption, - IonText, LoadingController, } from '@ionic/angular/standalone'; import { type PortInfo } from '@serialport/bindings-interface'; import { Subject, tap } from 'rxjs'; -import { ConnectionAdvancedComponent } from './connection-advanced-modal/connection-advanced-modal.component'; -import { type SelectCustomEvent } from '../../../interfaces/ionic.interface'; -import { ElectronService } from '../../../services/electron.service'; -import { SerialPortService } from '../../../services/serial-port.service'; -import { ToasterService } from '../../../services/toaster.service'; +import { type SelectCustomEvent } from '../../interfaces/ionic.interface'; +import { ElectronService } from '../../services/electron.service'; +import { SerialPortService } from '../../services/serial-port.service'; +import { ToasterService } from '../../services/toaster.service'; +import { ConnectionAdvancedComponent } from '../connection-advanced-modal/connection-advanced-modal.component'; @Component({ - selector: 'app-connection', - templateUrl: './connection.component.html', + selector: 'app-connection-component', + templateUrl: 'connection.component.html', styleUrls: ['./connection.component.scss'], standalone: true, imports: [ + IonAccordion, + IonAccordionGroup, IonButton, IonCard, - IonCardHeader, + IonCardContent, IonCol, IonGrid, IonIcon, IonItem, + IonLabel, IonRow, IonSelect, IonSelectOption, - IonText, ConnectionAdvancedComponent, ], }) @@ -53,7 +57,6 @@ export class ConnectionComponent { this.reconnectSubject .pipe( takeUntilDestroyed(), - tap(async () => { if (this.isOpen()) { const loading = await this.loadingController.create(); @@ -61,10 +64,7 @@ export class ConnectionComponent { try { await this.electronService.serialPort.close(); await this.electronService.serialPort.open(this.openOptions()); - this.serialPortService.clearDataSubject.next({ - event: 'data', - data: '', - }); + this.serialPortService.clearDataSubject.next(); } catch (error) { await this.toasterService.presentErrorToast(error); } @@ -73,6 +73,32 @@ export class ConnectionComponent { }) ) .subscribe(); + + // Retrieve previously connected serial port (useful for development) + this.electronService.serialPort + .getOpenOptions() + .then((openOptions) => { + const connectedSerialPortPath = openOptions?.path; + if (!connectedSerialPortPath) { + return; + } + + return this.electronService.serialPort.list().then((serialPorts) => { + const isSerialPortInList = serialPorts?.some( + (serialPort) => serialPort.path === openOptions.path + ); + + if (!isSerialPortInList) { + return; + } + + this.openOptions.set(openOptions); + this.serialPorts.set(serialPorts); + }); + }) + .catch((error) => { + console.error('Error initializing serial ports:', error); // TODO: Toaster? + }); } isOpen = this.serialPortService.isOpen; @@ -117,41 +143,32 @@ export class ConnectionComponent { onChangeSerialPort(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, path: selectedOption, })); } onChangeBaudRate(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.openOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.openOptions.update((openOptions) => ({ + ...openOptions, baudRate: parseInt(selectedOption, 10), })); this.reconnectSubject.next(); } - async onClickDisconnect(): Promise { - const loading = await this.loadingController.create(); - await loading.present(); - - try { - await this.electronService.serialPort.close(); - } catch (error) { - await this.toasterService.presentErrorToast(error); - } - - await loading.dismiss(); - } - async onClickConnect(): Promise { const loading = await this.loadingController.create(); await loading.present(); try { - await this.electronService.serialPort.open(this.openOptions()); + if (this.isOpen()) { + await this.electronService.serialPort.close(); + } else { + await this.electronService.serialPort.open(this.openOptions()); + } } catch (error) { await this.toasterService.presentErrorToast(error); } diff --git a/apps/spie-ui/src/app/components/plotter/plotter.component.html b/apps/spie-ui/src/app/components/plotter/plotter.component.html new file mode 100644 index 0000000..bf2fa3d --- /dev/null +++ b/apps/spie-ui/src/app/components/plotter/plotter.component.html @@ -0,0 +1,72 @@ + + + Plotter + + + + + + + + + + + {{ + isDataEventPausedSubject.getValue() && isOpen() + ? 'Continue' + : 'Pause' + }} + + + + + + + Clear + + + + + + + + + + + + + diff --git a/apps/spie-ui/src/app/pages/home/home.component.scss b/apps/spie-ui/src/app/components/plotter/plotter.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/home.component.scss rename to apps/spie-ui/src/app/components/plotter/plotter.component.scss diff --git a/apps/spie-ui/src/app/components/plotter/plotter.component.ts b/apps/spie-ui/src/app/components/plotter/plotter.component.ts new file mode 100644 index 0000000..9ce091c --- /dev/null +++ b/apps/spie-ui/src/app/components/plotter/plotter.component.ts @@ -0,0 +1,247 @@ +import { Component, computed, inject, signal, viewChild } from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCol, + IonGrid, + IonIcon, + IonRow, + IonText, +} from '@ionic/angular/standalone'; +import { + type ApexChart, + type ApexDataLabels, + type ApexGrid, + type ApexStroke, + type ApexTooltip, + type ApexXAxis, + type ApexYAxis, + type ChartComponent, + NgApexchartsModule, +} from 'ng-apexcharts'; +import { BehaviorSubject, Subject, filter, map, merge, tap } from 'rxjs'; + +import { + type DataEvent, + SerialPortService, +} from '../../services/serial-port.service'; + +interface ChartOptions { + dataLabels: ApexDataLabels; + yaxis: ApexYAxis; + xaxis: ApexXAxis; + grid: ApexGrid; + stroke: ApexStroke; +} + +@Component({ + selector: 'app-plotter-component', + templateUrl: 'plotter.component.html', + styleUrls: ['./plotter.component.scss'], + standalone: true, + imports: [ + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCol, + IonGrid, + IonIcon, + IonRow, + IonText, + NgApexchartsModule, + ], +}) +export class PlotterComponent { + private readonly serialPortService = inject(SerialPortService); + + constructor() { + this.dataEvent$.subscribe(); + } + + chartArea = viewChild.required('chartObj'); + + clearSeriesSubject = new Subject(); + isOpen = this.serialPortService.isOpen; + private dataEvent$ = merge( + this.serialPortService.dataEvent$.pipe( + filter(() => !this.isDataEventPausedSubject.getValue()) + ), + this.clearSeriesSubject.pipe(map(() => ({ type: 'clear' } as DataEvent))) + ).pipe( + tap((dataEvent) => { + if (dataEvent.type === 'clear') { + // Clear series + this.series.set([]); + return; + } + + const data = dataEvent.data; + const isDataTruncated = data.split('\n').length - 1 > 1; + if (isDataTruncated) { + console.warn('data truncated:'); + return; + } + + // Detect separator + const detectedSeparator = this.detectSeparator(data); + + // Split values + const values = detectedSeparator + ? data.split(detectedSeparator).map((v) => parseFloat(v)) + : [parseFloat(data)]; + + // Update series with the correct amount of variables + if (this.series().length !== values.length) { + const newSeries = values.map((value, index) => ({ + name: `Variable ${index + 1}`, + data: [{ x: Date.now(), y: value }], + })); + + this.series.set(newSeries); + return; + } + + let variableData: { x: number; y: number }[][] = []; + + // Initialize variableData for the first time based on the number of variables + if (variableData.length === 0) { + variableData = Array.from({ length: values.length }, () => []); + } + + // Populate data points for each variable + values.forEach((value, variableIndex) => { + variableData[variableIndex].push({ + x: Date.now(), + y: value, + }); + }); + + // TODO: plotter options scrollbackLength + const scrollbackLength = 1000; + + // Slice series based on scrollbackLength + if (this.series()[0].data.length > scrollbackLength) { + this.series.update((series) => { + return series.map((variable) => { + const data = variable.data as { x: any; y: any }[]; + const truncatedData = data.slice(1); + + return { ...variable, data: truncatedData }; + }); + }); + } + + // Update series + this.series.update((series) => { + return series.map((variable, index) => { + const data = variable.data as { x: any; y: any }[]; + const updatedData = [...data, variableData[index][0]]; + + return { ...variable, data: updatedData }; + }); + }); + }), + takeUntilDestroyed() + ); + series = signal([]); + + isDataEventPausedSubject = new BehaviorSubject(false); + private isDataEventPaused = toSignal(this.isDataEventPausedSubject, { + initialValue: false, + }); + + private detectSeparator(line: string): string { + if (line.includes('\t')) return '\t'; + if (line.includes(',')) return ','; + if (line.includes(' ')) return ' '; + return ''; + } + + chart = computed(() => { + return { + type: 'line', + animations: { + enabled: false, + }, + zoom: { + enabled: this.isDataEventPaused(), + }, + }; + }); + + tooltip = computed(() => { + return { + enabled: this.isDataEventPaused(), + x: { + show: true, + // format: 'dd/MM/yy HH:mm:ss:fff', // milliseconds is not working here + formatter: (timestamp: number) => { + const date = new Date(timestamp); + + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = String(date.getFullYear()).slice(-2); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const milliseconds = String(date.getMilliseconds()).padStart(3, '0'); + + return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}:${milliseconds}`; + }, + }, + }; + }); + + chartOptions: ChartOptions = { + dataLabels: { + enabled: false, + }, + yaxis: { + // axisTicks: { + // show: false, + // }, + }, + xaxis: { + type: 'datetime', + // axisTicks: { + // show: false, + // }, + }, + grid: { + show: true, + strokeDashArray: 2, + xaxis: { + lines: { + show: true, + }, + }, + yaxis: { + lines: { + show: true, + }, + }, + }, + stroke: { + show: true, + curve: 'straight', + width: 2, + }, + }; + + onClickClearTerminal(): void { + this.clearSeriesSubject.next(); + } + + onClickPauseTerminal(): void { + const currentValue = this.isDataEventPausedSubject.getValue(); + this.isDataEventPausedSubject.next(!currentValue); + } + + async onClickTerminalAdvancedModal() { + // this.terminalAdvancedComponent().terminalAdvancedModal().present(); // TODO + } +} diff --git a/apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.html b/apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.html similarity index 100% rename from apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.html rename to apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.html diff --git a/apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.scss b/apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.scss rename to apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.scss diff --git a/apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.ts b/apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.ts similarity index 76% rename from apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.ts rename to apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.ts index 196ffa0..53518d0 100644 --- a/apps/spie-ui/src/app/pages/home/send/send-advanced-modal/send-advanced-modal.component.ts +++ b/apps/spie-ui/src/app/components/send-advanced-modal/send-advanced-modal.component.ts @@ -14,11 +14,11 @@ import { } from '@ionic/angular/standalone'; import { type Delimiter, type Encoding } from '@spie/types'; -import { type SelectCustomEvent } from '../../../../interfaces/ionic.interface'; -import { SerialPortService } from '../../../../services/serial-port.service'; +import { type SelectCustomEvent } from '../../interfaces/ionic.interface'; +import { SerialPortService } from '../../services/serial-port.service'; @Component({ - selector: 'app-send-advanced-modal', + selector: 'app-send-advanced-modal-component', templateUrl: 'send-advanced-modal.component.html', styleUrls: ['./send-advanced-modal.component.scss'], standalone: true, @@ -45,16 +45,16 @@ export class SendAdvancedComponent { onChangeSendEncoding(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.sendOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.sendOptions.update((sendOptions) => ({ + ...sendOptions, encoding: selectedOption, })); } onChangeDelimiter(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.sendOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.sendOptions.update((sendOptions) => ({ + ...sendOptions, delimiter: selectedOption, })); } diff --git a/apps/spie-ui/src/app/components/send/send.component.html b/apps/spie-ui/src/app/components/send/send.component.html new file mode 100644 index 0000000..ca3b94f --- /dev/null +++ b/apps/spie-ui/src/app/components/send/send.component.html @@ -0,0 +1,45 @@ + + + Send + + + + + + + + + + + + + Clear Send + + + + + + + + + + + + + diff --git a/apps/spie-ui/src/app/pages/home/send/send.component.scss b/apps/spie-ui/src/app/components/send/send.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/send/send.component.scss rename to apps/spie-ui/src/app/components/send/send.component.scss diff --git a/apps/spie-ui/src/app/pages/home/send/send.component.ts b/apps/spie-ui/src/app/components/send/send.component.ts similarity index 83% rename from apps/spie-ui/src/app/pages/home/send/send.component.ts rename to apps/spie-ui/src/app/components/send/send.component.ts index 85b9de1..8e81568 100644 --- a/apps/spie-ui/src/app/pages/home/send/send.component.ts +++ b/apps/spie-ui/src/app/components/send/send.component.ts @@ -3,6 +3,7 @@ import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { IonButton, IonCard, + IonCardContent, IonCardHeader, IonCol, IonGrid, @@ -15,20 +16,21 @@ import { import { type Delimiter } from '@spie/types'; import { scan } from 'rxjs'; -import { SendAdvancedComponent } from './send-advanced-modal/send-advanced-modal.component'; -import { type IonInputCustomEvent } from '../../../interfaces/ionic.interface'; -import { ElectronService } from '../../../services/electron.service'; -import { SerialPortService } from '../../../services/serial-port.service'; -import { ToasterService } from '../../../services/toaster.service'; +import { type IonInputCustomEvent } from '../../interfaces/ionic.interface'; +import { ElectronService } from '../../services/electron.service'; +import { SerialPortService } from '../../services/serial-port.service'; +import { ToasterService } from '../../services/toaster.service'; +import { SendAdvancedComponent } from '../send-advanced-modal/send-advanced-modal.component'; @Component({ - selector: 'app-send', + selector: 'app-send-component', templateUrl: 'send.component.html', styleUrls: ['./send.component.scss'], standalone: true, imports: [ IonButton, IonCard, + IonCardContent, IonCardHeader, IonCol, IonGrid, @@ -106,16 +108,16 @@ export class SendComponent { onChangeSendInput(event: IonInputCustomEvent): void { const inputValue = event.detail.value; if (!inputValue) { - this.sendOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.sendOptions.update((sendOptions) => ({ + ...sendOptions, isSendInputValid: false, })); return; } if (this.sendOptions().encoding !== 'hex') { - this.sendOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.sendOptions.update((sendOptions) => ({ + ...sendOptions, isSendInputValid: true, })); return; @@ -129,8 +131,8 @@ export class SendComponent { ?.join(' ') ?? ''; event.target.value = formattedHexValue; const isEvenLength = formattedHexValue.replace(/\s+/g, '').length % 2 === 0; - this.sendOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.sendOptions.update((sendOptions) => ({ + ...sendOptions, isSendInputValid: isEvenLength, })); } diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.html b/apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.html similarity index 100% rename from apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.html rename to apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.html diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.scss b/apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.scss rename to apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.scss diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.ts b/apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.ts similarity index 65% rename from apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.ts rename to apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.ts index 301a92d..8315442 100644 --- a/apps/spie-ui/src/app/pages/home/terminal/terminal-advanced-modal/terminal-advanced-modal.component.ts +++ b/apps/spie-ui/src/app/components/terminal-advanced-modal/terminal-advanced-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, viewChild } from '@angular/core'; +import { Component, inject, input, viewChild } from '@angular/core'; import { IonButton, IonButtons, @@ -15,17 +15,18 @@ import { IonToolbar, } from '@ionic/angular/standalone'; import { type Encoding } from '@spie/types'; +import { type Subject } from 'rxjs'; import { type CheckboxCustomEvent, type RangeCustomEvent, type SelectCustomEvent, -} from '../../../../interfaces/ionic.interface'; -import { ElectronService } from '../../../../services/electron.service'; -import { SerialPortService } from '../../../../services/serial-port.service'; +} from '../../interfaces/ionic.interface'; +import { ElectronService } from '../../services/electron.service'; +import { SerialPortService } from '../../services/serial-port.service'; @Component({ - selector: 'app-terminal-advanced-modal', + selector: 'app-terminal-advanced-modal-component', templateUrl: 'terminal-advanced-modal.component.html', styleUrls: ['./terminal-advanced-modal.component.scss'], standalone: true, @@ -49,49 +50,49 @@ export class TerminalAdvancedComponent { private readonly electronService = inject(ElectronService); private readonly serialPortService = inject(SerialPortService); - clearDataSubject = this.serialPortService.clearDataSubject; + clearTerminalSubject = input.required>(); terminalOptions = this.serialPortService.terminalOptions; terminalAdvancedModal = viewChild.required('terminalAdvancedModal'); onChangeTerminalEncoding(event: SelectCustomEvent): void { const selectedOption = event.detail.value; - this.terminalOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.terminalOptions.update((terminalOptions) => ({ + ...terminalOptions, encoding: selectedOption, })); this.electronService.serialPort.setReadEncoding(selectedOption); - this.clearDataSubject.next({ event: 'data', data: '' }); + this.clearTerminalSubject().next(); } onChangeShowTimestamps(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.terminalOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.terminalOptions.update((terminalOptions) => ({ + ...terminalOptions, showTimestampsEnabled: selectedOption, })); - this.clearDataSubject.next({ event: 'data', data: '' }); + this.clearTerminalSubject().next(); } onChangeAutoScroll(event: CheckboxCustomEvent): void { const selectedOption = event.detail.checked; - this.terminalOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.terminalOptions.update((terminalOptions) => ({ + ...terminalOptions, isAutoScrollEnabled: selectedOption, })); - // this.clearDataSubject.next({ event: 'data', data: '' }); + // this.clearTerminalSubject().next(); } onScrollbackLength(event: RangeCustomEvent): void { const selectedOption = event.detail.value as number; - this.terminalOptions.update((currentOpenOptions) => ({ - ...currentOpenOptions, + this.terminalOptions.update((terminalOptions) => ({ + ...terminalOptions, scrollbackLength: selectedOption, })); - // this.clearDataSubject.next({ event: 'data', data: '' }); + // this.clearTerminalSubject().next(); } pinFormatter(value: number): string { diff --git a/apps/spie-ui/src/app/components/terminal/terminal.component.html b/apps/spie-ui/src/app/components/terminal/terminal.component.html new file mode 100644 index 0000000..2e67c45 --- /dev/null +++ b/apps/spie-ui/src/app/components/terminal/terminal.component.html @@ -0,0 +1,73 @@ + + + Terminal + + + + + + + + + + + + + {{ + isDataEventPausedSubject.getValue() && isOpen() + ? 'Continue' + : 'Pause' + }} + + + + + + + Clear + + + + + + + + + + + + + diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal.component.scss b/apps/spie-ui/src/app/components/terminal/terminal.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/terminal/terminal.component.scss rename to apps/spie-ui/src/app/components/terminal/terminal.component.scss diff --git a/apps/spie-ui/src/app/components/terminal/terminal.component.ts b/apps/spie-ui/src/app/components/terminal/terminal.component.ts new file mode 100644 index 0000000..811c375 --- /dev/null +++ b/apps/spie-ui/src/app/components/terminal/terminal.component.ts @@ -0,0 +1,146 @@ +import { Component, inject, signal, viewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCol, + IonGrid, + IonIcon, + IonItem, + IonRow, + IonText, + IonTextarea, +} from '@ionic/angular/standalone'; +import { BehaviorSubject, Subject, filter, map, merge, tap } from 'rxjs'; + +import { ElectronService } from '../../services/electron.service'; +import { + type DataEvent, + SerialPortService, +} from '../../services/serial-port.service'; +import { TerminalAdvancedComponent } from '../terminal-advanced-modal/terminal-advanced-modal.component'; + +@Component({ + selector: 'app-terminal-component', + templateUrl: 'terminal.component.html', + styleUrls: ['./terminal.component.scss'], + standalone: true, + imports: [ + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCol, + IonGrid, + IonIcon, + IonItem, + IonRow, + IonText, + IonTextarea, + TerminalAdvancedComponent, + ], +}) +export class TerminalComponent { + private readonly serialPortService = inject(SerialPortService); + private readonly electronService = inject(ElectronService); + + constructor() { + // Retrieve previous readEncoding (useful for development) + this.electronService.serialPort.getReadEncoding().then((readEncoding) => { + this.terminalOptions.update((terminalOptions) => ({ + ...terminalOptions, + encoding: readEncoding, + })); + }); + + this.dataEvent$.subscribe(); + } + + clearTerminalSubject = new Subject(); + isOpen = this.serialPortService.isOpen; + terminalOptions = this.serialPortService.terminalOptions; + private dataEvent$ = merge( + this.serialPortService.dataEvent$.pipe( + filter(() => !this.isDataEventPausedSubject.getValue()) + ), + this.clearTerminalSubject.pipe(map(() => ({ type: 'clear' } as DataEvent))) + ).pipe( + tap(async (dataEvent) => { + if (dataEvent.type === 'clear') { + this.data.set(''); + return; + } + + const data = dataEvent.data; + const isDataTruncated = data.split('\n').length - 1 > 1; + if (isDataTruncated) { + console.warn('data truncated:'); + return; + } + + this.data.update((prevData) => { + // Append timestamp if it is enabled + if (this.terminalOptions().showTimestampsEnabled) { + const date = new Date(); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + + prevData += `[${hours}:${minutes}:${seconds}] `; + } + + // Append data + prevData += `${data}`; + + // TODO: evaluate if this should also be done for ascii + // Append new line if hex encoding + if (this.terminalOptions().encoding === 'hex') { + prevData += '\n'; + } + + const excess = + prevData.length - this.terminalOptions().scrollbackLength * 10000; + + if (excess > 0) { + return prevData.slice(excess); + } + + return prevData; + }); + + // Apply autoscroll + const isAutoScrollEnabled = this.terminalOptions().isAutoScrollEnabled; + if (isAutoScrollEnabled) { + const terminalTextArea = this.terminalTextArea(); + const textarea = await terminalTextArea.getInputElement(); + textarea.scrollTo({ + top: textarea.scrollHeight, + behavior: 'instant', + }); + } + }), + takeUntilDestroyed() + ); + data = signal(''); + isDataEventPausedSubject = new BehaviorSubject(false); + + terminalTextArea = viewChild.required('terminalTextArea'); + private terminalAdvancedComponent = viewChild.required( + TerminalAdvancedComponent + ); + + onClickClearTerminal(): void { + this.clearTerminalSubject.next(); + } + + onClickPauseTerminal(): void { + const currentValue = this.isDataEventPausedSubject.getValue(); + this.isDataEventPausedSubject.next(!currentValue); + } + + async onClickTerminalAdvancedModal() { + this.terminalAdvancedComponent().terminalAdvancedModal().present(); + } +} diff --git a/apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.html b/apps/spie-ui/src/app/components/update-modal/update-modal.component.html similarity index 100% rename from apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.html rename to apps/spie-ui/src/app/components/update-modal/update-modal.component.html diff --git a/apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.scss b/apps/spie-ui/src/app/components/update-modal/update-modal.component.scss similarity index 100% rename from apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.scss rename to apps/spie-ui/src/app/components/update-modal/update-modal.component.scss diff --git a/apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.ts b/apps/spie-ui/src/app/components/update-modal/update-modal.component.ts similarity index 94% rename from apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.ts rename to apps/spie-ui/src/app/components/update-modal/update-modal.component.ts index 012a5f8..1a3250e 100644 --- a/apps/spie-ui/src/app/pages/home/update-modal/update-modal.component.ts +++ b/apps/spie-ui/src/app/components/update-modal/update-modal.component.ts @@ -17,14 +17,15 @@ import { IonTitle, IonToolbar, } from '@ionic/angular/standalone'; +import { type ProgressInfo } from 'electron-updater'; import { of, switchMap, tap } from 'rxjs'; -import { ElectronService } from '../../../services/electron.service'; -import { ToasterService } from '../../../services/toaster.service'; +import { ElectronService } from '../../services/electron.service'; +import { ToasterService } from '../../services/toaster.service'; @Component({ selector: 'app-update-modal', - templateUrl: './update-modal.component.html', + templateUrl: 'update-modal.component.html', styleUrls: ['./update-modal.component.scss'], standalone: true, imports: [ @@ -133,7 +134,7 @@ export class UpdateModalComponent { transferred: 0, percent: 0, bytesPerSecond: 0, - }, + } as ProgressInfo, } ); diff --git a/apps/spie-ui/src/app/pages/home/connection/connection.component.html b/apps/spie-ui/src/app/pages/home/connection/connection.component.html deleted file mode 100644 index 81dd7ee..0000000 --- a/apps/spie-ui/src/app/pages/home/connection/connection.component.html +++ /dev/null @@ -1,73 +0,0 @@ - - - Connection - - - - - - - - @for (serialPort of serialPorts(); track $index) { - - {{ serialPort.path }} - {{ serialPort.manufacturer }} - - } - - - - - - - - @for (baudRate of baudRates; track $index) { - {{ - baudRate - }} - } - - - - - - - - @if (isOpen()) { - Disconnect - } @else { - Connect - } - - - - - - - - - - - diff --git a/apps/spie-ui/src/app/pages/home/home.component.html b/apps/spie-ui/src/app/pages/home/home.component.html deleted file mode 100644 index 4fa7c2f..0000000 --- a/apps/spie-ui/src/app/pages/home/home.component.html +++ /dev/null @@ -1,16 +0,0 @@ - - - Serial Monitor - - - - - - - - - - - diff --git a/apps/spie-ui/src/app/pages/home/home.component.ts b/apps/spie-ui/src/app/pages/home/home.component.ts deleted file mode 100644 index a204ca2..0000000 --- a/apps/spie-ui/src/app/pages/home/home.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component } from '@angular/core'; -import { - IonContent, - IonHeader, - IonTitle, - IonToolbar, -} from '@ionic/angular/standalone'; - -import { ConnectionComponent } from './connection/connection.component'; -import { SendComponent } from './send/send.component'; -import { TerminalComponent } from './terminal/terminal.component'; -import { UpdateModalComponent } from './update-modal/update-modal.component'; - -@Component({ - selector: 'app-home', - templateUrl: 'home.component.html', - styleUrls: ['./home.component.scss'], - standalone: true, - imports: [ - IonContent, - IonHeader, - IonTitle, - IonToolbar, - SendComponent, - ConnectionComponent, - TerminalComponent, - UpdateModalComponent, - ], -}) -export class HomeComponent {} diff --git a/apps/spie-ui/src/app/pages/home/home.page.html b/apps/spie-ui/src/app/pages/home/home.page.html new file mode 100644 index 0000000..b971d87 --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/home.page.html @@ -0,0 +1,17 @@ + + + + + + + Terminal + + + + + Plotter + + + + + diff --git a/apps/spie-ui/src/app/pages/home/home.page.scss b/apps/spie-ui/src/app/pages/home/home.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/spie-ui/src/app/pages/home/home.page.ts b/apps/spie-ui/src/app/pages/home/home.page.ts new file mode 100644 index 0000000..2de879f --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/home.page.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { + IonIcon, + IonLabel, + IonTabBar, + IonTabButton, + IonTabs, +} from '@ionic/angular/standalone'; + +import { ConnectionComponent } from '../../components/connection/connection.component'; +import { UpdateModalComponent } from '../../components/update-modal/update-modal.component'; + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['./home.page.scss'], + standalone: true, + imports: [ + IonIcon, + IonLabel, + IonTabBar, + IonTabButton, + IonTabs, + ConnectionComponent, + UpdateModalComponent, + ], +}) +export class HomePage {} diff --git a/apps/spie-ui/src/app/pages/home/home.routes.ts b/apps/spie-ui/src/app/pages/home/home.routes.ts new file mode 100644 index 0000000..529acee --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/home.routes.ts @@ -0,0 +1,37 @@ +import { type Routes } from '@angular/router'; + +import { HomePage } from './home.page'; + +export const routes: Routes = [ + { + path: '', + component: HomePage, + children: [ + { + path: 'terminal', + loadComponent: () => + import('./terminal/terminal.page').then((m) => m.TerminalPage), + }, + { + path: 'plotter', + loadComponent: () => + import('./plotter/plotter.page').then((m) => m.PlotterPage), + }, + { + path: '', + redirectTo: 'terminal', + pathMatch: 'full', + }, + { + path: '**', + redirectTo: '', + pathMatch: 'full', + }, + ], + }, + { + path: '**', + redirectTo: '', + pathMatch: 'full', + }, +]; diff --git a/apps/spie-ui/src/app/pages/home/plotter/plotter.page.html b/apps/spie-ui/src/app/pages/home/plotter/plotter.page.html new file mode 100644 index 0000000..fb316d5 --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/plotter/plotter.page.html @@ -0,0 +1,9 @@ + + + Plotter + + + + + + diff --git a/apps/spie-ui/src/app/pages/home/plotter/plotter.page.scss b/apps/spie-ui/src/app/pages/home/plotter/plotter.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/spie-ui/src/app/pages/home/plotter/plotter.page.ts b/apps/spie-ui/src/app/pages/home/plotter/plotter.page.ts new file mode 100644 index 0000000..388af27 --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/plotter/plotter.page.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { + IonContent, + IonHeader, + IonTitle, + IonToolbar, +} from '@ionic/angular/standalone'; + +import { PlotterComponent } from '../../../components/plotter/plotter.component'; + +@Component({ + selector: 'app-plotter', + templateUrl: 'plotter.page.html', + styleUrls: ['./plotter.page.scss'], + standalone: true, + imports: [IonContent, IonHeader, IonTitle, IonToolbar, PlotterComponent], +}) +export class PlotterPage {} diff --git a/apps/spie-ui/src/app/pages/home/send/send.component.html b/apps/spie-ui/src/app/pages/home/send/send.component.html deleted file mode 100644 index 7dd546e..0000000 --- a/apps/spie-ui/src/app/pages/home/send/send.component.html +++ /dev/null @@ -1,41 +0,0 @@ - - - Send - - - - - - - - - - Send - - - - - - - - - - - diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal.component.html b/apps/spie-ui/src/app/pages/home/terminal/terminal.component.html deleted file mode 100644 index f6b58c1..0000000 --- a/apps/spie-ui/src/app/pages/home/terminal/terminal.component.html +++ /dev/null @@ -1,40 +0,0 @@ - - - Terminal - - - - - - - - - - Clear Terminal - - - - - - - - - - - diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal.component.ts b/apps/spie-ui/src/app/pages/home/terminal/terminal.component.ts deleted file mode 100644 index 3ffe3a1..0000000 --- a/apps/spie-ui/src/app/pages/home/terminal/terminal.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Component, effect, inject, viewChild } from '@angular/core'; -import { - IonButton, - IonCard, - IonCardHeader, - IonCol, - IonGrid, - IonIcon, - IonItem, - IonRow, - IonText, - IonTextarea, -} from '@ionic/angular/standalone'; - -import { TerminalAdvancedComponent } from './terminal-advanced-modal/terminal-advanced-modal.component'; -import { SerialPortService } from '../../../services/serial-port.service'; - -@Component({ - selector: 'app-terminal', - templateUrl: 'terminal.component.html', - styleUrls: ['./terminal.component.scss'], - standalone: true, - imports: [ - IonButton, - IonCard, - IonCardHeader, - IonCol, - IonGrid, - IonIcon, - IonItem, - IonRow, - IonText, - IonTextarea, - TerminalAdvancedComponent, - ], -}) -export class TerminalComponent { - private readonly serialPortService = inject(SerialPortService); - - constructor() { - effect(async () => { - if (this.data() !== '') { - // Apply auto scroll - const isAutoScrollEnabled = this.terminalOptions().isAutoScrollEnabled; - if (isAutoScrollEnabled) { - const terminalTextArea = this.terminalTextArea(); - const textarea = await terminalTextArea.getInputElement(); - textarea.scrollTo({ - top: textarea.scrollHeight, - behavior: 'instant', - }); - } - } - }); - } - - clearDataSubject = this.serialPortService.clearDataSubject; - data = this.serialPortService.data; - terminalOptions = this.serialPortService.terminalOptions; - - terminalTextArea = viewChild.required('terminalTextArea'); - private terminalAdvancedComponent = viewChild.required( - TerminalAdvancedComponent - ); - - onClickClearTerminal(): void { - this.clearDataSubject.next({ event: 'data', data: '' }); - } - - async onClickTerminalAdvancedModal() { - this.terminalAdvancedComponent().terminalAdvancedModal().present(); - } -} diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal.page.html b/apps/spie-ui/src/app/pages/home/terminal/terminal.page.html new file mode 100644 index 0000000..9360848 --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/terminal/terminal.page.html @@ -0,0 +1,10 @@ + + + Terminal + + + + + + + diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal.page.scss b/apps/spie-ui/src/app/pages/home/terminal/terminal.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/spie-ui/src/app/pages/home/terminal/terminal.page.ts b/apps/spie-ui/src/app/pages/home/terminal/terminal.page.ts new file mode 100644 index 0000000..b8e2e16 --- /dev/null +++ b/apps/spie-ui/src/app/pages/home/terminal/terminal.page.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { + IonContent, + IonHeader, + IonTitle, + IonToolbar, +} from '@ionic/angular/standalone'; + +import { SendComponent } from '../../../components/send/send.component'; +import { TerminalComponent } from '../../../components/terminal/terminal.component'; + +@Component({ + selector: 'app-terminal', + templateUrl: 'terminal.page.html', + styleUrls: ['./terminal.page.scss'], + standalone: true, + imports: [ + IonContent, + IonHeader, + IonTitle, + IonToolbar, + SendComponent, + TerminalComponent, + ], +}) +export class TerminalPage {} diff --git a/apps/spie-ui/src/app/services/electron.service.ts b/apps/spie-ui/src/app/services/electron.service.ts index ea7473f..e265208 100644 --- a/apps/spie-ui/src/app/services/electron.service.ts +++ b/apps/spie-ui/src/app/services/electron.service.ts @@ -14,9 +14,7 @@ import { Observable } from 'rxjs'; providedIn: 'root', }) export class ElectronService { - getPlatform(): string { - return window.electron.platform; - } + platform = window.electron.platform; quit(code = 0): void { window.electron.quit(code); @@ -79,6 +77,14 @@ export class ElectronService { return window.electron.serialPort.setReadEncoding(encoding); } + getReadEncoding(): Promise { + return window.electron.serialPort.getReadEncoding(); + } + + getOpenOptions(): Promise { + return window.electron.serialPort.getOpenOptions(); + } + onEvent(): Observable { return new Observable((observer) => { const removeListener = window.electron.serialPort.onEvent((data) => { diff --git a/apps/spie-ui/src/app/services/serial-port.service.ts b/apps/spie-ui/src/app/services/serial-port.service.ts index e8b6d5b..fe6703a 100644 --- a/apps/spie-ui/src/app/services/serial-port.service.ts +++ b/apps/spie-ui/src/app/services/serial-port.service.ts @@ -1,8 +1,17 @@ import { Injectable, inject, signal } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { type OpenOptions } from '@serialport/bindings-interface'; -import { type SerialPortEvent } from '@spie/types'; -import { Subject, filter, from, map, merge, scan, switchMap } from 'rxjs'; +import { + type Observable, + Subject, + filter, + from, + map, + merge, + scan, + startWith, + switchMap, +} from 'rxjs'; import { ElectronService } from './electron.service'; import { @@ -43,23 +52,19 @@ export class SerialPortService { isSendInputValid: false, }); - clearDataSubject = new Subject(); + clearDataSubject = new Subject(); isOpen = toSignal( from(this.electronService.serialPort.isOpen()).pipe( switchMap((isOpen) => this.electronService.serialPort.onEvent().pipe( - filter( - (serialPortEvent) => - serialPortEvent.event === 'close' || - serialPortEvent.event === 'open' - ), + startWith({ type: isOpen ? 'open' : 'close' }), scan((currentIsOpen, serialPortEvent) => { - if (serialPortEvent.event === 'open') { + if (serialPortEvent.type === 'open') { return true; } - if (serialPortEvent.event === 'close') { + if (serialPortEvent.type === 'close') { return false; } @@ -71,63 +76,23 @@ export class SerialPortService { { initialValue: false } ); - data = toSignal( - toObservable(this.isOpen).pipe( - switchMap(() => - merge( - // Emissions to this.isOpen will resubscribe these - this.electronService.serialPort.onEvent(), - this.clearDataSubject - ) - ), - filter((serialPortEvent) => serialPortEvent.event === 'data'), - map((serialPortEvent) => { - const data = serialPortEvent.data; - // If data it a "clear terminal" signal - if (data === '') { - return ''; - } - - if (this.terminalOptions().showTimestampsEnabled) { - const date = new Date(); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - return `[${hours}:${minutes}:${seconds}] ${data}`; - } - - return data; - }), - scan( - (acc, value) => { - // Reset on empty string - if (value === '') { - return { items: [] as string[], length: 0 }; - } - - acc.items.push(value); - acc.length += value.length; - const maxLength = this.terminalOptions().scrollbackLength * 10000; - - while (acc.length > maxLength) { - const removed = acc.items.shift(); - if (removed) { - acc.length -= removed.length; - } - } - - return acc; - }, - { items: [] as string[], length: 0 } - ), - map((buffer) => { - if (this.terminalOptions().encoding === 'hex') { - return buffer.items.join('\n'); - } - - return buffer.items.join(''); - }) + dataEvent$: Observable = toObservable(this.isOpen).pipe( + switchMap(() => + merge( + this.electronService.serialPort.onEvent(), + this.clearDataSubject.pipe(map(() => ({ type: 'clear' } as DataEvent))) + ) ), - { initialValue: '' } + filter( + (serialPortEvent) => + serialPortEvent.type === 'data' || serialPortEvent.type === 'clear' + ) ); } + +export type DataEvent = + | { + type: 'data'; + data: string; + } + | { type: 'clear' }; diff --git a/apps/spie/src/app/api/main.preload.ts b/apps/spie/src/app/api/main.preload.ts index be46e72..a3eb918 100644 --- a/apps/spie/src/app/api/main.preload.ts +++ b/apps/spie/src/app/api/main.preload.ts @@ -37,6 +37,8 @@ export const electronAPI: ElectronAPI = { isOpen: () => ipcRenderer.invoke('serial-port-is-open'), setReadEncoding: (encoding: Encoding) => ipcRenderer.invoke('serial-port-set-read-encoding', encoding), + getReadEncoding: () => ipcRenderer.invoke('serial-port-get-read-encoding'), + getOpenOptions: () => ipcRenderer.invoke('serial-port-get-open-options'), onEvent: (callback: (serialPortEvent: SerialPortEvent) => void) => { const eventName = 'serial-port-notification'; const dataListener = ( diff --git a/apps/spie/src/app/events/serial-port.events.ts b/apps/spie/src/app/events/serial-port.events.ts index 1a3a785..f2e83dc 100644 --- a/apps/spie/src/app/events/serial-port.events.ts +++ b/apps/spie/src/app/events/serial-port.events.ts @@ -20,10 +20,13 @@ export default class SerialPortEvents { >(); private static encoding: Encoding = 'ascii'; private static areListenersRegistered = false; + private static openOptions: OpenOptions | null = null; - private static addEventListeners(event: Electron.IpcMainEvent) { + private static addEventListeners( + event: Electron.IpcMainEvent + ): Promise { if (SerialPortEvents.areListenersRegistered) { - return; + return Promise.resolve(); } const addEventListener = ( @@ -36,7 +39,7 @@ export default class SerialPortEvents { !SerialPortEvents.serialPort.isOpen ) { if (!SerialPortEvents.listenerQueue.has(event)) { - // console.log('addEventListener queue', event, callback); + // console.log('SerialPortEvents.addEventListener queue', event, callback); // Port is not open, queue the callback SerialPortEvents.listenerQueue.set(event, callback); } @@ -44,7 +47,7 @@ export default class SerialPortEvents { } if (!SerialPortEvents.eventListeners.has(event)) { - // console.log('addEventListener attach', event, callback); + // console.log('SerialPortEvents.addEventListener attach', event, callback); // Port is open, attach callback immediately if (event === 'data') { SerialPortEvents.parser.on(event, callback); @@ -56,17 +59,17 @@ export default class SerialPortEvents { }; addEventListener('error', (error: Error) => { - const notification: SerialPortEvent = { event: 'error', error }; + const notification: SerialPortEvent = { type: 'error', error }; event.sender.send('serial-port-notification', notification); }); addEventListener('open', () => { - const notification: SerialPortEvent = { event: 'open' }; + const notification: SerialPortEvent = { type: 'open' }; event.sender.send('serial-port-notification', notification); }); addEventListener('close', () => { - const notification: SerialPortEvent = { event: 'close' }; + const notification: SerialPortEvent = { type: 'close' }; event.sender.send('serial-port-notification', notification); }); @@ -76,25 +79,27 @@ export default class SerialPortEvents { ? chunk.toString('hex').toUpperCase().match(/.{2}/g).join(' ') : chunk.toString('ascii'); - const notification: SerialPortEvent = { event: 'data', data }; + const notification: SerialPortEvent = { type: 'data', data }; event.sender.send('serial-port-notification', notification); }); addEventListener('drain', () => { - const notification: SerialPortEvent = { event: 'drain' }; + const notification: SerialPortEvent = { type: 'drain' }; event.sender.send('serial-port-notification', notification); }); SerialPortEvents.areListenersRegistered = true; + + return Promise.resolve(); } - private static removeEventListeners() { + private static removeEventListeners(): Promise { if (!SerialPortEvents.areListenersRegistered) { - return; + return Promise.resolve(); } SerialPortEvents.eventListeners.forEach((callback, event) => { - // console.log('removeEventListener', event, callback); + // console.log('SerialPortEvents.removeEventListener', event, callback); if (event === 'data') { SerialPortEvents.parser.off(event, callback); } else { @@ -104,6 +109,8 @@ export default class SerialPortEvents { SerialPortEvents.eventListeners.clear(); SerialPortEvents.areListenersRegistered = false; + + return Promise.resolve(); } static bootstrapEvents(): void { @@ -154,6 +161,8 @@ export default class SerialPortEvents { return reject(error); } + SerialPortEvents.openOptions = openOptions; + resolve(); }); }); @@ -216,26 +225,38 @@ export default class SerialPortEvents { ipcMain.handle('serial-port-is-open', () => { // console.warn('serial-port-is-open'); if (SerialPortEvents.serialPort && SerialPortEvents.serialPort.isOpen) { - return true; + return Promise.resolve(true); } - return false; + return Promise.resolve(false); }); ipcMain.handle('serial-port-set-read-encoding', (_, encoding: Encoding) => { // console.warn('serial-port-set-read-encoding'); SerialPortEvents.encoding = encoding; + + return Promise.resolve(); + }); + + ipcMain.handle('serial-port-get-read-encoding', () => { + // console.warn('serial-port-get-read-encoding'); + + return Promise.resolve(SerialPortEvents.encoding); + }); + + ipcMain.handle('serial-port-get-open-options', () => { + // console.warn('serial-port-get-open-options'); + return Promise.resolve(SerialPortEvents.openOptions); }); ipcMain.on('serial-port-add-notification-event-listener', (event) => { // console.warn('serial-port-add-notification-event-listener'); - - SerialPortEvents.addEventListeners(event); + return SerialPortEvents.addEventListeners(event); }); ipcMain.on('serial-port-remove-notification-event-listener', () => { // console.warn('serial-port-remove-notification-event-listener'); - SerialPortEvents.removeEventListeners(); + return SerialPortEvents.removeEventListeners(); }); } } diff --git a/apps/spie/src/app/events/update.events.ts b/apps/spie/src/app/events/update.events.ts index 80014f5..591f240 100644 --- a/apps/spie/src/app/events/update.events.ts +++ b/apps/spie/src/app/events/update.events.ts @@ -19,6 +19,111 @@ export default class UpdateEvents { (...args: any[]) => void >(); + private static addEventListeners( + event: Electron.IpcMainEvent + ): Promise { + if (UpdateEvents.areListenersRegistered) { + return Promise.resolve(); + } + + UpdateEvents.areListenersRegistered = true; + + const addEventListener = ( + event: UpdaterEvents, + callback: (...args: any[]) => void + ) => { + if (!UpdateEvents.eventListeners.has(event)) { + // console.log('UpdateEvents.addEventListener attach', event, callback); + autoUpdater.on(event, callback); + UpdateEvents.eventListeners.set(event, callback); + } + }; + + addEventListener('error', (error: Error, message: string) => { + const updateNotification: AutoUpdaterEvent = { + event: 'error', + error, + message, + }; + event.sender.send('app-update-notification', updateNotification); + }); + + addEventListener('checking-for-update', () => { + const updateNotification: AutoUpdaterEvent = { + event: 'checking-for-update', + }; + event.sender.send('app-update-notification', updateNotification); + }); + + addEventListener('update-not-available', (updateInfo: UpdateInfo) => { + const updateNotification: AutoUpdaterEvent = { + event: 'update-not-available', + updateInfo, + }; + event.sender.send('app-update-notification', updateNotification); + }); + + addEventListener('update-available', (updateInfo: UpdateInfo) => { + new Notification({ + title: 'Update Available for Download', + body: `Version ${updateInfo.releaseName} is ready for download.`, + icon: join(__dirname, 'assets/icon.ico'), + }).show(); + + const updateNotification: AutoUpdaterEvent = { + event: 'update-available', + updateInfo, + }; + + event.sender.send('app-update-notification', updateNotification); + }); + + addEventListener( + 'update-downloaded', + (updateDownloadedEvent: UpdateDownloadedEvent) => { + const updateNotification: AutoUpdaterEvent = { + event: 'update-downloaded', + updateDownloadedEvent, + }; + event.sender.send('app-update-notification', updateNotification); + } + ); + + addEventListener('download-progress', (progressInfo: ProgressInfo) => { + const updateNotification: AutoUpdaterEvent = { + event: 'download-progress', + progressInfo, + }; + event.sender.send('app-update-notification', updateNotification); + }); + + addEventListener('update-cancelled', (updateInfo: UpdateInfo) => { + const updateNotification: AutoUpdaterEvent = { + event: 'update-cancelled', + updateInfo, + }; + event.sender.send('app-update-notification', updateNotification); + }); + + return Promise.resolve(); + } + + private static removeEventListeners(): Promise { + if (!UpdateEvents.areListenersRegistered) { + return Promise.resolve(); + } + + UpdateEvents.eventListeners.forEach((listener, event) => { + // console.log('UpdateEvents.removeEventListener', event, callback); + autoUpdater.off(event, listener); + }); + UpdateEvents.eventListeners.clear(); + + UpdateEvents.areListenersRegistered = false; + + return Promise.resolve(); + } + static bootstrapEvents(): void { const checkForUpdates = async () => { try { @@ -50,108 +155,26 @@ export default class UpdateEvents { setTimeout(checkForUpdates, delayAfterAppReady); ipcMain.on('app-update-add-notification-event-listener', (event) => { - if (UpdateEvents.areListenersRegistered) { - return; - } + // console.warn('app-update-add-notification-event-listener'); - UpdateEvents.areListenersRegistered = true; - - const addEventListener = ( - event: UpdaterEvents, - callback: (...args: any[]) => void - ) => { - if (!UpdateEvents.eventListeners.has(event)) { - autoUpdater.on(event, callback); - UpdateEvents.eventListeners.set(event, callback); - } - }; - - addEventListener('error', (error: Error, message: string) => { - const updateNotification: AutoUpdaterEvent = { - event: 'error', - error, - message, - }; - event.sender.send('app-update-notification', updateNotification); - }); - - addEventListener('checking-for-update', () => { - const updateNotification: AutoUpdaterEvent = { - event: 'checking-for-update', - }; - event.sender.send('app-update-notification', updateNotification); - }); - - addEventListener('update-not-available', (updateInfo: UpdateInfo) => { - const updateNotification: AutoUpdaterEvent = { - event: 'update-not-available', - updateInfo, - }; - event.sender.send('app-update-notification', updateNotification); - }); - - addEventListener('update-available', (updateInfo: UpdateInfo) => { - new Notification({ - title: 'Update Available for Download', - body: `Version ${updateInfo.releaseName} is ready for download.`, - icon: join(__dirname, 'assets/icon.ico'), - }).show(); - - const updateNotification: AutoUpdaterEvent = { - event: 'update-available', - updateInfo, - }; - - event.sender.send('app-update-notification', updateNotification); - }); - - addEventListener( - 'update-downloaded', - (updateDownloadedEvent: UpdateDownloadedEvent) => { - const updateNotification: AutoUpdaterEvent = { - event: 'update-downloaded', - updateDownloadedEvent, - }; - event.sender.send('app-update-notification', updateNotification); - } - ); - - addEventListener('download-progress', (progressInfo: ProgressInfo) => { - const updateNotification: AutoUpdaterEvent = { - event: 'download-progress', - progressInfo, - }; - event.sender.send('app-update-notification', updateNotification); - }); - - addEventListener('update-cancelled', (updateInfo: UpdateInfo) => { - const updateNotification: AutoUpdaterEvent = { - event: 'update-cancelled', - updateInfo, - }; - event.sender.send('app-update-notification', updateNotification); - }); + return UpdateEvents.addEventListeners(event); }); ipcMain.on('app-update-remove-notification-event-listener', () => { - if (!UpdateEvents.areListenersRegistered) { - return; - } - - UpdateEvents.eventListeners.forEach((listener, event) => { - autoUpdater.off(event, listener); - }); - UpdateEvents.eventListeners.clear(); - - UpdateEvents.areListenersRegistered = false; + // console.warn('app-update-remove-notification-event-listener'); + return UpdateEvents.removeEventListeners(); }); ipcMain.handle('app-download-update', () => { + // console.warn('app-download-update'); return autoUpdater.downloadUpdate(); }); ipcMain.handle('app-install-update', () => { + // console.warn('app-install-update'); autoUpdater.quitAndInstall(); + + return Promise.resolve(); }); } } diff --git a/libs/types/src/lib/electron.d.ts b/libs/types/src/lib/electron.d.ts index 7049522..bba42cb 100644 --- a/libs/types/src/lib/electron.d.ts +++ b/libs/types/src/lib/electron.d.ts @@ -24,11 +24,11 @@ export type Encoding = 'ascii' | 'hex'; export type SerialPortEventType = 'error' | 'open' | 'close' | 'data' | 'drain'; export type SerialPortEvent = - | { event: 'error'; error: Error } - | { event: 'open' } - | { event: 'close' } - | { event: 'data'; data: string } - | { event: 'drain' }; + | { type: 'error'; error: Error } + | { type: 'open' } + | { type: 'close' } + | { type: 'data'; data: string } + | { type: 'drain' }; export interface SerialPortAPI { list: () => Promise; @@ -37,6 +37,8 @@ export interface SerialPortAPI { write: (data: string, encoding: Encoding) => Promise; isOpen: () => Promise; setReadEncoding: (encoding: Encoding) => Promise; + getReadEncoding: () => Promise; + getOpenOptions: () => Promise; onEvent: (callback: (serialPortEvent: SerialPortEvent) => void) => () => void; } diff --git a/package-lock.json b/package-lock.json index 62d1ee0..3bcdb02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,10 @@ "@angular/platform-browser-dynamic": "~18.2.0", "@angular/router": "~18.2.0", "@ionic/angular": "^8.0.0", + "apexcharts": "^4.1.0", "electron-updater": "^6.3.9", "ionicons": "^7.0.0", + "ng-apexcharts": "^1.13.0", "patch-package": "^8.0.0", "rxjs": "~7.8.0", "serialport": "^12.0.0", @@ -9297,6 +9299,62 @@ "npm": ">=7.10.0" } }, + "node_modules/@svgdotjs/svg.draggable.js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.4.tgz", + "integrity": "sha512-vWi/Col5Szo74HJVBgMHz23kLVljt3jvngmh0DzST45iO2ubIZ487uUAHIxSZH2tVRyiaaTL+Phaasgp4gUD2g==", + "license": "MIT", + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, + "node_modules/@svgdotjs/svg.filter.js": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.8.tgz", + "integrity": "sha512-YshF2YDaeRA2StyzAs5nUPrev7npQ38oWD0eTRwnsciSL2KrRPMoUw8BzjIXItb3+dccKGTX3IQOd2NFzmHkog==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.js": "^3.1.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@svgdotjs/svg.resize.js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", + "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.select.js": "^4.0.1" + } + }, + "node_modules/@svgdotjs/svg.select.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.2.tgz", + "integrity": "sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, "node_modules/@swc-node/core": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.13.3.tgz", @@ -10646,6 +10704,12 @@ "node": ">=14.15.0" } }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT" + }, "node_modules/@zkochan/js-yaml": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", @@ -11011,6 +11075,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/apexcharts": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.1.0.tgz", + "integrity": "sha512-TE0q0cXeS5k/AByLqlZAQ/aRQfdD3z0Ajd1uQWWZEjxiIC5qcBpMrTaG+aT+c3golqkvLH3u6kxDW8HBrggpLw==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.draggable.js": "^3.0.4", + "@svgdotjs/svg.filter.js": "^3.0.8", + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.resize.js": "^2.0.2", + "@svgdotjs/svg.select.js": "^4.0.1", + "@yr/monotone-cubic-spline": "^1.0.3" + } + }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.10", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", @@ -23803,6 +23881,21 @@ "dev": true, "license": "MIT" }, + "node_modules/ng-apexcharts": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.13.0.tgz", + "integrity": "sha512-YftYLsYTabbGYZZTceXRnliLaQ1/uAp4yx/kanRP5VJtNl4hObRv9hV5WLTXoQnYY0fzfseotgf7Uz0bUMLvAw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^18.0.4", + "@angular/core": "^18.0.4", + "apexcharts": "^4.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", diff --git a/package.json b/package.json index dae3e62..8998a5f 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "@angular/platform-browser-dynamic": "~18.2.0", "@angular/router": "~18.2.0", "@ionic/angular": "^8.0.0", + "apexcharts": "^4.1.0", "electron-updater": "^6.3.9", "ionicons": "^7.0.0", + "ng-apexcharts": "^1.13.0", "patch-package": "^8.0.0", "rxjs": "~7.8.0", "serialport": "^12.0.0",