@@ -17,15 +17,11 @@ import { formatRealtimeClockForZpl, toLocalIsoString } from './realtimeClock';
1717 * earlier script left the printer in a non-default state. */
1818
1919type SetupScriptEntry =
20- | {
21- kind : 'emit' ;
22- channel : 'tilde' | 'block' ;
23- emit : ( profile : PrinterProfile ) => string | null ;
24- }
25- | {
26- kind : 'foldedInto' ;
27- target : keyof PrinterProfile ;
28- } ;
20+ | { kind : 'emit' ; channel : 'tilde' ; emit : ( p : PrinterProfile ) => string | null }
21+ // `scope` is required on block entries so every new command makes the
22+ // persistence call explicit; 'session' entries emit after ^JUS.
23+ | { kind : 'emit' ; channel : 'block' ; scope : 'persistent' | 'session' ; emit : ( p : PrinterProfile ) => string | null }
24+ | { kind : 'foldedInto' ; target : keyof PrinterProfile } ;
2925
3026const SETUP_SCRIPT_EMITTERS = {
3127 tearOffAdjust : {
@@ -45,16 +41,19 @@ const SETUP_SCRIPT_EMITTERS = {
4541 reprintAfterError : {
4642 kind : 'emit' ,
4743 channel : 'block' ,
44+ scope : 'persistent' ,
4845 emit : ( p ) => p . reprintAfterError !== undefined ? `^JZ${ p . reprintAfterError } ` : null ,
4946 } ,
5047 headTestInterval : {
5148 kind : 'emit' ,
5249 channel : 'block' ,
50+ scope : 'persistent' ,
5351 emit : ( p ) => p . headTestInterval !== undefined ? `^JT${ p . headTestInterval } ` : null ,
5452 } ,
5553 setRealtimeClock : {
5654 kind : 'emit' ,
5755 channel : 'block' ,
56+ scope : 'persistent' ,
5857 // Live mode (useCurrentTimeForClock) captures now() at emit-time
5958 // and wins over the stored static value. Toggling the checkbox in
6059 // the UI is therefore unambiguous regardless of stored ISO state.
@@ -72,26 +71,31 @@ const SETUP_SCRIPT_EMITTERS = {
7271 clockFormat : {
7372 kind : 'emit' ,
7473 channel : 'block' ,
74+ scope : 'persistent' ,
7575 emit : ( p ) => p . clockFormat !== undefined ? `^KD${ p . clockFormat } ` : null ,
7676 } ,
7777 printerLocale : {
7878 kind : 'emit' ,
7979 channel : 'block' ,
80+ scope : 'persistent' ,
8081 emit : ( p ) => p . printerLocale !== undefined ? `^KL${ p . printerLocale } ` : null ,
8182 } ,
8283 encodingTable : {
8384 kind : 'emit' ,
8485 channel : 'block' ,
86+ scope : 'persistent' ,
8587 emit : ( p ) => p . encodingTable !== undefined ? `^SE${ p . encodingTable } ` : null ,
8688 } ,
8789 zplMode : {
8890 kind : 'emit' ,
8991 channel : 'block' ,
92+ scope : 'persistent' ,
9093 emit : ( p ) => p . zplMode !== undefined ? `^SZ${ p . zplMode } ` : null ,
9194 } ,
9295 printerName : {
9396 kind : 'emit' ,
9497 channel : 'block' ,
98+ scope : 'persistent' ,
9599 emit : ( p ) => {
96100 if ( p . printerName === undefined ) return null ;
97101 // Trim both halves: parser trims on import; asymmetric whitespace
@@ -107,11 +111,13 @@ const SETUP_SCRIPT_EMITTERS = {
107111 setPassword : {
108112 kind : 'emit' ,
109113 channel : 'block' ,
114+ scope : 'persistent' ,
110115 emit : ( p ) => p . setPassword !== undefined ? `^KP${ p . setPassword } ` : null ,
111116 } ,
112117 clockMode : {
113118 kind : 'emit' ,
114119 channel : 'block' ,
120+ scope : 'persistent' ,
115121 // ^SL `a` slot is tri-shape: 'S', 'T', or numeric tolerance (TOL
116122 // mode). Schema's cross-field refine guarantees TOL always has a
117123 // defined clockTolerance, so no fallback needed.
@@ -133,6 +139,7 @@ const SETUP_SCRIPT_EMITTERS = {
133139 earlyWarningMaintenance : {
134140 kind : 'emit' ,
135141 channel : 'block' ,
142+ scope : 'persistent' ,
136143 emit : ( p ) => {
137144 if ( p . earlyWarningMaintenance === undefined && p . headCleaningIntervalMeters === undefined ) {
138145 return null ;
@@ -152,6 +159,7 @@ const SETUP_SCRIPT_EMITTERS = {
152159 maintenanceAlert : {
153160 kind : 'emit' ,
154161 channel : 'block' ,
162+ scope : 'persistent' ,
155163 emit : ( p ) => {
156164 const m = p . maintenanceAlert ;
157165 if ( ! m ) return null ;
@@ -161,28 +169,31 @@ const SETUP_SCRIPT_EMITTERS = {
161169 maintenanceMessage : {
162170 kind : 'emit' ,
163171 channel : 'block' ,
172+ scope : 'persistent' ,
164173 emit : ( p ) => p . maintenanceMessage
165174 ? `^MI${ p . maintenanceMessage . type } ,${ p . maintenanceMessage . text } `
166175 : null ,
167176 } ,
168177 headColdWarning : {
169178 kind : 'emit' ,
170179 channel : 'block' ,
180+ scope : 'persistent' ,
171181 emit : ( p ) => p . headColdWarning !== undefined ? `^MW${ p . headColdWarning } ` : null ,
172182 } ,
173- // configurationUpdate is the registry's last entry on purpose: a
174- // commit (`^JUS`) needs to follow every other persistent write so
175- // the EEPROM lands the rest of the block before being asked to
176- // save itself. The Zebra app note for ^JU also warns that more
177- // than one ^JU per file is unsupported, so we never split this.
178- // The "is-last" invariant is anchored by a tripwire in
179- // zplSetupScript.test.ts so adding a later entry can't silently
180- // break the commit semantics.
183+ // Last persistent entry: ^JUS must follow every persistent write so the
184+ // EEPROM commits the block. Tripwire in zplSetupScript.test.ts pins this.
181185 configurationUpdate : {
182186 kind : 'emit' ,
183187 channel : 'block' ,
188+ scope : 'persistent' ,
184189 emit : ( p ) => p . configurationUpdate !== undefined ? `^JU${ p . configurationUpdate } ` : null ,
185190 } ,
191+ codeValidation : {
192+ kind : 'emit' ,
193+ channel : 'block' ,
194+ scope : 'session' ,
195+ emit : ( p ) => p . codeValidation !== undefined ? `^CV${ p . codeValidation } ` : null ,
196+ } ,
186197} as const satisfies Partial < Record < keyof PrinterProfile , SetupScriptEntry > > ;
187198
188199/** Public list of the PrinterProfile fields that flow through the
@@ -200,15 +211,32 @@ export const __SETUP_SCRIPT_EMITTERS_FOR_TESTS = SETUP_SCRIPT_EMITTERS;
200211
201212export function generateSetupScript ( profile : PrinterProfile ) : string {
202213 const tildeLines : string [ ] = [ ] ;
203- const blockLines : string [ ] = [ ] ;
214+ const persistentBlock : string [ ] = [ ] ;
215+ const sessionBlock : string [ ] = [ ] ;
204216
205217 for ( const field of SETUP_SCRIPT_FIELDS ) {
206218 const e = SETUP_SCRIPT_EMITTERS [ field ] ;
207219 if ( e . kind !== 'emit' ) continue ;
208220 const line = e . emit ( profile ) ;
209221 if ( line === null ) continue ;
210- ( e . channel === 'tilde' ? tildeLines : blockLines ) . push ( line ) ;
222+ if ( e . channel === 'tilde' ) {
223+ tildeLines . push ( line ) ;
224+ continue ;
225+ }
226+ // Exhaustive on scope: a future variant forces a case here at
227+ // compile time instead of silently falling through to persistent.
228+ switch ( e . scope ) {
229+ case 'persistent' : persistentBlock . push ( line ) ; break ;
230+ case 'session' : sessionBlock . push ( line ) ; break ;
231+ default : {
232+ const _exhaustive : never = e ;
233+ throw new Error ( `unhandled scope on emit entry: ${ JSON . stringify ( _exhaustive ) } ` ) ;
234+ }
235+ }
211236 }
237+ // Order is load-bearing: session entries trail ^JUS so the commit
238+ // never tries to persist them.
239+ const blockLines = [ ...persistentBlock , ...sessionBlock ] ;
212240
213241 if ( tildeLines . length === 0 && blockLines . length === 0 ) return '' ;
214242
0 commit comments