@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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long Tail's NATS server, rendered. Accounts keep traffic apart:
|
|
3
|
+
*
|
|
4
|
+
* - LT: Long Tail itself. It owns the durable streams and imports each edge's
|
|
5
|
+
* subjects, which its streams can then keep.
|
|
6
|
+
* - one edge account per line: the site broker's bridge logs in here (MQTT
|
|
7
|
+
* only) and may publish its line's subjects and nothing else. The account
|
|
8
|
+
* exports exactly those subjects, so nothing of Long Tail's is reachable.
|
|
9
|
+
* - SYS: the server's own monitoring.
|
|
10
|
+
*
|
|
11
|
+
* NATS configuration accepts JSON, so accounts are written as JSON.
|
|
12
|
+
*/
|
|
13
|
+
export interface NatsPermissions {
|
|
14
|
+
publish?: string[] | {
|
|
15
|
+
allow?: string[];
|
|
16
|
+
deny?: string[];
|
|
17
|
+
};
|
|
18
|
+
subscribe?: string[] | {
|
|
19
|
+
allow?: string[];
|
|
20
|
+
deny?: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface NatsUser {
|
|
24
|
+
user: string;
|
|
25
|
+
password?: string;
|
|
26
|
+
permissions?: NatsPermissions;
|
|
27
|
+
allowed_connection_types?: string[];
|
|
28
|
+
}
|
|
29
|
+
/** A line's edge: who logs in for it, and the subjects it may publish (e.g. `line.v1.site-1.line-1.>`). */
|
|
30
|
+
export interface EdgeAccount {
|
|
31
|
+
name: string;
|
|
32
|
+
subjects: string;
|
|
33
|
+
users: NatsUser[];
|
|
34
|
+
}
|
|
35
|
+
export interface NatsConfOptions {
|
|
36
|
+
serverName: string;
|
|
37
|
+
port?: number;
|
|
38
|
+
httpPort?: number;
|
|
39
|
+
jetstream?: {
|
|
40
|
+
storeDir: string;
|
|
41
|
+
};
|
|
42
|
+
/** WebSocket listener for browsers; `noAuthUser` maps a connection without credentials to that LT user. */
|
|
43
|
+
websocket?: {
|
|
44
|
+
port: number;
|
|
45
|
+
noTls?: boolean;
|
|
46
|
+
noAuthUser?: string;
|
|
47
|
+
};
|
|
48
|
+
/** MQTT listener for edge bridges; with `tls`, certificates map to users by their subject. */
|
|
49
|
+
mqtt?: {
|
|
50
|
+
port: number;
|
|
51
|
+
tls?: {
|
|
52
|
+
certFile: string;
|
|
53
|
+
keyFile: string;
|
|
54
|
+
caFile: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
lt: {
|
|
58
|
+
users: NatsUser[];
|
|
59
|
+
};
|
|
60
|
+
edges: EdgeAccount[];
|
|
61
|
+
system?: {
|
|
62
|
+
user: string;
|
|
63
|
+
password: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export declare const LT_ACCOUNT = "LT";
|
|
67
|
+
export declare const SYS_ACCOUNT = "SYS";
|
|
68
|
+
export declare function renderNatsConf(opts: NatsConfOptions): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Long Tail's NATS server, rendered. Accounts keep traffic apart:
|
|
4
|
+
*
|
|
5
|
+
* - LT: Long Tail itself. It owns the durable streams and imports each edge's
|
|
6
|
+
* subjects, which its streams can then keep.
|
|
7
|
+
* - one edge account per line: the site broker's bridge logs in here (MQTT
|
|
8
|
+
* only) and may publish its line's subjects and nothing else. The account
|
|
9
|
+
* exports exactly those subjects, so nothing of Long Tail's is reachable.
|
|
10
|
+
* - SYS: the server's own monitoring.
|
|
11
|
+
*
|
|
12
|
+
* NATS configuration accepts JSON, so accounts are written as JSON.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SYS_ACCOUNT = exports.LT_ACCOUNT = void 0;
|
|
16
|
+
exports.renderNatsConf = renderNatsConf;
|
|
17
|
+
exports.LT_ACCOUNT = 'LT';
|
|
18
|
+
exports.SYS_ACCOUNT = 'SYS';
|
|
19
|
+
function accounts(opts) {
|
|
20
|
+
const out = {
|
|
21
|
+
[exports.LT_ACCOUNT]: {
|
|
22
|
+
jetstream: opts.jetstream ? 'enabled' : 'disabled',
|
|
23
|
+
users: opts.lt.users,
|
|
24
|
+
imports: opts.edges.map((e) => ({ stream: { account: e.name, subject: e.subjects } })),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
for (const edge of opts.edges) {
|
|
28
|
+
// The edge account has JetStream because NATS keeps MQTT session state there.
|
|
29
|
+
out[edge.name] = { jetstream: opts.jetstream ? 'enabled' : 'disabled', users: edge.users, exports: [{ stream: edge.subjects }] };
|
|
30
|
+
}
|
|
31
|
+
if (opts.system)
|
|
32
|
+
out[exports.SYS_ACCOUNT] = { users: [opts.system] };
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function renderNatsConf(opts) {
|
|
36
|
+
const lines = [
|
|
37
|
+
'# Rendered by Long Tail (lib/messaging/nats/render-conf.ts). Do not edit by hand.',
|
|
38
|
+
`server_name: ${JSON.stringify(opts.serverName)}`,
|
|
39
|
+
`port: ${opts.port ?? 4222}`,
|
|
40
|
+
`http: ${opts.httpPort ?? 8222}`,
|
|
41
|
+
];
|
|
42
|
+
if (opts.jetstream)
|
|
43
|
+
lines.push(`jetstream { store_dir: ${JSON.stringify(opts.jetstream.storeDir)} }`);
|
|
44
|
+
if (opts.websocket) {
|
|
45
|
+
const ws = opts.websocket;
|
|
46
|
+
lines.push(`websocket { port: ${ws.port}${ws.noTls ? ', no_tls: true, same_origin: false' : ''}${ws.noAuthUser ? `, no_auth_user: ${JSON.stringify(ws.noAuthUser)}` : ''} }`);
|
|
47
|
+
}
|
|
48
|
+
if (opts.mqtt) {
|
|
49
|
+
const tls = opts.mqtt.tls;
|
|
50
|
+
lines.push(tls
|
|
51
|
+
? `mqtt { port: ${opts.mqtt.port}, tls { cert_file: ${JSON.stringify(tls.certFile)}, key_file: ${JSON.stringify(tls.keyFile)}, ca_file: ${JSON.stringify(tls.caFile)}, verify_and_map: true, timeout: 5 } }`
|
|
52
|
+
: `mqtt { port: ${opts.mqtt.port} }`);
|
|
53
|
+
}
|
|
54
|
+
lines.push(`accounts: ${JSON.stringify(accounts(opts), null, 2)}`);
|
|
55
|
+
if (opts.system)
|
|
56
|
+
lines.push(`system_account: ${exports.SYS_ACCOUNT}`);
|
|
57
|
+
return `${lines.join('\n')}\n`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable streams on JetStream. A spec becomes a file-backed stream or an
|
|
3
|
+
* explicit-ack durable consumer; ensuring either is safe on every boot, and an
|
|
4
|
+
* existing consumer keeps its position while its subjects follow the spec.
|
|
5
|
+
*/
|
|
6
|
+
import { type ConsumerConfig, type JetStreamClient, type JetStreamManager, type StreamConfig } from 'nats';
|
|
7
|
+
import type { ConsumerSpec, Delivery, LTConsumeOptions, LTStreamMessage, LTStreams, LTSubscription, StreamSpec, Subject } from '../../../types/messaging';
|
|
8
|
+
export declare function streamConfig(spec: StreamSpec): Partial<StreamConfig>;
|
|
9
|
+
export declare function consumerConfig(spec: ConsumerSpec): Partial<ConsumerConfig>;
|
|
10
|
+
/** Create the stream, or update an existing one to the spec. */
|
|
11
|
+
export declare function ensureStream(jsm: JetStreamManager, spec: StreamSpec): Promise<void>;
|
|
12
|
+
/** Create the consumer, or move an existing one to the spec's subjects without losing its place. */
|
|
13
|
+
export declare function ensureConsumer(jsm: JetStreamManager, stream: string, spec: ConsumerSpec): Promise<void>;
|
|
14
|
+
export declare function payloadOf(m: {
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare class NatsStreams implements LTStreams {
|
|
18
|
+
private readonly jsm;
|
|
19
|
+
private readonly js;
|
|
20
|
+
constructor(jsm: JetStreamManager, js: JetStreamClient);
|
|
21
|
+
ensureStream(spec: StreamSpec): Promise<void>;
|
|
22
|
+
ensureConsumer(stream: string, spec: ConsumerSpec): Promise<void>;
|
|
23
|
+
append(subject: Subject, payload: string): Promise<void>;
|
|
24
|
+
/** One message at a time: the next is not taken until the handler settles this one. */
|
|
25
|
+
consume(stream: string, consumer: string, handler: (m: LTStreamMessage) => Promise<Delivery>, opts?: LTConsumeOptions): Promise<LTSubscription>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable streams on JetStream. A spec becomes a file-backed stream or an
|
|
4
|
+
* explicit-ack durable consumer; ensuring either is safe on every boot, and an
|
|
5
|
+
* existing consumer keeps its position while its subjects follow the spec.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.NatsStreams = void 0;
|
|
9
|
+
exports.streamConfig = streamConfig;
|
|
10
|
+
exports.consumerConfig = consumerConfig;
|
|
11
|
+
exports.ensureStream = ensureStream;
|
|
12
|
+
exports.ensureConsumer = ensureConsumer;
|
|
13
|
+
exports.payloadOf = payloadOf;
|
|
14
|
+
const nats_1 = require("nats");
|
|
15
|
+
const constants_1 = require("../constants");
|
|
16
|
+
const DEFAULT_ACK_WAIT_MS = 30_000;
|
|
17
|
+
const DEFAULT_NAK_DELAY_MS = 1_000;
|
|
18
|
+
function streamConfig(spec) {
|
|
19
|
+
return { name: spec.name, subjects: spec.subjects, max_age: (0, nats_1.nanos)(spec.maxAgeMs), storage: nats_1.StorageType.File };
|
|
20
|
+
}
|
|
21
|
+
function consumerConfig(spec) {
|
|
22
|
+
return {
|
|
23
|
+
durable_name: spec.name,
|
|
24
|
+
ack_policy: nats_1.AckPolicy.Explicit,
|
|
25
|
+
deliver_policy: (spec.startAt ?? constants_1.START_AT.NEW) === constants_1.START_AT.ALL ? nats_1.DeliverPolicy.All : nats_1.DeliverPolicy.New,
|
|
26
|
+
ack_wait: (0, nats_1.nanos)(spec.ackWaitMs ?? DEFAULT_ACK_WAIT_MS),
|
|
27
|
+
filter_subjects: spec.filterSubjects,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Create the stream, or update an existing one to the spec. */
|
|
31
|
+
async function ensureStream(jsm, spec) {
|
|
32
|
+
const config = streamConfig(spec);
|
|
33
|
+
try {
|
|
34
|
+
await jsm.streams.info(spec.name);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (!/stream not found/i.test(err?.message ?? ''))
|
|
38
|
+
throw err;
|
|
39
|
+
await jsm.streams.add(config);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await jsm.streams.update(spec.name, config);
|
|
43
|
+
}
|
|
44
|
+
/** Create the consumer, or move an existing one to the spec's subjects without losing its place. */
|
|
45
|
+
async function ensureConsumer(jsm, stream, spec) {
|
|
46
|
+
let existing;
|
|
47
|
+
try {
|
|
48
|
+
existing = await jsm.consumers.info(stream, spec.name);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (!/consumer not found/i.test(err?.message ?? ''))
|
|
52
|
+
throw err;
|
|
53
|
+
await jsm.consumers.add(stream, consumerConfig(spec));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const want = [...spec.filterSubjects].sort();
|
|
57
|
+
const have = [...(existing.config.filter_subjects ?? [])].sort();
|
|
58
|
+
if (want.join('|') !== have.join('|'))
|
|
59
|
+
await jsm.consumers.update(stream, spec.name, { filter_subjects: spec.filterSubjects });
|
|
60
|
+
}
|
|
61
|
+
function payloadOf(m) {
|
|
62
|
+
return Buffer.from(m.data).toString('utf8');
|
|
63
|
+
}
|
|
64
|
+
class NatsStreams {
|
|
65
|
+
constructor(jsm, js) {
|
|
66
|
+
this.jsm = jsm;
|
|
67
|
+
this.js = js;
|
|
68
|
+
}
|
|
69
|
+
ensureStream(spec) {
|
|
70
|
+
return ensureStream(this.jsm, spec);
|
|
71
|
+
}
|
|
72
|
+
ensureConsumer(stream, spec) {
|
|
73
|
+
return ensureConsumer(this.jsm, stream, spec);
|
|
74
|
+
}
|
|
75
|
+
async append(subject, payload) {
|
|
76
|
+
await this.js.publish(subject, payload);
|
|
77
|
+
}
|
|
78
|
+
/** One message at a time: the next is not taken until the handler settles this one. */
|
|
79
|
+
async consume(stream, consumer, handler, opts = {}) {
|
|
80
|
+
const nakDelayMs = opts.nakDelayMs ?? DEFAULT_NAK_DELAY_MS;
|
|
81
|
+
const messages = await (await this.js.consumers.get(stream, consumer)).consume();
|
|
82
|
+
void (async () => {
|
|
83
|
+
for await (const m of messages) {
|
|
84
|
+
const delivery = await handler({ subject: m.subject, payload: payloadOf(m), deliveries: m.info.deliveryCount })
|
|
85
|
+
.catch(() => constants_1.DELIVERY.NAK);
|
|
86
|
+
if (delivery === constants_1.DELIVERY.ACK)
|
|
87
|
+
m.ack();
|
|
88
|
+
else if (delivery === constants_1.DELIVERY.NAK)
|
|
89
|
+
m.nak(nakDelayMs);
|
|
90
|
+
else
|
|
91
|
+
m.term();
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
return { close: async () => { await messages.close(); } };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.NatsStreams = NatsStreams;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subject filters. Subjects are dot-separated tokens; in a filter `*` matches
|
|
3
|
+
* exactly one token and `>` matches one or more trailing tokens.
|
|
4
|
+
*/
|
|
5
|
+
import type { Subject } from '../../types/messaging';
|
|
6
|
+
export declare function subjectMatches(filter: Subject, subject: Subject): boolean;
|
|
7
|
+
/** Whether `subject` is a valid concrete subject: nonempty tokens, no wildcards or whitespace. */
|
|
8
|
+
export declare function isSubject(subject: string): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Subject filters. Subjects are dot-separated tokens; in a filter `*` matches
|
|
4
|
+
* exactly one token and `>` matches one or more trailing tokens.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.subjectMatches = subjectMatches;
|
|
8
|
+
exports.isSubject = isSubject;
|
|
9
|
+
function subjectMatches(filter, subject) {
|
|
10
|
+
const f = filter.split('.');
|
|
11
|
+
const s = subject.split('.');
|
|
12
|
+
for (let i = 0; i < f.length; i++) {
|
|
13
|
+
if (f[i] === '>')
|
|
14
|
+
return i === f.length - 1 && s.length > i;
|
|
15
|
+
if (i >= s.length)
|
|
16
|
+
return false;
|
|
17
|
+
if (f[i] !== '*' && f[i] !== s[i])
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return f.length === s.length;
|
|
21
|
+
}
|
|
22
|
+
/** Whether `subject` is a valid concrete subject: nonempty tokens, no wildcards or whitespace. */
|
|
23
|
+
function isSubject(subject) {
|
|
24
|
+
return !!subject && subject.split('.').every((t) => !!t && !/[\s*>]/.test(t));
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make the example's first print: a small 3DBenchy (by Creative Tools), sliced
|
|
3
|
+
* for the printer with OrcaSlicer, written to examples/workflows/bambu-printer/files/boat.gcode.3mf.
|
|
4
|
+
* Run it on the host, where OrcaSlicer is installed:
|
|
5
|
+
*
|
|
6
|
+
* npm run bambu:boat
|
|
7
|
+
*
|
|
8
|
+
* 1. Download the model once into files/.
|
|
9
|
+
* 2. Scale it (BAMBU_BOAT_SCALE, 0.5 by default: about ten minutes on a P1S).
|
|
10
|
+
* 3. Slice it with the printer, process and filament presets. Each is an
|
|
11
|
+
* OrcaSlicer system preset name or a path to a preset JSON, which may
|
|
12
|
+
* inherit a system preset by name.
|
|
13
|
+
*
|
|
14
|
+
* Env: BAMBU_BOAT_PRINTER, BAMBU_BOAT_PROCESS, BAMBU_BOAT_FILAMENT, BAMBU_BOAT_SCALE,
|
|
15
|
+
* ORCA_SLICER (the OrcaSlicer executable) and ORCA_PROFILES (its BBL profiles directory).
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Make the example's first print: a small 3DBenchy (by Creative Tools), sliced
|
|
4
|
+
* for the printer with OrcaSlicer, written to examples/workflows/bambu-printer/files/boat.gcode.3mf.
|
|
5
|
+
* Run it on the host, where OrcaSlicer is installed:
|
|
6
|
+
*
|
|
7
|
+
* npm run bambu:boat
|
|
8
|
+
*
|
|
9
|
+
* 1. Download the model once into files/.
|
|
10
|
+
* 2. Scale it (BAMBU_BOAT_SCALE, 0.5 by default: about ten minutes on a P1S).
|
|
11
|
+
* 3. Slice it with the printer, process and filament presets. Each is an
|
|
12
|
+
* OrcaSlicer system preset name or a path to a preset JSON, which may
|
|
13
|
+
* inherit a system preset by name.
|
|
14
|
+
*
|
|
15
|
+
* Env: BAMBU_BOAT_PRINTER, BAMBU_BOAT_PROCESS, BAMBU_BOAT_FILAMENT, BAMBU_BOAT_SCALE,
|
|
16
|
+
* ORCA_SLICER (the OrcaSlicer executable) and ORCA_PROFILES (its BBL profiles directory).
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
try {
|
|
20
|
+
require('dotenv/config');
|
|
21
|
+
}
|
|
22
|
+
catch { /* dotenv optional */ }
|
|
23
|
+
const child_process_1 = require("child_process");
|
|
24
|
+
const fs_1 = require("fs");
|
|
25
|
+
const os_1 = require("os");
|
|
26
|
+
const path_1 = require("path");
|
|
27
|
+
const env = process.env;
|
|
28
|
+
const FILES = (0, path_1.resolve)('examples/workflows/bambu-printer/files');
|
|
29
|
+
const MODEL_URL = 'https://raw.githubusercontent.com/CreativeTools/3DBenchy/master/Single-part/3DBenchy.stl';
|
|
30
|
+
const ORCA = env.ORCA_SLICER || '/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer';
|
|
31
|
+
const PROFILES = env.ORCA_PROFILES || '/Applications/OrcaSlicer.app/Contents/Resources/profiles/BBL';
|
|
32
|
+
const PRESET = {
|
|
33
|
+
PRINTER: env.BAMBU_BOAT_PRINTER || 'Bambu Lab P1S 0.4 nozzle',
|
|
34
|
+
PROCESS: env.BAMBU_BOAT_PROCESS || '0.20mm Standard @BBL X1C',
|
|
35
|
+
FILAMENT: env.BAMBU_BOAT_FILAMENT || 'Bambu PLA Basic @BBL X1C',
|
|
36
|
+
};
|
|
37
|
+
const PRESET_DIR = { PRINTER: 'machine', PROCESS: 'process', FILAMENT: 'filament' };
|
|
38
|
+
const STL_HEADER = 84;
|
|
39
|
+
const STL_TRIANGLE = 50;
|
|
40
|
+
const say = (msg) => console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`);
|
|
41
|
+
async function fetchModel() {
|
|
42
|
+
const path = (0, path_1.join)(FILES, '3DBenchy.stl');
|
|
43
|
+
if ((0, fs_1.existsSync)(path))
|
|
44
|
+
return path;
|
|
45
|
+
const res = await fetch(MODEL_URL);
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
throw new Error(`GET ${MODEL_URL} -> ${res.status}`);
|
|
48
|
+
(0, fs_1.writeFileSync)(path, Buffer.from(await res.arrayBuffer()));
|
|
49
|
+
return path;
|
|
50
|
+
}
|
|
51
|
+
/** A binary STL with every vertex scaled about the origin; the slicer places it on the bed. */
|
|
52
|
+
function scaleStl(stl, factor) {
|
|
53
|
+
const count = stl.readUInt32LE(STL_HEADER - 4);
|
|
54
|
+
if (stl.length !== STL_HEADER + count * STL_TRIANGLE)
|
|
55
|
+
throw new Error('the model is not a binary STL');
|
|
56
|
+
const out = Buffer.from(stl);
|
|
57
|
+
for (let t = 0; t < count; t++) {
|
|
58
|
+
const at = STL_HEADER + t * STL_TRIANGLE;
|
|
59
|
+
for (let v = 3; v < 12; v++)
|
|
60
|
+
out.writeFloatLE(stl.readFloatLE(at + v * 4) * factor, at + v * 4);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** The system presets of one kind in ORCA_PROFILES, by name. */
|
|
65
|
+
function systemPresets(kind) {
|
|
66
|
+
const dir = (0, path_1.join)(PROFILES, PRESET_DIR[kind]);
|
|
67
|
+
const byName = new Map();
|
|
68
|
+
for (const file of (0, fs_1.readdirSync)(dir).filter((f) => f.endsWith('.json'))) {
|
|
69
|
+
const preset = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, file), 'utf8'));
|
|
70
|
+
if (preset.name)
|
|
71
|
+
byName.set(preset.name, preset);
|
|
72
|
+
}
|
|
73
|
+
return byName;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A preset by path or system preset name, with every setting it inherits
|
|
77
|
+
* written out: OrcaSlicer's command line reads only the keys in the file, so
|
|
78
|
+
* an inherited start, layer-change or end G-code would otherwise be lost.
|
|
79
|
+
*/
|
|
80
|
+
function fullPreset(kind, value) {
|
|
81
|
+
const system = systemPresets(kind);
|
|
82
|
+
const own = (0, fs_1.existsSync)(value) ? JSON.parse((0, fs_1.readFileSync)(value, 'utf8')) : system.get(value);
|
|
83
|
+
if (!own)
|
|
84
|
+
throw new Error(`no ${kind.toLowerCase()} preset ${value} (a path, or a name in ${PROFILES}/${PRESET_DIR[kind]})`);
|
|
85
|
+
const chain = [own];
|
|
86
|
+
for (let p = own; p.inherits;) {
|
|
87
|
+
p = system.get(p.inherits);
|
|
88
|
+
if (!p)
|
|
89
|
+
throw new Error(`${kind.toLowerCase()} preset ${value} inherits ${chain[chain.length - 1].inherits}, which is not in ${PROFILES}`);
|
|
90
|
+
chain.unshift(p);
|
|
91
|
+
}
|
|
92
|
+
return Object.assign({}, ...chain, { inherits: '' });
|
|
93
|
+
}
|
|
94
|
+
/** Write a full preset for the slicer; a process or filament is marked compatible with the printer, as OrcaSlicer requires. */
|
|
95
|
+
function writePreset(dir, kind, preset, printerName) {
|
|
96
|
+
const path = (0, path_1.join)(dir, `${PRESET_DIR[kind]}.json`);
|
|
97
|
+
const compatibility = kind === 'PRINTER' ? {} : { from: 'User', compatible_printers: [printerName], compatible_printers_condition: '' };
|
|
98
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify({ ...preset, ...compatibility }));
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
async function main() {
|
|
102
|
+
if (!(0, fs_1.existsSync)(ORCA))
|
|
103
|
+
throw new Error(`OrcaSlicer is not at ${ORCA}; install it or set ORCA_SLICER`);
|
|
104
|
+
(0, fs_1.mkdirSync)(FILES, { recursive: true });
|
|
105
|
+
const work = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'bambu-boat-'));
|
|
106
|
+
// 1. The model
|
|
107
|
+
const model = await fetchModel();
|
|
108
|
+
// 2. Scaled
|
|
109
|
+
const scale = Number(env.BAMBU_BOAT_SCALE || 0.5);
|
|
110
|
+
if (!(scale > 0))
|
|
111
|
+
throw new Error(`BAMBU_BOAT_SCALE="${env.BAMBU_BOAT_SCALE}" must be a number above 0`);
|
|
112
|
+
const scaled = (0, path_1.join)(work, 'boat.stl');
|
|
113
|
+
(0, fs_1.writeFileSync)(scaled, scaleStl((0, fs_1.readFileSync)(model), scale));
|
|
114
|
+
// 3. Sliced
|
|
115
|
+
const printerPreset = fullPreset('PRINTER', PRESET.PRINTER);
|
|
116
|
+
const printerName = printerPreset.name;
|
|
117
|
+
const printer = writePreset(work, 'PRINTER', printerPreset, printerName);
|
|
118
|
+
const process_ = writePreset(work, 'PROCESS', fullPreset('PROCESS', PRESET.PROCESS), printerName);
|
|
119
|
+
const filament = writePreset(work, 'FILAMENT', fullPreset('FILAMENT', PRESET.FILAMENT), printerName);
|
|
120
|
+
say(`slicing at ${scale}x for ${printerName}`);
|
|
121
|
+
(0, child_process_1.execFileSync)(ORCA, [
|
|
122
|
+
'--load-settings', `${printer};${process_}`, '--load-filaments', filament,
|
|
123
|
+
'--arrange', '1', '--slice', '0', '--outputdir', work, '--export-3mf', 'boat.gcode.3mf', scaled,
|
|
124
|
+
], { stdio: 'inherit' });
|
|
125
|
+
const target = (0, path_1.join)(FILES, 'boat.gcode.3mf');
|
|
126
|
+
(0, fs_1.renameSync)((0, path_1.join)(work, 'boat.gcode.3mf'), target);
|
|
127
|
+
const gcode = (0, fs_1.readdirSync)(work).find((f) => f.endsWith('.gcode'));
|
|
128
|
+
const time = gcode ? (0, fs_1.readFileSync)((0, path_1.join)(work, gcode), 'utf8').slice(0, 2000).match(/total estimated time: ([^\n]+)/)?.[1] : undefined;
|
|
129
|
+
say(`${target}${time ? ` (about ${time})` : ''}`);
|
|
130
|
+
}
|
|
131
|
+
main().catch((err) => {
|
|
132
|
+
console.error(`failed: ${err.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print one file on a Bambu printer through Long Tail, and follow its rows.
|
|
3
|
+
* Run it where the printer is reachable, e.g. in the Long Tail container:
|
|
4
|
+
*
|
|
5
|
+
* docker compose exec long-tail npm run bambu:print -- [examples/workflows/bambu-printer/files/part.gcode.3mf] [station]
|
|
6
|
+
*
|
|
7
|
+
* Without a file it prints the example's boat, a half-size 3DBenchy for a P1S with a 0.8 nozzle and TPU;
|
|
8
|
+
* `npm run bambu:boat` slices one for another printer or filament.
|
|
9
|
+
*
|
|
10
|
+
* 1. Put the file on the printer (it prints only files on its own storage) and hash it: the unit's artifact.
|
|
11
|
+
* 2. Start the unit route: one print step at the station, then a person unloads.
|
|
12
|
+
* 3. Print the route's rows and their lifecycle items as they change, until the
|
|
13
|
+
* route ends. The unload row is resolved by a person on the dashboard.
|
|
14
|
+
*
|
|
15
|
+
* Env: LT_USERNAME and LT_PASSWORD (a Long Tail login that may invoke the route), BASE_URL
|
|
16
|
+
* (http://localhost:3000), BAMBU_SITE_DIR (deploy/bambu-site), and the printers in .env.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Print one file on a Bambu printer through Long Tail, and follow its rows.
|
|
4
|
+
* Run it where the printer is reachable, e.g. in the Long Tail container:
|
|
5
|
+
*
|
|
6
|
+
* docker compose exec long-tail npm run bambu:print -- [examples/workflows/bambu-printer/files/part.gcode.3mf] [station]
|
|
7
|
+
*
|
|
8
|
+
* Without a file it prints the example's boat, a half-size 3DBenchy for a P1S with a 0.8 nozzle and TPU;
|
|
9
|
+
* `npm run bambu:boat` slices one for another printer or filament.
|
|
10
|
+
*
|
|
11
|
+
* 1. Put the file on the printer (it prints only files on its own storage) and hash it: the unit's artifact.
|
|
12
|
+
* 2. Start the unit route: one print step at the station, then a person unloads.
|
|
13
|
+
* 3. Print the route's rows and their lifecycle items as they change, until the
|
|
14
|
+
* route ends. The unload row is resolved by a person on the dashboard.
|
|
15
|
+
*
|
|
16
|
+
* Env: LT_USERNAME and LT_PASSWORD (a Long Tail login that may invoke the route), BASE_URL
|
|
17
|
+
* (http://localhost:3000), BAMBU_SITE_DIR (deploy/bambu-site), and the printers in .env.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
try {
|
|
21
|
+
require('dotenv/config');
|
|
22
|
+
}
|
|
23
|
+
catch { /* dotenv optional */ }
|
|
24
|
+
const crypto_1 = require("crypto");
|
|
25
|
+
const fs_1 = require("fs");
|
|
26
|
+
const path_1 = require("path");
|
|
27
|
+
const env_1 = require("../examples/workflows/bambu-printer/env");
|
|
28
|
+
const files_1 = require("../examples/workflows/bambu-printer/files");
|
|
29
|
+
const route_1 = require("../examples/workflows/machine-line/workflows/route");
|
|
30
|
+
const env = process.env;
|
|
31
|
+
const BASE_URL = env.BASE_URL || 'http://localhost:3000';
|
|
32
|
+
const SITE_DIR = env.BAMBU_SITE_DIR || 'deploy/bambu-site';
|
|
33
|
+
const WATCH_MS = 3_000;
|
|
34
|
+
const BOAT = 'examples/workflows/bambu-printer/boat/p1s-0.8-tpu.gcode.3mf';
|
|
35
|
+
const ts = () => new Date().toISOString().slice(11, 19);
|
|
36
|
+
const say = (msg) => console.log(`[${ts()}] ${msg}`);
|
|
37
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
38
|
+
let token = '';
|
|
39
|
+
async function api(method, path, body) {
|
|
40
|
+
const res = await fetch(`${BASE_URL}/api${path}`, {
|
|
41
|
+
method,
|
|
42
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
43
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
const data = await res.json().catch(() => ({}));
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(data)}`);
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
/** The route's rows, and the items each holds, printed as they change; the route's result once it ends. */
|
|
51
|
+
async function follow(orderRef, workflowId) {
|
|
52
|
+
const shown = new Map();
|
|
53
|
+
for (;;) {
|
|
54
|
+
try {
|
|
55
|
+
const result = await look(orderRef, workflowId, shown);
|
|
56
|
+
if (result)
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// A restart or a database blip is not the route ending: keep following.
|
|
61
|
+
say(`(could not read the route: ${err.message}; trying again)`);
|
|
62
|
+
}
|
|
63
|
+
await sleep(WATCH_MS);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function look(orderRef, workflowId, shown) {
|
|
67
|
+
const facets = encodeURIComponent(JSON.stringify({ orderRef }));
|
|
68
|
+
const { escalations } = await api('GET', `/escalations?facets=${facets}&limit=20&sort_by=created_at&order=asc`);
|
|
69
|
+
for (const row of escalations) {
|
|
70
|
+
const { items = [] } = await api('GET', `/escalations/${row.id}/items`);
|
|
71
|
+
const view = `${row.type}/${row.role} ${row.status}${row.assigned_to ? ' claimed' : ''} [${items.map((i) => i.itemKey).join(', ')}]`;
|
|
72
|
+
if (shown.get(row.id) !== view)
|
|
73
|
+
say(`${row.id.slice(0, 8)} ${view}`);
|
|
74
|
+
shown.set(row.id, view);
|
|
75
|
+
}
|
|
76
|
+
const resp = await api('GET', `/workflows/${workflowId}/result`).catch(() => null);
|
|
77
|
+
return resp?.result?.data?.status ? resp.result.data : null;
|
|
78
|
+
}
|
|
79
|
+
async function main() {
|
|
80
|
+
const [file = BOAT, stationArg] = process.argv.slice(2);
|
|
81
|
+
if (!(0, fs_1.existsSync)(file))
|
|
82
|
+
throw new Error(`${file} is missing; usage: npm run bambu:print -- [path/to/part.gcode.3mf] [station]`);
|
|
83
|
+
const line = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(SITE_DIR, 'line.json'), 'utf8'));
|
|
84
|
+
const station = stationArg || line.machines[0].machineId;
|
|
85
|
+
const printer = (0, env_1.printersFromEnv)().find((p) => p.station === station);
|
|
86
|
+
if (!printer || !line.machines.some((m) => m.machineId === station))
|
|
87
|
+
throw new Error(`${station} is not an enabled printer on the line in ${SITE_DIR}/line.json`);
|
|
88
|
+
// 1. The artifact, on the printer
|
|
89
|
+
const routeKey = `P${Date.now().toString(36)}`;
|
|
90
|
+
const sha256 = (0, crypto_1.createHash)('sha256').update((0, fs_1.readFileSync)(file)).digest('hex');
|
|
91
|
+
say(`copying ${(0, path_1.basename)(file)} to ${station} (a minute or two for a few MB)`);
|
|
92
|
+
const url = await (0, files_1.stageFile)(printer, file, `${routeKey}.gcode.3mf`);
|
|
93
|
+
say(`${url} (sha256 ${sha256.slice(0, 12)}…)`);
|
|
94
|
+
// 2. The route
|
|
95
|
+
const { LT_USERNAME: username, LT_PASSWORD: password } = env;
|
|
96
|
+
if (!username || !password)
|
|
97
|
+
throw new Error('set LT_USERNAME and LT_PASSWORD in .env');
|
|
98
|
+
token = (await api('POST', '/auth/login', { username, password })).token;
|
|
99
|
+
const input = {
|
|
100
|
+
siteId: line.siteId, lineId: line.lineId, routeKey, orderRef: `ORD-${routeKey}`,
|
|
101
|
+
units: [{ unitId: `U-${routeKey}`, carrierId: `PLATE-${routeKey}` }],
|
|
102
|
+
steps: [{ machineId: station, process: env_1.PRINT_PROCESS, recipe: { ...env_1.PRINT_RECIPE } }],
|
|
103
|
+
artifact: { url, sha256 }, exitRole: env_1.BAMBU_ROLES.UNLOAD, handoffTimeout: env.BAMBU_HANDOFF_TIMEOUT || '6h',
|
|
104
|
+
};
|
|
105
|
+
const workflowId = `bambu-${routeKey}`;
|
|
106
|
+
await api('POST', `/workflows/${route_1.UNIT_ROUTE_WORKFLOW}/invoke`, { data: input, metadata: { source: 'bambu-print' }, workflowId });
|
|
107
|
+
say(`started ${workflowId} on ${station}; unload the part on the dashboard (${env_1.BAMBU_ROLES.UNLOAD}) when it finishes`);
|
|
108
|
+
// 3. Follow it
|
|
109
|
+
say(`route ended: ${JSON.stringify(await follow(input.orderRef, workflowId))}`);
|
|
110
|
+
}
|
|
111
|
+
main().catch((err) => {
|
|
112
|
+
console.error(`[${ts()}] failed: ${err.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* machine-line: drive the machine line example end to end.
|
|
3
|
+
*
|
|
4
|
+
* Starts unit routes on the factory line and plays the people: at every
|
|
5
|
+
* station people work (and at the inspector, where every route ends) a
|
|
6
|
+
* person takes the units off the station before and receives them; a
|
|
7
|
+
* technician clears alarms; an operator decides each failed handoff. The
|
|
8
|
+
* machines and the gateway do the rest over MQTT. MIX sets the share of
|
|
9
|
+
* routes minted from the automated manifest; the rest run the manual one,
|
|
10
|
+
* through the same workflow. Rows are watched on Long Tail's NATS.
|
|
11
|
+
*
|
|
12
|
+
* Usage (after `docker compose -f docker-compose.yml -f docker-compose.nats.yml -f docker-compose.machines.yml up -d --build`):
|
|
13
|
+
* npm run machine:line
|
|
14
|
+
* MIX=50 UNITS=8 npm run machine:line # half automated, half manual, same factory
|
|
15
|
+
* UNITS=8 BATCH=2 npm run machine:line
|
|
16
|
+
* FAULT=jam npm run machine:line # first route jams leaving FAULT_AT; operator retries
|
|
17
|
+
* FAULT=reject DECIDE=scrap npm run machine:line
|
|
18
|
+
*
|
|
19
|
+
* Env vars:
|
|
20
|
+
* BASE_URL Long Tail API (default http://localhost:3000)
|
|
21
|
+
* SIM_URL mock line drill server (default http://localhost:8090)
|
|
22
|
+
* NATS_URL Long Tail NATS (default nats://localhost:4222), NATS_TOKEN (default dev_api_secret)
|
|
23
|
+
* UNITS units to run (default 6); BATCH units per route (default 2)
|
|
24
|
+
* MIX percent of routes on the automated manifest (default 100); the rest run manual
|
|
25
|
+
* MACHINE_LINE_CONFIG, MACHINE_MANIFESTS line and manifests (default the factory line and route-manifests.json)
|
|
26
|
+
* FAULT jam | incomplete | carrier_mismatch | unreadable | stall | reject | hold | estop | offline
|
|
27
|
+
* FAULT_AT machine the fault happens at (default the first machine)
|
|
28
|
+
* DECIDE operator decision on a failed handoff: retry | manual | scrap (default retry;
|
|
29
|
+
* manual where the row does not offer retry)
|
|
30
|
+
* OFFLINE_S how long FAULT=offline keeps the machine away (default 25)
|
|
31
|
+
* HANDOFF_TIMEOUT per-handoff timeout (default 5m); TIMEOUT_S whole run (default 300)
|
|
32
|
+
*/
|
|
33
|
+
export {};
|