@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +10 -7
  3. package/cordis.patch.yml +1 -1
  4. package/entries/netlogic.js +266 -0
  5. package/entries/optix.js +13 -0
  6. package/package.json +3 -2
  7. package/vendor/ftview/SKILL.md +6 -0
  8. package/vendor/ftview/lib/model.d.ts +8 -3
  9. package/vendor/ftview/lib/model.js +63 -16
  10. package/vendor/l5x/lib/engine/analysis.d.ts +9 -1
  11. package/vendor/l5x/lib/engine/analysis.js +28 -3
  12. package/vendor/l5x/lib/engine/change-set.js +5 -4
  13. package/vendor/l5x/lib/engine/cip-request.d.ts +1 -0
  14. package/vendor/l5x/lib/engine/cip-request.js +71 -26
  15. package/vendor/l5x/lib/engine/direct-polling.js +12 -6
  16. package/vendor/l5x/lib/engine/review.js +12 -1
  17. package/vendor/l5x/lib/engine/studio-sdk.js +24 -6
  18. package/vendor/mwi/code/mw_code.py +4 -1
  19. package/vendor/mwi/legacy/native-variables.js +9 -1
  20. package/vendor/mwi/lib/read/service.d.ts +7 -1
  21. package/vendor/mwi/lib/read/service.js +13 -1
  22. package/vendor/mwi/lib/session/service.js +7 -1
  23. package/vendor/optix/SKILL.md +68 -0
  24. package/vendor/optix/netlogic/SOURCE.json +12 -0
  25. package/vendor/optix/netlogic/index.json +1987 -0
  26. package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
  27. package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
  28. package/vendor/optix/netlogic/pages/alarming.md +489 -0
  29. package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
  30. package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
  31. package/vendor/optix/netlogic/pages/colors.md +122 -0
  32. package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
  33. package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
  34. package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
  35. package/vendor/optix/netlogic/pages/datagrids.md +123 -0
  36. package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
  37. package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
  38. package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
  39. package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
  40. package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
  41. package/vendor/optix/netlogic/pages/element-access.md +1208 -0
  42. package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
  43. package/vendor/optix/netlogic/pages/events.md +340 -0
  44. package/vendor/optix/netlogic/pages/execute-command.md +202 -0
  45. package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
  46. package/vendor/optix/netlogic/pages/good-practices.md +50 -0
  47. package/vendor/optix/netlogic/pages/information-model.md +89 -0
  48. package/vendor/optix/netlogic/pages/log-output.md +22 -0
  49. package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
  50. package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
  51. package/vendor/optix/netlogic/pages/mqtt.md +240 -0
  52. package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
  53. package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
  54. package/vendor/optix/netlogic/pages/opcua.md +325 -0
  55. package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
  56. package/vendor/optix/netlogic/pages/queries.md +1297 -0
  57. package/vendor/optix/netlogic/pages/random-generation.md +90 -0
  58. package/vendor/optix/netlogic/pages/recipes.md +375 -0
  59. package/vendor/optix/netlogic/pages/recipex.md +2383 -0
  60. package/vendor/optix/netlogic/pages/regex.md +15 -0
  61. package/vendor/optix/netlogic/pages/register-observers.md +346 -0
  62. package/vendor/optix/netlogic/pages/reports.md +141 -0
  63. package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
  64. package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
  65. package/vendor/optix/netlogic/pages/serial-port.md +357 -0
  66. package/vendor/optix/netlogic/pages/sessions.md +58 -0
  67. package/vendor/optix/netlogic/pages/system-node.md +153 -0
  68. package/vendor/optix/netlogic/pages/template-library.md +127 -0
  69. package/vendor/optix/netlogic/pages/threading.md +236 -0
  70. package/vendor/optix/netlogic/pages/translations.md +417 -0
  71. package/vendor/optix/netlogic/pages/trends.md +128 -0
  72. package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
  73. package/vendor/optix/netlogic/pages/users-groups.md +416 -0
  74. package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
  75. package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
  76. package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
@@ -0,0 +1,240 @@
1
+ # MQTT
2
+
3
+ The MQTT (Message Queuing Telemetry Transport) protocol is a lightweight messaging protocol designed for constrained devices and low-bandwidth, high-latency, or unreliable networks. It is widely used in IoT (Internet of Things) applications for its efficiency and simplicity.
4
+
5
+ Starting from FactoryTalk Optix 1.6.X, the MQTT module provides built-in support for MQTT, allowing you to easily integrate MQTT messaging into your applications.
6
+
7
+ > [!NOTE]
8
+ > The MQTT module is only loaded at the Runtime startup, so MQTT features are not available at design time.
9
+
10
+ ## General Concepts about MQTT in FactoryTalk Optix
11
+
12
+ FactoryTalk Optix provides a robust MQTT module that allows you to:
13
+ - Publish and subscribe to topics using the `MQTTClient` and `MQTTPublisher` classes.
14
+ - Manage incoming and outgoing messages efficiently.
15
+ - Dynamically configure MQTT clients and publishers.
16
+
17
+ ## Using MQTT in FactoryTalk Optix
18
+
19
+ ### Managing MQTT Client IDs
20
+
21
+ The `MQTTClient` class allows you to dynamically set the `ClientId` for an MQTT client. This is useful for ensuring unique identifiers for clients in a network.
22
+
23
+ ```csharp
24
+ public override void Start()
25
+ {
26
+ // Retrieve the MQTT client instance from the project
27
+ var mqttClient = Project.Current.Get<MQTTClient>("MQTT/MQTTClient");
28
+
29
+ // Check if the default ClientId is set and update it to a unique value
30
+ if (mqttClient.ClientId == "FTOptix-1")
31
+ {
32
+ mqttClient.ClientId = $"OptixUser-{Guid.NewGuid().ToString().Split("-")[0]}";
33
+
34
+ // Restart the client to apply the new ClientId
35
+ mqttClient.Stop();
36
+ mqttClient.Start();
37
+
38
+ // Log the updated ClientId
39
+ Log.Info("ClientIdGeneratorLogic", $"ClientId was set to {mqttClient.ClientId}");
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Handling Incoming MQTT Messages
45
+
46
+ As the `MQTTSubscriber`, when set to `PlainTextSubscription` allows to read messages from a topic and write the received data to a Model variable, you can use the `VariableChange` event to process incoming MQTT messages. This ensures that your application can react to new messages in real-time.
47
+
48
+ ```csharp
49
+ public override void Start()
50
+ {
51
+ // Get the variable that stores incoming MQTT messages
52
+ mqttIncomingMessage = Project.Current.GetVariable("Model/Data/MQTT/IncomingMessage");
53
+
54
+ // Attach an event handler to process changes in the variable
55
+ mqttIncomingMessage.VariableChange += MqttIncomingMessagePile_VariableChanged;
56
+ }
57
+
58
+ private void MqttIncomingMessagePile_VariableChanged(object sender, VariableChangeEventArgs e)
59
+ {
60
+ // Ensure the new value is not null
61
+ if (e.NewValue == null)
62
+ return;
63
+
64
+ // Retrieve the incoming message as a string
65
+ var incomingMessage = (string)e.NewValue;
66
+
67
+ // Log the received message for debugging purposes
68
+ Log.Debug("MqttChatLogic", $"Received MQTT message: {incomingMessage}");
69
+ }
70
+ ```
71
+
72
+ ### Displaying MQTT Messages in the UI
73
+
74
+ The `ScrollView` and `ColumnLayout` classes can be used to dynamically display incoming MQTT messages in the user interface. These classes are part of the FactoryTalk Optix UI framework and allow developers to create flexible and dynamic layouts for displaying data.
75
+
76
+ - **`ScrollView`**: This is a container that provides scrolling functionality for its child elements. It ensures that the user can view all messages even if the content exceeds the visible area.
77
+ - **`ColumnLayout`**: This layout organizes its child elements vertically, making it ideal for displaying a list of messages.
78
+ - **`MqttChatMessage`**: This is a custom UI element that represents a single MQTT message. It includes variables such as `Content` to store the message text.
79
+
80
+ The following code demonstrates how to use these classes:
81
+
82
+ ```csharp
83
+ private void MqttIncomingMessagePile_VariableChanged(object sender, VariableChangeEventArgs e)
84
+ {
85
+ // Create a new column layout to organize messages vertically
86
+ var newColumnLayout = InformationModel.Make<ColumnLayout>("VerticalLayout");
87
+
88
+ // Retrieve the pile of messages from the variable
89
+ var messagesPile = (string[])mqttIncomingMessagePile.Value.Value;
90
+
91
+ foreach (var message in messagesPile)
92
+ {
93
+ // Skip empty messages
94
+ if (string.IsNullOrEmpty(message))
95
+ continue;
96
+
97
+ // Create a new UI element for the message
98
+ var textBlock = InformationModel.Make<MqttChatMessage>(Guid.NewGuid().ToString());
99
+
100
+ // Deserialize the message content
101
+ var mqttMessage = MqttChatLogic.SampleChatMessage.Deserialize(message);
102
+
103
+ // Set the content of the text block
104
+ textBlock.GetVariable("Content").Value = mqttMessage.Content;
105
+
106
+ // Add the text block to the column layout
107
+ newColumnLayout.Add(textBlock);
108
+ }
109
+
110
+ // Clear the existing layout and add the new one
111
+ scrollView.Get("VerticalLayout")?.Delete();
112
+ scrollView.Add(newColumnLayout);
113
+ }
114
+ ```
115
+
116
+ #### Serialization and Deserialization
117
+
118
+ The `SampleChatMessage` class is used to represent an MQTT message. It includes properties such as `Sender`, `Content`, and `Timestamp`. This class uses JSON serialization to convert the message into a string format for transmission and deserialization to reconstruct the object from the string.
119
+
120
+ - **Serialization**: Converts the object into a JSON string using `JsonSerializer.Serialize`.
121
+ - **Deserialization**: Converts the JSON string back into an object using `JsonSerializer.Deserialize`.
122
+
123
+ > [!NOTE]
124
+ > The MQTT publisher works with any kind of string message as payload, but using serialization allows to structure the data and include multiple fields in a single message and it is widely used in IoT applications. Creating a custom class and using the serialization/deserialization methods makes it easier to manage the message content.
125
+
126
+ ```csharp
127
+ public class SampleChatMessage
128
+ {
129
+ public string Sender { get; }
130
+ public string Content { get; }
131
+ public string Timestamp { get; }
132
+
133
+ [JsonConstructor]
134
+ public SampleChatMessage(string sender, string content, string timestamp)
135
+ {
136
+ this.Sender = sender;
137
+ this.Content = content;
138
+ this.Timestamp = timestamp ?? new DateTime(0).ToString("yyyy/MM/dd HH:mm:ss");
139
+ }
140
+
141
+ public string Serialize()
142
+ {
143
+ return JsonSerializer.Serialize(this);
144
+ }
145
+
146
+ public static SampleChatMessage Deserialize(string json)
147
+ {
148
+ try
149
+ {
150
+ return JsonSerializer.Deserialize<SampleChatMessage>(json);
151
+ }
152
+ catch (JsonException ex)
153
+ {
154
+ Log.Warning("MqttChatLogic", $"Failed to deserialize MQTT message: {ex.Message}, content: {json}");
155
+ return null;
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ ### Sending MQTT Messages
162
+
163
+ You can publish messages to an MQTT topic using the `MQTTPublisher` class. This allows you to send data to other clients or systems. The `MQTTPublisher` is configured with a topic, quality of service (QoS), and retain flag, which determine how the message is delivered.
164
+
165
+ The following code demonstrates how to send a message:
166
+
167
+ ```csharp
168
+ [ExportMethod]
169
+ public void SendMessage(string content)
170
+ {
171
+ // Retrieve the MQTT publisher and client instances
172
+ var mqttPublisher = Project.Current.Get<MQTTPublisher>("MQTT/Mosquitto_MQTTClient/MQTTPublisher");
173
+ var mqttClient = Project.Current.Get<MQTTClient>("MQTT/Mosquitto_MQTTClient");
174
+
175
+ // Create a new MQTT message with the provided content
176
+ var dateTime = DateTime.UtcNow.ToString("yyyy/MM/dd HH:mm:ss");
177
+ var mqttMessage = new SampleChatMessage("ClientId", content, dateTime);
178
+
179
+ // Serialize the MQTT configuration and message payload
180
+ var mqttConfigJson = new OptixMqttConfig(mqttPublisher).Serialize();
181
+ var mqttMessageJson = mqttMessage.Serialize();
182
+
183
+ // Publish the message to the configured topic
184
+ mqttClient.Publish(mqttConfigJson, mqttMessageJson);
185
+ }
186
+ ```
187
+
188
+ #### Why Serialization is Needed
189
+
190
+ Serialization is essential for converting the `SampleChatMessage` object into a format that can be transmitted over the MQTT protocol. The JSON format is lightweight and widely supported, making it ideal for IoT applications. By serializing the message, you ensure that all its properties are preserved during transmission.
191
+
192
+ #### Understanding `OptixMqttConfig`
193
+
194
+ The `OptixMqttConfig` class is a helper class designed to encapsulate the configuration details of an MQTT publisher. It simplifies the process of serializing and transmitting the configuration data required for publishing messages.
195
+
196
+ ```csharp
197
+ public class OptixMqttConfig
198
+ {
199
+ public string Topic { get; }
200
+ public int QoS { get; }
201
+ public bool Retain { get; }
202
+
203
+ public OptixMqttConfig(MQTTPublisher mqttPublisher)
204
+ {
205
+ Topic = mqttPublisher.Topic;
206
+ QoS = (int) mqttPublisher.QoS;
207
+ Retain = mqttPublisher.Retain;
208
+ }
209
+
210
+ public string Serialize()
211
+ {
212
+ return JsonSerializer.Serialize(this);
213
+ }
214
+ }
215
+ ```
216
+
217
+ ##### Key Properties
218
+
219
+ - **`Topic`**: The MQTT topic to which the message will be published. This is a string that acts as the address for the message.
220
+ - **`QoS`**: The Quality of Service level for the message. It determines the guarantee of message delivery
221
+ - **`Retain`**: A boolean flag indicating whether the message should be retained by the broker. Retained messages are delivered to new subscribers immediately upon subscription.
222
+
223
+ ##### Example Usage
224
+
225
+ The `OptixMqttConfig` class is used in the `SendMessage` method to serialize the MQTT publisher's configuration:
226
+
227
+ ```csharp
228
+ var mqttPublisher = Project.Current.Get<MQTTPublisher>("MQTT/Mosquitto_MQTTClient/MQTTPublisher");
229
+ var mqttConfig = new MqttChatLogic.OptixMqttConfig(mqttPublisher);
230
+ var mqttConfigJson = mqttConfig.Serialize();
231
+ ```
232
+
233
+ This serialized configuration is then passed as the first argument to the `Publish` method of the `MQTTClient` class.
234
+
235
+ ## Best Practices for MQTT in FactoryTalk Optix
236
+
237
+ - **Dispose Resources:** Always dispose of MQTT clients and tasks when they are no longer needed to free up resources.
238
+ - **Handle Errors Gracefully:** Use try-catch blocks to handle exceptions during MQTT operations.
239
+ - **Use Unique Client IDs:** Ensure that each MQTT client has a unique `ClientId` to avoid conflicts.
240
+ - **Optimize Message Handling:** Use efficient data structures and algorithms to process incoming messages.
@@ -0,0 +1,270 @@
1
+ # NetLogic Lifecycle
2
+
3
+ Understanding how FactoryTalk Optix manages threads is essential to writing reliable NetLogic code, especially in projects with complex pages, many widgets, or heavy processing requirements.
4
+
5
+ ## Overview
6
+
7
+ FactoryTalk Optix runtime operates with two primary execution contexts that are always present:
8
+
9
+ | Thread | Responsible for |
10
+ |--------|----------------|
11
+ | **UI thread** | Rendering all graphical objects, handling user input events |
12
+ | **Behavior/Core thread** | Calling `Start()` and `Stop()` on all behaviors (including NetLogic), sequentially |
13
+
14
+ These two threads are **independent** from each other. The UI can render while behaviors are being initialized, and vice versa.
15
+
16
+ ## Start() lifecycle
17
+
18
+ ### When is Start() called?
19
+
20
+ `Start()` is invoked by the FactoryTalk Optix runtime when a node (page, object, widget) is activated - either at application startup or when a page is navigated to.
21
+
22
+ The fundamental rule is: **all `Start()` methods are called sequentially, one at a time, on the behavior thread**. There is no parallelism between different NetLogic `Start()` calls.
23
+
24
+ > [!WARNING]
25
+ > The `Start()` method of a Runtime NetLogic should not add or remove nodes from the page tree, as this can cause unpredictable behavior in the loading sequence. If dynamic modification of the tree is needed, it should be done on the page type definition, and then load the page on the session.
26
+
27
+ ### Execution order
28
+
29
+ The runtime determines the execution order based on the priority of the objects type (DynamicLinks, EURange, NetLogic, etc.) in the page tree. The general principle is: **deeper nodes start before their parents**. This allows child components to initialize before their containers, which is usually desirable.
30
+
31
+ The practical outcome for a page where multiple NetLogic exist, is: the most deeply nested NetLogic starts first, and execution proceeds upward through the tree until the page-level NetLogic is last. This is the common case.
32
+
33
+ If multiple NetLogics have the same depth, their order is determined by the tree traversal (i.e., top to bottom as they appear in the project tree), but this cannot be relied upon for deterministic behavior.
34
+
35
+ ```
36
+ Example tree (all same priority):
37
+
38
+ Page (depth 1)
39
+ ├── PanelA (depth 2)
40
+ │ └── NetLogic_A (depth 3) <- starts FIRST
41
+ ├── PanelB (depth 2)
42
+ │ ├── NetLogic_B (depth 3) <- starts SECOND
43
+ │ └── NetLogic_PanelB <- starts THIRD (same depth as PanelB, ordered by tree traversal)
44
+ └── NetLogic_Page (depth 1) <- starts LAST
45
+ ```
46
+
47
+ ### 2-second threshold warning
48
+
49
+ If a `Start()` method takes longer than **2000 ms** to return, the runtime logs a warning:
50
+
51
+ ```text
52
+ Start method on <NodeName> took too long to execute, Long tasks should alternatively be implemented inside a LongRunningTask.
53
+ ```
54
+
55
+ This is not an error - the runtime will wait for `Start()` to finish before moving on to the next behavior. However, until all behaviors have started, any other behavior whose `Start()` is queued will be blocked.
56
+
57
+ > [!WARNING]
58
+ > A slow `Start()` on one NetLogic delays the `Start()` of every other NetLogic that comes after it in the queue. On pages with many components, this can cause a visible freeze during page load.
59
+
60
+ ```csharp
61
+ // BAD: blocking operation in Start() delays everything after it
62
+ public override void Start()
63
+ {
64
+ // This blocks the behavior thread for potentially seconds
65
+ var data = File.ReadAllText("/mnt/slow-nfs/data.csv");
66
+ ProcessData(data);
67
+ }
68
+
69
+ // GOOD: offload to LongRunningTask
70
+ public override void Start()
71
+ {
72
+ loadTask = new LongRunningTask(LoadData, LogicObject);
73
+ loadTask.Start();
74
+ }
75
+
76
+ private void LoadData()
77
+ {
78
+ var data = File.ReadAllText("/mnt/slow-nfs/data.csv");
79
+ ProcessData(data);
80
+ }
81
+
82
+ private LongRunningTask loadTask;
83
+ ```
84
+
85
+ ## Stop() lifecycle
86
+
87
+ `Stop()` is called in the **exact reverse order** of `Start()`. This is guaranteed by the runtime regardless of how many NetLogics are present on the page.
88
+
89
+ - `Stop()` is also called sequentially on the behavior thread
90
+ - Exceptions thrown in `Stop()` are caught and logged; they do not interrupt the stop sequence of other behaviors
91
+ - After `Stop()`, the NetLogic instance is disposed
92
+
93
+ ```csharp
94
+ public override void Stop()
95
+ {
96
+ // Always dispose tasks and observers here
97
+ myPeriodicTask?.Dispose();
98
+ myLongRunningTask?.Dispose();
99
+ myVariableObserver?.Dispose();
100
+ }
101
+ ```
102
+
103
+ ### Stop() ordering during page navigation
104
+
105
+ When navigating from **SCREEN1** to **SCREEN2**, the runtime prioritizes loading the new page to minimize time-to-first-content for the user. As a consequence, **`Stop()` on SCREEN1's NetLogics may execute *after* `Start()` has already been called on SCREEN2's NetLogics**.
106
+
107
+ This is by design: the destruction and cleanup of nodes belonging to the previous page is intentionally deferred in favor of the new page becoming interactive as quickly as possible.
108
+
109
+ > [!WARNING]
110
+ > Do not assume that all `Stop()` calls from the previous page have completed before `Start()` begins on the new page. If SCREEN1 and SCREEN2 share any global/static state, session variables, or external resources, this overlap can cause unexpected behavior.
111
+
112
+ ```
113
+ Page transition timeline:
114
+
115
+ User behavior: [Navigate to SCREEN2]
116
+ ↑ user clicks on the NavigationPanel tab
117
+
118
+ Behavior thread: [Start NL_A (SCREEN2)][Start NL_B (SCREEN2)]
119
+ ↑ new page load starts first
120
+
121
+ [Stop NL_A (SCREEN1)][Stop NL_B (SCREEN1)]
122
+ ↑ old page cleanup follows asynchronously
123
+ ```
124
+
125
+ Practical guidelines:
126
+ - **Do not rely on SCREEN1's `Stop()` having run** before SCREEN2's `Start()` initializes shared resources.
127
+ - **Release exclusive resources** (serial ports, file handles, database connections) as early as possible within `Stop()`, and acquire them as late as possible in `Start()`, to minimize the overlap window.
128
+ - If a clean handoff is required, use a shared session variable or a global synchronization flag to signal that SCREEN1 has fully torn down before SCREEN2 proceeds with the conflicting initialization.
129
+
130
+ ## ExportMethod execution
131
+
132
+ Methods decorated with `[ExportMethod]` are called synchronously on **whichever thread triggered them**:
133
+
134
+ | Trigger | Thread used |
135
+ |---------|-------------|
136
+ | Button click / UI interaction | UI thread |
137
+ | Called from another NetLogic's `Start()` | Behavior thread |
138
+ | Called from a `LongRunningTask` | That task's dedicated thread |
139
+ | Called from a `PeriodicTask` | That task's dedicated thread |
140
+
141
+ ```csharp
142
+ // BAD: performs blocking operation on the UI thread
143
+ // While the UI remains responsive, other methods executed from the same UI session
144
+ // (including page switches) will wait for this method to complete
145
+ [ExportMethod]
146
+ public void GenerateReport()
147
+ {
148
+ Thread.Sleep(5000); // blocks the UI thread, other operations from this session must wait
149
+ WriteReportFile();
150
+ }
151
+
152
+ // GOOD: return immediately, do the work in background
153
+ [ExportMethod]
154
+ public void GenerateReport()
155
+ {
156
+ reportTask?.Dispose();
157
+ reportTask = new LongRunningTask(WriteReportFile, LogicObject);
158
+ reportTask.Start();
159
+ }
160
+
161
+ private LongRunningTask reportTask;
162
+ ```
163
+
164
+ ### ExportMethod and thread safety
165
+
166
+ When an `[ExportMethod]` runs on a thread different from the one that owns a shared variable, concurrent access can occur. The safest pattern is to keep all model-node reads/writes inside the method call itself (the FactoryTalk Optix information model is thread-safe at the node level)
167
+
168
+ ```csharp
169
+ // Sharing a plain C# object between a PeriodicTask and an ExportMethod: use a lock
170
+ private readonly object _lock = new object();
171
+ private List<double> _buffer = new();
172
+
173
+ private void SampleData()
174
+ {
175
+ lock (_lock)
176
+ {
177
+ _buffer.Add(GetSensorValue());
178
+ }
179
+ }
180
+
181
+ [ExportMethod]
182
+ public void FlushBuffer()
183
+ {
184
+ List<double> snapshot;
185
+ lock (_lock)
186
+ {
187
+ snapshot = new List<double>(_buffer);
188
+ _buffer.Clear();
189
+ }
190
+ // Work on snapshot safely
191
+ }
192
+ ```
193
+
194
+ ## Pages with many widgets or NetLogics
195
+
196
+ ### What happens at page load
197
+
198
+ When a page is navigated to, the runtime:
199
+
200
+ 1. Instantiates and renders all graphical widgets on the **UI thread** (independent, non-blocking for the behavior thread)
201
+ 2. Traverses the page subtree on the **behavior thread** and calls all `Start()` methods in priority order - **sequentially**
202
+
203
+ These two activities happen **concurrently** but on separate threads. The UI may already be partially visible while NetLogics are still starting up.
204
+
205
+ ### Practical implications for complex pages
206
+
207
+ - **Many NetLogics + fast `Start()` methods**: no meaningful issue. Each `Start()` completes in milliseconds and the total startup time is the sum of all of them.
208
+ - **One slow `Start()`**: blocks the startup of every NetLogic that follows it in the queue. Use `LongRunningTask` to move the slow work off the behavior thread.
209
+ - **Many heavy widgets**: the UI thread handles all rendering; a heavy widget (e.g., a large DataGrid populating from a database) may cause the UI to feel sluggish, but it does not block `Start()` on other NetLogics.
210
+ - Having multiple heavy widget might cause the Start() of the NetLogic to be delayed until the node is created, so multiple heavy widgets can cause a longer delay before the some NetLogic starts.
211
+
212
+ ```
213
+ Page load timeline (simplified):
214
+
215
+ UI thread: [render widget 1][render widget 2]...[render widget N]
216
+ Behavior thread: [Start NL_A][Start NL_B]...[Start NL_N]
217
+ ↑ these are sequential, not parallel
218
+ ```
219
+
220
+ ### Avoid cascading Start() delays
221
+
222
+ If you have, for example, 10 NetLogics each doing a 300 ms blocking operation in `Start()`, total startup time is **3 seconds** during which any NetLogic not yet started is completely inert. This can cause:
223
+ - Variables remaining at their default value longer than expected
224
+ - Observers not yet registered, missing initial state changes
225
+ - Timers not yet running
226
+
227
+ Offload anything non-trivial to `LongRunningTask`:
228
+
229
+ ```csharp
230
+ public override void Start()
231
+ {
232
+ // Register observers immediately (fast)
233
+ myVar = LogicObject.GetVariable("MyVar");
234
+ observer = new RemoteVariableSynchronizer();
235
+ observer.Add(myVar);
236
+ observer.VariableChange += OnVarChanged;
237
+
238
+ // Defer heavy initialization
239
+ initTask = new LongRunningTask(HeavyInit, LogicObject);
240
+ initTask.Start();
241
+ }
242
+
243
+ private void HeavyInit()
244
+ {
245
+ // Connect to database, read config file, etc.
246
+ }
247
+ ```
248
+
249
+ ## Async tasks and their threads
250
+
251
+ For a detailed reference, see [async-tasks.md](./async-tasks.md). Quick summary:
252
+
253
+ | Task type | Creates a new thread? | Can access info model? | When to use |
254
+ |-----------|----------------------|----------------------|-------------|
255
+ | `LongRunningTask` | Yes, dedicated | Yes (thread-safe) | Long/blocking operations |
256
+ | `PeriodicTask` | Yes, dedicated | Yes (thread-safe) | Recurring work (polling, watchdog) |
257
+ | `DelayedTask` | Yes, dedicated | Yes (thread-safe) | One-shot deferred action |
258
+ | `async/await` (C# native) | Uses ThreadPool | **No** - avoid accessing nodes | Pure computation, I/O without node access |
259
+
260
+ > [!NOTE]
261
+ > All FactoryTalk Optix task types (`LongRunningTask`, `PeriodicTask`, `DelayedTask`) are [information-model](./information-model.md)-safe. Native C# `async/await` or `Task.Run()` should only be used for operations that do not touch any project node.
262
+
263
+ ## DesignTime NetLogic and ExportMethod
264
+
265
+ DesignTime NetLogics follow a different loading strategy:
266
+ - The .NET assembly is **reloaded from disk on every `[ExportMethod]` call** (to pick up freshly compiled changes)
267
+ - `Start()` is **never called** for DesignTime NetLogics (and, it is not even created by the IDE when adding a new DesignTime NetLogic, the instance is only created when an `[ExportMethod]` is invoked)
268
+ - `[ExportMethod]` runs synchronously on the thread that triggered it (typically the Studio UI thread) - this means blocking operations inside a DesignTime `[ExportMethod]` will freeze Optix Studio until it returns.
269
+ - A DesignTime NetLogic can have multiple `[ExportMethod]`s, but they are all executed only on when the user invokes them by the context menu
270
+ - Asynchronous operations are allowed using `LongRunningTask`, but they will still execute on the same project, so it is advisable not to change the project tree or access project nodes from them to avoid concurrency issues with the IDE thread
@@ -0,0 +1,155 @@
1
+ # NetLogic overview
2
+
3
+ - NetLogic are C# scripts that help you implementing custom and/or advanced logic in FactoryTalk Optix with custom code.
4
+ - There are no limits on the amount of C# snippet you can add to a project
5
+ - There are some limitations on the packages you can add to a NetLogic
6
+ - Example:
7
+ - `WPF` (WindowsForms) -> These are not supported as they are not cross platform in the way FactoryTalk Optix works
8
+ - `ActiveX` -> These are not supported as they are old and unsecure
9
+ - `MSSQL` (Microsoft SQL) -> This is not supported as FactoryTalk Optix APIs offers a common syntax to access both EmbeddedDatabase and ODBC with same syntax by translating the output query for you. This is needed to make SQL independents from the type of database you are accessing
10
+ - This limitation was removed starting from FactoryTalk Optix 1.6.0.44, assuming at least one ODBC Connector is added to the project (even if unused)
11
+ - NetLogic can be RunTime or DesignTime
12
+ - `RunTime NetLogic` -> This is the most common type of NetLogic, you can use it to trigger custom logic when the RunTime is running
13
+ - `DesignTime NetLogic` -> These king of NetLogic are typically used to make the designer's life easier by automating some repetitive operations (such as creating hundreds of alarms, variables, assigning DynamicLink and more)
14
+ - Distinction between the two kind of NetLogic only exists in the YAML definition, the same APIs can be used both at DesignTime and/or at RunTime
15
+ - DesignTime NetLogic can only access elements that exists at DesignTime, you cannot access a `CommDriver` or a `Database` if the application is not running
16
+
17
+ ## Methods visible to FactoryTalk Optix
18
+
19
+ Not all methods in a NetLogic class are exposed to the runtime in the same way. FactoryTalk Optix distinguishes three categories:
20
+
21
+ ### Start() and Stop()
22
+
23
+ These are the two lifecycle methods inherited from `BaseNetLogic`. They are always called by the runtime automatically - you never invoke them yourself.
24
+
25
+ - `Start()` is called when the node (page, object) is activated
26
+ - `Stop()` is called when the node is deactivated, in reverse order relative to `Start()`
27
+ - Both must return quickly; slow implementations block the startup of subsequent NetLogics (see [thread-model.md](thread-model.md))
28
+
29
+ > [!NOTE]
30
+ > `Start()` and `Stop()` are not decorated with `[ExportMethod]` because they are not meant to be called manually. They are automatically invoked by the runtime based on the lifecycle of the NetLogic's parent node.
31
+
32
+ > [!NOTE]
33
+ > Only Runtime NetLogic can have `Start()` and `Stop()` methods, DesignTime NetLogic are meant to be triggered manually via `[ExportMethod]`, if `Start()`/`Stop()` methods are defined in a DesignTime NetLogic, they will not be called automatically.
34
+
35
+ ```csharp
36
+ public override void Start()
37
+ {
38
+ // Setup: register observers, create tasks, read initial values
39
+ }
40
+
41
+ public override void Stop()
42
+ {
43
+ // Teardown: dispose tasks, unregister observers
44
+ }
45
+ ```
46
+
47
+ ### `[ExportMethod]` - callable from the UI or other NetLogics
48
+
49
+ Methods decorated with `[ExportMethod]` are **registered as OPC UA Methods** on the NetLogic node. This makes them:
50
+ - Callable from buttons, event handlers, and other UI controls in Studio (they appear in the method picker)
51
+ - Callable from other NetLogics via `ExecuteMethod`
52
+ - Callable by external OPC UA clients
53
+
54
+ Rules for `[ExportMethod]`:
55
+ - Must be `public`
56
+ - Can have input parameters (mapped to OPC UA input arguments)
57
+ - Can have `out` parameters to return a value (which gets mapped to OPC UA output arguments)
58
+ - All methods must be `void` return type, use `out` parameters to return values instead
59
+ - The method name must be unique within the NetLogic class
60
+
61
+ ```csharp
62
+ // Simple ExportMethod with no parameters
63
+ [ExportMethod]
64
+ public void ResetCounter()
65
+ {
66
+ LogicObject.GetVariable("Counter").Value = 0;
67
+ }
68
+
69
+ // ExportMethod with input and output
70
+ [ExportMethod]
71
+ public void Multiply(double a, double b, out double result)
72
+ {
73
+ result = a * b;
74
+ }
75
+ ```
76
+
77
+ > [!WARNING]
78
+ > If called from a UI button, an `[ExportMethod]` runs on the **UI thread**. Blocking operations will freeze the UI. See [thread-model.md](./thread-model.md) for the correct pattern.
79
+
80
+ ### Public methods - internal use only
81
+
82
+ `public` methods that are **not** decorated with `[ExportMethod]` are plain C# class members. They are:
83
+ - **Not visible** to Studio, event handlers, or OPC UA clients
84
+ - Callable only from C# code within the same NetLogic or from other NetLogics via direct object reference
85
+
86
+ Use them to factor out reusable logic within the class without exposing it to the outside world.
87
+
88
+ ```csharp
89
+ // Visible to Studio and OPC UA clients
90
+ [ExportMethod]
91
+ public void TriggerAlarm(string message)
92
+ {
93
+ SetAlarmState(true, message); // calls the private helper below
94
+ }
95
+
96
+ // NOT visible to Studio - internal helper only
97
+ public void SetAlarmState(bool active, string message)
98
+ {
99
+ LogicObject.GetVariable("AlarmActive").Value = active;
100
+ LogicObject.GetVariable("AlarmMessage").Value = message;
101
+ }
102
+ ```
103
+
104
+ ### Private and internal methods
105
+
106
+ `private` (and `internal`) methods are standard C# - not accessible from outside the class at all. Use them freely for implementation details.
107
+
108
+ ### Summary table
109
+
110
+ | Method type | Visible in Studio? | Called by runtime? | Callable from other NetLogics? |
111
+ |-------------|-------------------|-------------------|-------------------------------|
112
+ | `Start()` / `Stop()` | No (automatic) | Yes, automatically | No (use ExportMethod instead) |
113
+ | `[ExportMethod]` public | **Yes** | No (on-demand) | Yes, via `ExecuteMethod` |
114
+ | `public` (no attribute) | No | No | Yes, via object reference |
115
+ | `private` / `internal` | No | No | No |
116
+
117
+ ---
118
+
119
+ ## Things to remember
120
+
121
+ - NetLogic class name is the same of the NetLogic object name
122
+ - Don't change the NetLogic name from any external editor, rename it from Studio instead!
123
+ - NetLogic objects can be browsed from the NetSolution
124
+ - Don't delete a NetLogic from the solution, remove it from Studio instead!
125
+ - Don't move a NetLogic from any external editor, move it from Studio (better to copy paste the NetLogic in the new position and then delete it from the unused place)
126
+
127
+ ## DesignTime helper: create a Runtime NetLogic (Blinker example)
128
+
129
+ ```csharp
130
+ /// <summary>
131
+ /// DesignTime helper that scaffolds a runtime NetLogic source file and supporting model nodes (Blinkers example).
132
+ /// Inputs: none. This is executed at DesignTime (Exported method).
133
+ /// Output: new folder/variables under Model, a NetLogic object under NetLogic and a .cs file written into NetSolution.
134
+ /// Caution: This writes files into the project - use only in design phase.
135
+ /// </summary>
136
+ [ExportMethod]
137
+ public void AddBlinkers() {
138
+ var varModelFolder = Project.Current.Get("Model");
139
+ varModelFolder.Add(InformationModel.Make<Folder>("Blinkers"));
140
+
141
+ var varBlinkersFolder = Project.Current.Get("Model/Blinkers");
142
+ varBlinkersFolder.Add(InformationModel.MakeVariable("P1ms", OpcUa.DataTypes.Boolean));
143
+ varBlinkersFolder.Add(InformationModel.MakeVariable("P10ms", OpcUa.DataTypes.Boolean));
144
+ varBlinkersFolder.Add(InformationModel.MakeVariable("P100ms", OpcUa.DataTypes.Boolean));
145
+ varBlinkersFolder.Add(InformationModel.MakeVariable("P1s", OpcUa.DataTypes.Boolean));
146
+ varBlinkersFolder.Add(InformationModel.MakeVariable("P10s", OpcUa.DataTypes.Boolean));
147
+
148
+ var varNetLogicFolder = Project.Current.Get("NetLogic");
149
+ varNetLogicFolder.Add(InformationModel.Make<NetLogicObject>("BlinkerNetLogic"));
150
+
151
+ var varNetLogicFileUri = ResourceUri.FromProjectRelativePath("NetSolution/BlinkerNetLogic.cs");
152
+ // The `varBlinkersNetLogicContent` variable should contain the runtime NetLogic source text (omitted here for brevity)
153
+ File.WriteAllText(varNetLogicFileUri.Uri, varBlinkersNetLogicContent);
154
+ }
155
+ ```