diff --git a/README.md b/README.md index ac955a8..a623523 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ pygnssutils [gnssstreamer CLI](#gnssstreamer) | [gnssserver CLI](#gnssserver) | [gnssntripclient CLI](#gnssntripclient) | -[gnssmqttclient CLI](#gnssmqttclient) | [socketserver](#socketserver) | [RINEX Conversion](#rinexconvert) | [RTK Demonstration](#rtkdemo) | @@ -27,17 +26,14 @@ pygnssutils is an original series of Python GNSS utility classes and CLI tools b Originally developed in support of the [PyGPSClient](https://github.com/semuconsulting/PyGPSClient) GUI GNSS application, the utilities provided by pygnssutils can also be used in their own right: -1. `GNSSReader` class. This is essentially an amalgamation of the `*Reader` classes in all the subsidiary parsers listed above, allowing the user to seamlessly stream any of NMEA, UBX, SBF, UNI, QGC, RTCM3 and SPARTN message protocols concurrently from a single stream. +1. `GNSSReader` class. This is essentially an amalgamation of the `*Reader` classes in all the subsidiary parsers listed above, allowing the user to seamlessly stream any of NMEA, UBX, SBF, UNI, QGC and RTCM3 message protocols concurrently from a single stream. 1. `GNSSStreamer` class and its associated [`gnssstreamer`](#gnssstreamer) (*formerly `gnssdump`*) CLI utility. This is essentially a configurable bidirectional input/output wrapper around the `GNSSReader` class with flexible message formatting, filtering and output handling options for NMEA, UBX, SBF, UNI, QGC and RTCM3 protocols. 1. `GNSSSocketServer` class and its associated [`gnssserver`](#gnssserver) CLI utility. This implements a TCP Socket Server for GNSS data streams which is also capable of being run as a simple NTRIP Server/Caster. 1. `GNSSNTRIPClient` class and its associated [`gnssntripclient`](#gnssntripclient) CLI utility. This implements a simple NTRIP Client which receives RTCM3 or SPARTN correction data from an NTRIP Server and (optionally) sends this to a designated output stream. -1. `GNSSMQTTClient` class and its associated [`gnssmqttclient`](#gnssmqttclient) CLI utility. This implements -a simple SPARTN IP (MQTT) Client which receives SPARTN correction data from an SPARTN IP location service and (optionally) sends this to a -designated output stream. 1. `SocketServer` class based on the native Python `ThreadingTCPServer`. Capable of operating in two modes - Socket Server or NTRIP Caster. Provides two alternate client request handler classes - `ClientHandler` (HTTP) or `ClientHandlerTLS` (HTTPS). -1. `RinexConverter` class and its associated [`pyrinexconv`](#rinexconvert) CLI utility. This implements a binary GNSS data log file to RINEX text file conversion facility. **NB: RINEX conversion is currently an experimental work in progress (*contributions and feedback welcome*)** +1. `RinexConverter` class and its associated [`pyrinexconv`](#rinexconvert) CLI utility. This implements a binary GNSS data log file to RINEX text file conversion facility. **NB: RINEX conversion is currently an experimental feature (*contributions and feedback welcome*)** The pygnssutils homepage is located at [https://github.com/semuconsulting/pygnssutils](https://github.com/semuconsulting/pygnssutils). @@ -53,7 +49,7 @@ The pygnssutils homepage is located at [https://github.com/semuconsulting/pygnss Sphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pygnssutils](https://www.semuconsulting.com/pygnssutils). -Contributions welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pygnssutils/blob/main/CONTRIBUTING.md). +Contributions **_from human beings_** welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pygnssutils/blob/main/CONTRIBUTING.md). [Bug reports](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, post a message to one of the [pygnssutils Discussions](https://github.com/semuconsulting/pygnssutils/discussions) channels. @@ -138,7 +134,6 @@ class pygnssutils.gnssstreamer.GNSSStreamer(**kwargs) - 0 = none (default) - 1 = RTK NTRIP RTCM caster - 2 = RTK NTRIP SPARTN caster - - 3 = RTK MQTT SPARTN source (see [gnssmqttclient](#gnssmqttclient) for MQTT client configuration details) - 4 = serial port - 5 = binary file. @@ -428,41 +423,6 @@ gnssntripclient -h Refer to the [Sphinx API documentation](https://www.semuconsulting.com/pygnssutils/pygnssutils.html#module-pygnssutils.gnssntripclient) for further details. ---- -## GNSSMQTTClient and gnssmqttclient CLI -``` -class pygnssutils.gnssmqttclient.GNSSMQTTClient(app=None, **kwargs) -``` - -The `GNSSMQTTClient` class provides a basic SPARTN IP (MQTT) Client capability and forms the basis of a [`gnssmqttclient`](#gnssmqttclient) CLI utility. It receives RTK correction data from a SPARTN IP (MQTT) location service (e.g. the u-blox / Thingstream PointPerfect service) and (optionally) sends this to a designated output stream. - -### CLI Usage: - -The `clientid` provided by the location service may be set as environment variable `MQTTCLIENTID`. If this environment variable is set and the TLS certificate (\*.crt) and key (\*.pem) files provided by the location service are placed in the user's `HOME` directory, the utility can use these as default settings and may be invoked without any arguments. - -Assuming the Python 3 scripts (bin) directory is in your PATH, the CLI utility may be invoked from the shell thus (press CTRL-C to terminate): - -```shell -gnssmqttclient --clientid yourclientid --server pp.services.u-blox.com --port 8883 --region eu --mode 0 --topic_ip 1 --topic_mga 1 --topic_key 1 --tlscrt '/Users/{your-user}/device-{your-clientid}-pp-cert.crt' --tlskey '/Users/{your-user}/device-{your-client-id}-pp-key.pem'} --spartndecode 0 --clioutput 0 --verbosity 2 -``` -``` -2024-08-15 09:14:50.544 - INFO - pygnssutils.gnssmqttclient - Starting MQTT client with arguments {'server': 'pp.services.u-blox.com', 'port': 8883, 'clientid': 'your-client-id', 'region': 'eu', 'mode': 0, 'topic_ip': 1, 'topic_mga': 1, 'topic_key': 1, 'tlscrt': '/Users/myuser/device-your-client-id-pp-cert.crt', 'tlskey': '/Users/myuser/device-your-client-id-pp-key.pem', 'spartndecode': 0, 'output': None}. -2024-08-15 09:14:50.840 - INFO - pygnssutils.gnssmqttclient - RXM-SPARTN-KEY -2024-08-15 09:14:50.854 - INFO - pygnssutils.gnssmqttclient - MGA-INI-TIME-UTC -2024-08-15 09:14:50.858 - INFO - pygnssutils.gnssmqttclient - MGA-GPS-EPH -... -``` - -Command line arguments can be stored in a configuration file and invoked using the `-C` or `--config` argument. The location of the configuration file can be set in environment variable `GNSSMQTTCLIENT_CONF`. - -For help and full list of optional arguments, type: - -```shell -gnssmqttclient -h -``` - -Refer to the [pyspartn documentation](https://github.com/semuconsulting/pyspartn?tab=readme-ov-file#reading) for further details on decrypting encrypted (`eaf=1`) SPARTN payloads. - --- ## SocketServer diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cbf979d..49db842 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # pygnssutils +### RELEASE 1.2.7 + +1. Deprecated gnssmqttclient package removed. + ### RELEASE 1.2.6 1. Add `msgfilter` argument to GNSSReader - will only parse GNSS messages where msgid is in msgfilter. msgid is string type for NMEA, otherwise integer. diff --git a/docs/pygnssutils.rst b/docs/pygnssutils.rst index f963e0c..d1dc679 100644 --- a/docs/pygnssutils.rst +++ b/docs/pygnssutils.rst @@ -20,22 +20,6 @@ pygnssutils.globals module :undoc-members: :show-inheritance: -pygnssutils.gnssmqttclient module ---------------------------------- - -.. automodule:: pygnssutils.gnssmqttclient - :members: - :undoc-members: - :show-inheritance: - -pygnssutils.gnssmqttclient\_cli module --------------------------------------- - -.. automodule:: pygnssutils.gnssmqttclient_cli - :members: - :undoc-members: - :show-inheritance: - pygnssutils.gnssntripclient module ---------------------------------- @@ -100,14 +84,6 @@ pygnssutils.helpers module :undoc-members: :show-inheritance: -pygnssutils.mqttmessage module ------------------------------- - -.. automodule:: pygnssutils.mqttmessage - :members: - :undoc-members: - :show-inheritance: - pygnssutils.rawnav module ------------------------- diff --git a/pyproject.toml b/pyproject.toml index 21a8223..a6707b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,11 +35,10 @@ classifiers = [ dependencies = [ "certifi>=2025.0.0", - "paho-mqtt>=2.1.0", "pyserial>=3.5", "pyspartn>=1.0.8", "pyubx2>=1.3.6", - "pynmeagps>=1.1.5", + "pynmeagps>=1.1.7", "pysbf2>=1.0.4", "pyubxutils>=1.0.6", "pyqgc>=1.0.0", @@ -50,7 +49,6 @@ dependencies = [ gnssstreamer = "pygnssutils.gnssstreamer_cli:main" gnssserver = "pygnssutils.gnssserver_cli:main" gnssntripclient = "pygnssutils.gnssntripclient_cli:main" -gnssmqttclient = "pygnssutils.gnssmqttclient_cli:main" pyrinexconv = "pygnssutils.rinex_conv_cli:main" [project.urls] diff --git a/src/pygnssutils/__init__.py b/src/pygnssutils/__init__.py index 49c4e69..11e50da 100644 --- a/src/pygnssutils/__init__.py +++ b/src/pygnssutils/__init__.py @@ -12,7 +12,6 @@ from pygnssutils._version import __version__ from pygnssutils.exceptions import GNSSError, GNSSStreamError, ParameterError from pygnssutils.globals import * -from pygnssutils.gnssmqttclient import GNSSMQTTClient from pygnssutils.gnssntripclient import GNSSNTRIPClient from pygnssutils.gnssreader import ( NMEA_PROTOCOL, @@ -26,7 +25,6 @@ from pygnssutils.gnssserver import GNSSSocketServer from pygnssutils.gnssstreamer import GNSSStreamer from pygnssutils.helpers import * -from pygnssutils.mqttmessage import * from pygnssutils.rawnav import * from pygnssutils.rinex_conv import RinexConverter diff --git a/src/pygnssutils/_version.py b/src/pygnssutils/_version.py index 7701d71..2a27e6a 100644 --- a/src/pygnssutils/_version.py +++ b/src/pygnssutils/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.2.6" +__version__ = "1.2.7" diff --git a/src/pygnssutils/gnssmqttclient.py b/src/pygnssutils/gnssmqttclient.py deleted file mode 100644 index c1fdd37..0000000 --- a/src/pygnssutils/gnssmqttclient.py +++ /dev/null @@ -1,481 +0,0 @@ -""" -gnssmqttclient.py - -MQTT SPARTN client class, retrieving correction data from an IP (MQTT) -source and (optionally) sending the data to a designated writeable output -medium (serial, file, socket, queue). - -Calling app, if defined, can implement the following methods: - -- set_event() - create <> event -- dialog() - return reference to MQTT client configuration dialog - -Can utilise the following environment variables: - -- MQTTKEY - SPARTN payload decription key (valid for 4 weeks) -- MQTTCRT - MQTT server (PointPerfect) TLS certificate -- MQTTPEM - MQTT server (PointPerfect) TLS key -- MQTTCLIENTID - MQTT server client ID - -Credentials can be download from: - -Thingstream > Location Services > PointPerfect Thing > Credentials - -Default location for key files is user's HOME directory - -Created on 20 Feb 2023 - -:author: semuadmin (Steve Smith) -:copyright: semuadmin © 2023 -:license: BSD 3-Clause -""" - -# pylint: disable=invalid-name - -import socket -from io import BufferedWriter, BytesIO, TextIOWrapper -from logging import getLogger -from os import getenv, path -from pathlib import Path -from queue import Queue -from ssl import SSLError -from threading import Event, Thread -from time import sleep - -import paho.mqtt.client as mqtt -from paho.mqtt import __version__ as PAHO_MQTT_VERSION -from pyspartn import ( - ERRLOG, - SPARTNDecryptionError, - SPARTNMessageError, - SPARTNParseError, - SPARTNReader, - SPARTNStreamError, -) -from pyubx2 import SET, UBXParseError, UBXReader -from serial import Serial - -from pygnssutils.exceptions import ParameterError -from pygnssutils.globals import ( - CLIAPP, - ENV_MQTT_CLIENTID, - ENV_MQTT_KEY, - OUTPORT_SPARTN, - SPARTN_EVENT, - SPARTN_PPSERVER, - TOPIC_ASSIST, - TOPIC_DATA, - TOPIC_FREQ, - TOPIC_KEY, - VERBOSITY_MEDIUM, -) -from pygnssutils.helpers import set_logging -from pygnssutils.mqttmessage import MQTTMessage - -TIMEOUT = 8 -DLGTSPARTN = "SPARTN Configuration" - -_global_timetags = {} # for want of a better approach - - -class GNSSMQTTClient: - """ - SPARTN MQTT client class. - """ - - def __init__(self, app=None, **kwargs): - """ - Constructor. - - :param object app: application from which this class is invoked (None) - """ - - self.__app = app # Reference to calling application class (if applicable) - # configure logger with name "pygnssutils" in calling module - verbosity = int(kwargs.pop("verbosity", VERBOSITY_MEDIUM)) - logtofile = kwargs.pop("logtofile", "") - self.logger = getLogger(__name__) - set_logging(getLogger("pyspartn"), verbosity, logtofile) - self._validargs = True - clientid = getenv(ENV_MQTT_CLIENTID, default="enter-client-id") - - self._settings = { - "server": SPARTN_PPSERVER, - "port": OUTPORT_SPARTN, - "clientid": clientid, - "region": "eu", - "mode": 0, - "topic_ip": 1, - "topic_mga": 1, - "topic_key": 1, - "topic_freq": 0, - "tlscrt": getenv( - "MQTTCRT", - default=path.join(Path.home(), f"device-{clientid}-pp-cert.crt"), - ), - "tlskey": getenv( - "MQTTPEM", - default=path.join(Path.home(), f"device-{clientid}-pp-key.pem"), - ), - "spartndecode": 0, - "spartnkey": getenv(ENV_MQTT_KEY, default=None), - "spartnbasedate": None, - "output": None, - } - - self._timeout = kwargs.get("timeout", TIMEOUT) - self.errevent = kwargs.get("errevent", Event()) - self._logpath = kwargs.get("logpath", ".") - self._loglines = 0 - self._socket = None - self._connected = False - self._stopevent = Event() - self._mqtt_thread = None - self._logfile = "" - - def __enter__(self): - """ - Context manager enter routine. - """ - - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - """ - Context manager exit routine. - - Terminates threads in an orderly fashion. - """ - - self.stop() - - @property - def settings(self): - """ - Getter for SPARTN IP settings. - """ - - return self._settings - - @settings.setter - def settings(self, settings: dict): - """ - Setter for SPARTN IP settings. - - :param dict settings: SPARTN IP settings dictionary - """ - - self._settings = settings - - @property - def connected(self): - """ - Connection status getter. - """ - - return self._connected - - def start(self, **kwargs) -> int: - """ - Start MQTT handler thread. - - :returns: return code - :rtype: int - """ - - try: - self._settings["server"] = kwargs.get("server", self._settings["server"]) - self._settings["port"] = int(kwargs.get("port", self._settings["port"])) - self._settings["clientid"] = kwargs.get( - "clientid", self._settings["clientid"] - ) - self._settings["region"] = kwargs.get("region", self._settings["region"]) - self._settings["mode"] = int(kwargs.get("mode", self._settings["mode"])) - self._settings["topic_ip"] = int( - kwargs.get("topic_ip", self._settings["topic_ip"]) - ) - self._settings["topic_mga"] = int( - kwargs.get("topic_mga", self._settings["topic_mga"]) - ) - self._settings["topic_key"] = int( - kwargs.get("topic_key", self._settings["topic_key"]) - ) - self._settings["topic_freq"] = int( - kwargs.get("topic_freq", self._settings["topic_freq"]) - ) - self._settings["tlscrt"] = kwargs.get("tlscrt", self._settings["tlscrt"]) - self._settings["tlskey"] = kwargs.get("tlskey", self._settings["tlskey"]) - self._settings["spartndecode"] = int( - kwargs.get("spartndecode", self._settings["spartndecode"]) - ) - self._settings["spartnkey"] = kwargs.get( - "spartnkey", self._settings["spartnkey"] - ) - self._settings["spartnbasedate"] = kwargs.get( - "spartnbasedate", self._settings["spartnbasedate"] - ) - self._settings["output"] = kwargs.get("output", self._settings["output"]) - - except (ParameterError, ValueError, TypeError) as err: - self.logger.critical( - f"Invalid input arguments {kwargs}\n{err}\nType gnssntripclient -h for help." - ) - self._validargs = False - return 0 - - self.logger.info(f"Starting MQTT client with arguments {self._settings}.") - self._stopevent.clear() - self._mqtt_thread = Thread( - target=self._run, - args=( - self.__app, - self._settings, - self._timeout, - self._stopevent, - ), - daemon=True, - ) - self._mqtt_thread.start() - return 1 - - def stop(self): - """ - Stop MQTT handler thread. - """ - - self._stopevent.set() - self._mqtt_thread = None - self.logger.info("MQTT Client Stopped.") - - def _run( - self, - app: object, - settings: dict, - timeout: int, - stopevent: Event, - ): - """ - THREADED Run MQTT client thread. - - :param object app: calling application - :param dict settings: dict of settings - :param int timeout: connection timeout in seconds - :param event stopevent: stop event - """ - - # these pylint exclusions are necessary to accommodate old and new versions - # of the paho.mqtt api... - # pylint: disable=redundant-keyword-arg, no-member, no-value-for-parameter - - topics = [] - mode = "Lb" if settings.get("mode", 0) else "ip" - if settings["topic_ip"]: - topics.append((TOPIC_DATA.format(mode, settings["region"]), 0)) - if settings["topic_mga"]: - topics.append((TOPIC_ASSIST, 0)) - if settings["topic_key"]: - topics.append((TOPIC_KEY.format(mode), 0)) - if settings["topic_freq"]: - topics.append((TOPIC_FREQ, 0)) - userdata = { - "output": settings["output"], - "topics": topics, - "app": app, - "decode": settings["spartndecode"], - "key": settings["spartnkey"], - "basedate": settings["spartnbasedate"], - "logger": self.logger, - } - - try: - if PAHO_MQTT_VERSION < "2.0.0": - client = mqtt.Client( - client_id=settings["clientid"], - userdata=userdata, - ) - else: - client = mqtt.Client( - mqtt.CallbackAPIVersion.VERSION1, - client_id=settings["clientid"], - userdata=userdata, - ) - client.on_connect = self.on_connect - client.on_disconnect = self.on_disconnect - client.on_message = self.on_message - client.tls_set(certfile=settings["tlscrt"], keyfile=settings["tlskey"]) - i = 1 - while not stopevent.is_set(): - try: - client.connect(settings["server"], port=settings["port"]) - break - except Exception as err: # pylint: disable=broad-exception-caught - if i > 4: - raise TimeoutError( - f"Unable to connect to {settings['server']}" - + f":{settings['port']} in {timeout} seconds. {err}" - ) from err - self.logger.info(f"Trying to connect {i} ...") - sleep(timeout / 4) - i += 1 - - client.loop_start() - while not stopevent.is_set(): - # run the client loop in the same thread, as callback access gnss - # client.loop(timeout=0.1) - sleep(0.1) - except (FileNotFoundError, TimeoutError, SSLError) as err: - if "[SSL] PEM lib" in str(err): - err = f"Invalid Certificate or Key File {err}" - self.logger.critical(f"ERROR! {err}") - GNSSMQTTClient.on_error(userdata, err) - self.stop() - self.errevent.set() - - finally: - client.loop_stop() - - @staticmethod - def on_connect(client, userdata, flags, rcd): # pylint: disable=unused-argument - """ - The callback for when the client receives a CONNACK response from the server. - - :param object client: client - :param list userdata: list of user defined data items - :param list flags: optional flags - :param int rcd: return status code - """ - - if rcd == 0: - client.subscribe(userdata["topics"]) - else: - GNSSMQTTClient.on_error(userdata, rcd) - - @staticmethod - def on_connect_fail(client, userdata, rcd): # pylint: disable=unused-argument - """ - The callback for when the client fails to connect to the server. - - :param object client: client - :param list userdata: list of user defined data items - :param int rcd: return status code - """ - - GNSSMQTTClient.on_error(userdata, rcd) - - @staticmethod - def on_disconnect(client, userdata, rcd): # pylint: disable=unused-argument - """ - The callback for when the client disconnects from the server. - - :param object client: client - :param list userdata: list of user defined data items - :param int rcd: return status code - """ - - GNSSMQTTClient.on_error(userdata, rcd) - - @staticmethod - def on_message(client, userdata, msg): # pylint: disable=unused-argument - """ - The callback for when a PUBLISH message is received from the server. - Some MQTT topics may contain more than one UBX or SPARTN message in - a single payload. - - :param object client: MQTT client - :param list userdata: list of user defined data items - :param object msg: SPARTN or UBX message topic content - """ - - global _global_timetags - output = userdata["output"] - app = userdata["app"] - msglogger = userdata["logger"] - - def do_write(raw: bytes, parsed: object): - """ - Send SPARTN data to designated output medium. - - If output is Queue, will send both raw and parsed data. - - :param dict userdata: user defined data dict - :param bytes raw: raw data - :param object parsed: parsed message - """ - - if hasattr(parsed, "identity"): - msglogger.info(parsed.identity) - msglogger.debug(parsed) - - if output is not None: - if isinstance(output, (Serial, BufferedWriter)): - output.write(raw) - elif isinstance(output, TextIOWrapper): - output.write(str(parsed)) - elif isinstance(output, Queue): - output.put(raw if app == CLIAPP else (raw, parsed)) - elif isinstance(output, socket.socket): - output.sendall(raw) - - if app is not None: - if hasattr(app, "set_event"): - app.set_event(SPARTN_EVENT) - - if "ubx" in msg.topic: # UBX MGA-* or RXM-SPARTNKEY messages - ubr = UBXReader(BytesIO(msg.payload), msgmode=SET) - try: - for raw, parsed in ubr: - do_write(raw, parsed) - except UBXParseError: - parsed = f"MQTT UBXParseError {msg.topic} {msg.payload}" - do_write(msg.payload, parsed) - elif "frequencies" in msg.topic: # frequency values - parsed = MQTTMessage(msg.topic, msg.payload) - do_write(msg.payload, parsed) - else: # SPARTN protocol message - spr = SPARTNReader( - BytesIO(msg.payload), - decode=userdata["decode"], - key=userdata["key"], - basedate=userdata["basedate"], - timetags=_global_timetags, - quitonerror=ERRLOG, - ) - try: - for raw, parsed in spr: - do_write(raw, parsed) - _global_timetags = spr.timetags - except ( - SPARTNMessageError, - SPARTNParseError, - SPARTNStreamError, - ) as err: - msglogger.error(err) - parsed = f"{msg.topic} {err}" - do_write(msg.payload, parsed) - except SPARTNDecryptionError as err: - msglogger.error(err) - parsed = f"{msg.topic} {err}" - do_write(msg.payload, parsed) - - @staticmethod - def on_error(userdata: dict, err: object): - """ - Report return code back to any calling application. - - :param dict userdata: user defined data dict - :param object rcd: return code (int or str) - """ - - errlogger = userdata["logger"] - - if isinstance(err, int): - err = mqtt.error_string(err) - app = userdata["app"] - if app is None: - errlogger.error(err) - else: - if hasattr(app, "dialog"): - dlg = app.dialog(DLGTSPARTN) - if dlg is not None: - if hasattr(dlg, "disconnect_ip"): - dlg.disconnect_ip(f"{err} ") diff --git a/src/pygnssutils/gnssmqttclient_cli.py b/src/pygnssutils/gnssmqttclient_cli.py deleted file mode 100644 index eec1184..0000000 --- a/src/pygnssutils/gnssmqttclient_cli.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -gnssmqttclient_cli.py - -CLI wrapper for GNSSMQTTClient class. - -Created on 24 Jul 2024 - -:author: semuadmin (Steve Smith) -:copyright: semuadmin © 2023 -:license: BSD 3-Clause -""" - -from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser -from os import getenv, path -from pathlib import Path -from queue import Queue -from threading import Event, Thread -from time import sleep - -from serial import Serial - -from pygnssutils._version import __version__ as VERSION -from pygnssutils.globals import ( - CLIAPP, - ENV_MQTT_CLIENTID, - ENV_MQTT_KEY, - EPILOG, - MAXCONNECTION, - NTRIP2, - OUTPORT_SPARTN, - OUTPUT_FILE, - OUTPUT_NONE, - OUTPUT_SERIAL, - OUTPUT_SOCKET, - OUTPUT_SOCKET_TLS, - SPARTN_PPSERVER, -) -from pygnssutils.gnssmqttclient import TIMEOUT, GNSSMQTTClient -from pygnssutils.helpers import set_common_args -from pygnssutils.socket_server import runserver - -TIMEOUT = 8 -DLGTSPARTN = "SPARTN Configuration" - - -def runclient(**kwargs): - """ - Start MQTT client with CLI parameters. - """ - - waittime = float(kwargs["waittime"]) - with GNSSMQTTClient(CLIAPP, **kwargs) as gsc: - streaming = gsc.start(**kwargs) - while streaming and not kwargs["errevent"].is_set(): - sleep(waittime) - sleep(waittime) - - -def main(): - """ - CLI Entry point. - """ - # pylint: disable=raise-missing-from - - clientid = getenv(ENV_MQTT_CLIENTID, default="enter-client-id") - ap = ArgumentParser( - description="Client ID can be read from environment variable MQTTCLIENTID", - epilog=EPILOG - + " NB: This utility is DEPRECATED and will be removed in the next release of pygnssutils (1.2.7)", - formatter_class=ArgumentDefaultsHelpFormatter, - ) - ap.add_argument("-V", "--version", action="version", version="%(prog)s " + VERSION) - ap.add_argument( - "-I", - "--clientid", - required=False, - help="Client ID", - default=clientid, - ) - ap.add_argument( - "-S", - "--server", - required=False, - help="SPARTN MQTT server URL", - default=SPARTN_PPSERVER, - ) - ap.add_argument( - "-P", - "--port", - required=False, - help="SPARTN MQTT server port", - type=int, - default=OUTPORT_SPARTN, - ) - ap.add_argument( - "-R", - "--region", - required=False, - help="SPARTN region code", - choices=["us", "eu", "au", "kr", "jp"], - default="eu", - ) - ap.add_argument( - "-M", - "--mode", - required=False, - help="SPARTN mode (0 - IP,1 - L-Band)", - type=int, - choices=[0, 1], - default=0, - ) - ap.add_argument( - "--topic_ip", - required=False, - help="Subscribe to SPARTN IP topic for the selected region", - type=int, - choices=[0, 1], - default=1, - ) - ap.add_argument( - "--topic_mga", - required=False, - help="Subscribe to UBX Assist-Now (MGA-EPH) topic", - type=int, - choices=[0, 1], - default=1, - ) - ap.add_argument( - "--topic_key", - required=False, - help="Subscribe to UBX Key (RXM-SPARTNKEY) topic", - type=int, - choices=[0, 1], - default=1, - ) - ap.add_argument( - "--tlscrt", - required=False, - help="Fully-qualified path to TLS cert (*.crt)", - default=getenv( - "MQTTCRT", default=path.join(Path.home(), f"device-{clientid}-pp-cert.crt") - ), - ) - ap.add_argument( - "--tlskey", - required=False, - help="Fully-qualified path to TLS key (*.pem)", - default=getenv( - "MQTTPEM", default=path.join(Path.home(), f"device-{clientid}-pp-key.pem") - ), - ) - ap.add_argument( - "--spartndecode", - required=False, - help="Decode payload?", - type=int, - choices=[0, 1], - default=0, - ) - ap.add_argument( - "--spartnkey", - required=False, - help="Decryption key for encrypted payloads", - default=getenv(ENV_MQTT_KEY, default=None), - ) - ap.add_argument( - "--spartnbasedate", - required=False, - help=( - "Decryption basedate for encrypted payloads (-1 = current datetime, " - "0 = use timetags from data stream, " - "other integer = 32-bit gnssTimeTag value)" - ), - type=int, - default=-1, - ) - ap.add_argument( - "--waittime", - required=False, - help="waitimer", - type=float, - default=0.5, - ) - ap.add_argument( - "--timeout", - required=False, - help="MQTT connection timeout (seconds)", - type=int, - default=TIMEOUT, - ) - ap.add_argument( - "--clioutput", - required=False, - help=( - f"CLI output type {OUTPUT_NONE} = none, " - f"{OUTPUT_FILE} = binary file, " - f"{OUTPUT_SERIAL} = serial port, " - f"{OUTPUT_SOCKET} = TCP socket server, " - f"{OUTPUT_SOCKET_TLS} = TCP socket server with TLS" - ), - type=int, - choices=[ - OUTPUT_NONE, - OUTPUT_FILE, - OUTPUT_SERIAL, - OUTPUT_SOCKET, - OUTPUT_SOCKET_TLS, - ], - default=OUTPUT_NONE, - ) - ap.add_argument( - "--output", - required=False, - help=( - "Output medium as formatted string. " - f"If clioutput = {OUTPUT_FILE}, format = file name (e.g. '/home/myuser/spartn.log'); " - f"If clioutput = {OUTPUT_SERIAL}, format = port@baudrate (e.g. '/dev/tty.ACM0@38400'); " - f"If clioutput = {OUTPUT_SOCKET} or {OUTPUT_SOCKET_TLS}, " - "format = hostip:port (e.g. '0.0.0.0:50010'). " - "NB: gnssmqttclient will have exclusive use of any serial or server port." - ), - default=None, - ) - kwargs = set_common_args("gnssmqttclient", ap) - - if kwargs.get("spartnbasedate", 0) == -1: - kwargs["spartnbasedate"] = None # will default to current datetime in pyspartn - - kwargs["errevent"] = Event() - cliout = int(kwargs.pop("clioutput", OUTPUT_NONE)) - try: - if cliout == OUTPUT_FILE: - filename = kwargs["output"] - with open(filename, "wb") as output: - kwargs["output"] = output - runclient(**kwargs) - elif cliout == OUTPUT_SERIAL: - port, baud = kwargs["output"].split("@") - with Serial(port, int(baud), timeout=3) as output: - kwargs["output"] = output - runclient(**kwargs) - elif cliout in (OUTPUT_SOCKET, OUTPUT_SOCKET_TLS): - host, port = kwargs["output"].split(":") - tls = cliout == OUTPUT_SOCKET_TLS - kwargs["output"] = Queue() - # socket server runs as background thread, piping - # output from mqtt client via a message queue - Thread( - target=runserver, - args=(host, int(port), kwargs["output"], 0, MAXCONNECTION, tls, NTRIP2), - daemon=True, - ).start() - runclient(**kwargs) - else: - kwargs["output"] = None - runclient(**kwargs) - except (KeyboardInterrupt, TimeoutError): - pass - - -if __name__ == "__main__": - main() diff --git a/src/pygnssutils/gnssstreamer_cli.py b/src/pygnssutils/gnssstreamer_cli.py index 89e72ec..4531622 100644 --- a/src/pygnssutils/gnssstreamer_cli.py +++ b/src/pygnssutils/gnssstreamer_cli.py @@ -17,7 +17,6 @@ Supported input channels: - NTRIP RTCM client - NTRIP SPARTN client - - MQTT SPARTN client - serial stream - file stream @@ -57,7 +56,6 @@ FORMAT_PARSED, FORMAT_PARSEDSTRING, INPUT_FILE, - INPUT_MQTT_SPARTN, INPUT_NONE, INPUT_NTRIP_RTCM, INPUT_NTRIP_SPARTN, @@ -73,7 +71,6 @@ OUTPUT_TEXT_FILE, UBXSIMULATOR, ) -from pygnssutils.gnssmqttclient import GNSSMQTTClient from pygnssutils.gnssntripclient import GNSSNTRIPClient from pygnssutils.gnssreader import ( ERR_LOG, @@ -162,34 +159,6 @@ def _setup_input_ntrip(app: object, datatype: str, **kwargs) -> object: return gnc -def _setup_input_mqtt(app: object, datatype: str, **kwargs) -> object: - """ - Set up MQTT SPARTN client as input data source. - - :param app: calling application (i.e. gnssstreamer) - :param datatype: "MQTT" - :returns: reference to MQTT client - :rtype: GNSSMQTTClient - """ - - # pylint: disable=unused-argument - - prot, hostname, port, path = parse_url(kwargs["input"]) - prot = 1 if prot == "https" else 0 - - gmq = GNSSMQTTClient(app) - gmq.start( - server=hostname, - port=port, - clientid=kwargs.get("rtkuser", "anon"), - region=path.lower(), # e.g. "eu" - mode=0, # IP (as opposed to 1 = L-Band) - output=kwargs["inqueue"], # send SPARTN data to receiver - ) - - return gmq - - def _setup_input_stream(app: object, datatype: str, **kwargs) -> object: """ Set up serial stream as input data source. @@ -379,8 +348,6 @@ def _run_streamer(stream, **kwargs): _setup_input_ntrip(gns, "RTCM", **kwargs) elif cliinput == INPUT_NTRIP_SPARTN: _setup_input_ntrip(gns, "SPARTN", **kwargs) - elif cliinput == INPUT_MQTT_SPARTN: - _setup_input_mqtt(gns, "MQTT", **kwargs) elif cliinput == INPUT_SERIAL: _setup_input_stream(gns, "SERIAL", **kwargs) elif cliinput == INPUT_FILE: @@ -568,7 +535,6 @@ def main(): f"CLI input type {INPUT_NONE} = none, " f"{INPUT_NTRIP_RTCM} = RTK NTRIP RTCM, " f"{INPUT_NTRIP_SPARTN} = RTK NTRIP SPARTN, " - f"{INPUT_MQTT_SPARTN} = RTK MQTT SPARTN, " f"{INPUT_SERIAL} = serial port, " f"{INPUT_FILE} = binary file" ), @@ -577,7 +543,6 @@ def main(): INPUT_NONE, INPUT_NTRIP_RTCM, INPUT_NTRIP_SPARTN, - INPUT_MQTT_SPARTN, INPUT_SERIAL, INPUT_FILE, ], @@ -592,8 +557,6 @@ def main(): "(e.g. 'http://rtk2go.com:2101/MOUNTPOINT'); " f"If cliinput = {INPUT_NTRIP_SPARTN}, format = full url " "(e.g. 'https://ppntrip.services.u-blox.com:2102/EU'); " - f"If cliinput = {INPUT_MQTT_SPARTN}, format = full url " - "(e.g. 'https://pp.services.u-blox.com:8883/eu', where /path signifies region); " f"If cliinput = {INPUT_SERIAL}, format = port@baudrate (e.g. '/dev/tty.ACM1@38400'); " f"If cliinput = {INPUT_FILE}, format = file name (e.g. '/home/myuser/ubxconfig.ubx'). " "NB: gnssstreamer will have exclusive use of any serial port." diff --git a/src/pygnssutils/mqttmessage.py b/src/pygnssutils/mqttmessage.py deleted file mode 100644 index 138e98d..0000000 --- a/src/pygnssutils/mqttmessage.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -mqttmessage.py - -MQTTMessage container class for MQTT topics with json payloads. - -Created on 1 Sep 2023 - -:author: semuadmin (Steve Smith) -:copyright: semuadmin © 2020 -:license: BSD 3-Clause -""" - -from io import BytesIO -from json import JSONDecodeError, load - - -class MQTTMessage: - """ - Container class for MQTT topics with json payloads. - """ - - def __init__(self, topic: str, payload: bytes): - """ - Constructor - - :param str topic: MQTT topic e.g. "\\\\pp\\\\frequencies\\\\Lb" - :param bytes payload: MQTT topic json payload as bytes - :raises: ValueError if payload is invalid json - """ - - self.identity = topic.upper() - try: - payjson = load(BytesIO(payload)) - self._parse_payload(payjson) - except JSONDecodeError as err: - raise ValueError( - f"Topic {topic} payload was not valid json - {err}" - ) from err - - def __str__(self) -> str: - """ - Human readable representation. - - :returns: human readable representation - :rtype: str - """ - - stg = f" 1: # element group - att = tmp + key + "_" - else: - att += key + "_" - self._parse_payload(val, att) - else: # value - setattr(self, att[:-1], pay) diff --git a/src/pygnssutils/rinex_globals.py b/src/pygnssutils/rinex_globals.py index 298eb79..37be9e0 100644 --- a/src/pygnssutils/rinex_globals.py +++ b/src/pygnssutils/rinex_globals.py @@ -52,7 +52,7 @@ NMEA = "nmea" OBS = "O" OMEGADOTREF = -2.6e-9 -PYRINEXCONV_VERSION = "0.1.7 Alpha" +PYRINEXCONV_VERSION = "0.2.0 Beta" QZS = "J" RINEX_CANCELLED = 2 RINEX_ERROR = 99 diff --git a/src/pygnssutils/rinex_helpers.py b/src/pygnssutils/rinex_helpers.py index c14cde6..743d4ac 100644 --- a/src/pygnssutils/rinex_helpers.py +++ b/src/pygnssutils/rinex_helpers.py @@ -1355,7 +1355,7 @@ def format_licenseofuse(lou: str = "") -> str: if lou == "": return "" - return f"{license:<{DATAWIDTH}}LICENSE OF USE\n" + return f"{lou:<{DATAWIDTH}}LICENSE OF USE\n" def format_stationinfo(station: str = "") -> str: diff --git a/tests/test_cli.py b/tests/test_cli.py index c375539..b6be59b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -55,11 +55,6 @@ def testntripclient(self): res = res.stdout.decode("utf-8") self.assertEqual(res[0:22], "usage: gnssntripclient") - def testsmqttlient(self): - res = run(["gnssmqttclient", "-h"], stdout=PIPE, check=False) - res = res.stdout.decode("utf-8") - self.assertEqual(res[0:21], "usage: gnssmqttclient") - if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] diff --git a/tests/test_static.py b/tests/test_static.py index c61a4da..7d34fa3 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -29,7 +29,6 @@ parse_config, parse_url, ) -from pygnssutils.mqttmessage import MQTTMessage from tests.test_sourcetable import TESTSRT from pygnssutils.gnssreader import GNSSMessage @@ -172,23 +171,6 @@ def testipprot2str(self): # test IP family to str self.assertEqual("IPv4", ipprot2str(AF_INET)) self.assertEqual("IPv6", ipprot2str(AF_INET6)) - def testparsemqttfreq(self): # test MQTTMessage constructor - topic = "/pp/frequencies/Lb" - payload = b'{\n "frequencies": {\n "us": {\n "current": {\n "value": "1556.29"\n }\n },\n "eu": {\n "current": {\n "value": "1545.26"\n }\n }\n }\n}' - parsed = MQTTMessage(topic, payload=payload) - self.assertEqual( - str(parsed), - "", - ) - payload2 = b'{\n "frequencies": {\n "us": {\n "current": {\n "value": {"msb": "1556.29", "lsb": "0.645"}\n }\n },\n "eu": {\n "current": {\n "value": "1545.26"\n }\n },\n "jp": {\n "current": {\n "value": "1548.23"\n }\n }\n}\n}' - parsed = MQTTMessage(topic, payload=payload2) - self.assertEqual( - str(parsed), - "", - ) - with self.assertRaises(ValueError): - MQTTMessage(topic, payload=b"arsebiscuits") - def testparseconfig(self): EXPECTED_RESULT = { "filename": "pygpsdata-MIXED3.log",