[{"data":1,"prerenderedAt":850},["ShallowReactive",2],{"post-\u002Fblog\u002F2026\u002Fptouch-webapp-browser-label-printing":3},{"id":4,"title":5,"body":6,"categories":830,"date":834,"description":835,"extension":836,"image":837,"meta":838,"navigation":361,"path":839,"seo":840,"stem":841,"tags":842,"__hash__":849},"blog\u002Fblog\u002F2026\u002Fptouch-webapp-browser-label-printing.md","Ditch the Desktop App – Browser Label Printing for the Brother PT-E560BT",{"type":7,"value":8,"toc":816},"minimark",[9,23,30,33,38,135,142,161,165,172,183,190,194,199,205,220,263,267,281,285,288,294,297,392,403,407,413,416,419,467,471,488,491,499,521,524,589,604,608,615,660,663,692,698,702,705,761,764,768,771,809,812],[10,11,12,13,17,18,22],"p",{},"Every mole eventually labels its tunnels. Patch panel port 14 is \"the server room switch uplink\", not \"mystery cable, do not unplug\" – I learned that distinction the hard way at 2 am. The answer is obviously a label maker. The answer to ",[14,15,16],"em",{},"which"," label maker turned out to be the one the Molewife spotted at the hardware store: a ",[19,20,21],"strong",{},"Brother PT-E560BTVP",", all orange and industrial-looking and promising to survive being sat on by a badger.",[10,24,25],{},[26,27],"img",{"alt":28,"src":29},"Brother PT-E560BT – the orange handheld label printer with QWERTY keyboard","\u002Fimages\u002Fblog\u002F2026\u002F05\u002Fpte560btvp.png",[10,31,32],{},"The hardware is excellent. The official software is the usual desktop-app tax: download P-touch Editor, register an account, wait for the installer, wrestle with the driver, repeat on every machine you use. For a device that lives on the workbench and gets grabbed between soldering sessions, that friction is unacceptable. There had to be a better tunnel.",[34,35,37],"h2",{"id":36},"the-stack-at-a-glance","The Stack at a Glance",[39,40,41,57],"table",{},[42,43,44],"thead",{},[45,46,47,51,54],"tr",{},[48,49,50],"th",{},"Layer",[48,52,53],{},"Choice",[48,55,56],{},"Why",[58,59,60,72,83,94,109,124],"tbody",{},[45,61,62,66,69],{},[63,64,65],"td",{},"Runtime",[63,67,68],{},"Browser (Chrome\u002FEdge)",[63,70,71],{},"Web Serial API, no install",[45,73,74,77,80],{},[63,75,76],{},"Build tool",[63,78,79],{},"Vite 8",[63,81,82],{},"Fast, ESM-native, tiny output",[45,84,85,88,91],{},[63,86,87],{},"Styling",[63,89,90],{},"Tailwind CSS v4",[63,92,93],{},"Dark-mode utility classes, zero config",[45,95,96,99,102],{},[63,97,98],{},"Printer comms",[63,100,101],{},"Web Serial API",[63,103,104,105,108],{},"Works over USB ",[14,106,107],{},"and"," BT Classic SPP",[45,110,111,114,121],{},[63,112,113],{},"QR codes",[63,115,116,120],{},[117,118,119],"code",{},"qrcode"," npm package",[63,122,123],{},"One dependency, client-side only",[45,125,126,129,132],{},[63,127,128],{},"i18n",[63,130,131],{},"Hand-rolled (EN \u002F DE)",[63,133,134],{},"No framework overhead",[10,136,137,138,141],{},"No backend. No Electron wrapper. No driver installation. Open a tab, click ",[19,139,140],{},"Connect Printer",", print a label.",[10,143,144,145,154,155,160],{},"The app lives at ",[19,146,147],{},[148,149,153],"a",{"href":150,"rel":151},"https:\u002F\u002Fthe78mole.github.io\u002Fptouch-webapp\u002F",[152],"nofollow","the78mole.github.io\u002Fptouch-webapp"," and the source is on GitHub at ",[148,156,159],{"href":157,"rel":158},"https:\u002F\u002Fgithub.com\u002Fthe78mole\u002Fptouch-webapp",[152],"the78mole\u002Fptouch-webapp",".",[34,162,164],{"id":163},"why-web-serial-and-not-web-bluetooth","Why Web Serial and Not Web Bluetooth?",[10,166,167,168,171],{},"This is the first question everyone asks. The PT-E560BT has ",[14,169,170],{},"Bluetooth"," right there in the name, so why use a serial API?",[10,173,174,175,178,179,182],{},"The answer is in the protocol stack. Web Bluetooth only supports ",[19,176,177],{},"Bluetooth Low Energy (BLE)",". The PT-E560BT uses ",[19,180,181],{},"Bluetooth Classic with the Serial Port Profile (SPP)"," – a fundamentally different radio protocol aimed at replacing RS-232 cables, not at IoT sensors. The two are not interchangeable at the browser API level.",[10,184,185,186,189],{},"What saves us is that every operating system exposes a paired Bluetooth SPP device as a ",[19,187,188],{},"virtual COM port"," – the same kind of serial port the browser's Web Serial API already knows how to open. So whether the printer is plugged in via USB or connected over Bluetooth, from the browser's perspective it looks identical: a serial port with a baud rate and a byte stream.",[34,191,193],{"id":192},"connecting-the-printer","Connecting the Printer",[195,196,198],"h3",{"id":197},"usb","USB",[10,200,201,202,204],{},"Plug the printer in. Click ",[19,203,140],{},". Select the Brother device in the browser's port picker. The status dot turns green.",[10,206,207,208,211,212,215,216,219],{},"On Linux the port appears as ",[117,209,210],{},"\u002Fdev\u002FttyUSB0"," or ",[117,213,214],{},"\u002Fdev\u002FttyACM0",". If the picker is empty, your user probably isn't in the ",[117,217,218],{},"dialout"," group yet:",[221,222,227],"pre",{"className":223,"code":224,"language":225,"meta":226,"style":226},"language-bash shiki shiki-themes github-dark","sudo usermod -a -G dialout $USER\n# Log out and back in for the change to take effect\n","bash","",[117,228,229,256],{"__ignoreMap":226},[230,231,234,238,242,246,249,252],"span",{"class":232,"line":233},"line",1,[230,235,237],{"class":236},"svObZ","sudo",[230,239,241],{"class":240},"sU2Wk"," usermod",[230,243,245],{"class":244},"sDLfK"," -a",[230,247,248],{"class":244}," -G",[230,250,251],{"class":240}," dialout",[230,253,255],{"class":254},"s95oV"," $USER\n",[230,257,259],{"class":232,"line":258},2,[230,260,262],{"class":261},"sAwPA","# Log out and back in for the change to take effect\n",[195,264,266],{"id":265},"bluetooth-windows-macos","Bluetooth (Windows \u002F macOS)",[10,268,269,270,273,274,277,278,280],{},"Pair the printer once through the OS Bluetooth settings (",[117,271,272],{},"PT-E560BT_xxxx",", PIN ",[117,275,276],{},"0000","). After pairing the OS creates a virtual COM port automatically. Open the app, click ",[19,279,140],{},", pick the COM port.",[195,282,284],{"id":283},"bluetooth-on-linux","Bluetooth on Linux",[10,286,287],{},"Linux doesn't create a virtual serial port for Bluetooth devices automatically.",[10,289,290,291,160],{},"Gnome usually provides a Bluetooth-Manager, where you can pair the printer. After pairing, you need to manually bind the Bluetooth device to a serial port using ",[117,292,293],{},"rfcomm",[10,295,296],{},"If you tend to use the command line, it gets a bit more complicated, but it's still straightforward:",[221,298,300],{"className":223,"code":299,"language":225,"meta":226,"style":226},"# Step 1 – pair via bluetoothctl\nbluetoothctl\npower on\nscan on\npair   94:DD:F8:A1:35:80   # replace with your printer's MAC\ntrust  94:DD:F8:A1:35:80\nquit\n\n# Step 2 – bind to a serial device\nsudo rfcomm bind 0 94:DD:F8:A1:35:80\n# Creates \u002Fdev\u002Frfcomm0\n",[117,301,302,307,312,321,329,341,350,356,363,369,386],{"__ignoreMap":226},[230,303,304],{"class":232,"line":233},[230,305,306],{"class":261},"# Step 1 – pair via bluetoothctl\n",[230,308,309],{"class":232,"line":258},[230,310,311],{"class":236},"bluetoothctl\n",[230,313,315,318],{"class":232,"line":314},3,[230,316,317],{"class":236},"power",[230,319,320],{"class":240}," on\n",[230,322,324,327],{"class":232,"line":323},4,[230,325,326],{"class":236},"scan",[230,328,320],{"class":240},[230,330,332,335,338],{"class":232,"line":331},5,[230,333,334],{"class":236},"pair",[230,336,337],{"class":240},"   94:DD:F8:A1:35:80",[230,339,340],{"class":261},"   # replace with your printer's MAC\n",[230,342,344,347],{"class":232,"line":343},6,[230,345,346],{"class":236},"trust",[230,348,349],{"class":240},"  94:DD:F8:A1:35:80\n",[230,351,353],{"class":232,"line":352},7,[230,354,355],{"class":236},"quit\n",[230,357,359],{"class":232,"line":358},8,[230,360,362],{"emptyLinePlaceholder":361},true,"\n",[230,364,366],{"class":232,"line":365},9,[230,367,368],{"class":261},"# Step 2 – bind to a serial device\n",[230,370,372,374,377,380,383],{"class":232,"line":371},10,[230,373,237],{"class":236},[230,375,376],{"class":240}," rfcomm",[230,378,379],{"class":240}," bind",[230,381,382],{"class":244}," 0",[230,384,385],{"class":240}," 94:DD:F8:A1:35:80\n",[230,387,389],{"class":232,"line":388},11,[230,390,391],{"class":261},"# Creates \u002Fdev\u002Frfcomm0\n",[10,393,394,395,398,399,402],{},"Then connect to ",[117,396,397],{},"\u002Fdev\u002Frfcomm0"," in the port picker. To survive a reboot, add the bind command to ",[117,400,401],{},"\u002Fetc\u002Frc.local"," or a small systemd unit.",[34,404,406],{"id":405},"the-web-app","The Web App",[10,408,409],{},[26,410],{"alt":411,"src":412},"ptouch-webapp running in the browser – dark mode label designer with live preview canvas","\u002Fimages\u002Fblog\u002F2026\u002F05\u002Fptouch-webapp.png",[10,414,415],{},"The UI is intentionally minimal: a text area, font-size slider, bold toggle, tape-width selector, copy count, and a half-cut toggle. A live canvas preview updates on every keystroke so you know exactly what the printer will produce before a single dot of ink (or better heat) hits the tape.",[10,417,418],{},"Notable details:",[420,421,422,433,439,449,455,461],"ul",{},[423,424,425,428,429,432],"li",{},[19,426,427],{},"Multi-width tape support"," – 12, 18, and 24 mm TZe cartridges, each with the correct printable dot count from Brother's own ",[117,430,431],{},"tape_info[]"," table.",[423,434,435,438],{},[19,436,437],{},"QR code mode"," – swap the text label for a scannable QR code; handy for asset tags and network gear.",[423,440,441,444,445,448],{},[19,442,443],{},"Half-cut"," – ",[117,446,447],{},"ESC i K 0x08"," between labels makes tear-off trivial without cutting all the way through the backing.",[423,450,451,454],{},[19,452,453],{},"Chain printing"," – minimises tape waste when printing multiple labels in sequence.",[423,456,457,460],{},[19,458,459],{},"EN \u002F DE i18n"," – the entire UI switches language with one click.",[423,462,463,466],{},[19,464,465],{},"Debug log panel"," – every byte sent to the printer is shown in hex. Invaluable for protocol archaeology.",[34,468,470],{"id":469},"under-the-hood-the-raster-protocol","Under the Hood: The Raster Protocol",[10,472,473,474,479,480,483,484,487],{},"The PT-E560BT speaks Brother's raster command language, reverse-engineered and documented in the open-source ",[148,475,478],{"href":476,"rel":477},"https:\u002F\u002Fgit.familie-radermacher.ch\u002Flinux\u002Fptouch-print.git",[152],"libptouch \u002F ptouch-print"," project. The print head runs at ",[19,481,482],{},"180 DPI","; each raster line is always ",[19,485,486],{},"16 bytes"," (128 dots) regardless of the tape width – narrower tapes are simply centred within those 128 dots.",[10,489,490],{},"The per-copy command sequence looks like this:",[221,492,497],{"className":493,"code":495,"language":496},[494],"language-text","100 × 0x00 + ESC @      → invalidation + soft reset\nESC i a 0x01            → switch to raster mode\nESC i z …               → print information (tape width, line count, D460BT flag)\nESC i d 01 00 4D 00     → D460BT magic (n3 MUST be 0x4D for the E560BT)\nESC i K …               → half-cut flag\nG + len + data …        → raster lines, 16 bytes each\n0x1A                    → eject \u002F finalise\n","text",[117,498,495],{"__ignoreMap":226},[10,500,501,502,505,506,509,510,513,514,517,518,160],{},"The ",[117,503,504],{},"0x4D"," magic byte in the ",[117,507,508],{},"ESC i d"," command is a fun one. Omit it or set it to ",[117,511,512],{},"0x00"," and the printer silently produces nothing. The flag name ",[117,515,516],{},"FLAG_D460BT_MAGIC"," in libptouch says it all: ",[14,519,520],{},"someone figured this out the hard way",[10,522,523],{},"Bit packing matches the libptouch reference implementation – LSB-first, reverse-indexed within each 16-byte line:",[221,525,529],{"className":526,"code":527,"language":528,"meta":226,"style":226},"language-javascript shiki shiki-themes github-dark","rasterLine[(16 - 1) - Math.floor(pixel \u002F 8)] |= 1 \u003C\u003C (pixel % 8);\n","javascript",[117,530,531],{"__ignoreMap":226},[230,532,533,536,539,543,546,549,552,555,558,561,564,567,570,573,575,578,581,584,586],{"class":232,"line":233},[230,534,535],{"class":254},"rasterLine[(",[230,537,538],{"class":244},"16",[230,540,542],{"class":541},"snl16"," -",[230,544,545],{"class":244}," 1",[230,547,548],{"class":254},") ",[230,550,551],{"class":541},"-",[230,553,554],{"class":254}," Math.",[230,556,557],{"class":236},"floor",[230,559,560],{"class":254},"(pixel ",[230,562,563],{"class":541},"\u002F",[230,565,566],{"class":244}," 8",[230,568,569],{"class":254},")] ",[230,571,572],{"class":541},"|=",[230,574,545],{"class":244},[230,576,577],{"class":541}," \u003C\u003C",[230,579,580],{"class":254}," (pixel ",[230,582,583],{"class":541},"%",[230,585,566],{"class":244},[230,587,588],{"class":254},");\n",[10,590,591,592,595,596,599,600,603],{},"The canvas is rasterised column-by-column: ",[117,593,594],{},"canvas.width"," is the label length in dots (number of raster lines), ",[117,597,598],{},"canvas.height"," is the tape dot count. A ",[117,601,602],{},"getImageData()"," call on a black-on-white canvas maps directly to the bit pattern the printer expects.",[34,605,607],{"id":606},"running-it-locally","Running It Locally",[10,609,610,611,614],{},"The dev server runs on ",[117,612,613],{},"localhost",", which satisfies the browser's HTTPS requirement for Web Serial without needing a certificate.",[221,616,618],{"className":223,"code":617,"language":225,"meta":226,"style":226},"git clone https:\u002F\u002Fgithub.com\u002Fthe78mole\u002Fptouch-webapp.git\ncd ptouch-webapp\nnpm install\nnpm run dev     # http:\u002F\u002Flocalhost:5173\n",[117,619,620,631,639,647],{"__ignoreMap":226},[230,621,622,625,628],{"class":232,"line":233},[230,623,624],{"class":236},"git",[230,626,627],{"class":240}," clone",[230,629,630],{"class":240}," https:\u002F\u002Fgithub.com\u002Fthe78mole\u002Fptouch-webapp.git\n",[230,632,633,636],{"class":232,"line":258},[230,634,635],{"class":244},"cd",[230,637,638],{"class":240}," ptouch-webapp\n",[230,640,641,644],{"class":232,"line":314},[230,642,643],{"class":236},"npm",[230,645,646],{"class":240}," install\n",[230,648,649,651,654,657],{"class":232,"line":323},[230,650,643],{"class":236},[230,652,653],{"class":240}," run",[230,655,656],{"class":240}," dev",[230,658,659],{"class":261},"     # http:\u002F\u002Flocalhost:5173\n",[10,661,662],{},"For a production build:",[221,664,666],{"className":223,"code":665,"language":225,"meta":226,"style":226},"npm run build   # outputs to dist\u002F\nnpm run preview # serve the build locally\n",[117,667,668,680],{"__ignoreMap":226},[230,669,670,672,674,677],{"class":232,"line":233},[230,671,643],{"class":236},[230,673,653],{"class":240},[230,675,676],{"class":240}," build",[230,678,679],{"class":261},"   # outputs to dist\u002F\n",[230,681,682,684,686,689],{"class":232,"line":258},[230,683,643],{"class":236},[230,685,653],{"class":240},[230,687,688],{"class":240}," preview",[230,690,691],{"class":261}," # serve the build locally\n",[10,693,501,694,697],{},[117,695,696],{},"dist\u002F"," folder can be hosted on any HTTPS-capable static host – GitHub Pages, Netlify, Cloudflare Pages, whatever you have handy.",[34,699,701],{"id":700},"browser-requirements","Browser Requirements",[10,703,704],{},"One genuine limitation: Web Serial is a Chromium exclusive.",[39,706,707,717],{},[42,708,709],{},[45,710,711,714],{},[48,712,713],{},"Requirement",[48,715,716],{},"Notes",[58,718,719,738,748],{},[45,720,721,728],{},[63,722,723],{},[19,724,725,726],{},"HTTPS or ",[117,727,613],{},[63,729,730,733,734,737],{},[117,731,732],{},"navigator.serial"," is ",[117,735,736],{},"undefined"," on plain HTTP",[45,739,740,745],{},[63,741,742],{},[19,743,744],{},"Chromium 89+",[63,746,747],{},"Chrome, Edge, or Opera; Firefox and Safari do not support Web Serial",[45,749,750,755],{},[63,751,752],{},[19,753,754],{},"User gesture",[63,756,757,760],{},[117,758,759],{},"serial.requestPort()"," must be triggered by a click event",[10,762,763],{},"If you're on Firefox or Safari, the connect button will do nothing. There is no polyfill for this – the browser API simply isn't there.",[34,765,767],{"id":766},"whats-next","What's Next",[10,769,770],{},"The current version covers the basics well: text labels, QR codes, half-cut, chain printing. A few tunnels still to dig:",[420,772,773,779,785,791,797,803],{},[423,774,775,778],{},[19,776,777],{},"Image \u002F logo import"," – render an arbitrary PNG onto the canvas and print it",[423,780,781,784],{},[19,782,783],{},"Label templates"," – save and recall frequently used layouts",[423,786,787,790],{},[19,788,789],{},"PWA \u002F offline mode"," – service-worker cache so the app works without a network connection",[423,792,793,796],{},[19,794,795],{},"More tape series"," – PT-P series printers share most of the protocol; adding device profiles should be straightforward",[423,798,799,802],{},[19,800,801],{},"Templates"," As an underground electrician, I usually need the same labels all over the day, some to stick to wires, some to stick to patch panels. A template system would let me save those layouts and recall them with one click.",[423,804,805,808],{},[19,806,807],{},"Database integration"," – pull asset info from a local database or API and generate labels on the fly.",[10,810,811],{},"The code is MIT-licensed and the protocol implementation is well-commented. If you own a compatible PT-E or PT-P printer and want to take a spade to one of those tunnels, pull requests are welcome.",[813,814,815],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}",{"title":226,"searchDepth":258,"depth":258,"links":817},[818,819,820,825,826,827,828,829],{"id":36,"depth":258,"text":37},{"id":163,"depth":258,"text":164},{"id":192,"depth":258,"text":193,"children":821},[822,823,824],{"id":197,"depth":314,"text":198},{"id":265,"depth":314,"text":266},{"id":283,"depth":314,"text":284},{"id":405,"depth":258,"text":406},{"id":469,"depth":258,"text":470},{"id":606,"depth":258,"text":607},{"id":700,"depth":258,"text":701},{"id":766,"depth":258,"text":767},[831,832,833],"Dev","Tools","DIY","2026-05-14","A driver-free, backend-free SPA that prints to a Brother PT-E560BT over USB or Bluetooth Classic using the Web Serial API – because installing P-touch Editor in the burrow is a hard no.","md","\u002Fimages\u002Fblog\u002F2026\u002F05\u002Fmole-with-p-touch.png",{},"\u002Fblog\u002F2026\u002Fptouch-webapp-browser-label-printing",{"title":5,"description":835},"blog\u002F2026\u002Fptouch-webapp-browser-label-printing",[843,844,845,846,847,848,528],"brother","p-touch","label printer","web serial","bluetooth","vite","gcBJQD6x3I_HltICH-8hS94ytfbjljUv9sj51sI22QU",1778790787853]