Anleitung
Strukturierte Daten
Auf strukturierte Datentypen zugreifen, einfach, namenbasiert, dynamisch oder typisiert, und Datentypen generieren und definieren.
Anleitung
Strukturierte Daten
Die folgenden Abschnitte gehen davon aus, dass der Server einen Node mit der NodeId ns=2;s=Machine/Operator bereitstellt. Dieser Node verwendet den fiktiven Datentyp StaffType. Der Datentyp hat die folgende Struktur:
StaffType
.Name : string
.ID : int
.Shift : ShiftInfoType
.Name : string
.Elapsed : DateTime
.Remaining : byteEinfachster Zugriff mit dynamic
Beteiligte TypenOpcClientOpcNodeIdOpcValueOpcDataObject
Für den einfachen Zugriff auf Werte von Variablen-Nodes mit strukturierten Daten unterstützt das OPC UA .NET SDK das Schlüsselwort dynamic. Zugriffe auf Variablen, die mit dynamic deklariert sind, wertet .NET zur Laufzeit aus. Sie greifen damit auf die Daten eines strukturierten Datentyps zu, ohne vorher einen .NET-Typ zu implementieren. So sieht ein solcher Zugriff aus:
client.UseDynamic = true;
client.Connect();
dynamic staff = client.ReadNode("ns=2;s=Machine/Operator").Value;
// Access the 'Name' and 'ID' field of the data without to declare the data type itself.
// Just use the field names known as they would be defined in a .NET Type.
Console.WriteLine("Name: {0}", staff.Name);
Console.WriteLine("Staff ID: {0}", staff.ID);
// Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name);
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed);
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining);
// Change Shift
staff.Name = "John";
staff.ID = 4242;
staff.Shift.Name = "Swing Shift";
client.WriteNode("ns=2;s=Machine/Operator", staff);client.UseDynamic = True
client.Connect()
Dim staff = client.ReadNode("ns=2;s=Machine/Operator").Value
'Access the 'Name' and 'ID' field of the data without to declare the data type itself.
'Just use the field names known as they would be defined in a .NET Type.
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)
'Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name)
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed)
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining)
'Change Shift
staff.Name = "John"
staff.ID = 4242
staff.Shift.Name = "Swing Shift"
client.WriteNode("ns=2;s=Machine/Operator", staff)Namen-basierter Zugriff mit OpcDataObject
Beteiligte TypenOpcClientOpcNodeIdOpcValueOpcDataObjectOpcDataField
Für den Namen-basierten Zugriff auf Werte von Variablen-Nodes mit strukturierten Daten verwenden Sie direkt die Klasse OpcDataObject. Wenn Sie die Namen der Felder des Datentyps kennen, greifen Sie so auf die Felder zu:
client.UseDynamic = true;
client.Connect();
OpcDataObject staff = client.ReadNode("ns=2;s=Machine/Operator").As<OpcDataObject>();
// Access the 'Name' and 'ID' field of the data without to declare the data type itself.
// Just use the field names known as the 'key' to access the according field value.
Console.WriteLine("Name: {0}", staff["Name"].Value);
Console.WriteLine("Staff ID: {0}", staff["ID"].Value);
// Continue accessing subsequently used data types using the OpcDataObject as before.
OpcDataObject shift = (OpcDataObject)staff["Shift"].Value;
Console.WriteLine("Shift: {0}", shift["Name"].Value);
Console.WriteLine("- Time Elapsed: {0}", shift["Elapsed"].Value);
Console.WriteLine("- Jobs Remaining: {0}", shift["Remaining"].Value);
// Change Shift
staff["Name"].Value = "John";
staff["ID"].Value = 4242;
shift["Name"].Value = "Swing Shift";
client.WriteNode("ns=2;s=Machine/Operator", staff);client.UseDynamic = True
client.Connect()
Dim staff As OpcDataObject = client.ReadNode("ns=2;s=Machine/Operator").As(Of OpcDataObject)()
'Access the 'Name' and 'ID' field of the data without to declare the data type itself.
'Just use the field names known as the 'key' to access the according field value.
Console.WriteLine("Name: {0}", staff("Name").Value)
Console.WriteLine("Staff ID: {0}", staff("ID").Value)
'Continue accessing subsequently used data types using the OpcDataObject as before.
Dim shift As OpcDataObject = CType(staff("Shift").Value, OpcDataObject)
Console.WriteLine("Shift: {0}", shift("Name").Value)
Console.WriteLine("- Time Elapsed: {0}", shift("Elapsed").Value)
Console.WriteLine("- Jobs Remaining: {0}", shift("Remaining").Value)
'Change Shift
staff("Name").Value = "John"
staff("ID").Value = 4242
shift("Name").Value = "Swing Shift"
client.WriteNode("ns=2;s=Machine/Operator", staff)Typinformationen für den dynamischen Zugriff laden
Beteiligte TypenOpcClientOpcNodeSetOpcAutomatismOpcDataTypeSystemOpcRawObjectOpcData
Für den Zugriff mit dynamic (Abschnitt Einfachster Zugriff mit dynamic) und den Namen-basierten Zugriff (Abschnitt Namen-basierter Zugriff mit OpcDataObject) benötigt der Client Informationen über die Datentypen des Servers. In Visual Basic entspricht dynamic einer nicht typisierten Deklaration mit Dim. Die Klasse OpcClient ruft diese Informationen beim Aufruf von Connect() automatisch vom Server ab, wenn Sie zuvor die Eigenschaft UseDynamic auf true setzen. Diese Eigenschaft gibt es auf der Instanz von OpcClient und auf der statischen Klasse OpcAutomatism.
Mit dieser Einstellung lädt der Client die notwendigen Typinformationen vom Server. Nach dem Aufruf von Connect() greifen Sie dann ohne eigenen Code für die Typen auf die Daten zu, wie in den vorherigen Abschnitten gezeigt.
Wenn eine UANodeSet.xml oder eine andere XML-Datei mit der Beschreibung des Servers vorliegt, können Sie diese Datei auch in die Clientanwendung laden. Der Inhalt einer solchen Datei beginnt mit UANodeSet. Der Client bezieht die Informationen dann aus dem NodeSet und muss die Typinformationen nicht beim Verbindungsaufbau abrufen. So laden Sie das NodeSet:
using (var client = new OpcClient("opc.tcp://localhost:4840")) {
client.NodeSet = OpcNodeSet.Load(@"..\Resources\MyServersNodeSet.xml");
client.UseDynamic = true;
client.Connect();
dynamic staff = client.ReadNode("ns=2;s=Machine/Operator").Value;
Console.WriteLine("Name: {0}", staff.Name);
Console.WriteLine("Staff ID: {0}", staff.ID);
}Using client = New OpcClient("opc.tcp://localhost:4840")
client.NodeSet = OpcNodeSet.Load("..\Resources\MyServersNodeSet.xml")
client.UseDynamic = True
client.Connect()
Dim staff = client.ReadNode("ns=2;s=Machine/Operator").Value
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)
End UsingWenn ein NodeSet gesetzt ist, liefert die Methode GetDataTypeSystem() der Klasse OpcClient eine Instanz von OpcDataTypeSystem, die das Typsystem aus dem NodeSet beschreibt. Wenn Sie kein NodeSet über die Eigenschaft NodeSet festlegen, beschreibt die gelieferte Instanz das Typsystem des Servers. Die Informationen dazu fragt der Client bei Bedarf vom Server ab. In beiden Fällen setzt der Aufruf eine bestehende Verbindung zum Server voraus. Umfangreiche Typsysteme können die Grenzen der Datenübertragung überschreiten (Abschnitt Grenzen der Datenübertragung).
Unbekannte Strukturen: Wenn der Client den Datentyp einer Struktur nicht kennt oder ein Feld keinen festen ValueRank hat, kann er den Wert nicht dekodieren. Er liefert ihn dann als OpcRawObject. Dessen Eigenschaft Encoding nennt die Kodierung (binär oder XML), die Eigenschaft RawData enthält den kodierten Inhalt. Mit der Methode Decode<T> dekodieren Sie den Inhalt, sobald der Typ bekannt ist. Umgekehrt kodiert OpcData.Encode eine Struktur als OpcRawObject.
Typisierter Zugriff mit eigenen .NET-Typen
Beteiligte TypenOpcClientOpcNodeIdOpcDataTypeAttributeOpcDataTypeEncodingAttribute
Für den typisierten Zugriff definieren Sie .NET-Typen in der Clientanwendung so, wie sie im Server definiert sind. Alle nötigen Metadaten stellen Attribute bereit. Typen mit diesen Attributen registriert das SDK standardmäßig automatisch beim Verbindungsaufbau. Abschalten lässt sich das über die Eigenschaft OpcAutomatism.UseDynamicTypeRegistration. So definieren Sie .NET-Typen für strukturierte Datentypen:
[OpcDataType("ns=2;s=StaffType")]
[OpcDataTypeEncoding("ns=2;s=StaffType-Binary")]
public class Staff
{
public string Name { get; set; }
public int ID { get; set; }
public ShiftInfo Shift { get; set; }
}
[OpcDataType("ns=2;s=ShiftInfoType")]
[OpcDataTypeEncoding("ns=2;s=ShiftInfoType-Binary")]
public class ShiftInfo
{
public string Name { get; set; }
public DateTime Elapsed { get; set; }
public byte Remaining { get; set; }
}<OpcDataType("ns=2;s=StaffType")>
<OpcDataTypeEncoding("ns=2;s=StaffType-Binary")>
Public Class Staff
Public Property Name As String
Public Property ID As Integer
Public Property Shift As ShiftInfo
End Class
<OpcDataType("ns=2;s=ShiftInfoType")>
<OpcDataTypeEncoding("ns=2;s=ShiftInfoType-Binary")>
Public Class ShiftInfo
Public Property Name As String
Public Property Elapsed As DateTime
Public Property Remaining As Byte
End ClassWenn Sie clientseitig einen .NET-Typ für einen serverseitig definierten strukturierten Datentyp definiert haben, verwenden Sie ihn so:
client.Connect();
Staff staff = client.ReadNode("ns=2;s=Machine/Operator").As<Staff>();
// Access the 'Name' and 'ID' field of the data with the declared the data type.
Console.WriteLine("Name: {0}", staff.Name);
Console.WriteLine("Staff ID: {0}", staff.ID);
// Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name);
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed);
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining);
// Change Shift
staff.Name = "John";
staff.ID = 4242;
staff.Shift.Name = "Swing Shift";
client.WriteNode("ns=2;s=Machine/Operator", staff);client.Connect()
Dim staff As Staff = client.ReadNode("ns=2;s=Machine/Operator").As(Of Staff)()
'Access the 'Name' and 'ID' field of the data with the declared the data type.
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)
'Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name)
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed)
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining)
'Change Shift
staff.Name = "John"
staff.ID = 4242
staff.Shift.Name = "Swing Shift"
client.WriteNode("ns=2;s=Machine/Operator", staff)Datentypen generieren
Für den typisierten Zugriff (Abschnitt Typisierter Zugriff mit eigenen .NET-Typen) können Sie einzelne oder alle Datentypen eines OPC UA Servers mit OPC Watch generieren (Seite Downloads).
Über den Server generieren
So generieren Sie einen einzelnen Datentyp als .NET-Implementierung:
- OPC Watch öffnen
- Neue Verbindung anlegen (+) und bei Bedarf konfigurieren
- Verbindung zum Server herstellen (Stecker-Symbol)
- Einen Node auswählen:
- entweder einen Variablen-Node mit einem strukturierten Datentyp als Wert
- oder einen Datentyp-Node unter
/Types/DataTypes/BaseDataType/Structureoder/Enumeration
- Rechtsklick auf diesen Node
- „Generate DataType“ klicken
- Den Code in die Anwendung einfügen
Alle Datentypen generieren Sie wahlweise in eine Code-Datei (*.cs) oder eine Assembly-Datei (*.dll). So gehen Sie vor:
- OPC Watch öffnen
- Neue Verbindung anlegen (+) und bei Bedarf konfigurieren
- Verbindung zum Server herstellen (Stecker-Symbol)
- Den Server-Node auswählen, dessen Name mit
opc.tcp://beginnt - Rechtsklick auf diesen Node
- „Generate Models“ klicken
- Im Dialog den gewünschten Dateityp auswählen
- „Speichern“ klicken
- Die Datei dem Projekt hinzufügen
Über ein NodeSet generieren
- OPC Watch öffnen
- In der oberen rechten Ecke der Anwendung das erste Symbol anklicken
- NodeSet-Datei öffnen, meist eine XML-Datei einer Companion-Spezifikation
- OPC Watch lädt das NodeSet und zeigt anschließend den Dialog „Speichern unter“ an
- Im Dialog den gewünschten Dateityp auswählen
- „Speichern“ klicken
- Die Datei dem Projekt hinzufügen
Datentypen definieren
Beteiligte TypenOpcClientOpcNodeIdOpcDataOpcDataTypeAttributeOpcDataTypeEncodingAttributeOpcDataTypeSystemOpcDataTypeInfo
Statt den Datentyp als .NET-Code oder .NET-Assembly zu generieren (Abschnitt Datentypen generieren), können Sie ihn auch manuell definieren. Implementieren Sie dazu den Typ so, wie der Server ihn definiert. Der .NET-Typ muss also die Felder des strukturierten Datentyps in genau derselben Reihenfolge und mit denselben Typen bereitstellen, egal ob er eine Struktur oder eine Klasse ist. Alle weiteren Metadaten stellen Attribute bereit:
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
"<NodeId of Binary Encoding Node>",
NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
internal struct MyDataType
{
public short FieldA;
public int FieldB;
public string FieldC;
...
}<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
"<NodeId of Binary Encoding Node>", _
NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Structure MyDataType
Public FieldA As Short
Public FieldB As Integer
Public FieldC As String
...
End StructureDie nötigen Informationen erhalten Sie aus dem Handbuch des OPC UA Servers, vom Team, das die SPS programmiert, oder über die Klasse OpcClient. Mit OpcClient untersuchen Sie dazu den Variablen-Node, der den strukturierten Datentyp verwendet:
OpcNodeInfo node = client.BrowseNode("ns=2;s=Machine/Operator");
if (node is OpcVariableNodeInfo variableNode) {
OpcNodeId dataTypeId = variableNode.DataTypeId;
OpcDataTypeInfo dataType = client.GetDataTypeSystem().GetType(dataTypeId);
Console.WriteLine(dataType.TypeId);
Console.WriteLine(dataType.Encoding);
Console.WriteLine(dataType.Name);
foreach (OpcDataFieldInfo field in dataType.GetFields())
Console.WriteLine(".{0} : {1}", field.Name, field.FieldType);
Console.WriteLine();
Console.WriteLine("Data Type Attributes:");
Console.WriteLine(
"\t[OpcDataType(\"{0}\")]",
dataType.TypeId.ToString(OpcNodeIdFormat.Foundation));
Console.WriteLine(
"\t[OpcDataTypeEncoding(\"{0}\", NamespaceUri = \"{1}\")]",
dataType.Encoding.Id.ToString(OpcNodeIdFormat.Foundation),
dataType.Encoding.Namespace.Value);
}Dim node As OpcNodeInfo = client.BrowseNode("ns=2;s=Machine/Operator")
Dim variableNode As OpcVariableNodeInfo = TryCast(node, OpcVariableNodeInfo)
If variableNode IsNot Nothing Then
Dim dataTypeId As OpcNodeId = variableNode.DataTypeId
Dim dataType As OpcDataTypeInfo = client.GetDataTypeSystem().GetType(dataTypeId)
Console.WriteLine(dataType.TypeId)
Console.WriteLine(dataType.Encoding)
Console.WriteLine(dataType.Name)
For Each field As OpcDataFieldInfo In dataType.GetFields()
Console.WriteLine(".{0} : {1}", field.Name, field.FieldType)
Next
Console.WriteLine()
Console.WriteLine("Data Type Attributes:")
Console.WriteLine( _
vbTab & "[OpcDataType(""{0}"")]", _
dataType.TypeId.ToString(OpcNodeIdFormat.Foundation))
Console.WriteLine( _
vbTab & "[OpcDataTypeEncoding(""{0}"", NamespaceUri = ""{1}"")]", _
dataType.Encoding.Id.ToString(OpcNodeIdFormat.Foundation), _
dataType.Encoding.Namespace.Value)
End IfUnion-Datentypen
Beteiligte TypenOpcClientOpcDataTypeAttributeOpcDataTypeEncodingAttributeIOpcUnion
Ein Union-Datentyp überträgt von mehreren Feldern immer nur eines. Welches Feld übertragen wird, legt ein vorangestellter Auswahlwert fest. Um einen solchen Datentyp zu definieren, implementiert der .NET-Typ die Schnittstelle IOpcUnion. Ihre Eigenschaft SwitchValue enthält den Auswahlwert. Der Wert 1 wählt das erste Feld, der Wert 2 das zweite und so weiter. Beim Wert 0 wird kein Feld übertragen:
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
"<NodeId of Binary Encoding Node>",
NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
internal class MyUnionDataType : IOpcUnion
{
// 0 = no field, 1 = 'Number', 2 = 'Text'.
public uint SwitchValue { get; set; }
public int Number { get; set; }
public string Text { get; set; }
}<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
"<NodeId of Binary Encoding Node>", _
NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Class MyUnionDataType
Implements IOpcUnion
'0 = no field, 1 = 'Number', 2 = 'Text'.
Public Property SwitchValue As UInteger Implements IOpcUnion.SwitchValue
Public Property Number As Integer
Public Property Text As String
End ClassDatentypen mit optionalen Feldern
Beteiligte TypenOpcClientOpcNodeIdOpcDataOpcDataTypeAttributeOpcDataTypeEncodingAttributeOpcDataTypeEncodingMaskAttributeOpcEncodingMaskKindOpcDataTypeMemberSwitchAttributeOpcMemberSwitchOperator
Um die übertragene Datenmenge zu reduzieren, können Sie bestimmte Felder eines strukturierten Datentyps im Datenstrom als vorhanden oder fehlend kennzeichnen. Das hängt von bestimmten Bedingungen oder vom Wert eines anderen Felds in der Datenstruktur ab. Ob ein Feld vorhanden ist, zeigt also entweder der Wert eines anderen Felds oder ein einzelnes Bit in der EncodingMask. Die EncodingMask ist ein Feld, das als Präambel vor den Daten im Datenstrom kodiert ist. Ihre Größe in Bytes geben Sie im OpcDataTypeEncodingMaskAttribute an. Wenn Sie die Eigenschaft Size nicht explizit festlegen, erhält sie bei OpcEncodingMaskKind.Auto die kleinste nötige Anzahl Bytes, berechnet aus der Anzahl der optionalen Felder.
Optionale Felder, deren Vorhandensein ein Bit der EncodingMask anzeigt, definieren Sie bei OpcEncodingMaskKind.Auto auf drei Arten. Sie verwenden Nullable-Typen (Nullable), das OpcDataTypeMemberSwitchAttribute ohne Parameter (das nächste freie Bit) oder das OpcDataTypeMemberSwitchAttribute mit der Nummer eines bestimmten Bits:
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
"<NodeId of Binary Encoding Node>",
NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
[OpcDataTypeEncodingMask(OpcEncodingMaskKind.Auto)]
internal struct MyDataTypeWithOptionalFields
{
public short FieldA;
public int FieldB;
public string FieldC;
// Nullables are treat as optional fields by default.
// Existence-Indicator-Bit is Bit0 in the encoding mask.
public uint? OptionalField1;
// Existence-Indicator-Bit is Bit1 (the next unused bit) in the encoding mask.
[OpcDataTypeMemberSwitch]
public int OptionalField2;
// Existence-Indicator-Bit is Bit3 (bit 2 is unused) in the encoding mask.
[OpcDataTypeMemberSwitch(bit: 3)]
public byte OptionalField3;
}<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
"<NodeId of Binary Encoding Node>", _
NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
<OpcDataTypeEncodingMask(OpcEncodingMaskKind.Auto)>
Friend Structure MyDataTypeWithOptionalFields
Public FieldA As Short
Public FieldB As Integer
Public FieldC As String
'Nullables are treat as optional fields by default.
'Existence-Indicator-Bit is Bit0 in the encoding mask.
Public OptionalField1 As UInteger?
'Existence-Indicator-Bit is Bit1 (the next unused bit) in the encoding mask.
<OpcDataTypeMemberSwitch>
Public OptionalField2 As Integer
'Existence-Indicator-Bit is Bit3 (bit 2 is unused) in the encoding mask.
<OpcDataTypeMemberSwitch(3)>
Public OptionalField3 As Byte
End StructureWenn das Vorhandensein eines Felds stattdessen vom Wert eines vorangehenden Felds abhängen soll, geben Sie beim OpcDataTypeMemberSwitchAttribute den Namen dieses Felds an, optional zusammen mit einem Vergleichswert und einem OpcMemberSwitchOperator. Ein solches Feld belegt kein Bit der EncodingMask:
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
"<NodeId of Binary Encoding Node>",
NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
internal struct MyDataTypeWithConditionalFields
{
public bool FieldD;
// 'OptionalField4' exists only if the value of 'FieldD' is equals 'true'.
[OpcDataTypeMemberSwitch("FieldD")]
public string OptionalField4;
public int FieldE;
// 'OptionalField5' exists only if the value of 'FieldE' is greater than '42'.
[OpcDataTypeMemberSwitch("FieldE", value: 42, @operator: OpcMemberSwitchOperator.GreaterThan)]
public string OptionalField5;
}<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
"<NodeId of Binary Encoding Node>", _
NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Structure MyDataTypeWithConditionalFields
Public FieldD As Boolean
''OptionalField4' exists only if the value of 'FieldD' is equals 'true'.
<OpcDataTypeMemberSwitch("FieldD")>
Public OptionalField4 As String
Public FieldE As Integer
''OptionalField5' exists only if the value of 'FieldE' is greater than '42'.
<OpcDataTypeMemberSwitch("FieldE", 42, OpcMemberSwitchOperator.GreaterThan)>
Public OptionalField5 As String
End StructureIn OPC UA beschreibt eine DataTypeDefinition optionale Felder nur über Bits der EncodingMask. Andere Clients, die einen solchen Datentyp nur über seine DataTypeDefinition kennen, können ihn deshalb unter Umständen nicht dekodieren.

