feat(adminui): writable checkbox in driver-typed tag editors (R2-11, 05/UNDER-6)
Models gain an optional bool? Writable (absent stays omitted; explicit false round-trips, unknown keys preserved); the six editors gain a Writable checkbox (FOCAS disabled+read-only hint). 9 model round-trip tests green. docker-dev /run verify of one editor deferred-live.
This commit is contained in:
@@ -23,7 +23,7 @@
|
|||||||
{ "id": "T19", "subject": "Modbus probe parses the factory DTO shape (timeoutMs round-trip; OpcUaClient parity rule)", "status": "completed", "blockedBy": [] },
|
{ "id": "T19", "subject": "Modbus probe parses the factory DTO shape (timeoutMs round-trip; OpcUaClient parity rule)", "status": "completed", "blockedBy": [] },
|
||||||
{ "id": "T20", "subject": "EquipmentTagConfigInspector DriverType-dispatch map in ControlPlane (+6 Contracts references)", "status": "completed", "blockedBy": ["T12", "T13", "T14", "T15", "T16", "T17"] },
|
{ "id": "T20", "subject": "EquipmentTagConfigInspector DriverType-dispatch map in ControlPlane (+6 Contracts references)", "status": "completed", "blockedBy": ["T12", "T13", "T14", "T15", "T16", "T17"] },
|
||||||
{ "id": "T21", "subject": "AdminOperationsActor deploy-gate wiring + Deployment:TagConfigValidationMode (Warn default | Error opt-in); actor-level consuming test", "status": "completed", "blockedBy": ["T20"] },
|
{ "id": "T21", "subject": "AdminOperationsActor deploy-gate wiring + Deployment:TagConfigValidationMode (Warn default | Error opt-in); actor-level consuming test", "status": "completed", "blockedBy": ["T20"] },
|
||||||
{ "id": "T22", "subject": "AdminUI writable checkbox in the six driver-typed tag editors (models + razor; live /run verify at execution)", "status": "pending", "blockedBy": [] },
|
{ "id": "T22", "subject": "AdminUI writable checkbox in the six driver-typed tag editors (models + razor; live /run verify at execution)", "status": "deferred-live", "blockedBy": [] },
|
||||||
{ "id": "T23", "subject": "Docs + decision record: two-mode rollout, unknown-keys non-goal, FOCAS writable migration note, STATUS.md/00-INDEX.md entries + Phase-C follow-up", "status": "pending", "blockedBy": ["T17", "T21"] },
|
{ "id": "T23", "subject": "Docs + decision record: two-mode rollout, unknown-keys non-goal, FOCAS writable migration note, STATUS.md/00-INDEX.md entries + Phase-C follow-up", "status": "pending", "blockedBy": ["T17", "T21"] },
|
||||||
{ "id": "T24", "subject": "Final sweep: zero parity-comment/ReadEnum-copy grep hits; whole-solution build + test gate", "status": "pending", "blockedBy": ["T5", "T6", "T7", "T8", "T9", "T10", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T21", "T22", "T23"] }
|
{ "id": "T24", "subject": "Final sweep: zero parity-comment/ReadEnum-copy grep hits; whole-solution build + test gate", "status": "pending", "blockedBy": ["T5", "T6", "T7", "T8", "T9", "T10", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T21", "T22", "T23"] }
|
||||||
]
|
]
|
||||||
|
|||||||
+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)))">
|
<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> }
|
@foreach (var v in Enum.GetValues<AbCipDataType>()) { <option value="@v">@v</option> }
|
||||||
</select></div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@if (_showPicker)
|
@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)))">
|
<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> }
|
@foreach (var v in Enum.GetValues<AbLegacyDataType>()) { <option value="@v">@v</option> }
|
||||||
</select></div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@if (_showPicker)
|
@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)))">
|
<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> }
|
@foreach (var v in Enum.GetValues<FocasDataType>()) { <option value="@v">@v</option> }
|
||||||
</select></div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@if (_showPicker)
|
@if (_showPicker)
|
||||||
|
|||||||
+8
@@ -25,6 +25,14 @@
|
|||||||
<div class="col-12 mt-1">
|
<div class="col-12 mt-1">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="@(() => _showPicker = true)">Build address</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="@(() => _showPicker = true)">Build address</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@if (_showPicker)
|
@if (_showPicker)
|
||||||
|
|||||||
+8
@@ -17,6 +17,14 @@
|
|||||||
</select></div>
|
</select></div>
|
||||||
<div class="col-md-3"><label class="form-label">String len</label>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
@if (_showPicker)
|
@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)))">
|
<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> }
|
@foreach (var v in Enum.GetValues<TwinCATDataType>()) { <option value="@v">@v</option> }
|
||||||
</select></div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@if (_showPicker)
|
@if (_showPicker)
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ public sealed class AbCipTagConfigModel
|
|||||||
/// <summary>Logix atomic type, or <c>AbCipDataType.Structure</c> for UDT-typed tags.</summary>
|
/// <summary>Logix atomic type, or <c>AbCipDataType.Structure</c> for UDT-typed tags.</summary>
|
||||||
public AbCipDataType DataType { get; set; } = AbCipDataType.DInt;
|
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();
|
private JsonObject _bag = new();
|
||||||
|
|
||||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
/// <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") ?? "",
|
DeviceHostAddress = TagConfigJson.GetString(o, "deviceHostAddress") ?? "",
|
||||||
TagPath = TagConfigJson.GetString(o, "tagPath") ?? "",
|
TagPath = TagConfigJson.GetString(o, "tagPath") ?? "",
|
||||||
DataType = TagConfigJson.GetEnum(o, "dataType", AbCipDataType.DInt),
|
DataType = TagConfigJson.GetEnum(o, "dataType", AbCipDataType.DInt),
|
||||||
|
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||||
_bag = o,
|
_bag = o,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -42,6 +47,7 @@ public sealed class AbCipTagConfigModel
|
|||||||
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
||||||
TagConfigJson.Set(_bag, "tagPath", TagPath.Trim());
|
TagConfigJson.Set(_bag, "tagPath", TagPath.Trim());
|
||||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||||
|
TagConfigJson.Set(_bag, "writable", Writable);
|
||||||
return TagConfigJson.Serialize(_bag);
|
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>
|
/// <summary>PCCC data type that maps onto SLC / MicroLogix / PLC-5 files.</summary>
|
||||||
public AbLegacyDataType DataType { get; set; } = AbLegacyDataType.Int;
|
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();
|
private JsonObject _bag = new();
|
||||||
|
|
||||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
/// <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") ?? "",
|
DeviceHostAddress = TagConfigJson.GetString(o, "deviceHostAddress") ?? "",
|
||||||
Address = TagConfigJson.GetString(o, "address") ?? "",
|
Address = TagConfigJson.GetString(o, "address") ?? "",
|
||||||
DataType = TagConfigJson.GetEnum(o, "dataType", AbLegacyDataType.Int),
|
DataType = TagConfigJson.GetEnum(o, "dataType", AbLegacyDataType.Int),
|
||||||
|
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||||
_bag = o,
|
_bag = o,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -42,6 +47,7 @@ public sealed class AbLegacyTagConfigModel
|
|||||||
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
||||||
TagConfigJson.Set(_bag, "address", Address.Trim());
|
TagConfigJson.Set(_bag, "address", Address.Trim());
|
||||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||||
|
TagConfigJson.Set(_bag, "writable", Writable);
|
||||||
return TagConfigJson.Serialize(_bag);
|
return TagConfigJson.Serialize(_bag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public sealed class ModbusTagConfigModel
|
|||||||
/// <summary>String length in characters for String tags.</summary>
|
/// <summary>String length in characters for String tags.</summary>
|
||||||
public int StringLength { get; set; }
|
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();
|
private JsonObject _bag = new();
|
||||||
|
|
||||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
/// <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),
|
ByteOrder = TagConfigJson.GetEnum(o, "byteOrder", ModbusByteOrder.BigEndian),
|
||||||
BitIndex = TagConfigJson.GetInt(o, "bitIndex"),
|
BitIndex = TagConfigJson.GetInt(o, "bitIndex"),
|
||||||
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
||||||
|
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||||
_bag = o,
|
_bag = o,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -57,6 +63,7 @@ public sealed class ModbusTagConfigModel
|
|||||||
TagConfigJson.Set(_bag, "byteOrder", ByteOrder);
|
TagConfigJson.Set(_bag, "byteOrder", ByteOrder);
|
||||||
TagConfigJson.Set(_bag, "bitIndex", BitIndex);
|
TagConfigJson.Set(_bag, "bitIndex", BitIndex);
|
||||||
TagConfigJson.Set(_bag, "stringLength", StringLength);
|
TagConfigJson.Set(_bag, "stringLength", StringLength);
|
||||||
|
TagConfigJson.Set(_bag, "writable", Writable);
|
||||||
return TagConfigJson.Serialize(_bag);
|
return TagConfigJson.Serialize(_bag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ public sealed class S7TagConfigModel
|
|||||||
/// <summary>For <c>DataType = String</c>: S7-string max length (max 254).</summary>
|
/// <summary>For <c>DataType = String</c>: S7-string max length (max 254).</summary>
|
||||||
public int StringLength { get; set; }
|
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();
|
private JsonObject _bag = new();
|
||||||
|
|
||||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
/// <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") ?? "",
|
Address = TagConfigJson.GetString(o, "address") ?? "",
|
||||||
DataType = TagConfigJson.GetEnum(o, "dataType", S7DataType.Int16),
|
DataType = TagConfigJson.GetEnum(o, "dataType", S7DataType.Int16),
|
||||||
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
||||||
|
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||||
_bag = o,
|
_bag = o,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -42,6 +47,7 @@ public sealed class S7TagConfigModel
|
|||||||
TagConfigJson.Set(_bag, "address", Address.Trim());
|
TagConfigJson.Set(_bag, "address", Address.Trim());
|
||||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||||
TagConfigJson.Set(_bag, "stringLength", StringLength);
|
TagConfigJson.Set(_bag, "stringLength", StringLength);
|
||||||
|
TagConfigJson.Set(_bag, "writable", Writable);
|
||||||
return TagConfigJson.Serialize(_bag);
|
return TagConfigJson.Serialize(_bag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ public static class TagConfigJson
|
|||||||
public static bool GetBool(JsonObject o, string name, bool fallback = false)
|
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;
|
=> 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>
|
/// <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>
|
/// <typeparam name="TEnum">The enum type to parse.</typeparam>
|
||||||
/// <param name="o">The JSON object to read from.</param>
|
/// <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>
|
/// <summary>TwinCAT / IEC 61131-3 atomic data type.</summary>
|
||||||
public TwinCATDataType DataType { get; set; } = TwinCATDataType.DInt;
|
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();
|
private JsonObject _bag = new();
|
||||||
|
|
||||||
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
|
/// <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") ?? "",
|
DeviceHostAddress = TagConfigJson.GetString(o, "deviceHostAddress") ?? "",
|
||||||
SymbolPath = TagConfigJson.GetString(o, "symbolPath") ?? "",
|
SymbolPath = TagConfigJson.GetString(o, "symbolPath") ?? "",
|
||||||
DataType = TagConfigJson.GetEnum(o, "dataType", TwinCATDataType.DInt),
|
DataType = TagConfigJson.GetEnum(o, "dataType", TwinCATDataType.DInt),
|
||||||
|
Writable = TagConfigJson.GetBoolNullable(o, "writable"),
|
||||||
_bag = o,
|
_bag = o,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -42,6 +47,7 @@ public sealed class TwinCATTagConfigModel
|
|||||||
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim());
|
||||||
TagConfigJson.Set(_bag, "symbolPath", SymbolPath.Trim());
|
TagConfigJson.Set(_bag, "symbolPath", SymbolPath.Trim());
|
||||||
TagConfigJson.Set(_bag, "dataType", DataType);
|
TagConfigJson.Set(_bag, "dataType", DataType);
|
||||||
|
TagConfigJson.Set(_bag, "writable", Writable);
|
||||||
return TagConfigJson.Serialize(_bag);
|
return TagConfigJson.Serialize(_bag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R2-11 (05/UNDER-6): the new optional <c>writable</c> key round-trips through the driver-typed tag
|
||||||
|
/// config models — a value the editors' new checkbox authors. Absent stays absent (so existing configs
|
||||||
|
/// are untouched); an explicit <c>false</c> round-trips; unknown keys are preserved.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TagConfigModelWritableTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Modbus_absent_writable_stays_omitted()
|
||||||
|
{
|
||||||
|
var m = ModbusTagConfigModel.FromJson("{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Int16\"}");
|
||||||
|
m.Writable.ShouldBeNull();
|
||||||
|
m.ToJson().ShouldNotContain("writable");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Modbus_explicit_false_round_trips()
|
||||||
|
{
|
||||||
|
var m = ModbusTagConfigModel.FromJson("{\"address\":1,\"writable\":false}");
|
||||||
|
m.Writable.ShouldBe(false);
|
||||||
|
var json = ModbusTagConfigModel.FromJson(m.ToJson());
|
||||||
|
json.Writable.ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Modbus_setting_writable_true_writes_the_key()
|
||||||
|
{
|
||||||
|
var m = ModbusTagConfigModel.FromJson("{\"address\":1}");
|
||||||
|
m.Writable = true;
|
||||||
|
ModbusTagConfigModel.FromJson(m.ToJson()).Writable.ShouldBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Writable_preserves_unknown_keys()
|
||||||
|
{
|
||||||
|
var m = ModbusTagConfigModel.FromJson("{\"address\":1,\"writable\":false,\"foo\":\"bar\"}");
|
||||||
|
m.ToJson().ShouldContain("foo");
|
||||||
|
m.ToJson().ShouldContain("bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void S7_writable_round_trips()
|
||||||
|
{
|
||||||
|
var m = S7TagConfigModel.FromJson("{\"address\":\"DB1.DBW0\",\"writable\":false}");
|
||||||
|
m.Writable.ShouldBe(false);
|
||||||
|
S7TagConfigModel.FromJson(m.ToJson()).Writable.ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbCip_writable_round_trips()
|
||||||
|
{
|
||||||
|
var m = AbCipTagConfigModel.FromJson("{\"tagPath\":\"Motor.Speed\",\"writable\":false}");
|
||||||
|
m.Writable.ShouldBe(false);
|
||||||
|
AbCipTagConfigModel.FromJson(m.ToJson()).Writable.ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbLegacy_writable_round_trips()
|
||||||
|
{
|
||||||
|
var m = AbLegacyTagConfigModel.FromJson("{\"address\":\"N7:0\",\"writable\":false}");
|
||||||
|
m.Writable.ShouldBe(false);
|
||||||
|
AbLegacyTagConfigModel.FromJson(m.ToJson()).Writable.ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwinCAT_writable_round_trips()
|
||||||
|
{
|
||||||
|
var m = TwinCATTagConfigModel.FromJson("{\"symbolPath\":\"MAIN.x\",\"writable\":false}");
|
||||||
|
m.Writable.ShouldBe(false);
|
||||||
|
TwinCATTagConfigModel.FromJson(m.ToJson()).Writable.ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Absent_writable_omitted_across_models()
|
||||||
|
{
|
||||||
|
ModbusTagConfigModel.FromJson("{\"address\":1}").ToJson().ShouldNotContain("writable");
|
||||||
|
S7TagConfigModel.FromJson("{\"address\":\"DB1.DBW0\"}").ToJson().ShouldNotContain("writable");
|
||||||
|
AbCipTagConfigModel.FromJson("{\"tagPath\":\"M.S\"}").ToJson().ShouldNotContain("writable");
|
||||||
|
AbLegacyTagConfigModel.FromJson("{\"address\":\"N7:0\"}").ToJson().ShouldNotContain("writable");
|
||||||
|
TwinCATTagConfigModel.FromJson("{\"symbolPath\":\"MAIN.x\"}").ToJson().ShouldNotContain("writable");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user