Merge R2-11 TagConfig consolidation (arch-review round 2) [PR #434]
Findings 01/C-1, 01/P-1 (single TagConfigIntent.Parse in Commons, 4 copies delegate, parse-once) + 05 CONV-2/UNDER-1/UNDER-6 (shared strict TagConfigJson readers, per-driver Inspect() + writable key, FOCAS capability pre-flight, Modbus probe timeoutMs, deploy-gate Warn|Error). Phase C runtime-strict flip deferred. T22/T24 deferred. Auto-merged AddressSpaceApplier.cs + DriverHostActor.cs with R2-04/R2-10; verified OpcUaServer.Tests 286/286 + Runtime.Tests 396/396. Build clean.
This commit is contained in:
+8
@@ -17,6 +17,14 @@
|
||||
<select class="form-select form-select-sm" value="@_m.DataType" @onchange="@(e => Update(() => _m.DataType = ParseEnum(e.Value, AbCipDataType.DInt)))">
|
||||
@foreach (var v in Enum.GetValues<AbCipDataType>()) { <option value="@v">@v</option> }
|
||||
</select></div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="abcip-writable"
|
||||
checked="@(_m.Writable ?? true)"
|
||||
@onchange="@(e => Update(() => _m.Writable = e.Value is bool b && b))" />
|
||||
<label class="form-check-label small" for="abcip-writable">Writable (uncheck for a read-only tag)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showPicker)
|
||||
|
||||
+8
@@ -17,6 +17,14 @@
|
||||
<select class="form-select form-select-sm" value="@_m.DataType" @onchange="@(e => Update(() => _m.DataType = ParseEnum(e.Value, AbLegacyDataType.Int)))">
|
||||
@foreach (var v in Enum.GetValues<AbLegacyDataType>()) { <option value="@v">@v</option> }
|
||||
</select></div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="ablegacy-writable"
|
||||
checked="@(_m.Writable ?? true)"
|
||||
@onchange="@(e => Update(() => _m.Writable = e.Value is bool b && b))" />
|
||||
<label class="form-check-label small" for="ablegacy-writable">Writable (uncheck for a read-only tag)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showPicker)
|
||||
|
||||
+6
@@ -17,6 +17,12 @@
|
||||
<select class="form-select form-select-sm" value="@_m.DataType" @onchange="@(e => Update(() => _m.DataType = ParseEnum(e.Value, FocasDataType.Int32)))">
|
||||
@foreach (var v in Enum.GetValues<FocasDataType>()) { <option value="@v">@v</option> }
|
||||
</select></div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="focas-writable" disabled checked="false" />
|
||||
<label class="form-check-label small text-muted" for="focas-writable">Writable — FOCAS writes are unsupported; tags are read-only.</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showPicker)
|
||||
|
||||
+8
@@ -25,6 +25,14 @@
|
||||
<div class="col-12 mt-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="@(() => _showPicker = true)">Build address</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="modbus-writable"
|
||||
checked="@(_m.Writable ?? true)"
|
||||
@onchange="@(e => Update(() => _m.Writable = e.Value is bool b && b))" />
|
||||
<label class="form-check-label small" for="modbus-writable">Writable (uncheck for a read-only tag)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showPicker)
|
||||
|
||||
+8
@@ -17,6 +17,14 @@
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label">String len</label>
|
||||
<input type="number" class="form-control form-control-sm" value="@_m.StringLength" @onchange="@(e => Update(() => _m.StringLength = ParseInt(e.Value)))" /></div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="s7-writable"
|
||||
checked="@(_m.Writable ?? true)"
|
||||
@onchange="@(e => Update(() => _m.Writable = e.Value is bool b && b))" />
|
||||
<label class="form-check-label small" for="s7-writable">Writable (uncheck for a read-only tag)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showPicker)
|
||||
|
||||
+8
@@ -17,6 +17,14 @@
|
||||
<select class="form-select form-select-sm" value="@_m.DataType" @onchange="@(e => Update(() => _m.DataType = ParseEnum(e.Value, TwinCATDataType.DInt)))">
|
||||
@foreach (var v in Enum.GetValues<TwinCATDataType>()) { <option value="@v">@v</option> }
|
||||
</select></div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="twincat-writable"
|
||||
checked="@(_m.Writable ?? true)"
|
||||
@onchange="@(e => Update(() => _m.Writable = e.Value is bool b && b))" />
|
||||
<label class="form-check-label small" for="twincat-writable">Writable (uncheck for a read-only tag)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showPicker)
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed record ScriptTagInfo(string Path, string Kind, string DataType, st
|
||||
/// <c>DriverInstanceActor.AttributeValuePublished.FullReference</c>
|
||||
/// (<c>src/Server/…Runtime/VirtualTags/DependencyMuxActor.cs:97</c>) — and that
|
||||
/// <c>FullReference</c> is the <c>FullName</c> field extracted from <c>Tag.TagConfig</c>
|
||||
/// (see <c>AddressSpaceComposer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
|
||||
/// (see <c>Commons.Types.TagConfigIntent.Parse</c>, the shared byte-parity FullName authority).
|
||||
/// The UNS-path engine (<c>Core.VirtualTags.VirtualTagEngine</c>, keyed by a slash-joined
|
||||
/// <c>Enterprise/Site/Area/Line/Equipment/TagName</c>) is dormant — it is NOT wired into the
|
||||
/// host — so UNS browse paths never resolve at runtime and are intentionally NOT suggested.
|
||||
@@ -179,8 +179,8 @@ public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> d
|
||||
/// <summary>
|
||||
/// Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the
|
||||
/// top-level <c>FullName</c> string every shipped driver stores. Mirrors
|
||||
/// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>AddressSpaceComposer.ExtractTagFullName</c>
|
||||
/// (AdminUI does not reference those assemblies). Falls back to the raw blob when it is not
|
||||
/// <c>Commons.Types.TagConfigIntent.Parse(...).FullName</c> (the shared byte-parity authority;
|
||||
/// AdminUI does not reference that assembly here). Falls back to the raw blob when it is not
|
||||
/// a JSON object carrying a string <c>FullName</c>.
|
||||
/// </summary>
|
||||
private static string ExtractFullNameFromTagConfig(string tagConfig)
|
||||
|
||||
@@ -16,6 +16,10 @@ public sealed class AbCipTagConfigModel
|
||||
/// <summary>Logix atomic type, or <c>AbCipDataType.Structure</c> for UDT-typed tags.</summary>
|
||||
public AbCipDataType DataType { get; set; } = AbCipDataType.DInt;
|
||||
|
||||
/// <summary>Optional writable flag (R2-11); <c>null</c> ⇒ omitted (driver default true), explicit
|
||||
/// <c>false</c> ⇒ read-only. Preserved across load→save.</summary>
|
||||
public bool? Writable { get; set; }
|
||||
|
||||
private JsonObject _bag = new();
|
||||
|
||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
||||
@@ -30,6 +34,7 @@ public sealed class AbCipTagConfigModel
|
||||
DeviceHostAddress = TagConfigJson.GetString(o, "deviceHostAddress") ?? "",
|
||||
TagPath = TagConfigJson.GetString(o, "tagPath") ?? "",
|
||||
DataType = TagConfigJson.GetEnum(o, "dataType", AbCipDataType.DInt),
|
||||
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||
_bag = o,
|
||||
};
|
||||
}
|
||||
@@ -42,6 +47,7 @@ public sealed class AbCipTagConfigModel
|
||||
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
||||
TagConfigJson.Set(_bag, "tagPath", TagPath.Trim());
|
||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||
TagConfigJson.Set(_bag, "writable", Writable);
|
||||
return TagConfigJson.Serialize(_bag);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ public sealed class AbLegacyTagConfigModel
|
||||
/// <summary>PCCC data type that maps onto SLC / MicroLogix / PLC-5 files.</summary>
|
||||
public AbLegacyDataType DataType { get; set; } = AbLegacyDataType.Int;
|
||||
|
||||
/// <summary>Optional writable flag (R2-11); <c>null</c> ⇒ omitted (driver default true), explicit
|
||||
/// <c>false</c> ⇒ read-only. Preserved across load→save.</summary>
|
||||
public bool? Writable { get; set; }
|
||||
|
||||
private JsonObject _bag = new();
|
||||
|
||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
||||
@@ -30,6 +34,7 @@ public sealed class AbLegacyTagConfigModel
|
||||
DeviceHostAddress = TagConfigJson.GetString(o, "deviceHostAddress") ?? "",
|
||||
Address = TagConfigJson.GetString(o, "address") ?? "",
|
||||
DataType = TagConfigJson.GetEnum(o, "dataType", AbLegacyDataType.Int),
|
||||
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||
_bag = o,
|
||||
};
|
||||
}
|
||||
@@ -42,6 +47,7 @@ public sealed class AbLegacyTagConfigModel
|
||||
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
||||
TagConfigJson.Set(_bag, "address", Address.Trim());
|
||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||
TagConfigJson.Set(_bag, "writable", Writable);
|
||||
return TagConfigJson.Serialize(_bag);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ public sealed class ModbusTagConfigModel
|
||||
/// <summary>String length in characters for String tags.</summary>
|
||||
public int StringLength { get; set; }
|
||||
|
||||
/// <summary>Optional writable flag (R2-11). <c>null</c> ⇒ the key is omitted (driver default true);
|
||||
/// an explicit <c>false</c> makes the equipment tag read-only. Preserved across load→save so an absent
|
||||
/// key stays absent.</summary>
|
||||
public bool? Writable { get; set; }
|
||||
|
||||
private JsonObject _bag = new();
|
||||
|
||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
||||
@@ -42,6 +47,7 @@ public sealed class ModbusTagConfigModel
|
||||
ByteOrder = TagConfigJson.GetEnum(o, "byteOrder", ModbusByteOrder.BigEndian),
|
||||
BitIndex = TagConfigJson.GetInt(o, "bitIndex"),
|
||||
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
||||
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||
_bag = o,
|
||||
};
|
||||
}
|
||||
@@ -57,6 +63,7 @@ public sealed class ModbusTagConfigModel
|
||||
TagConfigJson.Set(_bag, "byteOrder", ByteOrder);
|
||||
TagConfigJson.Set(_bag, "bitIndex", BitIndex);
|
||||
TagConfigJson.Set(_bag, "stringLength", StringLength);
|
||||
TagConfigJson.Set(_bag, "writable", Writable);
|
||||
return TagConfigJson.Serialize(_bag);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
/// stable <c>nsu=…;s=…</c> or plain <c>ns=2;s=…</c> NodeId the driver reads/writes/subscribes against).
|
||||
/// Preserves unrecognised JSON keys across a load→save.</summary>
|
||||
/// <remarks>
|
||||
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
|
||||
/// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
|
||||
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
|
||||
/// The <c>FullName</c> key is intentionally PascalCase: the shared deploy-time parser
|
||||
/// (<c>Commons.Types.TagConfigIntent.Parse</c>, consumed by the composer, artifact decoder, draft
|
||||
/// gate, and node walker) reads it via a case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
|
||||
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
|
||||
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
|
||||
/// (<see cref="TagHistorizeConfig"/>) and survive a load→save of this model as preserved unknown keys.
|
||||
|
||||
@@ -16,6 +16,10 @@ public sealed class S7TagConfigModel
|
||||
/// <summary>For <c>DataType = String</c>: S7-string max length (max 254).</summary>
|
||||
public int StringLength { get; set; }
|
||||
|
||||
/// <summary>Optional writable flag (R2-11); <c>null</c> ⇒ omitted (driver default true), explicit
|
||||
/// <c>false</c> ⇒ read-only. Preserved across load→save.</summary>
|
||||
public bool? Writable { get; set; }
|
||||
|
||||
private JsonObject _bag = new();
|
||||
|
||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
||||
@@ -30,6 +34,7 @@ public sealed class S7TagConfigModel
|
||||
Address = TagConfigJson.GetString(o, "address") ?? "",
|
||||
DataType = TagConfigJson.GetEnum(o, "dataType", S7DataType.Int16),
|
||||
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
||||
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||
_bag = o,
|
||||
};
|
||||
}
|
||||
@@ -42,6 +47,7 @@ public sealed class S7TagConfigModel
|
||||
TagConfigJson.Set(_bag, "address", Address.Trim());
|
||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||
TagConfigJson.Set(_bag, "stringLength", StringLength);
|
||||
TagConfigJson.Set(_bag, "writable", Writable);
|
||||
return TagConfigJson.Serialize(_bag);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,14 @@ public static class TagConfigJson
|
||||
public static bool GetBool(JsonObject o, string name, bool fallback = false)
|
||||
=> o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<bool>(out var b) ? b : fallback;
|
||||
|
||||
/// <summary>Reads a bool value, or <c>null</c> when the property is absent/null/non-boolean — so an
|
||||
/// absent key stays absent through a load→save (distinguishes "not set" from an explicit false).</summary>
|
||||
/// <param name="o">The JSON object to read from.</param>
|
||||
/// <param name="name">The property name to read.</param>
|
||||
/// <returns>The bool value, or <c>null</c> when absent/null/non-boolean.</returns>
|
||||
public static bool? GetBoolNullable(JsonObject o, string name)
|
||||
=> o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<bool>(out var b) ? b : null;
|
||||
|
||||
/// <summary>Reads an enum by its serialised name, or <paramref name="fallback"/> if absent/unparseable.</summary>
|
||||
/// <typeparam name="TEnum">The enum type to parse.</typeparam>
|
||||
/// <param name="o">The JSON object to read from.</param>
|
||||
|
||||
@@ -16,6 +16,10 @@ public sealed class TwinCATTagConfigModel
|
||||
/// <summary>TwinCAT / IEC 61131-3 atomic data type.</summary>
|
||||
public TwinCATDataType DataType { get; set; } = TwinCATDataType.DInt;
|
||||
|
||||
/// <summary>Optional writable flag (R2-11); <c>null</c> ⇒ omitted (driver default true), explicit
|
||||
/// <c>false</c> ⇒ read-only. Preserved across load→save.</summary>
|
||||
public bool? Writable { get; set; }
|
||||
|
||||
private JsonObject _bag = new();
|
||||
|
||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
||||
@@ -30,6 +34,7 @@ public sealed class TwinCATTagConfigModel
|
||||
DeviceHostAddress = TagConfigJson.GetString(o, "deviceHostAddress") ?? "",
|
||||
SymbolPath = TagConfigJson.GetString(o, "symbolPath") ?? "",
|
||||
DataType = TagConfigJson.GetEnum(o, "dataType", TwinCATDataType.DInt),
|
||||
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||
_bag = o,
|
||||
};
|
||||
}
|
||||
@@ -42,6 +47,7 @@ public sealed class TwinCATTagConfigModel
|
||||
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
||||
TagConfigJson.Set(_bag, "symbolPath", SymbolPath.Trim());
|
||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||
TagConfigJson.Set(_bag, "writable", Writable);
|
||||
return TagConfigJson.Serialize(_bag);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user