diff --git a/Htmldocs/release_notes.html b/Htmldocs/release_notes.html index e2dd1c9b..24370a09 100644 --- a/Htmldocs/release_notes.html +++ b/Htmldocs/release_notes.html @@ -12,16 +12,21 @@
http://www.apneaboard.com/wiki/index.php/OSCAR_Release_Notes

Changes and fixes in OSCAR v1.X.Y -
Portions of OSCAR are © 2019-2020 by +
Portions of OSCAR are © 2019-2021 by The OSCAR Team

Changes and fixes in OSCAR v1.2.0 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/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 @@ &About - &Относно + За &приложение @@ -22,12 +22,13 @@ Credits - + Заслуги GPL License - + As a whole this actually should read "Общ публичен лиценз на ГНУ", but that is a bit long on a tab. I think it would be acceptable to just say "license GPL" and then the explanation is written in the tab. The Bulgarian wikipedia page for the GPL gives a couple of translation options. + лиценз GPL @@ -37,27 +38,27 @@ Show data folder - + Покажи папката на данните About OSCAR %1 - + За предложението OSCAR %1 Sorry, could not locate About file. - + За съжаление, файлът За приложение не се намери. Sorry, could not locate Credits file. - + За съжаление, файлът Заслуги не се намери. Sorry, could not locate Release Notes. - + За съжаление, Бележки по изданието не се намери. @@ -72,12 +73,12 @@ As this is a pre-release version, it is recommended that you <b>back up your data folder manually</b> before proceeding, because attempting to roll back later may break things. - + Тъй като това е предварително издание, препоръчано е <b>да направите ръчно архивиране на своята папка с данни</b> преди да продължите, защото е възможно при по-късен опит за връщане назад нещата да се повредят. To see if the license text is available in your language, see %1. - + За да проверите дали съществува превода на лиценз на Вашия език, вижте %1. @@ -85,12 +86,12 @@ Could not find the oximeter file: - + Файлът на оксиметър не се намери: Could not open the oximeter file: - + Не може да се отвори файлът на оксиметър: @@ -108,12 +109,12 @@ Could not find the oximeter file: - + Файлът на оксиметър не се намери: Could not open the oximeter file: - + Не може да се отвори файлът на оксиметър: @@ -121,7 +122,7 @@ Checking for newer OSCAR versions - + Проверяваме за за нова версия на OSCAR @@ -191,12 +192,12 @@ I'm feeling ... - + Чувствам се ... If height is greater than zero in Preferences Dialog, setting weight here will show Body Mass Index (BMI) value - + Ако в настройки ръст е над нула, задаване на тегло тук ще се покажи стойността на индекса на телесната маса (ИТМ) @@ -211,7 +212,7 @@ Show/hide available graphs. - + Покажи или скрий достъпни графики. @@ -361,7 +362,7 @@ Unable to display Pie Chart on this system - + @@ -391,7 +392,7 @@ Sorry, this machine only provides compliance data. - + За съжаление, тази машина предоставя само данни за съответствие. @@ -441,7 +442,7 @@ <b>Please Note:</b> All settings shown below are based on assumptions that nothing has changed since previous days. - + <b>Моля, Забележете:</b>Всичките настройки, които са показани надолу, се основават на предположения, че нищо не се е променило от предишните дни. 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 ®ion) 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/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) 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 ®ion) 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 ®ion) 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 ®ion) 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) { //////////////////////////////////////////////////////////////////////////// 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) { 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 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 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 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; } diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index 7c6cedb8..4a5a4717 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 @@ -607,38 +608,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 +627,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 +765,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 +872,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 DailySummaries; +QMap SessionData; +SET_BIN_REC * settings; + unsigned int ep = 0; // Convert a 4-character number in DV6 data file to a standard int @@ -918,41 +902,204 @@ 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; + 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); + 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; + + 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" + histfile.fileName() << histfile.error() << histfile.errorString(); + histfile.close(); + return false; + } + + } + } +#else + Q_UNUSED(filetype) + Q_UNUSED(newhdr) +#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 +1134,11 @@ unsigned char * RollingFile::get() { file.close(); return nullptr; } +#ifdef ROLLBACKUP + if (!rb.save(dataBA)) { + qWarning() << "DV6 RollingBackup failed"; + } +#endif number_read++; @@ -995,21 +1147,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 DailySummaries; -QMap 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 +1200,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 +1261,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 +1282,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 + << "written" << dailyData.written; +#endif dailyData.hours = float(rec->hours) / 10.0F; dailyData.pressureSetMin = float(rec->pressureSetMin) / 10.0F; @@ -1135,6 +1323,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 +1501,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 +2014,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; } @@ -1848,18 +2056,18 @@ bool load6PerMinute (const QString & path) { << 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 @@ -1960,7 +2168,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 +2185,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 +2378,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 +2414,104 @@ 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; + qDebug() << "DV6 loader started"; + 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 +2519,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 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 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 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 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 diff --git a/oscar/daily.cpp b/oscar/daily.cpp index 84b8a86f..8511a362 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"); @@ -571,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; @@ -580,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) { @@ -1005,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; @@ -1530,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(""+date.toString(Qt::SystemLocaleLongDate)+""); previous_date=date; @@ -2206,6 +2226,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 +2243,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 +2290,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 +2465,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 +2487,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 +2518,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() 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(); 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 { 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::iterator s; + QList::iterator s; - QList list; - for (s = day->begin(); s != day->end(); ++s) { - Session *sess = *s; + QList 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 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 selectCPAPDataCards(const QString & prompt); void importCPAPDataCards(const QList & 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; } Purge ALL Machine Data - + + + Purge &Current Selected Day + + + + + + + + + + @@ -3084,11 +3096,6 @@ p, li { white-space: pre-wrap; } Change &User - - - Purge &Current Selected Day - - true @@ -3318,6 +3325,41 @@ p, li { white-space: pre-wrap; } true + + + Purge Current Selected Day + + + + + &CPAP + + + + + &Oximetry + + + + + &Sleep Stage + + + + + &Position + + + + + &All except Notes + + + + + All including &Notes + + 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 diff --git a/oscar/overview.cpp b/oscar/overview.cpp index 755f4b05..324206a3 100644 --- a/oscar/overview.cpp +++ b/oscar/overview.cpp @@ -231,8 +231,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