fix(driver-modbus): resolve Low code-review findings (Driver.Modbus-003,007,008,009,010,011,012)
- Driver.Modbus-003: route every _health access through ReadHealth / WriteHealth helpers backed by Volatile.Read / Volatile.Write so a burst of concurrent ReadAsync callers always sees a complete snapshot. - Driver.Modbus-007: promoted the Int64 / UInt64 → Int32 surfacing caveat to a full <remarks> block; rewrote DisableFC23's doc to flag it as reserved / no-op. - Driver.Modbus-008: deleted stale duplicate doc, rewrote the prohibition-block summaries to credit the shipped re-probe loop, and removed the unused 'status' local in the ModbusException catch arm. - Driver.Modbus-009: bind-time validation rejects StringLength < 1 for String tags; ModbusTcpTransport clamps keep-alive intervals to whole seconds (>=1). - Driver.Modbus-010: documented WriteOnChangeOnly's cache-invalidation policy (reads-only) and the write-only-tag caveat. - Driver.Modbus-011: collected the scattered instance fields into a single contiguous block at the top of ModbusDriver. - Driver.Modbus-012: covered the previously-uncovered Reinitialize state-hygiene, malformed/truncated/empty-bitmap response, and DisposeAsync teardown paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -111,16 +111,22 @@ public static class ModbusDriverFactoryExtensions
|
||||
var name = t.Name ?? throw new InvalidOperationException(
|
||||
$"Modbus config for '{driverInstanceId}' has a tag missing Name");
|
||||
|
||||
// Driver.Modbus-009: a String tag with StringLength = 0 yields RegisterCount = 0, which
|
||||
// turns into an FC03/FC04 with quantity 0 — a spec-illegal request the PLC rejects with
|
||||
// exception 03. Catch the misconfiguration at bind time with a clear diagnostic instead
|
||||
// of waiting for the cryptic Illegal Data Value to surface at runtime.
|
||||
|
||||
// AddressString takes precedence over the structured fields (Region/Address/DataType/
|
||||
// ByteOrder/BitIndex/StringLength/ArrayCount). Tags can mix forms freely — newer pasted
|
||||
// rows use the grammar string, legacy rows keep the structured form. Fields not derivable
|
||||
// from the grammar (Writable, WriteIdempotent, StringByteOrder) always come from the DTO.
|
||||
ModbusTagDefinition tag;
|
||||
if (!string.IsNullOrWhiteSpace(t.AddressString))
|
||||
{
|
||||
if (!ModbusAddressParser.TryParse(t.AddressString, family, melsecSubFamily, out var parsed, out var parseError))
|
||||
throw new InvalidOperationException(
|
||||
$"Modbus tag '{name}' in '{driverInstanceId}' has invalid AddressString '{t.AddressString}': {parseError}");
|
||||
return new ModbusTagDefinition(
|
||||
tag = new ModbusTagDefinition(
|
||||
Name: name,
|
||||
Region: parsed!.Region,
|
||||
Address: parsed.Offset,
|
||||
@@ -137,26 +143,45 @@ public static class ModbusDriverFactoryExtensions
|
||||
Deadband: t.Deadband,
|
||||
UnitId: t.UnitId);
|
||||
}
|
||||
else
|
||||
{
|
||||
tag = new ModbusTagDefinition(
|
||||
Name: name,
|
||||
Region: ParseEnum<ModbusRegion>(t.Region, t.Name, driverInstanceId, "Region"),
|
||||
Address: t.Address ?? throw new InvalidOperationException(
|
||||
$"Modbus tag '{t.Name}' in '{driverInstanceId}' missing Address"),
|
||||
DataType: ParseEnum<ModbusDataType>(t.DataType, t.Name, driverInstanceId, "DataType"),
|
||||
Writable: t.Writable ?? true,
|
||||
ByteOrder: t.ByteOrder is null
|
||||
? ModbusByteOrder.BigEndian
|
||||
: ParseEnum<ModbusByteOrder>(t.ByteOrder, t.Name, driverInstanceId, "ByteOrder"),
|
||||
BitIndex: t.BitIndex ?? 0,
|
||||
StringLength: t.StringLength ?? 0,
|
||||
StringByteOrder: t.StringByteOrder is null
|
||||
? ModbusStringByteOrder.HighByteFirst
|
||||
: ParseEnum<ModbusStringByteOrder>(t.StringByteOrder, t.Name, driverInstanceId, "StringByteOrder"),
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
ArrayCount: t.ArrayCount,
|
||||
Deadband: t.Deadband,
|
||||
UnitId: t.UnitId);
|
||||
}
|
||||
|
||||
return new ModbusTagDefinition(
|
||||
Name: name,
|
||||
Region: ParseEnum<ModbusRegion>(t.Region, t.Name, driverInstanceId, "Region"),
|
||||
Address: t.Address ?? throw new InvalidOperationException(
|
||||
$"Modbus tag '{t.Name}' in '{driverInstanceId}' missing Address"),
|
||||
DataType: ParseEnum<ModbusDataType>(t.DataType, t.Name, driverInstanceId, "DataType"),
|
||||
Writable: t.Writable ?? true,
|
||||
ByteOrder: t.ByteOrder is null
|
||||
? ModbusByteOrder.BigEndian
|
||||
: ParseEnum<ModbusByteOrder>(t.ByteOrder, t.Name, driverInstanceId, "ByteOrder"),
|
||||
BitIndex: t.BitIndex ?? 0,
|
||||
StringLength: t.StringLength ?? 0,
|
||||
StringByteOrder: t.StringByteOrder is null
|
||||
? ModbusStringByteOrder.HighByteFirst
|
||||
: ParseEnum<ModbusStringByteOrder>(t.StringByteOrder, t.Name, driverInstanceId, "StringByteOrder"),
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
ArrayCount: t.ArrayCount,
|
||||
Deadband: t.Deadband,
|
||||
UnitId: t.UnitId);
|
||||
ValidateStringLength(tag, driverInstanceId);
|
||||
return tag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Modbus-009: reject <c>StringLength = 0</c> for <c>String</c>-typed tags. The
|
||||
/// driver computes <c>RegisterCount = (StringLength + 1) / 2</c> which would emit an
|
||||
/// FC03/FC04 with <c>quantity = 0</c>, a spec-illegal request the PLC rejects with
|
||||
/// exception 03 (Illegal Data Value). Surface as a clear bind-time error.
|
||||
/// </summary>
|
||||
private static void ValidateStringLength(ModbusTagDefinition tag, string driverInstanceId)
|
||||
{
|
||||
if (tag.DataType == ModbusDataType.String && tag.StringLength < 1)
|
||||
throw new InvalidOperationException(
|
||||
$"Modbus tag '{tag.Name}' in '{driverInstanceId}' has DataType=String but StringLength={tag.StringLength}. " +
|
||||
$"String tags must declare StringLength >= 1 (the number of ASCII characters, packed 2 per register).");
|
||||
}
|
||||
|
||||
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field) where T : struct, Enum
|
||||
|
||||
Reference in New Issue
Block a user