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
18 KiB
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:2563226.192.206.101:2564239.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.
Recommended Approach for Reliable Decoding
Option 1: GPS-Anchored Parsing
- Find GPS using the reliable
0x09/0x11pattern - Use GPS offset as anchor point
- Extract values at fixed byte offsets relative to GPS
- Maintain separate offset tables for each packet size
Option 2: Packet Size Dispatch
- Identify packet by size
- Apply size-specific parsing rules
- Use absolute byte offsets (not field numbers)
- Maintain a mapping table:
(packet_size, offset) → sensor_type
Option 3: Value Correlation
- Collect all extracted values
- Compare against known ground truth (displayed values on MFD)
- Use statistical correlation to identify correct mappings
- 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
SOG/COG extraction✅ DONE - Field 5.5 (SOG) and Field 5.1 (COG) identified- Apparent Wind Angle - AWA field location to be confirmed
- Additional engine data - RPM, fuel flow, oil pressure likely in Field 14
- Field 5.3, 5.4, 5.7 - Unknown constants (0.05, 0.1, 11.93) - purpose TBD
- Investigate SignalK - The MFDs expose HTTP on port 8080 which may provide a cleaner API
- NMEA TCP/UDP - Check if standard NMEA is available on other ports (10110, 2000, etc.)
References
- Protocol Buffers Encoding
- Raymarine LightHouse OS
- Test location: Florida Keys (24° 55' N, 80° 37' W)
License
This reverse-engineering effort is for personal/educational use. The Raymarine protocol is proprietary.