@aryacode/dsh-engineering 0.5.0 → 0.7.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 +52 -0
- package/README.md +9 -8
- package/cordis.patch.yml +1 -1
- package/entries/netlogic.js +266 -0
- package/entries/optix.js +13 -0
- package/package.json +10 -10
- package/vendor/l5x/SKILL.md +27 -7
- package/vendor/l5x/lib/engine/change-set.d.ts +9 -1
- package/vendor/l5x/lib/engine/change-set.js +32 -7
- package/vendor/l5x/lib/engine/documentation.d.ts +58 -0
- package/vendor/l5x/lib/engine/documentation.js +237 -0
- package/vendor/l5x/lib/engine/instructions.d.ts +50 -0
- package/vendor/l5x/lib/engine/instructions.js +107 -0
- package/vendor/l5x/lib/engine/ladder.d.ts +74 -0
- package/vendor/l5x/lib/engine/ladder.js +209 -0
- package/vendor/l5x/lib/index.d.ts +1 -0
- package/vendor/l5x/lib/index.js +3 -1
- package/vendor/l5x/lib/model/routines.js +6 -1
- package/vendor/l5x/lib/session.d.ts +20 -0
- package/vendor/l5x/lib/session.js +36 -0
- package/vendor/l5x/lib/tools/document.d.ts +18 -0
- package/vendor/l5x/lib/tools/document.js +119 -0
- package/vendor/l5x/lib/tools/drafts.js +109 -3
- 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,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
|
+
```
|