Skip to content

Commit af1de67

Browse files
GhostTypesclaude
andcommitted
feat(creator5): HTTP-only mode for Creator 5 / 5 Pro (no legacy TCP)
The Creator 5 / 5 Pro run no legacy TCP server (no port 8899) — they are HTTP-only (OrcaServer on 8898). Add an httpOnly mode to FiveMClient so it can drive them without the TCP control channel. - FiveMClientConnectionOptions.httpOnly + FiveMClient.httpOnly; auto-enabled in verifyConnection() when a Creator 5 / 5 Pro is detected from /detail. - initControl() succeeds on the HTTP product command alone in httpOnly mode; verifyConnection() skips the TCP getPrinterInfo() probe; dispose() skips TCP. - TempControl set/cancel route through HTTP temperatureCtl_cmd (rightNozzle/ leftNozzle/platform/chamber; -200 = no change, -100 = off) instead of TCP g-code; waitForPartCool() is a no-op in httpOnly. - Files.getLocalFileList() falls back to HTTP /gcodeList when no TCP channel. - Control TCP-only ops (home/runout/filament-load) return false (logged) in httpOnly instead of hanging on a dead socket. - Backfill CHANGELOG for 1.3.3 / 1.3.4; +13 tests (364 total). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LEiNF9KzwvnF4kAinmvu2a
1 parent e11b241 commit af1de67

9 files changed

Lines changed: 308 additions & 39 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [1.3.5] - 2026-06-24
11+
12+
### Added
13+
14+
- **HTTP-only mode for the Creator 5 / Creator 5 Pro.** These printers run no legacy TCP server (no port 8899) — they are HTTP-only (`OrcaServer` on 8898). `FiveMClient` now supports an `httpOnly` connection so it can drive them:
15+
- New `FiveMClientConnectionOptions.httpOnly?: boolean` and public `FiveMClient.httpOnly`. Set it explicitly (e.g. when the model is known from discovery's USB product ID), or let `verifyConnection()` auto-enable it when a Creator 5 / 5 Pro is detected from `/detail`.
16+
- In HTTP-only mode `initControl()` succeeds on the HTTP product command alone (never opens the TCP control channel), `verifyConnection()` skips the TCP `getPrinterInfo()` probe (no connect timeout), and `dispose()` skips TCP teardown.
17+
- `TempControl` set/cancel extruder/bed temperatures route through the HTTP `temperatureCtl_cmd` (args `rightNozzle`/`leftNozzle`/`platform`/`chamber`; `-200` = leave unchanged, `-100` = off) instead of TCP G-code. `waitForPartCool()` is a no-op in HTTP-only mode (poll `info.get()` instead).
18+
- `Files.getLocalFileList()` falls back to the HTTP `/gcodeList` (10 most-recent files) when there is no TCP channel.
19+
- `Control` TCP-only operations (`homeAxes`, `homeAxesRapid`, `turnRunoutSensorOn/Off`, `prepareFilamentLoad`/`loadFilament`/`finishFilamentLoad`) return `false` with a clear log in HTTP-only mode rather than hanging on a dead socket.
20+
21+
## [1.3.4] - 2026-06-22
22+
23+
### Added
24+
25+
- `JobControl.startCreator5Job(params)` — Creator 5 native print start via `POST /printGcode` (the Creator 5 does material matching at print-start, not at upload time), with optional per-tool `materialMappings` (`{ toolId, slotId, materialName }`). `Creator5JobParams` / `Creator5MaterialMapping` types exported.
26+
27+
## [1.3.3] - 2026-06-22
28+
29+
### Added
30+
31+
- Creator 5 / Creator 5 Pro `/detail` model support: `nozzleTemps[]` / `nozzleTargetTemps[]` (per-tool arrays), `lidar`, `model`, and `FFMachineInfo` fields `IsCreator5`, `IsCreator5Pro`, `Model`, `HasCamera`, `HasLidar`, `HasDoorSensor`, `ToolTemps`.
32+
- `FiveMClient` Creator 5 flags (`isCreator5`, `isCreator5Pro`, `model`, `hasCamera`, `hasLidar`, `hasDoorSensor`, `toolTemps`) and capability derivation (`deriveCapabilities`, `ProductCapabilities`) from `/product` (polarity `1` = available), with a Creator 5 Pro filtration force-enable (its `/product` under-reports the fan control states).
33+
- Creator 5 PIDs added to model detection (`0x28` Creator 5, `0x29` Creator 5 Pro).
34+
1035
## [1.3.2] - 2026-06-05
1136

1237
### 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.4",
3+
"version": "1.3.5",
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,38 @@ describe('FiveMClient', () => {
181181
});
182182
});
183183
});
184+
185+
// Creator 5 / 5 Pro run no legacy TCP server; initControl must succeed on the
186+
// HTTP product command alone and never touch the TCP client.
187+
describe('HTTP-only mode', () => {
188+
const okProduct = { status: 200, data: { code: 0, message: 'Success', product: {} } };
189+
190+
it('initControl skips TCP init when constructed with httpOnly: true', async () => {
191+
httpPost.mockResolvedValue(okProduct);
192+
const client = new FiveMClient('192.168.1.10', 'SN-1', 'CHK-1', { httpOnly: true });
193+
194+
await expect(client.initControl()).resolves.toBe(true);
195+
expect(client.httpOnly).toBe(true);
196+
expect((client.tcpClient as unknown as { initControl: ReturnType<typeof vi.fn> }).initControl)
197+
.not.toHaveBeenCalled();
198+
});
199+
200+
it('initControl still initializes TCP for dual-API printers (default)', async () => {
201+
httpPost.mockResolvedValue(okProduct);
202+
const client = new FiveMClient('192.168.1.10', 'SN-1', 'CHK-1');
203+
204+
await expect(client.initControl()).resolves.toBe(true);
205+
expect(client.httpOnly).toBe(false);
206+
expect((client.tcpClient as unknown as { initControl: ReturnType<typeof vi.fn> }).initControl)
207+
.toHaveBeenCalled();
208+
});
209+
210+
it('dispose does not touch the TCP client in HTTP-only mode', async () => {
211+
const client = new FiveMClient('192.168.1.10', 'SN-1', 'CHK-1', { httpOnly: true });
212+
213+
await client.dispose();
214+
expect((client.tcpClient as unknown as { dispose: ReturnType<typeof vi.fn> }).dispose)
215+
.not.toHaveBeenCalled();
216+
});
217+
});
184218
});

src/FiveMClient.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ export interface FiveMClientConnectionOptions {
2222
httpPort?: number;
2323
/** Legacy TCP command port override used by the embedded tcp client (defaults to 8899). */
2424
tcpPort?: number;
25+
/**
26+
* Force HTTP-only mode: never open or use the legacy TCP control channel.
27+
* Required for the Creator 5 / 5 Pro, which run no TCP server on 8899 (HTTP
28+
* `OrcaServer` on 8898 only). When omitted, HTTP-only mode is auto-enabled
29+
* once the printer is detected as a Creator 5 during {@link FiveMClient.verifyConnection}.
30+
* Pass `true` up-front (e.g. when the model is already known from discovery's
31+
* USB product ID) to skip the TCP probe entirely and avoid its connect timeout.
32+
*/
33+
httpOnly?: boolean;
2534
}
2635

2736
/**
@@ -46,6 +55,15 @@ export class FiveMClient {
4655
/** Instance for lower-level TCP communication with the printer. */
4756
public tcpClient: FlashForgeClient;
4857

58+
/**
59+
* When true, the legacy TCP control channel is never used (HTTP API only).
60+
* Set from the `httpOnly` constructor option, and auto-enabled for the
61+
* Creator 5 / 5 Pro during {@link verifyConnection}. TCP-only operations
62+
* (homing, jog, direct g-code, runout sensor, filament load) are unavailable
63+
* while this is true.
64+
*/
65+
public httpOnly: boolean = false;
66+
4967
public serialNumber: string;
5068
public checkCode: string;
5169
/** HTTP client for making requests to the printer's API. */
@@ -117,6 +135,7 @@ export class FiveMClient {
117135
this.serialNumber = serialNumber;
118136
this.checkCode = checkCode;
119137
this.PORT = options?.httpPort ?? 8898;
138+
this.httpOnly = options?.httpOnly ?? false;
120139

121140
this.httpClient = axios.create({
122141
timeout: 5000,
@@ -168,12 +187,17 @@ export class FiveMClient {
168187

169188
/**
170189
* Initializes the control interface with the printer.
171-
* This involves sending a product command and initializing TCP control.
190+
* Sends the product command (HTTP) and, unless in HTTP-only mode, initializes
191+
* the legacy TCP control channel. HTTP-only printers (Creator 5 / 5 Pro) have
192+
* no TCP server, so success is determined solely by the product command.
172193
* @returns A Promise that resolves to true if control initialization is successful, false otherwise.
173194
*/
174195
public async initControl(): Promise<boolean> {
175196
//console.log("InitControl()");
176197
if (await this.sendProductCommand()) {
198+
// Creator 5 / 5 Pro expose no TCP control channel; verifyConnection runs
199+
// before initControl and sets httpOnly when such a model is detected.
200+
if (this.httpOnly) return true;
177201
return await this.tcpClient.initControl();
178202
}
179203
console.log('New API control failed!');
@@ -185,6 +209,8 @@ export class FiveMClient {
185209
*/
186210
public async dispose(): Promise<void> {
187211
this.cameraStreamUrl = '';
212+
// No TCP channel was opened in HTTP-only mode, so nothing to tear down.
213+
if (this.httpOnly) return;
188214
await this.tcpClient.dispose();
189215
}
190216

@@ -317,24 +343,34 @@ export class FiveMClient {
317343
return false;
318344
}
319345

346+
// The Creator 5 / 5 Pro run no legacy TCP server, so auto-enable HTTP-only
347+
// mode here (before any TCP probe) once the model is detected from /detail.
348+
// initControl() runs after this and relies on the flag being set.
349+
if (machineInfo.IsCreator5 || machineInfo.IsCreator5Pro) {
350+
this.httpOnly = true;
351+
}
352+
320353
// Check for Pro model with the machine TypeName (can't be changed by user)
321354
// We now rely on MachineInfo.fromDetail to set IsPro and IsAD5X based on detail.name
322355
// So, the TCP check for "Pro" might be redundant or could be a fallback.
323356
// For now, let's keep it but prioritize what's in machineInfo.
324-
const tcpInfo = await this.tcpClient.getPrinterInfo();
325-
if (tcpInfo) {
326-
// If machineInfo hasn't already set isPro, we can use TCP info as a fallback.
327-
// However, machineInfo.IsPro (derived from detail.name) should be more reliable.
328-
// This line effectively gets overridden by cacheDetails if machineInfo.IsPro is set.
329-
if (tcpInfo.TypeName.includes('Pro') && !machineInfo.IsPro && !machineInfo.IsAD5X) {
330-
// Only set this if not already determined by machineInfo, and it's not an AD5X
331-
this.isPro = true;
357+
// HTTP-only printers have no TCP API to probe — skip it (avoids a connect timeout).
358+
if (!this.httpOnly) {
359+
const tcpInfo = await this.tcpClient.getPrinterInfo();
360+
if (tcpInfo) {
361+
// If machineInfo hasn't already set isPro, we can use TCP info as a fallback.
362+
// However, machineInfo.IsPro (derived from detail.name) should be more reliable.
363+
// This line effectively gets overridden by cacheDetails if machineInfo.IsPro is set.
364+
if (tcpInfo.TypeName.includes('Pro') && !machineInfo.IsPro && !machineInfo.IsAD5X) {
365+
// Only set this if not already determined by machineInfo, and it's not an AD5X
366+
this.isPro = true;
367+
}
368+
} else {
369+
console.error('Unable to get PrinterInfo from TcpAPI, some details might be incomplete');
332370
}
333-
} else {
334-
console.error('Unable to get PrinterInfo from TcpAPI, some details might be incomplete');
371+
// we should probably return false if tcpInfo is null here, like we do for machineInfo,
372+
// but for now, we'll let cacheDetails be the primary source of truth for these flags.
335373
}
336-
// we should probably return false if tcpInfo is null here, like we do for machineInfo,
337-
// but for now, we'll let cacheDetails be the primary source of truth for these flags.
338374

339375
return this.cacheDetails(machineInfo);
340376
} catch (error: unknown) {

src/api/controls/Control.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,4 +615,48 @@ describe('Control', () => {
615615
);
616616
});
617617
});
618+
619+
// HTTP-only printers (Creator 5 / 5 Pro) have no TCP control channel, so the
620+
// TCP-bound operations must fail cleanly (return false) without touching the
621+
// dead socket, rather than hang.
622+
describe('HTTP-only mode (no TCP channel)', () => {
623+
let httpControl: Control;
624+
625+
beforeEach(() => {
626+
const httpClient = { ...mockFiveMClient, httpOnly: true } as any;
627+
httpControl = new Control(httpClient);
628+
});
629+
630+
it('homeAxes returns false without calling the TCP client', async () => {
631+
const result = await httpControl.homeAxes();
632+
expect(result).toBe(false);
633+
expect(mockTcpClient.homeAxes).not.toHaveBeenCalled();
634+
});
635+
636+
it('homeAxesRapid returns false without calling the TCP client', async () => {
637+
const result = await httpControl.homeAxesRapid();
638+
expect(result).toBe(false);
639+
expect(mockTcpClient.rapidHome).not.toHaveBeenCalled();
640+
});
641+
642+
it('turnRunoutSensorOn/Off return false without calling the TCP client', async () => {
643+
expect(await httpControl.turnRunoutSensorOn()).toBe(false);
644+
expect(await httpControl.turnRunoutSensorOff()).toBe(false);
645+
expect(mockTcpClient.turnRunoutSensorOn).not.toHaveBeenCalled();
646+
expect(mockTcpClient.turnRunoutSensorOff).not.toHaveBeenCalled();
647+
});
648+
649+
it('filament load operations return false without calling the TCP client', async () => {
650+
expect(await httpControl.loadFilament()).toBe(false);
651+
expect(await httpControl.finishFilamentLoad()).toBe(false);
652+
expect(mockTcpClient.loadFilament).not.toHaveBeenCalled();
653+
expect(mockTcpClient.finishFilamentLoad).not.toHaveBeenCalled();
654+
});
655+
656+
it('HTTP-based controls (LED) still dispatch over HTTP', async () => {
657+
mockedAxios.post.mockResolvedValue({ status: 200, data: { code: 0, message: 'ok' } });
658+
await httpControl.setLedOn();
659+
expect(mockedAxios.post).toHaveBeenCalled();
660+
});
661+
});
618662
});

src/api/controls/Control.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,29 @@ export class Control {
4040
this.tcpClient = client.tcpClient;
4141
}
4242

43+
/**
44+
* Guards a TCP-only operation. HTTP-only printers (Creator 5 / 5 Pro) have no
45+
* legacy TCP control channel, so these operations cannot run. Logs and returns
46+
* false rather than hanging on a dead socket. The Creator 5 exposes some of
47+
* these via its HTTP API (Klipper g-code), but the request schemas are not yet
48+
* confirmed; route them here once they are.
49+
* @param op Human-readable operation name for the log message.
50+
* @returns True if a TCP operation may proceed, false if it must be skipped.
51+
*/
52+
private canUseTcp(op: string): boolean {
53+
if (this.client.httpOnly) {
54+
console.log(`${op}() unavailable: printer has no TCP control channel (HTTP-only).`);
55+
return false;
56+
}
57+
return true;
58+
}
59+
4360
/**
4461
* Homes the X, Y, and Z axes of the printer.
4562
* @returns A Promise that resolves to true if the command is successful, false otherwise.
4663
*/
4764
public async homeAxes(): Promise<boolean> {
65+
if (!this.canUseTcp('homeAxes')) return false;
4866
return await this.tcpClient.homeAxes();
4967
}
5068

@@ -53,6 +71,7 @@ export class Control {
5371
* @returns A Promise that resolves to true if the command is successful, false otherwise.
5472
*/
5573
public async homeAxesRapid(): Promise<boolean> {
74+
if (!this.canUseTcp('homeAxesRapid')) return false;
5675
return await this.tcpClient.rapidHome();
5776
}
5877

@@ -182,6 +201,7 @@ export class Control {
182201
* @returns A Promise that resolves to true if the command is successful, false otherwise.
183202
*/
184203
public async turnRunoutSensorOn(): Promise<boolean> {
204+
if (!this.canUseTcp('turnRunoutSensorOn')) return false;
185205
return await this.tcpClient.turnRunoutSensorOn();
186206
}
187207

@@ -190,6 +210,7 @@ export class Control {
190210
* @returns A Promise that resolves to true if the command is successful, false otherwise.
191211
*/
192212
public async turnRunoutSensorOff(): Promise<boolean> {
213+
if (!this.canUseTcp('turnRunoutSensorOff')) return false;
193214
return await this.tcpClient.turnRunoutSensorOff();
194215
}
195216

@@ -201,6 +222,7 @@ export class Control {
201222
* @returns A Promise that resolves to true if the command is successful, false otherwise.
202223
*/
203224
public async prepareFilamentLoad(filament: Filament): Promise<boolean> {
225+
if (!this.canUseTcp('prepareFilamentLoad')) return false;
204226
return await this.tcpClient.prepareFilamentLoad(filament);
205227
}
206228

@@ -209,6 +231,7 @@ export class Control {
209231
* @returns A Promise that resolves to true if the command is successful, false otherwise.
210232
*/
211233
public async loadFilament(): Promise<boolean> {
234+
if (!this.canUseTcp('loadFilament')) return false;
212235
return await this.tcpClient.loadFilament();
213236
}
214237

@@ -217,6 +240,7 @@ export class Control {
217240
* @returns A Promise that resolves to true if the command is successful, false otherwise.
218241
*/
219242
public async finishFilamentLoad(): Promise<boolean> {
243+
if (!this.canUseTcp('finishFilamentLoad')) return false;
220244
return await this.tcpClient.finishFilamentLoad();
221245
}
222246

src/api/controls/Files.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@ export class Files {
2727
}
2828

2929
/**
30-
* Retrieves a list of all G-code files stored locally on the printer via TCP.
30+
* Retrieves a list of G-code files stored locally on the printer.
31+
* Dual-API printers use the TCP file list; HTTP-only printers (Creator 5 /
32+
* 5 Pro) have no TCP channel, so this falls back to the HTTP `/gcodeList`
33+
* (the 10 most-recent files) and returns their names.
3134
* @returns A Promise that resolves to an array of file names (strings).
3235
*/
3336
public async getLocalFileList(): Promise<string[]> {
37+
if (this.client.httpOnly) {
38+
const recent = await this.getRecentFileList();
39+
return recent.map((entry) => entry.gcodeFileName);
40+
}
3441
return await this.client.tcpClient.getFileListAsync();
3542
}
3643

0 commit comments

Comments
 (0)