diff --git a/docs/_static/ssm_nmea0183_input.png b/docs/_static/ssm_nmea0183_input.png new file mode 100644 index 00000000..70e9297b Binary files /dev/null and b/docs/_static/ssm_nmea0183_input.png differ diff --git a/docs/_static/ssm_nmea0183_listeners.png b/docs/_static/ssm_nmea0183_listeners.png new file mode 100644 index 00000000..c92e7fe0 Binary files /dev/null and b/docs/_static/ssm_nmea0183_listeners.png differ diff --git a/docs/user_manual_setup.rst b/docs/user_manual_setup.rst index 272a1abb..927fc0eb 100644 --- a/docs/user_manual_setup.rst +++ b/docs/user_manual_setup.rst @@ -10,3 +10,4 @@ Setup user_manual_setup_settings user_manual_setup_sis_v4 user_manual_setup_sis_v5 + user_manual_setup_nmea0183 diff --git a/docs/user_manual_setup_nmea0183.rst b/docs/user_manual_setup_nmea0183.rst new file mode 100644 index 00000000..b08e4cab --- /dev/null +++ b/docs/user_manual_setup_nmea0183.rst @@ -0,0 +1,30 @@ +.. _nmea0183: + +Sound Speed Manager - NMEA-0183 interaction +=========================================== + +.. index:: NMEA-0183; + +A rudimentary NMEA-0183 listener can be used to capture the current location via a UDP broadcast of NMEA-0183 $--GGA or $--GLL sentences. This feature can be used to associate the current position to the sound speed profile formats that cannot store location information. + +Open in editing mode the Sound Speed Manager’s Setup Tab, then set the NMEA-0183 listen port the Listeners sub-tab (see :numref:`ssm_nmea0183_listeners_fig`). + +.. _ssm_nmea0183_listeners_fig: +.. figure:: ./_static/ssm_nmea0183_listeners.png + :width: 618px + :align: center + :alt: figure with nmea0183 settings part 1 + :figclass: align-center + + *Sound Speed Manager Setup Listeners* dialog, with the *Listen Port* setting and incoming NMEA-0183 data highlighted in red. + +Then, switch to the Input sub-tab (see :numref:`ssm_nmea0183_input_fig`) and select the True value for the Listen NMEA-0183 field. After a **restart**, the current position is displayed in the status bar. + +.. _ssm_nmea0183_input_fig: +.. figure:: ./_static/ssm_nmea0183_input.png + :width: 618px + :align: center + :alt: figure with nmea0183 settings part 2 + :figclass: align-center + + *Input tab* in the Sound Speed Manager’s Setup. diff --git a/hyo2/soundspeed/base/callbacks/abstract_callbacks.py b/hyo2/soundspeed/base/callbacks/abstract_callbacks.py index 6c0e84c9..800f982c 100644 --- a/hyo2/soundspeed/base/callbacks/abstract_callbacks.py +++ b/hyo2/soundspeed/base/callbacks/abstract_callbacks.py @@ -5,6 +5,7 @@ if TYPE_CHECKING: from hyo2.soundspeed.listener.sis.sis import Sis + from hyo2.soundspeed.listener.nmea.nmea import Nmea logger = logging.getLogger(__name__) @@ -15,6 +16,7 @@ class GeneralAbstractCallbacks(metaclass=ABCMeta): def __init__(self) -> None: self.sis_listener = None # type: Optional[Sis] + self.nmea_listener = None # type: Optional[Nmea] @abstractmethod def ask_number(self, title: Optional[str] = "", msg: Optional[str] = "Enter number", default: Optional[float] = 0.0, @@ -73,6 +75,11 @@ def ask_location_from_sis(self) -> bool: """Ask user for location""" pass + @abstractmethod + def ask_location_from_nmea(self) -> bool: + """Ask user for location""" + pass + @abstractmethod def ask_tss(self) -> Optional[float]: """Ask user for transducer sound speed""" diff --git a/hyo2/soundspeed/base/callbacks/cli_callbacks.py b/hyo2/soundspeed/base/callbacks/cli_callbacks.py index 8dee8f3f..019be685 100644 --- a/hyo2/soundspeed/base/callbacks/cli_callbacks.py +++ b/hyo2/soundspeed/base/callbacks/cli_callbacks.py @@ -170,6 +170,17 @@ def ask_location_from_sis(self) -> bool: return True return False + def ask_location_from_nmea(self) -> bool: + """Ask user whether retrieving location from NMEA-0183""" + bool_msg = "Geographic location required for pressure/depth conversion and atlas lookup.\n" \ + "Use geographic position from NMEA-0183?\'y' for yes, other inputs to enter position manually." + + raw = input(bool_msg) + # print(raw) + if (raw == "Y") or (raw == "y"): + return True + return False + def ask_tss(self) -> Optional[float]: """Ask user for transducer sound speed""" tss = 1500.0 diff --git a/hyo2/soundspeed/base/callbacks/fake_callbacks.py b/hyo2/soundspeed/base/callbacks/fake_callbacks.py index b17a58b1..25169359 100644 --- a/hyo2/soundspeed/base/callbacks/fake_callbacks.py +++ b/hyo2/soundspeed/base/callbacks/fake_callbacks.py @@ -55,6 +55,9 @@ def ask_directory(self, key_name: Optional[str] = None, default_path: Optional[s def ask_location_from_sis(self) -> bool: return True + def ask_location_from_nmea(self) -> bool: + return True + def ask_tss(self) -> Optional[float]: return 1500.0 diff --git a/hyo2/soundspeed/base/setup.py b/hyo2/soundspeed/base/setup.py index c39328ac..1f6d723d 100644 --- a/hyo2/soundspeed/base/setup.py +++ b/hyo2/soundspeed/base/setup.py @@ -65,6 +65,7 @@ def __init__(self, release_folder, use_setup_name=None): self.use_sis4 = None self.use_sis5 = None self.use_sippican = None + self.use_nmea = None self.use_mvp = None # output @@ -78,6 +79,9 @@ def __init__(self, release_folder, use_setup_name=None): # listeners - sippican self.sippican_listen_port = None self.sippican_listen_timeout = None + # listeners - nmea + self.nmea_listen_port = None + self.nmea_listen_timeout = None # listeners - mvp self.mvp_ip_address = None self.mvp_listen_port = None @@ -158,6 +162,7 @@ def load_from_db(self, db_path: Optional[str] = None): self.use_sis4 = db.use_sis4 self.use_sis5 = db.use_sis5 self.use_sippican = db.use_sippican + self.use_nmea = db.use_nmea self.use_mvp = db.use_mvp # output @@ -178,6 +183,10 @@ def load_from_db(self, db_path: Optional[str] = None): self.sippican_listen_port = db.sippican_listen_port self.sippican_listen_timeout = db.sippican_listen_timeout + # listeners - nmea + self.nmea_listen_port = db.nmea_listen_port + self.nmea_listen_timeout = db.nmea_listen_timeout + # listeners - mvp self.mvp_ip_address = db.mvp_ip_address self.mvp_listen_port = db.mvp_listen_port @@ -243,6 +252,7 @@ def save_to_db(self): db.use_sis4 = self.use_sis4 db.use_sis5 = self.use_sis5 db.use_sippican = self.use_sippican + db.use_nmea = self.use_nmea db.use_mvp = self.use_mvp # client list @@ -264,6 +274,10 @@ def save_to_db(self): db.sippican_listen_port = self.sippican_listen_port db.sippican_listen_timeout = self.sippican_listen_timeout + # listeners - nmea + db.nmea_listen_port = self.nmea_listen_port + db.nmea_listen_timeout = self.nmea_listen_timeout + # listeners - mvp db.mvp_ip_address = self.mvp_ip_address db.mvp_listen_port = self.mvp_listen_port @@ -322,6 +336,7 @@ def __repr__(self): msg += " \n" % self.use_sis4 msg += " \n" % self.use_sis5 msg += " \n" % self.use_sippican + msg += " \n" % self.use_nmea msg += " \n" % self.use_mvp msg += " \n" msg += " \n" @@ -334,6 +349,9 @@ def __repr__(self): msg += " \n" msg += " \n" % self.sippican_listen_port msg += " \n" % self.sippican_listen_timeout + msg += " \n" + msg += " \n" % self.nmea_listen_port + msg += " \n" % self.nmea_listen_timeout msg += " \n" msg += " \n" % self.mvp_ip_address msg += " \n" % self.mvp_listen_port diff --git a/hyo2/soundspeed/base/setup_db.py b/hyo2/soundspeed/base/setup_db.py index d2665c70..e4e27b67 100644 --- a/hyo2/soundspeed/base/setup_db.py +++ b/hyo2/soundspeed/base/setup_db.py @@ -464,6 +464,15 @@ def use_sippican(self): def use_sippican(self, value): self._setter_bool("use_sippican", value) + # --- use_nmea + @property + def use_nmea(self): + return self._getter_bool("use_nmea") + + @use_nmea.setter + def use_nmea(self, value): + self._setter_bool("use_nmea", value) + # --- use_mvp @property def use_mvp(self): @@ -527,6 +536,24 @@ def sippican_listen_timeout(self): def sippican_listen_timeout(self, value): self._setter_int("sippican_listen_timeout", value) + # --- nmea_listen_port + @property + def nmea_listen_port(self): + return self._getter_int("nmea_listen_port") + + @nmea_listen_port.setter + def nmea_listen_port(self, value): + self._setter_int("nmea_listen_port", value) + + # --- nmea_listen_timeout + @property + def nmea_listen_timeout(self): + return self._getter_int("nmea_listen_timeout") + + @nmea_listen_timeout.setter + def nmea_listen_timeout(self, value): + self._setter_int("nmea_listen_timeout", value) + # --- mvp_ip_address @property def mvp_ip_address(self): diff --git a/hyo2/soundspeed/base/setup_sql.py b/hyo2/soundspeed/base/setup_sql.py index 6e455c9b..07fccd3f 100644 --- a/hyo2/soundspeed/base/setup_sql.py +++ b/hyo2/soundspeed/base/setup_sql.py @@ -82,6 +82,7 @@ use_sis integer NOT NULL DEFAULT 1, use_sis5 integer NOT NULL DEFAULT 0, use_sippican integer NOT NULL DEFAULT 0, + use_nmea integer NOT NULL DEFAULT 0, use_mvp integer NOT NULL DEFAULT 0, /* listeners - sis4 */ sis_listen_port integer NOT NULL DEFAULT 16103, @@ -90,6 +91,9 @@ /* listeners - sippican */ sippican_listen_port integer NOT NULL DEFAULT 2002, sippican_listen_timeout integer NOT NULL DEFAULT 10, + /* listeners - nmea */ + nmea_listen_port integer NOT NULL DEFAULT 2006, + nmea_listen_timeout integer NOT NULL DEFAULT 10, /* listeners - mvp */ mvp_ip_address text NOT NULL DEFAULT "127.0.0.1", mvp_listen_port integer NOT NULL DEFAULT 2006, @@ -173,13 +177,13 @@ INSERT INTO general (id, setup_name, setup_status, use_woa09, use_woa13, use_woa18, use_rtofs, ssp_extension_source, ssp_salinity_source, ssp_temp_sal_source, ssp_up_or_down, rx_max_wait_time, - use_sis, use_sippican, use_mvp, sis_listen_port, sis_listen_timeout, - sis_auto_apply_manual_casts, sippican_listen_port, sippican_listen_timeout, mvp_ip_address, - mvp_listen_port, mvp_listen_timeout, mvp_transmission_protocol, mvp_format, mvp_winch_port, - mvp_fish_port, mvp_nav_port, mvp_system_port, mvp_sw_version, mvp_instrument_id, mvp_instrument, - server_source, server_apply_surface_sound_speed, current_project, custom_projects_folder, - custom_outputs_folder, custom_woa09_folder, custom_woa13_folder, noaa_tools, default_institution, - default_survey, default_vessel, auto_apply_default_metadata) + use_sis, use_sippican, use_nmea, use_mvp, sis_listen_port, sis_listen_timeout, + sis_auto_apply_manual_casts, sippican_listen_port, sippican_listen_timeout, nmea_listen_port, + nmea_listen_timeout, mvp_ip_address, mvp_listen_port, mvp_listen_timeout, mvp_transmission_protocol, + mvp_format, mvp_winch_port, mvp_fish_port, mvp_nav_port, mvp_system_port, mvp_sw_version, + mvp_instrument_id, mvp_instrument, server_source, server_apply_surface_sound_speed, current_project, + custom_projects_folder, custom_outputs_folder, custom_woa09_folder, custom_woa13_folder, noaa_tools, + default_institution, default_survey, default_vessel, auto_apply_default_metadata) SELECT id, setup_name, setup_status, CASE WHEN typeof(use_woa09) == 'text' THEN @@ -213,6 +217,11 @@ ELSE use_sippican END, + CASE WHEN typeof(use_nmea) == 'text' THEN + use_nmea == 'True' + ELSE + use_nmea + END, CASE WHEN typeof(use_mvp) == 'text' THEN use_mvp == 'True' ELSE @@ -225,6 +234,7 @@ sis_auto_apply_manual_casts END, sippican_listen_port, sippican_listen_timeout, + nmea_listen_port, nmea_listen_timeout, mvp_ip_address, mvp_listen_port, mvp_listen_timeout, mvp_transmission_protocol, mvp_format, mvp_winch_port, mvp_fish_port, mvp_nav_port, mvp_system_port, mvp_sw_version, mvp_instrument_id, mvp_instrument, server_source, diff --git a/hyo2/soundspeed/formats/nmea0183.py b/hyo2/soundspeed/formats/nmea0183.py new file mode 100644 index 00000000..41b4f423 --- /dev/null +++ b/hyo2/soundspeed/formats/nmea0183.py @@ -0,0 +1,94 @@ +import logging + +logger = logging.getLogger(__name__) + + +class Nmea0183Nav: + + def __init__(self, data): + + self.data = data + self.msg = None + + self.latitude = None + self.longitude = None + + self.parse() + + def parse(self) -> None: + self.msg = self.data.split(',') + + def __str__(self): + return "Latitude: {0}, Longitude: {1}\n".format(self.latitude, self.longitude) + + + +class Nmea0183GGA(Nmea0183Nav): + + def __init__(self, data): + super(Nmea0183GGA, self).__init__(data) + + self.timestamp = self.msg[1] + self.lat = self.msg[2] + self.lat_dir = self.msg[3] + self.lon = self.msg[4] + self.lon_dir = self.msg[5] + self.gps_qual = self.msg[6] + self.num_sats = self.msg[7] + self.horizontal_dil = self.msg[8] + self.altitude = self.msg[9] + self.altitude_units = self.msg[10] + self.geo_sep = self.msg[11] + self.geo_sep_units = self.msg[12] + self.age_gps_data = self.msg[13] + self.ref_station_id = self.msg[14] + + try: + self.latitude = int(self.lat[:2]) + float(self.lat[2:])/60. + if self.lat_dir == 'S': + self.latitude = -1.0 * self.latitude + #logger.debug("NMEA-0183 $$GGA lat: {}".format(self.latitude)) + except Exception as e: + logger.warning("unable to interpret latitude from {0} and {1}, {2}".format(self.lat, self.lat_dir, e)) + + try: + self.longitude = int(self.lon[:3]) + float(self.lon[3:])/60. + if self.lon_dir == 'W': + self.longitude = -1.0 * self.longitude + #logger.debug("NMEA-0183 $$GGA lon: {}".format(self.longitude)) + except Exception as e: + logger.warning("unable to interpret longitude from {0} and {1}, {2}".format(self.lon, self.lon_dir, e)) + + + + +class Nmea0183GLL(Nmea0183Nav): + + def __init__(self, data): + super(Nmea0183GLL, self).__init__(data) + + self.msg = self.data.split(',') + + self.lat = self.msg[1] + self.lat_dir = self.msg[2] + self.lon = self.msg[3] + self.lon_dir = self.msg[4] + self.timestamp = self.msg[5] + self.status = self.msg[6] + + try: + self.latitude = int(self.lat[:2]) + float(self.lat[2:])/60. + if self.lat_dir == 'S': + self.latitude = -1.0 * self.latitude + #logger.debug("NMEA-0183 $$GLL lat: {}".format(self.latitude)) + except Exception as e: + logger.warning("unable to interpret latitude from {0} and {1}, {2}".format(self.lat, self.lat_dir, e)) + + try: + self.longitude = int(self.lon[:3]) + float(self.lon[3:])/60. + if self.lon_dir == 'W': + self.longitude = -1.0 * self.longitude + #logger.debug("NMEA-0183 $$GLL lon: {}".format(self.longitude)) + except Exception as e: + logger.warning("unable to interpret longitude from {0} and {1}, {2}".format(self.lon, self.lon_dir, e)) + diff --git a/hyo2/soundspeed/listener/listeners.py b/hyo2/soundspeed/listener/listeners.py index 5c2bb08a..fe3af6a8 100644 --- a/hyo2/soundspeed/listener/listeners.py +++ b/hyo2/soundspeed/listener/listeners.py @@ -4,6 +4,7 @@ from hyo2.soundspeed.listener.sis.sis import Sis from hyo2.soundspeed.listener.sippican.sippican import Sippican +from hyo2.soundspeed.listener.nmea.nmea import Nmea from hyo2.soundspeed.listener.mvp.mvp import Mvp if TYPE_CHECKING: from hyo2.soundspeed.soundspeed import SoundSpeedLibrary @@ -24,6 +25,8 @@ def __init__(self, prj: 'SoundSpeedLibrary'): timeout=self.prj.setup.sis_listen_timeout, use_sis5=self.prj.setup.use_sis5) self.sippican = Sippican(port=self.prj.setup.sippican_listen_port, prj=prj) + self.nmea = Nmea(port=self.prj.setup.nmea_listen_port, + timeout=self.prj.setup.nmea_listen_timeout) self.mvp = Mvp(port=self.prj.setup.mvp_listen_port, prj=prj) @property @@ -72,6 +75,20 @@ def stop_listen_sippican(self) -> bool: logger.debug("stop") return not self.sippican.is_alive() + def listen_nmea(self) -> bool: + if not self.nmea.is_alive(): + self.nmea.start() + time.sleep(0.1) + logger.debug("start") + return self.nmea.is_alive() + + def stop_listen_nmea(self) -> bool: + if self.nmea.is_alive(): + self.nmea.stop() + self.nmea.join(2) + logger.debug("stop") + return not self.nmea.is_alive() + def listen_mvp(self) -> bool: if not self.mvp.is_alive(): self.mvp.start() @@ -89,11 +106,13 @@ def stop_listen_mvp(self) -> bool: def stop(self) -> None: self.stop_listen_sis() self.stop_listen_sippican() + self.stop_listen_nmea() self.stop_listen_mvp() def __repr__(self) -> str: msg = "\n" msg += "%s" % self.sis msg += "%s" % self.sippican + msg += "%s" % self.nmea msg += "%s" % self.mvp return msg diff --git a/hyo2/soundspeed/listener/nmea/__init__.py b/hyo2/soundspeed/listener/nmea/__init__.py new file mode 100644 index 00000000..f0d3cf25 --- /dev/null +++ b/hyo2/soundspeed/listener/nmea/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import logging + +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) diff --git a/hyo2/soundspeed/listener/nmea/nmea.py b/hyo2/soundspeed/listener/nmea/nmea.py new file mode 100644 index 00000000..d8d05237 --- /dev/null +++ b/hyo2/soundspeed/listener/nmea/nmea.py @@ -0,0 +1,52 @@ +import logging +from datetime import datetime +from typing import Optional, Union + +from hyo2.soundspeed.listener.abstract import AbstractListener +from hyo2.soundspeed.formats import nmea0183 + +logger = logging.getLogger(__name__) + + +class Nmea(AbstractListener): + """NMEA listener""" + + def __init__(self, port: int, timeout: int = 1, ip: str = "0.0.0.0", + target: Optional[object] = None, name: str = "NMEA", debug: bool = False) -> None: + super(Nmea, self).__init__(port=port, ip=ip, timeout=timeout, target=target, name=name, debug=debug) + self.desc = name + + self.nav = None # type: Union[nmea0183.Nmea0183GGA, nmea0183.Nmea0183GLL, None] + self.nav_last_time = None # type: Optional[datetime] + + @property + def nav_latitude(self) -> Optional[float]: + if self.nav is None: + return None + return self.nav.latitude + + @property + def nav_longitude(self) -> Optional[float]: + if self.nav is None: + return None + return self.nav.longitude + + def parse(self) -> None: + this_data = self.data[:].decode("utf-8") + if self.debug: + logger.debug("Received: %s)" % (this_data)) + + sentence_type = this_data[3:6] + + if sentence_type == 'GGA': + self.nav = nmea0183.Nmea0183GGA(this_data) + self.nav_last_time = datetime.utcnow() + + elif sentence_type == 'GLL': + self.nav = nmea0183.Nmea0183GLL(this_data) + self.nav_last_time = datetime.utcnow() + + def __repr__(self): + msg = "%s" % super(Nmea, self).__repr__() + # msg += " \n" % self.has_data_loaded + return msg diff --git a/hyo2/soundspeed/soundspeed.py b/hyo2/soundspeed/soundspeed.py index ad1d05fb..8a97a6e8 100644 --- a/hyo2/soundspeed/soundspeed.py +++ b/hyo2/soundspeed/soundspeed.py @@ -70,6 +70,7 @@ def __init__(self, data_folder: Optional[str] = None, self.check_custom_folders() self.listeners = Listeners(prj=self) self.cb.sis_listener = self.listeners.sis # to provide default values from SIS (if available) + self.cb.nmea_listener = self.listeners.nmea # to provide default values from NMEA (if available) self.server = Server(prj=self) # logger.info("** > LIB: initialized!") @@ -394,18 +395,18 @@ def has_ref(self) -> bool: # --- listeners - def has_mvp_to_process(self) -> bool: - if not self.use_mvp(): - return False - - return self.listeners.mvp_to_process - def has_sippican_to_process(self) -> bool: if not self.use_sippican(): return False return self.listeners.sippican_to_process + def has_mvp_to_process(self) -> bool: + if not self.use_mvp(): + return False + + return self.listeners.mvp_to_process + # --- import data def import_data(self, data_path: str, data_format: str, skip_atlas: bool = False) -> None: @@ -953,6 +954,34 @@ def retrieve_sis(self) -> None: self.ssp = ssp_list self.progress.end() + def retrieve_nmea(self) -> None: + """Retrieve data from NMEA""" + if not self.use_nmea(): + raise RuntimeError("use NMEA option is disabled") + + self.progress.start(text="Retrieve from NMEA") + + if not self.listen_nmea(): + raise RuntimeError("unable to listen NMEA") + + # try to retrieve the location from NMEA + lat = None + lon = None + if self.listeners.nmea.nav: + from_nmea = self.cb.ask_location_from_nmea() + if from_nmea: + lat, lon = self.listeners.nmea.nav_latitude, self.listeners.nmea.nav_longitude + # if we don't have a location, ask user + if (lat is None) or (lon is None): + lat, lon = self.cb.ask_location() + if (lat is None) or (lon is None): + self.progress.end() + raise RuntimeError("missing geographic location required for database lookup") + + ssp.meta.latitude = lat + ssp.meta.longitude = lon + self.progress.end() + # --- export data def export_data(self, data_formats: list, data_paths: Optional[dict], @@ -2081,6 +2110,9 @@ def use_sis4(self) -> bool: def use_sippican(self) -> bool: return self.setup.use_sippican + def use_nmea(self) -> bool: + return self.setup.use_nmea + def use_mvp(self) -> bool: return self.setup.use_mvp @@ -2090,6 +2122,9 @@ def listen_sis(self) -> bool: def listen_sippican(self) -> bool: return self.listeners.listen_sippican() + def listen_nmea(self) -> bool: + return self.listeners.listen_nmea() + def listen_mvp(self) -> bool: return self.listeners.listen_mvp() @@ -2099,6 +2134,9 @@ def stop_listen_sis(self) -> bool: def stop_listen_sippican(self) -> bool: return self.listeners.stop_listen_sippican() + def stop_listen_nmea(self) -> bool: + return self.listeners.stop_listen_nmea() + def stop_listen_mvp(self) -> bool: return self.listeners.stop_listen_mvp() diff --git a/hyo2/soundspeedmanager/mainwin.py b/hyo2/soundspeedmanager/mainwin.py index 9ccb07ef..a81551ec 100644 --- a/hyo2/soundspeedmanager/mainwin.py +++ b/hyo2/soundspeedmanager/mainwin.py @@ -78,6 +78,7 @@ def __init__(self): # self.check_rtofs() # no need to wait for the download at the beginning self.check_sis() self.check_sippican() + self.check_nmea() self.check_mvp() # init default settings @@ -195,6 +196,7 @@ def __init__(self): self.release_checked = False self.old_sis_xyz_data = False self.old_sis_nav_data = False + self.old_nmea_nav_data = False timer = QtCore.QTimer(self) # noinspection PyUnresolvedReferences timer.timeout.connect(self.update_gui) @@ -460,6 +462,20 @@ def check_sippican(self): QtWidgets.QMessageBox.warning(self, "Sound Speed Manager - Sippican", msg, QtWidgets.QMessageBox.Ok) + def check_nmea(self): + if self.lib.use_nmea(): + if not self.lib.listen_nmea(): + msg = 'Unable to listening NMEA-0183.\nCheck whether another process is already using the NMEA-0183 port.' + # noinspection PyCallByClass,PyArgumentList + QtWidgets.QMessageBox.warning(self, "Sound Speed Manager - NMEA-0183", msg, + QtWidgets.QMessageBox.Ok) + else: + if not self.lib.stop_listen_nmea(): + msg = 'Unable to stop listening Nmea.' + # noinspection PyCallByClass,PyArgumentList + QtWidgets.QMessageBox.warning(self, "Sound Speed Manager - NMEA-0183", msg, + QtWidgets.QMessageBox.Ok) + def check_mvp(self): if self.lib.use_mvp(): if not self.lib.listen_mvp(): @@ -575,6 +591,8 @@ def _update_gui_in_use(self) -> str: tokens.append("W18") if self.lib.use_sippican(): tokens.append("SIP") + if self.lib.use_nmea(): + tokens.append("NMEA-0183") if self.lib.use_mvp(): tokens.append("MVP") if self.lib.use_sis(): @@ -672,6 +690,43 @@ def _update_gui_from_sis(self, msg: str) -> str: return msg + def _update_gui_from_nmea_nav(self, msg: str) -> str: + self.old_nmea_nav_data = False + if self.lib.listeners.nmea.nav_last_time is not None: + diff_time = datetime.utcnow() - self.lib.listeners.nmea.nav_last_time + self.old_nmea_nav_data = diff_time.total_seconds() > (self.lib.setup.nmea_listen_timeout * 10) + if self.old_nmea_nav_data: + logger.warning("%s: navigation message is too old (%d seconds)" + % (datetime.utcnow(), diff_time.total_seconds())) + + # position + msg += " - pos:" + if self.old_nmea_nav_data: + msg += "(TOO OLD)" + elif (self.lib.listeners.nmea.nav_latitude is None) or \ + (self.lib.listeners.nmea.nav_longitude is None): + msg += "(NA, NA)" + else: + latitude = self.lib.listeners.nmea.nav_latitude + if latitude >= 0: + letter = "N" + else: + letter = "S" + lat_min = float(60 * math.fabs(latitude - int(latitude))) + lat_str = "%02d\N{DEGREE SIGN}%7.3f'%s" % (int(math.fabs(latitude)), lat_min, letter) + + longitude = self.lib.listeners.nmea.nav_longitude + if longitude < 0: + letter = "W" + else: + letter = "E" + lon_min = float(60 * math.fabs(longitude - int(longitude))) + lon_str = "%03d\N{DEGREE SIGN}%7.3f'%s" % (int(math.fabs(longitude)), lon_min, letter) + + msg += "(%s, %s)" % (lat_str, lon_str) + + return msg + def update_gui(self): self.timer_execs += 1 @@ -690,6 +745,8 @@ def update_gui(self): self.old_sis_xyz_data = False if self.lib.use_sis(): # in case that SIS4 and SIS5 are enabled msg = self._update_gui_from_sis(msg=msg) + elif self.lib.use_nmea(): # in case that the NMEA listener is enabled + msg = self._update_gui_from_nmea_nav(msg=msg) self.statusBar().showMessage(msg, 2000) if self.lib.has_ssp(): @@ -721,7 +778,7 @@ def update_gui(self): if self.lib.has_mvp_to_process() or self.lib.has_sippican_to_process(): self.statusBar().setStyleSheet(self.orange_stylesheet) else: - if self.old_sis_nav_data or self.old_sis_xyz_data: + if self.old_sis_nav_data or self.old_sis_xyz_data or self.old_nmea_nav_data: self.statusBar().setStyleSheet(self.purple_stylesheet) else: self.statusBar().setStyleSheet(self.normal_stylesheet) diff --git a/hyo2/soundspeedmanager/qt_callbacks.py b/hyo2/soundspeedmanager/qt_callbacks.py index 07728af7..bac2f997 100644 --- a/hyo2/soundspeedmanager/qt_callbacks.py +++ b/hyo2/soundspeedmanager/qt_callbacks.py @@ -298,6 +298,9 @@ def ask_location(self, default_lat: Optional[float] = None, default_lon: Optiona settings = QtCore.QSettings() + msg_lat = "Enter latitude as DD, DM, or DMS:" + msg_lon = "Enter longitude as DD, DM, or DMS:" + # try to convert the passed default values if (default_lat is not None) and (default_lon is not None): @@ -310,14 +313,28 @@ def ask_location(self, default_lat: Optional[float] = None, default_lon: Optiona default_lat = 43.13555 default_lon = -70.9395 - # if both default lat and lon are None, check if sis has position + # if both default lat and lon are None, check if position can be retrieved from listeners else: + # sis listener if self.sis_listener is not None: if self.sis_listener.nav is not None: if (self.sis_listener.nav_latitude is not None) and (self.sis_listener.nav_longitude is not None): - default_lat = self.sis_listener.nav_latitude - default_lon = self.sis_listener.nav_longitude + if self.ask_location_from_sis(): + msg_lat = "Latitude retrieved from SIS listener:" + msg_lon = "Longitude retrieved from SIS listener:" + default_lat = self.sis_listener.nav_latitude + default_lon = self.sis_listener.nav_longitude + + # nmea listener + if self.nmea_listener is not None: + if self.nmea_listener.nav is not None: + if (self.nmea_listener.nav_latitude is not None) and (self.nmea_listener.nav_longitude is not None): + if self.ask_location_from_nmea(): + msg_lat = "Latitude retrieved from NMEA-0183 listener:" + msg_lon = "Longitude retrieved from NMEA-0183 listener:" + default_lat = self.nmea_listener.nav_latitude + default_lon = self.nmea_listener.nav_longitude if (default_lat is None) or (default_lon is None): @@ -330,14 +347,14 @@ def ask_location(self, default_lat: Optional[float] = None, default_lon: Optiona default_lon = -70.9395 # ask user for both lat and long + lon = None # first latitude while True: # noinspection PyArgumentList,PyCallByClass - lat, ok = QtWidgets.QInputDialog.getText(self._parent, "Location", "Enter latitude as DD, DM, or DMS:", - text="%s" % default_lat) + lat, ok = QtWidgets.QInputDialog.getText(self._parent, "Location", msg_lat, text="%s" % default_lat) if not ok: lat = None break @@ -352,9 +369,7 @@ def ask_location(self, default_lat: Optional[float] = None, default_lon: Optiona while True: # noinspection PyCallByClass,PyArgumentList - lon, ok = QtWidgets.QInputDialog.getText(self._parent, "Location", - "Enter longitude as DD, DM, or DMS:", - text="%s" % default_lon) + lon, ok = QtWidgets.QInputDialog.getText(self._parent, "Location", msg_lon, text="%s" % default_lon) if not ok: lat = None break @@ -482,6 +497,17 @@ def ask_location_from_sis(self) -> bool: return False return True + def ask_location_from_nmea(self) -> bool: + """Ask user whether retrieving location from NMEA""" + msg = "Geographic location required for pressure/depth conversion and atlas lookup.\n" \ + "Use geographic position from NMEA-0183?\nChoose 'no' to enter position manually." + # noinspection PyArgumentList,PyCallByClass + ret = QtWidgets.QMessageBox.information(self._parent, "Location", msg, + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.No) + if ret == QtWidgets.QMessageBox.No: + return False + return True + def ask_tss(self) -> Optional[float]: """Ask user for transducer sound speed""" settings = QtCore.QSettings() diff --git a/hyo2/soundspeedsettings/widgets/input.py b/hyo2/soundspeedsettings/widgets/input.py index 66385838..8c7f9a4c 100644 --- a/hyo2/soundspeedsettings/widgets/input.py +++ b/hyo2/soundspeedsettings/widgets/input.py @@ -285,6 +285,26 @@ def __init__(self, main_win, db): vbox.addWidget(self.use_sippican) vbox.addStretch() + # - use nmea + hbox = QtWidgets.QHBoxLayout() + self.right_layout.addLayout(hbox) + # -- label + vbox = QtWidgets.QVBoxLayout() + hbox.addLayout(vbox) + vbox.addStretch() + label = QtWidgets.QLabel("Listen NMEA-0183:") + label.setFixedWidth(lbl_width) + vbox.addWidget(label) + vbox.addStretch() + # -- value + vbox = QtWidgets.QVBoxLayout() + hbox.addLayout(vbox) + vbox.addStretch() + self.use_nmea = QtWidgets.QComboBox() + self.use_nmea.addItems(["True", "False"]) + vbox.addWidget(self.use_nmea) + vbox.addStretch() + # - use mvp hbox = QtWidgets.QHBoxLayout() self.right_layout.addLayout(hbox) @@ -396,6 +416,8 @@ def __init__(self, main_win, db): # noinspection PyUnresolvedReferences self.use_sippican.currentIndexChanged.connect(self.apply_use_sippican) # noinspection PyUnresolvedReferences + self.use_nmea.currentIndexChanged.connect(self.apply_use_nmea) + # noinspection PyUnresolvedReferences self.use_mvp.currentIndexChanged.connect(self.apply_use_mvp) # noinspection PyUnresolvedReferences self.rx_max_wait_time.textChanged.connect(self.apply_rx_max_wait_time) @@ -494,6 +516,12 @@ def apply_use_sippican(self): self.setup_changed() self.main_win.reload_settings() + def apply_use_nmea(self): + # logger.debug("apply use Nmea") + self.db.use_nmea = self.use_nmea.currentText() == "True" + self.setup_changed() + self.main_win.reload_settings() + def apply_use_mvp(self): # logger.debug("apply use MVP") self.db.use_mvp = self.use_mvp.currentText() == "True" @@ -576,6 +604,12 @@ def setup_changed(self): else: self.use_sippican.setCurrentIndex(1) # False + # use nmea + if self.db.use_nmea: + self.use_nmea.setCurrentIndex(0) # True + else: + self.use_nmea.setCurrentIndex(1) # False + # use mvp if self.db.use_mvp: self.use_mvp.setCurrentIndex(0) # True diff --git a/hyo2/soundspeedsettings/widgets/listeners.py b/hyo2/soundspeedsettings/widgets/listeners.py index 7fd7db2c..23b24b38 100644 --- a/hyo2/soundspeedsettings/widgets/listeners.py +++ b/hyo2/soundspeedsettings/widgets/listeners.py @@ -115,6 +115,42 @@ def __init__(self, main_win, db): self.sippican_listen_timeout.setValidator(validator) hbox.addWidget(self.sippican_listen_timeout) + self.left_layout.addSpacing(12) + + # NMEA + hbox = QtWidgets.QHBoxLayout() + self.left_layout.addLayout(hbox) + hbox.addStretch() + self.label = QtWidgets.QLabel("NMEA-0183(*):") + hbox.addWidget(self.label) + hbox.addStretch() + + # - nmea_listen_port + hbox = QtWidgets.QHBoxLayout() + self.left_layout.addLayout(hbox) + # -- label + label = QtWidgets.QLabel("Listen port:") + label.setFixedWidth(lbl_width) + hbox.addWidget(label) + # -- value + self.nmea_listen_port = QtWidgets.QLineEdit() + validator = QtGui.QIntValidator(0, 99999) + self.nmea_listen_port.setValidator(validator) + hbox.addWidget(self.nmea_listen_port) + + # - nmea_listen_timeout + hbox = QtWidgets.QHBoxLayout() + self.left_layout.addLayout(hbox) + # -- label + label = QtWidgets.QLabel("Listen timeout:") + label.setFixedWidth(lbl_width) + hbox.addWidget(label) + # -- value + self.nmea_listen_timeout = QtWidgets.QLineEdit() + validator = QtGui.QIntValidator(0, 99999) + self.nmea_listen_timeout.setValidator(validator) + hbox.addWidget(self.nmea_listen_timeout) + self.left_layout.addStretch() # RIGHT @@ -400,6 +436,10 @@ def __init__(self, main_win, db): self.sippican_listen_port.textChanged.connect(self.apply_sippican_listen_port) # noinspection PyUnresolvedReferences self.sippican_listen_timeout.textChanged.connect(self.apply_sippican_listen_timeout) + # noinspection PyUnresolvedReferences + self.nmea_listen_port.textChanged.connect(self.apply_nmea_listen_port) + # noinspection PyUnresolvedReferences + self.nmea_listen_timeout.textChanged.connect(self.apply_nmea_listen_timeout) # --- right # noinspection PyUnresolvedReferences self.mvp_ip_address.textChanged.connect(self.apply_mvp_ip_address) @@ -438,15 +478,27 @@ def apply_sis_listen_timeout(self): self.setup_changed() self.main_win.reload_settings() + def apply_sippican_listen_port(self): + # logger.debug("apply listen port") + self.db.sippican_listen_port = int(self.sippican_listen_port.text()) + self.setup_changed() + self.main_win.reload_settings() + def apply_sippican_listen_timeout(self): # logger.debug("apply listen timeout") self.db.sippican_listen_timeout = int(self.sippican_listen_timeout.text()) self.setup_changed() self.main_win.reload_settings() - def apply_sippican_listen_port(self): + def apply_nmea_listen_port(self): # logger.debug("apply listen port") - self.db.sippican_listen_port = int(self.sippican_listen_port.text()) + self.db.nmea_listen_port = int(self.nmea_listen_port.text()) + self.setup_changed() + self.main_win.reload_settings() + + def apply_nmea_listen_timeout(self): + # logger.debug("apply listen timeout") + self.db.nmea_listen_timeout = int(self.nmea_listen_timeout.text()) self.setup_changed() self.main_win.reload_settings() @@ -544,6 +596,14 @@ def setup_changed(self): # sippican_listen_timeout self.sippican_listen_timeout.setText("%d" % self.db.sippican_listen_timeout) + # - NMEA + + # nmea_listen_port + self.nmea_listen_port.setText("%d" % self.db.nmea_listen_port) + + # nmea_listen_timeout + self.nmea_listen_timeout.setText("%d" % self.db.nmea_listen_timeout) + # - MVP # mvp_ip_address