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..ca773cae 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) { @@ -119,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); } 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) { 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"; 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; diff --git a/oscar/SleepLib/loader_plugins/intellipap_loader.cpp b/oscar/SleepLib/loader_plugins/intellipap_loader.cpp index 2263c974..4cc6d69e 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,24 +2572,10 @@ 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; - } + copyPath(ipath.absolutePath(), bpath.absolutePath(), true); // 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/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..b50ab72a --- /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 = (a3 >> 6) << 4 | ((a4 >> 6) << 2) | (a5 >> 6); + + // this does the same thing as behaviour +// a6 = (a3 >> 7) << 3 | ((a3 >> 6) & 1); + 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) { UA->AddEvent(ti+60000, 0); } // may be CA? + if (a3 & bitmask) { H->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); } + + 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 c6008bc0..7e2a882e 100644 --- a/oscar/SleepLib/machine_common.cpp +++ b/oscar/SleepLib/machine_common.cpp @@ -28,12 +28,14 @@ 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, 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 6e54a10e..8acbf76d 100644 --- a/oscar/SleepLib/machine_common.h +++ b/oscar/SleepLib/machine_common.h @@ -164,7 +164,9 @@ 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 SS_SenseAwakeLevel, SS_SmartFlexLevel; extern ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy; 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" 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/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 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 \