Cross-platform port: WPF → Avalonia 12 + .NET 10 (Windows/macOS/Linux)#3
Open
danielmeza wants to merge 82 commits into
Open
Cross-platform port: WPF → Avalonia 12 + .NET 10 (Windows/macOS/Linux)#3danielmeza wants to merge 82 commits into
danielmeza wants to merge 82 commits into
Conversation
…port, CI/CD) (#1) Squash-merge of the Avalonia/.NET 10 migration and feature work.
Unsigned bundles are reported as "damaged and can't be opened" by Gatekeeper on Apple Silicon when downloaded. make-macos-app.sh now ad-hoc signs the assembled bundle (codesign --force --deep --sign -) as the final step, turning that into the normal quarantine prompt. Not notarized — README documents clearing the quarantine flag (xattr -dr com.apple.quarantine) on first launch. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… app dir (#3) The Test print and Hex Dump buttons read test_receipt.txt with a bare relative path. When the app is launched from Finder/Explorer the process working directory is "/" (not the app folder), so File.Exists fails and the buttons silently do nothing — "no receipt". Resolve the path against AppContext.BaseDirectory (the executable's directory, where the file is copied) so it works regardless of CWD. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reen toast)
Previously Beep() only wrote a terminal bell (inaudible from a GUI app) and the
flash was Windows-only, so on macOS/Linux the buzzer and cash-drawer events
produced nothing visible or audible.
- NotificationService now plays a best-effort system sound: afplay (macOS),
Console.Beep on a background thread (Windows), paplay/aplay/canberra (Linux),
terminal-bell fallback.
- The view model shows a transient on-screen toast ("🔔 Buzzer" /
"💵 Cash drawer opened") that auto-hides — always-visible feedback since the
machine may be muted. New ToastMessage/ToastVisible bound to an overlay in
MainWindow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tate panel Foundation for status reporting and simulation: - PrinterState (observable): online, cover, paper level, drawer sensor, error, feed button. - IPrinterResponder + duplex transports: NetClient/SerialServer register as responders and can write bytes back to the host; FeedEscPos carries the active responder. - Real-time DLE path in the interpreter: DLE EOT n (status 1-4), DLE ENQ (recover), DLE DC4 1 (drawer pulse). StatusByteBuilder computes ESC/POS status bytes from state. - Commands: GS r (paper/drawer status), GS I (printer ID), GS a (Automatic Status Back, pushes a 4-byte block on every state change). - Right-side 'Printer state' panel bound two-way to PrinterState (online/cover/paper/ error/drawer/feed). Off-thread state writes (drawer kick over TCP) marshal to the UI thread via printer.UiDispatch. Verified over TCP: DLE EOT/GS r return correct bytes; ESC p flips drawer status (0x12->0x16, GS r 2 0->1).
…D symbols - ESC ( A beeper -> Buzz(); ParenCommand base for length-prefixed ESC(/GS( commands. - Config no-ops parsed-and-ignored: GS ( E (user setup), GS ( K (print control), GS ( H (response request), GS P (motion units). - Generalize GS ( k to all 2D families: PDF417 (cn=48), QR (cn=49, QRCoder), DataMatrix (cn=54), Aztec (cn=55) via ZXing BarcodeRenderer.Render2D. Verified: PDF417/DataMatrix/Aztec render and round-trip decode to their content.
- ESC * m nL nH ...: classic inline bit-image band (8-dot m=0/1, 24-dot m=32/33), column-major vertical bytes rendered to SKBitmap. - GS * x y ...: define downloaded bit image (x*8 by y*8); GS / m prints it with scaling (0 normal, 1 dw, 2 dh, 3 dwh). Verified ESC * renders a recognizable image.
- ESC L buffers output into an off-stack receipt; FF rasterizes it onto the receipt and returns to standard mode; ESC S / CAN discard the buffer. - ESC W (print area), ESC T (direction), ESC $ / GS $ / GS \ (positioning) are parsed and accepted (approximated — content is buffered then rasterized). - FixedArgNoOpCommand consumes fixed-length args for positioning commands. Verified: text buffered in page mode prints on FF; standard text resumes after.
- ESC t selects the character code table; high bytes are remapped through the active code page (PC437/850/852/858/860/863/865/866/1252, Katakana) to Unicode before rendering. Registers CodePagesEncodingProvider for cross-platform support. - ESC & parses and stores user-defined glyph bitmaps (column-major); ESC % toggles the set; ESC ? cancels a glyph. (Inline glyph substitution during text rendering is not applied — the font glyph is drawn; these rarely appear in modern streams.) Verified: CP1252 maps 0xE9/0xEF -> é/ï; ESC & stores a 4x24 glyph; text after the sequence prints without corruption.
…image Regenerate test_receipt.txt via scripts/gen-test-receipt.py to exercise every supported feature: text styles (bold/italic/underline/font B/double sizes), alignment, all 1D barcodes (UPC-A, EAN-13/8, CODE39/93/128, ITF, CODABAR), all 2D symbols (QR, PDF417, DataMatrix, Aztec), an ESC * bit image, and the buzzer + cash drawer. Verified all barcodes/2D render without errors.
- New Monitor window (separate) using ESC-POS-.NET (NetworkPrinter + EPSON emitter) connects to the emulator over TCP and can: print a sample receipt, all 1D barcodes, QR/PDF417/DataMatrix/Aztec, the full feature test, open the drawer, buzz, and cut. - Subscribes to StatusChanged and enables Automatic Status Back, so toggling the Printer state panel updates the monitor's status display live. - Fix StatusByteBuilder.AutoStatusBack to match the ESC-POS-.NET 4-byte ASB parser (byte0 bit4 fixed, inverted drawer bit, paired paper-low/out bits). Verified end-to-end: paper-low/out, cover, drawer and offline all round-trip to PrinterStatusEventArgs. - 'Open monitor' button in the main window (single instance).
- Like a real printer, the emulator now refuses to print when not ready (out of paper, cover open, offline, or error): print/barcode/2D/bit-image ops are dropped and an OnPrintBlocked notification fires (shown as a toast). Notifies once per blocked episode and re-arms when the printer becomes ready again. - Monitor: replace the plain text status with colored indicator rows (dot + label + value) for Printer/Paper/Cover/Cash drawer/Error, plus a Ready/Not ready summary. Verified: printing blocked on paper-out/cover-open/offline and resumes when restored (single notification per episode).
LineFeed was not gated, so while printing was blocked the paper-feed commands still added empty lines (and cuts started receipts), producing blank receipt cards. Gate LineFeed on the ready state too; a not-ready printer now produces no output at all. Verified: sending the full test receipt with the cover open yields 0 receipts; ready yields output.
…fies The 'blocked' latch only reset when the printer returned to ready, so repeated print attempts while still not-ready (e.g. clicking Test print again with the cover open) were silently suppressed and showed no toast. Reset the latch at the start of each FeedEscPos so every received job that is blocked notifies once (still deduped within a single job). Verified: 3 separate blocked jobs -> 3 notifications; one multi-print job -> 1.
…gic constants
- Enable TreatWarningsAsErrors and NuGetAuditMode=all: build/CI now fails on any compiler,
analyzer, or NuGet security-audit warning (verified: a vulnerable transitive package
produces 'error NU1903: Warning As Error').
- Fix existing warnings:
- CA1416: repeat the OperatingSystem.IsWindows() guard inside the Task.Run lambda so
the Console.Beep call site is platform-checked; drop the annotated helper.
- NU1902/NU1903: override ESCPOS_NET's vulnerable transitive SixLabors.ImageSharp 2.1.3
with patched 2.1.13.
- NU1510: drop System.Text.Encoding.CodePages (provided by the framework on .NET 10;
code-page remapping still works).
- Replace magic numbers with named constants: DLE real-time sub-commands (EOT/ENQ/DC4/pulse)
and lengths, GS ( k function codes, a shared TwoDimensionCode (cn) type used by the printer,
the GS ( k command and the monitor, and BEL/module-size in the monitor.
Build is clean (0 warnings) in Debug and Release.
…e Monitor - Notifications (buzzer / cash drawer / print-blocked for text, barcode, QR, etc.) were cut short when several fired close together: each ShowToast scheduled its own one-shot timer and an earlier timer would hide a later toast. Use a single re-armable DispatcherTimer restarted on each toast, and lengthen the duration to 3.5s. - Remove the emulator's 'Test print' button/command: the emulator is the device and the Monitor window is the client that drives it over the wire (Send full feature test, all barcodes, QR/PDF417/DataMatrix/Aztec, drawer, buzz, cut). README updated to reflect this.
Add the monitor client screenshot and a side-by-side showing live status sync (the emulator's Printer state panel pushing paper-low / recoverable-error to the monitor, which then reports Not ready) to the Monitor section.
The Monitor window now offers a transport toggle (TCP/IP or Serial). Serial uses ESC-POS-.NET's SerialPrinter (port picker + baud, with a refresh button) sharing the same BasePrinter Write/StatusChanged path as the TCP NetworkPrinter, so sending jobs and the live status round-trip work over either transport. Pairs with the emulator's serial transport via a virtual port bridge (socat/com0com). Verified status parsing over serial.
The monitor's transport selector now offers TCP / Serial / USB. USB enumerates connected devices (VID:PID) via LibUsbDotNet/libusb, opens the selected one, claims its first interface and writes ESC/POS to the bulk-OUT endpoint — for printing directly to a real USB receipt printer that isn't exposed as a COM port. Send-only (no status read-back). - New UsbBulkTransport (enumerate / open by VID:PID / write); lazily loads libusb so the app still runs without it, and surfaces a clear error if libusb is missing or the OS already owns the device. - Monitor VM holds either an ESC-POS-.NET BasePrinter (TCP/serial) or the USB transport; Send writes to whichever is active. - README documents the USB transport and its native libusb requirement. Note: USB depends on native libusb and a real device, so it could not be exercised in CI.
LibUsbDotNet P/Invokes 'libusb-1.0', but .NET's default loader doesn't search Homebrew (/opt/homebrew/lib) or manual install paths, so an installed libusb still fails to load. Register a DllImportResolver on the LibUsbDotNet assembly that locates libusb across common macOS/Linux/Windows locations (and a copy shipped next to the executable), falling back to default resolution. Registered lazily in UsbBulkTransport's static ctor. Users still need libusb present (macOS: brew install libusb); the resolver makes an installed copy discoverable without extra DYLD/LD path configuration.
…vity log - When libusb can't be loaded (refresh or connect over USB), log a clear, multi-line message explaining USB is unavailable and how to install libusb for the current OS (brew/apt), instead of a raw exception. - Activity log entries are now selectable text, plus a 'Copy' button that copies the whole log (chronological) to the clipboard via Avalonia 12's clipboard API.
…f a resolver The previous DllImportResolver was registered on the LibUsbDotNet assembly — but LibUsbDotNet registers its OWN resolver on that same assembly in a static initializer, and the duplicate registration threw during its type init (surfacing as 'Unable to load libusb-1.0' even when libusb was installed). LibUsbDotNet's resolver searches NATIVE_DLL_SEARCH_DIRECTORIES (then the default OS path), which omits Homebrew/manual locations. So instead of a resolver, append the common libusb dirs (/opt/homebrew/lib, /opt/homebrew/opt/libusb/lib, /usr/local/lib, Linux lib dirs) to that AppContext list in UsbBulkTransport's static ctor, before any libusb call. No symlinks, no DYLD_LIBRARY_PATH. Verified: with libusb installed via brew, the app now enumerates USB devices (incl. 0471:0055).
…inter) Replace the send-only UsbBulkTransport with a UsbPrinter that derives from ESC-POS-.NET's BasePrinter, backed by a USB-endpoint Stream. Writes go to the bulk-OUT endpoint and status is read from the bulk-IN endpoint, so USB now joins the same write-queue + Automatic-Status-Back pipeline as the serial/TCP printers and the monitor reflects the printer's reported state over USB too. - Fix "SafeHandle cannot be null" on send: the device collection was disposed in Open(), invalidating the device handle before the write. Keep the collection + context alive for the connection's lifetime (owned by the stream, freed on Dispose). - UsbStream buffers bulk-IN reads (USB is packet-oriented; BasePrinter reads the status channel one byte at a time) and returns 0 on timeout so the read loop stays responsive without spinning. - Auto-detect bulk OUT/IN endpoints from the interface descriptors (direction via bEndpointAddress bit 7, type via bmAttributes); fall back to Ep01 for output, and treat a missing bulk-IN as send-only rather than failing. - Keep the NATIVE_DLL_SEARCH_DIRECTORIES libusb-discovery fix. - Monitor: drop the separate send-only _usb field; USB connects as a BasePrinter and shares the StatusChanged / EnableAutomaticStatusBack path. Verified on a real printer (0471:0055): open + interface claim succeed, write returns with no SafeHandle error, and an ASB status block reads back and parses.
…image Regenerate test_receipt.txt via scripts/gen-test-receipt.py to exercise every supported feature: text styles (bold/italic/underline/font B/double sizes), alignment, all 1D barcodes (UPC-A, EAN-13/8, CODE39/93/128, ITF, CODABAR), all 2D symbols (QR, PDF417, DataMatrix, Aztec), an ESC * bit image, and the buzzer + cash drawer. Verified all barcodes/2D render without errors.
- New Monitor window (separate) using ESC-POS-.NET (NetworkPrinter + EPSON emitter) connects to the emulator over TCP and can: print a sample receipt, all 1D barcodes, QR/PDF417/DataMatrix/Aztec, the full feature test, open the drawer, buzz, and cut. - Subscribes to StatusChanged and enables Automatic Status Back, so toggling the Printer state panel updates the monitor's status display live. - Fix StatusByteBuilder.AutoStatusBack to match the ESC-POS-.NET 4-byte ASB parser (byte0 bit4 fixed, inverted drawer bit, paired paper-low/out bits). Verified end-to-end: paper-low/out, cover, drawer and offline all round-trip to PrinterStatusEventArgs. - 'Open monitor' button in the main window (single instance).
- Like a real printer, the emulator now refuses to print when not ready (out of paper, cover open, offline, or error): print/barcode/2D/bit-image ops are dropped and an OnPrintBlocked notification fires (shown as a toast). Notifies once per blocked episode and re-arms when the printer becomes ready again. - Monitor: replace the plain text status with colored indicator rows (dot + label + value) for Printer/Paper/Cover/Cash drawer/Error, plus a Ready/Not ready summary. Verified: printing blocked on paper-out/cover-open/offline and resumes when restored (single notification per episode).
LineFeed was not gated, so while printing was blocked the paper-feed commands still added empty lines (and cuts started receipts), producing blank receipt cards. Gate LineFeed on the ready state too; a not-ready printer now produces no output at all. Verified: sending the full test receipt with the cover open yields 0 receipts; ready yields output.
…fies The 'blocked' latch only reset when the printer returned to ready, so repeated print attempts while still not-ready (e.g. clicking Test print again with the cover open) were silently suppressed and showed no toast. Reset the latch at the start of each FeedEscPos so every received job that is blocked notifies once (still deduped within a single job). Verified: 3 separate blocked jobs -> 3 notifications; one multi-print job -> 1.
1.1.0 adds the WASM JavaScript interop module (render ESC/POS to PNG from any web project), package usage docs, raw byte[] FeedEscPos overloads, and converts the remaining magic-number switches to SmartEnum object models. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds CrossEscPos.Rendering.ImageSharp, a 100% managed render backend (SixLabors.ImageSharp) implementing the graphics abstractions alongside Rendering.Skia. With no native dependency it renders ESC/POS to PNG in Blazor WASM without a native relink; output is byte-compatible with the Skia backend. Includes ImageSharpRenderTests (mirror of SkiaRenderTests; suite 129 green), the README package row, and the release.yml pack entry.
Publish the packable libraries to the GasoleoTechnology org on nuget.org using OIDC Trusted Publishing (no stored API key): - release.yml: add publish-nuget job (NuGet/login OIDC -> dotnet nuget push), gated to v* tags and the `release` environment; upload .snupkg symbols alongside .nupkg from the pack job. - Directory.Build.props: MIT license expression, packed README, Company, Copyright, and Source Link (repo/commit + untracked sources). - Directory.Build.targets: pack README.md into every packable package. - LICENSE: MIT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a managed ImageSharp render backend (browser-safe, no native)
Publish NuGet packages to GasoleoTechnology via Trusted Publishing
ImageSharp MeasureText used TextMeasurer.MeasureSize (glyph bounds), which drops side bearings and trailing whitespace and diverged from the Skia backend's advance width — shifting justified and padded lines. Use MeasureAdvance and count trailing whitespace via a sentinel so justification, multi-run advance, and underline length match Skia exactly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a --backend arg (and ESCPOS_RENDER_BACKEND env var) to choose the SkiaSharp or managed ImageSharp backend, resolved through a small named registry and surfaced in the window title for visual A/B testing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
samples/CrossEscPos.Web renders ESC/POS in the browser with a runtime engine selector (managed ImageSharp by default, native SkiaSharp), Razor ReceiptView / PrinterStatePanel mirroring the desktop controls, and paste/upload input. Replaces the old [JSExport] src/CrossEscPos.Wasm demo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release the managed ImageSharp render backend (new feature) plus the ImageSharp text-measurement parity fix and the Blazor web sample. v1.1.0 is already published. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release v1.2.0: ImageSharp measurement fix, desktop backend switch, Blazor web sample
…guide Updates the package docs for the v1.2.0 managed ImageSharp backend and the Blazor web sample: adds ImageSharp to the package table/diagrams, replaces the retired wasm-interop guide with a Blazor web guide, and expands the rendering guide with a 'writing your own backend' section that cites yhonc9's PR #11 as the worked sample. Links to the new project wiki. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docs: ImageSharp backend, Blazor web guide, and a render-backend guide
Extracts the Avalonia App, a shared MainView (UserControl) and MainViewModel from the desktop app into a new CrossEscPos.App library, behind an IPlatformServices seam. The shared app owns the platform-agnostic features (paste/upload ESC/POS input + render, receipts, printer state, export via Avalonia's cross-platform storage provider, buzzer/drawer toast) and hosts the platform's transport UI in a connections slot. The Desktop head becomes thin: DesktopPlatformServices (Skia/ImageSharp backend, native dialogs/notifications, TCP+serial ConnectionsView, Monitor window) + a shell window. Verified: builds clean (0 warnings/0 errors) and the desktop app launches through the shared composition. Browser head, WebSocket transport + WsProxy, and Blazor retirement are later phases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ts + WsProxy, retire Blazor Phase 2 — the Browser head becomes a thin head over CrossEscPos.App: BrowserPlatformServices (Skia backend, Avalonia storage-provider export = a download, no-op notifications, a Web Serial/WebUSB/WebSocket connections view). Its old bespoke App/View/ViewModel are gone; the shared MainView/MainViewModel now render in the browser single view. FileDialogService moved to the shared app and resolves its TopLevel lazily so export works in a window and a single-view alike. Phase 3 — WebSocketTransport (cross-platform, ClientWebSocket — works in WASM) + samples/CrossEscPos.WsProxy, an ASP.NET Core TCP:9100 <-> WebSocket bridge so the browser app gets TCP reception (browsers can't open raw sockets). WasmJsTransportBridge wires the shared transports.js over [JSImport]/[JSExport]. Phase 4 — retire the Blazor samples/CrossEscPos.Web (superseded by the browser head with full parity); update slnx + README. Verified: desktop, browser (WASM), and WsProxy all build 0 errors; desktop runs. Browser runtime + device transports need a real browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ut native Web Serial
transports.js now acquires a SerialPort-shaped object from WebUSB (SET_LINE_CODING + SET_CONTROL_LINE_STATE, bulk in/out streams) when navigator.serial is absent — the JS 'serial port lib' brought to the browser (mirrors google/web-serial-polyfill). The read/write stream code is unchanged since the polyfill port exposes readable/writable/open/close/getInfo. isSupported('serial') is now true wherever WebUSB exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ool)
Threads a baud rate through the transport abstraction: WebTransport.Options -> IJsTransportBridge.ConnectAsync(kind, options) -> transports.js connect(kind, baud), applied to both native port.open({baudRate}) and the WebUSB CDC SET_LINE_CODING. A Baud field on the browser connections panel (default 9600), matching the desktop serial panel and how esptool.spacehuhn.com opens its port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the two per-head connections panels with a single data-templated view over a shared TransportEntry/TransportField model exposed by IPlatformServices.CreateTransports. Desktop yields TcpTransportEntry + SerialTransportEntry (address/port, port/baud + refresh); browser yields Web Serial / WebUSB / WebSocket entries (wrapping the IReceiptTransport transports). Each transport self-describes its name, config fields (text or dropdown), toggle action, and status, so one shared ConnectionsView in MainView renders them all. Removes DesktopConnectionsView/VM + BrowserConnectionsView/VM. Verified: both heads build 0 errors; the uniform connections UI renders in the browser (screenshot). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates the browser backend into one ASP.NET Core host (CrossEscPos.Host, replacing the WsProxy): it serves the Avalonia WASM app AND hosts the SignalR broker + TCP:9100 bridge on one origin, so the browser reaches /bridge with no CORS. A net10.0 host can't ProjectReference the net10.0-browser WASM head (unlike Blazor's hosted template), so the host publishes the client into wwwroot (generated, gitignored). The bridge is strongly typed on both ends: a dependency-free CrossEscPos.Bridge project holds IBridgeServer (client->server) and IBridgeClient (server->client); BridgeHub is Hub<IBridgeClient> implementing IBridgeServer, and the WASM clients invoke the server through a shared BridgeServerProxy (no magic strings). The Monitor (test client) is now shared: MonitorView + MonitorViewModel + test-job generation live in CrossEscPos.App behind an IMonitorClient seam. Desktop implements it over TCP/serial/USB (ESC-POS-.NET) in a window; the browser implements it as a SignalR sender that round-trips through the host to the in-page emulator, hosted as an in-page overlay. Status is unified via MonitorStatus (desktop maps ESC-POS-.NET; browser parses the 4-byte ASB block). Removes the desktop-only MonitorWindowViewModel. Also: restore TreatWarningsAsErrors=true on the browser head (the blanket opt-out was stale; a targeted CS0067 pragma covers the one interface event a client doesn't raise); default the browser SignalR URL to the served origin. Verified: full solution builds 0/0; 131 tests pass; SignalR broker round-trips a job+status end-to-end against the running host; the app boots served from the host with the Monitor wired into the browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SignalR emulator transport now sends a listen address + port with AttachEmulator; the hub opens a per-session TcpListener on it (throwing a HubException the client surfaces if the port can't be bound) and tears it down on disconnect (OnDisconnectedAsync) or re-attach. POS software connects to that port and its jobs bridge to the in-page emulator, status flowing back to the socket. Removes the host's fixed boot-time :9100 listener. The emulator re-attaches after an automatic reconnect, so connect/disconnect (and drops) are seamless. The connections panel gains Listen address + Listen port fields (ReceiptTransportEntry now takes multiple fields + an apply-before-connect hook, and shows connect errors). Verified against the running host: an emulator opens 127.0.0.1:9199 and both a SignalR monitor and a raw TCP POS client round-trip a job + status through it; full solution builds 0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nsport README now describes CrossEscPos.Host (serves the WASM app + SignalR broker on one origin), the browser's SignalR TCP proxy with a caller-chosen listen port, and the shared Monitor (desktop window / browser overlay). Removes the unused WebSocketTransport (superseded by SignalRTransport) and a couple of stale WsProxy comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lution build) The PublishWasmClient target ran a nested 'dotnet publish' of the browser head during Build, which rebuilt the shared libs concurrently with the outer solution build and raced on CrossEscPos.Controls.pdb (AVLN9999) on a fresh CI clone. It's now gated behind -p:PublishWasmClient=true, so plain/solution/CI builds never trigger it; run/publish the host with the flag to populate wwwroot (cached after). Also fixes an illegal '--' in the csproj comment and updates the README run command. Verified: Release solution build with no flag + no wwwroot builds 0/0 (no race); the disconnect flow frees the TCP port and a re-attach on the same port succeeds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…kip on solution build Two bugs made 'dotnet run --project samples/CrossEscPos.Host' serve a 404 for /index.html: (1) the wwwroot-populate target was gated off entirely (opt-in) so a plain run never populated it; (2) the nested publish used a relative PublishDir (), which resolved against the client project — the output landed in the client's obj and the copy found nothing (I'd only ever populated wwwroot by hand). Now the publish dir is absolute, and the target runs automatically for a project/run/publish build but is skipped during a solution build ($(SolutionPath) is set), which is where it raced the shared-lib build on CI. Overridable with -p:PublishWasmClient=true/false. Verified: a bare 'dotnet run' self-populates wwwroot and serves / , /index.html, /_framework/*, and /bridge all 200; a Release solution build with no wwwroot still builds 0/0 (no race). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…SB / proxy) The browser Monitor was single-mode (SignalR proxy only). It now has the same shape as desktop: a transport selector + per-mode fields. WebMonitorClient (replacing SignalRMonitorClient) offers three modes — SignalR proxy (round-trip to the in-page emulator), Web Serial (baud), and WebUSB (paired-device list by VID:PID). Serial/USB drive a real printer through the shared JS bridge on their own sender channels (mon-serial/mon-usb) so they don't clash with the emulator's connections; device selection is the browser's native picker, and paired USB devices are listed via navigator.usb.getDevices(). transports.js: serial/usb are now per-kind factories behind a channels map (so emulator + Monitor can each own a device); usb connect can target a getDevices() index; added crossescpos.listUsb. IJsTransportBridge gains ListUsbDevicesAsync (newline-joined, trim-safe — no reflection JSON). BrowserPlatformServices shares one bridge instance between the connections and the Monitor. Verified in-browser: the Monitor overlay renders the transport dropdown + fields; the app boots clean; full solution builds 0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stem Access API
In the browser, export silently did nothing: Avalonia's storage provider goes through showSaveFilePicker (Chromium-only, and its writable stream rejects the encoder's synchronous writes). The browser head now implements IFileDialogService itself — SavePngAsync returns a MemoryStream that, on dispose, blobs its bytes and downloads them via an anchor (works in every browser). Desktop is unchanged (native save dialog). Folder export ('each cut') stays desktop-only.
Verified in-browser: 'Export all' fires a download of a valid receipts.png (PNG magic confirmed); the per-cut PNG button uses the same path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…proxy The browser Monitor now matches the desktop transports. Added a Network (TCP) mode: the browser can't open a raw socket, so it asks the host (IBridgeServer.ConnectTcp) to open an outbound TcpClient to the printer's host:port; the monitor's jobs are written to that socket and its status replies routed back via ReceiveStatus. The hub tracks the outbound socket per connection, routes SendToEmulator to it, and closes it on disconnect. So the Monitor now offers SignalR proxy (in-page emulator) + Network (TCP) + Web Serial + WebUSB. Verified end-to-end against the running host: the monitor dialed a fake TCP printer, which received the job and replied a status that flowed back (TCP-OUT OK); the Monitor dropdown shows all four transports; full solution builds 0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On desktop the TCP listener auto-starts; the browser now matches. The SignalR proxy transport connects on load without a user gesture (it's gesture-free, unlike Web Serial/USB) and attaches the emulator, so the host opens the TCP listener (default 0.0.0.0:9100) immediately — POS software can print to the in-page emulator with no manual 'Connect'. ReceiptTransportEntry gained an autoConnect flag (only the SignalR entry uses it); it no-ops quietly if the hub isn't reachable (app served without the host). Verified end-to-end: loading the served app opens :9100 automatically ([hub] emulator attached; listening on 0.0.0.0:9100), and a raw TCP job sent to :9100 with no UI interaction rendered in the in-page emulator (Received: ESC @ AUTO-TCP). Full solution builds 0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… updates The browser Monitor never sent GS a, so the emulator's _asbMask stayed 0 and it never broadcast status on state changes — the Monitor's status indicators sat blank. WebMonitorClient now sends GS a (enable ASB) right after connecting, exactly like the desktop DesktopMonitorClient does. The emulator then pushes a 4-byte ASB block on every state change, routed back to the monitor as the current sender. Verified end-to-end against the served app: a monitor enabled ASB, got the initial status (14 00 00 00, online), then toggling the emulator's Online switch in the UI pushed 1C 00 00 00 (offline) and toggling back pushed 14 00 00 00 (online). Full solution builds 0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Connections panel left its config fields (proxy URL, listen address/port, baud) editable while connected — you could change the endpoint mid-session with no effect. They're now disabled (IsEnabled='{Binding !IsActive}'), along with the refresh button, matching the Monitor view (which already locked its fields when connected) and the desktop original.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unify desktop + browser into one shared Avalonia app; SignalR host + shared Monitor
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.
This ports the emulator from Windows-only WPF to a cross-platform Avalonia 12 + SkiaSharp + .NET 10 app that runs on Windows, macOS and Linux, and extends it with a few commonly requested features. Opening it upstream in case it's useful — happy to split it into smaller PRs or adjust anything if you'd prefer to take it in pieces.
Core port
net9.0-windows→net10.0(noUseWPF).New features
GS k): UPC-A/E, EAN-13/8, CODE39/93/128, ITF, CODABAR (both function A & B forms) with HRI text — via ZXing.Net.GS ( k, cn=49): model / module size / EC level / store / print — via QRCoder.ESC p) and buzzer (BEL) — surfaced as a sound + on-screen toast.Packaging
.appbundle) and attaches them to a release on eachvX.X.Xtag.Notes