fix(abcip): bit-in-word writes take the parent width from its read
H17: the .n read-modify-write hard-coded a DINT parent. On an INT or SINT host (both legal in Logix, and reads of Flags.3 there already worked) the DINT decode of a 2-byte buffer threw OutOfBounds, the generic catch evicted the BIT tag's (non-existent) runtime and reported BadCommunicationError + Degraded. The parent width now comes from the parent read's size (1 SINT / 2 INT / 4 DINT / 8 LINT; unknown keeps the DINT assumption). Narrow hosts use unsigned arithmetic so a set top bit encodes without overflow; a bit past the host's width is BadOutOfRange and a non-integer width BadTypeMismatch, both refused before anything is written, warned once, no health write. A transport failure in the RMW now evicts the parent runtime it actually used. Native end-to-end: an INT host is written as ONE INT (type 0xC3, 2 bytes). Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -1374,6 +1374,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
continue;
|
||||
}
|
||||
|
||||
string? rmwParent = null;
|
||||
try
|
||||
{
|
||||
var parsedPath = AbCipTagPath.TryParse(def.TagPath);
|
||||
@@ -1384,6 +1385,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
// losing one another's update.
|
||||
if (def.DataType == AbCipDataType.Bool && parsedPath?.BitIndex is int bit)
|
||||
{
|
||||
rmwParent = (parsedPath with { BitIndex = null }).ToLibplctagName();
|
||||
results[i] = new WriteResult(
|
||||
await WriteBitInDIntAsync(device, parsedPath, bit, w.Value, cancellationToken)
|
||||
.ConfigureAwait(false));
|
||||
@@ -1542,8 +1544,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Transport / wire error — evict so the next write creates a fresh handle.
|
||||
EvictRuntime(device, def.Name);
|
||||
// Transport / wire error — evict so the next write creates a fresh handle (for a bit write, the
|
||||
// parent runtime the read-modify-write used; the bit tag itself holds none).
|
||||
if (rmwParent is not null) EvictParentRuntime(device, rmwParent);
|
||||
else EvictRuntime(device, def.Name);
|
||||
results[i] = new WriteResult(AbCipStatusMapper.BadCommunicationError);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
_logger.LogWarning(ex,
|
||||
@@ -1556,10 +1560,11 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-modify-write one bit within a DINT parent. Creates / reuses a parallel
|
||||
/// parent-DINT runtime (distinct from the bit-selector handle) + serialises concurrent
|
||||
/// writers against the same parent via a per-parent <see cref="SemaphoreSlim"/>.
|
||||
/// Matches the Modbus BitInRegister + FOCAS PMC Bit pattern.
|
||||
/// Read-modify-write one bit within an integer parent (SINT / INT / DINT / LINT — Logix allows <c>.n</c> on
|
||||
/// each). Creates / reuses a parallel parent runtime (distinct from the bit-selector handle) + serialises
|
||||
/// concurrent writers against the same parent via a per-parent <see cref="SemaphoreSlim"/>. The parent's
|
||||
/// width comes from the size its read returned (H17 — a hard-coded DINT decode threw OutOfBounds on an INT
|
||||
/// host and was reported as a comms fault). Matches the Modbus BitInRegister + FOCAS PMC Bit pattern.
|
||||
/// </summary>
|
||||
private async Task<uint> WriteBitInDIntAsync(
|
||||
DeviceState device, AbCipTagPath bitPath, int bit, object? value, CancellationToken ct)
|
||||
@@ -1576,12 +1581,49 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var readStatus = parentRuntime.GetStatus();
|
||||
if (readStatus != 0) return AbCipStatusMapper.MapStatus(readStatus);
|
||||
|
||||
var current = Convert.ToInt32(parentRuntime.DecodeValue(AbCipDataType.DInt, bitIndex: null) ?? 0);
|
||||
var updated = Convert.ToBoolean(value)
|
||||
? current | (1 << bit)
|
||||
: current & ~(1 << bit);
|
||||
// Unsigned at the narrow widths so a set top bit (SINT bit 7, INT bit 15) encodes without overflowing the
|
||||
// signed type; DINT keeps its signed int arithmetic (1 << 31 is int.MinValue, which a DINT encodes as is).
|
||||
var size = parentRuntime.GetSize();
|
||||
AbCipDataType? hostType = size switch
|
||||
{
|
||||
1 => AbCipDataType.USInt,
|
||||
2 => AbCipDataType.UInt,
|
||||
8 => AbCipDataType.LInt,
|
||||
4 or <= 0 => AbCipDataType.DInt, // <= 0: the runtime cannot say — the historical DINT assumption
|
||||
_ => null,
|
||||
};
|
||||
if (hostType is not { } type)
|
||||
{
|
||||
WarnTypeMismatchOnce(parentName,
|
||||
"AbCip bit write to {Parent}.{Bit} on device {Device} refused: the parent is {Size} byte(s), not an integer host (logged once per tag)",
|
||||
parentName, bit, device.Options.HostAddress, size);
|
||||
return AbCipStatusMapper.BadTypeMismatch;
|
||||
}
|
||||
var widthBits = type switch { AbCipDataType.USInt => 8, AbCipDataType.UInt => 16, AbCipDataType.LInt => 64, _ => 32 };
|
||||
if (bit >= widthBits)
|
||||
{
|
||||
WarnTypeMismatchOnce(parentName,
|
||||
"AbCip bit write to {Parent}.{Bit} on device {Device} refused: the parent holds only {Bits} bits (logged once per tag)",
|
||||
parentName, bit, device.Options.HostAddress, widthBits);
|
||||
return AbCipStatusMapper.BadOutOfRange;
|
||||
}
|
||||
|
||||
parentRuntime.EncodeValue(AbCipDataType.DInt, bitIndex: null, updated);
|
||||
var set = Convert.ToBoolean(value);
|
||||
var decoded = parentRuntime.DecodeValue(type, bitIndex: null) ?? 0;
|
||||
object updated;
|
||||
if (type == AbCipDataType.LInt)
|
||||
{
|
||||
var current = Convert.ToInt64(decoded);
|
||||
updated = set ? current | (1L << bit) : current & ~(1L << bit);
|
||||
}
|
||||
else
|
||||
{
|
||||
var mask = type switch { AbCipDataType.USInt => 0xFF, AbCipDataType.UInt => 0xFFFF, _ => -1 };
|
||||
var current = Convert.ToInt32(decoded) & mask;
|
||||
updated = (set ? current | (1 << bit) : current & ~(1 << bit)) & mask;
|
||||
}
|
||||
|
||||
parentRuntime.EncodeValue(type, bitIndex: null, updated);
|
||||
await parentRuntime.WriteAsync(ct).ConfigureAwait(false);
|
||||
var writeStatus = parentRuntime.GetStatus();
|
||||
return writeStatus == 0
|
||||
@@ -1763,6 +1805,15 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Evicts + disposes a bit-write parent runtime so the next read-modify-write re-creates it.</summary>
|
||||
private static void EvictParentRuntime(DeviceState device, string parentName)
|
||||
{
|
||||
if (device.ParentRuntimes.TryRemove(parentName, out var stale))
|
||||
{
|
||||
try { stale.Dispose(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => _health;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user