@aryacode/dsh-engineering 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +6 -6
- package/cordis.patch.yml +1 -1
- package/entries/netlogic.js +266 -0
- package/entries/optix.js +13 -0
- package/package.json +3 -2
- package/vendor/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,2383 @@
|
|
|
1
|
+
# RecipeX
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> This feature is available starting from FactoryTalk Optix 1.7.X and should not be confused with [legacy recipe](./recipes.md) features.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
The **RecipeX** module provides a new way to handle recipes in FactoryTalk Optix applications. Recipes are structured data templates used to store, retrieve, and transfer configuration parameters and data items. The RecipeX system supports:
|
|
9
|
+
|
|
10
|
+
- Recipe creation, duplication, renaming, and deletion
|
|
11
|
+
- Data item management and value manipulation
|
|
12
|
+
- Metadata handling with custom fields
|
|
13
|
+
- Recipe transfers between store and target locations
|
|
14
|
+
- Edit model management for recipe editing workflows
|
|
15
|
+
|
|
16
|
+
Before reading this chapter, please check the [Recipe Schemas Documentation](https://www.rockwellautomation.com/en-us/docs/factorytalk-optix/1-7-0/contents-ditamap/creating-projects/recipes/recipex-schemas.html) for an introduction to recipe schemas and their configuration in the FactoryTalk Optix HMI project.
|
|
17
|
+
|
|
18
|
+
> [!NOTE]
|
|
19
|
+
> If a recipe schema is edited, the Runtime will require a Refactor to update the database table structure accordingly. It is advised to backup your project before making changes to recipe schemas.
|
|
20
|
+
|
|
21
|
+
## Core Types
|
|
22
|
+
|
|
23
|
+
### FTOptix.RecipeX.RecipeId
|
|
24
|
+
|
|
25
|
+
Uniquely identifies a recipe using name and version information.
|
|
26
|
+
|
|
27
|
+
```csharp
|
|
28
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 10)]
|
|
29
|
+
public class RecipeId : TypedStruct
|
|
30
|
+
{
|
|
31
|
+
public string Name { get; set; } // Recipe name
|
|
32
|
+
public string Version { get; set; } // Recipe version
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Usage Example:**
|
|
37
|
+
```csharp
|
|
38
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1", Version = "1.0" };
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Recipe
|
|
42
|
+
|
|
43
|
+
Represents a complete recipe with metadata and timestamps.
|
|
44
|
+
|
|
45
|
+
```csharp
|
|
46
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 14)]
|
|
47
|
+
public classRecipe : TypedStruct
|
|
48
|
+
{
|
|
49
|
+
public FTOptix.RecipeX.RecipeId FTOptix.RecipeX.RecipeId { get; set; } // Recipe identifier
|
|
50
|
+
public DateTime RecipeSchemaTimestamp { get; set; } // Schema timestamp
|
|
51
|
+
public DateTime CreatedAt { get; set; } // Creation timestamp
|
|
52
|
+
public DateTime ModifiedAt { get; set; } // Last modification timestamp
|
|
53
|
+
public FTOptix.RecipeX.RecipeMetadata[] MetadataValues { get; set; } // Custom metadata fields
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### RecipeMetadata
|
|
58
|
+
|
|
59
|
+
Represents custom metadata associated with a recipe.
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 12)]
|
|
63
|
+
public classRecipeMetadata : TypedStruct
|
|
64
|
+
{
|
|
65
|
+
public string Name { get; set; } // Metadata field name
|
|
66
|
+
public LocalizedText DisplayName { get; set; } // Localized display name
|
|
67
|
+
public NodeId DataTypeId { get; set; } // OPC UA data type reference
|
|
68
|
+
public object Value { get; set; } // Metadata value
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### RecipeDataItem
|
|
73
|
+
|
|
74
|
+
Describes a data item within a recipe, including path information for accessing nested properties.
|
|
75
|
+
|
|
76
|
+
```csharp
|
|
77
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 171)]
|
|
78
|
+
public classRecipeDataItem : TypedStruct
|
|
79
|
+
{
|
|
80
|
+
public string[] ItemRelativeBrowsePath { get; set; } // Path to the recipe item, relative to the schema's target node
|
|
81
|
+
public string[] DataItemRelativeBrowsePath { get; set; } // Path to data item, relative to the item node reached by ItemRelativeBrowsePath
|
|
82
|
+
public FTOptix.Core.ElementAccessStruct ElementAccess { get; set; } // Array element access (index/range)
|
|
83
|
+
public NodeId DataTypeId { get; set; } // Data type identifier
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### FTOptix.RecipeX.ErrorPolicy
|
|
88
|
+
|
|
89
|
+
Defines error handling behavior for recipe operations.
|
|
90
|
+
|
|
91
|
+
```csharp
|
|
92
|
+
public enum FTOptix.RecipeX.ErrorPolicy : int
|
|
93
|
+
{
|
|
94
|
+
Strict = 0, // Stop on first error
|
|
95
|
+
BestEffort = 1 // Continue despite errors
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### The Edit Model
|
|
100
|
+
|
|
101
|
+
An **Edit Model** is a temporary, session-exclusive working copy of a recipe used to support editing workflows. Key characteristics:
|
|
102
|
+
|
|
103
|
+
- **Session-exclusive**: only one session can hold an Edit Model at a time; concurrent access is not allowed. Each session can have a dedicated Edit Model instance if needed.
|
|
104
|
+
- **Non-destructive**: changes made inside the Edit Model are isolated from the persisted recipe until explicitly committed back to the store.
|
|
105
|
+
- **Queryable**: the Edit Model supports filtering and sorting, making it suitable as the data source for recipe-editing UIs (e.g. grids, lists).
|
|
106
|
+
- **Lifecycle**: the Edit Model is created by loading a recipe via `TransferFromStoreToEditModel` and is automatically destroyed when released or when `TransferFromEditModelToStore` completes. The `requesterNodeId` parameter identifies the session owner and is used to enforce the exclusivity constraint. An `EditModel` can also be created programmatically via `CreateEditModel` for use cases that require more direct control over its lifecycle and interactions or retrieved from an existing interactive session where a `ListView` is being used by the user to interact with a recipe.
|
|
107
|
+
|
|
108
|
+
#### FTOptix.RecipeX.EditModel Class
|
|
109
|
+
|
|
110
|
+
The `FTOptix.RecipeX.EditModel` class exposes the following members. Obtain an instance by resolving the `EditModelNodeId` returned by `CreateEditModel` and casting it:
|
|
111
|
+
|
|
112
|
+
```csharp
|
|
113
|
+
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Properties:**
|
|
117
|
+
|
|
118
|
+
| Member | Type | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `RecipeSchemaNodeId` | `NodeId` | NodeId of the `FTOptix.RecipeX.RecipeSchema` that owns this Edit Model |
|
|
121
|
+
| `RequesterNodeId` | `NodeId` | NodeId of the session owner; used to enforce exclusivity |
|
|
122
|
+
| `TargetNodeId` | `NodeId` | NodeId of the target node the Edit Model was created against |
|
|
123
|
+
| `FTOptix.RecipeX.RecipeId` | `FTOptix.RecipeX.RecipeId` | Identifies which recipe is loaded in the Edit Model |
|
|
124
|
+
| `PendingChanges` | `bool` | `true` if the Edit Model contains unsaved changes relative to the store; set to `false` after a successful commit |
|
|
125
|
+
|
|
126
|
+
**Methods:**
|
|
127
|
+
|
|
128
|
+
| Method | Description |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `TransferFromStore(FTOptix.RecipeX.RecipeId)` | Loads a recipe from the store into this Edit Model |
|
|
131
|
+
| `TransferToStore(FTOptix.RecipeX.RecipeId)` | Commits Edit Model changes back to the store |
|
|
132
|
+
| `TransferToTarget(NodeId targetNodeId, FTOptix.RecipeX.ErrorPolicy)` | Applies Edit Model values to a target node |
|
|
133
|
+
| `TransferFromTarget(NodeId targetNodeId, FTOptix.RecipeX.ErrorPolicy)` | Overwrites Edit Model values with the current state of a target node |
|
|
134
|
+
| `GetDataItemValue(NodeId itemNodeId, string[] dataItemRelativeBrowsePath, FTOptix.Core.ElementAccessStruct elementAccess, NodeId dataTypeId)` | Reads the current in-memory value of a data item held by this Edit Model, without transferring anything to/from the store or target |
|
|
135
|
+
|
|
136
|
+
> [!TIP]
|
|
137
|
+
> `GetDataItemValue` was added into FactoryTalk Optix 1.8.x and is the recommended way to inspect values that a user is currently editing (e.g. in a `ListView` bound to a recipe) **before** calling `TransferFromEditModelToStore` or `TransferFromEditModelToTarget`. Unlike `RecipeSchema.GetRecipeDataItemValue`, which reads from the persisted recipe in the store, `EditModel.GetDataItemValue` reads the live, unsaved value from the Edit Model itself, so it reflects any pending edits. Note that this method takes the resolved `itemNodeId` (the actual node reached by navigating `ItemRelativeBrowsePath` from the Edit Model's `TargetNodeId`), not the browse path itself.
|
|
138
|
+
|
|
139
|
+
Edit Model instances can be programmatically created and/or interacted with as shown in the examples below.
|
|
140
|
+
|
|
141
|
+
### The Schema Editor (Design Time)
|
|
142
|
+
|
|
143
|
+
> [!NOTE]
|
|
144
|
+
> `RecipeSchema.Editor` is available starting from FactoryTalk Optix 1.8.x. It is **design-time only**: use it from a Design Time NetLogic to configure which items and data items belong to a recipe schema. It does not run at runtime.
|
|
145
|
+
|
|
146
|
+
`RecipeSchema.Editor` returns a `RecipeSchemaEditor` that builds the item structure of a recipe schema in code, instead of using the Studio schema editor UI. Browse paths passed to it are relative to the schema's `TargetNode`, exactly like `ItemRelativeBrowsePath`/`DataItemRelativeBrowsePath` described above.
|
|
147
|
+
|
|
148
|
+
```csharp
|
|
149
|
+
var editor = recipeSchema.Editor;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### RecipeItemToAdd
|
|
153
|
+
|
|
154
|
+
Describes one item to add to the schema: either its full subtree, or a selected subset of its data items.
|
|
155
|
+
|
|
156
|
+
```csharp
|
|
157
|
+
public struct RecipeItemToAdd
|
|
158
|
+
{
|
|
159
|
+
public string[] ItemBrowsePath { get; set; } // Path to the item, relative to TargetNode
|
|
160
|
+
public string[][] DataItemsBrowsePaths { get; set; } // Selected data items; null = full subtree
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `new RecipeItemToAdd(itemBrowsePath)` — imports the item and its **entire** subtree of data items.
|
|
165
|
+
- `new RecipeItemToAdd(itemBrowsePath, dataItemsBrowsePaths)` — imports **only** the listed data items.
|
|
166
|
+
|
|
167
|
+
#### SchemaEditorResultCode
|
|
168
|
+
|
|
169
|
+
Result code returned by `RecipeSchemaEditor` methods.
|
|
170
|
+
|
|
171
|
+
```csharp
|
|
172
|
+
public enum SchemaEditorResultCode
|
|
173
|
+
{
|
|
174
|
+
Success = 0,
|
|
175
|
+
ItemNotFoundInTree = 1,
|
|
176
|
+
UnsupportedNodeType = 2,
|
|
177
|
+
NotFoundInSchema = 4,
|
|
178
|
+
OperationFailed = 5
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### RecipeSchemaEditor Methods Used Below
|
|
183
|
+
|
|
184
|
+
| Method | Description |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `ClearSchema()` | Removes all items currently configured in the schema |
|
|
187
|
+
| `AddRecipeItems(RecipeItemToAdd[])` | Adds a batch of items (full subtree or selective data items) to the schema in a single call |
|
|
188
|
+
|
|
189
|
+
## Understanding Data Items
|
|
190
|
+
|
|
191
|
+
### What is a Data Item?
|
|
192
|
+
|
|
193
|
+
A **data item** is the fundamental building block of a recipe. It represents a single value that can be stored, retrieved, and transferred as part of a recipe. Data items map to variables or properties in your application model and provide a structured way to capture and apply configurations.
|
|
194
|
+
|
|
195
|
+
### How Data Items Work
|
|
196
|
+
|
|
197
|
+
When you define a recipe schema in the FactoryTalk Optix designer, you select variables and properties to include in the recipe. Each selected variable becomes a **data item**. The recipe system then:
|
|
198
|
+
|
|
199
|
+
1. **Captures** current values from the target location when saving
|
|
200
|
+
2. **Stores** values in the database as part of the recipe
|
|
201
|
+
3. **Restores** values to the target location when applying
|
|
202
|
+
|
|
203
|
+
### Data Item Structure
|
|
204
|
+
|
|
205
|
+
Data items are hierarchically organized using **browse paths** - arrays of strings that represent the navigation path through your object model:
|
|
206
|
+
|
|
207
|
+
```txt
|
|
208
|
+
ItemRelativeBrowsePath = ["Settings", "Motor"]
|
|
209
|
+
DataItemRelativeBrowsePath = ["Speed", "MaxRPM"]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Please note:** Both paths are **relative**, never absolute.
|
|
213
|
+
- `ItemRelativeBrowsePath` is relative to the **target node** configured on the `FTOptix.RecipeX.RecipeSchema` (e.g. `TargetNode = Section1`).
|
|
214
|
+
- `DataItemRelativeBrowsePath` is relative to the **item node** that is reached after navigating `ItemRelativeBrowsePath` from the target node.
|
|
215
|
+
|
|
216
|
+
This structure allows you to:
|
|
217
|
+
- Navigate through nested objects and folders
|
|
218
|
+
- Access properties within objects
|
|
219
|
+
- Handle array subscripts using `ElementAccess`
|
|
220
|
+
|
|
221
|
+
**Visual Example:**
|
|
222
|
+
```txt
|
|
223
|
+
Target Object (Settings)
|
|
224
|
+
|- Motor (Item)
|
|
225
|
+
| |- Speed (DataItem) <- Gets/Sets here
|
|
226
|
+
| |- MaxRPM (DataItem) <- Gets/Sets here
|
|
227
|
+
|- Conveyor (Item)
|
|
228
|
+
|- Enable (DataItem)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Common Data Item Scenarios
|
|
232
|
+
|
|
233
|
+
#### Simple Variable (No Nesting)
|
|
234
|
+
|
|
235
|
+
```csharp
|
|
236
|
+
var itemPath = new string[] { "Temperature" };
|
|
237
|
+
var dataItemPath = new string[] { }; // Empty = direct variable
|
|
238
|
+
var elementAccess = new FTOptix.Core.ElementAccessStruct();
|
|
239
|
+
|
|
240
|
+
// Gets/sets "Temperature" variable directly
|
|
241
|
+
var result = schema.GetRecipeDataItemValue(
|
|
242
|
+
recipeId,
|
|
243
|
+
itemPath,
|
|
244
|
+
dataItemPath,
|
|
245
|
+
elementAccess);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Nested Object Property
|
|
249
|
+
|
|
250
|
+
```csharp
|
|
251
|
+
var itemPath = new string[] { "MotorSettings" };
|
|
252
|
+
var dataItemPath = new string[] { "Speed" };
|
|
253
|
+
var elementAccess = new FTOptix.Core.ElementAccessStruct();
|
|
254
|
+
|
|
255
|
+
// Gets/sets MotorSettings.Speed
|
|
256
|
+
var result = schema.GetRecipeDataItemValue(
|
|
257
|
+
recipeId,
|
|
258
|
+
itemPath,
|
|
259
|
+
dataItemPath,
|
|
260
|
+
elementAccess);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Deeply Nested Property
|
|
264
|
+
|
|
265
|
+
```csharp
|
|
266
|
+
var itemPath = new string[] { "Parameters", "Industrial", "Motor1" };
|
|
267
|
+
var dataItemPath = new string[] { "Configuration", "MaxSpeed" };
|
|
268
|
+
var elementAccess = new FTOptix.Core.ElementAccessStruct();
|
|
269
|
+
|
|
270
|
+
// Gets/sets Parameters -> Industrial -> Motor1 -> Configuration -> MaxSpeed
|
|
271
|
+
var result = schema.GetRecipeDataItemValue(
|
|
272
|
+
recipeId,
|
|
273
|
+
itemPath,
|
|
274
|
+
dataItemPath,
|
|
275
|
+
elementAccess);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### Array Element Access
|
|
279
|
+
|
|
280
|
+
```csharp
|
|
281
|
+
var itemPath = new string[] { "Sensors" };
|
|
282
|
+
var dataItemPath = new string[] { "Readings" };
|
|
283
|
+
var elementAccess = new FTOptix.Core.ElementAccessStruct
|
|
284
|
+
{
|
|
285
|
+
Index = 0 // Get first array element
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Gets/sets Sensors.Readings[0]
|
|
289
|
+
var result = schema.GetRecipeDataItemValue(
|
|
290
|
+
recipeId,
|
|
291
|
+
itemPath,
|
|
292
|
+
dataItemPath,
|
|
293
|
+
elementAccess);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Working with Data Items in Code
|
|
297
|
+
|
|
298
|
+
#### Retrieving All Data Items in a Recipe
|
|
299
|
+
|
|
300
|
+
```csharp
|
|
301
|
+
[ExportMethod]
|
|
302
|
+
public void InspectDataItems(string recipeName, string version)
|
|
303
|
+
{
|
|
304
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
305
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
306
|
+
|
|
307
|
+
// Get the structure of data items in this recipe
|
|
308
|
+
var result = schema.GetDataItems(recipeId);
|
|
309
|
+
|
|
310
|
+
if (result.ResultCode == GetDataItemsResultCode.Success)
|
|
311
|
+
{
|
|
312
|
+
Log.Info($"Recipe contains {result.DataItems.Length} data items:");
|
|
313
|
+
|
|
314
|
+
foreach (var dataItem in result.DataItems)
|
|
315
|
+
{
|
|
316
|
+
// Build readable path
|
|
317
|
+
var itemPath = string.Join("/", dataItem.ItemRelativeBrowsePath);
|
|
318
|
+
var dataPath = string.Join("/", dataItem.DataItemRelativeBrowsePath);
|
|
319
|
+
|
|
320
|
+
if (!string.IsNullOrEmpty(dataPath))
|
|
321
|
+
{
|
|
322
|
+
Log.Info($" {itemPath} -> {dataPath}");
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
{
|
|
326
|
+
Log.Info($" {itemPath}");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### Reading Individual Data Item Values
|
|
334
|
+
|
|
335
|
+
```csharp
|
|
336
|
+
[ExportMethod]
|
|
337
|
+
public object ReadDataItemValue(string recipeName, string[] itemPath,
|
|
338
|
+
string[] dataItemPath)
|
|
339
|
+
{
|
|
340
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
341
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
|
|
342
|
+
|
|
343
|
+
var result = schema.GetRecipeDataItemValue(
|
|
344
|
+
recipeId,
|
|
345
|
+
itemPath,
|
|
346
|
+
dataItemPath,
|
|
347
|
+
new FTOptix.Core.ElementAccessStruct());
|
|
348
|
+
|
|
349
|
+
if (result.ResultCode == GetRecipeDataItemValueResultCode.Success)
|
|
350
|
+
{
|
|
351
|
+
return result.DataItemValue;
|
|
352
|
+
}
|
|
353
|
+
else
|
|
354
|
+
{
|
|
355
|
+
Log.Error($"Failed to read data item: {result.ResultCode}");
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### Writing Data Item Values
|
|
362
|
+
|
|
363
|
+
```csharp
|
|
364
|
+
[ExportMethod]
|
|
365
|
+
public void WriteDataItemValue(string recipeName, string[] itemPath,
|
|
366
|
+
string[] dataItemPath, object value)
|
|
367
|
+
{
|
|
368
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
369
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
|
|
370
|
+
|
|
371
|
+
var result = schema.SetRecipeDataItemValue(
|
|
372
|
+
recipeId,
|
|
373
|
+
itemPath,
|
|
374
|
+
dataItemPath,
|
|
375
|
+
new FTOptix.Core.ElementAccessStruct(),
|
|
376
|
+
value);
|
|
377
|
+
|
|
378
|
+
if (result == FTOptix.RecipeX.SetRecipeDataItemValueResultCode.Success)
|
|
379
|
+
{
|
|
380
|
+
Log.Info("Data item value updated");
|
|
381
|
+
}
|
|
382
|
+
else
|
|
383
|
+
{
|
|
384
|
+
Log.Error($"Failed to update data item: {result}");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### Comparing Current vs. Stored Values
|
|
390
|
+
|
|
391
|
+
```csharp
|
|
392
|
+
[ExportMethod]
|
|
393
|
+
public bool CompareDataItemWithTarget(string recipeName, string[] itemPath,
|
|
394
|
+
string[] dataItemPath, string targetVariablePath)
|
|
395
|
+
{
|
|
396
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
397
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
|
|
398
|
+
var target = LogicObject.Owner.GetVariable(targetVariablePath);
|
|
399
|
+
|
|
400
|
+
// Get stored recipe value
|
|
401
|
+
var recipeResult = schema.GetRecipeDataItemValue(
|
|
402
|
+
recipeId,
|
|
403
|
+
itemPath,
|
|
404
|
+
dataItemPath,
|
|
405
|
+
new FTOptix.Core.ElementAccessStruct());
|
|
406
|
+
|
|
407
|
+
if (recipeResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
|
|
408
|
+
return false;
|
|
409
|
+
|
|
410
|
+
// Get current target value
|
|
411
|
+
var targetValue = target?.Value.Value;
|
|
412
|
+
|
|
413
|
+
// Compare
|
|
414
|
+
bool valuesMatch = Equals(recipeResult.DataItemValue, targetValue);
|
|
415
|
+
|
|
416
|
+
Log.Info($"Recipe value: {recipeResult.DataItemValue}");
|
|
417
|
+
Log.Info($"Target value: {targetValue}");
|
|
418
|
+
Log.Info($"Match: {valuesMatch}");
|
|
419
|
+
|
|
420
|
+
return valuesMatch;
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Data Item Tips and Best Practices
|
|
425
|
+
|
|
426
|
+
1. **Path Ordering Matters**: The order of path segments must exactly match your object hierarchy
|
|
427
|
+
2. **Empty Data Item Path**: If the item itself is the variable, use an empty array for `dataItemRelativeBrowsePath`
|
|
428
|
+
3. **Type Consistency**: Ensure the value type matches the data item's data type
|
|
429
|
+
4. **Array Indices**: Use `FTOptix.Core.ElementAccessStruct` only for array subscripts; regular object properties go in the data item path
|
|
430
|
+
5. **Performance**: Reading/writing individual data items has overhead; consider batch operations when possible
|
|
431
|
+
|
|
432
|
+
### Common Mistakes
|
|
433
|
+
|
|
434
|
+
**Wrong:** Using the whole path as item path
|
|
435
|
+
```csharp
|
|
436
|
+
// Incorrect
|
|
437
|
+
var itemPath = new string[] { "Settings", "Motor", "Speed" };
|
|
438
|
+
var dataItemPath = new string[] { };
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Right:** Splitting item and property paths
|
|
442
|
+
```csharp
|
|
443
|
+
// Correct
|
|
444
|
+
var itemPath = new string[] { "Settings", "Motor" };
|
|
445
|
+
var dataItemPath = new string[] { "Speed" };
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Result Types
|
|
451
|
+
|
|
452
|
+
All methods return result types containing both a result code and relevant data. These are used to handle success and error conditions consistently.
|
|
453
|
+
|
|
454
|
+
### GetRecipesResult
|
|
455
|
+
|
|
456
|
+
Result of retrieving all recipes from a recipe schema.
|
|
457
|
+
|
|
458
|
+
```csharp
|
|
459
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 182)]
|
|
460
|
+
public class GetRecipesResult : TypedStruct
|
|
461
|
+
{
|
|
462
|
+
public Recipe[] Recipes { get; set; } // Array of recipes
|
|
463
|
+
public GetRecipesResultCode ResultCode { get; set; } // Result code
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Result Codes:**
|
|
468
|
+
- `Success = 0` - Recipes retrieved successfully
|
|
469
|
+
- `StoreError = 1` - Error accessing the store
|
|
470
|
+
- `GenericError = 2` - General error occurred
|
|
471
|
+
|
|
472
|
+
### GetDataItemsResult
|
|
473
|
+
|
|
474
|
+
Result of retrieving data items from a recipe.
|
|
475
|
+
|
|
476
|
+
```csharp
|
|
477
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 178)]
|
|
478
|
+
public class GetDataItemsResult : TypedStruct
|
|
479
|
+
{
|
|
480
|
+
public RecipeDataItem[] DataItems { get; set; } // Array of data items
|
|
481
|
+
public GetDataItemsResultCode ResultCode { get; set; } // Result code
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Result Codes:**
|
|
486
|
+
- `Success = 0` - Data items retrieved successfully
|
|
487
|
+
- `RecipeNotFound = 1` - Specified recipe not found
|
|
488
|
+
- `StoreError = 2` - Error accessing the store
|
|
489
|
+
- `GenericError = 3` - General error occurred
|
|
490
|
+
|
|
491
|
+
### GetRecipeDataItemValueResult
|
|
492
|
+
|
|
493
|
+
Result of retrieving a specific data item value from a recipe.
|
|
494
|
+
|
|
495
|
+
```csharp
|
|
496
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 127)]
|
|
497
|
+
public class GetRecipeDataItemValueResult : TypedStruct
|
|
498
|
+
{
|
|
499
|
+
public object DataItemValue { get; set; } // Retrieved value (scalar or array)
|
|
500
|
+
public GetRecipeDataItemValueResultCode ResultCode { get; set; } // Result code
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Result Codes:**
|
|
505
|
+
- `Success = 0` - Value retrieved successfully
|
|
506
|
+
- `GenericError = 1` - General error occurred
|
|
507
|
+
- `StoreError = 2` - Error accessing the store
|
|
508
|
+
- `DataItemNotFound = 3` - Data item not found in recipe
|
|
509
|
+
- `UnexpectedEmptyRecipeName = 4` - Recipe name is empty/null
|
|
510
|
+
- `RecipeNotFound = 5` - Recipe not found in store
|
|
511
|
+
|
|
512
|
+
### GetDataItemValueResult
|
|
513
|
+
|
|
514
|
+
Result of `FTOptix.RecipeX.EditModel.GetDataItemValue`, used to read the current in-memory value of a data item held by an Edit Model (i.e. the value as currently edited, before it is transferred to the store or a target).
|
|
515
|
+
|
|
516
|
+
```csharp
|
|
517
|
+
public class GetDataItemValueResult : TypedStruct
|
|
518
|
+
{
|
|
519
|
+
public object DataItemValue { get; set; } // Value currently held in the Edit Model (scalar or array)
|
|
520
|
+
public GetDataItemValueResultCode ResultCode { get; set; } // Result code
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Result Codes:**
|
|
525
|
+
- `Success = 0` - Value retrieved successfully
|
|
526
|
+
- `GenericError = 1` - General error occurred
|
|
527
|
+
- `DataItemNotFound = 2` - Data item not found in the Edit Model
|
|
528
|
+
|
|
529
|
+
### FTOptix.RecipeX.GetRecipeMetadataValueResult
|
|
530
|
+
|
|
531
|
+
Result of retrieving a single metadata value.
|
|
532
|
+
|
|
533
|
+
```csharp
|
|
534
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 134)]
|
|
535
|
+
public classGetRecipeMetadataValueResult : TypedStruct
|
|
536
|
+
{
|
|
537
|
+
public RecipeMetadata MetadataValue { get; set; } // Retrieved metadata
|
|
538
|
+
public GetRecipeMetadataValueResultCode ResultCode { get; set; } // Result code
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Result Codes:**
|
|
543
|
+
- `Success = 0` - Metadata retrieved successfully
|
|
544
|
+
- `GenericError = 1` - General error occurred
|
|
545
|
+
- `StoreError = 2` - Error accessing the store
|
|
546
|
+
- `UnexpectedEmptyRecipeName = 3` - Recipe name is empty/null
|
|
547
|
+
- `RecipeNotFound = 4` - Recipe not found in store
|
|
548
|
+
- `MetadataNotFound = 5` - Metadata field not found
|
|
549
|
+
|
|
550
|
+
### GetRecipeMetadataValuesResult
|
|
551
|
+
|
|
552
|
+
Result of retrieving all metadata for a recipe.
|
|
553
|
+
|
|
554
|
+
```csharp
|
|
555
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 138)]
|
|
556
|
+
public class GetRecipeMetadataValuesResult : TypedStruct
|
|
557
|
+
{
|
|
558
|
+
public RecipeMetadata[] MetadataValues { get; set; } // Array of metadata
|
|
559
|
+
public GetRecipeMetadataValuesResultCode ResultCode { get; set; } // Result code
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Result Codes:**
|
|
564
|
+
- `Success = 0` - Metadata values retrieved successfully
|
|
565
|
+
- `GenericError = 1` - General error occurred
|
|
566
|
+
- `StoreError = 2` - Error accessing the store
|
|
567
|
+
- `UnexpectedEmptyRecipeName = 3` - Recipe name is empty/null
|
|
568
|
+
- `RecipeNotFound = 4` - Recipe not found in store
|
|
569
|
+
|
|
570
|
+
### RenameRecipeResult
|
|
571
|
+
|
|
572
|
+
Result of renaming a recipe.
|
|
573
|
+
|
|
574
|
+
```csharp
|
|
575
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 115)]
|
|
576
|
+
public class RenameRecipeResult : TypedStruct
|
|
577
|
+
{
|
|
578
|
+
public FTOptix.RecipeX.RecipeId NewRecipeId { get; set; } // New recipe identifier (after rename)
|
|
579
|
+
public RenameRecipeResultCode ResultCode { get; set; } // Result code
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Result Codes:**
|
|
584
|
+
- `Success = 0` - Recipe renamed successfully
|
|
585
|
+
- `GenericError = 1` - General error occurred
|
|
586
|
+
- `StoreError = 2` - Error accessing the store
|
|
587
|
+
- `UnexpectedEmptyRecipeName = 3` - Recipe name is empty/null
|
|
588
|
+
- `SourceRecipeDoesNotExist = 4` - Recipe not found
|
|
589
|
+
- `TargetRecipeAlreadyExist = 5` - Target recipe name already in use
|
|
590
|
+
|
|
591
|
+
### FTOptix.RecipeX.CreateEditModelResult
|
|
592
|
+
|
|
593
|
+
Result of creating an edit model.
|
|
594
|
+
|
|
595
|
+
```csharp
|
|
596
|
+
[MapDataType(NamespaceUri = Module.Uri, Number = 142)]
|
|
597
|
+
public classCreateEditModelResult : TypedStruct
|
|
598
|
+
{
|
|
599
|
+
public NodeId EditModelNodeId { get; set; } // NodeId of created edit model
|
|
600
|
+
public CreateEditModelResultCode ResultCode { get; set; } // Result code
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Result Codes:**
|
|
605
|
+
- `Success = 0` - Edit model created successfully
|
|
606
|
+
- `GenericError = 1` - General error occurred
|
|
607
|
+
- `StoreError = 2` - Error accessing the store
|
|
608
|
+
- `ParentNodeNotFound = 3` - Parent node for edit model not found
|
|
609
|
+
- `RequesterNodeNotFound = 4` - Requester node not found
|
|
610
|
+
|
|
611
|
+
## FTOptix.RecipeX.RecipeSchema Class
|
|
612
|
+
|
|
613
|
+
The main class for recipe operations in FTOptix. Inherit from `UAObject` and provides methods for all recipe management tasks.
|
|
614
|
+
|
|
615
|
+
```csharp
|
|
616
|
+
[MapType(NamespaceUri = Module.Uri, Number = 1)]
|
|
617
|
+
public classRecipeSchema : UAObject
|
|
618
|
+
{
|
|
619
|
+
// Properties
|
|
620
|
+
public NodeId TargetNode { get; set; } // Target node for recipe operations
|
|
621
|
+
public NodeId Store { get; set; } // Store node reference
|
|
622
|
+
public RecipeSchemaStatus Status { get; } // Current status
|
|
623
|
+
public UInt32 RemoteReadTimeout { get; set; } // Remote read timeout in ms
|
|
624
|
+
public PlaceholderChildNodeCollection<UAVariable> MetaData { get; } // Metadata variables
|
|
625
|
+
|
|
626
|
+
// Methods...
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## FTOptix.RecipeX.RecipeSchema Methods
|
|
631
|
+
|
|
632
|
+
### Recipe Management Methods
|
|
633
|
+
|
|
634
|
+
#### CreateRecipe
|
|
635
|
+
|
|
636
|
+
Creates a new empty recipe in the store.
|
|
637
|
+
|
|
638
|
+
```csharp
|
|
639
|
+
public CreateRecipeResultCode CreateRecipe(FTOptix.RecipeX.RecipeId recipeId)
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**Parameters:**
|
|
643
|
+
- `recipeId` - Identifier with name and version
|
|
644
|
+
|
|
645
|
+
**Returns:** `CreateRecipeResultCode`
|
|
646
|
+
- `Success` - Recipe created successfully
|
|
647
|
+
- `GenericError` - General error occurred
|
|
648
|
+
- `StoreError` - Error accessing the store
|
|
649
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
650
|
+
- `RecipeAlreadyExist` - Recipe with same name/version exists
|
|
651
|
+
- `TargetNotFound` - Target node not found
|
|
652
|
+
|
|
653
|
+
**Example:**
|
|
654
|
+
```csharp
|
|
655
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)hmiProject.GetVariable("RecipeSchema1").Value;
|
|
656
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = "MyRecipe", Version = "1.0" };
|
|
657
|
+
var result = schema.CreateRecipe(recipeId);
|
|
658
|
+
if (result == CreateRecipeResultCode.Success)
|
|
659
|
+
{
|
|
660
|
+
Log.Info("Recipe created successfully");
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
#### DeleteRecipe
|
|
665
|
+
|
|
666
|
+
Deletes a recipe from the store.
|
|
667
|
+
|
|
668
|
+
```csharp
|
|
669
|
+
public DeleteRecipeResultCode DeleteRecipe(FTOptix.RecipeX.RecipeId recipeId)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Parameters:**
|
|
673
|
+
- `recipeId` - Recipe to delete
|
|
674
|
+
|
|
675
|
+
**Returns:** `DeleteRecipeResultCode`
|
|
676
|
+
- `Success` - Recipe deleted successfully
|
|
677
|
+
- `GenericError` - General error occurred
|
|
678
|
+
- `StoreError` - Error accessing the store
|
|
679
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
680
|
+
- `RecipeDoesNotExist` - Recipe not found
|
|
681
|
+
|
|
682
|
+
**Example:**
|
|
683
|
+
```csharp
|
|
684
|
+
var result = schema.DeleteRecipe(recipeId);
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
#### DuplicateRecipe
|
|
688
|
+
|
|
689
|
+
Creates a copy of an existing recipe with a new identifier.
|
|
690
|
+
|
|
691
|
+
```csharp
|
|
692
|
+
public DuplicateRecipeResultCode DuplicateRecipe(FTOptix.RecipeX.RecipeId recipeId, FTOptix.RecipeX.RecipeId newRecipeId)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Parameters:**
|
|
696
|
+
- `recipeId` - Source recipe to duplicate
|
|
697
|
+
- `newRecipeId` - Identifier for the new recipe
|
|
698
|
+
|
|
699
|
+
**Returns:** `DuplicateRecipeResultCode`
|
|
700
|
+
- `Success` - Recipe duplicated successfully
|
|
701
|
+
- `GenericError` - General error during duplication
|
|
702
|
+
- `StoreError` - Error accessing the store
|
|
703
|
+
- `UnexpectedSourceEmptyRecipeName` - Source recipe name is empty
|
|
704
|
+
- `UnexpectedTargetEmptyRecipeName` - Target recipe name is empty
|
|
705
|
+
- `SourceRecipeDoesNotExist` - Source recipe not found
|
|
706
|
+
- `TargetRecipeAlreadyExist` - Target recipe name already exists
|
|
707
|
+
- `TargetNotFound` - Target node not found
|
|
708
|
+
|
|
709
|
+
**Example:**
|
|
710
|
+
```csharp
|
|
711
|
+
var sourceId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1", Version = "1.0" };
|
|
712
|
+
var targetId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1_Copy", Version = "1.0" };
|
|
713
|
+
var result = schema.DuplicateRecipe(sourceId, targetId);
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
#### RenameRecipe
|
|
717
|
+
|
|
718
|
+
Renames an existing recipe.
|
|
719
|
+
|
|
720
|
+
```csharp
|
|
721
|
+
public RenameRecipeResult RenameRecipe(FTOptix.RecipeX.RecipeId recipeId, string newName)
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Parameters:**
|
|
725
|
+
- `recipeId` - Recipe to rename
|
|
726
|
+
- `newName` - New recipe name
|
|
727
|
+
|
|
728
|
+
**Returns:** `RenameRecipeResult`
|
|
729
|
+
- `NewRecipeId` - Updated recipe identifier
|
|
730
|
+
- `ResultCode` - Operation result
|
|
731
|
+
|
|
732
|
+
**Example:**
|
|
733
|
+
```csharp
|
|
734
|
+
var result = schema.RenameRecipe(recipeId, "NewRecipeName");
|
|
735
|
+
if (result.ResultCode == RenameRecipeResultCode.Success)
|
|
736
|
+
{
|
|
737
|
+
Log.Info($"Recipe renamed to: {result.NewRecipeId.Name}");
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Data Retrieval Methods
|
|
742
|
+
|
|
743
|
+
#### GetRecipes
|
|
744
|
+
|
|
745
|
+
Retrieves all recipes from the schema.
|
|
746
|
+
|
|
747
|
+
```csharp
|
|
748
|
+
public GetRecipesResult GetRecipes()
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
**Returns:** `GetRecipesResult`
|
|
752
|
+
- `Recipes` - Array of all recipes
|
|
753
|
+
- `ResultCode` - Operation result
|
|
754
|
+
|
|
755
|
+
**Example:**
|
|
756
|
+
```csharp
|
|
757
|
+
var result = schema.GetRecipes();
|
|
758
|
+
if (result.ResultCode == GetRecipesResultCode.Success)
|
|
759
|
+
{
|
|
760
|
+
foreach (var recipe in result.Recipes)
|
|
761
|
+
{
|
|
762
|
+
Log.Info($"Recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version}");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
#### GetDataItems
|
|
768
|
+
|
|
769
|
+
Retrieves the data item structure for a recipe.
|
|
770
|
+
|
|
771
|
+
```csharp
|
|
772
|
+
public GetDataItemsResult GetDataItems(FTOptix.RecipeX.RecipeId recipeId)
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Parameters:**
|
|
776
|
+
- `recipeId` - Recipe identifier
|
|
777
|
+
|
|
778
|
+
**Returns:** `GetDataItemsResult`
|
|
779
|
+
- `DataItems` - Array describing data items in the recipe
|
|
780
|
+
- `ResultCode` - Operation result
|
|
781
|
+
|
|
782
|
+
**Example:**
|
|
783
|
+
```csharp
|
|
784
|
+
var result = schema.GetDataItems(recipeId);
|
|
785
|
+
if (result.ResultCode == GetDataItemsResultCode.Success)
|
|
786
|
+
{
|
|
787
|
+
foreach (var item in result.DataItems)
|
|
788
|
+
{
|
|
789
|
+
var itemPath = string.Join("/", item.ItemRelativeBrowsePath);
|
|
790
|
+
Log.Info($"Data Item: {itemPath}");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
#### GetRecipeDataItemValue
|
|
796
|
+
|
|
797
|
+
Retrieves a specific data item value from a recipe.
|
|
798
|
+
|
|
799
|
+
```csharp
|
|
800
|
+
public GetRecipeDataItemValueResult GetRecipeDataItemValue(
|
|
801
|
+
FTOptix.RecipeX.RecipeId recipeId,
|
|
802
|
+
string[] itemRelativeBrowsePath,
|
|
803
|
+
string[] dataItemRelativeBrowsePath,
|
|
804
|
+
FTOptix.Core.ElementAccessStruct elementAccess)
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**Parameters:**
|
|
808
|
+
- `recipeId` - Recipe identifier
|
|
809
|
+
- `itemRelativeBrowsePath` - Browse path to item (array of path segments)
|
|
810
|
+
- `dataItemRelativeBrowsePath` - Browse path to data item within the item
|
|
811
|
+
- `elementAccess` - Array element access for subscripts (if applicable)
|
|
812
|
+
|
|
813
|
+
**Returns:** `GetRecipeDataItemValueResult`
|
|
814
|
+
- `DataItemValue` - Retrieved value (scalar or array)
|
|
815
|
+
- `ResultCode` - Operation result
|
|
816
|
+
|
|
817
|
+
**Example:**
|
|
818
|
+
```csharp
|
|
819
|
+
var itemPath = new string[] { "Item1" };
|
|
820
|
+
var dataItemPath = new string[] { "Property1" };
|
|
821
|
+
var elementAccess = new FTOptix.Core.ElementAccessStruct(); // No array index
|
|
822
|
+
|
|
823
|
+
var result = schema.GetRecipeDataItemValue(recipeId, itemPath, dataItemPath, elementAccess);
|
|
824
|
+
if (result.ResultCode == GetRecipeDataItemValueResultCode.Success)
|
|
825
|
+
{
|
|
826
|
+
Log.Info($"Value: {result.DataItemValue}");
|
|
827
|
+
}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
#### GetRecipeMetadataValue
|
|
831
|
+
|
|
832
|
+
Retrieves a single metadata value.
|
|
833
|
+
|
|
834
|
+
```csharp
|
|
835
|
+
public FTOptix.RecipeX.GetRecipeMetadataValueResult GetRecipeMetadataValue(FTOptix.RecipeX.RecipeId recipeId, string metadataName)
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**Parameters:**
|
|
839
|
+
- `recipeId` - Recipe identifier
|
|
840
|
+
- `metadataName` - Name of the metadata field
|
|
841
|
+
|
|
842
|
+
**Returns:** `FTOptix.RecipeX.GetRecipeMetadataValueResult`
|
|
843
|
+
- `MetadataValue` - Retrieved metadata
|
|
844
|
+
- `ResultCode` - Operation result
|
|
845
|
+
|
|
846
|
+
**Example:**
|
|
847
|
+
```csharp
|
|
848
|
+
var result = schema.GetRecipeMetadataValue(recipeId, "Author");
|
|
849
|
+
if (result.ResultCode == GetRecipeMetadataValueResultCode.Success)
|
|
850
|
+
{
|
|
851
|
+
Log.Info($"Author: {result.MetadataValue.Value}");
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
#### GetRecipeMetadataValues
|
|
856
|
+
|
|
857
|
+
Retrieves all metadata for a recipe.
|
|
858
|
+
|
|
859
|
+
```csharp
|
|
860
|
+
public GetRecipeMetadataValuesResult GetRecipeMetadataValues(FTOptix.RecipeX.RecipeId recipeId)
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**Parameters:**
|
|
864
|
+
- `recipeId` - Recipe identifier
|
|
865
|
+
|
|
866
|
+
**Returns:** `GetRecipeMetadataValuesResult`
|
|
867
|
+
- `MetadataValues` - Array of all metadata fields
|
|
868
|
+
- `ResultCode` - Operation result
|
|
869
|
+
|
|
870
|
+
**Example:**
|
|
871
|
+
```csharp
|
|
872
|
+
var result = schema.GetRecipeMetadataValues(recipeId);
|
|
873
|
+
if (result.ResultCode == GetRecipeMetadataValuesResultCode.Success)
|
|
874
|
+
{
|
|
875
|
+
foreach (var metadata in result.MetadataValues)
|
|
876
|
+
{
|
|
877
|
+
Log.Info($"{metadata.Name}: {metadata.Value}");
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### Data Modification Methods
|
|
883
|
+
|
|
884
|
+
#### SetRecipeDataItemValue
|
|
885
|
+
|
|
886
|
+
Updates a data item value in a recipe.
|
|
887
|
+
|
|
888
|
+
```csharp
|
|
889
|
+
public FTOptix.RecipeX.SetRecipeDataItemValueResultCode SetRecipeDataItemValue(
|
|
890
|
+
FTOptix.RecipeX.RecipeId recipeId,
|
|
891
|
+
string[] itemRelativeBrowsePath,
|
|
892
|
+
string[] dataItemRelativeBrowsePath,
|
|
893
|
+
FTOptix.Core.ElementAccessStruct elementAccess,
|
|
894
|
+
object value)
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**Parameters:**
|
|
898
|
+
- `recipeId` - Recipe identifier
|
|
899
|
+
- `itemRelativeBrowsePath` - Browse path to item
|
|
900
|
+
- `dataItemRelativeBrowsePath` - Browse path to data item
|
|
901
|
+
- `elementAccess` - Array element access
|
|
902
|
+
- `value` - New value to set
|
|
903
|
+
|
|
904
|
+
**Returns:** `FTOptix.RecipeX.SetRecipeDataItemValueResultCode`
|
|
905
|
+
- `Success` - Value set successfully
|
|
906
|
+
- `GenericError` - General error occurred
|
|
907
|
+
- `StoreError` - Error accessing the store
|
|
908
|
+
- `DataItemNotFound` - Data item not found
|
|
909
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
910
|
+
- `RecipeNotFound` - Recipe not found
|
|
911
|
+
|
|
912
|
+
**Example:**
|
|
913
|
+
```csharp
|
|
914
|
+
var result = schema.SetRecipeDataItemValue(
|
|
915
|
+
recipeId,
|
|
916
|
+
new string[] { "Item1" },
|
|
917
|
+
new string[] { "Property1" },
|
|
918
|
+
new FTOptix.Core.ElementAccessStruct(),
|
|
919
|
+
42);
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
#### SetRecipeMetadataValue
|
|
923
|
+
|
|
924
|
+
Updates a metadata value.
|
|
925
|
+
|
|
926
|
+
```csharp
|
|
927
|
+
public FTOptix.RecipeX.SetRecipeMetadataValueResultCode SetRecipeMetadataValue(
|
|
928
|
+
FTOptix.RecipeX.RecipeId recipeId,
|
|
929
|
+
string metadataName,
|
|
930
|
+
object value)
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
**Parameters:**
|
|
934
|
+
- `recipeId` - Recipe identifier
|
|
935
|
+
- `metadataName` - Metadata field name
|
|
936
|
+
- `value` - New metadata value
|
|
937
|
+
|
|
938
|
+
**Returns:** `FTOptix.RecipeX.SetRecipeMetadataValueResultCode`
|
|
939
|
+
- `Success` - Metadata set successfully
|
|
940
|
+
- `GenericError` - General error occurred
|
|
941
|
+
- `StoreError` - Error accessing the store
|
|
942
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
943
|
+
- `RecipeNotFound` - Recipe not found
|
|
944
|
+
- `MetadataNotFound` - Metadata field not found
|
|
945
|
+
|
|
946
|
+
**Example:**
|
|
947
|
+
```csharp
|
|
948
|
+
var result = schema.SetRecipeMetadataValue(recipeId, "Author", "John Doe");
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
### Transfer Methods
|
|
952
|
+
|
|
953
|
+
Transfer methods move recipes between three locations: **Store** (persistent database), **Edit Model** (temporary working copy), and **Target** (active runtime location).
|
|
954
|
+
|
|
955
|
+
#### TransferFromStoreToTarget
|
|
956
|
+
|
|
957
|
+
Loads a recipe from the store and applies it to a target location.
|
|
958
|
+
|
|
959
|
+
```csharp
|
|
960
|
+
public FTOptix.RecipeX.TransferFromStoreToTargetResultCode TransferFromStoreToTarget(
|
|
961
|
+
FTOptix.RecipeX.RecipeId recipeId,
|
|
962
|
+
NodeId targetNodeId,
|
|
963
|
+
FTOptix.RecipeX.ErrorPolicy errorPolicy)
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
**Parameters:**
|
|
967
|
+
- `recipeId` - Recipe to transfer
|
|
968
|
+
- `targetNodeId` - Destination node
|
|
969
|
+
- `errorPolicy` - Error handling mode
|
|
970
|
+
|
|
971
|
+
**Returns:** `FTOptix.RecipeX.TransferFromStoreToTargetResultCode`
|
|
972
|
+
- `Success` - Transfer successful
|
|
973
|
+
- `GenericError` - General error occurred
|
|
974
|
+
- `StoreError` - Error accessing the store
|
|
975
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
976
|
+
- `RecipeNotFound` - Recipe not found
|
|
977
|
+
- `TargetNotFound` - Target node not found
|
|
978
|
+
- `DataMismatch` - Data structure mismatch
|
|
979
|
+
|
|
980
|
+
**Example:**
|
|
981
|
+
```csharp
|
|
982
|
+
var targetNode = hmiProject.GetVariable("TargetObject").Value;
|
|
983
|
+
var result = schema.TransferFromStoreToTarget(recipeId, targetNode, FTOptix.RecipeX.ErrorPolicy.Strict);
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
#### TransferFromTargetToStore
|
|
987
|
+
|
|
988
|
+
Captures the current state of a target and saves it as a recipe.
|
|
989
|
+
|
|
990
|
+
```csharp
|
|
991
|
+
public FTOptix.RecipeX.TransferFromTargetToStoreResultCode TransferFromTargetToStore(
|
|
992
|
+
FTOptix.RecipeX.RecipeId recipeId,
|
|
993
|
+
NodeId targetNodeId,
|
|
994
|
+
bool overwrite,
|
|
995
|
+
FTOptix.RecipeX.ErrorPolicy errorPolicy)
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**Parameters:**
|
|
999
|
+
- `recipeId` - Recipe identifier to create/update
|
|
1000
|
+
- `targetNodeId` - Source location to capture
|
|
1001
|
+
- `overwrite` - Whether to overwrite existing recipe
|
|
1002
|
+
- `errorPolicy` - Error handling mode
|
|
1003
|
+
|
|
1004
|
+
**Returns:** `FTOptix.RecipeX.TransferFromTargetToStoreResultCode`
|
|
1005
|
+
- `SuccessRecipeCreated` - New recipe created
|
|
1006
|
+
- `SuccessRecipeUpdated` - Existing recipe updated
|
|
1007
|
+
- `GenericError` - General error
|
|
1008
|
+
- `StoreError` - Store access error
|
|
1009
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
1010
|
+
- `RecipeAlreadyExists` - Recipe exists and overwrite is false
|
|
1011
|
+
- `TargetNotFound` - Target node not found
|
|
1012
|
+
- `DataMismatch` - Data structure mismatch
|
|
1013
|
+
|
|
1014
|
+
**Example:**
|
|
1015
|
+
```csharp
|
|
1016
|
+
var result = schema.TransferFromTargetToStore(
|
|
1017
|
+
recipeId,
|
|
1018
|
+
targetNode,
|
|
1019
|
+
overwrite: true,
|
|
1020
|
+
FTOptix.RecipeX.ErrorPolicy.BestEffort);
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
#### TransferFromStoreToEditModel
|
|
1024
|
+
|
|
1025
|
+
Loads a recipe from the store into the session's Edit Model.
|
|
1026
|
+
|
|
1027
|
+
```csharp
|
|
1028
|
+
public FTOptix.RecipeX.TransferFromStoreToEditModelResultCode TransferFromStoreToEditModel(
|
|
1029
|
+
NodeId requesterNodeId,
|
|
1030
|
+
FTOptix.RecipeX.RecipeId recipeId)
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
**Parameters:**
|
|
1034
|
+
- `requesterNodeId` - Node requesting the operation
|
|
1035
|
+
- `recipeId` - Recipe to load
|
|
1036
|
+
|
|
1037
|
+
**Returns:** `FTOptix.RecipeX.TransferFromStoreToEditModelResultCode`
|
|
1038
|
+
- `Success` - Transfer successful
|
|
1039
|
+
- `GenericError` - General error
|
|
1040
|
+
- `StoreError` - Store access error
|
|
1041
|
+
- `RequesterNodeNotFound` - Requester node not found
|
|
1042
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
1043
|
+
- `RecipeNotFound` - Recipe not found
|
|
1044
|
+
- `EditModelNodeNotFound` - Edit model not found
|
|
1045
|
+
|
|
1046
|
+
**Example:**
|
|
1047
|
+
```csharp
|
|
1048
|
+
var requester = hmiProject.GetVariable("LogicObject").Value;
|
|
1049
|
+
var result = schema.TransferFromStoreToEditModel(requester, recipeId);
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
#### TransferFromEditModelToStore
|
|
1053
|
+
|
|
1054
|
+
Saves the edit model changes back to the store.
|
|
1055
|
+
|
|
1056
|
+
```csharp
|
|
1057
|
+
public FTOptix.RecipeX.TransferFromEditModelToStoreResultCode TransferFromEditModelToStore(
|
|
1058
|
+
NodeId requesterNodeId,
|
|
1059
|
+
FTOptix.RecipeX.RecipeId recipeId)
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
**Parameters:**
|
|
1063
|
+
- `requesterNodeId` - Node requesting the operation
|
|
1064
|
+
- `recipeId` - Recipe identifier (for new recipes)
|
|
1065
|
+
|
|
1066
|
+
**Returns:** `FTOptix.RecipeX.TransferFromEditModelToStoreResultCode`
|
|
1067
|
+
- `SuccessRecipeCreated` - New recipe created
|
|
1068
|
+
- `SuccessRecipeUpdated` - Existing recipe updated
|
|
1069
|
+
- `GenericError` - General error
|
|
1070
|
+
- `StoreError` - Store access error
|
|
1071
|
+
- `RequesterNodeNotFound` - Requester node not found
|
|
1072
|
+
- `UnexpectedEmptyRecipeName` - Recipe name is empty
|
|
1073
|
+
- `TargetNotFound` - Target not found
|
|
1074
|
+
- `EditModelNodeNotFound` - Edit model not found
|
|
1075
|
+
|
|
1076
|
+
**Example:**
|
|
1077
|
+
```csharp
|
|
1078
|
+
var result = schema.TransferFromEditModelToStore(requester, recipeId);
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
#### TransferFromEditModelToTarget
|
|
1082
|
+
|
|
1083
|
+
Applies edit model changes to a target location.
|
|
1084
|
+
|
|
1085
|
+
```csharp
|
|
1086
|
+
public FTOptix.RecipeX.TransferFromEditModelToTargetResultCode TransferFromEditModelToTarget(
|
|
1087
|
+
NodeId requesterNodeId,
|
|
1088
|
+
NodeId targetNodeId,
|
|
1089
|
+
FTOptix.RecipeX.ErrorPolicy errorPolicy)
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
**Parameters:**
|
|
1093
|
+
- `requesterNodeId` - Node requesting the operation
|
|
1094
|
+
- `targetNodeId` - Destination node
|
|
1095
|
+
- `errorPolicy` - Error handling mode
|
|
1096
|
+
|
|
1097
|
+
**Returns:** `FTOptix.RecipeX.TransferFromEditModelToTargetResultCode`
|
|
1098
|
+
- `Success` - Transfer successful
|
|
1099
|
+
- `GenericError` - General error
|
|
1100
|
+
- `StoreError` - Store access error
|
|
1101
|
+
- `RequesterNodeNotFound` - Requester node not found
|
|
1102
|
+
- `TargetNotFound` - Target node not found
|
|
1103
|
+
- `EditModelNodeNotFound` - Edit model not found
|
|
1104
|
+
- `DataMismatch` - Data structure mismatch
|
|
1105
|
+
|
|
1106
|
+
#### TransferFromTargetToEditModel
|
|
1107
|
+
|
|
1108
|
+
Loads target state into edit model for editing.
|
|
1109
|
+
|
|
1110
|
+
```csharp
|
|
1111
|
+
public FTOptix.RecipeX.TransferFromTargetToEditModelResultCode TransferFromTargetToEditModel(
|
|
1112
|
+
NodeId requesterNodeId,
|
|
1113
|
+
NodeId targetNodeId,
|
|
1114
|
+
FTOptix.RecipeX.ErrorPolicy errorPolicy)
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
**Parameters:**
|
|
1118
|
+
- `requesterNodeId` - Node requesting the operation
|
|
1119
|
+
- `targetNodeId` - Source location
|
|
1120
|
+
- `errorPolicy` - Error handling mode
|
|
1121
|
+
|
|
1122
|
+
**Returns:** `FTOptix.RecipeX.TransferFromTargetToEditModelResultCode`
|
|
1123
|
+
- `Success` - Transfer successful
|
|
1124
|
+
- `GenericError` - General error
|
|
1125
|
+
- `StoreError` - Store access error
|
|
1126
|
+
- `RequesterNodeNotFound` - Requester node not found
|
|
1127
|
+
- `TargetNotFound` - Target node not found
|
|
1128
|
+
- `EditModelNodeNotFound` - Edit model not found
|
|
1129
|
+
- `DataMismatch` - Data structure mismatch
|
|
1130
|
+
|
|
1131
|
+
### Edit Model Methods
|
|
1132
|
+
|
|
1133
|
+
#### CreateEditModel
|
|
1134
|
+
|
|
1135
|
+
Creates a working copy of a recipe for editing.
|
|
1136
|
+
|
|
1137
|
+
```csharp
|
|
1138
|
+
public FTOptix.RecipeX.CreateEditModelResult CreateEditModel(
|
|
1139
|
+
NodeId parentNodeId,
|
|
1140
|
+
NodeId requesterNodeId,
|
|
1141
|
+
NodeId targetNodeId,
|
|
1142
|
+
FTOptix.RecipeX.RecipeId recipeId)
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
**Parameters:**
|
|
1146
|
+
- `parentNodeId` - Parent node for the edit model
|
|
1147
|
+
- `requesterNodeId` - Node requesting the operation
|
|
1148
|
+
- `targetNodeId` - Target structure reference
|
|
1149
|
+
- `recipeId` - Recipe to edit
|
|
1150
|
+
|
|
1151
|
+
**Returns:** `FTOptix.RecipeX.CreateEditModelResult`
|
|
1152
|
+
- `EditModelNodeId` - NodeId of created edit model
|
|
1153
|
+
- `ResultCode` - Operation result
|
|
1154
|
+
|
|
1155
|
+
> [!TIP]
|
|
1156
|
+
> Once you have the `EditModelNodeId`, you can resolve it to the actual `FTOptix.RecipeX.EditModel` node and cast it to `FTOptix.RecipeX.EditModel`. The cast exposes additional members not available through `FTOptix.RecipeX.RecipeSchema`, including:
|
|
1157
|
+
> - `PendingChanges` — indicates whether the edit model has unsaved changes relative to the recipe in the store.
|
|
1158
|
+
> - Transfer methods directly on the `FTOptix.RecipeX.EditModel` instance (e.g. applying to target, committing to store) that operate implicitly on this edit model without having to pass the requester/node again.
|
|
1159
|
+
|
|
1160
|
+
> [!NOTE]
|
|
1161
|
+
> `CreateEditModel` behaves as **get-or-create**: if an edit model already exists for the given `parentNodeId`/`requesterNodeId` pair, it returns the existing instance instead of creating a new one. This is the correct way to access an edit model that was created and is managed by a `ListView` widget — pass the `ListView.NodeId` as both `parentNodeId` and `requesterNodeId`, and you will get back the live edit model the ListView is currently using.
|
|
1162
|
+
|
|
1163
|
+
**Example:**
|
|
1164
|
+
```csharp
|
|
1165
|
+
var parent = hmiProject.GetVariable("Parent").Value;
|
|
1166
|
+
var requester = hmiProject.GetVariable("Requester").Value;
|
|
1167
|
+
var target = hmiProject.GetVariable("Target").Value;
|
|
1168
|
+
|
|
1169
|
+
var result = schema.CreateEditModel(parent, requester, target, recipeId);
|
|
1170
|
+
if (result.ResultCode == CreateEditModelResultCode.Success)
|
|
1171
|
+
{
|
|
1172
|
+
// Cast to FTOptix.RecipeX.EditModel to access PendingChanges and FTOptix.RecipeX.EditModel-level transfer methods
|
|
1173
|
+
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(result.EditModelNodeId);
|
|
1174
|
+
Log.Info($"Pending changes: {editModel.PendingChanges}");
|
|
1175
|
+
}
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Maintenance Methods
|
|
1179
|
+
|
|
1180
|
+
#### RefactorRecipes
|
|
1181
|
+
|
|
1182
|
+
Asynchronously refactors the recipe schema. This is a long-running operation that completes asynchronously. When finished, a `RefactorCompleted` event is fired.
|
|
1183
|
+
|
|
1184
|
+
```csharp
|
|
1185
|
+
public void RefactorRecipes()
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
**Notes:**
|
|
1189
|
+
- This is the only async method
|
|
1190
|
+
- Does not block the calling thread
|
|
1191
|
+
- Listen for `RefactorCompleted` event for completion notification
|
|
1192
|
+
|
|
1193
|
+
**Example:**
|
|
1194
|
+
```csharp
|
|
1195
|
+
schema.RefactorRecipes();
|
|
1196
|
+
// Method returns immediately
|
|
1197
|
+
// Listen for RefactorCompleted event to know when complete
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
## Events
|
|
1201
|
+
|
|
1202
|
+
### RefactorCompleted
|
|
1203
|
+
|
|
1204
|
+
Fired when the `RefactorRecipes()` async method completes.
|
|
1205
|
+
|
|
1206
|
+
**Example:**
|
|
1207
|
+
```csharp
|
|
1208
|
+
public class RecipeRefactorLogic : BaseNetLogic
|
|
1209
|
+
{
|
|
1210
|
+
public override void Start()
|
|
1211
|
+
{
|
|
1212
|
+
var schema = LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1213
|
+
if (schema != null)
|
|
1214
|
+
{
|
|
1215
|
+
// Subscribe to event
|
|
1216
|
+
schema.EventOccurred += OnRefactorCompleted;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private void OnRefactorCompleted(IUANode sender, UAEventArgs args)
|
|
1221
|
+
{
|
|
1222
|
+
if (args.Event.BrowseName.Name == "RefactorCompleted")
|
|
1223
|
+
{
|
|
1224
|
+
Log.Info("Recipe refactoring completed");
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
### RecipeApplicationEvent
|
|
1231
|
+
|
|
1232
|
+
Fired when `TransferFromStoreToTarget` or `TransferFromEditModelToTarget` operations complete successfully.
|
|
1233
|
+
|
|
1234
|
+
## Database Schema
|
|
1235
|
+
|
|
1236
|
+
RecipeX uses database tables to persist recipe data. Understanding this structure is useful for direct database queries.
|
|
1237
|
+
|
|
1238
|
+
### Recipes Table
|
|
1239
|
+
|
|
1240
|
+
```sql
|
|
1241
|
+
CREATE TABLE Recipes (
|
|
1242
|
+
Id, -- PRIMARY KEY [UInt32]
|
|
1243
|
+
Name, -- [String] Recipe name
|
|
1244
|
+
Version, -- [String] Recipe version
|
|
1245
|
+
RecipeSchemaName, -- [String] Schema name
|
|
1246
|
+
RecipeSchemaNamespaceUri, -- [String] Namespace URI
|
|
1247
|
+
RecipeSchemaTimestamp, -- [DateTime] Schema timestamp
|
|
1248
|
+
CreatedAt, -- [DateTime] Creation time
|
|
1249
|
+
ModifiedAt -- [DateTime] Last modification
|
|
1250
|
+
)
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
### RecipeItems Table
|
|
1254
|
+
|
|
1255
|
+
```sql
|
|
1256
|
+
CREATE TABLE RecipeItems (
|
|
1257
|
+
Id, -- PRIMARY KEY [UInt32]
|
|
1258
|
+
FTOptix.RecipeX.RecipeId, -- [UInt32] Foreign key to Recipes
|
|
1259
|
+
RelativeBrowsePath, -- [String] Item path
|
|
1260
|
+
TypeId -- [String] OPC UA type identifier
|
|
1261
|
+
)
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### RecipeDataItems Table
|
|
1265
|
+
|
|
1266
|
+
```sql
|
|
1267
|
+
CREATE TABLE RecipeDataItems (
|
|
1268
|
+
Id, -- PRIMARY KEY [UInt32]
|
|
1269
|
+
RecipeItemId, -- [UInt32] Foreign key to RecipeItems
|
|
1270
|
+
RelativeBrowsePath, -- [String] Data item path
|
|
1271
|
+
ElementAccess, -- [String] Array element access
|
|
1272
|
+
DataTypeId, -- [String] OPC UA data type
|
|
1273
|
+
Value -- [String] Stored value
|
|
1274
|
+
)
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
### RecipeMetadata Table
|
|
1278
|
+
|
|
1279
|
+
Dynamically created as `RecipeMetadata_<RecipeSchemaName>` per schema.
|
|
1280
|
+
|
|
1281
|
+
```sql
|
|
1282
|
+
-- Example: RecipeMetadata_RecipeSchema1
|
|
1283
|
+
CREATE TABLE RecipeMetadata_RecipeSchema1 (
|
|
1284
|
+
FTOptix.RecipeX.RecipeId, -- Foreign key to Recipes
|
|
1285
|
+
Metadata1, -- [UserDefined] Custom metadata
|
|
1286
|
+
Metadata2, -- [UserDefined] Custom metadata
|
|
1287
|
+
-- ... additional metadata columns as defined in schema
|
|
1288
|
+
)
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
**Sample Query:**
|
|
1292
|
+
```sql
|
|
1293
|
+
SELECT R.*, M.*
|
|
1294
|
+
FROM Recipes AS R
|
|
1295
|
+
LEFT JOIN "RecipeMetadata_RecipeSchema1" AS M
|
|
1296
|
+
ON R.Id = M.FTOptix.RecipeX.RecipeId
|
|
1297
|
+
WHERE R.RecipeSchemaName = 'RecipeSchema1'
|
|
1298
|
+
ORDER BY R.Name
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
## Common Usage Patterns
|
|
1302
|
+
|
|
1303
|
+
### Complete Recipe Workflow
|
|
1304
|
+
|
|
1305
|
+
```csharp
|
|
1306
|
+
public class RecipeManager : BaseNetLogic
|
|
1307
|
+
{
|
|
1308
|
+
public override void Start()
|
|
1309
|
+
{
|
|
1310
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1311
|
+
|
|
1312
|
+
// Create recipe
|
|
1313
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = "Configuration1", Version = "1.0" };
|
|
1314
|
+
var createResult = schema.CreateRecipe(recipeId);
|
|
1315
|
+
|
|
1316
|
+
if (createResult == CreateRecipeResultCode.Success)
|
|
1317
|
+
{
|
|
1318
|
+
// Set metadata
|
|
1319
|
+
schema.SetRecipeMetadataValue(recipeId, "Author", "Engineering");
|
|
1320
|
+
schema.SetRecipeMetadataValue(recipeId, "Department", "Manufacturing");
|
|
1321
|
+
|
|
1322
|
+
// Set data values
|
|
1323
|
+
var itemPath = new string[] { "Settings" };
|
|
1324
|
+
var dataItemPath = new string[] { "Parameter1" };
|
|
1325
|
+
var elementAccess = new FTOptix.Core.ElementAccessStruct();
|
|
1326
|
+
|
|
1327
|
+
schema.SetRecipeDataItemValue(recipeId, itemPath, dataItemPath, elementAccess, 100);
|
|
1328
|
+
|
|
1329
|
+
Log.Info($"Recipe '{recipeId.Name}' created and configured");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
### Loading and Applying a Recipe
|
|
1336
|
+
|
|
1337
|
+
```csharp
|
|
1338
|
+
public class RecipeLoader : BaseNetLogic
|
|
1339
|
+
{
|
|
1340
|
+
[ExportMethod]
|
|
1341
|
+
public void ApplyRecipe(string recipeName, string version)
|
|
1342
|
+
{
|
|
1343
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1344
|
+
var targetNode = LogicObject.Owner.GetVariable("TargetObject").Value;
|
|
1345
|
+
|
|
1346
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
1347
|
+
|
|
1348
|
+
var result = schema.TransferFromStoreToTarget(
|
|
1349
|
+
recipeId,
|
|
1350
|
+
targetNode,
|
|
1351
|
+
FTOptix.RecipeX.ErrorPolicy.Strict);
|
|
1352
|
+
|
|
1353
|
+
if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
|
|
1354
|
+
{
|
|
1355
|
+
Log.Info($"Recipe '{recipeName}' applied to target");
|
|
1356
|
+
}
|
|
1357
|
+
else
|
|
1358
|
+
{
|
|
1359
|
+
Log.Error($"Failed to apply recipe: {result}");
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
### Capturing Configuration as Recipe
|
|
1366
|
+
|
|
1367
|
+
```csharp
|
|
1368
|
+
public class ConfigurationCapture : BaseNetLogic
|
|
1369
|
+
{
|
|
1370
|
+
[ExportMethod]
|
|
1371
|
+
public void SaveCurrentConfiguration(string recipeName)
|
|
1372
|
+
{
|
|
1373
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1374
|
+
var targetNode = LogicObject.Owner.GetVariable("SourceObject").Value;
|
|
1375
|
+
|
|
1376
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
|
|
1377
|
+
|
|
1378
|
+
var result = schema.TransferFromTargetToStore(
|
|
1379
|
+
recipeId,
|
|
1380
|
+
targetNode,
|
|
1381
|
+
overwrite: true,
|
|
1382
|
+
FTOptix.RecipeX.ErrorPolicy.BestEffort);
|
|
1383
|
+
|
|
1384
|
+
if (result == FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeUpdated ||
|
|
1385
|
+
result == FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeCreated)
|
|
1386
|
+
{
|
|
1387
|
+
Log.Info($"Configuration saved as '{recipeName}'");
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
### Listing All Recipes
|
|
1394
|
+
|
|
1395
|
+
```csharp
|
|
1396
|
+
public class RecipeList : BaseNetLogic
|
|
1397
|
+
{
|
|
1398
|
+
[ExportMethod]
|
|
1399
|
+
public void ListAllRecipes()
|
|
1400
|
+
{
|
|
1401
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1402
|
+
|
|
1403
|
+
var result = schema.GetRecipes();
|
|
1404
|
+
|
|
1405
|
+
if (result.ResultCode == GetRecipesResultCode.Success)
|
|
1406
|
+
{
|
|
1407
|
+
foreach (var recipe in result.Recipes)
|
|
1408
|
+
{
|
|
1409
|
+
var metadata = schema.GetRecipeMetadataValues(recipe.FTOptix.RecipeX.RecipeId);
|
|
1410
|
+
|
|
1411
|
+
Log.Info($"Recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version}");
|
|
1412
|
+
Log.Info($" Created: {recipe.CreatedAt}");
|
|
1413
|
+
Log.Info($" Modified: {recipe.ModifiedAt}");
|
|
1414
|
+
|
|
1415
|
+
if (metadata.ResultCode == GetRecipeMetadataValuesResultCode.Success)
|
|
1416
|
+
{
|
|
1417
|
+
foreach (var meta in metadata.MetadataValues)
|
|
1418
|
+
{
|
|
1419
|
+
Log.Info($" {meta.Name}: {meta.Value}");
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
## Practical Examples
|
|
1429
|
+
|
|
1430
|
+
### Configure a Recipe Schema at Design Time
|
|
1431
|
+
|
|
1432
|
+
Use `RecipeSchema.Editor` from a Design Time NetLogic to (re)build a schema's item structure: import one item's full subtree, and import only selected data items from other items.
|
|
1433
|
+
|
|
1434
|
+
```csharp
|
|
1435
|
+
public class RecipeSchemaLogic : BaseNetLogic
|
|
1436
|
+
{
|
|
1437
|
+
// Configures MyRecipeSchema at design time: MyMotor1 gets its full subtree, MyMotor2/MyMotor3 get only selected data items.
|
|
1438
|
+
[ExportMethod]
|
|
1439
|
+
public void ConfigureMotorRecipeSchema()
|
|
1440
|
+
{
|
|
1441
|
+
if (LogicObject.Owner is not RecipeSchema recipeSchema)
|
|
1442
|
+
{
|
|
1443
|
+
Log.Error("RecipeSchemaLogic", "Owner is not a RecipeSchema");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
var editor = recipeSchema.Editor;
|
|
1448
|
+
|
|
1449
|
+
try
|
|
1450
|
+
{
|
|
1451
|
+
var clearResult = editor.ClearSchema();
|
|
1452
|
+
if (clearResult != SchemaEditorResultCode.Success)
|
|
1453
|
+
{
|
|
1454
|
+
Log.Error("RecipeSchemaLogic", $"ClearSchema failed: {clearResult}");
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
var itemsToAdd = new RecipeItemToAdd[]
|
|
1459
|
+
{
|
|
1460
|
+
// Full subtree: Enable, Speed, Acceleration
|
|
1461
|
+
new(new string[] { "MyMotor1" }),
|
|
1462
|
+
// Selective: Speed only
|
|
1463
|
+
new(new string[] { "MyMotor2" }, new string[][] { new string[] { "Speed" } }),
|
|
1464
|
+
// Selective: Acceleration only
|
|
1465
|
+
new(new string[] { "MyMotor3" }, new string[][] { new string[] { "Acceleration" } })
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
var addResult = editor.AddRecipeItems(itemsToAdd);
|
|
1469
|
+
switch (addResult)
|
|
1470
|
+
{
|
|
1471
|
+
case SchemaEditorResultCode.Success:
|
|
1472
|
+
Log.Info("RecipeSchemaLogic", "Recipe schema configured successfully");
|
|
1473
|
+
break;
|
|
1474
|
+
case SchemaEditorResultCode.ItemNotFoundInTree:
|
|
1475
|
+
Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: ItemNotFoundInTree (an item path was not found under TargetNode)");
|
|
1476
|
+
break;
|
|
1477
|
+
case SchemaEditorResultCode.UnsupportedNodeType:
|
|
1478
|
+
Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: UnsupportedNodeType (an item resolved to an unsupported node type)");
|
|
1479
|
+
break;
|
|
1480
|
+
case SchemaEditorResultCode.NotFoundInSchema:
|
|
1481
|
+
Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: NotFoundInSchema");
|
|
1482
|
+
break;
|
|
1483
|
+
case SchemaEditorResultCode.OperationFailed:
|
|
1484
|
+
Log.Error("RecipeSchemaLogic", "AddRecipeItems failed: OperationFailed");
|
|
1485
|
+
break;
|
|
1486
|
+
default:
|
|
1487
|
+
Log.Error("RecipeSchemaLogic", $"AddRecipeItems failed: {addResult}");
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
catch (Exception ex)
|
|
1492
|
+
{
|
|
1493
|
+
Log.Error("RecipeSchemaLogic", $"Unexpected exception configuring recipe schema: {ex.Message}");
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
Here, the schema's `TargetNode` points at a folder containing `MyMotor1`, `MyMotor2` and `MyMotor3` (each with `Enable`, `Speed` and `Acceleration` variables):
|
|
1500
|
+
- `MyMotor1` is added with no `DataItemsBrowsePaths` — its entire subtree (`Enable`, `Speed`, `Acceleration`) is imported.
|
|
1501
|
+
- `MyMotor2` and `MyMotor3` are added with an explicit `DataItemsBrowsePaths`, so only `Speed` (respectively `Acceleration`) is imported for them.
|
|
1502
|
+
|
|
1503
|
+
> [!NOTE]
|
|
1504
|
+
> Calling `ClearSchema()` before `AddRecipeItems(...)` resets the schema's item structure before rebuilding it, which is useful when re-running this logic (e.g. after changing which items/data items should be included).
|
|
1505
|
+
|
|
1506
|
+
### Checking if a Recipe Exists
|
|
1507
|
+
|
|
1508
|
+
Verify whether a recipe exists before attempting operations:
|
|
1509
|
+
|
|
1510
|
+
```csharp
|
|
1511
|
+
[ExportMethod]
|
|
1512
|
+
public bool RecipeExists(string recipeName, string version)
|
|
1513
|
+
{
|
|
1514
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1515
|
+
if (schema == null || string.IsNullOrEmpty(recipeName))
|
|
1516
|
+
return false;
|
|
1517
|
+
|
|
1518
|
+
var result = schema.GetRecipes();
|
|
1519
|
+
if (result.ResultCode != GetRecipesResultCode.Success)
|
|
1520
|
+
return false;
|
|
1521
|
+
|
|
1522
|
+
// Check if recipe matches
|
|
1523
|
+
foreach (var recipe in result.Recipes)
|
|
1524
|
+
{
|
|
1525
|
+
if (recipe.FTOptix.RecipeX.RecipeId.Name == recipeName &&
|
|
1526
|
+
recipe.FTOptix.RecipeX.RecipeId.Version == version)
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
### Safe Recipe Application with Error Handling
|
|
1535
|
+
|
|
1536
|
+
Apply a recipe with comprehensive error handling and logging:
|
|
1537
|
+
|
|
1538
|
+
```csharp
|
|
1539
|
+
[ExportMethod]
|
|
1540
|
+
public void SafeApplyRecipe(string recipeName, string version)
|
|
1541
|
+
{
|
|
1542
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1543
|
+
if (schema == null)
|
|
1544
|
+
{
|
|
1545
|
+
Log.Error("Recipe schema not found");
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
var target = LogicObject.Owner.GetVariable("TargetObject").Value;
|
|
1550
|
+
if (target == null)
|
|
1551
|
+
{
|
|
1552
|
+
Log.Error("Target object not found");
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
var recipeId = new FTOptix.RecipeX.RecipeId
|
|
1557
|
+
{
|
|
1558
|
+
Name = recipeName,
|
|
1559
|
+
Version = version
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// Check if recipe exists first
|
|
1563
|
+
var recipesResult = schema.GetRecipes();
|
|
1564
|
+
bool recipeExists = false;
|
|
1565
|
+
|
|
1566
|
+
if (recipesResult.ResultCode == GetRecipesResultCode.Success)
|
|
1567
|
+
{
|
|
1568
|
+
recipeExists = recipesResult.Recipes.Any(r =>
|
|
1569
|
+
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
|
|
1570
|
+
r.FTOptix.RecipeX.RecipeId.Version == version);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (!recipeExists)
|
|
1574
|
+
{
|
|
1575
|
+
Log.Error($"Recipe '{recipeName}' v{version} not found");
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Apply the recipe
|
|
1580
|
+
var result = schema.TransferFromStoreToTarget(
|
|
1581
|
+
recipeId,
|
|
1582
|
+
target,
|
|
1583
|
+
FTOptix.RecipeX.ErrorPolicy.Strict);
|
|
1584
|
+
|
|
1585
|
+
if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
|
|
1586
|
+
{
|
|
1587
|
+
Log.Info($"Recipe '{recipeName}' applied successfully");
|
|
1588
|
+
}
|
|
1589
|
+
else
|
|
1590
|
+
{
|
|
1591
|
+
switch (result)
|
|
1592
|
+
{
|
|
1593
|
+
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.RecipeNotFound:
|
|
1594
|
+
Log.Error("Recipe not found in store");
|
|
1595
|
+
break;
|
|
1596
|
+
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.TargetNotFound:
|
|
1597
|
+
Log.Error("Target node not found");
|
|
1598
|
+
break;
|
|
1599
|
+
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.DataMismatch:
|
|
1600
|
+
Log.Error("Recipe structure does not match target");
|
|
1601
|
+
break;
|
|
1602
|
+
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.StoreError:
|
|
1603
|
+
Log.Error("Error accessing recipe store");
|
|
1604
|
+
break;
|
|
1605
|
+
default:
|
|
1606
|
+
Log.Error($"Failed to apply recipe: {result}");
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
### Create Recipe from Current Target State
|
|
1614
|
+
|
|
1615
|
+
Capture current configuration as a recipe:
|
|
1616
|
+
|
|
1617
|
+
```csharp
|
|
1618
|
+
[ExportMethod]
|
|
1619
|
+
public void CaptureAsRecipe(string recipeName)
|
|
1620
|
+
{
|
|
1621
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1622
|
+
var source = LogicObject.Owner.GetVariable("SourceObject").Value;
|
|
1623
|
+
|
|
1624
|
+
if (schema == null || source == null)
|
|
1625
|
+
{
|
|
1626
|
+
Log.Error("Recipe schema or source not found");
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
var recipeId = new FTOptix.RecipeX.RecipeId
|
|
1631
|
+
{
|
|
1632
|
+
Name = recipeName,
|
|
1633
|
+
Version = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss")
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
// Capture and save
|
|
1637
|
+
var result = schema.TransferFromTargetToStore(
|
|
1638
|
+
recipeId,
|
|
1639
|
+
source,
|
|
1640
|
+
overwrite: false, // Don't overwrite existing
|
|
1641
|
+
FTOptix.RecipeX.ErrorPolicy.Strict);
|
|
1642
|
+
|
|
1643
|
+
switch (result)
|
|
1644
|
+
{
|
|
1645
|
+
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeCreated:
|
|
1646
|
+
Log.Info($"Recipe '{recipeName}' created and saved");
|
|
1647
|
+
// Optionally set metadata
|
|
1648
|
+
schema.SetRecipeMetadataValue(recipeId, "CaptureTime", DateTime.UtcNow);
|
|
1649
|
+
schema.SetRecipeMetadataValue(recipeId, "CapturedBy", "AutoCapture");
|
|
1650
|
+
break;
|
|
1651
|
+
|
|
1652
|
+
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeUpdated:
|
|
1653
|
+
Log.Info($"Recipe '{recipeName}' updated");
|
|
1654
|
+
break;
|
|
1655
|
+
|
|
1656
|
+
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.RecipeAlreadyExists:
|
|
1657
|
+
Log.Warning($"Recipe '{recipeName}' already exists");
|
|
1658
|
+
break;
|
|
1659
|
+
|
|
1660
|
+
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.DataMismatch:
|
|
1661
|
+
Log.Error("Source structure does not match recipe schema");
|
|
1662
|
+
break;
|
|
1663
|
+
|
|
1664
|
+
default:
|
|
1665
|
+
Log.Error($"Failed to capture recipe: {result}");
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
### Batch Duplicate Recipe with Metadata
|
|
1672
|
+
|
|
1673
|
+
Duplicate a recipe while updating metadata:
|
|
1674
|
+
|
|
1675
|
+
```csharp
|
|
1676
|
+
[ExportMethod]
|
|
1677
|
+
public void DuplicateRecipeWithMetadata(string sourceRecipe, string targetRecipe)
|
|
1678
|
+
{
|
|
1679
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1680
|
+
if (schema == null)
|
|
1681
|
+
return;
|
|
1682
|
+
|
|
1683
|
+
// Get source metadata
|
|
1684
|
+
var sourceId = new FTOptix.RecipeX.RecipeId { Name = sourceRecipe, Version = "1.0" };
|
|
1685
|
+
var metadataResult = schema.GetRecipeMetadataValues(sourceId);
|
|
1686
|
+
|
|
1687
|
+
if (metadataResult.ResultCode != GetRecipeMetadataValuesResultCode.Success)
|
|
1688
|
+
{
|
|
1689
|
+
Log.Error("Failed to retrieve source metadata");
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Duplicate the recipe
|
|
1694
|
+
var targetId = new FTOptix.RecipeX.RecipeId { Name = targetRecipe, Version = "1.0" };
|
|
1695
|
+
var dupResult = schema.DuplicateRecipe(sourceId, targetId);
|
|
1696
|
+
|
|
1697
|
+
if (dupResult != DuplicateRecipeResultCode.Success)
|
|
1698
|
+
{
|
|
1699
|
+
Log.Error($"Failed to duplicate recipe: {dupResult}");
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Copy metadata from source
|
|
1704
|
+
foreach (var metadata in metadataResult.MetadataValues)
|
|
1705
|
+
{
|
|
1706
|
+
schema.SetRecipeMetadataValue(targetId, metadata.Name, metadata.Value);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Update some fields
|
|
1710
|
+
schema.SetRecipeMetadataValue(targetId, "DuplicatedFrom", sourceRecipe);
|
|
1711
|
+
schema.SetRecipeMetadataValue(targetId, "DuplicatedAt", DateTime.UtcNow);
|
|
1712
|
+
|
|
1713
|
+
Log.Info($"Recipe '{targetRecipe}' created as copy of '{sourceRecipe}'");
|
|
1714
|
+
}
|
|
1715
|
+
```
|
|
1716
|
+
|
|
1717
|
+
### Interactive Recipe Selection and Application
|
|
1718
|
+
|
|
1719
|
+
Create an interactive experience for selecting and applying recipes:
|
|
1720
|
+
|
|
1721
|
+
```csharp
|
|
1722
|
+
[ExportMethod]
|
|
1723
|
+
public string[] GetAvailableRecipes()
|
|
1724
|
+
{
|
|
1725
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1726
|
+
if (schema == null)
|
|
1727
|
+
return new string[] { };
|
|
1728
|
+
|
|
1729
|
+
var result = schema.GetRecipes();
|
|
1730
|
+
if (result.ResultCode != GetRecipesResultCode.Success)
|
|
1731
|
+
return new string[] { };
|
|
1732
|
+
|
|
1733
|
+
return result.Recipes
|
|
1734
|
+
.Select(r => $"{r.FTOptix.RecipeX.RecipeId.Name} (v{r.FTOptix.RecipeX.RecipeId.Version})")
|
|
1735
|
+
.ToArray();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
[ExportMethod]
|
|
1739
|
+
public void ApplySelectedRecipe(string recipeName, string version)
|
|
1740
|
+
{
|
|
1741
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1742
|
+
var target = LogicObject.Owner.GetVariable("TargetObject").Value;
|
|
1743
|
+
|
|
1744
|
+
if (schema == null || target == null)
|
|
1745
|
+
return;
|
|
1746
|
+
|
|
1747
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
1748
|
+
var result = schema.TransferFromStoreToTarget(recipeId, target, FTOptix.RecipeX.ErrorPolicy.Strict);
|
|
1749
|
+
|
|
1750
|
+
if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
|
|
1751
|
+
{
|
|
1752
|
+
Log.Info($"Applied recipe: {recipeName}");
|
|
1753
|
+
// Update UI display variable
|
|
1754
|
+
LogicObject.Owner.GetVariable("CurrentRecipe").Value = recipeName;
|
|
1755
|
+
}
|
|
1756
|
+
else
|
|
1757
|
+
{
|
|
1758
|
+
Log.Error($"Application failed: {result}");
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
[ExportMethod]
|
|
1763
|
+
public Dictionary<string, object> GetRecipeDetails(string recipeName, string version)
|
|
1764
|
+
{
|
|
1765
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1766
|
+
var details = new Dictionary<string, object>();
|
|
1767
|
+
|
|
1768
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
1769
|
+
|
|
1770
|
+
// Get general recipe info
|
|
1771
|
+
var recipesResult = schema.GetRecipes();
|
|
1772
|
+
if (recipesResult.ResultCode == GetRecipesResultCode.Success)
|
|
1773
|
+
{
|
|
1774
|
+
var recipe = recipesResult.Recipes.FirstOrDefault(r =>
|
|
1775
|
+
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
|
|
1776
|
+
r.FTOptix.RecipeX.RecipeId.Version == version);
|
|
1777
|
+
|
|
1778
|
+
if (recipe != null)
|
|
1779
|
+
{
|
|
1780
|
+
details["Name"] = recipe.FTOptix.RecipeX.RecipeId.Name;
|
|
1781
|
+
details["Version"] = recipe.FTOptix.RecipeX.RecipeId.Version;
|
|
1782
|
+
details["CreatedAt"] = recipe.CreatedAt;
|
|
1783
|
+
details["ModifiedAt"] = recipe.ModifiedAt;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Get metadata
|
|
1788
|
+
var metaResult = schema.GetRecipeMetadataValues(recipeId);
|
|
1789
|
+
if (metaResult.ResultCode == GetRecipeMetadataValuesResultCode.Success)
|
|
1790
|
+
{
|
|
1791
|
+
var metadata = new Dictionary<string, object>();
|
|
1792
|
+
foreach (var meta in metaResult.MetadataValues)
|
|
1793
|
+
{
|
|
1794
|
+
metadata[meta.Name] = meta.Value;
|
|
1795
|
+
}
|
|
1796
|
+
details["Metadata"] = metadata;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Get data items count
|
|
1800
|
+
var itemsResult = schema.GetDataItems(recipeId);
|
|
1801
|
+
if (itemsResult.ResultCode == GetDataItemsResultCode.Success)
|
|
1802
|
+
{
|
|
1803
|
+
details["DataItemCount"] = itemsResult.DataItems.Length;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
return details;
|
|
1807
|
+
}
|
|
1808
|
+
```
|
|
1809
|
+
|
|
1810
|
+
### Recipe Validation and Consistency Check
|
|
1811
|
+
|
|
1812
|
+
Verify recipe consistency and values:
|
|
1813
|
+
|
|
1814
|
+
```csharp
|
|
1815
|
+
[ExportMethod]
|
|
1816
|
+
public bool ValidateRecipe(string recipeName, string version)
|
|
1817
|
+
{
|
|
1818
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1819
|
+
if (schema == null)
|
|
1820
|
+
return false;
|
|
1821
|
+
|
|
1822
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
1823
|
+
|
|
1824
|
+
// Check recipe exists
|
|
1825
|
+
var recipesResult = schema.GetRecipes();
|
|
1826
|
+
if (recipesResult.ResultCode != GetRecipesResultCode.Success)
|
|
1827
|
+
{
|
|
1828
|
+
Log.Error("Failed to retrieve recipes");
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
var recipeExists = recipesResult.Recipes.Any(r =>
|
|
1833
|
+
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
|
|
1834
|
+
r.FTOptix.RecipeX.RecipeId.Version == version);
|
|
1835
|
+
|
|
1836
|
+
if (!recipeExists)
|
|
1837
|
+
{
|
|
1838
|
+
Log.Error($"Recipe '{recipeName}' not found");
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// Validate data items
|
|
1843
|
+
var itemsResult = schema.GetDataItems(recipeId);
|
|
1844
|
+
if (itemsResult.ResultCode != GetDataItemsResultCode.Success)
|
|
1845
|
+
{
|
|
1846
|
+
Log.Error("Failed to retrieve data items");
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
if (itemsResult.DataItems.Length == 0)
|
|
1851
|
+
{
|
|
1852
|
+
Log.Warning("Recipe contains no data items");
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Validate individual values
|
|
1856
|
+
bool allValid = true;
|
|
1857
|
+
foreach (var item in itemsResult.DataItems)
|
|
1858
|
+
{
|
|
1859
|
+
var valueResult = schema.GetRecipeDataItemValue(
|
|
1860
|
+
recipeId,
|
|
1861
|
+
item.ItemRelativeBrowsePath,
|
|
1862
|
+
item.DataItemRelativeBrowsePath,
|
|
1863
|
+
item.ElementAccess);
|
|
1864
|
+
|
|
1865
|
+
if (valueResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
|
|
1866
|
+
{
|
|
1867
|
+
Log.Warning($"Could not retrieve value for {string.Join("/", item.ItemRelativeBrowsePath)}");
|
|
1868
|
+
allValid = false;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Validate metadata
|
|
1873
|
+
var metaResult = schema.GetRecipeMetadataValues(recipeId);
|
|
1874
|
+
if (metaResult.ResultCode != GetRecipeMetadataValuesResultCode.Success)
|
|
1875
|
+
{
|
|
1876
|
+
Log.Warning("Could not retrieve metadata");
|
|
1877
|
+
allValid = false;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
return allValid;
|
|
1881
|
+
}
|
|
1882
|
+
```
|
|
1883
|
+
|
|
1884
|
+
### Delete Recipe with Confirmation
|
|
1885
|
+
|
|
1886
|
+
This method implements a **two-step confirmation pattern** to prevent accidental deletion. It is designed to be called from a UI button via `ExportMethod`:
|
|
1887
|
+
|
|
1888
|
+
1. **First call** (`confirmed = false`, the default): performs a dry run. It looks up the recipe and logs the details of what *would* be deleted, then returns without making any changes. This gives the operator a chance to review before committing.
|
|
1889
|
+
2. **Second call** (`confirmed = true`): performs the actual deletion.
|
|
1890
|
+
|
|
1891
|
+
This pattern is useful in HMI screens where a "Delete" button can first show a confirmation prompt (`confirmed` parameter set to `false`) and only proceed when the operator explicitly confirms the action (`confirmed` parameter set to `true`).
|
|
1892
|
+
|
|
1893
|
+
```csharp
|
|
1894
|
+
[ExportMethod]
|
|
1895
|
+
public void DeleteRecipeWithConfirmation(string recipeName, string version, bool confirmed)
|
|
1896
|
+
{
|
|
1897
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1898
|
+
if (schema == null)
|
|
1899
|
+
return;
|
|
1900
|
+
|
|
1901
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
1902
|
+
|
|
1903
|
+
if (!confirmed)
|
|
1904
|
+
{
|
|
1905
|
+
// First call: log what would be deleted
|
|
1906
|
+
var result = schema.GetRecipes();
|
|
1907
|
+
if (result.ResultCode == GetRecipesResultCode.Success)
|
|
1908
|
+
{
|
|
1909
|
+
var recipe = result.Recipes.FirstOrDefault(r =>
|
|
1910
|
+
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
|
|
1911
|
+
r.FTOptix.RecipeX.RecipeId.Version == version);
|
|
1912
|
+
|
|
1913
|
+
if (recipe != null)
|
|
1914
|
+
{
|
|
1915
|
+
Log.Info($"Would delete recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version} " +
|
|
1916
|
+
$"(modified: {recipe.ModifiedAt})");
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
Log.Warning("Recipe not found");
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// Second call with confirmed=true: actually delete
|
|
1926
|
+
var deleteResult = schema.DeleteRecipe(recipeId);
|
|
1927
|
+
|
|
1928
|
+
if (deleteResult == DeleteRecipeResultCode.Success)
|
|
1929
|
+
{
|
|
1930
|
+
Log.Info($"Recipe '{recipeName}' deleted successfully");
|
|
1931
|
+
}
|
|
1932
|
+
else
|
|
1933
|
+
{
|
|
1934
|
+
Log.Error($"Failed to delete recipe: {deleteResult}");
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
### Edit Recipe (Edit Model Workflow)
|
|
1940
|
+
|
|
1941
|
+
Complete workflow for editing a recipe using edit models:
|
|
1942
|
+
|
|
1943
|
+
> [!NOTE]
|
|
1944
|
+
> The Edit Model session is tied to the lifecycle of its **parent node** (`parentNodeId` passed to `CreateEditModel`). If the parent node is deleted, the Edit Model node created underneath it is automatically deleted as well. This means you can scope Edit Model cleanup to session or screen lifecycle by controlling the parent node's lifetime rather than managing the Edit Model node directly.
|
|
1945
|
+
|
|
1946
|
+
```csharp
|
|
1947
|
+
[ExportMethod]
|
|
1948
|
+
public NodeId StartEditingRecipe(string recipeName, string version)
|
|
1949
|
+
{
|
|
1950
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
1951
|
+
var parentNode = LogicObject.Owner.GetVariable("EditModelsParent").Value;
|
|
1952
|
+
var requester = LogicObject.GetNodeId();
|
|
1953
|
+
var target = LogicObject.Owner.GetVariable("TargetObject").Value;
|
|
1954
|
+
|
|
1955
|
+
if (schema == null)
|
|
1956
|
+
return NodeId.Empty;
|
|
1957
|
+
|
|
1958
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
1959
|
+
|
|
1960
|
+
// Create edit model
|
|
1961
|
+
var createResult = schema.CreateEditModel(parentNode, requester, target, recipeId);
|
|
1962
|
+
|
|
1963
|
+
if (createResult.ResultCode != CreateEditModelResultCode.Success)
|
|
1964
|
+
{
|
|
1965
|
+
Log.Error($"Failed to create edit model: {createResult.ResultCode}");
|
|
1966
|
+
return NodeId.Empty;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Load recipe into edit model
|
|
1970
|
+
var transferResult = schema.TransferFromStoreToEditModel(requester, recipeId);
|
|
1971
|
+
|
|
1972
|
+
if (transferResult != FTOptix.RecipeX.TransferFromStoreToEditModelResultCode.Success)
|
|
1973
|
+
{
|
|
1974
|
+
Log.Error($"Failed to load recipe into edit model: {transferResult}");
|
|
1975
|
+
return NodeId.Empty;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// Cast to FTOptix.RecipeX.EditModel to access additional members such as PendingChanges
|
|
1979
|
+
// and transfer methods that operate directly on this edit model instance.
|
|
1980
|
+
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
|
|
1981
|
+
Log.Info($"Recipe '{recipeName}' loaded for editing. Pending changes: {editModel.PendingChanges}");
|
|
1982
|
+
return createResult.EditModelNodeId;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
[ExportMethod]
|
|
1986
|
+
public void CancelEditingRecipe(NodeId editModelId)
|
|
1987
|
+
{
|
|
1988
|
+
// Simply delete the edit model
|
|
1989
|
+
var editModel = InformationModel.Get(editModelId);
|
|
1990
|
+
if (editModel != null)
|
|
1991
|
+
{
|
|
1992
|
+
editModel.Delete();
|
|
1993
|
+
Log.Info("Edit model discarded");
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
[ExportMethod]
|
|
1998
|
+
public void SaveEditedRecipe(NodeId editModelId, string recipeName, string version,
|
|
1999
|
+
NodeId requesterNodeId)
|
|
2000
|
+
{
|
|
2001
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
2002
|
+
if (schema == null)
|
|
2003
|
+
return;
|
|
2004
|
+
|
|
2005
|
+
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
|
|
2006
|
+
|
|
2007
|
+
// Save changes back to store
|
|
2008
|
+
var result = schema.TransferFromEditModelToStore(requesterNodeId, recipeId);
|
|
2009
|
+
|
|
2010
|
+
if (result == FTOptix.RecipeX.TransferFromEditModelToStoreResultCode.SuccessRecipeUpdated ||
|
|
2011
|
+
result == FTOptix.RecipeX.TransferFromEditModelToStoreResultCode.SuccessRecipeCreated)
|
|
2012
|
+
{
|
|
2013
|
+
Log.Info($"Recipe '{recipeName}' saved successfully");
|
|
2014
|
+
|
|
2015
|
+
// Clean up edit model
|
|
2016
|
+
var editModel = InformationModel.Get(editModelId);
|
|
2017
|
+
if (editModel != null)
|
|
2018
|
+
editModel.Delete();
|
|
2019
|
+
}
|
|
2020
|
+
else
|
|
2021
|
+
{
|
|
2022
|
+
Log.Error($"Failed to save recipe: {result}");
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
[ExportMethod]
|
|
2027
|
+
public void ApplyAndClose(NodeId editModelId, NodeId targetNodeId, string recipeName,
|
|
2028
|
+
NodeId requesterNodeId, FTOptix.RecipeX.ErrorPolicy errorPolicy = FTOptix.RecipeX.ErrorPolicy.Strict)
|
|
2029
|
+
{
|
|
2030
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
2031
|
+
if (schema == null)
|
|
2032
|
+
return;
|
|
2033
|
+
|
|
2034
|
+
// Apply edit model directly to target
|
|
2035
|
+
var applyResult = schema.TransferFromEditModelToTarget(requesterNodeId, targetNodeId, errorPolicy);
|
|
2036
|
+
|
|
2037
|
+
if (applyResult == FTOptix.RecipeX.TransferFromEditModelToTargetResultCode.Success)
|
|
2038
|
+
{
|
|
2039
|
+
Log.Info($"Recipe '{recipeName}' applied to target");
|
|
2040
|
+
|
|
2041
|
+
// Clean up
|
|
2042
|
+
var editModel = InformationModel.Get(editModelId);
|
|
2043
|
+
if (editModel != null)
|
|
2044
|
+
editModel.Delete();
|
|
2045
|
+
}
|
|
2046
|
+
else
|
|
2047
|
+
{
|
|
2048
|
+
Log.Error($"Failed to apply recipe to target: {applyResult}");
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
```
|
|
2052
|
+
|
|
2053
|
+
### Accessing an Edit Model Managed by a ListView
|
|
2054
|
+
|
|
2055
|
+
When the **Recipes Editor** widget from the TemplateLibrary is used, FactoryTalk Optix automatically creates an `FTOptix.RecipeX.EditModel` to back the `ListView`. This edit model is not exposed as a visible project node, it exists internally and is not directly accessible as children of the `ListView` instance. To programmatically obtain a reference to it, call `CreateEditModel` passing the `ListView.NodeId` as both `parentNodeId` and `requesterNodeId`. Because the edit model already exists for that parent/requester pair, the call returns the existing instance rather than creating a new one.
|
|
2056
|
+
|
|
2057
|
+
This pattern is useful when you want to run server-side logic against the values the user is currently editing, for example to validate ranges or cross-field constraints before the recipe is saved.
|
|
2058
|
+
|
|
2059
|
+
```csharp
|
|
2060
|
+
[ExportMethod]
|
|
2061
|
+
public void ValidateListViewRecipe(NodeId listViewNodeId, out bool isValidRecipe)
|
|
2062
|
+
{
|
|
2063
|
+
isValidRecipe = true;
|
|
2064
|
+
|
|
2065
|
+
var listView = InformationModel.Get<ListView>(listViewNodeId);
|
|
2066
|
+
if (listView == null)
|
|
2067
|
+
{
|
|
2068
|
+
Log.Error("ListView not found");
|
|
2069
|
+
isValidRecipe = false;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
var recipeSchema = Project.Current.Get<FTOptix.RecipeX.RecipeSchema>("Recipes/RecipeSchema1");
|
|
2074
|
+
if (recipeSchema == null)
|
|
2075
|
+
{
|
|
2076
|
+
Log.Error("FTOptix.RecipeX.RecipeSchema not found");
|
|
2077
|
+
isValidRecipe = false;
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Use get-or-create: if the ListView already owns an edit model this returns it,
|
|
2082
|
+
// otherwise a new one is created. Pass NodeId.Empty and an empty FTOptix.RecipeX.RecipeId so the
|
|
2083
|
+
// call does not load a different recipe on top of the one already active.
|
|
2084
|
+
var createResult = recipeSchema.CreateEditModel(
|
|
2085
|
+
listView.NodeId, // parentNodeId — same as the ListView used when it created the edit model
|
|
2086
|
+
listView.NodeId, // requesterNodeId — same as the ListView used when it created the edit model
|
|
2087
|
+
NodeId.Empty, // targetNodeId — not needed; we are accessing an existing model
|
|
2088
|
+
new FTOptix.RecipeX.RecipeId()); // recipeId — empty; the loaded recipe is already in the model
|
|
2089
|
+
|
|
2090
|
+
if (createResult.ResultCode != CreateEditModelResultCode.Success)
|
|
2091
|
+
{
|
|
2092
|
+
Log.Error($"Failed to access edit model: {createResult.ResultCode}");
|
|
2093
|
+
isValidRecipe = false;
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
|
|
2098
|
+
if (editModel == null)
|
|
2099
|
+
{
|
|
2100
|
+
Log.Error("FTOptix.RecipeX.EditModel not found");
|
|
2101
|
+
isValidRecipe = false;
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// Retrieve the data items defined in the recipe that is currently loaded
|
|
2106
|
+
var dataItemsResult = recipeSchema.GetDataItems(editModel.RecipeId);
|
|
2107
|
+
if (dataItemsResult.ResultCode != GetDataItemsResultCode.Success)
|
|
2108
|
+
{
|
|
2109
|
+
Log.Error($"Failed to retrieve data items: {dataItemsResult.ResultCode}");
|
|
2110
|
+
isValidRecipe = false;
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Iterate every field and apply validation rules
|
|
2115
|
+
foreach (var dataItem in dataItemsResult.DataItems)
|
|
2116
|
+
{
|
|
2117
|
+
var valueResult = recipeSchema.GetRecipeDataItemValue(
|
|
2118
|
+
editModel.RecipeId,
|
|
2119
|
+
dataItem.ItemRelativeBrowsePath,
|
|
2120
|
+
dataItem.DataItemRelativeBrowsePath,
|
|
2121
|
+
dataItem.ElementAccess);
|
|
2122
|
+
|
|
2123
|
+
if (valueResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
|
|
2124
|
+
{
|
|
2125
|
+
Log.Warning($"Could not read '{string.Join("/", dataItem.ItemRelativeBrowsePath)}': {valueResult.ResultCode}");
|
|
2126
|
+
isValidRecipe = false;
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
var value = valueResult.DataItemValue;
|
|
2131
|
+
var fieldName = string.Join("/", dataItem.ItemRelativeBrowsePath);
|
|
2132
|
+
|
|
2133
|
+
if (value == null)
|
|
2134
|
+
{
|
|
2135
|
+
Log.Warning($"Field '{fieldName}' has a null value");
|
|
2136
|
+
isValidRecipe = false;
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// Add your custom validation logic here.
|
|
2141
|
+
// Examples:
|
|
2142
|
+
// if (value is double d && (d < 0 || d > 100)) { ... isValidRecipe = false; }
|
|
2143
|
+
// if (fieldName == "Motor/MaxSpeed" && (int)value > 3000) { ... isValidRecipe = false; }
|
|
2144
|
+
Log.Info($"Field '{fieldName}' = {value} — OK");
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
Log.Info(isValidRecipe
|
|
2148
|
+
? $"Recipe '{editModel.RecipeId.Name}' passed validation"
|
|
2149
|
+
: $"Recipe '{editModel.RecipeId.Name}' failed validation");
|
|
2150
|
+
}
|
|
2151
|
+
```
|
|
2152
|
+
|
|
2153
|
+
> [!NOTE]
|
|
2154
|
+
> Do not delete or commit the edit model inside this method. Lifecycle management of the edit model (saving, discarding) belongs to the ListView. This method is read-only with respect to the edit model — it only inspects the current values.
|
|
2155
|
+
|
|
2156
|
+
### Validating Edit Model Values Before Save
|
|
2157
|
+
|
|
2158
|
+
The example above reads the recipe values from the **store** via `RecipeSchema.GetRecipeDataItemValue`, which does not reflect changes the user has typed into a `ListView` but not yet saved. To validate the **live, unsaved values** currently held by the Edit Model, use `EditModel.GetDataItemValue` instead. This is the recommended check to run just before calling `TransferFromEditModelToStore` or `TransferFromEditModelToTarget`, so invalid input can be rejected before it is persisted or applied.
|
|
2159
|
+
|
|
2160
|
+
Key differences from `RecipeSchema.GetRecipeDataItemValue`:
|
|
2161
|
+
- It is called on the resolved `FTOptix.RecipeX.EditModel` instance, not on the `RecipeSchema`.
|
|
2162
|
+
- It takes the resolved **item node** (`NodeId`) rather than `ItemRelativeBrowsePath` — resolve it once via `editModel.TargetNodeId` and `IUANode.Get(itemRelativePath)`.
|
|
2163
|
+
- It returns the value currently in the Edit Model, including any pending, unsaved edits.
|
|
2164
|
+
|
|
2165
|
+
```csharp
|
|
2166
|
+
[ExportMethod]
|
|
2167
|
+
public void ValidateEditedParameterRange(double minValue, double maxValue, out bool isValidRecipe, out string validationMessage)
|
|
2168
|
+
{
|
|
2169
|
+
isValidRecipe = false;
|
|
2170
|
+
validationMessage = string.Empty;
|
|
2171
|
+
|
|
2172
|
+
if (minValue > maxValue)
|
|
2173
|
+
{
|
|
2174
|
+
SetFailure("Minimum value cannot be greater than maximum value.", ref isValidRecipe, ref validationMessage);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
var parametersListView = Owner?.Owner?.Owner?.Get<ListView>("RecipeParameters/ParametersListView");
|
|
2179
|
+
if (parametersListView == null)
|
|
2180
|
+
{
|
|
2181
|
+
SetFailure("Recipe parameters ListView was not found.", ref isValidRecipe, ref validationMessage);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
var recipeSchema = Project.Current.Get<FTOptix.RecipeX.RecipeSchema>("Recipes/RecipeSchema1");
|
|
2186
|
+
if (recipeSchema == null)
|
|
2187
|
+
{
|
|
2188
|
+
SetFailure("Recipe schema not found at 'Recipes/RecipeSchema1'.", ref isValidRecipe, ref validationMessage);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// Get-or-create: accesses the edit model already owned by the ListView
|
|
2193
|
+
var createResult = recipeSchema.CreateEditModel(
|
|
2194
|
+
parametersListView.NodeId,
|
|
2195
|
+
parametersListView.NodeId,
|
|
2196
|
+
NodeId.Empty,
|
|
2197
|
+
new FTOptix.RecipeX.RecipeId { Name = string.Empty, Version = string.Empty });
|
|
2198
|
+
|
|
2199
|
+
if (createResult.ResultCode != FTOptix.RecipeX.CreateEditModelResultCode.Success)
|
|
2200
|
+
{
|
|
2201
|
+
SetFailure($"Failed to access ListView edit model: {createResult.ResultCode}.", ref isValidRecipe, ref validationMessage);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
var editModel = InformationModel.Get<FTOptix.RecipeX.EditModel>(createResult.EditModelNodeId);
|
|
2206
|
+
var targetNode = InformationModel.Get(editModel.TargetNodeId);
|
|
2207
|
+
if (editModel == null || targetNode == null)
|
|
2208
|
+
{
|
|
2209
|
+
SetFailure("Edit model or its target node could not be resolved.", ref isValidRecipe, ref validationMessage);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
var dataItemsResult = recipeSchema.GetDataItems(editModel.FTOptix.RecipeX.RecipeId);
|
|
2214
|
+
if (dataItemsResult.ResultCode != FTOptix.RecipeX.GetDataItemsResultCode.Success || dataItemsResult.DataItems.Length == 0)
|
|
2215
|
+
{
|
|
2216
|
+
SetFailure($"Unable to retrieve recipe data items: {dataItemsResult.ResultCode}.", ref isValidRecipe, ref validationMessage);
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
var invalidFields = 0;
|
|
2221
|
+
|
|
2222
|
+
foreach (var dataItem in dataItemsResult.DataItems)
|
|
2223
|
+
{
|
|
2224
|
+
var itemRelativePath = string.Join("/", dataItem.ItemRelativeBrowsePath ?? Array.Empty<string>());
|
|
2225
|
+
var fullPath = string.Join("/", (dataItem.ItemRelativeBrowsePath ?? Array.Empty<string>())
|
|
2226
|
+
.Concat(dataItem.DataItemRelativeBrowsePath ?? Array.Empty<string>()));
|
|
2227
|
+
|
|
2228
|
+
// Resolve the item node reached by ItemRelativeBrowsePath from the Edit Model's target
|
|
2229
|
+
var itemNodeId = targetNode.NodeId;
|
|
2230
|
+
if (!string.IsNullOrWhiteSpace(itemRelativePath))
|
|
2231
|
+
{
|
|
2232
|
+
var itemNode = targetNode.Get(itemRelativePath);
|
|
2233
|
+
if (itemNode == null)
|
|
2234
|
+
{
|
|
2235
|
+
invalidFields++;
|
|
2236
|
+
Log.Warning("RecipeValidation", $"Unable to resolve item node for '{itemRelativePath}'.");
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
itemNodeId = itemNode.NodeId;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// Read the current, unsaved value directly from the Edit Model
|
|
2243
|
+
var valueResult = editModel.GetDataItemValue(
|
|
2244
|
+
itemNodeId,
|
|
2245
|
+
dataItem.DataItemRelativeBrowsePath,
|
|
2246
|
+
dataItem.ElementAccess,
|
|
2247
|
+
dataItem.DataTypeId);
|
|
2248
|
+
|
|
2249
|
+
if (valueResult.ResultCode != FTOptix.RecipeX.GetDataItemValueResultCode.Success || valueResult.DataItemValue == null)
|
|
2250
|
+
{
|
|
2251
|
+
invalidFields++;
|
|
2252
|
+
Log.Warning("RecipeValidation", $"Unable to read edited value for '{fullPath}': {valueResult.ResultCode}");
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
if (!TryConvertToDouble(valueResult.DataItemValue, out var numericValue) || numericValue < minValue || numericValue > maxValue)
|
|
2257
|
+
{
|
|
2258
|
+
invalidFields++;
|
|
2259
|
+
Log.Warning("RecipeValidation", $"Edited value for '{fullPath}' is out of range [{minValue}, {maxValue}].");
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
isValidRecipe = invalidFields == 0;
|
|
2265
|
+
validationMessage = isValidRecipe
|
|
2266
|
+
? "Validation passed: all fields are within range."
|
|
2267
|
+
: $"Validation failed: {invalidFields} of {dataItemsResult.DataItems.Length} fields are invalid or out of range.";
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
private void SetFailure(string message, ref bool isValidRecipe, ref string validationMessage)
|
|
2271
|
+
{
|
|
2272
|
+
isValidRecipe = false;
|
|
2273
|
+
validationMessage = message;
|
|
2274
|
+
Log.Warning("RecipeValidation", message);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// Converts numeric-like UAValue/CLR types to double for range comparison; returns false for non-numeric values
|
|
2278
|
+
private static bool TryConvertToDouble(object value, out double numericValue)
|
|
2279
|
+
{
|
|
2280
|
+
if (value is UAValue uaValue)
|
|
2281
|
+
value = uaValue.Value;
|
|
2282
|
+
|
|
2283
|
+
switch (value)
|
|
2284
|
+
{
|
|
2285
|
+
case double d: numericValue = d; return true;
|
|
2286
|
+
case float f: numericValue = f; return true;
|
|
2287
|
+
case int i: numericValue = i; return true;
|
|
2288
|
+
case long l: numericValue = l; return true;
|
|
2289
|
+
case string str when double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed):
|
|
2290
|
+
numericValue = parsed;
|
|
2291
|
+
return true;
|
|
2292
|
+
default:
|
|
2293
|
+
numericValue = 0;
|
|
2294
|
+
return false;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
```
|
|
2298
|
+
|
|
2299
|
+
> [!TIP]
|
|
2300
|
+
> Because `GetDataItemValue` reads directly from the Edit Model, this pattern lets you reject invalid input (e.g. out-of-range values) before calling `TransferFromEditModelToStore`/`TransferFromEditModelToTarget`, preventing bad data from ever reaching the store or the running target.
|
|
2301
|
+
|
|
2302
|
+
### Bulk Recipe Operations
|
|
2303
|
+
|
|
2304
|
+
Perform operations on multiple recipes:
|
|
2305
|
+
|
|
2306
|
+
```csharp
|
|
2307
|
+
[ExportMethod]
|
|
2308
|
+
public void RenameAllRecipesWithPrefix(string oldPrefix, string newPrefix)
|
|
2309
|
+
{
|
|
2310
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
2311
|
+
if (schema == null)
|
|
2312
|
+
return;
|
|
2313
|
+
|
|
2314
|
+
var recipesResult = schema.GetRecipes();
|
|
2315
|
+
if (recipesResult.ResultCode != GetRecipesResultCode.Success)
|
|
2316
|
+
{
|
|
2317
|
+
Log.Error("Failed to retrieve recipes");
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
int renamed = 0;
|
|
2322
|
+
foreach (var recipe in recipesResult.Recipes)
|
|
2323
|
+
{
|
|
2324
|
+
if (recipe.RecipeId.Name.StartsWith(oldPrefix))
|
|
2325
|
+
{
|
|
2326
|
+
var newName = recipe.RecipeId.Name.Replace(oldPrefix, newPrefix);
|
|
2327
|
+
var result = schema.RenameRecipe(recipe.RecipeId, newName);
|
|
2328
|
+
|
|
2329
|
+
if (result.ResultCode == RenameRecipeResultCode.Success)
|
|
2330
|
+
{
|
|
2331
|
+
renamed++;
|
|
2332
|
+
Log.Info($"Renamed: {recipe.RecipeId.Name} -> {newName}");
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
Log.Info($"Renamed {renamed} recipes");
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
[ExportMethod]
|
|
2341
|
+
public void DeleteRecipesOlderThan(int daysOld)
|
|
2342
|
+
{
|
|
2343
|
+
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
|
|
2344
|
+
if (schema == null)
|
|
2345
|
+
return;
|
|
2346
|
+
|
|
2347
|
+
var recipesResult = schema.GetRecipes();
|
|
2348
|
+
if (recipesResult.ResultCode != GetRecipesResultCode.Success)
|
|
2349
|
+
return;
|
|
2350
|
+
|
|
2351
|
+
var cutoffDate = DateTime.UtcNow.AddDays(-daysOld);
|
|
2352
|
+
int deleted = 0;
|
|
2353
|
+
|
|
2354
|
+
foreach (var recipe in recipesResult.Recipes)
|
|
2355
|
+
{
|
|
2356
|
+
if (recipe.ModifiedAt < cutoffDate)
|
|
2357
|
+
{
|
|
2358
|
+
var result = schema.DeleteRecipe(recipe.FTOptix.RecipeX.RecipeId);
|
|
2359
|
+
if (result == DeleteRecipeResultCode.Success)
|
|
2360
|
+
{
|
|
2361
|
+
deleted++;
|
|
2362
|
+
Log.Info($"Deleted old recipe: {recipe.FTOptix.RecipeX.RecipeId.Name}");
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
Log.Info($"Deleted {deleted} old recipes");
|
|
2368
|
+
}
|
|
2369
|
+
```
|
|
2370
|
+
|
|
2371
|
+
## Important Considerations
|
|
2372
|
+
|
|
2373
|
+
### Schema Compatibility
|
|
2374
|
+
|
|
2375
|
+
- Legacy recipe schemas and new RecipeX schemas can coexist in the same project but they have different structures, so they cannot easily interoperate.
|
|
2376
|
+
- Recipe parameters represent Items or DataItems selected in the FTOptix.RecipeX.RecipeSchema editor.
|
|
2377
|
+
- Recipe parameters displayed in a ListView are always presented as read-only.
|
|
2378
|
+
|
|
2379
|
+
### Error Handling Best Practices
|
|
2380
|
+
|
|
2381
|
+
- Always check result codes after operations; they indicate specific error conditions.
|
|
2382
|
+
- Use `FTOptix.RecipeX.ErrorPolicy.Strict` for critical operations and `FTOptix.RecipeX.ErrorPolicy.BestEffort` for optional operations.
|
|
2383
|
+
- Log failures with the specific result code for easier diagnosis.
|