Printers and serial.
The player finds printers and serial ports and moves bytes. What the bytes say is yours: a receipt printer is sent ESC/POS, a label printer ZPL or TSPL, exactly as you built them.
Finding a printer
Arda.printers.list().then(function (list) {
// [{id, make, model, proto, conn, addr, dpi, widthMM, dots, usable, error}]
var receipt = list.filter(function (p) { return p.usable && p.proto === 'escpos'; })[0];
if (!receipt) return say('No receipt printer: ' + list.map(function (p) { return p.error; }).join('; '));
printReceipt(receipt);
});
proto:escpos,zpl,star,tspl,escp(Epson ESC/P: an impact or dot-matrix printer such as an LX-350 — text and ESC/P codes, no cutter, no receipt raster; from v1.2.2) orraw.rawmeans the player could not tell: never send it ESC/POS on a guess. An operator can set the language on the portal's Peripherals tab.connis how it is reached:usb,net(raw TCP 9100;addris itshost:port) or Bluetooth. All print the same way. A network printer is one the device was told about — on the portal's Peripherals tab, or by the app: below.- Which printer, per device. A
selectsetting with"options": {"device": "printers"}lists the printers each device reports, in the portal and to the app; its value is the printer'sid. From v1.2.1: see Settings. - Check
usable. A printer that can be seen but not written to is worse than none.errorsays why. dotsis the printable width for a raster image — use it, notwidthMM × dpi(80 mm at 203 dpi computes to 639; the real width is 576). On a receipt printer it is derived from the paper size and can be wrong; an operator can correct it on the Peripherals tab. A wrong width does not fail, it wraps.
Telling the player what a printer is from ArdaPlayer v1.5.1
Arda.printers.set(id, {name, columns, dots, proto}) is the operator’s override of a printer from the app — what it is called, its printable width in characters (count them off a test print; dots for somebody who knows the head) and its language (escpos, star, zpl, tspl, escp, raw) — written to the same record the device’s Peripherals tab writes, so the portal and the app never disagree. Send only the fields you mean: a field left out keeps what is held, '' or 0 clears it back to what the player detected, a value sets it — so set(id, {proto: 'star'}) changes only the language and set(id, {columns: 0}) puts the detected width back. It resolves with the printer as list() shows it afterwards.
From the same version every list() entry carries the values in force (name, proto, dots, columns — the override folded in, as the Peripherals tab shows) and, beside them, detected: {columns, dots, proto}, what the player worked out on its own, and override: {name, columns, dots, proto} while one is held — enough for an edit window to say “Auto (48)” and to put the detected value back. Not darkness, not the drawer pin: those are a job’s own bytes. And print() never translates: proto is what the player believes the printer speaks, for you to read — the bytes go through untouched.
Printing
// ESC/POS: text is a single-byte code page, not UTF-8.
var CP437 = { '£': 0x9c, 'é': 0x82, '°': 0xf8 };
function text(s) {
// strip control bytes from anything that came from data: ESC (0x1b) and GS (0x1d)
// inside a product name would be printer commands
s = String(s).replace(/[\u0000-\u001f\u007f]/g, ' ');
var out = [];
for (var i = 0; i < s.length; i++) {
var c = s.charCodeAt(i);
out.push(c < 0x80 ? c : (CP437[s[i]] || 0x3f)); // '?' for anything unmapped
}
return out;
}
function receiptBytes(lines) {
var b = [0x1b, 0x40, 0x1b, 0x74, 0x00]; // ESC @ reset, ESC t 0 = code page 437
lines.forEach(function (l) { b = b.concat(text(l), [0x0a]); });
b = b.concat([0x1b, 0x64, 0x04, 0x1d, 0x56, 0x42, 0x00]); // feed 4 lines, then cut
return new Uint8Array(b);
}
function printReceipt(p) {
return Arda.printers.print(p.id, receiptBytes(['Tea £2.50', 'Total £2.50']))
.then(function () { say('Printed'); }, function (e) { say('Printer: ' + e.message); });
}
print(id, bytes)takes aUint8ArrayorArrayBufferand resolves when the bytes are written.- A
£sent as UTF-8 prints as two wrong characters and shifts every column of its row. - Feed before cutting, or the cutter slices the last line.
The ESC/POS byte values are the common Epson ones; printers differ. Not verified against a device here.
Copies and a status per copy
Since v1.1.11 (feature printStatus), print also takes options and a status callback:
var stop = new AbortController();
Arda.printers.print(printer.id, labelBytes, { copies: 24, signal: stop.signal }, function (s) {
// s = {state, done, copies, confirmed, code, error}
if (s.state === 'printing') say(s.done + ' of ' + s.copies);
if (s.state === 'queued') say('Waiting for the printer');
if (s.state === 'stalled') say('Printer stopped: ' + (s.code || s.error)); // not the end
}).then(function (last) {
say('Done: ' + last.copies + ' labels');
}, function (err) {
say(err.status && err.status.state === 'cancelled' ? 'Cancelled' : 'Failed: ' + err.message);
});
cancelButton.onclick = function () { stop.abort(); }; // ends the job between copies
state | Meaning |
|---|---|
printing | copy done of copies has gone |
queued | another job has this printer; this one waits |
stalled | the printer stopped taking data or reports a fault (code: paper_out, cover_open, paper_jam, ribbon_out, paused, or stalled). Not final: the job carries on when the printer does. |
done | final: the promise resolves |
error, cancelled | final: the promise rejects; err.status is the last status (code unreachable or write_failed on error) |
- The bytes are sent
copiestimes (1–999) over one connection. Either argument may be left out:print(id, bytes, onStatus)is one copy with a status. confirmed: falsemeans the printer took the copy, not that it came out. Say “sent to printer” unless it is true, and show something from the call until the promise settles: a Bluetooth print takes a second or three to dial before the first byte goes, and a blank screen meanwhile looks stuck. Most printers report nothing back; some take a whole job into memory with the cover open and print it when it closes. If the screen must follow the paper, time it yourself (label length ÷ print speed).confirmed: trueappears only when the printer itself said so.- Cancelling — the signal, or the page going away — never cuts a copy in half (half a command in a printer garbles the next job). The copy in flight finishes, then the job ends
cancelled. - A network printer that takes nothing for 30 s ends the job
error; USB and Bluetooth jobs wait for the printer. - On an older player, test
featuresforprintStatusand fall back toprint(id, bytes)in a loop.
Printers on the network from ArdaPlayer v1.3.0
A Wi-Fi or Ethernet receipt printer is not plugged into the device, so the device has to be told about it once. The portal's Peripherals tab does that (Find printers on the network, or an address typed in). An app can do it too, for a setup screen of its own:
Arda.printers.discover().then(function (found) {
// [{host, port, name, make, model, mac, known}] — known: already added on this device
showChoices(found.map(function (f) {
return { label: (f.name || 'Network printer') + ' ' + f.host + ':' + f.port, found: f };
}));
});
function chosen(f) {
return Arda.printers.add({ host: f.host, port: f.port, name: f.name })
.then(function (p) {
// p is the printer as list() shows it: {id: 'net:192.168.0.69:9100', conn: 'net', addr, proto, usable, error, ...}
if (!p.usable) return say('Added, but not answering: ' + p.error);
return Arda.printers.print(p.id, receiptBytes(['ArdaForm test'])); // printing is unchanged
});
}
Arda.printers.remove('net:192.168.0.69:9100').then(function (r) { // r = {ok} });
discover()→[{host, port, name, make, model, mac, known}].name,make,modelandmacare what the printer said about itself and may be empty. From v1.5.1 far more printers say it: the device asks mDNS two ways (office printers such as a Canon or an Epson answer only a question sent from port 5353), asks a printer that advertises nothing for its model over SNMP, and takes its web page’s title last;knownmeans it is already on this device. Finding a printer does not add it.add({host, port, name, proto})→ the printer aslist()shows it, withidnet:host:port,conn: 'net'andaddr.portis 9100 unless you give one;nameandprotoare optional. Adding an address a second time returns the printer already there.usable: falsewith anerrormeans it was kept but is not answering.remove(id)→{ok}. The name, width and protocol set for it on the Peripherals tab go with it.- Printing is unchanged:
print(id, bytes, …)with thenet:id, copies and status included. A network printer that takes nothing for 30 s ends the joberror.
The rules. The device scans only when asked, never by itself, and only its own network: every address on it is asked once whether it takes print jobs on port 9100, which takes three to five seconds, and it does this at most once every 20 seconds — a second call inside that returns the last result. Anything answering on 9100 appears, so an office printer may appear beside the receipt printer: show the person what was found and let them choose rather than adding everything. The proto of a found printer is a labelled guess from what it said about itself; the Peripherals tab can correct it, and so can add's proto. On an older player none of the three calls exists: test features for netPrinters.
Bluetooth printers from ArdaPlayer v1.4.1
A receipt printer with a Bluetooth module — a Star TSP650 with its Bluetooth interface, a battery printer — is printed to over its serial port (RFCOMM), once the device is paired with it. Pairing is a one-time trust decision, and the player makes it the way a person would: one named printer, at your request, with its PIN if it has one. On the device’s Peripherals tab: Find Bluetooth printers listens for eight seconds and lists everything the radio heard, printers and serial-port devices first; Pair & add on the one you mean. Put the printer in pairing mode first — on the Star that is the PAIR button on the back held for about five seconds until its light flashes green (a short press does nothing, and a printer not in pairing mode refuses every attempt). It is then kept on the device as bt:address and appears in the Printers list, usable while the pairing holds.
discoverBluetooth()→[{address, name, class, printer, serial, paired, known, rssi, make, proto}].makeandproto(from v1.5.1) are the maker and the language the player read from the printer’s own module where it publishes one — a Star’s says'Star Micronics','star'— on a Linux player; Android shows none, so there the name decides andset(id, {proto})corrects it.addBluetooth()records that language unless you passproto.printersays the device announces itself as one,serialthat it offers the serial port a printer is printed over,pairedthat this device already holds a pairing,knownthat it is already added. Only when you ask, never on a timer.addBluetooth({address, pin, name, proto, channel})→ the printer aslist()shows it,idbt:address,conn: 'bt'. The call pairs first (allow up to a minute and a half) — somebody has pressed the printer’s pairing button — then trusts and remembers it.pinonly when the printer wants one (a Star’s is 1234 when it does);protois guessed from the name when you do not say (a name with TSP or Star means Star Line Mode) — a printer you have renamed needsproto: 'star'said, or the pencil on the Peripherals tab;channelis the serial port’s, 1 unless you know better.- Send a document as one
print()call. Every call is its own dial of the printer’s serial port — a second or three on a Star after an idle — while a job’s bytes go over one open connection whatever their size. A receipt split into eight writes (a USB habit) takes most of ten seconds over Bluetooth; in one write, about one. The same holds for a network printer, where every call is a connect. remove(id)works for abt:id too. The pairing itself stays on the device (the Bluetooth pane can forget it), so a printer removed by mistake is added again without the walk to its button.- Printing is unchanged:
print(id, bytes, …)with thebt:id. A connect the module refuses in its first moment is tried again twice, a moment apart; a printer out of range ends the job with an error.featuressaysbtPrinterswhere this works — Linux players (a Pi, an x86 till) from v1.4.1, and from v1.5.1 an Android tablet too, once Bluetooth is switched on in its settings (a tablet set up as device owner gives the player the Bluetooth permissions itself; an ordinary install asks the first time). Not the developer binaries.
A Star prints Star Line Mode
A Star Micronics receipt printer (proto: 'star') speaks its own language, not ESC/POS: ESC d 3 cuts (GS V does nothing, and ESC d n is a cut there, not a feed — feed with ESC a n), ESC i n1 n2 sizes, ESC GS a n aligns, ESC GS t 1 picks code page 437. Proved on a TSP650’s paper, 2026-10-29:
- The logo is
ESC X n1 n2 d…— the same 24-dot column stripes you build for ESC/POSESC * 33(three bytes a column, top byte first, the top dot the high bit, one dot per dot,nthe number of columns, up to 576), sent underESC 0(a 3 mm line, 24 dots, so the stripes join) withESC z 1(4 mm) after. NotESC k: on a Star that takes rows, and a count the printer reads as wider than the paper swallows the rest of the job as picture data and leaves the printer waiting for more. - A barcode is
ESC b 6 2 1 h … RS: Code128, the digits printed under it, 2-dot modules,hthe height in dots, the data plain ASCII between the parameters andRS(0x1E) — the printer picks the code sets itself.
Serial ports: Arda.serial
// A scale on a USB-serial adapter. The protocol here is an example; yours is in the device's manual.
var port;
Arda.serial.list().then(function (ports) {
// [{id, path, kind, vendor, product, serial, vid, pid, driver, usable, error}]
port = ports.filter(function (p) { return p.usable; })[0];
if (!port) throw new Error('no serial port');
return Arda.serial.open(port.id, { baud: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
}).then(function () {
return Arda.serial.write(port.id, new Uint8Array([0x57, 0x0d])); // "W\r"
}).then(function () {
return Arda.serial.read(port.id, { max: 64, timeoutMs: 1500 });
}).then(function (reply) {
if (!reply.length) return say('The scale did not answer'); // silence is an answer
say('Weight: ' + new TextDecoder().decode(reply).trim());
}).catch(function (e) { say('Scale: ' + e.message); });
- Use
id(a stable name), neverpath:ttyUSB0andttyUSB1swap places depending on which adapter was plugged in first. opendefaults to 9600 8N1. The port is raw: no echo, no line editing, no CR/LF translation; binary passes byte for byte.readresolves aUint8Array, empty if nothing came in time — not an error.close(id)when you are done.
Checked: call names, options and status fields are from ArdaJS() in the player source; printers.list() answering an empty list on a machine with no printer was observed. The calls are shaped like those of an app printing on a device today. The printed output and the serial exchange were not verified against a device here. The network calls were run against a Wi-Fi receipt printer: found by the sweep, added, and printed to.