VYROX PRINTER CONNECTOR - DEVELOPER INTEGRATION GUIDE

Version:  2.0
Updated:  2026-02-04
Website:  https://vyrox.com
================================================================================


TABLE OF CONTENTS
================================================================================

  1.  Overview
  2.  Architecture
  3.  Prerequisites
  4.  Quick Start (Minimum Viable Print)
  5.  HTML Print Content Structure
  6.  Method A: Auto-Detection (HTML Element)
  7.  Method B: JavaScript postMessage API
  8.  Method C: Popup Trigger (Manual Print)
  9.  Formatting Tags Reference
      9.1   Text Formatting
      9.2   Alignment
      9.3   Character Size
      9.4   Spacing
      9.5   Structure and Cutting
      9.6   Separators
      9.7   Barcodes
      9.8   QR Codes
      9.9   Images
      9.10  Tables and Columns
      9.11  Code Pages and Character Sets
      9.12  Hardware Control
      9.13  Margins and Print Area
      9.14  Raw Bytes
      9.15  Reset
  10. HTML-to-Text Auto-Conversion Rules
  11. Paper Size Reference
  12. Full Print Examples
      12.1  Simple Retail POS
      12.2  Restaurant Kitchen Order
      12.3  Retail with Barcode and QR
      12.4  Multi-Language Print
      12.5  Minimal JavaScript-Only Print
      12.6  Sports Facility Booking Confirmation
      12.7  Snooker and Billiard Table Billing
      12.8  Sports Booking with Membership Discount
      12.9  Snooker Tournament Scorecard
      12.10 Sports Complex Multi-Activity Booking
      12.11 Snooker Table Session Timer Slip
  13. CSS Classes Auto-Detection
  14. Existing Print Button Interception
  15. Error Handling
  16. Debugging and Troubleshooting
  17. Security Considerations
  18. Browser Compatibility
  19. Advanced Integration Patterns
      19.1  Auto-Print After AJAX Order Submission
      19.2  Print Queue with Multiple Jobs
      19.3  Dual-Copy Printing (Customer + Merchant)
      19.4  Conditional Print Based on Printer Status
      19.5  Framework Integration (React, Vue, Angular)
  20. Frequently Asked Questions

1. OVERVIEW

VYROX Printer Connector is a Chrome extension that sends print jobs from any
web page to a locally connected ESC/POS thermal printer via USB.

It eliminates the need for:
  - Browser print dialogs
  - Print preview windows
  - CSS print stylesheets
  - PDF generation
  - Server-side printing software
  - Printer-specific SDKs or drivers

The extension detects print content on your web page, converts it to ESC/POS
commands, and sends it directly to the thermal printer through a native host
bridge running on the user's Windows computer.

Who this guide is for:
  - Web developers building POS, billing, or order management systems
  - PHP, Node.js, Python, or any backend developer serving web pages
  - Frontend developers working with React, Vue, Angular, or plain HTML
  - Anyone who needs silent thermal printing from a web application
  - Sports facility management system developers
  - Snooker, billiard, and pool hall billing system developers
  - Booking and reservation system developers

2. ARCHITECTURE

The print flow works as follows:

  Your Web Page (HTML/JS)
       |
       | (1) User clicks print, or your JS sends a message
       |
       v
  Chrome Extension (content.js)
       |
       | (2) Detects print content element OR receives postMessage
       | (3) Converts HTML to tagged plain text (if HTML method)
       |
       v
  Chrome Extension (background.js)
       |
       | (4) Sends data via Chrome Native Messaging
       |
       v
  Native Host (PowerShell on Windows)
       |
       | (5) Writes content to print.this file
       | (6) Print loop picks up file
       | (7) Parses formatting tags into ESC/POS byte commands
       |
       v
  USB Thermal Printer (ESC/POS)
       |
       | (8) Paper prints and cuts
       v
     Done

Key points:
  - The extension runs on ALL web pages (content script injected globally)
  - No server-side component is needed on YOUR server
  - The native host runs locally on the user's Windows machine
  - Communication is one-way: web page -> printer
  - The extension handles all ESC/POS encoding automatically
  - Your web page never needs to know about ESC/POS commands

3. PREREQUISITES

End-user requirements (your customer's computer):

  1. Google Chrome browser (version 88 or later)
  2. VYROX Printer Connector extension installed from Chrome Web Store
     or loaded as unpacked extension
  3. VYROX Printer Connector native host installed and running
     (VYROX_PrinterConnector.exe, runs on Windows)
  4. USB thermal printer connected and detected by the native host
  5. Windows operating system (the native host is Windows-only)

Developer requirements (your side):

  1. A web page served over HTTP or HTTPS
  2. Basic HTML knowledge
  3. Optionally: JavaScript for programmatic printing

You do NOT need:
  - Any special SDK or library
  - Any npm package
  - Any server-side print software
  - Any modification to your backend
  - Any WebSocket or API server

4. QUICK START (MINIMUM VIABLE PRINT)

The absolute minimum to get printing working is ONE HTML element with a
recognized ID or class. Here is the simplest possible example:

  <div id="vyrox_printer_connector">
    My Store Name
    123 Main Street
    --------------------------------
    Item A                      5.00
    Item B                      3.50

TOTAL 8.50

Thank you!
  </div>

That is it. When the extension detects an element with
id="vyrox_printer_connector" on the page, it automatically shows a floating
print button in the bottom-right corner. The user clicks it, and the content
prints on the thermal printer.

If you want to trigger printing from your own button without the user
clicking the floating button:

  <div id="vyrox_printer_connector">
    (your print content here)
  </div>

  <button onclick="window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: document.getElementById('vyrox_printer_connector').innerText
  }, '*')">
    Print Now
  </button>

If you do not have any HTML element on the page and want to print purely
from JavaScript:

  window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: '##CENTER####BOLD##Hello World\n##SEP##\nTest print\n##CUT##'
  }, '*');

That covers the core usage. Read on for full details and examples.

5. HTML PRINT CONTENT STRUCTURE

The extension looks for print content using CSS selectors, checked in this
priority order:

  1.  #vyrox_printer_connector
  2.  #vyrox-printer-connector
  3.  #vyrox_printer_connector-content
  4.  #print-area
  5.  .vyrox_printer_connector
  6.  .vyrox-printer-connector
  7.  .vyrox_printer_connector-content
  8.  .print-area
  9.  [data-vyrox-printer-connector="true"]

The FIRST matching element found is used as the print content source.

Recommended approach: use id="vyrox_printer_connector" on your container.

  <div id="vyrox_printer_connector">
    ... your print content ...
  </div>

Alternative approaches:

  Using the hyphenated ID:
    <div id="vyrox-printer-connector">...</div>

  Using a class:
    <div class="vyrox_printer_connector">...</div>

  Using a data attribute:
    <div data-vyrox-printer-connector="true">...</div>

  Using the print-area ID:
    <div id="print-area">...</div>

IMPORTANT: Only ONE print content element should exist on the page at a
time. If multiple matching elements exist, the first one found (in selector
priority order) will be used.

The print content element can be:
  - Visible on the page (normal display)
  - Hidden with CSS (display:none or visibility:hidden)
  - Dynamically created via JavaScript after page load
  - Inside a modal, tab, or any container

The extension watches for dynamically added elements using a
MutationObserver, so even if your print content is created after page load
(for example after an AJAX call or a single-page app route change), it
will be detected automatically.

6. METHOD A: AUTO-DETECTION (HTML ELEMENT)

This is the simplest method. You write your print content as HTML, and the
extension handles everything else.

How it works:
  1. The extension scans the page for a matching print content element
  2. If found, a floating print button appears (bottom-right corner)
  3. When clicked, the extension reads the HTML content
  4. It converts the HTML structure into formatted plain text
  5. Formatting tags (##CENTER##, ##BOLD##, etc.) are added automatically
     based on CSS styles and HTML structure
  6. The tagged text is sent to the native host for printing

Example print content in HTML:

  <div id="vyrox_printer_connector">
    <div style="text-align: center;">
      <h2>ACME STORE</h2>
      <p>123 Main Street, Suite 100</p>
      <p>Tel: 03-1234 5678</p>
    </div>

    <hr>

    <table style="width: 100%;">
      <tr>
        <td>Americano</td>
        <td style="text-align: right;">12.00</td>
      </tr>
      <tr>
        <td>Croissant</td>
        <td style="text-align: right;">8.50</td>
      </tr>
    </table>

    <hr>

    <table style="width: 100%;">
      <tr>
        <td><strong>TOTAL</strong></td>
        <td style="text-align: right;"><strong>RM 20.50</strong></td>
      </tr>
    </table>

    <hr>

    <div style="text-align: center;">
      <p>Thank you for your purchase!</p>
      <p style="font-size: 10px; color: #999;">Ref #TXN-20260204-001</p>
    </div>
  </div>

The extension auto-converts this to:

  ##CENTER####BOLD####BIG##ACME STORE
  ##CENTER##123 Main Street, Suite 100
  ##CENTER##Tel: 03-1234 5678
  --------------------------------
  Americano                   12.00
  Croissant                    8.50

##BOLD##TOTAL RM 20.50

##CENTER##Thank you for your purchase!
  ##CENTER##Ref #TXN-20260204-001
  ##FEED3##
  ##CUT##

You do not need to write any of these tags yourself when using HTML mode.
The extension reads your CSS styles and HTML tags to determine formatting.

7. METHOD B: JAVASCRIPT postMessage API

For full control over what gets printed, you can send pre-formatted text
directly to the extension using the browser's postMessage API.

This method is ideal when:
  - You want exact control over formatting
  - Your print content is generated dynamically
  - You are building a single-page application (SPA)
  - You want to use formatting tags directly
  - You need barcodes, QR codes, images, or advanced features
  - You do not want any visible print content on the page

Basic usage:

  window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: 'Hello World\n##CUT##'
  }, '*');

The message object must have:
  - type:    exactly 'VYROX_PRINTER_CONNECTOR_PRINT' (string, case-sensitive)
  - content: the text to print (string, with \n for newlines)

Full example with formatting:

  function printBill(orderData) {
    var lines = [];

    lines.push('##CENTER####BOLD####BIG##ACME STORE');
    lines.push('##CENTER##123 Main Street');
    lines.push('##CENTER##Tel: 03-1234 5678');
    lines.push('##SEP##');
    lines.push('');
    lines.push('##CENTER####BOLD##SALES BILL');
    lines.push('##SEP##');
    lines.push('');

    lines.push('Date: ' + new Date().toLocaleString());
    lines.push('Bill No: ' + orderData.billNumber);
    lines.push('');

    lines.push('##DSEP##');
    lines.push('##TABLE:<Item|^Qty|>Amount##');
    lines.push('##SEP##');

    orderData.items.forEach(function(item) {
      lines.push('##TABLE:<' + item.name
        + '|^' + item.qty
        + '|>' + item.amount.toFixed(2) + '##');
    });

    lines.push('##DSEP##');
    lines.push('');

    lines.push('##TABLE:<Subtotal|>' + orderData.subtotal.toFixed(2) + '##');
    lines.push('##TABLE:<Tax (6%)|>' + orderData.tax.toFixed(2) + '##');
    lines.push('##SEP##');
    lines.push('##BOLD####BIG####TABLE:<TOTAL|>RM '
      + orderData.total.toFixed(2) + '##');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Payment|>' + orderData.paymentMethod + '##');
    lines.push('##TABLE:<Tendered|>' + orderData.tendered.toFixed(2) + '##');
    lines.push('##TABLE:<Change|>' + orderData.change.toFixed(2) + '##');
    lines.push('');
    lines.push('##SEP##');

    lines.push('##CENTER##Scan for e-bill:');
    lines.push('##QR:https://acme.com/bill/'
      + orderData.billNumber + '##');
    lines.push('');

    lines.push('##CENTER##Thank you!');
    lines.push('##CENTER####SMALL##Goods sold are not refundable');
    lines.push('');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printBill({
    billNumber: 'TXN-20260204-001',
    items: [
      { name: 'Americano', qty: 2, amount: 24.00 },
      { name: 'Croissant', qty: 1, amount: 8.50 },
      { name: 'Muffin', qty: 3, amount: 19.50 }
    ],
    subtotal: 52.00,
    tax: 3.12,
    total: 55.12,
    paymentMethod: 'CASH',
    tendered: 60.00,
    change: 4.88
  });

8. METHOD C: POPUP TRIGGER (MANUAL PRINT)

Users can also print by clicking the extension icon in Chrome's toolbar
and pressing the "Print Current Page" button in the popup.

This works the same as clicking the floating button: it looks for a
print content element on the current page, extracts the content, and
sends it to the printer.

No developer action is needed for this to work, as long as a valid
print content element exists on the page.

9. FORMATTING TAGS REFERENCE

Formatting tags are special markers placed inside the text content.
They are processed by the native host and converted to ESC/POS commands
before being sent to the printer.

Tags are:
  - Case-sensitive (always UPPERCASE)
  - Wrapped in double hashes: ##TAGNAME##
  - Placed at the start of a line (for line-level tags)
  - Inline with text (for inline formatting tags)
  - One or more tags can be combined on a single line

Tags fall into two categories:

  STANDALONE TAGS: occupy the entire line, no text follows
    Example: ##CUT##
    Example: ##SEP##

  INLINE TAGS: placed before text on the same line
    Example: ##CENTER####BOLD##Hello World

Multiple inline tags can be combined:
    ##CENTER####BOLD####BIG##GRAND TOTAL

9.1 TEXT FORMATTING

##BOLD##
    Prints the text on this line in bold (emphasized).
    Example:
      ##BOLD##Total: RM 25.00
    Result: Total: RM 25.00 (printed darker/bolder)

  ##UNDERLINE##
    Prints the text on this line with an underline.
    Example:
      ##UNDERLINE##Section Title
    Result: Section Title (with underline beneath)

  ##INVERSE##
    Prints white text on a black background (reverse video).
    Example:
      ##INVERSE## PAID IN FULL
    Result: white text on black band

  ##UPSIDEDOWN##
    Prints the text upside-down (180 degree rotation).
    Example:
      ##UPSIDEDOWN##Upside down text

  ##SMALL##
    Uses Font B (smaller characters, more characters per line).
    On 80mm paper: approximately 64 characters per line instead of 48.
    Example:
      ##SMALL##This is fine print for terms and conditions

9.2 ALIGNMENT

##CENTER##
    Center-aligns the text on this line.
    Example:
      ##CENTER##ACME STORE

  ##RIGHT##
    Right-aligns the text on this line.
    Example:
      ##RIGHT##Page 1 of 1

  (no tag)
    Default alignment is left.
    Example:
      This text is left-aligned

9.3 CHARACTER SIZE

##BIG##
    Double width AND double height. Good for store names and totals.
    Characters per line is halved (e.g., 24 on 80mm paper).
    Example:
      ##CENTER####BIG##ACME STORE

  ##WIDE##
    Double width only. Text is wider but same height.
    Characters per line is halved.
    Example:
      ##WIDE##Wide text here

  ##TALL##
    Double height only. Text is taller but same width.
    Characters per line stays the same.
    Example:
      ##TALL##Tall text here

  ##SIZE:WxH##
    Granular size control. W = width multiplier (1-8), H = height
    multiplier (1-8).
    Example:
      ##SIZE:3x2##Large text
      (3x width, 2x height)

    Example:
      ##SIZE:1x4##Very tall thin text
      (normal width, 4x height)

    Note: characters per line = normal_chars / W
    So ##SIZE:4x1## on 80mm paper = 48 / 4 = 12 characters per line.

9.4 SPACING

##LINESPACE:N##
    Set the line spacing to N dots. Default is approximately 30 dots.
    Lower values pack lines closer together.
    Example:
      ##LINESPACE:20##
      Tight line
      Another tight line
      ##LINESPACE:DEFAULT##

  ##LINESPACE:DEFAULT##
    Reset line spacing to the printer's default.

  ##CHARSPACE:N##
    Set the right-side character spacing to N dots. Default is 0.
    Higher values add space between characters.
    Example:
      ##CHARSPACE:5##
      S P A C E D   O U T
      ##CHARSPACE:0##

9.5 STRUCTURE AND CUTTING

##CUT##
    Performs a full paper cut with a small feed.
    Typically placed at the very end of a print job.
    Example:
      Thank you!
      ##FEED:4##
      ##CUT##

  ##PARTCUT##
    Performs a partial cut (leaves a small uncut point so the paper
    stays attached). Useful for kitchen orders in a queue.
    Example:
      -- Order #45 --
      ##FEED:3##
      ##PARTCUT##

  ##FEED##
    Feeds 3 blank lines (default).
    Example:
      Last line of content
      ##FEED##
      ##CUT##

  ##FEED:N##
    Feeds exactly N blank lines.
    Example:
      ##FEED:5##
      (feeds 5 lines)

  ##FEEDMM:N##
    Feeds N motion units (approximately 0.125mm per unit).
    For precise spacing control.
    Example:
      ##FEEDMM:40##
      (feeds about 5mm)

9.6 SEPARATORS

##SEP##
    Prints a full-width line of dashes (-------).
    Width matches the paper (32 chars for 58mm, 48 for 80mm, 64 for 112mm).
    Example:
      Item total
      ##SEP##
      Grand total

  ##DSEP##
    Prints a full-width line of equals signs (=======).
    Example:
      ##DSEP##
      BILL
      ##DSEP##

  ##STARSEP##
    Prints a full-width line of asterisks (*******).
    Example:
      ##STARSEP##
      SPECIAL NOTICE
      ##STARSEP##

  Manual separator:
    You can also type your own separator in the text content:
      --------------------------------
    But ##SEP## is preferred because it auto-adjusts to the paper width.

9.7 BARCODES

##BARCODE:TYPE:DATA##
    Prints a 1D barcode.

    Supported barcode types:
      UPC-A     12 digits (11 + check digit)
      UPC-E     8 digits
      EAN13     13 digits (12 + check digit)
      EAN8      8 digits
      CODE39    Alphanumeric + special chars
      ITF       Even number of digits
      CODABAR   Digits + A-D start/stop
      CODE93    Full ASCII
      CODE128   Full ASCII (most versatile, recommended)

    Examples:
      ##BARCODE:EAN13:4901234567890##
      ##BARCODE:CODE128:TXN-2026-0001##
      ##BARCODE:CODE39:HELLO123##

  ##BARCODE_HEIGHT:N##
    Set barcode height in dots. Default is 80. Range: 1-255.
    Place this BEFORE the ##BARCODE## tag.
    Example:
      ##BARCODE_HEIGHT:100##
      ##BARCODE:CODE128:ABC123##

  ##BARCODE_WIDTH:N##
    Set barcode module width (bar thickness). Default is 3. Range: 2-6.
    Larger values make the barcode wider and easier to scan.
    Place this BEFORE the ##BARCODE## tag.
    Example:
      ##BARCODE_WIDTH:4##
      ##BARCODE:CODE128:ABC123##

  ##BARCODE_HRI:POSITION##
    Set the Human Readable Interpretation (the text below/above the
    barcode). Options: NONE, ABOVE, BELOW, BOTH. Default is NONE.
    Place this BEFORE the ##BARCODE## tag.
    Example:
      ##BARCODE_HRI:BELOW##
      ##BARCODE:CODE128:TXN-2026-0001##
    Result: barcode with "TXN-2026-0001" printed below it

  Complete barcode example:
      ##BARCODE_HEIGHT:80##
      ##BARCODE_WIDTH:3##
      ##BARCODE_HRI:BELOW##
      ##BARCODE:CODE128:TXN-20260204-001##

  Note: Barcode settings (height, width, HRI) persist across multiple
  barcodes in the same print job. Set them once and they apply to all
  subsequent barcodes.

9.8 QR CODES

##QR:DATA##
    Prints a QR code containing DATA. DATA can be any text, URL, or
    other string content. Supports UTF-8.

    Examples:
      ##QR:https://acme.com/bill/TXN-001##
      ##QR:WIFI:T:WPA;S:MyNetwork;P:password123;;##
      ##QR:Hello World##

  ##QR_SIZE:N##
    Set the QR code module size (dot size of each black/white square).
    Range: 1-16. Default is 6.
    Smaller values = smaller QR code.
    Larger values = larger, easier-to-scan QR code.
    Place this BEFORE the ##QR## tag.
    Example:
      ##QR_SIZE:8##
      ##QR:https://example.com##

  ##QR_EC:LEVEL##
    Set the QR code error correction level.
    Options:
      L = Low (7% recovery)
      M = Medium (15% recovery, default)
      Q = Quartile (25% recovery)
      H = High (30% recovery)
    Higher levels make the QR code larger but more resilient to damage.
    Place this BEFORE the ##QR## tag.
    Example:
      ##QR_EC:H##
      ##QR:https://example.com##

  Complete QR example:
      ##CENTER##Scan for e-bill:
      ##QR_SIZE:6##
      ##QR_EC:M##
      ##QR:https://acme.com/bill/TXN-20260204-001##

  Note: QR settings persist across multiple QR codes in the same
  print job.

9.9 IMAGES

##IMAGE:PATH##
    Prints a raster image from a local file path on the user's computer.
    Supported formats: BMP, PNG, JPG.

    The image is automatically:
      - Scaled down to fit the paper width (if wider)
      - Converted to 1-bit black and white (threshold at 50% brightness)
      - Width rounded to a multiple of 8 pixels

    Example:
      ##IMAGE:C:\VYROX_PrinterConnector\logo.bmp##

    Notes:
      - The path must be accessible on the local Windows machine
      - This is useful for store logos
      - For best results, use a black-and-white image
      - Recommended width: 384px for 58mm, 576px for 80mm, 832px for 112mm
      - Large images may print slowly

    Tip: Pre-process your logo as a 1-bit BMP at the correct width for
    fastest printing and best quality.

9.10 TABLES AND COLUMNS

##TABLE:col1|col2|col3##
    Prints a single row of aligned columns.
    Columns are separated by the pipe character (|).
    Column widths are distributed evenly across the paper width.

    Column alignment prefixes:
      <   Left-align (default)
      >   Right-align
      ^   Center-align

    Examples:

      Two columns (left + right):
        ##TABLE:<Americano|>12.00##
        Result: Americano                   12.00

      Three columns (left + center + right):
        ##TABLE:<2x Americano|^RM|>24.00##
        Result: 2x Americano       RM        24.00

      Header row:
        ##TABLE:^Item|^Qty|^Price##
        Result:     Item          Qty        Price

    Full table example:
        ##BOLD####TABLE:^ITEM|^QTY|^AMOUNT##
        ##SEP##
        ##TABLE:<Americano|^2|>24.00##
        ##TABLE:<Croissant|^1|>8.50##
        ##TABLE:<Muffin|^3|>19.50##
        ##SEP##
        ##BOLD####TABLE:<TOTAL||>RM 52.00##

    Note: If text in a column is longer than the column width, it will be
    truncated. Plan your content to fit within the available width.

    Characters per column = total_chars / number_of_columns
    For 80mm paper with 3 columns: 48 / 3 = 16 characters each.
    For 80mm paper with 2 columns: 48 / 2 = 24 characters each.

9.11 CODE PAGES AND CHARACTER SETS

##CODEPAGE:N## or ##CODEPAGE:NAME##
    Sets the printer's character code table for printing special characters.
    This determines which characters are printed for byte values 128-255.

    By number:
      ##CODEPAGE:0##     CP437 (US English, default on most printers)
      ##CODEPAGE:2##     CP850 (Western European)
      ##CODEPAGE:16##    CP1252 (Windows Latin-1)
      ##CODEPAGE:19##    CP858 (Western European with Euro sign)
      ##CODEPAGE:255##   UTF-8 (if your printer supports it)

    By name:
      ##CODEPAGE:CP437##
      ##CODEPAGE:CP850##
      ##CODEPAGE:CP858##
      ##CODEPAGE:CP1252##
      ##CODEPAGE:LATIN1##    (same as CP1252)
      ##CODEPAGE:LATIN2##    (same as CP852)
      ##CODEPAGE:KATAKANA##  (Japanese half-width Katakana)
      ##CODEPAGE:THAI##      (Thai, CP874)
      ##CODEPAGE:CP866##     (Cyrillic)
      ##CODEPAGE:UTF8##      (UTF-8, code page 255)

    Additional supported code pages:
      CP720, CP737, CP775, CP852, CP855, CP857, CP860, CP862, CP863,
      CP864, CP865, CP869, CP874, CP1251, CP1253, CP1254, CP1255,
      CP1256, CP1257, CP1258

    Example for Malaysian Ringgit sign and accented characters:
      ##CODEPAGE:CP1252##
      Total: RM 25.00

    Example for Chinese/Unicode (requires printer UTF-8 support):
      ##CODEPAGE:UTF8##
      Chinese text here

    Note: Code page support varies by printer model. Most modern thermal
    printers support at least CP437, CP850, CP1252, and a few others.
    Check your printer's manual for the full list.

9.12 HARDWARE CONTROL

##DRAWER##
    Kicks open the cash drawer connected to the printer (pin 2, default).
    Example:
      ##DRAWER##
      ##CENTER####BIG##ACME STORE
      (rest of content...)

  ##DRAWER:1##
    Kicks cash drawer on pin 2 (same as ##DRAWER##).

  ##DRAWER:2##
    Kicks cash drawer on pin 5 (for printers with two drawer ports).

  ##BEEP:N##
    Sounds the printer's built-in buzzer N times (1-9).
    Not all printers have a buzzer.
    Example:
      ##BEEP:3##
      (beeps 3 times)

  ##BEEP:N:D##
    Sounds the buzzer N times with duration D (1-9).
    Higher D values = longer beep duration.
    Example:
      ##BEEP:2:5##
      (beeps 2 times, longer duration)

9.13 MARGINS AND PRINT AREA

##MARGIN:N##
    Set the left margin to N dots.
    Example:
      ##MARGIN:20##
      This text has a left margin

  ##PRINTAREA:N##
    Set the print area width to N dots.
    Example:
      ##PRINTAREA:400##
      This text is confined to 400 dots width

    Note: These are advanced features. Most print jobs do not need them.
    Refer to the Paper Size Reference (section 11) for dots per line.

9.14 RAW BYTES

##RAW:HEXSTRING##
    Sends raw hex bytes directly to the printer with no processing.
    The hex string must have an even number of characters (0-9, A-F).

    This is for advanced users who need to send printer-specific commands
    that are not covered by the formatting tags.

    Example:
      ##RAW:1B40##
      (sends ESC @ which is the printer initialize command)

    Example:
      ##RAW:1B6101##
      (sends ESC a 1 which is center alignment)

    Common ESC/POS hex commands:
      1B 40       ESC @     Initialize printer
      1B 61 00    ESC a 0   Left align
      1B 61 01    ESC a 1   Center align
      1B 61 02    ESC a 2   Right align
      1B 45 01    ESC E 1   Bold on
      1B 45 00    ESC E 0   Bold off
      1D 56 42 03 GS V B 3  Cut paper with feed

9.15 RESET

##RESET##
    Sends the ESC @ command to reinitialize the printer to its default
    state. This clears all formatting, alignment, and size settings.

    Useful in the middle of a print job if formatting gets confused:
      ##BOLD####BIG##HEADER
      ##RESET##
      Normal text after reset

10. HTML-TO-TEXT AUTO-CONVERSION RULES

When using Method A (HTML element), the extension automatically converts
your HTML into tagged text. Here is how it maps HTML/CSS to formatting:

  ALIGNMENT DETECTION:
    text-align: center     ->  ##CENTER##
    class="center"         ->  ##CENTER##
    class="text-center"    ->  ##CENTER##

  BOLD DETECTION:
    <strong> or <b> tag    ->  ##BOLD##
    font-weight >= 700     ->  ##BOLD##
    class="bold"           ->  ##BOLD##
    class="font-bold"      ->  ##BOLD##
    class="fw-bold"        ->  ##BOLD##

  LARGE TEXT DETECTION:
    font-size >= 20px      ->  ##BIG##
    <h1> or <h2> tag       ->  ##BIG##
    class="big"            ->  ##BIG##
    class="large"          ->  ##BIG##
    class="title"          ->  ##BIG##
    class="total"          ->  ##BIG##
    class="text-xl"        ->  ##BIG##
    class="text-2xl"       ->  ##BIG##

  SEPARATOR DETECTION:
    <hr> tag               ->  --------------------------------
    class="separator"      ->  --------------------------------
    class="divider"        ->  --------------------------------
    class="line"           ->  --------------------------------
    (only if element has no text or only dashes/equals/dots)

  CUT DETECTION:
    class="cut"            ->  ##CUT##
    data-cut="true"        ->  ##CUT##

  FEED DETECTION:
    class="feed"           ->  ##FEED3##
    data-feed="5"          ->  ##FEED5##

  TABLE DETECTION:
    <table> with <tr>/<td> elements are auto-formatted into aligned
    columns.
    2-column tables: left-aligned first column, right-aligned second
    column (ideal for item + price layout).
    3-column tables: quantity (padded to 4 chars), item name, price
    (right-aligned).
    Other column counts: joined with double-space.

  AUTO FEED AND CUT:
    If your print content does not end with ##CUT##, the extension
    automatically appends:
      ##FEED3##
      ##CUT##
    This ensures the paper feeds enough for tearing and the cutter
    activates.

  PRE-FORMATTED TEXT:
    If your element has a data-vyrox-printer-connector-raw attribute,
    its value is used as-is (no HTML parsing):

      <div id="vyrox_printer_connector"
           data-vyrox-printer-connector-raw="##CENTER##My Store\nItem 1  5.00\n##CUT##">
      </div>

    If your element contains a child element with
    class="vyrox-printer-connector-raw" or a
    <pre class="print-content-text"> element, its text content is used
    as-is:

      <div id="vyrox_printer_connector">
        <pre class="print-content-text" style="display:none;">
##CENTER####BOLD####BIG##MY STORE
##CENTER##123 Main Street
##SEP##
Americano                   12.00
##SEP##
##BOLD##TOTAL            RM 12.00
##FEED:4##
##CUT##
        </pre>
        <!-- visible HTML version for screen display -->
        <h2>MY STORE</h2>
        ...
      </div>

    This is the recommended approach when you need precise control over
    the print output while also showing a formatted version on screen.

11. PAPER SIZE REFERENCE

58mm paper (2-inch, small/portable printers):
    Characters per line (Font A):  32
    Characters per line (Font B):  42
    Dots per line:                 384
    Typical use: Mobile billing, parking tickets, small kiosks

  80mm paper (3-inch, standard POS printers):
    Characters per line (Font A):  48
    Characters per line (Font B):  64
    Dots per line:                 576
    Typical use: Retail POS, restaurant orders, most billing systems

  112mm paper (4-inch, wide/kitchen printers):
    Characters per line (Font A):  64
    Characters per line (Font B):  85
    Dots per line:                 832
    Typical use: Kitchen orders, detailed bills, wide-format printouts

  The paper size is auto-detected by the native host during setup.
  All ##SEP##, ##DSEP##, ##STARSEP##, and ##TABLE## tags auto-adjust
  to the detected paper width. You do NOT need to hard-code character
  counts.

  Character count under different size modes (80mm paper example):
    Normal text:          48 characters
    ##SMALL## (Font B):   64 characters
    ##BIG## or ##WIDE##:  24 characters
    ##SIZE:3x1##:         16 characters
    ##SIZE:4x1##:         12 characters

12. FULL PRINT EXAMPLES

12.1 SIMPLE RETAIL POS

This is the most common billing format. Use this as your starting template.

Using postMessage:

  var bill = [
    '##CENTER####BOLD####BIG##ACME CAFE',
    '##CENTER##Lot 12, Setia City Mall',
    '##CENTER##Tel: 03-1234 5678',
    '##DSEP##',
    '##CENTER####BOLD##SALES BILL',
    '##DSEP##',
    '',
    'Date: 2026-02-04 14:30',
    'Cashier: Ahmad',
    'Bill No: #B-20260204-0042',
    '',
    '##SEP##',
    '##TABLE:<Americano x2|>24.00##',
    '##TABLE:<Latte x1|>14.00##',
    '##TABLE:<Croissant x1|>8.50##',
    '##TABLE:<Cheesecake x1|>16.00##',
    '##SEP##',
    '##TABLE:<Subtotal|>62.50##',
    '##TABLE:<Service Tax 6%|>3.75##',
    '##SEP##',
    '##BOLD####BIG####TABLE:<TOTAL|>RM 66.25##',
    '##SEP##',
    '',
    '##TABLE:<Cash|>70.00##',
    '##TABLE:<Change|>3.75##',
    '',
    '##SEP##',
    '##CENTER##Thank you for dining with us!',
    '##CENTER####SMALL##GST Reg: 001234567890',
    '##CENTER####SMALL##SST-02-1234-56789012',
    '',
    '##FEED:4##',
    '##CUT##'
  ].join('\n');

  window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: bill
  }, '*');


Using HTML auto-detection:

  <div id="vyrox_printer_connector">
    <div style="text-align:center;">
      <h2>ACME CAFE</h2>
      <p>Lot 12, Setia City Mall</p>
      <p>Tel: 03-1234 5678</p>
    </div>
    <hr>
    <p style="text-align:center;"><strong>SALES BILL</strong></p>
    <hr>
    <p>Date: 2026-02-04 14:30</p>
    <p>Cashier: Ahmad</p>
    <p>Bill No: #B-20260204-0042</p>
    <hr>
    <table style="width:100%">
      <tr><td>Americano x2</td><td style="text-align:right">24.00</td></tr>
      <tr><td>Latte x1</td><td style="text-align:right">14.00</td></tr>
      <tr><td>Croissant x1</td><td style="text-align:right">8.50</td></tr>
      <tr><td>Cheesecake x1</td><td style="text-align:right">16.00</td></tr>
    </table>
    <hr>
    <table style="width:100%">
      <tr><td>Subtotal</td><td style="text-align:right">62.50</td></tr>
      <tr><td>Service Tax 6%</td><td style="text-align:right">3.75</td></tr>
    </table>
    <hr>
    <table style="width:100%">
      <tr>
        <td><strong style="font-size:20px">TOTAL</strong></td>
        <td style="text-align:right;font-size:20px">
          <strong>RM 66.25</strong>
        </td>
      </tr>
    </table>
    <hr>
    <table style="width:100%">
      <tr><td>Cash</td><td style="text-align:right">70.00</td></tr>
      <tr><td>Change</td><td style="text-align:right">3.75</td></tr>
    </table>
    <hr>
    <p style="text-align:center">Thank you for dining with us!</p>
  </div>

12.2 RESTAURANT KITCHEN ORDER

var kitchenOrder = [
    '##BEEP:3##',
    '##CENTER####BOLD####BIG##KITCHEN ORDER',
    '##DSEP##',
    '',
    '##BOLD##Table: 12',
    '##BOLD##Time:  14:32',
    '##BOLD##Server: Siti',
    '',
    '##DSEP##',
    '##BOLD####BIG##2x  Nasi Lemak Special',
    '    - Extra sambal',
    '    - No cucumber',
    '',
    '##BOLD####BIG##1x  Mee Goreng Mamak',
    '    - Extra spicy',
    '',
    '##BOLD####BIG##3x  Teh Tarik',
    '',
    '##BOLD####BIG##1x  Iced Milo Dinosaur',
    '',
    '##DSEP##',
    '##CENTER####BOLD##PRIORITY: NORMAL',
    '##DSEP##',
    '',
    '##FEED:3##',
    '##PARTCUT##'
  ].join('\n');

  window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: kitchenOrder
  }, '*');

12.3 RETAIL WITH BARCODE AND QR

var retailBill = [
    '##CENTER####BOLD####BIG##GADGET WORLD',
    '##CENTER##Plaza 123, Level 2',
    '##CENTER##Tel: 011-2233 4455',
    '##DSEP##',
    '',
    'Date:     2026-02-04 15:45',
    'Bill No:  GW-20260204-0108',
    'Staff:    Raj',
    '',
    '##SEP##',
    '##TABLE:<USB-C Cable 2m|^1|>29.90##',
    '##TABLE:<Phone Case X15|^1|>49.90##',
    '##TABLE:<Screen Protector|^2|>19.80##',
    '##SEP##',
    '##TABLE:<Subtotal|>99.60##',
    '##TABLE:<Member Disc 10%|>-9.96##',
    '##SEP##',
    '##BOLD####BIG####CENTER##RM 89.64',
    '##SEP##',
    '',
    '##TABLE:<E-Wallet|>89.64##',
    '##TABLE:<Ref: TNG-8834521|>##',
    '',
    '##SEP##',
    '',
    '##CENTER##Scan to check warranty:',
    '##QR_SIZE:8##',
    '##QR:https://gadgetworld.com/warranty/GW-20260204-0108##',
    '',
    '##CENTER##Bill barcode:',
    '##BARCODE_HRI:BELOW##',
    '##BARCODE:CODE128:GW202602040108##',
    '',
    '##SEP##',
    '##CENTER####SMALL##Exchange within 7 days with bill',
    '##CENTER####SMALL##No refund on accessories',
    '',
    '##FEED:4##',
    '##CUT##'
  ].join('\n');

  window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: retailBill
  }, '*');

12.4 MULTI-LANGUAGE PRINT

var multiLangBill = [
    '##CODEPAGE:UTF8##',
    '##CENTER####BOLD####BIG##RESTORAN ABC',
    '##CENTER##No. 5, Jalan Maju',
    '##DSEP##',
    '',
    '##TABLE:<Nasi Kandar|>15.00##',
    '##TABLE:<Roti Canai|>2.50##',
    '##TABLE:<Teh Tarik|>3.00##',
    '##SEP##',
    '##BOLD####TABLE:<Jumlah / Total|>RM 20.50##',
    '##SEP##',
    '',
    '##CENTER##Terima kasih!',
    '##CENTER##Thank you!',
    '',
    '##FEED:4##',
    '##CUT##'
  ].join('\n');

  window.postMessage({
    type: 'VYROX_PRINTER_CONNECTOR_PRINT',
    content: multiLangBill
  }, '*');

12.5 MINIMAL JAVASCRIPT-ONLY PRINT

If you do not have any print content HTML on the page and just want to
print from a JavaScript event (e.g., after an AJAX order submission):

  fetch('/api/orders', { method: 'POST', body: orderData })
    .then(function(res) { return res.json(); })
    .then(function(order) {

      var lines = [];
      lines.push('##CENTER####BOLD##Order #' + order.id);
      lines.push('##SEP##');

      order.items.forEach(function(item) {
        lines.push('##TABLE:<' + item.name
          + '|^' + item.qty
          + '|>' + item.price.toFixed(2) + '##');
      });

      lines.push('##SEP##');
      lines.push('##BOLD####TABLE:<TOTAL|>RM '
        + order.total.toFixed(2) + '##');
      lines.push('##FEED:4##');
      lines.push('##CUT##');

      window.postMessage({
        type: 'VYROX_PRINTER_CONNECTOR_PRINT',
        content: lines.join('\n')
      }, '*');

    });

No HTML print content element is needed. No floating button will appear.
The print is entirely triggered by JavaScript.

12.6 SPORTS FACILITY BOOKING CONFIRMATION

This example shows a booking confirmation for a badminton court, futsal
pitch, or any sports facility. Includes time slot, booking reference,
and QR code for check-in.

  function printBookingConfirmation(booking) {
    var lines = [];

    lines.push('##CENTER####BOLD####BIG##SPORTSPARK');
    lines.push('##CENTER##Shah Alam Sports Complex');
    lines.push('##CENTER##Jalan Kelab, 40000 Shah Alam');
    lines.push('##CENTER##Tel: 03-5511 2233');
    lines.push('##DSEP##');
    lines.push('##CENTER####BOLD##BOOKING CONFIRMATION');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('Booking Ref:  ' + booking.ref);
    lines.push('Booked By:    ' + booking.customerName);
    lines.push('Contact:      ' + booking.phone);
    lines.push('Printed:      ' + new Date().toLocaleString());
    lines.push('');

    lines.push('##SEP##');
    lines.push('##BOLD##Facility Details');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Facility|>' + booking.facilityName + '##');
    lines.push('##TABLE:<Court/Pitch|>' + booking.courtName + '##');
    lines.push('##TABLE:<Date|>' + booking.date + '##');
    lines.push('##TABLE:<Time|>'
      + booking.startTime + ' - ' + booking.endTime + '##');
    lines.push('##TABLE:<Duration|>'
      + booking.durationHours + ' hour(s)##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##BOLD##Payment Summary');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Rate per hour|>RM '
      + booking.ratePerHour.toFixed(2) + '##');
    lines.push('##TABLE:<Hours|>'
      + booking.durationHours + '##');

    if (booking.discount > 0) {
      lines.push('##TABLE:<Member Discount|>-RM '
        + booking.discount.toFixed(2) + '##');
    }

    lines.push('##SEP##');
    lines.push('##BOLD####BIG####TABLE:<TOTAL|>RM '
      + booking.total.toFixed(2) + '##');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Payment|>' + booking.paymentMethod + '##');

    if (booking.paymentRef) {
      lines.push('##TABLE:<Ref|>' + booking.paymentRef + '##');
    }

    lines.push('');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('##CENTER##Show this QR at the counter:');
    lines.push('##QR_SIZE:8##');
    lines.push('##QR_EC:H##');
    lines.push('##QR:https://sportspark.com/checkin/'
      + booking.ref + '##');
    lines.push('');

    lines.push('##BARCODE_HRI:BELOW##');
    lines.push('##BARCODE:CODE128:' + booking.ref + '##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##CENTER####SMALL##Please arrive 15 min before');
    lines.push('##CENTER####SMALL##your booking time.');
    lines.push('##CENTER####SMALL##No-show bookings are non-refundable.');
    lines.push('##CENTER####SMALL##Cancellation: 24 hours notice required.');
    lines.push('');
    lines.push('##CENTER##Thank you for choosing SportsPark!');
    lines.push('');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printBookingConfirmation({
    ref: 'BK-20260204-0089',
    customerName: 'Tan Wei Ming',
    phone: '012-345 6789',
    facilityName: 'Badminton',
    courtName: 'Court A3',
    date: '2026-02-05',
    startTime: '19:00',
    endTime: '21:00',
    durationHours: 2,
    ratePerHour: 25.00,
    discount: 5.00,
    total: 45.00,
    paymentMethod: 'Online (FPX)',
    paymentRef: 'FPX-88712340'
  });

12.7 SNOOKER AND BILLIARD TABLE BILLING

This example shows a complete snooker or billiard hall session bill with
time-based charging, food and beverage items, and table details.

  function printSnookerBill(session) {
    var lines = [];

    lines.push('##DRAWER##');
    lines.push('##CENTER####BOLD####BIG##Q-BALL SNOOKER');
    lines.push('##CENTER##Level 3, Plaza Pelangi');
    lines.push('##CENTER##80400 Johor Bahru');
    lines.push('##CENTER##Tel: 07-3344 5566');
    lines.push('##DSEP##');
    lines.push('##CENTER####BOLD##TABLE BILL');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('Bill No:    ' + session.billNo);
    lines.push('Date:       ' + session.date);
    lines.push('Cashier:    ' + session.cashier);
    lines.push('');

    lines.push('##SEP##');
    lines.push('##BOLD##Table Session');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Table|>'
      + session.tableName + '##');
    lines.push('##TABLE:<Type|>'
      + session.tableType + '##');
    lines.push('##TABLE:<Start|>'
      + session.startTime + '##');
    lines.push('##TABLE:<End|>'
      + session.endTime + '##');
    lines.push('##TABLE:<Duration|>'
      + session.durationDisplay + '##');
    lines.push('##TABLE:<Rate|>RM '
      + session.ratePerHour.toFixed(2) + '/hr##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##BOLD####TABLE:<Table Charge|>RM '
      + session.tableCharge.toFixed(2) + '##');
    lines.push('##SEP##');
    lines.push('');

    if (session.fnbItems && session.fnbItems.length > 0) {
      lines.push('##BOLD##Food and Beverages');
      lines.push('##SEP##');
      lines.push('');

      session.fnbItems.forEach(function(item) {
        lines.push('##TABLE:<' + item.name
          + '|^' + item.qty
          + '|>RM ' + item.amount.toFixed(2) + '##');
      });

      lines.push('##SEP##');
      lines.push('##BOLD####TABLE:<F&B Subtotal|>RM '
        + session.fnbTotal.toFixed(2) + '##');
      lines.push('##SEP##');
      lines.push('');
    }

    lines.push('##DSEP##');
    lines.push('##TABLE:<Table Charge|>RM '
      + session.tableCharge.toFixed(2) + '##');

    if (session.fnbItems && session.fnbItems.length > 0) {
      lines.push('##TABLE:<F&B|>RM '
        + session.fnbTotal.toFixed(2) + '##');
    }

    if (session.serviceTax > 0) {
      lines.push('##TABLE:<Service Tax 6%|>RM '
        + session.serviceTax.toFixed(2) + '##');
    }

    if (session.memberDiscount > 0) {
      lines.push('##TABLE:<Member Disc|>-RM '
        + session.memberDiscount.toFixed(2) + '##');
    }

    lines.push('##DSEP##');
    lines.push('##BOLD####BIG####CENTER##RM '
      + session.grandTotal.toFixed(2));
    lines.push('##DSEP##');
    lines.push('');

    lines.push('##TABLE:<Payment|>' + session.paymentMethod + '##');
    lines.push('##TABLE:<Tendered|>RM '
      + session.tendered.toFixed(2) + '##');
    lines.push('##TABLE:<Change|>RM '
      + session.change.toFixed(2) + '##');
    lines.push('');

    if (session.memberName) {
      lines.push('##SEP##');
      lines.push('##TABLE:<Member|>' + session.memberName + '##');
      lines.push('##TABLE:<Member ID|>' + session.memberId + '##');
      lines.push('##TABLE:<Points Earned|>+'
        + session.pointsEarned + '##');
      lines.push('##TABLE:<Points Balance|>'
        + session.pointsBalance + '##');
      lines.push('');
    }

    lines.push('##SEP##');
    lines.push('');
    lines.push('##CENTER##Scan to join membership:');
    lines.push('##QR_SIZE:6##');
    lines.push('##QR:https://qball.com/member/join##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##CENTER####SMALL##Operating hours: 12pm - 2am daily');
    lines.push('##CENTER####SMALL##Happy hour rate: 12pm - 5pm');
    lines.push('##CENTER####SMALL##Follow us: @qballsnooker');
    lines.push('');
    lines.push('##CENTER##See you at the table!');
    lines.push('');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printSnookerBill({
    billNo: 'QB-20260204-0156',
    date: '2026-02-04 23:15',
    cashier: 'Kumar',
    tableName: 'Table 7',
    tableType: 'Snooker (Full Size)',
    startTime: '20:30',
    endTime: '23:10',
    durationDisplay: '2h 40m',
    ratePerHour: 18.00,
    tableCharge: 48.00,
    fnbItems: [
      { name: 'Canned Drink', qty: 3, amount: 10.50 },
      { name: 'Hot Coffee', qty: 2, amount: 7.00 },
      { name: 'Maggi Goreng', qty: 1, amount: 8.00 },
      { name: 'Roti Bakar', qty: 2, amount: 6.00 }
    ],
    fnbTotal: 31.50,
    serviceTax: 4.77,
    memberDiscount: 4.80,
    grandTotal: 79.47,
    paymentMethod: 'CASH',
    tendered: 80.00,
    change: 0.53,
    memberName: 'Ali bin Abu',
    memberId: 'QBM-0042',
    pointsEarned: 79,
    pointsBalance: 1245
  });

12.8 SPORTS BOOKING WITH MEMBERSHIP DISCOUNT

A futsal booking example showing group booking, membership tiers, and
deposit/balance payment flow.

  function printFutsalBooking(booking) {
    var lines = [];

    lines.push('##CENTER####BOLD####BIG##KICK ARENA');
    lines.push('##CENTER##Futsal and Sports Centre');
    lines.push('##CENTER##Setia Alam, Shah Alam');
    lines.push('##CENTER##Tel: 03-5123 4567');
    lines.push('##DSEP##');
    lines.push('##CENTER####BOLD##BOOKING BILL');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('Booking ID:   ' + booking.id);
    lines.push('Date Booked:  ' + booking.bookedDate);
    lines.push('Booked By:    ' + booking.customerName);
    lines.push('Phone:        ' + booking.phone);
    lines.push('');

    lines.push('##SEP##');
    lines.push('##BOLD##Session Details');
    lines.push('##SEP##');
    lines.push('');

lines.push('##TABLE:<Activity|>' + booking.activity + '##');
    lines.push('##TABLE:<Pitch|>' + booking.pitchName + '##');
    lines.push('##TABLE:<Session Date|>' + booking.sessionDate + '##');
    lines.push('##TABLE:<Time|>'
      + booking.startTime + ' - ' + booking.endTime + '##');
    lines.push('##TABLE:<Duration|>'
      + booking.durationHours + ' hour(s)##');
    lines.push('##TABLE:<Players|>'
      + booking.playerCount + ' pax##');
    lines.push('');

    if (booking.extras && booking.extras.length > 0) {
      lines.push('##BOLD##Add-Ons');
      lines.push('##SEP##');
      lines.push('');

      booking.extras.forEach(function(extra) {
        lines.push('##TABLE:<' + extra.name
          + '|^' + extra.qty
          + '|>RM ' + extra.amount.toFixed(2) + '##');
      });

      lines.push('##SEP##');
      lines.push('');
    }

    lines.push('##BOLD##Payment Breakdown');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Pitch Rate|>RM '
      + booking.ratePerHour.toFixed(2) + '/hr##');
    lines.push('##TABLE:<Hours|>'
      + booking.durationHours + '##');
    lines.push('##TABLE:<Pitch Charge|>RM '
      + booking.pitchCharge.toFixed(2) + '##');

    if (booking.extrasTotal > 0) {
      lines.push('##TABLE:<Add-Ons|>RM '
        + booking.extrasTotal.toFixed(2) + '##');
    }

    if (booking.memberTier) {
      lines.push('##TABLE:<Member Tier|>'
        + booking.memberTier + '##');
      lines.push('##TABLE:<Discount ('
        + booking.discountPercent + '%)|>-RM '
        + booking.discountAmount.toFixed(2) + '##');
    }

    lines.push('##SEP##');
    lines.push('##BOLD####BIG####TABLE:<TOTAL|>RM '
      + booking.total.toFixed(2) + '##');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##TABLE:<Deposit Paid|>RM '
      + booking.depositPaid.toFixed(2) + '##');
    lines.push('##BOLD####TABLE:<Balance Due|>RM '
      + booking.balanceDue.toFixed(2) + '##');
    lines.push('##TABLE:<Pay Before|>'
      + booking.payBeforeDate + '##');
    lines.push('');

    lines.push('##DSEP##');
    lines.push('');

    lines.push('##CENTER##Show this QR for check-in:');
    lines.push('##QR_SIZE:8##');
    lines.push('##QR_EC:H##');
    lines.push('##QR:https://kickarena.com/checkin/'
      + booking.id + '##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##CENTER####SMALL##Deposit is non-refundable.');
    lines.push('##CENTER####SMALL##Full payment required on session day.');
    lines.push('##CENTER####SMALL##Reschedule: min 12 hours notice.');
    lines.push('##CENTER####SMALL##Bibs and balls provided at counter.');
    lines.push('');
    lines.push('##CENTER##See you on the pitch!');
    lines.push('');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printFutsalBooking({
    id: 'KA-20260205-0034',
    bookedDate: '2026-02-04 10:15',
    customerName: 'Mohd Faizal',
    phone: '019-876 5432',
    activity: 'Futsal',
    pitchName: 'Pitch B (Indoor)',
    sessionDate: '2026-02-08',
    startTime: '20:00',
    endTime: '22:00',
    durationHours: 2,
    playerCount: 12,
    ratePerHour: 120.00,
    pitchCharge: 240.00,
    extras: [
      { name: 'Bibs (12 pcs)', qty: 1, amount: 10.00 },
      { name: 'Drinking Water', qty: 12, amount: 18.00 }
    ],
    extrasTotal: 28.00,
    memberTier: 'Gold',
    discountPercent: 15,
    discountAmount: 36.00,
    total: 232.00,
    depositPaid: 120.00,
    balanceDue: 112.00,
    payBeforeDate: '2026-02-08 19:30'
  });

12.9 SNOOKER TOURNAMENT SCORECARD

A tournament match scorecard printed at the end of each frame or match,
showing player names, frame scores, and match result.

  function printTournamentScorecard(match) {
    var lines = [];

    lines.push('##CENTER####BOLD####BIG##Q-BALL SNOOKER');
    lines.push('##DSEP##');
    lines.push('##CENTER####BOLD####BIG##' + match.tournamentName);
    lines.push('##CENTER##' + match.round);
    lines.push('##DSEP##');
    lines.push('');

    lines.push('Match ID:    ' + match.matchId);
    lines.push('Date:        ' + match.date);
    lines.push('Table:       ' + match.tableName);
    lines.push('Referee:     ' + match.referee);
    lines.push('');

    lines.push('##DSEP##');
    lines.push('##BOLD####TABLE:<PLAYER|^FRAMES|>H/BREAK##');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('##TABLE:<' + match.player1.name
      + '|^' + match.player1.framesWon
      + '|>' + (match.player1.highBreak || '-') + '##');

    lines.push('##CENTER##vs');

    lines.push('##TABLE:<' + match.player2.name
      + '|^' + match.player2.framesWon
      + '|>' + (match.player2.highBreak || '-') + '##');

    lines.push('');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('##BOLD##Frame-by-Frame');
    lines.push('##SEP##');
    lines.push('##TABLE:<Frame|^'
      + match.player1.name.substring(0, 10) + '|^'
      + match.player2.name.substring(0, 10) + '##');
    lines.push('##SEP##');

    match.frames.forEach(function(frame, index) {
      var frameNum = (index + 1).toString();
      var winMarker1 = frame.winner === 1 ? ' *' : '';
      var winMarker2 = frame.winner === 2 ? ' *' : '';
      lines.push('##TABLE:<' + frameNum
        + '|^' + frame.score1 + winMarker1
        + '|^' + frame.score2 + winMarker2 + '##');
    });

    lines.push('##DSEP##');
    lines.push('');

    lines.push('##CENTER####BOLD####BIG##WINNER');
    lines.push('##CENTER####BOLD####BIG##' + match.winnerName);
    lines.push('');

    lines.push('##CENTER##Final Score: '
      + match.player1.framesWon + ' - '
      + match.player2.framesWon);
    lines.push('');

    if (match.highestBreak) {
      lines.push('##CENTER####BOLD##Match Highest Break: '
        + match.highestBreak.score
        + ' (' + match.highestBreak.player + ')');
      lines.push('');
    }

    lines.push('##SEP##');
    lines.push('##TABLE:<Match Start|>'
      + match.startTime + '##');
    lines.push('##TABLE:<Match End|>'
      + match.endTime + '##');
    lines.push('##TABLE:<Total Time|>'
      + match.totalDuration + '##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##CENTER####SMALL##Official tournament record');
    lines.push('##CENTER####SMALL##' + match.tournamentName);
    lines.push('##CENTER####SMALL##Sanctioned by '
      + match.sanctionedBy);
    lines.push('');

    lines.push('##CENTER##Signed: ___________________');
    lines.push('##CENTER####SMALL##Referee');
    lines.push('');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printTournamentScorecard({
    tournamentName: 'Q-BALL OPEN 2026',
    round: 'Quarter Final',
    matchId: 'QBO-QF-003',
    date: '2026-02-04',
    tableName: 'Table 1 (Match Table)',
    referee: 'Encik Razak',
    player1: {
      name: 'James Lee',
      framesWon: 4,
      highBreak: 87
    },
    player2: {
      name: 'Ahmad Rizal',
      framesWon: 2,
      highBreak: 63
    },
    frames: [
      { score1: 72, score2: 45, winner: 1 },
      { score1: 38, score2: 69, winner: 2 },
      { score1: 87, score2: 22, winner: 1 },
      { score1: 51, score2: 63, winner: 2 },
      { score1: 66, score2: 41, winner: 1 },
      { score1: 59, score2: 30, winner: 1 }
    ],
    winnerName: 'James Lee',
    highestBreak: { score: 87, player: 'James Lee' },
    startTime: '14:00',
    endTime: '16:45',
    totalDuration: '2h 45m',
    sanctionedBy: 'Johor Snooker Association'
  });

12.10 SPORTS COMPLEX MULTI-ACTIVITY BOOKING

A sports complex bill that covers multiple activities booked together,
such as swimming plus gym plus court rental in a single transaction.

  function printMultiActivityBill(bill) {
    var lines = [];

    lines.push('##CENTER####BOLD####BIG##ACTIVE SPORTS HUB');
    lines.push('##CENTER##Persiaran Setia, Setia Alam');
    lines.push('##CENTER##40170 Shah Alam, Selangor');
    lines.push('##CENTER##Tel: 03-3345 6677');
    lines.push('##DSEP##');
    lines.push('##CENTER####BOLD##ACTIVITY BILL');
    lines.push('##DSEP##');
    lines.push('');

    lines.push('Bill No:     ' + bill.billNo);
    lines.push('Date:        ' + bill.date);
    lines.push('Staff:       ' + bill.staff);
    lines.push('Customer:    ' + bill.customerName);

    if (bill.memberId) {
      lines.push('Member ID:   ' + bill.memberId);
      lines.push('Tier:        ' + bill.memberTier);
    }

    lines.push('');

    bill.activities.forEach(function(activity, index) {
      if (index > 0) lines.push('');
      lines.push('##BOLD##' + activity.name);
      lines.push('##SEP##');

      if (activity.facility) {
        lines.push('##TABLE:<Facility|>'
          + activity.facility + '##');
      }

      if (activity.timeSlot) {
        lines.push('##TABLE:<Time|>'
          + activity.timeSlot + '##');
      }

      if (activity.duration) {
        lines.push('##TABLE:<Duration|>'
          + activity.duration + '##');
      }

      if (activity.pax) {
        lines.push('##TABLE:<Pax|>'
          + activity.pax + '##');
      }

      if (activity.details && activity.details.length > 0) {
        activity.details.forEach(function(detail) {
          lines.push('##TABLE:<' + detail.label
            + '|>' + detail.value + '##');
        });
      }

      lines.push('##TABLE:<Charge|>RM '
        + activity.charge.toFixed(2) + '##');
    });

    lines.push('');
    lines.push('##DSEP##');
    lines.push('##BOLD##Summary');
    lines.push('##DSEP##');
    lines.push('');

    bill.activities.forEach(function(activity) {
      lines.push('##TABLE:<' + activity.name + '|>RM '
        + activity.charge.toFixed(2) + '##');
    });

    lines.push('##SEP##');
    lines.push('##TABLE:<Subtotal|>RM '
      + bill.subtotal.toFixed(2) + '##');

    if (bill.discount > 0) {
      lines.push('##TABLE:<'
        + bill.discountLabel + '|>-RM '
        + bill.discount.toFixed(2) + '##');
    }

    if (bill.tax > 0) {
      lines.push('##TABLE:<Service Tax 6%|>RM '
        + bill.tax.toFixed(2) + '##');
    }

    lines.push('##DSEP##');
    lines.push('##BOLD####BIG####CENTER##RM '
      + bill.grandTotal.toFixed(2));
    lines.push('##DSEP##');
    lines.push('');

    lines.push('##TABLE:<Payment|>'
      + bill.paymentMethod + '##');

    if (bill.paymentRef) {
      lines.push('##TABLE:<Ref|>'
        + bill.paymentRef + '##');
    }

    lines.push('');

    if (bill.pointsEarned) {
      lines.push('##SEP##');
      lines.push('##TABLE:<Points Earned|>+'
        + bill.pointsEarned + '##');
      lines.push('##TABLE:<Total Points|>'
        + bill.totalPoints + '##');
      lines.push('');
    }

    lines.push('##SEP##');
    lines.push('');
    lines.push('##CENTER##Download our app:');
    lines.push('##QR_SIZE:6##');
    lines.push('##QR:https://activesportshub.com/app##');
    lines.push('');
    lines.push('##SEP##');
    lines.push('##CENTER####SMALL##All bookings are subject to');
    lines.push('##CENTER####SMALL##availability and our T&C.');
    lines.push('##CENTER####SMALL##www.activesportshub.com/terms');
    lines.push('');
    lines.push('##CENTER##Stay active, stay healthy!');
    lines.push('');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printMultiActivityBill({
    billNo: 'ASH-20260204-0211',
    date: '2026-02-04 18:30',
    staff: 'Nurul',
    customerName: 'Lim Chee Keong',
    memberId: 'ASH-G-0088',
    memberTier: 'Gold',
    activities: [
      {
        name: 'Badminton Court',
        facility: 'Court B2',
        timeSlot: '18:00 - 20:00',
        duration: '2 hours',
        pax: 4,
        details: [
          { label: 'Shuttle (prov)', value: '2 tubes' }
        ],
        charge: 50.00
      },
      {
        name: 'Swimming Pool',
        facility: 'Olympic Pool',
        timeSlot: '18:00 - 20:00',
        duration: '2 hours',
        pax: 2,
        details: [],
        charge: 20.00
      },
      {
        name: 'Gym Access',
        facility: 'Main Gym',
        timeSlot: '18:00 - 20:00',
        duration: '2 hours',
        pax: 1,
        details: [
          { label: 'Locker', value: 'L-045' }
        ],
        charge: 15.00
      }
    ],
    subtotal: 85.00,
    discountLabel: 'Gold Member 20%',
    discount: 17.00,
    tax: 4.08,
    grandTotal: 72.08,
    paymentMethod: 'Debit Card',
    paymentRef: 'DC-99210045',
    pointsEarned: 72,
    totalPoints: 3480
  });

12.11 SNOOKER TABLE SESSION TIMER SLIP

A small slip printed when a table session starts, given to the customer
as a reminder of their table assignment and start time. This uses partial
cut so slips stay attached in a roll for the counter staff.

  function printTableStartSlip(session) {
    var lines = [];

    lines.push('##BEEP:1##');
    lines.push('##CENTER####BOLD####BIG##Q-BALL SNOOKER');
    lines.push('##SEP##');
    lines.push('##CENTER####BOLD##TABLE SESSION STARTED');
    lines.push('##SEP##');
    lines.push('');

    lines.push('##CENTER####BOLD####BIG##'
      + session.tableName);
    lines.push('');

    lines.push('##TABLE:<Type|>'
      + session.tableType + '##');
    lines.push('##TABLE:<Player(s)|>'
      + session.playerCount + '##');
    lines.push('');

    lines.push('##DSEP##');
    lines.push('##CENTER####BOLD##Start Time');
    lines.push('##CENTER####BOLD####BIG##'
      + session.startTime);
    lines.push('##DSEP##');
    lines.push('');

    lines.push('##TABLE:<Rate|>RM '
      + session.ratePerHour.toFixed(2) + '/hr##');

    if (session.isHappyHour) {
      lines.push('##CENTER####BOLD##HAPPY HOUR RATE APPLIED');
      lines.push('##TABLE:<Normal Rate|>RM '
        + session.normalRate.toFixed(2) + '/hr##');
      lines.push('##TABLE:<Your Rate|>RM '
        + session.ratePerHour.toFixed(2) + '/hr##');
    }

    lines.push('');
    lines.push('##TABLE:<Session ID|>'
      + session.sessionId + '##');
    lines.push('');

    lines.push('##SEP##');
    lines.push('##CENTER####SMALL##Present this slip at counter');
    lines.push('##CENTER####SMALL##when your session ends.');
    lines.push('##CENTER####SMALL##Timer runs until checkout.');
    lines.push('');

    lines.push('##BARCODE_HEIGHT:50##');
    lines.push('##BARCODE_HRI:BELOW##');
    lines.push('##BARCODE:CODE128:' + session.sessionId + '##');
    lines.push('');

    lines.push('##CENTER##Enjoy your game!');
    lines.push('');
    lines.push('##FEED:3##');
    lines.push('##PARTCUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

  // Usage:
  printTableStartSlip({
    tableName: 'TABLE 7',
    tableType: 'Snooker (Full Size)',
    playerCount: 2,
    startTime: '20:30',
    ratePerHour: 12.00,
    isHappyHour: true,
    normalRate: 18.00,
    sessionId: 'QBS-20260204-0071'
  });

13. CSS CLASSES AUTO-DETECTION

If you use common CSS frameworks (Bootstrap, Tailwind), the extension
recognizes these classes and converts them to formatting:

  Bootstrap:
    text-center       ->  ##CENTER##
    fw-bold           ->  ##BOLD##

  Tailwind CSS:
    text-center       ->  ##CENTER##
    font-bold         ->  ##BOLD##
    text-xl           ->  ##BIG##
    text-2xl          ->  ##BIG##

  Generic:
    center            ->  ##CENTER##
    bold              ->  ##BOLD##
    big               ->  ##BIG##
    large             ->  ##BIG##
    title             ->  ##BIG##
    total             ->  ##BIG##

  Computed styles:
    font-weight >= 700  ->  ##BOLD##
    font-size >= 20px   ->  ##BIG##
    text-align: center  ->  ##CENTER##

14. EXISTING PRINT BUTTON INTERCEPTION

If your page already has a print button, the extension can intercept it
and redirect the print action to the thermal printer instead of the
browser's print dialog.

The extension looks for these selectors:

    #btn-print
    #print-btn
    .btn-print
    .print-btn
    [data-action="print"]
    button[onclick*="print"]

If any of these buttons are found AND a print content element exists on
the page, clicking the button will:
  1. Prevent the default action (e.g., window.print())
  2. Extract the print content
  3. Send it to the thermal printer

This means if your existing code has:

  <button id="btn-print" onclick="window.print()">Print</button>

The extension will automatically intercept this and print to the thermal
printer instead of opening the browser print dialog.

If you do NOT want this interception, use a button ID/class that does
not match the above selectors.

15. ERROR HANDLING

When using Method B (postMessage), there is no direct callback. The
extension shows toast notifications on the page:

  Success:  "Print Sent" - green toast, top-right corner
  Error:    "Print Error" or "Print Failed" - red toast with details

Common error messages and their solutions:

  "Could not connect to VYROX Printer Connector. Is the native host
   installed?"
    Cause:  The Chrome extension cannot reach the native host.
    Fix:    Start the VYROX_PrinterConnector.exe application.

  "Extension Reloaded - Please refresh this page to reconnect to VYROX
   Printer Connector."
    Cause:  The extension was updated or reloaded while the page was open.
    Fix:    Refresh the web page (F5).

  "Native host disconnected without response"
    Cause:  The native host crashed or is not properly installed.
    Fix:    Re-run the installer (VYROX_PrinterConnector.exe -Install).

  "Native host timeout (10s)"
    Cause:  The native host did not respond within 10 seconds.
    Fix:    Check if the printer is jammed or offline. Restart the
            native host.

  "Printer busy"
    Cause:  A previous print job is still being processed.
    Fix:    Wait a moment and try again.

  "Empty content"
    Cause:  The content sent to the printer was empty.
    Fix:    Check that your print text is not blank.

  "No print content element found on this page"
    Cause:  The floating button or popup print was used, but no
            matching element was found.
    Fix:    Add id="vyrox_printer_connector" to your print container.

  "Print content is empty"
    Cause:  A print content element was found but it had no text.
    Fix:    Make sure the element has visible text content.

For programmatic error handling, you can listen for messages back from
the extension (reserved for future support):

  window.addEventListener('message', function(event) {
    if (event.data
        && event.data.type === 'VYROX_PRINTER_CONNECTOR_RESULT') {
      if (event.data.success) {
        console.log('Print sent successfully');
      } else {
        console.error('Print failed:', event.data.error);
      }
    }
  });

  NOTE: The current version does not send result messages back via
  postMessage. This is documented here for potential future support.
  Currently, the extension shows toast notifications only.

16. DEBUGGING AND TROUBLESHOOTING

Step 1: Check extension popup
  Click the VYROX Printer Connector icon in Chrome toolbar.
  - Native Host should show "Connected" (green dot)
  - Printer should show the printer name (green dot)
  - If either is red, the native host is not running or not installed.

Step 2: Check the log
  In the extension popup, click "Toggle Log" to see debug messages.

Step 3: Check if print content element is detected
  Open Chrome DevTools (F12) and run in the console:
    document.querySelector('#vyrox_printer_connector')
  If this returns null, the extension cannot find your print content
  element. Try the other supported selectors:
    document.querySelector('#vyrox-printer-connector')
    document.querySelector('.vyrox_printer_connector')
    document.querySelector('[data-vyrox-printer-connector="true"]')
    document.querySelector('#print-area')

Step 4: Check if the floating button appears
  If a print content element exists, a floating print button should
  appear in the bottom-right corner of the page. If it does not:
  - Check that the extension is enabled in chrome://extensions
  - Check that content scripts are allowed on your page
  - Try refreshing the page
  - Check if another element with a higher z-index is covering it

Step 5: Test with a simple print
  Add this to any page and check if printing works:
    <div id="vyrox_printer_connector">Test print 123</div>

Step 6: Test postMessage directly
  Open Chrome DevTools console and run:
    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: 'Hello from console\n##CUT##'
    }, '*');
  You should see a toast notification on the page.

Step 7: Check native host directly
  Open the folder C:\VYROX_PrinterConnector
  - printer_settings.json should exist (printer detected)
  - If you create a print.this file manually with text content,
    the native host should pick it up and print it within 1 second.

Step 8: Check for Chrome extension errors
  Go to chrome://extensions
  Find VYROX Printer Connector
  Click "Errors" (if shown) to see any console errors
  Click "Inspect views: service worker" to see background script logs

Step 9: Check native host process
  Open Windows Task Manager (Ctrl+Shift+Esc)
  Look for VYROX_PrinterConnector.exe in the processes list.
  If it is not running, launch it from C:\VYROX_PrinterConnector.

Step 10: Test printer hardware
  If the native host is connected but prints fail:
  - Check that the USB cable is firmly connected
  - Check that the printer is powered on
  - Check that paper is loaded and cover is closed
  - Check for blinking error lights on the printer
  - Try power-cycling the printer (off for 10 seconds, then on)

17. SECURITY CONSIDERATIONS

- The extension only listens for postMessage from the same window
    (event.source === window check). Cross-origin iframes cannot trigger
    printing.

  - The native host only accepts three commands: ping, status, and print.
    It does not execute arbitrary code.

  - Print content is written to a temporary file (print.this) and
    deleted immediately after printing.

  - The native host communicates only with the locally connected USB
    printer. It does not make any network connections.

  - The extension runs on all URLs by default. If you want to restrict
    it, you can modify the manifest.json matches pattern.

  - All HTML content in toast notifications is escaped to prevent XSS.

  - The native host runs under the current Windows user account. No
    elevated privileges are required for normal operation (only for
    initial installation).

18. BROWSER COMPATIBILITY

Supported:
    Google Chrome (version 88+, Manifest V3)
    Microsoft Edge (Chromium-based, version 88+)

  Not supported:
    Firefox (different native messaging API)
    Safari (no native messaging support)
    Opera (may work with Chrome extension sideloading)

  Operating system:
    The native host requires Windows (PowerShell).
    The Chrome extension itself works on any OS, but printing only works
    on Windows where the native host is installed.

19. ADVANCED INTEGRATION PATTERNS

19.1 AUTO-PRINT AFTER AJAX ORDER SUBMISSION

The most common integration pattern: print automatically after an order
is saved to your backend, with no user interaction required.

  function saveAndPrint(orderData) {
    fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(orderData)
    })
    .then(function(res) { return res.json(); })
    .then(function(savedOrder) {
      // Build print content from server response
      var lines = [];
      lines.push('##CENTER####BOLD####BIG##MY STORE');
      lines.push('##SEP##');
      lines.push('Bill No: ' + savedOrder.billNo);
      lines.push('Date: ' + savedOrder.date);
      lines.push('##SEP##');

      savedOrder.items.forEach(function(item) {
        lines.push('##TABLE:<' + item.name
          + '|^' + item.qty
          + '|>' + item.price.toFixed(2) + '##');
      });

      lines.push('##SEP##');
      lines.push('##BOLD####TABLE:<TOTAL|>RM '
        + savedOrder.total.toFixed(2) + '##');
      lines.push('##FEED:4##');
      lines.push('##CUT##');

      window.postMessage({
        type: 'VYROX_PRINTER_CONNECTOR_PRINT',
        content: lines.join('\n')
      }, '*');
    })
    .catch(function(err) {
      alert('Order save failed: ' + err.message);
    });
  }

19.2 PRINT QUEUE WITH MULTIPLE JOBS

When you need to print multiple slips from one action (for example a
customer copy plus a kitchen copy), add a delay between each job to
allow the printer to finish.

  function printCustomerAndKitchenCopy(order) {
    var customerCopy = buildCustomerBill(order);
    var kitchenCopy = buildKitchenOrder(order);

    // Print customer copy first
    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: customerCopy
    }, '*');

    // Wait 3 seconds then print kitchen copy
    setTimeout(function() {
      window.postMessage({
        type: 'VYROX_PRINTER_CONNECTOR_PRINT',
        content: kitchenCopy
      }, '*');
    }, 3000);
  }

  function buildCustomerBill(order) {
    var lines = [];
    lines.push('##CENTER####BOLD####BIG##MY RESTAURANT');
    lines.push('##CENTER##CUSTOMER COPY');
    lines.push('##DSEP##');
    // ... build full bill ...
    lines.push('##FEED:4##');
    lines.push('##CUT##');
    return lines.join('\n');
  }

  function buildKitchenOrder(order) {
    var lines = [];
    lines.push('##BEEP:2##');
    lines.push('##CENTER####BOLD####BIG##KITCHEN');
    lines.push('##DSEP##');
    lines.push('##BOLD##Table: ' + order.tableNo);
    // ... build kitchen items ...
    lines.push('##FEED:3##');
    lines.push('##PARTCUT##');
    return lines.join('\n');
  }

  Recommended delays between print jobs:
    Short content (under 20 lines):    2000ms (2 seconds)
    Medium content (20-50 lines):      3000ms (3 seconds)
    Long content or images/QR codes:   5000ms (5 seconds)

19.3 DUAL-COPY PRINTING (CUSTOMER + MERCHANT)

Print two copies in one print job by including both copies in a single
content string with a cut between them. This is faster than sending
two separate jobs.

  function printDualCopy(order) {
    var lines = [];

    // --- MERCHANT COPY ---
    lines.push('##CENTER####BOLD##** MERCHANT COPY **');
    lines.push('##CENTER####BOLD####BIG##MY STORE');
    lines.push('##DSEP##');
    lines.push('Bill No: ' + order.billNo);
    // ... full bill content ...
    lines.push('##CENTER####SMALL##Merchant copy - do not discard');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    // --- CUSTOMER COPY ---
    lines.push('##CENTER####BOLD##** CUSTOMER COPY **');
    lines.push('##CENTER####BOLD####BIG##MY STORE');
    lines.push('##DSEP##');
    lines.push('Bill No: ' + order.billNo);
    // ... full bill content ...
    lines.push('##CENTER##Thank you!');
    lines.push('##FEED:4##');
    lines.push('##CUT##');

    window.postMessage({
      type: 'VYROX_PRINTER_CONNECTOR_PRINT',
      content: lines.join('\n')
    }, '*');
  }

19.4 CONDITIONAL PRINT BASED ON PRINTER STATUS

Before printing, you can check if the native host is connected by
sending a test message through the extension. This requires listening
for a response from the content script.

  function checkPrinterAndPrint(content) {
    // First test connection via the extension popup mechanism
    chrome.runtime.sendMessage(
      { action: 'testConnection' },
      function(response) {
        if (response && response.success) {
          // Printer is connected, proceed with print
          window.postMessage({
            type: 'VYROX_PRINTER_CONNECTOR_PRINT',
            content: content
          }, '*');
        } else {
          // Printer not available, show fallback
          alert('Printer is not connected. Please check the printer.');
        }
      }
    );
  }

  NOTE: The chrome.runtime.sendMessage API is only available within
  the extension context. For web pages, rely on the toast notification
  feedback from the extension to know if the print succeeded or failed.

19.5 FRAMEWORK INTEGRATION (REACT, VUE, ANGULAR)

React example:

  function PrintButton({ orderData }) {
    const handlePrint = () => {
      const lines = [];
      lines.push('##CENTER####BOLD####BIG##MY STORE');
      lines.push('##SEP##');

      orderData.items.forEach(item => {
        lines.push('##TABLE:<' + item.name
          + '|^' + item.qty
          + '|>' + item.price.toFixed(2) + '##');
      });

      lines.push('##SEP##');
      lines.push('##BOLD####TABLE:<TOTAL|>RM '
        + orderData.total.toFixed(2) + '##');
      lines.push('##FEED:4##');
      lines.push('##CUT##');

      window.postMessage({
        type: 'VYROX_PRINTER_CONNECTOR_PRINT',
        content: lines.join('\n')
      }, '*');
    };

    return <button onClick={handlePrint}>Print Bill</button>;
  }

Vue example:

  <template>
    <button @click="printBill">Print Bill</button>
  </template>

  <script>
  export default {
    props: ['orderData'],
    methods: {
      printBill() {
        const lines = [];
        lines.push('##CENTER####BOLD####BIG##MY STORE');
        lines.push('##SEP##');

        this.orderData.items.forEach(item => {
          lines.push('##TABLE:<' + item.name
            + '|^' + item.qty
            + '|>' + item.price.toFixed(2) + '##');
        });

        lines.push('##SEP##');
        lines.push('##BOLD####TABLE:<TOTAL|>RM '
          + this.orderData.total.toFixed(2) + '##');
        lines.push('##FEED:4##');
        lines.push('##CUT##');

        window.postMessage({
          type: 'VYROX_PRINTER_CONNECTOR_PRINT',
          content: lines.join('\n')
        }, '*');
      }
    }
  };
  </script>

Angular example:

  import { Component, Input } from '@angular/core';

  @Component({
    selector: 'app-print-button',
    template: '<button (click)="printBill()">Print Bill</button>'
  })
  export class PrintButtonComponent {
    @Input() orderData: any;

    printBill() {
      const lines: string[] = [];
      lines.push('##CENTER####BOLD####BIG##MY STORE');
      lines.push('##SEP##');

      this.orderData.items.forEach((item: any) => {
        lines.push('##TABLE:<' + item.name
          + '|^' + item.qty
          + '|>' + item.price.toFixed(2) + '##');
      });

      lines.push('##SEP##');
      lines.push('##BOLD####TABLE:<TOTAL|>RM '
        + this.orderData.total.toFixed(2) + '##');
      lines.push('##FEED:4##');
      lines.push('##CUT##');

      window.postMessage({
        type: 'VYROX_PRINTER_CONNECTOR_PRINT',
        content: lines.join('\n')
      }, '*');
    }
  }

Plain PHP (server-rendered page with print trigger):

  <?php
  $order = getOrderFromDatabase($orderId);
  ?>

  <div id="vyrox_printer_connector" style="display:none;">
    <pre class="print-content-text">
##CENTER####BOLD####BIG##MY STORE
##CENTER##123 Main Street
##DSEP##
##CENTER####BOLD##SALES BILL
##DSEP##

Bill No:  <?= $order['bill_no'] ?>

Date:     <?= $order['date'] ?>

##SEP##
<?php foreach ($order['items'] as $item): ?>
##TABLE:<<?= $item['name'] ?>|^<?= $item['qty'] ?>|><?= number_format($item['price'], 2) ?>##
<?php endforeach; ?>
##SEP##
##BOLD####BIG####TABLE:<TOTAL|>RM <?= number_format($order['total'], 2) ?>##
##SEP##

##TABLE:<<?= $order['payment_method'] ?>|><?= number_format($order['amount_paid'], 2) ?>##
##TABLE:<Change|><?= number_format($order['change'], 2) ?>##

##SEP##
##CENTER##Thank you!
##FEED:4##
##CUT##
    </pre>
  </div>

  <button onclick="document.getElementById('vyrox_printer_connector')
    .style.display='block'; setTimeout(function(){
    document.getElementById('vyrox_printer_connector')
    .style.display='none';}, 500);">
    Print Bill
  </button>

  Explanation: The hidden div contains pre-formatted print content. When
  the button is clicked, the div is briefly made visible so the extension
  MutationObserver detects it and shows the floating print button.
  Alternatively, use the postMessage method for a cleaner approach.

20. FREQUENTLY ASKED QUESTIONS

Q: Do I need to install anything on my web server?
A: No. Everything runs client-side. Your server just serves the web page
   as normal. The extension and native host are installed on the
   end-user's computer.

Q: Can I print without showing any print content on the page?
A: Yes. Use Method B (postMessage) to send content directly. No HTML
   element is needed on the page. No floating button will appear.

Q: Can I print automatically without user interaction?
A: Yes. Call window.postMessage with VYROX_PRINTER_CONNECTOR_PRINT from
   your JavaScript at any time. For example, after saving an order via
   AJAX, you can immediately trigger the print.

Q: Can I print multiple copies?
A: Yes. Two approaches:
   1. Send two separate postMessage calls with a 2-3 second delay.
   2. Include both copies in one content string with ##CUT## between
      them (see section 19.3 for dual-copy example).

Q: Can I print to multiple printers?
A: The current version supports one printer at a time, as configured
   in the native host. Multiple printer support may be added in future
   versions.

Q: What if the printer is offline or disconnected?
A: The extension will show a red error toast notification. The print job
   will fail. Your application should handle this gracefully (for
   example show a retry button or queue the job for later).

Q: What is the maximum print content length?
A: There is no hard limit. The native host processes the content line
   by line. However, very long content (thousands of lines) may take
   longer to print. The native messaging channel has a 1MB message
   size limit.

Q: Can I use this with React, Vue, or Angular?
A: Yes. The postMessage API works with any framework. Just call
   window.postMessage from your component. See section 19.5 for
   framework-specific examples.

Q: Can I print images like logos?
A: Yes, using the ##IMAGE:PATH## tag. The image file must exist on the
   user's local computer. Common approach: include the logo in the
   native host installation at C:\VYROX_PrinterConnector\logo.bmp.

Q: Does the extension work on mobile?
A: No. Chrome extensions with native messaging do not work on Android
   or iOS. This is a desktop-only solution.

Q: Can I customize the floating button position or appearance?
A: The floating button is styled by the extension's content script.
   It has a z-index of 999999 and is positioned at bottom: 24px,
   right: 24px. To customize it, you would need to modify the
   extension source code (content.js).

Q: My print output has special characters that do not display correctly.
A: Use the ##CODEPAGE## tag to set the correct character encoding.
   Try ##CODEPAGE:CP1252## for Western European characters, or
   ##CODEPAGE:UTF8## if your printer supports UTF-8.

Q: Can I open the cash drawer without printing anything visible?
A: Yes. Send just the drawer command:
     window.postMessage({
       type: 'VYROX_PRINTER_CONNECTOR_PRINT',
       content: '##DRAWER##\n##CUT##'
     }, '*');

Q: How do I print a barcode for scanning at checkout?
A: Use the ##BARCODE## tag. CODE128 is the most versatile type:
     ##BARCODE_HRI:BELOW##
     ##BARCODE:CODE128:YOUR-DATA-HERE##
   See section 9.7 for all barcode types and options.

Q: Can I use this for a snooker or pool hall billing system?
A: Yes. See sections 12.7, 12.9, and 12.11 for complete snooker and
   billiard examples including session billing, tournament scorecards,
   and table timer slips.

Q: Can I use this for a sports facility booking system?
A: Yes. See sections 12.6, 12.8, and 12.10 for complete sports booking
   examples including single-activity bookings, membership discount
   handling, and multi-activity complex billing.

Q: What HTML element IDs does the extension look for?
A: In priority order: #vyrox_printer_connector,
   #vyrox-printer-connector, #vyrox_printer_connector-content,
   #print-area. It also checks classes: .vyrox_printer_connector,
   .vyrox-printer-connector, .vyrox_printer_connector-content,
   .print-area. And the data attribute:
   [data-vyrox-printer-connector="true"].

Q: What is the postMessage type string I need to use?
A: The exact string is: VYROX_PRINTER_CONNECTOR_PRINT
   It is case-sensitive and must match exactly.

Q: Can I prevent the auto-cut at the end of a print?
A: If your content includes any ##CUT## or ##PARTCUT## tag anywhere,
   the extension will not add an automatic cut at the end. So place
   your own ##CUT## or ##PARTCUT## exactly where you want it.

Q: The floating button is covering my page content. How do I move it?
A: The button is fixed-positioned at bottom: 24px, right: 24px. You
   can add CSS on your page to reposition it:
     #vyrox-printer-connector-fab { bottom: 80px !important; }

Q: How long does the printer take to process a print job?
A: Typical print jobs (under 50 lines) process in under 1 second.
   Jobs with images or many QR codes may take 2-5 seconds. Jobs with
   barcodes and QR codes together may take 3-7 seconds depending on
   the printer model.

END OF INTEGRATION GUIDE

For support:  https://vyrox.com
================================================================================