Source code for Expedition.dll_wrapper

import ctypes
import os
from ctypes import POINTER, c_bool, c_char_p, c_double, c_int, c_ubyte, c_uint16
from datetime import datetime
from typing import Dict, List, Optional, Tuple, Union

from .enums import SysBooleanVar, SysVar, Var

EXPEDITION_DLL_REG_KEY = r"SOFTWARE\Expedition\Core"

# Boat index used when reading MagVar via set_boat_position
# boat 0 cannot always read Lat/Lon due to GPS source selection
_VARIATION_SCRATCH_BOAT = 2
_OLE_DATE_EPOCH = datetime(1899, 12, 30)


def _datetime_to_ole_date(dt: datetime) -> float:
    """OLE DATE: days since 1899-12-30 (see ExpDLL.h GetVariation DATE utc)."""
    if dt.tzinfo is not None:
        dt = dt.astimezone().replace(tzinfo=None)
    delta = dt - _OLE_DATE_EPOCH
    return delta.days + (delta.seconds + delta.microseconds / 1e6) / 86400.0


__all__ = ["ExpeditionDLL"]


def get_expedition_location():
    import winreg

    key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, EXPEDITION_DLL_REG_KEY)
    value, _ = winreg.QueryValueEx(key, "Location")
    return value


[docs] class ExpeditionDLL: """ Wrapper for the Expedition DLL This class provides a Python interface to the Expedition DLL. """
[docs] @staticmethod def from_default_location(): """ Create an instance of the ExpeditionDLL class using the default installation location :return: an instance of the ExpeditionDLL class """ expedition_location = get_expedition_location() return ExpeditionDLL(expedition_location)
def __init__(self, exp_install_dir): """ Create an instance of the ExpeditionDLL class :param exp_install_dir: The directory where Expedition is installed (e.g. C:\\Program Files (x86)\\Expedition) """ self.exp_install_dir = exp_install_dir dll_name = "ExpDLL.dll" dll_path = os.path.join(exp_install_dir, dll_name) if not os.path.exists(dll_path): raise FileNotFoundError(f"Could not find {dll_name} in {exp_install_dir}") self.exp_dll = ctypes.windll.LoadLibrary(dll_path) # Define return types and argument types for the functions self.exp_dll.GetExpVarNum.argtypes = [] self.exp_dll.GetExpVarNum.restype = c_int self.exp_dll.GetExpVarName.argtypes = [c_uint16, POINTER(ctypes.c_char)] self.exp_dll.SetExpUserVarName.argtypes = [c_uint16, c_char_p] self.exp_dll.SetVarPrecision.argtypes = [c_uint16, c_uint16] self.exp_dll.GetVarPrecision.argtypes = [c_uint16, POINTER(c_uint16)] self.exp_dll.SetExpVar.argtypes = [c_uint16, c_double, c_uint16] self.exp_dll.GetExpVar.argtypes = [c_uint16, POINTER(c_double), c_uint16, POINTER(c_uint16)] self.exp_dll.GetExpVar.restype = c_bool self.exp_dll.SetExpVars.argtypes = [ POINTER(c_uint16), POINTER(c_double), c_uint16, c_uint16, ] self.exp_dll.GetExpVars.argtypes = [ POINTER(c_uint16), POINTER(c_double), c_uint16, c_uint16, ] self.exp_dll.GetExpVars.restype = c_bool self.exp_dll.GetSysVar.argtypes = [c_uint16, POINTER(c_double)] self.exp_dll.GetSysVar.restype = c_bool self.exp_dll.GetSysBool.argtypes = [c_uint16, POINTER(c_bool)] self.exp_dll.GetSysBool.restype = c_bool self.exp_dll.GetBoatNum.argtypes = [] self.exp_dll.GetBoatNum.restype = c_int self.exp_dll.SetBoatName.argtypes = [c_uint16, c_char_p] self.exp_dll.GetBoatColour.argtypes = [ c_uint16, POINTER(c_ubyte), POINTER(c_ubyte), POINTER(c_ubyte), ] self.exp_dll.SetBoatColour.argtypes = [c_uint16, c_ubyte, c_ubyte, c_ubyte] self.exp_dll.GetVariation.argtypes = [ ctypes.c_double, c_double, c_double, POINTER(c_double), ] self.exp_dll.GetVariation.restype = c_bool self.exp_dll.GetAisDangerousCPA.argtypes = [ POINTER(c_bool), POINTER(ctypes.c_wchar), c_uint16, ] self.exp_dll.SetMOB.argtypes = [c_double, c_double] self.exp_dll.PingMark.argtypes = [c_char_p, c_double, c_double, c_bool] self.exp_dll.CreateActiveRoute.argtypes = [c_char_p, c_bool] self.exp_dll.AddMarkToActiveRoute.argtypes = [c_char_p, c_double, c_double, c_bool, c_bool] # Now you can define Python functions that wrap the DLL functions @property def number_of_vars(self) -> int: """ Get the number of Expedition variables :return: The number of Expedition variables """ return int(self.exp_dll.GetExpVarNum())
[docs] def get_exp_var_name(self, var: Var) -> str: """ Get the name of an Expedition variable :param var: enumeration of the variable :return: the name of the variable """ name = ctypes.create_string_buffer(16) self.exp_dll.GetExpVarName(c_uint16(int(var)), name) return name.value.decode("utf-8")
[docs] def set_exp_user_var_name(self, var: Var, name): """ Set the name of a user variable :param var: enumeration of the variable :param name: the name of the variable :return: None """ # name must be a string of length 16 or less if len(name) > 16: raise ValueError("name must be 16 characters or less") if (0 <= var <= 31) or (Var.User0 <= var <= Var.User31): self.exp_dll.SetExpUserVarName(c_uint16(int(var)), name.encode("utf-8")) else: raise ValueError("var must be between 0 and 31 or between Var.User0 and Var.User31")
[docs] def set_var_precision(self, var: Var, precision: int): """ Set precision for a user variable. :param var: variable id (0-31 or Var.User0-Var.User31) :param precision: decimal precision :return: None """ if (0 <= var <= 31) or (Var.User0 <= var <= Var.User31): self.exp_dll.SetVarPrecision(c_uint16(int(var)), c_uint16(precision)) else: raise ValueError("var must be between 0 and 31 or between Var.User0 and Var.User31")
[docs] def get_var_precision(self, var: Var) -> int: """ Get precision for a user variable. :param var: variable id (0-31 or Var.User0-Var.User31) :return: precision value """ if not ((0 <= var <= 31) or (Var.User0 <= var <= Var.User31)): raise ValueError("var must be between 0 and 31 or between Var.User0 and Var.User31") precision = c_uint16() self.exp_dll.GetVarPrecision(c_uint16(int(var)), ctypes.byref(precision)) return int(precision.value)
[docs] def set_exp_var_value(self, var: Var, value: float, boat=0): """ Set the value of an Expedition variable :param var: enumeration of the variable :param value: the value to set :param boat: the boat number to set the variable for (default 0) """ self.exp_dll.SetExpVar(c_uint16(int(var)), c_double(value), c_uint16(boat))
[docs] def set_exp_var_by_name(self, name: str, value: float, boat=0): """ Set the value of an Expedition variable by name :param name: the name of the variable :param value: the value to set :param boat: the boat number to set the variable for (default 0) """ try: var_id = Var[name] except KeyError: raise ValueError(f"Variable name '{name}' not found") self.exp_dll.SetExpVar(c_uint16(int(var_id)), c_double(value), c_uint16(boat))
[docs] def get_exp_var_value(self, var: Var, boat=0) -> Optional[float]: """ Get the value of an Expedition variable :param var: enumeration of the variable :param boat: the boat number to get the variable for (default 0) :return: value of the variable """ value = c_double() id_alt = c_uint16() valid = self.exp_dll.GetExpVar( c_uint16(int(var)), ctypes.byref(value), c_uint16(boat), ctypes.byref(id_alt) ) if valid: return value.value else: return None
[docs] def get_exp_var_value_by_name(self, name: str, boat=0): """ Get the value of an Expedition variable by name :param name: the name of the variable :param boat: the boat number to get the variable for (default 0) :return: value of the variable """ var_id = Var[name] value = c_double() id_alt = c_uint16() valid = self.exp_dll.GetExpVar( c_uint16(int(var_id)), ctypes.byref(value), c_uint16(boat), ctypes.byref(id_alt) ) if valid: return value.value return None
[docs] def set_exp_vars(self, var_list: List[Var], value_list: List[float], boat=0): """ Set the values of a list of Expedition variables :param var_list: list of variables to set :param value_list: values to set :param boat: boat number to set the variables for (default 0) :return: None """ if len(var_list) != len(value_list): raise ValueError("vars and values must be the same length") var_array = (c_uint16 * len(var_list))(*(int(v) for v in var_list)) value_array = (c_double * len(value_list))(*value_list) self.exp_dll.SetExpVars(var_array, value_array, c_uint16(len(var_list)), c_uint16(boat))
[docs] def get_exp_vars(self, var_list: List[Var], boat=0) -> Optional[List[float]]: """ Get the values of a list of Expedition variables :param var_list: list of variables to get :param boat: boat number to get the variables for (default 0) :return: list of values """ var_array = (c_uint16 * len(var_list))(*(int(v) for v in var_list)) value_array = (c_double * len(var_list))() valid = self.exp_dll.GetExpVars( var_array, value_array, c_uint16(len(var_list)), c_uint16(boat) ) if valid: return list(value_array) else: return None
[docs] def set_exp_vars_dict(self, var_dict: Dict[Var, float], boat=0): """ Set the values of a dictionary of Expedition variables :param var_dict: dictionary of variables to set :param boat: boat number to set the variables for (default 0) :return: None """ var_list = list(var_dict.keys()) value_list = list(var_dict.values()) self.set_exp_vars(var_list, value_list, boat)
[docs] def get_exp_vars_dict(self, var_list: List[Var], boat=0) -> Optional[Dict[Var, float]]: """ Get the values of a list of Expedition variables :param var_list: list of variables to get :param boat: boat number to get the variables for (default 0) :return: dictionary of values """ values = self.get_exp_vars(var_list, boat) if values: return dict(zip(var_list, values)) else: return None
[docs] def get_sys_var(self, var: SysVar) -> Optional[float]: """ Get the value of a system variable :param var: system variable enumeration :return: value """ value = c_double() valid = self.exp_dll.GetSysVar(c_uint16(int(var)), ctypes.byref(value)) if valid: return value.value else: return None
[docs] def get_sys_bool(self, var: SysBooleanVar) -> Optional[bool]: """ Get the value of a system boolean variable :param var: system boolean variable enumeration :return: value """ value = c_bool() valid = self.exp_dll.GetSysBool(c_uint16(int(var)), ctypes.byref(value)) if valid: return value.value else: return None
[docs] def get_number_of_boats(self) -> int: """ Get the number of boats in the Expedition :return: maximum boat number """ return int(self.exp_dll.GetBoatNum())
[docs] def set_boat_name(self, boat: int, name: str): """ Set the name of a boat :param boat: :param name: :return: """ if len(name) > 32: raise ValueError("name must be 32 characters or less") self.exp_dll.SetBoatName(c_uint16(boat), name.encode("utf-8"))
[docs] def get_boat_colour(self, boat: int) -> Tuple[int, int, int]: """ Get the colour of a boat :param boat: :return: r, g, b colour values """ r = c_ubyte() g = c_ubyte() b = c_ubyte() self.exp_dll.GetBoatColour( c_uint16(boat), ctypes.byref(r), ctypes.byref(g), ctypes.byref(b) ) return r.value, g.value, b.value
[docs] def set_boat_colour(self, boat: int, r: int, g: int, b: int): """ Set the colour of a boat :param boat: :param r: red :param g: green :param b: blue :return: """ self.exp_dll.SetBoatColour(c_uint16(boat), c_ubyte(r), c_ubyte(g), c_ubyte(b))
[docs] def get_ais_dangerous_cpa(self) -> Tuple[bool, str]: """ Get dangerous AIS CPA state and target name. :return: tuple (dangerous, target_name) """ dangerous = c_bool() name = ctypes.create_unicode_buffer(32) self.exp_dll.GetAisDangerousCPA(ctypes.byref(dangerous), name, c_uint16(len(name))) return bool(dangerous.value), name.value
[docs] def set_boat_position(self, boat: int, lat_lon: Tuple[float, float]): """ Set the position of a boat :param boat: the number of the boat :param lat_lon: tuple of latitude and longitude :return: """ self.set_exp_vars([Var.Lat, Var.Lon], list(lat_lon), boat)
[docs] def get_boat_position(self, boat: int) -> Union[Tuple[float, float], None]: """ Get the position of a boat :param boat: the number of the boat :return: tuple of latitude and longitude """ values = self.get_exp_vars([Var.Lat, Var.Lon], boat) if values is None: return None if len(values) == 2 and all(isinstance(x, float) for x in values): return values[0], values[1] else: return None
[docs] def set_mob(self, lat: float, lon: float): """ Set the MOB position :param lat: latitude :param lon: longitude :return: """ self.exp_dll.SetMOB(c_double(lat), c_double(lon))
[docs] def ping_mark(self, name: str, lat: float, lon: float, save: bool): """ Ping a mark :param name: name of the mark :param lat: latitude :param lon: longitude :param save: save the mark :return: """ self.exp_dll.PingMark(name.encode("utf-8"), c_double(lat), c_double(lon), c_bool(save))
[docs] def create_active_route(self, name: str, save: bool = True): """ Create an active route :param name: route name :param save: whether to save (default True) :return: """ self.exp_dll.CreateActiveRoute(name.encode("utf-8"), c_bool(save))
[docs] def add_mark_to_active_route( self, name: str, lat: float, lon: float, locked: bool = True, save: bool = True ): """ Add a mark to the active route :param name: mark name :param lat: latitude :param lon: longitude :param locked: locked (default True) :param save: whether to save (default True) :return: """ self.exp_dll.AddMarkToActiveRoute( name.encode("utf-8"), c_double(lat), c_double(lon), c_bool(locked), c_bool(save) )
[docs] def get_variation( self, lat: float, lon: float, date: Optional[datetime] = None ) -> Optional[float]: """ Get magnetic variation (degrees) at a position. Uses ExpDLL GetVariation (OLE DATE utc per ExpDLL.h). If that fails, reads Var.MagVar after setting position on a scratch boat (Expedition must be running). """ if date is None: date = datetime.now() ole_date = _datetime_to_ole_date(date) variation = c_double() success = self.exp_dll.GetVariation( c_double(ole_date), c_double(lat), c_double(lon), ctypes.byref(variation) ) if success: return variation.value boat = _VARIATION_SCRATCH_BOAT prev_position = self.get_boat_position(boat) try: self.set_boat_position(boat, (lat, lon)) magvar = self.get_exp_var_value(Var.MagVar, boat) finally: if prev_position is not None: self.set_boat_position(boat, prev_position) return magvar