@flux-control/effect-modbus-rs 0.3.0 → 0.4.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/README.md +74 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +114 -118
- package/dist/src/AsciiTransportService.d.ts +31 -22
- package/dist/src/RtuTransportService.d.ts +31 -22
- package/dist/src/SerialTransportService.d.ts +17 -7
- package/dist/src/TcpTransportService.d.ts +31 -22
- package/dist/src/WasmAsciiTransportService.d.ts +30 -21
- package/dist/src/WasmRtuTransportService.d.ts +30 -21
- package/dist/src/WasmSerialPort.d.ts +2 -2
- package/dist/src/WasmSerialTransportService.d.ts +20 -10
- package/dist/src/WasmWsTransportService.d.ts +30 -21
- package/dist/src/connection.d.ts +9 -9
- package/dist/src/mock-options.test.d.ts +1 -0
- package/dist/src/mocks.d.ts +41 -57
- package/dist/src/retry.d.ts +11 -8
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
For the complete API reference, see the [GitHub Pages documentation](https://flux-control-solutions.github.io/Effect-modbus-rs/).
|
|
6
6
|
|
|
7
|
-
Provides scoped [`
|
|
7
|
+
Provides scoped [`Context.Service`](https://effect.website) constructors for RTU (serial), TCP, and ASCII Modbus transports. Clients expose a typed `Effect`-based API for all standard Modbus function codes.
|
|
8
8
|
|
|
9
9
|
> This project is under active development. Its API may change before the 1.0 release.
|
|
10
10
|
|
|
@@ -44,7 +44,7 @@ program.pipe(
|
|
|
44
44
|
ModbusInvalidArgumentError: (err) => Console.log(`Invalid argument: ${err.message}`),
|
|
45
45
|
}),
|
|
46
46
|
Effect.catchAll((err) => Console.log(`Unhandled error: ${err.message}`)),
|
|
47
|
-
Effect.provide(RtuTransportService.
|
|
47
|
+
Effect.provide(RtuTransportService.make({ portPath: '/dev/ttyUSB0', baudRate: 9600 })),
|
|
48
48
|
Effect.scoped,
|
|
49
49
|
Effect.runPromise,
|
|
50
50
|
);
|
|
@@ -64,7 +64,7 @@ const program = Effect.gen(function* () {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
program.pipe(
|
|
67
|
-
Effect.provide(TcpTransportService.
|
|
67
|
+
Effect.provide(TcpTransportService.make({ host: '192.168.1.100', port: 502 })),
|
|
68
68
|
Effect.scoped,
|
|
69
69
|
Effect.runPromise,
|
|
70
70
|
);
|
|
@@ -87,7 +87,7 @@ const program = Effect.gen(function* () {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
program.pipe(
|
|
90
|
-
Effect.provide(AsciiTransportService.
|
|
90
|
+
Effect.provide(AsciiTransportService.make({ portPath: '/dev/ttyUSB0', baudRate: 9600 })),
|
|
91
91
|
Effect.scoped,
|
|
92
92
|
Effect.runPromise,
|
|
93
93
|
);
|
|
@@ -112,7 +112,7 @@ const program = Effect.gen(function* () {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
program.pipe(
|
|
115
|
-
Effect.provide(WasmWsTransportService.
|
|
115
|
+
Effect.provide(WasmWsTransportService.make({ wsUrl: 'ws://localhost:8080' })),
|
|
116
116
|
Effect.scoped,
|
|
117
117
|
Effect.runPromise,
|
|
118
118
|
);
|
|
@@ -129,7 +129,7 @@ connectButton.addEventListener('click', () => {
|
|
|
129
129
|
Effect.gen(function* () {
|
|
130
130
|
const port = yield* requestSerialPort();
|
|
131
131
|
yield* program.pipe(
|
|
132
|
-
Effect.provide(WasmRtuTransportService.
|
|
132
|
+
Effect.provide(WasmRtuTransportService.make({ port, baudRate: 19200 })),
|
|
133
133
|
Effect.scoped,
|
|
134
134
|
);
|
|
135
135
|
}),
|
|
@@ -150,7 +150,7 @@ Not demonstrated in `examples/wasm/` (see that app's README) — the same `impor
|
|
|
150
150
|
|
|
151
151
|
## Transports
|
|
152
152
|
|
|
153
|
-
Each transport is a scoped `
|
|
153
|
+
Each transport is a scoped `Context.Service`. You provide it with `Effect.provide`, and the connection is opened on service access and closed when the scope ends.
|
|
154
154
|
|
|
155
155
|
| Service | Options | Connection |
|
|
156
156
|
| ------------------------------------- | ------------------------------ | ----------------------------- |
|
|
@@ -193,7 +193,32 @@ program.pipe(
|
|
|
193
193
|
// );
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
`WasmSerialTransportService` is the browser equivalent. Provide it with `fromRtu` or `fromAscii`, and give it a port handle from `requestSerialPort`.
|
|
197
|
+
|
|
198
|
+
Both abstract tags also have `makeMockTransport` for tests. The option set is the same as the option set of the concrete tags. Thus a test that keeps the framing abstract can also set `retry`, `reconnect`, and the mock fault hooks:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import {
|
|
202
|
+
ModbusTimeoutError,
|
|
203
|
+
RetryPolicies,
|
|
204
|
+
SerialTransportService,
|
|
205
|
+
} from '@flux-control/effect-modbus-rs';
|
|
206
|
+
|
|
207
|
+
let attempts = 0;
|
|
208
|
+
|
|
209
|
+
const layer = SerialTransportService.makeMockTransport([device])({
|
|
210
|
+
portPath: '/dev/ttyUSB0',
|
|
211
|
+
baudRate: 9600,
|
|
212
|
+
retry: RetryPolicies.serial(),
|
|
213
|
+
// The first two attempts of each operation fail. The policy retries them.
|
|
214
|
+
fault: () =>
|
|
215
|
+
attempts++ < 2
|
|
216
|
+
? new ModbusTimeoutError({ message: 'no response', cause: new Error('timeout') })
|
|
217
|
+
: undefined,
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [Testing with mocks](#testing-with-mocks) for the `fault` hook and the `reconnectFault` hook.
|
|
197
222
|
|
|
198
223
|
## Client API
|
|
199
224
|
|
|
@@ -254,7 +279,7 @@ Handle with `Effect.catchTags`. The `ModbusError` union type covers all seven va
|
|
|
254
279
|
Resilience belongs to the **transport**, not to call sites. Attach a policy where the transport is created and every client derived from it carries it:
|
|
255
280
|
|
|
256
281
|
```ts
|
|
257
|
-
const layer = TcpTransportService.
|
|
282
|
+
const layer = TcpTransportService.make({
|
|
258
283
|
host: '192.168.1.50',
|
|
259
284
|
port: 502,
|
|
260
285
|
retry: RetryPolicies.tcp(), // applied to every operation
|
|
@@ -368,7 +393,7 @@ So `RetryPolicies.tcp()` waits roughly 80–120 ms before its first retry, not e
|
|
|
368
393
|
Passing `reconnect` hands reconnection to a supervisor fiber owned by the transport — **one reconnect for the whole application**, however many fibers were in flight when the link dropped:
|
|
369
394
|
|
|
370
395
|
```ts
|
|
371
|
-
TcpTransportService.
|
|
396
|
+
TcpTransportService.make({
|
|
372
397
|
host,
|
|
373
398
|
port,
|
|
374
399
|
reconnect: {
|
|
@@ -420,7 +445,7 @@ See `examples/retry-policies.ts` for a runnable walkthrough.
|
|
|
420
445
|
`modbus-rs` exposes `retryAttempts`, `retryDelayMs`, and `retryBackoffStrategy` on its transport options. This package removes all three from every transport constructor, so setting one is a compile error rather than a documented hazard:
|
|
421
446
|
|
|
422
447
|
```ts
|
|
423
|
-
TcpTransportService.
|
|
448
|
+
TcpTransportService.make({ host, port, retryAttempts: 3 });
|
|
424
449
|
// ^^^^^^^^^^^^^ Object literal may only specify
|
|
425
450
|
// known properties, and 'retryAttempts' does not
|
|
426
451
|
// exist in type 'TcpTransportOpenOptions & …'
|
|
@@ -475,10 +500,41 @@ const mockLayer = RtuTransportService.makeMockTransport([device])({
|
|
|
475
500
|
program.pipe(Effect.provide(mockLayer), Effect.scoped, Effect.runPromise);
|
|
476
501
|
```
|
|
477
502
|
|
|
478
|
-
The mock factory is
|
|
503
|
+
The mock factory is the same for every transport. Each tag has a static `makeMockTransport` method, and each accepts the same options: the open options of that transport, the resilience options (`retry` and `reconnect`), and the two fault hooks below. To change transport, use a different tag and adjust the shape of the open options.
|
|
479
504
|
|
|
480
505
|
See `examples/rtu-mock.ts`, `examples/tcp-mock.ts`, and `examples/ascii-mock.ts` for full walkthroughs covering read, write, multi-device access, and error-case testing.
|
|
481
506
|
|
|
507
|
+
### Fault injection
|
|
508
|
+
|
|
509
|
+
Two mock-only hooks make a policy testable without hardware:
|
|
510
|
+
|
|
511
|
+
| Hook | When it runs | Return value |
|
|
512
|
+
| ---------------- | ------------------------------ | ---------------------------------------------------------------------------- |
|
|
513
|
+
| `fault` | Before every operation attempt | A `ModbusError` fails that attempt. `undefined` lets it through. |
|
|
514
|
+
| `reconnectFault` | Before every reconnect attempt | A `ModbusError` keeps the link down. `undefined` lets the reconnect succeed. |
|
|
515
|
+
|
|
516
|
+
Because `fault` runs before each _attempt_, an error from it is the same as a device that refused that attempt. A retry policy, the backoff, and the circuit breaker therefore behave as they do on a real bus:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import {
|
|
520
|
+
ModbusTimeoutError,
|
|
521
|
+
RetryPolicies,
|
|
522
|
+
RtuTransportService,
|
|
523
|
+
} from '@flux-control/effect-modbus-rs';
|
|
524
|
+
|
|
525
|
+
let attempts = 0;
|
|
526
|
+
|
|
527
|
+
const mockLayer = RtuTransportService.makeMockTransport([device])({
|
|
528
|
+
portPath: '/dev/ttyUSB0',
|
|
529
|
+
baudRate: 9600,
|
|
530
|
+
retry: RetryPolicies.serial(),
|
|
531
|
+
fault: () =>
|
|
532
|
+
attempts++ < 2
|
|
533
|
+
? new ModbusTimeoutError({ message: 'no response', cause: new Error('timeout') })
|
|
534
|
+
: undefined,
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
482
538
|
### Slave device schema
|
|
483
539
|
|
|
484
540
|
| Property | Type | Default |
|
|
@@ -512,17 +568,17 @@ src/
|
|
|
512
568
|
connection.ts — Connection state machine, reconnect supervisor, circuit breaker
|
|
513
569
|
retry.ts — Opt-in retry policies (backoff, jitter, per-error rules)
|
|
514
570
|
shared-transport.ts — Generic scoped transport lifecycle management, WithoutUpstreamRetry
|
|
515
|
-
RtuTransportService.ts — Scoped
|
|
516
|
-
TcpTransportService.ts — Scoped
|
|
517
|
-
AsciiTransportService.ts — Scoped
|
|
571
|
+
RtuTransportService.ts — Scoped Context.Service wrapping AsyncRtuTransport
|
|
572
|
+
TcpTransportService.ts — Scoped Context.Service wrapping AsyncTcpTransport
|
|
573
|
+
AsciiTransportService.ts — Scoped Context.Service wrapping AsyncAsciiTransport
|
|
518
574
|
SerialTransportService.ts — Abstract serial transport (RTU/ASCII) tag
|
|
519
575
|
TcpModbusServerService.ts — tcpServerLayer
|
|
520
576
|
SerialModbusServerService.ts — serialRtuServerLayer / serialAsciiServerLayer
|
|
521
577
|
TcpGatewayService.ts — tcpGatewayLayer
|
|
522
578
|
WasmSerialPort.ts — requestSerialPort() Effect helper (browser, user-gesture gated)
|
|
523
|
-
WasmWsTransportService.ts — Scoped
|
|
524
|
-
WasmRtuTransportService.ts — Scoped
|
|
525
|
-
WasmAsciiTransportService.ts — Scoped
|
|
579
|
+
WasmWsTransportService.ts — Scoped Context.Service wrapping WasmWsTransport (browser, WS gateway)
|
|
580
|
+
WasmRtuTransportService.ts — Scoped Context.Service wrapping WasmRtuTransport (browser, Web Serial RTU)
|
|
581
|
+
WasmAsciiTransportService.ts — Scoped Context.Service wrapping WasmAsciiTransport (browser, Web Serial ASCII)
|
|
526
582
|
WasmSerialTransportService.ts — Abstract browser serial transport (RTU/ASCII) tag
|
|
527
583
|
WasmTcpServerService.ts — wasmWsServerLayer (experimental)
|
|
528
584
|
WasmSerialModbusServerService.ts — wasmSerialRtuServerLayer / wasmSerialAsciiServerLayer (experimental)
|
package/dist/index.d.ts
CHANGED