namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// /// Turns a device-reported sibling-name sequence (axes, spindles) into unique fixed-tree /// path segments (Gitea #548). /// /// /// /// A real CNC can report duplicate sibling names — one observed Makino returns two axis /// folders both called AA. 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 Axes/ subtree was untrustworthy on such a machine. /// /// /// Scheme: every member of a collision set gets a 1-based occurrence-ordinal suffix in /// slot order (AA_1, AA_2); unique names stay bare. Three deliberate /// choices inside that: /// /// /// Names stay the primary identity, not slot numbers. 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 (3_AA) 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. /// /// /// The whole collision set is suffixed, not just the second occurrence. When two /// axes are both called AA, the bare segment AA 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 AA on such a machine reads Bad, never one /// arbitrary axis's data. /// /// /// Occurrence ordinal, not absolute slot number. The ordinal is stable under /// insertion/removal of unrelated axes (a new B axis ahead of the two AAs /// renames nothing; their slot numbers would both change). If the collision-set membership /// itself changes — a third AA 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). /// /// /// Suffix collisions with genuine names are structurally impossible for axes (FOCAS axis /// displays are at most two characters — name + suffix — so they cannot contain _), /// 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. /// /// internal static class FixedTreeNames { /// /// Maps each name in (in slot order) to a unique sibling path /// segment. Unique names map to themselves; duplicated names map to /// {name}_{occurrence}. Comparison is case-insensitive, matching the fixed-tree /// snapshot cache and read-path lookups. /// /// Device-reported sibling names, in device (slot) order. /// One segment per input name, unique within the returned list. public static IReadOnlyList Disambiguate(IEnumerable names) { var list = names as IReadOnlyList ?? [.. names]; var counts = new Dictionary(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(StringComparer.OrdinalIgnoreCase); foreach (var name in list) if (counts[name] == 1) used.Add(name); var nextOrdinal = new Dictionary(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; } }