40c7493829
The bit write read and wrote the parent word as a 32-bit UDInt with no parent check: a bit beyond a 16-bit parent was written into neighbouring bytes, and a signed parent with its sign bit set read back >= 2^31, so the checked conversion turned any bit write into BadOutOfRange. The write now goes through ITwinCATClient.WriteBitAsync: the ADS client resolves the parent symbol, checks the bit against it (TwinCATBitAccess. CheckParent, as the notification path already did), reads exactly parent.ByteSize raw bytes, flips one bit (TwinCATBitAccess.ApplyBitWrite) and writes the bytes back by index group/offset or variable handle. A bit beyond the parent is BadOutOfRange, a non-integer parent BadTypeMismatch, a short parent read BadCommunicationError (never written back), all without a health write. The per-parent RMW lock stays in the driver. Refs #739 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
72 lines
4.0 KiB
C#
72 lines
4.0 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
|
|
|
/// <summary>
|
|
/// Serves a bit-indexed BOOL tag (<c>MAIN.Flags.3</c>, <c>.ADSSTATE_RUN.0</c>) from its PARENT word.
|
|
/// ADS has no symbol for <c>WORD.N</c> — live on a TwinCAT 2.11 target a notification registered on
|
|
/// <c>.ADSSTATE_RUN.0</c> failed with <c>DeviceSymbolNotFound</c> — so a native notification registers
|
|
/// the parent as raw bytes at its resolved width and reads the bit here, exactly as a read already
|
|
/// reads the parent word and extracts the bit.
|
|
/// </summary>
|
|
internal static class TwinCATBitAccess
|
|
{
|
|
/// <summary>The parent word's path: the bit path without its trailing <c>.N</c>.</summary>
|
|
/// <param name="symbolPath">The bit path (<c>MAIN.Flags.3</c>).</param>
|
|
/// <returns>The parent path, or <paramref name="symbolPath"/> when it has no numeric last segment.</returns>
|
|
public static string ParentPath(string symbolPath)
|
|
{
|
|
var lastDot = symbolPath.LastIndexOf('.');
|
|
if (lastDot < 0) return symbolPath;
|
|
return int.TryParse(symbolPath.AsSpan(lastDot + 1), out _) ? symbolPath[..lastDot] : symbolPath;
|
|
}
|
|
|
|
/// <summary>Checks that <paramref name="bit"/> addresses a bit of an integer parent word.</summary>
|
|
/// <param name="bit">The bit index.</param>
|
|
/// <param name="parent">The parent symbol's shape.</param>
|
|
/// <returns><c>null</c> when addressable; otherwise the reason.</returns>
|
|
public static string? CheckParent(int bit, TwinCATPlcSymbolShape parent)
|
|
{
|
|
if (parent.Kind != TwinCATPlcKind.Scalar || parent.IsFloat)
|
|
return $"bit {bit} of {parent.TypeName} ({parent.ByteSize} B) — only an integer word has addressable bits";
|
|
return bit >= 0 && bit < parent.ByteSize * 8
|
|
? null
|
|
: $"bit {bit} is outside the {parent.ByteSize * 8}-bit parent {parent.TypeName}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// The modify step of a bit write's read-modify-write: checks the bit against the parent
|
|
/// (<see cref="CheckParent"/>) and returns the parent's bytes — exactly <c>parent.ByteSize</c> of them —
|
|
/// with one bit set or cleared. Works on raw bytes, so the parent's width and signedness play no part.
|
|
/// </summary>
|
|
/// <param name="parent">The parent symbol's resolved shape.</param>
|
|
/// <param name="current">The parent's current bytes as read from the PLC.</param>
|
|
/// <param name="bit">The bit index.</param>
|
|
/// <param name="value">The bit value.</param>
|
|
/// <param name="updated">The bytes to write back when the result is Good; otherwise empty.</param>
|
|
/// <returns>Good; <c>BadOutOfRange</c> for a bit beyond the parent; <c>BadTypeMismatch</c> for a parent
|
|
/// that is not an integer word; <c>BadCommunicationError</c> for a read shorter than the parent
|
|
/// (writing it back would zero the missing bytes).</returns>
|
|
public static uint ApplyBitWrite(TwinCATPlcSymbolShape parent, ReadOnlySpan<byte> current, int bit, bool value,
|
|
out byte[] updated)
|
|
{
|
|
updated = [];
|
|
if (CheckParent(bit, parent) is not null)
|
|
return parent.Kind != TwinCATPlcKind.Scalar || parent.IsFloat
|
|
? TwinCATStatusMapper.BadTypeMismatch
|
|
: TwinCATStatusMapper.BadOutOfRange;
|
|
if (current.Length < parent.ByteSize) return TwinCATStatusMapper.BadCommunicationError;
|
|
|
|
var bytes = current[..parent.ByteSize].ToArray();
|
|
var mask = (byte)(1 << (bit % 8));
|
|
bytes[bit / 8] = value ? (byte)(bytes[bit / 8] | mask) : (byte)(bytes[bit / 8] & ~mask);
|
|
updated = bytes;
|
|
return TwinCATStatusMapper.Good;
|
|
}
|
|
|
|
/// <summary>Reads bit <paramref name="bit"/> of a little-endian parent word.</summary>
|
|
/// <param name="parent">The parent word's raw bytes.</param>
|
|
/// <param name="bit">The bit index (0 = least significant bit of the first byte).</param>
|
|
/// <returns>The bit's value.</returns>
|
|
public static bool ReadBit(ReadOnlySpan<byte> parent, int bit) =>
|
|
(parent[bit / 8] & (1 << (bit % 8))) != 0;
|
|
}
|