Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ On desktop, **Connect data** also reads an Excel worksheet. **File → Settings

### Printer settings

**File → Settings… → Per Label** configures media, print quality, output and RFID. The **Setup Script** covers clock, encoding, fonts, printer identity and maintenance.
**File → Settings… → Per Label** configures media, print quality, output and RFID. The **Setup Script** covers clock, encoding, fonts, printer identity, maintenance, sensors and interfaces.

- The **Objects** group selects the fonts and graphics the **Setup Script** uploads.
- It lists cached images and deletes the unused ones.
Expand Down Expand Up @@ -188,7 +188,7 @@ On desktop, **File → Settings… → App → MCP** starts a local MCP server,
## Coverage

<!-- coverage:start (generated from the command catalog by scripts/gen-coverage.mjs; run `pnpm coverage:gen`) -->
125 of the 225 ZPL II commands are modelled in the browser; desktop covers 9 more with a connected printer. 2 more are planned for both builds. 67 need a connected printer and are planned for desktop. See per-command coverage: [docs/zpl-coverage.md](docs/zpl-coverage.md).
132 of the 225 ZPL II commands are modelled in the browser; desktop covers 9 more with a connected printer. 2 more are planned for both builds. 60 need a connected printer and are planned for desktop. See per-command coverage: [docs/zpl-coverage.md](docs/zpl-coverage.md).

| Area | Modelled |
|---|---|
Expand All @@ -205,7 +205,7 @@ On desktop, **File → Settings… → App → MCP** starts a local MCP server,
| Text & fonts | 14 / 15 |
| Print quality | 10 / 18 |
| Configuration & persistence | 4 / 5 |
| Hardware / Host comm / RFID / Network | 4 / 89 |
| Hardware / Host comm / RFID / Network | 11 / 89 |
<!-- coverage:end -->

## Limitations
Expand Down
14 changes: 7 additions & 7 deletions docs/zpl-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ Printer-side control, calibration and device actions; most need a connection or
| `~JF` | set battery condition, PA400 and PT400 only | `[ ]` | `[ ]` | `[ ]` |
| `~JG` | graphing sensor calibration | `[ ]` | `[~]` | `[~]` |
| `^JI` / `~JI` | start ZBI | `[ ]` | `[~]` | `[~]` |
| `^JJ` | set auxiliary port | `[ ]` | `[~]` | `[~]` |
| `^JS` | sensor select | `[ ]` | `[~]` | `[~]` |
| `^JJ` | set auxiliary port | `[x]` | `[x]` | `[x]` |
| `^JS` | sensor select | `[x]` | `[x]` | `[x]` |
| `~JL` | set label length | `[ ]` | `[~]` | `[~]` |
| `^JM` | dots per millimeter | `[x]` | `[x]` | `[x]` |
| `~JN` | printhead test fatal | `[ ]` | `[~]` | `[~]` |
Expand All @@ -251,19 +251,19 @@ Printer-side control, calibration and device actions; most need a connection or
| `~JR` | power on reset | `[ ]` | `[~]` | `[~]` |
| `~JX` | cancel partial format | `[ ]` | `[~]` | `[~]` |
| `~RO` | reset advanced counter | `[ ]` | `[~]` | `[~]` |
| `^SC` | set serial communications | `[ ]` | `[~]` | `[~]` |
| `^SI` | set sensor intensity | `[ ]` | `[~]` | `[~]` |
| `^SC` | set serial communications | `[x]` | `[x]` | `[x]` |
| `^SI` | set sensor intensity | `[x]` | `[x]` | `[x]` |
| `^SP` | start print at dot row, throughput hint only | `[ ]` | `[ ]` | `[ ]` |
| `^SQ` | halt ZebraNet alert | `[ ]` | `[~]` | `[~]` |
| `^SR` | set printhead resistance | `[ ]` | `[~]` | `[~]` |
| `^SS` | set media sensors | `[ ]` | `[~]` | `[~]` |
| `^SR` | set printhead resistance | `[x]` | `[x]` | `[x]` |
| `^SS` | set media sensors | `[x]` | `[x]` | `[x]` |
| `^SX` | set ZebraNet alert | `[ ]` | `[~]` | `[~]` |
| `^TO` | transfer object | `[ ]` | `[~]` | `[~]` |
| `~WC` | print configuration label | `[ ]` | `[~]` | `[~]` |
| `^WD` | print directory label | `[ ]` | `[~]` | `[~]` |
| `~WQ` | write query | `[ ]` | `[~]` | `[~]` |
| `^KV` | cutter and presenter settings, KR403 only | `[ ]` | `[ ]` | `[ ]` |
| `^XS` | set dynamic media calibration | `[ ]` | `[~]` | `[~]` |
| `^XS` | set dynamic media calibration | `[x]` | `[x]` | `[x]` |
| `~PM` | decommissioning mode | `[ ]` | `[~]` | `[~]` |
| `^ZZ` | printer sleep, PA400 and PT400 only | `[ ]` | `[ ]` | `[ ]` |
| `^CP` | eject or retract a presented page, KR403 only | `[ ]` | `[ ]` | `[ ]` |
Expand Down
47 changes: 23 additions & 24 deletions packages/core/src/catalog/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -5286,9 +5286,8 @@
"example": "^JJ1,0,p,f,d,e"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand All @@ -5312,9 +5311,8 @@
"example": "^JSA"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand Down Expand Up @@ -5552,9 +5550,8 @@
"example": "^SC9600,8,N,1,X,N"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand Down Expand Up @@ -5584,9 +5581,8 @@
"example": "^SI1,120"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand Down Expand Up @@ -5671,9 +5667,8 @@
"example": "^SR0675"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand Down Expand Up @@ -5745,9 +5740,8 @@
"example": "^SS075,045,070,0812,090,085,060,055,075"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand Down Expand Up @@ -6000,9 +5994,9 @@
],
"section": "Hardware control & calibration",
"title": "set dynamic media calibration",
"summary": "Controls dynamic recalibration of label length and sensor threshold after power-up or media-compartment open.",
"summary": "Controls dynamic recalibration of label length and sensor threshold after power-up or media-compartment open. The gain slot is announced for a later firmware.",
"reference": {
"syntax": "^XSa,b",
"syntax": "^XSa,b,c",
"params": [
{
"name": "a",
Expand All @@ -6015,14 +6009,19 @@
"meaning": "enable dynamic threshold calibration",
"values": "Y enable, N disable",
"default": "Y"
},
{
"name": "c",
"meaning": "enable dynamic gain calibration",
"values": "Y enable, N disable",
"default": "Y"
}
],
"example": "^XSY,Y"
"example": "^XSY,Y,Y"
},
"support": {
"web": "no",
"desktop": "planned",
"docker": "planned"
"web": "yes",
"desktop": "yes"
}
},
{
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/lib/importReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export function resolveRoutedReport(
}
if (f.pageIndex === DOCUMENT_FINDING) return [f];
const newIndex = newIndexOf.get(f.pageIndex);
return newIndex === undefined ? [] : [{ ...f, pageIndex: newIndex }];
// A setup-only page leaves with its block, but what the profile could not take is still lost.
if (newIndex === undefined) return f.kind === 'partial' ? [{ ...f, pageIndex: DOCUMENT_FINDING }] : [];
return [{ ...f, pageIndex: newIndex }];
});
return reportOf(findings);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lib/zplParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const BY_CONSUMING_BARCODE_TYPES = new Set<string>([
* support "no" plus a handler also holds for ^FM, ^HT and ^LF. */
export const DEVICE_ACTION_IDS: ReadonlySet<string> = new Set([
"~JA", "~JC", "~JD", "~JE", "^JI", "~JI", "~JR", "^ID", "^IS", "~EG",
"~PH", "~PP", "~PM", "~PR", "^JS",
"~PH", "~PP", "~PM", "~PR",
]);

/** Commands defining persistent state a later field may consume implicitly;
Expand Down Expand Up @@ -575,7 +575,7 @@ export function parseZPL(
const canonical = `${prefix}${cmd}`;
const takesPrefix = commandTakesPrefix(cmd, prefix);
// The device action quotes its payload: the report must name the bytes that will run.
if (takesPrefix && replayRiskCodes.has(cmd)) replayRisk.push({ command: s.result.tokenCommand, span: s.result.tokenSpan });
if (takesPrefix && (replayRiskCodes.has(cmd) || replayRiskCodes.has(canonical))) replayRisk.push({ command: s.result.tokenCommand, span: s.result.tokenSpan });
else if (takesPrefix && DEVICE_ACTION_IDS.has(canonical)) deviceAction.push({ command: sourceToken, span: s.result.tokenSpan });
// Balance bookkeeping needs the token offset and the state the handler is
// about to flip, so it sits here rather than in the two handlers.
Expand Down
82 changes: 80 additions & 2 deletions packages/core/src/lib/zplParser/handlers/setupScript.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CLOCK_TOLERANCE_RANGE, FONT_CACHE_KB_RANGE, FONT_LINKS_PATH_MAX_LEN, HEAD_CLEANING_INTERVAL_METERS, HEAD_TEST_INTERVAL_RANGE, JH_SLOT_F, JH_SLOT_G, MAINTENANCE_ALERT_DEFAULTS, MAINTENANCE_DISTANCE_MAX_BY_TYPE, MAINTENANCE_MESSAGE_MAX_LEN, PRINTER_NAME_MAX_LEN, PRINTER_PASSWORD_REGEX, TEAR_OFF_ADJUST_RANGE, isClockFormat, isClockLanguage, isConfigUpdateAction, isFontCacheType, isMaintenanceAlertPrint, isMaintenanceAlertType, isMaintenanceAlertUnit, isModeProtection, isPrinterLocale, isZplMode, setupScriptUnsafeCharRegex } from "../../../types/PrinterProfile";
import { AUX_PORT_SLOTS, CLOCK_TOLERANCE_RANGE, FONT_CACHE_KB_RANGE, FONT_LINKS_PATH_MAX_LEN, HEAD_CLEANING_INTERVAL_METERS, HEAD_TEST_INTERVAL_RANGE, JH_SLOT_F, JH_SLOT_G, MAINTENANCE_ALERT_DEFAULTS, MAINTENANCE_DISTANCE_MAX_BY_TYPE, MAINTENANCE_MESSAGE_MAX_LEN, PRINTER_NAME_MAX_LEN, PRINTER_PASSWORD_REGEX, PRINTHEAD_RESISTANCE_RANGE, SENSOR_INTENSITY_RANGE, SENSOR_SLOTS, SI_FIELD_BY_SLOT, TEAR_OFF_ADJUST_RANGE, XS_SLOTS, isClockFormat, isClockLanguage, isConfigUpdateAction, isFontCacheType, isMaintenanceAlertPrint, isMaintenanceAlertType, isMaintenanceAlertUnit, isMediaSensorSelect, isModeProtection, isPrinterLocale, isSerialBaud, isSerialDataBits, isSerialHandshake, isSerialParity, isSerialProtocol, isSerialStopBits, isZplMode, setupScriptUnsafeCharRegex, type AuxPortField, type SensorSlotField } from "../../../types/PrinterProfile";
import { parseIntOrUndef } from "../../inputParse";
import { isYesNo } from "../../../types/typeHelpers";
import { parseRealtimeClock } from "../../realtimeClock";
import type { ParserState } from "../context";
import { notePartial, type ParserState } from "../context";
import { inRange, strParam } from "../helpers";
import type { Handler } from "../types";

Expand Down Expand Up @@ -207,6 +207,84 @@ export function createSetupScriptHandlers(s: ParserState): Record<string, Handle
if (list.length > 0) printerProfile.fontLinks = list;
else delete printerProfile.fontLinks;
},
// Keyed with its prefix: ~JS is the backfeed sequence.
"^JS"(p) {
const v = strParam(p[0]);
if (isMediaSensorSelect(v)) printerProfile.mediaSensorSelect = v;
else notePartial(s.result, "^JS");
},
// The spec says nothing about a bad ^SS slot, so it follows ^SI and ^SC: the command changes nothing and the loss is reported.
SS(p) {
const read = new Map<SensorSlotField, number | undefined>();
for (const [i, [field, range]] of SENSOR_SLOTS.entries()) {
const text = strParam(p[i]);
// An empty slot leaves the calibrated value in force, so it clears the field.
const v = text === "" ? undefined : inRange(parseIntOrUndef(text), range);
if (text !== "" && v === undefined) {
notePartial(s.result, "^SS");
return;
}
read.set(field, v);
}
for (const [field, v] of read) {
if (v === undefined) Reflect.deleteProperty(printerProfile, field);
else printerProfile[field] = v;
}
},
// ^SI names one value and sets it, so an empty slot is a loss and not a clear, spec p.338.
SI(p) {
const field = SI_FIELD_BY_SLOT[strParam(p[0])];
const v = inRange(parseIntOrUndef(p[1]), SENSOR_INTENSITY_RANGE);
if (field === undefined || v === undefined) notePartial(s.result, "^SI");
else printerProfile[field] = v;
},
XS(p) {
// Every slot defaults to Y, spec p.374, so a bare ^XS must not vanish on re-emit.
const bare = p.every((v) => v.trim() === "");
const values = XS_SLOTS.map((_field, i) => (bare ? "Y" : strParam(p[i])));
if (values.some((v) => v !== "" && !isYesNo(v)) || p.slice(XS_SLOTS.length).some((v) => v.trim() !== "")) {
notePartial(s.result, "^XS");
return;
}
XS_SLOTS.forEach((field, i) => {
const v = values[i] ?? "";
if (isYesNo(v)) printerProfile[field] = v;
else Reflect.deleteProperty(printerProfile, field);
});
},
SR(p) {
const text = strParam(p[0]);
if (text === "") delete printerProfile.printheadResistance;
else {
const v = inRange(parseIntOrUndef(text), PRINTHEAD_RESISTANCE_RANGE);
if (v === undefined) notePartial(s.result, "^SR");
else printerProfile.printheadResistance = v;
}
},
SC(p) {
const baud = strParam(p[0]);
const dataBits = strParam(p[1]);
const parity = strParam(p[2]);
const stopBits = strParam(p[3]);
const handshake = strParam(p[4]);
const protocol = strParam(p[5]);
if (!isSerialBaud(baud) || !isSerialDataBits(dataBits) || !isSerialParity(parity) || !isSerialStopBits(stopBits) || !isSerialHandshake(handshake) || !isSerialProtocol(protocol)) {
notePartial(s.result, "^SC");
return;
}
printerProfile.serialPort = { baud, dataBits, parity, stopBits, handshake, protocol };
},
JJ(p) {
// Every slot has a default, so a bare ^JJ must not vanish on re-emit.
const bare = p.every((v) => v.trim() === "");
AUX_PORT_SLOTS.forEach(([field, values, fallback], i) => {
const v = bare ? fallback : (p[i] ?? "").trim().toLowerCase();
Reflect.deleteProperty(printerProfile, field);
if (v === "") return;
if (values.some((allowed) => allowed === v)) (printerProfile as Record<AuxPortField, string>)[field] = v;
else notePartial(s.result, "^JJ");
});
},
// ^JH: only f/g modelled; other slots are runtime reset flags.
// g is a 0..16 index into HEAD_CLEANING_INTERVAL_METERS per spec.
JH(p) {
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/lib/zplParser/handlers/unsupported.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export function createUnsupportedHandlers(s: ParserState): Record<string, Handle
"~PP": noop,
"~PM": noop,
"~PR": noop,
"^JS": noop,
// A delete decides what a later recall resolves.
ID: (p) => deleteStoredObjects(s, p[0] ?? ""),
IS: noop,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/types/PrinterProfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ describe("setupFonts download", () => {
});

describe("repairPrinterProfile", () => {
it("drops the serial port as one key, while an applicator slot costs only itself", () => {
expect(repairPrinterProfile({ serialPort: { baud: "9600", dataBits: "9" }, auxPortMode: "1", auxPortEndPrint: "9" })).toEqual({ auxPortMode: "1" });
expect(repairPrinterProfile({ serialPort: {} })).toEqual({});
});

it("drops the key a cross-field rule names, even when the rule reports the absent side", () => {
expect(repairPrinterProfile({ clockMode: "TOL" })).toEqual({});
expect(repairPrinterProfile({ clockMode: "S", clockTolerance: 30, printerName: "P1" })).toEqual({ clockMode: "S", printerName: "P1" });
Expand Down
Loading
Loading