Skip to content

Commit 5753e89

Browse files
committed
fix: use firmware pid for IsPro/IsAD5X detection
The /detail `name` field is user-mutable, so renaming a printer broke model detection. `pid` is firmware-set and stable. Also exposes Pid on FFMachineInfo. Bumps to 1.3.1. Refs: GhostTypes/ff-5mp-hass#13
1 parent bc1098a commit 5753e89

5 files changed

Lines changed: 109 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.3.1] - 2026-05-08
11+
12+
### Fixed
13+
14+
- Printer model detection (`IsPro`, `IsAD5X` on `FFMachineInfo`) now reads the firmware-set integer `pid` field on `/detail` instead of string-matching the user-mutable `name` field. Renaming an Adventurer 5M / 5M Pro / AD5X via the LCD or cloud no longer breaks model detection in downstream consumers (Electron UI, Web UI). Falls back to the legacy name+capability heuristic when `pid` is absent so older firmware still works. Reported in [ff-5mp-hass#13](https://github.com/GhostTypes/ff-5mp-hass/issues/13).
15+
16+
### Added
17+
18+
- `FFMachineInfo.Pid: number | undefined` — the raw firmware PID exposed for consumers that want to do their own model-class gating. Known modern HTTP-capable PIDs: `35` (Adventurer 5M), `36` (5M Pro), `38` (AD5X).
19+
20+
### Changed
21+
22+
- Corrected misleading JSDoc on `FFPrinterDetail.pid` (was "Process ID, possibly related to the current print job"; it is in fact the firmware-set Product ID).
23+
1024
## [1.3.0] - 2026-03-21
1125

1226
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ghosttypes/ff-api",
3-
"version": "1.3.0",
3+
"version": "1.3.1",
44
"description": "FlashForge 3D Printer API for Node.js",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/models/MachineInfo.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,76 @@ describe('MachineInfo', () => {
215215
expect(result.IsPro).toBe(true);
216216
});
217217

218+
it('should detect AD5X from pid when renamed without matl_station fields', () => {
219+
// Regression test for ff-5mp-hass#13: a user renamed their printer and
220+
// the name+capability fallback could no longer recognize the model.
221+
// The firmware-set integer pid is stable across renames.
222+
const renamedAd5x: FFPrinterDetail = {
223+
name: 'LegoTech82',
224+
pid: 38,
225+
firmwareVersion: '1.1.7-1.0.2',
226+
ipAddr: '192.168.1.120',
227+
status: 'ready',
228+
};
229+
230+
const result = machineInfoConverter.fromDetail(renamedAd5x);
231+
232+
expect(result).not.toBeNull();
233+
if (!result) return;
234+
expect(result.Name).toBe('LegoTech82');
235+
expect(result.Pid).toBe(38);
236+
expect(result.IsAD5X).toBe(true);
237+
expect(result.IsPro).toBe(false);
238+
});
239+
240+
it('should detect 5M Pro from pid even when renamed', () => {
241+
const renamedPro: FFPrinterDetail = {
242+
...GENERIC_PRINTER_DETAIL_JSON,
243+
name: 'MyPrinter',
244+
pid: 36,
245+
};
246+
247+
const result = machineInfoConverter.fromDetail(renamedPro);
248+
249+
expect(result).not.toBeNull();
250+
if (!result) return;
251+
expect(result.Name).toBe('MyPrinter');
252+
expect(result.Pid).toBe(36);
253+
expect(result.IsPro).toBe(true);
254+
expect(result.IsAD5X).toBe(false);
255+
});
256+
257+
it('should treat pid 35 as a plain 5M even if name suggests Pro', () => {
258+
const plain5M: FFPrinterDetail = {
259+
...GENERIC_PRINTER_DETAIL_JSON,
260+
name: 'FlashForge 5M Pro',
261+
pid: 35,
262+
};
263+
264+
const result = machineInfoConverter.fromDetail(plain5M);
265+
266+
expect(result).not.toBeNull();
267+
if (!result) return;
268+
expect(result.Pid).toBe(35);
269+
expect(result.IsPro).toBe(false);
270+
expect(result.IsAD5X).toBe(false);
271+
});
272+
273+
it('should fall back to name+capability when pid is absent', () => {
274+
const noPidAd5x: FFPrinterDetail = {
275+
...AD5X_PRINTER_DETAIL_JSON,
276+
pid: undefined,
277+
};
278+
279+
const result = machineInfoConverter.fromDetail(noPidAd5x);
280+
281+
expect(result).not.toBeNull();
282+
if (!result) return;
283+
expect(result.Pid).toBeUndefined();
284+
expect(result.IsAD5X).toBe(true); // detected via hasMatlStation fallback
285+
expect(result.IsPro).toBe(false);
286+
});
287+
218288
it('should return null if detail is null', () => {
219289
const result = machineInfoConverter.fromDetail(null);
220290
expect(result).toBeNull();

src/models/MachineInfo.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
*/
44
import { type FFMachineInfo, type FFPrinterDetail, MachineState } from './ff-models';
55

6+
// Firmware-reported PIDs from FlashForge's /detail endpoint. These are stable
7+
// identifiers set by firmware, unlike the user-mutable `name` field.
8+
const PID_5M = 35;
9+
const PID_5M_PRO = 36;
10+
const PID_AD5X = 38;
11+
const KNOWN_HTTP_PIDS = new Set<number>([PID_5M, PID_5M_PRO, PID_AD5X]);
12+
613
/**
714
* Transforms printer detail data from the API response format into a structured `FFMachineInfo` object.
815
* This class centralizes the logic for mapping and calculating various properties of the printer's state
@@ -33,8 +40,19 @@ export class MachineInfo {
3340
detail.hasMatlStation === true ||
3441
(detail.matlStationInfo?.slotCnt ?? 0) > 0 ||
3542
(detail.matlStationInfo?.slotInfos?.length ?? 0) > 0;
36-
const isAD5X = detail.name === 'AD5X' || hasMaterialStation;
37-
const isPro = (detail.name || '').includes('Pro') && !isAD5X;
43+
const pid = detail.pid;
44+
let isAD5X: boolean;
45+
let isPro: boolean;
46+
if (pid !== undefined && KNOWN_HTTP_PIDS.has(pid)) {
47+
isAD5X = pid === PID_AD5X;
48+
isPro = pid === PID_5M_PRO;
49+
} else {
50+
// Fallback for firmware that doesn't report pid: legacy
51+
// name+capability heuristic. Vulnerable to user renames, which
52+
// is why pid-based detection is preferred when available.
53+
isAD5X = detail.name === 'AD5X' || hasMaterialStation;
54+
isPro = (detail.name || '').includes('Pro') && !isAD5X;
55+
}
3856
const printEta = this.formatTimeFromSeconds(detail.estimatedTime || 0);
3957
const completionTime = new Date(Date.now() + (detail.estimatedTime || 0) * 1000);
4058
const formattedRunTime = this.formatTimeFromSeconds(detail.printDuration || 0);
@@ -99,6 +117,7 @@ export class MachineInfo {
99117
FillAmount: detail.fillAmount || 0,
100118
FirmwareVersion: detail.firmwareVersion || '',
101119
Name: detail.name || '',
120+
Pid: pid,
102121
IsPro: isPro,
103122
IsAD5X: isAD5X,
104123
NozzleSize: detail.nozzleModel || '',

src/models/ff-models.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export interface FFPrinterDetail {
8989
nozzleModel?: string;
9090
/** Style or type of the nozzle. */
9191
nozzleStyle?: number;
92-
/** Process ID, possibly related to the current print job. */
92+
/** Firmware-set Product ID identifying the printer model (e.g. 35=Adventurer 5M, 36=5M Pro, 38=AD5X). Stable across user renames. */
9393
pid?: number;
9494
/** Target temperature for the print bed (platform). */
9595
platTargetTemp?: number;
@@ -236,6 +236,8 @@ export interface FFMachineInfo {
236236
FirmwareVersion: string;
237237
/** User-configured name of the printer. */
238238
Name: string;
239+
/** Firmware-set Product ID identifying the printer model (e.g. 35=Adventurer 5M, 36=5M Pro, 38=AD5X). Undefined on firmware that doesn't report it. */
240+
Pid?: number;
239241
/** Indicates if the printer model is a "Pro" version. */
240242
IsPro: boolean;
241243
/** Indicates if the printer is an AD5X model. */

0 commit comments

Comments
 (0)