@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +9 -8
  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 +10 -10
  7. package/vendor/l5x/SKILL.md +27 -7
  8. package/vendor/l5x/lib/engine/change-set.d.ts +9 -1
  9. package/vendor/l5x/lib/engine/change-set.js +32 -7
  10. package/vendor/l5x/lib/engine/documentation.d.ts +58 -0
  11. package/vendor/l5x/lib/engine/documentation.js +237 -0
  12. package/vendor/l5x/lib/engine/instructions.d.ts +50 -0
  13. package/vendor/l5x/lib/engine/instructions.js +107 -0
  14. package/vendor/l5x/lib/engine/ladder.d.ts +74 -0
  15. package/vendor/l5x/lib/engine/ladder.js +209 -0
  16. package/vendor/l5x/lib/index.d.ts +1 -0
  17. package/vendor/l5x/lib/index.js +3 -1
  18. package/vendor/l5x/lib/model/routines.js +6 -1
  19. package/vendor/l5x/lib/session.d.ts +20 -0
  20. package/vendor/l5x/lib/session.js +36 -0
  21. package/vendor/l5x/lib/tools/document.d.ts +18 -0
  22. package/vendor/l5x/lib/tools/document.js +119 -0
  23. package/vendor/l5x/lib/tools/drafts.js +109 -3
  24. package/vendor/optix/SKILL.md +68 -0
  25. package/vendor/optix/netlogic/SOURCE.json +12 -0
  26. package/vendor/optix/netlogic/index.json +1987 -0
  27. package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
  28. package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
  29. package/vendor/optix/netlogic/pages/alarming.md +489 -0
  30. package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
  31. package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
  32. package/vendor/optix/netlogic/pages/colors.md +122 -0
  33. package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
  34. package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
  35. package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
  36. package/vendor/optix/netlogic/pages/datagrids.md +123 -0
  37. package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
  38. package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
  39. package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
  40. package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
  41. package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
  42. package/vendor/optix/netlogic/pages/element-access.md +1208 -0
  43. package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
  44. package/vendor/optix/netlogic/pages/events.md +340 -0
  45. package/vendor/optix/netlogic/pages/execute-command.md +202 -0
  46. package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
  47. package/vendor/optix/netlogic/pages/good-practices.md +50 -0
  48. package/vendor/optix/netlogic/pages/information-model.md +89 -0
  49. package/vendor/optix/netlogic/pages/log-output.md +22 -0
  50. package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
  51. package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
  52. package/vendor/optix/netlogic/pages/mqtt.md +240 -0
  53. package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
  54. package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
  55. package/vendor/optix/netlogic/pages/opcua.md +325 -0
  56. package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
  57. package/vendor/optix/netlogic/pages/queries.md +1297 -0
  58. package/vendor/optix/netlogic/pages/random-generation.md +90 -0
  59. package/vendor/optix/netlogic/pages/recipes.md +375 -0
  60. package/vendor/optix/netlogic/pages/recipex.md +2383 -0
  61. package/vendor/optix/netlogic/pages/regex.md +15 -0
  62. package/vendor/optix/netlogic/pages/register-observers.md +346 -0
  63. package/vendor/optix/netlogic/pages/reports.md +141 -0
  64. package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
  65. package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
  66. package/vendor/optix/netlogic/pages/serial-port.md +357 -0
  67. package/vendor/optix/netlogic/pages/sessions.md +58 -0
  68. package/vendor/optix/netlogic/pages/system-node.md +153 -0
  69. package/vendor/optix/netlogic/pages/template-library.md +127 -0
  70. package/vendor/optix/netlogic/pages/threading.md +236 -0
  71. package/vendor/optix/netlogic/pages/translations.md +417 -0
  72. package/vendor/optix/netlogic/pages/trends.md +128 -0
  73. package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
  74. package/vendor/optix/netlogic/pages/users-groups.md +416 -0
  75. package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
  76. package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
  77. package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
@@ -0,0 +1,236 @@
1
+ # FT Optix Runtime – Threading Model
2
+
3
+ ## Overview
4
+
5
+ The FT Optix Runtime is built around a **shared, priority-based thread pool** combined with an **affinity-based task serializer**. This design allows many independent subsystems to execute concurrently without requiring each one to own a dedicated OS thread, while still giving every behavior object a strong single-threaded execution guarantee.
6
+
7
+ Understanding this model is important when reasoning about the relationship between **CPU clock speed** and **CPU core count** on overall application performance.
8
+
9
+
10
+
11
+ ## Core Thread Pools
12
+
13
+ ### Main dispatch pool
14
+
15
+ At startup, the runtime creates one shared thread pool containing **one worker thread per logical CPU** (determined via `std::thread::hardware_concurrency()`). On a 4-core/8-thread CPU this gives 8 workers; on a 32-core/64-thread server it gives 64.
16
+
17
+ All asynchronous callbacks for behavior objects - variable change notifications, OPC UA events, and user-defined dispatch calls - are served from this single pool.
18
+
19
+ The pool uses a six-level priority queue, so time-sensitive work is always promoted ahead of background tasks:
20
+
21
+ | Priority | Intended use |
22
+ |---|---|
23
+ | Blocking | Tasks that must start immediately to unblock a caller |
24
+ | Highest | Critical-path control actions |
25
+ | High | Time-sensitive HMI updates |
26
+ | Normal | Default for most dispatched work |
27
+ | Low | Background data logging, analytics |
28
+ | Lowest | Maintenance sweeps |
29
+
30
+ ### Timer pool
31
+
32
+ One-shot and periodic timers are managed by a separate timer subsystem backed by its own **fixed-size thread pool** (independent of the main dispatch pool). This ensures that a slow or blocked timer callback cannot starve other dispatched work, and vice versa.
33
+
34
+ Timers are added programmatically and are represented by a RAII handle: dropping the handle cancels the timer. A blocking removal API is also available that waits for any currently executing callback to finish before returning.
35
+
36
+ ## Affinity-Based Dispatch
37
+
38
+ ### The affinity concept
39
+
40
+ Every behavior object in the runtime is assigned a unique numeric **affinity ID** at construction time. The task scheduler guarantees that **no two tasks sharing the same affinity ID ever run concurrently**, regardless of how many pool threads are available.
41
+
42
+ This is the primary concurrency safety mechanism in the runtime. Behavior code can access its own state without locks because only one task bearing its affinity ID will ever run at a time. Mutexes are only required when data is shared *across* affinity boundaries.
43
+
44
+ ### Task lifecycle
45
+
46
+ ```
47
+ Trigger (variable change / OPC UA event / timer expiry)
48
+ │
49
+ ▼
50
+ Dispatch(callback, affinityId)
51
+ │
52
+ ┌────┴─────┐
53
+ │ Is the │ Yes ──► park task until current task for
54
+ │ affinity │ this affinity completes
55
+ │ busy? │
56
+ └────┬─────┘
57
+ │ No
58
+ ▼
59
+ Mark affinity as "in-flight"
60
+ │
61
+ ▼
62
+ Submit to thread pool ──► picked up by an idle worker thread
63
+ │
64
+ ▼
65
+ Execute callback on pool thread
66
+ │
67
+ ▼
68
+ On completion: promote next parked task for this affinity (if any)
69
+ ```
70
+
71
+ ### Shutdown guards
72
+
73
+ Three lifecycle guards coordinate ordered shutdown of a behavior:
74
+
75
+ | Guard | Effect |
76
+ |---|---|
77
+ | Suspend dispatch | New tasks queue up; the current in-flight task runs to completion |
78
+ | Suspend on stop | Same, but auto-resumes when the guard scope exits |
79
+ | Terminate on stop | All queued tasks for this affinity are abandoned when the guard scope exits |
80
+
81
+ ## Per-Subsystem Dedicated Threads
82
+
83
+ Beyond the shared pool, several subsystems own their own dedicated OS thread for specialized I/O serialization:
84
+
85
+ | Subsystem | Thread count | Purpose |
86
+ |---|---|---|
87
+ | Communication driver scheduler | One per configured driver station | Runs timed poll tasks in priority order (read/write priority levels); also handles on-demand requests enqueued from other threads |
88
+ | Persistent store I/O | One per store node | Serializes all database reads and writes; exceptions propagate back to the calling thread synchronously |
89
+ | UI rendering queue | One | Batch consumer queue for layout and rendering operations that must not block OPC UA callbacks |
90
+ | Write batching | One | Collects write values from any thread, batches them, and forwards them on its own thread to reduce protocol round-trips |
91
+ | Store-and-forward retry | Timer-driven | Uses the timer subsystem for flush and retry intervals; no permanently blocked thread |
92
+
93
+ ## Communication Driver Threading
94
+
95
+ Each configured **driver station** runs its own scheduler thread. This thread:
96
+
97
+ - Maintains a sorted queue of tasks keyed by **priority level** (Read / Write) and **next execution time**.
98
+ - Pulls the next due task, executes it, then reschedules it if it is periodic (configured with a period in milliseconds).
99
+ - Accepts on-demand tasks (single-execution jobs) from any thread via an enqueue API; these are prioritized above or below cyclic poll tasks depending on their priority level.
100
+ - Transitions through a communication state machine (connecting → connected → error → reconnecting) independently of any other station's thread.
101
+
102
+ Because each station has its own thread, **N configured stations require N dedicated threads** regardless of pool size. Stations are fully isolated: a network hang on station A does not affect the scheduler thread of station B.
103
+
104
+ ### Threads vs CPU cores - over-subscription
105
+
106
+ Having more driver stations than CPU cores is normal and supported. A scheduler thread spends the vast majority of its time **sleeping** (blocked on a timed wait until the next task is due). Sleeping threads do not consume a CPU slot - the OS only schedules them onto a core when they have actual work to do. Twenty stations on an 8-core machine is a common and well-functioning configuration.
107
+
108
+ Over-subscription only becomes noticeable when **many stations have work ready at the same instant**:
109
+
110
+ - If N stations all have a short-period poll task that fires simultaneously, and N is greater than the number of logical CPUs, the OS will time-slice. Some stations will be delayed by one OS scheduling quantum while waiting for a free CPU slot.
111
+ - The magnitude of this delay is typically in the range of a few hundred microseconds to a few milliseconds, depending on OS and system load.
112
+ - This introduces **jitter** into the actual poll period - the configured period is a minimum interval, not a hard real-time guarantee.
113
+
114
+ **When this matters in practice:**
115
+
116
+ | Scenario | Impact |
117
+ |---|---|
118
+ | Poll periods ≥ 100 ms (typical SCADA/HMI) | Negligible - a 1–2 ms jitter on a 500 ms cycle is imperceptible |
119
+ | Poll periods 10–100 ms with many stations | May cause occasional late scans under load; monitor with task execution counters |
120
+ | Poll periods < 10 ms with many stations | Requires careful sizing; use more physical cores or reduce the number of concurrently active stations |
121
+
122
+ **Mitigations if over-subscription causes issues:**
123
+
124
+ - Increase the number of physical CPU cores.
125
+ - Stagger station start times to spread wake-up patterns naturally (stations started at different times will tend to drift into different scheduling slots).
126
+ - Increase poll periods where sub-100 ms scanning is not strictly required.
127
+ - Run the runtime process at elevated OS scheduling priority to reduce competition from other processes on the same machine.
128
+
129
+ ## NetLogic Execution Model
130
+
131
+ ### Default execution (short-lived operations)
132
+
133
+ C# NetLogic methods (`Start()`, UA method callbacks) execute on a **pool thread from the main dispatch pool**, within the affinity context of the owning NetLogic node. This means:
134
+
135
+ - `Start()` and method callbacks are serialized with respect to that node - no locks are needed for node-local state.
136
+ - A `Start()` or method callback that runs longer than **2 seconds** triggers a runtime warning in the log.
137
+ - Any computation or I/O that may take longer must be explicitly offloaded to a `LongRunningTask`.
138
+
139
+ ### Long-running tasks
140
+
141
+ `LongRunningTask` allocates a **dedicated OS thread** for the duration of the task (using .NET's `TaskCreationOptions.LongRunning`). This prevents slow user code from occupying a shared pool thread and impacting other nodes.
142
+
143
+ ```csharp
144
+ public override void Start()
145
+ {
146
+ longTask = new LongRunningTask(RunSlowOperation, LogicObject);
147
+ longTask.Start();
148
+ }
149
+
150
+ private void RunSlowOperation(LongRunningTask task)
151
+ {
152
+ // Runs on its own dedicated OS thread.
153
+ // Use task.IsCancellationRequested to cooperate with Stop().
154
+ while (!task.IsCancellationRequested)
155
+ {
156
+ // ... polling loop, database query, external API call, etc.
157
+ }
158
+ }
159
+
160
+ public override void Stop()
161
+ {
162
+ longTask?.Dispose(); // requests cancellation and waits for the thread to exit
163
+ }
164
+ ```
165
+
166
+ **Important:** `LongRunningTask` instances must always be disposed in `Stop()`. Failing to do so will leave an OS thread running after the node is stopped.
167
+
168
+ ## Thread Count at a Glance
169
+
170
+ ```
171
+ Main dispatch pool: hardware_concurrency threads (all behavior callbacks)
172
+ Comm driver scheduler: 1 thread per station (protocol polling & writes)
173
+ Persistent store I/O: 1 thread per store node (DB serialization)
174
+ UI rendering queue: 1 thread (layout / paint batching)
175
+ Write batching: 1 thread (output batching)
176
+ LongRunningTask (.NET): 1 thread per active instance (user long-running logic)
177
+ ```
178
+
179
+ ## How the Application Benefits from More CPU Threads vs Higher Clock Speed
180
+
181
+ ### Workloads that scale with more CPU cores
182
+
183
+ Because the main dispatch pool is sized to `hardware_concurrency`, adding more logical CPUs directly increases the number of behavior callbacks, timer tasks, and OPC UA events that can execute simultaneously.
184
+
185
+ | Workload | Why more cores help |
186
+ |---|---|
187
+ | **Many independent HMI nodes with data subscriptions** | Each affinity can be scheduled on a different thread. With 100 nodes and 8 cores, up to 8 nodes update in parallel; with 16 cores, up to 16 do. |
188
+ | **Multiple communication driver stations** | Each station has its own dedicated scheduler thread. More cores prevent those threads from time-sharing with pool workers. |
189
+ | **Multiple store nodes** | Each store node has its own I/O thread. Concurrent database operations proceed in parallel. |
190
+ | **Many active `LongRunningTask` instances** | Each occupies a dedicated OS thread. More physical cores allow more of them to run truly in parallel. |
191
+ | **High-frequency periodic timers** | The timer pool has a fixed number of threads. If many timers fire at the same millisecond, more idle cores prevent queuing delays. |
192
+
193
+ **Rule of thumb:** if the application has more independent behavior objects, driver stations, or long-running tasks than the CPU has threads, adding more cores will improve throughput approximately proportionally - until another bottleneck (network, storage, database latency) takes over.
194
+
195
+ ### Workloads that scale with higher clock speed
196
+
197
+ Some execution paths are natively serial and cannot be parallelized at the dispatch level:
198
+
199
+ | Workload | Why higher clock helps |
200
+ |---|---|
201
+ | **Single "hot" behavior receiving events faster than it can process them** | Clock speed directly reduces per-callback latency. Task queue depth stays near zero. |
202
+ | **NetLogic `Start()` or method with heavy computation** | These run sequentially within their affinity. A computationally intensive method benefits from more MHz, not more cores. |
203
+ | **Communication driver scan cycle** | Each station runs on a single thread. Heavy protocol decoding or tag-mapping logic is bounded by single-thread performance. |
204
+ | **Persistent store transactions** | One thread per store node. Reducing query or write time (clock speed, fast SSD) reduces transaction latency. |
205
+ | **OPC UA Server session management** | Session-level state is managed on dedicated internal threads. Reducing per-session processing time allows more concurrent clients without queuing. |
206
+ | **Low-latency periodic timers (< 10 ms)** | Short-period timers are sensitive to OS scheduling jitter and CPU cache performance, both of which improve with higher base clock. |
207
+
208
+ ### Practical guidance
209
+
210
+ | Scenario | Recommended focus |
211
+ |---|---|
212
+ | Faster HMI screen load and rendering | Higher clock speed |
213
+ | Multiple HMI sessions (WebPresentationEngine clients) | More cores but with some limitations (see the [hot-path issue](#the-hot-path-issue)) |
214
+ | Many communication driver stations | More cores (each needs its own thread) |
215
+ | Heavy per-event computation in NetLogic | Higher clock speed |
216
+ | Fast scan-rate polling (1–10 ms) | Higher clock + real-time OS configuration |
217
+ | Large data logger write throughput | More cores AND fast storage I/O |
218
+ | Typical mixed HMI application | 4–8 fast cores is the sweet spot; beyond ~8 cores gives diminishing returns unless the project has a very large number of independent subsystems (see the [hot-path issue](#the-hot-path-issue)) |
219
+
220
+ ### The hot-path issue
221
+
222
+ A significant benefit of a multi-core CPU is on applications having many independent modules (HMI sessions, driver stations, data stores, long-running tasks). In these cases, adding cores increases throughput as the different modules are assigned to different threads.
223
+
224
+ If multiple modules are contending for the same set of items (same PLC tag, same store, etc.), then adding cores will not help, and the application will be limited by the single-threaded performance of the hot path.
225
+
226
+ ### Hyper-Threading / SMT
227
+
228
+ The main pool creates one thread per **logical** CPU. On a system with Hyper-Threading, two logical threads share one physical core's execution units. Because most runtime pool tasks involve a mix of memory accesses and computation, Hyper-Threading typically provides a 20–40 % throughput improvement over the equivalent physical core count without it. However, Hyper-Threading does **not** reduce latency for a single hot path - only additional physical cores do.
229
+
230
+ ## Key Design Principles
231
+
232
+ 1. **One main pool, many affinities.** All behavior callbacks share the same thread pool. Isolation is achieved through affinity serialization, not through private threads per behavior.
233
+ 2. **Timer callbacks are isolated from dispatch callbacks.** A blocking timer never starves the main pool.
234
+ 3. **Communication drivers are fully isolated from each other.** Each station has its own scheduler thread; a network fault on one station does not affect others.
235
+ 4. **Long user code must be explicit.** NetLogic that takes more than a few milliseconds must be moved to a `LongRunningTask`. Short callbacks keep the pool responsive for all other nodes.
236
+ 5. **Hardware concurrency drives pool size.** Adding CPU cores directly translates to pool thread count with no configuration change required.
@@ -0,0 +1,417 @@
1
+ # Translations
2
+
3
+ ## Assign a translation key to a Text property
4
+
5
+ Please note: `myKey` should already exist in the TranslationDictionary
6
+
7
+ ```csharp
8
+ // Create a new label
9
+ Label myLabel = InformationModel.Make<Label>("MyLabelName");
10
+ // Create a new LocalizedText with a specific Key (that should exist in the LocalizationDictionary)
11
+ myLabel.LocalizedText = new LocalizedText(myLabel.NodeId.NamespaceIndex, "myKey1");
12
+ // Add the label to the project
13
+ Owner.Add(myLabel);
14
+ ```
15
+
16
+ ## Read the translation key of a Text property
17
+
18
+ ```csharp
19
+ // Get the LocalizedText variable from the Label
20
+ string myKey = myLabel.LocalizedText.TextId
21
+ ```
22
+
23
+ ```csharp
24
+ // Alternative way (same result)
25
+ string myKey = ((LocalizedText)myVariable.Value).TextId
26
+ ```
27
+
28
+ ## Read the translation content for a specific key
29
+
30
+ ```csharp
31
+ // Create a new temporary localized text to read the key value from the dictionary
32
+ LocalizedText alarmKey = new LocalizedText(LogicObject.NodeId.NamespaceIndex, "Alarm_TL_Key");
33
+ // Get the text value from the current language
34
+ string alarmMessage = InformationModel.LookupTranslation(alarmKey).Text;
35
+ ```
36
+
37
+ ## Adding a description to a variable
38
+
39
+ ```csharp
40
+ // Set the description of an object to a localized text
41
+ variable.Description = new LocalizedText(Project.Current.NodeId.NamespaceIndex, "descriptionTextId");
42
+ ```
43
+
44
+ ## Create a dictionary
45
+
46
+ Dictionary is a variable with dataType OpcUa.String and variableType FTOptix.Core.LocalizationDictionary, values are stored in a bidimensional array of string
47
+
48
+ ```csharp
49
+ private void StuffMakeNewDictionary(string browseName, string[]languages, IUANode dictionaryOwner)
50
+ {
51
+ if (dictionaryOwner == null || languages.Length <= 0 || string.IsNullOrEmpty(browseName))
52
+ return;
53
+ if (languages[0] != "")
54
+ {
55
+ // The first column needs to be empty as it's the keys list
56
+ string[] firstColumnEmpty = new string[1] {""};
57
+ // Add the languages to the other columns
58
+ languages = firstColumnEmpty.Concat(languages).ToArray();
59
+ }
60
+ List<string> languageVerifiedToAdd = new List<string>();
61
+ foreach (string language in languages)
62
+ {
63
+ try
64
+ {
65
+ if (CultureInfo.GetCultureInfo(language, false) != null)
66
+ languageVerifiedToAdd.Add(language);
67
+ }
68
+ catch
69
+ {
70
+ // In case of culture identifier is not valid, skip to add to dictionary
71
+ }
72
+ }
73
+ string[,] newDictionaryValues = new string[1, languageVerifiedToAdd.Count];
74
+ for (int i = 0; i < languageVerifiedToAdd.Count; i++)
75
+ {
76
+ newDictionaryValues[0,i] = languageVerifiedToAdd[i];
77
+ }
78
+ // Create the new dictionary
79
+ IUAVariable newDictionary = InformationModel.MakeVariable(
80
+ browseName,
81
+ OpcUa.DataTypes.String,
82
+ FTOptix.Core.VariableTypes.LocalizationDictionary,
83
+ new uint[2] {
84
+ (uint)newDictionaryValues.GetLength(0),
85
+ (uint)newDictionaryValues.GetLength(1)
86
+ });
87
+ // Set the dictionary content
88
+ newDictionary.Value = new UAValue(newDictionaryValues);
89
+ // Check if the dictionary does not exist already, then add it
90
+ if (dictionaryOwner.Get(browseName) == null)
91
+ dictionaryOwner.Add(newDictionary);
92
+ }
93
+ ```
94
+
95
+ ## Add/Modify/Remove translation from a dictionary
96
+
97
+ Dictionary values are stored in a bidimensional array of string, you need to handle this rectangular array and regenerate the UAValue to store into the dictionary.
98
+
99
+ ### Add elements to the dictionary
100
+
101
+ This example provide a collection of array to add multiple records into the dictionary
102
+
103
+ ```csharp
104
+ private void StuffAddDictionaryTranslations(IUAVariable dictionary, List<string[]> valuesToAdd)
105
+ {
106
+ // A big dictionary may take a long processing time, so using a LongRunningTask is recommended to avoid blocking the main thread.
107
+
108
+ // Validate input: if any of the inputs are null or empty, exit early.
109
+ if (dictionary == null || dictionary.Value.Value == null || valuesToAdd == null || valuesToAdd.Count <= 0)
110
+ return;
111
+
112
+ string[,] actualDictionaryValues;
113
+ try
114
+ {
115
+ // Attempt to cast the dictionary's value to a 2D string array (dictionary format).
116
+ actualDictionaryValues = dictionary.Value.Value as string[,];
117
+ }
118
+ catch
119
+ {
120
+ // If the dictionary value is not a valid 2D array, stop further processing.
121
+ return;
122
+ }
123
+
124
+ // List to store only verified values that can be safely added to the dictionary.
125
+ List<string[]> valuesVerifiedToAdd = new List<string[]>();
126
+
127
+ // Iterate through each value set to be added to the dictionary.
128
+ foreach (string[] valueToAdd in valuesToAdd)
129
+ {
130
+ if (valueToAdd == null)
131
+ continue; // Skip null entries.
132
+
133
+ // Check if the number of elements in the value matches the number of columns in the dictionary.
134
+ if (valueToAdd.Length != actualDictionaryValues.GetLength(1))
135
+ {
136
+ Log.Warning(LogicObject.BrowseName, "The count of elements to be added is different from the number of columns in the dictionary, skipped");
137
+ continue;
138
+ }
139
+
140
+ // Validate that the first column (key) is not null or empty.
141
+ if (string.IsNullOrEmpty(valueToAdd[0]))
142
+ {
143
+ Log.Warning(LogicObject.BrowseName, "Missing key, skipped");
144
+ continue;
145
+ }
146
+
147
+ // Check if the key already exists in the current dictionary.
148
+ string checkIfKeyExist = Enumerable
149
+ .Range(0, actualDictionaryValues.GetLength(0)) // Generate row indices.
150
+ .Where(x => actualDictionaryValues[x, 0] == valueToAdd[0]) // Check if the key matches the first column.
151
+ .Select(y => actualDictionaryValues[y, 0]) // Get the matching key.
152
+ .FirstOrDefault(); // Return the first match or null.
153
+
154
+ if (!string.IsNullOrEmpty(checkIfKeyExist))
155
+ {
156
+ Log.Warning(LogicObject.BrowseName, $"Key {valueToAdd[0]} already exists, cannot add to dictionary, use Edit method");
157
+ continue;
158
+ }
159
+
160
+ // Add the value to the list of verified values for addition.
161
+ valuesVerifiedToAdd.Add(valueToAdd);
162
+ }
163
+
164
+ // Create a new dictionary with additional rows for the new entries.
165
+ string[,] newDictionaryValues = new string[actualDictionaryValues.GetLength(0) + valuesVerifiedToAdd.Count, actualDictionaryValues.GetLength(1)];
166
+
167
+ // Copy the existing dictionary values to the new dictionary.
168
+ for (int i = 0; i < actualDictionaryValues.GetLength(0); i++)
169
+ {
170
+ for (int j = 0; j < actualDictionaryValues.GetLength(1); j++)
171
+ {
172
+ newDictionaryValues[i, j] = actualDictionaryValues[i, j];
173
+ }
174
+ }
175
+
176
+ // Add the verified new values to the new dictionary starting from the next available row.
177
+ int rowOffset = 0;
178
+ foreach (string[] valueToAdd in valuesVerifiedToAdd)
179
+ {
180
+ for (int i = 0; i < valueToAdd.Length; i++)
181
+ {
182
+ int rowIndex = actualDictionaryValues.GetLength(0) + rowOffset; // Calculate the row index for the new entry.
183
+ newDictionaryValues[rowIndex, i] = valueToAdd[i]; // Add the value to the corresponding position.
184
+ }
185
+ rowOffset++; // Move to the next row for subsequent entries.
186
+ }
187
+
188
+ // Update the dictionary's value with the new dictionary containing the added values.
189
+ dictionary.Value = new UAValue(newDictionaryValues);
190
+ }
191
+
192
+ ```
193
+
194
+ ### Modify content of a dictionary
195
+
196
+ This example provide a collection of array to edit multiple records into the dictionary. Into a single array you can only write the language value to modify (take in consideration to place in correct order) and leave other field empty.
197
+
198
+ ```csharp
199
+ private void StuffModifyDictionaryTranslations(IUAVariable dictionary, List<string[]> valuesToEdit)
200
+ {
201
+ // A big dictionary may take a long time to process, so using a LongRunningTask is recommended to avoid blocking processes.
202
+
203
+ // Validate inputs: check if dictionary or its value is null, or if there are no values to edit.
204
+ if (dictionary == null || dictionary.Value.Value == null || valuesToEdit == null || valuesToEdit.Count <= 0)
205
+ return;
206
+
207
+ string[,] actualDictionaryValues;
208
+ try
209
+ {
210
+ // Attempt to cast the dictionary's value to a two-dimensional string array.
211
+ actualDictionaryValues = dictionary.Value.Value as string[,];
212
+ }
213
+ catch
214
+ {
215
+ // If the value cannot be cast, it may not be a dictionary, so stop further processing.
216
+ return;
217
+ }
218
+
219
+ // Iterate over each entry in the valuesToEdit list.
220
+ foreach (string[] valueToEdit in valuesToEdit)
221
+ {
222
+ // Skip null entries.
223
+ if (valueToEdit == null)
224
+ continue;
225
+
226
+ // Check if the entry length matches the number of dictionary columns.
227
+ if (valueToEdit.Length != actualDictionaryValues.GetLength(1))
228
+ {
229
+ Log.Warning(LogicObject.BrowseName, "The count of elements to be edited is different from the number of columns in the dictionary, skipped");
230
+ continue;
231
+ }
232
+
233
+ // Ensure the key (first element) is not null or empty.
234
+ if (string.IsNullOrEmpty(valueToEdit[0]))
235
+ {
236
+ Log.Warning(LogicObject.BrowseName, "Missing key, skipped");
237
+ continue;
238
+ }
239
+
240
+ // Find the row in the dictionary corresponding to the key.
241
+ int rowNumberToEdit = Enumerable
242
+ .Range(0, actualDictionaryValues.GetLength(0)) // Iterate over all rows.
243
+ .Where(x => actualDictionaryValues[x, 0] == valueToEdit[0]) // Check if the first column matches the key.
244
+ .FirstOrDefault(-1); // Default to -1 if no match is found.
245
+
246
+ // If the key does not exist in the dictionary, log a warning and skip the entry.
247
+ if (rowNumberToEdit == -1)
248
+ {
249
+ Log.Warning(LogicObject.BrowseName, $"Key {valueToEdit[0]} does not exist, cannot edit the dictionary, use Add method");
250
+ continue;
251
+ }
252
+
253
+ // Update the corresponding row with the new values.
254
+ for (int i = 0; i < valueToEdit.Length; i++)
255
+ {
256
+ // Only update non-empty values to avoid overwriting existing data with empty values.
257
+ if (!string.IsNullOrEmpty(valueToEdit[i]))
258
+ actualDictionaryValues[rowNumberToEdit, i] = valueToEdit[i];
259
+ }
260
+ }
261
+
262
+ // Update the dictionary with the modified values.
263
+ dictionary.Value = new UAValue(actualDictionaryValues);
264
+ }
265
+
266
+ ```
267
+
268
+ ### Remove
269
+
270
+ This example provide a list of keys to delete multiple records into the dictionary.
271
+
272
+ ```csharp
273
+ private void StuffRemoveDictionaryTranslations(IUAVariable dictionary, List<string> keysToRemove)
274
+ {
275
+ // A big dictionary may take a long processing time, usage of a LongRunningTask is recommended
276
+ if (dictionary == null || dictionary.Value.Value == null || keysToRemove == null || keysToRemove.Count <= 0)
277
+ return;
278
+ string[,] actualDictionaryValues;
279
+ try
280
+ {
281
+ actualDictionaryValues = dictionary.Value.Value as string[,];
282
+ }
283
+ catch
284
+ {
285
+ //Maybe is not a dictionary, so is better to stop processing here
286
+ return;
287
+ }
288
+ List<int> rowsToRemove = new List<int>();
289
+ const int defaultValue = 0;
290
+ foreach(string keyToRemove in keysToRemove)
291
+ {
292
+ if (string.IsNullOrEmpty(keyToRemove))
293
+ {
294
+ Log.Warning(LogicObject.BrowseName, "Missing key, skipped");
295
+ continue;
296
+ }
297
+ int rowToDelete = Enumerable
298
+ .Range(0, actualDictionaryValues.GetLength(0))
299
+ .Where(x => actualDictionaryValues[x, 0] == keyToRemove)
300
+ .FirstOrDefault(defaultValue);
301
+ if (rowToDelete == defaultValue)
302
+ {
303
+ Log.Warning(LogicObject.BrowseName, $"Key {keyToRemove} not exist, cannot edit the dictionary, use Add method");
304
+ continue;
305
+ }
306
+ rowsToRemove.Add(rowToDelete);
307
+ }
308
+ if (rowsToRemove.Count >= actualDictionaryValues.GetLength(0))
309
+ {
310
+ Log.Error(LogicObject.BrowseName, "The rows to be removed are equal to or more than the dictionary entries with header, impossible to continue");
311
+ return;
312
+ }
313
+ string[,] newDictionaryValues = new string[actualDictionaryValues.GetLength(0) - rowsToRemove.Count, actualDictionaryValues.GetLength(1)];
314
+ int rowToWrite = 0;
315
+ for (int i = 0; i < actualDictionaryValues.GetLength(0); i++)
316
+ {
317
+ if (!rowsToRemove.Contains(i))
318
+ {
319
+ for (int j = 0; j < actualDictionaryValues.GetLength(1); j++)
320
+ {
321
+ newDictionaryValues[rowToWrite, j] = actualDictionaryValues[i, j];
322
+ }
323
+ rowToWrite++;
324
+ }
325
+ }
326
+ dictionary.Value = new UAValue(newDictionaryValues);
327
+ }
328
+ ```
329
+
330
+ ### Example of previous code snippets
331
+
332
+ You can build a Design NetLogic with all previous methods to create and populate a Dictionary
333
+
334
+ ```csharp
335
+ [ExportMethod]
336
+ public void Method1()
337
+ {
338
+ // Get to the translations folder
339
+ Folder translation = Project.Current.Get<Folder>("Translations");
340
+ // Create the new dictionary object
341
+ StuffMakeNewDictionary("MyNewDictionary", new string[6] { "", "it-IT", "en-US", "fr-FR", "es-ES","de-DE" }, translation);
342
+ // Set some content to the dictionary
343
+ List<string[]> fillThisDict = new List<string[]>
344
+ {
345
+ new string[6] { "keyHello1", "Ciao", "Hello", "Bonjour", "Hola", "Hallo" },
346
+ new string[6] { "keyHello", "Ciao", "Hello", "Bonjour", "Hola", "Hallo" },
347
+ new string[6] { "keyStart2", "Avviare!", "Start!", "Démarrer!", "Inicio!","Start!" },
348
+ new string[6] { "keyStop2", "Fermare", "Stop", "Arrêter", "Para", "Stopp" },
349
+ new string[6] { "keyStart", "Avviare", "Start", "Démarrer", "Inicio", "Start" },
350
+ new string[6] { "keyStop", "Fermare", "Stop", "Arrêter", "Para", "Stopp" },
351
+ new string[6] { "keyStop3", "Fermare", "Stop", "Arrêter", "Para", "Stopp" }
352
+ };
353
+ StuffAddDictionaryTranslations(translation.GetVariable("MyNewDictionary"), fillThisDict);
354
+ // Add even more lines to the dictionary
355
+ List<string[]> editThisDict = new List<string[]>
356
+ {
357
+ new string[6] { "keyHello", "Ciao!", "Hello!", "Bonjour!", "Hola!", "Hallo!" },
358
+ new string[6] { "keyStop", "Fermati!", "Stop", "Arrêter", "Para", "Stopp" },
359
+ new string[6] { "keyStart2", "Avviare", "Start", "Démarrer", "Inicio", "Start" }
360
+ };
361
+ StuffModifyDictionaryTranslations(translation.GetVariable("MyNewDictionary"), editThisDict);
362
+ // Remove some unused keys
363
+ List<string> removeToThisDict = new List<string>
364
+ {
365
+ "keyHello1",
366
+ "keyStart2",
367
+ "keyStop2",
368
+ "keyStop3"
369
+ };
370
+ StuffRemoveDictionaryTranslations(translation.GetVariable("MyNewDictionary"), removeToThisDict);
371
+ }
372
+ ```
373
+
374
+ ## Send the current locale to the PLC
375
+
376
+ > [!NOTE]
377
+ > This code might limit the flexibility of locales switching in FactoryTalk Optix.
378
+
379
+ By default, each session has its own independent locale. This means that the script will be triggered every time any connected session changes its locale. The last session to change the locale will update the value in the controller.
380
+
381
+ This setup works fine if you are using only one session. However, if you are using multiple sessions, you should use a different approach. For example, you could log the locale change for each session separately into different variables.
382
+
383
+ ```csharp
384
+ public class RuntimeNetLogic1 : BaseNetLogic
385
+ {
386
+ // Place this code in the RuntimeNetLogic1.cs file
387
+ // in the MainWindow object of the project
388
+
389
+ public override void Start()
390
+ {
391
+ // Subscribe to the LocaleId variable change event
392
+ Session.ActualLocaleIdVariable.VariableChange += Session_LocaleIdVariable_VariableChange;
393
+ // You can also log the initial LocaleId value if needed
394
+ Log.Info("Initial LocaleId: " + Session.ActualLocaleId);
395
+ }
396
+
397
+ private void Session_LocaleIdVariable_VariableChange(object sender, VariableChangeEventArgs e)
398
+ {
399
+ // This code will be called when the LocaleId variable changes
400
+ // You can add your custom logic here
401
+ var newLocaleId = e.NewValue;
402
+ // For example, you can log the new LocaleId
403
+ Log.Info("New LocaleId: " + newLocaleId);
404
+ // Or send the new LocaleId to the PLC
405
+ Project.Current.Get<Variable>("CommDrivers/PLC1/Tags/LocaleId").Value = newLocaleId;
406
+ }
407
+
408
+ public override void Stop()
409
+ {
410
+ // Unsubscribe from the variable change event
411
+ if (Session.LocaleIdVariable != null)
412
+ {
413
+ Session.ActualLocaleIdVariable.VariableChange -= Session_LocaleIdVariable_VariableChange;
414
+ }
415
+ }
416
+ }
417
+ ```