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

153 lines
4.1 KiB
Python

"""
Base class for NMEA 0183 sentences.
NMEA 0183 Sentence Format:
$XXYYY,field1,field2,...,fieldN*CC<CR><LF>
Where:
$ = Start delimiter
XX = Talker ID (e.g., GP, II, WI)
YYY = Sentence type (e.g., GGA, RMC)
, = Field delimiter
* = Checksum delimiter
CC = Two-digit hex checksum (XOR of all chars between $ and *)
<CR><LF> = Carriage return and line feed
"""
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional
class NMEASentence(ABC):
"""Abstract base class for NMEA 0183 sentences.
Subclasses must implement:
- sentence_type: The 3-character sentence type (e.g., "GGA")
- format_fields(): Returns the comma-separated field data
Example:
class GGASentence(NMEASentence):
sentence_type = "GGA"
def format_fields(self) -> str:
return "123456.00,2456.123,N,08037.456,W,1,08,0.9,..."
"""
# Talker ID for this sentence (default: II for integrated instrumentation)
talker_id: str = "II"
# Sentence type (e.g., "GGA", "RMC")
sentence_type: str = ""
@abstractmethod
def format_fields(self) -> Optional[str]:
"""Format the sentence fields.
Returns:
Comma-separated field string, or None if sentence cannot be generated
"""
pass
@staticmethod
def calculate_checksum(sentence: str) -> str:
"""Calculate NMEA checksum.
The checksum is the XOR of all characters between $ and *.
Args:
sentence: The sentence content (without $ prefix and * suffix)
Returns:
Two-character hex checksum
"""
checksum = 0
for char in sentence:
checksum ^= ord(char)
return f"{checksum:02X}"
def to_nmea(self) -> Optional[str]:
"""Generate the complete NMEA sentence.
Returns:
Complete NMEA sentence with $ prefix, checksum, and CRLF,
or None if sentence cannot be generated
"""
fields = self.format_fields()
if fields is None:
return None
# Build sentence content (between $ and *)
content = f"{self.talker_id}{self.sentence_type},{fields}"
# Calculate checksum
checksum = self.calculate_checksum(content)
# Return complete sentence
return f"${content}*{checksum}\r\n"
def __str__(self) -> str:
"""Return the NMEA sentence as a string."""
result = self.to_nmea()
return result if result else ""
@staticmethod
def format_latitude(lat: float) -> str:
"""Format latitude for NMEA (DDMM.MMMMM,N/S).
Args:
lat: Latitude in decimal degrees (-90 to 90)
Returns:
Formatted string like "2456.12345,N"
"""
hemisphere = 'N' if lat >= 0 else 'S'
lat = abs(lat)
degrees = int(lat)
minutes = (lat - degrees) * 60
return f"{degrees:02d}{minutes:09.6f},{hemisphere}"
@staticmethod
def format_longitude(lon: float) -> str:
"""Format longitude for NMEA (DDDMM.MMMMM,E/W).
Args:
lon: Longitude in decimal degrees (-180 to 180)
Returns:
Formatted string like "08037.45678,W"
"""
hemisphere = 'E' if lon >= 0 else 'W'
lon = abs(lon)
degrees = int(lon)
minutes = (lon - degrees) * 60
return f"{degrees:03d}{minutes:09.6f},{hemisphere}"
@staticmethod
def format_time(dt: Optional[datetime] = None) -> str:
"""Format time for NMEA (HHMMSS.SS).
Args:
dt: Datetime object, or None for current time
Returns:
Formatted string like "123456.00"
"""
if dt is None:
dt = datetime.utcnow()
return dt.strftime("%H%M%S.00")
@staticmethod
def format_date(dt: Optional[datetime] = None) -> str:
"""Format date for NMEA (DDMMYY).
Args:
dt: Datetime object, or None for current time
Returns:
Formatted string like "231224"
"""
if dt is None:
dt = datetime.utcnow()
return dt.strftime("%d%m%y")