Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
69dec5d
Add 6 wizard steps, wire WizardCommand to launch dialog
Xyntexx Apr 16, 2026
47d6561
Add live hardware interaction to wizard steps
Xyntexx Apr 16, 2026
62bac9d
Add 17 e2e tests for full wizard flow
Xyntexx Apr 16, 2026
c9d2ac4
Register new wizard steps in WizardHost, fix theme colors
Xyntexx Apr 16, 2026
fdd97a2
Fix all wizard step views for light/dark mode theme support
Xyntexx Apr 16, 2026
bcfbdba
Add VehicleType step, screenshot tests for all 19 wizard steps
Xyntexx Apr 16, 2026
6a9a93c
Add wizard screenshots to integration test catalog
Xyntexx Apr 17, 2026
1e5cd39
Use vehiclePage icons for vehicle type, not antenna icons
Xyntexx Apr 17, 2026
6bf2b74
Refactor wizard: 12 focused steps with custom views
Xyntexx Apr 17, 2026
00646d5
Full screen wizard, move WAS invert to WAS step, add hardware
Xyntexx Apr 17, 2026
0d24a1c
Major wizard UI improvements
Xyntexx Apr 17, 2026
cf81fe9
Legacy icons, visual indicators, refined wizard UI
Xyntexx Apr 18, 2026
baf42b7
Custom wheelbase images, side-by-side layouts for steps 4-5
Xyntexx Apr 18, 2026
60f08f0
Split antenna step into two columns with cropped images
Xyntexx Apr 18, 2026
6ccd223
Vehicle dimensions two-column layout with separate images
Xyntexx Apr 18, 2026
9e68f31
Fix wheelbase crop: 320x320 from (256,225) on RadiusWheelBase
Xyntexx Apr 18, 2026
e4e96eb
Fix tests for 12-step wizard (was 10)
Xyntexx Apr 18, 2026
2a2b034
Add ShouldSkip to WizardStepViewModel for conditional step navigation
Xyntexx Apr 18, 2026
4946a6e
Wire HardwareInstalled to skip autosteer steps for GPS Only
Xyntexx Apr 18, 2026
41b2b80
Add global status bar to wizard showing WAS, Roll, GPS, Speed, PWM
Xyntexx Apr 18, 2026
14988ff
Add Motor Direction Test step to AutoSteer wizard
Xyntexx Apr 18, 2026
3681989
Add CPD Circle Test step to AutoSteer wizard
Xyntexx Apr 18, 2026
c8b904a
Add Ackermann Calibration step to AutoSteer wizard
Xyntexx Apr 18, 2026
9f45078
UI polish: bigger text, fix contrast, improve WAS/Roll guides, pulse …
Xyntexx Apr 18, 2026
7017b0f
Add Ackermann, SideHill compensation, and safety settings
Xyntexx Apr 18, 2026
82db992
Final screenshots and test verification for 15-step wizard
Xyntexx Apr 18, 2026
632753e
Fix Vehicle Type/Hardware icons size, selected contrast, steer enable…
Xyntexx Apr 18, 2026
0e0648f
Fix selected description contrast, Cytron/Single as config defaults
Xyntexx Apr 18, 2026
4d07b35
Refine WAS step, zero buttons, and relay label
Xyntexx Apr 18, 2026
ada27db
Lighter accent color in light mode, fix contrast and cleanup
Xyntexx Apr 18, 2026
5a56b7f
Replace Motor Direction + PWM steps with Auto Motor Calibration
Xyntexx Apr 18, 2026
4ec0e44
Replace motor/PWM steps with automated motor calibration
Xyntexx Apr 18, 2026
322cc0e
Add Vehicle Simulator application
Xyntexx Apr 18, 2026
608df23
Add bicycle physics model to Vehicle Simulator
Xyntexx Apr 18, 2026
b7a04a1
Add auto-discovery UDP broadcasting for module detection
Xyntexx Apr 18, 2026
011ae81
Fix GPS Timeout: zero-copy path now updates GpsService
Xyntexx Apr 19, 2026
120a5e9
Auto-create local plane from first GPS fix without a field
Xyntexx Apr 19, 2026
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
17 changes: 17 additions & 0 deletions AgValoniaGPS.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgValoniaGPS.UI.Tests", "Te
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgValoniaGPS.ViewModels.Tests", "Tests\AgValoniaGPS.ViewModels.Tests\AgValoniaGPS.ViewModels.Tests.csproj", "{6FDD0F8C-981C-4F77-BCFE-0CD17ED91D7A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Simulators", "Simulators", "{F41F6DDB-AB3C-76ED-EA37-439EFA4D513D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgValoniaGPS.VehicleSimulator", "Simulators\AgValoniaGPS.VehicleSimulator\AgValoniaGPS.VehicleSimulator.csproj", "{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -173,6 +177,18 @@ Global
{6FDD0F8C-981C-4F77-BCFE-0CD17ED91D7A}.Release|x64.Build.0 = Release|Any CPU
{6FDD0F8C-981C-4F77-BCFE-0CD17ED91D7A}.Release|x86.ActiveCfg = Release|Any CPU
{6FDD0F8C-981C-4F77-BCFE-0CD17ED91D7A}.Release|x86.Build.0 = Release|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Debug|x64.ActiveCfg = Debug|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Debug|x64.Build.0 = Debug|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Debug|x86.ActiveCfg = Debug|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Debug|x86.Build.0 = Debug|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Release|Any CPU.Build.0 = Release|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Release|x64.ActiveCfg = Release|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Release|x64.Build.0 = Release|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Release|x86.ActiveCfg = Release|Any CPU
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -184,5 +200,6 @@ Global
{36B31CF8-F762-4767-8A87-32AB5BC72E26} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{84DCD742-8E48-4579-90C1-4FF9D1820FDA} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{6FDD0F8C-981C-4F77-BCFE-0CD17ED91D7A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{6EFCAF65-34FF-4AA8-B9E5-8CCB3DC67B2A} = {F41F6DDB-AB3C-76ED-EA37-439EFA4D513D}
EndGlobalSection
EndGlobal
8 changes: 7 additions & 1 deletion Platforms/AgValoniaGPS.Desktop/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</Application.DataTemplates>

<Application.Styles>
<FluentTheme />
<FluentTheme>
<FluentTheme.Palettes>
<!-- Lighter accent for better contrast on colored buttons in light mode -->
<ColorPaletteResources x:Key="Light" Accent="#2B88D8"/>
<ColorPaletteResources x:Key="Dark" Accent="#0078D7"/>
</FluentTheme.Palettes>
</FluentTheme>
<!-- Include shared styles -->
<StyleInclude Source="avares://AgValoniaGPS.Views/Styles/SharedStyles.axaml"/>
</Application.Styles>
Expand Down
4 changes: 2 additions & 2 deletions Shared/AgValoniaGPS.Models/Configuration/AutoSteerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ public bool InvertRelays
set => SetProperty(ref _invertRelays, value);
}

private int _motorDriver = 0;
private int _motorDriver = 1;
/// <summary>
/// Motor driver type: 0 = IBT2, 1 = Cytron
/// </summary>
Expand All @@ -339,7 +339,7 @@ public int MotorDriver
set => SetProperty(ref _motorDriver, value);
}

private int _adConverter = 0;
private int _adConverter = 1;
/// <summary>
/// A/D converter type: 0 = Differential, 1 = Single
/// </summary>
Expand Down
17 changes: 16 additions & 1 deletion Shared/AgValoniaGPS.Services/AutoSteer/AutoSteerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ public void Stop()
/// <summary>Sensor reading as percentage (0-100).</summary>
public double SensorPercent => _sensorPercent;

/// <inheritdoc/>
public VehicleStateSnapshot? LatestSnapshot => _latestSnapshot;
private VehicleStateSnapshot? _latestSnapshot;

/// <summary>
/// Handle incoming UDP data from steering module.
/// </summary>
Expand Down Expand Up @@ -307,6 +311,15 @@ public void ProcessGpsBuffer(byte[] buffer, int length)
return;
}

// Auto-create a temporary local plane from first GPS fix
// so the tractor moves on screen without opening a field
if (_localPlane == null && _state.FixQuality > 0)
{
_localPlane = new LocalPlane(
new Wgs84(_state.Latitude, _state.Longitude),
new SharedFieldProperties());
}

// Convert to local coordinates if we have a plane
if (_localPlane != null)
{
Expand Down Expand Up @@ -456,7 +469,9 @@ private void RecordLatency(double latencyMs)

private void NotifyStateUpdated()
{
StateUpdated?.Invoke(this, CreateSnapshot());
var snapshot = CreateSnapshot();
_latestSnapshot = snapshot;
StateUpdated?.Invoke(this, snapshot);
}

private VehicleStateSnapshot CreateSnapshot()
Expand Down
6 changes: 6 additions & 0 deletions Shared/AgValoniaGPS.Services/GpsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ public void UpdateImuData()
/// <summary>
/// Check if GPS data is flowing (10Hz expected)
/// </summary>
public void MarkGpsReceived()
{
_lastGpsDataReceived = Clock.Current.Now;
IsConnected = true;
}

public bool IsGpsDataOk()
{
bool ok = (Clock.Current.Now - _lastGpsDataReceived).TotalMilliseconds < GPS_TIMEOUT_MS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ void ProcessSimulatedPosition(double latitude, double longitude, double altitude
/// Derived from LastSensorData.SensorValue.
/// </summary>
double SensorPercent { get; }

/// <summary>
/// Latest vehicle state snapshot for UI/wizard consumption.
/// Null if no state has been produced yet.
/// </summary>
VehicleStateSnapshot? LatestSnapshot { get; }
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions Shared/AgValoniaGPS.Services/Interfaces/IGpsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public interface IGpsService
/// </summary>
void UpdateGpsData(GpsData newData);

/// <summary>
/// Mark that GPS data was received (updates timeout tracking).
/// Called by the zero-copy pipeline path which bypasses UpdateGpsData.
/// </summary>
void MarkGpsReceived();

/// <summary>
/// Update IMU data timestamp (called when IMU data received)
/// </summary>
Expand Down
129 changes: 113 additions & 16 deletions Shared/AgValoniaGPS.Services/UdpCommunicationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -48,8 +50,14 @@ public class UdpCommunicationService : IUdpCommunicationService, IDisposable
// AutoSteer service for zero-copy GPS processing
private IAutoSteerService? _autoSteerService;

// Module broadcast endpoint (e.g., 192.168.5.255:8888)
private IPEndPoint? _moduleEndpoint;
// Auto-discovery: broadcast on all interfaces until a module responds
private List<IPEndPoint> _discoveryEndpoints = new();
private IPEndPoint? _lockedEndpoint;
private DateTime _lastModuleResponse = DateTime.MinValue;
private DateTime _lastDiscoveryRefresh = DateTime.MinValue;
private static readonly IPEndPoint _localhostEndpoint = new(IPAddress.Loopback, 8888);
private const int ModuleTimeoutSeconds = 5;
private const int DiscoveryRefreshSeconds = 30;

// Hello packet: [0x80, 0x81, 0x7F, 200, 3, 56, 0, 0, CRC]
private readonly byte[] _helloPacket = { 0x80, 0x81, 0x7F, 200, 3, 56, 0, 0, 0x47 };
Expand Down Expand Up @@ -100,8 +108,10 @@ public async Task StartAsync()

_udpSocket.Bind(new IPEndPoint(IPAddress.Any, 9999));

// Set up module broadcast endpoint (default: 192.168.5.255:8888)
_moduleEndpoint = new IPEndPoint(IPAddress.Parse("192.168.5.255"), 8888);
// Discover broadcast endpoints on all network interfaces
_discoveryEndpoints = GetBroadcastEndpoints();
_lockedEndpoint = null;
_lastDiscoveryRefresh = DateTime.UtcNow;

IsConnected = true;

Expand Down Expand Up @@ -133,19 +143,42 @@ public async Task StopAsync()

public void SendToModules(byte[] data)
{
if (!IsConnected || _udpSocket == null || _moduleEndpoint == null) return;
if (!IsConnected || _udpSocket == null) return;

// Refresh discovery endpoints periodically
if ((DateTime.UtcNow - _lastDiscoveryRefresh).TotalSeconds > DiscoveryRefreshSeconds)
{
_discoveryEndpoints = GetBroadcastEndpoints();
_lastDiscoveryRefresh = DateTime.UtcNow;
}

// Check for module timeout -> go back to discovery
if (_lockedEndpoint != null &&
(DateTime.UtcNow - _lastModuleResponse).TotalSeconds > ModuleTimeoutSeconds)
{
_lockedEndpoint = null;
}

if (_lockedEndpoint != null)
{
// Connected: send to locked endpoint + localhost only
SendPacket(data, _lockedEndpoint);
SendPacket(data, _localhostEndpoint);
}
else
{
// Discovery: broadcast on all interfaces
foreach (var ep in _discoveryEndpoints)
SendPacket(data, ep);
}
}

private void SendPacket(byte[] data, IPEndPoint endpoint)
{
try
{
_udpSocket.BeginSendTo(data, 0, data.Length, SocketFlags.None, _moduleEndpoint,
ar =>
{
try
{
_udpSocket?.EndSendTo(ar);
}
catch { }
}, null);
_udpSocket!.BeginSendTo(data, 0, data.Length, SocketFlags.None, endpoint,
ar => { try { _udpSocket?.EndSendTo(ar); } catch { } }, null);
}
catch { }
}
Expand Down Expand Up @@ -253,7 +286,7 @@ private void ProcessReceivedData(byte[] data, IPEndPoint remoteEndPoint)
byte pgn = data[3];

// Track module connections based on hello messages
UpdateModuleConnection(pgn);
UpdateModuleConnection(pgn, remoteEndPoint);

// Fire event
DataReceived?.Invoke(this, new UdpDataReceivedEventArgs
Expand All @@ -278,7 +311,7 @@ private void ProcessReceivedData(byte[] data, IPEndPoint remoteEndPoint)
}
}

private void UpdateModuleConnection(byte pgn)
private void UpdateModuleConnection(byte pgn, IPEndPoint remoteEndPoint)
{
var now = DateTime.Now;

Expand All @@ -288,6 +321,7 @@ private void UpdateModuleConnection(byte pgn)
// AutoSteer PGNs
case PgnNumbers.HELLO_FROM_AUTOSTEER: // 126
_lastHelloFromAutoSteer = now;
LockToSubnet(remoteEndPoint.Address);
System.Diagnostics.Debug.WriteLine($"AutoSteer HELLO received at {now:HH:mm:ss.fff}");
break;

Expand All @@ -297,17 +331,20 @@ private void UpdateModuleConnection(byte pgn)
case PgnNumbers.STEER_SETTINGS: // 252
case PgnNumbers.STEER_CONFIG: // 251
_lastDataFromAutoSteer = now;
_lastModuleResponse = DateTime.UtcNow;
break;

// Machine PGNs (receive-only, only Hello matters)
case PgnNumbers.HELLO_FROM_MACHINE: // 123
_lastHelloFromMachine = now;
LockToSubnet(remoteEndPoint.Address);
System.Diagnostics.Debug.WriteLine($"Machine HELLO received at {now:HH:mm:ss.fff}");
break;

// IMU PGNs (only Hello matters - data only sent when active)
case PgnNumbers.HELLO_FROM_IMU: // 121
_lastHelloFromIMU = now;
LockToSubnet(remoteEndPoint.Address);
System.Diagnostics.Debug.WriteLine($"IMU HELLO received at {now:HH:mm:ss.fff}");
break;

Expand All @@ -318,6 +355,66 @@ private void UpdateModuleConnection(byte pgn)
}
}

/// <summary>
/// Lock outgoing packets to the subnet of a responding module.
/// Assumes /24 subnet (most common for field hardware).
/// </summary>
private void LockToSubnet(IPAddress remoteIP)
{
_lastModuleResponse = DateTime.UtcNow;

if (_lockedEndpoint != null || IPAddress.IsLoopback(remoteIP))
return;

var ipBytes = remoteIP.GetAddressBytes();
ipBytes[3] = 255;
_lockedEndpoint = new IPEndPoint(new IPAddress(ipBytes), 8888);
System.Diagnostics.Debug.WriteLine($"Auto-discovery: locked to subnet {_lockedEndpoint}");
}

/// <summary>
/// Enumerate broadcast addresses for all active IPv4 network interfaces.
/// Always includes localhost for simulator support.
/// </summary>
private static List<IPEndPoint> GetBroadcastEndpoints()
{
var endpoints = new List<IPEndPoint> { _localhostEndpoint };

try
{
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
{
if (nic.OperationalStatus != OperationalStatus.Up)
continue;
if (!nic.Supports(NetworkInterfaceComponent.IPv4))
continue;

foreach (var addr in nic.GetIPProperties().UnicastAddresses)
{
if (addr.Address.AddressFamily != AddressFamily.InterNetwork)
continue;
if (IPAddress.IsLoopback(addr.Address))
continue;

// Calculate broadcast: IP | ~SubnetMask
var ipBytes = addr.Address.GetAddressBytes();
var maskBytes = addr.IPv4Mask.GetAddressBytes();
var broadcastBytes = new byte[4];
for (int i = 0; i < 4; i++)
broadcastBytes[i] = (byte)(ipBytes[i] | ~maskBytes[i]);

endpoints.Add(new IPEndPoint(new IPAddress(broadcastBytes), 8888));
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to enumerate network interfaces: {ex.Message}");
}

return endpoints;
}

private string? GetLocalIPAddress()
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<DefineConstants>$(DefineConstants);IOS</DefineConstants>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="AgValoniaGPS.Services.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
Expand Down
9 changes: 7 additions & 2 deletions Shared/AgValoniaGPS.ViewModels/AutoSteerConfigViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public partial class AutoSteerConfigViewModel : ObservableObject
private readonly IConfigurationService _configService;
private readonly IUdpCommunicationService? _udpService;
private readonly IAutoSteerService? _autoSteerService;
private readonly Action? _launchWizard;

// Debounce timer for auto-sending slider changes
private readonly System.Timers.Timer _sliderDebounceTimer;
Expand All @@ -65,11 +66,13 @@ public partial class AutoSteerConfigViewModel : ObservableObject
public AutoSteerConfigViewModel(
IConfigurationService configService,
IUdpCommunicationService? udpService = null,
IAutoSteerService? autoSteerService = null)
IAutoSteerService? autoSteerService = null,
Action? launchWizard = null)
{
_configService = configService;
_udpService = udpService;
_autoSteerService = autoSteerService;
_launchWizard = launchWizard;

// Set up debounce timer for slider changes
_sliderDebounceTimer = new System.Timers.Timer(SliderDebounceDelayMs);
Expand Down Expand Up @@ -974,7 +977,9 @@ private void InitializeActionCommands()

WizardCommand = new RelayCommand(() =>
{
// TODO: Launch setup wizard
// Close the config panel before launching the wizard
IsPanelVisible = false;
_launchWizard?.Invoke();
});
}

Expand Down
Loading
Loading