Skip to content

Commit b3c7e45

Browse files
GhostTypesclaude
andcommitted
feat(creator5): add Creator 5 / 5 Pro detail, capabilities, and material-station support
Extend the modern client to support the Creator 5 series, which is functionally "AD5X + per-tool temps". All additions are backwards-compatible and null-safe so existing 5M/AD5X parsing is unaffected. - FFPrinterDetail: add camera, lidar, model, nozzleTemps[], nozzleTargetTemps[]. - FFMachineInfo: add IsCreator5Pro, Model, HasCamera, HasLidar, HasDoorSensor, ToolTemps[] (single-nozzle models report a 1-element array). - MachineInfo.fromDetail: build ToolTemps with single-extruder fallback; derive capabilities by presence (HasDoorSensor only on Creator 5 Pro, since plain C5 has no real door sensor); Model resolves model -> PID name -> name. - FiveMClient: optional Product fields; ProductCapabilities + deriveCapabilities (1 = available); force-enable filtration on Creator 5 Pro (its /product under-reports the fans despite having the hardware); cache tool temps + caps. - JobControl: generalize the AD5X material-station guard to also accept Creator 5 and add model-neutral aliases (uploadFileWithMaterialMappings, startMaterialMappingJob, startSingleColorMaterialStationJob). Bump to 1.3.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8bc3f27 commit b3c7e45

8 files changed

Lines changed: 375 additions & 31 deletions

File tree

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.2",
3+
"version": "1.3.3",
44
"description": "FlashForge 3D Printer API for Node.js",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/FiveMClient.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,54 @@ describe('FiveMClient', () => {
131131
expect(httpGet).toHaveBeenCalledTimes(1);
132132
expect(destroy).toHaveBeenCalledTimes(1);
133133
});
134+
135+
describe('deriveCapabilities', () => {
136+
it('maps 1=available control states to capability flags', () => {
137+
const caps = FiveMClient.deriveCapabilities({
138+
chamberTempCtrlState: 1,
139+
externalFanCtrlState: 1,
140+
internalFanCtrlState: 1,
141+
lightCtrlState: 1,
142+
nozzleTempCtrlState: 1,
143+
platformTempCtrlState: 1,
144+
});
145+
146+
expect(caps).toEqual({
147+
hasLed: true,
148+
hasFiltration: true,
149+
hasChamberControl: true,
150+
hasNozzleControl: true,
151+
hasPlatformControl: true,
152+
});
153+
});
154+
155+
it('requires BOTH fans for filtration (Creator 5 Pro reports 0/0)', () => {
156+
// Live C5 Pro /product: light + chamber + nozzle + platform available,
157+
// but internal/external fans reported 0 -> filtration must be false here.
158+
const caps = FiveMClient.deriveCapabilities({
159+
chamberTempCtrlState: 1,
160+
externalFanCtrlState: 0,
161+
internalFanCtrlState: 0,
162+
lightCtrlState: 1,
163+
nozzleTempCtrlState: 1,
164+
platformTempCtrlState: 1,
165+
});
166+
167+
expect(caps.hasFiltration).toBe(false);
168+
expect(caps.hasLed).toBe(true);
169+
expect(caps.hasChamberControl).toBe(true);
170+
});
171+
172+
it('treats missing control states as not-available', () => {
173+
const caps = FiveMClient.deriveCapabilities({});
174+
175+
expect(caps).toEqual({
176+
hasLed: false,
177+
hasFiltration: false,
178+
hasChamberControl: false,
179+
hasNozzleControl: false,
180+
hasPlatformControl: false,
181+
});
182+
});
183+
});
134184
});

src/FiveMClient.ts

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { JobControl } from './api/controls/JobControl';
1010
import { TempControl } from './api/controls/TempControl';
1111
import { NetworkUtils } from './api/network/NetworkUtils';
1212
import { Endpoints } from './api/server/Endpoints';
13-
import type { FFMachineInfo } from './models/ff-models';
13+
import type { FFMachineInfo, Temperature } from './models/ff-models';
1414
import { MachineInfo } from './models/MachineInfo';
1515
import { FlashForgeClient } from './tcpapi/FlashForgeClient';
1616

@@ -58,10 +58,23 @@ export class FiveMClient {
5858
public isPro: boolean = false;
5959
public isAD5X: boolean = false;
6060
public isCreator5: boolean = false;
61+
public isCreator5Pro: boolean = false;
62+
/** Immutable factory model name (e.g. "Creator 5 Pro"); not user-editable. */
63+
public model: string = '';
6164
public firmwareVersion: string = '';
6265
public firmVer: string = '';
6366
public cameraStreamUrl: string = '';
6467

68+
// Hardware capability flags (presence-derived, see MachineInfo.fromDetail).
69+
/** Whether the printer has a built-in camera. */
70+
public hasCamera: boolean = false;
71+
/** Whether the printer has a lidar/first-layer scanner. */
72+
public hasLidar: boolean = false;
73+
/** Whether the printer has a real door sensor (only then is door status meaningful). */
74+
public hasDoorSensor: boolean = false;
75+
/** Current/target temperatures for every tool/nozzle (1 entry for single-nozzle models). */
76+
public toolTemps: Temperature[] = [];
77+
6578
public ipAddress: string;
6679
public macAddress: string = '';
6780

@@ -78,6 +91,14 @@ export class FiveMClient {
7891
public filtrationControl: boolean = false;
7992
/** Raw product info containing all control states */
8093
public productInfo: Product | null = null;
94+
/** Capabilities derived from the /product control states (1 = available). */
95+
public capabilities: ProductCapabilities = {
96+
hasLed: false,
97+
hasFiltration: false,
98+
hasChamberControl: false,
99+
hasNozzleControl: false,
100+
hasPlatformControl: false,
101+
};
81102

82103
/**
83104
* Creates an instance of FiveMClient.
@@ -201,6 +222,12 @@ export class FiveMClient {
201222
this.isPro = info.IsPro; // Use the value from MachineInfo
202223
this.isAD5X = info.IsAD5X; // Cache the AD5X status
203224
this.isCreator5 = info.IsCreator5; // Cache the Creator 5 status
225+
this.isCreator5Pro = info.IsCreator5Pro; // Cache the Creator 5 Pro status
226+
this.model = info.Model || '';
227+
this.hasCamera = info.HasCamera;
228+
this.hasLidar = info.HasLidar;
229+
this.hasDoorSensor = info.HasDoorSensor;
230+
this.toolTemps = info.ToolTemps || [];
204231
this.firmwareVersion = info.FirmwareVersion || '';
205232
this.firmVer = info.FirmwareVersion ? info.FirmwareVersion.split('-')[0] : '';
206233
this.cameraStreamUrl = info.CameraStreamUrl || '';
@@ -318,6 +345,31 @@ export class FiveMClient {
318345
}
319346
}
320347

348+
/**
349+
* Derives capability flags from a /product response. Polarity is
350+
* `1 = available, 0 = not available`; any missing field is treated as 0.
351+
*
352+
* NOTE: `hasFiltration` requires BOTH internal and external fan controls to be
353+
* available, matching the 5M Pro charcoal-filtration behavior. This is a pure
354+
* read of what /product advertises. The Creator 5 Pro under-reports it (both
355+
* fans 0 despite having the hardware), so its filtration is force-enabled by
356+
* model in `sendProductCommand` rather than here.
357+
*
358+
* @param product Raw (possibly sparse) product control-state object.
359+
* @returns Derived {@link ProductCapabilities}.
360+
*/
361+
public static deriveCapabilities(product: Partial<Product>): ProductCapabilities {
362+
const avail = (v: number | undefined): boolean => v === 1;
363+
return {
364+
hasLed: avail(product.lightCtrlState),
365+
hasFiltration:
366+
avail(product.internalFanCtrlState) && avail(product.externalFanCtrlState),
367+
hasChamberControl: avail(product.chamberTempCtrlState),
368+
hasNozzleControl: avail(product.nozzleTempCtrlState),
369+
hasPlatformControl: avail(product.platformTempCtrlState),
370+
};
371+
}
372+
321373
/**
322374
* Sends a product command to the printer to retrieve control states.
323375
* This method sets the `httpClientBusy` flag while the request is in progress.
@@ -341,13 +393,20 @@ export class FiveMClient {
341393
try {
342394
const productResponse = response.data as ProductResponse;
343395
if (productResponse && NetworkUtils.isOk(productResponse)) {
344-
// Parse & set control states
345-
const product = productResponse.product;
396+
// Parse & set control states. /product polarity is 1 = available,
397+
// 0 = not available. Fields may be absent on some models, so default
398+
// missing values to 0 (treat as not-available) rather than assume.
399+
const product = productResponse.product ?? {};
346400
this.productInfo = product; // Store raw product data
347-
this.ledControl = product.lightCtrlState !== 0;
348-
this.filtrationControl = !(
349-
product.internalFanCtrlState === 0 || product.externalFanCtrlState === 0
350-
);
401+
this.capabilities = FiveMClient.deriveCapabilities(product);
402+
// Creator 5 Pro has 5M-Pro-style filtration/aux fan per FlashForge
403+
// specs, but its /product under-reports it (both fan states 0), so
404+
// enable by model. The 5M Pro advertises it correctly and needs no
405+
// override. isCreator5Pro is already set here (verifyConnection ->
406+
// cacheDetails runs before initControl -> sendProductCommand).
407+
if (this.isCreator5Pro) this.capabilities.hasFiltration = true;
408+
this.ledControl = this.capabilities.hasLed;
409+
this.filtrationControl = this.capabilities.hasFiltration;
351410
//console.log("LedControl: " + this.ledControl);
352411
//console.log("FiltrationControl: " + this.filtrationControl);
353412
return true;
@@ -388,16 +447,33 @@ interface ProductResponse extends GenericResponse {
388447
* while other numbers (typically 1) mean on/available or a specific mode.
389448
*/
390449
export interface Product {
391-
/** State of the chamber temperature control. */
392-
chamberTempCtrlState: number;
393-
/** State of the external fan control. */
394-
externalFanCtrlState: number;
395-
/** State of the internal fan control. */
396-
internalFanCtrlState: number;
397-
/** State of the light control. */
398-
lightCtrlState: number;
399-
/** State of the nozzle temperature control. */
400-
nozzleTempCtrlState: number;
401-
/** State of the platform (bed) temperature control. */
402-
platformTempCtrlState: number;
450+
/** State of the chamber temperature control (1 = available). */
451+
chamberTempCtrlState?: number;
452+
/** State of the external fan control (1 = available). */
453+
externalFanCtrlState?: number;
454+
/** State of the internal fan control (1 = available). */
455+
internalFanCtrlState?: number;
456+
/** State of the light control (1 = available). */
457+
lightCtrlState?: number;
458+
/** State of the nozzle temperature control (1 = available). */
459+
nozzleTempCtrlState?: number;
460+
/** State of the platform (bed) temperature control (1 = available). */
461+
platformTempCtrlState?: number;
462+
}
463+
464+
/**
465+
* Capability flags derived from a printer's /product control states.
466+
* Each is true only when the corresponding control is reported as available.
467+
*/
468+
export interface ProductCapabilities {
469+
/** LED light control is available. */
470+
hasLed: boolean;
471+
/** Air-filtration control (both internal and external fans) is available. */
472+
hasFiltration: boolean;
473+
/** Chamber temperature control is available. */
474+
hasChamberControl: boolean;
475+
/** Nozzle temperature control is available. */
476+
hasNozzleControl: boolean;
477+
/** Platform (bed) temperature control is available. */
478+
hasPlatformControl: boolean;
403479
}

src/api/controls/JobControl.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export class JobControl {
227227
*/
228228
public async uploadFileAD5X(params: AD5XUploadParams): Promise<boolean> {
229229
// Validate that this is an AD5X printer
230-
if (!this.validateAD5XPrinter()) {
230+
if (!this.validateMaterialStationPrinter()) {
231231
return false;
232232
}
233233

@@ -400,7 +400,7 @@ export class JobControl {
400400
*/
401401
public async startAD5XMultiColorJob(params: AD5XLocalJobParams): Promise<boolean> {
402402
// Validate that this is an AD5X printer
403-
if (!this.validateAD5XPrinter()) {
403+
if (!this.validateMaterialStationPrinter()) {
404404
return false;
405405
}
406406

@@ -457,7 +457,7 @@ export class JobControl {
457457
*/
458458
public async startAD5XSingleColorJob(params: AD5XSingleColorJobParams): Promise<boolean> {
459459
// Validate that this is an AD5X printer
460-
if (!this.validateAD5XPrinter()) {
460+
if (!this.validateMaterialStationPrinter()) {
461461
return false;
462462
}
463463

@@ -498,14 +498,53 @@ export class JobControl {
498498
}
499499
}
500500

501+
// --- Model-neutral material-station aliases ---
502+
// The Creator 5 series shares the AD5X material-mapping print flow, so these
503+
// delegate to the AD5X implementations. Prefer these names in model-agnostic
504+
// code (e.g. a Creator5 backend) so call sites aren't misleadingly "AD5X".
505+
506+
/**
507+
* Uploads a file with material-station mappings (AD5X / Creator 5 series).
508+
* @param params Upload parameters including material mappings.
509+
* @returns Promise resolving to true on success.
510+
*/
511+
public async uploadFileWithMaterialMappings(params: AD5XUploadParams): Promise<boolean> {
512+
return this.uploadFileAD5X(params);
513+
}
514+
515+
/**
516+
* Starts a multi-color local job using material-station mappings (AD5X / Creator 5 series).
517+
* @param params Job parameters including material mappings.
518+
* @returns Promise resolving to true on success.
519+
*/
520+
public async startMaterialMappingJob(params: AD5XLocalJobParams): Promise<boolean> {
521+
return this.startAD5XMultiColorJob(params);
522+
}
523+
524+
/**
525+
* Starts a single-color local job on a material-station printer without using the
526+
* station (AD5X / Creator 5 series).
527+
* @param params Job parameters.
528+
* @returns Promise resolving to true on success.
529+
*/
530+
public async startSingleColorMaterialStationJob(
531+
params: AD5XSingleColorJobParams
532+
): Promise<boolean> {
533+
return this.startAD5XSingleColorJob(params);
534+
}
535+
501536
/**
502-
* Validates that the current printer is an AD5X model.
503-
* @returns True if the printer is AD5X, false otherwise
537+
* Validates that the current printer has a material station and therefore
538+
* supports material-mapping uploads/jobs. Covers AD5X and the Creator 5 series,
539+
* which share the same material-mapping print flow (Creator 5 = "AD5X + more").
540+
* @returns True if the printer supports material mappings, false otherwise
504541
* @private
505542
*/
506-
private validateAD5XPrinter(): boolean {
507-
if (!this.client.isAD5X) {
508-
console.error('AD5X Job error: This method can only be used with AD5X printers');
543+
private validateMaterialStationPrinter(): boolean {
544+
if (!this.client.isAD5X && !this.client.isCreator5) {
545+
console.error(
546+
'Material-station job error: this method requires an AD5X or Creator 5 series printer'
547+
);
509548
return false;
510549
}
511550
return true;

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ export {
3131
// Server constants
3232
export { Commands } from './api/server/Commands';
3333
export { Endpoints } from './api/server/Endpoints';
34-
export { FiveMClient, type FiveMClientConnectionOptions, Product } from './FiveMClient';
34+
export {
35+
FiveMClient,
36+
type FiveMClientConnectionOptions,
37+
Product,
38+
type ProductCapabilities,
39+
} from './FiveMClient';
3540
// Models
3641
export {
3742
AD5XLocalJobParams,

0 commit comments

Comments
 (0)