ptouch-webapp

May 14, 2026 ยท View on GitHub

A modern, client-side Single-Page Application (SPA) for designing and printing labels on a Brother PT-E560BTVP (and compatible TZe-tape PT-E/PT-P series printers) via Web Serial.

Works over both USB (direct connection) and Bluetooth Classic (SPP) via the operating system's virtual COM port โ€” no driver installation required on modern operating systems.

Built with Vite 8, Tailwind CSS v4, and vanilla ES2022+ JavaScript. No backend required โ€” everything runs entirely in the browser.

Why Web Serial instead of Web Bluetooth? The PT-E560BT uses Bluetooth Classic (SPP profile), not Bluetooth Low Energy (BLE). Web Bluetooth only supports BLE, making it incompatible with this device. Web Serial works because both USB and paired Bluetooth SPP connections are exposed by the OS as standard virtual serial ports.


Features

FeatureDetails
๐Ÿ”Œ Web SerialConnects via USB or paired Bluetooth COM port
๐Ÿ–จ Brother Raster ProtocolFull ESC/P command set for PT-E series
โœ‚ Half-CutESC i K 0x08 between labels for easy tear-off
๐Ÿ”— Chain PrintingESC i M 0x08 to minimise tape waste
๐Ÿ”ข CopiesPrint N copies with a single button press
๐Ÿ‘ Live PreviewReal-time canvas preview updates as you type
๐Ÿ“ Multi-width tapesSupports 12 mm, 18 mm, and 24 mm TZe tapes
๐Ÿ“ฑ Mobile-first UIResponsive dark-mode interface

Browser Requirements

Web Serial is a privileged API with strict requirements:

RequirementNotes
HTTPS or localhostPlain HTTP origins will have navigator.serial undefined
Chromium-based browserChrome 89+, Edge 89+, or Opera 75+ on Windows, macOS, Linux, or ChromeOS
User Activationserial.requestPort() must be called from a user gesture (click)

Firefox and Safari do not support Web Serial. Use Chrome, Edge, or Opera.


How to Connect

USB Connection

  1. Plug the printer into a USB port on your computer.
  2. Open the web app and click Connect Printer.
  3. A browser dialog lists all available serial/USB devices. Select the entry for the Brother printer (it may appear as USB Serial Device, USB VID:PID, or similar, depending on the OS).
  4. Click Connect in the dialog. The status indicator turns green.

Bluetooth โ€” Windows / macOS

Bluetooth Classic (SPP) creates a virtual COM port through the OS. The browser then treats it like any other serial port.

  1. Pair the printer first: Open the system Bluetooth settings, scan for devices, and pair with PT-E560BT_xxxx. Accept any PIN prompt (default PIN is usually 0000).
  2. After pairing, a virtual COM port is automatically created (e.g., COM5 on Windows, /dev/cu.PT-E560BT_xxxx-SerialPort on macOS).
  3. Open the web app and click Connect Printer.
  4. Select the COM port that corresponds to the printer in the browser dialog.

Bluetooth & USB โ€” Linux

Serial Port Permissions

By default, serial/USB devices on Linux are owned by the dialout group. Without membership in that group the browser will either fail silently or the port will not appear in the picker.

# Add your user to the dialout group (Debian, Ubuntu, Mint, Fedora, โ€ฆ)
sudo usermod -a -G dialout $USER

# On Arch-based distros (Manjaro, EndeavourOS, โ€ฆ) use uucp instead:
sudo usermod -a -G uucp $USER

A logout/login (or full reboot) is required for the group change to take effect. Verify with groups $USER โ€” dialout (or uucp) must appear.

USB on Linux

Once in the dialout group, plug in the printer. It will appear as /dev/ttyUSB0 or /dev/ttyACM0. Click Connect Printer in the app and select that device from the port picker.

Bluetooth on Linux

The OS does not create a virtual serial port for Bluetooth devices automatically. You need to bind the printer to an rfcomm device first.

Step 1 โ€” Pair the printer:

bluetoothctl
# Inside the interactive shell:
power on
scan on
# Wait until PT-E560BT_xxxx appears, note the MAC address, then:
pair   94:DD:F8:A1:35:80   # replace with your printer's MAC address
trust  94:DD:F8:A1:35:80
quit

Step 2 โ€” Bind to an rfcomm device:

sudo rfcomm bind 0 94:DD:F8:A1:35:80
# Creates /dev/rfcomm0

To make this persistent across reboots, add it to /etc/rc.local or create a small systemd service.

Step 3 โ€” Open the web app, click Connect Printer, and select /dev/rfcomm0 in the port picker.

Troubleshooting โ€” port picker is empty:

  • Ensure dialout group membership is active (groups $USER).
  • Check whether ModemManager has claimed the port: sudo systemctl stop ModemManager
  • Confirm the rfcomm binding exists: ls -l /dev/rfcomm*

Quick Start

Prerequisites

Install & Run

# Clone the repository
git clone https://github.com/the78mole/ptouch-webapp.git
cd ptouch-webapp

# Install dependencies
npm install

# Start development server (served on http://localhost:5173)
npm run dev

The dev server runs on localhost, which satisfies the HTTPS requirement for Web Serial.

Production Build

npm run build    # outputs to dist/
npm run preview  # serve the production build locally

For deployment, host the dist/ folder on any HTTPS-capable static host (GitHub Pages, Netlify, Vercel, Cloudflare Pages, etc.).


Deployment โ€” GitHub Pages

The app is automatically deployed to https://the78mole.github.io/ptouch-webapp/ via the .github/workflows/deploy.yml workflow.

One-time repository setup

  1. Go to Settings โ†’ Pages in the GitHub repository.
  2. Under Build and deployment / Source, select GitHub Actions (not the legacy "Deploy from a branch" option).
  3. Save. That's it โ€” no gh-pages branch needed.

Automated deployments

TriggerAction
Push to mainBuild + deploy automatically
ManualActions โ†’ Deploy to GitHub Pages โ†’ Run workflow

The workflow uses three official GitHub Actions:

actions/configure-pages        โ€” reads Pages settings, injects base URL
actions/upload-pages-artifact  โ€” packages dist/ as a Pages artifact
actions/deploy-pages           โ€” publishes the artifact to GitHub Pages

Only one deployment runs at a time; a newer push automatically cancels any in-progress run (concurrency: group: pages, cancel-in-progress: true).


Project Structure

ptouch-webapp/
โ”œโ”€โ”€ index.html          # App shell; Tailwind v4 entry point
โ”œโ”€โ”€ vite.config.js      # Vite 8 + @tailwindcss/vite plugin
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ scan_ptouch.py  # BLE/BT diagnostic script (uv run)
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ style.css       # @import "tailwindcss" + custom CSS
    โ”œโ”€โ”€ serial.js       # Web Serial connection & chunked writes
    โ”œโ”€โ”€ protocol.js     # Brother raster commands & canvas rasterization
    โ””โ”€โ”€ app.js          # UI event wiring & canvas rendering

Serial Module (src/serial.js)

PropertyValue
Baud Rate115200 (required by the API; ignored by USB/BT virtual ports)
Chunk Size512 bytes
Inter-chunk delay10 ms
import { SerialManager } from "./src/serial.js";

const serial = new SerialManager();
serial.onStatusChange = (status) => console.log(status);

// Must be called inside a user-gesture handler:
await serial.connect();
await serial.sendData(myUint8Array);
await serial.disconnect();

Protocol Module (src/protocol.js)

Tape Configuration

Dot counts from the libptouch reference implementation (tape_info[]). The print head is always 128 dots wide; narrower tapes are centred automatically.

WidthPrintable DotsOffset (dots)Bytes / Line
24 mm128016
18 mm120416
12 mm762616
9 mm523816
6 mm324816

$\text{text} \text{buildInvalidation}() 100 \times 0\text{x00} + \text{ESC} @ โ€” \text{reset} \text{printer} โ”Œโ”€ \text{per} \text{copy} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ \text{buildRasterMode}() 1\text{B} 69 61 01 โ€” \text{raster} \text{mode} โ”‚ โ”‚ \text{buildMediaCommand}(\text{mm}, \text{n}) 1\text{B} 69 7\text{A} โ€ฆ โ€” \text{media} \text{info} โ”‚ โ”‚ โ””โ”€ \text{n9} = 0\text{x02} (\text{required} \text{for} \text{D460BT}/\text{E560BT}) โ”‚ โ”‚ \text{buildD460btMagic}() 1\text{B} 69 64 01 00 4\text{D} 00 โ”‚ โ”‚ \text{buildCutCommand}(\text{false}) 1\text{B} 69 4\text{B} 00 โ€” \text{chain} (\text{if} \text{chain})โ”‚ โ”‚ \text{buildRasterLine}(\text{data}) 47 \text{lo} \text{hi} โ€ฆ โ€” \text{raster} \text{line} โ”‚ โ”‚ \text{buildEject}() 1\text{A} โ€” \text{always} \text{for} \text{D460BT}โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ $

Canvas โ†’ Raster Conversion

canvas.width  = label length (dots = number of raster lines)
canvas.height = tape dots    (e.g., 128 for 24 mm)

Bit packing (LSB-first, matches libptouch rasterline_setpixel):
  pixel     = offsetDots + (tapeDots - 1 - dot)   โ† centred, Y-flipped
  byteIndex = (15) - floor(pixel / 8)             โ† reverse-indexed
  bitIndex  = pixel & 7                           โ† LSB-first
  line[byteIndex] |= 1 << bitIndex

Troubleshooting

SymptomLikely CauseFix
navigator.serial is undefinedNot HTTPS / unsupported browserUse localhost in dev, deploy to HTTPS; use Chrome/Edge/Opera
Port picker is emptyNo serial port permissionsudo usermod -a -G dialout $USER, then re-login
Port picker is empty (BT, Linux)rfcomm not boundsudo rfcomm bind 0 <MAC> first
Port picker is empty (BT)Printer not paired via OSPair through OS Bluetooth settings before opening the app
Port claimed by ModemManagerModemManager auto-connects serial devicessudo systemctl stop ModemManager
Print garbled / no outputWrong tape width selectedMatch the tape actually loaded in the printer
Write errors during printBuffer overflowReduce CHUNK_SIZE or increase INTER_CHUNK_DELAY_MS in serial.js

License

MIT โ€” see LICENSE.