@aubron/ankerts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,766 @@
1
+ type Region = "us" | "eu";
2
+ interface AnkerAccount {
3
+ user_id: string;
4
+ auth_token: string;
5
+ email: string;
6
+ region: Region;
7
+ country?: string;
8
+ }
9
+ interface AnkerPrinter {
10
+ id: string;
11
+ sn: string;
12
+ name: string;
13
+ model: string;
14
+ /** PPPP device id (`p2p_did`), e.g. `USPRAKM-000994-YYLLG`. */
15
+ duid: string;
16
+ /** LAN IP — populated by discovery, not login. Empty until discovered. */
17
+ ip_addr: string;
18
+ wifi_mac: string;
19
+ /** Per-printer MQTT AES key, hex-encoded (`secret_key` from the cloud). */
20
+ mqtt_key: string;
21
+ /** PPPP device secret key (`dsk_key`). */
22
+ p2p_key: string;
23
+ api_hosts: string[];
24
+ p2p_hosts: string[];
25
+ /** MQTT broker host (region-derived if absent). */
26
+ mqtt_host?: string;
27
+ }
28
+ interface AnkerConfig {
29
+ account: AnkerAccount | null;
30
+ printers: AnkerPrinter[];
31
+ /** DUID of the default-selected printer (`printer select`). */
32
+ selected?: string;
33
+ }
34
+ /** Region → cloud MQTT broker host. */
35
+ declare const MQTT_HOSTS: Record<Region, string>;
36
+ /** Region → cloud HTTPS app-API host. */
37
+ declare const API_HOSTS: Record<Region, string>;
38
+ declare const REDACTED = "<redacted>";
39
+ /** MQTT username/password are derived from the account (reference `model.py`). */
40
+ declare const mqttUsername: (acct: AnkerAccount) => string;
41
+ declare const mqttPassword: (acct: AnkerAccount) => string;
42
+ /** Resolve the per-printer MQTT broker host (explicit, else region default). */
43
+ declare function mqttHostFor(acct: AnkerAccount, printer: AnkerPrinter): string;
44
+ /** OS-appropriate config directory for the `ankerts` app. */
45
+ declare function configDir(): string;
46
+ /** A JSON-file-backed configuration store. */
47
+ declare class ConfigStore {
48
+ readonly path: string;
49
+ constructor(path?: string);
50
+ exists(): boolean;
51
+ load(): AnkerConfig;
52
+ save(config: AnkerConfig): void;
53
+ /** Mutate-and-persist helper. */
54
+ update(fn: (config: AnkerConfig) => void): AnkerConfig;
55
+ }
56
+ /** Return a copy of the config with secrets masked (unless `reveal`). */
57
+ declare function redactConfig(config: AnkerConfig, reveal?: boolean): AnkerConfig;
58
+ /**
59
+ * Resolve a printer reference (DUID, serial, name, or numeric index) against the
60
+ * configured list. Returns the printer or null.
61
+ */
62
+ declare function findPrinter(config: AnkerConfig, ref: string | number): AnkerPrinter | null;
63
+
64
+ /**
65
+ * Gcode request/response handling (brief §6).
66
+ *
67
+ * IMPORTANT — corrected against real M5 hardware (see the project memory). The
68
+ * brief assumed a gcode reply arrives across multiple `0x0413` frames to be
69
+ * reassembled. It does not: each send yields exactly ONE reply whose `resData`
70
+ * is a point-in-time snapshot of the firmware's ~512-byte serial ring buffer,
71
+ * and the snapshot is RACY. Short replies (M105) come back whole; replies that
72
+ * exceed the window are capped at 512 bytes; and a reply caught mid-write
73
+ * truncates early with a `+ringbuf:<a>,512,<b>` marker (the buffer reporting its
74
+ * own state) — this is the real `echo:Ad` bug.
75
+ *
76
+ * So we still concatenate whatever chunks the transport collected, strip ANSI,
77
+ * and parse — but the key honesty fix is `truncated`: rather than silently
78
+ * handing back a partial line (as the reference did), we DETECT truncation and
79
+ * flag it, so a caller never mistakes `echo:Ad` for a complete reply.
80
+ *
81
+ * This module is pure: the transport collects the reply and decides completion;
82
+ * these functions turn the collected chunks into a result, keeping the parser
83
+ * fully unit-testable against the observed captures.
84
+ */
85
+ interface GcodeResult {
86
+ /** Echoed input command. */
87
+ command: string;
88
+ /** FULL reassembled text, ANSI-free, all frames concatenated in order. */
89
+ raw: string;
90
+ /** `raw` split on newlines, trimmed, with empty lines removed. */
91
+ lines: string[];
92
+ /** A terminal `ok` line was seen. */
93
+ ok: boolean;
94
+ /** False iff an `echo:Unknown command` line was seen. */
95
+ recognized: boolean;
96
+ /** Parsed `echo:Key=Value` / `Key:Value` pairs, e.g. `{ "Advance K": "0.00" }`. */
97
+ fields: Record<string, string>;
98
+ /** Parsed Marlin report lines keyed by code, e.g. `{ "M900": "K0.00" }`. */
99
+ reports: Record<string, string>;
100
+ /** Wall-clock time spent collecting the response. */
101
+ durationMs: number;
102
+ /** Completion signal never arrived within the timeout. */
103
+ timedOut: boolean;
104
+ /**
105
+ * The reply is incomplete: it hit the firmware's ~512-byte snapshot window, or
106
+ * carries a `+ringbuf:` marker showing the serial buffer was mid-write. When
107
+ * true, `raw`/`fields`/`reports` may be partial — do not trust them as the
108
+ * full command output. (The reference silently returned these as if complete.)
109
+ */
110
+ truncated: boolean;
111
+ /** How many reply chunks were collected (diagnostic; usually 1 — see header). */
112
+ frames: number;
113
+ }
114
+ declare function stripAnsi(text: string): string;
115
+ /** Concatenate reply chunks in arrival order; strip ANSI; normalize newlines. */
116
+ declare function reassembleRaw(chunks: readonly string[]): string;
117
+ /** Split reassembled text into trimmed, non-empty lines. */
118
+ declare function splitLines(raw: string): string[];
119
+ /**
120
+ * Does the reassembled text END with a terminal `ok`? Marlin terminates a
121
+ * command's output with an `ok` line (sometimes carrying data, e.g. M105's
122
+ * `ok T:...`). The check is on the LAST non-empty line, not any line: the M5
123
+ * firmware emits a leading `ok` (and a double-`ok`) BEFORE the real output (e.g.
124
+ * `ok\n\nok\n\n+ringbuf:...\necho:Advance K=0.00\nok`), so matching any `ok`
125
+ * would stop reassembly early and truncate the reply — the `echo:Ad` bug. Used
126
+ * by the transport's completion detection.
127
+ */
128
+ declare function gcodeHasTerminalOk(raw: string): boolean;
129
+ /**
130
+ * Parse the collected reply chunks for one command into a complete GcodeResult.
131
+ *
132
+ * @param command the gcode that was sent (echoed back)
133
+ * @param chunks `resData` strings from each reply frame, in arrival order
134
+ * @param meta timing/diagnostic info supplied by the transport
135
+ */
136
+ declare function parseGcodeResult(command: string, chunks: readonly string[], meta: {
137
+ durationMs: number;
138
+ timedOut: boolean;
139
+ }): GcodeResult;
140
+
141
+ /**
142
+ * Printer status/telemetry normalization (brief §5 + §4A).
143
+ *
144
+ * Notices stream continuously over MQTT `.../notice`. They carry raw firmware
145
+ * units — temperatures in 1/100 °C, progress in 1/100 % — which we normalize so
146
+ * callers never touch raw values. Crucially, for third-party-sliced gcode the
147
+ * firmware's headline ETA is garbage (§4A); we detect that and mark the ETA
148
+ * unreliable rather than surfacing a bogus 20,000-hour estimate.
149
+ */
150
+ type JobState = "idle" | "printing" | "paused" | "complete" | "failed" | "cancelled";
151
+ interface PrinterStatus {
152
+ nozzle: {
153
+ current: number;
154
+ target: number;
155
+ };
156
+ bed: {
157
+ current: number;
158
+ target: number;
159
+ };
160
+ job?: {
161
+ name: string;
162
+ state: JobState;
163
+ progressPct: number;
164
+ layer: number;
165
+ totalLayers: number;
166
+ etaSeconds?: number;
167
+ etaReliable?: boolean;
168
+ filamentUsed?: number;
169
+ filamentUnit?: string;
170
+ speedMmS?: number;
171
+ speedFactorPct?: number;
172
+ };
173
+ raw: Record<string, unknown>;
174
+ }
175
+ /** A raw notice payload: at minimum a `commandType`, plus arbitrary fields. */
176
+ type RawNotice = Record<string, unknown> & {
177
+ commandType?: number;
178
+ };
179
+ /**
180
+ * Decide whether a `print_schedule` notice's ETA is trustworthy. Native
181
+ * AnkerMake/eufyMake gcode carries the proprietary time metadata the firmware
182
+ * needs; third-party slicers (OrcaSlicer/PrusaSlicer) do not, so the firmware
183
+ * emits an inconsistent `time` vs `totalTime` and an absurd remaining time.
184
+ */
185
+ declare function isEtaReliable(schedule: RawNotice): boolean;
186
+ /**
187
+ * Fold a set of notice payloads into a normalized {@link PrinterStatus}.
188
+ * The latest notice of each type wins. `etaReliableOverride` lets a caller that
189
+ * already knows the file is third-party force the ETA to be marked unreliable.
190
+ */
191
+ declare function normalizeStatus(notices: readonly RawNotice[], opts?: {
192
+ etaReliableOverride?: boolean;
193
+ }): PrinterStatus;
194
+
195
+ /**
196
+ * HTTPS cloud API transport (brief §5, §8) — ported from the reference
197
+ * `libflagship/httpapi.py` and the `anselor` `config login` flow.
198
+ *
199
+ * Handles email/password login (ECDH-encrypted password), profile + printer
200
+ * list retrieval, and PPPP key fetch — assembling a full {@link AnkerConfig}.
201
+ * Region-coded endpoints; the login captcha branch surfaces as a structured
202
+ * {@link AuthError} (never a hang).
203
+ */
204
+
205
+ type Json = Record<string, unknown>;
206
+ declare function guessRegion(countryCode: string): Region;
207
+ interface LoginResult {
208
+ auth_token: string;
209
+ user_id: string;
210
+ email: string;
211
+ region: Region;
212
+ ab_code?: string;
213
+ }
214
+ interface LoginOptions {
215
+ email: string;
216
+ password: string;
217
+ /** 2-letter country code selecting the API region. */
218
+ country: string;
219
+ captchaId?: string;
220
+ captchaAnswer?: string;
221
+ }
222
+ /** Low-level cloud HTTPS client for a single region. */
223
+ declare class AnkerHttpApi {
224
+ readonly region: Region;
225
+ private readonly authToken?;
226
+ constructor(region: Region, authToken?: string | undefined);
227
+ private base;
228
+ private request;
229
+ /** Translate a non-zero API response into a structured error (captcha-aware). */
230
+ private apiError;
231
+ login(opts: LoginOptions): Promise<LoginResult>;
232
+ profile(): Promise<{
233
+ user_id: string;
234
+ email: string;
235
+ country: string;
236
+ }>;
237
+ queryFdmList(): Promise<Json[]>;
238
+ getDskKeys(stationSns: string[]): Promise<Record<string, string>>;
239
+ }
240
+ /**
241
+ * Full login → config bootstrap (reference `fetch_config_by_login` +
242
+ * `load_config_from_api`). Logs in, then pulls profile, printer list, and PPPP
243
+ * keys, returning a ready-to-store {@link AnkerConfig}.
244
+ */
245
+ declare function loginAndBuildConfig(opts: LoginOptions): Promise<AnkerConfig>;
246
+
247
+ declare const PPPP_LAN_PORT = 32108;
248
+ declare enum PpppState {
249
+ Idle = 1,
250
+ Connecting = 2,
251
+ Connected = 3,
252
+ Disconnected = 4
253
+ }
254
+ interface LanPrinter {
255
+ duid: string;
256
+ ip: string;
257
+ }
258
+ interface UploadProgress {
259
+ sent: number;
260
+ total: number;
261
+ pct: number;
262
+ }
263
+ interface PpppClientOptions {
264
+ duid: string;
265
+ host: string;
266
+ port?: number;
267
+ log?: (msg: string) => void;
268
+ }
269
+ /**
270
+ * One-shot LAN discovery: broadcast a `LAN_SEARCH` and collect `PUNCH_PKT`
271
+ * replies (each yields a DUID + source IP). Retried by the caller — UDP
272
+ * broadcast is flaky (the §4 lesson).
273
+ */
274
+ declare function discoverLan(opts?: {
275
+ timeoutMs?: number;
276
+ bindAddr?: string;
277
+ log?: (m: string) => void;
278
+ }): Promise<LanPrinter[]>;
279
+ declare class AnkerPpppClient {
280
+ private sock?;
281
+ private addr;
282
+ private readonly duid;
283
+ private readonly chans;
284
+ private pollTimer?;
285
+ private readonly log;
286
+ state: PpppState;
287
+ private connectResolve?;
288
+ private connectReject?;
289
+ constructor(opts: PpppClientOptions);
290
+ private get host();
291
+ private send;
292
+ /** Connect over the LAN via the punch/ready handshake. */
293
+ connect(timeoutMs?: number): Promise<void>;
294
+ private failConnect;
295
+ private pollChannels;
296
+ private process;
297
+ stop(): void;
298
+ /** Send one channel-1 AABB request and await the printer's reply byte. */
299
+ private aabbRequest;
300
+ /**
301
+ * Upload a gcode file and (by default) start the print. Mirrors the reference
302
+ * web service: XZYH(P2P_SEND_FILE) → AABB BEGIN (metadata) → AABB DATA chunks
303
+ * → AABB END (starts printing).
304
+ */
305
+ uploadFile(filename: string, data: Buffer, opts?: {
306
+ userName?: string;
307
+ userId?: string;
308
+ machineId?: string;
309
+ start?: boolean;
310
+ onProgress?: (p: UploadProgress) => void;
311
+ }): Promise<{
312
+ name: string;
313
+ size: number;
314
+ md5: string;
315
+ started: boolean;
316
+ }>;
317
+ }
318
+
319
+ /**
320
+ * Waitable conditions over server-authoritative printer state (brief §6A).
321
+ *
322
+ * Every wait is re-derivable from a fresh status snapshot, which is what makes
323
+ * waits re-attachable: an agent that crashed or timed out mid-wait can re-issue
324
+ * the same wait and it resolves against current state — no dependence on a live
325
+ * subscription. This module is the pure predicate layer; `AnkerClient.waitFor`
326
+ * supplies the polling/event loop.
327
+ */
328
+
329
+ type WaitCondition = {
330
+ kind: "connected";
331
+ } | {
332
+ kind: "lan";
333
+ } | {
334
+ kind: "nozzle";
335
+ atLeast: number;
336
+ } | {
337
+ kind: "bed";
338
+ atLeast: number;
339
+ } | {
340
+ kind: "temp-stable";
341
+ } | {
342
+ kind: "printing";
343
+ } | {
344
+ kind: "idle";
345
+ } | {
346
+ kind: "progress";
347
+ atLeast: number;
348
+ } | {
349
+ kind: "layer";
350
+ atLeast: number;
351
+ } | {
352
+ kind: "complete";
353
+ } | {
354
+ kind: "failed";
355
+ } | {
356
+ kind: "cancelled";
357
+ } | {
358
+ kind: "runout";
359
+ };
360
+ /**
361
+ * Parse a CLI condition string such as `nozzle>=210`, `progress>=50`, or
362
+ * `complete` into a {@link WaitCondition}.
363
+ */
364
+ declare function parseWaitCondition(input: string): WaitCondition;
365
+ /** Render a condition back to its canonical string form. */
366
+ declare function describeWaitCondition(cond: WaitCondition): string;
367
+ /**
368
+ * Evaluate a status-derived condition against a snapshot. Transport-only
369
+ * conditions (`connected`, `lan`) return `null` — the caller resolves those from
370
+ * transport state, not status.
371
+ */
372
+ declare function conditionHolds(cond: WaitCondition, status: PrinterStatus): boolean | null;
373
+
374
+ type PrinterEvent = RawNotice;
375
+ type Unsubscribe = () => void;
376
+ type Logger = (msg: string) => void;
377
+ interface AnkerClientOptions {
378
+ store?: ConfigStore;
379
+ log?: Logger;
380
+ printer?: string | number;
381
+ /** Disable TLS verification on transports (testing only). */
382
+ insecure?: boolean;
383
+ }
384
+ interface GcodeOptions {
385
+ timeoutMs?: number;
386
+ quietMs?: number;
387
+ wait?: boolean;
388
+ /** Append M400 semantics so the call returns on true motion completion. */
389
+ waitMotion?: boolean;
390
+ }
391
+ interface WaitOptions {
392
+ source?: "events" | "poll" | "hybrid";
393
+ pollMs?: number;
394
+ timeoutMs?: number;
395
+ onTick?: (status: PrinterStatus) => void;
396
+ }
397
+ interface MachineSettings {
398
+ /**
399
+ * The M503 result. NOTE: M503's output usually exceeds the firmware's
400
+ * ~512-byte reply window, so this can be partial — check `result.truncated`
401
+ * before treating `reports` as the complete settings set.
402
+ */
403
+ result: GcodeResult;
404
+ reports: Record<string, string>;
405
+ linearAdvanceK?: number;
406
+ hotendPid?: {
407
+ p: number;
408
+ i: number;
409
+ d: number;
410
+ };
411
+ steps?: string;
412
+ probeZOffset?: number;
413
+ }
414
+ interface JobResult {
415
+ name: string;
416
+ size: number;
417
+ md5: string;
418
+ started: boolean;
419
+ transport: "lan";
420
+ duid: string;
421
+ ip: string;
422
+ }
423
+ interface LanDiscoverOptions {
424
+ retries?: number;
425
+ timeoutMs?: number;
426
+ store?: boolean;
427
+ }
428
+ /** Per-command default gcode timeouts by latency class (§6A). */
429
+ declare function defaultTimeoutFor(command: string): number;
430
+ declare class AnkerClient {
431
+ private config;
432
+ private readonly store?;
433
+ private readonly log;
434
+ private readonly insecure;
435
+ private printerRef?;
436
+ private mqtt?;
437
+ constructor(config: AnkerConfig, opts?: AnkerClientOptions);
438
+ static login(opts: LoginOptions & {
439
+ save?: boolean;
440
+ }): Promise<AnkerClient>;
441
+ static fromStoredConfig(path?: string, opts?: AnkerClientOptions): AnkerClient;
442
+ getConfig(): AnkerConfig;
443
+ listPrinters(): AnkerPrinter[];
444
+ selectPrinter(ref: string | number): AnkerPrinter;
445
+ /** The currently selected printer (explicit ref → stored default → first). */
446
+ currentPrinter(): AnkerPrinter;
447
+ private notFound;
448
+ private account;
449
+ private ensureMqtt;
450
+ /** Close any open transports. */
451
+ close(): Promise<void>;
452
+ getStatus(): Promise<PrinterStatus>;
453
+ subscribeEvents(handler: (e: PrinterEvent) => void): Promise<Unsubscribe>;
454
+ gcode(command: string, opts?: GcodeOptions): Promise<GcodeResult>;
455
+ /** Run many commands, yielding each result as it completes (NDJSON-friendly). */
456
+ gcodeBatch(commands: string[], opts?: GcodeOptions): AsyncIterable<GcodeResult>;
457
+ snapshotState(): Promise<MachineSettings>;
458
+ restoreState(): Promise<GcodeResult>;
459
+ cancelJob(): Promise<void>;
460
+ pauseJob(): Promise<void>;
461
+ resumeJob(): Promise<void>;
462
+ discoverLan(opts?: LanDiscoverOptions): Promise<{
463
+ duid: string;
464
+ ip: string;
465
+ }[]>;
466
+ private persistDiscoveredIps;
467
+ /**
468
+ * Upload a gcode file over the LAN and (by default) start the print. Auto-runs
469
+ * discovery if the printer's IP is unknown; if it's still unreachable, throws
470
+ * {@link TransportUnavailableError} (exit 6) naming the transport and the fix.
471
+ */
472
+ uploadAndPrint(file: string | Buffer, opts?: {
473
+ start?: boolean;
474
+ transport?: "lan" | "auto";
475
+ /**
476
+ * Auto-fix the LCD ETA/filament display by transcoding a third-party
477
+ * slicer's embedded estimate into the Anker `;TIME:` header. ON by default
478
+ * (it auto-detects: a no-op on natively-sliced files, which already carry
479
+ * `;TIME:`). Operates on a copy — never mutates the file. Set `false` to
480
+ * upload the file byte-for-byte untouched.
481
+ */
482
+ fixMetadata?: boolean;
483
+ filename?: string;
484
+ onProgress?: (p: UploadProgress) => void;
485
+ }): Promise<JobResult>;
486
+ /**
487
+ * Block until `cond` holds, resolving the current status. Re-attachable: it
488
+ * re-derives state from fresh snapshots, so a re-issued wait still resolves.
489
+ * Rejects with a retriable {@link TimeoutError} (exit 5) on timeout.
490
+ */
491
+ waitFor(cond: WaitCondition, opts?: WaitOptions): Promise<PrinterStatus>;
492
+ }
493
+
494
+ /**
495
+ * Typed errors (brief §3, §7). Every error the SDK throws carries a structured,
496
+ * machine-parseable shape — code, transport, retriability, an actionable hint,
497
+ * and the echoed input — and maps to a documented CLI exit code. The SDK never
498
+ * writes to stderr or exits; it throws these and lets the CLI format + map them.
499
+ */
500
+ type Transport = "mqtt" | "pppp" | "https";
501
+ /** The structured body emitted as the JSON error and on stderr. */
502
+ interface AnkerErrorBody {
503
+ code: string;
504
+ message: string;
505
+ transport?: Transport;
506
+ retriable: boolean;
507
+ hint?: string;
508
+ input?: Record<string, unknown>;
509
+ }
510
+ interface AnkerErrorOptions {
511
+ code: string;
512
+ message: string;
513
+ transport?: Transport;
514
+ retriable?: boolean;
515
+ hint?: string;
516
+ input?: Record<string, unknown>;
517
+ cause?: unknown;
518
+ }
519
+ /**
520
+ * Base class for all SDK errors. `exitCode` is the documented CLI contract:
521
+ *
522
+ * ```
523
+ * 1 generic / unexpected 4 printer not found / not selected
524
+ * 2 usage error 5 connectivity / timeout (RETRIABLE)
525
+ * 3 auth error 6 transport unavailable for this op
526
+ * 7 printer-side error (gcode rejected, job refused)
527
+ * ```
528
+ */
529
+ declare class AnkerError extends Error {
530
+ readonly exitCode: number;
531
+ readonly code: string;
532
+ readonly transport?: Transport;
533
+ readonly retriable: boolean;
534
+ readonly hint?: string;
535
+ input?: Record<string, unknown>;
536
+ constructor(opts: AnkerErrorOptions);
537
+ /** Attach/merge the failing input (the CLI echoes this back). */
538
+ withInput(input: Record<string, unknown>): this;
539
+ /** The structured body for JSON output and stderr. */
540
+ body(): AnkerErrorBody;
541
+ toJSON(): {
542
+ error: AnkerErrorBody;
543
+ };
544
+ }
545
+ /** Exit 2 — bad/missing arguments. Thrown mostly at the CLI boundary. */
546
+ declare class UsageError extends AnkerError {
547
+ readonly exitCode = 2;
548
+ constructor(opts: Omit<AnkerErrorOptions, "code"> & {
549
+ code?: string;
550
+ });
551
+ }
552
+ /** Exit 3 — login required/expired/captcha. */
553
+ declare class AuthError extends AnkerError {
554
+ readonly exitCode = 3;
555
+ constructor(opts: Omit<AnkerErrorOptions, "code"> & {
556
+ code?: string;
557
+ });
558
+ }
559
+ /** Exit 4 — printer not found on the account / none selected. */
560
+ declare class PrinterNotFoundError extends AnkerError {
561
+ readonly exitCode = 4;
562
+ constructor(opts: Omit<AnkerErrorOptions, "code"> & {
563
+ code?: string;
564
+ });
565
+ }
566
+ /** Exit 5 — connectivity/timeout. Always retriable (transient). */
567
+ declare class TimeoutError extends AnkerError {
568
+ readonly exitCode = 5;
569
+ constructor(opts: Omit<AnkerErrorOptions, "code" | "retriable"> & {
570
+ code?: string;
571
+ });
572
+ }
573
+ /** Exit 6 — the chosen op needs a transport the printer isn't reachable on. */
574
+ declare class TransportUnavailableError extends AnkerError {
575
+ readonly exitCode = 6;
576
+ constructor(opts: Omit<AnkerErrorOptions, "code"> & {
577
+ code?: string;
578
+ });
579
+ }
580
+ /** Exit 7 — the printer rejected the request (gcode/job refused). */
581
+ declare class PrinterRejectedError extends AnkerError {
582
+ readonly exitCode = 7;
583
+ constructor(opts: Omit<AnkerErrorOptions, "code"> & {
584
+ code?: string;
585
+ });
586
+ }
587
+ /** Map any thrown value to an {@link AnkerError} (wrapping unknowns as exit 1). */
588
+ declare function toAnkerError(err: unknown): AnkerError;
589
+
590
+ /**
591
+ * State-mutating gcode detection (brief §4, lesson #5).
592
+ *
593
+ * Many gcode commands change persistent or volatile machine state. The reference
594
+ * silently let us set Linear Advance K in RAM, contaminating the next print. The
595
+ * SDK flags these so the CLI can warn — noting that the change is volatile, that
596
+ * `M500` persists it to EEPROM, and that `M501` reloads EEPROM to revert.
597
+ */
598
+ interface GcodeInspection {
599
+ /** The leading G/M code, uppercased (e.g. `M900`), or null if unparseable. */
600
+ code: string | null;
601
+ /** True if the command writes machine state (volatile or persistent). */
602
+ mutatesState: boolean;
603
+ /** True if the effect lives in volatile RAM until power-cycle or `M501`. */
604
+ volatile: boolean;
605
+ /** True if the command persists settings to EEPROM (`M500`). */
606
+ persists: boolean;
607
+ /** Human-readable note about side effects (empty when none). */
608
+ note: string;
609
+ }
610
+ /** Extract the leading gcode word (e.g. `M900` from `M900 K0.5 ; comment`). */
611
+ declare function gcodeCode(command: string): string | null;
612
+ /** Inspect a gcode command for state-mutation side effects. */
613
+ declare function inspectGcode(command: string): GcodeInspection;
614
+
615
+ /**
616
+ * Gcode metadata transcoder (brief §4A, optional `print --fix-metadata`).
617
+ *
618
+ * Third-party slicers (OrcaSlicer/PrusaSlicer) print correctly on the M5, but
619
+ * the firmware's headline ETA/filament display reads from Anker-proprietary
620
+ * header comments those slicers don't emit. AnkerMake Studio is Cura-based and
621
+ * writes a `;TIME:<seconds>s` header; OrcaSlicer writes a human-readable
622
+ * `; estimated printing time (normal mode) = 1h 39m 52s` instead.
623
+ *
624
+ * This is a TRANSCODER, not a calculator: the slicer already embedded its own
625
+ * estimate; we only rewrite it into the keys/units Anker firmware reads. Motion,
626
+ * temps, and `M73` progress are left untouched (they already work cross-slicer).
627
+ * We never mutate the user's file — callers transcode a copy in the upload path.
628
+ */
629
+ interface TranscodeResult {
630
+ /** The (possibly) rewritten gcode. */
631
+ content: string;
632
+ /** True if any Anker header line was injected. */
633
+ changed: boolean;
634
+ /** What was transcoded (for logging/reporting). */
635
+ injected: {
636
+ timeSeconds?: number;
637
+ filamentMm?: number;
638
+ filamentGrams?: number;
639
+ };
640
+ }
641
+ /** Parse a duration like `1d 2h 39m 52s` / `1h 39m 52s` / `99m` into seconds. */
642
+ declare function parseDurationToSeconds(text: string): number | undefined;
643
+ /** Heuristic slicer detection from header comments. */
644
+ declare function detectSlicer(gcode: string): "ankermake" | "orca" | "prusa" | "unknown";
645
+ /** True when the firmware would already read a correct headline ETA. */
646
+ declare function hasAnkerTimeHeader(gcode: string): boolean;
647
+ /**
648
+ * Transcode a third-party slicer's embedded estimates into Anker/Cura header
649
+ * comments. No-op (returns the input unchanged) when the file already carries a
650
+ * `;TIME:` header — i.e. it was sliced natively.
651
+ */
652
+ declare function transcodeMetadata(gcode: string): TranscodeResult;
653
+
654
+ /**
655
+ * MQTT `commandType` enum and notice identifiers, extracted from the reference
656
+ * `libflagship/mqtt.py` and the live captures documented in the brief (§5).
657
+ *
658
+ * The firmware speaks numeric command types; we name the ones that matter and
659
+ * leave the rest reachable through the raw `send`/numeric value.
660
+ */
661
+ /** MQTT command/notice types (`commandType` field). Values are decimal. */
662
+ declare enum MqttCommandType {
663
+ EVENT_NOTIFY = 1000,// 1000
664
+ PRINT_SCHEDULE = 1001,// 1001
665
+ FIRMWARE_VERSION = 1002,// 1002
666
+ NOZZLE_TEMP = 1003,// 1003 — 1/100 °C
667
+ HOTBED_TEMP = 1004,// 1004 — 1/100 °C
668
+ FAN_SPEED = 1005,// 1005
669
+ PRINT_SPEED = 1006,// 1006
670
+ AUTO_LEVELING = 1007,// 1007
671
+ PRINT_CONTROL = 1008,// 1008
672
+ FILE_LIST_REQUEST = 1009,// 1009
673
+ APP_QUERY_STATUS = 1027,// 1027
674
+ ONLINE_NOTIFY = 1028,// 1028
675
+ RECOVER_FACTORY = 1029,// 1029
676
+ BREAK_POINT = 1039,// 1039
677
+ MODEL_LAYER = 1052,// 1052
678
+ GCODE_COMMAND = 1043
679
+ }
680
+ /** Notice `commandType` values seen streaming on `.../notice` (decimal §5). */
681
+ declare enum NoticeType {
682
+ EVENT_NOTIFY = 1000,
683
+ PRINT_SCHEDULE = 1001,
684
+ NOZZLE_TEMP = 1003,
685
+ HOTBED_TEMP = 1004,
686
+ PRINT_SPEED = 1006,
687
+ MODEL_LAYER = 1052
688
+ }
689
+ /**
690
+ * `PRINT_CONTROL` (0x03f0) `value` for job control. Reverse-engineered live
691
+ * against an M5 (2026-06-07): each value was sent and confirmed by watching the
692
+ * printer's authoritative job state and heater targets.
693
+ */
694
+ declare enum PrintControl {
695
+ PAUSE = 2,// → state 2 (paused), progress freezes
696
+ RESUME = 3,// → state 1 (printing)
697
+ STOP = 4
698
+ }
699
+
700
+ interface MqttClientOptions {
701
+ sn: string;
702
+ /** Per-printer AES key (decoded from the hex `mqtt_key`). */
703
+ key: Buffer;
704
+ host: string;
705
+ port?: number;
706
+ username: string;
707
+ password: string;
708
+ guid?: string;
709
+ /** Override the pinned CA (advanced). */
710
+ ca?: string;
711
+ /** Disable TLS verification (testing only). */
712
+ insecure?: boolean;
713
+ /** Diagnostic sink (defaults to no-op; the CLI routes this to stderr). */
714
+ log?: (msg: string) => void;
715
+ }
716
+ interface GcodeWaitOptions {
717
+ /** Hard ceiling before giving up (latency-class aware; default 10s). */
718
+ timeoutMs?: number;
719
+ /** Quiet period with no new frame that signals completion (default 600ms). */
720
+ quietMs?: number;
721
+ /** When false, fire-and-forget: publish and return without collecting. */
722
+ wait?: boolean;
723
+ }
724
+ type NoticeHandler = (notice: RawNotice) => void;
725
+ declare class AnkerMqttClient {
726
+ private readonly opts;
727
+ private client?;
728
+ private readonly guid;
729
+ private readonly log;
730
+ private readonly noticeHandlers;
731
+ private readonly replyHandlers;
732
+ private readonly latestNotices;
733
+ private gcodeLock;
734
+ constructor(opts: MqttClientOptions);
735
+ get connected(): boolean;
736
+ connect(timeoutMs?: number): Promise<void>;
737
+ disconnect(): Promise<void>;
738
+ private onMessage;
739
+ private publish;
740
+ /** Subscribe to streaming notices. Returns an unsubscribe function. */
741
+ onNotice(handler: NoticeHandler): () => void;
742
+ /** Publish a raw command payload (escape hatch for un-modeled commands). */
743
+ command(payload: Record<string, unknown>): void;
744
+ /** Publish a raw query payload. */
745
+ query(payload: Record<string, unknown>): void;
746
+ /**
747
+ * Send a single gcode command and return the parsed response (§6). The result
748
+ * carries `truncated` when the firmware's snapshot was partial. Serialized:
749
+ * only one gcode is in flight at a time.
750
+ */
751
+ gcode(command: string, opts?: GcodeWaitOptions): Promise<GcodeResult>;
752
+ private gcodeOnce;
753
+ /**
754
+ * Snapshot current printer status by normalizing the latest notice of each
755
+ * type. Optionally nudges the printer with an APP_QUERY_STATUS query and waits
756
+ * briefly for fresh telemetry.
757
+ */
758
+ getStatus(opts?: {
759
+ refresh?: boolean;
760
+ waitMs?: number;
761
+ }): Promise<PrinterStatus>;
762
+ /** Throw a structured timeout error (used by waiters). */
763
+ static timeout(message: string, hint?: string): never;
764
+ }
765
+
766
+ export { API_HOSTS, type AnkerAccount, AnkerClient, type AnkerClientOptions, type AnkerConfig, AnkerError, type AnkerErrorBody, AnkerHttpApi, AnkerMqttClient, AnkerPpppClient, type AnkerPrinter, AuthError, ConfigStore, type GcodeInspection, type GcodeOptions, type GcodeResult, type JobResult, type JobState, type LanDiscoverOptions, type LanPrinter, type Logger, type LoginOptions, type LoginResult, MQTT_HOSTS, type MachineSettings, type MqttClientOptions, MqttCommandType, NoticeType, PPPP_LAN_PORT, PpppState, PrintControl, type PrinterEvent, PrinterNotFoundError, PrinterRejectedError, type PrinterStatus, REDACTED, type RawNotice, type Region, TimeoutError, type TranscodeResult, type Transport, TransportUnavailableError, type Unsubscribe, type UploadProgress, UsageError, type WaitCondition, type WaitOptions, conditionHolds, configDir, defaultTimeoutFor, describeWaitCondition, detectSlicer, discoverLan, findPrinter, gcodeCode, gcodeHasTerminalOk, guessRegion, hasAnkerTimeHeader, inspectGcode, isEtaReliable, loginAndBuildConfig, mqttHostFor, mqttPassword, mqttUsername, normalizeStatus, parseDurationToSeconds, parseGcodeResult, parseWaitCondition, reassembleRaw, redactConfig, splitLines, stripAnsi, toAnkerError, transcodeMetadata };