Compare commits

...

97 commits

Author SHA1 Message Date
Arnaud Ferraris
c13d7bcb58 d/changelog: release version 0.4.2-1 2023-10-04 12:24:56 +02:00
Arnaud Ferraris
686fb560ae d/control: drop now-unneeded pydbus dependency 2023-10-04 12:24:09 +02:00
Arnaud Ferraris
3eecde511b New upstream release 2023-10-04 12:15:54 +02:00
Arnaud Ferraris
c82cf9e70f Update upstream source from tag 'upstream/0.4.2'
Update to upstream version '0.4.2'
with Debian dir 7222150841
2023-10-04 12:15:54 +02:00
Arnaud Ferraris
6ea71b18f2 New upstream version 0.4.2 2023-10-04 12:15:53 +02:00
Teemu Ikonen
81569df8e2 Release 0.4.2 2023-09-23 14:31:05 +03:00
Teemu Ikonen
ccaa2db75d flatpak: Update Gnome runtime to version 45 2023-09-23 13:54:52 +03:00
Teemu Ikonen
90384a0e27 Print DOPs (PDOP, HDOP, VDOP) on a single line 2023-09-23 13:27:33 +03:00
Teemu Ikonen
a5453cf85f Add 'Geoidal separation' field to dataframe 2023-09-23 13:03:38 +03:00
Teemu Ikonen
5d6059416e nmea: Rename 'geo_sep' field to 'geoid_sep', parse it also from GGA 2023-09-23 13:02:56 +03:00
Teemu Ikonen
85c6bfefd7 nmea: Fix iget() 2023-09-23 12:34:22 +03:00
Teemu Ikonen
c615a432f4 Parse 'fixtime' also from GNS and GGA, remove unused 'time' from data dict 2023-08-06 17:18:25 +03:00
Teemu Ikonen
f1d04c6868 Use last_mode as default GSA mode to avoid 'Lock lost' log spam 2023-07-07 16:51:24 +03:00
Teemu Ikonen
2a5785292f nmeasource: Keep update_cb running on UnixSocketNmeaSource 2023-07-07 16:26:07 +03:00
Teemu Ikonen
41f4b196cc Don't print source model if it does not exist 2023-07-07 16:02:11 +03:00
Teemu Ikonen
2c5d3a7e93 nmeasource: Add manufacturer string to gnss-share source 2023-07-07 16:02:11 +03:00
Teemu Ikonen
8f863ae042 Update copyright years 2023-07-07 15:43:29 +03:00
Teemu Ikonen
84ea03f704 Release 0.4.1 2023-05-26 20:20:51 +03:00
Teemu Ikonen
69551fa7a5 mm_glib_source: Add a quirk to disable MSB in SDM845 / OP6 2023-05-26 20:07:54 +03:00
Teemu Ikonen
a895bfc32f Also detect quirks when autodetecting source 2023-05-25 17:36:56 +03:00
Teemu Ikonen
da78c02a68 Add NMEA source autodetection 2023-05-25 17:28:44 +03:00
Teemu Ikonen
0f49eb2670 README.md: update 2023-05-13 14:24:35 +03:00
Teemu Ikonen
0af37d1dc3 flatpak: Convert build manifest to YAML 2023-05-05 12:22:12 +03:00
Teemu Ikonen
38bc1f7eb5 flatpak: Add filesystem read permission to /run/gnss-share.sock 2023-05-05 11:50:32 +03:00
Teemu Ikonen
aec07d5fd4 setup.py: set Development Status to Beta 2023-04-19 14:22:35 +03:00
Teemu Ikonen
6d1a10f96a nmea: Create a non-naive fix datetime 2023-04-19 14:22:12 +03:00
Teemu Ikonen
98da3e4ffa mm_glib_source: Handle None from get_signaled_gps_nmea() 2023-03-23 13:53:08 +02:00
Teemu Ikonen
f77ce58ee8 Release 0.4.0 2023-03-22 20:35:04 +02:00
Teemu Ikonen
88e53d34bb nmea: Parse 'num_sats' also from GGA sentences 2023-03-22 20:35:04 +02:00
Teemu Ikonen
89f6ac85db nmea: Move GGA timestamp from 'fixtime' to 'time' 2023-03-22 20:35:04 +02:00
Teemu Ikonen
c3115dd751 Output 'n/a' for missing mode_indicator 2023-03-22 20:35:04 +02:00
Teemu Ikonen
782499e27b nmea: Get system time as non-naive datetime object 2023-03-22 17:48:21 +02:00
Teemu Ikonen
fbfd588f78 appdata: Improve description 2023-03-22 16:31:09 +02:00
Teemu Ikonen
51eb4bb714 flatpak: Use ModemManager release tarball 2023-03-22 16:12:59 +02:00
Teemu Ikonen
83164bb80f flatpak: Update python3-requirements 2023-03-22 16:12:59 +02:00
Teemu Ikonen
4d73be8c73 requirements.txt: Remove pydbus 2023-03-22 16:12:59 +02:00
Teemu Ikonen
e6205bd9e5 mm_glib_source: Ignore error when enabling AGPS MSB
AGPS enablement sometimes timeouts on Oneplus 6.

Also improve logging of NMEA initialization errors.
2023-03-22 16:12:59 +02:00
Teemu Ikonen
d6e4a71380 mm_glib_source: Ignore error in disconnect when closing an uninitialized source 2023-03-22 16:12:59 +02:00
Teemu Ikonen
a4a599a6c5 mm_glib_source: Enable AGPS_MSB only if it's in modem capabilities 2023-03-22 16:12:59 +02:00
Teemu Ikonen
2bb1b70994 mm_glib_source: Disconnect update cb in initialization 2023-03-22 16:12:59 +02:00
Teemu Ikonen
8c0f1587f4 Remove modem_manager_defs.py 2023-03-22 16:12:59 +02:00
Teemu Ikonen
7fd0d42f04 Add .editorconfig file 2023-03-22 16:12:59 +02:00
Teemu Ikonen
ffe6a6221a Merge pull request 'add-mm-as-flatpak-module' (#10) from ferenc/satellite:add-mm-as-flatpak-module into main
Reviewed-on: https://codeberg.org/tpikonen/satellite/pulls/10
2023-03-22 14:09:45 +00:00
Ferenc Géczi
9de70b5446 Add ModemManager to flatpak as module
Signed-off-by: Ferenc Géczi <ferenc.gm@gmail.com>
2023-03-22 00:00:00 +00:00
Ferenc Géczi
7206198321 Update to Gnome runtime 44
Signed-off-by: Ferenc Géczi <ferenc.gm@gmail.com>
2023-03-22 00:00:00 +00:00
Teemu Ikonen
0b2cc89635 Remove mm_pydbus_source.py (ModemManagerPyDBusNmeaSource) 2023-01-30 22:28:03 +02:00
Teemu Ikonen
17baef902d Refactor '--source' CLI arg handling, add 'mm' source
Use QuectelTalker quirk in 'quectel' source, add a plain ModemManager
source 'mm' without quirks.

Fix '--source' arg help string formatting.
2023-01-30 22:28:03 +02:00
Teemu Ikonen
7115b1697c mm_glib_source: Support quirks, add QuectelTalker quirk 2023-01-30 22:28:03 +02:00
Teemu Ikonen
58f4433cda Add ModemManagerGLibNmeaSource, use it 2023-01-30 22:28:03 +02:00
Teemu Ikonen
3ebf88d0f7 Linter config improvements, code style fixes 2023-01-30 22:24:00 +02:00
Evangelos Ribeiro Tzaras
7f5075e0ab Document and release 0.3.1-1 2023-01-22 16:07:10 +01:00
Evangelos Ribeiro Tzaras
473ffec331 Update upstream source from tag 'upstream/0.3.1'
Update to upstream version '0.3.1'
with Debian dir 262782492d3d908dd5f3694cc0974be311bc4667
2023-01-22 15:57:33 +01:00
Evangelos Ribeiro Tzaras
5ffa466048 d/watch: Remove filename mangling
While uscan downloaded (and symlinked) the correct tarball, it exited
with 1.
2023-01-22 15:57:33 +01:00
Teemu Ikonen
760f80fb49 Split PyDBus based ModemManager source to its own file 2023-01-16 16:21:19 +02:00
Evangelos Ribeiro Tzaras
f6e8cdd30f New upstream version 0.3.1 2022-12-03 18:18:07 +01:00
Teemu Ikonen
0861c78c76 Yet Another Error Handling Refactor 2022-11-18 17:10:09 +02:00
Teemu Ikonen
93cb27fb27 Simplify error handling in update() 2022-11-18 14:21:32 +02:00
Teemu Ikonen
bef48e4056 Update display also when new NMEAs are not received 2022-11-18 14:07:41 +02:00
Teemu Ikonen
ffd3f29ae3 Log messages to the app window before initializing
Use GLib.timeout_add instead of idle_add to have the window drawn and
messages logged before starting source initialization.
2022-11-17 21:05:31 +02:00
Teemu Ikonen
f15dd098e3 More GLib.SOURCE_REMOVE usage in return values 2022-11-17 21:04:47 +02:00
Teemu Ikonen
2ed6f27786 Release 0.3.1 2022-11-17 18:58:42 +02:00
Teemu Ikonen
f71906723c appdata: Fix screenshot links to point to flathub 2022-11-17 18:55:19 +02:00
Teemu Ikonen
a1431c8785 Release 0.3.0 2022-11-17 18:38:29 +02:00
Teemu Ikonen
ef089aa991 README.md: Move screenshots to the flathub repo 2022-11-17 18:27:25 +02:00
Teemu Ikonen
21857aa3f4 Merge pull request 'misc-cleanups' (#8) from devrtz/satellite:misc-cleanups into main
Reviewed-on: https://codeberg.org/tpikonen/satellite/pulls/8
2022-11-16 15:19:30 +01:00
Evangelos Ribeiro Tzaras
9d6d5d5322 application: Remove unnecessary return values
Most of these are handlers for signals which according to the C
documentation don't expect any return values, see below:

"activate" from GApplication:
void
user_function (GApplication *application,
               gpointer      user_data)

"clicked" from GtkButton:
void
user_function (GtkButton *button,
               gpointer   user_data)

"released" from GtkGestureMultiPress:
void
user_function (GtkGestureMultiPress *gesture,
               int                   n_press,
               double                x,
               double                y,
               gpointer              user_data)
2022-11-08 14:53:08 +01:00
Evangelos Ribeiro Tzaras
6abb6297c6 application: Prefer named constants in GSourceFunc
As opposed to True and False, this makes the purpose immediately clear
and is a recommended practice.
2022-11-08 14:51:45 +01:00
Evangelos Ribeiro Tzaras
326164cc97 application: Remove unused return values from update()
The return value was never used, so it should be removed.
2022-11-08 14:50:40 +01:00
Teemu Ikonen
4d5a9cc46e nmeasource: Rename NmeaSource.restore() method to close() 2022-11-06 22:37:29 +02:00
Teemu Ikonen
0644484db0 nmeasource: Disconnect from MM updates when quitting 2022-11-06 22:36:26 +02:00
Teemu Ikonen
1092aa3a31 Remove unused arg from SatelliteApp.update() 2022-11-06 21:55:19 +02:00
Teemu Ikonen
966633133a Set main_box sensitive to False if updates timeout 2022-11-06 21:55:19 +02:00
Teemu Ikonen
53a71b7fb8 widgets: Remove unused LabelBarChart widget 2022-11-06 21:55:19 +02:00
Teemu Ikonen
0374b9e7f7 Remove prints, add string arg to ModemError and log it 2022-11-06 21:55:19 +02:00
Teemu Ikonen
4717c146e4 Replace is_on_mobile_screen() with have_touchscreen() 2022-11-06 21:55:19 +02:00
Teemu Ikonen
cb6a018cf4 setup.cfg: Set flake8 max-line-length=88 2022-11-06 21:55:19 +02:00
Teemu Ikonen
447ed22f56 nmeasource: Raise ModemError when modem is in a bad state (e.g. no SIM) 2022-11-06 21:55:19 +02:00
Teemu Ikonen
ad381023ca nmea: Get the 'valid' key also from GSA (mode > 1) 2022-11-06 21:55:19 +02:00
Teemu Ikonen
247b7a5683 Define appname and app_id only once 2022-11-06 21:55:19 +02:00
Teemu Ikonen
1f1435ee2a Use Gio.Application signal handlers for startup, shutdown etc. 2022-11-06 21:55:19 +02:00
Teemu Ikonen
19db5edc9a Return to mainloop after calling Gtk.Application.quit()
The quit() function does not exit immediately.
2022-11-06 21:55:19 +02:00
Teemu Ikonen
940208e7a3 Make GErrors during init fatal 2022-11-06 21:55:19 +02:00
Teemu Ikonen
e3290957c7 nmea: Prefilter NMEA sentences to please pynmea2 2022-11-06 21:55:19 +02:00
Teemu Ikonen
9b8f991e49 nmea: Remove dead code 2022-11-06 21:55:19 +02:00
Teemu Ikonen
28978d0906 README.md: Update with gnss-share info, add links 2022-11-06 21:55:19 +02:00
Teemu Ikonen
63f1dd652d Merge pull request 'Introduce NmeaSource based on gnss-share' (#5) from devrtz/satellite:gnss-share into main
Reviewed-on: https://codeberg.org/tpikonen/satellite/pulls/5
2022-11-06 20:54:27 +01:00
Evangelos Ribeiro Tzaras
c32f7be5db application: Log exceptions when using the GnssShareNmeaSource 2022-11-04 09:02:06 +01:00
Evangelos Ribeiro Tzaras
78554c1afd Add plumbing enabling use of GnssShareNmeaSource 2022-11-04 09:02:06 +01:00
Evangelos Ribeiro Tzaras
b76c8850c0 Add UnixNmeaSource and GnssShareNmeaSource
gnss-share is used to share a /dev/gnssX GPS character device and
provide access to the data over a unix socket.

gnss-share currently works with sensors from STM and is used on the
Librem5.
2022-11-04 09:02:06 +01:00
Evangelos Ribeiro Tzaras
7701e998e1 Allow specifying NmeaSource on application startup
This adds another startup option '-s' to select the source
and runs the source specific initialization routines.

This patch moves source specific bits into a init_X_Y_source() function
leaving the common bits in init_source()
2022-11-04 09:00:30 +01:00
tpikonen
58f3849108 Merge pull request 'Update to Gnome runtime 43' (#7) from ferenc/satellite:feat/flatpak-runtime-update into main
Reviewed-on: https://codeberg.org/tpikonen/satellite/pulls/7
2022-10-25 20:04:00 +02:00
Ferenc Géczi
f7c85c2dd1 Update to Gnome runtime 43
Signed-off-by: Ferenc Géczi <ferenc.gm@gmail.com>
2022-10-25 00:00:00 +00:00
Teemu Ikonen
c108403b89 Only connect edge-overshot signal when on mobile screen
Add function is_on_mobile_screen(widget) which determines the size of
the monitor on which the widget (or it's parent) is realized.

Use this function to determine whether to show a pulley menu on top edge
overshot or not.
2022-09-07 17:27:14 +03:00
Teemu Ikonen
10b992dcb7 Do not request new data when updating barchart with gestures 2022-09-07 16:05:40 +03:00
Teemu Ikonen
b121db5477 ReplayNmeaSource._really_get(): Return None if NMEA file could not be read 2022-09-07 15:34:43 +03:00
Teemu Ikonen
914015cd33 update(): Return if we get() empty list or None as nmeas 2022-09-07 15:33:16 +03:00
Teemu Ikonen
9f93eb7b1a Display exception messages correctly 2022-09-07 15:28:31 +03:00
29 changed files with 630 additions and 403 deletions

26
.editorconfig Normal file
View file

@ -0,0 +1,26 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# 4 space indentation
[*.{py,java,r,R}]
indent_style = space
indent_size = 4
# 2 space indentation
[*.{js,json,y{a,}ml,html,cwl}]
indent_style = space
indent_size = 2
[*.{md,Rmd,rst}]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2

View file

@ -1,12 +1,13 @@
# Satellite
![The main view with a GPS fix](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot3.png)
![Logging](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot2.png)
![Expanded satellite SNR view](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot5.png)
![Speedometer and track recording](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot-track.png)
![The main view with a GPS fix](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-fix.png)
![Logging](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-log.png)
![Expanded satellite SNR view](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-snr.png)
![Speedometer and track recording](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-track.png)
Satellite is an adaptive GTK / libhandy application which displays global navigation satellite system
(GNSS: GPS et al.) data obtained from modemmanager API. It can also save your position to a GPX-file.
Satellite is an adaptive GTK3 / libhandy application which displays global navigation satellite system
(GNSS: GPS et al.) data obtained from [ModemManager](https://www.freedesktop.org/wiki/Software/ModemManager/)
or [gnss-share](https://gitlab.com/postmarketOS/gnss-share). It can also save your position to a GPX-file.
## License
@ -14,7 +15,7 @@ GPL-3.0
## Dependencies:
python 3.6+, gi, Gtk, libhandy, pydbus, pynmea2, gpxpy
python 3.6+, gi, Gtk3, libhandy, libmm-glib, pynmea2, gpxpy
## Installing and running
@ -44,9 +45,9 @@ Run the script `bin/satellite`.
Run
pip3 install --user ./
pip install --user ./
in the source tree root.
in the source tree root (use `pipx` instead of `pip` if necessary).
This creates an executable Python script in `$HOME/.local/bin/satellite`.
@ -54,7 +55,7 @@ This creates an executable Python script in `$HOME/.local/bin/satellite`.
Run
flatpak-builder --install --user build-dir flatpak/page.codeberg.tpikonen.satellite.json
flatpak-builder --install --user build-dir flatpak/page.codeberg.tpikonen.satellite.yaml
in the source tree root to install a local build to the user flatpak repo.

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import os

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2021-2022 Teemu Ikonen -->
<!-- Copyright 2021-2023 Teemu Ikonen -->
<!-- SPDX-License-Identifier: GPL-3.0-only -->
<component type="desktop-application">
<id>page.codeberg.tpikonen.satellite</id>
@ -9,10 +9,11 @@
<summary>Check your GPS reception and save your tracks</summary>
<description>
<p>Satellite displays global navigation satellite system (GNSS: that's GPS,
Galileo, Glonass etc.) data obtained from the ModemManager API. You can use
it to check the navigation satellite signal strength in your location and
see your speed, coordinates and other parameters once a fix is obtained.
It can also save GPX-tracks of your travels.</p>
Galileo, Glonass etc.) data obtained from an NMEA source in your device.
Currently the ModemManager and gnss-share APIs are supported. You can use
it to check the navigation satellite signal strength and see your speed,
coordinates and other parameters once a fix is obtained. It can also save
GPX-tracks of your travels.</p>
</description>
<launchable type="desktop-id">page.codeberg.tpikonen.satellite.desktop</launchable>
<url type="homepage">https://codeberg.org/tpikonen/satellite</url>
@ -32,22 +33,71 @@
<screenshots>
<screenshot type="default">
<caption>The main view with a GPS fix</caption>
<image>https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot3.png</image>
<image>https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-fix.png</image>
</screenshot>
<screenshot>
<caption>Logging</caption>
<image>https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot2.png</image>
<image>https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-log.png</image>
</screenshot>
<screenshot>
<caption>Expanded satellite SNR view</caption>
<image>https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot5.png</image>
<image>https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-snr.png</image>
</screenshot>
<screenshot>
<caption>Speedometer and track recording</caption>
<image>https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot-track.png</image>
<image>https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-track.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.4.2" date="2023-09-23">
<description>
<p>The geoidal release</p>
<ul>
<li>Add 'Geoidal separation' field to dataframe</li>
<li>Display DOPs (PDOP, HDOP, VDOP) on a single dataframe line</li>
<li>Various small fixes to gnss-share source, logging, NMEA parsing etc.</li>
</ul>
</description>
</release>
<release version="0.4.1" date="2023-05-26">
<description>
<p>The automatic release</p>
<ul>
<li>Autodetect sources and source quirks when --source option is not given</li>
<li>Some small fixes to mm_glib_source, NMEA parsing, flatpak, etc.</li>
</ul>
</description>
</release>
<release version="0.4.0" date="2023-03-22">
<description>
<p>The managerial release</p>
<ul>
<li>Use mm-glib to talk to ModemManager, remove pydbus</li>
<li>Support 'quirks' in the ModemManager source, e.g. Quectel talker fixes</li>
<li>Various reliability fixes</li>
</ul>
</description>
</release>
<release version="0.3.1" date="2022-11-17">
<description>
<p>The quickfix release </p>
<ul>
<li>Fix screenshot links, so that flathub builds work</li>
</ul>
</description>
</release>
<release version="0.3.0" date="2022-11-17">
<description>
<p>The multiplatform release</p>
<ul>
<li>Add UnixNmeaSource and GnssShareNmeaSource, enabling support for devices which use gnss-share, like Librem 5 (thanks devrtz)</li>
<li>Allow specifying NmeaSource from the command line (thanks devrtz)</li>
<li>Prefilter NMEA sentences before parsing, enabling support for devices which emit proprietary sentences, like Oneplus 6</li>
<li>Display app menu on edge-overshot only on touchscreen devices</li>
<li>Flatpak: Update to Gnome runtime 43 (thanks ferenc)</li>
</ul>
</description>
</release>
<release version="0.2.8" date="2022-09-07">
<description>
<p>Ageless no more</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

16
debian/changelog vendored
View file

@ -1,3 +1,19 @@
satellite-gtk (0.4.2-1) unstable; urgency=medium
* Team upload
* New upstream release
* d/control: drop now-unneeded pydbus dependency
-- Arnaud Ferraris <aferraris@debian.org> Wed, 04 Oct 2023 12:24:35 +0200
satellite-gtk (0.3.1-1) unstable; urgency=medium
* New upstream release
* d/watch: Remove filename mangling
-- Evangelos Ribeiro Tzaras <devrtz-debian@fortysixandtwo.eu> Sun, 22 Jan 2023 16:06:59 +0100
satellite-gtk (0.2.8-2) unstable; urgency=medium
* Source-only upload to allow migration to testing

1
debian/control vendored
View file

@ -23,7 +23,6 @@ Depends:
python3-gi,
python3-gpxpy,
python3-nmea2,
python3-pydbus,
${misc:Depends},
${python3:Depends},
Description: Adaptive GTK application which displays GNSS data

3
debian/watch vendored
View file

@ -1,3 +1,2 @@
version=4
opts=filenamemangle=s/.*\/archive\/(\d+\.\S+)\.tar\.gz/satellite-gtk-$1\.tar\.gz
https://codeberg.org/tpikonen/satellite/tags .*/archive/(\d+\.\S+)\.tar\.gz
https://codeberg.org/tpikonen/satellite/tags .*/archive/@ANY_VERSION@\.tar\.gz

View file

@ -1,39 +0,0 @@
{
"app-id": "page.codeberg.tpikonen.satellite",
"runtime": "org.gnome.Platform",
"runtime-version": "42",
"sdk": "org.gnome.Sdk",
"command": "satellite",
"rename-desktop-file": "satellite.desktop",
"finish-args": [
"--socket=fallback-x11",
"--socket=wayland",
"--share=ipc",
"--device=dri",
"--talk-name=org.gtk.vfs.*",
"--system-talk-name=org.freedesktop.ModemManager1.*",
"--filesystem=xdg-documents/satellite-tracks:create"
],
"cleanup": [
],
"modules": [
"python3-requirements.json",
{
"name": "satellite",
"sources": [
{
"type": "git",
"path": "../",
"branch": "main"
}
],
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --no-index --no-deps --no-build-isolation --prefix=${FLATPAK_DEST} ./"
],
"post-install": [
"install -Dm644 data/appdata.xml $FLATPAK_DEST/share/metainfo/$FLATPAK_ID.appdata.xml"
]
}
]
}

View file

@ -0,0 +1,42 @@
app-id: page.codeberg.tpikonen.satellite
runtime: org.gnome.Platform
runtime-version: "45"
sdk: org.gnome.Sdk
command: satellite
rename-desktop-file: satellite.desktop
finish-args:
- --socket=fallback-x11
- --socket=wayland
- --share=ipc
- --device=dri
- --talk-name=org.gtk.vfs.*
- --system-talk-name=org.freedesktop.ModemManager1.*
- --filesystem=xdg-documents/satellite-tracks:create
- --filesystem=/run/gnss-share.sock:ro
cleanup: []
modules:
- python3-requirements.json
- name: ModemManager
config-opts:
- --without-udev
- --with-udev-base-dir=/app/lib/udev
- --with-systemdsystemunitdir=/app/lib/systemd/system
- --without-examples
- --without-tests
- --without-mbim
- --without-qmi
- --without-qrtr
- --without-man
sources:
- type: archive
url: https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/archive/1.20.6/ModemManager-1.20.6.tar.gz
sha256: d3e8112810e48ba32e80757fced218cf65b135b5a2987dad6b431d8cfbba765f
- name: satellite
sources:
- type: git
path: ../
branch: main
buildsystem: simple
build-commands:
- pip3 install --verbose --no-index --no-deps --no-build-isolation --prefix=${FLATPAK_DEST} ./
- install -Dm644 data/appdata.xml $FLATPAK_DEST/share/metainfo/$FLATPAK_ID.appdata.xml

View file

@ -26,22 +26,8 @@
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/c9/13/6117f735c3e8083bfce0ccd31a1d561fc2adb0e0e2d1ab3ace12256a3513/pynmea2-1.18.0-py3-none-any.whl",
"sha256": "098f9ffd89c4a6c5e137b8b59e5b38194888d4a557c50b003ebcf2c3c15ec22e"
}
]
},
{
"name": "python3-pydbus",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pydbus\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/92/56/27148014c2f85ce70332f18612f921f682395c7d4e91ec103783be4fce00/pydbus-0.6.0-py2.py3-none-any.whl",
"sha256": "66b80106352a718d80d6c681dc2a82588048e30b75aab933e4020eb0660bf85e"
"url": "https://files.pythonhosted.org/packages/75/24/1f575eb17a8135e54b3c243ff87e2f4d6b2389942836021d0628ed837559/pynmea2-1.19.0-py3-none-any.whl",
"sha256": "5138558b4fb5daa587b2c17de99eb43df0297039de1c98010c996624abfb00eb"
}
]
}

View file

@ -1,3 +1,2 @@
gpxpy
pynmea2
pydbus

View file

@ -1,4 +1,4 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
__version__ = "0.2.8"
__version__ = "0.4.2"

View file

@ -1,4 +1,4 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import sys
@ -8,7 +8,6 @@ from .application import SatelliteApp
def main():
app = SatelliteApp()
app.run()
app.quit_function()
sys.exit(0)

View file

@ -1,9 +1,8 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import argparse
import gi
import gpxpy
import importlib.resources as resources
import os
import re
import signal
@ -12,41 +11,57 @@ import time
import tokenize
from datetime import datetime
import importlib.resources as resources
import gi
import gpxpy
import satellite.nmea as nmea
import satellite.quectel as quectel
from .nmeasource import (
ModemNoNMEAError,
ModemLockedError,
ModemError,
NmeaSourceNotFoundError,
QuectelNmeaSource,
)
from .util import now, unique_filename, bearing_to_arrow
from .widgets import text_barchart, DataFrame
from satellite import __version__
from .mm_glib_source import ModemManagerGLibNmeaSource
from .nmeasource import (
GnssShareNmeaSource,
ModemError,
ModemLockedError,
ModemNoNMEAError,
NmeaSourceNotFoundError,
)
from .util import bearing_to_arrow, have_touchscreen, now, unique_filename
from .widgets import DataFrame, text_barchart
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Handy', '1')
from gi.repository import Gdk, Gio, GLib, Gtk, Handy # noqa: E402
from gi.repository import GLib, Gdk, Gio, Gtk, Handy # noqa: E402, I100
appname = 'Satellite'
app_id = 'page.codeberg.tpikonen.satellite'
class SatelliteApp(Gtk.Application):
def __init__(self, *args, **kwargs):
Gtk.Application.__init__(
self, *args, application_id="page.codeberg.tpikonen.satellite",
self, *args, application_id=app_id,
flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs)
Handy.init()
desc = "Displays navigation satellite data and saves GPX tracks"
parser = argparse.ArgumentParser(description=desc)
parser = argparse.ArgumentParser(
description=desc, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
'-c', '--console-output', dest='console_output',
action='store_true', default=False,
help='Output satellite data to console')
parser.add_argument(
'-s', '--source', dest='source',
choices=['auto', 'quectel', 'mm', 'gnss-share'],
default='auto',
help="Select NMEA source. Options are:\n"
"'auto' (default) Automatic source detection\n"
"'quectel' ModemManager with Quectel quirks\n"
"'mm' ModemManager without quirks\n"
"'gnss-share' Read from gnss-share socket\n")
self.args = parser.parse_args()
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
@ -71,7 +86,6 @@ class SatelliteApp(Gtk.Application):
self, widget_api_name))
else:
setattr(self, widget_api_name, widget)
self.connect("activate", self.do_activate)
self.app_menu = self.builder.get_object('app-menu')
self.menu_popover = Gtk.Popover.new_from_model(
@ -81,7 +95,7 @@ class SatelliteApp(Gtk.Application):
self.source = None
self.infolabel.set_markup("<tt>" + "\n"*10 + "</tt>")
self.infolabel.set_markup("<tt>" + "\n" * 10 + "</tt>")
self.dataframe = DataFrame()
# self.dataframe.header.set_text("Satellite info")
@ -99,13 +113,16 @@ class SatelliteApp(Gtk.Application):
self.set_speedlabel(None)
self.leaflet.set_visible_child(self.databox)
self.datascroll.connect('edge-overshot', self.on_edge_overshot)
self.connect('startup', self.on_startup)
self.connect('activate', self.on_activate)
self.connect('shutdown', self.on_shutdown)
# Internal state
self.last_mode = 1
self.last_data = None
self.last_speed = None
self.last_update = None
self.source_lost = False
self.had_error = False
self.sigint_received = False
self.refresh_rate = 1 # Really delay between updates in seconds
@ -147,51 +164,66 @@ class SatelliteApp(Gtk.Application):
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def do_startup(self):
Gtk.Application.do_startup(self)
def on_startup(self, app):
self.create_actions()
# Initialize modem after GUI startup
GLib.idle_add(self.init_source)
def do_activate(self, *args):
def on_activate(self, app):
self.setup_styles()
self.add_window(self.window)
self.window.show()
return True
if have_touchscreen():
self.datascroll.connect('edge-overshot', self.on_edge_overshot)
def init_source(self):
self.log_msg(f"Satellite version {__version__} started")
try:
self.source = QuectelNmeaSource(
self.location_update_cb,
refresh_rate=self.refresh_rate,
# save_filename=unique_filename(self.gpx_save_dir + '/nmeas',
# '.txt')
)
self.source.initialize()
except Exception as e:
fatal = False
if isinstance(e, ModemLockedError):
self.log_msg("Modem is locked")
dtext = "Please unlock the Modem"
else:
self.log_msg("Error initializing NMEA source")
dtext = e.message if hasattr(e, 'message') else (
"Could not find or initialize NMEA source")
dialog = Gtk.MessageDialog(
parent=self.window, modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK, text=dtext)
dialog.set_title("Error initializing NMEA source")
dialog.run()
dialog.destroy()
if fatal:
self.quit()
self.log_msg(f"{appname} version {__version__} started")
# Initialize modem after GUI startup
GLib.timeout_add(1000, self.init_source, None)
def on_shutdown(self, app):
print("Cleaning up...")
self.gpx_write()
if self.source is not None:
self.source.close()
print("...done.")
def init_source(self, unused):
source_init = False
if self.args.source == 'auto':
self.log_msg("Detecting NMEA sources...")
if not source_init:
source_init = self.init_gnss_share_source(autodetect=True)
if not source_init:
source_init = self.init_mm_source(
quirks=['detect'], autodetect=True)
if not source_init:
self.log_msg('NMEA source not found')
dialog = Gtk.MessageDialog(
parent=self.window, modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text="Could not find an NMEA source")
dialog.set_title("Error initializing NMEA source")
dialog.run()
dialog.destroy()
return GLib.SOURCE_REMOVE
else:
self.log_msg(f'NMEA source "{self.args.source}" selected')
if self.args.source == 'quectel':
source_init = self.init_mm_source(quirks=['QuectelTalker'])
elif self.args.source == 'mm':
source_init = self.init_mm_source()
elif self.args.source == 'gnss-share':
source_init = self.init_gnss_share_source()
if not source_init:
self.log_msg('Could not initialize NMEA source')
return GLib.SOURCE_REMOVE
self.log_msg(
f"Source is {self.source.manufacturer}, model {self.source.model}"
+ f", revision {self.source.revision}"
if self.source.revision else "")
f"Source is {self.source.manufacturer}"
+ (f", model {self.source.model}" if self.source.model else "")
+ (f", revision {self.source.revision}" if self.source.revision else "")
+ (f" using {', '.join(self.source.quirks)} quirks"
if hasattr(self.source, "quirks") and self.source.quirks else ""))
if (self.source.model and self.source.model.startswith("QUECTEL")):
constellations = quectel.get_constellations(self.source)
@ -210,15 +242,62 @@ class SatelliteApp(Gtk.Application):
GLib.timeout_add(self.refresh_rate * 1000, self.timeout_cb, None)
return False # Remove from idle_add
return GLib.SOURCE_REMOVE
def quit_function(self):
"""Called after main loop exits."""
print("Cleaning up...")
self.gpx_write()
if self.source is not None:
self.source.restore()
print("...done.")
def init_gnss_share_source(self, autodetect=False):
try:
self.source = GnssShareNmeaSource(self.location_update_cb)
self.source.initialize()
except Exception as e:
if autodetect:
return False
self.log_msg(str(e))
dtext = str(e)
dialog = Gtk.MessageDialog(
parent=self.window, modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK, text=dtext)
dialog.set_title("Error initializing NMEA source")
dialog.run()
dialog.destroy()
return False
return True
def init_mm_source(self, quirks=[], autodetect=False):
try:
self.source = ModemManagerGLibNmeaSource(
self.location_update_cb,
refresh_rate=self.refresh_rate,
quirks=quirks,
# save_filename=unique_filename(self.gpx_save_dir + '/nmeas',
# '.txt')
)
self.source.initialize()
except Exception as e:
if autodetect:
return False
if isinstance(e, ModemLockedError):
self.log_msg("Modem is locked")
dtext = "Please unlock the Modem"
else:
etext = str(e)
self.log_msg(f"Error initializing ModemManager NMEA source: {etext}")
dtext = etext if etext else (
"Could not initialize ModemManager NMEA source")
dialog = Gtk.MessageDialog(
parent=self.window, modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=dtext)
dialog.set_title("Error initializing NMEA source")
dialog.run()
dialog.destroy()
return False
return True
def sigint_handler(self):
if not self.sigint_received:
@ -240,12 +319,12 @@ class SatelliteApp(Gtk.Application):
adlg = Gtk.AboutDialog(
transient_for=self.window,
modal=True,
program_name="Satellite",
logo_icon_name="page.codeberg.tpikonen.satellite",
program_name=appname,
logo_icon_name=app_id,
version=__version__,
comments="A program for showing navigation satellite data",
license_type=Gtk.License.GPL_3_0_ONLY,
copyright="Copyright 2021-2022 Teemu Ikonen",
copyright="Copyright 2021-2023 Teemu Ikonen",
)
adlg.present()
@ -311,31 +390,31 @@ class SatelliteApp(Gtk.Application):
def leaflet_forward_cb(self, button):
self.leaflet.navigate(Handy.NavigationDirection.FORWARD)
return True
def leaflet_back_cb(self, button):
self.leaflet.navigate(Handy.NavigationDirection.BACK)
return True
def infolabel_released_cb(self, gesture, n_press, x, y):
if n_press != 1 or self.carousel.get_position() > 0.5:
return False
return
self.chart_size = self.chart_small if (
self.chart_size == self.chart_large) else self.chart_large
self.update(None)
return True
self.set_barchart(self.last_data)
def carousel_page_changed_cb(self, carousel, index):
if index == 1 and self.chart_size == self.chart_large:
self.chart_size = self.chart_small
self.update(None)
self.set_barchart(self.last_data)
return False
def format_satellite_data(self, d):
bchart = text_barchart(
((e['prn'], e['snr']) for e in d['visibles']),
d['actives'], height=self.chart_size)
return bchart
def set_barchart(self, data):
if data is None:
return ''
barchart = text_barchart(
((e['prn'], e['snr']) for e in data['visibles']),
data['actives'], height=self.chart_size)
self.infolabel.set_markup("<tt>" + barchart + "</tt>")
return barchart
def set_values(self, data):
def to_str(x, fmt="%s"):
@ -351,6 +430,12 @@ class SatelliteApp(Gtk.Application):
fixage = to_str(data.get("fixage"), "%0.0f s")
return "%s / %s" % (up_age, fixage)
def get_dops(xkey):
pdop = to_str(data.get("pdop"), "%1.1f")
hdop = to_str(data.get("hdop"), "%1.1f")
vdop = to_str(data.get("vdop"), "%1.1f")
return f"{pdop} / {hdop} / {vdop}"
mode2fix = {
"2": "2 D",
"3": "3 D",
@ -359,19 +444,19 @@ class SatelliteApp(Gtk.Application):
# Mapping: Data key, description, converter func
order = [
("mode", "Fix type", lambda x: mode2fix.get(x, "No Fix")),
("mode_indicator", "Modes (GP,GL,GA)", lambda x: str(x)),
("mode_indicator", "Modes (GP,GL,GA)",
lambda x: str(x) if x is not None else "n/a"),
("actives", "Active / in use sats", get_actives),
("visibles", "Receiving sats", lambda x: str(len(
list(r for r in x if r['snr'] > 0.0)))),
[r for r in x if r['snr'] > 0.0]))),
("visibles", "Visible sats", lambda x: str(len(x))),
# ("fixage", "Age of fix", lambda x: to_str(x, "%0.0f s")),
("fixage", "Age of update / fix", get_ages),
("systime", "Sys. Time", lambda x: x.strftime(utcfmt)),
("latlon", "Latitude",
lambda x: "%0.6f" % x[0] if x else "-"),
("latlon", "Longitude",
lambda x: "%0.6f" % x[1] if x else "-"),
("altitude", "Altitude", lambda x: to_str(x, "%0.1f m")),
("latlon", "Latitude", lambda x: "%0.6f" % x[0] if x else "-"),
("latlon", "Longitude", lambda x: "%0.6f" % x[1] if x else "-"),
("altitude", "Altitude", lambda x: to_str(x, "%0.1f m")),
("geoid_sep", "Geoidal separation", lambda x: to_str(x, "%0.1f m")),
# ("fixtime", "Time of fix",
# lambda x: x.strftime(utcfmt) if x else "-"),
# ("date", "Date of fix",
@ -380,9 +465,7 @@ class SatelliteApp(Gtk.Application):
("true_course", "True Course",
lambda x: to_str(x, "%0.1f deg ")
+ (bearing_to_arrow(x) if x is not None else "")),
("pdop", "PDOP", lambda x: to_str(x)),
("hdop", "HDOP", lambda x: to_str(x)),
("vdop", "VDOP", lambda x: to_str(x)),
("pdop", "PDOP/HDOP/VDOP", get_dops),
]
descs = []
vals = []
@ -415,10 +498,9 @@ class SatelliteApp(Gtk.Application):
self.last_mode = mode
def set_speedlabel(self, speed, bearing=None):
spd = str(int(3.6*speed)) if speed else "-"
spd = str(int(3.6 * speed)) if speed else "-"
arrow = bearing_to_arrow(bearing) if bearing is not None else ""
speedfmt = ('<span size="50000">%s%s</span>\n' +
'<span size="30000">%s</span>')
speedfmt = '<span size="50000">%s%s</span>\n<span size="30000">%s</span>'
speedstr = speedfmt % (spd, arrow, "km/h")
self.speedlabel.set_markup(speedstr)
@ -471,19 +553,28 @@ class SatelliteApp(Gtk.Application):
def timeout_cb(self, x):
dt = (time.time() - self.last_update) if self.last_update else 100
if dt > 2 * self.refresh_rate:
self.update(None)
return True
self.main_box.set_sensitive(False)
self.update()
return GLib.SOURCE_CONTINUE
def location_update_cb(self, *args):
self.last_update = time.time()
self.update(None)
self.main_box.set_sensitive(True)
self.update()
def update(self, x):
def update(self):
try:
nmeas = self.source.get()
if self.had_error:
self.log_msg("Getting updates")
self.main_box.set_sensitive(True)
self.had_error = False
data = nmea.parse(nmeas)
except Exception as e:
fatal = False
nmeas = None
show_dialog = False
etext = str(e)
dtext = None
if isinstance(e, ModemLockedError):
dtext = "Please unlock the Modem"
@ -491,44 +582,40 @@ class SatelliteApp(Gtk.Application):
elif isinstance(e, ModemNoNMEAError):
dtext = "NMEA info not received with location"
elif isinstance(e, ModemError):
dtext = "Unspecified modem error"
dtext = "Modem error: " + str(e)
elif isinstance(e, NmeaSourceNotFoundError):
if not self.source_lost:
dtext = e.message if (
hasattr(e, 'message')) else "Modem disappeared"
self.source_lost = True
if not self.had_error:
dtext = etext if etext else "Modem disappeared"
self.had_error = True
self.main_box.set_sensitive(False)
else:
dtext = e.message if hasattr(e, 'message') else "Unknown error"
if show_dialog:
dialog = Gtk.MessageDialog(
parent=self.window, modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK, text=dtext)
dialog.set_title("Unrecoverable error" if fatal else "Error")
dialog.run()
dialog.destroy()
if fatal:
self.quit()
elif dtext is not None:
self.log_msg(dtext)
return True
dtext = etext if etext else "Unknown error"
if not self.had_error:
if show_dialog:
dialog = Gtk.MessageDialog(
parent=self.window, modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK, text=dtext)
dialog.set_title("Error")
dialog.run()
dialog.destroy()
elif dtext is not None:
self.log_msg(dtext)
self.had_error = True
if self.last_data is None:
return
else:
data = self.last_data
if self.source_lost:
self.log_msg("Modem appeared")
self.main_box.set_sensitive(True)
self.source_lost = False
data = nmea.parse(nmeas)
data["updateage"] = ((time.time() - self.last_update)
if self.last_update else None)
barchart = self.format_satellite_data(data)
barchart = self.set_barchart(data)
if self.args.console_output:
print(barchart)
self.infolabel.set_markup("<tt>" + barchart + "</tt>")
self.set_values(data)
mode = data["mode"]
mode = int(mode) if mode else 0
speed = data['speed']
bearing = data['true_course']
self.set_speedlabel(speed, bearing)
@ -537,13 +624,18 @@ class SatelliteApp(Gtk.Application):
elif not speed and self.last_speed:
self.carousel.scroll_to(self.infolabel)
self.last_speed = speed
# log
mode = data["mode"]
mode = int(mode) if mode else self.last_mode
if mode != self.last_mode:
if mode > 1:
self.log_msg(f"Got lock, mode: {mode}")
elif mode <= 1:
self.log_msg("Lock lost")
self.last_mode = mode
if self.gpx is not None and data.get("valid"):
self.gpx_update(data)
return True
self.last_data = data

158
satellite/mm_glib_source.py Normal file
View file

@ -0,0 +1,158 @@
# Copyright 2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import re
import gi
from pynmea2.nmea import NMEASentence
from satellite.nmeasource import ( # noqa: E402
ModemError,
ModemLockedError,
ModemNoNMEAError,
NmeaSource,
NmeaSourceNotFoundError,
)
gi.require_version('ModemManager', '1.0')
from gi.repository import Gio, ModemManager # noqa: E402, I100
class ModemManagerGLibNmeaSource(NmeaSource):
def __init__(self, update_callback, quirks=[], **kwargs):
super().__init__(update_callback, **kwargs)
self.bus = None
self.manager = None
self.modem = None
self.mlocation = None
self.old_refresh_rate = None
self.old_sources_enabled = None
self.old_signals_location = None
self.location_updated = None
self.quirks = set(quirks)
def initialize(self):
# If reinitializing, disconnect old update cb
if self.mlocation is not None:
self.mlocation.disconnect_by_func(self.update_callback)
self.bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
self.manager = ModemManager.Manager.new_sync(
self.bus, Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, None)
if self.manager.get_name_owner() is None:
raise NmeaSourceNotFoundError("ModemManager is not running")
objs = self.manager.get_objects()
if objs:
self.modem = objs[0].get_modem()
self.mlocation = objs[0].get_modem_location()
else:
raise NmeaSourceNotFoundError("No Modems Found")
self.manufacturer = self.modem.get_manufacturer()
self.model = self.modem.get_model()
self.revision = self.modem.get_revision()
if 'detect' in self.quirks:
self.quirks.remove('detect')
if (self.model.startswith('QUECTEL')
and self.manufacturer == 'QUALCOMM INCORPORATED'):
self.quirks.add('QuectelTalker')
# Detect SDM845 GNSS unit and disable MSB assistance,
# which causes stalling at startup due to some bug somewhere
if (self.manufacturer == 'QUALCOMM INCORPORATED'
and self.model == '0'
and self.revision.find('SDM845') >= 0):
self.quirks.add('NoMSB')
try:
state = self.modem.get_state()
if int(state) > 0:
if self.old_refresh_rate is None:
self.old_refresh_rate = self.mlocation.props.gps_refresh_rate
if self.old_sources_enabled is None:
self.old_sources_enabled = self.mlocation.props.enabled
if self.old_signals_location is None:
self.old_signals_location = self.mlocation.props.signals_location
caps = self.mlocation.get_capabilities()
if not caps & ModemManager.ModemLocationSource.GPS_NMEA:
raise NmeaSourceNotFoundError(
"Modem does not support NMEA")
enable = ModemManager.ModemLocationSource.GPS_NMEA
if (caps & ModemManager.ModemLocationSource.AGPS_MSB
and 'NoMSB' not in self.quirks):
enable |= ModemManager.ModemLocationSource.AGPS_MSB
self.mlocation.setup_sync(enable, True, None)
else:
raise ModemError("Modem state is: %d" % state)
except AttributeError as e:
if state == ModemManager.ModemState.LOCKED:
raise ModemLockedError from e
else:
raise e
except gi.repository.GLib.GError as e:
# Ignore error on AGPS enablement by this hack
if 'agps-msb' not in str(e):
raise e
self.mlocation.set_gps_refresh_rate_sync(self.refresh_rate, None)
self.mlocation.connect('notify::location', self.update_callback)
self.initialized = True
def _really_get(self):
if not self.initialized:
self.initialize()
try:
loc = self.mlocation.get_signaled_gps_nmea()
except Exception as e:
self.initialized = False
raise e
if loc is None:
raise ModemNoNMEAError
nmeas = loc.get_traces()
if nmeas is None:
self.initialized = False
raise ModemNoNMEAError
if 'QuectelTalker' in self.quirks:
nmeas = self.quectel_talker_quirk(nmeas)
return '\r\n'.join(nmeas)
def close(self):
if self.mlocation is None:
return
try:
self.mlocation.disconnect_by_func(self.update_callback)
except TypeError:
pass # Ignore error when nothing is connected
if self.old_sources_enabled is not None:
self.mlocation.setup_sync(
ModemManager.ModemLocationSource(self.old_sources_enabled),
self.old_signals_location, None)
if self.old_refresh_rate is not None:
self.mlocation.set_gps_refresh_rate_sync(self.old_refresh_rate, None)
def quectel_talker_quirk(self, nmeas):
pq_re = re.compile(r"""
^\s*\$?
(?P<talker>PQ)
(?P<sentence>\w{3})
(?P<data>[^*]*)
(?:[*](?P<checksum>[A-F0-9]{2}))$""", re.VERBOSE)
out = []
for nmea in (n for n in nmeas if n):
mo = pq_re.match(nmea)
if mo:
# The last extra data field is Signal ID, these are
# 1 = GPS, 2 = Glonass, 3 = Galileo, 4 = BeiDou, 5 = QZSS
# Determine talker from Signal ID
talker = 'QZ' if mo.group('data').endswith('5') else 'BD'
# Fake talker and checksum
fake = talker + "".join(mo.group(2, 3))
out.append('$' + fake + "*%02X" % NMEASentence.checksum(fake))
else:
out.append(nmea)
return out

View file

@ -1,31 +0,0 @@
# Copyright 2021-2022 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
# flake8: noqa
# See /usr/include/ModemManager/ModemManager-enums.h in modemmanager-dev
MM_MODEM_LOCATION_SOURCE_NONE = 0
MM_MODEM_LOCATION_SOURCE_3GPP_LAC_CI = 1 << 0
MM_MODEM_LOCATION_SOURCE_GPS_RAW = 1 << 1
MM_MODEM_LOCATION_SOURCE_GPS_NMEA = 1 << 2
MM_MODEM_LOCATION_SOURCE_CDMA_BS = 1 << 3
MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED = 1 << 4
MM_MODEM_LOCATION_SOURCE_AGPS_MSA = 1 << 5
MM_MODEM_LOCATION_SOURCE_AGPS_MSB = 1 << 6
MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_NONE = 0
MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_XTRA = 1 << 0
MM_MODEM_STATE_FAILED = -1
MM_MODEM_STATE_UNKNOWN = 0
MM_MODEM_STATE_INITIALIZING = 1
MM_MODEM_STATE_LOCKED = 2
MM_MODEM_STATE_DISABLED = 3
MM_MODEM_STATE_DISABLING = 4
MM_MODEM_STATE_ENABLING = 5
MM_MODEM_STATE_ENABLED = 6
MM_MODEM_STATE_SEARCHING = 7
MM_MODEM_STATE_REGISTERED = 8
MM_MODEM_STATE_DISCONNECTING = 9
MM_MODEM_STATE_CONNECTING = 10
MM_MODEM_STATE_CONNECTED = 11

View file

@ -1,7 +1,9 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import datetime
import re
import pynmea2
MS_PER_KNOT = 0.514444
@ -27,7 +29,7 @@ def fget(key, scale=1.0):
def iget(key, default=None):
def fn(d):
try:
return int(d.get(key))
return int(d.get(key, default))
except ValueError:
return default
return fn
@ -60,9 +62,9 @@ def get_latlon(mdict):
lat_min = float(lat[2:])
lon_deg = float(lon[:3])
lon_min = float(lon[3:])
flat = lat_deg + lat_min/60
flat = lat_deg + lat_min / 60
flat = -1 * flat if lat_dir == 'S' else flat
flon = lon_deg + lon_min/60
flon = lon_deg + lon_min / 60
flon = -1 * flon if lon_dir == 'W' else flon
return (flat, flon)
@ -97,18 +99,17 @@ getters = {
'GGA': get_altitude_gga,
'GNS': fget('altitude'),
},
"fixtime": {
"fixtime": { # Time of position report
'RMC': get_time,
'GGA': get_time,
},
"time": { # Reported also when no fix
'GNS': get_time,
'GGA': get_time,
},
"date": {
'RMC': get_date,
},
"valid": {
'RMC': lambda x: x.get('status') == 'A',
'GSA': lambda x: iget('mode_fix_type', 1)(x) > 1,
},
"speed": {
'RMC': fget('spd_over_grnd', MS_PER_KNOT),
@ -129,6 +130,7 @@ getters = {
},
"num_sats": {
'GNS': iget('num_sats', default=0),
'GGA': iget('num_sats', default=0),
},
"pdop": {
'GSA': fget('pdop'),
@ -141,7 +143,8 @@ getters = {
"vdop": {
'GSA': fget('vdop'),
},
"geo_sep": {
"geoid_sep": {
'GGA': fget('geo_sep'),
'GNS': fget('geo_sep'),
},
"sel_mode": {
@ -180,7 +183,7 @@ def parse(nmeas, always_add_prefix=False):
return float(s) if s else empty_val
def add_prn_prefix(prns, talker, always=always_add_prefix):
"""Add constellation prefix to PRN string"""
"""Add constellation prefix to PRN string."""
beidou_prefix = "C"
galileo_prefix = "E"
glonass_prefix = "R"
@ -206,12 +209,13 @@ def parse(nmeas, always_add_prefix=False):
return "%s%02d" % (prefix, prn)
parsed = [pynmea2.parse(n) for n in nmeas.split('\n') if n]
# Prevent pynmea2 failing with unknown NMEA sentence types
supported_nmeas = ('GSV', 'GSA', 'GGA', 'RMC', 'VTG', 'GNS')
supported_re = re.compile(
r'^\$?..(?:' + '|'.join(f'(?:{s})' for s in supported_nmeas) + ')')
parsed = [pynmea2.parse(n) for n in nmeas.split('\n')
if re.match(supported_re, n)]
for msg in parsed:
# print(repr(msg))
keys = []
for field in msg.fields:
keys.append(field[1])
if isinstance(msg, pynmea2.types.GSV):
for n in range(1, (len(msg.data) - 4) // 4 + 1):
prns = getattr(msg, f'sv_prn_num_{n}', None)
@ -224,7 +228,7 @@ def parse(nmeas, always_add_prefix=False):
'snr': fl(getattr(msg, f'snr_{n}', None), 0.0),
})
elif isinstance(msg, pynmea2.types.GSA):
for n in range(1, 12+1):
for n in range(1, 12 + 1):
prns = getattr(msg, f'sv_id{n:02d}')
if prns and prns.isdigit():
actives.append(add_prn_prefix(prns, msg.talker))
@ -245,13 +249,13 @@ def parse(nmeas, always_add_prefix=False):
}
out.update({k: msg_get(msgs, k) for k in getters.keys()})
datenow = datetime.datetime.utcnow()
datenow = datetime.datetime.now(datetime.timezone.utc)
fixtime = out.get('fixtime')
fixdate = out.get('date')
if fixdate is None and fixtime is not None:
# We have a fix but no RMC sentence
fixdate = datenow.date()
fixdt = (datetime.datetime.combine(fixdate, fixtime)
fixdt = (datetime.datetime.combine(fixdate, fixtime, datetime.timezone.utc)
if (fixtime and fixdate) else None)
out["datetime"] = fixdt
out["systime"] = datenow

View file

@ -1,10 +1,8 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import re
import satellite.modem_manager_defs as mm
from pydbus import SystemBus
from pynmea2.nmea import NMEASentence
import os.path
import socket
from gi.repository import GLib
@ -57,121 +55,62 @@ class NmeaSource:
self._maybe_save(nmeas)
return nmeas
def restore(self):
def close(self):
pass
class ModemManagerNmeaSource(NmeaSource):
def __init__(self, update_callback, **kwargs):
class UnixSocketNmeaSource(NmeaSource):
def __init__(self,
update_callback,
socket_file_path=None,
**kwargs):
super().__init__(update_callback, **kwargs)
self.bus = SystemBus()
self.manager = self.bus.get('.ModemManager1')
self.modem = None
self.old_refresh_rate = None
self.old_sources_enabled = None
self.old_signals = None
self.socket_file_path = socket_file_path
def on_read_data_available(self, io_channel, condition, **unused):
self.update_callback()
return True
def initialize(self):
objs = self.manager.GetManagedObjects()
mkeys = list(objs.keys())
if mkeys:
mstr = mkeys[0]
else:
raise NmeaSourceNotFoundError("No Modems Found")
print(f"Modem is: {mstr}")
info = objs[mstr]['org.freedesktop.ModemManager1.Modem']
self.manufacturer = info.get('Manufacturer')
self.model = info.get('Model')
self.revision = info.get('Revision')
self.modem = self.bus.get('.ModemManager1', mstr)
if (self.socket_file_path is None
or not os.path.exists(self.socket_file_path)):
raise FileNotFoundError(f"Could not open socket {self.socket_file_path}")
self.s = socket.socket(socket.AF_UNIX,
socket.SOCK_NONBLOCK | socket.SOCK_STREAM)
try:
print("Modem state is: %d" % self.modem.State)
if self.modem.State > 0:
if self.old_refresh_rate is None:
self.old_refresh_rate = self.modem.GpsRefreshRate
if self.old_sources_enabled is None:
self.old_sources_enabled = self.modem.Enabled
if self.old_signals is None:
self.old_signals = self.modem.SignalsLocation
cap = self.modem.Capabilities
if (cap & mm.MM_MODEM_LOCATION_SOURCE_GPS_NMEA) == 0:
raise NmeaSourceNotFoundError(
"Modem does not support NMEA")
self.modem.Setup(
(mm.MM_MODEM_LOCATION_SOURCE_GPS_NMEA
| (cap & mm.MM_MODEM_LOCATION_SOURCE_AGPS_MSB)),
True)
else:
print("Modem is in a bad state, retrying later...")
except AttributeError as e:
print("Error during initialization")
if self.modem.State == mm.MM_MODEM_STATE_LOCKED:
raise ModemLockedError from e
else:
raise ModemError from e
self.s.connect(self.socket_file_path)
except Exception as e:
raise e
self.modem.SetGpsRefreshRate(self.refresh_rate)
self.bus.subscribe(
sender='org.freedesktop.ModemManager1',
iface='org.freedesktop.DBus.Properties',
signal='PropertiesChanged',
arg0='org.freedesktop.ModemManager1.Modem.Location',
signal_fired=self.update_callback)
self.channel = GLib.IOChannel.unix_new(self.s.fileno())
self.channel.add_watch(GLib.IO_IN, self.on_read_data_available)
self.old_nmea_buf = ''
self.initialized = True
def _really_get(self):
if not self.initialized:
self.initialize()
try:
loc = self.modem.GetLocation()
except Exception as e:
self.initialized = False
raise e
retval = loc.get(mm.MM_MODEM_LOCATION_SOURCE_GPS_NMEA)
if retval is None:
print("Location does not have NMEA information")
self.initialized = False
raise ModemNoNMEAError
return retval
buf = ''
do_read = True
def restore(self):
if self.old_sources_enabled is not None:
self.modem.Setup(self.old_sources_enabled, self.old_signals)
if self.old_refresh_rate is not None:
self.modem.SetGpsRefreshRate(self.old_refresh_rate)
while do_read:
read_buf = self.channel.readline()
buf += read_buf
if read_buf == '':
do_read = False
return buf
class QuectelNmeaSource(ModemManagerNmeaSource):
def _really_get(self):
return self.fix_talker(super()._really_get())
def fix_talker(self, nmeas):
pq_re = re.compile(r'''
^\s*\$?
(?P<talker>PQ)
(?P<sentence>\w{3})
(?P<data>[^*]*)
(?:[*](?P<checksum>[A-F0-9]{2}))$''', re.VERBOSE)
out = []
for nmea in (n for n in nmeas.split('\r\n') if n):
mo = pq_re.match(nmea)
if mo:
# The last extra data field is Signal ID, these are
# 1 = GPS, 2 = Glonass, 3 = Galileo, 4 = BeiDou, 5 = QZSS
# Determine talker from Signal ID
talker = 'QZ' if mo.group('data').endswith('5') else 'BD'
# Fake talker and checksum
fake = talker + "".join(mo.group(2, 3))
out.append('$' + fake + "*%02X" % NMEASentence.checksum(fake))
else:
out.append(nmea)
return "\r\n".join(out)
class GnssShareNmeaSource(UnixSocketNmeaSource):
def __init__(self, update_callback, **kwargs):
super().__init__(update_callback,
socket_file_path='/var/run/gnss-share.sock',
**kwargs)
self.manufacturer = "gnss-share"
class ReplayNmeaSource(NmeaSource):
@ -201,6 +140,8 @@ class ReplayNmeaSource(NmeaSource):
return True
def _really_get(self):
if not self.nmeas:
return None
nmea = self.nmeas[self.counter]
self.counter += 1
if self.counter >= len(self.nmeas):

View file

@ -1,8 +1,7 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import re
from datetime import datetime, timezone
from .util import (

View file

@ -1,19 +1,27 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import os
from datetime import datetime, timezone
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk # noqa: E402
gps_epoch = datetime(1980, 1, 6, 0, 0, tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
one_week = 7 * 24 * 60 * 60
week_now = int((now.timestamp() - gps_epoch.timestamp()) / one_week)
def have_touchscreen():
"""Return True if the default seat of default display has touch capability."""
return bool(Gdk.Display.get_default_seat(
Gdk.Display.get_default()).get_capabilities() & Gdk.SeatCapabilities.TOUCH)
def datetime_from_gpstime(week, millisecs, fix_week=False):
"""Return a datetime object formed from GPS week number and
milliseconds from week start.
"""Return a datetime from GPS week number and milliseconds from week start.
If fix_week is True, set the bits above 10 in week number from
current date, see
@ -27,7 +35,7 @@ def datetime_from_gpstime(week, millisecs, fix_week=False):
def gpstime_from_datetime(dt):
"""Return a (gps_week, millisec) tuple from a datetime object"""
"""Return a (gps_week, millisec) tuple from a datetime object."""
if dt < gps_epoch:
raise ValueError("Time cannot be less than GPS epoch")
ts = dt.timestamp()
@ -40,7 +48,7 @@ def gpstime_from_datetime(dt):
def unique_filename(namestem, ext, timestamp=False):
if timestamp:
namestem += "-" + datetime.now().isoformat(
'_', 'seconds').replace(':', '.')
'_', 'seconds').replace(':', '.')
name = None
for count in ('~%d' % n if n > 0 else '' for n in range(100)):
test = namestem + count + ext
@ -66,7 +74,7 @@ def bearing_to_arrow(bearing):
'\u2196',
'\u2191',
]
edges = list(22.5 + 45.0 * n for n in range(0, 8)) + [360.0]
edges = [22.5 + 45.0 * n for n in range(0, 8)] + [360.0]
angle = bearing - (bearing // 360) * 360
index = next(ind for (ind, e) in enumerate(edges) if angle < e)

View file

@ -1,10 +1,9 @@
# Copyright 2021-2022 Teemu Ikonen
# Copyright 2021-2023 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only
import gi
import importlib.resources as resources
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk # noqa: E402
@ -18,8 +17,7 @@ def text_barchart(data, highlights, height=None, width=30):
height Number of lines in the generated bar chart
width Width of the generated bar chart in chars
"""
sdata = list((d[0] if d[0] else '',
int(d[1]) if d[1] else 0) for d in data)
sdata = [(d[0] if d[0] else '', int(d[1]) if d[1] else 0) for d in data]
sdata.sort(key=lambda x: x[1], reverse=True)
dstr = ''
@ -41,43 +39,17 @@ def text_barchart(data, highlights, height=None, width=30):
cmax_xaxis = cmaxbar + 3
for d in sdata[:barlines]:
block = '\u2585' if d[0] in highlights else '='
dstr += "%3s\u2502%s %d\n" % (d[0], block*int(scale*d[1]), d[1])
dstr += "%3s\u2502%s %d\n" % (d[0], block * int(scale * d[1]), d[1])
if barlines < len(sdata):
dstr += " \u256a\n"
elif (len(sdata) - axislines) < height:
# Add empty lines to y-axis
dstr += ' \u2502\n' * (height - len(sdata) - axislines)
dstr += " \u251c" + '\u2500'*(cmax_xaxis) + '\u2524\n'
dstr += " 0" + ' '*(cmax_xaxis - 1) + str(max_xaxis)
dstr += " \u251c" + '\u2500' * (cmax_xaxis) + '\u2524\n'
dstr += " 0" + ' ' * (cmax_xaxis - 1) + str(max_xaxis)
return dstr
class LabelBarChart(Gtk.Label):
def __init__(self):
super().__init__()
# self.connect("draw", self.draw_cb)
# self.connect("size-allocate", self.size_allocate_cb)
def update_data(self, data, highlights):
xpx_per_char = 12
width = self.get_allocated_width()
height = self.get_allocated_height()
cwidth = int(width / xpx_per_char) - 2
cwidth = cwidth if cwidth < 40 else 40
print(width)
print(height)
print(cwidth)
self.set_markup(
"<tt>\n%s</tt>" % text_barchart(data, highlights, cwidth))
def draw_cb(self, cr, *args):
print("Draw!")
def size_allocate_cb(self, *args):
pass
@Gtk.Template(string=resources.read_text('satellite', 'dataframe.ui'))
class DataFrame(Gtk.Bin):
__gtype_name__ = 'DataFrame'

View file

@ -1,2 +1,8 @@
[flake8]
exclude=.git,__pycache__,build
max-line-length=88
ignore = B902, BLK100, CCR001, CNL100, D1, I201, Q000, W503
[pycodestyle]
count=1
max-line-length = 88

View file

@ -29,7 +29,7 @@ setuptools.setup(
"Bug Tracker": "https://codeberg.org/tpikonen/satellite/issues",
},
classifiers=[
"Development Status :: 3 - Alpha",
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: GTK",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",