16 Commits

Author SHA1 Message Date
Alessandro
84dc68dd55 Update changelog for v3.2.1 2020-04-25 15:42:00 +02:00
Alessandro
cfd302d3ca Close #16 Remove 'exclusive' parameter from a PyQt function. Also update readme.md 2020-04-18 21:43:03 +02:00
AresValley
3c6658d19d Merge commit '5af0faaa65432db23d391ee65a6fe9f85021f032' 2020-04-18 21:04:16 +02:00
AresValley
5af0faaa65 Minor raspberry fix 2020-04-18 21:02:28 +02:00
Alessandro
ce2cfdc76a Update README.md with latest changes 2020-04-15 20:23:26 +02:00
Alessandro
6e0a161b89 Merge branch 'new_forecast_files' 2020-04-15 20:14:44 +02:00
Alessandro
940c6a0d58 Merge branch 'master' of https://github.com/AresValley/Artemis 2020-04-15 20:14:36 +02:00
AresValley
194b5c8fb8 Fixed categorization for very low x-ray flux according to NOAA format 2020-04-15 13:13:33 +02:00
Alessandro
4e1b3f24c5 Close #21 Some forecast data has benn changed to json in the download site.
Such cases are now handled
2020-04-11 17:16:21 +02:00
Alessandro
eaeb51de65 Add some basic logging to the application. Also for severe errors, track them in info.log file in local folder 2020-04-11 15:27:05 +02:00
Alessandro
995696f11a Add raspberryPI support 2020-04-11 00:50:37 +02:00
Marco Dalla Tiezza
7503b6bb14 License update 2020-04-10 13:11:24 +02:00
Marco Dalla Tiezza
b867ca849d Update README.md 2020-04-10 13:09:29 +02:00
Alessandro
ab32fbbf98 Some minor style improvements 2020-02-29 21:43:40 +01:00
Alessandro
bcd24cc035 Close #14 Make font customizable. Also manage user settings via a settings.json
file. Also improve 'dark' and 'elegant_dark' themes.
Finally improve 'Signal's wiki' button behaviour.
Also fix a bug in forecast/now view which caused a crash if solar activity
was inactive
2019-12-14 11:50:35 +01:00
Alessandro
5908110a43 Remove _AsyncDownloader, just use a regular function instead 2019-11-28 22:08:38 +01:00
22 changed files with 684 additions and 299 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,4 @@
__pycache__ __PYCache__
Data Data
src/themes/__current_theme src/themes/__current_theme
designer.bat designer.bat
@@ -7,3 +7,5 @@ launch.bat
.code-workspace .code-workspace
spec_files/**/output spec_files/**/output
*.txt *.txt
*.json
info.log

View File

@@ -6,6 +6,26 @@ The first release is [3.0.0] because this is actually the third major version (c
## [Unreleased] ## [Unreleased]
... ...
## [3.2.1] - 2020-04-25
### Added
- Add some basic logging to the application. Also for severe errors, track them in info.log file in local folder.
- Add Raspberry PI support ([#18](https://github.com/AresValley/Artemis/pull/18), [#20](https://github.com/AresValley/Artemis/pull/20))
### Fixed
- Support new `JSON` format for some forecast data ([#21](https://github.com/AresValley/Artemis/pull/14)).
- Fixed categorization for very low x-ray flux according to NOAA format.
- Remove the `exclusive` parameter in a PyQt function ([#16](https://github.com/AresValley/Artemis/pull/16)).
## [3.2.0] - 2019-12-14
### Added
- The default font can be changed ([#14](https://github.com/AresValley/Artemis/pull/14)).
- Move `Themes` into `Settings`.
- Better settings management in `settings.json`.
### Fixed
- Fix a bug in the space weather. An inactive k-index caused a crash.
## [3.1.0] - 2019-10-21 ## [3.1.0] - 2019-10-21
### Added ### Added
@@ -36,7 +56,9 @@ First release.
<!-- Links definitions --> <!-- Links definitions -->
[Unreleased]: https://github.com/AresValley/Artemis/compare/v3.1.0...HEAD [Unreleased]: https://github.com/AresValley/Artemis/compare/v3.2.1...HEAD
[3.2.1]: https://github.com/AresValley/Artemis/compare/v3.2.0...v3.2.1
[3.2.0]: https://github.com/AresValley/Artemis/compare/v3.1.0...v3.2.0
[3.1.0]: https://github.com/AresValley/Artemis/compare/v3.0.1...v3.1.0 [3.1.0]: https://github.com/AresValley/Artemis/compare/v3.0.1...v3.1.0
[3.0.1]: https://github.com/AresValley/Artemis/compare/v3.0.0...v3.0.1 [3.0.1]: https://github.com/AresValley/Artemis/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/AresValley/Artemis/releases/tag/v3.0.0 [3.0.0]: https://github.com/AresValley/Artemis/releases/tag/v3.0.0

View File

@@ -147,7 +147,7 @@ The only folder with the pre-built package is the `themes` one. In this way the
Some of the available themes were adapted from https://github.com/GTRONICK/QSS. Some of the available themes were adapted from https://github.com/GTRONICK/QSS.
## License ## License
This program (ARTEMIS 3, 2014-2019) is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program (ARTEMIS 3, 2014-2020) is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
@@ -159,5 +159,6 @@ You should have received a copy of the GNU General Public License along with thi
* **Paolo Romani (IZ1MLL)** - *Lead β Tester, RF specialist* * **Paolo Romani (IZ1MLL)** - *Lead β Tester, RF specialist*
* **Carl Colena** - *Sigidwiki admin, β Tester, Signals expert* * **Carl Colena** - *Sigidwiki admin, β Tester, Signals expert*
* [**Marco Bortoli**](https://github.com/marbort "GitHub profile") - *macOS deployment, β Tester* * [**Marco Bortoli**](https://github.com/marbort "GitHub profile") - *macOS deployment, β Tester*
* [**Eric Wiessner (KI7POL)**](https://github.com/WheezyE "GitHub profile") - *ARM port (Raspberry Pi3B+ and Pi4B)*
* [**Pierpaolo Pravatto**](https://github.com/ppravatto "GitHub profile") - *Wiki page, β Tester* * [**Pierpaolo Pravatto**](https://github.com/ppravatto "GitHub profile") - *Wiki page, β Tester*
* [**Francesco Capostagno**](https://github.com/fcapostagno "GitHub profile"), **Luca**, **Pietro** - *β Tester* * [**Francesco Capostagno**](https://github.com/fcapostagno "GitHub profile"), **Luca**, **Pietro** - *β Tester*

View File

@@ -28,14 +28,13 @@ class ACFValue:
self._description = "" self._description = ""
self._value = value self._value = value
self._string = self._value self._string = self._value
try: if self._value.isdigit():
self.numeric_value = float(self._value) self.numeric_value = float(self._value)
except Exception:
self.is_numeric = False
self.numeric_value = 0.0
else:
self.is_numeric = True self.is_numeric = True
self._string += " ms" self._string += " ms"
else:
self.is_numeric = False
self.numeric_value = 0.0
@classmethod @classmethod
def list_from_series(cls, series): def list_from_series(cls, series):

View File

@@ -1,9 +1,11 @@
from collections import namedtuple from collections import namedtuple
from itertools import chain
from functools import partial from functools import partial
import webbrowser import webbrowser
import os import os
import sys import sys
from time import sleep, time from time import sleep, time
import logging
from pandas import read_csv from pandas import read_csv
@@ -15,8 +17,10 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QListWidgetItem,
QMessageBox, QMessageBox,
QSplashScreen, QSplashScreen,
QFontDialog,
QWidget,
) )
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap, QFont
from PyQt5 import uic from PyQt5 import uic
from PyQt5.QtCore import ( from PyQt5.QtCore import (
QFileInfo, QFileInfo,
@@ -46,17 +50,21 @@ from utilities import (
is_undef_band, is_undef_band,
format_numbers, format_numbers,
safe_cast, safe_cast,
UniqueMessageBox,
) )
from executable_utilities import IS_BINARY, resource_path from executable_utilities import IS_BINARY, resource_path
from os_utilities import IS_MAC from os_utilities import IS_MAC
from web_utilities import get_db_hash_code from web_utilities import get_db_hash_code
from downloadtargetfactory import get_download_target from downloadtargetfactory import get_download_target
from settings import Settings
from updatescontroller import UpdatesController from updatescontroller import UpdatesController
from urlbutton import UrlButton
import loggingconf # noqa 401
# import default_imgs_rc # import default_imgs_rc
__LATEST_VERSION__ = "3.1.0" __LATEST_VERSION__ = "3.2.1"
if IS_BINARY: if IS_BINARY:
__VERSION__ = __LATEST_VERSION__ __VERSION__ = __LATEST_VERSION__
@@ -101,6 +109,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.action_github.triggered.connect( self.action_github.triggered.connect(
lambda: webbrowser.open(Constants.GITHUB_REPO) lambda: webbrowser.open(Constants.GITHUB_REPO)
) )
self.action_font.triggered.connect(self.start_font_selection)
self.db = None self.db = None
self.current_signal_name = '' self.current_signal_name = ''
self.signal_names = [] self.signal_names = []
@@ -124,8 +133,6 @@ class Artemis(QMainWindow, Ui_MainWindow):
# ####################################################################################### # #######################################################################################
UrlColors = namedtuple("UrlColors", ["inactive", "active", "clicked"])
self.url_button.colors = UrlColors("#9f9f9f", "#4c75ff", "#942ccc")
self.category_labels = [ self.category_labels = [
self.cat_mil, self.cat_mil,
self.cat_rad, self.cat_rad,
@@ -205,10 +212,81 @@ class Artemis(QMainWindow, Ui_MainWindow):
) )
# Final operations. # Final operations.
self.settings = Settings()
self.settings.load()
self.theme_manager.start() self.theme_manager.start()
self.load_font()
self.load_db() self.load_db()
self.display_signals() self.display_signals()
def apply_font(self, font):
"""Apply a given QFont object to all the widgets."""
UniqueMessageBox.set_font(font)
# This is the smaller-text label. Not the most general strategy, but whatever..
smaller_point_size = self.forecast_today_0_lbl.font().pointSize()
min_reference_font = 4
for w in set(chain(
self.findChildren(QWidget),
self.download_window.findChildren(QWidget)
)):
old_font = w.font()
point_size = old_font.pointSize()
new_font = QFont(font)
new_font.setUnderline(old_font.underline())
new_font.setBold(old_font.bold())
new_font.setItalic(old_font.italic())
new_size = font.pointSize() + (point_size - smaller_point_size)
if new_size < min_reference_font:
new_size = min_reference_font
new_font.setPointSize(new_size)
w.setFont(new_font)
def load_font(self):
"""Apply a QFont object if present."""
if self.settings.font is None:
return
try:
font = QFont(
self.settings.font['family'],
self.settings.font['point_size'],
self.settings.font['weight'],
self.settings.font['italic']
)
font.setStyle(self.settings.font['style'])
font.setPointSize(self.settings.font['point_size'])
font.setStrikeOut(self.settings.font['strikeout'])
font.setUnderline(self.settings.font['underline'])
self.apply_font(font)
except Exception: # Invalid font
logging.warning("Invalid Font in settings.json")
pass
@pyqtSlot()
def start_font_selection(self):
"""Open a font selection widget and apply the selected font."""
initial_font = self.description_text.font()
dialog = QFontDialog()
dialog.setCurrentFont(initial_font)
font, ok = dialog.getFont(
initial_font,
self,
"Choose a font",
options=QFontDialog.DontUseNativeDialog
)
if ok:
self.apply_font(font)
self.settings.save(
font={
'family': font.family(),
'style': font.style(),
'point_size': font.pointSize(),
'weight': font.weight(),
'italic': font.italic(),
'strikeout': font.strikeOut(),
'underline': font.underline(),
}
)
def action_after_download(self): def action_after_download(self):
"""Decide what to do after a successful download. """Decide what to do after a successful download.
@@ -282,6 +360,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
try: try:
webbrowser.open(Constants.GFD_SITE + query.lower()) webbrowser.open(Constants.GFD_SITE + query.lower())
except Exception: except Exception:
logging.error("Cannot open browser")
pass pass
def set_initial_size(self): def set_initial_size(self):
@@ -370,7 +449,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
else: else:
try: try:
is_checksum_ok = checksum_ok(db, get_db_hash_code()) is_checksum_ok = checksum_ok(db, get_db_hash_code())
except Exception: except ValueError as e:
logging.info(e)
pop_up(self, title=Messages.NO_CONNECTION, pop_up(self, title=Messages.NO_CONNECTION,
text=Messages.NO_CONNECTION_MSG).show() text=Messages.NO_CONNECTION_MSG).show()
else: else:
@@ -410,7 +490,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
else: else:
try: try:
is_checksum_ok = checksum_ok(db, get_db_hash_code()) is_checksum_ok = checksum_ok(db, get_db_hash_code())
except Exception: except ValueError as e:
logging.info(e)
pop_up(self, title=Messages.NO_CONNECTION, pop_up(self, title=Messages.NO_CONNECTION,
text=Messages.NO_CONNECTION_MSG).show() text=Messages.NO_CONNECTION_MSG).show()
else: else:
@@ -544,15 +625,11 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.name_lab.setText(self.current_signal_name) self.name_lab.setText(self.current_signal_name)
self.name_lab.setAlignment(Qt.AlignHCenter) self.name_lab.setAlignment(Qt.AlignHCenter)
current_signal = self.db.loc[self.current_signal_name] current_signal = self.db.loc[self.current_signal_name]
self.url_button.setEnabled(True)
if not current_signal.at[Signal.WIKI_CLICKED]: if not current_signal.at[Signal.WIKI_CLICKED]:
self.url_button.setStyleSheet( state = UrlButton.State.ACTIVE
f"color: {self.url_button.colors.active};"
)
else: else:
self.url_button.setStyleSheet( state = UrlButton.State.CLICKED
f"color: {self.url_button.colors.clicked};" self.url_button.set_enabled(state)
)
category_code = current_signal.at[Signal.CATEGORY_CODE] category_code = current_signal.at[Signal.CATEGORY_CODE]
undef_freq = is_undef_freq(current_signal) undef_freq = is_undef_freq(current_signal)
undef_band = is_undef_band(current_signal) undef_band = is_undef_band(current_signal)
@@ -590,10 +667,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.set_band_range(current_signal) self.set_band_range(current_signal)
self.audio_widget.set_audio_player(self.current_signal_name) self.audio_widget.set_audio_player(self.current_signal_name)
else: else:
self.url_button.setEnabled(False) self.url_button.set_disabled()
self.url_button.setStyleSheet(
f"color: {self.url_button.colors.inactive};"
)
self.current_signal_name = '' self.current_signal_name = ''
self.name_lab.setText("No Signal") self.name_lab.setText("No Signal")
self.name_lab.setAlignment(Qt.AlignHCenter) self.name_lab.setAlignment(Qt.AlignHCenter)
@@ -665,9 +739,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
Do nothing if no signal is selected. Do nothing if no signal is selected.
""" """
if self.current_signal_name: if self.current_signal_name:
self.url_button.setStyleSheet( self.url_button.set_clicked()
f"color: {self.url_button.colors.clicked}"
)
webbrowser.open(self.db.at[self.current_signal_name, Signal.URL]) webbrowser.open(self.db.at[self.current_signal_name, Signal.URL])
self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True

View File

@@ -1255,7 +1255,7 @@
</property> </property>
<layout class="QHBoxLayout" name="horizontalLayout_17"> <layout class="QHBoxLayout" name="horizontalLayout_17">
<item> <item>
<widget class="QPushButton" name="url_button"> <widget class="UrlButton" name="url_button">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
@@ -6949,7 +6949,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -6977,7 +6979,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7005,7 +7009,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7033,7 +7039,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7061,7 +7069,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7092,7 +7102,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7120,7 +7132,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7148,7 +7162,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -7176,7 +7192,9 @@ STORM</string>
<property name="font"> <property name="font">
<font> <font>
<pointsize>13</pointsize> <pointsize>13</pointsize>
<weight>75</weight>
<italic>false</italic> <italic>false</italic>
<bold>true</bold>
</font> </font>
</property> </property>
<property name="layoutDirection"> <property name="layoutDirection">
@@ -9516,11 +9534,6 @@ QSlider::handle:horizontal {
<addaction name="action_update_database"/> <addaction name="action_update_database"/>
<addaction name="action_check_software_version"/> <addaction name="action_check_software_version"/>
</widget> </widget>
<widget class="QMenu" name="menu_themes">
<property name="title">
<string>Themes</string>
</property>
</widget>
<widget class="QMenu" name="menuSigidwiki"> <widget class="QMenu" name="menuSigidwiki">
<property name="title"> <property name="title">
<string>Sigidwiki</string> <string>Sigidwiki</string>
@@ -9537,10 +9550,16 @@ QSlider::handle:horizontal {
<addaction name="action_rtl_sdr_com"/> <addaction name="action_rtl_sdr_com"/>
<addaction name="action_github"/> <addaction name="action_github"/>
</widget> </widget>
<widget class="QMenu" name="settings_menu">
<property name="title">
<string>Settings</string>
</property>
<addaction name="action_font"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuUpdates"/> <addaction name="menuUpdates"/>
<addaction name="menu_themes"/>
<addaction name="menuSigidwiki"/> <addaction name="menuSigidwiki"/>
<addaction name="settings_menu"/>
<addaction name="menuAbout"/> <addaction name="menuAbout"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"> <widget class="QStatusBar" name="statusbar">
@@ -9598,6 +9617,21 @@ QSlider::handle:horizontal {
<string>Check software version</string> <string>Check software version</string>
</property> </property>
</action> </action>
<action name="action_themes">
<property name="text">
<string>Themes</string>
</property>
</action>
<action name="action_font">
<property name="text">
<string>Font...</string>
</property>
</action>
<action name="actionThemes">
<property name="text">
<string>Themes</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
@@ -9636,6 +9670,11 @@ QSlider::handle:horizontal {
<extends>QLabel</extends> <extends>QLabel</extends>
<header>switchable_label.h</header> <header>switchable_label.h</header>
</customwidget> </customwidget>
<customwidget>
<class>UrlButton</class>
<extends>QPushButton</extends>
<header>urlbutton.h</header>
</customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="default_imgs.qrc"/> <include location="default_imgs.qrc"/>

View File

@@ -12,6 +12,7 @@ class SupportedOs:
WINDOWS = "windows" WINDOWS = "windows"
LINUX = "linux" LINUX = "linux"
MAC = "mac" MAC = "mac"
RASPBIAN = "raspberry"
class Ftype: class Ftype:
@@ -113,8 +114,8 @@ class Constants:
UPDATING_STR = "Updating..." UPDATING_STR = "Updating..."
ACF_DOCS = "https://aresvalley.com/documentation/" ACF_DOCS = "https://aresvalley.com/documentation/"
FORECAST_PROBABILITIES = "https://services.swpc.noaa.gov/text/sgarf.txt" FORECAST_PROBABILITIES = "https://services.swpc.noaa.gov/text/sgarf.txt"
SPACE_WEATHER_XRAY = "https://services.swpc.noaa.gov/text/goes-xray-flux-primary.txt" SPACE_WEATHER_XRAY = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json"
SPACE_WEATHER_PROT_EL = "https://services.swpc.noaa.gov/text/goes-particle-flux-primary.txt" SPACE_WEATHER_PROT_EL = "https://services.swpc.noaa.gov/json/goes/primary/integral-protons-1-day.json"
SPACE_WEATHER_AK_INDEX = "https://services.swpc.noaa.gov/text/wwv.txt" SPACE_WEATHER_AK_INDEX = "https://services.swpc.noaa.gov/text/wwv.txt"
SPACE_WEATHER_SGAS = "https://services.swpc.noaa.gov/text/sgas.txt" SPACE_WEATHER_SGAS = "https://services.swpc.noaa.gov/text/sgas.txt"
SPACE_WEATHER_GEO_STORM = "https://services.swpc.noaa.gov/text/3-day-forecast.txt" SPACE_WEATHER_GEO_STORM = "https://services.swpc.noaa.gov/text/3-day-forecast.txt"
@@ -181,6 +182,8 @@ class Constants:
DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics") DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics")
DEFAULT_NOT_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED) DEFAULT_NOT_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED)
DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE) DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE)
FONT_FILE = os.path.join(__BASE_FOLDER__, 'font.json')
SETTINGS_FILE = os.path.join(__BASE_FOLDER__, "settings.json")
class Messages: class Messages:
@@ -207,6 +210,8 @@ class Messages:
NEW_VERSION_AVAILABLE = "New software version" NEW_VERSION_AVAILABLE = "New software version"
NEW_VERSION_MSG = lambda v: f"The software version {v} is available." # noqa: E731 NEW_VERSION_MSG = lambda v: f"The software version {v} is available." # noqa: E731
DOWNLOAD_SUGG_MSG = "Download new version now?" DOWNLOAD_SUGG_MSG = "Download new version now?"
SCREEN_UPDATE_FAIL = "Unable to update the data"
SCREEN_UPDATE_FAIL_MSG = "Downloaded data currupted or invalid"
class ThemeConstants: class ThemeConstants:
@@ -215,7 +220,6 @@ class ThemeConstants:
EXTENSION = ".qss" EXTENSION = ".qss"
ICONS_FOLDER = "icons" ICONS_FOLDER = "icons"
DEFAULT = "dark" DEFAULT = "dark"
CURRENT = "__current_theme"
COLORS = "colors.txt" COLORS = "colors.txt"
COLOR_SEPARATOR = "=" COLOR_SEPARATOR = "="
DEFAULT_ACTIVE_COLOR = "#000000" DEFAULT_ACTIVE_COLOR = "#000000"
@@ -231,5 +235,4 @@ class ThemeConstants:
DEFAULT_ICONS_PATH = os.path.join(FOLDER, DEFAULT, ICONS_FOLDER) DEFAULT_ICONS_PATH = os.path.join(FOLDER, DEFAULT, ICONS_FOLDER)
DEFAULT_SEARCH_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.SEARCH_LABEL_IMG) DEFAULT_SEARCH_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.SEARCH_LABEL_IMG)
DEFAULT_VOLUME_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.VOLUME_LABEL_IMG) DEFAULT_VOLUME_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.VOLUME_LABEL_IMG)
CURRENT_THEME_FILE = os.path.join(FOLDER, CURRENT)
DEFAULT_THEME_PATH = os.path.join(FOLDER, DEFAULT) DEFAULT_THEME_PATH = os.path.join(FOLDER, DEFAULT)

View File

@@ -49,6 +49,7 @@ class _TarExtractor:
EXTRACTORS = { EXTRACTORS = {
SupportedOs.WINDOWS: _ZipExtractor, SupportedOs.WINDOWS: _ZipExtractor,
SupportedOs.LINUX: _TarExtractor, SupportedOs.LINUX: _TarExtractor,
SupportedOs.RASPBIAN: _TarExtractor,
# No extractor for MacOs, just download the file through the browser. # No extractor for MacOs, just download the file through the browser.
} }

43
src/loggingconf.py Normal file
View File

@@ -0,0 +1,43 @@
import logging
import logging.config
from constants import __BASE_FOLDER__
import os.path
"""Import the module to initialize the logging configuration.
It is imported only for its side effects."""
_LOGGING_CONFIG = {
'version': 1,
'formatters': {
'general': {
'format': '%(asctime)s::%(levelname)s::%(module)s::%(funcName)s::%(message)s',
'datefmt': '%d/%m/%Y %I:%M:%S %p',
},
},
'handlers': {
'console': {
'level': 'INFO',
'formatter': 'general',
'class': 'logging.StreamHandler',
'stream': 'ext://sys.stdout',
},
'file': {
'class': 'logging.FileHandler',
'level': 'ERROR',
'filename': os.path.join(__BASE_FOLDER__, 'info.log'),
'mode': 'w',
'encoding': 'utf8',
'formatter': 'general',
},
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file'],
},
# Add loggers if required
# 'loggers': {}
}
logging.config.dictConfig(_LOGGING_CONFIG)

View File

@@ -1,4 +1,5 @@
import sys import sys
import platform
from constants import SupportedOs from constants import SupportedOs
@@ -20,6 +21,7 @@ def _is_linux_os():
IS_MAC = _is_mac_os() IS_MAC = _is_mac_os()
IS_LINUX = _is_linux_os() IS_LINUX = _is_linux_os()
IS_WINDOWS = _is_win_os() IS_WINDOWS = _is_win_os()
IS_RASPBIAN = IS_LINUX and 'arm' in platform.machine().lower()
def get_os(): def get_os():
@@ -27,8 +29,10 @@ def get_os():
if IS_WINDOWS: if IS_WINDOWS:
return SupportedOs.WINDOWS return SupportedOs.WINDOWS
elif IS_LINUX: elif IS_LINUX:
if IS_RASPBIAN:
return SupportedOs.RASPBIAN
return SupportedOs.LINUX return SupportedOs.LINUX
elif IS_MAC: elif IS_MAC:
return SupportedOs.MAC return SupportedOs.MAC
else: else:
raise Exception("ERROR: OS not recognized.") return None

43
src/settings.py Normal file
View File

@@ -0,0 +1,43 @@
import os.path
from constants import Constants
import json
import logging
class Settings:
"""Dynamically save and load the settings of the application."""
def __init__(self):
self._dct = {}
def load(self):
"""Load the setiings.json file."""
if not os.path.exists(Constants.SETTINGS_FILE):
return
try:
with open(Constants.SETTINGS_FILE, 'r') as settings_file:
self._dct = json.load(settings_file)
except FileNotFoundError:
logging.info("No settings.json file")
pass # Invalid file.
def save(self, **kwargs):
"""Save the settings.json file.
Also update the current settings specified in kwargs.
New settings can be dynamically added via this method."""
for k, v in kwargs.items():
self._dct[k] = v
with open(Constants.SETTINGS_FILE, mode='w') as settings_file:
json.dump(
self._dct,
settings_file,
sort_keys=True,
indent=4
)
def __getattr__(self, attr):
"""Return the corresponding setting.
Return None if there is no such setting yet."""
return self._dct.get(attr, None)

View File

@@ -1,9 +1,10 @@
import logging
import webbrowser import webbrowser
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from constants import Constants, Messages from constants import Constants, Messages
from switchable_label import SwitchableLabelsIterable from switchable_label import SwitchableLabelsIterable
from weatherdata import SpaceWeatherData from weatherdata import SpaceWeatherData
from utilities import safe_cast, pop_up from utilities import pop_up
class SpaceWeatherManager(QObject): class SpaceWeatherManager(QObject):
@@ -136,13 +137,14 @@ class SpaceWeatherManager(QObject):
""" """
self._owner.update_now_bar.set_idle() self._owner.update_now_bar.set_idle()
if status_ok: if status_ok:
xray_long = safe_cast(self._owner.space_weather_data.xray[-1][7], float) try:
xray_long = float(self._owner.space_weather_data.xray)
def format_text(letter, power): def format_text(letter, power):
return letter + f"{xray_long * 10**power:.1f}" return letter + f"{xray_long * 10**power:.1f}"
if xray_long < 1e-8 and xray_long != -1.00e+05: if xray_long < 1e-8 and xray_long != -1.00e+05:
self._owner.peak_flux_lbl.setText(format_text("<A", 8)) self._owner.peak_flux_lbl.setText("<A0.0")
elif xray_long >= 1e-8 and xray_long < 1e-7: elif xray_long >= 1e-8 and xray_long < 1e-7:
self._owner.peak_flux_lbl.setText(format_text("A", 8)) self._owner.peak_flux_lbl.setText(format_text("A", 8))
elif xray_long >= 1e-7 and xray_long < 1e-6: elif xray_long >= 1e-7 and xray_long < 1e-6:
@@ -171,7 +173,7 @@ class SpaceWeatherManager(QObject):
elif xray_long == -1.00e+05: elif xray_long == -1.00e+05:
self._switchable_r_labels.switch_off_all() self._switchable_r_labels.switch_off_all()
pro10 = safe_cast(self._owner.space_weather_data.prot_el[-1][8], float) pro10 = float(self._owner.space_weather_data.prot_el)
if pro10 < 10 and pro10 != -1.00e+05: if pro10 < 10 and pro10 != -1.00e+05:
self._switchable_s_labels.switch_on(self._owner.s0_now_lbl) self._switchable_s_labels.switch_on(self._owner.s0_now_lbl)
elif pro10 >= 10 and pro10 < 100: elif pro10 >= 10 and pro10 < 100:
@@ -187,18 +189,14 @@ class SpaceWeatherManager(QObject):
elif pro10 == -1.00e+05: elif pro10 == -1.00e+05:
self._switchable_s_labels.switch_off_all() self._switchable_s_labels.switch_off_all()
k_index = safe_cast( k_index = int(self._owner.space_weather_data.ak_index[8][11].replace('.', ''))
self._owner.space_weather_data.ak_index[8][11].replace('.', ''), int
)
self._owner.k_index_lbl.setText(str(k_index)) self._owner.k_index_lbl.setText(str(k_index))
a_index = safe_cast( a_index = int(self._owner.space_weather_data.ak_index[7][7].replace('.', ''))
self._owner.space_weather_data.ak_index[7][7].replace('.', ''), int
)
self._owner.a_index_lbl.setText(str(a_index)) self._owner.a_index_lbl.setText(str(a_index))
if k_index == 0: if k_index == 0:
self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl)
self._k_storm_labels.switch_on(self.k_inactive_lbl) self._k_storm_labels.switch_on(self._owner.k_inactive_lbl)
self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ") self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ")
elif k_index == 1: elif k_index == 1:
self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl)
@@ -252,9 +250,7 @@ class SpaceWeatherManager(QObject):
self._a_storm_labels.switch_on(self._owner.a_sev_storm_lbl) self._a_storm_labels.switch_on(self._owner.a_sev_storm_lbl)
index = self._owner.space_weather_data.geo_storm[6].index("was") + 1 index = self._owner.space_weather_data.geo_storm[6].index("was") + 1
k_index_24_hmax = safe_cast( k_index_24_hmax = int(self._owner.space_weather_data.geo_storm[6][index])
self._owner.space_weather_data.geo_storm[6][index], int
)
if k_index_24_hmax == 0: if k_index_24_hmax == 0:
self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl)
elif k_index_24_hmax == 1: elif k_index_24_hmax == 1:
@@ -276,13 +272,10 @@ class SpaceWeatherManager(QObject):
elif k_index_24_hmax == 9: elif k_index_24_hmax == 9:
self._switchable_g_today_labels.switch_on(self._owner.g5_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g5_today_lbl)
val = safe_cast( val = int(self._owner.space_weather_data.ak_index[7][2].replace('.', ''))
self._owner.space_weather_data.ak_index[7][2].replace('.', ''), int
)
self._owner.sfi_lbl.setText(f"{val}") self._owner.sfi_lbl.setText(f"{val}")
val = safe_cast( val = int(
[x[4] for x in self._owner.space_weather_data.sgas [x[4] for x in self._owner.space_weather_data.sgas if "SSN" in x][0]
if "SSN" in x][0], int
) )
self._owner.sn_lbl.setText(f"{val:d}") self._owner.sn_lbl.setText(f"{val:d}")
@@ -291,6 +284,14 @@ class SpaceWeatherManager(QObject):
label.pixmap = pixmap label.pixmap = pixmap
label.make_transparent() label.make_transparent()
label.apply_pixmap() label.apply_pixmap()
except Exception as e: # This is a mess, so log an error and give up
logging.error(f"Forecast update failure: {e}")
pop_up(
self._owner,
title=Messages.SCREEN_UPDATE_FAIL,
text=Messages.SCREEN_UPDATE_FAIL_MSG
).show()
elif not self._owner.closing: elif not self._owner.closing:
pop_up(self._owner, title=Messages.BAD_DOWNLOAD, pop_up(self._owner, title=Messages.BAD_DOWNLOAD,
text=Messages.BAD_DOWNLOAD_MSG).show() text=Messages.BAD_DOWNLOAD_MSG).show()

View File

@@ -260,8 +260,6 @@ QPushButton {
} }
QPushButton:hover { QPushButton:hover {
border: 2px dashed #4545e5;
border-radius: 13px;
color: #FFFFFF; color: #FFFFFF;
} }

View File

@@ -113,58 +113,60 @@ QSpinBox::down-button:disabled {
} }
QPushButton{ QPushButton{
border-style: outset; /*border-style: outset;
border-width: 2px; border-width: 2px;
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-bottom-color: rgb(58, 58, 58); border-bottom-color: rgb(58, 58, 58);
border-bottom-width: 1px; border-bottom-width: 1px;
border-style: solid; border-style: solid;*/
border: 0px;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
padding: 2px; padding: 2px;
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255)); background-color: transparent;
} }
QPushButton:hover{ QPushButton:hover{
border-style: outset; /*border-style: outset;
border-width: 2px; border-width: 2px;
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
border-bottom-color: rgb(115, 115, 115); border-bottom-color: rgb(115, 115, 115);
border-bottom-width: 1px; border-bottom-width: 1px;
border-style: solid; border-style: solid;*/
color: rgb(255, 255, 255); border: 0px;
color: #AFAFAF;
padding: 2px; padding: 2px;
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(107, 107, 107, 255), stop:1 rgba(157, 157, 157, 255));
} }
QPushButton:pressed{ QPushButton:pressed{
border-style: outset; /*border-style: outset;
border-width: 2px; border-width: 2px;
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255)); border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255));
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-bottom-color: rgb(58, 58, 58); border-bottom-color: rgb(58, 58, 58);
border-bottom-width: 1px; border-bottom-width: 1px;
border-style: solid; border-style: solid;*/
border: 0px;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
padding: 2px; padding: 2px;
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
} }
QPushButton:disabled{ QPushButton:disabled{
border-style: outset; /*border-style: outset;
border-width: 2px; border-width: 2px;
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
border-bottom-color: rgb(58, 58, 58); border-bottom-color: rgb(58, 58, 58);
border-bottom-width: 1px; border-bottom-width: 1px;
border-style: solid; border-style: solid;*/
border: 0px;
color: rgb(0, 0, 0); color: rgb(0, 0, 0);
padding: 2px; padding: 2px;
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(57, 57, 57, 255), stop:1 rgba(77, 77, 77, 255));
} }
QPushButton:checked { QPushButton:checked {
border: 0px;
color: #00ff00; color: #00ff00;
} }
@@ -187,6 +189,10 @@ QRadioButton{
color: #FFFFFF; color: #FFFFFF;
} }
QRadioButton:hover{
color: #AFAFAF;
}
QComboBox { QComboBox {
border: 0px solid transparent; border: 0px solid transparent;
border-radius: 2px; border-radius: 2px;

View File

@@ -102,7 +102,7 @@ class ThemeManager:
self._theme_names = {} self._theme_names = {}
@pyqtSlot() @pyqtSlot()
def _apply(self, theme_path): def _apply(self, theme_path, save=True):
"""Apply the selected theme. """Apply the selected theme.
Refresh all relevant widgets. Refresh all relevant widgets.
@@ -110,7 +110,7 @@ class ThemeManager:
self._theme_path = theme_path self._theme_path = theme_path
if os.path.exists(theme_path): if os.path.exists(theme_path):
if self._theme_path != self._current_theme: if self._theme_path != self._current_theme:
self._change() self._change(save)
self._owner.display_specs( self._owner.display_specs(
item=self._owner.signals_list.currentItem(), item=self._owner.signals_list.currentItem(),
previous_item=None previous_item=None
@@ -140,8 +140,12 @@ class ThemeManager:
Connect all the actions to change the theme. Connect all the actions to change the theme.
Display a QMessageBox if the theme folder is not found.""" Display a QMessageBox if the theme folder is not found."""
themes = [] themes = []
ag = QActionGroup(self._owner, exclusive=True) ag = QActionGroup(self._owner)
if os.path.exists(ThemeConstants.FOLDER): themes_menu = self._owner.settings_menu.addMenu("Themes")
if not os.path.exists(ThemeConstants.FOLDER):
pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
text=ThemeConstants.MISSING_THEME_FOLDER).show()
return
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)): for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)):
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder) relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
if os.path.isdir(os.path.abspath(relative_folder)): if os.path.isdir(os.path.abspath(relative_folder)):
@@ -156,14 +160,11 @@ class ThemeManager:
checkable=True checkable=True
) )
) )
self._owner.menu_themes.addAction(new_theme) themes_menu.addAction(new_theme)
self._theme_names[theme_name.lstrip('&')] = new_theme self._theme_names[theme_name.lstrip('&')] = new_theme
new_theme.triggered.connect(partial(self._apply, theme_path)) new_theme.triggered.connect(partial(self._apply, theme_path))
else:
pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
text=ThemeConstants.MISSING_THEME_FOLDER).show()
def _change(self): def _change(self, save=True):
"""Change the current theme. """Change the current theme.
Apply the stylesheet and set active and inactive colors. Apply the stylesheet and set active and inactive colors.
@@ -266,40 +267,33 @@ class ThemeManager:
ThemeConstants.DEFAULT_TEXT_COLOR ThemeConstants.DEFAULT_TEXT_COLOR
) )
self._current_theme = self._theme_path self._current_theme = self._theme_path
if save:
try: self._owner.settings.save(theme=os.path.basename(self._theme_path))
with open(ThemeConstants.CURRENT_THEME_FILE, "w") as current_theme:
current_theme.write(os.path.basename(self._theme_path))
except Exception:
pass
def apply_default_theme(self): def apply_default_theme(self):
"""Apply the default theme if no theme is set or the theme name is invalid.""" """Apply the default theme if no theme is set or the theme name is invalid."""
try: pretty_name = self._theme_names.get(self._pretty_name(ThemeConstants.DEFAULT), None)
self._theme_names[ if pretty_name is None:
self._pretty_name(ThemeConstants.DEFAULT)
].setChecked(True)
except Exception:
pop_up( pop_up(
self._owner, self._owner,
title=ThemeConstants.THEME_NOT_FOUND, title=ThemeConstants.THEME_NOT_FOUND,
text=ThemeConstants.MISSING_THEME text=ThemeConstants.MISSING_THEME
).show() ).show()
else: else:
pretty_name.setChecked(True)
self._apply(ThemeConstants.DEFAULT_THEME_PATH) self._apply(ThemeConstants.DEFAULT_THEME_PATH)
def start(self): def start(self):
"""Start the theme manager.""" """Start the theme manager."""
self._detect_themes() self._detect_themes()
if os.path.exists(ThemeConstants.CURRENT_THEME_FILE): if self._owner.settings.theme is not None:
with open(ThemeConstants.CURRENT_THEME_FILE, "r") as current_theme_name: theme_path = os.path.join(ThemeConstants.FOLDER, self._owner.settings.theme)
theme_path = os.path.join(ThemeConstants.FOLDER, current_theme_name.read())
theme_name = self._pretty_name(os.path.basename(theme_path)) theme_name = self._pretty_name(os.path.basename(theme_path))
try: theme = self._theme_names.get(theme_name, None)
self._theme_names[theme_name].setChecked(True) if theme is None:
except Exception:
self.apply_default_theme() self.apply_default_theme()
else: else:
self._apply(theme_path) theme.setChecked(True)
self._apply(theme_path, save=False)
else: else:
self.apply_default_theme() self.apply_default_theme()

View File

@@ -7,7 +7,7 @@ from time import perf_counter
import aiohttp import aiohttp
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
from constants import Constants from constants import Constants
from utilities import checksum_ok from utilities import checksum_ok, get_file_extension
from web_utilities import ( from web_utilities import (
get_cacert_file, get_cacert_file,
get_pool_manager, get_pool_manager,
@@ -156,7 +156,7 @@ class DownloadThread(BaseDownloadThread):
"""Verify the checksum of the downloaded data and set the status accordingly.""" """Verify the checksum of the downloaded data and set the status accordingly."""
try: try:
is_checksum_ok = checksum_ok(raw_data, self._target.hash_code) is_checksum_ok = checksum_ok(raw_data, self._target.hash_code)
except Exception: # Invalid hash code. except ValueError: # Invalid hash code.
self.status = ThreadStatus.NO_CONNECTION_ERR self.status = ThreadStatus.NO_CONNECTION_ERR
return True return True
else: else:
@@ -210,10 +210,7 @@ class UpdatesControllerThread(BaseDownloadThread):
# self.status = ThreadStatus.OK # self.status = ThreadStatus.OK
class _AsyncDownloader: async def _download_resource(session, link):
"""Mixin class for asynchronous threads."""
async def _download_resource(self, session, link):
"""Return the content of 'link' as bytes.""" """Return the content of 'link' as bytes."""
ssl_context = ssl.create_default_context( ssl_context = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH, purpose=ssl.Purpose.SERVER_AUTH,
@@ -223,7 +220,7 @@ class _AsyncDownloader:
return await resp.read() return await resp.read()
class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader): class UpdateSpaceWeatherThread(BaseDownloadThread):
"""Subclass BaseDownloadThread. Download the space weather data.""" """Subclass BaseDownloadThread. Download the space weather data."""
_PROPERTIES = ("xray", "prot_el", "ak_index", "sgas", "geo_storm") _PROPERTIES = ("xray", "prot_el", "ak_index", "sgas", "geo_storm")
@@ -236,14 +233,12 @@ class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
async def _download_property(self, session, property_name): async def _download_property(self, session, property_name):
"""Download the data conteining the information of a specific property.""" """Download the data conteining the information of a specific property."""
link = getattr(Constants, "SPACE_WEATHER_" + property_name.upper()) link = getattr(Constants, "SPACE_WEATHER_" + property_name.upper())
data = await self._download_resource(session, link) data = await _download_resource(session, link)
setattr(self._space_weather_data, property_name, str(data, 'utf-8')) self._space_weather_data.set_property(property_name, data, get_file_extension(link))
async def _download_image(self, session, n): async def _download_image(self, session, n):
"""Download the data corresponding the n-th image displayed in the screen.""" """Download the data corresponding the n-th image displayed in the screen."""
im = await self._download_resource( im = await _download_resource(session, Constants.SPACE_WEATHER_IMGS[n])
session, Constants.SPACE_WEATHER_IMGS[n]
)
self._space_weather_data.images[n].loadFromData(im) self._space_weather_data.images[n].loadFromData(im)
async def _download_resources(self): async def _download_resources(self):
@@ -278,7 +273,7 @@ class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
asyncio.run(self._download_resources()) asyncio.run(self._download_resources())
class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader): class UpdateForecastThread(BaseDownloadThread):
"""Subclass BaseDownloadThread. Download the forecast data.""" """Subclass BaseDownloadThread. Download the forecast data."""
class _PropertyName(Enum): class _PropertyName(Enum):
@@ -293,7 +288,7 @@ class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader):
async def _download_property(self, session, link, prop_name): async def _download_property(self, session, link, prop_name):
"""Download the data from 'link' and set the corresponding property of the owner.""" """Download the data from 'link' and set the corresponding property of the owner."""
resp = await self._download_resource(session, link) resp = await _download_resource(session, link)
resp = str(resp, 'utf-8') resp = str(resp, 'utf-8')
if prop_name is self._PropertyName.FORECAST: if prop_name is self._PropertyName.FORECAST:
self.owner.forecast = resp self.owner.forecast = resp

View File

@@ -1,3 +1,4 @@
import logging
import subprocess as sp import subprocess as sp
import webbrowser import webbrowser
from PyQt5.QtCore import QObject, pyqtSlot, QProcess from PyQt5.QtCore import QObject, pyqtSlot, QProcess
@@ -99,6 +100,7 @@ class UpdatesController(QObject):
try: try:
updater.startDetached(command) updater.startDetached(command)
except BaseException: except BaseException:
logging.error("Unable to start updater")
pass pass
else: else:
qApp.quit() qApp.quit()
@@ -110,7 +112,7 @@ class UpdatesController(QObject):
If so, ask to download the new version. If so, ask to download the new version.
If the software is not a compiled version, the function is a NOP.""" If the software is not a compiled version, the function is a NOP."""
if not IS_BINARY or IS_MAC: if not IS_BINARY or IS_MAC:
return return None
latest_updater_version = self.version_controller.updater.version latest_updater_version = self.version_controller.updater.version
try: try:
with sp.Popen( with sp.Popen(
@@ -122,9 +124,10 @@ class UpdatesController(QObject):
) as proc: ) as proc:
updater_version = proc.stdout.read().rstrip("\r\n") # Strip any possible newline, to be sure. updater_version = proc.stdout.read().rstrip("\r\n") # Strip any possible newline, to be sure.
except Exception: except Exception:
logging.error("Unable to query the updater")
updater_version = latest_updater_version updater_version = latest_updater_version
if latest_updater_version is None: if latest_updater_version is None:
return return None
if updater_version != latest_updater_version: if updater_version != latest_updater_version:
answer = pop_up( answer = pop_up(
self._owner, self._owner,

80
src/urlbutton.py Normal file
View File

@@ -0,0 +1,80 @@
from PyQt5.QtWidgets import QPushButton
from collections import namedtuple
from enum import Enum, auto
class UrlButton(QPushButton):
"""Define the behaviour of the wiki button."""
class State(Enum):
"""Possible states of the button."""
ACTIVE = auto()
INACTIVE = auto()
CLICKED = auto()
_UrlColors = namedtuple(
"UrlColors",
[
"INACTIVE",
"ACTIVE",
"CLICKED",
"ACTIVE_HOVER",
"CLICKED_HOVER",
]
)
_COLORS = _UrlColors(
"#9f9f9f",
"#4c75ff",
"#942ccc",
"#808FFF",
"#DE78FF",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def set_enabled(self, state):
"""Enable the button and set the stylesheet."""
super().setEnabled(True)
if state is self.State.ACTIVE:
color = self._COLORS.ACTIVE
else:
color = self._COLORS.CLICKED
self.setStyleSheet(f"""
QPushButton {{
border: 0px;
background-color: transparent;
color: {color};
}}
QPushButton::hover {{
border: 0px;
background-color: transparent;
color: {self._COLORS.ACTIVE_HOVER};
}}
""")
def set_disabled(self):
"""Disable the button and set the stylesheet."""
super().setEnabled(False)
self.setStyleSheet(f"""
QPushButton:disabled {{
border: 0px;
background-color: transparent;
color: {self._COLORS.INACTIVE};
}}
""")
def set_clicked(self):
"""Apply the stylesheet for the clicked state."""
self.setStyleSheet(f"""
QPushButton {{
border: 0px;
background-color: transparent;
color: {self._COLORS.CLICKED};
}}
QPushButton::hover {{
border: 0px;
background-color: transparent;
color: {self._COLORS.CLICKED_HOVER};
}}
""")

View File

@@ -1,3 +1,4 @@
import logging
from functools import partial from functools import partial
import hashlib import hashlib
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
@@ -11,20 +12,40 @@ class UniqueMessageBox(QMessageBox):
If another instance is the the exec loop, calling exec simply return None.""" If another instance is the the exec loop, calling exec simply return None."""
_open_message = False _open_message = False
_font = None
@classmethod
def set_font(cls, font):
"""Store the font for all UniqueMessageBox(es)."""
cls._font = font
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def setFont(self, font):
"""Extends QMessageBox.setFont. Apply the font only if it is not None."""
if font is not None:
super().setFont(font)
def exec(self): def exec(self):
"""Overrides QMessageBox.exec. Call the parent method if there are no """Overrides QMessageBox.exec. Call the parent method if there are no
other instances executing exec. Otherwise return None,""" other instances executing exec; also set the current font.
if UniqueMessageBox._open_message: Otherwise return None,"""
if self.__class__._open_message:
return None return None
UniqueMessageBox._open_message = True self.setFont(self._font)
self.__class__._open_message = True
answer = super().exec() answer = super().exec()
UniqueMessageBox._open_message = False self.__class__._open_message = False
return answer return answer
def show(self):
"""Extends QMessageBox.show().
Set the font before showing the message."""
self.setFont(self._font)
super().show()
def uncheck_and_emit(button): def uncheck_and_emit(button):
"""Set the button to the unchecked state and emit the clicked signal.""" """Set the button to the unchecked state and emit the clicked signal."""
@@ -87,7 +108,7 @@ def checksum_ok(data, reference_hash_code):
Expects a sha256 code as argument.""" Expects a sha256 code as argument."""
if reference_hash_code is None: if reference_hash_code is None:
raise Exception("ERROR: Invalid hash code.") raise ValueError("ERROR: Invalid hash code.")
code = hashlib.sha256() code = hashlib.sha256()
code.update(data) code.update(data)
return code.hexdigest() == reference_hash_code return code.hexdigest() == reference_hash_code
@@ -172,6 +193,25 @@ def safe_cast(value, cast_type, default=-1):
try: try:
r = cast_type(value) r = cast_type(value)
except Exception: except Exception:
logging.error("Cast type failure")
r = default r = default
finally: finally:
return r return r
def get_file_extension(file):
"""Return the extension of a file. Return None if there is not such property."""
components = file.split('.')
if len(components) > 1:
return components[-1]
return None
def get_value_from_list_of_dicts(iterable, callable_ok, key_value):
"""Return a value from a dict inside a list of dicts.
The iterable is reversed first, then the value corresponding to the key key_value
is returned from the first dict for which callable_ok(dict) returns True"""
for d in reversed(iterable):
if callable_ok(d):
return d[key_value]

View File

@@ -61,16 +61,25 @@ def _download_versions_file():
"size": ... "size": ...
} }
} }
"raspberry": {
"software": {
"version": "...",
"url": "...",
"hash_code": "...",
"size": ...
},
"updater": {
"version": "...",
"url": "...",
"hash_code": "...",
"size": ...
}
}
} }
""" """
try: return json.load(
version_dict = json.load(
BytesIO(download_file(Constants.VERSION_LINK)) BytesIO(download_file(Constants.VERSION_LINK))
)[get_os()] ).get(get_os(), None)
except Exception:
return None
else:
return version_dict
class VersionController: class VersionController:
@@ -80,7 +89,6 @@ class VersionController:
def __init__(self, dct=None): def __init__(self, dct=None):
"""Initialize the dictionary""" """Initialize the dictionary"""
super().__init__()
self._dct = dct self._dct = dct
def __getattr__(self, attr): def __getattr__(self, attr):
@@ -89,11 +97,9 @@ class VersionController:
if self._dct is None: if self._dct is None:
if not self.update(): if not self.update():
return None return None
try: dct_element = self._dct.get(attr, None)
dct_element = self._dct[attr] if dct_element is None:
except Exception("ERROR: Invalid attribute!"):
return None return None
else:
if isinstance(dct_element, dict): if isinstance(dct_element, dict):
setattr(self, attr, type(self)(dct_element)) setattr(self, attr, type(self)(dct_element))
else: else:

View File

@@ -1,3 +1,5 @@
import logging
import json
import re import re
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
@@ -9,7 +11,7 @@ from threads import (
) )
from constants import Constants from constants import Constants
from switchable_label import MultiColorSwitchableLabel from switchable_label import MultiColorSwitchableLabel
from utilities import safe_cast from utilities import safe_cast, get_value_from_list_of_dicts
class _BaseWeatherData(QObject): class _BaseWeatherData(QObject):
@@ -89,12 +91,35 @@ class SpaceWeatherData(_BaseWeatherData):
"""Override _BaseWeatherData._parse_data. """Override _BaseWeatherData._parse_data.
Set all the data.""" Set all the data."""
self.xray = self._double_split(self.xray) if self.xray is not None:
self.prot_el = self._double_split(self.prot_el) self.xray = get_value_from_list_of_dicts(
self.xray,
lambda d: d["energy"] == "0.1-0.8nm",
"flux"
)
if self.prot_el is not None:
self.prot_el = get_value_from_list_of_dicts(
self.prot_el,
lambda d: d["energy"] == ">=10 MeV",
"flux"
)
if self.ak_index is not None:
self.ak_index = self._double_split(self.ak_index) self.ak_index = self._double_split(self.ak_index)
if self.sgas is not None:
self.sgas = self._double_split(self.sgas) self.sgas = self._double_split(self.sgas)
if self.geo_storm is not None:
self.geo_storm = self._double_split(self.geo_storm) self.geo_storm = self._double_split(self.geo_storm)
def set_property(self, property_name, data, extension):
"""Set a property to the object. Format the data based on the extension."""
if extension == 'txt':
setattr(self, property_name, str(data, 'utf-8'))
elif extension == 'json':
setattr(self, property_name, json.loads(data))
else:
logging.error("Invalid file extension")
setattr(self, property_name, None)
def remove_data(self): def remove_data(self):
"""Remove the reference to all the data.""" """Remove the reference to all the data."""
self.xray = '' self.xray = ''
@@ -347,6 +372,7 @@ class ForecastData(_BaseWeatherData):
self._set_dates(forecast, rows["solar_row"]) self._set_dates(forecast, rows["solar_row"])
self._set_labels_values(labels_table) self._set_labels_values(labels_table)
except Exception: except Exception:
logging.error("Update ForecastData failure")
pass pass
def remove_data(self): def remove_data(self):

View File

@@ -1,3 +1,4 @@
import logging
import os import os
import sys import sys
import urllib3 import urllib3
@@ -37,15 +38,21 @@ def _download_multiline_file_as_list(url=Database.LINK_REF):
The downloaded file is a csv file with columns (last version == last line): The downloaded file is a csv file with columns (last version == last line):
data.zip_SHA256 | db.csv_SHA256 | Version | Creation_date""" data.zip_SHA256 | db.csv_SHA256 | Version | Creation_date"""
try: try:
f = download_file(url, encoding="UTF-8").splitlines()[-1].split(Database.DELIMITER) return download_file(url, encoding="UTF-8").splitlines()[-1].split(Database.DELIMITER)
except Exception: except Exception:
logging.error("Database metadata download failure")
return None return None
return f
def get_folder_hash_code(): def get_folder_hash_code():
return _download_multiline_file_as_list()[0] f = _download_multiline_file_as_list()
if f is not None:
return f[0]
return None
def get_db_hash_code(): def get_db_hash_code():
return _download_multiline_file_as_list()[1] f = _download_multiline_file_as_list()
if f is not None:
return f[1]
return None