@ecobridge.xyz/devicemanager 3.0.2

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.
Files changed (137) hide show
  1. package/.gitea/workflows/default_nottags.yaml +66 -0
  2. package/.gitea/workflows/default_tags.yaml +124 -0
  3. package/.vscode/launch.json +11 -0
  4. package/.vscode/settings.json +26 -0
  5. package/changelog.md +91 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/device/device.classes.device.d.ts +205 -0
  9. package/dist_ts/device/device.classes.device.js +344 -0
  10. package/dist_ts/devicemanager.classes.devicemanager.d.ts +210 -0
  11. package/dist_ts/devicemanager.classes.devicemanager.js +835 -0
  12. package/dist_ts/discovery/discovery.classes.homeassistant.d.ts +101 -0
  13. package/dist_ts/discovery/discovery.classes.homeassistant.js +313 -0
  14. package/dist_ts/discovery/discovery.classes.mdns.d.ts +90 -0
  15. package/dist_ts/discovery/discovery.classes.mdns.js +291 -0
  16. package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +64 -0
  17. package/dist_ts/discovery/discovery.classes.networkscanner.js +535 -0
  18. package/dist_ts/discovery/discovery.classes.ssdp.d.ts +112 -0
  19. package/dist_ts/discovery/discovery.classes.ssdp.js +255 -0
  20. package/dist_ts/factories/index.d.ts +190 -0
  21. package/dist_ts/factories/index.js +350 -0
  22. package/dist_ts/features/feature.abstract.d.ts +118 -0
  23. package/dist_ts/features/feature.abstract.js +172 -0
  24. package/dist_ts/features/feature.camera.d.ts +95 -0
  25. package/dist_ts/features/feature.camera.js +162 -0
  26. package/dist_ts/features/feature.climate.d.ts +174 -0
  27. package/dist_ts/features/feature.climate.js +328 -0
  28. package/dist_ts/features/feature.cover.d.ts +121 -0
  29. package/dist_ts/features/feature.cover.js +217 -0
  30. package/dist_ts/features/feature.fan.d.ts +127 -0
  31. package/dist_ts/features/feature.fan.js +235 -0
  32. package/dist_ts/features/feature.light.d.ts +156 -0
  33. package/dist_ts/features/feature.light.js +296 -0
  34. package/dist_ts/features/feature.lock.d.ts +103 -0
  35. package/dist_ts/features/feature.lock.js +170 -0
  36. package/dist_ts/features/feature.playback.d.ts +98 -0
  37. package/dist_ts/features/feature.playback.js +191 -0
  38. package/dist_ts/features/feature.power.d.ts +79 -0
  39. package/dist_ts/features/feature.power.js +173 -0
  40. package/dist_ts/features/feature.print.d.ts +78 -0
  41. package/dist_ts/features/feature.print.js +226 -0
  42. package/dist_ts/features/feature.scan.d.ts +79 -0
  43. package/dist_ts/features/feature.scan.js +315 -0
  44. package/dist_ts/features/feature.sensor.d.ts +95 -0
  45. package/dist_ts/features/feature.sensor.js +147 -0
  46. package/dist_ts/features/feature.snmp.d.ts +88 -0
  47. package/dist_ts/features/feature.snmp.js +175 -0
  48. package/dist_ts/features/feature.switch.d.ts +75 -0
  49. package/dist_ts/features/feature.switch.js +126 -0
  50. package/dist_ts/features/feature.volume.d.ts +104 -0
  51. package/dist_ts/features/feature.volume.js +186 -0
  52. package/dist_ts/features/index.d.ts +19 -0
  53. package/dist_ts/features/index.js +23 -0
  54. package/dist_ts/helpers/helpers.iprange.d.ts +35 -0
  55. package/dist_ts/helpers/helpers.iprange.js +151 -0
  56. package/dist_ts/helpers/helpers.retry.d.ts +25 -0
  57. package/dist_ts/helpers/helpers.retry.js +69 -0
  58. package/dist_ts/index.d.ts +15 -0
  59. package/dist_ts/index.js +40 -0
  60. package/dist_ts/interfaces/feature.interfaces.d.ts +255 -0
  61. package/dist_ts/interfaces/feature.interfaces.js +6 -0
  62. package/dist_ts/interfaces/homeassistant.interfaces.d.ts +550 -0
  63. package/dist_ts/interfaces/homeassistant.interfaces.js +111 -0
  64. package/dist_ts/interfaces/index.d.ts +330 -0
  65. package/dist_ts/interfaces/index.js +16 -0
  66. package/dist_ts/interfaces/smarthome.interfaces.d.ts +282 -0
  67. package/dist_ts/interfaces/smarthome.interfaces.js +7 -0
  68. package/dist_ts/paths.d.ts +1 -0
  69. package/dist_ts/paths.js +3 -0
  70. package/dist_ts/plugins.d.ts +23 -0
  71. package/dist_ts/plugins.js +30 -0
  72. package/dist_ts/protocols/index.d.ts +12 -0
  73. package/dist_ts/protocols/index.js +21 -0
  74. package/dist_ts/protocols/protocol.escl.d.ts +58 -0
  75. package/dist_ts/protocols/protocol.escl.js +364 -0
  76. package/dist_ts/protocols/protocol.homeassistant.d.ts +248 -0
  77. package/dist_ts/protocols/protocol.homeassistant.js +611 -0
  78. package/dist_ts/protocols/protocol.ipp.d.ts +45 -0
  79. package/dist_ts/protocols/protocol.ipp.js +284 -0
  80. package/dist_ts/protocols/protocol.nut.d.ts +160 -0
  81. package/dist_ts/protocols/protocol.nut.js +364 -0
  82. package/dist_ts/protocols/protocol.sane.d.ts +104 -0
  83. package/dist_ts/protocols/protocol.sane.js +597 -0
  84. package/dist_ts/protocols/protocol.snmp.d.ts +133 -0
  85. package/dist_ts/protocols/protocol.snmp.js +333 -0
  86. package/dist_ts/protocols/protocol.upnp.d.ts +207 -0
  87. package/dist_ts/protocols/protocol.upnp.js +442 -0
  88. package/dist_ts/protocols/protocol.upssnmp.d.ts +160 -0
  89. package/dist_ts/protocols/protocol.upssnmp.js +263 -0
  90. package/npmextra.json +24 -0
  91. package/package.json +39 -0
  92. package/readme.hints.md +115 -0
  93. package/readme.md +624 -0
  94. package/test/test.ts +261 -0
  95. package/ts/00_commitinfo_data.ts +8 -0
  96. package/ts/device/device.classes.device.ts +454 -0
  97. package/ts/devicemanager.classes.devicemanager.ts +1219 -0
  98. package/ts/discovery/discovery.classes.homeassistant.ts +376 -0
  99. package/ts/discovery/discovery.classes.mdns.ts +347 -0
  100. package/ts/discovery/discovery.classes.networkscanner.ts +648 -0
  101. package/ts/discovery/discovery.classes.ssdp.ts +357 -0
  102. package/ts/factories/index.ts +696 -0
  103. package/ts/features/feature.abstract.ts +251 -0
  104. package/ts/features/feature.camera.ts +214 -0
  105. package/ts/features/feature.climate.ts +407 -0
  106. package/ts/features/feature.cover.ts +278 -0
  107. package/ts/features/feature.fan.ts +296 -0
  108. package/ts/features/feature.light.ts +369 -0
  109. package/ts/features/feature.lock.ts +223 -0
  110. package/ts/features/feature.playback.ts +246 -0
  111. package/ts/features/feature.power.ts +231 -0
  112. package/ts/features/feature.print.ts +297 -0
  113. package/ts/features/feature.scan.ts +370 -0
  114. package/ts/features/feature.sensor.ts +202 -0
  115. package/ts/features/feature.snmp.ts +235 -0
  116. package/ts/features/feature.switch.ts +170 -0
  117. package/ts/features/feature.volume.ts +256 -0
  118. package/ts/features/index.ts +25 -0
  119. package/ts/helpers/helpers.iprange.ts +172 -0
  120. package/ts/helpers/helpers.retry.ts +100 -0
  121. package/ts/index.ts +165 -0
  122. package/ts/interfaces/feature.interfaces.ts +359 -0
  123. package/ts/interfaces/homeassistant.interfaces.ts +666 -0
  124. package/ts/interfaces/index.ts +421 -0
  125. package/ts/interfaces/smarthome.interfaces.ts +421 -0
  126. package/ts/paths.ts +5 -0
  127. package/ts/plugins.ts +41 -0
  128. package/ts/protocols/index.ts +59 -0
  129. package/ts/protocols/protocol.escl.ts +439 -0
  130. package/ts/protocols/protocol.homeassistant.ts +737 -0
  131. package/ts/protocols/protocol.ipp.ts +329 -0
  132. package/ts/protocols/protocol.nut.ts +471 -0
  133. package/ts/protocols/protocol.sane.ts +694 -0
  134. package/ts/protocols/protocol.snmp.ts +439 -0
  135. package/ts/protocols/protocol.upnp.ts +627 -0
  136. package/ts/protocols/protocol.upssnmp.ts +377 -0
  137. package/tsconfig.json +12 -0
@@ -0,0 +1,694 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type {
3
+ ISaneDevice,
4
+ ISaneOption,
5
+ ISaneParameters,
6
+ IScanOptions,
7
+ IScanResult,
8
+ TColorMode,
9
+ } from '../interfaces/index.js';
10
+
11
+ /**
12
+ * SANE network protocol RPC codes
13
+ */
14
+ const enum SaneRpc {
15
+ INIT = 0,
16
+ GET_DEVICES = 1,
17
+ OPEN = 2,
18
+ CLOSE = 3,
19
+ GET_OPTION_DESCRIPTORS = 4,
20
+ CONTROL_OPTION = 5,
21
+ GET_PARAMETERS = 6,
22
+ START = 7,
23
+ CANCEL = 8,
24
+ AUTHORIZE = 9,
25
+ EXIT = 10,
26
+ }
27
+
28
+ /**
29
+ * SANE status codes
30
+ */
31
+ const enum SaneStatus {
32
+ GOOD = 0,
33
+ UNSUPPORTED = 1,
34
+ CANCELLED = 2,
35
+ DEVICE_BUSY = 3,
36
+ INVAL = 4,
37
+ EOF = 5,
38
+ JAMMED = 6,
39
+ NO_DOCS = 7,
40
+ COVER_OPEN = 8,
41
+ IO_ERROR = 9,
42
+ NO_MEM = 10,
43
+ ACCESS_DENIED = 11,
44
+ }
45
+
46
+ /**
47
+ * SANE option types
48
+ */
49
+ const enum SaneValueType {
50
+ BOOL = 0,
51
+ INT = 1,
52
+ FIXED = 2,
53
+ STRING = 3,
54
+ BUTTON = 4,
55
+ GROUP = 5,
56
+ }
57
+
58
+ /**
59
+ * SANE option units
60
+ */
61
+ const enum SaneUnit {
62
+ NONE = 0,
63
+ PIXEL = 1,
64
+ BIT = 2,
65
+ MM = 3,
66
+ DPI = 4,
67
+ PERCENT = 5,
68
+ MICROSECOND = 6,
69
+ }
70
+
71
+ /**
72
+ * SANE constraint types
73
+ */
74
+ const enum SaneConstraintType {
75
+ NONE = 0,
76
+ RANGE = 1,
77
+ WORD_LIST = 2,
78
+ STRING_LIST = 3,
79
+ }
80
+
81
+ /**
82
+ * SANE control option actions
83
+ */
84
+ const enum SaneAction {
85
+ GET_VALUE = 0,
86
+ SET_VALUE = 1,
87
+ SET_AUTO = 2,
88
+ }
89
+
90
+ const SANE_PORT = 6566;
91
+ const SANE_NET_PROTOCOL_VERSION = 3;
92
+
93
+ /**
94
+ * Status code to error message mapping
95
+ */
96
+ const STATUS_MESSAGES: Record<number, string> = {
97
+ [SaneStatus.GOOD]: 'Success',
98
+ [SaneStatus.UNSUPPORTED]: 'Operation not supported',
99
+ [SaneStatus.CANCELLED]: 'Operation cancelled',
100
+ [SaneStatus.DEVICE_BUSY]: 'Device busy',
101
+ [SaneStatus.INVAL]: 'Invalid argument',
102
+ [SaneStatus.EOF]: 'End of file',
103
+ [SaneStatus.JAMMED]: 'Document feeder jammed',
104
+ [SaneStatus.NO_DOCS]: 'No documents in feeder',
105
+ [SaneStatus.COVER_OPEN]: 'Scanner cover open',
106
+ [SaneStatus.IO_ERROR]: 'I/O error',
107
+ [SaneStatus.NO_MEM]: 'Out of memory',
108
+ [SaneStatus.ACCESS_DENIED]: 'Access denied',
109
+ };
110
+
111
+ /**
112
+ * SANE network protocol client
113
+ */
114
+ export class SaneProtocol {
115
+ private socket: plugins.net.Socket | null = null;
116
+ private address: string;
117
+ private port: number;
118
+ private handle: number = -1;
119
+ private options: ISaneOption[] = [];
120
+ private readBuffer: Buffer = Buffer.alloc(0);
121
+
122
+ constructor(address: string, port: number = SANE_PORT) {
123
+ this.address = address;
124
+ this.port = port;
125
+ }
126
+
127
+ /**
128
+ * Connect to SANE daemon
129
+ */
130
+ public async connect(): Promise<void> {
131
+ return new Promise((resolve, reject) => {
132
+ this.socket = plugins.net.createConnection(
133
+ { host: this.address, port: this.port },
134
+ () => {
135
+ this.init()
136
+ .then(() => resolve())
137
+ .catch(reject);
138
+ }
139
+ );
140
+
141
+ this.socket.on('error', reject);
142
+ this.socket.on('data', (data: Buffer) => {
143
+ this.readBuffer = Buffer.concat([this.readBuffer, data]);
144
+ });
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Disconnect from SANE daemon
150
+ */
151
+ public async disconnect(): Promise<void> {
152
+ if (this.handle >= 0) {
153
+ await this.close();
154
+ }
155
+
156
+ await this.exit();
157
+
158
+ if (this.socket) {
159
+ this.socket.destroy();
160
+ this.socket = null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Initialize connection (SANE_NET_INIT)
166
+ */
167
+ private async init(): Promise<void> {
168
+ const request = this.buildRequest(SaneRpc.INIT);
169
+ this.writeWord(request, SANE_NET_PROTOCOL_VERSION);
170
+ this.writeString(request, ''); // Username (empty for now)
171
+
172
+ await this.sendRequest(request);
173
+ const response = await this.readResponse();
174
+
175
+ const status = this.readWord(response);
176
+ const version = this.readWord(response);
177
+
178
+ if (status !== SaneStatus.GOOD) {
179
+ throw new Error(`SANE init failed: ${STATUS_MESSAGES[status] || 'Unknown error'}`);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get available devices (SANE_NET_GET_DEVICES)
185
+ */
186
+ public async getDevices(): Promise<ISaneDevice[]> {
187
+ const request = this.buildRequest(SaneRpc.GET_DEVICES);
188
+ await this.sendRequest(request);
189
+ const response = await this.readResponse();
190
+
191
+ const status = this.readWord(response);
192
+ if (status !== SaneStatus.GOOD) {
193
+ throw new Error(`Failed to get devices: ${STATUS_MESSAGES[status]}`);
194
+ }
195
+
196
+ const devices: ISaneDevice[] = [];
197
+ const count = this.readWord(response);
198
+
199
+ for (let i = 0; i < count; i++) {
200
+ const hasDevice = this.readWord(response);
201
+ if (hasDevice) {
202
+ devices.push({
203
+ name: this.readString(response),
204
+ vendor: this.readString(response),
205
+ model: this.readString(response),
206
+ type: this.readString(response),
207
+ });
208
+ }
209
+ }
210
+
211
+ return devices;
212
+ }
213
+
214
+ /**
215
+ * Open a device (SANE_NET_OPEN)
216
+ */
217
+ public async open(deviceName: string): Promise<void> {
218
+ const request = this.buildRequest(SaneRpc.OPEN);
219
+ this.writeString(request, deviceName);
220
+
221
+ await this.sendRequest(request);
222
+ const response = await this.readResponse();
223
+
224
+ const status = this.readWord(response);
225
+ if (status !== SaneStatus.GOOD) {
226
+ // Check for authorization required
227
+ const resource = this.readString(response);
228
+ if (resource) {
229
+ throw new Error(`Authorization required for: ${resource}`);
230
+ }
231
+ throw new Error(`Failed to open device: ${STATUS_MESSAGES[status]}`);
232
+ }
233
+
234
+ this.handle = this.readWord(response);
235
+ this.readString(response); // resource (should be empty on success)
236
+
237
+ // Get option descriptors
238
+ await this.getOptionDescriptors();
239
+ }
240
+
241
+ /**
242
+ * Close the current device (SANE_NET_CLOSE)
243
+ */
244
+ public async close(): Promise<void> {
245
+ if (this.handle < 0) {
246
+ return;
247
+ }
248
+
249
+ const request = this.buildRequest(SaneRpc.CLOSE);
250
+ this.writeWord(request, this.handle);
251
+
252
+ await this.sendRequest(request);
253
+ await this.readResponse(); // Just read to clear buffer
254
+
255
+ this.handle = -1;
256
+ this.options = [];
257
+ }
258
+
259
+ /**
260
+ * Exit connection (SANE_NET_EXIT)
261
+ */
262
+ private async exit(): Promise<void> {
263
+ const request = this.buildRequest(SaneRpc.EXIT);
264
+ await this.sendRequest(request);
265
+ }
266
+
267
+ /**
268
+ * Get option descriptors (SANE_NET_GET_OPTION_DESCRIPTORS)
269
+ */
270
+ private async getOptionDescriptors(): Promise<void> {
271
+ const request = this.buildRequest(SaneRpc.GET_OPTION_DESCRIPTORS);
272
+ this.writeWord(request, this.handle);
273
+
274
+ await this.sendRequest(request);
275
+ const response = await this.readResponse();
276
+
277
+ const count = this.readWord(response);
278
+ this.options = [];
279
+
280
+ for (let i = 0; i < count; i++) {
281
+ const hasOption = this.readWord(response);
282
+ if (!hasOption) {
283
+ continue;
284
+ }
285
+
286
+ const option: ISaneOption = {
287
+ name: this.readString(response),
288
+ title: this.readString(response),
289
+ description: this.readString(response),
290
+ type: this.mapValueType(this.readWord(response)),
291
+ unit: this.mapUnit(this.readWord(response)),
292
+ size: this.readWord(response),
293
+ capabilities: this.readWord(response),
294
+ constraintType: this.mapConstraintType(this.readWord(response)),
295
+ };
296
+
297
+ // Read constraint based on type
298
+ if (option.constraintType === 'range') {
299
+ option.constraint = {
300
+ range: {
301
+ min: this.readWord(response),
302
+ max: this.readWord(response),
303
+ quant: this.readWord(response),
304
+ },
305
+ };
306
+ } else if (option.constraintType === 'word_list') {
307
+ const wordCount = this.readWord(response);
308
+ const words: number[] = [];
309
+ for (let j = 0; j < wordCount; j++) {
310
+ words.push(this.readWord(response));
311
+ }
312
+ option.constraint = { wordList: words };
313
+ } else if (option.constraintType === 'string_list') {
314
+ const strings: string[] = [];
315
+ let str: string;
316
+ while ((str = this.readString(response)) !== '') {
317
+ strings.push(str);
318
+ }
319
+ option.constraint = { stringList: strings };
320
+ }
321
+
322
+ this.options.push(option);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Set an option value
328
+ */
329
+ public async setOption(name: string, value: unknown): Promise<void> {
330
+ const optionIndex = this.options.findIndex((o) => o.name === name);
331
+ if (optionIndex < 0) {
332
+ throw new Error(`Unknown option: ${name}`);
333
+ }
334
+
335
+ const option = this.options[optionIndex];
336
+ const request = this.buildRequest(SaneRpc.CONTROL_OPTION);
337
+
338
+ this.writeWord(request, this.handle);
339
+ this.writeWord(request, optionIndex);
340
+ this.writeWord(request, SaneAction.SET_VALUE);
341
+ this.writeWord(request, option.type === 'string' ? (value as string).length + 1 : option.size);
342
+
343
+ // Write value based on type
344
+ if (option.type === 'bool' || option.type === 'int') {
345
+ this.writeWord(request, value as number);
346
+ } else if (option.type === 'fixed') {
347
+ this.writeWord(request, Math.round((value as number) * 65536));
348
+ } else if (option.type === 'string') {
349
+ this.writeString(request, value as string);
350
+ }
351
+
352
+ await this.sendRequest(request);
353
+ const response = await this.readResponse();
354
+
355
+ const status = this.readWord(response);
356
+ if (status !== SaneStatus.GOOD) {
357
+ throw new Error(`Failed to set option ${name}: ${STATUS_MESSAGES[status]}`);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Get scan parameters (SANE_NET_GET_PARAMETERS)
363
+ */
364
+ public async getParameters(): Promise<ISaneParameters> {
365
+ const request = this.buildRequest(SaneRpc.GET_PARAMETERS);
366
+ this.writeWord(request, this.handle);
367
+
368
+ await this.sendRequest(request);
369
+ const response = await this.readResponse();
370
+
371
+ const status = this.readWord(response);
372
+ if (status !== SaneStatus.GOOD) {
373
+ throw new Error(`Failed to get parameters: ${STATUS_MESSAGES[status]}`);
374
+ }
375
+
376
+ const formatCode = this.readWord(response);
377
+ const format = this.mapFormat(formatCode);
378
+
379
+ return {
380
+ format,
381
+ lastFrame: this.readWord(response) === 1,
382
+ bytesPerLine: this.readWord(response),
383
+ pixelsPerLine: this.readWord(response),
384
+ lines: this.readWord(response),
385
+ depth: this.readWord(response),
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Start scanning (SANE_NET_START)
391
+ */
392
+ public async start(): Promise<{ port: number; byteOrder: 'little' | 'big' }> {
393
+ const request = this.buildRequest(SaneRpc.START);
394
+ this.writeWord(request, this.handle);
395
+
396
+ await this.sendRequest(request);
397
+ const response = await this.readResponse();
398
+
399
+ const status = this.readWord(response);
400
+ if (status !== SaneStatus.GOOD) {
401
+ throw new Error(`Failed to start scan: ${STATUS_MESSAGES[status]}`);
402
+ }
403
+
404
+ const port = this.readWord(response);
405
+ const byteOrder = this.readWord(response) === 0x1234 ? 'little' : 'big';
406
+ this.readString(response); // resource
407
+
408
+ return { port, byteOrder };
409
+ }
410
+
411
+ /**
412
+ * Cancel scanning (SANE_NET_CANCEL)
413
+ */
414
+ public async cancel(): Promise<void> {
415
+ const request = this.buildRequest(SaneRpc.CANCEL);
416
+ this.writeWord(request, this.handle);
417
+
418
+ await this.sendRequest(request);
419
+ await this.readResponse();
420
+ }
421
+
422
+ /**
423
+ * Perform a complete scan
424
+ */
425
+ public async scan(options: IScanOptions): Promise<IScanResult> {
426
+ // Configure scan options
427
+ await this.configureOptions(options);
428
+
429
+ // Get parameters
430
+ const params = await this.getParameters();
431
+
432
+ // Start scan and get data port
433
+ const { port, byteOrder } = await this.start();
434
+
435
+ // Connect to data port and read image data
436
+ const imageData = await this.readImageData(port, params, byteOrder);
437
+
438
+ return {
439
+ data: imageData,
440
+ format: options.format ?? 'png',
441
+ width: params.pixelsPerLine,
442
+ height: params.lines,
443
+ resolution: options.resolution ?? 300,
444
+ colorMode: options.colorMode ?? 'color',
445
+ mimeType: `image/${options.format ?? 'png'}`,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Configure scan options based on IScanOptions
451
+ */
452
+ private async configureOptions(options: IScanOptions): Promise<void> {
453
+ // Set resolution
454
+ if (options.resolution) {
455
+ const resOption = this.options.find((o) => o.name === 'resolution');
456
+ if (resOption) {
457
+ await this.setOption('resolution', options.resolution);
458
+ }
459
+ }
460
+
461
+ // Set color mode
462
+ if (options.colorMode) {
463
+ const modeOption = this.options.find((o) => o.name === 'mode');
464
+ if (modeOption) {
465
+ const modeValue = this.mapColorMode(options.colorMode);
466
+ await this.setOption('mode', modeValue);
467
+ }
468
+ }
469
+
470
+ // Set scan area
471
+ if (options.area) {
472
+ const tlxOption = this.options.find((o) => o.name === 'tl-x');
473
+ const tlyOption = this.options.find((o) => o.name === 'tl-y');
474
+ const brxOption = this.options.find((o) => o.name === 'br-x');
475
+ const bryOption = this.options.find((o) => o.name === 'br-y');
476
+
477
+ if (tlxOption) await this.setOption('tl-x', options.area.x);
478
+ if (tlyOption) await this.setOption('tl-y', options.area.y);
479
+ if (brxOption) await this.setOption('br-x', options.area.x + options.area.width);
480
+ if (bryOption) await this.setOption('br-y', options.area.y + options.area.height);
481
+ }
482
+
483
+ // Set source
484
+ if (options.source) {
485
+ const sourceOption = this.options.find((o) => o.name === 'source');
486
+ if (sourceOption) {
487
+ await this.setOption('source', options.source);
488
+ }
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Read image data from data port
494
+ */
495
+ private async readImageData(
496
+ port: number,
497
+ params: ISaneParameters,
498
+ _byteOrder: 'little' | 'big'
499
+ ): Promise<Buffer> {
500
+ return new Promise((resolve, reject) => {
501
+ const dataSocket = plugins.net.createConnection(
502
+ { host: this.address, port },
503
+ () => {
504
+ const chunks: Buffer[] = [];
505
+
506
+ dataSocket.on('data', (data: Buffer) => {
507
+ // Data format: 4 bytes length + data
508
+ // Read records until length is 0xFFFFFFFF (end marker)
509
+ let offset = 0;
510
+ while (offset < data.length) {
511
+ if (offset + 4 > data.length) {
512
+ break;
513
+ }
514
+
515
+ const length = data.readUInt32BE(offset);
516
+ offset += 4;
517
+
518
+ if (length === 0xffffffff) {
519
+ // End of data
520
+ dataSocket.destroy();
521
+ resolve(Buffer.concat(chunks));
522
+ return;
523
+ }
524
+
525
+ if (offset + length > data.length) {
526
+ // Incomplete record, wait for more data
527
+ break;
528
+ }
529
+
530
+ chunks.push(data.subarray(offset, offset + length));
531
+ offset += length;
532
+ }
533
+ });
534
+
535
+ dataSocket.on('end', () => {
536
+ resolve(Buffer.concat(chunks));
537
+ });
538
+
539
+ dataSocket.on('error', reject);
540
+ }
541
+ );
542
+
543
+ dataSocket.on('error', reject);
544
+ });
545
+ }
546
+
547
+ /**
548
+ * Map color mode to SANE mode string
549
+ */
550
+ private mapColorMode(mode: TColorMode): string {
551
+ switch (mode) {
552
+ case 'color':
553
+ return 'Color';
554
+ case 'grayscale':
555
+ return 'Gray';
556
+ case 'blackwhite':
557
+ return 'Lineart';
558
+ default:
559
+ return 'Color';
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Map SANE format code to string
565
+ */
566
+ private mapFormat(code: number): ISaneParameters['format'] {
567
+ const formats: Record<number, ISaneParameters['format']> = {
568
+ 0: 'gray',
569
+ 1: 'rgb',
570
+ 2: 'red',
571
+ 3: 'green',
572
+ 4: 'blue',
573
+ };
574
+ return formats[code] ?? 'gray';
575
+ }
576
+
577
+ /**
578
+ * Map value type code to string
579
+ */
580
+ private mapValueType(code: number): ISaneOption['type'] {
581
+ const types: Record<number, ISaneOption['type']> = {
582
+ [SaneValueType.BOOL]: 'bool',
583
+ [SaneValueType.INT]: 'int',
584
+ [SaneValueType.FIXED]: 'fixed',
585
+ [SaneValueType.STRING]: 'string',
586
+ [SaneValueType.BUTTON]: 'button',
587
+ [SaneValueType.GROUP]: 'group',
588
+ };
589
+ return types[code] ?? 'int';
590
+ }
591
+
592
+ /**
593
+ * Map unit code to string
594
+ */
595
+ private mapUnit(code: number): ISaneOption['unit'] {
596
+ const units: Record<number, ISaneOption['unit']> = {
597
+ [SaneUnit.NONE]: 'none',
598
+ [SaneUnit.PIXEL]: 'pixel',
599
+ [SaneUnit.BIT]: 'bit',
600
+ [SaneUnit.MM]: 'mm',
601
+ [SaneUnit.DPI]: 'dpi',
602
+ [SaneUnit.PERCENT]: 'percent',
603
+ [SaneUnit.MICROSECOND]: 'microsecond',
604
+ };
605
+ return units[code] ?? 'none';
606
+ }
607
+
608
+ /**
609
+ * Map constraint type code to string
610
+ */
611
+ private mapConstraintType(code: number): ISaneOption['constraintType'] {
612
+ const types: Record<number, ISaneOption['constraintType']> = {
613
+ [SaneConstraintType.NONE]: 'none',
614
+ [SaneConstraintType.RANGE]: 'range',
615
+ [SaneConstraintType.WORD_LIST]: 'word_list',
616
+ [SaneConstraintType.STRING_LIST]: 'string_list',
617
+ };
618
+ return types[code] ?? 'none';
619
+ }
620
+
621
+ // =========================================================================
622
+ // Low-level protocol helpers
623
+ // =========================================================================
624
+
625
+ private buildRequest(rpc: SaneRpc): Buffer[] {
626
+ const chunks: Buffer[] = [];
627
+ const header = Buffer.alloc(4);
628
+ header.writeUInt32BE(rpc, 0);
629
+ chunks.push(header);
630
+ return chunks;
631
+ }
632
+
633
+ private writeWord(chunks: Buffer[], value: number): void {
634
+ const buf = Buffer.alloc(4);
635
+ buf.writeUInt32BE(value >>> 0, 0);
636
+ chunks.push(buf);
637
+ }
638
+
639
+ private writeString(chunks: Buffer[], str: string): void {
640
+ const strBuf = Buffer.from(str + '\0', 'utf-8');
641
+ this.writeWord(chunks, strBuf.length);
642
+ chunks.push(strBuf);
643
+ }
644
+
645
+ private async sendRequest(chunks: Buffer[]): Promise<void> {
646
+ if (!this.socket) {
647
+ throw new Error('Not connected');
648
+ }
649
+
650
+ const data = Buffer.concat(chunks);
651
+ return new Promise((resolve, reject) => {
652
+ this.socket!.write(data, (err) => {
653
+ if (err) reject(err);
654
+ else resolve();
655
+ });
656
+ });
657
+ }
658
+
659
+ private async readResponse(): Promise<{ buffer: Buffer; offset: number }> {
660
+ // Wait for data
661
+ await this.waitForData(4);
662
+
663
+ return { buffer: this.readBuffer, offset: 0 };
664
+ }
665
+
666
+ private async waitForData(minBytes: number): Promise<void> {
667
+ const startTime = Date.now();
668
+ const timeout = 30000;
669
+
670
+ while (this.readBuffer.length < minBytes) {
671
+ if (Date.now() - startTime > timeout) {
672
+ throw new Error('Timeout waiting for SANE response');
673
+ }
674
+ await plugins.smartdelay.delayFor(10);
675
+ }
676
+ }
677
+
678
+ private readWord(response: { buffer: Buffer; offset: number }): number {
679
+ const value = response.buffer.readUInt32BE(response.offset);
680
+ response.offset += 4;
681
+ return value;
682
+ }
683
+
684
+ private readString(response: { buffer: Buffer; offset: number }): string {
685
+ const length = this.readWord(response);
686
+ if (length === 0) {
687
+ return '';
688
+ }
689
+
690
+ const str = response.buffer.toString('utf-8', response.offset, response.offset + length - 1);
691
+ response.offset += length;
692
+ return str;
693
+ }
694
+ }