From 7f6128fe24c7f1dc65597dff1dae1057dc5c4129 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Sun, 6 Jun 2021 18:00:33 -0700 Subject: [PATCH 01/11] Improve dialog title when asking for CPAP data directory Previously, window title just said "Find Directory" and users could be confused about what directory they were supposed to be looking for. Now, window title says "Find your CPAP data card". --- oscar/mainwindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index 062e9e08..3739d245 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -1080,6 +1080,7 @@ QList MainWindow::selectCPAPDataCards(const QString & prompt) w.setDirectory(folder); w.setFileMode(QFileDialog::Directory); w.setOption(QFileDialog::ShowDirsOnly, true); + w.setWindowTitle(tr("Find your CPAP data card")); // This doesn't work on WinXP From e7ce6f00f162db46abbc81a817c0aee507fb527b Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Fri, 18 Jun 2021 13:31:56 -0700 Subject: [PATCH 02/11] DeVilbiss BLUE (DV6x) loader Backups complete and Build from Backup works Low resolution graphs corrected --- .../loader_plugins/intellipap_loader.cpp | 520 ++++++++++++------ oscar/SleepLib/machine_common.cpp | 2 +- oscar/SleepLib/machine_common.h | 2 +- 3 files changed, 364 insertions(+), 160 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index 2263c974..1bad34e0 100644 --- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp +++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp @@ -10,6 +10,7 @@ * for more details. */ #include +#include #include "intellipap_loader.h" @@ -357,7 +358,7 @@ int IntellipapLoader::OpenDV5(const QString & path) sess->AddEventList(CPAP_Snore, EVL_Event); sess->AddEventList(CPAP_Obstructive, EVL_Event); - sess->AddEventList(CPAP_VSnore, EVL_Event); + sess->AddEventList(INTP_SnoreFlag, EVL_Event); sess->AddEventList(CPAP_Hypopnea, EVL_Event); sess->AddEventList(CPAP_NRI, EVL_Event); sess->AddEventList(CPAP_LeakFlag, EVL_Event); @@ -375,7 +376,7 @@ int IntellipapLoader::OpenDV5(const QString & path) } } - QDateTime d = QDateTime::fromTime_t(sid); + QDateTime d = QDateTime::fromSecsSinceEpoch(sid); qDebug() << sid << "has double ups" << d; /*Session *sess=Sessions[sid]; Sessions.erase(Sessions.find(sid)); @@ -483,7 +484,7 @@ int IntellipapLoader::OpenDV5(const QString & path) sess->eventlist[CPAP_Snore][0]->AddEvent(time, m_buffer[pos + 0x4]); //4/5?? if (m_buffer[pos+0x4] > 0) { - sess->eventlist[CPAP_VSnore][0]->AddEvent(time, m_buffer[pos + 0x5]); + sess->eventlist[INTP_SnoreFlag][0]->AddEvent(time, m_buffer[pos + 0x5]); } // 0x0f == Leak Event @@ -720,7 +721,7 @@ PACK (struct DV6_S_REC{ unsigned char checksum; //54 }); -// DV6 SET.BIN - structure of the entire file +// DV6 SET.BIN - structure of the entire settings file PACK (struct SET_BIN_REC { char unknown_00; // assuming file version char serial[11]; // null terminated @@ -868,13 +869,16 @@ struct DV6_SessionInfo { unsigned int begin; unsigned int end; unsigned int written; - bool haveHighResData; +// bool haveHighResData; + unsigned int firstHighRes; + unsigned int lastHighRes; CPAPMode mode = MODE_UNKNOWN; }; QString card_path; QString backup_path; QString history_path; +QString rebuild_path; MachineInfo info; Machine * mach = nullptr; @@ -882,6 +886,8 @@ Machine * mach = nullptr; bool rebuild_from_backups = false; bool create_backups = false; +QStringList inputFilePaths; + QMap DailySummaries; QMap SessionData; SET_BIN_REC * settings; @@ -909,84 +915,119 @@ public: ~RollingBackup () { } - bool open (const QString filetype, DV6_HEADER * newhdr); // Open the file + bool open (const QString filetype, DV6_HEADER * newhdr, QByteArray * startTime); // Open the file bool close(); // close the file - bool save(QByteArray dataBA); // save the next record in the file + bool save(const QByteArray &dataBA); // save the next record in the file private: - //DV6_HEADER hdr; // file header + DV6_HEADER hdr; // file header QString filetype; - QFile hFile; + QFile histfile; - //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 + const qint64 maxHistFileSize = 10000000; // Maximum size of file before we create a new file, in MB (40 MB) + // (While 40e6 would be easier to understand, 40e6 is a double, not an int) - //int numWritten; // Number of records written - //quint32 lastTimestamp; - //unsigned int wrap_record; + unsigned int lastTimeInFile; // Timestamp of last data record in history file + int numWritten; // Number of records written }; -bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) { +QStringList getHistoryFileNames (const QString filetype, bool reversed = false) { + QStringList filters; + QDir hpath(history_path); + + filters.append(filetype); // Assume one-letter file name like "S.BIN" + filters[0].insert(1, "_*"); // Add a wild card like "S_*.BIN" + hpath.setNameFilters(filters); + hpath.setFilter(QDir::Files); + hpath.setSorting(QDir::Name); + if (reversed) hpath.setSorting(QDir::Name | QDir::Reversed); + + return hpath.entryList(); // Get list of files +} + +QString getNewFileName (QString filetype, QByteArray * startTime, int offset=0) { + unsigned char startTimeChar[5]; + for (int i = 0; i < 4; i++) + startTimeChar[i] = startTime->at(offset+i); + unsigned int ts = convertTime(startTimeChar); + QString newfile = filetype.left(1) + "_" + QDateTime::fromSecsSinceEpoch(ts).toString("yyyyMMdd") + ".BIN"; + qDebug() << "DV6 getNewFileName returns" << newfile; + return newfile; +} + +bool RollingBackup::open (const QString filetype, DV6_HEADER * inputhdr, QByteArray * startTimeOfBackup) { if (!create_backups) return true; -#ifdef ROLLBACKUP + QDir hpath(history_path); + QString historypath = hpath.absolutePath() + "/"; + int histfilesize = 0; + this->filetype = filetype; - QDir hpath(history_path); - QStringList filters; - + bool needNewFile = false; + memcpy (&hdr, inputhdr, sizeof(DV6_HEADER)); 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; + QStringList fileNames = getHistoryFileNames(filetype, true); // 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; + lastTimeInFile = 0; + histfile.setFileName(historypath + getNewFileName (filetype, startTimeOfBackup)); + needNewFile= true; } - + // We have an existing history record if (! fileNames.isEmpty()) { + histfile.setFileName(historypath + fileNames.first()); // File names are in reverse order, so latest is first + + // Open and read history file header and save the header + if (!histfile.open(QIODevice::ReadWrite)) { + qWarning() << "DV6 rb(open) could not open" << fileNames.first() << "for readwrite, error code" << histfile.error() << histfile.errorString(); + return false; + } + histfilesize = histfile.size(); + QByteArray dataBA = histfile.read(sizeof(DV6_HEADER)); + memcpy (&hdr, dataBA.data(), sizeof(DV6_HEADER)); + lastTimeInFile = convertTime(hdr.lasttime); + // See if this file is large enough that we want to create a new file + // If it is large, we'll start a new file. if (histfile.size() > maxHistFileSize) { - memcpy (&hdr, newhdr, sizeof(DV6_HEADER)); - for (int i = 0; i < 4; i++) - hdr.recordStart[i] = 0; + QString nextFile = historypath + getNewFileName (filetype, &dataBA, 51); + QString hh = histfile.fileName(); - 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(); + if (hh != nextFile) { + lastTimeInFile = convertTime(hdr.lasttime); histfile.close(); - return false; + // Update header saying we are starting at record 0 + for (int i = 0; i < 4; i++) + hdr.recordStart[i] = 0; + histfile.setFileName(nextFile); + needNewFile = true; } - } } -#else - Q_UNUSED(filetype) - Q_UNUSED(newhdr) -#endif + + if (needNewFile) { + if (!histfile.open(QIODevice::ReadWrite)) { + qWarning() << "DV6 rb(open) could not create new file" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString(); + return false; + } + if (histfile.write((char *)&hdr.unknown, sizeof(DV6_HEADER)) != sizeof(DV6_HEADER)) { + qWarning() << "DV6 rb(open) could not write header to new file" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString(); + histfile.close(); + return false; + } + } else { +// qDebug() << "DV6 rb(open) history file size" << histfilesize; + histfile.seek(histfilesize); + } return true; } @@ -994,13 +1035,62 @@ bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) { bool RollingBackup::close() { if (!create_backups) return true; + + qint32 size = histfile.size(); + + if (!histfile.seek(0)) { + qWarning() << "DV6 rb(close) unable to seek to file beginning" << histfile.error() << histfile.errorString(); + histfile.close(); + return false; + } + + quint32 sizehdr = sizeof(DV6_HEADER); + quint32 reclen = hdr.recordLength; + quint32 wrap_point = (size - sizehdr) / reclen; + + hdr.recordStart[0] = wrap_point & 0xff; + hdr.recordStart[1] = (wrap_point >> 8) & 0xff; + hdr.recordStart[2] = (wrap_point >> 16) & 0xff; + hdr.recordStart[3] = (wrap_point >> 24) & 0xff; + + if (histfile.write((char *)&hdr, sizeof(DV6_HEADER)) != sizeof(DV6_HEADER)) { + qWarning() << "DV6 rb(close) could not write header to file" << histfile.fileName() << "error code" << histfile.error() << histfile.errorString(); + histfile.close(); + return false; + } + + histfile.close(); + qDebug() << "DV6 rb(close) wrote" << numWritten << "records."; return true; } -bool RollingBackup::save(QByteArray dataBA) { - Q_UNUSED(dataBA) +bool RollingBackup::save(const QByteArray &dataBA) { + if (!create_backups) return true; + + unsigned char * data = (unsigned char *)dataBA.data(); + unsigned int thisTimeStamp = convertTime(data); + + if (thisTimeStamp > lastTimeInFile) { // Is this data new to us? + // If so, save it to the history file. + if (histfile.write(dataBA) == -1) { + qWarning() << "DV6 rb(save) could not save record" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString(); + histfile.close(); + return false; + } + memcpy(&hdr.lasttime, data, 4); +// if (!histfile.seek(histfile.pos() + dataBA.length())) +// qWarning() << "DV6 rb(save) failed respositioning" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString(); + numWritten++; + } +/*** + else { + qDebug() << "DV6 rb(save) skipping record" << numWritten << QDateTime::fromSecsSinceEpoch(thisTimeStamp).toString("MM/dd/yyyy hh:mm:ss") + << "last in file" << QDateTime::fromSecsSinceEpoch(lastTimeInFile).toString("MM/dd/yyyy hh:mm:ss"); + } +***/ + return true; } @@ -1018,7 +1108,7 @@ public: hdr = nullptr; } - bool open (QString fn); // Open the file + bool open (QString fn, bool getNext = false); // Open the file bool close(); // close the file unsigned char * get(); // read the next record in the file @@ -1043,13 +1133,33 @@ private: unsigned char * data = nullptr; // record pointer }; -bool RollingFile::open(QString filetype) { +bool RollingFile::open(QString filetype, bool getNext) { filename = filetype; - file.setFileName(card_path + "/" +filetype); + + if (rebuild_from_backups) { + // Building from backup + if (!getNext) { // Initialize on first call + inputFilePaths.clear(); + QStringList histFileNames = getHistoryFileNames(filetype); + qDebug() << "DV6 rf(open) History file names" << histFileNames; + for (int i=0; i < histFileNames.size(); i++) { + file.setFileName(history_path + "/" + histFileNames.at(i)); + inputFilePaths.append(file.fileName()); + } + } + if (inputFilePaths.empty()) + return false; + file.setFileName(inputFilePaths.at(0)); + inputFilePaths.removeAt(0); + + } else { + file.setFileName(card_path + "/" +filetype); + inputFilePaths.clear(); + } if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "DV6 RollingFile could not open" << filename << "for reading, error code" << file.error() << file.errorString(); + qWarning() << "DV6 rf(open) could not open" << filename << "for reading, error code" << file.error() << file.errorString(); return false; } @@ -1068,30 +1178,48 @@ bool RollingFile::open(QString filetype) { // 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(); + // Seek to oldest data record in file, which is always at the wrap point + // wrap_record is the C offset where the next data record is to be written. + // Since C offsets begin with zero, it is also the number of records in the file. + int seekpos = sizeof(DV6_HEADER) + wrap_record * record_length; + if (!file.seek(seekpos)) { + qWarning() << "DV6 rf(open) 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() << "DV6 rf(open)" << filetype << "positioning to oldest record at pos" << seekpos << "after seek" << file.pos(); - qDebug() << "DV6 RollingFile opening" << filename << "at wrap record" << wrap_record; + if (file.atEnd()) { + file.seek(sizeof(DV6_HEADER)); + } + dataBA = file.read(4); // Read timestamp of newest data record + file.seek(seekpos); // Reset read position before what we just read so we start reading data records here + if (!rb.open(filetype, hdr, &dataBA)) { + qWarning() << "DV6 rf(open) failed"; + file.close(); + return false; + } + + qDebug() << "DV6 rf(open)" << filename << "at wrap record" << wrap_record << "now at pos" << file.pos(); return true; } bool RollingFile::close() { + +/*** Works for backing up but prevents chart appearing for the last day + // Flush any additional input that has not been backed up + if (create_backups) { + do { + DV6_U_REC * rec = (DV6_U_REC *) get(); + if (rec == nullptr) + break; + } while (true); + } +***/ + file.close(); -#ifdef ROLLBACKUP rb.close(); -#endif if (data) delete [] data; @@ -1105,44 +1233,54 @@ bool RollingFile::close() { unsigned char * RollingFile::get() { +// int readpos; record_number++; // If we have found the wrap record again, we are done - if (wrapping && (record_number == wrap_record)) - return nullptr; - - // Hare we reached end of file and need to wrap around to beginning? - if (file.atEnd()) { - if (wrapping) { - qDebug() << "DV6 RollingFile wrap - second time through"; + if (wrapping && record_number == wrap_record) { + // Unless we are rebuilding from backup and may have more files + if (rebuild_from_backups && !inputFilePaths.empty()) { + qDebug() << "DV6 rf(get) closing" << file.fileName(); + file.close(); + open(inputFilePaths.at(0), true); + } else { return nullptr; } - qDebug() << "DV6 RollingFile wrapping to beginning of data in" << filename << "record number is" << record_number-1 << "records read" << number_read; - record_number = 0; + } + + // Have we reached end of file and need to wrap around to beginning? + if (file.atEnd()) { + if (wrapping) { + qDebug() << "DV6 rf(get) wrap - second time through"; + return nullptr; + } + qDebug() << "DV6 rf(get) wrapping to beginning of data in" << filename << "record number is" << record_number-1 << "records read" << number_read; + record_number = 1; wrapping = true; if (!file.seek(sizeof(DV6_HEADER))) { file.close(); - qWarning() << "DV6 RollingFile unable to seek to first data record in file"; + qWarning() << "DV6 rf(get) unable to seek to first data record in file"; return nullptr; } + qDebug() << "DV6 rf(get) #" << record_number << "now at pos" << file.pos(); } QByteArray dataBA; +// readpos = file.pos(); dataBA=file.read(record_length); // read next record if (dataBA.size() != record_length) { - qWarning() << "DV6 RollingFile record" << record_number << "wrong length"; + qWarning() << "DV6 rf(get) #" << record_number << "wrong length"; file.close(); return nullptr; } -#ifdef ROLLBACKUP + if (!rb.save(dataBA)) { - qWarning() << "DV6 RollingBackup failed"; + qWarning() << "DV6 rf(get) failed"; } -#endif number_read++; -// qDebug() << "RollingFile read" << filename << "record number" << record_number << "of length" << record_length << "number read so far" << number_read; +// qDebug() << "DV6 rf(get)" << filename << "at start pos" << readpos << "end pos" << file.pos() << "record number" << record_number << "of length" << record_length << "number read so far" << number_read; memcpy (data, (unsigned char *) dataBA.data(), record_length); return data; } @@ -1163,15 +1301,6 @@ QByteArray fileChecksum(const QString &fileName, 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 split time in preferences (usually noon) QDate getNominalDate (QDateTime dt) { QDate d = dt.date(); @@ -1213,15 +1342,17 @@ bool load6Sessions () { // big endian ts1 = convertTime(rec->begin); // session start time (this is also the session id) ts2 = convertTime(rec->end); // session end time -#ifdef DEBUG6 - qDebug() << "U.BIN Session" << QDateTime::fromTime_t(ts1).toString("MM/dd/yyyy hh:mm:ss") << ts1 << "to" << QDateTime::fromTime_t(ts2).toString("MM/dd/yyyy hh:mm:ss") << ts2; -#endif +//#ifdef DEBUG6 + qDebug() << "U.BIN Session" << QDateTime::fromSecsSinceEpoch(ts1).toString("MM/dd/yyyy hh:mm:ss") << ts1 << "to" << QDateTime::fromSecsSinceEpoch(ts2).toString("MM/dd/yyyy hh:mm:ss") << ts2; +//#endif sinfo.sess = nullptr; sinfo.dailyData = nullptr; sinfo.begin = ts1; sinfo.end = ts2; sinfo.written = 0; - sinfo.haveHighResData = false; +// sinfo.haveHighResData = false; + sinfo.firstHighRes = 0; + sinfo.lastHighRes = 0; SessionData[ts1] = sinfo; } while (true); @@ -1241,6 +1372,8 @@ bool load6Settings (const QString & path) { QByteArray dataBA; QFile f(path+"/"+SET_BIN); + if (rebuild_from_backups) + f.setFileName(rebuild_path+"/"+SET_BIN); if (f.open(QIODevice::ReadOnly)) { // Read and parse entire SET.BIN file @@ -1361,6 +1494,8 @@ bool load6VersionInfo(const QString & path) { QByteArray str; QFile f(path+"/VER.BIN"); + if (rebuild_from_backups) + f.setFileName(rebuild_path+"/VER.BIN"); info.series = "DV6"; info.brand = "DeVilbiss"; @@ -1434,19 +1569,21 @@ int create6Sessions() { if (mach->SessionExists(sid)) { // skip already imported sessions.. - qDebug() << "Session already exists" << QDateTime::fromTime_t(sid).toString("MM/dd/yyyy hh:mm:ss"); + qDebug() << "Session already exists" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss"); } else if (sinfo->sess == nullptr) { // process new sessions sess = new Session(mach, sid); #ifdef DEBUG6 - qDebug() << "Creating session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "to" << QDateTime::fromTime_t(sinfo->end).toString("MM/dd/yyyy hh:mm:ss"); + qDebug() << "Creating session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "to" << QDateTime::fromSecsSinceEpoch(sinfo->end).toString("MM/dd/yyyy hh:mm:ss"); #endif sinfo->sess = sess; sinfo->dailyData = nullptr; sinfo->written = 0; - sinfo->haveHighResData = false; +// sinfo->haveHighResData = false; + sinfo->firstHighRes = 0; + sinfo->lastHighRes = 0; sess->really_set_first(quint64(sinfo->begin) * 1000L); sess->really_set_last(quint64(sinfo->end) * 1000L); @@ -1464,9 +1601,9 @@ int create6Sessions() { sess->AddEventList(CPAP_MinuteVent, EVL_Event); sess->AddEventList(CPAP_RespRate, EVL_Event); sess->AddEventList(CPAP_Snore, EVL_Event); + sess->AddEventList(INTP_SnoreFlag, EVL_Event); sess->AddEventList(CPAP_Obstructive, EVL_Event); -// sess->AddEventList(CPAP_VSnore, EVL_Event); sess->AddEventList(CPAP_Hypopnea, EVL_Event); sess->AddEventList(CPAP_NRI, EVL_Event); // sess->AddEventList(CPAP_LeakFlag, EVL_Event); @@ -1483,7 +1620,7 @@ int create6Sessions() { //?? SessionEnd[z] = 0; //?? break; //?? } - qDebug() << sid << "has double ups" << QDateTime::fromTime_t(sid).toString("MM/dd/yyyy hh:mm:ss"); + qDebug() << sid << "has double ups" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss"); /*Session *sess=Sessions[sid]; Sessions.erase(Sessions.find(sid)); @@ -1560,9 +1697,9 @@ bool load6HighResData () { if (rec_ts1 < previousRecBegin) { qWarning() << "R.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev" - << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin + << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin << "this" - << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1; + << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1; continue; } @@ -1592,8 +1729,8 @@ bool load6HighResData () { // Skip over sessions until we find one that this record is in while (rec_ts1 > sinfo->end) { #ifdef DEBUG6 - qDebug() << "R.BIN - skipping session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") - << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") + qDebug() << "R.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") + << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "record" << rf.recnum(); #endif if (inSession && sess) { @@ -1657,10 +1794,13 @@ bool load6HighResData () { qint64 ti = qint64(rec_ts1) * 1000; flow->AddWaveform(ti,R->breath,50,2000); pressure->AddWaveform(ti, &R->pressure1, 2, 2000); - sinfo->haveHighResData = true; + if (sinfo->firstHighRes == 0 || sinfo->firstHighRes > rec_ts1) sinfo->firstHighRes = rec_ts1; + if (sinfo->lastHighRes == 0 || sinfo->lastHighRes < rec_ts1+2) sinfo->lastHighRes = rec_ts1+2; +// sinfo->haveHighResData = true; if (sess->first() == 0) qWarning() << "first = 0 - 1442"; + ////////////////////////////////////////////////////////////////// // Show Flow Limitation Events as a graph ////////////////////////////////////////////////////////////////// @@ -2050,18 +2190,20 @@ bool load6PerMinute () { rec_ts1 = convertTime(rec->timestamp); if (rec_ts1 < previousRecBegin) { +#ifdef DEBUG6 qWarning() << "L.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev" - << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin + << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin << "this" - << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1; + << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1; +#endif 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::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") + << "previous" << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss"); sess->set_last(maxleak->last()); sess = nullptr; leak = maxleak = MV = TV = RR = Pressure = nullptr; @@ -2071,7 +2213,7 @@ bool load6PerMinute () { // Skip over sessions until we find one that this record is in while (rec_ts1 > sinfo->end) { #ifdef DEBUG6 - qDebug() << "L.BIN - skipping session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); + qDebug() << "L.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); #endif if (inSession && sess) { // Close the open session and update the min and max @@ -2095,9 +2237,9 @@ bool load6PerMinute () { if (rec_ts1 < previousRecBegin) { qWarning() << "L.BIN - Corruption/Out of sequence data found, stopping import, prev" - << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") + << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << "this" - << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); + << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); break; } @@ -2114,10 +2256,9 @@ bool load6PerMinute () { if (sess->last()/1000 > sinfo->end) sinfo->end = sess->last()/1000; - if (!sinfo->haveHighResData) { +// if (!sinfo->haveHighResData) { // Don't use this pressure if we already have higher resolution data - Pressure = sess->AddEventList(CPAP_Pressure, EVL_Event); - } + if (sinfo->mode == MODE_UNKNOWN) { if (rec->pressureLimitLow != rec->pressureLimitHigh) { sess->settings[CPAP_PressureMin] = rec->pressureLimitLow / 10.0f; @@ -2141,7 +2282,29 @@ bool load6PerMinute () { leak->AddEvent(ti, rec->avgLeak); //??? RR->AddEvent(ti, rec->breathRate); - if (Pressure) Pressure->AddEvent(ti, rec->avgPressure / 10.0f); // average pressure + if ( sinfo->firstHighRes == 0 // No high res data + || rec_ts1 < sinfo->firstHighRes // Before high res data begins + || ((rec_ts1 > (sinfo->lastHighRes+2)) && (sinfo->lastHighRes > 0))) // or after high res data ends + { + if (!Pressure) + Pressure = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1f); + +// if (sinfo->firstHighRes == 0) { + Pressure->AddEvent(ti, rec->avgPressure); // average pressure for next minute + Pressure->AddEvent(ti + 59998, rec->avgPressure); // end of pressure block +// } else { +// for (int i = 0; i < 60; i++) { +// Pressure->AddEvent(ti+i, rec->avgPressure); // average pressure for next minute +// } +// } + } + +/*** + if (Pressure) + qDebug() << "Lowres pressure" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") + << "rec_ts1" << rec_ts1 << "firstHighRes" << sinfo->firstHighRes << "last" << sinfo->lastHighRes + << "Pressure" << rec->avgPressure / 10.0f; +***/ unsigned tv = rec->tidalVolume6 + (rec->tidalVolume7 << 8); MV->AddEvent(ti, rec->breathRate * tv / 1000.0 ); @@ -2177,13 +2340,14 @@ bool load6EventData () { EventList * OA = nullptr; EventList * CA = nullptr; - EventList * H = nullptr; + EventList * H = nullptr; EventList * RE = nullptr; EventList * PB = nullptr; EventList * LL = nullptr; EventList * EP = nullptr; EventList * SN = nullptr; EventList * FL = nullptr; +// EventList * FLG = nullptr; if (!rf.open("E.BIN")) { qWarning() << "DV6 Unable to open E.BIN"; @@ -2209,7 +2373,7 @@ bool load6EventData () { // Skip over sessions until we find one that this record is in while (rec_ts1 > sinfo->end) { #ifdef DEBUG6 - qDebug() << "E.BIN - skipping session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); + qDebug() << "E.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); #endif if (inSession) { // Close the open session and update the min and max @@ -2227,11 +2391,14 @@ bool load6EventData () { sess->set_last(LL->last()); if (EP->last() > 0) sess->set_last(EP->last()); - if (SN->last() > 0) - sess->set_last(SN->last()); if (FL->last() > 0) sess->set_last(FL->last()); - + if (SN->last() > 0) + sess->set_last(SN->last()); +/*** + if (FLG->last() > 0) + sess->set_last(FLG->last()); +***/ sess = nullptr; H = CA = RE = OA = PB = LL = EP = SN = FL = nullptr; inSession = false; @@ -2245,16 +2412,18 @@ bool load6EventData () { // If we have data beyond last session, we are in trouble (for unknown reasons) if (sinfo == SessionData.end()) { - qWarning() << "DV6 E.BIN import ran out of sessions to match flow data"; + qWarning() << "DV6 E.BIN import ran out of sessions," + << "event data begins" + << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); break; } if (rec_ts1 < previousRecBegin) { - qWarning() << "E.BIN - Corruption/Out of sequence data found, stopping import, prev" - << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") - << "this" - << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); - break; + qWarning() << "DV6 E.BIN - Out of sequence data found, skipping, prev" + << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") + << "this event" + << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss"); + continue; // break; } // Check if record belongs in this session or a future session @@ -2268,10 +2437,12 @@ bool load6EventData () { PB = sess->AddEventList(CPAP_PB, EVL_Event); LL = sess->AddEventList(CPAP_LargeLeak, EVL_Event); EP = sess->AddEventList(CPAP_ExP, EVL_Event); -// SN = sess->AddEventList(CPAP_VSnore, EVL_Event); + SN = sess->AddEventList(INTP_SnoreFlag, EVL_Event); FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event); - SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2)); +// FLG = sess->AddEventList(CPAP_FLG, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2)); +// SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2)); + inSession = true; } } @@ -2281,11 +2452,12 @@ bool load6EventData () { // TODO: We don't know what is really going on here. Is it sloppiness on the part of the DV6 in recording time stamps? qint64 ti = qint64(rec_ts1 - (duration/2)) * 1000L; if (duration < 0) { - qDebug() << "E.BIN at" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") + qDebug() << "E.BIN at" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "reports duration of" << duration - << "ending" << QDateTime::fromTime_t(rec_ts2).toString("MM/dd/yyyy hh:mm:ss"); + << "ending" << QDateTime::fromSecsSinceEpoch(rec_ts2).toString("MM/dd/yyyy hh:mm:ss"); } int code = rec->event_type; +/*** ////////////////////////////////////////////////////////////////// // Show Snore Events as a graph ////////////////////////////////////////////////////////////////// @@ -2293,6 +2465,15 @@ bool load6EventData () { qint16 severity = rec->event_severity; SN->AddWaveform(ti, &severity, 1, duration*1000); } + + ////////////////////////////////////////////////////////////////// + // Show Flow Limit Events as a graph + ////////////////////////////////////////////////////////////////// + if (code == 10) { + qint16 severity = rec->event_severity; + FLG->AddWaveform(ti, &severity, 1, duration*1000); + } +***/ if (rec->event_severity >= 3) switch (code) { case 1: @@ -2300,33 +2481,33 @@ bool load6EventData () { break; case 2: OA->AddEvent(ti, duration); -// qDebug() << "E.BIN - OA" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); +// qDebug() << "E.BIN - OA" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); break; case 4: H->AddEvent(ti, duration); break; case 5: RE->AddEvent(ti, duration); -// qDebug() << "E.BIN - RERA" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); +// qDebug() << "E.BIN - RERA" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); break; case 8: // snore SN->AddEvent(ti, duration); -// qDebug() << "E.BIN - Snore" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); +// qDebug() << "E.BIN - Snore" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); break; case 9: // expiratory puff EP->AddEvent(ti, duration); -// qDebug() << "E.BIN - exhale puff" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); +// qDebug() << "E.BIN - exhale puff" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); break; case 10: // flow limitation FL->AddEvent(ti, duration); -// qDebug() << "E.BIN - flow limit" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); +// qDebug() << "E.BIN - flow limit" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); break; case 11: // periodic breathing PB->AddEvent(ti, duration); break; case 12: // large leaks LL->AddEvent(ti, duration); -// qDebug() << "E.BIN - large leak" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); +// qDebug() << "E.BIN - large leak" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum(); break; case 13: // pressure change break; @@ -2359,7 +2540,7 @@ int addSessions() { } #ifdef DEBUG6 else - qDebug() << "Added session" << sess->session() << QDateTime::fromTime_t(sess->session()).toString("MM/dd/yyyy hh:mm:ss");; + qDebug() << "Added session" << sess->session() << QDateTime::fromSecsSinceEpoch(sess->session()).toString("MM/dd/yyyy hh:mm:ss");; #endif // Update indexes, process waveform and perform flagging @@ -2370,8 +2551,8 @@ int addSessions() { // Unload them from memory sess->TrashEvents(); - } else - qWarning() << "addSessions: session pointer is null"; + } // else + // qWarning() << "addSessions: session pointer is null"; } return SessionData.size(); @@ -2391,25 +2572,11 @@ bool backup6 (const QString & path) { QDir ipath(path); QDir cpath(card_path); QDir bpath(backup_path); - - if ( ! bpath.exists()) { - if ( ! bpath.mkpath(backup_path) ) { - qWarning() << "Could not create DV6 backup directory" << backup_path; - return false; - } - } + QDir hpath(history_path); // Copy input data to backup location copyPath(ipath.absolutePath(), bpath.absolutePath()); - // Create history directory for dated backups - QDir hpath(history_path); - if ( ! hpath.exists()) - if ( ! hpath.mkpath(history_path)) { - qWarning() << "Could not create DV6 archive directory" << history_path; - return false; - } - // Create archive of settings file if needed (SET.BIN) bool backup_settings = true; @@ -2460,10 +2627,12 @@ bool init6Environment (const QString & path) { backup_path = mach->getBackupPath(); history_path = backup_path + "/HISTORY"; + rebuild_path = backup_path + "/DV6"; // Compare QDirs rather than QStrings because separators may be different, especially on Windows. QDir ipath(path); QDir bpath(backup_path); + QDir hpath(history_path); if (ipath == bpath) { // Don't create backups if importing from backup folder @@ -2472,6 +2641,20 @@ bool init6Environment (const QString & path) { } else { rebuild_from_backups = false; create_backups = p_profile->session->backupCardData(); + + if ( ! bpath.exists()) { + if ( ! bpath.mkpath(backup_path) ) { + qWarning() << "Could not create DV6 backup directory" << backup_path; + return false; + } + } + if ( ! hpath.exists()) { + if ( ! hpath.mkpath(history_path) ) { + qWarning() << "Could not create DV6 backup HISTORY directory" << history_path; + return false; + } + } + } return true; @@ -2486,6 +2669,10 @@ int IntellipapLoader::OpenDV6(const QString & path) qDebug() << "DV6 loader started"; card_path = path + DV6_DIR; + emit updateMessage(QObject::tr("Getting Ready...")); + emit setProgressValue(0); + QCoreApplication::processEvents(); + // 1. Prime the machine database's info field with this machine info = newInfo(); @@ -2505,10 +2692,16 @@ int IntellipapLoader::OpenDV6(const QString & path) if (!load6DailySummaries()) return -1; + emit updateMessage(QObject::tr("Backing up files...")); + QCoreApplication::processEvents(); + // 6. Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine) if (!backup6(path)) return -1; + emit updateMessage(QObject::tr("Reading data files...")); + QCoreApplication::processEvents(); + // 7. U.BIN - Open and parse session list and create a list of session times // (S.BIN must already be loaded) if (!load6Sessions()) @@ -2530,6 +2723,9 @@ int IntellipapLoader::OpenDV6(const QString & path) if (!load6EventData()) return -1; + emit updateMessage(QObject::tr("Finishing up...")); + QCoreApplication::processEvents(); + // Finalize input return addSessions(); } @@ -2580,6 +2776,14 @@ void IntellipapLoader::initChannels() QObject::tr("Intellipap pressure relief level."), QObject::tr("SmartFlex Level"), "", DEFAULT, Qt::green)); + + channel.add(GRP_CPAP, new Channel(INTP_SnoreFlag = 0xe301, FLAG, MT_CPAP, SESSION, + "INTP_SnoreFlag", + QObject::tr("Snore"), + QObject::tr("Snoring event."), + QObject::tr("SN"), + STR_UNIT_EventsPerHour, DEFAULT, QColor("#e20004"))); + } bool intellipap_initialized = false; diff --git a/oscar/SleepLib/machine_common.cpp b/oscar/SleepLib/machine_common.cpp index c6008bc0..7828b509 100644 --- a/oscar/SleepLib/machine_common.cpp +++ b/oscar/SleepLib/machine_common.cpp @@ -28,7 +28,7 @@ ChannelID CPAP_IPAP, CPAP_IPAPLo, CPAP_IPAPHi, CPAP_EPAP, CPAP_EPAPLo, CPAP_EPAP ChannelID RMS9_E01, RMS9_E02, RMS9_SetPressure, RMS9_MaskOnTime; -ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2; +ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag; ChannelID CPAP_LargeLeak, PRS1_BND, PRS1_FlexMode, PRS1_FlexLevel, PRS1_HumidStatus, PRS1_HumidLevel, PRS1_HumidTargetTime, PRS1_MaskResistLock, diff --git a/oscar/SleepLib/machine_common.h b/oscar/SleepLib/machine_common.h index 6e54a10e..9b683658 100644 --- a/oscar/SleepLib/machine_common.h +++ b/oscar/SleepLib/machine_common.h @@ -164,7 +164,7 @@ extern ChannelID CPAP_LargeLeak, PRS1_BND, CPAP_HumidSetting, PRS1_MaskResistSet, PRS1_HoseDiam, PRS1_AutoOn, PRS1_AutoOff, PRS1_MaskAlert, PRS1_ShowAHI; -extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2; +extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag; extern ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy; From 5323a56186bb5ffd5d9503a9add175317598f132 Mon Sep 17 00:00:00 2001 From: Phil Olynyk Date: Thu, 24 Jun 2021 12:13:21 -0400 Subject: [PATCH 03/11] Update to use SleepLib's edfParser --- anotDump.pro | 4 +++- anotDump/main.cpp | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/anotDump.pro b/anotDump.pro index 1d671af5..95affd6e 100644 --- a/anotDump.pro +++ b/anotDump.pro @@ -12,6 +12,8 @@ CONFIG -= debug_and_release QT += core widgets +DEFINES+=DUMPSTR + TARGET = anotDump TEMPLATE = app @@ -27,6 +29,6 @@ SOURCES += \ dumpSTR/edfparser.cpp \ HEADERS += \ - dumpSTR/common.h \ + dumpSTR/SleepLib/common.h \ dumpSTR/edfparser.h \ diff --git a/anotDump/main.cpp b/anotDump/main.cpp index de661796..89c563e0 100644 --- a/anotDump/main.cpp +++ b/anotDump/main.cpp @@ -81,9 +81,14 @@ int main(int argc, char *argv[]) { } EDFInfo edf; - QByteArray * buffer = edf.Open(filename); - if ( ! edf.Parse(buffer) ) - exit(-1); + if ( ! edf.Open(filename) ) { + qDebug() << "Failed to open" << filename; + exit(-1); + } + if ( ! edf.Parse() ) { + qDebug() << "Parsing failed on" << filename; + exit(-1); + } QDate d2 = edf.edfHdr.startdate_orig.date(); if (d2.year() < 2000) { From bd21b1cda5474dd5e6fe6fd7ef657b51dc42a2ce Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:23:57 -0700 Subject: [PATCH 04/11] Add exit(0) to dumpSTR and anotDump to fix compiler warning --- anotDump/main.cpp | 3 ++- dumpSTR/main.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/anotDump/main.cpp b/anotDump/main.cpp index 89c563e0..ca773cae 100644 --- a/anotDump/main.cpp +++ b/anotDump/main.cpp @@ -124,5 +124,6 @@ int main(int argc, char *argv[]) { qDebug() << "Offset: " << anno->offset << " Duration: " << anno->duration << " Text: " << anno->text; } } - + + exit(0); } diff --git a/dumpSTR/main.cpp b/dumpSTR/main.cpp index 65e3d95f..4f2a3467 100644 --- a/dumpSTR/main.cpp +++ b/dumpSTR/main.cpp @@ -193,4 +193,6 @@ int main(int argc, char *argv[]) { // delete &str; QThread::sleep(1); qDebug() << "Done"; + + exit(0); } From 492254b580e58d02de9e0f376d699dd6ea1f0ba2 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:27:42 -0700 Subject: [PATCH 05/11] Update version number to alpha.1 --- oscar/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oscar/VERSION b/oscar/VERSION index de526223..2452e959 100644 --- a/oscar/VERSION +++ b/oscar/VERSION @@ -1,4 +1,4 @@ // Update the string below to set OSCAR's version and release status. // See https://semver.org/spec/v2.0.0.html for details on format. -#define VERSION "1.2.1-alpha.0" +#define VERSION "1.2.1-alpha.1" From 2ab4e7bbe2c216119d6c35965a7ea738a19b2428 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:28:53 -0700 Subject: [PATCH 06/11] Calcs.cpp - calculation of TV and MV now uses a rolling average Only a few loaders are affected by this, notably SleepStyle but not ResMed A rolling average calculation makes calculated values resemble CPAP values more closely --- oscar/SleepLib/calcs.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/oscar/SleepLib/calcs.cpp b/oscar/SleepLib/calcs.cpp index d69a281a..a7f5b196 100644 --- a/oscar/SleepLib/calcs.cpp +++ b/oscar/SleepLib/calcs.cpp @@ -480,6 +480,7 @@ void FlowParser::calc(bool calcResp, bool calcTv, bool calcTi, bool calcTe, bool quint32 *tv_tptr = nullptr; EventStoreType *tv_dptr = nullptr; int tv_count = 0; + double tvlast, tvlast2, tvlast3; if (calcTv) { TV = m_session->AddEventList(CPAP_TidalVolume, EVL_Event); @@ -598,8 +599,15 @@ void FlowParser::calc(bool calcResp, bool calcTv, bool calcTi, bool calcTe, bool //double x=sqrt(q)*2; //val2=x; - if (tv < mintv) { mintv = tv; } + // Average TV over last three data points + if (tv_count == 0) + tvlast = tvlast2 = tvlast3 = tv; + tv = (tvlast + tvlast2 + tvlast3 + tv*2)/5; + tvlast3 = tvlast2; + tvlast2 = tvlast; + tvlast = tv; + if (tv < mintv) { mintv = tv; } if (tv > maxtv) { maxtv = tv; } *tv_tptr++ = timeval; @@ -889,8 +897,9 @@ void calcRespRate(Session *session, FlowParser *flowparser) bool calcTe = !session->eventlist.contains(CPAP_Te); bool calcMv = !session->eventlist.contains(CPAP_MinuteVent); - int z = (calcResp ? 1 : 0) + (calcTv ? 1 : 0) + (calcMv ? 1 : 0); +// Force calculation for testing calculation vs CPAP data +// z = 1; // If any of these three missing, remove all, and switch all on if (z > 0 && z < 3) { From f0c7cfc991cfffe275d24d8502a8802d053fd6fc Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:32:50 -0700 Subject: [PATCH 07/11] Add option to copypath() to overwrite existing files copypath() only copies files that do not exist in the destination directory. Added an optional parameter that forces copypath() to overwrite existing files. This is needed for SleepStyle and DV6 loaders. PRS loader should not be affected (it is the only other loader using copypath) --- oscar/SleepLib/common.cpp | 7 +++++-- oscar/SleepLib/common.h | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/oscar/SleepLib/common.cpp b/oscar/SleepLib/common.cpp index 55f460b4..3251b815 100644 --- a/oscar/SleepLib/common.cpp +++ b/oscar/SleepLib/common.cpp @@ -415,7 +415,7 @@ bool removeDir(const QString &path) return result; } -void copyPath(QString src, QString dst) +void copyPath(QString src, QString dst, bool overwrite) { QDir dir(src); if (!dir.exists()) @@ -425,7 +425,7 @@ void copyPath(QString src, QString dst) foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { QString dst_path = dst + QDir::separator() + d; dir.mkpath(dst_path); - copyPath(src + QDir::separator() + d, dst_path); + copyPath(src + QDir::separator() + d, dst_path, overwrite); } // Files @@ -433,6 +433,9 @@ void copyPath(QString src, QString dst) QString srcFile = src + QDir::separator() + f; QString destFile = dst + QDir::separator() + f; + if (overwrite && QFile::exists(destFile)) { + QFile::remove(destFile); + } if (!QFile::exists(destFile)) { if (!QFile::copy(srcFile, destFile)) { qWarning() << "copyPath: could not copy" << srcFile << "to" << destFile; diff --git a/oscar/SleepLib/common.h b/oscar/SleepLib/common.h index f5f47382..1ef4dee7 100644 --- a/oscar/SleepLib/common.h +++ b/oscar/SleepLib/common.h @@ -74,7 +74,7 @@ struct ValueCount { extern int idealThreads(); -void copyPath(QString src, QString dst); +void copyPath(QString src, QString dst, bool overwrite=false); // Primarily sort by value @@ -157,6 +157,7 @@ const QString STR_MACH_Journal = "Journal"; const QString STR_MACH_Intellipap = "Intellipap"; const QString STR_MACH_Weinmann= "Weinmann"; const QString STR_MACH_FPIcon = "FPIcon"; +const QString STR_MACH_SleepStyle = "SleepStyle"; const QString STR_MACH_MSeries = "MSeries"; const QString STR_MACH_CMS50 = "CMS50"; const QString STR_MACH_ZEO = "Zeo"; From c1a99850c6302d37dde86c68b4d57f5d8c9cf755 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:35:44 -0700 Subject: [PATCH 08/11] EDFParser - add option to handle EDF files with UTC timestamps Needed for SleepStyle CPAP as dates in the data files are UTC and not local --- oscar/SleepLib/loader_plugins/edfparser.cpp | 6 ++++++ oscar/SleepLib/loader_plugins/edfparser.h | 3 +++ 2 files changed, 9 insertions(+) diff --git a/oscar/SleepLib/loader_plugins/edfparser.cpp b/oscar/SleepLib/loader_plugins/edfparser.cpp index f9944568..e1c64926 100644 --- a/oscar/SleepLib/loader_plugins/edfparser.cpp +++ b/oscar/SleepLib/loader_plugins/edfparser.cpp @@ -58,6 +58,12 @@ EDFInfo::~EDFInfo() // delete a; } +// Set timezone to UTC +void EDFInfo::setTimeZoneUTC () { + TZ_offset = 0; + EDFInfo::localNoDST = QTimeZone(TZ_offset); +} + bool EDFInfo::Open(const QString & name) { if (hdrPtr != nullptr) { diff --git a/oscar/SleepLib/loader_plugins/edfparser.h b/oscar/SleepLib/loader_plugins/edfparser.h index 1fc5eaf3..c84c49fe 100644 --- a/oscar/SleepLib/loader_plugins/edfparser.h +++ b/oscar/SleepLib/loader_plugins/edfparser.h @@ -142,6 +142,9 @@ class EDFInfo static QDateTime getStartDT(const QString str); //! \brief Returns the start time using noLocalDST + static void setTimeZoneUTC(); //! \brief Sets noLocalDST to UTC (for EDF files using UTC time) + + // The data members follow static int TZ_offset; From f9a2228b9c954cee88174c2d524907bda45767a7 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:38:16 -0700 Subject: [PATCH 09/11] DV6 loader now overwrites files when creating backups --- oscar/SleepLib/loader_plugins/intellipap_loader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index 1bad34e0..4cc6d69e 100644 --- a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp +++ b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp @@ -2575,7 +2575,7 @@ bool backup6 (const QString & path) { QDir hpath(history_path); // Copy input data to backup location - copyPath(ipath.absolutePath(), bpath.absolutePath()); + copyPath(ipath.absolutePath(), bpath.absolutePath(), true); // Create archive of settings file if needed (SET.BIN) bool backup_settings = true; From 41ea0389f609b0c508970df0ac1d3606fc961018 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:43:02 -0700 Subject: [PATCH 10/11] Add SleepStyle loader This is the first test version of the F&P SleepStyle loader Additional refinements are yet to be made. Events and timestamps need to be confirmed, but overall the loader appears to be working. --- .../loader_plugins/sleepstyle_EDFinfo.cpp | 111 +++ .../loader_plugins/sleepstyle_EDFinfo.h | 64 ++ .../loader_plugins/sleepstyle_loader.cpp | 936 ++++++++++++++++++ .../loader_plugins/sleepstyle_loader.h | 129 +++ oscar/SleepLib/machine_common.cpp | 2 + oscar/SleepLib/machine_common.h | 2 + oscar/main.cpp | 2 + oscar/oscar.pro | 4 + 8 files changed, 1250 insertions(+) create mode 100644 oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp create mode 100644 oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.h create mode 100644 oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp create mode 100644 oscar/SleepLib/loader_plugins/sleepstyle_loader.h diff --git a/oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp b/oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp new file mode 100644 index 00000000..39721ed4 --- /dev/null +++ b/oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp @@ -0,0 +1,111 @@ +/* SleepLib SleepStyle Loader Implementation + * + * Copyright (c) 2020 The OSCAR Team + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SleepLib/session.h" +#include "SleepLib/calcs.h" + +#include "SleepLib/loader_plugins/sleepstyle_EDFinfo.h" + + +SleepStyleEDFInfo::SleepStyleEDFInfo() : EDFInfo() { + setTimeZoneUTC(); // Ask EDF Parser to assume data is in UTC, not in local time +} +SleepStyleEDFInfo::~SleepStyleEDFInfo() { } + +bool SleepStyleEDFInfo::Parse( ) // overrides and calls the super's Parse +{ + if ( ! EDFInfo::Parse( ) ) { + qWarning() << "sleepStyle EDFInfo::Parse failed!"; +// sleep(1); + return false; + } + + // Now massage some stuff into OSCAR's layout + // Extract the serial number from header string + QStringList parts = edfHdr.recordingident.split(' '); + serialnumber = parts[6]; + + if (!edfHdr.startdate_orig.isValid()) { + qDebug() << "sleepStyle EDFInfo::Parse Invalid date time retreieved parsing EDF File" << filename; +// sleep(1); + return false; + } + + startdate = qint64(edfHdr.startdate_orig.toTime_t()) * 1000L; + //startdate-=timezoneOffset(); + if (startdate == 0) { + qDebug() << "sleepStyle EDFInfo::Parse Invalid startdate = 0 in EDF File" << filename; +// sleep(1); + return false; + } + + dur_data_record = (edfHdr.duration_Seconds * 1000.0L); + + enddate = startdate + dur_data_record * qint64(edfHdr.num_data_records); + + return true; + +} + +extern QHash resmed_codes; + +// Looks up foreign language Signal names that match this channelID +EDFSignal *SleepStyleEDFInfo::lookupSignal(ChannelID ch) +{ + // Get list of all known foreign language names for this channel + auto channames = resmed_codes.find(ch); + if (channames == resmed_codes.end()) { + // no alternatives strings found for this channel + return nullptr; + } + + // This is bad, because ResMed thinks it was a cool idea to use two channels with the same name. + + // Scan through EDF's list of signals to see if any match + for (auto & name : channames.value()) { + EDFSignal *sig = lookupLabel(name); + if (sig) + return sig; + } + + // Failed + return nullptr; +} + +QDateTime SleepStyleEDFInfo::getStartDT( QString dateTimeStr ) +{ +// edfHdr.startdate_orig = QDateTime::fromString(QString::fromLatin1(hdrPtr->datetime, 16), "dd.MM.yyHH.mm.ss"); +// QString dateTimeStr; // , dateStr, timeStr; + QDate qDate; + QTime qTime; +// dateTimeStr = QString::fromLatin1(hdrPtr->datetime, 16); +// dateStr = dateTimeStr.left(8); +// timeStr = dateTimeStr.right(8); + qDate = QDate::fromString(dateTimeStr.left(8), "dd.MM.yy"); + qTime = QTime::fromString(dateTimeStr.right(8), "HH.mm.ss"); + return QDateTime(qDate, qTime, Qt::UTC); +} + + +void dumpEDFduration( ssEDFduration dur ) +{ + qDebug() << "Fullpath" << dur.path << "Filename" << dur.filename << "Start" << dur.start << "End" << dur.end; +} + diff --git a/oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.h b/oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.h new file mode 100644 index 00000000..20896313 --- /dev/null +++ b/oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.h @@ -0,0 +1,64 @@ +/* SleepLib SleepStyle EDFinfo Header + * + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef SLEEPSTYLE_EDFINFO_H +#define SLEEPSTYLE_EDFINFO_H + +#include +#include "SleepLib/machine.h" // Base class: MachineLoader +#include "SleepLib/machine_loader.h" +#include "SleepLib/profiles.h" +#include "SleepLib/loader_plugins/edfparser.h" + +//enum EDFType { EDF_UNKNOWN, EDF_BRP, EDF_PLD, EDF_SAD, EDF_EVE, EDF_CSL, EDF_AEV }; +enum EDFType { EDF_UNKNOWN, EDF_RT }; + +// EDFType lookupEDFType(const QString & filename); + +const QString SLEEPSTYLE_class_name = STR_MACH_ResMed; + +//class STRFile; // forward + +class SleepStyleEDFInfo : public EDFInfo +{ +public: + SleepStyleEDFInfo(); + ~SleepStyleEDFInfo(); + + virtual bool Parse() override; // overrides and calls the super's Parse + + virtual qint64 GetDurationMillis() { return dur_data_record; } // overrides the super + + EDFSignal *lookupSignal(ChannelID ch); + + QDateTime getStartDT( QString dateTimeStr ); + + //! \brief The following are computed from the edfHdr data + QString serialnumber; + qint64 dur_data_record; + qint64 startdate; + qint64 enddate; +}; + +class ssEDFduration +{ +public: + ssEDFduration() { start = end = 0; type = EDF_UNKNOWN; } + ssEDFduration(quint32 start, quint32 end, QString path) : + start(start), end(end), path(path) {} + + quint32 start; + quint32 end; + QString path; + QString filename; + EDFType type; +}; + +void dumpEDFduration( ssEDFduration dur ); + +#endif // SLEEPSTYLE_EDFINFO_H diff --git a/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp b/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp new file mode 100644 index 00000000..b4514c20 --- /dev/null +++ b/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp @@ -0,0 +1,936 @@ +/* SleepLib Fisher & Paykel SleepStyle Loader Implementation + * + * Copyright (c) 2020 The Oscar Team + * + * Derived from icon_loader.cpp + * Copyright (c) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include +#include +#include +#include +#include +#include + +#include "sleepstyle_loader.h" +#include "sleepstyle_EDFinfo.h" + +const QString FPHCARE = "FPHCARE"; + +SleepStyle::SleepStyle(Profile *profile, MachineID id) + : CPAP(profile, id) +{ +} + +SleepStyle::~SleepStyle() +{ +} + +SleepStyleLoader::SleepStyleLoader() +{ + m_buffer = nullptr; + m_type = MT_CPAP; +} + +SleepStyleLoader::~SleepStyleLoader() +{ +} + +/* + * getIconDir - returns the path to the ICON directory + */ +QString getIconDir (QString givenpath) { + + QString path = givenpath; + + path = path.replace("\\", "/"); + + if (path.endsWith("/")) { + path.chop(1); + } + + if (path.endsWith("/" + FPHCARE)) { + path = path.section("/",0,-2); + } + + QDir dir(path); + + if (!dir.exists()) { + return ""; + } + + // If this is a backup directory, higher level directories have been + // omitted. + if (path.endsWith("/Backup/", Qt::CaseInsensitive)) + return path; + + // F&P Icon have a folder called FPHCARE in the root directory + if (!dir.exists(FPHCARE)) { + return ""; + } + + // CHECKME: I can't access F&P ICON data right now + if (!dir.exists("FPHCARE/ICON")) { + return ""; + } + + return dir.filePath("FPHCARE/ICON"); +} + +/* + * getSleepStyleMachines returns a list of all SleepStyle machine folders in the ICON directory + */ +QStringList getSleepStyleMachines (QString iconPath) { + QStringList ssMachines; + + QDir iconDir (iconPath); + + // SleepStyle are mixed alpha and numeric; ICON serial numbers (directory names) are all digits + iconDir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + iconDir.setSorting(QDir::Name); + + QFileInfoList flist = iconDir.entryInfoList(); // List of Icon subdirectories + + bool isIconFilename; + + // Walk though directory list and save those that appear to be for SleepStyle machins. + for (int i = 0; i < flist.size(); i++) { + QFileInfo fi = flist.at(i); + QString filename = fi.fileName(); + filename.toInt(&isIconFilename); + if (isIconFilename) // Ignore this directory if named as used for older F&P Icon machine + continue; + if (filename.length() < 8) // F&P machine names are 8 characters long, but we allow more just in case... + continue; + + // directory is serial number and must not be all digits (which would make it an ICON directory) + // and it must have *.FPH files within it to be a SleepStyle folder + + QDir machineDir (iconPath + "/" + filename); + machineDir.setFilter(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + machineDir.setSorting(QDir::Name); + QStringList filters; + filters << "*.fph"; + machineDir.setNameFilters(filters); + QFileInfoList flist = machineDir.entryInfoList(); + if (flist.size() <= 0) { + continue; + } + ssMachines.push_back(filename); + } + + return ssMachines; +} + +bool SleepStyleLoader::Detect(const QString & givenpath) +{ + QString iconPath = getIconDir(givenpath); + if (iconPath.isEmpty()) + return false; + + QStringList machines = getSleepStyleMachines(iconPath); + if (machines.length() <= 0) + // Did not find any SleepStyle machine directories + return false; + + return true; +} + +bool SleepStyleLoader::backupData (Machine * mach, const QString & path) { + + QDir ipath(path); + QDir bpath(mach->getBackupPath()); + + // Compare QDirs rather than QStrings because separators may be different, especially on Windows. + + 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(); + } + + if (rebuild_from_backups || !create_backups) + return true; + + // Copy input data to backup location + copyPath(ipath.absolutePath(), bpath.absolutePath()); + + return true; +} + + +int SleepStyleLoader::Open(const QString & path) +{ + QString iconPath = getIconDir(path); + if (iconPath.isEmpty()) + return false; + + QStringList serialNumbers = getSleepStyleMachines(iconPath); + if (serialNumbers.length() <= 0) + // Did not find any SleepStyle machine directories + return false; + + Machine *m; + + int c = 0; + for (int i = 0; i < serialNumbers.size(); i++) { + MachineInfo info = newInfo(); + info.serial = serialNumbers[i]; + m = p_profile->CreateMachine(info); + + setSerialPath(iconPath + "/" + info.serial); + + try { + if (m) { + c+=OpenMachine(m, path, serialPath); + } + } catch (OneTypePerDay& e) { + Q_UNUSED(e) + p_profile->DelMachine(m); + MachList.erase(MachList.find(info.serial)); + QMessageBox::warning(nullptr, tr("Import Error"), + tr("This Machine Record cannot be imported in this profile.")+"\n\n"+tr("The Day records overlap with already existing content."), + QMessageBox::Ok); + delete m; + } + } + + return c; +} + +int SleepStyleLoader::OpenMachine(Machine *mach, const QString & path, const QString & ssPath) +{ + emit updateMessage(QObject::tr("Getting Ready...")); + emit setProgressValue(0); + QCoreApplication::processEvents(); + + QDir dir(ssPath); + + if (!dir.exists() || (!dir.isReadable())) { + return -1; + } + + backupData(mach, path); + + qDebug() << "Opening F&P SleepStyle" << ssPath; + + dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + dir.setSorting(QDir::Name); + QFileInfoList flist = dir.entryInfoList(); + + QString filename, fpath; + + emit updateMessage(QObject::tr("Reading data files...")); + QCoreApplication::processEvents(); + + QStringList summary, det, his; + Sessions.clear(); + + for (int i = 0; i < flist.size(); i++) { + QFileInfo fi = flist.at(i); + filename = fi.fileName(); + fpath = ssPath + "/" + filename; + + if (filename.left(3).toUpper() == "SUM") { + summary.push_back(fpath); + OpenSummary(mach, fpath); + } else if (filename.left(3).toUpper() == "DET") { + det.push_back(fpath); + } else if (filename.left(3).toUpper() == "HIS") { + his.push_back(fpath); + } + } + + for (int i = 0; i < det.size(); i++) { + OpenDetail(mach, det[i]); + } + +// Process REALTIME files + dir.cd("REALTIME"); + QFileInfoList rtlist = dir.entryInfoList(); + for (int i = 0; i < rtlist.size(); i++) { + QFileInfo fi = rtlist.at(i); + filename = fi.fileName(); + fpath = ssPath + "/REALTIME/" + filename; + if (filename.left(3).toUpper() == "HRD" + && filename.right(3).toUpper() == "EDF" ) { + OpenRealTime (mach, filename, fpath); + } + } + +// LOG files were not processed by icon_loader +// So we don't need to do anything + + SessionID sid;//,st; + float hours, mins; + + // For diagnostics, print summary of last 20 session or one week + qDebug() << "SS Loader - last 20 Sessions:"; + + int cnt = 0; + QDateTime dt; + QString a = ""; + + if (Sessions.size() > 0) { + + QMap::iterator it = Sessions.end(); + it--; + + dt = QDateTime::fromTime_t(qint64(it.value()->first()) / 1000L); + QDate date = dt.date().addDays(-7); + it++; + + do { + it--; + Session *sess = it.value(); + sid = sess->session(); + hours = sess->hours(); + mins = hours * 60; + dt = QDateTime::fromTime_t(sid); + + qDebug() << cnt << ":" << dt << "session" << sid << "," << mins << "minutes" << a; + + if (dt.date() < date) { + break; + } + + ++cnt; + + } while (it != Sessions.begin()); + + } + + // qDebug() << "Unmatched Sessions"; + // QList chunks; + // for (QMap::iterator dit=FLWDate.begin();dit!=FLWDate.end();dit++) { + // int k=dit.key(); + // //QDate date=dit.value(); + //// QList values = SessDate.values(date); + // for (int j=0;jchannelDataExists(CPAP_FlowRate)) c=true; + // } + // qDebug() << k << "-" <channelDataExists(CPAP_FlowRate)) c=true; + // } + // qDebug() << chunk.file << ":" << i << zz << dur << "minutes" << (b ? "*" : "") << (c ? QDateTime::fromTime_t(zz).toString() : ""); + // } + + int c = Sessions.size(); + qDebug() << "SS Loader found" << c << "sessions"; + + emit updateMessage(QObject::tr("Finishing up...")); + QCoreApplication::processEvents(); + + finishAddingSessions(); + + mach->Save(); + + + return c; +} + +// !\brief Convert F&P 32bit date format to 32bit UNIX Timestamp +quint32 ssconvertDate(quint32 timestamp) +{ + quint16 day, month,hour=0, minute=0, second=0; + quint16 year; + + + day = timestamp & 0x1f; + month = (timestamp >> 5) & 0x0f; + year = 2000 + ((timestamp >> 9) & 0x3f); + quint32 ts2 = timestamp >> 15; + second = ts2 & 0x3f; + minute = (ts2 >> 6) & 0x3f; + hour = (ts2 >> 12); + + QDateTime dt = QDateTime(QDate(year, month, day), QTime(hour, minute, second), Qt::UTC); + + qDebug().noquote() << "SS timestamp" << timestamp << year << month << day << dt << hour << minute << second; + +// Q NO!!! _ASSERT(dt.isValid()); +// if ((year == 2013) && (month == 9) && (day == 18)) { +// // this is for testing.. set a breakpoint on here and +// int i=5; +// } + + + // From Rudd's data set compared to times reported from his F&P software's report (just the time bits left over) + // 90514 = 00:06:18 WET 23:06:18 UTC 09:06:18 AEST + // 94360 = 01:02:24 WET + // 91596 = 00:23:12 WET + // 19790 = 23:23:50 WET + + return dt.addSecs(-54).toTime_t(); // Huh? Why do this? +} + +// SessionID is in seconds, not msec +SessionID SleepStyleLoader::findSession (SessionID sid) { + for(auto sessKey : Sessions.keys()) + { + Session * sess = Sessions.value(sessKey); + if (sid >= (sess->realFirst() / 1000L) && sid <= (sess->realLast() / 1000L)) + return sessKey; + } + + return 0; +} + +bool SleepStyleLoader::OpenRealTime(Machine *mach, const QString & fname, const QString & filepath) +{ +// Q_UNUSED(filepath) + Q_UNUSED(mach) + Q_UNUSED(fname) + + SleepStyleEDFInfo edf; + + // Open the EDF file and read contents into edf object + if (!edf.Open(filepath)) { + qWarning() << "SS Realtime failed to open" << filepath; + return false; + } + + if (!edf.Parse()) { + qWarning() << "SS Realtime Parse failed to open" << filepath; + return false; + } + + qDebug().noquote() << "SS ORT timestamp" << edf.startdate / 1000L << QDateTime::fromSecsSinceEpoch(edf.startdate / 1000L).toString("MM/dd/yyyy hh:mm:ss"); + SessionID sessKey = findSession(edf.startdate / 1000L); + if (sessKey == 0) { + qWarning() << "SS ORT session not found"; + return true; + } + + Session * sess = Sessions.value(sessKey); + + if (sess == nullptr) { + qWarning() << "SS ORT session not found - nullptr"; + return true; + } + + sess->updateFirst(edf.startdate); + + qint64 duration = edf.GetNumDataRecords() * edf.GetDurationMillis(); + sess->updateLast(edf.startdate + duration); + +// Find the leak signal and data + long leakrecs = 0; + EDFSignal leakSignal; + for (auto & esleak : edf.edfsignals) { + leakrecs = esleak.sampleCnt * edf.GetNumDataRecords(); + if (leakrecs < 0) + continue; + if (esleak.label == "Leak") { + leakSignal = esleak; + } + } + +// Walk through all signals, ignoring leaks + for (auto & es : edf.edfsignals) { + long recs = es.sampleCnt * edf.GetNumDataRecords(); + if (recs < 0) + continue; + ChannelID code = 0; + + if (es.label == "Flow") { + // Flow data appears to include total leaks, which are also reported in the edf file. + // We subtract the leak from the flow data to get flow data that is centered around zero. + // This is needed for other derived graphs (tidal volume, insp and exp times, etc.) to be reasonable + code = CPAP_FlowRate; + bool done = false; + if (leakrecs > 0) { + for (int ileak = 0; ileak < leakrecs && !done; ileak++) { + for (int iflow = 0; iflow < 25 && !done; iflow++) { + if (ileak*25 + iflow >= recs) { + done = true; + break; + } + es.dataArray[ileak*25 + iflow] -= leakSignal.dataArray[ileak] - 500; + } + } + } + + } else if (es.label == "Pressure") { + code = CPAP_MaskPressure; + + } else + continue; + + if (code) { + double rate = double(duration) / double(recs); + EventList *a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate); + a->setDimension(es.physical_dimension); + a->AddWaveform(edf.startdate, es.dataArray, recs, duration); + + EventDataType min = a->Min(); + EventDataType max = a->Max(); + + // Cap to physical dimensions, because there can be ram glitches/whatever that throw really big outliers. + if (min < es.physical_minimum) + min = es.physical_minimum; + if (max > es.physical_maximum) + max = es.physical_maximum; + + sess->updateMin(code, min); + sess->updateMax(code, max); + sess->setPhysMin(code, es.physical_minimum); + sess->setPhysMax(code, es.physical_maximum); + } + + } + + return true; + +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Open Summary file, create list of sessions and session summary data +//////////////////////////////////////////////////////////////////////////////////////////// +bool SleepStyleLoader::OpenSummary(Machine *mach, const QString & filename) +{ + qDebug() << filename; + QByteArray header; + QFile file(filename); + + if (!file.open(QFile::ReadOnly)) { + qDebug() << "SS SUM Couldn't open" << filename; + return false; + } + + // Read header of summary file + header = file.read(0x200); + + if (header.size() != 0x200) { + qDebug() << "SS SUM Short file" << filename; + file.close(); + return false; + } + + // Header is terminated by ';' at 0x1ff + unsigned char hterm = 0x3b; + + if (hterm != header[0x1ff]) { + qWarning() << "SS SUM Header missing ';' terminator" << filename; + } + + QTextStream htxt(&header); + QString h1, version, fname, serial, model, type, unknownident; + htxt >> h1; + htxt >> version; + htxt >> fname; + htxt >> serial; + htxt >> model; //TODO: Should become Series in machine info??? + htxt >> type; // SPSAAN etc with 4th character being A (Auto) or C (CPAP) + htxt >> unknownident; // Constant, but has different value when version number is different. + + qDebug() << "SS SUM header" << h1 << version << fname << serial << model << type << unknownident; + + if (type.length() > 4) + type = (type.at(3) == 'C' ? "CPAP" : "Auto"); + mach->setModel(model + " " + type); + + // Read remainder of summary file + QByteArray data; + data = file.readAll(); + file.close(); + + QDataStream in(data); + in.setVersion(QDataStream::Qt_4_8); + in.setByteOrder(QDataStream::LittleEndian); + + quint32 ts; + //QByteArray line; + unsigned char p1, p2, p3, j1, x1, x2; + + unsigned char runTime, useTime, minPressSet, maxPressSet, minPressSeen, pct95PressSeen, maxPressSeen; + unsigned char senseAwakeLevel, humidityLevel, smartFlexLevel; + + quint16 c1, c2, c3, c4; +// quint16 d1, d2, d3; + unsigned char d1, d2, d3, d4, d5, d6; + + int usage; //,runtime; + + QDate date; + + int nblock = 0; + + // Go through blocks of data until end marker is found + do { + nblock++; + + in >> ts; + if (ts == 0xffffffff) { + qDebug() << "SS SUM 0xffffffff terminator found at block" << nblock; + break; + } + if ((ts & 0xffff) == 0xfafe) { + qDebug() << "SS SUM 0xfafa terminator found at block" << nblock; + break; + } + + ts = ssconvertDate(ts); + + qDebug() << "\nSS SUM Session" << nblock << "with timestamp" << ts << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss"); + + // the following two quite often match in value + in >> runTime; // 0x04 + in >> useTime; // 0x05 + usage = useTime * 360; // Convert to seconds (durations are in .1 hour intervals) + + in >> minPressSeen; // 0x06 + in >> pct95PressSeen; // 0x07 + in >> maxPressSeen; // 0x08 + + in >> d1; // 0x09 + in >> d2; // 0x0a + in >> d3; // 0x0b + in >> d4; // 0x0c + in >> d5; // 0x0d + in >> d6; // 0x0e + + in >> c1; // 0x0f + in >> c2; // 0x11 + in >> c3; // 0x13 + in >> c4; // 0x15 + + in >> j1; // 0x17 + + in >> p1; // 0x18 + in >> p2; // 0x19 + in >> p3; // 0x1a + + in >> x1; // 0x1b + in >> x2; // 0x1c + + in >> minPressSet; + in >> maxPressSet; + in >> senseAwakeLevel; + in >> humidityLevel; + in >> smartFlexLevel; + + // soak up unknown stuff to apparent end of data for the day + unsigned char s [6]; + for (unsigned int i=0; i < sizeof(s); i++) + in >> s[i]; + + qDebug() << "SS SUM block" << nblock + << "a:" <<"Pressure Min"<SessionExists(ts)) { + Session *sess = new Session(mach, ts); + sess->really_set_first(qint64(ts) * 1000L); + sess->really_set_last(qint64(ts + usage) * 1000L); + sess->SetChanged(true); +/**** + // TODO: None of the apnea numbers have been confirmed + sess->setCount(CPAP_Obstructive, c3); +// sess->setCph(CPAP_Obstructive, c3 / (float(usage)/3600.00)); + + sess->setCount(CPAP_Hypopnea, c4); +// sess->setCph(CPAP_Hypopnea, c4 / (float(usage)/3600.00)); + + sess->setCount(CPAP_ClearAirway, c1); +// sess->setCph(CPAP_ClearAirway, c1 / (float(usage)/3600.00)); + + sess->setCount(CPAP_FlowLimit, c2); +// sess->setCph(CPAP_FlowLimit, c2 / (float(usage)/3600.00)); +****/ + SessDate.insert(date, sess); + + if (minPressSet != maxPressSet) { + sess->settings[CPAP_Mode] = (int)MODE_APAP; + sess->settings[CPAP_PressureMin] = minPressSet / 10.0; + sess->settings[CPAP_PressureMax] = maxPressSet / 10.0; + } else { + sess->settings[CPAP_Mode] = (int)MODE_CPAP; + sess->settings[CPAP_Pressure] = minPressSet / 10.0; + } + + sess->settings[CPAP_HumidSetting] = humidityLevel; + sess->settings[SS_SenseAwakeLevel] = senseAwakeLevel / 10.0; + sess->settings[CPAP_PresReliefMode] = PR_SMARTFLEX; + sess->settings[SS_SmartFlexLevel] = smartFlexLevel / 1.0; + + Sessions[ts] = sess; + + addSession(sess); + } + } while (!in.atEnd()); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Open Detail record contains list of sessions and pressure, leak, and event flags +//////////////////////////////////////////////////////////////////////////////////////////// +bool SleepStyleLoader::OpenDetail(Machine *mach, const QString & filename) +{ + Q_UNUSED(mach); + + qDebug() << "SS DET Opening Detail" << filename; + QByteArray header; + QFile file(filename); + + if (!file.open(QFile::ReadOnly)) { + qDebug() << "SS DET Couldn't open" << filename; + return false; + } + + header = file.read(0x200); + + if (header.size() != 0x200) { + qDebug() << "SS DET short file" << filename; + file.close(); + return false; + } + + // Header is terminated by ';' at 0x1ff + unsigned char hterm = 0x3b; + + if (hterm != header[0x1ff]) { + file.close(); + qWarning() << "SS DET Header missing ';' terminator" << filename; + return false; + } + + QTextStream htxt(&header); + QString h1, version, fname, serial, model, type, unknownident; + htxt >> h1; + htxt >> version; + htxt >> fname; + htxt >> serial; + htxt >> model; //TODO: Should become Series in machine info??? + htxt >> type; // SPSAAN etc with 4th character being A (Auto) or C (CPAP) + htxt >> unknownident; // Constant, but has different value when version number is different. + + qDebug() << "SS DET file header" << h1 << version << fname << serial << model << type << unknownident; + + // Read session indices + QByteArray index = file.read(0x800); + if (index.size()!=0x800) { + // faulty file.. + qWarning() << "SS DET file short index block"; + file.close(); + return false; + } + QDataStream in(index); + quint32 ts; + + in.setVersion(QDataStream::Qt_4_6); + in.setByteOrder(QDataStream::LittleEndian); + + QVector times; + QVector start; + QVector records; + + quint16 strt; + quint8 recs; + quint16 unknownIndex; + + int totalrecs = 0; + + do { + // Read timestamp for session and check for end of data signal + in >> ts; + if (ts == 0xffffffff) break; + if ((ts & 0xffff) == 0xfafe) break; + + ts = ssconvertDate(ts); + + in >> strt; + in >> recs; + in >> unknownIndex; + totalrecs += recs; // Number of data records for this session + + qDebug().noquote() << "SS DET block timestamp" << ts << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss") << "start" << strt << "records" << recs << "unknown" << unknownIndex; + + if (Sessions.contains(ts)) { + times.push_back(ts); + start.push_back(strt); + records.push_back(recs); + } + else + qDebug() << "SS DET session not found" << ts; + } while (!in.atEnd()); + + QByteArray databytes = file.readAll(); + file.close(); + + in.setVersion(QDataStream::Qt_4_6); + in.setByteOrder(QDataStream::BigEndian); + + // 7 (was 5) byte repeating patterns + + quint8 *data = (quint8 *)databytes.data(); + + qint64 ti; + quint8 pressure, leak, a1, a2, a3, a4, a5, a6, a7; +// quint8 sa1, sa2; // The two sense awake bits per 2 minutes + SessionID sessid; + Session *sess; + int idx; + + for (int r = 0; r < start.size(); r++) { + sessid = times[r]; + sess = Sessions[sessid]; + ti = qint64(sessid) * 1000L; + sess->really_set_first(ti); + + EventList *LK = sess->AddEventList(CPAP_LeakTotal, EVL_Event, 1); + EventList *PR = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1F); + EventList *OA = sess->AddEventList(CPAP_Obstructive, EVL_Event); + EventList *H = sess->AddEventList(CPAP_Hypopnea, EVL_Event); + EventList *FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event); + EventList *SA = sess->AddEventList(CPAP_SensAwake, EVL_Event); + EventList *CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event); + EventList *UA = sess->AddEventList(CPAP_Apnea, EVL_Event); +// For testing to determine which bit is for which event type: +// EventList *UF1 = sess->AddEventList(CPAP_UserFlag1, EVL_Event); +// EventList *UF2 = sess->AddEventList(CPAP_UserFlag2, EVL_Event); + + unsigned stidx = start[r]; + int rec = records[r]; + + idx = stidx * 21; // Each record has three blocks of 7 bytes for 21 bytes total + + quint8 bitmask; + for (int i = 0; i < rec; ++i) { + for (int j = 0; j < 3; ++j) { + pressure = data[idx]; + PR->AddEvent(ti/*+120000*/, pressure); + + leak = data[idx + 1]; + LK->AddEvent(ti/*+120000*/, leak); + + // Comments below from MW. Appear not to be accurate + a1 = data[idx + 2]; // [0..5] Obstructive flag, [6..7] Unknown + a2 = data[idx + 3]; // [0..5] Hypopnea, [6..7] Unknown + a3 = data[idx + 4]; // [0..5] Flow Limitation, [6..7] Unknown + a4 = data[idx + 5]; // [0..5] UF1, [6..7] Unknown + a5 = data[idx + 6]; // [0..5] UF2, [6..7] Unknown + + // Sure there isn't 6 SenseAwake bits? + a6 = (a1 >> 6) << 4 | ((a2 >> 6) << 2) | (a3 >> 6); + + // this does the same thing as behaviour +// a6 = (a3 >> 7) << 3 | ((a3 >> 6) & 1); + a7 = (a4 >> 6) | (a5 >> 6); // Are these bits used? + + bitmask = 1; + for (int k = 0; k < 6; k++) { // There are 6 flag sets per 2 minutes + if (a1 & bitmask) { UA->AddEvent(ti+60000, 0); } + if (a2 & bitmask) { CA->AddEvent(ti+60000, 0); } + if (a3 & bitmask) { H->AddEvent(ti+60000, 0); } + if (a4 & bitmask) { OA->AddEvent(ti+60000, 0); } + if (a5 & bitmask) { FL->AddEvent(ti+60000, 0); } + if (a6 & bitmask) { SA->AddEvent(ti+60000, 0); } + + bitmask <<= 1; + ti += 20000L; // Increment 20 seconds + } + + // Debug print non-zero flags + if (a1 != 0 || a2 != 0 || a3 != 0 || a4 != 0 || a5 != 0 || a6 != 0 || a7 != 0) { + qDebug() << "SS DET events" << QDateTime::fromSecsSinceEpoch(ti/1000).toString("MM/dd/yyyy hh:mm:ss") + << "pressure" << pressure + << "leak" << leak + << "flags" << a1 << a2 << a3 << a4 << a5 << a6 << "unknown" << a7; + } + + idx += 7; //was 5; + } + } + + // Update indexes, process waveform and perform flagging + sess->UpdateSummaries(); + + // sess->really_set_last(ti-360000L); + // sess->SetChanged(true); + // addSession(sess,profile); + } + + return 1; +} + +void SleepStyleLoader::initChannels() +{ + using namespace schema; + Channel * chan = nullptr; + +/**** + channel.add(GRP_CPAP, chan = new Channel(INTP_SmartFlexMode = 0x1165, SETTING, MT_CPAP, SESSION, + "INTPSmartFlexMode", QObject::tr("SmartFlex Mode"), + QObject::tr("Pressure relief mode."), + QObject::tr("SmartFlex Mode"), + "", DEFAULT, Qt::green)); + + chan->addOption(0, STR_TR_Off); +****/ + + channel.add(GRP_CPAP, chan = new Channel(SS_SmartFlexLevel = 0xf304, SETTING, MT_CPAP, SESSION, + "SSSmartFlexLevel", QObject::tr("SmartFlex Level"), + QObject::tr("Exhalation pressure relief level."), + QObject::tr("SmartFlex"), + "", LOOKUP, Qt::green)); + chan->addOption(0, STR_TR_Off); + + channel.add(GRP_CPAP, new Channel(SS_SenseAwakeLevel = 0xf305, SETTING, MT_CPAP, SESSION, + "SS_SenseAwakeLevel", + QObject::tr("SenseAwake level"), + QObject::tr("SenseAwake level"), + QObject::tr("SenseAwake"), + STR_UNIT_CMH2O, LOOKUP, Qt::black)); + +} + +bool sleepstyle_initialized = false; +void SleepStyleLoader::Register() +{ + if (sleepstyle_initialized) { return; } + + qDebug() << "Registering F&P Sleepstyle Loader"; + RegisterLoader(new SleepStyleLoader()); + //InitModelMap(); + sleepstyle_initialized = true; +} diff --git a/oscar/SleepLib/loader_plugins/sleepstyle_loader.h b/oscar/SleepLib/loader_plugins/sleepstyle_loader.h new file mode 100644 index 00000000..f29e0486 --- /dev/null +++ b/oscar/SleepLib/loader_plugins/sleepstyle_loader.h @@ -0,0 +1,129 @@ +/* SleepLib Fisher & Paykel SleepStyle Loader Implementation + * + * Copyright (c) 2020 The Oscar Team (info@oscar-team.org) + * Copyright (C) 2011-2018 Mark Watkins + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef SLEEPSTYLE_LOADER_H +#define SLEEPSTYLE_LOADER_H + +#include +#include "SleepLib/machine.h" +#include "SleepLib/machine_loader.h" +#include "SleepLib/profiles.h" + + +//******************************************************************************************** +/// IMPORTANT!!! +//******************************************************************************************** +// Please INCREMENT the following value when making changes to this loaders implementation. +// +const int sleepstyle_data_version = 1; +// +//******************************************************************************************** + +/*! \class SleepStyle + \brief F&P SleepStyle customized machine object + */ +class SleepStyle: public CPAP +{ + public: + SleepStyle(Profile *, MachineID id = 0); + virtual ~SleepStyle(); +}; + + +const int sleepstyle_load_buffer_size = 1024 * 1024; + +extern ChannelID INTP_SmartFlexMode; +extern ChannelID SS_SmartFlexLevel; +extern ChannelID SS_SenseAwakeLevel; + +const QString sleepstyle_class_name = STR_MACH_SleepStyle; + +/*! \class SleepStyleLoader + \brief Loader for Fisher & Paykel SleepStyle data + This is only relatively recent addition and still needs more work + */ + +class SleepStyleLoader : public CPAPLoader +{ + Q_OBJECT + public: + SleepStyleLoader(); + virtual ~SleepStyleLoader(); + + //! \brief Detect if the given path contains a valid Folder structure + virtual bool Detect(const QString & path); + + //! \brief Scans path for F&P SleepStyle data signature, and Loads any new data + virtual int Open(const QString & path); + + int OpenMachine(Machine *mach, const QString & path, const QString & ssPath); + + bool OpenSummary(Machine *mach, const QString & path); + bool OpenDetail(Machine *mach, const QString & path); +// bool OpenFLW(Machine *mach, const QString & filename); + bool OpenRealTime(Machine *mach, const QString & fname, const QString & filename); + + //! \brief Returns SleepLib database version of this F&P SleepStyle loader + virtual int Version() { return sleepstyle_data_version; } + + //! \brief Returns the machine class name of this CPAP machine, "SleepStyle" + virtual const QString & loaderName() { return sleepstyle_class_name; } + + // ! \brief Creates a machine object, indexed by serial number + //Machine *CreateMachine(QString serial); + + QString getSerialPath () {return serialPath;} + void setSerialPath (QString sp) {serialPath = sp;} + bool backupData (Machine * mach, const QString & path); + + SessionID findSession (SessionID sid); + + void initChannels(); + + virtual MachineInfo newInfo() { + return MachineInfo(MT_CPAP, 0, sleepstyle_class_name, QObject::tr("Fisher & Paykel"), QString(), QString(), QString(), QObject::tr("SleepStyle"), QDateTime::currentDateTime(), sleepstyle_data_version); + } + + + //! \brief Registers this MachineLoader with the master list, so F&P Icon data can load + static void Register(); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Now for some CPAPLoader overrides + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + virtual QString presRelType() { return QObject::tr(""); } // might not need this one + + virtual ChannelID presRelSet() { return NoChannel; } + virtual ChannelID presRelLevel() { return NoChannel; } + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + protected: +// QDateTime readFPDateTime(quint8 *data); + + QString last; + QHash MachList; + QMap Sessions; + QMultiMap SessDate; + //QMap > FLWMapFlow; + //QMap > FLWMapLeak; + //QMap > FLWMapPres; + //QMap > FLWDuration; + //QMap > FLWTS; + //QMap FLWDate; + + QString serialPath; // fully qualified path to the input data, ...SDCard.../FPHCARE/ICON/serial +// QString serial; // Serial number + bool rebuild_from_backups = false; + bool create_backups = true; + + unsigned char *m_buffer; +}; + +#endif // SLEEPSTYLE_LOADER_H diff --git a/oscar/SleepLib/machine_common.cpp b/oscar/SleepLib/machine_common.cpp index 7828b509..7e2a882e 100644 --- a/oscar/SleepLib/machine_common.cpp +++ b/oscar/SleepLib/machine_common.cpp @@ -34,6 +34,8 @@ ChannelID CPAP_LargeLeak, PRS1_BND, PRS1_FlexMode, PRS1_FlexLevel, PRS1_HumidStatus, PRS1_HumidLevel, PRS1_HumidTargetTime, PRS1_MaskResistLock, PRS1_MaskResistSet, PRS1_HoseDiam, PRS1_AutoOn, PRS1_AutoOff, PRS1_MaskAlert, PRS1_ShowAHI; +ChannelID SS_SenseAwakeLevel, SS_SmartFlexLevel; + ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy; ChannelID Journal_Notes, Journal_Weight, Journal_BMI, Journal_ZombieMeter, LastUpdated, diff --git a/oscar/SleepLib/machine_common.h b/oscar/SleepLib/machine_common.h index 9b683658..8acbf76d 100644 --- a/oscar/SleepLib/machine_common.h +++ b/oscar/SleepLib/machine_common.h @@ -166,6 +166,8 @@ extern ChannelID CPAP_LargeLeak, PRS1_BND, extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag; +extern ChannelID SS_SenseAwakeLevel, SS_SmartFlexLevel; + extern ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy; extern ChannelID Journal_Notes, Journal_Weight, Journal_BMI, Journal_ZombieMeter, Bookmark_Start, diff --git a/oscar/main.cpp b/oscar/main.cpp index 29739096..3967cf5b 100644 --- a/oscar/main.cpp +++ b/oscar/main.cpp @@ -43,6 +43,7 @@ #include "SleepLib/loader_plugins/resmed_loader.h" #include "SleepLib/loader_plugins/intellipap_loader.h" #include "SleepLib/loader_plugins/icon_loader.h" +#include "SleepLib/loader_plugins/sleepstyle_loader.h" #include "SleepLib/loader_plugins/weinmann_loader.h" #include "SleepLib/loader_plugins/viatom_loader.h" @@ -666,6 +667,7 @@ int main(int argc, char *argv[]) { ResmedLoader::Register(); IntellipapLoader::Register(); FPIconLoader::Register(); + SleepStyleLoader::Register(); WeinmannLoader::Register(); CMS50Loader::Register(); CMS50F37Loader::Register(); diff --git a/oscar/oscar.pro b/oscar/oscar.pro index 313dfce6..853ef392 100644 --- a/oscar/oscar.pro +++ b/oscar/oscar.pro @@ -295,6 +295,8 @@ SOURCES += \ SleepLib/loader_plugins/cms50_loader.cpp \ SleepLib/loader_plugins/dreem_loader.cpp \ SleepLib/loader_plugins/icon_loader.cpp \ + SleepLib/loader_plugins/sleepstyle_loader.cpp \ + SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp \ SleepLib/loader_plugins/intellipap_loader.cpp \ SleepLib/loader_plugins/mseries_loader.cpp \ SleepLib/loader_plugins/prs1_loader.cpp \ @@ -380,6 +382,8 @@ HEADERS += \ SleepLib/loader_plugins/cms50_loader.h \ SleepLib/loader_plugins/dreem_loader.h \ SleepLib/loader_plugins/icon_loader.h \ + SleepLib/loader_plugins/sleepstyle_loader.h \ + SleepLib/loader_plugins/sleepstyle_EDFinfo.h \ SleepLib/loader_plugins/intellipap_loader.h \ SleepLib/loader_plugins/mseries_loader.h \ SleepLib/loader_plugins/prs1_loader.h \ From bf62344e5d47c67d1e0a9b268c380c048194e841 Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 22:20:59 -0700 Subject: [PATCH 11/11] SleepStyle loader now reports only H and UA (no CA or OA) While the event flags in SleepStyle data show four different types of apneas, Fisher & Paykel software reports only two types: Hypopneas and Apnea. OCAR how combines those four types to report the same way as F&P does. We don't know why F&P consolidates these different event types -- perhaps they are "dumbing-down" detail to make it easier for users, or perhaps they know that the identifcation of CA, OA, UA, and H is not reliable. We can easily restore more details to the event identification. --- oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp b/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp index b4514c20..b50ab72a 100644 --- a/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp +++ b/oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp @@ -819,11 +819,11 @@ bool SleepStyleLoader::OpenDetail(Machine *mach, const QString & filename) EventList *LK = sess->AddEventList(CPAP_LeakTotal, EVL_Event, 1); EventList *PR = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1F); - EventList *OA = sess->AddEventList(CPAP_Obstructive, EVL_Event); +// EventList *OA = sess->AddEventList(CPAP_Obstructive, EVL_Event); EventList *H = sess->AddEventList(CPAP_Hypopnea, EVL_Event); EventList *FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event); EventList *SA = sess->AddEventList(CPAP_SensAwake, EVL_Event); - EventList *CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event); +// EventList *CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event); EventList *UA = sess->AddEventList(CPAP_Apnea, EVL_Event); // For testing to determine which bit is for which event type: // EventList *UF1 = sess->AddEventList(CPAP_UserFlag1, EVL_Event); @@ -851,18 +851,18 @@ bool SleepStyleLoader::OpenDetail(Machine *mach, const QString & filename) a5 = data[idx + 6]; // [0..5] UF2, [6..7] Unknown // Sure there isn't 6 SenseAwake bits? - a6 = (a1 >> 6) << 4 | ((a2 >> 6) << 2) | (a3 >> 6); + a6 = (a3 >> 6) << 4 | ((a4 >> 6) << 2) | (a5 >> 6); // this does the same thing as behaviour // a6 = (a3 >> 7) << 3 | ((a3 >> 6) & 1); - a7 = (a4 >> 6) | (a5 >> 6); // Are these bits used? + a7 = (a1 >> 6) | (a2 >> 6); // Are these bits used? bitmask = 1; for (int k = 0; k < 6; k++) { // There are 6 flag sets per 2 minutes if (a1 & bitmask) { UA->AddEvent(ti+60000, 0); } - if (a2 & bitmask) { CA->AddEvent(ti+60000, 0); } + if (a2 & bitmask) { UA->AddEvent(ti+60000, 0); } // may be CA? if (a3 & bitmask) { H->AddEvent(ti+60000, 0); } - if (a4 & bitmask) { OA->AddEvent(ti+60000, 0); } + if (a4 & bitmask) { H->AddEvent(ti+60000, 0); } // may be OA? if (a5 & bitmask) { FL->AddEvent(ti+60000, 0); } if (a6 & bitmask) { SA->AddEvent(ti+60000, 0); }