Sync main with develop (24 commits, 10 PRs) - #84
Merged
Merged
Conversation
Added AF_NETLINK to the list of restricted address families in the agopenweb.service file.
…tions Fix linux headless service can not connect to UDP Modules input
Evaluate GeoPackage (.gpkg) as a replacement for the current text/binary field storage, and reject it: GPKG is SQLite plus OGC table conventions, and neither half earns its cost here. Our vector data is small and fully resident in RAM for guidance, so indexed/partial reads are wasted, and GeoJSON already reaches every consumer GPKG reaches. The coverage raster is the only heavy data, and standard GPKG has no good home for a 1-bit semantic mask on a local metric plane. Adoption would also mean a native SQLite dependency across all five platform heads and a hand-rolled container, since NetTopologySuite.IO.GeoPackage is a 2019 geometry codec rather than a container manager. Plan the incremental fix the analysis recommended instead. Reading the coverage code closely showed the dominant autosave cost is not the RLE: SaveSectionDisplay rebuilds the palette-index array from scratch every 30s by scanning the whole detection grid (~5e8 iterations plus a 25MB LOH allocation on a 520ha field). The plan moves both layers to a world-anchored tile grid so CheckAndExpandBounds cannot renumber tiles mid-job, tracks dirty tiles in a stream independent of the renderer and web-projector drains, and commits via a manifest written last. Also records two latent bugs found while reading: the save reads _detectionBits/_displayPixels off the lock while the GPS thread may reallocate them, and a bounds expansion racing the save makes SaveSectionDisplay log a mismatch and return without saving. Docs only; no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTikoBr49XDQmZ5an8qWUf
AgIO ships a stale 0x47 in the trailing CRC slot of its module-network packets and never recomputes it — UDP.designer.cs:82 rewrites helloFromAgIO[5] and leaves the checksum untouched. Modules gate on the header, PGN and magic bytes and ignore the CRC, so the placeholder has always been accepted. Compute it properly instead: firmware that ignores the byte is unaffected, firmware that verifies it now passes rather than relying on luck. Covers PGN 202 (scan), 201 (set subnet) and 200 (hello), the last of which moves out of UdpCommunicationService into PgnBuilder so every outbound packet is built in one place. Fix ValidateChecksum, which XOR'd bytes [0..len-2] and so disagreed with every packet this class builds — it would have rejected all valid traffic. It now sums [2..len-2] like CalculateCrc and PgnMessage.CalculateCRC. It stays diagnostics-only and deliberately unwired from the receive path: some modules transmit a placeholder CRC of their own, and gating inbound handling on this would drop legitimate traffic and take the link down. Route every builder through a single WithCrc helper. The seven remaining builders each hand-wrote `buf[N] = CalculateCrc(buf, 2, N-2)`; all seven were correct, but that is seven places to get wrong when a packet grows a data byte. WithCrc derives the offset and span from the buffer length, so the checksum cannot drift from the payload. It stamps in place, leaving the pooled thread-local buffers the hot-path builders reuse untouched. Add PgnChecksumTests covering all ten builders: expected length, header, CRC against an independently computed reference, and a ValidateChecksum round-trip. Also assert that corrupting any covered byte invalidates the checksum — without that, a builder regressing to a constant CRC would still satisfy every other assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTikoBr49XDQmZ5an8qWUf
PgnProtocol existed as two hand-maintained copies: one in the standalone vehicle simulator, one in AgOpenWeb.IntegrationTests' virtual UDP modules. They were byte-identical apart from the namespace and a Describe() helper the simulator added, so they had not drifted yet — but nothing was stopping them, and this is the module-side definition of the wire format the host side must agree with. Move the simulator's copy (the superset) to Shared/AgOpenWeb.Models/Communication/PgnProtocol.cs and delete the other. AgOpenWeb.Models is the right home: both consuming projects already reference it, so no csproj changes are needed, and PgnMessage — the host-side equivalent — already lives there. Expand the class doc with two things that were previously unwritten. First, the checksum rule here (sum of bytes [2 .. len-2]) has to stay in lockstep with PgnBuilder and PgnMessage.CalculateCRC. Second, IsValidPacket is genuinely enforced: the virtual modules drop packets that fail it without logging, so a host-side checksum bug surfaces as silence from the simulator rather than as an error. That is exactly how the AgIO placeholder CRC in the hello packet stayed hidden. Pure refactor — no behaviour change. Simulator builds; Services 1026 passed / 2 skipped, Models 146, ViewModels 222. Leaves the other four duplicated virtual modules alone. VirtualGpsReceiver, VirtualMachineModule, VirtualModuleHub and VirtualSteerModule have genuinely diverged between the two trees (45-153 diff lines each), so reconciling them involves behavioural decisions and is not a move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0133r866nfGcmeM3vabzrYRj
…analysis Docs: GeoPackage storage analysis and coverage persistence plan
Send computed PGN checksums instead of AgIO's placeholder (builds on #72)
…otocol Consolidate the duplicated PgnProtocol into Shared/AgOpenWeb.Models
The virtual GPS receiver, steer, machine and IMU modules existed as two hand-maintained copies: one under the vehicle simulator, one under AgOpenWeb.IntegrationTests. Unlike PgnProtocol these had genuinely diverged (45-153 diff lines each), so this reconciles them rather than just moving a file. Move them to a new Simulators/AgOpenWeb.VirtualModules library that both consumers reference. Deliberately not under Shared/: that tree compiles into every platform head, and 1000+ lines of UDP sockets and PID simulation have no business in a shipped app. The new project is a plain net10.0 library — no Avalonia, no NUnit — so the simulator app and the test-support library can both take it without dragging each other's dependencies along. Verified that no project under Platforms/ or Shared/ references it. In every case the simulator's copy was the superset: multi-destination UdpTargets alongside a convenience constructor with the tests' exact signature, OnSent/OnReceived taps, and a GPS receiver that also speaks PAOGI and the 65535 no-IMU sentinel. Those are adopted wholesale. The one genuine conflict was the bind address. The test copy bound IPAddress.Loopback, commented "so Windows Defender Firewall does not prompt during test runs"; the simulator bound IPAddress.Any with broadcast, because it has to be reachable from a host on another machine. Both are right for their caller, so neither wins: ModuleBindMode makes it an explicit choice, ModuleSocket applies it in one place, the UdpTargets constructors default to AllInterfaces, the single-host convenience constructors pin LoopbackOnly, and VirtualModuleHub defaults to LoopbackOnly so a test cannot accidentally raise a firewall prompt. The simulator passes AllInterfaces explicitly at its one call site. Two smaller reconciliations. VirtualModuleHub.Start() adopts the simulator's behaviour of not starting the GPS send loop — the host drives emission via SendOnce(), and a parallel 10 Hz emitter produced duplicate packets and jumpy motion. The two hub tests never call Start() and assert an exact SentCount, so they would have broken had the loop ever run. CreateIsolated() is dropped: the simulator had already removed it and it had no callers anywhere. Also ignore the *.SdkResolver.*.proj.Backup.tmp scratch files MSBuild drops next to the platform csproj files on restore. Pure refactor, no behaviour change for either consumer. Simulator and library build clean; Services 1051 passed / 2 skipped, Models 146, ViewModels 222 — identical to develop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0133r866nfGcmeM3vabzrYRj
…l-modules Consolidate the duplicated virtual hardware modules
Selecting Imperial flipped ConfigurationStore.IsMetric host-side, but the web client only honoured it in four places (status-bar speed, headland HUD, area/rate, U-turn distance). Every config panel had (m)/(cm)/(km/h) hard-coded in index.html, and populateCfgControls/wireCfgControls moved raw metric values straight in and out of the inputs. Models keep storing metric; conversion now happens only at the display/input boundary, the same contract as the native UnitConversion helper. Markup contract (index.html, 37 sites): <span class="u" data-unit="m"></span> unit label, text filled by applyUnits() <input data-unit="m" ...> value converted on populate + on send The HTML step/min stay metric and are stashed into data-mstep/data-mmin so a unit flip is reversible. app.js gains a UNIT_DEFS table (m<->ft, cm<->in, km/h<->mph, km<->mi, ha<->ac) plus unitLabel / toDisplayUnit / fromDisplayUnit / fmtUnit / readUnitInput / writeUnitInput / applyUnits. syncUnits() rides the Status frame and, on a change, relabels and marks the read-frames dirty so open panels re-populate immediately. Fields whose metric step is a whole unit (nudge distance, section widths, coverage margin) are int-backed host-side, so they round after converting -- 8 in becomes 20 cm, not 20.32, which the host's int parse would have dropped silently. Covered: Vehicle geometry, GPS distances and switch speed, Tool lengths / offsets / section + default widths / coverage margin / slow-speed cutoff / total width, U-turn, AutoSteer speed limits and nudge, Steer Wizard fields and live readouts, Field Builder headland + tram, boundary player offset and area, Offset Fix, field and AgShare lists, lightbar and XTE readouts, XTE chart. Verified against the running headless Desktop head over CDP: wheelbase 2.5 m shows 8.2 ft, nudge 20 cm shows 7.9 in, section width 100 cm shows 39.4 in, XTE 0.123 m reads "5 in R"; typing 10 ft sends 3.048 and 8 in sends 20; a live metric -> imperial -> metric flip restores every label, value, step and min. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132tf3qziiQTJv8AiJmwvVR
…#65) The display-boundary fix covered everything the client formats itself, but three measurements were rendered into strings host-side and printed verbatim by app.js, so they stayed wrong. Boundary list area was the worst of the three: BoundaryListItem.AreaDisplay was hardcoded "{AreaAcres:F2} Ac" and ignored IsMetric entirely, so the boundary menu showed acres even in metric mode. Fixed by sending the number instead of a string -- BoundaryListItem now stores AreaHectares, BoundaryItemDto carries AreaHa as f64, and the client formats it with fmtUnit like the sibling BoundaryDto.AreaHa it sits next to. Wire codec and fingerprint follow; client and server ship as one artifact so there is no protocol-version concern. Vehicle and tool profile previews (ConfigurationService.FormatVehicle / FormatTool) hardcoded " m" with no imperial branch. These stay host-rendered -- they are labelled free text, not editable values, so a structured DTO would only move the labels to JS -- but they now take the device IsMetric and render via UnitConversion.MetersToFeet. Note the flag comes from Store, not from the throwaway ConfigurationStore used to preview a non-active profile: IsMetric is a device setting and the temp store holds only the profile's own values. ProfilesFingerprint folds in IsMetric so a unit flip actually re-sends the frame; without it the picker preview stayed stale until some other config changed. renderBoundaryMenu joins onUnitsChanged for the same reason. Verified against the running headless Desktop head over CDP with a generated field, boundary and profile pair: boundary area 4.05 ha <-> 10.02 ac, vehicle wheelbase 2.50 m <-> 8.20 ft, tool width 6.00 m <-> 19.69 ft, all updating live on a metric -> imperial -> metric flip. Full suite green (1419 tests). The iOS and Android heads need no changes -- they ProjectReference the RemoteServer and pick up its embedded wwwroot on rebuild. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132tf3qziiQTJv8AiJmwvVR
Convert measurements at the web UI's display/input boundary (#65)
The Android head could open to a permanent "webpage not available" screen instead of the web UI. Users reported this as "crashes on startup" — the app never crashed, it just never loaded. Two bugs compounded: 1. Premature navigate. StartNavigationAsync waited Task.WhenAny(HostReady, Task.Delay(8000)) and then navigated regardless of whether the host had bound :5174. On a cold start the guidance host needs longer than that — 20.8 s and 41.8 s in two emulator repros, and even a warm relaunch measured 17.3 s. 2. The error page counted as a successful load. Android's WebView reports its own ERR_CONNECTION_REFUSED page via NavigationCompleted with IsSuccess=true. That latched `loaded = true`, which permanently disabled the retry watchdog (`if (!loaded) TryNavigate()`). So the app navigated too early, got connection-refused, and the error page's "success" killed every future retry. The host came up seconds later and nothing ever navigated again. Fix: use the port itself as ground truth instead of a timer plus a WebView signal that can lie. - IsHostAcceptingAsync() opens a loopback TCP connection to :5174. This is immune both to a stale HostReady (that TCS is static and survives a host restart in the same process) and to the fake error-page success. - Navigation only starts once the probe sees the host accepting, and an IsSuccess=true only latches when `hostUp` is true, so an error page can never end the retry loop. - Each attempt awaits its own navigation outcome via a per-attempt TaskCompletionSource rather than re-navigating on a fixed 2.5 s tick — a short watchdog restarts a load that is merely slow, which thrashes on slow devices. - If the host genuinely never starts, ShowSplashError surfaces the reason instead of leaving a frozen splash or an unexplained error page. iOS and Desktop are unaffected; both fully await the backend before navigating. Only Android decoupled the Activity from the foreground service, which is what forced the bounded-wait design in the first place. Verified on an Android 15 / API 35 emulator (cold boot, host binding at 20.8 s and 41.8 s — both previously dead, now load on attempt 1) and on a physical Galaxy Tab Active3 (arm64, Android 13, host at 2.7 s — fast path unchanged). Tests: 1419 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wion3j9RSizcV8SuAm4w92
…artup-race Fix Android launcher parking on a dead error page at startup (#73)
Vehicle/tool picker previews formatted lengths from Store.IsMetric, which only re-syncs to AppSettings on profile load/save, so a bare metric/imperial toggle left previews in metres while the rest of the UI showed feet. Read the authoritative settingsService.Settings.IsMetric instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Dre1bS3Fvi3VmUJGvnUDj
Fix: KML/ISO-XML import list always empty on headless installs
Fix: profile previews honor AppSettings units (imperial)
Fix: route data paths through AppDataRoot + honor imperial in remaining host strings
Bump sys/version.h to 26.6.75 for the 14 commits on develop since v26.6.74 (AppDataRoot path fixes, imperial units, Android startup race, PGN CRC, shared-module refactors). Also realign the platform heads, which had drifted from version.h and from each other: Desktop was pinned at 26.6.55 and iOS at 26.6.56 (both near the old 26.6.55 tag), and Android had never been versioned at all, still shipping the 1.0 template default. All three now read 26.6.75. Build numbers are deliberately untouched: iOS ApplicationVersion (2) and Android ApplicationVersion (1) are TestFlight/Play upload counters, not marketing versions, and bumping them is an upload-time concern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump to 26.6.75 and align platform versions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Routine
develop→mainsync. Brings 24 commits (10 merged PRs) tomain.maincarries no content of its own — its only unique commit is the merge commit from the previous sync (#64), so this merges cleanly with no conflicts.Fixes
AppDataRootand honor imperial units in the remaining host-side strings (ViewModels, RemoteWiring, Services).AppSettingsunits (imperial).AppDataRoot.RestrictAddressFamiliesinagopenweb.servicefor Linux headless network restrictions.Refactors
PgnProtocolintoShared/AgOpenWeb.Models.Docs
🤖 Generated with Claude Code