Skip to content

Cross-platform port: WPF → Avalonia 12 + .NET 10 (Windows/macOS/Linux)#3

Open
danielmeza wants to merge 43 commits into
roydejong:mainfrom
danielmeza:main
Open

Cross-platform port: WPF → Avalonia 12 + .NET 10 (Windows/macOS/Linux)#3
danielmeza wants to merge 43 commits into
roydejong:mainfrom
danielmeza:main

Conversation

@danielmeza

Copy link
Copy Markdown

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

  • UI: WPF → Avalonia 12, restructured into MVVM (CommunityToolkit.Mvvm) instead of code-behind.
  • Rendering: all receipt drawing moved from GDI+/System.Drawing (Windows-only) to SkiaSharp, so it works on every OS. Output is made deterministic by bundling JetBrains Mono (OFL) instead of relying on OS fonts.
  • Target: net9.0-windowsnet10.0 (no UseWPF).

New features

  • Barcodes (GS k): UPC-A/E, EAN-13/8, CODE39/93/128, ITF, CODABAR (both function A & B forms) with HRI text — via ZXing.Net.
  • QR codes (GS ( k, cn=49): model / module size / EC level / store / print — via QRCoder.
  • Cash drawer (ESC p) and buzzer (BEL) — surfaced as a sound + on-screen toast.
  • Serial transport in addition to TCP, both configurable live in the UI (listen address/port, serial port/baud). Includes an app-to-app virtual-port testing guide (socat/com0com).
  • Export rendered tickets to PNG — all stacked into one image, or one file per cut/page.

Packaging

  • App icon, and a GitHub Actions release pipeline that publishes self-contained Windows/Linux/macOS builds (macOS as a signed .app bundle) and attaches them to a release on each vX.X.X tag.

Notes

  • This is a large diff because the rendering and UI layers were rewritten. The ESC/POS interpreter, command classes and networking are largely preserved in structure.
  • The repo was renamed to CrossEscPosEmulator on the fork; README credits this project as the upstream original.

danielmeza and others added 30 commits June 5, 2026 01:42
…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.
danielmeza added 13 commits June 6, 2026 09:25
…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.
…port while connected

Disconnecting a USB session could hard-crash the app: BasePrinter's background
read loop issues a native libusb bulk read, and Dispose freed the device/context
(via the reader/writer close) while that read was still in flight — a native
use-after-free no managed catch can stop.

- UsbStream now serializes all native I/O and the device/context teardown under a
  single lock, with a _closing flag that short-circuits any read/write issued
  during/after teardown so it never touches a freed handle. Dispose flags first,
  then takes the lock to wait out any in-flight transfer (bounded by the I/O
  timeouts) before freeing the device and context. Verified on a real printer
  (0471:0055): 5 connect/disconnect cycles with the read loop active survive, and
  status still reads back.
- Logger: persist messages to a rolling log file (LocalApplicationData/
  CrossEscPosEmulator/logs/app.log) and add InstallGlobalHandlers() for
  AppDomain.UnhandledException + TaskScheduler.UnobservedTaskException (the latter
  SetObserved so a faulted background task can't silently kill the process).
  Installed first thing in Program.Main.
- Monitor: lock the transport selector and the TCP/Serial/USB endpoint fields while
  connected so the transport can't be switched mid-session.
Fix app crash on USB disconnect; log unhandled exceptions; lock transport while connected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant