diff --git a/README.md b/README.md index f8f3abe2..467e7042 100644 --- a/README.md +++ b/README.md @@ -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, maintenance, sensors and interfaces. +**File → Settings… → Per Label** configures media, print quality, output and RFID. The **Setup Script** covers clock, encoding, fonts, printer identity and maintenance. - The **Objects** group selects the fonts and graphics the **Setup Script** uploads. - It lists cached images and deletes the unused ones. @@ -188,7 +188,7 @@ On desktop, **File → Settings… → App → MCP** starts a local MCP server, ## Coverage -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). +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). | Area | Modelled | |---|---| @@ -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 | 11 / 89 | +| Hardware / Host comm / RFID / Network | 4 / 89 | ## Limitations diff --git a/docs/zpl-coverage.md b/docs/zpl-coverage.md index 7a259ea8..723ffdd8 100644 --- a/docs/zpl-coverage.md +++ b/docs/zpl-coverage.md @@ -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 | `[x]` | `[x]` | `[x]` | -| `^JS` | sensor select | `[x]` | `[x]` | `[x]` | +| `^JJ` | set auxiliary port | `[ ]` | `[~]` | `[~]` | +| `^JS` | sensor select | `[ ]` | `[~]` | `[~]` | | `~JL` | set label length | `[ ]` | `[~]` | `[~]` | | `^JM` | dots per millimeter | `[x]` | `[x]` | `[x]` | | `~JN` | printhead test fatal | `[ ]` | `[~]` | `[~]` | @@ -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 | `[x]` | `[x]` | `[x]` | -| `^SI` | set sensor intensity | `[x]` | `[x]` | `[x]` | +| `^SC` | set serial communications | `[ ]` | `[~]` | `[~]` | +| `^SI` | set sensor intensity | `[ ]` | `[~]` | `[~]` | | `^SP` | start print at dot row, throughput hint only | `[ ]` | `[ ]` | `[ ]` | | `^SQ` | halt ZebraNet alert | `[ ]` | `[~]` | `[~]` | -| `^SR` | set printhead resistance | `[x]` | `[x]` | `[x]` | -| `^SS` | set media sensors | `[x]` | `[x]` | `[x]` | +| `^SR` | set printhead resistance | `[ ]` | `[~]` | `[~]` | +| `^SS` | set media sensors | `[ ]` | `[~]` | `[~]` | | `^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 | `[x]` | `[x]` | `[x]` | +| `^XS` | set dynamic media calibration | `[ ]` | `[~]` | `[~]` | | `~PM` | decommissioning mode | `[ ]` | `[~]` | `[~]` | | `^ZZ` | printer sleep, PA400 and PT400 only | `[ ]` | `[ ]` | `[ ]` | | `^CP` | eject or retract a presented page, KR403 only | `[ ]` | `[ ]` | `[ ]` | diff --git a/packages/core/src/catalog/commands.json b/packages/core/src/catalog/commands.json index cfe9c294..a722b796 100644 --- a/packages/core/src/catalog/commands.json +++ b/packages/core/src/catalog/commands.json @@ -5286,8 +5286,9 @@ "example": "^JJ1,0,p,f,d,e" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { @@ -5311,8 +5312,9 @@ "example": "^JSA" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { @@ -5550,8 +5552,9 @@ "example": "^SC9600,8,N,1,X,N" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { @@ -5581,8 +5584,9 @@ "example": "^SI1,120" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { @@ -5667,8 +5671,9 @@ "example": "^SR0675" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { @@ -5740,8 +5745,9 @@ "example": "^SS075,045,070,0812,090,085,060,055,075" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { @@ -5994,9 +6000,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. The gain slot is announced for a later firmware.", + "summary": "Controls dynamic recalibration of label length and sensor threshold after power-up or media-compartment open.", "reference": { - "syntax": "^XSa,b,c", + "syntax": "^XSa,b", "params": [ { "name": "a", @@ -6009,19 +6015,14 @@ "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,Y" + "example": "^XSY,Y" }, "support": { - "web": "yes", - "desktop": "yes" + "web": "no", + "desktop": "planned", + "docker": "planned" } }, { diff --git a/packages/core/src/lib/importReport.ts b/packages/core/src/lib/importReport.ts index 84239056..4f0be4d1 100644 --- a/packages/core/src/lib/importReport.ts +++ b/packages/core/src/lib/importReport.ts @@ -50,9 +50,7 @@ export function resolveRoutedReport( } if (f.pageIndex === DOCUMENT_FINDING) return [f]; const newIndex = newIndexOf.get(f.pageIndex); - // 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 newIndex === undefined ? [] : [{ ...f, pageIndex: newIndex }]; }); return reportOf(findings); } diff --git a/packages/core/src/lib/zplParser.ts b/packages/core/src/lib/zplParser.ts index cc1cfb3e..9bd2e603 100644 --- a/packages/core/src/lib/zplParser.ts +++ b/packages/core/src/lib/zplParser.ts @@ -161,7 +161,7 @@ export const BY_CONSUMING_BARCODE_TYPES = new Set([ * support "no" plus a handler also holds for ^FM, ^HT and ^LF. */ export const DEVICE_ACTION_IDS: ReadonlySet = new Set([ "~JA", "~JC", "~JD", "~JE", "^JI", "~JI", "~JR", "^ID", "^IS", "~EG", - "~PH", "~PP", "~PM", "~PR", + "~PH", "~PP", "~PM", "~PR", "^JS", ]); /** Commands defining persistent state a later field may consume implicitly; @@ -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) || replayRiskCodes.has(canonical))) replayRisk.push({ command: s.result.tokenCommand, span: s.result.tokenSpan }); + if (takesPrefix && replayRiskCodes.has(cmd)) 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. diff --git a/packages/core/src/lib/zplParser/handlers/setupScript.ts b/packages/core/src/lib/zplParser/handlers/setupScript.ts index 9588711f..6e22dc3d 100644 --- a/packages/core/src/lib/zplParser/handlers/setupScript.ts +++ b/packages/core/src/lib/zplParser/handlers/setupScript.ts @@ -1,8 +1,8 @@ -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 { 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 { parseIntOrUndef } from "../../inputParse"; import { isYesNo } from "../../../types/typeHelpers"; import { parseRealtimeClock } from "../../realtimeClock"; -import { notePartial, type ParserState } from "../context"; +import type { ParserState } from "../context"; import { inRange, strParam } from "../helpers"; import type { Handler } from "../types"; @@ -207,84 +207,6 @@ export function createSetupScriptHandlers(s: ParserState): Record 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(); - 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)[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) { diff --git a/packages/core/src/lib/zplParser/handlers/unsupported.ts b/packages/core/src/lib/zplParser/handlers/unsupported.ts index bc697d11..c9697df2 100644 --- a/packages/core/src/lib/zplParser/handlers/unsupported.ts +++ b/packages/core/src/lib/zplParser/handlers/unsupported.ts @@ -18,6 +18,7 @@ export function createUnsupportedHandlers(s: ParserState): Record deleteStoredObjects(s, p[0] ?? ""), IS: noop, diff --git a/packages/core/src/types/PrinterProfile.test.ts b/packages/core/src/types/PrinterProfile.test.ts index b4ae8eb5..6b1ffaee 100644 --- a/packages/core/src/types/PrinterProfile.test.ts +++ b/packages/core/src/types/PrinterProfile.test.ts @@ -33,11 +33,6 @@ 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" }); diff --git a/packages/core/src/types/PrinterProfile.ts b/packages/core/src/types/PrinterProfile.ts index 3fec97bc..8bdab190 100644 --- a/packages/core/src/types/PrinterProfile.ts +++ b/packages/core/src/types/PrinterProfile.ts @@ -81,72 +81,6 @@ export function toggleModeProtection( } export const TEAR_OFF_ADJUST_RANGE = { min: -120, max: 120 } as const; -/** ^JS: A auto, R reflective, T transmissive. */ -export const MEDIA_SENSOR_SELECT_VALUES = ['A', 'R', 'T'] as const; -export type MediaSensorSelect = (typeof MEDIA_SENSOR_SELECT_VALUES)[number]; -export const isMediaSensorSelect = makeEnumGuard(MEDIA_SENSOR_SELECT_VALUES); -/** ^SS w, m, r, a, b, spec p.349. */ -const SENSOR_THRESHOLD_RANGE = { min: 0, max: 100 } as const; -/** ^SS n, o, c. */ -const SENSOR_LED_RANGE = { min: 0, max: 255 } as const; -/** ^SS l. */ -const SENSOR_LABEL_LENGTH_RANGE = { min: 1, max: 32000 } as const; -/** ^SI b, spec p.338. */ -export const SENSOR_INTENSITY_RANGE = { min: 0, max: 196 } as const; -/** ^XS a, b, c in wire order, spec p.374. */ -export const XS_SLOTS = ['dynamicCalibrationLength', 'dynamicCalibrationThreshold', 'dynamicCalibrationGain'] as const; -/** ^SR, spec p.348. */ -export const PRINTHEAD_RESISTANCE_RANGE = { min: 488, max: 1175 } as const; - -/** ^SC slots, spec p.332. */ -export const SERIAL_BAUD_VALUES = ['110', '300', '600', '1200', '2400', '4800', '9600', '14400', '19200', '28800', '38400', '57600', '115200'] as const; -export const isSerialBaud = makeEnumGuard(SERIAL_BAUD_VALUES); -export const SERIAL_DATA_BITS_VALUES = ['7', '8'] as const; -export const isSerialDataBits = makeEnumGuard(SERIAL_DATA_BITS_VALUES); -export const SERIAL_PARITY_VALUES = ['N', 'E', 'O'] as const; -export const isSerialParity = makeEnumGuard(SERIAL_PARITY_VALUES); -export const SERIAL_STOP_BITS_VALUES = ['1', '2'] as const; -export const isSerialStopBits = makeEnumGuard(SERIAL_STOP_BITS_VALUES); -export const SERIAL_HANDSHAKE_VALUES = ['X', 'D', 'R', 'M'] as const; -export const isSerialHandshake = makeEnumGuard(SERIAL_HANDSHAKE_VALUES); -export const SERIAL_PROTOCOL_VALUES = ['A', 'N', 'Z'] as const; -export const isSerialProtocol = makeEnumGuard(SERIAL_PROTOCOL_VALUES); -export const SERIAL_PORT_FIELDS = ['baud', 'dataBits', 'parity', 'stopBits', 'handshake', 'protocol'] as const; - -export const AUX_PORT_MODE_VALUES = ['0', '1', '2'] as const; -export const AUX_PORT_END_PRINT_VALUES = ['0', '1', '2', '3', '4'] as const; -export const AUX_PORT_START_PRINT_VALUES = ['p', 'l'] as const; -export const AUX_PORT_ERROR_MODE_VALUES = ['e', 'f'] as const; -export const AUX_PORT_SWITCH_VALUES = ['e', 'd'] as const; -/** ^JJ slots in wire order with their values and the printer default, spec p.266. - * The guide prints 0 as the start print default, the SGD device.applicator.start_print_mode says pulse, p.682. */ -export const AUX_PORT_SLOTS = [ - ['auxPortMode', AUX_PORT_MODE_VALUES, '0'], - ['auxPortEndPrint', AUX_PORT_END_PRINT_VALUES, '0'], - ['auxPortStartPrint', AUX_PORT_START_PRINT_VALUES, 'p'], - ['auxPortErrorMode', AUX_PORT_ERROR_MODE_VALUES, 'f'], - ['auxPortReprint', AUX_PORT_SWITCH_VALUES, 'd'], - ['auxPortRibbonLow', AUX_PORT_SWITCH_VALUES, 'e'], -] as const satisfies readonly (readonly [keyof PrinterProfile, readonly string[], string])[]; -export type AuxPortField = (typeof AUX_PORT_SLOTS)[number][0]; - -/** ^SS slots in wire order with their range and digit width, spec p.349. */ -export const SENSOR_SLOTS = [ - ['sensorWebThreshold', SENSOR_THRESHOLD_RANGE, 3], - ['sensorMediaThreshold', SENSOR_THRESHOLD_RANGE, 3], - ['sensorRibbonThreshold', SENSOR_THRESHOLD_RANGE, 3], - ['sensorLabelLengthDots', SENSOR_LABEL_LENGTH_RANGE, 4], - ['sensorMediaLedIntensity', SENSOR_LED_RANGE, 3], - ['sensorRibbonLedIntensity', SENSOR_LED_RANGE, 3], - ['sensorMarkThreshold', SENSOR_THRESHOLD_RANGE, 3], - ['sensorMarkMediaThreshold', SENSOR_THRESHOLD_RANGE, 3], - ['sensorMarkLedIntensity', SENSOR_LED_RANGE, 3], -] as const satisfies readonly (readonly [keyof PrinterProfile, { min: number; max: number }, number])[]; -export type SensorSlotField = (typeof SENSOR_SLOTS)[number][0]; -export const SENSOR_SLOT_RANGE = Object.fromEntries(SENSOR_SLOTS.map(([field, range]) => [field, range])) as Record; -/** ^SI a picks the value the b slot sets, spec p.338. */ -export const SI_FIELD_BY_SLOT: Readonly> = { '1': 'sensorBrightness', '2': 'sensorBaseline' }; - /** ^KP 4-digit; '0000' disables LCD-setup protection. */ export const PRINTER_PASSWORD_REGEX = /^\d{4}$/; @@ -286,44 +220,6 @@ export const printerProfileSchema = z.object({ path: z.string().min(1).max(FONT_LINKS_PATH_MAX_LEN).regex(setupScriptSafeStringRegex), gfa: z.string().min(1).max(SETUP_UPLOAD_MAX_CHARS), })).optional(), - /** ^JS media sensor. */ - mediaSensorSelect: z.enum(MEDIA_SENSOR_SELECT_VALUES).optional(), - /** ^SS slots, each on its own so one bad value drops one slot. */ - sensorWebThreshold: intInRange(SENSOR_THRESHOLD_RANGE).optional(), - sensorMediaThreshold: intInRange(SENSOR_THRESHOLD_RANGE).optional(), - sensorRibbonThreshold: intInRange(SENSOR_THRESHOLD_RANGE).optional(), - /** The calibrated length, not the design's ^LL. */ - sensorLabelLengthDots: intInRange(SENSOR_LABEL_LENGTH_RANGE).optional(), - sensorMediaLedIntensity: intInRange(SENSOR_LED_RANGE).optional(), - sensorRibbonLedIntensity: intInRange(SENSOR_LED_RANGE).optional(), - sensorMarkThreshold: intInRange(SENSOR_THRESHOLD_RANGE).optional(), - sensorMarkMediaThreshold: intInRange(SENSOR_THRESHOLD_RANGE).optional(), - sensorMarkLedIntensity: intInRange(SENSOR_LED_RANGE).optional(), - /** ^SI1 and ^SI2, the transmissive sensor's brightness and baseline. */ - sensorBrightness: intInRange(SENSOR_INTENSITY_RANGE).optional(), - sensorBaseline: intInRange(SENSOR_INTENSITY_RANGE).optional(), - /** ^XS length, threshold and gain slots. */ - dynamicCalibrationLength: z.enum(YES_NO_VALUES).optional(), - dynamicCalibrationThreshold: z.enum(YES_NO_VALUES).optional(), - dynamicCalibrationGain: z.enum(YES_NO_VALUES).optional(), - /** ^SR, a value above the head's rating damages it. */ - printheadResistance: intInRange(PRINTHEAD_RESISTANCE_RANGE).optional(), - /** ^SC. The spec gives no slot a default, so the wire carries all six or none, p.332. */ - serialPort: z.object({ - baud: z.enum(SERIAL_BAUD_VALUES).optional(), - dataBits: z.enum(SERIAL_DATA_BITS_VALUES).optional(), - parity: z.enum(SERIAL_PARITY_VALUES).optional(), - stopBits: z.enum(SERIAL_STOP_BITS_VALUES).optional(), - handshake: z.enum(SERIAL_HANDSHAKE_VALUES).optional(), - protocol: z.enum(SERIAL_PROTOCOL_VALUES).optional(), - }).refine((o) => SERIAL_PORT_FIELDS.some((f) => o[f] !== undefined), 'an empty serial port carries nothing').optional(), - /** ^JJ slots, each with its own printer default. */ - auxPortMode: z.enum(AUX_PORT_MODE_VALUES).optional(), - auxPortEndPrint: z.enum(AUX_PORT_END_PRINT_VALUES).optional(), - auxPortStartPrint: z.enum(AUX_PORT_START_PRINT_VALUES).optional(), - auxPortErrorMode: z.enum(AUX_PORT_ERROR_MODE_VALUES).optional(), - auxPortReprint: z.enum(AUX_PORT_SWITCH_VALUES).optional(), - auxPortRibbonLow: z.enum(AUX_PORT_SWITCH_VALUES).optional(), /** ^JH f master gate; without `E` ^MA sits dormant. */ earlyWarningMaintenance: z.enum(['E', 'D']).optional(), /** ^JH g; stored as meters for UX, wire is index into table. */ @@ -417,7 +313,7 @@ export const isSetupPath = (path: string): boolean => path.length <= FONT_LINKS_ /** Fields with their own Objects group: a source edit never strips them and Clear keeps them. */ export const SETUP_UPLOAD_FIELDS = ["setupFonts", "setupGraphics"] as const; -/** Fields whose value differs, by value since some fields hold objects. */ +/** Fields whose value differs. By value: four fields hold objects. */ export function changedProfileFields(before: PrinterProfile, after: PrinterProfile): PrinterProfileField[] { return PRINTER_PROFILE_FIELDS.filter((field) => JSON.stringify(after[field]) !== JSON.stringify(before[field])); } diff --git a/src/components/PrinterSettings/InterfacesTab.test.tsx b/src/components/PrinterSettings/InterfacesTab.test.tsx deleted file mode 100644 index 227c5827..00000000 --- a/src/components/PrinterSettings/InterfacesTab.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// @vitest-environment jsdom -import { describe, it, expect, afterEach, beforeEach } from "vitest"; -import { render, cleanup, fireEvent, act } from "@testing-library/react"; -import { InterfacesTab } from "./InterfacesTab"; -import { useLabelStore } from "../../store/labelStore"; -import { fallbackTranslations as en } from "../../locales"; - -afterEach(cleanup); -beforeEach(() => { - act(() => { - useLabelStore.setState({ printerProfile: {} }); - }); -}); - -const loc = en.printerSettings.interfaces; -const profile = () => useLabelStore.getState().printerProfile; -const pick = (r: ReturnType, field: string, option: string | RegExp) => { - fireEvent.click(r.getByRole("button", { name: field })); - fireEvent.click(r.getByRole("option", { name: option })); -}; - -describe("InterfacesTab", () => { - it("builds the serial port slot by slot and clears the field with the last slot", () => { - const r = render(); - pick(r, loc.parity, new RegExp(loc.parityE)); - pick(r, loc.baud, "9600"); - expect(profile().serialPort).toEqual({ baud: "9600", parity: "E" }); - pick(r, loc.baud, loc.notSet); - pick(r, loc.parity, loc.notSet); - expect(profile()).toEqual({}); - }); - - it("fills the six serial slots from a preset and says so while the port is incomplete", () => { - const r = render(); - expect(r.getByRole("button", { name: en.label.preset }).textContent).toContain(loc.notSet); - pick(r, loc.baud, "9600"); - expect(r.getByText(loc.serialIncomplete)).toBeTruthy(); - expect(r.getByRole("button", { name: en.label.preset }).textContent).toContain(en.label.presetCustom); - pick(r, en.label.preset, /^115200 8N1/); - expect(profile().serialPort).toEqual({ baud: "115200", dataBits: "8", parity: "N", stopBits: "1", handshake: "X", protocol: "N" }); - expect(r.queryByText(loc.serialIncomplete)).toBeNull(); - fireEvent.click(r.getByRole("button", { name: en.label.preset })); - expect(r.queryByRole("option", { name: en.label.presetCustom })).toBeNull(); - expect(r.queryByRole("option", { name: loc.notSet })).toBeNull(); - }); - - it("sets one applicator slot on its own once the port is shown", () => { - const r = render(); - expect(r.queryByRole("button", { name: loc.startPrint })).toBeNull(); - fireEvent.click(r.getByRole("button", { name: loc.auxShow })); - pick(r, loc.startPrint, new RegExp(loc.startPrintL)); - expect(profile()).toEqual({ auxPortStartPrint: "l" }); - expect(r.queryByRole("button", { name: loc.auxHide })).toBeNull(); - pick(r, loc.startPrint, en.printerSettings.defaultOption); - expect(profile()).toEqual({}); - }); -}); diff --git a/src/components/PrinterSettings/InterfacesTab.tsx b/src/components/PrinterSettings/InterfacesTab.tsx deleted file mode 100644 index c7de1812..00000000 --- a/src/components/PrinterSettings/InterfacesTab.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { useId, useState } from "react"; -import { useT } from "../../hooks/useT"; -import { useLabelStore } from "../../store/labelStore"; -import { - AUX_PORT_SLOTS, - AUX_PORT_END_PRINT_VALUES, - AUX_PORT_ERROR_MODE_VALUES, - AUX_PORT_MODE_VALUES, - AUX_PORT_START_PRINT_VALUES, - AUX_PORT_SWITCH_VALUES, - SERIAL_BAUD_VALUES, - SERIAL_DATA_BITS_VALUES, - SERIAL_HANDSHAKE_VALUES, - SERIAL_PARITY_VALUES, - SERIAL_PORT_FIELDS, - SERIAL_PROTOCOL_VALUES, - SERIAL_STOP_BITS_VALUES, - type PrinterProfile, -} from "@zplab/core/types/PrinterProfile"; -import { ZplCommandLabel, ZplEnumSubCustomSelect, ZplField, ZplFieldHint } from "./zplFieldPrimitives"; -import { fieldGridCols, fieldGridCell } from "../ui/formStyles"; - -type LocInterfaces = ReturnType["printerSettings"]["interfaces"]; -type SerialPort = NonNullable; - -const PARITY_KEYS = { N: "parityN", E: "parityE", O: "parityO" } as const satisfies Record<(typeof SERIAL_PARITY_VALUES)[number], keyof LocInterfaces>; -const HANDSHAKE_KEYS = { X: "handshakeX", D: "handshakeD", R: "handshakeR", M: "handshakeM" } as const satisfies Record<(typeof SERIAL_HANDSHAKE_VALUES)[number], keyof LocInterfaces>; -const PROTOCOL_KEYS = { A: "protocolA", N: "protocolN", Z: "protocolZ" } as const satisfies Record<(typeof SERIAL_PROTOCOL_VALUES)[number], keyof LocInterfaces>; -const AUX_MODE_KEYS = { "0": "auxMode0", "1": "auxMode1", "2": "auxMode2" } as const satisfies Record<(typeof AUX_PORT_MODE_VALUES)[number], keyof LocInterfaces>; -const END_PRINT_KEYS = { "0": "endPrint0", "1": "endPrint1", "2": "endPrint2", "3": "endPrint3", "4": "endPrint4" } as const satisfies Record<(typeof AUX_PORT_END_PRINT_VALUES)[number], keyof LocInterfaces>; -const START_PRINT_KEYS = { p: "startPrintP", l: "startPrintL" } as const satisfies Record<(typeof AUX_PORT_START_PRINT_VALUES)[number], keyof LocInterfaces>; -const ERROR_MODE_KEYS = { e: "errorModeE", f: "errorModeF" } as const satisfies Record<(typeof AUX_PORT_ERROR_MODE_VALUES)[number], keyof LocInterfaces>; -const SWITCH_KEYS = { e: "enabled", d: "disabled" } as const satisfies Record<(typeof AUX_PORT_SWITCH_VALUES)[number], keyof LocInterfaces>; -// The three line settings a host is set up with in practice, 8N1 with XON/XOFF. -const SERIAL_PRESET_BAUDS = ["9600", "19200", "115200"] as const; -const serialPreset = (baud: (typeof SERIAL_PRESET_BAUDS)[number]): SerialPort => ({ baud, dataBits: "8", parity: "N", stopBits: "1", handshake: "X", protocol: "N" }); -const noBadge = () => ""; - -/** Serial port and applicator port of the setup script (^SC, ^JJ). */ -export function InterfacesTab() { - const t = useT(); - const profile = useLabelStore((s) => s.printerProfile); - const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile); - const loc = t.printerSettings.interfaces; - const serial: SerialPort = profile.serialPort ?? {}; - const serialSlotsSet = SERIAL_PORT_FIELDS.filter((f) => serial[f] !== undefined).length; - const activePreset = SERIAL_PRESET_BAUDS.find((baud) => SERIAL_PORT_FIELDS.every((f) => serial[f] === serialPreset(baud)[f])); - const anyAuxSet = AUX_PORT_SLOTS.some(([field]) => profile[field] !== undefined); - // A set slot ships with the script, so the port hides only while it is empty. - const [auxOpen, setAuxOpen] = useState(false); - const auxId = useId(); - const showAux = auxOpen || anyAuxSet; - // The slots are one command on the wire, so the last cleared slot clears the field. - const setSerial = (patch: Partial) => { - const merged = { ...serial, ...patch }; - // Wire order keeps the object equal by value to what the parser builds. - const next: SerialPort = Object.fromEntries(SERIAL_PORT_FIELDS.flatMap((f) => (merged[f] === undefined ? [] : [[f, merged[f]]]))); - patchPrinterProfile({ serialPort: SERIAL_PORT_FIELDS.some((f) => next[f] !== undefined) ? next : undefined }); - }; - - return ( -
- - -
- `${baud} 8N1 ${loc.handshakeX}`} - optionBadge={noBadge} - onChange={(baud) => baud !== undefined && patchPrinterProfile({ serialPort: serialPreset(baud) })} - className={fieldGridCell} - /> -
-
- v} - optionBadge={noBadge} - onChange={(baud) => setSerial({ baud })} - className={fieldGridCell} - /> - v} - optionBadge={noBadge} - onChange={(dataBits) => setSerial({ dataBits })} - className={fieldGridCell} - /> - loc[PARITY_KEYS[v]]} - onChange={(parity) => setSerial({ parity })} - className={fieldGridCell} - /> - v} - optionBadge={noBadge} - onChange={(stopBits) => setSerial({ stopBits })} - className={fieldGridCell} - /> - loc[HANDSHAKE_KEYS[v]]} - onChange={(handshake) => setSerial({ handshake })} - className={fieldGridCell} - /> - loc[PROTOCOL_KEYS[v]]} - onChange={(protocol) => setSerial({ protocol })} - className={fieldGridCell} - /> -
- - {loc.serialBaudHint} - - {serialSlotsSet > 0 && serialSlotsSet < SERIAL_PORT_FIELDS.length ? ( - - {loc.serialIncomplete} - - ) : null} -
- - - - {anyAuxSet ? null : ( - - )} - {showAux ? ( -
- loc[AUX_MODE_KEYS[v]]} - onChange={(auxPortMode) => patchPrinterProfile({ auxPortMode })} - className={fieldGridCell} - /> - loc[END_PRINT_KEYS[v]]} - onChange={(auxPortEndPrint) => patchPrinterProfile({ auxPortEndPrint })} - className={fieldGridCell} - /> - loc[START_PRINT_KEYS[v]]} - onChange={(auxPortStartPrint) => patchPrinterProfile({ auxPortStartPrint })} - className={fieldGridCell} - /> - loc[ERROR_MODE_KEYS[v]]} - onChange={(auxPortErrorMode) => patchPrinterProfile({ auxPortErrorMode })} - className={fieldGridCell} - /> - loc[SWITCH_KEYS[v]]} - onChange={(auxPortReprint) => patchPrinterProfile({ auxPortReprint })} - className={fieldGridCell} - /> - loc[SWITCH_KEYS[v]]} - onChange={(auxPortRibbonLow) => patchPrinterProfile({ auxPortRibbonLow })} - className={fieldGridCell} - /> -
- ) : null} -
-
- ); -} diff --git a/src/components/PrinterSettings/PrinterSettingsModal.tsx b/src/components/PrinterSettings/PrinterSettingsModal.tsx index 5012deff..72385148 100644 --- a/src/components/PrinterSettings/PrinterSettingsModal.tsx +++ b/src/components/PrinterSettings/PrinterSettingsModal.tsx @@ -24,8 +24,6 @@ import { StoredFontsTab } from "./StoredFontsTab"; import { StoredGraphicsTab } from "./StoredGraphicsTab"; import { IdentityTab } from "./IdentityTab"; import { MaintenanceTab } from "./MaintenanceTab"; -import { SensorsTab } from "./SensorsTab"; -import { InterfacesTab } from "./InterfacesTab"; import { McpServerTab } from "./McpServerTab"; import { MediaFeedTab } from "./MediaFeedTab"; import { RfidTab } from "./RfidTab"; @@ -56,8 +54,6 @@ const TOP_TAB_OF = { fonts: 'setupScript', identity: 'setupScript', maintenance: 'setupScript', - sensors: 'setupScript', - interfaces: 'setupScript', storedFonts: 'objects', storedGraphics: 'objects', } as const satisfies Record; @@ -102,8 +98,6 @@ const TAB_COMPONENTS: Partial> = { storedGraphics: StoredGraphicsTab, identity: IdentityTab, maintenance: MaintenanceTab, - sensors: SensorsTab, - interfaces: InterfacesTab, }; const MODAL_BOX_CLS = @@ -305,7 +299,7 @@ export function PrinterSettingsModal() {