Skip to content

Commit 4817177

Browse files
committed
feat(zpl): maintenance batch (^MA / ^MI / ^MW / ^JH)
Adds four Setup-Script-channel commands and a dedicated Maintenance tab in the printer-settings modal. - ^MA (Maintenance Alert): type (H/R), print (Y/N), threshold, frequency, units (M/I/C). Blank slot in `print`/`units` falls back to spec defaults (Y / M) so partial dumps round-trip. frequency min=1 by deliberate parser stance; out-of-range and invalid type discarded. - ^MI (Maintenance Info Message): paired with ^MA via a shared type discriminator. Enforced cross-field at the schema layer (superRefine) so a mismatched import is rejected; the UI cascades alert-type changes into the existing message so the invariant never trips silently. - ^MW (Head Cold Warning): single Y/N flag. - ^JH (Early Warning Settings): only f-slot (early-warning gate) and g-slot (head-cleaning interval, `<n>M`) modelled; sibling slots emit empty. The registry places ^JH BEFORE ^MA so the gate is in effect when the alert lands. The g-slot wire format is marked TODO HW-verify (Zebra docs are ambiguous between raw `<n>M` and an indexed 0..16 table). UI surface: - New `maintenance` PrinterSettingsTab + MaintenanceTab component - Alert is fully clearable via the type select's default option; message text empty clears the message - Defaults sourced from MAINTENANCE_ALERT_DEFAULTS (single source of truth shared by UI placeholder + parser blank-slot fallback) - Hint shows the 63-char cap via `({MAINTENANCE_MESSAGE_MAX_LEN})` template, not a hardcoded string Locales seeded English across 32 files; translators can refine. Roadmap 98→102 / 202; READMEs Media-&-feed 8/10 and Print-quality 5/14.
1 parent 3a55206 commit 4817177

41 files changed

Lines changed: 1484 additions & 10 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte
116116

117117
## Coverage
118118

119-
98 of the 202 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown:
119+
102 of the 202 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown:
120120

121121
| Area | Supported |
122122
|---|---|
@@ -128,9 +128,9 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte
128128
| Clock & time | 2 / 3 |
129129
| Identity & access | 3 / 3 |
130130
| Graphics | 6 / 12 |
131-
| Media & feed | 5 / 10 |
131+
| Media & feed | 8 / 10 |
132132
| Text & fonts | 6 / 14 |
133-
| Print quality | 4 / 14 |
133+
| Print quality | 5 / 14 |
134134
| Configuration & persistence | 3 / 5 |
135135
| Hardware / Host comm / RFID / Network | 0 / 67 |
136136

‎docs/zpl-roadmap.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ The Printer Settings Modal (Media & Feed / Print Quality / Clock & Time / Encodi
158158
| `[x]` | `^ML` | maximum label length | |
159159
| `[x]` | `^MF` | media feed | |
160160
| `[x]` | `^XB` | suppress backfeed | |
161-
| `[ ]` | `^MA` | maintenance alert | `Coming soon` |
161+
| `[x]` | `^MA` | maintenance alert | |
162162
| `[ ]` | `^MC` | map clear | `Coming soon` |
163-
| `[ ]` | `^MI` | maintenance info message | `Coming soon` |
163+
| `[x]` | `^MI` | maintenance info message | |
164164
| `[ ]` | `^MP` | mode protection | `Coming soon` |
165165
| `[x]` | `^MU` | units of measure | |
166-
| `[ ]` | `^MW` | head cold warning | `Coming soon` |
166+
| `[x]` | `^MW` | head cold warning | |
167167

168168
## Print quality
169169

@@ -173,7 +173,7 @@ The Printer Settings Modal (Media & Feed / Print Quality / Clock & Time / Encodi
173173
| `[x]` | `^JT` | head test interval | |
174174
| `[x]` | `~TA` | tear off adjust | |
175175
| `[ ]` | `^PA` | advanced text properties | `Coming soon` |
176-
| `[ ]` | `^JH` | early warning settings | `Coming soon` |
176+
| `[x]` | `^JH` | early warning settings | |
177177
| `[ ]` | `^PF` | slew dot rows | `Coming soon` |
178178
| `[ ]` | `^PH` / `~PH` | slew to home | `Coming soon` |
179179
| `[ ]` | `^PN` | present now | `Coming soon` |
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { useT } from "../../lib/useT";
2+
import { useLabelStore } from "../../store/labelStore";
3+
import { inputCls, labelCls } from "../ui/formStyles";
4+
import type { PrinterProfile } from "../../types/PrinterProfile";
5+
import {
6+
HEAD_CLEANING_INTERVAL_RANGE,
7+
MAINTENANCE_ALERT_DEFAULTS,
8+
MAINTENANCE_ALERT_PRINT_VALUES,
9+
MAINTENANCE_ALERT_TYPES,
10+
MAINTENANCE_ALERT_UNITS,
11+
MAINTENANCE_DISTANCE_MAX,
12+
MAINTENANCE_MESSAGE_MAX_LEN,
13+
isMaintenanceAlertPrint,
14+
isMaintenanceAlertType,
15+
isMaintenanceAlertUnit,
16+
type MaintenanceAlertType,
17+
type MaintenanceAlertUnit,
18+
} from "../../types/PrinterProfile";
19+
import {
20+
ZplBoundedIntInput,
21+
ZplCommandLabel,
22+
ZplEnumSelect,
23+
ZplField,
24+
} from "./zplFieldPrimitives";
25+
26+
type LocMaintenance = ReturnType<typeof useT>["printerSettings"]["maintenance"];
27+
type MaintenanceAlert = NonNullable<PrinterProfile["maintenanceAlert"]>;
28+
29+
const ALERT_TYPE_LABEL_KEYS = {
30+
H: 'alertTypeH',
31+
R: 'alertTypeR',
32+
} as const satisfies Record<MaintenanceAlertType, keyof LocMaintenance>;
33+
34+
const ALERT_UNIT_LABEL_KEYS = {
35+
M: 'alertUnitsM',
36+
I: 'alertUnitsI',
37+
C: 'alertUnitsC',
38+
} as const satisfies Record<MaintenanceAlertUnit, keyof LocMaintenance>;
39+
40+
/** Maintenance tab (^MA/^MI/^MW/^JH). */
41+
export function MaintenanceTab() {
42+
const t = useT();
43+
const profile = useLabelStore((s) => s.printerProfile);
44+
const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile);
45+
const loc = t.printerSettings.maintenance;
46+
const alert = profile.maintenanceAlert;
47+
const message = profile.maintenanceMessage;
48+
49+
// Centralised mutation: any change to maintenanceAlert.type also
50+
// cascades into maintenanceMessage.type so the cross-field invariant
51+
// (superRefine in PrinterProfile.ts) never trips and a silent patch
52+
// rollback never strands the UI.
53+
const updateAlert = <K extends keyof MaintenanceAlert>(key: K, value: MaintenanceAlert[K]) => {
54+
if (!alert) return;
55+
const nextAlert = { ...alert, [key]: value };
56+
const next: Partial<PrinterProfile> = { maintenanceAlert: nextAlert };
57+
if (key === "type" && message) {
58+
next.maintenanceMessage = { ...message, type: nextAlert.type };
59+
}
60+
patchPrinterProfile(next);
61+
};
62+
63+
return (
64+
<div className="flex flex-col gap-4">
65+
<ZplField>
66+
<ZplCommandLabel text={loc.maintenanceAlertHeading} command="^MA" />
67+
<div className="flex flex-col gap-2 pl-2 border-l border-border">
68+
<div className="grid grid-cols-2 gap-2">
69+
<div className="flex flex-col gap-1">
70+
<label className={labelCls}>{loc.alertType}</label>
71+
<select
72+
className={inputCls}
73+
value={alert?.type ?? ""}
74+
onChange={(e) => {
75+
const raw = e.target.value;
76+
if (raw === "") {
77+
patchPrinterProfile({ maintenanceAlert: undefined });
78+
return;
79+
}
80+
if (!isMaintenanceAlertType(raw)) return;
81+
if (alert) {
82+
updateAlert("type", raw);
83+
return;
84+
}
85+
// First-time alert creation: cascade into an
86+
// existing typed-ahead message so the cross-field
87+
// invariant holds atomically.
88+
const next: Partial<PrinterProfile> = {
89+
maintenanceAlert: { ...MAINTENANCE_ALERT_DEFAULTS, type: raw },
90+
};
91+
if (message) next.maintenanceMessage = { ...message, type: raw };
92+
patchPrinterProfile(next);
93+
}}
94+
>
95+
<option value="">{t.printerSettings.defaultOption}</option>
96+
{MAINTENANCE_ALERT_TYPES.map((v) => (
97+
<option key={v} value={v}>{loc[ALERT_TYPE_LABEL_KEYS[v]]}</option>
98+
))}
99+
</select>
100+
</div>
101+
<div className="flex flex-col gap-1">
102+
<label className={labelCls}>{loc.alertPrint}</label>
103+
<select
104+
className={inputCls}
105+
value={alert?.print ?? MAINTENANCE_ALERT_DEFAULTS.print}
106+
onChange={(e) => {
107+
const raw = e.target.value;
108+
if (isMaintenanceAlertPrint(raw)) updateAlert("print", raw);
109+
}}
110+
disabled={!alert}
111+
>
112+
{MAINTENANCE_ALERT_PRINT_VALUES.map((v) => (
113+
<option key={v} value={v}>{v === "Y" ? loc.alertPrintY : loc.alertPrintN}</option>
114+
))}
115+
</select>
116+
</div>
117+
</div>
118+
<div className="grid grid-cols-3 gap-2">
119+
<BoundedIntCell
120+
label={loc.alertThreshold}
121+
min={0}
122+
max={MAINTENANCE_DISTANCE_MAX}
123+
value={alert?.threshold}
124+
onChange={(threshold) => {
125+
if (threshold !== undefined) updateAlert("threshold", threshold);
126+
}}
127+
disabled={!alert}
128+
/>
129+
<BoundedIntCell
130+
label={loc.alertFrequency}
131+
min={1}
132+
max={MAINTENANCE_DISTANCE_MAX}
133+
value={alert?.frequency}
134+
onChange={(frequency) => {
135+
if (frequency !== undefined) updateAlert("frequency", frequency);
136+
}}
137+
disabled={!alert}
138+
/>
139+
<div className="flex flex-col gap-1">
140+
<label className={labelCls}>{loc.alertUnits}</label>
141+
<select
142+
className={inputCls}
143+
value={alert?.units ?? MAINTENANCE_ALERT_DEFAULTS.units}
144+
onChange={(e) => {
145+
const raw = e.target.value;
146+
if (isMaintenanceAlertUnit(raw)) updateAlert("units", raw);
147+
}}
148+
disabled={!alert}
149+
>
150+
{MAINTENANCE_ALERT_UNITS.map((v) => (
151+
<option key={v} value={v}>{loc[ALERT_UNIT_LABEL_KEYS[v]]}</option>
152+
))}
153+
</select>
154+
</div>
155+
</div>
156+
</div>
157+
</ZplField>
158+
159+
<ZplField>
160+
<ZplCommandLabel text={loc.maintenanceMessageHeading} command="^MI" />
161+
<div className="flex flex-col gap-2 pl-2 border-l border-border">
162+
<div className="flex flex-col gap-1">
163+
<label className={labelCls}>{loc.messageText}</label>
164+
<input
165+
type="text"
166+
className={inputCls}
167+
maxLength={MAINTENANCE_MESSAGE_MAX_LEN}
168+
value={message?.text ?? ""}
169+
onChange={(e) => {
170+
const text = e.target.value;
171+
if (!text) {
172+
patchPrinterProfile({ maintenanceMessage: undefined });
173+
return;
174+
}
175+
// Type sticks to alert.type when an alert exists; otherwise
176+
// falls back to the default so superRefine stays satisfied.
177+
const type = message?.type ?? alert?.type ?? MAINTENANCE_ALERT_DEFAULTS.type;
178+
patchPrinterProfile({ maintenanceMessage: { type, text } });
179+
}}
180+
/>
181+
<span className={`${labelCls} normal-case tracking-normal text-muted/70`}>
182+
{loc.messageTextHint} ({MAINTENANCE_MESSAGE_MAX_LEN})
183+
</span>
184+
</div>
185+
</div>
186+
</ZplField>
187+
188+
<ZplEnumSelect
189+
label={loc.earlyWarningMaintenance}
190+
command="^JH"
191+
values={['E', 'D'] as const}
192+
isValid={(v): v is 'E' | 'D' => v === 'E' || v === 'D'}
193+
value={profile.earlyWarningMaintenance}
194+
onChange={(earlyWarningMaintenance) => patchPrinterProfile({ earlyWarningMaintenance })}
195+
defaultLabel={t.printerSettings.defaultOption}
196+
optionLabel={(v) => v === 'E' ? loc.earlyWarningE : loc.earlyWarningD}
197+
/>
198+
<span className={`${labelCls} normal-case tracking-normal text-muted/70 -mt-2`}>
199+
{loc.earlyWarningGateHint}
200+
</span>
201+
202+
<ZplBoundedIntInput
203+
label={loc.headCleaningIntervalMeters}
204+
command="^JH"
205+
min={HEAD_CLEANING_INTERVAL_RANGE.min}
206+
max={HEAD_CLEANING_INTERVAL_RANGE.max}
207+
value={profile.headCleaningIntervalMeters}
208+
onChange={(headCleaningIntervalMeters) =>
209+
patchPrinterProfile({ headCleaningIntervalMeters })
210+
}
211+
unit={loc.alertUnitsM}
212+
/>
213+
<span className={`${labelCls} normal-case tracking-normal text-muted/70 -mt-2`}>
214+
{loc.headCleaningIntervalHint}
215+
</span>
216+
217+
<ZplEnumSelect
218+
label={loc.headColdWarning}
219+
command="^MW"
220+
values={['Y', 'N'] as const}
221+
isValid={(v): v is 'Y' | 'N' => v === 'Y' || v === 'N'}
222+
value={profile.headColdWarning}
223+
onChange={(headColdWarning) => patchPrinterProfile({ headColdWarning })}
224+
defaultLabel={t.printerSettings.defaultOption}
225+
optionLabel={(v) => v === 'Y' ? loc.headColdWarningOn : loc.headColdWarningOff}
226+
/>
227+
</div>
228+
);
229+
}
230+
231+
function BoundedIntCell({
232+
label,
233+
min,
234+
max,
235+
value,
236+
onChange,
237+
disabled,
238+
}: {
239+
label: string;
240+
min: number;
241+
max: number;
242+
value: number | undefined;
243+
onChange: (next: number | undefined) => void;
244+
disabled?: boolean;
245+
}) {
246+
return (
247+
<div className="flex flex-col gap-1">
248+
<label className={labelCls}>{label}</label>
249+
<input
250+
type="number"
251+
className={inputCls}
252+
min={min}
253+
max={max}
254+
value={value ?? ""}
255+
disabled={disabled}
256+
onChange={(e) => {
257+
const raw = e.target.value;
258+
if (raw === "") return;
259+
const n = Number.parseInt(raw, 10);
260+
if (Number.isNaN(n)) return;
261+
onChange(Math.max(min, Math.min(max, n)));
262+
}}
263+
/>
264+
</div>
265+
);
266+
}

‎src/components/PrinterSettings/PrinterSettingsModal.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ZplLine } from "../Output/ZplLine";
1111
import { ClockAndTimeTab } from "./ClockAndTimeTab";
1212
import { EncodingAndLanguageTab } from "./EncodingAndLanguageTab";
1313
import { IdentityTab } from "./IdentityTab";
14+
import { MaintenanceTab } from "./MaintenanceTab";
1415
import { MediaFeedTab } from "./MediaFeedTab";
1516
import { PrintQualityTab } from "./PrintQualityTab";
1617

@@ -24,6 +25,7 @@ const TOP_TAB_OF = {
2425
clockTime: 'setupScript',
2526
encodingLanguage: 'setupScript',
2627
identity: 'setupScript',
28+
maintenance: 'setupScript',
2729
} as const satisfies Record<PrinterSettingsTab, TopTabId>;
2830

2931
const TOP_TAB_ORDER: readonly TopTabId[] = ['perLabel', 'setupScript'];
@@ -50,6 +52,7 @@ const TAB_COMPONENTS: Partial<Record<PrinterSettingsTab, FC>> = {
5052
clockTime: ClockAndTimeTab,
5153
encodingLanguage: EncodingAndLanguageTab,
5254
identity: IdentityTab,
55+
maintenance: MaintenanceTab,
5356
};
5457

5558
const MODAL_BOX_CLS =

0 commit comments

Comments
 (0)