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
| Feature | Details |
|---|---|
| ๐ Web Serial | Connects via USB or paired Bluetooth COM port |
| ๐จ Brother Raster Protocol | Full ESC/P command set for PT-E series |
| โ Half-Cut | ESC i K 0x08 between labels for easy tear-off |
| ๐ Chain Printing | ESC i M 0x08 to minimise tape waste |
| ๐ข Copies | Print N copies with a single button press |
| ๐ Live Preview | Real-time canvas preview updates as you type |
| ๐ Multi-width tapes | Supports 12 mm, 18 mm, and 24 mm TZe tapes |
| ๐ฑ Mobile-first UI | Responsive dark-mode interface |
Browser Requirements
Web Serial is a privileged API with strict requirements:
| Requirement | Notes |
|---|---|
HTTPS or localhost | Plain HTTP origins will have navigator.serial undefined |
| Chromium-based browser | Chrome 89+, Edge 89+, or Opera 75+ on Windows, macOS, Linux, or ChromeOS |
| User Activation | serial.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
- Plug the printer into a USB port on your computer.
- Open the web app and click Connect Printer.
- 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).
- 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.
- 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 usually0000). - After pairing, a virtual COM port is automatically created
(e.g.,
COM5on Windows,/dev/cu.PT-E560BT_xxxx-SerialPorton macOS). - Open the web app and click Connect Printer.
- 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(oruucp) 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
dialoutgroup membership is active (groups $USER).- Check whether
ModemManagerhas 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
- Go to Settings โ Pages in the GitHub repository.
- Under Build and deployment / Source, select GitHub Actions (not the legacy "Deploy from a branch" option).
- Save. That's it โ no
gh-pagesbranch needed.
Automated deployments
| Trigger | Action |
|---|---|
Push to main | Build + deploy automatically |
| Manual | Actions โ 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)
| Property | Value |
|---|---|
| Baud Rate | 115200 (required by the API; ignored by USB/BT virtual ports) |
| Chunk Size | 512 bytes |
| Inter-chunk delay | 10 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.
| Width | Printable Dots | Offset (dots) | Bytes / Line |
|---|---|---|---|
| 24 mm | 128 | 0 | 16 |
| 18 mm | 120 | 4 | 16 |
| 12 mm | 76 | 26 | 16 |
| 9 mm | 52 | 38 | 16 |
| 6 mm | 32 | 48 | 16 |
Print Job Pipeline (PT-E560BT / D460BT)
$\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
| Symptom | Likely Cause | Fix |
|---|---|---|
navigator.serial is undefined | Not HTTPS / unsupported browser | Use localhost in dev, deploy to HTTPS; use Chrome/Edge/Opera |
| Port picker is empty | No serial port permission | sudo usermod -a -G dialout $USER, then re-login |
| Port picker is empty (BT, Linux) | rfcomm not bound | sudo rfcomm bind 0 <MAC> first |
| Port picker is empty (BT) | Printer not paired via OS | Pair through OS Bluetooth settings before opening the app |
| Port claimed by ModemManager | ModemManager auto-connects serial devices | sudo systemctl stop ModemManager |
| Print garbled / no output | Wrong tape width selected | Match the tape actually loaded in the printer |
| Write errors during print | Buffer overflow | Reduce CHUNK_SIZE or increase INTER_CHUNK_DELAY_MS in serial.js |
License
MIT โ see LICENSE.