Skip to content

Add POS MV as an alternative to the onboard dual RTK-GPS - #4

Open
belasi01 wants to merge 17 commits into
OceanOptics:masterfrom
belasi01:posmv-integration
Open

Add POS MV as an alternative to the onboard dual RTK-GPS#4
belasi01 wants to merge 17 commits into
OceanOptics:masterfrom
belasi01:posmv-integration

Conversation

@belasi01

@belasi01 belasi01 commented Sep 8, 2026

Copy link
Copy Markdown

Summary

On pySAS006, one of the two RTK GPS antennas failed mid-leg, and the RTK moving-baseline heading solution was separately found to occasionally lock onto a wrong integer ambiguity (heading briefly sweeping through most of the compass while still reporting deceptively tight accuracy). Since the vessel carries its own integrated GPS+IMU navigation system (Applanix POS MV) broadcasting NMEA-like sentences over UDP, this adds it as a selectable alternative source for heading, position/time, and pitch/roll -- so pySAS can keep operating normally if its own GPS antennas fail.

  • New POSMV interface (interfaces.py): parses INGGA/INZDA/INHDT/PASHR/INVTG over UDP and writes a synthesized $GPRMC frame to the shared data logger, so downstream tools (prepSAS.py/HyperInSPACE) keep working unmodified.
  • heading_source = posmv_heading: uses POS MV's true heading directly (no RTK-antenna mounting offset applied, unlike the RTK sources). When selected, sun position and clock sync also switch to POS MV, and the onboard GPS's own $GPRMC logging is suppressed, so pySAS becomes fully independent of its own GPS hardware end-to-end.
  • motion_source setting (independent of heading_source): choose pitch/roll from THS (default), the separate IMU, or POS MV -- added after the IMU proved unreliable in the field. Writes a synthetic SATTHS frame for IMU/POS MV, mirroring the existing IMU writer.
  • Settings UI: dropdowns for Heading Source (RTK / POS MV) and Motion Source (THS / IMU / POS MV), with disabled options and explanatory text when the corresponding config section isn't present. A tower_orientation_on_ship setting to correct the tower's own physical mounting offset relative to the ship's bow (found to be ~10 degrees off on this unit), applied consistently to both the displayed heading and the polar plot's blind-zone sector.
  • Sun Azimuth and a ship-heading-vs-sun-azimuth difference readout, with a red "Potential Ship Shadow on Lt" warning when the sun is within 5 degrees of dead astern.

All of this is opt-in via a [POSMV]/[IMU] config section and the Settings dropdowns; a pySAS with neither configured behaves exactly as before (heading_source = gps_relative_position, motion_source = ths by default).

Test plan

  • Deployed and run live on pySAS006 for multiple days, switching between RTK and POS MV heading sources during active acquisition.
  • Verified a single POS MV UDP capture round-trips correctly to $GPRMC (position/time/speed/course match input).
  • Confirmed UMTWR frames log POS MV-derived heading/accuracy (distinct 0.02 degree accuracy signature) when selected, and that switching sources stops the onboard GPS's own $GPRMC writes.
  • Verified the tower mounting offset and blind-zone fix against the System Orientation plot.

🤖 Generated with Claude Code

belasi01 and others added 17 commits September 7, 2026 22:23
The ship's Applanix POS MV broadcasts INGGA/INZDA/INHDT/PASHR/INVTG
sentences over UDP (port configurable, e.g. 12000). This adds a
POSMV class that parses them and writes a synthesized $GPRMC frame
to the shared data logger, keeping raw files compatible with
downstream tools (prepSAS.py/HyperInSPACE) that expect $GPRMC.
Modeled on GPS.format_data_as_gprmc(); heading/heading_accuracy are
also captured for use as an optional autopilot heading source.

Not wired into Runner yet -- inert until a [POSMV] config section
exists and instantiates it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the optional POSMV interface into Runner: instantiated only if
a [POSMV] config section exists, started continuously alongside GPS,
and usable as ship_heading source via heading_source = posmv_heading.
Inert by default -- heading_source stays gps_relative_position unless
explicitly changed, so this has no effect on the currently deployed
config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Note the POS MV source address (10.0.0.16) for reference; the host
config key itself is the local UDP bind address (0.0.0.0), not the
sender's address.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live testing against the ship's actual POS MV feed on the pySAS006
showed $INGGA (the longest sentence) gets split by the sender across
two UDP datagrams, so treating each recvfrom() as one complete line
silently dropped every INGGA and left datetime/position unset,
blocking all $GPRMC writes. Accumulate a buffer across recvfrom()
calls and split on newlines instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Displays runner.heading_source (e.g. RTK, POS MV, THS) so it's clear
at a glance which source is driving the autopilot -- useful now that
posmv_heading is a real option alongside the RTK GPS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
make_umtwr_frame() always recomputed ship_heading directly from the
RTK GPS regardless of heading_source, while the SAS heading field
right next to it already used self.ship_heading (the active-source
value). This meant switching heading_source (e.g. to posmv_heading)
silently kept logging RTK data under "ship heading" in the UMTWR
frames instead of the source actually steering the tower. Track
ship_heading_accuracy per source in get_ship_heading() and reuse
self.ship_heading/self.ship_heading_accuracy in make_umtwr_frame()
so both fields stay consistent with whatever source is configured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets the operator switch heading_source (RTK, GPS Motion, GPS
Vehicle, THS, POS MV) from the Settings modal instead of editing the
config file, applied live via runner.heading_source (no restart
needed for already-configured sources). POS MV is shown disabled
with an explanatory label when no [POSMV] section is configured,
and save_settings rejects it defensively even if selected anyway.
The sidebar's heading source label is now wired to the same
1s-refresh callback as the other telemetry fields instead of being
frozen at page load, so it reflects live changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_ship_heading() routed posmv.heading through
pilot.get_ship_heading(), which subtracts compass_zero
(gps_orientation_on_ship, calibrated for the RTK antenna mounting
offset). POS MV already outputs the ship's true heading directly, so
subtracting that offset introduced a ~90 degree error (confirmed live:
displayed 220 vs actual ship heading 312, a 92 degree gap matching
compass_zero=90). Use normalize_angle() directly instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The gps_motion/gps_vehicle/ths_heading options remain supported via
the config file for advanced use, but are noisy for normal operators
choosing between the two sources actually in use on this vessel.
Also note in GPS Orientation's help text that it only applies to the
RTK source, since POS MV reports true heading directly and ignores
that offset (see previous fix removing compass_zero from posmv_heading).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Explain the two operator-facing heading source choices (RTK vs POS
MV), how to enable POS MV ([POSMV] config section + restart), and
that the GPS Orientation setting only applies to RTK. Note the
advanced/legacy sources still available via config file only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
heading_source = posmv_heading only swapped the heading calculation;
sun position and clock sync still hardcoded self.gps, and the onboard
GPS kept writing its own $GPRMC alongside POS MV's, producing two
interleaved position streams in the raw files. This defeated the
actual goal: after one of the two RTK antennas failed at the end of
leg 1, being able to run fully off the ship's POS MV without
depending on pySAS's own GPS hardware at all.

- Add POSMV.packet_pvt_received (named to match GPS's attribute so
  both can be used interchangeably as a position/time source).
- Factor get_sun_position()/get_time_sync() through a shared
  _position_source() that follows heading_source, same no-fallback
  philosophy as get_ship_heading() (a stale/missing source fails
  loudly rather than silently reverting to GPS).
- Skip self.gps.start_logging() in wakeup()/run_manual() while
  posmv_heading is active, and stop it immediately on Settings save,
  so only POS MV's $GPRMC ends up in the raw files.
- Update the Settings FormText and README to describe the expanded
  scope (heading + sun position + clock sync + raw file logging).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…source

get_fig_system_orientation() still hardcoded self.gps for the THS/IMU
magnetic declination correction and the ship heading accuracy text,
even when heading_source = posmv_heading. Route both through
Runner._position_source() instead, matching the backend fix. Also
relabel the polar plot's "Ship (Dual GPS)" trace to "Ship (POS MV)"
when active, and add a Position (lat/lon) readout in the sidebar
sourced from whichever system is actually driving heading/sun
position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Show Sun Azimuth below Sun Elevation, plus the angular difference
between ship heading and sun azimuth. When that difference is within
5 degrees of 180 (sun near dead astern), display a red "Potential
Ship Shadow on Lt" badge -- the ship's own hull/superstructure can
shadow the water surface Lt is measuring in that geometry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…p) in Settings

The pySAS tower was found mounted ~10 degrees off from the ship's
bow. AutoPilot.tower_zero (indexing_table_orientation_on_ship)
already exists for exactly this and is applied to ship_heading in
steer()/make_umtwr_frame()/the polar plot regardless of
heading_source -- unlike gps_orientation_on_ship (compass_zero),
which only corrects the RTK antenna mounting and explicitly does not
apply to POS MV. It just wasn't exposed in the UI, only settable by
editing the config file. Add a "Tower Orientation" field mirroring
GPS Orientation's pattern so it can be tuned live from Settings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
blind_zone_center was shifted by ship heading only, while the tower
trace right next to it also subtracts tower_zero -- both are derived
from the same raw tower_limits (indexing table's own position frame),
so both need the same correction. Left uncorrected, the gray blind
zone sector drifted out of alignment with the actual unreachable
sector as soon as tower_zero was set away from 0 (as it now is, to
fix the ~10 degree mounting offset).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The separate IMU sensor has been unreliable in the field. Add
motion_source (independent of heading_source) so the operator can
pick which sensor's pitch/roll is displayed and logged, defaulting
to THS (always available, embedded in HyperSAS).

- POSMV: parse roll/pitch/roll_accuracy/pitch_accuracy from PASHR
  (previously only heading was read from it), and add
  format_data_as_satths()/_write_attitude() mirroring IMU's existing
  synthetic SATTHS frame writer, so downstream tools reading pitch/
  roll from a SATTHS frame keep working when POS MV is selected.
- IMU: add a _log_data gate (it previously always wrote,
  unconditionally) so its raw SATTHS frame stops when motion_source
  != 'imu', avoiding two conflicting synthetic frames in the raw
  files -- same principle as the earlier GPS/$GPRMC suppression
  under heading_source = posmv_heading.
- Runner: sync both sensors' logging flags to motion_source at
  startup; ui.py's Settings toggles them live on save.
- ui.py: collapse the timeseries plot's four fixed Pitch/Roll (IMU)/
  (THS) traces into a single dynamically-labeled Pitch/Roll pair
  sourced via the new get_motion_source() helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POSMV.start() unconditionally set _log_data = True the moment the
UDP socket connected, and nothing ever set it back to False -- so
once heading_source was switched to posmv_heading, POS MV kept
writing a continuous $GPRMC stream to the raw files regardless of
whether HyperSAS was actually measuring, unlike GPS whose logging is
explicitly started/stopped alongside HyperSAS via start_logging()/
stop_logging(). Reported by Nils on PR review.

Add the same start_logging()/stop_logging() pair to POSMV (for
$GPRMC) plus start_logging_attitude()/stop_logging_attitude() (for
the SATTHS-like pitch/roll frame, gated by motion_source), and wire
them into the same four call sites GPS already uses: run_manual(),
go_to_sleep(), wakeup(), and the manual-mode HyperSAS switch in
ui.py, plus applied immediately on Settings save.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant