@flux-control/effect-modbus-rs 0.1.1 → 0.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/README.md +147 -86
- package/dist/index.d.ts +31 -11
- package/dist/index.js +326 -251
- package/dist/src/AsciiTransportService.d.ts +3 -3
- package/dist/src/RtuTransportService.d.ts +3 -3
- package/dist/src/SerialModbusServerService.d.ts +5 -5
- package/dist/src/SerialTransportService.d.ts +4 -4
- package/dist/src/TcpGatewayService.d.ts +4 -4
- package/dist/src/TcpModbusServerService.d.ts +4 -4
- package/dist/src/TcpTransportService.d.ts +3 -3
- package/dist/src/WasmAsciiTransportService.d.ts +56 -0
- package/dist/src/WasmRtuTransportService.d.ts +56 -0
- package/dist/src/WasmSerialModbusServerService.d.ts +54 -0
- package/dist/src/WasmSerialPort.d.ts +27 -0
- package/dist/src/WasmSerialTransportService.d.ts +48 -0
- package/dist/src/WasmTcpServerService.d.ts +38 -0
- package/dist/src/WasmWsTransportService.d.ts +45 -0
- package/dist/src/mocks.d.ts +6 -5
- package/dist/src/modbus-client.d.ts +29 -21
- package/dist/src/modbus-client.wasm.test.d.ts +1 -0
- package/dist/src/shared-transport.d.ts +8 -4
- package/dist/src/wasm-mocks.test.d.ts +1 -0
- package/package.json +24 -17
package/dist/index.js
CHANGED
|
@@ -54,216 +54,11 @@ var toModbusError = (cause) => {
|
|
|
54
54
|
// src/AsciiTransportService.ts
|
|
55
55
|
import { Effect as Effect4, Layer } from "effect";
|
|
56
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
57
|
// src/mocks.ts
|
|
266
|
-
import { Effect
|
|
58
|
+
import { Effect, Schema } from "effect";
|
|
59
|
+
import {
|
|
60
|
+
CoilState
|
|
61
|
+
} from "modbus-rs";
|
|
267
62
|
var CoilDefinition = Schema.Struct({
|
|
268
63
|
address: Schema.Number,
|
|
269
64
|
default: Schema.Boolean
|
|
@@ -317,30 +112,30 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
|
|
|
317
112
|
message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
|
|
318
113
|
});
|
|
319
114
|
var makeMockModbusClient = (state, unitId) => ({
|
|
320
|
-
readCoils:
|
|
321
|
-
yield*
|
|
115
|
+
readCoils: Effect.fnUntraced(function* (opts) {
|
|
116
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
|
|
322
117
|
if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
|
|
323
118
|
return yield* failOutOfRange("Coil", opts.address, opts.quantity);
|
|
324
119
|
}
|
|
325
120
|
const result = [];
|
|
326
121
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
327
|
-
result.push(state.coils.get(i) ?? false);
|
|
122
|
+
result.push(state.coils.get(i) ?? false ? CoilState.On : CoilState.Off);
|
|
328
123
|
}
|
|
329
124
|
return result;
|
|
330
125
|
}),
|
|
331
|
-
readDiscreteInputs:
|
|
332
|
-
yield*
|
|
126
|
+
readDiscreteInputs: Effect.fnUntraced(function* (opts) {
|
|
127
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
|
|
333
128
|
if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
|
|
334
129
|
return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
|
|
335
130
|
}
|
|
336
131
|
const result = [];
|
|
337
132
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
338
|
-
result.push(state.discreteInputs.get(i) ?? false);
|
|
133
|
+
result.push(state.discreteInputs.get(i) ?? false ? CoilState.On : CoilState.Off);
|
|
339
134
|
}
|
|
340
135
|
return result;
|
|
341
136
|
}),
|
|
342
|
-
readHoldingRegisters:
|
|
343
|
-
yield*
|
|
137
|
+
readHoldingRegisters: Effect.fnUntraced(function* (opts) {
|
|
138
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
|
|
344
139
|
if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
|
|
345
140
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
|
|
346
141
|
}
|
|
@@ -348,10 +143,10 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
348
143
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
349
144
|
result.push(state.holdingRegisters.get(i) ?? 0);
|
|
350
145
|
}
|
|
351
|
-
return result;
|
|
146
|
+
return new Uint16Array(result);
|
|
352
147
|
}),
|
|
353
|
-
readInputRegisters:
|
|
354
|
-
yield*
|
|
148
|
+
readInputRegisters: Effect.fnUntraced(function* (opts) {
|
|
149
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
|
|
355
150
|
if (opts.address + opts.quantity > state.maxInputAddress + 1) {
|
|
356
151
|
return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
|
|
357
152
|
}
|
|
@@ -359,33 +154,33 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
359
154
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
360
155
|
result.push(state.inputRegisters.get(i) ?? 0);
|
|
361
156
|
}
|
|
362
|
-
return result;
|
|
157
|
+
return new Uint16Array(result);
|
|
363
158
|
}),
|
|
364
|
-
writeSingleCoil:
|
|
365
|
-
yield*
|
|
159
|
+
writeSingleCoil: Effect.fnUntraced(function* (opts) {
|
|
160
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
|
|
366
161
|
if (opts.address > state.maxCoilAddress) {
|
|
367
162
|
return yield* failOutOfRange("Coil", opts.address);
|
|
368
163
|
}
|
|
369
|
-
state.coils.set(opts.address, opts.value);
|
|
164
|
+
state.coils.set(opts.address, opts.value === CoilState.On);
|
|
370
165
|
}),
|
|
371
|
-
writeMultipleCoils:
|
|
372
|
-
yield*
|
|
166
|
+
writeMultipleCoils: Effect.fnUntraced(function* (opts) {
|
|
167
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
|
|
373
168
|
if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
|
|
374
169
|
return yield* failOutOfRange("Coil", opts.address, opts.values.length);
|
|
375
170
|
}
|
|
376
171
|
for (let i = 0;i < opts.values.length; i++) {
|
|
377
|
-
state.coils.set(opts.address + i, opts.values[i]);
|
|
172
|
+
state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
|
|
378
173
|
}
|
|
379
174
|
}),
|
|
380
|
-
writeSingleRegister:
|
|
381
|
-
yield*
|
|
175
|
+
writeSingleRegister: Effect.fnUntraced(function* (opts) {
|
|
176
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
|
|
382
177
|
if (opts.address > state.maxHoldingAddress) {
|
|
383
178
|
return yield* failOutOfRange("HoldingRegister", opts.address);
|
|
384
179
|
}
|
|
385
180
|
state.holdingRegisters.set(opts.address, opts.value);
|
|
386
181
|
}),
|
|
387
|
-
writeMultipleRegisters:
|
|
388
|
-
yield*
|
|
182
|
+
writeMultipleRegisters: Effect.fnUntraced(function* (opts) {
|
|
183
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
|
|
389
184
|
if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
|
|
390
185
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
|
|
391
186
|
}
|
|
@@ -393,8 +188,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
393
188
|
state.holdingRegisters.set(opts.address + i, opts.values[i]);
|
|
394
189
|
}
|
|
395
190
|
}),
|
|
396
|
-
readWriteMultipleRegisters:
|
|
397
|
-
yield*
|
|
191
|
+
readWriteMultipleRegisters: Effect.fnUntraced(function* (opts) {
|
|
192
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
|
|
398
193
|
if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
|
|
399
194
|
return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
|
|
400
195
|
}
|
|
@@ -408,36 +203,36 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
408
203
|
for (let i = opts.readAddress;i < opts.readAddress + opts.readQuantity; i++) {
|
|
409
204
|
result.push(state.holdingRegisters.get(i) ?? 0);
|
|
410
205
|
}
|
|
411
|
-
return result;
|
|
206
|
+
return new Uint16Array(result);
|
|
412
207
|
}),
|
|
413
|
-
readFifoQueue:
|
|
208
|
+
readFifoQueue: Effect.fnUntraced(function* (_opts) {
|
|
414
209
|
return yield* new ModbusInvalidArgumentError({
|
|
415
210
|
cause: new Error("FIFO queue not yet supported in mock"),
|
|
416
211
|
message: "FIFO queue not yet supported in mock"
|
|
417
212
|
});
|
|
418
213
|
}),
|
|
419
|
-
readFileRecord:
|
|
214
|
+
readFileRecord: Effect.fnUntraced(function* (_opts) {
|
|
420
215
|
return yield* new ModbusInvalidArgumentError({
|
|
421
216
|
cause: new Error("File records not yet supported in mock"),
|
|
422
217
|
message: "File records not yet supported in mock"
|
|
423
218
|
});
|
|
424
219
|
}),
|
|
425
|
-
writeFileRecord:
|
|
220
|
+
writeFileRecord: Effect.fnUntraced(function* (_opts) {
|
|
426
221
|
return yield* new ModbusInvalidArgumentError({
|
|
427
222
|
cause: new Error("File records not yet supported in mock"),
|
|
428
223
|
message: "File records not yet supported in mock"
|
|
429
224
|
});
|
|
430
225
|
}),
|
|
431
|
-
readExceptionStatus:
|
|
432
|
-
yield*
|
|
226
|
+
readExceptionStatus: Effect.fnUntraced(function* () {
|
|
227
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
|
|
433
228
|
return 0;
|
|
434
229
|
}),
|
|
435
|
-
diagnostics:
|
|
436
|
-
yield*
|
|
437
|
-
return { subFunction: opts.subFunction, data:
|
|
230
|
+
diagnostics: Effect.fnUntraced(function* (opts) {
|
|
231
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
|
|
232
|
+
return { subFunction: opts.subFunction, data: new Uint16Array };
|
|
438
233
|
}),
|
|
439
|
-
readDeviceIdentification:
|
|
440
|
-
yield*
|
|
234
|
+
readDeviceIdentification: Effect.fnUntraced(function* (opts) {
|
|
235
|
+
yield* Effect.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
|
|
441
236
|
return {
|
|
442
237
|
conformityLevel: 1,
|
|
443
238
|
moreFollows: false,
|
|
@@ -465,10 +260,10 @@ var makeMockTransport = (devices) => {
|
|
|
465
260
|
maxInputAddress: input.maxAddress
|
|
466
261
|
});
|
|
467
262
|
}
|
|
468
|
-
return (_options) =>
|
|
469
|
-
yield*
|
|
263
|
+
return (_options) => Effect.gen(function* () {
|
|
264
|
+
yield* Effect.logDebug("Mock transport opened with devices:", deviceDefs);
|
|
470
265
|
return {
|
|
471
|
-
withClient:
|
|
266
|
+
withClient: Effect.fnUntraced(function* (unitId) {
|
|
472
267
|
const state = deviceStates.get(unitId);
|
|
473
268
|
if (!state) {
|
|
474
269
|
return yield* new ModbusInvalidArgumentError({
|
|
@@ -478,15 +273,182 @@ var makeMockTransport = (devices) => {
|
|
|
478
273
|
}
|
|
479
274
|
return makeMockModbusClient(state, unitId);
|
|
480
275
|
}),
|
|
481
|
-
setRequestTimeout: (_timeoutMs) =>
|
|
482
|
-
clearRequestTimeout: () =>
|
|
483
|
-
reconnect: () =>
|
|
484
|
-
close: () =>
|
|
276
|
+
setRequestTimeout: (_timeoutMs) => Effect.void,
|
|
277
|
+
clearRequestTimeout: () => Effect.void,
|
|
278
|
+
reconnect: () => Effect.asVoid(Effect.logDebug("Mock: reconnecting")),
|
|
279
|
+
close: () => Effect.logDebug("Mock: closing transport"),
|
|
485
280
|
hasPendingRequests: () => false
|
|
486
281
|
};
|
|
487
282
|
});
|
|
488
283
|
};
|
|
489
284
|
|
|
285
|
+
// src/shared-transport.ts
|
|
286
|
+
import { Effect as Effect3, Exit, Scope } from "effect";
|
|
287
|
+
|
|
288
|
+
// src/modbus-client.ts
|
|
289
|
+
import { Effect as Effect2 } from "effect";
|
|
290
|
+
var wrap = (try_) => Effect2.tryPromise({
|
|
291
|
+
try: try_,
|
|
292
|
+
catch: (error) => toModbusError(error)
|
|
293
|
+
});
|
|
294
|
+
var makeEffectModbusClient = (client) => ({
|
|
295
|
+
readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
|
|
296
|
+
readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
|
|
297
|
+
writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
|
|
298
|
+
writeMultipleRegisters: (opts) => wrap(() => client.writeMultipleRegisters(opts)),
|
|
299
|
+
readWriteMultipleRegisters: (opts) => wrap(() => client.readWriteMultipleRegisters(opts)),
|
|
300
|
+
readCoils: (opts) => wrap(() => client.readCoils(opts)),
|
|
301
|
+
writeSingleCoil: (opts) => wrap(() => client.writeSingleCoil(opts)),
|
|
302
|
+
writeMultipleCoils: (opts) => wrap(() => client.writeMultipleCoils(opts)),
|
|
303
|
+
readDiscreteInputs: (opts) => wrap(() => client.readDiscreteInputs(opts)),
|
|
304
|
+
readFifoQueue: (opts) => wrap(() => client.readFifoQueue(opts)),
|
|
305
|
+
readFileRecord: (opts) => wrap(() => client.readFileRecord(opts)),
|
|
306
|
+
writeFileRecord: (opts) => wrap(() => client.writeFileRecord(opts)),
|
|
307
|
+
readExceptionStatus: () => wrap(() => client.readExceptionStatus()),
|
|
308
|
+
diagnostics: (opts) => wrap(() => client.diagnostics(opts)),
|
|
309
|
+
readDeviceIdentification: (opts) => wrap(() => client.readDeviceIdentification(opts))
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// src/shared-transport.ts
|
|
313
|
+
function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
314
|
+
return Effect3.fnUntraced(function* (options) {
|
|
315
|
+
const mod = config?.moduleSpecifier === "modbus-rs/web" ? yield* Effect3.promise(() => import("modbus-rs/web")) : yield* Effect3.promise(() => import("modbus-rs"));
|
|
316
|
+
const TC = mod[transportKey];
|
|
317
|
+
let transport = null;
|
|
318
|
+
let connectPromise = null;
|
|
319
|
+
let reconnectPromise = null;
|
|
320
|
+
const clientSet = new Map;
|
|
321
|
+
let closed = false;
|
|
322
|
+
const ensureOpen = Effect3.fnUntraced(function* () {
|
|
323
|
+
if (transport) {
|
|
324
|
+
if (closed) {
|
|
325
|
+
return yield* new ModbusNotConnectedError({
|
|
326
|
+
cause: new Error("Transport has been closed"),
|
|
327
|
+
message: "Transport has been closed"
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return transport;
|
|
331
|
+
}
|
|
332
|
+
if (closed) {
|
|
333
|
+
return yield* new ModbusNotConnectedError({
|
|
334
|
+
cause: new Error("Transport has been closed"),
|
|
335
|
+
message: "Transport has been closed"
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (!connectPromise) {
|
|
339
|
+
connectPromise = openMethod(TC, options);
|
|
340
|
+
}
|
|
341
|
+
const t = yield* Effect3.tryPromise({
|
|
342
|
+
try: () => connectPromise,
|
|
343
|
+
catch: (error) => toModbusError(error)
|
|
344
|
+
}).pipe(Effect3.catchAll((err) => {
|
|
345
|
+
connectPromise = null;
|
|
346
|
+
return Effect3.fail(err);
|
|
347
|
+
}));
|
|
348
|
+
if (closed) {
|
|
349
|
+
connectPromise = null;
|
|
350
|
+
yield* Effect3.fork(Effect3.promise(() => t.close()).pipe(Effect3.catchAll(() => Effect3.void)));
|
|
351
|
+
return yield* new ModbusNotConnectedError({
|
|
352
|
+
cause: new Error("Transport has been closed"),
|
|
353
|
+
message: "Transport has been closed"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
transport = t;
|
|
357
|
+
return t;
|
|
358
|
+
});
|
|
359
|
+
yield* Effect3.addFinalizer(() => {
|
|
360
|
+
if (closed)
|
|
361
|
+
return Effect3.void;
|
|
362
|
+
closed = true;
|
|
363
|
+
const t = transport;
|
|
364
|
+
if (!t)
|
|
365
|
+
return Effect3.void;
|
|
366
|
+
return Effect3.andThen(Effect3.logDebug(`Closing ${serviceName}`), Effect3.promise(() => t.close()));
|
|
367
|
+
});
|
|
368
|
+
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
369
|
+
return {
|
|
370
|
+
withClient: Effect3.fnUntraced(function* (unitId) {
|
|
371
|
+
const t = yield* ensureOpen();
|
|
372
|
+
let client = clientSet.get(unitId);
|
|
373
|
+
if (!client) {
|
|
374
|
+
client = yield* Effect3.try({
|
|
375
|
+
try: () => t.createClient({ unitId }),
|
|
376
|
+
catch: (error) => toModbusError(error)
|
|
377
|
+
});
|
|
378
|
+
clientSet.set(unitId, client);
|
|
379
|
+
}
|
|
380
|
+
return makeEffectModbusClient(client);
|
|
381
|
+
}),
|
|
382
|
+
setRequestTimeout: Effect3.fnUntraced(function* (timeoutMs) {
|
|
383
|
+
const t = transport;
|
|
384
|
+
if (!t || closed) {
|
|
385
|
+
return yield* new ModbusNotConnectedError({
|
|
386
|
+
cause: new Error(notConnectedMsg),
|
|
387
|
+
message: notConnectedMsg
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
t.setRequestTimeout(timeoutMs);
|
|
391
|
+
}),
|
|
392
|
+
clearRequestTimeout: Effect3.fnUntraced(function* () {
|
|
393
|
+
const t = transport;
|
|
394
|
+
if (!t || closed) {
|
|
395
|
+
return yield* new ModbusNotConnectedError({
|
|
396
|
+
cause: new Error(notConnectedMsg),
|
|
397
|
+
message: notConnectedMsg
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
t.clearRequestTimeout();
|
|
401
|
+
}),
|
|
402
|
+
reconnect: Effect3.fnUntraced(function* () {
|
|
403
|
+
if (closed) {
|
|
404
|
+
return yield* new ModbusNotConnectedError({
|
|
405
|
+
cause: new Error("Transport has been closed"),
|
|
406
|
+
message: "Transport has been closed"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (transport) {
|
|
410
|
+
if (!reconnectPromise) {
|
|
411
|
+
reconnectPromise = transport.reconnect().then(() => {
|
|
412
|
+
reconnectPromise = null;
|
|
413
|
+
}).catch((err) => {
|
|
414
|
+
reconnectPromise = null;
|
|
415
|
+
throw err;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
yield* Effect3.tryPromise({
|
|
419
|
+
try: () => reconnectPromise,
|
|
420
|
+
catch: (error) => toModbusError(error)
|
|
421
|
+
});
|
|
422
|
+
} else {
|
|
423
|
+
yield* ensureOpen();
|
|
424
|
+
}
|
|
425
|
+
}),
|
|
426
|
+
close: Effect3.fnUntraced(function* () {
|
|
427
|
+
if (closed)
|
|
428
|
+
return;
|
|
429
|
+
closed = true;
|
|
430
|
+
const t = transport;
|
|
431
|
+
if (t) {
|
|
432
|
+
yield* Effect3.tryPromise({
|
|
433
|
+
try: () => t.close(),
|
|
434
|
+
catch: (error) => toModbusError(error)
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const scope = yield* Effect3.scope;
|
|
438
|
+
yield* Scope.close(scope, Exit.void);
|
|
439
|
+
}),
|
|
440
|
+
hasPendingRequests: () => {
|
|
441
|
+
if (closed)
|
|
442
|
+
return false;
|
|
443
|
+
const t = transport;
|
|
444
|
+
if (!t)
|
|
445
|
+
return false;
|
|
446
|
+
return t.pendingRequests;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
490
452
|
// src/AsciiTransportService.ts
|
|
491
453
|
class AsciiTransportService extends Effect4.Service()("AsciiTransportService", {
|
|
492
454
|
scoped: makeTransportScoped("AsyncAsciiTransport", (TC, options) => TC.open(options), "AsciiTransportService")
|
|
@@ -587,12 +549,125 @@ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.scopedDiscard(Effect10.
|
|
|
587
549
|
catch: (error) => toModbusError(error)
|
|
588
550
|
})), Effect10.catchAll(() => Effect10.void)));
|
|
589
551
|
}));
|
|
552
|
+
// src/WasmWsTransportService.ts
|
|
553
|
+
import { Effect as Effect11, Layer as Layer8 } from "effect";
|
|
554
|
+
class WasmWsTransportService extends Effect11.Service()("WasmWsTransportService", {
|
|
555
|
+
scoped: makeTransportScoped("WasmWsTransport", (TC, options) => TC.connect(options), "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
556
|
+
}) {
|
|
557
|
+
static makeMockTransport = (devices) => {
|
|
558
|
+
const factory = makeMockTransport(devices);
|
|
559
|
+
return (options) => Layer8.scoped(WasmWsTransportService, factory(options));
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
// src/WasmRtuTransportService.ts
|
|
563
|
+
import { Effect as Effect12, Layer as Layer9 } from "effect";
|
|
564
|
+
class WasmRtuTransportService extends Effect12.Service()("WasmRtuTransportService", {
|
|
565
|
+
scoped: makeTransportScoped("WasmRtuTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
566
|
+
}) {
|
|
567
|
+
static makeMockTransport = (devices) => {
|
|
568
|
+
const factory = makeMockTransport(devices);
|
|
569
|
+
return (options) => Layer9.scoped(WasmRtuTransportService, factory(options));
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// src/WasmAsciiTransportService.ts
|
|
573
|
+
import { Effect as Effect13, Layer as Layer10 } from "effect";
|
|
574
|
+
class WasmAsciiTransportService extends Effect13.Service()("WasmAsciiTransportService", {
|
|
575
|
+
scoped: makeTransportScoped("WasmAsciiTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
576
|
+
}) {
|
|
577
|
+
static makeMockTransport = (devices) => {
|
|
578
|
+
const factory = makeMockTransport(devices);
|
|
579
|
+
return (options) => Layer10.scoped(WasmAsciiTransportService, factory(options));
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
// src/WasmSerialTransportService.ts
|
|
583
|
+
import { Context as Context2, Layer as Layer11 } from "effect";
|
|
584
|
+
class WasmSerialTransportService extends Context2.Tag("WasmSerialTransportService")() {
|
|
585
|
+
static fromAscii(options) {
|
|
586
|
+
return Layer11.project(WasmAsciiTransportService, WasmSerialTransportService, (ascii) => ascii)(WasmAsciiTransportService.Default(options));
|
|
587
|
+
}
|
|
588
|
+
static fromRtu(options) {
|
|
589
|
+
return Layer11.project(WasmRtuTransportService, WasmSerialTransportService, (rtu) => rtu)(WasmRtuTransportService.Default(options));
|
|
590
|
+
}
|
|
591
|
+
static makeMockTransport = (devices) => {
|
|
592
|
+
const factory = makeMockTransport(devices);
|
|
593
|
+
return (options) => Layer11.scoped(WasmSerialTransportService, factory(options));
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// src/WasmSerialPort.ts
|
|
597
|
+
import { Effect as Effect15 } from "effect";
|
|
598
|
+
var requestSerialPort = () => Effect15.tryPromise({
|
|
599
|
+
try: async () => {
|
|
600
|
+
const mod = await import("modbus-rs/web");
|
|
601
|
+
return mod.requestSerialPort();
|
|
602
|
+
},
|
|
603
|
+
catch: (error) => toModbusError(error)
|
|
604
|
+
});
|
|
605
|
+
// src/WasmTcpServerService.ts
|
|
606
|
+
import { Effect as Effect16, Layer as Layer12 } from "effect";
|
|
607
|
+
var wasmWsServerLayer = (options, handlers) => Layer12.scopedDiscard(Effect16.gen(function* () {
|
|
608
|
+
const { WasmWsModbusServer } = yield* Effect16.promise(() => import("modbus-rs/web"));
|
|
609
|
+
const server = yield* Effect16.tryPromise({
|
|
610
|
+
try: () => WasmWsModbusServer.bind(options, handlers),
|
|
611
|
+
catch: (error) => toModbusError(error)
|
|
612
|
+
});
|
|
613
|
+
yield* Effect16.logDebug(`WASM WS server bound to ${options.wsUrl}`);
|
|
614
|
+
yield* Effect16.forkScoped(Effect16.tryPromise({
|
|
615
|
+
try: () => server.serve(),
|
|
616
|
+
catch: (error) => toModbusError(error)
|
|
617
|
+
}).pipe(Effect16.catchAll((error) => Effect16.logError("WASM WS server loop ended", error))));
|
|
618
|
+
yield* Effect16.addFinalizer(() => Effect16.logDebug("WASM WS server shutting down").pipe(Effect16.andThen(Effect16.tryPromise({
|
|
619
|
+
try: () => server.shutdown(),
|
|
620
|
+
catch: (error) => toModbusError(error)
|
|
621
|
+
})), Effect16.catchAll(() => Effect16.void)));
|
|
622
|
+
}));
|
|
623
|
+
// src/WasmSerialModbusServerService.ts
|
|
624
|
+
import { Effect as Effect17, Layer as Layer13 } from "effect";
|
|
625
|
+
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.scopedDiscard(Effect17.gen(function* () {
|
|
626
|
+
const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
|
|
627
|
+
const server = yield* Effect17.tryPromise({
|
|
628
|
+
try: () => WasmSerialModbusServer.bindRtu(options, handlers),
|
|
629
|
+
catch: (error) => toModbusError(error)
|
|
630
|
+
});
|
|
631
|
+
yield* Effect17.logDebug("WASM serial RTU server bound");
|
|
632
|
+
yield* Effect17.forkScoped(Effect17.tryPromise({
|
|
633
|
+
try: () => server.serve(),
|
|
634
|
+
catch: (error) => toModbusError(error)
|
|
635
|
+
}).pipe(Effect17.catchAll((error) => Effect17.logError("WASM serial RTU server loop ended", error))));
|
|
636
|
+
yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial RTU server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
|
|
637
|
+
try: () => server.shutdown(),
|
|
638
|
+
catch: (error) => toModbusError(error)
|
|
639
|
+
})), Effect17.catchAll(() => Effect17.void)));
|
|
640
|
+
}));
|
|
641
|
+
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.scopedDiscard(Effect17.gen(function* () {
|
|
642
|
+
const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
|
|
643
|
+
const server = yield* Effect17.tryPromise({
|
|
644
|
+
try: () => WasmSerialModbusServer.bindAscii(options, handlers),
|
|
645
|
+
catch: (error) => toModbusError(error)
|
|
646
|
+
});
|
|
647
|
+
yield* Effect17.logDebug("WASM serial ASCII server bound");
|
|
648
|
+
yield* Effect17.forkScoped(Effect17.tryPromise({
|
|
649
|
+
try: () => server.serve(),
|
|
650
|
+
catch: (error) => toModbusError(error)
|
|
651
|
+
}).pipe(Effect17.catchAll((error) => Effect17.logError("WASM serial ASCII server loop ended", error))));
|
|
652
|
+
yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial ASCII server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
|
|
653
|
+
try: () => server.shutdown(),
|
|
654
|
+
catch: (error) => toModbusError(error)
|
|
655
|
+
})), Effect17.catchAll(() => Effect17.void)));
|
|
656
|
+
}));
|
|
590
657
|
export {
|
|
658
|
+
wasmWsServerLayer,
|
|
659
|
+
wasmSerialRtuServerLayer,
|
|
660
|
+
wasmSerialAsciiServerLayer,
|
|
591
661
|
toModbusError,
|
|
592
662
|
tcpServerLayer,
|
|
593
663
|
tcpGatewayLayer,
|
|
594
664
|
serialRtuServerLayer,
|
|
595
665
|
serialAsciiServerLayer,
|
|
666
|
+
requestSerialPort,
|
|
667
|
+
WasmWsTransportService,
|
|
668
|
+
WasmSerialTransportService,
|
|
669
|
+
WasmRtuTransportService,
|
|
670
|
+
WasmAsciiTransportService,
|
|
596
671
|
TcpTransportService,
|
|
597
672
|
SerialTransportService,
|
|
598
673
|
RtuTransportService,
|