From 0aa68e14d29585196f22e82056c41bda61ff97d6 Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Sat, 1 Aug 2026 09:53:43 +0100
Subject: [PATCH 1/4] remove gnssmqttclient
---
README.md | 46 +--
RELEASE_NOTES.md | 4 +
docs/pygnssutils.rst | 24 --
pyproject.toml | 2 -
src/pygnssutils/__init__.py | 2 -
src/pygnssutils/_version.py | 2 +-
src/pygnssutils/gnssmqttclient.py | 481 --------------------------
src/pygnssutils/gnssmqttclient_cli.py | 262 --------------
src/pygnssutils/gnssstreamer_cli.py | 37 --
src/pygnssutils/mqttmessage.py | 74 ----
src/pygnssutils/rinex_globals.py | 2 +-
tests/test_static.py | 18 -
12 files changed, 9 insertions(+), 945 deletions(-)
delete mode 100644 src/pygnssutils/gnssmqttclient.py
delete mode 100644 src/pygnssutils/gnssmqttclient_cli.py
delete mode 100644 src/pygnssutils/mqttmessage.py
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..a5ddf40 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,6 @@ classifiers = [
dependencies = [
"certifi>=2025.0.0",
- "paho-mqtt>=2.1.0",
"pyserial>=3.5",
"pyspartn>=1.0.8",
"pyubx2>=1.3.6",
@@ -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""
- return stg
-
- def _parse_payload(self, pay: dict, att: str = ""):
- """
- Recursively traverse json payload structure and set
- attribute for each element value.
-
- :param dict pay: json payload as dict
- :param str att: attribute name
- """
-
- tmp = att
- if isinstance(pay, dict): # nested elements
- for key, val in pay.items():
- if len(pay) > 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/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",
From 98688f344390af92f8d08f347d498f791cc7ecde Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Sat, 1 Aug 2026 10:00:36 +0100
Subject: [PATCH 2/4] update cli tests
---
tests/test_cli.py | 5 -----
1 file changed, 5 deletions(-)
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']
From 8a8664c1b024d4e8bad7f83bca5f0d11221cd005 Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Tue, 11 Aug 2026 08:30:08 +0100
Subject: [PATCH 3/4] update min pynmeagps ver
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index a5ddf40..a6707b1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,7 +38,7 @@ dependencies = [
"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",
From 338a14790a50129ce075b55558e5a03a99794b17 Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Sun, 16 Aug 2026 09:39:22 +0100
Subject: [PATCH 4/4] fix rinex licenseofuse formatting
---
src/pygnssutils/rinex_helpers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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: