Files
venus/axiom-nmea/PROTOCOL.md
dev 9756538f16 Initial commit: Venus OS boat addons monorepo
Organizes 11 projects for Cerbo GX/Venus OS into a single repository:
- axiom-nmea: Raymarine LightHouse protocol decoder
- dbus-generator-ramp: Generator current ramp controller
- dbus-lightning: Blitzortung lightning monitor
- dbus-meteoblue-forecast: Meteoblue weather forecast
- dbus-no-foreign-land: noforeignland.com tracking
- dbus-tides: Tide prediction from depth + harmonics
- dbus-vrm-history: VRM cloud history proxy
- dbus-windy-station: Windy.com weather upload
- mfd-custom-app: MFD app deployment package
- venus-html5-app: Custom Victron HTML5 app fork
- watermaker: Watermaker PLC control UI

Adds root README, .gitignore, project template, and per-project
.gitignore files. Sensitive config files excluded via .gitignore
with .example templates provided.

Made-with: Cursor
2026-03-16 17:04:16 +00:00

18 KiB
Raw Blame History

Raymarine LightHouse Protocol Analysis

Overview

This document describes the findings from reverse-engineering the Raymarine LightHouse network protocol used by AXIOM MFDs to share sensor data over IP multicast.

Key Discovery: Raymarine does NOT use standard NMEA 0183 text sentences on its multicast network. Instead, it uses Google Protocol Buffers (protobuf) binary encoding over UDP multicast.

Quick Reference

Decoding Status Summary

Sensor Status Field Unit
GPS Position Reliable 2.1, 2.2 Decimal degrees
SOG (Speed Over Ground) Reliable 5.5 m/s → knots
COG (Course Over Ground) Reliable 5.1 Radians → degrees
Compass Heading ⚠️ Variable 3.2 Radians → degrees
Wind Direction ⚠️ Variable 13.4 Radians → degrees
Wind Speed ⚠️ Variable 13.5, 13.6 m/s → knots
Depth ⚠️ Variable 7.1 Meters → feet
Barometric Pressure Reliable 15.1 Pascals → mbar
Water Temperature Reliable 15.9 Kelvin → Celsius
Air Temperature ⚠️ Variable 15.3 Kelvin → Celsius
Tank Levels Reliable 16 Percentage (0-100%)
House Batteries Reliable 20 Volts (direct)
Engine Batteries Reliable 14.3.4 Volts (direct)

Primary Data Source

  • Multicast: 226.192.206.102:2565
  • Source IP: 198.18.1.170 (AXIOM 12 Data Master)
  • Packet Format: 20-byte header + Protocol Buffers payload

Network Configuration

Multicast Groups

Group Port Source IP Device Purpose
226.192.206.98 2561 10.22.6.115 Unknown Navigation (mostly zeros)
226.192.206.99 2562 198.18.1.170 AXIOM 12 Data Master Heartbeat/status
226.192.206.102 2565 198.18.1.170 AXIOM 12 Data Master Primary sensor data
226.192.219.0 3221 198.18.2.191 AXIOM PLUS 12 RV Display synchronization

Additional groups that may contain sensor/tank data:

  • 226.192.206.100:2563
  • 226.192.206.101:2564
  • 239.2.1.1:2154

Data Sources

IP Address Ports Device Data Types
198.18.1.170 35044, 41741 AXIOM 12 (Data Master) GPS, Wind, Depth, Heading, Temp, Tanks, Batteries
198.18.2.191 35022, 45403, 50194 AXIOM PLUS 12 RV Display sync, possible depth relay
10.22.6.115 57601 Unknown Mostly zero values

Packet Sizes

The data master (198.18.1.170) sends packets of varying sizes:

Size (bytes) Frequency Contents
16 Low Minimal/heartbeat
54 Low Short messages
91-92 Medium Status/heartbeat
344 Medium Partial sensor data
446 Medium Sensor data
788-903 Medium Extended sensor data
1003 Medium Extended sensor data
1810-2056 High Full navigation data including GPS

Packet Structure

Fixed Header (20 bytes)

All packets begin with a 20-byte fixed header before the protobuf payload:

Offset    Size    Description
------    ----    -----------
0x0000    8       Packet identifier (00 00 00 00 00 00 00 01)
0x0008    4       Source ID
0x000C    4       Message type indicator
0x0010    4       Payload length

Protobuf payload starts at offset 0x14 (20 decimal).

Protobuf Message Structure

The payload uses Google Protocol Buffers wire format. Top-level fields:

Field 1  (length) - Device Info (name, serial number)
Field 2  (length) - GPS/Position Data
  ├─ Field 1 (fixed64/double) - Latitude
  └─ Field 2 (fixed64/double) - Longitude

Field 3  (length) - Heading Block
  └─ Field 2 (fixed32/float) - Heading (radians)

Field 5  (length) - SOG/COG Navigation Data (86-92 byte packets)
  ├─ Field 1 (fixed32/float) - COG Course Over Ground (radians)
  ├─ Field 3 (fixed32/float) - Unknown constant (0.05)
  ├─ Field 4 (fixed32/float) - Unknown constant (0.1)
  ├─ Field 5 (fixed32/float) - SOG Speed Over Ground (m/s)
  ├─ Field 6 (fixed32/float) - Secondary angle (radians) - possibly heading
  └─ Field 7 (fixed32/float) - Unknown constant (11.93)

Field 7  (length) - Depth Block (large packets only)
  └─ Field 1 (fixed32/float) - Depth (meters)

Field 13 (length) - Wind/Navigation Data
  ├─ Field 4 (fixed32/float) - True Wind Direction (radians)
  ├─ Field 5 (fixed32/float) - True Wind Speed (m/s)
  └─ Field 6 (fixed32/float) - Apparent Wind Speed (m/s)

Field 14 (repeated) - Engine Data
  ├─ Field 1 (varint) - Engine ID (0=Port, 1=Starboard)
  └─ Field 3 (length) - Engine Sensor Data
      └─ Field 4 (fixed32/float) - Battery Voltage (volts)

Field 15 (length) - Environment Data
  ├─ Field 1 (fixed32/float) - Barometric Pressure (Pascals)
  ├─ Field 3 (fixed32/float) - Air Temperature (Kelvin)
  └─ Field 9 (fixed32/float) - Water Temperature (Kelvin)

Field 16 (repeated) - Tank Data
  ├─ Field 1 (varint) - Tank ID
  ├─ Field 2 (varint) - Status/Flag
  └─ Field 3 (fixed32/float) - Tank Level (percentage)

Field 20 (repeated) - House Battery Data
  ├─ Field 1 (varint) - Battery ID (11=Aft, 13=Stern)
  └─ Field 3 (fixed32/float) - Voltage (volts)

Wire Format Details

Raymarine uses Protocol Buffers with these wire types:

Wire Type Name Size Usage
0 Varint Variable IDs, counts, enums, status flags
1 Fixed64 8 bytes High-precision values (GPS coordinates)
2 Length-delimited Variable Nested messages, byte strings
5 Fixed32 4 bytes Floats (angles, speeds, voltages)

Tag Format

Each field is prefixed by a tag byte: (field_number << 3) | wire_type

Examples:

  • 0x09 = Field 1, wire type 1 (fixed64)
  • 0x11 = Field 2, wire type 1 (fixed64)
  • 0x15 = Field 2, wire type 5 (fixed32)
  • 0x1d = Field 3, wire type 5 (fixed32)

Unit Conventions

Measurement Raw Unit Conversion
Latitude/Longitude Decimal degrees Direct (WGS84)
Angles (heading, wind) Radians × 57.2957795131 = degrees
Wind speed m/s × 1.94384449 = knots
Depth Meters ÷ 0.3048 = feet
Temperature Kelvin 273.15 = Celsius
Barometric Pressure Pascals × 0.01 = mbar (hPa)
Tank levels Percentage 0-100% direct
Voltage Volts Direct value

Field Extraction Methods

GPS Position RELIABLE

Location: Field 2.1 (latitude), Field 2.2 (longitude)

# Parse Field 2 as nested message, then extract:
Field 2.1 (fixed64/double)  Latitude in decimal degrees
Field 2.2 (fixed64/double)  Longitude in decimal degrees

# Validation
-90  latitude  90
-180  longitude  180
abs(lat) > 0.1 or abs(lon) > 0.1  # Not at null island

Example decode:

Hex: 09 cf 20 f4 22 c9 ee 38 40 11 b4 6f 93 f6 2b 28 54 c0
     |  |                       |  |
     |  +-- Latitude double     |  +-- Longitude double
     +-- Field 1 tag            +-- Field 2 tag

Latitude:  24.932757° N
Longitude: -80.627683° W

SOG (Speed Over Ground) RELIABLE

Location: Field 5.5

Field 5.5 (fixed32/float)  SOG in meters per second

# Conversion
sog_knots = sog_ms × 1.94384449

# Validation
0  sog  50 (m/s, roughly 0-100 knots)

Notes:

  • Found in 86-92 byte packets
  • At dock, value is near zero (~0.01 m/s = 0.02 kts)
  • Derived from GPS, so requires GPS lock

COG (Course Over Ground) RELIABLE

Location: Field 5.1

Field 5.1 (fixed32/float)  COG in radians

# Conversion
cog_degrees = (radians × 57.2957795131) % 360

# Validation
0  radians  6.5 (approximately 0 to 2π)

Notes:

  • Found in 86-92 byte packets
  • At dock/low speed, COG jumps randomly (GPS noise when stationary)
  • Field 5.6 also contains an angle that varies similarly (possibly heading-from-GPS)

Field 5 Complete Structure

Subfield Wire Type Purpose Notes
5 f64 Unknown Often zero
5.1 f32 COG (radians) Course Over Ground
5.3 f32 Unknown Constant 0.05
5.4 f32 Unknown Constant 0.1
5.5 f32 SOG (m/s) Speed Over Ground
5.6 f32 Secondary angle Varies like COG
5.7 f32 Unknown Constant 11.93

Compass Heading ⚠️ VARIABLE

Location: Field 3.2

Field 3.2 (fixed32/float)  Heading in radians

# Conversion
heading_degrees = radians × 57.2957795131
heading_degrees = heading_degrees % 360

# Validation
0  radians  6.5 (approximately 0 to 2π)

Wind Data ⚠️ VARIABLE

Location: Field 13.4, 13.5, 13.6

Field 13.4 (fixed32/float)  True Wind Direction (radians)
Field 13.5 (fixed32/float)  True Wind Speed (m/s)
Field 13.6 (fixed32/float)  Apparent Wind Speed (m/s)

# Conversions
direction_deg = radians × 57.2957795131
speed_kts = speed_ms × 1.94384449

# Validation
0  angle  6.5 (radians)
0  speed  100 (m/s)

Depth ⚠️ VARIABLE

Location: Field 7.1 (only in larger packets 1472B+)

Field 7.1 (fixed32/float)  Depth in meters

# Conversion
depth_feet = depth_meters / 0.3048

# Validation
0 < depth  1000 (meters)

Barometric Pressure RELIABLE

Location: Field 15.1

Field 15.1 (fixed32/float)  Barometric Pressure (Pascals)

# Conversion
pressure_mbar = pressure_pa * 0.01
pressure_inhg = pressure_mbar * 0.02953

# Validation
87000  value  108400 Pa (870-1084 mbar)

Temperature RELIABLE

Location: Field 15.3 (air), Field 15.9 (water)

Field 15.3 (fixed32/float)  Air Temperature (Kelvin)
Field 15.9 (fixed32/float)  Water Temperature (Kelvin)

# Conversion
temp_celsius = temp_kelvin - 273.15
temp_fahrenheit = temp_celsius × 9/5 + 32

# Validation
Air:   200  value  350 K (-73°C to 77°C)
Water: 270  value  320 K (-3°C to 47°C)

Tank Levels RELIABLE

Location: Field 16 (repeated)

Field 16 (repeated messages):
  Field 1 (varint)  Tank ID
  Field 2 (varint)  Status flag
  Field 3 (fixed32/float)  Level percentage

# Validation
0  level  100 (percentage)

Tank ID Mapping:

ID Name Capacity Notes
1 Starboard Fuel 265 gal Has explicit ID
2 Port Fuel 265 gal Inferred (no ID, no status)
10 Forward Water 90 gal
11 Aft Water 90 gal
100 Black Water 53 gal Inferred (status=5)

Inference Logic:

if tank_id is None:
    if status == 5:
        tank_id = 100  # Black/waste water
    elif status is None:
        tank_id = 2    # Port Fuel (only tank with no ID or status)

House Batteries RELIABLE

Location: Field 20 (repeated)

Field 20 (repeated messages):
  Field 1 (varint)  Battery ID
  Field 3 (fixed32/float)  Voltage (volts)

# Validation
10  voltage  60 (covers 12V, 24V, 48V systems)

Battery ID Mapping:

ID Name Expected Voltage
11 Aft House ~26.3V (24V system)
13 Stern House ~27.2V (24V system)

Engine Batteries RELIABLE

Location: Field 14.3.4 (deep nested - 3 levels)

Field 14 (repeated messages):
  Field 1 (varint)  Engine ID (0=Port, 1=Starboard)
  Field 3 (length/nested message):
    Field 4 (fixed32/float)  Battery voltage (volts)

# Extraction requires parsing Field 14.3 as nested protobuf
# to extract Field 4 (voltage)

# Battery ID calculation
battery_id = 1000 + engine_id
# Port Engine = 1000, Starboard Engine = 1001

# Validation
10  voltage  60 (volts)

Engine Battery Mapping:

Engine ID Battery ID Name
0 1000 Port Engine
1 1001 Starboard Engine

Technical Challenges

1. No Schema Available

Protocol Buffers normally use a .proto schema file to define message structure. Without Raymarine's proprietary schema, we cannot:

  • Know message type identifiers
  • Understand field semantics
  • Differentiate between message types

2. Field Number Collision

The same protobuf field number means different things in different message types:

  • Field 4 at one offset might be wind speed
  • Field 4 at another offset might be something else entirely

3. Variable Packet Structure

Packets of different sizes have completely different internal layouts:

  • GPS appears at offset ~0x0032 in large packets
  • Sensor data appears at different offsets depending on packet size
  • Nested submessages add complexity

4. No Message Type Markers

Unlike some protocols, there's no obvious message type identifier in the packet header that would allow us to switch parsing logic based on message type.

5. Mixed Precision

Some values use 64-bit doubles, others use 32-bit floats. Both can appear in the same packet, and the same logical value (e.g., an angle) might be encoded differently in different message types.


Option 1: GPS-Anchored Parsing

  1. Find GPS using the reliable 0x09/0x11 pattern
  2. Use GPS offset as anchor point
  3. Extract values at fixed byte offsets relative to GPS
  4. Maintain separate offset tables for each packet size

Option 2: Packet Size Dispatch

  1. Identify packet by size
  2. Apply size-specific parsing rules
  3. Use absolute byte offsets (not field numbers)
  4. Maintain a mapping table: (packet_size, offset) → sensor_type

Option 3: Value Correlation

  1. Collect all extracted values
  2. Compare against known ground truth (displayed values on MFD)
  3. Use statistical correlation to identify correct mappings
  4. Build confidence scores for each mapping

Tools Included

Main Decoders

Tool Purpose
protobuf_decoder.py Primary decoder - all fields via proper protobuf parsing
raymarine_decoder.py High-level decoder with live dashboard display

Discovery & Debug Tools

Tool Purpose
battery_debug.py Deep nesting parser for Field 14.3.4 (engine batteries)
battery_finder.py Scans multicast groups for voltage-like values
tank_debug.py Raw Field 16 entry inspection
tank_finder.py Searches for tank level percentages
field_debugger.py Deep analysis of packet fields

Analysis Tools

Tool Purpose
analyze_structure.py Packet structure analysis
field_mapping.py Documents the protobuf structure
protobuf_parser.py Lower-level wire format decoder
watch_field.py Monitor specific field values over time

Wind/Heading Finders

Tool Purpose
wind_finder.py Searches for wind speed values
find_twd.py Searches for true wind direction
find_heading_vs_twd.py Compares heading and TWD values
find_consistent_heading.py Identifies stable heading fields

Usage

# Run the primary protobuf decoder (live network)
python protobuf_decoder.py -i YOUR_VLAN_IP

# JSON output for integration
python protobuf_decoder.py -i YOUR_VLAN_IP --json

# Decode from pcap file (offline analysis)
python protobuf_decoder.py --pcap raymarine_sample.pcap

# Debug battery extraction
python battery_debug.py --pcap raymarine_sample.pcap

# Debug tank data
python tank_debug.py --pcap raymarine_sample.pcap

Replace YOUR_VLAN_IP with your interface IP on the Raymarine VLAN (e.g., 198.18.5.5).

No external dependencies required - uses only Python standard library.


Sample Output

============================================================
  RAYMARINE DECODER (Protobuf)                    17:36:01
============================================================
  GPS:     24.932652, -80.627569
  Heading: 35.2°
  Wind:    14.6 kts @ 68.5° (true)
  Depth:   7.5 ft (2.3 m)
  Temp:    Air 24.8°C / 76.6°F, Water 26.2°C / 79.2°F
  Tanks:   Stbd Fuel: 75.2% (199gal), Port Fuel: 68.1% (180gal), ...
  Batts:   Aft House: 26.3V, Stern House: 27.2V, Port Engine: 26.5V
------------------------------------------------------------
  Packets: 4521  Decoded: 4312  Uptime: 85.2s
============================================================

JSON Output

{
  "timestamp": "2025-12-23T17:36:01.123456",
  "position": {"latitude": 24.932652, "longitude": -80.627569},
  "navigation": {"heading_deg": 35.2, "cog_deg": null, "sog_kts": null},
  "wind": {"true_direction_deg": 68.5, "true_speed_kts": 14.6, ...},
  "depth": {"feet": 7.5, "meters": 2.3},
  "temperature": {"water_c": 26.2, "air_c": 24.8},
  "tanks": {
    "1": {"name": "Stbd Fuel", "level_pct": 75.2, "capacity_gal": 265},
    "2": {"name": "Port Fuel", "level_pct": 68.1, "capacity_gal": 265}
  },
  "batteries": {
    "11": {"name": "Aft House", "voltage_v": 26.3},
    "13": {"name": "Stern House", "voltage_v": 27.2},
    "1000": {"name": "Port Engine", "voltage_v": 26.5}
  }
}

Future Work

  1. SOG/COG extraction DONE - Field 5.5 (SOG) and Field 5.1 (COG) identified
  2. Apparent Wind Angle - AWA field location to be confirmed
  3. Additional engine data - RPM, fuel flow, oil pressure likely in Field 14
  4. Field 5.3, 5.4, 5.7 - Unknown constants (0.05, 0.1, 11.93) - purpose TBD
  5. Investigate SignalK - The MFDs expose HTTP on port 8080 which may provide a cleaner API
  6. NMEA TCP/UDP - Check if standard NMEA is available on other ports (10110, 2000, etc.)

References


License

This reverse-engineering effort is for personal/educational use. The Raymarine protocol is proprietary.