@ecobridge.xyz/devicemanager 3.1.1 β 3.2.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/changelog.md +25 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/device/device.classes.device.d.ts +2 -2
- package/dist_ts/device/device.classes.device.js +3 -3
- package/dist_ts/devicemanager.classes.devicemanager.d.ts +17 -4
- package/dist_ts/devicemanager.classes.devicemanager.js +67 -5
- package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +4 -0
- package/dist_ts/discovery/discovery.classes.networkscanner.js +43 -2
- package/dist_ts/events/events.observable.d.ts +15 -0
- package/dist_ts/events/events.observable.js +22 -0
- package/dist_ts/events/index.d.ts +1 -0
- package/dist_ts/events/index.js +2 -0
- package/dist_ts/factories/index.js +7 -5
- package/dist_ts/features/feature.abstract.d.ts +2 -2
- package/dist_ts/features/feature.abstract.js +3 -3
- package/dist_ts/features/feature.print.d.ts +3 -0
- package/dist_ts/features/feature.print.js +17 -2
- package/dist_ts/features/feature.scan.d.ts +5 -2
- package/dist_ts/features/feature.scan.js +98 -27
- package/dist_ts/index.d.ts +3 -1
- package/dist_ts/index.js +4 -2
- package/dist_ts/interfaces/feature.interfaces.d.ts +21 -1
- package/dist_ts/interfaces/index.d.ts +21 -3
- package/dist_ts/interfaces/index.js +1 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/protocols/index.d.ts +2 -1
- package/dist_ts/protocols/index.js +4 -2
- package/dist_ts/protocols/protocol.brother.d.ts +67 -0
- package/dist_ts/protocols/protocol.brother.js +560 -0
- package/dist_ts/protocols/protocol.escl.d.ts +13 -4
- package/dist_ts/protocols/protocol.escl.js +344 -177
- package/dist_ts/protocols/protocol.ipp.d.ts +2 -0
- package/dist_ts/protocols/protocol.ipp.js +22 -2
- package/dist_ts/providers/index.d.ts +1 -0
- package/dist_ts/providers/index.js +2 -0
- package/dist_ts/providers/provider.smb.d.ts +60 -0
- package/dist_ts/providers/provider.smb.js +224 -0
- package/package.json +11 -7
- package/pnpm-workspace.yaml +2 -0
- package/readme.md +98 -4
- package/test/test.brother.node.ts +306 -0
- package/test/test.escl.node.ts +233 -0
- package/test/test.ipp.node.ts +75 -0
- package/test/test.node.ts +17 -0
- package/test/test.smb.node.ts +123 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/device/device.classes.device.ts +2 -2
- package/ts/devicemanager.classes.devicemanager.ts +81 -6
- package/ts/discovery/discovery.classes.networkscanner.ts +50 -1
- package/ts/events/events.observable.ts +31 -0
- package/ts/events/index.ts +4 -0
- package/ts/factories/index.ts +7 -5
- package/ts/features/feature.abstract.ts +2 -2
- package/ts/features/feature.print.ts +16 -1
- package/ts/features/feature.scan.ts +93 -27
- package/ts/index.ts +20 -0
- package/ts/interfaces/feature.interfaces.ts +22 -1
- package/ts/interfaces/index.ts +22 -3
- package/ts/plugins.ts +2 -1
- package/ts/protocols/index.ts +11 -1
- package/ts/protocols/protocol.brother.ts +648 -0
- package/ts/protocols/protocol.escl.ts +347 -190
- package/ts/protocols/protocol.ipp.ts +24 -1
- package/ts/providers/index.ts +7 -0
- package/ts/providers/provider.smb.ts +326 -0
package/readme.md
CHANGED
|
@@ -10,8 +10,9 @@ A comprehensive, TypeScript-first device manager for discovering and communicati
|
|
|
10
10
|
`@ecobridge.xyz/devicemanager` provides a unified, object-oriented API for discovering and modeling network devices, with live communication where protocol implementations are available. Whether you're building a document scanning workflow, managing printers, controlling smart home devices, or monitoring UPS systems β this library has you covered.
|
|
11
11
|
|
|
12
12
|
**Supported Device Types:**
|
|
13
|
-
- π¨οΈ **Scanners** β eSCL (AirScan), SANE
|
|
14
|
-
- π **Printers** β IPP discovery and print execution; JetDirect/raw port detection
|
|
13
|
+
- π¨οΈ **Scanners** β Brother native network scanning, eSCL (AirScan), and SANE
|
|
14
|
+
- π **Printers** β IPP/AirPrint discovery, capabilities, jobs, cancellation, and print execution; JetDirect/raw port detection
|
|
15
|
+
- π **SMB Shares** β Host local folders through smartsamba and emit settled document arrivals through RxJS
|
|
15
16
|
- π **Speakers** β Sonos, AirPlay, Chromecast, and DLNA discovery with normalized feature/factory modeling
|
|
16
17
|
- π **UPS Systems** β NUT, SNMP protocols
|
|
17
18
|
- π‘ **SNMP Devices** β Community-based SNMP v1 and v2c sessions; v3 is not implemented
|
|
@@ -107,7 +108,7 @@ The library follows a clean, composable architecture:
|
|
|
107
108
|
βΌ
|
|
108
109
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
109
110
|
β Protocols β
|
|
110
|
-
β
|
|
111
|
+
β Brother Scan β eSCL β SANE β IPP β SNMP β NUT β UPnP/SOAP β
|
|
111
112
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
112
113
|
```
|
|
113
114
|
|
|
@@ -142,6 +143,7 @@ const printers = await manager.discoverPrinters('192.168.1.0/24');
|
|
|
142
143
|
// General network scan (all device types)
|
|
143
144
|
const devices = await manager.scanNetwork({
|
|
144
145
|
ipRange: '192.168.1.0/24',
|
|
146
|
+
probeBrother: true,
|
|
145
147
|
probeEscl: true,
|
|
146
148
|
probeIpp: true,
|
|
147
149
|
probeSane: true,
|
|
@@ -237,11 +239,38 @@ const result = await scanFeature.scan({
|
|
|
237
239
|
|
|
238
240
|
// result.data is a Buffer containing the scanned image
|
|
239
241
|
await fs.writeFile('scan.jpg', result.data);
|
|
242
|
+
|
|
243
|
+
// ADF scans expose every returned document. `data` remains the first document
|
|
244
|
+
// for backwards compatibility. Use `pages` to avoid dropping later sheets/sides.
|
|
245
|
+
console.log(`Scanner returned ${result.pageCount} document(s)`);
|
|
246
|
+
for (const [index, page] of (result.pages ?? []).entries()) {
|
|
247
|
+
await fs.writeFile(`scan-${index + 1}.jpg`, page.data);
|
|
248
|
+
}
|
|
240
249
|
```
|
|
241
250
|
|
|
251
|
+
If a scanner times out or cancels after transferring one or more documents,
|
|
252
|
+
`EsclScanError.partialResult` contains those documents with `isComplete: false`.
|
|
253
|
+
Callers can persist `partialResult.pages` before deciding whether to retry.
|
|
254
|
+
Some scanner firmware accepts eSCL settings but returns a different format or
|
|
255
|
+
resolution. Check `result.settingsWarnings` before treating the requested scan
|
|
256
|
+
quality as confirmed.
|
|
257
|
+
|
|
258
|
+
Brother devices discovered on TCP 54921 use the native scan transport before
|
|
259
|
+
eSCL. It explicitly selects `AUTO` for ADF input, controls simplex/duplex, and
|
|
260
|
+
rejects a lease when the device does not grant the requested resolution. The
|
|
261
|
+
native transport currently returns color JPEG pages; unsupported output formats
|
|
262
|
+
or color modes fail explicitly instead of being silently substituted.
|
|
263
|
+
Duplex-capable scanners default to `adf-duplex`; callers can still request
|
|
264
|
+
`adf` or `flatbed` explicitly. The transport preserves every returned side.
|
|
265
|
+
Blank-page classification and removal belong in consuming applications, where
|
|
266
|
+
document-specific review rules can be applied.
|
|
267
|
+
|
|
242
268
|
#### π PrintFeature
|
|
243
269
|
|
|
244
|
-
`PrintFeature.print()` submits jobs through IPP
|
|
270
|
+
`PrintFeature.print()` submits jobs through IPP, the print transport used by
|
|
271
|
+
AirPrint. It reads printer capabilities, submits jobs, reports job state, and
|
|
272
|
+
supports cancellation. Network discovery can also identify an open
|
|
273
|
+
JetDirect/raw port at TCP 9100, but JetDirect print execution is not implemented.
|
|
245
274
|
|
|
246
275
|
```typescript
|
|
247
276
|
const printFeature = device.selectFeature<PrintFeature>('print');
|
|
@@ -249,6 +278,7 @@ await printFeature.connect();
|
|
|
249
278
|
|
|
250
279
|
// Get printer capabilities
|
|
251
280
|
const caps = await printFeature.getCapabilities();
|
|
281
|
+
console.log(caps.airPrintSupported, caps.documentFormats, caps.resolutions);
|
|
252
282
|
|
|
253
283
|
// Print a document
|
|
254
284
|
const job = await printFeature.print(pdfBuffer, {
|
|
@@ -264,6 +294,60 @@ const job = await printFeature.print(pdfBuffer, {
|
|
|
264
294
|
const jobInfo = await printFeature.getJobInfo(job.id);
|
|
265
295
|
```
|
|
266
296
|
|
|
297
|
+
#### π SMB Share Provider
|
|
298
|
+
|
|
299
|
+
`DeviceManager.provideSmbShares()` hosts one or more local folders with
|
|
300
|
+
`@push.rocks/smartsamba`. The provider waits until a new file has stopped
|
|
301
|
+
changing before it emits `document:arrived`, which keeps consumers from opening
|
|
302
|
+
a scan while the sending device is still writing it.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { DeviceManager } from '@ecobridge.xyz/devicemanager';
|
|
306
|
+
|
|
307
|
+
const manager = new DeviceManager({ autoDiscovery: false });
|
|
308
|
+
const provider = await manager.provideSmbShares({
|
|
309
|
+
host: '0.0.0.0',
|
|
310
|
+
port: 445,
|
|
311
|
+
users: [{
|
|
312
|
+
username: 'scanner',
|
|
313
|
+
password: process.env.SMB_SCANNER_PASSWORD!,
|
|
314
|
+
}],
|
|
315
|
+
shares: [{
|
|
316
|
+
name: 'scans',
|
|
317
|
+
path: '/srv/device-scans',
|
|
318
|
+
users: [{ username: 'scanner', access: 'readWrite' }],
|
|
319
|
+
}],
|
|
320
|
+
settleTimeMs: 1000,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const subscription = provider.documents$.subscribe((document) => {
|
|
324
|
+
console.log(`New document: ${document.shareName}/${document.relativePath}`);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// The same event is forwarded through the manager as smb:document:arrived.
|
|
328
|
+
const managerSubscription = manager.events$.subscribe((event) => {
|
|
329
|
+
if (event.name === 'smb:document:arrived') {
|
|
330
|
+
console.log(event.args[0]);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Later:
|
|
335
|
+
subscription.unsubscribe();
|
|
336
|
+
managerSubscription.unsubscribe();
|
|
337
|
+
await manager.shutdown();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The provider recursively monitors every configured share. It also emits
|
|
341
|
+
`document:changed`, `document:removed`, `started`, `stopped`, and
|
|
342
|
+
`provider:error`. Existing files are treated as the startup baseline unless
|
|
343
|
+
`emitExistingDocuments` is enabled.
|
|
344
|
+
|
|
345
|
+
Binding SMB's standard TCP port 445 can require elevated privileges. Published
|
|
346
|
+
smartsamba 0.3.1 packages support signed SMB 2.0.2 and 2.1 clients and include
|
|
347
|
+
Linux amd64 and arm64 engines. Other platforms can provide a locally built engine
|
|
348
|
+
through `SMARTSAMBA_RUST_BINARY`.
|
|
349
|
+
Do not store SMB passwords in device metadata or log provider options.
|
|
350
|
+
|
|
267
351
|
#### π PlaybackFeature & VolumeFeature
|
|
268
352
|
|
|
269
353
|
`PlaybackFeature` and `VolumeFeature` provide normalized capability and state models. No external Sonos or Chromecast protocol client is bundled or wired into these features, so their methods update local feature state and emit events but do not provide live playback control for those devices.
|
|
@@ -522,9 +606,17 @@ The smart-home factories require a `protocolClient` that implements the correspo
|
|
|
522
606
|
|
|
523
607
|
### Event Handling
|
|
524
608
|
|
|
609
|
+
`DeviceManager`, `UniversalDevice`, every `Feature`, and host-side providers
|
|
610
|
+
remain Node `EventEmitter` instances and additionally expose `events$`. Each
|
|
611
|
+
observable item contains the event `name`, its `args`, and `emittedAt`.
|
|
612
|
+
|
|
525
613
|
```typescript
|
|
526
614
|
const manager = new DeviceManager();
|
|
527
615
|
|
|
616
|
+
const subscription = manager.events$.subscribe((event) => {
|
|
617
|
+
console.log(event.name, event.args, event.emittedAt);
|
|
618
|
+
});
|
|
619
|
+
|
|
528
620
|
// Discovery events
|
|
529
621
|
manager.on('device:found', ({ device, featureType }) => {
|
|
530
622
|
console.log(`Found ${device.name} with ${featureType} capability`);
|
|
@@ -547,6 +639,8 @@ device.on('status:changed', ({ oldStatus, newStatus }) => {
|
|
|
547
639
|
device.on('feature:connected', (featureType) => {
|
|
548
640
|
console.log(`Feature ${featureType} connected`);
|
|
549
641
|
});
|
|
642
|
+
|
|
643
|
+
subscription.unsubscribe();
|
|
550
644
|
```
|
|
551
645
|
|
|
552
646
|
### Error Handling
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
|
|
3
|
+
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
4
|
+
import {
|
|
5
|
+
BrotherScanError,
|
|
6
|
+
BrotherScanProtocol,
|
|
7
|
+
} from '../ts/protocols/protocol.brother.js';
|
|
8
|
+
|
|
9
|
+
function makeJpeg(width: number, height: number, resolution: number): Buffer {
|
|
10
|
+
const padding = Buffer.alloc(80, 0x41);
|
|
11
|
+
return Buffer.concat([
|
|
12
|
+
Buffer.from([
|
|
13
|
+
0xff, 0xd8,
|
|
14
|
+
0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01,
|
|
15
|
+
(resolution >> 8) & 0xff, resolution & 0xff,
|
|
16
|
+
(resolution >> 8) & 0xff, resolution & 0xff, 0x00, 0x00,
|
|
17
|
+
0xff, 0xfe, 0x00, 0x52,
|
|
18
|
+
]),
|
|
19
|
+
padding,
|
|
20
|
+
Buffer.from([
|
|
21
|
+
0xff, 0xc0, 0x00, 0x0b, 0x08,
|
|
22
|
+
(height >> 8) & 0xff, height & 0xff,
|
|
23
|
+
(width >> 8) & 0xff, width & 0xff,
|
|
24
|
+
0x01, 0x01, 0x11, 0x00,
|
|
25
|
+
0xff, 0xd9,
|
|
26
|
+
]),
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function simpleFrame(payload: Buffer): Buffer {
|
|
31
|
+
const length = Buffer.alloc(2);
|
|
32
|
+
length.writeUInt16LE(payload.length);
|
|
33
|
+
return Buffer.concat([Buffer.from([0x40]), length, payload]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function wrappedFrame(payload: Buffer, imageNumber: number = 1): Buffer {
|
|
37
|
+
const header = Buffer.alloc(12, 0);
|
|
38
|
+
header[0] = 0x40;
|
|
39
|
+
header.writeUInt16LE(7, 1);
|
|
40
|
+
header.writeUInt16LE(imageNumber, 3);
|
|
41
|
+
header.writeUInt16LE(payload.length, 10);
|
|
42
|
+
return Buffer.concat([header, payload]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function endOfImage(imageNumber: number): Buffer {
|
|
46
|
+
const header = Buffer.alloc(10, 0);
|
|
47
|
+
header[0] = 0x82;
|
|
48
|
+
header.writeUInt16LE(7, 1);
|
|
49
|
+
header.writeUInt16LE(imageNumber, 3);
|
|
50
|
+
return header;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readCommands(socket: net.Socket, onCommand: (command: Buffer) => void): void {
|
|
54
|
+
let input = Buffer.alloc(0);
|
|
55
|
+
socket.on('data', (data) => {
|
|
56
|
+
input = Buffer.concat([input, data]);
|
|
57
|
+
while (true) {
|
|
58
|
+
const end = input.indexOf(0x80);
|
|
59
|
+
if (end < 0) return;
|
|
60
|
+
const command = input.subarray(0, end + 1);
|
|
61
|
+
input = input.subarray(end + 1);
|
|
62
|
+
onCommand(command);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
tap.test('Brother native scan supports variable-length ADF leases and drains every page', async () => {
|
|
68
|
+
const commands: string[] = [];
|
|
69
|
+
const firstPage = makeJpeg(5052, 8400, 600);
|
|
70
|
+
const secondPage = makeJpeg(5052, 8400, 600);
|
|
71
|
+
const secondPageWithoutEndMarker = secondPage.subarray(0, -2);
|
|
72
|
+
const server = net.createServer((socket) => {
|
|
73
|
+
socket.write('+OK 200\r\n');
|
|
74
|
+
readCommands(socket, (command) => {
|
|
75
|
+
const text = command.toString('latin1');
|
|
76
|
+
commands.push(text);
|
|
77
|
+
if (text.startsWith('\x1bS\n')) {
|
|
78
|
+
socket.write(Buffer.from([0x80]));
|
|
79
|
+
} else if (text.startsWith('\x1bI\n')) {
|
|
80
|
+
socket.write(Buffer.concat([
|
|
81
|
+
Buffer.from([0x1a, 0x00]),
|
|
82
|
+
Buffer.from('600,600,1,213,5052,0,0', 'ascii'),
|
|
83
|
+
]));
|
|
84
|
+
} else if (text.includes('D=DUP')) {
|
|
85
|
+
socket.write(Buffer.concat([
|
|
86
|
+
wrappedFrame(firstPage.subarray(0, Math.floor(firstPage.length / 2)), 1),
|
|
87
|
+
wrappedFrame(secondPageWithoutEndMarker.subarray(0, Math.floor(secondPageWithoutEndMarker.length / 2)), 2),
|
|
88
|
+
wrappedFrame(firstPage.subarray(Math.floor(firstPage.length / 2)), 1),
|
|
89
|
+
endOfImage(1),
|
|
90
|
+
wrappedFrame(secondPageWithoutEndMarker.subarray(Math.floor(secondPageWithoutEndMarker.length / 2)), 2),
|
|
91
|
+
endOfImage(2),
|
|
92
|
+
Buffer.from([0x80]),
|
|
93
|
+
]));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
98
|
+
const address = server.address() as net.AddressInfo;
|
|
99
|
+
const client = new BrotherScanProtocol('127.0.0.1', address.port, { timeout: 1000, scanTimeout: 1000 });
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await client.connect();
|
|
103
|
+
const result = await client.scan({
|
|
104
|
+
source: 'adf-duplex',
|
|
105
|
+
resolution: 600,
|
|
106
|
+
format: 'jpeg',
|
|
107
|
+
colorMode: 'color',
|
|
108
|
+
quality: 90,
|
|
109
|
+
area: { x: 0, y: 0, width: 215.9, height: 355.6 },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.pageCount).toEqual(2);
|
|
113
|
+
expect(result.pages?.[0]?.data.equals(firstPage)).toEqual(true);
|
|
114
|
+
expect(result.pages?.[1]?.data.equals(secondPage)).toEqual(true);
|
|
115
|
+
expect(result.width).toEqual(5052);
|
|
116
|
+
expect(result.height).toEqual(8400);
|
|
117
|
+
expect(result.resolution).toEqual(600);
|
|
118
|
+
expect(commands[0]).toEqual('\x1bS\nAUTO\n\x80');
|
|
119
|
+
expect(commands[1]).toContain('R=600,600');
|
|
120
|
+
expect(commands[2]).toContain('D=DUP');
|
|
121
|
+
expect(commands[2]).toContain('C=JPEG\nJ=MID');
|
|
122
|
+
expect(commands[2]).toContain('A=0,0,5052,8400');
|
|
123
|
+
expect(commands).toHaveLength(3);
|
|
124
|
+
} finally {
|
|
125
|
+
await client.disconnect();
|
|
126
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
tap.test('Brother native scan uses FB and bottom-right scan coordinates', async () => {
|
|
131
|
+
const commands: string[] = [];
|
|
132
|
+
const page = makeJpeg(1181, 1417, 300);
|
|
133
|
+
const server = net.createServer((socket) => {
|
|
134
|
+
socket.write('+OK 200\r\n');
|
|
135
|
+
readCommands(socket, (command) => {
|
|
136
|
+
const text = command.toString('latin1');
|
|
137
|
+
commands.push(text);
|
|
138
|
+
if (text.startsWith('\x1bS\n')) {
|
|
139
|
+
socket.write(Buffer.from([0x80]));
|
|
140
|
+
} else if (text.startsWith('\x1bI\n')) {
|
|
141
|
+
socket.write(Buffer.concat([
|
|
142
|
+
Buffer.from([0x1a, 0x00]),
|
|
143
|
+
Buffer.from('300,300,0,216,2551,356,4205', 'ascii'),
|
|
144
|
+
]));
|
|
145
|
+
} else if (text.startsWith('\x1bX\n')) {
|
|
146
|
+
socket.write(Buffer.concat([wrappedFrame(page), endOfImage(1), Buffer.from([0x80])]));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
151
|
+
const address = server.address() as net.AddressInfo;
|
|
152
|
+
const client = new BrotherScanProtocol('127.0.0.1', address.port, {
|
|
153
|
+
timeout: 1000,
|
|
154
|
+
scanTimeout: 1000,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await client.connect();
|
|
159
|
+
const result = await client.scan({
|
|
160
|
+
source: 'flatbed',
|
|
161
|
+
resolution: 300,
|
|
162
|
+
format: 'jpeg',
|
|
163
|
+
colorMode: 'color',
|
|
164
|
+
area: { x: 10, y: 20, width: 100, height: 120 },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result.pageCount).toEqual(1);
|
|
168
|
+
expect(commands[0]).toEqual('\x1bS\nFB\n\x80');
|
|
169
|
+
expect(commands[2]).toContain('A=118,236,1299,1653');
|
|
170
|
+
expect(commands[2]).not.toContain('U=OFF');
|
|
171
|
+
expect(commands[2]).not.toContain('P=OFF');
|
|
172
|
+
} finally {
|
|
173
|
+
await client.disconnect();
|
|
174
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
tap.test('Brother legacy page boundaries request the next page', async () => {
|
|
179
|
+
const commands: string[] = [];
|
|
180
|
+
const firstPage = makeJpeg(2480, 3507, 300);
|
|
181
|
+
const secondPage = makeJpeg(2480, 3507, 300);
|
|
182
|
+
const server = net.createServer((socket) => {
|
|
183
|
+
socket.write('+OK 200\r\n');
|
|
184
|
+
readCommands(socket, (command) => {
|
|
185
|
+
const text = command.toString('latin1');
|
|
186
|
+
commands.push(text);
|
|
187
|
+
if (text.startsWith('\x1bS\n')) {
|
|
188
|
+
socket.write(Buffer.from([0x80]));
|
|
189
|
+
} else if (text.startsWith('\x1bI\n')) {
|
|
190
|
+
socket.write(Buffer.concat([
|
|
191
|
+
Buffer.from([0x1a, 0x00]),
|
|
192
|
+
Buffer.from('300,300,2,210,2480,297,3507', 'ascii'),
|
|
193
|
+
]));
|
|
194
|
+
} else if (text.includes('C=JPEG')) {
|
|
195
|
+
socket.write(Buffer.concat([simpleFrame(firstPage), Buffer.from([0x81])]));
|
|
196
|
+
} else if (text === '\x1bX\n\x80') {
|
|
197
|
+
socket.write(Buffer.concat([simpleFrame(secondPage), Buffer.from([0x80])]));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
202
|
+
const address = server.address() as net.AddressInfo;
|
|
203
|
+
const client = new BrotherScanProtocol('127.0.0.1', address.port, {
|
|
204
|
+
timeout: 1000,
|
|
205
|
+
scanTimeout: 1000,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await client.connect();
|
|
210
|
+
const result = await client.scan({
|
|
211
|
+
source: 'adf', resolution: 300, format: 'jpeg', colorMode: 'color',
|
|
212
|
+
area: { x: 0, y: 0, width: 210, height: 297 },
|
|
213
|
+
});
|
|
214
|
+
expect(result.pageCount).toEqual(2);
|
|
215
|
+
expect(commands[3]).toEqual('\x1bX\n\x80');
|
|
216
|
+
} finally {
|
|
217
|
+
await client.disconnect();
|
|
218
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
tap.test('Brother native scan rejects a silently downgraded resolution', async () => {
|
|
223
|
+
const server = net.createServer((socket) => {
|
|
224
|
+
socket.write('+OK 200\r\n');
|
|
225
|
+
readCommands(socket, (command) => {
|
|
226
|
+
const text = command.toString('latin1');
|
|
227
|
+
if (text.startsWith('\x1bS\n')) socket.write(Buffer.from([0x80]));
|
|
228
|
+
if (text.startsWith('\x1bI\n')) {
|
|
229
|
+
socket.write(Buffer.concat([
|
|
230
|
+
Buffer.from([0x1a, 0x00]),
|
|
231
|
+
Buffer.from('300,300,2,210,2480,297,3507', 'ascii'),
|
|
232
|
+
]));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
237
|
+
const address = server.address() as net.AddressInfo;
|
|
238
|
+
const client = new BrotherScanProtocol('127.0.0.1', address.port, { timeout: 1000 });
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await client.connect();
|
|
242
|
+
let scanError: BrotherScanError | undefined;
|
|
243
|
+
try {
|
|
244
|
+
await client.scan({
|
|
245
|
+
source: 'adf', resolution: 600, format: 'jpeg', colorMode: 'color',
|
|
246
|
+
area: { x: 0, y: 0, width: 210, height: 297 },
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof BrotherScanError) scanError = error;
|
|
250
|
+
}
|
|
251
|
+
expect(scanError?.code).toEqual('settings-rejected');
|
|
252
|
+
expect(scanError?.message).toContain('offered 300x300 dpi');
|
|
253
|
+
} finally {
|
|
254
|
+
await client.disconnect();
|
|
255
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
tap.test('Brother native scan retries a temporarily busy device', async () => {
|
|
260
|
+
let connections = 0;
|
|
261
|
+
const server = net.createServer((socket) => {
|
|
262
|
+
connections++;
|
|
263
|
+
if (connections < 3) socket.end('-NG 401\r\n');
|
|
264
|
+
else socket.write('+OK 200\r\n');
|
|
265
|
+
});
|
|
266
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
267
|
+
const address = server.address() as net.AddressInfo;
|
|
268
|
+
const client = new BrotherScanProtocol('127.0.0.1', address.port, {
|
|
269
|
+
timeout: 1000,
|
|
270
|
+
busyRetryInterval: 5,
|
|
271
|
+
busyTimeout: 100,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await client.connect();
|
|
276
|
+
expect(connections).toEqual(3);
|
|
277
|
+
} finally {
|
|
278
|
+
await client.disconnect();
|
|
279
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
tap.test('Brother native scan reports a persistently occupied device as busy', async () => {
|
|
284
|
+
const server = net.createServer((socket) => socket.end('-NG 401\r\n'));
|
|
285
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
286
|
+
const address = server.address() as net.AddressInfo;
|
|
287
|
+
const client = new BrotherScanProtocol('127.0.0.1', address.port, {
|
|
288
|
+
timeout: 1000,
|
|
289
|
+
busyTimeout: 0,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
let connectError: BrotherScanError | undefined;
|
|
294
|
+
try {
|
|
295
|
+
await client.connect();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof BrotherScanError) connectError = error;
|
|
298
|
+
}
|
|
299
|
+
expect(connectError?.code).toEqual('busy');
|
|
300
|
+
} finally {
|
|
301
|
+
await client.disconnect();
|
|
302
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
export default tap.start();
|