@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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/index.d.ts +766 -0
- package/dist/index.js +2514 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2514 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { basename } from "path";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { homedir, platform } from "os";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
var MQTT_HOSTS = {
|
|
10
|
+
us: "make-mqtt.ankermake.com",
|
|
11
|
+
eu: "make-mqtt-eu.ankermake.com"
|
|
12
|
+
};
|
|
13
|
+
var API_HOSTS = {
|
|
14
|
+
us: "make-app.ankermake.com",
|
|
15
|
+
eu: "make-app-eu.ankermake.com"
|
|
16
|
+
};
|
|
17
|
+
var REDACTED = "<redacted>";
|
|
18
|
+
var mqttUsername = (acct) => `eufy_${acct.user_id}`;
|
|
19
|
+
var mqttPassword = (acct) => acct.email;
|
|
20
|
+
function mqttHostFor(acct, printer) {
|
|
21
|
+
return printer.mqtt_host ?? MQTT_HOSTS[acct.region];
|
|
22
|
+
}
|
|
23
|
+
function configDir() {
|
|
24
|
+
const override = process.env.ANKER_CONFIG_DIR;
|
|
25
|
+
if (override) return override;
|
|
26
|
+
const home = homedir();
|
|
27
|
+
switch (platform()) {
|
|
28
|
+
case "darwin":
|
|
29
|
+
return join(home, "Library", "Application Support", "ankerts");
|
|
30
|
+
case "win32":
|
|
31
|
+
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "ankerts");
|
|
32
|
+
default:
|
|
33
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "ankerts");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
var emptyConfig = () => ({ account: null, printers: [] });
|
|
37
|
+
var ConfigStore = class {
|
|
38
|
+
constructor(path = join(configDir(), "config.json")) {
|
|
39
|
+
this.path = path;
|
|
40
|
+
}
|
|
41
|
+
path;
|
|
42
|
+
exists() {
|
|
43
|
+
return existsSync(this.path);
|
|
44
|
+
}
|
|
45
|
+
load() {
|
|
46
|
+
if (!existsSync(this.path)) return emptyConfig();
|
|
47
|
+
const parsed = JSON.parse(readFileSync(this.path, "utf8"));
|
|
48
|
+
return {
|
|
49
|
+
account: parsed.account ?? null,
|
|
50
|
+
printers: parsed.printers ?? [],
|
|
51
|
+
...parsed.selected ? { selected: parsed.selected } : {}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
save(config) {
|
|
55
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
56
|
+
writeFileSync(this.path, `${JSON.stringify(config, null, 2)}
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
/** Mutate-and-persist helper. */
|
|
60
|
+
update(fn) {
|
|
61
|
+
const config = this.load();
|
|
62
|
+
fn(config);
|
|
63
|
+
this.save(config);
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function redactConfig(config, reveal = false) {
|
|
68
|
+
if (reveal) return config;
|
|
69
|
+
return {
|
|
70
|
+
account: config.account ? { ...config.account, auth_token: config.account.auth_token ? REDACTED : "" } : null,
|
|
71
|
+
printers: config.printers.map((p) => ({
|
|
72
|
+
...p,
|
|
73
|
+
mqtt_key: p.mqtt_key ? REDACTED : "",
|
|
74
|
+
p2p_key: p.p2p_key ? REDACTED : ""
|
|
75
|
+
})),
|
|
76
|
+
...config.selected ? { selected: config.selected } : {}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function findPrinter(config, ref) {
|
|
80
|
+
const { printers } = config;
|
|
81
|
+
if (typeof ref === "number" || /^\d+$/.test(String(ref))) {
|
|
82
|
+
const idx = Number(ref);
|
|
83
|
+
return printers[idx] ?? null;
|
|
84
|
+
}
|
|
85
|
+
const r = String(ref);
|
|
86
|
+
return printers.find((p) => p.duid === r || p.sn === r || p.id === r || p.name === r) ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/crypto.ts
|
|
90
|
+
import { createCipheriv, createDecipheriv, createECDH, createHash, randomBytes } from "crypto";
|
|
91
|
+
var unhex = (s) => Buffer.from(s, "hex");
|
|
92
|
+
var b64e = (b) => b.toString("base64");
|
|
93
|
+
var MQTT_AES_IV = Buffer.from("3DPrintAnkerMake", "ascii");
|
|
94
|
+
function pkcs7Pad(data, blockSize = 16) {
|
|
95
|
+
const padLen = blockSize - data.length % blockSize;
|
|
96
|
+
return Buffer.concat([data, Buffer.alloc(padLen, padLen)]);
|
|
97
|
+
}
|
|
98
|
+
function pkcs7Unpad(data) {
|
|
99
|
+
if (data.length === 0) return data;
|
|
100
|
+
const padLen = data[data.length - 1];
|
|
101
|
+
if (padLen < 1 || padLen > 16 || padLen > data.length) {
|
|
102
|
+
throw new Error("invalid PKCS#7 padding");
|
|
103
|
+
}
|
|
104
|
+
return data.subarray(0, data.length - padLen);
|
|
105
|
+
}
|
|
106
|
+
function aesCbcAlgo(key) {
|
|
107
|
+
switch (key.length) {
|
|
108
|
+
case 16:
|
|
109
|
+
return "aes-128-cbc";
|
|
110
|
+
case 24:
|
|
111
|
+
return "aes-192-cbc";
|
|
112
|
+
case 32:
|
|
113
|
+
return "aes-256-cbc";
|
|
114
|
+
default:
|
|
115
|
+
throw new Error(`unsupported AES key length: ${key.length} bytes`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function aesCbcEncrypt(msg, key, iv) {
|
|
119
|
+
const cipher = createCipheriv(aesCbcAlgo(key), key, iv);
|
|
120
|
+
cipher.setAutoPadding(false);
|
|
121
|
+
return Buffer.concat([cipher.update(pkcs7Pad(msg)), cipher.final()]);
|
|
122
|
+
}
|
|
123
|
+
function aesCbcDecrypt(cmsg, key, iv) {
|
|
124
|
+
const decipher = createDecipheriv(aesCbcAlgo(key), key, iv);
|
|
125
|
+
decipher.setAutoPadding(false);
|
|
126
|
+
return pkcs7Unpad(Buffer.concat([decipher.update(cmsg), decipher.final()]));
|
|
127
|
+
}
|
|
128
|
+
var mqttAesEncrypt = (msg, key, iv = MQTT_AES_IV) => aesCbcEncrypt(msg, key, iv);
|
|
129
|
+
var mqttAesDecrypt = (cmsg, key, iv = MQTT_AES_IV) => aesCbcDecrypt(cmsg, key, iv);
|
|
130
|
+
function xorBytes(data) {
|
|
131
|
+
let s = 0;
|
|
132
|
+
for (const x of data) s ^= x;
|
|
133
|
+
return s;
|
|
134
|
+
}
|
|
135
|
+
function mqttChecksumAdd(msg) {
|
|
136
|
+
return Buffer.concat([msg, Buffer.from([xorBytes(msg)])]);
|
|
137
|
+
}
|
|
138
|
+
function mqttChecksumRemove(payload) {
|
|
139
|
+
if (xorBytes(payload) !== 0) {
|
|
140
|
+
throw new Error("malformed MQTT message: checksum mismatch");
|
|
141
|
+
}
|
|
142
|
+
return payload.subarray(0, payload.length - 1);
|
|
143
|
+
}
|
|
144
|
+
var ANKER_EC_PUBKEY = Buffer.from(
|
|
145
|
+
"04c5c00c4f8d1197cc7c3167c52bf7acb054d722f0ef08dcd7e0883236e0d72a3868d9750cb47fa4619248f3d83f0f662671dadc6e2d31c2f41db0161651c7c076",
|
|
146
|
+
"hex"
|
|
147
|
+
);
|
|
148
|
+
function ecdhEncryptLoginPassword(password) {
|
|
149
|
+
const ecdh = createECDH("prime256v1");
|
|
150
|
+
ecdh.generateKeys();
|
|
151
|
+
const key = ecdh.computeSecret(ANKER_EC_PUBKEY);
|
|
152
|
+
const iv = key.subarray(0, 16);
|
|
153
|
+
const ciphertext = aesCbcEncrypt(Buffer.from(password, "utf8"), key, iv);
|
|
154
|
+
return {
|
|
155
|
+
publicKey: ecdh.getPublicKey("hex", "uncompressed"),
|
|
156
|
+
encryptedPassword: b64e(ciphertext)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var LOGIN_CACHE_KEY = unhex("1b55f97793d58864571e1055838cac97");
|
|
160
|
+
var md5Hex = (data) => createHash("md5").update(data).digest("hex");
|
|
161
|
+
function ppcsCrc16(data) {
|
|
162
|
+
let crc = 0;
|
|
163
|
+
for (const byte of data) {
|
|
164
|
+
crc ^= byte << 8;
|
|
165
|
+
for (let i = 0; i < 8; i++) {
|
|
166
|
+
crc = crc & 32768 ? (crc << 1 ^ 4129) & 65535 : crc << 1 & 65535;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const out = Buffer.alloc(2);
|
|
170
|
+
out.writeUInt16LE(crc, 0);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
var PPPP_SEED = "EUPRAKM";
|
|
174
|
+
var PPPP_SHUFFLE = [
|
|
175
|
+
[149, 229, 97, 151, 131, 13, 167, 241],
|
|
176
|
+
[211, 5, 149, 139, 223, 19, 109, 239],
|
|
177
|
+
[7, 97, 13, 109, 127, 103, 23, 43],
|
|
178
|
+
[193, 181, 19, 11, 223, 139, 73, 59],
|
|
179
|
+
[127, 7, 211, 2, 109, 47, 19, 197],
|
|
180
|
+
[109, 61, 251, 13, 11, 41, 233, 79],
|
|
181
|
+
[137, 47, 227, 233, 13, 131, 109, 229],
|
|
182
|
+
[7, 83, 139, 37, 149, 71, 31, 41]
|
|
183
|
+
];
|
|
184
|
+
function curseStep(a, b, c, d, q, shuffle) {
|
|
185
|
+
return [
|
|
186
|
+
shuffle[b + q % a & 7][q + c % d & 7],
|
|
187
|
+
shuffle[c + q % b & 7][q + d % a & 7],
|
|
188
|
+
shuffle[d + q % c & 7][q + a % b & 7],
|
|
189
|
+
shuffle[a + q % d & 7][q + b % c & 7]
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
function curseInit(key, shuffle) {
|
|
193
|
+
let [a, b, c, d] = [1, 3, 5, 7];
|
|
194
|
+
for (const ch of key) {
|
|
195
|
+
[a, b, c, d] = curseStep(a, b, c, d, ch.charCodeAt(0), shuffle);
|
|
196
|
+
}
|
|
197
|
+
return [a, b, c, d];
|
|
198
|
+
}
|
|
199
|
+
function cryptoCurse(input, key, shuffle) {
|
|
200
|
+
let [a, b, c, d] = curseInit(key, shuffle);
|
|
201
|
+
const output = new Array(input.length + 4).fill(0);
|
|
202
|
+
for (let p = 0; p < input.length; p++) {
|
|
203
|
+
const x = output[p] = input[p] ^ (a ^ b ^ c ^ d);
|
|
204
|
+
[a, b, c, d] = curseStep(a, b, c, d, x, shuffle);
|
|
205
|
+
}
|
|
206
|
+
for (let p = input.length; p < input.length + 4; p++) {
|
|
207
|
+
const x = output[p] = a ^ b ^ c ^ d ^ 67;
|
|
208
|
+
[a, b, c, d] = curseStep(a, b, c, d, x, shuffle);
|
|
209
|
+
}
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
function cryptoCurseString(input) {
|
|
213
|
+
return Buffer.from(cryptoCurse(input, PPPP_SEED, PPPP_SHUFFLE));
|
|
214
|
+
}
|
|
215
|
+
var PPPP_SIMPLE_SEED = Buffer.from("SSD@cs2-network.", "ascii");
|
|
216
|
+
var PPPP_SIMPLE_SHUFFLE = [
|
|
217
|
+
124,
|
|
218
|
+
156,
|
|
219
|
+
232,
|
|
220
|
+
74,
|
|
221
|
+
19,
|
|
222
|
+
222,
|
|
223
|
+
220,
|
|
224
|
+
178,
|
|
225
|
+
47,
|
|
226
|
+
33,
|
|
227
|
+
35,
|
|
228
|
+
228,
|
|
229
|
+
48,
|
|
230
|
+
123,
|
|
231
|
+
61,
|
|
232
|
+
140,
|
|
233
|
+
188,
|
|
234
|
+
11,
|
|
235
|
+
39,
|
|
236
|
+
12,
|
|
237
|
+
60,
|
|
238
|
+
247,
|
|
239
|
+
154,
|
|
240
|
+
231,
|
|
241
|
+
8,
|
|
242
|
+
113,
|
|
243
|
+
150,
|
|
244
|
+
0,
|
|
245
|
+
151,
|
|
246
|
+
133,
|
|
247
|
+
239,
|
|
248
|
+
193,
|
|
249
|
+
31,
|
|
250
|
+
196,
|
|
251
|
+
219,
|
|
252
|
+
161,
|
|
253
|
+
194,
|
|
254
|
+
235,
|
|
255
|
+
217,
|
|
256
|
+
1,
|
|
257
|
+
250,
|
|
258
|
+
186,
|
|
259
|
+
59,
|
|
260
|
+
5,
|
|
261
|
+
184,
|
|
262
|
+
21,
|
|
263
|
+
135,
|
|
264
|
+
131,
|
|
265
|
+
40,
|
|
266
|
+
114,
|
|
267
|
+
209,
|
|
268
|
+
139,
|
|
269
|
+
90,
|
|
270
|
+
214,
|
|
271
|
+
218,
|
|
272
|
+
147,
|
|
273
|
+
88,
|
|
274
|
+
254,
|
|
275
|
+
170,
|
|
276
|
+
204,
|
|
277
|
+
110,
|
|
278
|
+
27,
|
|
279
|
+
240,
|
|
280
|
+
163,
|
|
281
|
+
136,
|
|
282
|
+
171,
|
|
283
|
+
67,
|
|
284
|
+
192,
|
|
285
|
+
13,
|
|
286
|
+
181,
|
|
287
|
+
69,
|
|
288
|
+
56,
|
|
289
|
+
79,
|
|
290
|
+
80,
|
|
291
|
+
34,
|
|
292
|
+
102,
|
|
293
|
+
32,
|
|
294
|
+
127,
|
|
295
|
+
7,
|
|
296
|
+
91,
|
|
297
|
+
20,
|
|
298
|
+
152,
|
|
299
|
+
29,
|
|
300
|
+
155,
|
|
301
|
+
167,
|
|
302
|
+
42,
|
|
303
|
+
185,
|
|
304
|
+
168,
|
|
305
|
+
203,
|
|
306
|
+
241,
|
|
307
|
+
252,
|
|
308
|
+
73,
|
|
309
|
+
71,
|
|
310
|
+
6,
|
|
311
|
+
62,
|
|
312
|
+
177,
|
|
313
|
+
14,
|
|
314
|
+
4,
|
|
315
|
+
58,
|
|
316
|
+
148,
|
|
317
|
+
94,
|
|
318
|
+
238,
|
|
319
|
+
84,
|
|
320
|
+
17,
|
|
321
|
+
52,
|
|
322
|
+
221,
|
|
323
|
+
77,
|
|
324
|
+
249,
|
|
325
|
+
236,
|
|
326
|
+
199,
|
|
327
|
+
201,
|
|
328
|
+
227,
|
|
329
|
+
120,
|
|
330
|
+
26,
|
|
331
|
+
111,
|
|
332
|
+
112,
|
|
333
|
+
107,
|
|
334
|
+
164,
|
|
335
|
+
189,
|
|
336
|
+
169,
|
|
337
|
+
93,
|
|
338
|
+
213,
|
|
339
|
+
248,
|
|
340
|
+
229,
|
|
341
|
+
187,
|
|
342
|
+
38,
|
|
343
|
+
175,
|
|
344
|
+
66,
|
|
345
|
+
55,
|
|
346
|
+
216,
|
|
347
|
+
225,
|
|
348
|
+
2,
|
|
349
|
+
10,
|
|
350
|
+
174,
|
|
351
|
+
95,
|
|
352
|
+
28,
|
|
353
|
+
197,
|
|
354
|
+
115,
|
|
355
|
+
9,
|
|
356
|
+
78,
|
|
357
|
+
105,
|
|
358
|
+
36,
|
|
359
|
+
144,
|
|
360
|
+
109,
|
|
361
|
+
18,
|
|
362
|
+
179,
|
|
363
|
+
25,
|
|
364
|
+
173,
|
|
365
|
+
116,
|
|
366
|
+
138,
|
|
367
|
+
41,
|
|
368
|
+
64,
|
|
369
|
+
245,
|
|
370
|
+
45,
|
|
371
|
+
190,
|
|
372
|
+
165,
|
|
373
|
+
89,
|
|
374
|
+
224,
|
|
375
|
+
244,
|
|
376
|
+
121,
|
|
377
|
+
210,
|
|
378
|
+
75,
|
|
379
|
+
206,
|
|
380
|
+
137,
|
|
381
|
+
130,
|
|
382
|
+
72,
|
|
383
|
+
132,
|
|
384
|
+
37,
|
|
385
|
+
198,
|
|
386
|
+
145,
|
|
387
|
+
43,
|
|
388
|
+
162,
|
|
389
|
+
251,
|
|
390
|
+
143,
|
|
391
|
+
233,
|
|
392
|
+
166,
|
|
393
|
+
176,
|
|
394
|
+
158,
|
|
395
|
+
63,
|
|
396
|
+
101,
|
|
397
|
+
246,
|
|
398
|
+
3,
|
|
399
|
+
49,
|
|
400
|
+
46,
|
|
401
|
+
172,
|
|
402
|
+
15,
|
|
403
|
+
149,
|
|
404
|
+
44,
|
|
405
|
+
92,
|
|
406
|
+
237,
|
|
407
|
+
57,
|
|
408
|
+
183,
|
|
409
|
+
51,
|
|
410
|
+
108,
|
|
411
|
+
86,
|
|
412
|
+
126,
|
|
413
|
+
180,
|
|
414
|
+
160,
|
|
415
|
+
253,
|
|
416
|
+
122,
|
|
417
|
+
129,
|
|
418
|
+
83,
|
|
419
|
+
81,
|
|
420
|
+
134,
|
|
421
|
+
141,
|
|
422
|
+
159,
|
|
423
|
+
119,
|
|
424
|
+
255,
|
|
425
|
+
106,
|
|
426
|
+
128,
|
|
427
|
+
223,
|
|
428
|
+
226,
|
|
429
|
+
191,
|
|
430
|
+
16,
|
|
431
|
+
215,
|
|
432
|
+
117,
|
|
433
|
+
100,
|
|
434
|
+
87,
|
|
435
|
+
118,
|
|
436
|
+
243,
|
|
437
|
+
85,
|
|
438
|
+
205,
|
|
439
|
+
208,
|
|
440
|
+
200,
|
|
441
|
+
24,
|
|
442
|
+
230,
|
|
443
|
+
54,
|
|
444
|
+
65,
|
|
445
|
+
98,
|
|
446
|
+
207,
|
|
447
|
+
153,
|
|
448
|
+
242,
|
|
449
|
+
50,
|
|
450
|
+
76,
|
|
451
|
+
103,
|
|
452
|
+
96,
|
|
453
|
+
97,
|
|
454
|
+
146,
|
|
455
|
+
202,
|
|
456
|
+
211,
|
|
457
|
+
234,
|
|
458
|
+
99,
|
|
459
|
+
125,
|
|
460
|
+
22,
|
|
461
|
+
182,
|
|
462
|
+
142,
|
|
463
|
+
212,
|
|
464
|
+
104,
|
|
465
|
+
53,
|
|
466
|
+
195,
|
|
467
|
+
82,
|
|
468
|
+
157,
|
|
469
|
+
70,
|
|
470
|
+
68,
|
|
471
|
+
30,
|
|
472
|
+
23
|
|
473
|
+
];
|
|
474
|
+
function simpleHash(seed) {
|
|
475
|
+
const hash = [0, 0, 0, 0];
|
|
476
|
+
for (const byte of seed) {
|
|
477
|
+
hash[0] = (hash[0] ^ byte) & 255;
|
|
478
|
+
hash[1] = hash[1] + Math.floor(byte / 3) & 255;
|
|
479
|
+
hash[2] = hash[2] - byte & 255;
|
|
480
|
+
hash[3] = hash[3] + byte & 255;
|
|
481
|
+
}
|
|
482
|
+
return hash.reverse();
|
|
483
|
+
}
|
|
484
|
+
function simpleLookup(hash, b) {
|
|
485
|
+
const index = hash[b & 3] + b & 4294967295;
|
|
486
|
+
return PPPP_SIMPLE_SHUFFLE[(index % PPPP_SIMPLE_SHUFFLE.length + PPPP_SIMPLE_SHUFFLE.length) % PPPP_SIMPLE_SHUFFLE.length];
|
|
487
|
+
}
|
|
488
|
+
function simpleDecrypt(input, seed = PPPP_SIMPLE_SEED) {
|
|
489
|
+
const hash = simpleHash(seed);
|
|
490
|
+
const output = Buffer.alloc(input.length);
|
|
491
|
+
if (input.length === 0) return output;
|
|
492
|
+
output[0] = input[0] ^ simpleLookup(hash, 0);
|
|
493
|
+
for (let i = 1; i < input.length; i++) {
|
|
494
|
+
output[i] = input[i] ^ simpleLookup(hash, input[i - 1]);
|
|
495
|
+
}
|
|
496
|
+
return output;
|
|
497
|
+
}
|
|
498
|
+
var PPPP_INITSTRING_SHUFFLE = [
|
|
499
|
+
73,
|
|
500
|
+
89,
|
|
501
|
+
67,
|
|
502
|
+
61,
|
|
503
|
+
181,
|
|
504
|
+
191,
|
|
505
|
+
109,
|
|
506
|
+
163,
|
|
507
|
+
71,
|
|
508
|
+
83,
|
|
509
|
+
79,
|
|
510
|
+
97,
|
|
511
|
+
101,
|
|
512
|
+
227,
|
|
513
|
+
113,
|
|
514
|
+
233,
|
|
515
|
+
103,
|
|
516
|
+
127,
|
|
517
|
+
2,
|
|
518
|
+
3,
|
|
519
|
+
11,
|
|
520
|
+
173,
|
|
521
|
+
179,
|
|
522
|
+
137,
|
|
523
|
+
43,
|
|
524
|
+
47,
|
|
525
|
+
53,
|
|
526
|
+
193,
|
|
527
|
+
107,
|
|
528
|
+
139,
|
|
529
|
+
149,
|
|
530
|
+
151,
|
|
531
|
+
17,
|
|
532
|
+
229,
|
|
533
|
+
167,
|
|
534
|
+
13,
|
|
535
|
+
239,
|
|
536
|
+
241,
|
|
537
|
+
5,
|
|
538
|
+
7,
|
|
539
|
+
131,
|
|
540
|
+
251,
|
|
541
|
+
157,
|
|
542
|
+
59,
|
|
543
|
+
197,
|
|
544
|
+
199,
|
|
545
|
+
19,
|
|
546
|
+
23,
|
|
547
|
+
29,
|
|
548
|
+
31,
|
|
549
|
+
37,
|
|
550
|
+
41,
|
|
551
|
+
211,
|
|
552
|
+
223
|
|
553
|
+
];
|
|
554
|
+
function ppppDecodeInitstringRaw(input) {
|
|
555
|
+
const olen = input.length >> 1;
|
|
556
|
+
const output = new Array(olen).fill(0);
|
|
557
|
+
for (let q = 0; q < olen; q++) {
|
|
558
|
+
let xor = 57 ^ PPPP_INITSTRING_SHUFFLE[q % 54];
|
|
559
|
+
for (let p = 0; p <= q; p++) xor ^= output[p];
|
|
560
|
+
const l = input[q * 2 + 1] - 65;
|
|
561
|
+
const h = input[q * 2 + 0] - 65;
|
|
562
|
+
output[q] = (xor ^ l + (h << 4)) & 255;
|
|
563
|
+
}
|
|
564
|
+
return Buffer.from(output);
|
|
565
|
+
}
|
|
566
|
+
function ppppDecodeInitstring(input) {
|
|
567
|
+
const res = ppppDecodeInitstringRaw(Buffer.from(input, "ascii"));
|
|
568
|
+
return res.toString("utf8").replace(/,+$/, "").split(",");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/errors.ts
|
|
572
|
+
var AnkerError = class extends Error {
|
|
573
|
+
exitCode = 1;
|
|
574
|
+
code;
|
|
575
|
+
transport;
|
|
576
|
+
retriable;
|
|
577
|
+
hint;
|
|
578
|
+
input;
|
|
579
|
+
constructor(opts) {
|
|
580
|
+
super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
581
|
+
this.name = new.target.name;
|
|
582
|
+
this.code = opts.code;
|
|
583
|
+
this.transport = opts.transport;
|
|
584
|
+
this.retriable = opts.retriable ?? false;
|
|
585
|
+
this.hint = opts.hint;
|
|
586
|
+
this.input = opts.input;
|
|
587
|
+
}
|
|
588
|
+
/** Attach/merge the failing input (the CLI echoes this back). */
|
|
589
|
+
withInput(input) {
|
|
590
|
+
this.input = { ...this.input, ...input };
|
|
591
|
+
return this;
|
|
592
|
+
}
|
|
593
|
+
/** The structured body for JSON output and stderr. */
|
|
594
|
+
body() {
|
|
595
|
+
return {
|
|
596
|
+
code: this.code,
|
|
597
|
+
message: this.message,
|
|
598
|
+
...this.transport ? { transport: this.transport } : {},
|
|
599
|
+
retriable: this.retriable,
|
|
600
|
+
...this.hint ? { hint: this.hint } : {},
|
|
601
|
+
...this.input ? { input: this.input } : {}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
toJSON() {
|
|
605
|
+
return { error: this.body() };
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
var UsageError = class extends AnkerError {
|
|
609
|
+
exitCode = 2;
|
|
610
|
+
constructor(opts) {
|
|
611
|
+
super({ code: "usage", retriable: false, ...opts });
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
var AuthError = class extends AnkerError {
|
|
615
|
+
exitCode = 3;
|
|
616
|
+
constructor(opts) {
|
|
617
|
+
super({ code: "auth_required", transport: "https", retriable: false, ...opts });
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
var PrinterNotFoundError = class extends AnkerError {
|
|
621
|
+
exitCode = 4;
|
|
622
|
+
constructor(opts) {
|
|
623
|
+
super({ code: "printer_not_found", retriable: false, ...opts });
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var TimeoutError = class extends AnkerError {
|
|
627
|
+
exitCode = 5;
|
|
628
|
+
constructor(opts) {
|
|
629
|
+
super({ code: "timeout", retriable: true, ...opts });
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
var TransportUnavailableError = class extends AnkerError {
|
|
633
|
+
exitCode = 6;
|
|
634
|
+
constructor(opts) {
|
|
635
|
+
super({ code: "transport_unavailable", retriable: false, ...opts });
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
var PrinterRejectedError = class extends AnkerError {
|
|
639
|
+
exitCode = 7;
|
|
640
|
+
constructor(opts) {
|
|
641
|
+
super({ code: "printer_rejected", retriable: false, ...opts });
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function toAnkerError(err) {
|
|
645
|
+
if (err instanceof AnkerError) return err;
|
|
646
|
+
if (err instanceof Error) {
|
|
647
|
+
return new AnkerError({ code: "internal_error", message: err.message, cause: err });
|
|
648
|
+
}
|
|
649
|
+
return new AnkerError({ code: "internal_error", message: String(err) });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/protocol/commands.ts
|
|
653
|
+
var MqttCommandType = /* @__PURE__ */ ((MqttCommandType2) => {
|
|
654
|
+
MqttCommandType2[MqttCommandType2["EVENT_NOTIFY"] = 1e3] = "EVENT_NOTIFY";
|
|
655
|
+
MqttCommandType2[MqttCommandType2["PRINT_SCHEDULE"] = 1001] = "PRINT_SCHEDULE";
|
|
656
|
+
MqttCommandType2[MqttCommandType2["FIRMWARE_VERSION"] = 1002] = "FIRMWARE_VERSION";
|
|
657
|
+
MqttCommandType2[MqttCommandType2["NOZZLE_TEMP"] = 1003] = "NOZZLE_TEMP";
|
|
658
|
+
MqttCommandType2[MqttCommandType2["HOTBED_TEMP"] = 1004] = "HOTBED_TEMP";
|
|
659
|
+
MqttCommandType2[MqttCommandType2["FAN_SPEED"] = 1005] = "FAN_SPEED";
|
|
660
|
+
MqttCommandType2[MqttCommandType2["PRINT_SPEED"] = 1006] = "PRINT_SPEED";
|
|
661
|
+
MqttCommandType2[MqttCommandType2["AUTO_LEVELING"] = 1007] = "AUTO_LEVELING";
|
|
662
|
+
MqttCommandType2[MqttCommandType2["PRINT_CONTROL"] = 1008] = "PRINT_CONTROL";
|
|
663
|
+
MqttCommandType2[MqttCommandType2["FILE_LIST_REQUEST"] = 1009] = "FILE_LIST_REQUEST";
|
|
664
|
+
MqttCommandType2[MqttCommandType2["APP_QUERY_STATUS"] = 1027] = "APP_QUERY_STATUS";
|
|
665
|
+
MqttCommandType2[MqttCommandType2["ONLINE_NOTIFY"] = 1028] = "ONLINE_NOTIFY";
|
|
666
|
+
MqttCommandType2[MqttCommandType2["RECOVER_FACTORY"] = 1029] = "RECOVER_FACTORY";
|
|
667
|
+
MqttCommandType2[MqttCommandType2["BREAK_POINT"] = 1039] = "BREAK_POINT";
|
|
668
|
+
MqttCommandType2[MqttCommandType2["MODEL_LAYER"] = 1052] = "MODEL_LAYER";
|
|
669
|
+
MqttCommandType2[MqttCommandType2["GCODE_COMMAND"] = 1043] = "GCODE_COMMAND";
|
|
670
|
+
return MqttCommandType2;
|
|
671
|
+
})(MqttCommandType || {});
|
|
672
|
+
var NoticeType = /* @__PURE__ */ ((NoticeType2) => {
|
|
673
|
+
NoticeType2[NoticeType2["EVENT_NOTIFY"] = 1e3] = "EVENT_NOTIFY";
|
|
674
|
+
NoticeType2[NoticeType2["PRINT_SCHEDULE"] = 1001] = "PRINT_SCHEDULE";
|
|
675
|
+
NoticeType2[NoticeType2["NOZZLE_TEMP"] = 1003] = "NOZZLE_TEMP";
|
|
676
|
+
NoticeType2[NoticeType2["HOTBED_TEMP"] = 1004] = "HOTBED_TEMP";
|
|
677
|
+
NoticeType2[NoticeType2["PRINT_SPEED"] = 1006] = "PRINT_SPEED";
|
|
678
|
+
NoticeType2[NoticeType2["MODEL_LAYER"] = 1052] = "MODEL_LAYER";
|
|
679
|
+
return NoticeType2;
|
|
680
|
+
})(NoticeType || {});
|
|
681
|
+
var PrintControl = /* @__PURE__ */ ((PrintControl2) => {
|
|
682
|
+
PrintControl2[PrintControl2["PAUSE"] = 2] = "PAUSE";
|
|
683
|
+
PrintControl2[PrintControl2["RESUME"] = 3] = "RESUME";
|
|
684
|
+
PrintControl2[PrintControl2["STOP"] = 4] = "STOP";
|
|
685
|
+
return PrintControl2;
|
|
686
|
+
})(PrintControl || {});
|
|
687
|
+
|
|
688
|
+
// src/protocol/transcoder.ts
|
|
689
|
+
function parseDurationToSeconds(text) {
|
|
690
|
+
let total = 0;
|
|
691
|
+
let any = false;
|
|
692
|
+
for (const [, value, unit] of text.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) {
|
|
693
|
+
const n = Number(value);
|
|
694
|
+
if (Number.isNaN(n)) continue;
|
|
695
|
+
any = true;
|
|
696
|
+
total += n * { d: 86400, h: 3600, m: 60, s: 1 }[unit.toLowerCase()];
|
|
697
|
+
}
|
|
698
|
+
return any ? Math.round(total) : void 0;
|
|
699
|
+
}
|
|
700
|
+
function detectSlicer(gcode) {
|
|
701
|
+
const head = gcode.slice(0, 4096).toLowerCase();
|
|
702
|
+
if (/;time:\s*\d/.test(head) || head.includes("ankermake") || head.includes("eufymake")) {
|
|
703
|
+
return "ankermake";
|
|
704
|
+
}
|
|
705
|
+
if (head.includes("orcaslicer") || head.includes("orca_slicer")) return "orca";
|
|
706
|
+
if (head.includes("prusaslicer")) return "prusa";
|
|
707
|
+
if (/estimated printing time/.test(head)) return "orca";
|
|
708
|
+
return "unknown";
|
|
709
|
+
}
|
|
710
|
+
function hasAnkerTimeHeader(gcode) {
|
|
711
|
+
return /^;TIME:\s*\d+/m.test(gcode);
|
|
712
|
+
}
|
|
713
|
+
function findNumber(gcode, re) {
|
|
714
|
+
const m = re.exec(gcode);
|
|
715
|
+
if (!m) return void 0;
|
|
716
|
+
const n = Number(m[1]);
|
|
717
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
718
|
+
}
|
|
719
|
+
function transcodeMetadata(gcode) {
|
|
720
|
+
if (hasAnkerTimeHeader(gcode)) {
|
|
721
|
+
return { content: gcode, changed: false, injected: {} };
|
|
722
|
+
}
|
|
723
|
+
const injected = {};
|
|
724
|
+
const headerLines = [];
|
|
725
|
+
const timeMatch = /;\s*estimated printing time \(normal mode\)\s*=\s*(.+)/i.exec(gcode);
|
|
726
|
+
if (timeMatch) {
|
|
727
|
+
const seconds = parseDurationToSeconds(timeMatch[1]);
|
|
728
|
+
if (seconds !== void 0) {
|
|
729
|
+
injected.timeSeconds = seconds;
|
|
730
|
+
headerLines.push(`;TIME:${seconds}s`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const mm = findNumber(gcode, /;\s*filament used \[mm\]\s*=\s*([\d.]+)/i);
|
|
734
|
+
if (mm !== void 0) {
|
|
735
|
+
injected.filamentMm = mm;
|
|
736
|
+
headerLines.push(`;Filament used: ${mm}mm`);
|
|
737
|
+
}
|
|
738
|
+
const grams = findNumber(gcode, /;\s*total filament used \[g\]\s*=\s*([\d.]+)/i);
|
|
739
|
+
if (grams !== void 0) {
|
|
740
|
+
injected.filamentGrams = grams;
|
|
741
|
+
headerLines.push(`;Filament weight: ${grams}g`);
|
|
742
|
+
}
|
|
743
|
+
if (headerLines.length === 0) {
|
|
744
|
+
return { content: gcode, changed: false, injected };
|
|
745
|
+
}
|
|
746
|
+
const content = `${headerLines.join("\n")}
|
|
747
|
+
${gcode}`;
|
|
748
|
+
return { content, changed: true, injected };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/transport/mqtt.ts
|
|
752
|
+
import mqtt from "mqtt";
|
|
753
|
+
|
|
754
|
+
// src/protocol/gcode.ts
|
|
755
|
+
var GCODE_WINDOW_BYTES = 512;
|
|
756
|
+
var RINGBUF_RE = /\+ringbuf:\s*\d+,\s*\d+,\s*\d+/;
|
|
757
|
+
var ESC = String.fromCharCode(27);
|
|
758
|
+
var CSI = String.fromCharCode(155);
|
|
759
|
+
var ANSI_RE = new RegExp(
|
|
760
|
+
`[${ESC}${CSI}][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PR-TZcf-ntqry=><~]`,
|
|
761
|
+
"g"
|
|
762
|
+
);
|
|
763
|
+
function stripAnsi(text) {
|
|
764
|
+
return text.replace(ANSI_RE, "");
|
|
765
|
+
}
|
|
766
|
+
function reassembleRaw(chunks) {
|
|
767
|
+
return stripAnsi(chunks.join("")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
768
|
+
}
|
|
769
|
+
function splitLines(raw) {
|
|
770
|
+
return raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
771
|
+
}
|
|
772
|
+
function gcodeHasTerminalOk(raw) {
|
|
773
|
+
const lines = splitLines(raw).filter((l) => !/^\+/.test(l));
|
|
774
|
+
const last = lines[lines.length - 1];
|
|
775
|
+
return last !== void 0 && (/^ok\b/i.test(last) || last.toLowerCase() === "ok");
|
|
776
|
+
}
|
|
777
|
+
function hasUnknownCommand(lines) {
|
|
778
|
+
return lines.some((l) => /echo:\s*Unknown command/i.test(l));
|
|
779
|
+
}
|
|
780
|
+
var REPORT_RE = /^([GM]\d+)\b\s*(.*)$/;
|
|
781
|
+
function parseKeyColonTokens(line, out) {
|
|
782
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*:/.test(line)) return false;
|
|
783
|
+
const parts = line.split(/\s+(?=[A-Z][A-Z0-9_]*:)/);
|
|
784
|
+
let matched = false;
|
|
785
|
+
for (const part of parts) {
|
|
786
|
+
const idx = part.indexOf(":");
|
|
787
|
+
if (idx <= 0) continue;
|
|
788
|
+
const key = part.slice(0, idx).trim();
|
|
789
|
+
const value = part.slice(idx + 1).trim();
|
|
790
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
791
|
+
out[key] = value;
|
|
792
|
+
matched = true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return matched;
|
|
796
|
+
}
|
|
797
|
+
function parseGcodeResult(command, chunks, meta) {
|
|
798
|
+
const raw = reassembleRaw(chunks);
|
|
799
|
+
const lines = splitLines(raw);
|
|
800
|
+
const fields = {};
|
|
801
|
+
const reports = {};
|
|
802
|
+
const truncated = RINGBUF_RE.test(raw) || chunks.join("").length >= GCODE_WINDOW_BYTES;
|
|
803
|
+
for (const line of lines) {
|
|
804
|
+
const body = line.replace(/^echo:\s*/i, "").trim();
|
|
805
|
+
const report = REPORT_RE.exec(body);
|
|
806
|
+
if (report && !body.includes("=")) {
|
|
807
|
+
reports[report[1]] = report[2].trim();
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (body.includes("=") && !/^[A-Za-z_][A-Za-z0-9_]*:/.test(body)) {
|
|
811
|
+
const eq = body.indexOf("=");
|
|
812
|
+
const key = body.slice(0, eq).trim();
|
|
813
|
+
const value = body.slice(eq + 1).trim();
|
|
814
|
+
if (key) {
|
|
815
|
+
fields[key] = value;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
parseKeyColonTokens(body, fields);
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
command,
|
|
823
|
+
raw,
|
|
824
|
+
lines,
|
|
825
|
+
ok: gcodeHasTerminalOk(raw),
|
|
826
|
+
recognized: !hasUnknownCommand(lines),
|
|
827
|
+
fields,
|
|
828
|
+
reports,
|
|
829
|
+
durationMs: meta.durationMs,
|
|
830
|
+
timedOut: meta.timedOut,
|
|
831
|
+
truncated,
|
|
832
|
+
frames: chunks.length
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/protocol/status.ts
|
|
837
|
+
var centiToC = (v) => Math.round(v / 100 * 100) / 100;
|
|
838
|
+
var num = (v) => typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : void 0;
|
|
839
|
+
var SANE_ETA_SECONDS = 60 * 60 * 24 * 30;
|
|
840
|
+
function isEtaReliable(schedule) {
|
|
841
|
+
const totalTime = num(schedule.totalTime);
|
|
842
|
+
const time = num(schedule.time);
|
|
843
|
+
const left = num(schedule.startLeftTime);
|
|
844
|
+
if (totalTime === void 0 || totalTime <= 0) return false;
|
|
845
|
+
if (time !== void 0 && time > totalTime) return false;
|
|
846
|
+
if (left !== void 0 && (left < 0 || left > SANE_ETA_SECONDS)) return false;
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
function deriveState(schedule, progressPct, eventState) {
|
|
850
|
+
if (eventState === 0) return progressPct >= 100 ? "complete" : "idle";
|
|
851
|
+
if (eventState === 1) return progressPct >= 100 ? "complete" : "printing";
|
|
852
|
+
if (eventState === 2) return "paused";
|
|
853
|
+
const hint = schedule?.state;
|
|
854
|
+
if (typeof hint === "string") {
|
|
855
|
+
const h = hint.toLowerCase();
|
|
856
|
+
if (["idle", "printing", "paused", "complete", "failed", "cancelled"].includes(h)) {
|
|
857
|
+
return h;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (!schedule || !schedule.name) return "idle";
|
|
861
|
+
if (progressPct >= 100) return "complete";
|
|
862
|
+
return "printing";
|
|
863
|
+
}
|
|
864
|
+
function normalizeStatus(notices, opts = {}) {
|
|
865
|
+
const latest = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const n of notices) {
|
|
867
|
+
if (typeof n.commandType === "number") latest.set(n.commandType, n);
|
|
868
|
+
}
|
|
869
|
+
const nozzleN = latest.get(1003 /* NOZZLE_TEMP */);
|
|
870
|
+
const bedN = latest.get(1004 /* HOTBED_TEMP */);
|
|
871
|
+
const layerN = latest.get(1052 /* MODEL_LAYER */);
|
|
872
|
+
const speedN = latest.get(1006 /* PRINT_SPEED */);
|
|
873
|
+
const schedule = latest.get(1001 /* PRINT_SCHEDULE */);
|
|
874
|
+
const eventN = latest.get(1e3 /* EVENT_NOTIFY */);
|
|
875
|
+
const eventState = num(eventN?.subType) === 1 ? num(eventN?.value) : void 0;
|
|
876
|
+
const raw = {};
|
|
877
|
+
for (const [type, payload] of latest) raw[String(type)] = payload;
|
|
878
|
+
const status = {
|
|
879
|
+
nozzle: {
|
|
880
|
+
current: centiToC(num(nozzleN?.currentTemp) ?? 0),
|
|
881
|
+
target: centiToC(num(nozzleN?.targetTemp) ?? 0)
|
|
882
|
+
},
|
|
883
|
+
bed: {
|
|
884
|
+
current: centiToC(num(bedN?.currentTemp) ?? 0),
|
|
885
|
+
target: centiToC(num(bedN?.targetTemp) ?? 0)
|
|
886
|
+
},
|
|
887
|
+
raw
|
|
888
|
+
};
|
|
889
|
+
if (schedule) {
|
|
890
|
+
const progressPct = Math.round((num(schedule.progress) ?? 0) / 100 * 100) / 100;
|
|
891
|
+
const reliable = opts.etaReliableOverride ?? isEtaReliable(schedule);
|
|
892
|
+
const left = num(schedule.startLeftTime);
|
|
893
|
+
status.job = {
|
|
894
|
+
name: typeof schedule.name === "string" ? schedule.name : "",
|
|
895
|
+
state: deriveState(schedule, progressPct, eventState),
|
|
896
|
+
progressPct,
|
|
897
|
+
layer: num(layerN?.real_print_layer) ?? 0,
|
|
898
|
+
totalLayers: num(layerN?.total_layer) ?? 0,
|
|
899
|
+
etaReliable: reliable,
|
|
900
|
+
...reliable && left !== void 0 ? { etaSeconds: left } : {},
|
|
901
|
+
...num(schedule.filamentUsed) !== void 0 ? { filamentUsed: num(schedule.filamentUsed) } : {},
|
|
902
|
+
...typeof schedule.filamentUnit === "string" ? { filamentUnit: schedule.filamentUnit } : {},
|
|
903
|
+
...num(speedN?.value) !== void 0 ? { speedMmS: num(speedN?.value) } : {},
|
|
904
|
+
...num(schedule.realSpeed) !== void 0 ? { speedFactorPct: num(schedule.realSpeed) } : {}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
return status;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/transport/certs.ts
|
|
911
|
+
var ANKERMAKE_MQTT_CA = `-----BEGIN CERTIFICATE-----
|
|
912
|
+
MIIDwTCCAqmgAwIBAgIJAKrbZvWARI3BMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNV
|
|
913
|
+
BAYTAkNOMREwDwYDVQQIDAhTaGVuemhlbjERMA8GA1UEBwwIU2hlbnpoZW4xEjAQ
|
|
914
|
+
BgNVBAoMCWFua2VybWFrZTESMBAGA1UECwwJYW5rZXJtYWtlMRgwFgYDVQQDDA8q
|
|
915
|
+
LmFua2VybWFrZS5jb20wIBcNMjIwNjE3MDMwNzU5WhgPMjEyMjA1MjQwMzA3NTla
|
|
916
|
+
MHUxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGVuemhlbjERMA8GA1UEBwwIU2hl
|
|
917
|
+
bnpoZW4xEjAQBgNVBAoMCWFua2VybWFrZTESMBAGA1UECwwJYW5rZXJtYWtlMRgw
|
|
918
|
+
FgYDVQQDDA8qLmFua2VybWFrZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
|
919
|
+
ggEKAoIBAQC8JWJzdVJFqrarK5oMCF8nI5QZ2nebs9df6CQHuSZCOmGCav5sDDFt
|
|
920
|
+
5IGhQ6G44++YNexC10kwxy10fOzIT6cZWnQrYQPBfS0y7G+yu/GPe9vXMWwkIcWv
|
|
921
|
+
hg8xAO+/m5C/QAj4BOVTXVl5spuBGX644P3eErV+tUDwb1U2K6mMzmaJ7SZqkmiw
|
|
922
|
+
QKfTK1KxH7oczcxjDtdbNdtpa1Rm3IUCCI2eAOQTlDHlKGGM2T+e6qQRCUQYqkiY
|
|
923
|
+
jG+3ugTzHMe6FMzOB1EjG0bZDemQwgUdBJexLgxrJe4jsVcuP75DfrV0NL/Drrmt
|
|
924
|
+
uJax3V4tu5Yx1RQCWqGTNPOahpS+qD+NAgMBAAGjUjBQMA4GA1UdDwEB/wQEAwID
|
|
925
|
+
iDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAggg1hbmtlcm1ha2UuY29t
|
|
926
|
+
gg8qLmFua2VybWFrZS5jb20wDQYJKoZIhvcNAQELBQADggEBALF/VDyZ21IdFejE
|
|
927
|
+
awLriK+Xo78k1yqf2YKWYSDMEJPXXHfbkHZTU0IL+K9kToN19sObuWPA1oE2iyKp
|
|
928
|
+
h4nKVDjy56Ntgt5lXeSTN08jlD0PzuuGfzPVxMrky8sp14pFT+Kw2HOEMLU6Hxj0
|
|
929
|
+
WjpprKRbl1oI8JoksYNzCSelIItokA8CI3/p1j5FyWxok99sVvNUfjG9iaV74Nuh
|
|
930
|
+
kY/1nm0T0aMPZKpcS0xS0JwA0tsySdDJP5t1KgmDa5D0hIhXuAJWGwUvg15vSyme
|
|
931
|
+
bk3IO48Nh8QOG8PwGebPus1nnvKCbG6+iJaWp/PqSqNCzx/Nht+Tfi413dIc3exF
|
|
932
|
+
LX0ZR20=
|
|
933
|
+
-----END CERTIFICATE-----`;
|
|
934
|
+
|
|
935
|
+
// src/binary.ts
|
|
936
|
+
var BufReader = class {
|
|
937
|
+
constructor(buf) {
|
|
938
|
+
this.buf = buf;
|
|
939
|
+
}
|
|
940
|
+
buf;
|
|
941
|
+
offset = 0;
|
|
942
|
+
get remaining() {
|
|
943
|
+
return this.buf.length - this.offset;
|
|
944
|
+
}
|
|
945
|
+
take(n) {
|
|
946
|
+
if (this.offset + n > this.buf.length) {
|
|
947
|
+
throw new RangeError(`buffer underrun: need ${n}, have ${this.remaining}`);
|
|
948
|
+
}
|
|
949
|
+
const slice = this.buf.subarray(this.offset, this.offset + n);
|
|
950
|
+
this.offset += n;
|
|
951
|
+
return slice;
|
|
952
|
+
}
|
|
953
|
+
u8() {
|
|
954
|
+
return this.take(1).readUInt8(0);
|
|
955
|
+
}
|
|
956
|
+
u16be() {
|
|
957
|
+
return this.take(2).readUInt16BE(0);
|
|
958
|
+
}
|
|
959
|
+
u16le() {
|
|
960
|
+
return this.take(2).readUInt16LE(0);
|
|
961
|
+
}
|
|
962
|
+
u32be() {
|
|
963
|
+
return this.take(4).readUInt32BE(0);
|
|
964
|
+
}
|
|
965
|
+
u32le() {
|
|
966
|
+
return this.take(4).readUInt32LE(0);
|
|
967
|
+
}
|
|
968
|
+
i32be() {
|
|
969
|
+
return this.take(4).readInt32BE(0);
|
|
970
|
+
}
|
|
971
|
+
i32le() {
|
|
972
|
+
return this.take(4).readInt32LE(0);
|
|
973
|
+
}
|
|
974
|
+
bytes(n) {
|
|
975
|
+
return Buffer.from(this.take(n));
|
|
976
|
+
}
|
|
977
|
+
tail() {
|
|
978
|
+
return this.bytes(this.remaining);
|
|
979
|
+
}
|
|
980
|
+
/** Read `n` zero bytes, asserting they are all zero. */
|
|
981
|
+
zeroes(n) {
|
|
982
|
+
const b = this.take(n);
|
|
983
|
+
for (const x of b) if (x !== 0) throw new Error("expected zero padding");
|
|
984
|
+
return Buffer.from(b);
|
|
985
|
+
}
|
|
986
|
+
/** Read `expected.length` bytes and assert they match (a magic signature). */
|
|
987
|
+
magic(expected) {
|
|
988
|
+
const b = this.take(expected.length);
|
|
989
|
+
if (!b.equals(expected)) {
|
|
990
|
+
throw new Error(`bad magic: expected ${expected.toString("hex")}, got ${b.toString("hex")}`);
|
|
991
|
+
}
|
|
992
|
+
return Buffer.from(b);
|
|
993
|
+
}
|
|
994
|
+
/** Fixed-width, NUL-terminated string field of `size` bytes (last byte NUL). */
|
|
995
|
+
string(size) {
|
|
996
|
+
const b = this.take(size);
|
|
997
|
+
if (b[size - 1] !== 0) throw new Error("expected NUL-terminated fixed string");
|
|
998
|
+
let end = size - 1;
|
|
999
|
+
for (let i = 0; i < size; i++) {
|
|
1000
|
+
if (b[i] === 0) {
|
|
1001
|
+
end = i;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return b.subarray(0, end).toString("utf8");
|
|
1006
|
+
}
|
|
1007
|
+
/** IPv4 address stored as 4 little-endian bytes (reversed dotted quad). */
|
|
1008
|
+
ipv4() {
|
|
1009
|
+
const b = this.take(4);
|
|
1010
|
+
return `${b[3]}.${b[2]}.${b[1]}.${b[0]}`;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
var BufWriter = class {
|
|
1014
|
+
chunks = [];
|
|
1015
|
+
u8(v) {
|
|
1016
|
+
const b = Buffer.alloc(1);
|
|
1017
|
+
b.writeUInt8(v & 255, 0);
|
|
1018
|
+
this.chunks.push(b);
|
|
1019
|
+
return this;
|
|
1020
|
+
}
|
|
1021
|
+
u16be(v) {
|
|
1022
|
+
const b = Buffer.alloc(2);
|
|
1023
|
+
b.writeUInt16BE(v & 65535, 0);
|
|
1024
|
+
this.chunks.push(b);
|
|
1025
|
+
return this;
|
|
1026
|
+
}
|
|
1027
|
+
u16le(v) {
|
|
1028
|
+
const b = Buffer.alloc(2);
|
|
1029
|
+
b.writeUInt16LE(v & 65535, 0);
|
|
1030
|
+
this.chunks.push(b);
|
|
1031
|
+
return this;
|
|
1032
|
+
}
|
|
1033
|
+
u32be(v) {
|
|
1034
|
+
const b = Buffer.alloc(4);
|
|
1035
|
+
b.writeUInt32BE(v >>> 0, 0);
|
|
1036
|
+
this.chunks.push(b);
|
|
1037
|
+
return this;
|
|
1038
|
+
}
|
|
1039
|
+
u32le(v) {
|
|
1040
|
+
const b = Buffer.alloc(4);
|
|
1041
|
+
b.writeUInt32LE(v >>> 0, 0);
|
|
1042
|
+
this.chunks.push(b);
|
|
1043
|
+
return this;
|
|
1044
|
+
}
|
|
1045
|
+
i32be(v) {
|
|
1046
|
+
const b = Buffer.alloc(4);
|
|
1047
|
+
b.writeInt32BE(v | 0, 0);
|
|
1048
|
+
this.chunks.push(b);
|
|
1049
|
+
return this;
|
|
1050
|
+
}
|
|
1051
|
+
i32le(v) {
|
|
1052
|
+
const b = Buffer.alloc(4);
|
|
1053
|
+
b.writeInt32LE(v | 0, 0);
|
|
1054
|
+
this.chunks.push(b);
|
|
1055
|
+
return this;
|
|
1056
|
+
}
|
|
1057
|
+
bytes(b) {
|
|
1058
|
+
this.chunks.push(Buffer.from(b));
|
|
1059
|
+
return this;
|
|
1060
|
+
}
|
|
1061
|
+
zeroes(n) {
|
|
1062
|
+
this.chunks.push(Buffer.alloc(n));
|
|
1063
|
+
return this;
|
|
1064
|
+
}
|
|
1065
|
+
/** Write a fixed-width string, NUL-padded to `size` bytes. */
|
|
1066
|
+
string(s, size) {
|
|
1067
|
+
const b = Buffer.alloc(size);
|
|
1068
|
+
Buffer.from(s, "utf8").copy(b, 0, 0, Math.min(size - 1, Buffer.byteLength(s, "utf8")));
|
|
1069
|
+
this.chunks.push(b);
|
|
1070
|
+
return this;
|
|
1071
|
+
}
|
|
1072
|
+
ipv4(addr) {
|
|
1073
|
+
const parts = addr.split(".").map((x) => parseInt(x, 10) & 255);
|
|
1074
|
+
if (parts.length !== 4) throw new Error(`invalid IPv4 address: ${addr}`);
|
|
1075
|
+
this.chunks.push(Buffer.from([parts[3], parts[2], parts[1], parts[0]]));
|
|
1076
|
+
return this;
|
|
1077
|
+
}
|
|
1078
|
+
build() {
|
|
1079
|
+
return Buffer.concat(this.chunks);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// src/transport/mqttframe.ts
|
|
1084
|
+
var SIGNATURE = Buffer.from("MA", "ascii");
|
|
1085
|
+
var M7_F = 70;
|
|
1086
|
+
var BODY_LEN = { 1: 24, 2: 64 };
|
|
1087
|
+
function packMqttMessage(opts) {
|
|
1088
|
+
const data = mqttAesEncrypt(Buffer.from(JSON.stringify(opts.payload), "utf8"), opts.key);
|
|
1089
|
+
const bodyLen = 64;
|
|
1090
|
+
const size = bodyLen + data.length + 1;
|
|
1091
|
+
const header = new BufWriter().bytes(SIGNATURE).u16le(size).u8(5).u8(1).u8(2).u8(5).u8(M7_F).u8(opts.packetType ?? 192 /* Single */).u16le(opts.packetNum ?? 0).u32le(opts.time ?? 0).string(opts.guid, 37).zeroes(11).build();
|
|
1092
|
+
return mqttChecksumAdd(Buffer.concat([header.subarray(0, bodyLen), data]));
|
|
1093
|
+
}
|
|
1094
|
+
function parseMqttMessage(payload, key) {
|
|
1095
|
+
const stripped = mqttChecksumRemove(payload);
|
|
1096
|
+
const m5 = stripped[6];
|
|
1097
|
+
const bodyLen = m5 !== void 0 ? BODY_LEN[m5] : void 0;
|
|
1098
|
+
if (bodyLen === void 0) {
|
|
1099
|
+
throw new Error(`unsupported MQTT message format (m5=${m5})`);
|
|
1100
|
+
}
|
|
1101
|
+
const body = stripped.subarray(0, bodyLen);
|
|
1102
|
+
const data = mqttAesDecrypt(stripped.subarray(bodyLen), key);
|
|
1103
|
+
const r = new BufReader(body);
|
|
1104
|
+
r.magic(SIGNATURE);
|
|
1105
|
+
r.u16le();
|
|
1106
|
+
r.u8();
|
|
1107
|
+
r.u8();
|
|
1108
|
+
r.u8();
|
|
1109
|
+
r.u8();
|
|
1110
|
+
r.u8();
|
|
1111
|
+
const packetType = r.u8();
|
|
1112
|
+
const packetNum = r.u16le();
|
|
1113
|
+
let time = 0;
|
|
1114
|
+
let deviceGuid = "none";
|
|
1115
|
+
if (m5 === 2) {
|
|
1116
|
+
time = r.u32le();
|
|
1117
|
+
deviceGuid = r.string(37);
|
|
1118
|
+
}
|
|
1119
|
+
return { packetType, packetNum, time, deviceGuid, payload: JSON.parse(data.toString("utf8")) };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/transport/mqtt.ts
|
|
1123
|
+
var COMMAND_REPLY = (sn) => `/phone/maker/${sn}/command/reply`;
|
|
1124
|
+
var QUERY_REPLY = (sn) => `/phone/maker/${sn}/query/reply`;
|
|
1125
|
+
var NOTICE = (sn) => `/phone/maker/${sn}/notice`;
|
|
1126
|
+
var COMMAND_TOPIC = (sn) => `/device/maker/${sn}/command`;
|
|
1127
|
+
var QUERY_TOPIC = (sn) => `/device/maker/${sn}/query`;
|
|
1128
|
+
function randomGuid() {
|
|
1129
|
+
const h = (n) => Math.floor(Math.random() * 16 ** n).toString(16).padStart(n, "0");
|
|
1130
|
+
return `${h(8)}-${h(4)}-4${h(3)}-${h(4)}-${h(8)}${h(4)}`;
|
|
1131
|
+
}
|
|
1132
|
+
var AnkerMqttClient = class {
|
|
1133
|
+
constructor(opts) {
|
|
1134
|
+
this.opts = opts;
|
|
1135
|
+
this.guid = opts.guid ?? randomGuid();
|
|
1136
|
+
this.log = opts.log ?? (() => {
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
opts;
|
|
1140
|
+
client;
|
|
1141
|
+
guid;
|
|
1142
|
+
log;
|
|
1143
|
+
noticeHandlers = /* @__PURE__ */ new Set();
|
|
1144
|
+
replyHandlers = /* @__PURE__ */ new Set();
|
|
1145
|
+
latestNotices = /* @__PURE__ */ new Map();
|
|
1146
|
+
gcodeLock = Promise.resolve();
|
|
1147
|
+
get connected() {
|
|
1148
|
+
return this.client?.connected ?? false;
|
|
1149
|
+
}
|
|
1150
|
+
async connect(timeoutMs = 3e4) {
|
|
1151
|
+
const { host, port = 8789, username, password, sn } = this.opts;
|
|
1152
|
+
this.log(`mqtt: connecting to ${host}:${port} as ${username}`);
|
|
1153
|
+
const client = await mqtt.connectAsync(`mqtts://${host}:${port}`, {
|
|
1154
|
+
username,
|
|
1155
|
+
password,
|
|
1156
|
+
ca: this.opts.ca ?? ANKERMAKE_MQTT_CA,
|
|
1157
|
+
rejectUnauthorized: !this.opts.insecure,
|
|
1158
|
+
connectTimeout: timeoutMs,
|
|
1159
|
+
reconnectPeriod: 0
|
|
1160
|
+
});
|
|
1161
|
+
this.client = client;
|
|
1162
|
+
client.on("message", (topic, payload) => this.onMessage(topic, payload));
|
|
1163
|
+
await client.subscribeAsync([COMMAND_REPLY(sn), QUERY_REPLY(sn), NOTICE(sn)]);
|
|
1164
|
+
this.log("mqtt: connected and subscribed");
|
|
1165
|
+
}
|
|
1166
|
+
async disconnect() {
|
|
1167
|
+
await this.client?.endAsync();
|
|
1168
|
+
this.client = void 0;
|
|
1169
|
+
}
|
|
1170
|
+
onMessage(_topic, payload) {
|
|
1171
|
+
let msg;
|
|
1172
|
+
try {
|
|
1173
|
+
msg = parseMqttMessage(payload, this.opts.key);
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
this.log(`mqtt: failed to decode message: ${err.message}`);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const objects = Array.isArray(msg.payload) ? msg.payload : [msg.payload];
|
|
1179
|
+
for (const obj of objects) {
|
|
1180
|
+
if (obj && typeof obj === "object") {
|
|
1181
|
+
const notice = obj;
|
|
1182
|
+
if (typeof notice.commandType === "number")
|
|
1183
|
+
this.latestNotices.set(notice.commandType, notice);
|
|
1184
|
+
for (const h of this.replyHandlers) h(notice);
|
|
1185
|
+
for (const h of this.noticeHandlers) h(notice);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
publish(topic, payload) {
|
|
1190
|
+
if (!this.client) throw new Error("MQTT client not connected");
|
|
1191
|
+
const packed = packMqttMessage({ guid: this.guid, payload, key: this.opts.key });
|
|
1192
|
+
this.client.publish(topic, packed);
|
|
1193
|
+
}
|
|
1194
|
+
/** Subscribe to streaming notices. Returns an unsubscribe function. */
|
|
1195
|
+
onNotice(handler) {
|
|
1196
|
+
this.noticeHandlers.add(handler);
|
|
1197
|
+
return () => this.noticeHandlers.delete(handler);
|
|
1198
|
+
}
|
|
1199
|
+
/** Publish a raw command payload (escape hatch for un-modeled commands). */
|
|
1200
|
+
command(payload) {
|
|
1201
|
+
this.publish(COMMAND_TOPIC(this.opts.sn), payload);
|
|
1202
|
+
}
|
|
1203
|
+
/** Publish a raw query payload. */
|
|
1204
|
+
query(payload) {
|
|
1205
|
+
this.publish(QUERY_TOPIC(this.opts.sn), payload);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Send a single gcode command and return the parsed response (§6). The result
|
|
1209
|
+
* carries `truncated` when the firmware's snapshot was partial. Serialized:
|
|
1210
|
+
* only one gcode is in flight at a time.
|
|
1211
|
+
*/
|
|
1212
|
+
async gcode(command, opts = {}) {
|
|
1213
|
+
const run = this.gcodeLock.then(() => this.gcodeOnce(command, opts));
|
|
1214
|
+
this.gcodeLock = run.catch(() => void 0);
|
|
1215
|
+
return run;
|
|
1216
|
+
}
|
|
1217
|
+
gcodeOnce(command, opts) {
|
|
1218
|
+
const { timeoutMs = 1e4, quietMs = 600, wait = true } = opts;
|
|
1219
|
+
const started = Date.now();
|
|
1220
|
+
const payload = {
|
|
1221
|
+
commandType: 1043 /* GCODE_COMMAND */,
|
|
1222
|
+
cmdData: command,
|
|
1223
|
+
cmdLen: Buffer.byteLength(command, "utf8")
|
|
1224
|
+
};
|
|
1225
|
+
if (!wait) {
|
|
1226
|
+
this.command(payload);
|
|
1227
|
+
return Promise.resolve(
|
|
1228
|
+
parseGcodeResult(command, [], { durationMs: Date.now() - started, timedOut: false })
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
return new Promise((resolve) => {
|
|
1232
|
+
const chunks = [];
|
|
1233
|
+
let lastFrameAt = Date.now();
|
|
1234
|
+
let settled = false;
|
|
1235
|
+
const finish = (timedOut) => {
|
|
1236
|
+
if (settled) return;
|
|
1237
|
+
settled = true;
|
|
1238
|
+
clearInterval(ticker);
|
|
1239
|
+
this.replyHandlers.delete(collector);
|
|
1240
|
+
resolve(parseGcodeResult(command, chunks, { durationMs: Date.now() - started, timedOut }));
|
|
1241
|
+
};
|
|
1242
|
+
const collector = (obj) => {
|
|
1243
|
+
if (obj.commandType !== 1043 /* GCODE_COMMAND */) return;
|
|
1244
|
+
const chunk = obj.resData;
|
|
1245
|
+
if (typeof chunk === "string") {
|
|
1246
|
+
chunks.push(chunk);
|
|
1247
|
+
lastFrameAt = Date.now();
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
this.replyHandlers.add(collector);
|
|
1251
|
+
this.command(payload);
|
|
1252
|
+
const okGraceMs = Math.min(quietMs, 250);
|
|
1253
|
+
const ticker = setInterval(
|
|
1254
|
+
() => {
|
|
1255
|
+
const now = Date.now();
|
|
1256
|
+
const idle = now - lastFrameAt;
|
|
1257
|
+
if (now - started >= timeoutMs) {
|
|
1258
|
+
finish(true);
|
|
1259
|
+
} else if (chunks.length > 0) {
|
|
1260
|
+
const trailingOk = gcodeHasTerminalOk(reassembleRaw(chunks));
|
|
1261
|
+
if (trailingOk && idle >= okGraceMs)
|
|
1262
|
+
finish(false);
|
|
1263
|
+
else if (idle >= quietMs) finish(false);
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
Math.max(25, Math.floor(okGraceMs / 2))
|
|
1267
|
+
);
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Snapshot current printer status by normalizing the latest notice of each
|
|
1272
|
+
* type. Optionally nudges the printer with an APP_QUERY_STATUS query and waits
|
|
1273
|
+
* briefly for fresh telemetry.
|
|
1274
|
+
*/
|
|
1275
|
+
async getStatus(opts = {}) {
|
|
1276
|
+
const { refresh = true, waitMs = 1200 } = opts;
|
|
1277
|
+
if (refresh && this.client) {
|
|
1278
|
+
this.query({ commandType: 1027 /* APP_QUERY_STATUS */ });
|
|
1279
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
1280
|
+
}
|
|
1281
|
+
return normalizeStatus([...this.latestNotices.values()]);
|
|
1282
|
+
}
|
|
1283
|
+
/** Throw a structured timeout error (used by waiters). */
|
|
1284
|
+
static timeout(message, hint) {
|
|
1285
|
+
throw new TimeoutError({ message, transport: "mqtt", hint });
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// src/transport/https.ts
|
|
1290
|
+
var US_REGIONS = /* @__PURE__ */ new Set(["US", "CA", "MX", "BR", "AR", "CU", "BS", "AU", "NZ"]);
|
|
1291
|
+
function guessRegion(countryCode) {
|
|
1292
|
+
return US_REGIONS.has(countryCode.toUpperCase()) ? "us" : "eu";
|
|
1293
|
+
}
|
|
1294
|
+
var LOGIN_HEADERS = {
|
|
1295
|
+
App_name: "anker_make",
|
|
1296
|
+
App_version: "",
|
|
1297
|
+
Model_type: "PC",
|
|
1298
|
+
Os_type: "windows",
|
|
1299
|
+
Os_version: "10sp1"
|
|
1300
|
+
};
|
|
1301
|
+
function num2(v) {
|
|
1302
|
+
return typeof v === "number" ? v : void 0;
|
|
1303
|
+
}
|
|
1304
|
+
function str(v, fallback = "") {
|
|
1305
|
+
return typeof v === "string" ? v : fallback;
|
|
1306
|
+
}
|
|
1307
|
+
var AnkerHttpApi = class {
|
|
1308
|
+
constructor(region, authToken) {
|
|
1309
|
+
this.region = region;
|
|
1310
|
+
this.authToken = authToken;
|
|
1311
|
+
}
|
|
1312
|
+
region;
|
|
1313
|
+
authToken;
|
|
1314
|
+
base() {
|
|
1315
|
+
return `https://${API_HOSTS[this.region]}`;
|
|
1316
|
+
}
|
|
1317
|
+
async request(scope, path, body, auth = false) {
|
|
1318
|
+
const headers = {
|
|
1319
|
+
"Content-Type": "application/json",
|
|
1320
|
+
...LOGIN_HEADERS
|
|
1321
|
+
};
|
|
1322
|
+
if (auth) {
|
|
1323
|
+
if (!this.authToken) throw new AuthError({ message: "Missing auth token" });
|
|
1324
|
+
headers["X-Auth-Token"] = this.authToken;
|
|
1325
|
+
}
|
|
1326
|
+
let resp;
|
|
1327
|
+
try {
|
|
1328
|
+
resp = await fetch(`${this.base()}${scope}${path}`, {
|
|
1329
|
+
method: body ? "POST" : "GET",
|
|
1330
|
+
headers,
|
|
1331
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
1332
|
+
});
|
|
1333
|
+
} catch (cause) {
|
|
1334
|
+
throw new AuthError({
|
|
1335
|
+
code: "https_unreachable",
|
|
1336
|
+
message: `Could not reach Anker cloud API (${API_HOSTS[this.region]})`,
|
|
1337
|
+
retriable: true,
|
|
1338
|
+
hint: "Check your internet connection and try again.",
|
|
1339
|
+
cause
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (!resp.ok) {
|
|
1343
|
+
throw new AuthError({
|
|
1344
|
+
code: "https_error",
|
|
1345
|
+
message: `API request failed: ${resp.status} ${resp.statusText}`,
|
|
1346
|
+
retriable: resp.status >= 500
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
const jsn = await resp.json();
|
|
1350
|
+
if (num2(jsn.code) !== 0) {
|
|
1351
|
+
throw this.apiError(jsn);
|
|
1352
|
+
}
|
|
1353
|
+
return jsn.data ?? {};
|
|
1354
|
+
}
|
|
1355
|
+
/** Translate a non-zero API response into a structured error (captcha-aware). */
|
|
1356
|
+
apiError(jsn) {
|
|
1357
|
+
const data = jsn.data ?? {};
|
|
1358
|
+
const message = str(jsn.msg, "API error");
|
|
1359
|
+
if (typeof data.captcha_id === "string") {
|
|
1360
|
+
return new AuthError({
|
|
1361
|
+
code: "captcha_required",
|
|
1362
|
+
message: "Login requires solving a captcha challenge",
|
|
1363
|
+
hint: "Re-run login with `--captcha-answer <text>` (and the captcha_id below). The captcha image URL is in `input`.",
|
|
1364
|
+
input: {
|
|
1365
|
+
captcha_id: data.captcha_id,
|
|
1366
|
+
...typeof data.item === "string" ? { captcha_image: data.item } : {}
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return new AuthError({
|
|
1371
|
+
code: "login_rejected",
|
|
1372
|
+
message,
|
|
1373
|
+
input: { api_code: num2(jsn.code) }
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
// --- /v2/passport/login ---
|
|
1377
|
+
async login(opts) {
|
|
1378
|
+
const { publicKey, encryptedPassword } = ecdhEncryptLoginPassword(opts.password);
|
|
1379
|
+
const body = {
|
|
1380
|
+
client_secret_info: { public_key: publicKey },
|
|
1381
|
+
email: opts.email,
|
|
1382
|
+
password: encryptedPassword
|
|
1383
|
+
};
|
|
1384
|
+
if (opts.captchaId) body.captcha_id = opts.captchaId;
|
|
1385
|
+
if (opts.captchaAnswer) body.answer = opts.captchaAnswer;
|
|
1386
|
+
const data = await this.request("/v2/passport/login", "", body);
|
|
1387
|
+
const authToken = str(data.auth_token);
|
|
1388
|
+
if (!authToken) {
|
|
1389
|
+
throw new AuthError({ code: "login_failed", message: "Login returned no auth token" });
|
|
1390
|
+
}
|
|
1391
|
+
return {
|
|
1392
|
+
auth_token: authToken,
|
|
1393
|
+
user_id: str(data.user_id),
|
|
1394
|
+
email: str(data.email, opts.email),
|
|
1395
|
+
region: this.region,
|
|
1396
|
+
ab_code: str(data.ab_code) || void 0
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
// --- /v1/passport/profile ---
|
|
1400
|
+
async profile() {
|
|
1401
|
+
const data = await this.request("/v1/passport", "/profile", void 0, true);
|
|
1402
|
+
const country = data.country?.code;
|
|
1403
|
+
return {
|
|
1404
|
+
user_id: str(data.user_id),
|
|
1405
|
+
email: str(data.email),
|
|
1406
|
+
country: str(country)
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
// --- /v1/app/query_fdm_list ---
|
|
1410
|
+
async queryFdmList() {
|
|
1411
|
+
const data = await this.request("/v1/app", "/query_fdm_list", {}, true);
|
|
1412
|
+
return Array.isArray(data) ? data : data.data ?? [];
|
|
1413
|
+
}
|
|
1414
|
+
// --- /v1/app/equipment/get_dsk_keys ---
|
|
1415
|
+
async getDskKeys(stationSns) {
|
|
1416
|
+
const data = await this.request(
|
|
1417
|
+
"/v1/app",
|
|
1418
|
+
"/equipment/get_dsk_keys",
|
|
1419
|
+
{ invalid_dsks: {}, station_sns: stationSns },
|
|
1420
|
+
true
|
|
1421
|
+
);
|
|
1422
|
+
const out = {};
|
|
1423
|
+
for (const dsk of data.dsk_keys ?? []) {
|
|
1424
|
+
const sn = str(dsk.station_sn);
|
|
1425
|
+
if (sn) out[sn] = str(dsk.dsk_key);
|
|
1426
|
+
}
|
|
1427
|
+
return out;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
function toPrinter(pr, dskKeys) {
|
|
1431
|
+
const sn = str(pr.station_sn);
|
|
1432
|
+
return {
|
|
1433
|
+
id: str(pr.station_id),
|
|
1434
|
+
sn,
|
|
1435
|
+
name: str(pr.station_name),
|
|
1436
|
+
model: str(pr.station_model),
|
|
1437
|
+
duid: str(pr.p2p_did),
|
|
1438
|
+
ip_addr: str(pr.ip_addr),
|
|
1439
|
+
wifi_mac: str(pr.wifi_mac),
|
|
1440
|
+
mqtt_key: str(pr.secret_key),
|
|
1441
|
+
p2p_key: dskKeys[sn] ?? "",
|
|
1442
|
+
api_hosts: pr.app_conn ? ppppDecodeInitstring(str(pr.app_conn)) : [],
|
|
1443
|
+
p2p_hosts: pr.p2p_conn ? ppppDecodeInitstring(str(pr.p2p_conn)) : []
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
async function loginAndBuildConfig(opts) {
|
|
1447
|
+
const region = guessRegion(opts.country);
|
|
1448
|
+
const api = new AnkerHttpApi(region);
|
|
1449
|
+
const login = await api.login(opts);
|
|
1450
|
+
const authed = new AnkerHttpApi(region, login.auth_token);
|
|
1451
|
+
const profile = await authed.profile();
|
|
1452
|
+
const printersRaw = await authed.queryFdmList();
|
|
1453
|
+
const sns = printersRaw.map((p) => str(p.station_sn)).filter(Boolean);
|
|
1454
|
+
const dskKeys = sns.length ? await authed.getDskKeys(sns) : {};
|
|
1455
|
+
const printers = printersRaw.map((p) => toPrinter(p, dskKeys)).sort((a, b) => Number(a.id) - Number(b.id));
|
|
1456
|
+
return {
|
|
1457
|
+
account: {
|
|
1458
|
+
user_id: profile.user_id || login.user_id,
|
|
1459
|
+
auth_token: login.auth_token,
|
|
1460
|
+
email: profile.email || login.email,
|
|
1461
|
+
region,
|
|
1462
|
+
country: profile.country || opts.country.toUpperCase()
|
|
1463
|
+
},
|
|
1464
|
+
printers
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/transport/pppp/client.ts
|
|
1469
|
+
import { createSocket } from "dgram";
|
|
1470
|
+
|
|
1471
|
+
// src/transport/pppp/packets.ts
|
|
1472
|
+
var MSG_MAGIC = 241;
|
|
1473
|
+
var FileTransferReply = /* @__PURE__ */ ((FileTransferReply2) => {
|
|
1474
|
+
FileTransferReply2[FileTransferReply2["OK"] = 0] = "OK";
|
|
1475
|
+
FileTransferReply2[FileTransferReply2["ERR_TIMEOUT"] = 252] = "ERR_TIMEOUT";
|
|
1476
|
+
FileTransferReply2[FileTransferReply2["ERR_FRAME_TYPE"] = 253] = "ERR_FRAME_TYPE";
|
|
1477
|
+
FileTransferReply2[FileTransferReply2["ERR_WRONG_MD5"] = 254] = "ERR_WRONG_MD5";
|
|
1478
|
+
FileTransferReply2[FileTransferReply2["ERR_BUSY"] = 255] = "ERR_BUSY";
|
|
1479
|
+
return FileTransferReply2;
|
|
1480
|
+
})(FileTransferReply || {});
|
|
1481
|
+
function parseDuidString(s) {
|
|
1482
|
+
const [prefix, serial, check] = s.split("-");
|
|
1483
|
+
return { prefix: prefix ?? "", serial: Number(serial ?? 0), check: check ?? "" };
|
|
1484
|
+
}
|
|
1485
|
+
function duidToString(d) {
|
|
1486
|
+
return `${d.prefix}-${String(d.serial).padStart(6, "0")}-${d.check}`;
|
|
1487
|
+
}
|
|
1488
|
+
function writeDuid(w, d) {
|
|
1489
|
+
w.string(d.prefix, 8).u32be(d.serial).string(d.check, 6).zeroes(2);
|
|
1490
|
+
}
|
|
1491
|
+
function readDuid(r) {
|
|
1492
|
+
const prefix = r.string(8);
|
|
1493
|
+
const serial = r.u32be();
|
|
1494
|
+
const check = r.string(6);
|
|
1495
|
+
r.zeroes(2);
|
|
1496
|
+
return { prefix, serial, check };
|
|
1497
|
+
}
|
|
1498
|
+
function writeHost(w, h) {
|
|
1499
|
+
w.zeroes(1).u8(h.afam).u16le(h.port).ipv4(h.addr).zeroes(8);
|
|
1500
|
+
}
|
|
1501
|
+
function wrapMessage(type, body) {
|
|
1502
|
+
return new BufWriter().u8(MSG_MAGIC).u8(type).u16be(body.length).bytes(body).build();
|
|
1503
|
+
}
|
|
1504
|
+
function parseMessage(buf) {
|
|
1505
|
+
const r = new BufReader(buf);
|
|
1506
|
+
const magic = r.u8();
|
|
1507
|
+
if (magic !== MSG_MAGIC) throw new Error(`bad PPPP magic: 0x${magic.toString(16)}`);
|
|
1508
|
+
const type = r.u8();
|
|
1509
|
+
const size = r.u16be();
|
|
1510
|
+
const body = r.bytes(size);
|
|
1511
|
+
const out = { type, body };
|
|
1512
|
+
switch (type) {
|
|
1513
|
+
case 65 /* PUNCH_PKT */:
|
|
1514
|
+
case 66 /* P2P_RDY */: {
|
|
1515
|
+
out.duid = readDuid(new BufReader(body));
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
case 208 /* DRW */: {
|
|
1519
|
+
const b = new BufReader(body);
|
|
1520
|
+
b.magic(Buffer.from([209]));
|
|
1521
|
+
out.chan = b.u8();
|
|
1522
|
+
out.index = b.u16be();
|
|
1523
|
+
out.data = b.tail();
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
case 209 /* DRW_ACK */: {
|
|
1527
|
+
const b = new BufReader(body);
|
|
1528
|
+
b.magic(Buffer.from([209]));
|
|
1529
|
+
out.chan = b.u8();
|
|
1530
|
+
const count = b.u16be();
|
|
1531
|
+
out.acks = Array.from({ length: count }, () => b.u16be());
|
|
1532
|
+
break;
|
|
1533
|
+
}
|
|
1534
|
+
case 249 /* REPORT_SESSION_READY */: {
|
|
1535
|
+
try {
|
|
1536
|
+
out.duid = readDuid(new BufReader(simpleDecrypt(body)));
|
|
1537
|
+
} catch {
|
|
1538
|
+
}
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
default:
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
return out;
|
|
1545
|
+
}
|
|
1546
|
+
var pktLanSearch = () => wrapMessage(48 /* LAN_SEARCH */, Buffer.alloc(0));
|
|
1547
|
+
var pktClose = () => wrapMessage(240 /* CLOSE */, Buffer.alloc(0));
|
|
1548
|
+
var pktAliveAck = () => wrapMessage(225 /* ALIVE_ACK */, Buffer.alloc(0));
|
|
1549
|
+
function pktP2pRdy(duid) {
|
|
1550
|
+
const w = new BufWriter();
|
|
1551
|
+
writeDuid(w, duid);
|
|
1552
|
+
return wrapMessage(66 /* P2P_RDY */, w.build());
|
|
1553
|
+
}
|
|
1554
|
+
function pktP2pRdyAck(duid, host) {
|
|
1555
|
+
const w = new BufWriter();
|
|
1556
|
+
writeDuid(w, duid);
|
|
1557
|
+
writeHost(w, host);
|
|
1558
|
+
w.zeroes(8);
|
|
1559
|
+
return wrapMessage(67 /* P2P_RDY_ACK */, w.build());
|
|
1560
|
+
}
|
|
1561
|
+
function pktHelloAck(host) {
|
|
1562
|
+
const w = new BufWriter();
|
|
1563
|
+
writeHost(w, host);
|
|
1564
|
+
return wrapMessage(1 /* HELLO_ACK */, w.build());
|
|
1565
|
+
}
|
|
1566
|
+
function pktDevLgnAckCrc() {
|
|
1567
|
+
return wrapMessage(19 /* DEV_LGN_ACK_CRC */, cryptoCurseString(Buffer.alloc(4)));
|
|
1568
|
+
}
|
|
1569
|
+
function pktDrw(chan, index, data) {
|
|
1570
|
+
const body = new BufWriter().u8(209).u8(chan).u16be(index).bytes(data).build();
|
|
1571
|
+
return wrapMessage(208 /* DRW */, body);
|
|
1572
|
+
}
|
|
1573
|
+
function pktDrwAck(chan, acks) {
|
|
1574
|
+
const w = new BufWriter().u8(209).u8(chan).u16be(acks.length);
|
|
1575
|
+
for (const a of acks) w.u16be(a);
|
|
1576
|
+
return wrapMessage(209 /* DRW_ACK */, w.build());
|
|
1577
|
+
}
|
|
1578
|
+
function packXzyh(opts) {
|
|
1579
|
+
const { cmd, data, chan = 0, signCode = 0, devType = 0 } = opts;
|
|
1580
|
+
return new BufWriter().bytes(Buffer.from("XZYH", "ascii")).u16le(cmd).u32le(data.length).u8(0).u8(0).u8(chan).u8(signCode).u8(0).u8(devType).bytes(data).build();
|
|
1581
|
+
}
|
|
1582
|
+
var AABB_SIG = Buffer.from([170, 187]);
|
|
1583
|
+
function packAabbHeader(h) {
|
|
1584
|
+
return new BufWriter().bytes(AABB_SIG).u8(h.frametype).u8(h.sn).u32le(h.pos).u32le(h.len).build();
|
|
1585
|
+
}
|
|
1586
|
+
function packAabb(frametype, data, opts = {}) {
|
|
1587
|
+
const header = packAabbHeader({
|
|
1588
|
+
frametype,
|
|
1589
|
+
sn: opts.sn ?? 0,
|
|
1590
|
+
pos: opts.pos ?? 0,
|
|
1591
|
+
len: data.length
|
|
1592
|
+
});
|
|
1593
|
+
const crc = ppcsCrc16(Buffer.concat([header.subarray(2), data]));
|
|
1594
|
+
return Buffer.concat([header, data, crc]);
|
|
1595
|
+
}
|
|
1596
|
+
function parseAabbWithCrc(buf) {
|
|
1597
|
+
const head = buf.subarray(0, 12);
|
|
1598
|
+
const r = new BufReader(head);
|
|
1599
|
+
r.magic(AABB_SIG);
|
|
1600
|
+
const frametype = r.u8();
|
|
1601
|
+
const sn = r.u8();
|
|
1602
|
+
const pos = r.u32le();
|
|
1603
|
+
const len = r.u32le();
|
|
1604
|
+
const data = buf.subarray(12, 12 + len);
|
|
1605
|
+
const crc1 = buf.subarray(12 + len, 12 + len + 2);
|
|
1606
|
+
const crc2 = ppcsCrc16(Buffer.concat([head.subarray(2), data]));
|
|
1607
|
+
if (!crc1.equals(crc2)) throw new Error("AABB CRC mismatch");
|
|
1608
|
+
return { header: { frametype, sn, pos, len }, data: Buffer.from(data) };
|
|
1609
|
+
}
|
|
1610
|
+
function sanitizeFilename(name) {
|
|
1611
|
+
const base = name.split(/[\\/]/).pop() ?? name;
|
|
1612
|
+
const cleaned = [...base].map((c) => /[A-Za-z0-9._-]/.test(c) ? c : "_").join("");
|
|
1613
|
+
return cleaned.replace(/^\.+/, "").replace(/\.\./g, ".");
|
|
1614
|
+
}
|
|
1615
|
+
function packFileUploadInfo(f) {
|
|
1616
|
+
const type = f.type ?? 0;
|
|
1617
|
+
const str2 = `${type},${f.name},${f.size},${f.md5},${f.userName},${f.userId},${f.machineId}`;
|
|
1618
|
+
return Buffer.concat([Buffer.from(str2, "utf8"), Buffer.from([0])]);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/transport/pppp/channel.ts
|
|
1622
|
+
var U16 = 65535;
|
|
1623
|
+
var trunc = (n) => n & U16;
|
|
1624
|
+
function cyclicLt(a, b, wrap = 256) {
|
|
1625
|
+
a = trunc(a);
|
|
1626
|
+
b = trunc(b);
|
|
1627
|
+
if ((a ^ b) & 32768) return trunc(a - wrap) < trunc(b - wrap);
|
|
1628
|
+
return a < b;
|
|
1629
|
+
}
|
|
1630
|
+
function cyclicGt(a, b, wrap = 256) {
|
|
1631
|
+
a = trunc(a);
|
|
1632
|
+
b = trunc(b);
|
|
1633
|
+
if ((a ^ b) & 32768) return trunc(a - wrap) > trunc(b - wrap);
|
|
1634
|
+
return a > b;
|
|
1635
|
+
}
|
|
1636
|
+
var cyclicGte = (a, b, wrap = 256) => !cyclicLt(a, b, wrap);
|
|
1637
|
+
var CHUNK = 1024;
|
|
1638
|
+
var Channel = class {
|
|
1639
|
+
constructor(index, timeoutMs = 500, maxInFlight = 64) {
|
|
1640
|
+
this.index = index;
|
|
1641
|
+
this.timeoutMs = timeoutMs;
|
|
1642
|
+
this.maxInFlight = maxInFlight;
|
|
1643
|
+
}
|
|
1644
|
+
index;
|
|
1645
|
+
timeoutMs;
|
|
1646
|
+
maxInFlight;
|
|
1647
|
+
txCtr = 0;
|
|
1648
|
+
txAck = 0;
|
|
1649
|
+
backlog = [];
|
|
1650
|
+
txqueue = /* @__PURE__ */ new Map();
|
|
1651
|
+
acks = /* @__PURE__ */ new Set();
|
|
1652
|
+
ackWaiters = [];
|
|
1653
|
+
rxCtr = 0;
|
|
1654
|
+
rxqueue = /* @__PURE__ */ new Map();
|
|
1655
|
+
rxBuf = Buffer.alloc(0);
|
|
1656
|
+
rxWaiters = [];
|
|
1657
|
+
// --- transmit side ---
|
|
1658
|
+
/** Queue `payload` for reliable delivery; returns its sequence range. */
|
|
1659
|
+
write(payload) {
|
|
1660
|
+
const start = this.txCtr;
|
|
1661
|
+
let rest = payload;
|
|
1662
|
+
do {
|
|
1663
|
+
const data = rest.subarray(0, CHUNK);
|
|
1664
|
+
rest = rest.subarray(CHUNK);
|
|
1665
|
+
this.backlog.push({ ctr: this.txCtr, data });
|
|
1666
|
+
this.txCtr = trunc(this.txCtr + 1);
|
|
1667
|
+
} while (rest.length > 0);
|
|
1668
|
+
return { start, done: this.txCtr };
|
|
1669
|
+
}
|
|
1670
|
+
/** Resolve once every chunk up to (but excluding) `done` has been ACKed. */
|
|
1671
|
+
ackUpTo(done) {
|
|
1672
|
+
if (cyclicGte(this.txAck, done)) return Promise.resolve();
|
|
1673
|
+
return new Promise((resolve) => this.ackWaiters.push({ ctr: done, resolve }));
|
|
1674
|
+
}
|
|
1675
|
+
/** Apply received ACKs, advancing `txAck` and releasing waiters. */
|
|
1676
|
+
rxAck(acks) {
|
|
1677
|
+
for (const a of acks) {
|
|
1678
|
+
this.txqueue.delete(a);
|
|
1679
|
+
if (cyclicGte(a, this.txAck)) this.acks.add(a);
|
|
1680
|
+
}
|
|
1681
|
+
while (this.acks.has(this.txAck)) {
|
|
1682
|
+
this.acks.delete(this.txAck);
|
|
1683
|
+
this.txAck = trunc(this.txAck + 1);
|
|
1684
|
+
}
|
|
1685
|
+
for (let i = this.ackWaiters.length - 1; i >= 0; i--) {
|
|
1686
|
+
if (cyclicGte(this.txAck, this.ackWaiters[i].ctr)) {
|
|
1687
|
+
this.ackWaiters.splice(i, 1)[0].resolve();
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/** Produce DRW packets to (re)transmit; called on each poll tick. */
|
|
1692
|
+
poll(now) {
|
|
1693
|
+
while (this.backlog.length && this.txqueue.size < this.maxInFlight) {
|
|
1694
|
+
const item = this.backlog.shift();
|
|
1695
|
+
this.txqueue.set(item.ctr, { deadline: now, data: item.data });
|
|
1696
|
+
}
|
|
1697
|
+
const out = [];
|
|
1698
|
+
for (const [ctr, entry] of this.txqueue) {
|
|
1699
|
+
if (entry.deadline <= now) {
|
|
1700
|
+
out.push(pktDrw(this.index, ctr, entry.data));
|
|
1701
|
+
entry.deadline = now + this.timeoutMs;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return out;
|
|
1705
|
+
}
|
|
1706
|
+
/** True once all queued writes have been fully ACKed. */
|
|
1707
|
+
get drained() {
|
|
1708
|
+
return this.backlog.length === 0 && this.txqueue.size === 0;
|
|
1709
|
+
}
|
|
1710
|
+
// --- receive side ---
|
|
1711
|
+
/** Ingest a received DRW chunk, reordering into the contiguous stream. */
|
|
1712
|
+
rxDrw(index, data) {
|
|
1713
|
+
if (cyclicGt(this.rxCtr, index)) return;
|
|
1714
|
+
this.rxqueue.set(trunc(index), data);
|
|
1715
|
+
let advanced = false;
|
|
1716
|
+
while (this.rxqueue.has(this.rxCtr)) {
|
|
1717
|
+
const chunk = this.rxqueue.get(this.rxCtr);
|
|
1718
|
+
this.rxqueue.delete(this.rxCtr);
|
|
1719
|
+
this.rxCtr = trunc(this.rxCtr + 1);
|
|
1720
|
+
this.rxBuf = Buffer.concat([this.rxBuf, chunk]);
|
|
1721
|
+
advanced = true;
|
|
1722
|
+
}
|
|
1723
|
+
if (advanced) {
|
|
1724
|
+
for (const w of this.rxWaiters.splice(0)) w();
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
waitForData(timeoutMs) {
|
|
1728
|
+
return new Promise((resolve, reject) => {
|
|
1729
|
+
const timer = timeoutMs !== void 0 ? setTimeout(() => {
|
|
1730
|
+
const i = this.rxWaiters.indexOf(onData);
|
|
1731
|
+
if (i >= 0) this.rxWaiters.splice(i, 1);
|
|
1732
|
+
reject(new Error("channel read timeout"));
|
|
1733
|
+
}, timeoutMs) : void 0;
|
|
1734
|
+
const onData = () => {
|
|
1735
|
+
if (timer) clearTimeout(timer);
|
|
1736
|
+
resolve();
|
|
1737
|
+
};
|
|
1738
|
+
this.rxWaiters.push(onData);
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
/** Read exactly `n` bytes from the reassembled stream (awaiting more). */
|
|
1742
|
+
async read(n, timeoutMs) {
|
|
1743
|
+
while (this.rxBuf.length < n) {
|
|
1744
|
+
await this.waitForData(timeoutMs);
|
|
1745
|
+
}
|
|
1746
|
+
const out = this.rxBuf.subarray(0, n);
|
|
1747
|
+
this.rxBuf = this.rxBuf.subarray(n);
|
|
1748
|
+
return Buffer.from(out);
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// src/transport/pppp/client.ts
|
|
1753
|
+
var PPPP_LAN_PORT = 32108;
|
|
1754
|
+
var PpppState = /* @__PURE__ */ ((PpppState2) => {
|
|
1755
|
+
PpppState2[PpppState2["Idle"] = 1] = "Idle";
|
|
1756
|
+
PpppState2[PpppState2["Connecting"] = 2] = "Connecting";
|
|
1757
|
+
PpppState2[PpppState2["Connected"] = 3] = "Connected";
|
|
1758
|
+
PpppState2[PpppState2["Disconnected"] = 4] = "Disconnected";
|
|
1759
|
+
return PpppState2;
|
|
1760
|
+
})(PpppState || {});
|
|
1761
|
+
function discoverLan(opts = {}) {
|
|
1762
|
+
const { timeoutMs = 1e3, bindAddr, log = () => {
|
|
1763
|
+
} } = opts;
|
|
1764
|
+
return new Promise((resolve, reject) => {
|
|
1765
|
+
const sock = createSocket({ type: "udp4", reuseAddr: true });
|
|
1766
|
+
const found = /* @__PURE__ */ new Map();
|
|
1767
|
+
const done = () => {
|
|
1768
|
+
try {
|
|
1769
|
+
sock.close();
|
|
1770
|
+
} catch {
|
|
1771
|
+
}
|
|
1772
|
+
resolve([...found].map(([duid, ip]) => ({ duid, ip })));
|
|
1773
|
+
};
|
|
1774
|
+
sock.on("error", (err) => {
|
|
1775
|
+
try {
|
|
1776
|
+
sock.close();
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
reject(err);
|
|
1780
|
+
});
|
|
1781
|
+
sock.on("message", (data, rinfo) => {
|
|
1782
|
+
try {
|
|
1783
|
+
const msg = parseMessage(data);
|
|
1784
|
+
if (msg.type === 65 /* PUNCH_PKT */ && msg.duid) {
|
|
1785
|
+
found.set(duidToString(msg.duid), rinfo.address);
|
|
1786
|
+
}
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
sock.bind(bindAddr ? { address: bindAddr, port: 0 } : { port: 0 }, () => {
|
|
1791
|
+
sock.setBroadcast(true);
|
|
1792
|
+
log(`pppp: broadcasting LAN_SEARCH (timeout ${timeoutMs}ms)`);
|
|
1793
|
+
sock.send(pktLanSearch(), PPPP_LAN_PORT, "255.255.255.255");
|
|
1794
|
+
setTimeout(done, timeoutMs);
|
|
1795
|
+
});
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
var AnkerPpppClient = class {
|
|
1799
|
+
sock;
|
|
1800
|
+
addr;
|
|
1801
|
+
duid;
|
|
1802
|
+
chans = Array.from({ length: 8 }, (_, i) => new Channel(i));
|
|
1803
|
+
pollTimer;
|
|
1804
|
+
log;
|
|
1805
|
+
state = 1 /* Idle */;
|
|
1806
|
+
connectResolve;
|
|
1807
|
+
connectReject;
|
|
1808
|
+
constructor(opts) {
|
|
1809
|
+
this.duid = parseDuidString(opts.duid);
|
|
1810
|
+
this.addr = { host: opts.host, port: opts.port ?? PPPP_LAN_PORT };
|
|
1811
|
+
this.log = opts.log ?? (() => {
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
get host() {
|
|
1815
|
+
return { afam: 2, port: this.addr.port, addr: this.addr.host };
|
|
1816
|
+
}
|
|
1817
|
+
send(buf) {
|
|
1818
|
+
this.sock?.send(buf, this.addr.port, this.addr.host);
|
|
1819
|
+
}
|
|
1820
|
+
/** Connect over the LAN via the punch/ready handshake. */
|
|
1821
|
+
connect(timeoutMs = 1e4) {
|
|
1822
|
+
this.sock = createSocket("udp4");
|
|
1823
|
+
this.sock.on("message", (data, rinfo) => {
|
|
1824
|
+
this.addr = { host: rinfo.address, port: rinfo.port };
|
|
1825
|
+
this.process(data);
|
|
1826
|
+
});
|
|
1827
|
+
this.sock.on("error", (err) => this.failConnect(err));
|
|
1828
|
+
this.state = 2 /* Connecting */;
|
|
1829
|
+
const ready = new Promise((resolve, reject) => {
|
|
1830
|
+
this.connectResolve = resolve;
|
|
1831
|
+
this.connectReject = reject;
|
|
1832
|
+
});
|
|
1833
|
+
this.send(pktLanSearch());
|
|
1834
|
+
this.pollTimer = setInterval(() => this.pollChannels(), 20);
|
|
1835
|
+
const timer = setTimeout(() => {
|
|
1836
|
+
if (this.state !== 3 /* Connected */) {
|
|
1837
|
+
this.failConnect(
|
|
1838
|
+
new TimeoutError({
|
|
1839
|
+
message: `PPPP handshake to ${this.addr.host} timed out`,
|
|
1840
|
+
transport: "pppp",
|
|
1841
|
+
hint: "The printer may be off-LAN or asleep. Re-run `ankerts discover --store` and retry."
|
|
1842
|
+
})
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
}, timeoutMs);
|
|
1846
|
+
return ready.finally(() => clearTimeout(timer));
|
|
1847
|
+
}
|
|
1848
|
+
failConnect(err) {
|
|
1849
|
+
this.state = 4 /* Disconnected */;
|
|
1850
|
+
this.stop();
|
|
1851
|
+
this.connectReject?.(err);
|
|
1852
|
+
this.connectReject = void 0;
|
|
1853
|
+
this.connectResolve = void 0;
|
|
1854
|
+
}
|
|
1855
|
+
pollChannels() {
|
|
1856
|
+
const now = Date.now();
|
|
1857
|
+
for (const ch of this.chans) {
|
|
1858
|
+
for (const pkt of ch.poll(now)) this.send(pkt);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
process(data) {
|
|
1862
|
+
let msg;
|
|
1863
|
+
try {
|
|
1864
|
+
msg = parseMessage(data);
|
|
1865
|
+
} catch {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
switch (msg.type) {
|
|
1869
|
+
case 240 /* CLOSE */:
|
|
1870
|
+
this.failConnect(new Error("PPPP connection closed by device"));
|
|
1871
|
+
break;
|
|
1872
|
+
case 224 /* ALIVE */:
|
|
1873
|
+
this.send(pktAliveAck());
|
|
1874
|
+
break;
|
|
1875
|
+
case 208 /* DRW */:
|
|
1876
|
+
if (msg.chan !== void 0 && msg.index !== void 0 && msg.data) {
|
|
1877
|
+
this.send(pktDrwAck(msg.chan, [msg.index]));
|
|
1878
|
+
this.chans[msg.chan]?.rxDrw(msg.index, msg.data);
|
|
1879
|
+
}
|
|
1880
|
+
break;
|
|
1881
|
+
case 209 /* DRW_ACK */:
|
|
1882
|
+
if (msg.chan !== void 0 && msg.acks) this.chans[msg.chan]?.rxAck(msg.acks);
|
|
1883
|
+
break;
|
|
1884
|
+
case 18 /* DEV_LGN_CRC */:
|
|
1885
|
+
this.send(pktDevLgnAckCrc());
|
|
1886
|
+
break;
|
|
1887
|
+
case 0 /* HELLO */:
|
|
1888
|
+
this.send(pktHelloAck(this.host));
|
|
1889
|
+
break;
|
|
1890
|
+
case 66 /* P2P_RDY */:
|
|
1891
|
+
this.send(pktP2pRdyAck(this.duid, this.host));
|
|
1892
|
+
this.state = 3 /* Connected */;
|
|
1893
|
+
this.connectResolve?.();
|
|
1894
|
+
this.connectResolve = void 0;
|
|
1895
|
+
this.connectReject = void 0;
|
|
1896
|
+
this.log("pppp: connected");
|
|
1897
|
+
break;
|
|
1898
|
+
case 65 /* PUNCH_PKT */:
|
|
1899
|
+
if (this.state === 2 /* Connecting */) {
|
|
1900
|
+
this.send(pktClose());
|
|
1901
|
+
this.send(pktP2pRdy(this.duid));
|
|
1902
|
+
}
|
|
1903
|
+
break;
|
|
1904
|
+
default:
|
|
1905
|
+
break;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
stop() {
|
|
1909
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
1910
|
+
this.pollTimer = void 0;
|
|
1911
|
+
if (this.sock) {
|
|
1912
|
+
try {
|
|
1913
|
+
if (this.state === 3 /* Connected */) this.send(pktClose());
|
|
1914
|
+
this.sock.close();
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
this.sock = void 0;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/** Send one channel-1 AABB request and await the printer's reply byte. */
|
|
1921
|
+
async aabbRequest(frametype, data, opts = {}) {
|
|
1922
|
+
const ch = this.chans[1];
|
|
1923
|
+
const { done } = ch.write(packAabb(frametype, data, { pos: opts.pos ?? 0 }));
|
|
1924
|
+
await ch.ackUpTo(done);
|
|
1925
|
+
const replyTimeout = opts.replyTimeoutMs ?? 3e4;
|
|
1926
|
+
const head = await ch.read(12, replyTimeout);
|
|
1927
|
+
const len = head.readUInt32LE(8);
|
|
1928
|
+
const rest = await ch.read(len + 2, replyTimeout);
|
|
1929
|
+
const { data: payload } = parseAabbWithCrc(Buffer.concat([head, rest]));
|
|
1930
|
+
const reply = payload[0] ?? 255 /* ERR_BUSY */;
|
|
1931
|
+
if (reply !== 0 /* OK */) {
|
|
1932
|
+
throw new PrinterRejectedError({
|
|
1933
|
+
code: "upload_rejected",
|
|
1934
|
+
message: `Printer rejected file transfer: ${FileTransferReply[reply] ?? reply}`,
|
|
1935
|
+
transport: "pppp"
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
return reply;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Upload a gcode file and (by default) start the print. Mirrors the reference
|
|
1942
|
+
* web service: XZYH(P2P_SEND_FILE) → AABB BEGIN (metadata) → AABB DATA chunks
|
|
1943
|
+
* → AABB END (starts printing).
|
|
1944
|
+
*/
|
|
1945
|
+
async uploadFile(filename, data, opts = {}) {
|
|
1946
|
+
if (this.state !== 3 /* Connected */) {
|
|
1947
|
+
throw new TransportUnavailableError({
|
|
1948
|
+
message: "PPPP not connected \u2014 cannot upload",
|
|
1949
|
+
transport: "pppp",
|
|
1950
|
+
hint: "Run discovery and connect to the printer on the LAN first."
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
const name = sanitizeFilename(filename);
|
|
1954
|
+
const md5 = md5Hex(data);
|
|
1955
|
+
const fui = packFileUploadInfo({
|
|
1956
|
+
name,
|
|
1957
|
+
size: data.length,
|
|
1958
|
+
md5,
|
|
1959
|
+
userName: opts.userName ?? "ankerts",
|
|
1960
|
+
userId: opts.userId ?? "-",
|
|
1961
|
+
machineId: opts.machineId ?? "-"
|
|
1962
|
+
});
|
|
1963
|
+
const token = Buffer.from(
|
|
1964
|
+
globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 16),
|
|
1965
|
+
"utf8"
|
|
1966
|
+
);
|
|
1967
|
+
const ch0 = this.chans[0];
|
|
1968
|
+
const { done: tokenDone } = ch0.write(packXzyh({ cmd: 15e3 /* P2P_SEND_FILE */, data: token }));
|
|
1969
|
+
await ch0.ackUpTo(tokenDone);
|
|
1970
|
+
this.log(`pppp: uploading ${data.length} bytes as ${name}`);
|
|
1971
|
+
await this.aabbRequest(0 /* BEGIN */, Buffer.concat([fui, Buffer.from([0])]));
|
|
1972
|
+
const blockSize = 1024 * 32;
|
|
1973
|
+
let sent = 0;
|
|
1974
|
+
for (let pos = 0; pos < data.length; pos += blockSize) {
|
|
1975
|
+
const chunk = data.subarray(pos, pos + blockSize);
|
|
1976
|
+
await this.aabbRequest(1 /* DATA */, chunk, { pos });
|
|
1977
|
+
sent += chunk.length;
|
|
1978
|
+
opts.onProgress?.({ sent, total: data.length, pct: sent / data.length * 100 });
|
|
1979
|
+
}
|
|
1980
|
+
const started = opts.start !== false;
|
|
1981
|
+
if (started) {
|
|
1982
|
+
await this.aabbRequest(2 /* END */, Buffer.alloc(0));
|
|
1983
|
+
}
|
|
1984
|
+
return { name, size: data.length, md5, started };
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
// src/wait.ts
|
|
1989
|
+
var TEMP_STABLE_DELTA = 2;
|
|
1990
|
+
function parseWaitCondition(input) {
|
|
1991
|
+
const s = input.trim();
|
|
1992
|
+
const cmp = /^(nozzle|bed|progress|layer)\s*>=\s*(-?\d+(?:\.\d+)?)$/.exec(s);
|
|
1993
|
+
if (cmp) {
|
|
1994
|
+
const value = Number(cmp[2]);
|
|
1995
|
+
switch (cmp[1]) {
|
|
1996
|
+
case "nozzle":
|
|
1997
|
+
return { kind: "nozzle", atLeast: value };
|
|
1998
|
+
case "bed":
|
|
1999
|
+
return { kind: "bed", atLeast: value };
|
|
2000
|
+
case "progress":
|
|
2001
|
+
return { kind: "progress", atLeast: value };
|
|
2002
|
+
case "layer":
|
|
2003
|
+
return { kind: "layer", atLeast: value };
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
switch (s) {
|
|
2007
|
+
case "connected":
|
|
2008
|
+
case "lan":
|
|
2009
|
+
case "temp-stable":
|
|
2010
|
+
case "printing":
|
|
2011
|
+
case "idle":
|
|
2012
|
+
case "complete":
|
|
2013
|
+
case "failed":
|
|
2014
|
+
case "cancelled":
|
|
2015
|
+
case "runout":
|
|
2016
|
+
return { kind: s };
|
|
2017
|
+
default:
|
|
2018
|
+
throw new Error(
|
|
2019
|
+
`unknown wait condition: "${input}" (try connected|lan|nozzle>=C|bed>=C|temp-stable|printing|idle|progress>=pct|layer>=n|complete|failed|cancelled|runout)`
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function describeWaitCondition(cond) {
|
|
2024
|
+
switch (cond.kind) {
|
|
2025
|
+
case "nozzle":
|
|
2026
|
+
return `nozzle>=${cond.atLeast}`;
|
|
2027
|
+
case "bed":
|
|
2028
|
+
return `bed>=${cond.atLeast}`;
|
|
2029
|
+
case "progress":
|
|
2030
|
+
return `progress>=${cond.atLeast}`;
|
|
2031
|
+
case "layer":
|
|
2032
|
+
return `layer>=${cond.atLeast}`;
|
|
2033
|
+
default:
|
|
2034
|
+
return cond.kind;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function conditionHolds(cond, status) {
|
|
2038
|
+
const job = status.job;
|
|
2039
|
+
switch (cond.kind) {
|
|
2040
|
+
case "connected":
|
|
2041
|
+
case "lan":
|
|
2042
|
+
return null;
|
|
2043
|
+
case "nozzle":
|
|
2044
|
+
return status.nozzle.current >= cond.atLeast;
|
|
2045
|
+
case "bed":
|
|
2046
|
+
return status.bed.current >= cond.atLeast;
|
|
2047
|
+
case "temp-stable": {
|
|
2048
|
+
const nozzleOk = status.nozzle.target > 0 && Math.abs(status.nozzle.current - status.nozzle.target) <= TEMP_STABLE_DELTA;
|
|
2049
|
+
const bedOk = status.bed.target <= 0 || Math.abs(status.bed.current - status.bed.target) <= TEMP_STABLE_DELTA;
|
|
2050
|
+
return nozzleOk && bedOk;
|
|
2051
|
+
}
|
|
2052
|
+
case "printing":
|
|
2053
|
+
return job?.state === "printing";
|
|
2054
|
+
case "idle":
|
|
2055
|
+
return !job || job.state === "idle" || job.state === "complete";
|
|
2056
|
+
case "progress":
|
|
2057
|
+
return (job?.progressPct ?? -1) >= cond.atLeast;
|
|
2058
|
+
case "layer":
|
|
2059
|
+
return (job?.layer ?? -1) >= cond.atLeast;
|
|
2060
|
+
case "complete":
|
|
2061
|
+
return job?.state === "complete";
|
|
2062
|
+
case "failed":
|
|
2063
|
+
return job?.state === "failed";
|
|
2064
|
+
case "cancelled":
|
|
2065
|
+
return job?.state === "cancelled";
|
|
2066
|
+
case "runout":
|
|
2067
|
+
return job?.state === "failed" || JSON.stringify(status.raw).toLowerCase().includes("runout");
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/client.ts
|
|
2072
|
+
function defaultTimeoutFor(command) {
|
|
2073
|
+
const code = /^\s*([GM]\d+)/i.exec(command)?.[1]?.toUpperCase();
|
|
2074
|
+
switch (code) {
|
|
2075
|
+
case "M109":
|
|
2076
|
+
// heat-and-wait hotend
|
|
2077
|
+
case "M190":
|
|
2078
|
+
// heat-and-wait bed
|
|
2079
|
+
case "M303":
|
|
2080
|
+
// PID autotune
|
|
2081
|
+
case "G29":
|
|
2082
|
+
return 6e5;
|
|
2083
|
+
// minutes
|
|
2084
|
+
case "G28":
|
|
2085
|
+
// homing
|
|
2086
|
+
case "M400":
|
|
2087
|
+
return 12e4;
|
|
2088
|
+
default:
|
|
2089
|
+
return 1e4;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
var AnkerClient = class _AnkerClient {
|
|
2093
|
+
config;
|
|
2094
|
+
store;
|
|
2095
|
+
log;
|
|
2096
|
+
insecure;
|
|
2097
|
+
printerRef;
|
|
2098
|
+
mqtt;
|
|
2099
|
+
constructor(config, opts = {}) {
|
|
2100
|
+
this.config = config;
|
|
2101
|
+
this.store = opts.store;
|
|
2102
|
+
this.log = opts.log ?? (() => {
|
|
2103
|
+
});
|
|
2104
|
+
this.insecure = opts.insecure ?? false;
|
|
2105
|
+
this.printerRef = opts.printer;
|
|
2106
|
+
}
|
|
2107
|
+
// --- construction / auth (HTTPS) ---
|
|
2108
|
+
static async login(opts) {
|
|
2109
|
+
const config = await loginAndBuildConfig(opts);
|
|
2110
|
+
const store = new ConfigStore();
|
|
2111
|
+
if (opts.save) store.save(config);
|
|
2112
|
+
return new _AnkerClient(config, { store });
|
|
2113
|
+
}
|
|
2114
|
+
static fromStoredConfig(path, opts = {}) {
|
|
2115
|
+
const store = opts.store ?? new ConfigStore(path);
|
|
2116
|
+
return new _AnkerClient(store.load(), { ...opts, store });
|
|
2117
|
+
}
|
|
2118
|
+
getConfig() {
|
|
2119
|
+
return this.config;
|
|
2120
|
+
}
|
|
2121
|
+
// --- account / selection ---
|
|
2122
|
+
listPrinters() {
|
|
2123
|
+
return this.config.printers;
|
|
2124
|
+
}
|
|
2125
|
+
selectPrinter(ref) {
|
|
2126
|
+
const printer = findPrinter(this.config, ref);
|
|
2127
|
+
if (!printer) throw this.notFound(ref);
|
|
2128
|
+
this.printerRef = printer.duid;
|
|
2129
|
+
this.config.selected = printer.duid;
|
|
2130
|
+
this.store?.update((c) => {
|
|
2131
|
+
c.selected = printer.duid;
|
|
2132
|
+
});
|
|
2133
|
+
return printer;
|
|
2134
|
+
}
|
|
2135
|
+
/** The currently selected printer (explicit ref → stored default → first). */
|
|
2136
|
+
currentPrinter() {
|
|
2137
|
+
if (this.config.printers.length === 0) {
|
|
2138
|
+
throw new PrinterNotFoundError({
|
|
2139
|
+
message: "No printers configured",
|
|
2140
|
+
hint: "Run `ankerts login --save` to populate the account's printer list."
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
const ref = this.printerRef ?? this.config.selected;
|
|
2144
|
+
if (ref !== void 0) {
|
|
2145
|
+
const p = findPrinter(this.config, ref);
|
|
2146
|
+
if (!p) throw this.notFound(ref);
|
|
2147
|
+
return p;
|
|
2148
|
+
}
|
|
2149
|
+
return this.config.printers[0];
|
|
2150
|
+
}
|
|
2151
|
+
notFound(ref) {
|
|
2152
|
+
return new PrinterNotFoundError({
|
|
2153
|
+
message: `Printer "${ref}" not found on the account`,
|
|
2154
|
+
hint: "List printers with `ankerts printer list`.",
|
|
2155
|
+
input: { printer: ref }
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
account() {
|
|
2159
|
+
if (!this.config.account) {
|
|
2160
|
+
throw new PrinterNotFoundError({
|
|
2161
|
+
code: "not_logged_in",
|
|
2162
|
+
message: "Not logged in",
|
|
2163
|
+
transport: "https",
|
|
2164
|
+
hint: "Run `ankerts login --email \u2026 --password \u2026 --country \u2026 --save`."
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
return this.config.account;
|
|
2168
|
+
}
|
|
2169
|
+
// --- MQTT lifecycle ---
|
|
2170
|
+
async ensureMqtt() {
|
|
2171
|
+
if (this.mqtt?.connected) return this.mqtt;
|
|
2172
|
+
const account = this.account();
|
|
2173
|
+
const printer = this.currentPrinter();
|
|
2174
|
+
this.mqtt = new AnkerMqttClient({
|
|
2175
|
+
sn: printer.sn,
|
|
2176
|
+
key: unhex(printer.mqtt_key),
|
|
2177
|
+
host: mqttHostFor(account, printer),
|
|
2178
|
+
username: mqttUsername(account),
|
|
2179
|
+
password: mqttPassword(account),
|
|
2180
|
+
insecure: this.insecure,
|
|
2181
|
+
log: this.log
|
|
2182
|
+
});
|
|
2183
|
+
await this.mqtt.connect();
|
|
2184
|
+
return this.mqtt;
|
|
2185
|
+
}
|
|
2186
|
+
/** Close any open transports. */
|
|
2187
|
+
async close() {
|
|
2188
|
+
await this.mqtt?.disconnect();
|
|
2189
|
+
this.mqtt = void 0;
|
|
2190
|
+
}
|
|
2191
|
+
// --- telemetry / status (MQTT) ---
|
|
2192
|
+
async getStatus() {
|
|
2193
|
+
const mqtt2 = await this.ensureMqtt();
|
|
2194
|
+
return mqtt2.getStatus();
|
|
2195
|
+
}
|
|
2196
|
+
async subscribeEvents(handler) {
|
|
2197
|
+
const mqtt2 = await this.ensureMqtt();
|
|
2198
|
+
return mqtt2.onNotice(handler);
|
|
2199
|
+
}
|
|
2200
|
+
// --- gcode (MQTT, §6) ---
|
|
2201
|
+
async gcode(command, opts = {}) {
|
|
2202
|
+
const mqtt2 = await this.ensureMqtt();
|
|
2203
|
+
const timeoutMs = opts.timeoutMs ?? defaultTimeoutFor(command);
|
|
2204
|
+
const result = await mqtt2.gcode(command, { timeoutMs, quietMs: opts.quietMs, wait: opts.wait });
|
|
2205
|
+
if (opts.waitMotion && opts.wait !== false) {
|
|
2206
|
+
const motion = await mqtt2.gcode("M400", { timeoutMs: defaultTimeoutFor("M400") });
|
|
2207
|
+
return {
|
|
2208
|
+
...result,
|
|
2209
|
+
raw: `${result.raw}
|
|
2210
|
+
${motion.raw}`,
|
|
2211
|
+
lines: [...result.lines, ...motion.lines],
|
|
2212
|
+
ok: motion.ok,
|
|
2213
|
+
timedOut: result.timedOut || motion.timedOut,
|
|
2214
|
+
frames: result.frames + motion.frames,
|
|
2215
|
+
durationMs: result.durationMs + motion.durationMs
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
return result;
|
|
2219
|
+
}
|
|
2220
|
+
/** Run many commands, yielding each result as it completes (NDJSON-friendly). */
|
|
2221
|
+
async *gcodeBatch(commands, opts = {}) {
|
|
2222
|
+
for (const command of commands) {
|
|
2223
|
+
if (command.trim() === "") continue;
|
|
2224
|
+
yield await this.gcode(command, opts);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
// --- state helpers ---
|
|
2228
|
+
async snapshotState() {
|
|
2229
|
+
const result = await this.gcode("M503", { timeoutMs: 15e3 });
|
|
2230
|
+
const reports = result.reports;
|
|
2231
|
+
const pid = reports.M301?.match(/P([\d.]+)\s+I([\d.]+)\s+D([\d.]+)/);
|
|
2232
|
+
const settings = { result, reports };
|
|
2233
|
+
if (reports.M900) {
|
|
2234
|
+
const k = reports.M900.match(/K([\d.]+)/);
|
|
2235
|
+
if (k) settings.linearAdvanceK = Number(k[1]);
|
|
2236
|
+
}
|
|
2237
|
+
if (pid) settings.hotendPid = { p: Number(pid[1]), i: Number(pid[2]), d: Number(pid[3]) };
|
|
2238
|
+
if (reports.M92) settings.steps = reports.M92;
|
|
2239
|
+
const z = reports.M851?.match(/Z(-?[\d.]+)/);
|
|
2240
|
+
if (z) settings.probeZOffset = Number(z[1]);
|
|
2241
|
+
return settings;
|
|
2242
|
+
}
|
|
2243
|
+
async restoreState() {
|
|
2244
|
+
return this.gcode("M501", { timeoutMs: 15e3 });
|
|
2245
|
+
}
|
|
2246
|
+
// --- job control (MQTT) ---
|
|
2247
|
+
//
|
|
2248
|
+
// PRINT_CONTROL (0x03f0) with the `value` codes in PrintControl, reverse-
|
|
2249
|
+
// engineered and confirmed live against an M5 (2026-06-07): cancel drops the
|
|
2250
|
+
// job to idle with heaters off; pause/resume toggle the print.
|
|
2251
|
+
async cancelJob() {
|
|
2252
|
+
(await this.ensureMqtt()).command({
|
|
2253
|
+
commandType: 1008 /* PRINT_CONTROL */,
|
|
2254
|
+
value: 4 /* STOP */
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
async pauseJob() {
|
|
2258
|
+
(await this.ensureMqtt()).command({
|
|
2259
|
+
commandType: 1008 /* PRINT_CONTROL */,
|
|
2260
|
+
value: 2 /* PAUSE */
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
async resumeJob() {
|
|
2264
|
+
(await this.ensureMqtt()).command({
|
|
2265
|
+
commandType: 1008 /* PRINT_CONTROL */,
|
|
2266
|
+
value: 3 /* RESUME */
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
// --- discovery + jobs (PPPP, LAN) ---
|
|
2270
|
+
async discoverLan(opts = {}) {
|
|
2271
|
+
const { retries = 3, timeoutMs = 1e3, store = false } = opts;
|
|
2272
|
+
let found = [];
|
|
2273
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
2274
|
+
this.log(`pppp: LAN discovery attempt ${attempt}/${retries}`);
|
|
2275
|
+
found = await discoverLan({ timeoutMs, log: this.log });
|
|
2276
|
+
if (found.length > 0) break;
|
|
2277
|
+
}
|
|
2278
|
+
if (store && found.length > 0) {
|
|
2279
|
+
this.config = this.persistDiscoveredIps(found);
|
|
2280
|
+
}
|
|
2281
|
+
return found;
|
|
2282
|
+
}
|
|
2283
|
+
persistDiscoveredIps(found) {
|
|
2284
|
+
const apply = (c) => {
|
|
2285
|
+
for (const f of found) {
|
|
2286
|
+
const p = c.printers.find((x) => x.duid === f.duid);
|
|
2287
|
+
if (p) p.ip_addr = f.ip;
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
if (this.store) return this.store.update(apply);
|
|
2291
|
+
apply(this.config);
|
|
2292
|
+
return this.config;
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Upload a gcode file over the LAN and (by default) start the print. Auto-runs
|
|
2296
|
+
* discovery if the printer's IP is unknown; if it's still unreachable, throws
|
|
2297
|
+
* {@link TransportUnavailableError} (exit 6) naming the transport and the fix.
|
|
2298
|
+
*/
|
|
2299
|
+
async uploadAndPrint(file, opts = {}) {
|
|
2300
|
+
let printer = this.currentPrinter();
|
|
2301
|
+
const filename = opts.filename ?? (typeof file === "string" ? basename(file) : "print.gcode");
|
|
2302
|
+
let data;
|
|
2303
|
+
if (typeof file === "string") {
|
|
2304
|
+
try {
|
|
2305
|
+
data = await readFile(file);
|
|
2306
|
+
} catch (cause) {
|
|
2307
|
+
throw new UsageError({
|
|
2308
|
+
code: "file_not_found",
|
|
2309
|
+
message: `Cannot read gcode file: ${file}`,
|
|
2310
|
+
hint: "Check the path. Provide a sliced .gcode file to upload.",
|
|
2311
|
+
input: { file },
|
|
2312
|
+
cause
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
} else {
|
|
2316
|
+
data = file;
|
|
2317
|
+
}
|
|
2318
|
+
if (opts.fixMetadata !== false) {
|
|
2319
|
+
const transcoded = transcodeMetadata(data.toString("utf8"));
|
|
2320
|
+
if (transcoded.changed) {
|
|
2321
|
+
this.log(`transcoder: injected Anker metadata (${JSON.stringify(transcoded.injected)})`);
|
|
2322
|
+
data = Buffer.from(transcoded.content, "utf8");
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (!printer.ip_addr) {
|
|
2326
|
+
this.log("pppp: no stored IP \u2014 running discovery");
|
|
2327
|
+
await this.discoverLan({ store: true });
|
|
2328
|
+
printer = this.currentPrinter();
|
|
2329
|
+
}
|
|
2330
|
+
if (!printer.ip_addr) {
|
|
2331
|
+
throw new TransportUnavailableError({
|
|
2332
|
+
code: "lan_printer_unreachable",
|
|
2333
|
+
message: `Printer ${printer.duid} not found on the local network`,
|
|
2334
|
+
transport: "pppp",
|
|
2335
|
+
hint: "File upload is LAN-only (PPPP). Run `ankerts discover --store` while on the same LAN as the printer, then retry.",
|
|
2336
|
+
input: { file: filename }
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
const pppp = new AnkerPpppClient({ duid: printer.duid, host: printer.ip_addr, log: this.log });
|
|
2340
|
+
try {
|
|
2341
|
+
await pppp.connect();
|
|
2342
|
+
const account = this.config.account;
|
|
2343
|
+
const res = await pppp.uploadFile(filename, data, {
|
|
2344
|
+
start: opts.start,
|
|
2345
|
+
userName: account?.email ?? "ankerts",
|
|
2346
|
+
userId: account?.user_id ?? "-",
|
|
2347
|
+
onProgress: opts.onProgress
|
|
2348
|
+
});
|
|
2349
|
+
return { ...res, transport: "lan", duid: printer.duid, ip: printer.ip_addr };
|
|
2350
|
+
} finally {
|
|
2351
|
+
pppp.stop();
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
// --- waiting (§6A) ---
|
|
2355
|
+
/**
|
|
2356
|
+
* Block until `cond` holds, resolving the current status. Re-attachable: it
|
|
2357
|
+
* re-derives state from fresh snapshots, so a re-issued wait still resolves.
|
|
2358
|
+
* Rejects with a retriable {@link TimeoutError} (exit 5) on timeout.
|
|
2359
|
+
*/
|
|
2360
|
+
async waitFor(cond, opts = {}) {
|
|
2361
|
+
const { pollMs = 2e3, timeoutMs = 6e5, onTick } = opts;
|
|
2362
|
+
const deadline = Date.now() + timeoutMs;
|
|
2363
|
+
if (cond.kind === "connected") {
|
|
2364
|
+
await this.ensureMqtt();
|
|
2365
|
+
return this.getStatus();
|
|
2366
|
+
}
|
|
2367
|
+
if (cond.kind === "lan") {
|
|
2368
|
+
const found = await this.discoverLan({ store: true, retries: Math.ceil(timeoutMs / 1e3) });
|
|
2369
|
+
if (found.length === 0) {
|
|
2370
|
+
throw new TimeoutError({
|
|
2371
|
+
message: "Printer not found on the local network within the timeout",
|
|
2372
|
+
transport: "pppp",
|
|
2373
|
+
hint: "Ensure you are on the same LAN as the printer and it is powered on."
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
return this.getStatus();
|
|
2377
|
+
}
|
|
2378
|
+
for (; ; ) {
|
|
2379
|
+
const status = await this.getStatus();
|
|
2380
|
+
onTick?.(status);
|
|
2381
|
+
if (conditionHolds(cond, status) === true) return status;
|
|
2382
|
+
if (Date.now() >= deadline) {
|
|
2383
|
+
throw new TimeoutError({
|
|
2384
|
+
message: `Condition "${cond.kind}" not met within ${timeoutMs}ms`,
|
|
2385
|
+
transport: "mqtt",
|
|
2386
|
+
hint: "Waits are re-attachable \u2014 re-run the same `printer wait` to continue.",
|
|
2387
|
+
input: { condition: cond.kind }
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
|
|
2395
|
+
// src/protocol/safety.ts
|
|
2396
|
+
var VOLATILE_SETTERS = /* @__PURE__ */ new Set([
|
|
2397
|
+
"M92",
|
|
2398
|
+
// steps/mm
|
|
2399
|
+
"M201",
|
|
2400
|
+
// max acceleration
|
|
2401
|
+
"M203",
|
|
2402
|
+
// max feedrate
|
|
2403
|
+
"M204",
|
|
2404
|
+
// accel for print/retract/travel
|
|
2405
|
+
"M301",
|
|
2406
|
+
// hotend PID
|
|
2407
|
+
"M304",
|
|
2408
|
+
// bed PID
|
|
2409
|
+
"M851",
|
|
2410
|
+
// probe Z offset
|
|
2411
|
+
"M900",
|
|
2412
|
+
// linear advance K
|
|
2413
|
+
"M906",
|
|
2414
|
+
// stepper current (TMC)
|
|
2415
|
+
"M913"
|
|
2416
|
+
// hybrid threshold (TMC)
|
|
2417
|
+
]);
|
|
2418
|
+
function gcodeCode(command) {
|
|
2419
|
+
const m = /^\s*([GM]\d+)/i.exec(command);
|
|
2420
|
+
return m ? m[1].toUpperCase() : null;
|
|
2421
|
+
}
|
|
2422
|
+
function inspectGcode(command) {
|
|
2423
|
+
const code = gcodeCode(command);
|
|
2424
|
+
const base = {
|
|
2425
|
+
code,
|
|
2426
|
+
mutatesState: false,
|
|
2427
|
+
volatile: false,
|
|
2428
|
+
persists: false,
|
|
2429
|
+
note: ""
|
|
2430
|
+
};
|
|
2431
|
+
if (!code) return base;
|
|
2432
|
+
switch (code) {
|
|
2433
|
+
case "M500":
|
|
2434
|
+
return {
|
|
2435
|
+
...base,
|
|
2436
|
+
mutatesState: true,
|
|
2437
|
+
persists: true,
|
|
2438
|
+
note: "M500 writes current settings to EEPROM \u2014 the change persists across power cycles."
|
|
2439
|
+
};
|
|
2440
|
+
case "M501":
|
|
2441
|
+
return {
|
|
2442
|
+
...base,
|
|
2443
|
+
mutatesState: true,
|
|
2444
|
+
note: "M501 reloads settings from EEPROM, discarding volatile (RAM) changes."
|
|
2445
|
+
};
|
|
2446
|
+
case "M502":
|
|
2447
|
+
return {
|
|
2448
|
+
...base,
|
|
2449
|
+
mutatesState: true,
|
|
2450
|
+
volatile: true,
|
|
2451
|
+
note: "M502 resets settings to firmware defaults in RAM (not saved until M500)."
|
|
2452
|
+
};
|
|
2453
|
+
default:
|
|
2454
|
+
if (VOLATILE_SETTERS.has(code)) {
|
|
2455
|
+
return {
|
|
2456
|
+
...base,
|
|
2457
|
+
mutatesState: true,
|
|
2458
|
+
volatile: true,
|
|
2459
|
+
note: `${code} changes a machine setting in volatile RAM \u2014 it persists until power-cycle and can contaminate the next print. M500 persists it; M501 reverts to the EEPROM value.`
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
return base;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
export {
|
|
2466
|
+
API_HOSTS,
|
|
2467
|
+
AnkerClient,
|
|
2468
|
+
AnkerError,
|
|
2469
|
+
AnkerHttpApi,
|
|
2470
|
+
AnkerMqttClient,
|
|
2471
|
+
AnkerPpppClient,
|
|
2472
|
+
AuthError,
|
|
2473
|
+
ConfigStore,
|
|
2474
|
+
MQTT_HOSTS,
|
|
2475
|
+
MqttCommandType,
|
|
2476
|
+
NoticeType,
|
|
2477
|
+
PPPP_LAN_PORT,
|
|
2478
|
+
PpppState,
|
|
2479
|
+
PrintControl,
|
|
2480
|
+
PrinterNotFoundError,
|
|
2481
|
+
PrinterRejectedError,
|
|
2482
|
+
REDACTED,
|
|
2483
|
+
TimeoutError,
|
|
2484
|
+
TransportUnavailableError,
|
|
2485
|
+
UsageError,
|
|
2486
|
+
conditionHolds,
|
|
2487
|
+
configDir,
|
|
2488
|
+
defaultTimeoutFor,
|
|
2489
|
+
describeWaitCondition,
|
|
2490
|
+
detectSlicer,
|
|
2491
|
+
discoverLan,
|
|
2492
|
+
findPrinter,
|
|
2493
|
+
gcodeCode,
|
|
2494
|
+
gcodeHasTerminalOk,
|
|
2495
|
+
guessRegion,
|
|
2496
|
+
hasAnkerTimeHeader,
|
|
2497
|
+
inspectGcode,
|
|
2498
|
+
isEtaReliable,
|
|
2499
|
+
loginAndBuildConfig,
|
|
2500
|
+
mqttHostFor,
|
|
2501
|
+
mqttPassword,
|
|
2502
|
+
mqttUsername,
|
|
2503
|
+
normalizeStatus,
|
|
2504
|
+
parseDurationToSeconds,
|
|
2505
|
+
parseGcodeResult,
|
|
2506
|
+
parseWaitCondition,
|
|
2507
|
+
reassembleRaw,
|
|
2508
|
+
redactConfig,
|
|
2509
|
+
splitLines,
|
|
2510
|
+
stripAnsi,
|
|
2511
|
+
toAnkerError,
|
|
2512
|
+
transcodeMetadata
|
|
2513
|
+
};
|
|
2514
|
+
//# sourceMappingURL=index.js.map
|