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) {