From 88014234ad9a7e8b2fef18188504f2d80d4be9d7 Mon Sep 17 00:00:00 2001 From: Timon G Date: Tue, 22 Feb 2022 13:52:17 +0100 Subject: [PATCH] Action commands and printer notifications (#2586) * change setNotification to setInfo conforming to standard logging levels and clarifying notification roles * implement printernotification socket event emitter * implement printernotification prompt * handle prompts correctly * add reheating message * update import * starting cleanup * update notification styles * cleanup * clean up * fix codefactor issues * only show notifications with data * improve notifications Co-authored-by: Pierre-Alexis Ciavaldini --- .github/workflows/manual.yml | 2 +- src/app/app.module.ts | 11 +- src/app/event.service.ts | 46 +++---- src/app/model/event.model.ts | 7 ++ src/app/model/octoprint/socket.model.ts | 5 +- src/app/model/system.model.ts | 3 + .../notification/notification.component.html | 13 +- .../notification/notification.component.scss | 27 +++++ .../notification/notification.component.ts | 17 ++- .../socket/socket.octoprint.service.ts | 112 ++++++++++++++++-- src/app/services/socket/socket.service.ts | 4 +- src/locale/messages.fr.xlf | 48 ++++++++ themes/theGarbz/Glanceable/custom-styles.css | 80 ++++++------- 13 files changed, 288 insertions(+), 87 deletions(-) diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml index 13b09e07e..86e92789f 100644 --- a/.github/workflows/manual.yml +++ b/.github/workflows/manual.yml @@ -2,7 +2,7 @@ name: Manual Build with Artifacts on: workflow_dispatch: - + jobs: build: runs-on: ubuntu-latest diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3c0591ea9..a8218d495 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -139,14 +139,21 @@ export function playerFactory(): LottiePlayer { [ { provide: SocketService, - deps: [ConfigService, SystemService, ConversionService, HttpClient], + deps: [ConfigService, SystemService, ConversionService, NotificationService, HttpClient], useFactory: ( configService: ConfigService, systemService: SystemService, conversionService: ConversionService, + notificationService: NotificationService, httpClient: HttpClient, ) => { - return new OctoPrintSocketService(configService, systemService, conversionService, httpClient); + return new OctoPrintSocketService( + configService, + systemService, + conversionService, + notificationService, + httpClient, + ); }, }, ], diff --git a/src/app/event.service.ts b/src/app/event.service.ts index ef629aaca..a5d744a68 100644 --- a/src/app/event.service.ts +++ b/src/app/event.service.ts @@ -18,30 +18,32 @@ export class EventService implements OnDestroy { private router: Router, ) { this.subscriptions.add( - this.socketService.getEventSubscribable().subscribe((event: PrinterEvent) => { - if (event === PrinterEvent.PRINTING || event === PrinterEvent.PAUSED) { - setTimeout(() => { - this.printing = true; - }, 500); - } else { - setTimeout(() => { - this.printing = false; - }, 1000); - } + this.socketService.getEventSubscribable().subscribe((event: PrinterEvent) => this.handlePrinterEvent(event)), + ); + } - if (event === PrinterEvent.CLOSED) { - this.router.navigate(['/standby']); - } else if (event === PrinterEvent.CONNECTED) { - setTimeout(() => { - if (this.configService.isTouchscreen()) { - this.router.navigate(['/main-screen']); - } else { - this.router.navigate(['/main-screen-no-touch']); - } - }, 1000); + private handlePrinterEvent(event: PrinterEvent): void { + if (event === PrinterEvent.PRINTING || event === PrinterEvent.PAUSED) { + setTimeout(() => { + this.printing = true; + }, 500); + } else { + setTimeout(() => { + this.printing = false; + }, 1000); + } + + if (event === PrinterEvent.CLOSED) { + this.router.navigate(['/standby']); + } else if (event === PrinterEvent.CONNECTED) { + setTimeout(() => { + if (this.configService.isTouchscreen()) { + this.router.navigate(['/main-screen']); + } else { + this.router.navigate(['/main-screen-no-touch']); } - }), - ); + }, 500); + } } ngOnDestroy(): void { diff --git a/src/app/model/event.model.ts b/src/app/model/event.model.ts index 21f30cffa..35cc04b74 100644 --- a/src/app/model/event.model.ts +++ b/src/app/model/event.model.ts @@ -6,3 +6,10 @@ export enum PrinterEvent { IDLE, UNKNOWN, } + +export interface PrinterNotification { + message?: string; + action?: string; + text?: string; + choices?: string[]; +} diff --git a/src/app/model/octoprint/socket.model.ts b/src/app/model/octoprint/socket.model.ts index d8875aad6..782f3dc2d 100644 --- a/src/app/model/octoprint/socket.model.ts +++ b/src/app/model/octoprint/socket.model.ts @@ -20,7 +20,10 @@ export interface OctoprintSocketCurrent { export interface OctoprintSocketEvent { event: { type: string; - payload: unknown; + payload: { + error: string; + reason: string; + }; }; } export interface OctoprintPluginMessage { diff --git a/src/app/model/system.model.ts b/src/app/model/system.model.ts index cbeabad54..8f3a3e77e 100644 --- a/src/app/model/system.model.ts +++ b/src/app/model/system.model.ts @@ -3,6 +3,8 @@ export interface Notification { text: string; type: NotificationType; time: Date; + choices?: Array; + callback?: (index: number) => void; sticky?: boolean; } @@ -10,6 +12,7 @@ export enum NotificationType { INFO, WARN, ERROR, + PROMPT, } export interface UpdateError { diff --git a/src/app/notification/notification.component.html b/src/app/notification/notification.component.html index e27c13970..eb2891b04 100644 --- a/src/app/notification/notification.component.html +++ b/src/app/notification/notification.component.html @@ -1,9 +1,18 @@
+ (click)="hideNotification(true, true)"> {{ notification?.time | date: 'HH:mm' }} {{ notification?.heading }} {{ notification?.text }} - tap this card to close it + tap this card to close it +
+
+ {{ choice }} +
+
diff --git a/src/app/notification/notification.component.scss b/src/app/notification/notification.component.scss index 7b9d01b20..dfae1a485 100644 --- a/src/app/notification/notification.component.scss +++ b/src/app/notification/notification.component.scss @@ -11,6 +11,29 @@ z-index: 100; border-right: 1vw solid #5a6675; + &-prompt { + &__choices { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + } + + &__choice { + display: block; + font-size: 2.7vw; + background-color: #2196f3; + text-align: center; + padding: 3vh; + border-radius: 0.8vw; + margin: 1vw; + flex-grow: 100; + + &-first { + background-color: #44bd32; + } + } + } + &__show { top: 5vh; } @@ -47,6 +70,10 @@ } &__border { + &-3 { + border-left: 1vw solid #a1abb7; + } + &-2 { border-left: 1vw solid #c23616; } diff --git a/src/app/notification/notification.component.ts b/src/app/notification/notification.component.ts index b238d8d47..45701bcfe 100644 --- a/src/app/notification/notification.component.ts +++ b/src/app/notification/notification.component.ts @@ -24,10 +24,17 @@ export class NotificationComponent implements OnDestroy { ); } - public hideNotification(removeFromStack = true): void { - this.show = false; - clearTimeout(this.notificationCloseTimeout); - if (removeFromStack) this.notificationService.removeNotification(this.notification); + public hideNotification(removeFromStack = true, userTriggered = false): void { + if (!userTriggered || (userTriggered && !this.notification.choices)) { + this.show = false; + clearTimeout(this.notificationCloseTimeout); + if (removeFromStack) this.notificationService.removeNotification(this.notification); + } + } + + public chooseAction(index: number, callback: (index: number) => void): void { + callback(index); + this.hideNotification(); } private setNotification(notification: Notification | 'close'): void { @@ -41,7 +48,7 @@ export class NotificationComponent implements OnDestroy { if (!notification.sticky) { clearTimeout(this.notificationCloseTimeout); - this.notificationCloseTimeout = setTimeout(this.hideNotification.bind(this), 30 * 1000, false); + this.notificationCloseTimeout = setTimeout(this.hideNotification.bind(this), 15 * 1000, false); } } }); diff --git a/src/app/services/socket/socket.octoprint.service.ts b/src/app/services/socket/socket.octoprint.service.ts index eb91e7554..ca9bd4b44 100644 --- a/src/app/services/socket/socket.octoprint.service.ts +++ b/src/app/services/socket/socket.octoprint.service.ts @@ -1,13 +1,22 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import _ from 'lodash-es'; -import { Observable, ReplaySubject, Subject } from 'rxjs'; -import { pluck, startWith } from 'rxjs/operators'; +import { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { catchError, pluck, startWith } from 'rxjs/operators'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { ConfigService } from '../../config/config.service'; import { ConversionService } from '../../conversion.service'; -import { JobStatus, PrinterEvent, PrinterState, PrinterStatus, SocketAuth } from '../../model'; +import { + JobStatus, + Notification, + NotificationType, + PrinterEvent, + PrinterNotification, + PrinterState, + PrinterStatus, + SocketAuth, +} from '../../model'; import { DisplayLayerProgressData, OctoprintFilament, @@ -15,6 +24,7 @@ import { OctoprintSocketCurrent, OctoprintSocketEvent, } from '../../model/octoprint'; +import { NotificationService } from '../../notification/notification.service'; import { SystemService } from '../system/system.service'; import { SocketService } from './socket.service'; @@ -36,11 +46,12 @@ export class OctoPrintSocketService implements SocketService { private configService: ConfigService, private systemService: SystemService, private conversionService: ConversionService, + private notificationService: NotificationService, private http: HttpClient, ) { this.printerStatusSubject = new ReplaySubject(1); this.jobStatusSubject = new Subject(); - this.eventSubject = new ReplaySubject(1); + this.eventSubject = new ReplaySubject(); } //==== SETUP & AUTH ====// @@ -118,6 +129,25 @@ export class OctoPrintSocketService implements SocketService { this.socket.next(payload); } + private handlePluginMessage(pluginMessage: OctoprintPluginMessage) { + const plugins = [ + { + check: (plugin: string) => + plugin === 'DisplayLayerProgress-websocket-payload' && this.configService.isDisplayLayerProgressEnabled(), + handler: (message: unknown) => { + this.extractFanSpeed(message as DisplayLayerProgressData); + this.extractLayerHeight(message as DisplayLayerProgressData); + }, + }, + { + check: (plugin: string) => ['action_command_prompt', 'action_command_notification'].includes(plugin), + handler: (message: unknown) => this.handlePrinterNotification(message as PrinterNotification), + }, + ]; + + plugins.forEach(plugin => plugin.check(pluginMessage.plugin.plugin) && plugin.handler(pluginMessage.plugin.data)); + } + private setupSocket(resolve: () => void) { this.socket.subscribe({ next: message => { @@ -132,14 +162,7 @@ export class OctoPrintSocketService implements SocketService { } else if (Object.hasOwnProperty.bind(message)('event')) { this.extractPrinterEvent(message as OctoprintSocketEvent); } else if (Object.hasOwnProperty.bind(message)('plugin')) { - const pluginMessage = message as OctoprintPluginMessage; - if ( - pluginMessage.plugin.plugin === 'DisplayLayerProgress-websocket-payload' && - this.configService.isDisplayLayerProgressEnabled() - ) { - this.extractFanSpeed(pluginMessage.plugin.data as DisplayLayerProgressData); - this.extractLayerHeight(pluginMessage.plugin.data as DisplayLayerProgressData); - } + this.handlePluginMessage(message as OctoprintPluginMessage); } else if (Object.hasOwnProperty.bind(message)('reauthRequired')) { this.systemService.getSessionKey().subscribe(socketAuth => this.authenticateSocket(socketAuth)); } else if (Object.hasOwnProperty.bind(message)('connected')) { @@ -300,6 +323,15 @@ export class OctoPrintSocketService implements SocketService { break; case 'Error': newState = PrinterEvent.CLOSED; + if (state.event.payload) { + this.notificationService.setNotification({ + heading: $localize`:@@printer-information:Printer error`, + text: state.event.payload.error, + type: NotificationType.ERROR, + time: new Date(), + sticky: true, + } as Notification); + } break; default: break; @@ -311,6 +343,62 @@ export class OctoPrintSocketService implements SocketService { } } + //==== Notifications ====// + + private handlePrinterNotification(notification: PrinterNotification) { + if (Object.keys(notification).length > 0) { + if (notification.action === 'close') { + this.notificationService.closeNotification(); + } else if (notification.choices?.length > 0) { + this.notificationService.setNotification({ + heading: $localize`:@@action-required:Action required`, + text: notification.text ?? notification.message, + type: NotificationType.PROMPT, + time: new Date(), + choices: notification.choices, + callback: this.callbackFunction.bind(this), + sticky: true, + } as Notification); + } else if (notification.choices?.length == 0) { + this.notificationService.setNotification({ + heading: $localize`:@@printer-information:Printer information`, + text: notification.text ?? notification.message, + type: NotificationType.WARN, + time: new Date(), + sticky: true, + } as Notification); + } else if (notification.text || notification.message) { + this.notificationService.setNotification({ + heading: $localize`:@@printer-information:Printer information`, + text: notification.text ?? notification.message, + type: NotificationType.INFO, + time: new Date(), + } as Notification); + } + } + } + + private callbackFunction(index: number) { + this.http + .post( + this.configService.getApiURL('plugin/action_command_prompt'), + { command: 'select', choice: index }, + this.configService.getHTTPHeaders(), + ) + .pipe( + catchError(error => { + this.notificationService.setNotification({ + heading: $localize`:@@error-answer-prompt:Can't answer prompt!`, + text: error.message, + type: NotificationType.ERROR, + time: new Date(), + }); + return of(null); + }), + ) + .subscribe(); + } + //==== Subscribables ====// public getPrinterStatusSubscribable(): Observable { diff --git a/src/app/services/socket/socket.service.ts b/src/app/services/socket/socket.service.ts index 259fa5ceb..47d1f532e 100644 --- a/src/app/services/socket/socket.service.ts +++ b/src/app/services/socket/socket.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { JobStatus, PrinterEvent, PrinterStatus } from '../../model'; +import { JobStatus, PrinterEvent, PrinterNotification, PrinterStatus } from '../../model'; @Injectable() export abstract class SocketService { @@ -11,5 +11,5 @@ export abstract class SocketService { abstract getJobStatusSubscribable(): Observable; - abstract getEventSubscribable(): Observable; + abstract getEventSubscribable(): Observable; } diff --git a/src/locale/messages.fr.xlf b/src/locale/messages.fr.xlf index 81414431d..02a5052e6 100644 --- a/src/locale/messages.fr.xlf +++ b/src/locale/messages.fr.xlf @@ -294,6 +294,50 @@ do you want to execute the following command? voulez-vous exécuter la commande suivante ? + + Filament runout detected. Ejecting filament, please wait... + Fin de filament détectée. Ejection du filament, veuillez patienter... + + + A filament runout has been detected. Please remove the ejected filament, insert filament from a new spool and press Continue. + Une fin de filament a été détectée. Veuillez retirer le filament éjecté, insérer le filament d'une nouvelle bobine et presser Continuer. + + + Continue + Continuer + + + The filament has been primed. Do you want to continue printing? + Le filament a été amorcé. Souhaitez-vous continuer l'impression ? + + + Purge more filament + Purger plus de filament + + + The hotend has been disabled due to inactivity, to avoid burning the filament. Press Reheat when ready to resume. + La buse a été désactivée car l'imprimante était inactive, pour éviter de surchauffer le filament. Appuyez sur 'Activer la chauffe' pour reprendre + + + Reheat + Activer la chauffe + + + Reheating... + Reprise de la chauffe... + + + The hotend is now ready. + La buse est à température. + + + Action required + Action requise + + + Printer information + Information de l'imprimante + load new filament charger un nouveau filament @@ -538,6 +582,10 @@ tap this card to close it touchez cette carte pour la fermer + + Can't answer prompt! + Impossible de répondre à la demande! + cancel annuler diff --git a/themes/theGarbz/Glanceable/custom-styles.css b/themes/theGarbz/Glanceable/custom-styles.css index 6c3350021..818b8da5d 100644 --- a/themes/theGarbz/Glanceable/custom-styles.css +++ b/themes/theGarbz/Glanceable/custom-styles.css @@ -1,64 +1,64 @@ /**** Job-Info Resizes ****/ -app-job-status ~ app-printer-status .printer-status{ - position:absolute!important; - top: 42vh!important; - left: 0vw!important; +app-job-status ~ app-printer-status .printer-status { + position: absolute !important; + top: 42vh !important; + left: 0 !important; } app-job-status ~ app-printer-status .printer-status__set-value { - margin-top: -2vh!important; - font-size: 3.9vw!important; + margin-top: -2vh !important; + font-size: 3.9vw !important; } /**** Job-Info Layer Progress ****/ -.height-indication{ - position:absolute!important; - top: 66vh!important; - left: 0vw!important; - font-size: 3vw!important; +.height-indication { + position: absolute !important; + top: 66vh !important; + left: 0 !important; + font-size: 3vw !important; } -.height-indication__current-height{ - font-size: 6vh!important; +.height-indication__current-height { + font-size: 6vh !important; } -.height-indication__total-height{ - font-size: 5vh!important; +.height-indication__total-height { + font-size: 5vh !important; } /**** Job-Info Progress ****/ .job-info__progress-bar { - height: 8vh!important; - border-radius: 1.7vw!important; + height: 8vh !important; + border-radius: 1.7vw !important; } .job-info__progress-bar__wrapper { - position:fixed!important; - top: 77vh!important; - left: 3vw!important; - width: 92vw!important; - height: 8vh!important; - border-radius: 2vw!important; - background-color: rgba(38, 44, 53, 1); - border-style: solid!important; - border-color: rgb(121, 121, 121); - border-width: 2px!important; + position: fixed !important; + top: 77vh !important; + left: 3vw !important; + width: 92vw !important; + height: 8vh !important; + border-radius: 2vw !important; + background-color: rgba(38, 44, 53, 1); + border-style: solid !important; + border-color: rgb(121, 121, 121); + border-width: 2px !important; } #progress-preview-bar + .job-info__progress-percentage { - margin-top: -0.3vh!important; - margin-left: 0vw!important; - font-size: 6.7vh!important; - font-weight: 500!important; - display:block!important; - position:fixed!important; - left: 43.4vw!important; - top: 77vh!important; - z-index:1!important; - color:white!important; - text-shadow: 1px 1px 4px black; - visibility: visible!important; -} \ No newline at end of file + margin-top: -0.3vh !important; + margin-left: 0 !important; + font-size: 6.7vh !important; + font-weight: 500 !important; + display: block !important; + position: fixed !important; + left: 43.4vw !important; + top: 77vh !important; + z-index: 1 !important; + color: white !important; + text-shadow: 1px 1px 4px black; + visibility: visible !important; +}