Skip to content

Commit 133d2ef

Browse files
committed
feat(printer): the label and profile adopt the printer's own settings
1 parent 10004af commit 133d2ef

46 files changed

Lines changed: 1291 additions & 20 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ On desktop, **Connect data** also reads an Excel worksheet. **File → Settings
155155
- Saved designs and label exports exclude **Setup Script** values such as printer name and locale.
156156
- **Clear** resets the **Setup Script** values and keeps the uploads.
157157

158-
The **App → Printer** tab sets how labels are sent and to which printer. **File → Output** opens with that method selected. The **App → Preview** tab selects the renderer: the connected printer (desktop), Labelary, or off. It takes a premium Labelary endpoint and API key. The key is stored in the OS keychain on desktop and in browser storage on the web.
158+
The **App → Printer** tab sets how labels are sent and to which printer. On desktop, **Adopt from printer** reads the printer's own settings and asks which of the differing values to adopt. **File → Output** opens with that method selected. The **App → Preview** tab selects the renderer: the connected printer (desktop), Labelary, or off. It takes a premium Labelary endpoint and API key. The key is stored in the OS keychain on desktop and in browser storage on the web.
159159

160160
### MCP server
161161

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { describe, it, expect } from 'vitest';
2+
import type { LabelConfig } from '../types/LabelConfig';
3+
import type { PrinterProfile } from '../types/PrinterProfile';
4+
import { adoptionDiff, adoptionPatches, isAdoptable, preselectedRows } from './printerSettingsAdoption';
5+
import { ZD230_ALLCV_EXCERPT } from './sgd.fixture';
6+
import { parseSgdDump } from './sgd';
7+
8+
const LABEL: LabelConfig = { widthMm: 100, heightMm: 60, dpmm: 8 };
9+
10+
const diff = (dump = ZD230_ALLCV_EXCERPT, label: LabelConfig = LABEL, profile: PrinterProfile = {}) =>
11+
adoptionDiff(parseSgdDump(dump), label, profile);
12+
13+
const adopted = (field: string, dump?: string) => {
14+
const row = diff(dump).find((r) => r.field === field);
15+
const value = row?.patch?.value as Record<string, unknown> | undefined;
16+
return value?.[field];
17+
};
18+
19+
const ticked = (label: LabelConfig = LABEL, profile: PrinterProfile = {}) =>
20+
preselectedRows(diff(ZD230_ALLCV_EXCERPT, label, profile)).map((r) => r.field);
21+
22+
describe('adoptionDiff converters', () => {
23+
it('reads the measured ZD230 configuration into app values', () => {
24+
expect(adopted('instantDarkness')).toBe(15);
25+
expect(adopted('printSpeed')).toBe(6);
26+
expect(adopted('mediaMode')).toBe('T');
27+
expect(adopted('mediaTracking')).toBe('Y');
28+
expect(adopted('mediaType')).toBe('D');
29+
expect(adopted('dpmm')).toBe(8);
30+
expect(adopted('widthMm')).toBe(101.6);
31+
expect(adopted('heightMm')).toBe(152.4);
32+
expect(adopted('labelTop')).toBe(30);
33+
expect(adopted('labelShift')).toBe(0);
34+
expect(adopted('printOrientation')).toBe('N');
35+
expect(adopted('backfeedSequence')).toBe('N');
36+
expect(adopted('mediaFeedPowerUp')).toBe('N');
37+
expect(adopted('mediaFeedHeadClose')).toBe('F');
38+
expect(adopted('reprintAfterError')).toBe('N');
39+
expect(adopted('tearOffAdjust')).toBe(0);
40+
expect(adopted('printerName')).toBe('D4J260700032');
41+
expect(adopted('zplMode')).toBe('2');
42+
});
43+
44+
it('reads a thermal transfer print method in both spellings', () => {
45+
expect(adopted('mediaType', 'ezpl.print_method : thermal trans\n')).toBe('T');
46+
expect(adopted('mediaType', 'ezpl.print_method : thermal transfer\n')).toBe('T');
47+
});
48+
49+
it('reads the thermal mode when the print method is missing', () => {
50+
expect(adopted('mediaType', 'media.thermal_mode : TT\n')).toBe('T');
51+
expect(adopted('mediaType', 'media.thermal_mode : DT\n')).toBe('D');
52+
});
53+
54+
it('reads a bar-sensed gap media as mark tracking', () => {
55+
expect(adopted('mediaTracking', 'ezpl.media_type : gap/notch\nmedia.sense_mode : bar\n')).toBe('M');
56+
});
57+
58+
it('reads a gap media without a sense mode as gap tracking', () => {
59+
expect(adopted('mediaTracking', 'ezpl.media_type : gap/notch\n')).toBe('Y');
60+
});
61+
62+
it('reads the selected sensor when no sense mode answers', () => {
63+
expect(adopted('mediaTracking', 'ezpl.media_type : gap/notch\ndevice.sensor_select : reflective\n')).toBe('M');
64+
expect(adopted('mediaTracking', 'ezpl.media_type : gap/notch\ndevice.sensor_select : transmissive\n')).toBe('Y');
65+
});
66+
67+
it('reads a continuous media as continuous tracking', () => {
68+
expect(adopted('mediaTracking', 'ezpl.media_type : continuous\n')).toBe('N');
69+
});
70+
71+
it('reads an auto detecting media as auto tracking', () => {
72+
expect(adopted('mediaTracking', 'ezpl.media_type : auto_detect\n')).toBe('A');
73+
});
74+
75+
it('keeps a continuous media continuous however the sensor is set', () => {
76+
expect(adopted('mediaTracking', 'ezpl.media_type : continuous\nmedia.sense_mode : bar\n')).toBe('N');
77+
});
78+
79+
it('reads a bar sensor without a media type as mark tracking', () => {
80+
expect(adopted('mediaTracking', 'media.sense_mode : bar\n')).toBe('M');
81+
});
82+
83+
it('reads a transmissive sensor without a media type as gap tracking', () => {
84+
expect(adopted('mediaTracking', 'device.sensor_select : transmissive\n')).toBe('Y');
85+
});
86+
87+
it('reads the spec letters other firmware answers with', () => {
88+
expect(adopted('mediaMode', 'media.printmode : D\n')).toBe('D');
89+
expect(adopted('mediaType', 'ezpl.print_method : T\n')).toBe('T');
90+
});
91+
92+
it('reads a backfeed percent', () => {
93+
expect(adopted('backfeedSequence', 'media.backfeed : 80\n')).toBe(80);
94+
});
95+
96+
it('reads an inverted orientation', () => {
97+
expect(adopted('printOrientation', 'zpl.print_orientation : inv\n')).toBe('I');
98+
});
99+
100+
it('reads an inverted label as the orientation when no orientation answers', () => {
101+
expect(adopted('printOrientation', 'print.invert_label : on\n')).toBe('I');
102+
expect(adopted('printOrientation', 'print.invert_label : off\n')).toBe('N');
103+
});
104+
105+
it('converts dots in the printer head density, not the label one', () => {
106+
const at300 = 'ezpl.print_width : 1200\nzpl.label_length : 1800\nhead.resolution.in_dpi : 300\n';
107+
expect(adopted('widthMm', at300)).toBe(100);
108+
expect(adopted('heightMm', at300)).toBe(150);
109+
expect(adopted('dpmm', at300)).toBe(12);
110+
});
111+
});
112+
113+
describe('adoptionDiff rows', () => {
114+
it('lists a row only for a setting the printer answered', () => {
115+
const rows = diff('print.tone : 15.0 , Choices: 0.0-30.0\n');
116+
expect(rows.map((r) => r.field)).toEqual(['instantDarkness']);
117+
expect(rows[0]).toMatchObject({ key: 'print.tone', command: '~SD', printerValue: '15.0', currentValue: '', differs: true });
118+
});
119+
120+
it('names the printer answer and the value the app would hold', () => {
121+
const rows = diff('print.tone : 15.0\nezpl.print_width : 813\nhead.resolution.in_dpi : 203\nzpl.zpl_mode : zpl II\n');
122+
expect(rows.find((r) => r.field === 'instantDarkness')).toMatchObject({ printerValue: '15.0', adoptedValue: '15' });
123+
expect(rows.find((r) => r.field === 'widthMm')).toMatchObject({ printerValue: '813', adoptedValue: '101.6' });
124+
expect(rows.find((r) => r.field === 'zplMode')).toMatchObject({ printerValue: 'zpl II', adoptedValue: '2' });
125+
});
126+
127+
it('marks a setting the app cannot hold as not adoptable', () => {
128+
const row = diff('media.printmode : cutter\n')[0];
129+
expect(row?.field).toBe('mediaMode');
130+
expect(isAdoptable(row!)).toBe(false);
131+
expect(row).toMatchObject({ adoptedValue: '', differs: false });
132+
});
133+
134+
it('names the key it read when the first one is missing', () => {
135+
const row = diff('ezpl.print_mode : tear off\n')[0];
136+
expect(row?.key).toBe('ezpl.print_mode');
137+
expect(row?.printerValue).toBe('tear off');
138+
});
139+
140+
it('says a value the app already holds does not differ', () => {
141+
const row = diff('zpl.label_top : 30\n', { ...LABEL, labelTop: 30 })[0];
142+
expect(row).toMatchObject({ currentValue: '30', differs: false });
143+
expect(isAdoptable(row!)).toBe(true);
144+
});
145+
146+
it('says a value an unset field already stands for does not differ', () => {
147+
expect(diff('zpl.left_position : 0\n')[0]).toMatchObject({ differs: false });
148+
expect(diff('zpl.label_top : 0\n')[0]).toMatchObject({ differs: false });
149+
expect(diff('media.backfeed : N\n')[0]).toMatchObject({ differs: false });
150+
expect(diff('zpl.print_orientation : nor\n')[0]).toMatchObject({ differs: false });
151+
expect(diff('ezpl.tear_off : 0\n')[0]).toMatchObject({ differs: false });
152+
});
153+
154+
it('refuses a printer name no setup script could carry', () => {
155+
expect(isAdoptable(diff('device.friendly_name : a name with a comma, and more\n')[0]!)).toBe(false);
156+
});
157+
158+
it('refuses a head density the app has no setting for', () => {
159+
expect(isAdoptable(diff('head.resolution.in_dpi : 400\n')[0]!)).toBe(false);
160+
});
161+
});
162+
163+
describe('adoptionPatches', () => {
164+
it('folds the chosen rows into one patch per target', () => {
165+
const rows = diff().filter((r) => r.field === 'printSpeed' || r.field === 'printerName');
166+
expect(adoptionPatches(rows)).toEqual({ label: { printSpeed: 6 }, profile: { printerName: 'D4J260700032' } });
167+
});
168+
});
169+
170+
describe('preselectedRows', () => {
171+
it('ticks the settings the printer and a fresh label disagree on', () => {
172+
expect(ticked()).toEqual(expect.arrayContaining(['instantDarkness', 'widthMm', 'heightMm', 'labelTop']));
173+
});
174+
175+
it('leaves a setting the app already holds unticked', () => {
176+
const fields = ticked({ ...LABEL, labelTop: 30, printSpeed: 6 });
177+
expect(fields).not.toContain('labelTop');
178+
expect(fields).not.toContain('printSpeed');
179+
});
180+
181+
it('leaves a row the app cannot hold unticked', () => {
182+
expect(preselectedRows(diff('media.printmode : cutter\n'))).toEqual([]);
183+
});
184+
185+
it('leaves a printer value an unset field already stands for unticked', () => {
186+
const fields = ticked();
187+
expect(fields).not.toContain('labelShift');
188+
expect(fields).not.toContain('printOrientation');
189+
expect(fields).not.toContain('backfeedSequence');
190+
expect(fields).not.toContain('tearOffAdjust');
191+
expect(fields).not.toContain('zplMode');
192+
});
193+
194+
it('leaves the serial number the printer carries as a name unticked', () => {
195+
expect(ticked()).not.toContain('printerName');
196+
});
197+
198+
it('ticks a name the profile already holds and the printer spells differently', () => {
199+
expect(ticked(LABEL, { printerName: 'FRONTDESK' })).toContain('printerName');
200+
});
201+
});

0 commit comments

Comments
 (0)