@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,15 @@
1
+ # Regex
2
+
3
+ ## Extract alphanumeric characters from a string
4
+
5
+ ```csharp
6
+ Regex regexFileName = new Regex("[^a-zA-Z0-9 -]");
7
+ fileName = regexFileName.Replace(fileName, "");
8
+ ```
9
+
10
+ ## Extract the table name from a query
11
+
12
+ ```csharp
13
+ Regex regexTable = new Regex("(?<=\\bFROM\\s)(\\w+)");
14
+ Var tableName = regexTable.Match(Query, 0, Query.Length)
15
+ ```
@@ -0,0 +1,346 @@
1
+ # OPC/UA Observers
2
+
3
+ > [!WARNING]
4
+ > This page describes low-level features and should be used with caution as they can potentially lead to unexpected results or even break your project.
5
+ > These features are not officially supported and may be subject to changes in future releases.
6
+
7
+ > [!NOTE]
8
+ > Observers can only be registered at runtime. Attempting to register an observer at design time will not work.
9
+
10
+ The OPC/UA protocol implements some ways to monitor changes in the nodes tree, this allows the creation of observers that can monitor some project nodes and triggers some logics based on a set of events (which can be specified using a mask).
11
+
12
+ This technique allows multiple scenarios like:
13
+
14
+ - Detecting the nodes status (started, stopped, attached, detached, dispose)
15
+ - Detecting changes in values
16
+ - Detecting changes in attributes
17
+ - Detecting changes in references
18
+ - Detecting OPC/UA events
19
+
20
+ Subscribers must be added to all the elements that needs to be monitored, attaching an observer to the `Model` folder, won't enable the listener on all children nodes
21
+
22
+ ## References to OPC/UA Specifications
23
+
24
+ - **OPC UA Part 4 - Services**: Describes the services provided by the OPC UA server, including event subscriptions.
25
+ - **OPC UA Part 9 - Alarms & Conditions**: Provides details on how events and conditions are managed in OPC UA.
26
+
27
+ For more detailed information, refer to the [OPC UA Specifications](https://opcfoundation.org/developer-tools/specifications-unified-architecture).
28
+
29
+ ## Intercepting FactoryTalk Optix Runtime messages
30
+
31
+ The messages that are printed to the "Runtime Output" (or "Emulator Output") are generated as per OPC/UA specs by the Server object, which can be subscribed to trigger custom logics
32
+
33
+ ```csharp
34
+ #region Using directives
35
+ using System;
36
+ using UAManagedCore;
37
+ using OpcUa = UAManagedCore.OpcUa;
38
+ using FTOptix.NetLogic;
39
+ using System.Collections.Generic;
40
+ using LogsHandler;
41
+ #endregion
42
+
43
+ public class RuntimeLoggerMonitor : BaseNetLogic
44
+ {
45
+ public override void Start()
46
+ {
47
+ // Build the observer for the output messages
48
+ StartObserver();
49
+ }
50
+
51
+ public override void Stop()
52
+ {
53
+ // Destroy the observer when closing the NetLogic
54
+ logsRegistration?.Dispose();
55
+ }
56
+
57
+ public void StartObserver()
58
+ {
59
+ // Get the server object
60
+ var serverObject = LogicObject.Context.GetObject(OpcUa.Objects.Server);
61
+ // Create a new observer
62
+ var logsObserver = new LogsHandler.LogsEventObserver();
63
+ // Register the observer to the server node
64
+ logsRegistration = serverObject.RegisterUAEventObserver(logsObserver, FTOptix.Core.ObjectTypes.LogEvent);
65
+ }
66
+
67
+ // This node must stay "alive" as long as the subscriber is needed
68
+ // Do not move it to a local variable or the observer won't work
69
+ private IEventRegistration logsRegistration;
70
+ }
71
+
72
+ namespace LogsHandler
73
+ {
74
+ class LogsEventObserver : IUAEventObserver
75
+ {
76
+ public LogsEventObserver()
77
+ {
78
+ Log.Info("LogsEventObserver", "Starting observer");
79
+ }
80
+
81
+ public void OnEvent(IUAObject eventNotifier, IUAObjectType eventType, IReadOnlyList<object> args, ulong senderId)
82
+ {
83
+ Console.WriteLine(((LocalizedText) args[7]).Text); // Content of the message
84
+ Console.WriteLine(args[15]); // Module name
85
+ Console.WriteLine(args[13]); // Message level
86
+ // Do not use `Log.` events here or it will loop!
87
+ }
88
+ }
89
+ }
90
+
91
+ ```
92
+
93
+ ## Subscribe to methods invoke
94
+
95
+ This script can be put anywhere on the project and will listen to every OPCUA method being called (script or UI)
96
+
97
+ ```csharp
98
+ using System.Collections.Generic;
99
+ using System.Text;
100
+ using FTOptix.NetLogic;
101
+ using OpcUa = UAManagedCore.OpcUa;
102
+ using UAManagedCore;
103
+
104
+ public class RuntimeNetLogic1 : BaseNetLogic
105
+ {
106
+ public override void Start()
107
+ {
108
+ var serverObject = LogicObject.Context.GetObject(OpcUa.Objects.Server);
109
+ var eventHandler = new EventsHandler.EventsHandler();
110
+ eventRegistration = serverObject.RegisterUAEventObserver(eventHandler, UAManagedCore.OpcUa.ObjectTypes.AuditUpdateMethodEventType);
111
+ }
112
+
113
+ public override void Stop()
114
+ {
115
+ eventRegistration?.Dispose();
116
+ }
117
+
118
+ private IEventRegistration eventRegistration;
119
+ }
120
+
121
+ namespace EventsHandler
122
+ {
123
+ public class EventsHandler : IUAEventObserver
124
+ {
125
+ public EventsHandler()
126
+ {
127
+ Log.Info("EventsHandler", "EventsHandler instance created");
128
+ }
129
+
130
+ public void OnEvent(IUAObject eventNotifier, IUAObjectType eventType, IReadOnlyList<object> eventData, ulong senderId)
131
+ {
132
+ StringBuilder builder = new StringBuilder();
133
+ builder.Append($"Event of type {eventType.BrowseName} triggered");
134
+ var eventArguments = eventType.EventArguments;
135
+ foreach (var eventField in eventArguments.GetFields())
136
+ {
137
+ var fieldValue = eventArguments.GetFieldValue(eventData, eventField);
138
+ builder.Append($"\t{eventField} = {fieldValue?.ToString() ?? "null"}");
139
+ }
140
+ builder.Append("\n");
141
+ Log.Info(builder.ToString());
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ## Track the login and logout of users and get the list of groups for the current user
148
+
149
+ This example leverages the observers to track the login and logout of users, as well as to get the list of groups for the current user.
150
+
151
+ ```csharp
152
+ using System.Collections.Generic;
153
+ using System.Text;
154
+ using FTOptix.NetLogic;
155
+ using OpcUa = UAManagedCore.OpcUa;
156
+ using UAManagedCore;
157
+
158
+ public class RuntimeNetLogic1 : BaseNetLogic
159
+ {
160
+ public override void Start()
161
+ {
162
+ var serverObject = LogicObject.Context.GetObject(OpcUa.Objects.Server);
163
+ var eventHandler = new EventsHandler.EventsHandler();
164
+ eventRegistration = serverObject.RegisterUAEventObserver(eventHandler, FTOptix.Core.ObjectTypes.UserSessionEvent);
165
+ }
166
+
167
+ public override void Stop()
168
+ {
169
+ eventRegistration?.Dispose();
170
+ }
171
+
172
+ private IEventRegistration eventRegistration;
173
+ }
174
+
175
+ namespace EventsHandler
176
+ {
177
+ public class EventsHandler : IUAEventObserver
178
+ {
179
+ public EventsHandler()
180
+ {
181
+ Log.Info("EventsHandler", "EventsHandler instance created");
182
+ }
183
+
184
+ public void OnEvent(IUAObject eventNotifier, IUAObjectType eventType, IReadOnlyList<object> eventData, ulong senderId)
185
+ {
186
+ // Log the event to the console
187
+ StringBuilder builder = new StringBuilder();
188
+ builder.Append($"Event of type {eventType.BrowseName} triggered");
189
+ var eventArguments = eventType.EventArguments;
190
+ foreach (var eventField in eventArguments.GetFields())
191
+ {
192
+ var fieldValue = eventArguments.GetFieldValue(eventData, eventField);
193
+ builder.Append($"\t{eventField} = {fieldValue?.ToString() ?? "null"}");
194
+ }
195
+ Log.Debug(builder.ToString());
196
+
197
+ // Optional: only operate on Login event
198
+ if ((string)eventArguments.GetFieldValue(eventData, "SourceName") == "Login")
199
+ {
200
+ // Get the groups linked to the current user
201
+ var userNodeId = (NodeId)eventArguments.GetFieldValue(eventData, "UserId");
202
+ // Get the user object from the node ID
203
+ var newUser = InformationModel.Get<User>(userNodeId);
204
+ // Get all groups the user is part of
205
+ var userGroups = newUser.Refs.GetObjects(FTOptix.Core.ReferenceTypes.HasGroup, false);
206
+ // Create a string with the groups and log it
207
+ builder = new StringBuilder();
208
+ builder.Append($"User {newUser.BrowseName} is part of the following groups: ");
209
+ foreach (var group in userGroups)
210
+ {
211
+ builder.Append($"{group.BrowseName}, ");
212
+ }
213
+ // Remove the last comma and space
214
+ if (userGroups.Count > 0)
215
+ {
216
+ builder.Length -= 2; // Remove the last comma and space
217
+ }
218
+ else
219
+ {
220
+ builder.Append("No groups found");
221
+ }
222
+ Log.Info(builder.ToString());
223
+ }
224
+ }
225
+ }
226
+ }
227
+ ```
228
+
229
+ ## Subscribe to alarm events
230
+
231
+ This code subscribes to every event being performed on any alarm in the project, specific filtering can be added on the `OnEvent` to perform different operations when different actions are triggered on the alarm (like acknowledging or silencing the alarm).
232
+
233
+ ```csharp
234
+ using System.Collections.Generic;
235
+ using System.Text;
236
+ using FTOptix.NetLogic;
237
+ using OpcUa = UAManagedCore.OpcUa;
238
+ using UAManagedCore;
239
+ using FTOptix.Alarm;
240
+
241
+ public class RuntimeNetLogic1 : BaseNetLogic
242
+ {
243
+ public override void Start()
244
+ {
245
+ var serverObject = LogicObject.Context.GetObject(OpcUa.Objects.Server);
246
+ var eventHandler = new EventsHandler.EventsHandler();
247
+ // Subscribe to alarm events
248
+ eventRegistration = serverObject.RegisterUAEventObserver(eventHandler, UAManagedCore.OpcUa.ObjectTypes.AlarmConditionType);
249
+ }
250
+
251
+ public override void Stop()
252
+ {
253
+ eventRegistration?.Dispose();
254
+ }
255
+
256
+ private IEventRegistration eventRegistration;
257
+ }
258
+
259
+ namespace EventsHandler
260
+ {
261
+ public class EventsHandler : IUAEventObserver
262
+ {
263
+ public EventsHandler()
264
+ {
265
+ Log.Info("EventsHandler", "EventsHandler instance created");
266
+ }
267
+
268
+ public void OnEvent(IUAObject eventNotifier, IUAObjectType eventType, IReadOnlyList<object> eventData, ulong senderId)
269
+ {
270
+ StringBuilder builder = new StringBuilder();
271
+ builder.Append($"Event of type {eventType.BrowseName} triggered");
272
+ var eventArguments = eventType.EventArguments;
273
+ foreach (var eventField in eventArguments.GetFields())
274
+ {
275
+ var fieldValue = eventArguments.GetFieldValue(eventData, eventField);
276
+ builder.Append($"\t{eventField} = {fieldValue?.ToString() ?? "null"}");
277
+ }
278
+ builder.Append("\n");
279
+ Log.Info(builder.ToString());
280
+ }
281
+ }
282
+ }
283
+ ```
284
+
285
+ ## Monitor adding/removing users at runtime
286
+
287
+ ```csharp
288
+ /// <summary>
289
+ /// Example Runtime NetLogic to observe additions and removals of users under Security/Users.
290
+ /// The observer registers to forward reference added/removed events and logs the change.
291
+ /// </summary>
292
+ public class UserCreationObserverLogic : BaseNetLogic
293
+ {
294
+ private IEventRegistration userCreationObserver;
295
+ private uint affinityId; // store affinity to avoid cross-thread issues
296
+
297
+ public override void Start()
298
+ {
299
+ // Get the affinity ID of the current NetLogic to avoid cross thread issues
300
+ affinityId = LogicObject.Context.AssignAffinityId();
301
+ // Create and register the observer
302
+ StartObserver();
303
+ }
304
+
305
+ public override void Stop()
306
+ {
307
+ // Dispose the observer when stopping
308
+ userCreationObserver?.Dispose();
309
+ }
310
+
311
+ public void StartObserver()
312
+ {
313
+ // Get the user folder server object
314
+ var userObject = LogicObject.Context.GetNode(Project.Current.Get("Security/Users").NodeId);
315
+ // Create a new observer instance
316
+ var logsObserver = new LogsHandler.UserCreationObserver();
317
+ // Register the observer to the server node
318
+ userCreationObserver = userObject.RegisterEventObserver(
319
+ logsObserver,
320
+ EventType.ForwardReferenceAdded | // when users are created
321
+ EventType.ForwardReferenceRemoved, // when users are deleted
322
+ affinityId);
323
+ }
324
+ }
325
+
326
+ namespace LogsHandler
327
+ {
328
+ class UserCreationObserver : IReferenceObserver
329
+ {
330
+ public UserCreationObserver()
331
+ {
332
+ Log.Info("LogsEventObserver", "Starting users observer");
333
+ }
334
+
335
+ public void OnReferenceAdded(IUANode sourceNode, IUANode targetNode, NodeId referenceTypeId, ulong senderId)
336
+ {
337
+ Log.Info("LogsEventObserver", $"{targetNode.BrowseName} user is added");
338
+ }
339
+
340
+ public void OnReferenceRemoved(IUANode sourceNode, IUANode targetNode, NodeId referenceTypeId, ulong senderId)
341
+ {
342
+ Log.Info("LogsEventObserver", $"{targetNode.BrowseName} user got removed");
343
+ }
344
+ }
345
+ }
346
+ ```
@@ -0,0 +1,141 @@
1
+ # Reports
2
+
3
+ ## Generate a report and trigger some action based on the result code
4
+
5
+ > [!NOTE]
6
+ > Report generation can only be triggered at runtime.
7
+
8
+ ```csharp
9
+ #region Using directives
10
+ using System;
11
+ using System.IO;
12
+ using System.Linq;
13
+ using System.Reflection.Emit;
14
+ using FTOptix.Core;
15
+ using FTOptix.HMIProject;
16
+ using FTOptix.NetLogic;
17
+ using FTOptix.Report;
18
+ using FTOptix.UI;
19
+ using UAManagedCore;
20
+ #endregion
21
+
22
+ public partial class PdfReportLogic : BaseNetLogic
23
+ {
24
+ public override void Start()
25
+ {
26
+ // Try to assign a value to the button
27
+ try
28
+ {
29
+ generatePdfButton = Owner.Get<Button>("TrackedValues/Generate");
30
+ viewPdfButton = Owner.Get<Button>("TrackedValues/View");
31
+ viewPdfButton.Enabled = false;
32
+ }
33
+ catch
34
+ {
35
+ // Button does not exist
36
+ Log.Warning("PdfReportLogic", "Can't find PDF buttons, maybe they were renamed?");
37
+ }
38
+ }
39
+
40
+ public override void Stop()
41
+ {
42
+ myReport.OnGeneratePdfCompleted -= MyReport_OnGeneratePdfCompleted;
43
+ }
44
+
45
+ [ExportMethod]
46
+ public void GenerateReport(string fileName, string pdfLocale)
47
+ {
48
+ // Hide button
49
+ generatePdfButton.Enabled = false;
50
+
51
+ // Check if the path is empty
52
+ if (string.IsNullOrEmpty(fileName))
53
+ {
54
+ Log.Error("PdfReportLogic", "Empty PDF name");
55
+ SetOutputMessage("ReportEmptyPath");
56
+ throw new ArgumentNullException();
57
+ }
58
+
59
+ // Check if the pdf has the extension
60
+ if (!fileName.EndsWith(".pdf", StringComparison.InvariantCultureIgnoreCase))
61
+ {
62
+ Log.Warning("PdfReportLogic", "PDF extension not found, adding it");
63
+ fileName += ".pdf";
64
+ }
65
+
66
+ // Make sure to use only the file name
67
+ fileName = fileName.Split('\\', '/').Last();
68
+
69
+ // Check if the path is an Optix path variable
70
+ var pdfResourceUri = ResourceUri.FromProjectRelativePath(fileName);
71
+
72
+ // Check if the locale is valid
73
+ if (string.IsNullOrEmpty(pdfLocale))
74
+ {
75
+ Log.Error("PdfReportLogic", "Empty locale");
76
+ SetOutputMessage("ReportEmptyLocale");
77
+ throw new ArgumentNullException();
78
+ }
79
+
80
+ // Check if the locale is valid using regex
81
+ if (!LocaleIdRegex().IsMatch(pdfLocale))
82
+ {
83
+ Log.Error("PdfReportLogic", "Invalid locale");
84
+ SetOutputMessage("ReportInvalidLocale");
85
+ throw new ArgumentException();
86
+ }
87
+
88
+ // Generate the report
89
+ myReport = Project.Current.Get<Report>("Reports/LoggerReport");
90
+ Log.Debug("PdfReportLogic", $"Generating PDF report with locale: {pdfLocale}");
91
+ Log.Debug("PdfReportLogic", $"Saving PDF report to: {pdfResourceUri.Uri}");
92
+ myReport.GeneratePdf(pdfResourceUri, pdfLocale, out Guid operationId);
93
+
94
+ // Subscribe to the report generation event
95
+ myReport.OnGeneratePdfCompleted += MyReport_OnGeneratePdfCompleted;
96
+
97
+ Log.Debug("PdfReportLogic", $"Report generation started with operation ID: {operationId}");
98
+ }
99
+
100
+ private void MyReport_OnGeneratePdfCompleted(object sender, GeneratePdfCompletedEvent e)
101
+ {
102
+ Log.Info("PdfReportLogic", $"Report generation completed with result: {e.Result}");
103
+ if (e.Result == GeneratePdfCompletedResult.PdfSuccessfullyGenerated)
104
+ {
105
+ SetOutputMessage("ReportSuccess");
106
+ viewPdfButton.Enabled = true;
107
+ }
108
+ else
109
+ {
110
+ SetOutputMessage("ReportFailed");
111
+ viewPdfButton.Enabled = false;
112
+ Log.Error("PdfReportLogic", $"PDF generation failed, error: {e.Result}");
113
+ }
114
+
115
+ generatePdfButton.Enabled = true;
116
+ }
117
+
118
+ // Set output message
119
+ private void SetOutputMessage(string translationKey)
120
+ {
121
+ var outputLabel = Owner.Get<FTOptix.UI.Label>("TrackedValues/OutputMessage");
122
+ try
123
+ {
124
+ outputLabel.LocalizedText = new LocalizedText(outputLabel.NodeId.NamespaceIndex, translationKey);
125
+ }
126
+ catch
127
+ {
128
+ Log.Warning("PdfReportLogic", $"Translation key not found: {translationKey}");
129
+ outputLabel.Text = translationKey;
130
+ }
131
+ }
132
+
133
+ // Regex for locale ID
134
+ [System.Text.RegularExpressions.GeneratedRegex("^[a-z]{2}-[A-Z]{2}$")]
135
+ private static partial System.Text.RegularExpressions.Regex LocaleIdRegex();
136
+ // PDF generator button
137
+ private FTOptix.UI.Button generatePdfButton;
138
+ private FTOptix.UI.Button viewPdfButton;
139
+ private FTOptix.Report.Report myReport;
140
+ }
141
+ ```