Skip to content

Commit 2213213

Browse files
committed
update uni_handler
1 parent 2a282f2 commit 2213213

6 files changed

Lines changed: 235 additions & 33 deletions

File tree

src/pygpsclient/globals.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,31 @@
423423
"PVTGeodetic7": "RTK FIXED",
424424
"PVTGeodetic8": "RTK FLOAT",
425425
"PVTGeodetic10": "PPP",
426+
"BESTNAV0": "NO FIX",
427+
"BESTNAV1": "FIXEDPOS", # FIXEDPOS Position fixed by the FIX POSITION command
428+
"BESTNAV2": "FIXEDHMSL", # FIXEDHEIGHT Not supported currently
429+
"BESTNAV8": "DOPP", # DOPPLER_VELOCITY Velocity computed using instantaneous Doppler
430+
"BESTNAV16": "3D", # SINGLE Single point positioning
431+
"BESTNAV17": "RTK FLOAT", # SRDIFF Pseudorange differential solution
432+
"BESTNAV18": "SBAS", # SBAS SBAS positioning
433+
"BESTNAV32": "RTK FLOAT", # L1_FLOAT L1 float solution
434+
"BESTNAV33": "RTK FLOAT", # IONOFREE_FLOAT Ionosphere-free float solution
435+
"BESTNAV34": "RTK FLOAT", # NARROW_FLOAT Narrow-lane float solution
436+
"BESTNAV48": "RTK FIXED", # L1_INT L1 fixed solution
437+
"BESTNAV49": "RTK FIXED", # WIDE_INT Wide-lane fixed solution
438+
"BESTNAV50": "RTK FIXED", # NARROW_INT Narrow-lane fixed solution
439+
"BESTNAV52": "DR", # INS Inertial navigation solution
440+
"BESTNAV53": "3D+DR", # INS_PSRSP Integrated solution of INS and single point pos
441+
"BESTNAV54": "RTK+DR", # INS_PSRDIFF Integrated solution of INS and pseudorange diff pos
442+
"BESTNAV55": "RTK FLOAT+DR", # INS_RTKFLOAT Integrated solution of INS and RTK float
443+
"BESTNAV56": "RTK FIXED+DR", # INS_RTKFIXED Integrated solution of INS and RTK fix
444+
"BESTNAV68": "RTK FIXED+DR", # INS_RTKFIXED Integrated solution of INS and RTK fix
445+
"BESTNAV69": "PPP", # PPP
426446
}
427447
"""
428448
Map of fix values to descriptions.
429-
The keys in this map are a concatenation of NMEA/UBX
430-
message identifier and attribute value e.g.
449+
The keys in this map are a concatenation of
450+
message identity and attribute value e.g.
431451
GGA1: GGA + quality = 1
432452
NAV-STATUS3: NAV-STATUS + gpsFix = 3
433453
(valid for NMEA >=4)

src/pygpsclient/helpers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from typing import Any, Literal
4343

4444
from pygnssutils import version as PGVERSION
45-
from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine
45+
from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine, leapsecond
4646
from pynmeagps import version as NMEAVERSION
4747
from pyqgc import version as QGCVERSION
4848
from pyrtcm import version as RTCMVERSION
@@ -1447,20 +1447,24 @@ def valid_geom(geom: str) -> bool:
14471447
return regexgeom.match(geom) is not None
14481448

14491449

1450-
def wnotow2date(wno: int, tow: int) -> datetime:
1450+
def wnotow2date(wno: int, tow: int, ls: int | NoneType = None) -> datetime:
14511451
"""
1452-
Get datetime from GPS Week number (Wno) and Time of Week (Tow).
1452+
Get datetime from GPS Week number (Wno), Time of Week (Tow)
1453+
and leapsecond offset.
14531454
14541455
GPS Epoch 0 = 6th Jan 1980
14551456
14561457
:param int wno: week number
14571458
:param int tow: time of week
1459+
:param int ls: leapsecond offset
14581460
:return: datetime
14591461
:rtype: datetime
14601462
"""
14611463

1464+
if ls is None:
1465+
ls = leapsecond(datetime.now())
14621466
dat = GPSEPOCH0 + timedelta(days=wno * 7)
1463-
dat += timedelta(seconds=tow)
1467+
dat += timedelta(seconds=tow - ls)
14641468
return dat
14651469

14661470

src/pygpsclient/init_presets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"Unicore UM98n Save Current Configuration to NVM CONFIRM; saveconfig",
102102
"Unicore UM98n Force COLD reset CONFIRM; reset",
103103
"Unicore UM98n Enable NMEA on current port; gpgsa 1; gpgga 1; gpgll 1; gpgsv 4; gpvtg 1; gprmc 1",
104-
"Unicore UM98n Enable UNI on current port; PVTSLNB 1; SATELLITEB 4; BESTNAV 1; STADOP 1",
104+
"Unicore UM98n Enable UNI on current port; PVTSLNB 1; SATSINFOB 4; BESTNAV 1; STADOP 1",
105105
"Unicore UM98n Query Version; version",
106106
"Unicore UM98n Query Config; config",
107107
"Unicore UM98n Stop All Output on COM1; unlog COM1",

src/pygpsclient/skyview_frame.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,9 @@ def update_frame(self):
110110
"""
111111

112112
data = self.__app.gnss_status.gsv_data
113-
show_unused = self.__app.configuration.get("unusedsat_b")
114113
siv = len(data)
115-
siv = siv if show_unused else siv - unused_sats(data)
116114
sel = sum(1 for (_, _, ele, _, _, _) in data.values() if ele not in ("", None))
117-
# ignore if cno are all zero and 'show_unused' is not set,
118-
# or if elevation values are all null
115+
# ignore if elevation values are all null
119116
if siv <= 0 or sel <= 0:
120117
return
121118

@@ -125,11 +122,7 @@ def update_frame(self):
125122
for val in sorted(data.values(), key=lambda x: x[4]): # sort by ascending C/N0
126123
try:
127124
gnssId, prn, ele, azi, cno, _ = val
128-
if (
129-
(cno == 0 and not show_unused)
130-
or ele in ("", None)
131-
or azi in ("", None)
132-
):
125+
if ele in ("", None) or azi in ("", None):
133126
continue
134127
x, y = self._canvas.d2xy(int(azi), int(ele))
135128
_, ol_col = GNSS_LIST[gnssId]

src/pygpsclient/uni_handler.py

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""
22
uni_handler.py
33
4-
WORK IN PROGRESS - AWAITING PARSER DEVELOPMENT
5-
64
Unicore GNSS Protocol handler - handles all incoming UNI messages
75
86
Parses individual UNI (Unicore UM98n GNSS) messages (using pyunignss library)
@@ -18,6 +16,31 @@
1816
"""
1917

2018
import logging
19+
from time import time
20+
21+
from pyunigps import UNIMessage
22+
23+
from pygpsclient.helpers import fix2desc, wnotow2date
24+
25+
SATSINFO_GNSSID = {
26+
0: 0, # GPS
27+
1: 6, # GLONASS
28+
2: 1, # SBAS
29+
3: 2, # GAL
30+
4: 3, # BDS
31+
5: 5, # QZSS
32+
6: 7, # IRNSS
33+
}
34+
35+
SATELLITE_GNSSID = {
36+
0: 0, # GPS
37+
1: 6, # GLONASS
38+
2: 1, # SBAS
39+
5: 2, # GALILEO
40+
6: 3, # BEIDOU
41+
7: 5, # QZSS
42+
9: 7, # NAVIC
43+
}
2144

2245

2346
class UNIHandler:
@@ -40,7 +63,7 @@ def __init__(self, app):
4063
self._parsed_data = None
4164

4265
# pylint: disable=unused-argument
43-
def process_data(self, raw_data: bytes, parsed_data: object):
66+
def process_data(self, raw_data: bytes, parsed_data: UNIMessage):
4467
"""
4568
Process relevant UNI message types
4669
@@ -49,5 +72,169 @@ def process_data(self, raw_data: bytes, parsed_data: object):
4972
"""
5073

5174
if raw_data is None:
52-
pass
75+
return
5376
# self.logger.debug(f"data received {parsed_data.identity}")
77+
self.__app.gnss_status.utc = wnotow2date(
78+
parsed_data.wno, int(parsed_data.tow / 1000), parsed_data.leapsecond
79+
) # datetime.time
80+
if parsed_data.identity in ("BESTNAV", "BESTNAVH"):
81+
self._process_BESTNAV(parsed_data)
82+
elif parsed_data.identity in ("PVTSLT",):
83+
self._process_PVTSLT(parsed_data)
84+
elif parsed_data.identity in (
85+
"ADRNAV",
86+
"ADRNAVH",
87+
"PPPNAV",
88+
"SPPNAV",
89+
"SPPNAVH",
90+
):
91+
self._process_ADRNAV(parsed_data)
92+
elif parsed_data.identity in ("SATSINFO",):
93+
self._process_SATSINFO(parsed_data)
94+
elif parsed_data.identity in ("SATELLITE",):
95+
self._process_SATELLITE(parsed_data)
96+
elif parsed_data.identity in ("STADOP", "ADRDOP", "PPPDOP"):
97+
self._process_STADOP(parsed_data)
98+
99+
def _process_pos(self, lat: float, lon: float, hmsl: float, undulation: float):
100+
"""
101+
Process lat/lon/hmsl/hae.
102+
103+
:param float lat: lat
104+
:param float lon: lon
105+
:param float hmsl: hmsl in meters
106+
:param float undulation: separation in meters
107+
"""
108+
109+
self.__app.gnss_status.lat = lat
110+
self.__app.gnss_status.lon = lon
111+
self.__app.gnss_status.alt = hmsl
112+
self.__app.gnss_status.hae = hmsl + undulation
113+
114+
def _process_fix(self, postype: int):
115+
"""
116+
Process fix type.
117+
118+
:param int postype: attribute representing fix type
119+
"""
120+
121+
self.__app.gnss_status.fix = fix2desc("BESTNAV", postype)
122+
self.__app.gnss_status.diff_corr = ("RTK" in self.__app.gnss_status.fix) or (
123+
"PPP" in self.__app.gnss_status.fix
124+
)
125+
126+
def _process_BESTNAV(self, data: UNIMessage):
127+
"""
128+
Process BESTNAV sentence - Navigation position velocity time solution.
129+
130+
:param UNIMessage data: BESTNAV parsed message
131+
"""
132+
133+
self._process_pos(data.lat, data.lon, data.hmsl, data.undulation)
134+
self._process_fix(data.postype)
135+
self.__app.gnss_status.sip = data.numsolnsvs
136+
self.__app.gnss_status.diff_age = data.diffage
137+
self.__app.gnss_status.speed = data.horspd
138+
self.__app.gnss_status.track = data.trkgnd
139+
self.__app.gnss_status.diff_station = data.stationid
140+
141+
def _process_PVTSLT(self, data: UNIMessage):
142+
"""
143+
Process PVTSLT sentence - Navigation position velocity time solution.
144+
145+
:param UNIMessage data: PVTSLT parsed message
146+
"""
147+
148+
self._process_pos(
149+
data.psrposlat, data.psrposlon, data.psrposhmsl, data.undulation
150+
)
151+
self._process_fix(data.psrpostype)
152+
self.__app.gnss_status.sip = data.psrpossolnsvs
153+
self.__app.gnss_status.speed = data.psrvelground
154+
self.__app.gnss_status.track = data.headingdegree
155+
self.__app.gnss_status.pdop = data.pdop
156+
self.__app.gnss_status.hdop = data.hdop
157+
self.__app.gnss_status.diff_age = data.bestpos_diffage
158+
159+
def _process_ADRNAV(self, data: UNIMessage):
160+
"""
161+
Process ADRNAV, PPPNAV, SPPNAV sentences.
162+
163+
:param UNIMessage data: ADRNAV/PPPNAV/SPPNAV parsed message
164+
"""
165+
166+
self._process_pos(data.lat, data.lon, data.hmsl, data.undulation)
167+
self._process_fix(data.postype)
168+
169+
def _process_SATSINFO(self, data: UNIMessage):
170+
"""
171+
Process SATSINFO sentences - Space Vehicle Information for all
172+
GNSS constellations.
173+
174+
:param UNIMessage data: SATSINFO parsed message
175+
"""
176+
177+
self.__app.gnss_status.gsv_data = {}
178+
num_siv = int(data.numsat)
179+
now = time()
180+
181+
for i in range(num_siv):
182+
idx = f"_{i+1:02d}"
183+
gnssId = SATSINFO_GNSSID[getattr(data, "sysstatus" + idx + "_01")]
184+
svid = getattr(data, "prn" + idx)
185+
elev = getattr(data, "elev" + idx)
186+
azim = getattr(data, "azi" + idx)
187+
cno = getattr(data, "cno" + idx + "_01")
188+
self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
189+
gnssId,
190+
svid,
191+
elev,
192+
azim,
193+
cno,
194+
now,
195+
)
196+
197+
self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data)
198+
199+
def _process_SATELLITE(self, data: UNIMessage):
200+
"""
201+
Process SATELLITE sentences - Space Vehicle Information for a
202+
specific GNSS constellation (7 in total).
203+
204+
:param UNIMessage data: SATSINFO parsed message
205+
"""
206+
207+
gnssId = SATELLITE_GNSSID[getattr(data, "gnss")]
208+
for gnss, prn in list(self.__app.gnss_status.gsv_data.keys()):
209+
if gnss == gnssId:
210+
self.__app.gnss_status.gsv_data.pop((gnss, prn))
211+
212+
num_siv = int(data.numsat)
213+
now = time()
214+
for i in range(num_siv):
215+
idx = f"_{i+1:02d}"
216+
svid = getattr(data, "prn" + idx)
217+
elev = getattr(data, "elv" + idx)
218+
azim = getattr(data, "azi" + idx)
219+
cno = 0 # cno not available from this message
220+
self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
221+
gnssId,
222+
svid,
223+
elev,
224+
azim,
225+
cno,
226+
now,
227+
)
228+
229+
self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data)
230+
231+
def _process_STADOP(self, data: UNIMessage):
232+
"""
233+
Process STADOP/ADRDOP/PPPDOP sentences - DOP Information.
234+
235+
:param UNIMessage data: STADOP/ADRDOP/PPPDOP parsed message
236+
"""
237+
238+
self.__app.gnss_status.pdop = data.pdop
239+
self.__app.gnss_status.hdop = data.hdop
240+
self.__app.gnss_status.vdop = data.vdop

tests/test_static.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -406,17 +406,17 @@ def testwnotow2date(self):
406406
(2263, 518400),
407407
]
408408
dats = [
409-
"2023-01-01 00:00:00",
410-
"2005-11-05 00:00:00",
411-
"2020-08-20 00:00:00",
412-
"2014-03-16 00:00:00",
413-
"2023-05-21 00:00:00",
414-
"2023-05-27 00:00:00",
409+
"2022-12-31 23:59:42",
410+
"2005-11-04 23:59:43",
411+
"2020-08-19 23:59:44",
412+
"2014-03-15 23:59:45",
413+
"2023-05-20 23:59:46",
414+
"2023-05-26 23:59:47",
415415
]
416416
for i, (wno, tow) in enumerate(vals):
417-
self.assertEqual(str(wnotow2date(wno, tow)), dats[i])
418-
wno, tow = date2wnotow(datetime(2020, 4, 12))
419-
self.assertEqual(wnotow2date(wno, tow), datetime(2020, 4, 12))
417+
dat = wnotow2date(wno, tow, 18-i)
418+
# print(f'"{dat}",')
419+
self.assertEqual(str(dat), dats[i])
420420

421421
def testbitsval(self):
422422
bits = [(7, 1), (8, 8), (22, 2), (24, 4), (40, 16)]
@@ -428,13 +428,11 @@ def testbitsval(self):
428428
self.assertEqual(res, EXPECTED_RESULT[i])
429429

430430
def testparserxm(self):
431-
EXPECTED_RESULT = [
432-
("0c00", datetime(1988, 3, 1, 7, 40)),
433-
("290900", datetime(1988, 7, 4, 2, 40)),
434-
]
431+
EXPECTED_RESULT = [('0c00', datetime(1988, 3, 1, 7, 39, 42)), ('290900', datetime(1988, 7, 4, 2, 39, 42))]
435432
RXM_SPARTNKEY = b"\xb5b\x026\x19\x00\x01\x02\x00\x00\x00\x02+\x00\xd0Y\xc8\r\x00\x03+\x00\x00\xdfl\x0e\x0c\x00)\t\x00D;"
436433
msg = UBXReader.parse(RXM_SPARTNKEY)
437434
res = parse_rxmspartnkey(msg)
435+
# print(f'"{res}",')
438436
self.assertEqual(res, EXPECTED_RESULT)
439437

440438
def testmapqcompress(self):

0 commit comments

Comments
 (0)