divoom-timesgate-sdk 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 +371 -0
- package/dist/common-D8oHDNi6.d.cts +54 -0
- package/dist/common-D8oHDNi6.d.ts +54 -0
- package/dist/image/index.cjs +515 -0
- package/dist/image/index.cjs.map +1 -0
- package/dist/image/index.d.cts +279 -0
- package/dist/image/index.d.ts +279 -0
- package/dist/image/index.js +498 -0
- package/dist/image/index.js.map +1 -0
- package/dist/index.cjs +1021 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +921 -0
- package/dist/index.d.ts +921 -0
- package/dist/index.js +991 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var DivoomError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "DivoomError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var DivoomValidationError = class extends DivoomError {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "DivoomValidationError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var DivoomConnectionError = class extends DivoomError {
|
|
15
|
+
constructor(message, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.name = "DivoomConnectionError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var DivoomTimeoutError = class extends DivoomError {
|
|
21
|
+
/** The command that timed out. */
|
|
22
|
+
command;
|
|
23
|
+
/** The timeout budget, in milliseconds, that was exceeded. */
|
|
24
|
+
timeoutMs;
|
|
25
|
+
constructor(command, timeoutMs) {
|
|
26
|
+
super(`Command "${command}" timed out after ${timeoutMs}ms.`);
|
|
27
|
+
this.name = "DivoomTimeoutError";
|
|
28
|
+
this.command = command;
|
|
29
|
+
this.timeoutMs = timeoutMs;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var DivoomHttpError = class extends DivoomError {
|
|
33
|
+
/** The HTTP status code returned by the device. */
|
|
34
|
+
status;
|
|
35
|
+
/** The command that produced the error. */
|
|
36
|
+
command;
|
|
37
|
+
/** The full request URL. */
|
|
38
|
+
url;
|
|
39
|
+
constructor(status, command, url) {
|
|
40
|
+
super(`Times Gate returned HTTP ${status} for command "${command}" (${url}).`);
|
|
41
|
+
this.name = "DivoomHttpError";
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.command = command;
|
|
44
|
+
this.url = url;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var DivoomCloudError = class extends DivoomError {
|
|
48
|
+
/** The non-zero `ReturnCode` reported by the cloud API. */
|
|
49
|
+
returnCode;
|
|
50
|
+
/** The cloud endpoint path that failed. */
|
|
51
|
+
endpoint;
|
|
52
|
+
/** The `ReturnMessage`, when provided. */
|
|
53
|
+
returnMessage;
|
|
54
|
+
constructor(returnCode, endpoint, returnMessage) {
|
|
55
|
+
super(
|
|
56
|
+
`Divoom cloud endpoint "${endpoint}" failed with ReturnCode ${returnCode}` + (returnMessage ? `: ${returnMessage}` : ".")
|
|
57
|
+
);
|
|
58
|
+
this.name = "DivoomCloudError";
|
|
59
|
+
this.returnCode = returnCode;
|
|
60
|
+
this.endpoint = endpoint;
|
|
61
|
+
this.returnMessage = returnMessage;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var DivoomDeviceError = class extends DivoomError {
|
|
65
|
+
/** The non-zero `error_code` reported by the device. */
|
|
66
|
+
errorCode;
|
|
67
|
+
/** The command that was rejected. */
|
|
68
|
+
command;
|
|
69
|
+
/** The raw response body returned by the device. */
|
|
70
|
+
response;
|
|
71
|
+
constructor(errorCode, command, response) {
|
|
72
|
+
super(
|
|
73
|
+
`Times Gate rejected command "${command}" with error_code ${errorCode}. This often means the LocalToken is missing or incorrect for a command that requires it.`
|
|
74
|
+
);
|
|
75
|
+
this.name = "DivoomDeviceError";
|
|
76
|
+
this.errorCode = errorCode;
|
|
77
|
+
this.command = command;
|
|
78
|
+
this.response = response;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/constants.ts
|
|
83
|
+
var PANEL_COUNT = 5;
|
|
84
|
+
var PANEL_SIZE = 128;
|
|
85
|
+
var DEFAULT_PORT = 80;
|
|
86
|
+
var DEFAULT_PATH = "/post";
|
|
87
|
+
var HARDWARE_402_PORT = 9e3;
|
|
88
|
+
var HARDWARE_402_PATH = "/divoom_api";
|
|
89
|
+
var LAN_DISCOVERY_URL = "https://app.divoom-gz.com/Device/ReturnSameLANDevice";
|
|
90
|
+
|
|
91
|
+
// src/utils.ts
|
|
92
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
async function drainBody(response) {
|
|
94
|
+
try {
|
|
95
|
+
await response.body?.cancel?.();
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertIntInRange(value, min, max, label) {
|
|
100
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
101
|
+
throw new DivoomValidationError(
|
|
102
|
+
`${label} must be an integer between ${min} and ${max} (received ${String(value)}).`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function assertPanelIndex(value) {
|
|
107
|
+
if (!Number.isInteger(value) || value < 0 || value >= PANEL_COUNT) {
|
|
108
|
+
throw new DivoomValidationError(
|
|
109
|
+
`panel must be an integer between 0 and ${PANEL_COUNT - 1} (received ${String(value)}).`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function assertNonEmptyString(value, label) {
|
|
114
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
115
|
+
throw new DivoomValidationError(`${label} must be a non-empty string.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function panelToLcdArray(panel) {
|
|
119
|
+
assertPanelIndex(panel);
|
|
120
|
+
const arr = [0, 0, 0, 0, 0];
|
|
121
|
+
arr[panel] = 1;
|
|
122
|
+
return arr;
|
|
123
|
+
}
|
|
124
|
+
function panelsToLcdArray(panels) {
|
|
125
|
+
const arr = [0, 0, 0, 0, 0];
|
|
126
|
+
for (const panel of panels) {
|
|
127
|
+
assertPanelIndex(panel);
|
|
128
|
+
arr[panel] = 1;
|
|
129
|
+
}
|
|
130
|
+
return arr;
|
|
131
|
+
}
|
|
132
|
+
var PicIdGenerator = class {
|
|
133
|
+
base;
|
|
134
|
+
constructor(seed = Math.floor(Date.now() / 1e3)) {
|
|
135
|
+
this.base = seed;
|
|
136
|
+
}
|
|
137
|
+
/** Returns the next unique PicID for the given panel. */
|
|
138
|
+
next(panel = 0) {
|
|
139
|
+
this.base += 10;
|
|
140
|
+
return this.base + panel;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/commands/animation.ts
|
|
145
|
+
var DIRECTION = { left: 0, right: 1 };
|
|
146
|
+
var ALIGN = { left: 1, center: 2, right: 3 };
|
|
147
|
+
var AnimationCommands = class {
|
|
148
|
+
constructor(transport) {
|
|
149
|
+
this.transport = transport;
|
|
150
|
+
}
|
|
151
|
+
transport;
|
|
152
|
+
/**
|
|
153
|
+
* Plays a stored Divoom GIF (by `FileId`) on the given panels
|
|
154
|
+
* (`Draw/SendRemote`). Obtain `fileId` from
|
|
155
|
+
* {@link DivoomCloudClient.getLikedImages} or
|
|
156
|
+
* {@link DivoomCloudClient.getUploadedImages}.
|
|
157
|
+
*/
|
|
158
|
+
async playStoredGif(panels, fileId) {
|
|
159
|
+
assertNonEmptyString(fileId, "fileId");
|
|
160
|
+
if (panels.length === 0) {
|
|
161
|
+
throw new DivoomValidationError("playStoredGif requires at least one panel.");
|
|
162
|
+
}
|
|
163
|
+
panels.forEach(assertPanelIndex);
|
|
164
|
+
return this.transport.send({
|
|
165
|
+
Command: "Draw/SendRemote",
|
|
166
|
+
FileId: fileId,
|
|
167
|
+
LcdArray: panelsToLcdArray(panels)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Plays one or more GIF URLs on the given panels (`Device/PlayGif`).
|
|
172
|
+
*
|
|
173
|
+
* @param urls - Up to 10 GIF URLs (dimensions 16/32/64/128).
|
|
174
|
+
*/
|
|
175
|
+
async playGifUrls(panels, urls) {
|
|
176
|
+
if (panels.length === 0) {
|
|
177
|
+
throw new DivoomValidationError("playGifUrls requires at least one panel.");
|
|
178
|
+
}
|
|
179
|
+
panels.forEach(assertPanelIndex);
|
|
180
|
+
if (urls.length === 0 || urls.length > 10) {
|
|
181
|
+
throw new DivoomValidationError(`playGifUrls accepts 1\u201310 URLs (received ${urls.length}).`);
|
|
182
|
+
}
|
|
183
|
+
return this.transport.send({
|
|
184
|
+
Command: "Device/PlayGif",
|
|
185
|
+
LcdArray: panelsToLcdArray(panels),
|
|
186
|
+
FileName: urls
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Plays a distinct list of GIF URLs on each panel (`Device/PlayGifLCDs`).
|
|
191
|
+
*
|
|
192
|
+
* @param perPanel - GIF URL lists keyed by panel index (0–4).
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* await client.animation.playGifPerPanel({
|
|
197
|
+
* 0: ['http://f.divoom-gz.com/Kirby.gif'],
|
|
198
|
+
* 1: ['http://f.divoom-gz.com/loz.gif'],
|
|
199
|
+
* });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
async playGifPerPanel(perPanel) {
|
|
203
|
+
const payload = { Command: "Device/PlayGifLCDs" };
|
|
204
|
+
let any = false;
|
|
205
|
+
for (let panel = 0; panel < 5; panel += 1) {
|
|
206
|
+
const urls = perPanel[panel];
|
|
207
|
+
if (urls && urls.length > 0) {
|
|
208
|
+
payload[`LCD${panel}GifFile`] = urls;
|
|
209
|
+
any = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!any) {
|
|
213
|
+
throw new DivoomValidationError("playGifPerPanel requires URLs for at least one panel.");
|
|
214
|
+
}
|
|
215
|
+
return this.transport.send(payload);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Overlays a single scrolling/aligned text string on a panel
|
|
219
|
+
* (`Draw/SendHttpText`).
|
|
220
|
+
*
|
|
221
|
+
* @param panel - Panel index 0–4.
|
|
222
|
+
* @param text - UTF-8 string (< 512 chars).
|
|
223
|
+
*/
|
|
224
|
+
async sendText(panel, text, options = {}) {
|
|
225
|
+
assertPanelIndex(panel);
|
|
226
|
+
assertNonEmptyString(text, "text");
|
|
227
|
+
if (options.textId !== void 0) assertIntInRange(options.textId, 0, 19, "textId");
|
|
228
|
+
if (options.font !== void 0) assertIntInRange(options.font, 0, 7, "font");
|
|
229
|
+
if (options.width !== void 0) assertIntInRange(options.width, 16, 64, "width");
|
|
230
|
+
return this.transport.send({
|
|
231
|
+
Command: "Draw/SendHttpText",
|
|
232
|
+
LcdIndex: panel,
|
|
233
|
+
TextId: options.textId ?? 1,
|
|
234
|
+
x: options.x ?? 0,
|
|
235
|
+
y: options.y ?? 40,
|
|
236
|
+
dir: DIRECTION[options.direction ?? "left"],
|
|
237
|
+
font: options.font ?? 4,
|
|
238
|
+
TextWidth: options.width ?? 56,
|
|
239
|
+
speed: options.speed ?? 100,
|
|
240
|
+
TextString: text,
|
|
241
|
+
color: options.color ?? "#FFFFFF",
|
|
242
|
+
align: ALIGN[options.align ?? "left"]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Composes a panel from a list of display items — static text, live data
|
|
247
|
+
* fields, and URL-backed text — over an optional background GIF
|
|
248
|
+
* (`Draw/SendHttpItemList`).
|
|
249
|
+
*
|
|
250
|
+
* @param panel - Panel index 0–4.
|
|
251
|
+
* @param items - The display elements to render.
|
|
252
|
+
*/
|
|
253
|
+
async sendItemList(panel, items, options = {}) {
|
|
254
|
+
assertPanelIndex(panel);
|
|
255
|
+
if (items.length === 0) {
|
|
256
|
+
throw new DivoomValidationError("sendItemList requires at least one item.");
|
|
257
|
+
}
|
|
258
|
+
const payload = {
|
|
259
|
+
Command: "Draw/SendHttpItemList",
|
|
260
|
+
LcdIndex: panel,
|
|
261
|
+
NewFlag: options.replace === false ? 0 : 1,
|
|
262
|
+
ItemList: items
|
|
263
|
+
};
|
|
264
|
+
if (options.backgroundGif !== void 0) {
|
|
265
|
+
payload.BackgroudGif = options.backgroundGif;
|
|
266
|
+
}
|
|
267
|
+
return this.transport.send(payload);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/commands/batch.ts
|
|
272
|
+
var BatchCommands = class {
|
|
273
|
+
constructor(transport) {
|
|
274
|
+
this.transport = transport;
|
|
275
|
+
}
|
|
276
|
+
transport;
|
|
277
|
+
/**
|
|
278
|
+
* Runs multiple commands in sequence in a single request (`Draw/CommandList`).
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts
|
|
282
|
+
* await client.batch.run([
|
|
283
|
+
* { Command: 'Channel/SetBrightness', Brightness: 100 },
|
|
284
|
+
* { Command: 'Channel/OnOffScreen', OnOff: 1 },
|
|
285
|
+
* ]);
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
run(commands) {
|
|
289
|
+
return this.transport.send({ Command: "Draw/CommandList", CommandList: commands });
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Tells the device to fetch and execute a command-array file from a URL
|
|
293
|
+
* (`Draw/UseHTTPCommandSource`).
|
|
294
|
+
*/
|
|
295
|
+
async useCommandSource(commandUrl) {
|
|
296
|
+
assertNonEmptyString(commandUrl, "commandUrl");
|
|
297
|
+
return this.transport.send({ Command: "Draw/UseHTTPCommandSource", CommandUrl: commandUrl });
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/commands/dial.ts
|
|
302
|
+
var DialCommands = class {
|
|
303
|
+
constructor(transport) {
|
|
304
|
+
this.transport = transport;
|
|
305
|
+
}
|
|
306
|
+
transport;
|
|
307
|
+
/**
|
|
308
|
+
* Selects a "whole" dial that spans all five panels
|
|
309
|
+
* (`Channel/Set5LcdWholeClockId`).
|
|
310
|
+
*
|
|
311
|
+
* @param clockId - A whole-dial id from
|
|
312
|
+
* {@link DivoomCloudClient.getWholeDialList}.
|
|
313
|
+
*/
|
|
314
|
+
selectWholeDial(clockId) {
|
|
315
|
+
return this.transport.send({ Command: "Channel/Set5LcdWholeClockId", ClockId: clockId });
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Switches between whole and independent dial modes
|
|
319
|
+
* (`Channel/Set5LcdChannelType`).
|
|
320
|
+
*
|
|
321
|
+
* @param mode - `"whole"` (one dial across all panels) or `"independent"`.
|
|
322
|
+
* @param lcdIndependence - Required for `"independent"` mode; the group id from
|
|
323
|
+
* {@link DivoomCloudClient.getChannelInfo}.
|
|
324
|
+
*/
|
|
325
|
+
async setChannelMode(mode, lcdIndependence) {
|
|
326
|
+
if (mode === "independent" && lcdIndependence === void 0) {
|
|
327
|
+
throw new DivoomValidationError(
|
|
328
|
+
"setChannelMode('independent') requires an lcdIndependence id (from DivoomCloudClient.getChannelInfo)."
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const payload = {
|
|
332
|
+
Command: "Channel/Set5LcdChannelType",
|
|
333
|
+
ChannelType: mode === "independent" ? 1 : 0
|
|
334
|
+
};
|
|
335
|
+
if (lcdIndependence !== void 0) {
|
|
336
|
+
payload.LcdIndependence = lcdIndependence;
|
|
337
|
+
}
|
|
338
|
+
return this.transport.send(payload);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Selects a sub-dial for a single panel (`Channel/SetClockSelectId`).
|
|
342
|
+
*
|
|
343
|
+
* @param panel - Panel index 0–4.
|
|
344
|
+
* @param clockId - A sub-dial id from {@link DivoomCloudClient.getDialList}.
|
|
345
|
+
* @param lcdIndependence - The group id from {@link DivoomCloudClient.getChannelInfo}.
|
|
346
|
+
*/
|
|
347
|
+
async selectSubDial(panel, clockId, lcdIndependence) {
|
|
348
|
+
assertPanelIndex(panel);
|
|
349
|
+
return this.transport.send({
|
|
350
|
+
Command: "Channel/SetClockSelectId",
|
|
351
|
+
ClockId: clockId,
|
|
352
|
+
LcdIndex: panel,
|
|
353
|
+
LcdIndependence: lcdIndependence
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Selects a sub-visualizer (EQ) component for a panel
|
|
358
|
+
* (`Channel/SetEqPosition`).
|
|
359
|
+
*
|
|
360
|
+
* @param panel - Panel index 0–4.
|
|
361
|
+
* @param eqPosition - Visualizer index, starting at 0.
|
|
362
|
+
* @param lcdIndependence - The group id from {@link DivoomCloudClient.getChannelInfo}.
|
|
363
|
+
*/
|
|
364
|
+
async selectVisualizer(panel, eqPosition, lcdIndependence) {
|
|
365
|
+
assertPanelIndex(panel);
|
|
366
|
+
return this.transport.send({
|
|
367
|
+
Command: "Channel/SetEqPosition",
|
|
368
|
+
EqPosition: eqPosition,
|
|
369
|
+
LcdIndex: panel,
|
|
370
|
+
LcdIndependence: lcdIndependence
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/** Reads the currently-selected channel index (`Channel/GetIndex`). */
|
|
374
|
+
getIndex() {
|
|
375
|
+
return this.transport.send({ Command: "Channel/GetIndex" });
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/commands/draw.ts
|
|
380
|
+
var MAX_FRAMES = 59;
|
|
381
|
+
var DrawCommands = class {
|
|
382
|
+
constructor(transport, picIdSeed) {
|
|
383
|
+
this.transport = transport;
|
|
384
|
+
this.picIds = new PicIdGenerator(picIdSeed);
|
|
385
|
+
}
|
|
386
|
+
transport;
|
|
387
|
+
picIds;
|
|
388
|
+
/**
|
|
389
|
+
* Pushes a single still image to one panel.
|
|
390
|
+
*
|
|
391
|
+
* @param panel - Panel index 0–4.
|
|
392
|
+
* @param picData - Base64-encoded JPEG/PNG (e.g. {@link EncodedFrame.data}).
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```ts
|
|
396
|
+
* import { encodePanel } from 'divoom-timesgate-sdk/image';
|
|
397
|
+
* const frame = await encodePanel('./cover.jpg');
|
|
398
|
+
* await client.draw.sendImage(0, frame.data);
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
async sendImage(panel, picData, options = {}) {
|
|
402
|
+
assertPanelIndex(panel);
|
|
403
|
+
return this.pushStill(panelToLcdArray(panel), picData, panel, options);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Pushes the same still image to several panels at once.
|
|
407
|
+
*
|
|
408
|
+
* @param panels - Panel indices to target.
|
|
409
|
+
* @param picData - Base64-encoded JPEG/PNG.
|
|
410
|
+
*/
|
|
411
|
+
async sendImageToPanels(panels, picData, options = {}) {
|
|
412
|
+
if (panels.length === 0) {
|
|
413
|
+
throw new DivoomValidationError("sendImageToPanels requires at least one panel.");
|
|
414
|
+
}
|
|
415
|
+
panels.forEach(assertPanelIndex);
|
|
416
|
+
return this.pushStill(panelsToLcdArray(panels), picData, panels[0] ?? 0, options);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Pushes a multi-frame animation to one or more panels. Each frame is sent as
|
|
420
|
+
* a separate `Draw/SendHttpGif` request sharing one PicID, per the protocol.
|
|
421
|
+
*
|
|
422
|
+
* @param panels - Panel indices to target.
|
|
423
|
+
* @param frames - Base64-encoded frames, in order (1–59 frames).
|
|
424
|
+
* @returns One response per frame.
|
|
425
|
+
*/
|
|
426
|
+
async sendAnimation(panels, frames, options = {}) {
|
|
427
|
+
if (panels.length === 0) {
|
|
428
|
+
throw new DivoomValidationError("sendAnimation requires at least one panel.");
|
|
429
|
+
}
|
|
430
|
+
panels.forEach(assertPanelIndex);
|
|
431
|
+
if (frames.length === 0) {
|
|
432
|
+
throw new DivoomValidationError("sendAnimation requires at least one frame.");
|
|
433
|
+
}
|
|
434
|
+
if (frames.length > MAX_FRAMES) {
|
|
435
|
+
throw new DivoomValidationError(
|
|
436
|
+
`An animation may contain at most ${MAX_FRAMES} frames (received ${frames.length}).`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const lcdArray = panelsToLcdArray(panels);
|
|
440
|
+
const picId = options.picId ?? this.picIds.next(panels[0] ?? 0);
|
|
441
|
+
const picWidth = options.picWidth ?? PANEL_SIZE;
|
|
442
|
+
const picSpeed = options.picSpeed ?? 100;
|
|
443
|
+
const responses = [];
|
|
444
|
+
for (let offset = 0; offset < frames.length; offset += 1) {
|
|
445
|
+
responses.push(
|
|
446
|
+
await this.sendFrame({
|
|
447
|
+
LcdArray: lcdArray,
|
|
448
|
+
PicNum: frames.length,
|
|
449
|
+
PicWidth: picWidth,
|
|
450
|
+
PicOffset: offset,
|
|
451
|
+
PicID: picId,
|
|
452
|
+
PicSpeed: picSpeed,
|
|
453
|
+
PicData: frames[offset] ?? ""
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return responses;
|
|
458
|
+
}
|
|
459
|
+
/** Sends a fully-specified `Draw/SendHttpGif` frame — maximum control. */
|
|
460
|
+
sendFrame(request) {
|
|
461
|
+
return this.transport.send({ Command: "Draw/SendHttpGif", ...request });
|
|
462
|
+
}
|
|
463
|
+
/** Clears the device's image cache and resets the PicID counter (`Draw/ResetHttpGifId`). */
|
|
464
|
+
resetCache() {
|
|
465
|
+
return this.transport.send({ Command: "Draw/ResetHttpGifId" });
|
|
466
|
+
}
|
|
467
|
+
/** Reads the device's current PicID (`Draw/GetHttpGifId`). */
|
|
468
|
+
async getPicId() {
|
|
469
|
+
const response = await this.transport.send({ Command: "Draw/GetHttpGifId" });
|
|
470
|
+
if (typeof response.PicId !== "number") {
|
|
471
|
+
throw new DivoomError("Draw/GetHttpGifId did not return a numeric PicId.");
|
|
472
|
+
}
|
|
473
|
+
return response.PicId;
|
|
474
|
+
}
|
|
475
|
+
/** Returns the next locally-generated, strictly-increasing PicID. */
|
|
476
|
+
nextPicId(panel = 0) {
|
|
477
|
+
return this.picIds.next(panel);
|
|
478
|
+
}
|
|
479
|
+
pushStill(lcdArray, picData, offsetPanel, options) {
|
|
480
|
+
return this.sendFrame({
|
|
481
|
+
LcdArray: lcdArray,
|
|
482
|
+
PicNum: 1,
|
|
483
|
+
PicWidth: options.picWidth ?? PANEL_SIZE,
|
|
484
|
+
PicOffset: 0,
|
|
485
|
+
PicID: options.picId ?? this.picIds.next(offsetPanel),
|
|
486
|
+
PicSpeed: options.picSpeed ?? 1e3,
|
|
487
|
+
PicData: picData
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/commands/system.ts
|
|
493
|
+
var SystemCommands = class {
|
|
494
|
+
constructor(transport) {
|
|
495
|
+
this.transport = transport;
|
|
496
|
+
}
|
|
497
|
+
transport;
|
|
498
|
+
/** Sets the LCD brightness (`Channel/SetBrightness`). @param brightness 0–100. */
|
|
499
|
+
async setBrightness(brightness) {
|
|
500
|
+
assertIntInRange(brightness, 0, 100, "brightness");
|
|
501
|
+
return this.transport.send({ Command: "Channel/SetBrightness", Brightness: brightness });
|
|
502
|
+
}
|
|
503
|
+
/** Reads every device setting (`Channel/GetAllConf`). */
|
|
504
|
+
getAllConfig() {
|
|
505
|
+
return this.transport.send({ Command: "Channel/GetAllConf" });
|
|
506
|
+
}
|
|
507
|
+
/** Turns the screen on or off (`Channel/OnOffScreen`). */
|
|
508
|
+
setScreen(on) {
|
|
509
|
+
return this.transport.send({ Command: "Channel/OnOffScreen", OnOff: on ? 1 : 0 });
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Sets the weather location (`Sys/LogAndLat`). Coordinates drive the on-device
|
|
513
|
+
* weather readout.
|
|
514
|
+
*/
|
|
515
|
+
setWeatherLocation(longitude, latitude) {
|
|
516
|
+
return this.transport.send({
|
|
517
|
+
Command: "Sys/LogAndLat",
|
|
518
|
+
Longitude: String(longitude),
|
|
519
|
+
Latitude: String(latitude)
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/** Sets the time zone (`Sys/TimeZone`). @param timeZone e.g. `"GMT-5"`. */
|
|
523
|
+
async setTimeZone(timeZone) {
|
|
524
|
+
assertNonEmptyString(timeZone, "timeZone");
|
|
525
|
+
return this.transport.send({ Command: "Sys/TimeZone", TimeZoneValue: timeZone });
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Sets the system clock (`Device/SetUTC`).
|
|
529
|
+
*
|
|
530
|
+
* @param time - A `Date` or a UTC epoch in **seconds**.
|
|
531
|
+
*/
|
|
532
|
+
async setSystemTime(time) {
|
|
533
|
+
const utc = time instanceof Date ? Math.floor(time.getTime() / 1e3) : Math.floor(time);
|
|
534
|
+
if (!Number.isFinite(utc)) {
|
|
535
|
+
throw new DivoomValidationError(
|
|
536
|
+
"setSystemTime requires a valid Date or a finite epoch-seconds number."
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
return this.transport.send({ Command: "Device/SetUTC", Utc: utc });
|
|
540
|
+
}
|
|
541
|
+
/** Reads the device's UTC and local time (`Device/GetDeviceTime`). */
|
|
542
|
+
getDeviceTime() {
|
|
543
|
+
return this.transport.send({ Command: "Device/GetDeviceTime" });
|
|
544
|
+
}
|
|
545
|
+
/** Sets the temperature unit (`Device/SetDisTempMode`). */
|
|
546
|
+
setTemperatureMode(unit) {
|
|
547
|
+
return this.transport.send({
|
|
548
|
+
Command: "Device/SetDisTempMode",
|
|
549
|
+
Mode: unit === "fahrenheit" ? 1 : 0
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/** Enables or disables mirror mode (`Device/SetMirrorMode`). */
|
|
553
|
+
setMirrorMode(enabled) {
|
|
554
|
+
return this.transport.send({ Command: "Device/SetMirrorMode", Mode: enabled ? 1 : 0 });
|
|
555
|
+
}
|
|
556
|
+
/** Selects 24-hour (`true`) or 12-hour (`false`) clock (`Device/SetTime24Flag`). */
|
|
557
|
+
setHourMode(use24Hour) {
|
|
558
|
+
return this.transport.send({ Command: "Device/SetTime24Flag", Mode: use24Hour ? 1 : 0 });
|
|
559
|
+
}
|
|
560
|
+
/** Reads the current on-device weather (`Device/GetWeatherInfo`). */
|
|
561
|
+
getWeather() {
|
|
562
|
+
return this.transport.send({ Command: "Device/GetWeatherInfo" });
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// src/commands/tool.ts
|
|
567
|
+
var STOPWATCH_STATUS = { stop: 0, start: 1, reset: 2 };
|
|
568
|
+
var ToolCommands = class {
|
|
569
|
+
constructor(transport) {
|
|
570
|
+
this.transport = transport;
|
|
571
|
+
}
|
|
572
|
+
transport;
|
|
573
|
+
/**
|
|
574
|
+
* Starts or stops a countdown timer (`Tools/SetTimer`).
|
|
575
|
+
*
|
|
576
|
+
* @param minutes - Countdown minutes (0–99).
|
|
577
|
+
* @param seconds - Countdown seconds (0–59).
|
|
578
|
+
* @param start - `true` to start, `false` to stop. Defaults to `true`.
|
|
579
|
+
*/
|
|
580
|
+
async setCountdown(minutes, seconds, start = true) {
|
|
581
|
+
assertIntInRange(minutes, 0, 99, "minutes");
|
|
582
|
+
assertIntInRange(seconds, 0, 59, "seconds");
|
|
583
|
+
return this.transport.send({
|
|
584
|
+
Command: "Tools/SetTimer",
|
|
585
|
+
Minute: minutes,
|
|
586
|
+
Second: seconds,
|
|
587
|
+
Status: start ? 1 : 0
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** Controls the stopwatch (`Tools/SetStopWatch`). */
|
|
591
|
+
setStopwatch(action) {
|
|
592
|
+
return this.transport.send({ Command: "Tools/SetStopWatch", Status: STOPWATCH_STATUS[action] });
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Sets the scoreboard (`Tools/SetScoreBoard`).
|
|
596
|
+
*
|
|
597
|
+
* @param redScore - Red team score (0–999).
|
|
598
|
+
* @param blueScore - Blue team score (0–999).
|
|
599
|
+
*/
|
|
600
|
+
async setScoreboard(redScore, blueScore) {
|
|
601
|
+
assertIntInRange(redScore, 0, 999, "redScore");
|
|
602
|
+
assertIntInRange(blueScore, 0, 999, "blueScore");
|
|
603
|
+
return this.transport.send({
|
|
604
|
+
Command: "Tools/SetScoreBoard",
|
|
605
|
+
RedScore: redScore,
|
|
606
|
+
BlueScore: blueScore
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
/** Starts or stops the noise meter (`Tools/SetNoiseStatus`). */
|
|
610
|
+
setNoiseMeter(start) {
|
|
611
|
+
return this.transport.send({ Command: "Tools/SetNoiseStatus", NoiseStatus: start ? 1 : 0 });
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Plays the buzzer (`Device/PlayBuzzer`). Requires firmware ≥ 90109.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```ts
|
|
618
|
+
* await client.tool.playBuzzer({ activeMs: 200, offMs: 100, totalMs: 900 });
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
async playBuzzer(options = {}) {
|
|
622
|
+
const activeMs = options.activeMs ?? 500;
|
|
623
|
+
const offMs = options.offMs ?? 500;
|
|
624
|
+
const totalMs = options.totalMs ?? 1e3;
|
|
625
|
+
assertIntInRange(activeMs, 0, 36e5, "activeMs");
|
|
626
|
+
assertIntInRange(offMs, 0, 36e5, "offMs");
|
|
627
|
+
assertIntInRange(totalMs, 0, 36e5, "totalMs");
|
|
628
|
+
return this.transport.send({
|
|
629
|
+
Command: "Device/PlayBuzzer",
|
|
630
|
+
ActiveTimeInCycle: activeMs,
|
|
631
|
+
OffTimeInCycle: offMs,
|
|
632
|
+
PlayTotalTime: totalMs
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/transport.ts
|
|
638
|
+
var DEFAULTS = {
|
|
639
|
+
port: DEFAULT_PORT,
|
|
640
|
+
path: DEFAULT_PATH,
|
|
641
|
+
protocol: "http",
|
|
642
|
+
localToken: 0,
|
|
643
|
+
timeoutMs: 8e3,
|
|
644
|
+
retries: 2,
|
|
645
|
+
retryDelayMs: 300
|
|
646
|
+
};
|
|
647
|
+
var HttpTransport = class {
|
|
648
|
+
/** The fully-resolved endpoint URL every request is POSTed to. */
|
|
649
|
+
endpoint;
|
|
650
|
+
localToken;
|
|
651
|
+
timeoutMs;
|
|
652
|
+
retries;
|
|
653
|
+
retryDelayMs;
|
|
654
|
+
fetchImpl;
|
|
655
|
+
headers;
|
|
656
|
+
constructor(options) {
|
|
657
|
+
const host = options.host?.trim();
|
|
658
|
+
if (!host) {
|
|
659
|
+
throw new DivoomValidationError("A device `host` (IP address or hostname) is required.");
|
|
660
|
+
}
|
|
661
|
+
if (/[\s/\\?#@]/.test(host)) {
|
|
662
|
+
throw new DivoomValidationError(
|
|
663
|
+
`Invalid device host "${host}": expected a bare IP address or hostname.`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const port = options.port ?? DEFAULTS.port;
|
|
667
|
+
const rawPath = options.path ?? DEFAULTS.path;
|
|
668
|
+
const path = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
669
|
+
const protocol = options.protocol ?? DEFAULTS.protocol;
|
|
670
|
+
this.endpoint = `${protocol}://${host}:${port}${path}`;
|
|
671
|
+
this.localToken = options.localToken ?? DEFAULTS.localToken;
|
|
672
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;
|
|
673
|
+
this.retries = Math.max(0, options.retries ?? DEFAULTS.retries);
|
|
674
|
+
this.retryDelayMs = options.retryDelayMs ?? DEFAULTS.retryDelayMs;
|
|
675
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
676
|
+
this.headers = { "Content-Type": "application/json", ...options.headers };
|
|
677
|
+
if (typeof this.fetchImpl !== "function") {
|
|
678
|
+
throw new DivoomValidationError(
|
|
679
|
+
"No `fetch` implementation is available. Use Node 18+ or pass a custom `fetch`."
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Sends a command payload to the device and returns the parsed response.
|
|
685
|
+
*
|
|
686
|
+
* The configured `LocalToken` is merged in automatically (an explicit
|
|
687
|
+
* `LocalToken` on the payload wins). Transient network errors, timeouts, and
|
|
688
|
+
* 5xx responses are retried with exponential backoff; device-level errors and
|
|
689
|
+
* 4xx responses are thrown immediately.
|
|
690
|
+
*
|
|
691
|
+
* @typeParam TResponse - The expected response shape.
|
|
692
|
+
* @throws {@link DivoomTimeoutError} when the request exceeds the timeout.
|
|
693
|
+
* @throws {@link DivoomHttpError} on a non-2xx HTTP status.
|
|
694
|
+
* @throws {@link DivoomDeviceError} when the device reports a non-zero `error_code`.
|
|
695
|
+
* @throws {@link DivoomConnectionError} when the device cannot be reached.
|
|
696
|
+
*/
|
|
697
|
+
async send(payload) {
|
|
698
|
+
const body = JSON.stringify({ LocalToken: this.localToken, ...payload });
|
|
699
|
+
let lastError;
|
|
700
|
+
for (let attempt = 0; attempt <= this.retries; attempt += 1) {
|
|
701
|
+
if (attempt > 0) {
|
|
702
|
+
await delay(this.retryDelayMs * 2 ** (attempt - 1));
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
return await this.attempt(body, payload.Command);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
lastError = error;
|
|
708
|
+
if (!isRetryable(error)) {
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
throw lastError;
|
|
714
|
+
}
|
|
715
|
+
async attempt(body, command) {
|
|
716
|
+
const controller = new AbortController();
|
|
717
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
718
|
+
try {
|
|
719
|
+
let response;
|
|
720
|
+
try {
|
|
721
|
+
response = await this.fetchImpl(this.endpoint, {
|
|
722
|
+
method: "POST",
|
|
723
|
+
headers: this.headers,
|
|
724
|
+
body,
|
|
725
|
+
signal: controller.signal
|
|
726
|
+
});
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (controller.signal.aborted) {
|
|
729
|
+
throw new DivoomTimeoutError(command, this.timeoutMs);
|
|
730
|
+
}
|
|
731
|
+
throw new DivoomConnectionError(`Failed to reach Times Gate at ${this.endpoint}.`, {
|
|
732
|
+
cause: error
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (!response.ok) {
|
|
736
|
+
await drainBody(response);
|
|
737
|
+
throw new DivoomHttpError(response.status, command, this.endpoint);
|
|
738
|
+
}
|
|
739
|
+
let text;
|
|
740
|
+
try {
|
|
741
|
+
text = await response.text();
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (controller.signal.aborted) {
|
|
744
|
+
throw new DivoomTimeoutError(command, this.timeoutMs);
|
|
745
|
+
}
|
|
746
|
+
throw new DivoomConnectionError(
|
|
747
|
+
`Failed to read the response body from Times Gate for command "${command}".`,
|
|
748
|
+
{ cause: error }
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const data = parseDeviceBody(text, command);
|
|
752
|
+
if (typeof data.error_code === "number" && data.error_code !== 0) {
|
|
753
|
+
throw new DivoomDeviceError(data.error_code, command, data);
|
|
754
|
+
}
|
|
755
|
+
return data;
|
|
756
|
+
} finally {
|
|
757
|
+
clearTimeout(timer);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
function parseDeviceBody(text, command) {
|
|
762
|
+
if (text.trim() === "") {
|
|
763
|
+
return { error_code: 0 };
|
|
764
|
+
}
|
|
765
|
+
let parsed;
|
|
766
|
+
try {
|
|
767
|
+
parsed = JSON.parse(text);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
throw new DivoomConnectionError(
|
|
770
|
+
`Times Gate returned a malformed (non-JSON) response for command "${command}".`,
|
|
771
|
+
{ cause: error }
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
775
|
+
throw new DivoomConnectionError(
|
|
776
|
+
`Times Gate returned an unexpected (non-object) response for command "${command}".`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
return parsed;
|
|
780
|
+
}
|
|
781
|
+
function isRetryable(error) {
|
|
782
|
+
if (error instanceof DivoomTimeoutError || error instanceof DivoomConnectionError) {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
if (error instanceof DivoomHttpError) {
|
|
786
|
+
return error.status >= 500;
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/client.ts
|
|
792
|
+
var TimesGateClient = class {
|
|
793
|
+
/** The underlying HTTP transport. */
|
|
794
|
+
transport;
|
|
795
|
+
/** System settings: brightness, screen, time, weather, display modes. */
|
|
796
|
+
system;
|
|
797
|
+
/** Dial/channel control: whole vs independent dials, sub-dials, visualizers. */
|
|
798
|
+
dial;
|
|
799
|
+
/** Built-in tools: countdown, stopwatch, scoreboard, noise meter, buzzer. */
|
|
800
|
+
tool;
|
|
801
|
+
/** The image pipeline: push frames/animations, manage the device cache. */
|
|
802
|
+
draw;
|
|
803
|
+
/** Animations & text: stored GIFs, GIF URLs, text overlays, item lists. */
|
|
804
|
+
animation;
|
|
805
|
+
/** Batch execution: run multiple commands or a remote command list. */
|
|
806
|
+
batch;
|
|
807
|
+
constructor(options) {
|
|
808
|
+
this.transport = new HttpTransport(options);
|
|
809
|
+
this.system = new SystemCommands(this.transport);
|
|
810
|
+
this.dial = new DialCommands(this.transport);
|
|
811
|
+
this.tool = new ToolCommands(this.transport);
|
|
812
|
+
this.draw = new DrawCommands(this.transport, options.picIdSeed);
|
|
813
|
+
this.animation = new AnimationCommands(this.transport);
|
|
814
|
+
this.batch = new BatchCommands(this.transport);
|
|
815
|
+
}
|
|
816
|
+
/** The resolved endpoint URL this client talks to. */
|
|
817
|
+
get endpoint() {
|
|
818
|
+
return this.transport.endpoint;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Sends a raw command payload — an escape hatch for commands the typed API
|
|
822
|
+
* doesn't cover yet. The configured `LocalToken` is still injected.
|
|
823
|
+
*/
|
|
824
|
+
send(payload) {
|
|
825
|
+
return this.transport.send(payload);
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Checks whether the device is reachable by issuing `Channel/GetIndex`.
|
|
829
|
+
* Returns `false` instead of throwing on failure.
|
|
830
|
+
*/
|
|
831
|
+
async ping() {
|
|
832
|
+
try {
|
|
833
|
+
await this.dial.getIndex();
|
|
834
|
+
return true;
|
|
835
|
+
} catch {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// src/cloud.ts
|
|
842
|
+
var DEFAULT_CLOUD_BASE_URL = "https://app.divoom-gz.com";
|
|
843
|
+
function parseCloudBody(text, url) {
|
|
844
|
+
if (text.trim() === "") {
|
|
845
|
+
return { ReturnCode: 0 };
|
|
846
|
+
}
|
|
847
|
+
let parsed;
|
|
848
|
+
try {
|
|
849
|
+
parsed = JSON.parse(text);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
throw new DivoomConnectionError(
|
|
852
|
+
`Cloud endpoint ${url} returned a malformed (non-JSON) response.`,
|
|
853
|
+
{
|
|
854
|
+
cause: error
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
859
|
+
throw new DivoomConnectionError(
|
|
860
|
+
`Cloud endpoint ${url} returned an unexpected (non-object) response.`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
return parsed;
|
|
864
|
+
}
|
|
865
|
+
async function cloudPost(url, body, options = {}) {
|
|
866
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
867
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
868
|
+
const retries = Math.max(0, options.retries ?? 2);
|
|
869
|
+
const retryDelayMs = options.retryDelayMs ?? 300;
|
|
870
|
+
const headers = { "Content-Type": "application/json", ...options.headers };
|
|
871
|
+
const payload = JSON.stringify(body);
|
|
872
|
+
let lastError;
|
|
873
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
874
|
+
if (attempt > 0) await delay(retryDelayMs * 2 ** (attempt - 1));
|
|
875
|
+
const controller = new AbortController();
|
|
876
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
877
|
+
let response;
|
|
878
|
+
try {
|
|
879
|
+
response = await fetchImpl(url, {
|
|
880
|
+
method: "POST",
|
|
881
|
+
headers,
|
|
882
|
+
body: payload,
|
|
883
|
+
signal: controller.signal
|
|
884
|
+
});
|
|
885
|
+
} catch (error) {
|
|
886
|
+
lastError = controller.signal.aborted ? new DivoomTimeoutError(url, timeoutMs) : new DivoomConnectionError(`Failed to reach cloud endpoint ${url}.`, { cause: error });
|
|
887
|
+
continue;
|
|
888
|
+
} finally {
|
|
889
|
+
clearTimeout(timer);
|
|
890
|
+
}
|
|
891
|
+
if (!response.ok) {
|
|
892
|
+
await drainBody(response);
|
|
893
|
+
const httpError = new DivoomConnectionError(
|
|
894
|
+
`Cloud request to ${url} failed: HTTP ${response.status}.`
|
|
895
|
+
);
|
|
896
|
+
if (response.status < 500) throw httpError;
|
|
897
|
+
lastError = httpError;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
let text;
|
|
901
|
+
try {
|
|
902
|
+
text = await response.text();
|
|
903
|
+
} catch (error) {
|
|
904
|
+
lastError = new DivoomConnectionError(`Failed to read cloud response body from ${url}.`, {
|
|
905
|
+
cause: error
|
|
906
|
+
});
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
let data;
|
|
910
|
+
try {
|
|
911
|
+
data = parseCloudBody(text, url);
|
|
912
|
+
} catch (error) {
|
|
913
|
+
lastError = error;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (typeof data.ReturnCode === "number" && data.ReturnCode !== 0) {
|
|
917
|
+
throw new DivoomCloudError(data.ReturnCode, url, data.ReturnMessage);
|
|
918
|
+
}
|
|
919
|
+
return data;
|
|
920
|
+
}
|
|
921
|
+
throw lastError instanceof Error ? lastError : new DivoomConnectionError(`Cloud request to ${url} failed.`);
|
|
922
|
+
}
|
|
923
|
+
var DivoomCloudClient = class {
|
|
924
|
+
baseUrl;
|
|
925
|
+
options;
|
|
926
|
+
constructor(options = {}) {
|
|
927
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_CLOUD_BASE_URL).replace(/\/$/, "");
|
|
928
|
+
this.options = options;
|
|
929
|
+
}
|
|
930
|
+
post(path, body = {}) {
|
|
931
|
+
return cloudPost(`${this.baseUrl}${path}`, body, this.options);
|
|
932
|
+
}
|
|
933
|
+
/** Lists sub-dial category names (`Channel/GetDialType`). */
|
|
934
|
+
getDialTypes() {
|
|
935
|
+
return this.post("/Channel/GetDialType");
|
|
936
|
+
}
|
|
937
|
+
/** Lists sub-dials in a category, paged 30 at a time (`Channel/GetDialList`). */
|
|
938
|
+
getDialList(dialType, page = 1, deviceType = "LCD") {
|
|
939
|
+
return this.post("/Channel/GetDialList", {
|
|
940
|
+
DialType: dialType,
|
|
941
|
+
DeviceType: deviceType,
|
|
942
|
+
Page: page
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
/** Lists whole (5-panel) dials, paged (`Channel/Get5LcdClockListForCommon`). */
|
|
946
|
+
getWholeDialList(page = 1) {
|
|
947
|
+
return this.post("/Channel/Get5LcdClockListForCommon", { Page: page });
|
|
948
|
+
}
|
|
949
|
+
/** Gets channel info incl. independent dial groups (`Channel/Get5LcdInfoV2`). */
|
|
950
|
+
getChannelInfo(deviceId, deviceType = "LCD") {
|
|
951
|
+
return this.post("/Channel/Get5LcdInfoV2", {
|
|
952
|
+
DeviceId: deviceId,
|
|
953
|
+
DeviceType: deviceType
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/** Lists the account's liked images (`Device/GetImgLikeList`). */
|
|
957
|
+
getLikedImages(deviceId, deviceMac, page = 1) {
|
|
958
|
+
return this.post("/Device/GetImgLikeList", {
|
|
959
|
+
DeviceId: deviceId,
|
|
960
|
+
DeviceMac: deviceMac,
|
|
961
|
+
Page: page
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/** Lists the account's uploaded images (`Device/GetImgUploadList`). */
|
|
965
|
+
getUploadedImages(deviceId, deviceMac, page = 1) {
|
|
966
|
+
return this.post("/Device/GetImgUploadList", {
|
|
967
|
+
DeviceId: deviceId,
|
|
968
|
+
DeviceMac: deviceMac,
|
|
969
|
+
Page: page
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/** Lists fonts usable in {@link AnimationCommands.sendItemList} (`Device/GetTimeDialFontList`). */
|
|
973
|
+
getFontList() {
|
|
974
|
+
return this.post("/Device/GetTimeDialFontList");
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// src/discovery.ts
|
|
979
|
+
async function discoverDevices(options = {}) {
|
|
980
|
+
const response = await cloudPost(LAN_DISCOVERY_URL, {}, options);
|
|
981
|
+
return (response.DeviceList ?? []).map((device) => ({
|
|
982
|
+
name: device.DeviceName ?? "",
|
|
983
|
+
id: device.DeviceId ?? 0,
|
|
984
|
+
ip: device.DevicePrivateIP ?? "",
|
|
985
|
+
mac: device.DeviceMac ?? ""
|
|
986
|
+
}));
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export { AnimationCommands, BatchCommands, DEFAULT_CLOUD_BASE_URL, DEFAULT_PATH, DEFAULT_PORT, DialCommands, DivoomCloudClient, DivoomCloudError, DivoomConnectionError, DivoomDeviceError, DivoomError, DivoomHttpError, DivoomTimeoutError, DivoomValidationError, DrawCommands, HARDWARE_402_PATH, HARDWARE_402_PORT, HttpTransport, LAN_DISCOVERY_URL, PANEL_COUNT, PANEL_SIZE, PicIdGenerator, SystemCommands, TimesGateClient, ToolCommands, cloudPost, discoverDevices, panelToLcdArray, panelsToLcdArray };
|
|
990
|
+
//# sourceMappingURL=index.js.map
|
|
991
|
+
//# sourceMappingURL=index.js.map
|