9460da513c
ReadNameRecord read only the first 2 bytes of each 4-byte 0x0089/0x008A record, so the real 31i-B's paired axes 'AA6'/'AA7' (live capture ref-axisname-0x0089.bin) both decoded as 'AA' -- the 'Makino with two AA axes' that #548 then had to disambiguate as AA_1/AA_2. The name is now the record's ASCII up to its FIRST NUL (not trailing-NUL trimming), so buffer residue such as Z-34184's spindle record 53 31 00 4e ('S1' + an 'N' left from an earlier 'CNC') is discarded. Client-visible: on such a machine Axes/AA_1 and Axes/AA_2 become Axes/AA6 and Axes/AA7 (the old names were a decode artefact). #548's disambiguation stays as the guard for genuinely duplicate names. Ported from the sister ignitionoee FOCAS driver (Focas.Wire-001, Focas.Wire-022 / v0.18.1).
103 lines
5.2 KiB
C#
103 lines
5.2 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
/// <summary>
|
|
/// Turns a device-reported sibling-name sequence (axes, spindles) into unique fixed-tree
|
|
/// path segments (Gitea #548).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// A real CNC can report duplicate sibling names — one observed Makino returns two axis
|
|
/// folders both called <c>AA</c>. Before this existed, the emitter offered two identical
|
|
/// browse folders, commit-time collision detection refused to author both (same RawPath),
|
|
/// and the poll loop's name-keyed snapshot cache let the second axis silently overwrite the
|
|
/// first — so the <c>Axes/</c> subtree was untrustworthy on such a machine.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Scheme: every member of a collision set gets a 1-based occurrence-ordinal suffix in
|
|
/// slot order (<c>AA_1</c>, <c>AA_2</c>); unique names stay bare.</b> Three deliberate
|
|
/// choices inside that:
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Names stay the primary identity, not slot numbers.</b> The segment map is re-derived
|
|
/// from the live device at every session bootstrap, so a name-keyed path follows the
|
|
/// physical axis across configuration changes; a slot-qualified path (<c>3_AA</c>) would
|
|
/// silently rebind to a different physical axis whenever the axis list shifts, and would
|
|
/// rename every axis path on every machine for the sake of the rare duplicate.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>The whole collision set is suffixed, not just the second occurrence.</b> When two
|
|
/// axes are both called <c>AA</c>, the bare segment <c>AA</c> ceases to exist — a bare name
|
|
/// that silently means "one of the two" is the same silent-wrongness class #534 and #540
|
|
/// were about. A tag authored against bare <c>AA</c> on such a machine reads Bad, never one
|
|
/// arbitrary axis's data.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Occurrence ordinal, not absolute slot number.</b> The ordinal is stable under
|
|
/// insertion/removal of <em>unrelated</em> axes (a new <c>B</c> axis ahead of the two AAs
|
|
/// renames nothing; their slot numbers would both change). If the collision-set membership
|
|
/// itself changes — a third <c>AA</c> appears, or one of two is removed — the segments do
|
|
/// change; that surfaces on the next browse + commit, and until then the stale authored
|
|
/// segment reads Bad rather than another axis's data (the served tree only changes on a
|
|
/// config redeploy).
|
|
/// </para>
|
|
/// <para>
|
|
/// Suffix collisions with genuine names are not expected (a FOCAS name record is at most four
|
|
/// ASCII characters and controls do not use <c>_</c>), but the generator still checks: a
|
|
/// generated segment that would collide with anything else in the set escalates its ordinal
|
|
/// until unique, so the output is always collision-free whatever the input.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Note on the Makino that motivated this.</b> Its two "AA" axes were a decode artefact:
|
|
/// the name records actually read <c>"AA6"</c> and <c>"AA7"</c>, and the wire layer read only
|
|
/// the first two bytes of each 4-byte record. With the full record decoded
|
|
/// (<see cref="Wire.FocasWireProtocol.ReadNameRecord"/>) that machine reports unique names and
|
|
/// its axes publish bare (<c>Axes/AA6</c>, <c>Axes/AA7</c>). This disambiguation remains as the
|
|
/// guard for a control that genuinely reports duplicates.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal static class FixedTreeNames
|
|
{
|
|
/// <summary>
|
|
/// Maps each name in <paramref name="names"/> (in slot order) to a unique sibling path
|
|
/// segment. Unique names map to themselves; duplicated names map to
|
|
/// <c>{name}_{occurrence}</c>. Comparison is case-insensitive, matching the fixed-tree
|
|
/// snapshot cache and read-path lookups.
|
|
/// </summary>
|
|
/// <param name="names">Device-reported sibling names, in device (slot) order.</param>
|
|
/// <returns>One segment per input name, unique within the returned list.</returns>
|
|
public static IReadOnlyList<string> Disambiguate(IEnumerable<string> names)
|
|
{
|
|
var list = names as IReadOnlyList<string> ?? [.. names];
|
|
|
|
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var name in list)
|
|
counts[name] = counts.TryGetValue(name, out var c) ? c + 1 : 1;
|
|
|
|
// Bare (unique) names are claimed first so a generated "{name}_{k}" can never shadow one.
|
|
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var name in list)
|
|
if (counts[name] == 1)
|
|
used.Add(name);
|
|
|
|
var nextOrdinal = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
var result = new string[list.Count];
|
|
for (var i = 0; i < list.Count; i++)
|
|
{
|
|
var name = list[i];
|
|
if (counts[name] == 1)
|
|
{
|
|
result[i] = name;
|
|
continue;
|
|
}
|
|
|
|
var k = nextOrdinal.TryGetValue(name, out var n) ? n : 1;
|
|
string segment;
|
|
do { segment = $"{name}_{k}"; k++; }
|
|
while (!used.Add(segment));
|
|
nextOrdinal[name] = k;
|
|
result[i] = segment;
|
|
}
|
|
return result;
|
|
}
|
|
}
|