Skip to content

Commit 2ea04ff

Browse files
GhostTypesclaude
andcommitted
feat(ad5x): add IFS material-station slot write commands
Backport configureSlot (msConfig_cmd) and slotAction (ms_cmd) from ff-5mp-api-kt so AD5X consumers can set the material/color of an IFS slot and drive load/unload/cancel over HTTP. Releases 1.3.2. - Control.configureSlot(slot, materialName, hexRgb): sends msConfig_cmd; strips a leading "#" from the color to match the RRGGBB wire format. - Control.slotAction(slot, action): sends ms_cmd (load/unload/cancel). - SlotAction enum (Load=0, Unload=1, Cancel=2), exported from the root. - Both methods gate on isAD5X and return false on non-AD5X printers. - Commands.MaterialStationConfigCmd / MaterialStationCmd constants. Tests cover the hex-prefix strip, all three action codes, and AD5X gating. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5753e89 commit 2ea04ff

7 files changed

Lines changed: 177 additions & 1 deletion

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [1.3.2] - 2026-06-05
11+
12+
### Added
13+
14+
- AD5X Intelligent Filament Station (IFS) slot write commands, backported from `ff-5mp-api-kt`:
15+
- `Control.configureSlot(slot, materialName, hexRgb)` — sends `msConfig_cmd` to set a slot's material name and color (a leading `#` on the color is stripped to match the `RRGGBB` wire format).
16+
- `Control.slotAction(slot, action)` — sends `ms_cmd` to load / unload / cancel a slot.
17+
- `SlotAction` enum (`Load = 0`, `Unload = 1`, `Cancel = 2`) exported from the package root.
18+
- Both methods are gated on `isAD5X` and return `false` on non-AD5X printers.
19+
- `Commands.MaterialStationConfigCmd` (`msConfig_cmd`) and `Commands.MaterialStationCmd` (`ms_cmd`) constants.
20+
1021
## [1.3.1] - 2026-05-08
1122

1223
### Fixed

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

src/api/controls/Control.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import axios from 'axios';
77
import { beforeEach, describe, expect, it, vi } from 'vitest';
88
import type { FiveMClient } from '../../FiveMClient';
9+
import { SlotAction } from '../../models/ff-models';
910
import type { FlashForgeClient } from '../../tcpapi/FlashForgeClient';
1011
import { Commands } from '../server/Commands';
1112
import { Endpoints } from '../server/Endpoints';
@@ -495,6 +496,100 @@ describe('Control', () => {
495496
});
496497
});
497498

499+
describe('AD5X material station (IFS) slot operations', () => {
500+
beforeEach(() => {
501+
mockedAxios.post.mockResolvedValue({
502+
status: 200,
503+
data: { code: 0, message: 'Success' },
504+
});
505+
mockFiveMClient.isAD5X = true;
506+
});
507+
508+
it('should configure a slot and strip the leading # from the color', async () => {
509+
const result = await control.configureSlot(1, 'PLA', '#FF0000');
510+
511+
expect(result).toBe(true);
512+
expect(mockedAxios.post).toHaveBeenCalledWith(
513+
`http://printer:8898${Endpoints.Control}`,
514+
expect.objectContaining({
515+
payload: {
516+
cmd: Commands.MaterialStationConfigCmd,
517+
args: { slot: 1, mt: 'PLA', rgb: 'FF0000' },
518+
},
519+
}),
520+
expect.any(Object)
521+
);
522+
});
523+
524+
it('should accept a color that already lacks a # prefix', async () => {
525+
await control.configureSlot(2, 'PETG', '46328E');
526+
527+
expect(mockedAxios.post).toHaveBeenCalledWith(
528+
`http://printer:8898${Endpoints.Control}`,
529+
expect.objectContaining({
530+
payload: {
531+
cmd: Commands.MaterialStationConfigCmd,
532+
args: { slot: 2, mt: 'PETG', rgb: '46328E' },
533+
},
534+
}),
535+
expect.any(Object)
536+
);
537+
});
538+
539+
it('should send a load slot action', async () => {
540+
const result = await control.slotAction(1, SlotAction.Load);
541+
542+
expect(result).toBe(true);
543+
expect(mockedAxios.post).toHaveBeenCalledWith(
544+
`http://printer:8898${Endpoints.Control}`,
545+
expect.objectContaining({
546+
payload: {
547+
cmd: Commands.MaterialStationCmd,
548+
args: { slot: 1, action: 0 },
549+
},
550+
}),
551+
expect.any(Object)
552+
);
553+
});
554+
555+
it('should send unload and cancel slot actions with the correct codes', async () => {
556+
await control.slotAction(3, SlotAction.Unload);
557+
expect(mockedAxios.post).toHaveBeenLastCalledWith(
558+
`http://printer:8898${Endpoints.Control}`,
559+
expect.objectContaining({
560+
payload: {
561+
cmd: Commands.MaterialStationCmd,
562+
args: { slot: 3, action: 1 },
563+
},
564+
}),
565+
expect.any(Object)
566+
);
567+
568+
await control.slotAction(0, SlotAction.Cancel);
569+
expect(mockedAxios.post).toHaveBeenLastCalledWith(
570+
`http://printer:8898${Endpoints.Control}`,
571+
expect.objectContaining({
572+
payload: {
573+
cmd: Commands.MaterialStationCmd,
574+
args: { slot: 0, action: 2 },
575+
},
576+
}),
577+
expect.any(Object)
578+
);
579+
});
580+
581+
it('should refuse slot operations on non-AD5X printers', async () => {
582+
mockFiveMClient.isAD5X = false;
583+
584+
const configResult = await control.configureSlot(1, 'PLA', '#FF0000');
585+
const actionResult = await control.slotAction(1, SlotAction.Load);
586+
587+
expect(configResult).toBe(false);
588+
expect(actionResult).toBe(false);
589+
expect(mockedAxios.post).not.toHaveBeenCalled();
590+
});
591+
});
592+
498593
describe('sendJobControlCmd', () => {
499594
it('should send job control command', async () => {
500595
mockedAxios.post.mockResolvedValue({

src/api/controls/Control.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import axios from 'axios';
88
import type { FiveMClient } from '../../FiveMClient';
99
import type { FlashForgeClient } from '../../tcpapi/FlashForgeClient';
10+
import type { SlotAction } from '../../models/ff-models';
1011
import type { Filament } from '../filament/Filament';
1112
import { NetworkUtils } from '../network/NetworkUtils';
1213
import { Commands } from '../server/Commands';
@@ -219,6 +220,58 @@ export class Control {
219220
return await this.tcpClient.finishFilamentLoad();
220221
}
221222

223+
// AD5X Intelligent Filament Station (IFS) slot operations
224+
225+
/**
226+
* Configures the material name and color metadata for an AD5X material station slot.
227+
* This information is shown on the printer UI and used for print validation; it does
228+
* not move any filament. Only available on AD5X printers.
229+
*
230+
* The firmware accepts arbitrary material/color strings, but only the recognized
231+
* materials and the 24-color UI palette render an icon on the printer — callers that
232+
* want a clean display should map to the nearest recognized values first.
233+
*
234+
* @param slot The slot number (1-4).
235+
* @param materialName The material type (e.g., "PLA", "PETG").
236+
* @param hexRgb The color as a hex string; a leading "#" is stripped to match the wire format ("RRGGBB").
237+
* @returns A Promise that resolves to true if the command is successful, false otherwise.
238+
*/
239+
public async configureSlot(
240+
slot: number,
241+
materialName: string,
242+
hexRgb: string
243+
): Promise<boolean> {
244+
if (!this.client.isAD5X) {
245+
console.log('configureSlot() error, material station only available on AD5X.');
246+
return false;
247+
}
248+
return await this.sendControlCommand(Commands.MaterialStationConfigCmd, {
249+
slot,
250+
mt: materialName,
251+
rgb: hexRgb.replace(/^#/, ''),
252+
});
253+
}
254+
255+
/**
256+
* Performs a load, unload, or cancel operation on an AD5X material station slot.
257+
* Only available on AD5X printers. Operations are asynchronous — poll
258+
* `info.get()` / `matlStationInfo.stateAction` to monitor completion.
259+
*
260+
* @param slot The target slot number (1-4). For {@link SlotAction.Cancel} the slot is ignored by firmware.
261+
* @param action The slot action to perform (load/unload/cancel).
262+
* @returns A Promise that resolves to true if the command is successful, false otherwise.
263+
*/
264+
public async slotAction(slot: number, action: SlotAction): Promise<boolean> {
265+
if (!this.client.isAD5X) {
266+
console.log('slotAction() error, material station only available on AD5X.');
267+
return false;
268+
}
269+
return await this.sendControlCommand(Commands.MaterialStationCmd, {
270+
slot,
271+
action,
272+
});
273+
}
274+
222275
// Internal methods for sending commands
223276

224277
/**

src/api/server/Commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ export class Commands {
2323
static readonly CameraControlCmd = 'streamCtrl_cmd';
2424
/** Command for controlling the printer's temperatures (e.g., setting extruder or bed temperature via HTTP, if supported). */
2525
static readonly TempControlCmd = 'temperatureCtl_cmd';
26+
/** Command for configuring an AD5X material station slot's material name and color (IFS). */
27+
static readonly MaterialStationConfigCmd = 'msConfig_cmd';
28+
/** Command for AD5X material station slot load/unload/cancel actions (IFS). */
29+
static readonly MaterialStationCmd = 'ms_cmd';
2630
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export {
4444
FFPrinterDetail,
4545
MachineState,
4646
MatlStationInfo,
47+
SlotAction,
4748
SlotInfo,
4849
Temperature as TemperatureInterface,
4950
} from './models/ff-models';

src/models/ff-models.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,18 @@ export interface FFGcodeFileEntry {
378378
// For now, focusing on AD5X structure.
379379
}
380380

381+
/**
382+
* AD5X material station (IFS) slot operation, carrying the on-wire `ms_cmd` action code.
383+
* - `Load` (0): load filament from the slot into the extruder.
384+
* - `Unload` (1): retract filament from the extruder back into the slot.
385+
* - `Cancel` (2): cancel the current IFS operation.
386+
*/
387+
export enum SlotAction {
388+
Load = 0,
389+
Unload = 1,
390+
Cancel = 2,
391+
}
392+
381393
// --- AD5X Local Job Start Interfaces ---
382394

383395
/**

0 commit comments

Comments
 (0)