feat(server): equipment-tag node writability from Tag.AccessLevel (parity-safe, no migration)
This commit is contained in:
@@ -633,7 +633,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="parentFolderNodeId">The node identifier of the parent folder; null to use the namespace root.</param>
|
||||
/// <param name="displayName">The display name of the variable.</param>
|
||||
/// <param name="dataType">The OPC UA data type name (e.g., "Boolean", "Int32", "String").</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
|
||||
/// <param name="writable">When true the node is created <c>CurrentReadWrite</c> (an authored
|
||||
/// ReadWrite equipment tag); when false it stays <c>CurrentRead</c> (read-only). This task only sets
|
||||
/// the access level — no OnWriteValue handler is attached here (the inbound-write handler is owned
|
||||
/// by a later task).</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
@@ -655,8 +659,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
ReferenceTypeId = ReferenceTypeIds.Organizes,
|
||||
DataType = ResolveBuiltInDataType(dataType),
|
||||
ValueRank = ValueRanks.Scalar,
|
||||
AccessLevel = AccessLevels.CurrentRead,
|
||||
UserAccessLevel = AccessLevels.CurrentRead,
|
||||
// The SDK exposes the flags separately (no CurrentReadWrite composite): ReadWrite is
|
||||
// CurrentRead | CurrentWrite. OR-ing two byte constants promotes to int, so cast back.
|
||||
AccessLevel = writable ? (byte)(AccessLevels.CurrentRead | AccessLevels.CurrentWrite) : AccessLevels.CurrentRead,
|
||||
UserAccessLevel = writable ? (byte)(AccessLevels.CurrentRead | AccessLevels.CurrentWrite) : AccessLevels.CurrentRead,
|
||||
Historizing = false,
|
||||
Value = null,
|
||||
StatusCode = StatusCodes.BadWaitingForInitialData,
|
||||
|
||||
@@ -189,7 +189,7 @@ public sealed class Phase7Applier
|
||||
? tag.EquipmentId
|
||||
: EquipmentNodeIds.SubFolder(tag.EquipmentId, tag.FolderPath);
|
||||
var nodeId = EquipmentNodeIds.Variable(tag.EquipmentId, tag.FolderPath, tag.Name);
|
||||
SafeEnsureVariable(nodeId, parent, tag.Name, tag.DataType);
|
||||
SafeEnsureVariable(nodeId, parent, tag.Name, tag.DataType, tag.Writable);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
@@ -236,7 +236,8 @@ public sealed class Phase7Applier
|
||||
? v.EquipmentId
|
||||
: EquipmentNodeIds.SubFolder(v.EquipmentId, v.FolderPath);
|
||||
var nodeId = EquipmentNodeIds.Variable(v.EquipmentId, v.FolderPath, v.Name);
|
||||
SafeEnsureVariable(nodeId, parent, v.Name, v.DataType);
|
||||
// VirtualTags are computed outputs — read-only nodes (no inbound write).
|
||||
SafeEnsureVariable(nodeId, parent, v.Name, v.DataType, writable: false);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
@@ -281,9 +282,9 @@ public sealed class Phase7Applier
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureFolder threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType)
|
||||
private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable)
|
||||
{
|
||||
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType); }
|
||||
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureVariable threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,11 @@ public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentI
|
||||
/// <see cref="DataType"/>, and the driver-side <see cref="FullName"/> reference (extracted from
|
||||
/// <c>Tag.TagConfig</c>) the later values milestone routes reads/writes by. The variable's NodeId
|
||||
/// is folder-scoped (<c>parent/Name</c>), NOT <see cref="FullName"/>, because a raw driver ref
|
||||
/// (e.g. a Modbus register) is not unique across identical machines.
|
||||
/// (e.g. a Modbus register) is not unique across identical machines. <see cref="Writable"/>
|
||||
/// mirrors the authored <c>Tag.AccessLevel == ReadWrite</c> so the materialised node is created
|
||||
/// <c>CurrentReadWrite</c> (the prerequisite for the inbound-write pipeline); a <c>Read</c> tag
|
||||
/// stays read-only. This flag is derived identically on the artifact-decode side
|
||||
/// (<c>DeploymentArtifact.BuildEquipmentTagPlans</c>) for byte-parity.
|
||||
/// </summary>
|
||||
public sealed record EquipmentTagPlan(
|
||||
string TagId,
|
||||
@@ -80,7 +84,8 @@ public sealed record EquipmentTagPlan(
|
||||
string FolderPath,
|
||||
string Name,
|
||||
string DataType,
|
||||
string FullName);
|
||||
string FullName,
|
||||
bool Writable);
|
||||
|
||||
/// <summary>
|
||||
/// One Equipment-namespace VirtualTag from a <see cref="VirtualTag"/> row (joined to its
|
||||
@@ -322,7 +327,8 @@ public static class Phase7Composer
|
||||
FolderPath: t.FolderPath ?? string.Empty,
|
||||
Name: t.Name,
|
||||
DataType: t.DataType,
|
||||
FullName: ExtractTagFullName(t.TagConfig)))
|
||||
FullName: ExtractTagFullName(t.TagConfig),
|
||||
Writable: t.AccessLevel == TagAccessLevel.ReadWrite))
|
||||
.ToList();
|
||||
|
||||
// Per-equipment tag base = the shared substring-before-first-dot across each equipment's
|
||||
|
||||
@@ -56,8 +56,9 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
/// <param name="parentFolderNodeId">The parent folder node identifier.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="dataType">The OPC UA data type.</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
|
||||
=> _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType);
|
||||
/// <param name="writable">When true the node is created read/write; otherwise read-only.</param>
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable)
|
||||
=> _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable);
|
||||
|
||||
/// <summary>Rebuilds the entire OPC UA address space.</summary>
|
||||
public void RebuildAddressSpace() => _nodeManager.RebuildAddressSpace();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
@@ -417,6 +418,17 @@ public static class DeploymentArtifact
|
||||
var dataType = el.TryGetProperty("DataType", out var dtEl) ? dtEl.GetString() : null;
|
||||
var tagConfig = el.TryGetProperty("TagConfig", out var tcEl) && tcEl.ValueKind == JsonValueKind.String
|
||||
? tcEl.GetString() : null;
|
||||
// AccessLevel → Writable. ConfigComposer serialises the TagAccessLevel enum WITHOUT a
|
||||
// string converter, so it lands as a number (Read = 0, ReadWrite = 1); tolerate the string
|
||||
// form ("ReadWrite") too — same defensive both-forms parse as the Kind gate above. MUST match
|
||||
// Phase7Composer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing
|
||||
// field defaults to non-writable (read-only).
|
||||
var writable = el.TryGetProperty("AccessLevel", out var alEl) && alEl.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number => alEl.GetInt32() == (int)TagAccessLevel.ReadWrite,
|
||||
JsonValueKind.String => string.Equals(alEl.GetString(), nameof(TagAccessLevel.ReadWrite), StringComparison.Ordinal),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tagId) || string.IsNullOrWhiteSpace(di) || string.IsNullOrWhiteSpace(name)) continue;
|
||||
if (!driverToNamespace.TryGetValue(di!, out var nsId)) continue;
|
||||
@@ -432,7 +444,8 @@ public static class DeploymentArtifact
|
||||
FolderPath: folder ?? string.Empty,
|
||||
Name: name!,
|
||||
DataType: dataType ?? "BaseDataType",
|
||||
FullName: ExtractTagFullName(tagConfig)));
|
||||
FullName: ExtractTagFullName(tagConfig),
|
||||
Writable: writable));
|
||||
}
|
||||
|
||||
result.Sort((a, b) =>
|
||||
|
||||
Reference in New Issue
Block a user