feat(adminui): author MTConnect write-back on the driver form (Driver.MTConnect-004)

IWritable shipped behind write.enabled and docs/drivers/MTConnect.md told the operator to
set it, but MTConnectDriverForm — the only editor DriverFormMaps maps for this type, with
no raw-JSON escape hatch and no driver-instance API — exposed no write section, so the
documented enablement path was a hand edit of DriverInstance.DriverConfig in the config DB.

- Write-back section: switch + form text stating it is observation injection, that the
  Agent's AllowPut is an independent second gate, and that it does not by itself make a
  node writable over OPC UA (Tag.AccessLevel still composes CurrentWrite).
- Write added to the mirrored MTConnectConfigDto and to the form model, written in BOTH
  states — an omitted block would leave a previously-authored true standing, so the switch
  could never turn writing off again.
- The false 'MTConnect is read-only' header comment is gone; the doc names the new section.

No bUnit here, so the model/serialization seam is unit-tested both sides: the AdminUI test
pins the emitted bytes, and MTConnectFactoryTests parses that same document and asserts
MTConnectDriverOptions.Write.Enabled. Live page verification remains a rig step.
This commit is contained in:
Joseph Doherty
2026-08-10 08:07:36 -04:00
parent 2d7dffcc42
commit 298ad04fba
4 changed files with 182 additions and 4 deletions
@@ -3,7 +3,9 @@
paths (/probe, /current, /sample) to it and optionally narrows to one DeviceName, so unlike Modbus/S7
there is no host/port to split onto the device.
MTConnect is read-only — there are no write knobs to author. *@
Write-back is authored here too (Driver.MTConnect-004): the driver gained IWritable behind
write.enabled, and until this section existed the documented enablement path was a hand edit of
DriverInstance.DriverConfig in the config DB. *@
@using System.Text.Json
@using System.Text.Json.Nodes
@using System.Text.Json.Serialization
@@ -109,6 +111,34 @@
</div>
</section>
@* Write-back *@
<section class="panel rise mt-3" style="animation-delay:.14s">
<div class="panel-head">Write-back</div>
<div style="padding:1rem">
<div class="row g-3">
<div class="col-md-4">
<div class="form-check form-switch">
<InputCheckbox @bind-Value="_form.WriteEnabled" @bind-Value:after="EmitAsync" class="form-check-input" id="mtcWriteEnabled" />
<label class="form-check-label" for="mtcWriteEnabled">Allow write-back</label>
</div>
<div class="form-text mt-0">Default off.</div>
</div>
<div class="col-md-8">
<div class="form-text">
An MTConnect write is <strong>observation injection</strong>: it sets what the Agent
<em>reports</em> for a DataItem, not the machine itself, and the next adapter update overwrites it.
<br />
This is only one of two gates — the Agent's own <span class="mono">AllowPut</span> (its
<span class="mono">agent.cfg</span>) must be on as well, and neither gate can arm the other.
Turning this on does not by itself make any node writable over OPC UA: the tag's
<span class="mono">AccessLevel</span> still composes the <span class="mono">CurrentWrite</span> bit.
CONDITION items are never writable.
</div>
</div>
</div>
</div>
</section>
@if (_form.Validate() is { } error)
{
<div class="panel notice mt-3" style="border-color:var(--alert)">@error</div>
@@ -165,8 +195,8 @@
/// <para>
/// Unknown TOP-LEVEL keys in <paramref name="existingJson"/> are preserved (notably the
/// driver's hand-authored <c>tags[]</c> surface, which this form does not expose and must not
/// silently delete). The <c>probe</c> and <c>reconnect</c> sub-objects are replaced wholesale
/// — the form owns every field in both.
/// silently delete). The <c>probe</c>, <c>reconnect</c> and <c>write</c> sub-objects are
/// replaced wholesale — the form owns every field in all three.
/// </para>
/// <para>
/// A key whose form value is unusable (blank URI, non-positive timing knob) is REMOVED rather
@@ -268,6 +298,12 @@
public int ReconnectMaxBackoffMs { get; set; } = 30_000;
/// <summary>Multiplier applied to the reconnect backoff each retry.</summary>
public double ReconnectBackoffMultiplier { get; set; } = 2.0;
/// <summary>
/// Whether the driver's half of the write-back gate is armed. Default <c>false</c> — the
/// driver shipped read-only, and the Agent's own <c>AllowPut</c> is an independent second
/// gate that this cannot arm.
/// </summary>
public bool WriteEnabled { get; set; }
/// <summary>Loads a form model from a DriverConfig JSON document, falling back to the driver's
/// own defaults for anything absent or unusable.</summary>
@@ -293,6 +329,7 @@
ReconnectMinBackoffMs = dto.Reconnect?.MinBackoffMs ?? defaults.ReconnectMinBackoffMs,
ReconnectMaxBackoffMs = dto.Reconnect?.MaxBackoffMs ?? defaults.ReconnectMaxBackoffMs,
ReconnectBackoffMultiplier = dto.Reconnect?.BackoffMultiplier ?? defaults.ReconnectBackoffMultiplier,
WriteEnabled = dto.Write?.Enabled ?? defaults.WriteEnabled,
};
}
@@ -319,6 +356,11 @@
MaxBackoffMs = Positive(ReconnectMaxBackoffMs),
BackoffMultiplier = ReconnectBackoffMultiplier > 0 ? ReconnectBackoffMultiplier : null,
},
// Written even when false, unlike the timing knobs: those are omitted so the driver
// substitutes its own positive default, whereas an omitted write block would leave a
// previously-authored "enabled": true standing and the switch could never turn it off.
Write = new MTConnectWriteConfigDto { Enabled = WriteEnabled },
};
/// <summary>Returns an operator-facing message describing why this config would not start, or
@@ -380,6 +422,8 @@
public MTConnectProbeConfigDto? Probe { get; init; }
/// <summary>Reconnect backoff knobs.</summary>
public MTConnectReconnectConfigDto? Reconnect { get; init; }
/// <summary>Write-back knobs — the driver's half of the two-layer opt-in on observation injection.</summary>
public MTConnectWriteConfigDto? Write { get; init; }
}
/// <summary>Wire mirror of the driver's probe config block.</summary>
@@ -403,4 +447,11 @@
/// <summary>Multiplier applied each retry.</summary>
public double? BackoffMultiplier { get; init; }
}
/// <summary>Wire mirror of the driver's write-back config block.</summary>
public sealed class MTConnectWriteConfigDto
{
/// <summary>Whether the driver accepts write-back at all.</summary>
public bool? Enabled { get; init; }
}
}