From 2a0edcdb5e4bde01c36454a8ef1b73dd00367344 Mon Sep 17 00:00:00 2001
From: MultiMote <contact@mmote.ru>
Date: Sun, 16 Feb 2025 22:46:47 +0300
Subject: [PATCH] Fix outdated code

---
 src/lib/utils.ts                 | 198 +++++++++++++++++++++++++++++++
 src/routes/+page.svelte          | 113 +++++++++++-------
 src/routes/api/upload/+server.ts |  11 +-
 3 files changed, 280 insertions(+), 42 deletions(-)

diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a9a0fa5..0994d6c 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,3 +1,4 @@
+import { NiimbotPacket, PacketParser, RequestCommandId, ResponseCommandId, Utils } from "@mmote/niimbluelib";
 import child_process from "child_process";
 
 interface TsharkJson {
@@ -21,6 +22,27 @@ export interface TsharkJsonNormalized {
   data: string;
 }
 
+export const plainTextProcess = (data: Buffer): TsharkJsonNormalized[] => {
+  const lines = data.toString().split(/(?:\r\n|\r|\n)/g);
+  const result: TsharkJsonNormalized[] = [];
+
+  for (const line of lines) {
+    const lineTrimmed = line.trim();
+    const direction = lineTrimmed.substring(0, 2);
+    const data = lineTrimmed.substring(2);
+
+    if (direction == "<<" || direction == ">>") {
+      result.push({
+        time: 0,
+        proto: "usb",
+        direction,
+        data,
+      });
+    }
+  }
+  return result;
+};
+
 export const runTshark = async (path: string): Promise<TsharkJsonNormalized[]> => {
   const tshark = process.env.TSHARK_PATH || "tshark";
 
@@ -103,3 +125,179 @@ export const runTshark = async (path: string): Promise<TsharkJsonNormalized[]> =
     resolve(normalized);
   });
 };
+
+export interface ParseResult extends TsharkJsonNormalized {
+  packets: { typeStr: string; packet: NiimbotPacket; info?: string }[];
+  error: string;
+}
+
+export const extractPacketInfo = (p: NiimbotPacket): string => {
+  if (p.command === RequestCommandId.SetPageSize && p.dataLength >= 4) {
+    const rows = Utils.bytesToI16(p.data.slice(0, 2));
+    const cols = Utils.bytesToI16(p.data.slice(2, 4));
+    return `${cols}x${rows}px, ${cols / 8}x${rows / 8}mm 203dpi`;
+  }
+
+  if (p.command === RequestCommandId.PrintBitmapRow) {
+    const row = Utils.bytesToI16(p.data.slice(0, 2));
+    const w = (p.data.length - 6) * 8;
+    return `row=${row} width=${w} x${p.data[5]}`;
+  }
+
+  if (p.command === RequestCommandId.PrintBitmapRowIndexed) {
+    const row = Utils.bytesToI16(p.data.slice(0, 2));
+    return `row=${row} x${p.data[5]}`;
+  }
+
+  if (p.command === RequestCommandId.PrintEmptyRow) {
+    const row = Utils.bytesToI16(p.data.slice(0, 2));
+    return `row=${row} x${p.data[2]}`;
+  }
+
+  if ([RequestCommandId.PrintQuantity, ResponseCommandId.In_PrinterInfoPrinterCode].includes(p.command as number)) {
+    return `${Utils.bytesToI16(p.data.slice(0, 2))}`;
+  }
+
+  if ([RequestCommandId.SetDensity, RequestCommandId.SetLabelType].includes(p.command as number)) {
+    return `${p.data[0]}`;
+  }
+
+  return "";
+};
+
+export const parseDump = (dump: TsharkJsonNormalized[]): ParseResult[] => {
+  let rxBuf: Uint8Array = new Uint8Array();
+  let txBuf: Uint8Array = new Uint8Array();
+  const results: ParseResult[] = [];
+
+  for (const d of dump) {
+    let data: Uint8Array = Utils.hexToBuf(d.data);
+
+    if (d.direction === "<<") {
+      rxBuf = Utils.u8ArrayAppend(rxBuf, data);
+
+      if (rxBuf.length > 1 && !Utils.hasSubarrayAtPos(rxBuf, NiimbotPacket.HEAD, 0)) {
+        rxBuf = new Uint8Array();
+        results.push({ ...d, error: "Dropping invalid buffer", packets: [] });
+        continue;
+      }
+    } else {
+      if (Utils.hasSubarrayAtPos(data, [0x03, ...NiimbotPacket.HEAD], 0)) data = data.slice(1); // drop 03 prefix
+
+      txBuf = Utils.u8ArrayAppend(txBuf, data);
+
+      if (txBuf.length > 1 && !Utils.hasSubarrayAtPos(txBuf, NiimbotPacket.HEAD, 0)) {
+        txBuf = new Uint8Array();
+        results.push({ ...d, error: "Dropping invalid buffer", packets: [] });
+        continue;
+      }
+    }
+
+    try {
+      const packets: NiimbotPacket[] = PacketParser.parsePacketBundle(d.direction === "<<" ? rxBuf : txBuf);
+
+      if (packets.length > 0) {
+        results.push({
+          ...d,
+          error: "",
+          packets: packets.map((packet) => ({
+            packet,
+            typeStr: (d.direction === ">>" ? RequestCommandId[packet.command] : ResponseCommandId[packet.command]) ?? "?",
+            info: extractPacketInfo(packet),
+          })),
+        });
+
+        if (d.direction === "<<") {
+          rxBuf = new Uint8Array();
+        } else {
+          txBuf = new Uint8Array();
+        }
+      } else {
+        results.push({ ...d, error: "No packets", packets: [] });
+      }
+    } catch (_e) {
+      results.push({ ...d, error: "Fragment", packets: [] });
+    }
+  }
+
+  return results;
+};
+
+export const detectImage = (parsed: ParseResult[]): ImageData | null => {
+  let dimensions: { rows: number; cols: number } | undefined;
+  let image: ImageData | undefined;
+  let pageStarted = false;
+
+  for (const d of parsed) {
+    for (const { packet } of d.packets) {
+      if (packet.command === RequestCommandId.SetPageSize && packet.dataLength >= 4 && pageStarted) {
+        dimensions = {
+          rows: Utils.bytesToI16(packet.data.slice(0, 2)),
+          cols: Utils.bytesToI16(packet.data.slice(2, 4)),
+        };
+        const data = new Uint8ClampedArray(dimensions.rows * dimensions.cols * 4).fill(0x55);
+        image = new ImageData(data, dimensions.cols, dimensions.rows, { colorSpace: "srgb" });
+      } else if (packet.command === RequestCommandId.PageStart) {
+        pageStarted = true;
+      } else if (packet.command === RequestCommandId.PageEnd && pageStarted && image !== undefined) {
+        return image;
+      } else if (packet.command === RequestCommandId.PrintEmptyRow && image !== undefined) {
+        const row = Utils.bytesToI16(packet.data.slice(0, 2));
+        const repeats = packet.data[2];
+
+        for (let repeat = 0; repeat < repeats; repeat++) {
+          for (let i = 0; i < image.width; i++) {
+            const idx = image.width * (row + repeat) * 4 + i * 4;
+            image.data[idx] = 0xf6;
+            image.data[idx + 1] = 0xf6;
+            image.data[idx + 2] = 0xf6;
+            image.data[idx + 3] = 0xf6;
+          }
+        }
+      } else if (packet.command === RequestCommandId.PrintBitmapRow && image !== undefined) {
+        const row = Utils.bytesToI16(packet.data.slice(0, 2));
+        const repeats = packet.data[5];
+        const data = packet.data.slice(6);
+        console.log(Utils.bufToHex(data, ""));
+        console.log(row, repeats);
+
+        for (let repeat = 0; repeat < repeats; repeat++) {
+          for (let byteIdx = 0; byteIdx < data.length; byteIdx++) {
+            const b = data[byteIdx];
+
+            for (let bit = 0; bit < 8; bit++) {
+              const idx = image.width * (row + repeat) * 4 + byteIdx * 4 * 8 + bit * 4;
+              if ((b & (1 << (7 - bit))) !== 0) {
+                image.data[idx] = 0x00;
+                image.data[idx + 1] = 0x00;
+                image.data[idx + 2] = 0x00;
+                image.data[idx + 3] = 0xff;
+              } else {
+                image.data[idx] = 0xf3;
+                image.data[idx + 1] = 0xd3;
+                image.data[idx + 2] = 0xff;
+                image.data[idx + 3] = 0xff;
+              }
+            }
+          }
+        }
+      } else if (packet.command === RequestCommandId.PrintBitmapRowIndexed && image !== undefined) {
+        const row = Utils.bytesToI16(packet.data.slice(0, 2));
+        const repeats = packet.data[5];
+        console.log(row, repeats);
+
+        for (let repeat = 0; repeat < repeats; repeat++) {
+          for (let i = 0; i < image.width; i++) {
+            const idx = image.width * (row + repeat) * 4 + i * 4;
+            image.data[idx] = 0xbb;
+            image.data[idx + 1] = 0x38;
+            image.data[idx + 2] = 0x3e;
+            image.data[idx + 3] = 0xff;
+          }
+        }
+      }
+    }
+  }
+
+  return null;
+};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index b31c650..4c635b9 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,16 +1,18 @@
 <script lang="ts">
   import { PUBLIC_BASE_URL } from "$env/static/public";
-  import type { TsharkJsonNormalized } from "$lib/utils";
+  import { detectImage, parseDump, type ParseResult, type TsharkJsonNormalized } from "$lib/utils";
   import { Utils, PacketParser, RequestCommandId, ResponseCommandId } from "@mmote/niimbluelib";
   import Dropzone from "svelte-file-dropzone";
 
   let data: TsharkJsonNormalized[] = [];
+  let parsed: ParseResult[] = [];
   let error: string = "";
   let uploading: boolean = false;
   let rx: boolean = true;
   let tx: boolean = true;
   let showInfo: boolean = true;
   let showTime: boolean = true;
+  let canvas: HTMLCanvasElement;
 
   let allPacketTypes: { rx: string[]; tx: string[] } = {
     tx: Object.values(RequestCommandId)
@@ -22,6 +24,10 @@
   };
   let disabledPacketTypes: string[] = [];
 
+  const invert = () => {
+    disabledPacketTypes = [...allPacketTypes.rx, ...allPacketTypes.tx].filter((e) => !disabledPacketTypes.includes(e));
+  };
+
   const switchPacket = (p: string) => {
     if (disabledPacketTypes.includes(p)) {
       disabledPacketTypes = disabledPacketTypes.filter((e) => e !== p);
@@ -48,6 +54,17 @@
 
       if (resp.ok) {
         data = json as TsharkJsonNormalized[];
+        parsed = parseDump(data);
+
+        const image = detectImage(parsed);
+        // console.log(image)
+
+        if (image !== null) {
+          canvas.width = image.width;
+          canvas.height = image.height;
+          const ctx = canvas.getContext("2d");
+          ctx?.putImageData(image, 0, 0);
+        }
       } else if ("error" in json) {
         error = json.error;
       }
@@ -58,39 +75,20 @@
 
   const formatHex = (hex: string) => {
     return hex
-      .toUpperCase()
-      .replace(/^(03)?(5555)([A-F0-9]{2})([A-F0-9]{2})(.*?)([A-F0-9]{2})(AAAA)$/, "$1 $2 $3 $4 $5 $6 $7");
+      .toLowerCase()
+      .replace(/^(03)?(5555)([a-f0-9]{2})([a-f0-9]{2})(.*?)([a-f0-9]{2})(aaaa)$/, "$1 $2 $3 $4 $5 $6 $7");
   };
 
-  const formatInfo = (direction: string, hex: string): { typeStr: string; msg: string } => {
-    try {
-      const infos = [];
-
-      if (hex.startsWith("035555")) hex = hex.slice(2);
-
-      const buf = Utils.hexToBuf(hex);
-      const packets = PacketParser.parsePacketBundle(buf);
-      // if(comment += ResponseCommandId[packet.command])
-      infos.push(
-        ...packets.map((p) => {
-          let msg = "";
-          if (direction === ">>") {
-            msg = RequestCommandId[p.command];
-            if (p.command === RequestCommandId.SetPageSize && p.dataLength >= 4) {
-              const cols = Utils.bytesToI16(p.data.slice(0, 2));
-              const rows = Utils.bytesToI16(p.data.slice(2, 4));
-              msg += ` (${rows}x${cols}px, ${rows / 8}x${cols / 8}mm 203dpi)`;
-            }
-          } else {
-            msg = ResponseCommandId[p.command];
-          }
-          return msg;
-        })
-      );
-      return { typeStr: infos.join(", "), msg: infos.join(", ") || "???" };
-    } catch (e) {
-      return { typeStr: "Invalid", msg: "❌" };
+  const isPacketDisplayed = (r: ParseResult) => {
+    if (r.packets.length > 0) {
+      const first = r.packets[0].typeStr;
+      const allEqual = r.packets.every((val) => val.typeStr === first);
+      if (allEqual && disabledPacketTypes.includes(first)) {
+        return false;
+      }
     }
+
+    return true;
   };
 </script>
 
@@ -104,7 +102,7 @@
     <div class="error">{error}</div>
   {/if}
 
-  <div class="filters">
+  <div class="page-row">
     <input type="checkbox" bind:checked={rx} id="rx" />
     <label for="rx">rx</label>
 
@@ -118,7 +116,11 @@
     <label for="info">show info</label>
   </div>
 
-  <div class="filters">
+  <div class="page-row">
+    <button class="pill" on:click={() => invert()}>Invert packet filter</button>
+  </div>
+
+  <div class="page-row">
     {#each allPacketTypes.tx as t}
       <button class="parsed tx pill {disabledPacketTypes.includes(t) && 'disabled'}" on:click={() => switchPacket(t)}
         >{t}</button
@@ -126,7 +128,7 @@
     {/each}
   </div>
 
-  <div class="filters">
+  <div class="page-row">
     {#each allPacketTypes.rx as t}
       <button class="parsed rx pill {disabledPacketTypes.includes(t) && 'disabled'}" on:click={() => switchPacket(t)}
         >{t}</button
@@ -134,18 +136,35 @@
     {/each}
   </div>
 
+  <div class="page-row">
+    <div>Preview</div>
+    <canvas bind:this={canvas} width="20" height="10"></canvas>
+  </div>
+
   <div class="data">
-    {#each data as d}
-      {@const info = formatInfo(d.direction, d.data)}
-      {#if ((d.direction === "<<" && rx) || (d.direction === ">>" && tx)) && !disabledPacketTypes.includes(info.typeStr)}
+    {#each parsed as d}
+      {#if ((d.direction === "<<" && rx) || (d.direction === ">>" && tx)) && isPacketDisplayed(d)}
         <div class="row">
           {#if showTime}
             <span class="time pill">{d.time.toFixed(3)}</span>
           {/if}
+
           <span class="{d.direction == '<<' ? 'rx' : 'tx'} pill">{d.direction}</span>
+
           <span class="hex pill">{formatHex(d.data)}</span>
+
           {#if showInfo}
-            <span class="parsed pill {d.direction == '<<' ? 'rx' : 'tx'}">{info.msg}</span>
+            {#if d.error}
+              <span class="parsed pill">{d.error}</span>
+            {:else}
+              {#each d.packets as p}
+                <span class="parsed pill {d.direction == '<<' ? 'rx' : 'tx'}">
+                  {p.typeStr}
+                  {#if p.info}({p.info}){/if}
+                  <button class="hide" title="Hide" on:click={() => switchPacket(p.typeStr)}>x</button>
+                </span>
+              {/each}
+            {/if}
           {/if}
         </div>
       {/if}
@@ -158,16 +177,21 @@
     margin: 16px 64px;
   }
 
+  canvas {
+    border: 1px solid #292929;
+  }
+
   .error {
     color: red;
     margin-top: 1em;
   }
 
-  .filters {
+  .page-row {
     margin-top: 1em;
   }
 
   .pill {
+    background: none;
     padding: 0 0.3em;
     border-radius: 4px;
     border: 1px solid gray;
@@ -209,11 +233,20 @@
   }
 
   .pill.parsed.rx.disabled {
-    color: #747474;
+    color: #585858;
     border-color: #042e04;
     background-color: rgba(11, 156, 11, 0.1);
   }
 
+  .pill.parsed .hide {
+    color: #a7a7a7;
+    border: none;
+    background: none;
+    padding: 0;
+    cursor: pointer;
+    user-select: none;
+  }
+
   .hex {
     overflow-wrap: break-word;
   }
diff --git a/src/routes/api/upload/+server.ts b/src/routes/api/upload/+server.ts
index f8fda9e..adbfd75 100644
--- a/src/routes/api/upload/+server.ts
+++ b/src/routes/api/upload/+server.ts
@@ -1,7 +1,7 @@
 import { json, type RequestHandler } from "@sveltejs/kit";
 import { writeFile, unlink } from "node:fs/promises";
 import { v4 as uuidv4 } from "uuid";
-import { runTshark } from "$lib/utils";
+import { runTshark, plainTextProcess } from "$lib/utils";
 
 export const POST: RequestHandler = async ({ request }) => {
   const formData = await request.formData();
@@ -15,7 +15,14 @@ export const POST: RequestHandler = async ({ request }) => {
       return json({ error: "No file" }, { status: 400 });
     }
 
-    await writeFile(path, Buffer.from(await file.arrayBuffer()));
+    const buf = Buffer.from(await file.arrayBuffer());
+    await writeFile(path, buf);
+
+    if(file.name.endsWith(".txt")) {
+      const out = plainTextProcess(buf)
+      return json(out);
+    }
+
     const out = await runTshark(path);
     return json(out);
   } catch (e) {