@tsplc/sim 0.0.0-stage → 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 +6 -2
- package/dist/hardware/cognex.js +169 -0
- package/dist/hardware/cognex.js.map +12 -0
- package/dist/hardware/generic.js +141 -0
- package/dist/hardware/generic.js.map +12 -0
- package/dist/hardware/intelligrated.js +158 -0
- package/dist/hardware/intelligrated.js.map +12 -0
- package/dist/index.js +605 -0
- package/dist/index.js.map +15 -0
- package/dist/serve.js +292 -0
- package/dist/serve.js.map +11 -0
- package/package.json +48 -4
- package/src/hardware/cognex.ts +165 -0
- package/src/hardware/generic.ts +74 -0
- package/src/hardware/intelligrated.ts +150 -0
- package/src/hardware.ts +167 -0
- package/src/index.ts +44 -0
- package/src/io.ts +27 -0
- package/src/layout.ts +117 -0
- package/src/line.ts +198 -0
- package/src/page.ts +208 -0
- package/src/paths.ts +51 -0
- package/src/serve.ts +113 -0
- package/src/simulation.ts +533 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
// packages/sim/src/paths.ts
|
|
2
|
+
var pathKey = Symbol("tsplc.path");
|
|
3
|
+
var join = (segments) => segments.reduce((path, segment) => /^\d+$/.test(segment) ? `${path}[${segment}]` : path ? `${path}.${segment}` : segment, "");
|
|
4
|
+
var proxy = (segments) => new Proxy(() => {
|
|
5
|
+
return;
|
|
6
|
+
}, {
|
|
7
|
+
get: (_, key) => {
|
|
8
|
+
if (key === pathKey)
|
|
9
|
+
return join(segments);
|
|
10
|
+
if (typeof key === "symbol")
|
|
11
|
+
return;
|
|
12
|
+
return proxy([...segments, key]);
|
|
13
|
+
},
|
|
14
|
+
apply: (_, __, [index]) => proxy([...segments.slice(0, -1), String(index)])
|
|
15
|
+
});
|
|
16
|
+
var plcPaths = () => proxy([]);
|
|
17
|
+
var pathName = (path) => {
|
|
18
|
+
const name = Reflect.get(path, pathKey);
|
|
19
|
+
if (typeof name !== "string" || !name)
|
|
20
|
+
throw new Error("Expected a path from plcPaths().");
|
|
21
|
+
return name;
|
|
22
|
+
};
|
|
23
|
+
// packages/sim/src/hardware.ts
|
|
24
|
+
var pinIn = (type) => ({ direction: "in", type });
|
|
25
|
+
var pinOut = (type) => ({ direction: "out", type });
|
|
26
|
+
var analog = (path, range) => {
|
|
27
|
+
const [plcLow, plcHigh] = range.plc, [pinLow, pinHigh] = range.pin;
|
|
28
|
+
return {
|
|
29
|
+
path,
|
|
30
|
+
toPin: (plc) => pinLow + (plc - plcLow) * (pinHigh - pinLow) / (plcHigh - plcLow),
|
|
31
|
+
toPlc: (pin) => Math.round(plcLow + (pin - pinLow) * (plcHigh - plcLow) / (pinHigh - pinLow))
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
var defaultValue = (type) => type === "bool" ? false : type === "number" ? 0 : [];
|
|
35
|
+
var isScaled = (wire) => ("toPin" in wire);
|
|
36
|
+
var wirePath = (wire) => isScaled(wire) ? pathName(wire.path) : pathName(wire);
|
|
37
|
+
var defineHardware = (model) => (options) => {
|
|
38
|
+
const state = model.setup(options.config);
|
|
39
|
+
const world = model.world();
|
|
40
|
+
const pins = Object.fromEntries(Object.entries(model.pins).map(([name, pin]) => [name, defaultValue(pin.type)]));
|
|
41
|
+
let dtMs = 0;
|
|
42
|
+
const context = () => ({
|
|
43
|
+
pins,
|
|
44
|
+
world,
|
|
45
|
+
config: options.config,
|
|
46
|
+
state,
|
|
47
|
+
dtMs
|
|
48
|
+
});
|
|
49
|
+
const wired = Object.entries(options.wiring).flatMap(([name, wire]) => {
|
|
50
|
+
const pin = model.pins[name];
|
|
51
|
+
if (!pin)
|
|
52
|
+
throw new Error(`${model.name} has no pin ${String(name)}.`);
|
|
53
|
+
const target = wire;
|
|
54
|
+
return target ? [{ name, pin, wire: target, path: wirePath(target) }] : [];
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
model: model.name,
|
|
58
|
+
world,
|
|
59
|
+
reads: wired.filter(({ pin }) => pin.direction === "in").map(({ path }) => path),
|
|
60
|
+
update: (plc, elapsed) => {
|
|
61
|
+
dtMs = elapsed;
|
|
62
|
+
for (const { name, pin, wire, path } of wired) {
|
|
63
|
+
if (pin.direction !== "in" || !plc.has(path))
|
|
64
|
+
continue;
|
|
65
|
+
const value = plc.get(path);
|
|
66
|
+
Reflect.set(pins, name, isScaled(wire) ? wire.toPin(Number(value)) : value);
|
|
67
|
+
}
|
|
68
|
+
model.update(context());
|
|
69
|
+
const writes = new Map;
|
|
70
|
+
for (const { name, pin, wire, path } of wired) {
|
|
71
|
+
if (pin.direction !== "out")
|
|
72
|
+
continue;
|
|
73
|
+
const value = Reflect.get(pins, name);
|
|
74
|
+
writes.set(path, isScaled(wire) ? wire.toPlc(Number(value)) : value);
|
|
75
|
+
}
|
|
76
|
+
return writes;
|
|
77
|
+
},
|
|
78
|
+
view: () => model.view?.(context()) ?? { status: "idle" },
|
|
79
|
+
pins: () => ({ ...pins }),
|
|
80
|
+
actions: Object.keys(model.actions ?? {}),
|
|
81
|
+
act: (action, argument) => {
|
|
82
|
+
const handler = model.actions?.[action];
|
|
83
|
+
if (!handler)
|
|
84
|
+
throw new Error(`${model.name} has no action ${action}.`);
|
|
85
|
+
handler(context(), argument);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
// packages/sim/src/line.ts
|
|
90
|
+
var part = (options) => ({ kind: "part", ...options });
|
|
91
|
+
var zone = (options) => part({
|
|
92
|
+
...options.id !== undefined && { id: options.id },
|
|
93
|
+
label: "zone",
|
|
94
|
+
length: options.length,
|
|
95
|
+
devices: { controller: options.controller },
|
|
96
|
+
motion: ({ controller }) => ({ along: controller.world.out.motor }),
|
|
97
|
+
sense: ({ blocked }, { controller }) => {
|
|
98
|
+
controller.world.in.photoeye = blocked(options.eye ?? options.length - 50);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
var section = (options) => part({
|
|
102
|
+
...options.id !== undefined && { id: options.id },
|
|
103
|
+
label: "section",
|
|
104
|
+
length: options.length,
|
|
105
|
+
devices: options.motor ? { motor: options.motor } : {},
|
|
106
|
+
motion: () => ({ along: options.motor?.world.out.motor ?? options.speed ?? 0 })
|
|
107
|
+
});
|
|
108
|
+
var pickup = (options) => ({ kind: "pickup", ...options });
|
|
109
|
+
var photoeye = (options) => ({
|
|
110
|
+
kind: "photoeye",
|
|
111
|
+
...options
|
|
112
|
+
});
|
|
113
|
+
var reader = (options) => ({ kind: "reader", ...options });
|
|
114
|
+
var conveyor = (options) => options;
|
|
115
|
+
var station = (options = {}) => options;
|
|
116
|
+
var targetNames = (line) => new Map([
|
|
117
|
+
...Object.entries(line.conveyors).map(([name, target]) => [target, name]),
|
|
118
|
+
...Object.entries(line.stations ?? {}).map(([name, target]) => [target, name])
|
|
119
|
+
]);
|
|
120
|
+
var defineLine = (line) => {
|
|
121
|
+
const names = targetNames(line);
|
|
122
|
+
for (const [id, item] of Object.entries(line.conveyors)) {
|
|
123
|
+
const next = item.next && "to" in item.next ? item.next.to : item.next;
|
|
124
|
+
const targets = [next];
|
|
125
|
+
for (const part of item.parts) {
|
|
126
|
+
if (part.kind === "part")
|
|
127
|
+
targets.push(part.exits?.left, part.exits?.right);
|
|
128
|
+
if (part.kind === "pickup")
|
|
129
|
+
targets.push(part.station);
|
|
130
|
+
}
|
|
131
|
+
for (const target of targets)
|
|
132
|
+
if (target !== undefined && !names.has(target))
|
|
133
|
+
throw new Error(`Conveyor ${id} leads to a conveyor or station missing from the line.`);
|
|
134
|
+
}
|
|
135
|
+
return line;
|
|
136
|
+
};
|
|
137
|
+
var keyed = (ids, make) => Object.fromEntries(ids.map((id) => [id, make(id)]));
|
|
138
|
+
// packages/sim/src/simulation.ts
|
|
139
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
140
|
+
|
|
141
|
+
// packages/sim/src/layout.ts
|
|
142
|
+
var scale = 0.05;
|
|
143
|
+
var exitClearance = 1000;
|
|
144
|
+
var direction = {
|
|
145
|
+
right: { x: 1, y: 0 },
|
|
146
|
+
left: { x: -1, y: 0 },
|
|
147
|
+
up: { x: 0, y: -1 },
|
|
148
|
+
down: { x: 0, y: 1 }
|
|
149
|
+
};
|
|
150
|
+
var leftOf = { right: "up", up: "left", left: "down", down: "right" };
|
|
151
|
+
var rightOf = { right: "down", down: "left", left: "up", up: "right" };
|
|
152
|
+
var lengthOf = (conveyor) => conveyor.parts.reduce((sum, part) => sum + (part.kind === "part" ? part.length : 0), 0);
|
|
153
|
+
var travel = (from, heading, distance) => ({
|
|
154
|
+
x: from.x + direction[heading].x * distance,
|
|
155
|
+
y: from.y + direction[heading].y * distance
|
|
156
|
+
});
|
|
157
|
+
var pointAt = (route, mm) => {
|
|
158
|
+
let remaining = mm;
|
|
159
|
+
for (const [index, heading] of route.headings.entries()) {
|
|
160
|
+
const from = route.points[index], to = route.points[index + 1], length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
|
|
161
|
+
if (remaining <= length || index === route.headings.length - 1)
|
|
162
|
+
return { point: travel(from, heading, remaining), heading };
|
|
163
|
+
remaining -= length;
|
|
164
|
+
}
|
|
165
|
+
throw new Error("A conveyor needs a route.");
|
|
166
|
+
};
|
|
167
|
+
var drawLine = (line) => {
|
|
168
|
+
const conveyors = Object.values(line.conveyors);
|
|
169
|
+
const isConveyor = new Set(conveyors);
|
|
170
|
+
const feeders = new Map;
|
|
171
|
+
for (const conveyor of conveyors) {
|
|
172
|
+
let position = 0;
|
|
173
|
+
for (const part of conveyor.parts) {
|
|
174
|
+
if (part.kind !== "part")
|
|
175
|
+
continue;
|
|
176
|
+
for (const side of ["left", "right"]) {
|
|
177
|
+
const exit = part.exits?.[side];
|
|
178
|
+
if (exit && isConveyor.has(exit))
|
|
179
|
+
feeders.set(exit, { from: conveyor, at: position + part.length / 2, side });
|
|
180
|
+
}
|
|
181
|
+
position += part.length;
|
|
182
|
+
}
|
|
183
|
+
const next = conveyor.next;
|
|
184
|
+
if (next && !("to" in next) && isConveyor.has(next))
|
|
185
|
+
feeders.set(next, { from: conveyor, at: position });
|
|
186
|
+
}
|
|
187
|
+
const routes = new Map;
|
|
188
|
+
const active = new Set;
|
|
189
|
+
const route = (conveyor) => {
|
|
190
|
+
const known = routes.get(conveyor);
|
|
191
|
+
if (known)
|
|
192
|
+
return known;
|
|
193
|
+
if (active.has(conveyor))
|
|
194
|
+
throw new Error("Conveyors with no `at` cannot feed each other.");
|
|
195
|
+
active.add(conveyor);
|
|
196
|
+
let start = conveyor.at ?? { x: 0, y: 0 };
|
|
197
|
+
let heading = conveyor.heading ?? "right";
|
|
198
|
+
const feeder = conveyor.at ? undefined : feeders.get(conveyor);
|
|
199
|
+
if (feeder) {
|
|
200
|
+
const exit = pointAt(route(feeder.from), feeder.at);
|
|
201
|
+
heading = !feeder.side ? exit.heading : feeder.side === "left" ? leftOf[exit.heading] : rightOf[exit.heading];
|
|
202
|
+
start = feeder.side ? travel(exit.point, heading, exitClearance) : exit.point;
|
|
203
|
+
}
|
|
204
|
+
const result = { points: [start], headings: [] };
|
|
205
|
+
let remaining = lengthOf(conveyor);
|
|
206
|
+
for (const turn of conveyor.turns ?? []) {
|
|
207
|
+
if (turn.after <= 0 || turn.after >= remaining)
|
|
208
|
+
throw new Error(`A turn ${turn.after} mm along must fall inside its conveyor.`);
|
|
209
|
+
result.headings.push(heading);
|
|
210
|
+
result.points.push(travel(result.points.at(-1), heading, turn.after));
|
|
211
|
+
remaining -= turn.after;
|
|
212
|
+
heading = turn.to;
|
|
213
|
+
}
|
|
214
|
+
result.headings.push(heading);
|
|
215
|
+
result.points.push(travel(result.points.at(-1), heading, remaining));
|
|
216
|
+
active.delete(conveyor);
|
|
217
|
+
routes.set(conveyor, result);
|
|
218
|
+
return result;
|
|
219
|
+
};
|
|
220
|
+
const pixels = (mm) => Math.round(mm * scale * 100) / 100;
|
|
221
|
+
return new Map(conveyors.map((conveyor) => [
|
|
222
|
+
conveyor,
|
|
223
|
+
route(conveyor).points.map(({ x, y }, index) => `${index ? "L" : "M"} ${pixels(x)} ${pixels(y)}`).join(" ")
|
|
224
|
+
]));
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// packages/sim/src/simulation.ts
|
|
228
|
+
var same = (a, b) => Array.isArray(a) && Array.isArray(b) ? a.length === b.length && a.every((item, index) => item === b[index]) : a === b;
|
|
229
|
+
var resolve = (id, conveyor, draw, name) => {
|
|
230
|
+
const segments = [];
|
|
231
|
+
const points = [];
|
|
232
|
+
let position = 0;
|
|
233
|
+
conveyor.parts.forEach((part, index) => {
|
|
234
|
+
const partId = part.id ?? `${id}[${index}]`;
|
|
235
|
+
if (part.kind !== "part") {
|
|
236
|
+
points.push({ id: partId, part, at: position + (part.offset ?? 0) });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
segments.push({ id: partId, part, start: position, end: position + part.length });
|
|
240
|
+
position += part.length;
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
id,
|
|
244
|
+
draw,
|
|
245
|
+
length: position,
|
|
246
|
+
segments,
|
|
247
|
+
points,
|
|
248
|
+
...conveyor.next !== undefined && {
|
|
249
|
+
next: "to" in conveyor.next ? { to: name(conveyor.next.to), at: conveyor.next.at } : { to: name(conveyor.next), at: 0 }
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
var devicesOf = (segment) => Object.entries(segment.part.devices).map(([name, device]) => [`${segment.id}.${name}`, device]);
|
|
254
|
+
var along = (motion) => ("along" in motion) ? Math.max(0, motion.along) : 0;
|
|
255
|
+
var createSimulation = (line, options) => {
|
|
256
|
+
const names = targetNames(line);
|
|
257
|
+
const name = (target) => {
|
|
258
|
+
const found = names.get(target);
|
|
259
|
+
if (found === undefined)
|
|
260
|
+
throw new Error("A part leads outside the line.");
|
|
261
|
+
return found;
|
|
262
|
+
};
|
|
263
|
+
const drawings = drawLine(line);
|
|
264
|
+
const tracks = new Map(Object.entries(line.conveyors).map(([id, item]) => [
|
|
265
|
+
id,
|
|
266
|
+
resolve(id, item, drawings.get(item), name)
|
|
267
|
+
]));
|
|
268
|
+
const stations = new Map(Object.entries(line.stations ?? {}));
|
|
269
|
+
const devices = new Map;
|
|
270
|
+
const lineDevices = new Set(Object.keys(line.devices ?? {}));
|
|
271
|
+
for (const [id, device] of Object.entries(line.devices ?? {}))
|
|
272
|
+
devices.set(id, device);
|
|
273
|
+
for (const track of tracks.values()) {
|
|
274
|
+
for (const segment of track.segments)
|
|
275
|
+
for (const [id, device] of devicesOf(segment))
|
|
276
|
+
devices.set(id, device);
|
|
277
|
+
for (const point of track.points)
|
|
278
|
+
if (point.part.kind === "reader")
|
|
279
|
+
devices.set(point.id, point.part.device);
|
|
280
|
+
}
|
|
281
|
+
const reads = [...new Set([...devices.values()].flatMap((device) => device.reads))];
|
|
282
|
+
const placed = new Map;
|
|
283
|
+
const written = new Map;
|
|
284
|
+
const pendingReads = new Map;
|
|
285
|
+
let timeMs = 0, sequence = 0, error = null;
|
|
286
|
+
const onConveyor = (conveyor) => [...placed.values()].flatMap(({ load, place }) => place.kind === "conveyor" && place.conveyor === conveyor ? [{ load, place }] : []).sort((a, b) => b.place.head - a.place.head);
|
|
287
|
+
const footprints = (track) => [
|
|
288
|
+
...onConveyor(track.id).map(({ load, place }) => ({ load, head: place.head, place })),
|
|
289
|
+
...track.next && tracks.has(track.next.to) ? onConveyor(track.next.to).flatMap(({ load, place }) => place.from?.conveyor === track.id && place.head - place.from.at < load.length ? [{ load, head: track.length + place.head - place.from.at, place }] : []) : []
|
|
290
|
+
];
|
|
291
|
+
const covers = (head, length, at) => head - length <= at && at <= head;
|
|
292
|
+
const stationLoads = (id) => [...placed.values()].filter(({ place }) => place.kind === "station" && place.station === id).length;
|
|
293
|
+
const sense = () => {
|
|
294
|
+
const writes = new Map;
|
|
295
|
+
for (const track of tracks.values()) {
|
|
296
|
+
const loads = footprints(track);
|
|
297
|
+
const blocked = (at) => loads.some(({ load, head, place }) => covers(head, load.length, at) && Math.abs(place.lateral) < load.width / 2);
|
|
298
|
+
for (const segment of track.segments) {
|
|
299
|
+
const onPart = loads.filter(({ load, head }) => head > segment.start && head - load.length < segment.end).map(({ load, head, place }) => ({
|
|
300
|
+
head: head - segment.start,
|
|
301
|
+
tail: head - load.length - segment.start,
|
|
302
|
+
lateral: place.lateral
|
|
303
|
+
}));
|
|
304
|
+
const sensed = segment.part.sense?.({
|
|
305
|
+
blocked: (at) => blocked(segment.start + at),
|
|
306
|
+
occupied: onPart.length > 0,
|
|
307
|
+
loads: onPart
|
|
308
|
+
}, segment.part.devices);
|
|
309
|
+
for (const [wire, value] of sensed ?? [])
|
|
310
|
+
writes.set(wirePath(wire), value);
|
|
311
|
+
}
|
|
312
|
+
for (const point of track.points) {
|
|
313
|
+
if (point.part.kind === "photoeye")
|
|
314
|
+
writes.set(wirePath(point.part.output), blocked(point.at));
|
|
315
|
+
else if (point.part.kind === "reader") {
|
|
316
|
+
const queue = pendingReads.get(point.id) ?? [];
|
|
317
|
+
point.part.device.world.in.read = queue.shift() ?? null;
|
|
318
|
+
pendingReads.set(point.id, queue);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const [id, item] of stations)
|
|
323
|
+
for (const sensor of item.sensors ?? [])
|
|
324
|
+
writes.set(wirePath(sensor.output), sensor.active({ loads: stationLoads(id) }));
|
|
325
|
+
return writes;
|
|
326
|
+
};
|
|
327
|
+
const enter = (target, load, head, from) => {
|
|
328
|
+
const track = tracks.get(target);
|
|
329
|
+
if (track) {
|
|
330
|
+
const blocked = onConveyor(target).some((other) => other.load !== load && other.place.head > head - load.length && other.place.head - other.load.length < head);
|
|
331
|
+
if (blocked)
|
|
332
|
+
return false;
|
|
333
|
+
placed.set(load.id, {
|
|
334
|
+
load,
|
|
335
|
+
place: {
|
|
336
|
+
kind: "conveyor",
|
|
337
|
+
conveyor: target,
|
|
338
|
+
head,
|
|
339
|
+
lateral: 0,
|
|
340
|
+
...from !== undefined && { from }
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
const capacity = stations.get(target)?.capacity;
|
|
346
|
+
if (capacity !== undefined && stationLoads(target) >= capacity)
|
|
347
|
+
return false;
|
|
348
|
+
placed.set(load.id, { load, place: { kind: "station", station: target } });
|
|
349
|
+
return true;
|
|
350
|
+
};
|
|
351
|
+
const move = (dtMs) => {
|
|
352
|
+
for (const track of tracks.values()) {
|
|
353
|
+
const motions = new Map(track.segments.map((segment) => [segment, segment.part.motion(segment.part.devices)]));
|
|
354
|
+
let limit = Number.POSITIVE_INFINITY;
|
|
355
|
+
for (const { load, place } of onConveyor(track.id)) {
|
|
356
|
+
if (place.kind !== "conveyor")
|
|
357
|
+
continue;
|
|
358
|
+
const tail = place.head - load.length;
|
|
359
|
+
const middle = place.head - load.length / 2;
|
|
360
|
+
const across = track.segments.find((segment) => segment.start <= middle && middle <= segment.end);
|
|
361
|
+
const motion = across && motions.get(across);
|
|
362
|
+
if (across && motion && "across" in motion) {
|
|
363
|
+
place.lateral += motion.across * dtMs / 1000;
|
|
364
|
+
const width = across.part.width ?? 0;
|
|
365
|
+
const exit = across.part.exits?.[place.lateral > 0 ? "right" : "left"];
|
|
366
|
+
if (Math.abs(place.lateral) >= width && (exit === undefined || !enter(name(exit), load, load.length)))
|
|
367
|
+
place.lateral = Math.sign(place.lateral) * width;
|
|
368
|
+
limit = tail;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (place.lateral !== 0) {
|
|
372
|
+
limit = tail;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const covered = track.segments.filter(({ start, end }) => start < place.head && end > Math.max(tail, 0));
|
|
376
|
+
const speed = covered.length ? Math.min(...covered.map((segment) => along(motions.get(segment) ?? { held: true }))) : 0;
|
|
377
|
+
let target = Math.min(place.head + speed * dtMs / 1000, limit);
|
|
378
|
+
let picked = false;
|
|
379
|
+
for (const point of track.points) {
|
|
380
|
+
if (target < point.at)
|
|
381
|
+
continue;
|
|
382
|
+
if (point.part.kind === "pickup") {
|
|
383
|
+
if (picked || place.head > point.at)
|
|
384
|
+
continue;
|
|
385
|
+
picked = enter(name(point.part.station), load, 0);
|
|
386
|
+
if (!picked)
|
|
387
|
+
target = point.at;
|
|
388
|
+
} else if (point.part.kind === "reader" && place.head < point.at)
|
|
389
|
+
pendingReads.set(point.id, [...pendingReads.get(point.id) ?? [], load.code]);
|
|
390
|
+
}
|
|
391
|
+
if (picked)
|
|
392
|
+
continue;
|
|
393
|
+
const next = track.next;
|
|
394
|
+
if (target > track.length && next !== undefined) {
|
|
395
|
+
const entered = enter(next.to, load, next.at + target - track.length, {
|
|
396
|
+
conveyor: track.id,
|
|
397
|
+
at: next.at
|
|
398
|
+
});
|
|
399
|
+
if (!entered)
|
|
400
|
+
place.head = track.length;
|
|
401
|
+
} else
|
|
402
|
+
place.head = Math.min(target, track.length);
|
|
403
|
+
limit = place.head - load.length;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const { load, place } of placed.values())
|
|
407
|
+
if (place.kind === "waiting")
|
|
408
|
+
enter(place.conveyor, load, load.length);
|
|
409
|
+
};
|
|
410
|
+
const pointView = (point) => {
|
|
411
|
+
switch (point.part.kind) {
|
|
412
|
+
case "reader":
|
|
413
|
+
return point.part.device.view();
|
|
414
|
+
case "photoeye":
|
|
415
|
+
return { status: written.get(wirePath(point.part.output)) ? "busy" : "idle" };
|
|
416
|
+
case "pickup":
|
|
417
|
+
return {
|
|
418
|
+
status: stationLoads(name(point.part.station)) ? "busy" : "idle",
|
|
419
|
+
label: name(point.part.station)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const segmentView = (segment) => {
|
|
424
|
+
const views = devicesOf(segment).map(([, device]) => device.view());
|
|
425
|
+
if (!views.length)
|
|
426
|
+
return {
|
|
427
|
+
status: along(segment.part.motion(segment.part.devices)) > 0 ? "running" : "stopped"
|
|
428
|
+
};
|
|
429
|
+
const order = ["fault", "warning", "busy", "running", "stopped", "idle"];
|
|
430
|
+
return views.reduce((worst, view) => order.indexOf(view.status) < order.indexOf(worst.status) ? view : worst);
|
|
431
|
+
};
|
|
432
|
+
const simulation = {
|
|
433
|
+
tick: async (dtMs) => {
|
|
434
|
+
const plc = await options.io.read(reads);
|
|
435
|
+
const writes = sense();
|
|
436
|
+
for (const device of devices.values())
|
|
437
|
+
for (const [path, value] of device.update(plc, dtMs))
|
|
438
|
+
writes.set(path, value);
|
|
439
|
+
const changed = new Map([...writes].filter(([path, value]) => !same(written.get(path), value)));
|
|
440
|
+
if (changed.size) {
|
|
441
|
+
await options.io.write(changed);
|
|
442
|
+
for (const [path, value] of changed)
|
|
443
|
+
written.set(path, value);
|
|
444
|
+
}
|
|
445
|
+
move(dtMs);
|
|
446
|
+
timeMs += dtMs;
|
|
447
|
+
},
|
|
448
|
+
run: async (signal, options = {}) => {
|
|
449
|
+
const periodMs = options.periodMs ?? 50;
|
|
450
|
+
let last = performance.now();
|
|
451
|
+
while (!signal.aborted) {
|
|
452
|
+
const now = performance.now();
|
|
453
|
+
if (!options.paused?.())
|
|
454
|
+
await simulation.step(Math.min(now - last, 1000));
|
|
455
|
+
last = now;
|
|
456
|
+
await delay(Math.max(0, periodMs - (performance.now() - now)), undefined, {
|
|
457
|
+
signal
|
|
458
|
+
}).catch(() => {
|
|
459
|
+
return;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
step: async (dtMs) => {
|
|
464
|
+
try {
|
|
465
|
+
await simulation.tick(dtMs);
|
|
466
|
+
error = null;
|
|
467
|
+
} catch (cause) {
|
|
468
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
469
|
+
written.clear();
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
addLoad: (conveyor, load) => {
|
|
473
|
+
const kind = line.loads[load.kind];
|
|
474
|
+
if (!kind)
|
|
475
|
+
throw new Error(`Unknown load kind ${load.kind}.`);
|
|
476
|
+
if (!tracks.has(conveyor))
|
|
477
|
+
throw new Error(`Unknown conveyor ${conveyor}.`);
|
|
478
|
+
const created = {
|
|
479
|
+
id: `L${++sequence}`,
|
|
480
|
+
length: kind.length,
|
|
481
|
+
width: kind.width ?? kind.length,
|
|
482
|
+
...load
|
|
483
|
+
};
|
|
484
|
+
placed.set(created.id, { load: created, place: { kind: "waiting", conveyor } });
|
|
485
|
+
return created;
|
|
486
|
+
},
|
|
487
|
+
place: (id, target, at) => {
|
|
488
|
+
const entry = placed.get(id);
|
|
489
|
+
if (!entry)
|
|
490
|
+
throw new Error(`Unknown load ${id}.`);
|
|
491
|
+
if (tracks.has(target) && at === undefined)
|
|
492
|
+
entry.place = { kind: "waiting", conveyor: target };
|
|
493
|
+
else if (!enter(target, entry.load, (at ?? 0) + entry.load.length))
|
|
494
|
+
throw new Error(`No room for ${entry.load.code} at ${target}.`);
|
|
495
|
+
},
|
|
496
|
+
remove: (id) => {
|
|
497
|
+
placed.delete(id);
|
|
498
|
+
},
|
|
499
|
+
loads: () => [...placed.values()].map(({ load, place }) => ({ load, place })),
|
|
500
|
+
loadsAt: (station) => [...placed.values()].filter(({ place }) => place.kind === "station" && place.station === station).map(({ load }) => load),
|
|
501
|
+
act: (device, action, argument = "") => {
|
|
502
|
+
const target = devices.get(device);
|
|
503
|
+
if (!target)
|
|
504
|
+
throw new Error(`Unknown device ${device}.`);
|
|
505
|
+
target.act(action, argument);
|
|
506
|
+
},
|
|
507
|
+
state: () => ({
|
|
508
|
+
timeMs,
|
|
509
|
+
error,
|
|
510
|
+
conveyors: [...tracks.values()].map((track) => ({
|
|
511
|
+
id: track.id,
|
|
512
|
+
draw: track.draw,
|
|
513
|
+
length: track.length,
|
|
514
|
+
...track.next !== undefined && { next: track.next.to },
|
|
515
|
+
parts: [
|
|
516
|
+
...track.segments.map((segment) => ({
|
|
517
|
+
id: segment.id,
|
|
518
|
+
kind: segment.part.kind,
|
|
519
|
+
...segment.part.label !== undefined && { label: segment.part.label },
|
|
520
|
+
start: segment.start,
|
|
521
|
+
end: segment.end,
|
|
522
|
+
...segment.part.exits && {
|
|
523
|
+
exits: {
|
|
524
|
+
...segment.part.exits.left && { left: name(segment.part.exits.left) },
|
|
525
|
+
...segment.part.exits.right && { right: name(segment.part.exits.right) }
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
view: segmentView(segment)
|
|
529
|
+
})),
|
|
530
|
+
...track.points.map((point) => ({
|
|
531
|
+
id: point.id,
|
|
532
|
+
kind: point.part.kind,
|
|
533
|
+
start: point.at,
|
|
534
|
+
end: point.at,
|
|
535
|
+
...point.part.kind === "pickup" && {
|
|
536
|
+
station: name(point.part.station),
|
|
537
|
+
side: point.part.side ?? "right"
|
|
538
|
+
},
|
|
539
|
+
view: pointView(point)
|
|
540
|
+
}))
|
|
541
|
+
]
|
|
542
|
+
})),
|
|
543
|
+
stations: [...stations].map(([id, item]) => ({
|
|
544
|
+
id,
|
|
545
|
+
label: item.label ?? id,
|
|
546
|
+
loads: stationLoads(id)
|
|
547
|
+
})),
|
|
548
|
+
loads: [...placed.values()].map(({ load, place }) => ({
|
|
549
|
+
id: load.id,
|
|
550
|
+
kind: load.kind,
|
|
551
|
+
code: load.code,
|
|
552
|
+
length: load.length,
|
|
553
|
+
width: load.width,
|
|
554
|
+
place: { ...place }
|
|
555
|
+
})),
|
|
556
|
+
devices: [...devices].map(([id, device]) => ({
|
|
557
|
+
id,
|
|
558
|
+
part: lineDevices.has(id) ? null : id.split(".")[0],
|
|
559
|
+
model: device.model,
|
|
560
|
+
view: device.view(),
|
|
561
|
+
pins: device.pins(),
|
|
562
|
+
actions: device.actions
|
|
563
|
+
}))
|
|
564
|
+
})
|
|
565
|
+
};
|
|
566
|
+
return simulation;
|
|
567
|
+
};
|
|
568
|
+
// packages/sim/src/io.ts
|
|
569
|
+
var plcIo = (plc) => ({
|
|
570
|
+
read: async (paths) => {
|
|
571
|
+
const readings = await plc.read(paths);
|
|
572
|
+
return new Map(paths.map((path) => {
|
|
573
|
+
const reading = readings[path];
|
|
574
|
+
if (reading?.quality !== "good")
|
|
575
|
+
throw new Error(`Cannot read ${path}: ${reading?.status ?? "missing"}`);
|
|
576
|
+
return [path, reading.value];
|
|
577
|
+
}));
|
|
578
|
+
},
|
|
579
|
+
write: async (values) => {
|
|
580
|
+
await plc.write(values);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
export {
|
|
584
|
+
analog,
|
|
585
|
+
conveyor,
|
|
586
|
+
createSimulation,
|
|
587
|
+
defineHardware,
|
|
588
|
+
defineLine,
|
|
589
|
+
keyed,
|
|
590
|
+
part,
|
|
591
|
+
pathName,
|
|
592
|
+
photoeye,
|
|
593
|
+
pickup,
|
|
594
|
+
pinIn,
|
|
595
|
+
pinOut,
|
|
596
|
+
plcIo,
|
|
597
|
+
plcPaths,
|
|
598
|
+
reader,
|
|
599
|
+
section,
|
|
600
|
+
station,
|
|
601
|
+
zone
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
//# debugId=68AEBE1381BBE02964756E2164756E21
|
|
605
|
+
//# sourceMappingURL=index.js.map
|