This file provides guidance to Claude Code when working with this repository.
Key documentation:
- Plans/ARCHITECTURE.md - Full architecture: services, state management, data flow
- CONTRIBUTING.md - Contributor guide with cross-platform parity rules
- PGN.md - UDP packet protocol for hardware communication
AgOpenWeb3 is a cross-platform agricultural GPS guidance application built with Avalonia UI. It's a clean rewrite achieving 91.7% shared code across platforms.
What it does:
- Real-time GPS guidance for agricultural equipment
- Field boundary management and recording
- Unified track guidance (AB lines and curves use same system)
- U-turn path generation and following
- Section control for sprayers/planters
- NTRIP RTK corrections support
- Configurable keyboard hotkeys
- Integration with AgOpenGPS ecosystem via UDP
⚠️ Web UI is the only UI (as of v26.6.41). The native Avalonia UI was removed:AgOpenWeb.Views(SkiaMapControl, panels, dialogs), the platformMainWindow/MainViewshells, the--windowedDesktop mode, the.use_webview_launchermarker, andAgOpenWeb.UI.Testsare all gone. The UI is now the web client (AgOpenWeb.RemoteServer
- its
wwwrootCanvasKit PWA) on every platform. Each head boots the shared guidance backend (AgOpenWeb.RemoteWiring.WebBackend— VM brain on aHostLoopDispatcher, embeddedRemoteServerHost) and shows a full-screenNativeWebView(or you browse to:5174).MainViewModelis retained as the headless control brain the web client drives viaRemoteServerWiring. Sections of this doc below that describe native dialogs/panels/SkiaMapControl are historical and pending a fuller rewrite.
AgOpenWeb3/
├── Shared/ # Platform-agnostic code
│ ├── AgOpenWeb.Models/ # Data models, geometry, configuration, DTOs
│ ├── AgOpenWeb.Services/ # Business logic, GPS, NTRIP, UDP, audio (.wav embedded)
│ ├── AgOpenWeb.ViewModels/ # MVVM control brain (MainViewModel); no native UI
│ ├── AgOpenWeb.RemoteServer/ # Embedded web server + wwwroot CanvasKit PWA (the UI)
│ └── AgOpenWeb.RemoteWiring/ # WebBackend host core + RemoteServer↔VM command/projector wiring
│
├── Platforms/ # Thin WebView shells + platform services
│ ├── AgOpenWeb.Desktop/ # Windows/macOS/Linux (launcher window / headless daemon)
│ ├── AgOpenWeb.iOS/ # iOS/iPadOS (NativeWebView over WebBackend)
│ └── AgOpenWeb.Android/ # Android (foreground BackendService + WebView)
│
├── Tests/ # NUnit test projects
│ ├── AgOpenWeb.Models.Tests/ # Geometry, coordinate conversion
│ ├── AgOpenWeb.Services.Tests/ # NMEA parsing, guidance
│ ├── AgOpenWeb.ViewModels.Tests/ # ViewModel/control-brain logic
│ └── AgOpenWeb.IntegrationTests/ # Test-support lib: virtual UDP modules (used by Services.Tests)
│
├── TestRunner/ # Legacy test harness for guidance algorithms
└── AgOpenWeb.sln # Solution file
| Platform | Project | Notes |
|---|---|---|
| Windows | AgOpenWeb.Desktop | Same codebase as macOS/Linux |
| macOS | AgOpenWeb.Desktop | Same codebase as Windows/Linux |
| Linux | AgOpenWeb.Desktop | Same codebase as Windows/macOS |
| iOS/iPadOS | AgOpenWeb.iOS | Requires Xcode 26.3+, runs on ARM64 simulator |
| Android | AgOpenWeb.Android | APK build, sideload install |
# Build and run Desktop (works on Windows, macOS, Linux)
dotnet build Platforms/AgOpenWeb.Desktop/AgOpenWeb.Desktop.csproj
dotnet run --project Platforms/AgOpenWeb.Desktop/AgOpenWeb.Desktop.csproj
# Build iOS (requires macOS with Xcode 26.3+)
dotnet build Platforms/AgOpenWeb.iOS/AgOpenWeb.iOS.csproj -c Debug -f net10.0-ios -r iossimulator-arm64
# Deploy and run iOS on simulator
dotnet build Platforms/AgOpenWeb.iOS/AgOpenWeb.iOS.csproj -c Debug -f net10.0-ios -r iossimulator-arm64 -t:Run
# Alternative iOS deployment (if -t:Run doesn't work)
xcrun simctl install booted Platforms/AgOpenWeb.iOS/bin/Debug/net10.0-ios/iossimulator-arm64/AgOpenWeb.iOS.app
xcrun simctl launch booted com.agopenweb.ios
# Build Android APK
dotnet build Platforms/AgOpenWeb.Android/AgOpenWeb.Android.csproj
# Build entire solution
dotnet build AgOpenWeb.sln
# Run tests
dotnet test Tests/All platforms render the map through SkiaMapControl, which leases the Skia
GPU surface inside a CompositionCustomVisualHandler and re-arms via
RegisterForNextAnimationFrameUpdate. This bucket sits outside the Av12
commit throttle that capped OpenGlControlBase at ~32 FPS on iPad
(issue #21409).
True perspective comes from SKMatrix44; top-down mode is just
pitch = 90° on the same control — no second renderer behind a toggle.
All panels, dialogs, and controls live in AgOpenWeb.Views:
Controls/SkiaMapControl.cs- Main map renderingControls/DialogOverlayHost.axaml- Hosts all modal dialog overlays (shared across platforms)Controls/Panels/- LeftNavigationPanel, SimulatorPanel, SectionControlPanel, etc.Controls/Dialogs/- All modal dialogs (FieldSelection, DataIO, AgShare, etc.)Converters/- Shared value converters (BoolToColor, FixQualityToColor, etc.)
Dialogs use a centralized state machine in UIState. Only one dialog can be open at a time.
To show a dialog from a ViewModel:
State.UI.ShowDialog(DialogType.YourDialog); // Opens dialog
State.UI.CloseDialog(); // Closes any open dialogDialog visibility in AXAML binds to computed properties on UIState:
<dialogs:YourDialogPanel
IsVisible="{Binding State.UI.IsYourDialogVisible}"
IsHitTestVisible="{Binding State.UI.IsYourDialogVisible}"/>All dialog panels are registered in DialogOverlayHost.axaml (shared, not per-platform).
Confirmation dialogs use a callback pattern:
ShowConfirmationDialog("Title", "Message", () => { /* on confirm */ });MainViewModel is a large partial class split across ~19 files by domain:
| File | Domain |
|---|---|
MainViewModel.cs |
Core state, constructor, DI, properties |
MainViewModel.Commands.Track.cs |
Track/AB line commands |
MainViewModel.Commands.Boundary.cs |
Boundary, headland, AgShare commands |
MainViewModel.Commands.Fields.cs |
Field open/close/create commands |
MainViewModel.Commands.Ntrip.cs |
NTRIP profile management |
MainViewModel.Commands.Navigation.cs |
View settings, camera, zoom |
MainViewModel.Commands.Hotkeys.cs |
Hotkey configuration and dispatch |
MainViewModel.Commands.Settings.cs |
App directories, reset settings |
MainViewModel.Commands.Simulator.cs |
Simulator controls |
MainViewModel.Commands.Configuration.cs |
Vehicle/tool configuration |
MainViewModel.Commands.Wizards.cs |
Setup wizards |
MainViewModel.YouTurn.cs |
U-turn path generation and following |
MainViewModel.Guidance.cs |
Guidance algorithm orchestration |
MainViewModel.GpsHandling.cs |
GPS data processing |
MainViewModel.SectionControl.cs |
Section on/off logic |
MainViewModel.BoundaryRecording.cs |
Boundary recording state |
MainViewModel.Ntrip.cs |
NTRIP connection management |
MainViewModel.Simulator.cs |
GPS simulator state |
MainViewModel.ViewSettings.cs |
Display/view settings |
ConfigurationStore is a reactive singleton holding all runtime configuration (vehicle, tool, guidance, hotkeys, etc.). It syncs to/from AppSettings JSON via ConfigurationService.
ConfigStore.Vehicle.AntennaHeight // Vehicle config
ConfigStore.Tool.ToolWidth // Tool/implement config
ConfigStore.Hotkeys.GetActionForKey("A") // Hotkey lookupPanels use Canvas positioning with pointer event handlers for dragging:
- Desktop: Handlers in
MainWindow.axaml.cs - iOS: Handlers in
MainView.axaml.cs - LeftNavigationPanel has built-in drag support for sub-panels
Key insight from AgOpenGPS creator Brian: "An AB line is just a curve with 2 points."
All guidance track types use a single Track model (Shared/AgOpenWeb.Models/Track/Track.cs):
public class Track
{
public string Name { get; set; }
public List<Vec3> Points { get; set; } // AB lines have 2 points, curves have N
public TrackMode Mode { get; set; }
public bool IsVisible { get; set; }
public double NudgeDistance { get; set; }
// Computed properties
public bool IsABLine => Points.Count == 2;
public bool IsCurve => Points.Count > 2;
}Single guidance service (TrackGuidanceService) handles both Pure Pursuit and Stanley algorithms for all track types. This replaced 4 separate guidance services and reduced ~2,100 lines of duplicated code.
Shared utilities in GeometryMath.cs:
Distance(),DistanceSquared()- various overloads for Vec2/Vec3ToDegrees(),ToRadians()- angle conversionIsPointInPolygon()- boundary checksPIBy2,twoPI- common constants
AgOpenWeb may use different/improved formats from AgOpenGPS when it benefits code simplicity or features. Provide one-way import from AgOpenGPS formats rather than maintaining full backwards compatibility.
- Current: Legacy text formats (Field.txt, Boundary.txt, etc.) and XML profiles
- Future: Unified JSON formats (see
Plans/FILE_FORMAT_MODERNIZATION_PLAN.md) - Migration: Auto-detect legacy files, import once, save in new format only
- .NET 10.0 - Target framework
- Avalonia 11.3.9 - Cross-platform UI framework
- ReactiveUI 20.1.1 - MVVM framework with reactive extensions
- Microsoft.Extensions.DependencyInjection - Dependency injection
- NUnit 4.3 + Avalonia.Headless.NUnit - Testing framework
- NSubstitute - Mocking for UI tests
| File | Purpose |
|---|---|
Shared/AgOpenWeb.ViewModels/MainViewModel.cs |
Main application state, constructor, DI |
Shared/AgOpenWeb.Views/Controls/SkiaMapControl.cs |
Map rendering (Skia via CompositionCustomVisualHandler) |
Shared/AgOpenWeb.Views/Controls/DialogOverlayHost.axaml |
All dialog overlay registrations |
Shared/AgOpenWeb.Views/Controls/Panels/LeftNavigationPanel.axaml |
Main navigation sidebar |
Shared/AgOpenWeb.Models/Track/Track.cs |
Unified track model (AB lines + curves) |
Shared/AgOpenWeb.Models/Base/GeometryMath.cs |
Shared geometry utilities |
Shared/AgOpenWeb.Models/State/UIState.cs |
Dialog state machine, panel visibility |
Shared/AgOpenWeb.Models/Configuration/ConfigurationStore.cs |
Reactive config singleton |
Shared/AgOpenWeb.Models/Configuration/HotkeyConfig.cs |
Hotkey bindings model |
Shared/AgOpenWeb.Services/Track/TrackGuidanceService.cs |
Pure Pursuit + Stanley guidance |
Shared/AgOpenWeb.Services/YouTurn/YouTurnGuidanceService.cs |
U-turn path following |
Shared/AgOpenWeb.Services/NtripClientService.cs |
NTRIP RTK corrections |
Shared/AgOpenWeb.Services/GpsService.cs |
GPS data processing |
Shared/AgOpenWeb.Services/ConfigurationService.cs |
AppSettings ↔ ConfigurationStore sync |
Platforms/AgOpenWeb.Desktop/Views/MainWindow.axaml |
Desktop main window |
Platforms/AgOpenWeb.iOS/Views/MainView.axaml |
iOS main view |
Tests/AgOpenWeb.UI.Tests/MainViewModelBuilder.cs |
Test helper: builds fully-mocked MainViewModel |
Services use interface-based design in Shared/AgOpenWeb.Services/Interfaces/:
ITrackGuidanceService- Unified guidance (Pure Pursuit + Stanley) for all track typesIGpsService- GPS data processing and position updatesIUdpCommunicationService- UDP communication with AgOpenGPS modulesINtripClientService- NTRIP caster connections for RTKIFieldService- Field loading/saving/managementIBoundaryRecordingService- Recording field boundariesIMapService- Map control registration and track/boundary renderingIConfigurationService- AppSettings ↔ ConfigurationStore sync, vehicle profilesIVehicleProfileService- Vehicle profile CRUDINtripProfileService- NTRIP profile CRUDIAutoSteerService- Zero-copy GPS→steering pipelineISettingsService- AppSettings JSON persistenceICoverageMapService- Worked area tracking (triangle strips)ISectionControlService- Automatic section on/off based on coverage/boundaries
Platform projects contain only what must differ per platform. All UI, dialogs, and business logic live in Shared.
App.axaml/cs- Application entry point, DI container setupProgram.cs- Main entry pointMainWindow.axaml/cs- Window with drag handlers, hotkey dispatch, stylesServices/MapService.cs- Map control registrationDependencyInjection/ServiceCollectionExtensions.cs- DI setup
App.axaml/cs- Application entry pointAppDelegate.cs- iOS app delegateMainView.axaml/cs- Main view with drag handlersServices/MapService.cs- Map control registrationInfo.plist- iOS app configuration
App.axaml/cs- Application entry pointMainActivity.cs- Android activity with immersive modeMainView.axaml/cs- Main view with drag handlersServices/MapService.cs- Map control registration
- Add a new
DialogTypeenum value inShared/AgOpenWeb.Models/State/UIState.cs - Add
IsYourDialogVisiblecomputed property andRaisePropertyChangedcall inUIState - Create
YourDialogPanel.axaml/csinShared/AgOpenWeb.Views/Controls/Dialogs/- Bind
IsVisibleandIsHitTestVisibletoState.UI.IsYourDialogVisible - Include semi-transparent backdrop with
PointerPressedhandler to close
- Bind
- Register in
Shared/AgOpenWeb.Views/Controls/DialogOverlayHost.axaml - Add show/close commands in a
MainViewModel.Commands.*.cspartial class file:ShowYourDialogCommand = ReactiveCommand.Create(() => State.UI.ShowDialog(DialogType.YourDialog)); CloseYourDialogCommand = ReactiveCommand.Create(() => State.UI.CloseDialog());
- Create
YourPanel.axaml/csinShared/AgOpenWeb.Views/Controls/Panels/ - Add to
LeftNavigationPanel.axamlif it's a sub-panel - Or add directly to platform views if standalone
SkiaMapControl drives frames via RegisterForNextAnimationFrameUpdate,
which runs at the platform's display refresh rate (60 Hz on most devices,
120 Hz on ProMotion iPad). There is no fixed-rate DispatcherTimer. To
gate redraws on state changes (instead of every animation tick), see
SendStateToHandler and the _pendingComposite coalescing logic.
The NTRIP client uses HTTP/1.1 format:
GET /mountpoint HTTP/1.1
Host: caster.example.com
Ntrip-Version: Ntrip/2.0
Authorization: Basic base64(username:password)
User-Agent: NTRIP AgOpenWeb
- iOS simulator issues: Use
xcrun simctlcommands directly ifdotnet build -t:Runfails - Frame rate: ARM64 Macs handle 60 FPS fine; Intel Macs may need 10-15 FPS due to emulation
- Dialog not showing: Check
DialogTypeenum,UIStatevisibility property, andDialogOverlayHost.axamlregistration - Panel not dragging: Verify Canvas positioning and pointer event handlers
- iOS Release builds hang in CI: Use Debug configuration (Release triggers AOT compilation that hangs on runners)
- Cross-platform parity is mandatory. All code MUST go in
Shared/unless it requires platform-specific APIs. Platforms only contain: app entry point, DI setup, MainWindow/MainView shell with drag handlers, MapService registration. SeeCONTRIBUTING.mdfor examples of violations fixed in #187-192. - Use
Classes.Activebinding for state-based styling instead of converters where possible - Dialogs are overlay panels via
DialogOverlayHost, not separate windows - Use dependency injection for services
- Use shared
GeometryMathutilities instead of duplicating distance/angle calculations - New dialog state goes through
UIState.ShowDialog(DialogType.X), not ad-hoc boolean properties
# Run specific test project
dotnet test Tests/AgOpenWeb.Models.Tests/
dotnet test Tests/AgOpenWeb.Services.Tests/
dotnet test Tests/AgOpenWeb.ViewModels.Tests/
# Legacy guidance algorithm test harness
dotnet run --project TestRunner/TestRunner.csprojTest projects:
AgOpenWeb.Models.Tests- GeometryMath, GeoConversion, boundary/curve utilitiesAgOpenWeb.Services.Tests- NMEA parsing, TrackGuidanceService (uses the virtual UDP modules inAgOpenWeb.IntegrationTests)AgOpenWeb.ViewModels.Tests- ViewModel / control-brain logicAgOpenWeb.IntegrationTests- test-support library (virtual GPS/steer/machine UDP modules, fake settings); not a standalone test run
U-turns are generated in MainViewModel.CreateSimpleUTurnPath():
- Entry leg: straight line from cultivated area into headland
- Arc: semicircle positioned so it fits within headland zone
- Exit leg: straight line back to next track
Key parameters:
HeadlandDistance- width of headland zone (green to yellow line)turnRadius- half of track offset (based on implement width x row skip)- Arc positioning:
headlandLegLength = max(HeadlandDistance - turnRadius, 2.0)
The arc must fit between the headland boundary (green line) and outer boundary (yellow line). If the headland is too narrow for the turn radius, the arc will extend past the outer boundary.
Two GitHub Actions workflows, split by purpose:
build-and-release.yml("CI") — on every push / PR tomain: runs the test suite plus a compile-check of each platform head (Desktop, Android, iOS). No packaging, no releases.build-deploy-bundles.yml— the single packaging + release publisher. Runs thedeploy/{linux,windows,macos}/package.shscripts + builds the signed Android APK on clean runners:- on a
v*tag (or manual dispatch with a tag) → publishes one complete Release with every artifact: Linux daemon (x64/arm64) + desktop launcher (x64/arm64) tarballs, Windows zip (launcher + service installer), macOS.dmg, and the Android APK; - on a daily schedule → refreshes a rolling
nightlyprerelease with the same artifacts; - on a plain dispatch with no tag → builds + uploads artifacts only (dry run, no Release).
- on a
To cut a release: bump sys/version.h, then push a tag — git tag v26.6.x && git push origin v26.6.x.
ABLine.cs- Marked[Obsolete], retained only for AgOpenGPS file I/O compatibility- Use
Trackmodel for all new guidance code TestRunner/- Legacy console test harness, superseded by NUnit test projects inTests/