diff --git a/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts b/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts
index 3a9aecc18..c82b5ab59 100644
--- a/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts
+++ b/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts
@@ -48,7 +48,7 @@ export class OctoprintAuthenticationComponent {
private sendLoginRequest(): void {
this.authService.startAuthProcess(this.octoprintURL).subscribe(
token => {
- this.notificationService.setNotification(
+ this.notificationService.setInfo(
$localize`:@@login-request-sent:Login request send!`,
$localize`:@@login-request-sent-message:Please confirm the request via the popup in the OctoPrint WebUI.`,
);
diff --git a/src/app/event.service.ts b/src/app/event.service.ts
index ef629aaca..d68ad1ac5 100644
--- a/src/app/event.service.ts
+++ b/src/app/event.service.ts
@@ -3,8 +3,9 @@ import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ConfigService } from './config/config.service';
-import { PrinterEvent } from './model/event.model';
+import { PrinterEvent, PrinterNotification } from './model/event.model';
import { SocketService } from './services/socket/socket.service';
+import { NotificationService } from './notification/notification.service';
@Injectable()
export class EventService implements OnDestroy {
@@ -15,35 +16,89 @@ export class EventService implements OnDestroy {
public constructor(
private socketService: SocketService,
private configService: ConfigService,
+ private notificationService: NotificationService,
private router: Router,
) {
this.subscriptions.add(
- this.socketService.getEventSubscribable().subscribe((event: PrinterEvent) => {
- if (event === PrinterEvent.PRINTING || event === PrinterEvent.PAUSED) {
- setTimeout(() => {
- this.printing = true;
- }, 500);
+ this.socketService.getEventSubscribable().subscribe((event: PrinterEvent | PrinterNotification) => {
+ if (this.isPrinterNotification(event)) {
+ this.handlePrinterNotification(event);
} 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']);
- }
- }, 1000);
+ this.handlePrinterEvent(event);
}
}),
);
}
+ // https://stackoverflow.com/questions/14425568/interface-type-check-with-typescript
+ // https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript/8511350#8511350
+ private isPrinterNotification(object: any): object is PrinterNotification {
+ return typeof object === 'object'
+ && object !== null
+ && ('text' in object || 'message' in object || 'action' in object);
+ }
+
+ private handlePrinterNotification(event: PrinterNotification): void {
+ const messages = {
+ 'FilamentRunout T0': $localize`:@@prompt-filament-runout-t0:Filament runout detected. Ejecting filament, please wait...`,
+ 'Nozzle Parked': $localize`:@@prompt-filament-runout:A filament runout has been detected. Please remove the ejected filament, insert filament from a new spool and press Continue.`,
+ 'Continue': $localize`:@@prompt-continue:Continue`,
+ 'Paused': $localize`:@@prompt-filament-runout-resume:The filament has been primed. Do you want to continue printing?`,
+ 'PurgeMore': $localize`:@@prompt-filament-runout-purge:Purge more filament`,
+ 'Heater Timeout': $localize`:@@prompt-heater-timeout:The hotend has been disabled due to inactivity, to avoid burning the filament. Press Reheat when ready to resume.`,
+ 'Reheat': $localize`:@@prompt-reheat:Reheat`,
+ 'Reheat Done': $localize`:@@prompt-reheat-done:The hotend is now ready.`,
+ };
+ if (event.action === 'close') {
+ this.notificationService.closeNotification();
+ } else if (event.choices?.length > 0) {
+ // event is action:prompt
+ this.notificationService.setPrompt(
+ $localize`:@@action-required:Action required`,
+ messages[event.text] || event.text,
+ event.choices.map(c => messages[c] || c)
+ );
+ } else if (event.choices?.length == 0) {
+ // event is action:prompt without choices
+ this.notificationService.setWarning(
+ $localize`:@@printer-information:Printer information`,
+ messages[event.text] || event.text
+ );
+ } else {
+ // event is action:notification
+ // this.notificationService.setInfo(
+ // $localize`:@@printer-information:Printer information`,
+ // messages[event.message] || event.message
+ // );
+ // TODO: annoying as a notification
+ // should be put with an autoclear timeout in the bottom-right statusline
+ }
+ }
+
+ 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']);
+ }
+ }, 1000);
+ }
+ }
+
ngOnDestroy(): void {
this.subscriptions.unsubscribe();
}
diff --git a/src/app/model/event.model.ts b/src/app/model/event.model.ts
index 21f30cffa..46cdee9bd 100644
--- a/src/app/model/event.model.ts
+++ b/src/app/model/event.model.ts
@@ -6,3 +6,11 @@ export enum PrinterEvent {
IDLE,
UNKNOWN,
}
+
+// either notification (message) or prompt (action, text and choices)
+export interface PrinterNotification {
+ message?: string,
+ action?: string,
+ text?: string,
+ choices?: string[],
+}
diff --git a/src/app/model/system.model.ts b/src/app/model/system.model.ts
index afc37d66b..d10480e4b 100644
--- a/src/app/model/system.model.ts
+++ b/src/app/model/system.model.ts
@@ -2,6 +2,7 @@ export interface Notification {
heading: string;
text: string;
type: string;
+ choices?: string[];
closed: () => void;
}
diff --git a/src/app/notification/notification.component.html b/src/app/notification/notification.component.html
index d2159874e..6a1191980 100644
--- a/src/app/notification/notification.component.html
+++ b/src/app/notification/notification.component.html
@@ -1,5 +1,6 @@
@@ -7,3 +8,22 @@
{{ notification.text }}
tap this card to close it
+
+ 0"
+ [ngClass]="[show ? 'notification-prompt__show' : '']"
+>
+
{{ notification.heading }}
+
{{ notification.text }}
+
+
diff --git a/src/app/notification/notification.component.scss b/src/app/notification/notification.component.scss
index 8f4110c1b..bc9195efd 100644
--- a/src/app/notification/notification.component.scss
+++ b/src/app/notification/notification.component.scss
@@ -1,54 +1,94 @@
-.notification {
- display: block;
- position: fixed;
- width: 80vw;
- background-color: #5a6675;
- border-radius: 1.3vw;
- left: 10vw;
- top: -75vh;
- transition: top 0.7s ease-in-out;
- box-shadow: 0 4px 10px -3px rgba(0, 0, 0, 0.75);
- z-index: 100;
- border-right: 1vw solid #5a6675;
-
- &__show {
- top: 5vh;
- }
-
- &__heading {
- display: block;
- text-align: center;
- font-size: 3.5vw;
- font-weight: 500;
- margin-top: 4vh;
- }
-
- &__text {
- font-size: 2.7vw;
- padding: 5vh 3vw 1vh;
- display: block;
- text-align: center;
- }
-
- &__close {
- font-size: 2vw;
- display: block;
- text-align: center;
- padding-bottom: 2vh;
- opacity: 0.4;
- }
-
- &__border {
- &-error {
- border-left: 1vw solid #c23616;
- }
-
- &-warn {
- border-left: 1vw solid #fbc531;
- }
-
- &-notification {
- border-left: 1vw solid #4bae50;
- }
- }
-}
+.notification {
+ display: block;
+ position: fixed;
+ width: 80vw;
+ background-color: #5a6675;
+ border-radius: 1.3vw;
+ left: 10vw;
+ top: -75vh;
+ transition: top 0.7s ease-in-out;
+ box-shadow: 0 4px 10px -3px rgba(0, 0, 0, 0.75);
+ z-index: 100;
+ border-right: 1vw solid #5a6675;
+
+ &-prompt {
+ position: fixed;
+ width: 100vw;
+ height: 100vh;
+ left: 0vw;
+ top: -100vh;
+ transition: top 0.7s ease-in-out;
+ background-color: #5a6675;
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+
+ &__show {
+ top: 0vh;
+ }
+
+ &__choices {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ justify-content: center;
+ padding: 0vw 5vw;
+ }
+
+ &__choice {
+ display: block;
+ font-size: 3vw;
+ background-color: #2196f3;
+ text-align: center;
+ padding: 3vh;
+ border-radius: 0.8vw;
+ margin: 1vw;
+
+ &-first {
+ background-color: #44bd32;
+ }
+ }
+ }
+
+ &__show {
+ top: 5vh;
+ }
+
+ &__heading {
+ display: block;
+ text-align: center;
+ font-size: 3.5vw;
+ font-weight: 500;
+ margin-top: 4vh;
+ }
+
+ &__text {
+ font-size: 2.7vw;
+ padding: 5vh 3vw 1vh;
+ display: block;
+ text-align: center;
+ }
+
+ &__close {
+ font-size: 2vw;
+ display: block;
+ text-align: center;
+ padding-bottom: 2vh;
+ opacity: 0.4;
+ }
+
+ &__border {
+ &-error {
+ border-left: 1vw solid #c23616;
+ }
+
+ &-warn {
+ border-left: 1vw solid #fbc531;
+ }
+
+ &-info {
+ border-left: 1vw solid #4bae50;
+ }
+ }
+}
diff --git a/src/app/notification/notification.component.ts b/src/app/notification/notification.component.ts
index a0b306129..1f57799f9 100644
--- a/src/app/notification/notification.component.ts
+++ b/src/app/notification/notification.component.ts
@@ -1,7 +1,10 @@
import { Component, NgZone, OnDestroy } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
import { Subscription } from 'rxjs';
+import { catchError } from 'rxjs/operators';
import { Notification } from '../model';
+import { ConfigService } from '../config/config.service';
import { NotificationService } from './notification.service';
@Component({
@@ -16,11 +19,17 @@ export class NotificationComponent implements OnDestroy {
heading: '',
text: '',
type: '',
+ choices: null,
closed: null,
};
public show = false;
- public constructor(private notificationService: NotificationService, private zone: NgZone) {
+ public constructor(
+ private notificationService: NotificationService,
+ private zone: NgZone,
+ private http: HttpClient,
+ private configService: ConfigService,
+ ) {
this.subscriptions.add(
this.notificationService
.getObservable()
@@ -35,6 +44,21 @@ export class NotificationComponent implements OnDestroy {
}
}
+ public answerPrompt(index: number): void {
+ this.http
+ .post(
+ this.configService.getApiURL('plugin/action_command_prompt'),
+ { command: 'select', choice: index },
+ this.configService.getHTTPHeaders()
+ )
+ .pipe(
+ catchError(error =>
+ this.notificationService.setError($localize`:@@error-answer-prompt:Can't answer prompt!`, error.message),
+ ),
+ )
+ .subscribe();
+ }
+
private setNotification(notification: Notification | 'close'): void {
this.zone.run(() => {
if (notification === 'close') {
diff --git a/src/app/notification/notification.service.ts b/src/app/notification/notification.service.ts
index 22b69ab58..9cf956ecf 100644
--- a/src/app/notification/notification.service.ts
+++ b/src/app/notification/notification.service.ts
@@ -49,13 +49,25 @@ export class NotificationService {
});
}
- public setNotification(heading: string, text: string): Promise {
+ public setInfo(heading: string, text: string): Promise {
return new Promise(resolve => {
if (this.observer) {
- this.observer.next({ heading, text, type: 'notification', closed: resolve });
+ this.observer.next({ heading, text, type: 'info', closed: resolve });
} else {
setTimeout(() => {
- this.setNotification(heading, text);
+ this.setInfo(heading, text);
+ }, 1000);
+ }
+ });
+ }
+
+ public setPrompt(heading: string, text: string, choices: string[]): Promise {
+ return new Promise(resolve => {
+ if (this.observer) {
+ this.observer.next({ heading, text, type: 'prompt', choices, closed: resolve });
+ } else {
+ setTimeout(() => {
+ this.setPrompt(heading, text, choices);
}, 1000);
}
});
diff --git a/src/app/services/socket/socket.octoprint.service.ts b/src/app/services/socket/socket.octoprint.service.ts
index b51124c56..469359900 100644
--- a/src/app/services/socket/socket.octoprint.service.ts
+++ b/src/app/services/socket/socket.octoprint.service.ts
@@ -7,7 +7,7 @@ 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, PrinterEvent, PrinterNotification, PrinterState, PrinterStatus, SocketAuth } from '../../model';
import {
DisplayLayerProgressData,
OctoprintFilament,
@@ -25,7 +25,7 @@ export class OctoPrintSocketService implements SocketService {
private printerStatusSubject: Subject;
private jobStatusSubject: Subject;
- private eventSubject: Subject;
+ private eventSubject: Subject;
private printerStatus: PrinterStatus;
private jobStatus: JobStatus;
@@ -39,7 +39,7 @@ export class OctoPrintSocketService implements SocketService {
) {
this.printerStatusSubject = new ReplaySubject();
this.jobStatusSubject = new Subject();
- this.eventSubject = new ReplaySubject();
+ this.eventSubject = new ReplaySubject();
}
//==== SETUP & AUTH ====//
@@ -116,6 +116,27 @@ 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: (data: unknown) => {
+ this.extractFanSpeed(data as DisplayLayerProgressData);
+ this.extractLayerHeight(data as DisplayLayerProgressData);
+ },
+ },
+ {
+ check: (plugin: string) => ['action_command_prompt', 'action_command_notification'].includes(plugin),
+ handler: (data: unknown) => this.eventSubject.next(data as PrinterNotification),
+ },
+ ];
+
+ plugins.forEach(plugin =>
+ plugin.check(pluginMessage.plugin.plugin) && plugin.handler(pluginMessage.plugin.data)
+ );
+ }
+
private setupSocket(resolve: () => void) {
this.socket.subscribe(
message => {
@@ -125,14 +146,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)('reauth')) {
this.systemService.getSessionKey().subscribe(socketAuth => this.authenticateSocket(socketAuth));
} else if (Object.hasOwnProperty.bind(message)('connected')) {
@@ -314,7 +328,7 @@ export class OctoPrintSocketService implements SocketService {
return this.jobStatusSubject.pipe(startWith(this.jobStatus));
}
- public getEventSubscribable(): Observable {
+ public getEventSubscribable(): Observable {
return this.eventSubject;
}
}
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 591881d2a..9e54e6e2c 100644
--- a/src/locale/messages.fr.xlf
+++ b/src/locale/messages.fr.xlf
@@ -290,6 +290,46 @@
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
+
+
+ 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
@@ -534,6 +574,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