Add Debug page and fix BLE communication issues

Major changes:
- Added Debug tab with real-time message logging and hex viewer
- Implemented timed data logging ($UMTDL command) per API v1.3
- Fixed BLE message parsing to handle notifications without line endings
- Added Schema 2 support (22-field timed logging packets)
- Updated Maglink BLE UUIDs (fff0/fff1/fff2)
- Added connection timeout (10s) to prevent hanging on unavailable devices
- Added connection status display and manual push-button logging trigger
- Improved retry logging with attempt numbers and delays
- Added console window allocation for Windows debug output
- Added BLE disconnect on app close

Bug fixes:
- Fixed device name filtering for Maglink (ML-* prefix)
- Fixed RtkGps enum reference in DeviceScanViewModel
- Fixed DebugMessage namespace (moved to Models)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-07-06 17:32:00 -05:00
parent b602b762c9
commit 299ec3875d
19 changed files with 608 additions and 27 deletions

View File

@@ -10,12 +10,16 @@ public enum UmUtility { None = 0, Gas = 1, Power = 2, Communications = 3, Water
public enum UmBatteryType { NotMeasured = 0, Alkaline = 1, AlkalineError = 2, Lithium = 3 }
public enum UmLogType { PushButton = 0, TimeBased = 1 }
/// <summary>
/// A single push-button data-log packet from the UM locating receiver
/// (PBDL schema 1, 21 comma-delimited fields).
/// A single data-log packet from the UM locating receiver.
/// Schema 1 (push-button): 21 fields
/// Schema 2 (timed logging): 22 fields with LogType as first field
/// </summary>
public sealed class UmLogPacket
{
public UmLogType? LogType { get; init; } // Schema 2 only
public UmMode Mode { get; init; }
public int Frequency { get; init; }
public UmFreqType FreqType { get; init; }
@@ -39,7 +43,8 @@ public sealed class UmLogPacket
public bool MenuInUse { get; init; }
public string RawText { get; init; } = "";
public const int FieldCount = 21;
public const int Schema1FieldCount = 21;
public const int Schema2FieldCount = 22;
/// <summary>Depth converted to meters, when the raw string can be interpreted; otherwise null.</summary>
public double? DepthMeters => TryParseDepthMeters(DepthRaw);
@@ -90,23 +95,45 @@ public sealed class UmLogPacket
}
/// <summary>
/// Parses a 21-field PBDL schema 1 packet. Returns null if the line does not look like a data packet.
/// Parses either Schema 1 (21 fields, push-button) or Schema 2 (22 fields, timed logging).
/// Returns null if the line does not look like a data packet.
/// </summary>
public static UmLogPacket? TryParse(string line)
{
var fields = line.Trim().Split(',');
if (fields.Length < FieldCount)
return null;
// First field must be a small integer (Mode 0-6) to qualify as a data packet.
if (!int.TryParse(fields[0], out var mode) || mode is < 0 or > 6)
return null;
// Try Schema 2 (22 fields with LogType)
if (fields.Length >= Schema2FieldCount)
{
if (int.TryParse(fields[0], out var logType) && logType is 0 or 1)
{
if (int.TryParse(fields[1], out var mode) && mode is >= 0 and <= 6)
{
return ParseSchema2(fields, (UmLogType)logType, line);
}
}
}
// Try Schema 1 (21 fields without LogType)
if (fields.Length >= Schema1FieldCount)
{
if (int.TryParse(fields[0], out var mode) && mode is >= 0 and <= 6)
{
return ParseSchema1(fields, line);
}
}
return null;
}
private static UmLogPacket? ParseSchema1(string[] fields, string line)
{
try
{
return new UmLogPacket
{
Mode = (UmMode)mode,
LogType = null, // Schema 1 doesn't have LogType
Mode = (UmMode)ParseInt(fields[0]),
Frequency = ParseInt(fields[1]),
FreqType = (UmFreqType)ParseInt(fields[2]),
Signal = ParseInt(fields[3]),
@@ -136,6 +163,44 @@ public sealed class UmLogPacket
}
}
private static UmLogPacket? ParseSchema2(string[] fields, UmLogType logType, string line)
{
try
{
// Schema 2 has LogType at index 0, then same fields as Schema 1 shifted by 1
return new UmLogPacket
{
LogType = logType,
Mode = (UmMode)ParseInt(fields[1]),
Frequency = ParseInt(fields[2]),
FreqType = (UmFreqType)ParseInt(fields[3]),
Signal = ParseInt(fields[4]),
GainDb = ParseInt(fields[5]),
DepthRaw = fields[6].Trim(),
CurrentRaw = fields[7].Trim(),
CompassAngle = ParseInt(fields[8]),
GuidanceArrows = ParseInt(fields[9]),
LdPhase = ParseInt(fields[10]),
Clipping = ParseInt(fields[11]) != 0,
DepthCurrentSetting = ParseInt(fields[12]),
LrArrowStyle = ParseInt(fields[13]),
AudioVolume = ParseInt(fields[14]),
AudioModulation = ParseInt(fields[15]),
AudioSound = ParseInt(fields[16]),
BatteryVoltage = ParseDouble(fields[17]),
BatteryType = (UmBatteryType)ParseInt(fields[18]),
Backlight = ParseInt(fields[19]),
Utility = (UmUtility)ParseInt(fields[20]),
MenuInUse = ParseInt(fields[21]) != 0,
RawText = line.Trim(),
};
}
catch (FormatException)
{
return null;
}
}
private static int ParseInt(string s) => int.Parse(s.Trim(), CultureInfo.InvariantCulture);
private static double ParseDouble(string s) => double.Parse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
}