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:
@@ -2,7 +2,10 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(git init)",
|
"Bash(git init)",
|
||||||
"Bash(git add .)"
|
"Bash(git add .)",
|
||||||
|
"Bash(git commit -m \"$(cat <<''EOF''\nInitial commit: FieldLogger MAUI app with Maglink BLE support\n\nAdded Maglink RTK GNSS receiver integration with correct BLE UUIDs and device name filtering (ML-* prefix).\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
|
||||||
|
"Read(//d/maglink/**)",
|
||||||
|
"Bash(git add -A)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -1,14 +1,57 @@
|
|||||||
namespace FieldLogger;
|
using FieldLogger.Services;
|
||||||
|
|
||||||
|
namespace FieldLogger;
|
||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
public App()
|
private readonly DeviceConnectionManager _connectionManager;
|
||||||
|
|
||||||
|
public App(DeviceConnectionManager connectionManager)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
_connectionManager = connectionManager;
|
||||||
|
|
||||||
|
Console.WriteLine("===== FIELD LOGGER APP STARTING =====");
|
||||||
|
Console.WriteLine($"Console output is working! Time: {DateTime.Now:HH:mm:ss}");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Window CreateWindow(IActivationState? activationState)
|
protected override Window CreateWindow(IActivationState? activationState)
|
||||||
{
|
{
|
||||||
return new Window(new AppShell());
|
var window = new Window(new AppShell());
|
||||||
|
|
||||||
|
// Disconnect BLE devices when window is closing
|
||||||
|
window.Destroying += async (s, e) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine("App: Window closing, disconnecting devices...");
|
||||||
|
await DisconnectAllDevicesAsync();
|
||||||
|
};
|
||||||
|
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DisconnectAllDevicesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Disconnect locator
|
||||||
|
if (_connectionManager.Locator.IsConnected)
|
||||||
|
{
|
||||||
|
Console.WriteLine("App: Disconnecting locator...");
|
||||||
|
await _connectionManager.Locator.DisconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect GPS
|
||||||
|
if (_connectionManager.Gps.IsConnected)
|
||||||
|
{
|
||||||
|
Console.WriteLine("App: Disconnecting GPS...");
|
||||||
|
await _connectionManager.Gps.DisconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("App: All devices disconnected");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"App: Error disconnecting devices: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<ShellContent Title="Home" Route="home" ContentTemplate="{DataTemplate views:HomePage}" />
|
<ShellContent Title="Home" Route="home" ContentTemplate="{DataTemplate views:HomePage}" />
|
||||||
<ShellContent Title="Jobs" Route="jobs" ContentTemplate="{DataTemplate views:JobsPage}" />
|
<ShellContent Title="Jobs" Route="jobs" ContentTemplate="{DataTemplate views:JobsPage}" />
|
||||||
<ShellContent Title="Map" Route="map" ContentTemplate="{DataTemplate views:MapPage}" />
|
<ShellContent Title="Map" Route="map" ContentTemplate="{DataTemplate views:MapPage}" />
|
||||||
|
<ShellContent Title="Debug" Route="debug" ContentTemplate="{DataTemplate views:DebugPage}" />
|
||||||
<ShellContent Title="Settings" Route="settings" ContentTemplate="{DataTemplate views:SettingsPage}" />
|
<ShellContent Title="Settings" Route="settings" ContentTemplate="{DataTemplate views:SettingsPage}" />
|
||||||
</TabBar>
|
</TabBar>
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public static class MauiProgram
|
|||||||
builder.Services.AddSingleton<HomeViewModel>();
|
builder.Services.AddSingleton<HomeViewModel>();
|
||||||
builder.Services.AddSingleton<JobsViewModel>();
|
builder.Services.AddSingleton<JobsViewModel>();
|
||||||
builder.Services.AddSingleton<MapViewModel>();
|
builder.Services.AddSingleton<MapViewModel>();
|
||||||
|
builder.Services.AddSingleton<DebugViewModel>();
|
||||||
builder.Services.AddSingleton<SettingsViewModel>();
|
builder.Services.AddSingleton<SettingsViewModel>();
|
||||||
builder.Services.AddTransient<DeviceScanViewModel>();
|
builder.Services.AddTransient<DeviceScanViewModel>();
|
||||||
builder.Services.AddTransient<JobDetailViewModel>();
|
builder.Services.AddTransient<JobDetailViewModel>();
|
||||||
@@ -50,6 +51,7 @@ public static class MauiProgram
|
|||||||
builder.Services.AddSingleton<HomePage>();
|
builder.Services.AddSingleton<HomePage>();
|
||||||
builder.Services.AddSingleton<JobsPage>();
|
builder.Services.AddSingleton<JobsPage>();
|
||||||
builder.Services.AddSingleton<MapPage>();
|
builder.Services.AddSingleton<MapPage>();
|
||||||
|
builder.Services.AddSingleton<DebugPage>();
|
||||||
builder.Services.AddSingleton<SettingsPage>();
|
builder.Services.AddSingleton<SettingsPage>();
|
||||||
builder.Services.AddTransient<DeviceScanPage>();
|
builder.Services.AddTransient<DeviceScanPage>();
|
||||||
builder.Services.AddTransient<JobDetailPage>();
|
builder.Services.AddTransient<JobDetailPage>();
|
||||||
|
|||||||
24
FieldLogger/Models/DebugMessage.cs
Normal file
24
FieldLogger/Models/DebugMessage.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace FieldLogger.Models;
|
||||||
|
|
||||||
|
public class DebugMessage
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string Direction { get; set; } = "";
|
||||||
|
public string RawMessage { get; set; } = "";
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
public string? ParsedData { get; set; }
|
||||||
|
public string TypeColor { get; set; } = "Gray";
|
||||||
|
public bool HasParsedData => !string.IsNullOrEmpty(ParsedData);
|
||||||
|
|
||||||
|
public string HexBytes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(RawMessage))
|
||||||
|
return "[empty]";
|
||||||
|
|
||||||
|
var bytes = System.Text.Encoding.ASCII.GetBytes(RawMessage);
|
||||||
|
return BitConverter.ToString(bytes).Replace("-", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 UmBatteryType { NotMeasured = 0, Alkaline = 1, AlkalineError = 2, Lithium = 3 }
|
||||||
|
|
||||||
|
public enum UmLogType { PushButton = 0, TimeBased = 1 }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A single push-button data-log packet from the UM locating receiver
|
/// A single data-log packet from the UM locating receiver.
|
||||||
/// (PBDL schema 1, 21 comma-delimited fields).
|
/// Schema 1 (push-button): 21 fields
|
||||||
|
/// Schema 2 (timed logging): 22 fields with LogType as first field
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UmLogPacket
|
public sealed class UmLogPacket
|
||||||
{
|
{
|
||||||
|
public UmLogType? LogType { get; init; } // Schema 2 only
|
||||||
public UmMode Mode { get; init; }
|
public UmMode Mode { get; init; }
|
||||||
public int Frequency { get; init; }
|
public int Frequency { get; init; }
|
||||||
public UmFreqType FreqType { get; init; }
|
public UmFreqType FreqType { get; init; }
|
||||||
@@ -39,7 +43,8 @@ public sealed class UmLogPacket
|
|||||||
public bool MenuInUse { get; init; }
|
public bool MenuInUse { get; init; }
|
||||||
public string RawText { 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>
|
/// <summary>Depth converted to meters, when the raw string can be interpreted; otherwise null.</summary>
|
||||||
public double? DepthMeters => TryParseDepthMeters(DepthRaw);
|
public double? DepthMeters => TryParseDepthMeters(DepthRaw);
|
||||||
@@ -90,23 +95,45 @@ public sealed class UmLogPacket
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public static UmLogPacket? TryParse(string line)
|
public static UmLogPacket? TryParse(string line)
|
||||||
{
|
{
|
||||||
var fields = line.Trim().Split(',');
|
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.
|
// Try Schema 2 (22 fields with LogType)
|
||||||
if (!int.TryParse(fields[0], out var mode) || mode is < 0 or > 6)
|
if (fields.Length >= Schema2FieldCount)
|
||||||
return null;
|
{
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
return new UmLogPacket
|
return new UmLogPacket
|
||||||
{
|
{
|
||||||
Mode = (UmMode)mode,
|
LogType = null, // Schema 1 doesn't have LogType
|
||||||
|
Mode = (UmMode)ParseInt(fields[0]),
|
||||||
Frequency = ParseInt(fields[1]),
|
Frequency = ParseInt(fields[1]),
|
||||||
FreqType = (UmFreqType)ParseInt(fields[2]),
|
FreqType = (UmFreqType)ParseInt(fields[2]),
|
||||||
Signal = ParseInt(fields[3]),
|
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 int ParseInt(string s) => int.Parse(s.Trim(), CultureInfo.InvariantCulture);
|
||||||
private static double ParseDouble(string s) => double.Parse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
private static double ParseDouble(string s) => double.Parse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
|
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
|
||||||
<!-- Google Maps API key: replace with your key from https://console.cloud.google.com (Maps SDK for Android) -->
|
<!-- Google Maps API key: replace with your key from https://console.cloud.google.com (Maps SDK for Android) -->
|
||||||
<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_GOOGLE_MAPS_ANDROID_API_KEY" />
|
<meta-data android:name="com.google.android.geo.API_KEY" android:value="AIzaSyDhH16gF-7UN-CBsTQGfQSHNGjLC6VJ5dI" />
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
// To learn more about WinUI, the WinUI project structure,
|
// To learn more about WinUI, the WinUI project structure,
|
||||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||||
@@ -10,6 +11,10 @@ namespace FieldLogger.WinUI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : MauiWinUIApplication
|
public partial class App : MauiWinUIApplication
|
||||||
{
|
{
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
static extern bool AllocConsole();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the singleton application object. This is the first line of authored code
|
/// Initializes the singleton application object. This is the first line of authored code
|
||||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||||
@@ -17,6 +22,12 @@ public partial class App : MauiWinUIApplication
|
|||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// Allocate console window for debug output
|
||||||
|
#if DEBUG
|
||||||
|
AllocConsole();
|
||||||
|
Console.WriteLine("===== DEBUG CONSOLE ALLOCATED =====");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||||
|
|||||||
@@ -42,10 +42,14 @@ public sealed class BleSerialClient : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
await DisconnectAsync().ConfigureAwait(false);
|
await DisconnectAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Add a timeout to prevent hanging indefinitely when device is not available
|
||||||
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
var device = await _adapter.ConnectToKnownDeviceAsync(
|
var device = await _adapter.ConnectToKnownDeviceAsync(
|
||||||
deviceId,
|
deviceId,
|
||||||
new ConnectParameters(autoConnect: false, forceBleTransport: true),
|
new ConnectParameters(autoConnect: false, forceBleTransport: true),
|
||||||
cancellationToken).ConfigureAwait(false);
|
timeoutCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -53,7 +57,7 @@ public sealed class BleSerialClient : IAsyncDisposable
|
|||||||
try { await device.RequestMtuAsync(247).ConfigureAwait(false); }
|
try { await device.RequestMtuAsync(247).ConfigureAwait(false); }
|
||||||
catch { /* not supported on this platform */ }
|
catch { /* not supported on this platform */ }
|
||||||
|
|
||||||
var service = await device.GetServiceAsync(serviceUuid, cancellationToken).ConfigureAwait(false)
|
var service = await device.GetServiceAsync(serviceUuid, timeoutCts.Token).ConfigureAwait(false)
|
||||||
?? throw new InvalidOperationException($"Service {serviceUuid} not found on {device.Name}.");
|
?? throw new InvalidOperationException($"Service {serviceUuid} not found on {device.Name}.");
|
||||||
|
|
||||||
var notifyChar = await service.GetCharacteristicAsync(notifyUuid).ConfigureAwait(false)
|
var notifyChar = await service.GetCharacteristicAsync(notifyUuid).ConfigureAwait(false)
|
||||||
@@ -66,12 +70,16 @@ public sealed class BleSerialClient : IAsyncDisposable
|
|||||||
? CharacteristicWriteType.WithoutResponse
|
? CharacteristicWriteType.WithoutResponse
|
||||||
: CharacteristicWriteType.WithResponse;
|
: CharacteristicWriteType.WithResponse;
|
||||||
|
|
||||||
|
Console.WriteLine($"BLE: Subscribing to notifications on {notifyUuid}");
|
||||||
notifyChar.ValueUpdated += OnValueUpdated;
|
notifyChar.ValueUpdated += OnValueUpdated;
|
||||||
await notifyChar.StartUpdatesAsync(cancellationToken).ConfigureAwait(false);
|
await notifyChar.StartUpdatesAsync(timeoutCts.Token).ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"BLE: Successfully subscribed to notifications");
|
||||||
|
|
||||||
_device = device;
|
_device = device;
|
||||||
_notifyChar = notifyChar;
|
_notifyChar = notifyChar;
|
||||||
_writeChar = writeChar;
|
_writeChar = writeChar;
|
||||||
|
|
||||||
|
Console.WriteLine($"BLE: Connection complete. Device={device.Name}, CanWrite={writeChar.CanWrite}, CanNotify={notifyChar.Properties.HasFlag(CharacteristicPropertyType.Notify)}");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -87,7 +95,10 @@ public sealed class BleSerialClient : IAsyncDisposable
|
|||||||
public async Task WriteAsync(string text, CancellationToken cancellationToken = default)
|
public async Task WriteAsync(string text, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var writeChar = _writeChar ?? throw new InvalidOperationException("Not connected.");
|
var writeChar = _writeChar ?? throw new InvalidOperationException("Not connected.");
|
||||||
await writeChar.WriteAsync(Encoding.ASCII.GetBytes(text), cancellationToken).ConfigureAwait(false);
|
var bytes = Encoding.ASCII.GetBytes(text);
|
||||||
|
Console.WriteLine($"BLE TX: {text.Replace("\r", "\\r").Replace("\n", "\\n")} ({bytes.Length} bytes)");
|
||||||
|
await writeChar.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
|
||||||
|
Console.WriteLine("BLE TX: Write completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DisconnectAsync()
|
public async Task DisconnectAsync()
|
||||||
@@ -117,13 +128,18 @@ public sealed class BleSerialClient : IAsyncDisposable
|
|||||||
private void OnValueUpdated(object? sender, CharacteristicUpdatedEventArgs e)
|
private void OnValueUpdated(object? sender, CharacteristicUpdatedEventArgs e)
|
||||||
{
|
{
|
||||||
var bytes = e.Characteristic.Value;
|
var bytes = e.Characteristic.Value;
|
||||||
|
Console.WriteLine($"BLE RX: {bytes?.Length ?? 0} bytes");
|
||||||
|
|
||||||
if (bytes is null || bytes.Length == 0)
|
if (bytes is null || bytes.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
List<string> lines = new();
|
List<string> lines = new();
|
||||||
lock (_rxBuffer)
|
lock (_rxBuffer)
|
||||||
{
|
{
|
||||||
_rxBuffer.Append(Encoding.ASCII.GetString(bytes));
|
var text = Encoding.ASCII.GetString(bytes);
|
||||||
|
Console.WriteLine($"BLE RX decoded: {text.Replace("\r", "\\r").Replace("\n", "\\n")}");
|
||||||
|
|
||||||
|
_rxBuffer.Append(text);
|
||||||
var buffered = _rxBuffer.ToString();
|
var buffered = _rxBuffer.ToString();
|
||||||
int newline;
|
int newline;
|
||||||
while ((newline = buffered.IndexOf('\n')) >= 0)
|
while ((newline = buffered.IndexOf('\n')) >= 0)
|
||||||
@@ -133,13 +149,26 @@ public sealed class BleSerialClient : IAsyncDisposable
|
|||||||
if (line.Length > 0)
|
if (line.Length > 0)
|
||||||
lines.Add(line);
|
lines.Add(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no newlines found but we have data, treat each notification as a complete message
|
||||||
|
// This handles devices that send complete messages per notification without line endings
|
||||||
|
if (lines.Count == 0 && buffered.Length > 0)
|
||||||
|
{
|
||||||
|
lines.Add(buffered.Trim());
|
||||||
|
buffered = "";
|
||||||
|
}
|
||||||
|
|
||||||
_rxBuffer.Clear();
|
_rxBuffer.Clear();
|
||||||
_rxBuffer.Append(buffered);
|
_rxBuffer.Append(buffered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"BLE RX: {lines.Count} complete lines");
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"BLE RX line: {line}");
|
||||||
LineReceived?.Invoke(this, line);
|
LineReceived?.Invoke(this, line);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDeviceDisconnected(object? sender, DeviceEventArgs e) => HandleDisconnect(e.Device);
|
private void OnDeviceDisconnected(object? sender, DeviceEventArgs e) => HandleDisconnect(e.Device);
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ public sealed partial class DeviceConnectionManager : ObservableObject
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("{Kind} connection attempt {Attempt}", kind, attempt + 1);
|
||||||
await BleScanner.EnsurePermissionsAsync();
|
await BleScanner.EnsurePermissionsAsync();
|
||||||
if (kind == DeviceKind.Locator)
|
if (kind == DeviceKind.Locator)
|
||||||
await _locator.ConnectAsync(id.Value, cts.Token);
|
await _locator.ConnectAsync(id.Value, cts.Token);
|
||||||
@@ -133,19 +134,25 @@ public sealed partial class DeviceConnectionManager : ObservableObject
|
|||||||
await _gps.ConnectAsync(id.Value, cts.Token);
|
await _gps.ConnectAsync(id.Value, cts.Token);
|
||||||
|
|
||||||
SetState(kind, ConnectionState.Connected);
|
SetState(kind, ConnectionState.Connected);
|
||||||
_logger.LogInformation("{Kind} connected", kind);
|
_logger.LogInformation("{Kind} connected successfully", kind);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("{Kind} connection cancelled", kind);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "{Kind} connect attempt {Attempt} failed", kind, attempt + 1);
|
_logger.LogWarning(ex, "{Kind} connect attempt {Attempt} failed: {Message}", kind, attempt + 1, ex.Message);
|
||||||
var delay = RetryDelays[Math.Min(attempt, RetryDelays.Length - 1)];
|
var delay = RetryDelays[Math.Min(attempt, RetryDelays.Length - 1)];
|
||||||
|
_logger.LogInformation("{Kind} retrying in {Delay}s...", kind, delay.TotalSeconds);
|
||||||
try { await Task.Delay(delay, cts.Token); }
|
try { await Task.Delay(delay, cts.Token); }
|
||||||
catch (OperationCanceledException) { return; }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Kind} retry cancelled", kind);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public sealed class MaglinkService : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
public static readonly Guid ServiceUuid = Guid.Parse("0000fff0-0000-1000-8000-00805f9b34fb");
|
public static readonly Guid ServiceUuid = Guid.Parse("0000fff0-0000-1000-8000-00805f9b34fb");
|
||||||
public static readonly Guid NotifyUuid = Guid.Parse("0000fff2-0000-1000-8000-00805f9b34fb");
|
public static readonly Guid NotifyUuid = Guid.Parse("0000fff2-0000-1000-8000-00805f9b34fb");
|
||||||
public static readonly Guid WriteUuid = Guid.Parse("0000fff2-0000-1000-8000-00805f9b34fb");
|
public static readonly Guid WriteUuid = Guid.Parse("0000fff1-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
private readonly BleSerialClient _client = new();
|
private readonly BleSerialClient _client = new();
|
||||||
private readonly ILogger<MaglinkService> _logger;
|
private readonly ILogger<MaglinkService> _logger;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace FieldLogger.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UmReceiverService : IAsyncDisposable
|
public sealed class UmReceiverService : IAsyncDisposable
|
||||||
{
|
{
|
||||||
// UM Receiver BLE External Logging API v1.2:
|
// UM Receiver BLE External Logging API v1.3:
|
||||||
// serial port service with notify (RX) and write-no-response (TX) characteristics.
|
// serial port service with notify (RX) and write-no-response (TX) characteristics.
|
||||||
public static readonly Guid ServiceUuid = Guid.Parse("554d0000-261f-677e-a6f1-54c57aa996d4");
|
public static readonly Guid ServiceUuid = Guid.Parse("554d0000-261f-677e-a6f1-54c57aa996d4");
|
||||||
public static readonly Guid NotifyUuid = Guid.Parse("554d0001-261f-677e-a6f1-54c57aa996d4");
|
public static readonly Guid NotifyUuid = Guid.Parse("554d0001-261f-677e-a6f1-54c57aa996d4");
|
||||||
@@ -23,11 +23,14 @@ public sealed class UmReceiverService : IAsyncDisposable
|
|||||||
public bool IsConnected => _client.IsConnected;
|
public bool IsConnected => _client.IsConnected;
|
||||||
public string? DeviceName => _client.DeviceName;
|
public string? DeviceName => _client.DeviceName;
|
||||||
|
|
||||||
/// <summary>Raised when the operator presses the log button on the receiver.</summary>
|
/// <summary>Raised when the operator presses the log button on the receiver or timed logging triggers.</summary>
|
||||||
public event EventHandler<UmLogPacket>? PacketReceived;
|
public event EventHandler<UmLogPacket>? PacketReceived;
|
||||||
public event EventHandler<UmDeviceInfo>? DeviceInfoReceived;
|
public event EventHandler<UmDeviceInfo>? DeviceInfoReceived;
|
||||||
public event EventHandler? Disconnected;
|
public event EventHandler? Disconnected;
|
||||||
|
|
||||||
|
/// <summary>Raised for all received lines (for debugging).</summary>
|
||||||
|
public event EventHandler<string>? LineReceived;
|
||||||
|
|
||||||
public UmReceiverService(ILogger<UmReceiverService> logger)
|
public UmReceiverService(ILogger<UmReceiverService> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -54,12 +57,31 @@ public sealed class UmReceiverService : IAsyncDisposable
|
|||||||
public Task DisableLoggingAsync(CancellationToken cancellationToken = default)
|
public Task DisableLoggingAsync(CancellationToken cancellationToken = default)
|
||||||
=> _client.WriteLineAsync("$UMPBDL,0", cancellationToken);
|
=> _client.WriteLineAsync("$UMPBDL,0", cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables timed data logging at the specified period.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logPeriod">Log period multiplier (period = logPeriod * 100ms). Valid: 10-100.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
public Task EnableTimedLoggingAsync(int logPeriod, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (logPeriod < 10 || logPeriod > 100)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(logPeriod), "Log period must be between 10 and 100 (1s to 10s)");
|
||||||
|
return _client.WriteLineAsync($"$UMETDL,{logPeriod}", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Disables timed data logging.</summary>
|
||||||
|
public Task DisableTimedLoggingAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> _client.WriteLineAsync("$UMETDL,0", cancellationToken);
|
||||||
|
|
||||||
public Task DisconnectAsync() => _client.DisconnectAsync();
|
public Task DisconnectAsync() => _client.DisconnectAsync();
|
||||||
|
|
||||||
private void OnLineReceived(object? sender, string line)
|
private void OnLineReceived(object? sender, string line)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("UM rx: {Line}", line);
|
_logger.LogDebug("UM rx: {Line}", line);
|
||||||
|
|
||||||
|
// Notify debug listeners of all lines
|
||||||
|
LineReceived?.Invoke(this, line);
|
||||||
|
|
||||||
// Bare command acknowledgements.
|
// Bare command acknowledgements.
|
||||||
var trimmed = line.Trim();
|
var trimmed = line.Trim();
|
||||||
if (trimmed.Equals("OK", StringComparison.OrdinalIgnoreCase) ||
|
if (trimmed.Equals("OK", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
|||||||
254
FieldLogger/ViewModels/DebugViewModel.cs
Normal file
254
FieldLogger/ViewModels/DebugViewModel.cs
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using FieldLogger.Models;
|
||||||
|
using FieldLogger.Services;
|
||||||
|
|
||||||
|
namespace FieldLogger.ViewModels;
|
||||||
|
|
||||||
|
public sealed partial class DebugViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly UmReceiverService _receiver;
|
||||||
|
private readonly DeviceConnectionManager _manager;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isTimedLoggingEnabled;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private int _logPeriod = 10; // 10 = 1000ms (10 * 100ms)
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _timedLoggingStatus = "Disabled";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _autoScroll = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _showHex = false;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private int _messageCount;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isConnected;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _connectionStatus = "Not connected";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _connectionStateColor = "Gray";
|
||||||
|
|
||||||
|
public ObservableCollection<DebugMessage> Messages { get; } = new();
|
||||||
|
|
||||||
|
public string LogPeriodLabel => $"Period: {LogPeriod * 100}ms ({(LogPeriod * 100 / 1000.0):F1}s)";
|
||||||
|
|
||||||
|
public DebugViewModel(UmReceiverService receiver, DeviceConnectionManager manager)
|
||||||
|
{
|
||||||
|
_receiver = receiver;
|
||||||
|
_manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAppearing()
|
||||||
|
{
|
||||||
|
_receiver.LineReceived += OnLineReceived;
|
||||||
|
UpdateConnectionStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDisappearing()
|
||||||
|
{
|
||||||
|
_receiver.LineReceived -= OnLineReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateConnectionStatus()
|
||||||
|
{
|
||||||
|
IsConnected = _receiver.IsConnected;
|
||||||
|
if (IsConnected)
|
||||||
|
{
|
||||||
|
ConnectionStatus = $"Connected to {_receiver.DeviceName ?? "receiver"}";
|
||||||
|
ConnectionStateColor = "Green";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConnectionStatus = "Not connected";
|
||||||
|
ConnectionStateColor = "Gray";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task EnablePushButtonLoggingAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _receiver.EnableLoggingAsync();
|
||||||
|
AddMessage("TX", "$UMPBDL,1", "Command");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Shell.Current.DisplayAlert("Error", $"Failed to enable logging: {ex.Message}", "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsTimedLoggingEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
_ = UpdateTimedLoggingAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnLogPeriodChanged(int value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(LogPeriodLabel));
|
||||||
|
if (IsTimedLoggingEnabled)
|
||||||
|
{
|
||||||
|
_ = UpdateTimedLoggingAsync(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateTimedLoggingAsync(bool enabled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_receiver.IsConnected)
|
||||||
|
{
|
||||||
|
TimedLoggingStatus = "Not connected to receiver";
|
||||||
|
IsTimedLoggingEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
await _receiver.EnableTimedLoggingAsync(LogPeriod);
|
||||||
|
TimedLoggingStatus = $"Enabled - {LogPeriod * 100}ms interval";
|
||||||
|
AddMessage("TX", $"$UMETDL,{LogPeriod}", "Command");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _receiver.DisableTimedLoggingAsync();
|
||||||
|
TimedLoggingStatus = "Disabled";
|
||||||
|
AddMessage("TX", "$UMETDL,0", "Command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TimedLoggingStatus = $"Error: {ex.Message}";
|
||||||
|
IsTimedLoggingEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLineReceived(object? sender, string line)
|
||||||
|
{
|
||||||
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
var messageType = ClassifyMessage(line);
|
||||||
|
var parsedData = ParseMessage(line, messageType);
|
||||||
|
AddMessage("RX", line, messageType, parsedData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMessage(string direction, string rawMessage, string type, string? parsedData = null)
|
||||||
|
{
|
||||||
|
var message = new DebugMessage
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.Now,
|
||||||
|
Direction = direction,
|
||||||
|
RawMessage = rawMessage,
|
||||||
|
Type = type,
|
||||||
|
ParsedData = parsedData,
|
||||||
|
TypeColor = GetTypeColor(type)
|
||||||
|
};
|
||||||
|
|
||||||
|
Messages.Add(message);
|
||||||
|
MessageCount = Messages.Count;
|
||||||
|
|
||||||
|
// Keep last 500 messages
|
||||||
|
while (Messages.Count > 500)
|
||||||
|
{
|
||||||
|
Messages.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Auto-scroll if enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ClassifyMessage(string line)
|
||||||
|
{
|
||||||
|
line = line.Trim();
|
||||||
|
|
||||||
|
// Try parsing as UmLogPacket (data packet)
|
||||||
|
var packet = UmLogPacket.TryParse(line);
|
||||||
|
if (packet is not null)
|
||||||
|
{
|
||||||
|
return packet.LogType.HasValue ? "UMLOG (Timed)" : "UMLOG (Button)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as device info
|
||||||
|
var info = UmDeviceInfo.TryParse(line);
|
||||||
|
if (info is not null)
|
||||||
|
return "UMDEV";
|
||||||
|
|
||||||
|
if (line.Equals("OK", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "OK";
|
||||||
|
if (line.Equals("ERROR", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "ERROR";
|
||||||
|
if (line.StartsWith("$", StringComparison.Ordinal))
|
||||||
|
return "Unknown";
|
||||||
|
return "Text";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ParseMessage(string line, string type)
|
||||||
|
{
|
||||||
|
if (type.StartsWith("UMLOG"))
|
||||||
|
{
|
||||||
|
var packet = UmLogPacket.TryParse(line);
|
||||||
|
if (packet is not null)
|
||||||
|
{
|
||||||
|
var logTypeStr = packet.LogType.HasValue ? $"[{packet.LogType}] " : "";
|
||||||
|
return $"{logTypeStr}{packet.Mode}, {packet.Frequency}Hz, Signal={packet.Signal}, " +
|
||||||
|
$"Depth={packet.DepthRaw}, Current={packet.CurrentRaw}, Battery={packet.BatteryVoltage:F2}V";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == "UMDEV")
|
||||||
|
{
|
||||||
|
var info = UmDeviceInfo.TryParse(line);
|
||||||
|
if (info is not null)
|
||||||
|
{
|
||||||
|
return $"Mfr={info.Manufacturer}, Model={info.ModelName}, SN={info.SerialNumber}, FW={info.SoftwareVersion}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetModeText(string mode)
|
||||||
|
{
|
||||||
|
return mode switch
|
||||||
|
{
|
||||||
|
"0" => "Single",
|
||||||
|
"1" => "Twin",
|
||||||
|
"2" => "Null",
|
||||||
|
"3" => "Sweep",
|
||||||
|
"4" => "Twin Sweep",
|
||||||
|
"5" => "Omni",
|
||||||
|
"6" => "Twin Omni",
|
||||||
|
_ => $"Mode {mode}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTypeColor(string type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
"OK" => "Green",
|
||||||
|
"ERROR" => "Red",
|
||||||
|
"UMLOG (Button)" => "Blue",
|
||||||
|
"UMLOG (Timed)" => "DodgerBlue",
|
||||||
|
"UMDEV" => "Purple",
|
||||||
|
"Command" => "Orange",
|
||||||
|
_ when type.StartsWith("UMLOG") => "Blue",
|
||||||
|
_ => "Gray"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ClearMessages()
|
||||||
|
{
|
||||||
|
Messages.Clear();
|
||||||
|
MessageCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ public sealed partial class DeviceScanViewModel : ObservableObject
|
|||||||
// Filter by device name prefix based on device type.
|
// Filter by device name prefix based on device type.
|
||||||
if (Kind == DeviceKind.Locator && !UmReceiverService.IsUmReceiverName(device.Name))
|
if (Kind == DeviceKind.Locator && !UmReceiverService.IsUmReceiverName(device.Name))
|
||||||
return;
|
return;
|
||||||
if (Kind == DeviceKind.Maglink && !MaglinkService.IsMaglinkName(device.Name))
|
if (Kind == DeviceKind.RtkGps && !MaglinkService.IsMaglinkName(device.Name))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
MainThread.BeginInvokeOnMainThread(() =>
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
|||||||
91
FieldLogger/Views/DebugPage.xaml
Normal file
91
FieldLogger/Views/DebugPage.xaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
|
xmlns:models="clr-namespace:FieldLogger.Models"
|
||||||
|
x:Class="FieldLogger.Views.DebugPage"
|
||||||
|
x:DataType="vm:DebugViewModel"
|
||||||
|
Title="Debug">
|
||||||
|
<ScrollView>
|
||||||
|
<VerticalStackLayout Padding="16" Spacing="12">
|
||||||
|
|
||||||
|
<Label Text="Connection Status" FontAttributes="Bold" FontSize="16" />
|
||||||
|
|
||||||
|
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||||
|
<VerticalStackLayout Spacing="10">
|
||||||
|
<Grid ColumnDefinitions="16,*,Auto" ColumnSpacing="8">
|
||||||
|
<Ellipse Grid.Column="0" WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
||||||
|
Fill="{Binding ConnectionStateColor}" />
|
||||||
|
<Label Grid.Column="1" Text="{Binding ConnectionStatus}" VerticalOptions="Center" />
|
||||||
|
<Button Grid.Column="2" Text="Enable Push-Button" FontSize="12" Padding="10,4"
|
||||||
|
Command="{Binding EnablePushButtonLoggingCommand}"
|
||||||
|
IsEnabled="{Binding IsConnected}" />
|
||||||
|
</Grid>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Label Text="Timed Data Logging" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
|
||||||
|
|
||||||
|
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||||
|
<VerticalStackLayout Spacing="10">
|
||||||
|
<Label Text="Enable continuous data streaming from UM Receiver" FontSize="12" TextColor="Gray" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
|
||||||
|
<Switch Grid.Column="0" IsToggled="{Binding IsTimedLoggingEnabled}" IsEnabled="{Binding IsConnected}" />
|
||||||
|
<Label Grid.Column="1" Text="{Binding TimedLoggingStatus}" VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8" IsVisible="{Binding IsTimedLoggingEnabled}">
|
||||||
|
<Label Grid.Column="0" Text="{Binding LogPeriodLabel}" VerticalOptions="Center" FontSize="14" />
|
||||||
|
<Stepper Grid.Column="1" Minimum="10" Maximum="100" Increment="10" Value="{Binding LogPeriod}" />
|
||||||
|
</Grid>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Label Text="Message Log" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
|
||||||
|
|
||||||
|
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||||
|
<VerticalStackLayout Spacing="10">
|
||||||
|
<Grid ColumnDefinitions="*,Auto,Auto,Auto" ColumnSpacing="8">
|
||||||
|
<Label Grid.Column="0" Text="{Binding MessageCount, StringFormat='{0} messages'}" VerticalOptions="Center" FontSize="12" TextColor="Gray" />
|
||||||
|
<Label Grid.Column="1" Text="Show Hex" VerticalOptions="Center" FontSize="12" TextColor="Gray" />
|
||||||
|
<Switch Grid.Column="2" IsToggled="{Binding ShowHex}" />
|
||||||
|
<Button Grid.Column="3" Text="Clear" FontSize="12" Padding="10,4" Command="{Binding ClearMessagesCommand}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<BoxView HeightRequest="1" Color="{AppThemeBinding Light=#E0E0E0, Dark=#333}" />
|
||||||
|
|
||||||
|
<CollectionView ItemsSource="{Binding Messages}" HeightRequest="400">
|
||||||
|
<CollectionView.EmptyView>
|
||||||
|
<Label Text="No messages yet. Messages will appear here when received from the device."
|
||||||
|
TextColor="Gray" FontSize="12" HorizontalOptions="Center" VerticalOptions="Center" />
|
||||||
|
</CollectionView.EmptyView>
|
||||||
|
<CollectionView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:DebugMessage">
|
||||||
|
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#FFFFFF, Dark=#2C2C2E}"
|
||||||
|
StrokeShape="RoundRectangle 8" Padding="8" Margin="0,2">
|
||||||
|
<VerticalStackLayout Spacing="4">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="8">
|
||||||
|
<Label Grid.Column="0" Text="{Binding Timestamp, StringFormat='{0:HH:mm:ss.fff}'}"
|
||||||
|
FontSize="10" TextColor="Gray" FontFamily="Courier" />
|
||||||
|
<Label Grid.Column="1" Text="{Binding Type}"
|
||||||
|
FontSize="10" FontAttributes="Bold" TextColor="{Binding TypeColor}" />
|
||||||
|
<Label Grid.Column="2" Text="{Binding Direction}"
|
||||||
|
FontSize="10" TextColor="Gray" />
|
||||||
|
</Grid>
|
||||||
|
<Label Text="{Binding RawMessage}" FontSize="11" FontFamily="Courier" />
|
||||||
|
<Label Text="{Binding HexBytes}" FontSize="9" TextColor="DarkGray" FontFamily="Courier"
|
||||||
|
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type vm:DebugViewModel}}, Path=ShowHex}" />
|
||||||
|
<Label Text="{Binding ParsedData}" FontSize="10" TextColor="Gray"
|
||||||
|
IsVisible="{Binding HasParsedData}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</CollectionView.ItemTemplate>
|
||||||
|
</CollectionView>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</ContentPage>
|
||||||
27
FieldLogger/Views/DebugPage.xaml.cs
Normal file
27
FieldLogger/Views/DebugPage.xaml.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using FieldLogger.ViewModels;
|
||||||
|
|
||||||
|
namespace FieldLogger.Views;
|
||||||
|
|
||||||
|
public partial class DebugPage : ContentPage
|
||||||
|
{
|
||||||
|
private readonly DebugViewModel _viewModel;
|
||||||
|
|
||||||
|
public DebugPage(DebugViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_viewModel = viewModel;
|
||||||
|
BindingContext = viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
_viewModel.OnAppearing();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
base.OnDisappearing();
|
||||||
|
_viewModel.OnDisappearing();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ Cross-platform .NET MAUI app (Windows, macOS, iOS, Android) for logging utility
|
|||||||
points from an **Underground Magnetics locating receiver** paired with RTK GPS positions
|
points from an **Underground Magnetics locating receiver** paired with RTK GPS positions
|
||||||
from a **Maglink (H11) RTK receiver**, both over BLE.
|
from a **Maglink (H11) RTK receiver**, both over BLE.
|
||||||
|
|
||||||
|
Maps API Key: AIzaSyDhH16gF-7UN-CBsTQGfQSHNGjLC6VJ5dI
|
||||||
|
|
||||||
## Solution layout
|
## Solution layout
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
BIN
doc/UM Receiver BLE External Logging API v1.3 DRAFT.pdf
Normal file
BIN
doc/UM Receiver BLE External Logging API v1.3 DRAFT.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user