Summary
Two keys in the specification declare positive to port. Every other signed lateral or rotational quantity declares positive to starboard. This RFC proposes a single principle:
Signed athwartships quantities in Signal K are positive to starboard, negative to port, matching the right-handed body frame that navigation.attitude.yaw and navigation.rateOfTurn already use.
The two outliers are sensors.*.fromCenter and steering.autopilot.target.windAngle*. They are separable — one is a breaking change, the other is a free clarification — and are proposed as separate commits so they can be accepted independently.
This came out of review on SignalK/signalk-server#2399, where @keesverruijt asked why fromCenter is port-positive: "Just seems backwards to me. If you draw a boat on xy 2d frame with nose up positive is SB."
Part 1 — sensors.*.fromCenter (breaking, MODEL bump)
schemas/groups/sensors.json defines:
"The perpendicular distance from the longitudinal centerline of the vessel to the sensor location, -ve to starboard, +ve to port."
It is the only port-positive key of its kind
| Key |
File |
Sign |
sensors.*.fromCenter |
sensors.json:36 |
+port ⚠️ |
steering.rudderAngle |
steering.json:9 |
+stbd |
steering.rudderAngleTarget |
steering.json:15 |
+stbd |
navigation.attitude.roll |
navigation.json:520 |
+stbd |
navigation.attitude.yaw |
navigation.json:530 |
+stbd |
navigation.rateOfTurn |
navigation.json:600 |
+stbd |
propulsion.*.thrustAngle |
propulsion.json:159 |
+stbd |
design.keel.angle |
design.json:528 |
+stbd |
environment.wind.angleApparent |
environment.json:296 |
+stbd |
Where it came from
Commit fa162f7, 23 Aug 2014, "Added sensors, design data, and anchor data". The clause has survived 12 years with only cosmetic edits (units dropped, indentation, punctuation). There is no issue, RFC, or discussion anywhere in the repository history justifying it — it appears to be an initial-commit choice that was never revisited.
Why it now matters
It used to be inert metadata. It no longer is. Server-side GNSS lever-arm correction (SignalK/signalk-server#2399, #680) multiplies fromCenter by a heading taken from the same body frame as attitude.yaw. Mixing a port-positive offset with a starboard-positive rotation forces a non-standard rotation matrix — the implementation documents its own deviation:
* (Note: this differs from the standard NED rotation matrix because
* +y body is port here, not starboard.)
Under a starboard-positive convention this collapses to the standard NED rotation and the note disappears.
Implementations already disagree with the spec, and with each other
n2k-signalk/aisFromStarboard.js computes beam/2 - fromStarboard, i.e. positive = starboard, feeding sensors.ais.fromCenter from five PGNs. Pinned by a real-data test: beam 32 m, reference point 8 m from the starboard rail, asserts +8.
signalk-to-nmea2000/conversions/ais.js encodes beam/2 + fromCenter, i.e. positive = port — spec-compliant.
Because the decoder and encoder disagree, the AIS round trip is broken today:
posRefFromStarboard = 8 (8 m from the starboard rail, 32 m beam)
→ n2k-signalk decodes → fromCenter = 8
→ signalk-to-nmea2000 → posRefFromStarboard = 24
The reference point is mirrored to the opposite side of the hull, off by 16 m. A live bug regardless of how this RFC resolves — but only fixable once the two ends agree.
There is also a third position: signalk-server's @signalk/path-metadata has served "positive towards starboard" for /vessels/*/sensors/*/fromCenter since v2.28.0, contradicting the same server's own OpenAPI description and admin UI.
NMEA 2000 does not settle it
Worth stating explicitly, since "match N2K" would look like the obvious tiebreak. Per canboat, positionReferenceFromStarboard is:
"Signed": false, "RangeMin": 0, "Unit": "m"
An unsigned distance from the starboard side (AIS dimension C/D per ITU-R M.1371). NMEA 2000 has no sign here at all — the signed centreline-relative form is entirely a Signal K invention. There is no external convention to preserve, only which one Signal K standardises on.
Proposal
Change to "+ve to starboard, -ve to port".
Under mdbook/src/versioning.md this is a MODEL bump: it inverts the meaning of existing data with no structural change, so historical values silently become wrong. Suggest tracking it as a v2 item — v2-food-for-thought.md exists for exactly this and has no axis-convention entry yet.
Mitigating the blast radius: fromCenter is not widely consumed. Freeboard-SK ignores the reference-point offsets entirely. The known sign-sensitive consumers are n2k-signalk, signalk-to-nmea2000 and hoekens-anchor-alarm. On the server side the multi-GNSS feature that uses it most heavily has shipped only in v2.31.0-beta.1/beta.2 — never in a stable release — so there is an unusually clean window.
Part 2 — steering.autopilot.target.windAngle* (non-breaking, ADDITION)
Separable from Part 1 and much cheaper. The spec currently contradicts itself about the same physical quantity:
environment.wind.angleApparent / angleTrueGround / angleTrueWater — "negative to port" → +stbd
steering.autopilot.target.windAngleApparent / windAngleTrue — "+port -starboard" → +port
An autopilot comparing measured apparent wind angle against its target must silently negate one, or steer to the mirror image of the requested angle.
Nearly free to fix:
- No implementation pins the autopilot keys. No consumer of
steering.autopilot.target.windAngle* found in signalk-server, signalk-autopilot, signalk-to-nmea2000 or Freeboard-SK.
- The measured side is already starboard-positive in practice.
n2k-signalk/pgns/130306.js maps the N2K wind angle (0…2π clockwise from bow) into (-π, π], so 0…π is starboard — matching environment.wind, not the autopilot keys.
- The only spec fixture (
test/data/vessel-valid/steering-sample.json) holds 1.0472 and 1.39626, both positive, so no test data encodes a side.
A two-line description change, no code migration, no data reinterpretation — ADDITION-level, not a MODEL bump.
Prepared changes
Both parts are implemented, and the ecosystem consequences worked through, so the full picture is visible rather than just the argument. None should merge before this RFC is settled — if it resolves the other way they are discarded, except the two that are independent of it.
Questions
- Does the principle hold — should all signed athwartships quantities be starboard-positive?
- For Part 1, is a v2/MODEL bump the right vehicle, or is there appetite for a 1.x REVISION with a documented migration note, given how few consumers exist and that the largest is beta-only?
- Should Part 2 be split out and landed immediately, independent of Part 1?
fromBow has no sign statement at all — implicitly "toward the stern". Worth stating explicitly while the frame is being documented?
/cc @keesverruijt @tkurki
Summary
Two keys in the specification declare positive to port. Every other signed lateral or rotational quantity declares positive to starboard. This RFC proposes a single principle:
The two outliers are
sensors.*.fromCenterandsteering.autopilot.target.windAngle*. They are separable — one is a breaking change, the other is a free clarification — and are proposed as separate commits so they can be accepted independently.This came out of review on SignalK/signalk-server#2399, where @keesverruijt asked why
fromCenteris port-positive: "Just seems backwards to me. If you draw a boat on xy 2d frame with nose up positive is SB."Part 1 —
sensors.*.fromCenter(breaking, MODEL bump)schemas/groups/sensors.jsondefines:It is the only port-positive key of its kind
sensors.*.fromCentersensors.json:36steering.rudderAnglesteering.json:9steering.rudderAngleTargetsteering.json:15navigation.attitude.rollnavigation.json:520navigation.attitude.yawnavigation.json:530navigation.rateOfTurnnavigation.json:600propulsion.*.thrustAnglepropulsion.json:159design.keel.angledesign.json:528environment.wind.angleApparentenvironment.json:296Where it came from
Commit
fa162f7, 23 Aug 2014, "Added sensors, design data, and anchor data". The clause has survived 12 years with only cosmetic edits (units dropped, indentation, punctuation). There is no issue, RFC, or discussion anywhere in the repository history justifying it — it appears to be an initial-commit choice that was never revisited.Why it now matters
It used to be inert metadata. It no longer is. Server-side GNSS lever-arm correction (SignalK/signalk-server#2399, #680) multiplies
fromCenterby a heading taken from the same body frame asattitude.yaw. Mixing a port-positive offset with a starboard-positive rotation forces a non-standard rotation matrix — the implementation documents its own deviation:Under a starboard-positive convention this collapses to the standard NED rotation and the note disappears.
Implementations already disagree with the spec, and with each other
n2k-signalk/aisFromStarboard.jscomputesbeam/2 - fromStarboard, i.e. positive = starboard, feedingsensors.ais.fromCenterfrom five PGNs. Pinned by a real-data test: beam 32 m, reference point 8 m from the starboard rail, asserts+8.signalk-to-nmea2000/conversions/ais.jsencodesbeam/2 + fromCenter, i.e. positive = port — spec-compliant.Because the decoder and encoder disagree, the AIS round trip is broken today:
The reference point is mirrored to the opposite side of the hull, off by 16 m. A live bug regardless of how this RFC resolves — but only fixable once the two ends agree.
There is also a third position: signalk-server's
@signalk/path-metadatahas served "positive towards starboard" for/vessels/*/sensors/*/fromCentersince v2.28.0, contradicting the same server's own OpenAPI description and admin UI.NMEA 2000 does not settle it
Worth stating explicitly, since "match N2K" would look like the obvious tiebreak. Per canboat,
positionReferenceFromStarboardis:An unsigned distance from the starboard side (AIS dimension C/D per ITU-R M.1371). NMEA 2000 has no sign here at all — the signed centreline-relative form is entirely a Signal K invention. There is no external convention to preserve, only which one Signal K standardises on.
Proposal
Change to
"+ve to starboard, -ve to port".Under
mdbook/src/versioning.mdthis is a MODEL bump: it inverts the meaning of existing data with no structural change, so historical values silently become wrong. Suggest tracking it as a v2 item —v2-food-for-thought.mdexists for exactly this and has no axis-convention entry yet.Mitigating the blast radius:
fromCenteris not widely consumed. Freeboard-SK ignores the reference-point offsets entirely. The known sign-sensitive consumers aren2k-signalk,signalk-to-nmea2000andhoekens-anchor-alarm. On the server side the multi-GNSS feature that uses it most heavily has shipped only inv2.31.0-beta.1/beta.2— never in a stable release — so there is an unusually clean window.Part 2 —
steering.autopilot.target.windAngle*(non-breaking, ADDITION)Separable from Part 1 and much cheaper. The spec currently contradicts itself about the same physical quantity:
environment.wind.angleApparent/angleTrueGround/angleTrueWater— "negative to port" → +stbdsteering.autopilot.target.windAngleApparent/windAngleTrue— "+port -starboard" → +portAn autopilot comparing measured apparent wind angle against its target must silently negate one, or steer to the mirror image of the requested angle.
Nearly free to fix:
steering.autopilot.target.windAngle*found in signalk-server, signalk-autopilot, signalk-to-nmea2000 or Freeboard-SK.n2k-signalk/pgns/130306.jsmaps the N2K wind angle (0…2π clockwise from bow) into(-π, π], so 0…π is starboard — matchingenvironment.wind, not the autopilot keys.test/data/vessel-valid/steering-sample.json) holds1.0472and1.39626, both positive, so no test data encodes a side.A two-line description change, no code migration, no data reinterpretation — ADDITION-level, not a MODEL bump.
Prepared changes
Both parts are implemented, and the ecosystem consequences worked through, so the full picture is visible rather than just the argument. None should merge before this RFC is settled — if it resolves the other way they are discarded, except the two that are independent of it.
NaNbug (PGN 129041)idversions stuck at 1.5.1Questions
fromBowhas no sign statement at all — implicitly "toward the stern". Worth stating explicitly while the frame is being documented?/cc @keesverruijt @tkurki