From 7d680e469fc26094b244fc0ad485c2009460c83a Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Tue, 17 Jun 2025 21:26:28 -0500 Subject: [PATCH] switch printing to command pattern --- server/src/db/repositories/base-repository.ts | 2 + server/src/db/repositories/step-repository.ts | 11 +- .../printer/__tests__/barcode-debug.test.ts | 98 +++++++ .../printer/__tests__/barcode-utils.test.ts | 167 +++++++++++ .../printer/__tests__/command-system.test.ts | 133 +++++++++ .../printer/__tests__/format-utils.test.ts | 163 +++-------- server/src/printer/__tests__/printer.test.ts | 111 ++++++++ .../printer/__tests__/step-data-debug.test.ts | 51 ++++ server/src/printer/barcode-utils.ts | 106 +++++++ server/src/printer/command-executor.ts | 266 ++++++++++++++++++ server/src/printer/format-utils.ts | 96 ++++--- server/src/printer/index.ts | 3 + server/src/printer/printer-commands.ts | 148 ++++++++++ server/src/printer/printer-constants.ts | 5 +- server/src/printer/serial-printer.ts | 115 ++++---- server/src/printer/test-printer.ts | 76 ++++- 16 files changed, 1311 insertions(+), 240 deletions(-) create mode 100644 server/src/printer/__tests__/barcode-debug.test.ts create mode 100644 server/src/printer/__tests__/barcode-utils.test.ts create mode 100644 server/src/printer/__tests__/command-system.test.ts create mode 100644 server/src/printer/__tests__/step-data-debug.test.ts create mode 100644 server/src/printer/barcode-utils.ts create mode 100644 server/src/printer/command-executor.ts create mode 100644 server/src/printer/printer-commands.ts diff --git a/server/src/db/repositories/base-repository.ts b/server/src/db/repositories/base-repository.ts index 65f3950..e49dbda 100644 --- a/server/src/db/repositories/base-repository.ts +++ b/server/src/db/repositories/base-repository.ts @@ -1,4 +1,5 @@ import { Knex } from 'knex'; +import logger from '../../logger'; export abstract class BaseRepository { protected tableName: string; @@ -16,6 +17,7 @@ export abstract class BaseRepository { const result = await this.db(this.tableName).where('id', id).first(); + logger.info(`findById(${id}) for table ${this.tableName}:`, result); return result || null; } diff --git a/server/src/db/repositories/step-repository.ts b/server/src/db/repositories/step-repository.ts index 8210138..314b094 100644 --- a/server/src/db/repositories/step-repository.ts +++ b/server/src/db/repositories/step-repository.ts @@ -1,6 +1,7 @@ import { Knex } from 'knex'; import { BaseRepository } from './base-repository'; import { Step, Task } from '@shared/index'; +import logger from '../../logger'; export class StepRepository extends BaseRepository { constructor(db: Knex) { @@ -8,10 +9,18 @@ export class StepRepository extends BaseRepository { } async findByTaskId(taskId: number): Promise { - return await this.db(this.tableName) + const steps = await this.db(this.tableName) .where('task_id', taskId) .orderBy('order') .select('*'); + + logger.info(`Retrieved ${steps.length} steps for task ${taskId}`); + for (const step of steps) { + logger.info(`Step ${step.id}: name="${step.name}", instructions="${step.instructions}"`); + logger.info(`Full step data:`, JSON.stringify(step, null, 2)); + } + + return steps; } async incrementPrintCount(id: number): Promise { diff --git a/server/src/printer/__tests__/barcode-debug.test.ts b/server/src/printer/__tests__/barcode-debug.test.ts new file mode 100644 index 0000000..2669471 --- /dev/null +++ b/server/src/printer/__tests__/barcode-debug.test.ts @@ -0,0 +1,98 @@ +import { CommandBuilder } from '../printer-commands'; +import { TestCommandExecutor } from '../command-executor'; +import { BARCODE_CONFIG } from '../printer-constants'; + +describe('Barcode Debug', () => { + let commandExecutor: TestCommandExecutor; + + beforeEach(() => { + commandExecutor = new TestCommandExecutor(); + }); + + it('should test different barcode formats', async () => { + const testData = '123456789'; + + // Test basic barcode + const commands = [ + CommandBuilder.text('Testing barcode:'), + CommandBuilder.barcode(testData), + CommandBuilder.newline(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + console.log('Barcode test output:', output); + expect(output).toContain('[BARCODE: 123456789]'); + }); + + it('should test task barcode', async () => { + const taskId = 123; + + const commands = [ + CommandBuilder.text('Testing task barcode:'), + CommandBuilder.taskBarcode(taskId), + CommandBuilder.newline(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + console.log('Task barcode test output:', output); + expect(output).toContain('[BARCODE: 100000123]'); + }); + + it('should test step barcode', async () => { + const stepId = 456; + + const commands = [ + CommandBuilder.text('Testing step barcode:'), + CommandBuilder.stepBarcode(stepId), + CommandBuilder.newline(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + console.log('Step barcode test output:', output); + expect(output).toContain('[BARCODE: 200000456]'); + }); + + it('should test barcode with alignment', async () => { + const testData = '987654321'; + + const commands = [ + CommandBuilder.text('Testing barcode with alignment:'), + ...CommandBuilder.barcodeWithAlignment(testData), + CommandBuilder.newline(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + console.log('Barcode with alignment test output:', output); + expect(output).toContain('[BARCODE: 987654321]'); + }); + + it('should test task barcode with alignment', async () => { + const taskId = 789; + + const commands = [ + CommandBuilder.text('Testing task barcode with alignment:'), + ...CommandBuilder.taskBarcodeWithAlignment(taskId), + CommandBuilder.newline(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + console.log('Task barcode with alignment test output:', output); + expect(output).toContain('[BARCODE: 100000789]'); + }); + + it('should test barcode configuration', () => { + console.log('Barcode config:', BARCODE_CONFIG); + expect(BARCODE_CONFIG.TYPE).toBe('CODE39'); + expect(BARCODE_CONFIG.ALTERNATIVE_TYPES).toContain('CODE128'); + }); +}); \ No newline at end of file diff --git a/server/src/printer/__tests__/barcode-utils.test.ts b/server/src/printer/__tests__/barcode-utils.test.ts new file mode 100644 index 0000000..cc39bf0 --- /dev/null +++ b/server/src/printer/__tests__/barcode-utils.test.ts @@ -0,0 +1,167 @@ +import { BarcodeUtils, BarcodeData } from '../barcode-utils'; + +describe('BarcodeUtils', () => { + describe('parseBarcode', () => { + it('should parse valid task barcode', () => { + const result = BarcodeUtils.parseBarcode('100000123'); + expect(result).toEqual({ type: 'TASK', id: 123 }); + }); + + it('should parse valid step barcode', () => { + const result = BarcodeUtils.parseBarcode('200000456'); + expect(result).toEqual({ type: 'STEP', id: 456 }); + }); + + it('should parse barcode with non-numeric characters', () => { + const result = BarcodeUtils.parseBarcode('1ABC000123DEF'); + expect(result).toEqual({ type: 'TASK', id: 123 }); + }); + + it('should return null for invalid entity type', () => { + const result = BarcodeUtils.parseBarcode('300000123'); + expect(result).toBeNull(); + }); + + it('should return null for too short barcode', () => { + const result = BarcodeUtils.parseBarcode('1'); + expect(result).toBeNull(); + }); + + it('should return null for invalid ID', () => { + const result = BarcodeUtils.parseBarcode('100000000'); + expect(result).toBeNull(); + }); + + it('should return null for empty string', () => { + const result = BarcodeUtils.parseBarcode(''); + expect(result).toBeNull(); + }); + + it('should parse large IDs correctly', () => { + const result = BarcodeUtils.parseBarcode('199999999'); + expect(result).toEqual({ type: 'TASK', id: 99999999 }); + }); + }); + + describe('generateTaskBarcode', () => { + it('should generate task barcode', () => { + const result = BarcodeUtils.generateTaskBarcode(123); + expect(result).toBe('100000123'); + }); + + it('should generate task barcode with padding', () => { + const result = BarcodeUtils.generateTaskBarcode(1); + expect(result).toBe('100000001'); + }); + }); + + describe('generateStepBarcode', () => { + it('should generate step barcode', () => { + const result = BarcodeUtils.generateStepBarcode(456); + expect(result).toBe('200000456'); + }); + + it('should generate step barcode with padding', () => { + const result = BarcodeUtils.generateStepBarcode(1); + expect(result).toBe('200000001'); + }); + }); + + describe('isValidBarcode', () => { + it('should return true for valid task barcode', () => { + const result = BarcodeUtils.isValidBarcode('100000123'); + expect(result).toBe(true); + }); + + it('should return true for valid step barcode', () => { + const result = BarcodeUtils.isValidBarcode('200000456'); + expect(result).toBe(true); + }); + + it('should return false for invalid barcode', () => { + const result = BarcodeUtils.isValidBarcode('300000123'); + expect(result).toBe(false); + }); + }); + + describe('getEntityType', () => { + it('should return TASK for task barcode', () => { + const result = BarcodeUtils.getEntityType('100000123'); + expect(result).toBe('TASK'); + }); + + it('should return STEP for step barcode', () => { + const result = BarcodeUtils.getEntityType('200000456'); + expect(result).toBe('STEP'); + }); + + it('should return null for invalid barcode', () => { + const result = BarcodeUtils.getEntityType('300000123'); + expect(result).toBeNull(); + }); + }); + + describe('getEntityId', () => { + it('should return ID for task barcode', () => { + const result = BarcodeUtils.getEntityId('100000123'); + expect(result).toBe(123); + }); + + it('should return ID for step barcode', () => { + const result = BarcodeUtils.getEntityId('200000456'); + expect(result).toBe(456); + }); + + it('should return null for invalid barcode', () => { + const result = BarcodeUtils.getEntityId('300000123'); + expect(result).toBeNull(); + }); + }); + + describe('getMaxId', () => { + it('should return maximum supported ID', () => { + const result = BarcodeUtils.getMaxId(); + expect(result).toBe(99999999); + }); + }); + + describe('isValidId', () => { + it('should return true for valid ID', () => { + const result = BarcodeUtils.isValidId(123); + expect(result).toBe(true); + }); + + it('should return false for zero ID', () => { + const result = BarcodeUtils.isValidId(0); + expect(result).toBe(false); + }); + + it('should return false for negative ID', () => { + const result = BarcodeUtils.isValidId(-1); + expect(result).toBe(false); + }); + + it('should return false for ID too large', () => { + const result = BarcodeUtils.isValidId(100000000); + expect(result).toBe(false); + }); + }); + + describe('round-trip tests', () => { + it('should generate and parse task barcode correctly', () => { + const taskId = 789; + const generated = BarcodeUtils.generateTaskBarcode(taskId); + const parsed = BarcodeUtils.parseBarcode(generated); + + expect(parsed).toEqual({ type: 'TASK', id: taskId }); + }); + + it('should generate and parse step barcode correctly', () => { + const stepId = 101; + const generated = BarcodeUtils.generateStepBarcode(stepId); + const parsed = BarcodeUtils.parseBarcode(generated); + + expect(parsed).toEqual({ type: 'STEP', id: stepId }); + }); + }); +}); \ No newline at end of file diff --git a/server/src/printer/__tests__/command-system.test.ts b/server/src/printer/__tests__/command-system.test.ts new file mode 100644 index 0000000..c90ac19 --- /dev/null +++ b/server/src/printer/__tests__/command-system.test.ts @@ -0,0 +1,133 @@ +import { CommandBuilder } from '../printer-commands'; +import { TestCommandExecutor } from '../command-executor'; + +describe('Command System (Standalone)', () => { + let commandExecutor: TestCommandExecutor; + + beforeEach(() => { + commandExecutor = new TestCommandExecutor(); + }); + + it('should execute text commands', async () => { + const commands = [ + CommandBuilder.text('Hello World'), + CommandBuilder.newline(), + CommandBuilder.text('Test'), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('Hello World'); + expect(output).toContain('Test'); + }); + + it('should execute header commands', async () => { + const commands = [ + CommandBuilder.header('Test Header'), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Test Header'); + }); + + it('should execute banner commands', async () => { + const commands = [ + CommandBuilder.banner('=', 10), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('=========='); + }); + + it('should execute barcode commands', async () => { + const commands = [ + CommandBuilder.barcode('123456'), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[BARCODE: 123456]'); + }); + + it('should execute section commands', async () => { + const commands = [ + CommandBuilder.section('Test Section', 'Test Content', '-', true), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Test Section'); + expect(output).toContain('Test Content'); + }); + + it('should execute step header commands', async () => { + const commands = [ + CommandBuilder.stepHeader('Test Step', 1, 'Test Task', true), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Step 1: Test Step'); + }); + + it('should execute cut commands', async () => { + const commands = [ + CommandBuilder.cut(true, 4), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('--- CUT ---'); + }); + + it('should execute complex command sequences', async () => { + const commands = [ + CommandBuilder.header('Complex Test'), + CommandBuilder.banner('=', 20), + CommandBuilder.text('Some content'), + CommandBuilder.newline(), + CommandBuilder.barcode('123'), + CommandBuilder.cut(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Complex Test'); + expect(output).toContain('===================='); + expect(output).toContain('Some content'); + expect(output).toContain('[BARCODE: 123]'); + expect(output).toContain('--- CUT ---'); + }); + + it('should handle empty command arrays', async () => { + const commands: any[] = []; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toBe('\n'); + }); + + it('should handle list commands', async () => { + const commands = [ + CommandBuilder.list(['Item 1', 'Item 2', 'Item 3'], 1, '- '), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('- [ ] 1: Item 1'); + expect(output).toContain('- [ ] 2: Item 2'); + expect(output).toContain('- [ ] 3: Item 3'); + }); +}); \ No newline at end of file diff --git a/server/src/printer/__tests__/format-utils.test.ts b/server/src/printer/__tests__/format-utils.test.ts index 053038c..9abc3a6 100644 --- a/server/src/printer/__tests__/format-utils.test.ts +++ b/server/src/printer/__tests__/format-utils.test.ts @@ -1,144 +1,51 @@ import { formatUtils } from '../format-utils'; +import { Command } from '../printer-commands'; describe('formatUtils', () => { - describe('createBanner', () => { - it('should create a banner with default length', () => { - const banner = formatUtils.createBanner('='); - expect(banner).toBe('='.repeat(40)); - }); - - it('should create a banner with custom length', () => { - const banner = formatUtils.createBanner('-', 32); - expect(banner).toBe('-'.repeat(32)); - }); - - it('should handle empty character', () => { - const banner = formatUtils.createBanner('', 10); - expect(banner).toBe(''); - }); - - it('should handle zero length', () => { - const banner = formatUtils.createBanner('*', 0); - expect(banner).toBe(''); - }); + it('banner returns a BANNER command', () => { + expect(formatUtils.banner('=', 10)).toEqual([Command.BANNER, '=', 10]); }); - describe('formatCheckbox', () => { - it('should format text with checkbox', () => { - const formatted = formatUtils.formatCheckbox('Test Item'); - expect(formatted).toBe('[ ] Test Item'); - }); - - it('should handle empty text', () => { - const formatted = formatUtils.formatCheckbox(''); - expect(formatted).toBe('[ ] '); - }); - - it('should handle text with special characters', () => { - const formatted = formatUtils.formatCheckbox('Test: Item (123)'); - expect(formatted).toBe('[ ] Test: Item (123)'); - }); + it('checkbox returns a CHECKBOX command', () => { + expect(formatUtils.checkbox('Test')).toEqual([Command.CHECKBOX, 'Test']); }); - describe('formatList', () => { - it('should format list without numbering', () => { - const items = ['Item 1', 'Item 2', 'Item 3']; - const formatted = formatUtils.formatList(items); - expect(formatted).toEqual([ - '[ ] Item 1', - '[ ] Item 2', - '[ ] Item 3' - ]); - }); - - it('should format list with numbering', () => { - const items = ['Item 1', 'Item 2', 'Item 3']; - const formatted = formatUtils.formatList(items, 1); - expect(formatted).toEqual([ - '[ ] 1: Item 1', - '[ ] 2: Item 2', - '[ ] 3: Item 3' - ]); - }); - - it('should handle empty list', () => { - const formatted = formatUtils.formatList([]); - expect(formatted).toEqual([]); - }); - - it('should handle list with empty items', () => { - const items = ['', 'Item 2', '']; - const formatted = formatUtils.formatList(items); - expect(formatted).toEqual([ - '[ ] ', - '[ ] Item 2', - '[ ] ' - ]); - }); + it('list returns a LIST command', () => { + expect(formatUtils.list(['A', 'B'], 1, '- ')).toEqual([[Command.LIST, ['A', 'B'], 1, '- ']]); }); - describe('formatSection', () => { - it('should format section with default banner', () => { - const section = formatUtils.formatSection('Header', 'Content'); - expect(section).toEqual([ - '[ ] Header', - '='.repeat(40), - 'Content', - ]); - }); - - it('should format section with custom banner', () => { - const section = formatUtils.formatSection('Header', 'Content', '-'); - expect(section).toEqual([ - '[ ] Header', - '-'.repeat(40), - 'Content', - ]); - }); - - it('should handle empty content', () => { - const section = formatUtils.formatSection('Header', ''); - expect(section).toEqual([ - '[ ] Header', - '='.repeat(40), - '', - ]); - }); - - it('should handle empty header', () => { - const section = formatUtils.formatSection('', 'Content'); - expect(section).toEqual([ - '[ ] ', - '='.repeat(40), - 'Content', - ]); - }); + it('stepHeader returns a CHECKBOX command with formatted text', () => { + expect(formatUtils.stepHeader('StepName', 2, 'TaskName', true)).toEqual([Command.CHECKBOX, 'Step 2: StepName']); + expect(formatUtils.stepHeader('StepName', 2, 'TaskName', false)).toEqual([Command.CHECKBOX, 'Step 2 of TaskName: StepName']); }); - describe('formatStepHeader', () => { - it('should format step header with just step name and number', () => { - const header = formatUtils.formatStepHeader('Test Step', 1); - expect(header).toBe('Step 1: Test Step'); - }); + it('section returns a list of commands', () => { + expect(formatUtils.section('Header', 'Content')).toEqual([ + [Command.CHECKBOX, 'Header'], + [Command.BANNER, '='], + [Command.TEXT, 'Content'], + ]); + expect(formatUtils.section('Header', 'Content', '-', true)).toEqual([ + [Command.CHECKBOX, 'Header'], + [Command.BANNER, '-'], + [Command.TEXT, 'Content'], + [Command.NEWLINE], + ]); + }); - it('should format step header with step number and task name in single step view', () => { - const header = formatUtils.formatStepHeader('Test Step', 1, 'Test Task'); - expect(header).toBe('Step 1 of Test Task: Test Step'); - }); + it('getCheckboxText extracts text from a CHECKBOX command', () => { + expect(formatUtils.getCheckboxText([Command.CHECKBOX, 'Hello'])).toBe('Hello'); + expect(formatUtils.getCheckboxText([Command.BANNER, '='])).toBe(''); + }); - it('should format step header with step number but no task name in task view', () => { - const header = formatUtils.formatStepHeader('Test Step', 1, 'Test Task', true); - expect(header).toBe('Step 1: Test Step'); - }); + it('taskHeader returns a CHECKBOX command for the task', () => { + expect(formatUtils.taskHeader('My Task')).toEqual([Command.CHECKBOX, 'Task: My Task']); + }); - it('should handle empty step name', () => { - const header = formatUtils.formatStepHeader('', 1); - expect(header).toBe('Step 1: '); - }); - - it('should handle zero step number', () => { - const header = formatUtils.formatStepHeader('Test Step', 0); - expect(header).toBe('Step 0: Test Step'); - }); + it('taskSection returns a list of commands for the task section', () => { + expect(formatUtils.taskSection('My Task')).toEqual([ + [Command.CHECKBOX, 'Task: My Task'], + [Command.BANNER, '='], + ]); }); }); \ No newline at end of file diff --git a/server/src/printer/__tests__/printer.test.ts b/server/src/printer/__tests__/printer.test.ts index 6a2246a..27abc4a 100644 --- a/server/src/printer/__tests__/printer.test.ts +++ b/server/src/printer/__tests__/printer.test.ts @@ -3,6 +3,8 @@ import { TestPrinter } from '../index'; import { Task, Step } from '@shared/index'; import { InMemoryPrintHistoryRepository, InMemoryStepRepository } from '../../db/repositories/in-memory-repository'; import { Knex } from 'knex'; +import { CommandBuilder } from '../printer-commands'; +import { TestCommandExecutor } from '../command-executor'; describe('Printer', () => { let printHistoryRepo: InMemoryPrintHistoryRepository; @@ -40,4 +42,113 @@ describe('Printer', () => { printed_at: expect.any(Date), }); }); +}); + +describe('Command System', () => { + let commandExecutor: TestCommandExecutor; + + beforeEach(() => { + commandExecutor = new TestCommandExecutor(); + }); + + it('should execute text commands', async () => { + const commands = [ + CommandBuilder.text('Hello World'), + CommandBuilder.newline(), + CommandBuilder.text('Test'), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('Hello World'); + expect(output).toContain('Test'); + }); + + it('should execute header commands', async () => { + const commands = [ + CommandBuilder.header('Test Header'), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Test Header'); + }); + + it('should execute banner commands', async () => { + const commands = [ + CommandBuilder.banner('=', 10), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('=========='); + }); + + it('should execute barcode commands', async () => { + const commands = [ + CommandBuilder.barcode('123456'), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[BARCODE: 123456]'); + }); + + it('should execute section commands', async () => { + const commands = [ + CommandBuilder.section('Test Section', 'Test Content', '-', true), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Test Section'); + expect(output).toContain('Test Content'); + }); + + it('should execute step header commands', async () => { + const commands = [ + CommandBuilder.stepHeader('Test Step', 1, 'Test Task', true), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Step 1: Test Step'); + }); + + it('should execute cut commands', async () => { + const commands = [ + CommandBuilder.cut(true, 4), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('--- CUT ---'); + }); + + it('should execute complex command sequences', async () => { + const commands = [ + CommandBuilder.header('Complex Test'), + CommandBuilder.banner('=', 20), + CommandBuilder.text('Some content'), + CommandBuilder.newline(), + CommandBuilder.barcode('123'), + CommandBuilder.cut(), + ]; + + await commandExecutor.executeCommands(commands); + const output = commandExecutor.getOutput(); + + expect(output).toContain('[ ] Complex Test'); + expect(output).toContain('===================='); + expect(output).toContain('Some content'); + expect(output).toContain('[BARCODE: 123]'); + expect(output).toContain('--- CUT ---'); + }); }); \ No newline at end of file diff --git a/server/src/printer/__tests__/step-data-debug.test.ts b/server/src/printer/__tests__/step-data-debug.test.ts new file mode 100644 index 0000000..a0dd07e --- /dev/null +++ b/server/src/printer/__tests__/step-data-debug.test.ts @@ -0,0 +1,51 @@ +import { Step } from '@shared/index'; + +describe('Step Data Debug', () => { + it('should test step data structure', () => { + // Test with sample step data that matches the database + const step: Step = { + id: 10, + name: 'Counter', + instructions: 'Was hit', + task_id: 20, + order: 1, + print_count: 9, + created_at: new Date('2025-06-15T01:55:41.121Z'), + updated_at: new Date('2025-06-15T01:55:41.121Z'), + }; + + console.log(`Step ${step.id}:`); + console.log(` name: "${step.name}"`); + console.log(` instructions: "${step.instructions}"`); + console.log(` task_id: ${step.task_id}`); + console.log(` order: ${step.order}`); + console.log(` print_count: ${step.print_count}`); + console.log(` created_at: ${step.created_at}`); + console.log(` updated_at: ${step.updated_at}`); + + expect(step.instructions).toBe('Was hit'); + expect(typeof step.instructions).toBe('string'); + }); + + it('should test step data with different instructions', () => { + const step: Step = { + id: 11, + name: 'Sink', + instructions: 'asdf', + task_id: 20, + order: 2, + print_count: 2, + created_at: new Date('2025-06-15T01:55:46.466Z'), + updated_at: new Date('2025-06-15T01:55:46.466Z'), + }; + + console.log(`Step ${step.id}:`); + console.log(` name: "${step.name}"`); + console.log(` instructions: "${step.instructions}"`); + console.log(` task_id: ${step.task_id}`); + console.log(` order: ${step.order}`); + + expect(step.instructions).toBe('asdf'); + expect(typeof step.instructions).toBe('string'); + }); +}); \ No newline at end of file diff --git a/server/src/printer/barcode-utils.ts b/server/src/printer/barcode-utils.ts new file mode 100644 index 0000000..90ac842 --- /dev/null +++ b/server/src/printer/barcode-utils.ts @@ -0,0 +1,106 @@ +export interface BarcodeData { + type: 'TASK' | 'STEP'; + id: number; +} + +export class BarcodeUtils { + // Entity type codes - using numeric prefixes + private static readonly TASK_PREFIX = 1; + private static readonly STEP_PREFIX = 2; + + // Format: [entity_type][padding][id] + // Example: 100000123 for task 123, 200000456 for step 456 + private static readonly ID_PADDING = 4; // 4 digits for ID + + /** + * Parse barcode data to extract entity type and ID + * @param barcodeData The raw barcode data (e.g., "100000123" or "200000456") + * @returns Parsed barcode data with type and ID, or null if invalid + */ + static parseBarcode(barcodeData: string): BarcodeData | null { + const numericData = barcodeData.replace(/\D/g, ''); // Remove non-digits + + if (numericData.length < 2) { + return null; + } + + const entityType = parseInt(numericData.charAt(0), 10); + const id = parseInt(numericData.substring(1), 10); + + if (isNaN(id) || id <= 0) { + return null; + } + + if (entityType === this.TASK_PREFIX) { + return { type: 'TASK', id }; + } else if (entityType === this.STEP_PREFIX) { + return { type: 'STEP', id }; + } + + return null; + } + + /** + * Generate barcode data for a task + * @param taskId The task ID + * @returns Formatted barcode data (numeric only) + */ + static generateTaskBarcode(taskId: number): string { + return `${this.TASK_PREFIX}${taskId.toString().padStart(this.ID_PADDING, '0')}`; + } + + /** + * Generate barcode data for a step + * @param stepId The step ID + * @returns Formatted barcode data (numeric only) + */ + static generateStepBarcode(stepId: number): string { + return `${this.STEP_PREFIX}${stepId.toString().padStart(this.ID_PADDING, '0')}`; + } + + /** + * Validate if a barcode data string is valid + * @param barcodeData The barcode data to validate + * @returns True if valid, false otherwise + */ + static isValidBarcode(barcodeData: string): boolean { + return this.parseBarcode(barcodeData) !== null; + } + + /** + * Get the entity type from barcode data + * @param barcodeData The barcode data + * @returns The entity type or null if invalid + */ + static getEntityType(barcodeData: string): 'TASK' | 'STEP' | null { + const parsed = this.parseBarcode(barcodeData); + return parsed?.type || null; + } + + /** + * Get the entity ID from barcode data + * @param barcodeData The barcode data + * @returns The entity ID or null if invalid + */ + static getEntityId(barcodeData: string): number | null { + const parsed = this.parseBarcode(barcodeData); + return parsed?.id || null; + } + + /** + * Get the maximum supported ID for the current format + * @returns Maximum ID value + */ + static getMaxId(): number { + return Math.pow(10, this.ID_PADDING) - 1; + } + + /** + * Check if an ID is within the supported range + * @param id The ID to check + * @returns True if valid, false otherwise + */ + static isValidId(id: number): boolean { + return id > 0 && id <= this.getMaxId(); + } +} \ No newline at end of file diff --git a/server/src/printer/command-executor.ts b/server/src/printer/command-executor.ts new file mode 100644 index 0000000..5a1fefe --- /dev/null +++ b/server/src/printer/command-executor.ts @@ -0,0 +1,266 @@ +import { Printer as EscposPrinter } from '@node-escpos/core'; +import { formatUtils } from './format-utils'; +import { Command, CommandTuple, CommandArray } from './printer-commands'; +import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG } from './printer-constants'; +import logger from '../logger'; + +export interface CommandExecutor { + executeCommands(commands: CommandArray): Promise; +} + +export class SerialCommandExecutor implements CommandExecutor { + constructor(private printer: EscposPrinter<[]>) {} + + async executeCommands(commands: CommandArray): Promise { + for (const command of commands) { + await this.executeCommand(command); + } + } + + private async executeCommand(commandTuple: CommandTuple): Promise { + const [command, ...params] = commandTuple; + + switch (command) { + case Command.TEXT: + await this.printer.text(params[0] as string); + break; + + case Command.HEADER: + await this.printer + .font(FONT.DEFAULT) + .align(ALIGNMENT.CENTER) + .style(STYLE.BOLD) + .size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height) + .text(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); + break; + + case Command.BANNER: + const char = params[0] as string; + const length = params[1] as number; + await this.printer.text(formatUtils.bannerString(char, length)); + break; + + case Command.NEWLINE: + await this.printer.text(''); + break; + + case Command.BARCODE: + const barcodeData = params[0] as string; + logger.info(`Printing barcode: ${barcodeData} with type: ${BARCODE_CONFIG.TYPE}`); + + // Try multiple barcode formats until one works + const barcodeTypes = [BARCODE_CONFIG.TYPE, ...BARCODE_CONFIG.ALTERNATIVE_TYPES]; + let barcodePrinted = false; + + for (const barcodeType of barcodeTypes) { + try { + logger.info(`Trying barcode type: ${barcodeType}`); + await this.printer.barcode( + barcodeData, + barcodeType, + BARCODE_CONFIG.DIMENSIONS + ); + logger.info(`Successfully printed barcode with type: ${barcodeType}`); + barcodePrinted = true; + break; + } catch (error) { + logger.warn(`Barcode type ${barcodeType} failed: ${error}`); + continue; + } + } + + if (!barcodePrinted) { + logger.error(`All barcode types failed for data: ${barcodeData}`); + // As a last resort, just print the data as text + await this.printer.text(`[BARCODE: ${barcodeData}]`); + } + break; + + case Command.FONT_SIZE: + const size = params[0] as typeof FONT_SIZES[keyof typeof FONT_SIZES]; + await this.printer.size(size.width, size.height); + break; + + case Command.ALIGN: + await this.printer.align(params[0] as any); + break; + + case Command.FONT_FAMILY: + await this.printer.font(params[0] as any); + break; + + case Command.STYLE: + await this.printer.style(params[0] as any); + break; + + case Command.CUT: + const partial = params[0] as boolean; + const lines = params[1] as number; + await this.printer.cut(partial, lines); + break; + + case Command.SECTION: + const header = params[0] as string; + const content = params[1] as string; + const bannerChar = params[2] as string; + const trailingNewline = params[3] as boolean; + const section = formatUtils.section(header, content, bannerChar, trailingNewline); + for (const cmd of section) { + if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) { + await this.printer.text(cmd[1] as string); + } else if (cmd[0] === Command.BANNER) { + await this.printer.text(formatUtils.bannerString(cmd[1] as string, cmd[2] as number)); + } else if (cmd[0] === Command.NEWLINE) { + await this.printer.text(''); + } + } + break; + + case Command.STEP_HEADER: + const stepName = params[0] as string; + const stepNumber = params[1] as number; + const taskName = params[2] as string; + const isTaskView = params[3] as boolean; + const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView); + await this.printer.text(formatUtils.getCheckboxText(stepHeader)); + break; + + case Command.CHECKBOX: + await this.printer.text(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); + break; + + case Command.LIST: + const items = params[0] as string[]; + const startIndex = params[1] as number; + const prefix = params[2] as string; + const listCmds = formatUtils.list(items, startIndex, prefix); + for (const cmd of listCmds) { + if (cmd[0] === Command.LIST) { + // Render as text for test printer, or as needed for real printer + for (let i = 0; i < items.length; i++) { + const num = startIndex > 0 ? `${i + startIndex}: ` : ''; + const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`)); + await this.printer.text(`${prefix}${body}`); + } + } + } + break; + + default: + throw new Error(`Unknown command: ${command}`); + } + } +} + +export class TestCommandExecutor implements CommandExecutor { + private output: string[] = []; + + async executeCommands(commands: CommandArray): Promise { + this.output = []; + + for (const command of commands) { + await this.executeCommand(command); + } + } + + private async executeCommand(commandTuple: CommandTuple): Promise { + const [command, ...params] = commandTuple; + + switch (command) { + case Command.TEXT: + this.output.push(params[0] as string); + break; + + case Command.HEADER: + this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); + break; + + case Command.BANNER: + const char = params[0] as string; + const length = params[1] as number; + this.output.push(formatUtils.banner(char, length)[1] as string); + break; + + case Command.NEWLINE: + this.output.push(''); + break; + + case Command.BARCODE: + this.output.push(`[BARCODE: ${params[0] as string}]`); + break; + + case Command.FONT_SIZE: + // Test printer ignores font size changes + break; + + case Command.ALIGN: + // Test printer ignores alignment changes + break; + + case Command.FONT_FAMILY: + // Test printer ignores font changes + break; + + case Command.STYLE: + // Test printer ignores style changes + break; + + case Command.CUT: + this.output.push('--- CUT ---'); + break; + + case Command.SECTION: + const header = params[0] as string; + const content = params[1] as string; + const bannerChar = params[2] as string; + const trailingNewline = params[3] as boolean; + const section = formatUtils.section(header, content, bannerChar, trailingNewline); + for (const cmd of section) { + if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) { + this.output.push(cmd[1] as string); + } else if (cmd[0] === Command.BANNER) { + this.output.push(formatUtils.bannerString(cmd[1] as string, cmd[2] as number)); + } else if (cmd[0] === Command.NEWLINE) { + this.output.push(''); + } + } + break; + + case Command.STEP_HEADER: + const stepName = params[0] as string; + const stepNumber = params[1] as number; + const taskName = params[2] as string; + const isTaskView = params[3] as boolean; + const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView); + this.output.push(formatUtils.getCheckboxText(stepHeader)); + break; + + case Command.CHECKBOX: + this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); + break; + + case Command.LIST: + const items = params[0] as string[]; + const startIndex = params[1] as number; + const prefix = params[2] as string; + const listCmds = formatUtils.list(items, startIndex, prefix); + for (const cmd of listCmds) { + if (cmd[0] === Command.LIST) { + for (let i = 0; i < items.length; i++) { + const num = startIndex > 0 ? `${i + startIndex}: ` : ''; + const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`)); + this.output.push(`${prefix}${body}`); + } + } + } + break; + + default: + throw new Error(`Unknown command: ${command}`); + } + } + + getOutput(): string { + return this.output.join('\n') + '\n'; + } +} \ No newline at end of file diff --git a/server/src/printer/format-utils.ts b/server/src/printer/format-utils.ts index d1df82e..8610908 100644 --- a/server/src/printer/format-utils.ts +++ b/server/src/printer/format-utils.ts @@ -1,48 +1,39 @@ import { PAPER_CONFIG } from "./printer-constants"; +import { Command, CommandTuple } from './printer-commands'; export const formatUtils = { /** - * Creates a banner line with the specified character - * @param char Character to repeat - * @param length Length of the banner - * @returns Formatted banner string + * Creates a banner command */ - createBanner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string { + banner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): CommandTuple { + return [Command.BANNER, char, length]; + }, + + /** + * Creates a banner string (repeated character) + */ + bannerString(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string { return char.repeat(length); }, /** - * Formats a checkbox item with the given text - * @param text Text to display after the checkbox - * @returns Formatted checkbox string + * Checkbox command */ - formatCheckbox(text: string): string { - return `[ ] ${text}`; + checkbox(text: string): CommandTuple { + return [Command.CHECKBOX, text]; }, /** - * Formats a list of items into lines with optional numbering - * @param items List of items to format - * @param startIndex Starting index for numbering (0 for no numbers) - * @returns Array of formatted lines + * List of items as commands */ - formatList(items: string[], startIndex: number = 0, prefix: string = ''): string[] { - return items.map((item, index) => { - const num = startIndex > 0 ? `${index + startIndex}: ` : ''; - const body = this.formatCheckbox(`${num}${item}`); - return `${prefix}${body}`; - }); + list(items: string[], startIndex: number = 0, prefix: string = ''): CommandTuple[] { + return [[Command.LIST, items, startIndex, prefix]]; }, /** - * Formats a step header with task context - * @param stepName Name of the step - * @param stepNumber Step number (1-based) - * @param taskName Name of the parent task (only used in single step view) - * @param isTaskView Whether this is being used in a task view - * @returns Formatted step header + * Step header as a command */ - formatStepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): string { + stepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): CommandTuple { const parts = ['Step']; if (stepNumber !== undefined) { parts.push(' '); @@ -53,25 +44,48 @@ export const formatUtils = { } parts.push(': '); parts.push(stepName); - return parts.join(''); + return [Command.CHECKBOX, parts.join('')]; }, /** - * Formats a section with a header and content - * @param header Section header - * @param content Section content - * @param bannerChar Character to use for the banner - * @returns Array of formatted lines + * Section as a list of commands */ - formatSection(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): string[] { - const parts = [ - this.formatCheckbox(header), - this.createBanner(bannerChar), - content, + section(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): CommandTuple[] { + const commands: CommandTuple[] = [ + [Command.CHECKBOX, header], + [Command.BANNER, bannerChar], + [Command.TEXT, content], ]; if (trailingNewline) { - parts.push(''); + commands.push([Command.NEWLINE]); } - return parts; - } + return commands; + }, + + /** + * Extracts the text from a CHECKBOX CommandTuple + */ + getCheckboxText(cmd: CommandTuple): string { + if (cmd[0] === Command.CHECKBOX && typeof cmd[1] === 'string') { + return cmd[1]; + } + return ''; + }, + + /** + * Task header as a command + */ + taskHeader(taskName: string): CommandTuple { + return [Command.CHECKBOX, `Task: ${taskName}`]; + }, + + /** + * Task section as a list of commands + */ + taskSection(taskName: string): CommandTuple[] { + return [ + [Command.CHECKBOX, `Task: ${taskName}`], + [Command.BANNER, '='], + ]; + }, }; \ No newline at end of file diff --git a/server/src/printer/index.ts b/server/src/printer/index.ts index e9d5b6d..2eee3e6 100644 --- a/server/src/printer/index.ts +++ b/server/src/printer/index.ts @@ -16,3 +16,6 @@ export function createPrinter(printHistoryRepo: PrintHistoryRepository, stepRepo export { SerialPrinter } from './serial-printer'; export { TestPrinter } from './test-printer'; +export { CommandBuilder, Command, type CommandArray, type CommandTuple } from './printer-commands'; +export { SerialCommandExecutor, TestCommandExecutor, type CommandExecutor } from './command-executor'; +export { BarcodeUtils, type BarcodeData } from './barcode-utils'; diff --git a/server/src/printer/printer-commands.ts b/server/src/printer/printer-commands.ts new file mode 100644 index 0000000..f5f8d74 --- /dev/null +++ b/server/src/printer/printer-commands.ts @@ -0,0 +1,148 @@ +import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG, PAPER_CONFIG } from './printer-constants'; +import { BarcodeUtils } from './barcode-utils'; + +// Command types for printer operations +export enum Command { + // Text commands + TEXT = 'TEXT', + HEADER = 'HEADER', + BANNER = 'BANNER', + NEWLINE = 'NEWLINE', + + // Barcode commands + BARCODE = 'BARCODE', + + // Formatting commands + FONT_SIZE = 'FONT_SIZE', + ALIGN = 'ALIGN', + FONT_FAMILY = 'FONT_FAMILY', + STYLE = 'STYLE', + + // Paper commands + CUT = 'CUT', + + // Section formatting + SECTION = 'SECTION', + STEP_HEADER = 'STEP_HEADER', + CHECKBOX = 'CHECKBOX', + + // List formatting + LIST = 'LIST', +} + +// Command parameter types +export type FontSize = typeof FONT_SIZES[keyof typeof FONT_SIZES]; +export type Alignment = typeof ALIGNMENT[keyof typeof ALIGNMENT]; +export type FontFamily = typeof FONT[keyof typeof FONT]; +export type TextStyle = typeof STYLE[keyof typeof STYLE]; + +// Command tuple type - [Command, ...parameters] +export type CommandTuple = + | [Command.TEXT, string] + | [Command.HEADER, string] + | [Command.BANNER, string, number?] + | [Command.NEWLINE] + | [Command.BARCODE, string] + | [Command.FONT_SIZE, FontSize] + | [Command.ALIGN, Alignment] + | [Command.FONT_FAMILY, FontFamily] + | [Command.STYLE, TextStyle] + | [Command.CUT, boolean, number?] + | [Command.SECTION, string, string, string?, boolean?] + | [Command.STEP_HEADER, string, number, string?, boolean?] + | [Command.CHECKBOX, string] + | [Command.LIST, string[], number?, string?]; + +// Command array type +export type CommandArray = CommandTuple[]; + +// Command builder utility +export class CommandBuilder { + static text(text: string): CommandTuple { + return [Command.TEXT, text]; + } + + static header(text: string): CommandTuple { + return [Command.HEADER, text]; + } + + static banner(char: string = '=', length?: number): CommandTuple { + return [Command.BANNER, char, length]; + } + + static newline(): CommandTuple { + return [Command.NEWLINE]; + } + + static barcode(data: string): CommandTuple { + return [Command.BARCODE, data]; + } + + static taskBarcode(taskId: number): CommandTuple { + return [Command.BARCODE, BarcodeUtils.generateTaskBarcode(taskId)]; + } + + static stepBarcode(stepId: number): CommandTuple { + return [Command.BARCODE, BarcodeUtils.generateStepBarcode(stepId)]; + } + + static barcodeWithAlignment(data: string, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] { + return [ + [Command.ALIGN, alignment], + [Command.BARCODE, data], + [Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment + ]; + } + + static taskBarcodeWithAlignment(taskId: number, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] { + return [ + [Command.ALIGN, alignment], + [Command.BARCODE, BarcodeUtils.generateTaskBarcode(taskId)], + [Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment + ]; + } + + static stepBarcodeWithAlignment(stepId: number, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] { + return [ + [Command.ALIGN, alignment], + [Command.BARCODE, BarcodeUtils.generateStepBarcode(stepId)], + [Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment + ]; + } + + static fontSize(size: FontSize): CommandTuple { + return [Command.FONT_SIZE, size]; + } + + static align(alignment: Alignment): CommandTuple { + return [Command.ALIGN, alignment]; + } + + static font(font: FontFamily): CommandTuple { + return [Command.FONT_FAMILY, font]; + } + + static style(style: TextStyle): CommandTuple { + return [Command.STYLE, style]; + } + + static cut(partial: boolean = true, lines: number = PAPER_CONFIG.CUT_LINES): CommandTuple { + return [Command.CUT, partial, lines]; + } + + static section(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): CommandTuple { + return [Command.SECTION, header, content, bannerChar, trailingNewline]; + } + + static stepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): CommandTuple { + return [Command.STEP_HEADER, stepName, stepNumber, taskName, isTaskView]; + } + + static checkbox(text: string): CommandTuple { + return [Command.CHECKBOX, text]; + } + + static list(items: string[], startIndex: number = 0, prefix: string = ''): CommandTuple { + return [Command.LIST, items, startIndex, prefix]; + } +} \ No newline at end of file diff --git a/server/src/printer/printer-constants.ts b/server/src/printer/printer-constants.ts index 9dcfd82..4cdf224 100644 --- a/server/src/printer/printer-constants.ts +++ b/server/src/printer/printer-constants.ts @@ -5,13 +5,14 @@ export const PRINTER_CONFIG = { export const FONT_SIZES = { NORMAL: { width: 1, height: 1 }, // 0.08 x 2.13 mm - SMALL: { width: 1, height: 1 }, // Same as normal for now, but can be adjusted if needed + SMALL: { width: 0.5, height: 0.5 }, // Half size LARGE: { width: 2, height: 2 }, // Double size } as const; export const BARCODE_CONFIG = { - TYPE: 'CODE128', + TYPE: 'CODE39', DIMENSIONS: { width: 2, height: 50 }, + ALTERNATIVE_TYPES: ['CODE128', 'EAN13', 'UPC_A'] as const, } as const; export const PAPER_CONFIG = { diff --git a/server/src/printer/serial-printer.ts b/server/src/printer/serial-printer.ts index 23982b9..7c2d9bf 100644 --- a/server/src/printer/serial-printer.ts +++ b/server/src/printer/serial-printer.ts @@ -5,6 +5,8 @@ import { StepRepository, PrintHistoryRepository } from '../db/repositories'; import { Knex } from 'knex'; import logger from '../logger'; import { formatUtils } from './format-utils'; +import { CommandBuilder } from './printer-commands'; +import { SerialCommandExecutor } from './command-executor'; import { PRINTER_CONFIG, FONT_SIZES, @@ -20,6 +22,7 @@ export class SerialPrinter implements PrinterInterface { private printer: Printer<[]> | null = null; private printHistoryRepo: PrintHistoryRepository; private stepRepository: StepRepository; + private commandExecutor: SerialCommandExecutor | null = null; constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) { this.printHistoryRepo = printHistoryRepo; @@ -32,6 +35,7 @@ export class SerialPrinter implements PrinterInterface { this.device = new USB(); const options = { encoding: PRINTER_CONFIG.ENCODING }; this.printer = new Printer(this.device, options); + this.commandExecutor = new SerialCommandExecutor(this.printer); logger.info('Printer device initialized successfully'); } catch (error) { logger.error('Failed to initialize printer device:', error); @@ -80,58 +84,51 @@ export class SerialPrinter implements PrinterInterface { } async printTask(task: Task, db: Knex): Promise { - if (!this.printer || !this.device) { + if (!this.commandExecutor) { throw new Error('Printer not initialized'); } const taskSteps = await this.getTaskSteps(db, task); + logger.info(`Printing task ${task.id} with ${taskSteps.length} steps`); try { await this.openPrinter(); - // Print header with task ID as barcode - await this.printer - .font(FONT.DEFAULT) - .align(ALIGNMENT.CENTER) - .style(STYLE.BOLD) - .size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height) - .text(formatUtils.formatCheckbox(`Task: ${task.name}`)) - .text(formatUtils.createBanner('=', PAPER_CONFIG.BANNER_LENGTH)) - // .text('') - .align(ALIGNMENT.LEFT); + const commands = [ + // Header with task name + CommandBuilder.header(`Task: ${task.name}`), + CommandBuilder.banner('=', PAPER_CONFIG.BANNER_LENGTH), + CommandBuilder.align(ALIGNMENT.LEFT), - // Print task ID as barcode - await this.printer - .barcode(task.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS); - // .text('') - // .text(''); + // Task ID as barcode + ...CommandBuilder.taskBarcodeWithAlignment(task.id), + CommandBuilder.newline(), + CommandBuilder.newline(), + ]; - // Print steps + // Add steps for (let i = 0; i < taskSteps.length; i++) { const step = taskSteps[i]; - const stepSection = formatUtils.formatSection( - formatUtils.formatStepHeader(step.name, i + 1, task.name, true), - step.instructions || 'No instructions provided', - '-' + logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`); + + const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, i + 1, task.name, true)); + commands.push( + CommandBuilder.fontSize(FONT_SIZES.NORMAL), + CommandBuilder.text(headerText), + ...formatUtils.section( + '', + step.instructions || 'No instructions provided', + '-' + ), + CommandBuilder.newline(), + ...CommandBuilder.stepBarcodeWithAlignment(step.id), + CommandBuilder.newline() ); - - await this.printer - .size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height) - .text(stepSection[0]) - .text(stepSection[1]) - .size(FONT_SIZES.SMALL.width, FONT_SIZES.SMALL.height) - .text(stepSection[3]); - // .text(''); - - // Print step ID as barcode - await this.printer - .barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS); - // .text(''); } - await this.printer - .cut(true, PAPER_CONFIG.CUT_LINES); + commands.push(CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES)); + await this.commandExecutor.executeCommands(commands); await this.closePrinter(); logger.info(`Printed task ${task.id}`); @@ -148,10 +145,12 @@ export class SerialPrinter implements PrinterInterface { } async printStep(step: Step, db: Knex): Promise { - if (!this.printer || !this.device) { + if (!this.commandExecutor) { throw new Error('Printer not initialized'); } + logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`); + try { await this.openPrinter(); @@ -159,30 +158,30 @@ export class SerialPrinter implements PrinterInterface { const task = await this.stepRepository.findTaskById(step.id); const stepNumber = await this.stepRepository.findStepNumber(step.id); - const stepSection = formatUtils.formatSection( - formatUtils.formatStepHeader(step.name, stepNumber, task?.name), - step.instructions || 'No instructions provided' + const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, stepNumber, task?.name)); + const stepSection = formatUtils.section( + headerText, + step.instructions || 'No instructions provided', + '-' ); - await this.printer - .font(FONT.DEFAULT) - .align(ALIGNMENT.CENTER) - .style(STYLE.BOLD) - .size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height) - .text(stepSection[0]) - .text(stepSection[1]) - .text('') - .align(ALIGNMENT.LEFT) - .size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height) - .text(stepSection[3]) - .text('') - .text(''); - - // Print step ID as barcode - await this.printer - .barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS) - .cut(true, PAPER_CONFIG.CUT_LINES); + const commands = [ + CommandBuilder.font(FONT.DEFAULT), + CommandBuilder.align(ALIGNMENT.CENTER), + CommandBuilder.style(STYLE.BOLD), + CommandBuilder.fontSize(FONT_SIZES.NORMAL), + CommandBuilder.text(headerText), + CommandBuilder.newline(), + CommandBuilder.align(ALIGNMENT.LEFT), + CommandBuilder.fontSize(FONT_SIZES.NORMAL), + CommandBuilder.text(step.instructions || 'No instructions provided'), + CommandBuilder.newline(), + CommandBuilder.newline(), + ...CommandBuilder.stepBarcodeWithAlignment(step.id), + CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES), + ]; + await this.commandExecutor.executeCommands(commands); await this.closePrinter(); logger.info(`Printed step ${step.id}`); diff --git a/server/src/printer/test-printer.ts b/server/src/printer/test-printer.ts index 7786f51..b55c036 100644 --- a/server/src/printer/test-printer.ts +++ b/server/src/printer/test-printer.ts @@ -5,16 +5,27 @@ import { StepRepository, PrintHistoryRepository } from '../db/repositories'; import { Knex } from 'knex'; import logger from '../logger'; import { formatUtils } from './format-utils'; +import { CommandBuilder } from './printer-commands'; +import { TestCommandExecutor } from './command-executor'; +import { + PAPER_CONFIG, + FONT_SIZES, + ALIGNMENT, + FONT, + STYLE, +} from './printer-constants'; export class TestPrinter implements Printer { private readonly outputDir: string; private printHistoryRepo: PrintHistoryRepository; private stepRepository: StepRepository; + private commandExecutor: TestCommandExecutor; constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) { this.outputDir = path.join(process.cwd(), 'test-output'); this.printHistoryRepo = printHistoryRepo; this.stepRepository = stepRepo; + this.commandExecutor = new TestCommandExecutor(); this.ensureOutputDir(); } @@ -35,12 +46,41 @@ export class TestPrinter implements Printer { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = path.join(this.outputDir, `task-${task.id}-${timestamp}.txt`); - const content = [ - ...formatUtils.formatSection(`Task: ${task.name}`, ''), - ...formatUtils.formatList(taskSteps.map((step, index) => - formatUtils.formatStepHeader(step.name, index + 1, task.name, true) - ), 0, '- '), - ].join('\n') + '\n'; + const commands = [ + // Header with task name + CommandBuilder.header(`Task: ${task.name}`), + CommandBuilder.banner('=', PAPER_CONFIG.BANNER_LENGTH), + CommandBuilder.align(ALIGNMENT.LEFT), + + // Task ID as barcode + ...CommandBuilder.taskBarcodeWithAlignment(task.id), + CommandBuilder.newline(), + ]; + + // Add steps + for (let i = 0; i < taskSteps.length; i++) { + const step = taskSteps[i]; + logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`); + + const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, i + 1, task.name, true)); + commands.push( + CommandBuilder.fontSize(FONT_SIZES.NORMAL), + CommandBuilder.text(headerText), + CommandBuilder.newline(), + ...formatUtils.section( + '', + step.instructions || 'No instructions provided', + '-' + ), + ...CommandBuilder.stepBarcodeWithAlignment(step.id), + CommandBuilder.newline() + ); + } + + commands.push(CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES)); + + await this.commandExecutor.executeCommands(commands); + const content = this.commandExecutor.getOutput(); await fs.writeFile(filename, content); logger.info(`Printed task ${task.id} to ${filename}`); @@ -61,10 +101,26 @@ export class TestPrinter implements Printer { const task = await this.stepRepository.findTaskById(step.id); const stepNumber = await this.stepRepository.findStepNumber(step.id); - const content = formatUtils.formatSection( - formatUtils.formatStepHeader(step.name, stepNumber, task?.name), - step.instructions - ).join('\n') + '\n'; + const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, stepNumber, task?.name)); + + const commands = [ + CommandBuilder.font(FONT.DEFAULT), + CommandBuilder.align(ALIGNMENT.CENTER), + CommandBuilder.style(STYLE.BOLD), + CommandBuilder.fontSize(FONT_SIZES.NORMAL), + CommandBuilder.text(headerText), + CommandBuilder.newline(), + CommandBuilder.align(ALIGNMENT.LEFT), + CommandBuilder.fontSize(FONT_SIZES.NORMAL), + CommandBuilder.text(step.instructions || 'No instructions provided'), + CommandBuilder.newline(), + CommandBuilder.newline(), + ...CommandBuilder.stepBarcodeWithAlignment(step.id), + CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES), + ]; + + await this.commandExecutor.executeCommands(commands); + const content = this.commandExecutor.getOutput(); await fs.writeFile(filename, content); logger.info(`Printed step ${step.id} to ${filename}`);