Thank you for your interest in contributing to AgOpenWeb! This document lists features that need implementation and provides guidance for contributors.
- Fork the repository
- Clone your fork locally
- Build the project following instructions in
CLAUDE.md - Pick a feature from the list below
- Create a feature branch off
developand implement - Submit a pull request targeting the
developbranch
master- Stable releases onlydevelop- Active development branch, PR target for all new work- Feature branches - Create
feature/your-featureoffdevelopfor each issue
- Plans/ARCHITECTURE.md - Full architecture documentation: service communication, state management, domain models, data flow diagrams
- CLAUDE.md - Build commands, key files reference, common development tasks
- PGN.md - UDP packet protocol for hardware communication
-
Shared code (~92%): Located in
Shared/folderAgOpenWeb.Models/- Data modelsAgOpenWeb.Services/- Business logicAgOpenWeb.ViewModels/- MVVM ViewModels (ReactiveUI)AgOpenWeb.Views/- Shared UI controls, panels, dialogs
-
Platform code (~8%): Located in
Platforms/AgOpenWeb.Desktop/- Windows/macOS/LinuxAgOpenWeb.iOS/- iOS/iPadOSAgOpenWeb.Android/- Android
All code MUST go in Shared/ unless it requires platform-specific APIs. This is a hard architectural requirement, not a preference. Putting code in a single platform folder means the other platforms lose that feature.
What goes in Shared/: UI controls, panels, dialogs, view models, services, models, converters, styles, icons, localization strings.
What goes in Platforms/: Application entry point (App.axaml.cs), DI container setup, MainWindow/MainView (layout shell + drag handlers), MapService (map control registration), platform-specific APIs (file pickers, notifications).
Example violations fixed in #187-192:
- Status bar indicators were in Desktop
MainWindow.axamlinstead of a sharedStatusBarPanel - Localization init was Desktop-only in
App.axaml.csinstead of all three platforms - Flag placement banner existed only in Desktop
- Screenshot capture code was duplicated across all three platforms instead of a shared helper
AgOpenWeb follows strict MVVM layering. An earlier refactor cleaned up significant violations of this pattern — regressions are not welcome.
Pipeline computes, ViewModel coordinates, View binds.
- Services / pipeline (
AgOpenWeb.Services/): all domain computation. Geometry, guidance math, coordinate conversion, coverage painting, section logic, pathing, state machines. These are the units under test. - Models (
AgOpenWeb.Models/): data shapes.*WorkingStatePOCOs,*State : ObservableObjectmirrors, records, DTOs, geometry primitives. Behavior is limited to pure helpers (e.g.,GeometryMath). - ViewModels (
AgOpenWeb.ViewModels/): orchestration only. Expose bindable properties, wire commands to services, translate user intent into intents/service calls. ViewModels are thin. - Views (
AgOpenWeb.Views/): AXAML bindings and presentation. Code-behind is limited to view concerns (pointer handlers for dragging, focus, keyboard routing).
- No domain computation in the ViewModel. If you find yourself writing geometry, distance math, pathing, or multi-step business logic inside a
*ViewModel.cs, stop — move it to a service and call the service from the VM. The VM's job is to coordinate, not to compute. - No service calls from code-behind. Views bind to ViewModel properties and commands. Don't inject services into a
View.axaml.cs. - Commands stay thin. A
ReactiveCommanddelegate should read as "ask service X to do Y, optionally push an intent, optionally show a dialog." If it's longer than that, the body belongs in a service method. - No direct
State.*mutation from commands for pipeline-owned state. Push an intent throughIPipelineIntents— see the Threading Model section. UI-only state (dialog visibility, panel position) is fine to mutate directly. - No ViewModel references from services. Services expose interfaces, raise events, or return results. The ViewModel subscribes/consumes. Dependency flows one direction.
Services are unit-testable without a dispatcher, a view, or a mocked VM. Once computation leaks into the VM, that testability is gone and the VM becomes a 3000-line god object — the exact problem the earlier refactor fixed. Keep the layers clean.
AgOpenWeb uses a strict one-way data flow driven by a dedicated background cycle worker. This is a hard architectural requirement, not a convention — violating it reintroduces the AgOpenGPS/WinForms failure mode where domain logic races on the UI thread.
See Plans/Completed/threading_model_overview.svg for the full picture (current → phases → target in one frame) and Plans/Completed/THREADING_MIGRATION_PLAN.md for the historical migration plan (now complete).
Per-cycle GPS pipeline work runs on a dedicated cycle worker (
Task.Runper tick, with single-cycle-in-flight back-pressure viaInterlocked). It does not run on the UI dispatcher, and it does not run on any I/O thread — UDP receive, NMEA parse, file watcher. I/O threads parse, hand off, and return immediately.
Only the cycle-worker failure mode is survivable: back-pressure drops the next tick, the current cycle completes uninterrupted, I/O and UI keep running. Running cycle work on the UI dispatcher drops frames during turns (violating the 24 FPS floor); running it on the UDP receive thread stalls packet ingestion and can lose fixes.
*WorkingState— plain POCO/record. Owned by the cycle worker. Mutated freely on the background thread. NoObservableObject, noPropertyChanged, no UI awareness. Single-writer.*State : ObservableObject— the UI-bound type. A one-way mirror. The only writer isApplyGpsCycleResulton the UI thread. No service writes to it directly.
Cycle → UI (snapshots): Cycle worker mutates *WorkingState during a tick → builds an immutable GpsCycleResult snapshot at end of tick → posts to the UI dispatcher → ApplyGpsCycleResult writes the snapshot fields onto State.*, firing PropertyChanged and updating bindings.
UI → Cycle (intents): UI command writes to a thread-safe intent field/queue (IPipelineIntents) → cycle worker drains intents at the start of each tick → reacts on that tick → result appears in the UI on the same cycle's snapshot.
- Cycle worker never touches
*State : ObservableObject. Only*WorkingState. - ViewModel never mutates
*Statefrom a service callback. OnlyApplyGpsCycleResult. - UI commands push intents, they don't reach into cycle-worker state.
- Adding a new domain state: create a
FooWorkingStatePOCO, extendGpsCycleResultwith aFoosnapshot record, mirror it inApplyGpsCycleResult. - Adding a new UI command that changes pipeline behavior: define a method on
IPipelineIntents, push from the command, drain at the start of the cycle. - Adding a new service that reads GPS/position: take
*WorkingStateas a parameter, don't injectApplicationState. - Avoid writing to
State.YouTurn,State.Guidance,State.Vehicle,State.Sectionfrom anywhere exceptApplyGpsCycleResult.
Anything that lands on disk or on a wire (file I/O, NMEA / PGN packets,
NTRIP HTTP, AgShare uploads, ISO-XML, UDP) must format and parse
numbers and dates with CultureInfo.InvariantCulture. Without it,
(42.0308).ToString("F8") produces "42,03080000" in fi-FI / sv-SE /
de-DE / ..., which can break GPS NMEA parsing, PGN packet construction,
saved field metadata, or any other code that round-trips strings
between machines or processes.
The CA1305 analyzer (Specify IFormatProvider) fences this. It is
enabled solution-wide as a warning for visibility, and promoted to
error in the persistence- and wire-format-shaped paths via
.editorconfig globs (Services/Fields/, Services/AgShare/,
Services/IsoXml/, Services/Tram/, Services/Pipeline/,
Services/AutoSteer/PgnBuilder.cs, Services/NmeaParser*.cs,
Services/NtripClientService.cs, Services/FieldPlaneFileService.cs,
Simulators/**/Modules/).
If you add a new persistence sink, either drop it under one of those
folders or extend the .editorconfig glob list. UI display code is
free to use CurrentCulture (be explicit about it though — pin one
or the other, never the implicit default).
Open work is tracked on the AgOpenWeb project board. Pick a card, comment on the linked issue to claim it, and open your PR against develop.
Translation contributions are on hold until we decide how AgOpenWeb will connect to the shared AgOpenGPS Weblate project. AgOpen has used Weblate for over a year; our goal is to let one Weblate contribution benefit both projects rather than maintain parallel hand-edited .resx files.
Until that workflow is decided, please do not open PRs that add or edit files under Shared/AgOpenWeb.Views/Localization/. If you'd like to translate, watch this section — we'll link to the Weblate project here once it's set up.
-
Find the button in the relevant AXAML file under
Shared/AgOpenWeb.Views/Controls/ -
Add a Command binding to the button:
<Button Content="My Feature" Command="{Binding MyFeatureCommand}" />
-
Create the command in
MainViewModel.csor appropriate ViewModel:public ReactiveCommand<Unit, Unit> MyFeatureCommand { get; } // In constructor: MyFeatureCommand = ReactiveCommand.Create(ExecuteMyFeature); private void ExecuteMyFeature() { // Implementation here }
-
For dialogs, follow the existing pattern:
- Add
IsMyDialogVisibleproperty to ViewModel - Create dialog panel in
Shared/AgOpenWeb.Views/Controls/Dialogs/ - Add dialog to
MainWindow.axaml(Desktop) andMainView.axaml(iOS/Android)
- Add
Open an issue on GitHub or reach out to the maintainers.