Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Daqifi.Core.Tests/Device/ScpiResponseClassifierTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,31 @@ public void IsScpiErrorLine_DoesNotMatchNonScpiText(string line)
{
Assert.False(ScpiResponseClassifier.IsScpiErrorLine(line));
}

[Theory]
[InlineData("**ERROR: -200, \"Execution error\"", -200)]
[InlineData("**ERROR -200, \"Execution error\"", -200)]
[InlineData("**ERROR\t-200, \"Execution error\"", -200)]
[InlineData("ERROR: -113, \"Undefined header\"", -113)]
[InlineData("ERROR -113", -113)] // no trailing comma
[InlineData(" ERROR: -200, \"x\" \r\n", -200)] // leading/trailing whitespace + CRLF
[InlineData("ERROR: 42, \"positive\"", 42)] // positive code
public void TryExtractErrorCode_ExtractsCode_AcrossDelimiterVariants(string line, int expected)
{
Assert.True(ScpiResponseClassifier.TryExtractErrorCode(line, out var code));
Assert.Equal(expected, code);
}

[Theory]
[InlineData("Error !! No SD Card Detected")] // ERROR token but non-numeric follow
[InlineData("error_log.bin")] // filename
[InlineData("Errors.txt")] // filename
[InlineData("OK")] // not an error line
[InlineData("")]
public void TryExtractErrorCode_ReturnsFalseAndZero_ForNonNumericOrNonError(string line)
{
Assert.False(ScpiResponseClassifier.TryExtractErrorCode(line, out var code));
Assert.Equal(0, code);
}
}
}
39 changes: 2 additions & 37 deletions src/Daqifi.Core/Device/DaqifiStreamingDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1347,7 +1347,7 @@ public async Task<SdCardStorageInfo> GetSdCardStorageAsync(CancellationToken can
// query at all — typically because it predates the version that introduced it — so
// it gets the typed feature-gating exception instead of a generic operation error.
if (lastScpiError != null
&& TryParseScpiErrorCode(lastScpiError, out var scpiErrorCode)
&& ScpiResponseClassifier.TryExtractErrorCode(lastScpiError, out var scpiErrorCode)
&& scpiErrorCode == ScpiErrorCodeUndefinedHeader)
{
throw new FeatureNotSupportedException(
Expand Down Expand Up @@ -1846,41 +1846,6 @@ private static bool IsScpiErrorLine(string line)
return ScpiResponseClassifier.IsScpiErrorLine(line);
}

/// <summary>
/// Parses the numeric error code out of a SCPI error line matched by
/// <see cref="IsScpiErrorLine"/> — e.g. <c>**ERROR: -113, "Undefined header"</c>,
/// <c>ERROR: -113,"Undefined header"</c>, or a space/tab-delimited variant like
/// <c>**ERROR -113, "Undefined header"</c>. The delimiter between the <c>ERROR</c>/
/// <c>**ERROR</c> token and the code may be <c>:</c>, space, or tab — matching the
/// delimiters <see cref="ScpiResponseClassifier"/> accepts — and the code is the text
/// up to the following comma (if any).
/// </summary>
private static bool TryParseScpiErrorCode(string line, out int code)
{
code = 0;
var trimmed = line.TrimStart();

string afterToken;
if (trimmed.StartsWith("**ERROR", StringComparison.OrdinalIgnoreCase))
{
afterToken = trimmed[7..];
}
else if (trimmed.StartsWith("ERROR", StringComparison.OrdinalIgnoreCase))
{
afterToken = trimmed[5..];
}
else
{
return false;
}

afterToken = afterToken.TrimStart(':', ' ', '\t');

var commaIndex = afterToken.IndexOf(',');
var codeSpan = (commaIndex >= 0 ? afterToken[..commaIndex] : afterToken).Trim();
return int.TryParse(codeSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out code);
}

// Permissive: any line that looks like a device error or status message,
// including firmware text such as "Error !! ...". Used to recognize that
// the parser would yield no result, without polluting LastScpiError with
Expand Down Expand Up @@ -1998,7 +1963,7 @@ private static void ValidateSdCardFileName(string fileName)
// so the caller's retry loop can react (kick LAN:APPLY) instead of just
// waiting out a blind delay.
var errorLine = lines.LastOrDefault(IsScpiErrorLine);
if (errorLine != null && TryParseScpiErrorCode(errorLine, out var errorCode) && errorCode == -200)
if (errorLine != null && ScpiResponseClassifier.TryExtractErrorCode(errorLine, out var errorCode) && errorCode == -200)
{
throw new LanNotInitializedException(errorLine.Trim());
}
Expand Down
54 changes: 52 additions & 2 deletions src/Daqifi.Core/Device/ScpiResponseClassifier.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;

#nullable enable

Expand Down Expand Up @@ -50,6 +51,54 @@ internal static bool IsScpiErrorLine(string line)
|| MatchesStrictScpiErrorPrefix(trimmed, "ERROR");
}

/// <summary>
/// Extracts the numeric error code from a SCPI error line — e.g. <c>-200</c> from
/// <c>**ERROR: -200,"Execution error"</c>, <c>ERROR -113,"Undefined header"</c>, or
/// <c>**ERROR\t-113,...</c>. The delimiter between the <c>ERROR</c>/<c>**ERROR</c> token and
/// the code may be <c>:</c>, space, or tab — the same set the line matchers above accept, kept
/// here so the accepted delimiters can't drift between detection and extraction. The code is
/// the text up to the following comma (if any).
/// </summary>
/// <param name="line">The candidate error line.</param>
/// <param name="code">The parsed error code when the method returns <c>true</c>; otherwise 0.</param>
/// <returns><c>true</c> if a numeric error code was extracted; otherwise <c>false</c>.</returns>
internal static bool TryExtractErrorCode(string line, out int code)
{
code = 0;
var trimmed = line.TrimStart();

string afterToken;
if (trimmed.StartsWith("**ERROR", StringComparison.OrdinalIgnoreCase))
{
afterToken = trimmed[7..];
}
else if (trimmed.StartsWith("ERROR", StringComparison.OrdinalIgnoreCase))
{
afterToken = trimmed[5..];
}
else
{
return false;
}

afterToken = afterToken.TrimStart(TokenDelimiters);

var commaIndex = afterToken.IndexOf(',');
var codeSpan = (commaIndex >= 0 ? afterToken[..commaIndex] : afterToken).Trim();
return int.TryParse(codeSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out code);
}

/// <summary>
/// The delimiters accepted between the <c>ERROR</c>/<c>**ERROR</c> token and the error code.
/// Single-sourced here: the line matchers test membership via <see cref="IsTokenDelimiter"/>, and
/// <see cref="TryExtractErrorCode"/> trims with this array directly — both draw from this one set,
/// so detection and extraction can't drift.
/// </summary>
private static readonly char[] TokenDelimiters = { ':', ' ', '\t' };

/// <summary>Returns true if <paramref name="c"/> is one of the accepted <see cref="TokenDelimiters"/>.</summary>
private static bool IsTokenDelimiter(char c) => Array.IndexOf(TokenDelimiters, c) >= 0;

private static bool MatchesErrorPrefix(string trimmed, string prefix)
{
if (trimmed.Length < prefix.Length
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Expand All @@ -58,7 +107,7 @@ private static bool MatchesErrorPrefix(string trimmed, string prefix)
if (trimmed.Length == prefix.Length)
return true;
var next = trimmed[prefix.Length];
if (next == ':' || next == ' ' || next == '\t')
if (IsTokenDelimiter(next))
return true;
// Single '!' is ambiguous (could be a filename like "error!log.bin").
// Require '!!' so we still catch firmware "Error!!" status text but
Expand All @@ -79,7 +128,8 @@ private static bool MatchesStrictScpiErrorPrefix(string trimmed, string prefix)
var next = trimmed[prefix.Length];
if (next == ':')
return true;
if (next != ' ' && next != '\t')
// ':' already returned above, so this rejects anything that isn't a space/tab delimiter.
if (!IsTokenDelimiter(next))
return false;

// A space/tab delimiter alone is ambiguous — firmware status text like
Expand Down