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
|
||||
|
||||
![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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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
|
||||
|
||||
* Source-only upload to allow migration to testing
|
||||
|
|
1
debian/control
vendored
1
debian/control
vendored
|
@ -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
3
debian/watch
vendored
|
@ -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
|
||||
|
|
|
@ -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": [
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
gpxpy
|
||||
pynmea2
|
||||
pydbus
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
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
|
||||
|
||||
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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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)",
|
||||
|
|
Loading…
Reference in a new issue