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
153 lines
4.1 KiB
Python
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")
|