Files
venus/axiom-nmea/debug/pressure_finder.py
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

347 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Pressure Finder - Locate barometric pressure data in Raymarine protobuf stream.
Scans for float values that could be pressure in various units.
Uses known pressure value to correlate field locations.
Usage:
python pressure_finder.py -i YOUR_INTERFACE_IP -p 1021 # Known pressure in mbar
"""
import struct
import socket
import time
import argparse
from typing import Dict, List, Any, Optional
from collections import defaultdict
WIRE_VARINT = 0
WIRE_FIXED64 = 1
WIRE_LENGTH = 2
WIRE_FIXED32 = 5
HEADER_SIZE = 20
MULTICAST_GROUPS = [
("226.192.206.102", 2565), # Main sensor data
("239.2.1.1", 2154), # May contain additional data
]
class ProtobufParser:
"""Parse protobuf without schema."""
def __init__(self, data: bytes):
self.data = data
self.pos = 0
def remaining(self):
return len(self.data) - self.pos
def read_varint(self) -> int:
result = 0
shift = 0
while self.pos < len(self.data):
byte = self.data[self.pos]
self.pos += 1
result |= (byte & 0x7F) << shift
if not (byte & 0x80):
break
shift += 7
return result
def parse_nested_deep(self, data: bytes, path: str = "", depth: int = 0, max_depth: int = 5) -> List[tuple]:
"""Recursively parse nested protobuf and return list of (path, type, value) tuples."""
results = []
pos = 0
if depth > max_depth:
return results
while pos < len(data):
if pos >= len(data):
break
try:
# Read tag
tag_byte = data[pos]
pos += 1
# Handle multi-byte varints for tag
tag = tag_byte & 0x7F
shift = 7
while tag_byte & 0x80 and pos < len(data):
tag_byte = data[pos]
pos += 1
tag |= (tag_byte & 0x7F) << shift
shift += 7
field_num = tag >> 3
wire_type = tag & 0x07
if field_num == 0 or field_num > 100:
break
field_path = f"{path}.{field_num}" if path else str(field_num)
if wire_type == WIRE_VARINT:
val = 0
shift = 0
while pos < len(data):
byte = data[pos]
pos += 1
val |= (byte & 0x7F) << shift
if not (byte & 0x80):
break
shift += 7
results.append((field_path, 'varint', val))
elif wire_type == WIRE_FIXED32:
raw = data[pos:pos + 4]
pos += 4
try:
f = struct.unpack('<f', raw)[0]
if f == f: # not NaN
results.append((field_path, 'float', f))
except:
pass
elif wire_type == WIRE_FIXED64:
raw = data[pos:pos + 8]
pos += 8
try:
d = struct.unpack('<d', raw)[0]
if d == d: # not NaN
results.append((field_path, 'double', d))
except:
pass
elif wire_type == WIRE_LENGTH:
length = 0
shift = 0
while pos < len(data):
byte = data[pos]
pos += 1
length |= (byte & 0x7F) << shift
if not (byte & 0x80):
break
shift += 7
raw = data[pos:pos + length]
pos += length
# Try to parse as nested message
if len(raw) >= 2:
nested_results = self.parse_nested_deep(raw, field_path, depth + 1, max_depth)
if nested_results:
results.extend(nested_results)
else:
break
except Exception:
break
return results
def is_pressure_like(val: float, target_mbar: float) -> Optional[str]:
"""Check if a float value could be barometric pressure.
Checks multiple unit possibilities and returns match description.
"""
tolerance = 0.02 # 2% tolerance
# Convert target to various units
target_pa = target_mbar * 100 # Pascals (1021 mbar = 102100 Pa)
target_hpa = target_mbar # hPa = mbar
target_kpa = target_mbar / 10 # kPa (102.1)
target_bar = target_mbar / 1000 # bar (1.021)
target_inhg = target_mbar * 0.02953 # inHg (~30.15)
target_psi = target_mbar * 0.0145 # PSI (~14.8)
checks = [
(target_mbar, "mbar (direct)"),
(target_hpa, "hPa"),
(target_pa, "Pascals"),
(target_kpa, "kPa"),
(target_bar, "bar"),
(target_inhg, "inHg"),
(target_psi, "PSI"),
]
for target, unit in checks:
if target > 0 and abs(val - target) / target < tolerance:
return unit
return None
def scan_packet(data: bytes, target_mbar: float, group: str, port: int,
candidates: Dict[str, Dict]) -> None:
"""Scan packet for pressure-like values."""
if len(data) < HEADER_SIZE + 5:
return
proto_data = data[HEADER_SIZE:]
parser = ProtobufParser(proto_data)
# Get all fields recursively
all_fields = parser.parse_all_fields() if hasattr(parser, 'parse_all_fields') else {}
# Deep parse all length-delimited fields
parser = ProtobufParser(proto_data)
parser.pos = 0
all_results = parser.parse_nested_deep(proto_data, "")
for path, vtype, value in all_results:
if vtype in ('float', 'double'):
unit = is_pressure_like(value, target_mbar)
if unit:
if path not in candidates:
candidates[path] = {
'values': [],
'unit': unit,
'type': vtype,
'count': 0
}
candidates[path]['values'].append(value)
candidates[path]['count'] += 1
def parse_all_fields(self) -> Dict[int, List[Any]]:
"""Parse and collect all fields."""
fields = {}
while self.pos < len(self.data):
if self.remaining() < 1:
break
try:
tag = self.read_varint()
field_num = tag >> 3
wire_type = tag & 0x07
if field_num == 0 or field_num > 1000:
break
if wire_type == WIRE_VARINT:
value = ('varint', self.read_varint())
elif wire_type == WIRE_FIXED64:
raw = self.data[self.pos:self.pos + 8]
self.pos += 8
try:
d = struct.unpack('<d', raw)[0]
value = ('double', d)
except:
value = ('fixed64', raw.hex())
elif wire_type == WIRE_LENGTH:
length = self.read_varint()
raw = self.data[self.pos:self.pos + length]
self.pos += length
value = ('length', raw)
elif wire_type == WIRE_FIXED32:
raw = self.data[self.pos:self.pos + 4]
self.pos += 4
try:
f = struct.unpack('<f', raw)[0]
value = ('float', f)
except:
value = ('fixed32', raw.hex())
else:
break
if field_num not in fields:
fields[field_num] = []
fields[field_num].append(value)
except:
break
return fields
ProtobufParser.parse_all_fields = parse_all_fields
def main():
parser = argparse.ArgumentParser(description="Find barometric pressure field in Raymarine data")
parser.add_argument('-i', '--interface', required=True, help='Interface IP')
parser.add_argument('-p', '--pressure', type=float, required=True,
help='Known current pressure in mbar (e.g., 1021)')
parser.add_argument('-t', '--time', type=int, default=10, help='Capture time (seconds)')
parser.add_argument('-g', '--group', default="226.192.206.102", help='Multicast group')
parser.add_argument('--port', type=int, default=2565, help='UDP Port')
args = parser.parse_args()
print(f"Pressure Finder - Looking for {args.pressure} mbar")
print("=" * 60)
print(f"Target values to find:")
print(f" mbar/hPa: {args.pressure:.1f}")
print(f" Pascals: {args.pressure * 100:.0f}")
print(f" kPa: {args.pressure / 10:.2f}")
print(f" inHg: {args.pressure * 0.02953:.2f}")
print(f" bar: {args.pressure / 1000:.4f}")
print("=" * 60)
print(f"\nListening on {args.group}:{args.port} for {args.time} seconds...")
# Create socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, 'SO_REUSEPORT'):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('', args.port))
mreq = struct.pack("4s4s", socket.inet_aton(args.group), socket.inet_aton(args.interface))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
sock.settimeout(1.0)
candidates = {} # field_path -> info
packet_count = 0
end_time = time.time() + args.time
try:
while time.time() < end_time:
try:
data, _ = sock.recvfrom(65535)
packet_count += 1
scan_packet(data, args.pressure, args.group, args.port, candidates)
except socket.timeout:
continue
except KeyboardInterrupt:
pass
finally:
sock.close()
print(f"\n\nResults after scanning {packet_count} packets:")
print("=" * 60)
if not candidates:
print("No pressure-like values found!")
print("\nSuggestions:")
print("1. Verify the pressure sensor is connected and broadcasting")
print("2. Try different multicast groups (239.2.1.1:2154)")
print("3. Check if pressure is in a different packet size")
else:
print(f"\nFound {len(candidates)} candidate field(s):\n")
# Sort by count (most frequent first)
for path, info in sorted(candidates.items(), key=lambda x: -x[1]['count']):
values = info['values']
avg_val = sum(values) / len(values)
min_val = min(values)
max_val = max(values)
print(f"Field {path}:")
print(f" Type: {info['type']}")
print(f" Matches: {info['count']} packets")
print(f" Unit likely: {info['unit']}")
print(f" Values: min={min_val:.2f}, max={max_val:.2f}, avg={avg_val:.2f}")
if info['unit'] == 'Pascals':
print(f" As mbar: {avg_val/100:.1f} mbar")
elif info['unit'] == 'kPa':
print(f" As mbar: {avg_val*10:.1f} mbar")
print()
print("\nDone.")
if __name__ == "__main__":
main()