33 Commits

Author SHA1 Message Date
AresValley
d2e0ece9eb Linux and MacOS autopack actions 2022-10-01 19:40:06 +02:00
AresValley
d1716621b0 Auto packaging script for Linux OS 2022-10-01 12:30:32 +02:00
AresValley
cd5d31b404 Updated changelog 2022-09-30 22:37:22 +02:00
AresValley
5a1db2c0d4 Updated version info 2022-09-30 22:33:25 +02:00
AresValley
e4fdf1d564 Changed python version (3.10->3.9) for packaging 2022-09-30 22:10:12 +02:00
AresValley
3e02dd7bfa Updated changelog and version 2022-09-29 18:12:56 +02:00
AresValley
0e20fcbc71 Auto packaging for windows with GitHub actions 2022-09-29 18:02:38 +02:00
Marco Dalla Tiezza
91099940ff Merge pull request #34 from pinkavaj/pi-fix-play
Fix crash for playing audio
2022-09-29 17:58:29 +02:00
Jiří Pinkava
490d9cf3e6 Fix crash for playing audio
pygame 2.1.2 (SDL 2.24.0, Python 3.10.7
  File "audio_player.py", line 122, in _set_max_progress_bar
    self._audio_progress.setMaximum(
TypeError: setMaximum(self, int): argument 1 has unexpected type 'float'
2022-09-27 23:39:24 +02:00
Marco Dalla Tiezza
c84b0d2963 Update README.md 2022-07-29 22:15:57 +02:00
Marco Dalla Tiezza
e69f6a2bee Minor fix, updated dependencies 2022-07-28 23:56:07 +02:00
Alessandro
c16de1f1eb Advance version in artemis.py 2020-05-19 14:55:49 +02:00
Alessandro
bf3efd11a4 Merge branch 'master' of https://github.com/AresValley/Artemis 2020-05-19 14:54:25 +02:00
Alessandro
4a26ac58d0 Fix #23 Revert a bug introduced with ab32fbbf98
Crash if checking for updates without an internet connection
2020-05-19 14:54:02 +02:00
Marco Dalla Tiezza
26f329740d Update README.md for RPi 2020-05-11 12:31:18 +02:00
Marco Dalla Tiezza
0825b8f7d1 Merge pull request #22 from WheezyE/patch-1
raspbian_build.sh
2020-05-11 12:10:00 +02:00
WheezyE
896fef5f2d raspbian_build.sh
This script will automatically install Artemis' build requirements (pip modules) and then build Artemis.
The script accomplishes this by: Detecting which Raspbian operating system is being used (Buster or Stretch), installing PyEnv on the system (if PyEnv isn't already installed), installing a Python v3.7.0 virtual environment inside of PyEnv (so that Artemis' pip modules do not conflict with Raspbian's System Python), installing pip modules (Artemis requirements) within virtual Python 3.7.0 (modules are specific to Buster or Stretch), then building Artemis (using build.sh).  If the script is re-run, it will skip over parts it has already installed. 
 Options are included to clean up after the script is run.
This script takes 14 hours on a Pi 0W, but much less time on multi-core Pi's.
2020-05-10 21:17:08 -07:00
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
29 changed files with 1310 additions and 306 deletions

64
.github/workflows/linux-packaging.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Auto Packaging - Linux
on:
workflow_dispatch:
jobs:
linux-packaging:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Use Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: pip install -r ./requirements/requirements.txt
- name: Install PyInstaller
run: |
pip install pyinstaller
- name: Build Artemis main executable
run: |
cd ./spec_files/Linux
mkdir output
mkdir output/artemis
pyinstaller Artemis.spec
mv -v ./dist/Artemis ./output/Artemis
rm -rfv dist build
pyinstaller updater.spec
mv -v ./dist/_ArtemisUpdater ./output/_ArtemisUpdater
rm -rfv dist build
echo "Create single archives"
cd output
cp -r ../../../src/themes artemis/themes
rm -f artemis/themes/__current_theme
cp Artemis artemis/Artemis
cp _ArtemisUpdater artemis/_ArtemisUpdater
tar -czvf Artemis_linux.tar.gz Artemis -C artemis themes
tar -czvf _ArtemisUpdater_linux.tar.gz ./_ArtemisUpdater
echo "Create full archive for website"
cp ../artemis3.svg artemis
cp ../create_shortcut.sh artemis
tar -czvf ArtemisWebDownlaod_linux.tar.gz artemis
echo "Get size and sha256"
python ../../__get_hash_code.py Artemis_linux.tar.gz _ArtemisUpdater_linux.tar.gz ArtemisWebDownlaod_linux.tar.gz > checksum.txt
- uses: actions/upload-artifact@v3
with:
name: Artemis_Linux
path: |
./spec_files/Linux/output/Artemis_linux.tar.gz
./spec_files/Linux/output/_ArtemisUpdater_linux.tar.gz
./spec_files/Linux/output/ArtemisWebDownlaod_linux.tar.gz
./spec_files/Linux/output/checksum.txt

34
.github/workflows/macOS-packaging.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Auto Packaging - MacOS
on:
workflow_dispatch:
jobs:
macos-packaging:
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- name: Use Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: pip install -r ./requirements/requirements.txt
- name: Install PyInstaller
run: |
pip install pyinstaller
- name: Build Artemis main executable
run: |
cd ./spec_files/macOS
pyinstaller Artemis.spec
ls -lart
- uses: actions/upload-artifact@v3
with:
name: Artemis_MacOS
path: |
./spec_files/macOS/dist

79
.github/workflows/windows-packaging.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Auto Packaging - Windows
on:
workflow_dispatch:
jobs:
windows-packaging:
runs-on: windows-2022
steps:
- uses: actions/checkout@v3
- name: Use Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: pip install -r ./requirements/requirements.txt
- name: Install PyInstaller
run: |
pip install pyinstaller
- name: Build Artemis main executables
run: |
CD spec_files\Windows
ECHO "Building Artemis executable..."
MKDIR output
pyinstaller artemis.spec
MOVE dist\Artemis.exe .\output\Artemis.exe
RMDIR -recurse -force dist
RMDIR -recurse -force build
ECHO "Building updater..."
pyinstaller updater.spec
MOVE dist\_ArtemisUpdater.exe .\output\_ArtemisUpdater.exe
RMDIR -recurse -force dist
RMDIR -recurse -force build
CD output
MKDIR Artemis
XCOPY /y Artemis.exe Artemis\
XCOPY /e /k /y ..\..\..\src\themes Artemis\themes\ /EXCLUDE:..\excluded_files.txt
XCOPY /y _ArtemisUpdater.exe Artemis\
ECHO "Compress files themes+Artemis.exe -> Artemis.zip"
$compress = @{
Path = ".\Artemis.exe", "..\..\..\src\themes"
CompressionLevel = "Optimal"
DestinationPath = ".\Artemis_win.zip"
}
Compress-Archive @compress
$compress = @{
Path = ".\_ArtemisUpdater.exe"
CompressionLevel = "Optimal"
DestinationPath = ".\_ArtemisUpdater_win.zip"
}
Compress-Archive @compress
ECHO "Compress all files for website download"
$compress = @{
Path = "Artemis"
CompressionLevel = "Optimal"
DestinationPath = ".\ArtemisWebsite_win.zip"
}
Compress-Archive @compress
python ..\..\__get_hash_code.py Artemis_win.zip _ArtemisUpdater_win.zip ArtemisWebsite_win.zip > checksum.txt
- uses: actions/upload-artifact@v3
with:
name: Artemis_Windows
path: |
.\spec_files\Windows\output\Artemis_win.zip
.\spec_files\Windows\output\_ArtemisUpdater_win.zip
.\spec_files\Windows\output\ArtemisWebsite_win.zip
.\spec_files\Windows\output\checksum.txt

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

@@ -3,9 +3,41 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
The first release is [3.0.0] because this is actually the third major version (completely rewritten) of the software. The first release is [3.0.0] because this is actually the third major version (completely rewritten) of the software.
## [Unreleased] ## [3.2.4] - 2022-09-30
... ### Fixed
- Fixed crash on opening the Rx/Tx Condition tab
## [3.2.3] - 2022-09-29
### Added
- Add auto-packaging feature using GitHub actions for Windows OS (experimental)
### Fixed
- Fix crash for playing audio ([#34](https://github.com/AresValley/Artemis/pull/34))
## [3.2.2] - 2022-07-29
### Fixed
- Fixed crash on startup or if checking for updates without an internet connection ([#23](https://github.com/AresValley/Artemis/pull/23))
- Updated dependencies for security reasons (urllib3) and to address the main application failure to launch under certain conditions.
## [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 +68,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

@@ -33,8 +33,8 @@ For more information, follow [the main page of Artemis 3](https://aresvalley.com
**Requirements:** **Requirements:**
- Windows 7/8/8.1/10 - Windows 7/8/8.1/10
- Linux: Ubuntu 18.04+, Mint 19+, Fedora 28+ and many other. **You need at least version 2.27 of the GLIBC system library** ([details](https://github.com/AresValley/Artemis/tree/master/spec_files)) - Linux: Ubuntu 20.04+, Mint 20+, Fedora 32+ and many other. **You need at least version 2.31 of the GLIBC system library** ([details](https://github.com/AresValley/Artemis/tree/master/spec_files))
- macOS - macOS 11+ (Big Sur or later)
### Run from source code ### Run from source code
Run the software from the source code with the Python interpreter is the simplest and natural way to run Artemis 3. Run the software from the source code with the Python interpreter is the simplest and natural way to run Artemis 3.
@@ -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-2022) 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

@@ -1,8 +1,8 @@
numpy==1.17.2 numpy>=1.23.1
pandas>=0.24.2 pandas>=1.4.3
certifi>=2019.6.16 certifi>=2022.6.15
aiohttp>=3.5.4 aiohttp>=3.8.1
urllib3==1.24.3 urllib3>=1.26.9
pygame>=1.9.6 pygame>=2.1.2
QtAwesome>=0.6.0 QtAwesome>=1.1.1
PyQt5==5.12.2 PyQt5>=5.15.7

View File

@@ -0,0 +1,412 @@
#!/bin/bash
### Build Artemis for Raspberry Pi (for Raspbian Buster or Raspbian Stretch)
# raspbian_build.sh
# Author: Eric (KI7POL)
# Credits: MarcoDT (testing, trouble-shooting, guidance). Keith (N7ACW) (inspiration). Jason (KM4ACK) (inspiration).
# Version: 0.1 (May 10, 2020)
# Description: Install pre-requisites for building Artemis on Raspberry Pi, then build Artemis from source
clear
echo "======= Build Artemis for the Raspberry Pi ======="
echo " (Raspbian Buster or Stretch)"
echo
echo "This script will help you build distributable Artemis executable binaries for Raspbian."
echo
echo "We will update your Pi, prepare a virtual environment, then make your Artemis binary."
echo " * We will need about 1.3 GB of storage space."
echo " * This may take anywhere from 14 hours to 45 minutes (faster Raspberry Pi's take less time)"
echo " * PyQt5 pip module installation is the longest step, especially if it must be built from source or build its 'wheel'."
echo
read -n 1 -s -r -p "Press any key to continue (more instructions to follow) ..."
clear
# Before building an Artemis executable binary (exe) on any Raspberry Pi computers, we need to install some system software and Artemis' pip requirements. Raspberry Pi also comes with "System Python" pre-installed, which will interfere with our building of a compact Artemis exe. This script will assist with installing system software, a clean virtual Python environment (PyEnv virtualenv), and Artemis requirements (Python pip packages), to build Artemis from. This way, Artemis will be built from a fresh virtual Python 3.7.0 installation apart from Raspbian's System Python installation.
# About 1.3 GB will be needed [size on disk: PyEnv 200 MB, 200 MB virtual build environment, 112 MB of temp files (PyQt5 downloads and build directory, and a 520 MB Artemis output folder after downloading the Artemis signal database]. The output folder will contain three tar.gz archives, two EXE files (Artemis 97-100 MB, depending on Buster or Stretch, and _ArtemisUpdater 49 MB), theme files, audio files, and a database file. The 'artemis' folder or tar.gz files can be distributed to end-users and run on other Raspberry Pi's. Any end-users receiving the EXE files will not need to install Artemis dependencies on their Raspberry Pi's to run the compiled EXE's. The EXE files do not seem to become smaller after running "sudo trim" commands on them.
# Though it is recommended to build Artemis on the oldest operating system version available to avoid 'GLIBC out of date errors' when our Artemis binary is run on newer OS's, we have found that Raspbian Stretch and Buster are sufficiently different that they require their own builds of Artemis to function without throwing error messages in the Linux terminal. Stretch and Buster also have different installation procedures. This script will detect whether you are running Buster or Stretch and then automatically build a correct Artemis binary for your Raspbian distribution.
# In case the PyQt5 pip module is not able to be installed on your version of Raspbian or does not work in the future, we have included the option to let the script build and install the old stable PyQt5 pip module. Just set the User-defined variable PYQTVER below to 'old'. NOTE! the "make" process for building a PyQt5 pip module from source can take over 14 hours on a Raspberry Pi 2B, over 12 hours on a Pi 0W, over 3.5 hours on a Pi 3B+, and about 2 hours on a Pi 4B-2GB. Building PyQt5 from source is only recommended if you must do it. If you can install a new (pre-built) PyQt5 module with pip and the wheel module, this script will take MUCH less time to finish (this is the default mode for this script).
### User-defined variables
PYTHVER='3.7.0' # Specify a version of Python to install into your PyEnv virtual environment (to build Artemis from).
PYQTVER='new' # Options are 'new' and 'old'. Try forcing 'old' if you get installation errors, but old may take 2-14 more hours (the script will try to run 'old' automatically though if 'new' doesn't work)
### Static Variables
TMPVAR1=$(tr -d '\0' </sys/firmware/devicetree/base/model) # Store full name of Pi
TMPVAR2=`grep VERSION_CODENAME /etc/os-release` # Store name of Raspbian operating system version
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # Store current location of this bash script
BUILDENV="artemis3_python"$PYTHVER"pyqt"$PYQTVER
RPIHARDWARE=${TMPVAR1%" Rev"*} # Extract Pi's name before " Rev", so we don't store the Pi's revision name.
RPIOS=`cut -d "=" -f2- <<< "$TMPVAR2"` # Store 'buster', 'stretch', or something else unknown
TSTART=`date +%s` # Log the start time of the script
if [[ "$RPIOS" = "stretch" ]]; then
echo
echo "Your operating system version is Raspbian Stretch. Since you are building Artemis on Stretch, your Artemis binary should work great on Stretch, but if you try to run it on Raspbian Buster it will throw minor font and GLIB errors in the console."
elif [[ "$RPIOS" = "buster" ]]; then
echo
echo "Your operating system version is Raspbian Buster. Since you are building Artemis on Buster, your Artemis binary will work great on Buster, but will not run on Raspbian Stretch due to GLIBC out of date errors."
else
echo
echo "WARNING: This script was not able to determine your operating system version (you are not running Raspbian Buster or Raspbian Stretch). We will attempt to prepare the requirements for building Artemis, but we will likely encounter errors with the preparation or build."
fi
echo
echo "For trouble-shooting, please read the comments inside this script. You can also open an issue in the Ares Valley / Artemis github or ask for assistance on the forums. Please include a link to a pastebin with the contents of your raspbian_build_debug.log file"
echo
read -n 1 -s -r -p "Press any key to begin ..."
clear
echo "We will now update your system."
echo "This may take a while if you haven't updated recently."
echo
exec > >(tee "$DIR/raspbian_build_debug.log") 2>&1 # Make a log of this script's output
sudo apt-get update -y
sudo apt-get upgrade -y
######################################### Install PyEnv #########################################
### Install pyenv so we can build Artemis from a fresh virtual Python (apart from Raspbian's System Python)
export PYENV_ROOT="$HOME/.pyenv" # Needed to help this if statement find PyEnv if it's already installed.
export PATH="$PYENV_ROOT/bin:$PATH" # Both of these exports are also needed later in this script.
if hash pyenv 2>/dev/null; then
echo "Pyenv is already installed, skipping pyenv installation..." >&2
else
echo "Installing pyenv now..." >&2
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev \
libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-openssl
curl https://pyenv.run | bash
# Pyenv needs to be initialized whenever SHELL is loaded, so we need to add these values to .bashrc
sudo echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
sudo echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
sudo echo 'eval "$(pyenv init -)"' >> ~/.bashrc
sudo echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
fi
# Initialize pyenv so this instance of SHELL can find PyEnv (so we don't have to restart SHELL).
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
######################################### Install Python 3.7.x Inside Pyenv #########################################
### We will install our Artemis requirements pip modules inside of this virtualenv.
# Check to see if Python 3.7.x is already installed in pyenv. If so, skip Python 3.7.x installation.
if ! [ -d "/$HOME/.pyenv/versions/$PYTHVER/" ]; then
echo "Installing Python" $PYTHVER "within Pyenv now..." >&2
env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install -v $PYTHVER
else
echo "Python" $PYTHVER "is already installed within Pyenv, skipping Python installation..." >&2
fi
pyenv global $PYTHVER # Make your python 3.7.0 environment the default whenever "python" is typed (instead of System python)
### Create our new python virtual environment using Python 3.7.x (we will activate it as soon as we need it)
# Check to see if we've already made a Python 3.7.x virtual environment named (for example) 'artemis3_py3.7.0_pyqtnew'
if ! [ -d "/$HOME/.pyenv/versions/$PYTHVER/envs/$BUILDENV" ]; then
echo 'Creating a Python' $PYTHVER 'virtual environment named' $BUILDENV '...' >&2
pyenv virtualenv $PYTHVER $BUILDENV
else
echo 'A virtual environment with Python' $PYTHVER 'named' $BUILDENV 'was found. We will install pip modules here...' >&2
fi
###################################### Install Artemis Requirements Into Our Pyenv ######################################
### When building Artemis for the Raspberry Pi, we must install our Artemis requirements ourselves - without the aid of the 'requirements.txt' file in this Git (though other Linux distros might be able to still use it). This is because the Raspbian pip distro doesn't include the recommended 5.12.2 version of PyQt5, as requested by 'requirements.txt'. Therefore, if we try to install requirements using the recommended "python3.7 -m pip install -r cd ~/Artemis/requirements/requirements.txt" we will error when trying to download PyQt5 and auto-abort before we get to install ANY of the required pip modules.
### Note that the commands below downloads the pip modules specified in requirements.txt (including building our own PyQt5 5.12.2 from source) and then installs them within our fresh virtual python 3.7.0 environment (thereby avoiding any interference with Raspbian's default System python installation). If you try to install these modules using "sudo python3.7" or "sudo python", however, you will accidentally be installing them into System python (and not into our pristine pyenv virtualenv python 3.7.0 environment).
pyenv activate $BUILDENV # Activate the new Python 3.7.x virtualenv so we can start installing python pip modules in it
sudo apt-get install libatlas-base-dev -y # Needed for numpy (on Buster?)
sudo apt-get install libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsdl1.2-dev libsmpeg-dev subversion libportmidi-dev ffmpeg libswscale-dev libavformat-dev libavcodec-dev -y # Needed for pygame on Raspbian Stretch? (not needed for Buster)
sudo apt-get install qt5-default -y # Ensure Qt5 is installed (doesn't come pre-install on Buster for Pi 0-2)
python3.7 -m pip install --upgrade pip wheel # Necessary for Stretch installations of pandas and numpy
if [[ "$RPIOS" = "stretch" ]]; then
python3.7 -m pip install --upgrade 'setuptools==44.0.0' # This specific version needed for NumPy on Stretch
python3.7 -m pip install 'numpy==1.17.2' --no-cache-dir --no-binary :all: # Needed for Stretch but takes longer
else
python3.7 -m pip install 'numpy==1.17.2' # Works on Buster
fi
python3.7 -m pip install 'pandas>=0.24.2'
python3.7 -m pip install 'certifi>=2019.6.16'
python3.7 -m pip install 'aiohttp>=3.5.4'
python3.7 -m pip install 'urllib3==1.24.3'
python3.7 -m pip install 'pygame>=1.9.6'
python3.7 -m pip install 'QtAwesome>=0.6.0'
if [[ "$PYQTVER" = "new" ]]; then
python3.7 -m pip install PyQt5-sip
echo
echo "WARNING: If PyQt5 needs to build its own wheel from scratch on your system then this step may take anywhere from 2-14 hours (depending on your Raspberry Pi's hardware)."
echo
python3.7 -m pip install PyQt5
fi
# Find out if PyQt5 needs to be installed (useful if we already installed it in a previous run of this script, or if the newer PyQt5 version failed to install)
pip list | grep 'PyQt5 ' &> /dev/null
if [ $? == 0 ]; then
echo "PyQt5 is already installed in this Pyenv."
else
echo "Downloading and installing PyQt5 and sip into your Pyenv virtual environment"
mkdir $DIR/Downloads
cd $DIR/Downloads
wget https://www.riverbankcomputing.com/static/Downloads/sip/4.19.17/sip-4.19.17.tar.gz # Old PyQt5 requires sip
wget https://www.riverbankcomputing.com/static/Downloads/PyQt5/5.12.2/PyQt5_gpl-5.12.2.tar.gz
tar xvfz sip-4.19.17.tar.gz # I chose sip 4.19.17 since it was released the same day as PyQt5 5.12.2.
tar xvfz PyQt5_gpl-5.12.2.tar.gz
cd $DIR/Downloads
cd sip-4.19.17
python configure.py --sip-module PyQt5.sip # Pip will list the sip module as "PyQt5-sip", module file is "sip.*so"
if [[ "$RPIHARDWARE" = "Raspberry Pi 3 Model A+" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 2 Model B" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 3 Model B" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 3 Model B+" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 4 Model B" ]]; then # Only run -j4 on Raspberry Pi computers with four cores
make -j4
else
make
fi
make install
cd $DIR/Downloads
cd PyQt5_gpl-5.12.2
python configure.py --confirm-license --sip-incdir="$DIR/Downloads/sip-4.19.17/siplib"
if [[ "$RPIHARDWARE" = "Raspberry Pi 3 Model A+" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 2 Model B" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 3 Model B" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 3 Model B+" ]] || [[ "$RPIHARDWARE" = "Raspberry Pi 4 Model B" ]]; then # Only run -j4 on Raspberry Pi computers with four cores
echo
echo "WARNING: This step may take anywhere from 1-14 hours, depending on your Raspberry Pi's hardware."
echo
make -j4
else
make
fi
make install
fi
# Stretch needs Pyinstaller 3.4 to build Artemis correctly (otherwise sip errors on run). Buster isn't picky.
if [[ "$RPIOS" = "stretch" ]]; then
cd ~/Downloads
wget https://github.com/pyinstaller/pyinstaller/releases/download/v3.4/PyInstaller-3.4.tar.gz
tar -xvf PyInstaller-3.4.tar.gz
cd PyInstaller-3.4/bootloader/
sudo python ./waf configure --no-lsb all
cd ..
python3.7 setup.py install
else
python3.7 -m pip install pyinstaller
fi
eval "$(pyenv init -)" # We need to remind SHELL that we just installed pyinstaller exe into our pyenv
eval "$(pyenv virtualenv-init -)"
######################################### Build Artemis From Our Pyenv #########################################
cd $DIR # Change the shell's working directory to the same location as our .spec files. This is a critical step before compiling! (This should put us into ...'/Artemis/spec_files/Linux/', where 'build.sh' and 'Artemis.spec' are located.)
sudo chmod +x build.sh
./build.sh # The build script builds two Artemis exe's, copies the themes and shortcut-maker files into the output directory, packages everything, and then runs hashes.
sudo chmod -R 755 output/artemis
sudo rm output/Artemis output/_ArtemisUpdater # We'll just remove these redundant output folders so users don't get confused
TEND=`date +%s` # Log the end time of the script
TTOTAL=$((TEND-TSTART))
echo '(Script completed in' $TTOTAL 'seconds)' # Report how long it took to install requirements and build Artemis
######################################### Clean Up #########################################
pyenv deactivate # The virtualenv may also be deactivated when we close the terminal window
pyenv global system # Set the active Python version back to System Python (instead of PyEnv Python 3.7.x)
echo
read -p "Would you like to remove the build pre-requisites we installed? (y/n) `echo $'\n '`(Removing these files will free up about 200 MB, but keeping the files will make re-running this script take much less time. We will not delete PyEnv which is another 200 MB, but you can delete its folder manually to remove it if you like.) `echo $'\n> '`" REMOVEFILES
if [ $REMOVEFILES = "y" ] || [ $REMOVEFILES = "Y" ]; then
sudo rm -rf /home/pi/.pyenv/versions/$BUILDENV
sudo rm -rf /home/pi/.pyenv/versions/$PYTHVER/envs/$BUILDENV
fi
######################################### Notes #########################################
### Run Artemis from a shortcut, from File Manager, or with the './Artemis' command in the terminal. Follow the on-screen prompts to download the database and audio files.
### Other Resources
# Python Virtual Environments https://www.youtube.com/watch?v=N5vscPTWKOk
# Python VirtEnv reduces built EXE file sizes https://stackoverflow.com/questions/47692213/reducing-size-of-pyinstaller-exe
# Setuptools error: "ModuleNotFoundError: No module named 'pkg_resources.py2_warn'" https://github.com/pypa/setuptools/issues/1963
# Trouble-shooting pyinstaller https://github.com/pyinstaller/pyinstaller/wiki/How-to-Report-Bugs#make-sure-everything-is-packaged-correctly
# Using pyinstaller with venv: https://pyinstaller.readthedocs.io/en/stable/development/venv.html
# Learning Pyenv https://realpython.com/intro-to-pyenv/
# Search Debian apt-get repo for missing packages https://www.debian.org/distrib/packages#search_contents
# https://www.cyberciti.biz/faq/howto-check-if-a-directory-exists-in-a-bash-shellscript/
# https://pyinstaller.readthedocs.io/en/stable/development/venv.html
# https://stackoverflow.com/questions/16931244/checking-if-output-of-a-command-contains-a-certain-string-in-a-shell-script
# PyQt5 build instructions: https://www.riverbankcomputing.com/static/Docs/PyQt5/building_with_configure.html
### Future work
# See if PyQt5 install can be sped up with --no-tools and other conditions.
#
### Approximate installation times:
# RPi 4B (2GB) ???. "Raspberry Pi 4 Model B Rev 1.2"
# RPi 3B+ 3.5 hours. "Raspberry Pi 3 Model B Plus Rev 1.3"
# RPi 3B ??? hours. "Raspberry Pi 3 Model B Rev 1.2"
# Rpi 2B 12? hours.
# Rpi 0W 12 hours. "Raspberry Pi Zero W Rev 1.1"
# Find model using 'cat /sys/firmware/devicetree/base/model'
### Other commands
#python -V # You can test which version of python has priority now on your system if you like
#python -m test # You can run python diagnostics if you want to check the integrity of your new python. My test for Python 3.7.0 took 56 minutes on the Pi3B+ and resulted in a "== Tests result: FAILURE == ... 6 tests failed: test_asyncio test_ftplib test_imaplib test_nntplib test_poplib test_ssl" but my Artemis 3.2.0 build still worked.
##################################### Error Messages and Workarounds ##############################
### NOTE!!! Artemis must be built by running "pyinstaller Artemis.spec" WHILE THE TERMINAL'S WORKING DIRECTORY IS INSIDE THE "/Artemis/spec_files/Linux" folder! If you try to build the Artemis.spec while not in the working directory (for example, by being inside "~/Downloads/ArtemisTestBuild/" and running "PyInstaller ~/Downloads/Artemis/spec_files/Linux/Artemis.spec") then Artemis will build ok but then crash when you try to run the Artemis executable: "FileNotFoundError: [Errno 2] No such file or directory: '/tmp/_MEIaHNect/download_db_window.ui'". Said another way, you MUST cd into the "Artemis/spec_files/Linux" directory BEFORE running the "pyinstaller Artemis.spec" command!
### If you get: "[1449] Error loading Python lib '/tmp/_MEIATNM1X/libpython3.7m.so.1.0': dlopen: /lib/arm-linux-gnueabihf/libc.so.6: version `GLIBC_2.28' not found (required by /tmp/_MEIATNM1X/libpython3.7m.so.1.0)" Try updating GLIBC with "sudo apt-get install libc6". If this doesn't work, this error might be due to your running a pre-compiled Artemis binary that was built on a newer Linux OS than the one you are running. If this is the case, then use this build script to build Artemis for your system.
### If you get errors installing newer PyQt5 modules with pip or troubles compiling Artemis like: "ModuleNotFoundError: No module named 'pkg_resources.py2_warn'", you might try upgrading pip, installing a different version of pip setuptools (newer or older), and then reinstalling pyinstaller. That error may show up for setuptools newer than 44.0.0. Therefore, try: "python3.7 -m pip install --upgrade 'setuptools==44.0.0'"
### If you get the error "bash: PyInstaller: command not found" when trying to build Artemis, then run the commands below (or reload shell or just close and re-open the terminal). After that, ensure pyenv is also reinitialized by activating your virtual env again.
#eval "$(pyenv init -)"
#eval "$(pyenv virtualenv-init -)"
#pyenv activate YOUR_BUILD_ENVIRONMENT_HERE
### We need to install python inside of pyenv with '--enable-shared' or else we will get this when trying to build Artemis on Stretch "OSError: Python library not found: libpython3.7m.so, libpython3.7mu.so.1.0, libpython3.7.so.1.0, libpython3.7m.so.1.0"
### If you get "libf77blas.so.3: cannot open shared object file: No such file or directory" during Artemis compile, make sure 'libatlas-base-dev' was installed with apt-get (this error is caused by NumPy).
### NumPy 1.17.2 errors on Stretch if it is not installed with "--no-cache-dir --no-binary :all:". "Original error was: /lib/arm-linux-gnueabihf/libm.so.6: version `GLIBC_2.27' not found (required by /home/pi/.pyenv/versions/artemis3_py370_pyqt5122/lib/python3.7/site-packages/numpy/core/_multiarray_umath.cpython-37m-arm-linux-gnueabihf.so)"
### If we installed our pip modules inside of System Python (ie we didn't run 'pyenv activate YOUR_VIRTUAL_ENV_NAME_HERE'), then we would end up with a final Artemis file that was 1.3GB and which would error when trying to copy itself from build/PKG-00.pkg into its final dist/Artemis EXE, with error message "raise SystemError("objcopy Failure: %s" % stderr) SystemError: objcopy Failure: objcopy: out of memory allocating 536870912 bytes after a total of 0 bytes". However, since we're building a fresh Python environment to install ONLY our needed Artemis modules inside of, we will not get this error and we will build an Artemis EXE that is about 97.1 MB.
### On Raspbian Stretch (not Buster), any Pyinstaller version >= 3.5 will cause PyQt5.sip (sip.so) to be incorrectly installed into the main directory within the Artemis exe (and not into the PyQt5 folder within the exe, where sip should be), so that Artemis will build without errors but will error when run ("ModuleNotFoundError: No module named 'PyQt5.sip'"). Therefore, PyInstaller 3.4 should be used on Raspbian Stretch (earlier PyInstaller versions don't install correctly on Stretch anyway). This error might be fixable on Stretch with a custom-written PyInstaller hook for sip if you need to use a newer PyInstaller version on Raspbian Stretch, but I haven't tested this. Raspbian Buster does not have this problem with PyInstaller and sip for some reason, so that any PyInstaller version should be fine.
### If we try to install PyQt5 (using pip or by building from source) without having Qt5 installed on our system, then we will get errors related to qmake: “Error: Use the --qmake argument to explicitly specify a working Qt qmake. qmake not compatible for building PyQt5” Qt5 doesn't come pre-installed on Raspbian Buster for Raspberry Pi 0-2 (but does on newer Pi's running Raspbian Buster). Install it with "sudo apt-get install qt5-default"
### If you get build errors with Artemis on Raspbian Buster, you might consider a different PyInstaller version built from source - Either the newest development version ("python3.7 -m pip install https://github.com/pyinstaller/pyinstaller/archive/develop.zip"), or an older version ("python3.7 -m pip install --upgrade 'PyInstaller==3.5'").
### If you type ./Artemis in the terminal and Artemis runs, but looks all grey when it runs and gives you a "Missing theme folder" dialog box error, then make sure that, when you are running the ./Artemis command, your terminal's working directory is the same directory that houses the Artemis executable file: For example, while terminal is in the "/opt/" directory, if you run "Artemis/./Artemis" then you'll get the theme folder error, but if you cd to the "/opt/Artemis/" directory and run "./Artemis", then this may fix the theme folder error.
### Similar to the theme folder error, if you run Artemis from terminal while your working directory is outside the folder that houses the Artemis executable file, then downloading the database may create a "Data" folder outside of the folder where the Artemis executable is. Just close Artemis, move the Data folder into the Artemis folder, run the Artemis exe again from a shortcut or from File Manager, or from terminal (but in the correct working directory) and everything should work.
### If you get this error: "QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'", log out of your Pi then log back in. This may be a problem with the EXE getting confused by pyenv if pyenv is still active when the EXE is first run. Pyenv activates automatically by being inside the ~/Environments/artemis3_py370_pyqt5122/ directory.
################################# Working installations and console outputs ########################
### My Raspberry Pi 3B+ build specs (Raspbian Buster)
#python3.7 -O -m PyInstaller --clean Artemis.spec
#323 INFO: PyInstaller: 3.5 # Also successfully tested with Pyinstaller 3.6
#323 INFO: Python: 3.7.0
#328 INFO: Platform: Linux-4.19.97-v7+-armv7l-with-debian-10.3
#335 INFO: UPX is not available.
#pip list
#Package Version
#--------------- ----------
#aiohttp 3.6.2
#altgraph 0.17
#async-timeout 3.0.1
#attrs 19.3.0
#certifi 2020.4.5.1
#chardet 3.0.4
#idna 2.9
#multidict 4.7.5
#numpy 1.17.2
#pandas 1.0.3
#pip 10.0.1
#pygame 1.9.6
#PyInstaller 3.6
#PyQt5 5.12.2
#PyQt5-sip 4.19.17
#python-dateutil 2.8.1
#pytz 2019.3
#QtAwesome 0.7.0
#QtPy 1.9.0
#setuptools 39.0.1
#six 1.14.0
#urllib3 1.24.3
#yarl 1.4.2
### Terminal output on RPi3B+ (Buster) for our build
#spec_files/Linux $ dist/./Artemis
#pygame 1.9.6
#Hello from the pygame community. https://www.pygame.org/contribute.html
#libEGL warning: DRI2: failed to authenticate
#qt5ct: using qt5ct plugin
#qt5ct: D-Bus global menu: no
#[Artemis then launches]
### Terminal output on RPi3B (not plus) (Stretch) for our build
#(artemis3_pyqt5122) pi@raspberrypi:~/Downloads/Artemis/spec_files/Linux/dist $ ./Artemis
#pygame 1.9.6
#Hello from the pygame community. https://www.pygame.org/contribute.html
#libEGL warning: DRI2: failed to authenticate
#[Artemis then launches]
#NOTE: This RPi3B Stretch build will run on Buster, but only will display fonts correctly if we rename our Buster /etc/fonts/ directory, copy the Stretch fonts directory into Buster, and run ./Artemis as sudo. I am investigating whether this may be due to old PyInstaller installing weirdly.
#[on stretch] sudo cp -L -r /etc/fonts /media/pi/ENFAIN/fonts_stretch
#[on buster] sudo mv /etc/fonts /etc/fonts.bak
#[on buster] sudo cp -L -r /media/pi/ENFAIN/fonts_stretch /etc/fonts
#https://askubuntu.com/questions/1098809/ubuntu-18-10-fontconfig
#(artemis3_pyqt5122) pi@raspberrypi:~/Downloads/Artemis/spec_files/Linux/dist $ pip list
#Package Version
#--------------- ----------
#aiohttp 3.6.2
#altgraph 0.17
#async-timeout 3.0.1
#attrs 19.3.0
#certifi 2020.4.5.1
#chardet 3.0.4
#future 0.18.2
#idna 2.9
#macholib 1.14
#multidict 4.7.5
#numpy 1.17.2
#pandas 1.0.3
#pefile 2019.4.18
#pip 20.1
#pygame 1.9.6
#PyInstaller 3.4
#PyQt5 5.12.2
#PyQt5-sip 4.19.17
#python-dateutil 2.8.1
#pytz 2020.1
#QtAwesome 0.7.1
#QtPy 1.9.0
#setuptools 44.0.0
#six 1.14.0
#urllib3 1.24.3
#wheel 0.34.2
#yarl 1.4.2
### Terminal output on RPi4B for our build
#pi@raspberrypi:~/Desktop/Artemis3 Pi3B+ $ ./Artemis
#pygame 1.9.6
#Hello from the pygame community. https://www.pygame.org/contribute.html
#qt5ct: using qt5ct plugin
#qt5ct: D-Bus global menu: no
#[Artemis then launches]
### Terminal output on RPi4B for the official Linux build
#pi@raspberrypi:~/Downloads/artemis $ ./Artemis
#bash: ./Artemis: cannot execute binary file: Exec format error
#pi@raspberrypi:~/Downloads/artemis $ ./_ArtemisUpdater
#bash: ./_ArtemisUpdater: cannot execute binary file: Exec format error
#[Artemis doesn't launch]

View File

@@ -39,7 +39,7 @@ pyinstaller Artemis_onedir.spec
You can save a copy of the executable in a folder of you choice. At startup it will ask you to download the database and also warn you that the `themes` folder is missing. To avoid this, copy `src/Data` and `src/themes` in the folder containing the executable. You can save a copy of the executable in a folder of you choice. At startup it will ask you to download the database and also warn you that the `themes` folder is missing. To avoid this, copy `src/Data` and `src/themes` in the folder containing the executable.
## Build scripts ## Build scripts (Windows, Linux, macOS)
Provided you satisfy the requirements (see [requirements.txt](../requirements/requirements.txt)) and have pyinstaller installed, running a `build.*` script in `<your os>` folder will produce an `output` folder with: Provided you satisfy the requirements (see [requirements.txt](../requirements/requirements.txt)) and have pyinstaller installed, running a `build.*` script in `<your os>` folder will produce an `output` folder with:
- Executable versions of Artemis and the updater; - Executable versions of Artemis and the updater;
@@ -50,3 +50,15 @@ Provided you satisfy the requirements (see [requirements.txt](../requirements/re
At the end of the process the script writes on standard output the size and sha256 code for the compressed files. At the end of the process the script writes on standard output the size and sha256 code for the compressed files.
**NOTE.** For Windows you will need a 7z installation. Also check the path hardcoded in `/Windows/build.bat`. **NOTE.** For Windows you will need a 7z installation. Also check the path hardcoded in `/Windows/build.bat`.
## Build script (Raspberry Pi)
Thanks to [**Eric Wiessner (KI7POL)**](https://github.com/WheezyE "GitHub profile"), an automatic script to compile Artemis (and meet all the Artemis' build requirements) is available in the Linux folder `Linux/raspbian_build.sh`. The script will proceed as follows:
- Detect which Raspbian operating system is being used (Buster or Stretch), installing PyEnv on the system (if PyEnv isn't already installed).
- Installation of a Python v3.7.0 virtual environment inside of PyEnv (so that Artemis' pip modules do not conflict with Raspbian's System Python).
- Installation of pip modules (Artemis requirements) within the previously built virtual Python 3.7.0 (modules are specific to Buster or Stretch).
- Canonical Artemis building for Linux OS (using build.sh).
**The complete instructions, a troubleshooting guide and many other usefull details are available in the script itself.**
If the script is re-run, it will skip over parts it has already installed. Options are included to clean up after the script is run. In the worst case scenario, this script takes 14 hours on a Pi 0W, but much less time on multi-core Pi's.

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.4"
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

@@ -1,3 +1,4 @@
import math
import os import os
from pygame import mixer from pygame import mixer
from PyQt5.QtCore import QTimer, pyqtSlot, QObject from PyQt5.QtCore import QTimer, pyqtSlot, QObject
@@ -120,7 +121,7 @@ class AudioPlayer(QObject):
def _set_max_progress_bar(self): def _set_max_progress_bar(self):
"""Set the maximum value of the progress bar.""" """Set the maximum value of the progress bar."""
self._audio_progress.setMaximum( self._audio_progress.setMaximum(
mixer.Sound(self._audio_file).get_length() * 1000 math.ceil(mixer.Sound(self._audio_file).get_length() * 1000)
) )
def set_audio_player(self, fname=""): def set_audio_player(self, fname=""):

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,161 +137,161 @@ 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:
self._owner.peak_flux_lbl.setText(format_text("B", 7)) self._owner.peak_flux_lbl.setText(format_text("B", 7))
elif xray_long >= 1e-6 and xray_long < 1e-5: elif xray_long >= 1e-6 and xray_long < 1e-5:
self._owner.peak_flux_lbl.setText(format_text("C", 6)) self._owner.peak_flux_lbl.setText(format_text("C", 6))
elif xray_long >= 1e-5 and xray_long < 1e-4: elif xray_long >= 1e-5 and xray_long < 1e-4:
self._owner.peak_flux_lbl.setText(format_text("M", 5)) self._owner.peak_flux_lbl.setText(format_text("M", 5))
elif xray_long >= 1e-4: elif xray_long >= 1e-4:
self._owner.peak_flux_lbl.setText(format_text("X", 4)) self._owner.peak_flux_lbl.setText(format_text("X", 4))
elif xray_long == -1.00e+05: elif xray_long == -1.00e+05:
self._owner.peak_flux_lbl.setText("No Data") self._owner.peak_flux_lbl.setText("No Data")
if xray_long < 1e-5 and xray_long != -1.00e+05: if xray_long < 1e-5 and xray_long != -1.00e+05:
self._switchable_r_labels.switch_on(self._owner.r0_now_lbl) self._switchable_r_labels.switch_on(self._owner.r0_now_lbl)
elif xray_long >= 1e-5 and xray_long < 5e-5: elif xray_long >= 1e-5 and xray_long < 5e-5:
self._switchable_r_labels.switch_on(self._owner.r1_now_lbl) self._switchable_r_labels.switch_on(self._owner.r1_now_lbl)
elif xray_long >= 5e-5 and xray_long < 1e-4: elif xray_long >= 5e-5 and xray_long < 1e-4:
self._switchable_r_labels.switch_on(self._owner.r2_now_lbl) self._switchable_r_labels.switch_on(self._owner.r2_now_lbl)
elif xray_long >= 1e-4 and xray_long < 1e-3: elif xray_long >= 1e-4 and xray_long < 1e-3:
self._switchable_r_labels.switch_on(self._owner.r3_now_lbl) self._switchable_r_labels.switch_on(self._owner.r3_now_lbl)
elif xray_long >= 1e-3 and xray_long < 2e-3: elif xray_long >= 1e-3 and xray_long < 2e-3:
self._switchable_r_labels.switch_on(self._owner.r4_now_lbl) self._switchable_r_labels.switch_on(self._owner.r4_now_lbl)
elif xray_long >= 2e-3: elif xray_long >= 2e-3:
self._switchable_r_labels.switch_on(self._owner.r5_now_lbl) self._switchable_r_labels.switch_on(self._owner.r5_now_lbl)
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:
self._switchable_s_labels.switch_on(self._owner.s1_now_lbl) self._switchable_s_labels.switch_on(self._owner.s1_now_lbl)
elif pro10 >= 100 and pro10 < 1000: elif pro10 >= 100 and pro10 < 1000:
self._switchable_s_labels.switch_on(self._owner.s2_now_lbl) self._switchable_s_labels.switch_on(self._owner.s2_now_lbl)
elif pro10 >= 1000 and pro10 < 10000: elif pro10 >= 1000 and pro10 < 10000:
self._switchable_s_labels.switch_on(self._owner.s3_now_lbl) self._switchable_s_labels.switch_on(self._owner.s3_now_lbl)
elif pro10 >= 10000 and pro10 < 100000: elif pro10 >= 10000 and pro10 < 100000:
self._switchable_s_labels.switch_on(self._owner.s4_now_lbl) self._switchable_s_labels.switch_on(self._owner.s4_now_lbl)
elif pro10 >= 100000: elif pro10 >= 100000:
self._switchable_s_labels.switch_on(self._owner.s5_now_lbl) self._switchable_s_labels.switch_on(self._owner.s5_now_lbl)
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))
) a_index = int(self._owner.space_weather_data.ak_index[7][7].replace('.', ''))
self._owner.k_index_lbl.setText(str(k_index)) self._owner.a_index_lbl.setText(str(a_index))
a_index = safe_cast(
self._owner.space_weather_data.ak_index[7][7].replace('.', ''), int
)
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)
self._k_storm_labels.switch_on(self._owner.k_very_quiet_lbl) self._k_storm_labels.switch_on(self._owner.k_very_quiet_lbl)
self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ") self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ")
elif k_index == 2: elif k_index == 2:
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._owner.k_quiet_lbl) self._k_storm_labels.switch_on(self._owner.k_quiet_lbl)
self._owner.expected_noise_lbl.setText(" S1 - S2 (-115 dBm) ") self._owner.expected_noise_lbl.setText(" S1 - S2 (-115 dBm) ")
elif k_index == 3: elif k_index == 3:
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._owner.k_unsettled_lbl) self._k_storm_labels.switch_on(self._owner.k_unsettled_lbl)
self._owner.expected_noise_lbl.setText(" S2 - S3 (-110 dBm) ") self._owner.expected_noise_lbl.setText(" S2 - S3 (-110 dBm) ")
elif k_index == 4: elif k_index == 4:
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._owner.k_active_lbl) self._k_storm_labels.switch_on(self._owner.k_active_lbl)
self._owner.expected_noise_lbl.setText(" S3 - S4 (-100 dBm) ") self._owner.expected_noise_lbl.setText(" S3 - S4 (-100 dBm) ")
elif k_index == 5: elif k_index == 5:
self._switchable_g_now_labels.switch_on(self._owner.g1_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g1_now_lbl)
self._k_storm_labels.switch_on(self._owner.k_min_storm_lbl) self._k_storm_labels.switch_on(self._owner.k_min_storm_lbl)
self._owner.expected_noise_lbl.setText(" S4 - S6 (-90 dBm) ") self._owner.expected_noise_lbl.setText(" S4 - S6 (-90 dBm) ")
elif k_index == 6: elif k_index == 6:
self._switchable_g_now_labels.switch_on(self._owner.g2_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g2_now_lbl)
self._k_storm_labels.switch_on(self._owner.k_maj_storm_lbl) self._k_storm_labels.switch_on(self._owner.k_maj_storm_lbl)
self._owner.expected_noise_lbl.setText(" S6 - S9 (-80 dBm) ") self._owner.expected_noise_lbl.setText(" S6 - S9 (-80 dBm) ")
elif k_index == 7: elif k_index == 7:
self._switchable_g_now_labels.switch_on(self._owner.g3_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g3_now_lbl)
self._k_storm_labels.switch_on(self._owner.k_sev_storm_lbl) self._k_storm_labels.switch_on(self._owner.k_sev_storm_lbl)
self._owner.expected_noise_lbl.setText(" S9 - S20 (>-60 dBm) ") self._owner.expected_noise_lbl.setText(" S9 - S20 (>-60 dBm) ")
elif k_index == 8: elif k_index == 8:
self._switchable_g_now_labels.switch_on(self._owner.g4_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g4_now_lbl)
self._k_storm_labels.switch_on(self._owner.k_very_sev_storm_lbl) self._k_storm_labels.switch_on(self._owner.k_very_sev_storm_lbl)
self._owner.expected_noise_lbl.setText(" S20 - S30 (>-60 dBm) ") self._owner.expected_noise_lbl.setText(" S20 - S30 (>-60 dBm) ")
elif k_index == 9: elif k_index == 9:
self._switchable_g_now_labels.switch_on(self._owner.g5_now_lbl) self._switchable_g_now_labels.switch_on(self._owner.g5_now_lbl)
self._k_storm_labels.switch_on(self._owner.k_ex_sev_storm_lbl) self._k_storm_labels.switch_on(self._owner.k_ex_sev_storm_lbl)
self._owner.expected_noise_lbl.setText(" S30+ (>>-60 dBm) ") self._owner.expected_noise_lbl.setText(" S30+ (>>-60 dBm) ")
self._owner.expected_noise_lbl.switch_on() self._owner.expected_noise_lbl.switch_on()
if a_index >= 0 and a_index < 8: if a_index >= 0 and a_index < 8:
self._a_storm_labels.switch_on(self._owner.a_quiet_lbl) self._a_storm_labels.switch_on(self._owner.a_quiet_lbl)
elif a_index >= 8 and a_index < 16: elif a_index >= 8 and a_index < 16:
self._a_storm_labels.switch_on(self._owner.a_unsettled_lbl) self._a_storm_labels.switch_on(self._owner.a_unsettled_lbl)
elif a_index >= 16 and a_index < 30: elif a_index >= 16 and a_index < 30:
self._a_storm_labels.switch_on(self._owner.a_active_lbl) self._a_storm_labels.switch_on(self._owner.a_active_lbl)
elif a_index >= 30 and a_index < 50: elif a_index >= 30 and a_index < 50:
self._a_storm_labels.switch_on(self._owner.a_min_storm_lbl) self._a_storm_labels.switch_on(self._owner.a_min_storm_lbl)
elif a_index >= 50 and a_index < 100: elif a_index >= 50 and a_index < 100:
self._a_storm_labels.switch_on(self._owner.a_maj_storm_lbl) self._a_storm_labels.switch_on(self._owner.a_maj_storm_lbl)
elif a_index >= 100 and a_index < 400: elif a_index >= 100 and a_index < 400:
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:
) self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl)
if k_index_24_hmax == 0: elif k_index_24_hmax == 1:
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 == 2:
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 == 2: elif k_index_24_hmax == 3:
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 == 3: elif k_index_24_hmax == 4:
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 == 4: elif k_index_24_hmax == 5:
self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g1_today_lbl)
elif k_index_24_hmax == 5: elif k_index_24_hmax == 6:
self._switchable_g_today_labels.switch_on(self._owner.g1_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g2_today_lbl)
elif k_index_24_hmax == 6: elif k_index_24_hmax == 7:
self._switchable_g_today_labels.switch_on(self._owner.g2_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g3_today_lbl)
elif k_index_24_hmax == 7: elif k_index_24_hmax == 8:
self._switchable_g_today_labels.switch_on(self._owner.g3_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g4_today_lbl)
elif k_index_24_hmax == 8: elif k_index_24_hmax == 9:
self._switchable_g_today_labels.switch_on(self._owner.g4_today_lbl) self._switchable_g_today_labels.switch_on(self._owner.g5_today_lbl)
elif k_index_24_hmax == 9:
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}")
) val = int(
self._owner.sfi_lbl.setText(f"{val}") [x[4] for x in self._owner.space_weather_data.sgas if "SSN" in x][0]
val = safe_cast( )
[x[4] for x in self._owner.space_weather_data.sgas self._owner.sn_lbl.setText(f"{val:d}")
if "SSN" in x][0], int
) for label, pixmap in zip(self.space_weather_labels,
self._owner.sn_lbl.setText(f"{val:d}") self._owner.space_weather_data.images):
label.pixmap = pixmap
label.make_transparent()
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()
for label, pixmap in zip(self.space_weather_labels,
self._owner.space_weather_data.images):
label.pixmap = pixmap
label.make_transparent()
label.apply_pixmap()
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,30 +140,31 @@ 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")
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)): if not os.path.exists(ThemeConstants.FOLDER):
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
if os.path.isdir(os.path.abspath(relative_folder)):
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
themes.append(relative_folder)
for theme_path in themes:
theme_name = '&' + self._pretty_name(os.path.basename(theme_path))
new_theme = ag.addAction(
QAction(
theme_name,
self._owner,
checkable=True
)
)
self._owner.menu_themes.addAction(new_theme)
self._theme_names[theme_name.lstrip('&')] = new_theme
new_theme.triggered.connect(partial(self._apply, theme_path))
else:
pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND, pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
text=ThemeConstants.MISSING_THEME_FOLDER).show() text=ThemeConstants.MISSING_THEME_FOLDER).show()
return
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)):
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
if os.path.isdir(os.path.abspath(relative_folder)):
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
themes.append(relative_folder)
for theme_path in themes:
theme_name = '&' + self._pretty_name(os.path.basename(theme_path))
new_theme = ag.addAction(
QAction(
theme_name,
self._owner,
checkable=True
)
)
themes_menu.addAction(new_theme)
self._theme_names[theme_name.lstrip('&')] = new_theme
new_theme.triggered.connect(partial(self._apply, theme_path))
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.
@@ -173,8 +174,8 @@ class ThemeManager:
try: try:
with open(os.path.join(self._theme_path, theme_name), "r") as stylesheet: with open(os.path.join(self._theme_path, theme_name), "r") as stylesheet:
style = stylesheet.read() style = stylesheet.read()
self._owner.setStyleSheet(style) self._owner.setStyleSheet(style)
self._owner.download_window.setStyleSheet(style) self._owner.download_window.setStyleSheet(style)
except FileNotFoundError: except FileNotFoundError:
pop_up(self._owner, title=ThemeConstants.THEME_NOT_FOUND, pop_up(self._owner, title=ThemeConstants.THEME_NOT_FOUND,
text=ThemeConstants.MISSING_THEME).show() text=ThemeConstants.MISSING_THEME).show()
@@ -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,20 +210,17 @@ class UpdatesControllerThread(BaseDownloadThread):
# self.status = ThreadStatus.OK # self.status = ThreadStatus.OK
class _AsyncDownloader: async def _download_resource(session, link):
"""Mixin class for asynchronous threads.""" """Return the content of 'link' as bytes."""
ssl_context = ssl.create_default_context(
async def _download_resource(self, session, link): purpose=ssl.Purpose.SERVER_AUTH,
"""Return the content of 'link' as bytes.""" cafile=get_cacert_file()
ssl_context = ssl.create_default_context( )
purpose=ssl.Purpose.SERVER_AUTH, resp = await session.get(link, ssl=ssl_context)
cafile=get_cacert_file() return await resp.read()
)
resp = await session.get(link, ssl=ssl_context)
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,6 +61,20 @@ def _download_versions_file():
"size": ... "size": ...
} }
} }
"raspberry": {
"software": {
"version": "...",
"url": "...",
"hash_code": "...",
"size": ...
},
"updater": {
"version": "...",
"url": "...",
"hash_code": "...",
"size": ...
}
}
} }
""" """
try: try:
@@ -80,7 +94,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,16 +102,14 @@ 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
if isinstance(dct_element, dict):
setattr(self, attr, type(self)(dct_element))
else: else:
if isinstance(dct_element, dict): setattr(self, attr, dct_element)
setattr(self, attr, type(self)(dct_element)) return getattr(self, attr)
else:
setattr(self, attr, dct_element)
return getattr(self, attr)
def update(self): def update(self):
"""Reset the dictionary to the correspondig json file containing """Reset the dictionary to the correspondig json file containing

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,11 +91,34 @@ 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.ak_index = self._double_split(self.ak_index) self.xray,
self.sgas = self._double_split(self.sgas) lambda d: d["energy"] == "0.1-0.8nm",
self.geo_storm = self._double_split(self.geo_storm) "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)
if self.sgas is not None:
self.sgas = self._double_split(self.sgas)
if self.geo_storm is not None:
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."""
@@ -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