Fix/init and timeouts - #19
Closed
SimonSchick wants to merge 7 commits into
Closed
Conversation
…e buffer Three defects in the receive path combined to explain both reported symptoms: printer init failing on Windows, and every operation timing out on macOS after the first hiccup. parseRaw's candidate filter had no return on its non-match path, so the implicit undefined removed any awaited command that did not match the byte in hand. An unsolicited ASB frame arriving mid-query was enough to strand the query until its timeout. parseRaw also ignored the parser's remainder whenever it reported the message as incomplete. The ESC/POS "not prepared yet" reply is a complete but empty two-byte packet that the parser consumes before asking for more, so those bytes stayed at the head of the buffer forever. Every later parse re-read them and reported incomplete again, which timed out not just that command but every operation after it. callMessageHandler threw when a reply arrived with no matching command - routine after a timeout clears the awaited list. The throw escaped through the input listener's read loop, which stopped permanently while the channel stayed open and connected kept reporting true. parseRaw now offers the buffer head to each candidate without ever dropping one, checks incompleteness before matching so a split reply is not handed to the unsolicited path, always honours a reported remainder, falls back to the unsolicited path for anything unclaimed, and refuses to loop when a handler consumes nothing. Unparseable input resynchronizes by one byte instead of throwing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
Verified against the Epson ESC/POS reference for GS a and GS r as printed in the TM-T20 quick reference. The ASB online bit was inverted. The spec reads "bit 3 = 1: in Offline, 0: in Online", but a set bit was reported as PrinterOnline, so the library announced the printer as healthy at exactly the moment it went offline. Adds a distinct PrinterOffline status rather than reporting neither. The invalid-trailer branch pushed a parse error and then fell through and decoded the garbage bytes anyway, emitting fabricated paper-out and unrecoverable-error events. It also consumed four bytes on a desync, which shifts the desync along instead of correcting it, so a single lost byte corrupted every following frame. It now discards the frame and advances by one byte to let the next real header line up. Second-byte bits 0-2 are fixed 0 in basic ASB, so the recovery-waiting, feed-button and recoverable-error flags mapped there could never fire. Those belong to DLE EOT n=2 and FS ( e, not GS a. GS r replies were claimed unconditionally, so any stray byte resolved the awaiting command with a fabricated status. Replies are now checked against the fixed bits the spec defines for the subcommand actually asked about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
promiseWithTimeout never cleared its timer, leaving an armed timeout behind on every call - dozens per connect. On timeout the awaited commands were only dereferenced, never rejected, so anything still holding one waited forever, and dispose() did not settle them either. The channel reports write failures by returning an error rather than throwing, and that value was discarded. A write that never reached the device was then followed by a full wait for a reply that could not come, so the user saw a timeout instead of the actual USB error. A failed setup() left the channel open with its interface claimed and the read loop running, with nothing referencing either. The interface could never be released, so the next connect could not claim it and the printer could not be re-added without a physical replug. handleInputError only logged. Since the input listener does not restart, the printer then looked connected but timed out every subsequent operation forever; it now tears down so callers get a prompt DeviceNotReadyError. Also serializes transactions, since _awaitedCommands is a single rendezvous slot that concurrent sends would clobber; wires up the messageWaitTimeoutMS option, which was declared but never read; drops an unbounded busy-wait in the pre-command-set message path; and fixes the retry message that always reported "Tried 0 times". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
asString decoded with TextDecoder('ascii'), which the WHATWG Encoding
Standard defines as an alias for windows-1252, while asUint8Array encoded
with TextEncoder, which emits UTF-8. The two were not inverses: every
codepage byte in 0x80-0xFF came back as a different character, or as two
or three bytes. Latent over USB, which uses the raw transformer, but it
corrupts any string channel and already corrupted debug output. No
TextDecoder label round-trips these bytes, so DecodeAscii now maps them
by hand.
The CP850 table held a byte-for-byte copy of CP852. Since CP850 is in the
default candidate list, autoEncode selected it, emitted ESC t 2 to switch
the printer to real CP850, and then sent a CP852 byte - printing the
wrong glyph. Characters CP850 exists to provide and CP852 lacks were
unmappable entirely. Replaced with the canonical table.
autoEncode indexed by UTF-16 unit while advancing one unit at a time,
splitting astral characters into two lone surrogates and emitting two
replacement bytes for one character, which shifted every following
column.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
setTextFormatting's alignment switch placed its default case ahead of 'Center', so resetToDefault - which arrives with alignment undefined - emitted ESC a 1 and centred everything that followed. Left is the printer's power-on state and what the other reset paths in this function use. Also makes enc() reject anything that isn't exactly one character. Every call site passes a literal, so a bad argument is a programming mistake rather than something to silently encode as a stray zero byte in the middle of a command sequence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
Matches the WebDeviceMux change: TypeScript 6, eslint 10, vitest 5, vite 8, typescript-eslint 8.69, with a stripped strict flat config (recommended + strictTypeChecked + stylisticTypeChecked, projectService for type-aware rules, no react/prettier/import-sort). The rules that map to bugs already found here are pinned to error; purely aesthetic ones are off. tsconfig gains noImplicitReturns, noUncheckedIndexedAccess, exactOptionalPropertyTypes and noFallthroughCasesInSwitch. noImplicitReturns alone would have caught the parseRaw candidate-dropping bug. The source changes are what the new checks surfaced: - Parser.ts read line[0] and used it unguarded in 23 places, so an empty line would have thrown rather than rendering nothing. - CmdTransmitPrinterId asserted printerHardware non-null nine times; it now takes the hardware update object directly. - offsetPrintPosition fell off the end for an unrecognized origin, silently returning undefined instead of a command. - MessageCandidates becomes a const object rather than an enum, since its values are compared against raw bytes off the wire. - UpdateFor now spells out `| undefined`, which is what an update built from a device descriptor that omitted a field actually looks like. - numberInRange had bare returns mixed with value returns. src/ReceiptLine/Parser.ts keeps a documented rule override. It is a port of the ReceiptLine reference implementation, kept close to upstream so it can be diffed against it; it is still fully type-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
The demo compiles its TypeScript on the fly in a service worker, and that worker's fetch handler returned early for any URL not starting with https://. Served from localhost over http nothing was ever intercepted, so no TypeScript was transpiled: the page rendered, but none of its code ran and every button was inert with no error to explain why. A service worker runs on http://localhost and http://127.0.0.1 because those are secure contexts, so the check is now about the origin rather than the scheme. Compiled output for a local origin is also no longer cached, so editing a source file doesn't serve a stale build. The same mistake gated the page itself: it compared location.protocol against 'https:' to decide whether to show a "WebUSB requires HTTPS" warning. It now reads window.isSecureContext, which is the condition WebUSB actually gates on. ts-browser.js posted every inline script to the worker up front while reassigning navigator.serviceWorker.onmessage each time round the loop, so only the last handler survived. Replies carry no id and the worker compiles asynchronously, so they can arrive in a different order than the requests were sent: the wrong blob could be executed, and the promise for every script but one never settled. Scripts are now compiled one at a time. The worker also asks for root scope where the server allows it. The import map reaches outside demo/ for the library source, which a worker registered at its default scope cannot intercept. It falls back to the default scope when the server sends no Service-Worker-Allowed header, as GitHub Pages doesn't. Adds Wincor Nixdorf to the device filters, tested against a TH230. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HIGHLY VIP, EXTREMELY VIBE CODED, HERE MOSTLY TO KEEP TRACK OF THINGS, FIXES AND IMPROVEMENTS WILL BE UPSTREAMED.