Skip to content

Commit 7497ffd

Browse files
committed
fix(legacy): make M115 parser resilient to blank lines in response
Adventurer 3C Pro firmware inserts a blank line between Machine Name and Firmware fields in the M115 response, causing the fixed-index parser to fail with "null firmware version". Replaces positional array indexing with flexible prefix-based line iteration (matching the approach already used by A3/A4 clients). Also handles SN:/Serial Number: and Tool count:/Tool Count: variants.
1 parent 8b2b1b1 commit 7497ffd

2 files changed

Lines changed: 75 additions & 59 deletions

File tree

src/tcpapi/replays/PrinterInfo.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ Machine Type: Adventurer 5M Pro`;
6464
expect(result).toBeNull();
6565
});
6666

67-
it('should return null when machine type line is malformed', () => {
67+
it('should return null when machine type is missing', () => {
6868
const invalidResponse = `CMD M115 Received.
69-
InvalidLine
7069
Machine Name: MyPrinter
7170
Firmware: V1.2.3
7271
SN: SN123456789
@@ -80,6 +79,49 @@ Mac Address: AA:BB:CC:DD:EE:FF`;
8079
expect(result).toBeNull();
8180
});
8281

82+
it('should handle blank lines in response (Adventurer 3C Pro format)', () => {
83+
const response = `CMD M115 Received.
84+
Machine Type: FlashForge Adventurer III Pro
85+
Machine Name: CowaPrint
86+
87+
Firmware: v2.1.2
88+
SN: SNCCCA95105901
89+
X: 150 Y: 150 Z: 150
90+
Tool Count: 1
91+
Mac Address:88:A9:A7:92:DE:72
92+
93+
ok`;
94+
95+
const printerInfo = new PrinterInfo();
96+
const result = printerInfo.fromReplay(response);
97+
98+
expect(result).not.toBeNull();
99+
expect(result?.TypeName).toBe('FlashForge Adventurer III Pro');
100+
expect(result?.Name).toBe('CowaPrint');
101+
expect(result?.FirmwareVersion).toBe('v2.1.2');
102+
expect(result?.SerialNumber).toBe('SNCCCA95105901');
103+
expect(result?.Dimensions).toBe('X: 150 Y: 150 Z: 150');
104+
expect(result?.ToolCount).toBe('1');
105+
expect(result?.MacAddress).toBe('88:A9:A7:92:DE:72');
106+
});
107+
108+
it('should handle Serial Number: prefix variant', () => {
109+
const response = `CMD M115 Received.
110+
Machine Type: FlashForge Adventurer 3
111+
Machine Name: MyPrinter
112+
Firmware: V1.0.0
113+
Serial Number: SN12345
114+
X:150 Y:150 Z:150
115+
Tool count: 1
116+
Mac Address: AA:BB:CC:DD:EE:FF`;
117+
118+
const printerInfo = new PrinterInfo();
119+
const result = printerInfo.fromReplay(response);
120+
121+
expect(result).not.toBeNull();
122+
expect(result?.SerialNumber).toBe('SN12345');
123+
});
124+
83125
it('should handle extra whitespace in values', () => {
84126
const response = `CMD M115 Received.
85127
Machine Type: Adventurer 5M Pro

src/tcpapi/replays/PrinterInfo.ts

Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,11 @@ export class PrinterInfo {
3232
* provides a piece of information in a "Key: Value" format.
3333
*
3434
* Parsing logic:
35-
* - Splits the replay by newline characters.
36-
* - Line 1 (data[0]): Usually command echo/header, often ignored or assumed specific format.
37-
* - Line 2 (data[1]): Expected to be "Machine Type: [TypeName]". `getRight` extracts the value.
38-
* - Line 3 (data[2]): Expected to be "Machine Name: [Name]". `getRight` extracts the value.
39-
* - Line 4 (data[3]): Expected to be "Firmware: [FirmwareVersion]". `getRight` extracts the value.
40-
* - Line 5 (data[4]): Expected to be "SN: [SerialNumber]". `getRight` extracts the value.
41-
* - Line 6 (data[5]): Expected to be the dimensions string directly (e.g., "X:220 Y:220 Z:220").
42-
* - Line 7 (data[6]): Expected to be "Tool count: [ToolCount]". `getRight` extracts the value.
43-
* - Line 8 (data[7]): Expected to be "Mac Address:[MacAddress]". The prefix is removed.
44-
*
45-
* The `getRight` helper function is used to extract the value part after the colon for several lines.
35+
* - Splits the replay by newline characters, trims each line, and filters out blank lines.
36+
* - Iterates through lines matching known prefixes (Machine Type, Machine Name, Firmware, etc.).
37+
* - This approach is resilient to blank lines, extra whitespace, and minor formatting variations
38+
* across different printer firmware versions (e.g., Adventurer 3C Pro inserts a blank line
39+
* between Machine Name and Firmware).
4640
*
4741
* @param replay The raw multi-line string response from the M115 command.
4842
* @returns The populated `PrinterInfo` instance, or null if parsing fails
@@ -52,47 +46,41 @@ export class PrinterInfo {
5246
if (!replay) return null;
5347

5448
try {
55-
const data = replay.split('\n');
56-
// Assumes data[0] is "CMD M115 Received." or similar header.
49+
const lines = replay
50+
.split('\n')
51+
.map((line) => line.trim())
52+
.filter((line) => line.length > 0 && line !== 'ok');
5753

58-
const name = getRight(data[1]); // Expected: "Machine Type: Adventurer 5M Pro"
59-
if (name === null) {
60-
console.log('PrinterInfo replay has null Machine Type');
61-
return null;
54+
for (const line of lines) {
55+
if (line.startsWith('Machine Type:')) {
56+
this.TypeName = line.replace('Machine Type:', '').trim();
57+
} else if (line.startsWith('Machine Name:')) {
58+
this.Name = line.replace('Machine Name:', '').trim();
59+
} else if (line.startsWith('Firmware:')) {
60+
this.FirmwareVersion = line.replace('Firmware:', '').trim();
61+
} else if (line.startsWith('SN:') || line.startsWith('Serial Number:')) {
62+
this.SerialNumber = line.replace(/^(SN|Serial Number):/, '').trim();
63+
} else if (line.startsWith('Tool Count:') || line.startsWith('Tool count:')) {
64+
this.ToolCount = line.split(':')[1]?.trim() ?? '';
65+
} else if (line.startsWith('Mac Address:')) {
66+
this.MacAddress = line.replace('Mac Address:', '').trim();
67+
} else {
68+
const volumeMatch = line.match(/X:\s*\d+\s+Y:\s*\d+\s+Z:\s*\d+/i);
69+
if (volumeMatch) {
70+
this.Dimensions = line;
71+
}
72+
}
6273
}
63-
this.TypeName = name;
6474

65-
const nick = getRight(data[2]); // Expected: "Machine Name: MyPrinter"
66-
if (nick === null) {
67-
console.log('PrinterInfo replay has null Machine Name');
75+
if (!this.TypeName) {
76+
console.log('PrinterInfo replay has null Machine Type');
6877
return null;
6978
}
70-
this.Name = nick;
71-
72-
const fw = getRight(data[3]); // Expected: "Firmware: V1.2.3"
73-
if (fw === null) {
79+
if (!this.FirmwareVersion) {
7480
console.log('PrinterInfo replay has null firmware version');
7581
return null;
7682
}
77-
this.FirmwareVersion = fw;
78-
79-
const sn = getRight(data[4]); // Expected: "SN: SN12345"
80-
if (sn === null) {
81-
console.log('PrinterInfo replay has null serial number');
82-
return null;
83-
}
84-
this.SerialNumber = sn;
8583

86-
this.Dimensions = data[5].trim(); // Expected: "X:220 Y:220 Z:220" (or similar, directly)
87-
88-
const tcs = getRight(data[6]); // Expected: "Tool count: 1"
89-
if (tcs === null) {
90-
console.log('PrinterInfo replay has null tool count');
91-
return null;
92-
}
93-
this.ToolCount = tcs;
94-
95-
this.MacAddress = data[7].replace('Mac Address:', '').trim(); // Expected: "Mac Address: XX:XX:XX:XX:XX:XX"
9684
return this;
9785
} catch (_error) {
9886
console.log('Error creating PrinterInfo instance from replay');
@@ -130,17 +118,3 @@ export class PrinterInfo {
130118
}
131119
}
132120

133-
/**
134-
* Helper function to extract the value part of a "Key: Value" string.
135-
* It splits the input string by the first colon and returns the trimmed value part.
136-
* @param rpData The input string (e.g., "Machine Type: Adventurer 5M Pro").
137-
* @returns The extracted value string (e.g., "Adventurer 5M Pro"), or null if parsing fails.
138-
* @private
139-
*/
140-
function getRight(rpData: string): string | null {
141-
try {
142-
return rpData.split(':')[1].trim();
143-
} catch {
144-
return null;
145-
}
146-
}

0 commit comments

Comments
 (0)