@flux-control/effect-modbus-rs 0.1.1 → 0.3.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 +344 -87
- package/dist/index.d.ts +85 -11
- package/dist/index.js +576 -280
- package/dist/src/AsciiTransportService.d.ts +29 -8
- package/dist/src/RtuTransportService.d.ts +29 -8
- package/dist/src/SerialModbusServerService.d.ts +5 -5
- package/dist/src/SerialTransportService.d.ts +8 -7
- package/dist/src/TcpGatewayService.d.ts +4 -4
- package/dist/src/TcpModbusServerService.d.ts +4 -4
- package/dist/src/TcpTransportService.d.ts +29 -8
- package/dist/src/WasmAsciiTransportService.d.ts +73 -0
- package/dist/src/WasmRtuTransportService.d.ts +73 -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 +60 -0
- package/dist/src/connection.d.ts +170 -0
- package/dist/src/errors.d.ts +25 -1
- package/dist/src/mocks.d.ts +49 -9
- package/dist/src/modbus-client.d.ts +82 -23
- package/dist/src/modbus-client.wasm.test.d.ts +1 -0
- package/dist/src/resilience.test.d.ts +1 -0
- package/dist/src/retry.d.ts +227 -0
- package/dist/src/retry.test.d.ts +1 -0
- package/dist/src/shared-transport.d.ts +113 -8
- package/dist/src/shared-transport.test.d.ts +1 -0
- package/dist/src/upstream-options.test.d.ts +1 -0
- package/dist/src/wasm-mocks.test.d.ts +1 -0
- package/package.json +24 -17
package/dist/index.js
CHANGED
|
@@ -25,6 +25,9 @@ class ModbusInternalError extends Data.TaggedError("ModbusInternalError") {
|
|
|
25
25
|
|
|
26
26
|
class ModbusNotConnectedError extends Data.TaggedError("ModbusNotConnectedError") {
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
class ModbusCircuitOpenError extends Data.TaggedError("ModbusCircuitOpenError") {
|
|
30
|
+
}
|
|
28
31
|
function parseExceptionCode(message) {
|
|
29
32
|
const m = message.match(/\[MODBUS_EXCEPTION:(\d+)\]/);
|
|
30
33
|
return m ? Number(m[1]) : undefined;
|
|
@@ -51,219 +54,204 @@ var toModbusError = (cause) => {
|
|
|
51
54
|
return new ModbusInternalError({ cause, message });
|
|
52
55
|
}
|
|
53
56
|
};
|
|
57
|
+
// src/retry.ts
|
|
58
|
+
import { Duration, Effect, Schedule } from "effect";
|
|
59
|
+
var retryableExceptionCodes = [5, 6, 10, 11];
|
|
60
|
+
var defaultRetryableTags = {
|
|
61
|
+
ModbusTimeoutError: true,
|
|
62
|
+
ModbusTransportError: true,
|
|
63
|
+
ModbusConnectionClosedError: true,
|
|
64
|
+
ModbusExceptionError: true,
|
|
65
|
+
ModbusInvalidArgumentError: false,
|
|
66
|
+
ModbusNotConnectedError: false,
|
|
67
|
+
ModbusCircuitOpenError: true,
|
|
68
|
+
ModbusInternalError: false
|
|
69
|
+
};
|
|
70
|
+
var allTags = Object.keys(defaultRetryableTags);
|
|
71
|
+
var toMillis = (input) => Duration.toMillis(Duration.decode(input));
|
|
72
|
+
var resolveRetryableTags = (options) => {
|
|
73
|
+
const retryable = {};
|
|
74
|
+
for (const tag of allTags) {
|
|
75
|
+
const entry = options.errors?.[tag];
|
|
76
|
+
retryable[tag] = entry === undefined ? defaultRetryableTags[tag] : entry !== false;
|
|
77
|
+
}
|
|
78
|
+
return retryable;
|
|
79
|
+
};
|
|
80
|
+
var defaultCurve = { baseMs: 100, factor: 2, maxMs: 5000 };
|
|
81
|
+
var resolveCurve = (overrides, fallback) => ({
|
|
82
|
+
baseMs: overrides.baseDelay === undefined ? fallback.baseMs : toMillis(overrides.baseDelay),
|
|
83
|
+
factor: overrides.factor ?? fallback.factor,
|
|
84
|
+
maxMs: overrides.maxDelay === undefined ? fallback.maxMs : toMillis(overrides.maxDelay)
|
|
85
|
+
});
|
|
86
|
+
var resolveDelays = (options) => {
|
|
87
|
+
const policyCurve = resolveCurve(options, defaultCurve);
|
|
88
|
+
const delays = {};
|
|
89
|
+
for (const tag of allTags) {
|
|
90
|
+
const entry = options.errors?.[tag];
|
|
91
|
+
delays[tag] = resolveCurve(typeof entry === "object" ? entry : {}, policyCurve);
|
|
92
|
+
}
|
|
93
|
+
return delays;
|
|
94
|
+
};
|
|
95
|
+
var mergeOptions = (base, overrides) => overrides === undefined ? base : {
|
|
96
|
+
...base,
|
|
97
|
+
...overrides,
|
|
98
|
+
...base.errors || overrides.errors ? { errors: { ...base.errors, ...overrides.errors } } : {}
|
|
99
|
+
};
|
|
100
|
+
var makeRetryPolicy = (options = {}) => {
|
|
101
|
+
const retryable = resolveRetryableTags(options);
|
|
102
|
+
const delays = resolveDelays(options);
|
|
103
|
+
const exceptions = options.retryableExceptions ?? retryableExceptionCodes;
|
|
104
|
+
const isRetryable = (error) => {
|
|
105
|
+
if (!retryable[error._tag])
|
|
106
|
+
return false;
|
|
107
|
+
if (error._tag === "ModbusExceptionError")
|
|
108
|
+
return exceptions.includes(error.exception);
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
const delayFor = (error, retryIndex) => {
|
|
112
|
+
const { baseMs, factor, maxMs } = delays[error._tag];
|
|
113
|
+
return Duration.millis(Math.min(maxMs, baseMs * factor ** retryIndex));
|
|
114
|
+
};
|
|
115
|
+
const base = Schedule.recurs(options.maxRetries ?? 3).pipe(Schedule.intersect(Schedule.identity()), Schedule.modifyDelay(([retryIndex, error]) => delayFor(error, retryIndex)), Schedule.whileInput(isRetryable));
|
|
116
|
+
const jitter = options.jitter ?? true;
|
|
117
|
+
const jittered = jitter === false ? base : Schedule.jitteredWith(base, jitter === true ? {} : jitter);
|
|
118
|
+
const schedule = options.maxElapsed === undefined ? jittered : Schedule.upTo(jittered, options.maxElapsed);
|
|
119
|
+
return { schedule, isRetryable };
|
|
120
|
+
};
|
|
121
|
+
var RetryPolicies = {
|
|
122
|
+
none: (overrides) => makeRetryPolicy(mergeOptions({ maxRetries: 0 }, overrides)),
|
|
123
|
+
serial: (overrides) => makeRetryPolicy(mergeOptions({
|
|
124
|
+
maxRetries: 3,
|
|
125
|
+
baseDelay: "50 millis",
|
|
126
|
+
factor: 2,
|
|
127
|
+
maxDelay: "1 second",
|
|
128
|
+
errors: { ModbusTimeoutError: { baseDelay: "100 millis" } }
|
|
129
|
+
}, overrides)),
|
|
130
|
+
tcp: (overrides) => makeRetryPolicy(mergeOptions({
|
|
131
|
+
maxRetries: 4,
|
|
132
|
+
baseDelay: "100 millis",
|
|
133
|
+
factor: 2,
|
|
134
|
+
maxDelay: "5 seconds",
|
|
135
|
+
errors: {
|
|
136
|
+
ModbusConnectionClosedError: { baseDelay: "250 millis", maxDelay: "10 seconds" }
|
|
137
|
+
}
|
|
138
|
+
}, overrides)),
|
|
139
|
+
persistent: (overrides) => makeRetryPolicy(mergeOptions({
|
|
140
|
+
maxRetries: 10,
|
|
141
|
+
baseDelay: "250 millis",
|
|
142
|
+
factor: 2,
|
|
143
|
+
maxDelay: "30 seconds",
|
|
144
|
+
maxElapsed: "5 minutes"
|
|
145
|
+
}, overrides))
|
|
146
|
+
};
|
|
147
|
+
var retryModbus = (policy) => (self) => Effect.retry(self, policy.schedule);
|
|
148
|
+
// src/connection.ts
|
|
149
|
+
import { Data as Data2, Duration as Duration2, Effect as Effect2, Either, SubscriptionRef } from "effect";
|
|
150
|
+
var ConnectionState = Data2.taggedEnum();
|
|
151
|
+
var defaultReconnectPolicy = makeRetryPolicy({
|
|
152
|
+
maxRetries: 5,
|
|
153
|
+
baseDelay: "250 millis",
|
|
154
|
+
factor: 2,
|
|
155
|
+
maxDelay: "10 seconds"
|
|
156
|
+
});
|
|
157
|
+
var defaultTriggers = [
|
|
158
|
+
"ModbusConnectionClosedError",
|
|
159
|
+
"ModbusTransportError"
|
|
160
|
+
];
|
|
161
|
+
var resolveReconnect = (options) => {
|
|
162
|
+
const triggerTags = options.triggerOn ?? defaultTriggers;
|
|
163
|
+
return {
|
|
164
|
+
policy: options.policy ?? defaultReconnectPolicy,
|
|
165
|
+
resetAfter: Duration2.decode(options.resetAfter ?? "30 seconds"),
|
|
166
|
+
triggers: (error) => triggerTags.includes(error._tag)
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
var guardCircuit = (state) => Effect2.flatMap(SubscriptionRef.get(state), (current) => ConnectionState.$match(current, {
|
|
170
|
+
Disconnected: () => Effect2.void,
|
|
171
|
+
Connected: () => Effect2.void,
|
|
172
|
+
Reconnecting: ({ attempt }) => Effect2.fail(new ModbusCircuitOpenError({
|
|
173
|
+
cause: new Error("Transport is reconnecting"),
|
|
174
|
+
message: `Transport is reconnecting (attempt ${attempt}); request refused`
|
|
175
|
+
})),
|
|
176
|
+
Down: ({ cause }) => Effect2.fail(new ModbusCircuitOpenError({
|
|
177
|
+
cause: cause.cause,
|
|
178
|
+
message: `Transport is down (${cause.message}); request refused`
|
|
179
|
+
}))
|
|
180
|
+
}));
|
|
181
|
+
var superviseReconnect = (reconnect, state, resolved) => Effect2.gen(function* () {
|
|
182
|
+
while (true) {
|
|
183
|
+
const attempt = reconnect.pipe(Effect2.tapError(() => SubscriptionRef.update(state, (current) => ConnectionState.$is("Reconnecting")(current) ? ConnectionState.Reconnecting({ attempt: current.attempt + 1 }) : current)), retryModbus(resolved.policy));
|
|
184
|
+
const result = yield* Effect2.either(attempt);
|
|
185
|
+
if (Either.isRight(result)) {
|
|
186
|
+
yield* SubscriptionRef.set(state, ConnectionState.Connected());
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
yield* Effect2.logDebug(`Reconnect attempts exhausted: ${result.left.message}`);
|
|
190
|
+
yield* SubscriptionRef.set(state, ConnectionState.Down({ cause: result.left }));
|
|
191
|
+
yield* Effect2.sleep(resolved.resetAfter);
|
|
192
|
+
const probe = yield* SubscriptionRef.modify(state, (current) => ConnectionState.$is("Down")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
193
|
+
if (!probe)
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
54
197
|
// src/AsciiTransportService.ts
|
|
55
|
-
import { Effect as
|
|
198
|
+
import { Effect as Effect6, Layer } from "effect";
|
|
56
199
|
|
|
57
|
-
// src/
|
|
58
|
-
import { Effect as
|
|
200
|
+
// src/mocks.ts
|
|
201
|
+
import { Effect as Effect4, Schema, SubscriptionRef as SubscriptionRef2 } from "effect";
|
|
202
|
+
import {
|
|
203
|
+
CoilState
|
|
204
|
+
} from "modbus-rs";
|
|
59
205
|
|
|
60
206
|
// src/modbus-client.ts
|
|
61
|
-
import { Effect } from "effect";
|
|
207
|
+
import { Effect as Effect3 } from "effect";
|
|
208
|
+
var wrap = (try_) => Effect3.tryPromise({
|
|
209
|
+
try: try_,
|
|
210
|
+
catch: (error) => toModbusError(error)
|
|
211
|
+
});
|
|
62
212
|
var makeEffectModbusClient = (client) => ({
|
|
63
|
-
readHoldingRegisters: (opts) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
})
|
|
213
|
+
readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
|
|
214
|
+
readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
|
|
215
|
+
writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
|
|
216
|
+
writeMultipleRegisters: (opts) => wrap(() => client.writeMultipleRegisters(opts)),
|
|
217
|
+
readWriteMultipleRegisters: (opts) => wrap(() => client.readWriteMultipleRegisters(opts)),
|
|
218
|
+
readCoils: (opts) => wrap(() => client.readCoils(opts)),
|
|
219
|
+
writeSingleCoil: (opts) => wrap(() => client.writeSingleCoil(opts)),
|
|
220
|
+
writeMultipleCoils: (opts) => wrap(() => client.writeMultipleCoils(opts)),
|
|
221
|
+
readDiscreteInputs: (opts) => wrap(() => client.readDiscreteInputs(opts)),
|
|
222
|
+
readFifoQueue: (opts) => wrap(() => client.readFifoQueue(opts)),
|
|
223
|
+
readFileRecord: (opts) => wrap(() => client.readFileRecord(opts)),
|
|
224
|
+
writeFileRecord: (opts) => wrap(() => client.writeFileRecord(opts)),
|
|
225
|
+
readExceptionStatus: () => wrap(() => client.readExceptionStatus()),
|
|
226
|
+
diagnostics: (opts) => wrap(() => client.diagnostics(opts)),
|
|
227
|
+
readDeviceIdentification: (opts) => wrap(() => client.readDeviceIdentification(opts))
|
|
123
228
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
}
|
|
229
|
+
var withResilience = (operations, resilience) => {
|
|
230
|
+
const run = (operation) => {
|
|
231
|
+
const attempt = Effect3.zipRight(resilience.guard, Effect3.suspend(operation)).pipe(Effect3.tapError((error) => resilience.report(error)));
|
|
232
|
+
return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
readHoldingRegisters: (opts) => run(() => operations.readHoldingRegisters(opts)),
|
|
236
|
+
readInputRegisters: (opts) => run(() => operations.readInputRegisters(opts)),
|
|
237
|
+
writeSingleRegister: (opts) => run(() => operations.writeSingleRegister(opts)),
|
|
238
|
+
writeMultipleRegisters: (opts) => run(() => operations.writeMultipleRegisters(opts)),
|
|
239
|
+
readWriteMultipleRegisters: (opts) => run(() => operations.readWriteMultipleRegisters(opts)),
|
|
240
|
+
readCoils: (opts) => run(() => operations.readCoils(opts)),
|
|
241
|
+
writeSingleCoil: (opts) => run(() => operations.writeSingleCoil(opts)),
|
|
242
|
+
writeMultipleCoils: (opts) => run(() => operations.writeMultipleCoils(opts)),
|
|
243
|
+
readDiscreteInputs: (opts) => run(() => operations.readDiscreteInputs(opts)),
|
|
244
|
+
readFifoQueue: (opts) => run(() => operations.readFifoQueue(opts)),
|
|
245
|
+
readFileRecord: (opts) => run(() => operations.readFileRecord(opts)),
|
|
246
|
+
writeFileRecord: (opts) => run(() => operations.writeFileRecord(opts)),
|
|
247
|
+
readExceptionStatus: () => run(() => operations.readExceptionStatus()),
|
|
248
|
+
diagnostics: (opts) => run(() => operations.diagnostics(opts)),
|
|
249
|
+
readDeviceIdentification: (opts) => run(() => operations.readDeviceIdentification(opts)),
|
|
250
|
+
withRetry: (policy) => withResilience(operations, { ...resilience, policy })
|
|
251
|
+
};
|
|
252
|
+
};
|
|
264
253
|
|
|
265
254
|
// src/mocks.ts
|
|
266
|
-
import { Effect as Effect3, Schema } from "effect";
|
|
267
255
|
var CoilDefinition = Schema.Struct({
|
|
268
256
|
address: Schema.Number,
|
|
269
257
|
default: Schema.Boolean
|
|
@@ -317,30 +305,30 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
|
|
|
317
305
|
message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
|
|
318
306
|
});
|
|
319
307
|
var makeMockModbusClient = (state, unitId) => ({
|
|
320
|
-
readCoils:
|
|
321
|
-
yield*
|
|
308
|
+
readCoils: Effect4.fnUntraced(function* (opts) {
|
|
309
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
|
|
322
310
|
if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
|
|
323
311
|
return yield* failOutOfRange("Coil", opts.address, opts.quantity);
|
|
324
312
|
}
|
|
325
313
|
const result = [];
|
|
326
314
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
327
|
-
result.push(state.coils.get(i) ?? false);
|
|
315
|
+
result.push(state.coils.get(i) ?? false ? CoilState.On : CoilState.Off);
|
|
328
316
|
}
|
|
329
317
|
return result;
|
|
330
318
|
}),
|
|
331
|
-
readDiscreteInputs:
|
|
332
|
-
yield*
|
|
319
|
+
readDiscreteInputs: Effect4.fnUntraced(function* (opts) {
|
|
320
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
|
|
333
321
|
if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
|
|
334
322
|
return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
|
|
335
323
|
}
|
|
336
324
|
const result = [];
|
|
337
325
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
338
|
-
result.push(state.discreteInputs.get(i) ?? false);
|
|
326
|
+
result.push(state.discreteInputs.get(i) ?? false ? CoilState.On : CoilState.Off);
|
|
339
327
|
}
|
|
340
328
|
return result;
|
|
341
329
|
}),
|
|
342
|
-
readHoldingRegisters:
|
|
343
|
-
yield*
|
|
330
|
+
readHoldingRegisters: Effect4.fnUntraced(function* (opts) {
|
|
331
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
|
|
344
332
|
if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
|
|
345
333
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
|
|
346
334
|
}
|
|
@@ -348,10 +336,10 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
348
336
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
349
337
|
result.push(state.holdingRegisters.get(i) ?? 0);
|
|
350
338
|
}
|
|
351
|
-
return result;
|
|
339
|
+
return new Uint16Array(result);
|
|
352
340
|
}),
|
|
353
|
-
readInputRegisters:
|
|
354
|
-
yield*
|
|
341
|
+
readInputRegisters: Effect4.fnUntraced(function* (opts) {
|
|
342
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
|
|
355
343
|
if (opts.address + opts.quantity > state.maxInputAddress + 1) {
|
|
356
344
|
return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
|
|
357
345
|
}
|
|
@@ -359,33 +347,33 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
359
347
|
for (let i = opts.address;i < opts.address + opts.quantity; i++) {
|
|
360
348
|
result.push(state.inputRegisters.get(i) ?? 0);
|
|
361
349
|
}
|
|
362
|
-
return result;
|
|
350
|
+
return new Uint16Array(result);
|
|
363
351
|
}),
|
|
364
|
-
writeSingleCoil:
|
|
365
|
-
yield*
|
|
352
|
+
writeSingleCoil: Effect4.fnUntraced(function* (opts) {
|
|
353
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
|
|
366
354
|
if (opts.address > state.maxCoilAddress) {
|
|
367
355
|
return yield* failOutOfRange("Coil", opts.address);
|
|
368
356
|
}
|
|
369
|
-
state.coils.set(opts.address, opts.value);
|
|
357
|
+
state.coils.set(opts.address, opts.value === CoilState.On);
|
|
370
358
|
}),
|
|
371
|
-
writeMultipleCoils:
|
|
372
|
-
yield*
|
|
359
|
+
writeMultipleCoils: Effect4.fnUntraced(function* (opts) {
|
|
360
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
|
|
373
361
|
if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
|
|
374
362
|
return yield* failOutOfRange("Coil", opts.address, opts.values.length);
|
|
375
363
|
}
|
|
376
364
|
for (let i = 0;i < opts.values.length; i++) {
|
|
377
|
-
state.coils.set(opts.address + i, opts.values[i]);
|
|
365
|
+
state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
|
|
378
366
|
}
|
|
379
367
|
}),
|
|
380
|
-
writeSingleRegister:
|
|
381
|
-
yield*
|
|
368
|
+
writeSingleRegister: Effect4.fnUntraced(function* (opts) {
|
|
369
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
|
|
382
370
|
if (opts.address > state.maxHoldingAddress) {
|
|
383
371
|
return yield* failOutOfRange("HoldingRegister", opts.address);
|
|
384
372
|
}
|
|
385
373
|
state.holdingRegisters.set(opts.address, opts.value);
|
|
386
374
|
}),
|
|
387
|
-
writeMultipleRegisters:
|
|
388
|
-
yield*
|
|
375
|
+
writeMultipleRegisters: Effect4.fnUntraced(function* (opts) {
|
|
376
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
|
|
389
377
|
if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
|
|
390
378
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
|
|
391
379
|
}
|
|
@@ -393,8 +381,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
393
381
|
state.holdingRegisters.set(opts.address + i, opts.values[i]);
|
|
394
382
|
}
|
|
395
383
|
}),
|
|
396
|
-
readWriteMultipleRegisters:
|
|
397
|
-
yield*
|
|
384
|
+
readWriteMultipleRegisters: Effect4.fnUntraced(function* (opts) {
|
|
385
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
|
|
398
386
|
if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
|
|
399
387
|
return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
|
|
400
388
|
}
|
|
@@ -408,36 +396,36 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
408
396
|
for (let i = opts.readAddress;i < opts.readAddress + opts.readQuantity; i++) {
|
|
409
397
|
result.push(state.holdingRegisters.get(i) ?? 0);
|
|
410
398
|
}
|
|
411
|
-
return result;
|
|
399
|
+
return new Uint16Array(result);
|
|
412
400
|
}),
|
|
413
|
-
readFifoQueue:
|
|
401
|
+
readFifoQueue: Effect4.fnUntraced(function* (_opts) {
|
|
414
402
|
return yield* new ModbusInvalidArgumentError({
|
|
415
403
|
cause: new Error("FIFO queue not yet supported in mock"),
|
|
416
404
|
message: "FIFO queue not yet supported in mock"
|
|
417
405
|
});
|
|
418
406
|
}),
|
|
419
|
-
readFileRecord:
|
|
407
|
+
readFileRecord: Effect4.fnUntraced(function* (_opts) {
|
|
420
408
|
return yield* new ModbusInvalidArgumentError({
|
|
421
409
|
cause: new Error("File records not yet supported in mock"),
|
|
422
410
|
message: "File records not yet supported in mock"
|
|
423
411
|
});
|
|
424
412
|
}),
|
|
425
|
-
writeFileRecord:
|
|
413
|
+
writeFileRecord: Effect4.fnUntraced(function* (_opts) {
|
|
426
414
|
return yield* new ModbusInvalidArgumentError({
|
|
427
415
|
cause: new Error("File records not yet supported in mock"),
|
|
428
416
|
message: "File records not yet supported in mock"
|
|
429
417
|
});
|
|
430
418
|
}),
|
|
431
|
-
readExceptionStatus:
|
|
432
|
-
yield*
|
|
419
|
+
readExceptionStatus: Effect4.fnUntraced(function* () {
|
|
420
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
|
|
433
421
|
return 0;
|
|
434
422
|
}),
|
|
435
|
-
diagnostics:
|
|
436
|
-
yield*
|
|
437
|
-
return { subFunction: opts.subFunction, data:
|
|
423
|
+
diagnostics: Effect4.fnUntraced(function* (opts) {
|
|
424
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
|
|
425
|
+
return { subFunction: opts.subFunction, data: new Uint16Array };
|
|
438
426
|
}),
|
|
439
|
-
readDeviceIdentification:
|
|
440
|
-
yield*
|
|
427
|
+
readDeviceIdentification: Effect4.fnUntraced(function* (opts) {
|
|
428
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
|
|
441
429
|
return {
|
|
442
430
|
conformityLevel: 1,
|
|
443
431
|
moreFollows: false,
|
|
@@ -465,10 +453,32 @@ var makeMockTransport = (devices) => {
|
|
|
465
453
|
maxInputAddress: input.maxAddress
|
|
466
454
|
});
|
|
467
455
|
}
|
|
468
|
-
return (
|
|
469
|
-
yield*
|
|
456
|
+
return (options) => Effect4.gen(function* () {
|
|
457
|
+
yield* Effect4.logDebug("Mock transport opened with devices:", deviceDefs);
|
|
458
|
+
const connectionState = yield* SubscriptionRef2.make(ConnectionState.Connected());
|
|
459
|
+
const supervised = options.reconnect ? resolveReconnect(options.reconnect) : null;
|
|
460
|
+
const serviceScope = yield* Effect4.scope;
|
|
461
|
+
const reconnectOnce = Effect4.zipRight(Effect4.logDebug("Mock: reconnecting"), Effect4.suspend(() => {
|
|
462
|
+
const injected = options.reconnectFault?.();
|
|
463
|
+
return injected ? Effect4.fail(injected) : Effect4.void;
|
|
464
|
+
}));
|
|
465
|
+
const report = (error) => {
|
|
466
|
+
if (!supervised || !supervised.triggers(error))
|
|
467
|
+
return Effect4.void;
|
|
468
|
+
return Effect4.gen(function* () {
|
|
469
|
+
const claimed = yield* SubscriptionRef2.modify(connectionState, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
470
|
+
if (!claimed)
|
|
471
|
+
return;
|
|
472
|
+
yield* Effect4.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
const guard = Effect4.zipRight(supervised ? guardCircuit(connectionState) : Effect4.void, Effect4.suspend(() => {
|
|
476
|
+
const injected = options.fault?.();
|
|
477
|
+
return injected ? Effect4.fail(injected) : Effect4.void;
|
|
478
|
+
}));
|
|
470
479
|
return {
|
|
471
|
-
|
|
480
|
+
connectionState,
|
|
481
|
+
withClient: Effect4.fnUntraced(function* (unitId, clientOptions) {
|
|
472
482
|
const state = deviceStates.get(unitId);
|
|
473
483
|
if (!state) {
|
|
474
484
|
return yield* new ModbusInvalidArgumentError({
|
|
@@ -476,19 +486,186 @@ var makeMockTransport = (devices) => {
|
|
|
476
486
|
message: `Device with unitId ${unitId} not found in mock configuration`
|
|
477
487
|
});
|
|
478
488
|
}
|
|
479
|
-
return makeMockModbusClient(state, unitId)
|
|
489
|
+
return withResilience(makeMockModbusClient(state, unitId), {
|
|
490
|
+
guard,
|
|
491
|
+
report,
|
|
492
|
+
policy: clientOptions?.retry ?? options.retry
|
|
493
|
+
});
|
|
480
494
|
}),
|
|
481
|
-
setRequestTimeout: (_timeoutMs) =>
|
|
482
|
-
clearRequestTimeout: () =>
|
|
483
|
-
reconnect: () =>
|
|
484
|
-
close: () =>
|
|
495
|
+
setRequestTimeout: (_timeoutMs) => Effect4.void,
|
|
496
|
+
clearRequestTimeout: () => Effect4.void,
|
|
497
|
+
reconnect: () => Effect4.zipRight(reconnectOnce, SubscriptionRef2.set(connectionState, ConnectionState.Connected())),
|
|
498
|
+
close: () => Effect4.zipRight(Effect4.logDebug("Mock: closing transport"), SubscriptionRef2.set(connectionState, ConnectionState.Disconnected())),
|
|
485
499
|
hasPendingRequests: () => false
|
|
486
500
|
};
|
|
487
501
|
});
|
|
488
502
|
};
|
|
489
503
|
|
|
504
|
+
// src/shared-transport.ts
|
|
505
|
+
import { Deferred, Effect as Effect5, Exit, Option, Ref, Scope, SubscriptionRef as SubscriptionRef3 } from "effect";
|
|
506
|
+
var makeInFlight = () => Ref.make(Option.none());
|
|
507
|
+
var singleFlight = (inFlight, work) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
|
|
508
|
+
const fresh = yield* Deferred.make();
|
|
509
|
+
const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
|
|
510
|
+
onSome: (existing) => [[existing, false], current],
|
|
511
|
+
onNone: () => [[fresh, true], Option.some(fresh)]
|
|
512
|
+
}));
|
|
513
|
+
if (isLeader) {
|
|
514
|
+
yield* Effect5.forkDaemon(Effect5.interruptible(work).pipe(Effect5.onExit((exit) => Effect5.zipRight(Ref.set(inFlight, Option.none()), Deferred.done(deferred, exit)))));
|
|
515
|
+
}
|
|
516
|
+
return yield* restore(Deferred.await(deferred));
|
|
517
|
+
}));
|
|
518
|
+
function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
519
|
+
return Effect5.fnUntraced(function* (options) {
|
|
520
|
+
const { retry: transportRetry, reconnect: reconnectOptions, ...rest } = options;
|
|
521
|
+
const openOptions = rest;
|
|
522
|
+
const mod = config?.moduleSpecifier === "modbus-rs/web" ? yield* Effect5.promise(() => import("modbus-rs/web")) : yield* Effect5.promise(() => import("modbus-rs"));
|
|
523
|
+
const TC = mod[transportKey];
|
|
524
|
+
let transport = null;
|
|
525
|
+
const opening = yield* makeInFlight();
|
|
526
|
+
const reconnecting = yield* makeInFlight();
|
|
527
|
+
const connectionState = yield* SubscriptionRef3.make(ConnectionState.Disconnected());
|
|
528
|
+
const supervised = reconnectOptions ? resolveReconnect(reconnectOptions) : null;
|
|
529
|
+
const serviceScope = yield* Effect5.scope;
|
|
530
|
+
const clientSet = new Map;
|
|
531
|
+
let closed = false;
|
|
532
|
+
const transportClosed = () => new ModbusNotConnectedError({
|
|
533
|
+
cause: new Error("Transport has been closed"),
|
|
534
|
+
message: "Transport has been closed"
|
|
535
|
+
});
|
|
536
|
+
const closeOrphan = (t) => Effect5.forkDaemon(Effect5.ignore(Effect5.tryPromise(() => t.close())));
|
|
537
|
+
const openTransport = Effect5.tryPromise({
|
|
538
|
+
try: () => openMethod(TC, openOptions),
|
|
539
|
+
catch: (error) => toModbusError(error)
|
|
540
|
+
}).pipe(Effect5.tap((t) => closed ? closeOrphan(t) : Effect5.zipRight(Effect5.sync(() => {
|
|
541
|
+
transport = t;
|
|
542
|
+
}), SubscriptionRef3.set(connectionState, ConnectionState.Connected()))));
|
|
543
|
+
const ensureOpen = Effect5.fnUntraced(function* () {
|
|
544
|
+
if (closed)
|
|
545
|
+
return yield* transportClosed();
|
|
546
|
+
if (transport)
|
|
547
|
+
return transport;
|
|
548
|
+
const t = yield* singleFlight(opening, openTransport);
|
|
549
|
+
if (closed)
|
|
550
|
+
return yield* transportClosed();
|
|
551
|
+
return t;
|
|
552
|
+
});
|
|
553
|
+
yield* Effect5.addFinalizer(() => {
|
|
554
|
+
if (closed)
|
|
555
|
+
return Effect5.void;
|
|
556
|
+
closed = true;
|
|
557
|
+
const t = transport;
|
|
558
|
+
if (!t)
|
|
559
|
+
return SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
|
|
560
|
+
return Effect5.andThen(Effect5.logDebug(`Closing ${serviceName}`), Effect5.zipRight(Effect5.promise(() => t.close()), SubscriptionRef3.set(connectionState, ConnectionState.Disconnected())));
|
|
561
|
+
});
|
|
562
|
+
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
563
|
+
const reconnectOnce = Effect5.suspend(() => {
|
|
564
|
+
const t = transport;
|
|
565
|
+
if (closed || !t)
|
|
566
|
+
return transportClosed();
|
|
567
|
+
return singleFlight(reconnecting, Effect5.tryPromise({
|
|
568
|
+
try: () => t.reconnect(),
|
|
569
|
+
catch: (error) => toModbusError(error)
|
|
570
|
+
}).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
|
|
571
|
+
});
|
|
572
|
+
const report = (error) => {
|
|
573
|
+
if (!supervised || closed || !supervised.triggers(error))
|
|
574
|
+
return Effect5.void;
|
|
575
|
+
return Effect5.gen(function* () {
|
|
576
|
+
const claimed = yield* SubscriptionRef3.modify(connectionState, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
577
|
+
if (!claimed)
|
|
578
|
+
return;
|
|
579
|
+
yield* Effect5.logDebug(`${serviceName}: reconnecting after ${error.message}`);
|
|
580
|
+
yield* Effect5.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
const resilience = {
|
|
584
|
+
guard: supervised ? guardCircuit(connectionState) : Effect5.void,
|
|
585
|
+
report
|
|
586
|
+
};
|
|
587
|
+
return {
|
|
588
|
+
connectionState,
|
|
589
|
+
withClient: Effect5.fnUntraced(function* (unitId, clientOptions) {
|
|
590
|
+
const t = yield* ensureOpen();
|
|
591
|
+
let client = clientSet.get(unitId);
|
|
592
|
+
if (!client) {
|
|
593
|
+
client = yield* Effect5.try({
|
|
594
|
+
try: () => t.createClient({ unitId }),
|
|
595
|
+
catch: (error) => toModbusError(error)
|
|
596
|
+
});
|
|
597
|
+
clientSet.set(unitId, client);
|
|
598
|
+
}
|
|
599
|
+
return withResilience(makeEffectModbusClient(client), {
|
|
600
|
+
...resilience,
|
|
601
|
+
policy: clientOptions?.retry ?? transportRetry
|
|
602
|
+
});
|
|
603
|
+
}),
|
|
604
|
+
setRequestTimeout: Effect5.fnUntraced(function* (timeoutMs) {
|
|
605
|
+
const t = transport;
|
|
606
|
+
if (!t || closed) {
|
|
607
|
+
return yield* new ModbusNotConnectedError({
|
|
608
|
+
cause: new Error(notConnectedMsg),
|
|
609
|
+
message: notConnectedMsg
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
t.setRequestTimeout(timeoutMs);
|
|
613
|
+
}),
|
|
614
|
+
clearRequestTimeout: Effect5.fnUntraced(function* () {
|
|
615
|
+
const t = transport;
|
|
616
|
+
if (!t || closed) {
|
|
617
|
+
return yield* new ModbusNotConnectedError({
|
|
618
|
+
cause: new Error(notConnectedMsg),
|
|
619
|
+
message: notConnectedMsg
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
t.clearRequestTimeout();
|
|
623
|
+
}),
|
|
624
|
+
reconnect: Effect5.fnUntraced(function* () {
|
|
625
|
+
if (closed)
|
|
626
|
+
return yield* transportClosed();
|
|
627
|
+
const t = transport;
|
|
628
|
+
if (!t) {
|
|
629
|
+
yield* ensureOpen();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
yield* singleFlight(reconnecting, Effect5.tryPromise({
|
|
633
|
+
try: () => t.reconnect(),
|
|
634
|
+
catch: (error) => toModbusError(error)
|
|
635
|
+
}).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
|
|
636
|
+
if (closed)
|
|
637
|
+
return yield* transportClosed();
|
|
638
|
+
yield* SubscriptionRef3.set(connectionState, ConnectionState.Connected());
|
|
639
|
+
}),
|
|
640
|
+
close: Effect5.fnUntraced(function* () {
|
|
641
|
+
if (closed)
|
|
642
|
+
return;
|
|
643
|
+
closed = true;
|
|
644
|
+
yield* SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
|
|
645
|
+
const t = transport;
|
|
646
|
+
if (t) {
|
|
647
|
+
yield* Effect5.tryPromise({
|
|
648
|
+
try: () => t.close(),
|
|
649
|
+
catch: (error) => toModbusError(error)
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
const scope = yield* Effect5.scope;
|
|
653
|
+
yield* Scope.close(scope, Exit.void);
|
|
654
|
+
}),
|
|
655
|
+
hasPendingRequests: () => {
|
|
656
|
+
if (closed)
|
|
657
|
+
return false;
|
|
658
|
+
const t = transport;
|
|
659
|
+
if (!t)
|
|
660
|
+
return false;
|
|
661
|
+
return t.pendingRequests;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
490
667
|
// src/AsciiTransportService.ts
|
|
491
|
-
class AsciiTransportService extends
|
|
668
|
+
class AsciiTransportService extends Effect6.Service()("AsciiTransportService", {
|
|
492
669
|
scoped: makeTransportScoped("AsyncAsciiTransport", (TC, options) => TC.open(options), "AsciiTransportService")
|
|
493
670
|
}) {
|
|
494
671
|
static makeMockTransport = (devices) => {
|
|
@@ -500,8 +677,8 @@ class AsciiTransportService extends Effect4.Service()("AsciiTransportService", {
|
|
|
500
677
|
import { Context, Layer as Layer3 } from "effect";
|
|
501
678
|
|
|
502
679
|
// src/RtuTransportService.ts
|
|
503
|
-
import { Effect as
|
|
504
|
-
class RtuTransportService extends
|
|
680
|
+
import { Effect as Effect7, Layer as Layer2 } from "effect";
|
|
681
|
+
class RtuTransportService extends Effect7.Service()("RtuTransportService", {
|
|
505
682
|
scoped: makeTransportScoped("AsyncRtuTransport", (TC, options) => TC.open(options), "RtuTransportService")
|
|
506
683
|
}) {
|
|
507
684
|
static makeMockTransport = (devices) => {
|
|
@@ -524,8 +701,8 @@ class SerialTransportService extends Context.Tag("SerialTransportService")() {
|
|
|
524
701
|
};
|
|
525
702
|
}
|
|
526
703
|
// src/TcpTransportService.ts
|
|
527
|
-
import { Effect as
|
|
528
|
-
class TcpTransportService extends
|
|
704
|
+
import { Effect as Effect8, Layer as Layer4 } from "effect";
|
|
705
|
+
class TcpTransportService extends Effect8.Service()("TcpTransportService", {
|
|
529
706
|
scoped: makeTransportScoped("AsyncTcpTransport", (TC, options) => TC.connect(options), "TcpTransportService")
|
|
530
707
|
}) {
|
|
531
708
|
static makeMockTransport = (devices) => {
|
|
@@ -534,68 +711,185 @@ class TcpTransportService extends Effect7.Service()("TcpTransportService", {
|
|
|
534
711
|
};
|
|
535
712
|
}
|
|
536
713
|
// src/SerialModbusServerService.ts
|
|
537
|
-
import { Effect as
|
|
538
|
-
var serialRtuServerLayer = (options, handlers) => Layer5.scopedDiscard(
|
|
539
|
-
const { AsyncSerialModbusServer } = yield*
|
|
540
|
-
const server = yield*
|
|
714
|
+
import { Effect as Effect9, Layer as Layer5 } from "effect";
|
|
715
|
+
var serialRtuServerLayer = (options, handlers) => Layer5.scopedDiscard(Effect9.gen(function* () {
|
|
716
|
+
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
717
|
+
const server = yield* Effect9.tryPromise({
|
|
541
718
|
try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
|
|
542
719
|
catch: (error) => toModbusError(error)
|
|
543
720
|
});
|
|
544
|
-
yield*
|
|
545
|
-
yield*
|
|
721
|
+
yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
|
|
722
|
+
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
546
723
|
try: () => server.shutdown(),
|
|
547
724
|
catch: (error) => toModbusError(error)
|
|
548
|
-
})),
|
|
725
|
+
})), Effect9.catchAll(() => Effect9.void)));
|
|
549
726
|
}));
|
|
550
|
-
var serialAsciiServerLayer = (options, handlers) => Layer5.scopedDiscard(
|
|
551
|
-
const { AsyncSerialModbusServer } = yield*
|
|
552
|
-
const server = yield*
|
|
727
|
+
var serialAsciiServerLayer = (options, handlers) => Layer5.scopedDiscard(Effect9.gen(function* () {
|
|
728
|
+
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
729
|
+
const server = yield* Effect9.tryPromise({
|
|
553
730
|
try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
|
|
554
731
|
catch: (error) => toModbusError(error)
|
|
555
732
|
});
|
|
556
|
-
yield*
|
|
557
|
-
yield*
|
|
733
|
+
yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
|
|
734
|
+
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
558
735
|
try: () => server.shutdown(),
|
|
559
736
|
catch: (error) => toModbusError(error)
|
|
560
|
-
})),
|
|
737
|
+
})), Effect9.catchAll(() => Effect9.void)));
|
|
561
738
|
}));
|
|
562
739
|
// src/TcpModbusServerService.ts
|
|
563
|
-
import { Effect as
|
|
564
|
-
var tcpServerLayer = (options, handlers) => Layer6.scopedDiscard(
|
|
565
|
-
const { AsyncTcpModbusServer } = yield*
|
|
566
|
-
const server = yield*
|
|
740
|
+
import { Effect as Effect10, Layer as Layer6 } from "effect";
|
|
741
|
+
var tcpServerLayer = (options, handlers) => Layer6.scopedDiscard(Effect10.gen(function* () {
|
|
742
|
+
const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
|
|
743
|
+
const server = yield* Effect10.tryPromise({
|
|
567
744
|
try: () => AsyncTcpModbusServer.bind(options, handlers),
|
|
568
745
|
catch: (error) => toModbusError(error)
|
|
569
746
|
});
|
|
570
|
-
yield*
|
|
571
|
-
yield*
|
|
747
|
+
yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
|
|
748
|
+
yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
|
|
572
749
|
try: () => server.shutdown(),
|
|
573
750
|
catch: (error) => toModbusError(error)
|
|
574
|
-
})),
|
|
751
|
+
})), Effect10.catchAll(() => Effect10.void)));
|
|
575
752
|
}));
|
|
576
753
|
// src/TcpGatewayService.ts
|
|
577
|
-
import { Effect as
|
|
578
|
-
var tcpGatewayLayer = (options, gatewayConfig) => Layer7.scopedDiscard(
|
|
579
|
-
const { AsyncTcpGateway } = yield*
|
|
580
|
-
const gateway = yield*
|
|
754
|
+
import { Effect as Effect11, Layer as Layer7 } from "effect";
|
|
755
|
+
var tcpGatewayLayer = (options, gatewayConfig) => Layer7.scopedDiscard(Effect11.gen(function* () {
|
|
756
|
+
const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
|
|
757
|
+
const gateway = yield* Effect11.tryPromise({
|
|
581
758
|
try: () => AsyncTcpGateway.bind(options, gatewayConfig),
|
|
582
759
|
catch: (error) => toModbusError(error)
|
|
583
760
|
});
|
|
584
|
-
yield*
|
|
585
|
-
yield*
|
|
761
|
+
yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
|
|
762
|
+
yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
|
|
586
763
|
try: () => gateway.shutdown(),
|
|
587
764
|
catch: (error) => toModbusError(error)
|
|
588
|
-
})),
|
|
765
|
+
})), Effect11.catchAll(() => Effect11.void)));
|
|
766
|
+
}));
|
|
767
|
+
// src/WasmWsTransportService.ts
|
|
768
|
+
import { Effect as Effect12, Layer as Layer8 } from "effect";
|
|
769
|
+
class WasmWsTransportService extends Effect12.Service()("WasmWsTransportService", {
|
|
770
|
+
scoped: makeTransportScoped("WasmWsTransport", (TC, options) => TC.connect(options), "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
771
|
+
}) {
|
|
772
|
+
static makeMockTransport = (devices) => {
|
|
773
|
+
const factory = makeMockTransport(devices);
|
|
774
|
+
return (options) => Layer8.scoped(WasmWsTransportService, factory(options));
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
// src/WasmRtuTransportService.ts
|
|
778
|
+
import { Effect as Effect13, Layer as Layer9 } from "effect";
|
|
779
|
+
class WasmRtuTransportService extends Effect13.Service()("WasmRtuTransportService", {
|
|
780
|
+
scoped: makeTransportScoped("WasmRtuTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
781
|
+
}) {
|
|
782
|
+
static makeMockTransport = (devices) => {
|
|
783
|
+
const factory = makeMockTransport(devices);
|
|
784
|
+
return (options) => Layer9.scoped(WasmRtuTransportService, factory(options));
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
// src/WasmAsciiTransportService.ts
|
|
788
|
+
import { Effect as Effect14, Layer as Layer10 } from "effect";
|
|
789
|
+
class WasmAsciiTransportService extends Effect14.Service()("WasmAsciiTransportService", {
|
|
790
|
+
scoped: makeTransportScoped("WasmAsciiTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
791
|
+
}) {
|
|
792
|
+
static makeMockTransport = (devices) => {
|
|
793
|
+
const factory = makeMockTransport(devices);
|
|
794
|
+
return (options) => Layer10.scoped(WasmAsciiTransportService, factory(options));
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
// src/WasmSerialTransportService.ts
|
|
798
|
+
import { Context as Context2, Layer as Layer11 } from "effect";
|
|
799
|
+
class WasmSerialTransportService extends Context2.Tag("WasmSerialTransportService")() {
|
|
800
|
+
static fromAscii(options) {
|
|
801
|
+
return Layer11.project(WasmAsciiTransportService, WasmSerialTransportService, (ascii) => ascii)(WasmAsciiTransportService.Default(options));
|
|
802
|
+
}
|
|
803
|
+
static fromRtu(options) {
|
|
804
|
+
return Layer11.project(WasmRtuTransportService, WasmSerialTransportService, (rtu) => rtu)(WasmRtuTransportService.Default(options));
|
|
805
|
+
}
|
|
806
|
+
static makeMockTransport = (devices) => {
|
|
807
|
+
const factory = makeMockTransport(devices);
|
|
808
|
+
return (options) => Layer11.scoped(WasmSerialTransportService, factory(options));
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
// src/WasmSerialPort.ts
|
|
812
|
+
import { Effect as Effect15 } from "effect";
|
|
813
|
+
var requestSerialPort = () => Effect15.tryPromise({
|
|
814
|
+
try: async () => {
|
|
815
|
+
const mod = await import("modbus-rs/web");
|
|
816
|
+
return mod.requestSerialPort();
|
|
817
|
+
},
|
|
818
|
+
catch: (error) => toModbusError(error)
|
|
819
|
+
});
|
|
820
|
+
// src/WasmTcpServerService.ts
|
|
821
|
+
import { Effect as Effect16, Layer as Layer12 } from "effect";
|
|
822
|
+
var wasmWsServerLayer = (options, handlers) => Layer12.scopedDiscard(Effect16.gen(function* () {
|
|
823
|
+
const { WasmWsModbusServer } = yield* Effect16.promise(() => import("modbus-rs/web"));
|
|
824
|
+
const server = yield* Effect16.tryPromise({
|
|
825
|
+
try: () => WasmWsModbusServer.bind(options, handlers),
|
|
826
|
+
catch: (error) => toModbusError(error)
|
|
827
|
+
});
|
|
828
|
+
yield* Effect16.logDebug(`WASM WS server bound to ${options.wsUrl}`);
|
|
829
|
+
yield* Effect16.forkScoped(Effect16.tryPromise({
|
|
830
|
+
try: () => server.serve(),
|
|
831
|
+
catch: (error) => toModbusError(error)
|
|
832
|
+
}).pipe(Effect16.catchAll((error) => Effect16.logError("WASM WS server loop ended", error))));
|
|
833
|
+
yield* Effect16.addFinalizer(() => Effect16.logDebug("WASM WS server shutting down").pipe(Effect16.andThen(Effect16.tryPromise({
|
|
834
|
+
try: () => server.shutdown(),
|
|
835
|
+
catch: (error) => toModbusError(error)
|
|
836
|
+
})), Effect16.catchAll(() => Effect16.void)));
|
|
837
|
+
}));
|
|
838
|
+
// src/WasmSerialModbusServerService.ts
|
|
839
|
+
import { Effect as Effect17, Layer as Layer13 } from "effect";
|
|
840
|
+
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.scopedDiscard(Effect17.gen(function* () {
|
|
841
|
+
const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
|
|
842
|
+
const server = yield* Effect17.tryPromise({
|
|
843
|
+
try: () => WasmSerialModbusServer.bindRtu(options, handlers),
|
|
844
|
+
catch: (error) => toModbusError(error)
|
|
845
|
+
});
|
|
846
|
+
yield* Effect17.logDebug("WASM serial RTU server bound");
|
|
847
|
+
yield* Effect17.forkScoped(Effect17.tryPromise({
|
|
848
|
+
try: () => server.serve(),
|
|
849
|
+
catch: (error) => toModbusError(error)
|
|
850
|
+
}).pipe(Effect17.catchAll((error) => Effect17.logError("WASM serial RTU server loop ended", error))));
|
|
851
|
+
yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial RTU server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
|
|
852
|
+
try: () => server.shutdown(),
|
|
853
|
+
catch: (error) => toModbusError(error)
|
|
854
|
+
})), Effect17.catchAll(() => Effect17.void)));
|
|
855
|
+
}));
|
|
856
|
+
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.scopedDiscard(Effect17.gen(function* () {
|
|
857
|
+
const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
|
|
858
|
+
const server = yield* Effect17.tryPromise({
|
|
859
|
+
try: () => WasmSerialModbusServer.bindAscii(options, handlers),
|
|
860
|
+
catch: (error) => toModbusError(error)
|
|
861
|
+
});
|
|
862
|
+
yield* Effect17.logDebug("WASM serial ASCII server bound");
|
|
863
|
+
yield* Effect17.forkScoped(Effect17.tryPromise({
|
|
864
|
+
try: () => server.serve(),
|
|
865
|
+
catch: (error) => toModbusError(error)
|
|
866
|
+
}).pipe(Effect17.catchAll((error) => Effect17.logError("WASM serial ASCII server loop ended", error))));
|
|
867
|
+
yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial ASCII server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
|
|
868
|
+
try: () => server.shutdown(),
|
|
869
|
+
catch: (error) => toModbusError(error)
|
|
870
|
+
})), Effect17.catchAll(() => Effect17.void)));
|
|
589
871
|
}));
|
|
590
872
|
export {
|
|
873
|
+
wasmWsServerLayer,
|
|
874
|
+
wasmSerialRtuServerLayer,
|
|
875
|
+
wasmSerialAsciiServerLayer,
|
|
591
876
|
toModbusError,
|
|
592
877
|
tcpServerLayer,
|
|
593
878
|
tcpGatewayLayer,
|
|
594
879
|
serialRtuServerLayer,
|
|
595
880
|
serialAsciiServerLayer,
|
|
881
|
+
retryableExceptionCodes,
|
|
882
|
+
retryModbus,
|
|
883
|
+
requestSerialPort,
|
|
884
|
+
makeRetryPolicy,
|
|
885
|
+
WasmWsTransportService,
|
|
886
|
+
WasmSerialTransportService,
|
|
887
|
+
WasmRtuTransportService,
|
|
888
|
+
WasmAsciiTransportService,
|
|
596
889
|
TcpTransportService,
|
|
597
890
|
SerialTransportService,
|
|
598
891
|
RtuTransportService,
|
|
892
|
+
RetryPolicies,
|
|
599
893
|
ModbusTransportError,
|
|
600
894
|
ModbusTimeoutError,
|
|
601
895
|
ModbusNotConnectedError,
|
|
@@ -603,5 +897,7 @@ export {
|
|
|
603
897
|
ModbusInternalError,
|
|
604
898
|
ModbusExceptionError,
|
|
605
899
|
ModbusConnectionClosedError,
|
|
900
|
+
ModbusCircuitOpenError,
|
|
901
|
+
ConnectionState,
|
|
606
902
|
AsciiTransportService
|
|
607
903
|
};
|