diff --git a/Building/Linux/OSCAR.desktop b/Building/Linux/OSCAR.desktop old mode 100755 new mode 100644 diff --git a/Building/Linux/README.first b/Building/Linux/README.first index 1dffd613..4116b500 100644 --- a/Building/Linux/README.first +++ b/Building/Linux/README.first @@ -4,8 +4,10 @@ you have already compiled and linked OSCAR using qmake and make, or QtCreator. The packages, once built, can be installed using 'dpkg i' or 'gdebi' - with either su or sudo. If you use sudo, you will get a desktop icon installed, which must be double-clicked and trusted to see the actual icon. -The packaged file cannot be installed using apt, apt-get, or aptitude, because those programs -install from the distribution repository, but not a plan package file. +The packaged file cannot be installed using apt-get, or aptitude, because those programs +install from the distribution repository, but not a plain package file. + +The package can be installed with apt or apt -F if the file name is preceded by ./ to force filename recognition. The packaging scripts assume the following folder structure: @@ -22,6 +24,9 @@ The code to put a desktop icon in the Desktop folder, regardless of language, wa Getting a menu item istalled is largely due to the efforts of CrimsonNape. +The mkDebian9.sh and mkUbuntu.sh scripts have been consolidated into mkDistDeb.sh and are now depreciated and will be removed. mkDistDeb.sh +has code to query which packages are available for certail libraries. + Finally, the mkRedHat.sh script has not been tested. diff --git a/Building/Linux/ln_usrbin-test.sh b/Building/Linux/ln_usrbin-test.sh index 2f522f17..fa66e44d 100755 --- a/Building/Linux/ln_usrbin-test.sh +++ b/Building/Linux/ln_usrbin-test.sh @@ -10,32 +10,32 @@ appli_name="OSCAR-test" # binary copy flag copy_flag=0 -# Select the binary file for the goog version of QT (5.7 ou 5.9) -#echo "test the version of qt5 core version" +#### Select the binary file for the goog version of QT (5.7 ou 5.9) +####echo "test the version of qt5 core version" +### +###if [ -f "/etc/redhat-release" ]; then +### # pour mageia (red hat) +### echo "distribution : mageia (red hat)" +### Qt5_core=$(yum search -q qt5core5 | awk '{print $1}' | grep lib | sort -u) +### MajorVer_no=$(yum info $Qt5_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $1}') +### MinorVer_no=$(yum info $Qt5_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $2}') +### +###elif [ -f "/etc/lsb-release" ] || [ -f "/etc/debian_version" ]; then +### # pour debian +#### echo "distribution : debian" +### Qt5_core=$(dpkg -l | awk '{print $2}'| grep qt5 | grep core | awk -F: '{print $1}') +### Major_ver=$(dpkg -l | grep $Qt5_core | awk '{print $3}' | awk -F. '{print $1}') +### Minor_ver=$(dpkg -l | grep $Qt5_core | awk '{print $3}' | awk -F. '{print $2}') +###else +### echo "unknown distribution " +### exit +###fi +### +####echo "Qt5_core = '$Qt5_core'" +####echo "Major_ver = $Major_ver" +####echo "Minor_ver = $Minor_ver" -if [ -f "/etc/redhat-release" ]; then - # pour mageia (red hat) - echo "distribution : mageia (red hat)" - Qt5_core=$(yum search -q qt5core5 | awk '{print $1}' | grep lib | sort -u) - MajorVer_no=$(yum info $Qt5_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $1}') - MinorVer_no=$(yum info $Qt5_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $2}') - -elif [ -f "/etc/lsb-release" ] || [ -f "/etc/debian_version" ]; then - # pour debian -# echo "distribution : debian" - Qt5_core=$(dpkg -l | awk '{print $2}'| grep qt5 | grep core | awk -F: '{print $1}') - Major_ver=$(dpkg -l | grep $Qt5_core | awk '{print $3}' | awk -F. '{print $1}') - Minor_ver=$(dpkg -l | grep $Qt5_core | awk '{print $3}' | awk -F. '{print $2}') -else - echo "unknown distribution " - exit -fi - -#echo "Qt5_core = '$Qt5_core'" -#echo "Major_ver = $Major_ver" -#echo "Minor_ver = $Minor_ver" - -if [ -x /usr/bin/update-menus ]; then update-menus; fi +##if [ -x /usr/bin/update-menus ]; then update-menus; fi if [ ! -z "$SUDO_USER" ]; then # find real name of the Desktop folder (Bureau for xubuntu french version) diff --git a/Building/Linux/ln_usrbin.sh b/Building/Linux/ln_usrbin.sh index a8946424..ed4f4fa3 100755 --- a/Building/Linux/ln_usrbin.sh +++ b/Building/Linux/ln_usrbin.sh @@ -10,38 +10,38 @@ appli_name="OSCAR" # binary copy flag copy_flag=0 -# Select the binary file for the goog version of QT (5.7 ou 5.9) -echo "test the version of qt5 core version" +#### Select the binary file for the goog version of QT (5.7 ou 5.9) +###echo "test the version of qt5 core version" +### +###if [ -f "/etc/redhat-release" ]; then +### # pour mageia (red hat) +### echo "distribution : mageia (red hat)" +### nom_core=$(yum search -q qt5core5 | awk '{print $1}' | grep lib | sort -u) +### no_vermaj=$(yum info $nom_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $1}') +### no_vermin=$(yum info $nom_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $2}') +### +###elif [ -f "/etc/lsb-release" ] || [ -f "/etc/debian_version" ]; then +### # pour debian +### echo "distribution : debian" +### nom_core=$(dpkg -l | awk '{print $2}'| grep qt5 | grep core | awk -F: '{print $1}') +### no_vermaj=$(dpkg -l | grep $nom_core | awk '{print $3}' | awk -F. '{print $1}') +### no_vermin=$(dpkg -l | grep $nom_core | awk '{print $3}' | awk -F. '{print $2}') +###else +### echo "unknown distribution " +### exit +###fi +### +###echo "QT5_core = '$nom_core'" +###echo "Major ver = $no_vermaj" +###echo "Minor ver = $no_vermin" -if [ -f "/etc/redhat-release" ]; then - # pour mageia (red hat) - echo "distribution : mageia (red hat)" - nom_core=$(yum search -q qt5core5 | awk '{print $1}' | grep lib | sort -u) - no_vermaj=$(yum info $nom_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $1}') - no_vermin=$(yum info $nom_core | grep -i "version" | awk '{print $3}' | awk -F. '{print $2}') - -elif [ -f "/etc/lsb-release" ] || [ -f "/etc/debian_version" ]; then - # pour debian - echo "distribution : debian" - nom_core=$(dpkg -l | awk '{print $2}'| grep qt5 | grep core | awk -F: '{print $1}') - no_vermaj=$(dpkg -l | grep $nom_core | awk '{print $3}' | awk -F. '{print $1}') - no_vermin=$(dpkg -l | grep $nom_core | awk '{print $3}' | awk -F. '{print $2}') -else - echo "unknown distribution " - exit -fi - -echo "QT5_core = '$nom_core'" -echo "Major ver = $no_vermaj" -echo "Minor ver = $no_vermin" - -if [ -x /usr/bin/update-menus ]; then update-menus; fi +##if [ -x /usr/bin/update-menus ]; then update-menus; fi if [ X_$SUDO_USER != "X_" ]; then # find real name of the Desktop folder (Bureau for xubuntu french version) desktop_folder_name="/home/$SUDO_USER/Desktop" - # si doesn't exist, try to find it translated name + # if doesn't exist, try to find it translated name tmp_dir="" if [ ! -d "$desktop_folder_name" ]; then tmp_dir=`cat /home/$SUDO_USER/.config/user-dirs.dirs | grep XDG_DESKTOP_DIR | awk -F= '{print $2}' | awk -F\" '{print $2}' | awk -F\/ '{print $2}'` diff --git a/Building/Linux/ln_usrlocalbin.sh b/Building/Linux/ln_usrlocalbin.sh deleted file mode 100755 index 08043b01..00000000 --- a/Building/Linux/ln_usrlocalbin.sh +++ /dev/null @@ -1,8 +0,0 @@ -#! /bin/bash -# -mv /opt/tempDir /opt/OSCAR -ln -s /opt/OSCAR/OSCAR /usr/bin/OSCAR -cp /opt/OSCAR/OSCAR.desktop /home/$SUDO_USER/Desktop -chown $SUDO_USER:$SUDO_USER /home/$SUDO_USER/Desktop/OSCAR.desktop -chmod a+x /home/$SUDO_USER/Desktop/OSCAR.desktop -# diff --git a/Building/Linux/mkDebian9.sh b/Building/Linux/mkDebian9.sh index 268296fe..8a4f6b00 100755 --- a/Building/Linux/mkDebian9.sh +++ b/Building/Linux/mkDebian9.sh @@ -30,7 +30,7 @@ pre_rem="rm_usrbin.sh" post_rem="clean_rm.sh" # build folder (absolute path is better) build_folder="/home/$USER/OSCAR/build" -if [[ -n ${PRERELEASE} && ${RC} ]] ; then +if [[ -n ${PRERELEASE} && -z ${RC} ]] ; then appli_name=${appli_name}-test post_inst="ln_usrbin-test.sh" pre_rem="rm_usrbin-test.sh" diff --git a/Building/Linux/mkDistDeb.sh b/Building/Linux/mkDistDeb.sh new file mode 100755 index 00000000..c0417773 --- /dev/null +++ b/Building/Linux/mkDistDeb.sh @@ -0,0 +1,179 @@ +#! /bin/bash +# First parameter is optional +# +function getPkg () { + unset PKGNAME + unset PKGVERS + while read stat pkg ver other ; + do + if [[ ${stat} == "ii" ]] ; then + PKGNAME=`awk -F: '{print $1}' <<< ${pkg}` + PKGVERS=`awk -F. '{print $1 "." $2}' <<< ${ver}` + break + fi ; + done <<< $(dpkg -l | grep $1) +} + +ITERATION=$1 +if [ -z ${ITERATION} ]; then + ITERATION="1" +fi + +SRC=/home/$USER/OSCAR/OSCAR-code/oscar + +VERSION=`awk '/#define VERSION / { gsub(/"/, "", $3); print $3 }' ${SRC}/VERSION` +if [[ ${VERSION} == *-* ]]; then + # Use ~ for prerelease information so that it sorts correctly compared to release + # versions. See https://www.debian.org/doc/debian-policy/ch-controlfields.html#version + IFS="-" read -r VERSION PRERELEASE <<< ${VERSION} + if [[ ${PRERELEASE} == *rc* ]]; then + RC=1 + fi + VERSION="${VERSION}~${PRERELEASE}" +fi +GIT_REVISION=`awk '/#define GIT_REVISION / { gsub(/"/, "", $3); print $3 }' ${SRC}/git_info.h` +echo Version: ${VERSION} + +# application name +appli_name="OSCAR" +pre_inst="tst_user.sh" +# build folder (absolute path is better) +build_folder="/home/$USER/OSCAR/build" +if [[ -n ${PRERELEASE} && -z ${RC} ]] ; then + appli_name=${appli_name}-test + post_inst="ln_usrbin-test.sh" + pre_rem="rm_usrbin-test.sh" + post_rem="clean_rm-test.sh" +else + post_inst="ln_usrbin.sh" + pre_rem="rm_usrbin.sh" + post_rem="clean_rm.sh" +fi + +# temporary folder (absolute path is better) +temp_folder="/home/$USER/tmp_deb_${appli_name}/" + +# destination folder in the .deb file +dest_folder="/usr/" + +# the .deb file mustn't exist +archi_tmp=$(lscpu | grep -i architecture | awk -F: {'print $2'} | tr -d " ") +if [ "$archi_tmp" = "x86_64" ];then + archi="amd64" +else + archi="unknown" +fi +deb_file="${appli_name}_${VERSION}-${ITERATION}_$archi.deb" + +# if deb file exists, fatal error +if [ -f "./$deb_file" ]; then + echo "destination file (./$deb_file) exists. fatal error" + exit +fi + +# retrieve packages version for the dependencies +getPkg libqt5core +qtver=$PKGVERS + +getPkg libdouble +dblPkg=$PKGNAME + +echo "QT version " $qtver +echo "DblConv package " $dblPkg + +# clean folders need to create the package +if [ -d "${temp_folder}" ]; then + rm -r ${temp_folder} +fi +mkdir ${temp_folder} +if [ ! -d "${temp_folder}" ]; then + echo "Folder (${temp_folder}) not created : fatal error." + exit +fi +chmod 0755 ${temp_folder} +# save current value of umask (for u=g and not g=o) +current_value=$(umask) +umask 022 +mkdir ${temp_folder}/bin +mkdir ${temp_folder}/share +mkdir ${temp_folder}/share/${appli_name} +mkdir ${temp_folder}/share/doc +share_doc_folder="${temp_folder}/share/doc/${appli_name}" +mkdir ${share_doc_folder} +mkdir ${temp_folder}/share/icons +mkdir ${temp_folder}/share/icons/hicolor +mkdir ${temp_folder}/share/icons/hicolor/48x48 +mkdir ${temp_folder}/share/icons/hicolor/48x48/apps +mkdir ${temp_folder}/share/icons/hicolor/scalable +mkdir ${temp_folder}/share/icons/hicolor/scalable/apps +mkdir ${temp_folder}/share/applications + +# must delete debug symbol in OSCAR binary file +# --- V1 +strip -s -o ${temp_folder}/bin/${appli_name} ${build_folder}/oscar/OSCAR +#old code : cp ${build_folder}/oscar/OSCAR ${temp_folder}/bin + +# 2>/dev/null : errors does not appear : we don't care about them +cp -r ${build_folder}/oscar/Help ${temp_folder}/share/${appli_name} 2>/dev/null +cp -r ${build_folder}/oscar/Html ${temp_folder}/share/${appli_name} 2>/dev/null +cp -r ${build_folder}/oscar/Translations ${temp_folder}/share/${appli_name} 2>/dev/null +cp ./${appli_name}.png ${temp_folder}/share/icons/hicolor/48x48/apps/${appli_name}.png +cp ./${appli_name}.svg ${temp_folder}/share/icons/hicolor/scalable/apps/${appli_name}.svg +cp ./${appli_name}.desktop ${temp_folder}/share/applications/${appli_name}.desktop + +echo "Copyright 2019-2020 oscar-team.org " > $share_doc_folder/copyright + +changelog_file="$share_doc_folder/changelog" + +#automatic changelog as a bad name +# need to generate one and say fpm to use it instead of create one +# it seems that it needs both of them... + +# creation of the changelog.Debian.gz +echo "$appli_name (${VERSION}-${ITERATION}) whatever; urgency=medium" > $changelog_file +echo "" >> $changelog_file +echo " * Package created with FPM." >> $changelog_file +echo "" >> $changelog_file +echo " -- oscar-team.org " >> $changelog_file +gzip --best $changelog_file +description='Open Source CPAP Analysis Reporter\n' +# trick for dummies : need to use echo -e to take care of \n (cariage return to slip description and extra one +description=$(echo -e $description) + +# restore umask value +umask $current_value + +# create the .deb file (litian test show juste a warning with a man that doesn't exists : don't care about that) +fpm --input-type dir --output-type deb \ + --prefix ${dest_folder} \ + --before-install ${pre_inst} \ + --after-install ${post_inst} \ + --before-remove ${pre_rem} \ + --after-remove ${post_rem} \ + --name ${appli_name} --version ${VERSION} --iteration ${ITERATION} \ + --category misc \ + --deb-priority optional \ + --maintainer " -- oscar-team.org " \ + --license GPL-v3 \ + --vendor oscar-team.org \ + --description "$description" \ + --url https://sleepfiles.com/OSCAR \ + --deb-no-default-config-files \ + --depends $dblPkg \ + --depends libpcre16-3 \ + --depends qttranslations5-l10n \ + --depends "libqt5core5a > $qtver" \ + --depends libqt5serialport5 \ + --depends libqt5xml5 \ + --depends libqt5network5 \ + --depends libqt5gui5 \ + --depends libqt5widgets5 \ + --depends libqt5opengl5 \ + --depends libqt5printsupport5 \ + --depends libglu1-mesa \ + --depends libgl1 \ + --depends libc6 \ + --no-deb-generate-changes \ + -C ${temp_folder} \ + . + diff --git a/Building/Linux/rm_usrlocalbin.sh b/Building/Linux/rm_usrlocalbin.sh deleted file mode 100755 index c2a149e5..00000000 --- a/Building/Linux/rm_usrlocalbin.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /bin/bash -# -mv /opt/OSCAR /opt/tempDir -rm /usr/bin/OSCAR -rm /home/$SUDO_USER/Desktop/OSCAR.desktop -# diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index 50d8d281..4ccde935 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -21,10 +21,15 @@
  • [new] Add the "peak flow" channel reported by pre-DreamStation ventilators.
  • [new] Automatically detect and resolve graphics-related crashes on Windows.
  • +
  • [new] Support AVAPS in the Overview pressure chart.
  • +
  • [fix] Fix missing bars in the Overview pressure chart for Philips Respironics devices.
  • +
  • [fix] Add missing Philips Respironics pressure channels to CSV export.
  • +
  • [fix] Fix zero Philips Respironics AHI in CSV session export.
  • [fix] Add support for the Bi-Flex lock setting on pre-DreamStation ventilators.
  • [fix] Fix the pressure waveform scale for the BiPAP autoSV Advanced 30 (960T)
  • [fix] Add support for rise time mode on DreamStation BiPAP devices (600X-700X).
  • [fix] Remove the ramp time and pressure settings when the ramp is disabled on pre-DreamStation devices.
  • +
  • [fix] Improve import of Philips Respironics oximetry data.
  • [fix] Fix occasional failure to save imported Viatom data.
  • diff --git a/dumpSTR/main.cpp b/dumpSTR/main.cpp index 4f3e54db..65e3d95f 100644 --- a/dumpSTR/main.cpp +++ b/dumpSTR/main.cpp @@ -54,6 +54,8 @@ void usage() { qDebug() << "\t-g ### First signal"; qDebug() << "\t-m ### Last signal"; qDebug() << "\t-s Signal list only"; + qDebug() << "\t-H Don't print the header"; + qDebug() << "\t-S Don't print signals list"; qDebug() << "\t-h or -? This help message"; } @@ -77,6 +79,7 @@ int main(int argc, char *argv[]) { QString filename = args[args.size()-1]; bool showall = false, brief = false; + bool skipHeader = false, skipSignals = false; for (int i = 1; i < args.size()-1; i++) { if (args[i] == "-f") @@ -91,6 +94,10 @@ int main(int argc, char *argv[]) { showall = true; else if (args[i] == "-s") brief = true; + else if (args[i] == "-H") + skipHeader = true; + else if (args[i] == "-S") + skipSignals = true; else if ((args[i] == "-?") || (args[i] == "-h")) { usage(); exit(0); @@ -116,17 +123,19 @@ int main(int argc, char *argv[]) { QDate date = str.edfHdr.startdate_orig.date(); // each STR.edf record starts at 12 noon + int numDays = str.GetNumDataRecords(); - qDebug() << str.filename << " starts at " << date << " for " << str.GetNumDataRecords() - << " days, with " << str.GetNumSignals() << " signals"; + qDebug() << str.filename << " starts at " << date << " for " << numDays + << " days, ending at " << date.addDays(numDays) << " with " << str.GetNumSignals() << " signals"; if (args.size() == 2) { exit(0); } - dumpHeader( (str.edfHdr) ); - - dumpSignals( (str.edfsignals) ); + if ( ! skipHeader) + dumpHeader( (str.edfHdr) ); + if ( ! skipSignals ) + dumpSignals( (str.edfsignals) ); if ( brief ) exit(0); @@ -142,7 +151,7 @@ int main(int argc, char *argv[]) { if (lastSig == 0 ) lastSig = str.GetNumSignals(); - if (((first > 0)&&(last == 0)) || last > size) + if ((last == 0) || (last > size)) last = size; date = date.addDays(first); diff --git a/oscar/Graphs/gPressureChart.cpp b/oscar/Graphs/gPressureChart.cpp new file mode 100644 index 00000000..e7f8b98d --- /dev/null +++ b/oscar/Graphs/gPressureChart.cpp @@ -0,0 +1,239 @@ +/* gPressureChart Implementation + * + * Copyright (c) 2020 The Oscar Team + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include "gPressureChart.h" + +gPressureChart::gPressureChart() + : gSummaryChart("Pressure", MT_CPAP) +{ + addCalc(CPAP_Pressure, ST_SETMAX); + addCalc(CPAP_Pressure, ST_MID); + addCalc(CPAP_Pressure, ST_90P); + addCalc(CPAP_PressureMin, ST_SETMIN); + addCalc(CPAP_PressureMax, ST_SETMAX); + + addCalc(CPAP_EPAP, ST_SETMAX); + addCalc(CPAP_IPAP, ST_SETMAX); + addCalc(CPAP_EPAPLo, ST_SETMAX); + addCalc(CPAP_IPAPHi, ST_SETMAX); + + addCalc(CPAP_EPAP, ST_MID); + addCalc(CPAP_EPAP, ST_90P); + addCalc(CPAP_IPAP, ST_MID); + addCalc(CPAP_IPAP, ST_90P); + + // PRS1 reports pressure adjustments instead of observed pressures on some machines + addCalc(CPAP_PressureSet, ST_MID); + addCalc(CPAP_PressureSet, ST_90P); + addCalc(CPAP_EPAPSet, ST_MID); + addCalc(CPAP_EPAPSet, ST_90P); + addCalc(CPAP_IPAPSet, ST_MID); + addCalc(CPAP_IPAPSet, ST_90P); +} + + +int gPressureChart::addCalc(ChannelID code, SummaryType type) +{ + QColor color = schema::channel[code].defaultColor(); + if (type == ST_90P) { + color = brighten(color, 1.33f); + } + + int index = gSummaryChart::addCalc(code, type, color); + + // Save the code and type used to add this calculation so that getCalc() + // can retrieve it by code and type instead of by hard-coded index. + m_calcs[code][type] = index; + + return index; +} + + +SummaryCalcItem* gPressureChart::getCalc(ChannelID code, SummaryType type) +{ + return &calcitems[m_calcs[code][type]]; +} + + +void gPressureChart::afterDraw(QPainter &, gGraph &graph, QRectF rect) +{ + QStringList presstr; + + if (getCalc(CPAP_Pressure)->cnt > 0) { + presstr.append(channelRange(CPAP_Pressure, STR_TR_CPAP)); + } + + if (getCalc(CPAP_PressureMin, ST_SETMIN)->cnt > 0) { + // TODO: If using machines from different manufacturers in an overview, + // the below may not accurately find the APAP pressure channel for all + // days; but it only affects the summary label at the top. + ChannelID pressure = CPAP_Pressure; + if (getCalc(CPAP_PressureSet, ST_MID)->cnt > 0) { + pressure = CPAP_PressureSet; + } + presstr.append(QString("%1 %2/%3/%4/%5"). + arg(STR_TR_APAP). + arg(getCalc(CPAP_PressureMin, ST_SETMIN)->min,0,'f',1). + arg(getCalc(pressure, ST_MID)->mid(), 0, 'f', 1). + arg(getCalc(pressure, ST_90P)->mid(),0,'f',1). + arg(getCalc(CPAP_PressureMax, ST_SETMAX)->max, 0, 'f', 1)); + + } + + if (getCalc(CPAP_EPAP)->cnt > 0) { + // See CPAP_PressureSet note above. + ChannelID epap = CPAP_EPAP; + if (getCalc(CPAP_EPAPSet, ST_MID)->cnt > 0) { + epap = CPAP_EPAPSet; + } + presstr.append(channelRange(epap, STR_TR_EPAP)); + } + + if (getCalc(CPAP_IPAP)->cnt > 0) { + // See CPAP_PressureSet note above. + ChannelID ipap = CPAP_IPAP; + if (getCalc(CPAP_IPAPSet, ST_MID)->cnt > 0) { + ipap = CPAP_IPAPSet; + } + presstr.append(channelRange(ipap, STR_TR_IPAP)); + } + + if (getCalc(CPAP_EPAPLo)->cnt > 0) { + presstr.append(channelRange(CPAP_EPAPLo, STR_TR_EPAPLo)); + } + + if (getCalc(CPAP_IPAPHi)->cnt > 0) { + presstr.append(channelRange(CPAP_IPAPHi, STR_TR_IPAPHi)); + } + + QString txt = presstr.join(" "); + graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); + +} + + +QString gPressureChart::channelRange(ChannelID code, const QString & label) +{ + SummaryCalcItem* calc = getCalc(code); + return QString("%1 %2/%3/%4"). + arg(label). + arg(calc->min, 0, 'f', 1). + arg(calc->mid(), 0, 'f', 1). + arg(calc->max, 0, 'f', 1); +} + + +void gPressureChart::addSlice(ChannelID code, SummaryType type) +{ + float value = 0; + QString label; + + switch (type) { + case ST_SETMIN: + value = m_day->settings_min(code); + label = schema::channel[code].label(); + break; + case ST_SETMAX: + value = m_day->settings_max(code); + label = schema::channel[code].label(); + break; + case ST_MID: + value = m_day->calcMiddle(code); + label = m_day->calcMiddleLabel(code); + break; + case ST_90P: + value = m_day->calcPercentile(code); + label = m_day->calcPercentileLabel(code); + break; + default: + qWarning() << "Unsupported summary type in gPressureChart"; + break; + } + + SummaryCalcItem* calc = getCalc(code, type); + float height = value - m_height; + + m_slices->append(SummaryChartSlice(calc, value, height, label, calc->color)); + m_height += height; +} + + +void gPressureChart::populate(Day * day, int idx) +{ + CPAPMode mode = (CPAPMode)(int)qRound(day->settings_wavg(CPAP_Mode)); + m_day = day; + m_slices = &cache[idx]; + m_height = 0; + + if (mode == MODE_CPAP) { + addSlice(CPAP_Pressure); + + } else if (mode == MODE_APAP) { + addSlice(CPAP_PressureMin, ST_SETMIN); + if (!day->summaryOnly()) { + // Handle PRS1 pressure adjustments reported separately from average (EPAP) pressure + ChannelID pressure = CPAP_Pressure; + if (m_day->channelHasData(CPAP_PressureSet)) { + pressure = CPAP_PressureSet; + } + addSlice(pressure, ST_MID); + addSlice(pressure, ST_90P); + } + addSlice(CPAP_PressureMax, ST_SETMAX); + + } else if (mode == MODE_BILEVEL_FIXED) { + addSlice(CPAP_EPAP); + addSlice(CPAP_IPAP); + + } else if (mode == MODE_BILEVEL_AUTO_FIXED_PS) { + addSlice(CPAP_EPAPLo); + if (!day->summaryOnly()) { + addSlice(CPAP_EPAP, ST_MID); + addSlice(CPAP_EPAP, ST_90P); + addSlice(CPAP_IPAP, ST_MID); + addSlice(CPAP_IPAP, ST_90P); + } + addSlice(CPAP_IPAPHi); + + } else if ((mode == MODE_BILEVEL_AUTO_VARIABLE_PS) || (mode == MODE_ASV_VARIABLE_EPAP)) { + addSlice(CPAP_EPAPLo); + if (!day->summaryOnly()) { + // Handle PRS1 pressure adjustments when reported instead of observed pressures + ChannelID epap = CPAP_EPAP; + if (m_day->channelHasData(CPAP_EPAPSet)) { + epap = CPAP_EPAPSet; + } + ChannelID ipap = CPAP_IPAP; + if (m_day->channelHasData(CPAP_IPAPSet)) { + ipap = CPAP_IPAPSet; + } + addSlice(epap, ST_MID); + addSlice(epap, ST_90P); + addSlice(ipap, ST_MID); + addSlice(ipap, ST_90P); + } + addSlice(CPAP_IPAPHi); + + } else if (mode == MODE_ASV) { + addSlice(CPAP_EPAP); + if (!day->summaryOnly()) { + addSlice(CPAP_IPAP, ST_MID); + addSlice(CPAP_IPAP, ST_90P); + } + addSlice(CPAP_IPAPHi); + + } else if (mode == MODE_AVAPS) { + addSlice(CPAP_EPAP); + if (!day->summaryOnly()) { + addSlice(CPAP_IPAP, ST_MID); + addSlice(CPAP_IPAP, ST_90P); + } + addSlice(CPAP_IPAPHi); + } +} diff --git a/oscar/Graphs/gPressureChart.h b/oscar/Graphs/gPressureChart.h new file mode 100644 index 00000000..842d1c94 --- /dev/null +++ b/oscar/Graphs/gPressureChart.h @@ -0,0 +1,61 @@ +/* gPressureChart Header + * + * Copyright (c) 2020 The Oscar Team + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef GPRESSURECHART_H +#define GPRESSURECHART_H + +#include "gSessionTimesChart.h" + +class gPressureChart : public gSummaryChart +{ +public: + gPressureChart(); + virtual ~gPressureChart() {} + + virtual Layer * Clone() { + gPressureChart * sc = new gPressureChart(); + gSummaryChart::CloneInto(sc); + return sc; + } + +// virtual void preCalc(); + virtual void customCalc(Day *day, QVector &slices) { + int size = slices.size(); + float hour = day->hours(m_machtype); + for (int i=0; i < size; ++i) { + SummaryChartSlice & slice = slices[i]; + SummaryCalcItem * calc = slices[i].calc; + + calc->update(slice.value, hour); + } + } + virtual void afterDraw(QPainter &, gGraph &, QRectF); + + virtual void populate(Day * day, int idx); + + virtual QString tooltipData(Day * day, int idx) { + return day->getCPAPModeStr() + "\n" + day->getPressureSettings() + gSummaryChart::tooltipData(day, idx); + } + + virtual int addCalc(ChannelID code, SummaryType type); + +protected: + SummaryCalcItem* getCalc(ChannelID code, SummaryType type = ST_SETMAX); + QString channelRange(ChannelID code, const QString & label); + void addSlice(ChannelID code, SummaryType type = ST_SETMAX); + + QHash> m_calcs; + + // State passed between populate() and addSlice(): + Day* m_day; + QVector* m_slices; + float m_height; +}; + +#endif // GPRESSURECHART_H diff --git a/oscar/Graphs/gSessionTimesChart.cpp b/oscar/Graphs/gSessionTimesChart.cpp index b64373a2..29aafb29 100644 --- a/oscar/Graphs/gSessionTimesChart.cpp +++ b/oscar/Graphs/gSessionTimesChart.cpp @@ -1,5 +1,6 @@ /* gSessionTimesChart Implementation * + * Copyright (c) 2020 The Oscar Team * Copyright (c) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public @@ -101,6 +102,17 @@ void gSummaryChart::SetDay(Day *unused_day) //QMap gSummaryChart::dayindex; //QList gSummaryChart::daylist; +int gSummaryChart::addCalc(ChannelID code, SummaryType type, QColor color) +{ + calcitems.append(SummaryCalcItem(code, type, color)); + return calcitems.size() - 1; // return the index of the newly appended calc +} + +int gSummaryChart::addCalc(ChannelID code, SummaryType type) +{ + return addCalc(code, type, schema::channel[code].defaultColor()); +} + bool gSummaryChart::keyPressEvent(QKeyEvent *event, gGraph *graph) { @@ -1288,199 +1300,3 @@ QString gAHIChart::tooltipData(Day *day, int idx) } return QString("\n%1: %2").arg(STR_TR_AHI).arg(float(total) / hour,0,'f',2)+txt; } - - -gPressureChart::gPressureChart() - :gSummaryChart("Pressure", MT_CPAP) -{ - - // Do not reorder these!!! :P - addCalc(CPAP_Pressure, ST_SETMAX, schema::channel[CPAP_Pressure].defaultColor()); // 00 - addCalc(CPAP_Pressure, ST_MID, schema::channel[CPAP_Pressure].defaultColor()); // 01 - addCalc(CPAP_Pressure, ST_90P, brighten(schema::channel[CPAP_Pressure].defaultColor(), 1.33f)); // 02 - addCalc(CPAP_PressureMin, ST_SETMIN, schema::channel[CPAP_PressureMin].defaultColor()); // 03 - addCalc(CPAP_PressureMax, ST_SETMAX, schema::channel[CPAP_PressureMax].defaultColor()); // 04 - - addCalc(CPAP_EPAP, ST_SETMAX, schema::channel[CPAP_EPAP].defaultColor()); // 05 - addCalc(CPAP_IPAP, ST_SETMAX, schema::channel[CPAP_IPAP].defaultColor()); // 06 - addCalc(CPAP_EPAPLo, ST_SETMAX, schema::channel[CPAP_EPAPLo].defaultColor()); // 07 - addCalc(CPAP_IPAPHi, ST_SETMAX, schema::channel[CPAP_IPAPHi].defaultColor()); // 08 - - addCalc(CPAP_EPAP, ST_MID, schema::channel[CPAP_EPAP].defaultColor()); // 09 - addCalc(CPAP_EPAP, ST_90P, brighten(schema::channel[CPAP_EPAP].defaultColor(),1.33f)); // 10 - addCalc(CPAP_IPAP, ST_MID, schema::channel[CPAP_IPAP].defaultColor()); // 11 - addCalc(CPAP_IPAP, ST_90P, brighten(schema::channel[CPAP_IPAP].defaultColor(),1.33f)); // 12 -} - -void gPressureChart::afterDraw(QPainter &, gGraph &graph, QRectF rect) -{ - int pressure_cnt = calcitems[0].cnt; - int pressuremin_cnt = calcitems[3].cnt; - int epap_cnt = calcitems[5].cnt; - int ipap_cnt = calcitems[6].cnt; - int ipaphi_cnt = calcitems[8].cnt; - int epaplo_cnt = calcitems[7].cnt; - - QStringList presstr; - - float mid = 0; - - if (pressure_cnt > 0) { - mid = calcitems[0].mid(); - presstr.append(QString("%1 %2/%3/%4"). - arg(STR_TR_CPAP). - arg(calcitems[0].min,0,'f',1). - arg(mid, 0, 'f', 1). - arg(calcitems[0].max,0,'f',1)); - } - if (pressuremin_cnt > 0) { - presstr.append(QString("%1 %2/%3/%4/%5"). - arg(STR_TR_APAP). - arg(calcitems[3].min,0,'f',1). - arg(calcitems[1].mid(), 0, 'f', 1). - arg(calcitems[2].mid(),0,'f',1). - arg(calcitems[4].max, 0, 'f', 1)); - - } - if (epap_cnt > 0) { - presstr.append(QString("%1 %2/%3/%4"). - arg(STR_TR_EPAP). - arg(calcitems[5].min,0,'f',1). - arg(calcitems[5].mid(), 0, 'f', 1). - arg(calcitems[5].max, 0, 'f', 1)); - } - if (ipap_cnt > 0) { - presstr.append(QString("%1 %2/%3/%4"). - arg(STR_TR_IPAP). - arg(calcitems[6].min,0,'f',1). - arg(calcitems[6].mid(), 0, 'f', 1). - arg(calcitems[6].max, 0, 'f', 1)); - } - if (epaplo_cnt > 0) { - presstr.append(QString("%1 %2/%3/%4"). - arg(STR_TR_EPAPLo). - arg(calcitems[7].min,0,'f',1). - arg(calcitems[7].mid(), 0, 'f', 1). - arg(calcitems[7].max, 0, 'f', 1)); - } - - if (ipaphi_cnt > 0) { - presstr.append(QString("%1 %2/%3/%4"). - arg(STR_TR_IPAPHi). - arg(calcitems[8].min,0,'f',1). - arg(calcitems[8].mid(), 0, 'f', 1). - arg(calcitems[8].max, 0, 'f', 1)); - } - QString txt = presstr.join(" "); - graph.renderText(txt, rect.left(), rect.top()-5*graph.printScaleY(), 0); - -} - - -void gPressureChart::populate(Day * day, int idx) -{ - float tmp; - CPAPMode mode = (CPAPMode)(int)qRound(day->settings_wavg(CPAP_Mode)); - QVector & slices = cache[idx]; - - if (mode == MODE_CPAP) { - float pr = day->settings_max(CPAP_Pressure); - slices.append(SummaryChartSlice(&calcitems[0], pr, pr, schema::channel[CPAP_Pressure].label(), calcitems[0].color)); - } else if (mode == MODE_APAP) { - float min = day->settings_min(CPAP_PressureMin); - float max = day->settings_max(CPAP_PressureMax); - - tmp = min; - - slices.append(SummaryChartSlice(&calcitems[3], min, min, schema::channel[CPAP_PressureMin].label(), calcitems[3].color)); - if (!day->summaryOnly()) { - float med = day->calcMiddle(CPAP_Pressure); - slices.append(SummaryChartSlice(&calcitems[1], med, med - tmp, day->calcMiddleLabel(CPAP_Pressure), calcitems[1].color)); - tmp += med - tmp; - - float p90 = day->calcPercentile(CPAP_Pressure); - slices.append(SummaryChartSlice(&calcitems[2], p90, p90 - tmp, day->calcPercentileLabel(CPAP_Pressure), calcitems[2].color)); - tmp += p90 - tmp; - } - slices.append(SummaryChartSlice(&calcitems[4], max, max - tmp, schema::channel[CPAP_PressureMax].label(), calcitems[4].color)); - - } else if (mode == MODE_BILEVEL_FIXED) { - float epap = day->settings_max(CPAP_EPAP); - float ipap = day->settings_max(CPAP_IPAP); - - slices.append(SummaryChartSlice(&calcitems[5], epap, epap, schema::channel[CPAP_EPAP].label(), calcitems[5].color)); - slices.append(SummaryChartSlice(&calcitems[6], ipap, ipap - epap, schema::channel[CPAP_IPAP].label(), calcitems[6].color)); - - } else if (mode == MODE_BILEVEL_AUTO_FIXED_PS) { - float epap = day->settings_max(CPAP_EPAPLo); - tmp = epap; - float ipap = day->settings_max(CPAP_IPAPHi); - - slices.append(SummaryChartSlice(&calcitems[7], epap, epap, schema::channel[CPAP_EPAPLo].label(), calcitems[7].color)); - if (!day->summaryOnly()) { - - float e50 = day->calcMiddle(CPAP_EPAP); - slices.append(SummaryChartSlice(&calcitems[9], e50, e50 - tmp, day->calcMiddleLabel(CPAP_EPAP), calcitems[9].color)); - tmp += e50 - tmp; - - float e90 = day->calcPercentile(CPAP_EPAP); - slices.append(SummaryChartSlice(&calcitems[10], e90, e90 - tmp, day->calcPercentileLabel(CPAP_EPAP), calcitems[10].color)); - tmp += e90 - tmp; - - float i50 = day->calcMiddle(CPAP_IPAP); - slices.append(SummaryChartSlice(&calcitems[11], i50, i50 - tmp, day->calcMiddleLabel(CPAP_IPAP), calcitems[11].color)); - tmp += i50 - tmp; - - float i90 = day->calcPercentile(CPAP_IPAP); - slices.append(SummaryChartSlice(&calcitems[12], i90, i90 - tmp, day->calcPercentileLabel(CPAP_IPAP), calcitems[12].color)); - tmp += i90 - tmp; - } - slices.append(SummaryChartSlice(&calcitems[8], ipap, ipap - tmp, schema::channel[CPAP_IPAPHi].label(), calcitems[8].color)); - } else if ((mode == MODE_BILEVEL_AUTO_VARIABLE_PS) || (mode == MODE_ASV_VARIABLE_EPAP)) { - float epap = day->settings_max(CPAP_EPAPLo); - tmp = epap; - - slices.append(SummaryChartSlice(&calcitems[7], epap, epap, schema::channel[CPAP_EPAPLo].label(), calcitems[7].color)); - if (!day->summaryOnly()) { - float e50 = day->calcMiddle(CPAP_EPAP); - slices.append(SummaryChartSlice(&calcitems[9], e50, e50 - tmp, day->calcMiddleLabel(CPAP_EPAP), calcitems[9].color)); - tmp += e50 - tmp; - - float e90 = day->calcPercentile(CPAP_EPAP); - slices.append(SummaryChartSlice(&calcitems[10], e90, e90 - tmp, day->calcPercentileLabel(CPAP_EPAP), calcitems[10].color)); - tmp += e90 - tmp; - - float i50 = day->calcMiddle(CPAP_IPAP); - slices.append(SummaryChartSlice(&calcitems[11], i50, i50 - tmp, day->calcMiddleLabel(CPAP_IPAP), calcitems[11].color)); - tmp += i50 - tmp; - - float i90 = day->calcPercentile(CPAP_IPAP); - slices.append(SummaryChartSlice(&calcitems[12], i90, i90 - tmp, day->calcPercentileLabel(CPAP_IPAP), calcitems[12].color)); - tmp += i90 - tmp; - } - float ipap = day->settings_max(CPAP_IPAPHi); - slices.append(SummaryChartSlice(&calcitems[8], ipap, ipap - tmp, schema::channel[CPAP_IPAPHi].label(), calcitems[8].color)); - } else if (mode == MODE_ASV) { - float epap = day->settings_max(CPAP_EPAP); - tmp = epap; - - slices.append(SummaryChartSlice(&calcitems[5], epap, epap, schema::channel[CPAP_EPAP].label(), calcitems[5].color)); - if (!day->summaryOnly()) { - float i50 = day->calcMiddle(CPAP_IPAP); - slices.append(SummaryChartSlice(&calcitems[11], i50, i50 - tmp, day->calcMiddleLabel(CPAP_IPAP), calcitems[11].color)); - tmp += i50 - tmp; - - float i90 = day->calcPercentile(CPAP_IPAP); - slices.append(SummaryChartSlice(&calcitems[12], i90, i90 - tmp, day->calcPercentileLabel(CPAP_IPAP), calcitems[12].color)); - tmp += i90 - tmp; - } - float ipap = day->settings_max(CPAP_IPAPHi); - slices.append(SummaryChartSlice(&calcitems[8], ipap, ipap - tmp, schema::channel[CPAP_IPAPHi].label(), calcitems[8].color)); - } - -} - -//void gPressureChart::afterDraw(QPainter &painter, gGraph &graph, QRect rect) -//{ -//} - diff --git a/oscar/Graphs/gSessionTimesChart.h b/oscar/Graphs/gSessionTimesChart.h index 5e56c0d0..bdd8bcc8 100644 --- a/oscar/Graphs/gSessionTimesChart.h +++ b/oscar/Graphs/gSessionTimesChart.h @@ -1,5 +1,6 @@ -/* gSessionTimesChart Header +/* gSessionTimesChart Header * + * Copyright (c) 2020 The Oscar Team * Copyright (C) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public @@ -188,13 +189,8 @@ public: cache.clear(); } - - void addCalc(ChannelID code, SummaryType type, QColor color) { - calcitems.append(SummaryCalcItem(code, type, color)); - } - void addCalc(ChannelID code, SummaryType type) { - calcitems.append(SummaryCalcItem(code, type, schema::channel[code].defaultColor())); - } + virtual int addCalc(ChannelID code, SummaryType type, QColor color); + virtual int addCalc(ChannelID code, SummaryType type); virtual Layer * Clone() { gSummaryChart * sc = new gSummaryChart(m_label, m_machtype); @@ -429,37 +425,4 @@ public: }; -class gPressureChart : public gSummaryChart -{ -public: - gPressureChart(); - virtual ~gPressureChart() {} - - virtual Layer * Clone() { - gPressureChart * sc = new gPressureChart(); - gSummaryChart::CloneInto(sc); - return sc; - } - -// virtual void preCalc(); - virtual void customCalc(Day *day, QVector &slices) { - int size = slices.size(); - float hour = day->hours(m_machtype); - for (int i=0; i < size; ++i) { - SummaryChartSlice & slice = slices[i]; - SummaryCalcItem * calc = slices[i].calc; - - calc->update(slice.value, hour); - } - } - virtual void afterDraw(QPainter &, gGraph &, QRectF); - - virtual void populate(Day * day, int idx); - - virtual QString tooltipData(Day * day, int idx) { - return day->getCPAPModeStr() + "\n" + day->getPressureSettings() + gSummaryChart::tooltipData(day, idx); - } - -}; - #endif // GSESSIONTIMESCHART_H diff --git a/oscar/SleepLib/loader_plugins/edfparser.cpp b/oscar/SleepLib/loader_plugins/edfparser.cpp index 6d21fb9a..7a1b0452 100644 --- a/oscar/SleepLib/loader_plugins/edfparser.cpp +++ b/oscar/SleepLib/loader_plugins/edfparser.cpp @@ -134,7 +134,7 @@ bool EDFInfo::parseHeader( EDFHeaderRaw *hdrPtr ) } edfHdr.reserved44=QString::fromLatin1(hdrPtr->reserved, 44).trimmed(); edfHdr.num_data_records = QString::fromLatin1(hdrPtr->num_data_records, 8).toLong(&ok); - if (!ok) { + if ( (! ok) || (edfHdr.num_data_records < 1) ) { qWarning() << "EDFInfo::Parse() Bad data record count " << filename; // sleep(1); fileData.clear(); @@ -148,7 +148,7 @@ bool EDFInfo::parseHeader( EDFHeaderRaw *hdrPtr ) return false; } edfHdr.num_signals = QString::fromLatin1(hdrPtr->num_signals, 4).toLong(&ok); - if (!ok) { + if ( (! ok) || (edfHdr.num_signals < 1) || (edfHdr.num_signals > 256) ) { qWarning() << "EDFInfo::Parse() Bad number of signals " << filename; // sleep(1); fileData.clear(); diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 61488972..809c3976 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -982,12 +982,9 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin // All samples exhibiting this behavior are DreamStations. task->m_wavefiles.append(fi.canonicalFilePath()); } else if (ext == 6) { - if (!task->oxifile.isEmpty()) { - qDebug() << sid << "already has oximetry file" << relativePath(task->oxifile) - << "skipping" << relativePath(fi.canonicalFilePath()); - continue; - } - task->oxifile = fi.canonicalFilePath(); + // Oximetry data can also be split into multiple files, see waveform + // comment above. + task->m_oxifiles.append(fi.canonicalFilePath()); } continue; @@ -6854,12 +6851,16 @@ bool PRS1DataChunk::ParseSettingsF0V6(const unsigned char* data, int size) CHECK_VALUES(data[pos], 1, 2); // 1 when EZ-Start is enabled? 2 when Auto-Trial? 3 when Auto-Trial is off or Opti-Start isn't off? } if (len == 2) { // 400G, 500G has extra byte - // 0x80 seen with EZ-Start and CPAP-Check+ on 500X150 - if (data[pos+1] != 0x80) { - // 0x10 seen with EZ-Start enabled, Opti-Start off on 500X110 - // 0x20 seen with Opti-Start enabled - // 0x30 seen with both Opti-Start and EZ-Start enabled on 500X110 - CHECK_VALUE(data[pos+1] & ~(0x10 | 0x20), 0); + switch (data[pos+1]) { + case 0x00: // 0x00 seen with EZ-Start disabled, no auto-trial, with CPAP-Check on 400X110 + case 0x10: // 0x10 seen with EZ-Start enabled, Opti-Start off on 500X110 + case 0x20: // 0x20 seen with Opti-Start enabled + case 0x30: // 0x30 seen with both Opti-Start and EZ-Start enabled on 500X110 + case 0x40: // 0x40 seen with Auto-Trial + case 0x80: // 0x80 seen with EZ-Start and CPAP-Check+ on 500X150 + break; + default: + UNEXPECTED_VALUE(data[pos+1], "[0,0x10,0x20,0x30,0x40,0x80]") } } break; @@ -6919,7 +6920,9 @@ bool PRS1DataChunk::ParseSettingsF0V6(const unsigned char* data, int size) // but curiously report the use of C-Flex+, even though Auto-CPAP uses A-Flex. CHECK_VALUE(len, 3); CHECK_VALUES(cpapmode, PRS1_MODE_CPAP, PRS1_MODE_CPAPCHECK); - CHECK_VALUES(data[pos], 30, 5); // Auto-Trial Duration + if (data[pos] != 30) { + CHECK_VALUES(data[pos], 5, 25); // Auto-Trial Duration + } this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_TRIAL, data[pos])); // If we want C-Flex+ to be reported as A-Flex, we can set cpapmode = PRS1_MODE_AUTOTRIAL here. // (Note that the setting event has already been added above, which is why ImportSummary needs @@ -7131,7 +7134,7 @@ bool PRS1DataChunk::ParseSummaryF0V6(void) qWarning() << this->sessionid << "summary data too short:" << chunk_size; return false; } - if (chunk_size < 60) UNEXPECTED_VALUE(chunk_size, ">= 60"); + if (chunk_size < 59) UNEXPECTED_VALUE(chunk_size, ">= 59"); bool ok = true; int pos = 0; @@ -8152,7 +8155,7 @@ QList PRS1Import::CoalesceWaveformChunks(QList } -void PRS1Import::ParseOximetry() +void PRS1Import::ImportOximetry() { int size = oximetry.size(); @@ -8208,10 +8211,10 @@ void PRS1Import::ImportOximetryChannel(ChannelID channel, QByteArray & data, qui quint64 start_ti; int start_i; - // Split eventlist on invalid values (255) + // Split eventlist on invalid values (254-255) for (int i=0; i < data.size(); i++) { unsigned char value = raw[i]; - bool valid = (value != 255); + bool valid = (value < 254); if (valid) { if (pending_samples == false) { @@ -8221,7 +8224,7 @@ void PRS1Import::ImportOximetryChannel(ChannelID channel, QByteArray & data, qui } if (channel == OXI_Pulse) { - if (value > 200) UNEXPECTED_VALUE(value, "<= 200 bpm"); + if (value > 240) UNEXPECTED_VALUE(value, "<= 240 bpm"); } else { if (value > 100) UNEXPECTED_VALUE(value, "<= 100%"); } @@ -8245,7 +8248,7 @@ void PRS1Import::ImportOximetryChannel(ChannelID channel, QByteArray & data, qui } -void PRS1Import::ParseWaveforms() +void PRS1Import::ImportWaveforms() { int size = waveforms.size(); quint64 s1, s2; @@ -8413,7 +8416,7 @@ bool PRS1Import::ParseSession(void) // If are no mask-on slices, then there's not any meaningful event or waveform data for the session. // If there's no no event or waveform data, mark this session as a summary. - if (session->m_slices.count() == 0 || (m_event_chunks.count() == 0 && m_wavefiles.isEmpty() && oxifile.isEmpty())) { + if (session->m_slices.count() == 0 || (m_event_chunks.count() == 0 && m_wavefiles.isEmpty() && m_oxifiles.isEmpty())) { session->setSummaryOnly(true); save = true; break; // and skip the occasional fragmentary event or waveform data @@ -8430,14 +8433,18 @@ bool PRS1Import::ParseSession(void) if (!m_wavefiles.isEmpty()) { // Parse .005 Waveform files + waveforms = ReadWaveformData(m_wavefiles, "Waveform"); + + // Extract and import raw data into channels. ImportWaveforms(); } - if (!oxifile.isEmpty()) { - // Parse .006 Waveform file - oximetry = loader->ParseFile(oxifile); - oximetry = CoalesceWaveformChunks(oximetry); - ParseOximetry(); + if (!m_oxifiles.isEmpty()) { + // Parse .006 Waveform files + oximetry = ReadWaveformData(m_oxifiles, "Oximetry"); + + // Extract and import raw data into channels. + ImportOximetry(); } save = true; @@ -8447,16 +8454,17 @@ bool PRS1Import::ParseSession(void) } -void PRS1Import::ImportWaveforms() +QList PRS1Import::ReadWaveformData(QList & files, const char* label) { QMap waveform_chunks; + QList result; - if (m_wavefiles.count() > 1) { - qDebug() << session->session() << "Waveform data split across multiple files"; + if (files.count() > 1) { + qDebug() << session->session() << label << "data split across multiple files"; } - for (auto & f : m_wavefiles) { - // Parse a single .005 Waveform file + for (auto & f : files) { + // Parse a single .005 or .006 waveform file QList file_chunks = loader->ParseFile(f); for (auto & chunk : file_chunks) { PRS1DataChunk* previous = waveform_chunks[chunk->timestamp]; @@ -8471,21 +8479,12 @@ void PRS1Import::ImportWaveforms() } // Get the list of pointers sorted by timestamp. - waveforms = waveform_chunks.values(); + result = waveform_chunks.values(); // Coalesce contiguous waveform chunks into larger chunks. - waveforms = CoalesceWaveformChunks(waveforms); + result = CoalesceWaveformChunks(result); - if (session->eventlist.contains(CPAP_FlowRate)) { - if (waveforms.size() > 0) { - // Delete anything called "Flow rate" picked up in the events file if real data is present - qWarning() << session->session() << "Deleting flow rate events due to flow rate waveform data"; - session->destroyEvent(CPAP_FlowRate); - } - } - - // Extract raw data into channels. - ParseWaveforms(); + return result; } diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index f6469895..d44d7f4a 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -295,7 +295,7 @@ public: QList m_wavefiles; - QString oxifile; + QList m_oxifiles; //! \brief Imports .000 files for bricks. bool ImportCompliance(); @@ -306,17 +306,17 @@ public: //! \brief Imports the .002 event file(s). bool ImportEvents(); - //! \brief Imports the .005 event file(s). - void ImportWaveforms(); + //! \brief Reads the .005 or .006 waveform file(s). + QList ReadWaveformData(QList & files, const char* label); //! \brief Coalesce contiguous .005 or .006 waveform chunks from the file into larger chunks for import. QList CoalesceWaveformChunks(QList & allchunks); //! \brief Takes the parsed list of Flow/MaskPressure waveform chunks and adds them to the database - void ParseWaveforms(); + void ImportWaveforms(); //! \brief Takes the parsed list of oximeter waveform chunks and adds them to the database. - void ParseOximetry(); + void ImportOximetry(); //! \brief Adds a single channel of continuous oximetry data to the database, splitting on any missing samples. void ImportOximetryChannel(ChannelID channel, QByteArray & data, quint64 ti, qint64 dur); diff --git a/oscar/SleepLib/loader_plugins/resmed_EDFinfo.cpp b/oscar/SleepLib/loader_plugins/resmed_EDFinfo.cpp index f0571530..9f6f018c 100644 --- a/oscar/SleepLib/loader_plugins/resmed_EDFinfo.cpp +++ b/oscar/SleepLib/loader_plugins/resmed_EDFinfo.cpp @@ -30,7 +30,11 @@ ResMedEDFInfo::~ResMedEDFInfo() { } bool ResMedEDFInfo::Parse( ) // overrides and calls the super's Parse { - EDFInfo::Parse( ); + if ( ! EDFInfo::Parse( ) ) { + qWarning() << "EDFInfo::Parse failed!"; + sleep(1); + return false; + } // Now massage some stuff into OSCAR's layout int snp = edfHdr.recordingident.indexOf("SRN="); diff --git a/oscar/SleepLib/loader_plugins/resmed_EDFinfo.h b/oscar/SleepLib/loader_plugins/resmed_EDFinfo.h index 107f8adf..02a9f9f4 100644 --- a/oscar/SleepLib/loader_plugins/resmed_EDFinfo.h +++ b/oscar/SleepLib/loader_plugins/resmed_EDFinfo.h @@ -229,14 +229,15 @@ class STRFile { public: STRFile() : - filename(QString()), edf(nullptr) {} - STRFile(QString name, ResMedEDFInfo *str) : - filename(name), edf(str) {} + filename(QString()), days(0), edf(nullptr) {} + STRFile(QString name, long int recCnt, ResMedEDFInfo *str) : + filename(name), days(recCnt), edf(str) {} STRFile(const STRFile & copy) = default; virtual ~STRFile() {} - QString filename; + QString filename; + long int days; ResMedEDFInfo * edf; }; diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.cpp b/oscar/SleepLib/loader_plugins/resmed_loader.cpp index 90bec3f4..7a5dcc05 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.cpp +++ b/oscar/SleepLib/loader_plugins/resmed_loader.cpp @@ -284,14 +284,17 @@ int ResmedLoader::Open(const QString & dirpath) // Check DATALOG folder exists and is readable if (!QDir().exists(datalogPath)) { + qDebug() << "Missing DATALOG in" << dirpath; return -1; } m_abort = false; MachineInfo info = newInfo(); - if ( ! parseIdentTGT(path, & info, idmap) ) + if ( ! parseIdentTGT(path, & info, idmap) ) { + qDebug() << "Failed to parse Identification.tgt"; return -1; + } qDebug() << "Info:" << info.series << info.model << info.modelnumber << info.serial; #ifdef IDENT_DEBUG @@ -303,7 +306,7 @@ int ResmedLoader::Open(const QString & dirpath) // Abort if no serial number if (info.serial.isEmpty()) { - qDebug() << "ResMed Data card has no valid serial number in Indentification.tgt"; + qDebug() << "ResMed Data card is missing serial number in Indentification.tgt"; return -1; } @@ -383,9 +386,34 @@ int ResmedLoader::Open(const QString & dirpath) if ( ! dir.exists(strBackupPath)) dir.mkpath(strBackupPath); + QString newpath = backup_path + "DATALOG"; + if ( ! dir.exists(newpath) ) + dir.mkpath(newpath); + if ( ! importing_backups ) { BackupSTRfiles( strpath, path, strBackupPath, info, STRmap ); - } // end if not importing the backup files + } else { // get the STR file that is in the BACKUP folder + ResMedEDFInfo * stredf = new ResMedEDFInfo(); + if ( stredf->Open(strpath) ) { + if ( stredf->Parse()) { + if (stredf->serialnumber != info.serial) { + qDebug() << "Identification.tgt Serial number doesn't match" << strpath; + delete stredf; + } else { // passed the tests, stuff it into the map + QDate date = stredf->edfHdr.startdate_orig.date(); + long int days = stredf->GetNumDataRecords(); + qDebug() << strpath << "starts at" << date << "for" << days; + STRmap[date] = STRFile(strpath, days, stredf); + } + } else { + qDebug() << "Faulty STR file" << strpath; + delete stredf; + } + } else { + qDebug() << "Failed to open" << strpath; + delete stredf; + } + } // end if not importing the backup files #ifdef STR_DEBUG qDebug() << "STRmap size is " << STRmap.size(); #endif @@ -396,6 +424,7 @@ int ResmedLoader::Open(const QString & dirpath) dir.setFilter(QDir::Files | QDir::Hidden | QDir::Readable); QFileInfoList flist = dir.entryInfoList(); QDate date; + long int days; #ifdef STR_DEBUG qDebug() << "STR_Backup folder size is " << flist.size(); #endif @@ -407,11 +436,13 @@ int ResmedLoader::Open(const QString & dirpath) continue; if (!(filename.endsWith("edf.gz", Qt::CaseInsensitive) || filename.endsWith("edf", Qt::CaseInsensitive))) continue; - QString datestr = filename.section("STR-",-1).section(".edf",0,0)+"01"; - date = QDate().fromString(datestr,"yyyyMMdd"); - - if (STRmap.contains(date)) - continue; + QString datestr = filename.section("STR-",-1).section(".edf",0,0); // +"01"; +// date = QDate().fromString(datestr,"yyyyMMdd"); +// +// if (STRmap.contains(date)) { +// qDebug() << filename << "overlaps anothor STR file"; +// continue; +// } ResMedEDFInfo * stredf = new ResMedEDFInfo(); if ( ! stredf->Open(fi.canonicalFilePath() ) ) { @@ -433,10 +464,20 @@ int ResmedLoader::Open(const QString & dirpath) // Don't trust the filename date, pick the one inside the STR... date = stredf->edfHdr.startdate_orig.date(); - qDebug() << "Resetting STR date from" << date.toString() << "to first of month ... WHY???"; - date = QDate(date.year(), date.month(), 1); + days = stredf->GetNumDataRecords(); + if (STRmap.contains(date)) { // Keep the longer of the two STR files + qDebug() << filename << "overlaps" << STRmap[date].filename << "for" << days; + if (days <= STRmap[date].days) { + qDebug() << "Skipping" << filename; + delete stredf; + continue; + } + } +// qDebug() << "Resetting STR date from" << date.toString() << "to first of month ... WHY???"; +// date = QDate(date.year(), date.month(), 1); - STRmap[date] = STRFile(fi.canonicalFilePath(), stredf); + qDebug() << fi.canonicalFilePath() << "starts at" << date << "for" << days; + STRmap[date] = STRFile(fi.canonicalFilePath(), days, stredf); } // end for walking the STR_Backup directory #ifdef STR_DEBUG qDebug() << "STRmap size is now " << STRmap.size(); @@ -453,9 +494,21 @@ int ResmedLoader::Open(const QString & dirpath) // We are done with the Parsed STR EDF objects, so delete them for (auto it=STRmap.begin(), end=STRmap.end(); it != end; ++it) { + QString fullname = it.value().filename; #ifdef STR_DEBUG - qDebug() << "Deleting edf of" << it.value().filename; + qDebug() << "Deleting edf of" << fullname; #endif + QString datepart = fullname.section("STR-",-1).section(".edf",0,0); + if (datepart.size() == 6 ) { // old style name, change to full date + QFile str(fullname); + QString newdate = it.key().toString("yyyyMMdd"); + QString newName = fullname.replace(datepart, newdate); + qDebug() << "Renaming" << it.value().filename << "to" << newName; + if ( str.rename(newName) ) + qDebug() << "Success"; + else + qDebug() << "Failed"; + } delete it.value().edf; } #ifdef STR_DEBUG @@ -531,6 +584,8 @@ int ResmedLoader::Open(const QString & dirpath) sessionCount = 0; emit updateMessage(QObject::tr("Importing Sessions...")); + + // Walk down the resDay list runTasks(); int num_new_sessions = sessionCount; @@ -683,7 +738,7 @@ int ResmedLoader::ScanFiles(Machine * mach, const QString & datalog_path, QDate filename = fi.fileName(); int len = filename.length(); - if (len == 4) { // when does this happen? + if (len == 4) { // This is a year folder in BackupDATALOG filename.toInt(&ok); if ( ! ok ) { qDebug() << "Skipping directory - bad 4-letter name" << filename; @@ -836,12 +891,13 @@ QString ResmedLoader::Backup(const QString & fullname, const QString & backup_pa yearstr.toInt(&ok, 10); if (!ok) { - qDebug() << "Invalid EDF filename given to ResMedLoader::backup()" << fullname; + qDebug() << "Invalid EDF filename given to ResMedLoader::Backup()" << fullname; return ""; } - QString newpath = backup_path + RMS9_STR_datalog + "/" + yearstr; - !dir.exists(newpath) && dir.mkpath(newpath); + QString newpath = backup_path + "DATALOG" + "/" + yearstr; + if ( ! dir.exists(newpath) ) + dir.mkpath(newpath); newname = newpath+"/"+filename; @@ -894,10 +950,11 @@ bool ResmedLoader::ProcessSTRfiles(Machine *mach, QMap & STRmap, for (auto it=STRmap.begin(), end=STRmap.end(); it != end; ++it) { STRFile & file = it.value(); ResMedEDFInfo & str = *file.edf; - totalRecs += str.GetNumDataRecords(); + int days = str.GetNumDataRecords(); + totalRecs += days; #ifdef STR_DEBUG qDebug() << "STR file is" << file.filename; - qDebug() << "First day" << QDateTime::fromMSecsSinceEpoch(str.startdate, EDFInfo::localNoDST).date().toString() << "for" << totalRecs << "days"; + qDebug() << "First day" << QDateTime::fromMSecsSinceEpoch(str.startdate, EDFInfo::localNoDST).date().toString() << "for" << days << "days"; #endif } @@ -917,7 +974,7 @@ bool ResmedLoader::ProcessSTRfiles(Machine *mach, QMap & STRmap, QDate lastDay = date.addDays(size-1); #ifdef STR_DEBUG - qDebug() << "Parsing" << strfile << date.toString() << size << str.GetNumSignals(); + qDebug() << "Processing" << strfile << date.toString() << size << str.GetNumSignals(); qDebug() << "Last day is" << lastDay; #endif @@ -1383,6 +1440,9 @@ bool ResmedLoader::ProcessSTRfiles(Machine *mach, QMap & STRmap, qDebug() << "Finished" << date.toString(); #endif } +#ifdef STR_DEBUG + qDebug() << "Finished" << strfile; +#endif } #ifdef STR_DEBUG qDebug() << "Finished ProcessSTR"; @@ -1462,13 +1522,17 @@ void BackupSTRfiles( const QString strpath, const QString path, const QString st continue; } QDate date = stredf->edfHdr.startdate_orig.date(); - date = QDate(date.year(), date.month(), 1); + long int days = stredf->GetNumDataRecords(); +// date = QDate(date.year(), date.month(), 1); if (STRmap.contains(date)) { qDebug() << "STRmap already contains" << date.toString("YYYY-MM-dd"); - delete stredf; - continue; + if ( days <= STRmap[date].days ) { + qDebug() << "Skipping" << filename; + delete stredf; + continue; + } } - QString newname = "STR-"+date.toString("yyyyMM")+"."+STR_ext_EDF; + QString newname = "STR-"+date.toString("yyyyMMdd")+"."+STR_ext_EDF; QString backupfile = strBackupPath+"/"+newname; @@ -1502,7 +1566,7 @@ void BackupSTRfiles( const QString strpath, const QString path, const QString st else QFile::exists(gzfile) && QFile::remove(gzfile); - STRmap[date] = STRFile(backupfile, stredf); + STRmap[date] = STRFile(backupfile, days, stredf); } // end for walking the STR files list #ifdef STR_DEBUG qDebug() << "STRmap has" << STRmap.size() << "entries"; diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.h b/oscar/SleepLib/loader_plugins/resmed_loader.h index 35802e4b..ac8c82de 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.h +++ b/oscar/SleepLib/loader_plugins/resmed_loader.h @@ -22,7 +22,7 @@ //******************************************************************************************** // Please INCREMENT the following value when making changes to this loaders implementation. // -const int resmed_data_version = 13; +const int resmed_data_version = 14; // //******************************************************************************************** diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.cpp b/oscar/SleepLib/loader_plugins/viatom_loader.cpp index c784a779..1f61a8c3 100644 --- a/oscar/SleepLib/loader_plugins/viatom_loader.cpp +++ b/oscar/SleepLib/loader_plugins/viatom_loader.cpp @@ -303,11 +303,8 @@ bool ViatomFile::ParseHeader() //int spo2_avg = header[17]; //int spo2_min = header[18]; //int spo2_3pct = header[19]; // number of events - int spo2_4pct = header[20]; // number of events - if (header[21] > spo2_4pct) { - //CHECK_VALUE(header[21], 0); // sometimes nonzero; maybe spo2_5pct or something like that? - UNEXPECTED_VALUE(header[21], "< drops over 4%"); - } + //int spo2_4pct = header[20]; // number of events + //CHECK_VALUE(header[21], 0); // ??? sometimes nonzero; maybe pulse spike, not a threshold of SpO2 or pulse, not always smaller than spo2_4pct //int time_under_90pct = header[22] | (header[23] << 8); // in seconds //int events_under_90pct = header[24]; // number of distinct events //float o2_score = header[25] * 0.1; diff --git a/oscar/SleepLib/profiles.cpp b/oscar/SleepLib/profiles.cpp index fd2e52c6..e0d855fd 100644 --- a/oscar/SleepLib/profiles.cpp +++ b/oscar/SleepLib/profiles.cpp @@ -566,11 +566,12 @@ void Profile::DataFormatError(Machine *m) // Note: I deliberately haven't added a Profile help for this if (backups) { MachineLoader * loader = lookupLoader(m); - /* int c = */ - mainwin->importCPAP(ImportPath(m->getBackupPath(), loader), - QObject::tr("Rebuilding from %1 Backup").arg(m->brand())); -// if ( c > 0 ) -// m->info.version = loader->Version(); + int c = mainwin->importCPAP(ImportPath(m->getBackupPath(), loader), + QObject::tr("Rebuilding from %1 Backup").arg(m->brand())); + if (c >= 0) { + // Make sure the updated version gets saved, even if there were no sessions to import. + mainwin->finishCPAPImport(); + } } else { if (!p_profile->session->backupCardData()) { // Automatic backups not available for Intellipap users yet, so don't taunt them.. diff --git a/oscar/SleepLib/session.h b/oscar/SleepLib/session.h index 80a167a5..4350dbdc 100644 --- a/oscar/SleepLib/session.h +++ b/oscar/SleepLib/session.h @@ -194,6 +194,7 @@ class Session t += slice.end - slice.start; } } + t = t / 3600000.0; } return t; } diff --git a/oscar/exportcsv.cpp b/oscar/exportcsv.cpp index 635922cc..4c5e6a32 100644 --- a/oscar/exportcsv.cpp +++ b/oscar/exportcsv.cpp @@ -1,5 +1,6 @@ -/* ExportCSV module implementation +/* ExportCSV module implementation * + * Copyright (c) 2020 The OSCAR Team * Copyright (c) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public @@ -180,24 +181,16 @@ void ExportCSV::on_exportButton_clicked() countlist.append(CPAP_UserFlag2); countlist.append(CPAP_PressurePulse); - avglist.append(CPAP_Pressure); - avglist.append(CPAP_IPAP); - avglist.append(CPAP_EPAP); - avglist.append(CPAP_FLG); // Pholynyk, 25Aug2015, add ResMed Flow Limitation + QVector statChannels = { CPAP_Pressure, CPAP_PressureSet, CPAP_IPAP, CPAP_IPAPSet, CPAP_EPAP, CPAP_EPAPSet, CPAP_FLG }; + for (auto & chan : statChannels) { + avglist.append(chan); + p90list.append(chan); + maxlist.append(chan); + } - p90list.append(CPAP_Pressure); - p90list.append(CPAP_IPAP); - p90list.append(CPAP_EPAP); - p90list.append(CPAP_FLG); - float percentile=p_profile->general->prefCalcPercentile()/100.0; // Pholynyk, 18Aug2015 EventDataType percent = percentile; // was 0.90F - maxlist.append(CPAP_Pressure); // Pholynyk, 18Aug2015, add maximums - maxlist.append(CPAP_IPAP); - maxlist.append(CPAP_EPAP); - maxlist.append(CPAP_FLG); - // Not sure this section should be translateable.. :-/ if (ui->rb1_details->isChecked()) { header = tr("DateTime") + sep + tr("Session") + sep + tr("Event") + sep + tr("Data/Duration"); diff --git a/oscar/logger.cpp b/oscar/logger.cpp index b57152ce..986b5c40 100644 --- a/oscar/logger.cpp +++ b/oscar/logger.cpp @@ -72,7 +72,7 @@ void initializeLogger() s_LoggerRunning.lock(); // wait until the thread begins running s_LoggerRunning.unlock(); // we no longer need the lock } - qInstallMessageHandler(MyOutputHandler); + qInstallMessageHandler(MyOutputHandler); // NOTE: comment this line out when debugging a crash, otherwise the deferred output will mislead you. if (b) { qDebug() << "Started logging thread"; } else { diff --git a/oscar/main.cpp b/oscar/main.cpp index a3a11682..5d37cc37 100644 --- a/oscar/main.cpp +++ b/oscar/main.cpp @@ -196,11 +196,11 @@ bool migrateFromSH(QString destDir) { if (!file.exists() || !dirP.exists()) { // It doesn't have a Preferences.xml file or a Profiles directory in it // Not a new directory.. nag the user. - if (QMessageBox::warning(nullptr, STR_MessageBox_Error, - QObject::tr("The folder you chose does not contain valid SleepyHead data.") + - "\n\n"+QObject::tr("You cannot use this folder:")+" " + datadir ), QMessageBox::Ok) { - continue; // Nope, don't use it, go around the loop again - } + QMessageBox::warning(nullptr, STR_MessageBox_Error, + QObject::tr("The folder you chose does not contain valid SleepyHead data.") + + "\n\n"+QObject::tr("You cannot use this folder:")+" " + datadir, + QMessageBox::Ok); + continue; // Nope, don't use it, go around the loop again } qDebug() << "Migration folder is" << datadir; diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index 369e2199..82030cfa 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -743,7 +743,8 @@ void MainWindow::finishCPAPImport() GenerateStatistics(); profileSelector->updateProfileList(); - welcome->refreshPage(); + if (welcome) + welcome->refreshPage(); if (overview) { overview->ReloadGraphs(); } if (daily) { @@ -2504,10 +2505,17 @@ void MainWindow::on_actionImport_Viatom_Data_triggered() #if defined(Q_OS_WIN) // Windows can't handle this name filter. w.setOption(QFileDialog::DontUseNativeDialog, true); + // And since the non-native dialog can't select both directories and files, + // it needs the following to enable selecting multiple files. + w.setFileMode(QFileDialog::ExistingFiles); #endif if (w.exec() == QFileDialog::Accepted) { QString filename = w.selectedFiles()[0]; + if (w.selectedFiles().size() > 1) { + // The user selected multiple files in a directory, so use the parent directory as the filename. + filename = QFileInfo(filename).absoluteDir().canonicalPath(); + } int c = viatom.Open(filename); if (c > 0) { diff --git a/oscar/mainwindow.h b/oscar/mainwindow.h index 37592e17..ee2444ba 100644 --- a/oscar/mainwindow.h +++ b/oscar/mainwindow.h @@ -164,6 +164,7 @@ class MainWindow : public QMainWindow void setStatsHTML(QString html); int importCPAP(ImportPath import, const QString &message); + void finishCPAPImport(); void startImportDialog() { on_action_Import_Data_triggered(); } @@ -365,7 +366,6 @@ class MainWindow : public QMainWindow private: QString getMainWindowTitle(); void importCPAPBackups(); - void finishCPAPImport(); QList detectCPAPCards(); QList selectCPAPDataCards(const QString & prompt); void importCPAPDataCards(const QList & datacards); diff --git a/oscar/oscar.pro b/oscar/oscar.pro index a021d135..be134f98 100644 --- a/oscar/oscar.pro +++ b/oscar/oscar.pro @@ -305,6 +305,7 @@ SOURCES += \ SleepLib/serialoximeter.cpp \ SleepLib/loader_plugins/md300w1_loader.cpp \ Graphs/gSessionTimesChart.cpp \ + Graphs/gPressureChart.cpp \ logger.cpp \ SleepLib/machine_common.cpp \ SleepLib/loader_plugins/weinmann_loader.cpp \ @@ -383,6 +384,7 @@ HEADERS += \ SleepLib/serialoximeter.h \ SleepLib/loader_plugins/md300w1_loader.h \ Graphs/gSessionTimesChart.h \ + Graphs/gPressureChart.h \ logger.h \ SleepLib/loader_plugins/weinmann_loader.h \ Graphs/gdailysummary.h \ diff --git a/oscar/overview.cpp b/oscar/overview.cpp index 9cc986a0..9dc2d8e9 100644 --- a/oscar/overview.cpp +++ b/oscar/overview.cpp @@ -1,5 +1,6 @@ -/* Overview GUI Implementation +/* Overview GUI Implementation * + * Copyright (c) 2020 The Oscar Team * Copyright (c) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public @@ -23,6 +24,7 @@ #include "Graphs/gXAxis.h" #include "Graphs/gLineChart.h" #include "Graphs/gYAxis.h" +#include "Graphs/gPressureChart.h" #include "cprogressbar.h" #include "mainwindow.h"