fix(focas): refuse String on PMC/PARAM/MACRO in the factory, editor and CLI (R07:U8)
FocasDataType.String was authorable on any address, but no area read returns text: PMC fell through to a byte read, PARAM to Int32, MACRO to a double, and the String node received a boxed number at Good. One rule, FocasTagDefinitionFactory.DescribeDataTypeMismatch, now backs every path: - FromTagConfig rejects the pair, so the driver skips + reports it (BadNodeIdUnknown, never a Good number) and Inspect names the reason at deploy; - FocasTagConfigModel.Validate refuses it on the typed fields; - the AdminUI editor only offers String where it is readable and shows the rule's text inline (kept visible for a loaded blob that already carries it); - the CLI read/subscribe/probe/write commands refuse it at the boundary. String stays valid on FIXED: paths (Identity/Program name/mode text) and is ignored on ALARM: tags; an unparseable address keeps its own diagnosis. Razor change (FocasTagConfigEditor.razor) needs the Wave 4 live check. Refs #740 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -28,6 +28,7 @@ public sealed class ProbeCommand : FocasCommandBase
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateOptions();
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var probeTag = new FocasTagDefinition(
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
|
||||
/// <summary>Gets the data type to interpret the address as.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -28,6 +28,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
ConfigureLogging();
|
||||
// Validate numeric option ranges before any driver work.
|
||||
ValidateOptions();
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = SynthesiseTagName(Address, DataType);
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
|
||||
/// <summary>Gets the data type of the address.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <summary>Gets the polling interval in milliseconds.</summary>
|
||||
@@ -33,6 +33,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
// --interval-ms) before any driver work so a zero/negative interval surfaces as a
|
||||
// clean CommandException rather than a tight-spinning poll loop.
|
||||
ValidateOptions(IntervalMs);
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
|
||||
/// <summary>Gets the data type of the value to write.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <summary>Gets the value to write.</summary>
|
||||
@@ -37,6 +37,7 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
// port/timeout surfaces as a clean CommandException rather than an opaque
|
||||
// downstream exception.
|
||||
ValidateOptions();
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
|
||||
|
||||
@@ -89,4 +89,17 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"--interval-ms must be positive (got {iv}).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refuses an <c>--address</c> / <c>--type</c> pair the driver cannot serve (String on a PMC / PARAM / MACRO
|
||||
/// address — the driver skips such a tag, so the command would only ever print BadNodeIdUnknown). Same
|
||||
/// rule as the driver and the AdminUI editor: <see cref="FocasTagDefinitionFactory.DescribeDataTypeMismatch"/>.
|
||||
/// </summary>
|
||||
/// <param name="address">The FOCAS address.</param>
|
||||
/// <param name="type">The data type.</param>
|
||||
protected static void ValidateDataType(string address, FocasDataType type)
|
||||
{
|
||||
if (FocasTagDefinitionFactory.DescribeDataTypeMismatch(type, address) is { } error)
|
||||
throw new CliFx.Exceptions.CommandException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ public static class FocasTagDefinitionFactory
|
||||
// Strict enum read: a typo'd dataType rejects the tag (→ BadNodeIdUnknown)
|
||||
// instead of silently defaulting to a wrong-width Good.
|
||||
if (!TagConfigJson.TryReadEnumStrict(root, "dataType", FocasDataType.Int32, out var dataType)) return false;
|
||||
// A data type no read of this address can produce (String on PMC / PARAM / MACRO) rejects the tag:
|
||||
// served, it published a boxed number at Good into a String node.
|
||||
if (DescribeDataTypeMismatch(dataType, address) is not null) return false;
|
||||
// FOCAS authored tags carry no `writable` key (writability lives on the Tag entity / node ACL):
|
||||
// absent ⇒ read-only. An explicit writable:true is honoured for the driver's internal write gate
|
||||
// (default-false, so a missing key stays read-only — the note is preserved).
|
||||
@@ -73,6 +76,26 @@ public static class FocasTagDefinitionFactory
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ONE address/data-type compatibility rule, shared by the mapper (and so the driver's init and the
|
||||
/// deploy inspector), the AdminUI tag editor and the CLI. <see cref="FocasDataType.String"/> is only
|
||||
/// servable from the fixed tree (Identity / Program name / mode text …): a PMC, parameter or macro read
|
||||
/// returns a number, which a String node would receive at Good. An <c>ALARM:</c> tag has no value, so its
|
||||
/// data type is irrelevant; an unparseable address is left to the address check, which names it better.
|
||||
/// </summary>
|
||||
/// <param name="dataType">The authored data type.</param>
|
||||
/// <param name="address">The authored address.</param>
|
||||
/// <returns>The reason the pair cannot be served, or <see langword="null"/> when it can.</returns>
|
||||
public static string? DescribeDataTypeMismatch(FocasDataType dataType, string? address)
|
||||
{
|
||||
if (dataType != FocasDataType.String) return null;
|
||||
var parsed = FocasAddress.TryParse(address);
|
||||
if (parsed is null || parsed.Kind is FocasAreaKind.Fixed or FocasAreaKind.Alarm) return null;
|
||||
return $"FOCAS data type 'String' is not readable at '{address!.Trim()}' — PMC, PARAM: and MACRO: reads " +
|
||||
"return numbers (String is only valid on FIXED: paths such as FIXED:Identity/SeriesNumber). " +
|
||||
"Choose Bit / Byte / Int16 / Int32 / Float32 / Float64.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tag's cross-driver <c>alarm</c> object, or <see langword="null"/> when it carries none.
|
||||
/// Lenient like the host's <c>TagConfigIntent</c>: a non-integer <c>severity</c> is simply absent.
|
||||
@@ -236,6 +259,10 @@ public static class FocasTagDefinitionFactory
|
||||
}
|
||||
|
||||
if (TagConfigJson.DescribeInvalidEnum<FocasDataType>(root, "dataType") is { } w) rejections.Add(w);
|
||||
else if (addr.ValueKind == JsonValueKind.String
|
||||
&& TagConfigJson.TryReadEnumStrict(root, "dataType", FocasDataType.Int32, out var dataType)
|
||||
&& DescribeDataTypeMismatch(dataType, addr.GetString()) is { } mismatch)
|
||||
rejections.Add(mismatch + " The driver will skip this tag and it will read BadNodeIdUnknown.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user