fix(historian): SQL quality contract, long-name refusal, info-bit masking, AtTime-after-now, bootstrap hardening
- I-1: an Empty sample is always Bad. The writer stores a non-Bad NaN/Inf as Empty+BadOutOfRange and any other non-Bad Empty as BadNoData; the updater refuses a non-Bad Empty with BadInvalidArgument; reads clamp a legacy non-Bad Empty row to BadNoData (Raw and AtTime). - Tag/source names over 400 chars are refused, never truncated: writer per-tag failure, updater BadInvalidArgument, reads not-found, events none. - HistorianInfoBits masks the historian bits before setting one. - AtTime past now, stepped from the last sample => UncertainDataSubNormal. - Bootstrapper: logger, 5 s negative cache, ClearPool + Warning when sp_releaseapplock fails. - Provider builds its shared state lazily (an invalid Schema no longer throws at Resolve); Kinds is a FrozenSet; Gateway reuses the recorder's DefaultSupportedKinds. - Cleanup: SqlHistorianDataTypes in its own file (updater dup removed), dead Invalidate and unused status consts deleted, named severity masks, alarm writer brace. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+44
-7
@@ -7,17 +7,46 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
||||
internal static class SqlHistorianStatusCodes
|
||||
{
|
||||
public const uint Good = 0x00000000u;
|
||||
public const uint GoodNoData = 0x00A50000u;
|
||||
public const uint GoodEntryInserted = 0x00A20000u;
|
||||
public const uint GoodEntryReplaced = 0x00A30000u;
|
||||
public const uint BadNoData = 0x809B0000u;
|
||||
public const uint BadEntryExists = 0x809F0000u;
|
||||
public const uint BadNoEntryExists = 0x80A00000u;
|
||||
public const uint BadInvalidTimestamp = 0x80230000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
public const uint BadAggregateInvalidInputs = 0x80D60000u;
|
||||
public const uint BadHistoryOperationUnsupported = 0x80720000u;
|
||||
public const uint UncertainDataSubNormal = 0x40A40000u;
|
||||
|
||||
/// <summary>Stored for a non-finite Double (NaN / ±Infinity) that arrived with a non-Bad status.</summary>
|
||||
public const uint BadOutOfRange = 0x803C0000u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bit masks over a 32-bit OPC UA status code (Part 4 §7.39). These are masks, not emitted codes, so their
|
||||
/// names deliberately avoid the Good/Bad/Uncertain prefix the status-code parity scan keys on.
|
||||
/// </summary>
|
||||
internal static class StatusBits
|
||||
{
|
||||
/// <summary>Severity + sub-code (top 16 bits) of a status, i.e. the code without info bits.</summary>
|
||||
public const uint CodeBitsMask = 0xFFFF0000u;
|
||||
|
||||
/// <summary>The two severity bits (Good = 00, Uncertain = 01, Bad = 10).</summary>
|
||||
public const uint SeverityMask = 0xC0000000u;
|
||||
|
||||
/// <summary>The severity bit set on every Bad code.</summary>
|
||||
public const uint SeverityBadBit = 0x80000000u;
|
||||
|
||||
/// <summary>The two historian info bits (Calculated, Interpolated) in the low bits of the info field.</summary>
|
||||
public const uint HistorianBitsMask = 0x3u;
|
||||
|
||||
/// <summary>True when <paramref name="status"/> has Bad severity.</summary>
|
||||
/// <param name="status">A status code.</param>
|
||||
/// <returns>Whether the Bad severity bit is set.</returns>
|
||||
public static bool IsBad(uint status) => (status & SeverityBadBit) != 0;
|
||||
|
||||
/// <summary>True when <paramref name="status"/> has Good severity.</summary>
|
||||
/// <param name="status">A status code.</param>
|
||||
/// <returns>Whether both severity bits are clear.</returns>
|
||||
public static bool IsGood(uint status) => (status & SeverityMask) == 0;
|
||||
}
|
||||
|
||||
/// <summary>Part 4 §7.39 historian info bits OR-ed onto a status code.</summary>
|
||||
@@ -32,13 +61,21 @@ internal static class HistorianInfoBits
|
||||
/// <summary>The value was interpolated.</summary>
|
||||
public const uint HistorianInterpolated = 0x2u;
|
||||
|
||||
/// <summary>Marks <paramref name="status"/> as a Calculated data value.</summary>
|
||||
/// <summary>
|
||||
/// Marks <paramref name="status"/> as a Calculated data value. Any historian bits the status already carries
|
||||
/// (e.g. a stored Interpolated sample) are cleared first, so the result is never both.
|
||||
/// </summary>
|
||||
/// <param name="status">Base status code.</param>
|
||||
/// <returns>Status with InfoType and Calculated bits set.</returns>
|
||||
public static uint WithCalculated(uint status) => status | InfoTypeDataValue | HistorianCalculated;
|
||||
public static uint WithCalculated(uint status) =>
|
||||
(status & ~StatusBits.HistorianBitsMask) | InfoTypeDataValue | HistorianCalculated;
|
||||
|
||||
/// <summary>Marks <paramref name="status"/> as an Interpolated data value.</summary>
|
||||
/// <summary>
|
||||
/// Marks <paramref name="status"/> as an Interpolated data value. Any historian bits the status already
|
||||
/// carries (e.g. a stored Calculated sample) are cleared first, so the result is never both.
|
||||
/// </summary>
|
||||
/// <param name="status">Base status code.</param>
|
||||
/// <returns>Status with InfoType and Interpolated bits set.</returns>
|
||||
public static uint WithInterpolated(uint status) => status | InfoTypeDataValue | HistorianInterpolated;
|
||||
public static uint WithInterpolated(uint status) =>
|
||||
(status & ~StatusBits.HistorianBitsMask) | InfoTypeDataValue | HistorianInterpolated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user