diff --git a/collectd-extensions/src/ptp_gnss_monitor.py b/collectd-extensions/src/ptp_gnss_monitor.py index cf626cf..4098897 100644 --- a/collectd-extensions/src/ptp_gnss_monitor.py +++ b/collectd-extensions/src/ptp_gnss_monitor.py @@ -115,6 +115,12 @@ def get_gps_data_by_session(session, device_path): signal_quality_db=SignalQualityDb(min=0, max=0, avg=0), ) try: + # - TPV and SKY report are independent information and could come in any order. + # - A GPS receiver might have satellite visibility data (a SKY report) but still not + # be able to calculate a usable position, hence sending a TPV report with mode: 0. + # - SKY report won't follow TPV, when GPS receiver is not functional. + tpv_report_scanned = False + sky_report_scanned = False for report in session: if report["class"] in ["VERSION", "WATCH", "DEVICE"]: continue @@ -138,12 +144,17 @@ def get_gps_data_by_session(session, device_path): message = f"{PLUGIN} {device_path} have not achieved satellite lock: {report}" collectd.debug(message) data.lock_state = 0 - # reset satellite_count and signal_quality_db - data.satellite_count = 0 - data.signal_quality_db = SignalQualityDb(min=0, max=0, avg=0) + # Do not change satellite_count and signal_quality_db in case + # SKY already scanned break else: data.lock_state = 1 + if sky_report_scanned: + break + + # Set TPV report scanned flag + tpv_report_scanned = True + # device key is optional in SKY class elif ( report["class"] == "SKY" @@ -153,14 +164,23 @@ def get_gps_data_by_session(session, device_path): # uSat key is optional in SKY class, Number of satellites used in navigation solution if "uSat" in report: data.satellite_count = report["uSat"] + else: + data.satellite_count = 0 # satellites key is optional in SKY class, List of satellite objects in skyview if "satellites" in report: data.signal_quality_db = get_signal_to_noise_ratio( report["satellites"] ) + else: + data.signal_quality_db = SignalQualityDb(min=0, max=0, avg=0) # All reports collected, No more polling required. - break + if tpv_report_scanned: + break + + # Set SKY report scanned flag + sky_report_scanned = True + except Exception as exc: # In case of parsing error, should be reported instead of throwing exception message = f"{PLUGIN} Programming error occured: {type(exc)}:{exc}" diff --git a/collectd-extensions/tests/test_ptp_gnss_monitor.py b/collectd-extensions/tests/test_ptp_gnss_monitor.py index ff0f7c1..0307ac2 100644 --- a/collectd-extensions/tests/test_ptp_gnss_monitor.py +++ b/collectd-extensions/tests/test_ptp_gnss_monitor.py @@ -321,6 +321,280 @@ class TestPtpMonitoring(unittest.TestCase): msg=f"actual {data} not equal to expected {expected_data} ", ) + def test_get_gps_data_by_session_sky_before_tpv_mode_2_valid(self): + # out of order test: SKY before TPV (with mode:2) + device_path = "/dev/gnss0" + session = [ + { + "class": "VERSION", + "release": "3.22", + "rev": "3.22", + "proto_major": 3, + "proto_minor": 14, + }, + { + "class": "DEVICES", + "devices": [ + { + "class": "DEVICE", + "path": "/dev/gnss0", + "activated": "2025-06-13T12:52:15.463Z", + "native": 0, + "bps": 9600, + "parity": "N", + "stopbits": 0, + "cycle": 1.0, + }, + { + "class": "DEVICE", + "path": "/dev/gnss1", + "activated": "2025-06-13T12:52:19.637Z", + "native": 0, + "bps": 9600, + "parity": "N", + "stopbits": 0, + "cycle": 1.0, + }, + ], + }, + { + "class": "WATCH", + "enable": True, + "json": True, + "nmea": False, + "raw": 0, + "scaled": False, + "timing": False, + "split24": False, + "pps": False, + }, + { + "class": "DEVICE", + "path": "/dev/gnss0", + "driver": "NMEA0183", + "activated": "2025-06-13T12:52:19.637Z", + "native": 0, + "bps": 9600, + "parity": "N", + "stopbits": 0, + "cycle": 1.0, + }, + { + "class": "SKY", + "device": "/dev/gnss0", + "time": "2019-04-10T20:27:52.000Z", + "xdop": 0.69, + "ydop": 0.68, + "vdop": 1.33, + "tdop": 0.88, + "hdop": 0.97, + "gdop": 1.86, + "pdop": 1.64, + "nSat": 4, + "uSat": 3, + "satellites": [ + { + "PRN": 5, + "el": 31.0, + "az": 86.0, + "ss": 45.2, + "used": True, + "gnssid": 0, + "svid": 5, + "health": 1, + }, + { + "PRN": 10, + "el": 10.0, + "az": 278.0, + "ss": 40.0, + "used": False, + "gnssid": 0, + "svid": 10, + "health": 1, + }, + { + "PRN": 13, + "el": 44.0, + "az": 53.0, + "ss": 47.4, + "used": True, + "gnssid": 0, + "svid": 13, + "health": 1, + }, + { + "PRN": 15, + "el": 80.0, + "az": 68.0, + "ss": 48.8, + "used": True, + "gnssid": 0, + "svid": 15, + "health": 1, + }, + ], + }, + {"class": "TPV", "device": "/dev/gnss0", "mode": 2}, + ] + snr = [45.2, 47.4, 48.8] + avg = sum(snr) / len(snr) + trunc_avg = int(avg * 1000) / 1000 + expected_signal_quality_db = ptp_monitoring.SignalQualityDb( + min=min(snr), max=max(snr), avg=trunc_avg + ) + expected_data = ptp_monitoring.GpsData( + gpsd_running=1, + lock_state=1, + satellite_count=3, + signal_quality_db=expected_signal_quality_db, + ) + + data = ptp_monitoring.get_gps_data_by_session(session, device_path) + mock_collectd.assert_not_called() + self.assertEqual( + data, + expected_data, + msg=f"actual {data} not equal to expected {expected_data} ", + ) + + def test_get_gps_data_by_session_sky_before_tpv_mode_1_valid(self): + # out of order test: SKY before TPV (with mode:1) + device_path = "/dev/gnss0" + session = [ + { + "class": "VERSION", + "release": "3.22", + "rev": "3.22", + "proto_major": 3, + "proto_minor": 14, + }, + { + "class": "DEVICES", + "devices": [ + { + "class": "DEVICE", + "path": "/dev/gnss0", + "activated": "2025-06-13T12:52:15.463Z", + "native": 0, + "bps": 9600, + "parity": "N", + "stopbits": 0, + "cycle": 1.0, + }, + { + "class": "DEVICE", + "path": "/dev/gnss1", + "activated": "2025-06-13T12:52:19.637Z", + "native": 0, + "bps": 9600, + "parity": "N", + "stopbits": 0, + "cycle": 1.0, + }, + ], + }, + { + "class": "WATCH", + "enable": True, + "json": True, + "nmea": False, + "raw": 0, + "scaled": False, + "timing": False, + "split24": False, + "pps": False, + }, + { + "class": "DEVICE", + "path": "/dev/gnss0", + "driver": "NMEA0183", + "activated": "2025-06-13T12:52:19.637Z", + "native": 0, + "bps": 9600, + "parity": "N", + "stopbits": 0, + "cycle": 1.0, + }, + { + "class": "SKY", + "device": "/dev/gnss0", + "time": "2019-04-10T20:27:52.000Z", + "xdop": 0.69, + "ydop": 0.68, + "vdop": 1.33, + "tdop": 0.88, + "hdop": 0.97, + "gdop": 1.86, + "pdop": 1.64, + "nSat": 4, + "uSat": 3, + "satellites": [ + { + "PRN": 5, + "el": 31.0, + "az": 86.0, + "ss": 45.2, + "used": True, + "gnssid": 0, + "svid": 5, + "health": 1, + }, + { + "PRN": 10, + "el": 10.0, + "az": 278.0, + "ss": 40.0, + "used": False, + "gnssid": 0, + "svid": 10, + "health": 1, + }, + { + "PRN": 13, + "el": 44.0, + "az": 53.0, + "ss": 47.4, + "used": True, + "gnssid": 0, + "svid": 13, + "health": 1, + }, + { + "PRN": 15, + "el": 80.0, + "az": 68.0, + "ss": 48.8, + "used": True, + "gnssid": 0, + "svid": 15, + "health": 1, + }, + ], + }, + {"class": "TPV", "device": "/dev/gnss0", "mode": 1}, + ] + snr = [45.2, 47.4, 48.8] + avg = sum(snr) / len(snr) + trunc_avg = int(avg * 1000) / 1000 + expected_signal_quality_db = ptp_monitoring.SignalQualityDb( + min=min(snr), max=max(snr), avg=trunc_avg + ) + expected_data = ptp_monitoring.GpsData( + gpsd_running=1, + lock_state=0, + satellite_count=3, + signal_quality_db=expected_signal_quality_db, + ) + + data = ptp_monitoring.get_gps_data_by_session(session, device_path) + mock_collectd.assert_not_called() + self.assertEqual( + data, + expected_data, + msg=f"actual {data} not equal to expected {expected_data} ", + ) + def test_get_gps_data_by_session_with_garbase_data(self): device_path = "/dev/gnss0" session = [