Files
venus/axiom-nmea/raymarine_nmea/nmea/sentences/transducer.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

216 lines
6.0 KiB
Python

"""
Transducer measurement NMEA sentences.
XDR - Transducer Measurements (generic)
Used for tanks, batteries, and other sensor data that doesn't have
a dedicated NMEA sentence type.
"""
from typing import Optional, List, Tuple
from ..sentence import NMEASentence
class XDRSentence(NMEASentence):
"""XDR - Transducer Measurements.
Format:
$IIXDR,T,D.D,U,N,T,D.D,U,N,...*CC
Each transducer reading has 4 fields:
1. Transducer type
2. Data value
3. Units
4. Transducer name/ID
Transducer Types:
A = Angular displacement (degrees)
C = Temperature (Celsius)
D = Depth (meters)
F = Frequency (Hz)
H = Humidity (percent)
I = Current (amps)
N = Force (Newtons)
P = Pressure (Pascals)
R = Flow Rate (liters/second)
S = Salinity (ppt)
T = Tachometer (RPM)
U = Volume (liters)
V = Voltage (volts)
G = Generic (no units)
Tank Level Examples:
$IIXDR,V,75.2,P,FUEL1,V,68.1,P,FUEL2*XX
(P = percent for tank levels)
Battery Examples:
$IIXDR,U,26.3,V,HOUSE1,U,27.2,V,HOUSE2*XX
(U is sometimes used for voltage, or V for volts unit)
Note:
XDR can contain multiple transducer readings in one sentence.
Maximum sentence length is 82 characters, so multiple XDR
sentences may be needed for many sensors.
"""
talker_id = "II"
sentence_type = "XDR"
def __init__(self, readings: Optional[List[Tuple[str, float, str, str]]] = None):
"""Initialize XDR sentence.
Args:
readings: List of (type, value, unit, name) tuples
Example: [("V", 75.2, "P", "FUEL1"), ("V", 26.3, "V", "HOUSE1")]
"""
self.readings = readings or []
def add_reading(
self,
transducer_type: str,
value: float,
unit: str,
name: str
) -> None:
"""Add a transducer reading.
Args:
transducer_type: Type code (e.g., "V" for volume, "U" for voltage)
value: Numeric value
unit: Unit code (e.g., "P" for percent, "V" for volts)
name: Transducer name/ID
"""
self.readings.append((transducer_type, value, unit, name))
def format_fields(self) -> Optional[str]:
"""Format XDR fields."""
if not self.readings:
return None
parts = []
for trans_type, value, unit, name in self.readings:
# Clean the name to be NMEA-safe (no commas, asterisks)
safe_name = name.replace(",", "_").replace("*", "_")
# Use 4 decimal places for pressure (bar ~1.0209), 1 for others
if trans_type == "P" and unit == "B":
parts.append(f"{trans_type},{value:.4f},{unit},{safe_name}")
else:
parts.append(f"{trans_type},{value:.1f},{unit},{safe_name}")
return ",".join(parts)
@classmethod
def for_tank(
cls,
tank_id: int,
level_pct: float,
name: Optional[str] = None
) -> 'XDRSentence':
"""Create XDR sentence for a tank level.
Args:
tank_id: Tank ID number
level_pct: Tank level in percent (0-100)
name: Tank name (uses ID if not provided)
Returns:
XDRSentence instance
"""
tank_name = name or f"TANK{tank_id}"
xdr = cls()
xdr.add_reading("V", level_pct, "P", tank_name)
return xdr
@classmethod
def for_battery(
cls,
battery_id: int,
voltage: float,
name: Optional[str] = None
) -> 'XDRSentence':
"""Create XDR sentence for a battery voltage.
Args:
battery_id: Battery ID number
voltage: Battery voltage in volts
name: Battery name (uses ID if not provided)
Returns:
XDRSentence instance
"""
battery_name = name or f"BATT{battery_id}"
xdr = cls()
xdr.add_reading("U", voltage, "V", battery_name)
return xdr
@classmethod
def for_tanks(
cls,
tanks: dict,
names: Optional[dict] = None
) -> 'XDRSentence':
"""Create XDR sentence for multiple tanks.
Args:
tanks: Dict of tank_id -> level_pct
names: Optional dict of tank_id -> name
Returns:
XDRSentence instance
"""
names = names or {}
xdr = cls()
for tank_id, level in sorted(tanks.items()):
tank_name = names.get(tank_id, f"TANK{tank_id}")
xdr.add_reading("V", level, "P", tank_name)
return xdr
@classmethod
def for_batteries(
cls,
batteries: dict,
names: Optional[dict] = None
) -> 'XDRSentence':
"""Create XDR sentence for multiple batteries.
Args:
batteries: Dict of battery_id -> voltage
names: Optional dict of battery_id -> name
Returns:
XDRSentence instance
"""
names = names or {}
xdr = cls()
for battery_id, voltage in sorted(batteries.items()):
battery_name = names.get(battery_id, f"BATT{battery_id}")
xdr.add_reading("U", voltage, "V", battery_name)
return xdr
@classmethod
def for_pressure(
cls,
pressure_mbar: float,
name: str = "Barometer"
) -> 'XDRSentence':
"""Create XDR sentence for barometric pressure.
Args:
pressure_mbar: Pressure in millibars (hPa)
name: Sensor name (default: "Barometer")
Returns:
XDRSentence instance
Note:
Pressure is output in bar (1 bar = 1000 mbar) as per NMEA convention.
Example: 1020.9 mbar = 1.0209 bar
Output: $IIXDR,P,1.0209,B,Barometer*XX
"""
xdr = cls()
# Convert mbar to bar (NMEA standard unit)
pressure_bar = pressure_mbar / 1000.0
xdr.add_reading("P", pressure_bar, "B", name)
return xdr