Compare commits
97 commits
debian/0.2
...
debian/lat
Author | SHA1 | Date | |
---|---|---|---|
|
c13d7bcb58 | ||
|
686fb560ae | ||
|
3eecde511b | ||
|
c82cf9e70f | ||
|
6ea71b18f2 | ||
|
81569df8e2 | ||
|
ccaa2db75d | ||
|
90384a0e27 | ||
|
a5453cf85f | ||
|
5d6059416e | ||
|
85c6bfefd7 | ||
|
c615a432f4 | ||
|
f1d04c6868 | ||
|
2a5785292f | ||
|
41f4b196cc | ||
|
2c5d3a7e93 | ||
|
8f863ae042 | ||
|
84ea03f704 | ||
|
69551fa7a5 | ||
|
a895bfc32f | ||
|
da78c02a68 | ||
|
0f49eb2670 | ||
|
0af37d1dc3 | ||
|
38bc1f7eb5 | ||
|
aec07d5fd4 | ||
|
6d1a10f96a | ||
|
98da3e4ffa | ||
|
f77ce58ee8 | ||
|
88e53d34bb | ||
|
89f6ac85db | ||
|
c3115dd751 | ||
|
782499e27b | ||
|
fbfd588f78 | ||
|
51eb4bb714 | ||
|
83164bb80f | ||
|
4d73be8c73 | ||
|
e6205bd9e5 | ||
|
d6e4a71380 | ||
|
a4a599a6c5 | ||
|
2bb1b70994 | ||
|
8c0f1587f4 | ||
|
7fd0d42f04 | ||
|
ffe6a6221a | ||
|
9de70b5446 | ||
|
7206198321 | ||
|
0b2cc89635 | ||
|
17baef902d | ||
|
7115b1697c | ||
|
58f4433cda | ||
|
3ebf88d0f7 | ||
|
7f5075e0ab | ||
|
473ffec331 | ||
|
5ffa466048 | ||
|
760f80fb49 | ||
|
f6e8cdd30f | ||
|
0861c78c76 | ||
|
93cb27fb27 | ||
|
bef48e4056 | ||
|
ffd3f29ae3 | ||
|
f15dd098e3 | ||
|
2ed6f27786 | ||
|
f71906723c | ||
|
a1431c8785 | ||
|
ef089aa991 | ||
|
21857aa3f4 | ||
|
9d6d5d5322 | ||
|
6abb6297c6 | ||
|
326164cc97 | ||
|
4d5a9cc46e | ||
|
0644484db0 | ||
|
1092aa3a31 | ||
|
966633133a | ||
|
53a71b7fb8 | ||
|
0374b9e7f7 | ||
|
4717c146e4 | ||
|
cb6a018cf4 | ||
|
447ed22f56 | ||
|
ad381023ca | ||
|
247b7a5683 | ||
|
1f1435ee2a | ||
|
19db5edc9a | ||
|
940208e7a3 | ||
|
e3290957c7 | ||
|
9b8f991e49 | ||
|
28978d0906 | ||
|
63f1dd652d | ||
|
c32f7be5db | ||
|
78554c1afd | ||
|
b76c8850c0 | ||
|
7701e998e1 | ||
|
58f3849108 | ||
|
f7c85c2dd1 | ||
|
c108403b89 | ||
|
10b992dcb7 | ||
|
b121db5477 | ||
|
914015cd33 | ||
|
9f93eb7b1a |
29 changed files with 630 additions and 403 deletions
26
.editorconfig
Normal file
26
.editorconfig
Normal 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
|
21
README.md
21
README.md
|
@ -1,12 +1,13 @@
|
||||||
# Satellite
|
# Satellite
|
||||||
|
|
||||||
![The main view with a GPS fix](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot3.png)
|
![The main view with a GPS fix](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-fix.png)
|
||||||
![Logging](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot2.png)
|
![Logging](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-log.png)
|
||||||
![Expanded satellite SNR view](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot5.png)
|
![Expanded satellite SNR view](https://github.com/flathub/page.codeberg.tpikonen.satellite/raw/master/screenshot-snr.png)
|
||||||
![Speedometer and track recording](https://codeberg.org/tpikonen/satellite/raw/branch/main/data/screenshots/screenshot-track.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
|
Satellite is an adaptive GTK3 / 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.
|
(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
|
## License
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ GPL-3.0
|
||||||
|
|
||||||
## Dependencies:
|
## Dependencies:
|
||||||
|
|
||||||
python 3.6+, gi, Gtk, libhandy, pydbus, pynmea2, gpxpy
|
python 3.6+, gi, Gtk3, libhandy, libmm-glib, pynmea2, gpxpy
|
||||||
|
|
||||||
## Installing and running
|
## Installing and running
|
||||||
|
|
||||||
|
@ -44,9 +45,9 @@ Run the script `bin/satellite`.
|
||||||
|
|
||||||
Run
|
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`.
|
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
|
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.
|
in the source tree root to install a local build to the user flatpak repo.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Copyright 2021-2022 Teemu Ikonen -->
|
<!-- Copyright 2021-2023 Teemu Ikonen -->
|
||||||
<!-- SPDX-License-Identifier: GPL-3.0-only -->
|
<!-- SPDX-License-Identifier: GPL-3.0-only -->
|
||||||
<component type="desktop-application">
|
<component type="desktop-application">
|
||||||
<id>page.codeberg.tpikonen.satellite</id>
|
<id>page.codeberg.tpikonen.satellite</id>
|
||||||
|
@ -9,10 +9,11 @@
|
||||||
<summary>Check your GPS reception and save your tracks</summary>
|
<summary>Check your GPS reception and save your tracks</summary>
|
||||||
<description>
|
<description>
|
||||||
<p>Satellite displays global navigation satellite system (GNSS: that's GPS,
|
<p>Satellite displays global navigation satellite system (GNSS: that's GPS,
|
||||||
Galileo, Glonass etc.) data obtained from the ModemManager API. You can use
|
Galileo, Glonass etc.) data obtained from an NMEA source in your device.
|
||||||
it to check the navigation satellite signal strength in your location and
|
Currently the ModemManager and gnss-share APIs are supported. You can use
|
||||||
see your speed, coordinates and other parameters once a fix is obtained.
|
it to check the navigation satellite signal strength and see your speed,
|
||||||
It can also save GPX-tracks of your travels.</p>
|
coordinates and other parameters once a fix is obtained. It can also save
|
||||||
|
GPX-tracks of your travels.</p>
|
||||||
</description>
|
</description>
|
||||||
<launchable type="desktop-id">page.codeberg.tpikonen.satellite.desktop</launchable>
|
<launchable type="desktop-id">page.codeberg.tpikonen.satellite.desktop</launchable>
|
||||||
<url type="homepage">https://codeberg.org/tpikonen/satellite</url>
|
<url type="homepage">https://codeberg.org/tpikonen/satellite</url>
|
||||||
|
@ -32,22 +33,71 @@
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<caption>The main view with a GPS fix</caption>
|
<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>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Logging</caption>
|
<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>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Expanded satellite SNR view</caption>
|
<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>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Speedometer and track recording</caption>
|
<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>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<releases>
|
<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">
|
<release version="0.2.8" date="2022-09-07">
|
||||||
<description>
|
<description>
|
||||||
<p>Ageless no more</p>
|
<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
16
debian/changelog
vendored
|
@ -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
|
satellite-gtk (0.2.8-2) unstable; urgency=medium
|
||||||
|
|
||||||
* Source-only upload to allow migration to testing
|
* Source-only upload to allow migration to testing
|
||||||
|
|
1
debian/control
vendored
1
debian/control
vendored
|
@ -23,7 +23,6 @@ Depends:
|
||||||
python3-gi,
|
python3-gi,
|
||||||
python3-gpxpy,
|
python3-gpxpy,
|
||||||
python3-nmea2,
|
python3-nmea2,
|
||||||
python3-pydbus,
|
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
${python3:Depends},
|
${python3:Depends},
|
||||||
Description: Adaptive GTK application which displays GNSS data
|
Description: Adaptive GTK application which displays GNSS data
|
||||||
|
|
3
debian/watch
vendored
3
debian/watch
vendored
|
@ -1,3 +1,2 @@
|
||||||
version=4
|
version=4
|
||||||
opts=filenamemangle=s/.*\/archive\/(\d+\.\S+)\.tar\.gz/satellite-gtk-$1\.tar\.gz
|
https://codeberg.org/tpikonen/satellite/tags .*/archive/@ANY_VERSION@\.tar\.gz
|
||||||
https://codeberg.org/tpikonen/satellite/tags .*/archive/(\d+\.\S+)\.tar\.gz
|
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
42
flatpak/page.codeberg.tpikonen.satellite.yaml
Normal file
42
flatpak/page.codeberg.tpikonen.satellite.yaml
Normal 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
|
|
@ -26,22 +26,8 @@
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"url": "https://files.pythonhosted.org/packages/c9/13/6117f735c3e8083bfce0ccd31a1d561fc2adb0e0e2d1ab3ace12256a3513/pynmea2-1.18.0-py3-none-any.whl",
|
"url": "https://files.pythonhosted.org/packages/75/24/1f575eb17a8135e54b3c243ff87e2f4d6b2389942836021d0628ed837559/pynmea2-1.19.0-py3-none-any.whl",
|
||||||
"sha256": "098f9ffd89c4a6c5e137b8b59e5b38194888d4a557c50b003ebcf2c3c15ec22e"
|
"sha256": "5138558b4fb5daa587b2c17de99eb43df0297039de1c98010c996624abfb00eb"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
gpxpy
|
gpxpy
|
||||||
pynmea2
|
pynmea2
|
||||||
pydbus
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
__version__ = "0.2.8"
|
__version__ = "0.4.2"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
@ -8,7 +8,6 @@ from .application import SatelliteApp
|
||||||
def main():
|
def main():
|
||||||
app = SatelliteApp()
|
app = SatelliteApp()
|
||||||
app.run()
|
app.run()
|
||||||
app.quit_function()
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import gi
|
import importlib.resources as resources
|
||||||
import gpxpy
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
@ -12,41 +11,57 @@ import time
|
||||||
import tokenize
|
import tokenize
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import importlib.resources as resources
|
import gi
|
||||||
|
import gpxpy
|
||||||
|
|
||||||
import satellite.nmea as nmea
|
import satellite.nmea as nmea
|
||||||
import satellite.quectel as quectel
|
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 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('Gtk', '3.0')
|
||||||
gi.require_version('Gdk', '3.0')
|
gi.require_version('Gdk', '3.0')
|
||||||
gi.require_version('Handy', '1')
|
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):
|
class SatelliteApp(Gtk.Application):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
Gtk.Application.__init__(
|
Gtk.Application.__init__(
|
||||||
self, *args, application_id="page.codeberg.tpikonen.satellite",
|
self, *args, application_id=app_id,
|
||||||
flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs)
|
flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs)
|
||||||
Handy.init()
|
Handy.init()
|
||||||
|
|
||||||
desc = "Displays navigation satellite data and saves GPX tracks"
|
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(
|
parser.add_argument(
|
||||||
'-c', '--console-output', dest='console_output',
|
'-c', '--console-output', dest='console_output',
|
||||||
action='store_true', default=False,
|
action='store_true', default=False,
|
||||||
help='Output satellite data to console')
|
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()
|
self.args = parser.parse_args()
|
||||||
|
|
||||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
|
||||||
|
@ -71,7 +86,6 @@ class SatelliteApp(Gtk.Application):
|
||||||
self, widget_api_name))
|
self, widget_api_name))
|
||||||
else:
|
else:
|
||||||
setattr(self, widget_api_name, widget)
|
setattr(self, widget_api_name, widget)
|
||||||
self.connect("activate", self.do_activate)
|
|
||||||
|
|
||||||
self.app_menu = self.builder.get_object('app-menu')
|
self.app_menu = self.builder.get_object('app-menu')
|
||||||
self.menu_popover = Gtk.Popover.new_from_model(
|
self.menu_popover = Gtk.Popover.new_from_model(
|
||||||
|
@ -81,7 +95,7 @@ class SatelliteApp(Gtk.Application):
|
||||||
|
|
||||||
self.source = None
|
self.source = None
|
||||||
|
|
||||||
self.infolabel.set_markup("<tt>" + "\n"*10 + "</tt>")
|
self.infolabel.set_markup("<tt>" + "\n" * 10 + "</tt>")
|
||||||
|
|
||||||
self.dataframe = DataFrame()
|
self.dataframe = DataFrame()
|
||||||
# self.dataframe.header.set_text("Satellite info")
|
# self.dataframe.header.set_text("Satellite info")
|
||||||
|
@ -99,13 +113,16 @@ class SatelliteApp(Gtk.Application):
|
||||||
self.set_speedlabel(None)
|
self.set_speedlabel(None)
|
||||||
self.leaflet.set_visible_child(self.databox)
|
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
|
# Internal state
|
||||||
self.last_mode = 1
|
self.last_mode = 1
|
||||||
|
self.last_data = None
|
||||||
self.last_speed = None
|
self.last_speed = None
|
||||||
self.last_update = None
|
self.last_update = None
|
||||||
self.source_lost = False
|
self.had_error = False
|
||||||
self.sigint_received = False
|
self.sigint_received = False
|
||||||
self.refresh_rate = 1 # Really delay between updates in seconds
|
self.refresh_rate = 1 # Really delay between updates in seconds
|
||||||
|
|
||||||
|
@ -147,51 +164,66 @@ class SatelliteApp(Gtk.Application):
|
||||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||||
)
|
)
|
||||||
|
|
||||||
def do_startup(self):
|
def on_startup(self, app):
|
||||||
Gtk.Application.do_startup(self)
|
|
||||||
self.create_actions()
|
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.setup_styles()
|
||||||
self.add_window(self.window)
|
self.add_window(self.window)
|
||||||
self.window.show()
|
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"{appname} version {__version__} started")
|
||||||
self.log_msg(f"Satellite version {__version__} started")
|
# Initialize modem after GUI startup
|
||||||
try:
|
GLib.timeout_add(1000, self.init_source, None)
|
||||||
self.source = QuectelNmeaSource(
|
|
||||||
self.location_update_cb,
|
def on_shutdown(self, app):
|
||||||
refresh_rate=self.refresh_rate,
|
print("Cleaning up...")
|
||||||
# save_filename=unique_filename(self.gpx_save_dir + '/nmeas',
|
self.gpx_write()
|
||||||
# '.txt')
|
if self.source is not None:
|
||||||
)
|
self.source.close()
|
||||||
self.source.initialize()
|
print("...done.")
|
||||||
except Exception as e:
|
|
||||||
fatal = False
|
def init_source(self, unused):
|
||||||
if isinstance(e, ModemLockedError):
|
source_init = False
|
||||||
self.log_msg("Modem is locked")
|
|
||||||
dtext = "Please unlock the Modem"
|
if self.args.source == 'auto':
|
||||||
else:
|
self.log_msg("Detecting NMEA sources...")
|
||||||
self.log_msg("Error initializing NMEA source")
|
if not source_init:
|
||||||
dtext = e.message if hasattr(e, 'message') else (
|
source_init = self.init_gnss_share_source(autodetect=True)
|
||||||
"Could not find or initialize NMEA source")
|
if not source_init:
|
||||||
dialog = Gtk.MessageDialog(
|
source_init = self.init_mm_source(
|
||||||
parent=self.window, modal=True,
|
quirks=['detect'], autodetect=True)
|
||||||
message_type=Gtk.MessageType.ERROR,
|
if not source_init:
|
||||||
buttons=Gtk.ButtonsType.OK, text=dtext)
|
self.log_msg('NMEA source not found')
|
||||||
dialog.set_title("Error initializing NMEA source")
|
dialog = Gtk.MessageDialog(
|
||||||
dialog.run()
|
parent=self.window, modal=True,
|
||||||
dialog.destroy()
|
message_type=Gtk.MessageType.ERROR,
|
||||||
if fatal:
|
buttons=Gtk.ButtonsType.OK,
|
||||||
self.quit()
|
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(
|
self.log_msg(
|
||||||
f"Source is {self.source.manufacturer}, model {self.source.model}"
|
f"Source is {self.source.manufacturer}"
|
||||||
+ f", revision {self.source.revision}"
|
+ (f", model {self.source.model}" if self.source.model else "")
|
||||||
if self.source.revision 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")):
|
if (self.source.model and self.source.model.startswith("QUECTEL")):
|
||||||
constellations = quectel.get_constellations(self.source)
|
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)
|
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):
|
def init_gnss_share_source(self, autodetect=False):
|
||||||
"""Called after main loop exits."""
|
try:
|
||||||
print("Cleaning up...")
|
self.source = GnssShareNmeaSource(self.location_update_cb)
|
||||||
self.gpx_write()
|
self.source.initialize()
|
||||||
if self.source is not None:
|
except Exception as e:
|
||||||
self.source.restore()
|
if autodetect:
|
||||||
print("...done.")
|
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):
|
def sigint_handler(self):
|
||||||
if not self.sigint_received:
|
if not self.sigint_received:
|
||||||
|
@ -240,12 +319,12 @@ class SatelliteApp(Gtk.Application):
|
||||||
adlg = Gtk.AboutDialog(
|
adlg = Gtk.AboutDialog(
|
||||||
transient_for=self.window,
|
transient_for=self.window,
|
||||||
modal=True,
|
modal=True,
|
||||||
program_name="Satellite",
|
program_name=appname,
|
||||||
logo_icon_name="page.codeberg.tpikonen.satellite",
|
logo_icon_name=app_id,
|
||||||
version=__version__,
|
version=__version__,
|
||||||
comments="A program for showing navigation satellite data",
|
comments="A program for showing navigation satellite data",
|
||||||
license_type=Gtk.License.GPL_3_0_ONLY,
|
license_type=Gtk.License.GPL_3_0_ONLY,
|
||||||
copyright="Copyright 2021-2022 Teemu Ikonen",
|
copyright="Copyright 2021-2023 Teemu Ikonen",
|
||||||
)
|
)
|
||||||
adlg.present()
|
adlg.present()
|
||||||
|
|
||||||
|
@ -311,31 +390,31 @@ class SatelliteApp(Gtk.Application):
|
||||||
|
|
||||||
def leaflet_forward_cb(self, button):
|
def leaflet_forward_cb(self, button):
|
||||||
self.leaflet.navigate(Handy.NavigationDirection.FORWARD)
|
self.leaflet.navigate(Handy.NavigationDirection.FORWARD)
|
||||||
return True
|
|
||||||
|
|
||||||
def leaflet_back_cb(self, button):
|
def leaflet_back_cb(self, button):
|
||||||
self.leaflet.navigate(Handy.NavigationDirection.BACK)
|
self.leaflet.navigate(Handy.NavigationDirection.BACK)
|
||||||
return True
|
|
||||||
|
|
||||||
def infolabel_released_cb(self, gesture, n_press, x, y):
|
def infolabel_released_cb(self, gesture, n_press, x, y):
|
||||||
if n_press != 1 or self.carousel.get_position() > 0.5:
|
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_small if (
|
||||||
self.chart_size == self.chart_large) else self.chart_large
|
self.chart_size == self.chart_large) else self.chart_large
|
||||||
self.update(None)
|
self.set_barchart(self.last_data)
|
||||||
return True
|
|
||||||
|
|
||||||
def carousel_page_changed_cb(self, carousel, index):
|
def carousel_page_changed_cb(self, carousel, index):
|
||||||
if index == 1 and self.chart_size == self.chart_large:
|
if index == 1 and self.chart_size == self.chart_large:
|
||||||
self.chart_size = self.chart_small
|
self.chart_size = self.chart_small
|
||||||
self.update(None)
|
self.set_barchart(self.last_data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def format_satellite_data(self, d):
|
def set_barchart(self, data):
|
||||||
bchart = text_barchart(
|
if data is None:
|
||||||
((e['prn'], e['snr']) for e in d['visibles']),
|
return ''
|
||||||
d['actives'], height=self.chart_size)
|
barchart = text_barchart(
|
||||||
return bchart
|
((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 set_values(self, data):
|
||||||
def to_str(x, fmt="%s"):
|
def to_str(x, fmt="%s"):
|
||||||
|
@ -351,6 +430,12 @@ class SatelliteApp(Gtk.Application):
|
||||||
fixage = to_str(data.get("fixage"), "%0.0f s")
|
fixage = to_str(data.get("fixage"), "%0.0f s")
|
||||||
return "%s / %s" % (up_age, fixage)
|
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 = {
|
mode2fix = {
|
||||||
"2": "2 D",
|
"2": "2 D",
|
||||||
"3": "3 D",
|
"3": "3 D",
|
||||||
|
@ -359,19 +444,19 @@ class SatelliteApp(Gtk.Application):
|
||||||
# Mapping: Data key, description, converter func
|
# Mapping: Data key, description, converter func
|
||||||
order = [
|
order = [
|
||||||
("mode", "Fix type", lambda x: mode2fix.get(x, "No Fix")),
|
("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),
|
("actives", "Active / in use sats", get_actives),
|
||||||
("visibles", "Receiving sats", lambda x: str(len(
|
("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))),
|
("visibles", "Visible sats", lambda x: str(len(x))),
|
||||||
# ("fixage", "Age of fix", lambda x: to_str(x, "%0.0f s")),
|
# ("fixage", "Age of fix", lambda x: to_str(x, "%0.0f s")),
|
||||||
("fixage", "Age of update / fix", get_ages),
|
("fixage", "Age of update / fix", get_ages),
|
||||||
("systime", "Sys. Time", lambda x: x.strftime(utcfmt)),
|
("systime", "Sys. Time", lambda x: x.strftime(utcfmt)),
|
||||||
("latlon", "Latitude",
|
("latlon", "Latitude", lambda x: "%0.6f" % x[0] if x else "-"),
|
||||||
lambda x: "%0.6f" % x[0] if x else "-"),
|
("latlon", "Longitude", lambda x: "%0.6f" % x[1] if x else "-"),
|
||||||
("latlon", "Longitude",
|
("altitude", "Altitude", lambda x: to_str(x, "%0.1f m")),
|
||||||
lambda x: "%0.6f" % x[1] if x else "-"),
|
("geoid_sep", "Geoidal separation", lambda x: to_str(x, "%0.1f m")),
|
||||||
("altitude", "Altitude", lambda x: to_str(x, "%0.1f m")),
|
|
||||||
# ("fixtime", "Time of fix",
|
# ("fixtime", "Time of fix",
|
||||||
# lambda x: x.strftime(utcfmt) if x else "-"),
|
# lambda x: x.strftime(utcfmt) if x else "-"),
|
||||||
# ("date", "Date of fix",
|
# ("date", "Date of fix",
|
||||||
|
@ -380,9 +465,7 @@ class SatelliteApp(Gtk.Application):
|
||||||
("true_course", "True Course",
|
("true_course", "True Course",
|
||||||
lambda x: to_str(x, "%0.1f deg ")
|
lambda x: to_str(x, "%0.1f deg ")
|
||||||
+ (bearing_to_arrow(x) if x is not None else "")),
|
+ (bearing_to_arrow(x) if x is not None else "")),
|
||||||
("pdop", "PDOP", lambda x: to_str(x)),
|
("pdop", "PDOP/HDOP/VDOP", get_dops),
|
||||||
("hdop", "HDOP", lambda x: to_str(x)),
|
|
||||||
("vdop", "VDOP", lambda x: to_str(x)),
|
|
||||||
]
|
]
|
||||||
descs = []
|
descs = []
|
||||||
vals = []
|
vals = []
|
||||||
|
@ -415,10 +498,9 @@ class SatelliteApp(Gtk.Application):
|
||||||
self.last_mode = mode
|
self.last_mode = mode
|
||||||
|
|
||||||
def set_speedlabel(self, speed, bearing=None):
|
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 ""
|
arrow = bearing_to_arrow(bearing) if bearing is not None else ""
|
||||||
speedfmt = ('<span size="50000">%s%s</span>\n' +
|
speedfmt = '<span size="50000">%s%s</span>\n<span size="30000">%s</span>'
|
||||||
'<span size="30000">%s</span>')
|
|
||||||
speedstr = speedfmt % (spd, arrow, "km/h")
|
speedstr = speedfmt % (spd, arrow, "km/h")
|
||||||
self.speedlabel.set_markup(speedstr)
|
self.speedlabel.set_markup(speedstr)
|
||||||
|
|
||||||
|
@ -471,19 +553,28 @@ class SatelliteApp(Gtk.Application):
|
||||||
def timeout_cb(self, x):
|
def timeout_cb(self, x):
|
||||||
dt = (time.time() - self.last_update) if self.last_update else 100
|
dt = (time.time() - self.last_update) if self.last_update else 100
|
||||||
if dt > 2 * self.refresh_rate:
|
if dt > 2 * self.refresh_rate:
|
||||||
self.update(None)
|
self.main_box.set_sensitive(False)
|
||||||
return True
|
self.update()
|
||||||
|
return GLib.SOURCE_CONTINUE
|
||||||
|
|
||||||
def location_update_cb(self, *args):
|
def location_update_cb(self, *args):
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
self.update(None)
|
self.main_box.set_sensitive(True)
|
||||||
|
self.update()
|
||||||
|
|
||||||
def update(self, x):
|
def update(self):
|
||||||
try:
|
try:
|
||||||
nmeas = self.source.get()
|
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:
|
except Exception as e:
|
||||||
fatal = False
|
nmeas = None
|
||||||
show_dialog = False
|
show_dialog = False
|
||||||
|
etext = str(e)
|
||||||
dtext = None
|
dtext = None
|
||||||
if isinstance(e, ModemLockedError):
|
if isinstance(e, ModemLockedError):
|
||||||
dtext = "Please unlock the Modem"
|
dtext = "Please unlock the Modem"
|
||||||
|
@ -491,44 +582,40 @@ class SatelliteApp(Gtk.Application):
|
||||||
elif isinstance(e, ModemNoNMEAError):
|
elif isinstance(e, ModemNoNMEAError):
|
||||||
dtext = "NMEA info not received with location"
|
dtext = "NMEA info not received with location"
|
||||||
elif isinstance(e, ModemError):
|
elif isinstance(e, ModemError):
|
||||||
dtext = "Unspecified modem error"
|
dtext = "Modem error: " + str(e)
|
||||||
elif isinstance(e, NmeaSourceNotFoundError):
|
elif isinstance(e, NmeaSourceNotFoundError):
|
||||||
if not self.source_lost:
|
if not self.had_error:
|
||||||
dtext = e.message if (
|
dtext = etext if etext else "Modem disappeared"
|
||||||
hasattr(e, 'message')) else "Modem disappeared"
|
self.had_error = True
|
||||||
self.source_lost = True
|
|
||||||
self.main_box.set_sensitive(False)
|
self.main_box.set_sensitive(False)
|
||||||
else:
|
else:
|
||||||
dtext = e.message if hasattr(e, 'message') else "Unknown error"
|
dtext = etext if etext else "Unknown error"
|
||||||
if show_dialog:
|
if not self.had_error:
|
||||||
dialog = Gtk.MessageDialog(
|
if show_dialog:
|
||||||
parent=self.window, modal=True,
|
dialog = Gtk.MessageDialog(
|
||||||
message_type=Gtk.MessageType.ERROR,
|
parent=self.window, modal=True,
|
||||||
buttons=Gtk.ButtonsType.OK, text=dtext)
|
message_type=Gtk.MessageType.ERROR,
|
||||||
dialog.set_title("Unrecoverable error" if fatal else "Error")
|
buttons=Gtk.ButtonsType.OK, text=dtext)
|
||||||
dialog.run()
|
dialog.set_title("Error")
|
||||||
dialog.destroy()
|
dialog.run()
|
||||||
if fatal:
|
dialog.destroy()
|
||||||
self.quit()
|
elif dtext is not None:
|
||||||
elif dtext is not None:
|
self.log_msg(dtext)
|
||||||
self.log_msg(dtext)
|
self.had_error = True
|
||||||
return 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)
|
data["updateage"] = ((time.time() - self.last_update)
|
||||||
if self.last_update else None)
|
if self.last_update else None)
|
||||||
barchart = self.format_satellite_data(data)
|
|
||||||
|
barchart = self.set_barchart(data)
|
||||||
if self.args.console_output:
|
if self.args.console_output:
|
||||||
print(barchart)
|
print(barchart)
|
||||||
self.infolabel.set_markup("<tt>" + barchart + "</tt>")
|
|
||||||
self.set_values(data)
|
self.set_values(data)
|
||||||
mode = data["mode"]
|
|
||||||
mode = int(mode) if mode else 0
|
|
||||||
speed = data['speed']
|
speed = data['speed']
|
||||||
bearing = data['true_course']
|
bearing = data['true_course']
|
||||||
self.set_speedlabel(speed, bearing)
|
self.set_speedlabel(speed, bearing)
|
||||||
|
@ -537,13 +624,18 @@ class SatelliteApp(Gtk.Application):
|
||||||
elif not speed and self.last_speed:
|
elif not speed and self.last_speed:
|
||||||
self.carousel.scroll_to(self.infolabel)
|
self.carousel.scroll_to(self.infolabel)
|
||||||
self.last_speed = speed
|
self.last_speed = speed
|
||||||
|
|
||||||
# log
|
# log
|
||||||
|
mode = data["mode"]
|
||||||
|
mode = int(mode) if mode else self.last_mode
|
||||||
if mode != self.last_mode:
|
if mode != self.last_mode:
|
||||||
if mode > 1:
|
if mode > 1:
|
||||||
self.log_msg(f"Got lock, mode: {mode}")
|
self.log_msg(f"Got lock, mode: {mode}")
|
||||||
elif mode <= 1:
|
elif mode <= 1:
|
||||||
self.log_msg("Lock lost")
|
self.log_msg("Lock lost")
|
||||||
self.last_mode = mode
|
self.last_mode = mode
|
||||||
|
|
||||||
if self.gpx is not None and data.get("valid"):
|
if self.gpx is not None and data.get("valid"):
|
||||||
self.gpx_update(data)
|
self.gpx_update(data)
|
||||||
return True
|
|
||||||
|
self.last_data = data
|
||||||
|
|
158
satellite/mm_glib_source.py
Normal file
158
satellite/mm_glib_source.py
Normal 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
|
|
@ -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
|
|
|
@ -1,7 +1,9 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
import pynmea2
|
import pynmea2
|
||||||
|
|
||||||
MS_PER_KNOT = 0.514444
|
MS_PER_KNOT = 0.514444
|
||||||
|
@ -27,7 +29,7 @@ def fget(key, scale=1.0):
|
||||||
def iget(key, default=None):
|
def iget(key, default=None):
|
||||||
def fn(d):
|
def fn(d):
|
||||||
try:
|
try:
|
||||||
return int(d.get(key))
|
return int(d.get(key, default))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return default
|
return default
|
||||||
return fn
|
return fn
|
||||||
|
@ -60,9 +62,9 @@ def get_latlon(mdict):
|
||||||
lat_min = float(lat[2:])
|
lat_min = float(lat[2:])
|
||||||
lon_deg = float(lon[:3])
|
lon_deg = float(lon[:3])
|
||||||
lon_min = 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
|
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
|
flon = -1 * flon if lon_dir == 'W' else flon
|
||||||
return (flat, flon)
|
return (flat, flon)
|
||||||
|
|
||||||
|
@ -97,18 +99,17 @@ getters = {
|
||||||
'GGA': get_altitude_gga,
|
'GGA': get_altitude_gga,
|
||||||
'GNS': fget('altitude'),
|
'GNS': fget('altitude'),
|
||||||
},
|
},
|
||||||
"fixtime": {
|
"fixtime": { # Time of position report
|
||||||
'RMC': get_time,
|
'RMC': get_time,
|
||||||
'GGA': get_time,
|
|
||||||
},
|
|
||||||
"time": { # Reported also when no fix
|
|
||||||
'GNS': get_time,
|
'GNS': get_time,
|
||||||
|
'GGA': get_time,
|
||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
'RMC': get_date,
|
'RMC': get_date,
|
||||||
},
|
},
|
||||||
"valid": {
|
"valid": {
|
||||||
'RMC': lambda x: x.get('status') == 'A',
|
'RMC': lambda x: x.get('status') == 'A',
|
||||||
|
'GSA': lambda x: iget('mode_fix_type', 1)(x) > 1,
|
||||||
},
|
},
|
||||||
"speed": {
|
"speed": {
|
||||||
'RMC': fget('spd_over_grnd', MS_PER_KNOT),
|
'RMC': fget('spd_over_grnd', MS_PER_KNOT),
|
||||||
|
@ -129,6 +130,7 @@ getters = {
|
||||||
},
|
},
|
||||||
"num_sats": {
|
"num_sats": {
|
||||||
'GNS': iget('num_sats', default=0),
|
'GNS': iget('num_sats', default=0),
|
||||||
|
'GGA': iget('num_sats', default=0),
|
||||||
},
|
},
|
||||||
"pdop": {
|
"pdop": {
|
||||||
'GSA': fget('pdop'),
|
'GSA': fget('pdop'),
|
||||||
|
@ -141,7 +143,8 @@ getters = {
|
||||||
"vdop": {
|
"vdop": {
|
||||||
'GSA': fget('vdop'),
|
'GSA': fget('vdop'),
|
||||||
},
|
},
|
||||||
"geo_sep": {
|
"geoid_sep": {
|
||||||
|
'GGA': fget('geo_sep'),
|
||||||
'GNS': fget('geo_sep'),
|
'GNS': fget('geo_sep'),
|
||||||
},
|
},
|
||||||
"sel_mode": {
|
"sel_mode": {
|
||||||
|
@ -180,7 +183,7 @@ def parse(nmeas, always_add_prefix=False):
|
||||||
return float(s) if s else empty_val
|
return float(s) if s else empty_val
|
||||||
|
|
||||||
def add_prn_prefix(prns, talker, always=always_add_prefix):
|
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"
|
beidou_prefix = "C"
|
||||||
galileo_prefix = "E"
|
galileo_prefix = "E"
|
||||||
glonass_prefix = "R"
|
glonass_prefix = "R"
|
||||||
|
@ -206,12 +209,13 @@ def parse(nmeas, always_add_prefix=False):
|
||||||
|
|
||||||
return "%s%02d" % (prefix, prn)
|
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:
|
for msg in parsed:
|
||||||
# print(repr(msg))
|
|
||||||
keys = []
|
|
||||||
for field in msg.fields:
|
|
||||||
keys.append(field[1])
|
|
||||||
if isinstance(msg, pynmea2.types.GSV):
|
if isinstance(msg, pynmea2.types.GSV):
|
||||||
for n in range(1, (len(msg.data) - 4) // 4 + 1):
|
for n in range(1, (len(msg.data) - 4) // 4 + 1):
|
||||||
prns = getattr(msg, f'sv_prn_num_{n}', None)
|
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),
|
'snr': fl(getattr(msg, f'snr_{n}', None), 0.0),
|
||||||
})
|
})
|
||||||
elif isinstance(msg, pynmea2.types.GSA):
|
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}')
|
prns = getattr(msg, f'sv_id{n:02d}')
|
||||||
if prns and prns.isdigit():
|
if prns and prns.isdigit():
|
||||||
actives.append(add_prn_prefix(prns, msg.talker))
|
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()})
|
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')
|
fixtime = out.get('fixtime')
|
||||||
fixdate = out.get('date')
|
fixdate = out.get('date')
|
||||||
if fixdate is None and fixtime is not None:
|
if fixdate is None and fixtime is not None:
|
||||||
# We have a fix but no RMC sentence
|
# We have a fix but no RMC sentence
|
||||||
fixdate = datenow.date()
|
fixdate = datenow.date()
|
||||||
fixdt = (datetime.datetime.combine(fixdate, fixtime)
|
fixdt = (datetime.datetime.combine(fixdate, fixtime, datetime.timezone.utc)
|
||||||
if (fixtime and fixdate) else None)
|
if (fixtime and fixdate) else None)
|
||||||
out["datetime"] = fixdt
|
out["datetime"] = fixdt
|
||||||
out["systime"] = datenow
|
out["systime"] = datenow
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import re
|
import os.path
|
||||||
import satellite.modem_manager_defs as mm
|
import socket
|
||||||
from pydbus import SystemBus
|
|
||||||
from pynmea2.nmea import NMEASentence
|
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,121 +55,62 @@ class NmeaSource:
|
||||||
self._maybe_save(nmeas)
|
self._maybe_save(nmeas)
|
||||||
return nmeas
|
return nmeas
|
||||||
|
|
||||||
def restore(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModemManagerNmeaSource(NmeaSource):
|
class UnixSocketNmeaSource(NmeaSource):
|
||||||
def __init__(self, update_callback, **kwargs):
|
def __init__(self,
|
||||||
|
update_callback,
|
||||||
|
socket_file_path=None,
|
||||||
|
**kwargs):
|
||||||
super().__init__(update_callback, **kwargs)
|
super().__init__(update_callback, **kwargs)
|
||||||
self.bus = SystemBus()
|
self.socket_file_path = socket_file_path
|
||||||
self.manager = self.bus.get('.ModemManager1')
|
|
||||||
self.modem = None
|
def on_read_data_available(self, io_channel, condition, **unused):
|
||||||
self.old_refresh_rate = None
|
self.update_callback()
|
||||||
self.old_sources_enabled = None
|
return True
|
||||||
self.old_signals = None
|
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
objs = self.manager.GetManagedObjects()
|
if (self.socket_file_path is None
|
||||||
mkeys = list(objs.keys())
|
or not os.path.exists(self.socket_file_path)):
|
||||||
if mkeys:
|
raise FileNotFoundError(f"Could not open socket {self.socket_file_path}")
|
||||||
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)
|
|
||||||
|
|
||||||
|
self.s = socket.socket(socket.AF_UNIX,
|
||||||
|
socket.SOCK_NONBLOCK | socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
print("Modem state is: %d" % self.modem.State)
|
self.s.connect(self.socket_file_path)
|
||||||
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
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
self.modem.SetGpsRefreshRate(self.refresh_rate)
|
self.channel = GLib.IOChannel.unix_new(self.s.fileno())
|
||||||
self.bus.subscribe(
|
self.channel.add_watch(GLib.IO_IN, self.on_read_data_available)
|
||||||
sender='org.freedesktop.ModemManager1',
|
|
||||||
iface='org.freedesktop.DBus.Properties',
|
self.old_nmea_buf = ''
|
||||||
signal='PropertiesChanged',
|
|
||||||
arg0='org.freedesktop.ModemManager1.Modem.Location',
|
|
||||||
signal_fired=self.update_callback)
|
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
|
|
||||||
def _really_get(self):
|
def _really_get(self):
|
||||||
if not self.initialized:
|
if not self.initialized:
|
||||||
self.initialize()
|
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)
|
buf = ''
|
||||||
if retval is None:
|
do_read = True
|
||||||
print("Location does not have NMEA information")
|
|
||||||
self.initialized = False
|
|
||||||
raise ModemNoNMEAError
|
|
||||||
return retval
|
|
||||||
|
|
||||||
def restore(self):
|
while do_read:
|
||||||
if self.old_sources_enabled is not None:
|
read_buf = self.channel.readline()
|
||||||
self.modem.Setup(self.old_sources_enabled, self.old_signals)
|
buf += read_buf
|
||||||
if self.old_refresh_rate is not None:
|
if read_buf == '':
|
||||||
self.modem.SetGpsRefreshRate(self.old_refresh_rate)
|
do_read = False
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
class QuectelNmeaSource(ModemManagerNmeaSource):
|
class GnssShareNmeaSource(UnixSocketNmeaSource):
|
||||||
|
def __init__(self, update_callback, **kwargs):
|
||||||
def _really_get(self):
|
super().__init__(update_callback,
|
||||||
return self.fix_talker(super()._really_get())
|
socket_file_path='/var/run/gnss-share.sock',
|
||||||
|
**kwargs)
|
||||||
def fix_talker(self, nmeas):
|
self.manufacturer = "gnss-share"
|
||||||
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 ReplayNmeaSource(NmeaSource):
|
class ReplayNmeaSource(NmeaSource):
|
||||||
|
@ -201,6 +140,8 @@ class ReplayNmeaSource(NmeaSource):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _really_get(self):
|
def _really_get(self):
|
||||||
|
if not self.nmeas:
|
||||||
|
return None
|
||||||
nmea = self.nmeas[self.counter]
|
nmea = self.nmeas[self.counter]
|
||||||
self.counter += 1
|
self.counter += 1
|
||||||
if self.counter >= len(self.nmeas):
|
if self.counter >= len(self.nmeas):
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from .util import (
|
from .util import (
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
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)
|
gps_epoch = datetime(1980, 1, 6, 0, 0, tzinfo=timezone.utc)
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
one_week = 7 * 24 * 60 * 60
|
one_week = 7 * 24 * 60 * 60
|
||||||
week_now = int((now.timestamp() - gps_epoch.timestamp()) / one_week)
|
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):
|
def datetime_from_gpstime(week, millisecs, fix_week=False):
|
||||||
"""Return a datetime object formed from GPS week number and
|
"""Return a datetime from GPS week number and milliseconds from week start.
|
||||||
milliseconds from week start.
|
|
||||||
|
|
||||||
If fix_week is True, set the bits above 10 in week number from
|
If fix_week is True, set the bits above 10 in week number from
|
||||||
current date, see
|
current date, see
|
||||||
|
@ -27,7 +35,7 @@ def datetime_from_gpstime(week, millisecs, fix_week=False):
|
||||||
|
|
||||||
|
|
||||||
def gpstime_from_datetime(dt):
|
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:
|
if dt < gps_epoch:
|
||||||
raise ValueError("Time cannot be less than GPS epoch")
|
raise ValueError("Time cannot be less than GPS epoch")
|
||||||
ts = dt.timestamp()
|
ts = dt.timestamp()
|
||||||
|
@ -40,7 +48,7 @@ def gpstime_from_datetime(dt):
|
||||||
def unique_filename(namestem, ext, timestamp=False):
|
def unique_filename(namestem, ext, timestamp=False):
|
||||||
if timestamp:
|
if timestamp:
|
||||||
namestem += "-" + datetime.now().isoformat(
|
namestem += "-" + datetime.now().isoformat(
|
||||||
'_', 'seconds').replace(':', '.')
|
'_', 'seconds').replace(':', '.')
|
||||||
name = None
|
name = None
|
||||||
for count in ('~%d' % n if n > 0 else '' for n in range(100)):
|
for count in ('~%d' % n if n > 0 else '' for n in range(100)):
|
||||||
test = namestem + count + ext
|
test = namestem + count + ext
|
||||||
|
@ -66,7 +74,7 @@ def bearing_to_arrow(bearing):
|
||||||
'\u2196',
|
'\u2196',
|
||||||
'\u2191',
|
'\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
|
angle = bearing - (bearing // 360) * 360
|
||||||
index = next(ind for (ind, e) in enumerate(edges) if angle < e)
|
index = next(ind for (ind, e) in enumerate(edges) if angle < e)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# Copyright 2021-2022 Teemu Ikonen
|
# Copyright 2021-2023 Teemu Ikonen
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
import importlib.resources as resources
|
import importlib.resources as resources
|
||||||
|
|
||||||
|
import gi
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
gi.require_version('Gdk', '3.0')
|
gi.require_version('Gdk', '3.0')
|
||||||
from gi.repository import Gtk # noqa: E402
|
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
|
height Number of lines in the generated bar chart
|
||||||
width Width of the generated bar chart in chars
|
width Width of the generated bar chart in chars
|
||||||
"""
|
"""
|
||||||
sdata = list((d[0] if d[0] else '',
|
sdata = [(d[0] if d[0] else '', int(d[1]) if d[1] else 0) for d in data]
|
||||||
int(d[1]) if d[1] else 0) for d in data)
|
|
||||||
sdata.sort(key=lambda x: x[1], reverse=True)
|
sdata.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
dstr = ''
|
dstr = ''
|
||||||
|
@ -41,43 +39,17 @@ def text_barchart(data, highlights, height=None, width=30):
|
||||||
cmax_xaxis = cmaxbar + 3
|
cmax_xaxis = cmaxbar + 3
|
||||||
for d in sdata[:barlines]:
|
for d in sdata[:barlines]:
|
||||||
block = '\u2585' if d[0] in highlights else '='
|
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):
|
if barlines < len(sdata):
|
||||||
dstr += " \u256a\n"
|
dstr += " \u256a\n"
|
||||||
elif (len(sdata) - axislines) < height:
|
elif (len(sdata) - axislines) < height:
|
||||||
# Add empty lines to y-axis
|
# Add empty lines to y-axis
|
||||||
dstr += ' \u2502\n' * (height - len(sdata) - axislines)
|
dstr += ' \u2502\n' * (height - len(sdata) - axislines)
|
||||||
dstr += " \u251c" + '\u2500'*(cmax_xaxis) + '\u2524\n'
|
dstr += " \u251c" + '\u2500' * (cmax_xaxis) + '\u2524\n'
|
||||||
dstr += " 0" + ' '*(cmax_xaxis - 1) + str(max_xaxis)
|
dstr += " 0" + ' ' * (cmax_xaxis - 1) + str(max_xaxis)
|
||||||
return dstr
|
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'))
|
@Gtk.Template(string=resources.read_text('satellite', 'dataframe.ui'))
|
||||||
class DataFrame(Gtk.Bin):
|
class DataFrame(Gtk.Bin):
|
||||||
__gtype_name__ = 'DataFrame'
|
__gtype_name__ = 'DataFrame'
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude=.git,__pycache__,build
|
exclude=.git,__pycache__,build
|
||||||
|
max-line-length=88
|
||||||
|
ignore = B902, BLK100, CCR001, CNL100, D1, I201, Q000, W503
|
||||||
|
|
||||||
|
[pycodestyle]
|
||||||
|
count=1
|
||||||
|
max-line-length = 88
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -29,7 +29,7 @@ setuptools.setup(
|
||||||
"Bug Tracker": "https://codeberg.org/tpikonen/satellite/issues",
|
"Bug Tracker": "https://codeberg.org/tpikonen/satellite/issues",
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: X11 Applications :: GTK",
|
"Environment :: X11 Applications :: GTK",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
|
Loading…
Reference in a new issue