@aryacode/dsh-engineering 0.4.22 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/README.md +10 -7
- package/cordis.patch.yml +1 -1
- package/entries/netlogic.js +266 -0
- package/entries/optix.js +13 -0
- package/package.json +3 -2
- package/vendor/ftview/SKILL.md +6 -0
- package/vendor/ftview/lib/model.d.ts +8 -3
- package/vendor/ftview/lib/model.js +63 -16
- package/vendor/l5x/lib/engine/analysis.d.ts +9 -1
- package/vendor/l5x/lib/engine/analysis.js +28 -3
- package/vendor/l5x/lib/engine/change-set.js +5 -4
- package/vendor/l5x/lib/engine/cip-request.d.ts +1 -0
- package/vendor/l5x/lib/engine/cip-request.js +71 -26
- package/vendor/l5x/lib/engine/direct-polling.js +12 -6
- package/vendor/l5x/lib/engine/review.js +12 -1
- package/vendor/l5x/lib/engine/studio-sdk.js +24 -6
- package/vendor/mwi/code/mw_code.py +4 -1
- package/vendor/mwi/legacy/native-variables.js +9 -1
- package/vendor/mwi/lib/read/service.d.ts +7 -1
- package/vendor/mwi/lib/read/service.js +13 -1
- package/vendor/mwi/lib/session/service.js +7 -1
- package/vendor/optix/SKILL.md +68 -0
- package/vendor/optix/netlogic/SOURCE.json +12 -0
- package/vendor/optix/netlogic/index.json +1987 -0
- package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
- package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
- package/vendor/optix/netlogic/pages/alarming.md +489 -0
- package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
- package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
- package/vendor/optix/netlogic/pages/colors.md +122 -0
- package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
- package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
- package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
- package/vendor/optix/netlogic/pages/datagrids.md +123 -0
- package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
- package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
- package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
- package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
- package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
- package/vendor/optix/netlogic/pages/element-access.md +1208 -0
- package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
- package/vendor/optix/netlogic/pages/events.md +340 -0
- package/vendor/optix/netlogic/pages/execute-command.md +202 -0
- package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
- package/vendor/optix/netlogic/pages/good-practices.md +50 -0
- package/vendor/optix/netlogic/pages/information-model.md +89 -0
- package/vendor/optix/netlogic/pages/log-output.md +22 -0
- package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
- package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
- package/vendor/optix/netlogic/pages/mqtt.md +240 -0
- package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
- package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
- package/vendor/optix/netlogic/pages/opcua.md +325 -0
- package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
- package/vendor/optix/netlogic/pages/queries.md +1297 -0
- package/vendor/optix/netlogic/pages/random-generation.md +90 -0
- package/vendor/optix/netlogic/pages/recipes.md +375 -0
- package/vendor/optix/netlogic/pages/recipex.md +2383 -0
- package/vendor/optix/netlogic/pages/regex.md +15 -0
- package/vendor/optix/netlogic/pages/register-observers.md +346 -0
- package/vendor/optix/netlogic/pages/reports.md +141 -0
- package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
- package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
- package/vendor/optix/netlogic/pages/serial-port.md +357 -0
- package/vendor/optix/netlogic/pages/sessions.md +58 -0
- package/vendor/optix/netlogic/pages/system-node.md +153 -0
- package/vendor/optix/netlogic/pages/template-library.md +127 -0
- package/vendor/optix/netlogic/pages/threading.md +236 -0
- package/vendor/optix/netlogic/pages/translations.md +417 -0
- package/vendor/optix/netlogic/pages/trends.md +128 -0
- package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
- package/vendor/optix/netlogic/pages/users-groups.md +416 -0
- package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
- package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
- package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Engineering Units
|
|
2
|
+
|
|
3
|
+
Engineering units are exposed through the `EngineeringUnits` property (`EUInformation`) on analog variables (or any variable where this property was added).
|
|
4
|
+
|
|
5
|
+
When working with Engineering Units in FactoryTalk Optix, you can define custom units in an `EngineeringUnitDictionary` variable and reference them from the `EUInformation` of your variables. Alternatively, you can directly assign standard OPC UA engineering units by setting the appropriate `UnitId`.
|
|
6
|
+
|
|
7
|
+
AnalogVariables are not required to have engineering units, but when they do, it allows clients to understand the physical meaning of the variable's value and display it accordingly.
|
|
8
|
+
|
|
9
|
+
## EngineeringUnitDictionary
|
|
10
|
+
|
|
11
|
+
### Create an EngineeringUnitDictionary
|
|
12
|
+
|
|
13
|
+
The following example shows how to create (or overwrite) an `EngineeringUnitDictionary` variable in `Model` and add custom engineering units (including a `Percent` unit with `UnitId = 0`)
|
|
14
|
+
|
|
15
|
+
```csharp
|
|
16
|
+
using FTOptix.Core;
|
|
17
|
+
using FTOptix.NetLogic;
|
|
18
|
+
using UAManagedCore;
|
|
19
|
+
|
|
20
|
+
public class DesignTimeNetLogic1 : BaseNetLogic
|
|
21
|
+
{
|
|
22
|
+
[ExportMethod]
|
|
23
|
+
public void CreateCustomEngineeringUnitDictionary()
|
|
24
|
+
{
|
|
25
|
+
var model = Project.Current.Get("Model");
|
|
26
|
+
if (model == null)
|
|
27
|
+
{
|
|
28
|
+
Log.Warning("DesignTimeNetLogic1", "Model folder not found");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const string dictionaryName = "EngineeringUnitDictionary1";
|
|
33
|
+
|
|
34
|
+
// Recreate the dictionary to keep this example deterministic.
|
|
35
|
+
var existingDictionary = model.GetVariable(dictionaryName);
|
|
36
|
+
existingDictionary?.Delete();
|
|
37
|
+
|
|
38
|
+
var dictionary = InformationModel.MakeVariable(
|
|
39
|
+
dictionaryName,
|
|
40
|
+
FTOptix.Core.DataTypes.EngineeringUnitDictionaryItem,
|
|
41
|
+
FTOptix.Core.VariableTypes.EngineeringUnitDictionary,
|
|
42
|
+
new uint[] { 0 });
|
|
43
|
+
|
|
44
|
+
// UnitId values are project-defined for custom units.
|
|
45
|
+
// Percent example: UnitId = 0.
|
|
46
|
+
// Use LocalizedText(text, localeId) to populate Text/LocaleId.
|
|
47
|
+
// LocalizedText("...") alone sets TextId, not Text.
|
|
48
|
+
var units = new EngineeringUnitDictionaryItem[]
|
|
49
|
+
{
|
|
50
|
+
new EngineeringUnitDictionaryItem
|
|
51
|
+
{
|
|
52
|
+
UnitId = 0,
|
|
53
|
+
DisplayName = new LocalizedText("%", "en-US"),
|
|
54
|
+
Description = new LocalizedText("Percentage", "en-US"),
|
|
55
|
+
PhysicalDimension = PhysicalDimension.None,
|
|
56
|
+
Slope = 0.0,
|
|
57
|
+
Intercept = 0.0
|
|
58
|
+
},
|
|
59
|
+
new EngineeringUnitDictionaryItem
|
|
60
|
+
{
|
|
61
|
+
UnitId = 10,
|
|
62
|
+
DisplayName = new LocalizedText("bpm", "en-US"),
|
|
63
|
+
Description = new LocalizedText("Beats per minute", "en-US"),
|
|
64
|
+
PhysicalDimension = PhysicalDimension.Frequency,
|
|
65
|
+
Slope = 60.0,
|
|
66
|
+
Intercept = 0.0
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
dictionary.Value = units;
|
|
71
|
+
model.Add(dictionary);
|
|
72
|
+
|
|
73
|
+
Log.Info("DesignTimeNetLogic1", $"Created {dictionaryName} with {units.Length} custom units");
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Read an EngineeringUnitDictionary
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
using FTOptix.Core;
|
|
81
|
+
using FTOptix.NetLogic;
|
|
82
|
+
using UAManagedCore;
|
|
83
|
+
|
|
84
|
+
public class DesignTimeNetLogic1 : BaseNetLogic
|
|
85
|
+
{
|
|
86
|
+
[ExportMethod]
|
|
87
|
+
public void PrintCustomEngineeringUnitDictionary()
|
|
88
|
+
{
|
|
89
|
+
var dictionary = Project.Current.GetVariable("Model/EngineeringUnitDictionary1");
|
|
90
|
+
if (dictionary == null)
|
|
91
|
+
{
|
|
92
|
+
Log.Warning("DesignTimeNetLogic1", "EngineeringUnitDictionary1 not found");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (dictionary.Value.Value is EngineeringUnitDictionaryItem[] typedUnits)
|
|
97
|
+
{
|
|
98
|
+
foreach (var unit in typedUnits)
|
|
99
|
+
{
|
|
100
|
+
Log.Info(
|
|
101
|
+
"DesignTimeNetLogic1",
|
|
102
|
+
$"UnitId={unit.UnitId}, DisplayName={unit.DisplayName}, Description={unit.Description}, " +
|
|
103
|
+
$"PhysicalDimension={unit.PhysicalDimension}, Slope={unit.Slope}, Intercept={unit.Intercept}");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fallback in case runtime returns a raw Struct array.
|
|
110
|
+
if (dictionary.Value.Value is Struct[] rawUnits)
|
|
111
|
+
{
|
|
112
|
+
foreach (var rawUnit in rawUnits)
|
|
113
|
+
{
|
|
114
|
+
var values = rawUnit.Values;
|
|
115
|
+
if (values == null || values.Count < 6)
|
|
116
|
+
continue;
|
|
117
|
+
|
|
118
|
+
Log.Info(
|
|
119
|
+
"DesignTimeNetLogic1",
|
|
120
|
+
$"UnitId={values[0]}, DisplayName={values[1]}, Description={values[2]}, " +
|
|
121
|
+
$"PhysicalDimension={values[3]}, Slope={values[4]}, Intercept={values[5]}");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Log.Warning("DesignTimeNetLogic1", "Engineering unit dictionary has an unexpected value format");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## EngineeringUnit
|
|
133
|
+
|
|
134
|
+
### Read engineering units of a variable
|
|
135
|
+
|
|
136
|
+
The `EngineeringUnits` node can be retrieved from an `AnalogItem` (or from a variable that contains this property) and inspected through `EUInformation` fields such as `UnitId`, `NamespaceIndex`, `DisplayName`, and `Description`.
|
|
137
|
+
|
|
138
|
+
```csharp
|
|
139
|
+
using FTOptix.Core;
|
|
140
|
+
using FTOptix.NetLogic;
|
|
141
|
+
using UAManagedCore;
|
|
142
|
+
|
|
143
|
+
public class RuntimeNetLogic1 : BaseNetLogic
|
|
144
|
+
{
|
|
145
|
+
[ExportMethod]
|
|
146
|
+
public void GetEUInfoDisplayName()
|
|
147
|
+
{
|
|
148
|
+
var analogVariable = Project.Current.GetVariable("Model/Object1/AnalogVariable1");
|
|
149
|
+
if (analogVariable == null)
|
|
150
|
+
{
|
|
151
|
+
Log.Warning("RuntimeNetLogic1", "Variable not found");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var euInfo = analogVariable.Get<EUInformation>("EngineeringUnits");
|
|
156
|
+
if (euInfo == null)
|
|
157
|
+
{
|
|
158
|
+
Log.Warning("RuntimeNetLogic1", "EngineeringUnits property not found");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var translation = InformationModel.LookupTranslation(euInfo.DisplayName);
|
|
163
|
+
Log.Info("RuntimeNetLogic1", $"EU DisplayName: {translation.Text}");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Assign engineering units to a variable
|
|
169
|
+
|
|
170
|
+
The engineering unit follows the OPC UA model and is identified by a `UnitId`.
|
|
171
|
+
|
|
172
|
+
#### Standard engineering units
|
|
173
|
+
|
|
174
|
+
Reference: [OPC UA Engineering Units specification](https://reference.opcfoundation.org/Core/Part8/v104/docs/5.6.3)
|
|
175
|
+
|
|
176
|
+
> [!WARNING]
|
|
177
|
+
> - It is mandatory to set `UnitId` when generating or updating `EUInformation`.
|
|
178
|
+
> - Not all `UnitId` from the OPC UA specification are mapped into FactoryTalk Optix
|
|
179
|
+
|
|
180
|
+
For standard OPC UA units, set `UnitId` only.
|
|
181
|
+
|
|
182
|
+
Example below assigns **degree Celsius** (`UnitId = 4408652` from OPC UA Part 8):
|
|
183
|
+
|
|
184
|
+
```csharp
|
|
185
|
+
using FTOptix.Core;
|
|
186
|
+
using FTOptix.NetLogic;
|
|
187
|
+
using UAManagedCore;
|
|
188
|
+
using OpcUa = UAManagedCore.OpcUa;
|
|
189
|
+
|
|
190
|
+
public class DesignTimeNetLogic1 : BaseNetLogic
|
|
191
|
+
{
|
|
192
|
+
[ExportMethod]
|
|
193
|
+
public void AssignCelsiusUnit()
|
|
194
|
+
{
|
|
195
|
+
var targetVariable = Project.Current.GetVariable("Model/AnalogVariable1");
|
|
196
|
+
if (targetVariable == null)
|
|
197
|
+
{
|
|
198
|
+
Log.Info("DesignTimeNetLogic1", "Target variable not found");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const int celsiusUnitId = 4408652;
|
|
203
|
+
SetStandardEngineeringUnitById(targetVariable, celsiusUnitId);
|
|
204
|
+
Log.Info("DesignTimeNetLogic1", $"Assigned Celsius (UnitId={celsiusUnitId}) to {targetVariable.BrowseName}");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private void SetStandardEngineeringUnitById(IUAVariable targetVariable, int euId)
|
|
208
|
+
{
|
|
209
|
+
var euInfo = targetVariable.GetVariable("EngineeringUnits");
|
|
210
|
+
if (euInfo == null)
|
|
211
|
+
{
|
|
212
|
+
euInfo = InformationModel.MakeVariable<EUInformation>("EngineeringUnits", OpcUa.DataTypes.EUInformation);
|
|
213
|
+
euInfo.QualifiedBrowseName = new QualifiedName(0, "EngineeringUnits");
|
|
214
|
+
euInfo.ModellingRule = NamingRuleType.Mandatory;
|
|
215
|
+
euInfo.SetModellingRuleRecursive();
|
|
216
|
+
targetVariable.Refs.AddReference(OpcUa.ReferenceTypes.HasProperty, euInfo);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
euInfo.GetVariable("UnitId").Value = euId;
|
|
220
|
+
|
|
221
|
+
// NamespaceIndex is not required for standard OPC UA units.
|
|
222
|
+
var namespaceIndexVariable = euInfo.GetVariable("NamespaceIndex");
|
|
223
|
+
namespaceIndexVariable?.Delete();
|
|
224
|
+
|
|
225
|
+
if (euInfo.ModellingRule != NamingRuleType.Mandatory)
|
|
226
|
+
{
|
|
227
|
+
euInfo.ModellingRule = NamingRuleType.Mandatory;
|
|
228
|
+
euInfo.SetModellingRuleRecursive();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Custom engineering units from an EngineeringUnitDictionary
|
|
235
|
+
|
|
236
|
+
For custom units defined in an `EngineeringUnitDictionary`, set:
|
|
237
|
+
|
|
238
|
+
1. `UnitId` (the custom ID in your dictionary item)
|
|
239
|
+
2. `NamespaceIndex` (the namespace index of the dictionary variable)
|
|
240
|
+
|
|
241
|
+
If `NamespaceIndex` is missing, custom units may not resolve correctly.
|
|
242
|
+
|
|
243
|
+
Example below assigns a custom unit called **Percent** (`UnitId = 0`) from `EngineeringUnitDictionary1`:
|
|
244
|
+
|
|
245
|
+
```csharp
|
|
246
|
+
using FTOptix.Core;
|
|
247
|
+
using FTOptix.NetLogic;
|
|
248
|
+
using UAManagedCore;
|
|
249
|
+
using OpcUa = UAManagedCore.OpcUa;
|
|
250
|
+
|
|
251
|
+
public class DesignTimeNetLogic1 : BaseNetLogic
|
|
252
|
+
{
|
|
253
|
+
[ExportMethod]
|
|
254
|
+
public void AssignPercentUnit()
|
|
255
|
+
{
|
|
256
|
+
// This method assumes a custom unit, called "Percent" with UnitId=0 is defined in the EngineeringUnitDictionary. The dictionary should have the same NamespaceIndex of the project
|
|
257
|
+
|
|
258
|
+
var targetVariable = Project.Current.GetVariable("Model/AnalogVariable1");
|
|
259
|
+
if (targetVariable == null)
|
|
260
|
+
{
|
|
261
|
+
Log.Info("DesignTimeNetLogic1", "Target variable not found");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
var euDictionary = Project.Current.GetVariable("Model/EngineeringUnitDictionary1");
|
|
266
|
+
var customNamespaceIndex = euDictionary?.NodeId.NamespaceIndex ?? targetVariable.NodeId.NamespaceIndex;
|
|
267
|
+
|
|
268
|
+
// Here is where the UnitId should be passed as an argument,
|
|
269
|
+
// UnitId is displayed in the IDE after opening the custom EngineeringUnitDictionary
|
|
270
|
+
SetEngineeringUnitById(targetVariable, 0u, true, customNamespaceIndex);
|
|
271
|
+
Log.Info("DesignTimeNetLogic1", $"Assigned UnitId=0 to {targetVariable.BrowseName} (NamespaceIndex={customNamespaceIndex})");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private void SetEngineeringUnitById(IUAVariable targetVariable, uint euId, bool isCustomEngineeringUnit, int customNamespaceIndex)
|
|
275
|
+
{
|
|
276
|
+
var euInfo = targetVariable.GetVariable("EngineeringUnits");
|
|
277
|
+
if (euInfo == null)
|
|
278
|
+
{
|
|
279
|
+
euInfo = InformationModel.MakeVariable<EUInformation>("EngineeringUnits", OpcUa.DataTypes.EUInformation);
|
|
280
|
+
euInfo.QualifiedBrowseName = new QualifiedName(0, "EngineeringUnits");
|
|
281
|
+
euInfo.ModellingRule = NamingRuleType.Mandatory;
|
|
282
|
+
euInfo.SetModellingRuleRecursive();
|
|
283
|
+
targetVariable.Refs.AddReference(OpcUa.ReferenceTypes.HasProperty, euInfo);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
euInfo.GetVariable("UnitId").Value = (int)euId;
|
|
287
|
+
|
|
288
|
+
if (isCustomEngineeringUnit)
|
|
289
|
+
euInfo.GetOrCreateVariable("NamespaceIndex").Value = customNamespaceIndex;
|
|
290
|
+
|
|
291
|
+
if (euInfo.ModellingRule != NamingRuleType.Mandatory)
|
|
292
|
+
{
|
|
293
|
+
euInfo.ModellingRule = NamingRuleType.Mandatory;
|
|
294
|
+
euInfo.SetModellingRuleRecursive();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Events handling
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This section is a work in progress and uses some low-level APIs.
|
|
5
|
+
> The documentation will be completed as soon as possible.
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> It is recommended to add events to the InformationModel at design time, and then trigger them at runtime, instead of creating and dispatching events fully at runtime.
|
|
9
|
+
|
|
10
|
+
## Add MouseClick event to a Button
|
|
11
|
+
|
|
12
|
+
This code adds a GeneratePDF method to a button, and a SetVariableValue to a different button
|
|
13
|
+
|
|
14
|
+
```csharp
|
|
15
|
+
#region Using directives
|
|
16
|
+
using System;
|
|
17
|
+
using System.Collections.Generic;
|
|
18
|
+
using System.Linq;
|
|
19
|
+
using UAManagedCore;
|
|
20
|
+
using OpcUa = UAManagedCore.OpcUa;
|
|
21
|
+
using FTOptix.HMIProject;
|
|
22
|
+
using FTOptix.UI;
|
|
23
|
+
using FTOptix.Core;
|
|
24
|
+
using FTOptix.CoreBase;
|
|
25
|
+
using FTOptix.NetLogic;
|
|
26
|
+
#endregion
|
|
27
|
+
|
|
28
|
+
public class DesignTimeNetLogic1 : BaseNetLogic
|
|
29
|
+
{
|
|
30
|
+
[ExportMethod]
|
|
31
|
+
public void CreateEventHandlers()
|
|
32
|
+
{
|
|
33
|
+
var mainWindow = Project.Current.Get<WindowType>("UI/MainWindow");
|
|
34
|
+
var generateReportButton = mainWindow.Get<Button>("GenerateReportButton");
|
|
35
|
+
var changeColorButton = mainWindow.Get<Button>("ChangeColorButton");
|
|
36
|
+
var button1 = mainWindow.Get<Button>("Button1");
|
|
37
|
+
var button1BackgroundColor = button1.BackgroundColorVariable;
|
|
38
|
+
|
|
39
|
+
// Make sure to remove old handlers (if any)
|
|
40
|
+
var oldEventHandlers = generateReportButton.Children.OfType<FTOptix.CoreBase.EventHandler>().ToList();
|
|
41
|
+
oldEventHandlers.AddRange(changeColorButton.Children.OfType<FTOptix.CoreBase.EventHandler>());
|
|
42
|
+
foreach (var oldEventHandler in oldEventHandlers)
|
|
43
|
+
oldEventHandler.Delete();
|
|
44
|
+
|
|
45
|
+
// Report object that we want to create with a button
|
|
46
|
+
var report1 = Project.Current.GetObject("Reports/Report1");
|
|
47
|
+
|
|
48
|
+
MakeEventHandler( // Add the GeneratePdf method to the button
|
|
49
|
+
generateReportButton, // Object where to create the method
|
|
50
|
+
FTOptix.UI.ObjectTypes.MouseClickEvent, // Method to be created
|
|
51
|
+
report1, // Object pointer where to call the method
|
|
52
|
+
"GeneratePdf", // Method name
|
|
53
|
+
new List<Tuple<string, NodeId, object>> // Method arguments
|
|
54
|
+
{
|
|
55
|
+
// Each row is a tuple, each tuple contains a string (name of the argument), a NodeId (DataType) and an Object (value)
|
|
56
|
+
new("OutputPath", FTOptix.Core.DataTypes.ResourceUri, (string)ResourceUri.FromApplicationRelativePath("MyReport.pdf")),
|
|
57
|
+
new("LocaleId", OpcUa.DataTypes.String, "en-US")
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
MakeEventHandler( // Add the SetVariableValue method to the button
|
|
62
|
+
changeColorButton, // Object where to create the method
|
|
63
|
+
FTOptix.UI.ObjectTypes.MouseClickEvent, // Event to be added
|
|
64
|
+
InformationModel.GetObject(FTOptix.CoreBase.Objects.VariableCommands), // Object that exposes the method
|
|
65
|
+
"Set", // Name of the method
|
|
66
|
+
new List<Tuple<string, NodeId, object>> // Method arguments
|
|
67
|
+
{
|
|
68
|
+
// Each row is a tuple, each tuple contains a string (name of the argument), a NodeId (DataType) and an Object (value)
|
|
69
|
+
new("VariableToModify", FTOptix.Core.DataTypes.VariablePointer, NodeId.Empty),
|
|
70
|
+
new("Value", FTOptix.Core.DataTypes.Color, new Color(0xff3480ebu).ARGB),
|
|
71
|
+
new("ArrayIndex", OpcUa.DataTypes.UInt32, 0u)
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Set which variable to be modified by the Set method
|
|
76
|
+
var changeColorButtonEventHandler = changeColorButton.Get("EventHandler");
|
|
77
|
+
// This path can be different depending on the MethodContainer name assigned by the MakeEventHandler method
|
|
78
|
+
var variableToModifyArgumentVariable = changeColorButtonEventHandler.GetVariable("MethodsToCall/MethodContainer1/InputArguments/VariableToModify");
|
|
79
|
+
// Assign a DynamicLink to the VariableToModify
|
|
80
|
+
variableToModifyArgumentVariable.SetDynamicLink(button1BackgroundColor);
|
|
81
|
+
// Change the DynamicLink to make sure the NodeId attribute is used (IDE has a fallback to the @Value otherwise)
|
|
82
|
+
var dynamicLink = variableToModifyArgumentVariable.Children.OfType<DynamicLink>().First();
|
|
83
|
+
dynamicLink.Value = dynamicLink.Value + "@NodeId";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private FTOptix.CoreBase.EventHandler MakeEventHandler(
|
|
87
|
+
IUANode parentNode, // The parent node to which the event handler is to be added
|
|
88
|
+
NodeId listenEventTypeId, // The NodeID of the event to be listened
|
|
89
|
+
IUAObject callingObject, // The object on which the method is to be called
|
|
90
|
+
string methodName, // The name of the method to be called
|
|
91
|
+
List<Tuple<string, NodeId, object>> arguments = null // List of input arguments (name, data type NodeID, value)
|
|
92
|
+
)
|
|
93
|
+
{
|
|
94
|
+
// Create event handler object
|
|
95
|
+
var eventHandler = InformationModel.MakeObject<FTOptix.CoreBase.EventHandler>("EventHandler");
|
|
96
|
+
parentNode.Add(eventHandler);
|
|
97
|
+
|
|
98
|
+
// Set the ListenEventType variable value to the Node ID of the event to be listened
|
|
99
|
+
eventHandler.ListenEventType = listenEventTypeId;
|
|
100
|
+
|
|
101
|
+
// Create method container
|
|
102
|
+
// This must be an unique name if the multiple methods are added to the same button. A random name can also be used
|
|
103
|
+
var methodIndex = eventHandler.MethodsToCall.Any() ? eventHandler.MethodsToCall.Count + 1 : 1;
|
|
104
|
+
var methodContainer = InformationModel.MakeObject($"MethodContainer{methodIndex}");
|
|
105
|
+
eventHandler.MethodsToCall.Add(methodContainer);
|
|
106
|
+
|
|
107
|
+
// Create the ObjectPointer variable and set its value to the object on which the method is to be called
|
|
108
|
+
var objectPointerVariable = InformationModel.MakeVariable<NodePointer>("ObjectPointer", OpcUa.DataTypes.NodeId);
|
|
109
|
+
objectPointerVariable.Value = callingObject.NodeId;
|
|
110
|
+
methodContainer.Add(objectPointerVariable);
|
|
111
|
+
|
|
112
|
+
// Create the Method variable and set its value to the name of the method to be called
|
|
113
|
+
var methodNameVariable = InformationModel.MakeVariable("Method", OpcUa.DataTypes.String);
|
|
114
|
+
methodNameVariable.Value = methodName;
|
|
115
|
+
methodContainer.Add(methodNameVariable);
|
|
116
|
+
|
|
117
|
+
if (arguments != null)
|
|
118
|
+
CreateInputArguments(methodContainer, arguments);
|
|
119
|
+
|
|
120
|
+
return eventHandler;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private void CreateInputArguments(
|
|
124
|
+
IUANode methodContainer,
|
|
125
|
+
List<Tuple<string, NodeId, object>> arguments)
|
|
126
|
+
{
|
|
127
|
+
IUAObject inputArguments = InformationModel.MakeObject("InputArguments");
|
|
128
|
+
methodContainer.Add(inputArguments);
|
|
129
|
+
|
|
130
|
+
foreach (var arg in arguments)
|
|
131
|
+
{
|
|
132
|
+
var argumentVariable = inputArguments.Context.NodeFactory.MakeVariable(
|
|
133
|
+
NodeId.Random(inputArguments.NodeId.NamespaceIndex),
|
|
134
|
+
arg.Item1,
|
|
135
|
+
arg.Item2,
|
|
136
|
+
OpcUa.VariableTypes.BaseDataVariableType,
|
|
137
|
+
false,
|
|
138
|
+
arg.Item3);
|
|
139
|
+
|
|
140
|
+
inputArguments.Add(argumentVariable);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Add a method handler to an object
|
|
148
|
+
|
|
149
|
+
```csharp
|
|
150
|
+
#region Using directives
|
|
151
|
+
|
|
152
|
+
using System;
|
|
153
|
+
using System.Collections.Generic;
|
|
154
|
+
using System.Linq;
|
|
155
|
+
using FTOptix.Core;
|
|
156
|
+
using FTOptix.CoreBase;
|
|
157
|
+
using FTOptix.HMIProject;
|
|
158
|
+
using FTOptix.NetLogic;
|
|
159
|
+
using UAManagedCore;
|
|
160
|
+
using OpcUa = UAManagedCore.OpcUa;
|
|
161
|
+
|
|
162
|
+
#endregion
|
|
163
|
+
|
|
164
|
+
public class CreateEventHandler : BaseNetLogic
|
|
165
|
+
{
|
|
166
|
+
private FTOptix.CoreBase.EventHandler MakeEventHandler(
|
|
167
|
+
IUANode parentNode, // The parent node to which the event handler is to be added
|
|
168
|
+
NodeId listenEventTypeId, // The NodeID of the event to be listened
|
|
169
|
+
IUAObject callingObject, // The object on which the method is to be called
|
|
170
|
+
string methodName, // The name of the method to be called
|
|
171
|
+
List<Tuple<string, NodeId, object>> arguments = null // List of input arguments (name, data type NodeID, value)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
// Create event handler object
|
|
176
|
+
var eventHandler = InformationModel.MakeObject<FTOptix.CoreBase.EventHandler>("EventHandler");
|
|
177
|
+
parentNode.Add(eventHandler);
|
|
178
|
+
|
|
179
|
+
// Set the ListenEventType variable value to the Node ID of the event to be listened
|
|
180
|
+
eventHandler.ListenEventType = listenEventTypeId;
|
|
181
|
+
|
|
182
|
+
// Create method container
|
|
183
|
+
var methodContainer = InformationModel.MakeObject("MethodContainer1");
|
|
184
|
+
eventHandler.MethodsToCall.Add(methodContainer);
|
|
185
|
+
|
|
186
|
+
// Create the ObjectPointer variable and set its value to the object on which the method is to be called
|
|
187
|
+
var objectPointerVariable = InformationModel.MakeVariable<NodePointer>("ObjectPointer", OpcUa.DataTypes.NodeId);
|
|
188
|
+
objectPointerVariable.Value = callingObject.NodeId;
|
|
189
|
+
string resultPath = CreateRelativePath(parentNode, callingObject);
|
|
190
|
+
var isCore = resultPath.Contains("Root");
|
|
191
|
+
if (!isCore)
|
|
192
|
+
{
|
|
193
|
+
IUAVariable temp = null;
|
|
194
|
+
objectPointerVariable.SetDynamicLink(temp, DynamicLinkMode.ReadWrite);
|
|
195
|
+
var dynamicLinkVar = objectPointerVariable.Refs.GetVariable(FTOptix.CoreBase.ReferenceTypes.HasDynamicLink) as IUAVariable;
|
|
196
|
+
if (dynamicLinkVar != null)
|
|
197
|
+
{
|
|
198
|
+
dynamicLinkVar.Value = "../../../../" + resultPath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
methodContainer.Add(objectPointerVariable);
|
|
203
|
+
|
|
204
|
+
// Create the Method variable and set its value to the name of the method to be called
|
|
205
|
+
var methodNameVariable = InformationModel.MakeVariable("Method", OpcUa.DataTypes.String);
|
|
206
|
+
methodNameVariable.Value = methodName;
|
|
207
|
+
methodContainer.Add(methodNameVariable);
|
|
208
|
+
|
|
209
|
+
if (arguments != null)
|
|
210
|
+
CreateInputArguments(methodContainer, arguments);
|
|
211
|
+
|
|
212
|
+
return eventHandler;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private void CreateInputArguments(
|
|
216
|
+
IUANode methodContainer,
|
|
217
|
+
List<Tuple<string, NodeId, object>> arguments)
|
|
218
|
+
{
|
|
219
|
+
IUAObject inputArguments = InformationModel.MakeObject("InputArguments");
|
|
220
|
+
methodContainer.Add(inputArguments);
|
|
221
|
+
|
|
222
|
+
foreach (var arg in arguments)
|
|
223
|
+
{
|
|
224
|
+
var argumentVariable = inputArguments.Context.NodeFactory.MakeVariable(
|
|
225
|
+
NodeId.Random(inputArguments.NodeId.NamespaceIndex),
|
|
226
|
+
arg.Item1,
|
|
227
|
+
arg.Item2,
|
|
228
|
+
OpcUa.VariableTypes.BaseDataVariableType,
|
|
229
|
+
false,
|
|
230
|
+
arg.Item3);
|
|
231
|
+
|
|
232
|
+
inputArguments.Add(argumentVariable);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private static string CreateRelativePath(IUANode source, IUANode target)
|
|
237
|
+
{
|
|
238
|
+
string result = "";
|
|
239
|
+
var srcPath = GetCurrentProjectBrowsePath(source);
|
|
240
|
+
var tarPath = GetCurrentProjectBrowsePath(target);
|
|
241
|
+
var srcArr = srcPath.Split("/");
|
|
242
|
+
var tarArr = tarPath.Split("/");
|
|
243
|
+
var max = Math.Max(srcPath.Count(), tarPath.Count());
|
|
244
|
+
int i = 0;
|
|
245
|
+
for (; i < max; i++)
|
|
246
|
+
{
|
|
247
|
+
result = "..";
|
|
248
|
+
if (srcArr[i] != tarArr[i])
|
|
249
|
+
{
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (int j = srcArr.Count() - 1; j > i; j--)
|
|
254
|
+
{
|
|
255
|
+
result = result + "/..";
|
|
256
|
+
}
|
|
257
|
+
for (int k = i; k < tarArr.Count(); k++)
|
|
258
|
+
{
|
|
259
|
+
result = result + "/" + tarArr[k];
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private static string GetCurrentProjectBrowsePath(IUANode node)
|
|
265
|
+
{
|
|
266
|
+
if (node.Owner == Project.Current || node.BrowseName == "Root")
|
|
267
|
+
return node.BrowseName;
|
|
268
|
+
return GetCurrentProjectBrowsePath(node.Owner) + "/" + node.BrowseName;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Subscribe to OPCUA methods
|
|
274
|
+
|
|
275
|
+
This script can be put anywhere on the project and will listen to every OPCUA method being called (script or UI)
|
|
276
|
+
|
|
277
|
+
> [!NOTE]
|
|
278
|
+
> In FactoryTalk Optix, any method is mapped to an OPCUA method, so even when calling a NetLogic method, or a core command, this script will intercept the call.
|
|
279
|
+
|
|
280
|
+
```csharp
|
|
281
|
+
public class OpcMethodsAuditLogic : BaseNetLogic, IUAEventObserver
|
|
282
|
+
{
|
|
283
|
+
public override void Start()
|
|
284
|
+
{
|
|
285
|
+
var serverObject = LogicObject.Context.GetObject(OpcUa.Objects.Server);
|
|
286
|
+
//eventRegistration = serverObject.RegisterUAEventObserver(this, UAManagedCore.OpcUa.ObjectTypes.AuditUpdateMethodEventType);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public override void Stop()
|
|
290
|
+
{
|
|
291
|
+
// Insert code to be run when the user-defined logic is stopped
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public void OnEvent(IUAObject eventNotifier, IUAObjectType eventType, IReadOnlyList<object> eventData, ulong senderId)
|
|
295
|
+
{
|
|
296
|
+
StringBuilder builder = new StringBuilder();
|
|
297
|
+
builder.Append($"Event of type {eventType.BrowseName} triggered");
|
|
298
|
+
|
|
299
|
+
var eventArguments = eventType.EventArguments;
|
|
300
|
+
foreach (var eventField in eventArguments.GetFields())
|
|
301
|
+
{
|
|
302
|
+
var fieldValue = eventArguments.GetFieldValue(eventData, eventField);
|
|
303
|
+
builder.Append($"\t{eventField} = {fieldValue?.ToString() ?? "null"}");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
builder.Append("\n");
|
|
307
|
+
Log.Info(builder.ToString());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// private IEventRegistration eventRegistration;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Add a VariableChanged event to a variable
|
|
315
|
+
|
|
316
|
+
> [!WARNING]
|
|
317
|
+
> This method can only work at DesignTime
|
|
318
|
+
|
|
319
|
+
```csharp
|
|
320
|
+
[ExportMethod]
|
|
321
|
+
public void AddVariableChangeEvent()
|
|
322
|
+
{
|
|
323
|
+
IUAVariable targetVariable = null;
|
|
324
|
+
try
|
|
325
|
+
{
|
|
326
|
+
targetVariable = InformationModel.GetVariable(LogicObject.GetVariable("VariableToAddChangeMethod").Value);
|
|
327
|
+
}
|
|
328
|
+
catch
|
|
329
|
+
{
|
|
330
|
+
Log.Error("Variable not found");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
var variableOwner = targetVariable.Owner;
|
|
335
|
+
var variableChangedEventDispatcher = InformationModel.Make<VariableChangedEventDispatcher>($"{targetVariable.BrowseName}Changed");
|
|
336
|
+
variableChangedEventDispatcher.GetVariable("VariableNodePath").Value = $"../{targetVariable.BrowseName}";
|
|
337
|
+
|
|
338
|
+
variableOwner.Add(variableChangedEventDispatcher);
|
|
339
|
+
}
|
|
340
|
+
```
|