From b5263c027de108047d02aadf6ef2f4f4d98a047f Mon Sep 17 00:00:00 2001
From: MultiMote <contact@mmote.ru>
Date: Sat, 19 Oct 2024 23:06:00 +0300
Subject: [PATCH] Print tasks rework

---
 README.md                            | 16 +++--
 src/client/index.ts                  |  6 +-
 src/index.ts                         |  2 +-
 src/packets/abstraction.ts           | 65 +++++---------------
 src/packets/packet_generator.ts      | 92 ----------------------------
 src/print_task_versions.ts           | 33 ----------
 src/print_tasks/AbstractPrintTask.ts | 51 +++++++++++++++
 src/print_tasks/B1PrintTask.ts       | 31 ++++++++++
 src/print_tasks/D110PrintTask.ts     | 32 ++++++++++
 src/print_tasks/OldD11PrintTask.ts   | 33 ++++++++++
 src/print_tasks/V5PrintTask.ts       | 31 ++++++++++
 src/print_tasks/index.ts             | 26 ++++++++
 tsconfig.json                        |  3 +-
 13 files changed, 237 insertions(+), 184 deletions(-)
 delete mode 100644 src/print_task_versions.ts
 create mode 100644 src/print_tasks/AbstractPrintTask.ts
 create mode 100644 src/print_tasks/B1PrintTask.ts
 create mode 100644 src/print_tasks/D110PrintTask.ts
 create mode 100644 src/print_tasks/OldD11PrintTask.ts
 create mode 100644 src/print_tasks/V5PrintTask.ts
 create mode 100644 src/print_tasks/index.ts

diff --git a/README.md b/README.md
index 6c47ff2..99cf22c 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Yarn:
 yarn add @mmote/niimbluelib --exact
 ```
 
-### Usage example
+### Usage example (may be outdated)
 
 ```js
 import { Utils, RequestCommandId, ResponseCommandId, NiimbotBluetoothClient, ImageEncoder, PrintTaskVersion } from "@mmote/niimbluelib";
@@ -75,14 +75,20 @@ ctx.stroke();
 // draw border
 ctx.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
 
-const image = ImageEncoder.encodeCanvas(canvas, props.printDirection);
+const encoded = ImageEncoder.encodeCanvas(canvas, props.printDirection);
 
-const taskVersion = client.getPrintTaskVersion() ?? PrintTaskVersion.V3;
+const printTaskName = client.getPrintTaskType() ?? "D110";
 
-await client.abstraction.print(taskVersion, image, { quantity });
+const printTask = client.abstraction.newPrintTask(printTaskName, {
+  totalPages: quantity,
+  statusPollIntervalMs: 100,
+  statusTimeoutMs: 8_000
+})
 
 try {
-  await client.abstraction.waitUntilPrintFinished(taskVersion, quantity);
+  await printTask.printInit();
+  await printTask.printPage(encoded, quantity);
+  await printTask.waitForFinished();
 } catch (e) {
   console.error(e);
 }
diff --git a/src/client/index.ts b/src/client/index.ts
index e0ea156..6a9b3ac 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -9,7 +9,7 @@ import {
 } from "../packets";
 import { PrinterModelMeta, getPrinterMetaById } from "../printer_models";
 import { ClientEventMap, PacketSentEvent, PrinterInfoFetchedEvent, HeartbeatEvent, HeartbeatFailedEvent } from "./events";
-import { getPrintTaskVersion, PrintTaskVersion } from "../print_task_versions";
+import { findPrintTask, PrintTaskName } from "../print_tasks";
 
 export type ConnectionInfo = {
   deviceName?: string;
@@ -159,14 +159,14 @@ export abstract class NiimbotAbstractClient extends TypedEventTarget<ClientEvent
   }
 
   /** Determine print task version if any */
-  public getPrintTaskVersion(): PrintTaskVersion | undefined {
+  public getPrintTaskType(): PrintTaskName | undefined {
     const meta = this.getModelMetadata();
 
     if (meta === undefined) {
       return undefined;
     }
 
-    return getPrintTaskVersion(meta.model);
+    return findPrintTask(meta.model);
   }
 
   public setPacketInterval(milliseconds: number) {
diff --git a/src/index.ts b/src/index.ts
index 549ddba..1769d2c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,4 +3,4 @@ export * from "./packets";
 export * from "./image_encoder";
 export * from "./utils";
 export * from "./printer_models";
-export * from "./print_task_versions";
+export * from "./print_tasks";
diff --git a/src/packets/abstraction.ts b/src/packets/abstraction.ts
index ae48c01..e33af73 100644
--- a/src/packets/abstraction.ts
+++ b/src/packets/abstraction.ts
@@ -11,13 +11,13 @@ import {
   SoundSettingsType,
 } from ".";
 import { NiimbotAbstractClient, PacketReceivedEvent, PrintProgressEvent } from "../client";
-import { EncodedImage } from "../image_encoder";
-import { PrintTaskVersion } from "../print_task_versions";
+import { PrintTaskName, printTasks } from "../print_tasks";
+import { AbstractPrintTask, PrintOptions } from "../print_tasks/AbstractPrintTask";
 import { PrinterModel } from "../printer_models";
 import { Validators, Utils } from "../utils";
 import { SequentialDataReader } from "./data_reader";
 import { NiimbotPacket } from "./packet";
-import { PacketGenerator, PrintOptions } from "./packet_generator";
+import { PacketGenerator } from "./packet_generator";
 
 export class PrintError extends Error {
   public readonly reasonId: number;
@@ -92,10 +92,16 @@ export class Abstraction {
   }
 
   /** Send packet and wait for response */
-  private async send(packet: NiimbotPacket, forceTimeout?: number): Promise<NiimbotPacket> {
+  public async send(packet: NiimbotPacket, forceTimeout?: number): Promise<NiimbotPacket> {
     return this.client.sendPacketWaitResponse(packet, forceTimeout ?? this.timeout);
   }
 
+  public async sendAll(packets: NiimbotPacket[], forceTimeout?: number): Promise<void> {
+    for (const p of packets) {
+      await this.send(p, forceTimeout);
+    }
+  }
+
   public async getPrintStatus(): Promise<PrintStatus> {
     const packet = await this.send(PacketGenerator.printStatus());
 
@@ -342,32 +348,6 @@ export class Abstraction {
     await this.send(PacketGenerator.printerReset());
   }
 
-  /**
-   *
-   * Call client.stopHeartbeat before print is started!
-   *
-   * @param taskVersion
-   * @param image
-   * @param options
-   * @param timeout
-   */
-  public async print(
-    taskVersion: PrintTaskVersion,
-    image: EncodedImage,
-    options?: PrintOptions,
-    timeout?: number
-  ): Promise<void> {
-    this.setTimeout(timeout ?? this.DEFAULT_PRINT_TIMEOUT);
-    const packets: NiimbotPacket[] = PacketGenerator.generatePrintSequence(taskVersion, image, options);
-    try {
-      for (const element of packets) {
-        await this.send(element);
-      }
-    } finally {
-      this.setDefaultTimeout();
-    }
-  }
-
   public async waitUntilPrintFinishedV1(pagesToPrint: number, timeoutMs: number = 5_000): Promise<void> {
     return new Promise<void>((resolve, reject) => {
       const listener = (evt: PacketReceivedEvent) => {
@@ -381,7 +361,7 @@ export class Abstraction {
           this.statusTimeoutTimer = setTimeout(() => {
             this.client.removeEventListener("packetreceived", listener);
             reject(new Error("Timeout waiting print status"));
-          }, timeoutMs);
+          }, timeoutMs ?? 5_000);
 
           if (page === pagesToPrint) {
             clearTimeout(this.statusTimeoutTimer);
@@ -431,28 +411,15 @@ export class Abstraction {
             clearInterval(this.statusPollTimer);
             reject(e as Error);
           });
-      }, pollIntervalMs);
+      }, pollIntervalMs ?? 300);
     });
   }
 
-  /**
-   * printprogress event is firing during this process.
-   *
-   * @param pagesToPrint Total pages to print.
-   */
-  public async waitUntilPrintFinished(
-    taskVersion: PrintTaskVersion,
-    pagesToPrint: number,
-    options?: { pollIntervalMs?: number; timeoutMs?: number }
-  ): Promise<void> {
-    if (taskVersion === PrintTaskVersion.V1) {
-      return this.waitUntilPrintFinishedV1(pagesToPrint, options?.timeoutMs);
-    }
-
-    return this.waitUntilPrintFinishedV2(pagesToPrint, options?.pollIntervalMs);
-  }
-
   public async printEnd(): Promise<void> {
     await this.send(PacketGenerator.printEnd());
   }
+
+  public newPrintTask(name: PrintTaskName, options?: PrintOptions): AbstractPrintTask {
+    return new printTasks[name](this, options);
+  }
 }
diff --git a/src/packets/packet_generator.ts b/src/packets/packet_generator.ts
index 71c23b3..ade938c 100644
--- a/src/packets/packet_generator.ts
+++ b/src/packets/packet_generator.ts
@@ -1,7 +1,6 @@
 import {
   AutoShutdownTime,
   HeartbeatType,
-  LabelType,
   NiimbotPacket,
   PrinterInfoType,
   RequestCommandId,
@@ -10,15 +9,8 @@ import {
   SoundSettingsType,
 } from ".";
 import { EncodedImage, ImageEncoder, ImageRow } from "../image_encoder";
-import { PrintTaskVersion } from "../print_task_versions";
 import { Utils } from "../utils";
 
-export type PrintOptions = {
-  labelType?: LabelType;
-  density?: number;
-  quantity?: number;
-};
-
 export class PacketGenerator {
   public static generic(
     requestId: RequestCommandId,
@@ -294,88 +286,4 @@ export class PacketGenerator {
       }
     });
   }
-
-  public static generatePrintPageSequence(
-    taskVersion: PrintTaskVersion,
-    image: EncodedImage,
-    options?: PrintOptions
-  ): NiimbotPacket[] {
-    const packets: NiimbotPacket[] = [];
-
-    switch (taskVersion) {
-      case PrintTaskVersion.V1:
-        packets.push(this.printClear());
-        packets.push(this.pageStart());
-        packets.push(this.setPageSizeV1(image.rows));
-        packets.push(this.setPrintQuantity(options?.quantity ?? 1));
-        break;
-      case PrintTaskVersion.V2:
-        packets.push(this.printClear());
-        packets.push(this.pageStart());
-        packets.push(this.setPageSizeV2(image.rows, image.cols));
-        packets.push(this.setPrintQuantity(options?.quantity ?? 1));
-
-        break;
-      case PrintTaskVersion.V3:
-        packets.push(this.pageStart());
-        packets.push(this.setPageSizeV2(image.rows, image.cols));
-        packets.push(this.setPrintQuantity(options?.quantity ?? 1));
-        break;
-      case PrintTaskVersion.V4:
-        packets.push(this.pageStart());
-        packets.push(this.setPageSizeV3(image.rows, image.cols, options?.quantity ?? 1));
-        break;
-      case PrintTaskVersion.V5:
-        packets.push(this.pageStart());
-        packets.push(this.setPageSizeV4(image.rows, image.cols, options?.quantity ?? 1, 0, false));
-        break;
-      default:
-        taskVersion satisfies never;
-    }
-
-    packets.push(...this.writeImageData(image));
-    packets.push(this.pageEnd());
-    return packets;
-  }
-
-  public static generatePrintInitSequence(taskVersion: PrintTaskVersion, options?: PrintOptions): NiimbotPacket[] {
-    const packets: NiimbotPacket[] = [];
-
-    packets.push(this.setDensity(options?.density ?? 2));
-    packets.push(this.setLabelType(options?.labelType ?? LabelType.WithGaps));
-
-    switch (taskVersion) {
-      case PrintTaskVersion.V1:
-      case PrintTaskVersion.V2:
-      case PrintTaskVersion.V3:
-        packets.push(this.printStart());
-        break;
-      case PrintTaskVersion.V4:
-        packets.push(this.printStartV4(options?.quantity ?? 1));
-        break;
-      case PrintTaskVersion.V5:
-        packets.push(this.printStartV5(options?.quantity ?? 1, 0, 0));
-        break;
-      default:
-        taskVersion satisfies never;
-    }
-
-    return packets;
-  }
-
-  /**
-   * Generate print sequence for one page (with one or multiple copies).
-   *
-   * You should send PrintEnd manually after this sequence after print is finished
-   */
-  public static generatePrintSequence(
-    taskVersion: PrintTaskVersion,
-    image: EncodedImage,
-    options?: PrintOptions
-  ): NiimbotPacket[] {
-    return [
-      ...this.generatePrintInitSequence(taskVersion, options),
-      ...this.generatePrintPageSequence(taskVersion, image, options),
-    ];
-  }
 }
diff --git a/src/print_task_versions.ts b/src/print_task_versions.ts
deleted file mode 100644
index ccfc4fb..0000000
--- a/src/print_task_versions.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PrinterModel as M } from "./printer_models";
-
-export enum PrintTaskVersion {
-  V1 = 1,
-  V2,
-  V3,
-  V4,
-  V5,
-}
-
-export const getPrintTaskVersion = (model: M): PrintTaskVersion | undefined => {
-  switch (model) {
-    case M.D11:
-    case M.D11S:
-    case M.B21_L2B:
-    case M.B21:
-    case M.B21_PRO:
-    case M.B21S:
-    case M.B21S_C2B:
-    case M.B21_C2B:
-      return PrintTaskVersion.V1;
-
-    case M.D110:
-      return PrintTaskVersion.V3;
-
-    case M.D11_H:
-    case M.D110_M:
-    case M.B1:
-      return PrintTaskVersion.V4;
-  }
-
-  return undefined;
-};
diff --git a/src/print_tasks/AbstractPrintTask.ts b/src/print_tasks/AbstractPrintTask.ts
new file mode 100644
index 0000000..d907cad
--- /dev/null
+++ b/src/print_tasks/AbstractPrintTask.ts
@@ -0,0 +1,51 @@
+import { EncodedImage } from "../image_encoder";
+import { LabelType } from "../packets";
+import { Abstraction } from "../packets/abstraction";
+
+export type PrintOptions = {
+  /** Printer label type */
+  labelType?: LabelType;
+  /** Print density */
+  density?: number;
+  /** How many pages will be printed */
+  totalPages?: number;
+  /** Used in {@link waitForFinished} where status is received by polling */
+  statusPollIntervalMs?: number;
+  /** Used in {@link waitForFinished} where status is received by waiting */
+  statusTimeoutMs?: number;
+};
+
+const printOptionsDefaults: PrintOptions = {
+  totalPages: 1,
+  statusPollIntervalMs: 300,
+  statusTimeoutMs: 5_000,
+};
+
+export abstract class AbstractPrintTask {
+  protected abstraction: Abstraction;
+  protected printOptions: PrintOptions;
+  protected pagesPrinted: number;
+
+  constructor(abstraction: Abstraction, printOptions?: PrintOptions) {
+    this.abstraction = abstraction;
+    this.pagesPrinted = 0;
+
+    this.printOptions = {
+      ...printOptionsDefaults,
+      ...printOptions,
+    };
+  }
+
+  protected checkAddPage(quantity: number) {
+    if (this.pagesPrinted + quantity > (this.printOptions.totalPages ?? 1)) {
+      throw new Error("Trying to print too many pages (task totalPages may not be set correctly)");
+    }
+  }
+
+  /** Prepare print (set label type, density, print start, ...) */
+  abstract printInit(): Promise<void>;
+  /** Print image with a specified number of copies */
+  abstract printPage(image: EncodedImage, quantity?: number): Promise<void>;
+  /** Wait for print is finished */
+  abstract waitForFinished(): Promise<void>;
+}
diff --git a/src/print_tasks/B1PrintTask.ts b/src/print_tasks/B1PrintTask.ts
new file mode 100644
index 0000000..f9664fd
--- /dev/null
+++ b/src/print_tasks/B1PrintTask.ts
@@ -0,0 +1,31 @@
+import { EncodedImage } from "..";
+import { LabelType, PacketGenerator } from "../packets";
+import { AbstractPrintTask } from "./AbstractPrintTask";
+
+export class B1PrintTask extends AbstractPrintTask {
+  override printInit(): Promise<void> {
+    return this.abstraction.sendAll([
+      PacketGenerator.setDensity(this.printOptions.density ?? 3),
+      PacketGenerator.setLabelType(this.printOptions.labelType ?? LabelType.WithGaps),
+      PacketGenerator.printStartV4(this.printOptions.totalPages ?? 1),
+    ]);
+  }
+
+  override printPage(image: EncodedImage, quantity?: number): Promise<void> {
+    this.checkAddPage(quantity ?? 1);
+
+    return this.abstraction.sendAll([
+      PacketGenerator.pageStart(),
+      PacketGenerator.setPageSizeV3(image.rows, image.cols, quantity ?? 1),
+      ...PacketGenerator.writeImageData(image),
+      PacketGenerator.pageEnd(),
+    ]);
+  }
+
+  override waitForFinished(): Promise<void> {
+    return this.abstraction.waitUntilPrintFinishedV2(
+      this.printOptions.totalPages ?? 1,
+      this.printOptions.statusPollIntervalMs
+    );
+  }
+}
diff --git a/src/print_tasks/D110PrintTask.ts b/src/print_tasks/D110PrintTask.ts
new file mode 100644
index 0000000..0c03d2f
--- /dev/null
+++ b/src/print_tasks/D110PrintTask.ts
@@ -0,0 +1,32 @@
+import { EncodedImage } from "../image_encoder";
+import { LabelType, PacketGenerator } from "../packets";
+import { AbstractPrintTask } from "./AbstractPrintTask";
+
+export class D110PrintTask extends AbstractPrintTask {
+  override printInit(): Promise<void> {
+    return this.abstraction.sendAll([
+      PacketGenerator.setDensity(this.printOptions.density ?? 2),
+      PacketGenerator.setLabelType(this.printOptions.labelType ?? LabelType.WithGaps),
+      PacketGenerator.printStart(),
+    ]);
+  }
+
+  override printPage(image: EncodedImage, quantity?: number): Promise<void> {
+    this.checkAddPage(quantity ?? 1);
+
+    return this.abstraction.sendAll([
+      PacketGenerator.pageStart(),
+      PacketGenerator.setPageSizeV2(image.rows, image.cols),
+      PacketGenerator.setPrintQuantity(quantity ?? 1),
+      ...PacketGenerator.writeImageData(image),
+      PacketGenerator.pageEnd(),
+    ]);
+  }
+
+  override waitForFinished(): Promise<void> {
+    return this.abstraction.waitUntilPrintFinishedV2(
+      this.printOptions.totalPages ?? 1,
+      this.printOptions.statusPollIntervalMs
+    );
+  }
+}
diff --git a/src/print_tasks/OldD11PrintTask.ts b/src/print_tasks/OldD11PrintTask.ts
new file mode 100644
index 0000000..b16a583
--- /dev/null
+++ b/src/print_tasks/OldD11PrintTask.ts
@@ -0,0 +1,33 @@
+import { EncodedImage } from "../image_encoder";
+import { LabelType, PacketGenerator } from "../packets";
+import { AbstractPrintTask } from "./AbstractPrintTask";
+
+export class OldD11PrintTask extends AbstractPrintTask {
+  override printInit(): Promise<void> {
+    return this.abstraction.sendAll([
+      PacketGenerator.setDensity(this.printOptions.density ?? 2),
+      PacketGenerator.setLabelType(this.printOptions.labelType ?? LabelType.WithGaps),
+      PacketGenerator.printStart(),
+    ]);
+  }
+
+  override printPage(image: EncodedImage, quantity: number): Promise<void> {
+    this.checkAddPage(quantity ?? 1);
+    
+    return this.abstraction.sendAll([
+      PacketGenerator.printClear(),
+      PacketGenerator.pageStart(),
+      PacketGenerator.setPageSizeV1(image.rows),
+      PacketGenerator.setPrintQuantity(quantity ?? 1),
+      ...PacketGenerator.writeImageData(image),
+      PacketGenerator.pageEnd(),
+    ]);
+  }
+
+  override waitForFinished(): Promise<void> {
+    return this.abstraction.waitUntilPrintFinishedV1(
+      this.printOptions.totalPages ?? 1,
+      this.printOptions.statusTimeoutMs
+    );
+  }
+}
diff --git a/src/print_tasks/V5PrintTask.ts b/src/print_tasks/V5PrintTask.ts
new file mode 100644
index 0000000..60c0d77
--- /dev/null
+++ b/src/print_tasks/V5PrintTask.ts
@@ -0,0 +1,31 @@
+import { EncodedImage } from "..";
+import { LabelType, PacketGenerator } from "../packets";
+import { AbstractPrintTask } from "./AbstractPrintTask";
+
+export class V5PrintTask extends AbstractPrintTask {
+  override printInit(): Promise<void> {
+    return this.abstraction.sendAll([
+      PacketGenerator.setDensity(this.printOptions.density ?? 3),
+      PacketGenerator.setLabelType(this.printOptions.labelType ?? LabelType.WithGaps),
+      PacketGenerator.printStartV5(this.printOptions.totalPages ?? 1, 0, 0)
+    ]);
+  }
+
+  override printPage(image: EncodedImage, quantity?: number): Promise<void> {
+    this.checkAddPage(quantity ?? 1);
+
+    return this.abstraction.sendAll([
+      PacketGenerator.pageStart(),
+      PacketGenerator.setPageSizeV4(image.rows, image.cols, quantity ?? 1, 0, false),
+      ...PacketGenerator.writeImageData(image),
+      PacketGenerator.pageEnd(),
+    ]);
+  }
+
+  override waitForFinished(): Promise<void> {
+    return this.abstraction.waitUntilPrintFinishedV2(
+      this.printOptions.totalPages ?? 1,
+      this.printOptions.statusPollIntervalMs
+    );
+  }
+}
diff --git a/src/print_tasks/index.ts b/src/print_tasks/index.ts
new file mode 100644
index 0000000..8dcb860
--- /dev/null
+++ b/src/print_tasks/index.ts
@@ -0,0 +1,26 @@
+import { PrinterModel as M } from "../printer_models";
+import { B1PrintTask } from "./B1PrintTask";
+import { D110PrintTask } from "./D110PrintTask";
+import { OldD11PrintTask } from "./OldD11PrintTask";
+import { V5PrintTask } from "./V5PrintTask";
+
+export const printTasks = {
+  D11_OLD: OldD11PrintTask,
+  D110: D110PrintTask,
+  B1: B1PrintTask,
+  V5: V5PrintTask,
+};
+
+export type PrintTaskName = keyof typeof printTasks;
+
+export const printTaskNames = Object.keys(printTasks) as PrintTaskName[];
+
+export const modelPrintTasks: Partial<Record<PrintTaskName, M[]>> = {
+  D11_OLD: [M.D11, M.D11S, M.B21_L2B, M.B21, M.B21_PRO, M.B21_C2B],
+  D110: [M.B21S, M.B21S_C2B, M.D110],
+  B1: [M.D11_H, M.D110_M, M.B1],
+};
+
+export const findPrintTask = (model: M): PrintTaskName | undefined => {
+  return (Object.keys(modelPrintTasks) as PrintTaskName[]).find((key) => modelPrintTasks[key]?.includes(model));
+};
diff --git a/tsconfig.json b/tsconfig.json
index 0b15f57..8a86686 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
     "esModuleInterop": true,                  /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
     "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
     "strict": true,                           /* Enable all strict type-checking options. */
-    "skipLibCheck": true                      /* Skip type checking all .d.ts files. */
+    "skipLibCheck": true,                     /* Skip type checking all .d.ts files. */
+    "noImplicitOverride": true                /* Ensure overriding members in derived classes are marked with an override modifier. */
   }
 }