@camstack/system 1.1.23 → 1.1.25
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/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/device-manager/device-link-overlay.d.ts +15 -5
- package/dist/builtins/device-manager/device-link-resolver.d.ts +11 -0
- package/dist/builtins/device-manager/device-manager.addon.js +255 -33
- package/dist/builtins/device-manager/device-manager.addon.mjs +256 -34
- package/dist/builtins/device-manager/device-meta-actions.d.ts +21 -0
- package/dist/builtins/device-manager/device-meta-types.d.ts +12 -1
- package/dist/builtins/device-manager/device-projection.d.ts +1 -0
- package/dist/builtins/platform-probe/index.d.ts +22 -0
- package/dist/builtins/platform-probe/index.js +83 -2
- package/dist/builtins/platform-probe/index.mjs +84 -3
- package/dist/index.js +1 -2
- package/dist/index.mjs +2 -2
- package/dist/kernel/index.d.ts +0 -2
- package/dist/kernel/transport/parent-unowned-call.d.ts +20 -0
- package/dist/{manifest-python-deps-D7iR07uA.js → manifest-python-deps-BqXCckob.js} +13 -36
- package/dist/{manifest-python-deps-hDWMDe_b.mjs → manifest-python-deps-Dxl4twM3.mjs} +17 -34
- package/package.json +1 -1
- package/dist/kernel/hwaccel/hwaccel-service.d.ts +0 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
2
|
-
import { ALL_CAPABILITY_DEFINITIONS, BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceRole, DeviceStatusSchema, DeviceType, EventCategory, STREAM_PROFILE_META, WELL_KNOWN_TAB_MAP, applyTransform, buildStreamParamsConfigSchema, deviceManagerCapability, deviceStateCapability, deviceStatusCapability, enumerateItemArrayFields, enumerateSchemaFields, errMsg, getByPath, isDeviceConfigCap, parseStreamParamsFormPatch, setByPath, sleep } from "@camstack/types";
|
|
2
|
+
import { ALL_CAPABILITY_DEFINITIONS, BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceRole, DeviceStatusSchema, DeviceType, EventCategory, STREAM_PROFILE_META, WELL_KNOWN_TAB_MAP, applyTransform, buildStreamParamsConfigSchema, deviceManagerCapability, deviceStateCapability, deviceStatusCapability, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateLinkExpression, getByPath, isDeviceConfigCap, normalizeUnit, parseStreamParamsFormPatch, setByPath, sleep, toExpressionValue, validateExpressionSource } from "@camstack/types";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
/**
|
|
@@ -295,7 +295,8 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
|
|
|
295
295
|
...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
|
|
296
296
|
...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
|
|
297
297
|
...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
|
|
298
|
-
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {}
|
|
298
|
+
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
|
|
299
|
+
...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
|
|
299
300
|
};
|
|
300
301
|
}
|
|
301
302
|
function resolveDeviceById(registry, deviceId) {
|
|
@@ -1366,20 +1367,41 @@ var DeviceEventPropagator = class {
|
|
|
1366
1367
|
function nodeKey(deviceId, cap) {
|
|
1367
1368
|
return `${deviceId}:${cap}`;
|
|
1368
1369
|
}
|
|
1370
|
+
/** The target's container stableId — its parent's (falling back to its own for
|
|
1371
|
+
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
1372
|
+
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
1373
|
+
function containerStableId(targetRow, rowById) {
|
|
1374
|
+
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
1375
|
+
}
|
|
1376
|
+
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
1377
|
+
* null when it is a literal or resolves to no known device. */
|
|
1378
|
+
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
1379
|
+
if (binding.kind === "literal") return null;
|
|
1380
|
+
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
1381
|
+
const srcId = idByStableId.get(wantedStableId);
|
|
1382
|
+
if (srcId === void 0) return null;
|
|
1383
|
+
return nodeKey(srcId, binding.cap);
|
|
1384
|
+
}
|
|
1369
1385
|
/**
|
|
1370
|
-
* Resolve a link source to
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1386
|
+
* Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
|
|
1387
|
+
* A literal source depends on nothing; a field/global source depends on exactly
|
|
1388
|
+
* one node; an EXPRESSION source depends on one node per resolvable non-literal
|
|
1389
|
+
* binding. Unresolvable (dangling) bindings contribute no edge — they cannot
|
|
1390
|
+
* loop while dangling; a cycle that only becomes resolvable later is bounded by
|
|
1391
|
+
* the resolve-time re-entrancy guard.
|
|
1375
1392
|
*/
|
|
1376
|
-
function
|
|
1393
|
+
function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
|
|
1377
1394
|
const src = link.source;
|
|
1378
|
-
if (src.kind === "
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1395
|
+
if (src.kind === "expression") {
|
|
1396
|
+
const nodes = [];
|
|
1397
|
+
for (const binding of Object.values(src.bindings)) {
|
|
1398
|
+
const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
|
|
1399
|
+
if (node !== null) nodes.push(node);
|
|
1400
|
+
}
|
|
1401
|
+
return nodes;
|
|
1402
|
+
}
|
|
1403
|
+
const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
|
|
1404
|
+
return node !== null ? [node] : [];
|
|
1383
1405
|
}
|
|
1384
1406
|
/**
|
|
1385
1407
|
* Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
|
|
@@ -1397,11 +1419,11 @@ function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
|
|
|
1397
1419
|
const dependsOn = /* @__PURE__ */ new Map();
|
|
1398
1420
|
const addEdges = (row, links) => {
|
|
1399
1421
|
for (const link of links) {
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1422
|
+
const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
|
|
1423
|
+
if (srcNodes.length === 0) continue;
|
|
1402
1424
|
const tNode = nodeKey(row.id, link.target.cap);
|
|
1403
1425
|
const list = dependsOn.get(tNode) ?? [];
|
|
1404
|
-
list.push(srcNode);
|
|
1426
|
+
for (const srcNode of srcNodes) list.push(srcNode);
|
|
1405
1427
|
dependsOn.set(tNode, list);
|
|
1406
1428
|
}
|
|
1407
1429
|
};
|
|
@@ -1468,7 +1490,7 @@ function dfsFindCycle(root, dependsOn) {
|
|
|
1468
1490
|
* removeDevice), config persistence (persistConfig, loadConfig), the meta
|
|
1469
1491
|
* surface load (loadMeta, loadRuntimeState), every meta setter (setName,
|
|
1470
1492
|
* setLocation, setType, setIntegrationId, setLinkDeviceId,
|
|
1471
|
-
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole,
|
|
1493
|
+
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
|
|
1472
1494
|
* applyInitialMeta, setMetadata, setDisabled), and the location registry
|
|
1473
1495
|
* (listLocations, addLocation, removeLocation).
|
|
1474
1496
|
*
|
|
@@ -1577,6 +1599,7 @@ async function registerDevice(pctx, input) {
|
|
|
1577
1599
|
...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
|
|
1578
1600
|
...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
|
|
1579
1601
|
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
1602
|
+
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
1580
1603
|
parentDeviceId,
|
|
1581
1604
|
id,
|
|
1582
1605
|
features: featuresArr,
|
|
@@ -2018,6 +2041,11 @@ async function setDeviceLinks(pctx, input) {
|
|
|
2018
2041
|
const { deviceId, deviceLinks } = input;
|
|
2019
2042
|
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
2020
2043
|
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
2044
|
+
for (const link of deviceLinks) {
|
|
2045
|
+
if (link.source.kind !== "expression") continue;
|
|
2046
|
+
const err = validateExpressionSource(link.source);
|
|
2047
|
+
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
2048
|
+
}
|
|
2021
2049
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2022
2050
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2023
2051
|
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
@@ -2088,6 +2116,84 @@ async function setRole(pctx, input) {
|
|
|
2088
2116
|
});
|
|
2089
2117
|
}
|
|
2090
2118
|
/**
|
|
2119
|
+
* Normalize an operator-authored display override at write time so the render
|
|
2120
|
+
* path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
|
|
2121
|
+
* every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
|
|
2122
|
+
* never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
|
|
2123
|
+
* as-is (the render path refuses conversion for unknown spellings).
|
|
2124
|
+
*/
|
|
2125
|
+
function normalizeDisplayOverride(display) {
|
|
2126
|
+
return {
|
|
2127
|
+
...display,
|
|
2128
|
+
...display.unit !== void 0 ? { unit: normalizeUnit(display.unit) ?? display.unit } : {},
|
|
2129
|
+
...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
|
|
2130
|
+
...refine,
|
|
2131
|
+
unit: normalizeUnit(refine.unit) ?? refine.unit
|
|
2132
|
+
} : refine])) } : {}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
2137
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
2138
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
2139
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
2140
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
2141
|
+
*/
|
|
2142
|
+
async function setDisplay(pctx, input) {
|
|
2143
|
+
const { deviceId, display } = input;
|
|
2144
|
+
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
2145
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2146
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2147
|
+
if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
2148
|
+
const { meta: m } = persisted;
|
|
2149
|
+
const key = String(deviceId);
|
|
2150
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2151
|
+
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
2152
|
+
...m,
|
|
2153
|
+
display: normalized
|
|
2154
|
+
};
|
|
2155
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2156
|
+
...allMeta,
|
|
2157
|
+
[key]: nextRow
|
|
2158
|
+
} });
|
|
2159
|
+
});
|
|
2160
|
+
pctx.host.ctx.eventBus.emit({
|
|
2161
|
+
id: randomUUID(),
|
|
2162
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2163
|
+
source: {
|
|
2164
|
+
type: "device",
|
|
2165
|
+
id: deviceId
|
|
2166
|
+
},
|
|
2167
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2168
|
+
data: {
|
|
2169
|
+
deviceId,
|
|
2170
|
+
field: "display",
|
|
2171
|
+
value: normalized
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
2177
|
+
* set. Not per-device — a plain top-level-key read.
|
|
2178
|
+
*/
|
|
2179
|
+
async function getRoleDisplayDefaults(pctx, _input) {
|
|
2180
|
+
return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
2184
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
2185
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
2186
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
2187
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
2188
|
+
*/
|
|
2189
|
+
async function setRoleDisplayDefaults(pctx, input) {
|
|
2190
|
+
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
2191
|
+
...def,
|
|
2192
|
+
unit: normalizeUnit(def.unit) ?? def.unit
|
|
2193
|
+
} : def]));
|
|
2194
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2091
2197
|
* Batched meta pre-seed. Applies every provided field to the
|
|
2092
2198
|
* device's meta row in ONE read-modify-write under a single
|
|
2093
2199
|
* `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
|
|
@@ -2332,23 +2438,33 @@ async function removeLocation(pctx, input) {
|
|
|
2332
2438
|
function buildLinkIndexes(entries) {
|
|
2333
2439
|
const targets = /* @__PURE__ */ new Map();
|
|
2334
2440
|
const dependents = /* @__PURE__ */ new Map();
|
|
2335
|
-
for (const { targetDeviceId, link, sourceDeviceId } of entries) {
|
|
2441
|
+
for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
|
|
2336
2442
|
const tKey = `${targetDeviceId}:${link.target.cap}`;
|
|
2337
2443
|
const tList = targets.get(tKey) ?? [];
|
|
2338
2444
|
tList.push({
|
|
2339
2445
|
link,
|
|
2340
|
-
sourceDeviceId
|
|
2446
|
+
sourceDeviceId,
|
|
2447
|
+
...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
|
|
2341
2448
|
});
|
|
2342
2449
|
targets.set(tKey, tList);
|
|
2343
|
-
|
|
2344
|
-
const sKey = `${
|
|
2450
|
+
const addDependent = (sourceId, sourceCap) => {
|
|
2451
|
+
const sKey = `${sourceId}:${sourceCap}`;
|
|
2345
2452
|
const sList = dependents.get(sKey) ?? [];
|
|
2346
2453
|
sList.push({
|
|
2347
2454
|
targetDeviceId,
|
|
2348
2455
|
targetCap: link.target.cap
|
|
2349
2456
|
});
|
|
2350
2457
|
dependents.set(sKey, sList);
|
|
2458
|
+
};
|
|
2459
|
+
if (link.source.kind === "expression") {
|
|
2460
|
+
if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
|
|
2461
|
+
const binding = link.source.bindings[name];
|
|
2462
|
+
if (binding === void 0 || binding.kind === "literal") continue;
|
|
2463
|
+
addDependent(bindingDeviceId, binding.cap);
|
|
2464
|
+
}
|
|
2465
|
+
continue;
|
|
2351
2466
|
}
|
|
2467
|
+
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
2352
2468
|
}
|
|
2353
2469
|
return {
|
|
2354
2470
|
targets,
|
|
@@ -2503,6 +2619,23 @@ var DeviceMetaStore = class {
|
|
|
2503
2619
|
if (container === void 0) continue;
|
|
2504
2620
|
for (const link of targetMeta.deviceLinks ?? []) {
|
|
2505
2621
|
const src = link.source;
|
|
2622
|
+
if (src.kind === "expression") {
|
|
2623
|
+
const bindingSourceIds = {};
|
|
2624
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
2625
|
+
if (b.kind === "literal") continue;
|
|
2626
|
+
const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
|
|
2627
|
+
expectedSources.add(wanted);
|
|
2628
|
+
const bid = idByStableId.get(wanted);
|
|
2629
|
+
if (bid !== void 0) bindingSourceIds[name] = bid;
|
|
2630
|
+
}
|
|
2631
|
+
entries.push({
|
|
2632
|
+
targetDeviceId: targetId,
|
|
2633
|
+
link,
|
|
2634
|
+
sourceDeviceId: -1,
|
|
2635
|
+
bindingSourceIds
|
|
2636
|
+
});
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2506
2639
|
if (src.kind === "literal") {
|
|
2507
2640
|
entries.push({
|
|
2508
2641
|
targetDeviceId: targetId,
|
|
@@ -2620,6 +2753,7 @@ async function listAll(pctx, input) {
|
|
|
2620
2753
|
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2621
2754
|
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2622
2755
|
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2756
|
+
...m?.display !== void 0 ? { display: m.display } : {},
|
|
2623
2757
|
...(() => {
|
|
2624
2758
|
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
2625
2759
|
return si !== void 0 ? { sourceInfo: si } : {};
|
|
@@ -2668,6 +2802,7 @@ async function getDevice(pctx, input) {
|
|
|
2668
2802
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2669
2803
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2670
2804
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2805
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2671
2806
|
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
2672
2807
|
};
|
|
2673
2808
|
}
|
|
@@ -2730,6 +2865,7 @@ async function getChildren(pctx, input) {
|
|
|
2730
2865
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2731
2866
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2732
2867
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2868
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2733
2869
|
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
2734
2870
|
});
|
|
2735
2871
|
}
|
|
@@ -3069,7 +3205,37 @@ function isRecord(x) {
|
|
|
3069
3205
|
* `source.kind` directly.
|
|
3070
3206
|
*/
|
|
3071
3207
|
function resolveLinkValue(link, readField) {
|
|
3072
|
-
|
|
3208
|
+
const src = link.source;
|
|
3209
|
+
if (src.kind === "expression") return void 0;
|
|
3210
|
+
return applyTransform(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Sync-channel expression resolution: bindings read from the mirror via
|
|
3214
|
+
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
3215
|
+
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
3216
|
+
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
3217
|
+
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
3218
|
+
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
3219
|
+
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
3220
|
+
*/
|
|
3221
|
+
function resolveExpressionLinkValue(rl, readField, now) {
|
|
3222
|
+
const src = rl.link.source;
|
|
3223
|
+
if (src.kind !== "expression") return void 0;
|
|
3224
|
+
const bindingValues = {};
|
|
3225
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3226
|
+
if (b.kind === "literal") {
|
|
3227
|
+
bindingValues[name] = b.value;
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
const bId = rl.bindingSourceIds?.[name];
|
|
3231
|
+
if (bId === void 0) {
|
|
3232
|
+
bindingValues[name] = null;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
bindingValues[name] = toExpressionValue(readField(bId, b.cap, b.fieldPath)) ?? null;
|
|
3236
|
+
}
|
|
3237
|
+
const result = evaluateLinkExpression(src.expr, bindingValues, now);
|
|
3238
|
+
return result.ok ? applyTransform(result.value, rl.link.transform) : void 0;
|
|
3073
3239
|
}
|
|
3074
3240
|
/** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
|
|
3075
3241
|
* concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
|
|
@@ -3390,9 +3556,10 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3390
3556
|
const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
|
|
3391
3557
|
const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
|
|
3392
3558
|
if (!links || links.length === 0) return raw ? { ...raw } : null;
|
|
3559
|
+
const now = Date.now();
|
|
3393
3560
|
const resolved = links.map((rl) => ({
|
|
3394
3561
|
link: rl.link,
|
|
3395
|
-
sourceValue: resolveLinkValue(rl.link, (srcCap, fieldPath) => getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3562
|
+
sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => getByPath(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3396
3563
|
}));
|
|
3397
3564
|
const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
|
|
3398
3565
|
const schema = capStatus?.schema;
|
|
@@ -3583,6 +3750,39 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3583
3750
|
const parentMeta = allMeta[String(parentId)];
|
|
3584
3751
|
if (parentMeta) containerStableId = parentMeta.stableId;
|
|
3585
3752
|
}
|
|
3753
|
+
const now = Date.now();
|
|
3754
|
+
const statusMemo = /* @__PURE__ */ new Map();
|
|
3755
|
+
const readSourceStatus = async (srcId, srcCap) => {
|
|
3756
|
+
const key = `${srcId}:${srcCap}`;
|
|
3757
|
+
const cached = statusMemo.get(key);
|
|
3758
|
+
if (cached !== void 0) return cached;
|
|
3759
|
+
let result;
|
|
3760
|
+
try {
|
|
3761
|
+
const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
|
|
3762
|
+
result = {
|
|
3763
|
+
status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
|
|
3764
|
+
ok: true
|
|
3765
|
+
};
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3768
|
+
tags: {
|
|
3769
|
+
deviceId,
|
|
3770
|
+
capName: cap
|
|
3771
|
+
},
|
|
3772
|
+
meta: {
|
|
3773
|
+
sourceCap: srcCap,
|
|
3774
|
+
sourceId: srcId,
|
|
3775
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
result = {
|
|
3779
|
+
status: void 0,
|
|
3780
|
+
ok: false
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
statusMemo.set(key, result);
|
|
3784
|
+
return result;
|
|
3785
|
+
};
|
|
3586
3786
|
const resolved = [];
|
|
3587
3787
|
for (const link of links) {
|
|
3588
3788
|
const src = link.source;
|
|
@@ -3593,27 +3793,46 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3593
3793
|
});
|
|
3594
3794
|
continue;
|
|
3595
3795
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3796
|
+
if (src.kind === "expression") {
|
|
3797
|
+
const bindingValues = {};
|
|
3798
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3799
|
+
if (b.kind === "literal") {
|
|
3800
|
+
bindingValues[name] = b.value;
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
|
|
3804
|
+
if (bId === null) {
|
|
3805
|
+
bindingValues[name] = null;
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
bindingValues[name] = toExpressionValue(getByPath((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
|
|
3809
|
+
}
|
|
3810
|
+
const result = evaluateLinkExpression(src.expr, bindingValues, now);
|
|
3811
|
+
if (result.ok) resolved.push({
|
|
3602
3812
|
link,
|
|
3603
|
-
sourceValue: applyTransform(
|
|
3813
|
+
sourceValue: applyTransform(result.value, link.transform)
|
|
3604
3814
|
});
|
|
3605
|
-
|
|
3606
|
-
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3815
|
+
else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
|
|
3607
3816
|
tags: {
|
|
3608
3817
|
deviceId,
|
|
3609
3818
|
capName: cap
|
|
3610
3819
|
},
|
|
3611
3820
|
meta: {
|
|
3612
|
-
|
|
3613
|
-
error:
|
|
3821
|
+
linkId: link.id,
|
|
3822
|
+
error: result.error
|
|
3614
3823
|
}
|
|
3615
3824
|
});
|
|
3825
|
+
continue;
|
|
3616
3826
|
}
|
|
3827
|
+
const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
|
|
3828
|
+
if (srcId === null) continue;
|
|
3829
|
+
const read = await readSourceStatus(srcId, src.cap);
|
|
3830
|
+
if (!read.ok) continue;
|
|
3831
|
+
const raw = getByPath(read.status, src.fieldPath);
|
|
3832
|
+
resolved.push({
|
|
3833
|
+
link,
|
|
3834
|
+
sourceValue: applyTransform(raw, link.transform)
|
|
3835
|
+
});
|
|
3617
3836
|
}
|
|
3618
3837
|
if (resolved.length === 0) return null;
|
|
3619
3838
|
const synthesizing = !isRecord$1(baseStatus);
|
|
@@ -3926,6 +4145,9 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
3926
4145
|
setChildLayout: (input) => setChildLayout(pctx, input),
|
|
3927
4146
|
setDeviceLinks: (input) => setDeviceLinks(pctx, input),
|
|
3928
4147
|
setRole: (input) => setRole(pctx, input),
|
|
4148
|
+
setDisplay: (input) => setDisplay(pctx, input),
|
|
4149
|
+
getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
|
|
4150
|
+
setRoleDisplayDefaults: (input) => setRoleDisplayDefaults(pctx, input),
|
|
3929
4151
|
applyInitialMeta: (input) => applyInitialMeta(pctx, input),
|
|
3930
4152
|
setMetadata: (input) => setMetadata(pctx, input),
|
|
3931
4153
|
setDisabled: (input) => setDisabled(pctx, input),
|
|
@@ -97,6 +97,27 @@ export declare function setDeviceLinks(pctx: ProviderContext, input: Parameters<
|
|
|
97
97
|
* `setIntegrationId`). Idempotent. `null` clears a previous role.
|
|
98
98
|
*/
|
|
99
99
|
export declare function setRole(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setRole']>[0]): ReturnType<IDeviceManagerProvider['setRole']>;
|
|
100
|
+
/**
|
|
101
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
102
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
103
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
104
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
105
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
106
|
+
*/
|
|
107
|
+
export declare function setDisplay(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setDisplay']>[0]): ReturnType<IDeviceManagerProvider['setDisplay']>;
|
|
108
|
+
/**
|
|
109
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
110
|
+
* set. Not per-device — a plain top-level-key read.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getRoleDisplayDefaults(pctx: ProviderContext, _input: Parameters<IDeviceManagerProvider['getRoleDisplayDefaults']>[0]): ReturnType<IDeviceManagerProvider['getRoleDisplayDefaults']>;
|
|
113
|
+
/**
|
|
114
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
115
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
116
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
117
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
118
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
119
|
+
*/
|
|
120
|
+
export declare function setRoleDisplayDefaults(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setRoleDisplayDefaults']>[0]): ReturnType<IDeviceManagerProvider['setRoleDisplayDefaults']>;
|
|
100
121
|
/**
|
|
101
122
|
* Batched meta pre-seed. Applies every provided field to the
|
|
102
123
|
* device's meta row in ONE read-modify-write under a single
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChildLayout, DeviceLinks } from '@camstack/types';
|
|
1
|
+
import { ChildLayout, DeviceDisplayOverride, DeviceLinks, RoleDisplayDefault } from '@camstack/types';
|
|
2
2
|
/**
|
|
3
3
|
* Wire shape matching `z.infer<typeof SettingsSchemaWithValuesSchema>` —
|
|
4
4
|
* duplicated as a plain interface because importing the Zod schema across
|
|
@@ -132,6 +132,11 @@ export interface PersistedDeviceMeta {
|
|
|
132
132
|
* Optional: only present for accessory children that carry a known role
|
|
133
133
|
* (e.g. 'numeric-sensor', 'binary-sensor'). `null` clears a previous role. */
|
|
134
134
|
role?: string | null;
|
|
135
|
+
/** Operator-authored per-device display override (icon/label/unit/precision/
|
|
136
|
+
* hidden), set via `setDisplay`. Same create/persist/project/restore
|
|
137
|
+
* lifecycle as `deviceLinks`. Absent ⇒ no override. Applied at render time
|
|
138
|
+
* only — storage stays in source units. */
|
|
139
|
+
display?: DeviceDisplayOverride;
|
|
135
140
|
}
|
|
136
141
|
export interface AddonStore {
|
|
137
142
|
deviceIndex?: Record<string, string[]>;
|
|
@@ -166,4 +171,10 @@ export interface AddonStore {
|
|
|
166
171
|
* the operator forgot to register a label before assigning it.
|
|
167
172
|
*/
|
|
168
173
|
locations?: readonly string[];
|
|
174
|
+
/** Operator-authored per-role display defaults (unit/precision/icon), keyed
|
|
175
|
+
* by `DeviceRole` string. Resolution merges these UNDER any per-device
|
|
176
|
+
* `display` override. Written whole-record by `setRoleDisplayDefaults`
|
|
177
|
+
* (full replace — no read-modify-write, single writer, so it never
|
|
178
|
+
* interacts with the `deviceMeta` write lock). */
|
|
179
|
+
roleDisplayDefaults?: Record<string, RoleDisplayDefault>;
|
|
169
180
|
}
|
|
@@ -47,6 +47,7 @@ export declare function isDeviceRole(value: string): value is DeviceRole;
|
|
|
47
47
|
* never leaks an off-enum string onto the wire shape. */
|
|
48
48
|
export declare function toDeviceRole(value: string | null | undefined): DeviceRole | null;
|
|
49
49
|
export declare function toDeviceInfo(addonId: string, device: IDevice, metadata?: Record<string, unknown> | null, metaRow?: PersistedDeviceMeta | null): {
|
|
50
|
+
display?: import('@camstack/types').DeviceDisplayOverride | undefined;
|
|
50
51
|
deviceLinks?: import('@camstack/types').DeviceLinks | undefined;
|
|
51
52
|
childLayout?: import('@camstack/types').ChildLayout | undefined;
|
|
52
53
|
primaryChildEntityId?: string | null | undefined;
|
|
@@ -8,7 +8,29 @@ export declare class PlatformProbeNativeAddon extends BaseAddon {
|
|
|
8
8
|
private encoderProber;
|
|
9
9
|
private decodeAccelProber;
|
|
10
10
|
private cachedCaps;
|
|
11
|
+
/**
|
|
12
|
+
* Per-boot generation stamp for the manual readiness emissions below.
|
|
13
|
+
* Constant for the lifetime of this addon instance (== one process boot);
|
|
14
|
+
* consumer-side registries derive a monotonic epoch from generation
|
|
15
|
+
* transitions. Mirrors `BaseAddon._readinessGeneration` (private there).
|
|
16
|
+
*/
|
|
17
|
+
private readonly readinessGeneration;
|
|
11
18
|
constructor();
|
|
19
|
+
/**
|
|
20
|
+
* Manual readiness protocol. The provider registers synchronously from
|
|
21
|
+
* `onInitialize`, but the REAL hardware + EP scoring is the async
|
|
22
|
+
* `probePromise` (embedded-Python install included). The BaseAddon
|
|
23
|
+
* auto-emit would flip `ready` at registration time — BEFORE any
|
|
24
|
+
* accelerator is visible — so probe-gated consumers (detection-pipeline
|
|
25
|
+
* engine auto-pick) would read accelerator-blind results and stick on
|
|
26
|
+
* onnx-CPU. Instead: `starting` at init, the single authoritative
|
|
27
|
+
* `ready` once the probe resolves, `down` on shutdown.
|
|
28
|
+
*/
|
|
29
|
+
protected get autoEmitReadiness(): boolean;
|
|
30
|
+
/** Bare cluster node id — readiness is scoped `{type:'node', nodeId}`. */
|
|
31
|
+
private bareLocalNodeId;
|
|
32
|
+
/** Emit a `system.ready-state` transition for the platform-probe cap. */
|
|
33
|
+
private emitProbeReadiness;
|
|
12
34
|
/**
|
|
13
35
|
* Resolve the ffmpeg binary the encoder probe should test, from the cluster
|
|
14
36
|
* `ffmpeg` config section (`binaryPath`). The probe MUST exercise the same
|
|
@@ -594,15 +594,86 @@ var HardwareDecodeAccelProber = class {
|
|
|
594
594
|
};
|
|
595
595
|
//#endregion
|
|
596
596
|
//#region src/builtins/platform-probe/index.ts
|
|
597
|
+
/**
|
|
598
|
+
* The decode-hwaccel backends `ctx.kernel.hwaccel.resolve` accepts. The cap
|
|
599
|
+
* input enum is WIDER (it also carries EP-only names — coreml/openvino/… —
|
|
600
|
+
* shared with other probe surfaces), so the provider param must stay
|
|
601
|
+
* `string`-typed to satisfy the `InferProvider` contract. We narrow it here
|
|
602
|
+
* with a type guard instead of a cast: any value that is not a known decode
|
|
603
|
+
* backend (or the `'none'` sentinel) resolves to `null` → auto-probe.
|
|
604
|
+
*/
|
|
605
|
+
var HWACCEL_DECODE_BACKENDS = [
|
|
606
|
+
"videotoolbox",
|
|
607
|
+
"cuda",
|
|
608
|
+
"nvdec",
|
|
609
|
+
"vaapi",
|
|
610
|
+
"qsv",
|
|
611
|
+
"d3d11va",
|
|
612
|
+
"dxva2",
|
|
613
|
+
"amf",
|
|
614
|
+
"vdpau",
|
|
615
|
+
"drm"
|
|
616
|
+
];
|
|
617
|
+
function isHwAccelBackend(value) {
|
|
618
|
+
return HWACCEL_DECODE_BACKENDS.includes(value);
|
|
619
|
+
}
|
|
620
|
+
function narrowHwAccelPrefer(value) {
|
|
621
|
+
if (value === "none") return "none";
|
|
622
|
+
if (typeof value === "string" && isHwAccelBackend(value)) return value;
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
597
625
|
var PlatformProbeNativeAddon = class extends _camstack_types.BaseAddon {
|
|
598
626
|
scorer = null;
|
|
599
627
|
encoderProber = null;
|
|
600
628
|
decodeAccelProber = null;
|
|
601
629
|
cachedCaps = null;
|
|
630
|
+
/**
|
|
631
|
+
* Per-boot generation stamp for the manual readiness emissions below.
|
|
632
|
+
* Constant for the lifetime of this addon instance (== one process boot);
|
|
633
|
+
* consumer-side registries derive a monotonic epoch from generation
|
|
634
|
+
* transitions. Mirrors `BaseAddon._readinessGeneration` (private there).
|
|
635
|
+
*/
|
|
636
|
+
readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
|
|
602
637
|
constructor() {
|
|
603
638
|
super({});
|
|
604
639
|
}
|
|
605
640
|
/**
|
|
641
|
+
* Manual readiness protocol. The provider registers synchronously from
|
|
642
|
+
* `onInitialize`, but the REAL hardware + EP scoring is the async
|
|
643
|
+
* `probePromise` (embedded-Python install included). The BaseAddon
|
|
644
|
+
* auto-emit would flip `ready` at registration time — BEFORE any
|
|
645
|
+
* accelerator is visible — so probe-gated consumers (detection-pipeline
|
|
646
|
+
* engine auto-pick) would read accelerator-blind results and stick on
|
|
647
|
+
* onnx-CPU. Instead: `starting` at init, the single authoritative
|
|
648
|
+
* `ready` once the probe resolves, `down` on shutdown.
|
|
649
|
+
*/
|
|
650
|
+
get autoEmitReadiness() {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
/** Bare cluster node id — readiness is scoped `{type:'node', nodeId}`. */
|
|
654
|
+
bareLocalNodeId() {
|
|
655
|
+
const raw = this.ctx.kernel?.localNodeId ?? "hub";
|
|
656
|
+
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
657
|
+
}
|
|
658
|
+
/** Emit a `system.ready-state` transition for the platform-probe cap. */
|
|
659
|
+
emitProbeReadiness(state) {
|
|
660
|
+
const ctx = this.ctxIfReady;
|
|
661
|
+
if (!ctx) return;
|
|
662
|
+
const nodeId = this.bareLocalNodeId();
|
|
663
|
+
try {
|
|
664
|
+
(0, _camstack_types.emitReadiness)(ctx.eventBus, {
|
|
665
|
+
capName: _camstack_types.platformProbeCapability.name,
|
|
666
|
+
scope: {
|
|
667
|
+
type: "node",
|
|
668
|
+
nodeId
|
|
669
|
+
},
|
|
670
|
+
state,
|
|
671
|
+
generation: this.readinessGeneration,
|
|
672
|
+
sourceNodeId: nodeId
|
|
673
|
+
});
|
|
674
|
+
} catch {}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
606
677
|
* Resolve the ffmpeg binary the encoder probe should test, from the cluster
|
|
607
678
|
* `ffmpeg` config section (`binaryPath`). The probe MUST exercise the same
|
|
608
679
|
* binary the broker/recorder spawn, or it may report encoders for a different
|
|
@@ -617,6 +688,7 @@ var PlatformProbeNativeAddon = class extends _camstack_types.BaseAddon {
|
|
|
617
688
|
}
|
|
618
689
|
}
|
|
619
690
|
async onInitialize() {
|
|
691
|
+
this.emitProbeReadiness("starting");
|
|
620
692
|
const embeddedPython = await this.ctx.deps.ensurePython().catch((err) => {
|
|
621
693
|
this.ctx.logger.debug("ensurePython unavailable for platform probe", { meta: { error: (0, _camstack_types.errMsg)(err) } });
|
|
622
694
|
return null;
|
|
@@ -651,6 +723,7 @@ var PlatformProbeNativeAddon = class extends _camstack_types.BaseAddon {
|
|
|
651
723
|
bestReason: caps.bestScore.reason,
|
|
652
724
|
bestScore: caps.bestScore.score
|
|
653
725
|
} });
|
|
726
|
+
this.emitProbeReadiness("ready");
|
|
654
727
|
return caps;
|
|
655
728
|
}).catch((err) => {
|
|
656
729
|
const msg = (0, _camstack_types.errMsg)(err);
|
|
@@ -676,8 +749,15 @@ var PlatformProbeNativeAddon = class extends _camstack_types.BaseAddon {
|
|
|
676
749
|
},
|
|
677
750
|
resolveHwAccel: async (input) => {
|
|
678
751
|
const hwaccel = this.ctx.kernel.hwaccel;
|
|
679
|
-
if (!hwaccel) return {
|
|
680
|
-
|
|
752
|
+
if (!hwaccel) return {
|
|
753
|
+
preferred: [],
|
|
754
|
+
rationale: "kernel hwaccel unavailable"
|
|
755
|
+
};
|
|
756
|
+
const res = await hwaccel.resolve(narrowHwAccelPrefer(input.prefer));
|
|
757
|
+
return {
|
|
758
|
+
preferred: res.preferred,
|
|
759
|
+
rationale: res.rationale
|
|
760
|
+
};
|
|
681
761
|
},
|
|
682
762
|
getHardwareEncoders: async () => {
|
|
683
763
|
const prober = this.encoderProber;
|
|
@@ -707,6 +787,7 @@ var PlatformProbeNativeAddon = class extends _camstack_types.BaseAddon {
|
|
|
707
787
|
}];
|
|
708
788
|
}
|
|
709
789
|
async onShutdown() {
|
|
790
|
+
this.emitProbeReadiness("down");
|
|
710
791
|
this.scorer = null;
|
|
711
792
|
this.encoderProber = null;
|
|
712
793
|
this.decodeAccelProber = null;
|