diaggpsd 2026.11.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. diaggpsd-2026.11.8/LICENSE +201 -0
  2. diaggpsd-2026.11.8/NOTICE +33 -0
  3. diaggpsd-2026.11.8/PKG-INFO +203 -0
  4. diaggpsd-2026.11.8/README.md +170 -0
  5. diaggpsd-2026.11.8/pyproject.toml +71 -0
  6. diaggpsd-2026.11.8/setup.cfg +4 -0
  7. diaggpsd-2026.11.8/src/diaggpsd/__init__.py +1 -0
  8. diaggpsd-2026.11.8/src/diaggpsd/__main__.py +4 -0
  9. diaggpsd-2026.11.8/src/diaggpsd/at.py +321 -0
  10. diaggpsd-2026.11.8/src/diaggpsd/diag_setup.py +377 -0
  11. diaggpsd-2026.11.8/src/diaggpsd/diaggpsd.py +611 -0
  12. diaggpsd-2026.11.8/src/diaggpsd/dlf_to_jsonl.py +229 -0
  13. diaggpsd-2026.11.8/src/diaggpsd/dump_pos_report.py +150 -0
  14. diaggpsd-2026.11.8/src/diaggpsd/gnss_compare.py +1051 -0
  15. diaggpsd-2026.11.8/src/diaggpsd/gnss_compare_ref_filter.py +396 -0
  16. diaggpsd-2026.11.8/src/diaggpsd/gnss_packets.py +81 -0
  17. diaggpsd-2026.11.8/src/diaggpsd/gpsd_server.py +339 -0
  18. diaggpsd-2026.11.8/src/diaggpsd/modem_profiles.py +155 -0
  19. diaggpsd-2026.11.8/src/diaggpsd/nmea.py +307 -0
  20. diaggpsd-2026.11.8/src/diaggpsd/rinex_writer.py +1287 -0
  21. diaggpsd-2026.11.8/src/diaggpsd.egg-info/PKG-INFO +203 -0
  22. diaggpsd-2026.11.8/src/diaggpsd.egg-info/SOURCES.txt +46 -0
  23. diaggpsd-2026.11.8/src/diaggpsd.egg-info/dependency_links.txt +1 -0
  24. diaggpsd-2026.11.8/src/diaggpsd.egg-info/entry_points.txt +6 -0
  25. diaggpsd-2026.11.8/src/diaggpsd.egg-info/requires.txt +5 -0
  26. diaggpsd-2026.11.8/src/diaggpsd.egg-info/top_level.txt +1 -0
  27. diaggpsd-2026.11.8/tests/test_bad_cmd_handling.py +242 -0
  28. diaggpsd-2026.11.8/tests/test_diag_setup_at_port_6585.py +86 -0
  29. diaggpsd-2026.11.8/tests/test_diag_setup_diag_port_6624.py +49 -0
  30. diaggpsd-2026.11.8/tests/test_diaggpsd_broker_client_5548.py +108 -0
  31. diaggpsd-2026.11.8/tests/test_disarm_on_subscribe_failure.py +143 -0
  32. diaggpsd-2026.11.8/tests/test_dlf_to_jsonl_input_gate.py +84 -0
  33. diaggpsd-2026.11.8/tests/test_gnss_compare_cli.py +27 -0
  34. diaggpsd-2026.11.8/tests/test_gnss_compare_oemdre_reason.py +59 -0
  35. diaggpsd-2026.11.8/tests/test_gnss_compare_rollover.py +76 -0
  36. diaggpsd-2026.11.8/tests/test_gnss_packets_import.py +77 -0
  37. diaggpsd-2026.11.8/tests/test_gpsd_tpv_no_fix.py +83 -0
  38. diaggpsd-2026.11.8/tests/test_json_bytes_fields.py +45 -0
  39. diaggpsd-2026.11.8/tests/test_lazy_diaggrok_5547.py +53 -0
  40. diaggpsd-2026.11.8/tests/test_mask_own_5530.py +146 -0
  41. diaggpsd-2026.11.8/tests/test_nmea_5535.py +252 -0
  42. diaggpsd-2026.11.8/tests/test_nmea_epoch_gate_5551.py +145 -0
  43. diaggpsd-2026.11.8/tests/test_rawfd_transport_reset.py +51 -0
  44. diaggpsd-2026.11.8/tests/test_readme_commands.py +85 -0
  45. diaggpsd-2026.11.8/tests/test_rinex_glonass_epoch.py +110 -0
  46. diaggpsd-2026.11.8/tests/test_rinex_time_unknown.py +65 -0
  47. diaggpsd-2026.11.8/tests/test_rinex_tx_time.py +149 -0
  48. diaggpsd-2026.11.8/tests/test_udp_listen_transport.py +59 -0
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,33 @@
1
+ diaggpsd: live GNSS from a cellular modem's DIAG stream to gpsd clients + RINEX
2
+ Copyright 2026 Luke Jenkins
3
+
4
+ This product includes software developed by Luke Jenkins
5
+ (https://github.com/lukejenkins).
6
+
7
+ Licensed under the Apache License, Version 2.0. See the LICENSE file
8
+ for the full license text.
9
+
10
+ ------------------------------------------------------------------------
11
+ Acknowledgements
12
+ ------------------------------------------------------------------------
13
+
14
+ Reverse-engineering and documentation of DIAG GNSS log records
15
+ was pioneered by projects including openpilot's qcomgpsd, QCSuper, and
16
+ SCAT. diaggpsd does not incorporate their code: the GNSS log-record
17
+ struct definitions it relies on live in the separate diaggrok library and
18
+ were re-derived clean-room there. diaggpsd itself is a first-party
19
+ orchestration layer, but it stands on the reverse-engineering those
20
+ projects contributed to the community.
21
+
22
+ diaggpsd feeds gpsd clients by speaking the gpsd JSON protocol; it is not
23
+ derived from, and does not replace, the gpsd daemon.
24
+
25
+ ------------------------------------------------------------------------
26
+ Dependencies
27
+ ------------------------------------------------------------------------
28
+
29
+ No third-party code is incorporated into diaggpsd itself. At install time
30
+ it depends on two sibling libraries — diaggrok (DIAG GNSS parsers) and
31
+ diagmunge (DIAG transport) — and, for the live AT-port path, on the
32
+ optional `serial` extra (pyserial). Those are distributed separately
33
+ under their own licenses and are not included in this repository.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: diaggpsd
3
+ Version: 2026.11.8
4
+ Summary: Live GNSS from a Qualcomm modem's DIAG stream — serves gpsd clients and writes RINEX
5
+ Author: Luke Jenkins
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/lukejenkins/diaggpsd
8
+ Project-URL: Source, https://github.com/lukejenkins/diaggpsd
9
+ Project-URL: Changelog, https://github.com/lukejenkins/diaggpsd/releases
10
+ Keywords: diag,modem,cellular,baseband,lte,5g
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Telecommunications Industry
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Communications
23
+ Classifier: Topic :: System :: Hardware
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ License-File: NOTICE
28
+ Requires-Dist: diaggrok>=2026.11.8
29
+ Requires-Dist: diagmunge>=2026.11.8
30
+ Provides-Extra: serial
31
+ Requires-Dist: pyserial; extra == "serial"
32
+ Dynamic: license-file
33
+
34
+ # diaggpsd
35
+
36
+ Live GNSS from a Qualcomm modem's **DIAG** stream — served to gpsd clients and
37
+ written to RINEX. diaggpsd subscribes to a Qualcomm modem's GNSS DIAG log
38
+ records, decodes them, and makes the fixes and raw measurements available three
39
+ ways:
40
+
41
+ - **gpsd clients** — a gpsd-compatible JSON server on TCP 2947, so `gpspipe`,
42
+ `cgps`, `xgps`, `gpsmon`, JOSM, and anything built on `libgps` just work.
43
+ - **Raw JSON** — one packet per line on stdout or a plain TCP port, for logging
44
+ and custom tooling without the gpsd protocol overhead.
45
+ - **RINEX** — observation/navigation files for post-processing pipelines.
46
+
47
+ diaggpsd *feeds* gpsd clients by speaking the gpsd protocol; it is not the gpsd
48
+ daemon and does not replace it. It reads the modem directly over DIAG, so it
49
+ works where no kernel GPS driver or NMEA port is exposed.
50
+
51
+ ## Layout
52
+
53
+ diaggpsd is one Python package, `diaggpsd`, under `src/diaggpsd/`. Its
54
+ console commands (`diaggpsd`, `diaggpsd-diag-setup`, `diaggpsd-dlf-to-jsonl`,
55
+ `diaggpsd-dump-pos-report`, `diaggpsd-gnss-compare`) are the supported way to
56
+ run it; `python -m diaggpsd` starts the daemon too. The modules:
57
+
58
+ | Module | Role |
59
+ |--------|------|
60
+ | `diaggpsd.py` | the live daemon — argument parsing, DIAG subscription, main loop, gpsd/raw output |
61
+ | `gnss_packets.py` | thin re-export shim over the `diaggrok` GNSS parsers |
62
+ | `at.py` | AT-command interface for GNSS engine setup (Quectel-oriented) |
63
+ | `gpsd_server.py` | the gpsd JSON protocol server (TCP 2947) |
64
+ | `diag_setup.py` | DIAG log-mask setup helper |
65
+ | `dlf_to_jsonl.py` | offline: decode a DIAG capture to JSONL |
66
+ | `dump_pos_report.py` | offline: dump position reports from a live/captured stream |
67
+ | `gnss_compare.py` / `gnss_compare_ref_filter.py` | compare decoded fixes against a reference track |
68
+ | `rinex_writer.py` | RINEX observation/navigation output |
69
+ | `modem_profiles.py` | per-chipset NV/AT setup profiles |
70
+
71
+ ## Dependencies
72
+
73
+ diaggpsd is first-party Python. At install time it pulls two sibling libraries
74
+ that hold the reverse-engineered pieces:
75
+
76
+ - **[diaggrok](https://github.com/lukejenkins/diaggrok)** — the DIAG GNSS
77
+ log-record parsers (imported through `gnss_packets.py`).
78
+ - **[diagmunge](https://github.com/lukejenkins/diagmunge)** — the DIAG transport
79
+ core (`DiagClient`) and replay.
80
+
81
+ The live AT-port path additionally needs **pyserial**, installed via the
82
+ optional `serial` extra. DIAG capture, replay, decode, RINEX, and the gpsd
83
+ server are all pyserial-free.
84
+
85
+ From [PyPI](https://pypi.org/project/diaggpsd/):
86
+
87
+ ```bash
88
+ pip install diaggpsd # daemon + gpsd server + offline tooling
89
+ pip install "diaggpsd[serial]" # + the live AT-port path (pyserial)
90
+ ```
91
+
92
+ diaggrok and diagmunge come from PyPI with it. Each PyPI version is the same
93
+ code as the GitHub tag of the same number (`diaggpsd==2026.11.8` is tag
94
+ `v2026.11.8`). From a clone, `pip install .` works the same way.
95
+
96
+ Requires Python 3.11+.
97
+
98
+ ## Quick start
99
+
100
+ Two steps: `diaggpsd-diag-setup` prepares the modem once (NV items, the GNSS
101
+ session, AT commands), then `diaggpsd` streams. `diaggpsd` itself never sends AT
102
+ commands, so the AT options (`--at-port`, `--no-at`, `--cold-start`) belong to
103
+ the setup step.
104
+
105
+ ### USB-connected modem
106
+
107
+ ```bash
108
+ # One-time setup: enable GNSS logging and start the engine over AT.
109
+ diaggpsd-diag-setup --chipset sdx55 --diag-port /dev/ttyUSB0 --at-port /dev/ttyUSB2
110
+
111
+ # Stream to gpsd clients.
112
+ diaggpsd --diag-port /dev/ttyUSB0 --debug
113
+
114
+ # In another terminal:
115
+ gpspipe -w 127.0.0.1
116
+ ```
117
+
118
+ `diaggpsd-diag-setup --list-profiles` lists the chipset profiles. Add
119
+ `--cold-start` to delete cached assistance, or `--no-at` when the GNSS engine is
120
+ already running or the AT path is unavailable. On QuecOpen builds the host-side
121
+ AT port is bridged to an SMD device node by an AP-side service; if that bridge
122
+ is stopped, AT commands can fail silently, so `--no-at` is the robust choice
123
+ there.
124
+
125
+ ### Network-attached modem
126
+
127
+ A modem that relays its DIAG stream over the network is read without a serial
128
+ port. In the two passive modes the device sets the log mask and `diaggpsd`
129
+ never writes one.
130
+
131
+ ```bash
132
+ # The device pushes a DGE1-framed UDP stream (diagbarf):
133
+ diaggpsd --diag-port udp-listen:0.0.0.0:12399 --debug
134
+
135
+ # The device runs diag_socket_log and connects to this host:
136
+ diaggpsd --diag-port tcp --debug
137
+ # on the modem: diag_socket_log -a <host_ip> -p 2500
138
+ ```
139
+
140
+ ## Transport modes
141
+
142
+ | Mode | Invocation | Who sets the log mask |
143
+ |------|------------|-----------------------|
144
+ | USB / serial (default) | `--diag-port /dev/ttyUSB0` | diaggpsd |
145
+ | TCP `diag_socket_log` | `--diag-port tcp` or `tcp:0.0.0.0:2500` | diaggpsd |
146
+ | UDP, DGE1 framing (diagbarf) | `--diag-port udp-listen` or `udp-listen:0.0.0.0:12399` | the device |
147
+ | UDP, QMDL2 chunks (`diag_udp_fwd`) | `--diag-port udp` or `udp:0.0.0.0:2500` | the device |
148
+
149
+ When diaggpsd sets the log mask, it clears the mask again on exit with one
150
+ LOG_CONFIG DISABLE, draining queued frames for up to 10 s while it waits for
151
+ the ack. That also happens when the subscribe itself fails, for example when
152
+ a slow `diag_socket_log` tunnel backs up until a mask ack never arrives (exit
153
+ status 1). If no ack comes, the log says so; power-cycle the modem
154
+ (`AT+CFUN=1,1`) and send a zero log mask before the next run.
155
+
156
+ ## Output modes
157
+
158
+ - **stdout JSON** (default) — one parsed packet per line
159
+ (`PositionReport`, `GpsMeasurementReport`, `GlonassMeasurementReport`,
160
+ `OemdreMeasurementReport`, `OemdreSvPolyReport`). Suppress with `--no-stdout`.
161
+ - **Raw TCP** — `--raw-port 5000` broadcasts the JSON lines to connected TCP
162
+ clients.
163
+ - **gpsd server** (TCP 2947) — the standard gpsd JSON protocol. Disable with
164
+ `--no-gpsd`, move with `--gpsd-port PORT`.
165
+
166
+ ## Offline decoding: input formats for `dlf_to_jsonl`
167
+
168
+ How a DIAG capture was produced determines how to decode it. Two on-disk formats
169
+ both commonly carry the `.dlf` extension but are **not** interchangeable:
170
+
171
+ | Source | On-disk format | Invocation |
172
+ |--------|----------------|------------|
173
+ | QCSuper (`--dlf`) | DLF records (per-record length/type header) | `dlf_to_jsonl capture.dlf out.jsonl` |
174
+ | raw HDLC dump | 0x7E-delimited HDLC byte stream (no DLF envelope) | `dlf_to_jsonl --format bin --verify-crc capture.dlf out.jsonl` |
175
+
176
+ Using DLF mode on a raw-HDLC file yields a near-zero parse rate (the 0x7E
177
+ delimiters do not form a valid DLF record header). DLF mode on QCSuper output is
178
+ correct and needs no `--verify-crc` (QCSuper validates CRCs at capture time).
179
+
180
+ ## GPS lock notes
181
+
182
+ After start, the modem needs time to acquire satellites: typically 10–30 s
183
+ outdoors with fresh XTRA assistance, 1–10 min on a cold start with clear sky.
184
+ The first `PositionReport` with `pos_source=2` (Kalman fix) marks a usable
185
+ position; the gpsd server begins pushing TPV at that point.
186
+
187
+ ## gpsd protocol subset
188
+
189
+ `VERSION` (on connect), `WATCH` (subscribe/unsubscribe), `TPV` (from
190
+ `PositionReport`), `SKY` (from GPS/GLONASS measurement reports), and minimal
191
+ `DEVICES` / `POLL` stubs for client compatibility. `TPV.mode`: 1 = no fix,
192
+ 2 = 2D, 3 = 3D (Kalman).
193
+
194
+ ## Acknowledgements
195
+
196
+ Reverse-engineering of Qualcomm DIAG GNSS log records was pioneered by projects
197
+ including openpilot's qcomgpsd, QCSuper, and SCAT. diaggpsd relies on struct
198
+ definitions re-derived clean-room in the `diaggrok` library and stands on the
199
+ documentation those projects contributed to the community.
200
+
201
+ ## License
202
+
203
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -0,0 +1,170 @@
1
+ # diaggpsd
2
+
3
+ Live GNSS from a Qualcomm modem's **DIAG** stream — served to gpsd clients and
4
+ written to RINEX. diaggpsd subscribes to a Qualcomm modem's GNSS DIAG log
5
+ records, decodes them, and makes the fixes and raw measurements available three
6
+ ways:
7
+
8
+ - **gpsd clients** — a gpsd-compatible JSON server on TCP 2947, so `gpspipe`,
9
+ `cgps`, `xgps`, `gpsmon`, JOSM, and anything built on `libgps` just work.
10
+ - **Raw JSON** — one packet per line on stdout or a plain TCP port, for logging
11
+ and custom tooling without the gpsd protocol overhead.
12
+ - **RINEX** — observation/navigation files for post-processing pipelines.
13
+
14
+ diaggpsd *feeds* gpsd clients by speaking the gpsd protocol; it is not the gpsd
15
+ daemon and does not replace it. It reads the modem directly over DIAG, so it
16
+ works where no kernel GPS driver or NMEA port is exposed.
17
+
18
+ ## Layout
19
+
20
+ diaggpsd is one Python package, `diaggpsd`, under `src/diaggpsd/`. Its
21
+ console commands (`diaggpsd`, `diaggpsd-diag-setup`, `diaggpsd-dlf-to-jsonl`,
22
+ `diaggpsd-dump-pos-report`, `diaggpsd-gnss-compare`) are the supported way to
23
+ run it; `python -m diaggpsd` starts the daemon too. The modules:
24
+
25
+ | Module | Role |
26
+ |--------|------|
27
+ | `diaggpsd.py` | the live daemon — argument parsing, DIAG subscription, main loop, gpsd/raw output |
28
+ | `gnss_packets.py` | thin re-export shim over the `diaggrok` GNSS parsers |
29
+ | `at.py` | AT-command interface for GNSS engine setup (Quectel-oriented) |
30
+ | `gpsd_server.py` | the gpsd JSON protocol server (TCP 2947) |
31
+ | `diag_setup.py` | DIAG log-mask setup helper |
32
+ | `dlf_to_jsonl.py` | offline: decode a DIAG capture to JSONL |
33
+ | `dump_pos_report.py` | offline: dump position reports from a live/captured stream |
34
+ | `gnss_compare.py` / `gnss_compare_ref_filter.py` | compare decoded fixes against a reference track |
35
+ | `rinex_writer.py` | RINEX observation/navigation output |
36
+ | `modem_profiles.py` | per-chipset NV/AT setup profiles |
37
+
38
+ ## Dependencies
39
+
40
+ diaggpsd is first-party Python. At install time it pulls two sibling libraries
41
+ that hold the reverse-engineered pieces:
42
+
43
+ - **[diaggrok](https://github.com/lukejenkins/diaggrok)** — the DIAG GNSS
44
+ log-record parsers (imported through `gnss_packets.py`).
45
+ - **[diagmunge](https://github.com/lukejenkins/diagmunge)** — the DIAG transport
46
+ core (`DiagClient`) and replay.
47
+
48
+ The live AT-port path additionally needs **pyserial**, installed via the
49
+ optional `serial` extra. DIAG capture, replay, decode, RINEX, and the gpsd
50
+ server are all pyserial-free.
51
+
52
+ From [PyPI](https://pypi.org/project/diaggpsd/):
53
+
54
+ ```bash
55
+ pip install diaggpsd # daemon + gpsd server + offline tooling
56
+ pip install "diaggpsd[serial]" # + the live AT-port path (pyserial)
57
+ ```
58
+
59
+ diaggrok and diagmunge come from PyPI with it. Each PyPI version is the same
60
+ code as the GitHub tag of the same number (`diaggpsd==2026.11.8` is tag
61
+ `v2026.11.8`). From a clone, `pip install .` works the same way.
62
+
63
+ Requires Python 3.11+.
64
+
65
+ ## Quick start
66
+
67
+ Two steps: `diaggpsd-diag-setup` prepares the modem once (NV items, the GNSS
68
+ session, AT commands), then `diaggpsd` streams. `diaggpsd` itself never sends AT
69
+ commands, so the AT options (`--at-port`, `--no-at`, `--cold-start`) belong to
70
+ the setup step.
71
+
72
+ ### USB-connected modem
73
+
74
+ ```bash
75
+ # One-time setup: enable GNSS logging and start the engine over AT.
76
+ diaggpsd-diag-setup --chipset sdx55 --diag-port /dev/ttyUSB0 --at-port /dev/ttyUSB2
77
+
78
+ # Stream to gpsd clients.
79
+ diaggpsd --diag-port /dev/ttyUSB0 --debug
80
+
81
+ # In another terminal:
82
+ gpspipe -w 127.0.0.1
83
+ ```
84
+
85
+ `diaggpsd-diag-setup --list-profiles` lists the chipset profiles. Add
86
+ `--cold-start` to delete cached assistance, or `--no-at` when the GNSS engine is
87
+ already running or the AT path is unavailable. On QuecOpen builds the host-side
88
+ AT port is bridged to an SMD device node by an AP-side service; if that bridge
89
+ is stopped, AT commands can fail silently, so `--no-at` is the robust choice
90
+ there.
91
+
92
+ ### Network-attached modem
93
+
94
+ A modem that relays its DIAG stream over the network is read without a serial
95
+ port. In the two passive modes the device sets the log mask and `diaggpsd`
96
+ never writes one.
97
+
98
+ ```bash
99
+ # The device pushes a DGE1-framed UDP stream (diagbarf):
100
+ diaggpsd --diag-port udp-listen:0.0.0.0:12399 --debug
101
+
102
+ # The device runs diag_socket_log and connects to this host:
103
+ diaggpsd --diag-port tcp --debug
104
+ # on the modem: diag_socket_log -a <host_ip> -p 2500
105
+ ```
106
+
107
+ ## Transport modes
108
+
109
+ | Mode | Invocation | Who sets the log mask |
110
+ |------|------------|-----------------------|
111
+ | USB / serial (default) | `--diag-port /dev/ttyUSB0` | diaggpsd |
112
+ | TCP `diag_socket_log` | `--diag-port tcp` or `tcp:0.0.0.0:2500` | diaggpsd |
113
+ | UDP, DGE1 framing (diagbarf) | `--diag-port udp-listen` or `udp-listen:0.0.0.0:12399` | the device |
114
+ | UDP, QMDL2 chunks (`diag_udp_fwd`) | `--diag-port udp` or `udp:0.0.0.0:2500` | the device |
115
+
116
+ When diaggpsd sets the log mask, it clears the mask again on exit with one
117
+ LOG_CONFIG DISABLE, draining queued frames for up to 10 s while it waits for
118
+ the ack. That also happens when the subscribe itself fails, for example when
119
+ a slow `diag_socket_log` tunnel backs up until a mask ack never arrives (exit
120
+ status 1). If no ack comes, the log says so; power-cycle the modem
121
+ (`AT+CFUN=1,1`) and send a zero log mask before the next run.
122
+
123
+ ## Output modes
124
+
125
+ - **stdout JSON** (default) — one parsed packet per line
126
+ (`PositionReport`, `GpsMeasurementReport`, `GlonassMeasurementReport`,
127
+ `OemdreMeasurementReport`, `OemdreSvPolyReport`). Suppress with `--no-stdout`.
128
+ - **Raw TCP** — `--raw-port 5000` broadcasts the JSON lines to connected TCP
129
+ clients.
130
+ - **gpsd server** (TCP 2947) — the standard gpsd JSON protocol. Disable with
131
+ `--no-gpsd`, move with `--gpsd-port PORT`.
132
+
133
+ ## Offline decoding: input formats for `dlf_to_jsonl`
134
+
135
+ How a DIAG capture was produced determines how to decode it. Two on-disk formats
136
+ both commonly carry the `.dlf` extension but are **not** interchangeable:
137
+
138
+ | Source | On-disk format | Invocation |
139
+ |--------|----------------|------------|
140
+ | QCSuper (`--dlf`) | DLF records (per-record length/type header) | `dlf_to_jsonl capture.dlf out.jsonl` |
141
+ | raw HDLC dump | 0x7E-delimited HDLC byte stream (no DLF envelope) | `dlf_to_jsonl --format bin --verify-crc capture.dlf out.jsonl` |
142
+
143
+ Using DLF mode on a raw-HDLC file yields a near-zero parse rate (the 0x7E
144
+ delimiters do not form a valid DLF record header). DLF mode on QCSuper output is
145
+ correct and needs no `--verify-crc` (QCSuper validates CRCs at capture time).
146
+
147
+ ## GPS lock notes
148
+
149
+ After start, the modem needs time to acquire satellites: typically 10–30 s
150
+ outdoors with fresh XTRA assistance, 1–10 min on a cold start with clear sky.
151
+ The first `PositionReport` with `pos_source=2` (Kalman fix) marks a usable
152
+ position; the gpsd server begins pushing TPV at that point.
153
+
154
+ ## gpsd protocol subset
155
+
156
+ `VERSION` (on connect), `WATCH` (subscribe/unsubscribe), `TPV` (from
157
+ `PositionReport`), `SKY` (from GPS/GLONASS measurement reports), and minimal
158
+ `DEVICES` / `POLL` stubs for client compatibility. `TPV.mode`: 1 = no fix,
159
+ 2 = 2D, 3 = 3D (Kalman).
160
+
161
+ ## Acknowledgements
162
+
163
+ Reverse-engineering of Qualcomm DIAG GNSS log records was pioneered by projects
164
+ including openpilot's qcomgpsd, QCSuper, and SCAT. diaggpsd relies on struct
165
+ definitions re-derived clean-room in the `diaggrok` library and stands on the
166
+ documentation those projects contributed to the community.
167
+
168
+ ## License
169
+
170
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "diaggpsd"
3
+ version = "2026.11.8"
4
+ description = "Live GNSS from a Qualcomm modem's DIAG stream — serves gpsd clients and writes RINEX"
5
+ license = "Apache-2.0"
6
+ requires-python = ">=3.11"
7
+ readme = "README.md"
8
+ authors = [{ name = "Luke Jenkins" }]
9
+ keywords = ["diag", "modem", "cellular", "baseband", "lte", "5g"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: Science/Research",
14
+ "Intended Audience :: Telecommunications Industry",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Topic :: Communications",
23
+ "Topic :: System :: Hardware",
24
+ ]
25
+ # Hard, import-time dependencies — both inward edges to already-public siblings:
26
+ # - diagmunge: the DIAG transport core (DiagClient + DIAG_LOG_F), imported at
27
+ # module scope in diaggpsd.py / diag_setup.py / dump_pos_report.py and via
28
+ # diagmunge.munge.replay in dlf_to_jsonl.py.
29
+ # - diaggrok: the GNSS log-record parsers, imported at module scope through
30
+ # the gnss_packets.py re-export shim.
31
+ # Edge direction is inward-only: diaggpsd -> {diagmunge, diaggrok}. Never the
32
+ # reverse. Bare PyPI names, each floored at the sibling release this package's
33
+ # full suite was verified against in an isolated venv; PyPI
34
+ # rejects direct-URL deps. Release order: diaggrok and diagmunge must be on
35
+ # PyPI before this package is uploaded.
36
+ dependencies = [
37
+ "diaggrok>=2026.11.8",
38
+ "diagmunge>=2026.11.8",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ # The live AT-port path (at.py) imports pyserial lazily, inside the serial
43
+ # adapter constructor — only talking to a real modem's AT port needs it. DIAG
44
+ # capture, replay, decode, RINEX, and the gpsd server are all pyserial-free.
45
+ # pyserial IS on PyPI, so it stays a bare name.
46
+ serial = ["pyserial"]
47
+
48
+ [project.scripts]
49
+ # The live daemon: DIAG -> gpsd JSON server (+ optional RINEX).
50
+ diaggpsd = "diaggpsd.diaggpsd:main"
51
+ # Offline tooling shipped alongside the daemon.
52
+ diaggpsd-diag-setup = "diaggpsd.diag_setup:main"
53
+ diaggpsd-dlf-to-jsonl = "diaggpsd.dlf_to_jsonl:main"
54
+ diaggpsd-dump-pos-report = "diaggpsd.dump_pos_report:main"
55
+ diaggpsd-gnss-compare = "diaggpsd.gnss_compare:main"
56
+
57
+ [project.urls]
58
+ Homepage = "https://github.com/lukejenkins/diaggpsd"
59
+ Source = "https://github.com/lukejenkins/diaggpsd"
60
+ Changelog = "https://github.com/lukejenkins/diaggpsd/releases"
61
+
62
+ [build-system]
63
+ requires = ["setuptools>=77.0"]
64
+ build-backend = "setuptools.build_meta"
65
+
66
+ # The modules live flat in the source tree beside each other; the public carve
67
+ # packages them under src/diaggpsd/ so a pip install adds one top-level name
68
+ # (`diaggpsd`), not twelve generic ones like `at` and `nmea`. `gnss_packets`
69
+ # is a thin re-export shim over the diaggrok parsers.
70
+ [tool.setuptools.packages.find]
71
+ where = ["src"]