From 562cd9cc37af2e997533f00a4b095b75c98ac2fe Mon Sep 17 00:00:00 2001
From: LoudSnorer <Just4Me.1947@gmail.com>
Date: Sun, 21 Mar 2021 14:15:47 -0400
Subject: [PATCH 01/20] fix display issues for short span events

---
 oscar/Graphs/gFlagsLine.cpp   | 16 +++++++++-------
 oscar/Graphs/gLineOverlay.cpp | 19 ++++++++++++++-----
 2 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/oscar/Graphs/gFlagsLine.cpp b/oscar/Graphs/gFlagsLine.cpp
index 50f39e37..f4738c1f 100644
--- a/oscar/Graphs/gFlagsLine.cpp
+++ b/oscar/Graphs/gFlagsLine.cpp
@@ -366,23 +366,25 @@ void gFlagsLine::paint(QPainter &painter, gGraph &w, const QRegion &region)
 
                     x1 = double(X - minx) * xmult + left;
                     x2 = double(X2 - minx) * xmult + left;
+                    int width = x1-x2;
+                    width = qMax(2,width);    // Insure Rectangle will be visable. Flag events are 2 pixels wide.
 
                     brush = QBrush(color);
-                    painter.fillRect(x2, bartop, x1-x2, bottom-bartop, brush);
-                    if (!w.selectingArea() && !hover && QRect(x2, bartop, x1-x2, bottom-bartop).contains(w.graphView()->currentMousePos())) {
+                    painter.fillRect(x2, bartop, width, bottom-bartop, brush);
+                    if (!w.selectingArea() && !hover && QRect(x2, bartop, width , bottom-bartop).contains(w.graphView()->currentMousePos())) {
                         hover = true;
                         painter.setPen(QPen(Qt::red,1));
 
-                        painter.drawRect(x2, bartop, x1-x2, bottom-bartop);
+                        painter.drawRect(x2, bartop, width, bottom-bartop);
                         int x,y;
-                        int s = *dptr;
-                        int m = s / 60;
-                        s %= 60;
+                        double s = *dptr;
+                        double m;
+                        s=60*modf(s/60,&m);
                         QString lab = QString("%1").arg(schema::channel[m_code].fullname());
                         if (m>0) {
                             lab += QObject::tr(" (%2 min, %3 sec)").arg(m).arg(s);
                         } else {
-                            lab += QObject::tr(" (%3 sec)").arg(m).arg(s);
+                            lab += QObject::tr(" (%3 sec)").arg(s);
                         }
                         GetTextExtent(lab, x, y);
                         w.ToolTip(lab, x2 - 10, bartop + (3 * w.printScaleY()), TT_AlignRight, tooltipTimeout);
diff --git a/oscar/Graphs/gLineOverlay.cpp b/oscar/Graphs/gLineOverlay.cpp
index b6e1484f..ce42c37c 100644
--- a/oscar/Graphs/gLineOverlay.cpp
+++ b/oscar/Graphs/gLineOverlay.cpp
@@ -28,7 +28,6 @@ void gLineOverlayBar::paint(QPainter &painter, gGraph &w, const QRegion &region)
     if (!schema::channel[m_code].enabled())
         return;
 
-
     int left = region.boundingRect().left();
     int topp = region.boundingRect().top(); // FIXME: Misspelling intentional.
     double width = region.boundingRect().width();
@@ -42,10 +41,12 @@ void gLineOverlayBar::paint(QPainter &painter, gGraph &w, const QRegion &region)
 
     double xx = w.max_x - w.min_x;
     //double yy = w.max_y - w.min_y;
+
+    if (xx <= 0) { return; }
+
     double jj = width / xx;
 
 
-    if (xx <= 0) { return; }
 
     double x1, x2;
 
@@ -138,12 +139,20 @@ void gLineOverlayBar::paint(QPainter &painter, gGraph &w, const QRegion &region)
                     x1 = jj * double(X - w.min_x);
                     x2 = jj * double(Y - w.min_x);
 
-                    x2 += (int(x1)==int(x2)) ? 1 : 0;
-
                     x2 = qMax(0.0, x2)+left;
                     x1 = qMin(width, x1)+left;
 
-                    painter.fillRect(QRect(x2, start_py, x1-x2, height), brush);
+                    // x2 represents the begining of a span in pixels
+                    // x1 represent the end of the span in pixels
+                    // BUG HERE
+                    //x2 += (int(x1)==int(x2)) ? 1 : 0;
+                    // Fixed BY 
+                    int duration = x1-x2;
+                    if (duration<2) duration=2;     // display minial span with 2 pixels.
+                    x2 =x1-duration;
+
+                    painter.fillRect(QRect(x2, start_py, duration, height), brush);
+
                 }
             }/* else if (m_flt == FT_Dot) {
                 ////////////////////////////////////////////////////////////////////////////

From 2d502f6a6804c764b90a6a8197e35d7f08bc7130 Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Tue, 23 Mar 2021 09:42:47 +1100
Subject: [PATCH 02/20] Revert viatom data version to avoid purge

---
 oscar/SleepLib/loader_plugins/viatom_loader.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.h b/oscar/SleepLib/loader_plugins/viatom_loader.h
index 97697daf..7b326ac4 100644
--- a/oscar/SleepLib/loader_plugins/viatom_loader.h
+++ b/oscar/SleepLib/loader_plugins/viatom_loader.h
@@ -14,7 +14,7 @@
 #include "SleepLib/machine_loader.h"
 
 const QString viatom_class_name = "Viatom";
-const int viatom_data_version = 3; //CN increased from 2
+const int viatom_data_version = 2;
 
 
 /*! \class ViatomLoader

From 00ad97ff70184d56af7f6fde360943b8957f5c49 Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Tue, 23 Mar 2021 09:43:14 +1100
Subject: [PATCH 03/20] Update loader version change comment

---
 oscar/SleepLib/loader_plugins/cms50_loader.cpp     | 9 +++++----
 oscar/SleepLib/loader_plugins/cms50f37_loader.cpp  | 9 +++++----
 oscar/SleepLib/loader_plugins/dreem_loader.cpp     | 9 +++++----
 oscar/SleepLib/loader_plugins/somnopose_loader.cpp | 9 +++++----
 oscar/SleepLib/loader_plugins/viatom_loader.cpp    | 9 +++++----
 oscar/SleepLib/loader_plugins/zeo_loader.cpp       | 9 +++++----
 6 files changed, 30 insertions(+), 24 deletions(-)

diff --git a/oscar/SleepLib/loader_plugins/cms50_loader.cpp b/oscar/SleepLib/loader_plugins/cms50_loader.cpp
index 2d79cfb8..b7b89888 100644
--- a/oscar/SleepLib/loader_plugins/cms50_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/cms50_loader.cpp
@@ -7,10 +7,11 @@
  * for more details. */
 
 //********************************************************************************************
-/// IMPORTANT!!!
-//********************************************************************************************
-// Please INCREMENT the cms50_data_version in cms50_loader.h when making changes to this loader
-// that change loader behaviour or modify channels.
+// Please only INCREMENT the cms50_data_version in cms50_loader.h when making changes
+// that change loader behaviour or modify channels in a manner that fixes old data imports.
+// Note that changing the data version will require a reimport of existing data for which OSCAR
+// does not keep a backup - so it should be avoided if possible.
+// i.e. there is no need to change the version when adding support for new devices
 //********************************************************************************************
 
 #include <QApplication>
diff --git a/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp b/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp
index 59b02928..fa88bc38 100644
--- a/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/cms50f37_loader.cpp
@@ -7,10 +7,11 @@
  * for more details. */
 
 //********************************************************************************************
-/// IMPORTANT!!!
-//********************************************************************************************
-// Please INCREMENT the cms50_data_version in cms50_loader.h when making changes to this loader
-// that change loader behaviour or modify channels.
+// Please only INCREMENT the cms50f37_data_version in cms50f37_loader.h when making changes
+// that change loader behaviour or modify channels in a manner that fixes old data imports.
+// Note that changing the data version will require a reimport of existing data for which OSCAR
+// does not keep a backup - so it should be avoided if possible.
+// i.e. there is no need to change the version when adding support for new devices
 //********************************************************************************************
 
 // #include <QProgressBar>
diff --git a/oscar/SleepLib/loader_plugins/dreem_loader.cpp b/oscar/SleepLib/loader_plugins/dreem_loader.cpp
index 872dacca..a6c032d6 100644
--- a/oscar/SleepLib/loader_plugins/dreem_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/dreem_loader.cpp
@@ -7,10 +7,11 @@
  * for more details. */
 
 //********************************************************************************************
-// IMPORTANT!!!
-//********************************************************************************************
-// Please INCREMENT the dreem_data_version in dreem_loader.h when making changes to this loader
-// that change loader behaviour or modify channels.
+// Please only INCREMENT the dreem_data_version in dreem_loader.h when making changes
+// that change loader behaviour or modify channels in a manner that fixes old data imports.
+// Note that changing the data version will require a reimport of existing data for which OSCAR
+// does not keep a backup - so it should be avoided if possible.
+// i.e. there is no need to change the version when adding support for new devices
 //********************************************************************************************
 
 #include <QDir>
diff --git a/oscar/SleepLib/loader_plugins/somnopose_loader.cpp b/oscar/SleepLib/loader_plugins/somnopose_loader.cpp
index 605a3a89..40f45de0 100644
--- a/oscar/SleepLib/loader_plugins/somnopose_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/somnopose_loader.cpp
@@ -7,10 +7,11 @@
  * for more details. */
 
 //********************************************************************************************
-// IMPORTANT!!!
-//********************************************************************************************
-// Please INCREMENT the somnopose_data_version in somnopose_loader.h when making changes to this loader
-// that change loader behaviour or modify channels.
+// Please only INCREMENT the somnopose_data_version in somnopose_loader.h when making changes
+// that change loader behaviour or modify channels in a manner that fixes old data imports.
+// Note that changing the data version will require a reimport of existing data for which OSCAR
+// does not keep a backup - so it should be avoided if possible.
+// i.e. there is no need to change the version when adding support for new devices
 //********************************************************************************************
 
 #include <QDir>
diff --git a/oscar/SleepLib/loader_plugins/viatom_loader.cpp b/oscar/SleepLib/loader_plugins/viatom_loader.cpp
index 7c2f3e44..19147c4e 100644
--- a/oscar/SleepLib/loader_plugins/viatom_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/viatom_loader.cpp
@@ -9,10 +9,11 @@
  * for more details. */
 
 //********************************************************************************************
-// IMPORTANT!!!
-//********************************************************************************************
-// Please INCREMENT the viatom_data_version in viatom_loader.h when making changes to this loader
-// that change loader behaviour or modify channels.
+// Please only INCREMENT the viatom_data_version in viatom_loader.h when making changes
+// that change loader behaviour or modify channels in a manner that fixes old data imports.
+// Note that changing the data version will require a reimport of existing data for which OSCAR
+// does not keep a backup - so it should be avoided if possible.
+// i.e. there is no need to change the version when adding support for new devices
 //********************************************************************************************
 
 #include <QDir>
diff --git a/oscar/SleepLib/loader_plugins/zeo_loader.cpp b/oscar/SleepLib/loader_plugins/zeo_loader.cpp
index e282d075..bdaf4c4d 100644
--- a/oscar/SleepLib/loader_plugins/zeo_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/zeo_loader.cpp
@@ -8,10 +8,11 @@
  * for more details. */
 
 //********************************************************************************************
-// IMPORTANT!!!
-//********************************************************************************************
-// Please INCREMENT the zeo_data_version in zel_loader.h when making changes to this loader
-// that change loader behaviour or modify channels.
+// Please only INCREMENT the zeo_data_version in zeo_loader.h when making changes
+// that change loader behaviour or modify channels in a manner that fixes old data imports.
+// Note that changing the data version will require a reimport of existing data for which OSCAR
+// does not keep a backup - so it should be avoided if possible.
+// i.e. there is no need to change the version when adding support for new devices
 //********************************************************************************************
 
 #include <QDir>

From 37483de62a432d2183de85d4c3822c9cf20ca3cc Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Sun, 28 Mar 2021 14:15:10 +1100
Subject: [PATCH 04/20] Display Weight/BMI/Zombie graphs

---
 oscar/overview.cpp | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/oscar/overview.cpp b/oscar/overview.cpp
index de2b930b..4260b8c3 100644
--- a/oscar/overview.cpp
+++ b/oscar/overview.cpp
@@ -236,8 +236,20 @@ void Overview::CreateAllGraphs() {
     } // for chit
 
     WEIGHT = createGraph(STR_GRAPH_Weight, STR_TR_Weight, STR_TR_Weight, YT_Weight);
+    weight = new SummaryChart("Weight", GT_LINE);
+    weight->setMachineType(MT_JOURNAL);
+    weight->addSlice(Journal_Weight, QColor("black"), ST_SETAVG);
+    WEIGHT->AddLayer(weight);
     BMI = createGraph(STR_GRAPH_BMI, STR_TR_BMI, tr("Body\nMass\nIndex"));
+    bmi = new SummaryChart("BMI", GT_LINE);
+    bmi->setMachineType(MT_JOURNAL);
+    bmi->addSlice(Journal_BMI, QColor("black"), ST_SETAVG);
+    BMI->AddLayer(bmi);
     ZOMBIE = createGraph(STR_GRAPH_Zombie, STR_TR_Zombie, tr("How you felt\n(0-10)"));
+    zombie = new SummaryChart("Zombie", GT_LINE);
+    zombie->setMachineType(MT_JOURNAL);
+    zombie->addSlice(Journal_ZombieMeter, QColor("black"), ST_SETAVG);
+    ZOMBIE->AddLayer(zombie);
 }
 
 // Recalculates Overview chart info

From 2331bbba267cc795f4d08af042456f77ed905edb Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Sun, 28 Mar 2021 14:20:19 +1100
Subject: [PATCH 05/20] Don't ResetBounds when setDay is called to avoid
 changing overview graphs to full date range on BMI/Weight change

---
 oscar/Graphs/gGraph.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/oscar/Graphs/gGraph.cpp b/oscar/Graphs/gGraph.cpp
index 5244d6be..9c82f9b8 100644
--- a/oscar/Graphs/gGraph.cpp
+++ b/oscar/Graphs/gGraph.cpp
@@ -275,7 +275,9 @@ void gGraph::setDay(Day *day)
     }
 
     rmin_y = rmax_y = 0;
-    ResetBounds();
+    // This resets weight and bmi overview graphs to full date range when they are changed.
+    // is it required ever?
+    // ResetBounds();
 }
 
 void gGraph::setZoomY(short zoom)

From a0b9488aa96d5e63bc611afe6332eb293f597d76 Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Sun, 28 Mar 2021 14:22:31 +1100
Subject: [PATCH 06/20] Save notes immediately when focus leaves notes widget.
 Fix saving of weight changes when using up/down arrows

---
 oscar/daily.cpp | 130 ++++++++++++++++++++++++------------------------
 1 file changed, 66 insertions(+), 64 deletions(-)

diff --git a/oscar/daily.cpp b/oscar/daily.cpp
index 84b8a86f..98e2e8d4 100644
--- a/oscar/daily.cpp
+++ b/oscar/daily.cpp
@@ -509,6 +509,9 @@ Daily::Daily(QWidget *parent,gGraphView * shared)
     connect(GraphView, SIGNAL(updateCurrentTime(double)), this, SLOT(on_LineCursorUpdate(double)));
     connect(GraphView, SIGNAL(updateRange(double,double)), this, SLOT(on_RangeUpdate(double,double)));
     connect(GraphView, SIGNAL(GraphsChanged()), this, SLOT(updateGraphCombo()));
+
+    // Watch for focusOut events on the JournalNotes widget
+    ui->JournalNotes->installEventFilter(this);
 //    qDebug() << "Finished making new Daily object";
 //    sleep(3);
 }
@@ -521,9 +524,11 @@ Daily::~Daily()
 
     disconnect(sessionbar, SIGNAL(sessionClicked(Session*)), this, SLOT(doToggleSession(Session*)));
     disconnect(webView,SIGNAL(anchorClicked(QUrl)),this,SLOT(Link_clicked(QUrl)));
+    ui->JournalNotes->removeEventFilter(this);
 
-    if (previous_date.isValid())
+    if (previous_date.isValid()) {
         Unload(previous_date);
+    }
 
     // Save graph orders and pin status, etc...
     GraphView->SaveSettings("Daily");
@@ -2206,6 +2211,9 @@ void Daily::on_JournalNotesUnderline_clicked()
 
 void Daily::on_prevDayButton_clicked()
 {
+    if (previous_date.isValid()) {
+         Unload(previous_date);
+    }
     if (!p_profile->ExistsAndTrue("SkipEmptyDays")) {
         LoadDate(previous_date.addDays(-1));
     } else {
@@ -2220,8 +2228,23 @@ void Daily::on_prevDayButton_clicked()
     }
 }
 
+bool Daily::eventFilter(QObject *object, QEvent *event)
+{
+    if (object == ui->JournalNotes && event->type() == QEvent::FocusOut) {
+        // Trigger immediate save of journal when we focus out from it so we never
+        // lose any journal entry text...
+        if (previous_date.isValid()) {
+            Unload(previous_date);
+        }
+    }
+    return false;
+}
+
 void Daily::on_nextDayButton_clicked()
 {
+    if (previous_date.isValid()) {
+         Unload(previous_date);
+    }
     if (!p_profile->ExistsAndTrue("SkipEmptyDays")) {
         LoadDate(previous_date.addDays(1));
     } else {
@@ -2252,6 +2275,9 @@ void Daily::on_calButton_toggled(bool checked)
 
 void Daily::on_todayButton_clicked()
 {
+    if (previous_date.isValid()) {
+         Unload(previous_date);
+    }
 //    QDate d=QDate::currentDate();
 //    if (d > p_profile->LastDay()) {
         QDate lastcpap = p_profile->LastDay(MT_CPAP);
@@ -2424,21 +2450,10 @@ void Daily::on_bookmarkTable_itemChanged(QTableWidgetItem *item)
 
 void Daily::on_weightSpinBox_valueChanged(double arg1)
 {
-    // Update the BMI display
-    double kg;
-    if (p_profile->general->unitSystem()==US_English) {
-         kg=((arg1*pound_convert) + (ui->ouncesSpinBox->value()*ounce_convert)) / 1000.0;
-    } else kg=arg1;
-    double height=p_profile->user->height()/100.0;
-    if ((height>0) && (kg>0)) {
-        double bmi=kg/(height * height);
-        ui->BMI->display(bmi);
-        ui->BMI->setVisible(true);
-        ui->BMIlabel->setVisible(true);
-    } else {
-        ui->BMI->setVisible(false);
-        ui->BMIlabel->setVisible(false);
-    }
+    // This is called if up/down arrows are used, in which case editingFinished is
+    // never called. So always call editingFinished instead
+    Q_UNUSED(arg1);
+    this->on_weightSpinBox_editingFinished();
 }
 
 void Daily::on_weightSpinBox_editingFinished()
@@ -2457,7 +2472,25 @@ void Daily::on_weightSpinBox_editingFinished()
     } else {
             kg=arg1;
     }
-    journal->settings[Journal_Weight]=kg;
+    if (journal->settings.contains(Journal_Weight)) {
+        QVariant old = journal->settings[Journal_Weight];
+        if (old == kg && kg > 0) {
+            // No change to weight - skip
+            return;
+        }
+    } else if (kg == 0) {
+        // Still zero - skip
+        return;
+    }
+    if (kg > 0) {
+        journal->settings[Journal_Weight]=kg;
+    } else {
+        // Weight now zero - remove from journal
+        auto jit = journal->settings.find(Journal_Weight);
+        if (jit != journal->settings.end()) {
+            journal->settings.erase(jit);
+        }
+    }
     gGraphView *gv=mainwin->getOverview()->graphView();
     gGraph *g;
     if (gv) {
@@ -2470,66 +2503,35 @@ void Daily::on_weightSpinBox_editingFinished()
         ui->BMI->setVisible(true);
         ui->BMIlabel->setVisible(true);
         journal->settings[Journal_BMI]=bmi;
-        if (gv) {
-            g=gv->findGraph(STR_GRAPH_BMI);
-            if (g) g->setDay(nullptr);
-        }
     } else {
+        // BMI now zero - remove it
+        auto jit = journal->settings.find(Journal_BMI);
+        if (jit != journal->settings.end()) {
+            journal->settings.erase(jit);
+        }
+        // And make it invisible
         ui->BMI->setVisible(false);
         ui->BMIlabel->setVisible(false);
     }
+    if (gv) {
+        g=gv->findGraph(STR_GRAPH_BMI);
+        if (g) g->setDay(nullptr);
+    }
     journal->SetChanged(true);
 }
 
 void Daily::on_ouncesSpinBox_valueChanged(int arg1)
 {
-    // just update for BMI display
-    double height=p_profile->user->height()/100.0;
-    double kg=((ui->weightSpinBox->value()*pound_convert) + (arg1*ounce_convert)) / 1000.0;
-    if ((height>0) && (kg>0)) {
-        double bmi=kg/(height * height);
-        ui->BMI->display(bmi);
-        ui->BMI->setVisible(true);
-        ui->BMIlabel->setVisible(true);
-    } else {
-        ui->BMI->setVisible(false);
-        ui->BMIlabel->setVisible(false);
-    }
+    // This is called if up/down arrows are used, in which case editingFinished is
+    // never called. So always call editingFinished instead
+    Q_UNUSED(arg1);
+    this->on_weightSpinBox_editingFinished();
 }
 
 void Daily::on_ouncesSpinBox_editingFinished()
 {
-    double arg1=ui->ouncesSpinBox->value();
-    Session *journal=GetJournalSession(previous_date);
-    if (!journal) {
-        journal=CreateJournalSession(previous_date);
-    }
-    double height=p_profile->user->height()/100.0;
-    double kg=((ui->weightSpinBox->value()*pound_convert) + (arg1*ounce_convert)) / 1000.0;
-    journal->settings[Journal_Weight]=kg;
-
-    gGraph *g;
-    if (mainwin->getOverview()) {
-        g=mainwin->getOverview()->graphView()->findGraph(STR_GRAPH_Weight);
-        if (g) g->setDay(nullptr);
-    }
-
-    if ((height>0) && (kg>0)) {
-        double bmi=kg/(height * height);
-        ui->BMI->display(bmi);
-        ui->BMI->setVisible(true);
-        ui->BMIlabel->setVisible(true);
-
-        journal->settings[Journal_BMI]=bmi;
-        if (mainwin->getOverview()) {
-            g=mainwin->getOverview()->graphView()->findGraph(STR_GRAPH_BMI);
-            if (g) g->setDay(nullptr);
-        }
-    } else {
-        ui->BMI->setVisible(false);
-        ui->BMIlabel->setVisible(false);
-    }
-    journal->SetChanged(true);
+    // This is functionally identical to the weightSpinBox_editingFinished, so just call that
+    this->on_weightSpinBox_editingFinished();
 }
 
 QString Daily::GetDetailsText()

From 00225103fd4313096b7eeb2653a21752d62c0c85 Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Sun, 28 Mar 2021 14:22:57 +1100
Subject: [PATCH 07/20] Save notes immediately when focus leaves notes widget.

---
 oscar/daily.h | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/oscar/daily.h b/oscar/daily.h
index 35e9d4cb..94aad87a 100644
--- a/oscar/daily.h
+++ b/oscar/daily.h
@@ -304,6 +304,8 @@ private:
         */
     void UpdateEventsTree(QTreeWidget * tree,Day *day);
 
+    virtual bool eventFilter(QObject *object, QEvent *event) override;
+
     void updateCube();
 
 

From 3362fa4a4d1cf858c9dab91d04ad1560b9900391 Mon Sep 17 00:00:00 2001
From: Arie Klerk <arie.klerk@gmail.com>
Date: Mon, 29 Mar 2021 20:54:36 +0200
Subject: [PATCH 08/20] We have a new translator for BULGARIAN! Here's his
 first update.

---
 Translations/Bulgarian.bg.ts | 43 ++++++++++++++++++------------------
 1 file changed, 22 insertions(+), 21 deletions(-)

diff --git a/Translations/Bulgarian.bg.ts b/Translations/Bulgarian.bg.ts
index 2e3b91be..d777a896 100644
--- a/Translations/Bulgarian.bg.ts
+++ b/Translations/Bulgarian.bg.ts
@@ -11,7 +11,7 @@
     <message>
         <location filename="../oscar/aboutdialog.ui" line="35"/>
         <source>&amp;About</source>
-        <translation>&amp;Относно</translation>
+        <translation type="unfinished">За &amp;приложение</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.ui" line="49"/>
@@ -22,12 +22,13 @@
     <message>
         <location filename="../oscar/aboutdialog.ui" line="63"/>
         <source>Credits</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Заслуги</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.ui" line="77"/>
         <source>GPL License</source>
-        <translation type="unfinished"></translation>
+        <translatorcomment>As a whole this actually should read &quot;Общ публичен лиценз на ГНУ&quot;, but that is a bit long on a tab. I think it would be acceptable to just say &quot;license GPL&quot; and then the explanation is written in the tab. The Bulgarian wikipedia page for the GPL gives a couple of translation options.</translatorcomment>
+        <translation type="unfinished">лиценз GPL</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.ui" line="239"/>
@@ -37,27 +38,27 @@
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="35"/>
         <source>Show data folder</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Покажи папката на данните</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="39"/>
         <source>About OSCAR %1</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">За предложението OSCAR %1</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="83"/>
         <source>Sorry, could not locate About file.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">За съжаление, файлът За приложение не се намери.</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="97"/>
         <source>Sorry, could not locate Credits file.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">За съжаление, файлът Заслуги не се намери.</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="111"/>
         <source>Sorry, could not locate Release Notes.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">За съжаление, Бележки по изданието не се намери.</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="123"/>
@@ -72,12 +73,12 @@
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="127"/>
         <source>As this is a pre-release version, it is recommended that you &lt;b&gt;back up your data folder manually&lt;/b&gt; before proceeding, because attempting to roll back later may break things.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Тъй като това е предварително издание, препоръчано е &lt;b&gt;да направите ръчно архивиране на своята папка с данни&lt;/b&gt; преди да продължите, защото е възможно при по-късен опит за връщане назад нещата да се повредят.</translation>
     </message>
     <message>
         <location filename="../oscar/aboutdialog.cpp" line="139"/>
         <source>To see if the license text is available in your language, see %1.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">За да проверите дали съществува превода на лиценз на Вашия език, вижте %1.</translation>
     </message>
 </context>
 <context>
@@ -85,12 +86,12 @@
     <message>
         <location filename="../oscar/SleepLib/loader_plugins/cms50f37_loader.cpp" line="878"/>
         <source>Could not find the oximeter file:</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Файлът на оксиметър не се намери:</translation>
     </message>
     <message>
         <location filename="../oscar/SleepLib/loader_plugins/cms50f37_loader.cpp" line="884"/>
         <source>Could not open the oximeter file:</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Не може да се отвори файлът на оксиметър:</translation>
     </message>
 </context>
 <context>
@@ -108,12 +109,12 @@
     <message>
         <location filename="../oscar/SleepLib/loader_plugins/cms50_loader.cpp" line="546"/>
         <source>Could not find the oximeter file:</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Файлът на оксиметър не се намери:</translation>
     </message>
     <message>
         <location filename="../oscar/SleepLib/loader_plugins/cms50_loader.cpp" line="552"/>
         <source>Could not open the oximeter file:</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Не може да се отвори файлът на оксиметър:</translation>
     </message>
 </context>
 <context>
@@ -121,7 +122,7 @@
     <message>
         <location filename="../oscar/checkupdates.cpp" line="240"/>
         <source>Checking for newer OSCAR versions</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Проверяваме за за нова версия на OSCAR</translation>
     </message>
 </context>
 <context>
@@ -191,12 +192,12 @@
     <message>
         <location filename="../oscar/daily.ui" line="1199"/>
         <source>I&apos;m feeling ...</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Чувствам се ...</translation>
     </message>
     <message>
         <location filename="../oscar/daily.ui" line="1222"/>
         <source>If height is greater than zero in Preferences Dialog, setting weight here will show Body Mass Index (BMI) value</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Ако в настройки ръст е над нула, задаване на тегло тук ще се покажи стойността на индекса на телесната маса (ИТМ)</translation>
     </message>
     <message>
         <location filename="../oscar/daily.ui" line="1496"/>
@@ -211,7 +212,7 @@
     <message>
         <location filename="../oscar/daily.ui" line="1573"/>
         <source>Show/hide available graphs.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">Покажи или скрий достъпни графики.</translation>
     </message>
     <message>
         <location filename="../oscar/daily.ui" line="1085"/>
@@ -361,7 +362,7 @@
     <message>
         <location filename="../oscar/daily.cpp" line="1474"/>
         <source>Unable to display Pie Chart on this system</source>
-        <translation type="unfinished"></translation>
+        <translation></translation>
     </message>
     <message>
         <location filename="../oscar/daily.cpp" line="1708"/>
@@ -391,7 +392,7 @@
     <message>
         <location filename="../oscar/daily.cpp" line="1718"/>
         <source>Sorry, this machine only provides compliance data.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">За съжаление, тази машина предоставя само данни за съответствие.</translation>
     </message>
     <message>
         <location filename="../oscar/daily.cpp" line="1719"/>
@@ -441,7 +442,7 @@
     <message>
         <location filename="../oscar/daily.cpp" line="1091"/>
         <source>&lt;b&gt;Please Note:&lt;/b&gt; All settings shown below are based on assumptions that nothing has changed since previous days.</source>
-        <translation type="unfinished"></translation>
+        <translation type="unfinished">&lt;b&gt;Моля, Забележете:&lt;/b&gt;Всичките настройки, които са показани надолу, се основават на предположения, че нищо не се е променило от предишните дни.</translation>
     </message>
     <message>
         <location filename="../oscar/daily.cpp" line="1215"/>

From 85b4013b2d681fcccf1ae4aec0761e39d57918f4 Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Thu, 1 Apr 2021 11:32:58 +1100
Subject: [PATCH 09/20] Fix stage & position session toggle

---
 oscar/daily.cpp | 23 +++++++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/oscar/daily.cpp b/oscar/daily.cpp
index 98e2e8d4..8511a362 100644
--- a/oscar/daily.cpp
+++ b/oscar/daily.cpp
@@ -576,6 +576,7 @@ void Daily::Link_clicked(const QUrl &url)
   //      webView->page()->mainFrame()->setScrollBarValue(Qt::Vertical, webView->page()->mainFrame()->scrollBarMaximum(Qt::Vertical)-i);
     } else  if (code=="toggleoxisession") { // Enable/Disable Oximetry session
         day=p_profile->GetDay(previous_date,MT_OXIMETER);
+        if (!day) return;
         Session *sess=day->find(sid, MT_OXIMETER);
         if (!sess)
             return;
@@ -585,6 +586,20 @@ void Daily::Link_clicked(const QUrl &url)
         // Reload day
         LoadDate(previous_date);
   //      webView->page()->mainFrame()->setScrollBarValue(Qt::Vertical, webView->page()->mainFrame()->scrollBarMaximum(Qt::Vertical)-i);
+    } else  if (code=="togglestagesession") { // Enable/Disable Sleep Stage session
+        day=p_profile->GetDay(previous_date,MT_SLEEPSTAGE);
+        if (!day) return;
+        Session *sess=day->find(sid, MT_SLEEPSTAGE);
+        if (!sess) return;
+        sess->setEnabled(!sess->enabled());
+        LoadDate(previous_date);
+    } else  if (code=="togglepositionsession") { // Enable/Disable Position session
+        day=p_profile->GetDay(previous_date,MT_POSITION);
+        if (!day) return;
+        Session *sess=day->find(sid, MT_POSITION);
+        if (!sess) return;
+        sess->setEnabled(!sess->enabled());
+        LoadDate(previous_date);
     } else if (code=="cpap")  {
         day=p_profile->GetDay(previous_date,MT_CPAP);
         if (day) {
@@ -1010,7 +1025,7 @@ QString Daily::getSessionInformation(Day * day)
             case MT_SLEEPSTAGE: type="stage";
                 html+=tr("Sleep Stage Sessions");
                 break;
-            case MT_POSITION: type="stage";
+            case MT_POSITION: type="position";
                 html+=tr("Position Sensor Sessions");
                 break;
 
@@ -1535,10 +1550,10 @@ QVariant MyTextBrowser::loadResource(int type, const QUrl &url)
 
 void Daily::Load(QDate date)
 {
-	qDebug() << "Daily::Load called for" << date.toString() << "using" << QApplication::font().toString();
+    qDebug() << "Daily::Load called for" << date.toString() << "using" << QApplication::font().toString();
 
-	qDebug() << "Setting App font in Daily::Load";
-	setApplicationFont();
+    qDebug() << "Setting App font in Daily::Load";
+    setApplicationFont();
 
     dateDisplay->setText("<i>"+date.toString(Qt::SystemLocaleLongDate)+"</i>");
     previous_date=date;

From e358d31f269dd01bf63a420267e9d9a23f387ac1 Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Thu, 1 Apr 2021 21:39:06 -0700
Subject: [PATCH 10/20] Add logic to read rolling files created by DeVilbiss
 BLUE CPAPs

---
 .../loader_plugins/intellipap_loader.cpp      | 469 +++++++++++++-----
 1 file changed, 332 insertions(+), 137 deletions(-)

diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
index 7c6cedb8..e4a98e87 100644
--- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
@@ -607,38 +607,6 @@ int IntellipapLoader::OpenDV5(const QString & path)
 //     May be same as what we call large leak time for other machines?
 ////////////////////////////////////////////////////////////////////////////
 
-class RollingFile
-{
-public:
-    RollingFile () { }
-
-    ~RollingFile () {
-        if (data)
-            delete [] data;
-        data = nullptr;
-    }
-
-    bool open (QString fn); // Open the file
-    bool close();           // close the file
-    unsigned char * get();  // read the next record in the file
-
-    int numread () {return number_read;};   // Return number of records read
-    int recnum () {return record_number;};  // Return last-read record number
-
-private:
-    QString filename;
-    QFile file;
-    int record_length;
-    int wrap_record;
-    bool wrapping = false;
-
-    int number_read = 0;    // Number of records read
-
-    int record_number = 0;  // Number of record.  First record in the file  is #1. First record read is wrap_record;
-
-    unsigned char * data = nullptr;
-};
-
 struct DV6TestedModel
 {
     QString model;
@@ -658,9 +626,9 @@ struct DV6_S_Data // Daily summary
     Session * sess;
     unsigned char u1;            //00 (position)
 ***/    
-    unsigned int start_time;     //01
-    unsigned int stop_time;      //05
-    unsigned int atpressure_time;//09
+    unsigned int start_time;     //01 Start time for date
+    unsigned int stop_time;      //05 End time
+    unsigned int written;        //09 timestamp when this record was written
     EventDataType hours;         //13
 //    EventDataType unknown14;   //14
     EventDataType pressureAvg;   //15
@@ -796,13 +764,14 @@ PACK (struct SET_BIN_REC {
 // Unless explicitly noted, all other DV6_x_REC are definitions for the repeating data structure that follows the header
 PACK (struct DV6_HEADER {
     unsigned char unknown;          // 0 always zero
-    unsigned char filetype;         // 1 always "R"
+    unsigned char filetype;         // 1 e.g. "R" for a R.BIN file
     unsigned char serial[11];       // 2 serial number
-    unsigned char numRecords[4];    // 13 Number of records in file (always 180,000)
+    unsigned char numRecords[4];    // 13 Number of records in file (always fixed, 180,000 for R.BIN)
     unsigned char recordLength;     // 17 Length of data record (always 117)
     unsigned char recordStart[4];   // 18 First record in wrap-around buffer
     unsigned char unknown_22[21];   // 22 Unknown values
-    unsigned char unknown_43[12];   // 43 Seems always to be zero
+    unsigned char unknown_43[8];    // 43 Seems always to be zero
+    unsigned char lasttime[4];      // 51 OSCAR only: Last timestamp, in history files only
     unsigned char checksum;         // 55 Checksum
 });
 
@@ -902,6 +871,20 @@ struct DV6_SessionInfo {
     CPAPMode mode = MODE_UNKNOWN;
 };
 
+QString card_path;
+QString backup_path;
+QString history_path;
+
+MachineInfo info;
+Machine * mach = nullptr;
+
+bool rebuild_from_backups = false;
+bool create_backups = false;
+
+QMap<SessionID, DV6_S_Data> DailySummaries;
+QMap<SessionID, DV6_SessionInfo> SessionData;
+SET_BIN_REC * settings;
+
 unsigned int ep = 0;
 
 // Convert a 4-character number in DV6 data file to a standard int
@@ -918,41 +901,199 @@ unsigned int convertTime (unsigned char time[]) {
     return ((time[3] << 24) + (time[2] << 16) + (time[1] << 8) + time[0]) + ep; // Time as Unix epoch time
 }
 
-bool RollingFile::open(QString fn) {
+class RollingBackup
+{
+public:
+    RollingBackup () {}
+    ~RollingBackup () {
+    }
 
-    filename = fn;
-    file.setFileName(filename);
+    bool open (const QString filetype, DV6_HEADER * newhdr); // Open the file
+    bool close();           // close the file
+    bool save(QByteArray dataBA);  // save the next record in the file
+
+private:
+    DV6_HEADER hdr;       // file header
+    QString filetype;
+    QFile hFile;
+
+    int record_length;      // Length of record block in incoming file
+    const int maxHistFileSize = 20*10e6;   // Maximum size of file before we create a new file
+
+    int numWritten;     // Number of records written
+    quint32 lastTimestamp;
+};
+
+bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
+    if (!create_backups)
+        return true;
+
+    this->filetype = filetype;
+
+    QDir hpath(history_path);
+    QStringList filters;
+
+    numWritten = 0;
+
+    filters.append(filetype);
+    filters[0].insert(1, "_*");
+    hpath.setNameFilters(filters);
+    hpath.setFilter(QDir::Files);
+    hpath.setSorting(QDir::Name | QDir::Reversed);
+
+    QStringList fileNames = hpath.entryList(); // Get list of files
+    QFile histfile(fileNames.first());
+
+//    bool needNewFile = false;
+
+    // Handle first time a history file is being created
+    if (fileNames.isEmpty()) {
+        memcpy (&hdr, newhdr, sizeof(DV6_HEADER));
+        for (int i = 0; i < 4; i++) {
+            hdr.recordStart[i] = 0;
+            hdr.lasttime[i] = 0;
+        }
+        record_length = hdr.recordLength;
+    }
+
+    // We have an existing history record
+    if (! fileNames.isEmpty()) {
+        // See if this file is large enough that we want to create a new file
+        if (histfile.size() > maxHistFileSize) {
+            memcpy (&hdr, newhdr, sizeof(DV6_HEADER));
+            for (int i = 0; i < 4; i++)
+                hdr.recordStart[i] = 0;
+
+            if (!histfile.open(QIODevice::ReadOnly)) {
+                qWarning() << "DV6 RollingBackup could not open" << fileNames.first() << "for reading, error code" << histfile.error() << histfile.errorString();
+                return false;
+            }
+            record_length = hdr.recordLength;
+
+#ifdef ROLLBACKUP
+            wrap_record = convertNum(hdr.recordStart);
+            if (!histfile.seek(sizeof(DV6_HEADER) + (wrap_record-1) * record_length)) {
+                qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
+                file.close();
+                return false;
+            }
+#endif
+
+        }
+    }
+
+    return true;
+}
+
+bool RollingBackup::close() {
+    if (!create_backups)
+        return true;
+    return true;
+}
+
+bool RollingBackup::save(QByteArray dataBA) {
+    Q_UNUSED(dataBA)
+    if (!create_backups)
+        return true;
+    return true;
+}
+
+class RollingFile
+{
+public:
+    RollingFile () { }
+
+    ~RollingFile () {
+        if (data)
+            delete [] data;
+        data = nullptr;
+        if (hdr)
+            delete hdr;
+        hdr = nullptr;
+    }
+
+    bool open (QString fn); // Open the file
+    bool close();           // close the file
+    unsigned char * get();  // read the next record in the file
+
+    int numread () {return number_read;};   // Return number of records read
+    int recnum () {return record_number;};  // Return last-read record number
+
+    RollingBackup rb;
+
+private:
+    QString filename;
+    QFile file;
+    int record_length;
+    int wrap_record;
+    bool wrapping = false;
+
+    int number_read = 0;    // Number of records read
+
+    int record_number = 0;  // Number of record.  First record in the file  is #1. First record read is wrap_record;
+
+    DV6_HEADER * hdr;       // file header
+
+    unsigned char * data = nullptr; // record pointer
+};
+
+bool RollingFile::open(QString filetype) {
+
+    filename = filetype;
+    file.setFileName(card_path + "/" +filetype);
 
     if (!file.open(QIODevice::ReadOnly)) {
         qWarning() << "DV6 RollingFile could not open" << filename << "for reading, error code" << file.error() << file.errorString();
         return false;
     }
 
+    // Save header for use in making backups of data
+    hdr = new DV6_HEADER;
     QByteArray dataBA = file.read(sizeof(DV6_HEADER));
-    DV6_HEADER * hdr = (DV6_HEADER *) dataBA.data();
+    memcpy (hdr, dataBA.data(), sizeof(DV6_HEADER));
+
+    // Extract control information from header
     record_length = hdr->recordLength;
     wrap_record = convertNum(hdr->recordStart);
     record_number = wrap_record;
     number_read = 0;
     wrapping = false;
 
+    // Create buffer to hold each record as it is read
     data = new unsigned char[record_length];
 
+    // Seek to first data record in file
     if (!file.seek(sizeof(DV6_HEADER) + wrap_record * record_length)) {
         qWarning() << "DV6 RollingFile unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
         file.close();
         return false;
     }
+#ifdef ROLLBACKUP
+    if (!rb.open(filetype, hdr)) {
+        qWarning() << "DV6 RollingBackup failed";
+        file.close();
+        return false;
+    }
+#endif
 
-    qDebug() << "RollingFile opening" << filename << "at wrap record" << wrap_record;
+    qDebug() << "DV6 RollingFile opening" << filename << "at wrap record" << wrap_record;
     return true;
 }
 
 bool RollingFile::close() {
     file.close();
-    if (data != nullptr)
+
+#ifdef ROLLBACKUP
+    rb.close();
+#endif
+
+    if (data)
         delete [] data;
     data = nullptr;
+    if (hdr)
+        delete hdr;
+    hdr = nullptr;
+
     return true;
 }
 
@@ -987,6 +1128,11 @@ unsigned char * RollingFile::get() {
         file.close();
         return nullptr;
     }
+#ifdef ROLLBACKUP
+    if (!rb.save(dataBA)) {
+        qWarning() << "DV6 RollingBackup failed";
+    }
+#endif
 
     number_read++;
 
@@ -995,21 +1141,51 @@ unsigned char * RollingFile::get() {
     return data;
 }
 
-MachineInfo info;
-Machine * mach = nullptr;
+// Returns empty QByteArray() on failure.
+QByteArray fileChecksum(const QString &fileName,
+                        QCryptographicHash::Algorithm hashAlgorithm)
+{
+    QFile f(fileName);
+    if (f.open(QFile::ReadOnly)) {
+        QCryptographicHash hash(hashAlgorithm);
+        bool res = hash.addData(&f);
+        f.close();
+        if (res) {
+            return hash.result();
+        }
+    }
+    return QByteArray();
+}
 
-bool rebuild_from_backups = false;
+/***
+// Return the OSCAR date that the last data was written.
+// This will be considered to be the last day for which we have any data.
+// Adjust to get the correct date for sessions starting after midnight.
+QDate getLastDate () {
+    return QDate();
+}
+***/
 
-QMap<SessionID, DV6_S_Data> DailySummaries;
-QMap<SessionID, DV6_SessionInfo> SessionData;
-SET_BIN_REC * settings;
+// Return date used within OSCAR, assuming day ends at split time in preferences (usually noon)
+QDate getNominalDate (QDateTime dt) {
+    QDate d = dt.date();
+    QTime tm = dt.time();
+    QTime daySplitTime = p_profile->session->getPref(STR_IS_DaySplitTime).toTime();
+    if (tm < daySplitTime)
+        d = d.addDays(-1);
+    return d;
+}
+QDate getNominalDate (unsigned int dt) {
+    QDateTime xdt = QDateTime::fromSecsSinceEpoch(dt);
+    return getNominalDate(xdt);
+}
 
 ///////////////////////////////////////////////
 // U.BIN - Open and parse session list and create session data structures
 //         with session start and stop times.
 ///////////////////////////////////////////////
 
-bool load6Sessions (const QString & path) {
+bool load6Sessions () {
 
     RollingFile rf;
     unsigned int ts1,ts2;
@@ -1018,7 +1194,7 @@ bool load6Sessions (const QString & path) {
 
     qDebug() << "Parsing U.BIN";
 
-    if (!rf.open(path+"/U.BIN")) {
+    if (!rf.open("U.BIN")) {
         qWarning() << "Unable to open U.BIN";
         return false;
     }
@@ -1079,12 +1255,12 @@ bool load6Settings (const QString & path) {
 // S.BIN - Open and load day summary list
 ////////////////////////////////////////////////////////////////////////////////////////
 
-bool load6DailySummaries (const QString & path) {
+bool load6DailySummaries () {
     RollingFile rf;
 
     DailySummaries.clear();
 
-    if (!rf.open(path+"/S.BIN")) {
+    if (!rf.open("S.BIN")) {
         qWarning() << "Unable to open S.BIN";
         return false;
     }
@@ -1100,7 +1276,13 @@ bool load6DailySummaries (const QString & path) {
 
         dailyData.start_time = convertTime(rec->begin);
         dailyData.stop_time = convertTime(rec->end);
-        dailyData.atpressure_time = convertTime(rec->written);
+        dailyData.written = convertTime(rec->written);
+
+#ifdef DEBUG6
+        qDebug() << "DV6 S.BIN start" << dailyData.start_time
+                 << "stop" << dailyData.stop_time
+                 << "at pressure?" << dailyData.atpressure_time;
+#endif
 
         dailyData.hours = float(rec->hours) / 10.0F;
         dailyData.pressureSetMin = float(rec->pressureSetMin) / 10.0F;
@@ -1135,6 +1317,26 @@ bool load6DailySummaries (const QString & path) {
 
         DailySummaries[dailyData.start_time] = dailyData;
 
+/**** Previous loader did this:
+        if (!mach->sessionlist.contains(ts1)) { // Check if already imported
+            qDebug() << "Detected new Session" << ts1;
+            R.sess = new Session(mach, ts1);
+            R.sess->SetChanged(true);
+
+            R.sess->really_set_first(qint64(ts1) * 1000L);
+            R.sess->really_set_last(qint64(ts2) * 1000L);
+
+            if (data[49] != data[50]) {
+                R.sess->settings[CPAP_PressureMin] = R.pressureSetMin;
+                R.sess->settings[CPAP_PressureMax] = R.pressureSetMax;
+                R.sess->settings[CPAP_Mode] = MODE_APAP;
+            } else {
+                R.sess->settings[CPAP_Mode] = MODE_CPAP;
+                R.sess->settings[CPAP_Pressure] = R.pressureSetMin;
+            }
+            R.hasMaskPressure = false;
+***/
+
     } while (true);
 
     rf.close();
@@ -1293,14 +1495,14 @@ int create6Sessions() {
 // Parse R.BIN for high resolution flow data
 ////////////////////////////////////////////////////////////////////////////////////////
 
-bool load6HighResData (const QString & path) {
+bool load6HighResData () {
 
     RollingFile rf;
     Session *sess = nullptr;
     unsigned int rec_ts1, previousRecBegin = 0;
     bool inSession = false; // true if we are adding data to this session
 
-    if (!rf.open(path+"/R.BIN")) {
+    if (!rf.open("R.BIN")) {
         qWarning() << "DV6 Unable to open R.BIN";
         return false;
     }
@@ -1806,14 +2008,14 @@ bool load6HighResData (const QString & path) {
 // Parse L.BIN for per minute data
 ////////////////////////////////////////////////////////////////////////////////////////
 
-bool load6PerMinute (const QString & path) {
+bool load6PerMinute () {
 
     RollingFile rf;
     Session *sess = nullptr;
     unsigned int rec_ts1, previousRecBegin = 0;
     bool inSession = false; // true if we are adding data to this session
 
-    if (!rf.open(path+"/L.BIN")) {
+    if (!rf.open("L.BIN")) {
         qWarning() << "DV6 Unable to open L.BIN";
         return false;
     }
@@ -1960,7 +2162,7 @@ bool load6PerMinute (const QString & path) {
 // Parse E.BIN for event data
 ////////////////////////////////////////////////////////////////////////////////////////
 
-bool load6EventData (const QString & path) {
+bool load6EventData () {
     RollingFile rf;
 
     Session *sess = nullptr;
@@ -1977,7 +2179,7 @@ bool load6EventData (const QString & path) {
     EventList * SN = nullptr;
     EventList * FL = nullptr;
 
-    if (!rf.open(path+"/E.BIN")) {
+    if (!rf.open("E.BIN")) {
         qWarning() << "DV6 Unable to open E.BIN";
         return false;
     }
@@ -2170,63 +2372,20 @@ int addSessions() {
 
 }
 
-// Returns empty QByteArray() on failure.
-QByteArray fileChecksum(const QString &fileName,
-                        QCryptographicHash::Algorithm hashAlgorithm)
-{
-    QFile f(fileName);
-    if (f.open(QFile::ReadOnly)) {
-        QCryptographicHash hash(hashAlgorithm);
-        if (hash.addData(&f)) {
-            return hash.result();
-        }
-    }
-    return QByteArray();
-}
-
-/****
-// Return the OSCAR date that the last data was written.
-// This will be considered to be the last day for which we have any data.
-// Adjust to get the correct date for sessions starting after midnight.
-QDate getLastDate () {
-    return QDate();
-}
-
-// Return date used within OSCAR, assuming day ends at noon
-QDate getOscarDate (QDateTime dt) {
-    QDate d = dt.date();
-    QTime tm = dt.time();
-    if (tm.hour() < 11)
-        d = d.addDays(-1);
-    return d;
-}
-***/
-
 ////////////////////////////////////////////////////////////////////////////////////////
 // Create backup of input files
-// Create dated backup files when necesaary
+// Create dated backup of settings file if changed
 ////////////////////////////////////////////////////////////////////////////////////////
 
 bool backup6 (const QString & path)  {
 
-    // Are backups enabled?
-    if (!p_profile->session->backupCardData())
+    if (rebuild_from_backups || !create_backups)
         return true;
 
-    QString backup_path = mach->getBackupPath();
-    QString history_path = backup_path + "/DV6/HISTORY";
-
-    // Compare QDirs rather than QStrings because separators may be different, especially on Windows.
-    // We want to check whether import and backup paths are the same, regardless of variations in the string representations.
     QDir ipath(path);
+    QDir cpath(card_path);
     QDir bpath(backup_path);
 
-    if (ipath == bpath) {
-        // Don't create backups if importing from backup folder
-        rebuild_from_backups = true;
-        return true;
-    }
-
     if ( ! bpath.exists()) {
         if ( ! bpath.mkpath(backup_path) ) {
             qWarning() << "Could not create DV6 backup directory" << backup_path;
@@ -2249,67 +2408,103 @@ bool backup6 (const QString & path)  {
     bool backup_settings = true;
 
     QStringList filters;
-    filters << "set_*.bin";
+
+    QFile settingsFile;
+    QString inputFile = cpath.absolutePath() + "/SET.BIN";
+    settingsFile.setFileName(inputFile);
+
+    filters << "SET_*.BIN";
     hpath.setNameFilters(filters);
     hpath.setFilter(QDir::Files);
-    QDir::Name | QDir::Reversed;
+    hpath.setSorting(QDir::Name | QDir::Reversed);
     QStringList fileNames = hpath.entryList(); // Get list of files
     if (! fileNames.isEmpty()) {
         QString lastFile = fileNames.first();
-        QString newFile = ipath.absolutePath() + "/set.bin";
-        qDebug() << "last settings file is" << lastFile << "new file is" << newFile;
-        QByteArray newMD5 = fileChecksum(newFile, QCryptographicHash::Md5);
-        QByteArray oldMD5 = fileChecksum(lastFile, QCryptographicHash::Md5);
+        qDebug() << "last settings file is" << lastFile << "new file is" << settingsFile;
+        QByteArray newMD5 = fileChecksum(settingsFile.fileName(), QCryptographicHash::Md5);
+        QByteArray oldMD5 = fileChecksum(hpath.absolutePath()+"/"+lastFile, QCryptographicHash::Md5);
         if (newMD5 == oldMD5)
             backup_settings = false;
     }
 
-    if (backup_settings) {
-        QString newFile = hpath.absolutePath() + "/set-" + "1234" + ".bin";
-        qDebug() << "history filename is" << newFile;
+    if (backup_settings && !DailySummaries.isEmpty()) {
+        DV6_S_Data ds = DailySummaries.last();
+        QString newFile = hpath.absolutePath() + "/SET_" + getNominalDate(ds.start_time).toString("yyyyMMdd") + ".BIN";
+        if (!settingsFile.copy(inputFile, newFile)) {
+            qWarning() << "DV6 backup could not copy" << inputFile << "to" << newFile << ", error code" << settingsFile.error() << settingsFile.errorString();
+        }
     }
 
     // We're done!
     return true;
 }
 
+////////////////////////////////////////////////////////////////////////////////////////
+// Initialize DV6 environment
+////////////////////////////////////////////////////////////////////////////////////////
+
+bool init6Environment (const QString & path) {
+
+    // Create Machine database record if it doesn't exist already
+    mach = p_profile->CreateMachine(info);
+    if (mach == nullptr) {
+        qWarning() << "Could not create DV6 Machine data structure";
+        return false;
+    }
+
+    backup_path = mach->getBackupPath();
+    history_path = backup_path + "/HISTORY";
+
+    // Compare QDirs rather than QStrings because separators may be different, especially on Windows.
+    QDir ipath(path);
+    QDir bpath(backup_path);
+
+    if (ipath == bpath) {
+        // Don't create backups if importing from backup folder
+        rebuild_from_backups = true;
+        create_backups = false;
+    } else {
+        rebuild_from_backups = false;
+        create_backups = p_profile->session->backupCardData();
+    }
+
+    return true;
+}
+
 ////////////////////////////////////////////////////////////////////////////////////////
 // Open a DV6 SD card, parse everything, add to OSCAR database
 ////////////////////////////////////////////////////////////////////////////////////////
 
 int IntellipapLoader::OpenDV6(const QString & path)
 {
-    QString newpath = path + DV6_DIR;
+    card_path = path + DV6_DIR;
 
-    // Prime the machine database's info field with stuff relevant to this machine
+    // 1. Prime the machine database's info field with this machine
     info = newInfo();
 
-    // VER.BIN - Parse model number, serial, etc.
-    if (!load6VersionInfo(newpath))
+    // 2. VER.BIN - Parse model number, serial, etc. into info structure
+    if (!load6VersionInfo(card_path))
         return -1;
 
-    // Now, create Machine database record if it doesn't exist already
-    mach = p_profile->CreateMachine(info);
-    if (mach == nullptr) {
-        qWarning() << "Could not create Machine data structure";
-        return -1;
-    }
-
-    // SET.BIN - Parse settings file (which is only the latest settings)
-    if (!load6Settings(newpath))
+    // 3. Initialize rest of the DV6 loader environment
+    if (!init6Environment (path))
         return -1;
 
-    // S.BIN - Open and parse day summary list and create a list of days
-    if (!load6DailySummaries(newpath))
+    // 4. SET.BIN - Parse settings file (which is only the latest settings)
+    if (!load6Settings(card_path))
         return -1;
 
-    // Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine)
+    // 5. S.BIN - Open and parse day summary list and create a list of days
+    if (!load6DailySummaries())
+        return -1;
+
+    // 6. Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine)
     if (!backup6(path))
         return -1;
 
-    // U.BIN - Open and parse session list and create a list of session times
+    // 7. U.BIN - Open and parse session list and create a list of session times
     // (S.BIN must already be loaded)
-    if (!load6Sessions(newpath))
+    if (!load6Sessions())
         return -1;
 
     // Create OSCAR session list from session times and summary data
@@ -2317,15 +2512,15 @@ int IntellipapLoader::OpenDV6(const QString & path)
         return -1;
 
     // R.BIN - Open and parse flow data
-    if (!load6HighResData(newpath))
+    if (!load6HighResData())
         return -1;
 
     // L.BIN - Open and parse per minute data
-    if (!load6PerMinute(newpath))
+    if (!load6PerMinute())
         return -1;
 
     // E.BIN - Open and parse event data
-    if (!load6EventData(newpath))
+    if (!load6EventData())
         return -1;
 
     // Finalize input

From e699ea182d435794ca1633102b3c7f71c2b1ef50 Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Fri, 2 Apr 2021 17:17:50 -0700
Subject: [PATCH 11/20] Update version checking to allow QT 6

---
 OSCAR_QT.pro    | 14 ++++++++++----
 oscar/oscar.pro | 16 +++++++++++-----
 2 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/OSCAR_QT.pro b/OSCAR_QT.pro
index df4e72bb..a9aa515c 100644
--- a/OSCAR_QT.pro
+++ b/OSCAR_QT.pro
@@ -1,7 +1,13 @@
-lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) {
-    message("You need to Qt 5.9 or newer to build OSCAR with Help Pages")
-    lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,7) {
-        error("You need Qt 5.7 or newer to build OSCAR")
+lessThan(QT_MAJOR_VERSION,5) {
+    error("You need Qt 5.7 or newer to build OSCAR");
+}
+
+if (equals(QT_MAJOR_VERSION,5)) {
+    lessThan(QT_MINOR_VERSION,9) {
+        message("You need Qt 5.9 to build OSCAR with Help Pages")
+    }
+    lessThan(QT_MINOR_VERSION,7) {
+        error("You need Qt 5.7 or newer to build OSCAR");
     }
 }
 
diff --git a/oscar/oscar.pro b/oscar/oscar.pro
index eb704b56..b2f8e977 100644
--- a/oscar/oscar.pro
+++ b/oscar/oscar.pro
@@ -6,14 +6,20 @@
 
 message(Platform is $$QMAKESPEC )
 
-lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) {
-    message("You need Qt 5.9 to build OSCAR with Help Pages")
-    DEFINES += helpless
-}
-lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,7) {
+lessThan(QT_MAJOR_VERSION,5) {
     error("You need Qt 5.7 or newer to build OSCAR");
 }
 
+if (equals(QT_MAJOR_VERSION,5)) {
+    lessThan(QT_MINOR_VERSION,9) {
+        message("You need Qt 5.9 to build OSCAR with Help Pages")
+        DEFINES += helpless
+    }
+    lessThan(QT_MINOR_VERSION,7) {
+        error("You need Qt 5.7 or newer to build OSCAR");
+    }
+}
+
 # get rid of the help browser, at least for now
 DEFINES += helpless
 

From e3a87a31481d36e3a7365ddbb6bc9c2fe9c46d6c Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Sun, 4 Apr 2021 04:56:06 -0700
Subject: [PATCH 12/20] Update release notes with recent changes.

---
 Htmldocs/release_notes.html | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html
index e2dd1c9b..304a8ca6 100644
--- a/Htmldocs/release_notes.html
+++ b/Htmldocs/release_notes.html
@@ -12,16 +12,20 @@
     <br><a href=http://www.apneaboard.com/wiki/index.php/OSCAR_Release_Notes>http://www.apneaboard.com/wiki/index.php/OSCAR_Release_Notes</a></p>
     <p>
     <b>Changes and fixes in OSCAR v1.X.Y</b>
-    <br>Portions of OSCAR are © 2019-2020 by
+    <br>Portions of OSCAR are © 2019-2021 by
     <i>The OSCAR Team</i></p>
     <ul>
+      <li>[new] Add support for DeVilbiss BLUE (DV6x) CPAP machines [experimental].</li>
       <li>[new] Additional Philips Respironics devices tested and fully supported:
         <ul>
           <li>DreamStation Go Auto (500G120)</li>
           <li>DreamStation Auto CPAP with A-Flex (500X140)</li>
+          <li>DreamStation BiPAP AVAPS 30 (1130X200)</li>
         </ul>
       </li>
       <li>[new] Add support for DreamStation Go humidifier Target Time setting.</li>
+      <li>[new] Add Bulgarian translation; update other languages.</li>
+      <li>[new] Improve Somnopose import options.</li>
       <li>[fix] Correct calculation of average leak rate on Welcome page.</li>
       <li>[fix] Correct installation of non-English Release Notes on Windows.</li>
       <li>[fix] About/Credits page now offers Google translations to other languages.</li>
@@ -33,6 +37,10 @@
       <li>[fix] Purge currently selected day no longer deletes bookmarks for that day.</li>
       <li>[fix] Remove warning from Chromebook when importing from previously used local folder.</li>
       <li>[fix] Update link to Contec drivers.</li>
+      <li>[fix] Fix display problems for short duration events.</li>
+      <li>[fix] Statistics headings will now be 99.5% or Max, depending on machine type and preference settings.</li>
+      <li>[fix] Mark exported Journal backup file as UTF-8.</li>
+      <li>[fix] Improve error message when unable to access OSCAR database.</li>
     </ul>
     <p>
     <b>Changes and fixes in OSCAR v1.2.0</b>

From 3114cf517d00b59b0664d510e73193ea220d681e Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Sun, 4 Apr 2021 04:56:39 -0700
Subject: [PATCH 13/20] Improve file identification in Icon loader    Will no
 longer attempt to process SleepStyle data with Icon loader    A new loader
 for SleepStyle machines is forthcoming.

---
 oscar/SleepLib/loader_plugins/icon_loader.cpp | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/oscar/SleepLib/loader_plugins/icon_loader.cpp b/oscar/SleepLib/loader_plugins/icon_loader.cpp
index f2ad2963..097d1e85 100644
--- a/oscar/SleepLib/loader_plugins/icon_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/icon_loader.cpp
@@ -66,6 +66,26 @@ bool FPIconLoader::Detect(const QString & givenpath)
         return false;
     }
 
+    // ICON serial numbers (directory names) are all digits (SleepStyle are mixed alpha and numeric)
+    QString serialDir(dir.path() + "/FPHCARE/ICON");
+    QDir iconDir(serialDir);
+
+    iconDir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
+    iconDir.setSorting(QDir::Name);
+    QFileInfoList flist = iconDir.entryInfoList();
+
+    bool ok;
+
+    for (int i = 0; i < flist.size(); i++) {
+        QFileInfo fi = flist.at(i);
+        QString filename = fi.fileName();
+
+        filename.toInt(&ok);
+
+        if (!ok) {
+            return false;
+        }
+    }
     return true;
 }
 

From 94faff5297d9671def8762790813a59129ef135b Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Mon, 5 Apr 2021 12:05:05 +1000
Subject: [PATCH 14/20] Allow purge current day for all machine types

---
 oscar/mainwindow.cpp | 100 ++++++++++++++++++++++++++++++++-----------
 oscar/mainwindow.h   |   6 +++
 oscar/mainwindow.ui  |  54 ++++++++++++++++++++---
 3 files changed, 129 insertions(+), 31 deletions(-)

diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp
index 390fed99..f49bc6ef 100644
--- a/oscar/mainwindow.cpp
+++ b/oscar/mainwindow.cpp
@@ -1805,51 +1805,101 @@ void MainWindow::RestartApplication(bool force_login, QString cmdline)
 }
 
 void MainWindow::on_actionPurge_Current_Day_triggered()
+{
+    this->purgeDay(MT_CPAP);
+}
+
+void MainWindow::on_actionPurgeCurrentDayOximetry_triggered()
+{
+    this->purgeDay(MT_OXIMETER);
+}
+
+void MainWindow::on_actionPurgeCurrentDaySleepStage_triggered()
+{
+    this->purgeDay(MT_SLEEPSTAGE);
+}
+
+void MainWindow::on_actionPurgeCurrentDayPosition_triggered()
+{
+    this->purgeDay(MT_POSITION);
+}
+
+void MainWindow::on_actionPurgeCurrentDayAllExceptNotes_triggered()
+{
+    this->purgeDay(MT_UNKNOWN);
+}
+
+void MainWindow::on_actionPurgeCurrentDayAll_triggered()
+{
+    this->purgeDay(MT_JOURNAL);
+}
+
+// Purge data for a given machine type.
+// Special handling: MT_JOURNAL == All data. MT_UNKNOWN == All except journal
+void MainWindow::purgeDay(MachineType type)
 {
     if (!daily)
         return;
     QDate date = daily->getDate();
-    qDebug() << "Purging CPAP data from" << date;
+    qDebug() << "Purging data from" << date;
     daily->Unload(date);
-    Day *day = p_profile->GetDay(date, MT_CPAP);
+    Day *day = p_profile->GetDay(date, MT_UNKNOWN);
     Machine *cpap = nullptr;
-    if (day)
-        cpap = day->machine(MT_CPAP);
+    if (!day)
+        return;
 
-    if (cpap) {
-        QList<Session *>::iterator s;
+    QList<Session *>::iterator s;
 
-        QList<Session *> list;
-        for (s = day->begin(); s != day->end(); ++s) {
-            Session *sess = *s;
+    QList<Session *> list;
+    for (s = day->begin(); s != day->end(); ++s) {
+        Session *sess = *s;
+        if (type == MT_JOURNAL || (type == MT_UNKNOWN && sess->type() != MT_JOURNAL) ||
+                sess->type() == type) {
+            list.append(*s);
+            qDebug() << "Purging session from " << (*s)->machine()->loaderName() << " ID:" << (*s)->session() << "["+QDateTime::fromTime_t((*s)->session()).toString()+"]";
+            qDebug() << "First Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realFirst()).toString();
+            qDebug() << "Last Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realLast()).toString();
             if (sess->type() == MT_CPAP) {
-                list.append(*s);
-                qDebug() << "Purging session ID:" << (*s)->session() << "["+QDateTime::fromTime_t((*s)->session()).toString()+"]";
-                qDebug() << "First Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realFirst()).toString();
-                qDebug() << "Last Time:" << QDateTime::fromMSecsSinceEpoch((*s)->realLast()).toString();
+                cpap = day->machine(MT_CPAP);
             }
+        } else {
+            qDebug() << "Skipping session from " << (*s)->machine()->loaderName() << " ID:" << (*s)->session() << "["+QDateTime::fromTime_t((*s)->session()).toString()+"]";
+        }
+    }
+
+    if (list.size() > 0) {
+        if (cpap) {
+            QFile rxcache(p_profile->Get("{" + STR_GEN_DataFolder + "}/RXChanges.cache" ));
+            rxcache.remove();
+
+            QFile sumfile(cpap->getDataPath()+"Summaries.xml.gz");
+            sumfile.remove();
         }
 
-        QFile rxcache(p_profile->Get("{" + STR_GEN_DataFolder + "}/RXChanges.cache" ));
-        rxcache.remove();
-
-        QFile sumfile(cpap->getDataPath()+"Summaries.xml.gz");
-        sumfile.remove();
-
 //        m->day.erase(m->day.find(date));
-
+        QSet<Machine *> machines;
         for (int i = 0; i < list.size(); i++) {
             Session *sess = list.at(i);
+            machines += sess->machine();
             sess->Destroy();    // remove the summary and event files
             delete sess;
         }
 
-        // save purge date where later import should start
-        QDate pd = cpap->purgeDate();
-        if (pd.isNull() || day->date() < pd)
-            cpap->setPurgeDate(day->date());
+        for (auto & mach : machines) {
+            mach->SaveSummaryCache();
+        }
+
+        if (cpap) {
+            // save purge date where later import should start
+            QDate pd = cpap->purgeDate();
+            if (pd.isNull() || day->date() < pd)
+                cpap->setPurgeDate(day->date());
+        }
+    } else {
+        // No data purged... could notify user?
+        return;
     }
-    day = p_profile->GetDay(date, MT_CPAP);
+    day = p_profile->GetDay(date, MT_UNKNOWN);
     Q_UNUSED(day);
 
     daily->clearLastDay();
diff --git a/oscar/mainwindow.h b/oscar/mainwindow.h
index 93834d6b..d2352277 100644
--- a/oscar/mainwindow.h
+++ b/oscar/mainwindow.h
@@ -268,6 +268,11 @@ class MainWindow : public QMainWindow
 
     //! \brief Destroy the CPAP data for the currently selected day, so it can be freshly imported again
     void on_actionPurge_Current_Day_triggered();
+    void on_actionPurgeCurrentDayOximetry_triggered();
+    void on_actionPurgeCurrentDaySleepStage_triggered();
+    void on_actionPurgeCurrentDayPosition_triggered();
+    void on_actionPurgeCurrentDayAllExceptNotes_triggered();
+    void on_actionPurgeCurrentDayAll_triggered();
 
     void on_action_Sidebar_Toggle_toggled(bool arg1);
 
@@ -372,6 +377,7 @@ private:
     QList<ImportPath> selectCPAPDataCards(const QString & prompt);
     void importCPAPDataCards(const QList<ImportPath> & datacards);
     void addMachineToMenu(Machine* mach, QMenu* menu);
+    void purgeDay(MachineType type);
 
 //    QString getWelcomeHTML();
     void FreeSessions();
diff --git a/oscar/mainwindow.ui b/oscar/mainwindow.ui
index 1b68d8c9..10d6b4ea 100644
--- a/oscar/mainwindow.ui
+++ b/oscar/mainwindow.ui
@@ -2903,7 +2903,19 @@ p, li { white-space: pre-wrap; }
        <string>Purge ALL Machine Data</string>
       </property>
      </widget>
-     <addaction name="actionPurge_Current_Day"/>
+     <widget class="QMenu" name="menuPurge_Current_Selected_Day">
+      <property name="title">
+       <string>Purge &amp;Current Selected Day</string>
+      </property>
+      <addaction name="actionPurge_Current_Day"/>
+      <addaction name="actionPurgeCurrentDayOximetry"/>
+      <addaction name="actionPurgeCurrentDaySleepStage"/>
+      <addaction name="actionPurgeCurrentDayPosition"/>
+      <addaction name="separator"/>
+      <addaction name="actionPurgeCurrentDayAllExceptNotes"/>
+      <addaction name="actionPurgeCurrentDayAll"/>
+     </widget>
+     <addaction name="menuPurge_Current_Selected_Day"/>
      <addaction name="menuPurge_CPAP_Data"/>
      <addaction name="separator"/>
      <addaction name="menuPurge_Oximetry_Data"/>
@@ -3084,11 +3096,6 @@ p, li { white-space: pre-wrap; }
     <string>Change &amp;User</string>
    </property>
   </action>
-  <action name="actionPurge_Current_Day">
-   <property name="text">
-    <string>Purge &amp;Current Selected Day</string>
-   </property>
-  </action>
   <action name="action_Sidebar_Toggle">
    <property name="checkable">
     <bool>true</bool>
@@ -3318,6 +3325,41 @@ p, li { white-space: pre-wrap; }
     <bool>true</bool>
    </property>
   </action>
+  <action name="actionPurge_Current_Selected_Day">
+   <property name="text">
+    <string>Purge Current Selected Day</string>
+   </property>
+  </action>
+  <action name="actionPurge_Current_Day">
+   <property name="text">
+    <string>&amp;CPAP</string>
+   </property>
+  </action>
+  <action name="actionPurgeCurrentDayOximetry">
+   <property name="text">
+    <string>&amp;Oximetry</string>
+   </property>
+  </action>
+  <action name="actionPurgeCurrentDaySleepStage">
+   <property name="text">
+    <string>&amp;Sleep Stage</string>
+   </property>
+  </action>
+  <action name="actionPurgeCurrentDayPosition">
+   <property name="text">
+    <string>&amp;Position</string>
+   </property>
+  </action>
+  <action name="actionPurgeCurrentDayAllExceptNotes">
+   <property name="text">
+    <string>&amp;All except Notes</string>
+   </property>
+  </action>
+  <action name="actionPurgeCurrentDayAll">
+   <property name="text">
+    <string>All including &amp;Notes</string>
+   </property>
+  </action>
  </widget>
  <customwidgets>
   <customwidget>

From 113d592af399b284888a4f073c69f8548fdaf36c Mon Sep 17 00:00:00 2001
From: kappa44 <6469032-kappa44@users.noreply.gitlab.com>
Date: Mon, 5 Apr 2021 12:12:16 +1000
Subject: [PATCH 15/20] Allow purge current day for all machine types

---
 Htmldocs/release_notes.html | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html
index e2dd1c9b..c5b72957 100644
--- a/Htmldocs/release_notes.html
+++ b/Htmldocs/release_notes.html
@@ -33,6 +33,7 @@
       <li>[fix] Purge currently selected day no longer deletes bookmarks for that day.</li>
       <li>[fix] Remove warning from Chromebook when importing from previously used local folder.</li>
       <li>[fix] Update link to Contec drivers.</li>
+      <li>[new] Purge Current Selected Day allows purge of each machine type separately</li>
     </ul>
     <p>
     <b>Changes and fixes in OSCAR v1.2.0</b>

From 889bc8842c8c6c11e7625d54f804ff79056c616c Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Fri, 9 Apr 2021 14:03:37 +1000
Subject: [PATCH 16/20] Logger.cpp: Add #define to allow all debug messages to
 be seen   I have DEFINES+=HARDLOG set up in my QT Debug configurations so I
 can always see messages in the event of a crash.

---
 oscar/logger.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/oscar/logger.cpp b/oscar/logger.cpp
index 6594f3e6..e3f4d108 100644
--- a/oscar/logger.cpp
+++ b/oscar/logger.cpp
@@ -70,7 +70,9 @@ void initializeLogger()
         s_LoggerRunning.lock();  // wait until the thread begins running
         s_LoggerRunning.unlock();  // we no longer need the lock
     }
+#ifndef HARDLOG
     qInstallMessageHandler(MyOutputHandler);  // NOTE: comment this line out when debugging a crash, otherwise the deferred output will mislead you.
+#endif
     if (b) {
         qDebug() << "Started logging thread";
     } else {

From 4234cb34a46f29d3119ac87ae0d8f20873f181de Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Fri, 9 Apr 2021 14:05:54 +1000
Subject: [PATCH 17/20] Fix problem with truncated per-minute data in graphs.

---
 oscar/SleepLib/loader_plugins/intellipap_loader.cpp | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
index e4a98e87..c40357ff 100644
--- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
@@ -89,6 +89,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
     QString newpath = path + SL_DIR;
     QString filename;
 
+    qDebug() << "DV5 Loader started";
 
     //////////////////////////
     // Parse the Settings File
@@ -1281,7 +1282,7 @@ bool load6DailySummaries () {
 #ifdef DEBUG6
         qDebug() << "DV6 S.BIN start" << dailyData.start_time
                  << "stop" << dailyData.stop_time
-                 << "at pressure?" << dailyData.atpressure_time;
+                 << "written" << dailyData.written;
 #endif
 
         dailyData.hours = float(rec->hours) / 10.0F;
@@ -2050,18 +2051,18 @@ bool load6PerMinute () {
                        << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
             continue;
         }
-
+/****
         // Look for a gap in DV6_L records.  They should be at one minute intervals.
         // If there is a gap, we are probably in a new session
         if (inSession && ((rec_ts1 - previousRecBegin) > 60)) {
-//            qDebug() << "L.BIN record gap, current" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
-//                     << "previous" << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss");
+            qDebug() << "L.BIN record gap, current" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
+                     << "previous" << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss");
             sess->set_last(maxleak->last());
             sess = nullptr;
             leak = maxleak = MV = TV = RR = Pressure = nullptr;
             inSession = false;
         }
-
+****/
         // Skip over sessions until we find one that this record is in
         while (rec_ts1 > sinfo->end) {
 #ifdef DEBUG6
@@ -2477,6 +2478,7 @@ bool init6Environment (const QString & path) {
 
 int IntellipapLoader::OpenDV6(const QString & path)
 {
+    qDebug() << "DV6 loader started";
     card_path = path + DV6_DIR;
 
     // 1. Prime the machine database's info field with this machine

From bbfa4aed6bd11c8b357315b0513ed0ef7dec95e6 Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Fri, 9 Apr 2021 14:08:30 +1000
Subject: [PATCH 18/20] Update release notes for latest changes (nothing
 important here)

---
 Htmldocs/release_notes.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html
index b20a2275..24370a09 100644
--- a/Htmldocs/release_notes.html
+++ b/Htmldocs/release_notes.html
@@ -15,7 +15,7 @@
     <br>Portions of OSCAR are © 2019-2021 by
     <i>The OSCAR Team</i></p>
     <ul>
-      <li>[new] Add support for DeVilbiss BLUE (DV6x) CPAP machines [experimental].</li>
+      <li>[new] Add support for DeVilbiss BLUE (DV6x) CPAP machines.</li>
       <li>[new] Additional Philips Respironics devices tested and fully supported:
         <ul>
           <li>DreamStation Go Auto (500G120)</li>
@@ -26,6 +26,7 @@
       <li>[new] Add support for DreamStation Go humidifier Target Time setting.</li>
       <li>[new] Add Bulgarian translation; update other languages.</li>
       <li>[new] Improve Somnopose import options.</li>
+      <li>[new] Purge Current Selected Day allows purge of each machine type separately</li>
       <li>[fix] Correct calculation of average leak rate on Welcome page.</li>
       <li>[fix] Correct installation of non-English Release Notes on Windows.</li>
       <li>[fix] About/Credits page now offers Google translations to other languages.</li>
@@ -41,7 +42,6 @@
       <li>[fix] Statistics headings will now be 99.5% or Max, depending on machine type and preference settings.</li>
       <li>[fix] Mark exported Journal backup file as UTF-8.</li>
       <li>[fix] Improve error message when unable to access OSCAR database.</li>
-      <li>[new] Purge Current Selected Day allows purge of each machine type separately</li>
     </ul>
     <p>
     <b>Changes and fixes in OSCAR v1.2.0</b>

From 037b13c73c8f138cd4f612321fb884ae6126408a Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Tue, 13 Apr 2021 17:47:15 -0600
Subject: [PATCH 19/20] Adjust labels on Daily and Overview pages to show 99.5%
 instead of Max when max is not used   Note that "Max" on the Statistics page
 really is the "max" and not 99.5%

---
 oscar/SleepLib/day.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/oscar/SleepLib/day.cpp b/oscar/SleepLib/day.cpp
index a752b6cf..a18f657a 100644
--- a/oscar/SleepLib/day.cpp
+++ b/oscar/SleepLib/day.cpp
@@ -182,7 +182,7 @@ QString Day::calcMiddleLabel(ChannelID code)
 }
 QString Day::calcMaxLabel(ChannelID code)
 {
-    return QObject::tr("%1 %2").arg(p_profile->general->prefCalcMax() ? QObject::tr("Peak") : STR_TR_Max).arg(schema::channel[code].label());
+    return QObject::tr("%1 %2").arg(p_profile->general->prefCalcMax() ? QObject::tr("99.5%") : STR_TR_Max).arg(schema::channel[code].label());
 }
 QString Day::calcPercentileLabel(ChannelID code)
 {

From 5a64262ff6d4fcef583717798f760a919a33f7e1 Mon Sep 17 00:00:00 2001
From: Guy Scharf <guy.oscar@moxis.com>
Date: Tue, 13 Apr 2021 17:48:39 -0600
Subject: [PATCH 20/20] Minor fix to #ifdefs in DV6 loader

---
 oscar/SleepLib/loader_plugins/intellipap_loader.cpp | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
index c40357ff..4a5a4717 100644
--- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp
@@ -923,12 +923,14 @@ private:
 
     int numWritten;     // Number of records written
     quint32 lastTimestamp;
+    unsigned int wrap_record;
 };
 
 bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
     if (!create_backups)
         return true;
 
+#ifdef ROLLBACKUP
     this->filetype = filetype;
 
     QDir hpath(history_path);
@@ -971,17 +973,20 @@ bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
             }
             record_length = hdr.recordLength;
 
-#ifdef ROLLBACKUP
             wrap_record = convertNum(hdr.recordStart);
             if (!histfile.seek(sizeof(DV6_HEADER) + (wrap_record-1) * record_length)) {
-                qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
-                file.close();
+                qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record
+                           << "in" + histfile.fileName() << histfile.error() << histfile.errorString();
+                histfile.close();
                 return false;
             }
-#endif
 
         }
     }
+#else
+    Q_UNUSED(filetype)
+    Q_UNUSED(newhdr)
+#endif
 
     return true;
 }