@sakibcoolz/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -7
- package/dist/acquisition.d.ts +29 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +30 -0
- package/dist/acquisition.js.map +1 -0
- package/dist/assembly.d.ts +5 -2
- package/dist/assembly.d.ts.map +1 -1
- package/dist/assembly.js +7 -4
- package/dist/assembly.js.map +1 -1
- package/dist/attach.d.ts +4 -2
- package/dist/attach.d.ts.map +1 -1
- package/dist/attach.js +5 -3
- package/dist/attach.js.map +1 -1
- package/dist/clipboard.d.ts +6 -4
- package/dist/clipboard.d.ts.map +1 -1
- package/dist/clipboard.js +25 -4
- package/dist/clipboard.js.map +1 -1
- package/dist/collision.d.ts +2 -2
- package/dist/collision.js.map +1 -1
- package/dist/digest.d.ts +9 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +68 -0
- package/dist/digest.js.map +1 -0
- package/dist/flow.d.ts +3 -2
- package/dist/flow.d.ts.map +1 -1
- package/dist/flow.js.map +1 -1
- package/dist/graph.d.ts +67 -7
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +195 -10
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/observation.d.ts +1 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +4 -1
- package/dist/observation.js.map +1 -1
- package/dist/routing.d.ts +6 -3
- package/dist/routing.d.ts.map +1 -1
- package/dist/routing.js +12 -2
- package/dist/routing.js.map +1 -1
- package/dist/scene.d.ts +245 -5
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +94 -3
- package/dist/scene.js.map +1 -1
- package/dist/serialization.d.ts +7 -7
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +120 -8
- package/dist/serialization.js.map +1 -1
- package/dist/snapping.d.ts +2 -2
- package/dist/snapping.d.ts.map +1 -1
- package/dist/snapping.js +10 -7
- package/dist/snapping.js.map +1 -1
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +114 -0
- package/dist/status.js.map +1 -0
- package/dist/tags.d.ts +52 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +103 -0
- package/dist/tags.js.map +1 -0
- package/dist/telemetry.d.ts +42 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +84 -8
- package/dist/telemetry.js.map +1 -1
- package/dist/touching.d.ts +2 -2
- package/dist/touching.d.ts.map +1 -1
- package/dist/touching.js +7 -4
- package/dist/touching.js.map +1 -1
- package/dist/units.js +1 -1
- package/dist/units.js.map +1 -1
- package/dist/upgrade.d.ts +2 -2
- package/dist/upgrade.js.map +1 -1
- package/dist/validation.d.ts +33 -6
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +806 -39
- package/dist/validation.js.map +1 -1
- package/package.json +4 -2
- package/schema/fixtures/invalid/association-without-relationship.scene.json +69 -0
- package/schema/fixtures/invalid/binding-kind.scene.json +14 -0
- package/schema/fixtures/invalid/command-zero-step.scene.json +105 -0
- package/schema/fixtures/invalid/empty-name.scene.json +30 -0
- package/schema/fixtures/invalid/expected.json +70 -0
- package/schema/fixtures/invalid/extension-not-namespaced.scene.json +10 -0
- package/schema/fixtures/invalid/extension-prototype-key.scene.json +12 -0
- package/schema/fixtures/invalid/missing-document-type.scene.json +6 -0
- package/schema/fixtures/invalid/non-string-label.scene.json +32 -0
- package/schema/fixtures/invalid/underlay-opacity.scene.json +17 -0
- package/schema/fixtures/invalid/unknown-component-kind.scene.json +29 -0
- package/schema/fixtures/invalid/unknown-relationship.scene.json +63 -0
- package/schema/fixtures/invalid/unknown-root-field.scene.json +8 -0
- package/schema/fixtures/invalid/unsupported-version.scene.json +7 -0
- package/schema/fixtures/invalid/view-rotation.scene.json +139 -0
- package/schema/fixtures/invalid/writable-string.scene.json +93 -0
- package/schema/fixtures/invalid/writable-without-limits.scene.json +100 -0
- package/schema/fixtures/invalid/zero-scale.scene.json +29 -0
- package/schema/fixtures/migration/README.md +18 -0
- package/schema/fixtures/migration/v1-demo.scene.json +160 -0
- package/schema/fixtures/migration/v1-demo.v4.json +198 -0
- package/schema/fixtures/migration/v2-demo.scene.json +244 -0
- package/schema/fixtures/migration/v2-demo.v4.json +245 -0
- package/schema/fixtures/migration/v2-generated-12.scene.json +473 -0
- package/schema/fixtures/migration/v2-generated-12.v4.json +474 -0
- package/schema/fixtures/migration/v3-demo.scene.json +244 -0
- package/schema/fixtures/migration/v3-demo.v4.json +245 -0
- package/schema/fixtures/migration/v3-generated-12.scene.json +473 -0
- package/schema/fixtures/migration/v3-generated-12.v4.json +474 -0
- package/schema/fixtures/migration/v3-mostly-connected-plant.scene.json +386 -0
- package/schema/fixtures/migration/v3-mostly-connected-plant.v4.json +387 -0
- package/schema/fixtures/migration/v3-open-joints-plant.scene.json +288 -0
- package/schema/fixtures/migration/v3-open-joints-plant.v4.json +289 -0
- package/schema/fixtures/migration/v3-unconnected-plant.scene.json +14 -0
- package/schema/fixtures/migration/v3-unconnected-plant.v4.json +276 -0
- package/schema/fixtures/projection/acquisition.plan.json +50 -0
- package/schema/fixtures/projection/control-line.plan.json +86 -0
- package/schema/fixtures/valid/acquisition.scene.json +117 -0
- package/schema/fixtures/valid/associations.scene.json +79 -0
- package/schema/fixtures/valid/commands.scene.json +155 -0
- package/schema/fixtures/valid/control-line.scene.json +210 -0
- package/schema/fixtures/valid/empty.scene.json +7 -0
- package/schema/fixtures/valid/identity-and-extensions.scene.json +96 -0
- package/schema/fixtures/valid/tank-valve-line.scene.json +89 -0
- package/schema/fixtures/valid/views.scene.json +139 -0
- package/schema/fixtures/validator-only/association-id-reuses-connection.scene.json +70 -0
- package/schema/fixtures/validator-only/association-unknown-component.scene.json +70 -0
- package/schema/fixtures/validator-only/command-max-below-min.scene.json +104 -0
- package/schema/fixtures/validator-only/command-out-of-type-range.scene.json +97 -0
- package/schema/fixtures/validator-only/connection-domain-mismatch.scene.json +63 -0
- package/schema/fixtures/validator-only/credential-in-connection.scene.json +66 -0
- package/schema/fixtures/validator-only/credential-in-extension.scene.json +14 -0
- package/schema/fixtures/validator-only/credential-label.scene.json +32 -0
- package/schema/fixtures/validator-only/duplicate-component-id.scene.json +50 -0
- package/schema/fixtures/validator-only/endpoint-with-credentials.scene.json +74 -0
- package/schema/fixtures/validator-only/expected.json +66 -0
- package/schema/fixtures/validator-only/modbus-coil-float.scene.json +100 -0
- package/schema/fixtures/validator-only/non-unit-rotation.scene.json +29 -0
- package/schema/fixtures/validator-only/unknown-data-source.scene.json +100 -0
- package/schema/fixtures/validator-only/unknown-port.scene.json +62 -0
- package/schema/fixtures/validator-only/unregistered-host-type.scene.json +29 -0
- package/schema/fixtures/validator-only/view-unknown-component.scene.json +143 -0
- package/schema/industrial-scene.v4.schema.json +365 -0
package/dist/tags.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { validateScene } from './validation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Proposes runtime variables and bindings from the component definitions' variable templates
|
|
4
|
+
* (ADR-0024): tag id `<componentId>.<name>`, read-only, no source (the host assigns addresses).
|
|
5
|
+
* Never guesses from names. Tags that already exist, and properties that are already bound, are
|
|
6
|
+
* left alone, so suggesting twice proposes nothing new.
|
|
7
|
+
*/
|
|
8
|
+
export function suggestVariables(scene, options) {
|
|
9
|
+
const existing = new Set((scene.runtimeVariables ?? []).map((v) => v.id));
|
|
10
|
+
const bindingIds = new Set((scene.tagBindings ?? []).map((b) => b.id));
|
|
11
|
+
const bound = new Set((scene.tagBindings ?? []).flatMap((b) => (b.kind === 'property' ? [`${b.instanceId}\u0000${b.property}`] : [])));
|
|
12
|
+
const variables = [];
|
|
13
|
+
const bindings = [];
|
|
14
|
+
for (const c of scene.components) {
|
|
15
|
+
const bindable = options.resolveBindable?.(c.type);
|
|
16
|
+
for (const t of options.resolveVariables(c.type, c.properties) ?? []) {
|
|
17
|
+
const id = `${c.id}.${t.name}`;
|
|
18
|
+
if (existing.has(id))
|
|
19
|
+
continue;
|
|
20
|
+
existing.add(id);
|
|
21
|
+
variables.push({ id, componentId: c.id, dataType: t.dataType, ...(t.unit !== undefined ? { unit: t.unit } : {}), access: 'read' });
|
|
22
|
+
const target = t.property !== undefined ? `${c.id}\u0000${t.property}` : undefined;
|
|
23
|
+
const bindingId = `bind:${id}`;
|
|
24
|
+
if (!target || !bindable?.[t.property] || bound.has(target) || bindingIds.has(bindingId))
|
|
25
|
+
continue;
|
|
26
|
+
bound.add(target);
|
|
27
|
+
bindingIds.add(bindingId);
|
|
28
|
+
bindings.push({ id: bindingId, tagId: id, kind: 'property', instanceId: c.id, property: t.property });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { variables, bindings };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Adds the confirmed suggestions to the scene and validates the result, so a confirmed proposal can
|
|
35
|
+
* never leave an invalid document. The input is not changed.
|
|
36
|
+
*/
|
|
37
|
+
export function applyTagSuggestions(scene, picked, options) {
|
|
38
|
+
const candidate = {
|
|
39
|
+
...scene,
|
|
40
|
+
...(picked.variables.length > 0 || scene.runtimeVariables ? { runtimeVariables: [...(scene.runtimeVariables ?? []), ...picked.variables] } : {}),
|
|
41
|
+
...(picked.bindings.length > 0 || scene.tagBindings ? { tagBindings: [...(scene.tagBindings ?? []), ...picked.bindings] } : {}),
|
|
42
|
+
};
|
|
43
|
+
const result = validateScene(candidate, options);
|
|
44
|
+
return result.ok ? { ok: true, scene: result.scene } : { ok: false, errors: result.errors };
|
|
45
|
+
}
|
|
46
|
+
const REGISTERS = { bool: 1, int16: 1, uint16: 1, int32: 2, uint32: 2, float32: 2, float64: 4 };
|
|
47
|
+
/**
|
|
48
|
+
* Reviews a validated scene's tags (ADR-0024) and returns findings, in this order:
|
|
49
|
+
* - components with bindable properties and no binding (needs `resolveBindable`);
|
|
50
|
+
* - runtime variables no binding uses;
|
|
51
|
+
* - bindings to tags with no runtime variable, when the scene defines any;
|
|
52
|
+
* - variables on one Modbus source and unit whose register spans overlap (bits of one register may share it).
|
|
53
|
+
* Advisory only: `validateScene` stays the rule for what a document may contain.
|
|
54
|
+
*/
|
|
55
|
+
export function reviewTags(scene, options = {}) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
const bindings = scene.tagBindings ?? [];
|
|
58
|
+
const variables = scene.runtimeVariables ?? [];
|
|
59
|
+
const boundComponents = new Set(bindings.flatMap((b) => (b.kind === 'property' ? [b.instanceId] : [])));
|
|
60
|
+
if (options.resolveBindable) {
|
|
61
|
+
scene.components.forEach((c, i) => {
|
|
62
|
+
const names = Object.keys(options.resolveBindable(c.type) ?? {});
|
|
63
|
+
if (names.length > 0 && !boundComponents.has(c.id)) {
|
|
64
|
+
findings.push({ code: 'UNBOUND_COMPONENT', path: `components[${i}]`, message: `"${c.id}" has bindable ${names.join(', ')} but no tag drives it.` });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const usedTags = new Set(bindings.map((b) => b.tagId));
|
|
69
|
+
variables.forEach((v, i) => {
|
|
70
|
+
if (!usedTags.has(v.id))
|
|
71
|
+
findings.push({ code: 'UNUSED_VARIABLE', path: `runtimeVariables[${i}]`, message: `No binding uses tag "${v.id}".` });
|
|
72
|
+
});
|
|
73
|
+
if (variables.length > 0) {
|
|
74
|
+
const described = new Set(variables.map((v) => v.id));
|
|
75
|
+
bindings.forEach((b, i) => {
|
|
76
|
+
if (!described.has(b.tagId))
|
|
77
|
+
findings.push({ code: 'UNDESCRIBED_TAG', path: `tagBindings[${i}].tagId`, message: `Tag "${b.tagId}" has no runtime variable.` });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const protocols = new Map((scene.dataSources ?? []).map((s) => [s.id, s.protocol]));
|
|
81
|
+
const spans = [];
|
|
82
|
+
variables.forEach((v, index) => {
|
|
83
|
+
const protocol = v.source && protocols.get(v.source.dataSourceId);
|
|
84
|
+
const a = v.source?.address;
|
|
85
|
+
if ((protocol !== 'modbus-tcp' && protocol !== 'modbus-rtu') || !a || typeof a.address !== 'number')
|
|
86
|
+
return;
|
|
87
|
+
const count = REGISTERS[v.dataType] ?? 1;
|
|
88
|
+
spans.push({ index, id: v.id, key: JSON.stringify([v.source.dataSourceId, a.unitId, a.area]), from: a.address, to: a.address + count - 1, ...(a.bit !== undefined ? { bit: a.bit } : {}) });
|
|
89
|
+
});
|
|
90
|
+
for (let i = 0; i < spans.length; i++) {
|
|
91
|
+
for (let j = 0; j < i; j++) {
|
|
92
|
+
const [a, b] = [spans[j], spans[i]];
|
|
93
|
+
if (a.key !== b.key || a.to < b.from || b.to < a.from)
|
|
94
|
+
continue;
|
|
95
|
+
if (a.bit !== undefined && b.bit !== undefined && a.bit !== b.bit)
|
|
96
|
+
continue;
|
|
97
|
+
findings.push({ code: 'OVERLAPPING_REGISTERS', path: `runtimeVariables[${b.index}].source.address`, message: `Tag "${b.id}" overlaps tag "${a.id}" on the same Modbus source, unit and area.` });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=tags.js.map
|
package/dist/tags.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tags.js","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentInstance, ComponentType, Quantity, RuntimeVariable, ScadaScene, TagBinding, VariableTemplate } from './scene.js';\nimport { validateScene, type SceneValidationError, type SceneValidationOptions } from './validation.js';\n\ntype Resolvers = {\n /** Bindable properties per type, as for `validateScene`. */\n resolveBindable?: (type: ComponentType) => Readonly<Record<string, Quantity>> | undefined;\n};\n\nexport interface SuggestOptions extends Resolvers {\n /** Variable templates per type; a registry's `resolvers.resolveVariables`. */\n resolveVariables: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly VariableTemplate[] | undefined;\n}\n\n/** Proposed tags and bindings. Nothing is applied: a host or the editor applies the ones the user confirms. */\nexport interface TagSuggestions {\n variables: RuntimeVariable[];\n bindings: TagBinding[];\n}\n\n/**\n * Proposes runtime variables and bindings from the component definitions' variable templates\n * (ADR-0024): tag id `<componentId>.<name>`, read-only, no source (the host assigns addresses).\n * Never guesses from names. Tags that already exist, and properties that are already bound, are\n * left alone, so suggesting twice proposes nothing new.\n */\nexport function suggestVariables(scene: ScadaScene, options: SuggestOptions): TagSuggestions {\n const existing = new Set((scene.runtimeVariables ?? []).map((v) => v.id));\n const bindingIds = new Set((scene.tagBindings ?? []).map((b) => b.id));\n const bound = new Set((scene.tagBindings ?? []).flatMap((b) => (b.kind === 'property' ? [`${b.instanceId}\\u0000${b.property}`] : [])));\n const variables: RuntimeVariable[] = [];\n const bindings: TagBinding[] = [];\n for (const c of scene.components) {\n const bindable = options.resolveBindable?.(c.type);\n for (const t of options.resolveVariables(c.type, c.properties) ?? []) {\n const id = `${c.id}.${t.name}`;\n if (existing.has(id)) continue;\n existing.add(id);\n variables.push({ id, componentId: c.id, dataType: t.dataType, ...(t.unit !== undefined ? { unit: t.unit } : {}), access: 'read' });\n const target = t.property !== undefined ? `${c.id}\\u0000${t.property}` : undefined;\n const bindingId = `bind:${id}`;\n if (!target || !bindable?.[t.property!] || bound.has(target) || bindingIds.has(bindingId)) continue;\n bound.add(target);\n bindingIds.add(bindingId);\n bindings.push({ id: bindingId, tagId: id, kind: 'property', instanceId: c.id, property: t.property! });\n }\n }\n return { variables, bindings };\n}\n\nexport type ApplyResult = { ok: true; scene: ScadaScene } | { ok: false; errors: SceneValidationError[] };\n\n/**\n * Adds the confirmed suggestions to the scene and validates the result, so a confirmed proposal can\n * never leave an invalid document. The input is not changed.\n */\nexport function applyTagSuggestions(scene: ScadaScene, picked: TagSuggestions, options: SceneValidationOptions): ApplyResult {\n const candidate: ScadaScene = {\n ...scene,\n ...(picked.variables.length > 0 || scene.runtimeVariables ? { runtimeVariables: [...(scene.runtimeVariables ?? []), ...picked.variables] } : {}),\n ...(picked.bindings.length > 0 || scene.tagBindings ? { tagBindings: [...(scene.tagBindings ?? []), ...picked.bindings] } : {}),\n };\n const result = validateScene(candidate, options);\n return result.ok ? { ok: true, scene: result.scene } : { ok: false, errors: result.errors };\n}\n\nexport type TagFindingCode = 'UNBOUND_COMPONENT' | 'UNUSED_VARIABLE' | 'UNDESCRIBED_TAG' | 'OVERLAPPING_REGISTERS';\n\n/** Something worth a look; never an error. `path` points into the scene, as validation errors do. */\nexport interface TagFinding {\n code: TagFindingCode;\n path: string;\n message: string;\n}\n\nconst REGISTERS: Partial<Record<RuntimeVariable['dataType'], number>> = { bool: 1, int16: 1, uint16: 1, int32: 2, uint32: 2, float32: 2, float64: 4 };\n\n/**\n * Reviews a validated scene's tags (ADR-0024) and returns findings, in this order:\n * - components with bindable properties and no binding (needs `resolveBindable`);\n * - runtime variables no binding uses;\n * - bindings to tags with no runtime variable, when the scene defines any;\n * - variables on one Modbus source and unit whose register spans overlap (bits of one register may share it).\n * Advisory only: `validateScene` stays the rule for what a document may contain.\n */\nexport function reviewTags(scene: ScadaScene, options: Resolvers = {}): TagFinding[] {\n const findings: TagFinding[] = [];\n const bindings = scene.tagBindings ?? [];\n const variables = scene.runtimeVariables ?? [];\n const boundComponents = new Set(bindings.flatMap((b) => (b.kind === 'property' ? [b.instanceId] : [])));\n if (options.resolveBindable) {\n scene.components.forEach((c, i) => {\n const names = Object.keys(options.resolveBindable!(c.type) ?? {});\n if (names.length > 0 && !boundComponents.has(c.id)) {\n findings.push({ code: 'UNBOUND_COMPONENT', path: `components[${i}]`, message: `\"${c.id}\" has bindable ${names.join(', ')} but no tag drives it.` });\n }\n });\n }\n const usedTags = new Set(bindings.map((b) => b.tagId));\n variables.forEach((v, i) => {\n if (!usedTags.has(v.id)) findings.push({ code: 'UNUSED_VARIABLE', path: `runtimeVariables[${i}]`, message: `No binding uses tag \"${v.id}\".` });\n });\n if (variables.length > 0) {\n const described = new Set(variables.map((v) => v.id));\n bindings.forEach((b, i) => {\n if (!described.has(b.tagId)) findings.push({ code: 'UNDESCRIBED_TAG', path: `tagBindings[${i}].tagId`, message: `Tag \"${b.tagId}\" has no runtime variable.` });\n });\n }\n const protocols = new Map((scene.dataSources ?? []).map((s) => [s.id, s.protocol]));\n type Span = { index: number; id: string; key: string; from: number; to: number; bit?: number };\n const spans: Span[] = [];\n variables.forEach((v, index) => {\n const protocol = v.source && protocols.get(v.source.dataSourceId);\n const a = v.source?.address as { area?: string; address?: number; unitId?: number; bit?: number } | undefined;\n if ((protocol !== 'modbus-tcp' && protocol !== 'modbus-rtu') || !a || typeof a.address !== 'number') return;\n const count = REGISTERS[v.dataType] ?? 1;\n spans.push({ index, id: v.id, key: JSON.stringify([v.source!.dataSourceId, a.unitId, a.area]), from: a.address, to: a.address + count - 1, ...(a.bit !== undefined ? { bit: a.bit } : {}) });\n });\n for (let i = 0; i < spans.length; i++) {\n for (let j = 0; j < i; j++) {\n const [a, b] = [spans[j], spans[i]];\n if (a.key !== b.key || a.to < b.from || b.to < a.from) continue;\n if (a.bit !== undefined && b.bit !== undefined && a.bit !== b.bit) continue;\n findings.push({ code: 'OVERLAPPING_REGISTERS', path: `runtimeVariables[${b.index}].source.address`, message: `Tag \"${b.id}\" overlaps tag \"${a.id}\" on the same Modbus source, unit and area.` });\n break;\n }\n }\n return findings;\n}\n\n"]}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FlowReading, Quality, ValueSource } from './flow.js';
|
|
2
2
|
import { type FreshnessOptions } from './observation.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type ComponentType, type DataType, type Quantity, type ScadaScene } from './scene.js';
|
|
4
4
|
/** One value for a host tag. Read-only: the SDK never writes tags (ADR-0003). */
|
|
5
5
|
export interface Observation {
|
|
6
6
|
tagId: string;
|
|
@@ -10,10 +10,15 @@ export interface Observation {
|
|
|
10
10
|
sourceTimestamp: string;
|
|
11
11
|
/** Default MEASURED. */
|
|
12
12
|
source?: ValueSource;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The value's data type as the gateway reports it (ADR-0029). When the tag's runtime variable
|
|
15
|
+
* declares another type, the value is unknown (TYPE_MISMATCH).
|
|
16
|
+
*/
|
|
17
|
+
dataType?: DataType;
|
|
18
|
+
/** Unit of `value`; when absent the binding's unit, then the runtime variable's unit, then the canonical unit, is assumed. */
|
|
14
19
|
unit?: string;
|
|
15
20
|
}
|
|
16
|
-
export type TelemetryUnknownReason = 'NO_READING' | 'BAD_QUALITY' | 'STALE' | 'INVALID_TIMESTAMP' | 'INVALID_VALUE' | 'INVALID_UNIT';
|
|
21
|
+
export type TelemetryUnknownReason = 'NO_READING' | 'BAD_QUALITY' | 'DISCONNECTED' | 'STALE' | 'INVALID_TIMESTAMP' | 'INVALID_VALUE' | 'INVALID_UNIT' | 'TYPE_MISMATCH';
|
|
17
22
|
type Target = {
|
|
18
23
|
bindingId: string;
|
|
19
24
|
instanceId: string;
|
|
@@ -40,7 +45,7 @@ export interface TelemetryOptions extends FreshnessOptions {
|
|
|
40
45
|
/** Which observations to use; measured and simulated are never mixed. Default MEASURED. */
|
|
41
46
|
source?: ValueSource;
|
|
42
47
|
/** Quantities of bindable properties. Without it, numbers and booleans pass through unconverted. */
|
|
43
|
-
resolveBindable?: (type:
|
|
48
|
+
resolveBindable?: (type: ComponentType) => Readonly<Record<string, Quantity>> | undefined;
|
|
44
49
|
}
|
|
45
50
|
export interface TelemetryResolution {
|
|
46
51
|
/** One state per property binding, in binding order. */
|
|
@@ -50,6 +55,39 @@ export interface TelemetryResolution {
|
|
|
50
55
|
/** Observed tag ids that no binding uses, in first-seen order. */
|
|
51
56
|
unmatchedTagIds: string[];
|
|
52
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether `value` fits a tag's data type (ADR-0029): a boolean for `bool`, an integer in range for the
|
|
60
|
+
* integer types, a finite number for the float types, a string for `string`. A `scaled` variable
|
|
61
|
+
* carries the host's engineering value, not the raw register, so any finite number fits.
|
|
62
|
+
*/
|
|
63
|
+
export declare function valueFitsDataType(value: unknown, dataType: DataType, scaled?: boolean): boolean;
|
|
64
|
+
/** The current value of one runtime variable, as the host sent it (its own unit; no conversion). */
|
|
65
|
+
export type VariableState = {
|
|
66
|
+
tagId: string;
|
|
67
|
+
status: 'ok';
|
|
68
|
+
value: number | boolean | string;
|
|
69
|
+
quality: Quality;
|
|
70
|
+
source: ValueSource;
|
|
71
|
+
sourceTimestamp: string;
|
|
72
|
+
uncertain: boolean;
|
|
73
|
+
reason?: undefined;
|
|
74
|
+
} | {
|
|
75
|
+
tagId: string;
|
|
76
|
+
status: 'unknown';
|
|
77
|
+
reason: TelemetryUnknownReason;
|
|
78
|
+
quality?: Quality;
|
|
79
|
+
source?: ValueSource;
|
|
80
|
+
sourceTimestamp?: string;
|
|
81
|
+
value?: undefined;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The latest value of every runtime variable, bound or not, with the same trust rules as bindings:
|
|
85
|
+
* type, quality and freshness (ADR-0029). The runtime shows it beside a writable variable, so an
|
|
86
|
+
* operator sees the state a request would change. Pure.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveVariableValues(scene: ScadaScene, observations: readonly Observation[], options: FreshnessOptions & {
|
|
89
|
+
source?: ValueSource;
|
|
90
|
+
}): Map<string, VariableState>;
|
|
53
91
|
/**
|
|
54
92
|
* Applies host observations to the scene's tag bindings. Pure: `now` is passed in and nothing is
|
|
55
93
|
* written back. Property bindings resolve to a trusted value or an unknown reason; flow bindings
|
package/dist/telemetry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
package/dist/telemetry.js
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
import { assessObservation, isNewer } from './observation.js';
|
|
2
|
+
import { dataTypes, ownValue } from './scene.js';
|
|
2
3
|
import { isKnownUnit, toCanonical } from './units.js';
|
|
4
|
+
const integerRanges = {
|
|
5
|
+
int16: [-32768, 32767],
|
|
6
|
+
uint16: [0, 65535],
|
|
7
|
+
int32: [-2147483648, 2147483647],
|
|
8
|
+
uint32: [0, 4294967295],
|
|
9
|
+
};
|
|
10
|
+
const FLOAT32_MAX = 3.4028234663852886e38;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `value` fits a tag's data type (ADR-0029): a boolean for `bool`, an integer in range for the
|
|
13
|
+
* integer types, a finite number for the float types, a string for `string`. A `scaled` variable
|
|
14
|
+
* carries the host's engineering value, not the raw register, so any finite number fits.
|
|
15
|
+
*/
|
|
16
|
+
export function valueFitsDataType(value, dataType, scaled = false) {
|
|
17
|
+
if (dataType === 'bool')
|
|
18
|
+
return typeof value === 'boolean';
|
|
19
|
+
if (dataType === 'string')
|
|
20
|
+
return typeof value === 'string';
|
|
21
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
22
|
+
return false;
|
|
23
|
+
if (scaled || dataType === 'float64')
|
|
24
|
+
return true;
|
|
25
|
+
if (dataType === 'float32')
|
|
26
|
+
return Math.abs(value) <= FLOAT32_MAX;
|
|
27
|
+
const range = integerRanges[dataType];
|
|
28
|
+
return !!range && Number.isInteger(value) && value >= range[0] && value <= range[1];
|
|
29
|
+
}
|
|
30
|
+
/** The declared type wins; a gateway's stated type must match it, and the value must fit (ADR-0029). */
|
|
31
|
+
function typeProblemOf(o, variable) {
|
|
32
|
+
if (o.dataType !== undefined && (!dataTypes.includes(o.dataType) || (variable && o.dataType !== variable.dataType)))
|
|
33
|
+
return 'TYPE_MISMATCH';
|
|
34
|
+
const declared = variable?.dataType ?? o.dataType;
|
|
35
|
+
return declared && !valueFitsDataType(o.value, declared, !!variable?.scaling) ? 'INVALID_VALUE' : undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The latest value of every runtime variable, bound or not, with the same trust rules as bindings:
|
|
39
|
+
* type, quality and freshness (ADR-0029). The runtime shows it beside a writable variable, so an
|
|
40
|
+
* operator sees the state a request would change. Pure.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveVariableValues(scene, observations, options) {
|
|
43
|
+
const source = options.source ?? 'MEASURED';
|
|
44
|
+
const variables = new Map((scene.runtimeVariables ?? []).map((v) => [v.id, v]));
|
|
45
|
+
const latest = new Map();
|
|
46
|
+
for (const o of observations) {
|
|
47
|
+
// Host data is untrusted: skip entries that are not observations at all (T-096).
|
|
48
|
+
if (typeof o !== 'object' || o === null || !variables.has(o.tagId) || (o.source ?? 'MEASURED') !== source)
|
|
49
|
+
continue;
|
|
50
|
+
if (isNewer(o, latest.get(o.tagId)))
|
|
51
|
+
latest.set(o.tagId, o);
|
|
52
|
+
}
|
|
53
|
+
const states = new Map();
|
|
54
|
+
for (const [tagId, variable] of variables) {
|
|
55
|
+
const o = latest.get(tagId);
|
|
56
|
+
if (!o) {
|
|
57
|
+
states.set(tagId, { tagId, status: 'unknown', reason: 'NO_READING' });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const problem = typeProblemOf(o, variable) ?? assessObservation(o, options);
|
|
61
|
+
states.set(tagId, problem
|
|
62
|
+
? { tagId, status: 'unknown', reason: problem, quality: o.quality, source, sourceTimestamp: o.sourceTimestamp }
|
|
63
|
+
: { tagId, status: 'ok', value: o.value, quality: o.quality, source, sourceTimestamp: o.sourceTimestamp, uncertain: o.quality === 'UNCERTAIN' });
|
|
64
|
+
}
|
|
65
|
+
return states;
|
|
66
|
+
}
|
|
3
67
|
/** Value in the canonical unit, or the reason it cannot be used. */
|
|
4
68
|
function convert(value, quantity, unit) {
|
|
5
69
|
if (quantity === 'boolean' || (quantity === undefined && typeof value === 'boolean')) {
|
|
@@ -27,8 +91,11 @@ export function resolveTelemetry(scene, observations, options) {
|
|
|
27
91
|
const latest = new Map();
|
|
28
92
|
const unmatched = new Set();
|
|
29
93
|
for (const o of observations) {
|
|
94
|
+
if (typeof o !== 'object' || o === null)
|
|
95
|
+
continue;
|
|
30
96
|
if (!boundTags.has(o.tagId)) {
|
|
31
|
-
|
|
97
|
+
if (typeof o.tagId === 'string')
|
|
98
|
+
unmatched.add(o.tagId);
|
|
32
99
|
continue;
|
|
33
100
|
}
|
|
34
101
|
if ((o.source ?? 'MEASURED') !== source)
|
|
@@ -37,6 +104,10 @@ export function resolveTelemetry(scene, observations, options) {
|
|
|
37
104
|
latest.set(o.tagId, o);
|
|
38
105
|
}
|
|
39
106
|
const components = new Map(scene.components.map((c) => [c.id, c]));
|
|
107
|
+
// A runtime variable's unit applies when neither the observation nor the binding gives one (ADR-0024).
|
|
108
|
+
const variableUnits = new Map((scene.runtimeVariables ?? []).filter((v) => v.unit !== undefined).map((v) => [v.id, v.unit]));
|
|
109
|
+
const variables = new Map((scene.runtimeVariables ?? []).map((v) => [v.id, v]));
|
|
110
|
+
const typeProblem = (o) => typeProblemOf(o, variables.get(o.tagId));
|
|
40
111
|
const properties = [];
|
|
41
112
|
const flowReadings = [];
|
|
42
113
|
for (const binding of bindings) {
|
|
@@ -44,10 +115,10 @@ export function resolveTelemetry(scene, observations, options) {
|
|
|
44
115
|
if (binding.kind === 'flow') {
|
|
45
116
|
if (!o)
|
|
46
117
|
continue;
|
|
47
|
-
const converted = convert(o.value, 'flow', o.unit ?? binding.unit);
|
|
118
|
+
const converted = convert(o.value, 'flow', o.unit ?? binding.unit ?? variableUnits.get(binding.tagId));
|
|
48
119
|
flowReadings.push({
|
|
49
120
|
connectionId: binding.connectionId,
|
|
50
|
-
rate: 'value' in converted ? converted.value : Number.NaN,
|
|
121
|
+
rate: 'value' in converted && !typeProblem(o) ? converted.value : Number.NaN,
|
|
51
122
|
quality: o.quality,
|
|
52
123
|
source,
|
|
53
124
|
sourceTimestamp: o.sourceTimestamp,
|
|
@@ -61,9 +132,9 @@ export function resolveTelemetry(scene, observations, options) {
|
|
|
61
132
|
}
|
|
62
133
|
const meta = { quality: o.quality, source, sourceTimestamp: o.sourceTimestamp };
|
|
63
134
|
const owner = components.get(binding.instanceId);
|
|
64
|
-
const quantity = owner ? options.resolveBindable?.(owner.type)
|
|
65
|
-
const converted = convert(o.value, quantity, o.unit ?? binding.unit);
|
|
66
|
-
const problem = 'reason' in converted ? converted.reason : assessObservation(o, options);
|
|
135
|
+
const quantity = owner ? ownValue(options.resolveBindable?.(owner.type), binding.property) : undefined;
|
|
136
|
+
const converted = convert(o.value, quantity, o.unit ?? binding.unit ?? variableUnits.get(binding.tagId));
|
|
137
|
+
const problem = typeProblem(o) ?? ('reason' in converted ? converted.reason : assessObservation(o, options));
|
|
67
138
|
if (problem)
|
|
68
139
|
properties.push({ ...target, status: 'unknown', reason: problem, ...meta });
|
|
69
140
|
else
|
|
@@ -80,8 +151,13 @@ export function displayScene(scene, properties) {
|
|
|
80
151
|
if (properties.length === 0)
|
|
81
152
|
return scene;
|
|
82
153
|
const byComponent = new Map();
|
|
83
|
-
for (const p of properties)
|
|
84
|
-
|
|
154
|
+
for (const p of properties) {
|
|
155
|
+
const list = byComponent.get(p.instanceId);
|
|
156
|
+
if (list)
|
|
157
|
+
list.push(p);
|
|
158
|
+
else
|
|
159
|
+
byComponent.set(p.instanceId, [p]);
|
|
160
|
+
}
|
|
85
161
|
return {
|
|
86
162
|
...scene,
|
|
87
163
|
components: scene.components.map((c) => {
|
package/dist/telemetry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FlowReading, Quality, ValueSource } from './flow.js';\nimport { assessObservation, isNewer, type FreshnessOptions } from './observation.js';\nimport type { ComponentKind, Quantity, ScadaScene } from './scene.js';\nimport { isKnownUnit, toCanonical } from './units.js';\n\n/** One value for a host tag. Read-only: the SDK never writes tags (ADR-0003). */\nexport interface Observation {\n tagId: string;\n value: number | boolean | string;\n quality: Quality;\n /** ISO 8601 time the value was observed at its source. */\n sourceTimestamp: string;\n /** Default MEASURED. */\n source?: ValueSource;\n /** Unit of `value`; when absent the binding's unit, then the canonical unit, is assumed. */\n unit?: string;\n}\n\nexport type TelemetryUnknownReason = 'NO_READING' | 'BAD_QUALITY' | 'STALE' | 'INVALID_TIMESTAMP' | 'INVALID_VALUE' | 'INVALID_UNIT';\n\ntype Target = { bindingId: string; instanceId: string; property: string };\n\nexport type PropertyState =\n | (Target & {\n status: 'ok';\n /** In the canonical unit of the property's quantity. */\n value: number | boolean;\n quality: Quality;\n source: ValueSource;\n sourceTimestamp: string;\n uncertain: boolean;\n reason?: undefined;\n })\n | (Target & {\n status: 'unknown';\n reason: TelemetryUnknownReason;\n quality?: Quality;\n source?: ValueSource;\n sourceTimestamp?: string;\n value?: undefined;\n });\n\nexport interface TelemetryOptions extends FreshnessOptions {\n /** Which observations to use; measured and simulated are never mixed. Default MEASURED. */\n source?: ValueSource;\n /** Quantities of bindable properties. Without it, numbers and booleans pass through unconverted. */\n resolveBindable?: (type: ComponentKind) => Readonly<Record<string, Quantity>> | undefined;\n}\n\nexport interface TelemetryResolution {\n /** One state per property binding, in binding order. */\n properties: PropertyState[];\n /** Readings for flow bindings that have an observation, in m³/s, ready for `resolveFlow`. */\n flowReadings: FlowReading[];\n /** Observed tag ids that no binding uses, in first-seen order. */\n unmatchedTagIds: string[];\n}\n\n/** Value in the canonical unit, or the reason it cannot be used. */\nfunction convert(value: Observation['value'], quantity: Quantity | undefined, unit: string | undefined): { value: number | boolean } | { reason: 'INVALID_VALUE' | 'INVALID_UNIT' } {\n if (quantity === 'boolean' || (quantity === undefined && typeof value === 'boolean')) {\n if (typeof value !== 'boolean') return { reason: 'INVALID_VALUE' };\n return unit === undefined || unit === '' ? { value } : { reason: 'INVALID_UNIT' };\n }\n if (typeof value !== 'number' || !Number.isFinite(value)) return { reason: 'INVALID_VALUE' };\n if (quantity === undefined) return { value };\n if (!isKnownUnit(quantity, unit)) return { reason: 'INVALID_UNIT' };\n return { value: toCanonical(quantity, value, unit)! };\n}\n\n/**\n * Applies host observations to the scene's tag bindings. Pure: `now` is passed in and nothing is\n * written back. Property bindings resolve to a trusted value or an unknown reason; flow bindings\n * become `FlowReading`s so `resolveFlow` stays the single rule for flow direction.\n */\nexport function resolveTelemetry(scene: ScadaScene, observations: readonly Observation[], options: TelemetryOptions): TelemetryResolution {\n const source = options.source ?? 'MEASURED';\n const bindings = scene.tagBindings ?? [];\n const boundTags = new Set(bindings.map((b) => b.tagId));\n const latest = new Map<string, Observation>();\n const unmatched = new Set<string>();\n for (const o of observations) {\n if (!boundTags.has(o.tagId)) {\n unmatched.add(o.tagId);\n continue;\n }\n if ((o.source ?? 'MEASURED') !== source) continue;\n if (isNewer(o, latest.get(o.tagId))) latest.set(o.tagId, o);\n }\n\n const components = new Map(scene.components.map((c) => [c.id, c]));\n const properties: PropertyState[] = [];\n const flowReadings: FlowReading[] = [];\n for (const binding of bindings) {\n const o = latest.get(binding.tagId);\n if (binding.kind === 'flow') {\n if (!o) continue;\n const converted = convert(o.value, 'flow', o.unit ?? binding.unit);\n flowReadings.push({\n connectionId: binding.connectionId,\n rate: 'value' in converted ? (converted.value as number) : Number.NaN,\n quality: o.quality,\n source,\n sourceTimestamp: o.sourceTimestamp,\n });\n continue;\n }\n const target: Target = { bindingId: binding.id, instanceId: binding.instanceId, property: binding.property };\n if (!o) {\n properties.push({ ...target, status: 'unknown', reason: 'NO_READING' });\n continue;\n }\n const meta = { quality: o.quality, source, sourceTimestamp: o.sourceTimestamp };\n const owner = components.get(binding.instanceId);\n const quantity = owner ? options.resolveBindable?.(owner.type)?.[binding.property] : undefined;\n const converted = convert(o.value, quantity, o.unit ?? binding.unit);\n const problem = 'reason' in converted ? converted.reason : assessObservation(o, options);\n if (problem) properties.push({ ...target, status: 'unknown', reason: problem, ...meta });\n else properties.push({ ...target, status: 'ok', value: (converted as { value: number | boolean }).value, ...meta, uncertain: o.quality === 'UNCERTAIN' });\n }\n return { properties, flowReadings, unmatchedTagIds: [...unmatched] };\n}\n\n/**\n * The scene as it should be drawn: trusted values replace authored ones, and bound properties\n * without a trusted value are removed so an authored default is never shown as if it were live.\n * For display only; never save the result.\n */\nexport function displayScene(scene: ScadaScene, properties: readonly PropertyState[]): ScadaScene {\n if (properties.length === 0) return scene;\n const byComponent = new Map<string, PropertyState[]>();\n for (const p of properties) byComponent.set(p.instanceId, [...(byComponent.get(p.instanceId) ?? []), p]);\n return {\n ...scene,\n components: scene.components.map((c) => {\n const states = byComponent.get(c.id);\n if (!states) return c;\n const next = { ...c.properties };\n for (const s of states) {\n if (s.status === 'ok') next[s.property] = s.value;\n else delete next[s.property];\n }\n return { ...c, properties: next };\n }),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAyB,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,SAAS,EAAE,QAAQ,EAA2F,MAAM,YAAY,CAAC;AAC1I,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA4DtD,MAAM,aAAa,GAAyD;IAC1E,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC;IAClB,KAAK,EAAE,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,MAAM,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC;CACxB,CAAC;AACF,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAE1C;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,QAAkB,EAAE,MAAM,GAAG,KAAK;IAClF,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,OAAO,KAAK,KAAK,SAAS,CAAC;IAC3D,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvE,IAAI,MAAM,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC;IAClE,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,wGAAwG;AACxG,SAAS,aAAa,CAAC,CAAc,EAAE,QAAqC;IAC1E,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,eAAe,CAAC;IAC5I,MAAM,QAAQ,GAAG,QAAQ,EAAE,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC;IAClD,OAAO,QAAQ,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9G,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAiB,EAAE,YAAoC,EAAE,OAAoD;IACjJ,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,CAAC;IAC5C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,iFAAiF;QACjF,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,UAAU,CAAC,KAAK,MAAM;YAAE,SAAS;QACpH,IAAI,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAChD,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;YACtE,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,EAAE,QAAQ,CAAC,IAAI,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5E,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO;YACvB,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,eAAe,EAAE;YAC/G,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,eAAe,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC,CAAC;IACrJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,oEAAoE;AACpE,SAAS,OAAO,CAAC,KAA2B,EAAE,QAA8B,EAAE,IAAwB;IACpG,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,SAAS,CAAC,EAAE,CAAC;QACrF,IAAI,OAAO,KAAK,KAAK,SAAS;YAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;QACnE,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpF,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC7F,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC7C,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpE,OAAO,EAAE,KAAK,EAAE,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAE,EAAE,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAiB,EAAE,YAAoC,EAAE,OAAyB;IACjH,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,CAAC;IAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS;QAClD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;gBAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACxD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,UAAU,CAAC,KAAK,MAAM;YAAE,SAAS;QAClD,IAAI,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,uGAAuG;IACvG,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7H,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,MAAM,WAAW,GAAG,CAAC,CAAc,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACjF,MAAM,UAAU,GAAoB,EAAE,CAAC;IACvC,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,CAAC;gBAAE,SAAS;YACjB,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YACvG,YAAY,CAAC,IAAI,CAAC;gBAChB,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,IAAI,EAAE,OAAO,IAAI,SAAS,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,SAAS,CAAC,KAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG;gBACxF,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,MAAM;gBACN,eAAe,EAAE,CAAC,CAAC,eAAe;aACnC,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAW,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC7G,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;YACxE,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC;QAChF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvG,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACzG,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAC7G,IAAI,OAAO;YAAE,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;;YACpF,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAG,SAAyC,CAAC,KAAK,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC,CAAC;IAC5J,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAiB,EAAE,UAAoC;IAClF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,EAA2B,CAAC;IACvD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAAC,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,IAAI,IAAI;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YAAM,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAC5I,OAAO;QACL,GAAG,KAAK;QACR,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACrC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACrC,IAAI,CAAC,MAAM;gBAAE,OAAO,CAAC,CAAC;YACtB,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;YACjC,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;gBACvB,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI;oBAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;;oBAC7C,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC;YACD,OAAO,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;QACpC,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["import type { FlowReading, Quality, ValueSource } from './flow.js';\nimport { assessObservation, isNewer, type FreshnessOptions } from './observation.js';\nimport { dataTypes, ownValue, type ComponentType, type DataType, type Quantity, type RuntimeVariable, type ScadaScene } from './scene.js';\nimport { isKnownUnit, toCanonical } from './units.js';\n\n/** One value for a host tag. Read-only: the SDK never writes tags (ADR-0003). */\nexport interface Observation {\n tagId: string;\n value: number | boolean | string;\n quality: Quality;\n /** ISO 8601 time the value was observed at its source. */\n sourceTimestamp: string;\n /** Default MEASURED. */\n source?: ValueSource;\n /**\n * The value's data type as the gateway reports it (ADR-0029). When the tag's runtime variable\n * declares another type, the value is unknown (TYPE_MISMATCH).\n */\n dataType?: DataType;\n /** Unit of `value`; when absent the binding's unit, then the runtime variable's unit, then the canonical unit, is assumed. */\n unit?: string;\n}\n\nexport type TelemetryUnknownReason = 'NO_READING' | 'BAD_QUALITY' | 'DISCONNECTED' | 'STALE' | 'INVALID_TIMESTAMP' | 'INVALID_VALUE' | 'INVALID_UNIT' | 'TYPE_MISMATCH';\n\ntype Target = { bindingId: string; instanceId: string; property: string };\n\nexport type PropertyState =\n | (Target & {\n status: 'ok';\n /** In the canonical unit of the property's quantity. */\n value: number | boolean;\n quality: Quality;\n source: ValueSource;\n sourceTimestamp: string;\n uncertain: boolean;\n reason?: undefined;\n })\n | (Target & {\n status: 'unknown';\n reason: TelemetryUnknownReason;\n quality?: Quality;\n source?: ValueSource;\n sourceTimestamp?: string;\n value?: undefined;\n });\n\nexport interface TelemetryOptions extends FreshnessOptions {\n /** Which observations to use; measured and simulated are never mixed. Default MEASURED. */\n source?: ValueSource;\n /** Quantities of bindable properties. Without it, numbers and booleans pass through unconverted. */\n resolveBindable?: (type: ComponentType) => Readonly<Record<string, Quantity>> | undefined;\n}\n\nexport interface TelemetryResolution {\n /** One state per property binding, in binding order. */\n properties: PropertyState[];\n /** Readings for flow bindings that have an observation, in m³/s, ready for `resolveFlow`. */\n flowReadings: FlowReading[];\n /** Observed tag ids that no binding uses, in first-seen order. */\n unmatchedTagIds: string[];\n}\n\nconst integerRanges: Partial<Record<DataType, readonly [number, number]>> = {\n int16: [-32768, 32767],\n uint16: [0, 65535],\n int32: [-2147483648, 2147483647],\n uint32: [0, 4294967295],\n};\nconst FLOAT32_MAX = 3.4028234663852886e38;\n\n/**\n * Whether `value` fits a tag's data type (ADR-0029): a boolean for `bool`, an integer in range for the\n * integer types, a finite number for the float types, a string for `string`. A `scaled` variable\n * carries the host's engineering value, not the raw register, so any finite number fits.\n */\nexport function valueFitsDataType(value: unknown, dataType: DataType, scaled = false): boolean {\n if (dataType === 'bool') return typeof value === 'boolean';\n if (dataType === 'string') return typeof value === 'string';\n if (typeof value !== 'number' || !Number.isFinite(value)) return false;\n if (scaled || dataType === 'float64') return true;\n if (dataType === 'float32') return Math.abs(value) <= FLOAT32_MAX;\n const range = integerRanges[dataType];\n return !!range && Number.isInteger(value) && value >= range[0] && value <= range[1];\n}\n\n/** The declared type wins; a gateway's stated type must match it, and the value must fit (ADR-0029). */\nfunction typeProblemOf(o: Observation, variable: RuntimeVariable | undefined): 'TYPE_MISMATCH' | 'INVALID_VALUE' | undefined {\n if (o.dataType !== undefined && (!dataTypes.includes(o.dataType) || (variable && o.dataType !== variable.dataType))) return 'TYPE_MISMATCH';\n const declared = variable?.dataType ?? o.dataType;\n return declared && !valueFitsDataType(o.value, declared, !!variable?.scaling) ? 'INVALID_VALUE' : undefined;\n}\n\n/** The current value of one runtime variable, as the host sent it (its own unit; no conversion). */\nexport type VariableState =\n | { tagId: string; status: 'ok'; value: number | boolean | string; quality: Quality; source: ValueSource; sourceTimestamp: string; uncertain: boolean; reason?: undefined }\n | { tagId: string; status: 'unknown'; reason: TelemetryUnknownReason; quality?: Quality; source?: ValueSource; sourceTimestamp?: string; value?: undefined };\n\n/**\n * The latest value of every runtime variable, bound or not, with the same trust rules as bindings:\n * type, quality and freshness (ADR-0029). The runtime shows it beside a writable variable, so an\n * operator sees the state a request would change. Pure.\n */\nexport function resolveVariableValues(scene: ScadaScene, observations: readonly Observation[], options: FreshnessOptions & { source?: ValueSource }): Map<string, VariableState> {\n const source = options.source ?? 'MEASURED';\n const variables = new Map((scene.runtimeVariables ?? []).map((v) => [v.id, v]));\n const latest = new Map<string, Observation>();\n for (const o of observations) {\n // Host data is untrusted: skip entries that are not observations at all (T-096).\n if (typeof o !== 'object' || o === null || !variables.has(o.tagId) || (o.source ?? 'MEASURED') !== source) continue;\n if (isNewer(o, latest.get(o.tagId))) latest.set(o.tagId, o);\n }\n const states = new Map<string, VariableState>();\n for (const [tagId, variable] of variables) {\n const o = latest.get(tagId);\n if (!o) {\n states.set(tagId, { tagId, status: 'unknown', reason: 'NO_READING' });\n continue;\n }\n const problem = typeProblemOf(o, variable) ?? assessObservation(o, options);\n states.set(tagId, problem\n ? { tagId, status: 'unknown', reason: problem, quality: o.quality, source, sourceTimestamp: o.sourceTimestamp }\n : { tagId, status: 'ok', value: o.value, quality: o.quality, source, sourceTimestamp: o.sourceTimestamp, uncertain: o.quality === 'UNCERTAIN' });\n }\n return states;\n}\n\n/** Value in the canonical unit, or the reason it cannot be used. */\nfunction convert(value: Observation['value'], quantity: Quantity | undefined, unit: string | undefined): { value: number | boolean } | { reason: 'INVALID_VALUE' | 'INVALID_UNIT' } {\n if (quantity === 'boolean' || (quantity === undefined && typeof value === 'boolean')) {\n if (typeof value !== 'boolean') return { reason: 'INVALID_VALUE' };\n return unit === undefined || unit === '' ? { value } : { reason: 'INVALID_UNIT' };\n }\n if (typeof value !== 'number' || !Number.isFinite(value)) return { reason: 'INVALID_VALUE' };\n if (quantity === undefined) return { value };\n if (!isKnownUnit(quantity, unit)) return { reason: 'INVALID_UNIT' };\n return { value: toCanonical(quantity, value, unit)! };\n}\n\n/**\n * Applies host observations to the scene's tag bindings. Pure: `now` is passed in and nothing is\n * written back. Property bindings resolve to a trusted value or an unknown reason; flow bindings\n * become `FlowReading`s so `resolveFlow` stays the single rule for flow direction.\n */\nexport function resolveTelemetry(scene: ScadaScene, observations: readonly Observation[], options: TelemetryOptions): TelemetryResolution {\n const source = options.source ?? 'MEASURED';\n const bindings = scene.tagBindings ?? [];\n const boundTags = new Set(bindings.map((b) => b.tagId));\n const latest = new Map<string, Observation>();\n const unmatched = new Set<string>();\n for (const o of observations) {\n if (typeof o !== 'object' || o === null) continue;\n if (!boundTags.has(o.tagId)) {\n if (typeof o.tagId === 'string') unmatched.add(o.tagId);\n continue;\n }\n if ((o.source ?? 'MEASURED') !== source) continue;\n if (isNewer(o, latest.get(o.tagId))) latest.set(o.tagId, o);\n }\n\n const components = new Map(scene.components.map((c) => [c.id, c]));\n // A runtime variable's unit applies when neither the observation nor the binding gives one (ADR-0024).\n const variableUnits = new Map((scene.runtimeVariables ?? []).filter((v) => v.unit !== undefined).map((v) => [v.id, v.unit]));\n const variables = new Map((scene.runtimeVariables ?? []).map((v) => [v.id, v]));\n const typeProblem = (o: Observation) => typeProblemOf(o, variables.get(o.tagId));\n const properties: PropertyState[] = [];\n const flowReadings: FlowReading[] = [];\n for (const binding of bindings) {\n const o = latest.get(binding.tagId);\n if (binding.kind === 'flow') {\n if (!o) continue;\n const converted = convert(o.value, 'flow', o.unit ?? binding.unit ?? variableUnits.get(binding.tagId));\n flowReadings.push({\n connectionId: binding.connectionId,\n rate: 'value' in converted && !typeProblem(o) ? (converted.value as number) : Number.NaN,\n quality: o.quality,\n source,\n sourceTimestamp: o.sourceTimestamp,\n });\n continue;\n }\n const target: Target = { bindingId: binding.id, instanceId: binding.instanceId, property: binding.property };\n if (!o) {\n properties.push({ ...target, status: 'unknown', reason: 'NO_READING' });\n continue;\n }\n const meta = { quality: o.quality, source, sourceTimestamp: o.sourceTimestamp };\n const owner = components.get(binding.instanceId);\n const quantity = owner ? ownValue(options.resolveBindable?.(owner.type), binding.property) : undefined;\n const converted = convert(o.value, quantity, o.unit ?? binding.unit ?? variableUnits.get(binding.tagId));\n const problem = typeProblem(o) ?? ('reason' in converted ? converted.reason : assessObservation(o, options));\n if (problem) properties.push({ ...target, status: 'unknown', reason: problem, ...meta });\n else properties.push({ ...target, status: 'ok', value: (converted as { value: number | boolean }).value, ...meta, uncertain: o.quality === 'UNCERTAIN' });\n }\n return { properties, flowReadings, unmatchedTagIds: [...unmatched] };\n}\n\n/**\n * The scene as it should be drawn: trusted values replace authored ones, and bound properties\n * without a trusted value are removed so an authored default is never shown as if it were live.\n * For display only; never save the result.\n */\nexport function displayScene(scene: ScadaScene, properties: readonly PropertyState[]): ScadaScene {\n if (properties.length === 0) return scene;\n const byComponent = new Map<string, PropertyState[]>();\n for (const p of properties) { const list = byComponent.get(p.instanceId); if (list) list.push(p); else byComponent.set(p.instanceId, [p]); }\n return {\n ...scene,\n components: scene.components.map((c) => {\n const states = byComponent.get(c.id);\n if (!states) return c;\n const next = { ...c.properties };\n for (const s of states) {\n if (s.status === 'ok') next[s.property] = s.value;\n else delete next[s.property];\n }\n return { ...c, properties: next };\n }),\n };\n}\n"]}
|
package/dist/touching.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type ComponentInstance, type
|
|
1
|
+
import { type ComponentInstance, type ComponentType, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';
|
|
2
2
|
import { type SceneValidationOptions } from './validation.js';
|
|
3
3
|
type Endpoint = SceneConnection['from'];
|
|
4
4
|
export interface TouchingOptions {
|
|
5
|
-
resolvePorts: (type:
|
|
5
|
+
resolvePorts: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
|
|
6
6
|
/** Largest distance between two port centres that still counts as the same point, metres. Default 0.001. */
|
|
7
7
|
tolerance?: number;
|
|
8
8
|
/** Largest deviation from exactly opposite facing directions, radians. Default 1°. */
|
package/dist/touching.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"touching.d.ts","sourceRoot":"","sources":["../src/touching.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"touching.d.ts","sourceRoot":"","sources":["../src/touching.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkD,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpL,OAAO,EAAiB,KAAK,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE7E,KAAK,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;AAExC,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK,SAAS,cAAc,EAAE,GAAG,SAAS,CAAC;IAC3H,4GAA4G;IAC5G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,QAAQ,CAAC;CACd;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,wDAAwD;IACxD,MAAM,EAAE,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAID;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAyD7F;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAAE,GAC7E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE5D,iGAAiG;AACjG,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,MAAM,CAAA;CAAE,GACnG,qBAAqB,CAOvB"}
|
package/dist/touching.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { portsCompatible } from './scene.js';
|
|
1
|
+
import { matesPhysically, portCapacity, portsCompatible } from './scene.js';
|
|
2
2
|
import { portToWorld } from './transforms.js';
|
|
3
3
|
import { validateScene } from './validation.js';
|
|
4
4
|
const key = (e) => JSON.stringify([e.instanceId, e.portId]);
|
|
@@ -12,10 +12,13 @@ const key = (e) => JSON.stringify([e.instanceId, e.portId]);
|
|
|
12
12
|
export function findTouchingPorts(scene, options) {
|
|
13
13
|
const tolerance = options.tolerance ?? 0.001;
|
|
14
14
|
const minOpposition = Math.cos(options.maxAngle ?? Math.PI / 180);
|
|
15
|
-
const occupied = new
|
|
15
|
+
const occupied = new Map();
|
|
16
|
+
for (const e of scene.connections.flatMap((c) => [c.from, c.to]))
|
|
17
|
+
occupied.set(key(e), (occupied.get(key(e)) ?? 0) + 1);
|
|
18
|
+
const full = (e) => (occupied.get(key(e.endpoint)) ?? 0) >= portCapacity(e.port);
|
|
16
19
|
// Pairs that are already joined to each other (either way round) are done, not problems.
|
|
17
20
|
const joined = new Set(scene.connections.flatMap((c) => [`${key(c.from)}|${key(c.to)}`, `${key(c.to)}|${key(c.from)}`]));
|
|
18
|
-
const entries = scene.components.flatMap((c) => (options.resolvePorts(c.type, c.properties) ?? []).map((port) => ({ endpoint: { instanceId: c.id, portId: port.id }, port, world: portToWorld(c, port) })));
|
|
21
|
+
const entries = scene.components.flatMap((c) => (options.resolvePorts(c.type, c.properties) ?? []).filter(matesPhysically).map((port) => ({ endpoint: { instanceId: c.id, portId: port.id }, port, world: portToWorld(c, port) })));
|
|
19
22
|
// Spatial hash: cells of the tolerance size; neighbours are found in the 27 surrounding cells.
|
|
20
23
|
const cell = (v) => Math.floor(v / Math.max(tolerance, 1e-6));
|
|
21
24
|
const grid = new Map();
|
|
@@ -59,7 +62,7 @@ export function findTouchingPorts(scene, options) {
|
|
|
59
62
|
rejected.push({ ...pair, reason: 'ambiguous' });
|
|
60
63
|
else if (!portsCompatible(a.port, b.port))
|
|
61
64
|
rejected.push({ ...pair, reason: 'incompatible' });
|
|
62
|
-
else if (
|
|
65
|
+
else if (full(a) || full(b))
|
|
63
66
|
rejected.push({ ...pair, reason: 'occupied' });
|
|
64
67
|
else
|
|
65
68
|
matches.push(pair);
|
package/dist/touching.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"touching.js","sourceRoot":"","sources":["../src/touching.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA0G,MAAM,YAAY,CAAC;AACrJ,OAAO,EAAE,WAAW,EAAkB,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAA+B,MAAM,iBAAiB,CAAC;AA4B7E,MAAM,GAAG,GAAG,CAAC,CAAW,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAiB,EAAE,OAAwB;IAC3E,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC7C,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrF,yFAAyF;IACzF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAGzH,MAAM,OAAO,GAAY,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CACtD,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAC3J,CAAC;IAEF,+FAA+F;IAC/F,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC;IACtE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAmB,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,KAAK,GAA6B,EAAE,CAAC;IAC3C,uGAAuG;IACvG,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChD,KAAK,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE;YAC7B,KAAK,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE;gBAC7B,KAAK,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE;oBAC7B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;wBACnE,IAAI,CAAC,CAAC,QAAQ,CAAC,UAAU,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;4BAAE,SAAS;wBACpG,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;wBACjF,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;wBAC3F,IAAI,CAAC,GAAG,SAAS,IAAI,UAAU,GAAG,aAAa;4BAAE,SAAS;wBAC1D,MAAM,IAAI,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;wBACrD,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;4BAAE,SAAS;wBAC7B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;wBACf,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;oBACvB,CAAC;IACT,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,KAAK;QAAE,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAE5H,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,KAAK,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClD,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,SAAS;QAClE,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAE,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAE,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;aACvH,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;aACzF,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;;YACnH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,CAAgB,EAAE,CAAgB,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxJ,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AAC1E,CAAC;AAMD,iGAAiG;AACjG,MAAM,UAAU,oBAAoB,CAClC,KAAiB,EACjB,OAAoG;IAEpG,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAChE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC;IACzE,MAAM,IAAI,GAAe,EAAE,GAAG,KAAK,EAAE,WAAW,EAAE,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACtI,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;IAChH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC5E,CAAC","sourcesContent":["import { portsCompatible, type ComponentInstance, type ComponentKind, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';\nimport { portToWorld, type WorldPort } from './transforms.js';\nimport { validateScene, type SceneValidationOptions } from './validation.js';\n\ntype Endpoint = SceneConnection['from'];\n\nexport interface TouchingOptions {\n resolvePorts: (type: ComponentKind, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;\n /** Largest distance between two port centres that still counts as the same point, metres. Default 0.001. */\n tolerance?: number;\n /** Largest deviation from exactly opposite facing directions, radians. Default 1°. */\n maxAngle?: number;\n}\n\nexport interface TouchingMatch {\n /** The endpoint that sorts first (instance id, then port id); `to` is the other. */\n from: Endpoint;\n to: Endpoint;\n}\n\nexport interface TouchingRejection extends TouchingMatch {\n /** Why a pair that meets face to face is not joined. */\n reason: 'incompatible' | 'occupied' | 'ambiguous';\n}\n\nexport interface TouchingResult {\n matches: TouchingMatch[];\n rejected: TouchingRejection[];\n}\n\nconst key = (e: Endpoint) => JSON.stringify([e.instanceId, e.portId]);\n\n/**\n * Ports of different components that sit at the same point and face each other: candidates for\n * explicit connections (ADR-0002 still holds: nothing is connected until `connectTouchingPorts`\n * or the user writes the edge). Pairs already connected to each other are skipped. Pairs with a\n * type/bore mismatch, a port used by another connection, or a port that touches more than one\n * candidate are reported in `rejected`, never matched.\n */\nexport function findTouchingPorts(scene: ScadaScene, options: TouchingOptions): TouchingResult {\n const tolerance = options.tolerance ?? 0.001;\n const minOpposition = Math.cos(options.maxAngle ?? Math.PI / 180);\n const occupied = new Set(scene.connections.flatMap((c) => [key(c.from), key(c.to)]));\n // Pairs that are already joined to each other (either way round) are done, not problems.\n const joined = new Set(scene.connections.flatMap((c) => [`${key(c.from)}|${key(c.to)}`, `${key(c.to)}|${key(c.from)}`]));\n\n type Entry = { endpoint: Endpoint; port: ConnectionPort; world: WorldPort };\n const entries: Entry[] = scene.components.flatMap((c) =>\n (options.resolvePorts(c.type, c.properties) ?? []).map((port) => ({ endpoint: { instanceId: c.id, portId: port.id }, port, world: portToWorld(c, port) })),\n );\n\n // Spatial hash: cells of the tolerance size; neighbours are found in the 27 surrounding cells.\n const cell = (v: number) => Math.floor(v / Math.max(tolerance, 1e-6));\n const grid = new Map<string, Entry[]>();\n for (const e of entries) {\n const k = e.world.position.map(cell).join(',');\n grid.set(k, [...(grid.get(k) ?? []), e]);\n }\n\n const pairs: { a: Entry; b: Entry }[] = [];\n // A resolver may list one port in more than one place (`upgradeDimensions`); a pair is still one pair.\n const seen = new Set<string>();\n for (const a of entries) {\n const [cx, cy, cz] = a.world.position.map(cell);\n for (let dx = -1; dx <= 1; dx++)\n for (let dy = -1; dy <= 1; dy++)\n for (let dz = -1; dz <= 1; dz++)\n for (const b of grid.get(`${cx + dx},${cy + dy},${cz + dz}`) ?? []) {\n if (b.endpoint.instanceId === a.endpoint.instanceId || key(b.endpoint) <= key(a.endpoint)) continue;\n const d = Math.hypot(...a.world.position.map((n, i) => n - b.world.position[i]));\n const opposition = -a.world.direction.reduce((s, n, i) => s + n * b.world.direction[i], 0);\n if (d > tolerance || opposition < minOpposition) continue;\n const pair = `${key(a.endpoint)}|${key(b.endpoint)}`;\n if (seen.has(pair)) continue;\n seen.add(pair);\n pairs.push({ a, b });\n }\n }\n\n const touches = new Map<string, number>();\n for (const { a, b } of pairs) for (const e of [a, b]) touches.set(key(e.endpoint), (touches.get(key(e.endpoint)) ?? 0) + 1);\n\n const matches: TouchingMatch[] = [];\n const rejected: TouchingRejection[] = [];\n for (const { a, b } of pairs) {\n const pair = { from: a.endpoint, to: b.endpoint };\n if (joined.has(`${key(a.endpoint)}|${key(b.endpoint)}`)) continue;\n if (touches.get(key(a.endpoint))! > 1 || touches.get(key(b.endpoint))! > 1) rejected.push({ ...pair, reason: 'ambiguous' });\n else if (!portsCompatible(a.port, b.port)) rejected.push({ ...pair, reason: 'incompatible' });\n else if (occupied.has(key(a.endpoint)) || occupied.has(key(b.endpoint))) rejected.push({ ...pair, reason: 'occupied' });\n else matches.push(pair);\n }\n const order = (x: TouchingMatch, y: TouchingMatch) => (key(x.from) < key(y.from) ? -1 : key(x.from) > key(y.from) ? 1 : key(x.to) < key(y.to) ? -1 : 1);\n return { matches: matches.sort(order), rejected: rejected.sort(order) };\n}\n\nexport type ConnectTouchingResult =\n | { ok: true; scene: ScadaScene; added: number; rejected: TouchingRejection[] }\n | { ok: false; error: { code: string; message: string } };\n\n/** Writes every `findTouchingPorts` match as an explicit connection, validated as one change. */\nexport function connectTouchingPorts(\n scene: ScadaScene,\n options: TouchingOptions & Omit<SceneValidationOptions, 'resolvePorts'> & { createId: () => string },\n): ConnectTouchingResult {\n const { matches, rejected } = findTouchingPorts(scene, options);\n if (matches.length === 0) return { ok: true, scene, added: 0, rejected };\n const next: ScadaScene = { ...scene, connections: [...scene.connections, ...matches.map((m) => ({ id: options.createId(), ...m }))] };\n const result = validateScene(next, options);\n if (!result.ok) return { ok: false, error: { code: result.errors[0].code, message: result.errors[0].message } };\n return { ok: true, scene: result.scene, added: matches.length, rejected };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"touching.js","sourceRoot":"","sources":["../src/touching.ts"],"names":[],"mappings":"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","sourcesContent":["import { matesPhysically, portCapacity, portsCompatible, type ComponentInstance, type ComponentType, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';\nimport { portToWorld, type WorldPort } from './transforms.js';\nimport { validateScene, type SceneValidationOptions } from './validation.js';\n\ntype Endpoint = SceneConnection['from'];\n\nexport interface TouchingOptions {\n resolvePorts: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;\n /** Largest distance between two port centres that still counts as the same point, metres. Default 0.001. */\n tolerance?: number;\n /** Largest deviation from exactly opposite facing directions, radians. Default 1°. */\n maxAngle?: number;\n}\n\nexport interface TouchingMatch {\n /** The endpoint that sorts first (instance id, then port id); `to` is the other. */\n from: Endpoint;\n to: Endpoint;\n}\n\nexport interface TouchingRejection extends TouchingMatch {\n /** Why a pair that meets face to face is not joined. */\n reason: 'incompatible' | 'occupied' | 'ambiguous';\n}\n\nexport interface TouchingResult {\n matches: TouchingMatch[];\n rejected: TouchingRejection[];\n}\n\nconst key = (e: Endpoint) => JSON.stringify([e.instanceId, e.portId]);\n\n/**\n * Ports of different components that sit at the same point and face each other: candidates for\n * explicit connections (ADR-0002 still holds: nothing is connected until `connectTouchingPorts`\n * or the user writes the edge). Pairs already connected to each other are skipped. Pairs with a\n * type/bore mismatch, a port used by another connection, or a port that touches more than one\n * candidate are reported in `rejected`, never matched.\n */\nexport function findTouchingPorts(scene: ScadaScene, options: TouchingOptions): TouchingResult {\n const tolerance = options.tolerance ?? 0.001;\n const minOpposition = Math.cos(options.maxAngle ?? Math.PI / 180);\n const occupied = new Map<string, number>();\n for (const e of scene.connections.flatMap((c) => [c.from, c.to])) occupied.set(key(e), (occupied.get(key(e)) ?? 0) + 1);\n const full = (e: Entry) => (occupied.get(key(e.endpoint)) ?? 0) >= portCapacity(e.port);\n // Pairs that are already joined to each other (either way round) are done, not problems.\n const joined = new Set(scene.connections.flatMap((c) => [`${key(c.from)}|${key(c.to)}`, `${key(c.to)}|${key(c.from)}`]));\n\n type Entry = { endpoint: Endpoint; port: ConnectionPort; world: WorldPort };\n const entries: Entry[] = scene.components.flatMap((c) =>\n (options.resolvePorts(c.type, c.properties) ?? []).filter(matesPhysically).map((port) => ({ endpoint: { instanceId: c.id, portId: port.id }, port, world: portToWorld(c, port) })),\n );\n\n // Spatial hash: cells of the tolerance size; neighbours are found in the 27 surrounding cells.\n const cell = (v: number) => Math.floor(v / Math.max(tolerance, 1e-6));\n const grid = new Map<string, Entry[]>();\n for (const e of entries) {\n const k = e.world.position.map(cell).join(',');\n grid.set(k, [...(grid.get(k) ?? []), e]);\n }\n\n const pairs: { a: Entry; b: Entry }[] = [];\n // A resolver may list one port in more than one place (`upgradeDimensions`); a pair is still one pair.\n const seen = new Set<string>();\n for (const a of entries) {\n const [cx, cy, cz] = a.world.position.map(cell);\n for (let dx = -1; dx <= 1; dx++)\n for (let dy = -1; dy <= 1; dy++)\n for (let dz = -1; dz <= 1; dz++)\n for (const b of grid.get(`${cx + dx},${cy + dy},${cz + dz}`) ?? []) {\n if (b.endpoint.instanceId === a.endpoint.instanceId || key(b.endpoint) <= key(a.endpoint)) continue;\n const d = Math.hypot(...a.world.position.map((n, i) => n - b.world.position[i]));\n const opposition = -a.world.direction.reduce((s, n, i) => s + n * b.world.direction[i], 0);\n if (d > tolerance || opposition < minOpposition) continue;\n const pair = `${key(a.endpoint)}|${key(b.endpoint)}`;\n if (seen.has(pair)) continue;\n seen.add(pair);\n pairs.push({ a, b });\n }\n }\n\n const touches = new Map<string, number>();\n for (const { a, b } of pairs) for (const e of [a, b]) touches.set(key(e.endpoint), (touches.get(key(e.endpoint)) ?? 0) + 1);\n\n const matches: TouchingMatch[] = [];\n const rejected: TouchingRejection[] = [];\n for (const { a, b } of pairs) {\n const pair = { from: a.endpoint, to: b.endpoint };\n if (joined.has(`${key(a.endpoint)}|${key(b.endpoint)}`)) continue;\n if (touches.get(key(a.endpoint))! > 1 || touches.get(key(b.endpoint))! > 1) rejected.push({ ...pair, reason: 'ambiguous' });\n else if (!portsCompatible(a.port, b.port)) rejected.push({ ...pair, reason: 'incompatible' });\n else if (full(a) || full(b)) rejected.push({ ...pair, reason: 'occupied' });\n else matches.push(pair);\n }\n const order = (x: TouchingMatch, y: TouchingMatch) => (key(x.from) < key(y.from) ? -1 : key(x.from) > key(y.from) ? 1 : key(x.to) < key(y.to) ? -1 : 1);\n return { matches: matches.sort(order), rejected: rejected.sort(order) };\n}\n\nexport type ConnectTouchingResult =\n | { ok: true; scene: ScadaScene; added: number; rejected: TouchingRejection[] }\n | { ok: false; error: { code: string; message: string } };\n\n/** Writes every `findTouchingPorts` match as an explicit connection, validated as one change. */\nexport function connectTouchingPorts(\n scene: ScadaScene,\n options: TouchingOptions & Omit<SceneValidationOptions, 'resolvePorts'> & { createId: () => string },\n): ConnectTouchingResult {\n const { matches, rejected } = findTouchingPorts(scene, options);\n if (matches.length === 0) return { ok: true, scene, added: 0, rejected };\n const next: ScadaScene = { ...scene, connections: [...scene.connections, ...matches.map((m) => ({ id: options.createId(), ...m }))] };\n const result = validateScene(next, options);\n if (!result.ok) return { ok: false, error: { code: result.errors[0].code, message: result.errors[0].message } };\n return { ok: true, scene: result.scene, added: matches.length, rejected };\n}\n"]}
|
package/dist/units.js
CHANGED
|
@@ -10,7 +10,7 @@ const conversions = {
|
|
|
10
10
|
/** True when `unit` (or no unit, meaning canonical) is accepted for `quantity`. */
|
|
11
11
|
export function isKnownUnit(quantity, unit) {
|
|
12
12
|
// Own string keys only: host data is untrusted, and `in` on a non-string key can throw or match inherited names.
|
|
13
|
-
return unit === undefined || (typeof unit === 'string' && Object.hasOwn(conversions[quantity], unit));
|
|
13
|
+
return unit === undefined || (typeof unit === 'string' && typeof quantity === 'string' && Object.hasOwn(conversions, quantity) && Object.hasOwn(conversions[quantity], unit));
|
|
14
14
|
}
|
|
15
15
|
/** Converts a value in `unit` (default canonical) to the canonical unit, or undefined if the unit is not accepted. */
|
|
16
16
|
export function toCanonical(quantity, value, unit) {
|
package/dist/units.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"units.js","sourceRoot":"","sources":["../src/units.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAuC,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAGjI,MAAM,IAAI,GAAe,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAElC,MAAM,WAAW,GAAiD;IAChE,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;IACtB,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE;IACxI,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE;CAC7L,CAAC;AAEF,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,QAAkB,EAAE,IAAwB;IACtE,iHAAiH;IACjH,OAAO,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"units.js","sourceRoot":"","sources":["../src/units.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAuC,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAGjI,MAAM,IAAI,GAAe,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAElC,MAAM,WAAW,GAAiD;IAChE,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;IACtB,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE;IACxI,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE;CAC7L,CAAC;AAEF,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,QAAkB,EAAE,IAAwB;IACtE,iHAAiH;IACjH,OAAO,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAChL,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,WAAW,CAAC,QAAkB,EAAE,KAAa,EAAE,IAAa;IAC1E,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACrC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACnD,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["import type { Quantity } from './scene.js';\n\n/** Canonical unit per quantity. Displayed and resolved values always use these. */\nexport const canonicalUnits: Readonly<Record<Quantity, string>> = { percent: '%', temperature: '°C', boolean: '', flow: 'm³/s' };\n\ntype Conversion = (value: number) => number;\nconst same: Conversion = (v) => v;\n\nconst conversions: Record<Quantity, Record<string, Conversion>> = {\n percent: { '%': same },\n temperature: { '°C': same, degC: same, C: same, K: (v) => v - 273.15, '°F': (v) => ((v - 32) * 5) / 9, degF: (v) => ((v - 32) * 5) / 9 },\n boolean: {},\n flow: { 'm³/s': same, 'm3/s': same, 'm³/h': (v) => v / 3600, 'm3/h': (v) => v / 3600, 'L/s': (v) => v / 1000, 'l/s': (v) => v / 1000, 'L/min': (v) => v / 60000, 'l/min': (v) => v / 60000 },\n};\n\n/** True when `unit` (or no unit, meaning canonical) is accepted for `quantity`. */\nexport function isKnownUnit(quantity: Quantity, unit: string | undefined): boolean {\n // Own string keys only: host data is untrusted, and `in` on a non-string key can throw or match inherited names.\n return unit === undefined || (typeof unit === 'string' && typeof quantity === 'string' && Object.hasOwn(conversions, quantity) && Object.hasOwn(conversions[quantity], unit));\n}\n\n/** Converts a value in `unit` (default canonical) to the canonical unit, or undefined if the unit is not accepted. */\nexport function toCanonical(quantity: Quantity, value: number, unit?: string): number | undefined {\n if (unit === undefined) return value;\n if (!isKnownUnit(quantity, unit)) return undefined;\n return conversions[quantity][unit](value);\n}\n"]}
|
package/dist/upgrade.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ComponentInstance,
|
|
2
|
-
type Resolver = (type:
|
|
1
|
+
import type { ComponentInstance, ComponentType, ConnectionPort, ScadaScene } from './scene.js';
|
|
2
|
+
type Resolver = (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
|
|
3
3
|
export interface UpgradeOptions {
|
|
4
4
|
/** Ports with the current dimensions. */
|
|
5
5
|
resolvePorts: Resolver;
|