miravo 0.4.0 → 0.6.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 -1
- package/console-dist/assets/activity-DsWeRq4h.js +1 -0
- package/console-dist/assets/api-4AqKvsm9.js +1 -0
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
- package/console-dist/assets/box-C1WaoOMF.js +1 -0
- package/console-dist/assets/card-BjeP32PN.js +1 -0
- package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
- package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
- package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
- package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
- package/console-dist/assets/control-CPhvwx9o.js +1 -0
- package/console-dist/assets/control-Cg2up9NA.js +1 -0
- package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
- package/console-dist/assets/dist-BhbDML0a.js +1 -0
- package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
- package/console-dist/assets/download-DQ6NH16U.js +1 -0
- package/console-dist/assets/engine-store-CrLnz692.js +1 -0
- package/console-dist/assets/events-CXm29Ok4.js +1 -0
- package/console-dist/assets/events-D01IxhnP.js +1 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
- package/console-dist/assets/fleet-lotMGVqe.js +1 -0
- package/console-dist/assets/format-kXTonpDJ.js +1 -0
- package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
- package/console-dist/assets/index-CVPKSDvJ.js +12 -0
- package/console-dist/assets/index-DJkgDeun.css +2 -0
- package/console-dist/assets/input-BWEQFptx.js +1 -0
- package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
- package/console-dist/assets/label-uephX5ZG.js +1 -0
- package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
- package/console-dist/assets/login-C3RJli0m.js +126 -0
- package/console-dist/assets/models-B_UioWQ5.js +1 -0
- package/console-dist/assets/models-_IPlPiZ7.js +1 -0
- package/console-dist/assets/not-found-B8gAm08S.js +1 -0
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
- package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
- package/console-dist/assets/platforms-DStNbtVc.js +1 -0
- package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
- package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
- package/console-dist/assets/radio-CoHegOv5.js +1 -0
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
- package/console-dist/assets/select-CeO4vP0G.js +1 -0
- package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
- package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
- package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
- package/console-dist/assets/tags-CiXOIHtY.js +4 -0
- package/console-dist/assets/tags-FQCXIxrP.js +1 -0
- package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
- package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
- package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
- package/console-dist/assets/validation-Blvud9_O.js +1 -0
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
- package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
- package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
- package/console-dist/index.html +70 -2
- package/package.json +9 -8
- package/src/__tests__/integration/executable-methods-runner.ts +7 -6
- package/src/__tests__/integration/executable-methods.integration.ts +214 -122
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
- package/src/commands/adapter.ts +180 -7
- package/src/commands/apply.test.ts +62 -0
- package/src/commands/apply.ts +216 -0
- package/src/commands/cleanup.test.ts +34 -0
- package/src/commands/cleanup.ts +201 -0
- package/src/commands/export.test.ts +51 -0
- package/src/commands/export.ts +404 -0
- package/src/commands/fork.ts +2 -2
- package/src/commands/inject.ts +2 -2
- package/src/commands/load.ts +1 -1
- package/src/commands/logs.ts +13 -15
- package/src/commands/models.test.ts +1 -1
- package/src/commands/models.ts +1 -1
- package/src/commands/pause.ts +3 -3
- package/src/commands/platforms.test.ts +186 -0
- package/src/commands/platforms.ts +371 -0
- package/src/commands/remove.ts +1 -1
- package/src/commands/reset.ts +2 -2
- package/src/commands/spawn.ts +2 -2
- package/src/commands/speed.ts +2 -2
- package/src/commands/start/adapter-config.ts +157 -0
- package/src/commands/start/catalog-le-dt.test.ts +50 -0
- package/src/commands/start/catalog-le-dt.ts +187 -0
- package/src/commands/start/catalog-model-schema.ts +89 -0
- package/src/commands/start/catalog-template-detail.ts +54 -0
- package/src/commands/start/daemon.ts +255 -0
- package/src/commands/start/display.ts +150 -0
- package/src/commands/start/engine-setup.ts +247 -0
- package/src/commands/start/events.ts +126 -0
- package/src/commands/start/index.ts +897 -0
- package/src/commands/start/protocol-plans.ts +239 -0
- package/src/commands/start/template-loader.ts +197 -0
- package/src/commands/start/template-resolver.ts +185 -0
- package/src/commands/start/types.ts +73 -0
- package/src/commands/start.test.ts +137 -16
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +19 -8
- package/src/commands/templates.test.ts +1 -1
- package/src/commands/templates.ts +1 -3
- package/src/index.ts +11 -3
- package/src/lib/admin-auth.ts +157 -0
- package/src/lib/admin-client.test.ts +31 -15
- package/src/lib/admin-client.ts +106 -6
- package/src/lib/admin-platform-routes.ts +1017 -0
- package/src/lib/admin-platform-stream.test.ts +434 -0
- package/src/lib/admin-platform-stream.ts +788 -0
- package/src/lib/admin-responses.ts +211 -0
- package/src/lib/admin-server.test.ts +274 -55
- package/src/lib/admin-server.ts +387 -248
- package/src/lib/auth-store.ts +87 -0
- package/src/lib/daemon-registry.ts +12 -14
- package/src/lib/le/le-apply.test.ts +591 -0
- package/src/lib/le/le-apply.ts +832 -0
- package/src/lib/le/le-auth.ts +89 -0
- package/src/lib/le/le-cleanup.test.ts +168 -0
- package/src/lib/le/le-cleanup.ts +399 -0
- package/src/lib/le/le-cli-helpers.ts +77 -0
- package/src/lib/le/le-client.ts +184 -0
- package/src/lib/le/le-concurrency.test.ts +157 -0
- package/src/lib/le/le-concurrency.ts +78 -0
- package/src/lib/le/le-connection-test.ts +42 -0
- package/src/lib/le/le-devicehub-properties.test.ts +53 -0
- package/src/lib/le/le-devicehub-properties.ts +33 -0
- package/src/lib/le/le-devicehub.ts +213 -0
- package/src/lib/le/le-digital-twins.ts +285 -0
- package/src/lib/le/le-drivers.ts +23 -0
- package/src/lib/le/le-endpoint.ts +40 -0
- package/src/lib/le/le-environments.test.ts +73 -0
- package/src/lib/le/le-environments.ts +156 -0
- package/src/lib/le/le-markers.ts +87 -0
- package/src/lib/le/le-plan.test.ts +26 -0
- package/src/lib/le/le-plan.ts +109 -0
- package/src/lib/le/le-platform-operations.test.ts +198 -0
- package/src/lib/le/le-platform-operations.ts +340 -0
- package/src/lib/le/le-static-model-schema.ts +78 -0
- package/src/lib/le/le-static-planner.test.ts +255 -0
- package/src/lib/le/le-static-planner.ts +442 -0
- package/src/lib/le/le-template-exporter.test.ts +527 -0
- package/src/lib/le/le-template-exporter.ts +459 -0
- package/src/lib/le/le-value-types.ts +23 -0
- package/src/lib/mqtt-client.ts +1 -2
- package/src/lib/resolve-simulation.ts +1 -6
- package/src/lib/session-store.ts +110 -0
- package/src/lib/shutdown.test.ts +184 -2
- package/src/lib/shutdown.ts +104 -34
- package/src/lib/template-category.ts +3 -3
- package/src/lib/template-locator.ts +8 -2
- package/src/lib/ws-hub.ts +72 -46
- package/src/logger.ts +39 -14
- package/src/output.ts +3 -0
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- package/console-dist/assets/index-hUJSpKtB.js +0 -119
- package/src/commands/start.ts +0 -2023
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { ProtocolName } from "@miravo/shared";
|
|
2
|
+
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { getLeDtJson, getManifest, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
+
import { LE_DRIVER_NAMES } from "../lib/le/le-drivers.ts";
|
|
6
|
+
import { buildDesiredStateFromTemplate } from "../lib/le/le-static-planner.ts";
|
|
7
|
+
import { buildLeExport } from "../lib/le/le-template-exporter.ts";
|
|
8
|
+
import { MODBUS_VALUE_TYPE, OPCUA_VALUE_TYPE } from "../lib/le/le-value-types.ts";
|
|
9
|
+
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
10
|
+
import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
|
|
11
|
+
import { handleCommandError, writeOutput } from "../logger.ts";
|
|
12
|
+
|
|
13
|
+
const VALID_PROTOCOLS = ["opcua", "modbus", "mqtt"] as const;
|
|
14
|
+
|
|
15
|
+
/** LE DeviceHub CSV header — matches real LE export format. */
|
|
16
|
+
const LE_CSV_HEADER =
|
|
17
|
+
"deviceId,deviceName,driverName,name,tagName,description,valueType,registerId,publishCoV,tagFormula,metaData,Retain,Qos,address,count,pollingInterval,endianness";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Command
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface ExportLeTagsOptions {
|
|
24
|
+
readonly device: string;
|
|
25
|
+
readonly protocol: string;
|
|
26
|
+
readonly instance?: string;
|
|
27
|
+
readonly output?: string;
|
|
28
|
+
readonly adminUrl?: string;
|
|
29
|
+
readonly timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerExportCommand(program: Command): void {
|
|
33
|
+
const exportCmd = program
|
|
34
|
+
.command("export")
|
|
35
|
+
.description("Export simulation data in platform-specific formats");
|
|
36
|
+
|
|
37
|
+
// -------------------------------------------------------------------------
|
|
38
|
+
// miravo export le-template — offline full LE template (JSON + tags CSV)
|
|
39
|
+
// -------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
interface ExportLeOptions {
|
|
42
|
+
readonly template: string;
|
|
43
|
+
readonly host: string;
|
|
44
|
+
readonly protocol?: string;
|
|
45
|
+
readonly output?: string;
|
|
46
|
+
readonly modelsDir?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exportCmd
|
|
50
|
+
.command("le-template")
|
|
51
|
+
.description(
|
|
52
|
+
"Export a full Litmus Edge template (devices + DT) and tags CSV — no running instance needed",
|
|
53
|
+
)
|
|
54
|
+
.requiredOption("-t, --template <id>", "Template ID, name, or path (required)")
|
|
55
|
+
.option("--host <hostname>", "Source hostname embedded in device endpoints", "localhost")
|
|
56
|
+
.option("--protocol <name>", "Protocol override: opcua, modbus, or mqtt")
|
|
57
|
+
.option("-o, --output <prefix>", "Output file prefix (default: <template-id>)")
|
|
58
|
+
.option("--models-dir <path>", "Extra directory to search for .twin.yaml model files")
|
|
59
|
+
.addHelpText(
|
|
60
|
+
"after",
|
|
61
|
+
`
|
|
62
|
+
Examples:
|
|
63
|
+
$ miravo export le-template -t smart-factory
|
|
64
|
+
$ miravo export le-template -t smart-factory --host 192.168.1.100 --protocol opcua
|
|
65
|
+
$ miravo export le-template -t ./my.miravo.yaml --models-dir ./models
|
|
66
|
+
$ miravo export le-template -t smart-factory -o ./out/sf`,
|
|
67
|
+
)
|
|
68
|
+
.action(async (options: ExportLeOptions) => {
|
|
69
|
+
try {
|
|
70
|
+
if (
|
|
71
|
+
options.protocol &&
|
|
72
|
+
!VALID_PROTOCOLS.includes(options.protocol as (typeof VALID_PROTOCOLS)[number])
|
|
73
|
+
) {
|
|
74
|
+
writeOutput(`Unknown protocol "${options.protocol}". Valid options: opcua, modbus, mqtt`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const catalog = options.modelsDir
|
|
80
|
+
? createCatalog({ extraModelsDir: options.modelsDir })
|
|
81
|
+
: createCatalog();
|
|
82
|
+
const templatePath = await resolveTemplateIdentifier(options.template, catalog);
|
|
83
|
+
const { templateId, desired } = await buildDesiredStateFromTemplate({
|
|
84
|
+
templatePath,
|
|
85
|
+
catalog,
|
|
86
|
+
sourceHostname: options.host,
|
|
87
|
+
protocol: options.protocol as ProtocolName | undefined,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { templateJson, tagsCSV } = buildLeExport(desired, templateId);
|
|
91
|
+
|
|
92
|
+
const prefix = options.output ?? templateId;
|
|
93
|
+
const jsonPath = `${prefix}.le-template.json`;
|
|
94
|
+
const csvPath = `${prefix}-tags.csv`;
|
|
95
|
+
|
|
96
|
+
await Bun.write(jsonPath, templateJson);
|
|
97
|
+
await Bun.write(csvPath, tagsCSV);
|
|
98
|
+
|
|
99
|
+
writeOutput(`LE template → ${jsonPath}`);
|
|
100
|
+
writeOutput(`Tags CSV → ${csvPath}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
handleCommandError(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
exportCmd
|
|
107
|
+
.command("le-tags")
|
|
108
|
+
.description("Export tags as Litmus Edge DeviceHub CSV for bulk import")
|
|
109
|
+
.requiredOption("--device <name>", "LE device name (required)")
|
|
110
|
+
.requiredOption("--protocol <name>", "Protocol to export: opcua, modbus, or mqtt (required)")
|
|
111
|
+
.option("--instance <id>", "Export only tags for a specific instance")
|
|
112
|
+
.option("-o, --output <file>", "Write CSV to file instead of stdout")
|
|
113
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
114
|
+
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
115
|
+
.addHelpText(
|
|
116
|
+
"after",
|
|
117
|
+
`
|
|
118
|
+
Examples:
|
|
119
|
+
$ miravo export le-tags --device DTW-FAB-PLC --protocol opcua
|
|
120
|
+
$ miravo export le-tags --device DTW-FAB-PLC --protocol modbus --instance pump-001
|
|
121
|
+
$ miravo export le-tags --device DTW-FAB-PLC --protocol mqtt -o tags.csv`,
|
|
122
|
+
)
|
|
123
|
+
.action(async (options: ExportLeTagsOptions) => {
|
|
124
|
+
try {
|
|
125
|
+
if (!VALID_PROTOCOLS.includes(options.protocol as (typeof VALID_PROTOCOLS)[number])) {
|
|
126
|
+
writeOutput(
|
|
127
|
+
`Unknown protocol "${options.protocol}". Valid options: ${VALID_PROTOCOLS.join(", ")}`,
|
|
128
|
+
);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
134
|
+
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
135
|
+
const manifest = await getManifest(adminUrl, timeoutMs);
|
|
136
|
+
|
|
137
|
+
const allTags = manifest["tags"] as Array<Record<string, unknown>> | undefined;
|
|
138
|
+
|
|
139
|
+
if (!allTags || allTags.length === 0) {
|
|
140
|
+
writeOutput("No tags found in manifest.");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Filter by instance if specified
|
|
145
|
+
const filteredTags = options.instance
|
|
146
|
+
? allTags.filter((s) => s["instance"] === options.instance)
|
|
147
|
+
: allTags;
|
|
148
|
+
|
|
149
|
+
if (filteredTags.length === 0) {
|
|
150
|
+
writeOutput(
|
|
151
|
+
options.instance
|
|
152
|
+
? `No tags found for instance "${options.instance}".`
|
|
153
|
+
: "No tags found in manifest.",
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const protocol = options.protocol;
|
|
159
|
+
const driverName = LE_DRIVER_NAMES[protocol as keyof typeof LE_DRIVER_NAMES] ?? protocol;
|
|
160
|
+
const deviceName = options.device;
|
|
161
|
+
|
|
162
|
+
const csvRows = [LE_CSV_HEADER];
|
|
163
|
+
let totalRows = 0;
|
|
164
|
+
|
|
165
|
+
for (const tag of filteredTags) {
|
|
166
|
+
const protocols = tag["protocols"] as Record<string, Record<string, unknown>> | undefined;
|
|
167
|
+
const memberName = String(tag["member"] ?? "");
|
|
168
|
+
const browsePath = String(tag["browsePath"] ?? "");
|
|
169
|
+
const dataType = String(tag["dataType"] ?? "");
|
|
170
|
+
const unit = typeof tag["unit"] === "string" ? tag["unit"] : undefined;
|
|
171
|
+
const euRangeRaw = tag["euRange"];
|
|
172
|
+
const euRange =
|
|
173
|
+
Array.isArray(euRangeRaw) &&
|
|
174
|
+
euRangeRaw.length === 2 &&
|
|
175
|
+
typeof euRangeRaw[0] === "number" &&
|
|
176
|
+
typeof euRangeRaw[1] === "number"
|
|
177
|
+
? ([euRangeRaw[0], euRangeRaw[1]] as [number, number])
|
|
178
|
+
: undefined;
|
|
179
|
+
const model = String(tag["model"] ?? "");
|
|
180
|
+
const instance = String(tag["instance"] ?? "");
|
|
181
|
+
const access = String(tag["access"] ?? "");
|
|
182
|
+
const protocolData = protocols?.[protocol];
|
|
183
|
+
if (!protocolData) continue; // tag not exposed on this protocol (e.g. String on Modbus, property on MQTT)
|
|
184
|
+
|
|
185
|
+
csvRows.push(
|
|
186
|
+
buildRow(protocol, {
|
|
187
|
+
deviceName,
|
|
188
|
+
driverName,
|
|
189
|
+
memberName,
|
|
190
|
+
browsePath,
|
|
191
|
+
dataType,
|
|
192
|
+
protocolData,
|
|
193
|
+
model,
|
|
194
|
+
instance,
|
|
195
|
+
access,
|
|
196
|
+
...(unit !== undefined && { unit }),
|
|
197
|
+
...(euRange !== undefined && { euRange }),
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
totalRows++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const csv = `${csvRows.join("\n")}\n`;
|
|
204
|
+
|
|
205
|
+
if (options.output) {
|
|
206
|
+
await Bun.write(options.output, csv);
|
|
207
|
+
writeOutput(`Wrote ${totalRows} tags to ${options.output}`);
|
|
208
|
+
} else {
|
|
209
|
+
writeOutput(csv);
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
handleCommandError(error);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
// miravo export le-dt — export a model as Litmus Edge Digital Twin JSON
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
exportCmd
|
|
221
|
+
.command("le-dt")
|
|
222
|
+
.description("Export a model as Litmus Edge Digital Twin JSON for import into LE")
|
|
223
|
+
.requiredOption("--model <name>", "Model name to export (required)")
|
|
224
|
+
.option("-o, --output <file>", "Write JSON to file instead of stdout")
|
|
225
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
226
|
+
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
227
|
+
.addHelpText(
|
|
228
|
+
"after",
|
|
229
|
+
`
|
|
230
|
+
Examples:
|
|
231
|
+
$ miravo export le-dt --model centrifugal-pump
|
|
232
|
+
$ miravo export le-dt --model centrifugal-pump -o pump.le-dt.json`,
|
|
233
|
+
)
|
|
234
|
+
.action(
|
|
235
|
+
async (options: {
|
|
236
|
+
readonly model: string;
|
|
237
|
+
readonly output?: string;
|
|
238
|
+
readonly adminUrl?: string;
|
|
239
|
+
readonly timeout?: number;
|
|
240
|
+
}) => {
|
|
241
|
+
try {
|
|
242
|
+
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
243
|
+
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
244
|
+
const leDt = await getLeDtJson(adminUrl, options.model, timeoutMs);
|
|
245
|
+
const json = JSON.stringify(leDt, null, 2);
|
|
246
|
+
|
|
247
|
+
if (options.output) {
|
|
248
|
+
await Bun.write(options.output, json);
|
|
249
|
+
writeOutput(`Wrote LE Digital Twin JSON to ${options.output}`);
|
|
250
|
+
} else {
|
|
251
|
+
writeOutput(json);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
handleCommandError(error);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Row builder
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
interface RowInput {
|
|
265
|
+
readonly deviceName: string;
|
|
266
|
+
readonly driverName: string;
|
|
267
|
+
readonly memberName: string;
|
|
268
|
+
readonly browsePath: string;
|
|
269
|
+
readonly dataType: string;
|
|
270
|
+
readonly protocolData: Record<string, unknown> | undefined;
|
|
271
|
+
readonly unit?: string;
|
|
272
|
+
readonly euRange?: readonly [number, number];
|
|
273
|
+
readonly model: string;
|
|
274
|
+
readonly instance: string;
|
|
275
|
+
readonly access: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildRow(protocol: string, input: RowInput): string {
|
|
279
|
+
switch (protocol) {
|
|
280
|
+
case "mqtt":
|
|
281
|
+
return buildMqttRow(input);
|
|
282
|
+
case "opcua":
|
|
283
|
+
return buildOpcuaRow(input);
|
|
284
|
+
case "modbus":
|
|
285
|
+
return buildModbusRow(input);
|
|
286
|
+
default: {
|
|
287
|
+
const _exhaustive: never = protocol as never;
|
|
288
|
+
throw new Error(`Unsupported LE export protocol: "${_exhaustive}"`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function buildMqttRow(input: RowInput): string {
|
|
294
|
+
const topic =
|
|
295
|
+
typeof input.protocolData?.["topic"] === "string"
|
|
296
|
+
? input.protocolData["topic"]
|
|
297
|
+
: `${input.browsePath}/${input.memberName}`;
|
|
298
|
+
|
|
299
|
+
return [
|
|
300
|
+
"", // deviceId
|
|
301
|
+
esc(input.deviceName),
|
|
302
|
+
esc(input.driverName),
|
|
303
|
+
esc(topic), // name = MQTT topic
|
|
304
|
+
esc(input.memberName), // tagName
|
|
305
|
+
"", // description
|
|
306
|
+
"string", // valueType — MQTT is always string
|
|
307
|
+
"", // registerId
|
|
308
|
+
"false", // publishCoV
|
|
309
|
+
"", // tagFormula
|
|
310
|
+
buildMetaData(input), // metaData
|
|
311
|
+
"Disable", // Retain
|
|
312
|
+
"0", // Qos
|
|
313
|
+
"", // address
|
|
314
|
+
"", // count
|
|
315
|
+
"", // pollingInterval
|
|
316
|
+
"", // endianness
|
|
317
|
+
].join(",");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildOpcuaRow(input: RowInput): string {
|
|
321
|
+
const nodeId =
|
|
322
|
+
typeof input.protocolData?.["nodeId"] === "string"
|
|
323
|
+
? input.protocolData["nodeId"]
|
|
324
|
+
: `ns=1;s=${input.browsePath}/${input.memberName}`;
|
|
325
|
+
const valueType = OPCUA_VALUE_TYPE[input.dataType] ?? "string";
|
|
326
|
+
|
|
327
|
+
return [
|
|
328
|
+
"", // deviceId
|
|
329
|
+
esc(input.deviceName),
|
|
330
|
+
esc(input.driverName),
|
|
331
|
+
esc(nodeId), // name = OPC UA NodeId
|
|
332
|
+
esc(input.memberName), // tagName
|
|
333
|
+
"", // description
|
|
334
|
+
valueType,
|
|
335
|
+
"", // registerId
|
|
336
|
+
"false", // publishCoV
|
|
337
|
+
"", // tagFormula
|
|
338
|
+
buildMetaData(input), // metaData
|
|
339
|
+
"", // Retain
|
|
340
|
+
"", // Qos
|
|
341
|
+
"", // address
|
|
342
|
+
"", // count
|
|
343
|
+
"", // pollingInterval
|
|
344
|
+
"", // endianness
|
|
345
|
+
].join(",");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildModbusRow(input: RowInput): string {
|
|
349
|
+
const registerType = String(input.protocolData?.["registerType"] ?? "");
|
|
350
|
+
const valueType = MODBUS_VALUE_TYPE[registerType] ?? "float32";
|
|
351
|
+
const address = input.protocolData?.["address"];
|
|
352
|
+
|
|
353
|
+
return [
|
|
354
|
+
"", // deviceId
|
|
355
|
+
esc(input.deviceName),
|
|
356
|
+
esc(input.driverName),
|
|
357
|
+
"H", // name = Holding register
|
|
358
|
+
esc(input.memberName), // tagName
|
|
359
|
+
"", // description
|
|
360
|
+
valueType,
|
|
361
|
+
"", // registerId
|
|
362
|
+
"false", // publishCoV
|
|
363
|
+
"", // tagFormula
|
|
364
|
+
buildMetaData(input), // metaData
|
|
365
|
+
"", // Retain
|
|
366
|
+
"", // Qos
|
|
367
|
+
typeof address === "number" ? String(address) : "0", // address
|
|
368
|
+
"1", // count — always 1 scalar value; LE derives register width from valueType
|
|
369
|
+
"1.0000000000", // pollingInterval
|
|
370
|
+
"AB CD", // endianness (big-endian — matches Miravo Modbus)
|
|
371
|
+
].join(",");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Helpers
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
/** Escape a comma-delimited CSV field: quote if it contains delimiters, quotes, or newlines. */
|
|
379
|
+
function esc(value: string): string {
|
|
380
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
381
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
382
|
+
}
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Build the LE metaData JSON cell from Miravo tag metadata. */
|
|
387
|
+
function buildMetaData(input: RowInput): string {
|
|
388
|
+
const unsPath = input.browsePath ? `${input.browsePath}/${input.memberName}` : input.memberName;
|
|
389
|
+
|
|
390
|
+
const meta: Record<string, unknown> = {
|
|
391
|
+
uns_path: unsPath,
|
|
392
|
+
model: input.model,
|
|
393
|
+
instance: input.instance,
|
|
394
|
+
access: input.access,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
if (input.unit != null) meta["unit"] = input.unit;
|
|
398
|
+
if (input.euRange != null) {
|
|
399
|
+
meta["eu_range_min"] = String(input.euRange[0]);
|
|
400
|
+
meta["eu_range_max"] = String(input.euRange[1]);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return esc(JSON.stringify(meta));
|
|
404
|
+
}
|
package/src/commands/fork.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function registerForkCommand(program: Command): void {
|
|
|
15
15
|
|
|
16
16
|
fork
|
|
17
17
|
.command("model <name>")
|
|
18
|
-
.description("
|
|
18
|
+
.description("Copy a built-in model to local registry for customization")
|
|
19
19
|
.option("--force", "Overwrite if already exists in local")
|
|
20
20
|
.addHelpText(
|
|
21
21
|
"after",
|
|
@@ -30,7 +30,7 @@ Examples:
|
|
|
30
30
|
|
|
31
31
|
fork
|
|
32
32
|
.command("template <idNameOrPath>")
|
|
33
|
-
.description("
|
|
33
|
+
.description("Copy a template and its referenced models to local registry")
|
|
34
34
|
.option("--force", "Overwrite if already exists in local")
|
|
35
35
|
.option("--models-only", "Only fork referenced models, skip the template itself")
|
|
36
36
|
.addHelpText(
|
package/src/commands/inject.ts
CHANGED
|
@@ -21,11 +21,11 @@ interface InjectOptions {
|
|
|
21
21
|
export function registerInjectCommand(program: Command): void {
|
|
22
22
|
program
|
|
23
23
|
.command("inject [instanceId] [fault]")
|
|
24
|
-
.description("Trigger or clear a fault on a running instance")
|
|
24
|
+
.description("Trigger or clear a fault on a running instance (interactive if no args)")
|
|
25
25
|
.option("--clear", "Clear the fault instead of triggering it")
|
|
26
26
|
.option("--json", "Machine-readable JSON output")
|
|
27
27
|
.option("--name <name>", "Target simulation name")
|
|
28
|
-
.option("--admin-url <url>", "
|
|
28
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
29
29
|
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
30
30
|
.addHelpText(
|
|
31
31
|
"after",
|
package/src/commands/load.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function registerLoadCommand(program: Command): void {
|
|
|
37
37
|
parsePositiveInt,
|
|
38
38
|
)
|
|
39
39
|
.option("--json", "Machine-readable JSON output")
|
|
40
|
-
.option("--admin-url <url>", "
|
|
40
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
41
41
|
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
42
42
|
.addHelpText(
|
|
43
43
|
"after",
|
package/src/commands/logs.ts
CHANGED
|
@@ -126,29 +126,27 @@ async function logsAction(name: string, options: LogsOptions): Promise<void> {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/** Format a WebSocket message for terminal output. Returns null for tick events. */
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
/** Format a WebSocket message for terminal output. Returns null for tick/snapshot events. */
|
|
130
|
+
function formatWSEvent(msg: WSMessage): string | null {
|
|
131
|
+
if (msg.type !== "runtime:event") return null;
|
|
132
|
+
const evt = msg.event;
|
|
133
|
+
switch (evt.kind) {
|
|
132
134
|
case "fault:triggered":
|
|
133
|
-
return `${colors.red("[fault:triggered]")} ${
|
|
135
|
+
return `${colors.red("[fault:triggered]")} ${evt.instanceId} · ${evt.fault}`;
|
|
134
136
|
case "fault:cleared":
|
|
135
|
-
return `${colors.green("[fault:cleared]")} ${
|
|
137
|
+
return `${colors.green("[fault:cleared]")} ${evt.instanceId} · ${evt.fault}`;
|
|
136
138
|
case "lifecycle:changed":
|
|
137
|
-
return `${colors.blue("[lifecycle:changed]")} ${
|
|
139
|
+
return `${colors.blue("[lifecycle:changed]")} ${evt.instanceId} · ${evt.from} → ${evt.to}`;
|
|
138
140
|
case "instance:created":
|
|
139
|
-
return `${colors.cyan("[instance:created]")} ${
|
|
141
|
+
return `${colors.cyan("[instance:created]")} ${evt.id} · ${evt.assetType}`;
|
|
140
142
|
case "instance:removed":
|
|
141
|
-
return `${colors.yellow("[instance:removed]")} ${
|
|
143
|
+
return `${colors.yellow("[instance:removed]")} ${evt.id}`;
|
|
142
144
|
case "engine:state":
|
|
143
|
-
return `${colors.magenta("[engine:state]")} ${
|
|
145
|
+
return `${colors.magenta("[engine:state]")} ${evt.from} → ${evt.to}`;
|
|
144
146
|
case "adapter:enabled":
|
|
145
|
-
return `${colors.cyan("[adapter:enabled]")} ${
|
|
147
|
+
return `${colors.cyan("[adapter:enabled]")} ${evt.name}`;
|
|
146
148
|
case "adapter:disabled":
|
|
147
|
-
return `${colors.yellow("[adapter:disabled]")} ${
|
|
148
|
-
case "tick":
|
|
149
|
-
case "snapshot":
|
|
150
|
-
case "resync-required":
|
|
151
|
-
return null;
|
|
149
|
+
return `${colors.yellow("[adapter:disabled]")} ${evt.name}`;
|
|
152
150
|
default:
|
|
153
151
|
return null;
|
|
154
152
|
}
|
|
@@ -88,7 +88,7 @@ describe("formatModelInspect", () => {
|
|
|
88
88
|
expect(output).toContain("centrifugal-pump");
|
|
89
89
|
expect(output).toContain("Parameters:");
|
|
90
90
|
expect(output).toContain("rated_rpm");
|
|
91
|
-
expect(output).toContain("Members (
|
|
91
|
+
expect(output).toContain("Members (24):");
|
|
92
92
|
expect(output).toContain("Methods (5):");
|
|
93
93
|
expect(output).toContain("availability_state");
|
|
94
94
|
expect(output).toContain("Lifecycle: healthy");
|
package/src/commands/models.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function registerModelsCommand(program: Command): void {
|
|
|
19
19
|
models
|
|
20
20
|
.command("list")
|
|
21
21
|
.description("Discover and list available models")
|
|
22
|
-
.option("--dir <path>", "Additional directory to search for models")
|
|
22
|
+
.option("--dir <path>", "Additional directory to search for .twin.yaml models")
|
|
23
23
|
.addHelpText(
|
|
24
24
|
"after",
|
|
25
25
|
`
|
package/src/commands/pause.ts
CHANGED
|
@@ -14,9 +14,9 @@ interface PauseResumeOptions {
|
|
|
14
14
|
export function registerPauseCommand(program: Command): void {
|
|
15
15
|
program
|
|
16
16
|
.command("pause")
|
|
17
|
-
.description("Pause the running simulation")
|
|
17
|
+
.description("Pause the running simulation (state is preserved)")
|
|
18
18
|
.option("--json", "Machine-readable JSON output")
|
|
19
|
-
.option("--admin-url <url>", "
|
|
19
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
20
20
|
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
21
21
|
.addHelpText(
|
|
22
22
|
"after",
|
|
@@ -38,7 +38,7 @@ export function registerResumeCommand(program: Command): void {
|
|
|
38
38
|
.command("resume")
|
|
39
39
|
.description("Resume a paused simulation")
|
|
40
40
|
.option("--json", "Machine-readable JSON output")
|
|
41
|
-
.option("--admin-url <url>", "
|
|
41
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
42
42
|
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
43
43
|
.addHelpText(
|
|
44
44
|
"after",
|