@flux-control/effect-modbus-rs 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/dist/index.js ADDED
@@ -0,0 +1,607 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/errors.ts
5
+ import { Data } from "effect";
6
+ import { getModbusErrorCode, ModbusErrorCode } from "modbus-rs";
7
+
8
+ class ModbusExceptionError extends Data.TaggedError("ModbusExceptionError") {
9
+ }
10
+
11
+ class ModbusTimeoutError extends Data.TaggedError("ModbusTimeoutError") {
12
+ }
13
+
14
+ class ModbusTransportError extends Data.TaggedError("ModbusTransportError") {
15
+ }
16
+
17
+ class ModbusInvalidArgumentError extends Data.TaggedError("ModbusInvalidArgumentError") {
18
+ }
19
+
20
+ class ModbusConnectionClosedError extends Data.TaggedError("ModbusConnectionClosedError") {
21
+ }
22
+
23
+ class ModbusInternalError extends Data.TaggedError("ModbusInternalError") {
24
+ }
25
+
26
+ class ModbusNotConnectedError extends Data.TaggedError("ModbusNotConnectedError") {
27
+ }
28
+ function parseExceptionCode(message) {
29
+ const m = message.match(/\[MODBUS_EXCEPTION:(\d+)\]/);
30
+ return m ? Number(m[1]) : undefined;
31
+ }
32
+ var toModbusError = (cause) => {
33
+ const code = getModbusErrorCode(cause);
34
+ const message = cause.message;
35
+ switch (code) {
36
+ case ModbusErrorCode.EXCEPTION:
37
+ return new ModbusExceptionError({
38
+ cause,
39
+ exception: parseExceptionCode(message) ?? 0,
40
+ message
41
+ });
42
+ case ModbusErrorCode.TIMEOUT:
43
+ return new ModbusTimeoutError({ cause, message });
44
+ case ModbusErrorCode.TRANSPORT:
45
+ return new ModbusTransportError({ cause, message });
46
+ case ModbusErrorCode.INVALID_ARGUMENT:
47
+ return new ModbusInvalidArgumentError({ cause, message });
48
+ case ModbusErrorCode.CONNECTION_CLOSED:
49
+ return new ModbusConnectionClosedError({ cause, message });
50
+ default:
51
+ return new ModbusInternalError({ cause, message });
52
+ }
53
+ };
54
+ // src/AsciiTransportService.ts
55
+ import { Effect as Effect4, Layer } from "effect";
56
+
57
+ // src/shared-transport.ts
58
+ import { Effect as Effect2, Exit, Scope } from "effect";
59
+
60
+ // src/modbus-client.ts
61
+ import { Effect } from "effect";
62
+ var makeEffectModbusClient = (client) => ({
63
+ readHoldingRegisters: (opts) => Effect.tryPromise({
64
+ try: () => client.readHoldingRegisters(opts),
65
+ catch: (error) => toModbusError(error)
66
+ }),
67
+ readInputRegisters: (opts) => Effect.tryPromise({
68
+ try: () => client.readInputRegisters(opts),
69
+ catch: (error) => toModbusError(error)
70
+ }),
71
+ writeSingleRegister: (opts) => Effect.tryPromise({
72
+ try: () => client.writeSingleRegister(opts),
73
+ catch: (error) => toModbusError(error)
74
+ }),
75
+ writeMultipleRegisters: (opts) => Effect.tryPromise({
76
+ try: () => client.writeMultipleRegisters(opts),
77
+ catch: (error) => toModbusError(error)
78
+ }),
79
+ readWriteMultipleRegisters: (opts) => Effect.tryPromise({
80
+ try: () => client.readWriteMultipleRegisters(opts),
81
+ catch: (error) => toModbusError(error)
82
+ }),
83
+ readCoils: (opts) => Effect.tryPromise({
84
+ try: () => client.readCoils(opts),
85
+ catch: (error) => toModbusError(error)
86
+ }),
87
+ writeSingleCoil: (opts) => Effect.tryPromise({
88
+ try: () => client.writeSingleCoil(opts),
89
+ catch: (error) => toModbusError(error)
90
+ }),
91
+ writeMultipleCoils: (opts) => Effect.tryPromise({
92
+ try: () => client.writeMultipleCoils(opts),
93
+ catch: (error) => toModbusError(error)
94
+ }),
95
+ readDiscreteInputs: (opts) => Effect.tryPromise({
96
+ try: () => client.readDiscreteInputs(opts),
97
+ catch: (error) => toModbusError(error)
98
+ }),
99
+ readFifoQueue: (opts) => Effect.tryPromise({
100
+ try: () => client.readFifoQueue(opts),
101
+ catch: (error) => toModbusError(error)
102
+ }),
103
+ readFileRecord: (opts) => Effect.tryPromise({
104
+ try: () => client.readFileRecord(opts),
105
+ catch: (error) => toModbusError(error)
106
+ }),
107
+ writeFileRecord: (opts) => Effect.tryPromise({
108
+ try: () => client.writeFileRecord(opts),
109
+ catch: (error) => toModbusError(error)
110
+ }),
111
+ readExceptionStatus: () => Effect.tryPromise({
112
+ try: () => client.readExceptionStatus(),
113
+ catch: (error) => toModbusError(error)
114
+ }),
115
+ diagnostics: (opts) => Effect.tryPromise({
116
+ try: () => client.diagnostics(opts),
117
+ catch: (error) => toModbusError(error)
118
+ }),
119
+ readDeviceIdentification: (opts) => Effect.tryPromise({
120
+ try: () => client.readDeviceIdentification(opts),
121
+ catch: (error) => toModbusError(error)
122
+ })
123
+ });
124
+
125
+ // src/shared-transport.ts
126
+ function makeTransportScoped(transportKey, openMethod, serviceName) {
127
+ return Effect2.fnUntraced(function* (options) {
128
+ const mod = yield* Effect2.promise(() => import("modbus-rs"));
129
+ const TC = mod[transportKey];
130
+ let transport = null;
131
+ let connectPromise = null;
132
+ let reconnectPromise = null;
133
+ const clientSet = new Map;
134
+ let closed = false;
135
+ const ensureOpen = Effect2.fnUntraced(function* () {
136
+ if (transport) {
137
+ if (closed) {
138
+ return yield* new ModbusNotConnectedError({
139
+ cause: new Error("Transport has been closed"),
140
+ message: "Transport has been closed"
141
+ });
142
+ }
143
+ return transport;
144
+ }
145
+ if (closed) {
146
+ return yield* new ModbusNotConnectedError({
147
+ cause: new Error("Transport has been closed"),
148
+ message: "Transport has been closed"
149
+ });
150
+ }
151
+ if (!connectPromise) {
152
+ connectPromise = openMethod(TC, options);
153
+ }
154
+ const t = yield* Effect2.tryPromise({
155
+ try: () => connectPromise,
156
+ catch: (error) => toModbusError(error)
157
+ }).pipe(Effect2.catchAll((err) => {
158
+ connectPromise = null;
159
+ return Effect2.fail(err);
160
+ }));
161
+ if (closed) {
162
+ connectPromise = null;
163
+ yield* Effect2.fork(Effect2.promise(() => t.close()).pipe(Effect2.catchAll(() => Effect2.void)));
164
+ return yield* new ModbusNotConnectedError({
165
+ cause: new Error("Transport has been closed"),
166
+ message: "Transport has been closed"
167
+ });
168
+ }
169
+ transport = t;
170
+ return t;
171
+ });
172
+ yield* Effect2.addFinalizer(() => {
173
+ if (closed)
174
+ return Effect2.void;
175
+ closed = true;
176
+ const t = transport;
177
+ if (!t)
178
+ return Effect2.void;
179
+ return Effect2.andThen(Effect2.logDebug(`Closing ${serviceName}`), Effect2.promise(() => t.close()));
180
+ });
181
+ const notConnectedMsg = "Transport is not connected. Call withClient() first.";
182
+ return {
183
+ withClient: Effect2.fnUntraced(function* (unitId) {
184
+ const t = yield* ensureOpen();
185
+ let client = clientSet.get(unitId);
186
+ if (!client) {
187
+ client = yield* Effect2.try({
188
+ try: () => t.createClient({ unitId }),
189
+ catch: (error) => toModbusError(error)
190
+ });
191
+ clientSet.set(unitId, client);
192
+ }
193
+ return makeEffectModbusClient(client);
194
+ }),
195
+ setRequestTimeout: Effect2.fnUntraced(function* (timeoutMs) {
196
+ const t = transport;
197
+ if (!t || closed) {
198
+ return yield* new ModbusNotConnectedError({
199
+ cause: new Error(notConnectedMsg),
200
+ message: notConnectedMsg
201
+ });
202
+ }
203
+ t.setRequestTimeout(timeoutMs);
204
+ }),
205
+ clearRequestTimeout: Effect2.fnUntraced(function* () {
206
+ const t = transport;
207
+ if (!t || closed) {
208
+ return yield* new ModbusNotConnectedError({
209
+ cause: new Error(notConnectedMsg),
210
+ message: notConnectedMsg
211
+ });
212
+ }
213
+ t.clearRequestTimeout();
214
+ }),
215
+ reconnect: Effect2.fnUntraced(function* () {
216
+ if (closed) {
217
+ return yield* new ModbusNotConnectedError({
218
+ cause: new Error("Transport has been closed"),
219
+ message: "Transport has been closed"
220
+ });
221
+ }
222
+ if (transport) {
223
+ if (!reconnectPromise) {
224
+ reconnectPromise = transport.reconnect().then(() => {
225
+ reconnectPromise = null;
226
+ }).catch((err) => {
227
+ reconnectPromise = null;
228
+ throw err;
229
+ });
230
+ }
231
+ yield* Effect2.tryPromise({
232
+ try: () => reconnectPromise,
233
+ catch: (error) => toModbusError(error)
234
+ });
235
+ } else {
236
+ yield* ensureOpen();
237
+ }
238
+ }),
239
+ close: Effect2.fnUntraced(function* () {
240
+ if (closed)
241
+ return;
242
+ closed = true;
243
+ const t = transport;
244
+ if (t) {
245
+ yield* Effect2.tryPromise({
246
+ try: () => t.close(),
247
+ catch: (error) => toModbusError(error)
248
+ });
249
+ }
250
+ const scope = yield* Effect2.scope;
251
+ yield* Scope.close(scope, Exit.void);
252
+ }),
253
+ hasPendingRequests: () => {
254
+ if (closed)
255
+ return false;
256
+ const t = transport;
257
+ if (!t)
258
+ return false;
259
+ return t.pendingRequests;
260
+ }
261
+ };
262
+ });
263
+ }
264
+
265
+ // src/mocks.ts
266
+ import { Effect as Effect3, Schema } from "effect";
267
+ var CoilDefinition = Schema.Struct({
268
+ address: Schema.Number,
269
+ default: Schema.Boolean
270
+ });
271
+ var DiscreteInputDefinition = Schema.Struct({
272
+ address: Schema.Number,
273
+ default: Schema.Boolean
274
+ });
275
+ var RegisterDefinition = Schema.Struct({
276
+ address: Schema.Number,
277
+ default: Schema.Number
278
+ });
279
+ var SlaveDeviceDefinition = Schema.Struct({
280
+ unitId: Schema.Number,
281
+ coils: Schema.optionalWith(Schema.Array(CoilDefinition), {
282
+ default: () => []
283
+ }),
284
+ discreteInputs: Schema.optionalWith(Schema.Array(DiscreteInputDefinition), {
285
+ default: () => []
286
+ }),
287
+ holdingRegisters: Schema.optionalWith(Schema.Array(RegisterDefinition), {
288
+ default: () => []
289
+ }),
290
+ inputRegisters: Schema.optionalWith(Schema.Array(RegisterDefinition), {
291
+ default: () => []
292
+ })
293
+ });
294
+ var SlaveDeviceDefinitions = Schema.Array(SlaveDeviceDefinition);
295
+ var buildCoils = (defs) => {
296
+ const map = new Map;
297
+ let max = -1;
298
+ for (const d of defs) {
299
+ map.set(d.address, d.default);
300
+ if (d.address > max)
301
+ max = d.address;
302
+ }
303
+ return { map, maxAddress: max };
304
+ };
305
+ var buildRegisters = (defs) => {
306
+ const map = new Map;
307
+ let max = -1;
308
+ for (const d of defs) {
309
+ map.set(d.address, d.default);
310
+ if (d.address > max)
311
+ max = d.address;
312
+ }
313
+ return { map, maxAddress: max };
314
+ };
315
+ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentError({
316
+ cause: new Error(quantity !== undefined ? `${label} address ${address} with quantity ${quantity} out of range` : `${label} address ${address} out of range`),
317
+ message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
318
+ });
319
+ var makeMockModbusClient = (state, unitId) => ({
320
+ readCoils: Effect3.fnUntraced(function* (opts) {
321
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
322
+ if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
323
+ return yield* failOutOfRange("Coil", opts.address, opts.quantity);
324
+ }
325
+ const result = [];
326
+ for (let i = opts.address;i < opts.address + opts.quantity; i++) {
327
+ result.push(state.coils.get(i) ?? false);
328
+ }
329
+ return result;
330
+ }),
331
+ readDiscreteInputs: Effect3.fnUntraced(function* (opts) {
332
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
333
+ if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
334
+ return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
335
+ }
336
+ const result = [];
337
+ for (let i = opts.address;i < opts.address + opts.quantity; i++) {
338
+ result.push(state.discreteInputs.get(i) ?? false);
339
+ }
340
+ return result;
341
+ }),
342
+ readHoldingRegisters: Effect3.fnUntraced(function* (opts) {
343
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
344
+ if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
345
+ return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
346
+ }
347
+ const result = [];
348
+ for (let i = opts.address;i < opts.address + opts.quantity; i++) {
349
+ result.push(state.holdingRegisters.get(i) ?? 0);
350
+ }
351
+ return result;
352
+ }),
353
+ readInputRegisters: Effect3.fnUntraced(function* (opts) {
354
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
355
+ if (opts.address + opts.quantity > state.maxInputAddress + 1) {
356
+ return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
357
+ }
358
+ const result = [];
359
+ for (let i = opts.address;i < opts.address + opts.quantity; i++) {
360
+ result.push(state.inputRegisters.get(i) ?? 0);
361
+ }
362
+ return result;
363
+ }),
364
+ writeSingleCoil: Effect3.fnUntraced(function* (opts) {
365
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
366
+ if (opts.address > state.maxCoilAddress) {
367
+ return yield* failOutOfRange("Coil", opts.address);
368
+ }
369
+ state.coils.set(opts.address, opts.value);
370
+ }),
371
+ writeMultipleCoils: Effect3.fnUntraced(function* (opts) {
372
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
373
+ if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
374
+ return yield* failOutOfRange("Coil", opts.address, opts.values.length);
375
+ }
376
+ for (let i = 0;i < opts.values.length; i++) {
377
+ state.coils.set(opts.address + i, opts.values[i]);
378
+ }
379
+ }),
380
+ writeSingleRegister: Effect3.fnUntraced(function* (opts) {
381
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
382
+ if (opts.address > state.maxHoldingAddress) {
383
+ return yield* failOutOfRange("HoldingRegister", opts.address);
384
+ }
385
+ state.holdingRegisters.set(opts.address, opts.value);
386
+ }),
387
+ writeMultipleRegisters: Effect3.fnUntraced(function* (opts) {
388
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
389
+ if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
390
+ return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
391
+ }
392
+ for (let i = 0;i < opts.values.length; i++) {
393
+ state.holdingRegisters.set(opts.address + i, opts.values[i]);
394
+ }
395
+ }),
396
+ readWriteMultipleRegisters: Effect3.fnUntraced(function* (opts) {
397
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
398
+ if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
399
+ return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
400
+ }
401
+ if (opts.readAddress + opts.readQuantity > state.maxHoldingAddress + 1) {
402
+ return yield* failOutOfRange("HoldingRegister", opts.readAddress, opts.readQuantity);
403
+ }
404
+ for (let i = 0;i < opts.writeValues.length; i++) {
405
+ state.holdingRegisters.set(opts.writeAddress + i, opts.writeValues[i]);
406
+ }
407
+ const result = [];
408
+ for (let i = opts.readAddress;i < opts.readAddress + opts.readQuantity; i++) {
409
+ result.push(state.holdingRegisters.get(i) ?? 0);
410
+ }
411
+ return result;
412
+ }),
413
+ readFifoQueue: Effect3.fnUntraced(function* (_opts) {
414
+ return yield* new ModbusInvalidArgumentError({
415
+ cause: new Error("FIFO queue not yet supported in mock"),
416
+ message: "FIFO queue not yet supported in mock"
417
+ });
418
+ }),
419
+ readFileRecord: Effect3.fnUntraced(function* (_opts) {
420
+ return yield* new ModbusInvalidArgumentError({
421
+ cause: new Error("File records not yet supported in mock"),
422
+ message: "File records not yet supported in mock"
423
+ });
424
+ }),
425
+ writeFileRecord: Effect3.fnUntraced(function* (_opts) {
426
+ return yield* new ModbusInvalidArgumentError({
427
+ cause: new Error("File records not yet supported in mock"),
428
+ message: "File records not yet supported in mock"
429
+ });
430
+ }),
431
+ readExceptionStatus: Effect3.fnUntraced(function* () {
432
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
433
+ return 0;
434
+ }),
435
+ diagnostics: Effect3.fnUntraced(function* (opts) {
436
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
437
+ return { subFunction: opts.subFunction, data: [] };
438
+ }),
439
+ readDeviceIdentification: Effect3.fnUntraced(function* (opts) {
440
+ yield* Effect3.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
441
+ return {
442
+ conformityLevel: 1,
443
+ moreFollows: false,
444
+ nextObjectId: 0,
445
+ objects: []
446
+ };
447
+ })
448
+ });
449
+ var makeMockTransport = (devices) => {
450
+ const deviceDefs = Schema.decodeUnknownSync(SlaveDeviceDefinitions)(devices);
451
+ const deviceStates = new Map;
452
+ for (const def of deviceDefs) {
453
+ const coils = buildCoils(def.coils);
454
+ const discrete = buildCoils(def.discreteInputs);
455
+ const holding = buildRegisters(def.holdingRegisters);
456
+ const input = buildRegisters(def.inputRegisters);
457
+ deviceStates.set(def.unitId, {
458
+ coils: coils.map,
459
+ discreteInputs: discrete.map,
460
+ holdingRegisters: holding.map,
461
+ inputRegisters: input.map,
462
+ maxCoilAddress: coils.maxAddress,
463
+ maxDiscreteAddress: discrete.maxAddress,
464
+ maxHoldingAddress: holding.maxAddress,
465
+ maxInputAddress: input.maxAddress
466
+ });
467
+ }
468
+ return (_options) => Effect3.gen(function* () {
469
+ yield* Effect3.logDebug("Mock transport opened with devices:", deviceDefs);
470
+ return {
471
+ withClient: Effect3.fnUntraced(function* (unitId) {
472
+ const state = deviceStates.get(unitId);
473
+ if (!state) {
474
+ return yield* new ModbusInvalidArgumentError({
475
+ cause: new Error(`Device with unitId ${unitId} not found in mock configuration`),
476
+ message: `Device with unitId ${unitId} not found in mock configuration`
477
+ });
478
+ }
479
+ return makeMockModbusClient(state, unitId);
480
+ }),
481
+ setRequestTimeout: (_timeoutMs) => Effect3.void,
482
+ clearRequestTimeout: () => Effect3.void,
483
+ reconnect: () => Effect3.asVoid(Effect3.logDebug("Mock: reconnecting")),
484
+ close: () => Effect3.logDebug("Mock: closing transport"),
485
+ hasPendingRequests: () => false
486
+ };
487
+ });
488
+ };
489
+
490
+ // src/AsciiTransportService.ts
491
+ class AsciiTransportService extends Effect4.Service()("AsciiTransportService", {
492
+ scoped: makeTransportScoped("AsyncAsciiTransport", (TC, options) => TC.open(options), "AsciiTransportService")
493
+ }) {
494
+ static makeMockTransport = (devices) => {
495
+ const factory = makeMockTransport(devices);
496
+ return (options) => Layer.scoped(AsciiTransportService, factory(options));
497
+ };
498
+ }
499
+ // src/SerialTransportService.ts
500
+ import { Context, Layer as Layer3 } from "effect";
501
+
502
+ // src/RtuTransportService.ts
503
+ import { Effect as Effect5, Layer as Layer2 } from "effect";
504
+ class RtuTransportService extends Effect5.Service()("RtuTransportService", {
505
+ scoped: makeTransportScoped("AsyncRtuTransport", (TC, options) => TC.open(options), "RtuTransportService")
506
+ }) {
507
+ static makeMockTransport = (devices) => {
508
+ const factory = makeMockTransport(devices);
509
+ return (options) => Layer2.scoped(RtuTransportService, factory(options));
510
+ };
511
+ }
512
+
513
+ // src/SerialTransportService.ts
514
+ class SerialTransportService extends Context.Tag("SerialTransportService")() {
515
+ static fromAscii(options) {
516
+ return Layer3.project(AsciiTransportService, SerialTransportService, (ascii) => ascii)(AsciiTransportService.Default(options));
517
+ }
518
+ static fromRtu(options) {
519
+ return Layer3.project(RtuTransportService, SerialTransportService, (rtu) => rtu)(RtuTransportService.Default(options));
520
+ }
521
+ static makeMockTransport = (devices) => {
522
+ const factory = makeMockTransport(devices);
523
+ return (options) => Layer3.scoped(SerialTransportService, factory(options));
524
+ };
525
+ }
526
+ // src/TcpTransportService.ts
527
+ import { Effect as Effect7, Layer as Layer4 } from "effect";
528
+ class TcpTransportService extends Effect7.Service()("TcpTransportService", {
529
+ scoped: makeTransportScoped("AsyncTcpTransport", (TC, options) => TC.connect(options), "TcpTransportService")
530
+ }) {
531
+ static makeMockTransport = (devices) => {
532
+ const factory = makeMockTransport(devices);
533
+ return (options) => Layer4.scoped(TcpTransportService, factory(options));
534
+ };
535
+ }
536
+ // src/SerialModbusServerService.ts
537
+ import { Effect as Effect8, Layer as Layer5 } from "effect";
538
+ var serialRtuServerLayer = (options, handlers) => Layer5.scopedDiscard(Effect8.gen(function* () {
539
+ const { AsyncSerialModbusServer } = yield* Effect8.promise(() => import("modbus-rs"));
540
+ const server = yield* Effect8.tryPromise({
541
+ try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
542
+ catch: (error) => toModbusError(error)
543
+ });
544
+ yield* Effect8.logDebug(`Serial RTU server bound to ${options.portPath}`);
545
+ yield* Effect8.addFinalizer(() => Effect8.logDebug("Serial RTU server shutting down").pipe(Effect8.andThen(Effect8.tryPromise({
546
+ try: () => server.shutdown(),
547
+ catch: (error) => toModbusError(error)
548
+ })), Effect8.catchAll(() => Effect8.void)));
549
+ }));
550
+ var serialAsciiServerLayer = (options, handlers) => Layer5.scopedDiscard(Effect8.gen(function* () {
551
+ const { AsyncSerialModbusServer } = yield* Effect8.promise(() => import("modbus-rs"));
552
+ const server = yield* Effect8.tryPromise({
553
+ try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
554
+ catch: (error) => toModbusError(error)
555
+ });
556
+ yield* Effect8.logDebug(`Serial ASCII server bound to ${options.portPath}`);
557
+ yield* Effect8.addFinalizer(() => Effect8.logDebug("Serial ASCII server shutting down").pipe(Effect8.andThen(Effect8.tryPromise({
558
+ try: () => server.shutdown(),
559
+ catch: (error) => toModbusError(error)
560
+ })), Effect8.catchAll(() => Effect8.void)));
561
+ }));
562
+ // src/TcpModbusServerService.ts
563
+ import { Effect as Effect9, Layer as Layer6 } from "effect";
564
+ var tcpServerLayer = (options, handlers) => Layer6.scopedDiscard(Effect9.gen(function* () {
565
+ const { AsyncTcpModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
566
+ const server = yield* Effect9.tryPromise({
567
+ try: () => AsyncTcpModbusServer.bind(options, handlers),
568
+ catch: (error) => toModbusError(error)
569
+ });
570
+ yield* Effect9.logDebug(`TCP server bound to ${options.host}:${options.port}`);
571
+ yield* Effect9.addFinalizer(() => Effect9.logDebug("TCP server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
572
+ try: () => server.shutdown(),
573
+ catch: (error) => toModbusError(error)
574
+ })), Effect9.catchAll(() => Effect9.void)));
575
+ }));
576
+ // src/TcpGatewayService.ts
577
+ import { Effect as Effect10, Layer as Layer7 } from "effect";
578
+ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.scopedDiscard(Effect10.gen(function* () {
579
+ const { AsyncTcpGateway } = yield* Effect10.promise(() => import("modbus-rs"));
580
+ const gateway = yield* Effect10.tryPromise({
581
+ try: () => AsyncTcpGateway.bind(options, gatewayConfig),
582
+ catch: (error) => toModbusError(error)
583
+ });
584
+ yield* Effect10.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
585
+ yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP gateway shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
586
+ try: () => gateway.shutdown(),
587
+ catch: (error) => toModbusError(error)
588
+ })), Effect10.catchAll(() => Effect10.void)));
589
+ }));
590
+ export {
591
+ toModbusError,
592
+ tcpServerLayer,
593
+ tcpGatewayLayer,
594
+ serialRtuServerLayer,
595
+ serialAsciiServerLayer,
596
+ TcpTransportService,
597
+ SerialTransportService,
598
+ RtuTransportService,
599
+ ModbusTransportError,
600
+ ModbusTimeoutError,
601
+ ModbusNotConnectedError,
602
+ ModbusInvalidArgumentError,
603
+ ModbusInternalError,
604
+ ModbusExceptionError,
605
+ ModbusConnectionClosedError,
606
+ AsciiTransportService
607
+ };
@@ -0,0 +1,46 @@
1
+ import type { AsciiTransportOptions } from "modbus-rs";
2
+ import { Effect, Layer } from "effect";
3
+ import { SlaveDeviceDefinitions } from "./mocks";
4
+ declare const AsciiTransportService_base: Effect.Service.Class<AsciiTransportService, "AsciiTransportService", {
5
+ readonly scoped: (options: AsciiTransportOptions) => Effect.Effect<{
6
+ withClient: (unitId: number) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
7
+ setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
8
+ clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
9
+ reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
10
+ close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
11
+ hasPendingRequests: () => boolean;
12
+ }, never, import("effect/Scope").Scope>;
13
+ }>;
14
+ /**
15
+ * Scoped Effect service wrapping the `modbus-rs` {@link AsyncAsciiTransport}
16
+ * for ASCII (serial) Modbus communication.
17
+ *
18
+ * The transport connection is opened lazily on the first call to
19
+ * `withClient(unitId)` and automatically closed when the consuming
20
+ * {@link Effect.Scope | Scope} finalizes.
21
+ *
22
+ * Clients are created per `unitId` via
23
+ * {@link AsyncAsciiTransport.createClient} and cached, so repeated
24
+ * requests for the same unit ID reuse the same client.
25
+ *
26
+ * @see AsyncAsciiTransport — Upstream `modbus-rs` ASCII transport.
27
+ * @see AsciiTransportOptions — Configuration for the ASCII serial port.
28
+ * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
29
+ */
30
+ export declare class AsciiTransportService extends AsciiTransportService_base {
31
+ /**
32
+ * Creates a {@link Layer} providing an in-memory mock
33
+ * {@link AsciiTransportService} for testing or development.
34
+ *
35
+ * Accepts an array of {@link SlaveDeviceDefinition} describing the
36
+ * simulated Modbus slaves and their register/coil maps.
37
+ *
38
+ * @param devices - Slave device definitions for the mock.
39
+ * @returns A function that takes {@link AsciiTransportOptions} and
40
+ * returns a scoped {@link Layer} providing the mock service.
41
+ *
42
+ * @see makeMockTransport — The underlying mock factory.
43
+ */
44
+ static makeMockTransport: (devices: SlaveDeviceDefinitions) => (options: AsciiTransportOptions) => Layer.Layer<AsciiTransportService, never, never>;
45
+ }
46
+ export {};
@@ -0,0 +1,46 @@
1
+ import type { RtuTransportOptions } from "modbus-rs";
2
+ import { Effect, Layer } from "effect";
3
+ import type { SlaveDeviceDefinitions } from "./mocks";
4
+ declare const RtuTransportService_base: Effect.Service.Class<RtuTransportService, "RtuTransportService", {
5
+ readonly scoped: (options: RtuTransportOptions) => Effect.Effect<{
6
+ withClient: (unitId: number) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
7
+ setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
8
+ clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
9
+ reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
10
+ close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
11
+ hasPendingRequests: () => boolean;
12
+ }, never, import("effect/Scope").Scope>;
13
+ }>;
14
+ /**
15
+ * Scoped Effect service wrapping the `modbus-rs` {@link AsyncRtuTransport}
16
+ * for RTU (serial) Modbus communication.
17
+ *
18
+ * The transport connection is opened lazily on the first call to
19
+ * `withClient(unitId)` and automatically closed when the consuming
20
+ * {@link Effect.Scope | Scope} finalizes.
21
+ *
22
+ * Clients are created per `unitId` via
23
+ * {@link AsyncRtuTransport.createClient} and cached, so repeated
24
+ * requests for the same unit ID reuse the same client.
25
+ *
26
+ * @see AsyncRtuTransport — Upstream `modbus-rs` RTU transport.
27
+ * @see RtuTransportOptions — Configuration for the RTU serial port.
28
+ * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
29
+ */
30
+ export declare class RtuTransportService extends RtuTransportService_base {
31
+ /**
32
+ * Creates a {@link Layer} providing an in-memory mock
33
+ * {@link RtuTransportService} for testing or development.
34
+ *
35
+ * Accepts an array of {@link SlaveDeviceDefinition} describing the
36
+ * simulated Modbus slaves and their register/coil maps.
37
+ *
38
+ * @param devices - Slave device definitions for the mock.
39
+ * @returns A function that takes {@link RtuTransportOptions} and
40
+ * returns a scoped {@link Layer} providing the mock service.
41
+ *
42
+ * @see makeMockTransport — The underlying mock factory.
43
+ */
44
+ static makeMockTransport: (devices: SlaveDeviceDefinitions) => (options: RtuTransportOptions) => Layer.Layer<RtuTransportService, never, never>;
45
+ }
46
+ export {};