fix(modbus): enforce region/type compatibility at the tag factory
P6: Coils/DiscreteInputs carry only Bool and register regions never Bool; the factory (and so the deploy-gate Inspect and the AdminUI parse consult) refuses a mismatch, and an absent dataType defaults per region (Bool for a bit region). A HoldingRegisters+Bool tag used to throw out of the coalescing planner and stall every tag of the subscription; the planner also skips any incompatible pair defensively. The tag editor keeps the type compatible when the region changes; the CLI refuses the pair up front. The array-write parity test records the Coils divergence from DriverArrayWriteSupport (owned outside this lane) as follow-up F1. Refs #741 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -57,6 +57,14 @@ public sealed class ReadCommand : ModbusCommandBase
|
||||
ValidateEndpoint();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
// The driver's tag factory refuses an incompatible region/type pair (the tag would read
|
||||
// BadNodeIdUnknown) — say why up front instead.
|
||||
if (!ModbusTagDefinitionFactory.IsRegionTypeCompatible(Region, DataType))
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
Region is ModbusRegion.Coils or ModbusRegion.DiscreteInputs
|
||||
? $"Region '{Region}' only carries Bool values (--type Bool); got '{DataType}'."
|
||||
: $"Region '{Region}' carries register types; use --type BitInRegister for a bit of a register, not '{DataType}'.");
|
||||
|
||||
var tagName = SynthesiseTagName(Region, Address, DataType);
|
||||
var tag = new ModbusTagDefinition(
|
||||
Name: tagName,
|
||||
|
||||
@@ -74,6 +74,9 @@ public sealed class WriteCommand : ModbusCommandBase
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"Region 'Coils' only supports boolean values (--type Bool). " +
|
||||
$"Type '{DataType}' cannot represent a single-bit coil write.");
|
||||
if (!ModbusTagDefinitionFactory.IsRegionTypeCompatible(Region, DataType))
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"Region '{Region}' carries register types; use --type BitInRegister for a bit of a register, not '{DataType}'.");
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Region, Address, DataType);
|
||||
var tag = new ModbusTagDefinition(
|
||||
|
||||
+37
-2
@@ -50,7 +50,13 @@ public static class ModbusTagDefinitionFactory
|
||||
// Strict enum reads: a present-but-invalid (typo'd) enum rejects the tag
|
||||
// (→ BadNodeIdUnknown) instead of silently defaulting to a wrong-width Good.
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "region", ModbusRegion.HoldingRegisters, out var region)) return false;
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", ModbusDataType.Int16, out var dataType)) return false;
|
||||
// An absent dataType defaults per region: a bit region (Coils / DiscreteInputs) only carries
|
||||
// Bool, so defaulting it to Int16 would reject every bit tag authored without the key.
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", DefaultDataType(region), out var dataType)) return false;
|
||||
// Region/type compatibility: a bit region carries only Bool, a register region never Bool.
|
||||
// A mismatch used to throw out of the coalescing planner (killing every tag's poll) or
|
||||
// silently return a bool into an Int32 node.
|
||||
if (!IsRegionTypeCompatible(region, dataType)) return false;
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "byteOrder", ModbusByteOrder.BigEndian, out var byteOrder)) return false;
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "stringByteOrder", ModbusStringByteOrder.HighByteFirst, out var stringByteOrder)) return false;
|
||||
var bitIndex = (byte)ReadInt(root, "bitIndex");
|
||||
@@ -94,6 +100,25 @@ public static class ModbusTagDefinitionFactory
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>The data type an authored tag gets when its <c>dataType</c> key is absent.</summary>
|
||||
/// <param name="region">The tag's region.</param>
|
||||
/// <returns><c>Bool</c> for Coils / DiscreteInputs, <c>Int16</c> for the register regions.</returns>
|
||||
public static ModbusDataType DefaultDataType(ModbusRegion region)
|
||||
=> region is ModbusRegion.Coils or ModbusRegion.DiscreteInputs ? ModbusDataType.Bool : ModbusDataType.Int16;
|
||||
|
||||
/// <summary>
|
||||
/// Region/type compatibility, the one rule every authoring path and the driver share: Coils and
|
||||
/// DiscreteInputs carry only <c>Bool</c>; HoldingRegisters and InputRegisters carry every register
|
||||
/// type (including <c>BitInRegister</c>) but never <c>Bool</c>.
|
||||
/// </summary>
|
||||
/// <param name="region">The tag's region.</param>
|
||||
/// <param name="dataType">The tag's data type.</param>
|
||||
/// <returns><see langword="true"/> when the driver can read the pair.</returns>
|
||||
public static bool IsRegionTypeCompatible(ModbusRegion region, ModbusDataType dataType)
|
||||
=> region is ModbusRegion.Coils or ModbusRegion.DiscreteInputs
|
||||
? dataType == ModbusDataType.Bool
|
||||
: dataType != ModbusDataType.Bool;
|
||||
|
||||
/// <summary>
|
||||
/// Inverse of <see cref="FromTagConfig"/>: serialises a typed definition back to the camelCase
|
||||
/// <c>TagConfig</c> JSON the mapper reads. Used by authoring paths (the Client CLI) that construct a
|
||||
@@ -247,7 +272,17 @@ public static class ModbusTagDefinitionFactory
|
||||
// Type-dependent guards. The byte/ushort NARROWING is part of the rule, not an accident of the
|
||||
// mapper: it reads `(byte)bitIndex` / `(ushort)stringLength`, so 256 is bit 0 and is ACCEPTED.
|
||||
// Describing the un-narrowed value here would call a blob invalid that the driver serves.
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", ModbusDataType.Int16, out var dataType)) return;
|
||||
var region = TagConfigJson.TryReadEnumStrict(root, "region", ModbusRegion.HoldingRegisters, out var r)
|
||||
? r : ModbusRegion.HoldingRegisters;
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", DefaultDataType(region), out var dataType)) return;
|
||||
|
||||
if (root.TryGetProperty("region", out _) && !IsRegionTypeCompatible(region, dataType))
|
||||
{
|
||||
warnings.Add(region is ModbusRegion.Coils or ModbusRegion.DiscreteInputs
|
||||
? $"Modbus region {region} only carries Bool tags; got {dataType} — the tag is rejected (BadNodeIdUnknown)."
|
||||
: $"Modbus Bool tags only live in Coils / DiscreteInputs; a {region} tag must use a register type "
|
||||
+ "(use BitInRegister for a bit of a register) — the tag is rejected (BadNodeIdUnknown).");
|
||||
}
|
||||
|
||||
if (dataType == ModbusDataType.String && (ushort)ReadInt(root, "stringLength") < 1)
|
||||
{
|
||||
|
||||
@@ -1186,6 +1186,9 @@ public sealed class ModbusDriver
|
||||
{
|
||||
if (!table.TryGetValue(fullReferences[i], out var tag)) continue;
|
||||
if (tag.CoalesceProhibited) continue;
|
||||
// Defensive: the factory refuses incompatible pairs, but the planner's register math must never
|
||||
// throw for a tag that slipped through — one bad tag would otherwise fail the whole batch.
|
||||
if (!ModbusTagDefinitionFactory.IsRegionTypeCompatible(tag.Region, tag.DataType)) continue;
|
||||
if (tag.ArrayCount.HasValue) continue;
|
||||
if (tag.Region is not (ModbusRegion.HoldingRegisters or ModbusRegion.InputRegisters)) continue;
|
||||
if (tag.DataType is ModbusDataType.String or ModbusDataType.BitInRegister) continue;
|
||||
|
||||
Reference in New Issue
Block a user