@hotmeshio/long-tail 0.25.4 → 0.27.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 +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +36 -7
- package/build/lib/events/nats.d.ts +11 -0
- package/build/lib/events/nats.js +33 -2
- package/build/lib/machines/index.d.ts +6 -0
- package/build/lib/machines/index.js +22 -0
- package/build/lib/machines/line.d.ts +44 -0
- package/build/lib/machines/line.js +114 -0
- package/build/lib/machines/protocol/codec.d.ts +108 -0
- package/build/lib/machines/protocol/codec.js +80 -0
- package/build/lib/machines/protocol/constants.d.ts +120 -0
- package/build/lib/machines/protocol/constants.js +97 -0
- package/build/lib/machines/protocol/contract.d.ts +19 -0
- package/build/lib/machines/protocol/contract.js +141 -0
- package/build/lib/machines/protocol/envelope.d.ts +14 -0
- package/build/lib/machines/protocol/envelope.js +43 -0
- package/build/lib/machines/protocol/index.d.ts +5 -0
- package/build/lib/machines/protocol/index.js +21 -0
- package/build/lib/machines/protocol/topics.d.ts +33 -0
- package/build/lib/machines/protocol/topics.js +79 -0
- package/build/lib/machines/site/edge-account.d.ts +11 -0
- package/build/lib/machines/site/edge-account.js +24 -0
- package/build/lib/machines/site/mosquitto-conf.d.ts +27 -0
- package/build/lib/machines/site/mosquitto-conf.js +81 -0
- package/build/lib/machines/site/site-conf.d.ts +23 -0
- package/build/lib/machines/site/site-conf.js +62 -0
- package/build/lib/machines/transport/callback-feed.d.ts +18 -0
- package/build/lib/machines/transport/callback-feed.js +31 -0
- package/build/lib/machines/transport/delivery.d.ts +20 -0
- package/build/lib/machines/transport/delivery.js +37 -0
- package/build/lib/machines/transport/index.d.ts +13 -0
- package/build/lib/machines/transport/index.js +29 -0
- package/build/lib/machines/transport/memory-broker.d.ts +35 -0
- package/build/lib/machines/transport/memory-broker.js +145 -0
- package/build/lib/machines/transport/memory.d.ts +40 -0
- package/build/lib/machines/transport/memory.js +79 -0
- package/build/lib/machines/transport/mqtt-bus.d.ts +24 -0
- package/build/lib/machines/transport/mqtt-bus.js +43 -0
- package/build/lib/machines/transport/mqtt-link.d.ts +16 -0
- package/build/lib/machines/transport/mqtt-link.js +37 -0
- package/build/lib/machines/transport/mqtt-session.d.ts +34 -0
- package/build/lib/machines/transport/mqtt-session.js +127 -0
- package/build/lib/machines/transport/nats-bus.d.ts +36 -0
- package/build/lib/machines/transport/nats-bus.js +47 -0
- package/build/lib/machines/transport/nats-feed.d.ts +26 -0
- package/build/lib/machines/transport/nats-feed.js +49 -0
- package/build/lib/machines/transport/pubsub-feed.d.ts +17 -0
- package/build/lib/machines/transport/pubsub-feed.js +39 -0
- package/build/lib/machines/transport/serial.d.ts +21 -0
- package/build/lib/machines/transport/serial.js +54 -0
- package/build/lib/machines/transport/stream-bus.d.ts +40 -0
- package/build/lib/machines/transport/stream-bus.js +57 -0
- package/build/lib/machines/transport/streams.d.ts +24 -0
- package/build/lib/machines/transport/streams.js +49 -0
- package/build/lib/messaging/config.d.ts +25 -0
- package/build/lib/messaging/config.js +55 -0
- package/build/lib/messaging/constants.d.ts +26 -0
- package/build/lib/messaging/constants.js +26 -0
- package/build/lib/messaging/index.d.ts +24 -0
- package/build/lib/messaging/index.js +65 -0
- package/build/lib/messaging/memory.d.ts +16 -0
- package/build/lib/messaging/memory.js +147 -0
- package/build/lib/messaging/nats/permissions.d.ts +13 -0
- package/build/lib/messaging/nats/permissions.js +22 -0
- package/build/lib/messaging/nats/provider.d.ts +30 -0
- package/build/lib/messaging/nats/provider.js +77 -0
- package/build/lib/messaging/nats/render-conf.d.ts +68 -0
- package/build/lib/messaging/nats/render-conf.js +58 -0
- package/build/lib/messaging/nats/streams.d.ts +26 -0
- package/build/lib/messaging/nats/streams.js +97 -0
- package/build/lib/messaging/subjects.d.ts +8 -0
- package/build/lib/messaging/subjects.js +25 -0
- package/build/scripts/bambu-boat.d.ts +17 -0
- package/build/scripts/bambu-boat.js +134 -0
- package/build/scripts/bambu-print.d.ts +18 -0
- package/build/scripts/bambu-print.js +114 -0
- package/build/scripts/machine-line.d.ts +33 -0
- package/build/scripts/machine-line.js +301 -0
- package/build/services/machines/commands.d.ts +29 -0
- package/build/services/machines/commands.js +37 -0
- package/build/services/machines/config.d.ts +49 -0
- package/build/services/machines/config.js +94 -0
- package/build/services/machines/contracts.d.ts +210 -0
- package/build/services/machines/contracts.js +124 -0
- package/build/services/machines/forms.d.ts +55 -0
- package/build/services/machines/forms.js +37 -0
- package/build/services/machines/gateway/actions.d.ts +111 -0
- package/build/services/machines/gateway/actions.js +8 -0
- package/build/services/machines/gateway/address-book.d.ts +16 -0
- package/build/services/machines/gateway/address-book.js +24 -0
- package/build/services/machines/gateway/admission.d.ts +35 -0
- package/build/services/machines/gateway/admission.js +66 -0
- package/build/services/machines/gateway/api-escalations.d.ts +23 -0
- package/build/services/machines/gateway/api-escalations.js +101 -0
- package/build/services/machines/gateway/dispatcher.d.ts +76 -0
- package/build/services/machines/gateway/dispatcher.js +197 -0
- package/build/services/machines/gateway/escalations.d.ts +51 -0
- package/build/services/machines/gateway/escalations.js +40 -0
- package/build/services/machines/gateway/executor.d.ts +60 -0
- package/build/services/machines/gateway/executor.js +221 -0
- package/build/services/machines/gateway/gateway.d.ts +88 -0
- package/build/services/machines/gateway/gateway.js +270 -0
- package/build/services/machines/gateway/index.d.ts +10 -0
- package/build/services/machines/gateway/index.js +26 -0
- package/build/services/machines/gateway/jobs.d.ts +16 -0
- package/build/services/machines/gateway/jobs.js +24 -0
- package/build/services/machines/gateway/lifecycle.d.ts +21 -0
- package/build/services/machines/gateway/lifecycle.js +89 -0
- package/build/services/machines/gateway/log.d.ts +41 -0
- package/build/services/machines/gateway/log.js +42 -0
- package/build/services/machines/gateway/lt-http.d.ts +51 -0
- package/build/services/machines/gateway/lt-http.js +120 -0
- package/build/services/machines/gateway/pacing.d.ts +43 -0
- package/build/services/machines/gateway/pacing.js +158 -0
- package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
- package/build/services/machines/gateway/plan-escalation.js +210 -0
- package/build/services/machines/gateway/plan-machine.d.ts +17 -0
- package/build/services/machines/gateway/plan-machine.js +283 -0
- package/build/services/machines/gateway/plan-shared.d.ts +18 -0
- package/build/services/machines/gateway/plan-shared.js +36 -0
- package/build/services/machines/gateway/provision.d.ts +22 -0
- package/build/services/machines/gateway/provision.js +79 -0
- package/build/services/machines/gateway/receipts.d.ts +33 -0
- package/build/services/machines/gateway/receipts.js +65 -0
- package/build/services/machines/gateway/reconcile.d.ts +35 -0
- package/build/services/machines/gateway/reconcile.js +99 -0
- package/build/services/machines/gateway/registration.d.ts +38 -0
- package/build/services/machines/gateway/registration.js +121 -0
- package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
- package/build/services/machines/gateway/reply-waiter.js +65 -0
- package/build/services/machines/gateway/state.d.ts +145 -0
- package/build/services/machines/gateway/state.js +241 -0
- package/build/services/machines/gateway/trace.d.ts +16 -0
- package/build/services/machines/gateway/trace.js +43 -0
- package/build/services/machines/identities.d.ts +25 -0
- package/build/services/machines/identities.js +77 -0
- package/build/services/machines/index.d.ts +11 -0
- package/build/services/machines/index.js +27 -0
- package/build/services/machines/leadership.d.ts +35 -0
- package/build/services/machines/leadership.js +100 -0
- package/build/services/machines/roles.d.ts +13 -0
- package/build/services/machines/roles.js +62 -0
- package/build/services/machines/rows.d.ts +30 -0
- package/build/services/machines/rows.js +71 -0
- package/build/services/machines/select.d.ts +25 -0
- package/build/services/machines/select.js +76 -0
- package/build/services/machines/surface.d.ts +33 -0
- package/build/services/machines/surface.js +104 -0
- package/build/services/machines/testing/fake-long-tail.d.ts +79 -0
- package/build/services/machines/testing/fake-long-tail.js +158 -0
- package/build/services/machines/testing/index.d.ts +3 -0
- package/build/services/machines/testing/index.js +19 -0
- package/build/services/machines/testing/recording-log.d.ts +14 -0
- package/build/services/machines/testing/recording-log.js +22 -0
- package/build/services/machines/testing/wait.d.ts +3 -0
- package/build/services/machines/testing/wait.js +17 -0
- package/build/services/mcp/db-server/schemas.d.ts +2 -2
- package/build/start/adapters.js +11 -2
- package/build/start/index.js +20 -1
- package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
- package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
- package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/index.d.ts +3 -1
- package/build/types/machines.d.ts +167 -0
- package/build/types/machines.js +7 -0
- package/build/types/messaging.d.ts +72 -0
- package/build/types/messaging.js +11 -0
- package/build/types/startup.d.ts +49 -1
- package/docs/events.md +50 -4
- package/docs/machines.md +322 -0
- package/package.json +13 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line protocol: how a machine on an automated line talks to the line
|
|
3
|
+
* controller over MQTT. Each machine publishes under its own subtree and
|
|
4
|
+
* hears only its own commands. Every message is a small JSON envelope.
|
|
5
|
+
*
|
|
6
|
+
* {root}/{site}/{line}/{machine}/presence retained; birth and will, with the software version
|
|
7
|
+
* {root}/{site}/{line}/{machine}/snapshot retained; full machine state and its boot id
|
|
8
|
+
* {root}/{site}/{line}/{machine}/job job lifecycle and unit handoff
|
|
9
|
+
* {root}/{site}/{line}/{machine}/alarm/{id} retained while active
|
|
10
|
+
* {root}/{site}/{line}/{machine}/telemetry best effort readings
|
|
11
|
+
* {root}/{site}/{line}/{machine}/cmd controller to machine
|
|
12
|
+
* {root}/{site}/{line}/{machine}/cmd/reply machine to controller
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROTOCOL_VERSION = "1";
|
|
15
|
+
export declare const QOS: {
|
|
16
|
+
readonly BEST_EFFORT: 0;
|
|
17
|
+
readonly RELIABLE: 1;
|
|
18
|
+
};
|
|
19
|
+
/** A command with no reply inside this window counts as refused. */
|
|
20
|
+
export declare const REPLY_DEADLINE_MS = 5000;
|
|
21
|
+
export declare const MAX_PAYLOAD_BYTES: number;
|
|
22
|
+
export declare const PRESENCE_HEARTBEAT_SEC = 30;
|
|
23
|
+
export declare const SNAPSHOT_HEARTBEAT_SEC = 60;
|
|
24
|
+
export declare const CHANNEL: {
|
|
25
|
+
readonly PRESENCE: "presence";
|
|
26
|
+
readonly SNAPSHOT: "snapshot";
|
|
27
|
+
readonly JOB: "job";
|
|
28
|
+
readonly ALARM: "alarm";
|
|
29
|
+
readonly TELEMETRY: "telemetry";
|
|
30
|
+
readonly CMD: "cmd";
|
|
31
|
+
readonly REPLY: "cmd/reply";
|
|
32
|
+
/** report/<name>: production, availability, inventory and other reporting, kept in the event stream. */
|
|
33
|
+
readonly REPORT: "report";
|
|
34
|
+
};
|
|
35
|
+
export declare const MODE: {
|
|
36
|
+
readonly IDLE: "idle";
|
|
37
|
+
readonly RUNNING: "running";
|
|
38
|
+
readonly CHANGEOVER: "changeover";
|
|
39
|
+
readonly FAULTED: "faulted";
|
|
40
|
+
readonly EMERGENCY_STOP: "emergency-stop";
|
|
41
|
+
readonly MAINTENANCE: "maintenance";
|
|
42
|
+
};
|
|
43
|
+
export type Mode = typeof MODE[keyof typeof MODE];
|
|
44
|
+
export declare const PRODUCTIVE_MODES: readonly Mode[];
|
|
45
|
+
export declare const LEVEL: {
|
|
46
|
+
readonly INFO: "info";
|
|
47
|
+
readonly WARNING: "warning";
|
|
48
|
+
readonly CRITICAL: "critical";
|
|
49
|
+
readonly EMERGENCY: "emergency";
|
|
50
|
+
};
|
|
51
|
+
export type Level = typeof LEVEL[keyof typeof LEVEL];
|
|
52
|
+
/** Levels that stop work and need a person. */
|
|
53
|
+
export declare const STOPPING_LEVELS: readonly Level[];
|
|
54
|
+
/**
|
|
55
|
+
* Job events, one per unit: a job carries one or more units through a
|
|
56
|
+
* machine together. A unit moves between machines in a handshake: the
|
|
57
|
+
* receiver says READY, the sender says RELEASED, the receiver confirms
|
|
58
|
+
* ARRIVED. ARRIVED is the receipt that ends a handoff. WITHDRAWN means the
|
|
59
|
+
* job left the queue, by a cancel or by the machine giving it up.
|
|
60
|
+
*/
|
|
61
|
+
export declare const JOB_EVENT: {
|
|
62
|
+
readonly QUEUED: "queued";
|
|
63
|
+
readonly READY: "ready";
|
|
64
|
+
readonly ARRIVED: "arrived";
|
|
65
|
+
/** The receiver put the unit on its carrier; some machines report this instead of an arrival. */
|
|
66
|
+
readonly BOUND: "bound";
|
|
67
|
+
readonly STARTED: "started";
|
|
68
|
+
readonly PROGRESS: "progress";
|
|
69
|
+
readonly FINISHED: "finished";
|
|
70
|
+
readonly RELEASED: "released";
|
|
71
|
+
readonly TRANSFER_FAILED: "transfer-failed";
|
|
72
|
+
readonly WITHDRAWN: "withdrawn";
|
|
73
|
+
};
|
|
74
|
+
export type JobEvent = typeof JOB_EVENT[keyof typeof JOB_EVENT];
|
|
75
|
+
export declare const JOB_RESULT: {
|
|
76
|
+
readonly OK: "ok";
|
|
77
|
+
readonly REJECT: "reject";
|
|
78
|
+
readonly HOLD: "hold";
|
|
79
|
+
readonly SCRAP: "scrap";
|
|
80
|
+
readonly REWORK: "rework";
|
|
81
|
+
};
|
|
82
|
+
export type JobResult = typeof JOB_RESULT[keyof typeof JOB_RESULT];
|
|
83
|
+
export declare const TRANSFER_FAILURE: {
|
|
84
|
+
readonly JAM: "jam";
|
|
85
|
+
readonly INCOMPLETE_HANDSHAKE: "incomplete-handshake";
|
|
86
|
+
readonly CARRIER_MISMATCH: "carrier-mismatch";
|
|
87
|
+
readonly UNREADABLE_ID: "unreadable-id";
|
|
88
|
+
};
|
|
89
|
+
export type TransferFailure = typeof TRANSFER_FAILURE[keyof typeof TRANSFER_FAILURE];
|
|
90
|
+
/** Commands. Every command carries a cmdId; a repeated cmdId is answered, never run twice. */
|
|
91
|
+
export declare const OP: {
|
|
92
|
+
readonly DISPATCH: "dispatch";
|
|
93
|
+
readonly CANCEL: "cancel";
|
|
94
|
+
/** Take work: begin or continue the queue. */
|
|
95
|
+
readonly START: "start";
|
|
96
|
+
/** Finish the current unit, then take no more; the queue is kept. */
|
|
97
|
+
readonly STOP: "stop";
|
|
98
|
+
/** Halt at the next safe point, holding the current unit; nothing new starts. */
|
|
99
|
+
readonly PAUSE: "pause";
|
|
100
|
+
/** Continue after a pause. */
|
|
101
|
+
readonly RESUME: "resume";
|
|
102
|
+
/** Load a recipe version the machine holds (`args.recipe`); refused when it holds none. */
|
|
103
|
+
readonly CHANGEOVER: "changeover";
|
|
104
|
+
readonly ACKNOWLEDGE_ALARM: "ack-alarm";
|
|
105
|
+
readonly SNAPSHOT: "snapshot";
|
|
106
|
+
};
|
|
107
|
+
export type Op = typeof OP[keyof typeof OP];
|
|
108
|
+
/** Commands a host may ask of a machine through a machine-command row. */
|
|
109
|
+
export declare const CONTROL_OPS: readonly Op[];
|
|
110
|
+
/**
|
|
111
|
+
* A command's reply. `accepted` means recorded; a machine that prepares the work first (fetch, verify)
|
|
112
|
+
* may later send `completed` (ready) or `failed` under the same cmdId.
|
|
113
|
+
*/
|
|
114
|
+
export declare const REPLY_STATUS: {
|
|
115
|
+
readonly ACCEPTED: "accepted";
|
|
116
|
+
readonly COMPLETED: "completed";
|
|
117
|
+
readonly REFUSED: "refused";
|
|
118
|
+
readonly FAILED: "failed";
|
|
119
|
+
};
|
|
120
|
+
export type ReplyStatus = typeof REPLY_STATUS[keyof typeof REPLY_STATUS];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The line protocol: how a machine on an automated line talks to the line
|
|
4
|
+
* controller over MQTT. Each machine publishes under its own subtree and
|
|
5
|
+
* hears only its own commands. Every message is a small JSON envelope.
|
|
6
|
+
*
|
|
7
|
+
* {root}/{site}/{line}/{machine}/presence retained; birth and will, with the software version
|
|
8
|
+
* {root}/{site}/{line}/{machine}/snapshot retained; full machine state and its boot id
|
|
9
|
+
* {root}/{site}/{line}/{machine}/job job lifecycle and unit handoff
|
|
10
|
+
* {root}/{site}/{line}/{machine}/alarm/{id} retained while active
|
|
11
|
+
* {root}/{site}/{line}/{machine}/telemetry best effort readings
|
|
12
|
+
* {root}/{site}/{line}/{machine}/cmd controller to machine
|
|
13
|
+
* {root}/{site}/{line}/{machine}/cmd/reply machine to controller
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.REPLY_STATUS = exports.CONTROL_OPS = exports.OP = exports.TRANSFER_FAILURE = exports.JOB_RESULT = exports.JOB_EVENT = exports.STOPPING_LEVELS = exports.LEVEL = exports.PRODUCTIVE_MODES = exports.MODE = exports.CHANNEL = exports.SNAPSHOT_HEARTBEAT_SEC = exports.PRESENCE_HEARTBEAT_SEC = exports.MAX_PAYLOAD_BYTES = exports.REPLY_DEADLINE_MS = exports.QOS = exports.PROTOCOL_VERSION = void 0;
|
|
17
|
+
exports.PROTOCOL_VERSION = '1';
|
|
18
|
+
exports.QOS = { BEST_EFFORT: 0, RELIABLE: 1 };
|
|
19
|
+
/** A command with no reply inside this window counts as refused. */
|
|
20
|
+
exports.REPLY_DEADLINE_MS = 5_000;
|
|
21
|
+
exports.MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
22
|
+
exports.PRESENCE_HEARTBEAT_SEC = 30;
|
|
23
|
+
exports.SNAPSHOT_HEARTBEAT_SEC = 60;
|
|
24
|
+
exports.CHANNEL = {
|
|
25
|
+
PRESENCE: 'presence',
|
|
26
|
+
SNAPSHOT: 'snapshot',
|
|
27
|
+
JOB: 'job',
|
|
28
|
+
ALARM: 'alarm',
|
|
29
|
+
TELEMETRY: 'telemetry',
|
|
30
|
+
CMD: 'cmd',
|
|
31
|
+
REPLY: 'cmd/reply',
|
|
32
|
+
/** report/<name>: production, availability, inventory and other reporting, kept in the event stream. */
|
|
33
|
+
REPORT: 'report',
|
|
34
|
+
};
|
|
35
|
+
exports.MODE = {
|
|
36
|
+
IDLE: 'idle',
|
|
37
|
+
RUNNING: 'running',
|
|
38
|
+
CHANGEOVER: 'changeover',
|
|
39
|
+
FAULTED: 'faulted',
|
|
40
|
+
EMERGENCY_STOP: 'emergency-stop',
|
|
41
|
+
MAINTENANCE: 'maintenance',
|
|
42
|
+
};
|
|
43
|
+
exports.PRODUCTIVE_MODES = [exports.MODE.IDLE, exports.MODE.RUNNING];
|
|
44
|
+
exports.LEVEL = { INFO: 'info', WARNING: 'warning', CRITICAL: 'critical', EMERGENCY: 'emergency' };
|
|
45
|
+
/** Levels that stop work and need a person. */
|
|
46
|
+
exports.STOPPING_LEVELS = [exports.LEVEL.CRITICAL, exports.LEVEL.EMERGENCY];
|
|
47
|
+
/**
|
|
48
|
+
* Job events, one per unit: a job carries one or more units through a
|
|
49
|
+
* machine together. A unit moves between machines in a handshake: the
|
|
50
|
+
* receiver says READY, the sender says RELEASED, the receiver confirms
|
|
51
|
+
* ARRIVED. ARRIVED is the receipt that ends a handoff. WITHDRAWN means the
|
|
52
|
+
* job left the queue, by a cancel or by the machine giving it up.
|
|
53
|
+
*/
|
|
54
|
+
exports.JOB_EVENT = {
|
|
55
|
+
QUEUED: 'queued',
|
|
56
|
+
READY: 'ready',
|
|
57
|
+
ARRIVED: 'arrived',
|
|
58
|
+
/** The receiver put the unit on its carrier; some machines report this instead of an arrival. */
|
|
59
|
+
BOUND: 'bound',
|
|
60
|
+
STARTED: 'started',
|
|
61
|
+
PROGRESS: 'progress',
|
|
62
|
+
FINISHED: 'finished',
|
|
63
|
+
RELEASED: 'released',
|
|
64
|
+
TRANSFER_FAILED: 'transfer-failed',
|
|
65
|
+
WITHDRAWN: 'withdrawn',
|
|
66
|
+
};
|
|
67
|
+
exports.JOB_RESULT = { OK: 'ok', REJECT: 'reject', HOLD: 'hold', SCRAP: 'scrap', REWORK: 'rework' };
|
|
68
|
+
exports.TRANSFER_FAILURE = {
|
|
69
|
+
JAM: 'jam',
|
|
70
|
+
INCOMPLETE_HANDSHAKE: 'incomplete-handshake',
|
|
71
|
+
CARRIER_MISMATCH: 'carrier-mismatch',
|
|
72
|
+
UNREADABLE_ID: 'unreadable-id',
|
|
73
|
+
};
|
|
74
|
+
/** Commands. Every command carries a cmdId; a repeated cmdId is answered, never run twice. */
|
|
75
|
+
exports.OP = {
|
|
76
|
+
DISPATCH: 'dispatch',
|
|
77
|
+
CANCEL: 'cancel',
|
|
78
|
+
/** Take work: begin or continue the queue. */
|
|
79
|
+
START: 'start',
|
|
80
|
+
/** Finish the current unit, then take no more; the queue is kept. */
|
|
81
|
+
STOP: 'stop',
|
|
82
|
+
/** Halt at the next safe point, holding the current unit; nothing new starts. */
|
|
83
|
+
PAUSE: 'pause',
|
|
84
|
+
/** Continue after a pause. */
|
|
85
|
+
RESUME: 'resume',
|
|
86
|
+
/** Load a recipe version the machine holds (`args.recipe`); refused when it holds none. */
|
|
87
|
+
CHANGEOVER: 'changeover',
|
|
88
|
+
ACKNOWLEDGE_ALARM: 'ack-alarm',
|
|
89
|
+
SNAPSHOT: 'snapshot',
|
|
90
|
+
};
|
|
91
|
+
/** Commands a host may ask of a machine through a machine-command row. */
|
|
92
|
+
exports.CONTROL_OPS = [exports.OP.START, exports.OP.STOP, exports.OP.PAUSE, exports.OP.RESUME, exports.OP.CHANGEOVER, exports.OP.CANCEL];
|
|
93
|
+
/**
|
|
94
|
+
* A command's reply. `accepted` means recorded; a machine that prepares the work first (fetch, verify)
|
|
95
|
+
* may later send `completed` (ready) or `failed` under the same cmdId.
|
|
96
|
+
*/
|
|
97
|
+
exports.REPLY_STATUS = { ACCEPTED: 'accepted', COMPLETED: 'completed', REFUSED: 'refused', FAILED: 'failed' };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line protocol as JSON Schema plus the rules a schema cannot express.
|
|
3
|
+
* Known fields are checked; unknown fields are allowed, so a machine on a
|
|
4
|
+
* newer minor version can add fields without breaking older gateways. The
|
|
5
|
+
* gateway checks every inbound message; the tests check everything the mock sends.
|
|
6
|
+
*/
|
|
7
|
+
import { type Envelope } from './envelope';
|
|
8
|
+
import { type ParsedTopic } from './topics';
|
|
9
|
+
export interface Inspection {
|
|
10
|
+
parsed: ParsedTopic | null;
|
|
11
|
+
body: Envelope | null;
|
|
12
|
+
violations: string[];
|
|
13
|
+
/** Set by a codec whose machines await a receipt: the id to confirm once the gateway is done with the message. */
|
|
14
|
+
receiptId?: string;
|
|
15
|
+
/** The station the message says it comes from, when the wire names one apart from the address (a line that learns addresses). */
|
|
16
|
+
claim?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Validate one message. `retained` is the publisher's flag when the transport reports it. */
|
|
19
|
+
export declare function inspectMessage(root: string, topic: string, raw: string | Buffer, retained?: boolean): Inspection;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The line protocol as JSON Schema plus the rules a schema cannot express.
|
|
4
|
+
* Known fields are checked; unknown fields are allowed, so a machine on a
|
|
5
|
+
* newer minor version can add fields without breaking older gateways. The
|
|
6
|
+
* gateway checks every inbound message; the tests check everything the mock sends.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.inspectMessage = inspectMessage;
|
|
13
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
14
|
+
const constants_1 = require("./constants");
|
|
15
|
+
const envelope_1 = require("./envelope");
|
|
16
|
+
const topics_1 = require("./topics");
|
|
17
|
+
const STR = { type: 'string' };
|
|
18
|
+
const STR_N = { type: ['string', 'null'] };
|
|
19
|
+
const NUM = { type: 'number' };
|
|
20
|
+
const INT0 = { type: 'integer', minimum: 0 };
|
|
21
|
+
const BOOL = { type: 'boolean' };
|
|
22
|
+
const TS = { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$' };
|
|
23
|
+
const TS_N = { anyOf: [TS, { type: 'null' }] };
|
|
24
|
+
const UUID = { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' };
|
|
25
|
+
const oneOf = (o, nullable = false) => ({ enum: nullable ? [...Object.values(o), null] : Object.values(o) });
|
|
26
|
+
function shape(props, optional = []) {
|
|
27
|
+
return { type: 'object', properties: props, required: Object.keys(props).filter((k) => !optional.includes(k)) };
|
|
28
|
+
}
|
|
29
|
+
const ENVELOPE = shape({
|
|
30
|
+
v: { type: 'string', pattern: '^1(\\.\\d+)?$' },
|
|
31
|
+
id: UUID,
|
|
32
|
+
at: TS,
|
|
33
|
+
source: shape({ site: STR, line: STR, machine: STR, station: STR }, ['station']),
|
|
34
|
+
body: { type: 'object' },
|
|
35
|
+
});
|
|
36
|
+
const RECIPE = { anyOf: [shape({ id: STR, version: STR }), { type: 'null' }] };
|
|
37
|
+
const BODY = {
|
|
38
|
+
[topics_1.MESSAGE_KIND.PRESENCE]: shape({ online: BOOL, software: STR }, ['software']),
|
|
39
|
+
[topics_1.MESSAGE_KIND.SNAPSHOT]: shape({
|
|
40
|
+
bootId: STR, software: STR, mode: oneOf(constants_1.MODE), enabled: BOOL, recipe: RECIPE,
|
|
41
|
+
jobs: { type: 'array', items: shape({ jobId: STR, units: { type: 'array', items: STR }, status: STR }) },
|
|
42
|
+
alarms: { type: 'array', items: STR },
|
|
43
|
+
cycles: INT0,
|
|
44
|
+
}),
|
|
45
|
+
[topics_1.MESSAGE_KIND.JOB]: shape({
|
|
46
|
+
event: oneOf(constants_1.JOB_EVENT), jobId: STR, unitId: STR, orderRef: STR, recipe: RECIPE, carrierId: STR_N, peer: STR_N,
|
|
47
|
+
result: oneOf(constants_1.JOB_RESULT), reason: STR_N,
|
|
48
|
+
measurements: {
|
|
49
|
+
type: 'array',
|
|
50
|
+
items: shape({ name: STR, value: NUM, unit: STR_N, min: { type: ['number', 'null'] }, max: { type: ['number', 'null'] }, ok: BOOL }),
|
|
51
|
+
},
|
|
52
|
+
materials: { type: 'array', items: shape({ material: STR, lot: STR }) },
|
|
53
|
+
startedAt: TS, endedAt: TS, durationMs: INT0, failure: oneOf(constants_1.TRANSFER_FAILURE),
|
|
54
|
+
percent: { type: 'number', minimum: 0, maximum: 100 }, remainingSec: INT0, stage: STR, detail: { type: 'object' },
|
|
55
|
+
}, ['jobId', 'unitId', 'peer', 'result', 'reason', 'measurements', 'materials', 'startedAt', 'endedAt', 'durationMs', 'failure', 'percent', 'remainingSec', 'stage', 'detail']),
|
|
56
|
+
[topics_1.MESSAGE_KIND.ALARM]: shape({
|
|
57
|
+
alarmId: STR, code: STR, level: oneOf(constants_1.LEVEL), text: STR, active: BOOL,
|
|
58
|
+
raisedAt: TS, clearedAt: TS_N, clearedBy: STR_N, ackedBy: STR_N, ackedAt: TS_N,
|
|
59
|
+
zone: STR_N, autoClears: BOOL, action: STR_N, jobId: STR_N,
|
|
60
|
+
}),
|
|
61
|
+
[topics_1.MESSAGE_KIND.TELEMETRY]: shape({ readings: { type: 'object', additionalProperties: NUM } }),
|
|
62
|
+
[topics_1.MESSAGE_KIND.CMD]: shape({ cmdId: STR, op: oneOf(constants_1.OP), args: { type: 'object' } }),
|
|
63
|
+
[topics_1.MESSAGE_KIND.REPLY]: shape({ cmdId: STR, status: oneOf(constants_1.REPLY_STATUS), reason: STR }),
|
|
64
|
+
[topics_1.MESSAGE_KIND.REPORT]: { type: 'object' },
|
|
65
|
+
};
|
|
66
|
+
const ajv = new ajv_1.default({ allErrors: true, strict: false });
|
|
67
|
+
const ENVELOPE_V = ajv.compile(ENVELOPE);
|
|
68
|
+
const BODY_V = Object.fromEntries(Object.entries(BODY).map(([k, s]) => [k, ajv.compile(s)]));
|
|
69
|
+
const RETAINED = [topics_1.MESSAGE_KIND.PRESENCE, topics_1.MESSAGE_KIND.SNAPSHOT, topics_1.MESSAGE_KIND.ALARM];
|
|
70
|
+
const errorsOf = (prefix, fn) => (fn.errors ?? []).map((e) => `${prefix}: ${e.instancePath || '.'} ${e.message}`);
|
|
71
|
+
/** Validate one message. `retained` is the publisher's flag when the transport reports it. */
|
|
72
|
+
function inspectMessage(root, topic, raw, retained) {
|
|
73
|
+
const parsed = (0, topics_1.parseTopic)(root, topic);
|
|
74
|
+
if (!parsed)
|
|
75
|
+
return { parsed, body: null, violations: ['unknown topic'] };
|
|
76
|
+
const violations = [];
|
|
77
|
+
const size = typeof raw === 'string' ? Buffer.byteLength(raw) : raw.length;
|
|
78
|
+
if (size > constants_1.MAX_PAYLOAD_BYTES)
|
|
79
|
+
violations.push(`payload ${size} B exceeds ${constants_1.MAX_PAYLOAD_BYTES} B`);
|
|
80
|
+
if (size === 0) {
|
|
81
|
+
if (parsed.kind !== topics_1.MESSAGE_KIND.ALARM)
|
|
82
|
+
violations.push('empty payload');
|
|
83
|
+
return { parsed, body: null, violations };
|
|
84
|
+
}
|
|
85
|
+
const body = (0, envelope_1.parsePayload)(raw);
|
|
86
|
+
if (!body)
|
|
87
|
+
return { parsed, body: null, violations: [...violations, 'not a JSON object'] };
|
|
88
|
+
if (!ENVELOPE_V(body))
|
|
89
|
+
violations.push(...errorsOf('envelope', ENVELOPE_V));
|
|
90
|
+
const bodyV = BODY_V[parsed.kind];
|
|
91
|
+
if (!bodyV(body.body ?? {}))
|
|
92
|
+
violations.push(...errorsOf(parsed.kind, bodyV));
|
|
93
|
+
violations.push(...crossFieldRules(parsed, body, retained));
|
|
94
|
+
return { parsed, body, violations };
|
|
95
|
+
}
|
|
96
|
+
/** Job events a receiver may report naming only the carrier it scanned. */
|
|
97
|
+
const CARRIER_EVENTS = [constants_1.JOB_EVENT.ARRIVED, constants_1.JOB_EVENT.TRANSFER_FAILED];
|
|
98
|
+
function crossFieldRules(parsed, env, retained) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const b = env.body ?? {};
|
|
101
|
+
const { ident, kind, key } = parsed;
|
|
102
|
+
const s = env.source ?? {};
|
|
103
|
+
if (s.site !== ident.siteId || s.line !== ident.lineId || s.machine !== ident.machineId)
|
|
104
|
+
out.push('source disagrees with the topic');
|
|
105
|
+
if (kind === topics_1.MESSAGE_KIND.ALARM && b.alarmId !== key)
|
|
106
|
+
out.push('alarmId disagrees with the topic');
|
|
107
|
+
if (kind === topics_1.MESSAGE_KIND.JOB) {
|
|
108
|
+
// A scan at a receiver may read only the carrier; every other job event names its unit and job.
|
|
109
|
+
const byCarrier = CARRIER_EVENTS.includes(b.event) && typeof b.carrierId === 'string';
|
|
110
|
+
if (b.unitId === undefined && !byCarrier)
|
|
111
|
+
out.push(`${b.event} without a unitId`);
|
|
112
|
+
if (b.jobId === undefined && !byCarrier)
|
|
113
|
+
out.push(`${b.event} without a jobId`);
|
|
114
|
+
if (b.event === constants_1.JOB_EVENT.FINISHED) {
|
|
115
|
+
for (const field of ['result', 'measurements', 'materials', 'startedAt', 'endedAt']) {
|
|
116
|
+
if (!(field in b))
|
|
117
|
+
out.push(`finished without ${field}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (b.result && b.result !== constants_1.JOB_RESULT.OK && !b.reason)
|
|
121
|
+
out.push(`${b.result} without a reason`);
|
|
122
|
+
if (b.event === constants_1.JOB_EVENT.TRANSFER_FAILED && !b.failure)
|
|
123
|
+
out.push('transfer-failed without a failure');
|
|
124
|
+
// The schema has already reported a malformed list; these rules read only well-formed entries.
|
|
125
|
+
for (const m of Array.isArray(b.measurements) ? b.measurements : []) {
|
|
126
|
+
if (!m || typeof m !== 'object')
|
|
127
|
+
continue;
|
|
128
|
+
if (m.min != null && m.max != null && (m.min <= m.value && m.value <= m.max) !== m.ok)
|
|
129
|
+
out.push(`${m.name}: ok disagrees with min/max`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (retained === undefined)
|
|
133
|
+
return out;
|
|
134
|
+
if ((kind === topics_1.MESSAGE_KIND.PRESENCE || kind === topics_1.MESSAGE_KIND.SNAPSHOT) && !retained)
|
|
135
|
+
out.push(`${kind} not retained`);
|
|
136
|
+
if (kind === topics_1.MESSAGE_KIND.ALARM && b.active === true && !retained)
|
|
137
|
+
out.push('active alarm not retained');
|
|
138
|
+
if (!RETAINED.includes(kind) && retained)
|
|
139
|
+
out.push(`${kind} must not be retained`);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Envelope, MachineIdent } from '../../../types/machines';
|
|
2
|
+
export type { Envelope, MachineIdent };
|
|
3
|
+
/** The machine's wire address: its topic level and client id. */
|
|
4
|
+
export declare function addressOf(ident: MachineIdent): string;
|
|
5
|
+
/** {site}-{line}-{address}, stable across reconnects. */
|
|
6
|
+
export declare function clientIdOf(ident: MachineIdent): string;
|
|
7
|
+
/** ISO 8601 UTC with milliseconds. */
|
|
8
|
+
export declare function isoTs(at?: Date | number): string;
|
|
9
|
+
export declare function buildEnvelope<T>(ident: MachineIdent, body: T, opts?: {
|
|
10
|
+
at?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
}): Envelope<T>;
|
|
13
|
+
/** Parse a JSON payload; null when it is empty or not an object. */
|
|
14
|
+
export declare function parsePayload(raw: string | Buffer): Envelope | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addressOf = addressOf;
|
|
4
|
+
exports.clientIdOf = clientIdOf;
|
|
5
|
+
exports.isoTs = isoTs;
|
|
6
|
+
exports.buildEnvelope = buildEnvelope;
|
|
7
|
+
exports.parsePayload = parsePayload;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const constants_1 = require("./constants");
|
|
10
|
+
/** The machine's wire address: its topic level and client id. */
|
|
11
|
+
function addressOf(ident) {
|
|
12
|
+
return ident.address ?? ident.machineId;
|
|
13
|
+
}
|
|
14
|
+
/** {site}-{line}-{address}, stable across reconnects. */
|
|
15
|
+
function clientIdOf(ident) {
|
|
16
|
+
return `${ident.siteId}-${ident.lineId}-${addressOf(ident)}`;
|
|
17
|
+
}
|
|
18
|
+
/** ISO 8601 UTC with milliseconds. */
|
|
19
|
+
function isoTs(at = Date.now()) {
|
|
20
|
+
return new Date(at).toISOString();
|
|
21
|
+
}
|
|
22
|
+
function buildEnvelope(ident, body, opts = {}) {
|
|
23
|
+
return {
|
|
24
|
+
v: constants_1.PROTOCOL_VERSION,
|
|
25
|
+
id: opts.id ?? (0, crypto_1.randomUUID)(),
|
|
26
|
+
at: opts.at ?? isoTs(),
|
|
27
|
+
source: { site: ident.siteId, line: ident.lineId, machine: addressOf(ident), ...(ident.address && ident.address !== ident.machineId ? { station: ident.machineId } : {}) },
|
|
28
|
+
body,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Parse a JSON payload; null when it is empty or not an object. */
|
|
32
|
+
function parsePayload(raw) {
|
|
33
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf8');
|
|
34
|
+
if (!text)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(text);
|
|
38
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./constants"), exports);
|
|
18
|
+
__exportStar(require("./envelope"), exports);
|
|
19
|
+
__exportStar(require("./topics"), exports);
|
|
20
|
+
__exportStar(require("./contract"), exports);
|
|
21
|
+
__exportStar(require("./codec"), exports);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic builders and the parser that classifies an inbound topic. NATS
|
|
3
|
+
* carries an MQTT topic on the subject with `/` replaced by `.`, so no topic
|
|
4
|
+
* level may contain `.`, `/`, `+`, `#` or whitespace.
|
|
5
|
+
*/
|
|
6
|
+
import { type MachineIdent } from './envelope';
|
|
7
|
+
export declare const MESSAGE_KIND: {
|
|
8
|
+
readonly PRESENCE: "presence";
|
|
9
|
+
readonly SNAPSHOT: "snapshot";
|
|
10
|
+
readonly JOB: "job";
|
|
11
|
+
readonly ALARM: "alarm";
|
|
12
|
+
readonly TELEMETRY: "telemetry";
|
|
13
|
+
readonly CMD: "cmd";
|
|
14
|
+
readonly REPLY: "reply";
|
|
15
|
+
readonly REPORT: "report";
|
|
16
|
+
};
|
|
17
|
+
export type MessageKind = typeof MESSAGE_KIND[keyof typeof MESSAGE_KIND];
|
|
18
|
+
export interface ParsedTopic {
|
|
19
|
+
ident: MachineIdent;
|
|
20
|
+
kind: MessageKind;
|
|
21
|
+
/** The alarm id on alarm topics; the report name on report topics. */
|
|
22
|
+
key?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function isTopicLevel(value: unknown): value is string;
|
|
25
|
+
export declare function assertTopicLevel(name: string, value: string): void;
|
|
26
|
+
export declare function machineTopic(root: string, ident: MachineIdent, channel: string): string;
|
|
27
|
+
export declare function alarmTopic(root: string, ident: MachineIdent, alarmId: string): string;
|
|
28
|
+
/** Every topic on one line, for a controller subscription. */
|
|
29
|
+
export declare function lineFilter(root: string, siteId: string, lineId: string): string;
|
|
30
|
+
export declare function toSubject(topic: string): string;
|
|
31
|
+
export declare function toTopic(subject: string): string;
|
|
32
|
+
/** Classify a topic under `root`; null when it is not a machine topic. */
|
|
33
|
+
export declare function parseTopic(root: string, topic: string): ParsedTopic | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Topic builders and the parser that classifies an inbound topic. NATS
|
|
4
|
+
* carries an MQTT topic on the subject with `/` replaced by `.`, so no topic
|
|
5
|
+
* level may contain `.`, `/`, `+`, `#` or whitespace.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MESSAGE_KIND = void 0;
|
|
9
|
+
exports.isTopicLevel = isTopicLevel;
|
|
10
|
+
exports.assertTopicLevel = assertTopicLevel;
|
|
11
|
+
exports.machineTopic = machineTopic;
|
|
12
|
+
exports.alarmTopic = alarmTopic;
|
|
13
|
+
exports.lineFilter = lineFilter;
|
|
14
|
+
exports.toSubject = toSubject;
|
|
15
|
+
exports.toTopic = toTopic;
|
|
16
|
+
exports.parseTopic = parseTopic;
|
|
17
|
+
const constants_1 = require("./constants");
|
|
18
|
+
const envelope_1 = require("./envelope");
|
|
19
|
+
exports.MESSAGE_KIND = {
|
|
20
|
+
PRESENCE: 'presence',
|
|
21
|
+
SNAPSHOT: 'snapshot',
|
|
22
|
+
JOB: 'job',
|
|
23
|
+
ALARM: 'alarm',
|
|
24
|
+
TELEMETRY: 'telemetry',
|
|
25
|
+
CMD: 'cmd',
|
|
26
|
+
REPLY: 'reply',
|
|
27
|
+
REPORT: 'report',
|
|
28
|
+
};
|
|
29
|
+
const RESERVED = /[/+#.\s]/;
|
|
30
|
+
function isTopicLevel(value) {
|
|
31
|
+
return typeof value === 'string' && !!value && !RESERVED.test(value);
|
|
32
|
+
}
|
|
33
|
+
function assertTopicLevel(name, value) {
|
|
34
|
+
if (!isTopicLevel(value)) {
|
|
35
|
+
throw new Error(`${name} "${value}" is not a valid topic level (no / + # . or whitespace)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function machineTopic(root, ident, channel) {
|
|
39
|
+
return `${root}/${ident.siteId}/${ident.lineId}/${(0, envelope_1.addressOf)(ident)}/${channel}`;
|
|
40
|
+
}
|
|
41
|
+
function alarmTopic(root, ident, alarmId) {
|
|
42
|
+
return machineTopic(root, ident, `${constants_1.CHANNEL.ALARM}/${alarmId}`);
|
|
43
|
+
}
|
|
44
|
+
/** Every topic on one line, for a controller subscription. */
|
|
45
|
+
function lineFilter(root, siteId, lineId) {
|
|
46
|
+
return `${root}/${siteId}/${lineId}/#`;
|
|
47
|
+
}
|
|
48
|
+
function toSubject(topic) {
|
|
49
|
+
return topic.split('/').join('.');
|
|
50
|
+
}
|
|
51
|
+
function toTopic(subject) {
|
|
52
|
+
return subject.split('.').join('/');
|
|
53
|
+
}
|
|
54
|
+
const CHANNEL_KIND = {
|
|
55
|
+
[constants_1.CHANNEL.PRESENCE]: exports.MESSAGE_KIND.PRESENCE,
|
|
56
|
+
[constants_1.CHANNEL.SNAPSHOT]: exports.MESSAGE_KIND.SNAPSHOT,
|
|
57
|
+
[constants_1.CHANNEL.JOB]: exports.MESSAGE_KIND.JOB,
|
|
58
|
+
[constants_1.CHANNEL.TELEMETRY]: exports.MESSAGE_KIND.TELEMETRY,
|
|
59
|
+
[constants_1.CHANNEL.CMD]: exports.MESSAGE_KIND.CMD,
|
|
60
|
+
[constants_1.CHANNEL.REPLY]: exports.MESSAGE_KIND.REPLY,
|
|
61
|
+
};
|
|
62
|
+
/** Classify a topic under `root`; null when it is not a machine topic. */
|
|
63
|
+
function parseTopic(root, topic) {
|
|
64
|
+
const prefix = `${root}/`;
|
|
65
|
+
if (!topic.startsWith(prefix))
|
|
66
|
+
return null;
|
|
67
|
+
const [siteId, lineId, machineId, ...rest] = topic.slice(prefix.length).split('/');
|
|
68
|
+
if (!siteId || !lineId || !machineId || rest.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const ident = { siteId, lineId, machineId };
|
|
71
|
+
const kind = CHANNEL_KIND[rest.join('/')];
|
|
72
|
+
if (kind)
|
|
73
|
+
return { ident, kind };
|
|
74
|
+
if (rest.length === 2 && rest[0] === constants_1.CHANNEL.ALARM)
|
|
75
|
+
return { ident, kind: exports.MESSAGE_KIND.ALARM, key: rest[1] };
|
|
76
|
+
if (rest.length >= 2 && rest[0] === constants_1.CHANNEL.REPORT)
|
|
77
|
+
return { ident, kind: exports.MESSAGE_KIND.REPORT, key: rest.slice(1).join('/') };
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line's edge account on Long Tail's NATS: the machine broker's bridge logs
|
|
3
|
+
* in by client certificate (the CN is its user), over MQTT only, and may
|
|
4
|
+
* publish under the line's topics and subscribe to nothing. Long Tail imports
|
|
5
|
+
* exactly those subjects.
|
|
6
|
+
*/
|
|
7
|
+
import type { EdgeAccount } from '../../messaging/nats/render-conf';
|
|
8
|
+
import type { LineDef } from '../line';
|
|
9
|
+
/** An account name for a line: letters, digits, `-` and `_`. */
|
|
10
|
+
export declare function edgeAccountName(line: LineDef): string;
|
|
11
|
+
export declare function edgeAccountFor(line: LineDef, root: string, bridgeUser?: string): EdgeAccount;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A line's edge account on Long Tail's NATS: the machine broker's bridge logs
|
|
4
|
+
* in by client certificate (the CN is its user), over MQTT only, and may
|
|
5
|
+
* publish under the line's topics and subscribe to nothing. Long Tail imports
|
|
6
|
+
* exactly those subjects.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.edgeAccountName = edgeAccountName;
|
|
10
|
+
exports.edgeAccountFor = edgeAccountFor;
|
|
11
|
+
const protocol_1 = require("../protocol");
|
|
12
|
+
const site_conf_1 = require("./site-conf");
|
|
13
|
+
/** An account name for a line: letters, digits, `-` and `_`. */
|
|
14
|
+
function edgeAccountName(line) {
|
|
15
|
+
return `EDGE_${line.siteId}_${line.lineId}`.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
16
|
+
}
|
|
17
|
+
function edgeAccountFor(line, root, bridgeUser = `CN=${(0, site_conf_1.bridgeId)(line)}`) {
|
|
18
|
+
const subjects = `${(0, protocol_1.toSubject)(`${root}/${line.siteId}/${line.lineId}`)}.>`;
|
|
19
|
+
return {
|
|
20
|
+
name: edgeAccountName(line),
|
|
21
|
+
subjects,
|
|
22
|
+
users: [{ user: bridgeUser, allowed_connection_types: ['MQTT'], permissions: { publish: [subjects], subscribe: { deny: ['>'] } } }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine broker (Mosquitto), rendered from the line. Machines speak
|
|
3
|
+
* MQTT 5 to it over TLS, each with its own client certificate (the CN is
|
|
4
|
+
* its username). The ACL lets a machine publish only its own channels and
|
|
5
|
+
* read only its own commands, and lets the line controller command every
|
|
6
|
+
* machine on the line. A bridge carries the line's topics out to the site
|
|
7
|
+
* NATS, where JetStream keeps them.
|
|
8
|
+
*/
|
|
9
|
+
import { type LineCodec } from '../protocol/codec';
|
|
10
|
+
import { type LineDef } from '../line';
|
|
11
|
+
export interface MosquittoConfOptions {
|
|
12
|
+
root: string;
|
|
13
|
+
/** Certificate directory as the broker sees it. */
|
|
14
|
+
certDir: string;
|
|
15
|
+
/** Where the site NATS MQTT listener is, host:port. */
|
|
16
|
+
siteNats: string;
|
|
17
|
+
aclFile: string;
|
|
18
|
+
maxPacketBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function renderMosquittoConf(line: LineDef, opts: MosquittoConfOptions): string;
|
|
21
|
+
/**
|
|
22
|
+
* The codec's channels decide the topics: a machine writes its events and telemetry and reads its commands.
|
|
23
|
+
* A station whose address the line learns has no user of its own: `pattern` rules give every certificate
|
|
24
|
+
* the channels under its own name (%u), so its certificate's common name must be its address. On a line
|
|
25
|
+
* that also declares addresses, a certificate named for a declared address is denied that station's channels.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderMosquittoAcl(line: LineDef, codec: Pick<LineCodec, 'root' | 'channels' | 'topicFor'>): string;
|