Skip to content

Commit 8bc3f27

Browse files
GhostTypesclaude
andcommitted
feat(discovery): detect printer model from USB product ID
Resolve the model from the firmware-set USB product ID (discovery packet offset 0x88) before falling back to productType (0x5A02, 5M-family only) and the user-mutable name. The product ID matches the /detail pid field and the update-checker keys, so it cleanly distinguishes 5M / 5M Pro / AD5X where productType cannot. - Add MODERN_PRODUCT_IDS map and pass productId into detectModernModel. - Add Creator5 / Creator5Pro to PrinterModel; register pids 40/41 in KNOWN_HTTP_PIDS and expose IsCreator5 on FFMachineInfo / FiveMClient. - Cover PID-first detection with PrinterDiscovery tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2ea04ff commit 8bc3f27

6 files changed

Lines changed: 88 additions & 14 deletions

File tree

src/FiveMClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export class FiveMClient {
5757
public printerName: string = '';
5858
public isPro: boolean = false;
5959
public isAD5X: boolean = false;
60+
public isCreator5: boolean = false;
6061
public firmwareVersion: string = '';
6162
public firmVer: string = '';
6263
public cameraStreamUrl: string = '';
@@ -199,6 +200,7 @@ export class FiveMClient {
199200
this.printerName = info.Name || '';
200201
this.isPro = info.IsPro; // Use the value from MachineInfo
201202
this.isAD5X = info.IsAD5X; // Cache the AD5X status
203+
this.isCreator5 = info.IsCreator5; // Cache the Creator 5 status
202204
this.firmwareVersion = info.FirmwareVersion || '';
203205
this.firmVer = info.FirmwareVersion ? info.FirmwareVersion.split('-')[0] : '';
204206
this.cameraStreamUrl = info.CameraStreamUrl || '';

src/api/PrinterDiscovery.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,30 @@ describe('PrinterDiscovery', () => {
421421
PrinterModel.Unknown
422422
);
423423
});
424+
425+
it('should prefer USB product ID over name/productType', () => {
426+
const discovery = createDiscovery();
427+
// PID is authoritative even when the (user-mutable) name disagrees.
428+
expect(discovery['detectModernModel']('My Renamed Printer', 0x5a02, 0x0024)).toBe(
429+
PrinterModel.Adventurer5MPro
430+
);
431+
expect(discovery['detectModernModel']('My Renamed Printer', 0x5a02, 0x0023)).toBe(
432+
PrinterModel.Adventurer5M
433+
);
434+
expect(discovery['detectModernModel']('whatever', 0x0000, 0x0026)).toBe(
435+
PrinterModel.AD5X
436+
);
437+
});
438+
439+
it('should detect Creator 5 / Creator 5 Pro by product ID', () => {
440+
const discovery = createDiscovery();
441+
expect(discovery['detectModernModel']('Creator 5', 0x0000, 0x0028)).toBe(
442+
PrinterModel.Creator5
443+
);
444+
expect(discovery['detectModernModel']('Creator 5 Pro', 0x0000, 0x0029)).toBe(
445+
PrinterModel.Creator5Pro
446+
);
447+
});
424448
});
425449

426450
describe('Legacy Protocol', () => {

src/api/PrinterDiscovery.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ const LEGACY_PRODUCT_IDS = {
5555
Adventurer4Pro: 0x001e,
5656
} as const;
5757

58+
/**
59+
* Canonical USB Product IDs (offset 0x88 of the discovery packet) for modern
60+
* printers. This is the same value space as the firmware `/detail` `pid` field
61+
* and the FlashForge update-checker keys, so it is the authoritative model
62+
* discriminator — unlike `productType` (0x5A02), which only identifies the 5M
63+
* *family* and cannot distinguish 5M / 5M Pro / AD5X from one another.
64+
*/
65+
const MODERN_PRODUCT_IDS: Readonly<Record<number, PrinterModel>> = {
66+
0x0023: PrinterModel.Adventurer5M,
67+
0x0024: PrinterModel.Adventurer5MPro,
68+
0x0026: PrinterModel.AD5X,
69+
0x0028: PrinterModel.Creator5,
70+
0x0029: PrinterModel.Creator5Pro,
71+
};
72+
5873
/**
5974
* EventEmitter-based continuous discovery monitor.
6075
*
@@ -522,7 +537,7 @@ export class PrinterDiscovery {
522537
const serialNumber = buffer.toString('utf8', 0x92, 0x92 + 128).replace(/\0.*$/, '');
523538

524539
// Detect model
525-
const model = this.detectModernModel(name, productType);
540+
const model = this.detectModernModel(name, productType, productId);
526541

527542
return {
528543
model,
@@ -593,32 +608,47 @@ export class PrinterDiscovery {
593608
}
594609

595610
/**
596-
* Detects printer model from modern protocol response.
611+
* Detects printer model from a modern protocol response.
597612
*
598-
* Uses both printer name and product type for accurate detection.
613+
* Resolution order:
614+
* 1. USB Product ID (offset 0x88) — the authoritative, user-immutable
615+
* discriminator (matches the firmware `/detail` pid and update-checker
616+
* keys). This is the only signal that distinguishes 5M / 5M Pro / AD5X /
617+
* Creator 5 / Creator 5 Pro from one another.
618+
* 2. `productType` 0x5A02 (5M *family*) + name — fallback for firmware that
619+
* reports an unknown/zero product ID.
620+
* 3. Name heuristics — last resort.
599621
*
600-
* @param name Printer name from response
601-
* @param productType Product type code (e.g., 0x5A02 for 5M series)
622+
* @param name Printer name from response (user-mutable)
623+
* @param productType Product type code (e.g., 0x5A02 for the 5M family)
624+
* @param productId USB product ID from offset 0x88
602625
* @returns Detected printer model
603626
* @private
604627
*/
605-
protected detectModernModel(name: string, productType: number): PrinterModel {
606-
const upperName = name.toUpperCase();
607-
608-
// Direct name matches (highest priority)
609-
if (upperName === 'AD5X') {
610-
return PrinterModel.AD5X;
628+
protected detectModernModel(name: string, productType: number, productId?: number): PrinterModel {
629+
// Product ID is authoritative (firmware-set, not user-mutable).
630+
if (productId !== undefined && productId in MODERN_PRODUCT_IDS) {
631+
return MODERN_PRODUCT_IDS[productId];
611632
}
612633

613-
// Product type-based detection (0x5A02 = 5M series)
634+
const upperName = name.toUpperCase();
635+
636+
// Fallback: 0x5A02 identifies the 5M family but not the specific model,
637+
// so we still need the name to separate base from Pro.
614638
if (productType === 0x5A02) {
639+
if (upperName === 'AD5X') {
640+
return PrinterModel.AD5X;
641+
}
615642
if (upperName.includes('PRO')) {
616643
return PrinterModel.Adventurer5MPro;
617644
}
618645
return PrinterModel.Adventurer5M;
619646
}
620647

621-
// Name-based fallback
648+
// Last-resort name heuristics.
649+
if (upperName === 'AD5X') {
650+
return PrinterModel.AD5X;
651+
}
622652
if (upperName.includes('ADVENTURER 5M') || upperName.includes('AD5M')) {
623653
if (upperName.includes('PRO')) {
624654
return PrinterModel.Adventurer5MPro;

src/models/MachineInfo.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ import { type FFMachineInfo, type FFPrinterDetail, MachineState } from './ff-mod
88
const PID_5M = 35;
99
const PID_5M_PRO = 36;
1010
const PID_AD5X = 38;
11-
const KNOWN_HTTP_PIDS = new Set<number>([PID_5M, PID_5M_PRO, PID_AD5X]);
11+
const PID_CREATOR5 = 40;
12+
const PID_CREATOR5_PRO = 41;
13+
const KNOWN_HTTP_PIDS = new Set<number>([
14+
PID_5M,
15+
PID_5M_PRO,
16+
PID_AD5X,
17+
PID_CREATOR5,
18+
PID_CREATOR5_PRO,
19+
]);
1220

1321
/**
1422
* Transforms printer detail data from the API response format into a structured `FFMachineInfo` object.
@@ -43,15 +51,18 @@ export class MachineInfo {
4351
const pid = detail.pid;
4452
let isAD5X: boolean;
4553
let isPro: boolean;
54+
let isCreator5: boolean;
4655
if (pid !== undefined && KNOWN_HTTP_PIDS.has(pid)) {
4756
isAD5X = pid === PID_AD5X;
4857
isPro = pid === PID_5M_PRO;
58+
isCreator5 = pid === PID_CREATOR5 || pid === PID_CREATOR5_PRO;
4959
} else {
5060
// Fallback for firmware that doesn't report pid: legacy
5161
// name+capability heuristic. Vulnerable to user renames, which
5262
// is why pid-based detection is preferred when available.
5363
isAD5X = detail.name === 'AD5X' || hasMaterialStation;
5464
isPro = (detail.name || '').includes('Pro') && !isAD5X;
65+
isCreator5 = (detail.name || '').includes('Creator 5');
5566
}
5667
const printEta = this.formatTimeFromSeconds(detail.estimatedTime || 0);
5768
const completionTime = new Date(Date.now() + (detail.estimatedTime || 0) * 1000);
@@ -120,6 +131,7 @@ export class MachineInfo {
120131
Pid: pid,
121132
IsPro: isPro,
122133
IsAD5X: isAD5X,
134+
IsCreator5: isCreator5,
123135
NozzleSize: detail.nozzleModel || '',
124136

125137
// Material Station Info

src/models/PrinterDiscovery.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export enum PrinterModel {
2121
Adventurer4 = 'Adventurer4',
2222
/** Adventurer 3 model */
2323
Adventurer3 = 'Adventurer3',
24+
/** Creator 5 tool-changer model */
25+
Creator5 = 'Creator5',
26+
/** Creator 5 Pro tool-changer model */
27+
Creator5Pro = 'Creator5Pro',
2428
/** Unknown or unrecognized printer model */
2529
Unknown = 'Unknown'
2630
}

src/models/ff-models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ export interface FFMachineInfo {
242242
IsPro: boolean;
243243
/** Indicates if the printer is an AD5X model. */
244244
IsAD5X: boolean;
245+
/** Indicates if the printer is a Creator 5 / Creator 5 Pro (4-head tool-changer). */
246+
IsCreator5: boolean;
245247
/** Nozzle size (e.g., "0.4mm"). */
246248
NozzleSize: string;
247249

0 commit comments

Comments
 (0)