From 024c0daf49c22b1f432dc5669902e94cd9b1ec6f Mon Sep 17 00:00:00 2001 From: Phil Olynyk Date: Sun, 6 Jun 2021 22:57:52 -0400 Subject: [PATCH 01/10] Parse identification.json for AS11 --- .../SleepLib/loader_plugins/resmed_loader.cpp | 141 ++++++++++++++---- 1 file changed, 111 insertions(+), 30 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.cpp b/oscar/SleepLib/loader_plugins/resmed_loader.cpp index c66e8f11..f67bcbf5 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.cpp +++ b/oscar/SleepLib/loader_plugins/resmed_loader.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include "SleepLib/session.h" @@ -194,6 +196,7 @@ ChannelID ResmedLoader::PresReliefLevel() { return RMS9_EPRLevel; } QHash resmed_codes; const QString STR_ext_TGT = "tgt"; +const QString STR_ext_JSON = "json"; const QString STR_ext_CRC = "crc"; const QString RMS9_STR_datalog = "DATALOG"; @@ -222,6 +225,7 @@ bool ResmedLoader::Detect(const QString & givenpath) } QHash parseIdentLine( const QString line, MachineInfo * info); // forward +void scanProductObject( const QJsonObject product, MachineInfo *info, QHash *idmap ); // forward MachineInfo ResmedLoader::PeekInfo(const QString & path) { if (!Detect(path)) @@ -230,23 +234,50 @@ MachineInfo ResmedLoader::PeekInfo(const QString & path) QFile f(path+"/"+RMS9_STR_idfile+"tgt"); // Abort if this file is dodgy.. - if (!f.exists() || !f.open(QIODevice::ReadOnly)) { - return MachineInfo(); - } - MachineInfo info = newInfo(); + if (f.exists() ) { + if ( !f.open(QIODevice::ReadOnly)) { + return MachineInfo(); + } + MachineInfo info = newInfo(); - // Parse # entries into idmap. - while (!f.atEnd()) { - QString line = f.readLine().trimmed(); - QHash hash = parseIdentLine( line, & info ); - } + // Parse # entries into idmap. + while (!f.atEnd()) { + QString line = f.readLine().trimmed(); + QHash hash = parseIdentLine( line, & info ); + } - return info; + return info; + } + QFile j(path+"/"+RMS9_STR_idfile+"json"); + if (j.exists() ) { + if ( !j.open(QIODevice::ReadOnly)) { + return MachineInfo(); + } + QByteArray identData = j.readAll(); + j.close(); + QJsonDocument identDoc(QJsonDocument::fromJson(identData)); + QJsonObject identObj = identDoc.object(); + if ( identObj.contains("FlowGenerator") && identObj["FlowGenerator"].isObject()) { + QJsonObject flow = identObj["FlowGenerator"].toObject(); + if ( flow.contains("IdentificationProfiles") && flow["IdentificationProfiles"].isObject()) { + QJsonObject profiles = flow["IdentificationProfiles"].toObject(); + if ( profiles.contains("Product") && profiles["Product"].isObject()) { + QJsonObject product = profiles["Product"].toObject(); + MachineInfo info = newInfo(); + scanProductObject( product, &info, nullptr); + return info; + } + } + } + + } + // neither filename exists, return empty info + return MachineInfo(); } long event_cnt = 0; -bool parseIdentTGT( QString path, MachineInfo * info, QHash & idmap ); // forward +bool parseIdentFile( QString path, MachineInfo * info, QHash & idmap ); // forward void backupSTRfiles( const QString strpath, const QString importPath, const QString backupPath, MachineInfo & info, QMap & STRmap ); // forward ResMedEDFInfo * fetchSTRandVerify( QString filename, QString serialNumber ); // forward @@ -294,8 +325,8 @@ int ResmedLoader::Open(const QString & dirpath) m_abort = false; MachineInfo info = newInfo(); - if ( ! parseIdentTGT(importPath, & info, idmap) ) { - qDebug() << "Failed to parse Identification.tgt"; + if ( ! parseIdentFile(importPath, & info, idmap) ) { + qDebug() << "Failed to parse Identification file"; return -1; } @@ -1625,29 +1656,79 @@ bool ResmedLoader::ProcessSTRfiles(Machine *mach, QMap & STRmap, /////////////////////////////////////////////////////////////////////////////////// // Parse Identification.tgt file (containing serial number and machine information) /////////////////////////////////////////////////////////////////////////////////// -QHash parseIdentLine( const QString line, MachineInfo * info); //forward - -bool parseIdentTGT( QString path, MachineInfo * info, QHash & idmap ) { +// QHash parseIdentLine( const QString line, MachineInfo * info); // forward +// void scanProductObject( QJsonObject product, MachineInfo *info, QHash *idmap); // forward +bool parseIdentFile( QString path, MachineInfo * info, QHash & idmap ) { QString filename = path + RMS9_STR_idfile + STR_ext_TGT; QFile f(filename); + QFile j(path + RMS9_STR_idfile + STR_ext_JSON); // Abort if this file is dodgy.. - if (!f.exists() || !f.open(QIODevice::ReadOnly)) { - return false; - } - qDebug() << "Parsing Identification File " << filename; -// emit updateMessage(QObject::tr("Parsing Identification File")); -// QApplication::processEvents(); + if (f.exists() ) { + if ( !f.open(QIODevice::ReadOnly)) { + return false; + } + qDebug() << "Parsing Identification File " << filename; + // emit updateMessage(QObject::tr("Parsing Identification File")); + // QApplication::processEvents(); - // Parse # entries into idmap. - while (!f.atEnd()) { - QString line = f.readLine().trimmed(); - QHash hash = parseIdentLine( line, info ); - idmap.unite(hash); - } + // Parse # entries into idmap. + while (!f.atEnd()) { + QString line = f.readLine().trimmed(); + QHash hash = parseIdentLine( line, info ); + idmap.unite(hash); + } - f.close(); - return true; + f.close(); + return true; + } + if (j.exists() ) { + if ( !j.open(QIODevice::ReadOnly)) { + return false; + } + QByteArray identData = j.readAll(); + j.close(); + QJsonDocument identDoc(QJsonDocument::fromJson(identData)); + QJsonObject identObj(identDoc.object()); + if ( identObj.contains("FlowGenerator") && identObj["FlowGenerator"].isObject()) { + QJsonObject flow = identObj["FlowGenerator"].toObject(); + if ( flow.contains("IdentificationProfiles") && flow["IdentificationProfiles"].isObject()) { + QJsonObject profiles = flow["IdentificationProfiles"].toObject(); + if ( profiles.contains("Product") && profiles["Product"].isObject()) { + QJsonObject product = profiles["Product"].toObject(); +// MachineInfo info = newInfo(); + scanProductObject( product, info, &idmap); + return true; + } + } + } + + } + return false; +} + +void scanProductObject( QJsonObject product, MachineInfo *info, QHash *idmap) { + QHash hash1, hash2, hash3; + if (product.contains("SerialNumber")) { + info->serial = product["SerialNumber"].toString(); + hash1["SerialNumber"] = product["SerialNumber"].toString(); + if (idmap) + idmap->unite(hash1); + } + if (product.contains("ProductCode")) { + info->modelnumber = product["ProductCode"].toString(); + hash2["ProductCode"] = info->modelnumber; + if (idmap) + idmap->unite(hash2); + } + if (product.contains("ProductName")) { + info->model = product["ProductName"].toString(); + hash3["ProductName"] = info->model; + if (idmap) + idmap->unite(hash3); + int idx = info->model.indexOf("11"); + info->series = info->model.left(idx+2); + } } void backupSTRfiles( const QString strpath, const QString importPath, const QString backupPath, From 83f80f44d2f0b146e4e23efad1a89212e8326ad1 Mon Sep 17 00:00:00 2001 From: Phil Olynyk Date: Mon, 7 Jun 2021 13:58:04 -0400 Subject: [PATCH 02/10] Backup either tgt or json Ident file --- .../SleepLib/loader_plugins/resmed_loader.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/resmed_loader.cpp b/oscar/SleepLib/loader_plugins/resmed_loader.cpp index f67bcbf5..6fbf8345 100644 --- a/oscar/SleepLib/loader_plugins/resmed_loader.cpp +++ b/oscar/SleepLib/loader_plugins/resmed_loader.cpp @@ -451,16 +451,27 @@ int ResmedLoader::Open(const QString & dirpath) // Copy Identification files to backup folder - QFile backupFile(backup_path + RMS9_STR_idfile + STR_ext_TGT); + QString idfile_ext; + if (QFile(importPath+RMS9_STR_idfile+STR_ext_TGT).exists()) { + idfile_ext = STR_ext_TGT; + } + else if (QFile(importPath+RMS9_STR_idfile+STR_ext_JSON).exists()) { + idfile_ext = STR_ext_JSON; + } + else { + idfile_ext = ""; // should never happen... + } + + QFile backupFile(backup_path + RMS9_STR_idfile + idfile_ext); if (backupFile.exists()) backupFile.remove(); - if (!QFile::copy(importPath + RMS9_STR_idfile + STR_ext_TGT, backup_path + RMS9_STR_idfile + STR_ext_TGT)) - qWarning() << "Could not copy" << importPath + RMS9_STR_idfile + STR_ext_TGT << "to backup" << backupFile.fileName(); + if ( ! QFile::copy(importPath + RMS9_STR_idfile + idfile_ext, backup_path + RMS9_STR_idfile + idfile_ext)) + qWarning() << "Could not copy" << importPath + RMS9_STR_idfile + idfile_ext << "to backup" << backupFile.fileName(); backupFile.setFileName(backup_path + RMS9_STR_idfile + STR_ext_CRC); if (backupFile.exists()) backupFile.remove(); - if (!QFile::copy(importPath + RMS9_STR_idfile + STR_ext_CRC, backup_path + RMS9_STR_idfile + STR_ext_CRC)) + if ( ! QFile::copy(importPath + RMS9_STR_idfile + STR_ext_CRC, backup_path + RMS9_STR_idfile + STR_ext_CRC)) qWarning() << "Could not copy" << importPath + RMS9_STR_idfile + STR_ext_CRC << "to backup" << backup_path; } From bd21b1cda5474dd5e6fe6fd7ef657b51dc42a2ce Mon Sep 17 00:00:00 2001 From: Guy Scharf Date: Thu, 8 Jul 2021 10:23:57 -0700 Subject: [PATCH 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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); }