Skip to content

Commit fa31e7b

Browse files
committed
fix(legacy): parse SN-prefixed serials in M115
Fixes M115 response parsing to handle serial numbers with SN: prefix (some firmware versions report serials this way). Also standardizes A3/A4 clients to use GCodes.CmdLogin constant instead of raw M601 string. Changes: - FlashForgeA3Client/A4Client: Parse SN:-prefixed serial numbers - FlashForgeA3Client/A4Client: Use GCodes.CmdLogin constant for login command - Add test coverage for both changes
1 parent 82d8c02 commit fa31e7b

5 files changed

Lines changed: 60 additions & 14 deletions

File tree

.pi/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"skills": ["../.claude/skills"]
3+
}

src/tcpapi/FlashForgeA3Client.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ describe('FlashForgeA3Client', () => {
2424
expect(client['port']).toBe(8899);
2525
});
2626

27-
it('should initialize connection with M601', async () => {
27+
it('should initialize connection with M601 S1', async () => {
2828
const sendCommandSpy = vi.spyOn(client, 'sendCommandAsync');
2929
sendCommandSpy.mockResolvedValue('CMD M601 Received.\nok\n');
3030

3131
const result = await client.initControl();
3232

33-
expect(sendCommandSpy).toHaveBeenCalledWith('~M601');
33+
expect(sendCommandSpy).toHaveBeenCalledWith('~M601 S1');
3434
expect(result).toBe(true);
3535
});
3636

@@ -72,6 +72,27 @@ describe('FlashForgeA3Client', () => {
7272

7373
await expect(client.getPrinterInfo()).resolves.toBeNull();
7474
});
75+
76+
it('should parse SN-prefixed serial numbers and blank lines in M115', async () => {
77+
const mockResponse = [
78+
'echo: Machine Type: FlashForge Adventurer III',
79+
'Machine Name: MyPrinter',
80+
'',
81+
'Firmware: v1.3.7',
82+
'SN: SNADVA3M12345',
83+
'X: 150 Y: 150 Z: 150',
84+
'Tool Count: 1',
85+
'Mac Address:00:11:22:33:44:55',
86+
'ok',
87+
].join('\n');
88+
vi.spyOn(client, 'sendCommandAsync').mockResolvedValue(mockResponse);
89+
90+
const info = await client.getPrinterInfo();
91+
92+
expect(info).not.toBeNull();
93+
expect(info?.serialNumber).toBe('SNADVA3M12345');
94+
expect(info?.firmware).toBe('v1.3.7');
95+
});
7596
});
7697

7798
describe('Print Job Control', () => {

src/tcpapi/FlashForgeA3Client.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,16 @@ export class FlashForgeA3Client extends FlashForgeTcpClient {
119119
}
120120

121121
/**
122-
* Initializes control by sending M601.
122+
* Initializes control by sending the legacy M601 S1 login command.
123123
*/
124124
public async initControl(): Promise<boolean> {
125125
try {
126126
await new Promise((resolve) => setTimeout(resolve, 500));
127127

128-
const response = await this.sendCommandAsync('~M601');
128+
const loginCommand = GCodes.CmdLogin;
129+
const response = await this.sendCommandAsync(loginCommand);
129130
if (response === null) {
130-
console.error('A3: Failed to send M601 connection command');
131+
console.error('A3: Failed to send M601 S1 login command');
131132
return false;
132133
}
133134

@@ -136,7 +137,7 @@ export class FlashForgeA3Client extends FlashForgeTcpClient {
136137
return true;
137138
}
138139

139-
return this.isSuccessfulCommandResponse('~M601', response);
140+
return this.isSuccessfulCommandResponse(loginCommand, response);
140141
} catch (error) {
141142
console.error('A3: initControl error:', error);
142143
return false;
@@ -170,6 +171,8 @@ export class FlashForgeA3Client extends FlashForgeTcpClient {
170171
info.machineName = line.replace('Machine Name:', '').trim();
171172
} else if (line.startsWith('Firmware:')) {
172173
info.firmware = line.replace('Firmware:', '').trim();
174+
} else if (line.startsWith('SN:')) {
175+
info.serialNumber = line.replace('SN:', '').trim();
173176
} else if (line.startsWith('Serial Number:')) {
174177
info.serialNumber = line.replace('Serial Number:', '').trim();
175178
} else if (line.startsWith('Tool Count:')) {

src/tcpapi/FlashForgeA4Client.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('FlashForgeA4Client', () => {
2323
expect(client['port']).toBe(8899);
2424
});
2525

26-
it('should initialize connection with the documented M601 flow', async () => {
26+
it('should initialize connection with the documented M601 S1 flow', async () => {
2727
const sendCommandSpy = vi.spyOn(client, 'sendCommandAsync');
2828
vi.spyOn(client, 'startKeepAlive').mockImplementation(() => {});
2929
sendCommandSpy
@@ -40,7 +40,7 @@ describe('FlashForgeA4Client', () => {
4040
].join('\n'));
4141

4242
await expect(client.initControl()).resolves.toBe(true);
43-
expect(sendCommandSpy).toHaveBeenNthCalledWith(1, '~M601');
43+
expect(sendCommandSpy).toHaveBeenNthCalledWith(1, '~M601 S1');
4444
expect(sendCommandSpy).toHaveBeenNthCalledWith(2, '~M115');
4545
});
4646

@@ -113,6 +113,24 @@ describe('FlashForgeA4Client', () => {
113113

114114
expect(info?.serialNumber).toBe('A4SN12345');
115115
});
116+
117+
it('should accept SN-prefixed serial numbers when firmware reports them', async () => {
118+
vi.spyOn(client, 'sendCommandAsync').mockResolvedValue([
119+
'CMD M115 Received.',
120+
'Machine Type: Flashforge Adventurer 4',
121+
'Machine Name: Serial Printer',
122+
'Firmware: v2.0.5 20220527',
123+
'SN: A4SN67890',
124+
'X: 220 Y: 200 Z: 250',
125+
'Tool Count: 1',
126+
'Mac Address: 00:11:22:33:44:55',
127+
'ok',
128+
].join('\n'));
129+
130+
const info = await client.getPrinterInfo();
131+
132+
expect(info?.serialNumber).toBe('A4SN67890');
133+
});
116134
});
117135

118136
describe('Status Parsers', () => {

src/tcpapi/FlashForgeA4Client.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @fileoverview TCP client for FlashForge Adventurer 4 printers.
33
*
44
* Adventurer 4 Lite and Pro share the same legacy TCP protocol on port 8899.
5-
* This client keeps the documented M601 and M115 behavior separate from the
5+
* This client keeps the legacy M601 S1 login flow and M115 behavior separate from the
66
* generic legacy client while reusing the shared TCP transport and parsers.
77
*/
88

@@ -73,13 +73,14 @@ export class FlashForgeA4Client
7373
}
7474

7575
/**
76-
* Initializes control by sending the documented M601 command.
76+
* Initializes control by sending the legacy M601 S1 login command.
7777
*/
7878
public async initControl(): Promise<boolean> {
7979
try {
80-
const response = await this.sendCommandAsync('~M601');
80+
const loginCommand = GCodes.CmdLogin;
81+
const response = await this.sendCommandAsync(loginCommand);
8182
if (response === null) {
82-
console.error('A4: Failed to send M601 connection command');
83+
console.error('A4: Failed to send M601 S1 login command');
8384
return false;
8485
}
8586

@@ -88,14 +89,14 @@ export class FlashForgeA4Client
8889
return true;
8990
}
9091

91-
if (!this.isSuccessfulCommandResponse('~M601', response)) {
92+
if (!this.isSuccessfulCommandResponse(loginCommand, response)) {
9293
return false;
9394
}
9495

9596
await new Promise((resolve) => setTimeout(resolve, 100));
9697
const info = await this.getPrinterInfo();
9798
if (!info) {
98-
console.error('A4: Failed to retrieve printer info after M601');
99+
console.error('A4: Failed to retrieve printer info after M601 S1');
99100
return false;
100101
}
101102

0 commit comments

Comments
 (0)